From 2bcdceef5962efee394c4854dbd525e3ad00dbf8 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Tue, 6 Jan 2026 16:03:44 -0600 Subject: [PATCH] feat(vision-training): animate background tiles between image and digit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each tile now randomly transitions between showing the training image and the numeral it represents. The animations are staggered with random initial delays and intervals so tiles don't all animate together. - Created AnimatedTile component with crossfade effect - Random 3-8 second intervals between transitions - 0.8s ease-in-out opacity transitions - Random initial delays (0-10s) for natural feel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/app/vision-training/train/page.tsx | 119 +++++++++++++++--- 1 file changed, 101 insertions(+), 18 deletions(-) diff --git a/apps/web/src/app/vision-training/train/page.tsx b/apps/web/src/app/vision-training/train/page.tsx index 496b1a54..301400d1 100644 --- a/apps/web/src/app/vision-training/train/page.tsx +++ b/apps/web/src/app/vision-training/train/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import Link from 'next/link' import { css } from '../../../../styled-system/css' import { TrainingWizard } from './components/wizard/TrainingWizard' @@ -15,6 +15,90 @@ import type { TrainingResult, } from './components/wizard/types' +/** Animated background tile that transitions between image and digit */ +function AnimatedTile({ src, digit, index }: { src: string; digit: number; index: number }) { + const [showDigit, setShowDigit] = useState(false) + + useEffect(() => { + // Random initial delay so tiles don't all animate together + const initialDelay = Math.random() * 10000 + + const startAnimation = () => { + // Random interval between 3-8 seconds + const interval = 3000 + Math.random() * 5000 + + const timer = setInterval(() => { + setShowDigit((prev) => !prev) + // Stay in the new state for 1-3 seconds before potentially switching back + setTimeout( + () => { + // 50% chance to switch back + if (Math.random() > 0.5) { + setShowDigit((prev) => !prev) + } + }, + 1000 + Math.random() * 2000 + ) + }, interval) + + return timer + } + + const delayTimer = setTimeout(() => { + const animTimer = startAnimation() + return () => clearInterval(animTimer) + }, initialDelay) + + return () => clearTimeout(delayTimer) + }, []) + + return ( +
+ {/* Image layer */} + + {/* Digit layer */} +
+ {digit} +
+
+ ) +} + export default function TrainModelPage() { // Configuration const [config, setConfig] = useState({ @@ -123,8 +207,13 @@ export default function TrainModelPage() { } }, []) - // Get all tile images for background - const allTileImages = samples ? Object.values(samples.digits).flatMap((d) => d.tilePaths) : [] + // Get all tile images with their digits for background + const allTiles = useMemo(() => { + if (!samples) return [] + return Object.entries(samples.digits).flatMap(([digit, data]) => + data.tilePaths.map((src) => ({ src, digit: parseInt(digit, 10) })) + ) + }, [samples]) // Start training const startTraining = useCallback(async () => { @@ -273,7 +362,7 @@ export default function TrainModelPage() { })} > {/* Tiled Background Effect */} - {allTileImages.length > 0 && ( + {allTiles.length > 0 && (
{/* Repeat tiles to fill background (need ~600+ for full coverage) */} - {Array.from({ length: Math.ceil(800 / Math.max(1, allTileImages.length)) }) - .flatMap(() => allTileImages) + {Array.from({ length: Math.ceil(800 / Math.max(1, allTiles.length)) }) + .flatMap(() => allTiles) .slice(0, 800) - .map((src, i) => ( - ( + ))}