Buckets:
| import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js" | |
| import "./spotlight.css" | |
| export interface ParticlesConfig { | |
| enabled: boolean | |
| amount: number | |
| size: [number, number] | |
| speed: number | |
| opacity: number | |
| drift: number | |
| } | |
| export interface SpotlightConfig { | |
| placement: [number, number] | |
| color: string | |
| speed: number | |
| spread: number | |
| length: number | |
| width: number | |
| pulsating: false | [number, number] | |
| distance: number | |
| saturation: number | |
| noiseAmount: number | |
| distortion: number | |
| opacity: number | |
| particles: ParticlesConfig | |
| } | |
| export const defaultConfig: SpotlightConfig = { | |
| placement: [0.5, -0.15], | |
| color: "#ffffff", | |
| speed: 0.8, | |
| spread: 0.5, | |
| length: 4.0, | |
| width: 0.15, | |
| pulsating: [0.95, 1.1], | |
| distance: 3.5, | |
| saturation: 0.35, | |
| noiseAmount: 0.15, | |
| distortion: 0.05, | |
| opacity: 0.325, | |
| particles: { | |
| enabled: true, | |
| amount: 70, | |
| size: [1.25, 1.5], | |
| speed: 0.75, | |
| opacity: 0.9, | |
| drift: 1.5, | |
| }, | |
| } | |
| export interface SpotlightAnimationState { | |
| time: number | |
| intensity: number | |
| pulseValue: number | |
| } | |
| interface SpotlightProps { | |
| config: Accessor<SpotlightConfig> | |
| class?: string | |
| onAnimationFrame?: (state: SpotlightAnimationState) => void | |
| } | |
| const hexToRgb = (hex: string): [number, number, number] => { | |
| const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) | |
| return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1] | |
| } | |
| const getAnchorAndDir = ( | |
| placement: [number, number], | |
| w: number, | |
| h: number, | |
| ): { anchor: [number, number]; dir: [number, number] } => { | |
| const [px, py] = placement | |
| const outside = 0.2 | |
| let anchorX = px * w | |
| let anchorY = py * h | |
| let dirX = 0 | |
| let dirY = 0 | |
| const centerX = 0.5 | |
| const centerY = 0.5 | |
| if (py <= 0.25) { | |
| anchorY = -outside * h + py * h | |
| dirY = 1 | |
| dirX = (centerX - px) * 0.5 | |
| } else if (py >= 0.75) { | |
| anchorY = (1 + outside) * h - (1 - py) * h | |
| dirY = -1 | |
| dirX = (centerX - px) * 0.5 | |
| } else if (px <= 0.25) { | |
| anchorX = -outside * w + px * w | |
| dirX = 1 | |
| dirY = (centerY - py) * 0.5 | |
| } else if (px >= 0.75) { | |
| anchorX = (1 + outside) * w - (1 - px) * w | |
| dirX = -1 | |
| dirY = (centerY - py) * 0.5 | |
| } else { | |
| dirY = 1 | |
| } | |
| const len = Math.sqrt(dirX * dirX + dirY * dirY) | |
| if (len > 0) { | |
| dirX /= len | |
| dirY /= len | |
| } | |
| return { anchor: [anchorX, anchorY], dir: [dirX, dirY] } | |
| } | |
| interface UniformData { | |
| iTime: number | |
| iResolution: [number, number] | |
| lightPos: [number, number] | |
| lightDir: [number, number] | |
| color: [number, number, number] | |
| speed: number | |
| lightSpread: number | |
| lightLength: number | |
| sourceWidth: number | |
| pulsating: number | |
| pulsatingMin: number | |
| pulsatingMax: number | |
| fadeDistance: number | |
| saturation: number | |
| noiseAmount: number | |
| distortion: number | |
| particlesEnabled: number | |
| particleAmount: number | |
| particleSizeMin: number | |
| particleSizeMax: number | |
| particleSpeed: number | |
| particleOpacity: number | |
| particleDrift: number | |
| } | |
| const WGSL_SHADER = ` | |
| struct Uniforms { | |
| iTime: f32, | |
| _pad0: f32, | |
| iResolution: vec2<f32>, | |
| lightPos: vec2<f32>, | |
| lightDir: vec2<f32>, | |
| color: vec3<f32>, | |
| speed: f32, | |
| lightSpread: f32, | |
| lightLength: f32, | |
| sourceWidth: f32, | |
| pulsating: f32, | |
| pulsatingMin: f32, | |
| pulsatingMax: f32, | |
| fadeDistance: f32, | |
| saturation: f32, | |
| noiseAmount: f32, | |
| distortion: f32, | |
| particlesEnabled: f32, | |
| particleAmount: f32, | |
| particleSizeMin: f32, | |
| particleSizeMax: f32, | |
| particleSpeed: f32, | |
| particleOpacity: f32, | |
| particleDrift: f32, | |
| _pad1: f32, | |
| _pad2: f32, | |
| }; | |
| @group(0) @binding(0) var<uniform> uniforms: Uniforms; | |
| struct VertexOutput { | |
| @builtin(position) position: vec4<f32>, | |
| @location(0) vUv: vec2<f32>, | |
| }; | |
| @vertex | |
| fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { | |
| var positions = array<vec2<f32>, 3>( | |
| vec2<f32>(-1.0, -1.0), | |
| vec2<f32>(3.0, -1.0), | |
| vec2<f32>(-1.0, 3.0) | |
| ); | |
| var output: VertexOutput; | |
| let pos = positions[vertexIndex]; | |
| output.position = vec4<f32>(pos, 0.0, 1.0); | |
| output.vUv = pos * 0.5 + 0.5; | |
| return output; | |
| } | |
| fn hash(p: vec2<f32>) -> f32 { | |
| let p3 = fract(p.xyx * 0.1031); | |
| return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33)); | |
| } | |
| fn hash2(p: vec2<f32>) -> vec2<f32> { | |
| let n = sin(dot(p, vec2<f32>(41.0, 289.0))); | |
| return fract(vec2<f32>(n * 262144.0, n * 32768.0)); | |
| } | |
| fn fastNoise(st: vec2<f32>) -> f32 { | |
| return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453); | |
| } | |
| fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 { | |
| let sourceToCoord = coord - lightSource; | |
| let distSq = dot(sourceToCoord, sourceToCoord); | |
| let distance = sqrt(distSq); | |
| let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y); | |
| let maxDistance = max(baseSize * uniforms.lightLength, 0.001); | |
| if (distance > maxDistance) { | |
| return 0.0; | |
| } | |
| let invDist = 1.0 / max(distance, 0.001); | |
| let dirNorm = sourceToCoord * invDist; | |
| let cosAngle = dot(dirNorm, lightRefDirection); | |
| if (cosAngle < 0.0) { | |
| return 0.0; | |
| } | |
| let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x)); | |
| let time = uniforms.iTime; | |
| let speed = uniforms.speed; | |
| let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0)); | |
| let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6; | |
| let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7; | |
| let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift; | |
| let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35; | |
| let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28; | |
| let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001); | |
| let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread); | |
| let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0); | |
| let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001); | |
| let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0); | |
| var pulse: f32 = 1.0; | |
| if (uniforms.pulsating > 0.5) { | |
| let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5; | |
| let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5; | |
| pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0); | |
| } | |
| let timeSpeed = time * speed; | |
| let wave = 0.5 | |
| + 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2) | |
| + 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0) | |
| + 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0); | |
| let minStrength = 0.14 + asymNoise * 0.06; | |
| let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength); | |
| let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker; | |
| let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor; | |
| return max(lightStrength, ambientLight); | |
| } | |
| fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 { | |
| let delta = coord - particlePos; | |
| let distSq = dot(delta, delta); | |
| let sizeSq = size * size; | |
| if (distSq > sizeSq * 9.0) { | |
| return 0.0; | |
| } | |
| let d = sqrt(distSq); | |
| let core = smoothstep(size, size * 0.35, d); | |
| let glow = smoothstep(size * 3.0, 0.0, d) * 0.55; | |
| return core + glow; | |
| } | |
| fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 { | |
| if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) { | |
| return 0.0; | |
| } | |
| var particleSum: f32 = 0.0; | |
| let particleCount = i32(uniforms.particleAmount); | |
| let time = uniforms.iTime * uniforms.particleSpeed; | |
| let perpDir = vec2<f32>(-lightDir.y, lightDir.x); | |
| let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y); | |
| let maxDist = max(baseSize * uniforms.lightLength, 1.0); | |
| let spreadScale = uniforms.lightSpread * baseSize * 0.65; | |
| let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55; | |
| for (var i: i32 = 0; i < particleCount; i = i + 1) { | |
| let fi = f32(i); | |
| let seed = vec2<f32>(fi * 127.1, fi * 311.7); | |
| let rnd = hash2(seed); | |
| let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0; | |
| let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration; | |
| let lifeProgress = fract((time + lifeOffset) / lifeDuration); | |
| let fadeIn = smoothstep(0.0, 0.2, lifeProgress); | |
| let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress); | |
| let lifeFade = fadeIn * fadeOut; | |
| if (lifeFade < 0.01) { | |
| continue; | |
| } | |
| let alongLight = rnd.x * maxDist * 0.8; | |
| let perpOffset = (rnd.y - 0.5) * spreadScale; | |
| let floatPhase = rnd.y * 6.28318 + fi * 0.37; | |
| let floatSpeed = 0.35 + rnd.x * 0.9; | |
| let drift = vec2<f32>( | |
| sin(time * floatSpeed + floatPhase), | |
| cos(time * floatSpeed * 0.85 + floatPhase * 1.3) | |
| ) * uniforms.particleDrift * baseSize * 0.08; | |
| let wobble = vec2<f32>( | |
| sin(time * 1.4 + floatPhase * 2.1), | |
| cos(time * 1.1 + floatPhase * 1.6) | |
| ) * uniforms.particleDrift * baseSize * 0.03; | |
| let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1; | |
| let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble; | |
| let toParticle = basePos - lightSource; | |
| let projLen = dot(toParticle, lightDir); | |
| if (projLen < 0.0 || projLen > maxDist) { | |
| continue; | |
| } | |
| let sideDist = abs(dot(toParticle, perpDir)); | |
| if (sideDist > coneHalfWidth) { | |
| continue; | |
| } | |
| let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x); | |
| let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase); | |
| let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen); | |
| if (distFade < 0.01) { | |
| continue; | |
| } | |
| let p = particle(coord, basePos, size); | |
| if (p > 0.0) { | |
| particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity; | |
| if (particleSum >= 1.0) { | |
| break; | |
| } | |
| } | |
| } | |
| return min(particleSum, 1.0); | |
| } | |
| @fragment | |
| fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> { | |
| let coord = vec2<f32>(fragCoord.x, fragCoord.y); | |
| let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5; | |
| let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x; | |
| let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x); | |
| let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset; | |
| let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord); | |
| if (lightValue < 0.001) { | |
| let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir); | |
| if (particles < 0.001) { | |
| return vec4<f32>(0.0, 0.0, 0.0, 0.0); | |
| } | |
| let particleBrightness = particles * 1.8; | |
| return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9); | |
| } | |
| var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue); | |
| if (uniforms.noiseAmount > 0.01) { | |
| let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5); | |
| let grain = mix(1.0, n, uniforms.noiseAmount * 0.5); | |
| fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a); | |
| } | |
| let brightness = 1.0 - (coord.y / uniforms.iResolution.y); | |
| fragColor = vec4<f32>( | |
| fragColor.x * (0.15 + brightness * 0.85), | |
| fragColor.y * (0.35 + brightness * 0.65), | |
| fragColor.z * (0.55 + brightness * 0.45), | |
| fragColor.a | |
| ); | |
| if (abs(uniforms.saturation - 1.0) > 0.01) { | |
| let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114)); | |
| fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a); | |
| } | |
| fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a); | |
| let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir); | |
| if (particles > 0.001) { | |
| let particleBrightness = particles * 1.8; | |
| fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9)); | |
| } | |
| return fragColor; | |
| } | |
| ` | |
| const UNIFORM_BUFFER_SIZE = 144 | |
| function updateUniformBuffer(buffer: Float32Array, data: UniformData): void { | |
| buffer[0] = data.iTime | |
| buffer[2] = data.iResolution[0] | |
| buffer[3] = data.iResolution[1] | |
| buffer[4] = data.lightPos[0] | |
| buffer[5] = data.lightPos[1] | |
| buffer[6] = data.lightDir[0] | |
| buffer[7] = data.lightDir[1] | |
| buffer[8] = data.color[0] | |
| buffer[9] = data.color[1] | |
| buffer[10] = data.color[2] | |
| buffer[11] = data.speed | |
| buffer[12] = data.lightSpread | |
| buffer[13] = data.lightLength | |
| buffer[14] = data.sourceWidth | |
| buffer[15] = data.pulsating | |
| buffer[16] = data.pulsatingMin | |
| buffer[17] = data.pulsatingMax | |
| buffer[18] = data.fadeDistance | |
| buffer[19] = data.saturation | |
| buffer[20] = data.noiseAmount | |
| buffer[21] = data.distortion | |
| buffer[22] = data.particlesEnabled | |
| buffer[23] = data.particleAmount | |
| buffer[24] = data.particleSizeMin | |
| buffer[25] = data.particleSizeMax | |
| buffer[26] = data.particleSpeed | |
| buffer[27] = data.particleOpacity | |
| buffer[28] = data.particleDrift | |
| } | |
| export default function Spotlight(props: SpotlightProps) { | |
| let containerRef: HTMLDivElement | undefined | |
| let canvasRef: HTMLCanvasElement | null = null | |
| let deviceRef: GPUDevice | null = null | |
| let contextRef: GPUCanvasContext | null = null | |
| let pipelineRef: GPURenderPipeline | null = null | |
| let uniformBufferRef: GPUBuffer | null = null | |
| let bindGroupRef: GPUBindGroup | null = null | |
| let animationIdRef: number | null = null | |
| let cleanupFunctionRef: (() => void) | null = null | |
| let uniformDataRef: UniformData | null = null | |
| let uniformArrayRef: Float32Array | null = null | |
| let configRef: SpotlightConfig = props.config() | |
| let frameCount = 0 | |
| const [isVisible, setIsVisible] = createSignal(false) | |
| createEffect(() => { | |
| configRef = props.config() | |
| }) | |
| onMount(() => { | |
| if (!containerRef) return | |
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| const entry = entries[0] | |
| setIsVisible(entry.isIntersecting) | |
| }, | |
| { threshold: 0.1 }, | |
| ) | |
| observer.observe(containerRef) | |
| onCleanup(() => { | |
| observer.disconnect() | |
| }) | |
| }) | |
| createEffect(() => { | |
| const visible = isVisible() | |
| const config = props.config() | |
| if (!visible || !containerRef) { | |
| return | |
| } | |
| if (cleanupFunctionRef) { | |
| cleanupFunctionRef() | |
| cleanupFunctionRef = null | |
| } | |
| const initializeWebGPU = async () => { | |
| if (!containerRef) { | |
| return | |
| } | |
| await new Promise((resolve) => setTimeout(resolve, 10)) | |
| if (!containerRef) { | |
| return | |
| } | |
| if (!navigator.gpu) { | |
| console.warn("WebGPU is not supported in this browser") | |
| return | |
| } | |
| const adapter = await navigator.gpu.requestAdapter({ | |
| powerPreference: "high-performance", | |
| }) | |
| if (!adapter) { | |
| console.warn("Failed to get WebGPU adapter") | |
| return | |
| } | |
| const device = await adapter.requestDevice() | |
| deviceRef = device | |
| const canvas = document.createElement("canvas") | |
| canvas.style.width = "100%" | |
| canvas.style.height = "100%" | |
| canvasRef = canvas | |
| while (containerRef.firstChild) { | |
| containerRef.removeChild(containerRef.firstChild) | |
| } | |
| containerRef.appendChild(canvas) | |
| const context = canvas.getContext("webgpu") | |
| if (!context) { | |
| console.warn("Failed to get WebGPU context") | |
| return | |
| } | |
| contextRef = context | |
| const presentationFormat = navigator.gpu.getPreferredCanvasFormat() | |
| context.configure({ | |
| device, | |
| format: presentationFormat, | |
| alphaMode: "premultiplied", | |
| }) | |
| const shaderModule = device.createShaderModule({ | |
| code: WGSL_SHADER, | |
| }) | |
| const uniformBuffer = device.createBuffer({ | |
| size: UNIFORM_BUFFER_SIZE, | |
| usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, | |
| }) | |
| uniformBufferRef = uniformBuffer | |
| const bindGroupLayout = device.createBindGroupLayout({ | |
| entries: [ | |
| { | |
| binding: 0, | |
| visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, | |
| buffer: { type: "uniform" }, | |
| }, | |
| ], | |
| }) | |
| const bindGroup = device.createBindGroup({ | |
| layout: bindGroupLayout, | |
| entries: [ | |
| { | |
| binding: 0, | |
| resource: { buffer: uniformBuffer }, | |
| }, | |
| ], | |
| }) | |
| bindGroupRef = bindGroup | |
| const pipelineLayout = device.createPipelineLayout({ | |
| bindGroupLayouts: [bindGroupLayout], | |
| }) | |
| const pipeline = device.createRenderPipeline({ | |
| layout: pipelineLayout, | |
| vertex: { | |
| module: shaderModule, | |
| entryPoint: "vertexMain", | |
| }, | |
| fragment: { | |
| module: shaderModule, | |
| entryPoint: "fragmentMain", | |
| targets: [ | |
| { | |
| format: presentationFormat, | |
| blend: { | |
| color: { | |
| srcFactor: "src-alpha", | |
| dstFactor: "one-minus-src-alpha", | |
| operation: "add", | |
| }, | |
| alpha: { | |
| srcFactor: "one", | |
| dstFactor: "one-minus-src-alpha", | |
| operation: "add", | |
| }, | |
| }, | |
| }, | |
| ], | |
| }, | |
| primitive: { | |
| topology: "triangle-list", | |
| }, | |
| }) | |
| pipelineRef = pipeline | |
| const { clientWidth: wCSS, clientHeight: hCSS } = containerRef | |
| const dpr = Math.min(window.devicePixelRatio, 2) | |
| const w = wCSS * dpr | |
| const h = hCSS * dpr | |
| const { anchor, dir } = getAnchorAndDir(config.placement, w, h) | |
| uniformDataRef = { | |
| iTime: 0, | |
| iResolution: [w, h], | |
| lightPos: anchor, | |
| lightDir: dir, | |
| color: hexToRgb(config.color), | |
| speed: config.speed, | |
| lightSpread: config.spread, | |
| lightLength: config.length, | |
| sourceWidth: config.width, | |
| pulsating: config.pulsating !== false ? 1.0 : 0.0, | |
| pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0, | |
| pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0, | |
| fadeDistance: config.distance, | |
| saturation: config.saturation, | |
| noiseAmount: config.noiseAmount, | |
| distortion: config.distortion, | |
| particlesEnabled: config.particles.enabled ? 1.0 : 0.0, | |
| particleAmount: config.particles.amount, | |
| particleSizeMin: config.particles.size[0], | |
| particleSizeMax: config.particles.size[1], | |
| particleSpeed: config.particles.speed, | |
| particleOpacity: config.particles.opacity, | |
| particleDrift: config.particles.drift, | |
| } | |
| const updatePlacement = () => { | |
| if (!containerRef || !canvasRef || !uniformDataRef) { | |
| return | |
| } | |
| const dpr = Math.min(window.devicePixelRatio, 2) | |
| const { clientWidth: wCSS, clientHeight: hCSS } = containerRef | |
| const w = Math.floor(wCSS * dpr) | |
| const h = Math.floor(hCSS * dpr) | |
| canvasRef.width = w | |
| canvasRef.height = h | |
| uniformDataRef.iResolution = [w, h] | |
| const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h) | |
| uniformDataRef.lightPos = anchor | |
| uniformDataRef.lightDir = dir | |
| } | |
| const loop = (t: number) => { | |
| if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) { | |
| return | |
| } | |
| const timeSeconds = t * 0.001 | |
| uniformDataRef.iTime = timeSeconds | |
| frameCount++ | |
| if (props.onAnimationFrame && frameCount % 2 === 0) { | |
| const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0 | |
| const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0 | |
| const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5 | |
| const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5 | |
| const pulseValue = | |
| configRef.pulsating !== false | |
| ? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0) | |
| : 1.0 | |
| const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5) | |
| const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1) | |
| const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55) | |
| props.onAnimationFrame({ | |
| time: timeSeconds, | |
| intensity, | |
| pulseValue: Math.max(pulseValue, 0.9), | |
| }) | |
| } | |
| try { | |
| if (!uniformArrayRef) { | |
| uniformArrayRef = new Float32Array(36) | |
| } | |
| updateUniformBuffer(uniformArrayRef, uniformDataRef) | |
| deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer) | |
| const commandEncoder = deviceRef.createCommandEncoder() | |
| const textureView = contextRef.getCurrentTexture().createView() | |
| const renderPass = commandEncoder.beginRenderPass({ | |
| colorAttachments: [ | |
| { | |
| view: textureView, | |
| clearValue: { r: 0, g: 0, b: 0, a: 0 }, | |
| loadOp: "clear", | |
| storeOp: "store", | |
| }, | |
| ], | |
| }) | |
| renderPass.setPipeline(pipelineRef) | |
| renderPass.setBindGroup(0, bindGroupRef) | |
| renderPass.draw(3) | |
| renderPass.end() | |
| deviceRef.queue.submit([commandEncoder.finish()]) | |
| animationIdRef = requestAnimationFrame(loop) | |
| } catch (error) { | |
| console.warn("WebGPU rendering error:", error) | |
| return | |
| } | |
| } | |
| window.addEventListener("resize", updatePlacement) | |
| updatePlacement() | |
| animationIdRef = requestAnimationFrame(loop) | |
| cleanupFunctionRef = () => { | |
| if (animationIdRef) { | |
| cancelAnimationFrame(animationIdRef) | |
| animationIdRef = null | |
| } | |
| window.removeEventListener("resize", updatePlacement) | |
| if (uniformBufferRef) { | |
| uniformBufferRef.destroy() | |
| uniformBufferRef = null | |
| } | |
| if (deviceRef) { | |
| deviceRef.destroy() | |
| deviceRef = null | |
| } | |
| if (canvasRef && canvasRef.parentNode) { | |
| canvasRef.parentNode.removeChild(canvasRef) | |
| } | |
| canvasRef = null | |
| contextRef = null | |
| pipelineRef = null | |
| bindGroupRef = null | |
| uniformDataRef = null | |
| } | |
| } | |
| void initializeWebGPU() | |
| onCleanup(() => { | |
| if (cleanupFunctionRef) { | |
| cleanupFunctionRef() | |
| cleanupFunctionRef = null | |
| } | |
| }) | |
| }) | |
| createEffect(() => { | |
| if (!uniformDataRef || !containerRef) { | |
| return | |
| } | |
| const config = props.config() | |
| uniformDataRef.color = hexToRgb(config.color) | |
| uniformDataRef.speed = config.speed | |
| uniformDataRef.lightSpread = config.spread | |
| uniformDataRef.lightLength = config.length | |
| uniformDataRef.sourceWidth = config.width | |
| uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0 | |
| uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0 | |
| uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0 | |
| uniformDataRef.fadeDistance = config.distance | |
| uniformDataRef.saturation = config.saturation | |
| uniformDataRef.noiseAmount = config.noiseAmount | |
| uniformDataRef.distortion = config.distortion | |
| uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0 | |
| uniformDataRef.particleAmount = config.particles.amount | |
| uniformDataRef.particleSizeMin = config.particles.size[0] | |
| uniformDataRef.particleSizeMax = config.particles.size[1] | |
| uniformDataRef.particleSpeed = config.particles.speed | |
| uniformDataRef.particleOpacity = config.particles.opacity | |
| uniformDataRef.particleDrift = config.particles.drift | |
| const dpr = Math.min(window.devicePixelRatio, 2) | |
| const { clientWidth: wCSS, clientHeight: hCSS } = containerRef | |
| const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr) | |
| uniformDataRef.lightPos = anchor | |
| uniformDataRef.lightDir = dir | |
| }) | |
| return ( | |
| <div | |
| ref={containerRef} | |
| class={`spotlight-container ${props.class ?? ""}`.trim()} | |
| style={{ opacity: props.config().opacity }} | |
| /> | |
| ) | |
| } | |
Xet Storage Details
- Size:
- 25.1 kB
- Xet hash:
- 146a49effc1c7628530ec0dd2750bde845958d41923bc610bac50839e4081270
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.