Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vector field #19

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
527 changes: 527 additions & 0 deletions labs/folk-brush-field.ts

Large diffs are not rendered by default.

204 changes: 204 additions & 0 deletions labs/folk-distance-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export class FolkDistanceField extends FolkBaseSet {
#jfaProgram!: WebGLProgram; // Shader program for the Jump Flooding Algorithm
#renderProgram!: WebGLProgram; // Shader program for final rendering
#seedProgram!: WebGLProgram; // Shader program for rendering seed points
#directionFieldTexture!: WebGLTexture;
#directionProgram!: WebGLProgram; // New shader program for direction computation

/**
* Groups data for handling different sets of shapes.
Expand Down Expand Up @@ -96,6 +98,9 @@ export class FolkDistanceField extends FolkBaseSet {
if (!this.#framebuffer) {
throw new Error('Failed to create framebuffer.');
}

// Create direction field texture
this.#directionFieldTexture = this.#createDirectionFieldTexture();
}

/**
Expand All @@ -117,6 +122,7 @@ export class FolkDistanceField extends FolkBaseSet {
this.#jfaProgram = WebGLUtils.createShaderProgram(this.#glContext, commonVertShader, jfaFragShader);
this.#renderProgram = WebGLUtils.createShaderProgram(this.#glContext, commonVertShader, renderFragShader);
this.#seedProgram = WebGLUtils.createShaderProgram(this.#glContext, seedVertShader, seedFragShader);
this.#directionProgram = WebGLUtils.createShaderProgram(this.#glContext, commonVertShader, directionFragShader);
}

/**
Expand Down Expand Up @@ -396,6 +402,9 @@ export class FolkDistanceField extends FolkBaseSet {
#renderToScreen() {
const gl = this.#glContext;

// First, compute direction vectors
this.#computeDirectionField();

// Unbind framebuffer to render directly to the canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, this.#canvas.width, this.#canvas.height);
Expand All @@ -414,6 +423,11 @@ export class FolkDistanceField extends FolkBaseSet {
textureUnit++;
}

// Add direction texture binding
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, this.#directionFieldTexture);
gl.uniform1i(gl.getUniformLocation(this.#renderProgram, 'u_directionTexture'), textureUnit);

// Draw a fullscreen quad to display the result
this.#drawFullscreenQuad();
}
Expand Down Expand Up @@ -507,6 +521,67 @@ export class FolkDistanceField extends FolkBaseSet {
return new Float32Array(offsets);
}

/**
* Computes the direction field from the final distance field
*/
#computeDirectionField() {
const gl = this.#glContext;

// Bind framebuffer to render to direction texture
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#directionFieldTexture, 0);

// Use direction computation shader
gl.useProgram(this.#directionProgram);

// Bind the final distance field textures
let textureUnit = 0;
for (const groupName in this.#groups) {
const group = this.#groups[groupName];
const finalTexture = group.textures[group.isPingTexture ? 0 : 1];
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, finalTexture);
gl.uniform1i(gl.getUniformLocation(this.#directionProgram, `u_texture_${groupName}`), textureUnit);
textureUnit++;
}

// Draw fullscreen quad to compute directions
this.#drawFullscreenQuad();

// Unbind framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}

/**
* Creates a texture for storing direction vectors
*/
#createDirectionFieldTexture(): WebGLTexture {
const gl = this.#glContext;
const texture = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, texture);

// Set texture parameters
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

// Initialize texture with empty data
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA16F,
this.#canvas.width,
this.#canvas.height,
0,
gl.RGBA,
gl.HALF_FLOAT,
null,
);

return texture;
}

/**
* Cleans up WebGL resources to prevent memory leaks.
* This is called when the element is disconnected from the DOM.
Expand Down Expand Up @@ -555,6 +630,72 @@ export class FolkDistanceField extends FolkBaseSet {
}

this.#groupBuffers = {};

// Delete direction resources
if (this.#directionFieldTexture) {
gl.deleteTexture(this.#directionFieldTexture);
}
if (this.#directionProgram) {
gl.deleteProgram(this.#directionProgram);
}
}

/**
* Samples the distance field at the given point.
*/
public sampleField(x: number, y: number): { distance: number; direction: Point } {
const gl = this.#glContext;

// Ensure coordinates are in bounds
const nx = x / this.clientWidth;
const ny = y / this.clientHeight;
if (nx < 0 || nx > 1 || ny < 0 || ny > 1) {
return { distance: Infinity, direction: { x: 0, y: 0 } };
}

// Create buffers for reading pixels
const distancePixels = new Float32Array(4);
const directionPixels = new Float32Array(4);

// Read distance from the appropriate texture
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#framebuffer);

// Find the active texture with minimum distance
let minDistance = Infinity;
let finalDirection: Point = { x: 0, y: 0 };

for (const groupName in this.#groups) {
const group = this.#groups[groupName];
const texture = group.textures[group.isPingTexture ? 0 : 1];

gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.readPixels(
x,
this.clientHeight - y - 1, // Flip Y coordinate
1,
1,
gl.RGBA,
gl.FLOAT,
distancePixels,
);

if (distancePixels[3] < minDistance) {
minDistance = distancePixels[3];

// Read direction vector
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#directionFieldTexture, 0);
gl.readPixels(x, this.clientHeight - y - 1, 1, 1, gl.RGBA, gl.FLOAT, directionPixels);

finalDirection = { x: directionPixels[0], y: directionPixels[1] };
}
}

gl.bindFramebuffer(gl.FRAMEBUFFER, null);

return {
distance: minDistance,
direction: finalDirection,
};
}
}

Expand Down Expand Up @@ -628,13 +769,15 @@ precision mediump float;
#define FALLOFF_FACTOR 10.0
#define SMOOTHING_FACTOR 0.1
#define DEBUG_HARD_CUTOFF_DISTANCE 0.2
#define DEBUG_VECTOR_FIELD true

in vec2 v_texCoord;
out vec4 outColor;

uniform sampler2D u_texture_mergeA;
uniform sampler2D u_texture_mergeB;
uniform sampler2D u_texture_others;
uniform sampler2D u_directionTexture;

vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
Expand Down Expand Up @@ -714,6 +857,14 @@ void main() {
finalColor *= exp(-finalDistance * FALLOFF_FACTOR);
}

// Sample direction vector
vec4 directionData = texture(u_directionTexture, v_texCoord);
vec2 direction = directionData.xy;

if (DEBUG_VECTOR_FIELD) {
// Convert direction vector to color (from -1..1 to 0..1 range)
finalColor = vec3(direction * 0.5 + 0.5, 0.0);
}

outColor = vec4(finalColor, 1.0);
}`;
Expand Down Expand Up @@ -749,3 +900,56 @@ void main() {
vec2 seedCoord = gl_FragCoord.xy / u_canvasSize;
outColor = vec4(seedCoord, v_shapeID, 0.0); // Seed coords (x, y), shape ID (z), initial distance (a)
}`;

/**
* Fragment shader for direction computation
*/
const directionFragShader = glsl`#version 300 es
precision mediump float;

in vec2 v_texCoord;
out vec4 outColor;

uniform sampler2D u_texture_mergeA;
uniform sampler2D u_texture_mergeB;
uniform sampler2D u_texture_others;

void main() {
// Sample from all distance field textures
vec4 texelMergeA = texture(u_texture_mergeA, v_texCoord);
vec4 texelMergeB = texture(u_texture_mergeB, v_texCoord);
vec4 texelOthers = texture(u_texture_others, v_texCoord);

// Get distances
float distA = texelMergeA.a;
float distB = texelMergeB.a;
float distOthers = texelOthers.a;

// Choose the texture with the smallest distance
vec4 closestTexel;
if (distA <= distB && distA <= distOthers) {
closestTexel = texelMergeA;
} else if (distB <= distOthers) {
closestTexel = texelMergeB;
} else {
closestTexel = texelOthers;
}

// Get closest seed point position from texel
vec2 seedPos = closestTexel.xy;

// If we're at zero distance (inside shape), output zero vector
if (closestTexel.a < 0.0001) {
outColor = vec4(0.0, 0.0, 0.0, 0.0);
return;
}

// Calculate direction, with x component reversed
vec2 direction = normalize(vec2(
-(v_texCoord.x - seedPos.x), // Reverse x component
v_texCoord.y - seedPos.y
));

// Store direction in xy, store original distance in z, w unused
outColor = vec4(direction, closestTexel.a, 0.0);
}`;
5 changes: 5 additions & 0 deletions labs/standalone/folk-brush-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FolkBrushField } from '../folk-brush-field';

FolkBrushField.define();

export { FolkBrushField };
72 changes: 72 additions & 0 deletions lib/Vector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ export class Vector {
return { x: 0, y: 0 };
}

/**
* Unit vector pointing right (1,0)
* @returns {Point} A point representing a right vector
*/
static right(): Point {
return { x: 1, y: 0 };
}

/**
* Unit vector pointing left (-1,0)
* @returns {Point} A point representing a left vector
*/
static left(): Point {
return { x: -1, y: 0 };
}

/**
* Unit vector pointing up (0,-1)
* @returns {Point} A point representing an up vector
*/
static up(): Point {
return { x: 0, y: -1 };
}

/**
* Unit vector pointing down (0,1)
* @returns {Point} A point representing a down vector
*/
static down(): Point {
return { x: 0, y: 1 };
}

/**
* Subtracts vector b from vector a
* @param {Point} a - The first vector
Expand Down Expand Up @@ -190,4 +222,44 @@ export class Vector {
static magSquared(v: Point): number {
return v.x * v.x + v.y * v.y;
}

/**
* Calculates the bounding box of a set of points
* @param {Point[]} points - Array of points to find bounds for
* @returns {{ min: Point, max: Point }} Object containing min and max points of the bounds
*/
static bounds(points: Point[]): { min: Point; max: Point } {
return points.reduce(
(acc, p) => ({
min: { x: Math.min(acc.min.x, p.x), y: Math.min(acc.min.y, p.y) },
max: { x: Math.max(acc.max.x, p.x), y: Math.max(acc.max.y, p.y) },
}),
{ min: { x: Infinity, y: Infinity }, max: { x: -Infinity, y: -Infinity } },
);
}

/**
* Calculates the center point of a set of points
* @param {Point[]} points - Array of points to find center for
* @returns {Point} The center point
*/
static center(points: Point[]): Point {
const bounds = Vector.bounds(points);
return {
x: (bounds.min.x + bounds.max.x) / 2,
y: (bounds.min.y + bounds.max.y) / 2,
};
}

/**
* Projects a point onto an axis
* @param {Point} point - The point to project
* @param {Point} axis - The axis to project onto
* @returns {Point} The projected point
*/
static project(point: Point, axis: Point): Point {
const normalized = Vector.normalized(axis);
const dot = point.x * normalized.x + point.y * normalized.y;
return Vector.scale(normalized, dot);
}
}
Loading