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 = {
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));
}

@ -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.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;
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 (
<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)
);
}
}}
/>
)
}
);
};

@ -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 = ({}) => {
<div class="label">Tools</div>
<div class="content">
<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)}>
<span class="material-symbols-outlined">gesture</span>
</button>

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

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

Loading…
Cancel
Save