Retro Grid
Retro Grid é um componente de efeito para React e Tailwind CSS, da biblioteca Magic UI, com licença MIT. Copia e cola no teu projeto.
Magic UI
MIT
effects
O que é
Retro Grid é um componente de efeito para React e Tailwind CSS, da biblioteca Magic UI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Não precisa de pacotes além do que o projeto já tem
Licença
Este componente vem de Magic UI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Retro Grid
"use client"
import { useEffect, useRef, useState } from "react"
import type { CSSProperties, HTMLAttributes } from "react"
import { cn } from "@/lib/utils"
const ANIMATION_DURATION_SECONDS = 15
const GRID_HEIGHT_RATIO = 3
const GRID_LINE_ALIGNMENT_OFFSET_PX = 0.5
const GRID_LINE_ANTIALIAS_MULTIPLIER = 0.9
const GRID_LINE_WIDTH_PX = 0.92
const GRID_START_OFFSET_RATIO = -0.5
const GRID_WIDTH_RATIO = 6
const GRID_X_OFFSET_RATIO = -2
const MAX_ANGLE = 89
const MAX_DEVICE_PIXEL_RATIO = 2
const MIN_ANGLE = 1
const PERSPECTIVE_PX = 200
const FALLBACK_ANIMATION_NAME = "retro-grid-fallback-scroll"
const FALLBACK_STYLES = `
@keyframes ${FALLBACK_ANIMATION_NAME} {
from {
transform: translateY(-50%);
}
to {
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
[data-retro-grid-scroll="true"] {
animation: none !important;
transform: translateY(-50%) !important;
}
}
`
const VERTEX_SHADER_SOURCE = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`
const FRAGMENT_SHADER_SOURCE = `
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform vec2 u_container_size;
uniform vec2 u_viewport_size;
uniform vec4 u_line_color;
uniform float u_angle;
uniform float u_cell_size;
uniform float u_device_pixel_ratio;
uniform float u_time;
const float animationDurationSeconds = ${ANIMATION_DURATION_SECONDS.toFixed(1)};
const float gridHeightRatio = ${GRID_HEIGHT_RATIO.toFixed(1)};
const float gridStartOffsetRatio = ${GRID_START_OFFSET_RATIO.toFixed(1)};
const float gridWidthRatio = ${GRID_WIDTH_RATIO.toFixed(1)};
const float gridXOffsetRatio = ${GRID_X_OFFSET_RATIO.toFixed(1)};
const float gridLineAlignmentOffsetPx = ${GRID_LINE_ALIGNMENT_OFFSET_PX.toFixed(1)};
const float gridLineAntialiasMultiplier = ${GRID_LINE_ANTIALIAS_MULTIPLIER.toFixed(1)};
const float horizontalLodLevelOneEndPx = 5.6;
const float horizontalLodLevelOneStartPx = 2.8;
const float horizontalLodLevelTwoEndPx = 3.0;
const float horizontalLodLevelTwoStartPx = 1.4;
const float horizontalCompressionEndPx = 2.8;
const float horizontalCompressionStartPx = 1.2;
const float lineWidthPx = ${GRID_LINE_WIDTH_PX.toFixed(2)};
const float perspectivePx = ${PERSPECTIVE_PX.toFixed(1)};
const float gridTravelRatio = 0.5;
const float verticalCompressionEndPx = 2.6;
const float verticalCompressionStartPx = 1.0;
const float verticalEdgeCompressionEnd = 0.95;
const float verticalEdgeCompressionStart = 0.45;
const float verticalLodLevelEnd = 0.64;
const float verticalLodLevelStart = 0.22;
const float verticalTopCompressionEndCells = 6.0;
const float verticalTopCompressionStartCells = 2.0;
float renderGridLine(
float wrappedCoord,
float antiAliasWidth,
float softnessBoost
) {
return 1.0 - smoothstep(
lineWidthPx,
lineWidthPx + (antiAliasWidth * (1.5 + softnessBoost)),
wrappedCoord
);
}
void main() {
float angle = radians(clamp(u_angle, 1.0, 89.0));
float sinAngle = sin(angle);
float cosAngle = cos(angle);
vec2 screen = vec2(
(gl_FragCoord.x / u_device_pixel_ratio) - (u_container_size.x * 0.5),
(u_container_size.y * 0.5) - (gl_FragCoord.y / u_device_pixel_ratio)
);
vec3 rayOrigin = vec3(0.0, 0.0, perspectivePx);
vec3 rayDirection = normalize(vec3(screen, -perspectivePx));
vec3 planeXAxis = vec3(1.0, 0.0, 0.0);
vec3 planeYAxis = vec3(0.0, cosAngle, sinAngle);
vec3 planeNormal = normalize(cross(planeXAxis, planeYAxis));
float denominator = dot(rayDirection, planeNormal);
if (abs(denominator) < 0.0001) {
discard;
}
float distanceToPlane = dot(-rayOrigin, planeNormal) / denominator;
if (distanceToPlane <= 0.0) {
discard;
}
vec3 hitPoint = rayOrigin + (rayDirection * distanceToPlane);
float localX = hitPoint.x;
float localY = dot(hitPoint, planeYAxis);
float gridWidth = u_viewport_size.x * gridWidthRatio;
float gridHeight = u_viewport_size.y * gridHeightRatio;
float gridScrollSpeed = (gridHeight * gridTravelRatio) / animationDurationSeconds;
float patternOffsetY = u_time * gridScrollSpeed;
float gridLeft = (-0.5 * u_container_size.x) + (gridXOffsetRatio * u_container_size.x);
float gridTop = (-0.5 * u_container_size.y) + (gridStartOffsetRatio * gridHeight);
vec2 planePosition = vec2(localX - gridLeft, localY - gridTop);
if (
planePosition.x < 0.0 ||
planePosition.y < 0.0 ||
planePosition.x > gridWidth ||
planePosition.y > gridHeight
) {
discard;
}
vec2 patternPosition = vec2(planePosition.x, planePosition.y - patternOffsetY);
vec2 wrapped = mod(
patternPosition + vec2(gridLineAlignmentOffsetPx),
u_cell_size
);
vec2 patternDerivative = max(fwidth(patternPosition), vec2(0.0001));
vec2 antiAliasWidth = patternDerivative * gridLineAntialiasMultiplier;
float horizontalCellSpanPx = u_cell_size / patternDerivative.y;
float horizontalCompression = 1.0 - smoothstep(
horizontalCompressionStartPx,
horizontalCompressionEndPx,
horizontalCellSpanPx
);
float verticalCellSpanPx = u_cell_size / patternDerivative.x;
float sideDistance = abs((planePosition.x / gridWidth) * 2.0 - 1.0);
float verticalEdgeCompression = smoothstep(
verticalEdgeCompressionStart,
verticalEdgeCompressionEnd,
sideDistance
);
float verticalTopCompression = 1.0 - smoothstep(
u_cell_size * verticalTopCompressionStartCells,
u_cell_size * verticalTopCompressionEndCells,
planePosition.y
);
float verticalCompression =
(1.0 - smoothstep(
verticalCompressionStartPx,
verticalCompressionEndPx,
verticalCellSpanPx
)) * verticalEdgeCompression * verticalTopCompression;
float horizontalSoftnessBoost = 1.0 + (horizontalCompression * 3.0);
float verticalSoftnessBoost = 1.0 + (verticalCompression * 3.5);
float verticalLod = smoothstep(
verticalLodLevelStart,
verticalLodLevelEnd,
verticalCompression
);
float verticalLineFine = renderGridLine(
wrapped.x,
antiAliasWidth.x,
verticalSoftnessBoost
);
float verticalWrappedLod = mod(
patternPosition.x + gridLineAlignmentOffsetPx,
u_cell_size * 2.0
);
float verticalLineCoarse = renderGridLine(
verticalWrappedLod,
antiAliasWidth.x,
verticalSoftnessBoost + verticalLod
);
float verticalLine = max(
verticalLineFine * (1.0 - verticalLod),
verticalLineCoarse * verticalLod
);
float horizontalLodLevelOne = 1.0 - smoothstep(
horizontalLodLevelOneStartPx,
horizontalLodLevelOneEndPx,
horizontalCellSpanPx
);
float horizontalLodLevelTwo = 1.0 - smoothstep(
horizontalLodLevelTwoStartPx,
horizontalLodLevelTwoEndPx,
horizontalCellSpanPx
);
float horizontalLineFine = renderGridLine(
wrapped.y,
antiAliasWidth.y,
horizontalSoftnessBoost
);
float horizontalWrappedLodOne = mod(
patternPosition.y + gridLineAlignmentOffsetPx,
u_cell_size * 2.0
);
float horizontalWrappedLodTwo = mod(
patternPosition.y + gridLineAlignmentOffsetPx,
u_cell_size * 4.0
);
float horizontalLineCoarse = renderGridLine(
horizontalWrappedLodOne,
antiAliasWidth.y,
horizontalSoftnessBoost + horizontalLodLevelOne
);
float horizontalLineExtraCoarse = renderGridLine(
horizontalWrappedLodTwo,
antiAliasWidth.y,
horizontalSoftnessBoost + horizontalLodLevelOne + horizontalLodLevelTwo
);
float horizontalLineReduced = max(
horizontalLineFine * (1.0 - horizontalLodLevelOne),
horizontalLineCoarse * horizontalLodLevelOne
);
float horizontalLine = max(
horizontalLineReduced * (1.0 - horizontalLodLevelTwo),
horizontalLineExtraCoarse * horizontalLodLevelTwo
);
float line = max(verticalLine, horizontalLine);
if (line <= 0.001) {
discard;
}
float alpha = u_line_color.a * line;
gl_FragColor = vec4(u_line_color.rgb * alpha, alpha);
}
`
interface RetroGridProps extends HTMLAttributes<HTMLDivElement> {
/**
* Additional CSS classes to apply to the grid container
*/
className?: string
/**
* Rotation angle of the grid in degrees
* @default 65
*/
angle?: number
/**
* Grid cell size in pixels
* @default 60
*/
cellSize?: number
/**
* Grid opacity value between 0 and 1
* @default 0.5
*/
opacity?: number
/**
* Grid line color in light mode
* @default "gray"
*/
lightLineColor?: string
/**
* Grid line color in dark mode
* @default "gray"
*/
darkLineColor?: string
}
interface ProgramInfo {
attributeLocation: number
program: WebGLProgram
uniforms: {
angle: WebGLUniformLocation
cellSize: WebGLUniformLocation
containerSize: WebGLUniformLocation
devicePixelRatio: WebGLUniformLocation
lineColor: WebGLUniformLocation
time: WebGLUniformLocation
viewportSize: WebGLUniformLocation
}
}
let colorResolveContext: CanvasRenderingContext2D | null | undefined
function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max)
}
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type)
if (!shader) {
return null
}
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
return shader
}
gl.deleteShader(shader)
return null
}
function createProgram(gl: WebGLRenderingContext) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER_SOURCE)
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
FRAGMENT_SHADER_SOURCE
)
if (!vertexShader || !fragmentShader) {
return null
}
const program = gl.createProgram()
if (!program) {
gl.deleteShader(vertexShader)
gl.deleteShader(fragmentShader)
return null
}
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
gl.deleteShader(vertexShader)
gl.deleteShader(fragmentShader)
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
return program
}
gl.deleteProgram(program)
return null
}
function getProgramInfo(
gl: WebGLRenderingContext,
program: WebGLProgram
): ProgramInfo | null {
const attributeLocation = gl.getAttribLocation(program, "a_position")
const angle = gl.getUniformLocation(program, "u_angle")
const cellSize = gl.getUniformLocation(program, "u_cell_size")
const containerSize = gl.getUniformLocation(program, "u_container_size")
const devicePixelRatio = gl.getUniformLocation(
program,
"u_device_pixel_ratio"
)
const lineColor = gl.getUniformLocation(program, "u_line_color")
const time = gl.getUniformLocation(program, "u_time")
const viewportSize = gl.getUniformLocation(program, "u_viewport_size")
if (
attributeLocation < 0 ||
!angle ||
!cellSize ||
!containerSize ||
!devicePixelRatio ||
!lineColor ||
!time ||
!viewportSize
) {
return null
}
return {
attributeLocation,
program,
uniforms: {
angle,
cellSize,
containerSize,
devicePixelRatio,
lineColor,
time,
viewportSize,
},
}
}
function isDarkMode(colorScheme: MediaQueryList) {
const root = document.documentElement
if (root.classList.contains("dark")) {
return true
}
if (root.classList.contains("light")) {
return false
}
return colorScheme.matches
}
function getColorResolveContext() {
if (colorResolveContext !== undefined) {
return colorResolveContext
}
const canvas = document.createElement("canvas")
canvas.width = 1
canvas.height = 1
colorResolveContext = canvas.getContext("2d", {
willReadFrequently: true,
})
return colorResolveContext
}
function resolveLineColor(color: string, element: HTMLElement) {
const resolver = document.createElement("span")
resolver.style.color = color
resolver.style.opacity = "0"
resolver.style.pointerEvents = "none"
resolver.style.position = "absolute"
element.appendChild(resolver)
const resolvedColor = getComputedStyle(resolver).color
resolver.remove()
const context = getColorResolveContext()
if (!context) {
return new Float32Array([0.5, 0.5, 0.5, 1])
}
context.clearRect(0, 0, 1, 1)
context.fillStyle = resolvedColor
context.fillRect(0, 0, 1, 1)
const pixel = context.getImageData(0, 0, 1, 1).data
return new Float32Array([
pixel[0] / 255,
pixel[1] / 255,
pixel[2] / 255,
pixel[3] / 255,
])
}
function createFallbackGridStyle(
cellSize: number,
lineColor: string
): CSSProperties {
return {
animation: `${FALLBACK_ANIMATION_NAME} ${ANIMATION_DURATION_SECONDS}s linear infinite`,
backgroundImage: `linear-gradient(to right, ${lineColor} 1px, transparent 0), linear-gradient(to bottom, ${lineColor} 1px, transparent 0)`,
backgroundRepeat: "repeat",
backgroundSize: `${cellSize}px ${cellSize}px`,
transform: "translateY(-50%)",
}
}
export function RetroGrid({
className,
angle = 65,
cellSize = 60,
opacity = 0.5,
lightLineColor = "gray",
darkLineColor = "gray",
style,
...props
}: RetroGridProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [isWebGlReady, setIsWebGlReady] = useState(false)
const angleRef = useRef(angle)
const cellSizeRef = useRef(cellSize)
const darkLineColorRef = useRef(darkLineColor)
const lightLineColorRef = useRef(lightLineColor)
const syncSceneRef = useRef<(() => void) | null>(null)
useEffect(() => {
angleRef.current = angle
cellSizeRef.current = cellSize
darkLineColorRef.current = darkLineColor
lightLineColorRef.current = lightLineColor
syncSceneRef.current?.()
}, [angle, cellSize, darkLineColor, lightLineColor])
useEffect(() => {
const canvas = canvasRef.current
const container = containerRef.current
if (!canvas || !container) {
return
}
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)")
const colorScheme = window.matchMedia("(prefers-color-scheme: dark)")
let animationFrameId: number | null = null
let currentWidth = 0
let currentHeight = 0
let currentDevicePixelRatio = 1
let gl: WebGLRenderingContext | null = null
let isVisible = true
let isContextLost = false
let lineColor = resolveLineColor(lightLineColorRef.current, container)
let positionBuffer: WebGLBuffer | null = null
let programInfo: ProgramInfo | null = null
const getContext = () => {
const nextGl = canvas.getContext("webgl", {
alpha: true,
antialias: true,
premultipliedAlpha: true,
})
if (!nextGl || !nextGl.getExtension("OES_standard_derivatives")) {
return null
}
return nextGl
}
const releasePipeline = (shouldDeleteResources: boolean) => {
if (shouldDeleteResources && gl) {
if (positionBuffer) {
gl.deleteBuffer(positionBuffer)
}
if (programInfo) {
gl.deleteProgram(programInfo.program)
}
}
positionBuffer = null
programInfo = null
if (shouldDeleteResources) {
gl = null
}
}
const initializePipeline = () => {
const nextGl = getContext()
if (!nextGl) {
releasePipeline(false)
return false
}
gl = nextGl
releasePipeline(true)
gl = nextGl
const program = createProgram(nextGl)
if (!program) {
return false
}
const nextProgramInfo = getProgramInfo(nextGl, program)
if (!nextProgramInfo) {
nextGl.deleteProgram(program)
return false
}
const nextPositionBuffer = nextGl.createBuffer()
if (!nextPositionBuffer) {
nextGl.deleteProgram(program)
return false
}
nextGl.bindBuffer(nextGl.ARRAY_BUFFER, nextPositionBuffer)
nextGl.bufferData(
nextGl.ARRAY_BUFFER,
new Float32Array([-1, -1, 3, -1, -1, 3]),
nextGl.STATIC_DRAW
)
positionBuffer = nextPositionBuffer
programInfo = nextProgramInfo
return true
}
const updateLineColor = () => {
const activeColor = isDarkMode(colorScheme)
? darkLineColorRef.current
: lightLineColorRef.current
lineColor = resolveLineColor(activeColor, container)
}
const resizeCanvas = () => {
currentWidth = Math.floor(container.clientWidth)
currentHeight = Math.floor(container.clientHeight)
if (currentWidth === 0 || currentHeight === 0 || !gl) {
return
}
currentDevicePixelRatio = Math.min(
window.devicePixelRatio || 1,
MAX_DEVICE_PIXEL_RATIO
)
canvas.width = Math.floor(currentWidth * currentDevicePixelRatio)
canvas.height = Math.floor(currentHeight * currentDevicePixelRatio)
canvas.style.width = `${currentWidth}px`
canvas.style.height = `${currentHeight}px`
gl.viewport(0, 0, canvas.width, canvas.height)
}
const draw = (timestamp: number) => {
if (
currentWidth === 0 ||
currentHeight === 0 ||
!gl ||
!positionBuffer ||
!programInfo ||
isContextLost
) {
return
}
gl.useProgram(programInfo.program)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
gl.enableVertexAttribArray(programInfo.attributeLocation)
gl.vertexAttribPointer(
programInfo.attributeLocation,
2,
gl.FLOAT,
false,
0,
0
)
gl.clearColor(0, 0, 0, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.uniform1f(
programInfo.uniforms.angle,
clamp(angleRef.current, MIN_ANGLE, MAX_ANGLE)
)
gl.uniform1f(
programInfo.uniforms.cellSize,
Math.max(cellSizeRef.current, 1)
)
gl.uniform2f(
programInfo.uniforms.containerSize,
currentWidth,
currentHeight
)
gl.uniform1f(
programInfo.uniforms.devicePixelRatio,
currentDevicePixelRatio
)
gl.uniform4fv(programInfo.uniforms.lineColor, lineColor)
gl.uniform1f(
programInfo.uniforms.time,
reducedMotion.matches ? 0 : timestamp / 1000
)
gl.uniform2f(
programInfo.uniforms.viewportSize,
window.innerWidth,
window.innerHeight
)
gl.drawArrays(gl.TRIANGLES, 0, 3)
}
const stopAnimation = () => {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
}
const frame = (timestamp: number) => {
draw(timestamp)
if (!reducedMotion.matches && isVisible) {
animationFrameId = requestAnimationFrame(frame)
return
}
animationFrameId = null
}
const syncScene = () => {
if (isContextLost) {
stopAnimation()
setIsWebGlReady(false)
return
}
if (!gl || !positionBuffer || !programInfo) {
if (!initializePipeline()) {
stopAnimation()
setIsWebGlReady(false)
return
}
}
resizeCanvas()
if (currentWidth === 0 || currentHeight === 0) {
stopAnimation()
return
}
updateLineColor()
draw(performance.now())
setIsWebGlReady(true)
if (reducedMotion.matches || !isVisible) {
stopAnimation()
return
}
if (animationFrameId === null) {
animationFrameId = requestAnimationFrame(frame)
}
}
syncSceneRef.current = syncScene
const resizeObserver = new ResizeObserver(() => {
syncScene()
})
resizeObserver.observe(container)
const handleWindowResize = () => {
syncScene()
}
const intersectionObserver = new IntersectionObserver(([entry]) => {
isVisible = entry?.isIntersecting ?? false
if (isVisible) {
syncScene()
return
}
stopAnimation()
})
intersectionObserver.observe(container)
const themeObserver = new MutationObserver(() => {
syncScene()
})
themeObserver.observe(document.documentElement, {
attributeFilter: ["class"],
attributes: true,
})
const handleMotionChange = () => {
syncScene()
}
const handleColorSchemeChange = () => {
syncScene()
}
const handleContextLost = (event: Event) => {
event.preventDefault()
isContextLost = true
stopAnimation()
releasePipeline(false)
setIsWebGlReady(false)
}
const handleContextRestored = () => {
isContextLost = false
syncScene()
}
reducedMotion.addEventListener("change", handleMotionChange)
colorScheme.addEventListener("change", handleColorSchemeChange)
window.addEventListener("resize", handleWindowResize)
canvas.addEventListener("webglcontextlost", handleContextLost)
canvas.addEventListener("webglcontextrestored", handleContextRestored)
syncScene()
return () => {
stopAnimation()
resizeObserver.disconnect()
intersectionObserver.disconnect()
themeObserver.disconnect()
reducedMotion.removeEventListener("change", handleMotionChange)
colorScheme.removeEventListener("change", handleColorSchemeChange)
window.removeEventListener("resize", handleWindowResize)
canvas.removeEventListener("webglcontextlost", handleContextLost)
canvas.removeEventListener("webglcontextrestored", handleContextRestored)
syncSceneRef.current = null
releasePipeline(!isContextLost)
}
}, [])
const gridStyles = {
...style,
opacity,
} as CSSProperties
const normalizedAngle = clamp(angle, MIN_ANGLE, MAX_ANGLE)
const normalizedCellSize = Math.max(cellSize, 1)
const fallbackProjectionStyles = {
perspective: `${PERSPECTIVE_PX}px`,
} as CSSProperties
const fallbackRotationStyles = {
transform: `rotateX(${normalizedAngle}deg)`,
} as CSSProperties
const lightFallbackGridStyles = createFallbackGridStyle(
normalizedCellSize,
lightLineColor
)
const darkFallbackGridStyles = createFallbackGridStyle(
normalizedCellSize,
darkLineColor
)
return (
<div
ref={containerRef}
className={cn(
"pointer-events-none absolute size-full overflow-hidden",
className
)}
style={gridStyles}
{...props}
>
<style>{FALLBACK_STYLES}</style>
{!isWebGlReady ? (
<div className="absolute inset-0" style={fallbackProjectionStyles}>
<div className="absolute inset-0" style={fallbackRotationStyles}>
<div
data-retro-grid-scroll="true"
className="absolute inset-[0%_0px] ml-[-200%] h-[300vh] w-[600vw] origin-[100%_0_0] dark:hidden"
style={lightFallbackGridStyles}
/>
<div
data-retro-grid-scroll="true"
className="absolute inset-[0%_0px] ml-[-200%] hidden h-[300vh] w-[600vw] origin-[100%_0_0] dark:block"
style={darkFallbackGridStyles}
/>
</div>
</div>
) : null}
<canvas
ref={canvasRef}
className={cn(
"absolute inset-0 size-full",
isWebGlReady ? "opacity-100" : "opacity-0"
)}
/>
<div className="absolute inset-0 bg-linear-to-t from-white to-transparent to-90% dark:from-black" />
</div>
)
}