@ntmats/hc-ui 0.9.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 +1 -1
- package/dist/index.d.ts +44 -2
- package/dist/index.js +82 -20
- package/package.json +1 -1
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
|
|
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
|
@@ -1512,8 +1512,24 @@ export interface MapProjection {
|
|
|
1512
1512
|
depth(x: number, y: number): number;
|
|
1513
1513
|
/** From the screen to the map, on a plane at height `z`. */
|
|
1514
1514
|
ground(px: number, py: number, z: number): [number, number];
|
|
1515
|
+
/**
|
|
1516
|
+
* How much a point at (x, y, z) is enlarged on screen compared with the parallel
|
|
1517
|
+
* projection: above 1 near the camera, below 1 far from it. Always 1 without
|
|
1518
|
+
* `perspective`. Optional so hand-made projections still type-check.
|
|
1519
|
+
*/
|
|
1520
|
+
factor?(x: number, y: number, z: number): number;
|
|
1521
|
+
}
|
|
1522
|
+
export interface MapProjectionOptions {
|
|
1523
|
+
/**
|
|
1524
|
+
* A light perspective, as the camera's distance in half-diagonals of the scene
|
|
1525
|
+
* (`MAP_PERSPECTIVE_DISTANCE` = 4; larger is gentler). Off by default: a parallel
|
|
1526
|
+
* projection, where near and far squares are the same size.
|
|
1527
|
+
*/
|
|
1528
|
+
perspective?: number;
|
|
1515
1529
|
}
|
|
1516
|
-
|
|
1530
|
+
/** The camera distance the Arena's table uses for `perspective`. */
|
|
1531
|
+
export declare const MAP_PERSPECTIVE_DISTANCE = 4;
|
|
1532
|
+
export declare function makeMapProjection(scene: MapScene, camera: MapCamera, viewport: MapViewport, { perspective }?: MapProjectionOptions): MapProjection;
|
|
1517
1533
|
/** The map's center may go as far as any of its corners reaches the middle of the screen. */
|
|
1518
1534
|
export declare function clampMapPan(scene: MapScene, camera: MapCamera, viewport: MapViewport): MapCamera;
|
|
1519
1535
|
/** Zooms keeping the point at `anchor` (measured from the screen's center) still. */
|
|
@@ -1575,6 +1591,31 @@ export declare function mapFootprintOutline({ x, y, width, height, cells }: {
|
|
|
1575
1591
|
edges: [Point, Point][];
|
|
1576
1592
|
corners: Point[];
|
|
1577
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
|
+
}
|
|
1578
1619
|
export interface MapSceneFootprint {
|
|
1579
1620
|
x: number;
|
|
1580
1621
|
y: number;
|
|
@@ -1599,8 +1640,9 @@ export interface MapSceneHighlight {
|
|
|
1599
1640
|
/** The target of a piece being moved: floor and volume, cut to the board. */
|
|
1600
1641
|
footprint?: MapSceneFootprint;
|
|
1601
1642
|
}
|
|
1602
|
-
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 }?: {
|
|
1603
1644
|
tufts?: boolean;
|
|
1645
|
+
decorate?: MapSceneDecoration;
|
|
1604
1646
|
}): void;
|
|
1605
1647
|
/** The thumbnail's default height, in pixels. */
|
|
1606
1648
|
export declare const MAP_THUMBNAIL_HEIGHT = 192;
|
package/dist/index.js
CHANGED
|
@@ -2758,7 +2758,8 @@ var MAP_DEFAULT_CAMERA = { azimuth: -32, tilt: 52, zoom: 1, panX: 0, panY: 0 };
|
|
|
2758
2758
|
var MAP_TILT_RANGE = [6, 90];
|
|
2759
2759
|
var MAP_ZOOM_RANGE = [1, 6];
|
|
2760
2760
|
var clampMapValue = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
2761
|
-
|
|
2761
|
+
var MAP_PERSPECTIVE_DISTANCE = 4;
|
|
2762
|
+
function makeMapProjection(scene, camera, viewport, { perspective } = {}) {
|
|
2762
2763
|
const rad = camera.azimuth * Math.PI / 180, tilt = camera.tilt * Math.PI / 180;
|
|
2763
2764
|
const sin = Math.sin(rad), cos = Math.cos(rad), sinT = Math.sin(tilt), cosT = Math.cos(tilt);
|
|
2764
2765
|
const cx2 = scene.width / 2, cy = scene.height / 2, midZ = (scene.maxTop + MAP_SCENE_BASE_Z) / 2;
|
|
@@ -2767,7 +2768,7 @@ function makeMapProjection(scene, camera, viewport) {
|
|
|
2767
2768
|
const fit = Math.min(viewport.width / (halfWidth * 2.06), viewport.height / (halfHeight * 2.06));
|
|
2768
2769
|
const scale = fit * camera.zoom;
|
|
2769
2770
|
const ox = viewport.width / 2 + camera.panX, oy = viewport.height / 2 + camera.panY;
|
|
2770
|
-
|
|
2771
|
+
const parallel = {
|
|
2771
2772
|
scale,
|
|
2772
2773
|
sin,
|
|
2773
2774
|
cos,
|
|
@@ -2781,6 +2782,44 @@ function makeMapProjection(scene, camera, viewport) {
|
|
|
2781
2782
|
ground(px, py, z) {
|
|
2782
2783
|
const rx = (px - ox) / scale, ry = ((py - oy) / scale + (z - midZ) * cosT) / sinT;
|
|
2783
2784
|
return [cx2 + rx * cos + ry * sin, cy - rx * sin + ry * cos];
|
|
2785
|
+
},
|
|
2786
|
+
factor: () => 1
|
|
2787
|
+
};
|
|
2788
|
+
if (!perspective || !(perspective > 0)) return parallel;
|
|
2789
|
+
const [sx, sy] = parallel.point(cx2, cy, midZ);
|
|
2790
|
+
const D = perspective * Math.max(1, halfWidth, (scene.maxTop - MAP_SCENE_BASE_Z) / 2);
|
|
2791
|
+
const toward = (x, y, z) => parallel.depth(x, y) * cosT + (z - midZ) * sinT;
|
|
2792
|
+
const raw = (x, y, z) => D / Math.max(D * 0.05, D - toward(x, y, z));
|
|
2793
|
+
const corners = [[0, 0], [scene.width, 0], [scene.width, scene.height], [0, scene.height]];
|
|
2794
|
+
const extremes = corners.flatMap(([x, y]) => [[x, y, scene.maxTop], [x, y, MAP_SCENE_BASE_Z]]);
|
|
2795
|
+
const box = (points) => {
|
|
2796
|
+
const xs = points.map((p) => p[0]), ys = points.map((p) => p[1]);
|
|
2797
|
+
return [Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys)];
|
|
2798
|
+
};
|
|
2799
|
+
const [flatW, flatH] = box(extremes.map(([x, y, z]) => parallel.point(x, y, z)));
|
|
2800
|
+
const [deepW, deepH] = box(extremes.map(([x, y, z]) => {
|
|
2801
|
+
const [px, py] = parallel.point(x, y, z), f = raw(x, y, z);
|
|
2802
|
+
return [sx + (px - sx) * f, sy + (py - sy) * f];
|
|
2803
|
+
}));
|
|
2804
|
+
const norm = Math.max(flatW > 1e-9 ? deepW / flatW : 0, flatH > 1e-9 ? deepH / flatH : 0) || 1;
|
|
2805
|
+
const factor = (x, y, z) => raw(x, y, z) / norm;
|
|
2806
|
+
return {
|
|
2807
|
+
scale,
|
|
2808
|
+
sin,
|
|
2809
|
+
cos,
|
|
2810
|
+
factor,
|
|
2811
|
+
depth: parallel.depth,
|
|
2812
|
+
point(x, y, z) {
|
|
2813
|
+
const [px, py] = parallel.point(x, y, z), f = factor(x, y, z);
|
|
2814
|
+
return [sx + (px - sx) * f, sy + (py - sy) * f];
|
|
2815
|
+
},
|
|
2816
|
+
ground(px, py, z) {
|
|
2817
|
+
const zc = z - midZ;
|
|
2818
|
+
const v = (py - sy) / scale * norm;
|
|
2819
|
+
const denom = D * sinT + v * cosT;
|
|
2820
|
+
const along = Math.abs(denom) < 1e-9 ? 0 : (v * D - v * zc * sinT + zc * cosT * D) / denom;
|
|
2821
|
+
const f = D / Math.max(D * 0.05, D - along * cosT - zc * sinT) / norm;
|
|
2822
|
+
return parallel.ground(sx + (px - sx) / f, sy + (py - sy) / f, z);
|
|
2784
2823
|
}
|
|
2785
2824
|
};
|
|
2786
2825
|
}
|
|
@@ -2834,11 +2873,9 @@ function pickMapScene(scene, view, px, py) {
|
|
|
2834
2873
|
const p00 = view.point(cell.x, cell.y, cell.top), p10 = view.point(cell.x + 1, cell.y, cell.top);
|
|
2835
2874
|
const p11 = view.point(cell.x + 1, cell.y + 1, cell.top), p01 = view.point(cell.x, cell.y + 1, cell.top);
|
|
2836
2875
|
if (inside(px, py, [p00, p10, p11, p01])) {
|
|
2837
|
-
const
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
const u = ((px - p00[0]) * by - (py - p00[1]) * bx) / det, v = ((py - p00[1]) * ax - (px - p00[0]) * ay) / det;
|
|
2841
|
-
return { x: cell.x + clampMapValue(u, 0, 1), y: cell.y + clampMapValue(v, 0, 1) };
|
|
2876
|
+
const [gx, gy] = view.ground(px, py, cell.top);
|
|
2877
|
+
if (!Number.isFinite(gx) || !Number.isFinite(gy)) return { x: cell.x + 0.5, y: cell.y + 0.5 };
|
|
2878
|
+
return { x: clampMapValue(gx, cell.x, cell.x + 1), y: clampMapValue(gy, cell.y, cell.y + 1) };
|
|
2842
2879
|
}
|
|
2843
2880
|
for (const side of MAP_CELL_SIDES) {
|
|
2844
2881
|
if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
|
|
@@ -2925,7 +2962,7 @@ function sideColor(cell) {
|
|
|
2925
2962
|
if (cell.terrain === "window") return MAP_TERRAIN_EDGE_COLORS.window;
|
|
2926
2963
|
return SIDE_COLORS[cell.environment] ?? SIDE_COLORS.unknown;
|
|
2927
2964
|
}
|
|
2928
|
-
function drawCellSides(ctx, scene, view, cell) {
|
|
2965
|
+
function drawCellSides(ctx, scene, view, cell, decorate) {
|
|
2929
2966
|
const { point } = view;
|
|
2930
2967
|
for (const side of MAP_CELL_SIDES) {
|
|
2931
2968
|
if (side.dx * view.sin + side.dy * view.cos <= 0) continue;
|
|
@@ -2934,7 +2971,13 @@ function drawCellSides(ctx, scene, view, cell) {
|
|
|
2934
2971
|
if (low >= cell.top) continue;
|
|
2935
2972
|
const ax = cell.x + side.ax, ay = cell.y + side.ay, bx = cell.x + side.bx, by = cell.y + side.by;
|
|
2936
2973
|
const light = faceLight(view, side.dx, side.dy);
|
|
2937
|
-
|
|
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
|
+
}
|
|
2938
2981
|
ctx.strokeStyle = "rgba(0,0,0,.26)";
|
|
2939
2982
|
ctx.lineWidth = 1;
|
|
2940
2983
|
for (let z = 0; z < cell.top - 0.01; z += MAP_SCENE_LEVEL_HEIGHT) {
|
|
@@ -3116,7 +3159,7 @@ function drawHighlight(ctx, scene, view, highlight) {
|
|
|
3116
3159
|
ctx.lineWidth = 2;
|
|
3117
3160
|
ctx.stroke();
|
|
3118
3161
|
}
|
|
3119
|
-
function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true } = {}) {
|
|
3162
|
+
function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true, decorate } = {}) {
|
|
3120
3163
|
const pad = 0.35;
|
|
3121
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)]);
|
|
3122
3165
|
ctx.fillStyle = "rgba(0,0,0,.14)";
|
|
@@ -3139,11 +3182,24 @@ function drawMapScene(ctx, scene, view, texture, highlight, { tufts = true } = {
|
|
|
3139
3182
|
items.push({
|
|
3140
3183
|
depth: view.depth(cell.x + 0.5, cell.y + 0.5),
|
|
3141
3184
|
draw() {
|
|
3142
|
-
drawCellSides(ctx, scene, view, cell);
|
|
3185
|
+
drawCellSides(ctx, scene, view, cell, decorate);
|
|
3143
3186
|
drawCellTop(ctx, scene, view, cell, texture, shadows);
|
|
3144
3187
|
if (mixed && cell.environment === "indoor") drawIndoorCurb(ctx, scene, view, cell);
|
|
3145
|
-
for (const ramp of rampsByCell.get(cell.y * scene.width + cell.x) ?? [])
|
|
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
|
+
}
|
|
3146
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
|
+
}
|
|
3147
3203
|
}
|
|
3148
3204
|
});
|
|
3149
3205
|
}
|
|
@@ -3189,14 +3245,19 @@ var MapBoardView = forwardRef(function MapBoardView2({ map, canvasLabel, selecti
|
|
|
3189
3245
|
useEffect(() => {
|
|
3190
3246
|
const canvas = canvasRef.current;
|
|
3191
3247
|
if (!canvas) return;
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3248
|
+
const draw = () => {
|
|
3249
|
+
const ctx = canvas.getContext("2d");
|
|
3250
|
+
if (!ctx) return;
|
|
3251
|
+
try {
|
|
3252
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3253
|
+
new HCMap(map).draw(ctx, { grid });
|
|
3254
|
+
} catch {
|
|
3255
|
+
failed.current?.();
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
draw();
|
|
3259
|
+
canvas.addEventListener("contextrestored", draw);
|
|
3260
|
+
return () => canvas.removeEventListener("contextrestored", draw);
|
|
3200
3261
|
}, [map, grid]);
|
|
3201
3262
|
function pointer(event) {
|
|
3202
3263
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
@@ -3307,6 +3368,7 @@ export {
|
|
|
3307
3368
|
MAP_GRID_SOFT_COLOR,
|
|
3308
3369
|
MAP_GRID_TERRAIN_SHADE,
|
|
3309
3370
|
MAP_INDOOR_EDGE_COLOR,
|
|
3371
|
+
MAP_PERSPECTIVE_DISTANCE,
|
|
3310
3372
|
MAP_SCENE_BASE_Z,
|
|
3311
3373
|
MAP_SCENE_LEVEL_HEIGHT,
|
|
3312
3374
|
MAP_TERRAIN_EDGE_COLORS,
|