more rope

This commit is contained in:
“chrisshank” 2024-11-16 17:44:38 -08:00
parent e99b42a626
commit 934dbeac7a
2 changed files with 96 additions and 95 deletions

View File

@ -1,8 +1,9 @@
import { AbstractArrow } from './abstract-arrow.ts'; import { AbstractArrow } from './abstract-arrow.ts';
import { Vertex } from './utils.ts';
// This is a direct port from https://github.com/guerrillacontra/html5-es6-physics-rope/blob/master/js/microlib.js // This is a direct port from https://github.com/guerrillacontra/html5-es6-physics-rope/blob/master/js/microlib.js
const lerp = (first, second, percentage) => first + (second - first) * percentage; const lerp = (first, second, percentage) => first + (second - first) * percentage;
class Vector2 { class Vector {
static zero() { static zero() {
return { x: 0, y: 0 }; return { x: 0, y: 0 };
} }
@ -28,90 +29,26 @@ class Vector2 {
} }
static normalized(v) { static normalized(v) {
const mag = Vector2.mag(v); const mag = Vector.mag(v);
if (mag === 0) { if (mag === 0) {
return Vector2.zero(); return Vector.zero();
} }
return { x: v.x / mag, y: v.y / mag }; return { x: v.x / mag, y: v.y / mag };
} }
} }
//each rope part is one of these // Each rope part is one of these uses a high precision variant of StörmerVerlet integration to keep the simulation consistent otherwise it would "explode"!
//uses a high precison varient of StörmerVerlet integration interface RopePoint {
//to keep the simulation consistant otherwise it would "explode"! pos: Vertex;
class RopePoint { distanceToNextPoint: number;
//integrates motion equations per node without taking into account relationship isFixed: boolean;
//with other nodes... oldPos: Vertex;
static integrate(point, gravity, dt, previousFrameDt) { velocity: Vertex;
if (!point.isFixed) { mass: number;
point.velocity = Vector2.sub(point.pos, point.oldPos); damping: number;
point.oldPos = { ...point.pos }; prev: RopePoint | null;
next: RopePoint | null;
//drastically improves stability
let timeCorrection = previousFrameDt != 0.0 ? dt / previousFrameDt : 0.0;
let accel = Vector2.add(gravity, { x: 0, y: point.mass });
const velCoef = timeCorrection * point.damping;
const accelCoef = Math.pow(dt, 2);
point.pos.x += point.velocity.x * velCoef + accel.x * accelCoef;
point.pos.y += point.velocity.y * velCoef + accel.y * accelCoef;
} else {
point.velocity = Vector2.zero();
point.oldPos = { ...point.pos };
}
}
//apply constraints related to other nodes next to it
//(keeps each node within distance)
static constrain(point) {
if (point.next) {
const delta = Vector2.sub(point.next.pos, point.pos);
const len = Vector2.mag(delta);
const diff = len - point.distanceToNextPoint;
const normal = Vector2.normalized(delta);
if (!point.isFixed) {
point.pos.x += normal.x * diff * 0.25;
point.pos.y += normal.y * diff * 0.25;
}
if (!point.next.isFixed) {
point.next.pos.x -= normal.x * diff * 0.25;
point.next.pos.y -= normal.y * diff * 0.25;
}
}
if (point.prev) {
const delta = Vector2.sub(point.prev.pos, point.pos);
const len = Vector2.mag(delta);
const diff = len - point.distanceToNextPoint;
const normal = Vector2.normalized(delta);
if (!point.isFixed) {
point.pos.x += normal.x * diff * 0.25;
point.pos.y += normal.y * diff * 0.25;
}
if (!point.prev.isFixed) {
point.prev.pos.x -= normal.x * diff * 0.25;
point.prev.pos.y -= normal.y * diff * 0.25;
}
}
}
constructor(initialPos, distanceToNextPoint, mass = 1, damping = 1, isFixed = false) {
this.pos = initialPos;
this.distanceToNextPoint = distanceToNextPoint;
this.isFixed = isFixed;
this.oldPos = { ...initialPos };
this.velocity = Vector2.zero();
this.mass = mass;
this.damping = damping;
this.prev = null;
this.next = null;
}
} }
declare global { declare global {
@ -157,12 +94,12 @@ export class FolkRope extends AbstractArrow {
const dts = this.#deltaTime * 0.001; const dts = this.#deltaTime * 0.001;
for (const point of this.#points) { for (const point of this.#points) {
RopePoint.integrate(point, this.#gravity, dts, this.#previousDelta); this.#integratePoint(point, this.#gravity, dts, this.#previousDelta);
} }
for (let iteration = 0; iteration < 600; iteration++) { for (let iteration = 0; iteration < 600; iteration++) {
for (const point of this.#points) { for (const point of this.#points) {
RopePoint.constrain(point); this.#constrainPoint(point);
} }
} }
@ -179,9 +116,7 @@ export class FolkRope extends AbstractArrow {
this.#points = this.generatePoints( this.#points = this.generatePoints(
{ x: 100, y: this.#canvas.height / 2 }, { x: 100, y: this.#canvas.height / 2 },
{ x: this.#canvas.width - 100, y: this.#canvas.height / 2 }, { x: this.#canvas.width - 100, y: this.#canvas.height / 2 },
5, 5
1,
0.99
); );
this.#lastTime = 0; this.#lastTime = 0;
@ -222,20 +157,31 @@ export class FolkRope extends AbstractArrow {
} }
} }
generatePoints(start, end, resolution, mass, damping) { generatePoints(start: Vertex, end: Vertex, resolution: number) {
const delta = Vector2.sub(end, start); const delta = Vector.sub(end, start);
const len = Vector2.mag(delta); const len = Vector.mag(delta);
let points: RopePoint[] = []; let points: RopePoint[] = [];
const pointsLen = Math.floor(len / resolution); const pointsLen = Math.floor(len / resolution);
for (let i = 0; i < pointsLen; i++) { for (let i = 0; i < pointsLen; i++) {
const percentage = i / (pointsLen - 1); const percentage = i / (pointsLen - 1);
const pos = {
const lerpX = lerp(start.x, end.x, percentage); x: lerp(start.x, end.x, percentage),
const lerpY = lerp(start.y, end.y, percentage); y: lerp(start.y, end.y, percentage),
const isFixed = i === 0 || i === pointsLen - 1; };
points.push(new RopePoint({ x: lerpX, y: lerpY }, resolution, mass, damping, isFixed)); // new RopePoint({ x: lerpX, y: lerpY }, resolution, mass, damping, isFixed)
points.push({
pos,
oldPos: { ...pos },
distanceToNextPoint: resolution,
mass: 1,
damping: 0.99,
velocity: Vector.zero(),
isFixed: i === 0 || i === pointsLen - 1,
prev: null,
next: null,
});
} }
//Link nodes into a doubly linked list //Link nodes into a doubly linked list
@ -250,4 +196,62 @@ export class FolkRope extends AbstractArrow {
return points; return points;
} }
// Integrate motion equations per node without taking into account relationship with other nodes...
#integratePoint(point: RopePoint, gravity, dt, previousFrameDt) {
if (!point.isFixed) {
point.velocity = Vector.sub(point.pos, point.oldPos);
point.oldPos = { ...point.pos };
//drastically improves stability
let timeCorrection = previousFrameDt != 0.0 ? dt / previousFrameDt : 0.0;
let accel = Vector.add(gravity, { x: 0, y: point.mass });
const velCoef = timeCorrection * point.damping;
const accelCoef = Math.pow(dt, 2);
point.pos.x += point.velocity.x * velCoef + accel.x * accelCoef;
point.pos.y += point.velocity.y * velCoef + accel.y * accelCoef;
} else {
point.velocity = Vector.zero();
point.oldPos = { ...point.pos };
}
}
//Apply constraints related to other nodes next to it (keeps each node within distance)
#constrainPoint(point: RopePoint) {
if (point.next) {
const delta = Vector.sub(point.next.pos, point.pos);
const len = Vector.mag(delta);
const diff = len - point.distanceToNextPoint;
const normal = Vector.normalized(delta);
if (!point.isFixed) {
point.pos.x += normal.x * diff * 0.25;
point.pos.y += normal.y * diff * 0.25;
}
if (!point.next.isFixed) {
point.next.pos.x -= normal.x * diff * 0.25;
point.next.pos.y -= normal.y * diff * 0.25;
}
}
if (point.prev) {
const delta = Vector.sub(point.prev.pos, point.pos);
const len = Vector.mag(delta);
const diff = len - point.distanceToNextPoint;
const normal = Vector.normalized(delta);
if (!point.isFixed) {
point.pos.x += normal.x * diff * 0.25;
point.pos.y += normal.y * diff * 0.25;
}
if (!point.prev.isFixed) {
point.prev.pos.x -= normal.x * diff * 0.25;
point.prev.pos.y -= normal.y * diff * 0.25;
}
}
}
} }

View File

@ -83,7 +83,7 @@ styles.replaceSync(`
:host::before { :host::before {
content: ''; content: '';
position: absolute; position: absolute;
inset: -10px -10px -10px -10px; inset: -15px -15px -15px -15px;
z-index: -1; z-index: -1;
} }
@ -91,9 +91,6 @@ div {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
}
::slotted(*) {
cursor: default; cursor: default;
} }