心臓形♡画像の生成（微調整可）

心臓形の数式はいろいろありますが、係数が変更可能な式に UI を付けたものを作ってみた。

<p class="codepen" data-height="1040" data-default-tab="result" data-slug-hash="EaYZLQN" data-pen-title="心臓形♡生成" data-user="ikiuo" style="height: 1040px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ikiuo/pen/EaYZLQN">
  心臓形♡生成</a> by Ikiuo (<a href="https://codepen.io/ikiuo">@ikiuo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

```HTML:
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>心臓形♡画像生成</title>
    <style>
     body {
         width: 40em;
         margin: auto;
         background-color: white;
     }
     .center {
         text-align: center;
     }
     .right {
         text-align: right;
     }
     .large {
         font-size: large;
     }
     .slider {
         width: 20em;
     }
    </style>
  </head>
  <body>
    <table>
      <tr><th><h2>心臓形<span style="color: red;">♡</span>画像生成</h2></th></tr>

      <tr>
        <td colspan="3" class="center">𝑡 を媒介変数として以下の式で左右対称の図形を生成します</td>
      </tr>
      <tr>
        <td>
          <table style="margin: auto">
            <tr>
              <td>𝑥</td>
              <td>=</td>
              <td>𝑥<sub>1</sub> 𝐬𝐢𝐧 𝑡 + 𝑥<sub>2</sub> 𝐬𝐢𝐧<sup>2</sup> 𝑡 + 𝑥<sub>3</sub> 𝐬𝐢𝐧<sup>3</sup> 𝑡</td>
            </tr>
            <tr>
              <td>𝑦</td>
              <td>=</td>
              <td>𝑦<sub>1</sub> 𝐜𝐨𝐬 𝑡 + 𝑦<sub>2</sub> 𝐜𝐨𝐬 2𝑡 + 𝑦<sub>3</sub> 𝐜𝐨𝐬 3𝑡 + 𝑦<sub>4</sub> 𝐜𝐨𝐬 4𝑡</td>
            </tr>
          </table>
        </td>
      </tr>

      <tr>
        <td>
          <table id="tagParameter" style="margin: auto;">
            <!-- auto -->

            <tr>
              <td class="right">頂点:</td>
              <td>
                <input id="tagResolution" name="tagResolution" class="slider"
                       type="range" min="3" max="500" step="1" value="100"
                       oninput="onUpdate()">
              </td>
              <td id="tagResVal" class="center">&nbsp;</td>
            </tr>

            <tr>
              <td class="center" colspan="3">
                <button onclick="onPreset(0)">円</button>
                <button onclick="onPreset(1)">♡形1</button>
                <button onclick="onPreset(2)">♡形2</button>
              </td>
            </tr>
          </table>
        </td>
      </tr>

      <tr>
        <td class="center" style="border: solid 1px lightgray;">
          <canvas id="tagCanvas" hidden></canvas>
          <img id="tagImage">
        </td>
      </tr>

    </table>

    <script>

     const backgroundColor = "#0000";
     const heartColor = "red";
     const drawSize = 512;

     const getTag = ((id) => document.getElementById(id));
     const tagSinCoeff = [...Array(3)].map((_, n) => 'tagSinCoeff' + (n + 1));
     const tagCosCoeff = [...Array(4)].map((_, n) => 'tagCosCoeff' + (n + 1));

     function createHeartCurve(res, coeff) {
         const PI = Math.PI;
         const [se, ce] = coeff
         const [se1, se2, se3] = se;
         const [ce1, ce2, ce3, ce4] = ce;

         let l = [];
         for (var p = 0; p <= res; p++) {
             const q1 = p / res;
             const x = q1 * PI;

             const sv1 = Math.sin(x);
             const sv2 = sv1 * sv1;
             const sv3 = sv1 * sv2;

             const cv1 = Math.cos(x);
             const cv2 = Math.cos(x * 2);
             const cv3 = Math.cos(x * 3);
             const cv4 = Math.cos(x * 4);

             l.push([
                 se1 * sv1 + se2 * sv2 + se3 * sv3,
                 ce1 * cv1 + ce2 * cv2 + ce3 * cv3 + ce4 *cv4
             ]);
         }
         const point = [];
         l.forEach(v => point.push(v));
         l.reverse();
         l.forEach(v => point.push([-v[0], v[1]]));
         return point;
     }

     function drawShape(tag, point) {
         const xlist = point.map((v) => v[0]);
         const ylist = point.map((v) => v[1]);
         const xmin = Math.min(...xlist);
         const xmax = Math.max(...xlist);
         const ymin = Math.min(...ylist);
         const ymax = Math.max(...ylist);
         const swidth = xmax - xmin;
         const sheight = ymax - ymin;

         const scale = drawSize / Math.max(swidth, sheight);
         const xscale = scale * swidth;
         const yscale = scale * sheight;

         const margin = 4;
         tag.width = Math.max(drawSize, xscale) + margin * 2;
         tag.height = Math.max(drawSize, yscale) + margin * 2;

         const ctx = tag.getContext("2d");

         ctx.fillStyle = backgroundColor;
         ctx.fillRect(0, 0, tag.width, tag.height);

         const dcx = tag.width / 2;
         const dcy = tag.height / 2;

         const ecx = (xmin + xmax) / 2;
         const ecy = (ymin + ymax) / 2;

         ctx.lineWidth = 1;
         ctx.beginPath();
         ctx.moveTo(0, 0);
         point.forEach(
             v => ctx.lineTo(dcx + (v[0] - ecx) * scale,
                             dcy - (v[1] - ecy) * scale));
         ctx.closePath();

         ctx.fillStyle = heartColor;
         ctx.fill();
     }

     function setParam(index) {
         const table = [
             [[1.0, 0.0,  0.0], [ 1.0,  0.0,  0.0,  0.0]],
             [[3.0, 2.0, 12.0], [16.0, -4.0, -2.5,  0.0]],
             [[0.0, 0.0, 12.0], [11.0, -4.5, -1.5, -0.5]],
         ];
         const param = table[index];
         tagSinCoeff.forEach((v, n) => getTag(v).value = param[0][n]);
         tagCosCoeff.forEach((v, n) => getTag(v).value = param[1][n]);
     }

     function onUpdate() {
         function setNumber(tag) {
             const nn = Number(getTag(tag).value);
             const sd = '' + Math.trunc(Math.abs(nn) * 100);
             const sn = sd.length < 3 ? ('000' + sd).slice(-3):  sd;
             const st = sn.slice(0, -2) + '.' + sn.slice(-2);
             const sz = '00000'.slice(0, 5 - st.length);
             const ss = (nn < 0 ? '-' : '+') + sz + st;
             getTag(tag + 'Value').textContent = ss;
             return nn;
         }

         const res = tagResolution.value;
         tagResVal.textContent = res * 2;

         const se = tagSinCoeff.map((v, n) => setNumber(v));
         const ce = tagCosCoeff.map((v, n) => setNumber(v));

         const canvas = getTag('tagCanvas')
         drawShape(canvas, createHeartCurve(res, [se, ce]));
         getTag('tagImage').src = canvas.toDataURL('image/png');
     }

     function onPreset(index) {
         setParam(index);
         onUpdate();
     }

     window.onload = (() => {
         const slider = ((tagname, varname, index, vmin, vmax) => [
             '<tr>',
             `<td class="right">${varname}<sub>${index}</sub>:</td>`,
             '<td>',
             `<input id="${tagname}${index}" name="${tagname}${index}" class="slider"`,
             ` type="range" min="${vmin}" max="${vmax}" step="0.01"`,
             ' oninput="onUpdate()">',
             '</td>',
             `<td id="${tagname}${index}Value" class="right">&nbsp;</td>`,
             '</tr>'
         ].join(''));

         getTag('tagParameter').insertAdjacentHTML('afterbegin', [
             [[0, 50], [0, 50], [0, 50]].map((v, n) => slider('tagSinCoeff', '𝑥', n+1, ...v)),
             [[0.01, 30], [-20, 5], [-10, 5], [-10, 5]].map((v, n) => slider('tagCosCoeff', '𝑦', n+1, ...v)),
         ].flat().join(''));

         onPreset(1);
     })

    </script>
  </body>
</html>
```