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 { FolkGeometry } from '../canvas/fc-geometry.ts';
|
||||||
import { parseVertex } from './utils';
|
import { parseVertex } from './utils.ts';
|
||||||
import { ClientRectObserverEntry, ClientRectObserverManager } from '../client-rect-observer.ts';
|
import { ClientRectObserverEntry, ClientRectObserverManager } from '../client-rect-observer.ts';
|
||||||
|
|
||||||
const clientRectObserver = new ClientRectObserverManager();
|
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