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>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"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" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user