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 MedicalPalette frozen 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 MedicalPaletteRGB frozen RGB-triple palette for alpha interpolation via rgba(${r}, ${g}, ${b}, ${alpha}) templates.
  • The MedicalTimings motion scale (snap, quick, base, reveal) in milliseconds.
  • The MedicalSpacing 8-step spacing scale (s1..s8) in px.
  • The MedicalRadii border-radius scale (sm, md, lg, xl) in px.
  • The MedicalShadow frozen shadow descriptors for panel and raised variants — cool-blue tinted, soft blur, gentle offset.
  • The medFontHead Cal Sans headline-font string builder for ctx.font.
  • The medFontBody Space Grotesk body-font string builder for ctx.font.
  • The medEaseOutSnap pneumatic easing curve — a numerical approximation of CSS cubic-bezier(0.16, 1, 0.3, 1) good to about 0.01 across [0, 1].
  • The medApplyPanelShadow helper that writes the chosen shadow variant onto a 2D canvas context.
  • The medClearShadow helper that resets all shadow state on a 2D canvas context.
  • The medDrawPanel helper that paints a rounded-rect medical panel with fill, hairline border, and optional drop shadow.
  • The medRoundRectPath helper that traces a rounded-rect path without painting, for clip regions or custom paint ordering.
  • The medDrawAccentStripe helper that paints a thin accent stripe along one edge of a rect.

READS FROM

  • The CanvasRenderingContext2D passed in by callers — including ctx.shadowColor, ctx.shadowBlur, ctx.shadowOffsetX, ctx.shadowOffsetY, ctx.fillStyle, ctx.strokeStyle, ctx.lineWidth, and ctx.roundRect.
  • Numeric inputs (sizes, weights, coordinates, radii, easing parameter t) from callers.
  • No globals, no stores, no DOM, no getComputedStyle lookups — by design, to avoid flash-of-wrong-color on first paint and to keep runtime cost at zero.

PUSHES TO

  • The caller-provided CanvasRenderingContext2DmedApplyPanelShadow mutates the four shadow properties, medClearShadow resets them, medDrawPanel issues save/beginPath/roundRect/fill/stroke/restore calls, medRoundRectPath issues beginPath/roundRect, and medDrawAccentStripe writes fillStyle and issues fillRect.
  • Return values: medFontHead/medFontBody return font strings, medEaseOutSnap returns an eased value in [0, 1], medApplyPanelShadow returns the ctx for chainable use.
  • No telemetry, no Supabase, no Sentry, no stores.

DOES NOT

  • Does not read CSS at runtime. There is no getComputedStyle lookup; 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.css and must be edited in lockstep — when tokens change there, mirror them here; when tokens are added here, mirror them in medical.css.
  • Does not load fonts. Cal Sans and Space Grotesk are loaded via Google Fonts in index.html; this module only formats the ctx.font strings.
  • Does not call ctx.save/ctx.restore around medApplyPanelShadow/medClearShadow. Callers handle isolation if they need it. medDrawPanel does wrap its own paint in save/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. medEaseOutSnap is a pure function of t; 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 for ctx.fillStyle / ctx.strokeStyle.
  • MedicalPaletteRGB: Readonly<{ panel, panelRaised, panelRecessed, border, borderActive, text, textSecondary, textMuted, accent, accentBright, statusGood, statusWarn, statusBad }> — each value a readonly [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 tinted rgba colors.

Entry points

  • medFontHead(sizePx: number, weight?: number = 600): string — returns a Cal Sans font string for ctx.font.
  • medFontBody(sizePx: number, weight?: number = 400): string — returns a Space Grotesk font string for ctx.font.
  • medEaseOutSnap(t: number): number — pneumatic snap-and-settle easing; clamps t <= 0 to 0 and t >= 1 to 1, otherwise returns 1 - (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 — resets shadowColor to 'transparent' and the three numeric shadow properties to 0.
  • medDrawPanel(ctx, x, y, w, h, opts?: { radius?, fill?, border?, borderWidth?, shadow? }): void — paints a rounded-rect panel; defaults are radius = MedicalRadii.md, fill = MedicalPalette.panelRaised, border = MedicalPalette.border, borderWidth = 1, shadow = true.
  • medRoundRectPath(ctx, x, y, w, h, radius?: number = MedicalRadii.md): void — issues beginPath and roundRect only; 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.css for 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 before fill, then immediately call medClearShadow before stroking, so the hairline border is not double-shadowed. Callers who paint their own shadowed shapes should follow the same apply -> fill -> clear -> stroke order.
  • medApplyPanelShadow deliberately does not wrap in ctx.save/ctx.restore. The caller decides whether shadow state needs isolation; pairing it with medClearShadow is 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 at rgba(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, the system-ui, sans-serif fallback in the font string takes over silently.
  • medRoundRectPath and medDrawPanel both rely on the standard CanvasRenderingContext2D.roundRect API; 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 in MedicalPaletteRGB. Callers needing alpha-interpolated versions must add them to both records together.