心臓形の数式はいろいろありますが、係数が変更可能な式に UI を付けたものを作ってみた。
See the Pen 心臓形♡生成 by Ikiuo (@ikiuo) on CodePen.
<!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"> </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"> </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>