[{"data":1,"prerenderedAt":1248},["ShallowReactive",2],{"content-/exterior-angles-vue-migration":3,"all-pages-for-dir":1246,"og-image-/exterior-angles-vue-migration":1247},{"id":4,"title":5,"body":6,"category":1224,"description":1225,"extension":1226,"meta":1227,"navigation":214,"path":1228,"project_name":1229,"published":1230,"publishedAt":1231,"seo":1232,"stem":1233,"tags":1234,"todo":1243,"updatedAt":1244,"__hash__":1245},"pages/2026-04/2026-04-24/exterior-angles-vue-migration.md","多角形の外角の和ページを React/JSX から Vue へ移植してアニメーションを4段階で改良した記録",{"type":7,"value":8,"toc":1210},"minimark",[9,13,22,29,32,37,40,85,88,351,354,356,360,367,562,577,579,583,586,591,602,605,609,624,683,686,690,693,699,843,846,850,857,874,880,882,886,889,1061,1070,1076,1078,1082,1089,1127,1130,1132,1136,1173,1175,1179,1206],[10,11,5],"h1",{"id":12},"多角形の外角の和ページを-reactjsx-から-vue-へ移植してアニメーションを4段階で改良した記録",[14,15,16,17,21],"p",{},"別プロジェクトで作っていた ",[18,19,20],"code",{},"exterior-angles.jsx"," という教材コンポーネント（多角形の外角を1点に集めて、和が 360° になる様子をアニメーションで見せるやつ）を、mdx-playground の Nuxt 環境に持ってきて Vue 3 の Composition API に書き換えた。アニメーションの設計を4回ひっくり返した結果、最終的にスライダーで手動操作する形に落ち着いた、というのが今日の話。",[14,23,24,25,28],{},"新規ページ ",[18,26,27],{},"/exterior-angles"," を切り、トップページの「学習・クイズ」セクションからリンクを張った。最後に Simplify レビューを3エージェント並列で走らせて、不要な手書きパスや使われていない CSS を剥がした。",[30,31],"hr",{},[33,34,36],"h2",{"id":35},"_0-移植の方針","0. 移植の方針",[14,38,39],{},"React/JSX 版はフックの中に計算ロジックと描画と副作用が同居していた。Vue 化するついでに、以下の方針で書き直した。",[41,42,43,65,75,82],"ul",{},[44,45,46,47,50,51,50,54,50,57,60,61,64],"li",{},"純粋関数（",[18,48,49],{},"computeVertices",", ",[18,52,53],{},"computeEdgeDirs",[18,55,56],{},"computeSectors",[18,58,59],{},"formatAngle"," 等）は ",[18,62,63],{},"\u003Cscript setup>"," の外、moduleレベルに置く",[44,66,67,70,71,74],{},[18,68,69],{},"ref"," / ",[18,72,73],{},"computed"," は値を保持するだけ",[44,76,77,78,81],{},"副作用（RAF を回すアニメーション、DOM測定）は ",[18,79,80],{},"watch"," の中に隔離する",[44,83,84],{},"props で受ける値（頂点数 n、半径 R 等）はリアクティブに反応するが、計算自体は引数だけで完結するようにする",[14,86,87],{},"要は CLAUDE.md にも書いてある「ロジックは純粋関数、副作用は薄いシェル」のルールをそのまま当てはめた形。",[89,90,95],"pre",{"className":91,"code":92,"language":93,"meta":94,"style":94},"language-ts shiki shiki-themes vitesse-light vitesse-light","// moduleレベル — 純粋関数。テストもしやすい\nconst computeSectors = (\n  vertices: Point[],\n  edgeDirs: EdgeDir[],\n  progress: number,\n): Sector[] => {\n  // 頂点ごとに「外角の弧」をどこに描くかを返す\n  // progress は 0〜1 で、0=各頂点に分散、1=中心に集約\n  // ...\n}\n\n// \u003Cscript setup> 内 — refで値を持ち、computedで派生\nconst polygonN = ref(5)\nconst progress = ref(0)\nconst vertices = computed(() => computeVertices(polygonN.value, R))\nconst sectors = computed(() => computeSectors(vertices.value, edgeDirs.value, progress.value))\n","ts","",[18,96,97,106,123,140,153,167,185,191,197,203,209,216,222,245,264,304],{"__ignoreMap":94},[98,99,102],"span",{"class":100,"line":101},"line",1,[98,103,105],{"class":104},"sxvE3","// moduleレベル — 純粋関数。テストもしやすい\n",[98,107,109,113,116,120],{"class":100,"line":108},2,[98,110,112],{"class":111},"stQ0i","const ",[98,114,56],{"class":115},"senZ8",[98,117,119],{"class":118},"shFtX"," =",[98,121,122],{"class":118}," (\n",[98,124,126,130,133,137],{"class":100,"line":125},3,[98,127,129],{"class":128},"s4oTP","  vertices",[98,131,132],{"class":118},": ",[98,134,136],{"class":135},"sSkh3","Point",[98,138,139],{"class":118},"[],\n",[98,141,143,146,148,151],{"class":100,"line":142},4,[98,144,145],{"class":128},"  edgeDirs",[98,147,132],{"class":118},[98,149,150],{"class":135},"EdgeDir",[98,152,139],{"class":118},[98,154,156,159,161,164],{"class":100,"line":155},5,[98,157,158],{"class":128},"  progress",[98,160,132],{"class":118},[98,162,163],{"class":135},"number",[98,165,166],{"class":118},",\n",[98,168,170,173,176,179,182],{"class":100,"line":169},6,[98,171,172],{"class":118},"):",[98,174,175],{"class":135}," Sector",[98,177,178],{"class":118},"[]",[98,180,181],{"class":118}," =>",[98,183,184],{"class":118}," {\n",[98,186,188],{"class":100,"line":187},7,[98,189,190],{"class":104},"  // 頂点ごとに「外角の弧」をどこに描くかを返す\n",[98,192,194],{"class":100,"line":193},8,[98,195,196],{"class":104},"  // progress は 0〜1 で、0=各頂点に分散、1=中心に集約\n",[98,198,200],{"class":100,"line":199},9,[98,201,202],{"class":104},"  // ...\n",[98,204,206],{"class":100,"line":205},10,[98,207,208],{"class":118},"}\n",[98,210,212],{"class":100,"line":211},11,[98,213,215],{"emptyLinePlaceholder":214},true,"\n",[98,217,219],{"class":100,"line":218},12,[98,220,221],{"class":104},"// \u003Cscript setup> 内 — refで値を持ち、computedで派生\n",[98,223,225,227,230,232,235,238,242],{"class":100,"line":224},13,[98,226,112],{"class":111},[98,228,229],{"class":128},"polygonN",[98,231,119],{"class":118},[98,233,234],{"class":115}," ref",[98,236,237],{"class":118},"(",[98,239,241],{"class":240},"sM54T","5",[98,243,244],{"class":118},")\n",[98,246,248,250,253,255,257,259,262],{"class":100,"line":247},14,[98,249,112],{"class":111},[98,251,252],{"class":128},"progress",[98,254,119],{"class":118},[98,256,234],{"class":115},[98,258,237],{"class":118},[98,260,261],{"class":240},"0",[98,263,244],{"class":118},[98,265,267,269,272,274,277,280,282,285,287,289,292,295,298,301],{"class":100,"line":266},15,[98,268,112],{"class":111},[98,270,271],{"class":128},"vertices",[98,273,119],{"class":118},[98,275,276],{"class":115}," computed",[98,278,279],{"class":118},"(()",[98,281,181],{"class":118},[98,283,284],{"class":115}," computeVertices",[98,286,237],{"class":118},[98,288,229],{"class":128},[98,290,291],{"class":118},".",[98,293,294],{"class":128},"value",[98,296,297],{"class":118},",",[98,299,300],{"class":128}," R",[98,302,303],{"class":118},"))\n",[98,305,307,309,312,314,316,318,320,323,325,327,329,331,333,336,338,340,342,345,347,349],{"class":100,"line":306},16,[98,308,112],{"class":111},[98,310,311],{"class":128},"sectors",[98,313,119],{"class":118},[98,315,276],{"class":115},[98,317,279],{"class":118},[98,319,181],{"class":118},[98,321,322],{"class":115}," computeSectors",[98,324,237],{"class":118},[98,326,271],{"class":128},[98,328,291],{"class":118},[98,330,294],{"class":128},[98,332,297],{"class":118},[98,334,335],{"class":128}," edgeDirs",[98,337,291],{"class":118},[98,339,294],{"class":128},[98,341,297],{"class":118},[98,343,344],{"class":128}," progress",[98,346,291],{"class":118},[98,348,294],{"class":128},[98,350,303],{"class":118},[14,352,353],{},"最初から純粋関数として切り出しておくと、後でアニメーション設計を作り直すときに描画ロジックだけ差し替えれば済むので、結果的にこの方針が4回の作り直しを支えてくれた。",[30,355],{},[33,357,359],{"id":358},"_1-数値表示のバグ-7200-の末尾ゼロ","1. 数値表示のバグ — 72.00° の末尾ゼロ",[14,361,362,363,366],{},"書き換えた直後、五角形（n=5）の外角を表示したら ",[18,364,365],{},"72.00°"," と出てきた。割り切れる値に末尾ゼロが付くのは見栄えが悪い。",[89,368,370],{"className":91,"code":369,"language":93,"meta":94,"style":94},"// Before — 常に小数2桁\nconst formatAngle = (deg: number) => `${deg.toFixed(2)}°`\n\n// After — 整数なら整数、そうでなければ小数2桁\nconst formatAngle = (deg: number) => {\n  const rounded = Math.round(deg * 100) / 100\n  return Number.isInteger(rounded) ? `${rounded}°` : `${rounded.toFixed(2)}°`\n}\n",[18,371,372,377,432,436,441,463,498,558],{"__ignoreMap":94},[98,373,374],{"class":100,"line":101},[98,375,376],{"class":104},"// Before — 常に小数2桁\n",[98,378,379,381,383,385,388,391,393,395,398,400,404,408,411,413,416,418,421,423,426,429],{"class":100,"line":108},[98,380,112],{"class":111},[98,382,59],{"class":115},[98,384,119],{"class":118},[98,386,387],{"class":118}," (",[98,389,390],{"class":128},"deg",[98,392,132],{"class":118},[98,394,163],{"class":135},[98,396,397],{"class":118},")",[98,399,181],{"class":118},[98,401,403],{"class":402},"sMJiu"," `",[98,405,407],{"class":406},"sHkkW","${",[98,409,390],{"class":410},"sdGka",[98,412,291],{"class":118},[98,414,415],{"class":115},"toFixed",[98,417,237],{"class":118},[98,419,420],{"class":240},"2",[98,422,397],{"class":118},[98,424,425],{"class":406},"}",[98,427,428],{"class":410},"°",[98,430,431],{"class":402},"`\n",[98,433,434],{"class":100,"line":125},[98,435,215],{"emptyLinePlaceholder":214},[98,437,438],{"class":100,"line":142},[98,439,440],{"class":104},"// After — 整数なら整数、そうでなければ小数2桁\n",[98,442,443,445,447,449,451,453,455,457,459,461],{"class":100,"line":155},[98,444,112],{"class":111},[98,446,59],{"class":115},[98,448,119],{"class":118},[98,450,387],{"class":118},[98,452,390],{"class":128},[98,454,132],{"class":118},[98,456,163],{"class":135},[98,458,397],{"class":118},[98,460,181],{"class":118},[98,462,184],{"class":118},[98,464,465,468,471,473,476,478,481,483,485,488,491,493,495],{"class":100,"line":169},[98,466,467],{"class":111},"  const ",[98,469,470],{"class":128},"rounded",[98,472,119],{"class":118},[98,474,475],{"class":128}," Math",[98,477,291],{"class":118},[98,479,480],{"class":115},"round",[98,482,237],{"class":118},[98,484,390],{"class":128},[98,486,487],{"class":111}," * ",[98,489,490],{"class":240},"100",[98,492,397],{"class":118},[98,494,70],{"class":111},[98,496,497],{"class":240},"100\n",[98,499,500,503,506,508,511,513,515,517,520,523,525,527,529,531,533,536,538,540,542,544,546,548,550,552,554,556],{"class":100,"line":187},[98,501,502],{"class":406},"  return",[98,504,505],{"class":128}," Number",[98,507,291],{"class":118},[98,509,510],{"class":115},"isInteger",[98,512,237],{"class":118},[98,514,470],{"class":128},[98,516,397],{"class":118},[98,518,519],{"class":111}," ? ",[98,521,522],{"class":402},"`",[98,524,407],{"class":406},[98,526,470],{"class":410},[98,528,425],{"class":406},[98,530,428],{"class":410},[98,532,522],{"class":402},[98,534,535],{"class":111}," : ",[98,537,522],{"class":402},[98,539,407],{"class":406},[98,541,470],{"class":410},[98,543,291],{"class":118},[98,545,415],{"class":115},[98,547,237],{"class":118},[98,549,420],{"class":240},[98,551,397],{"class":118},[98,553,425],{"class":406},[98,555,428],{"class":410},[98,557,431],{"class":402},[98,559,560],{"class":100,"line":193},[98,561,208],{"class":118},[14,563,564,565,568,569,572,573,576],{},"n=5 だと ",[18,566,567],{},"72°","、n=7 だと ",[18,570,571],{},"51.43°"," のように、必要なときだけ小数を出すようにした。教材なので「割り切れる」という事実そのものが情報になる。",[18,574,575],{},"72.00"," だと、生徒に「ちゃんと割り切れているのか」と聞かれそうな顔をしている。",[30,578],{},[33,580,582],{"id":581},"_2-アニメーション設計の4段階の変遷","2. アニメーション設計の4段階の変遷",[14,584,585],{},"ここが今日いちばん時間を吸われたところ。「外角を1点に集めると360°になる」というメッセージを、どう動かして見せるか、という設計を4回作り直した。",[587,588,590],"h3",{"id":589},"段階1-各頂点を中心に向かって移動させて外角を集約する","段階1: 各頂点を中心に向かって移動させて外角を集約する",[14,592,593,594,597,598,601],{},"最初に書いたのは「五角形の各頂点を、図形の中心点に向かって引き寄せる」アニメーション。",[18,595,596],{},"progress: 0"," で五角形が普通に表示されていて、",[18,599,600],{},"progress: 1"," で5つの頂点が中心の1点に重なる。外角の弧（セクター）も頂点と一緒に動かしたので、最終的に5つの弧が中心で円を描く。",[14,603,604],{},"実装してブラウザで再生したら、途中で図形がぐにゃっと潰れて、もはや五角形ではない歪な多角形になった。頂点をそれぞれ独立に中心へ引っ張ると、辺の長さが個別に縮んで形が崩れる。「途中で図形の輪郭が消える」と言ったほうが正確かもしれない。教材としては最悪で、何が起きているのか伝わらない。",[587,606,608],{"id":607},"段階2-図形を相似縮小しながら頂点を中心に集める","段階2: 図形を相似縮小しながら頂点を中心に集める",[14,610,611,612,615,616,619,620,623],{},"ユーザーから「図形の形を保ったまま、相似形で縮小しながら頂点を中心に集めてほしい」と指摘を受けた。なるほど、と思って ",[18,613,614],{},"polygonPoints"," を ",[18,617,618],{},"animatedVertices"," に差し替えて、半径 R を ",[18,621,622],{},"R * (1 - progress)"," で縮めるようにした。",[89,625,627],{"className":91,"code":626,"language":93,"meta":94,"style":94},"const animatedVertices = computed(() =>\n  computeVertices(polygonN.value, R * (1 - progress.value))\n)\n",[18,628,629,644,679],{"__ignoreMap":94},[98,630,631,633,635,637,639,641],{"class":100,"line":101},[98,632,112],{"class":111},[98,634,618],{"class":128},[98,636,119],{"class":118},[98,638,276],{"class":115},[98,640,279],{"class":118},[98,642,643],{"class":118}," =>\n",[98,645,646,649,651,653,655,657,659,661,663,665,668,671,673,675,677],{"class":100,"line":108},[98,647,648],{"class":115},"  computeVertices",[98,650,237],{"class":118},[98,652,229],{"class":128},[98,654,291],{"class":118},[98,656,294],{"class":128},[98,658,297],{"class":118},[98,660,300],{"class":128},[98,662,487],{"class":111},[98,664,237],{"class":118},[98,666,667],{"class":240},"1",[98,669,670],{"class":111}," - ",[98,672,252],{"class":128},[98,674,291],{"class":118},[98,676,294],{"class":128},[98,678,303],{"class":118},[98,680,681],{"class":100,"line":125},[98,682,244],{"class":118},[14,684,685],{},"これで五角形は形を保ったまま縮んで、最後は中心の1点に潰れる。頂点とセクターは一緒に中心に集まるので、見た目もスッキリした。よし、と思って次に進んだ。",[587,687,689],{"id":688},"段階3-セクターが回転してしまう問題","段階3: セクターが回転してしまう問題",[14,691,692],{},"ところが今度は「セクターが途中で回転している」とユーザーから指摘が入った。確認すると、外角の弧（円弧の開始角・終了角）を頂点位置と一緒に補間していたせいで、頂点が中心に近づくにつれて角度がぐるぐる回っていた。外角の「向き」自体は頂点の位置に依存しないはずなのに、補間ロジックの都合で角度まで動いていた。",[14,694,695,696,698],{},"修正としては、角度補間を全部やめて、セクターは平行移動だけさせるようにした。各セクターの向き（開始角・終了角）は最初に計算した値を固定で保持し、中心点 (cx, cy) だけを ",[18,697,252],{}," で動かす。",[89,700,702],{"className":91,"code":701,"language":93,"meta":94,"style":94},"// セクターの向きは固定。中心点だけを progress で動かす\nconst animatedSectors = computed(() =>\n  staticSectors.value.map((s) => ({\n    ...s,\n    cx: lerp(s.cx, CX, progress.value),\n    cy: lerp(s.cy, CY, progress.value),\n    // startAngle, endAngle は触らない\n  }))\n)\n",[18,703,704,709,724,751,760,796,829,834,839],{"__ignoreMap":94},[98,705,706],{"class":100,"line":101},[98,707,708],{"class":104},"// セクターの向きは固定。中心点だけを progress で動かす\n",[98,710,711,713,716,718,720,722],{"class":100,"line":108},[98,712,112],{"class":111},[98,714,715],{"class":128},"animatedSectors",[98,717,119],{"class":118},[98,719,276],{"class":115},[98,721,279],{"class":118},[98,723,643],{"class":118},[98,725,726,729,731,733,735,738,741,744,746,748],{"class":100,"line":125},[98,727,728],{"class":128},"  staticSectors",[98,730,291],{"class":118},[98,732,294],{"class":128},[98,734,291],{"class":118},[98,736,737],{"class":115},"map",[98,739,740],{"class":118},"((",[98,742,743],{"class":128},"s",[98,745,397],{"class":118},[98,747,181],{"class":118},[98,749,750],{"class":118}," ({\n",[98,752,753,756,758],{"class":100,"line":142},[98,754,755],{"class":118},"    ...",[98,757,743],{"class":128},[98,759,166],{"class":118},[98,761,762,766,768,771,773,775,777,780,782,785,787,789,791,793],{"class":100,"line":155},[98,763,765],{"class":764},"sz8Xr","    cx",[98,767,132],{"class":118},[98,769,770],{"class":115},"lerp",[98,772,237],{"class":118},[98,774,743],{"class":128},[98,776,291],{"class":118},[98,778,779],{"class":128},"cx",[98,781,50],{"class":118},[98,783,784],{"class":128},"CX",[98,786,50],{"class":118},[98,788,252],{"class":128},[98,790,291],{"class":118},[98,792,294],{"class":128},[98,794,795],{"class":118},"),\n",[98,797,798,801,803,805,807,809,811,814,816,819,821,823,825,827],{"class":100,"line":169},[98,799,800],{"class":764},"    cy",[98,802,132],{"class":118},[98,804,770],{"class":115},[98,806,237],{"class":118},[98,808,743],{"class":128},[98,810,291],{"class":118},[98,812,813],{"class":128},"cy",[98,815,50],{"class":118},[98,817,818],{"class":128},"CY",[98,820,50],{"class":118},[98,822,252],{"class":128},[98,824,291],{"class":118},[98,826,294],{"class":128},[98,828,795],{"class":118},[98,830,831],{"class":100,"line":187},[98,832,833],{"class":104},"    // startAngle, endAngle は触らない\n",[98,835,836],{"class":100,"line":193},[98,837,838],{"class":118},"  }))\n",[98,840,841],{"class":100,"line":199},[98,842,244],{"class":118},[14,844,845],{},"これで弧の向きが保たれたまま、5つの弧が中心の1点に集まる動きになった。集まった瞬間、5つの弧がぴったり繋がって360°の円を描く。やっと意図した見え方になった。",[587,847,849],{"id":848},"段階4-スライダーに一本化","段階4: スライダーに一本化",[14,851,852,853,856],{},"ここまで来てもう一つ問題があって、RAFで自動再生していると「途中の状態」を観察しづらい。",[18,854,855],{},"progress: 0.3"," あたりで止めてじっくり見たい、という需要に応えにくい。再生・一時停止ボタンも考えたが、教材として一番使いやすいのは「スライダーで自分で動かす」だった。",[14,858,859,860,863,864,866,867,870,871,873],{},"自動再生をバッサリ削って、",[18,861,862],{},"\u003Cinput type=\"range\" min=\"0\" max=\"100\" v-model.number=\"progressPercent\" />"," 1本に置き換えた。",[18,865,252],{}," は ",[18,868,869],{},"progressPercent / 100"," で computed する。RAF も ",[18,872,80],{}," も全部消した。コード量が一気に減った。",[14,875,876,877,879],{},"副作用が ",[18,878,80],{}," 内に閉じ込められていたので、削除も追加もこの場所の編集だけで済んだ。純粋関数を最初に切り出しておいたのが効いた瞬間だった。",[30,881],{},[33,883,885],{"id":884},"_3-4図形のグリッド表示に拡張","3. 4図形のグリッド表示に拡張",[14,887,888],{},"ここまでで n=5 の単一表示が完成したので、「他の頂点数でも同時に見たい」という拡張に進んだ。n=4（正方形）、n=6（六角形）、n=8（八角形）、n=10（正十角形）の4つを 2×2 グリッドで並べて、共通スライダー1本で全部を同期させる。",[89,890,894],{"className":891,"code":892,"language":893,"meta":94,"style":94},"language-vue shiki shiki-themes vitesse-light vitesse-light","\u003Ctemplate>\n  \u003Cdiv class=\"grid grid-cols-2 gap-4\">\n    \u003CPolygonCanvas v-for=\"n in [4, 6, 8, 10]\" :key=\"n\" :n=\"n\" :progress=\"progress\" />\n  \u003C/div>\n  \u003Cinput type=\"range\" min=\"0\" max=\"100\" v-model.number=\"progressPercent\" />\n\u003C/template>\n","vue",[18,895,896,907,931,988,997,1052],{"__ignoreMap":94},[98,897,898,901,904],{"class":100,"line":101},[98,899,900],{"class":118},"\u003C",[98,902,903],{"class":406},"template",[98,905,906],{"class":118},">\n",[98,908,909,912,915,918,921,924,927,929],{"class":100,"line":108},[98,910,911],{"class":118},"  \u003C",[98,913,914],{"class":406},"div",[98,916,917],{"class":128}," class",[98,919,920],{"class":118},"=",[98,922,923],{"class":402},"\"",[98,925,926],{"class":410},"grid grid-cols-2 gap-4",[98,928,923],{"class":402},[98,930,906],{"class":118},[98,932,933,936,939,942,944,946,949,951,954,956,958,961,963,966,968,970,972,974,977,979,981,983,985],{"class":100,"line":125},[98,934,935],{"class":118},"    \u003C",[98,937,938],{"class":406},"PolygonCanvas",[98,940,941],{"class":128}," v-for",[98,943,920],{"class":118},[98,945,923],{"class":402},[98,947,948],{"class":410},"n in [4, 6, 8, 10]",[98,950,923],{"class":402},[98,952,953],{"class":128}," :key",[98,955,920],{"class":118},[98,957,923],{"class":402},[98,959,960],{"class":410},"n",[98,962,923],{"class":402},[98,964,965],{"class":128}," :n",[98,967,920],{"class":118},[98,969,923],{"class":402},[98,971,960],{"class":410},[98,973,923],{"class":402},[98,975,976],{"class":128}," :progress",[98,978,920],{"class":118},[98,980,923],{"class":402},[98,982,252],{"class":410},[98,984,923],{"class":402},[98,986,987],{"class":118}," />\n",[98,989,990,993,995],{"class":100,"line":142},[98,991,992],{"class":118},"  \u003C/",[98,994,914],{"class":406},[98,996,906],{"class":118},[98,998,999,1001,1004,1007,1009,1011,1014,1016,1019,1021,1023,1025,1027,1030,1032,1034,1036,1038,1041,1043,1045,1048,1050],{"class":100,"line":155},[98,1000,911],{"class":118},[98,1002,1003],{"class":406},"input",[98,1005,1006],{"class":128}," type",[98,1008,920],{"class":118},[98,1010,923],{"class":402},[98,1012,1013],{"class":410},"range",[98,1015,923],{"class":402},[98,1017,1018],{"class":128}," min",[98,1020,920],{"class":118},[98,1022,923],{"class":402},[98,1024,261],{"class":410},[98,1026,923],{"class":402},[98,1028,1029],{"class":128}," max",[98,1031,920],{"class":118},[98,1033,923],{"class":402},[98,1035,490],{"class":410},[98,1037,923],{"class":402},[98,1039,1040],{"class":128}," v-model.number",[98,1042,920],{"class":118},[98,1044,923],{"class":402},[98,1046,1047],{"class":410},"progressPercent",[98,1049,923],{"class":402},[98,1051,987],{"class":118},[98,1053,1054,1057,1059],{"class":100,"line":169},[98,1055,1056],{"class":118},"\u003C/",[98,1058,903],{"class":406},[98,1060,906],{"class":118},[14,1062,1063,1065,1066,1069],{},[18,1064,938],{}," は props だけで動く純粋なコンポーネントになっているので、グリッド化は ",[18,1067,1068],{},"v-for"," を1行書くだけで済んだ。スライダーを動かすと4つの多角形が同期して縮み、4つとも中心で360°の円を描く。「頂点の数に関係なく外角の和は360°」というメッセージが、4つの図形が同じ瞬間に同じ円を完成させる絵で伝わる。",[14,1071,1072,1073,1075],{},"ついでにパンくず（Home › 多角形の外角の和）を追加して、トップページの「学習・クイズ」セクションに ",[18,1074,27],{}," へのリンクを差し込んだ。",[30,1077],{},[33,1079,1081],{"id":1080},"_4-simplify-レビュー3エージェント並列","4. Simplify レビュー（3エージェント並列）",[14,1083,1084,1085,1088],{},"仕上げに ",[18,1086,1087],{},"simplify"," スキルを3エージェント並列で走らせた。指摘されたのは以下。",[41,1090,1091,1102,1116],{},[44,1092,1093,1094,1097,1098,1101],{},"スライダーの input ハンドラが ",[18,1095,1096],{},"@input"," で値を取り出して別の ref に詰め直していたが、",[18,1099,1100],{},"v-model.number"," 1行で済む。書き換えた",[44,1103,1104,1105,1107,1108,1111,1112,1115],{},"セクターの SVG パスを毎フレーム手で組み立てていたが、",[18,1106,56],{}," 側で ",[18,1109,1110],{},"d"," を返すようにすれば、テンプレートは ",[18,1113,1114],{},":d=\"sec.d\""," を渡すだけになる。差し替えた",[44,1117,1118,1119,1122,1123,1126],{},"自動再生時代の名残で ",[18,1120,1121],{},".is-playing"," ",[18,1124,1125],{},".paused"," といった CSS クラスが残っていた。削除した",[14,1128,1129],{},"3エージェント並列でレビューを回すと、お互いに違う観点を出してくる（v-modelの簡素化、データ構造の責務移動、不要CSSの掃除）ので、1人より見落としが減る。指摘の重複もあったが、3つとも同じ箇所を指摘してきたところは「本当に直すべき」というシグナルになって判断が楽だった。",[30,1131],{},[33,1133,1135],{"id":1134},"_5-学び","5. 学び",[41,1137,1138,1151,1157,1167],{},[44,1139,1140,1144,1145,1147,1148,1150],{},[1141,1142,1143],"strong",{},"純粋関数を最初に切り出すと、後の作り直しが軽い","。今日アニメーション設計を4回作り直したが、",[18,1146,56],{}," の引数を増やしただけで済んだ回が多かった。",[18,1149,80],{}," の中身を全削除した最後の段階でも、純粋関数側は無傷だった",[44,1152,1153,1156],{},[1141,1154,1155],{},"教材アニメーションは「自動再生」より「スライダーで手動」のほうが伝わる","ことが多い。0.3秒の中に意味が詰まっている瞬間は、止めて見せないと頭に入らない",[44,1158,1159,1162,1163,1166],{},[1141,1160,1161],{},"形を保ったまま縮める","のと、",[1141,1164,1165],{},"頂点を独立に動かす","のでは、視覚的なメッセージが全く違う。同じ「中心に集まる」動きでも、相似縮小なら「形が保たれている」というメタメッセージが乗る",[44,1168,1169,1172],{},[1141,1170,1171],{},"角度の補間は罠","。位置を補間するときに、向きを表すパラメータまで巻き込んで動かしてしまうと、意図しない回転が生まれる。並進と回転を分けて考える",[30,1174],{},[33,1176,1178],{"id":1177},"_6-明日以降にやりたいこと","6. 明日以降にやりたいこと",[41,1180,1183,1191,1200],{"className":1181},[1182],"contains-task-list",[44,1184,1187,1190],{"className":1185},[1186],"task-list-item",[1003,1188],{"disabled":214,"type":1189},"checkbox"," 内角の和（180°×(n-2)）の教材ページも同じ枠組みで作る",[44,1192,1194,1196,1197,1199],{"className":1193},[1186],[1003,1195],{"disabled":214,"type":1189}," スライダーの値を URL クエリに反映して、特定の ",[18,1198,252],{}," をシェアできるようにする",[44,1201,1203,1205],{"className":1202},[1186],[1003,1204],{"disabled":214,"type":1189}," モバイルで4グリッド表示が窮屈なので、breakpoint 以下では1列に折り返す",[1207,1208,1209],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sSkh3, html code.shiki .sSkh3{--shiki-default:#2E8F82;--shiki-dark:#2E8F82}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}",{"title":94,"searchDepth":108,"depth":108,"links":1211},[1212,1213,1214,1220,1221,1222,1223],{"id":35,"depth":108,"text":36},{"id":358,"depth":108,"text":359},{"id":581,"depth":108,"text":582,"children":1215},[1216,1217,1218,1219],{"id":589,"depth":125,"text":590},{"id":607,"depth":125,"text":608},{"id":688,"depth":125,"text":689},{"id":848,"depth":125,"text":849},{"id":884,"depth":108,"text":885},{"id":1080,"depth":108,"text":1081},{"id":1134,"depth":108,"text":1135},{"id":1177,"depth":108,"text":1178},"dev","外角の和が360°になる教材ページをReact/JSXからVue 3 Composition APIへ移植。純粋関数をmoduleレベルに切り出し、副作用（RAFアニメーション）はwatch内に隔離。アニメーションが図形の形を歪める問題、セクターが回転する問題を経て、最終的にスライダー手動操作に一本化し、4図形（n=4,6,8,10）グリッド表示まで広げた1日の記録","md",{},"/exterior-angles-vue-migration","mdx-playground",false,"2026-04-24T00:00:00.000Z",{"title":5,"description":1225},"2026-04/2026-04-24/exterior-angles-vue-migration",[1235,1236,1237,1238,1239,1240,1241,1242],"Vue 3","Composition API","SVG","アニメーション","教材","幾何","純粋関数","リファクタリング","done",null,"TdyziiiVm5pq4NFV5kdpkgOfbOQ10SFSpDfE3mEg0Jc",[],"https://log.eurekapu.com/favicon.svg",1777329226786]