YearScan
YearScan is the panel under the video with the year on it. Before the first scan the year is blank. Press the button and a beam colours it week by week, doubles back, and stops on a day. A card comes out of that cell, turns over, and lands in the video. Afterwards the panel lists the finds, and the scan can be played again.
Your year is hiding 4 scenes.
Achievements you earned and a few easter eggs. Each one joins your video when the scan finds it.
- Still hidden
- Still hidden
- Still hidden
- Still hidden
The readout under the panel is the demo's own state.
Installation
pnpm add @react-wrapped/scan @react-wrapped/core gsapimport "@react-wrapped/core/styles.css";
import "@react-wrapped/scan/styles.css";Usage
The scan holds no list of finds. You give it found and it tells you through onOpen when there is one more. The component below is the preview above.
import { useState } from "react";
import { type CalendarLevels, resolvePalette, type WrappedConfig } from "@react-wrapped/core";
import { type HiddenDrop, useOpenedDrops, YearScan } from "@react-wrapped/scan";
export function ScanTheYear({
calendar,
drops,
describeDay,
config,
}: {
/** The same calendar the video draws, so the scan hides its drops in the year the viewer just watched. */
calendar: CalendarLevels;
drops: readonly HiddenDrop[];
/** How a day reads on a found card, by its index in the calendar: "Mar 14". */
describeDay: (day: number) => string;
config: WrappedConfig;
}) {
// What has been opened so far. With `null` it is kept for this visit only. A key keeps it on this device.
const { opened, open } = useOpenedDrops(null);
const [running, setRunning] = useState(false);
return (
<div>
<YearScan
calendar={calendar}
palette={resolvePalette(config)}
drops={drops}
found={opened}
// Called when a card has been turned over and added. This is the moment its scene joins your video.
onOpen={open}
describeDay={describeDay}
onRunChange={setRunning}
/>
<output>
{`${opened.length} of ${drops.length} found${running ? ", scanning" : ""}. opened: [${opened.join(", ")}]`}
</output>
</div>
);
}Props
| Prop | Type | What it does |
|---|---|---|
calendar | CalendarLevels | The year the scan sweeps. Use the one the video draws, so both show the same year. |
palette | WrappedPalette | The video's palette: resolvePalette(config). The panel and the rarity colours follow it. |
drops | readonly HiddenDrop[] | Every hidden scene, found or not, in the order the scan opens them. With none, nothing is drawn. |
found | readonly string[] | The ids of the drops already opened. |
onOpen | (id: string) => void | A card was turned over. Add the id to found and put that drop's scene in the video. |
describeDay | (day: number) => string | How a day reads on a found card, by its index in calendar.levels. |
actions? | ReactNode | Extra buttons for the panel's header, such as a link to every achievement. |
sessionKey? | string | null | A session storage key. With one, each find keeps its day for the visit, so a replay opens the same cells. |
gate? | boolean | The scan is the page for now: the panel says the video is almost ready and offers nothing but the scan. |
onRunChange? | (running: boolean) => void | A scan started, or came to rest: finished, stopped or skipped to its end. |
className? | string | A class for the panel. |
classNames? | SlotClassNames<YearScanSlot> | A class per part, by slot name: "scan-start", "scan-card". See styling. |
What the viewer does
- Scan my year starts a run through every drop not found yet, one after another.
- The card comes out face down. A click turns it over, and a second click sends it into the video. Left alone it does both by itself, staying face up as long as its text takes to read.
- Find all skips the rest of a run and opens everything that is left.
- A click outside the card stops the run. A card already turned over still lands. One still face down goes back unopened.
- Scan again replays the finds once everything is found. Nothing new joins the video.
- The badges under the year list the finds. A click on one, or on its cell in the year, brings its card back up until Escape or another click.
Where a drop hides
The scan picks the day, at random among the active days of the year, and no two drops share one. So a drop has no date of its own: describeDay only says how the day the scan picked reads on the card. With a sessionKey each find keeps its day for the rest of the visit, so replays open the same cells. Finds from an earlier visit get a day as soon as the panel mounts.
gate and onRunChange
gate is for the scan first flow. The panel fills the box the video is about to take, its headline says the Wrapped is almost ready, and a found card lands on its badge because there is no timeline yet. onRunChange tells you when a run starts and when it comes to rest. Keep gate on while a run you started from the gate is still going, or the first find swaps the page out from under the ones after it.
import { useState } from "react";
// Inside your page component, next to the list of found ids.
const [running, setRunning] = useState(false);
const [passed, setPassed] = useState(false);
// The scan is the page until one run has come to rest with something found.
const gated = !passed && (found.length === 0 || running);
const onRunChange = (now: boolean) => {
setRunning(now);
if (!now && found.length > 0) setPassed(true);
};The from parts example has this wired into a whole page.
Rarity on screen
Every find flares its cell in its rarity colour. The rarer the drop, the longer the card hums before it turns, and a legendary one gets the full show: a flash, a turning foil border and sparkles. The colours come from the palette, so they change with the theme. See rarity and drops.
