Bilanced simulation to make knots not untangle by themself

main
Antonio De Lucreziis 2 years ago
parent 56096644a0
commit 7259b590ba

@ -1,137 +1,146 @@
export const Vec2 = { export const Vec2 = {
Mutate: { Mutate: {
add(target, [x, y]) { add(target, [x, y]) {
target[0] += x target[0] += x;
target[1] += y target[1] += y;
return target return target;
}, },
scale(target, factor) { scale(target, factor) {
target[0] *= factor target[0] *= factor;
target[1] *= factor target[1] *= factor;
return target return target;
}, },
normalize(target) { normalize(target) {
const norm = Vec2.norm2([x, y]) const norm = Vec2.norm2([x, y]);
target[0] /= norm target[0] /= norm;
target[1] /= norm target[1] /= norm;
return target return target;
}, },
set(target, [x, y]) { set(target, [x, y]) {
target[0] = x target[0] = x;
target[1] = y target[1] = y;
return target return target;
}, },
}, },
perpendicular([x, y]) { perpendicular([x, y]) {
return [-y, x] return [-y, x];
}, },
distance2(v1, v2) { distance2(v1, v2) {
return Vec2.norm2(Vec2.sub(v1, v2)) return Vec2.norm2(Vec2.sub(v1, v2));
}, },
add([x1, y1], [x2, y2]) { add([x1, y1], [x2, y2]) {
return [x1 + x2, y1 + y2] return [x1 + x2, y1 + y2];
}, },
sub([x1, y1], [x2, y2]) { sub([x1, y1], [x2, y2]) {
return [x1 - x2, y1 - y2] return [x1 - x2, y1 - y2];
}, },
dot([x1, y1], [x2, y2]) { dot([x1, y1], [x2, y2]) {
return x1 * x2 + y1 * y2 return x1 * x2 + y1 * y2;
}, },
scale([x, y], factor) { scale([x, y], factor) {
return [x * factor, y * factor] return [x * factor, y * factor];
}, },
norm2([x, y]) { norm2([x, y]) {
return Math.sqrt(x ** 2 + y ** 2) return Math.sqrt(x ** 2 + y ** 2);
}, },
normalize([x, y]) { normalize([x, y]) {
const norm = Vec2.norm2([x, y]) const norm = Vec2.norm2([x, y]);
return [x / norm, y / norm] return [x / norm, y / norm];
}, },
findNearest(curve, pt) { findNearest(curve, pt) {
let index = -1 let index = -1;
let minDist = +Infinity let minDist = +Infinity;
for (let i = 0; i < curve.length; i++) { 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) { if (d < minDist) {
index = i index = i;
minDist = d minDist = d;
} }
} }
return [index, minDist] return [index, minDist];
}, },
} };
export const Vec3 = { export const Vec3 = {
Mutate: { Mutate: {
add(target, [x, y, z]) { add(target, [x, y, z]) {
target[0] += x target[0] += x;
target[1] += y target[1] += y;
target[2] += z target[2] += z;
return target return target;
}, },
scale(target, factor) { scale(target, factor) {
target[0] *= factor target[0] *= factor;
target[1] *= factor target[1] *= factor;
target[2] *= factor target[2] *= factor;
return target return target;
}, },
normalize(target) { normalize(target) {
const norm = Vec3.norm2(target) const norm = Vec3.norm2(target);
target[0] /= norm target[0] /= norm;
target[1] /= norm target[1] /= norm;
target[2] /= 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) { distance2(v1, v2) {
return Vec3.norm2(Vec3.sub(v1, v2)) return Vec3.norm2(Vec3.sub(v1, v2));
}, },
add([x1, y1, z1], [x2, y2, z2]) { 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]) { 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]) { 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) { scale([x, y, z], factor) {
return [x * factor, y * factor, z * factor] return [x * factor, y * factor, z * factor];
}, },
norm2([x, y, z]) { 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]) { normalize([x, y, z]) {
const norm = Vec3.norm2([x, y, z]) const norm = Vec3.norm2([x, y, z]);
return [x / norm, y / norm, z / norm] return [x / norm, y / norm, z / norm];
}, },
findNearest(curve, pt) { findNearest(curve, pt) {
let index = -1 let index = -1;
let minDist = +Infinity let minDist = +Infinity;
for (let i = 0; i < curve.length; i++) { 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) { if (d < minDist) {
index = i index = i;
minDist = d minDist = d;
} }
} }
return [index, minDist] return [index, minDist];
}, },
} };
export function clamp(min, value, max) { export function clamp(min, value, max) {
return Math.min(max, Math.max(min, value)) return Math.min(max, Math.max(min, value));
} }

@ -1,171 +1,310 @@
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from "preact/hooks";
import { enforceDistance } from '../../lib/math/constraint.js' import { enforceDistance } from "../../lib/math/constraint.js";
import { resampleCurve } from '../../lib/math/curves.js' import { resampleCurve } from "../../lib/math/curves.js";
import { Vec2, Vec3 } from '../../lib/math/math.js' import { Vec2, Vec3 } from "../../lib/math/math.js";
import { MODE_DRAW, MODE_FLIP, MODE_DRAG } from '../pages.jsx' import { MODE_DRAW, MODE_FLIP, MODE_DRAG, MODE_BUTTONS } from "../pages.jsx";
function mod(i, modulus) { function mod(i, modulus) {
const r = i % modulus const r = i % modulus;
return r < 0 ? r + modulus : r 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 { return {
positions, positions,
velocities, velocities,
accelerations, accelerations,
update(newAccelerations) { update(newAccelerations) {
const n = this.positions.length const n = this.positions.length;
const newPositions = Array.from({ length: n }, () => []) const newPositions = Array.from({ length: n }, () => []);
const newVelocities = Array.from({ length: n }, () => []) const newVelocities = Array.from({ length: n }, () => []);
for (let i = 0; i < n; i++) { 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)) newPositions[i] = Vec3.add(
newVelocities[i] = Vec3.add(this.velocities[i], Vec3.scale(Vec3.add(this.accelerations[i], newAccelerations[i]), 0.5)) 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.positions = newPositions;
this.velocities = newVelocities this.velocities = newVelocities;
this.accelerations = newAccelerations this.accelerations = newAccelerations;
}, },
} };
} }
const NODE_BALL_RADIUS = 30 const NODE_BALL_RADIUS = 30;
const SEGMENT_LENGTH = 10 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 { class KnotSimulation {
constructor(knotRef, modeRef) { constructor(knotRef, modeRef) {
/** ref allo stato esterno che maneggia anche Preact */ /** ref allo stato esterno che maneggia anche Preact */
this.knotRef = knotRef this.knotRef = knotRef;
this.modeRef = modeRef this.modeRef = modeRef;
/** percorso temporaneo utilizzato mentre si disegna sul layer */ /** 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 */ /** 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 */ /** particle simulation that holds positions, velocities and accelerations */
this.particleSimulation = null this.particleSimulation = null;
} }
onMouseDown(x, y, buttons) { onMouseDown(x, y, buttons) {
this.mousePosition = [x, y] this.mousePosition = [x, y];
if (buttons === 1) { switch (this.modeRef.current) {
switch (this.modeRef.current) { case MODE_BUTTONS:
case MODE_DRAW: switch (buttons) {
this.ghostPath = [] case 1:
break; this.ghostPath = [];
case MODE_FLIP: break;
const { positions } = this.particleSimulation 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++) { for (let i = 0; i < positions.length; i++) {
if (Vec2.distance2([x, y], positions[i]) < INVERT_CROSSING_RADIUS) { if (
positions[i][2] *= -1 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( const [i] = Vec2.findNearest(
this.particleSimulation.positions.map(([x, y]) => [x, y]), this.particleSimulation.positions.map(([x, y]) => [
this.mousePosition x,
) y,
this.draggingIndex = i ]),
break; this.mousePosition
} );
this.draggingIndex = i;
}
break;
} }
} }
onMouseDrag(x, y) { onMouseDrag(x, y) {
this.mousePosition = [x, y] this.mousePosition = [x, y];
if (this.ghostPath) { if (this.ghostPath) {
this.ghostPath.push([x, y]) this.ghostPath.push([x, y]);
} }
} }
onMouseUp() { onMouseUp() {
if (this.ghostPath) { if (this.ghostPath) {
this.ghostPath.push(this.ghostPath[0]) this.ghostPath.push(this.ghostPath[0]);
const curve = resampleCurve(this.ghostPath, SEGMENT_LENGTH, SEGMENT_LENGTH) const curve = resampleCurve(
curve.pop() this.ghostPath,
SEGMENT_LENGTH,
SEGMENT_LENGTH
);
curve.pop();
// convert the 2d curve to a 3d curve // 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.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) { setPositions(positions) {
this.particleSimulation = createSimulation3d( this.particleSimulation = createSimulation3dMaxDisplacement(
positions, 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]) Array.from({ length: positions.length }, () => [0, 0, 0]),
) NODE_BALL_RADIUS * 0.1
);
} }
update() { update() {
if (!this.particleSimulation) { if (!this.particleSimulation) {
return return;
} }
const { positions } = this.particleSimulation const { positions } = this.particleSimulation;
const N = positions.length 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++) { 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 // dragging point
if (this.draggingIndex !== null) { 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 // 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++) { for (let i = 0; i < N; i++) {
const curr = positions.at(i) const curr = positions.at(i);
const next = positions.at((i + 1) % N) 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 globalLinkMaxError = 0;
positions[mod(i + 1, N)] = pt2 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 // raddrizzamento dei link
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
const prev = positions.at((i - 1) % N) const prev = positions.at((i - 1) % N);
const curr = positions.at(i) const curr = positions.at(i);
const next = positions.at((i + 1) % N) const next = positions.at((i + 1) % N);
const baseForce = Vec3.scale(Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), 0.5) const baseForce = Vec3.scale(
const jointFactor = 0.1 Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)),
0.5
Vec3.Mutate.add(positions.at((i - 1) % N), Vec3.scale(baseForce, -jointFactor / 2)) );
Vec3.Mutate.add(positions[i], Vec3.scale(baseForce, jointFactor)) const jointFactor = 0.1;
Vec3.Mutate.add(positions.at((i + 1) % N), Vec3.scale(baseForce, -jointFactor / 2))
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% // the error of the next constraint will be at most of 90%
let globalNodeMinDistance = 0 let globalNodeMinDistance = 0;
let iter = 0 let iter = 0;
while (globalNodeMinDistance < NODE_BALL_RADIUS * 0.99 && iter++ < 100) { while (
globalNodeMinDistance < NODE_BALL_RADIUS * 0.99 &&
iter++ < 100
) {
// repulsione per ogni coppia // repulsione per ogni coppia
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) { 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 &&
Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES
) { ) {
const p1 = positions[i] const p1 = positions[i];
const p2 = positions[j] const p2 = positions[j];
const dist = Vec3.distance2(p1, p2) const dist = Vec3.distance2(p1, p2);
if (dist <= NODE_BALL_RADIUS) { if (dist <= NODE_BALL_RADIUS) {
const [newPt1, newPt2] = enforceDistance(p1, p2, NODE_BALL_RADIUS) const [newPt1, newPt2] = enforceDistance(
positions[i] = newPt1 p1,
positions[j] = newPt2 p2,
NODE_BALL_RADIUS
);
positions[i] = newPt1;
positions[j] = newPt2;
} }
} }
} }
} }
// estimating global previous constraint error // estimating global previous constraint error
globalNodeMinDistance = +Infinity globalNodeMinDistance = +Infinity;
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) { for (let j = i + 1; j < N; j++) {
if ( if (
@ -196,184 +339,202 @@ class KnotSimulation {
Math.abs(i - j + N) > PASSTHROUGH_MIN_INDICES && Math.abs(i - j + N) > PASSTHROUGH_MIN_INDICES &&
Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES
) { ) {
const p1 = positions[i] const p1 = positions[i];
const p2 = positions[j] const p2 = positions[j];
const dist = Vec3.distance2(p1, p2) const dist = Vec3.distance2(p1, p2);
if (dist < globalNodeMinDistance) { 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 // attrito viscoso
for (let i = 0; i < N; i++) { 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 */ /** @param {CanvasRenderingContext2D} g */
render(g) { render(g) {
const w = g.canvas.width const w = g.canvas.width;
const h = g.canvas.height const h = g.canvas.height;
g.clearRect(0, 0, w, h) g.clearRect(0, 0, w, h);
g.fillStyle = '#fff' g.fillStyle = "#fff";
g.fillRect(0, 0, w, h) g.fillRect(0, 0, w, h);
g.lineWidth = 3 g.lineWidth = 3;
g.lineCap = 'round' g.lineCap = "round";
g.lineJoin = 'round' g.lineJoin = "round";
g.strokeStyle = '#333' g.strokeStyle = "#333";
if (this.ghostPath && this.ghostPath.length > 0) { if (this.ghostPath && this.ghostPath.length > 0) {
g.strokeStyle = '#888' g.strokeStyle = "#888";
g.beginPath() g.beginPath();
{ {
const [x0, y0] = this.ghostPath[0] const [x0, y0] = this.ghostPath[0];
g.moveTo(x0, y0) g.moveTo(x0, y0);
for (const [x, y] of this.ghostPath) { 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.strokeStyle = "#080";
g.lineWidth = 3 g.lineWidth = 3;
g.setLineDash([6, 6]) g.setLineDash([6, 6]);
g.beginPath() g.beginPath();
g.moveTo(...this.ghostPath.at(-1)) g.moveTo(...this.ghostPath.at(-1));
g.lineTo(...this.ghostPath.at(0)) g.lineTo(...this.ghostPath.at(0));
g.stroke() g.stroke();
} }
g.restore() g.restore();
} }
if (!this.particleSimulation) { if (!this.particleSimulation) {
return return;
} }
const positions = this.particleSimulation.positions const positions = this.particleSimulation.positions;
const particleCount = positions.length const particleCount = positions.length;
if (particleCount > 0) { if (particleCount > 0) {
const sortedZ = [...positions] const sortedZ = [...positions]
.map(([, , z], i) => [i, z]) .map(([, , z], i) => [i, z])
.sort(([, z1], [, z2]) => z2 - z1) .sort(([, z1], [, z2]) => z2 - z1)
.map(([i]) => i) .map(([i]) => i);
g.strokeStyle = '#333' g.strokeStyle = "#333";
sortedZ.forEach(i => { sortedZ.forEach((i) => {
function toProjection([x, y, z]) { function toProjection([x, y, z]) {
// return [x + z, y + z] // return [x + z, y + z]
return [x, y] return [x, y];
} }
const prev2 = toProjection(positions.at((i - 2) % particleCount)) const prev2 = toProjection(
const prev = toProjection(positions.at((i - 1) % particleCount)) positions.at((i - 2) % particleCount)
const curr = toProjection(positions.at(i % particleCount)) );
const next = toProjection(positions.at((i + 1) % particleCount)) const prev = toProjection(
const next2 = toProjection(positions.at((i + 2) % particleCount)) positions.at((i - 1) % particleCount)
);
g.save() 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.strokeStyle = "#fff";
g.lineWidth = 12 g.lineWidth = 12;
g.lineJoin = 'round' g.lineJoin = "round";
g.lineCap = 'butt' g.lineCap = "butt";
g.beginPath() g.beginPath();
g.moveTo(...prev) g.moveTo(...prev);
g.lineTo(...curr) g.lineTo(...curr);
g.lineTo(...next) g.lineTo(...next);
g.stroke() g.stroke();
} }
g.restore() g.restore();
g.save() g.save();
{ {
g.strokeStyle = '#333' g.strokeStyle = "#333";
g.lineWidth = 3 g.lineWidth = 3;
g.lineCap = 'round' g.lineCap = "round";
g.lineJoin = 'round' g.lineJoin = "round";
g.beginPath() g.beginPath();
g.moveTo(...prev2) g.moveTo(...prev2);
g.moveTo(...prev) g.moveTo(...prev);
g.lineTo(...curr) g.lineTo(...curr);
g.lineTo(...next) g.lineTo(...next);
g.lineTo(...next2) g.lineTo(...next2);
g.stroke() g.stroke();
} }
g.restore() g.restore();
}) });
} }
} }
} }
export const KnotLayer = ({ knotRef, modeRef }) => { export const KnotLayer = ({ knotRef, modeRef }) => {
const canvasRef = useRef(null) const canvasRef = useRef(null);
const [knotSim] = useState(() => new KnotSimulation(knotRef, modeRef)) const [knotSim] = useState(() => new KnotSimulation(knotRef, modeRef));
useEffect(() => { useEffect(() => {
window.addEventListener('resize', () => { window.addEventListener("resize", () => {
// trigger repaint and get a new context const $canvas = canvasRef.current;
}) $canvas.width = $canvas.offsetWidth;
}, []) $canvas.height = $canvas.offsetHeight;
$canvas.graphicsContext = $canvas.getContext("2d");
});
}, []);
useEffect(() => { useEffect(() => {
let simTimerHandle let simTimerHandle;
if (canvasRef.current) { if (canvasRef.current) {
const $canvas = canvasRef.current const $canvas = canvasRef.current;
$canvas.width = $canvas.offsetWidth $canvas.width = $canvas.offsetWidth;
$canvas.height = $canvas.offsetHeight $canvas.height = $canvas.offsetHeight;
$canvas.graphicsContext = $canvas.getContext('2d') $canvas.graphicsContext = $canvas.getContext("2d");
simTimerHandle = setInterval(() => { simTimerHandle = setInterval(() => {
knotSim.update() knotSim.update();
requestAnimationFrame(() => knotSim.render($canvas.graphicsContext)) requestAnimationFrame(() =>
}, 1000 / 60) knotSim.render($canvas.graphicsContext)
);
}, 1000 / 120);
} }
return () => { return () => {
if (simTimerHandle) { if (simTimerHandle) {
clearInterval(simTimerHandle) clearInterval(simTimerHandle);
} }
} };
}, [canvasRef.current]) }, [canvasRef.current]);
return ( return (
<canvas <canvas
ref={canvasRef} ref={canvasRef}
onPointerDown={e => { onPointerDown={(e) => {
knotSim.onMouseDown(e.offsetX, e.offsetY, e.buttons) knotSim.onMouseDown(e.offsetX, e.offsetY, e.buttons);
}} }}
onPointerMove={e => { onPointerMove={(e) => {
if (canvasRef.current && e.buttons > 0) { if (canvasRef.current && e.buttons > 0) {
knotSim.onMouseDrag(e.offsetX, e.offsetY, e.buttons) knotSim.onMouseDrag(e.offsetX, e.offsetY, e.buttons);
requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext)) requestAnimationFrame(() =>
knotSim.render(canvasRef.current.graphicsContext)
);
} }
}} }}
onContextMenu={e => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
onPointerUp={e => { onPointerUp={(e) => {
if (canvasRef.current) { if (canvasRef.current) {
knotSim.onMouseUp() knotSim.onMouseUp();
requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext)) requestAnimationFrame(() =>
knotSim.render(canvasRef.current.graphicsContext)
);
} }
}} }}
/> />
) );
} };

@ -13,13 +13,14 @@ function filterClasses(...classes) {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(' ')
} }
export const MODE_BUTTONS = 'buttons'
export const MODE_DRAW = 'draw' export const MODE_DRAW = 'draw'
export const MODE_FLIP = 'flip' export const MODE_FLIP = 'flip'
export const MODE_DRAG = 'drag' export const MODE_DRAG = 'drag'
export const PageKnotEditor = ({}) => { export const PageKnotEditor = ({}) => {
const knot1 = useRef({ points: [] }) const knot1 = useRef({ points: [] })
const [mode, setMode] = useState(MODE_DRAW) const [mode, setMode] = useState(MODE_BUTTONS)
const modeRef = useRef(mode) const modeRef = useRef(mode)
modeRef.current = mode modeRef.current = mode
@ -36,6 +37,9 @@ export const PageKnotEditor = ({}) => {
<div class="label">Tools</div> <div class="label">Tools</div>
<div class="content"> <div class="content">
<div class="compound-select"> <div class="compound-select">
<button class={filterClasses("icon", mode === MODE_BUTTONS && 'selected')} onClick={() => setMode(MODE_BUTTONS)}>
<span class="material-symbols-outlined">mouse</span>
</button>
<button class={filterClasses("icon", mode === MODE_DRAW && 'selected')} onClick={() => setMode(MODE_DRAW)}> <button class={filterClasses("icon", mode === MODE_DRAW && 'selected')} onClick={() => setMode(MODE_DRAW)}>
<span class="material-symbols-outlined">gesture</span> <span class="material-symbols-outlined">gesture</span>
</button> </button>

@ -230,6 +230,10 @@ main {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
@media (prefers-color-scheme: dark) {
filter: invert(100%);
}
} }
} }
} }

@ -2,7 +2,7 @@
"name": "frontend", "name": "frontend",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --clearScreen false", "dev": "vite --host --clearScreen false",
"build": "vite build" "build": "vite build"
}, },
"author": "aziis98 <antonio.delucreziis@gmail.com>", "author": "aziis98 <antonio.delucreziis@gmail.com>",

Loading…
Cancel
Save