Skip to content
react-wrapped

Scanner

The scanner is the part of a Wrapped people come back to. Some scenes of the video start out hidden somewhere in the year: achievements that were earned, an easter egg or two. A beam sweeps the calendar, locks onto a day, a card flies out and turns over, and that scene joins the video. Use it when part of the story should be found, not just watched.

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
0 of 4 found. opened: []

Press the button. Click a card to turn it, click again to add it.

Installation

bash
pnpm add @react-wrapped/scan @react-wrapped/core gsap
tsx
import "@react-wrapped/core/styles.css";
import "@react-wrapped/scan/styles.css";

What is in it

How a find becomes a scene

  1. You decide which scenes are hidden and describe each as a HiddenDrop: a name, a rarity, a line and pixel art.
  2. The scan gets all of them in drops and the ids already opened in found.
  3. When a card is turned over the scan calls onOpen(id).
  4. You add the id to found and build your scenes again, this time with that drop's scene in them. Give that scene the key drop- followed by the drop's id.
  5. The player rebuilds only what changed, and the card flies to the timeline segment with that key.

So a drop is never a special kind of scene. It is an ordinary scene, usually a fact scene, that your code leaves out until its id is in found.

Scan first

For someone's own year the scan can come before the video. With opening.gate on WrappedView, a creator who has opened nothing gets the scan as the whole page. It says "Your Wrapped is almost ready." and offers one button. When the scan has run and found something, the video takes its place and the scan moves under it. Anyone who has scanned before, and anyone only watching a shared link, gets the video straight away.

The whole page, starting from the scan.

scan-first.tsx
import { useMemo, useState } from "react";

import {
  type CalendarLevels,
  DEFAULT_WRAPPED_CONFIG,
  type HiddenDrop,
  type SceneInstance,
  useOpenedDrops,
  type WrappedConfig,
  type WrappedSubject,
  WrappedView,
} from "@react-wrapped/kit";

/** What your director returns for one year, given the drops opened so far. */
interface Cut {
  subject: WrappedSubject;
  seed: string;
  /** The story, with a scene for every drop that has been opened. */
  scenes: readonly SceneInstance[];
  calendar: CalendarLevels;
  /** Every hidden scene, found or not. */
  drops: readonly HiddenDrop[];
}

export function ScanFirst({
  direct,
  describeDay,
  storageKey,
}: {
  direct: (opened: readonly string[]) => Cut;
  describeDay: (day: number) => string;
  /** Where the opened drops are remembered on this device. Null keeps them for this visit only. */
  storageKey: string | null;
}) {
  const [config, setConfig] = useState<WrappedConfig>(DEFAULT_WRAPPED_CONFIG);
  const { opened, open, ready } = useOpenedDrops(storageKey);
  // Directed again whenever a drop is opened: that is how a found scene gets into the video.
  const cut = useMemo(() => direct(opened), [direct, opened]);
  // Until storage has been read, nobody knows whether this creator has scanned before.
  if (!ready) return null;
  return (
    <WrappedView
      subject={cut.subject}
      seed={cut.seed}
      scenes={cut.scenes}
      config={config}
      onConfigChange={setConfig}
      opening={{
        calendar: cut.calendar,
        drops: cut.drops,
        found: opened,
        onOpen: open,
        describeDay,
        // The scan is the page until it has run once. Without this the video plays and the scan sits under it.
        gate: true,
      }}
    />
  );
}

opening is everything WrappedView needs to run the scan for you:

KeyTypeWhat it does
calendarCalendarLevelsHanded on to the scan.
dropsreadonly HiddenDrop[]Handed on to the scan.
foundreadonly string[]Handed on to the scan.
onOpen(id: string) => voidHanded on to the scan.
describeDay(day: number) => stringHanded on to the scan.
actions?ReactNodeHanded on to the scan.
sessionKey?string | nullHanded on to the scan.
gate?booleanPuts the scan first. Until the creator has run it once, the scan is the page and the video is not shown.

The full example has a switch for both starts. To run the same flow without WrappedView, see build the page from parts.

Last updated . Read this page as Markdown.