From 988519620d69ecf19e6819527025215ed8d9de36 Mon Sep 17 00:00:00 2001 From: eduramme Date: Mon, 3 Feb 2025 19:30:47 -0300 Subject: [PATCH 1/8] create basic video animatino example for nameai --- apps/nameai.dev/app/page.tsx | 4 +- .../video-animation/video-animation.tsx | 198 ++++++++++++++++++ apps/nameai.dev/package.json | 3 +- pnpm-lock.yaml | 12 +- 4 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 apps/nameai.dev/components/video-animation/video-animation.tsx diff --git a/apps/nameai.dev/app/page.tsx b/apps/nameai.dev/app/page.tsx index 53234ab73..b8f3986ce 100644 --- a/apps/nameai.dev/app/page.tsx +++ b/apps/nameai.dev/app/page.tsx @@ -1,9 +1,10 @@ import { Heading, Text } from "@namehash/namekit-react"; +import AsciiVideo from "../components/video-animation/video-animation"; export default function Page() { return ( <> -
+
@@ -12,6 +13,7 @@ export default function Page() { What will you build?
+
); 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..da47d7e53 --- /dev/null +++ b/apps/nameai.dev/components/video-animation/video-animation.tsx @@ -0,0 +1,198 @@ +"use client"; + +import React, { useEffect } from "react"; + +export default function VideoAsciiAnimation() { + useEffect(() => { + // Grab the necessary DOM elements. + const video = document.getElementById("input") as HTMLVideoElement | null; + const canvas = document.getElementById( + "prerender", + ) as HTMLCanvasElement | null; + const output = document.getElementById("output") as HTMLDivElement | null; + if (!video || !canvas || !output) return; + + // Add loop handling + video.addEventListener("timeupdate", () => { + // If we're near the end of the video (within 0.2 seconds) + if (video.duration - video.currentTime < 0.2) { + // Set the current time to 0 before it actually ends + video.currentTime = 0; + } + }); + + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + if (!ctx) return; + + // Define the 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]; + let charsLength = chars.length; + const MAX_COLOR_INDEX = 255; + + // Function to capture video frames, convert to ASCII, and render. + let lastDrawTime = 0; + const FRAME_INTERVAL = 1000 / 24; // Reduced to 24 FPS for better performance + let animationFrameId: number; + + function updateCanvas(timestamp: number) { + // Skip frame if too soon + if (timestamp - lastDrawTime < FRAME_INTERVAL) { + animationFrameId = requestAnimationFrame(updateCanvas); + return; + } + + const w = canvas?.width; + const h = canvas?.height; + if (!w || !h || !ctx || !video || video.paused) return; + + try { + ctx.drawImage(video, 0, 0, w, h); + const data = ctx.getImageData(0, 0, w, h).data; + const rows = []; + + // Process frames in chunks + for (let y = 0; y < h; y++) { + const spans = new Array(w); + 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 c = (r + g + b) / 3; + const charIndex = Math.floor((charsLength * c) / MAX_COLOR_INDEX); + const result = chars[charIndex]; + const char = Array.isArray(result) + ? result[Math.floor(Math.random() * result.length)] + : result; + spans[x] = + `${char || " "}`; + } + rows.push(`
${spans.join("")}
`); + } + + output!.innerHTML = rows.join(""); + output!.style.setProperty( + "--color", + `rgb(${data[0]},${data[1]},${data[2]})`, + ); + + lastDrawTime = timestamp; + } catch (error) { + console.error("Frame processing error:", error); + } + + animationFrameId = requestAnimationFrame(updateCanvas); + } + + // Start the video and begin the animation. + video.play().then(() => { + animationFrameId = requestAnimationFrame(updateCanvas); + }); + + // Clean up + return () => { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + } + video.removeEventListener("timeupdate", () => {}); + }; + }, []); + + return ( +
+ {/* The container where the ASCII art will be rendered */} +
+ + {/* Hidden video element used as the source */} + + + {/* Hidden canvas element used for processing */} + + + {/* CSS styling */} + +
+ ); +} 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: From 338d1796ed4d97e4d819ed06b9842d2d8bcefa77 Mon Sep 17 00:00:00 2001 From: eduramme Date: Mon, 3 Feb 2025 20:40:05 -0300 Subject: [PATCH 2/8] nice --- .../video-animation/video-animation.tsx | 155 +++++++----------- 1 file changed, 56 insertions(+), 99 deletions(-) diff --git a/apps/nameai.dev/components/video-animation/video-animation.tsx b/apps/nameai.dev/components/video-animation/video-animation.tsx index da47d7e53..07cc951d0 100644 --- a/apps/nameai.dev/components/video-animation/video-animation.tsx +++ b/apps/nameai.dev/components/video-animation/video-animation.tsx @@ -4,7 +4,6 @@ import React, { useEffect } from "react"; export default function VideoAsciiAnimation() { useEffect(() => { - // Grab the necessary DOM elements. const video = document.getElementById("input") as HTMLVideoElement | null; const canvas = document.getElementById( "prerender", @@ -12,19 +11,10 @@ export default function VideoAsciiAnimation() { const output = document.getElementById("output") as HTMLDivElement | null; if (!video || !canvas || !output) return; - // Add loop handling - video.addEventListener("timeupdate", () => { - // If we're near the end of the video (within 0.2 seconds) - if (video.duration - video.currentTime < 0.2) { - // Set the current time to 0 before it actually ends - video.currentTime = 0; - } - }); - const ctx = canvas.getContext("2d", { willReadFrequently: true }); if (!ctx) return; - // Define the ASCII character set. + // 🎨 Original ASCII Character Set const charsFixed: (string | string[])[] = [ "_", ".", @@ -55,85 +45,72 @@ export default function VideoAsciiAnimation() { ["X", "Y", "Z"], "'", ]; + let chars: (string | string[])[] = [...charsFixed]; let charsLength = chars.length; const MAX_COLOR_INDEX = 255; - // Function to capture video frames, convert to ASCII, and render. - let lastDrawTime = 0; - const FRAME_INTERVAL = 1000 / 24; // Reduced to 24 FPS for better performance let animationFrameId: number; + const FRAME_INTERVAL = 1000 / 30; // 30 FPS for smoothness + let lastDrawTime = 0; function updateCanvas(timestamp: number) { - // Skip frame if too soon if (timestamp - lastDrawTime < FRAME_INTERVAL) { animationFrameId = requestAnimationFrame(updateCanvas); return; } - const w = canvas?.width; - const h = canvas?.height; + const w = canvas?.width ?? 0; + const h = canvas?.height ?? 0; if (!w || !h || !ctx || !video || video.paused) return; - try { - ctx.drawImage(video, 0, 0, w, h); - const data = ctx.getImageData(0, 0, w, h).data; - const rows = []; - - // Process frames in chunks - for (let y = 0; y < h; y++) { - const spans = new Array(w); - 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 c = (r + g + b) / 3; - const charIndex = Math.floor((charsLength * c) / MAX_COLOR_INDEX); - const result = chars[charIndex]; - const char = Array.isArray(result) - ? result[Math.floor(Math.random() * result.length)] - : result; - spans[x] = - `${char || " "}`; - } - rows.push(`
${spans.join("")}
`); + ctx.drawImage(video, 0, 0, w, h); + const data = ctx.getImageData(0, 0, w, h).data; + + const fragment = document.createDocumentFragment(); // Batch DOM updates + for (let y = 0; y < h; y++) { + const row = document.createElement("div"); + 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 c = (r + g + b) / 3; // Calculate brightness + const charIndex = Math.floor((charsLength * c) / MAX_COLOR_INDEX); + const result = chars[charIndex]; + const char = Array.isArray(result) + ? result[Math.floor(Math.random() * result.length)] + : result; + + const span = document.createElement("span"); + span.style.color = `rgb(${r},${g},${b})`; + span.textContent = char ?? " "; + row.appendChild(span); } - - output!.innerHTML = rows.join(""); - output!.style.setProperty( - "--color", - `rgb(${data[0]},${data[1]},${data[2]})`, - ); - - lastDrawTime = timestamp; - } catch (error) { - console.error("Frame processing error:", error); + fragment.appendChild(row); } + output!.innerHTML = ""; + output!.appendChild(fragment); + + lastDrawTime = timestamp; animationFrameId = requestAnimationFrame(updateCanvas); } - // Start the video and begin the animation. video.play().then(() => { animationFrameId = requestAnimationFrame(updateCanvas); }); - // Clean up - return () => { - if (animationFrameId) { - cancelAnimationFrame(animationFrameId); - } - video.removeEventListener("timeupdate", () => {}); - }; + return () => cancelAnimationFrame(animationFrameId); }, []); return ( -
- {/* The container where the ASCII art will be rendered */} -
+
+
- {/* Hidden video element used as the source */} - {/* Hidden canvas element used for processing */} - + - {/* CSS styling */}
From 79574b0b77af85b4fa7f470425b98f13c116bdf8 Mon Sep 17 00:00:00 2001 From: eduramme Date: Mon, 3 Feb 2025 21:05:37 -0300 Subject: [PATCH 3/8] improve animation --- apps/nameai.dev/app/page.tsx | 18 +++++---- .../video-animation/video-animation.tsx | 39 ++++++++++++------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/apps/nameai.dev/app/page.tsx b/apps/nameai.dev/app/page.tsx index b8f3986ce..931e447fc 100644 --- a/apps/nameai.dev/app/page.tsx +++ b/apps/nameai.dev/app/page.tsx @@ -4,16 +4,18 @@ import AsciiVideo from "../components/video-animation/video-animation"; 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/video-animation/video-animation.tsx b/apps/nameai.dev/components/video-animation/video-animation.tsx index 07cc951d0..e6b8038cb 100644 --- a/apps/nameai.dev/components/video-animation/video-animation.tsx +++ b/apps/nameai.dev/components/video-animation/video-animation.tsx @@ -9,6 +9,7 @@ export default function VideoAsciiAnimation() { "prerender", ) as HTMLCanvasElement | null; const output = document.getElementById("output") as HTMLDivElement | null; + if (!video || !canvas || !output) return; const ctx = canvas.getContext("2d", { willReadFrequently: true }); @@ -60,14 +61,18 @@ export default function VideoAsciiAnimation() { return; } - const w = canvas?.width ?? 0; - const h = canvas?.height ?? 0; - if (!w || !h || !ctx || !video || video.paused) return; + const w = canvas!.width; + const h = canvas!.height; + if (!w || !h || !video || video.paused) { + animationFrameId = requestAnimationFrame(updateCanvas); + return; + } - ctx.drawImage(video, 0, 0, w, h); - const data = ctx.getImageData(0, 0, w, h).data; + ctx!.drawImage(video, 0, 0, w, h); + const data = ctx!.getImageData(0, 0, w, h).data; - const fragment = document.createDocumentFragment(); // Batch DOM updates + // Create a document fragment to batch DOM updates + const fragment = document.createDocumentFragment(); for (let y = 0; y < h; y++) { const row = document.createElement("div"); for (let x = 0; x < w; x++) { @@ -83,15 +88,15 @@ export default function VideoAsciiAnimation() { : result; const span = document.createElement("span"); - span.style.color = `rgb(${r},${g},${b})`; + span.style.color = `rgb(${r}, ${g}, ${b})`; span.textContent = char ?? " "; row.appendChild(span); } fragment.appendChild(row); } - output!.innerHTML = ""; - output!.appendChild(fragment); + // Update the output element in one operation using replaceChildren + output!.replaceChildren(fragment); lastDrawTime = timestamp; animationFrameId = requestAnimationFrame(updateCanvas); @@ -105,10 +110,10 @@ export default function VideoAsciiAnimation() { }, []); return ( -
+
); From 546d7101d2f51e273b933472cce86cd41c19d760 Mon Sep 17 00:00:00 2001 From: eduramme Date: Mon, 3 Feb 2025 23:08:26 -0300 Subject: [PATCH 6/8] remove comment --- apps/nameai.dev/app/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/nameai.dev/app/page.tsx b/apps/nameai.dev/app/page.tsx index 371dd7f5f..57f49b272 100644 --- a/apps/nameai.dev/app/page.tsx +++ b/apps/nameai.dev/app/page.tsx @@ -6,12 +6,11 @@ export default function Page() { <>
- {/*
*/}
From b659f104aa3a188eb372ea63ca17f91a98306502 Mon Sep 17 00:00:00 2001 From: eduramme Date: Mon, 3 Feb 2025 23:18:04 -0300 Subject: [PATCH 7/8] fix build --- apps/nameai.dev/components/video-animation/video-animation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/nameai.dev/components/video-animation/video-animation.tsx b/apps/nameai.dev/components/video-animation/video-animation.tsx index 33485c633..6b790bcca 100644 --- a/apps/nameai.dev/components/video-animation/video-animation.tsx +++ b/apps/nameai.dev/components/video-animation/video-animation.tsx @@ -166,7 +166,7 @@ export default function VideoAsciiAnimation() { className="hidden" /> -