[{"data":1,"prerenderedAt":1287},["ShallowReactive",2],{"article-spirograph-art":3},{"id":4,"title":5,"body":6,"date":1279,"description":1280,"extension":1281,"meta":1282,"navigation":111,"path":1283,"seo":1284,"stem":1285,"__hash__":1286},"articles\u002Farticles\u002Fspirograph-art.md","Canvas API で描くスピログラフ",{"type":7,"value":8,"toc":1268},"minimark",[9,13,22,25,28,32,40,186,197,292,294,297,300,350,476,478,482,491,632,656,658,662,668,1005,1010,1014,1021,1045,1056,1102,1104,1108,1127,1192,1194,1197,1253,1264],[10,11,12],"h2",{"id":12},"作品",[14,15,16,17,21],"p",{},"内側の円が外側の円の内壁を転がるとき、円上の一点が描く軌跡が ",[18,19,20],"strong",{},"ハイポトロコイド（スピログラフ）"," です。\nクリック \u002F タップするたびに異なる比率のパターンに切り替わります。",[23,24],"spirograph",{},[26,27],"hr",{},[10,29,31],{"id":30},"canvas-api-とは","Canvas API とは",[14,33,34,35,39],{},"これまでの作品は CSS や SVG という「宣言的」な手法でした。Canvas はその反対で、JavaScript から ",[36,37,38],"code",{},"ctx.lineTo()"," などのメソッドを呼び出してピクセルを直接操作する「命令的」な描画 API です。",[41,42,47],"pre",{"className":43,"code":44,"language":45,"meta":46,"style":46},"language-ts shiki shiki-themes github-light github-dark","const canvas = document.querySelector('canvas')\nconst ctx = canvas.getContext('2d')\n\nctx.beginPath()\nctx.moveTo(100, 100)\nctx.lineTo(200, 200)\nctx.strokeStyle = '#00DC82'\nctx.stroke()\n","ts","",[36,48,49,83,106,113,125,145,164,176],{"__ignoreMap":46},[50,51,54,58,62,65,69,73,76,80],"span",{"class":52,"line":53},"line",1,[50,55,57],{"class":56},"szBVR","const",[50,59,61],{"class":60},"sj4cs"," canvas",[50,63,64],{"class":56}," =",[50,66,68],{"class":67},"sVt8B"," document.",[50,70,72],{"class":71},"sScJk","querySelector",[50,74,75],{"class":67},"(",[50,77,79],{"class":78},"sZZnC","'canvas'",[50,81,82],{"class":67},")\n",[50,84,86,88,91,93,96,99,101,104],{"class":52,"line":85},2,[50,87,57],{"class":56},[50,89,90],{"class":60}," ctx",[50,92,64],{"class":56},[50,94,95],{"class":67}," canvas.",[50,97,98],{"class":71},"getContext",[50,100,75],{"class":67},[50,102,103],{"class":78},"'2d'",[50,105,82],{"class":67},[50,107,109],{"class":52,"line":108},3,[50,110,112],{"emptyLinePlaceholder":111},true,"\n",[50,114,116,119,122],{"class":52,"line":115},4,[50,117,118],{"class":67},"ctx.",[50,120,121],{"class":71},"beginPath",[50,123,124],{"class":67},"()\n",[50,126,128,130,133,135,138,141,143],{"class":52,"line":127},5,[50,129,118],{"class":67},[50,131,132],{"class":71},"moveTo",[50,134,75],{"class":67},[50,136,137],{"class":60},"100",[50,139,140],{"class":67},", ",[50,142,137],{"class":60},[50,144,82],{"class":67},[50,146,148,150,153,155,158,160,162],{"class":52,"line":147},6,[50,149,118],{"class":67},[50,151,152],{"class":71},"lineTo",[50,154,75],{"class":67},[50,156,157],{"class":60},"200",[50,159,140],{"class":67},[50,161,157],{"class":60},[50,163,82],{"class":67},[50,165,167,170,173],{"class":52,"line":166},7,[50,168,169],{"class":67},"ctx.strokeStyle ",[50,171,172],{"class":56},"=",[50,174,175],{"class":78}," '#00DC82'\n",[50,177,179,181,184],{"class":52,"line":178},8,[50,180,118],{"class":67},[50,182,183],{"class":71},"stroke",[50,185,124],{"class":67},[14,187,188,189,192,193,196],{},"Vue では ",[36,190,191],{},"ref"," でキャンバス要素を取得し、",[36,194,195],{},"onMounted"," で描画を開始します。",[41,198,200],{"className":43,"code":199,"language":45,"meta":46,"style":46},"const canvasRef = ref\u003CHTMLCanvasElement | null>(null)\n\nonMounted(() => {\n  const ctx = canvasRef.value!.getContext('2d')!\n  \u002F\u002F ここで描画処理\n})\n",[36,201,202,234,238,251,281,287],{"__ignoreMap":46},[50,203,204,206,209,211,214,217,220,223,226,229,232],{"class":52,"line":53},[50,205,57],{"class":56},[50,207,208],{"class":60}," canvasRef",[50,210,64],{"class":56},[50,212,213],{"class":71}," ref",[50,215,216],{"class":67},"\u003C",[50,218,219],{"class":71},"HTMLCanvasElement",[50,221,222],{"class":56}," |",[50,224,225],{"class":60}," null",[50,227,228],{"class":67},">(",[50,230,231],{"class":60},"null",[50,233,82],{"class":67},[50,235,236],{"class":52,"line":85},[50,237,112],{"emptyLinePlaceholder":111},[50,239,240,242,245,248],{"class":52,"line":108},[50,241,195],{"class":71},[50,243,244],{"class":67},"(() ",[50,246,247],{"class":56},"=>",[50,249,250],{"class":67}," {\n",[50,252,253,256,258,260,263,266,269,271,273,275,278],{"class":52,"line":115},[50,254,255],{"class":56},"  const",[50,257,90],{"class":60},[50,259,64],{"class":56},[50,261,262],{"class":67}," canvasRef.value",[50,264,265],{"class":56},"!",[50,267,268],{"class":67},".",[50,270,98],{"class":71},[50,272,75],{"class":67},[50,274,103],{"class":78},[50,276,277],{"class":67},")",[50,279,280],{"class":56},"!\n",[50,282,283],{"class":52,"line":127},[50,284,286],{"class":285},"sJ8bj","  \u002F\u002F ここで描画処理\n",[50,288,289],{"class":52,"line":147},[50,290,291],{"class":67},"})\n",[26,293],{},[10,295,296],{"id":296},"スピログラフの数式",[14,298,299],{},"ハイポトロコイドは 3 つのパラメータで定義されます。",[301,302,303,316],"table",{},[304,305,306],"thead",{},[307,308,309,313],"tr",{},[310,311,312],"th",{},"変数",[310,314,315],{},"意味",[317,318,319,330,340],"tbody",{},[307,320,321,327],{},[322,323,324],"td",{},[36,325,326],{},"R",[322,328,329],{},"外側の固定円の半径",[307,331,332,337],{},[322,333,334],{},[36,335,336],{},"r",[322,338,339],{},"内側の転がる円の半径",[307,341,342,347],{},[322,343,344],{},[36,345,346],{},"d",[322,348,349],{},"描画点と内円中心の距離",[41,351,353],{"className":43,"code":352,"language":45,"meta":46,"style":46},"\u002F\u002F 媒介変数 t を 0 から増やしながら座標を計算\nconst x = (R - r) * Math.cos(t) + d * Math.cos(((R - r) \u002F r) * t)\nconst y = (R - r) * Math.sin(t) - d * Math.sin(((R - r) \u002F r) * t)\n",[36,354,355,360,423],{"__ignoreMap":46},[50,356,357],{"class":52,"line":53},[50,358,359],{"class":285},"\u002F\u002F 媒介変数 t を 0 から増やしながら座標を計算\n",[50,361,362,364,367,369,372,374,377,380,383,386,389,392,395,398,400,402,404,407,409,411,413,416,418,420],{"class":52,"line":85},[50,363,57],{"class":56},[50,365,366],{"class":60}," x",[50,368,64],{"class":56},[50,370,371],{"class":67}," (",[50,373,326],{"class":60},[50,375,376],{"class":56}," -",[50,378,379],{"class":67}," r) ",[50,381,382],{"class":56},"*",[50,384,385],{"class":67}," Math.",[50,387,388],{"class":71},"cos",[50,390,391],{"class":67},"(t) ",[50,393,394],{"class":56},"+",[50,396,397],{"class":67}," d ",[50,399,382],{"class":56},[50,401,385],{"class":67},[50,403,388],{"class":71},[50,405,406],{"class":67},"(((",[50,408,326],{"class":60},[50,410,376],{"class":56},[50,412,379],{"class":67},[50,414,415],{"class":56},"\u002F",[50,417,379],{"class":67},[50,419,382],{"class":56},[50,421,422],{"class":67}," t)\n",[50,424,425,427,430,432,434,436,438,440,442,444,447,449,452,454,456,458,460,462,464,466,468,470,472,474],{"class":52,"line":108},[50,426,57],{"class":56},[50,428,429],{"class":60}," y",[50,431,64],{"class":56},[50,433,371],{"class":67},[50,435,326],{"class":60},[50,437,376],{"class":56},[50,439,379],{"class":67},[50,441,382],{"class":56},[50,443,385],{"class":67},[50,445,446],{"class":71},"sin",[50,448,391],{"class":67},[50,450,451],{"class":56},"-",[50,453,397],{"class":67},[50,455,382],{"class":56},[50,457,385],{"class":67},[50,459,446],{"class":71},[50,461,406],{"class":67},[50,463,326],{"class":60},[50,465,376],{"class":56},[50,467,379],{"class":67},[50,469,415],{"class":56},[50,471,379],{"class":67},[50,473,382],{"class":56},[50,475,422],{"class":67},[26,477],{},[10,479,481],{"id":480},"_1サイクルの長さを最大公約数で求める","1サイクルの長さを最大公約数で求める",[14,483,484,485,487,488,490],{},"曲線が「閉じる」（元に戻る）のは、内円が外円の内壁をちょうど整数回転したときです。\nこれは ",[36,486,326],{}," と ",[36,489,336],{}," の最大公約数（GCD）から求められます。",[41,492,494],{"className":43,"code":493,"language":45,"meta":46,"style":46},"function gcd(a: number, b: number): number {\n  return b === 0 ? a : gcd(b, a % b)  \u002F\u002F ユークリッドの互除法\n}\n\nconst cycles = r \u002F gcd(R, r)           \u002F\u002F 内円の回転数\nconst totalAngle = 2 * Math.PI * cycles \u002F\u002F 描画する総角度\n",[36,495,496,533,569,574,578,604],{"__ignoreMap":46},[50,497,498,501,504,506,510,513,516,518,521,523,525,527,529,531],{"class":52,"line":53},[50,499,500],{"class":56},"function",[50,502,503],{"class":71}," gcd",[50,505,75],{"class":67},[50,507,509],{"class":508},"s4XuR","a",[50,511,512],{"class":56},":",[50,514,515],{"class":60}," number",[50,517,140],{"class":67},[50,519,520],{"class":508},"b",[50,522,512],{"class":56},[50,524,515],{"class":60},[50,526,277],{"class":67},[50,528,512],{"class":56},[50,530,515],{"class":60},[50,532,250],{"class":67},[50,534,535,538,541,544,547,550,553,555,557,560,563,566],{"class":52,"line":85},[50,536,537],{"class":56},"  return",[50,539,540],{"class":67}," b ",[50,542,543],{"class":56},"===",[50,545,546],{"class":60}," 0",[50,548,549],{"class":56}," ?",[50,551,552],{"class":67}," a ",[50,554,512],{"class":56},[50,556,503],{"class":71},[50,558,559],{"class":67},"(b, a ",[50,561,562],{"class":56},"%",[50,564,565],{"class":67}," b)  ",[50,567,568],{"class":285},"\u002F\u002F ユークリッドの互除法\n",[50,570,571],{"class":52,"line":108},[50,572,573],{"class":67},"}\n",[50,575,576],{"class":52,"line":115},[50,577,112],{"emptyLinePlaceholder":111},[50,579,580,582,585,587,590,592,594,596,598,601],{"class":52,"line":127},[50,581,57],{"class":56},[50,583,584],{"class":60}," cycles",[50,586,64],{"class":56},[50,588,589],{"class":67}," r ",[50,591,415],{"class":56},[50,593,503],{"class":71},[50,595,75],{"class":67},[50,597,326],{"class":60},[50,599,600],{"class":67},", r)           ",[50,602,603],{"class":285},"\u002F\u002F 内円の回転数\n",[50,605,606,608,611,613,616,619,621,624,626,629],{"class":52,"line":147},[50,607,57],{"class":56},[50,609,610],{"class":60}," totalAngle",[50,612,64],{"class":56},[50,614,615],{"class":60}," 2",[50,617,618],{"class":56}," *",[50,620,385],{"class":67},[50,622,623],{"class":60},"PI",[50,625,618],{"class":56},[50,627,628],{"class":67}," cycles ",[50,630,631],{"class":285},"\u002F\u002F 描画する総角度\n",[14,633,634,637,638,641,642,645,646,637,649,641,652,655],{},[36,635,636],{},"R:r = 120:45"," のとき ",[36,639,640],{},"gcd(120, 45) = 15","、",[36,643,644],{},"cycles = 3"," → 内円を 3 周して閉じます。\n",[36,647,648],{},"R:r = 120:25",[36,650,651],{},"gcd(120, 25) = 5",[36,653,654],{},"cycles = 5"," → 5 周。周数が多いほど複雑な形状になります。",[26,657],{},[10,659,661],{"id":660},"requestanimationframe-でアニメーション描画","requestAnimationFrame でアニメーション描画",[14,663,664,665,667],{},"全座標を事前計算しておき、毎フレーム少しずつ ",[36,666,152],{}," で線を伸ばしていきます。",[41,669,671],{"className":43,"code":670,"language":45,"meta":46,"style":46},"\u002F\u002F 全点を先に計算\nconst pts = Array.from({ length: totalPoints + 1 }, (_, i) => {\n  const t = (i \u002F totalPoints) * totalAngle\n  return [\n    cx + (R - r) * Math.cos(t) + d * Math.cos(((R - r) \u002F r) * t),\n    cy + (R - r) * Math.sin(t) - d * Math.sin(((R - r) \u002F r) * t),\n  ]\n})\n\nlet i = 1\nfunction frame() {\n  ctx.beginPath()\n  ctx.moveTo(pts[i - 1][0], pts[i - 1][1])\n  ctx.lineTo(pts[i][0], pts[i][1])\n  ctx.stroke()\n  i++\n  if (i \u003C pts.length) requestAnimationFrame(frame)\n}\nframe()\n",[36,672,673,678,719,741,748,798,847,852,856,861,875,886,896,931,950,959,968,992,997],{"__ignoreMap":46},[50,674,675],{"class":52,"line":53},[50,676,677],{"class":285},"\u002F\u002F 全点を先に計算\n",[50,679,680,682,685,687,690,693,696,698,701,704,707,709,712,715,717],{"class":52,"line":85},[50,681,57],{"class":56},[50,683,684],{"class":60}," pts",[50,686,64],{"class":56},[50,688,689],{"class":67}," Array.",[50,691,692],{"class":71},"from",[50,694,695],{"class":67},"({ length: totalPoints ",[50,697,394],{"class":56},[50,699,700],{"class":60}," 1",[50,702,703],{"class":67}," }, (",[50,705,706],{"class":508},"_",[50,708,140],{"class":67},[50,710,711],{"class":508},"i",[50,713,714],{"class":67},") ",[50,716,247],{"class":56},[50,718,250],{"class":67},[50,720,721,723,726,728,731,733,736,738],{"class":52,"line":108},[50,722,255],{"class":56},[50,724,725],{"class":60}," t",[50,727,64],{"class":56},[50,729,730],{"class":67}," (i ",[50,732,415],{"class":56},[50,734,735],{"class":67}," totalPoints) ",[50,737,382],{"class":56},[50,739,740],{"class":67}," totalAngle\n",[50,742,743,745],{"class":52,"line":115},[50,744,537],{"class":56},[50,746,747],{"class":67}," [\n",[50,749,750,753,755,757,759,761,763,765,767,769,771,773,775,777,779,781,783,785,787,789,791,793,795],{"class":52,"line":127},[50,751,752],{"class":67},"    cx ",[50,754,394],{"class":56},[50,756,371],{"class":67},[50,758,326],{"class":60},[50,760,376],{"class":56},[50,762,379],{"class":67},[50,764,382],{"class":56},[50,766,385],{"class":67},[50,768,388],{"class":71},[50,770,391],{"class":67},[50,772,394],{"class":56},[50,774,397],{"class":67},[50,776,382],{"class":56},[50,778,385],{"class":67},[50,780,388],{"class":71},[50,782,406],{"class":67},[50,784,326],{"class":60},[50,786,376],{"class":56},[50,788,379],{"class":67},[50,790,415],{"class":56},[50,792,379],{"class":67},[50,794,382],{"class":56},[50,796,797],{"class":67}," t),\n",[50,799,800,803,805,807,809,811,813,815,817,819,821,823,825,827,829,831,833,835,837,839,841,843,845],{"class":52,"line":147},[50,801,802],{"class":67},"    cy ",[50,804,394],{"class":56},[50,806,371],{"class":67},[50,808,326],{"class":60},[50,810,376],{"class":56},[50,812,379],{"class":67},[50,814,382],{"class":56},[50,816,385],{"class":67},[50,818,446],{"class":71},[50,820,391],{"class":67},[50,822,451],{"class":56},[50,824,397],{"class":67},[50,826,382],{"class":56},[50,828,385],{"class":67},[50,830,446],{"class":71},[50,832,406],{"class":67},[50,834,326],{"class":60},[50,836,376],{"class":56},[50,838,379],{"class":67},[50,840,415],{"class":56},[50,842,379],{"class":67},[50,844,382],{"class":56},[50,846,797],{"class":67},[50,848,849],{"class":52,"line":166},[50,850,851],{"class":67},"  ]\n",[50,853,854],{"class":52,"line":178},[50,855,291],{"class":67},[50,857,859],{"class":52,"line":858},9,[50,860,112],{"emptyLinePlaceholder":111},[50,862,864,867,870,872],{"class":52,"line":863},10,[50,865,866],{"class":56},"let",[50,868,869],{"class":67}," i ",[50,871,172],{"class":56},[50,873,874],{"class":60}," 1\n",[50,876,878,880,883],{"class":52,"line":877},11,[50,879,500],{"class":56},[50,881,882],{"class":71}," frame",[50,884,885],{"class":67},"() {\n",[50,887,889,892,894],{"class":52,"line":888},12,[50,890,891],{"class":67},"  ctx.",[50,893,121],{"class":71},[50,895,124],{"class":67},[50,897,899,901,903,906,908,910,913,916,919,921,923,925,928],{"class":52,"line":898},13,[50,900,891],{"class":67},[50,902,132],{"class":71},[50,904,905],{"class":67},"(pts[i ",[50,907,451],{"class":56},[50,909,700],{"class":60},[50,911,912],{"class":67},"][",[50,914,915],{"class":60},"0",[50,917,918],{"class":67},"], pts[i ",[50,920,451],{"class":56},[50,922,700],{"class":60},[50,924,912],{"class":67},[50,926,927],{"class":60},"1",[50,929,930],{"class":67},"])\n",[50,932,934,936,938,941,943,946,948],{"class":52,"line":933},14,[50,935,891],{"class":67},[50,937,152],{"class":71},[50,939,940],{"class":67},"(pts[i][",[50,942,915],{"class":60},[50,944,945],{"class":67},"], pts[i][",[50,947,927],{"class":60},[50,949,930],{"class":67},[50,951,953,955,957],{"class":52,"line":952},15,[50,954,891],{"class":67},[50,956,183],{"class":71},[50,958,124],{"class":67},[50,960,962,965],{"class":52,"line":961},16,[50,963,964],{"class":67},"  i",[50,966,967],{"class":56},"++\n",[50,969,971,974,976,978,981,984,986,989],{"class":52,"line":970},17,[50,972,973],{"class":56},"  if",[50,975,730],{"class":67},[50,977,216],{"class":56},[50,979,980],{"class":67}," pts.",[50,982,983],{"class":60},"length",[50,985,714],{"class":67},[50,987,988],{"class":71},"requestAnimationFrame",[50,990,991],{"class":67},"(frame)\n",[50,993,995],{"class":52,"line":994},18,[50,996,573],{"class":67},[50,998,1000,1003],{"class":52,"line":999},19,[50,1001,1002],{"class":71},"frame",[50,1004,124],{"class":67},[14,1006,1007,1009],{},[36,1008,988],{}," はブラウザの描画タイミングに合わせてコールバックを呼ぶため、60fps でスムーズなアニメーションになります。",[1011,1012,1013],"h3",{"id":1013},"グロー効果",[14,1015,1016,1017,1020],{},"Canvas でも ",[36,1018,1019],{},"shadowBlur"," を設定すると発光感が出ます。",[41,1022,1024],{"className":43,"code":1023,"language":45,"meta":46,"style":46},"ctx.shadowBlur  = 8\nctx.shadowColor = '#00DC82'\n",[36,1025,1026,1036],{"__ignoreMap":46},[50,1027,1028,1031,1033],{"class":52,"line":53},[50,1029,1030],{"class":67},"ctx.shadowBlur  ",[50,1032,172],{"class":56},[50,1034,1035],{"class":60}," 8\n",[50,1037,1038,1041,1043],{"class":52,"line":85},[50,1039,1040],{"class":67},"ctx.shadowColor ",[50,1042,172],{"class":56},[50,1044,175],{"class":78},[14,1046,1047,1048,1051,1052,1055],{},"また ",[36,1049,1050],{},"progress","（描画の進行度）に合わせて ",[36,1053,1054],{},"strokeStyle"," の不透明度を上げることで、描かれていく線が徐々に明るくなるエフェクトを加えています。",[41,1057,1059],{"className":43,"code":1058,"language":45,"meta":46,"style":46},"const progress = i \u002F totalPoints\nctx.strokeStyle = `rgba(0, 220, 130, ${0.4 + 0.6 * progress})`\n",[36,1060,1061,1077],{"__ignoreMap":46},[50,1062,1063,1065,1068,1070,1072,1074],{"class":52,"line":53},[50,1064,57],{"class":56},[50,1066,1067],{"class":60}," progress",[50,1069,64],{"class":56},[50,1071,869],{"class":67},[50,1073,415],{"class":56},[50,1075,1076],{"class":67}," totalPoints\n",[50,1078,1079,1081,1083,1086,1089,1092,1095,1097,1099],{"class":52,"line":85},[50,1080,169],{"class":67},[50,1082,172],{"class":56},[50,1084,1085],{"class":78}," `rgba(0, 220, 130, ${",[50,1087,1088],{"class":60},"0.4",[50,1090,1091],{"class":56}," +",[50,1093,1094],{"class":60}," 0.6",[50,1096,618],{"class":56},[50,1098,1067],{"class":67},[50,1100,1101],{"class":78},"})`\n",[26,1103],{},[10,1105,1107],{"id":1106},"retina-hidpi-対応","Retina \u002F HiDPI 対応",[14,1109,1110,1111,1114,1115,1118,1119,1122,1123,1126],{},"Canvas は ",[36,1112,1113],{},"width"," \u002F ",[36,1116,1117],{},"height"," 属性（物理ピクセル）と CSS サイズ（論理ピクセル）が分離しています。\n",[36,1120,1121],{},"devicePixelRatio"," を掛けてから ",[36,1124,1125],{},"scale()"," することでシャープな描画になります。",[41,1128,1130],{"className":43,"code":1129,"language":45,"meta":46,"style":46},"const dpr = window.devicePixelRatio || 1\ncanvas.width  = 400 * dpr\ncanvas.height = 400 * dpr\nctx.scale(dpr, dpr)\n\u002F\u002F 以降は 400×400 の座標系で描けば OK\n",[36,1131,1132,1149,1164,1177,1187],{"__ignoreMap":46},[50,1133,1134,1136,1139,1141,1144,1147],{"class":52,"line":53},[50,1135,57],{"class":56},[50,1137,1138],{"class":60}," dpr",[50,1140,64],{"class":56},[50,1142,1143],{"class":67}," window.devicePixelRatio ",[50,1145,1146],{"class":56},"||",[50,1148,874],{"class":60},[50,1150,1151,1154,1156,1159,1161],{"class":52,"line":85},[50,1152,1153],{"class":67},"canvas.width  ",[50,1155,172],{"class":56},[50,1157,1158],{"class":60}," 400",[50,1160,618],{"class":56},[50,1162,1163],{"class":67}," dpr\n",[50,1165,1166,1169,1171,1173,1175],{"class":52,"line":108},[50,1167,1168],{"class":67},"canvas.height ",[50,1170,172],{"class":56},[50,1172,1158],{"class":60},[50,1174,618],{"class":56},[50,1176,1163],{"class":67},[50,1178,1179,1181,1184],{"class":52,"line":115},[50,1180,118],{"class":67},[50,1182,1183],{"class":71},"scale",[50,1185,1186],{"class":67},"(dpr, dpr)\n",[50,1188,1189],{"class":52,"line":127},[50,1190,1191],{"class":285},"\u002F\u002F 以降は 400×400 の座標系で描けば OK\n",[26,1193],{},[10,1195,1196],{"id":1196},"手法まとめ",[301,1198,1199,1212],{},[304,1200,1201],{},[307,1202,1203,1206,1209],{},[310,1204,1205],{},"技術",[310,1207,1208],{},"描画モデル",[310,1210,1211],{},"向いている表現",[317,1213,1214,1225,1236],{},[307,1215,1216,1219,1222],{},[322,1217,1218],{},"CSS",[322,1220,1221],{},"宣言的・ボックスモデル",[322,1223,1224],{},"レイアウト・トランジション",[307,1226,1227,1230,1233],{},[322,1228,1229],{},"SVG",[322,1231,1232],{},"宣言的・ベクター",[322,1234,1235],{},"図形・パスアニメーション",[307,1237,1238,1243,1248],{},[322,1239,1240],{},[18,1241,1242],{},"Canvas 2D",[322,1244,1245],{},[18,1246,1247],{},"命令的・ラスター",[322,1249,1250],{},[18,1251,1252],{},"数学的曲線・粒子・ゲーム",[1254,1255,1257],"callout",{"type":1256},"tip",[14,1258,1259,1260,1263],{},"Canvas は一度描いた内容を「消す」ことができません（正確には ",[36,1261,1262],{},"clearRect"," で上書きするしかない）。そのため粒子エフェクトなど「前フレームを残しながら描く」表現も Canvas ならではです。",[1265,1266,1267],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":46,"searchDepth":85,"depth":85,"links":1269},[1270,1271,1272,1273,1274,1277,1278],{"id":12,"depth":85,"text":12},{"id":30,"depth":85,"text":31},{"id":296,"depth":85,"text":296},{"id":480,"depth":85,"text":481},{"id":660,"depth":85,"text":661,"children":1275},[1276],{"id":1013,"depth":108,"text":1013},{"id":1106,"depth":85,"text":1107},{"id":1196,"depth":85,"text":1196},"2026-05-01T20:00:00+09:00","HTML Canvas と requestAnimationFrame を使い、数学的な曲線「スピログラフ（ハイポトロコイド）」をアニメーション描画します。","md",{},"\u002Farticles\u002Fspirograph-art",{"title":5,"description":1280},"articles\u002Fspirograph-art","4zgUl0xBoEesKmSIe69L660w-Ka7gD69F43ZJboUzGY",1777568742017]