PURPOSE
Canvas-side mirror of the medical-UI design tokens defined in src/metagame/styles/medical.css. The medical-UI design language is rendered in two languages — CSS for DOM surfaces and TS constants for canvas surfaces — and this module is the canvas half of that bundle. It is the foundational primitive that unblocks canvas-rendered surfaces (level-up cards, weapon/artifact/shooting-star cards, in-canvas reward screens, HUD bars) using the same palette, spacing, typography, motion, and shadow vocabulary as DOM-side medical-UI components.
OWNS
- The
MedicalPalettefrozen hex-string palette covering surfaces (panel,panelRaised,panelRecessed,scrim,seam), borders (border,borderStrong,borderActive,borderCritical,borderSuccess), text (text,textSecondary,textMuted,textInverse), accents (accent,accentBright,accentDeep,amber,magenta), and status (statusGood,statusWarn,statusBad). - The
MedicalPaletteRGBfrozen RGB-triple palette for alpha interpolation viargba(${r}, ${g}, ${b}, ${alpha})templates. - The
MedicalTimingsmotion scale (snap,quick,base,reveal) in milliseconds. - The
MedicalSpacing8-step spacing scale (s1..s8) in px. - The
MedicalRadiiborder-radius scale (sm,md,lg,xl) in px. - The
MedicalShadowfrozen shadow descriptors forpanelandraisedvariants — cool-blue tinted, soft blur, gentle offset. - The
medFontHeadCal Sans headline-font string builder forctx.font. - The
medFontBodySpace Grotesk body-font string builder forctx.font. - The
medEaseOutSnappneumatic easing curve — a numerical approximation of CSScubic-bezier(0.16, 1, 0.3, 1)good to about 0.01 across[0, 1]. - The
medApplyPanelShadowhelper that writes the chosen shadow variant onto a 2D canvas context. - The
medClearShadowhelper that resets all shadow state on a 2D canvas context. - The
medDrawPanelhelper that paints a rounded-rect medical panel with fill, hairline border, and optional drop shadow. - The
medRoundRectPathhelper that traces a rounded-rect path without painting, for clip regions or custom paint ordering. - The
medDrawAccentStripehelper that paints a thin accent stripe along one edge of a rect.
READS FROM
- The
CanvasRenderingContext2Dpassed in by callers — includingctx.shadowColor,ctx.shadowBlur,ctx.shadowOffsetX,ctx.shadowOffsetY,ctx.fillStyle,ctx.strokeStyle,ctx.lineWidth, andctx.roundRect. - Numeric inputs (sizes, weights, coordinates, radii, easing parameter
t) from callers. - No globals, no stores, no DOM, no
getComputedStylelookups — by design, to avoid flash-of-wrong-color on first paint and to keep runtime cost at zero.
PUSHES TO
- The caller-provided
CanvasRenderingContext2D—medApplyPanelShadowmutates the four shadow properties,medClearShadowresets them,medDrawPanelissuessave/beginPath/roundRect/fill/stroke/restorecalls,medRoundRectPathissuesbeginPath/roundRect, andmedDrawAccentStripewritesfillStyleand issuesfillRect. - Return values:
medFontHead/medFontBodyreturn font strings,medEaseOutSnapreturns an eased value in[0, 1],medApplyPanelShadowreturns the ctx for chainable use. - No telemetry, no Supabase, no Sentry, no stores.
DOES NOT
- Does not read CSS at runtime. There is no
getComputedStylelookup; the medical CSS tokens are mirrored as hand-maintained TS constants. - Does not own the DOM-side design tokens. Those live in
src/metagame/styles/medical.cssand must be edited in lockstep — when tokens change there, mirror them here; when tokens are added here, mirror them inmedical.css. - Does not load fonts. Cal Sans and Space Grotesk are loaded via Google Fonts in
index.html; this module only formats thectx.fontstrings. - Does not call
ctx.save/ctx.restorearoundmedApplyPanelShadow/medClearShadow. Callers handle isolation if they need it.medDrawPaneldoes wrap its own paint insave/restore. - Does not render any specific surface (cards, HUD bars, modals). It is a primitive toolkit; surface drawing lives in
hud.ts, card renderers, and other rendering modules. - Does not animate or schedule.
medEaseOutSnapis a pure function oft; the caller drives time.
Signals
MedicalPalette: Readonly<{ panel, panelRaised, panelRecessed, scrim, seam, border, borderStrong, borderActive, borderCritical, borderSuccess, text, textSecondary, textMuted, textInverse, accent, accentBright, accentDeep, amber, magenta, statusGood, statusWarn, statusBad }>— hex strings ready forctx.fillStyle/ctx.strokeStyle.MedicalPaletteRGB: Readonly<{ panel, panelRaised, panelRecessed, border, borderActive, text, textSecondary, textMuted, accent, accentBright, statusGood, statusWarn, statusBad }>— each value areadonly [r, g, b]triple.MedicalTimings: Readonly<{ snap: 140, quick: 220, base: 320, reveal: 480 }>— milliseconds.MedicalSpacing: Readonly<{ s1: 4, s2: 8, s3: 12, s4: 16, s5: 24, s6: 32, s7: 48, s8: 64 }>— px.MedicalRadii: Readonly<{ sm: 4, md: 8, lg: 12, xl: 16 }>— px.MedicalShadow: Readonly<{ panel: { color, blur, offsetX, offsetY }, raised: { color, blur, offsetX, offsetY } }>— shadow descriptors with cool-blue tintedrgbacolors.
Entry points
medFontHead(sizePx: number, weight?: number = 600): string— returns a Cal Sans font string forctx.font.medFontBody(sizePx: number, weight?: number = 400): string— returns a Space Grotesk font string forctx.font.medEaseOutSnap(t: number): number— pneumatic snap-and-settle easing; clampst <= 0to0andt >= 1to1, otherwise returns1 - (1 - t)^3.5.medApplyPanelShadow(ctx, variant?: 'panel' | 'raised' = 'panel'): CanvasRenderingContext2D— writes the chosen shadow variant onto the ctx and returns it.medClearShadow(ctx): void— resetsshadowColorto'transparent'and the three numeric shadow properties to0.medDrawPanel(ctx, x, y, w, h, opts?: { radius?, fill?, border?, borderWidth?, shadow? }): void— paints a rounded-rect panel; defaults areradius = MedicalRadii.md,fill = MedicalPalette.panelRaised,border = MedicalPalette.border,borderWidth = 1,shadow = true.medRoundRectPath(ctx, x, y, w, h, radius?: number = MedicalRadii.md): void— issuesbeginPathandroundRectonly; caller paints.medDrawAccentStripe(ctx, x, y, w, h, edge?: 'left' | 'right' | 'top' | 'bottom' = 'left', thickness?: number = 2, color?: string = MedicalPalette.accentBright): void— fills a stripe of the given thickness along the chosen edge.
Pattern notes
- Two-language single-source design tokens. The same token bundle is expressed twice — once in
medical.cssfor DOM and once here for canvas — with a hard rule that both files stay in sync. There is no runtime bridge; consistency is maintained by editing them together. The rationale stated in-file is to avoid flash-of-wrong-color on first paint and to keep runtime cost at zero. - All exported records use
Object.freeze, so palette, spacing, radii, timing, and shadow values are immutable at runtime. - Shadow paint follows a strict pattern in
medDrawPanel: apply shadow beforefill, then immediately callmedClearShadowbefore stroking, so the hairline border is not double-shadowed. Callers who paint their own shadowed shapes should follow the sameapply -> fill -> clear -> strokeorder. medApplyPanelShadowdeliberately does not wrap inctx.save/ctx.restore. The caller decides whether shadow state needs isolation; pairing it withmedClearShadowis the cheap alternative when full save/restore is not needed.- The pneumatic easing is a hand-fit closed-form approximation (
1 - u * u * u * Math.sqrt(u)) instead of a numerical cubic-bezier solver — cheap enough for per-frame use and accurate to about 0.01. - The shadow palette is intentionally cool-blue tinted (
rgba(20, 30, 50, ...)) and softly blurred. The in-file comment contrasts this with the prior V32 hard-offset noir shadow atrgba(0, 0, 0, 0.55); medical UI is “hospital-light”. - Fonts are formatted but not loaded. The module assumes Cal Sans and Space Grotesk have already been pulled in via Google Fonts in
index.html. If a font is missing, thesystem-ui, sans-seriffallback in the font string takes over silently. medRoundRectPathandmedDrawPanelboth rely on the standardCanvasRenderingContext2D.roundRectAPI; no polyfill or fallback is shipped with this module.- The amber and magenta accents and the inverse text token live in
MedicalPalette(hex) only — they are not mirrored inMedicalPaletteRGB. Callers needing alpha-interpolated versions must add them to both records together.