From fe58c649cb83c73c3f2c3094682189ffa78ccc4f Mon Sep 17 00:00:00 2001 From: Aaron Iker Date: Thu, 15 Jan 2026 21:31:50 +0100 Subject: feat(console): Update /black plan selection, light rays effect. mobile styles (#8731) Co-authored-by: Github Action --- packages/console/app/src/component/light-rays.css | 186 +++++ packages/console/app/src/component/light-rays.tsx | 924 ++++++++++++++++++++++ 2 files changed, 1110 insertions(+) create mode 100644 packages/console/app/src/component/light-rays.css create mode 100644 packages/console/app/src/component/light-rays.tsx (limited to 'packages/console/app/src/component') diff --git a/packages/console/app/src/component/light-rays.css b/packages/console/app/src/component/light-rays.css new file mode 100644 index 000000000..b688e6d9e --- /dev/null +++ b/packages/console/app/src/component/light-rays.css @@ -0,0 +1,186 @@ +.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 new file mode 100644 index 000000000..36b47a477 --- /dev/null +++ b/packages/console/app/src/component/light-rays.tsx @@ -0,0 +1,924 @@ +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.15, + rayLength: 4.0, + sourceWidth: 0.1, + pulsating: true, + pulsatingMin: 0.9, + pulsatingMax: 1.0, + fadeDistance: 1.15, + saturation: 0.325, + 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))} + /> +
+
+ +
+ +
+ + +
+
+
+ ) +} -- cgit v1.2.3