diff --git a/bun.lock b/bun.lock index e11ac0d8f8c..9fa7717dcaa 100644 --- a/bun.lock +++ b/bun.lock @@ -95,6 +95,7 @@ }, "devDependencies": { "@typescript/native-preview": "catalog:", + "@webgpu/types": "0.1.54", "typescript": "catalog:", "wrangler": "4.50.0", }, @@ -1903,7 +1904,7 @@ "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], - "@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="], + "@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="], "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], @@ -4291,6 +4292,8 @@ "body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="], + "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], diff --git a/nix/hashes.json b/nix/hashes.json index c89b60ef97a..e79d652cc45 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-4ndHIlS9t1ynRdFszJ1nvcu3YhunhuOc7jcuHI1FbnM=", + "x86_64-linux": "sha256-Fl1BdjNSg19LJVSgDMiBX8JuTaGlL2I5T+rqLfjSeO4=", "aarch64-darwin": "sha256-C0E9KAEj3GI83HwirIL2zlXYIe92T+7Iv6F51BB6slY=" } } diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 81e78fb3a2b..a09fd104afd 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@typescript/native-preview": "catalog:", + "@webgpu/types": "0.1.54", "typescript": "catalog:", "wrangler": "4.50.0" }, 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 00000000000..b688e6d9e3c --- /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 00000000000..36b47a47775 --- /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))} + /> +
+
+ +
+ +
+ + +
+
+
+ ) +} diff --git a/packages/console/app/src/lib/github.ts b/packages/console/app/src/lib/github.ts index cc266f58c4d..ccde5972d37 100644 --- a/packages/console/app/src/lib/github.ts +++ b/packages/console/app/src/lib/github.ts @@ -14,13 +14,14 @@ export const github = query(async () => { fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()), fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }), ]) + if (!Array.isArray(releases) || releases.length === 0) { + return undefined + } const [release] = releases - const contributorCount = Number.parseInt( - contributors.headers - .get("Link")! - .match(/&page=(\d+)>; rel="last"/)! - .at(1)!, - ) + const linkHeader = contributors.headers.get("Link") + const contributorCount = linkHeader + ? Number.parseInt(linkHeader.match(/&page=(\d+)>; rel="last"/)?.at(1) ?? "0") + : 0 return { stars: meta.stargazers_count, release: { diff --git a/packages/console/app/src/routes/black.css b/packages/console/app/src/routes/black.css index 99353f27332..a0cd5712b33 100644 --- a/packages/console/app/src/routes/black.css +++ b/packages/console/app/src/routes/black.css @@ -1,3 +1,135 @@ +::view-transition-group(*) { + animation-duration: 250ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +::view-transition-old(root), +::view-transition-new(root) { + animation-duration: 250ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +::view-transition-image-pair(root) { + isolation: isolate; +} + +::view-transition-old(root) { + animation: none; + mix-blend-mode: normal; +} + +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes reveal-terms { + from { + mask-position: 0% 200%; + } + to { + mask-position: 0% 50%; + } +} + +@keyframes hide-terms { + from { + mask-position: 0% 50%; + } + to { + mask-position: 0% 200%; + } +} + +::view-transition-old(terms-20), +::view-transition-old(terms-100), +::view-transition-old(terms-200) { + mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent); + mask-repeat: no-repeat; + mask-size: 100% 200%; + animation: hide-terms 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards; +} + +::view-transition-new(terms-20), +::view-transition-new(terms-100), +::view-transition-new(terms-200) { + mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent); + mask-repeat: no-repeat; + mask-position: 0% 200%; + mask-size: 100% 200%; + animation: reveal-terms 300ms cubic-bezier(0.25, 0, 0.5, 1) 50ms forwards; +} + +::view-transition-old(action-20), +::view-transition-old(action-100), +::view-transition-old(action-200) { + animation: fade-out 100ms cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +::view-transition-new(action-20), +::view-transition-new(action-100), +::view-transition-new(action-200) { + animation: fade-in-up 200ms cubic-bezier(0.16, 1, 0.3, 1) 250ms forwards; + opacity: 0; +} + +::view-transition-group(plan-card-20), +::view-transition-group(plan-card-100), +::view-transition-group(plan-card-200) { + animation-duration: 200ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +::view-transition-image-pair(plan-card-20), +::view-transition-image-pair(plan-card-100), +::view-transition-image-pair(plan-card-200) { + isolation: isolate; +} + +::view-transition-old(plan-card-20), +::view-transition-old(plan-card-100), +::view-transition-old(plan-card-200) { + animation: fade-out 120ms cubic-bezier(0.4, 0, 0.2, 1) forwards; + mix-blend-mode: normal; +} + +::view-transition-new(plan-card-20), +::view-transition-new(plan-card-100), +::view-transition-new(plan-card-200) { + animation: fade-in 150ms cubic-bezier(0.4, 0, 0.2, 1) 50ms forwards; + opacity: 0; + mix-blend-mode: normal; +} + [data-page="black"] { background: #000; min-height: 100vh; @@ -8,13 +140,18 @@ font-family: var(--font-mono); color: #fff; - [data-component="header-gradient"] { + [data-component="header-logo"] { + filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.25)) drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1)); + position: relative; + z-index: 1; + } + + .header-light-rays { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 288px; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0) 100%); + inset: 0 0 auto 0; + height: 30dvh; + pointer-events: none; + z-index: 0; } [data-component="header"] { @@ -48,27 +185,35 @@ h1 { color: rgba(255, 255, 255, 0.92); - font-size: 18px; + font-size: 16px; font-style: normal; font-weight: 400; - line-height: 160%; + line-height: 1.45; margin: 0; @media (min-width: 768px) { - font-size: 22px; + font-size: 20px; + } + + @media (max-width: 480px) { + font-size: 14px; } } p { color: rgba(255, 255, 255, 0.59); - font-size: 18px; + font-size: 16px; font-style: normal; font-weight: 400; - line-height: 160%; + line-height: 1.45; margin: 0; @media (min-width: 768px) { - font-size: 22px; + font-size: 20px; + } + + @media (max-width: 480px) { + font-size: 14px; } } } @@ -76,30 +221,36 @@ [data-slot="hero-black"] { margin-top: 40px; padding: 0 20px; + position: relative; @media (min-width: 768px) { margin-top: 60px; } svg { - --hero-black-fill-from: hsl(0 0% 100%); - --hero-black-fill-to: hsl(0 0% 100% / 0%); - --hero-black-stroke-from: hsl(0 0% 100% / 60%); - --hero-black-stroke-to: hsl(0 0% 100% / 0%); - width: 100%; max-width: 590px; height: auto; - filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1)); + overflow: visible; + filter: drop-shadow(0 0 20px rgba(255, 255, 255, calc(0.1 + var(--hero-black-glow-intensity, 0) * 0.15))) + drop-shadow(0 -5px 30px rgba(255, 255, 255, calc(var(--hero-black-glow-intensity, 0) * 0.2))); mask-image: linear-gradient(to bottom, black, transparent); stroke-width: 1.5; - [data-slot="black-fill"] { + [data-slot="black-base"] { fill: url(#hero-black-fill-gradient); + stroke: url(#hero-black-stroke-gradient); } - [data-slot="black-stroke"] { - fill: url(#hero-black-stroke-gradient); + [data-slot="black-glow"] { + fill: url(#hero-black-top-glow); + pointer-events: none; + } + + [data-slot="black-shimmer"] { + fill: url(#hero-black-shimmer-gradient); + pointer-events: none; + mix-blend-mode: overlay; } } } @@ -107,14 +258,14 @@ [data-slot="cta"] { display: flex; flex-direction: column; - gap: 32px; + gap: 16px; align-items: center; text-align: center; - margin-top: -32px; + margin-top: -40px; width: 100%; @media (min-width: 768px) { - margin-top: -16px; + margin-top: -20px; } [data-slot="heading"] { @@ -129,7 +280,6 @@ display: inline-block; } } - [data-slot="subheading"] { color: rgba(255, 255, 255, 0.59); font-size: 15px; @@ -142,7 +292,6 @@ line-height: 160%; } } - [data-slot="button"] { display: inline-flex; height: 40px; @@ -154,7 +303,7 @@ background: rgba(255, 255, 255, 0.92); text-decoration: none; color: #000; - font-family: var(--font-mono); + font-family: "JetBrains Mono Nerd Font"; font-size: 16px; font-style: normal; font-weight: 500; @@ -168,16 +317,14 @@ transform: scale(0.98); } } - [data-slot="back-soon"] { color: rgba(255, 255, 255, 0.59); text-align: center; font-size: 13px; font-style: normal; font-weight: 400; - line-height: 160%; + line-height: 160%; /* 20.8px */ } - [data-slot="follow-us"] { display: inline-flex; height: 40px; @@ -201,98 +348,99 @@ flex-direction: column; gap: 16px; width: 100%; - max-width: 680px; + max-width: 660px; padding: 0 20px; - box-sizing: border-box; + + @media (min-width: 768px) { + padding: 0; + } } [data-slot="pricing-card"] { display: flex; flex-direction: column; - align-items: flex-start; + gap: 12px; + padding: 24px; border: 1px solid rgba(255, 255, 255, 0.17); - border-radius: 5px; + background-color: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); + background-clip: padding-box; + border-radius: 4px; text-decoration: none; - background: #000; + transition: border-color 0.15s ease; + cursor: pointer; text-align: left; - overflow: hidden; - width: 100%; - transition: border-color 200ms ease; - &:hover:not([data-selected="true"]) { - border-color: rgba(255, 255, 255, 0.35); + @media (max-width: 480px) { + padding: 16px; } - [data-slot="card-trigger"] { - display: flex; - flex-direction: column; - align-items: flex-start; - width: 100%; - padding: 24px; - background: transparent; - border: none; - cursor: pointer; - font-family: inherit; - text-align: left; - transition: padding 200ms ease; - - &:disabled { - cursor: default; - } + &:hover:not(:active) { + border-color: rgba(255, 255, 255, 0.35); } - &[data-selected="true"] { - [data-slot="amount"] { - font-size: 22px; - } + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } - [data-slot="terms"] { - animation: reveal 500ms cubic-bezier(0.25, 0, 0.5, 1) forwards; - } + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } - [data-slot="actions"] { - [data-slot="continue"] { - animation-delay: 200ms; - } - } + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; } - &[data-collapsed="true"] { - [data-slot="card-trigger"] { - padding: 20px 24px; - } + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } - [data-slot="plan-header"] { - flex-direction: row; - } + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; - [data-slot="amount"] { - font-size: 20px; + &::before { + content: "·"; + margin-right: 8px; } } + } - &[data-selected="false"][data-collapsed="false"] { - [data-slot="amount"] { - font-size: 22px; - } + [data-slot="selected-plan"] { + display: flex; + flex-direction: column; + gap: 32px; + width: 100%; + max-width: 660px; + margin: 0 auto; + position: relative; + background-color: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); + z-index: 1; - [data-slot="period"], - [data-slot="multiplier"] { - font-size: 14px; - } + @media (max-width: 480px) { + margin: 0 20px; + width: calc(100% - 40px); } + } - [data-slot="plan-header"] { - display: flex; - flex-direction: column; - width: 100%; - gap: 12px; - transition: gap 200ms ease; - } + [data-slot="selected-card"] { + display: flex; + flex-direction: column; + gap: 12px; + padding: 24px; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + width: 100%; - [data-slot="plan-icon"] { + [data-slot="icon"] { color: rgba(255, 255, 255, 0.59); - flex-shrink: 0; } [data-slot="price"] { @@ -300,31 +448,22 @@ flex-wrap: wrap; align-items: baseline; gap: 8px; - line-height: 24px; - margin: 0; } [data-slot="amount"] { color: rgba(255, 255, 255, 0.92); + font-size: 24px; font-weight: 500; } - [data-slot="content"] { - width: 100%; - } - - [data-slot="period"], - [data-slot="multiplier"] { - color: rgba(255, 255, 255, 0.59); - } - - [data-slot="billing"] { + [data-slot="period"] { color: rgba(255, 255, 255, 0.59); font-size: 14px; } [data-slot="multiplier"] { color: rgba(255, 255, 255, 0.39); + font-size: 14px; &::before { content: "·"; @@ -334,32 +473,30 @@ [data-slot="terms"] { list-style: none; - padding: 0 24px 24px 24px; + padding: 0; margin: 0; display: flex; flex-direction: column; - gap: 12px; + gap: 8px; text-align: left; - width: 100%; - opacity: 0; - mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%); - mask-repeat: no-repeat; - mask-size: 100% 200%; - mask-position: 0% 320%; - } - [data-slot="terms"] li { - color: rgba(255, 255, 255, 0.59); - font-size: 13px; - line-height: 1.2; - padding-left: 16px; - position: relative; + li { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + line-height: 1.5; + padding-left: 16px; + position: relative; + + &::before { + content: "▪"; + position: absolute; + left: 0; + color: rgba(255, 255, 255, 0.39); + } - &::before { - content: "▪"; - position: absolute; - left: 0; - color: rgba(255, 255, 255, 0.39); + @media (max-width: 768px) { + font-size: 12px; + } } } @@ -367,48 +504,45 @@ display: flex; gap: 16px; margin-top: 8px; - padding: 0 24px 24px 24px; - box-sizing: border-box; - width: 100%; - } - [data-slot="actions"] button, - [data-slot="actions"] a { - flex: 1; - display: inline-flex; - height: 48px; - padding: 0 16px; - justify-content: center; - align-items: center; - border-radius: 4px; - font-family: var(--font-mono); - font-size: 16px; - font-weight: 400; - text-decoration: none; - cursor: pointer; - transition-property: background-color, border-color; - transition-duration: 200ms; - transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1); - } - - [data-slot="cancel"] { - border: 1px solid var(--border-base, rgba(255, 255, 255, 0.17)); - background: var(--surface-raised-base, rgba(255, 255, 255, 0.06)); - background-clip: border-box; - color: rgba(255, 255, 255, 0.92); + button, + a { + flex: 1; + display: inline-flex; + height: 48px; + padding: 0 16px; + justify-content: center; + align-items: center; + border-radius: 4px; + font-family: var(--font-mono); + font-size: 16px; + font-weight: 400; + text-decoration: none; + cursor: pointer; + } - &:hover { - background: var(--surface-raised-base, rgba(255, 255, 255, 0.08)); - border-color: rgba(255, 255, 255, 0.25); + [data-slot="cancel"] { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.92); + transition-property: background-color, border-color; + transition-duration: 150ms; + transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1); + + &:hover { + background-color: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.25); + } } - } - [data-slot="continue"] { - background: rgb(255, 255, 255); - color: rgb(0, 0, 0); + [data-slot="continue"] { + background: rgb(255, 255, 255); + color: rgb(0, 0, 0); + transition: background-color 150ms cubic-bezier(0.25, 0, 0.5, 1); - &:hover { - background: rgb(255, 255, 255, 0.9); + &:hover { + background: rgba(255, 255, 255, 0.9); + } } } } @@ -419,7 +553,8 @@ font-size: 13px; font-style: normal; font-weight: 400; - line-height: 160%; + line-height: 160%; /* 20.8px */ + font-style: italic; a { color: rgba(255, 255, 255, 0.39); @@ -436,7 +571,7 @@ align-items: center; margin-top: -18px; width: 100%; - max-width: 540px; + max-width: 660px; padding: 0 20px; @media (min-width: 768px) { @@ -491,7 +626,7 @@ [data-slot="multiplier"] { color: rgba(255, 255, 255, 0.39); - font-size: 13px; + font-size: 14px; &::before { content: "·"; @@ -510,39 +645,6 @@ font-weight: 400; } - [data-slot="tax-id-section"] { - display: flex; - flex-direction: column; - gap: 8px; - - [data-slot="label"] { - color: rgba(255, 255, 255, 0.59); - font-size: 14px; - } - - [data-slot="input"] { - width: 100%; - height: 44px; - padding: 0 12px; - background: #1a1a1a; - border: 1px solid rgba(255, 255, 255, 0.17); - border-radius: 4px; - color: #ffffff; - font-family: var(--font-mono); - font-size: 14px; - outline: none; - transition: border-color 0.15s ease; - - &::placeholder { - color: rgba(255, 255, 255, 0.39); - } - - &:focus { - border-color: rgba(255, 255, 255, 0.35); - } - } - } - [data-slot="checkout-form"] { display: flex; flex-direction: column; @@ -583,52 +685,6 @@ text-align: center; } - [data-slot="success"] { - display: flex; - flex-direction: column; - gap: 24px; - - [data-slot="title"] { - color: rgba(255, 255, 255, 0.92); - font-size: 18px; - font-weight: 400; - margin: 0; - } - - [data-slot="details"] { - display: flex; - flex-direction: column; - gap: 16px; - - > div { - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 16px; - } - - dt { - color: rgba(255, 255, 255, 0.59); - font-size: 14px; - font-weight: 400; - } - - dd { - color: rgba(255, 255, 255, 0.92); - font-size: 14px; - font-weight: 400; - margin: 0; - text-align: right; - } - } - - [data-slot="charge-notice"] { - color: #d4a500; - font-size: 14px; - text-align: left; - } - } - [data-slot="loading"] { display: flex; justify-content: center; @@ -645,6 +701,7 @@ text-align: center; font-size: 13px; font-style: italic; + view-transition-name: fine-print; a { color: rgba(255, 255, 255, 0.39); @@ -739,7 +796,7 @@ span, a { color: rgba(255, 255, 255, 0.39); - font-family: var(--font-mono); + font-family: "JetBrains Mono Nerd Font"; font-size: 16px; font-style: normal; font-weight: 400; @@ -749,7 +806,7 @@ [data-slot="github-stars"] { color: rgba(255, 255, 255, 0.25); - font-family: var(--font-mono); + font-family: "JetBrains Mono Nerd Font"; font-size: 16px; font-style: normal; font-weight: 400; @@ -764,10 +821,9 @@ } } } - [data-slot="anomaly-alt"] { color: rgba(255, 255, 255, 0.39); - font-family: var(--font-mono); + font-family: "JetBrains Mono Nerd Font"; font-size: 16px; font-style: normal; font-weight: 400; @@ -777,7 +833,7 @@ a { color: rgba(255, 255, 255, 0.39); - font-family: "JetBrains Mono Nerd Font", monospace; + font-family: "JetBrains Mono Nerd Font"; font-size: 16px; font-style: normal; font-weight: 400; @@ -791,15 +847,3 @@ } } } - -::view-transition-group(*) { - animation-duration: 200ms; - animation-timing-function: cubic-bezier(0.25, 0, 0.5, 1); -} - -@keyframes reveal { - 100% { - mask-position: 0% 0%; - opacity: 1; - } -} diff --git a/packages/console/app/src/routes/black.tsx b/packages/console/app/src/routes/black.tsx index bd836347070..36c9d1eaf08 100644 --- a/packages/console/app/src/routes/black.tsx +++ b/packages/console/app/src/routes/black.tsx @@ -1,8 +1,9 @@ import { A, createAsync, RouteSectionProps } from "@solidjs/router" import { Title, Meta, Link } from "@solidjs/meta" -import { createMemo } from "solid-js" +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 "./black.css" export default function BlackLayout(props: RouteSectionProps) { @@ -16,6 +17,49 @@ export default function BlackLayout(props: RouteSectionProps) { : config.github.starsFormatted.compact, ) + const [lightRaysConfig, setLightRaysConfig] = createSignal(defaultConfig) + const [rayAnimationState, setRayAnimationState] = createSignal({ + time: 0, + intensity: 0.5, + pulseValue: 1, + }) + + const svgLightingValues = createMemo(() => { + const state = rayAnimationState() + const t = state.time + + const wave1 = Math.sin(t * 1.5) * 0.5 + 0.5 + const wave2 = Math.sin(t * 2.3 + 1.2) * 0.5 + 0.5 + 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 shimmerIntensity = wave3 * 0.15 * state.pulseValue + + return { + glowIntensity, + fillOpacity, + strokeBrightness, + shimmerPos, + shimmerIntensity, + } + }) + + const svgLightingStyle = createMemo(() => { + const values = svgLightingValues() + return { + "--hero-black-glow-intensity": values.glowIntensity.toFixed(3), + "--hero-black-stroke-brightness": `${values.strokeBrightness.toFixed(0)}%`, + } as Record + }) + + const handleAnimationFrame = (state: LightRaysAnimationState) => { + setRayAnimationState(state) + } + return (
OpenCode Black | Access all the world's best coding models @@ -39,7 +83,9 @@ export default function BlackLayout(props: RouteSectionProps) { content="Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans." /> - -
+
- - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{props.children} diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index c46fdc0dabb..8f7ee95f3a5 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -1,12 +1,13 @@ import { A, useSearchParams } from "@solidjs/router" import { Title } from "@solidjs/meta" -import { createMemo, createSignal, For, onMount, Show } from "solid-js" +import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js" import { PlanIcon, plans } from "./common" export default function Black() { const [params] = useSearchParams() const [selected, setSelected] = createSignal((params.plan as string) || null) const [mounted, setMounted] = createSignal(false) + const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) onMount(() => { requestAnimationFrame(() => setMounted(true)) @@ -37,110 +38,138 @@ export default function Black() { <> opencode
-
- - {(plan) => { - const isSelected = createMemo(() => selected() === plan.id) - const isCollapsed = createMemo(() => selected() !== null && selected() !== plan.id) - - return ( -
+ + +
- ) - }} - -
-

+ )} + +

+ + + {(plan) => ( +
+
+
+ +
+

+ ${plan().id}{" "} + per person billed monthly + + {plan().multiplier} + +

+
    +
  • Your subscription will not start immediately
  • +
  • You will be added to the waitlist and activated soon
  • +
  • Your card will be only charged when your subscription is activated
  • +
  • Usage limits apply, heavily automated use may reach limits sooner
  • +
  • Subscriptions for individuals, contact Enterprise for teams
  • +
  • Limits may be adjusted and plans may be discontinued in the future
  • +
  • Cancel your subscription at anytime
  • +
+
+ + + Continue + +
+
+
+ )} +
+ + {(plan) => ( +
+
+
+ +
+

+ ${plan().id}{" "} + per person billed monthly + + {plan().multiplier} + +

+
    +
  • Your subscription will not start immediately
  • +
  • You will be added to the waitlist and activated soon
  • +
  • Your card will be only charged when your subscription is activated
  • +
  • Usage limits apply, heavily automated use may reach limits sooner
  • +
  • Subscriptions for individuals, contact Enterprise for teams
  • +
  • Limits may be adjusted and plans may be discontinued in the future
  • +
  • Cancel your subscription at anytime
  • +
+
+ + + Continue + +
+
+
+ )} +
+ + {(plan) => ( +
+
+
+ +
+

+ ${plan().id}{" "} + per person billed monthly + + {plan().multiplier} + +

+
    +
  • Your subscription will not start immediately
  • +
  • You will be added to the waitlist and activated soon
  • +
  • Your card will be only charged when your subscription is activated
  • +
  • Usage limits apply, heavily automated use may reach limits sooner
  • +
  • Subscriptions for individuals, contact Enterprise for teams
  • +
  • Limits may be adjusted and plans may be discontinued in the future
  • +
  • Cancel your subscription at anytime
  • +
+
+ + + Continue + +
+
+
+ )} +
+ +

Prices shown don't include applicable tax · Terms of Service

diff --git a/packages/console/app/tsconfig.json b/packages/console/app/tsconfig.json index e30a673892e..e5fb212de51 100644 --- a/packages/console/app/tsconfig.json +++ b/packages/console/app/tsconfig.json @@ -12,7 +12,7 @@ "allowJs": true, "strict": true, "noEmit": true, - "types": ["vite/client"], + "types": ["vite/client", "@webgpu/types"], "isolatedModules": true, "paths": { "~/*": ["./src/*"] diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index e0adae32c84..ebc7514d723 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -239,7 +239,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { {(item) => { return ( - + {item.file}