canvas製作數學動畫
做一個js,在div#ejtricc內增加一個canvas,大小是360*360,暗底色,在canvas中,有一個直徑為348的圓,圓心位於畫布中心。其內部存在一個三角形ABC,對應的邊為a,b,c,延長每條邊使其接觸到圓,保持「點A外的兩條延長線段長度等於a,點B外的兩條延長線段長度等於b,點C外的兩條延長線段長度等於c」。隨機緩慢移動A,B,C,但不能移至三點共線。不用標字母,三種長度相同的線段用相同顏色。加進css使canvas寬度等於div。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
在上面代碼的基礎上,增加「導出svg動畫」功能。svg中用circle和path替代canvas中的圓、點和線。三個點移動五次重回原位,用時5s,形成循環動畫。說明是否animate的values只需要5個幀即可實現,如不行,則做成每秒12幀更改點坐標和path的d值。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
代码: 全选
<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="360"
height="360"
viewBox="0 0 360 360"
role="img"
aria-label="循環移動的幾何三角形"
data-position-count="4"
data-duration="7"
data-fps="12"
>
<defs>
<radialGradient
id="ejtricc-circle-background"
cx="50%"
cy="50%"
r="50%"
>
<stop
offset="0%"
stop-color="#121b2f"
/>
<stop
offset="72%"
stop-color="#0d1424"
/>
<stop
offset="100%"
stop-color="#090e19"
/>
</radialGradient>
<clipPath id="ejtricc-circle-clip">
<circle
cx="180"
cy="180"
r="174"
/>
</clipPath>
<filter
id="ejtricc-soft-glow"
x="-20%"
y="-20%"
width="140%"
height="140%"
color-interpolation-filters="sRGB"
>
<feGaussianBlur
stdDeviation="2"
result="blur"
/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 暗色背景 -->
<path
d="M 0 0 H 360 V 360 H 0 Z"
fill="#070a12"
/>
<!-- 直徑 348 的圓形背景 -->
<circle
cx="180"
cy="180"
r="174"
fill="url(#ejtricc-circle-background)"
/>
<!-- 九條動畫線段 -->
<g
clip-path="url(#ejtricc-circle-clip)"
filter="url(#ejtricc-soft-glow)"
>
<path
d="M 99.742 334.385 L 128.398 241.16"
fill="none"
stroke="#36e2ff"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 99.742 334.385 L 128.398 241.16;M 100.192 334.618 L 128.607 241.161;M 101.472 335.272 L 129.199 241.159;M 103.481 336.272 L 130.117 241.151;M 106.124 337.538 L 131.301 241.128;M 109.302 338.99 L 132.695 241.083;M 112.916 340.548 L 134.241 241.011;M 116.859 342.14 L 135.882 240.907;M 121.021 343.699 L 137.567 240.771;M 125.284 345.173 L 139.245 240.605;M 129.523 346.518 L 140.869 240.413;M 133.611 347.702 L 142.395 240.204;M 137.412 348.708 L 143.78 239.99;M 140.791 349.525 L 144.985 239.784;M 143.608 350.152 L 145.972 239.6;M 145.727 350.591 L 146.704 239.456;M 147.009 350.844 L 147.142 239.366;M 147.379 350.915 L 147.269 239.344;M 148.638 351.15 L 147.698 239.372;M 151.525 351.654 L 148.677 239.426;M 155.832 352.313 L 150.123 239.479;M 161.349 352.998 L 151.95 239.497;M 167.864 353.576 L 154.072 239.449;M 175.151 353.932 L 156.401 239.309;M 182.979 353.974 L 158.852 239.06;M 191.105 353.645 L 161.343 238.694;M 199.279 352.929 L 163.795 238.218;M 207.254 351.852 L 166.136 237.649;M 214.783 350.488 L 168.299 237.017;M 221.629 348.947 L 170.227 236.363;M 227.565 347.373 L 171.868 235.732;M 232.373 345.931 L 173.177 235.178;M 235.846 344.795 L 174.11 234.753;M 237.776 344.128 L 174.625 234.508;M 237.961 344.063 L 174.68 234.496;M 236.386 344.61 L 174.329 234.826;M 233.252 345.651 L 173.62 235.479;M 228.762 347.028 L 172.582 236.404;M 223.126 348.571 L 171.238 237.55;M 216.557 350.116 L 169.613 238.863;M 209.28 351.519 L 167.738 240.293;M 201.532 352.663 L 165.646 241.789;M 193.555 353.471 L 163.386 243.302;M 185.594 353.91 L 161.013 244.786;M 177.893 353.987 L 158.598 246.197;M 170.689 353.751 L 156.224 247.495;M 164.207 353.282 L 153.985 248.646;M 158.661 352.687 L 151.986 249.618;M 154.254 352.085 L 150.339 250.381;M 151.182 351.597 L 149.158 250.908;M 149.64 351.331 L 148.555 251.171;M 149.294 351.269 L 148.406 251.014;M 148.116 351.054 L 147.873 249.959;M 146.022 350.65 L 146.961 248.081;M 143.173 350.058 L 145.794 245.521;M 139.73 349.276 L 144.498 242.423;M 135.853 348.306 L 143.188 238.931;M 131.695 347.16 L 141.963 235.188;M 127.399 345.859 L 140.892 231.331;M 123.098 344.433 L 140.02 227.487;M 118.916 342.925 L 139.367 223.774;M 114.963 341.388 L 138.926 220.295;M 111.344 339.882 L 138.674 217.139;M 108.152 338.474 L 138.572 214.387;M 105.478 337.234 L 138.575 212.105;M 103.407 336.235 L 138.631 210.354;M 102.026 335.551 L 138.696 209.196;M 101.426 335.249 L 138.731 208.695;M 101.365 335.218 L 138.642 208.849;M 101.201 335.135 L 138.302 209.501;M 100.945 335.004 L 137.748 210.6;M 100.631 334.843 L 137.028 212.1;M 100.293 334.67 L 136.189 213.961;M 99.964 334.5 L 135.275 216.14;M 99.673 334.349 L 134.327 218.594;M 99.439 334.227 L 133.379 221.271;M 99.278 334.143 L 132.461 224.111;M 99.195 334.099 L 131.599 227.043;M 99.188 334.096 L 130.812 229.981;M 99.247 334.127 L 130.116 232.824;M 99.355 334.183 L 129.525 235.456;M 99.486 334.251 L 129.048 237.746;M 99.613 334.318 L 128.695 239.555;M 99.706 334.366 L 128.474 240.738;M 99.742 334.385 L 128.398 241.16"
/>
</path>
<path
d="M 167.183 114.982 L 229.075 190.357"
fill="none"
stroke="#36e2ff"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 167.183 114.982 L 229.075 190.357;M 166.97 114.986 L 229.205 190.275;M 166.367 115.003 L 229.573 190.043;M 165.427 115.048 L 230.147 189.672;M 164.203 115.138 L 230.893 189.175;M 162.75 115.294 L 231.777 188.563;M 161.124 115.53 L 232.766 187.847;M 159.382 115.855 L 233.827 187.044;M 157.581 116.273 L 234.927 186.171;M 155.779 116.776 L 236.034 185.25;M 154.029 117.348 L 237.116 184.308;M 152.383 117.964 L 238.142 183.374;M 150.89 118.591 L 239.081 182.485;M 149.594 119.19 L 239.905 181.676;M 148.535 119.719 L 240.585 180.989;M 147.753 120.134 L 241.091 180.465;M 147.285 120.392 L 241.396 180.145;M 147.151 120.469 L 241.482 180.049;M 146.701 120.77 L 241.712 179.662;M 145.684 121.475 L 242.231 178.763;M 144.208 122.563 L 242.985 177.402;M 142.387 124.018 L 243.916 175.62;M 140.34 125.82 L 244.962 173.459;M 138.182 127.939 L 246.065 170.968;M 136.024 130.33 L 247.169 168.202;M 133.96 132.929 L 248.224 165.229;M 132.07 135.658 L 249.19 162.128;M 130.41 138.423 L 250.035 158.99;M 129.015 141.121 L 250.741 155.919;M 127.895 143.644 L 251.302 153.031;M 127.043 145.882 L 251.722 150.449;M 126.435 147.728 L 252.014 148.301;M 126.044 149.078 L 252.198 146.717;M 125.844 149.834 L 252.29 145.824;M 125.833 149.921 L 252.291 145.716;M 126.074 149.459 L 252.142 146.219;M 126.57 148.552 L 251.837 147.209;M 127.322 147.285 L 251.387 148.602;M 128.332 145.747 L 250.796 150.312;M 129.6 144.032 L 250.071 152.254;M 131.111 142.231 L 249.222 154.345;M 132.84 140.432 L 248.262 156.509;M 134.744 138.713 L 247.212 158.673;M 136.765 137.138 L 246.098 160.772;M 138.829 135.754 L 244.954 162.749;M 140.854 134.592 L 243.82 164.552;M 142.752 133.662 L 242.744 166.138;M 144.431 132.957 L 241.779 167.467;M 145.799 132.46 L 240.982 168.506;M 146.772 132.147 L 240.409 169.22;M 147.265 132.001 L 240.116 169.576;M 147.35 131.89 L 240.189 169.744;M 147.587 131.318 L 240.765 170.537;M 148.039 130.307 L 241.759 171.995;M 148.717 128.947 L 243.047 174.077;M 149.633 127.329 L 244.494 176.743;M 150.793 125.546 L 245.968 179.939;M 152.187 123.689 L 247.345 183.59;M 153.791 121.838 L 248.524 187.598;M 155.564 120.066 L 249.431 191.842;M 157.448 118.432 L 250.023 196.182;M 159.371 116.977 L 250.299 200.465;M 161.254 115.732 L 250.289 204.532;M 163.009 114.708 L 250.057 208.226;M 164.548 113.91 L 249.693 211.393;M 165.781 113.331 L 249.3 213.887;M 166.623 112.966 L 248.982 215.568;M 166.994 112.812 L 248.829 216.303;M 166.992 112.746 L 248.702 216.1;M 166.941 112.518 L 248.232 215.242;M 166.864 112.176 L 247.451 213.846;M 166.774 111.784 L 246.405 212.032;M 166.684 111.411 L 245.138 209.921;M 166.607 111.116 L 243.693 207.623;M 166.553 110.948 L 242.111 205.241;M 166.529 110.942 L 240.433 202.862;M 166.54 111.113 L 238.7 200.56;M 166.585 111.458 L 236.959 198.398;M 166.661 111.957 L 235.255 196.424;M 166.761 112.568 L 233.641 194.68;M 166.875 113.237 L 232.174 193.195;M 166.989 113.892 L 230.918 191.996;M 167.088 114.453 L 229.937 191.107;M 167.157 114.84 L 229.301 190.55;M 167.183 114.982 L 229.075 190.357"
/>
</path>
<path
d="M 128.398 241.16 L 41.326 285.098"
fill="none"
stroke="#36e2ff"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 128.398 241.16 L 41.326 285.098;M 128.607 241.161 L 41.442 285.251;M 129.199 241.159 L 41.771 285.683;M 130.117 241.151 L 42.285 286.351;M 131.301 241.128 L 42.956 287.214;M 132.695 241.083 L 43.755 288.228;M 134.241 241.011 L 44.655 289.351;M 135.882 240.907 L 45.626 290.543;M 137.567 240.771 L 46.64 291.764;M 139.245 240.605 L 47.668 292.979;M 140.869 240.413 L 48.679 294.153;M 142.395 240.204 L 49.645 295.255;M 143.78 239.99 L 50.535 296.254;M 144.985 239.784 L 51.32 297.122;M 145.972 239.6 L 51.971 297.833;M 146.704 239.456 L 52.457 298.359;M 147.142 239.366 L 52.751 298.675;M 147.269 239.344 L 52.842 298.772;M 147.698 239.372 L 53.339 299.302;M 148.677 239.426 L 54.483 300.505;M 150.123 239.479 L 56.198 302.266;M 151.95 239.497 L 58.411 304.467;M 154.072 239.449 L 61.047 306.989;M 156.401 239.309 L 64.027 309.716;M 158.852 239.06 L 67.266 312.541;M 161.343 238.694 L 70.672 315.364;M 163.795 238.218 L 74.147 318.098;M 166.136 237.649 L 77.586 320.668;M 168.299 237.017 L 80.882 323.009;M 170.227 236.363 L 83.923 325.069;M 171.868 235.732 L 86.595 326.804;M 173.177 235.178 L 88.786 328.176;M 174.11 234.753 L 90.384 329.147;M 174.625 234.508 L 91.278 329.681;M 174.68 234.496 L 91.404 329.756;M 174.329 234.826 L 91.115 329.584;M 173.62 235.479 L 90.538 329.24;M 172.582 236.404 L 89.709 328.74;M 171.238 237.55 L 88.661 328.098;M 169.613 238.863 L 87.429 327.332;M 167.738 240.293 L 86.05 326.456;M 165.646 241.789 L 84.561 325.49;M 163.386 243.302 L 83.002 324.456;M 161.013 244.786 L 81.418 323.379;M 158.598 246.197 L 79.853 322.291;M 156.224 247.495 L 78.357 321.226;M 153.985 248.646 L 76.982 320.226;M 151.986 249.618 L 75.781 319.336;M 150.339 250.381 L 74.81 318.604;M 149.158 250.908 L 74.123 318.08;M 148.555 251.171 L 73.775 317.812;M 148.406 251.014 L 73.343 317.478;M 147.873 249.959 L 71.034 315.656;M 146.961 248.081 L 66.967 312.286;M 145.794 245.521 L 61.52 307.431;M 144.498 242.423 L 55.098 301.143;M 143.188 238.931 L 48.115 293.5;M 141.963 235.188 L 40.976 284.634;M 140.892 231.331 L 34.054 274.741;M 140.02 227.487 L 27.669 264.091;M 139.367 223.774 L 22.065 253.024;M 138.926 220.295 L 17.395 241.933;M 138.674 217.139 L 13.72 231.253;M 138.572 214.387 L 11.006 221.436;M 138.575 212.105 L 9.145 212.933;M 138.631 210.354 L 7.981 206.184;M 138.696 209.196 L 7.347 201.607;M 138.731 208.695 L 7.107 199.599;M 138.642 208.849 L 7.175 200.186;M 138.302 209.501 L 7.483 202.671;M 137.748 210.6 L 8.071 206.763;M 137.028 212.1 L 9.001 212.179;M 136.189 213.961 L 10.342 218.627;M 135.275 216.14 L 12.14 225.814;M 134.327 218.594 L 14.413 233.45;M 133.379 221.271 L 17.141 241.26;M 132.461 224.111 L 20.259 248.985;M 131.599 227.043 L 23.664 256.389;M 130.812 229.981 L 27.216 263.265;M 130.116 232.824 L 30.746 269.438;M 129.525 235.456 L 34.065 274.757;M 129.048 237.746 L 36.976 279.096;M 128.695 239.555 L 39.281 282.343;M 128.474 240.738 L 40.789 284.385;M 128.398 241.16 L 41.326 285.098"
/>
</path>
<path
d="M 167.183 114.982 L 200.317 7.19"
fill="none"
stroke="#ff4fc8"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 167.183 114.982 L 200.317 7.19;M 166.97 114.986 L 199.765 7.126;M 166.367 115.003 L 198.201 6.955;M 165.427 115.048 L 195.762 6.715;M 164.203 115.138 L 192.585 6.456;M 162.75 115.294 L 188.81 6.223;M 161.124 115.53 L 184.576 6.06;M 159.382 115.855 L 180.026 6;M 157.581 116.273 L 175.298 6.064;M 155.779 116.776 L 170.535 6.258;M 154.029 117.348 L 165.874 6.574;M 152.383 117.964 L 161.451 6.992;M 150.89 118.591 L 157.398 7.474;M 149.594 119.19 L 153.844 7.977;M 148.535 119.719 L 150.914 8.448;M 147.753 120.134 L 148.731 8.833;M 147.285 120.392 L 147.418 9.078;M 147.151 120.469 L 147.04 9.15;M 146.701 120.77 L 145.765 9.401;M 145.684 121.475 L 142.856 10.011;M 144.208 122.563 L 138.564 11.006;M 142.387 124.018 L 133.146 12.427;M 140.34 125.82 L 126.864 14.312;M 138.182 127.939 L 119.982 16.679;M 136.024 130.33 L 112.758 19.518;M 133.96 132.929 L 105.443 22.783;M 132.07 135.658 L 98.27 26.39;M 130.41 138.423 L 91.451 30.217;M 129.015 141.121 L 85.176 34.108;M 127.895 143.644 L 79.608 37.883;M 127.043 145.882 L 74.886 41.338;M 126.435 147.728 L 71.135 44.264;M 126.044 149.078 L 68.466 46.448;M 125.844 149.834 L 66.998 47.688;M 125.833 149.921 L 66.857 47.808;M 126.074 149.459 L 68.044 46.801;M 126.57 148.552 L 70.43 44.832;M 127.322 147.285 L 73.9 42.092;M 128.332 145.747 L 78.342 38.785;M 129.6 144.032 L 83.643 35.116;M 131.111 142.231 L 89.672 31.283;M 132.84 140.432 L 96.277 27.466;M 134.744 138.713 L 103.283 23.825;M 136.765 137.138 L 110.489 20.488;M 138.829 135.754 L 117.67 17.547;M 140.854 134.592 L 124.582 15.061;M 142.752 133.662 L 130.97 13.051;M 144.431 132.957 L 136.565 11.508;M 145.799 132.46 L 141.101 10.404;M 146.772 132.147 L 144.311 9.699;M 147.265 132.001 L 145.937 9.367;M 147.35 131.89 L 146.264 9.302;M 147.587 131.318 L 147.293 9.102;M 148.039 130.307 L 149.152 8.756;M 148.717 128.947 L 151.742 8.31;M 149.633 127.329 L 154.967 7.81;M 150.793 125.546 L 158.723 7.306;M 152.187 123.689 L 162.902 6.842;M 153.791 121.838 L 167.385 6.458;M 155.564 120.066 L 172.043 6.182;M 157.448 118.432 L 176.74 6.031;M 159.371 116.977 L 181.331 6.005;M 161.254 115.732 L 185.666 6.092;M 163.009 114.708 L 189.594 6.265;M 164.548 113.91 L 192.962 6.483;M 165.781 113.331 L 195.618 6.702;M 166.623 112.966 L 197.413 6.873;M 166.994 112.812 L 198.198 6.954;M 166.992 112.746 L 198.199 6.954;M 166.941 112.518 L 198.118 6.946;M 166.864 112.176 L 197.998 6.933;M 166.774 111.784 L 197.87 6.92;M 166.684 111.411 L 197.761 6.909;M 166.607 111.116 L 197.698 6.902;M 166.553 110.948 L 197.702 6.903;M 166.529 110.942 L 197.787 6.912;M 166.54 111.113 L 197.959 6.929;M 166.585 111.458 L 198.215 6.956;M 166.661 111.957 L 198.543 6.991;M 166.761 112.568 L 198.92 7.032;M 166.875 113.237 L 199.317 7.076;M 166.989 113.892 L 199.697 7.118;M 167.088 114.453 L 200.017 7.155;M 167.157 114.84 L 200.237 7.181;M 167.183 114.982 L 200.317 7.19"
/>
</path>
<path
d="M 95.619 27.829 L 167.183 114.982"
fill="none"
stroke="#ff4fc8"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 95.619 27.829 L 167.183 114.982;M 95.144 28.094 L 166.97 114.986;M 93.802 28.852 L 166.367 115.003;M 91.718 30.059 L 165.427 115.048;M 89.025 31.678 L 164.203 115.138;M 85.853 33.671 L 162.75 115.294;M 82.333 35.996 L 161.124 115.53;M 78.595 38.603 L 159.382 115.855;M 74.764 41.431 L 157.581 116.273;M 70.957 44.406 L 155.779 116.776;M 67.286 47.442 L 154.029 117.348;M 63.853 50.44 L 152.383 117.964;M 60.753 53.287 L 150.89 118.591;M 58.07 55.866 L 149.594 119.19;M 55.886 58.051 L 148.535 119.719;M 54.274 59.713 L 147.753 120.134;M 53.311 60.727 L 147.285 120.392;M 53.033 61.023 L 147.151 120.469;M 52.038 62.094 L 146.701 120.77;M 49.795 64.577 L 145.684 121.475;M 46.549 68.345 L 144.208 122.563;M 42.567 73.284 L 142.387 124.018;M 38.119 79.274 L 140.34 125.82;M 33.465 86.173 L 138.182 127.939;M 28.847 93.809 L 136.024 130.33;M 24.473 101.979 L 133.96 132.929;M 20.507 110.444 L 132.07 135.658;M 17.067 118.936 L 130.41 138.423;M 14.216 127.165 L 129.015 141.121;M 11.966 134.826 L 127.895 143.644;M 10.289 141.606 L 127.043 145.882;M 9.121 147.192 L 126.435 147.728;M 8.387 151.28 L 126.044 149.078;M 8.019 153.572 L 125.844 149.834;M 7.978 153.84 L 125.833 149.921;M 8.189 152.489 L 126.074 149.459;M 8.638 149.816 L 126.57 148.552;M 9.348 146.032 L 127.322 147.285;M 10.347 141.349 L 128.332 145.747;M 11.66 135.983 L 129.6 144.032;M 13.295 130.147 L 131.111 142.231;M 15.24 124.052 L 132.84 140.432;M 17.46 117.898 L 134.744 138.713;M 19.892 111.873 L 136.765 137.138;M 22.449 106.151 L 138.829 135.754;M 25.024 100.889 L 140.854 134.592;M 27.494 96.228 L 142.752 133.662;M 29.722 92.293 L 144.431 132.957;M 31.57 89.201 L 145.799 132.46;M 32.899 87.063 L 146.772 132.147;M 33.579 85.995 L 147.265 132.001;M 33.832 85.603 L 147.35 131.89;M 34.943 83.904 L 147.587 131.318;M 36.975 80.905 L 148.039 130.307;M 39.86 76.866 L 148.717 128.947;M 43.529 72.058 L 149.633 127.329;M 47.904 66.746 L 150.793 125.546;M 52.886 61.18 L 152.187 123.689;M 58.353 55.589 L 153.791 121.838;M 64.155 50.17 L 155.564 120.066;M 70.117 45.086 L 157.448 118.432;M 76.044 40.468 L 159.371 116.977;M 81.724 36.411 L 161.254 115.732;M 86.934 32.98 L 163.009 114.708;M 91.448 30.219 L 164.548 113.91;M 95.035 28.155 L 165.781 113.331;M 97.472 26.817 L 166.623 112.966;M 98.542 26.245 L 166.994 112.812;M 98.587 26.221 L 166.992 112.746;M 98.631 26.198 L 166.941 112.518;M 98.691 26.166 L 166.864 112.176;M 98.743 26.139 L 166.774 111.784;M 98.764 26.127 L 166.684 111.411;M 98.735 26.143 L 166.607 111.116;M 98.638 26.194 L 166.553 110.948;M 98.466 26.286 L 166.529 110.942;M 98.214 26.419 L 166.54 111.113;M 97.89 26.592 L 166.585 111.458;M 97.506 26.798 L 166.661 111.957;M 97.086 27.025 L 166.761 112.568;M 96.658 27.258 L 166.875 113.237;M 96.258 27.477 L 166.989 113.892;M 95.926 27.66 L 167.088 114.453;M 95.701 27.784 L 167.157 114.84;M 95.619 27.829 L 167.183 114.982"
/>
</path>
<path
d="M 229.075 190.357 L 128.398 241.16"
fill="none"
stroke="#ff4fc8"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 229.075 190.357 L 128.398 241.16;M 229.205 190.275 L 128.607 241.161;M 229.573 190.043 L 129.199 241.159;M 230.147 189.672 L 130.117 241.151;M 230.893 189.175 L 131.301 241.128;M 231.777 188.563 L 132.695 241.083;M 232.766 187.847 L 134.241 241.011;M 233.827 187.044 L 135.882 240.907;M 234.927 186.171 L 137.567 240.771;M 236.034 185.25 L 139.245 240.605;M 237.116 184.308 L 140.869 240.413;M 238.142 183.374 L 142.395 240.204;M 239.081 182.485 L 143.78 239.99;M 239.905 181.676 L 144.985 239.784;M 240.585 180.989 L 145.972 239.6;M 241.091 180.465 L 146.704 239.456;M 241.396 180.145 L 147.142 239.366;M 241.482 180.049 L 147.269 239.344;M 241.712 179.662 L 147.698 239.372;M 242.231 178.763 L 148.677 239.426;M 242.985 177.402 L 150.123 239.479;M 243.916 175.62 L 151.95 239.497;M 244.962 173.459 L 154.072 239.449;M 246.065 170.968 L 156.401 239.309;M 247.169 168.202 L 158.852 239.06;M 248.224 165.229 L 161.343 238.694;M 249.19 162.128 L 163.795 238.218;M 250.035 158.99 L 166.136 237.649;M 250.741 155.919 L 168.299 237.017;M 251.302 153.031 L 170.227 236.363;M 251.722 150.449 L 171.868 235.732;M 252.014 148.301 L 173.177 235.178;M 252.198 146.717 L 174.11 234.753;M 252.29 145.824 L 174.625 234.508;M 252.291 145.716 L 174.68 234.496;M 252.142 146.219 L 174.329 234.826;M 251.837 147.209 L 173.62 235.479;M 251.387 148.602 L 172.582 236.404;M 250.796 150.312 L 171.238 237.55;M 250.071 152.254 L 169.613 238.863;M 249.222 154.345 L 167.738 240.293;M 248.262 156.509 L 165.646 241.789;M 247.212 158.673 L 163.386 243.302;M 246.098 160.772 L 161.013 244.786;M 244.954 162.749 L 158.598 246.197;M 243.82 164.552 L 156.224 247.495;M 242.744 166.138 L 153.985 248.646;M 241.779 167.467 L 151.986 249.618;M 240.982 168.506 L 150.339 250.381;M 240.409 169.22 L 149.158 250.908;M 240.116 169.576 L 148.555 251.171;M 240.189 169.744 L 148.406 251.014;M 240.765 170.537 L 147.873 249.959;M 241.759 171.995 L 146.961 248.081;M 243.047 174.077 L 145.794 245.521;M 244.494 176.743 L 144.498 242.423;M 245.968 179.939 L 143.188 238.931;M 247.345 183.59 L 141.963 235.188;M 248.524 187.598 L 140.892 231.331;M 249.431 191.842 L 140.02 227.487;M 250.023 196.182 L 139.367 223.774;M 250.299 200.465 L 138.926 220.295;M 250.289 204.532 L 138.674 217.139;M 250.057 208.226 L 138.572 214.387;M 249.693 211.393 L 138.575 212.105;M 249.3 213.887 L 138.631 210.354;M 248.982 215.568 L 138.696 209.196;M 248.829 216.303 L 138.731 208.695;M 248.702 216.1 L 138.642 208.849;M 248.232 215.242 L 138.302 209.501;M 247.451 213.846 L 137.748 210.6;M 246.405 212.032 L 137.028 212.1;M 245.138 209.921 L 136.189 213.961;M 243.693 207.623 L 135.275 216.14;M 242.111 205.241 L 134.327 218.594;M 240.433 202.862 L 133.379 221.271;M 238.7 200.56 L 132.461 224.111;M 236.959 198.398 L 131.599 227.043;M 235.255 196.424 L 130.812 229.981;M 233.641 194.68 L 130.116 232.824;M 232.174 193.195 L 129.525 235.456;M 230.918 191.996 L 129.048 237.746;M 229.937 191.107 L 128.695 239.555;M 229.301 190.55 L 128.474 240.738;M 229.075 190.357 L 128.398 241.16"
/>
</path>
<path
d="M 128.398 241.16 L 167.183 114.982"
fill="none"
stroke="#ffd447"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 128.398 241.16 L 167.183 114.982;M 128.607 241.161 L 166.97 114.986;M 129.199 241.159 L 166.367 115.003;M 130.117 241.151 L 165.427 115.048;M 131.301 241.128 L 164.203 115.138;M 132.695 241.083 L 162.75 115.294;M 134.241 241.011 L 161.124 115.53;M 135.882 240.907 L 159.382 115.855;M 137.567 240.771 L 157.581 116.273;M 139.245 240.605 L 155.779 116.776;M 140.869 240.413 L 154.029 117.348;M 142.395 240.204 L 152.383 117.964;M 143.78 239.99 L 150.89 118.591;M 144.985 239.784 L 149.594 119.19;M 145.972 239.6 L 148.535 119.719;M 146.704 239.456 L 147.753 120.134;M 147.142 239.366 L 147.285 120.392;M 147.269 239.344 L 147.151 120.469;M 147.698 239.372 L 146.701 120.77;M 148.677 239.426 L 145.684 121.475;M 150.123 239.479 L 144.208 122.563;M 151.95 239.497 L 142.387 124.018;M 154.072 239.449 L 140.34 125.82;M 156.401 239.309 L 138.182 127.939;M 158.852 239.06 L 136.024 130.33;M 161.343 238.694 L 133.96 132.929;M 163.795 238.218 L 132.07 135.658;M 166.136 237.649 L 130.41 138.423;M 168.299 237.017 L 129.015 141.121;M 170.227 236.363 L 127.895 143.644;M 171.868 235.732 L 127.043 145.882;M 173.177 235.178 L 126.435 147.728;M 174.11 234.753 L 126.044 149.078;M 174.625 234.508 L 125.844 149.834;M 174.68 234.496 L 125.833 149.921;M 174.329 234.826 L 126.074 149.459;M 173.62 235.479 L 126.57 148.552;M 172.582 236.404 L 127.322 147.285;M 171.238 237.55 L 128.332 145.747;M 169.613 238.863 L 129.6 144.032;M 167.738 240.293 L 131.111 142.231;M 165.646 241.789 L 132.84 140.432;M 163.386 243.302 L 134.744 138.713;M 161.013 244.786 L 136.765 137.138;M 158.598 246.197 L 138.829 135.754;M 156.224 247.495 L 140.854 134.592;M 153.985 248.646 L 142.752 133.662;M 151.986 249.618 L 144.431 132.957;M 150.339 250.381 L 145.799 132.46;M 149.158 250.908 L 146.772 132.147;M 148.555 251.171 L 147.265 132.001;M 148.406 251.014 L 147.35 131.89;M 147.873 249.959 L 147.587 131.318;M 146.961 248.081 L 148.039 130.307;M 145.794 245.521 L 148.717 128.947;M 144.498 242.423 L 149.633 127.329;M 143.188 238.931 L 150.793 125.546;M 141.963 235.188 L 152.187 123.689;M 140.892 231.331 L 153.791 121.838;M 140.02 227.487 L 155.564 120.066;M 139.367 223.774 L 157.448 118.432;M 138.926 220.295 L 159.371 116.977;M 138.674 217.139 L 161.254 115.732;M 138.572 214.387 L 163.009 114.708;M 138.575 212.105 L 164.548 113.91;M 138.631 210.354 L 165.781 113.331;M 138.696 209.196 L 166.623 112.966;M 138.731 208.695 L 166.994 112.812;M 138.642 208.849 L 166.992 112.746;M 138.302 209.501 L 166.941 112.518;M 137.748 210.6 L 166.864 112.176;M 137.028 212.1 L 166.774 111.784;M 136.189 213.961 L 166.684 111.411;M 135.275 216.14 L 166.607 111.116;M 134.327 218.594 L 166.553 110.948;M 133.379 221.271 L 166.529 110.942;M 132.461 224.111 L 166.54 111.113;M 131.599 227.043 L 166.585 111.458;M 130.812 229.981 L 166.661 111.957;M 130.116 232.824 L 166.761 112.568;M 129.525 235.456 L 166.875 113.237;M 129.048 237.746 L 166.989 113.892;M 128.695 239.555 L 167.088 114.453;M 128.474 240.738 L 167.157 114.84;M 128.398 241.16 L 167.183 114.982"
/>
</path>
<path
d="M 229.075 190.357 L 312.845 292.375"
fill="none"
stroke="#ffd447"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 229.075 190.357 L 312.845 292.375;M 229.205 190.275 L 313.227 291.922;M 229.573 190.043 L 314.3 290.632;M 230.147 189.672 L 315.947 288.603;M 230.893 189.175 L 318.042 285.926;M 231.777 188.563 L 320.461 282.697;M 232.766 187.847 L 323.081 279.014;M 233.827 187.044 L 325.788 274.983;M 234.927 186.171 L 328.481 270.716;M 236.034 185.25 L 331.07 266.335;M 237.116 184.308 L 333.483 261.97;M 238.142 183.374 L 335.66 257.755;M 239.081 182.485 L 337.559 253.831;M 239.905 181.676 L 339.148 250.341;M 240.585 180.989 L 340.403 247.43;M 241.091 180.465 L 341.306 245.24;M 241.396 180.145 L 341.836 243.915;M 241.482 180.049 L 341.988 243.53;M 241.712 179.662 L 342.523 242.148;M 242.231 178.763 L 343.701 238.973;M 242.985 177.402 L 345.335 234.225;M 243.916 175.62 L 347.214 228.121;M 244.962 173.459 L 349.127 220.89;M 246.065 170.968 L 350.885 212.775;M 247.169 168.202 L 352.332 204.036;M 248.224 165.229 L 353.357 194.948;M 249.19 162.128 L 353.904 185.794;M 250.035 158.99 L 353.972 176.859;M 250.741 155.919 L 353.615 168.425;M 251.302 153.031 L 352.933 160.762;M 251.722 150.449 L 352.065 154.125;M 252.014 148.301 L 351.171 148.753;M 252.198 146.717 L 350.418 144.878;M 252.29 145.824 L 349.961 142.725;M 252.291 145.716 L 349.905 142.471;M 252.142 146.219 L 350.171 143.7;M 251.837 147.209 L 350.675 146.149;M 251.387 148.602 L 351.335 149.663;M 250.796 150.312 L 352.059 154.086;M 250.071 152.254 L 352.76 159.262;M 249.222 154.345 L 353.354 165.026;M 248.262 156.509 L 353.778 171.206;M 247.212 158.673 L 353.984 177.622;M 246.098 160.772 L 353.952 184.087;M 244.954 162.749 L 353.688 190.408;M 243.82 164.552 L 353.227 196.386;M 242.744 166.138 L 352.626 201.825;M 241.779 167.467 L 351.966 206.528;M 240.982 168.506 L 351.342 210.299;M 240.409 169.22 L 350.852 212.947;M 240.116 169.576 L 350.589 214.282;M 240.189 169.744 L 350.5 214.723;M 240.765 170.537 L 350.115 216.563;M 241.759 171.995 L 349.372 219.862;M 243.047 174.077 L 348.239 224.404;M 244.494 176.743 L 346.671 229.968;M 245.968 179.939 L 344.632 236.324;M 247.345 183.59 L 342.102 243.237;M 248.524 187.598 L 339.093 250.466;M 249.431 191.842 L 335.652 257.772;M 250.023 196.182 L 331.87 264.921;M 250.299 200.465 L 327.878 271.696;M 250.289 204.532 L 323.847 277.897;M 250.057 208.226 L 319.983 283.348;M 249.693 211.393 L 316.511 287.892;M 249.3 213.887 L 313.673 291.39;M 248.982 215.568 L 311.705 293.709;M 248.829 216.303 L 310.831 294.714;M 248.702 216.1 L 310.842 294.701;M 248.232 215.242 L 310.984 294.539;M 247.451 213.846 L 311.208 294.283;M 246.405 212.032 L 311.486 293.963;M 245.138 209.921 L 311.79 293.611;M 243.693 207.623 L 312.094 293.257;M 242.111 205.241 L 312.376 292.928;M 240.433 202.862 L 312.617 292.644;M 238.7 200.56 L 312.807 292.42;M 236.959 198.398 L 312.939 292.264;M 235.255 196.424 L 313.012 292.177;M 233.641 194.68 L 313.033 292.153;M 232.174 193.195 L 313.011 292.178;M 230.918 191.996 L 312.963 292.235;M 229.937 191.107 L 312.908 292.301;M 229.301 190.55 L 312.863 292.354;M 229.075 190.357 L 312.845 292.375"
/>
</path>
<path
d="M 346.925 130.887 L 229.075 190.357"
fill="none"
stroke="#ffd447"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="M 346.925 130.887 L 229.075 190.357;M 346.885 130.75 L 229.205 190.275;M 346.769 130.36 L 229.573 190.043;M 346.586 129.75 L 230.147 189.672;M 346.343 128.95 L 230.893 189.175;M 346.046 127.992 L 231.777 188.563;M 345.702 126.907 L 232.766 187.847;M 345.32 125.729 L 233.827 187.044;M 344.909 124.492 L 234.927 186.171;M 344.478 123.229 L 236.034 185.25;M 344.041 121.977 L 237.116 184.308;M 343.61 120.774 L 238.142 183.374;M 343.202 119.658 L 239.081 182.485;M 342.832 118.668 L 239.905 181.676;M 342.519 117.843 L 240.585 180.989;M 342.281 117.223 L 241.091 180.465;M 342.135 116.848 L 241.396 180.145;M 342.09 116.731 L 241.482 180.049;M 341.831 116.073 L 241.712 179.662;M 341.229 114.57 L 242.231 178.763;M 340.308 112.343 L 242.985 177.402;M 339.085 109.517 L 243.916 175.62;M 337.581 106.214 L 244.962 173.459;M 335.817 102.559 L 246.065 170.968;M 333.826 98.676 L 247.169 168.202;M 331.649 94.686 L 248.224 165.229;M 329.342 90.709 L 249.19 162.128;M 326.972 86.858 L 250.035 158.99;M 324.619 83.246 L 250.741 155.919;M 322.378 79.977 L 251.302 153.031;M 320.351 77.153 L 251.722 150.449;M 318.65 74.87 L 252.014 148.301;M 317.386 73.224 L 252.198 146.717;M 316.67 72.309 L 252.29 145.824;M 316.572 72.185 L 252.291 145.716;M 316.848 72.536 L 252.142 146.219;M 317.391 73.23 L 251.837 147.209;M 318.151 74.215 L 251.387 148.602;M 319.078 75.438 L 250.796 150.312;M 320.124 76.844 L 250.071 152.254;M 321.242 78.38 L 249.222 154.345;M 322.388 79.992 L 248.262 156.509;M 323.525 81.63 L 247.212 158.673;M 324.617 83.242 L 246.098 160.772;M 325.636 84.783 L 244.954 162.749;M 326.558 86.209 L 243.82 164.552;M 327.363 87.478 L 242.744 166.138;M 328.033 88.555 L 241.779 167.467;M 328.554 89.404 L 240.982 168.506;M 328.912 89.993 L 240.409 169.22;M 329.089 90.287 L 240.116 169.576;M 329.379 90.77 L 240.189 169.744;M 330.94 93.437 L 240.765 170.537;M 333.612 98.272 L 241.759 171.995;M 337.025 105.038 L 243.047 174.077;M 340.789 113.494 L 244.494 176.743;M 344.526 123.369 L 245.968 179.939;M 347.906 134.352 L 247.345 183.59;M 350.665 146.096 L 248.524 187.598;M 352.631 158.219 L 249.431 191.842;M 353.731 170.322 L 250.023 196.182;M 353.988 182.003 L 250.299 200.465;M 353.523 192.871 L 250.289 204.532;M 352.531 202.563 L 250.057 208.226;M 351.263 210.742 L 249.693 211.393;M 349.999 217.101 L 249.3 213.887;M 349.016 221.348 L 248.982 215.568;M 348.553 223.195 L 248.829 216.303;M 348.682 222.688 L 248.702 216.1;M 349.217 220.515 L 248.232 215.242;M 350.046 216.881 L 247.451 213.846;M 351.038 211.967 L 246.405 212.032;M 352.053 205.956 L 245.138 209.921;M 352.955 199.04 L 243.693 207.623;M 353.624 191.425 L 242.111 205.241;M 353.968 183.337 L 240.433 202.862;M 353.929 175.016 L 238.7 200.56;M 353.492 166.715 L 236.959 198.398;M 352.691 158.693 L 235.255 196.424;M 351.603 151.216 L 233.641 194.68;M 350.349 144.542 L 232.174 193.195;M 349.083 138.928 L 230.918 191.996;M 347.978 134.62 L 229.937 191.107;M 347.208 131.86 L 229.301 190.55;M 346.925 130.887 L 229.075 190.357"
/>
</path>
</g>
<!-- 三個動畫頂點 -->
<g>
<circle
cx="128.398"
cy="241.16"
r="4.2"
fill="#080c16"
stroke="#ffffff"
stroke-opacity="0.9"
stroke-width="1.5"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="cx"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="128.398;128.607;129.199;130.117;131.301;132.695;134.241;135.882;137.567;139.245;140.869;142.395;143.78;144.985;145.972;146.704;147.142;147.269;147.698;148.677;150.123;151.95;154.072;156.401;158.852;161.343;163.795;166.136;168.299;170.227;171.868;173.177;174.11;174.625;174.68;174.329;173.62;172.582;171.238;169.613;167.738;165.646;163.386;161.013;158.598;156.224;153.985;151.986;150.339;149.158;148.555;148.406;147.873;146.961;145.794;144.498;143.188;141.963;140.892;140.02;139.367;138.926;138.674;138.572;138.575;138.631;138.696;138.731;138.642;138.302;137.748;137.028;136.189;135.275;134.327;133.379;132.461;131.599;130.812;130.116;129.525;129.048;128.695;128.474;128.398"
/>
<animate
attributeName="cy"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="241.16;241.161;241.159;241.151;241.128;241.083;241.011;240.907;240.771;240.605;240.413;240.204;239.99;239.784;239.6;239.456;239.366;239.344;239.372;239.426;239.479;239.497;239.449;239.309;239.06;238.694;238.218;237.649;237.017;236.363;235.732;235.178;234.753;234.508;234.496;234.826;235.479;236.404;237.55;238.863;240.293;241.789;243.302;244.786;246.197;247.495;248.646;249.618;250.381;250.908;251.171;251.014;249.959;248.081;245.521;242.423;238.931;235.188;231.331;227.487;223.774;220.295;217.139;214.387;212.105;210.354;209.196;208.695;208.849;209.501;210.6;212.1;213.961;216.14;218.594;221.271;224.111;227.043;229.981;232.824;235.456;237.746;239.555;240.738;241.16"
/>
</circle>
<circle
cx="167.183"
cy="114.982"
r="4.2"
fill="#080c16"
stroke="#ffffff"
stroke-opacity="0.9"
stroke-width="1.5"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="cx"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="167.183;166.97;166.367;165.427;164.203;162.75;161.124;159.382;157.581;155.779;154.029;152.383;150.89;149.594;148.535;147.753;147.285;147.151;146.701;145.684;144.208;142.387;140.34;138.182;136.024;133.96;132.07;130.41;129.015;127.895;127.043;126.435;126.044;125.844;125.833;126.074;126.57;127.322;128.332;129.6;131.111;132.84;134.744;136.765;138.829;140.854;142.752;144.431;145.799;146.772;147.265;147.35;147.587;148.039;148.717;149.633;150.793;152.187;153.791;155.564;157.448;159.371;161.254;163.009;164.548;165.781;166.623;166.994;166.992;166.941;166.864;166.774;166.684;166.607;166.553;166.529;166.54;166.585;166.661;166.761;166.875;166.989;167.088;167.157;167.183"
/>
<animate
attributeName="cy"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="114.982;114.986;115.003;115.048;115.138;115.294;115.53;115.855;116.273;116.776;117.348;117.964;118.591;119.19;119.719;120.134;120.392;120.469;120.77;121.475;122.563;124.018;125.82;127.939;130.33;132.929;135.658;138.423;141.121;143.644;145.882;147.728;149.078;149.834;149.921;149.459;148.552;147.285;145.747;144.032;142.231;140.432;138.713;137.138;135.754;134.592;133.662;132.957;132.46;132.147;132.001;131.89;131.318;130.307;128.947;127.329;125.546;123.689;121.838;120.066;118.432;116.977;115.732;114.708;113.91;113.331;112.966;112.812;112.746;112.518;112.176;111.784;111.411;111.116;110.948;110.942;111.113;111.458;111.957;112.568;113.237;113.892;114.453;114.84;114.982"
/>
</circle>
<circle
cx="229.075"
cy="190.357"
r="4.2"
fill="#080c16"
stroke="#ffffff"
stroke-opacity="0.9"
stroke-width="1.5"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="cx"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="229.075;229.205;229.573;230.147;230.893;231.777;232.766;233.827;234.927;236.034;237.116;238.142;239.081;239.905;240.585;241.091;241.396;241.482;241.712;242.231;242.985;243.916;244.962;246.065;247.169;248.224;249.19;250.035;250.741;251.302;251.722;252.014;252.198;252.29;252.291;252.142;251.837;251.387;250.796;250.071;249.222;248.262;247.212;246.098;244.954;243.82;242.744;241.779;240.982;240.409;240.116;240.189;240.765;241.759;243.047;244.494;245.968;247.345;248.524;249.431;250.023;250.299;250.289;250.057;249.693;249.3;248.982;248.829;248.702;248.232;247.451;246.405;245.138;243.693;242.111;240.433;238.7;236.959;235.255;233.641;232.174;230.918;229.937;229.301;229.075"
/>
<animate
attributeName="cy"
dur="7s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="0;0.01190476;0.02380952;0.03571429;0.04761905;0.05952381;0.07142857;0.08333333;0.0952381;0.10714286;0.11904762;0.13095238;0.14285714;0.1547619;0.16666667;0.17857143;0.19047619;0.20238095;0.21428571;0.22619048;0.23809524;0.25;0.26190476;0.27380952;0.28571429;0.29761905;0.30952381;0.32142857;0.33333333;0.3452381;0.35714286;0.36904762;0.38095238;0.39285714;0.4047619;0.41666667;0.42857143;0.44047619;0.45238095;0.46428571;0.47619048;0.48809524;0.5;0.51190476;0.52380952;0.53571429;0.54761905;0.55952381;0.57142857;0.58333333;0.5952381;0.60714286;0.61904762;0.63095238;0.64285714;0.6547619;0.66666667;0.67857143;0.69047619;0.70238095;0.71428571;0.72619048;0.73809524;0.75;0.76190476;0.77380952;0.78571429;0.79761905;0.80952381;0.82142857;0.83333333;0.8452381;0.85714286;0.86904762;0.88095238;0.89285714;0.9047619;0.91666667;0.92857143;0.94047619;0.95238095;0.96428571;0.97619048;0.98809524;1"
values="190.357;190.275;190.043;189.672;189.175;188.563;187.847;187.044;186.171;185.25;184.308;183.374;182.485;181.676;180.989;180.465;180.145;180.049;179.662;178.763;177.402;175.62;173.459;170.968;168.202;165.229;162.128;158.99;155.919;153.031;150.449;148.301;146.717;145.824;145.716;146.219;147.209;148.602;150.312;152.254;154.345;156.509;158.673;160.772;162.749;164.552;166.138;167.467;168.506;169.22;169.576;169.744;170.537;171.995;174.077;176.743;179.939;183.59;187.598;191.842;196.182;200.465;204.532;208.226;211.393;213.887;215.568;216.303;216.1;215.242;213.846;212.032;209.921;207.623;205.241;202.862;200.56;198.398;196.424;194.68;193.195;191.996;191.107;190.55;190.357"
/>
</circle>
</g>
<!-- 最上層圓框 -->
<circle
cx="180"
cy="180"
r="174"
fill="none"
stroke="#cde1ff"
stroke-opacity="0.7"
stroke-width="1.4"
vector-effect="non-scaling-stroke"
/>
</svg>https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
代码: 全选
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#ejtricc {
box-sizing: border-box;
width: 100%;
color: #eaf6ff;
background: #070a12;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
/* SVG 預覽區域 */
#ejtricc .ejtricc-preview {
display: block;
width: 100%;
overflow: hidden;
background: #070a12;
}
#ejtricc .ejtricc-preview svg {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
background: #070a12;
}
/* 設定面板 */
#ejtricc .ejtricc-panel {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
box-sizing: border-box;
width: 100%;
padding: 12px;
background: #0d1424;
border-top: 1px solid #243550;
}
#ejtricc .ejtricc-field {
display: flex;
flex-direction: column;
min-width: 0;
gap: 6px;
}
#ejtricc .ejtricc-field label {
color: #b9cae2;
font-size: 13px;
line-height: 1.4;
}
#ejtricc .ejtricc-field input {
box-sizing: border-box;
width: 100%;
min-width: 0;
padding: 9px 10px;
color: #f2f8ff;
background: #121d31;
border: 1px solid #385071;
border-radius: 6px;
font: inherit;
outline: none;
}
#ejtricc .ejtricc-field input:focus {
border-color: #36e2ff;
box-shadow: 0 0 0 2px rgb(54 226 255 / 15%);
}
#ejtricc .ejtricc-actions {
display: flex;
grid-column: 1 / -1;
gap: 10px;
}
#ejtricc .ejtricc-button {
flex: 1;
box-sizing: border-box;
padding: 10px 16px;
color: #eaf6ff;
background: #17233a;
border: 1px solid #385071;
border-radius: 6px;
font: inherit;
font-weight: 600;
cursor: pointer;
}
#ejtricc .ejtricc-button:hover {
background: #213352;
}
#ejtricc .ejtricc-button:active {
background: #101a2c;
}
#ejtricc .ejtricc-generate {
color: #061019;
background: #36e2ff;
border-color: #36e2ff;
}
#ejtricc .ejtricc-generate:hover {
background: #79edff;
}
#ejtricc .ejtricc-info {
grid-column: 1 / -1;
margin: 0;
color: #8195b2;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 520px) {
#ejtricc .ejtricc-panel {
grid-template-columns: 1fr;
}
#ejtricc .ejtricc-actions,
#ejtricc .ejtricc-info {
grid-column: 1;
}
#ejtricc .ejtricc-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div id="ejtricc"></div>
<script>
(() => {
"use strict";
const CANVAS_SIZE = 360;
const CENTER = CANVAS_SIZE / 2;
const CIRCLE_RADIUS = 348 / 2;
const DEFAULT_OPTIONS = {
positionCount: 3,
duration: 5,
fps: 12
};
const COLORS = {
a: "#36e2ff",
b: "#ff4fc8",
c: "#ffd447"
};
function initialize() {
const container = document.querySelector("#ejtricc");
if (!container) {
console.warn("找不到 div#ejtricc");
return;
}
/*
* 清除原來的 Canvas 和其他內容,
* 改成 SVG 預覽及設定面板。
*/
container.replaceChildren();
const preview = document.createElement("div");
preview.className = "ejtricc-preview";
preview.setAttribute("aria-label", "SVG 動畫預覽");
const panel = document.createElement("div");
panel.className = "ejtricc-panel";
panel.innerHTML = `
<div class="ejtricc-field">
<label for="ejtricc-position-count">
不同位置數量(不含起點)
</label>
<input
id="ejtricc-position-count"
class="ejtricc-position-count"
type="number"
min="1"
max="30"
step="1"
value="${DEFAULT_OPTIONS.positionCount}"
>
</div>
<div class="ejtricc-field">
<label for="ejtricc-duration">
動畫用時(秒)
</label>
<input
id="ejtricc-duration"
class="ejtricc-duration"
type="number"
min="1"
max="60"
step="0.1"
value="${DEFAULT_OPTIONS.duration}"
>
</div>
<div class="ejtricc-field">
<label for="ejtricc-fps">
每秒幀數
</label>
<input
id="ejtricc-fps"
class="ejtricc-fps"
type="number"
min="1"
max="60"
step="1"
value="${DEFAULT_OPTIONS.fps}"
>
</div>
<div class="ejtricc-actions">
<button
type="button"
class="ejtricc-button ejtricc-generate"
>
生成
</button>
<button
type="button"
class="ejtricc-button ejtricc-download"
>
導出 SVG
</button>
</div>
<p class="ejtricc-info"></p>
`;
/*
* 預覽放在原 Canvas 的位置,
* 控制面板放在預覽下方。
*/
container.append(preview, panel);
const positionInput = panel.querySelector(
".ejtricc-position-count"
);
const durationInput = panel.querySelector(
".ejtricc-duration"
);
const fpsInput = panel.querySelector(
".ejtricc-fps"
);
const generateButton = panel.querySelector(
".ejtricc-generate"
);
const downloadButton = panel.querySelector(
".ejtricc-download"
);
const infoElement = panel.querySelector(
".ejtricc-info"
);
let currentSVG = "";
function clamp(value, minimum, maximum) {
return Math.min(
maximum,
Math.max(minimum, value)
);
}
function readOptions() {
let positionCount = Number.parseInt(
positionInput.value,
10
);
let duration = Number.parseFloat(
durationInput.value
);
let fps = Number.parseInt(
fpsInput.value,
10
);
if (!Number.isFinite(positionCount)) {
positionCount =
DEFAULT_OPTIONS.positionCount;
}
if (!Number.isFinite(duration)) {
duration = DEFAULT_OPTIONS.duration;
}
if (!Number.isFinite(fps)) {
fps = DEFAULT_OPTIONS.fps;
}
positionCount = clamp(
Math.round(positionCount),
1,
30
);
duration = clamp(
duration,
1,
60
);
fps = clamp(
Math.round(fps),
1,
60
);
/*
* 將清理後的值寫回輸入框。
*/
positionInput.value = positionCount;
durationInput.value = duration;
fpsInput.value = fps;
return {
positionCount,
duration,
fps
};
}
function mix(a, b, t) {
return a + (b - a) * t;
}
function smoothstep(t) {
return t * t * (3 - 2 * t);
}
function distance(p1, p2) {
return Math.hypot(
p2.x - p1.x,
p2.y - p1.y
);
}
function rotatePoint(point, angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return {
x: point.x * cos - point.y * sin,
y: point.x * sin + point.y * cos
};
}
/*
* 每個角至少為 34 度,避免三點共線,
* 也避免生成過於扁平的三角形。
*/
function createRandomState(
rotationBase = Math.random() * Math.PI * 2
) {
const minimumAngle =
34 * Math.PI / 180;
const remainder =
Math.PI - minimumAngle * 3;
const weights = [
0.25 + Math.random(),
0.25 + Math.random(),
0.25 + Math.random()
];
const total =
weights[0] +
weights[1] +
weights[2];
return {
alpha:
minimumAngle +
remainder * weights[0] / total,
beta:
minimumAngle +
remainder * weights[1] / total,
rotation:
rotationBase +
(Math.random() - 0.5) * 1.15
};
}
/*
* 將 angle 轉換為距離 reference 最近的等效角度,
* 避免返回起點時突然旋轉一整圈。
*/
function nearestEquivalentAngle(
angle,
reference
) {
let result = angle;
while (result - reference > Math.PI) {
result -= Math.PI * 2;
}
while (result - reference < -Math.PI) {
result += Math.PI * 2;
}
return result;
}
/*
* 依照三個角和旋轉角度建立三角形。
*/
function buildTriangle(
alpha,
beta,
rotation
) {
const gamma =
Math.PI - alpha - beta;
/*
* 依正弦定理建立相對邊長。
*/
const rawA = Math.sin(alpha);
const rawB = Math.sin(beta);
const rawC = Math.sin(gamma);
const source = {
A: {
x: 0,
y: 0
},
B: {
x: rawC,
y: 0
},
C: {
x: rawB * Math.cos(alpha),
y: rawB * Math.sin(alpha)
}
};
const a = distance(
source.B,
source.C
);
const b = distance(
source.C,
source.A
);
const c = distance(
source.A,
source.B
);
const perimeter = a + b + c;
const semiperimeter = perimeter / 2;
const area = Math.abs(
(source.B.x - source.A.x) *
(source.C.y - source.A.y) -
(source.B.y - source.A.y) *
(source.C.x - source.A.x)
) / 2;
const inradius =
area / semiperimeter;
/*
* 三角形內心。
*/
const incenter = {
x: (
a * source.A.x +
b * source.B.x +
c * source.C.x
) / perimeter,
y: (
a * source.A.y +
b * source.B.y +
c * source.C.y
) / perimeter
};
/*
* 縮放後,三條邊的延長線都能按照指定長度
* 接觸半徑為 174 的圓。
*/
const scale =
CIRCLE_RADIUS /
Math.sqrt(
semiperimeter * semiperimeter +
inradius * inradius
);
function transform(point) {
const centered = {
x:
(point.x - incenter.x) *
scale,
y:
(point.y - incenter.y) *
scale
};
const rotated = rotatePoint(
centered,
rotation
);
return {
x: CENTER + rotated.x,
y: CENTER + rotated.y
};
}
const A = transform(source.A);
const B = transform(source.B);
const C = transform(source.C);
return {
A,
B,
C,
a: distance(B, C),
b: distance(C, A),
c: distance(A, B)
};
}
/*
* 從 point 沿著背離 awayFrom 的方向延長。
*/
function extendFrom(
point,
awayFrom,
length
) {
const dx = point.x - awayFrom.x;
const dy = point.y - awayFrom.y;
const d = Math.hypot(dx, dy);
return {
x: point.x + dx / d * length,
y: point.y + dy / d * length
};
}
/*
* 將三角形轉換成三個點和九條線段。
*/
function getGeometry(triangle) {
const {
A,
B,
C,
a,
b,
c
} = triangle;
/*
* AB 所在直線:
* 圓端點 --a-- A --c-- B --b-- 圓端點
*/
const ABAtA = extendFrom(A, B, a);
const ABAtB = extendFrom(B, A, b);
/*
* BC 所在直線:
* 圓端點 --b-- B --a-- C --c-- 圓端點
*/
const BCAtB = extendFrom(B, C, b);
const BCAtC = extendFrom(C, B, c);
/*
* CA 所在直線:
* 圓端點 --c-- C --b-- A --a-- 圓端點
*/
const CAAtC = extendFrom(C, A, c);
const CAAtA = extendFrom(A, C, a);
return {
points: {
A,
B,
C
},
segments: [
/*
* 長度 a。
*/
{
from: ABAtA,
to: A,
color: COLORS.a
},
{
from: B,
to: C,
color: COLORS.a
},
{
from: A,
to: CAAtA,
color: COLORS.a
},
/*
* 長度 b。
*/
{
from: B,
to: ABAtB,
color: COLORS.b
},
{
from: BCAtB,
to: B,
color: COLORS.b
},
{
from: C,
to: A,
color: COLORS.b
},
/*
* 長度 c。
*/
{
from: A,
to: B,
color: COLORS.c
},
{
from: C,
to: BCAtC,
color: COLORS.c
},
{
from: CAAtC,
to: C,
color: COLORS.c
}
]
};
}
/*
* 建立:
*
* 起始位置
* -> 隨機位置 1
* -> 隨機位置 2
* -> ...
* -> 隨機位置 N
* -> 起始位置
*/
function createPoses(positionCount) {
const initial = createRandomState();
const poses = [initial];
let previous = initial;
for (
let index = 0;
index < positionCount;
index++
) {
const next = createRandomState(
previous.rotation
);
poses.push(next);
previous = next;
}
poses.push({
alpha: initial.alpha,
beta: initial.beta,
rotation: nearestEquivalentAngle(
initial.rotation,
previous.rotation
)
});
return poses;
}
/*
* 取得某個標準化時間 0~1 對應的姿態。
*/
function getPoseAtTime(poses, time) {
const segmentCount =
poses.length - 1;
/*
* time === 1 時直接返回最後一個姿態,
* 也就是起始姿態。
*/
if (time >= 1) {
return {
...poses[poses.length - 1]
};
}
const position =
time * segmentCount;
const index = Math.min(
Math.floor(position),
segmentCount - 1
);
const localProgress =
position - index;
const eased =
smoothstep(localProgress);
const from = poses[index];
const to = poses[index + 1];
return {
alpha: mix(
from.alpha,
to.alpha,
eased
),
beta: mix(
from.beta,
to.beta,
eased
),
rotation: mix(
from.rotation,
to.rotation,
eased
)
};
}
/*
* 建立逐幀幾何資料。
*
* 例如:
* duration = 5
* fps = 12
*
* 會得到 60 個時間區間和 61 個取樣點。
* 第 0 個與第 60 個取樣點相同。
*/
function createFrames(options) {
const {
positionCount,
duration,
fps
} = options;
const intervalCount = Math.max(
1,
Math.round(duration * fps)
);
const poses =
createPoses(positionCount);
const frames = [];
for (
let index = 0;
index <= intervalCount;
index++
) {
const time =
index / intervalCount;
const pose = getPoseAtTime(
poses,
time
);
const triangle = buildTriangle(
pose.alpha,
pose.beta,
pose.rotation
);
frames.push(
getGeometry(triangle)
);
}
return {
frames,
intervalCount
};
}
function svgNumber(value) {
return Number(value.toFixed(3));
}
function createPathD(from, to) {
return [
"M",
svgNumber(from.x),
svgNumber(from.y),
"L",
svgNumber(to.x),
svgNumber(to.y)
].join(" ");
}
/*
* keyTimes 包含首尾:
*
* 0;1/60;2/60;...;59/60;1
*/
function createKeyTimes(intervalCount) {
const values = [];
for (
let index = 0;
index <= intervalCount;
index++
) {
values.push(
Number(
(index / intervalCount).toFixed(8)
)
);
}
return values.join(";");
}
function createPathSVG(
frames,
segmentIndex,
duration,
keyTimes
) {
const first =
frames[0].segments[segmentIndex];
const values = frames
.map(frame => {
const segment =
frame.segments[segmentIndex];
return createPathD(
segment.from,
segment.to
);
})
.join(";");
return `
<path
d="${createPathD(first.from, first.to)}"
fill="none"
stroke="${first.color}"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="d"
dur="${duration}s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="${keyTimes}"
values="${values}"
/>
</path>
`;
}
function createPointSVG(
frames,
pointName,
duration,
keyTimes
) {
const first =
frames[0].points[pointName];
const xValues = frames
.map(frame => {
return svgNumber(
frame.points[pointName].x
);
})
.join(";");
const yValues = frames
.map(frame => {
return svgNumber(
frame.points[pointName].y
);
})
.join(";");
return `
<circle
cx="${svgNumber(first.x)}"
cy="${svgNumber(first.y)}"
r="4.2"
fill="#080c16"
stroke="#ffffff"
stroke-opacity="0.9"
stroke-width="1.5"
vector-effect="non-scaling-stroke"
>
<animate
attributeName="cx"
dur="${duration}s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="${keyTimes}"
values="${xValues}"
/>
<animate
attributeName="cy"
dur="${duration}s"
repeatCount="indefinite"
calcMode="discrete"
keyTimes="${keyTimes}"
values="${yValues}"
/>
</circle>
`;
}
function createAnimatedSVG(options) {
const {
duration,
fps,
positionCount
} = options;
const {
frames,
intervalCount
} = createFrames(options);
const keyTimes =
createKeyTimes(intervalCount);
const paths = frames[0].segments
.map((segment, index) => {
return createPathSVG(
frames,
index,
duration,
keyTimes
);
})
.join("");
const points = ["A", "B", "C"]
.map(pointName => {
return createPointSVG(
frames,
pointName,
duration,
keyTimes
);
})
.join("");
return `
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="360"
height="360"
viewBox="0 0 360 360"
role="img"
aria-label="循環移動的幾何三角形"
data-position-count="${positionCount}"
data-duration="${duration}"
data-fps="${fps}"
>
<defs>
<radialGradient
id="ejtricc-circle-background"
cx="50%"
cy="50%"
r="50%"
>
<stop
offset="0%"
stop-color="#121b2f"
/>
<stop
offset="72%"
stop-color="#0d1424"
/>
<stop
offset="100%"
stop-color="#090e19"
/>
</radialGradient>
<clipPath id="ejtricc-circle-clip">
<circle
cx="${CENTER}"
cy="${CENTER}"
r="${CIRCLE_RADIUS}"
/>
</clipPath>
<filter
id="ejtricc-soft-glow"
x="-20%"
y="-20%"
width="140%"
height="140%"
color-interpolation-filters="sRGB"
>
<feGaussianBlur
stdDeviation="2"
result="blur"
/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 暗色背景 -->
<path
d="M 0 0 H 360 V 360 H 0 Z"
fill="#070a12"
/>
<!-- 直徑 348 的圓形背景 -->
<circle
cx="${CENTER}"
cy="${CENTER}"
r="${CIRCLE_RADIUS}"
fill="url(#ejtricc-circle-background)"
/>
<!-- 九條動畫線段 -->
<g
clip-path="url(#ejtricc-circle-clip)"
filter="url(#ejtricc-soft-glow)"
>
${paths}
</g>
<!-- 三個動畫頂點 -->
<g>
${points}
</g>
<!-- 最上層圓框 -->
<circle
cx="${CENTER}"
cy="${CENTER}"
r="${CIRCLE_RADIUS}"
fill="none"
stroke="#cde1ff"
stroke-opacity="0.7"
stroke-width="1.4"
vector-effect="non-scaling-stroke"
/>
</svg>`.trim();
}
function generate() {
const options = readOptions();
currentSVG =
createAnimatedSVG(options);
/*
* replaceChildren/innerHTML 會建立新的 SVG 節點,
* 因此每次按下生成後,動畫會從頭播放。
*/
preview.innerHTML = currentSVG;
const intervalCount = Math.max(
1,
Math.round(
options.duration * options.fps
)
);
infoElement.textContent =
`已生成 ${options.positionCount} 個不同位置,` +
`由起點移動後再返回起點;` +
`動畫 ${options.duration} 秒,` +
`約 ${options.fps} 幀/秒,` +
`共 ${intervalCount} 個時間區間、` +
`${intervalCount + 1} 組 SVG 座標值。`;
}
function downloadSVG() {
if (!currentSVG) {
generate();
}
const documentSource =
`<?xml version="1.0" encoding="UTF-8"?>\n` +
currentSVG;
const blob = new Blob(
[documentSource],
{
type: "image/svg+xml;charset=utf-8"
}
);
const url =
URL.createObjectURL(blob);
const link =
document.createElement("a");
link.href = url;
link.download =
`triangle-animation-${Date.now()}.svg`;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
}
generateButton.addEventListener(
"click",
generate
);
downloadButton.addEventListener(
"click",
downloadSVG
);
/*
* 按 Enter 也可以重新生成。
*/
panel.addEventListener(
"keydown",
event => {
if (
event.key === "Enter" &&
event.target.matches("input")
) {
event.preventDefault();
generate();
}
}
);
/*
* 頁面初始化時先生成一次預設動畫。
*/
generate();
}
if (document.readyState === "loading") {
document.addEventListener(
"DOMContentLoaded",
initialize,
{
once: true
}
);
} else {
initialize();
}
})();
</script>
</body>
</html>代码: 全选
可以使用 `Blob.size` 計算實際 UTF-8 編碼後的文件大小。為確保顯示的 byte 數與下載文件完全一致,預覽生成時就建立包含 XML 宣告的完整導出內容,下載時直接使用同一份字串。
在上一版 JavaScript 中作以下修改。
### 1. 修改目前 SVG 內容變數
找到:
```js
let currentSVG = "";
```
替換為:
```js
let currentSVG = "";
let currentSVGDocument = "";
let currentSVGByteSize = 0;
```
### 2. 替換 `generate()` 函數
```js
function generate() {
const options = readOptions();
currentSVG = createAnimatedSVG(options);
/*
* 預覽使用不帶 XML 宣告的 SVG。
*/
preview.innerHTML = currentSVG;
/*
* 導出的實際文件包含 XML 宣告。
* Blob.size 是 UTF-8 編碼後的實際 byte 數。
*/
currentSVGDocument =
`<?xml version="1.0" encoding="UTF-8"?>\n` +
currentSVG;
const svgBlob = new Blob(
[currentSVGDocument],
{
type: "image/svg+xml;charset=utf-8"
}
);
currentSVGByteSize = svgBlob.size;
const intervalCount = Math.max(
1,
Math.round(
options.duration * options.fps
)
);
infoElement.textContent =
`已生成 ${options.positionCount} 個不同位置,` +
`由起點移動後再返回起點;` +
`動畫 ${options.duration} 秒,` +
`約 ${options.fps} 幀/秒,` +
`共 ${intervalCount} 個時間區間、` +
`${intervalCount + 1} 組 SVG 座標值;` +
`導出的 SVG 文件大小:${currentSVGByteSize} byte。`;
}
```
### 3. 替換 `downloadSVG()` 函數
```js
function downloadSVG() {
if (!currentSVGDocument) {
generate();
}
/*
* 直接使用生成預覽時計算大小的同一份文件內容,
* 確保下載文件大小與文字顯示的 byte 數一致。
*/
const blob = new Blob(
[currentSVGDocument],
{
type: "image/svg+xml;charset=utf-8"
}
);
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download =
`triangle-animation-${Date.now()}.svg`;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
}
```
下方說明將顯示類似:
```text
已生成 3 個不同位置,由起點移動後再返回起點;動畫 5 秒,約 12 幀/秒,共 60 個時間區間、61 組 SVG 座標值;導出的 SVG 文件大小:43628 byte。
```
其中 byte 數會依隨機生成的座標、位置數量、動畫時長和幀率即時重新計算;每次按下「生成」後都會更新。顯示的是實際下載內容經 UTF-8 編碼後的大小,而不是 JavaScript 字串的字元數。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
寫一個html+css+js
在div#ejpolygonarea內,添加一個canvas,寬480,高540,其中上面480*480部分是暗色,下面高60部分是另一種暗色。上面部分是點格區域,下面部分是選項區域。
在選項中,有10*10,12*12,16*16三個選項。默認為10*10,則在點格區域就會把480劃分成10*10,然後在每個交點的位置上顯示小圓點,排除邊界上的點。其它兩個選項同理。
當點擊點格時,最靠近點擊坐標的點會被選中高亮,再次點擊,則會在這次所點擊的點和上次的點連接一條線段,最後點擊的點始終高亮,起始的點也始終以另一種顏色高亮。
要求所有的線段不能交叉,否則會顯示一條虛線並在一秒後消失。
當點擊一個前面已經選中過的點,則移除這個點以及往後的點和線。當只有一個起始點,再次點擊起始點則移除起始點。當場上有多個點,但都在同一條直線,這時點擊起始點,則移除所有點線。
當場上有多個點,點擊起始點,排除以上的特殊情況,則會形成一個多邊形。這時這個多邊形所有經過的點(之前不一定被選中)都高亮,其內部包含的所有點也用另一種顏色高亮。選項區將變成英文文字說明:「多邊形面積=內部的點的數量+邊上的點的數量÷2-1」,右邊有一個打勾確認按鈕,以及一個下載按鈕。
當點擊下載按鈕,把當前canvas下載為一個png檔,文件名為ejpolygonarea_yyyymmdd_hhiiss.png。當點擊確認按鈕,場上全部清空,選項恢復顯示。
canvas的寬度應始終等同於父元素的寬度。
在div#ejpolygonarea內,添加一個canvas,寬480,高540,其中上面480*480部分是暗色,下面高60部分是另一種暗色。上面部分是點格區域,下面部分是選項區域。
在選項中,有10*10,12*12,16*16三個選項。默認為10*10,則在點格區域就會把480劃分成10*10,然後在每個交點的位置上顯示小圓點,排除邊界上的點。其它兩個選項同理。
當點擊點格時,最靠近點擊坐標的點會被選中高亮,再次點擊,則會在這次所點擊的點和上次的點連接一條線段,最後點擊的點始終高亮,起始的點也始終以另一種顏色高亮。
要求所有的線段不能交叉,否則會顯示一條虛線並在一秒後消失。
當點擊一個前面已經選中過的點,則移除這個點以及往後的點和線。當只有一個起始點,再次點擊起始點則移除起始點。當場上有多個點,但都在同一條直線,這時點擊起始點,則移除所有點線。
當場上有多個點,點擊起始點,排除以上的特殊情況,則會形成一個多邊形。這時這個多邊形所有經過的點(之前不一定被選中)都高亮,其內部包含的所有點也用另一種顏色高亮。選項區將變成英文文字說明:「多邊形面積=內部的點的數量+邊上的點的數量÷2-1」,右邊有一個打勾確認按鈕,以及一個下載按鈕。
當點擊下載按鈕,把當前canvas下載為一個png檔,文件名為ejpolygonarea_yyyymmdd_hhiiss.png。當點擊確認按鈕,場上全部清空,選項恢復顯示。
canvas的寬度應始終等同於父元素的寬度。
代码: 全选
寫一個html+css+js
在div#ejpolygonarea內,添加一個canvas,寬480,高540,其中上面480*480部分是暗色,下面高60部分是另一種暗色。上面部分是點格區域,下面部分是選項區域。
在選項中,有10*10,12*12,16*16三個選項。默認為10*10,則在點格區域就會把480劃分成10*10,然後在每個交點的位置上顯示小圓點,排除邊界上的點。其它兩個選項同理。
當點擊點格時,最靠近點擊坐標的點會被選中高亮,再次點擊,則會在這次所點擊的點和上次的點連接一條線段,最後點擊的點始終高亮,起始的點也始終以另一種顏色高亮。
要求所有的線段不能交叉,否則會顯示一條虛線並在一秒後消失。
當點擊一個前面已經選中過的點,則移除這個點以及往後的點和線。當只有一個起始點,再次點擊起始點則移除起始點。當場上有多個點,但都在同一條直線,這時點擊起始點,則移除所有點線。
當場上有多個點,點擊起始點,排除以上的特殊情況,則會形成一個多邊形。這時這個多邊形所有經過的點(之前不一定被選中)都高亮,其內部包含的所有點也用另一種顏色高亮。選項區將變成英文文字說明:「多邊形面積=內部的點的數量+邊上的點的數量÷2-1」,右邊有一個打勾確認按鈕,以及一個下載按鈕。
當點擊下載按鈕,把當前canvas下載為一個png檔,文件名為ejpolygonarea_yyyymmdd_hhiiss.png。當點擊確認按鈕,場上全部清空,選項恢復顯示。
canvas的寬度應始終等同於父元素的寬度。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
代码: 全选
以下是可直接執行的完整 HTML。Canvas 使用 $480 \times 540$ 的邏輯尺寸,並以 CSS 自動縮放至父元素寬度。
```html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>點格多邊形</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
background: #11151c;
font-family: Arial, "Microsoft JhengHei", sans-serif;
}
#ejpolygonarea {
width: min(100%, 720px);
}
#ejpolygonarea canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 480 / 540;
border: 1px solid #3b4658;
border-radius: 8px;
background: #161b24;
box-shadow: 0 12px 32px rgb(0 0 0 / 35%);
touch-action: none;
cursor: crosshair;
}
</style>
</head>
<body>
<div id="ejpolygonarea"></div>
<script>
(() => {
"use strict";
const LOGICAL_WIDTH = 480;
const LOGICAL_HEIGHT = 540;
const GRID_HEIGHT = 480;
const OPTION_HEIGHT = 60;
const container = document.querySelector("#ejpolygonarea");
const canvas = document.createElement("canvas");
canvas.width = LOGICAL_WIDTH;
canvas.height = LOGICAL_HEIGHT;
canvas.setAttribute("aria-label", "點格多邊形繪圖區");
container.appendChild(canvas);
const ctx = canvas.getContext("2d");
const colors = {
gridBackground: "#151b25",
optionBackground: "#222a37",
optionBorder: "#3d485c",
gridLine: "rgba(255,255,255,0.045)",
point: "#8290a8",
pointShadow: "rgba(155,177,210,0.15)",
line: "#61dafb",
lineGlow: "rgba(97,218,251,0.22)",
startPoint: "#ffb74d",
currentPoint: "#ff5370",
selectedPoint: "#61dafb",
boundaryPoint: "#f7c65c",
interiorPoint: "#70e1a1",
polygonFill: "rgba(70,160,255,0.16)",
invalidLine: "#ff5370",
option: "#344055",
optionHover: "#42516b",
optionActive: "#2f94c7",
optionText: "#f4f7fb",
confirm: "#2eaf72",
download: "#397fd5"
};
let divisions = 10;
let gridPoints = [];
// 使用者按順序選取的控制點
let selectedPoints = [];
// 多邊形完成後的所有邊界格點與內部格點
let boundaryPoints = [];
let interiorPoints = [];
let polygonClosed = false;
let invalidSegment = null;
let invalidTimer = null;
let hoveredControl = null;
const optionControls = [];
const actionControls = [];
function pointKey(point) {
return `${point.col},${point.row}`;
}
function samePoint(a, b) {
return a && b && a.col === b.col && a.row === b.row;
}
function createGrid() {
gridPoints = [];
const step = GRID_HEIGHT / divisions;
// 排除四周邊界,因此使用 1 到 divisions - 1
for (let row = 1; row < divisions; row++) {
for (let col = 1; col < divisions; col++) {
gridPoints.push({
x: col * step,
y: row * step,
col,
row
});
}
}
}
function resetDrawing() {
selectedPoints = [];
boundaryPoints = [];
interiorPoints = [];
polygonClosed = false;
invalidSegment = null;
if (invalidTimer) {
clearTimeout(invalidTimer);
invalidTimer = null;
}
draw();
}
function setDivisions(value) {
divisions = value;
resetDrawing();
createGrid();
draw();
}
function roundedRectPath(x, y, width, height, radius) {
const r = Math.min(radius, width / 2, height / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + width - r, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
ctx.lineTo(x + width, y + height - r);
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
ctx.lineTo(x + r, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawBackground() {
ctx.clearRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
ctx.fillStyle = colors.gridBackground;
ctx.fillRect(0, 0, LOGICAL_WIDTH, GRID_HEIGHT);
ctx.fillStyle = colors.optionBackground;
ctx.fillRect(
0,
GRID_HEIGHT,
LOGICAL_WIDTH,
OPTION_HEIGHT
);
ctx.strokeStyle = colors.optionBorder;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, GRID_HEIGHT + 0.5);
ctx.lineTo(LOGICAL_WIDTH, GRID_HEIGHT + 0.5);
ctx.stroke();
}
function drawGridGuides() {
const step = GRID_HEIGHT / divisions;
ctx.save();
ctx.strokeStyle = colors.gridLine;
ctx.lineWidth = 1;
for (let i = 1; i < divisions; i++) {
const position = Math.round(i * step) + 0.5;
ctx.beginPath();
ctx.moveTo(position, 0);
ctx.lineTo(position, GRID_HEIGHT);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, position);
ctx.lineTo(LOGICAL_WIDTH, position);
ctx.stroke();
}
ctx.restore();
}
function drawCircle(point, radius, fill, stroke = null, lineWidth = 1) {
ctx.beginPath();
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
ctx.fillStyle = fill;
ctx.fill();
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
}
function drawBasePoints() {
ctx.save();
for (const point of gridPoints) {
drawCircle(point, 2.3, colors.pointShadow);
drawCircle(point, 1.45, colors.point);
}
ctx.restore();
}
function drawPolygonFill() {
if (!polygonClosed || selectedPoints.length < 3) {
return;
}
ctx.save();
ctx.beginPath();
ctx.moveTo(selectedPoints[0].x, selectedPoints[0].y);
for (let i = 1; i < selectedPoints.length; i++) {
ctx.lineTo(selectedPoints[i].x, selectedPoints[i].y);
}
ctx.closePath();
ctx.fillStyle = colors.polygonFill;
ctx.fill();
ctx.restore();
}
function drawSegments() {
if (selectedPoints.length < 2) {
return;
}
ctx.save();
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.shadowColor = colors.lineGlow;
ctx.shadowBlur = 8;
ctx.strokeStyle = colors.line;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(selectedPoints[0].x, selectedPoints[0].y);
for (let i = 1; i < selectedPoints.length; i++) {
ctx.lineTo(selectedPoints[i].x, selectedPoints[i].y);
}
if (polygonClosed) {
ctx.closePath();
}
ctx.stroke();
ctx.restore();
}
function drawHighlightedPoints() {
if (polygonClosed) {
// 內部點
for (const point of interiorPoints) {
drawCircle(
point,
5,
colors.interiorPoint,
"#d9ffea",
1.2
);
}
// 邊界上所有格點,包括之前沒有手動點選的點
for (const point of boundaryPoints) {
drawCircle(
point,
5,
colors.boundaryPoint,
"#fff0bb",
1.2
);
}
} else {
for (let i = 0; i < selectedPoints.length; i++) {
const point = selectedPoints[i];
if (i === 0) {
drawCircle(
point,
6.5,
colors.startPoint,
"#fff2d8",
1.5
);
} else if (i === selectedPoints.length - 1) {
drawCircle(
point,
6.5,
colors.currentPoint,
"#ffe1e7",
1.5
);
} else {
drawCircle(
point,
5,
colors.selectedPoint,
"#dff8ff",
1.2
);
}
}
}
}
function drawInvalidSegment() {
if (!invalidSegment) {
return;
}
ctx.save();
ctx.beginPath();
ctx.setLineDash([8, 6]);
ctx.lineWidth = 3;
ctx.lineCap = "round";
ctx.strokeStyle = colors.invalidLine;
ctx.moveTo(invalidSegment.from.x, invalidSegment.from.y);
ctx.lineTo(invalidSegment.to.x, invalidSegment.to.y);
ctx.stroke();
ctx.restore();
}
function drawButton(control, active = false) {
const hovered = hoveredControl === control;
if (active) {
ctx.fillStyle = colors.optionActive;
} else if (hovered) {
ctx.fillStyle = colors.optionHover;
} else {
ctx.fillStyle = control.color || colors.option;
}
roundedRectPath(
control.x,
control.y,
control.width,
control.height,
7
);
ctx.fill();
ctx.strokeStyle = active
? "rgba(255,255,255,0.55)"
: "rgba(255,255,255,0.13)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = colors.optionText;
ctx.font = control.font || "bold 15px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(
control.label,
control.x + control.width / 2,
control.y + control.height / 2 + 0.5
);
}
function drawOptions() {
optionControls.length = 0;
actionControls.length = 0;
if (!polygonClosed) {
const values = [10, 12, 16];
const buttonWidth = 108;
const buttonHeight = 38;
const gap = 18;
const totalWidth =
buttonWidth * values.length + gap * (values.length - 1);
const startX = (LOGICAL_WIDTH - totalWidth) / 2;
const y = GRID_HEIGHT + 11;
values.forEach((value, index) => {
const control = {
type: "division",
value,
label: `${value} × ${value}`,
x: startX + index * (buttonWidth + gap),
y,
width: buttonWidth,
height: buttonHeight
};
optionControls.push(control);
drawButton(control, divisions === value);
});
return;
}
const confirmControl = {
type: "confirm",
label: "✓",
x: 360,
y: GRID_HEIGHT + 11,
width: 45,
height: 38,
color: colors.confirm,
font: "bold 22px Arial"
};
const downloadControl = {
type: "download",
label: "↓ PNG",
x: 413,
y: GRID_HEIGHT + 11,
width: 58,
height: 38,
color: colors.download,
font: "bold 12px Arial"
};
actionControls.push(confirmControl, downloadControl);
ctx.fillStyle = "#f0f4fa";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.font = "12px Arial, Microsoft JhengHei";
ctx.fillText(
"Polygon area = interior points",
12,
GRID_HEIGHT + 20
);
ctx.fillText(
"+ boundary points ÷ 2 − 1",
12,
GRID_HEIGHT + 39
);
drawButton(confirmControl);
drawButton(downloadControl);
}
function draw() {
drawBackground();
drawGridGuides();
drawBasePoints();
drawPolygonFill();
drawSegments();
drawHighlightedPoints();
drawInvalidSegment();
drawOptions();
}
function getCanvasPosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) *
(LOGICAL_WIDTH / rect.width),
y: (event.clientY - rect.top) *
(LOGICAL_HEIGHT / rect.height)
};
}
function isInsideControl(position, control) {
return (
position.x >= control.x &&
position.x <= control.x + control.width &&
position.y >= control.y &&
position.y <= control.y + control.height
);
}
function getControlAt(position) {
const controls = polygonClosed
? actionControls
: optionControls;
return controls.find(control =>
isInsideControl(position, control)
) || null;
}
function nearestGridPoint(position) {
let nearest = null;
let nearestDistanceSquared = Infinity;
for (const point of gridPoints) {
const dx = point.x - position.x;
const dy = point.y - position.y;
const distanceSquared = dx * dx + dy * dy;
if (distanceSquared < nearestDistanceSquared) {
nearestDistanceSquared = distanceSquared;
nearest = point;
}
}
return nearest;
}
function orientation(a, b, c) {
const value =
(b.x - a.x) * (c.y - a.y) -
(b.y - a.y) * (c.x - a.x);
if (Math.abs(value) < 1e-9) {
return 0;
}
return value > 0 ? 1 : -1;
}
function onSegment(a, b, p) {
return (
orientation(a, b, p) === 0 &&
p.x >= Math.min(a.x, b.x) - 1e-9 &&
p.x <= Math.max(a.x, b.x) + 1e-9 &&
p.y >= Math.min(a.y, b.y) - 1e-9 &&
p.y <= Math.max(a.y, b.y) + 1e-9
);
}
function segmentsIntersect(a, b, c, d) {
const o1 = orientation(a, b, c);
const o2 = orientation(a, b, d);
const o3 = orientation(c, d, a);
const o4 = orientation(c, d, b);
if (o1 !== o2 && o3 !== o4) {
return true;
}
if (o1 === 0 && onSegment(a, b, c)) return true;
if (o2 === 0 && onSegment(a, b, d)) return true;
if (o3 === 0 && onSegment(c, d, a)) return true;
if (o4 === 0 && onSegment(c, d, b)) return true;
return false;
}
/*
* 檢查新線段是否和既有線段相交。
* 與相鄰線段共享端點不視為交叉。
*/
function wouldCrossExistingSegments(from, to, closing = false) {
const segmentCount = selectedPoints.length - 1;
for (let i = 0; i < segmentCount; i++) {
const a = selectedPoints[i];
const b = selectedPoints[i + 1];
// 新線段一定與最後一條線共用 from
if (i === segmentCount - 1) {
continue;
}
// 封閉線段會與第一條線共用起始點
if (closing && i === 0) {
continue;
}
if (segmentsIntersect(from, to, a, b)) {
return true;
}
}
return false;
}
function showInvalidSegment(from, to) {
invalidSegment = { from, to };
draw();
if (invalidTimer) {
clearTimeout(invalidTimer);
}
invalidTimer = setTimeout(() => {
invalidSegment = null;
invalidTimer = null;
draw();
}, 1000);
}
function allSelectedPointsCollinear() {
if (selectedPoints.length < 3) {
return true;
}
const a = selectedPoints[0];
const b = selectedPoints[1];
for (let i = 2; i < selectedPoints.length; i++) {
if (orientation(a, b, selectedPoints[i]) !== 0) {
return false;
}
}
return true;
}
function polygonSignedArea(points) {
let sum = 0;
for (let i = 0; i < points.length; i++) {
const next = (i + 1) % points.length;
sum +=
points[i].x * points[next].y -
points[next].x * points[i].y;
}
return sum / 2;
}
function pointInPolygon(point, polygon) {
let inside = false;
for (
let i = 0, j = polygon.length - 1;
i < polygon.length;
j = i++
) {
const a = polygon[i];
const b = polygon[j];
if (onSegment(a, b, point)) {
return false;
}
const intersects =
((a.y > point.y) !== (b.y > point.y)) &&
(
point.x <
((b.x - a.x) * (point.y - a.y)) /
(b.y - a.y) +
a.x
);
if (intersects) {
inside = !inside;
}
}
return inside;
}
function classifyPolygonPoints() {
boundaryPoints = [];
interiorPoints = [];
const boundaryKeys = new Set();
for (const point of gridPoints) {
for (let i = 0; i < selectedPoints.length; i++) {
const a = selectedPoints[i];
const b =
selectedPoints[(i + 1) % selectedPoints.length];
if (onSegment(a, b, point)) {
boundaryKeys.add(pointKey(point));
boundaryPoints.push(point);
break;
}
}
}
for (const point of gridPoints) {
if (boundaryKeys.has(pointKey(point))) {
continue;
}
if (pointInPolygon(point, selectedPoints)) {
interiorPoints.push(point);
}
}
}
function closePolygon() {
const first = selectedPoints[0];
const last = selectedPoints[selectedPoints.length - 1];
if (wouldCrossExistingSegments(last, first, true)) {
showInvalidSegment(last, first);
return;
}
// 防止零面積或所有點共線的圖形
if (Math.abs(polygonSignedArea(selectedPoints)) < 1e-9) {
resetDrawing();
return;
}
polygonClosed = true;
classifyPolygonPoints();
draw();
}
function handleGridClick(position) {
if (polygonClosed) {
return;
}
const point = nearestGridPoint(position);
if (!point) {
return;
}
if (selectedPoints.length === 0) {
selectedPoints.push(point);
draw();
return;
}
const existingIndex = selectedPoints.findIndex(
selected => samePoint(selected, point)
);
if (existingIndex !== -1) {
// 只有起始點,再次點擊起始點:移除起始點
if (
selectedPoints.length === 1 &&
existingIndex === 0
) {
resetDrawing();
return;
}
/*
* 多個點都在同一直線,點擊起始點:
* 移除所有點線。
*/
if (
existingIndex === 0 &&
allSelectedPointsCollinear()
) {
resetDrawing();
return;
}
/*
* 點擊起始點並且不是特殊情況:
* 嘗試封閉多邊形。
*/
if (
existingIndex === 0 &&
selectedPoints.length >= 3
) {
closePolygon();
return;
}
/*
* 點擊已經選取過的非起始點:
* 該點及其後面的點與線全部移除。
*/
selectedPoints = selectedPoints.slice(0, existingIndex);
draw();
return;
}
const last = selectedPoints[selectedPoints.length - 1];
if (wouldCrossExistingSegments(last, point, false)) {
showInvalidSegment(last, point);
return;
}
selectedPoints.push(point);
draw();
}
function pad2(number) {
return String(number).padStart(2, "0");
}
function createDownloadFilename() {
const date = new Date();
const yyyymmdd =
date.getFullYear() +
pad2(date.getMonth() + 1) +
pad2(date.getDate());
const hhiiss =
pad2(date.getHours()) +
pad2(date.getMinutes()) +
pad2(date.getSeconds());
return `ejpolygonarea_${yyyymmdd}_${hhiiss}.png`;
}
function downloadCanvas() {
/*
* 暫時取消按鈕 hover,避免把 hover 狀態下載進圖片。
*/
const previousHover = hoveredControl;
hoveredControl = null;
draw();
const filename = createDownloadFilename();
canvas.toBlob(blob => {
if (!blob) {
hoveredControl = previousHover;
draw();
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
hoveredControl = previousHover;
draw();
}, "image/png");
}
function handleOptionClick(position) {
const control = getControlAt(position);
if (!control) {
return;
}
if (control.type === "division") {
setDivisions(control.value);
return;
}
if (control.type === "confirm") {
resetDrawing();
return;
}
if (control.type === "download") {
downloadCanvas();
}
}
canvas.addEventListener("pointerdown", event => {
if (event.button !== 0 && event.pointerType === "mouse") {
return;
}
event.preventDefault();
const position = getCanvasPosition(event);
if (position.y < GRID_HEIGHT) {
handleGridClick(position);
} else {
handleOptionClick(position);
}
});
canvas.addEventListener("pointermove", event => {
const position = getCanvasPosition(event);
const previous = hoveredControl;
hoveredControl =
position.y >= GRID_HEIGHT
? getControlAt(position)
: null;
canvas.style.cursor = hoveredControl
? "pointer"
: position.y < GRID_HEIGHT && !polygonClosed
? "crosshair"
: "default";
if (previous !== hoveredControl) {
draw();
}
});
canvas.addEventListener("pointerleave", () => {
if (hoveredControl) {
hoveredControl = null;
draw();
}
canvas.style.cursor = "default";
});
createGrid();
draw();
})();
</script>
</body>
</html>
```
功能包括:
- `10 × 10`、`12 × 12`、`16 × 16` 三種點格。
- 排除 Canvas 外圍邊界上的點。
- 自動選取最接近點擊位置的格點。
- 起始點、最後點、一般選取點分色顯示。
- 阻止線段自相交,違規線段以虛線顯示一秒。
- 點擊已選取點可移除該點及其後續路徑。
- 點擊起始點封閉多邊形。
- 自動標記多邊形邊界上的所有格點及內部格點。
- 完成後顯示 Pick 定理英文說明。
- 確認按鈕清空並恢復選項。
- 下載按鈕輸出帶日期時間檔名的 PNG。
- Canvas 透過 `width: 100%` 始終與父元素同寬。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
改進:
把點格的白點變大一些。
把計算面積的文字寫到一行,除了Polygon area之外用實際數字代替,並算出結果,保留最多三位小數。
chatgpt
在下載的png中,不應出現兩個按鈕。
把點格的白點變大一些。
把計算面積的文字寫到一行,除了Polygon area之外用實際數字代替,並算出結果,保留最多三位小數。
chatgpt
代码: 全选
請在原程式中修改以下兩個部分。
### 1. 放大點格中的白點
找到 `drawBasePoints()`,替換為:
```js
function drawBasePoints() {
ctx.save();
for (const point of gridPoints) {
// 加大陰影及白色格點
drawCircle(point, 4, colors.pointShadow);
drawCircle(point, 2.6, colors.point);
}
ctx.restore();
}
```
如需讓白點更接近純白,也可以把 `colors` 中的:
```js
point: "#8290a8",
```
改成:
```js
point: "#d8e0ec",
```
---
### 2. 將面積算式改成單行,顯示實際數字及結果
在 `drawOptions()` 中,找到以下內容:
```js
ctx.fillStyle = "#f0f4fa";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.font = "12px Arial, Microsoft JhengHei";
ctx.fillText(
"Polygon area = interior points",
12,
GRID_HEIGHT + 20
);
ctx.fillText(
"+ boundary points ÷ 2 − 1",
12,
GRID_HEIGHT + 39
);
```
替換為:
```js
const interiorCount = interiorPoints.length;
const boundaryCount = boundaryPoints.length;
const polygonArea =
interiorCount + boundaryCount / 2 - 1;
// 最多保留三位小數,並移除末尾多餘的 0
const formattedArea = Number(
polygonArea.toFixed(3)
).toString();
const areaText =
`Polygon area = ${interiorCount} + ` +
`${boundaryCount} ÷ 2 − 1 = ${formattedArea}`;
ctx.fillStyle = "#f0f4fa";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.font = "11px Arial, Microsoft JhengHei";
ctx.fillText(
areaText,
10,
GRID_HEIGHT + OPTION_HEIGHT / 2,
342
);
```
這樣完成多邊形後會顯示成一行,例如:
```text
Polygon area = 12 + 8 ÷ 2 − 1 = 15
```
或:
```text
Polygon area = 9 + 7 ÷ 2 − 1 = 11.5
```
其中 `fillText()` 的第四個參數 `342` 是文字最大寬度,避免文字覆蓋右側的確認及下載按鈕;當內容較長時,Canvas 會自動壓縮文字寬度以維持單行顯示。代码: 全选
可以在下載前暫時隱藏兩個按鈕,取得 PNG 資料後立即重新繪製,這樣畫面上的按鈕仍然存在,但下載的圖片不會包含按鈕。
### 1. 新增匯出狀態變數
在這段變數附近:
```js
let hoveredControl = null;
```
後面加入:
```js
// 下載圖片時不繪製確認及下載按鈕
let isExporting = false;
```
---
### 2. 修改 `drawOptions()`
在 `drawOptions()` 中,找到:
```js
actionControls.push(confirmControl, downloadControl);
ctx.fillStyle = "#f0f4fa";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.font = "11px Arial, Microsoft JhengHei";
ctx.fillText(
areaText,
10,
GRID_HEIGHT + OPTION_HEIGHT / 2,
342
);
drawButton(confirmControl);
drawButton(downloadControl);
```
替換成:
```js
// 匯出 PNG 時不加入也不繪製按鈕
if (!isExporting) {
actionControls.push(confirmControl, downloadControl);
}
ctx.fillStyle = "#f0f4fa";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.font = "11px Arial, Microsoft JhengHei";
ctx.fillText(
areaText,
10,
GRID_HEIGHT + OPTION_HEIGHT / 2,
isExporting ? 460 : 342
);
if (!isExporting) {
drawButton(confirmControl);
drawButton(downloadControl);
}
```
匯出時文字可使用幾乎完整的選項區寬度,不會再為兩個按鈕預留空間。
---
### 3. 替換 `downloadCanvas()`
將原本的 `downloadCanvas()` 完整替換成:
```js
function downloadCanvas() {
const previousHover = hoveredControl;
// 暫時移除 hover 狀態及兩個操作按鈕
hoveredControl = null;
isExporting = true;
draw();
/*
* toDataURL() 會同步取得目前 Canvas 的圖像,
* 此時確認按鈕及下載按鈕尚未被繪製。
*/
const imageURL = canvas.toDataURL("image/png");
const filename = createDownloadFilename();
// 立即恢復正常畫面
isExporting = false;
hoveredControl = previousHover;
draw();
const link = document.createElement("a");
link.href = imageURL;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
}
```
這樣下載的 PNG 會保留:
- 點格背景
- 多邊形及線段
- 邊界點和內部點
- `Polygon area = ...` 面積算式
但不會包含:
- 打勾確認按鈕
- PNG 下載按鈕https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
在「下載png」的右邊,再增加一個「下載svg」的按鈕,用不同的按鈕顏色,也同樣在下載時去掉所有按鈕,canvas的元素都用svg元素替代。
chatgpt sol (low)
chatgpt sol (low)
代码: 全选
可以新增 SVG 匯出功能。下載的 SVG 會以真正的 `<rect>`、`<line>`、`<polyline>`、`<polygon>`、`<circle>` 和 `<text>` 元素重新建立畫面,不會把 Canvas 轉成點陣圖片,也不會包含三個操作按鈕。
## 1. 增加 SVG 按鈕顏色
在 `colors` 中找到:
```js
confirm: "#2eaf72",
download: "#397fd5"
```
改成:
```js
confirm: "#2eaf72",
download: "#397fd5",
downloadSvg: "#9b59d0"
```
---
## 2. 建立統一的面積文字函式
加入以下函式,之後 Canvas 和 SVG 都使用相同的面積文字:
```js
function getAreaText() {
const interiorCount = interiorPoints.length;
const boundaryCount = boundaryPoints.length;
const polygonArea =
interiorCount + boundaryCount / 2 - 1;
// 最多保留三位小數,移除末尾多餘的 0
const formattedArea = Number(
polygonArea.toFixed(3)
).toString();
return (
`Polygon area = ${interiorCount} + ` +
`${boundaryCount} ÷ 2 − 1 = ${formattedArea}`
);
}
```
---
## 3. 修改完成多邊形後的按鈕區域
在 `drawOptions()` 中,保留未完成多邊形時顯示點格選項的部分,將形成多邊形之後的部分改成以下內容:
```js
const confirmControl = {
type: "confirm",
label: "✓",
x: 315,
y: GRID_HEIGHT + 11,
width: 38,
height: 38,
color: colors.confirm,
font: "bold 20px Arial"
};
const downloadControl = {
type: "download",
label: "↓ PNG",
x: 360,
y: GRID_HEIGHT + 11,
width: 53,
height: 38,
color: colors.download,
font: "bold 11px Arial"
};
const downloadSvgControl = {
type: "download-svg",
label: "↓ SVG",
x: 420,
y: GRID_HEIGHT + 11,
width: 51,
height: 38,
color: colors.downloadSvg,
font: "bold 11px Arial"
};
// 匯出圖片時不加入操作按鈕,也不繪製按鈕
if (!isExporting) {
actionControls.push(
confirmControl,
downloadControl,
downloadSvgControl
);
}
const areaText = getAreaText();
ctx.fillStyle = "#f0f4fa";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.font = "11px Arial, Microsoft JhengHei";
ctx.fillText(
areaText,
10,
GRID_HEIGHT + OPTION_HEIGHT / 2,
isExporting ? 460 : 295
);
if (!isExporting) {
drawButton(confirmControl);
drawButton(downloadControl);
drawButton(downloadSvgControl);
}
```
完成後會有三個不同顏色的按鈕:
- 綠色:確認
- 藍色:下載 PNG
- 紫色:下載 SVG
---
## 4. 修改按鈕點擊處理
在 `handleOptionClick()` 中加入 SVG 下載處理。
完整函式可以改成:
```js
function handleOptionClick(position) {
const control = getControlAt(position);
if (!control) {
return;
}
if (control.type === "division") {
setDivisions(control.value);
return;
}
if (control.type === "confirm") {
resetDrawing();
return;
}
if (control.type === "download") {
downloadCanvas();
return;
}
if (control.type === "download-svg") {
downloadSvg();
}
}
```
---
## 5. 建立 SVG 檔名
在 `createDownloadFilename()` 後面加入:
```js
function createSvgDownloadFilename() {
const date = new Date();
const yyyymmdd =
date.getFullYear() +
pad2(date.getMonth() + 1) +
pad2(date.getDate());
const hhiiss =
pad2(date.getHours()) +
pad2(date.getMinutes()) +
pad2(date.getSeconds());
return `ejpolygonarea_${yyyymmdd}_${hhiiss}.svg`;
}
```
---
## 6. 加入 SVG 產生與下載功能
將以下所有函式加入 JavaScript。可以放在 `downloadCanvas()` 後面。
```js
function escapeSvgText(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function svgNumber(value) {
return Number(value.toFixed(3));
}
function svgCircle(
point,
radius,
fill,
stroke = "none",
strokeWidth = 0
) {
return `
<circle
cx="${svgNumber(point.x)}"
cy="${svgNumber(point.y)}"
r="${radius}"
fill="${fill}"
stroke="${stroke}"
stroke-width="${strokeWidth}"
/>
`;
}
function createSvgContent() {
const svg = [];
svg.push(
`<?xml version="1.0" encoding="UTF-8"?>`
);
svg.push(`
<svg
xmlns="http://www.w3.org/2000/svg"
width="${LOGICAL_WIDTH}"
height="${LOGICAL_HEIGHT}"
viewBox="0 0 ${LOGICAL_WIDTH} ${LOGICAL_HEIGHT}"
role="img"
aria-label="Point grid polygon"
>
`);
/*
* Canvas 陰影由 SVG filter 代替。
* 每個可見物件都使用 SVG 向量元素建立。
*/
svg.push(`
<defs>
<filter
id="lineGlow"
x="-30%"
y="-30%"
width="160%"
height="160%"
>
<feGaussianBlur
stdDeviation="3"
result="blur"
/>
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<clipPath id="gridClip">
<rect
x="0"
y="0"
width="${LOGICAL_WIDTH}"
height="${GRID_HEIGHT}"
/>
</clipPath>
</defs>
`);
// 上方點格背景
svg.push(`
<rect
x="0"
y="0"
width="${LOGICAL_WIDTH}"
height="${GRID_HEIGHT}"
fill="${colors.gridBackground}"
/>
`);
// 下方資訊區背景
svg.push(`
<rect
x="0"
y="${GRID_HEIGHT}"
width="${LOGICAL_WIDTH}"
height="${OPTION_HEIGHT}"
fill="${colors.optionBackground}"
/>
`);
// 上下區域分隔線
svg.push(`
<line
x1="0"
y1="${GRID_HEIGHT + 0.5}"
x2="${LOGICAL_WIDTH}"
y2="${GRID_HEIGHT + 0.5}"
stroke="${colors.optionBorder}"
stroke-width="1"
/>
`);
// 格線
const step = GRID_HEIGHT / divisions;
svg.push(`<g clip-path="url(#gridClip)">`);
for (let i = 1; i < divisions; i++) {
const position = svgNumber(i * step);
svg.push(`
<line
x1="${position}"
y1="0"
x2="${position}"
y2="${GRID_HEIGHT}"
stroke="rgba(255,255,255,0.045)"
stroke-width="1"
/>
`);
svg.push(`
<line
x1="0"
y1="${position}"
x2="${LOGICAL_WIDTH}"
y2="${position}"
stroke="rgba(255,255,255,0.045)"
stroke-width="1"
/>
`);
}
svg.push(`</g>`);
// 所有基礎格點
svg.push(`<g id="grid-points">`);
for (const point of gridPoints) {
svg.push(
svgCircle(
point,
4,
colors.pointShadow
)
);
svg.push(
svgCircle(
point,
2.6,
colors.point
)
);
}
svg.push(`</g>`);
/*
* 多邊形填色。
* SVG polygon 的 points 使用所有手動選中的頂點。
*/
if (polygonClosed && selectedPoints.length >= 3) {
const polygonPoints = selectedPoints
.map(point =>
`${svgNumber(point.x)},${svgNumber(point.y)}`
)
.join(" ");
svg.push(`
<polygon
points="${polygonPoints}"
fill="${colors.polygonFill}"
stroke="none"
/>
`);
}
// 已建立的線段或封閉多邊形邊線
if (selectedPoints.length >= 2) {
const linePoints = selectedPoints
.map(point =>
`${svgNumber(point.x)},${svgNumber(point.y)}`
)
.join(" ");
if (polygonClosed) {
svg.push(`
<polygon
points="${linePoints}"
fill="none"
stroke="${colors.line}"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
filter="url(#lineGlow)"
/>
`);
} else {
svg.push(`
<polyline
points="${linePoints}"
fill="none"
stroke="${colors.line}"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
filter="url(#lineGlow)"
/>
`);
}
}
// 高亮點
if (polygonClosed) {
/*
* 內部點使用與封閉前最後選中點相同的紅色。
*/
svg.push(`<g id="interior-points">`);
for (const point of interiorPoints) {
svg.push(
svgCircle(
point,
6.5,
colors.currentPoint,
"#ffe1e7",
1.5
)
);
}
svg.push(`</g>`);
// 邊界上的所有格點
svg.push(`<g id="boundary-points">`);
for (const point of boundaryPoints) {
svg.push(
svgCircle(
point,
5,
colors.boundaryPoint,
"#fff0bb",
1.2
)
);
}
svg.push(`</g>`);
} else {
// 尚未形成多邊形時的選取點
svg.push(`<g id="selected-points">`);
for (let i = 0; i < selectedPoints.length; i++) {
const point = selectedPoints[i];
if (i === 0) {
svg.push(
svgCircle(
point,
6.5,
colors.startPoint,
"#fff2d8",
1.5
)
);
} else if (i === selectedPoints.length - 1) {
svg.push(
svgCircle(
point,
6.5,
colors.currentPoint,
"#ffe1e7",
1.5
)
);
} else {
svg.push(
svgCircle(
point,
5,
colors.selectedPoint,
"#dff8ff",
1.2
)
);
}
}
svg.push(`</g>`);
}
// 如果下載時剛好有無效線段,使用 SVG 虛線表示
if (invalidSegment) {
svg.push(`
<line
x1="${svgNumber(invalidSegment.from.x)}"
y1="${svgNumber(invalidSegment.from.y)}"
x2="${svgNumber(invalidSegment.to.x)}"
y2="${svgNumber(invalidSegment.to.y)}"
fill="none"
stroke="${colors.invalidLine}"
stroke-width="3"
stroke-linecap="round"
stroke-dasharray="8 6"
/>
`);
}
/*
* 下方只輸出面積文字。
* 確認、PNG、SVG 三個按鈕均不加入 SVG。
*/
if (polygonClosed) {
const areaText = escapeSvgText(getAreaText());
svg.push(`
<text
x="10"
y="${GRID_HEIGHT + OPTION_HEIGHT / 2}"
fill="#f0f4fa"
font-family="Arial, Microsoft JhengHei, sans-serif"
font-size="11"
text-anchor="start"
dominant-baseline="middle"
>${areaText}</text>
`);
}
svg.push(`</svg>`);
return svg.join("");
}
function downloadSvg() {
const svgContent = createSvgContent();
const blob = new Blob(
[svgContent],
{
type: "image/svg+xml;charset=utf-8"
}
);
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = createSvgDownloadFilename();
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
}
```
---
## 7. 保持 PNG 下載時不出現任何按鈕
原本修改過的 `downloadCanvas()` 可以繼續使用:
```js
function downloadCanvas() {
const previousHover = hoveredControl;
hoveredControl = null;
isExporting = true;
draw();
const imageURL = canvas.toDataURL("image/png");
const filename = createDownloadFilename();
isExporting = false;
hoveredControl = previousHover;
draw();
const link = document.createElement("a");
link.href = imageURL;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
}
```
這樣:
- PNG 下載時,三個按鈕都不會出現在圖片中。
- SVG 下載內容不會包含三個按鈕。
- SVG 並非嵌入 PNG,而是使用真正的 SVG 向量元素重建畫面。
- SVG 內的格線、格點、多邊形、線段、高亮點、背景及面積文字在放大後仍保持清晰。
- SVG 檔名格式為 `ejpolygonarea_yyyymmdd_hhiiss.svg`。- 附件
-
ejpolyarea1.html.7z- (5.49 KiB) 已下载 1 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: canvas製作數學動畫
在形成多邊形後,下方面積的式子中,數字的顏色要對應點的顏色。比如有17個邊上的點,則「17」應跟邊上的點的顏色一致(黄色),有8個內部的點,則「8」應跟內部的點的顏色一致。把17÷2放到8的前面。注意svg下載也要作相應更改。
代码: 全选
已按你的要求整理修改方式:
- 面積公式改為:`Polygon area = 17 ÷ 2 + 8 − 1 = ...`
- 邊界點數字(如 `17`)使用邊界點黃色 `colors.boundaryPoint`
- 內部點數字(如 `8`)使用目前內部點的紅色 `colors.currentPoint`
- PNG 下載會沿用 Canvas 的彩色公式
- SVG 下載則使用 `<tspan>` 分段設定相同顏色
請在 `ejpolyarea2.html` 中作以下三處替換。
---
### 1. 在 `drawOptions()` 前加入這個函式
```js
function drawAreaFormula(maxWidth) {
const segments = getAreaSegments();
const baseFontSize = 16;
const fontFamily = 'Arial, Microsoft JhengHei';
ctx.save();
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.font = `${baseFontSize}px ${fontFamily}`;
const naturalWidth = segments.reduce(
(total, segment) => total + ctx.measureText(segment.text).width,
0
);
const fontSize =
naturalWidth > maxWidth
? Math.max(12, (baseFontSize * maxWidth) / naturalWidth)
: baseFontSize;
ctx.font = `${fontSize}px ${fontFamily}`;
let x = 10;
const y = GRID_HEIGHT + OPTION_HEIGHT / 2;
for (const segment of segments) {
ctx.fillStyle = segment.color;
ctx.fillText(segment.text, x, y);
x += ctx.measureText(segment.text).width;
}
ctx.restore();
}
```
---
### 2. 在 `drawOptions()` 裡找到以下程式碼
```js
const areaText = getAreaText();
ctx.fillStyle = '#f0f4fa';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.font = '16px Arial, Microsoft JhengHei';
ctx.fillText(areaText, 10, GRID_HEIGHT + OPTION_HEIGHT / 2, isExporting ? 460 : 295);
```
替換成:
```js
drawAreaFormula(isExporting ? 460 : 295);
```
---
### 3. 替換原本的 `getAreaText()`
把:
```js
function getAreaText() {
const interiorCount = interiorPoints.length;
const boundaryCount = boundaryPoints.length;
const polygonArea = interiorCount + boundaryCount / 2 - 1;
// 最多保留三位小數,移除末尾多餘的 0
const formattedArea = Number(polygonArea.toFixed(3)).toString();
return `Polygon area = ${interiorCount} + ` + `${boundaryCount} ÷ 2 − 1 = ${formattedArea}`;
}
```
替換成:
```js
function getAreaSegments() {
const interiorCount = interiorPoints.length;
const boundaryCount = boundaryPoints.length;
const polygonArea = interiorCount + boundaryCount / 2 - 1;
// 最多保留三位小數,移除末尾多餘的 0
const formattedArea = Number(polygonArea.toFixed(3)).toString();
return [
{
text: 'Polygon area = ',
color: '#f0f4fa'
},
{
text: String(boundaryCount),
color: colors.boundaryPoint
},
{
text: ' ÷ 2 + ',
color: '#f0f4fa'
},
{
text: String(interiorCount),
color: colors.currentPoint
},
{
text: ` − 1 = ${formattedArea}`,
color: '#f0f4fa'
}
];
}
function getAreaText() {
return getAreaSegments()
.map(segment => segment.text)
.join('');
}
```
---
### 4. 修改 SVG 下載中的面積文字
在 `createSvgContent()` 中找到:
```js
if (polygonClosed) {
const areaText = escapeSvgText(getAreaText());
svg.push(`
<text
x="10"
y="${GRID_HEIGHT + OPTION_HEIGHT / 2}"
fill="#f0f4fa"
font-family="Arial, Microsoft JhengHei, sans-serif"
font-size="16"
text-anchor="start"
dominant-baseline="middle"
>${areaText}</text>
`);
}
```
替換成:
```js
if (polygonClosed) {
const areaSegments = getAreaSegments();
const areaTspans = areaSegments
.map(
segment =>
`<tspan fill="${segment.color}">${escapeSvgText(segment.text)}</tspan>`
)
.join('');
svg.push(`
<text
x="10"
y="${GRID_HEIGHT + OPTION_HEIGHT / 2}"
fill="#f0f4fa"
font-family="Arial, Microsoft JhengHei, sans-serif"
font-size="16"
text-anchor="start"
dominant-baseline="middle"
xml:space="preserve"
>${areaTspans}</text>
`);
}
```
修改後,例如有 17 個邊界點、8 個內部點,會顯示:
**Polygon area = `17` ÷ 2 + `8` − 1 = 15.5**
其中 `17` 為黃色,`8` 為內部點所使用的紅色;畫面、PNG 與 SVG 下載內容會保持一致。- 附件
-
ejpolyarea2.html.7z- (6.97 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
在线用户
正浏览此版面之用户: 没有注册用户 和 1 访客