import { vec2 } from 'gl-matrix'; import './index.scss'; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d')!; interface Bezier { start: vec2; controlS: vec2; controlE: vec2; end: vec2; } interface DrawBezier extends Bezier { width: number; color: string; } const cursor: vec2 = [0, 0]; let movingPoint: vec2 | undefined; let currentCurve: Bezier | undefined; let currentPointIndex: number | undefined; let handleOffset: vec2 | undefined; let pointOffsets: vec2[] = []; let shiftDown = false; const drawCirclei = (x: number, y: number, radius: number) => { ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI, false); ctx.fill(); ctx.stroke(); }; const drawCirclep = (point: vec2, radius: number) => drawCirclei(point[0], point[1], radius); const drawBezier = (bezier: DrawBezier) => { ctx.strokeStyle = bezier.color; ctx.lineWidth = bezier.width; ctx.beginPath(); ctx.moveTo(bezier.start[0], bezier.start[1]); ctx.bezierCurveTo( bezier.controlS[0], bezier.controlS[1], bezier.controlE[0], bezier.controlE[1], bezier.end[0], bezier.end[1] ); ctx.stroke(); }; const inCircle = (point: vec2, circle: vec2, radius: number) => vec2.dist(point, circle) < radius; const drawHandle = (point: vec2, radius: number) => { ctx.fillStyle = inCircle(cursor, point, radius + 1) ? 'rgba(0, 0, 0, 0.15)' : 'transparent'; ctx.strokeStyle = '#000'; ctx.lineWidth = 1; drawCirclep(point, radius); }; const lines: DrawBezier[] = [ { start: [100, 100], controlS: [250, 250], controlE: [500, 250], end: [650, 100], width: 15, color: '#ff11aa', }, ]; const getControlledBezier = () => { let bezier: Bezier | undefined; let point: vec2 | undefined; let pindex: number | undefined; for (const curve of lines) { if (inCircle(cursor, curve.start, curve.width + 5 + 1)) { bezier = curve; point = curve.start; pindex = 0; } if (inCircle(cursor, curve.end, curve.width + 5 + 1)) { bezier = curve; point = curve.end; pindex = 3; } if (inCircle(cursor, curve.controlS, 10 + 1)) { bezier = curve; point = curve.controlS; pindex = 1; } if (inCircle(cursor, curve.controlE, 10 + 1)) { bezier = curve; point = curve.controlE; pindex = 2; } } return { bezier, point, pindex }; }; const getHandleOffset = () => { if (!currentCurve || !movingPoint) { return; } if (currentPointIndex === 1) { handleOffset = vec2.sub( vec2.create(), currentCurve.start, currentCurve.controlS ); } else if (currentPointIndex === 2) { handleOffset = vec2.sub( vec2.create(), currentCurve.end, currentCurve.controlE ); } else { handleOffset = undefined; if (currentPointIndex === 0 || currentPointIndex === 3) { pointOffsets = [ vec2.sub(vec2.create(), currentCurve.start, movingPoint), vec2.sub(vec2.create(), currentCurve.controlS, movingPoint), vec2.sub(vec2.create(), currentCurve.controlE, movingPoint), vec2.sub(vec2.create(), currentCurve.end, movingPoint), ]; } } }; const relativeMove = () => { if (!currentCurve || !movingPoint) { return; } if (currentPointIndex === 1) { vec2.add(currentCurve.start, movingPoint, handleOffset!); } else if (currentPointIndex === 2) { vec2.add(currentCurve.end, movingPoint, handleOffset!); } else { vec2.add(currentCurve.start, pointOffsets[0], movingPoint); vec2.add(currentCurve.controlS, pointOffsets[1], movingPoint); vec2.add(currentCurve.controlE, pointOffsets[2], movingPoint); vec2.add(currentCurve.end, pointOffsets[3], movingPoint); } }; function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.lineCap = 'round'; lines.forEach((item) => { drawBezier(item); drawHandle(item.start, item.width + 5); drawHandle(item.end, item.width + 5); drawHandle(item.controlS, 10); drawHandle(item.controlE, 10); }); } function loop() { requestAnimationFrame(loop); draw(); } canvas.addEventListener('mousemove', (ev) => { cursor[0] = ev.clientX; cursor[1] = ev.clientY; if (movingPoint) { if (shiftDown && currentCurve) { getHandleOffset(); } vec2.copy(movingPoint, cursor); if (shiftDown && currentCurve) { relativeMove(); } } }); canvas.addEventListener('mousedown', (ev) => { const { point, bezier, pindex } = getControlledBezier(); if (point && bezier) { movingPoint = point; currentCurve = bezier; currentPointIndex = pindex; } }); canvas.addEventListener('mouseup', () => { movingPoint = undefined; }); window.addEventListener('keydown', (ev) => { if (ev.key === 'Shift') { shiftDown = true; } }); window.addEventListener('keyup', (ev) => { if (ev.key === 'Shift') { shiftDown = false; } }); resize(); window.addEventListener('resize', resize); document.body.appendChild(canvas); loop();