Scanner HUD - lock ring and insight card
coin-scanner-hud/ ยท 4 files
componentskill: design-taste-frontendcanvascameradarkinteractiveevidence
Brief
The prompt that produced this. Re-run it to generate more in the same taste.
- Aesthetic
- instrument overlay - dark viewfinder, thin luminous ring, monospaced readout, state-led, never occluding the subject
- Reference
- coin-detect live capture HUD
- Intent
- narrate a recognition attempt while it is happening, so the person holding the camera knows whether to hold still, move closer, or that it already has what it needs
- Guardrails
- always keep the subject visible - the card yields to the ring, never covers it, name the phase in words, not only by ring colour, show confidence beside any identification, hold the last good reading rather than flickering to emptynever claim an identification while still acquiring, cover the centre of the frame, animate the ring faster than the detector actually updates, report confidence without a subject
Install
pnpm dlx shadcn@latest add http://localhost:3040/r/coin-scanner-hud.json
Source
Meta block and library type import stripped - this is what pastes cleanly elsewhere.
'use client'
import { useEffect, useState } from 'react'
import { CoinInsightCard } from './insight-card'
import type { CircleOverlayPx, PreviewInsight, TrackingPhase } from './lib'
const STAGE_W = 720
const STAGE_H = 480
/** Where the coin sits in the frame. Fixed: there is no detector here to move it. */
const ANCHOR: CircleOverlayPx = { cx: STAGE_W * 0.42, cy: STAGE_H * 0.5, r: 96 }
const INSIGHT: PreviewInsight = {
title: '20 Francs Marianne Coq',
coin_name: '20 Francs Marianne Coq',
year: '1907',
metal: 'Gold, .900',
catalog_ref: 'KM# 857',
one_liner: 'Third Republic gold franc, Chaplain design, common date in this grade.',
confidence: 0.87,
}
/** The phases a real scan walks through, in order, on a loop. */
const CYCLE: TrackingPhase[] = ['searching', 'acquiring', 'locked', 'identifying', 'identified']
const RING: Record<TrackingPhase, string> = {
idle: 'rgba(161,161,170,0.35)',
searching: 'rgba(161,161,170,0.6)',
acquiring: 'rgba(250,204,21,0.85)',
locked: 'rgba(56,189,248,0.95)',
identifying: 'rgba(56,189,248,0.95)',
identified: 'rgba(74,222,128,0.95)',
}
export default function CoinScannerHud() {
const [step, setStep] = useState(0)
const phase = CYCLE[step % CYCLE.length]
useEffect(() => {
const id = setInterval(() => setStep((s) => s + 1), 1800)
return () => clearInterval(id)
}, [])
return (
<main className="grid min-h-[100dvh] place-items-center bg-zinc-950 p-6 text-zinc-100">
<div className="w-full max-w-3xl">
<div className="mb-3 flex items-center justify-between">
<p className="font-mono text-[11px] text-zinc-500 uppercase tracking-widest">
Live capture
</p>
<p
aria-live="polite"
className="font-mono text-[11px] text-zinc-300 uppercase tracking-widest"
>
{phase}
</p>
</div>
<div
className="relative overflow-hidden rounded-lg bg-[radial-gradient(120%_100%_at_40%_45%,#2b2b31_0%,#1a1a1e_55%,#101013_100%)]"
style={{ width: '100%', aspectRatio: `${STAGE_W} / ${STAGE_H}` }}
>
{/* Stand-in for the camera feed: the design is the HUD, and a real
viewfinder would need a stream this preview cannot ask for. */}
<div
aria-hidden
className="absolute rounded-full bg-[radial-gradient(circle_at_38%_32%,#d9c98f_0%,#a8912f_45%,#6b5a18_100%)]"
style={{
left: `${(ANCHOR.cx / STAGE_W) * 100}%`,
top: `${(ANCHOR.cy / STAGE_H) * 100}%`,
width: `${((ANCHOR.r * 2) / STAGE_W) * 100}%`,
aspectRatio: '1',
transform: 'translate(-50%, -50%)',
}}
/>
<div
aria-hidden
className="absolute rounded-full transition-[border-color,transform] duration-300"
style={{
left: `${(ANCHOR.cx / STAGE_W) * 100}%`,
top: `${(ANCHOR.cy / STAGE_H) * 100}%`,
width: `${((ANCHOR.r * 2.25) / STAGE_W) * 100}%`,
aspectRatio: '1',
transform: `translate(-50%, -50%) scale(${phase === 'acquiring' ? 1.04 : 1})`,
border: `2px solid ${RING[phase]}`,
}}
/>
<CoinInsightCard
anchor={ANCHOR}
stageW={STAGE_W}
stageH={STAGE_H}
phase={phase}
insight={phase === 'identified' ? INSIGHT : null}
disabled={false}
/>
</div>
</div>
</main>
)
}