|
|
|
@ -1,34 +1,157 @@
|
|
|
|
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
|
|
|
import { simplifyCurve } from '../../lib/math/curves.js'
|
|
|
|
|
import { resampleCurve, simplifyCurve } from '../../lib/math/curves.js'
|
|
|
|
|
import { clamp, Vec2 } from '../../lib/math/math.js'
|
|
|
|
|
|
|
|
|
|
function createSimulation(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][0] = this.positions[i][0] + this.velocities[i][0] + this.accelerations[i][0] * 0.5
|
|
|
|
|
newPositions[i][1] = this.positions[i][1] + this.velocities[i][1] + this.accelerations[i][1] * 0.5
|
|
|
|
|
newVelocities[i][0] = this.velocities[i][0] + (this.accelerations[i][0] + newAccelerations[i][0]) * 0.5
|
|
|
|
|
newVelocities[i][1] = this.velocities[i][1] + (this.accelerations[i][1] + newAccelerations[i][1]) * 0.5
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.positions = newPositions
|
|
|
|
|
this.velocities = newVelocities
|
|
|
|
|
this.accelerations = newAccelerations
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class KnotSimulation {
|
|
|
|
|
constructor(knotRef) {
|
|
|
|
|
/** ref allo stato esterno che maneggia anche Preact */
|
|
|
|
|
this.knotRef = knotRef
|
|
|
|
|
|
|
|
|
|
/** 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
|
|
|
|
|
|
|
|
|
|
/** particle simulation that holds positions, velocities and accelerations */
|
|
|
|
|
this.particleSimulation = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMouseDrag(x, y) {
|
|
|
|
|
if (!this.ghostPath) {
|
|
|
|
|
onMouseDown(x, y, buttons) {
|
|
|
|
|
if (buttons === 1) {
|
|
|
|
|
this.ghostPath = []
|
|
|
|
|
}
|
|
|
|
|
if (buttons === 2) {
|
|
|
|
|
this.blackHolePosition = [x, y]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.ghostPath.push([x, y])
|
|
|
|
|
onMouseDrag(x, y) {
|
|
|
|
|
if (this.ghostPath) {
|
|
|
|
|
this.ghostPath.push([x, y])
|
|
|
|
|
}
|
|
|
|
|
if (this.blackHolePosition) {
|
|
|
|
|
this.blackHolePosition = [x, y]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMouseUp() {
|
|
|
|
|
this.knotRef.current.points = simplifyCurve(this.ghostPath, 15)
|
|
|
|
|
this.ghostPath = null
|
|
|
|
|
if (this.ghostPath) {
|
|
|
|
|
this.ghostPath.push(this.ghostPath[0])
|
|
|
|
|
|
|
|
|
|
const curve = resampleCurve(this.ghostPath, 10, 10)
|
|
|
|
|
curve.pop()
|
|
|
|
|
|
|
|
|
|
this.setPositions(curve)
|
|
|
|
|
|
|
|
|
|
this.ghostPath = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.blackHolePosition = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update() {}
|
|
|
|
|
setPositions(positions) {
|
|
|
|
|
this.particleSimulation = createSimulation(
|
|
|
|
|
positions,
|
|
|
|
|
Array.from({ length: positions.length }, () => [0, 0]),
|
|
|
|
|
Array.from({ length: positions.length }, () => [0, 0])
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update() {
|
|
|
|
|
if (!this.particleSimulation) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { positions, velocities } = this.particleSimulation
|
|
|
|
|
const N = positions.length
|
|
|
|
|
|
|
|
|
|
const newAccelerations = Array.from({ length: N }, () => [0, 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)
|
|
|
|
|
|
|
|
|
|
if (this.blackHolePosition) {
|
|
|
|
|
const v = Vec2.sub(this.blackHolePosition, curr)
|
|
|
|
|
const dir = Vec2.normalize(v)
|
|
|
|
|
const dist = Vec2.norm2(v)
|
|
|
|
|
|
|
|
|
|
Vec2.Mutate.add(newAccelerations[i], Vec2.scale(dir, 1e6 / Math.max(dist, 75) ** 3))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// barrette rigide
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
positions[i] = Vec2.add(curr, Vec2.scale(v, 0.5 * factor))
|
|
|
|
|
|
|
|
|
|
const nextPos = Vec2.add(next, Vec2.scale(v, -0.5 * factor))
|
|
|
|
|
next[0] = nextPos[0]
|
|
|
|
|
next[1] = nextPos[1]
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.particleSimulation.update(newAccelerations)
|
|
|
|
|
|
|
|
|
|
// attrito viscoso
|
|
|
|
|
for (let i = 0; i < N; i++) {
|
|
|
|
|
Vec2.Mutate.scale(this.particleSimulation.velocities[i], 0.1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.knotRef.current.points = this.particleSimulation.positions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {CanvasRenderingContext2D} g */
|
|
|
|
|
render(g) {
|
|
|
|
|
const w = g.canvas.width
|
|
|
|
|
const h = g.canvas.height
|
|
|
|
|
const knot = this.knotRef.current
|
|
|
|
|
|
|
|
|
|
g.clearRect(0, 0, w, h)
|
|
|
|
|
|
|
|
|
|
g.fillStyle = '#fff'
|
|
|
|
|
g.fillRect(0, 0, w, h)
|
|
|
|
|
|
|
|
|
|
g.lineWidth = 3
|
|
|
|
|
g.lineCap = 'round'
|
|
|
|
|
g.lineJoin = 'round'
|
|
|
|
@ -45,22 +168,80 @@ class KnotSimulation {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.stroke()
|
|
|
|
|
} else if (knot.points.length > 0) {
|
|
|
|
|
|
|
|
|
|
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.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.particleSimulation) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const positions = this.particleSimulation.positions
|
|
|
|
|
const particleCount = positions.length
|
|
|
|
|
|
|
|
|
|
if (particleCount > 0) {
|
|
|
|
|
g.beginPath()
|
|
|
|
|
const [x0, y0] = knot.points[0]
|
|
|
|
|
const [x0, y0] = positions[0]
|
|
|
|
|
g.moveTo(x0, y0)
|
|
|
|
|
|
|
|
|
|
for (const [x, y] of knot.points) {
|
|
|
|
|
g.lineTo(x, y)
|
|
|
|
|
}
|
|
|
|
|
g.stroke()
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
g.fillStyle = '#080'
|
|
|
|
|
for (const [x, y] of knot.points) {
|
|
|
|
|
g.beginPath()
|
|
|
|
|
g.ellipse(x, y, 3, 3, 0, 0, Math.PI * 2)
|
|
|
|
|
g.fill()
|
|
|
|
|
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.restore()
|
|
|
|
|
|
|
|
|
|
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.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()
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -79,16 +260,14 @@ export const KnotLayer = ({ knotRef }) => {
|
|
|
|
|
let simTimerHandle
|
|
|
|
|
|
|
|
|
|
if (canvasRef.current) {
|
|
|
|
|
canvasRef.current.width = canvasRef.current.offsetWidth
|
|
|
|
|
canvasRef.current.height = canvasRef.current.offsetHeight
|
|
|
|
|
|
|
|
|
|
const g = canvasRef.current.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(g))
|
|
|
|
|
|
|
|
|
|
console.log('Prova')
|
|
|
|
|
requestAnimationFrame(() => knotSim.render($canvas.graphicsContext))
|
|
|
|
|
}, 1000 / 30)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -103,17 +282,20 @@ export const KnotLayer = ({ knotRef }) => {
|
|
|
|
|
<canvas
|
|
|
|
|
ref={canvasRef}
|
|
|
|
|
onMouseDown={e => {
|
|
|
|
|
if (e.buttons === 1) {
|
|
|
|
|
knotSim.onMouseDrag(e.offsetX, e.offsetY)
|
|
|
|
|
}
|
|
|
|
|
knotSim.onMouseDown(e.offsetX, e.offsetY, e.buttons)
|
|
|
|
|
}}
|
|
|
|
|
onMouseMove={e => {
|
|
|
|
|
if (e.buttons === 1) {
|
|
|
|
|
knotSim.onMouseDrag(e.offsetX, e.offsetY)
|
|
|
|
|
if (canvasRef.current && e.buttons > 0) {
|
|
|
|
|
knotSim.onMouseDrag(e.offsetX, e.offsetY, e.buttons)
|
|
|
|
|
requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext))
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onContextMenu={e => e.preventDefault()}
|
|
|
|
|
onMouseUp={e => {
|
|
|
|
|
knotSim.onMouseUp()
|
|
|
|
|
if (canvasRef.current) {
|
|
|
|
|
knotSim.onMouseUp()
|
|
|
|
|
requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext))
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|