@neta-art/cohub 12.0.0 → 12.1.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.
Files changed (28) hide show
  1. package/dist/board/export/index.js +1 -1
  2. package/dist/board/export/scene.js +2 -1
  3. package/dist/board/render/card-tag.d.ts +0 -1
  4. package/dist/board/render/card-tag.js +15 -17
  5. package/dist/board/render/index.d.ts +2 -2
  6. package/dist/board/render/index.js +6 -6
  7. package/dist/board/render/renderers/arrow-card-renderer.js +5 -7
  8. package/dist/board/render/renderers/audio-card-renderer.js +8 -7
  9. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -1
  10. package/dist/board/render/renderers/base-card-renderer.js +2 -3
  11. package/dist/board/render/renderers/board-renderer-registry.d.ts +3 -1
  12. package/dist/board/render/renderers/custom-card-renderer.js +19 -17
  13. package/dist/board/render/renderers/file-card-renderer.js +10 -8
  14. package/dist/board/render/renderers/frame-card-renderer.js +5 -6
  15. package/dist/board/render/renderers/shape-card-renderer.js +5 -7
  16. package/dist/board/render/renderers/task-card-renderer.js +9 -9
  17. package/dist/board/render/renderers/text-card-renderer.js +5 -7
  18. package/dist/board/render/renderers/unknown-card-renderer.js +5 -7
  19. package/dist/board/render/text-resolution.d.ts +4 -7
  20. package/dist/board/render/text-resolution.js +20 -22
  21. package/dist/board/stage/canvas-size.d.ts +18 -0
  22. package/dist/board/stage/canvas-size.js +31 -0
  23. package/dist/board/stage/scene.d.ts +5 -1
  24. package/dist/board/stage/scene.js +35 -9
  25. package/dist/board/stage/stage.js +34 -13
  26. package/dist/board/stage/text-resolution-tracker.d.ts +8 -0
  27. package/dist/board/stage/text-resolution-tracker.js +33 -0
  28. package/package.json +1 -1
@@ -2,9 +2,9 @@ import { buildBoardScene } from "../model/scene.js";
2
2
  import { resolveBoardAnimationCamera } from "../model/sampling.js";
3
3
  import { normalizeBoardDocument, planBoardExport } from "../model/export-plan.js";
4
4
  import { parseBoardCssColor } from "../render/css-color.js";
5
- import { ensureBoardTextMeasurement } from "../render/text-measurement.js";
6
5
  import { defaultBoardPalette } from "../render/palette.js";
7
6
  import { createBoardRenderSampler } from "../render/sampling.js";
7
+ import { ensureBoardTextMeasurement } from "../render/text-measurement.js";
8
8
  import { createBoardExportScene } from "./scene.js";
9
9
  import { Rectangle, RendererType } from "pixi.js";
10
10
  //#region src/board/export/index.ts
@@ -1,7 +1,7 @@
1
1
  import { buildFallbackShapeColors } from "../model/palette.js";
2
2
  import { imageAssetKey } from "../model/image-key.js";
3
- import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
4
3
  import { defaultBoardPalette } from "../render/palette.js";
4
+ import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
5
5
  import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
6
6
  import { resolveBoardRenderStyle } from "../render/style.js";
7
7
  import { Container, Graphics, Sprite, TilingSprite } from "pixi.js";
@@ -29,6 +29,7 @@ function buildContext(input) {
29
29
  themeKey: "export",
30
30
  rendererType: "canvas",
31
31
  zoom: input.scale,
32
+ textResolution: input.scale,
32
33
  reducedMotion: true,
33
34
  labels: DEFAULT_BOARD_RENDER_LABELS,
34
35
  taskRun: () => null,
@@ -11,7 +11,6 @@ type CardTag = {
11
11
  icon: Graphics;
12
12
  text: Text | null;
13
13
  sig: string;
14
- resolution: number;
15
14
  width: number;
16
15
  };
17
16
  declare function createCardTag(): CardTag;
@@ -1,5 +1,5 @@
1
1
  import { fitTextToWidth } from "./text-fit.js";
2
- import { syncTextResolution, textResolutionForZoom } from "./text-resolution.js";
2
+ import { syncTextResolution } from "./text-resolution.js";
3
3
  import { Container, Graphics, Text } from "pixi.js";
4
4
  //#region src/board/render/card-tag.ts
5
5
  const CARD_TAG_SIZE = 11;
@@ -177,13 +177,15 @@ function createCardTag() {
177
177
  icon,
178
178
  text: null,
179
179
  sig: "",
180
- resolution: 0,
181
180
  width: 0
182
181
  };
183
182
  }
183
+ function tagScale(zoom) {
184
+ return Math.min(MAX_GROWTH, Math.max(1, 1 / Math.max(zoom, .01)));
185
+ }
184
186
  function zoomCardTag(tag, zoom) {
185
187
  if (!tag.text || !tag.sig) return;
186
- const scale = Math.min(MAX_GROWTH, Math.max(1, 1 / Math.max(zoom, .01)));
188
+ const scale = tagScale(zoom);
187
189
  const span = 17 + tag.text.width;
188
190
  tag.root.scale.set(scale);
189
191
  tag.root.position.set(0, -18 * scale);
@@ -197,7 +199,6 @@ function syncCardTag(tag, input, context) {
197
199
  return;
198
200
  }
199
201
  if (!tag.text) {
200
- tag.resolution = textResolutionForZoom(context.zoom);
201
202
  tag.text = new Text({
202
203
  text: "",
203
204
  style: {
@@ -205,30 +206,27 @@ function syncCardTag(tag, input, context) {
205
206
  fontSize: 11,
206
207
  fontWeight: "500"
207
208
  },
208
- resolution: tag.resolution,
209
209
  roundPixels: true
210
210
  });
211
211
  tag.root.addChild(tag.text);
212
212
  }
213
- syncTextResolution(tag.text, tag, context.zoom);
214
213
  const sig = [
215
214
  input.glyph,
216
215
  input.text,
217
216
  input.width,
218
217
  context.themeKey
219
218
  ].join("|");
220
- if (sig === tag.sig) {
221
- zoomCardTag(tag, context.zoom);
222
- return;
219
+ if (sig !== tag.sig) {
220
+ tag.sig = sig;
221
+ const color = context.palette.muted;
222
+ const pixel = context.style.icons === "pixel";
223
+ drawCardGlyph(tag.icon.clear(), input.glyph, color, pixel);
224
+ tag.text.style.fill = color;
225
+ tag.text.style.fontFamily = context.style.font;
226
+ fitTextToWidth(tag.text, input.text, Math.max(1, input.width - ICON - GAP));
227
+ tag.text.position.set(17, (ICON - tag.text.height) / 2);
223
228
  }
224
- tag.sig = sig;
225
- const color = context.palette.muted;
226
- const pixel = context.style.icons === "pixel";
227
- drawCardGlyph(tag.icon.clear(), input.glyph, color, pixel);
228
- tag.text.style.fill = color;
229
- tag.text.style.fontFamily = context.style.font;
230
- fitTextToWidth(tag.text, input.text, Math.max(1, input.width - ICON - GAP));
231
- tag.text.position.set(17, (ICON - tag.text.height) / 2);
229
+ syncTextResolution(tag.text, context.textResolution * tagScale(context.zoom));
232
230
  zoomCardTag(tag, context.zoom);
233
231
  }
234
232
  //#endregion
@@ -15,6 +15,6 @@ import { CardCrop, CardElevation, CardRect, CardSurface, PIXEL_STEP, cardElevati
15
15
  import { BOARD_EFFECT_MAX_PARTICLES } from "./renderers/effect-card-renderer.js";
16
16
  import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWidth } from "./text-fit.js";
17
17
  import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
18
- import { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
18
+ import { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket } from "./text-resolution.js";
19
19
  import { VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
20
- export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, BoardBackgroundContext, BoardCardRenderer, BoardCardRendererResolver, BoardCssPaint, BoardMediaAction, BoardRenderContext, BoardRenderDiagnostic, BoardRenderLabels, BoardRenderPalette, BoardRenderStyle, BoardScriptHost, BoardScriptHostOptions, BoardSelectionStyle, BoardShadowStyle, BrowserBoardScriptHost, CARD_TAG_SIZE, CardCrop, CardElevation, CardGlyph, CardRect, CardSurface, CardTag, ClipGroup, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, boardCardDetailed, boardMediaActionAt, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fillCardPicture, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
20
+ export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, BoardBackgroundContext, BoardCardRenderer, BoardCardRendererResolver, BoardCssPaint, BoardMediaAction, BoardRenderContext, BoardRenderDiagnostic, BoardRenderLabels, BoardRenderPalette, BoardRenderStyle, BoardScriptHost, BoardScriptHostOptions, BoardSelectionStyle, BoardShadowStyle, BrowserBoardScriptHost, CARD_TAG_SIZE, CardCrop, CardElevation, CardGlyph, CardRect, CardSurface, CardTag, ClipGroup, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, boardCardDetailed, boardMediaActionAt, boardTextResolution, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fillCardPicture, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
@@ -1,13 +1,13 @@
1
1
  import { parseBoardCssColor, parseBoardCssPaint } from "./css-color.js";
2
+ import { defaultBoardPalette, itemAlpha, itemColor } from "./palette.js";
3
+ import { createBoardRenderSampler } from "./sampling.js";
4
+ import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
5
+ import { inspectBoardItems, inspectBoardScene, measureBoardText } from "./diagnostics.js";
2
6
  import { createBoardBackground, updateBoardBackground } from "./board-background.js";
3
7
  import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWidth } from "./text-fit.js";
4
- import { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
8
+ import { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket } from "./text-resolution.js";
5
9
  import { CARD_TAG_SIZE, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, traceCube, zoomCardTag } from "./card-tag.js";
6
10
  import { clippingAncestor, isClippingFrame, syncClipGroup, traceFrameOutline } from "./clip.js";
7
- import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
8
- import { inspectBoardItems, inspectBoardScene, measureBoardText } from "./diagnostics.js";
9
- import { defaultBoardPalette, itemAlpha, itemColor } from "./palette.js";
10
- import { createBoardRenderSampler } from "./sampling.js";
11
11
  import { dashPattern, endAngle, roundRectPoints, traceArrowhead, tracePolyline, trimPolyline } from "./stroke.js";
12
12
  import { PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, fillCardPicture, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.js";
13
13
  import { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter } from "./media-interaction.js";
@@ -17,4 +17,4 @@ import { createBoardScriptHost } from "./script-host.js";
17
17
  import { prepareBrowserBoardScripts } from "./script-export.js";
18
18
  import { defaultBoardRenderStyle, resolveBoardRenderStyle } from "./style.js";
19
19
  import { VIDEO_THUMBNAIL_MAX_EDGE, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
20
- export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, CARD_TAG_SIZE, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, boardCardDetailed, boardMediaActionAt, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fillCardPicture, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
20
+ export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, CARD_TAG_SIZE, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, boardCardDetailed, boardMediaActionAt, boardTextResolution, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fillCardPicture, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
@@ -1,7 +1,7 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { pathPointAt, resolveSceneArrow } from "../../model/arrow-geometry.js";
3
- import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
4
3
  import { itemAlpha, itemColor } from "../palette.js";
4
+ import { syncTextResolution } from "../text-resolution.js";
5
5
  import { dashPattern, endAngle, traceArrowhead, tracePolyline, trimPolyline } from "../stroke.js";
6
6
  import { drawCardSurface } from "../surface.js";
7
7
  import { drawFarStroke } from "./far-plate.js";
@@ -18,7 +18,6 @@ function sync(container, item, context) {
18
18
  parts.line.alpha = itemAlpha(context, style.stroke);
19
19
  const color = itemColor(context, style.stroke, "brand");
20
20
  const size = style.strokeWidth ?? 2.5;
21
- syncTextResolution(parts.label, parts, context.zoom);
22
21
  const resolved = resolveSceneArrow(item, context.scene);
23
22
  const path = trimPolyline(resolved.path, style.trim);
24
23
  const lineSig = [
@@ -85,6 +84,7 @@ function sync(container, item, context) {
85
84
  border: context.palette.border
86
85
  });
87
86
  }
87
+ syncTextResolution(parts.label, context.textResolution);
88
88
  const visible = props.label.length > 0 && (style.trim === void 0 || style.trim >= .5);
89
89
  parts.label.visible = visible;
90
90
  parts.backdrop.visible = visible;
@@ -98,7 +98,6 @@ const arrowCardRenderer = {
98
98
  create: (item, context) => {
99
99
  const root = new Container();
100
100
  const line = new Graphics();
101
- const resolution = textResolutionForZoom(context.zoom);
102
101
  const label = new Text({
103
102
  text: "",
104
103
  style: {
@@ -107,7 +106,7 @@ const arrowCardRenderer = {
107
106
  fontSize: 14,
108
107
  fontWeight: "500"
109
108
  },
110
- resolution,
109
+ resolution: context.textResolution,
111
110
  roundPixels: true
112
111
  });
113
112
  label.anchor.set(.5);
@@ -119,8 +118,7 @@ const arrowCardRenderer = {
119
118
  backdrop,
120
119
  label,
121
120
  lineSig: "",
122
- labelSig: "",
123
- resolution
121
+ labelSig: ""
124
122
  });
125
123
  if (item.type === "arrow") sync(root, item, context);
126
124
  return root;
@@ -130,7 +128,7 @@ const arrowCardRenderer = {
130
128
  },
131
129
  resolution: (container, _item, context) => {
132
130
  const parts = partsByContainer.get(container);
133
- if (parts) syncTextResolution(parts.label, parts, context.zoom);
131
+ if (parts) syncTextResolution(parts.label, context.textResolution);
134
132
  },
135
133
  renderFar: (graphics, item, context) => {
136
134
  if (item.type !== "arrow") return;
@@ -1,8 +1,8 @@
1
1
  import { getMediaResourceTitle } from "../../model/media.js";
2
- import { drawAudioWaveform } from "../audio-waveform.js";
3
2
  import { fitTextToWidth } from "../text-fit.js";
4
- import { boardCardDetailed, syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
3
+ import { boardCardDetailed, syncTextResolution } from "../text-resolution.js";
5
4
  import { formatBoardDuration } from "../card-tag.js";
5
+ import { drawAudioWaveform } from "../audio-waveform.js";
6
6
  import { cardElevation, drawCardSurface } from "../surface.js";
7
7
  import { positionShell } from "./base-card-renderer.js";
8
8
  import { drawFarPlate } from "./far-plate.js";
@@ -25,12 +25,15 @@ function audioKeyCenter(width, height) {
25
25
  function sync(container, item, context) {
26
26
  const parts = partsByContainer.get(container);
27
27
  if (!parts) return;
28
+ syncContent(parts, item, context);
29
+ syncTextResolution(parts.title, context.textResolution);
30
+ }
31
+ function syncContent(parts, item, context) {
28
32
  positionShell(parts.root, item);
29
33
  const { width, height } = item.frame;
30
34
  const title = item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
31
35
  const durationMs = item.props.snapshot?.durationMs;
32
36
  const detailed = boardCardDetailed(context.zoom);
33
- syncTextResolution(parts.title, parts, context.zoom);
34
37
  const elevation = cardElevation(context, item.id);
35
38
  const sig = [
36
39
  width,
@@ -83,14 +86,13 @@ const audioCardRenderer = {
83
86
  const root = new Container();
84
87
  const plate = new Graphics();
85
88
  const wave = new Graphics();
86
- const resolution = textResolutionForZoom(context.zoom);
87
89
  const title = new Text({
88
90
  text: "",
89
91
  style: {
90
92
  fontSize: 13,
91
93
  fontWeight: "600"
92
94
  },
93
- resolution,
95
+ resolution: context.textResolution,
94
96
  roundPixels: true
95
97
  });
96
98
  root.addChild(plate, wave, title);
@@ -99,8 +101,7 @@ const audioCardRenderer = {
99
101
  plate,
100
102
  wave,
101
103
  title,
102
- sig: "",
103
- resolution
104
+ sig: ""
104
105
  });
105
106
  if (item.type === "audio") sync(root, item, context);
106
107
  return root;
@@ -2,7 +2,7 @@ import { BoardFrame } from "../../../protocol/dist/board-layout.js";
2
2
  import "../../../protocol/dist/index.js";
3
3
  import { Container, Text } from "pixi.js";
4
4
  //#region src/board/render/renderers/base-card-renderer.d.ts
5
- declare function createLabel(text: string, style: Record<string, unknown>): Text;
5
+ declare function createLabel(text: string, style: Record<string, unknown>, resolution: number): Text;
6
6
  declare function positionShell(root: Container, item: {
7
7
  frame: BoardFrame;
8
8
  }): void;
@@ -1,10 +1,9 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { getBoardResolution } from "../text-resolution.js";
3
2
  import { Text } from "pixi.js";
4
3
  //#region src/board/render/renderers/base-card-renderer.ts
5
- function createLabel(text, style) {
4
+ function createLabel(text, style, resolution) {
6
5
  return new Text({
7
- resolution: getBoardResolution(),
6
+ resolution,
8
7
  roundPixels: true,
9
8
  text,
10
9
  style: {
@@ -48,6 +48,8 @@ type BoardRenderContext = {
48
48
  themeKey: string;
49
49
  rendererType: "gpu" | "canvas";
50
50
  zoom: number;
51
+ /** Texture pixels per world unit for text; see `boardTextResolution`. */
52
+ textResolution: number;
51
53
  time: number;
52
54
  reducedMotion: boolean;
53
55
  labels: BoardRenderLabels;
@@ -73,7 +75,7 @@ type BoardCardRenderer = {
73
75
  tick?: (container: Container, clock: number) => number | null;
74
76
  create: (item: BoardSceneItem, context: BoardRenderContext) => Container;
75
77
  update: (container: Container, item: BoardSceneItem, context: BoardRenderContext) => void;
76
- /** Called only when the zoom bucket or display density changes. */
78
+ /** Called when the zoom bucket or text resolution changes. */
77
79
  resolution?: (container: Container, item: BoardSceneItem, context: BoardRenderContext) => void;
78
80
  /** Runs on every zoom step; only update transforms and visibility. */
79
81
  zoom?: (container: Container, zoom: number) => void;
@@ -1,5 +1,5 @@
1
1
  import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { getBoardResolution, syncTextResolution } from "../text-resolution.js";
2
+ import { syncTextResolution } from "../text-resolution.js";
3
3
  import { positionShell } from "./base-card-renderer.js";
4
4
  import { drawFarPlate } from "./far-plate.js";
5
5
  import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
@@ -13,7 +13,6 @@ function sync(container, item, context) {
13
13
  parts.id = item.id;
14
14
  }
15
15
  positionShell(parts.root, item);
16
- syncTextResolution(parts.label, parts.label, context.zoom);
17
16
  const { width, height } = item.frame;
18
17
  const texture = context.scripts?.frame(item, context.time) ?? null;
19
18
  const error = context.scripts?.error(item.id) ?? null;
@@ -31,19 +30,21 @@ function sync(container, item, context) {
31
30
  message,
32
31
  context.themeKey
33
32
  ].join("|");
34
- if (sig === parts.sig) return;
35
- parts.sig = sig;
36
- parts.placeholder.clear();
37
- if (!ready || error) parts.placeholder.rect(0, 0, width, height).stroke({
38
- color: context.palette.border,
39
- width: 1,
40
- alpha: .8
41
- });
42
- parts.label.visible = !ready || Boolean(error);
43
- parts.label.text = message;
44
- parts.label.style.fill = error ? context.colors.rose.stroke : context.palette.muted;
45
- parts.label.style.wordWrapWidth = Math.max(40, width - 16);
46
- parts.label.position.set(width / 2, height / 2);
33
+ if (sig !== parts.sig) {
34
+ parts.sig = sig;
35
+ parts.placeholder.clear();
36
+ if (!ready || error) parts.placeholder.rect(0, 0, width, height).stroke({
37
+ color: context.palette.border,
38
+ width: 1,
39
+ alpha: .8
40
+ });
41
+ parts.label.visible = !ready || Boolean(error);
42
+ parts.label.text = message;
43
+ parts.label.style.fill = error ? context.colors.rose.stroke : context.palette.muted;
44
+ parts.label.style.wordWrapWidth = Math.max(40, width - 16);
45
+ parts.label.position.set(width / 2, height / 2);
46
+ }
47
+ syncTextResolution(parts.label, context.textResolution);
47
48
  }
48
49
  const customCardRenderer = {
49
50
  id: "custom-card",
@@ -62,7 +63,8 @@ const customCardRenderer = {
62
63
  wordWrap: true,
63
64
  breakWords: true
64
65
  },
65
- resolution: getBoardResolution()
66
+ resolution: context.textResolution,
67
+ roundPixels: true
66
68
  });
67
69
  label.anchor.set(.5);
68
70
  root.addChild(sprite, placeholder, label);
@@ -82,7 +84,7 @@ const customCardRenderer = {
82
84
  },
83
85
  resolution: (container, _item, context) => {
84
86
  const parts = partsByContainer.get(container);
85
- if (parts) syncTextResolution(parts.label, parts.label, context.zoom);
87
+ if (parts) syncTextResolution(parts.label, context.textResolution);
86
88
  },
87
89
  renderFar: (graphics, item, context) => {
88
90
  drawFarPlate(graphics, item.frame, {
@@ -47,6 +47,14 @@ function syncClip(parts, rect, style) {
47
47
  function sync(container, item, context) {
48
48
  const parts = partsByContainer.get(container);
49
49
  if (!parts) return;
50
+ syncContent(parts, item, context);
51
+ for (const text of [
52
+ parts.title,
53
+ parts.excerpt,
54
+ parts.typeMark
55
+ ]) syncTextResolution(text, context.textResolution);
56
+ }
57
+ function syncContent(parts, item, context) {
50
58
  positionShell(parts.root, item);
51
59
  const { width, height } = item.frame;
52
60
  const detail = detailFor(context.zoom);
@@ -64,9 +72,6 @@ function sync(container, item, context) {
64
72
  const kind = filePreviewKind(item.props.snapshot);
65
73
  const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
66
74
  const accent = fileCategoryAccent(category, context.palette);
67
- syncTextResolution(parts.title, parts.titleRes, context.zoom);
68
- syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
69
- syncTextResolution(parts.typeMark, parts.typeMarkRes, context.zoom);
70
75
  const visualSig = [
71
76
  key ?? "",
72
77
  width,
@@ -190,7 +195,7 @@ const fileCardRenderer = {
190
195
  const cover = new Sprite(Texture.EMPTY);
191
196
  const coverMask = new Graphics();
192
197
  cover.mask = coverMask;
193
- const resolution = 1;
198
+ const resolution = context.textResolution;
194
199
  const title = new Text({
195
200
  text: "",
196
201
  style: {
@@ -247,10 +252,7 @@ const fileCardRenderer = {
247
252
  title,
248
253
  excerpt,
249
254
  visualSig: "",
250
- textSig: "",
251
- titleRes: { resolution },
252
- excerptRes: { resolution },
253
- typeMarkRes: { resolution }
255
+ textSig: ""
254
256
  });
255
257
  if (item.type === "file") sync(root, item, context);
256
258
  return root;
@@ -1,6 +1,6 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { syncTextResolution } from "../text-resolution.js";
3
2
  import { itemAlpha, itemColor } from "../palette.js";
3
+ import { syncTextResolution } from "../text-resolution.js";
4
4
  import { dashPattern, roundRectPoints, tracePolyline, trimPolyline } from "../stroke.js";
5
5
  import { createLabel, positionShell } from "./base-card-renderer.js";
6
6
  import { drawFarPlate } from "./far-plate.js";
@@ -13,7 +13,6 @@ function sync(container, item, context) {
13
13
  if (!parts) return;
14
14
  const { frame, style, props, locked } = item;
15
15
  positionShell(parts.root, item);
16
- syncTextResolution(parts.label, parts, context.zoom);
17
16
  const fill = itemColor(context, style.fill ?? style.stroke, "neutral", "fill");
18
17
  const fillAlpha = (style.fillOpacity ?? (style.fill ? 1 : DEFAULT_WASH)) * itemAlpha(context, style.fill ?? style.stroke);
19
18
  const baseStroke = itemColor(context, style.stroke, "neutral");
@@ -69,6 +68,7 @@ function sync(container, item, context) {
69
68
  parts.label.style.fill = labelColor;
70
69
  parts.label.style.fontFamily = context.style.font;
71
70
  }
71
+ syncTextResolution(parts.label, context.textResolution);
72
72
  parts.label.position.set(4, -18);
73
73
  }
74
74
  const frameCardRenderer = {
@@ -82,15 +82,14 @@ const frameCardRenderer = {
82
82
  fontFamily: BOARD_FONT_STACK,
83
83
  fontSize: 12,
84
84
  fontWeight: "600"
85
- });
85
+ }, context.textResolution);
86
86
  root.addChild(box, label);
87
87
  partsByContainer.set(root, {
88
88
  root,
89
89
  box,
90
90
  label,
91
91
  visualSig: "",
92
- textSig: "",
93
- resolution: label.resolution
92
+ textSig: ""
94
93
  });
95
94
  if (item.type === "frame") sync(root, item, context);
96
95
  return root;
@@ -100,7 +99,7 @@ const frameCardRenderer = {
100
99
  },
101
100
  resolution: (container, _item, context) => {
102
101
  const parts = partsByContainer.get(container);
103
- if (parts) syncTextResolution(parts.label, parts, context.zoom);
102
+ if (parts) syncTextResolution(parts.label, context.textResolution);
104
103
  },
105
104
  renderFar: (graphics, item, context) => {
106
105
  if (item.type !== "frame" || item.props.decoration === "none") return;
@@ -1,7 +1,7 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { worldPoint } from "../../model/geometry.js";
3
- import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
4
3
  import { itemAlpha, itemColor } from "../palette.js";
4
+ import { syncTextResolution, syncTextWrapWidth } from "../text-resolution.js";
5
5
  import { dashPattern, roundRectPoints, tracePolyline, trimPolyline } from "../stroke.js";
6
6
  import { positionShell } from "./base-card-renderer.js";
7
7
  import { drawFarPlate } from "./far-plate.js";
@@ -100,7 +100,6 @@ function sync(container, item, context) {
100
100
  const fillOpacity = (style.fillOpacity ?? (style.fill ? 1 : 0)) * itemAlpha(context, style.fill ?? style.stroke);
101
101
  const strokeAlpha = itemAlpha(context, style.stroke ?? style.fill);
102
102
  const strokeWidth = style.strokeWidth ?? 1.75;
103
- syncTextResolution(parts.label, parts, context.zoom);
104
103
  const visualSig = [
105
104
  width,
106
105
  height,
@@ -159,6 +158,7 @@ function sync(container, item, context) {
159
158
  parts.label.style.align = props.align;
160
159
  }
161
160
  syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), context.resizingIds.has(item.id));
161
+ syncTextResolution(parts.label, context.textResolution);
162
162
  parts.label.position.set(width / 2, height / 2);
163
163
  }
164
164
  const shapeCardRenderer = {
@@ -167,7 +167,6 @@ const shapeCardRenderer = {
167
167
  create: (item, context) => {
168
168
  const root = new Container();
169
169
  const shape = new Graphics();
170
- const resolution = textResolutionForZoom(context.zoom);
171
170
  const label = new Text({
172
171
  text: "",
173
172
  style: {
@@ -179,7 +178,7 @@ const shapeCardRenderer = {
179
178
  wordWrap: true,
180
179
  lineHeight: 28
181
180
  },
182
- resolution,
181
+ resolution: context.textResolution,
183
182
  roundPixels: true
184
183
  });
185
184
  label.anchor.set(.5);
@@ -190,8 +189,7 @@ const shapeCardRenderer = {
190
189
  label,
191
190
  visualSig: "",
192
191
  textSig: "",
193
- wrapWidth: 0,
194
- resolution
192
+ wrapWidth: 0
195
193
  });
196
194
  if (item.type === "shape") sync(root, item, context);
197
195
  return root;
@@ -201,7 +199,7 @@ const shapeCardRenderer = {
201
199
  },
202
200
  resolution: (container, _item, context) => {
203
201
  const parts = partsByContainer.get(container);
204
- if (parts) syncTextResolution(parts.label, parts, context.zoom);
202
+ if (parts) syncTextResolution(parts.label, context.textResolution);
205
203
  },
206
204
  renderFar: (graphics, item, context) => {
207
205
  if (item.type !== "shape") return;
@@ -1,9 +1,9 @@
1
1
  import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { featuredTaskArtifact } from "../../../protocol/dist/board-task.js";
3
- import { drawAudioWaveform } from "../audio-waveform.js";
4
3
  import { fitTextToLines } from "../text-fit.js";
5
- import { boardCardDetailed, syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
4
+ import { boardCardDetailed, syncTextResolution } from "../text-resolution.js";
6
5
  import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
6
+ import { drawAudioWaveform } from "../audio-waveform.js";
7
7
  import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
8
8
  import { positionShell } from "./base-card-renderer.js";
9
9
  import { drawFarPlate } from "./far-plate.js";
@@ -283,10 +283,6 @@ function sync(container, item, context) {
283
283
  const detailText = !full || !surface ? "" : status === "failed" ? snapshot.errorExcerpt ?? "" : working ? snapshot.promptExcerpt ?? "" : "";
284
284
  const textArtifact = kind === "text" && artifact?.type === "text";
285
285
  const bodyText = textArtifact ? artifact.textExcerpt : counting ? `${label} · ${elapsed}` : label;
286
- syncTextResolution(parts.body, parts, context.zoom);
287
- syncTextResolution(parts.detail, parts, context.zoom);
288
- syncTextResolution(parts.meta, parts, context.zoom);
289
- syncTextResolution(parts.badge, parts, context.zoom);
290
286
  const textSig = [
291
287
  counting ? label : bodyText,
292
288
  detailText,
@@ -317,6 +313,12 @@ function sync(container, item, context) {
317
313
  parts.badge.style.fill = context.palette.text;
318
314
  if (parts.badge.text !== count) parts.badge.text = count;
319
315
  }
316
+ for (const text of [
317
+ parts.body,
318
+ parts.detail,
319
+ parts.meta,
320
+ parts.badge
321
+ ]) syncTextResolution(text, context.textResolution);
320
322
  const badgeWidth = Math.max(22, parts.badge.width + 12);
321
323
  const visualSig = [
322
324
  width,
@@ -434,7 +436,6 @@ const taskCardRenderer = {
434
436
  const previewMask = new Graphics();
435
437
  const shimmer = new Graphics();
436
438
  const previewArt = new Graphics();
437
- const resolution = textResolutionForZoom(context.zoom);
438
439
  const text = (fontFamily, fontSize, fontWeight) => new Text({
439
440
  text: "",
440
441
  style: {
@@ -444,7 +445,7 @@ const taskCardRenderer = {
444
445
  wordWrap: true,
445
446
  breakWords: true
446
447
  },
447
- resolution,
448
+ resolution: context.textResolution,
448
449
  roundPixels: true
449
450
  });
450
451
  const body = text(BOARD_FONT_STACK, 11.5, "500");
@@ -478,7 +479,6 @@ const taskCardRenderer = {
478
479
  assetKey: null,
479
480
  visualSig: "",
480
481
  textSig: "",
481
- resolution,
482
482
  motion: {
483
483
  itemId: "",
484
484
  width: 0,
@@ -1,8 +1,8 @@
1
1
  import { layoutBoardText } from "../../../protocol/dist/board-layout.js";
2
2
  import { boardFontFamily } from "../../model/text-metrics.js";
3
- import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
4
- import { BOARD_FONT_STACKS } from "../text-measurement.js";
5
3
  import { itemAlpha, itemColor } from "../palette.js";
4
+ import { BOARD_FONT_STACKS } from "../text-measurement.js";
5
+ import { syncTextResolution } from "../text-resolution.js";
6
6
  import { positionShell } from "./base-card-renderer.js";
7
7
  import { drawFarPlate } from "./far-plate.js";
8
8
  import { Container, Graphics, Text } from "pixi.js";
@@ -32,7 +32,6 @@ function sync(container, item, context) {
32
32
  const anchorX = props.align === "center" ? .5 : props.align === "right" ? 1 : 0;
33
33
  parts.body.anchor.set(anchorX, 0);
34
34
  parts.body.position.set(item.frame.width * anchorX, 0);
35
- syncTextResolution(parts.body, parts, context.zoom * Math.max(1, previewScale));
36
35
  parts.body.alpha = itemAlpha(context, item.style.fill);
37
36
  parts.caret.alpha = parts.body.alpha;
38
37
  const ink = itemColor(context, item.style.fill, "neutral");
@@ -62,6 +61,7 @@ function sync(container, item, context) {
62
61
  breakWords: true
63
62
  });
64
63
  }
64
+ syncTextResolution(parts.body, context.textResolution * previewScale);
65
65
  syncCaret(parts, item, context);
66
66
  }
67
67
  const textCardRenderer = {
@@ -69,11 +69,10 @@ const textCardRenderer = {
69
69
  canRender: (item) => item.type === "text",
70
70
  create: (item, context) => {
71
71
  const root = new Container();
72
- const resolution = textResolutionForZoom(context.zoom);
73
72
  const body = new Text({
74
73
  text: "",
75
74
  style: { fontFamily: BOARD_FONT_STACKS.sans },
76
- resolution,
75
+ resolution: context.textResolution,
77
76
  roundPixels: true
78
77
  });
79
78
  const caret = new Graphics();
@@ -82,7 +81,6 @@ const textCardRenderer = {
82
81
  root,
83
82
  body,
84
83
  caret,
85
- resolution,
86
84
  contentSig: ""
87
85
  });
88
86
  if (item.type === "text") sync(root, item, context);
@@ -93,7 +91,7 @@ const textCardRenderer = {
93
91
  },
94
92
  resolution: (container, _item, context) => {
95
93
  const parts = partsByContainer.get(container);
96
- if (parts) syncTextResolution(parts.body, parts, context.zoom * Math.max(1, parts.body.scale.x));
94
+ if (parts) syncTextResolution(parts.body, context.textResolution * parts.body.scale.x);
97
95
  },
98
96
  renderFar: (graphics, item, context) => {
99
97
  if (item.type !== "text") return;
@@ -1,5 +1,5 @@
1
1
  import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
2
+ import { syncTextResolution } from "../text-resolution.js";
3
3
  import { cardElevation, drawCardSurface, traceCardOutline } from "../surface.js";
4
4
  import { positionShell } from "./base-card-renderer.js";
5
5
  import { drawFarPlate } from "./far-plate.js";
@@ -12,7 +12,6 @@ function sync(container, item, context) {
12
12
  positionShell(parts.root, item);
13
13
  const { width, height } = item.frame;
14
14
  const realType = item.type;
15
- syncTextResolution(parts.label, parts, context.zoom);
16
15
  const elevation = cardElevation(context, item.id);
17
16
  const visualSig = [
18
17
  width,
@@ -47,6 +46,7 @@ function sync(container, item, context) {
47
46
  parts.label.text = realType;
48
47
  parts.label.style.fill = context.palette.muted;
49
48
  }
49
+ syncTextResolution(parts.label, context.textResolution);
50
50
  parts.label.position.set(width / 2, height / 2);
51
51
  }
52
52
  const unknownCardRenderer = {
@@ -56,7 +56,6 @@ const unknownCardRenderer = {
56
56
  const root = new Container();
57
57
  const box = new Graphics();
58
58
  box.context.batchMode = "batch";
59
- const resolution = textResolutionForZoom(context.zoom);
60
59
  const label = new Text({
61
60
  text: "",
62
61
  style: {
@@ -64,7 +63,7 @@ const unknownCardRenderer = {
64
63
  fontSize: 11,
65
64
  fontWeight: "600"
66
65
  },
67
- resolution,
66
+ resolution: context.textResolution,
68
67
  roundPixels: true
69
68
  });
70
69
  label.anchor.set(.5);
@@ -74,8 +73,7 @@ const unknownCardRenderer = {
74
73
  box,
75
74
  label,
76
75
  visualSig: "",
77
- textSig: "",
78
- resolution
76
+ textSig: ""
79
77
  });
80
78
  sync(root, item, context);
81
79
  return root;
@@ -85,7 +83,7 @@ const unknownCardRenderer = {
85
83
  },
86
84
  resolution: (container, _item, context) => {
87
85
  const parts = partsByContainer.get(container);
88
- if (parts) syncTextResolution(parts.label, parts, context.zoom);
86
+ if (parts) syncTextResolution(parts.label, context.textResolution);
89
87
  },
90
88
  renderFar: (graphics, item, context) => {
91
89
  drawFarPlate(graphics, item.frame, {
@@ -6,15 +6,12 @@ declare function getBoardResolution(size?: {
6
6
  }, devicePixelRatio?: number): number;
7
7
  declare function textZoomBucket(zoom: number): number;
8
8
  declare function boardCardDetailed(zoom: number): boolean;
9
- /** Effective text resolution for a given camera zoom. */
10
- declare function textResolutionForZoom(zoom: number): number;
11
- /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
12
- declare function syncTextResolution(text: Text, state: {
13
- resolution: number;
14
- }, zoom: number): void;
9
+ /** Exact screen density at rest; reuse or step the raster while moving. */
10
+ declare function boardTextResolution(target: number, current: number, settled: boolean): number;
11
+ declare function syncTextResolution(text: Text, resolution: number): void;
15
12
  /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
16
13
  declare function syncTextWrapWidth(text: Text, state: {
17
14
  wrapWidth: number;
18
15
  }, width: number, defer: boolean): void;
19
16
  //#endregion
20
- export { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
17
+ export { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket };
@@ -1,19 +1,16 @@
1
1
  //#region src/board/render/text-resolution.ts
2
2
  const MAX_BOARD_RESOLUTION = 3;
3
- const MAX_BOARD_PIXELS = 8e6;
4
- const MAX_TEXT_RESOLUTION = 6;
3
+ const MAX_BOARD_PIXELS = 1e7;
4
+ const MIN_TEXT_RESOLUTION = .125;
5
+ const MAX_TEXT_EDGE = 4096;
6
+ const TEXT_UPSCALE_TOLERANCE = 1.25;
5
7
  function getBoardResolution(size, devicePixelRatio = globalThis.devicePixelRatio) {
6
8
  const density = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
7
9
  const pixels = size ? size.width * size.height : 0;
8
- const budget = pixels > 0 ? Math.sqrt(MAX_BOARD_PIXELS / pixels) : MAX_BOARD_RESOLUTION;
10
+ const budget = pixels > 0 ? Math.max(1, Math.sqrt(MAX_BOARD_PIXELS / pixels)) : MAX_BOARD_RESOLUTION;
9
11
  return Math.min(density, MAX_BOARD_RESOLUTION, budget);
10
12
  }
11
- /**
12
- * Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
13
- * looks soft unless we raise its resolution. We quantise zoom into a few
14
- * buckets so small zoom changes don't thrash texture regeneration.
15
- */
16
- const TEXT_ZOOM_BUCKETS = [
13
+ const ZOOM_BUCKETS = [
17
14
  .5,
18
15
  1,
19
16
  1.5,
@@ -23,23 +20,24 @@ const TEXT_ZOOM_BUCKETS = [
23
20
  ];
24
21
  function textZoomBucket(zoom) {
25
22
  const clamped = Math.max(.1, Math.min(4, zoom));
26
- for (const bucket of TEXT_ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
27
- return TEXT_ZOOM_BUCKETS[TEXT_ZOOM_BUCKETS.length - 1] ?? 4;
23
+ for (const bucket of ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
24
+ return ZOOM_BUCKETS[ZOOM_BUCKETS.length - 1] ?? 4;
28
25
  }
29
26
  function boardCardDetailed(zoom) {
30
27
  return textZoomBucket(zoom) >= 1;
31
28
  }
32
- /** Effective text resolution for a given camera zoom. */
33
- function textResolutionForZoom(zoom) {
34
- const bucket = textZoomBucket(zoom);
35
- return Math.min(getBoardResolution() * Math.max(1, bucket), MAX_TEXT_RESOLUTION);
29
+ /** Exact screen density at rest; reuse or step the raster while moving. */
30
+ function boardTextResolution(target, current, settled) {
31
+ if (!Number.isFinite(target) || target <= 0) return current > 0 ? current : 1;
32
+ if (settled || !(current > 0)) return target;
33
+ if (target <= current * TEXT_UPSCALE_TOLERANCE && target >= current / 2) return current;
34
+ return 2 ** Math.ceil(Math.log2(target));
36
35
  }
37
- /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
38
- function syncTextResolution(text, state, zoom) {
39
- const resolution = textResolutionForZoom(zoom);
40
- if (resolution === state.resolution && resolution === text.resolution) return;
41
- text.resolution = resolution;
42
- state.resolution = resolution;
36
+ function syncTextResolution(text, resolution) {
37
+ const { width, height } = text.bounds;
38
+ const limit = MAX_TEXT_EDGE / Math.max(1, Math.ceil(width + text.style.padding * 2), Math.ceil(height + text.style.padding * 2));
39
+ const next = Math.min(limit, Math.max(MIN_TEXT_RESOLUTION, resolution));
40
+ if (text.resolution !== next) text.resolution = next;
43
41
  }
44
42
  /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
45
43
  function syncTextWrapWidth(text, state, width, defer) {
@@ -48,4 +46,4 @@ function syncTextWrapWidth(text, state, width, defer) {
48
46
  state.wrapWidth = width;
49
47
  }
50
48
  //#endregion
51
- export { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
49
+ export { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket };
@@ -0,0 +1,18 @@
1
+ //#region src/board/stage/canvas-size.d.ts
2
+ type BoardCanvasBox = {
3
+ width: number;
4
+ height: number;
5
+ density: number;
6
+ device: {
7
+ width: number;
8
+ height: number;
9
+ } | null;
10
+ };
11
+ declare function measureBoardCanvasBox(entry: ResizeObserverEntry, density: number): BoardCanvasBox;
12
+ declare function boardCanvasSize(box: BoardCanvasBox, density: number): {
13
+ width: number;
14
+ height: number;
15
+ resolution: number;
16
+ };
17
+ //#endregion
18
+ export { BoardCanvasBox, boardCanvasSize, measureBoardCanvasBox };
@@ -0,0 +1,31 @@
1
+ import { getBoardResolution } from "../render/text-resolution.js";
2
+ //#region src/board/stage/canvas-size.ts
3
+ function measureBoardCanvasBox(entry, density) {
4
+ const css = entry.contentBoxSize?.[0];
5
+ const device = entry.devicePixelContentBoxSize?.[0];
6
+ return {
7
+ width: css?.inlineSize ?? entry.contentRect.width,
8
+ height: css?.blockSize ?? entry.contentRect.height,
9
+ density,
10
+ device: device ? {
11
+ width: device.inlineSize,
12
+ height: device.blockSize
13
+ } : null
14
+ };
15
+ }
16
+ function boardCanvasSize(box, density) {
17
+ const resolution = getBoardResolution(box, density);
18
+ const { device } = box;
19
+ if (device && box.density === density && resolution === density) return {
20
+ width: device.width / density,
21
+ height: device.height / density,
22
+ resolution
23
+ };
24
+ return {
25
+ width: box.width,
26
+ height: box.height,
27
+ resolution
28
+ };
29
+ }
30
+ //#endregion
31
+ export { boardCanvasSize, measureBoardCanvasBox };
@@ -24,6 +24,10 @@ type SceneSyncInput = {
24
24
  structureVersion: number;
25
25
  geometryVersion: number;
26
26
  gestureActive: boolean;
27
+ focus?: {
28
+ x: number;
29
+ y: number;
30
+ };
27
31
  };
28
32
  type BoardSceneNode = {
29
33
  item: BoardSceneItem;
@@ -43,7 +47,7 @@ declare function createBoardScene(options: {
43
47
  world: Container;
44
48
  overlay: Graphics;
45
49
  getRenderer: BoardCardRendererResolver;
46
- onFarLayerFrame?: () => void;
50
+ requestFrame?: () => void;
47
51
  }): BoardScene;
48
52
  //#endregion
49
53
  export { BoardLiveStroke, BoardScene, BoardSceneNode, SceneSyncInput, createBoardScene };
@@ -1,6 +1,6 @@
1
1
  import { parseBoardItem } from "../../protocol/dist/board-model.js";
2
2
  import { drawPointsBounds } from "../../protocol/dist/board-layout.js";
3
- import { getBoardResolution, textZoomBucket } from "../render/text-resolution.js";
3
+ import { textZoomBucket } from "../render/text-resolution.js";
4
4
  import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
5
5
  import { Graphics } from "pixi.js";
6
6
  //#region src/board/stage/scene.ts
@@ -8,6 +8,7 @@ const FAR_LAYER_ENTER = 450;
8
8
  const FAR_LAYER_EXIT = 350;
9
9
  const FAR_PASS_BATCH = 240;
10
10
  const FAR_LIVE_LIMIT = 64;
11
+ const RESOLUTION_BATCH = 32;
11
12
  const Z_FAR_LAYER = -1;
12
13
  const Z_LIFTED = 1e9;
13
14
  const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
@@ -40,6 +41,7 @@ function createBoardScene(options) {
40
41
  let lastStructureVersion = -1;
41
42
  let lastZoom = NaN;
42
43
  let lastResolution = "";
44
+ const pendingResolution = /* @__PURE__ */ new Set();
43
45
  let liveVersion = 0;
44
46
  let lifted = /* @__PURE__ */ new Set();
45
47
  const liveStrokes = /* @__PURE__ */ new Map();
@@ -69,6 +71,7 @@ function createBoardScene(options) {
69
71
  function recycle(id, entry, context, destroyed) {
70
72
  setHeldKey(context, id, null);
71
73
  cards.delete(id);
74
+ pendingResolution.delete(id);
72
75
  animatedIds.delete(id);
73
76
  tickingIds.delete(id);
74
77
  if (destroyed) return;
@@ -182,7 +185,7 @@ function createBoardScene(options) {
182
185
  }
183
186
  if (index < ids.length) {
184
187
  farFrame = requestAnimationFrame(step);
185
- if (target === farFront) options.onFarLayerFrame?.();
188
+ if (target === farFront) options.requestFrame?.();
186
189
  return;
187
190
  }
188
191
  if (target === farBack) {
@@ -195,7 +198,7 @@ function createBoardScene(options) {
195
198
  farFront.visible = farActive;
196
199
  farPainted = true;
197
200
  farRunning = false;
198
- options.onFarLayerFrame?.();
201
+ options.requestFrame?.();
199
202
  if (farQueued) runFarPass();
200
203
  };
201
204
  step();
@@ -292,9 +295,11 @@ function createBoardScene(options) {
292
295
  }
293
296
  function sync(input) {
294
297
  const { items, scene, context, getItem, visibleIds, pinnedIds, globalSig, structureVersion, geometryVersion, gestureActive } = input;
295
- const resolution = `${textZoomBucket(context.zoom)}:${getBoardResolution()}`;
296
- const resolutionChanged = resolution !== lastResolution;
297
- lastResolution = resolution;
298
+ const resolution = `${textZoomBucket(context.zoom)}:${context.textResolution}`;
299
+ if (resolution !== lastResolution) {
300
+ lastResolution = resolution;
301
+ queueResolution(input.focus);
302
+ }
298
303
  const structureChanged = structureVersion !== lastStructureVersion;
299
304
  if (structureChanged) {
300
305
  lastStructureVersion = structureVersion;
@@ -376,11 +381,10 @@ function createBoardScene(options) {
376
381
  entry.renderer.update(entry.container, item, context);
377
382
  entry.container.alpha = scene.opacity(id);
378
383
  if (entry.renderer.tick) tickingIds.add(id);
379
- } else if (resolutionChanged && entry.renderer.resolution) {
380
- entry.renderer.resolution(entry.container, item, context);
381
- if (entry.renderer.tick) tickingIds.add(id);
384
+ pendingResolution.delete(id);
382
385
  }
383
386
  }
387
+ refreshResolution(context);
384
388
  if (liveSetChanged) liveVersion += 1;
385
389
  if (structureChanged || liveSetChanged) syncClips(scene);
386
390
  if (context.zoom !== lastZoom) {
@@ -392,6 +396,27 @@ function createBoardScene(options) {
392
396
  if (structureChanged || farModeChanged || liveSetChanged || liftChanged) applyChildOrder(scene);
393
397
  syncLiveStrokes(input);
394
398
  }
399
+ function queueResolution(focus) {
400
+ const ids = [...cards].filter(([, entry]) => entry.renderer.resolution);
401
+ if (focus) {
402
+ const distance = ({ item: { frame } }) => (frame.x + frame.width / 2 - focus.x) ** 2 + (frame.y + frame.height / 2 - focus.y) ** 2;
403
+ ids.sort(([, a], [, b]) => distance(a) - distance(b));
404
+ }
405
+ pendingResolution.clear();
406
+ for (const [id] of ids) pendingResolution.add(id);
407
+ }
408
+ function refreshResolution(context) {
409
+ let budget = RESOLUTION_BATCH;
410
+ for (const id of pendingResolution) {
411
+ if (budget-- === 0) break;
412
+ pendingResolution.delete(id);
413
+ const entry = cards.get(id);
414
+ if (!entry) continue;
415
+ entry.renderer.resolution?.(entry.container, entry.item, context);
416
+ if (entry.renderer.tick) tickingIds.add(id);
417
+ }
418
+ if (pendingResolution.size) options.requestFrame?.();
419
+ }
395
420
  function tick(clock) {
396
421
  let next = null;
397
422
  for (const id of tickingIds) {
@@ -433,6 +458,7 @@ function createBoardScene(options) {
433
458
  lastStructureVersion = -1;
434
459
  lastZoom = NaN;
435
460
  lastResolution = "";
461
+ pendingResolution.clear();
436
462
  }
437
463
  return {
438
464
  sync,
@@ -1,6 +1,6 @@
1
1
  import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
2
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
3
- import { pointToWorld, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
3
+ import { pointToWorld, screenPoint, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
4
4
  import { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, validateBoardScriptCommit } from "../model/script-scene.js";
5
5
  import { boardScriptTarget } from "../model/script-hit.js";
6
6
  import { createBoardAnimationSampler, resolveBoardAnimationCamera } from "../model/sampling.js";
@@ -9,16 +9,17 @@ import { buildFallbackShapeColors, pickBoardColor, resolveItemColor } from "../m
9
9
  import { elementClientToLocal } from "../model/client-geometry.js";
10
10
  import { isCreationBoardTool } from "../editor/tool.js";
11
11
  import { resizeCursorForHandle } from "../editor/selection-transform.js";
12
- import { createBoardPlayer } from "../player/index.js";
13
- import { createBoardScriptObservers } from "../model/script-observers.js";
14
12
  import { parseBoardCssColor } from "../render/css-color.js";
13
+ import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
15
14
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
16
15
  import { getBoardResolution } from "../render/text-resolution.js";
17
- import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
18
16
  import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
19
17
  import { createBoardScriptHost } from "../render/script-host.js";
20
18
  import { defaultBoardRenderStyle } from "../render/style.js";
19
+ import { createBoardPlayer } from "../player/index.js";
20
+ import { createBoardScriptObservers } from "../model/script-observers.js";
21
21
  import { boardTextureEdge } from "./assets.js";
22
+ import { boardCanvasSize, measureBoardCanvasBox } from "./canvas-size.js";
22
23
  import { observeDisplayDensity } from "./display-density.js";
23
24
  import { toBoardPointerEvent } from "./pointer.js";
24
25
  import { createBoardPointerGestures } from "./pointer-gestures.js";
@@ -26,6 +27,7 @@ import { createBoardScene } from "./scene.js";
26
27
  import { scheduleBoardMaintenance } from "./maintenance.js";
27
28
  import { createBoardScriptPointerRouter } from "./script-pointer.js";
28
29
  import { drawBoardOverlay } from "./overlay.js";
30
+ import { createTextResolutionTracker } from "./text-resolution-tracker.js";
29
31
  import { createBoardItemViewLayer } from "./views.js";
30
32
  import { Application, Container, Graphics, RendererType } from "pixi.js";
31
33
  //#region src/board/stage/stage.ts
@@ -111,6 +113,7 @@ function mountBoardStage(host, options) {
111
113
  let cardTickDue = 0;
112
114
  const staleIds = /* @__PURE__ */ new Set();
113
115
  let resizeFrame = 0;
116
+ let canvasBox = null;
114
117
  let cardEpoch = 0;
115
118
  let surface = {
116
119
  width: 0,
@@ -146,6 +149,7 @@ function mountBoardStage(host, options) {
146
149
  return cull.ids;
147
150
  }
148
151
  const textureScale = () => renderCamera().zoom * (app?.renderer.resolution ?? 1);
152
+ const textResolution = createTextResolutionTracker(TEXTURE_SETTLE_MS, scheduleTick);
149
153
  let refined = {
150
154
  scale: 0,
151
155
  geometry: 0
@@ -240,6 +244,7 @@ function mountBoardStage(host, options) {
240
244
  themeKey: current.key,
241
245
  rendererType: app?.renderer.type === RendererType.CANVAS ? "canvas" : "gpu",
242
246
  zoom: renderCamera().zoom,
247
+ textResolution: textResolution.value,
243
248
  reducedMotion: options.reducedMotion?.() ?? false,
244
249
  labels: options.labels?.() ?? DEFAULT_BOARD_RENDER_LABELS,
245
250
  taskRun: options.taskRun ?? (() => null),
@@ -631,6 +636,7 @@ function mountBoardStage(host, options) {
631
636
  world.position.set(camera.x, camera.y);
632
637
  world.scale.set(camera.zoom);
633
638
  const ids = visibleIds();
639
+ textResolution.update(textureScale());
634
640
  const context = renderContext(current, renderScene, frame.time);
635
641
  if (animationId && effectSampler) {
636
642
  context.time = editor.playhead?.time ?? frame.time;
@@ -657,7 +663,8 @@ function mountBoardStage(host, options) {
657
663
  staleIds,
658
664
  structureVersion: editor.structureVersion,
659
665
  geometryVersion: editor.geometryVersion,
660
- gestureActive: editor.gestureActive && editor.interaction.type !== "panning"
666
+ gestureActive: editor.gestureActive && editor.interaction.type !== "panning",
667
+ focus: pointToWorld(screenPoint(surface.width / 2, surface.height / 2), camera)
661
668
  });
662
669
  staleIds.clear();
663
670
  if (scene.liveVersion !== liveVersion) {
@@ -692,10 +699,14 @@ function mountBoardStage(host, options) {
692
699
  height: host.clientHeight
693
700
  };
694
701
  if (size.width <= 0 || size.height <= 0) return;
695
- const resolution = getBoardResolution(size);
696
- const sizeChanged = size.width !== surface.width || size.height !== surface.height;
697
- if (sizeChanged || app.renderer.resolution !== resolution) app.renderer.resize(size.width, size.height, resolution);
698
- if (sizeChanged) {
702
+ const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
703
+ const canvas = boardCanvasSize(canvasBox ?? {
704
+ ...size,
705
+ density,
706
+ device: null
707
+ }, density);
708
+ if (canvas.width !== app.screen.width || canvas.height !== app.screen.height || canvas.resolution !== app.renderer.resolution) app.renderer.resize(canvas.width, canvas.height, canvas.resolution);
709
+ if (size.width !== surface.width || size.height !== surface.height) {
699
710
  surface = size;
700
711
  editor.surfaceSize = surface;
701
712
  options.onSurfaceChange?.(surface);
@@ -853,14 +864,17 @@ function mountBoardStage(host, options) {
853
864
  }
854
865
  scheduleTick();
855
866
  });
856
- const resizeObserver = new ResizeObserver(resize);
867
+ const resizeObserver = new ResizeObserver((entries) => {
868
+ const entry = entries[entries.length - 1];
869
+ if (entry) canvasBox = measureBoardCanvasBox(entry, host.ownerDocument.defaultView?.devicePixelRatio ?? 1);
870
+ resize();
871
+ });
857
872
  return {
858
873
  ready: (async () => {
859
874
  const instance = new Application();
860
875
  try {
861
876
  await instance.init({
862
877
  antialias: true,
863
- autoDensity: true,
864
878
  backgroundAlpha: 0,
865
879
  width: host.clientWidth,
866
880
  height: host.clientHeight,
@@ -885,6 +899,8 @@ function mountBoardStage(host, options) {
885
899
  position: "relative",
886
900
  zIndex: "1",
887
901
  display: "block",
902
+ width: "100%",
903
+ height: "100%",
888
904
  touchAction: "none"
889
905
  });
890
906
  host.insertBefore(instance.canvas, views.element);
@@ -903,10 +919,14 @@ function mountBoardStage(host, options) {
903
919
  world: nodes,
904
920
  overlay,
905
921
  getRenderer: resolveRenderer,
906
- onFarLayerFrame: scheduleTick
922
+ requestFrame: scheduleTick
907
923
  });
908
924
  for (const [type, listener, listenerOptions] of listeners) host.addEventListener(type, listener, listenerOptions);
909
- resizeObserver.observe(host);
925
+ try {
926
+ resizeObserver.observe(instance.canvas, { box: "device-pixel-content-box" });
927
+ } catch {
928
+ resizeObserver.observe(instance.canvas);
929
+ }
910
930
  const window = host.ownerDocument.defaultView;
911
931
  if (window) stopDensity = observeDisplayDensity(window, resize);
912
932
  resize();
@@ -990,6 +1010,7 @@ function mountBoardStage(host, options) {
990
1010
  pointers.cancel();
991
1011
  stopMarch();
992
1012
  stopRefine();
1013
+ textResolution.dispose();
993
1014
  scriptPointers.clear();
994
1015
  stopTransforms?.();
995
1016
  scriptObservers.clear();
@@ -0,0 +1,8 @@
1
+ //#region src/board/stage/text-resolution-tracker.d.ts
2
+ declare function createTextResolutionTracker(settleMs: number, onSettle: () => void): {
3
+ readonly value: number;
4
+ update(next: number): number;
5
+ dispose(): void;
6
+ };
7
+ //#endregion
8
+ export { createTextResolutionTracker };
@@ -0,0 +1,33 @@
1
+ import { boardTextResolution } from "../render/text-resolution.js";
2
+ //#region src/board/stage/text-resolution-tracker.ts
3
+ function createTextResolutionTracker(settleMs, onSettle) {
4
+ let target = 0;
5
+ let value = 0;
6
+ let settled = false;
7
+ let timer = null;
8
+ return {
9
+ get value() {
10
+ return value;
11
+ },
12
+ update(next) {
13
+ if (next !== target) {
14
+ target = next;
15
+ settled = false;
16
+ if (timer !== null) clearTimeout(timer);
17
+ timer = setTimeout(() => {
18
+ timer = null;
19
+ settled = true;
20
+ onSettle();
21
+ }, settleMs);
22
+ }
23
+ value = boardTextResolution(target, value, settled);
24
+ return value;
25
+ },
26
+ dispose() {
27
+ if (timer !== null) clearTimeout(timer);
28
+ timer = null;
29
+ }
30
+ };
31
+ }
32
+ //#endregion
33
+ export { createTextResolutionTracker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "12.0.0",
3
+ "version": "12.1.0",
4
4
  "description": "Cohub SDK for spaces, sessions, boards, and realtime agent collaboration.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,