light-rays.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. import { createSignal, createEffect, onMount, onCleanup, Show, For, Accessor, Setter } from "solid-js"
  2. import "./light-rays.css"
  3. export type RaysOrigin =
  4. | "top-center"
  5. | "top-left"
  6. | "top-right"
  7. | "right"
  8. | "left"
  9. | "bottom-center"
  10. | "bottom-right"
  11. | "bottom-left"
  12. export interface LightRaysConfig {
  13. raysOrigin: RaysOrigin
  14. raysColor: string
  15. raysSpeed: number
  16. lightSpread: number
  17. rayLength: number
  18. sourceWidth: number
  19. pulsating: boolean
  20. pulsatingMin: number
  21. pulsatingMax: number
  22. fadeDistance: number
  23. saturation: number
  24. followMouse: boolean
  25. mouseInfluence: number
  26. noiseAmount: number
  27. distortion: number
  28. opacity: number
  29. }
  30. export const defaultConfig: LightRaysConfig = {
  31. raysOrigin: "top-center",
  32. raysColor: "#ffffff",
  33. raysSpeed: 1.0,
  34. lightSpread: 1.2,
  35. rayLength: 4.5,
  36. sourceWidth: 0.1,
  37. pulsating: true,
  38. pulsatingMin: 0.9,
  39. pulsatingMax: 1.05,
  40. fadeDistance: 1.25,
  41. saturation: 0.35,
  42. followMouse: false,
  43. mouseInfluence: 0.05,
  44. noiseAmount: 0.5,
  45. distortion: 0.0,
  46. opacity: 0.35,
  47. }
  48. export interface LightRaysAnimationState {
  49. time: number
  50. intensity: number
  51. pulseValue: number
  52. }
  53. interface LightRaysProps {
  54. config: Accessor<LightRaysConfig>
  55. class?: string
  56. onAnimationFrame?: (state: LightRaysAnimationState) => void
  57. }
  58. const hexToRgb = (hex: string): [number, number, number] => {
  59. const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
  60. return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
  61. }
  62. const getAnchorAndDir = (
  63. origin: RaysOrigin,
  64. w: number,
  65. h: number,
  66. ): { anchor: [number, number]; dir: [number, number] } => {
  67. const outside = 0.2
  68. switch (origin) {
  69. case "top-left":
  70. return { anchor: [0, -outside * h], dir: [0, 1] }
  71. case "top-right":
  72. return { anchor: [w, -outside * h], dir: [0, 1] }
  73. case "left":
  74. return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] }
  75. case "right":
  76. return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] }
  77. case "bottom-left":
  78. return { anchor: [0, (1 + outside) * h], dir: [0, -1] }
  79. case "bottom-center":
  80. return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] }
  81. case "bottom-right":
  82. return { anchor: [w, (1 + outside) * h], dir: [0, -1] }
  83. default: // "top-center"
  84. return { anchor: [0.5 * w, -outside * h], dir: [0, 1] }
  85. }
  86. }
  87. interface UniformData {
  88. iTime: number
  89. iResolution: [number, number]
  90. rayPos: [number, number]
  91. rayDir: [number, number]
  92. raysColor: [number, number, number]
  93. raysSpeed: number
  94. lightSpread: number
  95. rayLength: number
  96. sourceWidth: number
  97. pulsating: number
  98. pulsatingMin: number
  99. pulsatingMax: number
  100. fadeDistance: number
  101. saturation: number
  102. mousePos: [number, number]
  103. mouseInfluence: number
  104. noiseAmount: number
  105. distortion: number
  106. }
  107. const WGSL_SHADER = `
  108. struct Uniforms {
  109. iTime: f32,
  110. _pad0: f32,
  111. iResolution: vec2<f32>,
  112. rayPos: vec2<f32>,
  113. rayDir: vec2<f32>,
  114. raysColor: vec3<f32>,
  115. raysSpeed: f32,
  116. lightSpread: f32,
  117. rayLength: f32,
  118. sourceWidth: f32,
  119. pulsating: f32,
  120. pulsatingMin: f32,
  121. pulsatingMax: f32,
  122. fadeDistance: f32,
  123. saturation: f32,
  124. mousePos: vec2<f32>,
  125. mouseInfluence: f32,
  126. noiseAmount: f32,
  127. distortion: f32,
  128. _pad1: f32,
  129. _pad2: f32,
  130. _pad3: f32,
  131. };
  132. @group(0) @binding(0) var<uniform> uniforms: Uniforms;
  133. struct VertexOutput {
  134. @builtin(position) position: vec4<f32>,
  135. @location(0) vUv: vec2<f32>,
  136. };
  137. @vertex
  138. fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
  139. var positions = array<vec2<f32>, 3>(
  140. vec2<f32>(-1.0, -1.0),
  141. vec2<f32>(3.0, -1.0),
  142. vec2<f32>(-1.0, 3.0)
  143. );
  144. var output: VertexOutput;
  145. let pos = positions[vertexIndex];
  146. output.position = vec4<f32>(pos, 0.0, 1.0);
  147. output.vUv = pos * 0.5 + 0.5;
  148. return output;
  149. }
  150. fn noise(st: vec2<f32>) -> f32 {
  151. return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453123);
  152. }
  153. fn rayStrength(raySource: vec2<f32>, rayRefDirection: vec2<f32>, coord: vec2<f32>,
  154. seedA: f32, seedB: f32, speed: f32) -> f32 {
  155. let sourceToCoord = coord - raySource;
  156. let dirNorm = normalize(sourceToCoord);
  157. let cosAngle = dot(dirNorm, rayRefDirection);
  158. let distortedAngle = cosAngle + uniforms.distortion * sin(uniforms.iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;
  159. let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(uniforms.lightSpread, 0.001));
  160. let distance = length(sourceToCoord);
  161. let maxDistance = uniforms.iResolution.x * uniforms.rayLength;
  162. let lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
  163. let fadeFalloff = clamp((uniforms.iResolution.x * uniforms.fadeDistance - distance) / (uniforms.iResolution.x * uniforms.fadeDistance), 0.5, 1.0);
  164. let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
  165. let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
  166. var pulse: f32;
  167. if (uniforms.pulsating > 0.5) {
  168. pulse = pulseCenter + pulseAmplitude * sin(uniforms.iTime * speed * 3.0);
  169. } else {
  170. pulse = 1.0;
  171. }
  172. let baseStrength = clamp(
  173. (0.45 + 0.15 * sin(distortedAngle * seedA + uniforms.iTime * speed)) +
  174. (0.3 + 0.2 * cos(-distortedAngle * seedB + uniforms.iTime * speed)),
  175. 0.0, 1.0
  176. );
  177. return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
  178. }
  179. @fragment
  180. fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
  181. let coord = vec2<f32>(fragCoord.x, fragCoord.y);
  182. let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
  183. let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
  184. let perpDir = vec2<f32>(-uniforms.rayDir.y, uniforms.rayDir.x);
  185. let adjustedRayPos = uniforms.rayPos + perpDir * widthOffset;
  186. var finalRayDir = uniforms.rayDir;
  187. if (uniforms.mouseInfluence > 0.0) {
  188. let mouseScreenPos = uniforms.mousePos * uniforms.iResolution;
  189. let mouseDirection = normalize(mouseScreenPos - adjustedRayPos);
  190. finalRayDir = normalize(mix(uniforms.rayDir, mouseDirection, uniforms.mouseInfluence));
  191. }
  192. let rays1 = vec4<f32>(1.0) *
  193. rayStrength(adjustedRayPos, finalRayDir, coord, 36.2214, 21.11349,
  194. 1.5 * uniforms.raysSpeed);
  195. let rays2 = vec4<f32>(1.0) *
  196. rayStrength(adjustedRayPos, finalRayDir, coord, 22.3991, 18.0234,
  197. 1.1 * uniforms.raysSpeed);
  198. var fragColor = rays1 * 0.5 + rays2 * 0.4;
  199. if (uniforms.noiseAmount > 0.0) {
  200. let n = noise(coord * 0.01 + uniforms.iTime * 0.1);
  201. fragColor = vec4<f32>(fragColor.rgb * (1.0 - uniforms.noiseAmount + uniforms.noiseAmount * n), fragColor.a);
  202. }
  203. let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
  204. fragColor.x = fragColor.x * (0.1 + brightness * 0.8);
  205. fragColor.y = fragColor.y * (0.3 + brightness * 0.6);
  206. fragColor.z = fragColor.z * (0.5 + brightness * 0.5);
  207. if (uniforms.saturation != 1.0) {
  208. let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
  209. fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
  210. }
  211. fragColor = vec4<f32>(fragColor.rgb * uniforms.raysColor, fragColor.a);
  212. return fragColor;
  213. }
  214. `
  215. const UNIFORM_BUFFER_SIZE = 96
  216. function createUniformBuffer(data: UniformData): Float32Array {
  217. const buffer = new Float32Array(24)
  218. buffer[0] = data.iTime
  219. buffer[1] = 0
  220. buffer[2] = data.iResolution[0]
  221. buffer[3] = data.iResolution[1]
  222. buffer[4] = data.rayPos[0]
  223. buffer[5] = data.rayPos[1]
  224. buffer[6] = data.rayDir[0]
  225. buffer[7] = data.rayDir[1]
  226. buffer[8] = data.raysColor[0]
  227. buffer[9] = data.raysColor[1]
  228. buffer[10] = data.raysColor[2]
  229. buffer[11] = data.raysSpeed
  230. buffer[12] = data.lightSpread
  231. buffer[13] = data.rayLength
  232. buffer[14] = data.sourceWidth
  233. buffer[15] = data.pulsating
  234. buffer[16] = data.pulsatingMin
  235. buffer[17] = data.pulsatingMax
  236. buffer[18] = data.fadeDistance
  237. buffer[19] = data.saturation
  238. buffer[20] = data.mousePos[0]
  239. buffer[21] = data.mousePos[1]
  240. buffer[22] = data.mouseInfluence
  241. buffer[23] = data.noiseAmount
  242. return buffer
  243. }
  244. const UNIFORM_BUFFER_SIZE_CORRECTED = 112
  245. function createUniformBufferCorrected(data: UniformData): Float32Array {
  246. const buffer = new Float32Array(28)
  247. buffer[0] = data.iTime
  248. buffer[1] = 0
  249. buffer[2] = data.iResolution[0]
  250. buffer[3] = data.iResolution[1]
  251. buffer[4] = data.rayPos[0]
  252. buffer[5] = data.rayPos[1]
  253. buffer[6] = data.rayDir[0]
  254. buffer[7] = data.rayDir[1]
  255. buffer[8] = data.raysColor[0]
  256. buffer[9] = data.raysColor[1]
  257. buffer[10] = data.raysColor[2]
  258. buffer[11] = data.raysSpeed
  259. buffer[12] = data.lightSpread
  260. buffer[13] = data.rayLength
  261. buffer[14] = data.sourceWidth
  262. buffer[15] = data.pulsating
  263. buffer[16] = data.pulsatingMin
  264. buffer[17] = data.pulsatingMax
  265. buffer[18] = data.fadeDistance
  266. buffer[19] = data.saturation
  267. buffer[20] = data.mousePos[0]
  268. buffer[21] = data.mousePos[1]
  269. buffer[22] = data.mouseInfluence
  270. buffer[23] = data.noiseAmount
  271. buffer[24] = data.distortion
  272. buffer[25] = 0
  273. buffer[26] = 0
  274. buffer[27] = 0
  275. return buffer
  276. }
  277. export default function LightRays(props: LightRaysProps) {
  278. let containerRef: HTMLDivElement | undefined
  279. let canvasRef: HTMLCanvasElement | null = null
  280. let deviceRef: GPUDevice | null = null
  281. let contextRef: GPUCanvasContext | null = null
  282. let pipelineRef: GPURenderPipeline | null = null
  283. let uniformBufferRef: GPUBuffer | null = null
  284. let bindGroupRef: GPUBindGroup | null = null
  285. let animationIdRef: number | null = null
  286. let cleanupFunctionRef: (() => void) | null = null
  287. let uniformDataRef: UniformData | null = null
  288. const mouseRef = { x: 0.5, y: 0.5 }
  289. const smoothMouseRef = { x: 0.5, y: 0.5 }
  290. const [isVisible, setIsVisible] = createSignal(false)
  291. onMount(() => {
  292. if (!containerRef) return
  293. const observer = new IntersectionObserver(
  294. (entries) => {
  295. const entry = entries[0]
  296. setIsVisible(entry.isIntersecting)
  297. },
  298. { threshold: 0.1 },
  299. )
  300. observer.observe(containerRef)
  301. onCleanup(() => {
  302. observer.disconnect()
  303. })
  304. })
  305. createEffect(() => {
  306. const visible = isVisible()
  307. const config = props.config()
  308. if (!visible || !containerRef) {
  309. return
  310. }
  311. if (cleanupFunctionRef) {
  312. cleanupFunctionRef()
  313. cleanupFunctionRef = null
  314. }
  315. const initializeWebGPU = async () => {
  316. if (!containerRef) {
  317. return
  318. }
  319. await new Promise((resolve) => setTimeout(resolve, 10))
  320. if (!containerRef) {
  321. return
  322. }
  323. if (!navigator.gpu) {
  324. console.warn("WebGPU is not supported in this browser")
  325. return
  326. }
  327. const adapter = await navigator.gpu.requestAdapter()
  328. if (!adapter) {
  329. console.warn("Failed to get WebGPU adapter")
  330. return
  331. }
  332. const device = await adapter.requestDevice()
  333. deviceRef = device
  334. const canvas = document.createElement("canvas")
  335. canvas.style.width = "100%"
  336. canvas.style.height = "100%"
  337. canvasRef = canvas
  338. while (containerRef.firstChild) {
  339. containerRef.removeChild(containerRef.firstChild)
  340. }
  341. containerRef.appendChild(canvas)
  342. const context = canvas.getContext("webgpu")
  343. if (!context) {
  344. console.warn("Failed to get WebGPU context")
  345. return
  346. }
  347. contextRef = context
  348. const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
  349. context.configure({
  350. device,
  351. format: presentationFormat,
  352. alphaMode: "premultiplied",
  353. })
  354. const shaderModule = device.createShaderModule({
  355. code: WGSL_SHADER,
  356. })
  357. const uniformBuffer = device.createBuffer({
  358. size: UNIFORM_BUFFER_SIZE_CORRECTED,
  359. usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  360. })
  361. uniformBufferRef = uniformBuffer
  362. const bindGroupLayout = device.createBindGroupLayout({
  363. entries: [
  364. {
  365. binding: 0,
  366. visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
  367. buffer: { type: "uniform" },
  368. },
  369. ],
  370. })
  371. const bindGroup = device.createBindGroup({
  372. layout: bindGroupLayout,
  373. entries: [
  374. {
  375. binding: 0,
  376. resource: { buffer: uniformBuffer },
  377. },
  378. ],
  379. })
  380. bindGroupRef = bindGroup
  381. const pipelineLayout = device.createPipelineLayout({
  382. bindGroupLayouts: [bindGroupLayout],
  383. })
  384. const pipeline = device.createRenderPipeline({
  385. layout: pipelineLayout,
  386. vertex: {
  387. module: shaderModule,
  388. entryPoint: "vertexMain",
  389. },
  390. fragment: {
  391. module: shaderModule,
  392. entryPoint: "fragmentMain",
  393. targets: [
  394. {
  395. format: presentationFormat,
  396. blend: {
  397. color: {
  398. srcFactor: "src-alpha",
  399. dstFactor: "one-minus-src-alpha",
  400. operation: "add",
  401. },
  402. alpha: {
  403. srcFactor: "one",
  404. dstFactor: "one-minus-src-alpha",
  405. operation: "add",
  406. },
  407. },
  408. },
  409. ],
  410. },
  411. primitive: {
  412. topology: "triangle-list",
  413. },
  414. })
  415. pipelineRef = pipeline
  416. const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
  417. const dpr = Math.min(window.devicePixelRatio, 2)
  418. const w = wCSS * dpr
  419. const h = hCSS * dpr
  420. const { anchor, dir } = getAnchorAndDir(config.raysOrigin, w, h)
  421. uniformDataRef = {
  422. iTime: 0,
  423. iResolution: [w, h],
  424. rayPos: anchor,
  425. rayDir: dir,
  426. raysColor: hexToRgb(config.raysColor),
  427. raysSpeed: config.raysSpeed,
  428. lightSpread: config.lightSpread,
  429. rayLength: config.rayLength,
  430. sourceWidth: config.sourceWidth,
  431. pulsating: config.pulsating ? 1.0 : 0.0,
  432. pulsatingMin: config.pulsatingMin,
  433. pulsatingMax: config.pulsatingMax,
  434. fadeDistance: config.fadeDistance,
  435. saturation: config.saturation,
  436. mousePos: [0.5, 0.5],
  437. mouseInfluence: config.mouseInfluence,
  438. noiseAmount: config.noiseAmount,
  439. distortion: config.distortion,
  440. }
  441. const updatePlacement = () => {
  442. if (!containerRef || !canvasRef || !uniformDataRef) {
  443. return
  444. }
  445. const dpr = Math.min(window.devicePixelRatio, 2)
  446. const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
  447. const w = Math.floor(wCSS * dpr)
  448. const h = Math.floor(hCSS * dpr)
  449. canvasRef.width = w
  450. canvasRef.height = h
  451. uniformDataRef.iResolution = [w, h]
  452. const currentConfig = props.config()
  453. const { anchor, dir } = getAnchorAndDir(currentConfig.raysOrigin, w, h)
  454. uniformDataRef.rayPos = anchor
  455. uniformDataRef.rayDir = dir
  456. }
  457. const loop = (t: number) => {
  458. if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
  459. return
  460. }
  461. const currentConfig = props.config()
  462. const timeSeconds = t * 0.001
  463. uniformDataRef.iTime = timeSeconds
  464. if (currentConfig.followMouse && currentConfig.mouseInfluence > 0.0) {
  465. const smoothing = 0.92
  466. smoothMouseRef.x = smoothMouseRef.x * smoothing + mouseRef.x * (1 - smoothing)
  467. smoothMouseRef.y = smoothMouseRef.y * smoothing + mouseRef.y * (1 - smoothing)
  468. uniformDataRef.mousePos = [smoothMouseRef.x, smoothMouseRef.y]
  469. }
  470. if (props.onAnimationFrame) {
  471. const pulseCenter = (currentConfig.pulsatingMin + currentConfig.pulsatingMax) * 0.5
  472. const pulseAmplitude = (currentConfig.pulsatingMax - currentConfig.pulsatingMin) * 0.5
  473. const pulseValue = currentConfig.pulsating
  474. ? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * currentConfig.raysSpeed * 3.0)
  475. : 1.0
  476. const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * currentConfig.raysSpeed * 1.5)
  477. const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * currentConfig.raysSpeed * 1.1)
  478. const intensity = (baseIntensity1 + baseIntensity2) * pulseValue
  479. props.onAnimationFrame({
  480. time: timeSeconds,
  481. intensity,
  482. pulseValue,
  483. })
  484. }
  485. try {
  486. const uniformData = createUniformBufferCorrected(uniformDataRef)
  487. deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformData.buffer)
  488. const commandEncoder = deviceRef.createCommandEncoder()
  489. const textureView = contextRef.getCurrentTexture().createView()
  490. const renderPass = commandEncoder.beginRenderPass({
  491. colorAttachments: [
  492. {
  493. view: textureView,
  494. clearValue: { r: 0, g: 0, b: 0, a: 0 },
  495. loadOp: "clear",
  496. storeOp: "store",
  497. },
  498. ],
  499. })
  500. renderPass.setPipeline(pipelineRef)
  501. renderPass.setBindGroup(0, bindGroupRef)
  502. renderPass.draw(3)
  503. renderPass.end()
  504. deviceRef.queue.submit([commandEncoder.finish()])
  505. animationIdRef = requestAnimationFrame(loop)
  506. } catch (error) {
  507. console.warn("WebGPU rendering error:", error)
  508. return
  509. }
  510. }
  511. window.addEventListener("resize", updatePlacement)
  512. updatePlacement()
  513. animationIdRef = requestAnimationFrame(loop)
  514. cleanupFunctionRef = () => {
  515. if (animationIdRef) {
  516. cancelAnimationFrame(animationIdRef)
  517. animationIdRef = null
  518. }
  519. window.removeEventListener("resize", updatePlacement)
  520. if (uniformBufferRef) {
  521. uniformBufferRef.destroy()
  522. uniformBufferRef = null
  523. }
  524. if (deviceRef) {
  525. deviceRef.destroy()
  526. deviceRef = null
  527. }
  528. if (canvasRef && canvasRef.parentNode) {
  529. canvasRef.parentNode.removeChild(canvasRef)
  530. }
  531. canvasRef = null
  532. contextRef = null
  533. pipelineRef = null
  534. bindGroupRef = null
  535. uniformDataRef = null
  536. }
  537. }
  538. initializeWebGPU()
  539. onCleanup(() => {
  540. if (cleanupFunctionRef) {
  541. cleanupFunctionRef()
  542. cleanupFunctionRef = null
  543. }
  544. })
  545. })
  546. createEffect(() => {
  547. if (!uniformDataRef || !containerRef) {
  548. return
  549. }
  550. const config = props.config()
  551. uniformDataRef.raysColor = hexToRgb(config.raysColor)
  552. uniformDataRef.raysSpeed = config.raysSpeed
  553. uniformDataRef.lightSpread = config.lightSpread
  554. uniformDataRef.rayLength = config.rayLength
  555. uniformDataRef.sourceWidth = config.sourceWidth
  556. uniformDataRef.pulsating = config.pulsating ? 1.0 : 0.0
  557. uniformDataRef.pulsatingMin = config.pulsatingMin
  558. uniformDataRef.pulsatingMax = config.pulsatingMax
  559. uniformDataRef.fadeDistance = config.fadeDistance
  560. uniformDataRef.saturation = config.saturation
  561. uniformDataRef.mouseInfluence = config.mouseInfluence
  562. uniformDataRef.noiseAmount = config.noiseAmount
  563. uniformDataRef.distortion = config.distortion
  564. const dpr = Math.min(window.devicePixelRatio, 2)
  565. const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
  566. const { anchor, dir } = getAnchorAndDir(config.raysOrigin, wCSS * dpr, hCSS * dpr)
  567. uniformDataRef.rayPos = anchor
  568. uniformDataRef.rayDir = dir
  569. })
  570. createEffect(() => {
  571. const config = props.config()
  572. if (!config.followMouse) {
  573. return
  574. }
  575. const handleMouseMove = (e: MouseEvent) => {
  576. if (!containerRef) {
  577. return
  578. }
  579. const rect = containerRef.getBoundingClientRect()
  580. const x = (e.clientX - rect.left) / rect.width
  581. const y = (e.clientY - rect.top) / rect.height
  582. mouseRef.x = x
  583. mouseRef.y = y
  584. }
  585. window.addEventListener("mousemove", handleMouseMove)
  586. onCleanup(() => {
  587. window.removeEventListener("mousemove", handleMouseMove)
  588. })
  589. })
  590. return (
  591. <div
  592. ref={containerRef}
  593. class={`light-rays-container ${props.class ?? ""}`.trim()}
  594. style={{ opacity: props.config().opacity }}
  595. />
  596. )
  597. }
  598. interface LightRaysControlsProps {
  599. config: Accessor<LightRaysConfig>
  600. setConfig: Setter<LightRaysConfig>
  601. }
  602. export function LightRaysControls(props: LightRaysControlsProps) {
  603. const [isOpen, setIsOpen] = createSignal(true)
  604. const updateConfig = <K extends keyof LightRaysConfig>(key: K, value: LightRaysConfig[K]) => {
  605. props.setConfig((prev) => ({ ...prev, [key]: value }))
  606. }
  607. const origins: RaysOrigin[] = [
  608. "top-center",
  609. "top-left",
  610. "top-right",
  611. "left",
  612. "right",
  613. "bottom-center",
  614. "bottom-left",
  615. "bottom-right",
  616. ]
  617. return (
  618. <div class="light-rays-controls">
  619. <button class="light-rays-controls-toggle" onClick={() => setIsOpen(!isOpen())}>
  620. {isOpen() ? "▼" : "▶"} Light Rays
  621. </button>
  622. <Show when={isOpen()}>
  623. <div class="light-rays-controls-panel">
  624. <div class="control-group">
  625. <label>Origin</label>
  626. <select
  627. value={props.config().raysOrigin}
  628. onChange={(e) => updateConfig("raysOrigin", e.currentTarget.value as RaysOrigin)}
  629. >
  630. <For each={origins}>{(origin) => <option value={origin}>{origin}</option>}</For>
  631. </select>
  632. </div>
  633. <div class="control-group">
  634. <label>Color</label>
  635. <input
  636. type="color"
  637. value={props.config().raysColor}
  638. onInput={(e) => updateConfig("raysColor", e.currentTarget.value)}
  639. />
  640. </div>
  641. <div class="control-group">
  642. <label>Speed: {props.config().raysSpeed.toFixed(2)}</label>
  643. <input
  644. type="range"
  645. min="0"
  646. max="3"
  647. step="0.01"
  648. value={props.config().raysSpeed}
  649. onInput={(e) => updateConfig("raysSpeed", parseFloat(e.currentTarget.value))}
  650. />
  651. </div>
  652. <div class="control-group">
  653. <label>Light Spread: {props.config().lightSpread.toFixed(2)}</label>
  654. <input
  655. type="range"
  656. min="0.1"
  657. max="5"
  658. step="0.01"
  659. value={props.config().lightSpread}
  660. onInput={(e) => updateConfig("lightSpread", parseFloat(e.currentTarget.value))}
  661. />
  662. </div>
  663. <div class="control-group">
  664. <label>Ray Length: {props.config().rayLength.toFixed(2)}</label>
  665. <input
  666. type="range"
  667. min="0.1"
  668. max="5"
  669. step="0.01"
  670. value={props.config().rayLength}
  671. onInput={(e) => updateConfig("rayLength", parseFloat(e.currentTarget.value))}
  672. />
  673. </div>
  674. <div class="control-group">
  675. <label>Source Width: {props.config().sourceWidth.toFixed(2)}</label>
  676. <input
  677. type="range"
  678. min="0"
  679. max="2"
  680. step="0.01"
  681. value={props.config().sourceWidth}
  682. onInput={(e) => updateConfig("sourceWidth", parseFloat(e.currentTarget.value))}
  683. />
  684. </div>
  685. <div class="control-group">
  686. <label>Fade Distance: {props.config().fadeDistance.toFixed(2)}</label>
  687. <input
  688. type="range"
  689. min="0.1"
  690. max="3"
  691. step="0.01"
  692. value={props.config().fadeDistance}
  693. onInput={(e) => updateConfig("fadeDistance", parseFloat(e.currentTarget.value))}
  694. />
  695. </div>
  696. <div class="control-group">
  697. <label>Saturation: {props.config().saturation.toFixed(2)}</label>
  698. <input
  699. type="range"
  700. min="0"
  701. max="2"
  702. step="0.01"
  703. value={props.config().saturation}
  704. onInput={(e) => updateConfig("saturation", parseFloat(e.currentTarget.value))}
  705. />
  706. </div>
  707. <div class="control-group">
  708. <label>Mouse Influence: {props.config().mouseInfluence.toFixed(2)}</label>
  709. <input
  710. type="range"
  711. min="0"
  712. max="1"
  713. step="0.01"
  714. value={props.config().mouseInfluence}
  715. onInput={(e) => updateConfig("mouseInfluence", parseFloat(e.currentTarget.value))}
  716. />
  717. </div>
  718. <div class="control-group">
  719. <label>Noise: {props.config().noiseAmount.toFixed(2)}</label>
  720. <input
  721. type="range"
  722. min="0"
  723. max="1"
  724. step="0.01"
  725. value={props.config().noiseAmount}
  726. onInput={(e) => updateConfig("noiseAmount", parseFloat(e.currentTarget.value))}
  727. />
  728. </div>
  729. <div class="control-group">
  730. <label>Distortion: {props.config().distortion.toFixed(2)}</label>
  731. <input
  732. type="range"
  733. min="0"
  734. max="2"
  735. step="0.01"
  736. value={props.config().distortion}
  737. onInput={(e) => updateConfig("distortion", parseFloat(e.currentTarget.value))}
  738. />
  739. </div>
  740. <div class="control-group">
  741. <label>Opacity: {props.config().opacity.toFixed(2)}</label>
  742. <input
  743. type="range"
  744. min="0"
  745. max="1"
  746. step="0.01"
  747. value={props.config().opacity}
  748. onInput={(e) => updateConfig("opacity", parseFloat(e.currentTarget.value))}
  749. />
  750. </div>
  751. <div class="control-group checkbox">
  752. <label>
  753. <input
  754. type="checkbox"
  755. checked={props.config().pulsating}
  756. onChange={(e) => updateConfig("pulsating", e.currentTarget.checked)}
  757. />
  758. Pulsating
  759. </label>
  760. </div>
  761. <Show when={props.config().pulsating}>
  762. <div class="control-group">
  763. <label>Pulse Min: {props.config().pulsatingMin.toFixed(2)}</label>
  764. <input
  765. type="range"
  766. min="0"
  767. max="1"
  768. step="0.01"
  769. value={props.config().pulsatingMin}
  770. onInput={(e) => updateConfig("pulsatingMin", parseFloat(e.currentTarget.value))}
  771. />
  772. </div>
  773. <div class="control-group">
  774. <label>Pulse Max: {props.config().pulsatingMax.toFixed(2)}</label>
  775. <input
  776. type="range"
  777. min="0"
  778. max="2"
  779. step="0.01"
  780. value={props.config().pulsatingMax}
  781. onInput={(e) => updateConfig("pulsatingMax", parseFloat(e.currentTarget.value))}
  782. />
  783. </div>
  784. </Show>
  785. <div class="control-group checkbox">
  786. <label>
  787. <input
  788. type="checkbox"
  789. checked={props.config().followMouse}
  790. onChange={(e) => updateConfig("followMouse", e.currentTarget.checked)}
  791. />
  792. Follow Mouse
  793. </label>
  794. </div>
  795. <button class="reset-button" onClick={() => props.setConfig(defaultConfig)}>
  796. Reset to Defaults
  797. </button>
  798. </div>
  799. </Show>
  800. </div>
  801. )
  802. }