From 7259b590bab01a7d1e34380b7e9c195ebea2bf81 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Mon, 13 Mar 2023 16:48:16 +0100 Subject: [PATCH] Bilanced simulation to make knots not untangle by themself --- frontend/lib/math/math.js | 125 +++--- frontend/src/components/KnotLayer.jsx | 539 +++++++++++++++++--------- frontend/src/pages.jsx | 6 +- frontend/styles/main.scss | 4 + package.json | 2 +- 5 files changed, 427 insertions(+), 249 deletions(-) diff --git a/frontend/lib/math/math.js b/frontend/lib/math/math.js index bd56ba4..ded34d0 100644 --- a/frontend/lib/math/math.js +++ b/frontend/lib/math/math.js @@ -1,137 +1,146 @@ export const Vec2 = { Mutate: { add(target, [x, y]) { - target[0] += x - target[1] += y + target[0] += x; + target[1] += y; - return target + return target; }, scale(target, factor) { - target[0] *= factor - target[1] *= factor + target[0] *= factor; + target[1] *= factor; - return target + return target; }, normalize(target) { - const norm = Vec2.norm2([x, y]) - target[0] /= norm - target[1] /= norm + const norm = Vec2.norm2([x, y]); + target[0] /= norm; + target[1] /= norm; - return target + return target; }, set(target, [x, y]) { - target[0] = x - target[1] = y - return target + target[0] = x; + target[1] = y; + return target; }, }, perpendicular([x, y]) { - return [-y, x] + return [-y, x]; }, distance2(v1, v2) { - return Vec2.norm2(Vec2.sub(v1, v2)) + return Vec2.norm2(Vec2.sub(v1, v2)); }, add([x1, y1], [x2, y2]) { - return [x1 + x2, y1 + y2] + return [x1 + x2, y1 + y2]; }, sub([x1, y1], [x2, y2]) { - return [x1 - x2, y1 - y2] + return [x1 - x2, y1 - y2]; }, dot([x1, y1], [x2, y2]) { - return x1 * x2 + y1 * y2 + return x1 * x2 + y1 * y2; }, scale([x, y], factor) { - return [x * factor, y * factor] + return [x * factor, y * factor]; }, norm2([x, y]) { - return Math.sqrt(x ** 2 + y ** 2) + return Math.sqrt(x ** 2 + y ** 2); }, normalize([x, y]) { - const norm = Vec2.norm2([x, y]) - return [x / norm, y / norm] + const norm = Vec2.norm2([x, y]); + return [x / norm, y / norm]; }, findNearest(curve, pt) { - let index = -1 - let minDist = +Infinity + let index = -1; + let minDist = +Infinity; for (let i = 0; i < curve.length; i++) { - const d = Vec2.distance2(curve[i], pt) + const d = Vec2.distance2(curve[i], pt); if (d < minDist) { - index = i - minDist = d + index = i; + minDist = d; } } - return [index, minDist] + return [index, minDist]; }, -} +}; export const Vec3 = { Mutate: { add(target, [x, y, z]) { - target[0] += x - target[1] += y - target[2] += z + target[0] += x; + target[1] += y; + target[2] += z; - return target + return target; }, scale(target, factor) { - target[0] *= factor - target[1] *= factor - target[2] *= factor + target[0] *= factor; + target[1] *= factor; + target[2] *= factor; - return target + return target; }, normalize(target) { - const norm = Vec3.norm2(target) - target[0] /= norm - target[1] /= norm - target[2] /= norm + const norm = Vec3.norm2(target); + target[0] /= norm; + target[1] /= norm; + target[2] /= norm; - return target + return target; }, }, + withMaxLength(v, max) { + const d = Vec3.norm2(v); + + if (d > max) { + return Vec3.scale(v, max / d); + } + + return v; + }, distance2(v1, v2) { - return Vec3.norm2(Vec3.sub(v1, v2)) + return Vec3.norm2(Vec3.sub(v1, v2)); }, add([x1, y1, z1], [x2, y2, z2]) { - return [x1 + x2, y1 + y2, z1 + z2] + return [x1 + x2, y1 + y2, z1 + z2]; }, sub([x1, y1, z1], [x2, y2, z2]) { - return [x1 - x2, y1 - y2, z1 - z2] + return [x1 - x2, y1 - y2, z1 - z2]; }, dot([x1, y1, z1], [x2, y2, z2]) { - return x1 * x2 + y1 * y2 + z1 * z2 + return x1 * x2 + y1 * y2 + z1 * z2; }, scale([x, y, z], factor) { - return [x * factor, y * factor, z * factor] + return [x * factor, y * factor, z * factor]; }, norm2([x, y, z]) { - return Math.sqrt(x ** 2 + y ** 2 + z ** 2) + return Math.sqrt(x ** 2 + y ** 2 + z ** 2); }, normalize([x, y, z]) { - const norm = Vec3.norm2([x, y, z]) - return [x / norm, y / norm, z / norm] + const norm = Vec3.norm2([x, y, z]); + return [x / norm, y / norm, z / norm]; }, findNearest(curve, pt) { - let index = -1 - let minDist = +Infinity + let index = -1; + let minDist = +Infinity; for (let i = 0; i < curve.length; i++) { - const d = Vec3.distance2(curve[i], pt) + const d = Vec3.distance2(curve[i], pt); if (d < minDist) { - index = i - minDist = d + index = i; + minDist = d; } } - return [index, minDist] + return [index, minDist]; }, -} +}; export function clamp(min, value, max) { - return Math.min(max, Math.max(min, value)) + return Math.min(max, Math.max(min, value)); } diff --git a/frontend/src/components/KnotLayer.jsx b/frontend/src/components/KnotLayer.jsx index 82ba868..297dec4 100644 --- a/frontend/src/components/KnotLayer.jsx +++ b/frontend/src/components/KnotLayer.jsx @@ -1,171 +1,310 @@ -import { useEffect, useRef, useState } from 'preact/hooks' -import { enforceDistance } from '../../lib/math/constraint.js' -import { resampleCurve } from '../../lib/math/curves.js' -import { Vec2, Vec3 } from '../../lib/math/math.js' -import { MODE_DRAW, MODE_FLIP, MODE_DRAG } from '../pages.jsx' +import { useEffect, useRef, useState } from "preact/hooks"; +import { enforceDistance } from "../../lib/math/constraint.js"; +import { resampleCurve } from "../../lib/math/curves.js"; +import { Vec2, Vec3 } from "../../lib/math/math.js"; +import { MODE_DRAW, MODE_FLIP, MODE_DRAG, MODE_BUTTONS } from "../pages.jsx"; function mod(i, modulus) { - const r = i % modulus - return r < 0 ? r + modulus : r + const r = i % modulus; + return r < 0 ? r + modulus : r; } -function createSimulation3d(positions, velocities, accelerations) { +// function createSimulation3d(positions, velocities, accelerations) { +// return { +// positions, +// velocities, +// accelerations, + +// update(newAccelerations) { +// const n = this.positions.length +// const newPositions = Array.from({ length: n }, () => []) +// const newVelocities = Array.from({ length: n }, () => []) + +// for (let i = 0; i < n; i++) { +// newPositions[i] = Vec3.add(Vec3.add(this.positions[i], this.velocities[i]), Vec3.scale(this.accelerations[i], 0.5)) +// newVelocities[i] = Vec3.add(this.velocities[i], Vec3.scale(Vec3.add(this.accelerations[i], newAccelerations[i]), 0.5)) +// } + +// this.positions = newPositions +// this.velocities = newVelocities +// this.accelerations = newAccelerations +// }, +// } +// } + +function createSimulation3dMaxDisplacement( + positions, + velocities, + accelerations, + maxDisplacement +) { return { positions, velocities, accelerations, update(newAccelerations) { - const n = this.positions.length - const newPositions = Array.from({ length: n }, () => []) - const newVelocities = Array.from({ length: n }, () => []) + const n = this.positions.length; + const newPositions = Array.from({ length: n }, () => []); + const newVelocities = Array.from({ length: n }, () => []); for (let i = 0; i < n; i++) { - newPositions[i] = Vec3.add(Vec3.add(this.positions[i], this.velocities[i]), Vec3.scale(this.accelerations[i], 0.5)) - newVelocities[i] = Vec3.add(this.velocities[i], Vec3.scale(Vec3.add(this.accelerations[i], newAccelerations[i]), 0.5)) + newPositions[i] = Vec3.add( + this.positions[i], + Vec3.withMaxLength( + Vec3.add( + this.velocities[i], + Vec3.scale(this.accelerations[i], 0.5) + ), + maxDisplacement + ) + ); + newVelocities[i] = Vec3.add( + this.velocities[i], + Vec3.scale( + Vec3.add(this.accelerations[i], newAccelerations[i]), + 0.5 + ) + ); } - this.positions = newPositions - this.velocities = newVelocities - this.accelerations = newAccelerations + this.positions = newPositions; + this.velocities = newVelocities; + this.accelerations = newAccelerations; }, - } + }; } -const NODE_BALL_RADIUS = 30 -const SEGMENT_LENGTH = 10 +const NODE_BALL_RADIUS = 30; +const SEGMENT_LENGTH = 10; -const PASSTHROUGH_MIN_INDICES = NODE_BALL_RADIUS / SEGMENT_LENGTH + 1 +const PASSTHROUGH_MIN_INDICES = NODE_BALL_RADIUS / SEGMENT_LENGTH + 1; -const INVERT_CROSSING_RADIUS = SEGMENT_LENGTH * 3 +const INVERT_CROSSING_RADIUS = SEGMENT_LENGTH * 3; class KnotSimulation { constructor(knotRef, modeRef) { /** ref allo stato esterno che maneggia anche Preact */ - this.knotRef = knotRef - this.modeRef = modeRef + this.knotRef = knotRef; + this.modeRef = modeRef; /** percorso temporaneo utilizzato mentre si disegna sul layer */ - this.ghostPath = null + this.ghostPath = null; /** position where to place the black hole while right-clicking the canvas */ - this.draggingIndex = null + this.draggingIndex = null; /** particle simulation that holds positions, velocities and accelerations */ - this.particleSimulation = null + this.particleSimulation = null; } onMouseDown(x, y, buttons) { - this.mousePosition = [x, y] - - if (buttons === 1) { - switch (this.modeRef.current) { - case MODE_DRAW: - this.ghostPath = [] - break; - case MODE_FLIP: - const { positions } = this.particleSimulation + this.mousePosition = [x, y]; + + switch (this.modeRef.current) { + case MODE_BUTTONS: + switch (buttons) { + case 1: + this.ghostPath = []; + break; + case 2: + const [i] = Vec2.findNearest( + this.particleSimulation.positions.map(([x, y]) => [ + x, + y, + ]), + this.mousePosition + ); + this.draggingIndex = i; + break; + case 4: + const { positions } = this.particleSimulation; + + for (let i = 0; i < positions.length; i++) { + if ( + Vec2.distance2([x, y], positions[i]) < + INVERT_CROSSING_RADIUS + ) { + positions[i][2] *= -1; + } + } + break; + } + case MODE_DRAW: + if (buttons === 1) { + this.ghostPath = []; + } + break; + case MODE_FLIP: + if (buttons === 1) { + const { positions } = this.particleSimulation; for (let i = 0; i < positions.length; i++) { - if (Vec2.distance2([x, y], positions[i]) < INVERT_CROSSING_RADIUS) { - positions[i][2] *= -1 + if ( + Vec2.distance2([x, y], positions[i]) < + INVERT_CROSSING_RADIUS + ) { + positions[i][2] *= -1; } } - break; - case MODE_DRAG: + } + break; + case MODE_DRAG: + if (buttons === 1) { const [i] = Vec2.findNearest( - this.particleSimulation.positions.map(([x, y]) => [x, y]), - this.mousePosition - ) - this.draggingIndex = i - break; - } + this.particleSimulation.positions.map(([x, y]) => [ + x, + y, + ]), + this.mousePosition + ); + this.draggingIndex = i; + } + break; } } onMouseDrag(x, y) { - this.mousePosition = [x, y] + this.mousePosition = [x, y]; if (this.ghostPath) { - this.ghostPath.push([x, y]) + this.ghostPath.push([x, y]); } } onMouseUp() { if (this.ghostPath) { - this.ghostPath.push(this.ghostPath[0]) + this.ghostPath.push(this.ghostPath[0]); - const curve = resampleCurve(this.ghostPath, SEGMENT_LENGTH, SEGMENT_LENGTH) - curve.pop() + const curve = resampleCurve( + this.ghostPath, + SEGMENT_LENGTH, + SEGMENT_LENGTH + ); + curve.pop(); // convert the 2d curve to a 3d curve // this.setPositions(curve.map(([x, y], i) => [x, y, 0.01 * (Math.random() * 2 - 1)])) - this.setPositions(curve.map(([x, y], i) => [x, y, 0.5 * i])) + this.setPositions(curve.map(([x, y], i) => [x, y, 0.5 * i])); - this.ghostPath = null + this.ghostPath = null; } - this.draggingIndex = null + this.draggingIndex = null; } setPositions(positions) { - this.particleSimulation = createSimulation3d( + this.particleSimulation = createSimulation3dMaxDisplacement( positions, Array.from({ length: positions.length }, () => [0, 0, 0]), - Array.from({ length: positions.length }, () => [0, 0, 0]) - ) + Array.from({ length: positions.length }, () => [0, 0, 0]), + NODE_BALL_RADIUS * 0.1 + ); } update() { if (!this.particleSimulation) { - return + return; } - const { positions } = this.particleSimulation - const N = positions.length + const { positions } = this.particleSimulation; + const N = positions.length; + + console.log(N); - const newAccelerations = Array.from({ length: N }, () => [0, 0, 0]) + const newAccelerations = Array.from({ length: N }, () => [0, 0, 0]); for (let i = 0; i < N; i++) { - Vec3.Mutate.add(newAccelerations[i], [0, 0, -0.1 * Math.sign(positions[i][2])]) + Vec3.Mutate.add(newAccelerations[i], [ + 0, + 0, + -0.1 * Math.sign(positions[i][2]), + ]); } // dragging point if (this.draggingIndex !== null) { - this.particleSimulation.positions[this.draggingIndex] = [...this.mousePosition, 0] + const RANGE = 2; + for ( + let i = this.draggingIndex - RANGE; + i <= this.draggingIndex + RANGE; + i++ + ) { + const node = positions[mod(i, N)]; + const factor = 1 / (1 + 10 * Math.abs(i - this.draggingIndex)); + + Vec3.Mutate.add( + newAccelerations[mod(i, N)], + Vec3.Mutate.scale( + Vec3.sub([...this.mousePosition, 0], node), + factor + ) + ); + } } // forzo la lunghezza delle barrette - for (let i = 0; i < 4; i++) { + let globalLinkMaxError = +Infinity; + let linkIterCount = 0; + while ( + globalLinkMaxError > SEGMENT_LENGTH * 0.01 && + linkIterCount++ < 100 + ) { for (let i = 0; i < N; i++) { - const curr = positions.at(i) - const next = positions.at((i + 1) % N) + const curr = positions.at(i); + const next = positions.at((i + 1) % N); - const [pt1, pt2] = enforceDistance(curr, next, SEGMENT_LENGTH) + const [pt1, pt2] = enforceDistance(curr, next, SEGMENT_LENGTH); + + positions[i] = pt1; + positions[mod(i + 1, N)] = pt2; + } - positions[i] = pt1 - positions[mod(i + 1, N)] = pt2 + globalLinkMaxError = 0; + for (let i = 0; i < N; i++) { + const curr = positions.at(i); + const next = positions.at((i + 1) % N); + + const d = Math.abs(Vec3.distance2(curr, next) - SEGMENT_LENGTH); + if (d > globalLinkMaxError) { + globalLinkMaxError = d; + } } } + console.log( + `[Link Constraint] Iterations: ${linkIterCount}, Error: ${globalLinkMaxError}` + ); // raddrizzamento dei link for (let i = 0; i < N; i++) { - const prev = positions.at((i - 1) % N) - const curr = positions.at(i) - const next = positions.at((i + 1) % N) - - const baseForce = Vec3.scale(Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), 0.5) - const jointFactor = 0.1 - - Vec3.Mutate.add(positions.at((i - 1) % N), Vec3.scale(baseForce, -jointFactor / 2)) - Vec3.Mutate.add(positions[i], Vec3.scale(baseForce, jointFactor)) - Vec3.Mutate.add(positions.at((i + 1) % N), Vec3.scale(baseForce, -jointFactor / 2)) + const prev = positions.at((i - 1) % N); + const curr = positions.at(i); + const next = positions.at((i + 1) % N); + + const baseForce = Vec3.scale( + Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), + 0.5 + ); + const jointFactor = 0.1; + + Vec3.Mutate.add( + positions.at((i - 1) % N), + Vec3.scale(baseForce, -jointFactor / 2) + ); + Vec3.Mutate.add(positions[i], Vec3.scale(baseForce, jointFactor)); + Vec3.Mutate.add( + positions.at((i + 1) % N), + Vec3.scale(baseForce, -jointFactor / 2) + ); } // the error of the next constraint will be at most of 90% - let globalNodeMinDistance = 0 - let iter = 0 - while (globalNodeMinDistance < NODE_BALL_RADIUS * 0.99 && iter++ < 100) { + let globalNodeMinDistance = 0; + let iter = 0; + while ( + globalNodeMinDistance < NODE_BALL_RADIUS * 0.99 && + iter++ < 100 + ) { // repulsione per ogni coppia for (let i = 0; i < N; i++) { for (let j = i + 1; j < N; j++) { @@ -174,21 +313,25 @@ class KnotSimulation { Math.abs(i - j + N) > PASSTHROUGH_MIN_INDICES && Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES ) { - const p1 = positions[i] - const p2 = positions[j] + const p1 = positions[i]; + const p2 = positions[j]; - const dist = Vec3.distance2(p1, p2) + const dist = Vec3.distance2(p1, p2); if (dist <= NODE_BALL_RADIUS) { - const [newPt1, newPt2] = enforceDistance(p1, p2, NODE_BALL_RADIUS) - positions[i] = newPt1 - positions[j] = newPt2 + const [newPt1, newPt2] = enforceDistance( + p1, + p2, + NODE_BALL_RADIUS + ); + positions[i] = newPt1; + positions[j] = newPt2; } } } } // estimating global previous constraint error - globalNodeMinDistance = +Infinity + globalNodeMinDistance = +Infinity; for (let i = 0; i < N; i++) { for (let j = i + 1; j < N; j++) { if ( @@ -196,184 +339,202 @@ class KnotSimulation { Math.abs(i - j + N) > PASSTHROUGH_MIN_INDICES && Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES ) { - const p1 = positions[i] - const p2 = positions[j] + const p1 = positions[i]; + const p2 = positions[j]; - const dist = Vec3.distance2(p1, p2) + const dist = Vec3.distance2(p1, p2); if (dist < globalNodeMinDistance) { - globalNodeMinDistance = dist + globalNodeMinDistance = dist; } } } } } + console.log( + `[NAIC Constraint] Iterations: ${iter}, Error: ${globalNodeMinDistance}` + ); - console.log(`Reached solution in ${iter} iterations with global min distance ${globalNodeMinDistance}`) - - this.particleSimulation.update(newAccelerations) + this.particleSimulation.update(newAccelerations); // attrito viscoso for (let i = 0; i < N; i++) { - Vec3.Mutate.scale(this.particleSimulation.velocities[i], 0.5) + Vec3.Mutate.scale(this.particleSimulation.velocities[i], 0.5); } - this.knotRef.current.points = this.particleSimulation.positions + this.knotRef.current.points = this.particleSimulation.positions; } /** @param {CanvasRenderingContext2D} g */ render(g) { - const w = g.canvas.width - const h = g.canvas.height + const w = g.canvas.width; + const h = g.canvas.height; - g.clearRect(0, 0, w, h) + g.clearRect(0, 0, w, h); - g.fillStyle = '#fff' - g.fillRect(0, 0, w, h) + g.fillStyle = "#fff"; + g.fillRect(0, 0, w, h); - g.lineWidth = 3 - g.lineCap = 'round' - g.lineJoin = 'round' + g.lineWidth = 3; + g.lineCap = "round"; + g.lineJoin = "round"; - g.strokeStyle = '#333' + g.strokeStyle = "#333"; if (this.ghostPath && this.ghostPath.length > 0) { - g.strokeStyle = '#888' - g.beginPath() + g.strokeStyle = "#888"; + g.beginPath(); { - const [x0, y0] = this.ghostPath[0] - g.moveTo(x0, y0) + const [x0, y0] = this.ghostPath[0]; + g.moveTo(x0, y0); for (const [x, y] of this.ghostPath) { - g.lineTo(x, y) + g.lineTo(x, y); } } - g.stroke() + g.stroke(); - g.save() + g.save(); { - g.strokeStyle = '#080' - g.lineWidth = 3 - g.setLineDash([6, 6]) - g.beginPath() - g.moveTo(...this.ghostPath.at(-1)) - g.lineTo(...this.ghostPath.at(0)) - g.stroke() + g.strokeStyle = "#080"; + g.lineWidth = 3; + g.setLineDash([6, 6]); + g.beginPath(); + g.moveTo(...this.ghostPath.at(-1)); + g.lineTo(...this.ghostPath.at(0)); + g.stroke(); } - g.restore() + g.restore(); } if (!this.particleSimulation) { - return + return; } - const positions = this.particleSimulation.positions - const particleCount = positions.length + const positions = this.particleSimulation.positions; + const particleCount = positions.length; if (particleCount > 0) { const sortedZ = [...positions] .map(([, , z], i) => [i, z]) .sort(([, z1], [, z2]) => z2 - z1) - .map(([i]) => i) + .map(([i]) => i); - g.strokeStyle = '#333' - sortedZ.forEach(i => { + g.strokeStyle = "#333"; + sortedZ.forEach((i) => { function toProjection([x, y, z]) { // return [x + z, y + z] - return [x, y] + return [x, y]; } - const prev2 = toProjection(positions.at((i - 2) % particleCount)) - const prev = toProjection(positions.at((i - 1) % particleCount)) - const curr = toProjection(positions.at(i % particleCount)) - const next = toProjection(positions.at((i + 1) % particleCount)) - const next2 = toProjection(positions.at((i + 2) % particleCount)) - - g.save() + const prev2 = toProjection( + positions.at((i - 2) % particleCount) + ); + const prev = toProjection( + positions.at((i - 1) % particleCount) + ); + const curr = toProjection(positions.at(i % particleCount)); + const next = toProjection( + positions.at((i + 1) % particleCount) + ); + const next2 = toProjection( + positions.at((i + 2) % particleCount) + ); + + g.save(); { - g.strokeStyle = '#fff' - g.lineWidth = 12 - g.lineJoin = 'round' - g.lineCap = 'butt' - - g.beginPath() - g.moveTo(...prev) - g.lineTo(...curr) - g.lineTo(...next) - g.stroke() + g.strokeStyle = "#fff"; + g.lineWidth = 12; + g.lineJoin = "round"; + g.lineCap = "butt"; + + g.beginPath(); + g.moveTo(...prev); + g.lineTo(...curr); + g.lineTo(...next); + g.stroke(); } - g.restore() + g.restore(); - g.save() + g.save(); { - g.strokeStyle = '#333' - g.lineWidth = 3 - g.lineCap = 'round' - g.lineJoin = 'round' - - g.beginPath() - g.moveTo(...prev2) - g.moveTo(...prev) - g.lineTo(...curr) - g.lineTo(...next) - g.lineTo(...next2) - g.stroke() + g.strokeStyle = "#333"; + g.lineWidth = 3; + g.lineCap = "round"; + g.lineJoin = "round"; + + g.beginPath(); + g.moveTo(...prev2); + g.moveTo(...prev); + g.lineTo(...curr); + g.lineTo(...next); + g.lineTo(...next2); + g.stroke(); } - g.restore() - }) + g.restore(); + }); } } } export const KnotLayer = ({ knotRef, modeRef }) => { - const canvasRef = useRef(null) - const [knotSim] = useState(() => new KnotSimulation(knotRef, modeRef)) + const canvasRef = useRef(null); + const [knotSim] = useState(() => new KnotSimulation(knotRef, modeRef)); useEffect(() => { - window.addEventListener('resize', () => { - // trigger repaint and get a new context - }) - }, []) + window.addEventListener("resize", () => { + const $canvas = canvasRef.current; + $canvas.width = $canvas.offsetWidth; + $canvas.height = $canvas.offsetHeight; + $canvas.graphicsContext = $canvas.getContext("2d"); + }); + }, []); useEffect(() => { - let simTimerHandle + let simTimerHandle; if (canvasRef.current) { - const $canvas = canvasRef.current - $canvas.width = $canvas.offsetWidth - $canvas.height = $canvas.offsetHeight - $canvas.graphicsContext = $canvas.getContext('2d') + const $canvas = canvasRef.current; + $canvas.width = $canvas.offsetWidth; + $canvas.height = $canvas.offsetHeight; + $canvas.graphicsContext = $canvas.getContext("2d"); simTimerHandle = setInterval(() => { - knotSim.update() - requestAnimationFrame(() => knotSim.render($canvas.graphicsContext)) - }, 1000 / 60) + knotSim.update(); + requestAnimationFrame(() => + knotSim.render($canvas.graphicsContext) + ); + }, 1000 / 120); } return () => { if (simTimerHandle) { - clearInterval(simTimerHandle) + clearInterval(simTimerHandle); } - } - }, [canvasRef.current]) + }; + }, [canvasRef.current]); return ( { - knotSim.onMouseDown(e.offsetX, e.offsetY, e.buttons) + onPointerDown={(e) => { + knotSim.onMouseDown(e.offsetX, e.offsetY, e.buttons); }} - onPointerMove={e => { + onPointerMove={(e) => { if (canvasRef.current && e.buttons > 0) { - knotSim.onMouseDrag(e.offsetX, e.offsetY, e.buttons) - requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext)) + knotSim.onMouseDrag(e.offsetX, e.offsetY, e.buttons); + requestAnimationFrame(() => + knotSim.render(canvasRef.current.graphicsContext) + ); } }} - onContextMenu={e => e.preventDefault()} - onPointerUp={e => { + onContextMenu={(e) => e.preventDefault()} + onPointerUp={(e) => { if (canvasRef.current) { - knotSim.onMouseUp() - requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext)) + knotSim.onMouseUp(); + requestAnimationFrame(() => + knotSim.render(canvasRef.current.graphicsContext) + ); } }} /> - ) -} + ); +}; diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index 1c0cdd4..d2d1f09 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -13,13 +13,14 @@ function filterClasses(...classes) { return classes.filter(Boolean).join(' ') } +export const MODE_BUTTONS = 'buttons' export const MODE_DRAW = 'draw' export const MODE_FLIP = 'flip' export const MODE_DRAG = 'drag' export const PageKnotEditor = ({}) => { const knot1 = useRef({ points: [] }) - const [mode, setMode] = useState(MODE_DRAW) + const [mode, setMode] = useState(MODE_BUTTONS) const modeRef = useRef(mode) modeRef.current = mode @@ -36,6 +37,9 @@ export const PageKnotEditor = ({}) => {
Tools
+ diff --git a/frontend/styles/main.scss b/frontend/styles/main.scss index 7a77e95..53274f9 100644 --- a/frontend/styles/main.scss +++ b/frontend/styles/main.scss @@ -230,6 +230,10 @@ main { position: absolute; width: 100%; height: 100%; + + @media (prefers-color-scheme: dark) { + filter: invert(100%); + } } } } diff --git a/package.json b/package.json index 256fdd2..7513829 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "frontend", "type": "module", "scripts": { - "dev": "vite --clearScreen false", + "dev": "vite --host --clearScreen false", "build": "vite build" }, "author": "aziis98 ",