Skip to content

Commit 0098240

Browse files
authored
feat: [sc-26234] Ideas for nameai.io - Video to ascii rainbow (#590)
* create basic video animatino example for nameai * nice * improve animation * fix text * improve video quality * remove comment * fix build * add github button, view the docs and npm install * Include rainbow video into repo * fix title z-index * list of all lowercase letters a-z * fadeIn effect on animation
1 parent 52f56f3 commit 0098240

File tree

8 files changed

+313
-11
lines changed

8 files changed

+313
-11
lines changed

apps/nameai.dev/app/page.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,51 @@
1-
import { Heading, Text } from "@namehash/namekit-react";
1+
import { Button, Heading, Link, Text } from "@namehash/namekit-react";
2+
import { HeroStartCommand } from "@/components/HeroStartCommand";
3+
import { GithubIcon } from "@/components/github-icon";
4+
import VideoAsciiAnimation from "@/components/VideoAsciiAnimation";
25

36
export default function Page() {
47
return (
58
<>
6-
<div className="bg-black py-12 md:py-32 lg:py-48">
7-
<div className="max-w-3xl mx-auto px-6">
8-
<div className="space-y-3 text-center">
9-
<Heading as="h1" className="text-white !text-6xl">
10-
Enable new ENS user experiences
11-
</Heading>
12-
<Text className="text-gray-400">What will you build?</Text>
9+
<div className="">
10+
<div className="w-screen h-[calc(100vh-65px)] pt-1 bg-white overflow-hidden relative">
11+
<div
12+
className="w-screen h-[calc(100vh-65px)] absolute top-0 left-0 z-10"
13+
style={{
14+
background:
15+
"radial-gradient(ellipse at center, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 10%, transparent 100%)",
16+
}}
17+
/>
18+
19+
<div className="absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 max-w-3xl mx-auto px-6 z-10">
20+
<div className="space-y-5 py-5 text-center bg-[radial-gradient(circle,white_80%,blue-500_90%,transparent_100%)]">
21+
<div className="space-y-2">
22+
<Heading as="h1" className="text-black !text-6xl">
23+
Enable new ENS user experiences
24+
</Heading>
25+
<Text className="text-gray-400">What will you build?</Text>
26+
</div>
27+
<div className="flex justify-center">
28+
<HeroStartCommand />
29+
</div>
30+
<div className="flex justify-center gap-2">
31+
<Button asChild>
32+
<Link target="_blank" href="https://api.nameai.dev/docs">
33+
View the docs
34+
</Link>
35+
</Button>
36+
<Button variant="secondary" asChild>
37+
<Link
38+
target="_blank"
39+
href="https://github.com/namehash/namekit/tree/main/packages/nameai-sdk"
40+
>
41+
<GithubIcon className="w-5 h-5" />
42+
Github
43+
</Link>
44+
</Button>
45+
</div>
46+
</div>
1347
</div>
48+
<VideoAsciiAnimation />
1449
</div>
1550
</div>
1651
</>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export const CopyIcon = () => (
2+
<>
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
width="21"
6+
height="20"
7+
viewBox="0 0 21 20"
8+
fill="none"
9+
className="block w-5 h-5 sm:hidden"
10+
>
11+
<path
12+
d="M14.25 6.875V5C14.25 3.96447 13.4105 3.125 12.375 3.125H5.5C4.46447 3.125 3.625 3.96447 3.625 5V11.875C3.625 12.9105 4.46447 13.75 5.5 13.75H7.375M14.25 6.875H15.5C16.5355 6.875 17.375 7.71447 17.375 8.75V15C17.375 16.0355 16.5355 16.875 15.5 16.875H9.25C8.21447 16.875 7.375 16.0355 7.375 15V13.75M14.25 6.875H9.25C8.21447 6.875 7.375 7.71447 7.375 8.75V13.75"
13+
stroke="#808080"
14+
strokeWidth="1.5"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
</svg>
19+
<svg
20+
xmlns="http://www.w3.org/2000/svg"
21+
fill="none"
22+
viewBox="0 0 24 24"
23+
strokeWidth="1.5"
24+
stroke="#808080"
25+
className="hidden w-6 h-6 transition hover:stroke-black sm:block"
26+
>
27+
<path
28+
strokeLinecap="round"
29+
strokeLinejoin="round"
30+
d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"
31+
/>
32+
</svg>
33+
</>
34+
);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { CopyIcon } from "./CopyIcon";
5+
import { CheckIcon } from "@heroicons/react/24/solid";
6+
7+
const npmCommand = "npm install @namehash/nameai";
8+
9+
export function HeroStartCommand() {
10+
const [copiedToClipboard, setCopiedToClipboard] = useState<boolean>(false);
11+
12+
const displayCopiedFor = 4000;
13+
14+
useEffect(() => {
15+
if (copiedToClipboard) {
16+
const timer = setTimeout(() => {
17+
setCopiedToClipboard(false);
18+
}, displayCopiedFor);
19+
return () => clearTimeout(timer);
20+
}
21+
}, [copiedToClipboard]);
22+
23+
return (
24+
<div className="hidden relative z-10 lg:flex items-center gap-2 py-[9px] pl-4 pr-[14px] rounded-lg bg-gray-100 border border-gray-300 sm:gap-3 sm:py-[13px] sm:pl-[20px] sm:pr-[16px]">
25+
<p className="text-black leading-6 font-normal text-sm sm:text-base">
26+
{npmCommand}
27+
</p>
28+
29+
<div
30+
className="w-fit h-fit z-10 cursor-pointer"
31+
onClick={() => {
32+
setCopiedToClipboard(true);
33+
navigator.clipboard.writeText(npmCommand);
34+
}}
35+
>
36+
{copiedToClipboard ? (
37+
<CheckIcon className="w-5 h-5 text-black" />
38+
) : (
39+
<CopyIcon />
40+
)}
41+
</div>
42+
</div>
43+
);
44+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"use client";
2+
3+
import React, { useEffect, useRef } from "react";
4+
5+
export default function VideoAsciiAnimation() {
6+
const videoRef = useRef<HTMLVideoElement>(null);
7+
const prerenderCanvasRef = useRef<HTMLCanvasElement>(null);
8+
const outputCanvasRef = useRef<HTMLCanvasElement>(null);
9+
10+
useEffect(() => {
11+
const video = videoRef.current;
12+
const prerender = prerenderCanvasRef.current;
13+
const outputCanvas = outputCanvasRef.current;
14+
if (!video || !prerender || !outputCanvas) return;
15+
16+
const preCtx = prerender.getContext("2d", { willReadFrequently: true });
17+
const outCtx = outputCanvas.getContext("2d");
18+
if (!preCtx || !outCtx) return;
19+
20+
const ratio = window.devicePixelRatio || 1;
21+
const computedStyle = getComputedStyle(outputCanvas);
22+
const cssWidth = parseInt(computedStyle.getPropertyValue("width"), 10);
23+
const cssHeight = parseInt(computedStyle.getPropertyValue("height"), 10);
24+
25+
// Adjust the canvas' backing store size:
26+
outputCanvas.width = cssWidth * ratio;
27+
outputCanvas.height = cssHeight * ratio;
28+
29+
// Scale the context so drawing operations use the proper pixel ratio
30+
outCtx.scale(ratio, ratio);
31+
32+
// Instead of fixed char scale factors, compute them to fill the entire canvas.
33+
// The prerender canvas resolution is used to decide how many "cells" the ASCII art will have.
34+
const prerenderWidth = prerender.width;
35+
const prerenderHeight = prerender.height;
36+
const charW = cssWidth / prerenderWidth;
37+
const charH = cssHeight / prerenderHeight;
38+
39+
// Set the font size to match the computed cell height
40+
outCtx.font = `${charH}px monospace`;
41+
outCtx.textBaseline = "top";
42+
43+
// 🎨 ENS Letters Character Set (lowercase a-z) plus ".eth"
44+
const charsFixed: string[] = [
45+
"a",
46+
"b",
47+
"c",
48+
"d",
49+
"e",
50+
"f",
51+
"g",
52+
"h",
53+
"i",
54+
"j",
55+
"k",
56+
"l",
57+
"m",
58+
"n",
59+
"o",
60+
"p",
61+
"q",
62+
"r",
63+
"s",
64+
"t",
65+
"u",
66+
"v",
67+
"w",
68+
"x",
69+
"y",
70+
"z",
71+
];
72+
73+
let chars: (string | string[])[] = [...charsFixed];
74+
const charsLength = chars.length;
75+
const MAX_COLOR_INDEX = 255;
76+
77+
let animationFrameId: number;
78+
const FRAME_INTERVAL = 1000 / 30;
79+
let lastDrawTime = 0;
80+
81+
function updateCanvas(timestamp: number) {
82+
if (timestamp - lastDrawTime < FRAME_INTERVAL) {
83+
animationFrameId = requestAnimationFrame(updateCanvas);
84+
return;
85+
}
86+
87+
const w = prerender!.width;
88+
const h = prerender!.height;
89+
if (!w || !h || video!.paused) {
90+
animationFrameId = requestAnimationFrame(updateCanvas);
91+
return;
92+
}
93+
94+
preCtx!.drawImage(video!, 0, 0, w, h);
95+
const data = preCtx!.getImageData(0, 0, w, h).data;
96+
97+
// Clear the output canvas
98+
outCtx!.clearRect(0, 0, outputCanvas!.width, outputCanvas!.height);
99+
100+
// Draw the ASCII art to the output canvas, scaling each "cell" to fully cover the canvas
101+
for (let y = 0; y < h; y++) {
102+
for (let x = 0; x < w; x++) {
103+
const index = (x + y * w) * 4;
104+
const r = data[index];
105+
const g = data[index + 1];
106+
const b = data[index + 2];
107+
const brightness = (r + g + b) / 3;
108+
const charIndex = Math.floor(
109+
(charsLength * brightness) / MAX_COLOR_INDEX,
110+
);
111+
const result = chars[charIndex];
112+
const char = Array.isArray(result)
113+
? result[Math.floor(Math.random() * result.length)]
114+
: result;
115+
if (!char) continue;
116+
117+
outCtx!.fillStyle = `rgb(${r}, ${g}, ${b})`;
118+
outCtx!.fillText(char, x * charW, y * charH);
119+
}
120+
}
121+
122+
lastDrawTime = timestamp;
123+
animationFrameId = requestAnimationFrame(updateCanvas);
124+
}
125+
126+
video.play().then(() => {
127+
animationFrameId = requestAnimationFrame(updateCanvas);
128+
});
129+
130+
return () => cancelAnimationFrame(animationFrameId);
131+
}, []);
132+
133+
return (
134+
<div className="relative w-full h-full animate-fadeIn">
135+
{/* Output canvas for displaying ASCII art */}
136+
<canvas
137+
ref={outputCanvasRef}
138+
width="1200" // This attribute can be used as a fallback resolution.
139+
height="640"
140+
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full"
141+
/>
142+
143+
{/* Hidden video and prerender canvases */}
144+
<video
145+
ref={videoRef}
146+
autoPlay
147+
muted
148+
loop
149+
playsInline
150+
crossOrigin="anonymous"
151+
className="hidden"
152+
>
153+
<source src="/rainbow.mp4" type="video/mp4" />
154+
</video>
155+
156+
{/* Hidden prerender canvas used for processing the video/image data */}
157+
<canvas
158+
ref={prerenderCanvasRef}
159+
width="240" // The resolution of the ASCII art "cells".
160+
height="80"
161+
className="hidden"
162+
/>
163+
164+
<style>{`
165+
body {
166+
margin: 0;
167+
padding: 0;
168+
background: white;
169+
font-family: "Courier New", monospace;
170+
}
171+
`}</style>
172+
</div>
173+
);
174+
}

apps/nameai.dev/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"react": "19.0.0",
1919
"react-dom": "19.0.0",
2020
"react-wrap-balancer": "1.1.1",
21-
"sonner": "1.5.0"
21+
"sonner": "1.5.0",
22+
"tweakpane": "4.0.5"
2223
},
2324
"devDependencies": {
2425
"@types/node": "^22.10.2",

apps/nameai.dev/public/rainbow.mp4

709 KB
Binary file not shown.

apps/nameai.dev/tailwind.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ export default {
2222
from: { transform: "translateX(0)" },
2323
to: { transform: "translateX(-100%)" },
2424
},
25+
fadeIn: {
26+
"0%": { opacity: "0" },
27+
"50%": { opacity: "0" },
28+
"100%": { opacity: "1" },
29+
},
2530
},
2631
animation: {
2732
carousel: "carousel 30s linear infinite",
33+
fadeIn: "fadeIn 1s linear forwards",
2834
},
2935
},
3036
},

pnpm-lock.yaml

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)