Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function App() {
<Header />
<Stepper at={at} onGo={setAt} />

<main className="flex-1">
<main className="min-w-0 flex-1">
{at === 0 && <StepIdentity id={id} onChange={setId} />}
{at === 1 && (
<StepStory
Expand Down
6 changes: 5 additions & 1 deletion web/src/lib/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export const SAMPLE = {

/** Whatever the reader has typed so far — every field is optional because a
* half-filled form still deserves a preview. */
export type Tokens = Partial<Record<string, unknown>> & { song?: string; artist?: string };
export type Tokens = Partial<Record<string, unknown>> & {
song?: string;
artist?: string;
lyrics?: string[];
};

/** Fill the `{tokens}` a caption carries.
*
Expand Down
2 changes: 1 addition & 1 deletion web/src/stage/Meter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function Meter({ story, at, solo, onPick }: { story: Scene[]; at: number;
const band = BANDS.find((b) => secs <= b.max)!;

return (
<div className="nb p-3">
<div className="nb min-w-0 p-3">
<div className="flex items-baseline gap-2">
<span className={`text-3xl tabular-nums ${band.tone}`}>{secs.toFixed(0)}s</span>
<span className={`text-[10px] uppercase ${band.tone}`}>{band.label}</span>
Expand Down
2 changes: 1 addition & 1 deletion web/src/stage/Reel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function Reel({
}

return (
<div className="nb p-3">
<div className="nb min-w-0 p-3">
<Stage reel={reel} story={story} tick={tick} id={id} />
<Transport
tick={tick}
Expand Down
16 changes: 8 additions & 8 deletions web/src/stage/Stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Scene } from "../lib/acts";
import type { Tokens } from "../lib/sample";
import { actIcon } from "../lib/acts";
import { fill } from "../lib/sample";
import { drawCaption, drawStreak, CAPTION_H } from "./text";
import { drawCaption, drawKaraoke, drawStreak, CAPTION_H } from "./text";
import { drawStats, drawWall } from "./overlays";
import { SAMPLE } from "../lib/sample";

Expand Down Expand Up @@ -53,15 +53,20 @@ export function Stage({ reel, story, tick, id }: Props) {
if (scene?.act === "stats") drawStats(ctx, k);
else drawStreak(ctx, SAMPLE.streak, w);

drawCaption(ctx, actIcon(scene?.act), captionOf(scene, k, leaving, id), w, ground);
// the renderer picks one or the other, never both
if (scene?.act === "sing" && !leaving) {
drawKaraoke(ctx, k, ground, id.song ?? "", id.artist ?? "", id.lyrics ?? []);
} else {
drawCaption(ctx, actIcon(scene?.act), captionOf(scene, k, leaving, id), w, ground);
}
}, [reel, story, tick, cols, rows, id]);

return (
<canvas
ref={ref}
width={cols * CELL_W}
height={rows * CELL_H + CAPTION_H}
className="stage block w-full [image-rendering:pixelated]"
className="stage block w-full max-w-full [image-rendering:pixelated]"
aria-label="Preview of your banner"
/>
);
Expand All @@ -72,11 +77,6 @@ export function Stage({ reel, story, tick, id }: Props) {
function captionOf(scene: Scene | undefined, k: number, leaving: boolean, id: Tokens): string {
if (leaving) return "thanks for stopping by ~";
if (!scene) return "";
if (scene.act === "sing") {
const song = id.song?.trim() || "an old favourite";
const artist = id.artist?.trim() || "someone great";
return fill(`my fav song "${song}" - ${artist}`, id);
}
const line = scene.then && k >= GLOW_AT ? scene.then : (scene.say ?? "");
return fill(line, id);
}
35 changes: 35 additions & 0 deletions web/src/stage/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { glyph, icon } from "../wasm/awan_wasm";
export const SCALE = 3;
export const GLYPH = 8 * SCALE;
export const CAPTION_H = 56;
/** The karaoke line is smaller and sits up beside him, not under the ground —
* every number from gif.rs. */
export const LYRIC_SCALE = 2;
export const LYRIC_LIMIT = 18 * 33;
/** Ticks a lyric holds before the next one, from script.rs. */
export const LYRIC_HOLD = 30;
export const INK = "#9696a0"; // [150, 150, 160] in gif.rs — convert, don't eyeball
export const ACCENT = "#e6b464"; // [230, 180, 100]

Expand Down Expand Up @@ -79,3 +85,32 @@ export function drawStreak(ctx: CanvasRenderingContext2D, streak: number, w: num
drawBits(ctx, icon("fire"), x, 12, SCALE, ACCENT);
drawText(ctx, num, x + 8 * SCALE + SCALE * 2, 12, SCALE, ACCENT);
}

/** One karaoke line down the left while he sings on the right.
*
* Not a caption: `rasterize` picks *either* the strip under the ground *or*
* this, never both, and this one is half the size and up at his shoulder. The
* preview drew the intro line, in the caption strip, at caption size, forever
* — which meant the lyrics somebody had just typed never appeared at all.
*/
export function drawKaraoke(
ctx: CanvasRenderingContext2D,
k: number,
ground: number,
song: string,
artist: string,
lyrics: string[],
) {
const step = Math.floor(k / LYRIC_HOLD);
const [iconName, text] =
step === 0
? ["star", `my fav song "${song || "an old favourite"}" - ${artist || "someone great"}`]
: lyrics.length
? ["globe", lyrics[(step - 1) % lyrics.length]]
: ["globe", "la la la ~"];

const fit = Math.floor(Math.max(LYRIC_LIMIT - 24, 0) / (8 * LYRIC_SCALE));
const y = Math.floor(ground / 2) - 4 * LYRIC_SCALE;
drawBits(ctx, icon(iconName), 24, y, LYRIC_SCALE, ACCENT);
drawText(ctx, [...text].slice(0, fit).join(""), 24 + 8 * LYRIC_SCALE + 6, y, LYRIC_SCALE, INK);
}
2 changes: 1 addition & 1 deletion web/src/steps/StepIdentity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const FIELDS: { key: keyof Identity; label: string; hint: string }[] = [
export function StepIdentity({ id, onChange }: { id: Identity; onChange: (id: Identity) => void }) {
const filled = Object.values(id).some((v) => (Array.isArray(v) ? v.length : v));
return (
<div className="grid gap-4 lg:grid-cols-2">
<div className="grid min-w-0 gap-4 lg:grid-cols-2">
<Card title="About you" hint="these fill the {tokens} in his lines">
<div className="mb-3 flex gap-2">
<Button tone="gold" onClick={() => onChange(EXAMPLE)}>
Expand Down
6 changes: 3 additions & 3 deletions web/src/steps/StepStory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ export function StepStory({ story, beat, cast, solo, id, onStory, onBeat, onCast
const shown = solo >= 0 && story[solo] ? [story[solo]] : story;

return (
<div className="grid gap-4 xl:grid-cols-[1.4fr_1fr]">
<div className="flex flex-col gap-4">
<div className="grid min-w-0 gap-4 xl:grid-cols-[1.4fr_1fr]">
<div className="flex min-w-0 flex-col gap-4">
<Reel story={shown} toml={castOf(cast).toml} id={id} onBeat={(i) => onBeat(solo >= 0 ? solo : i)} />
<Meter story={story} at={beat} solo={solo} onPick={(i) => onSolo(i === solo ? -1 : i)} />
</div>

<div className="flex flex-col gap-4">
<div className="flex min-w-0 flex-col gap-4">
<Card title="Who plays him" tone="text-grape-ink">
<div className="flex flex-wrap gap-2">
{CAST.map((c) => (
Expand Down
30 changes: 27 additions & 3 deletions web/src/story/SceneList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
import {
DndContext,
KeyboardSensor,
PointerSensor,
TouchSensor,
closestCenter,
useSensor,
useSensors,
} from "@dnd-kit/core";
import type { DragEndEvent } from "@dnd-kit/core";
import { SortableContext, arrayMove, verticalListSortingStrategy } from "@dnd-kit/sortable";
import {
SortableContext,
arrayMove,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import type { Scene } from "../lib/acts";
import { SceneRow } from "./SceneRow";

Expand All @@ -13,7 +26,18 @@ export function SceneList({
playing: number;
onChange: (s: Scene[]) => void;
}) {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }));
// A finger that starts on the handle has to mean "drag", not "scroll" — and
// a phone can't tell until you've already moved, by which point the page has
// gone. The delay is the tell: hold, then drag. Move first and it scrolls,
// which is what a finger on a long page usually wants.
//
// Keyboard too. This is a list you reorder; a list you can only reorder by
// dragging is a list some people can't reorder.
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 180, tolerance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
const onDragEnd = ({ active, over }: DragEndEvent) => {
if (over && active.id !== over.id) onChange(arrayMove(story, +active.id, +over.id));
};
Expand Down
7 changes: 5 additions & 2 deletions web/src/story/SceneRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export function SceneRow({ id, scene, live, onEdit, onDrop }: Props) {
<button
{...attributes}
{...listeners}
className="cursor-grab px-1 text-faint active:cursor-grabbing"
aria-label={`Reorder ${info.label}`}
// without this a touch-drag scrolls the page instead, and the row
// never moves — the gesture is swallowed before dnd-kit sees it
style={{ touchAction: "none" }}
className="-m-1 cursor-grab p-2 text-faint active:cursor-grabbing"
aria-label={`Reorder ${info.label}. Press space, then use the arrow keys.`}
>
</button>
Expand Down
2 changes: 1 addition & 1 deletion web/src/ui/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function Card({
children: ReactNode;
}) {
return (
<section className="nb p-4">
<section className="nb min-w-0 p-4">
{title && (
<div className="mb-4 flex items-baseline gap-2">
<h2 className={`text-sm uppercase ${tone}`}>{title}</h2>
Expand Down
Loading