All components

Photon Beam

Photon Beam is a React and Tailwind CSS effect component from Eldora UI, licensed MIT. Copy it and paste it into your project.

Eldora UI MIT effects

What it is

Photon Beam is a React and Tailwind CSS effect component from Eldora UI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i react three

Licence

This component comes from Eldora UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Photon Beam

registry/eldoraui/photon-beam.tsx
"use client"

import { useEffect, useRef } from "react"
import * as THREE from "three"
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js"
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js"

interface Signal {
  mesh: THREE.Line
  laneIndex: number
  speed: number
  progress: number
  history: THREE.Vector3[]
  assignedColor: THREE.Color
}

interface Params {
  colorBg: string
  colorLine: string
  colorSignal: string
  useColor2: boolean
  colorSignal2: string
  useColor3: boolean
  colorSignal3: string
  lineCount: number
  globalRotation: number
  positionX: number
  positionY: number
  spreadHeight: number
  spreadDepth: number
  curveLength: number
  straightLength: number
  curvePower: number
  waveSpeed: number
  waveHeight: number
  lineOpacity: number
  signalCount: number
  speedGlobal: number
  trailLength: number
  bloomStrength: number
  bloomRadius: number
}

interface PhotonBeamProps {
  colorBg?: string
  colorLine?: string
  colorSignal?: string
  useColor2?: boolean
  colorSignal2?: string
  useColor3?: boolean
  colorSignal3?: string
  lineCount?: number
  spreadHeight?: number
  spreadDepth?: number
  curveLength?: number
  straightLength?: number
  curvePower?: number
  waveSpeed?: number
  waveHeight?: number
  lineOpacity?: number
  signalCount?: number
  speedGlobal?: number
  trailLength?: number
  bloomStrength?: number
  bloomRadius?: number
}

const CONSTANTS = {
  segmentCount: 150,
}

export default function PhotonBeam(props: PhotonBeamProps = {}) {
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const container = containerRef.current
    if (!container) return

    let frameId: number
    let cancelled = false
    let renderer: THREE.WebGLRenderer | null = null
    let composer: InstanceType<typeof EffectComposer> | null = null
    let handleResize: (() => void) | null = null
    let backgroundLines: THREE.Line[] = []
    let signals: Signal[] = []

    const init = (): void => {
      if (cancelled) return

      const width = container.clientWidth
      const height = container.clientHeight
      if (width === 0 || height === 0) {
        frameId = requestAnimationFrame(init)
        return
      }

      // --- CONFIGURATION ---
      const params: Params = {
        colorBg: props.colorBg ?? "#080808",
        colorLine: props.colorLine ?? "#005f6f",
        colorSignal: props.colorSignal ?? "#00d9ff",
        useColor2: props.useColor2 ?? false,
        colorSignal2: props.colorSignal2 ?? "#00ffff",
        useColor3: props.useColor3 ?? false,
        colorSignal3: props.colorSignal3 ?? "#00b8d4",
        lineCount: props.lineCount ?? 80,
        globalRotation: 0,
        positionX: 0,
        positionY: 0,
        spreadHeight: props.spreadHeight ?? 30.33,
        spreadDepth: props.spreadDepth ?? 0,
        curveLength: props.curveLength ?? 50,
        straightLength: props.straightLength ?? 100,
        curvePower: props.curvePower ?? 0.8265,
        waveSpeed: props.waveSpeed ?? 2.48,
        waveHeight: props.waveHeight ?? 0.145,
        lineOpacity: props.lineOpacity ?? 0.557,
        signalCount: props.signalCount ?? 94,
        speedGlobal: props.speedGlobal ?? 0.345,
        trailLength: props.trailLength ?? 3,
        bloomStrength: props.bloomStrength ?? 3.0,
        bloomRadius: props.bloomRadius ?? 0.5,
      }

      params.positionX = (params.curveLength - params.straightLength) / 2

      // --- SCENE SETUP ---
      const scene = new THREE.Scene()
      scene.background = new THREE.Color(params.colorBg)
      scene.fog = new THREE.FogExp2(params.colorBg, 0.002)

      const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000)
      camera.position.set(0, 0, 90)
      camera.lookAt(0, 0, 0)

      renderer = new THREE.WebGLRenderer({ antialias: true })
      renderer.setSize(width, height)
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
      container.appendChild(renderer.domElement)

      const contentGroup = new THREE.Group()
      contentGroup.position.set(params.positionX, params.positionY, 0)
      scene.add(contentGroup)

      // --- POST-PROCESSING ---
      const renderScene = new RenderPass(scene, camera)
      const bloomPass = new UnrealBloomPass(
        new THREE.Vector2(width, height),
        1.5,
        0.4,
        0.85
      )
      bloomPass.threshold = 0
      bloomPass.strength = params.bloomStrength
      bloomPass.radius = params.bloomRadius

      composer = new EffectComposer(renderer)
      composer.addPass(renderScene)
      composer.addPass(bloomPass)

      // --- MATH & PATH CALCULATION ---
      function getPathPoint(
        t: number,
        lineIndex: number,
        time: number
      ): THREE.Vector3 {
        const totalLen = params.curveLength + params.straightLength
        const currentX = -params.curveLength + t * totalLen

        let y = 0
        let z = 0
        const spreadFactor = (lineIndex / params.lineCount - 0.5) * 2

        if (currentX < 0) {
          const ratio = (currentX + params.curveLength) / params.curveLength
          let shapeFactor = (Math.cos(ratio * Math.PI) + 1) / 2
          shapeFactor = Math.pow(shapeFactor, params.curvePower)

          y = spreadFactor * params.spreadHeight * shapeFactor
          z = spreadFactor * params.spreadDepth * shapeFactor

          const waveFactor = shapeFactor
          const wave =
            Math.sin(time * params.waveSpeed + currentX * 0.1 + lineIndex) *
            params.waveHeight *
            waveFactor
          y += wave
        }

        return new THREE.Vector3(currentX, y, z)
      }

      // --- OBJECTS MANAGEMENT ---
      backgroundLines = []
      signals = []

      const bgMaterial = new THREE.LineBasicMaterial({
        color: params.colorLine,
        transparent: true,
        opacity: params.lineOpacity,
        depthWrite: false,
      })

      const signalMaterial = new THREE.LineBasicMaterial({
        vertexColors: true,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        depthTest: false,
        transparent: true,
      })

      const signalColorObj1 = new THREE.Color(params.colorSignal)
      const signalColorObj2 = new THREE.Color(params.colorSignal2)
      const signalColorObj3 = new THREE.Color(params.colorSignal3)

      function pickSignalColor(): THREE.Color {
        const choices: THREE.Color[] = [signalColorObj1]
        if (params.useColor2) choices.push(signalColorObj2)
        if (params.useColor3) choices.push(signalColorObj3)
        return choices[Math.floor(Math.random() * choices.length)]
      }

      function createSignal(): void {
        const maxTrail = 150
        const geometry = new THREE.BufferGeometry()
        const positions = new Float32Array(maxTrail * 3)
        const colors = new Float32Array(maxTrail * 3)

        geometry.setAttribute(
          "position",
          new THREE.BufferAttribute(positions, 3)
        )
        geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3))

        const mesh = new THREE.Line(geometry, signalMaterial)
        mesh.frustumCulled = false
        mesh.renderOrder = 1
        contentGroup.add(mesh)

        signals.push({
          mesh: mesh,
          laneIndex: Math.floor(Math.random() * params.lineCount),
          speed: 0.2 + Math.random() * 0.5,
          progress: Math.random(),
          history: [],
          assignedColor: pickSignalColor(),
        })
      }

      function rebuildSignals(): void {
        signals.forEach((s) => {
          contentGroup.remove(s.mesh)
          s.mesh.geometry.dispose()
        })
        signals = []
        for (let i = 0; i < params.signalCount; i++) {
          createSignal()
        }
      }

      function rebuildLines(): void {
        backgroundLines.forEach((l) => {
          contentGroup.remove(l)
          l.geometry.dispose()
        })
        backgroundLines = []

        for (let i = 0; i < params.lineCount; i++) {
          const geometry = new THREE.BufferGeometry()
          const positions = new Float32Array(CONSTANTS.segmentCount * 3)
          geometry.setAttribute(
            "position",
            new THREE.BufferAttribute(positions, 3)
          )

          const line = new THREE.Line(geometry, bgMaterial)
          line.userData = { id: i }
          line.renderOrder = 0
          contentGroup.add(line)
          backgroundLines.push(line)
        }
        rebuildSignals()
      }

      // Initial Build
      rebuildLines()

      // --- ANIMATION LOOP ---
      const clock = new THREE.Clock()

      function animate(): void {
        if (cancelled) return
        frameId = requestAnimationFrame(animate)

        const time = clock.getElapsedTime()

        // Update Lines
        backgroundLines.forEach((line) => {
          const positions = line.geometry.attributes.position
            .array as Float32Array
          const lineId = line.userData.id
          for (let j = 0; j < CONSTANTS.segmentCount; j++) {
            const t = j / (CONSTANTS.segmentCount - 1)
            const vec = getPathPoint(t, lineId, time)
            positions[j * 3] = vec.x
            positions[j * 3 + 1] = vec.y
            positions[j * 3 + 2] = vec.z
          }
          line.geometry.attributes.position.needsUpdate = true
        })

        // Update Signals
        signals.forEach((sig) => {
          sig.progress += sig.speed * 0.005 * params.speedGlobal

          if (sig.progress > 1.0) {
            sig.progress = 0
            sig.laneIndex = Math.floor(Math.random() * params.lineCount)
            sig.history = []
            sig.assignedColor = pickSignalColor()
          }

          const pos = getPathPoint(sig.progress, sig.laneIndex, time)
          sig.history.push(pos)

          if (sig.history.length > params.trailLength + 1) {
            sig.history.shift()
          }

          const positions = sig.mesh.geometry.attributes.position
            .array as Float32Array
          const colors = sig.mesh.geometry.attributes.color
            .array as Float32Array

          const drawCount = Math.max(1, params.trailLength)
          const currentLen = sig.history.length

          for (let i = 0; i < drawCount; i++) {
            let index = currentLen - 1 - i
            if (index < 0) index = 0

            const p = sig.history[index] || new THREE.Vector3()

            positions[i * 3] = p.x
            positions[i * 3 + 1] = p.y
            positions[i * 3 + 2] = p.z

            let alpha = 1
            if (params.trailLength > 0) {
              alpha = Math.max(0, 1 - i / params.trailLength)
            }

            colors[i * 3] = sig.assignedColor.r * alpha
            colors[i * 3 + 1] = sig.assignedColor.g * alpha
            colors[i * 3 + 2] = sig.assignedColor.b * alpha
          }

          sig.mesh.geometry.setDrawRange(0, drawCount)
          sig.mesh.geometry.attributes.position.needsUpdate = true
          sig.mesh.geometry.attributes.color.needsUpdate = true
        })

        if (composer) composer.render()
      }

      // Resize Handler
      handleResize = (): void => {
        if (!container || cancelled || !renderer || !composer) return
        const w = container.clientWidth
        const h = container.clientHeight
        if (w === 0 || h === 0) return

        camera.aspect = w / h
        camera.updateProjectionMatrix()
        renderer.setSize(w, h)
        composer.setSize(w, h)
      }

      window.addEventListener("resize", handleResize)
      animate()
    }

    frameId = requestAnimationFrame(init)

    return () => {
      cancelled = true
      cancelAnimationFrame(frameId)
      if (handleResize) {
        window.removeEventListener("resize", handleResize)
      }
      if (containerRef.current && renderer?.domElement) {
        try {
          containerRef.current.removeChild(renderer.domElement)
        } catch {
          /* element may already be removed */
        }
        renderer.dispose()
      }
      composer?.dispose()
      backgroundLines.forEach((l) => l.geometry.dispose())
      signals.forEach((s) => s.mesh.geometry.dispose())
    }
  }, [])

  return (
    <div ref={containerRef} className="h-full min-h-[200px] w-full bg-black" />
  )
}

Similar components