Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<section id="logo" align="center">
<a href="https://animata.design/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/8d3da830-d1b0-4327-8695-edcea3534743">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/44be7a44-2e1f-4b90-a238-5cd10a376748">
<img alt="Animata Logo" src="https://user-images.githubusercontent.com/25423296/163456779-a8556205-d0a5-45e2-ac17-42d089e3c3f8.png" width="512">
<source media="(prefers-color-scheme: dark)" srcset="public/animata-logo-smil-dark.svg" />
<img alt="Animata Logo" src="public/animata-logo-smil.svg" width="512" height="215" />
</picture>
</a>
<section>
Expand Down
112 changes: 55 additions & 57 deletions animata/background/animated-beam.tsx
Original file line number Diff line number Diff line change
@@ -1,91 +1,89 @@
"use client";
import { cn } from "@/lib/utils";

import { useEffect, useRef, useState } from "react";
// Deterministic 0–1 pseudo-random from an integer — same on server and client,
// so each beam gets its own length/speed/etc. without a hydration mismatch.
const prand = (n: number) => {
const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
};
const lerp = (min: number, max: number, t: number) => min + (max - min) * t;

import { cn } from "@/lib/utils";
// Enough 40px lanes to cover wide screens; extras are clipped by overflow-hidden,
// so the line count stays responsive without measuring the container.
const LANES = 64;

function Beam({ index }: { index: number }) {
const flag = index % 8 === 0;
// Every dimension is varied per beam and exposed as a CSS variable.
// ~30% are quick zips (1.2–3.2s); the rest drift slowly (6–14s).
const fast = prand(index + 91) < 0.3;
const duration = fast ? lerp(1.2, 3.2, prand(index)) : lerp(6, 14, prand(index));
const delay = lerp(0, 9, prand(index + 13));
const length = Math.round(lerp(24, 84, prand(index + 29))); // streak length
const width = Math.round(lerp(3, 7, prand(index + 53)));
// fast streaks read brighter, like real meteors
const opacity = fast ? lerp(0.8, 1, prand(index + 71)) : lerp(0.35, 0.9, prand(index + 71));

return (
<div
className={cn("h-full", {
"[--duration:7s]": flag,
"[--duration:11s]": !flag,
})}
style={{
width: "6px",
transform: "translateY(-20%)",
"--delay": `${index * 0.5}s`,
animation: "meteor var(--duration) var(--delay) ease-in-out infinite",
}}
className="ab-beam h-full"
style={
{
"--duration": `${duration.toFixed(2)}s`,
"--delay": `${delay.toFixed(2)}s`,
"--length": `${length}px`,
"--opacity": opacity.toFixed(2),
width: `${width}px`,
transform: "translateY(-20%)",
// `backwards` shows the 0% keyframe (opacity 0) during the delay, so a
// beam never peeks at its resting position before its run begins.
animation: "meteor var(--duration) var(--delay) ease-in-out infinite backwards",
} as React.CSSProperties
}
>
<div
className="w-full"
style={{
height: "var(--length)",
clipPath: "polygon(54% 0, 54% 0, 60% 100%, 40% 100%)",
}}
className={cn("w-full", {
"h-8": flag,
"h-12": !flag,
})}
>
<div className="h-full w-full bg-linear-to-b from-neutral-50/50 via-neutral-100 via-75% to-neutral-50" />
</div>
</div>
);
}

function useGridCount() {
const containerRef = useRef<HTMLDivElement>(null);
const [count, setCount] = useState(0);

useEffect(() => {
const updateCount = () => {
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) {
return;
}
const width = rect.width;
const cellSize = 40;
setCount(Math.ceil(width / cellSize));
};

updateCount();

// Can be debounced if needed
window.addEventListener("resize", updateCount);
return () => window.removeEventListener("resize", updateCount);
}, []);

return {
count,
containerRef,
};
}

function Background() {
const { count, containerRef } = useGridCount();

return (
<div
ref={containerRef}
className="z-0 absolute inset-0 flex h-full w-full flex-row justify-between bg-linear-to-t from-indigo-900 to-indigo-950"
>
<div className="absolute inset-0 z-0 flex flex-row justify-center overflow-hidden bg-linear-to-t from-indigo-900 to-indigo-950">
<style>{`
@keyframes meteor {
0% { transform: translateY(-20%) translateX(-50%); }
100% { transform: translateY(300%) translateX(-50%); }
0% { transform: translateY(-20%) translateX(-50%); opacity: 0; }
12% { opacity: var(--opacity, 1); }
88% { opacity: var(--opacity, 1); }
100% { transform: translateY(300%) translateX(-50%); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.ab-beam { animation: none !important; opacity: 0 !important; }
}
`}</style>

{/* soft glow */}
<div
style={{
background:
"radial-gradient(50% 50% at 50% 50%,#072a39 0%,rgb(7,42,57) 50%,rgba(7,42,57,0) 100%)",
}}
className="absolute inset-0 top-1/2 h-full w-full rounded-full opacity-40"
/>
{Array.from({ length: count }, (_, i) => (
<div key={i} className="relative h-full w-px rotate-12 bg-gray-100/10">
{(1 + i) % 4 === 0 && <Beam index={i + 1} />}

{/* fixed-width lanes → constant 40px spacing, clipped to whatever fits */}
{Array.from({ length: LANES }, (_, i) => (
<div key={i} className="flex h-full w-10 shrink-0 justify-center">
<div className="relative h-full w-px rotate-12 bg-gray-100/10">
{/* beam rides this diagonal line; ~30% of lanes get one */}
{prand(i + 7) < 0.3 && <Beam index={i + 1} />}
</div>
</div>
))}
</div>
Expand Down
26 changes: 26 additions & 0 deletions animata/background/shooting-stars.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/react";

import ShootingStars from "@/animata/background/shooting-stars";

const meta = {
title: "Background/Shooting Stars",
component: ShootingStars,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
} satisfies Meta<typeof ShootingStars>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
children: (
<div className="flex min-h-96 w-full flex-col items-center justify-center text-center text-white">
<h2 className="text-3xl font-semibold tracking-tight md:text-5xl">Make a wish</h2>
<p className="mt-3 text-sm text-white/60">Watch the sky — they keep falling.</p>
</div>
),
},
};
Loading