@neta-art/cohub 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/{board.d.ts → board/animation.d.ts} +4 -2
- package/dist/{chunks/board.js → board/animation.js} +5 -44
- package/dist/board/codec.d.ts +25 -0
- package/dist/board/codec.js +243 -0
- package/dist/board/core/bindings.d.ts +45 -0
- package/dist/board/core/bindings.js +162 -0
- package/dist/board/core/draw-geometry.d.ts +30 -0
- package/dist/board/core/draw-geometry.js +175 -0
- package/dist/board/core/export-assets.d.ts +14 -0
- package/dist/board/core/export-assets.js +22 -0
- package/dist/board/core/export-plan.d.ts +84 -0
- package/dist/board/core/export-plan.js +128 -0
- package/dist/board/core/file-preview.d.ts +178 -0
- package/dist/board/core/file-preview.js +282 -0
- package/dist/board/core/palette.d.ts +42 -0
- package/dist/board/core/palette.js +166 -0
- package/dist/board/core/shape-definition.d.ts +35 -0
- package/dist/board/core/shape-definition.js +64 -0
- package/dist/board/core/shape-types.d.ts +140 -0
- package/dist/board/core/shape-types.js +27 -0
- package/dist/board/core/text-metrics.d.ts +18 -0
- package/dist/board/core/text-metrics.js +43 -0
- package/dist/board/core/tool-styles.d.ts +52 -0
- package/dist/board/core/tool-styles.js +51 -0
- package/dist/board/export/index.d.ts +56 -0
- package/dist/board/export/index.js +86 -0
- package/dist/board/export/scene.d.ts +35 -0
- package/dist/board/export/scene.js +69 -0
- package/dist/board/geometry.d.ts +139 -0
- package/dist/board/geometry.js +492 -0
- package/dist/board/headless/index.d.ts +69 -0
- package/dist/board/headless/index.js +150 -0
- package/dist/board/image-key.d.ts +19 -0
- package/dist/board/image-key.js +34 -0
- package/dist/board/index.d.ts +17 -0
- package/dist/board/index.js +17 -0
- package/dist/board/render/index.d.ts +6 -0
- package/dist/board/render/index.js +6 -0
- package/dist/board/render/palette.d.ts +5 -0
- package/dist/board/render/palette.js +30 -0
- package/dist/board/render/renderers/arrow-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/arrow-card-renderer.js +157 -0
- package/dist/board/render/renderers/base-card-renderer.d.ts +46 -0
- package/dist/board/render/renderers/base-card-renderer.js +153 -0
- package/dist/board/render/renderers/board-renderer-registry.d.ts +85 -0
- package/dist/board/render/renderers/board-renderer-registry.js +41 -0
- package/dist/board/render/renderers/draw-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/draw-card-renderer.js +82 -0
- package/dist/board/render/renderers/far-plate.d.ts +39 -0
- package/dist/board/render/renderers/far-plate.js +92 -0
- package/dist/board/render/renderers/file-card-renderer.d.ts +40 -0
- package/dist/board/render/renderers/file-card-renderer.js +350 -0
- package/dist/board/render/renderers/frame-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/frame-card-renderer.js +99 -0
- package/dist/board/render/renderers/geo-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/geo-card-renderer.js +128 -0
- package/dist/board/render/renderers/image-card-renderer.d.ts +8 -0
- package/dist/board/render/renderers/image-card-renderer.js +138 -0
- package/dist/board/render/renderers/text-card-renderer.d.ts +7 -0
- package/dist/board/render/renderers/text-card-renderer.js +94 -0
- package/dist/board/render/renderers/unknown-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/unknown-card-renderer.js +102 -0
- package/dist/board/render/renderers/video-card-renderer.d.ts +7 -0
- package/dist/board/render/renderers/video-card-renderer.js +146 -0
- package/dist/board/render/text-measurement.d.ts +30 -0
- package/dist/board/render/text-measurement.js +57 -0
- package/dist/board/render/text-resolution.d.ts +16 -0
- package/dist/board/render/text-resolution.js +43 -0
- package/dist/board/render/themes/board-theme-registry.d.ts +22 -0
- package/dist/board/render/themes/board-theme-registry.js +13 -0
- package/dist/board/render/themes/clean-theme.d.ts +5 -0
- package/dist/board/render/themes/clean-theme.js +105 -0
- package/dist/chunks/http.d.ts +2 -3
- package/dist/chunks/http.js +4 -0
- package/dist/chunks/websocket.d.ts +851 -353
- package/dist/http.d.ts +1 -2
- package/dist/index.d.ts +100 -5
- package/dist/index.js +391 -1
- package/dist/protocol/dist/board-constants.d.ts +23 -0
- package/dist/protocol/dist/board-constants.js +61 -0
- package/dist/protocol/dist/board-document.d.ts +1047 -0
- package/dist/protocol/dist/board-document.js +321 -0
- package/dist/protocol/dist/board.d.ts +186 -0
- package/dist/protocol/dist/board.js +207 -0
- package/dist/protocol/dist/index.d.ts +5 -0
- package/dist/protocol/dist/index.js +5 -0
- package/dist/protocol/dist/provenance.js +12 -0
- package/dist/protocol/dist/realtime/board-awareness.d.ts +1 -0
- package/dist/protocol/dist/realtime/board-awareness.js +119 -0
- package/dist/protocol/dist/realtime/types.d.ts +2 -0
- package/package.json +40 -4
- package/dist/board.js +0 -2
- package/dist/chunks/board.d.ts +0 -2589
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
2
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
3
|
+
import { Container, Graphics, Sprite, Texture } from "pixi.js";
|
|
4
|
+
//#region src/board/render/renderers/image-card-renderer.ts
|
|
5
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
const RADIUS = 2;
|
|
7
|
+
/**
|
|
8
|
+
* Lay the sprite out inside the frame.
|
|
9
|
+
*
|
|
10
|
+
* The frame normally already matches the image's pixel aspect (see the editor's
|
|
11
|
+
* media size adoption), so this fills it exactly. Until that correction lands
|
|
12
|
+
* for a freshly dropped file, `contain` keeps the whole image visible rather
|
|
13
|
+
* than cropping it — the brief letterbox is self-correcting.
|
|
14
|
+
*/
|
|
15
|
+
function layoutContain(sprite, width, height) {
|
|
16
|
+
const texture = sprite.texture;
|
|
17
|
+
const tw = texture.width;
|
|
18
|
+
const th = texture.height;
|
|
19
|
+
if (!tw || !th) return;
|
|
20
|
+
const scale = Math.min(width / tw, height / th);
|
|
21
|
+
sprite.width = tw * scale;
|
|
22
|
+
sprite.height = th * scale;
|
|
23
|
+
sprite.x = (width - sprite.width) / 2;
|
|
24
|
+
sprite.y = (height - sprite.height) / 2;
|
|
25
|
+
}
|
|
26
|
+
function sync(container, item, context) {
|
|
27
|
+
const parts = partsByContainer.get(container);
|
|
28
|
+
if (!parts) return;
|
|
29
|
+
positionShell(parts.root, item);
|
|
30
|
+
const { width, height } = item.frame;
|
|
31
|
+
const selected = context.selectedIds.has(item.id);
|
|
32
|
+
const hovered = context.hoveredId === item.id;
|
|
33
|
+
const key = context.assetKey(item);
|
|
34
|
+
const texture = key ? context.getTexture(key) : null;
|
|
35
|
+
const failed = Boolean(key && !texture && context.hasError(key));
|
|
36
|
+
const texId = texture ? `${texture.width}x${texture.height}` : "none";
|
|
37
|
+
const sig = [
|
|
38
|
+
key ?? "",
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
selected,
|
|
42
|
+
hovered,
|
|
43
|
+
texId,
|
|
44
|
+
failed,
|
|
45
|
+
context.colorScheme
|
|
46
|
+
].join("|");
|
|
47
|
+
if (sig === parts.sig) return;
|
|
48
|
+
parts.sig = sig;
|
|
49
|
+
if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
|
|
50
|
+
parts.sprite.visible = Boolean(texture);
|
|
51
|
+
if (texture) layoutContain(parts.sprite, width, height);
|
|
52
|
+
parts.placeholder.clear();
|
|
53
|
+
if (!texture) {
|
|
54
|
+
parts.placeholder.roundRect(0, 0, width, height, RADIUS).fill({
|
|
55
|
+
color: context.palette.surface,
|
|
56
|
+
alpha: .85
|
|
57
|
+
}).roundRect(0, 0, width, height, RADIUS).stroke({
|
|
58
|
+
color: failed ? context.palette.border : context.palette.hover,
|
|
59
|
+
width: 1,
|
|
60
|
+
alpha: .9
|
|
61
|
+
});
|
|
62
|
+
const cx = width / 2;
|
|
63
|
+
const cy = height / 2;
|
|
64
|
+
if (failed) parts.placeholder.moveTo(cx - 10, cy - 10).lineTo(cx + 10, cy + 10).moveTo(cx + 10, cy - 10).lineTo(cx - 10, cy + 10).stroke({
|
|
65
|
+
color: context.palette.muted,
|
|
66
|
+
width: 1.5,
|
|
67
|
+
alpha: .8
|
|
68
|
+
});
|
|
69
|
+
else parts.placeholder.circle(cx, cy, 6).stroke({
|
|
70
|
+
color: context.palette.muted,
|
|
71
|
+
width: 1.25,
|
|
72
|
+
alpha: .55
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
parts.placeholder.visible = !texture;
|
|
76
|
+
if (selected || hovered) {
|
|
77
|
+
const hasPixels = Boolean(texture) && parts.sprite.width > 0;
|
|
78
|
+
const ox = hasPixels ? parts.sprite.x : 0;
|
|
79
|
+
const oy = hasPixels ? parts.sprite.y : 0;
|
|
80
|
+
const ow = hasPixels ? parts.sprite.width : width;
|
|
81
|
+
const oh = hasPixels ? parts.sprite.height : height;
|
|
82
|
+
parts.placeholder.visible = true;
|
|
83
|
+
parts.placeholder.roundRect(ox, oy, ow, oh, RADIUS).stroke({
|
|
84
|
+
color: selected ? context.palette.brand : context.palette.muted,
|
|
85
|
+
width: selected ? 2 : 1,
|
|
86
|
+
alpha: selected ? .95 : .7
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const imageCardRenderer = {
|
|
91
|
+
id: "image-card",
|
|
92
|
+
canRender: (item) => item.type === "image",
|
|
93
|
+
create: (item, context) => {
|
|
94
|
+
const root = new Container();
|
|
95
|
+
const sprite = new Sprite(Texture.EMPTY);
|
|
96
|
+
const placeholder = new Graphics();
|
|
97
|
+
const mask = new Graphics();
|
|
98
|
+
const maskBox = () => {
|
|
99
|
+
mask.clear();
|
|
100
|
+
if (item.type === "image") mask.roundRect(0, 0, item.frame.width, item.frame.height, RADIUS).fill({ color: 16777215 });
|
|
101
|
+
};
|
|
102
|
+
maskBox();
|
|
103
|
+
root.addChild(placeholder, sprite, mask);
|
|
104
|
+
sprite.mask = mask;
|
|
105
|
+
partsByContainer.set(root, {
|
|
106
|
+
root,
|
|
107
|
+
sprite,
|
|
108
|
+
placeholder,
|
|
109
|
+
sig: ""
|
|
110
|
+
});
|
|
111
|
+
if (item.type === "image") sync(root, item, context);
|
|
112
|
+
return root;
|
|
113
|
+
},
|
|
114
|
+
update: (container, item, context) => {
|
|
115
|
+
if (item.type !== "image") return;
|
|
116
|
+
const parts = partsByContainer.get(container);
|
|
117
|
+
if (!parts) return;
|
|
118
|
+
const mask = parts.root.children.find((child) => child !== parts.sprite && child !== parts.placeholder);
|
|
119
|
+
if (mask) mask.clear().roundRect(0, 0, item.frame.width, item.frame.height, RADIUS).fill({ color: 16777215 });
|
|
120
|
+
sync(container, item, context);
|
|
121
|
+
},
|
|
122
|
+
renderFar: (graphics, item, context) => {
|
|
123
|
+
drawFarPlate(graphics, item.frame, {
|
|
124
|
+
fill: context.palette.hover,
|
|
125
|
+
fillAlpha: .9
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
destroy: (container) => {
|
|
129
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
130
|
+
partsByContainer.delete(container);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
/** Helper kept for type narrowing in tests. */
|
|
134
|
+
function isImageItem(item) {
|
|
135
|
+
return item.type === "image";
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
export { imageCardRenderer, isImageItem };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BoardItem, BoardTextItem } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
3
|
+
//#region src/board/render/renderers/text-card-renderer.d.ts
|
|
4
|
+
declare const textCardRenderer: BoardCardRenderer;
|
|
5
|
+
declare function isTextItem(item: BoardItem): item is BoardTextItem;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { isTextItem, textCardRenderer };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { TEXT_FONT_SIZE, boardTextLineHeight, clampBoardTextFontSize, measureBoardText } from "../../core/text-metrics.js";
|
|
3
|
+
import { pickBoardColor } from "../../core/palette.js";
|
|
4
|
+
import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
7
|
+
import { Container, Text } from "pixi.js";
|
|
8
|
+
//#region src/board/render/renderers/text-card-renderer.ts
|
|
9
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
function sync(container, item, context) {
|
|
11
|
+
const parts = partsByContainer.get(container);
|
|
12
|
+
if (!parts) return;
|
|
13
|
+
positionShell(parts.root, item);
|
|
14
|
+
const fontSize = clampBoardTextFontSize(item.fontSize);
|
|
15
|
+
const measured = measureBoardText(item.text, fontSize);
|
|
16
|
+
const previewScale = Math.max(1e-4, item.frame.width / Math.max(1e-4, measured.width));
|
|
17
|
+
parts.body.scale.set(previewScale);
|
|
18
|
+
parts.body.position.set(item.frame.width / 2, item.frame.height / 2);
|
|
19
|
+
syncTextResolution(parts.body, parts, context.zoom * Math.max(1, previewScale));
|
|
20
|
+
const color = pickBoardColor(context.colors, item.color || "neutral", context.colorScheme);
|
|
21
|
+
const ink = item.color === "neutral" || !item.color ? context.palette.text : color.stroke;
|
|
22
|
+
const contentSig = [
|
|
23
|
+
item.text,
|
|
24
|
+
item.color,
|
|
25
|
+
fontSize,
|
|
26
|
+
context.colorScheme,
|
|
27
|
+
context.colors.brand.stroke,
|
|
28
|
+
context.palette.text
|
|
29
|
+
].join("|");
|
|
30
|
+
if (contentSig === parts.contentSig) return;
|
|
31
|
+
parts.contentSig = contentSig;
|
|
32
|
+
if (parts.body.text !== item.text) parts.body.text = item.text || "";
|
|
33
|
+
parts.body.style.fill = ink;
|
|
34
|
+
parts.body.style.fontSize = fontSize;
|
|
35
|
+
parts.body.style.lineHeight = boardTextLineHeight(fontSize);
|
|
36
|
+
}
|
|
37
|
+
const textCardRenderer = {
|
|
38
|
+
id: "text-card",
|
|
39
|
+
canRender: (item) => item.type === "text",
|
|
40
|
+
create: (item, context) => {
|
|
41
|
+
const root = new Container();
|
|
42
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
43
|
+
const color = pickBoardColor(context.colors, item.type === "text" ? item.color || "neutral" : "neutral", context.colorScheme);
|
|
44
|
+
const body = new Text({
|
|
45
|
+
text: "",
|
|
46
|
+
style: {
|
|
47
|
+
fill: item.type === "text" && (item.color === "neutral" || !item.color) ? context.palette.text : color.stroke,
|
|
48
|
+
fontFamily: BOARD_FONT_STACK,
|
|
49
|
+
fontSize: TEXT_FONT_SIZE,
|
|
50
|
+
fontWeight: "500",
|
|
51
|
+
lineHeight: boardTextLineHeight(TEXT_FONT_SIZE),
|
|
52
|
+
wordWrap: false
|
|
53
|
+
},
|
|
54
|
+
resolution,
|
|
55
|
+
roundPixels: true
|
|
56
|
+
});
|
|
57
|
+
body.anchor.set(.5);
|
|
58
|
+
root.addChild(body);
|
|
59
|
+
partsByContainer.set(root, {
|
|
60
|
+
root,
|
|
61
|
+
body,
|
|
62
|
+
resolution,
|
|
63
|
+
contentSig: ""
|
|
64
|
+
});
|
|
65
|
+
if (item.type === "text") sync(root, item, context);
|
|
66
|
+
return root;
|
|
67
|
+
},
|
|
68
|
+
update: (container, item, context) => {
|
|
69
|
+
if (item.type === "text") sync(container, item, context);
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Far LOD: a low-alpha ink bar. Body text is well under a pixel tall at these
|
|
73
|
+
* zooms, so a rasterised glyph texture would only ever resolve to a smudge —
|
|
74
|
+
* this approximates that smudge for free and skips the texture entirely.
|
|
75
|
+
*/
|
|
76
|
+
renderFar: (graphics, item, context) => {
|
|
77
|
+
if (item.type !== "text") return;
|
|
78
|
+
const color = pickBoardColor(context.colors, item.color || "neutral", context.colorScheme);
|
|
79
|
+
const ink = item.color === "neutral" || !item.color ? context.palette.text : color.stroke;
|
|
80
|
+
drawFarPlate(graphics, item.frame, {
|
|
81
|
+
fill: ink,
|
|
82
|
+
fillAlpha: .35
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
destroy: (container) => {
|
|
86
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
87
|
+
partsByContainer.delete(container);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function isTextItem(item) {
|
|
91
|
+
return item.type === "text";
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { isTextItem, textCardRenderer };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { unknownRealType } from "../../../protocol/dist/board-document.js";
|
|
3
|
+
import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
|
|
4
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
5
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
6
|
+
import { Container, Graphics, Text } from "pixi.js";
|
|
7
|
+
//#region src/board/render/renderers/unknown-card-renderer.ts
|
|
8
|
+
const RADIUS = 10;
|
|
9
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Neutral placeholder for shape types this client does not recognise. The shape
|
|
12
|
+
* stays fully interactive (select/move/resize via the generic box definition) and
|
|
13
|
+
* its data is preserved elsewhere — this is only its display.
|
|
14
|
+
*/
|
|
15
|
+
function sync(container, item, context) {
|
|
16
|
+
const parts = partsByContainer.get(container);
|
|
17
|
+
if (!parts) return;
|
|
18
|
+
positionShell(parts.root, item);
|
|
19
|
+
const { width, height } = item.frame;
|
|
20
|
+
const selected = context.selectedIds.has(item.id);
|
|
21
|
+
const realType = unknownRealType(item);
|
|
22
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
23
|
+
const visualSig = [
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
selected,
|
|
27
|
+
context.palette.surface,
|
|
28
|
+
context.palette.brand,
|
|
29
|
+
context.palette.border,
|
|
30
|
+
context.palette.muted
|
|
31
|
+
].join("|");
|
|
32
|
+
if (visualSig !== parts.visualSig) {
|
|
33
|
+
parts.visualSig = visualSig;
|
|
34
|
+
parts.box.clear();
|
|
35
|
+
parts.box.roundRect(0, 0, width, height, RADIUS).fill({
|
|
36
|
+
color: context.palette.surface,
|
|
37
|
+
alpha: .7
|
|
38
|
+
}).roundRect(0, 0, width, height, RADIUS).stroke({
|
|
39
|
+
color: selected ? context.palette.brand : context.palette.border,
|
|
40
|
+
width: selected ? 2 : 1,
|
|
41
|
+
alpha: .8
|
|
42
|
+
});
|
|
43
|
+
parts.box.roundRect(6, 6, width - 12, height - 12, RADIUS - 4).stroke({
|
|
44
|
+
color: context.palette.muted,
|
|
45
|
+
width: 1,
|
|
46
|
+
alpha: .4
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const textSig = [realType, context.palette.muted].join("|");
|
|
50
|
+
if (textSig !== parts.textSig) {
|
|
51
|
+
parts.textSig = textSig;
|
|
52
|
+
parts.label.text = realType;
|
|
53
|
+
parts.label.style.fill = context.palette.muted;
|
|
54
|
+
}
|
|
55
|
+
parts.label.position.set(width / 2, height / 2);
|
|
56
|
+
}
|
|
57
|
+
const unknownCardRenderer = {
|
|
58
|
+
id: "unknown-card",
|
|
59
|
+
canRender: (item) => "raw" in item,
|
|
60
|
+
create: (item, context) => {
|
|
61
|
+
const root = new Container();
|
|
62
|
+
const box = new Graphics();
|
|
63
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
64
|
+
const label = new Text({
|
|
65
|
+
text: "",
|
|
66
|
+
style: {
|
|
67
|
+
fontFamily: BOARD_MONO_FONT_STACK,
|
|
68
|
+
fontSize: 11,
|
|
69
|
+
fontWeight: "600"
|
|
70
|
+
},
|
|
71
|
+
resolution,
|
|
72
|
+
roundPixels: true
|
|
73
|
+
});
|
|
74
|
+
label.anchor.set(.5);
|
|
75
|
+
root.addChild(box, label);
|
|
76
|
+
partsByContainer.set(root, {
|
|
77
|
+
root,
|
|
78
|
+
box,
|
|
79
|
+
label,
|
|
80
|
+
visualSig: "",
|
|
81
|
+
textSig: "",
|
|
82
|
+
resolution
|
|
83
|
+
});
|
|
84
|
+
if ("raw" in item) sync(root, item, context);
|
|
85
|
+
return root;
|
|
86
|
+
},
|
|
87
|
+
update: (container, item, context) => {
|
|
88
|
+
if ("raw" in item) sync(container, item, context);
|
|
89
|
+
},
|
|
90
|
+
renderFar: (graphics, item, context) => {
|
|
91
|
+
drawFarPlate(graphics, item.frame, {
|
|
92
|
+
fill: context.palette.muted,
|
|
93
|
+
fillAlpha: .2
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
destroy: (container) => {
|
|
97
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
98
|
+
partsByContainer.delete(container);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
export { unknownCardRenderer };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BoardItem, BoardVideoItem } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
3
|
+
//#region src/board/render/renderers/video-card-renderer.d.ts
|
|
4
|
+
declare const videoCardRenderer: BoardCardRenderer;
|
|
5
|
+
declare function isVideoItem(item: BoardItem): item is BoardVideoItem;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { isVideoItem, videoCardRenderer };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
3
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
4
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
6
|
+
//#region src/board/render/renderers/video-card-renderer.ts
|
|
7
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
const RADIUS = 4;
|
|
9
|
+
function layoutContain(sprite, width, height) {
|
|
10
|
+
const texture = sprite.texture;
|
|
11
|
+
if (!texture.width || !texture.height) return;
|
|
12
|
+
const scale = Math.min(width / texture.width, height / texture.height);
|
|
13
|
+
sprite.width = texture.width * scale;
|
|
14
|
+
sprite.height = texture.height * scale;
|
|
15
|
+
sprite.x = (width - sprite.width) / 2;
|
|
16
|
+
sprite.y = (height - sprite.height) / 2;
|
|
17
|
+
}
|
|
18
|
+
function sync(container, item, context) {
|
|
19
|
+
const parts = partsByContainer.get(container);
|
|
20
|
+
if (!parts) return;
|
|
21
|
+
positionShell(parts.root, item);
|
|
22
|
+
const { width, height } = item.frame;
|
|
23
|
+
const selected = context.selectedIds.has(item.id);
|
|
24
|
+
const hovered = context.hoveredId === item.id;
|
|
25
|
+
const resizing = context.resizingIds.has(item.id);
|
|
26
|
+
const title = item.snapshot?.title ?? item.ref.path.split("/").pop() ?? "Video";
|
|
27
|
+
const key = context.assetKey(item);
|
|
28
|
+
const texture = key ? context.getTexture(key) : null;
|
|
29
|
+
const failed = Boolean(key && !texture && context.hasError(key));
|
|
30
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
31
|
+
if (parts.label.text !== title) parts.label.text = title;
|
|
32
|
+
syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), resizing);
|
|
33
|
+
parts.label.position.set(8, Math.max(8, height - parts.label.height - 8));
|
|
34
|
+
const sig = [
|
|
35
|
+
key ?? "",
|
|
36
|
+
texture ? `${texture.width}x${texture.height}` : "none",
|
|
37
|
+
width,
|
|
38
|
+
height,
|
|
39
|
+
selected,
|
|
40
|
+
hovered,
|
|
41
|
+
failed,
|
|
42
|
+
title,
|
|
43
|
+
parts.label.height,
|
|
44
|
+
context.palette.surface,
|
|
45
|
+
context.palette.brand,
|
|
46
|
+
context.palette.muted,
|
|
47
|
+
context.palette.border,
|
|
48
|
+
context.palette.hover,
|
|
49
|
+
context.palette.text
|
|
50
|
+
].join("|");
|
|
51
|
+
if (sig === parts.sig) return;
|
|
52
|
+
parts.sig = sig;
|
|
53
|
+
if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
|
|
54
|
+
parts.sprite.visible = Boolean(texture);
|
|
55
|
+
if (texture) layoutContain(parts.sprite, width, height);
|
|
56
|
+
parts.mask.clear().roundRect(0, 0, width, height, RADIUS).fill({ color: 16777215 });
|
|
57
|
+
parts.plate.clear().roundRect(0, 0, width, height, RADIUS).fill({
|
|
58
|
+
color: context.palette.surface,
|
|
59
|
+
alpha: .96
|
|
60
|
+
});
|
|
61
|
+
parts.chrome.clear();
|
|
62
|
+
if (texture) {
|
|
63
|
+
const bandHeight = Math.min(height, parts.label.height + 16);
|
|
64
|
+
parts.chrome.rect(0, height - bandHeight, width, bandHeight).fill({
|
|
65
|
+
color: context.palette.surface,
|
|
66
|
+
alpha: .78
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const cx = width / 2;
|
|
70
|
+
const cy = height / 2;
|
|
71
|
+
const radius = Math.min(22, Math.min(width, height) * .18);
|
|
72
|
+
parts.chrome.circle(cx, cy, radius).fill({
|
|
73
|
+
color: selected ? context.palette.brand : context.palette.surface,
|
|
74
|
+
alpha: texture ? .88 : .96
|
|
75
|
+
});
|
|
76
|
+
const triangle = radius * .55;
|
|
77
|
+
parts.chrome.moveTo(cx - triangle * .35, cy - triangle * .6).lineTo(cx - triangle * .35, cy + triangle * .6).lineTo(cx + triangle * .7, cy).closePath().fill({
|
|
78
|
+
color: context.palette.text,
|
|
79
|
+
alpha: .94
|
|
80
|
+
});
|
|
81
|
+
parts.chrome.roundRect(0, 0, width, height, RADIUS).stroke({
|
|
82
|
+
color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
|
|
83
|
+
width: selected ? 2 : 1,
|
|
84
|
+
alpha: selected ? .95 : .85
|
|
85
|
+
});
|
|
86
|
+
parts.label.style.fill = texture ? context.palette.text : context.palette.muted;
|
|
87
|
+
}
|
|
88
|
+
const videoCardRenderer = {
|
|
89
|
+
id: "video-card",
|
|
90
|
+
canRender: (item) => item.type === "video",
|
|
91
|
+
create: (item, context) => {
|
|
92
|
+
const root = new Container();
|
|
93
|
+
const plate = new Graphics();
|
|
94
|
+
const sprite = new Sprite(Texture.EMPTY);
|
|
95
|
+
const mask = new Graphics();
|
|
96
|
+
const chrome = new Graphics();
|
|
97
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
98
|
+
const label = new Text({
|
|
99
|
+
text: "",
|
|
100
|
+
style: {
|
|
101
|
+
fill: context.palette.muted,
|
|
102
|
+
fontFamily: BOARD_FONT_STACK,
|
|
103
|
+
fontSize: 12,
|
|
104
|
+
fontWeight: "500",
|
|
105
|
+
wordWrap: true
|
|
106
|
+
},
|
|
107
|
+
resolution,
|
|
108
|
+
roundPixels: true
|
|
109
|
+
});
|
|
110
|
+
root.addChild(plate, sprite, mask, chrome, label);
|
|
111
|
+
sprite.mask = mask;
|
|
112
|
+
partsByContainer.set(root, {
|
|
113
|
+
root,
|
|
114
|
+
plate,
|
|
115
|
+
sprite,
|
|
116
|
+
mask,
|
|
117
|
+
chrome,
|
|
118
|
+
label,
|
|
119
|
+
sig: "",
|
|
120
|
+
wrapWidth: 0,
|
|
121
|
+
resolution
|
|
122
|
+
});
|
|
123
|
+
if (item.type === "video") sync(root, item, context);
|
|
124
|
+
return root;
|
|
125
|
+
},
|
|
126
|
+
update: (container, item, context) => {
|
|
127
|
+
if (item.type === "video") sync(container, item, context);
|
|
128
|
+
},
|
|
129
|
+
renderFar: (graphics, item, context) => {
|
|
130
|
+
drawFarPlate(graphics, item.frame, {
|
|
131
|
+
fill: context.palette.surface,
|
|
132
|
+
fillAlpha: .96,
|
|
133
|
+
accent: context.palette.brand,
|
|
134
|
+
accentAlpha: .75
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
destroy: (container) => {
|
|
138
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
139
|
+
partsByContainer.delete(container);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
function isVideoItem(item) {
|
|
143
|
+
return item.type === "video";
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { isVideoItem, videoCardRenderer };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/board/render/text-measurement.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Canvas-backed text measurement.
|
|
4
|
+
*
|
|
5
|
+
* Measurement goes through Pixi's `DOMAdapter` rather than `document`, so the
|
|
6
|
+
* browser and a headless export produce the same numbers. The core metrics
|
|
7
|
+
* module owns the layout maths and only borrows this measurer, which is what
|
|
8
|
+
* lets `@neta-art/cohub/board` lay text out with no PixiJS installed.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Route board text measurement through the active Pixi DOM adapter.
|
|
12
|
+
*
|
|
13
|
+
* Call this after swapping the adapter (see `../headless`), which drops the
|
|
14
|
+
* cached measuring context. Anything that goes through `getBoardCardRenderer`
|
|
15
|
+
* is already covered by `ensureBoardTextMeasurement`.
|
|
16
|
+
*/
|
|
17
|
+
declare function installBoardTextMeasurement(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Install the canvas measurer once, unless it already is.
|
|
20
|
+
*
|
|
21
|
+
* The core metrics module falls back to a per-character estimate when no
|
|
22
|
+
* measurer is set, which is off by ~90% for real text — wrong enough to
|
|
23
|
+
* misplace every label. Rather than make that a step consumers can forget, the
|
|
24
|
+
* renderer registry calls this before handing a renderer out. It cannot be an
|
|
25
|
+
* import-time side effect: the package is marked side-effect free, so a bundler
|
|
26
|
+
* is entitled to drop a module whose only job happens at load time.
|
|
27
|
+
*/
|
|
28
|
+
declare function ensureBoardTextMeasurement(): void;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ensureBoardTextMeasurement, installBoardTextMeasurement };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../protocol/dist/board-constants.js";
|
|
2
|
+
import { setBoardTextMeasurer } from "../core/text-metrics.js";
|
|
3
|
+
import { DOMAdapter } from "pixi.js";
|
|
4
|
+
//#region src/board/render/text-measurement.ts
|
|
5
|
+
/**
|
|
6
|
+
* Canvas-backed text measurement.
|
|
7
|
+
*
|
|
8
|
+
* Measurement goes through Pixi's `DOMAdapter` rather than `document`, so the
|
|
9
|
+
* browser and a headless export produce the same numbers. The core metrics
|
|
10
|
+
* module owns the layout maths and only borrows this measurer, which is what
|
|
11
|
+
* lets `@neta-art/cohub/board` lay text out with no PixiJS installed.
|
|
12
|
+
*/
|
|
13
|
+
let measureContext;
|
|
14
|
+
let installed = false;
|
|
15
|
+
function getMeasureContext() {
|
|
16
|
+
if (measureContext !== void 0) return measureContext;
|
|
17
|
+
try {
|
|
18
|
+
measureContext = DOMAdapter.get().createCanvas(1, 1).getContext("2d") ?? null;
|
|
19
|
+
} catch {
|
|
20
|
+
measureContext = null;
|
|
21
|
+
}
|
|
22
|
+
return measureContext;
|
|
23
|
+
}
|
|
24
|
+
function measureText(text, fontSize) {
|
|
25
|
+
const context = getMeasureContext();
|
|
26
|
+
if (!context) return null;
|
|
27
|
+
context.font = `500 ${fontSize}px ${BOARD_FONT_STACK}`;
|
|
28
|
+
return context.measureText(text).width;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Route board text measurement through the active Pixi DOM adapter.
|
|
32
|
+
*
|
|
33
|
+
* Call this after swapping the adapter (see `../headless`), which drops the
|
|
34
|
+
* cached measuring context. Anything that goes through `getBoardCardRenderer`
|
|
35
|
+
* is already covered by `ensureBoardTextMeasurement`.
|
|
36
|
+
*/
|
|
37
|
+
function installBoardTextMeasurement() {
|
|
38
|
+
measureContext = void 0;
|
|
39
|
+
installed = true;
|
|
40
|
+
setBoardTextMeasurer(measureText);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Install the canvas measurer once, unless it already is.
|
|
44
|
+
*
|
|
45
|
+
* The core metrics module falls back to a per-character estimate when no
|
|
46
|
+
* measurer is set, which is off by ~90% for real text — wrong enough to
|
|
47
|
+
* misplace every label. Rather than make that a step consumers can forget, the
|
|
48
|
+
* renderer registry calls this before handing a renderer out. It cannot be an
|
|
49
|
+
* import-time side effect: the package is marked side-effect free, so a bundler
|
|
50
|
+
* is entitled to drop a module whose only job happens at load time.
|
|
51
|
+
*/
|
|
52
|
+
function ensureBoardTextMeasurement() {
|
|
53
|
+
if (installed) return;
|
|
54
|
+
installBoardTextMeasurement();
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { ensureBoardTextMeasurement, installBoardTextMeasurement };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Text } from "pixi.js";
|
|
2
|
+
//#region src/board/render/text-resolution.d.ts
|
|
3
|
+
declare function getBoardResolution(): number;
|
|
4
|
+
declare function textZoomBucket(zoom: number): number;
|
|
5
|
+
/** Effective text resolution for a given camera zoom. */
|
|
6
|
+
declare function textResolutionForZoom(zoom: number): number;
|
|
7
|
+
/** Update a Pixi text texture only when zoom crosses a resolution bucket. */
|
|
8
|
+
declare function syncTextResolution(text: Text, state: {
|
|
9
|
+
resolution: number;
|
|
10
|
+
}, zoom: number): void;
|
|
11
|
+
/** Defer expensive Pixi word-wrap rasterisation during a live resize. */
|
|
12
|
+
declare function syncTextWrapWidth(text: Text, state: {
|
|
13
|
+
wrapWidth: number;
|
|
14
|
+
}, width: number, defer: boolean): void;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/board/render/text-resolution.ts
|
|
2
|
+
const MAX_BOARD_RESOLUTION = 2;
|
|
3
|
+
function getBoardResolution() {
|
|
4
|
+
return Math.min(globalThis.devicePixelRatio || 1, MAX_BOARD_RESOLUTION);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
|
|
8
|
+
* looks soft unless we raise its resolution. We quantise zoom into a few
|
|
9
|
+
* buckets so small zoom changes don't thrash texture regeneration.
|
|
10
|
+
*/
|
|
11
|
+
const TEXT_ZOOM_BUCKETS = [
|
|
12
|
+
.5,
|
|
13
|
+
1,
|
|
14
|
+
1.5,
|
|
15
|
+
2,
|
|
16
|
+
3,
|
|
17
|
+
4
|
|
18
|
+
];
|
|
19
|
+
function textZoomBucket(zoom) {
|
|
20
|
+
const clamped = Math.max(.1, Math.min(4, zoom));
|
|
21
|
+
for (const bucket of TEXT_ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
|
|
22
|
+
return TEXT_ZOOM_BUCKETS[TEXT_ZOOM_BUCKETS.length - 1] ?? 4;
|
|
23
|
+
}
|
|
24
|
+
/** Effective text resolution for a given camera zoom. */
|
|
25
|
+
function textResolutionForZoom(zoom) {
|
|
26
|
+
const bucket = textZoomBucket(zoom);
|
|
27
|
+
return Math.min(getBoardResolution() * Math.max(1, bucket), MAX_BOARD_RESOLUTION * 3);
|
|
28
|
+
}
|
|
29
|
+
/** Update a Pixi text texture only when zoom crosses a resolution bucket. */
|
|
30
|
+
function syncTextResolution(text, state, zoom) {
|
|
31
|
+
const resolution = textResolutionForZoom(zoom);
|
|
32
|
+
if (resolution === state.resolution) return;
|
|
33
|
+
text.resolution = resolution;
|
|
34
|
+
state.resolution = resolution;
|
|
35
|
+
}
|
|
36
|
+
/** Defer expensive Pixi word-wrap rasterisation during a live resize. */
|
|
37
|
+
function syncTextWrapWidth(text, state, width, defer) {
|
|
38
|
+
if (defer || width === state.wrapWidth) return;
|
|
39
|
+
text.style.wordWrapWidth = width;
|
|
40
|
+
state.wrapWidth = width;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BoardDocument, BoardViewport } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardRenderPalette } from "../renderers/board-renderer-registry.js";
|
|
3
|
+
import { Application, Container } from "pixi.js";
|
|
4
|
+
//#region src/board/render/themes/board-theme-registry.d.ts
|
|
5
|
+
type BoardThemeContext = {
|
|
6
|
+
app: Application;
|
|
7
|
+
document: BoardDocument;
|
|
8
|
+
viewport: BoardViewport;
|
|
9
|
+
palette: BoardRenderPalette;
|
|
10
|
+
/** The host renders an image backdrop below the transparent Pixi canvas. */
|
|
11
|
+
hasImageBackground?: boolean;
|
|
12
|
+
};
|
|
13
|
+
type BoardThemeRenderer = {
|
|
14
|
+
id: string;
|
|
15
|
+
canRender: (document: BoardDocument) => boolean;
|
|
16
|
+
createBackground: (context: BoardThemeContext) => Container;
|
|
17
|
+
updateBackground?: (container: Container, context: BoardThemeContext) => void;
|
|
18
|
+
};
|
|
19
|
+
declare function getBoardThemeRenderer(document: BoardDocument): BoardThemeRenderer;
|
|
20
|
+
declare function registerBoardThemeRenderer(renderer: BoardThemeRenderer): void;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { BoardThemeContext, BoardThemeRenderer, getBoardThemeRenderer, registerBoardThemeRenderer };
|