@neta-art/cohub 12.1.1 → 12.3.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 (134) hide show
  1. package/README.md +85 -8
  2. package/dist/board/editor/clipboard.js +1 -1
  3. package/dist/board/editor/creation.js +1 -1
  4. package/dist/board/editor/document.d.ts +3 -1
  5. package/dist/board/editor/document.js +8 -6
  6. package/dist/board/editor/editor.d.ts +8 -3
  7. package/dist/board/editor/editor.js +8 -2
  8. package/dist/board/editor/index.d.ts +3 -3
  9. package/dist/board/editor/index.js +2 -2
  10. package/dist/board/editor/interaction.d.ts +23 -2
  11. package/dist/board/editor/interaction.js +93 -4
  12. package/dist/board/editor/query.d.ts +1 -1
  13. package/dist/board/editor/query.js +3 -7
  14. package/dist/board/editor/selection.js +1 -1
  15. package/dist/board/editor/snapshots.js +5 -57
  16. package/dist/board/editor/tool.d.ts +4 -1
  17. package/dist/board/editor/tool.js +6 -1
  18. package/dist/board/export/index.d.ts +2 -2
  19. package/dist/board/export/index.js +1 -1
  20. package/dist/board/export/node/fonts.js +4 -0
  21. package/dist/board/export/node/index.d.ts +2 -0
  22. package/dist/board/export/node/index.js +8 -1
  23. package/dist/board/export/node/scripts.js +1 -1
  24. package/dist/board/export/scene.d.ts +2 -2
  25. package/dist/board/export/scene.js +2 -2
  26. package/dist/board/model/asset-key.d.ts +24 -0
  27. package/dist/board/model/asset-key.js +104 -0
  28. package/dist/board/model/export-assets.d.ts +7 -3
  29. package/dist/board/model/export-assets.js +34 -13
  30. package/dist/board/model/index.d.ts +9 -7
  31. package/dist/board/model/index.js +22 -20
  32. package/dist/board/model/items/create.js +3 -3
  33. package/dist/board/model/items/definition.d.ts +9 -1
  34. package/dist/board/model/items/index.d.ts +2 -2
  35. package/dist/board/model/items/registry.d.ts +2 -1
  36. package/dist/board/model/items/registry.js +1 -0
  37. package/dist/board/model/media-sizes.d.ts +28 -0
  38. package/dist/board/model/media-sizes.js +64 -0
  39. package/dist/board/model/palette.d.ts +1 -0
  40. package/dist/board/model/palette.js +20 -130
  41. package/dist/board/model/sampling.js +1 -1
  42. package/dist/board/model/text-metrics.d.ts +28 -3
  43. package/dist/board/model/text-metrics.js +36 -2
  44. package/dist/board/model/themes.d.ts +495 -0
  45. package/dist/board/model/themes.js +500 -0
  46. package/dist/board/render/card-tag.js +2 -2
  47. package/dist/board/render/index.d.ts +11 -7
  48. package/dist/board/render/index.js +9 -5
  49. package/dist/board/render/labels.d.ts +15 -0
  50. package/dist/board/render/labels.js +14 -0
  51. package/dist/board/render/lod.d.ts +30 -0
  52. package/dist/board/render/lod.js +33 -0
  53. package/dist/board/render/media-interaction.d.ts +5 -2
  54. package/dist/board/render/media-interaction.js +15 -3
  55. package/dist/board/render/palette.js +2 -25
  56. package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
  57. package/dist/board/render/renderers/audio-card-renderer.js +3 -2
  58. package/dist/board/render/renderers/board-renderer-registry.d.ts +22 -1
  59. package/dist/board/render/renderers/board-renderer-registry.js +9 -4
  60. package/dist/board/render/renderers/custom-card-renderer.js +5 -5
  61. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  62. package/dist/board/render/renderers/far-plate.d.ts +14 -1
  63. package/dist/board/render/renderers/far-plate.js +31 -8
  64. package/dist/board/render/renderers/file-card-renderer.js +49 -31
  65. package/dist/board/render/renderers/frame-card-renderer.js +9 -3
  66. package/dist/board/render/renderers/image-card-renderer.js +34 -8
  67. package/dist/board/render/renderers/model-card-renderer.js +6 -6
  68. package/dist/board/render/renderers/shape-card-renderer.js +25 -24
  69. package/dist/board/render/renderers/task-card-renderer.js +9 -10
  70. package/dist/board/render/renderers/text-card-renderer.js +86 -41
  71. package/dist/board/render/renderers/video-card-renderer.js +5 -4
  72. package/dist/board/render/script-export.js +1 -1
  73. package/dist/board/render/script-host.js +3 -3
  74. package/dist/board/render/style.d.ts +8 -2
  75. package/dist/board/render/style.js +8 -2
  76. package/dist/board/render/surface.d.ts +5 -0
  77. package/dist/board/render/surface.js +13 -3
  78. package/dist/board/render/text-edit.d.ts +32 -0
  79. package/dist/board/render/text-edit.js +134 -0
  80. package/dist/board/render/text-fit.d.ts +1 -3
  81. package/dist/board/render/text-fit.js +6 -49
  82. package/dist/board/render/text-measurement.d.ts +25 -1
  83. package/dist/board/render/text-measurement.js +157 -19
  84. package/dist/board/render/text-resolution.d.ts +1 -6
  85. package/dist/board/render/text-resolution.js +1 -10
  86. package/dist/board/render/theme.d.ts +15 -0
  87. package/dist/board/render/theme.js +15 -0
  88. package/dist/board/replica/index.d.ts +4 -2
  89. package/dist/board/replica/index.js +28 -17
  90. package/dist/board/replica/indexeddb.d.ts +16 -0
  91. package/dist/board/replica/indexeddb.js +155 -0
  92. package/dist/board/stage/assets.d.ts +28 -5
  93. package/dist/board/stage/assets.js +293 -102
  94. package/dist/board/stage/bench.d.ts +35 -0
  95. package/dist/board/stage/bench.js +153 -0
  96. package/dist/board/stage/device.d.ts +41 -0
  97. package/dist/board/stage/device.js +62 -0
  98. package/dist/board/stage/frame-budget.d.ts +19 -0
  99. package/dist/board/stage/frame-budget.js +69 -0
  100. package/dist/board/stage/idle.d.ts +4 -0
  101. package/dist/board/stage/idle.js +26 -0
  102. package/dist/board/stage/index.d.ts +11 -7
  103. package/dist/board/stage/index.js +7 -4
  104. package/dist/board/stage/lru.d.ts +13 -9
  105. package/dist/board/stage/lru.js +38 -20
  106. package/dist/board/stage/presses.d.ts +21 -0
  107. package/dist/board/stage/presses.js +66 -0
  108. package/dist/board/stage/scene.d.ts +15 -1
  109. package/dist/board/stage/scene.js +248 -141
  110. package/dist/board/stage/stage.d.ts +28 -2
  111. package/dist/board/stage/stage.js +404 -112
  112. package/dist/board/stage/texture-pool.d.ts +10 -1
  113. package/dist/board/stage/texture-pool.js +72 -2
  114. package/dist/chunks/environment.d.ts +6 -0
  115. package/dist/chunks/http.d.ts +9 -1
  116. package/dist/chunks/http.js +791 -4
  117. package/dist/index.d.ts +52 -1
  118. package/dist/index.js +202 -862
  119. package/dist/media.d.ts +3 -27
  120. package/dist/media.js +3 -82
  121. package/dist/protocol/dist/board-constants.js +3 -3
  122. package/dist/protocol/dist/board-layout.d.ts +17 -3
  123. package/dist/protocol/dist/board-layout.js +68 -11
  124. package/dist/protocol/dist/board-model.d.ts +15 -1
  125. package/dist/protocol/dist/board-model.js +12 -2
  126. package/dist/protocol/dist/board-patch.js +1 -1
  127. package/dist/protocol/dist/board-schema.d.ts +2 -1
  128. package/dist/protocol/dist/board-schema.js +4 -1
  129. package/dist/protocol/dist/core/media.d.ts +35 -1
  130. package/dist/protocol/dist/core/media.js +127 -1
  131. package/dist/protocol/dist/index.d.ts +3 -3
  132. package/package.json +4 -2
  133. package/dist/board/model/image-key.d.ts +0 -9
  134. package/dist/board/model/image-key.js +0 -33
@@ -1,6 +1,6 @@
1
1
  import { cardElevation, containRect, drawCardSurface, fillCardPicture } from "../surface.js";
2
2
  import { positionShell } from "./base-card-renderer.js";
3
- import { drawFarPlate } from "./far-plate.js";
3
+ import { drawFarPlate, farPictureTone } from "./far-plate.js";
4
4
  import { Container, Graphics } from "pixi.js";
5
5
  //#region src/board/render/renderers/image-card-renderer.ts
6
6
  const partsByContainer = /* @__PURE__ */ new WeakMap();
@@ -16,6 +16,8 @@ function cropRect(crop) {
16
16
  } : null;
17
17
  }
18
18
  const PRINT_MARGIN = 6;
19
+ const FADE_MS = 160;
20
+ const FAR_ALPHA = .9;
19
21
  function sync(container, item, context) {
20
22
  const parts = partsByContainer.get(container);
21
23
  if (!parts) return;
@@ -36,7 +38,15 @@ function sync(container, item, context) {
36
38
  context.themeKey
37
39
  ].join("|");
38
40
  if (sig === parts.sig) return;
41
+ if (texture && !parts.textured && parts.sig !== "" && !context.reducedMotion) {
42
+ parts.fadeFrom = performance.now();
43
+ parts.picture.alpha = 0;
44
+ } else if (!texture) {
45
+ parts.fadeFrom = null;
46
+ parts.picture.alpha = 1;
47
+ }
39
48
  parts.sig = sig;
49
+ parts.textured = Boolean(texture);
40
50
  const crop = cropRect(item.props.crop);
41
51
  const framed = context.style.media === "framed";
42
52
  const margin = framed ? Math.min(PRINT_MARGIN, width / 8, height / 8) : 0;
@@ -69,11 +79,13 @@ function sync(container, item, context) {
69
79
  radius: 0
70
80
  } : context.style, crop);
71
81
  parts.placeholder.clear();
72
- if (texture && !framed) return;
82
+ parts.clearSurface = !framed;
83
+ if (texture && !framed && parts.fadeFrom === null) return;
73
84
  drawCardSurface(parts.placeholder, card, context.style, {
74
- fill: context.palette.surface,
75
- border: texture ? null : failed ? context.palette.border : context.palette.hover,
76
- elevation
85
+ fill: failed || framed ? context.palette.surface : context.palette.hover,
86
+ fillAlpha: failed || framed ? 1 : FAR_ALPHA,
87
+ border: failed ? context.palette.border : null,
88
+ elevation: texture && !framed ? 0 : elevation
77
89
  });
78
90
  if (texture) return;
79
91
  const cx = width / 2;
@@ -101,7 +113,10 @@ const imageCardRenderer = {
101
113
  root,
102
114
  placeholder,
103
115
  picture,
104
- sig: ""
116
+ sig: "",
117
+ textured: false,
118
+ fadeFrom: null,
119
+ clearSurface: false
105
120
  });
106
121
  if (item.type === "image") sync(root, item, context);
107
122
  return root;
@@ -109,10 +124,21 @@ const imageCardRenderer = {
109
124
  update: (container, item, context) => {
110
125
  if (item.type === "image") sync(container, item, context);
111
126
  },
127
+ tick: (container, clock) => {
128
+ const parts = partsByContainer.get(container);
129
+ if (parts?.fadeFrom == null) return null;
130
+ const progress = Math.min(1, Math.max(0, (clock - parts.fadeFrom) / FADE_MS));
131
+ parts.picture.alpha = 1 - (1 - progress) ** 3;
132
+ if (progress < 1) return 16;
133
+ parts.fadeFrom = null;
134
+ if (parts.clearSurface) parts.placeholder.clear();
135
+ return null;
136
+ },
137
+ lod: { kind: "media" },
112
138
  renderFar: (graphics, item, context) => {
113
139
  drawFarPlate(graphics, item.frame, {
114
- fill: context.palette.hover,
115
- fillAlpha: .9
140
+ fill: farPictureTone(item, context) ?? context.palette.hover,
141
+ fillAlpha: FAR_ALPHA
116
142
  });
117
143
  },
118
144
  destroy: (container) => {
@@ -1,12 +1,12 @@
1
- import { isBoardModelItem } from "../../model/media.js";
2
- import { fileStem } from "../../model/file-snapshot.js";
3
- import { fileMetaLine } from "../../model/file-preview.js";
4
- import { boardCardDetailed } from "../text-resolution.js";
1
+ import { boardCardDetailed } from "../lod.js";
5
2
  import { createCardTag, syncCardTag, traceCube, zoomCardTag } from "../card-tag.js";
3
+ import { isBoardModelItem } from "../../model/media.js";
6
4
  import { cardElevation, containRect, drawCardSurface } from "../surface.js";
7
5
  import { positionShell } from "./base-card-renderer.js";
8
- import { drawFarPlate } from "./far-plate.js";
6
+ import { drawFarPlate, farPictureTone } from "./far-plate.js";
9
7
  import { drawModelKey } from "./media-key.js";
8
+ import { fileStem } from "../../model/file-snapshot.js";
9
+ import { fileMetaLine } from "../../model/file-preview.js";
10
10
  import { Container, Graphics, Sprite, Texture } from "pixi.js";
11
11
  //#region src/board/render/renderers/model-card-renderer.ts
12
12
  const partsByContainer = /* @__PURE__ */ new WeakMap();
@@ -120,7 +120,7 @@ const modelCardRenderer = {
120
120
  },
121
121
  renderFar: (graphics, item, context) => {
122
122
  drawFarPlate(graphics, item.frame, {
123
- fill: context.palette.surface,
123
+ fill: farPictureTone(item, context) ?? context.palette.surface,
124
124
  fillAlpha: .96,
125
125
  accent: context.palette.legendary,
126
126
  accentAlpha: .45
@@ -1,7 +1,8 @@
1
- import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
1
  import { worldPoint } from "../../model/geometry.js";
3
2
  import { itemAlpha, itemColor } from "../palette.js";
4
- import { syncTextResolution, syncTextWrapWidth } from "../text-resolution.js";
3
+ import { layoutShapeLabel } from "../../model/text-metrics.js";
4
+ import { boardTextStyle, cssBaselineShift } from "../text-measurement.js";
5
+ import { syncTextResolution } from "../text-resolution.js";
5
6
  import { dashPattern, roundRectPoints, tracePolyline, trimPolyline } from "../stroke.js";
6
7
  import { positionShell } from "./base-card-renderer.js";
7
8
  import { drawFarPlate } from "./far-plate.js";
@@ -142,24 +143,34 @@ function sync(container, item, context) {
142
143
  }
143
144
  parts.label.alpha = itemAlpha(context, style.stroke);
144
145
  const label = itemColor(context, style.stroke, "neutral", "label");
146
+ const layout = layoutShapeLabel(props, width);
147
+ const text = layout.lines.join("\n");
145
148
  const textSig = [
146
- props.text,
149
+ text,
147
150
  label,
148
151
  props.fontSize,
149
- props.align
152
+ props.align,
153
+ context.themeKey
150
154
  ].join("|");
151
155
  if (textSig !== parts.textSig) {
152
156
  parts.textSig = textSig;
153
- parts.label.text = props.text;
154
- parts.label.visible = props.text.length > 0;
155
- parts.label.style.fill = label;
156
- parts.label.style.fontSize = props.fontSize;
157
- parts.label.style.lineHeight = props.fontSize * 1.4;
158
- parts.label.style.align = props.align;
157
+ parts.label.text = text;
158
+ Object.assign(parts.label.style, {
159
+ ...boardTextStyle({
160
+ font: "sans",
161
+ fontSize: props.fontSize,
162
+ fontWeight: 500
163
+ }),
164
+ fill: label,
165
+ lineHeight: layout.lineHeight,
166
+ align: props.align
167
+ });
159
168
  }
160
- syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), context.resizingIds.has(item.id));
169
+ parts.label.visible = props.text.length > 0 && context.editingId !== item.id;
161
170
  syncTextResolution(parts.label, context.textResolution);
162
- parts.label.position.set(width / 2, height / 2);
171
+ const anchor = props.align === "left" ? 0 : props.align === "right" ? 1 : .5;
172
+ parts.label.anchor.set(anchor, .5);
173
+ parts.label.position.set(8 + Math.max(0, width - 16) * anchor, height / 2 + cssBaselineShift(parts.label.style));
163
174
  }
164
175
  const shapeCardRenderer = {
165
176
  id: "shape-card",
@@ -169,27 +180,17 @@ const shapeCardRenderer = {
169
180
  const shape = new Graphics();
170
181
  const label = new Text({
171
182
  text: "",
172
- style: {
173
- fill: 16777215,
174
- fontFamily: BOARD_FONT_STACK,
175
- fontSize: 20,
176
- fontWeight: "500",
177
- align: "center",
178
- wordWrap: true,
179
- lineHeight: 28
180
- },
183
+ style: { fill: 16777215 },
181
184
  resolution: context.textResolution,
182
185
  roundPixels: true
183
186
  });
184
- label.anchor.set(.5);
185
187
  root.addChild(shape, label);
186
188
  partsByContainer.set(root, {
187
189
  root,
188
190
  shape,
189
191
  label,
190
192
  visualSig: "",
191
- textSig: "",
192
- wrapWidth: 0
193
+ textSig: ""
193
194
  });
194
195
  if (item.type === "shape") sync(root, item, context);
195
196
  return root;
@@ -1,12 +1,13 @@
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 { fitTextToLines } from "../text-fit.js";
4
- import { boardCardDetailed, syncTextResolution } from "../text-resolution.js";
3
+ import { boardCardDetailed } from "../lod.js";
4
+ import { fitTextToLines, fitTextToWidth } from "../text-fit.js";
5
+ import { syncTextResolution } from "../text-resolution.js";
5
6
  import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
6
7
  import { drawAudioWaveform } from "../audio-waveform.js";
7
8
  import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
8
9
  import { positionShell } from "./base-card-renderer.js";
9
- import { drawFarPlate } from "./far-plate.js";
10
+ import { drawFarPlate, farPictureTone } from "./far-plate.js";
10
11
  import { Container, FillGradient, Graphics, Sprite, Text } from "pixi.js";
11
12
  //#region src/board/render/renderers/task-card-renderer.ts
12
13
  const PADDING = 12;
@@ -226,7 +227,7 @@ function advance(parts, clock) {
226
227
  const elapsed = formatBoardDuration(elapsedMs);
227
228
  if (elapsed !== motion.elapsed) {
228
229
  motion.elapsed = elapsed;
229
- parts.body.text = `${motion.label} · ${elapsed}`;
230
+ fitTextToWidth(parts.body, `${motion.label} · ${elapsed}`, Math.max(24, motion.width - 24));
230
231
  }
231
232
  const untilNextSecond = SECOND - Math.max(0, elapsedMs) % SECOND;
232
233
  next = next === null ? untilNextSecond : Math.min(next, untilNextSecond);
@@ -296,7 +297,8 @@ function sync(container, item, context) {
296
297
  color,
297
298
  context.palette.text,
298
299
  context.palette.muted,
299
- context.style.font
300
+ context.style.font,
301
+ context.themeKey
300
302
  ].join("|");
301
303
  if (textSig !== parts.textSig) {
302
304
  parts.textSig = textSig;
@@ -441,9 +443,7 @@ const taskCardRenderer = {
441
443
  style: {
442
444
  fontFamily,
443
445
  fontSize,
444
- fontWeight,
445
- wordWrap: true,
446
- breakWords: true
446
+ fontWeight
447
447
  },
448
448
  resolution: context.textResolution,
449
449
  roundPixels: true
@@ -452,7 +452,6 @@ const taskCardRenderer = {
452
452
  const detail = text(BOARD_FONT_STACK, 10.5, "400");
453
453
  const meta = text(BOARD_MONO_FONT_STACK, 10, "500");
454
454
  const badge = text(BOARD_MONO_FONT_STACK, 10, "500");
455
- badge.style.wordWrap = false;
456
455
  badge.anchor.set(.5);
457
456
  detail.anchor.set(.5, 0);
458
457
  detail.style.align = "center";
@@ -513,7 +512,7 @@ const taskCardRenderer = {
513
512
  const { status } = liveStatus(item, context);
514
513
  const active = status === "failed" || status === "running" || status === "pending";
515
514
  drawFarPlate(graphics, item.frame, {
516
- fill: context.palette.surface,
515
+ fill: farPictureTone(item, context) ?? context.palette.surface,
517
516
  fillAlpha: .82,
518
517
  ...active ? {
519
518
  accent: statusColor(status, context),
@@ -1,25 +1,43 @@
1
- import { layoutBoardText } from "../../../protocol/dist/board-layout.js";
2
- import { boardFontFamily } from "../../model/text-metrics.js";
1
+ import { estimateTextWidth, layoutBoardText } from "../../../protocol/dist/board-layout.js";
3
2
  import { itemAlpha, itemColor } from "../palette.js";
4
- import { BOARD_FONT_STACKS } from "../text-measurement.js";
3
+ import { boardTextStyle, canvasFont, cssBaselineShift, layoutTextLines } from "../text-measurement.js";
4
+ import { boardTextSize } from "../lod.js";
5
5
  import { syncTextResolution } from "../text-resolution.js";
6
6
  import { positionShell } from "./base-card-renderer.js";
7
- import { drawFarPlate } from "./far-plate.js";
7
+ import { drawFarBars } from "./far-plate.js";
8
8
  import { Container, Graphics, Text } from "pixi.js";
9
9
  //#region src/board/render/renderers/text-card-renderer.ts
10
10
  const partsByContainer = /* @__PURE__ */ new WeakMap();
11
- function revealed(text, reveal) {
12
- if (reveal >= 1) return text;
13
- return [...text].slice(0, Math.round(text.length * Math.max(0, reveal))).join("");
11
+ const ALIGN = {
12
+ left: 0,
13
+ center: .5,
14
+ right: 1
15
+ };
16
+ const FAR_MAX_LINES = 64;
17
+ function revealed(lines, reveal) {
18
+ if (reveal >= 1) return lines;
19
+ const glyphs = lines.map((line) => [...line]);
20
+ let budget = Math.round(glyphs.reduce((sum, line) => sum + line.length, 0) * Math.max(0, reveal));
21
+ const shown = [];
22
+ for (const line of glyphs) {
23
+ shown.push(line.slice(0, budget).join(""));
24
+ budget -= line.length;
25
+ if (budget <= 0) break;
26
+ }
27
+ return shown;
14
28
  }
15
- function syncCaret(parts, item, context) {
29
+ function syncCaret(parts, item, context, anchor) {
16
30
  const { props } = item;
17
- const visible = props.caret && props.reveal < 1 && Math.floor(context.time / 500) % 2 === 0;
31
+ const visible = parts.body.visible && props.caret && props.reveal < 1 && Math.floor(context.time / 500) % 2 === 0;
18
32
  parts.caret.visible = visible;
19
- if (!visible) return;
20
- const anchorX = props.align === "center" ? .5 : props.align === "right" ? 1 : 0;
21
- const x = parts.body.position.x + parts.body.width * (1 - anchorX) + 2;
22
- parts.caret.clear().rect(x, 1, Math.max(1, 1.5 / Math.max(.25, context.zoom)), Math.max(12, parts.body.height - 2)).fill({ color: itemColor(context, item.style.fill, "neutral") });
33
+ if (!visible || !parts.layout) return;
34
+ const scale = parts.body.scale.x;
35
+ const lastWidth = layoutTextLines(parts.lines.at(-1) ?? "", canvasFont(boardTextStyle(props))).width;
36
+ const x = parts.body.position.x + lastWidth * (1 - anchor) * scale + 2;
37
+ const y = Math.max(0, parts.lines.length - 1) * parts.layout.lineHeight * scale;
38
+ const width = Math.max(1, 1.5 / Math.max(.25, context.zoom));
39
+ const height = Math.max(12, parts.layout.lineHeight * scale - 2);
40
+ parts.caret.clear().rect(x, y + 1, width, height).fill({ color: itemColor(context, item.style.fill, "neutral") });
23
41
  }
24
42
  function sync(container, item, context) {
25
43
  const parts = partsByContainer.get(container);
@@ -27,42 +45,63 @@ function sync(container, item, context) {
27
45
  positionShell(parts.root, item);
28
46
  const { props } = item;
29
47
  const layout = layoutBoardText(props);
30
- const previewScale = Math.max(1e-4, item.frame.width / Math.max(1e-4, layout.width));
31
- parts.body.scale.set(previewScale);
32
- const anchorX = props.align === "center" ? .5 : props.align === "right" ? 1 : 0;
33
- parts.body.anchor.set(anchorX, 0);
34
- parts.body.position.set(item.frame.width * anchorX, 0);
48
+ const scale = Math.max(1e-4, item.frame.width / Math.max(1e-4, layout.width));
49
+ const anchor = ALIGN[props.align] ?? 0;
50
+ parts.body.scale.set(scale);
51
+ parts.body.anchor.set(anchor, 0);
52
+ parts.body.visible = context.editingId !== item.id;
35
53
  parts.body.alpha = itemAlpha(context, item.style.fill);
36
54
  parts.caret.alpha = parts.body.alpha;
37
55
  const ink = itemColor(context, item.style.fill, "neutral");
38
- const text = revealed(props.text, props.reveal);
39
- const contentSig = [
56
+ const lines = revealed(layout.lines, props.reveal);
57
+ const text = lines.join("\n");
58
+ const style = boardTextStyle(props);
59
+ const sig = [
40
60
  text,
41
61
  ink,
42
- props.fontSize,
43
- props.fontWeight,
44
- props.font,
45
62
  props.align,
46
- props.lineHeight,
47
- props.width ?? ""
63
+ layout.lineHeight,
64
+ style.fontFamily,
65
+ style.fontSize,
66
+ style.fontWeight,
67
+ context.themeKey
48
68
  ].join("|");
49
- if (contentSig !== parts.contentSig) {
50
- parts.contentSig = contentSig;
51
- parts.body.text = text;
69
+ parts.layout = layout;
70
+ parts.lines = lines;
71
+ if (sig !== parts.sig) {
72
+ parts.sig = sig;
52
73
  Object.assign(parts.body.style, {
74
+ ...style,
53
75
  fill: ink,
54
- fontSize: props.fontSize,
55
- fontWeight: String(Math.round(props.fontWeight / 100) * 100),
56
- fontFamily: boardFontFamily(props.font, BOARD_FONT_STACKS),
57
76
  align: props.align,
58
77
  lineHeight: layout.lineHeight,
59
- wordWrap: Boolean(props.width),
60
- wordWrapWidth: props.width ?? 0,
61
- breakWords: true
78
+ wordWrap: false
62
79
  });
80
+ parts.body.text = text;
81
+ parts.baseline = cssBaselineShift(parts.body.style);
63
82
  }
64
- syncTextResolution(parts.body, context.textResolution * previewScale);
65
- syncCaret(parts, item, context);
83
+ parts.body.position.set(item.frame.width * anchor, parts.baseline * scale);
84
+ syncTextResolution(parts.body, context.textResolution * scale);
85
+ syncCaret(parts, item, context, anchor);
86
+ }
87
+ function farBars({ frame, props }) {
88
+ const layout = layoutBoardText(props);
89
+ const scale = frame.width / Math.max(1e-4, layout.width);
90
+ const lines = layout.lines.slice(0, FAR_MAX_LINES);
91
+ const widths = lines.map((line) => estimateTextWidth(line.trim(), props.fontSize));
92
+ const widest = Math.max(1e-4, ...widths);
93
+ const step = layout.lineHeight * scale;
94
+ const height = step * .45;
95
+ return lines.flatMap((_, index) => {
96
+ const width = frame.width * (widths[index] ?? 0) / widest;
97
+ if (width <= 0) return [];
98
+ return [{
99
+ x: props.align === "center" ? (frame.width - width) / 2 : props.align === "right" ? frame.width - width : 0,
100
+ y: index * step + (step - height) / 2,
101
+ width,
102
+ height
103
+ }];
104
+ });
66
105
  }
67
106
  const textCardRenderer = {
68
107
  id: "text-card",
@@ -71,7 +110,6 @@ const textCardRenderer = {
71
110
  const root = new Container();
72
111
  const body = new Text({
73
112
  text: "",
74
- style: { fontFamily: BOARD_FONT_STACKS.sans },
75
113
  resolution: context.textResolution,
76
114
  roundPixels: true
77
115
  });
@@ -81,7 +119,10 @@ const textCardRenderer = {
81
119
  root,
82
120
  body,
83
121
  caret,
84
- contentSig: ""
122
+ layout: null,
123
+ lines: [],
124
+ sig: "",
125
+ baseline: 0
85
126
  });
86
127
  if (item.type === "text") sync(root, item, context);
87
128
  return root;
@@ -93,11 +134,15 @@ const textCardRenderer = {
93
134
  const parts = partsByContainer.get(container);
94
135
  if (parts) syncTextResolution(parts.body, context.textResolution * parts.body.scale.x);
95
136
  },
137
+ lod: {
138
+ kind: "text",
139
+ size: boardTextSize
140
+ },
96
141
  renderFar: (graphics, item, context) => {
97
142
  if (item.type !== "text") return;
98
- drawFarPlate(graphics, item.frame, {
99
- fill: itemColor(context, item.style.fill, "neutral"),
100
- fillAlpha: .35 * itemAlpha(context, item.style.fill)
143
+ drawFarBars(graphics, item.frame, farBars(item), {
144
+ color: itemColor(context, item.style.fill, "neutral"),
145
+ alpha: .4 * itemAlpha(context, item.style.fill)
101
146
  });
102
147
  },
103
148
  destroy: (container) => {
@@ -1,9 +1,9 @@
1
- import { getMediaResourceTitle } from "../../model/media.js";
2
- import { boardCardDetailed } from "../text-resolution.js";
1
+ import { boardCardDetailed } from "../lod.js";
3
2
  import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
3
+ import { getMediaResourceTitle } from "../../model/media.js";
4
4
  import { cardElevation, containRect, drawCardSurface, fillCardPicture } from "../surface.js";
5
5
  import { positionShell } from "./base-card-renderer.js";
6
- import { drawFarPlate } from "./far-plate.js";
6
+ import { drawFarPlate, farPictureTone } from "./far-plate.js";
7
7
  import { drawMediaPlayKey } from "./media-key.js";
8
8
  import { Container, Graphics } from "pixi.js";
9
9
  //#region src/board/render/renderers/video-card-renderer.ts
@@ -122,9 +122,10 @@ const videoCardRenderer = {
122
122
  const tag = partsByContainer.get(container)?.tag;
123
123
  if (tag) zoomCardTag(tag, zoom);
124
124
  },
125
+ lod: { kind: "media" },
125
126
  renderFar: (graphics, item, context) => {
126
127
  drawFarPlate(graphics, item.frame, {
127
- fill: context.palette.surface,
128
+ fill: farPictureTone(item, context) ?? context.palette.surface,
128
129
  fillAlpha: .96,
129
130
  accent: context.palette.brand,
130
131
  accentAlpha: .75
@@ -1,5 +1,5 @@
1
- import { composeBoardScriptItems } from "../model/script-scene.js";
2
1
  import { buildBoardScene } from "../model/scene.js";
2
+ import { composeBoardScriptItems } from "../model/script-scene.js";
3
3
  import { createBoardScriptHost } from "./script-host.js";
4
4
  //#region src/board/render/script-export.ts
5
5
  async function prepareBrowserBoardScripts(document, time, readModule, snapshots) {
@@ -1,9 +1,9 @@
1
- import { boardScriptItems } from "../model/script-index.js";
2
- import { boardScriptScope } from "../model/script-scene.js";
3
- import { boardScriptHitVisible, resolveBoardScriptHit } from "../model/script-hit.js";
4
1
  import { getBoardResolution } from "./text-resolution.js";
5
2
  import { BoardScriptTexture } from "./script-texture.js";
6
3
  import { scriptResolution } from "./script-resolution.js";
4
+ import { boardScriptItems } from "../model/script-index.js";
5
+ import { boardScriptScope } from "../model/script-scene.js";
6
+ import { boardScriptHitVisible, resolveBoardScriptHit } from "../model/script-hit.js";
7
7
  import { createBoardScriptModuleReader, loadBoardScriptSources } from "../../protocol/dist/board-script-modules.js";
8
8
  import { boardScriptFrameSource } from "./script-sandbox.js";
9
9
  //#region src/board/render/script-host.ts
@@ -9,6 +9,7 @@ type BoardRenderStyle = {
9
9
  icons: "line" | "pixel";
10
10
  motion: "smooth" | "stepped";
11
11
  font: string;
12
+ frameLabelSize: number;
12
13
  };
13
14
  type BoardShadowStyle = {
14
15
  color: number;
@@ -24,6 +25,11 @@ type BoardSelectionStyle = {
24
25
  handleSize: number;
25
26
  };
26
27
  declare function defaultBoardRenderStyle(mode: "dark" | "light"): BoardRenderStyle;
27
- declare function resolveBoardRenderStyle(mode: "dark" | "light", style?: Partial<BoardRenderStyle>): BoardRenderStyle;
28
+ /** Theme overrides of the default style; a shadow without positive alpha is none. */
29
+ type BoardRenderStyleInput = Partial<Omit<BoardRenderStyle, "shadow" | "selection">> & {
30
+ shadow?: Partial<BoardShadowStyle> | null;
31
+ selection?: Partial<BoardSelectionStyle>;
32
+ };
33
+ declare function resolveBoardRenderStyle(mode: "dark" | "light", style?: BoardRenderStyleInput): BoardRenderStyle;
28
34
  //#endregion
29
- export { BoardRenderStyle, BoardSelectionStyle, BoardShadowStyle, defaultBoardRenderStyle, resolveBoardRenderStyle };
35
+ export { BoardRenderStyle, BoardRenderStyleInput, BoardSelectionStyle, BoardShadowStyle, defaultBoardRenderStyle, resolveBoardRenderStyle };
@@ -1,4 +1,5 @@
1
1
  import { BOARD_FONT_STACK } from "../../protocol/dist/board-constants.js";
2
+ import "../model/text-metrics.js";
2
3
  //#region src/board/render/style.ts
3
4
  const BASE = {
4
5
  corner: "round",
@@ -14,7 +15,8 @@ const BASE = {
14
15
  progress: "shimmer",
15
16
  icons: "line",
16
17
  motion: "smooth",
17
- font: BOARD_FONT_STACK
18
+ font: BOARD_FONT_STACK,
19
+ frameLabelSize: 12
18
20
  };
19
21
  const SHADOW = {
20
22
  light: {
@@ -40,6 +42,10 @@ function defaultBoardRenderStyle(mode) {
40
42
  function resolveBoardRenderStyle(mode, style) {
41
43
  const base = defaultBoardRenderStyle(mode);
42
44
  if (!style) return base;
45
+ const shadow = style.shadow === null ? null : {
46
+ ...SHADOW[mode],
47
+ ...style.shadow
48
+ };
43
49
  return {
44
50
  ...base,
45
51
  ...style,
@@ -47,7 +53,7 @@ function resolveBoardRenderStyle(mode, style) {
47
53
  ...base.selection,
48
54
  ...style.selection
49
55
  },
50
- shadow: style.shadow === void 0 ? base.shadow : style.shadow
56
+ shadow: shadow && shadow.alpha > 0 ? shadow : null
51
57
  };
52
58
  }
53
59
  //#endregion
@@ -16,6 +16,11 @@ type CardCrop = {
16
16
  w: number;
17
17
  h: number;
18
18
  };
19
+ /**
20
+ * The canvas renderer fills with a repeating pattern, so bilinear samples at the
21
+ * edge would blend in the opposite side. Mapping the picture half a texel inside
22
+ * its source keeps every sample within it on both renderers.
23
+ */
19
24
  declare function fillCardPicture(graphics: Graphics, texture: Texture, picture: CardRect, style: Pick<BoardRenderStyle, "corner" | "radius">, crop?: CardCrop | null): Graphics;
20
25
  declare function tracePixelRect(graphics: Graphics, x: number, y: number, width: number, height: number, step: number): Graphics;
21
26
  declare function cardOutlinePoints(rect: CardRect, style: Pick<BoardRenderStyle, "corner" | "radius">, grow?: number): WorldPoint[];
@@ -13,23 +13,33 @@ function traceCardOutline(graphics, rect, style, grow = 0) {
13
13
  const radius = Math.min(Math.max(0, style.radius + grow), width / 2, height / 2);
14
14
  return radius > 0 ? graphics.roundRect(x, y, width, height, radius) : graphics.rect(x, y, width, height);
15
15
  }
16
+ /**
17
+ * The canvas renderer fills with a repeating pattern, so bilinear samples at the
18
+ * edge would blend in the opposite side. Mapping the picture half a texel inside
19
+ * its source keeps every sample within it on both renderers.
20
+ */
16
21
  function fillCardPicture(graphics, texture, picture, style, crop) {
17
22
  const sampling = texture.source.style;
18
23
  if (sampling.addressMode !== "mirror-repeat") {
19
24
  sampling.addressMode = "mirror-repeat";
20
25
  sampling.update();
21
26
  }
22
- const matrix = crop ? new Matrix(1 / crop.w, 0, 0, 1 / crop.h, -crop.x / crop.w, -crop.y / crop.h) : void 0;
27
+ const { x = 0, y = 0, w = 1, h = 1 } = crop ?? {};
28
+ const insetX = Math.min(.5 / texture.source.pixelWidth, w / 4);
29
+ const insetY = Math.min(.5 / texture.source.pixelHeight, h / 4);
30
+ const sw = w - insetX * 2;
31
+ const sh = h - insetY * 2;
32
+ const matrix = new Matrix(1 / sw, 0, 0, 1 / sh, -(x + insetX) / sw, -(y + insetY) / sh);
23
33
  return traceCardOutline(graphics, picture, style).fill({
24
34
  texture,
25
35
  textureSpace: "local",
26
- ...matrix ? { matrix } : {}
36
+ matrix
27
37
  });
28
38
  }
29
39
  function tracePixelRect(graphics, x, y, width, height, step) {
30
40
  const s = Math.min(step, width / 4, height / 4);
31
41
  if (s <= 0) return graphics.rect(x, y, width, height);
32
- return graphics.poly(pixelRectPoints(x, y, width, height, s).flatMap((point) => [point.x, point.y]));
42
+ return graphics.poly(pixelRectPoints(x, y, width, height, s).flatMap((point) => [point.x, point.y]), true);
33
43
  }
34
44
  function pixelRectPoints(x, y, width, height, s) {
35
45
  const r = x + width;
@@ -0,0 +1,32 @@
1
+ import { BoardViewport } from "../model/geometry.js";
2
+ import { BoardScene, BoardSceneItem } from "../model/scene.js";
3
+ import { BoardRenderStyle } from "./style.js";
4
+ import { BoardTextStyle } from "./text-measurement.js";
5
+ //#region src/board/render/text-edit.d.ts
6
+ /** A content-box textarea at left/top, transformed by `rotate(rotation deg) scale(scale)` about its center. */
7
+ type BoardTextEditLayout = {
8
+ left: number;
9
+ top: number;
10
+ width: number;
11
+ height: number;
12
+ padding: {
13
+ x: number;
14
+ y: number;
15
+ };
16
+ scale: number;
17
+ rotation: number;
18
+ font: BoardTextStyle;
19
+ lineHeight: number;
20
+ textAlign: "left" | "center" | "right";
21
+ whiteSpace: "pre" | "pre-wrap";
22
+ live: boolean;
23
+ };
24
+ type BoardTextEditInput = {
25
+ viewport: BoardViewport;
26
+ scene: BoardScene;
27
+ style?: Partial<Pick<BoardRenderStyle, "font" | "frameLabelSize">>;
28
+ draft: string;
29
+ };
30
+ declare function boardTextEditLayout(item: BoardSceneItem, { viewport, scene, style, draft }: BoardTextEditInput): BoardTextEditLayout | null;
31
+ //#endregion
32
+ export { BoardTextEditInput, BoardTextEditLayout, boardTextEditLayout };