distance fields demo
This commit is contained in:
parent
3c61a411cd
commit
c639101942
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Distance Field Demo</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fc-geometry {
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
cell-renderer canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<cell-renderer resolution="800" image-smoothing="true"></cell-renderer>
|
||||
<fc-geometry x="100" y="100" width="50" height="50"></fc-geometry>
|
||||
<fc-geometry x="100" y="200" width="50" height="50"></fc-geometry>
|
||||
<fc-geometry x="100" y="300" width="50" height="50"></fc-geometry>
|
||||
<fc-geometry x="300" y="150" width="80" height="40"></fc-geometry>
|
||||
<fc-geometry x="400" y="250" width="60" height="90"></fc-geometry>
|
||||
<fc-geometry x="200" y="400" width="100" height="100"></fc-geometry>
|
||||
<fc-geometry x="500" y="100" width="30" height="70"></fc-geometry>
|
||||
|
||||
<script type="module">
|
||||
import { FolkGeometry } from '../src/canvas/fc-geometry.ts';
|
||||
import { CellRenderer } from '../src/distanceField/cellRenderer.ts';
|
||||
|
||||
FolkGeometry.define();
|
||||
CellRenderer.define();
|
||||
|
||||
// Get all geometry elements and create points for the distance field
|
||||
const geometries = document.querySelectorAll('fc-geometry');
|
||||
const renderer = document.querySelector('cell-renderer');
|
||||
|
||||
// Update distance field when geometries move or resize
|
||||
geometries.forEach((geometry) => {
|
||||
geometry.addEventListener('move', (e) => renderer.handleGeometryUpdate(e));
|
||||
geometry.addEventListener('resize', (e) => renderer.handleGeometryUpdate(e));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { FolkGeometry } from '../canvas/fc-geometry';
|
||||
import { parseVertex } from './utils';
|
||||
import { FolkGeometry } from '../canvas/fc-geometry.ts';
|
||||
import { parseVertex } from './utils.ts';
|
||||
import { ClientRectObserverEntry, ClientRectObserverManager } from '../client-rect-observer.ts';
|
||||
|
||||
const clientRectObserver = new ClientRectObserverManager();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
import type { FolkGeometry } from '../canvas/fc-geometry.ts';
|
||||
import type { Vector2 } from '../utils/Vector2.ts';
|
||||
import { Fields } from './fields.ts';
|
||||
|
||||
export class CellRenderer extends HTMLElement {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private offscreenCtx: CanvasRenderingContext2D;
|
||||
private fields: Fields;
|
||||
private resolution: number;
|
||||
private imageSmoothing: boolean;
|
||||
static tagName = 'cell-renderer';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.resolution = 2000; // default resolution
|
||||
this.imageSmoothing = true;
|
||||
this.fields = new Fields(this.resolution);
|
||||
|
||||
const { ctx, offscreenCtx } = this.createCanvas(
|
||||
window.innerWidth,
|
||||
window.innerHeight,
|
||||
this.resolution,
|
||||
this.resolution
|
||||
);
|
||||
|
||||
this.ctx = ctx;
|
||||
this.offscreenCtx = offscreenCtx;
|
||||
|
||||
this.renderDistanceField();
|
||||
}
|
||||
|
||||
static define() {
|
||||
customElements.define(this.tagName, this);
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['resolution', 'image-smoothing'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
||||
if (name === 'resolution') {
|
||||
this.resolution = parseInt(newValue, 10);
|
||||
this.fields = new Fields(this.resolution);
|
||||
} else if (name === 'image-smoothing') {
|
||||
this.imageSmoothing = newValue === 'true';
|
||||
if (this.ctx) {
|
||||
this.ctx.imageSmoothingEnabled = this.imageSmoothing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderDistanceField() {
|
||||
const imageData = this.offscreenCtx.getImageData(0, 0, this.resolution, this.resolution);
|
||||
|
||||
for (let row = 0; row < this.resolution; row++) {
|
||||
for (let col = 0; col < this.resolution; col++) {
|
||||
const index = (col * this.resolution + row) * 4;
|
||||
const distance = this.fields.getDistance(row, col);
|
||||
const color = this.fields.getColor(row, col);
|
||||
|
||||
const maxDistance = 10;
|
||||
const normalizedDistance = Math.sqrt(distance) / maxDistance;
|
||||
const baseColor = {
|
||||
r: (color * 7) % 256,
|
||||
g: (color * 13) % 256,
|
||||
b: (color * 19) % 256,
|
||||
};
|
||||
|
||||
imageData.data[index] = baseColor.r * (1 - normalizedDistance);
|
||||
imageData.data[index + 1] = baseColor.g * (1 - normalizedDistance);
|
||||
imageData.data[index + 2] = baseColor.b * (1 - normalizedDistance);
|
||||
imageData.data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
this.offscreenCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Draw scaled version to main canvas
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.drawImage(
|
||||
this.offscreenCtx.canvas,
|
||||
0,
|
||||
0,
|
||||
this.resolution,
|
||||
this.resolution,
|
||||
0,
|
||||
0,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
}
|
||||
|
||||
// Public methods
|
||||
reset() {
|
||||
this.fields = new Fields(this.resolution);
|
||||
}
|
||||
|
||||
private transformToFieldCoordinates(point: Vector2): Vector2 {
|
||||
// Transform from screen coordinates to field coordinates (0 to resolution)
|
||||
return {
|
||||
x: (point.x / this.canvas.width) * this.resolution,
|
||||
y: (point.y / this.canvas.height) * this.resolution,
|
||||
};
|
||||
}
|
||||
|
||||
addShape(points: Vector2[], isClosed = true) {
|
||||
// Transform each point from screen coordinates to field coordinates
|
||||
const transformedPoints = points.map((point) => this.transformToFieldCoordinates(point));
|
||||
this.fields.addShape(transformedPoints, isClosed);
|
||||
this.renderDistanceField();
|
||||
}
|
||||
|
||||
removeShape(index: number) {
|
||||
this.fields.removeShape(index);
|
||||
this.renderDistanceField();
|
||||
}
|
||||
|
||||
private createCanvas(width: number, height: number, offScreenWidth: number, offScreenHeight: number) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
const offscreenCanvas = document.createElement('canvas');
|
||||
|
||||
// Set canvas styles to ensure it stays behind other elements
|
||||
this.canvas.style.position = 'absolute';
|
||||
this.canvas.style.top = '0';
|
||||
this.canvas.style.left = '0';
|
||||
this.canvas.style.width = '100%';
|
||||
this.canvas.style.height = '100%';
|
||||
this.canvas.style.zIndex = '-1';
|
||||
|
||||
offscreenCanvas.width = offScreenWidth;
|
||||
offscreenCanvas.height = offScreenHeight;
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
|
||||
const offscreenCtx = offscreenCanvas.getContext('2d', {
|
||||
willReadFrequently: true,
|
||||
});
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
|
||||
if (!ctx || !offscreenCtx) throw new Error('Could not get context');
|
||||
ctx.imageSmoothingEnabled = this.imageSmoothing;
|
||||
|
||||
this.appendChild(this.canvas);
|
||||
return { ctx, offscreenCtx };
|
||||
}
|
||||
|
||||
handleGeometryUpdate(event: Event) {
|
||||
const geometry = event.target as HTMLElement;
|
||||
const index = Array.from(document.querySelectorAll('fc-geometry')).indexOf(geometry as FolkGeometry);
|
||||
if (index === -1) return;
|
||||
|
||||
const rect = geometry.getBoundingClientRect();
|
||||
const points = [
|
||||
{ x: rect.x, y: rect.y },
|
||||
{ x: rect.x + rect.width, y: rect.y },
|
||||
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
||||
{ x: rect.x, y: rect.y + rect.height },
|
||||
];
|
||||
|
||||
if (index < this.fields.shapes.length) {
|
||||
this.updateShape(index, points, true);
|
||||
} else {
|
||||
this.addShape(points, true);
|
||||
}
|
||||
}
|
||||
|
||||
updateShape(index: number, points: Vector2[], isClosed = true) {
|
||||
// Transform each point from screen coordinates to field coordinates
|
||||
const transformedPoints = points.map((point) => this.transformToFieldCoordinates(point));
|
||||
this.fields.updateShape(index, transformedPoints, isClosed);
|
||||
this.renderDistanceField();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import type { Vector2 } from '../utils/Vector2.ts';
|
||||
import { findHullParabolas, transpose } from './utils.ts';
|
||||
|
||||
export function computeCPT(
|
||||
sedt: Float32Array[],
|
||||
cpt: Vector2[][],
|
||||
xcoords: Float32Array[],
|
||||
ycoords: Float32Array[]
|
||||
): Vector2[][] {
|
||||
const length = sedt.length;
|
||||
|
||||
for (let row = 0; row < length; row++) {
|
||||
horizontalPass(sedt[row], xcoords[row]);
|
||||
}
|
||||
|
||||
transpose(sedt);
|
||||
|
||||
for (let row = 0; row < length; row++) {
|
||||
horizontalPass(sedt[row], ycoords[row]);
|
||||
}
|
||||
|
||||
for (let col = 0; col < length; col++) {
|
||||
for (let row = 0; row < length; row++) {
|
||||
const y = ycoords[col][row];
|
||||
const x = xcoords[y][col];
|
||||
cpt[row][col] = { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
return cpt;
|
||||
}
|
||||
|
||||
function horizontalPass(singleRow: Float32Array, indices: Float32Array) {
|
||||
const hullVertices: Vector2[] = [];
|
||||
const hullIntersections: Vector2[] = [];
|
||||
findHullParabolas(singleRow, hullVertices, hullIntersections);
|
||||
marchParabolas(singleRow, hullVertices, hullIntersections, indices);
|
||||
}
|
||||
|
||||
function marchParabolas(row: Float32Array, verts: Vector2[], intersections: Vector2[], indices: Float32Array) {
|
||||
let k = 0;
|
||||
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
while (intersections[k + 1].x < i) {
|
||||
k++;
|
||||
}
|
||||
const dx = i - verts[k].x;
|
||||
row[i] = dx * dx + verts[k].y;
|
||||
indices[i] = verts[k].x;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { findHullParabolas, transpose } from './utils.ts';
|
||||
import type { Vector2 } from '../utils/Vector2.ts';
|
||||
|
||||
// TODO: test performance of non-square sedt
|
||||
export function computeEDT(sedt: Float32Array[]): Float32Array[] {
|
||||
for (let row = 0; row < sedt.length; row++) {
|
||||
horizontalPass(sedt[row]);
|
||||
}
|
||||
transpose(sedt);
|
||||
|
||||
for (let row = 0; row < sedt.length; row++) {
|
||||
horizontalPass(sedt[row]);
|
||||
}
|
||||
transpose(sedt);
|
||||
|
||||
return sedt.map((row) => row.map(Math.sqrt));
|
||||
}
|
||||
|
||||
function horizontalPass(singleRow: Float32Array) {
|
||||
const hullVertices: Vector2[] = [];
|
||||
const hullIntersections: Vector2[] = [];
|
||||
findHullParabolas(singleRow, hullVertices, hullIntersections);
|
||||
marchParabolas(singleRow, hullVertices, hullIntersections);
|
||||
}
|
||||
|
||||
function marchParabolas(row: Float32Array, verts: Vector2[], intersections: Vector2[]) {
|
||||
let k = 0;
|
||||
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
while (intersections[k + 1].x < i) {
|
||||
k++;
|
||||
}
|
||||
const dx = i - verts[k].x;
|
||||
row[i] = dx * dx + verts[k].y;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
import type { Vector2 } from '../utils/Vector2.ts';
|
||||
import { computeCPT } from './cpt.ts';
|
||||
|
||||
export class Fields {
|
||||
private edt: Float32Array[] = [];
|
||||
private cpt: Vector2[][] = [];
|
||||
private colorField: Float32Array[] = [];
|
||||
private xcoords: Float32Array[] = [];
|
||||
private ycoords: Float32Array[] = [];
|
||||
private resolution: number;
|
||||
shapes: Array<{
|
||||
points: Vector2[];
|
||||
color: number;
|
||||
isClosed: boolean;
|
||||
}> = [];
|
||||
|
||||
constructor(resolution: number) {
|
||||
this.resolution = resolution + 1;
|
||||
this.initializeArrays();
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
private initializeArrays() {
|
||||
this.edt = new Array(this.resolution).fill(Infinity).map(() => new Float32Array(this.resolution).fill(Infinity));
|
||||
this.colorField = new Array(this.resolution).fill(0).map(() => new Float32Array(this.resolution).fill(0));
|
||||
this.xcoords = Array.from({ length: this.resolution }, () => new Float32Array(this.resolution).fill(0));
|
||||
this.ycoords = Array.from({ length: this.resolution }, () => new Float32Array(this.resolution).fill(0));
|
||||
this.cpt = Array.from({ length: this.resolution }, () =>
|
||||
Array.from({ length: this.resolution }, () => ({ x: 0, y: 0 }))
|
||||
);
|
||||
}
|
||||
|
||||
// Public getters for field data
|
||||
getDistance(row: number, col: number): number {
|
||||
return this.edt[row][col];
|
||||
}
|
||||
|
||||
getColor(row: number, col: number): number {
|
||||
const { x, y } = this.cpt[row][col];
|
||||
return this.colorField[x][y];
|
||||
}
|
||||
|
||||
addShape(points: Vector2[], isClosed: boolean = true) {
|
||||
const color = Math.floor(Math.random() * 255);
|
||||
this.shapes.push({ points, color, isClosed });
|
||||
this.updateFields();
|
||||
console.log(this.shapes);
|
||||
}
|
||||
|
||||
removeShape(index: number) {
|
||||
this.shapes.splice(index, 1);
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
updateFields() {
|
||||
this.boolifyFields(this.edt, this.colorField);
|
||||
this.cpt = computeCPT(this.edt, this.cpt, this.xcoords, this.ycoords);
|
||||
this.deriveEDTfromCPT();
|
||||
}
|
||||
|
||||
deriveEDTfromCPT() {
|
||||
for (let x = 0; x < this.resolution; x++) {
|
||||
for (let y = 0; y < this.resolution; y++) {
|
||||
const { x: closestX, y: closestY } = this.cpt[y][x];
|
||||
const distance = Math.sqrt((x - closestX) ** 2 + (y - closestY) ** 2);
|
||||
this.edt[y][x] = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolifyFields(distanceField: Float32Array[], colorField: Float32Array[]): void {
|
||||
const LARGE_NUMBER = 1000000000000;
|
||||
const size = distanceField.length;
|
||||
const cellSize = 1;
|
||||
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let y = 0; y < size; y++) {
|
||||
distanceField[x][y] = LARGE_NUMBER;
|
||||
colorField[y][x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const drawLine = (start: Vector2, end: Vector2, color: number) => {
|
||||
const startCell = {
|
||||
x: Math.floor(start.x / cellSize),
|
||||
y: Math.floor(start.y / cellSize),
|
||||
};
|
||||
const endCell = {
|
||||
x: Math.floor(end.x / cellSize),
|
||||
y: Math.floor(end.y / cellSize),
|
||||
};
|
||||
if (startCell.x < 0 || startCell.x >= size || startCell.y < 0 || startCell.y >= size) {
|
||||
return;
|
||||
}
|
||||
if (endCell.x < 0 || endCell.x >= size || endCell.y < 0 || endCell.y >= size) {
|
||||
return;
|
||||
}
|
||||
if (startCell.x === endCell.x && startCell.y === endCell.y) {
|
||||
distanceField[startCell.x][startCell.y] = 0;
|
||||
colorField[startCell.y][startCell.x] = color;
|
||||
return;
|
||||
}
|
||||
|
||||
let x0 = startCell.x;
|
||||
let y0 = startCell.y;
|
||||
const x1 = endCell.x;
|
||||
const y1 = endCell.y;
|
||||
|
||||
const dx = Math.abs(x1 - x0);
|
||||
const dy = Math.abs(y1 - y0);
|
||||
const sx = x0 < x1 ? 1 : -1;
|
||||
const sy = y0 < y1 ? 1 : -1;
|
||||
let err = dx - dy;
|
||||
|
||||
while (true) {
|
||||
distanceField[x0][y0] = 0;
|
||||
colorField[y0][x0] = color;
|
||||
if (x0 === x1 && y0 === y1) break;
|
||||
const e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const shape of this.shapes) {
|
||||
const { points, color, isClosed } = shape;
|
||||
const length = isClosed ? points.length : points.length - 1;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const start = points[i];
|
||||
const end = points[(i + 1) % points.length];
|
||||
drawLine(start, end, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Color = {
|
||||
simpleColorFunc: (d: number) => {
|
||||
return { r: 250 - d * 2, g: 250 - d * 5, b: 250 - d * 3 };
|
||||
},
|
||||
simpleModuloColorFunc: (d: number) => {
|
||||
const period = 18;
|
||||
const modulo = d % period;
|
||||
return { r: modulo * period, g: (modulo * period) / 3, b: (modulo * period) / 2 };
|
||||
},
|
||||
moduloColorFunc: (d: number) => {
|
||||
const dPeriod = d % 15;
|
||||
return { r: dPeriod * 10, g: dPeriod * 20, b: dPeriod * 30 };
|
||||
},
|
||||
grayscaleColorFunc: (d: number) => {
|
||||
const value = 255 - Math.abs(d) * 10;
|
||||
return { r: value, g: value, b: value };
|
||||
},
|
||||
heatmapColorFunc: (d: number) => {
|
||||
const value = Math.min(255, Math.max(0, 255 - Math.abs(d) * 10));
|
||||
return { r: value, g: 0, b: 255 - value };
|
||||
},
|
||||
invertedColorFunc: (d: number) => {
|
||||
const value = Math.abs(d) % 255;
|
||||
return { r: 255 - value, g: 255 - value, b: 255 - value };
|
||||
},
|
||||
rainbowColorFunc: (d: number) => {
|
||||
const value = Math.abs(d) % 255;
|
||||
return { r: (value * 5) % 255, g: (value * 3) % 255, b: (value * 7) % 255 };
|
||||
},
|
||||
};
|
||||
|
||||
public renderEDT(canvas: CanvasRenderingContext2D): void {
|
||||
const imageData = canvas.getImageData(0, 0, this.resolution, this.resolution);
|
||||
for (let row = 0; row < this.resolution; row++) {
|
||||
for (let col = 0; col < this.resolution; col++) {
|
||||
const index = (col * this.resolution + row) * 4;
|
||||
const distance = this.edt[row][col];
|
||||
const color = this.Color.simpleColorFunc(distance);
|
||||
imageData.data[index] = color.r;
|
||||
imageData.data[index + 1] = color.g;
|
||||
imageData.data[index + 2] = color.b;
|
||||
imageData.data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
canvas.putImageData(imageData, 0, 0);
|
||||
}
|
||||
public renderCPT(canvas: CanvasRenderingContext2D): void {
|
||||
const imageData = canvas.getImageData(0, 0, this.resolution, this.resolution);
|
||||
|
||||
for (let row = 0; row < this.resolution; row++) {
|
||||
for (let col = 0; col < this.resolution; col++) {
|
||||
const { x, y } = this.cpt[row][col];
|
||||
const shapeColor = this.colorField[x][y];
|
||||
const color = {
|
||||
r: (shapeColor * 7) % 150,
|
||||
g: (shapeColor * 13) % 200,
|
||||
b: (shapeColor * 19) % 250,
|
||||
};
|
||||
const index = (col * this.resolution + row) * 4;
|
||||
imageData.data[index] = color.r;
|
||||
imageData.data[index + 1] = color.g;
|
||||
imageData.data[index + 2] = color.b;
|
||||
imageData.data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
canvas.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
public renderBoth(canvas: CanvasRenderingContext2D): void {
|
||||
const imageData = canvas.getImageData(0, 0, this.resolution, this.resolution);
|
||||
|
||||
for (let row = 0; row < this.resolution; row++) {
|
||||
for (let col = 0; col < this.resolution; col++) {
|
||||
const index = (col * this.resolution + row) * 4;
|
||||
const distance = this.edt[row][col];
|
||||
const { x, y } = this.cpt[row][col];
|
||||
const shapeColor = this.colorField[x][y] % 200;
|
||||
|
||||
const maxDistance = 10;
|
||||
const normalizedDistance = Math.sqrt(distance) / maxDistance;
|
||||
const baseColor = {
|
||||
r: (shapeColor * 7) % 256,
|
||||
g: (shapeColor * 13) % 256,
|
||||
b: (shapeColor * 19) % 256,
|
||||
};
|
||||
const color = {
|
||||
r: baseColor.r * (1 - normalizedDistance),
|
||||
g: baseColor.g * (1 - normalizedDistance),
|
||||
b: baseColor.b * (1 - normalizedDistance),
|
||||
};
|
||||
|
||||
imageData.data[index] = color.r;
|
||||
imageData.data[index + 1] = color.g;
|
||||
imageData.data[index + 2] = color.b;
|
||||
imageData.data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
canvas.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
updateShape(index: number, points: Vector2[], isClosed: boolean = true) {
|
||||
if (index >= 0 && index < this.shapes.length) {
|
||||
const existingColor = this.shapes[index].color;
|
||||
this.shapes[index] = { points, color: existingColor, isClosed };
|
||||
this.updateFields();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import type { Vector2 } from '../utils/Vector2.ts';
|
||||
|
||||
export function intersectParabolas(p: Vector2, q: Vector2): Vector2 {
|
||||
const x = (q.y + q.x * q.x - (p.y + p.x * p.x)) / (2 * q.x - 2 * p.x);
|
||||
return { x, y: 0 };
|
||||
}
|
||||
|
||||
export function transpose(matrix: Float32Array[]) {
|
||||
for (let i = 0; i < matrix.length; i++) {
|
||||
for (let j = i + 1; j < matrix[i].length; j++) {
|
||||
const temp = matrix[i][j];
|
||||
matrix[i][j] = matrix[j][i];
|
||||
matrix[j][i] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findHullParabolas(row: Float32Array, verts: Vector2[], intersections: Vector2[]) {
|
||||
let k = 0;
|
||||
|
||||
verts[0] = { x: 0, y: row[0] };
|
||||
intersections[0] = { x: -Infinity, y: 0 };
|
||||
intersections[1] = { x: Infinity, y: 0 };
|
||||
|
||||
for (let i = 1; i < row.length; i++) {
|
||||
const q: Vector2 = { x: i, y: row[i] };
|
||||
let p = verts[k];
|
||||
let s = intersectParabolas(p, q);
|
||||
|
||||
while (s.x <= intersections[k].x) {
|
||||
k--;
|
||||
p = verts[k];
|
||||
s = intersectParabolas(p, q);
|
||||
}
|
||||
|
||||
k++;
|
||||
verts[k] = q;
|
||||
intersections[k] = s;
|
||||
intersections[k + 1] = { x: Infinity, y: 0 };
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue