Merge branch 'main' of https://github.com/folk-canvas/folk-canvas
This commit is contained in:
commit
5f21c65baa
|
|
@ -13,6 +13,7 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
fc-geometry {
|
fc-geometry {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,40 @@
|
||||||
import { frag, vert } from './utils/tags.ts';
|
import { frag, vert } from './utils/tags.ts';
|
||||||
|
import { WebGLUtils } from './utils/webgl.ts';
|
||||||
|
|
||||||
/** Previously used a CPU-based implementation. https://github.com/folk-canvas/folk-canvas/commit/fdd7fb9d84d93ad665875cad25783c232fd17bcc */
|
/** Previously used a CPU-based implementation. https://github.com/folk-canvas/folk-canvas/commit/fdd7fb9d84d93ad665875cad25783c232fd17bcc */
|
||||||
export class DistanceField extends HTMLElement {
|
export class DistanceField extends HTMLElement {
|
||||||
static tagName = 'distance-field';
|
static tagName = 'distance-field';
|
||||||
|
|
||||||
static define() {
|
|
||||||
customElements.define(this.tagName, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private geometries: NodeListOf<Element>;
|
private geometries: NodeListOf<Element>;
|
||||||
private textures: WebGLTexture[] = [];
|
private textures: WebGLTexture[] = [];
|
||||||
private pingPongIndex: number = 0;
|
private pingPongIndex: number = 0;
|
||||||
|
|
||||||
private offsets!: Float32Array;
|
|
||||||
private canvas!: HTMLCanvasElement;
|
private canvas!: HTMLCanvasElement;
|
||||||
private gl!: WebGL2RenderingContext;
|
private glContext!: WebGL2RenderingContext;
|
||||||
private program!: WebGLProgram;
|
|
||||||
private displayProgram!: WebGLProgram;
|
|
||||||
private seedProgram!: WebGLProgram;
|
|
||||||
private framebuffer!: WebGLFramebuffer;
|
private framebuffer!: WebGLFramebuffer;
|
||||||
private fullscreenQuadVAO!: WebGLVertexArrayObject;
|
private fullscreenQuadVAO!: WebGLVertexArrayObject;
|
||||||
private shapeVAO!: WebGLVertexArrayObject;
|
private shapeVAO!: WebGLVertexArrayObject;
|
||||||
|
|
||||||
|
private jfaProgram!: WebGLProgram; // Jump Flooding Algorithm shader program
|
||||||
|
private renderProgram!: WebGLProgram; // Final rendering shader program
|
||||||
|
private seedProgram!: WebGLProgram; // Seed point shader program
|
||||||
|
|
||||||
|
private static readonly MAX_DISTANCE = 99999.0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.geometries = document.querySelectorAll('fc-geometry');
|
this.geometries = document.querySelectorAll('fc-geometry');
|
||||||
|
|
||||||
const { gl } = this.createWebGLCanvas(window.innerWidth, window.innerHeight);
|
const { gl, canvas } = WebGLUtils.createWebGLCanvas(window.innerWidth, window.innerHeight, this);
|
||||||
|
|
||||||
if (!gl) {
|
if (!gl || !canvas) {
|
||||||
|
console.error('Failed to initialize WebGL context.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.gl = gl;
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.glContext = gl;
|
||||||
|
|
||||||
// Initialize shaders
|
// Initialize shaders
|
||||||
this.initShaders();
|
this.initShaders();
|
||||||
|
|
@ -47,9 +49,12 @@ export class DistanceField extends HTMLElement {
|
||||||
this.runJFA();
|
this.runJFA();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle hooks
|
static define() {
|
||||||
|
customElements.define(this.tagName, this);
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// Update distance field when geometries move or resize
|
window.addEventListener('resize', this.handleResize);
|
||||||
this.geometries.forEach((geometry) => {
|
this.geometries.forEach((geometry) => {
|
||||||
geometry.addEventListener('move', this.handleGeometryUpdate);
|
geometry.addEventListener('move', this.handleGeometryUpdate);
|
||||||
geometry.addEventListener('resize', this.handleGeometryUpdate);
|
geometry.addEventListener('resize', this.handleGeometryUpdate);
|
||||||
|
|
@ -57,193 +62,36 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
// Remove event listeners
|
window.removeEventListener('resize', this.handleResize);
|
||||||
this.geometries.forEach((geometry) => {
|
this.geometries.forEach((geometry) => {
|
||||||
geometry.removeEventListener('move', this.handleGeometryUpdate);
|
geometry.removeEventListener('move', this.handleGeometryUpdate);
|
||||||
geometry.removeEventListener('resize', this.handleGeometryUpdate);
|
geometry.removeEventListener('resize', this.handleGeometryUpdate);
|
||||||
});
|
});
|
||||||
|
this.cleanupWebGLResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle updates from geometries
|
|
||||||
private handleGeometryUpdate = () => {
|
private handleGeometryUpdate = () => {
|
||||||
// Re-render seed points and rerun JFA
|
|
||||||
this.initSeedPointRendering();
|
this.initSeedPointRendering();
|
||||||
this.runJFA();
|
this.runJFA();
|
||||||
};
|
};
|
||||||
|
|
||||||
private createWebGLCanvas(width: number, height: number) {
|
|
||||||
this.canvas = document.createElement('canvas');
|
|
||||||
|
|
||||||
// Set canvas styles
|
|
||||||
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';
|
|
||||||
|
|
||||||
this.canvas.width = width;
|
|
||||||
this.canvas.height = height;
|
|
||||||
|
|
||||||
// Initialize WebGL2 context
|
|
||||||
const gl = this.canvas.getContext('webgl2');
|
|
||||||
if (!gl) {
|
|
||||||
console.error('WebGL2 is not available.');
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.appendChild(this.canvas);
|
|
||||||
return { gl };
|
|
||||||
}
|
|
||||||
|
|
||||||
private initShaders() {
|
private initShaders() {
|
||||||
const gl = this.gl;
|
this.jfaProgram = WebGLUtils.createShaderProgram(this.glContext, jfaVertShader, jfaFragShader);
|
||||||
|
this.seedProgram = WebGLUtils.createShaderProgram(this.glContext, seedVertexShaderSource, seedFragmentShaderSource);
|
||||||
// Shader sources
|
this.renderProgram = WebGLUtils.createShaderProgram(this.glContext, renderVertShader, renderFragShader);
|
||||||
const vertexShaderSource = vert`#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
in vec2 a_position;
|
|
||||||
out vec2 v_texCoord;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
v_texCoord = a_position * 0.5 + 0.5; // Transform to [0, 1] range
|
|
||||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const fragmentShaderSource = frag`#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
precision mediump int;
|
|
||||||
|
|
||||||
in vec2 v_texCoord;
|
|
||||||
out vec4 outColor;
|
|
||||||
|
|
||||||
uniform sampler2D u_previousTexture;
|
|
||||||
uniform vec2 u_offsets[9];
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// Start with the current texel's nearest seed point and distance
|
|
||||||
vec4 nearest = texture(u_previousTexture, v_texCoord);
|
|
||||||
|
|
||||||
// Initialize minDist with the current distance
|
|
||||||
float minDist = nearest.a;
|
|
||||||
|
|
||||||
// Loop through neighbor offsets
|
|
||||||
for (int i = 0; i < 9; ++i) {
|
|
||||||
vec2 sampleCoord = v_texCoord + u_offsets[i];
|
|
||||||
|
|
||||||
// Clamp sampleCoord to [0, 1] to prevent sampling outside texture
|
|
||||||
sampleCoord = clamp(sampleCoord, vec2(0.0), vec2(1.0));
|
|
||||||
|
|
||||||
vec4 sampled = texture(u_previousTexture, sampleCoord);
|
|
||||||
|
|
||||||
// Compute distance to the seed point stored in this neighbor
|
|
||||||
float dist = distance(sampled.xy, v_texCoord);
|
|
||||||
|
|
||||||
if (dist < minDist) {
|
|
||||||
nearest = sampled;
|
|
||||||
nearest.a = dist;
|
|
||||||
minDist = dist;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output the nearest seed point and updated distance
|
|
||||||
outColor = nearest;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const displayVertexShaderSource = vert`#version 300 es
|
|
||||||
in vec2 a_position;
|
|
||||||
out vec2 v_texCoord;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
v_texCoord = a_position * 0.5 + 0.5;
|
|
||||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const displayFragmentShaderSource = frag`#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
in vec2 v_texCoord;
|
|
||||||
out vec4 outColor;
|
|
||||||
|
|
||||||
uniform sampler2D u_texture;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec4 texel = texture(u_texture, v_texCoord);
|
|
||||||
|
|
||||||
// Extract shape ID and distance
|
|
||||||
float shapeID = texel.z;
|
|
||||||
float distance = texel.a;
|
|
||||||
|
|
||||||
// Hash-based color for shape
|
|
||||||
vec3 shapeColor = vec3(
|
|
||||||
fract(sin(shapeID * 12.9898) * 43758.5453),
|
|
||||||
fract(sin(shapeID * 78.233) * 43758.5453),
|
|
||||||
fract(sin(shapeID * 93.433) * 43758.5453)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Visualize distance (e.g., as intensity)
|
|
||||||
float intensity = exp(-distance * 10.0);
|
|
||||||
|
|
||||||
outColor = vec4(shapeColor * intensity, 1.0);
|
|
||||||
}`;
|
|
||||||
|
|
||||||
// Compute offsets on the CPU
|
|
||||||
const offsets = [];
|
|
||||||
for (let y = -1; y <= 1; y++) {
|
|
||||||
for (let x = -1; x <= 1; x++) {
|
|
||||||
offsets.push(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.offsets = new Float32Array(offsets);
|
|
||||||
|
|
||||||
// Compile JFA shaders
|
|
||||||
const vertexShader = this.createShader(gl.VERTEX_SHADER, vertexShaderSource);
|
|
||||||
const fragmentShader = this.createShader(gl.FRAGMENT_SHADER, fragmentShaderSource);
|
|
||||||
this.program = this.createProgram(vertexShader, fragmentShader);
|
|
||||||
|
|
||||||
// Compile display shaders
|
|
||||||
const displayVertexShader = this.createShader(gl.VERTEX_SHADER, displayVertexShaderSource);
|
|
||||||
const displayFragmentShader = this.createShader(gl.FRAGMENT_SHADER, displayFragmentShaderSource);
|
|
||||||
this.displayProgram = this.createProgram(displayVertexShader, displayFragmentShader);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createShader(type: GLenum, source: string): WebGLShader {
|
|
||||||
const gl = this.gl;
|
|
||||||
const shader = gl.createShader(type)!;
|
|
||||||
gl.shaderSource(shader, source);
|
|
||||||
gl.compileShader(shader);
|
|
||||||
|
|
||||||
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
|
||||||
if (!success) {
|
|
||||||
console.error('Could not compile shader:', gl.getShaderInfoLog(shader));
|
|
||||||
gl.deleteShader(shader);
|
|
||||||
throw new Error('Shader compilation failed');
|
|
||||||
}
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createProgram(vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram {
|
|
||||||
const gl = this.gl;
|
|
||||||
const program = gl.createProgram()!;
|
|
||||||
gl.attachShader(program, vertexShader);
|
|
||||||
gl.attachShader(program, fragmentShader);
|
|
||||||
gl.linkProgram(program);
|
|
||||||
|
|
||||||
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
|
||||||
if (!success) {
|
|
||||||
console.error('Program failed to link:', gl.getProgramInfoLog(program));
|
|
||||||
gl.deleteProgram(program);
|
|
||||||
throw new Error('Program linking failed');
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initPingPongTextures() {
|
private initPingPongTextures() {
|
||||||
const gl = this.gl;
|
const gl = this.glContext;
|
||||||
const width = this.canvas.width;
|
const width = this.canvas.width;
|
||||||
const height = this.canvas.height;
|
const height = this.canvas.height;
|
||||||
|
|
||||||
|
// Delete existing textures to prevent memory leaks
|
||||||
|
for (const texture of this.textures) {
|
||||||
|
gl.deleteTexture(texture);
|
||||||
|
}
|
||||||
|
this.textures = [];
|
||||||
|
|
||||||
// Enable the EXT_color_buffer_float extension
|
// Enable the EXT_color_buffer_float extension
|
||||||
const ext = gl.getExtension('EXT_color_buffer_float');
|
const ext = gl.getExtension('EXT_color_buffer_float');
|
||||||
if (!ext) {
|
if (!ext) {
|
||||||
|
|
@ -277,42 +125,14 @@ export class DistanceField extends HTMLElement {
|
||||||
this.textures.push(texture);
|
this.textures.push(texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create framebuffer
|
// Reuse existing framebuffer
|
||||||
this.framebuffer = gl.createFramebuffer()!;
|
if (!this.framebuffer) {
|
||||||
|
this.framebuffer = gl.createFramebuffer()!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private initSeedPointRendering() {
|
private initSeedPointRendering() {
|
||||||
const gl = this.gl;
|
const gl = this.glContext;
|
||||||
|
|
||||||
// Shader sources for seed point rendering
|
|
||||||
const seedVertexShaderSource = vert`#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
in vec3 a_position; // x, y, shapeID
|
|
||||||
flat out float v_shapeID;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(a_position.xy, 0.0, 1.0);
|
|
||||||
v_shapeID = a_position.z; // Pass shape ID to fragment shader
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const seedFragmentShaderSource = frag`#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
flat in float v_shapeID;
|
|
||||||
uniform vec2 u_resolution;
|
|
||||||
|
|
||||||
out vec4 outColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 seedCoord = gl_FragCoord.xy / u_resolution;
|
|
||||||
outColor = vec4(seedCoord, v_shapeID, 0.0); // Seed coords, shape ID, initial distance 0
|
|
||||||
}`;
|
|
||||||
|
|
||||||
// Compile seed shaders
|
|
||||||
const seedVertexShader = this.createShader(gl.VERTEX_SHADER, seedVertexShaderSource);
|
|
||||||
const seedFragmentShader = this.createShader(gl.FRAGMENT_SHADER, seedFragmentShaderSource);
|
|
||||||
this.seedProgram = this.createProgram(seedVertexShader, seedFragmentShader);
|
|
||||||
|
|
||||||
// Set up VAO and buffer for shapes
|
// Set up VAO and buffer for shapes
|
||||||
this.shapeVAO = gl.createVertexArray()!;
|
this.shapeVAO = gl.createVertexArray()!;
|
||||||
|
|
@ -359,6 +179,7 @@ export class DistanceField extends HTMLElement {
|
||||||
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
gl.useProgram(this.seedProgram);
|
||||||
const positionLocation = gl.getAttribLocation(this.seedProgram, 'a_position');
|
const positionLocation = gl.getAttribLocation(this.seedProgram, 'a_position');
|
||||||
gl.enableVertexAttribArray(positionLocation);
|
gl.enableVertexAttribArray(positionLocation);
|
||||||
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
|
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
|
||||||
|
|
@ -370,7 +191,7 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSeedPoints() {
|
private renderSeedPoints() {
|
||||||
const gl = this.gl;
|
const gl = this.glContext;
|
||||||
|
|
||||||
// Bind framebuffer to render to the seed texture
|
// Bind framebuffer to render to the seed texture
|
||||||
const seedTexture = this.textures[this.pingPongIndex % 2];
|
const seedTexture = this.textures[this.pingPongIndex % 2];
|
||||||
|
|
@ -379,14 +200,15 @@ export class DistanceField extends HTMLElement {
|
||||||
|
|
||||||
// Clear the texture with a large initial distance
|
// Clear the texture with a large initial distance
|
||||||
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||||
gl.clearColor(0.0, 0.0, 0.0, 99999.0); // Max initial distance
|
gl.clearColor(0.0, 0.0, 0.0, DistanceField.MAX_DISTANCE);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
// Use seed shader program
|
// Use seed shader program
|
||||||
gl.useProgram(this.seedProgram);
|
gl.useProgram(this.seedProgram);
|
||||||
|
|
||||||
// Set uniforms
|
// Set the canvas size uniform
|
||||||
gl.uniform2f(gl.getUniformLocation(this.seedProgram, 'u_resolution'), this.canvas.width, this.canvas.height);
|
const canvasSizeLocation = gl.getUniformLocation(this.seedProgram, 'u_canvasSize');
|
||||||
|
gl.uniform2f(canvasSizeLocation, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
// Bind VAO and draw shapes
|
// Bind VAO and draw shapes
|
||||||
gl.bindVertexArray(this.shapeVAO);
|
gl.bindVertexArray(this.shapeVAO);
|
||||||
|
|
@ -412,7 +234,7 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPass(stepSize: number) {
|
private renderPass(stepSize: number) {
|
||||||
const gl = this.gl;
|
const gl = this.glContext;
|
||||||
|
|
||||||
// Swap textures for ping-pong rendering
|
// Swap textures for ping-pong rendering
|
||||||
const inputTexture = this.textures[this.pingPongIndex % 2];
|
const inputTexture = this.textures[this.pingPongIndex % 2];
|
||||||
|
|
@ -422,36 +244,19 @@ export class DistanceField extends HTMLElement {
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0);
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0);
|
||||||
|
|
||||||
// Check framebuffer status
|
|
||||||
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
||||||
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
||||||
console.error('Framebuffer is incomplete:', status.toString(16));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
|
|
||||||
// Use shader program
|
// Use shader program
|
||||||
gl.useProgram(this.program);
|
gl.useProgram(this.jfaProgram);
|
||||||
|
|
||||||
// Adjust offsets based on step size and resolution
|
// Compute and set the offsets uniform
|
||||||
const adjustedOffsets = [];
|
const offsets = this.computeOffsets(stepSize);
|
||||||
for (let i = 0; i < this.offsets.length; i += 2) {
|
const offsetsLocation = gl.getUniformLocation(this.jfaProgram, 'u_offsets');
|
||||||
const offsetX = (this.offsets[i] * stepSize) / this.canvas.width;
|
gl.uniform2fv(offsetsLocation, offsets);
|
||||||
const offsetY = (this.offsets[i + 1] * stepSize) / this.canvas.height;
|
|
||||||
adjustedOffsets.push(offsetX, offsetY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the offsets uniform
|
|
||||||
const offsetsLocation = gl.getUniformLocation(this.program, 'u_offsets');
|
|
||||||
gl.uniform2fv(offsetsLocation, new Float32Array(adjustedOffsets));
|
|
||||||
|
|
||||||
// Bind input texture
|
// Bind input texture
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, inputTexture);
|
gl.bindTexture(gl.TEXTURE_2D, inputTexture);
|
||||||
gl.uniform1i(gl.getUniformLocation(this.program, 'u_previousTexture'), 0);
|
gl.uniform1i(gl.getUniformLocation(this.jfaProgram, 'u_previousTexture'), 0);
|
||||||
|
|
||||||
// Draw a fullscreen quad
|
|
||||||
this.drawFullscreenQuad();
|
this.drawFullscreenQuad();
|
||||||
|
|
||||||
// Swap ping-pong index
|
// Swap ping-pong index
|
||||||
|
|
@ -459,29 +264,28 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderToScreen() {
|
private renderToScreen() {
|
||||||
const gl = this.gl;
|
const gl = this.glContext;
|
||||||
|
|
||||||
// Unbind framebuffer to render to the canvas
|
// Unbind framebuffer to render to the canvas
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
// Use display shader program
|
// Use display shader program
|
||||||
gl.useProgram(this.displayProgram);
|
gl.useProgram(this.renderProgram);
|
||||||
|
|
||||||
// Bind the final texture
|
// Bind the final texture
|
||||||
const finalTexture = this.textures[this.pingPongIndex % 2];
|
const finalTexture = this.textures[this.pingPongIndex % 2];
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, finalTexture);
|
gl.bindTexture(gl.TEXTURE_2D, finalTexture);
|
||||||
gl.uniform1i(gl.getUniformLocation(this.displayProgram, 'u_texture'), 0);
|
gl.uniform1i(gl.getUniformLocation(this.renderProgram, 'u_texture'), 0);
|
||||||
|
|
||||||
// Draw a fullscreen quad
|
// Draw a fullscreen quad
|
||||||
this.drawFullscreenQuad();
|
this.drawFullscreenQuad();
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawFullscreenQuad() {
|
private drawFullscreenQuad() {
|
||||||
const gl = this.gl;
|
const gl = this.glContext;
|
||||||
|
|
||||||
// Initialize VAO if not already done
|
|
||||||
if (!this.fullscreenQuadVAO) {
|
if (!this.fullscreenQuadVAO) {
|
||||||
this.initFullscreenQuad();
|
this.initFullscreenQuad();
|
||||||
}
|
}
|
||||||
|
|
@ -492,7 +296,7 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initFullscreenQuad() {
|
private initFullscreenQuad() {
|
||||||
const gl = this.gl;
|
const gl = this.glContext;
|
||||||
|
|
||||||
const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
|
const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
|
||||||
|
|
||||||
|
|
@ -503,7 +307,7 @@ export class DistanceField extends HTMLElement {
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
|
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
|
||||||
|
|
||||||
const positionAttributeLocation = gl.getAttribLocation(this.program, 'a_position');
|
const positionAttributeLocation = gl.getAttribLocation(this.jfaProgram, 'a_position');
|
||||||
gl.enableVertexAttribArray(positionAttributeLocation);
|
gl.enableVertexAttribArray(positionAttributeLocation);
|
||||||
gl.vertexAttribPointer(
|
gl.vertexAttribPointer(
|
||||||
positionAttributeLocation,
|
positionAttributeLocation,
|
||||||
|
|
@ -516,4 +320,185 @@ export class DistanceField extends HTMLElement {
|
||||||
|
|
||||||
gl.bindVertexArray(null);
|
gl.bindVertexArray(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
private handleResize = () => {
|
||||||
|
const gl = this.glContext;
|
||||||
|
|
||||||
|
// Update canvas size
|
||||||
|
this.canvas.width = window.innerWidth;
|
||||||
|
this.canvas.height = window.innerHeight;
|
||||||
|
|
||||||
|
// Update the viewport
|
||||||
|
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
// Re-initialize textures with the new dimensions
|
||||||
|
this.initPingPongTextures();
|
||||||
|
|
||||||
|
// Re-initialize seed point rendering to update positions
|
||||||
|
this.initSeedPointRendering();
|
||||||
|
|
||||||
|
// Rerun JFA
|
||||||
|
this.runJFA();
|
||||||
|
};
|
||||||
|
|
||||||
|
private computeOffsets(stepSize: number): Float32Array {
|
||||||
|
const offsets: number[] = [];
|
||||||
|
for (let y = -1; y <= 1; y++) {
|
||||||
|
for (let x = -1; x <= 1; x++) {
|
||||||
|
offsets.push((x * stepSize) / this.canvas.width, (y * stepSize) / this.canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Float32Array(offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupWebGLResources() {
|
||||||
|
const gl = this.glContext;
|
||||||
|
|
||||||
|
// Delete textures
|
||||||
|
this.textures.forEach((texture) => gl.deleteTexture(texture));
|
||||||
|
this.textures = [];
|
||||||
|
|
||||||
|
// Delete framebuffers
|
||||||
|
if (this.framebuffer) {
|
||||||
|
gl.deleteFramebuffer(this.framebuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete VAOs
|
||||||
|
if (this.fullscreenQuadVAO) {
|
||||||
|
gl.deleteVertexArray(this.fullscreenQuadVAO);
|
||||||
|
}
|
||||||
|
if (this.shapeVAO) {
|
||||||
|
gl.deleteVertexArray(this.shapeVAO);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.jfaProgram) {
|
||||||
|
gl.deleteProgram(this.jfaProgram);
|
||||||
|
}
|
||||||
|
if (this.renderProgram) {
|
||||||
|
gl.deleteProgram(this.renderProgram);
|
||||||
|
}
|
||||||
|
if (this.seedProgram) {
|
||||||
|
gl.deleteProgram(this.seedProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear other references
|
||||||
|
this.geometries = null!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jfaVertShader = vert`#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
in vec2 a_position;
|
||||||
|
out vec2 v_texCoord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
v_texCoord = a_position * 0.5 + 0.5; // Transform to [0, 1] range
|
||||||
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const jfaFragShader = frag`#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
precision mediump int;
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
uniform sampler2D u_previousTexture;
|
||||||
|
uniform vec2 u_offsets[9];
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Start with the current texel's nearest seed point and distance
|
||||||
|
vec4 nearest = texture(u_previousTexture, v_texCoord);
|
||||||
|
|
||||||
|
// Initialize minDist with the current distance
|
||||||
|
float minDist = nearest.a;
|
||||||
|
|
||||||
|
// Loop through neighbor offsets
|
||||||
|
for (int i = 0; i < 9; ++i) {
|
||||||
|
vec2 sampleCoord = v_texCoord + u_offsets[i];
|
||||||
|
|
||||||
|
// Clamp sampleCoord to [0, 1] to prevent sampling outside texture
|
||||||
|
sampleCoord = clamp(sampleCoord, vec2(0.0), vec2(1.0));
|
||||||
|
|
||||||
|
vec4 sampled = texture(u_previousTexture, sampleCoord);
|
||||||
|
|
||||||
|
if (sampled.z == 0.0) {
|
||||||
|
continue; // Skip background pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute distance to the seed point stored in this neighbor
|
||||||
|
float dist = distance(sampled.xy, v_texCoord);
|
||||||
|
|
||||||
|
if (dist < minDist) {
|
||||||
|
nearest = sampled;
|
||||||
|
nearest.a = dist;
|
||||||
|
minDist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the nearest seed point and updated distance
|
||||||
|
outColor = nearest;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const renderVertShader = vert`#version 300 es
|
||||||
|
in vec2 a_position;
|
||||||
|
out vec2 v_texCoord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
v_texCoord = a_position * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const renderFragShader = frag`#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 texel = texture(u_texture, v_texCoord);
|
||||||
|
|
||||||
|
// Extract shape ID and distance
|
||||||
|
float shapeID = texel.z;
|
||||||
|
float distance = texel.a;
|
||||||
|
|
||||||
|
// Hash-based color for shape
|
||||||
|
vec3 shapeColor = vec3(
|
||||||
|
fract(sin(shapeID * 12.9898) * 43758.5453),
|
||||||
|
fract(sin(shapeID * 78.233) * 43758.5453),
|
||||||
|
fract(sin(shapeID * 93.433) * 43758.5453)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Visualize distance (e.g., as intensity)
|
||||||
|
float intensity = exp(-distance * 10.0);
|
||||||
|
|
||||||
|
outColor = vec4(shapeColor * intensity, 1.0);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// Shader sources for seed point rendering
|
||||||
|
const seedVertexShaderSource = vert`#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
in vec3 a_position; // x, y, shapeID
|
||||||
|
flat out float v_shapeID;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(a_position.xy, 0.0, 1.0);
|
||||||
|
v_shapeID = a_position.z; // Pass shape ID to fragment shader
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const seedFragmentShaderSource = frag`#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
flat in float v_shapeID;
|
||||||
|
uniform vec2 u_canvasSize;
|
||||||
|
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 seedCoord = gl_FragCoord.xy / u_canvasSize;
|
||||||
|
outColor = vec4(seedCoord, v_shapeID, 0.0); // Seed coords, shape ID, initial distance 0
|
||||||
|
}`;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
export function glsl(strings: TemplateStringsArray) {
|
export function glsl(strings: TemplateStringsArray, ...values: any[]) {
|
||||||
return strings[0];
|
return strings.reduce((result, str, i) => {
|
||||||
|
return result + str + (values[i] || '');
|
||||||
|
}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function vert(strings: TemplateStringsArray) {
|
export function vert(strings: TemplateStringsArray, ...values: any[]) {
|
||||||
return strings[0];
|
return strings.reduce((result, str, i) => {
|
||||||
|
return result + str + (values[i] || '');
|
||||||
|
}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function frag(strings: TemplateStringsArray) {
|
export function frag(strings: TemplateStringsArray, ...values: any[]) {
|
||||||
return strings[0];
|
return strings.reduce((result, str, i) => {
|
||||||
|
return result + str + (values[i] || '');
|
||||||
|
}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function css(strings: TemplateStringsArray) {
|
export function css(strings: TemplateStringsArray, ...values: any[]) {
|
||||||
return strings[0];
|
return strings.reduce((result, str, i) => {
|
||||||
|
return result + str + (values[i] || '');
|
||||||
|
}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function html(strings: TemplateStringsArray) {
|
export function html(strings: TemplateStringsArray, ...values: any[]) {
|
||||||
return strings[0];
|
return strings.reduce((result, str, i) => {
|
||||||
|
return result + str + (values[i] || '');
|
||||||
|
}, '');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
export class WebGLUtils {
|
||||||
|
static createShader(gl: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
|
||||||
|
const shader = gl.createShader(type)!;
|
||||||
|
gl.shaderSource(shader, source);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
|
||||||
|
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
||||||
|
if (!success) {
|
||||||
|
const error = gl.getShaderInfoLog(shader);
|
||||||
|
gl.deleteShader(shader);
|
||||||
|
throw new Error(`Shader compilation failed: ${error}`);
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createProgram(
|
||||||
|
gl: WebGL2RenderingContext,
|
||||||
|
vertexShader: WebGLShader,
|
||||||
|
fragmentShader: WebGLShader
|
||||||
|
): WebGLProgram {
|
||||||
|
const program = gl.createProgram()!;
|
||||||
|
gl.attachShader(program, vertexShader);
|
||||||
|
gl.attachShader(program, fragmentShader);
|
||||||
|
gl.linkProgram(program);
|
||||||
|
|
||||||
|
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||||
|
if (!success) {
|
||||||
|
const error = gl.getProgramInfoLog(program);
|
||||||
|
gl.deleteProgram(program);
|
||||||
|
throw new Error(`Program linking failed: ${error}`);
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createWebGLCanvas(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
parent: HTMLElement
|
||||||
|
): { gl: WebGL2RenderingContext | undefined; canvas: HTMLCanvasElement } {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
// Set canvas styles
|
||||||
|
canvas.style.position = 'absolute';
|
||||||
|
canvas.style.top = '0';
|
||||||
|
canvas.style.left = '0';
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
canvas.style.height = '100%';
|
||||||
|
canvas.style.zIndex = '-1';
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
// Initialize WebGL2 context
|
||||||
|
const gl = canvas.getContext('webgl2');
|
||||||
|
if (!gl) {
|
||||||
|
console.error('WebGL2 is not available.');
|
||||||
|
return { gl: undefined, canvas };
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.appendChild(canvas);
|
||||||
|
return { gl, canvas };
|
||||||
|
}
|
||||||
|
|
||||||
|
static createShaderProgram(gl: WebGL2RenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram {
|
||||||
|
const vertexShader = this.createShader(gl, gl.VERTEX_SHADER, vertexSource);
|
||||||
|
const fragmentShader = this.createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
||||||
|
const program = this.createProgram(gl, vertexShader, fragmentShader);
|
||||||
|
|
||||||
|
// Clean up shaders since they're now linked to the program
|
||||||
|
gl.deleteShader(vertexShader);
|
||||||
|
gl.deleteShader(fragmentShader);
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue