From dc8cea6a059a934c45358449ee9dc8f3df3111ba Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 29 Jul 2023 06:56:47 +0000 Subject: [PATCH] Add 'bezier.ts' --- bezier.ts | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 bezier.ts diff --git a/bezier.ts b/bezier.ts new file mode 100644 index 0000000..67fe88a --- /dev/null +++ b/bezier.ts @@ -0,0 +1,223 @@ +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();