|
|
|
@ -1,13 +1,14 @@
|
|
|
|
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
|
|
|
import { resampleCurve, simplifyCurve } from '../../lib/math/curves.js'
|
|
|
|
|
import { Vec2 } from '../../lib/math/math.js'
|
|
|
|
|
import { enforceDistance } from '../../lib/math/constraint.js'
|
|
|
|
|
import { resampleCurve } from '../../lib/math/curves.js'
|
|
|
|
|
import { Vec2, Vec3 } from '../../lib/math/math.js'
|
|
|
|
|
|
|
|
|
|
function mod(i, modulus) {
|
|
|
|
|
const r = i % modulus
|
|
|
|
|
return r < 0 ? r + modulus : r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSimulation2d(positions, velocities, accelerations) {
|
|
|
|
|
function createSimulation3d(positions, velocities, accelerations) {
|
|
|
|
|
return {
|
|
|
|
|
positions,
|
|
|
|
|
velocities,
|
|
|
|
@ -19,8 +20,8 @@ function createSimulation2d(positions, velocities, accelerations) {
|
|
|
|
|
const newVelocities = Array.from({ length: n }, () => [])
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
|
|
|
newPositions[i] = Vec2.add(Vec2.add(this.positions[i], this.velocities[i]), Vec2.scale(this.accelerations[i], 0.5))
|
|
|
|
|
newVelocities[i] = Vec2.add(this.velocities[i], Vec2.scale(Vec2.add(this.accelerations[i], newAccelerations[i]), 0.5))
|
|
|
|
|
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
|
|
|
|
@ -30,59 +31,80 @@ function createSimulation2d(positions, velocities, accelerations) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NODE_BALL_RADIUS = 30
|
|
|
|
|
const SEGMENT_LENGTH = 10
|
|
|
|
|
|
|
|
|
|
const PASSTHROUGH_MIN_INDICES = NODE_BALL_RADIUS / SEGMENT_LENGTH + 1
|
|
|
|
|
|
|
|
|
|
const INVERT_CROSSING_RADIUS = SEGMENT_LENGTH * 3
|
|
|
|
|
|
|
|
|
|
class KnotSimulation {
|
|
|
|
|
constructor(knotRef) {
|
|
|
|
|
constructor(knotRef, modeRef) {
|
|
|
|
|
/** ref allo stato esterno che maneggia anche Preact */
|
|
|
|
|
this.knotRef = knotRef
|
|
|
|
|
this.modeRef = modeRef
|
|
|
|
|
|
|
|
|
|
/** percorso temporaneo utilizzato mentre si disegna sul layer */
|
|
|
|
|
this.ghostPath = null
|
|
|
|
|
|
|
|
|
|
/** position where to place the black hole while right-clicking the canvas */
|
|
|
|
|
this.blackHolePosition = null
|
|
|
|
|
this.draggingIndex = null
|
|
|
|
|
|
|
|
|
|
/** particle simulation that holds positions, velocities and accelerations */
|
|
|
|
|
this.particleSimulation = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMouseDown(x, y, buttons) {
|
|
|
|
|
this.mousePosition = [x, y]
|
|
|
|
|
|
|
|
|
|
if (buttons === 1) {
|
|
|
|
|
this.ghostPath = []
|
|
|
|
|
}
|
|
|
|
|
if (buttons === 2) {
|
|
|
|
|
this.blackHolePosition = [x, y]
|
|
|
|
|
const [i] = Vec3.findNearest3d(this.particleSimulation.positions, [...this.mousePosition, 0])
|
|
|
|
|
this.draggingIndex = i
|
|
|
|
|
}
|
|
|
|
|
if (buttons === 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMouseDrag(x, y) {
|
|
|
|
|
this.mousePosition = [x, y]
|
|
|
|
|
|
|
|
|
|
if (this.ghostPath) {
|
|
|
|
|
this.ghostPath.push([x, y])
|
|
|
|
|
}
|
|
|
|
|
if (this.blackHolePosition) {
|
|
|
|
|
this.blackHolePosition = [x, y]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMouseUp() {
|
|
|
|
|
if (this.ghostPath) {
|
|
|
|
|
this.ghostPath.push(this.ghostPath[0])
|
|
|
|
|
|
|
|
|
|
const curve = resampleCurve(this.ghostPath, 10, 10)
|
|
|
|
|
const curve = resampleCurve(this.ghostPath, SEGMENT_LENGTH, SEGMENT_LENGTH)
|
|
|
|
|
curve.pop()
|
|
|
|
|
|
|
|
|
|
this.setPositions(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.5 * i]))
|
|
|
|
|
|
|
|
|
|
this.ghostPath = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.blackHolePosition = null
|
|
|
|
|
this.draggingIndex = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setPositions(positions) {
|
|
|
|
|
this.particleSimulation = createSimulation2d(
|
|
|
|
|
this.particleSimulation = createSimulation3d(
|
|
|
|
|
positions,
|
|
|
|
|
Array.from({ length: positions.length }, () => [0, 0]),
|
|
|
|
|
Array.from({ length: positions.length }, () => [0, 0])
|
|
|
|
|
Array.from({ length: positions.length }, () => [0, 0, 0]),
|
|
|
|
|
Array.from({ length: positions.length }, () => [0, 0, 0])
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -94,45 +116,71 @@ class KnotSimulation {
|
|
|
|
|
const { positions } = this.particleSimulation
|
|
|
|
|
const N = positions.length
|
|
|
|
|
|
|
|
|
|
const newAccelerations = Array.from({ length: N }, () => [0, 0])
|
|
|
|
|
const newAccelerations = Array.from({ length: N }, () => [0, 0, 0])
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < N; i++) {
|
|
|
|
|
if (this.blackHolePosition) {
|
|
|
|
|
const v = Vec2.sub(this.blackHolePosition, positions[i])
|
|
|
|
|
const dir = Vec2.normalize(v)
|
|
|
|
|
const dist = Vec2.norm2(v)
|
|
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// forzo la lunghezza delle barrette
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
|
for (let i = 0; i < N; i++) {
|
|
|
|
|
const curr = positions.at(i)
|
|
|
|
|
const next = positions.at((i + 1) % N)
|
|
|
|
|
|
|
|
|
|
Vec2.Mutate.add(newAccelerations[i], Vec2.scale(dir, 1e6 / Math.max(dist, 75) ** 3))
|
|
|
|
|
const [pt1, pt2] = enforceDistance(curr, next, SEGMENT_LENGTH)
|
|
|
|
|
|
|
|
|
|
positions[i] = pt1
|
|
|
|
|
positions[mod(i + 1, N)] = pt2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// barrette rigide
|
|
|
|
|
// 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 v = Vec2.sub(next, curr)
|
|
|
|
|
const d = Vec2.norm2(v)
|
|
|
|
|
|
|
|
|
|
const factor = (d - 10) / d
|
|
|
|
|
const baseForce = Vec3.scale(Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), 0.5)
|
|
|
|
|
const jointFactor = 0.5
|
|
|
|
|
|
|
|
|
|
positions[mod(i, N)] = Vec2.add(curr, Vec2.scale(v, 0.5 * factor))
|
|
|
|
|
positions[mod(i + 1, N)] = Vec2.add(next, Vec2.scale(v, -0.5 * factor))
|
|
|
|
|
|
|
|
|
|
const baseForce = Vec2.scale(Vec2.add(Vec2.sub(prev, curr), Vec2.sub(next, curr)), 0.5)
|
|
|
|
|
const jointFactor = 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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vec2.Mutate.add(positions.at((i - 1) % N), Vec2.scale(baseForce, -jointFactor / 2))
|
|
|
|
|
Vec2.Mutate.add(positions[i], Vec2.scale(baseForce, jointFactor))
|
|
|
|
|
Vec2.Mutate.add(positions.at((i + 1) % N), Vec2.scale(baseForce, -jointFactor / 2))
|
|
|
|
|
// repulsione per ogni coppia
|
|
|
|
|
for (let i = 0; i < N; i++) {
|
|
|
|
|
for (let j = 0; j < N; j++) {
|
|
|
|
|
if (
|
|
|
|
|
i < j &&
|
|
|
|
|
Math.abs(i - j) > PASSTHROUGH_MIN_INDICES &&
|
|
|
|
|
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 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.particleSimulation.update(newAccelerations)
|
|
|
|
|
|
|
|
|
|
// attrito viscoso
|
|
|
|
|
for (let i = 0; i < N; i++) {
|
|
|
|
|
Vec2.Mutate.scale(this.particleSimulation.velocities[i], 0.1)
|
|
|
|
|
Vec3.Mutate.scale(this.particleSimulation.velocities[i], 0.5)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.knotRef.current.points = this.particleSimulation.positions
|
|
|
|
@ -186,17 +234,23 @@ class KnotSimulation {
|
|
|
|
|
const particleCount = positions.length
|
|
|
|
|
|
|
|
|
|
if (particleCount > 0) {
|
|
|
|
|
g.beginPath()
|
|
|
|
|
const [x0, y0] = positions[0]
|
|
|
|
|
g.moveTo(x0, y0)
|
|
|
|
|
const sortedZ = [...positions]
|
|
|
|
|
.map(([, , z], i) => [i, z])
|
|
|
|
|
.sort(([, z1], [, z2]) => z2 - z1)
|
|
|
|
|
.map(([i]) => i)
|
|
|
|
|
|
|
|
|
|
g.strokeStyle = '#333'
|
|
|
|
|
for (let i = 0; i < particleCount; i++) {
|
|
|
|
|
const prev2 = positions.at((i - 2) % particleCount)
|
|
|
|
|
const prev = positions.at((i - 1) % particleCount)
|
|
|
|
|
const curr = positions.at(i % particleCount)
|
|
|
|
|
const next = positions.at((i + 1) % particleCount)
|
|
|
|
|
const next2 = positions.at((i + 2) % particleCount)
|
|
|
|
|
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()
|
|
|
|
|
{
|
|
|
|
@ -229,15 +283,7 @@ class KnotSimulation {
|
|
|
|
|
g.stroke()
|
|
|
|
|
}
|
|
|
|
|
g.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// for (let i = 0; i < particleCount; i++) {
|
|
|
|
|
// g.fillStyle = '#f00'
|
|
|
|
|
|
|
|
|
|
// g.beginPath()
|
|
|
|
|
// g.ellipse(...positions[i], 2, 2, 0, 0, 2 * Math.PI)
|
|
|
|
|
// g.fill()
|
|
|
|
|
// }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -264,7 +310,7 @@ export const KnotLayer = ({ knotRef }) => {
|
|
|
|
|
simTimerHandle = setInterval(() => {
|
|
|
|
|
knotSim.update()
|
|
|
|
|
requestAnimationFrame(() => knotSim.render($canvas.graphicsContext))
|
|
|
|
|
}, 1000 / 30)
|
|
|
|
|
}, 1000 / 60)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|