1/* Native WebGPU sculpture. A bounded Canvas 2D rendition is the deliberate fallback. */ 2(() => { 3 'use strict'; 4 const shader = ` 5 struct Scene { viewport: vec4f, pointer: vec4f }; 6 @group(0) @binding(0) var<uniform> scene: Scene; 7 struct VertexOut { 8 @builtin(position) position: vec4f, 9 @location(0) uv: vec2f, 10 @location(1) tint: vec4f, 11 }; 12 @vertex fn vertexMain(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VertexOut { 13 let corners = array<vec2f, 6>(vec2f(-1.,-1.),vec2f(1.,-1.),vec2f(-1.,1.),vec2f(-1.,1.),vec2f(1.,-1.),vec2f(1.,1.)); 14 let tau = 6.28318530718; 15 let time = scene.viewport.z; 16 let u = f32(ii % 240u) / 240. * tau; 17 let v = f32(ii / 240u) / 88. * tau; 18 let twist = v + u * 3. + time * 0.11; 19 let major = 1.8 + .23 * sin(u * 3. + time * .24); 20 let minor = .50 + .12 * sin(v * 2. + u * 4. - time * .26); 21 var p = vec3f((major + minor*cos(twist))*cos(u), (major + minor*cos(twist))*sin(u), minor*sin(twist)); 22 let ay = time * .085 + scene.pointer.x * .26; 23 p = vec3f(p.x*cos(ay)+p.z*sin(ay),p.y,-p.x*sin(ay)+p.z*cos(ay)); 24 let ax = .82 + sin(time*.09)*.2 + scene.pointer.y*.14; 25 p = vec3f(p.x,p.y*cos(ax)-p.z*sin(ax),p.y*sin(ax)+p.z*cos(ax)); 26 let az = -.32 + scene.pointer.z * .20; 27 p = vec3f(p.x*cos(az)-p.y*sin(az),p.x*sin(az)+p.y*cos(az),p.z); 28 let perspective = 4.6 / (5.4 + p.z); 29 let aspect = scene.viewport.x / scene.viewport.y; 30 let mobile = select(1., .8, aspect < .8); 31 var position = vec2f(p.x/aspect,p.y) * .49 * perspective * mobile; 32 position.x += select(.46, .08, aspect < .8); 33 position.y += .10 - scene.pointer.z * .08; 34 let pointerPos = vec2f(scene.pointer.x/aspect,scene.pointer.y); 35 let distance = length(position-pointerPos); 36 let repel = exp(-distance*distance*5.) * .045; 37 position += normalize(position-pointerPos+vec2f(.0001)) * repel; 38 let wave = sin(distance*16.-scene.pointer.w*8.) * exp(-scene.pointer.w*1.8) * .13; 39 position += normalize(position-pointerPos+vec2f(.0001))*wave; 40 let depth = clamp((p.z+2.6)/5.2,0.,1.); 41 let size = (1.25 + depth*1.6) * scene.viewport.w; 42 let corner = corners[vi]; 43 let offset = corner * size / scene.viewport.xy * 2.; 44 var output: VertexOut; 45 output.position = vec4f(position+offset,0.,1.); 46 output.uv = corner; 47 let base = mix(vec3f(.45,.91,.81),vec3f(.84,1.,.25),.5+.5*sin(u*2.+time*.1)); 48 let edgeFade = smoothstep(-1.,-.25,position.x); 49 output.tint = vec4f(base,(.14+depth*.52)*edgeFade); 50 return output; 51 } 52 @fragment fn fragmentMain(input: VertexOut) -> @location(0) vec4f { 53 let radius = length(input.uv); 54 let alpha = (1.-smoothstep(.15,1.,radius))*input.tint.a; 55 return vec4f(input.tint.rgb*alpha,alpha); 56 } 57 `; 58 59 class ShoeMoneyWorld { 60 constructor(canvas, options = {}) { 61 this.canvas = canvas; 62 this.motion = options.motion !== false; 63 this.canvas.dataset.motion = this.motion ? 'on' : 'off'; 64 this.destroyed = false; 65 this.renderedFrames = 0; 66 this.pointer = { x: .35, y: .1, tx: .35, ty: .1 }; 67 this.time = 16; 68 this.burstAge = 20; 69 this.lastFrame = 0; 70 this.raf = 0; 71 this.scroll = 0; 72 this.mode = 'initializing'; 73 this.uniformData = new Float32Array(8); 74 this.listeners = new AbortController(); 75 const signal = this.listeners.signal; 76 window.addEventListener('resize', () => this.resize(), { signal, passive: true }); 77 window.addEventListener('pointermove', event => { 78 this.pointer.tx = (event.clientX / innerWidth - .5) * 2; 79 this.pointer.ty = (.5 - event.clientY / innerHeight) * 2; 80 if (!this.motion) return; 81 this.requestFrame(); 82 }, { signal, passive: true }); 83 window.addEventListener('pointerdown', () => { if (this.motion) { this.burstAge = 0; this.requestFrame(); } }, { signal, passive: true }); 84 window.addEventListener('scroll', () => { this.scroll = Math.min(scrollY / innerHeight, 5); }, { signal, passive: true }); 85 document.addEventListener('visibilitychange', () => { 86 if (document.hidden) { cancelAnimationFrame(this.raf); this.raf = 0; this.lastFrame = 0; } 87 else this.requestFrame(); 88 }, { signal }); 89 this.initialize(); 90 } 91 92 async initialize() {
93 try { 94 if (!navigator.gpu || !window.isSecureContext) throw new Error('WebGPU unavailable'); 95 const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'low-power' }); 96 if (!adapter || this.destroyed) throw new Error('No GPU adapter'); 97 const device = await adapter.requestDevice(); 98 if (this.destroyed) { device.destroy(); return; } 99 this.device = device; 100 device.addEventListener('uncapturederror', event => { event.preventDefault(); this.useFallback(); }); 101 device.lost.then(() => { if (!this.destroyed) this.useFallback(); }); 102 const context = this.canvas.getContext('webgpu'); 103 if (!context) throw new Error('No WebGPU context'); 104 const format = navigator.gpu.getPreferredCanvasFormat(); 105 const module = device.createShaderModule({ label: 'ShoeMoney orbital field', code: shader }); 106 const info = await module.getCompilationInfo(); 107 if (info.messages.some(message => message.type === 'error')) throw new Error('Shader compilation failed'); 108 this.pipeline = await device.createRenderPipelineAsync({ 109 label: 'ShoeMoney instanced particle sculpture', 110 layout: 'auto', 111 vertex: { module, entryPoint: 'vertexMain' }, 112 fragment: { module, entryPoint: 'fragmentMain', targets: [{ format, blend: { 113 color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' }, 114 alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' }, 115 } }] }, 116 primitive: { topology: 'triangle-list' }, 117 }); 118 if (this.destroyed) return; 119 this.uniformBuffer = device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); 120 this.bindGroup = device.createBindGroup({ layout: this.pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.uniformBuffer } }] }); 121 this.context = context; 122 this.format = format; 123 this.mode = 'webgpu'; 124 this.canvas.dataset.renderer = this.mode; 125 this.resize(); 126 } catch { this.useFallback(); } 127 } 128 129 useFallback() { 130 if (this.destroyed || this.mode === 'canvas2d') return; 131 cancelAnimationFrame(this.raf); 132 this.raf = 0; 133 this.mode = 'canvas2d'; 134 // A canvas cannot change context types after creation. Replace only the decorative canvas. 135 const replacement = this.canvas.cloneNode(false); 136 this.canvas.replaceWith(replacement); 137 this.canvas = replacement; 138 this.context = replacement.getContext('2d', { alpha: true }); 139 this.canvas.dataset.renderer = this.context ? 'canvas2d' : 'css'; 140 if (this.uniformBuffer) { this.uniformBuffer.destroy(); this.uniformBuffer = null; } 141 if (this.device) { const oldDevice = this.device; this.device = null; oldDevice.destroy(); } 142 this.resize(); 143 } 144 145 resize() { 146 if (this.destroyed) return; 147 this.dpr = Math.min(devicePixelRatio || 1, 1.6); 148 this.canvas.width = Math.max(1, Math.round(innerWidth * this.dpr)); 149 this.canvas.height = Math.max(1, Math.round(innerHeight * this.dpr)); 150 if (this.mode === 'webgpu') this.context.configure({ device: this.device, format: this.format, alphaMode: 'premultiplied' }); 151 this.requestFrame(); 152 } 153 154 setMotion(enabled) { 155 this.motion = enabled; 156 this.canvas.dataset.motion = enabled ? 'on' : 'off'; 157 cancelAnimationFrame(this.raf); 158 this.raf = 0; 159 this.lastFrame = 0; 160 this.requestFrame(); 161 } 162 163 requestFrame() { 164 if (!this.raf && !this.destroyed && !document.hidden && this.mode !== 'initializing') this.raf = requestAnimationFrame(timestamp => this.frame(timestamp)); 165 } 166 167 frame(timestamp) { 168 this.raf = 0; 169 if (this.destroyed || document.hidden) return; 170 const delta = this.lastFrame ? Math.min((timestamp - this.lastFrame) / 1000, .05) : 0; 171 this.lastFrame = timestamp; 172 if (this.motion) { 173 this.time += delta; 174 this.burstAge += delta; 175 this.pointer.x += (this.pointer.tx - this.pointer.x) * .045; 176 this.pointer.y += (this.pointer.ty - this.pointer.y) * .045; 177 }
178 try { 179 if (this.mode === 'webgpu') this.drawGPU(); 180 else if (this.mode === 'canvas2d' && this.context) this.drawCanvas(); 181 } catch { this.useFallback(); } 182 this.renderedFrames++; 183 this.canvas.dataset.frames = String(this.renderedFrames); 184 if (this.motion) this.requestFrame(); 185 } 186 187 drawGPU() { 188 this.uniformData.set([this.canvas.width, this.canvas.height, this.time, this.dpr, this.pointer.x, this.pointer.y, this.scroll, this.burstAge]); 189 this.device.queue.writeBuffer(this.uniformBuffer, 0, this.uniformData); 190 const encoder = this.device.createCommandEncoder(); 191 const pass = encoder.beginRenderPass({ colorAttachments: [{ view: this.context.getCurrentTexture().createView(), clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store' }] }); 192 pass.setPipeline(this.pipeline); 193 pass.setBindGroup(0, this.bindGroup); 194 pass.draw(6, 240 * 88); 195 pass.end(); 196 this.device.queue.submit([encoder.finish()]); 197 } 198 199 drawCanvas() { 200 const ctx = this.context, width = this.canvas.width, height = this.canvas.height; 201 ctx.clearRect(0, 0, width, height); 202 const aspect = width / height, mobile = aspect < .8, t = this.time; 203 const ay = t * .085 + this.pointer.x * .26, ax = .82 + Math.sin(t * .09) * .2 + this.pointer.y * .14, az = -.32 + this.scroll * .20; 204 const cx = width * (mobile ? .54 : .73), cy = height * (.45 + this.scroll * .04), factor = height * .245 * (mobile ? .8 : 1); 205 const sin = Math.sin, cos = Math.cos; 206 for (let j = 0; j < 30; j++) { 207 for (let i = 0; i < 110; i++) { 208 const u = i / 110 * Math.PI * 2, v = j / 30 * Math.PI * 2, twist = v + u * 3 + t * .11; 209 const major = 1.8 + .23 * sin(u * 3 + t * .24), minor = .50 + .12 * sin(v * 2 + u * 4 - t * .26); 210 let x = (major + minor * cos(twist)) * cos(u), y = (major + minor * cos(twist)) * sin(u), z = minor * sin(twist); 211 [x, z] = [x * cos(ay) + z * sin(ay), -x * sin(ay) + z * cos(ay)]; 212 [y, z] = [y * cos(ax) - z * sin(ax), y * sin(ax) + z * cos(ax)]; 213 [x, y] = [x * cos(az) - y * sin(az), x * sin(az) + y * cos(az)]; 214 const perspective = 4.6 / (5.4 + z), depth = Math.max(0, Math.min(1, (z + 2.6) / 5.2)); 215 const px = cx + x * perspective * factor, py = cy - y * perspective * factor; 216 const mix = .5 + .5 * sin(u * 2 + t * .1), alpha = (.2 + depth * .48) * Math.min(1, Math.max(0, px / (width * .5))); 217 ctx.fillStyle = `rgba(${Math.round(115 + mix * 99)},${Math.round(232 + mix * 23)},${Math.round(207 - mix * 143)},${alpha})`; 218 ctx.beginPath(); ctx.arc(px, py, (.8 + depth * .8) * this.dpr, 0, Math.PI * 2); ctx.fill(); 219 } 220 } 221 } 222 223 destroy() { 224 this.destroyed = true; 225 cancelAnimationFrame(this.raf); 226 this.listeners.abort(); 227 if (this.uniformBuffer) this.uniformBuffer.destroy(); 228 if (this.device) this.device.destroy(); 229 } 230 } 231 window.ShoeMoneyWorld = ShoeMoneyWorld; 232})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.