313 lines
7.7 KiB
TypeScript
313 lines
7.7 KiB
TypeScript
import { Vector } from "@bearmetal/doodler";
|
|
|
|
export class ComplexPath {
|
|
points: Vector[] = [];
|
|
segments: PathSegment[] = [];
|
|
|
|
radius = 50;
|
|
|
|
ctx?: CanvasRenderingContext2D;
|
|
evenPoints: Vector[] = [];
|
|
|
|
constructor(points?: Vector[]) {
|
|
points && (this.points = points);
|
|
}
|
|
|
|
setContext(ctx: CanvasRenderingContext2D) {
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
draw() {
|
|
if (!this.ctx || !this.points.length) return;
|
|
const ctx = this.ctx;
|
|
|
|
ctx.save();
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = "white";
|
|
ctx.setLineDash([21, 6]);
|
|
|
|
let last = this.points[this.points.length - 1];
|
|
|
|
for (const point of this.points) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(last.x, last.y);
|
|
ctx.lineTo(point.x, point.y);
|
|
ctx.stroke();
|
|
last = point;
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
followEvenPoints(t: number) {
|
|
if (t < 0) t += this.evenPoints.length;
|
|
const i = Math.floor(t);
|
|
const a = this.evenPoints[i];
|
|
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
|
|
|
|
return Vector.lerp(a, b, t % 1);
|
|
}
|
|
|
|
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
|
// this.pointSpacing = 1;
|
|
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
|
|
const points: Vector[] = [];
|
|
|
|
points.push(this.segments[0].points[0]);
|
|
let prev = points[0];
|
|
let distSinceLastEvenPoint = 0;
|
|
for (const seg of this.segments) {
|
|
let t = 0;
|
|
|
|
const div = Math.ceil(seg.length * resolution * 10);
|
|
while (t < 1) {
|
|
t += 1 / div;
|
|
const point = seg.getPointAtT(t);
|
|
distSinceLastEvenPoint += prev.dist(point);
|
|
|
|
if (distSinceLastEvenPoint >= spacing) {
|
|
const overshoot = distSinceLastEvenPoint - spacing;
|
|
const evenPoint = Vector.add(
|
|
point,
|
|
Vector.sub(point, prev).normalize().mult(overshoot),
|
|
);
|
|
distSinceLastEvenPoint = overshoot;
|
|
points.push(evenPoint);
|
|
prev = evenPoint;
|
|
}
|
|
|
|
prev = point;
|
|
}
|
|
}
|
|
|
|
this.evenPoints = points;
|
|
|
|
return points;
|
|
}
|
|
}
|
|
|
|
export class PathSegment {
|
|
id: string;
|
|
points: [Vector, Vector, Vector, Vector];
|
|
|
|
length: number;
|
|
startingLength: number;
|
|
|
|
next?: PathSegment;
|
|
prev?: PathSegment;
|
|
|
|
constructor(points: [Vector, Vector, Vector, Vector]) {
|
|
this.id = crypto.randomUUID();
|
|
this.points = points;
|
|
this.length = this.calculateApproxLength(100);
|
|
this.startingLength = Math.round(this.length);
|
|
}
|
|
|
|
getPointAtT(t: number) {
|
|
const [a, b, c, d] = this.points;
|
|
const res = a.copy();
|
|
|
|
res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t));
|
|
res.add(
|
|
Vector.add(
|
|
Vector.add(a.copy().mult(3), b.copy().mult(-6)),
|
|
c.copy().mult(3),
|
|
).mult(Math.pow(t, 2)),
|
|
);
|
|
res.add(
|
|
Vector.add(
|
|
Vector.add(a.copy().mult(-1), b.copy().mult(3)),
|
|
Vector.add(c.copy().mult(-3), d.copy()),
|
|
).mult(Math.pow(t, 3)),
|
|
);
|
|
|
|
return res;
|
|
}
|
|
|
|
getClosestPoint(v: Vector): [Vector, number, number] {
|
|
const samples = 25;
|
|
const resolution = 1 / samples;
|
|
let closest = this.points[0];
|
|
let closestDistance = this.points[0].dist(v);
|
|
let closestT = 0;
|
|
|
|
for (let i = 0; i < samples; i++) {
|
|
const point = this.getPointAtT(i * resolution);
|
|
const distance = v.dist(point);
|
|
if (distance < closestDistance) {
|
|
closest = point;
|
|
closestDistance = distance;
|
|
closestT = i * resolution;
|
|
}
|
|
}
|
|
|
|
return [closest, closestDistance, closestT];
|
|
}
|
|
|
|
getPointsWithinRadius(v: Vector, r: number) {
|
|
const points: [number, PathSegment][] = [];
|
|
const samples = 25;
|
|
const resolution = 1 / samples;
|
|
|
|
for (let i = 0; i < samples; i++) {
|
|
const point = this.getPointAtT(i * resolution);
|
|
const distance = v.dist(point);
|
|
if (distance < r) {
|
|
points.push([i * resolution, this]);
|
|
}
|
|
}
|
|
return points;
|
|
}
|
|
|
|
tangent(t: number) {
|
|
// dP(t) / dt = -3(1-t)^2 * P0 + 3(1-t)^2 * P1 - 6t(1-t) * P1 - 3t^2 * P2 + 6t(1-t) * P2 + 3t^2 * P3
|
|
const [a, b, c, d] = this.points;
|
|
|
|
const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2));
|
|
res.add(
|
|
Vector.add(
|
|
Vector.sub(c, b).mult(6 * (1 - t) * t),
|
|
Vector.sub(d, c).mult(3 * Math.pow(t, 2)),
|
|
),
|
|
);
|
|
|
|
return res;
|
|
}
|
|
|
|
doesIntersectCircle(x: number, y: number, r: number) {
|
|
const v = new Vector(x, y);
|
|
const samples = 25;
|
|
const resolution = 1 / samples;
|
|
|
|
let distance = Infinity;
|
|
let t;
|
|
|
|
for (let i = 0; i < samples; i++) {
|
|
if (i !== samples - 1) {
|
|
const a = this.getPointAtT(i * resolution);
|
|
const b = this.getPointAtT((i + 1) * resolution);
|
|
const ac = Vector.sub(v, a);
|
|
const ab = Vector.sub(b, a);
|
|
|
|
const d = Vector.add(Vector.vectorProjection(ac, ab), a);
|
|
const ad = Vector.sub(d, a);
|
|
|
|
const k = Math.abs(ab.x) > Math.abs(ab.y) ? ad.x / ab.x : ad.y / ab.y;
|
|
|
|
let dist;
|
|
if (k <= 0.0) {
|
|
dist = Vector.hypot2(v, a);
|
|
} else if (k >= 1.0) {
|
|
dist = Vector.hypot2(v, b);
|
|
}
|
|
|
|
dist = Vector.hypot2(v, d);
|
|
|
|
if (dist < distance) {
|
|
distance = dist;
|
|
t = i * resolution;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (distance < r) return t;
|
|
|
|
return false;
|
|
}
|
|
|
|
calculateApproxLength(resolution = 25) {
|
|
const stepSize = 1 / resolution;
|
|
const points: Vector[] = [];
|
|
for (let i = 0; i <= resolution; i++) {
|
|
const current = stepSize * i;
|
|
points.push(this.getPointAtT(current));
|
|
}
|
|
this.length =
|
|
points.reduce((acc: { prev?: Vector; length: number }, cur) => {
|
|
const prev = acc.prev;
|
|
acc.prev = cur;
|
|
if (!prev) return acc;
|
|
acc.length += cur.dist(prev);
|
|
return acc;
|
|
}, { prev: undefined, length: 0 }).length;
|
|
return this.length;
|
|
}
|
|
|
|
calculateEvenlySpacedPoints(
|
|
spacing: number,
|
|
resolution = 1,
|
|
targetLength?: number,
|
|
) {
|
|
const points: Vector[] = [];
|
|
|
|
points.push(this.points[0]);
|
|
let prev = points[0];
|
|
let distSinceLastEvenPoint = 0;
|
|
|
|
let t = 0;
|
|
|
|
const div = Math.ceil(this.length * resolution * 10);
|
|
while (t < 1) {
|
|
t += 1 / div;
|
|
const point = this.getPointAtT(t);
|
|
distSinceLastEvenPoint += prev.dist(point);
|
|
|
|
if (distSinceLastEvenPoint >= spacing) {
|
|
const overshoot = distSinceLastEvenPoint - spacing;
|
|
const evenPoint = Vector.add(
|
|
point,
|
|
Vector.sub(point, prev).normalize().mult(overshoot),
|
|
);
|
|
distSinceLastEvenPoint = overshoot;
|
|
points.push(evenPoint);
|
|
prev = evenPoint;
|
|
}
|
|
|
|
prev = point;
|
|
}
|
|
|
|
if (targetLength && points.length < targetLength) {
|
|
while (points.length < targetLength) {
|
|
t += 1 / div;
|
|
const point = this.getPointAtT(t);
|
|
distSinceLastEvenPoint += prev.dist(point);
|
|
|
|
if (distSinceLastEvenPoint >= spacing) {
|
|
const overshoot = distSinceLastEvenPoint - spacing;
|
|
const evenPoint = Vector.add(
|
|
point,
|
|
Vector.sub(point, prev).normalize().mult(overshoot),
|
|
);
|
|
distSinceLastEvenPoint = overshoot;
|
|
points.push(evenPoint);
|
|
prev = evenPoint;
|
|
}
|
|
|
|
prev = point;
|
|
}
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
calculateSubdividedPoints(numberOfPoints: number) {
|
|
const points: Vector[] = [];
|
|
|
|
for (let i = 0; i < numberOfPoints; i++) {
|
|
const point = this.getPointAtT(i / numberOfPoints);
|
|
points.push(point);
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
clampLength() {
|
|
const curveLength = this.startingLength;
|
|
const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1);
|
|
if (points.length >= curveLength) {
|
|
this.points[3].set(points[curveLength]);
|
|
}
|
|
}
|
|
|
|
draw(): void {}
|
|
}
|