Add /set/spill: on-set green screen spill checker
Deploy / deploy (push) Successful in 35s

- Phone camera or photo -> WebGL shader that classifies every pixel against
  the sampled screen colour; colour-blind friendly (stripes, motion, words)
- Views: Spill, Screen (flat colour), rough live Key, Evenness
- Spill = worse of lean toward the screen hue and the green/blue despill
  limit, tuned on a real Alexa car-interior plate (teal screen, green bounce)
- Plain-English verdict incl. widespread colour cast; tap for readouts
- Pick screen / pick white (neutral reference), freeze, exposure/WB lock
- Service worker caches each /set page under its own path (spill page no
  longer overwrites the markers page offline); link from /set

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-10-07 11:55:24 +02:00
parent 250bf5ac19
commit 52d492506b
5 changed files with 1156 additions and 5 deletions
+10
View File
@@ -39,6 +39,16 @@ markers_laptop_1920x1080.png # name is optional
Kinds: `phone`, `tablet`, `laptop`, `monitor`. Any other PNG shows under "Other".
Bump `CACHE` in `public/set/sw.js` when replacing an existing image so phones fetch the new one.
## On-set spill checker (`/set/spill`)
Point a phone camera (or load a photo) at a green or blue screen setup. A WebGL shader
classifies every pixel against the sampled screen colour and shows spill as stripe
patterns plus a plain-English verdict. It is designed for colour-blind use, so it never
relies on red vs green. Views: Spill, Screen (keyed area as one flat colour), Key (rough
live key with despill) and Evenness (screen brightness bands). "Pick white" sets a
neutral reference, like a white balance pick. The code is in `components/set/SpillChecker.tsx`;
thresholds are tuned on a real Alexa car-interior plate. The camera needs https.
## Structure
```
+25
View File
@@ -0,0 +1,25 @@
import type { Metadata, Viewport } from "next";
import SpillChecker from "@/components/set/SpillChecker";
// On-set green/blue screen spill checker. Like /set, not linked from the site and
// kept out of search results and the sitemap.
export const metadata: Metadata = {
title: "Spill checker",
description: "Two Tales VFX on-set green screen spill checker.",
robots: { index: false, follow: false },
manifest: "/set/manifest.webmanifest",
appleWebApp: { capable: true, title: "Spill", statusBarStyle: "black-translucent" },
};
export const viewport: Viewport = {
themeColor: "#000000",
viewportFit: "cover",
};
export default function SpillPage() {
return (
<main className="h-dvh bg-black">
<SpillChecker />
</main>
);
}
+12
View File
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import Link from "next/link";
import type { Kind, MarkerExtra, MarkerScreen } from "@/lib/markers";
type Mode = "full" | "edge";
@@ -362,6 +363,17 @@ export default function MarkerPicker({ screens, extras }: { screens: MarkerScree
</p>
</details>
<Link
href="/set/spill/"
className="mt-8 flex items-center justify-between rounded-xl bg-white/5 px-4 py-4 transition hover:bg-white/10"
>
<span>
<span className="block font-medium">Spill checker</span>
<span className="block text-sm text-muted">Point your phone at the green screen setup to find spill</span>
</span>
<span aria-hidden className="text-xl text-muted">→</span>
</Link>
<p className="mt-10 text-center text-xs leading-relaxed text-muted">
Touch is locked while a marker is up · hold the top-left corner 3 s for controls · screen stays awake
</p>
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -1,4 +1,4 @@
// Offline cache for /set (screen markers). Crews often have no signal on set:
// Offline cache for /set (screen markers, spill checker). Crews often have no signal on set:
// once the page has loaded once, it and every marker image work offline.
// Must match OFFLINE_CACHE in components/set/MarkerPicker.tsx
const CACHE = "ttvfx-set-v1";
@@ -22,16 +22,20 @@ self.addEventListener("fetch", (event) => {
const url = new URL(req.url);
if (url.origin !== self.location.origin) return;
// Page: network first so updates arrive, cached copy when offline
// Pages (/set/, /set/spill/): network first so updates arrive, cached copy when offline.
// Each page is cached under its own path so one never replaces the other.
if (req.mode === "navigate") {
const page = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
event.respondWith(
fetch(req)
.then((res) => {
const copy = res.clone();
caches.open(CACHE).then((c) => c.put("/set/", copy));
if (res.ok) {
const copy = res.clone();
caches.open(CACHE).then((c) => c.put(page, copy));
}
return res;
})
.catch(() => caches.match("/set/")),
.catch(() => caches.match(page).then((hit) => hit || caches.match("/set/"))),
);
return;
}