Skip to content
Draft
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
51 changes: 43 additions & 8 deletions apps/nameai.dev/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
import { Heading, Text } from "@namehash/namekit-react";
import { Button, Heading, Link, Text } from "@namehash/namekit-react";
import AsciiVideo from "../components/video-animation/video-animation";
import { HeroStartCommand } from "@/components/HeroStartCommand";
import { GithubIcon } from "@/components/github-icon";

export default function Page() {
return (
<>
<div className="bg-black py-12 md:py-32 lg:py-48">
<div className="max-w-3xl mx-auto px-6">
<div className="space-y-3 text-center">
<Heading as="h1" className="text-white !text-6xl">
Enable new ENS user experiences
</Heading>
<Text className="text-gray-400">What will you build?</Text>
<div className="">
<div className="w-screen h-[calc(100vh-65px)] pt-1 bg-white overflow-hidden relative">
<div
className="w-screen h-[calc(100vh-65px)] absolute top-0 left-0 z-10"
style={{
background:
"radial-gradient(ellipse at center, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 10%, transparent 100%)",
}}
></div>

<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-50">
<div className="space-y-5 py-5 text-center bg-[radial-gradient(circle,white_80%,blue-500_90%,transparent_100%)]">
<div className="space-y-2">
<Heading as="h1" className="text-black !text-6xl">
Enable new ENS user experiences
</Heading>
<Text className="text-gray-400">What will you build?</Text>
</div>
<div className="flex justify-center">
<HeroStartCommand />
</div>
<div className="flex justify-center gap-2">
<Button asChild>
<Link target="_blank" href="https://api.nameai.dev/docs">
View the docs
</Link>
</Button>
<Button variant="secondary" asChild>
<Link
target="_blank"
href="https://github.com/namehash/namekit/tree/main/packages/nameai-sdk"
>
<GithubIcon className="w-5 h-5" />
Github
</Link>
</Button>
</div>
</div>
</div>
<AsciiVideo />
</div>
</div>
</>
Expand Down
34 changes: 34 additions & 0 deletions apps/nameai.dev/components/CopyIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const CopyIcon = () => (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="20"
viewBox="0 0 21 20"
fill="none"
className="block w-5 h-5 sm:hidden"
>
<path
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"
stroke="#808080"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="#808080"
className="hidden w-6 h-6 transition hover:stroke-black sm:block"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
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"
/>
</svg>
</>
);
44 changes: 44 additions & 0 deletions apps/nameai.dev/components/HeroStartCommand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { useEffect, useState } from "react";
import { CopyIcon } from "./CopyIcon";
import { CheckIcon } from "@heroicons/react/24/solid";

const npmCommand = "npm install @namehash/nameai";

export function HeroStartCommand() {
const [copiedToClipboard, setCopiedToClipboard] = useState<boolean>(false);

const displayCopiedFor = 4000;

useEffect(() => {
if (copiedToClipboard) {
const timer = setTimeout(() => {
setCopiedToClipboard(false);
}, displayCopiedFor);
return () => clearTimeout(timer);
}
}, [copiedToClipboard]);

return (
<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]">
<p className="text-black leading-6 font-normal text-sm sm:text-base">
{npmCommand}
</p>

<div
className="w-fit h-fit z-10 cursor-pointer"
onClick={() => {
setCopiedToClipboard(true);
navigator.clipboard.writeText(npmCommand);
}}
>
{copiedToClipboard ? (
<CheckIcon className="w-5 h-5 text-black" />
) : (
<CopyIcon />
)}
</div>
</div>
);
}
179 changes: 179 additions & 0 deletions apps/nameai.dev/components/video-animation/video-animation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"use client";

import React, { useEffect, useRef } from "react";

export default function VideoAsciiAnimation() {
const videoRef = useRef<HTMLVideoElement>(null);
const prerenderCanvasRef = useRef<HTMLCanvasElement>(null);
const outputCanvasRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
const video = videoRef.current;
const prerender = prerenderCanvasRef.current;
const outputCanvas = outputCanvasRef.current;
if (!video || !prerender || !outputCanvas) return;

const preCtx = prerender.getContext("2d", { willReadFrequently: true });
const outCtx = outputCanvas.getContext("2d");
if (!preCtx || !outCtx) return;

const ratio = window.devicePixelRatio || 1;
const computedStyle = getComputedStyle(outputCanvas);
const cssWidth = parseInt(computedStyle.getPropertyValue("width"), 10);
const cssHeight = parseInt(computedStyle.getPropertyValue("height"), 10);

// Adjust the canvas' backing store size:
outputCanvas.width = cssWidth * ratio;
outputCanvas.height = cssHeight * ratio;

// Scale the context so drawing operations use the proper pixel ratio
outCtx.scale(ratio, ratio);

// Instead of fixed char scale factors, compute them to fill the entire canvas.
// The prerender canvas resolution is used to decide how many "cells" the ASCII art will have.
const prerenderWidth = prerender.width;
const prerenderHeight = prerender.height;
const charW = cssWidth / prerenderWidth;
const charH = cssHeight / prerenderHeight;

// Set the font size to match the computed cell height
outCtx.font = `${charH}px monospace`;
outCtx.textBaseline = "top";

// 🎨 Original ASCII Character Set
const charsFixed: (string | string[])[] = [
"_",
".",
",",
"-",
"=",
"+",
":",
";",
"c",
"b",
"a",
"!",
"?",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
["9", "8"],
["✚", "✚", "✚", "✚", "✚", "⚛︎"],
["☺︎", "☹︎"],
"☀︎",
["@", "#"],
["X", "Y", "Z"],
"'",
];

let chars: (string | string[])[] = [...charsFixed];
const charsLength = chars.length;
const MAX_COLOR_INDEX = 255;

let animationFrameId: number;
const FRAME_INTERVAL = 1000 / 30;
let lastDrawTime = 0;

function updateCanvas(timestamp: number) {
if (timestamp - lastDrawTime < FRAME_INTERVAL) {
animationFrameId = requestAnimationFrame(updateCanvas);
return;
}

const w = prerender!.width;
const h = prerender!.height;
if (!w || !h || video!.paused) {
animationFrameId = requestAnimationFrame(updateCanvas);
return;
}

preCtx!.drawImage(video!, 0, 0, w, h);
const data = preCtx!.getImageData(0, 0, w, h).data;

// Clear the output canvas
outCtx!.clearRect(0, 0, outputCanvas!.width, outputCanvas!.height);

// Draw the ASCII art to the output canvas, scaling each "cell" to fully cover the canvas
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const index = (x + y * w) * 4;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
const brightness = (r + g + b) / 3;
const charIndex = Math.floor(
(charsLength * brightness) / MAX_COLOR_INDEX,
);
const result = chars[charIndex];
const char = Array.isArray(result)
? result[Math.floor(Math.random() * result.length)]
: result;
if (!char) continue;

outCtx!.fillStyle = `rgb(${r}, ${g}, ${b})`;
outCtx!.fillText(char, x * charW, y * charH);
}
}

lastDrawTime = timestamp;
animationFrameId = requestAnimationFrame(updateCanvas);
}

video.play().then(() => {
animationFrameId = requestAnimationFrame(updateCanvas);
});

return () => cancelAnimationFrame(animationFrameId);
}, []);

return (
<div className="relative w-full h-full">
{/* Output canvas for displaying ASCII art */}
<canvas
ref={outputCanvasRef}
width="1200" // This attribute can be used as a fallback resolution.
height="640"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full"
/>

{/* Hidden video and prerender canvases */}
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
crossOrigin="anonymous"
className="hidden"
>
<source
src="https://assets.codepen.io/907471/rainbow_s.mp4"
type="video/mp4"
/>
</video>

{/* Hidden prerender canvas used for processing the video/image data */}
<canvas
ref={prerenderCanvasRef}
width="240" // The resolution of the ASCII art "cells".
height="80"
className="hidden"
/>

<style>{`
body {
margin: 0;
padding: 0;
background: white;
font-family: "Courier New", monospace;
}
`}</style>
</div>
);
}
3 changes: 2 additions & 1 deletion apps/nameai.dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-wrap-balancer": "1.1.1",
"sonner": "1.5.0"
"sonner": "1.5.0",
"tweakpane": "4.0.5"
},
"devDependencies": {
"@types/node": "^22.10.2",
Expand Down
12 changes: 10 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.