@neta-art/cohub 11.1.0 → 11.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 (67) hide show
  1. package/dist/board/editor/context.d.ts +1 -0
  2. package/dist/board/editor/context.js +1 -0
  3. package/dist/board/editor/editor.d.ts +1 -0
  4. package/dist/board/editor/editor.js +7 -0
  5. package/dist/board/editor/interaction.d.ts +4 -0
  6. package/dist/board/editor/interaction.js +16 -7
  7. package/dist/board/editor/selection-transform.d.ts +1 -1
  8. package/dist/board/editor/selection-transform.js +13 -2
  9. package/dist/board/export/index.d.ts +2 -0
  10. package/dist/board/export/index.js +1 -0
  11. package/dist/board/export/scene.d.ts +2 -0
  12. package/dist/board/export/scene.js +4 -0
  13. package/dist/board/model/file-preview.d.ts +0 -1
  14. package/dist/board/model/file-preview.js +10 -1
  15. package/dist/board/render/audio-waveform.d.ts +1 -1
  16. package/dist/board/render/audio-waveform.js +8 -5
  17. package/dist/board/render/card-tag.d.ts +24 -0
  18. package/dist/board/render/card-tag.js +205 -0
  19. package/dist/board/render/index.d.ts +7 -4
  20. package/dist/board/render/index.js +7 -4
  21. package/dist/board/render/media-interaction.d.ts +2 -1
  22. package/dist/board/render/media-interaction.js +14 -6
  23. package/dist/board/render/palette.js +2 -2
  24. package/dist/board/render/renderers/arrow-card-renderer.js +20 -16
  25. package/dist/board/render/renderers/audio-card-renderer.d.ts +8 -1
  26. package/dist/board/render/renderers/audio-card-renderer.js +64 -60
  27. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -26
  28. package/dist/board/render/renderers/base-card-renderer.js +6 -92
  29. package/dist/board/render/renderers/board-renderer-registry.d.ts +8 -0
  30. package/dist/board/render/renderers/board-renderer-registry.js +3 -3
  31. package/dist/board/render/renderers/custom-card-renderer.js +4 -7
  32. package/dist/board/render/renderers/effect-card-renderer.js +0 -5
  33. package/dist/board/render/renderers/file-card-renderer.d.ts +1 -11
  34. package/dist/board/render/renderers/file-card-renderer.js +43 -105
  35. package/dist/board/render/renderers/frame-card-renderer.js +14 -15
  36. package/dist/board/render/renderers/image-card-renderer.js +62 -67
  37. package/dist/board/render/renderers/media-key.d.ts +6 -0
  38. package/dist/board/render/renderers/media-key.js +39 -0
  39. package/dist/board/render/renderers/shape-card-renderer.js +6 -12
  40. package/dist/board/render/renderers/task-card-renderer.d.ts +1 -9
  41. package/dist/board/render/renderers/task-card-renderer.js +82 -56
  42. package/dist/board/render/renderers/unknown-card-renderer.js +17 -16
  43. package/dist/board/render/renderers/video-card-renderer.js +73 -80
  44. package/dist/board/render/stroke.d.ts +1 -1
  45. package/dist/board/render/stroke.js +5 -4
  46. package/dist/board/render/style.d.ts +29 -0
  47. package/dist/board/render/style.js +54 -0
  48. package/dist/board/render/surface.d.ts +34 -0
  49. package/dist/board/render/surface.js +132 -0
  50. package/dist/board/render/text-fit.d.ts +8 -0
  51. package/dist/board/render/text-fit.js +60 -0
  52. package/dist/board/render/text-resolution.d.ts +2 -1
  53. package/dist/board/render/text-resolution.js +4 -1
  54. package/dist/board/stage/index.d.ts +3 -2
  55. package/dist/board/stage/index.js +2 -1
  56. package/dist/board/stage/overlay.d.ts +41 -0
  57. package/dist/board/stage/overlay.js +182 -0
  58. package/dist/board/stage/scene.d.ts +2 -43
  59. package/dist/board/stage/scene.js +23 -131
  60. package/dist/board/stage/stage.d.ts +2 -0
  61. package/dist/board/stage/stage.js +100 -33
  62. package/dist/chunks/environment.d.ts +997 -3
  63. package/dist/chunks/http.d.ts +48 -1
  64. package/dist/chunks/http.js +8 -0
  65. package/dist/http.d.ts +2 -2
  66. package/dist/index.d.ts +2 -2
  67. package/package.json +2 -1
@@ -40,6 +40,7 @@ type BoardEditorState = {
40
40
  width: number;
41
41
  height: number;
42
42
  };
43
+ controlOutset: number;
43
44
  cameraPolicy: BoardCameraPolicy;
44
45
  tool: BoardToolId;
45
46
  toolStyles: BoardToolStyleMap;
@@ -8,6 +8,7 @@ const STATE_CHANNELS = {
8
8
  selection: "selection",
9
9
  camera: "camera",
10
10
  surfaceSize: "camera",
11
+ controlOutset: "selection",
11
12
  cameraPolicy: "camera",
12
13
  tool: "tool",
13
14
  toolStyles: "tool",
@@ -126,6 +126,7 @@ declare function createBoardEditor(options: BoardEditorOptions): {
126
126
  width: number;
127
127
  height: number;
128
128
  };
129
+ controlOutset: number;
129
130
  loadDocument: (next: BoardDocument, key?: string) => void;
130
131
  undo: () => void;
131
132
  redo: () => void;
@@ -53,6 +53,7 @@ function createBoardEditor(options) {
53
53
  width: 0,
54
54
  height: 0
55
55
  },
56
+ controlOutset: 0,
56
57
  cameraPolicy: preferences?.cameraPolicy ?? "follow",
57
58
  tool: options.initialTool ?? "select",
58
59
  toolStyles: preferences?.toolStyles ?? createBoardToolStyles(),
@@ -339,6 +340,12 @@ function createBoardEditor(options) {
339
340
  set surfaceSize(value) {
340
341
  state.surfaceSize = value;
341
342
  },
343
+ get controlOutset() {
344
+ return read("selection", () => state.controlOutset);
345
+ },
346
+ set controlOutset(value) {
347
+ if (value !== state.controlOutset) state.controlOutset = value;
348
+ },
342
349
  loadDocument: remote.loadDocument,
343
350
  undo: document.undo,
344
351
  redo: document.redo,
@@ -28,6 +28,10 @@ type BoardInteraction = {
28
28
  single: BoardFrame | null;
29
29
  bounds: Rect;
30
30
  origin: Map<string, BoardFrame>;
31
+ grab: {
32
+ x: number;
33
+ y: number;
34
+ };
31
35
  moved: boolean;
32
36
  } | {
33
37
  type: "rotating";
@@ -1,4 +1,4 @@
1
- import { clampZoom, normalizeRotation, panBy, rectCenter, rectsIntersect, resizeFrame, rotateFrames, scaleFrames, screenPoint, selectionBounds, worldPoint, zoomAround } from "../model/geometry.js";
1
+ import { clampZoom, frameHandlePosition, normalizeRotation, panBy, rectCenter, rectsIntersect, resizeFrame, rotateFrames, scaleFrames, screenPoint, selectionBounds, worldPoint, zoomAround } from "../model/geometry.js";
2
2
  import { resolveSceneArrow } from "../model/arrow-geometry.js";
3
3
  import { appendBoardDrawSamples } from "../model/draw-input.js";
4
4
  import { createBoardItemId } from "../model/items/id.js";
@@ -51,8 +51,9 @@ function createInteractionModule(ctx) {
51
51
  state.hoverPoint,
52
52
  state.hoverPointerType,
53
53
  state.camera.zoom,
54
- ctx.selection.transform()
55
- ], (interaction, tool, hoverPoint, pointerType, zoom, transform) => interaction.type === "idle" && !pinch && tool === "select" && hoverPoint ? selectionTransformControlAt(transform, hoverPoint, zoom, pointerType) : null);
54
+ ctx.selection.transform(),
55
+ state.controlOutset
56
+ ], (interaction, tool, hoverPoint, pointerType, zoom, transform, outset) => interaction.type === "idle" && !pinch && tool === "select" && hoverPoint ? selectionTransformControlAt(transform, hoverPoint, zoom, pointerType, outset) : null);
56
57
  function gridSnapSize() {
57
58
  const grid = state.base.board.grid;
58
59
  return grid?.visible === true ? grid.size ?? 0 : 0;
@@ -244,7 +245,8 @@ function createInteractionModule(ctx) {
244
245
  return;
245
246
  }
246
247
  const bounds = ctx.selection.bounds();
247
- const control = selectionTransformControlAt(ctx.selection.transform(), event.world, camera.zoom, event.pointerType);
248
+ const transform = ctx.selection.transform();
249
+ const control = selectionTransformControlAt(transform, event.world, camera.zoom, event.pointerType, state.controlOutset);
248
250
  if (control?.kind === "rotate" && bounds) {
249
251
  const pivot = rectCenter(bounds);
250
252
  state.interaction = {
@@ -257,15 +259,21 @@ function createInteractionModule(ctx) {
257
259
  };
258
260
  return;
259
261
  }
260
- if (control?.kind === "resize" && bounds) {
262
+ if (control?.kind === "resize" && bounds && transform) {
261
263
  const origin = ctx.selection.framesFor(ctx.selection.movable());
262
264
  const single = origin.size === 1 ? { ...[...origin.values()][0] } : null;
265
+ const edge = frameHandlePosition(transform.frame, control.handle);
266
+ const grab = {
267
+ x: edge.x - event.world.x,
268
+ y: edge.y - event.world.y
269
+ };
263
270
  state.interaction = {
264
271
  type: "resizing",
265
272
  handle: control.handle,
266
273
  single,
267
274
  bounds,
268
275
  origin,
276
+ grab,
269
277
  moved: false
270
278
  };
271
279
  return;
@@ -349,13 +357,14 @@ function createInteractionModule(ctx) {
349
357
  moved: true
350
358
  };
351
359
  const frames = /* @__PURE__ */ new Map();
360
+ const point = worldPoint(event.world.x + gesture.grab.x, event.world.y + gesture.grab.y);
352
361
  if (gesture.single) {
353
362
  const id = [...gesture.origin.keys()][0];
354
363
  const item = state.scene.get(id);
355
364
  const keepAspect = item ? registry.capabilities(item).aspectLocked || event.shiftKey : event.shiftKey;
356
- frames.set(id, resizeFrame(gesture.single, gesture.handle, event.world, void 0, keepAspect));
365
+ frames.set(id, resizeFrame(gesture.single, gesture.handle, point, void 0, keepAspect));
357
366
  } else {
358
- const scaled = scaleFrames([...gesture.origin.values()], gesture.bounds, gesture.handle, event.world);
367
+ const scaled = scaleFrames([...gesture.origin.values()], gesture.bounds, gesture.handle, point);
359
368
  [...gesture.origin.keys()].forEach((id, index) => {
360
369
  const frame = scaled[index];
361
370
  if (frame) frames.set(id, frame);
@@ -19,6 +19,6 @@ type BoardSelectionTransform = {
19
19
  canRotate: boolean;
20
20
  };
21
21
  declare function resolveSelectionTransform(registry: BoardRegistry, items: BoardSceneItem[], bounds: Rect | null): BoardSelectionTransform | null;
22
- declare function selectionTransformControlAt(transform: BoardSelectionTransform | null, point: WorldPoint, zoom: number, pointerType: string): BoardTransformControl | null;
22
+ declare function selectionTransformControlAt(selection: BoardSelectionTransform | null, point: WorldPoint, zoom: number, pointerType: string, outset?: number): BoardTransformControl | null;
23
23
  //#endregion
24
24
  export { BoardSelectionTransform, BoardTransformControl, resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt };
@@ -51,10 +51,21 @@ function controlHitRadius(pointerType) {
51
51
  if (pointerType === "pen") return 12;
52
52
  return 8;
53
53
  }
54
- function selectionTransformControlAt(transform, point, zoom, pointerType) {
55
- if (!transform) return null;
54
+ function selectionTransformControlAt(selection, point, zoom, pointerType, outset = 0) {
55
+ if (!selection) return null;
56
56
  const screenRadius = controlHitRadius(pointerType);
57
57
  const radius = screenRadius / Math.max(zoom, 1e-4);
58
+ const grow = outset / Math.max(zoom, 1e-4);
59
+ const transform = grow > 0 ? {
60
+ ...selection,
61
+ frame: {
62
+ ...selection.frame,
63
+ x: selection.frame.x - grow,
64
+ y: selection.frame.y - grow,
65
+ width: selection.frame.width + grow * 2,
66
+ height: selection.frame.height + grow * 2
67
+ }
68
+ } : selection;
58
69
  if (transform.resizeMode !== "none") for (const handle of CORNER_RESIZE_HANDLES) {
59
70
  const position = frameHandlePosition(transform.frame, handle);
60
71
  if (Math.hypot(position.x - point.x, position.y - point.y) <= radius) return {
@@ -6,6 +6,7 @@ import { BoardEvaluation } from "../model/animation.js";
6
6
  import { BoardAnimationSampler } from "../model/sampling.js";
7
7
  import { BoardExportPlan, BoardExportPlanInput, BoardExportRegion } from "../model/export-plan.js";
8
8
  import { BoardShapeColors } from "../model/palette.js";
9
+ import { BoardRenderStyle } from "../render/style.js";
9
10
  import { BoardCardRendererResolver, BoardRenderPalette, BoardScriptHost } from "../render/renderers/board-renderer-registry.js";
10
11
  import "../render/index.js";
11
12
  import { BoardExportScene, BoardExportSceneInput, createBoardExportScene } from "./scene.js";
@@ -29,6 +30,7 @@ type BoardExportOptions = {
29
30
  background?: BoardExportBackground;
30
31
  palette?: Partial<BoardRenderPalette>;
31
32
  colors?: BoardShapeColors;
33
+ style?: Partial<BoardRenderStyle>;
32
34
  textures?: Map<string, Texture>;
33
35
  scripts?: BoardScriptHost;
34
36
  assetKey?: (item: BoardSceneItem) => string | null;
@@ -76,6 +76,7 @@ function renderBoardExport(renderer, input, options = {}) {
76
76
  colorScheme,
77
77
  palette,
78
78
  colors: options.colors,
79
+ style: options.style,
79
80
  textures: options.textures,
80
81
  scripts: options.scripts,
81
82
  assetKey: options.assetKey,
@@ -3,6 +3,7 @@ import "../../protocol/dist/index.js";
3
3
  import { Rect } from "../model/geometry.js";
4
4
  import { BoardScene, BoardSceneItem } from "../model/scene.js";
5
5
  import { BoardShapeColors } from "../model/palette.js";
6
+ import { BoardRenderStyle } from "../render/style.js";
6
7
  import { BoardCardRendererResolver, BoardRenderContext, BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
7
8
  import "../render/index.js";
8
9
  import { Container, Texture } from "pixi.js";
@@ -18,6 +19,7 @@ type BoardExportSceneInput = {
18
19
  colorScheme: "dark" | "light";
19
20
  palette?: Partial<BoardRenderPalette>;
20
21
  colors?: BoardShapeColors;
22
+ style?: Partial<BoardRenderStyle>;
21
23
  textures?: Map<string, Texture>;
22
24
  scripts?: BoardRenderContext["scripts"];
23
25
  assetKey?: (item: BoardSceneItem) => string | null;
@@ -3,6 +3,7 @@ import { imageAssetKey } from "../model/image-key.js";
3
3
  import { defaultBoardPalette } from "../render/palette.js";
4
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
+ import { resolveBoardRenderStyle } from "../render/style.js";
6
7
  import { Container, Graphics, Sprite, TilingSprite } from "pixi.js";
7
8
  //#region src/board/export/scene.ts
8
9
  function buildContext(input) {
@@ -17,12 +18,15 @@ function buildContext(input) {
17
18
  selectedIds: /* @__PURE__ */ new Set(),
18
19
  hoveredId: null,
19
20
  resizingIds: /* @__PURE__ */ new Set(),
21
+ liftedIds: /* @__PURE__ */ new Set(),
20
22
  palette: {
21
23
  ...defaultBoardPalette(input.colorScheme),
22
24
  ...input.palette
23
25
  },
24
26
  colors: input.colors ?? buildFallbackShapeColors(input.colorScheme),
25
27
  colorScheme: input.colorScheme,
28
+ style: resolveBoardRenderStyle(input.colorScheme, input.style),
29
+ themeKey: "export",
26
30
  rendererType: "canvas",
27
31
  zoom: input.scale,
28
32
  reducedMotion: true,
@@ -7,7 +7,6 @@ type FilePreviewKind = "cover" | "text" | "blank";
7
7
  * sync with them.
8
8
  */
9
9
  declare function filePreviewKind(snapshot: BoardFileSnapshotFacts | undefined): FilePreviewKind;
10
- /** Short uppercase type label for a file card (`MD`, `JSON`, `FILE`). */
11
10
  declare function fileTypeLabel(path: string): string;
12
11
  /** Human-readable byte size. Kept as a public helper for custom card renderers. */
13
12
  declare function formatFileSize(bytes: number | undefined): string;
@@ -11,13 +11,22 @@ function filePreviewKind(snapshot) {
11
11
  if (snapshot.excerpt) return "text";
12
12
  return "blank";
13
13
  }
14
- /** Short uppercase type label for a file card (`MD`, `JSON`, `FILE`). */
15
14
  function fileTypeLabel(path) {
15
+ if (isWebPageUrl(path)) return "LINK";
16
16
  const name = fileBaseName(path);
17
17
  const dot = name.lastIndexOf(".");
18
18
  if (dot <= 0) return (name.replace(/^\./, "") || "file").toUpperCase();
19
19
  return name.slice(dot + 1).toUpperCase();
20
20
  }
21
+ function isWebPageUrl(path) {
22
+ if (!/^https?:\/\//i.test(path)) return false;
23
+ try {
24
+ const last = new URL(path).pathname.split("/").filter(Boolean).pop() ?? "";
25
+ return !/\.[a-z\d]{1,8}$/i.test(last);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
21
30
  /** Human-readable byte size. Kept as a public helper for custom card renderers. */
22
31
  function formatFileSize(bytes) {
23
32
  if (bytes === void 0 || !Number.isFinite(bytes) || bytes < 0) return "";
@@ -7,6 +7,6 @@ declare function drawAudioWaveform(graphics: Graphics, seed: string, rect: {
7
7
  y: number;
8
8
  width: number;
9
9
  height: number;
10
- }, color: number, alpha?: number): void;
10
+ }, color: number, alpha?: number, square?: boolean): void;
11
11
  //#endregion
12
12
  export { audioWaveformBars, drawAudioWaveform };
@@ -17,7 +17,7 @@ function audioWaveformBars(seed, count) {
17
17
  }
18
18
  return bars;
19
19
  }
20
- function drawAudioWaveform(graphics, seed, rect, color, alpha = .72) {
20
+ function drawAudioWaveform(graphics, seed, rect, color, alpha = .72, square = false) {
21
21
  const count = Math.max(8, Math.min(DEFAULT_BARS, Math.floor(rect.width / 4)));
22
22
  const step = rect.width / count;
23
23
  const barWidth = Math.max(1.5, step - BAR_GAP);
@@ -26,11 +26,14 @@ function drawAudioWaveform(graphics, seed, rect, color, alpha = .72) {
26
26
  const bars = audioWaveformBars(seed, count);
27
27
  for (let index = 0; index < count; index += 1) {
28
28
  const height = Math.max(2, (bars[index] ?? .4) * maxHeight);
29
- graphics.roundRect(rect.x + index * step, centerY - height / 2, barWidth, height, barWidth / 2).fill({
30
- color,
31
- alpha
32
- });
29
+ const x = rect.x + index * step;
30
+ if (square) graphics.rect(x, centerY - height / 2, barWidth, height);
31
+ else graphics.roundRect(x, centerY - height / 2, barWidth, height, barWidth / 2);
33
32
  }
33
+ graphics.fill({
34
+ color,
35
+ alpha
36
+ });
34
37
  }
35
38
  //#endregion
36
39
  export { audioWaveformBars, drawAudioWaveform };
@@ -0,0 +1,24 @@
1
+ import { BoardRenderContext } from "./renderers/board-renderer-registry.js";
2
+ import { Container, Graphics, Text } from "pixi.js";
3
+ //#region src/board/render/card-tag.d.ts
4
+ type CardGlyph = "image" | "video" | "audio" | "file" | "task";
5
+ declare const CARD_TAG_SIZE = 11;
6
+ declare function formatBoardDuration(ms: number): string;
7
+ declare function drawCardGlyph(graphics: Graphics, glyph: CardGlyph, color: number, pixel: boolean): void;
8
+ type CardTag = {
9
+ root: Container;
10
+ icon: Graphics;
11
+ text: Text | null;
12
+ sig: string;
13
+ resolution: number;
14
+ width: number;
15
+ };
16
+ declare function createCardTag(): CardTag;
17
+ declare function zoomCardTag(tag: CardTag, zoom: number): void;
18
+ declare function syncCardTag(tag: CardTag, input: {
19
+ glyph: CardGlyph;
20
+ text: string;
21
+ width: number;
22
+ }, context: BoardRenderContext): void;
23
+ //#endregion
24
+ export { CARD_TAG_SIZE, CardGlyph, CardTag, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, zoomCardTag };
@@ -0,0 +1,205 @@
1
+ import { fitTextToWidth } from "./text-fit.js";
2
+ import { syncTextResolution, textResolutionForZoom } from "./text-resolution.js";
3
+ import { Container, Graphics, Text } from "pixi.js";
4
+ //#region src/board/render/card-tag.ts
5
+ const CARD_TAG_SIZE = 11;
6
+ const ICON = 12;
7
+ const GAP = 5;
8
+ const MAX_GROWTH = 1.25;
9
+ const MIN_READABLE = 7;
10
+ const SECOND = 1e3;
11
+ function formatBoardDuration(ms) {
12
+ const total = Math.max(0, Math.floor(ms / SECOND));
13
+ const hours = Math.floor(total / 3600);
14
+ const minutes = Math.floor(total % 3600 / 60);
15
+ const seconds = String(total % 60).padStart(2, "0");
16
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
17
+ }
18
+ const PIXEL_GLYPHS = {
19
+ image: [
20
+ "######",
21
+ "#....#",
22
+ "#.#..#",
23
+ "#.##.#",
24
+ "######",
25
+ "......"
26
+ ],
27
+ video: [
28
+ "######",
29
+ "#.#..#",
30
+ "#.##.#",
31
+ "#.#..#",
32
+ "######",
33
+ "......"
34
+ ],
35
+ audio: [
36
+ "...#..",
37
+ "...##.",
38
+ "...#.#",
39
+ "...#..",
40
+ ".###..",
41
+ ".###.."
42
+ ],
43
+ file: [
44
+ "####..",
45
+ "#..##.",
46
+ "#...##",
47
+ "#....#",
48
+ "#....#",
49
+ "######"
50
+ ],
51
+ task: [
52
+ "..#...",
53
+ ".###..",
54
+ "#####.",
55
+ ".###..",
56
+ "..#...",
57
+ "......"
58
+ ]
59
+ };
60
+ function drawLineGlyph(graphics, glyph, color) {
61
+ const line = {
62
+ color,
63
+ width: 1.25,
64
+ alpha: 1,
65
+ cap: "round",
66
+ join: "round"
67
+ };
68
+ switch (glyph) {
69
+ case "image":
70
+ graphics.roundRect(1, 2, 10, 8, 1.5).stroke(line);
71
+ graphics.moveTo(2.5, 9).lineTo(5, 6).lineTo(7, 8).lineTo(8.5, 6.5).lineTo(10, 8).stroke(line);
72
+ return;
73
+ case "video":
74
+ graphics.roundRect(1, 2.5, 10, 7, 1.5).stroke(line);
75
+ graphics.poly([
76
+ 5,
77
+ 4.5,
78
+ 5,
79
+ 7.5,
80
+ 7.5,
81
+ 6
82
+ ]).fill({ color });
83
+ return;
84
+ case "audio":
85
+ for (const [x, height] of [
86
+ [2.5, 3],
87
+ [5, 7],
88
+ [7.5, 9],
89
+ [10, 5]
90
+ ]) graphics.moveTo(x, 6 - height / 2).lineTo(x, 6 + height / 2);
91
+ graphics.stroke(line);
92
+ return;
93
+ case "file":
94
+ graphics.poly([
95
+ 2.5,
96
+ 1,
97
+ 7.5,
98
+ 1,
99
+ 9.5,
100
+ 3,
101
+ 9.5,
102
+ 11,
103
+ 2.5,
104
+ 11
105
+ ]).stroke(line);
106
+ graphics.moveTo(7.5, 1).lineTo(7.5, 3).lineTo(9.5, 3).stroke(line);
107
+ return;
108
+ case "task":
109
+ graphics.poly([
110
+ 6,
111
+ 1,
112
+ 7.3,
113
+ 4.7,
114
+ 11,
115
+ 6,
116
+ 7.3,
117
+ 7.3,
118
+ 6,
119
+ 11,
120
+ 4.7,
121
+ 7.3,
122
+ 1,
123
+ 6,
124
+ 4.7,
125
+ 4.7
126
+ ]).fill({ color });
127
+ return;
128
+ }
129
+ }
130
+ function drawCardGlyph(graphics, glyph, color, pixel) {
131
+ if (!pixel) {
132
+ drawLineGlyph(graphics, glyph, color);
133
+ return;
134
+ }
135
+ PIXEL_GLYPHS[glyph].forEach((row, y) => {
136
+ for (let x = 0; x < row.length; x += 1) if (row[x] === "#") graphics.rect(x * 2, y * 2, 2, 2);
137
+ });
138
+ graphics.fill({ color });
139
+ }
140
+ function createCardTag() {
141
+ const root = new Container({ label: "card-tag" });
142
+ const icon = new Graphics();
143
+ root.addChild(icon);
144
+ root.visible = false;
145
+ return {
146
+ root,
147
+ icon,
148
+ text: null,
149
+ sig: "",
150
+ resolution: 0,
151
+ width: 0
152
+ };
153
+ }
154
+ function zoomCardTag(tag, zoom) {
155
+ if (!tag.text || !tag.sig) return;
156
+ const scale = Math.min(MAX_GROWTH, Math.max(1, 1 / Math.max(zoom, .01)));
157
+ const span = 17 + tag.text.width;
158
+ tag.root.scale.set(scale);
159
+ tag.root.position.set(0, -18 * scale);
160
+ tag.root.visible = 11 * scale * zoom >= MIN_READABLE && span * scale <= tag.width + .5;
161
+ }
162
+ function syncCardTag(tag, input, context) {
163
+ tag.width = input.width;
164
+ if (!input.text || input.width <= 24) {
165
+ tag.sig = "";
166
+ tag.root.visible = false;
167
+ return;
168
+ }
169
+ if (!tag.text) {
170
+ tag.resolution = textResolutionForZoom(context.zoom);
171
+ tag.text = new Text({
172
+ text: "",
173
+ style: {
174
+ fontFamily: context.style.font,
175
+ fontSize: 11,
176
+ fontWeight: "500"
177
+ },
178
+ resolution: tag.resolution,
179
+ roundPixels: true
180
+ });
181
+ tag.root.addChild(tag.text);
182
+ }
183
+ syncTextResolution(tag.text, tag, context.zoom);
184
+ const sig = [
185
+ input.glyph,
186
+ input.text,
187
+ input.width,
188
+ context.themeKey
189
+ ].join("|");
190
+ if (sig === tag.sig) {
191
+ zoomCardTag(tag, context.zoom);
192
+ return;
193
+ }
194
+ tag.sig = sig;
195
+ const color = context.palette.muted;
196
+ const pixel = context.style.icons === "pixel";
197
+ drawCardGlyph(tag.icon.clear(), input.glyph, color, pixel);
198
+ tag.text.style.fill = color;
199
+ tag.text.style.fontFamily = context.style.font;
200
+ fitTextToWidth(tag.text, input.text, Math.max(1, input.width - ICON - GAP));
201
+ tag.text.position.set(17, (ICON - tag.text.height) / 2);
202
+ zoomCardTag(tag, context.zoom);
203
+ }
204
+ //#endregion
205
+ export { CARD_TAG_SIZE, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, zoomCardTag };
@@ -1,17 +1,20 @@
1
+ import { BoardRenderStyle, BoardSelectionStyle, BoardShadowStyle, defaultBoardRenderStyle, resolveBoardRenderStyle } from "./style.js";
1
2
  import { BoardCardRenderer, BoardCardRendererResolver, BoardRenderContext, BoardRenderLabels, BoardRenderPalette, BoardScriptHost, DEFAULT_BOARD_RENDER_LABELS, createBoardCardRendererResolver, getBoardCardRenderer } from "./renderers/board-renderer-registry.js";
2
3
  import { createBoardRenderSampler } from "./sampling.js";
3
4
  import { BoardRenderDiagnostic, inspectBoardScene, measureBoardText } from "./diagnostics.js";
4
5
  import { BoardBackgroundContext, createBoardBackground, updateBoardBackground } from "./board-background.js";
6
+ import { CARD_TAG_SIZE, CardGlyph, CardTag, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, zoomCardTag } from "./card-tag.js";
5
7
  import { ClipGroup, clippingAncestor, isClippingFrame, syncClipGroup, traceFrameOutline } from "./clip.js";
6
8
  import { BoardCssPaint, parseBoardCssColor, parseBoardCssPaint } from "./css-color.js";
7
- import { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible } from "./media-interaction.js";
9
+ import { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter } from "./media-interaction.js";
8
10
  import { defaultBoardPalette, itemAlpha, itemColor } from "./palette.js";
9
11
  import { BoardScriptHostOptions, BrowserBoardScriptHost, createBoardScriptHost } from "./script-host.js";
10
12
  import { prepareBrowserBoardScripts } from "./script-export.js";
11
13
  import { dashPattern, endAngle, roundRectPoints, traceArrowhead, tracePolyline, trimPolyline } from "./stroke.js";
14
+ import { CardElevation, CardRect, CardSurface, PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.js";
12
15
  import { BOARD_EFFECT_MAX_PARTICLES } from "./renderers/effect-card-renderer.js";
13
- import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
16
+ import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWidth } from "./text-fit.js";
14
17
  import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
15
- import { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
18
+ import { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
16
19
  import { VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
17
- export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, BoardBackgroundContext, BoardCardRenderer, BoardCardRendererResolver, BoardCssPaint, BoardMediaAction, BoardRenderContext, BoardRenderDiagnostic, BoardRenderLabels, BoardRenderPalette, BoardScriptHost, BoardScriptHostOptions, BrowserBoardScriptHost, ClipGroup, DEFAULT_BOARD_RENDER_LABELS, TASK_CARD_FULL_DETAIL_ZOOM, VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, boardMediaActionAt, clippingAncestor, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, dashPattern, defaultBoardPalette, endAngle, ensureBoardTextMeasurement, getBoardCardRenderer, getBoardResolution, inspectBoardScene, installBoardTextMeasurement, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, roundRectPoints, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceFrameOutline, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize };
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, 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, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
@@ -4,14 +4,17 @@ import { createBoardRenderSampler } from "./sampling.js";
4
4
  import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
5
5
  import { inspectBoardScene, measureBoardText } from "./diagnostics.js";
6
6
  import { createBoardBackground, updateBoardBackground } from "./board-background.js";
7
+ import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWidth } from "./text-fit.js";
8
+ import { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
9
+ import { CARD_TAG_SIZE, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, zoomCardTag } from "./card-tag.js";
7
10
  import { clippingAncestor, isClippingFrame, syncClipGroup, traceFrameOutline } from "./clip.js";
8
- import { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
9
- import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
10
- import { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible } from "./media-interaction.js";
11
11
  import { dashPattern, endAngle, roundRectPoints, traceArrowhead, tracePolyline, trimPolyline } from "./stroke.js";
12
+ import { PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.js";
13
+ import { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter } from "./media-interaction.js";
12
14
  import { BOARD_EFFECT_MAX_PARTICLES } from "./renderers/effect-card-renderer.js";
13
15
  import { DEFAULT_BOARD_RENDER_LABELS, createBoardCardRendererResolver, getBoardCardRenderer } from "./renderers/board-renderer-registry.js";
14
16
  import { createBoardScriptHost } from "./script-host.js";
15
17
  import { prepareBrowserBoardScripts } from "./script-export.js";
18
+ import { defaultBoardRenderStyle, resolveBoardRenderStyle } from "./style.js";
16
19
  import { VIDEO_THUMBNAIL_MAX_EDGE, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
17
- export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, DEFAULT_BOARD_RENDER_LABELS, TASK_CARD_FULL_DETAIL_ZOOM, VIDEO_THUMBNAIL_MAX_EDGE, boardMediaActionAt, clippingAncestor, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, dashPattern, defaultBoardPalette, endAngle, ensureBoardTextMeasurement, getBoardCardRenderer, getBoardResolution, inspectBoardScene, installBoardTextMeasurement, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, roundRectPoints, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceFrameOutline, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize };
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, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
@@ -9,10 +9,11 @@ declare function mediaPlayBadgeVisible(item: BoardSceneItem, zoom: number, optio
9
9
  materialized: boolean;
10
10
  hasVideoPreview: boolean;
11
11
  }): boolean;
12
+ declare function mediaPlayKeyCenter(item: BoardSceneItem): WorldPoint;
12
13
  declare function mediaPlayBadgeHit(item: BoardSceneItem, point: WorldPoint, zoom: number): boolean;
13
14
  declare function boardMediaActionAt(item: BoardSceneItem, point: WorldPoint, zoom: number, options: {
14
15
  materialized: boolean;
15
16
  hasVideoPreview: boolean;
16
17
  }): BoardMediaAction | null;
17
18
  //#endregion
18
- export { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible };
19
+ export { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter };
@@ -1,19 +1,27 @@
1
1
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
2
- import "./renderers/task-card-renderer.js";
2
+ import { degToRad, rotatePointAround, worldPoint } from "../model/geometry.js";
3
+ import { boardCardDetailed } from "./text-resolution.js";
4
+ import { audioKeyCenter } from "./renderers/audio-card-renderer.js";
3
5
  //#region src/board/render/media-interaction.ts
4
6
  function mediaPlayBadgeVisible(item, zoom, options) {
5
7
  if (!options.materialized) return false;
6
8
  if (item.type === "video" || item.type === "audio") return true;
7
- if (item.type !== "task" || zoom < .55) return false;
9
+ if (item.type !== "task" || !boardCardDetailed(zoom)) return false;
8
10
  const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
9
11
  if (artifact?.type === "audio") return true;
10
12
  return artifact?.type === "video" && (Boolean(artifact.previewUrl) || options.hasVideoPreview);
11
13
  }
14
+ function mediaPlayKeyCenter(item) {
15
+ const { x, y, width, height, rotation } = item.frame;
16
+ const center = worldPoint(x + width / 2, y + height / 2);
17
+ if (item.type !== "audio") return center;
18
+ const key = audioKeyCenter(width, height);
19
+ return rotatePointAround(worldPoint(x + key.x, y + key.y), center, degToRad(rotation || 0));
20
+ }
12
21
  function mediaPlayBadgeHit(item, point, zoom) {
13
22
  const radius = 28 / Math.max(zoom, .05);
14
- const centerX = item.frame.x + item.frame.width / 2;
15
- const centerY = item.frame.y + item.frame.height / 2;
16
- return Math.hypot(point.x - centerX, point.y - centerY) <= radius;
23
+ const center = mediaPlayKeyCenter(item);
24
+ return Math.hypot(point.x - center.x, point.y - center.y) <= radius;
17
25
  }
18
26
  function boardMediaActionAt(item, point, zoom, options) {
19
27
  return mediaPlayBadgeVisible(item, zoom, options) && mediaPlayBadgeHit(item, point, zoom) ? {
@@ -22,4 +30,4 @@ function boardMediaActionAt(item, point, zoom, options) {
22
30
  } : null;
23
31
  }
24
32
  //#endregion
25
- export { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible };
33
+ export { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter };
@@ -14,8 +14,8 @@ const DARK = {
14
14
  legendary: 16096779
15
15
  };
16
16
  const LIGHT = {
17
- bg: 16777215,
18
- surface: 16053493,
17
+ bg: 16251129,
18
+ surface: 16777215,
19
19
  hover: 15329772,
20
20
  border: 14079706,
21
21
  brand: 15222030,