From bab3208e61972851a5e609930a05e0d4322f8a06 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Wed, 7 Aug 2024 11:48:07 +0200 Subject: [PATCH] Add bezier demo --- cg/bezier/bezier.js | 197 +++++++++++++++++++++++++++++++++++++++++++ cg/bezier/index.html | 44 ++++++++++ 2 files changed, 241 insertions(+) create mode 100644 cg/bezier/bezier.js create mode 100644 cg/bezier/index.html diff --git a/cg/bezier/bezier.js b/cg/bezier/bezier.js new file mode 100644 index 0000000..853b7d0 --- /dev/null +++ b/cg/bezier/bezier.js @@ -0,0 +1,197 @@ +(function () { + function fillCanvas (element, draw) { + var canvas = document.createElement("canvas"); + var ctx = canvas.getContext("2d"); + + var windowToCanvas; + function resize () { + var vw = element.clientWidth; + var vh = element.clientHeight; + canvas.width = vw; + canvas.height = vh; + + var vpMin = Math.min(vw, vh); + + // cartesion coordinates [0, 100] on both x and y axes + ctx.setTransform( + vpMin / 100, 0, + 0, -vpMin / 100, + (vw - vpMin) / 2, (vh - vpMin) / 2 + vh + ); + windowToCanvas = ctx.getTransform().inverse(); + } + new ResizeObserver(resize).observe(element); + resize(); + + function render () { + draw(ctx, canvas.width, canvas.height, windowToCanvas); + requestAnimationFrame(render); + } + render(); + + element.appendChild(canvas); + } + + const Vec2 = { + add: (v1, v2) => [v1[0] + v2[0], v1[1] + v2[1]], + sub: (v1, v2) => [v1[0] - v2[0], v1[1] - v2[1]], + scale: (s, v) => [s * v[0], s * v[1]], + magnitude: (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1]), + distance: (v1, v2) => Vec2.magnitude(Vec2.sub(v1, v2)), + normalize: (v) => Vec2.scale(1 / Vec2.magnitude(v), v), + transform: (m, v) => { + return [ + m.a * v[0] + m.c * v[1] + m.e, + m.b * v[1] + m.d * v[1] + m.f + ]; + }, + dot: (v1, v2) => [v1[0] * v2[0] + v1[1] * v2[1]], + triangleArea: (p1, p2, p3) => { + // Cramers rule for signed area (necessary to get negative lambdas) + var [x1, y1] = p1; + var [x2, y2] = p2; + var [x3, y3] = p3; + + return (x1 * y2 - x1 * y3 - x2 * y1 + x3 * y1 + x2 * y3 - x3 * y2) / 2; + } + }; + + var ptr = null; + window.addEventListener("mousemove", function (e) { + ptr = [e.clientX, e.clientY]; + }); + + function drawPath (ctx, path) { + for (var i = 1; i < path.length; i++) { + ctx.beginPath(); + ctx.moveTo(path[i - 1][0], path[i - 1][1]); + ctx.lineTo(path[i][0], path[i][1]); + ctx.stroke(); + } + + for (var i = 0; i < path.length; i++) { + ctx.beginPath(); + ctx.arc(path[i][0], path[i][1], 1, 0, Math.PI * 2); + ctx.fill(); + } + } + + function drawCurve (ctx, curve, u, depth) { + var pointsOnCurve = drawPolygons(curve, u, depth); + pointsOnCurve.splice(0, 0, curve[0]); + pointsOnCurve.push(curve[curve.length - 1]); + + ctx.fillStyle = "blue"; + ctx.strokeStyle = "blue"; + ctx.lineWidth = 0.5; + drawPath(ctx, pointsOnCurve, true); + + function drawPolygons (curve, u, depth) { + if (depth <= 0) { + return []; + } + + ctx.fillStyle = "#555"; + ctx.strokeStyle = "#555"; + ctx.lineWidth = 0.25; + drawPath(ctx, curve, true); + + var b = [curve]; + for (var i = 1; i < curve.length; i++) { + var midpoints = []; + for (var k = 0; k < curve.length - i; k++) { + midpoints.push(Vec2.add( + Vec2.scale(u, b[i - 1][k]), + Vec2.scale(1 - u, b[i - 1][k + 1]) + )); + } + drawPath(ctx, midpoints); + b.push(midpoints); + } + + var pointsOnCurve = []; + var c0 = b.map(function (bi) { + return bi[0]; + }); + pointsOnCurve = pointsOnCurve.concat(drawPolygons(c0, u, depth - 1)); + + pointsOnCurve.push(b[b.length - 1][0]); + + var c1 = b.map(function (bi) { + return bi[bi.length - 1]; + }); + pointsOnCurve = pointsOnCurve.concat(drawPolygons(c1, 1 - u, depth - 1).toReversed()); + + return pointsOnCurve; + } + } + + var selected = 0; + var curve = [ + [10, 10], + [10, 90], + [90, 10], + [90, 90] + ]; + var depth = 5; + + window.addEventListener("keydown", function (e) { + switch (e.key) { + case "j": + selected = (selected + 1) % curve.length; + break; + case "k": + selected = (curve.length + selected - 1) % curve.length; + break; + case "ArrowLeft": + curve[selected][0]--; + break; + case "ArrowRight": + curve[selected][0]++; + break; + case "ArrowUp": + curve[selected][1]++; + break; + case "ArrowDown": + curve[selected][1]--; + break; + case "h": + depth = Math.max(0, depth - 1); + break; + case "l": + depth++; + break; + case "?": + document.querySelector(".legend").classList.toggle("--hidden"); + break; + } + }); + + fillCanvas(document.querySelector(".canvas"), function (ctx, vw, vh, w2c) { + var origin = Vec2.transform(w2c, [0, 0]); + var omega = Vec2.transform(w2c, [vw, vh]); + ctx.fillStyle = "black"; + ctx.fillRect(origin[0], origin[1], omega[0] - origin[0], omega[1] - origin[1]) + + drawCurve(ctx, curve, 0.5 + 0.45 * Math.cos(Math.PI * 2 * performance.now() / 5000), depth); + + /* + ctx.beginPath(); + ctx.moveTo(curve[0][0], curve[0][1]); + ctx.bezierCurveTo( + curve[1][0], curve[1][1], + curve[2][0], curve[2][1], + curve[3][0], curve[3][1] + ); + ctx.strokeStyle = "red"; + ctx.lineWidth = 0.25; + ctx.stroke(); + */ + + ctx.beginPath(); + ctx.arc(curve[selected][0], curve[selected][1], 2, 0, Math.PI * 2); + ctx.strokeStyle = "green"; + ctx.lineWidth = 0.5; + ctx.stroke(); + }); +})(); diff --git a/cg/bezier/index.html b/cg/bezier/index.html new file mode 100644 index 0000000..6f60931 --- /dev/null +++ b/cg/bezier/index.html @@ -0,0 +1,44 @@ + + + + + bezier + + + +
+
j, k: Select vertex +h, l: Select recursion depth +⬅⬆⬇➡: Move selected vertex +?: hide/unhide this tooltip
+ + +