Tebak Plat: license plate guesser
Guess the country from the plate's color, size, and lettering.
"use client";// Mirrored from tebak-plat plate.tsx on main (6ae6b708).// Upstream tilt uses motion/react. This file uses framer-motion, already in the portfolio.// Re-sync notes: SYNC.md. Japan, US, and Singapore layouts are portfolio-only.import { useEffect, useId, useRef, useSyncExternalStore, type PointerEvent, type SVGProps } from "react";import { useMotionValueEvent, useReducedMotion, useSpring } from "framer-motion";import localFont from "next/font/local";import type { WorldPlate } from "./types";import { plateDesign, type PlateModel } from "./plate-designs";import { plateGeometry, type PlateDimensions } from "./plate-geometry";import { plateLettering } from "./plate-typography";type PlateProps = Omit<SVGProps<SVGSVGElement>, "children"> & { text: string; world?: WorldPlate; model?: PlateModel; validity?: string | null; dimensions?: PlateDimensions; backdrop?: boolean; debug?: boolean; tilt?: boolean;};const spring = { duration: 0.4, bounce: 0 };const light = "oklch(1 0 0)";const dark = "oklch(0 0 0)";const euBlue = "oklch(0.34 0.16 263)";const euGold = "oklch(0.87 0.17 90)";const namedPaint: Record<string, string> = { yellow: "oklch(0.9 0.15 98)", white: "oklch(0.975 0.006 95)", black: "oklch(0.18 0.002 95)",};function paint(color: string) { return namedPaint[color] ?? color;}function faceWord(face: string) { if (face === "yellow") return "yellow"; if (face.includes(" 85)")) return "gold"; if (face === "white" || face.startsWith("oklch(0.9") || face.startsWith("oklch(0.97")) return "white"; if (face === "black" || face.startsWith("oklch(0.21")) return "black"; return "custom";}function inkWord(ink: string) { if (ink === "black" || ink.startsWith("oklch(0.18") || ink.startsWith("oklch(0.21")) return "black"; if (ink === "white" || ink.startsWith("oklch(0.95") || ink.startsWith("oklch(0.97")) return "white"; if (ink === "#8b1b32" || ink.includes("0.15 25")) return "red"; if (ink.includes("155")) return "green"; if (ink.includes("255") || ink.includes("260")) return "blue"; return "dark";}const condensed = localFont({ src: "../../../fonts/plate/D-DINCondensed-Bold.woff2", weight: "700", display: "swap", adjustFontFallback: "Arial", preload: false,});const regular = localFont({ src: "../../../fonts/plate/D-DINCondensed.woff2", weight: "400", display: "swap", adjustFontFallback: "Arial", preload: false,});const din = localFont({ src: "../../../fonts/plate/D-DIN.woff2", weight: "400", display: "swap", adjustFontFallback: "Arial", preload: false,});const faces = { condensed, regular, din };const debugRequested = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("debug") === "1";const subscribeDebug = () => () => {};function Lettering({ text, face, height, width, x, y, filter, row,}: { text: string; face: "condensed" | "regular" | "din"; height: number; width: number; x: number; y: number; filter?: string; row?: string;}) { const run = plateLettering(text, face, height, width); const fontSize = height / 0.69; return ( <text className={faces[face].className} x={x} y={y} data-plate-row={row} textAnchor="middle" dominantBaseline="central" dy={fontSize * 0.014} fontSize={fontSize} fontWeight={face === "condensed" ? 700 : 400} textLength={run.width * run.fit} lengthAdjust="spacingAndGlyphs" letterSpacing="2.2" filter={filter} style={row === "registration" ? { fontVariantNumeric: "tabular-nums" } : undefined} > {text} </text> );}export function Plate({ text, world, model = "private", validity = "10 · 29", dimensions, backdrop = false, debug, tilt = true, style, className, ref, ...props}: PlateProps) { const id = useId().replace(/[^a-zA-Z0-9_-]/g, ""); const svg = useRef<SVGSVGElement | null>(null); const sheen = useRef<SVGGElement>(null); const bounds = useRef<DOMRect | null>(null); const reduced = useReducedMotion(); const pitch = useSpring(0, spring); const yaw = useSpring(0, spring); const design = plateDesign(world, model); const geometry = plateGeometry(world, dimensions); const showDebug = useSyncExternalStore(subscribeDebug, () => debugRequested, () => false) || debug; const moving = tilt && !reduced; function paintTilt() { if (!svg.current) return; svg.current.style.transform = moving ? `perspective(800px) rotateX(${pitch.get()}deg) rotateY(${yaw.get()}deg)` : "none"; if (sheen.current) sheen.current.style.transform = moving ? `translateX(${yaw.get() * 12}px)` : "none"; if (pitch.get() === 0 && yaw.get() === 0) svg.current.style.willChange = "auto"; } useMotionValueEvent(pitch, "change", paintTilt); useMotionValueEvent(yaw, "change", paintTilt); useEffect(() => { if (!moving) { pitch.jump(0); yaw.jump(0); } }, [moving, pitch, yaw]); function track(event: PointerEvent<SVGSVGElement>) { if (!moving || event.pointerType !== "mouse" || !bounds.current) return; const box = bounds.current; let x = Math.max(-1, Math.min(1, ((event.clientX - box.left) / box.width) * 2 - 1)); let y = Math.max(-1, Math.min(1, ((event.clientY - box.top) / box.height) * 2 - 1)); const length = Math.max(1, Math.hypot(x, y)); x /= length; y /= length; pitch.set(-y * 4); yaw.set(x * 4); } if (!design) return null; const { width: w, height: h, face, rim, faceInset, bandWidth, rightBandWidth } = geometry; const legacy = !world && model === "legacy"; const showValidity = !world && validity !== null; const capHeight = (design.capHeight / design.height) * h; const mainY = showValidity ? face.top + (face.bottom - face.top) * 0.45 : design.caption ? (face.top + 18 + face.bottom) / 2 : face.cy; const validityY = (mainY + capHeight / 2 + face.bottom) / 2; const rivetY = (face.top + mainY - capHeight / 2) / 2; const rivetRadius = Math.min(3.1, (mainY - capHeight / 2 - face.top) / 4); const center = face.cx; const overhang = geometry.backdrop.overhang; const angle = (Math.abs(geometry.backdrop.rotation) * Math.PI) / 180; const backdropScale = Math.max( ((w + 2 * overhang) * Math.cos(angle) + (h + 2 * overhang) * Math.sin(angle)) / w, ((h + 2 * overhang) * Math.cos(angle) + (w + 2 * overhang) * Math.sin(angle)) / h, ); const emboss = `url(#${id}-emboss)`; const faceName = faceWord(design.face); const inkName = inkWord(design.ink); const label = `Plate ${text}. ${ world ? `${faceName[0]!.toUpperCase()}${faceName.slice(1)} background, ${inkName} text${design.band ? `, blue band on the left reading ${design.band}` : ""}.` : `Indonesian ${legacy ? "black" : "white"} plate, ${legacy ? "white" : "black"} text, valid through ${validity}.` }`; return ( <svg {...props} ref={(node) => { svg.current = node; if (typeof ref === "function") ref(node); else if (ref) ref.current = node; }} className={className} width={w} height={h} viewBox={`0 0 ${w} ${h}`} role="img" aria-label={props["aria-label"] ?? label} data-plate-model={world?.id ?? model} data-plate-text={text} data-plate-font={design.fontFamily} onPointerEnter={(event) => { if ( moving && event.pointerType === "mouse" && window.matchMedia("(hover: hover) and (pointer: fine)").matches ) { bounds.current = event.currentTarget.getBoundingClientRect(); event.currentTarget.style.willChange = "transform"; } props.onPointerEnter?.(event); }} onPointerMove={(event) => { track(event); props.onPointerMove?.(event); }} onPointerLeave={(event) => { bounds.current = null; pitch.set(0); yaw.set(0); props.onPointerLeave?.(event); }} style={{ display: "block", width: "100%", height: "auto", aspectRatio: `${w} / ${h}`, overflow: "visible", userSelect: "none", transformOrigin: "50% 50%", ...style, }} > <defs> <rect id={`${id}-blank`} x="0.4" y="0.4" width={w - 0.8} height={h - 0.8} rx={geometry.outerRadius - 0.4} /> <clipPath id={`${id}-face-clip`}> <rect x={faceInset} y={faceInset} width={w - faceInset * 2} height={h - faceInset * 2} rx={rim.rx - geometry.stroke / 2} /> </clipPath> <clipPath id={`${id}-backdrop-clip`}> <rect x={-overhang} y={-overhang} width={w + overhang * 2} height={h + overhang * 2} rx={geometry.outerRadius + overhang} /> </clipPath> <pattern id={`${id}-grid`} width="10" height="10" patternUnits="userSpaceOnUse"> <path d="M10 0H0V10" fill="none" stroke={euBlue} strokeOpacity="0.25" strokeWidth="1" vectorEffect="non-scaling-stroke" /> </pattern> <clipPath id={`${id}-clip`}> <use href={`#${id}-blank`} /> </clipPath> <linearGradient id={`${id}-face`} x1="0" y1="0" x2="0.25" y2="1"> <stop stopColor={light} stopOpacity={legacy ? 0.08 : 0.34} /> <stop offset="0.23" stopColor={light} stopOpacity="0.02" /> <stop offset="0.52" stopColor={dark} stopOpacity="0.035" /> <stop offset="0.78" stopColor={light} stopOpacity="0.1" /> <stop offset="1" stopColor={dark} stopOpacity="0.11" /> </linearGradient> <linearGradient id={`${id}-rim`} x1="0.15" y1="0" x2="0.45" y2="1"> <stop stopColor={light} stopOpacity="0.85" /> <stop offset="0.16" stopColor={light} stopOpacity="0.28" /> <stop offset="0.42" stopColor={dark} stopOpacity="0.12" /> <stop offset="0.75" stopColor={dark} stopOpacity="0.43" /> <stop offset="1" stopColor={light} stopOpacity="0.66" /> </linearGradient> <linearGradient id={`${id}-sheen`} x1="0" y1="0" x2="1" y2="0"> <stop stopColor={light} stopOpacity="0" /> <stop offset="0.3" stopColor={light} stopOpacity="0" /> <stop offset="0.48" stopColor={light} stopOpacity="0.28" /> <stop offset="0.6" stopColor={light} stopOpacity="0.035" /> <stop offset="1" stopColor={light} stopOpacity="0" /> </linearGradient> <filter id={`${id}-brush`} filterUnits="userSpaceOnUse" x="0" y="0" width={w} height={h} colorInterpolationFilters="sRGB"> <feTurbulence type="fractalNoise" baseFrequency="0.025 0.85" numOctaves="2" seed="17" stitchTiles="stitch" /> <feColorMatrix type="saturate" values="0" /> <feComposite in2="SourceAlpha" operator="in" /> </filter> <filter id={`${id}-emboss`} filterUnits="userSpaceOnUse" x="0" y="0" width={w} height={h} colorInterpolationFilters="sRGB"> <feGaussianBlur in="SourceAlpha" stdDeviation="0.45" result="height" /> <feDiffuseLighting in="height" surfaceScale="1.4" diffuseConstant="0.68" lightingColor={light} result="diffuse"> <feDistantLight azimuth="225" elevation="52" /> </feDiffuseLighting> <feComposite in="diffuse" in2="SourceGraphic" operator="arithmetic" k1="0.45" k2="0" k3="0.7" k4="0" result="body" /> <feSpecularLighting in="height" surfaceScale="2.1" specularConstant="0.55" specularExponent="20" lightingColor={light} result="specular"> <feDistantLight azimuth="225" elevation="48" /> </feSpecularLighting> <feComposite in="specular" in2="SourceAlpha" operator="in" result="crest" /> <feOffset in="SourceAlpha" dx="1" dy="1" result="offset" /> <feComposite in="offset" in2="SourceAlpha" operator="out" result="lowerEdge" /> <feFlood floodColor={dark} floodOpacity="0.65" /> <feComposite in2="lowerEdge" operator="in" result="edge" /> <feMerge> <feMergeNode in="edge" /> <feMergeNode in="body" /> <feMergeNode in="crest" /> </feMerge> </filter> </defs> <g aria-hidden="true"> {backdrop ? ( <g clipPath={`url(#${id}-backdrop-clip)`} data-plate-backdrop-window="3"> <g transform={`translate(${w / 2} ${h / 2}) rotate(${geometry.backdrop.rotation}) scale(${backdropScale}) translate(${-w / 2} ${-h / 2})`} > <rect data-plate-backdrop="true" x="0" y="0" width={w} height={h} rx={geometry.outerRadius} fill="color-mix(in oklab, var(--home-ink) 12%, var(--home-bg))" /> </g> </g> ) : null} <use href={`#${id}-blank`} fill={paint(design.face)} stroke={dark} strokeOpacity="0.45" strokeWidth="0.8" /> <use href={`#${id}-blank`} fill={`url(#${id}-face)`} /> <g clipPath={`url(#${id}-face-clip)`}> <rect width={w} height={h} fill={paint(design.face)} filter={`url(#${id}-brush)`} opacity={legacy ? 0.025 : 0.042} /> {design.band ? ( <> <rect data-plate-band="left" x={faceInset} y={faceInset} width={bandWidth} height={h - faceInset * 2} fill={euBlue} /> {design.eu ? Array.from({ length: 12 }, (_, i) => { const star = ((i * 30 - 90) * Math.PI) / 180; return ( <path key={i} fill={euGold} transform={`translate(${faceInset + bandWidth / 2 + Math.cos(star) * 10} ${face.top + (h - faceInset * 2) * 0.27 + Math.sin(star) * 10})`} d="M0-2.2 .52-.7 2.1-.68 .84.3 1.3 1.8 0 .88-1.3 1.8-.84.3-2.1-.68-.52-.7Z" /> ); }) : null} <g fill={light}> <Lettering text={design.band} face="din" height={design.band.length > 2 ? 11 : 14} width={28} x={faceInset + bandWidth / 2} y={face.top + (h - faceInset * 2) * 0.75} /> </g> </> ) : null} {design.rightBand !== undefined ? ( <> <rect data-plate-band="right" x={w - faceInset - rightBandWidth} y={faceInset} width={rightBandWidth} height={h - faceInset * 2} fill={euBlue} /> <g fill={light}> <Lettering text={design.rightBand} face="din" height={13} width={26} x={w - faceInset - rightBandWidth / 2} y={face.top + (h - faceInset * 2) * 0.75} /> </g> </> ) : null} {design.stripes ? ( <g fill="oklch(0.47 0.18 25)" opacity="0.8"> <rect x={face.left} y={face.top} width={face.right - face.left} height="1.7" /> <rect x={face.left} y={face.bottom - 1.7} width={face.right - face.left} height="1.7" /> </g> ) : null} <g ref={sheen} style={{ pointerEvents: "none" }}> <rect x="-80" width={w + 160} height={h} fill={`url(#${id}-sheen)`} /> </g> </g> <rect data-plate-rim="true" x={rim.x} y={rim.y} width={rim.width} height={rim.height} rx={rim.rx} fill="none" stroke={design.outline ?? `url(#${id}-rim)`} strokeWidth={geometry.stroke} /> {!world ? [w * 0.2, w * 0.8].map((x) => ( <g key={x}> <circle data-plate-rivet="true" cx={x} cy={rivetY} r={rivetRadius} fill={dark} opacity="0.48" /> <path d={`M${x - rivetRadius} ${rivetY}a${rivetRadius} ${rivetRadius} 0 0 0 ${rivetRadius * 2} 0`} fill="none" stroke={light} strokeOpacity="0.78" strokeWidth="0.8" /> </g> )) : null} <g fill={paint(design.ink)}> <Lettering text={text} face={design.glyphs} height={capHeight} width={face.right - face.left - 56} x={center} y={mainY} filter={emboss} row="registration" /> {showValidity ? ( <Lettering text={validity!} face="regular" height={17} width={90} x={center} y={validityY} filter={emboss} row="validity" /> ) : null} {design.caption ? ( <Lettering text={design.caption} face="din" height={8.5} width={240} x={center} y={face.top + 8} /> ) : null} </g> {showDebug ? ( <g data-plate-debug="true" pointerEvents="none"> <rect width={w} height={h} fill={`url(#${id}-grid)`} /> <rect x={rim.x} y={rim.y} width={rim.width} height={rim.height} rx={rim.rx} fill="none" stroke={euBlue} strokeWidth="1" vectorEffect="non-scaling-stroke" /> <path d={`M${w / 2} 0V${h}M0 ${h / 2}H${w}`} stroke={euBlue} strokeWidth="1" vectorEffect="non-scaling-stroke" strokeDasharray="3 3" /> </g> ) : null} </g> </svg> );}Build it yourself
This prompt has the plate files and a usage example. Paste it into a chat to get the same plate.
Why
The number is the easy read. The plate is what you guess from. A long white Indonesian plate and a yellow UK plate do not look like the same object. I draw them here as SVG, so the letters are text.
Size
Indonesia is 430×135. Private plates are white with black text. The older plates are black with white text. Germany's ratio is 4.73, with a blue band on the left. The UK rear plate is yellow and has no band. Japan is 330×165. California, New York, and Texas are 12 by 6 inches. Singapore is 370×110.
Indonesia 430×135
Germany ratio 4.73
California 12×6 inShuffle
Shuffle rolls a new country and a new number. A country chip stays in that country and rolls a new region and number. The old plate moves up and fades out. The new one fades in from 8px below. The swap is 220ms, ease-out, and it only animates transform and opacity.
duration: 0.22
ease: [0.32, 0.72, 0, 1]
transform + opacityReduced motion
With reduced motion on, that swap is a 150ms crossfade. The plate does not travel, and the pointer tilt stays off.
reduce ? { opacity: 0 } : { opacity: 0, y: 8 }Streak
The digit rolls in 150 ms. The slot stays two characters wide, so the line beside it does not shift.
Mode switch
One thumb moves in 220 ms. The labels keep the same weight.
Guessing the region.
Result
The round ends on a score, a dot for each plate, and two streak numbers.
Indonesia · Daily, 13 Oct
8 of 10 correct.Close. A couple of plates still look alike.
- Best streak
- 3
- Record
- 6
Pressed plate
Move the pointer across the plate. Tilt stops at 4°. Reduced motion keeps the light still. The menu switches the model.