diff --git a/apps/nameai.dev/app/page.tsx b/apps/nameai.dev/app/page.tsx index 53234ab73..f8c0e658d 100644 --- a/apps/nameai.dev/app/page.tsx +++ b/apps/nameai.dev/app/page.tsx @@ -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 ( <> -
-
-
- - Enable new ENS user experiences - - What will you build? +
+
+
+ +
+
+
+ + Enable new ENS user experiences + + What will you build? +
+
+ +
+
+ + +
+
+
diff --git a/apps/nameai.dev/components/CopyIcon.tsx b/apps/nameai.dev/components/CopyIcon.tsx new file mode 100644 index 000000000..76bf5e3eb --- /dev/null +++ b/apps/nameai.dev/components/CopyIcon.tsx @@ -0,0 +1,34 @@ +export const CopyIcon = () => ( + <> + + + + + + + +); diff --git a/apps/nameai.dev/components/HeroStartCommand.tsx b/apps/nameai.dev/components/HeroStartCommand.tsx new file mode 100644 index 000000000..b113fd16c --- /dev/null +++ b/apps/nameai.dev/components/HeroStartCommand.tsx @@ -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(false); + + const displayCopiedFor = 4000; + + useEffect(() => { + if (copiedToClipboard) { + const timer = setTimeout(() => { + setCopiedToClipboard(false); + }, displayCopiedFor); + return () => clearTimeout(timer); + } + }, [copiedToClipboard]); + + return ( +
+

+ {npmCommand} +

+ +
{ + setCopiedToClipboard(true); + navigator.clipboard.writeText(npmCommand); + }} + > + {copiedToClipboard ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/apps/nameai.dev/components/video-animation/video-animation.tsx b/apps/nameai.dev/components/video-animation/video-animation.tsx new file mode 100644 index 000000000..6b790bcca --- /dev/null +++ b/apps/nameai.dev/components/video-animation/video-animation.tsx @@ -0,0 +1,179 @@ +"use client"; + +import React, { useEffect, useRef } from "react"; + +export default function VideoAsciiAnimation() { + const videoRef = useRef(null); + const prerenderCanvasRef = useRef(null); + const outputCanvasRef = useRef(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 ( +
+ {/* Output canvas for displaying ASCII art */} + + + {/* Hidden video and prerender canvases */} + + + {/* Hidden prerender canvas used for processing the video/image data */} + + + +
+ ); +} diff --git a/apps/nameai.dev/package.json b/apps/nameai.dev/package.json index cf3cb9bfc..26daf45cd 100644 --- a/apps/nameai.dev/package.json +++ b/apps/nameai.dev/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf8cc6523..b49a65b0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,9 @@ importers: sonner: specifier: 1.5.0 version: 1.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + tweakpane: + specifier: 4.0.5 + version: 4.0.5 devDependencies: '@types/node': specifier: ^22.10.2 @@ -5677,6 +5680,9 @@ packages: resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==} hasBin: true + tweakpane@4.0.5: + resolution: {integrity: sha512-rxEXdSI+ArlG1RyO6FghC4ZUX8JkEfz8F3v1JuteXSV0pEtHJzyo07fcDG+NsJfN5L39kSbCYbB9cBGHyuI/tQ==} + tween-functions@1.2.0: resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} @@ -8885,7 +8891,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -8907,7 +8913,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11118,6 +11124,8 @@ snapshots: turbo-windows-64: 2.3.3 turbo-windows-arm64: 2.3.3 + tweakpane@4.0.5: {} + tween-functions@1.2.0: {} type-check@0.4.0: