let gl, program; let textureInfo = null; let isRendering = true; let lastRenderTime = 0; const fpsInterval = 1000 / 60; let startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0); let currentTime = Date.now() - startOfDay.getTime(); const vertexShaderSource = `#version 300 es in vec4 a_position; in vec2 a_texcoord; uniform float textureScale; out vec2 v_texcoord; void main() { v_texcoord = (a_texcoord - 0.5) / textureScale + 0.5; gl_Position = a_position; } `; const fragmentShaderSource = `#version 300 es precision lowp float; in vec2 v_texcoord; uniform sampler2D texture1; uniform float time, hue, noiseamount, speed, canvasTop, canvasHeight, viewportHeight, pixelRatio; out vec4 fragColor; #define NUM_OCTAVES 4 #define PI 3.14159265359 const vec2 offset = vec2(-0.014, -0.02); const float zoom = 0.97, ior = 1.18; const mat3 hueShiftMatrix = mat3( 0.167444, 0.329213, -0.496657, -0.327948, 0.035669, 0.292279, 1.250268, -1.047561, -0.202707 ); float rand(vec2 n) { return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453); } float noise(vec2 p) { vec2 ip = floor(p); vec2 u = fract(p); u = u * u * (3.0 - 2.0 * u); return mix( mix(rand(ip), rand(ip + vec2(1.0, 0.0)), u.x), mix(rand(ip + vec2(0.0, 1.0)), rand(ip + vec2(1.0, 1.0)), u.x), u.y); } float fbm(vec2 x) { float v = 0.0; float a = 0.5; vec2 shift = vec2(100); mat2 rot = mat2(cos(0.7), sin(0.4), -sin(0.5), cos(0.5)); for (int i = 0; i < NUM_OCTAVES; ++i) { v += a * noise(x); x = rot * x * 2.0 + shift; a *= 0.5; } return v; } vec3 hueshift(vec3 color, float dhue) { float s = sin(dhue), c = cos(dhue); return (color * c) + (color * s) * hueShiftMatrix + dot(vec3(0.299, 0.587, 0.114), color) * (1.0 - c); } vec2 computeSurface(float strength, float scale, vec2 uv, float timeOffset) { vec2 fbmInput = scale * uv + timeOffset; return strength * vec2( mix(-0.2, 0.4, fbm(fbmInput)), mix(-0.4, 0.4, fbm(fbmInput)) ); } void main() { vec2 uv = (v_texcoord - 0.5) * (zoom / (canvasHeight == 256.0 ? 0.95 : 1.0)) + 0.5 + offset; float multiplier = canvasHeight == 256.0 ? 8.1 : 1.0; float viewportY = canvasTop + canvasHeight - (gl_FragCoord.y / pixelRatio); float normalizedViewportY = viewportY / viewportHeight; float distortionAreaSize = 0.15; float smoothStep1 = smoothstep(0.0, distortionAreaSize, normalizedViewportY); float smoothStep2 = smoothstep(1.0 - distortionAreaSize, 1.0, normalizedViewportY); float distortionFactor = -1.0 + smoothStep1 + smoothStep2; float waveDistortion = fbm(uv * 6.90 * distortionFactor * multiplier * (0.2 * sin(time) + 1.2)) * distortionFactor * 0.13; uv.y += waveDistortion; float timeValue = time * speed * 2.0; float amplitude = 0.13; float baseStrength = 0.25; vec2 surface1 = computeSurface(baseStrength + amplitude * sin(time), 2.0, uv, 0.1 * timeValue + 0.01); vec2 surface2 = computeSurface(baseStrength + amplitude * sin(time + PI/3.0), 11.0, uv, 0.632 * timeValue + 0.82); vec2 surface3 = computeSurface(baseStrength + amplitude * sin(time + 2.0*PI/3.0), 18.0, uv, -0.332 * timeValue + 1.03); uv += refract(vec2(0.0), surface1, 1.0 / ior); vec4 color1 = texture(texture1, uv); uv += refract(vec2(0.0), surface2, 1.0 / ior); vec4 color2 = texture(texture1, uv); uv += refract(vec2(0.0), surface3, 1.0 / ior); vec4 color3 = texture(texture1, uv); float x = (uv.x * 40.0) * (uv.y + 5.0) * mix(abs(sin(time) * 0.203), 0.205, 0.4); vec4 grain = vec4(mod((mod(x, 13.0) + 11.0) * (mod(x, 71.0) + 1.0), 0.01) - 0.005); color3.rgb = hueshift(color3.rgb, -150.0); color2.rgb = hueshift(color2.rgb, 150.0); vec4 colorout = mix(color1 * color1.a, mix(color2, color3 + grain * noiseamount * color3.a, 0.40) * color1.a, 0.22) - grain * noiseamount * 1.3; if (hue != 0.0) { colorout.rgb = hueshift(colorout.rgb, hue); } fragColor = colorout; } `; let shaderUniformValues = { canvasTop: 0, canvasHeight: 0, viewportHeight: 0, pixelRatio: 1 }; function createShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } function createProgram(gl, vertexShader, fragmentShader) { const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)); gl.deleteProgram(program); return null; } return program; } function setupAttributes() { const positionAttributeLocation = gl.getAttribLocation(program, 'a_position'); const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); const positions = new Float32Array([ -1, -1, 1, -1, -1, 1, 1, 1, ]); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); gl.enableVertexAttribArray(positionAttributeLocation); gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0); const texCoordAttributeLocation = gl.getAttribLocation(program, 'a_texcoord'); const texCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); const texCoords = new Float32Array([ 0, 0, 1, 0, 0, 1, 1, 1, ]); gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW); gl.enableVertexAttribArray(texCoordAttributeLocation); gl.vertexAttribPointer(texCoordAttributeLocation, 2, gl.FLOAT, false, 0, 0); } function setupTexture(imageBitmap) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageBitmap); 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); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); return texture; } async function initializeScene(data) { const { offscreen, imgWidth, imgHeight, imageBitmap, hue, noiseAmount, speed } = data; gl = offscreen.getContext('webgl2'); if (!gl) throw new Error('WebGL2 not supported'); const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); program = createProgram(gl, vertexShader, fragmentShader); setupAttributes(); const texture = setupTexture(imageBitmap); // Set up uniforms const uniformLocations = { texture1: gl.getUniformLocation(program, 'texture1'), time: gl.getUniformLocation(program, 'time'), hue: gl.getUniformLocation(program, 'hue'), noiseamount: gl.getUniformLocation(program, 'noiseamount'), speed: gl.getUniformLocation(program, 'speed'), textureScale: gl.getUniformLocation(program, 'textureScale'), canvasTop: gl.getUniformLocation(program, 'canvasTop'), canvasHeight: gl.getUniformLocation(program, 'canvasHeight'), viewportHeight: gl.getUniformLocation(program, 'viewportHeight'), pixelRatio: gl.getUniformLocation(program, 'pixelRatio') }; gl.useProgram(program); gl.uniform1i(uniformLocations.texture1, 0); gl.uniform1f(uniformLocations.hue, hue); gl.uniform1f(uniformLocations.noiseamount, noiseAmount); gl.uniform1f(uniformLocations.speed, speed); gl.uniform1f(uniformLocations.textureScale, imgWidth === 512 ? 1.0 : 1.05); textureInfo = { width: imgWidth, height: imgHeight, uniformLocations }; render(); self.postMessage({ action: 'canvasReady' }); } function updateViewportData(data) { shaderUniformValues = { ...shaderUniformValues, ...data }; if (gl && program && textureInfo) { gl.useProgram(program); Object.entries(shaderUniformValues).forEach(([key, value]) => { const location = textureInfo.uniformLocations[key]; if (location !== null) { gl.uniform1f(location, value); } }); } } function render() { if (!isRendering || !gl || !program || !textureInfo) return; requestAnimationFrame(render); const elapsed = currentTime - lastRenderTime; if (elapsed > fpsInterval) { lastRenderTime = currentTime - (elapsed % fpsInterval); currentTime += fpsInterval; gl.viewport(0, 0, textureInfo.width, textureInfo.height); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(program); gl.uniform1f(textureInfo.uniformLocations.time, currentTime / 1000); Object.entries(shaderUniformValues).forEach(([key, value]) => { const location = textureInfo.uniformLocations[key]; if (location !== null) { gl.uniform1f(location, value); } }); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } } self.onmessage = async function(event) { const { action } = event.data; switch (action) { case 'init': await initializeScene(event.data); break; case 'resume': isRendering = true; if (gl && program) render(); break; case 'stop': isRendering = false; break; case 'updateViewport': updateViewportData(event.data); break; } };