Web of Springs (Interactive Animation)
Particles connected using simple springs (penalty-based forces). The springs are connected to a fixed location (calculated at startup). As you move the cursor around the window the springs respond by moving out of the way.
Fun with springs - connected in a 'web'.
Functions Used: requestAdapter(), getPreferredCanvasFormat(), createCommandEncoder(), beginRenderPass(), setPipeline(), draw(), end(), submit(), getCurrentTexture(), createView(), createShaderModule()
Complete Code
let div = document . createElement ( 'div' ); document . body . appendChild ( div ); div . style [ 'font-size' ] = '20pt' ; function log ( s ) { console . log ( s ); let args = [... arguments ]. join ( ' ' ); div . innerHTML += args + '<br><br>' ; } log ( 'WebGPU Compute Example' ); if (! navigator . gpu ) { log ( "WebGPU is not supported (or is it disabled? flags/settings)" ); return; } const adapter = await navigator . gpu . requestAdapter (); const device = await adapter . requestDevice (); // ---------------------------------------------------------- const timestep = new Float32Array ( [ 0.0 ] ); const timerUniformBuffer = device . createBuffer ({ size : timestep . byteLength , usage : GPUBufferUsage . UNIFORM | GPUBufferUsage . COPY_DST }); device . queue . writeBuffer ( timerUniformBuffer , 0 , timestep ); // ---------------------------------------------------------- const mouse = new Float32Array ( [ 900.0 , 900.0 ] ); const mouseBuffer = device . createBuffer ({ size : mouse . byteLength , usage : GPUBufferUsage . UNIFORM | GPUBufferUsage . COPY_DST }); device . queue . writeBuffer ( mouseBuffer , 0 , mouse ); // ---------------------------------------------------------- function Particle ( x , y ) { this . x = x ; this . y = y ; this . xBase = x ; this . yBase = y ; this . size = 3 , this . density = (( Math . random () * 30 ) + 1 ); this . random = Math . random (); this . spriteSize = Math . random () * 50 + 50 ; this . frameX = Math . floor ( Math . random () * 3 ); this . frameY = Math . floor ( Math . random () * 8 ); this . angle = Math . random () * 2 ; this . pad0 = 0 ; this . pad1 = 0 ; this . pad2 = 0 ; this . pad3 = 0 ; this . pad4 = 0 ; } const canvas = document . createElement ( 'canvas' ); canvas . style . border = '1px solid orange' ; document . body . appendChild ( canvas ); canvas . id = "canvas1" ; canvas . width = 1024 ; canvas . height = 512 ; const ctx = canvas . getContext ( "2d" ); ctx . font = 'bold 18px Verdana' ; ctx . fillText ( 'XBDEV' , 0 , 20 ); const imgData = ctx . getImageData ( 0 , 0 , canvas . width , canvas . height ); console . log ( 'canvas:' , canvas . width , canvas . height ); const sprite = new Image (); sprite . src = 'https://xbdev.net/retro/code/games/images/spiders.png' ; let particleSpacing = 15 ; let particles = []; for (var y = 0 , y2 = canvas . height ; y < y2 ; y ++) { for (var x = 0 , x2 = canvas . width ; x < x2 ; x ++) { if ( imgData . data [( y * 4 * canvas . width ) + ( x * 4 ) + 3 ] > 128 ) { let positionX = x ; let positionY = y ; particles . push (new Particle ( positionX * particleSpacing , positionY * particleSpacing )); } } } console . log ( 'number particles:' , particles . length ); let particlesTmp = particles . map ( ( a )=>{ return Object . values ( a ); } ). flat (); const particleArray = new Float32Array ( particlesTmp ); const particleStride = particleArray . length / particles . length ; console . log ( 'particle stride:' , particleStride ); console . log ( 'number particles:' , particles . length , particleArray . length ); console . log ( 'length buffer in bytes:' , particleArray . byteLength ); const particleBuffer = device . createBuffer ({ size : particleArray . byteLength , usage : GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_DST | GPUBufferUsage . COPY_SRC }); device . queue . writeBuffer ( particleBuffer , 0 , particleArray ); const particleBufferTmp = device . createBuffer ({ size : particleArray . byteLength , usage : GPUBufferUsage . COPY_DST | GPUBufferUsage . MAP_READ }); // ---------------------------------------------------------- const GCOMPUTE = GPUShaderStage . COMPUTE ; // Bind group layout and bind group const bindGroupLayout = device . createBindGroupLayout ({ entries : [ { binding : 0 , visibility : GCOMPUTE , buffer : { type : "uniform" } }, { binding : 1 , visibility : GCOMPUTE , buffer : { type : "uniform" } }, { binding : 2 , visibility : GCOMPUTE , buffer : { type : "storage" } } ] }); const bindGroup0 = device . createBindGroup ({ layout : bindGroupLayout , entries : [ { binding : 0 , resource : { buffer : timerUniformBuffer } }, { binding : 1 , resource : { buffer : mouseBuffer } }, { binding : 2 , resource : { buffer : particleBuffer } } ] }); // Compute shader code const computeShader = ` struct Particle { @align(4) @size(4) x : f32, @align(4) @size(4) y : f32, @align(4) @size(4) baseX : f32, @align(4) @size(4) baseY : f32, @align(4) @size(4) size : f32, @align(4) @size(4) density : f32, @align(4) @size(4) random : f32, @align(4) pad : array< f32, 9 > } @group(0) @binding(0) var<uniform> mytimer : f32; // timer increments each frame @group(0) @binding(1) var<uniform> mymouse : vec2<f32>; // oouse coordinates @group(0) @binding(2) var<storage, read_write> particles : array< Particle, ${ particles . length } >; @compute @workgroup_size( 8 ) fn main(@builtin(global_invocation_id) globalId : vec3<u32>, @builtin(local_invocation_id) localId : vec3<u32>, @builtin(workgroup_id) workgroupId : vec3<u32>, @builtin(num_workgroups) workgroupSize : vec3<u32> ) { let index = globalId.x; const mouseRadius = 150.0; var dx = mymouse.x - particles[index].x; var dy = mymouse.y - particles[index].y; var distance = sqrt(dx*dx + dy*dy); var forceDirectionX = dx / distance; var forceDirectionY = dy / distance; // distance past which the force is zero var maxDistance = mouseRadius; var force = (maxDistance - distance) / maxDistance; // if we went below zero, set it to zero. if (force < 0) { force = 0; } var directionX = (forceDirectionX * force * particles[index].density); var directionY = (forceDirectionY * force * particles[index].density); if (distance < mouseRadius + particles[index].size){ particles[index].x -= directionX; particles[index].y -= directionY; } else { if (particles[index].x != particles[index].baseX ) { let dx = particles[index].x - particles[index].baseX; particles[index].x -= dx/10; } if (particles[index].y != particles[index].baseY) { let dy = particles[index].y - particles[index].baseY; particles[index].y -= dy/10; } } } `; // Pipeline setup const computePipeline = device . createComputePipeline ({ layout : device . createPipelineLayout ({ bindGroupLayouts : [ bindGroupLayout ]}), compute : { module : device . createShaderModule ({ code : computeShader }), entryPoint : "main" } }); document . onmousemove = function( e ) { mouse [ 0 ] = e . x + canvas . clientLeft / 2 ; mouse [ 1 ] = e . y + canvas . clientTop / 2 ; } async function frame () { timestep [ 0 ] = timestep [ 0 ] + 0.01 ; device . queue . writeBuffer ( timerUniformBuffer , 0 , timestep ); device . queue . writeBuffer ( mouseBuffer , 0 , mouse ); // Commands submission const commandEncoder = device . createCommandEncoder (); { const passEncoder = commandEncoder . beginComputePass (); passEncoder . setPipeline ( computePipeline ); passEncoder . setBindGroup ( 0 , bindGroup0 ); // workgroup size of 8x8 on the wgsl shader passEncoder . dispatchWorkgroups ( particles . length / 8 , 1 ); await passEncoder . end (); } // -------------------------- commandEncoder . copyBufferToBuffer ( particleBuffer , 0 , particleBufferTmp , 0 , particleArray . byteLength ); const gpuCommands = commandEncoder . finish (); await device . queue . submit ([ gpuCommands ]); await particleBufferTmp . mapAsync ( GPUMapMode . READ ); const particleArrayTmp = new Float32Array ( particleBufferTmp . getMappedRange ()); const particleArrayOut = Array. from ( particleArrayTmp ); particleBufferTmp . unmap (); // ---------------------- ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ); for ( let i = 0 ; i < particles . length ; i ++) { let particle = particles [ i ]; let newx = particleArrayOut [ particleStride * i + 0 ]; let newy = particleArrayOut [ particleStride * i + 1 ]; particles [ i ]. x = newx ; particles [ i ]. y = newy ; particle . x = newx ; particle . y = newy ; if ( particle . random > 0.05 ) { ctx . fillStyle = 'black' ; ctx . beginPath (); ctx . arc ( particle . x , particle . y , particle . size , 0 , Math . PI * 2 ); ctx . closePath (); ctx . fill (); } else { ctx . save (); ctx . translate ( particle . x , particle . y ); ctx . rotate ( particle . angle ); ctx . drawImage ( sprite , particle . frameX * 213.3 , particle . frameY * 213.3 , 213.3 , 213.3 , 0 - particle . spriteSize / 2 , 0 - particle . spriteSize / 2 , particle . spriteSize , particle . spriteSize ); ctx . restore (); } } // end for i // --------------------- // Draw the links so it looks 'webby' (fits in with all the spider images) let mouseRadius = 150 ; let opacityValue = 1 ; for ( let a = 0 ; a < particles . length ; a ++) { for ( let b = a ; b < particles . length ; b ++) { let distance = (( particles [ a ]. x - particles [ b ]. x ) * ( particles [ a ]. x - particles [ b ]. x )) + (( particles [ a ]. y - particles [ b ]. y ) * ( particles [ a ]. y - particles [ b ]. y )); const maxdist = 2000 ; if ( distance < maxdist ) { opacityValue = 1 - ( distance / maxdist ); let dx = mouse [ 0 ] - particles [ a ]. x ; let dy = mouse [ 1 ] - particles [ a ]. y ; let mouseDistance = Math . sqrt ( dx * dx + dy * dy ); if ( mouseDistance < mouseRadius / 2 ) { ctx . strokeStyle = 'rgba(0,0,0,' + opacityValue + ')' ; } else if ( mouseDistance < mouseRadius - 50 ) { ctx . strokeStyle = 'rgba(0,0,90,' + opacityValue + ')' ; } else if ( mouseDistance < mouseRadius + 20 ) { ctx . strokeStyle = 'rgba(0,0,50,' + opacityValue + ')' ; } else { ctx . strokeStyle = 'rgba(0,0,0,' + opacityValue + ')' ; } ctx . lineWidth = 1 ; ctx . beginPath (); ctx . moveTo ( particles [ a ]. x , particles [ a ]. y ); ctx . lineTo ( particles [ b ]. x , particles [ b ]. y ); ctx . stroke (); } } } // --------------------- requestAnimationFrame ( frame ); } frame ();
Things to Try
• Try other text and patterns
• Add other disturbances (e.g., rippling due to noises)
• Mix in more colors (particles change color based on velocity/penalty)
Resources and Links
• WebGPU Lab Demo [LINK ]