use svg for rope

This commit is contained in:
Orion Reed 2024-11-26 00:13:38 -05:00
parent f134073309
commit 46f6c08bc2
2 changed files with 47 additions and 63 deletions

View File

@ -1,29 +1,12 @@
// This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope // This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope
import { ResizeObserverManager } from '../resize-observer.ts'; import { ResizeObserverManager } from '../resize-observer.ts';
import { Vector, type Vector2 } from '../utils/Vector2.ts';
import { AbstractArrow } from './abstract-arrow.ts'; import { AbstractArrow } from './abstract-arrow.ts';
import { Vertex } from './utils.ts'; import { Vertex } from './utils.ts';
const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage; const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage;
type Vector2 = { x: number; y: number };
class Vector {
static zero: () => Vector2 = () => ({ x: 0, y: 0 });
static sub: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
static add: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
static mult: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x * b.x, y: a.y * b.y });
static scale: (v: Vector2, scaleFactor: number) => Vector2 = (v, scaleFactor) => ({
x: v.x * scaleFactor,
y: v.y * scaleFactor,
});
static mag: (v: Vector2) => number = (v) => Math.sqrt(v.x * v.x + v.y * v.y);
static normalized: (v: Vector2) => Vector2 = (v) => {
const mag = Vector.mag(v);
return mag === 0 ? Vector.zero() : { x: v.x / mag, y: v.y / mag };
};
}
// 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"! // 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"!
interface RopePoint { interface RopePoint {
pos: Vertex; pos: Vertex;
@ -48,8 +31,8 @@ declare global {
export class FolkRope extends AbstractArrow { export class FolkRope extends AbstractArrow {
static override tagName = 'fc-rope'; static override tagName = 'fc-rope';
#canvas = document.createElement('canvas'); #svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
#context = this.#canvas.getContext('2d')!; #path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
#shadow = this.attachShadow({ mode: 'open' }); #shadow = this.attachShadow({ mode: 'open' });
#rAFId = -1; #rAFId = -1;
@ -77,7 +60,8 @@ export class FolkRope extends AbstractArrow {
constructor() { constructor() {
super(); super();
this.#shadow.appendChild(this.#canvas); this.#svg.appendChild(this.#path);
this.#shadow.appendChild(this.#svg);
} }
override connectedCallback(): void { override connectedCallback(): void {
@ -94,8 +78,8 @@ export class FolkRope extends AbstractArrow {
} }
#onResize = (entry: ResizeObserverEntry) => { #onResize = (entry: ResizeObserverEntry) => {
this.#canvas.width = entry.contentRect.width; this.#svg.setAttribute('width', entry.contentRect.width.toString());
this.#canvas.height = entry.contentRect.height; this.#svg.setAttribute('height', entry.contentRect.height.toString());
this.draw(); this.draw();
}; };
@ -154,20 +138,18 @@ export class FolkRope extends AbstractArrow {
} }
draw() { draw() {
this.#context.clearRect(0, 0, this.#canvas.width, this.#canvas.height); if (this.#points.length < 2) return;
for (let i = 0; i < this.#points.length; i++) { let pathData = `M ${this.#points[0].pos.x} ${this.#points[0].pos.y}`;
const p = this.#points[i];
if (p.prev) { for (let i = 1; i < this.#points.length; i++) {
this.#context.beginPath(); pathData += ` L ${this.#points[i].pos.x} ${this.#points[i].pos.y}`;
this.#context.moveTo(p.prev.pos.x, p.prev.pos.y);
this.#context.lineTo(p.pos.x, p.pos.y);
this.#context.lineWidth = 2;
this.#context.strokeStyle = this.#stroke;
this.#context.stroke();
}
} }
this.#path.setAttribute('d', pathData);
this.#path.setAttribute('stroke', this.#stroke);
this.#path.setAttribute('stroke-width', '2');
this.#path.setAttribute('fill', 'none');
} }
#generatePoints(start: Vertex, end: Vertex) { #generatePoints(start: Vertex, end: Vertex) {
@ -234,49 +216,34 @@ export class FolkRope extends AbstractArrow {
// Apply constraints related to other nodes next to it (keeps each node within distance) // Apply constraints related to other nodes next to it (keeps each node within distance)
#constrainPoint(point: RopePoint) { #constrainPoint(point: RopePoint) {
if (point.next) { const applyConstraint = (p1: RopePoint, p2: RopePoint) => {
const delta = Vector.sub(point.next.pos, point.pos); const delta = Vector.sub(p2.pos, p1.pos);
const len = Vector.mag(delta); const len = Vector.mag(delta);
const diff = len - point.distanceToNextPoint; const diff = len - p1.distanceToNextPoint;
const normal = Vector.normalized(delta); const normal = Vector.normalized(delta);
const adjustment = Vector.scale(normal, diff * 0.25);
if (!point.isFixed) { if (!p1.isFixed) {
point.pos.x += normal.x * diff * 0.25; p1.pos = Vector.add(p1.pos, adjustment);
point.pos.y += normal.y * diff * 0.25;
} }
if (!p2.isFixed) {
p2.pos = Vector.sub(p2.pos, adjustment);
}
};
if (!point.next.isFixed) { if (point.next) applyConstraint(point, point.next);
point.next.pos.x -= normal.x * diff * 0.25; if (point.prev) applyConstraint(point, point.prev);
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;
}
}
} }
cut(index = Math.floor(this.#points.length / 2)) { cut(index = Math.floor(this.#points.length / 2)) {
if (this.#points.length === 0) return; if (index < 0 || index >= this.#points.length - 1) return;
this.#points[index].next = null; this.#points[index].next = null;
this.#points[index + 1].prev = null; this.#points[index + 1].prev = null;
} }
mend(index = Math.floor(this.#points.length / 2)) { mend(index = Math.floor(this.#points.length / 2)) {
if (this.#points.length === 0) return; if (index < 0 || index >= this.#points.length - 1) return;
this.#points[index].next = this.#points[index + 1]; this.#points[index].next = this.#points[index + 1];
this.#points[index + 1].prev = this.#points[index]; this.#points[index + 1].prev = this.#points[index];

17
src/utils/Vector2.ts Normal file
View File

@ -0,0 +1,17 @@
export type Vector2 = { x: number; y: number };
export class Vector {
static zero: () => Vector2 = () => ({ x: 0, y: 0 });
static sub: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
static add: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
static mult: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x * b.x, y: a.y * b.y });
static scale: (v: Vector2, scaleFactor: number) => Vector2 = (v, scaleFactor) => ({
x: v.x * scaleFactor,
y: v.y * scaleFactor,
});
static mag: (v: Vector2) => number = (v) => Math.sqrt(v.x * v.x + v.y * v.y);
static normalized: (v: Vector2) => Vector2 = (v) => {
const mag = Vector.mag(v);
return mag === 0 ? Vector.zero() : { x: v.x / mag, y: v.y / mag };
};
}