@neta-art/cohub 12.1.0 → 12.2.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 (85) hide show
  1. package/README.md +8 -5
  2. package/dist/board/editor/editor.d.ts +1 -0
  3. package/dist/board/editor/editor.js +4 -0
  4. package/dist/board/editor/snapshots.js +5 -57
  5. package/dist/board/export/index.d.ts +2 -2
  6. package/dist/board/export/node/fonts.js +4 -0
  7. package/dist/board/export/node/index.d.ts +3 -0
  8. package/dist/board/export/node/index.js +13 -1
  9. package/dist/board/export/scene.d.ts +2 -2
  10. package/dist/board/export/scene.js +2 -2
  11. package/dist/board/model/asset-key.d.ts +16 -0
  12. package/dist/board/model/asset-key.js +92 -0
  13. package/dist/board/model/export-assets.d.ts +7 -3
  14. package/dist/board/model/export-assets.js +34 -13
  15. package/dist/board/model/index.d.ts +7 -5
  16. package/dist/board/model/index.js +9 -7
  17. package/dist/board/model/items/create.js +1 -1
  18. package/dist/board/model/media-sizes.d.ts +28 -0
  19. package/dist/board/model/media-sizes.js +64 -0
  20. package/dist/board/model/palette.d.ts +1 -0
  21. package/dist/board/model/palette.js +20 -130
  22. package/dist/board/model/text-metrics.d.ts +8 -3
  23. package/dist/board/model/text-metrics.js +13 -2
  24. package/dist/board/model/themes.d.ts +488 -0
  25. package/dist/board/model/themes.js +495 -0
  26. package/dist/board/render/card-tag.js +2 -2
  27. package/dist/board/render/index.d.ts +6 -4
  28. package/dist/board/render/index.js +5 -3
  29. package/dist/board/render/lod.d.ts +22 -0
  30. package/dist/board/render/lod.js +27 -0
  31. package/dist/board/render/media-interaction.js +1 -1
  32. package/dist/board/render/palette.js +2 -25
  33. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  34. package/dist/board/render/renderers/board-renderer-registry.d.ts +10 -0
  35. package/dist/board/render/renderers/custom-card-renderer.js +5 -5
  36. package/dist/board/render/renderers/far-plate.d.ts +11 -1
  37. package/dist/board/render/renderers/far-plate.js +27 -8
  38. package/dist/board/render/renderers/file-card-renderer.js +3 -5
  39. package/dist/board/render/renderers/image-card-renderer.js +32 -6
  40. package/dist/board/render/renderers/model-card-renderer.js +1 -1
  41. package/dist/board/render/renderers/shape-card-renderer.js +25 -24
  42. package/dist/board/render/renderers/task-card-renderer.js +7 -8
  43. package/dist/board/render/renderers/text-card-renderer.js +86 -41
  44. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  45. package/dist/board/render/style.d.ts +7 -2
  46. package/dist/board/render/style.js +5 -1
  47. package/dist/board/render/surface.d.ts +5 -0
  48. package/dist/board/render/surface.js +13 -3
  49. package/dist/board/render/text-fit.js +8 -12
  50. package/dist/board/render/text-measurement.d.ts +15 -1
  51. package/dist/board/render/text-measurement.js +113 -20
  52. package/dist/board/render/text-resolution.d.ts +1 -6
  53. package/dist/board/render/text-resolution.js +1 -10
  54. package/dist/board/render/theme.d.ts +15 -0
  55. package/dist/board/render/theme.js +15 -0
  56. package/dist/board/stage/assets.d.ts +15 -4
  57. package/dist/board/stage/assets.js +155 -89
  58. package/dist/board/stage/index.d.ts +5 -5
  59. package/dist/board/stage/index.js +4 -4
  60. package/dist/board/stage/lru.d.ts +13 -9
  61. package/dist/board/stage/lru.js +38 -20
  62. package/dist/board/stage/scene.d.ts +11 -1
  63. package/dist/board/stage/scene.js +213 -138
  64. package/dist/board/stage/stage.d.ts +12 -1
  65. package/dist/board/stage/stage.js +97 -24
  66. package/dist/board/stage/texture-pool.d.ts +8 -1
  67. package/dist/board/stage/texture-pool.js +37 -1
  68. package/dist/chunks/app-realtime.js +1 -1
  69. package/dist/chunks/{transport.js → http-error.js} +37 -1
  70. package/dist/chunks/http.js +30 -10
  71. package/dist/http.js +1 -1
  72. package/dist/index.js +139 -131
  73. package/dist/media.d.ts +3 -27
  74. package/dist/media.js +3 -82
  75. package/dist/protocol/dist/board-constants.js +3 -3
  76. package/dist/protocol/dist/board-layout.d.ts +17 -3
  77. package/dist/protocol/dist/board-layout.js +68 -11
  78. package/dist/protocol/dist/core/media.d.ts +35 -1
  79. package/dist/protocol/dist/core/media.js +123 -1
  80. package/dist/protocol/dist/index.d.ts +2 -2
  81. package/dist/websocket.js +951 -1
  82. package/package.json +3 -2
  83. package/dist/board/model/image-key.d.ts +0 -9
  84. package/dist/board/model/image-key.js +0 -33
  85. package/dist/chunks/websocket.js +0 -988
@@ -1,10 +1,12 @@
1
1
  import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { fitTextToLines } from "../text-fit.js";
2
3
  import { syncTextResolution } from "../text-resolution.js";
3
4
  import { positionShell } from "./base-card-renderer.js";
4
5
  import { drawFarPlate } from "./far-plate.js";
5
6
  import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
6
7
  //#region src/board/render/renderers/custom-card-renderer.ts
7
8
  const partsByContainer = /* @__PURE__ */ new WeakMap();
9
+ const LABEL_LINE = 16;
8
10
  function sync(container, item, context) {
9
11
  const parts = partsByContainer.get(container);
10
12
  if (!parts) return;
@@ -39,9 +41,8 @@ function sync(container, item, context) {
39
41
  alpha: .8
40
42
  });
41
43
  parts.label.visible = !ready || Boolean(error);
42
- parts.label.text = message;
43
44
  parts.label.style.fill = error ? context.colors.rose.stroke : context.palette.muted;
44
- parts.label.style.wordWrapWidth = Math.max(40, width - 16);
45
+ fitTextToLines(parts.label, message, Math.max(1, Math.floor((height - 16) / LABEL_LINE)), Math.max(40, width - 16));
45
46
  parts.label.position.set(width / 2, height / 2);
46
47
  }
47
48
  syncTextResolution(parts.label, context.textResolution);
@@ -59,9 +60,8 @@ const customCardRenderer = {
59
60
  style: {
60
61
  fontFamily: BOARD_MONO_FONT_STACK,
61
62
  fontSize: 12,
62
- align: "center",
63
- wordWrap: true,
64
- breakWords: true
63
+ lineHeight: LABEL_LINE,
64
+ align: "center"
65
65
  },
66
66
  resolution: context.textResolution,
67
67
  roundPixels: true
@@ -2,6 +2,12 @@ import { BoardFrame } from "../../../protocol/dist/board-layout.js";
2
2
  import "../../../protocol/dist/index.js";
3
3
  import { Graphics } from "pixi.js";
4
4
  //#region src/board/render/renderers/far-plate.d.ts
5
+ type LocalRect = {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ };
5
11
  type FarPlateStyle = {
6
12
  fill: number;
7
13
  fillAlpha?: number;
@@ -9,6 +15,10 @@ type FarPlateStyle = {
9
15
  accentAlpha?: number;
10
16
  };
11
17
  declare function drawFarPlate(graphics: Graphics, frame: BoardFrame, style: FarPlateStyle): void;
18
+ declare function drawFarBars(graphics: Graphics, frame: BoardFrame, bars: readonly LocalRect[], style: {
19
+ color: number;
20
+ alpha: number;
21
+ }): void;
12
22
  declare function farStrokeSamples<T>(points: readonly T[]): readonly T[];
13
23
  declare function drawFarStroke(graphics: Graphics, points: ReadonlyArray<{
14
24
  x: number;
@@ -19,4 +29,4 @@ declare function drawFarStroke(graphics: Graphics, points: ReadonlyArray<{
19
29
  alpha?: number;
20
30
  }): void;
21
31
  //#endregion
22
- export { FarPlateStyle, drawFarPlate, drawFarStroke, farStrokeSamples };
32
+ export { FarPlateStyle, drawFarBars, drawFarPlate, drawFarStroke, farStrokeSamples };
@@ -4,11 +4,11 @@ const FAR_STROKE_MAX_POINTS = 24;
4
4
  const ACCENT_RATIO = .14;
5
5
  const ACCENT_MIN = 2;
6
6
  const ACCENT_MAX = 10;
7
- function traceFrame(graphics, frame, inset = 0, heightOverride) {
8
- const x = frame.x + inset;
9
- const y = frame.y + inset;
10
- const width = Math.max(0, frame.width - inset * 2);
11
- const height = Math.max(0, (heightOverride ?? frame.height) - inset * 2);
7
+ function traceLocal(graphics, frame, rect) {
8
+ const x = frame.x + rect.x;
9
+ const y = frame.y + rect.y;
10
+ const width = Math.max(0, rect.width);
11
+ const height = Math.max(0, rect.height);
12
12
  if (!frame.rotation) {
13
13
  graphics.rect(x, y, width, height);
14
14
  return;
@@ -34,18 +34,37 @@ function traceFrame(graphics, frame, inset = 0, heightOverride) {
34
34
  }
35
35
  function drawFarPlate(graphics, frame, style) {
36
36
  if (frame.width <= 0 || frame.height <= 0) return;
37
- traceFrame(graphics, frame);
37
+ traceLocal(graphics, frame, {
38
+ x: 0,
39
+ y: 0,
40
+ width: frame.width,
41
+ height: frame.height
42
+ });
38
43
  graphics.fill({
39
44
  color: style.fill,
40
45
  alpha: style.fillAlpha ?? 1
41
46
  });
42
47
  if (style.accent === void 0) return;
43
- traceFrame(graphics, frame, 0, Math.min(ACCENT_MAX, Math.max(ACCENT_MIN, frame.height * ACCENT_RATIO)));
48
+ const band = Math.min(ACCENT_MAX, Math.max(ACCENT_MIN, frame.height * ACCENT_RATIO));
49
+ traceLocal(graphics, frame, {
50
+ x: 0,
51
+ y: 0,
52
+ width: frame.width,
53
+ height: band
54
+ });
44
55
  graphics.fill({
45
56
  color: style.accent,
46
57
  alpha: style.accentAlpha ?? .9
47
58
  });
48
59
  }
60
+ function drawFarBars(graphics, frame, bars, style) {
61
+ if (bars.length === 0) return;
62
+ for (const bar of bars) traceLocal(graphics, frame, bar);
63
+ graphics.fill({
64
+ color: style.color,
65
+ alpha: style.alpha
66
+ });
67
+ }
49
68
  function farStrokeSamples(points) {
50
69
  if (points.length <= FAR_STROKE_MAX_POINTS) return points;
51
70
  const step = Math.ceil(points.length / FAR_STROKE_MAX_POINTS);
@@ -70,4 +89,4 @@ function drawFarStroke(graphics, points, style) {
70
89
  });
71
90
  }
72
91
  //#endregion
73
- export { drawFarPlate, drawFarStroke, farStrokeSamples };
92
+ export { drawFarBars, drawFarPlate, drawFarStroke, farStrokeSamples };
@@ -1,8 +1,9 @@
1
1
  import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { fileCategory, fileStem } from "../../model/file-snapshot.js";
3
3
  import { fileCategoryAccent, fileMetaLine, filePreviewKind, fileTypeLabel } from "../../model/file-preview.js";
4
+ import { boardCardDetailed } from "../lod.js";
4
5
  import { fitTextToLines } from "../text-fit.js";
5
- import { boardCardDetailed, syncTextResolution } from "../text-resolution.js";
6
+ import { syncTextResolution } from "../text-resolution.js";
6
7
  import { createCardTag, syncCardTag, zoomCardTag } from "../card-tag.js";
7
8
  import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
8
9
  import { positionShell } from "./base-card-renderer.js";
@@ -146,6 +147,7 @@ function syncContent(parts, item, context) {
146
147
  context.palette.text,
147
148
  context.palette.muted,
148
149
  context.style.font,
150
+ context.themeKey,
149
151
  accent
150
152
  ].join("|");
151
153
  if (textSig === parts.textSig) return;
@@ -203,8 +205,6 @@ const fileCardRenderer = {
203
205
  fontFamily: BOARD_FONT_STACK,
204
206
  fontSize: TITLE_SIZE,
205
207
  fontWeight: "600",
206
- wordWrap: true,
207
- breakWords: true,
208
208
  lineHeight: TITLE_LINE
209
209
  },
210
210
  resolution,
@@ -217,8 +217,6 @@ const fileCardRenderer = {
217
217
  fontFamily: BOARD_FONT_STACK,
218
218
  fontSize: EXCERPT_SIZE,
219
219
  fontWeight: "400",
220
- wordWrap: true,
221
- breakWords: true,
222
220
  lineHeight: EXCERPT_LINE
223
221
  },
224
222
  resolution,
@@ -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
140
  fill: context.palette.hover,
115
- fillAlpha: .9
141
+ fillAlpha: FAR_ALPHA
116
142
  });
117
143
  },
118
144
  destroy: (container) => {
@@ -1,7 +1,7 @@
1
1
  import { isBoardModelItem } from "../../model/media.js";
2
2
  import { fileStem } from "../../model/file-snapshot.js";
3
3
  import { fileMetaLine } from "../../model/file-preview.js";
4
- import { boardCardDetailed } from "../text-resolution.js";
4
+ import { boardCardDetailed } from "../lod.js";
5
5
  import { createCardTag, syncCardTag, traceCube, zoomCardTag } from "../card-tag.js";
6
6
  import { cardElevation, containRect, drawCardSurface } from "../surface.js";
7
7
  import { positionShell } from "./base-card-renderer.js";
@@ -1,7 +1,8 @@
1
- import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
1
  import { worldPoint } from "../../model/geometry.js";
2
+ import { layoutShapeLabel } from "../../model/text-metrics.js";
3
3
  import { itemAlpha, itemColor } from "../palette.js";
4
- import { syncTextResolution, syncTextWrapWidth } from "../text-resolution.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,7 +1,8 @@
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";
@@ -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";
@@ -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,5 +1,5 @@
1
1
  import { getMediaResourceTitle } from "../../model/media.js";
2
- import { boardCardDetailed } from "../text-resolution.js";
2
+ import { boardCardDetailed } from "../lod.js";
3
3
  import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
4
4
  import { cardElevation, containRect, drawCardSurface, fillCardPicture } from "../surface.js";
5
5
  import { positionShell } from "./base-card-renderer.js";
@@ -122,6 +122,7 @@ 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
128
  fill: context.palette.surface,
@@ -24,6 +24,11 @@ type BoardSelectionStyle = {
24
24
  handleSize: number;
25
25
  };
26
26
  declare function defaultBoardRenderStyle(mode: "dark" | "light"): BoardRenderStyle;
27
- declare function resolveBoardRenderStyle(mode: "dark" | "light", style?: Partial<BoardRenderStyle>): BoardRenderStyle;
27
+ /** Theme overrides of the default style; a shadow without positive alpha is none. */
28
+ type BoardRenderStyleInput = Partial<Omit<BoardRenderStyle, "shadow" | "selection">> & {
29
+ shadow?: Partial<BoardShadowStyle> | null;
30
+ selection?: Partial<BoardSelectionStyle>;
31
+ };
32
+ declare function resolveBoardRenderStyle(mode: "dark" | "light", style?: BoardRenderStyleInput): BoardRenderStyle;
28
33
  //#endregion
29
- export { BoardRenderStyle, BoardSelectionStyle, BoardShadowStyle, defaultBoardRenderStyle, resolveBoardRenderStyle };
34
+ export { BoardRenderStyle, BoardRenderStyleInput, BoardSelectionStyle, BoardShadowStyle, defaultBoardRenderStyle, resolveBoardRenderStyle };
@@ -40,6 +40,10 @@ function defaultBoardRenderStyle(mode) {
40
40
  function resolveBoardRenderStyle(mode, style) {
41
41
  const base = defaultBoardRenderStyle(mode);
42
42
  if (!style) return base;
43
+ const shadow = style.shadow === null ? null : {
44
+ ...SHADOW[mode],
45
+ ...style.shadow
46
+ };
43
47
  return {
44
48
  ...base,
45
49
  ...style,
@@ -47,7 +51,7 @@ function resolveBoardRenderStyle(mode, style) {
47
51
  ...base.selection,
48
52
  ...style.selection
49
53
  },
50
- shadow: style.shadow === void 0 ? base.shadow : style.shadow
54
+ shadow: shadow && shadow.alpha > 0 ? shadow : null
51
55
  };
52
56
  }
53
57
  //#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[];