WrappedPlayer
WrappedPlayer is the video. It mounts every scene on one stage, builds a single GSAP timeline from them with a transition between each pair, and puts a transport under it: play, replay, the timeline, a clock and full screen. Use it whenever you want a video on a page without the rest of WrappedView.
Tap the left or right third of the frame to step between scenes.
Installation
pnpm add @react-wrapped/player @react-wrapped/core gsapimport "@react-wrapped/core/styles.css";
import "@react-wrapped/player/styles.css";Usage
Give it who the video is about, a seed, the scenes and a config. The component below is the preview above: the player in a box with a height, and five buttons that drive it through its ref.
import { type CSSProperties, useRef, useState } from "react";
import type { SceneInstance, WrappedConfig, WrappedSubject } from "@react-wrapped/core";
import { WrappedPlayer, type WrappedPlayerHandle } from "@react-wrapped/player";
// The player fits its frame into the room it is given, so the room needs a height.
const ROOM: CSSProperties = { display: "grid", height: 560 };
export function PlayerRemote({
subject,
seed,
scenes,
config,
}: {
subject: WrappedSubject;
seed: string;
scenes: readonly SceneInstance[];
config: WrappedConfig;
}) {
const player = useRef<WrappedPlayerHandle>(null);
const [said, setSaid] = useState("Nothing measured yet.");
const measure = () => {
const handle = player.current;
if (!handle) return;
// Both are in seconds at normal speed: the length of the video, and where each scene starts in it.
const starts = handle.scenes().map((scene) => `${scene.label} at ${scene.time.toFixed(1)}s`);
setSaid(`${handle.duration().toFixed(1)}s in all. ${starts.join(", ")}.`);
};
return (
<div>
<div style={ROOM}>
<WrappedPlayer ref={player} subject={subject} seed={seed} scenes={scenes} config={config} />
</div>
<div role="group" aria-label="Drive the player from outside">
<button type="button" onClick={() => player.current?.play()}>
Play
</button>
<button type="button" onClick={() => player.current?.pause()}>
Pause
</button>
<button type="button" onClick={() => player.current?.restart()}>
Restart
</button>
<button type="button" onClick={() => player.current?.seek(player.current.duration())}>
Jump to the end
</button>
<button type="button" onClick={measure}>
Measure
</button>
</div>
<output>{said}</output>
</div>
);
}Give it a height
The frame is always drawn at its real size and scaled to fit the room the player has: the width of its container and the height left above the transport. A container with no height gives it a room of 320 pixels, which makes a small 9:16 frame. Put the player in a box with a height, or in a flex column and let it grow. On a phone the frame never takes more than 72 percent of the screen's height, so the controls stay in view.
Props
| Prop | Type | What it does |
|---|---|---|
ref? | Ref<WrappedPlayerHandle> | Hands out the player's handle, to drive it from your own code. |
subject | WrappedSubject | Who the video is about. Shown in the corner of every frame. |
seed | string | Picks the transitions and the small variations in motion. The same seed always plays the same video. |
scenes | readonly SceneInstance[] | The scenes that play, in order. Keep the array the same between renders: a new one rebuilds the timeline. |
config | WrappedConfig | Palette, format, cell shape, speed, grain and picked transitions. |
autoplay? | boolean | Starts playing as soon as the video is built. On unless you say otherwise. |
editing? | TimelineEditing & { scenes: readonly SceneInstance[] } | Turns the timeline into the editor. Leave it out for someone who only watches. |
className? | string | A class for the player's root, for sizing it in your layout. |
classNames? | SlotClassNames<WrappedPlayerSlot | TimelineSlot> | A class per part, by slot name: "player-play", "timeline-segment-bar". See styling. |
The subject
| Key | Type | What it is |
|---|---|---|
handle | string | Shown as @handle in the frame's top corner. |
name? | string | null | The person's display name. The frame shows the handle. This one is for your own page. |
avatarUrl? | string | null | Their picture. Left out, the first two letters of the handle on a disc. |
caption | string | The small line under the handle: what this Wrapped is called. |
period? | string | The window the numbers cover, already formatted. The frame does not draw it: write it into a scene, such as the opening's line. |
mark? | string | readonly string[] | A logo pressed into the bottom right corner, as SVG path data. See marks. |
brand? | string | A quiet line in the bottom left: a site address or a product name. |
The handle
ref gives you a WrappedPlayerHandle. Every player has its own, so any number of them can share a page. All times are seconds of the video at normal speed, whatever speed the config sets.
| Method | Type | What it does |
|---|---|---|
play | () => void | Plays from where the playhead is. At the end of the video it starts over. |
pause | () => void | Stops where it is. |
restart | () => void | Plays from the first frame. |
seek | (seconds: number) => void | Jumps to a moment, in seconds at normal speed, and stays there paused. |
duration | () => number | The length of the video in seconds at normal speed. Zero while there is no video. |
scenes | () => readonly SceneMark[] | Each playing scene with its key, its label and the time it starts at, in play order. |
take | () => StageTake | null | Lends out the stage and the playhead for making a file. See video export. |
Controls
- The frame. A tap on the left third goes back to the start of the scene, or to the one before right after a cut. The right third goes on to the next scene. The middle pauses and plays.
- The buttons. Play or pause, play from the start, full screen. They are the accessible controls. The frame taps are a shortcut for touch.
- The keyboard. With focus in the transport, the left and right arrows step between scenes and
ftoggles full screen. - Full screen plays in a dark room, whatever the page's theme is.
When the video is rebuilt
The timeline is rebuilt when the scenes array, the format, the seed or config.cuts changes. The playhead stays where it was, and a scene whose content did not change keeps the animation it already built, so a scene joining mid-play costs almost nothing. Speed changes without a rebuild. This is why the scenes array and the config should come from state or useMemo: a fresh array on every render rebuilds the video on every render.
