|
|
|
@ -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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSimulation3d(positions, velocities, accelerations) {
|
|
|
|
|
const r = i % modulus;
|
|
|
|
|
return r < 0 ? r + modulus : r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
|
this.mousePosition = [x, y];
|
|
|
|
|
|
|
|
|
|
if (buttons === 1) {
|
|
|
|
|
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:
|
|
|
|
|
this.ghostPath = []
|
|
|
|
|
if (buttons === 1) {
|
|
|
|
|
this.ghostPath = [];
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case MODE_FLIP:
|
|
|
|
|
const { positions } = this.particleSimulation
|
|
|
|
|
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:
|
|
|
|
|
if (buttons === 1) {
|
|
|
|
|
const [i] = Vec2.findNearest(
|
|
|
|
|
this.particleSimulation.positions.map(([x, y]) => [x, y]),
|
|
|
|
|
this.particleSimulation.positions.map(([x, y]) => [
|
|
|
|
|
x,
|
|
|
|
|
y,
|
|
|
|
|
]),
|
|
|
|
|
this.mousePosition
|
|
|
|
|
)
|
|
|
|
|
this.draggingIndex = i
|
|
|
|
|
break;
|
|
|
|
|
);
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
const newAccelerations = Array.from({ length: N }, () => [0, 0, 0])
|
|
|
|
|
console.log(N);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// raddrizzamento dei link
|
|
|
|
|
globalLinkMaxError = 0;
|
|
|
|
|
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 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
|
|
|
|
|
const d = Math.abs(Vec3.distance2(curr, next) - SEGMENT_LENGTH);
|
|
|
|
|
if (d > globalLinkMaxError) {
|
|
|
|
|
globalLinkMaxError = d;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log(
|
|
|
|
|
`[Link Constraint] Iterations: ${linkIterCount}, Error: ${globalLinkMaxError}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
// 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)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
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();
|
|
|
|
|
{
|
|
|
|
|
g.strokeStyle = '#fff'
|
|
|
|
|
g.lineWidth = 12
|
|
|
|
|
g.lineJoin = 'round'
|
|
|
|
|
g.lineCap = 'butt'
|
|
|
|
|
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.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.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.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 (
|
|
|
|
|
<canvas
|
|
|
|
|
ref={canvasRef}
|
|
|
|
|
onPointerDown={e => {
|
|
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|