From a05f735bb3b200ac966e091c895fb36e4e47ed49 Mon Sep 17 00:00:00 2001 From: Aaron Iker Date: Fri, 16 Jan 2026 11:30:11 +0100 Subject: [PATCH 1/3] feat: removed unused files --- .../console/app/src/component/light-rays.css | 186 ---- .../console/app/src/component/light-rays.tsx | 924 ------------------ 2 files changed, 1110 deletions(-) delete mode 100644 packages/console/app/src/component/light-rays.css delete mode 100644 packages/console/app/src/component/light-rays.tsx diff --git a/packages/console/app/src/component/light-rays.css b/packages/console/app/src/component/light-rays.css deleted file mode 100644 index b688e6d9e3c..00000000000 --- a/packages/console/app/src/component/light-rays.css +++ /dev/null @@ -1,186 +0,0 @@ -.light-rays-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - overflow: hidden; -} - -.light-rays-container canvas { - display: block; - width: 100%; - height: 100%; -} - -.light-rays-controls { - position: fixed; - top: 16px; - right: 16px; - z-index: 9999; - font-family: var(--font-mono, monospace); - font-size: 12px; - color: #fff; -} - -.light-rays-controls-toggle { - background: rgba(0, 0, 0, 0.8); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - padding: 8px 12px; - color: #fff; - cursor: pointer; - font-family: inherit; - font-size: inherit; - width: 100%; - text-align: left; -} - -.light-rays-controls-toggle:hover { - background: rgba(0, 0, 0, 0.9); - border-color: rgba(255, 255, 255, 0.3); -} - -.light-rays-controls-panel { - background: rgba(0, 0, 0, 0.85); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - padding: 12px; - margin-top: 4px; - display: flex; - flex-direction: column; - gap: 10px; - min-width: 240px; - max-height: calc(100vh - 100px); - overflow-y: auto; - backdrop-filter: blur(8px); -} - -.control-group { - display: flex; - flex-direction: column; - gap: 4px; -} - -.control-group label { - color: rgba(255, 255, 255, 0.7); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.control-group.checkbox { - flex-direction: row; - align-items: center; -} - -.control-group.checkbox label { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - text-transform: none; -} - -.control-group input[type="range"] { - -webkit-appearance: none; - appearance: none; - width: 100%; - height: 4px; - background: rgba(255, 255, 255, 0.2); - border-radius: 2px; - outline: none; -} - -.control-group input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 14px; - height: 14px; - background: #fff; - border-radius: 50%; - cursor: pointer; - transition: transform 0.1s; -} - -.control-group input[type="range"]::-webkit-slider-thumb:hover { - transform: scale(1.1); -} - -.control-group input[type="range"]::-moz-range-thumb { - width: 14px; - height: 14px; - background: #fff; - border-radius: 50%; - cursor: pointer; - border: none; -} - -.control-group input[type="color"] { - -webkit-appearance: none; - appearance: none; - width: 100%; - height: 32px; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - background: transparent; - cursor: pointer; - padding: 2px; -} - -.control-group input[type="color"]::-webkit-color-swatch-wrapper { - padding: 0; -} - -.control-group input[type="color"]::-webkit-color-swatch { - border: none; - border-radius: 2px; -} - -.control-group select { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - padding: 6px 8px; - color: #fff; - font-family: inherit; - font-size: inherit; - cursor: pointer; - outline: none; -} - -.control-group select:hover { - border-color: rgba(255, 255, 255, 0.3); -} - -.control-group select option { - background: #1a1a1a; - color: #fff; -} - -.control-group input[type="checkbox"] { - width: 16px; - height: 16px; - accent-color: #fff; - cursor: pointer; -} - -.reset-button { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - padding: 8px 12px; - color: rgba(255, 255, 255, 0.7); - cursor: pointer; - font-family: inherit; - font-size: inherit; - margin-top: 4px; - transition: all 0.15s; -} - -.reset-button:hover { - background: rgba(255, 255, 255, 0.15); - border-color: rgba(255, 255, 255, 0.3); - color: #fff; -} diff --git a/packages/console/app/src/component/light-rays.tsx b/packages/console/app/src/component/light-rays.tsx deleted file mode 100644 index 53daeb2f34b..00000000000 --- a/packages/console/app/src/component/light-rays.tsx +++ /dev/null @@ -1,924 +0,0 @@ -import { createSignal, createEffect, onMount, onCleanup, Show, For, Accessor, Setter } from "solid-js" -import "./light-rays.css" - -export type RaysOrigin = - | "top-center" - | "top-left" - | "top-right" - | "right" - | "left" - | "bottom-center" - | "bottom-right" - | "bottom-left" - -export interface LightRaysConfig { - raysOrigin: RaysOrigin - raysColor: string - raysSpeed: number - lightSpread: number - rayLength: number - sourceWidth: number - pulsating: boolean - pulsatingMin: number - pulsatingMax: number - fadeDistance: number - saturation: number - followMouse: boolean - mouseInfluence: number - noiseAmount: number - distortion: number - opacity: number -} - -export const defaultConfig: LightRaysConfig = { - raysOrigin: "top-center", - raysColor: "#ffffff", - raysSpeed: 1.0, - lightSpread: 1.2, - rayLength: 4.5, - sourceWidth: 0.1, - pulsating: true, - pulsatingMin: 0.9, - pulsatingMax: 1.05, - fadeDistance: 1.25, - saturation: 0.35, - followMouse: false, - mouseInfluence: 0.05, - noiseAmount: 0.5, - distortion: 0.0, - opacity: 0.35, -} - -export interface LightRaysAnimationState { - time: number - intensity: number - pulseValue: number -} - -interface LightRaysProps { - config: Accessor - class?: string - onAnimationFrame?: (state: LightRaysAnimationState) => 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 = ( - origin: RaysOrigin, - w: number, - h: number, -): { anchor: [number, number]; dir: [number, number] } => { - const outside = 0.2 - switch (origin) { - case "top-left": - return { anchor: [0, -outside * h], dir: [0, 1] } - case "top-right": - return { anchor: [w, -outside * h], dir: [0, 1] } - case "left": - return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] } - case "right": - return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] } - case "bottom-left": - return { anchor: [0, (1 + outside) * h], dir: [0, -1] } - case "bottom-center": - return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] } - case "bottom-right": - return { anchor: [w, (1 + outside) * h], dir: [0, -1] } - default: // "top-center" - return { anchor: [0.5 * w, -outside * h], dir: [0, 1] } - } -} - -interface UniformData { - iTime: number - iResolution: [number, number] - rayPos: [number, number] - rayDir: [number, number] - raysColor: [number, number, number] - raysSpeed: number - lightSpread: number - rayLength: number - sourceWidth: number - pulsating: number - pulsatingMin: number - pulsatingMax: number - fadeDistance: number - saturation: number - mousePos: [number, number] - mouseInfluence: number - noiseAmount: number - distortion: number -} - -const WGSL_SHADER = ` - struct Uniforms { - iTime: f32, - _pad0: f32, - iResolution: vec2, - rayPos: vec2, - rayDir: vec2, - raysColor: vec3, - raysSpeed: f32, - lightSpread: f32, - rayLength: f32, - sourceWidth: f32, - pulsating: f32, - pulsatingMin: f32, - pulsatingMax: f32, - fadeDistance: f32, - saturation: f32, - mousePos: vec2, - mouseInfluence: f32, - noiseAmount: f32, - distortion: f32, - _pad1: f32, - _pad2: f32, - _pad3: f32, - }; - - @group(0) @binding(0) var uniforms: Uniforms; - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) vUv: vec2, - }; - - @vertex - fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 3>( - vec2(-1.0, -1.0), - vec2(3.0, -1.0), - vec2(-1.0, 3.0) - ); - - var output: VertexOutput; - let pos = positions[vertexIndex]; - output.position = vec4(pos, 0.0, 1.0); - output.vUv = pos * 0.5 + 0.5; - return output; - } - - fn noise(st: vec2) -> f32 { - return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453123); - } - - fn rayStrength(raySource: vec2, rayRefDirection: vec2, coord: vec2, - seedA: f32, seedB: f32, speed: f32) -> f32 { - let sourceToCoord = coord - raySource; - let dirNorm = normalize(sourceToCoord); - let cosAngle = dot(dirNorm, rayRefDirection); - - let distortedAngle = cosAngle + uniforms.distortion * sin(uniforms.iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2; - - let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(uniforms.lightSpread, 0.001)); - - let distance = length(sourceToCoord); - let maxDistance = uniforms.iResolution.x * uniforms.rayLength; - let lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0); - - let fadeFalloff = clamp((uniforms.iResolution.x * uniforms.fadeDistance - distance) / (uniforms.iResolution.x * uniforms.fadeDistance), 0.5, 1.0); - let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5; - let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5; - var pulse: f32; - if (uniforms.pulsating > 0.5) { - pulse = pulseCenter + pulseAmplitude * sin(uniforms.iTime * speed * 3.0); - } else { - pulse = 1.0; - } - - let baseStrength = clamp( - (0.45 + 0.15 * sin(distortedAngle * seedA + uniforms.iTime * speed)) + - (0.3 + 0.2 * cos(-distortedAngle * seedB + uniforms.iTime * speed)), - 0.0, 1.0 - ); - - return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse; - } - - @fragment - fn fragmentMain(@builtin(position) fragCoord: vec4, @location(0) vUv: vec2) -> @location(0) vec4 { - let coord = vec2(fragCoord.x, fragCoord.y); - - let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5; - let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x; - - let perpDir = vec2(-uniforms.rayDir.y, uniforms.rayDir.x); - let adjustedRayPos = uniforms.rayPos + perpDir * widthOffset; - - var finalRayDir = uniforms.rayDir; - if (uniforms.mouseInfluence > 0.0) { - let mouseScreenPos = uniforms.mousePos * uniforms.iResolution; - let mouseDirection = normalize(mouseScreenPos - adjustedRayPos); - finalRayDir = normalize(mix(uniforms.rayDir, mouseDirection, uniforms.mouseInfluence)); - } - - let rays1 = vec4(1.0) * - rayStrength(adjustedRayPos, finalRayDir, coord, 36.2214, 21.11349, - 1.5 * uniforms.raysSpeed); - let rays2 = vec4(1.0) * - rayStrength(adjustedRayPos, finalRayDir, coord, 22.3991, 18.0234, - 1.1 * uniforms.raysSpeed); - - var fragColor = rays1 * 0.5 + rays2 * 0.4; - - if (uniforms.noiseAmount > 0.0) { - let n = noise(coord * 0.01 + uniforms.iTime * 0.1); - fragColor = vec4(fragColor.rgb * (1.0 - uniforms.noiseAmount + uniforms.noiseAmount * n), fragColor.a); - } - - let brightness = 1.0 - (coord.y / uniforms.iResolution.y); - fragColor.x = fragColor.x * (0.1 + brightness * 0.8); - fragColor.y = fragColor.y * (0.3 + brightness * 0.6); - fragColor.z = fragColor.z * (0.5 + brightness * 0.5); - - if (uniforms.saturation != 1.0) { - let gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114)); - fragColor = vec4(mix(vec3(gray), fragColor.rgb, uniforms.saturation), fragColor.a); - } - - fragColor = vec4(fragColor.rgb * uniforms.raysColor, fragColor.a); - - return fragColor; - } -` - -const UNIFORM_BUFFER_SIZE = 96 - -function createUniformBuffer(data: UniformData): Float32Array { - const buffer = new Float32Array(24) - buffer[0] = data.iTime - buffer[1] = 0 - buffer[2] = data.iResolution[0] - buffer[3] = data.iResolution[1] - buffer[4] = data.rayPos[0] - buffer[5] = data.rayPos[1] - buffer[6] = data.rayDir[0] - buffer[7] = data.rayDir[1] - buffer[8] = data.raysColor[0] - buffer[9] = data.raysColor[1] - buffer[10] = data.raysColor[2] - buffer[11] = data.raysSpeed - buffer[12] = data.lightSpread - buffer[13] = data.rayLength - 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.mousePos[0] - buffer[21] = data.mousePos[1] - buffer[22] = data.mouseInfluence - buffer[23] = data.noiseAmount - return buffer -} - -const UNIFORM_BUFFER_SIZE_CORRECTED = 112 - -function createUniformBufferCorrected(data: UniformData): Float32Array { - const buffer = new Float32Array(28) - buffer[0] = data.iTime - buffer[1] = 0 - buffer[2] = data.iResolution[0] - buffer[3] = data.iResolution[1] - buffer[4] = data.rayPos[0] - buffer[5] = data.rayPos[1] - buffer[6] = data.rayDir[0] - buffer[7] = data.rayDir[1] - buffer[8] = data.raysColor[0] - buffer[9] = data.raysColor[1] - buffer[10] = data.raysColor[2] - buffer[11] = data.raysSpeed - buffer[12] = data.lightSpread - buffer[13] = data.rayLength - 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.mousePos[0] - buffer[21] = data.mousePos[1] - buffer[22] = data.mouseInfluence - buffer[23] = data.noiseAmount - buffer[24] = data.distortion - buffer[25] = 0 - buffer[26] = 0 - buffer[27] = 0 - return buffer -} - -export default function LightRays(props: LightRaysProps) { - 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 - - const mouseRef = { x: 0.5, y: 0.5 } - const smoothMouseRef = { x: 0.5, y: 0.5 } - - const [isVisible, setIsVisible] = createSignal(false) - - 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() - 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_CORRECTED, - 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.raysOrigin, w, h) - - uniformDataRef = { - iTime: 0, - iResolution: [w, h], - rayPos: anchor, - rayDir: dir, - raysColor: hexToRgb(config.raysColor), - raysSpeed: config.raysSpeed, - lightSpread: config.lightSpread, - rayLength: config.rayLength, - sourceWidth: config.sourceWidth, - pulsating: config.pulsating ? 1.0 : 0.0, - pulsatingMin: config.pulsatingMin, - pulsatingMax: config.pulsatingMax, - fadeDistance: config.fadeDistance, - saturation: config.saturation, - mousePos: [0.5, 0.5], - mouseInfluence: config.mouseInfluence, - noiseAmount: config.noiseAmount, - distortion: config.distortion, - } - - 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 currentConfig = props.config() - const { anchor, dir } = getAnchorAndDir(currentConfig.raysOrigin, w, h) - uniformDataRef.rayPos = anchor - uniformDataRef.rayDir = dir - } - - const loop = (t: number) => { - if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) { - return - } - - const currentConfig = props.config() - const timeSeconds = t * 0.001 - uniformDataRef.iTime = timeSeconds - - if (currentConfig.followMouse && currentConfig.mouseInfluence > 0.0) { - const smoothing = 0.92 - - smoothMouseRef.x = smoothMouseRef.x * smoothing + mouseRef.x * (1 - smoothing) - smoothMouseRef.y = smoothMouseRef.y * smoothing + mouseRef.y * (1 - smoothing) - - uniformDataRef.mousePos = [smoothMouseRef.x, smoothMouseRef.y] - } - - if (props.onAnimationFrame) { - const pulseCenter = (currentConfig.pulsatingMin + currentConfig.pulsatingMax) * 0.5 - const pulseAmplitude = (currentConfig.pulsatingMax - currentConfig.pulsatingMin) * 0.5 - const pulseValue = currentConfig.pulsating - ? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * currentConfig.raysSpeed * 3.0) - : 1.0 - - const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * currentConfig.raysSpeed * 1.5) - const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * currentConfig.raysSpeed * 1.1) - const intensity = (baseIntensity1 + baseIntensity2) * pulseValue - - props.onAnimationFrame({ - time: timeSeconds, - intensity, - pulseValue, - }) - } - - try { - const uniformData = createUniformBufferCorrected(uniformDataRef) - deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformData.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 - } - } - - initializeWebGPU() - - onCleanup(() => { - if (cleanupFunctionRef) { - cleanupFunctionRef() - cleanupFunctionRef = null - } - }) - }) - - createEffect(() => { - if (!uniformDataRef || !containerRef) { - return - } - - const config = props.config() - - uniformDataRef.raysColor = hexToRgb(config.raysColor) - uniformDataRef.raysSpeed = config.raysSpeed - uniformDataRef.lightSpread = config.lightSpread - uniformDataRef.rayLength = config.rayLength - uniformDataRef.sourceWidth = config.sourceWidth - uniformDataRef.pulsating = config.pulsating ? 1.0 : 0.0 - uniformDataRef.pulsatingMin = config.pulsatingMin - uniformDataRef.pulsatingMax = config.pulsatingMax - uniformDataRef.fadeDistance = config.fadeDistance - uniformDataRef.saturation = config.saturation - uniformDataRef.mouseInfluence = config.mouseInfluence - uniformDataRef.noiseAmount = config.noiseAmount - uniformDataRef.distortion = config.distortion - - const dpr = Math.min(window.devicePixelRatio, 2) - const { clientWidth: wCSS, clientHeight: hCSS } = containerRef - const { anchor, dir } = getAnchorAndDir(config.raysOrigin, wCSS * dpr, hCSS * dpr) - uniformDataRef.rayPos = anchor - uniformDataRef.rayDir = dir - }) - - createEffect(() => { - const config = props.config() - if (!config.followMouse) { - return - } - - const handleMouseMove = (e: MouseEvent) => { - if (!containerRef) { - return - } - const rect = containerRef.getBoundingClientRect() - const x = (e.clientX - rect.left) / rect.width - const y = (e.clientY - rect.top) / rect.height - mouseRef.x = x - mouseRef.y = y - } - - window.addEventListener("mousemove", handleMouseMove) - - onCleanup(() => { - window.removeEventListener("mousemove", handleMouseMove) - }) - }) - - return ( -
- ) -} - -interface LightRaysControlsProps { - config: Accessor - setConfig: Setter -} - -export function LightRaysControls(props: LightRaysControlsProps) { - const [isOpen, setIsOpen] = createSignal(true) - - const updateConfig = (key: K, value: LightRaysConfig[K]) => { - props.setConfig((prev) => ({ ...prev, [key]: value })) - } - - const origins: RaysOrigin[] = [ - "top-center", - "top-left", - "top-right", - "left", - "right", - "bottom-center", - "bottom-left", - "bottom-right", - ] - - return ( -
- - -
-
- - -
- -
- - updateConfig("raysColor", e.currentTarget.value)} - /> -
- -
- - updateConfig("raysSpeed", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("lightSpread", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("rayLength", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("sourceWidth", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("fadeDistance", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("saturation", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("mouseInfluence", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("noiseAmount", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("distortion", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("opacity", parseFloat(e.currentTarget.value))} - /> -
- -
- -
- - -
- - updateConfig("pulsatingMin", parseFloat(e.currentTarget.value))} - /> -
- -
- - updateConfig("pulsatingMax", parseFloat(e.currentTarget.value))} - /> -
-
- -
- -
- - -
-
-
- ) -} From 16b5a88a04d1c954650bfd66e53d44b4dd54c713 Mon Sep 17 00:00:00 2001 From: Aaron Iker Date: Fri, 16 Jan 2026 12:11:28 +0100 Subject: [PATCH 2/3] feat: spotlight updates, performance, particles --- .../console/app/src/component/spotlight.css | 15 + .../console/app/src/component/spotlight.tsx | 821 ++++++++++++++++++ packages/console/app/src/routes/black.tsx | 21 +- 3 files changed, 850 insertions(+), 7 deletions(-) create mode 100644 packages/console/app/src/component/spotlight.css create mode 100644 packages/console/app/src/component/spotlight.tsx diff --git a/packages/console/app/src/component/spotlight.css b/packages/console/app/src/component/spotlight.css new file mode 100644 index 00000000000..4b311c3d021 --- /dev/null +++ b/packages/console/app/src/component/spotlight.css @@ -0,0 +1,15 @@ +.spotlight-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 50dvh; + pointer-events: none; + overflow: hidden; +} + +.spotlight-container canvas { + display: block; + width: 100%; + height: 100%; +} diff --git a/packages/console/app/src/component/spotlight.tsx b/packages/console/app/src/component/spotlight.tsx new file mode 100644 index 00000000000..99847c72fa2 --- /dev/null +++ b/packages/console/app/src/component/spotlight.tsx @@ -0,0 +1,821 @@ +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 + 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, + lightPos: vec2, + lightDir: vec2, + color: vec3, + 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 uniforms: Uniforms; + + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) vUv: vec2, + }; + + @vertex + fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { + var positions = array, 3>( + vec2(-1.0, -1.0), + vec2(3.0, -1.0), + vec2(-1.0, 3.0) + ); + + var output: VertexOutput; + let pos = positions[vertexIndex]; + output.position = vec4(pos, 0.0, 1.0); + output.vUv = pos * 0.5 + 0.5; + return output; + } + + fn hash(p: vec2) -> 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) -> vec2 { + let n = sin(dot(p, vec2(41.0, 289.0))); + return fract(vec2(n * 262144.0, n * 32768.0)); + } + + fn fastNoise(st: vec2) -> f32 { + return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453); + } + + fn lightStrengthCombined(lightSource: vec2, lightRefDirection: vec2, coord: vec2) -> 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(-lightRefDirection.y, lightRefDirection.x)); + let time = uniforms.iTime; + let speed = uniforms.speed; + + let asymNoise = fastNoise(vec2(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(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, particlePos: vec2, 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, lightSource: vec2, lightDir: vec2) -> 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(-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(fi * 127.1, fi * 311.7); + let rnd = hash2(seed); + + let lifeDuration = 2.0 + hash(seed + vec2(19.0, 73.0)) * 3.0; + let lifeOffset = hash(seed + vec2(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( + sin(time * floatSpeed + floatPhase), + cos(time * floatSpeed * 0.85 + floatPhase * 1.3) + ) * uniforms.particleDrift * baseSize * 0.08; + + let wobble = vec2( + 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, @location(0) vUv: vec2) -> @location(0) vec4 { + let coord = vec2(fragCoord.x, fragCoord.y); + + let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5; + let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x; + + let perpDir = vec2(-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(0.0, 0.0, 0.0, 0.0); + } + let particleBrightness = particles * 1.8; + return vec4(uniforms.color * particleBrightness, particles * 0.9); + } + + var fragColor = vec4(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(fragColor.rgb * grain, fragColor.a); + } + + let brightness = 1.0 - (coord.y / uniforms.iResolution.y); + fragColor = vec4( + 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(0.299, 0.587, 0.114)); + fragColor = vec4(mix(vec3(gray), fragColor.rgb, uniforms.saturation), fragColor.a); + } + + fragColor = vec4(fragColor.rgb * uniforms.color, fragColor.a); + + let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir); + if (particles > 0.001) { + let particleBrightness = particles * 1.8; + fragColor = vec4(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9)); + } + + return fragColor; + } +` + +const UNIFORM_BUFFER_SIZE = 144 + +function createUniformBuffer(data: UniformData): Float32Array { + const buffer = new Float32Array(36) + buffer[0] = data.iTime + buffer[1] = 0 + 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 + buffer[29] = 0 + buffer[30] = 0 + buffer[31] = 0 + buffer[32] = 0 + return buffer +} + +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 configRef: SpotlightConfig = props.config() + + 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 + + if (props.onAnimationFrame) { + 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 = (baseIntensity1 + baseIntensity2) * pulseValue + + props.onAnimationFrame({ + time: timeSeconds, + intensity, + pulseValue, + }) + } + + try { + const uniformData = createUniformBuffer(uniformDataRef) + deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformData.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 + } + } + + 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 ( +
+ ) +} diff --git a/packages/console/app/src/routes/black.tsx b/packages/console/app/src/routes/black.tsx index 36c9d1eaf08..a8797c562c4 100644 --- a/packages/console/app/src/routes/black.tsx +++ b/packages/console/app/src/routes/black.tsx @@ -3,7 +3,7 @@ import { Title, Meta, Link } from "@solidjs/meta" import { createMemo, createSignal } from "solid-js" import { github } from "~/lib/github" import { config } from "~/config" -import LightRays, { defaultConfig, type LightRaysConfig, type LightRaysAnimationState } from "~/component/light-rays" +import Spotlight, { defaultConfig, type SpotlightAnimationState } from "~/component/spotlight" import "./black.css" export default function BlackLayout(props: RouteSectionProps) { @@ -17,15 +17,14 @@ export default function BlackLayout(props: RouteSectionProps) { : config.github.starsFormatted.compact, ) - const [lightRaysConfig, setLightRaysConfig] = createSignal(defaultConfig) - const [rayAnimationState, setRayAnimationState] = createSignal({ + const [spotlightAnimationState, setSpotlightAnimationState] = createSignal({ time: 0, intensity: 0.5, pulseValue: 1, }) const svgLightingValues = createMemo(() => { - const state = rayAnimationState() + const state = spotlightAnimationState() const t = state.time const wave1 = Math.sin(t * 1.5) * 0.5 + 0.5 @@ -56,10 +55,14 @@ export default function BlackLayout(props: RouteSectionProps) { } as Record }) - const handleAnimationFrame = (state: LightRaysAnimationState) => { - setRayAnimationState(state) + const handleAnimationFrame = (state: SpotlightAnimationState) => { + setSpotlightAnimationState(state) } + const spotlightConfig = createMemo(() => ({ + ...defaultConfig, + })) + return (
OpenCode Black | Access all the world's best coding models @@ -84,7 +87,11 @@ export default function BlackLayout(props: RouteSectionProps) { /> - +
From 13a47956f956d3fbea157ab75899ba8f0d14951a Mon Sep 17 00:00:00 2001 From: Aaron Iker Date: Fri, 16 Jan 2026 12:57:56 +0100 Subject: [PATCH 3/3] feat: performance improvements --- .../console/app/src/component/spotlight.tsx | 25 +++++++++---------- packages/console/app/src/routes/black.tsx | 18 +++++-------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/console/app/src/component/spotlight.tsx b/packages/console/app/src/component/spotlight.tsx index 99847c72fa2..70430699055 100644 --- a/packages/console/app/src/component/spotlight.tsx +++ b/packages/console/app/src/component/spotlight.tsx @@ -413,10 +413,8 @@ const WGSL_SHADER = ` const UNIFORM_BUFFER_SIZE = 144 -function createUniformBuffer(data: UniformData): Float32Array { - const buffer = new Float32Array(36) +function updateUniformBuffer(buffer: Float32Array, data: UniformData): void { buffer[0] = data.iTime - buffer[1] = 0 buffer[2] = data.iResolution[0] buffer[3] = data.iResolution[1] buffer[4] = data.lightPos[0] @@ -444,11 +442,6 @@ function createUniformBuffer(data: UniformData): Float32Array { buffer[26] = data.particleSpeed buffer[27] = data.particleOpacity buffer[28] = data.particleDrift - buffer[29] = 0 - buffer[30] = 0 - buffer[31] = 0 - buffer[32] = 0 - return buffer } export default function Spotlight(props: SpotlightProps) { @@ -462,7 +455,9 @@ export default function Spotlight(props: SpotlightProps) { 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) @@ -678,8 +673,9 @@ export default function Spotlight(props: SpotlightProps) { const timeSeconds = t * 0.001 uniformDataRef.iTime = timeSeconds + frameCount++ - if (props.onAnimationFrame) { + 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 @@ -691,18 +687,21 @@ export default function Spotlight(props: SpotlightProps) { 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 = (baseIntensity1 + baseIntensity2) * pulseValue + const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55) props.onAnimationFrame({ time: timeSeconds, intensity, - pulseValue, + pulseValue: Math.max(pulseValue, 0.9), }) } try { - const uniformData = createUniformBuffer(uniformDataRef) - deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformData.buffer) + if (!uniformArrayRef) { + uniformArrayRef = new Float32Array(36) + } + updateUniformBuffer(uniformArrayRef, uniformDataRef) + deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer) const commandEncoder = deviceRef.createCommandEncoder() diff --git a/packages/console/app/src/routes/black.tsx b/packages/console/app/src/routes/black.tsx index a8797c562c4..b991a60a77c 100644 --- a/packages/console/app/src/routes/black.tsx +++ b/packages/console/app/src/routes/black.tsx @@ -32,11 +32,11 @@ export default function BlackLayout(props: RouteSectionProps) { const wave3 = Math.sin(t * 0.8 + 2.5) * 0.5 + 0.5 const shimmerPos = Math.sin(t * 0.7) * 0.5 + 0.5 - const glowIntensity = state.intensity * state.pulseValue * 0.35 - const fillOpacity = 0.1 + wave1 * 0.08 * state.pulseValue - const strokeBrightness = 55 + wave2 * 25 * state.pulseValue + const glowIntensity = Math.max(state.intensity * state.pulseValue * 0.35, 0.15) + const fillOpacity = Math.max(0.1 + wave1 * 0.08 * state.pulseValue, 0.12) + const strokeBrightness = Math.max(55 + wave2 * 25 * state.pulseValue, 60) - const shimmerIntensity = wave3 * 0.15 * state.pulseValue + const shimmerIntensity = Math.max(wave3 * 0.15 * state.pulseValue, 0.08) return { glowIntensity, @@ -59,9 +59,7 @@ export default function BlackLayout(props: RouteSectionProps) { setSpotlightAnimationState(state) } - const spotlightConfig = createMemo(() => ({ - ...defaultConfig, - })) + const spotlightConfig = () => defaultConfig return (