diff --git a/labs/folk-distance-field.ts b/labs/folk-distance-field.ts index 391c9e5..08bf794 100644 --- a/labs/folk-distance-field.ts +++ b/labs/folk-distance-field.ts @@ -13,21 +13,20 @@ export class FolkDistanceField extends FolkBaseSet { static readonly MAX_DISTANCE = 99999.0; - private canvas!: HTMLCanvasElement; - private glContext!: WebGL2RenderingContext; - private framebuffer!: WebGLFramebuffer; - private fullscreenQuadVAO!: WebGLVertexArrayObject; - - private jfaProgram!: WebGLProgram; // Shader program for the Jump Flooding Algorithm - private renderProgram!: WebGLProgram; // Shader program for final rendering - private seedProgram!: WebGLProgram; // Shader program for rendering seed points + #canvas!: HTMLCanvasElement; + #glContext!: WebGL2RenderingContext; + #framebuffer!: WebGLFramebuffer; + #fullscreenQuadVAO!: WebGLVertexArrayObject; + #jfaProgram!: WebGLProgram; // Shader program for the Jump Flooding Algorithm + #renderProgram!: WebGLProgram; // Shader program for final rendering + #seedProgram!: WebGLProgram; // Shader program for rendering seed points /** * Groups data for handling different sets of shapes. * 'mergeA' and 'mergeB' shapes will have their distance fields merged in rendering, * while 'others' will be processed separately. */ - private groups: { + #groups: { [groupName: string]: { textures: WebGLTexture[]; isPingTexture: boolean; @@ -37,7 +36,7 @@ export class FolkDistanceField extends FolkBaseSet { } = {}; // Add class property to store Float32Arrays - private groupBuffers: { + #groupBuffers: { [groupName: string]: Float32Array; } = {}; @@ -45,7 +44,7 @@ export class FolkDistanceField extends FolkBaseSet { super.connectedCallback(); // Initialize groups for 'mergeA', 'mergeB', and 'others' - this.groups = { + this.#groups = { mergeA: { textures: [], isPingTexture: true, @@ -66,35 +65,35 @@ export class FolkDistanceField extends FolkBaseSet { }, }; - this.initWebGL(); - this.initShaders(); - this.initPingPongTextures(); + this.#initWebGL(); + this.#initShaders(); + this.#initPingPongTextures(); - window.addEventListener('resize', this.handleResize); + window.addEventListener('resize', this.#handleResize); } disconnectedCallback() { super.disconnectedCallback(); - window.removeEventListener('resize', this.handleResize); + window.removeEventListener('resize', this.#handleResize); - this.cleanupWebGLResources(); + this.#cleanupWebGLResources(); } - private initWebGL() { + #initWebGL() { const { gl, canvas } = WebGLUtils.createWebGLCanvas(this.clientWidth, this.clientHeight); if (!gl || !canvas) { throw new Error('Failed to initialize WebGL context.'); } - this.canvas = canvas; + this.#canvas = canvas; this.renderRoot.prepend(canvas); - this.glContext = gl; + this.#glContext = gl; // Create framebuffer object - this.framebuffer = gl.createFramebuffer(); - if (!this.framebuffer) { + this.#framebuffer = gl.createFramebuffer()!; + if (!this.#framebuffer) { throw new Error('Failed to create framebuffer.'); } } @@ -107,38 +106,38 @@ export class FolkDistanceField extends FolkBaseSet { if (this.sourcesMap.size !== this.sourceElements.size) return; - this.populateSeedPoints(); - this.runJumpFloodingAlgorithm(); + this.#populateSeedPoints(); + this.#runJumpFloodingAlgorithm(); } /** * Initializes all shader programs used in rendering. */ - private initShaders() { - this.jfaProgram = WebGLUtils.createShaderProgram(this.glContext, commonVertShader, jfaFragShader); - this.renderProgram = WebGLUtils.createShaderProgram(this.glContext, commonVertShader, renderFragShader); - this.seedProgram = WebGLUtils.createShaderProgram(this.glContext, seedVertShader, seedFragShader); + #initShaders() { + this.#jfaProgram = WebGLUtils.createShaderProgram(this.#glContext, commonVertShader, jfaFragShader); + this.#renderProgram = WebGLUtils.createShaderProgram(this.#glContext, commonVertShader, renderFragShader); + this.#seedProgram = WebGLUtils.createShaderProgram(this.#glContext, seedVertShader, seedFragShader); } /** * Initializes textures and framebuffer for ping-pong rendering. * Supports separate textures for 'mergeA', 'mergeB', and 'others' groups. */ - private initPingPongTextures() { + #initPingPongTextures() { // Initialize textures for each group - for (const groupName in this.groups) { - this.groups[groupName].textures = this.createPingPongTextures(); - this.groups[groupName].isPingTexture = true; + for (const groupName in this.#groups) { + this.#groups[groupName].textures = this.#createPingPongTextures(); + this.#groups[groupName].isPingTexture = true; } } /** * Utility method to create ping-pong textures. */ - private createPingPongTextures(): WebGLTexture[] { - const gl = this.glContext; - const width = this.canvas.width; - const height = this.canvas.height; + #createPingPongTextures(): WebGLTexture[] { + const gl = this.#glContext; + const width = this.#canvas.width; + const height = this.#canvas.height; const textures: WebGLTexture[] = []; // Enable the EXT_color_buffer_half_float extension for high-precision floating-point textures @@ -171,8 +170,8 @@ export class FolkDistanceField extends FolkBaseSet { * Populates seed points and assigns shapes to 'mergeA', 'mergeB', or 'others' groups. * Shapes with index 0 and 1 are assigned to 'mergeA' and 'mergeB' respectively. */ - private populateSeedPoints() { - const gl = this.glContext; + #populateSeedPoints() { + const gl = this.#glContext; const groupPositions: { [groupName: string]: number[] } = { mergeA: [], mergeB: [], @@ -252,7 +251,7 @@ export class FolkDistanceField extends FolkBaseSet { // Initialize buffers and VAOs for each group for (const groupName in groupPositions) { const positions = groupPositions[groupName]; - const group = this.groups[groupName]; + const group = this.#groups[groupName]; if (!group.shapeVAO) { // First time initialization @@ -261,23 +260,23 @@ export class FolkDistanceField extends FolkBaseSet { group.positionBuffer = gl.createBuffer()!; // Create and store the Float32Array - this.groupBuffers[groupName] = new Float32Array(positions); + this.#groupBuffers[groupName] = new Float32Array(positions); gl.bindBuffer(gl.ARRAY_BUFFER, group.positionBuffer); - gl.bufferData(gl.ARRAY_BUFFER, this.groupBuffers[groupName], gl.DYNAMIC_DRAW); + gl.bufferData(gl.ARRAY_BUFFER, this.#groupBuffers[groupName], gl.DYNAMIC_DRAW); - const positionLocation = gl.getAttribLocation(this.seedProgram, 'a_position'); + const positionLocation = gl.getAttribLocation(this.#seedProgram, 'a_position'); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0); gl.bindVertexArray(null); } else { // Reuse existing Float32Array if size hasn't changed - const existingArray = this.groupBuffers[groupName]; + const existingArray = this.#groupBuffers[groupName]; if (positions.length !== existingArray.length) { // Only create new array if size changed - this.groupBuffers[groupName] = new Float32Array(positions); + this.#groupBuffers[groupName] = new Float32Array(positions); gl.bindBuffer(gl.ARRAY_BUFFER, group.positionBuffer!); - gl.bufferData(gl.ARRAY_BUFFER, this.groupBuffers[groupName], gl.DYNAMIC_DRAW); + gl.bufferData(gl.ARRAY_BUFFER, this.#groupBuffers[groupName], gl.DYNAMIC_DRAW); } else { // Reuse existing array existingArray.set(positions); @@ -291,9 +290,9 @@ export class FolkDistanceField extends FolkBaseSet { for (const groupName in groupPositions) { const positions = groupPositions[groupName]; const vertexCount = positions.length / 3; - this.renderSeedPointsForGroup( - this.groups[groupName].shapeVAO, - this.groups[groupName].textures[this.groups[groupName].isPingTexture ? 0 : 1], + this.#renderSeedPointsForGroup( + this.#groups[groupName].shapeVAO, + this.#groups[groupName].textures[this.#groups[groupName].isPingTexture ? 0 : 1], vertexCount, ); } @@ -302,24 +301,24 @@ export class FolkDistanceField extends FolkBaseSet { /** * Utility method to render seed points for a given group. */ - private renderSeedPointsForGroup(vao: WebGLVertexArrayObject, seedTexture: WebGLTexture, vertexCount: number) { - const gl = this.glContext; + #renderSeedPointsForGroup(vao: WebGLVertexArrayObject, seedTexture: WebGLTexture, vertexCount: number) { + const gl = this.#glContext; // Bind framebuffer to render to the seed texture - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, seedTexture, 0); // 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, FolkDistanceField.MAX_DISTANCE); gl.clear(gl.COLOR_BUFFER_BIT); // Use the seed shader program - gl.useProgram(this.seedProgram); + gl.useProgram(this.#seedProgram); // Set the canvas size uniform - const canvasSizeLocation = gl.getUniformLocation(this.seedProgram, 'u_canvasSize'); - gl.uniform2f(canvasSizeLocation, 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 gl.bindVertexArray(vao); @@ -334,18 +333,18 @@ export class FolkDistanceField extends FolkBaseSet { * Executes the Jump Flooding Algorithm (JFA) for each group separately. * 'mergeA' and 'mergeB' groups will have their distance fields merged in rendering. */ - private runJumpFloodingAlgorithm() { + #runJumpFloodingAlgorithm() { // Compute initial step size - let stepSize = 1 << Math.floor(Math.log2(Math.max(this.canvas.width, this.canvas.height))); + let stepSize = 1 << Math.floor(Math.log2(Math.max(this.#canvas.width, this.#canvas.height))); // Perform passes with decreasing step sizes for each group - for (const groupName in this.groups) { - const group = this.groups[groupName]; + for (const groupName in this.#groups) { + const group = this.#groups[groupName]; const textures = group.textures; let isPingTexture = group.isPingTexture; for (let size = stepSize; size >= 1; size >>= 1) { - this.renderPass(size, textures, isPingTexture); + this.#renderPass(size, textures, isPingTexture); isPingTexture = !isPingTexture; } @@ -353,38 +352,38 @@ export class FolkDistanceField extends FolkBaseSet { } // Render the final result to the screen - this.renderToScreen(); + this.#renderToScreen(); } /** * Performs a single pass of the Jump Flooding Algorithm with a given step size for a specific distance field. */ - private renderPass(stepSize: number, textures: WebGLTexture[], isPingTexture: boolean) { - const gl = this.glContext; + #renderPass(stepSize: number, textures: WebGLTexture[], isPingTexture: boolean) { + const gl = this.#glContext; // Swap textures for ping-pong rendering const inputTexture = isPingTexture ? textures[0] : textures[1]; const outputTexture = isPingTexture ? textures[1] : textures[0]; // Bind framebuffer to output texture - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0); // Use the JFA shader program - gl.useProgram(this.jfaProgram); + gl.useProgram(this.#jfaProgram); // Compute and set the offsets uniform for neighboring pixels - const offsets = this.computeOffsets(stepSize); - const offsetsLocation = gl.getUniformLocation(this.jfaProgram, 'u_offsets'); + const offsets = this.#computeOffsets(stepSize); + const offsetsLocation = gl.getUniformLocation(this.#jfaProgram, 'u_offsets'); gl.uniform2fv(offsetsLocation, offsets); // Bind input texture containing the previous step's results gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, inputTexture); - gl.uniform1i(gl.getUniformLocation(this.jfaProgram, 'u_previousTexture'), 0); + gl.uniform1i(gl.getUniformLocation(this.#jfaProgram, 'u_previousTexture'), 0); // Draw a fullscreen quad to process all pixels - this.drawFullscreenQuad(); + this.#drawFullscreenQuad(); // Unbind framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, null); @@ -394,44 +393,44 @@ export class FolkDistanceField extends FolkBaseSet { * Renders the final distance field to the screen using the render shader program. * Merges 'mergeA' and 'mergeB' distance fields during rendering, while 'others' are not merged. */ - private renderToScreen() { - const gl = this.glContext; + #renderToScreen() { + const gl = this.#glContext; // Unbind framebuffer to render directly to the canvas 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 the render shader program - gl.useProgram(this.renderProgram); + gl.useProgram(this.#renderProgram); // Bind the final textures from each group let textureUnit = 0; - for (const groupName in this.groups) { - const group = this.groups[groupName]; + 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.renderProgram, `u_texture_${groupName}`), textureUnit); + gl.uniform1i(gl.getUniformLocation(this.#renderProgram, `u_texture_${groupName}`), textureUnit); textureUnit++; } // Draw a fullscreen quad to display the result - this.drawFullscreenQuad(); + this.#drawFullscreenQuad(); } /** * Draws a fullscreen quad to cover the entire canvas. * This is used in shader passes where every pixel needs to be processed. */ - private drawFullscreenQuad() { - const gl = this.glContext; + #drawFullscreenQuad() { + const gl = this.#glContext; // Initialize the quad geometry if it hasn't been done yet - if (!this.fullscreenQuadVAO) { - this.initFullscreenQuad(); + if (!this.#fullscreenQuadVAO) { + this.#initFullscreenQuad(); } - gl.bindVertexArray(this.fullscreenQuadVAO); + gl.bindVertexArray(this.#fullscreenQuadVAO); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); gl.bindVertexArray(null); } @@ -439,20 +438,20 @@ export class FolkDistanceField extends FolkBaseSet { /** * Initializes the geometry and buffers for the fullscreen quad. */ - private initFullscreenQuad() { - const gl = this.glContext; + #initFullscreenQuad() { + const gl = this.#glContext; // Define positions for a quad covering the entire screen const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); - this.fullscreenQuadVAO = gl.createVertexArray()!; - gl.bindVertexArray(this.fullscreenQuadVAO); + this.#fullscreenQuadVAO = gl.createVertexArray()!; + gl.bindVertexArray(this.#fullscreenQuadVAO); const positionBuffer = gl.createBuffer()!; gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); - const positionAttributeLocation = gl.getAttribLocation(this.jfaProgram, 'a_position'); + const positionAttributeLocation = gl.getAttribLocation(this.#jfaProgram, 'a_position'); gl.enableVertexAttribArray(positionAttributeLocation); gl.vertexAttribPointer( positionAttributeLocation, @@ -470,24 +469,24 @@ export class FolkDistanceField extends FolkBaseSet { * Handles window resize events by updating canvas size, re-initializing textures and seed points, * and rerunning the Jump Flooding Algorithm. */ - private handleResize = () => { - const gl = this.glContext; + #handleResize = () => { + const gl = this.#glContext; // Update canvas size to match the container instead of window - this.canvas.width = this.clientWidth; - this.canvas.height = this.clientHeight; + this.#canvas.width = this.clientWidth; + this.#canvas.height = this.clientHeight; // Update the viewport - gl.viewport(0, 0, this.canvas.width, this.canvas.height); + gl.viewport(0, 0, this.#canvas.width, this.#canvas.height); // Re-initialize textures with the new dimensions - this.initPingPongTextures(); + this.#initPingPongTextures(); // Re-initialize seed point rendering to update positions - this.populateSeedPoints(); + this.#populateSeedPoints(); // Rerun the Jump Flooding Algorithm with the new sizes - this.runJumpFloodingAlgorithm(); + this.#runJumpFloodingAlgorithm(); }; /** @@ -496,13 +495,13 @@ export class FolkDistanceField extends FolkBaseSet { * @param stepSize The current step size for neighbor sampling. * @returns A Float32Array of offsets. */ - private computeOffsets(stepSize: number): Float32Array { - const aspectRatio = this.canvas.width / this.canvas.height; + #computeOffsets(stepSize: number): Float32Array { + const aspectRatio = this.#canvas.width / this.#canvas.height; const offsets: number[] = []; for (let y = -1; y <= 1; y++) { for (let x = -1; x <= 1; x++) { // Adjust x offset by aspect ratio to maintain uniform distances - offsets.push((x * stepSize * aspectRatio) / this.canvas.width, (y * stepSize) / this.canvas.height); + offsets.push((x * stepSize * aspectRatio) / this.#canvas.width, (y * stepSize) / this.#canvas.height); } } return new Float32Array(offsets); @@ -512,12 +511,12 @@ export class FolkDistanceField extends FolkBaseSet { * Cleans up WebGL resources to prevent memory leaks. * This is called when the element is disconnected from the DOM. */ - private cleanupWebGLResources() { - const gl = this.glContext; + #cleanupWebGLResources() { + const gl = this.#glContext; // Delete resources for each group - for (const groupName in this.groups) { - const group = this.groups[groupName]; + for (const groupName in this.#groups) { + const group = this.#groups[groupName]; // Delete textures group.textures.forEach((texture) => gl.deleteTexture(texture)); @@ -535,27 +534,27 @@ export class FolkDistanceField extends FolkBaseSet { } // Delete framebuffer - if (this.framebuffer) { - gl.deleteFramebuffer(this.framebuffer); + if (this.#framebuffer) { + gl.deleteFramebuffer(this.#framebuffer); } // Delete fullscreen quad VAO - if (this.fullscreenQuadVAO) { - gl.deleteVertexArray(this.fullscreenQuadVAO); + if (this.#fullscreenQuadVAO) { + gl.deleteVertexArray(this.#fullscreenQuadVAO); } // Delete shader programs - if (this.jfaProgram) { - gl.deleteProgram(this.jfaProgram); + if (this.#jfaProgram) { + gl.deleteProgram(this.#jfaProgram); } - if (this.renderProgram) { - gl.deleteProgram(this.renderProgram); + if (this.#renderProgram) { + gl.deleteProgram(this.#renderProgram); } - if (this.seedProgram) { - gl.deleteProgram(this.seedProgram); + if (this.#seedProgram) { + gl.deleteProgram(this.#seedProgram); } - this.groupBuffers = {}; + this.#groupBuffers = {}; } } diff --git a/labs/folk-sand.ts b/labs/folk-sand.ts index e91e20f..c9d48ed 100644 --- a/labs/folk-sand.ts +++ b/labs/folk-sand.ts @@ -25,29 +25,29 @@ export class FolkSand extends FolkBaseSet { `, ]; - private canvas = document.createElement('canvas'); - private gl!: WebGL2RenderingContext; + #canvas = document.createElement('canvas'); + #gl!: WebGL2RenderingContext; - private program!: WebGLProgram; - private blitProgram!: WebGLProgram; - private jfaShadowProgram!: WebGLProgram; - private jfaInitProgram!: WebGLProgram; + #program!: WebGLProgram; + #blitProgram!: WebGLProgram; + #jfaShadowProgram!: WebGLProgram; + #jfaInitProgram!: WebGLProgram; - private vao!: WebGLVertexArrayObject; - private posBuffer!: WebGLBuffer; + #vao!: WebGLVertexArrayObject; + #posBuffer!: WebGLBuffer; - private bufferWidth!: number; - private bufferHeight!: number; + #bufferWidth!: number; + #bufferHeight!: number; - private fbo: WebGLFramebuffer[] = []; - private tex: WebGLTexture[] = []; + #fbo: WebGLFramebuffer[] = []; + #tex: WebGLTexture[] = []; - private shadowFbo: WebGLFramebuffer[] = []; - private shadowTexR: WebGLTexture[] = []; - private shadowTexG: WebGLTexture[] = []; - private shadowTexB: WebGLTexture[] = []; + #shadowFbo: WebGLFramebuffer[] = []; + #shadowTexR: WebGLTexture[] = []; + #shadowTexG: WebGLTexture[] = []; + #shadowTexB: WebGLTexture[] = []; - private pointer = { + #pointer = { x: -1, y: -1, prevX: -1, @@ -55,88 +55,88 @@ export class FolkSand extends FolkBaseSet { down: false, }; - private materialType = 4; - private brushRadius = 5; + #materialType = 4; + #brushRadius = 5; - private frames = 0; - private swap = 0; - private shadowSwap = 0; + #frames = 0; + #swap = 0; + #shadowSwap = 0; - private PIXELS_PER_PARTICLE = 4; - private PIXEL_RATIO = window.devicePixelRatio || 1; + #PIXELS_PER_PARTICLE = 4; + #PIXEL_RATIO = window.devicePixelRatio || 1; - private collisionProgram!: WebGLProgram; - private collisionFbo!: WebGLFramebuffer; - private collisionTex!: WebGLTexture; - private shapeVao!: WebGLVertexArrayObject; - private shapePositionBuffer!: WebGLBuffer; - private shapeIndexBuffer!: WebGLBuffer; - private shapeIndexCount = 0; + #collisionProgram!: WebGLProgram; + #collisionFbo!: WebGLFramebuffer; + #collisionTex!: WebGLTexture; + #shapeVao!: WebGLVertexArrayObject; + #shapePositionBuffer!: WebGLBuffer; + #shapeIndexBuffer!: WebGLBuffer; + #shapeIndexCount = 0; onMaterialChange?: (type: number) => void; connectedCallback(): void { super.connectedCallback(); - this.renderRoot.appendChild(this.canvas); - this.initializeWebGL(); - this.initializeSimulation(); - this.initializeCollisionDetection(); - this.attachEventListeners(); - this.handleShapeTransform(); - this.render(); + this.renderRoot.appendChild(this.#canvas); + this.#initializeWebGL(); + this.#initializeSimulation(); + this.#initializeCollisionDetection(); + this.#attachEventListeners(); + this.#handleShapeTransform(); + this.#render(); } disconnectedCallback() { super.disconnectedCallback(); - this.detachEventListeners(); + this.#detachEventListeners(); } - private initializeWebGL() { - this.gl = this.canvas.getContext('webgl2')!; - if (!this.gl) { + #initializeWebGL() { + this.#gl = this.#canvas.getContext('webgl2')!; + if (!this.#gl) { console.error('WebGL2 context not available!'); } - if (!this.gl.getExtension('EXT_color_buffer_float')) { + if (!this.#gl.getExtension('EXT_color_buffer_float')) { console.error('need EXT_color_buffer_float'); } - if (!this.gl.getExtension('OES_texture_float_linear')) { + if (!this.#gl.getExtension('OES_texture_float_linear')) { console.error('need OES_texture_float_linear'); } } - private initializeSimulation() { + #initializeSimulation() { // Create shaders and programs - this.program = this.createProgramFromStrings({ + this.#program = this.#createProgramFromStrings({ vertex: vertexShader, fragment: simulationShader, })!; - this.blitProgram = this.createProgramFromStrings({ + this.#blitProgram = this.#createProgramFromStrings({ vertex: vertexShader, fragment: visualizationShader, })!; - this.jfaShadowProgram = this.createProgramFromStrings({ + this.#jfaShadowProgram = this.#createProgramFromStrings({ vertex: vertexShader, fragment: distanceFieldPropagationShader, })!; - this.jfaInitProgram = this.createProgramFromStrings({ + this.#jfaInitProgram = this.#createProgramFromStrings({ vertex: vertexShader, fragment: distanceFieldInitShader, })!; // Setup buffers and vertex arrays - this.setupBuffers(); + this.#setupBuffers(); // Initialize framebuffers and textures - this.initializeFramebuffers(); + this.#initializeFramebuffers(); } - private initializeCollisionDetection() { - const gl = this.gl; + #initializeCollisionDetection() { + const gl = this.#gl; - const collisionProgram = this.createProgramFromStrings({ + const collisionProgram = this.#createProgramFromStrings({ vertex: collisionVertexShader, fragment: collisionFragmentShader, }); @@ -147,59 +147,59 @@ export class FolkSand extends FolkBaseSet { } // Create collision shader program - this.collisionProgram = collisionProgram!; + this.#collisionProgram = collisionProgram!; // Create collision texture - this.collisionTex = gl.createTexture()!; - gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + this.#collisionTex = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 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); // Create collision framebuffer - this.collisionFbo = gl.createFramebuffer()!; - gl.bindFramebuffer(gl.FRAMEBUFFER, this.collisionFbo); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.collisionTex, 0); + this.#collisionFbo = gl.createFramebuffer()!; + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#collisionFbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#collisionTex, 0); // Initialize shape buffers with larger initial sizes - this.shapeVao = gl.createVertexArray()!; - gl.bindVertexArray(this.shapeVao); + this.#shapeVao = gl.createVertexArray()!; + gl.bindVertexArray(this.#shapeVao); - this.shapePositionBuffer = gl.createBuffer()!; - gl.bindBuffer(gl.ARRAY_BUFFER, this.shapePositionBuffer); + this.#shapePositionBuffer = gl.createBuffer()!; + gl.bindBuffer(gl.ARRAY_BUFFER, this.#shapePositionBuffer); // Allocate space for up to 100 shapes (400 vertices) gl.bufferData(gl.ARRAY_BUFFER, 4 * 2 * 100 * Float32Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW); - this.shapeIndexBuffer = gl.createBuffer()!; - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.shapeIndexBuffer); + this.#shapeIndexBuffer = gl.createBuffer()!; + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#shapeIndexBuffer); // Allocate space for up to 100 shapes (600 indices) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, 6 * 100 * Uint16Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW); // Set up vertex attributes - const posAttribLoc = gl.getAttribLocation(this.collisionProgram, 'aPosition'); + const posAttribLoc = gl.getAttribLocation(this.#collisionProgram, 'aPosition'); gl.enableVertexAttribArray(posAttribLoc); gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 0, 0); gl.bindVertexArray(null); } - private setupBuffers() { - const gl = this.gl; + #setupBuffers() { + const gl = this.#gl; const quad = [-1.0, -1.0, 0.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0.0]; - this.posBuffer = gl.createBuffer()!; + this.#posBuffer = gl.createBuffer()!; - gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer); + gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(quad), gl.STATIC_DRAW); - this.vao = gl.createVertexArray()!; + this.#vao = gl.createVertexArray()!; - gl.bindVertexArray(this.vao); + gl.bindVertexArray(this.#vao); - const posAttribLoc = gl.getAttribLocation(this.program, 'aPosition'); - const uvAttribLoc = gl.getAttribLocation(this.program, 'aUv'); + const posAttribLoc = gl.getAttribLocation(this.#program, 'aPosition'); + const uvAttribLoc = gl.getAttribLocation(this.#program, 'aUv'); gl.vertexAttribPointer(posAttribLoc, 2, gl.FLOAT, false, 16, 0); gl.enableVertexAttribArray(posAttribLoc); @@ -208,21 +208,21 @@ export class FolkSand extends FolkBaseSet { gl.enableVertexAttribArray(uvAttribLoc); } - private initializeFramebuffers() { - const gl = this.gl; + #initializeFramebuffers() { + const gl = this.#gl; - this.resizeCanvas(); + this.#resizeCanvas(); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - this.bufferWidth = Math.ceil(gl.canvas.width / this.PIXELS_PER_PARTICLE); - this.bufferHeight = Math.ceil(gl.canvas.height / this.PIXELS_PER_PARTICLE); + this.#bufferWidth = Math.ceil(gl.canvas.width / this.#PIXELS_PER_PARTICLE); + this.#bufferHeight = Math.ceil(gl.canvas.height / this.#PIXELS_PER_PARTICLE); // Initialize framebuffers and textures for simulation for (let i = 0; i < 2; i++) { // Create textures - this.tex[i] = gl.createTexture()!; - gl.bindTexture(gl.TEXTURE_2D, this.tex[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + this.#tex[i] = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D, this.#tex[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); @@ -230,133 +230,133 @@ export class FolkSand extends FolkBaseSet { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // Create framebuffers - this.fbo[i] = gl.createFramebuffer()!; - gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[i]); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.tex[i], 0); + this.#fbo[i] = gl.createFramebuffer()!; + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo[i]); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#tex[i], 0); // Setup shadow textures - this.shadowTexR[i] = gl.createTexture()!; - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + this.#shadowTexR[i] = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); 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); - this.shadowTexG[i] = gl.createTexture()!; - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + this.#shadowTexG[i] = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); 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); - this.shadowTexB[i] = gl.createTexture()!; - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + this.#shadowTexB[i] = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); 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); // Setup shadow framebuffers - this.shadowFbo[i] = gl.createFramebuffer()!; - gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[i]); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.shadowTexR[i], 0); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.shadowTexG[i], 0); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.shadowTexB[i], 0); + this.#shadowFbo[i] = gl.createFramebuffer()!; + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo[i]); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#shadowTexR[i], 0); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.#shadowTexG[i], 0); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.#shadowTexB[i], 0); // Set up draw buffers for the shadow FBO gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]); } } - private attachEventListeners() { - this.canvas.addEventListener('pointerdown', this.handlePointerDown); - this.canvas.addEventListener('pointermove', this.handlePointerMove); - this.canvas.addEventListener('pointerup', this.handlePointerUp); - document.addEventListener('keydown', this.handleKeyDown); + #attachEventListeners() { + this.#canvas.addEventListener('pointerdown', this.#handlePointerDown); + this.#canvas.addEventListener('pointermove', this.#handlePointerMove); + this.#canvas.addEventListener('pointerup', this.#handlePointerUp); + document.addEventListener('keydown', this.#handleKeyDown); } - private detachEventListeners() { - this.canvas.removeEventListener('pointerdown', this.handlePointerDown); - this.canvas.removeEventListener('pointermove', this.handlePointerMove); - this.canvas.removeEventListener('pointerup', this.handlePointerUp); - document.removeEventListener('keydown', this.handleKeyDown); + #detachEventListeners() { + this.#canvas.removeEventListener('pointerdown', this.#handlePointerDown); + this.#canvas.removeEventListener('pointermove', this.#handlePointerMove); + this.#canvas.removeEventListener('pointerup', this.#handlePointerUp); + document.removeEventListener('keydown', this.#handleKeyDown); } - private handlePointerMove = (event: PointerEvent) => { - const rect = this.canvas.getBoundingClientRect(); + #handlePointerMove = (event: PointerEvent) => { + const rect = this.#canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; // Update previous position before setting new position - this.pointer.prevX = this.pointer.x; - this.pointer.prevY = this.pointer.y; + this.#pointer.prevX = this.#pointer.x; + this.#pointer.prevY = this.#pointer.y; // Scale coordinates relative to canvas size - this.pointer.x = (x / rect.width) * this.canvas.width; - this.pointer.y = (y / rect.height) * this.canvas.height; + this.#pointer.x = (x / rect.width) * this.#canvas.width; + this.#pointer.y = (y / rect.height) * this.#canvas.height; }; - private handlePointerDown = (event: PointerEvent) => { - const rect = this.canvas.getBoundingClientRect(); + #handlePointerDown = (event: PointerEvent) => { + const rect = this.#canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; // Scale coordinates relative to canvas size - this.pointer.x = (x / rect.width) * this.canvas.width; - this.pointer.y = (y / rect.height) * this.canvas.height; - this.pointer.prevX = this.pointer.x; - this.pointer.prevY = this.pointer.y; - this.pointer.down = true; + this.#pointer.x = (x / rect.width) * this.#canvas.width; + this.#pointer.y = (y / rect.height) * this.#canvas.height; + this.#pointer.prevX = this.#pointer.x; + this.#pointer.prevY = this.#pointer.y; + this.#pointer.down = true; }; - private handlePointerUp = () => { - this.pointer.down = false; + #handlePointerUp = () => { + this.#pointer.down = false; }; - private handleKeyDown = (event: KeyboardEvent) => { + #handleKeyDown = (event: KeyboardEvent) => { const key = parseInt(event.key); if (!isNaN(key)) { - this.setMaterialType(key); + this.#setMaterialType(key); } }; - private setMaterialType(type: number) { - this.materialType = Math.min(Math.max(type, 0), 9); - this.onMaterialChange?.(this.materialType); + #setMaterialType(type: number) { + this.#materialType = Math.min(Math.max(type, 0), 9); + this.onMaterialChange?.(this.#materialType); } - private resizeCanvas() { - const width = (this.canvas.clientWidth * this.PIXEL_RATIO) | 0; - const height = (this.canvas.clientHeight * this.PIXEL_RATIO) | 0; - if (this.canvas.width !== width || this.canvas.height !== height) { - this.canvas.width = width; - this.canvas.height = height; + #resizeCanvas() { + const width = (this.#canvas.clientWidth * this.#PIXEL_RATIO) | 0; + const height = (this.#canvas.clientHeight * this.#PIXEL_RATIO) | 0; + if (this.#canvas.width !== width || this.#canvas.height !== height) { + this.#canvas.width = width; + this.#canvas.height = height; return true; } return false; } - private render = (time: number = performance.now()) => { - if (this.resizeCanvas()) { - this.processResize(); + #render = (time: number = performance.now()) => { + if (this.#resizeCanvas()) { + this.#processResize(); } - this.simulationPass(time); - this.shadowPass(); - this.jfaPass(); - this.renderPass(time); + this.#simulationPass(time); + this.#shadowPass(); + this.#jfaPass(); + this.#renderPass(time); - this.pointer.prevX = this.pointer.x; - this.pointer.prevY = this.pointer.y; + this.#pointer.prevX = this.#pointer.x; + this.#pointer.prevY = this.#pointer.y; - requestAnimationFrame(this.render); + requestAnimationFrame(this.#render); }; - private renderPass(time: number) { - const gl = this.gl; + #renderPass(time: number) { + const gl = this.#gl; gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.drawBuffers([gl.BACK]); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); @@ -364,127 +364,127 @@ export class FolkSand extends FolkBaseSet { gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); - gl.useProgram(this.blitProgram); - gl.bindVertexArray(this.vao); + gl.useProgram(this.#blitProgram); + gl.bindVertexArray(this.#vao); - const timeLoc = gl.getUniformLocation(this.blitProgram, 'time'); - const resLoc = gl.getUniformLocation(this.blitProgram, 'resolution'); - const texLoc = gl.getUniformLocation(this.blitProgram, 'tex'); - const shadowTexLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexR'); - const shadowTexGLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexG'); - const shadowTexBLoc = gl.getUniformLocation(this.blitProgram, 'shadowTexB'); - const scaleLoc = gl.getUniformLocation(this.blitProgram, 'scale'); - const texResLoc = gl.getUniformLocation(this.blitProgram, 'texResolution'); - const texScaleLoc = gl.getUniformLocation(this.blitProgram, 'texScale'); + const timeLoc = gl.getUniformLocation(this.#blitProgram, 'time'); + const resLoc = gl.getUniformLocation(this.#blitProgram, 'resolution'); + const texLoc = gl.getUniformLocation(this.#blitProgram, 'tex'); + const shadowTexLoc = gl.getUniformLocation(this.#blitProgram, 'shadowTexR'); + const shadowTexGLoc = gl.getUniformLocation(this.#blitProgram, 'shadowTexG'); + const shadowTexBLoc = gl.getUniformLocation(this.#blitProgram, 'shadowTexB'); + const scaleLoc = gl.getUniformLocation(this.#blitProgram, 'scale'); + const texResLoc = gl.getUniformLocation(this.#blitProgram, 'texResolution'); + const texScaleLoc = gl.getUniformLocation(this.#blitProgram, 'texScale'); gl.uniform1f(timeLoc, time * 0.001); gl.uniform2f(resLoc, gl.canvas.width, gl.canvas.height); - gl.uniform2f(texResLoc, this.bufferWidth, this.bufferHeight); - gl.uniform1f(texScaleLoc, this.PIXELS_PER_PARTICLE); + gl.uniform2f(texResLoc, this.#bufferWidth, this.#bufferHeight); + gl.uniform1f(texScaleLoc, this.#PIXELS_PER_PARTICLE); gl.uniform1f(scaleLoc, 1.0); gl.uniform1i(texLoc, 0); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.tex[this.swap]); + gl.bindTexture(gl.TEXTURE_2D, this.#tex[this.#swap]); gl.uniform1i(shadowTexLoc, 1); gl.uniform1i(shadowTexGLoc, 2); gl.uniform1i(shadowTexBLoc, 3); gl.activeTexture(gl.TEXTURE1); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[1 - this.shadowSwap]); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[1 - this.#shadowSwap]); gl.activeTexture(gl.TEXTURE2); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[1 - this.shadowSwap]); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[1 - this.#shadowSwap]); gl.activeTexture(gl.TEXTURE3); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[1 - this.shadowSwap]); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[1 - this.#shadowSwap]); gl.activeTexture(gl.TEXTURE4); - gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); + gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); } - private simulationPass(time: number) { - const gl = this.gl; - gl.useProgram(this.program); - gl.bindVertexArray(this.vao); + #simulationPass(time: number) { + const gl = this.#gl; + gl.useProgram(this.#program); + gl.bindVertexArray(this.#vao); - const timeLoc = gl.getUniformLocation(this.program, 'time'); - const frameLoc = gl.getUniformLocation(this.program, 'frame'); - const resLoc = gl.getUniformLocation(this.program, 'resolution'); - const texLoc = gl.getUniformLocation(this.program, 'tex'); - const mouseLoc = gl.getUniformLocation(this.program, 'mouse'); - const materialTypeLoc = gl.getUniformLocation(this.program, 'materialType'); - const brushRadiusLoc = gl.getUniformLocation(this.program, 'brushRadius'); - const collisionTexLoc = gl.getUniformLocation(this.program, 'u_collisionTex'); + const timeLoc = gl.getUniformLocation(this.#program, 'time'); + const frameLoc = gl.getUniformLocation(this.#program, 'frame'); + const resLoc = gl.getUniformLocation(this.#program, 'resolution'); + const texLoc = gl.getUniformLocation(this.#program, 'tex'); + const mouseLoc = gl.getUniformLocation(this.#program, 'mouse'); + const materialTypeLoc = gl.getUniformLocation(this.#program, 'materialType'); + const brushRadiusLoc = gl.getUniformLocation(this.#program, 'brushRadius'); + const collisionTexLoc = gl.getUniformLocation(this.#program, 'u_collisionTex'); if (!collisionTexLoc) { console.error('Could not find u_collisionTex uniform 1'); } if (collisionTexLoc !== null) { gl.uniform1i(collisionTexLoc, 5); // Use texture unit 5 gl.activeTexture(gl.TEXTURE5); - gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); + gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex); } - let mx = (this.pointer.x / gl.canvas.width) * this.bufferWidth; - let my = (1.0 - this.pointer.y / gl.canvas.height) * this.bufferHeight; - let mpx = (this.pointer.prevX / gl.canvas.width) * this.bufferWidth; - let mpy = (1.0 - this.pointer.prevY / gl.canvas.height) * this.bufferHeight; + let mx = (this.#pointer.x / gl.canvas.width) * this.#bufferWidth; + let my = (1.0 - this.#pointer.y / gl.canvas.height) * this.#bufferHeight; + let mpx = (this.#pointer.prevX / gl.canvas.width) * this.#bufferWidth; + let mpy = (1.0 - this.#pointer.prevY / gl.canvas.height) * this.#bufferHeight; let pressed = false; gl.uniform1f(timeLoc, time * 0.001); - gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight); - gl.uniform1i(materialTypeLoc, this.materialType); - gl.uniform1f(brushRadiusLoc, this.brushRadius); + gl.uniform2f(resLoc, this.#bufferWidth, this.#bufferHeight); + gl.uniform1i(materialTypeLoc, this.#materialType); + gl.uniform1f(brushRadiusLoc, this.#brushRadius); - if (this.pointer.down || pressed) gl.uniform4f(mouseLoc, mx, my, mpx, mpy); + if (this.#pointer.down || pressed) gl.uniform4f(mouseLoc, mx, my, mpx, mpy); else gl.uniform4f(mouseLoc, -mx, -my, -mpx, -mpy); const PASSES = 3; for (let i = 0; i < PASSES; i++) { - gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[this.swap]); - gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo[this.#swap]); + gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); - gl.uniform1i(frameLoc, this.frames * PASSES + i); + gl.uniform1i(frameLoc, this.#frames * PASSES + i); gl.uniform1i(texLoc, 0); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.tex[1 - this.swap]); + gl.bindTexture(gl.TEXTURE_2D, this.#tex[1 - this.#swap]); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); - this.swap = 1 - this.swap; + this.#swap = 1 - this.#swap; } - this.frames++; + this.#frames++; } - private jfaPass() { - const gl = this.gl; + #jfaPass() { + const gl = this.#gl; const JFA_PASSES = 5; - gl.useProgram(this.jfaShadowProgram); + gl.useProgram(this.#jfaShadowProgram); - const resLoc = gl.getUniformLocation(this.jfaShadowProgram, 'resolution'); - const texLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texR'); - const texGLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texG'); - const texBLoc = gl.getUniformLocation(this.jfaShadowProgram, 'texB'); - const stepSizeLoc = gl.getUniformLocation(this.jfaShadowProgram, 'stepSize'); - const passCountLoc = gl.getUniformLocation(this.jfaShadowProgram, 'passCount'); - const passIdxLoc = gl.getUniformLocation(this.jfaShadowProgram, 'passIndex'); + const resLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'resolution'); + const texLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'texR'); + const texGLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'texG'); + const texBLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'texB'); + const stepSizeLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'stepSize'); + const passCountLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'passCount'); + const passIdxLoc = gl.getUniformLocation(this.#jfaShadowProgram, 'passIndex'); - gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight); + gl.uniform2f(resLoc, this.#bufferWidth, this.#bufferHeight); gl.uniform1i(texLoc, 0); gl.uniform1i(texGLoc, 1); gl.uniform1i(texBLoc, 2); gl.uniform1i(passCountLoc, JFA_PASSES); for (let i = 0; i < JFA_PASSES; i++) { - gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[this.shadowSwap]); - gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo[this.#shadowSwap]); + gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); @@ -495,65 +495,65 @@ export class FolkSand extends FolkBaseSet { gl.uniform1i(passIdxLoc, i); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[1 - this.shadowSwap]); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[1 - this.#shadowSwap]); gl.activeTexture(gl.TEXTURE1); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[1 - this.shadowSwap]); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[1 - this.#shadowSwap]); gl.activeTexture(gl.TEXTURE2); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[1 - this.shadowSwap]); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[1 - this.#shadowSwap]); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); - this.shadowSwap = 1 - this.shadowSwap; + this.#shadowSwap = 1 - this.#shadowSwap; } } - private shadowPass() { - const gl = this.gl; - this.shadowSwap = 0; + #shadowPass() { + const gl = this.#gl; + this.#shadowSwap = 0; - gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFbo[this.shadowSwap]); - gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo[this.#shadowSwap]); + gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); - gl.useProgram(this.jfaInitProgram); - gl.bindVertexArray(this.vao); + gl.useProgram(this.#jfaInitProgram); + gl.bindVertexArray(this.#vao); - const resLoc = gl.getUniformLocation(this.jfaInitProgram, 'resolution'); - const texLoc = gl.getUniformLocation(this.jfaInitProgram, 'dataTex'); + const resLoc = gl.getUniformLocation(this.#jfaInitProgram, 'resolution'); + const texLoc = gl.getUniformLocation(this.#jfaInitProgram, 'dataTex'); - gl.uniform2f(resLoc, this.bufferWidth, this.bufferHeight); + gl.uniform2f(resLoc, this.#bufferWidth, this.#bufferHeight); gl.uniform1i(texLoc, 0); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.tex[this.swap]); + gl.bindTexture(gl.TEXTURE_2D, this.#tex[this.#swap]); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); - this.shadowSwap = 1 - this.shadowSwap; + this.#shadowSwap = 1 - this.#shadowSwap; } - private processResize() { - const gl = this.gl; + #processResize() { + const gl = this.#gl; gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - this.bufferWidth = Math.ceil(gl.canvas.width / this.PIXELS_PER_PARTICLE); - this.bufferHeight = Math.ceil(gl.canvas.height / this.PIXELS_PER_PARTICLE); + this.#bufferWidth = Math.ceil(gl.canvas.width / this.#PIXELS_PER_PARTICLE); + this.#bufferHeight = Math.ceil(gl.canvas.height / this.#PIXELS_PER_PARTICLE); // Update collision texture size - gl.bindTexture(gl.TEXTURE_2D, this.collisionTex); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.bindTexture(gl.TEXTURE_2D, this.#collisionTex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // Re-render collision data after resize - this.handleShapeTransform(); + this.#handleShapeTransform(); for (let i = 0; i < 2; i++) { - gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo[i]); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo[i]); const newTex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, newTex); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); @@ -561,51 +561,51 @@ export class FolkSand extends FolkBaseSet { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, newTex); - gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, this.bufferWidth, this.bufferHeight); + gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, this.#bufferWidth, this.#bufferHeight); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTex, 0); - gl.deleteTexture(this.tex[i]); - if (!this.tex[i]) { + gl.deleteTexture(this.#tex[i]); + if (!this.#tex[i]) { throw new Error('Failed to create texture1'); } if (!newTex) { throw new Error('Failed to create texture2'); } - this.tex[i] = newTex; + this.#tex[i] = newTex; } for (let i = 0; i < 2; i++) { - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexR[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexR[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexG[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexG[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); - gl.bindTexture(gl.TEXTURE_2D, this.shadowTexB[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.bufferWidth, this.bufferHeight, 0, gl.RGBA, gl.FLOAT, null); + gl.bindTexture(gl.TEXTURE_2D, this.#shadowTexB[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, this.#bufferWidth, this.#bufferHeight, 0, gl.RGBA, gl.FLOAT, null); } } - private createProgramFromStrings({ + #createProgramFromStrings({ vertex, fragment, }: { vertex: string; fragment: string; }): WebGLProgram | undefined { - const vertexShader = WebGLUtils.createShader(this.gl, this.gl.VERTEX_SHADER, vertex); - const fragmentShader = WebGLUtils.createShader(this.gl, this.gl.FRAGMENT_SHADER, fragment); + const vertexShader = WebGLUtils.createShader(this.#gl, this.#gl.VERTEX_SHADER, vertex); + const fragmentShader = WebGLUtils.createShader(this.#gl, this.#gl.FRAGMENT_SHADER, fragment); if (!vertexShader || !fragmentShader) { console.error('Failed to create shaders'); return undefined; } - return WebGLUtils.createProgram(this.gl, vertexShader, fragmentShader); + return WebGLUtils.createProgram(this.#gl, vertexShader, fragmentShader); } - private collectShapeData() { + #collectShapeData() { const positions: number[] = []; const indices: number[] = []; let vertexOffset = 0; @@ -626,7 +626,7 @@ export class FolkSand extends FolkBaseSet { } // Convert the transformed points to buffer coordinates - const bufferPoints = transformedPoints.map((point) => this.convertToBufferCoordinates(point.x, point.y)); + const bufferPoints = transformedPoints.map((point) => this.#convertToBufferCoordinates(point.x, point.y)); // Add vertices bufferPoints.forEach((point) => { @@ -639,30 +639,30 @@ export class FolkSand extends FolkBaseSet { vertexOffset += 4; }); - const gl = this.gl; + const gl = this.#gl; // Update buffers with new data - gl.bindBuffer(gl.ARRAY_BUFFER, this.shapePositionBuffer); + gl.bindBuffer(gl.ARRAY_BUFFER, this.#shapePositionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW); - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.shapeIndexBuffer); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#shapeIndexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.DYNAMIC_DRAW); - this.shapeIndexCount = indices.length; + this.#shapeIndexCount = indices.length; } - private convertToBufferCoordinates(x: number, y: number) { + #convertToBufferCoordinates(x: number, y: number) { return { x: (x / this.clientWidth) * 2 - 1, y: -((y / this.clientHeight) * 2 - 1), // Flip Y coordinate }; } - private updateCollisionTexture() { - const gl = this.gl; + #updateCollisionTexture() { + const gl = this.#gl; - gl.bindFramebuffer(gl.FRAMEBUFFER, this.collisionFbo); - gl.viewport(0, 0, this.bufferWidth, this.bufferHeight); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.#collisionFbo); + gl.viewport(0, 0, this.#bufferWidth, this.#bufferHeight); // Clear with transparent black (no collision) gl.clearColor(0, 0, 0, 0); @@ -673,12 +673,12 @@ export class FolkSand extends FolkBaseSet { gl.disable(gl.BLEND); // Use collision shader program - gl.useProgram(this.collisionProgram); - gl.bindVertexArray(this.shapeVao); + gl.useProgram(this.#collisionProgram); + gl.bindVertexArray(this.#shapeVao); // Draw all shapes - if (this.shapeIndexCount > 0) { - gl.drawElements(gl.TRIANGLES, this.shapeIndexCount, gl.UNSIGNED_SHORT, 0); + if (this.#shapeIndexCount > 0) { + gl.drawElements(gl.TRIANGLES, this.#shapeIndexCount, gl.UNSIGNED_SHORT, 0); } // Cleanup @@ -691,13 +691,13 @@ export class FolkSand extends FolkBaseSet { if (this.sourcesMap.size !== this.sourceElements.size) return; - this.handleShapeTransform(); + this.#handleShapeTransform(); } - private handleShapeTransform() { + #handleShapeTransform() { // Recollect and update all shape data when any shape changes // TODO: do this more piecemeal - this.collectShapeData(); - this.updateCollisionTexture(); + this.#collectShapeData(); + this.#updateCollisionTexture(); } }