@ntmats/hc-ui 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,4 +95,4 @@ Fixed colors that stay the same in both themes are HeroClix tokens: `color-dial-
95
95
 
96
96
  `PhysicalCardView` is the printed card in HCUnits' format (375×525 px, absolute positions), scaled to its container's width: front and back side by side while each keeps about 86% of its size, otherwise one under the other, never above 1.2×. With `fitHeight` the front also fits the visible height of the scrolling area found by `fitHeightWithin`. Everything comes resolved: the backgrounds (`images.front`, `images.back`, `images.legacy`, `layout.rarityImage`, `layout.dialImage`: addresses the app serves; HCUI's are in its `public/card/` and do not ship here), the title (its type shrinks from 28px to fit), header lines, team abilities (buttons that open a tip, with the shield the caller passes in `teamAbilityIcon`), the collection number and set symbol, the token (`portrait` drawn by the caller with the class the card gives, `terrain` art or an `object` ring in its color), the bystander's token replica, keyphrases, the rules (already translated, with an optional `icon`; tapping a paragraph calls `onCopy(copyText)`), improved abilities, the printed dial as a node (`dial.node`, usually `CombatDialView` with `presentation="printed"`), the terrain dial, points (buttons that call `onSelectPoint`, an object's line or nothing) and the black back bar. Rules that do not fit the front move to the backs, two columns each, one block at a time (`nextPhysicalCardDistribution`); the split starts over when `contentKey` changes. Under a mouse the card tilts up to 3° toward the pointer, with parallax and a shine (nearly still over the dial); on a phone only the focus view moves, by the motion sensor (`requestPhysicalCardMotionAccess` asks iOS on the first touch). Holding the card with a finger for 450 ms calls `onRequestFocus`; the caller opens `PhysicalCardFocusView` (a modal `<dialog>` over a dimmed, blurred backdrop; Esc and a tap outside, once the opening finger is lifted, call `onClose`) with a second card with `focus`, which shows one side at a time and draws `focusActions({ nextSide, backs, flip })` under it (the library's `Button` `outline` there gets the surface fill). Tips are controlled as in the dial: hover with a mouse opens a passive tip described by the control, a press or Enter opens it as a dialog, leaving closes the passive one; the caller keeps `tip` and draws it in `tipSlot`. Paper, ink, shine and grain follow the theme (`color-card-*`, `filter-card-background`, `blend-card-*`, `opacity-card-grain*`); the printed black and white, the team-up band and the terrain dial are fixed (`color-card-print-*`, `color-card-team-up`, `color-card-terrain-panel*`). Letters are upright (`--hc-rond: 0` on the card, which its printed dial reads too). Motion only with a fine pointer or in the phone's focus view, and none under `prefers-reduced-motion`.
97
97
 
98
- `MapBoardView` draws a map already fetched (`map`: the hcmaps JSON, `HCMapJson`) with the hcmaps engine (`HCMap`) in a canvas named by `canvasLabel`, square-cornered and white in both themes (`color-map-board`), and shades the inspected square or draws the inspected edge (`selection`, from `inspectMap`; `color-map-selection*`). `onInspect(x, y, clientX, clientY)` gets the pointer in squares: mouse and pen while passing over, touch on press; `onLeave` when the mouse leaves. Other attributes (role, `tabIndex`, `onKeyDown`, `aria-*`) go to the container, so the caller adds the keyboard (HCUI: arrows move one square, Esc closes). `MapBoardPlaceholder` is the empty grid while it loads, a busy status whose `label` is visually hidden by the library. `MapTooltipView` is fixed next to `anchor` without leaving the screen (portal it out of a clipping container): the title, a wall's color, and for each square (one, or two on an edge) its coordinate, terrain and elevation swatches with the caller's names, the environment (a node, e.g. `MapEnvironmentView`, or `environmentNote`) and notes. `MapSwatch` is the board's square in a legend; `MapLegendView` lists them (the 3D model's legend) with children after; `MapEnvironmentView` shows indoor/outdoor with the caller's icons (black line icons, turned light in the dark theme by `filter-map-environment-icon`) and names. `renderMapThumbnail(json, height)` returns a PNG data URL. The engines are plain functions with no DOM beyond a canvas context: `inspectMap` (square, edge or wall at a point, in squares), `mapSquare`, `mapMaxLevel`, `mapElevationLevel`, `mapSquareCoordinate`; `buildMapScene` (the 3D model's heights, walls, ramps and ziplines), the camera (`makeMapProjection`, `pickMapScene`, `zoomMapCamera`, `clampMapPan`) and `drawMapScene` (Canvas 2D). The camera is parallel by default; `makeMapProjection(scene, camera, viewport, { perspective: MAP_PERSPECTIVE_DISTANCE })` adds a light perspective (near squares larger, far ones smaller) in the same screen box, with `ground` still exact and `factor(x, y, z)` telling how much a point grew, e.g. to size a figure drawn over the model. `MapBoardView` draws again when the browser gives its canvas back blank (`contextrestored`). The 3D model's component, its gestures and its icons stay in the app.
98
+ `MapBoardView` draws a map already fetched (`map`: the hcmaps JSON, `HCMapJson`) with the hcmaps engine (`HCMap`) in a canvas named by `canvasLabel`, square-cornered and white in both themes (`color-map-board`), and shades the inspected square or draws the inspected edge (`selection`, from `inspectMap`; `color-map-selection*`). `onInspect(x, y, clientX, clientY)` gets the pointer in squares: mouse and pen while passing over, touch on press; `onLeave` when the mouse leaves. Other attributes (role, `tabIndex`, `onKeyDown`, `aria-*`) go to the container, so the caller adds the keyboard (HCUI: arrows move one square, Esc closes). `MapBoardPlaceholder` is the empty grid while it loads, a busy status whose `label` is visually hidden by the library. `MapTooltipView` is fixed next to `anchor` without leaving the screen (portal it out of a clipping container): the title, a wall's color, and for each square (one, or two on an edge) its coordinate, terrain and elevation swatches with the caller's names, the environment (a node, e.g. `MapEnvironmentView`, or `environmentNote`) and notes. `MapSwatch` is the board's square in a legend; `MapLegendView` lists them (the 3D model's legend) with children after; `MapEnvironmentView` shows indoor/outdoor with the caller's icons (black line icons, turned light in the dark theme by `filter-map-environment-icon`) and names. `renderMapThumbnail(json, height)` returns a PNG data URL. The engines are plain functions with no DOM beyond a canvas context: `inspectMap` (square, edge or wall at a point, in squares), `mapSquare`, `mapMaxLevel`, `mapElevationLevel`, `mapSquareCoordinate`; `buildMapScene` (the 3D model's heights, walls, ramps and ziplines), the camera (`makeMapProjection`, `pickMapScene`, `zoomMapCamera`, `clampMapPan`) and `drawMapScene` (Canvas 2D). The camera is parallel by default; `makeMapProjection(scene, camera, viewport, { perspective: MAP_PERSPECTIVE_DISTANCE })` adds a light perspective (near squares larger, far ones smaller) in the same screen box, with `ground` still exact and `factor(x, y, z)` telling how much a point grew, e.g. to size a figure drawn over the model. `MapBoardView` draws again when the browser gives its canvas back blank (`contextrestored`). `drawMapScene(..., { decorate })` lets the app paint a side over its default face (`side(ctx, face)`, with the face's screen `quad`) and draw what stands on a square right after it in the painter's order (`cell(ctx, cell)`), or draw a ramp its own way (`ramp(ctx, ramp)` returning `true`), e.g. facades, props and stairs, without changing terrain or picking. The 3D model's component, its gestures and its icons stay in the app.
package/dist/index.d.ts CHANGED
@@ -1591,6 +1591,31 @@ export declare function mapFootprintOutline({ x, y, width, height, cells }: {
1591
1591
  edges: [Point, Point][];
1592
1592
  corners: Point[];
1593
1593
  };
1594
+ /** A square's visible side, as `drawMapScene` draws it: from the neighbor's top (`low`) to the square's. */
1595
+ export interface MapSceneFace {
1596
+ cell: MapSceneCell;
1597
+ /** The side's outward direction (one of `MAP_CELL_SIDES`). */
1598
+ dx: number;
1599
+ dy: number;
1600
+ low: number;
1601
+ top: number;
1602
+ /** Screen corners: low at the side's start, low at its end, top at its end, top at its start. */
1603
+ quad: [[number, number], [number, number], [number, number], [number, number]];
1604
+ /** How lit the side is, 0..1 (the default face is shaded by it). */
1605
+ light: number;
1606
+ }
1607
+ /**
1608
+ * What the app adds to the 3D model, in the painter's order of `drawMapScene`: a side
1609
+ * drawn over the default face (a facade, a wall's stones), and whatever stands on a
1610
+ * square, drawn right after that square (a car, a tree, a pillar), so nearer squares
1611
+ * and walls still cover it. Terrain, heights and picking do not change.
1612
+ */
1613
+ export interface MapSceneDecoration {
1614
+ side?(ctx: CanvasRenderingContext2D, face: MapSceneFace): void;
1615
+ cell?(ctx: CanvasRenderingContext2D, cell: MapSceneCell): void;
1616
+ /** Draws a ramp (e.g. as stairs) in place of the default wedge; return `true` when it did. */
1617
+ ramp?(ctx: CanvasRenderingContext2D, ramp: MapSceneRamp): boolean;
1618
+ }
1594
1619
  export interface MapSceneFootprint {
1595
1620
  x: number;
1596
1621
  y: number;
@@ -1615,8 +1640,9 @@ export interface MapSceneHighlight {
1615
1640
  /** The target of a piece being moved: floor and volume, cut to the board. */
1616
1641
  footprint?: MapSceneFootprint;
1617
1642
  }
1618
- export declare function drawMapScene(ctx: CanvasRenderingContext2D, scene: MapScene, view: MapProjection, texture: HTMLCanvasElement, highlight?: MapSceneHighlight | null, { tufts }?: {
1643
+ export declare function drawMapScene(ctx: CanvasRenderingContext2D, scene: MapScene, view: MapProjection, texture: HTMLCanvasElement, highlight?: MapSceneHighlight | null, { tufts, decorate }?: {
1619
1644
  tufts?: boolean;
1645
+ decorate?: MapSceneDecoration;
1620
1646
  }): void;
1621
1647
  /** The thumbnail's default height, in pixels. */
1622
1648
  export declare const MAP_THUMBNAIL_HEIGHT = 192;
package/dist/index.js CHANGED
@@ -2962,7 +2962,7 @@ function sideColor(cell) {
2962
2962
  if (cell.terrain === "window") return MAP_TERRAIN_EDGE_COLORS.window;
2963
2963
  return SIDE_COLORS[cell.environment] ?? SIDE_COLORS.unknown;
2964
2964
  }
2965
- function drawCellSides(ctx, scene, view, cell) {
2965
+ function drawCellSides(ctx, scene, view, cell, decorate) {
2966
2966
  const { point } = view;
2967
2967
  for (const side of MAP_CELL_SIDES) {
2968
2968
  if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
@@ -2971,7 +2971,13 @@ function drawCellSides(ctx, scene, view, cell) {
2971
2971
  if (low >= cell.top) continue;
2972
2972
  const ax = cell.x + side.ax, ay = cell.y + side.ay, bx = cell.x + side.bx, by = cell.y + side.by;
2973
2973
  const light = faceLight(view, side.dx, side.dy);
2974
- face(ctx, [point(ax, ay, low), point(bx, by, low), point(bx, by, cell.top), point(ax, ay, cell.top)], shade(sideColor(cell), light));
2974
+ const quad = [point(ax, ay, low), point(bx, by, low), point(bx, by, cell.top), point(ax, ay, cell.top)];
2975
+ face(ctx, quad, shade(sideColor(cell), light));
2976
+ if (decorate?.side) {
2977
+ ctx.save();
2978
+ decorate.side(ctx, { cell, dx: side.dx, dy: side.dy, low, top: cell.top, quad, light });
2979
+ ctx.restore();
2980
+ }
2975
2981
  ctx.strokeStyle = "rgba(0,0,0,.26)";
2976
2982
  ctx.lineWidth = 1;
2977
2983
  for (let z = 0; z < cell.top - 0.01; z += MAP_SCENE_LEVEL_HEIGHT) {
@@ -3153,7 +3159,7 @@ function drawHighlight(ctx, scene, view, highlight) {
3153
3159
  ctx.lineWidth = 2;
3154
3160
  ctx.stroke();
3155
3161
  }
3156
- function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true } = {}) {
3162
+ function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true, decorate } = {}) {
3157
3163
  const pad = 0.35;
3158
3164
  polygon(ctx, [view.point(-pad, -pad, MAP_SCENE_BASE_Z - 0.16), view.point(scene.width + pad, -pad, MAP_SCENE_BASE_Z - 0.16), view.point(scene.width + pad, scene.height + pad, MAP_SCENE_BASE_Z - 0.16), view.point(-pad, scene.height + pad, MAP_SCENE_BASE_Z - 0.16)]);
3159
3165
  ctx.fillStyle = "rgba(0,0,0,.14)";
@@ -3176,11 +3182,24 @@ function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true } = {
3176
3182
  items.push({
3177
3183
  depth: view.depth(cell.x + 0.5, cell.y + 0.5),
3178
3184
  draw() {
3179
- drawCellSides(ctx, scene, view, cell);
3185
+ drawCellSides(ctx, scene, view, cell, decorate);
3180
3186
  drawCellTop(ctx, scene, view, cell, texture, shadows);
3181
3187
  if (mixed && cell.environment === "indoor") drawIndoorCurb(ctx, scene, view, cell);
3182
- for (const ramp of rampsByCell.get(cell.y * scene.width + cell.x) ?? []) drawRamp(ctx, view, ramp);
3188
+ for (const ramp of rampsByCell.get(cell.y * scene.width + cell.x) ?? []) {
3189
+ let drawn = false;
3190
+ if (decorate?.ramp) {
3191
+ ctx.save();
3192
+ drawn = decorate.ramp(ctx, ramp);
3193
+ ctx.restore();
3194
+ }
3195
+ if (!drawn) drawRamp(ctx, view, ramp);
3196
+ }
3183
3197
  if (tufts && cell.terrain === "hindering") drawHindering(ctx, view, cell);
3198
+ if (decorate?.cell) {
3199
+ ctx.save();
3200
+ decorate.cell(ctx, cell);
3201
+ ctx.restore();
3202
+ }
3184
3203
  }
3185
3204
  });
3186
3205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ntmats/hc-ui",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Shared HCUI/Arena React presentation primitives.",
6
6
  "license": "UNLICENSED",