Working 3d simulation with various button actions
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { Vec3 } from './math.js'
|
||||
|
||||
export function enforceDistance(p1, p2, targetDistance) {
|
||||
const v = Vec3.sub(p2, p1)
|
||||
const d = Vec3.norm2(v)
|
||||
|
||||
const factor = (d - targetDistance) / d
|
||||
|
||||
return [Vec3.add(p1, Vec3.scale(v, 0.5 * factor)), Vec3.add(p2, Vec3.scale(v, -0.5 * factor))]
|
||||
}
|
||||
@@ -51,6 +51,7 @@ export const Vec2 = {
|
||||
return [x / norm, y / norm]
|
||||
},
|
||||
}
|
||||
|
||||
export const Vec3 = {
|
||||
Mutate: {
|
||||
add(target, [x, y, z]) {
|
||||
@@ -98,6 +99,21 @@ export const Vec3 = {
|
||||
const norm = Vec3.norm2([x, y, z])
|
||||
return [x / norm, y / norm, z / norm]
|
||||
},
|
||||
findNearest3d(curve, pt) {
|
||||
let index = 0
|
||||
let minDist = Infinity
|
||||
|
||||
for (let i = 0; i < curve.length; i++) {
|
||||
const d = Vec3.distance2(curve[i], pt)
|
||||
|
||||
if (d < minDist) {
|
||||
index = i
|
||||
minDist = d
|
||||
}
|
||||
}
|
||||
|
||||
return [index, minDist]
|
||||
},
|
||||
}
|
||||
|
||||
export function clamp(min, value, max) {
|
||||
|
||||
@@ -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])])
|
||||
}
|
||||
|
||||
Vec2.Mutate.add(newAccelerations[i], Vec2.scale(dir, 1e6 / Math.max(dist, 75) ** 3))
|
||||
// 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)
|
||||
|
||||
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 baseForce = Vec3.scale(Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), 0.5)
|
||||
const jointFactor = 0.5
|
||||
|
||||
const factor = (d - 10) / d
|
||||
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))
|
||||
}
|
||||
|
||||
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))
|
||||
// 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 baseForce = Vec2.scale(Vec2.add(Vec2.sub(prev, curr), Vec2.sub(next, curr)), 0.5)
|
||||
const jointFactor = 1
|
||||
|
||||
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))
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user