const addButton = document.getElementById('add'); const removeButton = document.getElementById('remove'); const c0Button = document.getElementById('c0'); const c1Button = document.getElementById('c1'); const canvas = document.querySelector('canvas'); const context = canvas.getContext('2d'); let adding = false; /** @type {number|null} */ let selected = null; let mouseDown = false; /** @type {number|null} */ let movePointIndex = null; /** @type {{x: number, y: number}[][]} */ const curves = []; addButton.addEventListener('click', () => { adding = true; curves.push([]); selected = curves.length - 1; }); removeButton.addEventListener('click', () => { if (selected === null) return; curves.splice(selected, 1); selected = null; draw(); }); c0Button.addEventListener('click', () => { if (curves.length <= 1 || selected === null) return; let [ci, pi] = getClosestEndPoint(curves[selected][0]); if (ci === null || pi === null) return; curves[selected][0] = curves[ci][pi]; [ci, pi] = getClosestEndPoint(curves[selected][3]); if (ci === null || pi === null) return; curves[selected][3] = curves[ci][pi]; draw(); }); c1Button.addEventListener('click', () => { if (curves.length <= 1 || selected === null) return; let [ci, pi] = getClosestEndPoint(curves[selected][0]); if (ci === null || pi === null) return; // Move first point of selected curve to closest found point curves[selected][0] = curves[ci][pi]; // Get control point of found end point const handle1 = curves[ci][pi === 0 ? 1 : 2]; // Set control point to location mirrored over the point curves[selected][1] = { x: 2 * curves[selected][0].x - handle1.x, y: 2 * curves[selected][0].y - handle1.y, }; [ci, pi] = getClosestEndPoint(curves[selected][3]); if (ci === null || pi === null) return; curves[selected][3] = curves[ci][pi]; const handle2 = curves[ci][pi === 0 ? 1 : 2]; curves[selected][2] = { x: 2 * curves[selected][3].x - handle2.x, y: 2 * curves[selected][3].y - handle2.y, }; draw(); }); /** * @param {number} x * @param {number} y */ function addPoint(x, y) { const curve = curves[curves.length - 1]; curve.push({ x, y }); if (curve.length === 4) { adding = false; } draw(); } /** * @param {number} t * @param {number} a * @param {number} b * @returns {number} */ function lerp(t, a, b) { return (1 - t) * a + t * b; } /** * interpolates the values * @param {number} t * @param {number[]} v array of values */ function lerpArray(t, v) { if (v.length === 1) return v[0]; return lerp(t, lerpArray(t, v.slice(0, -1)), lerpArray(t, v.slice(1))); } function drawCircles() { if (selected === null) return; for (const { x, y } of curves[selected]) { context.beginPath(); context.ellipse(x, y, 10, 10, 0, 0, Math.PI * 2); context.stroke(); } } // Failed attempt at adaptive line length /** * @param {number} tStart * @param {number} tEnd * @param {{x: number, y: number}[]} points * @returns {{x: number, y: number}[]} */ function calcPoints(tStart, tEnd, points) { const xPoints = points.map((p) => p.x); const yPoints = points.map((p) => p.y); const x1 = lerpArray(tStart, xPoints); const y1 = lerpArray(tStart, yPoints); const x2 = lerpArray((tStart + tEnd) / 2, xPoints); const y2 = lerpArray((tStart + tEnd) / 2, yPoints); const x3 = lerpArray(tEnd, xPoints); const y3 = lerpArray(tEnd, yPoints); const surface = (1 / 2) * Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)); // const d12 = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2); // const d13 = Math.sqrt((x1 - x3) ** 2 + (y1 - y3) ** 2); // const d23 = Math.sqrt((x2 - x3) ** 2 + (y2 - y3) ** 2); // // const angle = Math.acos((d12 ** 2 + d13 ** 2 - d23 ** 2) / (2 * d12 * d13)); if (surface < 20) { return [{ x: x3, y: y3 }]; } return [ ...calcPoints(tStart, (tStart + tEnd) / 2, points), ...calcPoints((tStart + tEnd) / 2, tEnd, points), ]; } function drawLines() { for (let i = 0; i < curves.length; i++) { const curve = curves[i]; for (let j = 0; j < curve.length - 3; j += 3) { const points = curve.slice(j, j + 4); context.beginPath(); if (selected === i) { context.moveTo(points[0].x, points[0].y); context.lineTo(points[1].x, points[1].y); context.moveTo(points[2].x, points[2].y); context.lineTo(points[3].x, points[3].y); } context.moveTo(points[0].x, points[0].y); // // const points = calcPoints(0, 1, curve); // // for (const { x, y } of points) { // context.lineTo(x, y); // } for (let t = 0; t <= 100; t++) { const tt = t / 100; const x = lerpArray( tt, points.map((p) => p.x), ); const y = lerpArray( tt, points.map((p) => p.y), ); context.lineTo(x, y); } context.stroke(); } } } function draw() { context.clearRect(0, 0, canvas.width, canvas.height); drawCircles(); drawLines(); } function resizeCanvas() { canvas.height = window.innerHeight; canvas.width = window.innerWidth; draw(); } /** * Gets the index of the closest curve to the point * @param {number} x * @param {number} y * @returns {number|null} */ function getClosestCurveIndex(x, y) { /** @type {number|null} */ let minDist = null; /** @type {number|null} */ let minIndex = null; for (let i = 0; i < curves.length; i++) { for (const point of curves[i]) { const dist = Math.abs( Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2), ); if (!minDist || dist < minDist) { minDist = dist; minIndex = i; } } } return minIndex; } /** * Gets the selected curve point under the mouse * @param {number} x * @param {number} y * @returns {number|null} */ function getSelectedPoint(x, y) { if (selected === null) return null; for (let i = 0; i < curves[selected].length; i++) { const point = curves[selected][i]; const dist = Math.abs( Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2), ); if (dist <= 10) { return i; } } return null; } /** * Gets the closest end point * @param {{x: number, y: number}} point * @returns {[number|null, number|null]} curveIndex and pointIndex */ function getClosestEndPoint(point) { /** @type {number|null} */ let minDist = null, minCurveIndex = null, minPointIndex = null; for (let i = 0; i < curves.length; i++) { if (i === selected) continue; let dist = Math.abs( Math.sqrt( (point.x - curves[i][0].x) ** 2 + (point.y - curves[i][0].y) ** 2, ), ); if (minDist === null || dist < minDist) { minDist = dist; minCurveIndex = i; minPointIndex = 0; } dist = Math.abs( Math.sqrt( (point.x - curves[i][3].x) ** 2 + (point.y - curves[i][3].y) ** 2, ), ); if (minDist === null || dist < minDist) { minDist = dist; minCurveIndex = i; minPointIndex = 3; } } return [minCurveIndex, minPointIndex]; } canvas.addEventListener('mousedown', (e) => { mouseDown = true; if (adding) { addPoint(e.pageX, e.pageY); return; } const point = getSelectedPoint(e.pageX, e.pageY); if (point !== null) { movePointIndex = point; return; } const i = getClosestCurveIndex(e.pageX, e.pageY); if (i === null) return; selected = i; draw(); }); canvas.addEventListener('mousemove', (e) => { if (!mouseDown) return; if (movePointIndex === null) return; curves[selected][movePointIndex] = { x: e.pageX, y: e.pageY }; draw(); }); canvas.addEventListener('mouseup', () => { mouseDown = false; movePointIndex = null; }); window.addEventListener('resize', resizeCanvas); resizeCanvas();