Files
dalcode-website/components/PixelTextReveal.tsx
T
Leon-in 97db9ee8c7 feat(site): redesign with product pages, ecosystem sections and pixel reveal
Add cli/code/office/platform/pricing pages, new home sections
(Ecosystem, FeatureGrid, Faq, WorkflowSteps, BottomCta, ProductEcosystem),
ScrollReveal and PixelTextReveal animation components, brand assets,
and expanded site-content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:00:18 +08:00

151 lines
4.1 KiB
TypeScript

"use client"
import { useRef, useEffect } from "react"
interface PixelTextRevealProps {
lines?: string[]
className?: string
}
export default function PixelTextReveal({
lines = ["DAL", "Ecosystem"],
className,
}: PixelTextRevealProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
const drawCtx = ctx
const dpr = window.devicePixelRatio || 1
const width = canvas.clientWidth
const height = canvas.clientHeight
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
const cellSize = 5
const gap = 1.5
const step = cellSize + gap
const cols = Math.floor(width / step)
const rows = Math.floor(height / step)
const scale = 6
const offW = cols * scale
const offH = rows * scale
const offscreen = document.createElement("canvas")
offscreen.width = offW
offscreen.height = offH
const offCtx = offscreen.getContext("2d")!
offCtx.fillStyle = "#000"
offCtx.fillRect(0, 0, offW, offH)
offCtx.fillStyle = "#fff"
offCtx.textAlign = "center"
offCtx.textBaseline = "middle"
const primarySize = Math.max(48, Math.floor(offH * 0.38))
const secondarySize = Math.max(36, Math.floor(offH * 0.26))
const lineGap = Math.floor(offH * 0.06)
const totalH = primarySize + (lines.length > 1 ? secondarySize + lineGap : 0)
const baseY = (offH - totalH) / 2
lines.forEach((line, i) => {
const size = i === 0 ? primarySize : secondarySize
offCtx.font = `${i === 0 ? 900 : 800} ${size}px "Inter", "SF Pro Display", system-ui, sans-serif`
offCtx.letterSpacing = i === 0 ? "0.05em" : "0.02em"
const y = i === 0
? baseY + primarySize / 2
: baseY + primarySize + lineGap + secondarySize / 2
offCtx.fillText(line, offW / 2, y)
})
const imgData = offCtx.getImageData(0, 0, offW, offH)
const textMap: boolean[][] = []
for (let gy = 0; gy < rows; gy++) {
textMap[gy] = []
for (let gx = 0; gx < cols; gx++) {
let sum = 0
for (let sy = 0; sy < scale; sy++) {
for (let sx = 0; sx < scale; sx++) {
sum += imgData.data[((gy * scale + sy) * offW + gx * scale + sx) * 4]
}
}
textMap[gy][gx] = sum / (scale * scale) > 60
}
}
const centerX = cols / 2
const centerY = rows / 2
const cells = Array.from({ length: rows }, (_, y) =>
Array.from({ length: cols }, (_, x) => {
const isText = textMap[y][x]
const dist = Math.hypot(x - centerX, y - centerY)
return {
opacity: 0,
target: isText ? 1.0 : 0.006 + Math.random() * 0.014,
delay: isText
? dist * 12 + Math.random() * 200
: Math.random() * 600,
isText,
}
})
)
let fg = document.documentElement.classList.contains("dark")
? "255,255,255"
: "5,5,5"
const observer = new MutationObserver(() => {
fg = document.documentElement.classList.contains("dark")
? "255,255,255"
: "5,5,5"
})
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
})
const startTime = performance.now()
let raf: number
function draw(now: number) {
const elapsed = now - startTime
drawCtx.clearRect(0, 0, width, height)
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const c = cells[y][x]
if (elapsed > c.delay) {
c.opacity += (c.target - c.opacity) * 0.06
}
if (c.opacity < 0.004) continue
drawCtx.fillStyle = `rgba(${fg},${c.opacity})`
drawCtx.fillRect(x * step, y * step, cellSize, cellSize)
}
}
raf = requestAnimationFrame(draw)
}
raf = requestAnimationFrame(draw)
return () => {
cancelAnimationFrame(raf)
observer.disconnect()
}
}, [lines])
return (
<canvas
ref={canvasRef}
className={className}
style={{ width: "100%", height: "100%", display: "block" }}
/>
)
}