fix(mascot): typecheck the clip metadata and keep the client ratchets green

The tuple cast on clips.meta.json failed tsc. The clip lookup and the 3D
opt-out move into mascotClips.ts so the component file only exports the
component, the crop offset is allowed as a deliberate physical side, and
MDancingTrek leaves the format baseline.
This commit is contained in:
Maurice
2026-10-11 21:33:31 +02:00
parent ee1b6a837f
commit aeaf3693ad
5 changed files with 43 additions and 40 deletions
-1
View File
@@ -1277,7 +1277,6 @@
"src/mobile/components/MBottomNav.test.tsx",
"src/mobile/components/MBottomNav.tsx",
"src/mobile/components/MChip.tsx",
"src/mobile/components/MDancingTrek.tsx",
"src/mobile/components/MDayImpactList.tsx",
"src/mobile/components/MDropdownPanel.tsx",
"src/mobile/components/MFab.tsx",
+2 -1
View File
@@ -1,3 +1,4 @@
{
"pages/tripPlanner/PlanMapStage.tsx": 4
"pages/tripPlanner/PlanMapStage.tsx": 4,
"mobile/components/trek3d/MascotClip.tsx": 1
}
@@ -1,5 +1,6 @@
import { useState } from 'react';
import MascotClip, { findClip, mascot3dEnabled } from './trek3d/MascotClip';
import MascotClip from './trek3d/MascotClip';
import { findClip, mascot3dEnabled } from './trek3d/mascotClips';
/**
* The TREK mascot — an empty-state character that IS the TREK mark: the logo is
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import meta from './clips.meta.json';
import { CLIPS } from './mascotClips';
/**
* The 3D TREK mascot as pre-rendered clips. Each clip is a plain H.264 MP4 with
@@ -17,48 +18,12 @@ import meta from './clips.meta.json';
* inside the full 88:96 box so every scene keeps the same scale. Only URLs are
* bundled; the files load when a mascot actually renders.
*/
const CLIPS = import.meta.glob('./clips/*.mp4', { query: '?url', import: 'default', eager: true }) as Record<
string,
string
>;
const META = meta as Record<string, { poke: number; crop: [number, number, number, number] }>;
const META: Record<string, { poke: number; crop: number[] }> = meta;
const FADE_MS = 140;
// The clips frame the figure with more air than the SVG, so they render at 1.6 times the requested size
const CLIP_SCALE = 1.6;
function clipKey(scene: string, mood: string, sceneMood: string) {
return mood === sceneMood ? scene : `${scene}--${mood}`;
}
export function findClip(scene: string, mood: string, sceneMood: string): string | null {
const key = clipKey(scene, mood, sceneMood);
return CLIPS[`./clips/${key}-light-loop.mp4`] ? key : null;
}
let webgl: boolean | null = null;
function hasWebgl(): boolean {
if (webgl === null) {
try {
webgl = !!document.createElement('canvas').getContext('webgl');
} catch {
webgl = false;
}
}
return webgl;
}
/** Opt-out for comparing against the SVG: localStorage `trek-mascot-3d` = `0`. */
export function mascot3dEnabled(): boolean {
try {
if (localStorage.getItem('trek-mascot-3d') === '0') return false;
} catch {
/* storage blocked: keep the default */
}
if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return false;
return hasWebgl();
}
function useIsDark() {
const read = () => document.documentElement.classList.contains('dark');
const [dark, setDark] = useState(read);
@@ -0,0 +1,37 @@
/** Clip lookup and the 3D opt-out, kept apart from the component so Fast Refresh keeps working. */
export const CLIPS = import.meta.glob('./clips/*.mp4', { query: '?url', import: 'default', eager: true }) as Record<
string,
string
>;
function clipKey(scene: string, mood: string, sceneMood: string) {
return mood === sceneMood ? scene : `${scene}--${mood}`;
}
export function findClip(scene: string, mood: string, sceneMood: string): string | null {
const key = clipKey(scene, mood, sceneMood);
return CLIPS[`./clips/${key}-light-loop.mp4`] ? key : null;
}
let webgl: boolean | null = null;
function hasWebgl(): boolean {
if (webgl === null) {
try {
webgl = !!document.createElement('canvas').getContext('webgl');
} catch {
webgl = false;
}
}
return webgl;
}
/** Opt-out for comparing against the SVG: localStorage `trek-mascot-3d` = `0`. */
export function mascot3dEnabled(): boolean {
try {
if (localStorage.getItem('trek-mascot-3d') === '0') return false;
} catch {
/* storage blocked: keep the default */
}
if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return false;
return hasWebgl();
}