@ume-group/contracts 0.2.1
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 +37 -0
- package/dist/adserving.d.ts +150 -0
- package/dist/adserving.d.ts.map +1 -0
- package/dist/adserving.js +8 -0
- package/dist/campaigns.d.ts +37 -0
- package/dist/campaigns.d.ts.map +1 -0
- package/dist/campaigns.js +8 -0
- package/dist/gausst.d.ts +236 -0
- package/dist/gausst.d.ts.map +1 -0
- package/dist/gausst.js +307 -0
- package/dist/gausst.test.d.ts +2 -0
- package/dist/gausst.test.d.ts.map +1 -0
- package/dist/gausst.test.js +71 -0
- package/dist/index.d.ts +1531 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1112 -0
- package/dist/layer2/index.d.ts +9 -0
- package/dist/layer2/index.d.ts.map +1 -0
- package/dist/layer2/index.js +10 -0
- package/dist/layer2/shaders.d.ts +185 -0
- package/dist/layer2/shaders.d.ts.map +1 -0
- package/dist/layer2/shaders.js +604 -0
- package/dist/layer2/webcam-utils.d.ts +113 -0
- package/dist/layer2/webcam-utils.d.ts.map +1 -0
- package/dist/layer2/webcam-utils.js +147 -0
- package/dist/layer2/webcam-utils.test.d.ts +2 -0
- package/dist/layer2/webcam-utils.test.d.ts.map +1 -0
- package/dist/layer2/webcam-utils.test.js +18 -0
- package/dist/layer2.d.ts +558 -0
- package/dist/layer2.d.ts.map +1 -0
- package/dist/layer2.js +376 -0
- package/dist/layer2.test.d.ts +2 -0
- package/dist/layer2.test.d.ts.map +1 -0
- package/dist/layer2.test.js +65 -0
- package/dist/perspective.d.ts +28 -0
- package/dist/perspective.d.ts.map +1 -0
- package/dist/perspective.js +157 -0
- package/dist/segmentation/MediaPipeSegmenter.d.ts +201 -0
- package/dist/segmentation/MediaPipeSegmenter.d.ts.map +1 -0
- package/dist/segmentation/MediaPipeSegmenter.js +434 -0
- package/dist/segmentation/index.d.ts +5 -0
- package/dist/segmentation/index.d.ts.map +1 -0
- package/dist/segmentation/index.js +4 -0
- package/dist/webcam/GarbageMatteDragManager.d.ts +63 -0
- package/dist/webcam/GarbageMatteDragManager.d.ts.map +1 -0
- package/dist/webcam/GarbageMatteDragManager.js +183 -0
- package/dist/webcam/WebcamStreamManager.d.ts +103 -0
- package/dist/webcam/WebcamStreamManager.d.ts.map +1 -0
- package/dist/webcam/WebcamStreamManager.js +356 -0
- package/dist/webcam/index.d.ts +5 -0
- package/dist/webcam/index.d.ts.map +1 -0
- package/dist/webcam/index.js +2 -0
- package/openapi/admetise.yaml +632 -0
- package/openapi/includu.yaml +621 -0
- package/openapi/integration.yaml +372 -0
- package/openapi/shared/schemas.yaml +227 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for Includu Platform
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - @includu/editor (timeline editor)
|
|
6
|
+
* - @includu/cms (video library)
|
|
7
|
+
*/
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Debug Utilities
|
|
10
|
+
// ============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Debug flag - true in Vite dev mode, false in production builds.
|
|
13
|
+
* Can also be set manually for testing.
|
|
14
|
+
*/
|
|
15
|
+
export let DEBUG = !!import.meta.env?.DEV;
|
|
16
|
+
/** Override the DEBUG flag (useful for testing or manual control) */
|
|
17
|
+
export function setDebug(value) {
|
|
18
|
+
DEBUG = value;
|
|
19
|
+
}
|
|
20
|
+
/** Debug logger - only outputs when DEBUG is true (dev mode by default) */
|
|
21
|
+
export function debug(...args) {
|
|
22
|
+
if (DEBUG)
|
|
23
|
+
console.log(...args);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates default corner offsets (no perspective adjustment)
|
|
27
|
+
*/
|
|
28
|
+
export function createDefaultCornerOffsets() {
|
|
29
|
+
return {
|
|
30
|
+
tl: { dx: 0, dy: 0 },
|
|
31
|
+
tr: { dx: 0, dy: 0 },
|
|
32
|
+
br: { dx: 0, dy: 0 },
|
|
33
|
+
bl: { dx: 0, dy: 0 }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Computes absolute corner positions from overlay position and corner offsets
|
|
38
|
+
* This is how corners are calculated for rendering
|
|
39
|
+
*/
|
|
40
|
+
export function computeCornersFromOffsets(position, offsets) {
|
|
41
|
+
// Base corners from overlay position (normalized 0-1)
|
|
42
|
+
const left = (position.x - position.width / 2) / 100;
|
|
43
|
+
const right = (position.x + position.width / 2) / 100;
|
|
44
|
+
const top = (position.y - position.height / 2) / 100;
|
|
45
|
+
const bottom = (position.y + position.height / 2) / 100;
|
|
46
|
+
// Apply offsets (convert from % of overlay size to normalized coordinates)
|
|
47
|
+
const widthNorm = position.width / 100;
|
|
48
|
+
const heightNorm = position.height / 100;
|
|
49
|
+
return {
|
|
50
|
+
tl: {
|
|
51
|
+
x: left + (offsets.tl.dx / 100) * widthNorm,
|
|
52
|
+
y: top + (offsets.tl.dy / 100) * heightNorm
|
|
53
|
+
},
|
|
54
|
+
tr: {
|
|
55
|
+
x: right + (offsets.tr.dx / 100) * widthNorm,
|
|
56
|
+
y: top + (offsets.tr.dy / 100) * heightNorm
|
|
57
|
+
},
|
|
58
|
+
br: {
|
|
59
|
+
x: right + (offsets.br.dx / 100) * widthNorm,
|
|
60
|
+
y: bottom + (offsets.br.dy / 100) * heightNorm
|
|
61
|
+
},
|
|
62
|
+
bl: {
|
|
63
|
+
x: left + (offsets.bl.dx / 100) * widthNorm,
|
|
64
|
+
y: bottom + (offsets.bl.dy / 100) * heightNorm
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* @deprecated Use createDefaultCornerOffsets instead
|
|
70
|
+
* Creates default corner points for a rectangular overlay
|
|
71
|
+
* Uses the overlay's position to calculate initial corner positions
|
|
72
|
+
*/
|
|
73
|
+
export function createDefaultCornerPoints(position) {
|
|
74
|
+
// Convert percentage position to normalized 0-1 coordinates
|
|
75
|
+
const left = (position.x - position.width / 2) / 100;
|
|
76
|
+
const right = (position.x + position.width / 2) / 100;
|
|
77
|
+
const top = (position.y - position.height / 2) / 100;
|
|
78
|
+
const bottom = (position.y + position.height / 2) / 100;
|
|
79
|
+
return {
|
|
80
|
+
tl: { x: left, y: top },
|
|
81
|
+
tr: { x: right, y: top },
|
|
82
|
+
br: { x: right, y: bottom },
|
|
83
|
+
bl: { x: left, y: bottom }
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Migrate old absolute corners to new offset-based format
|
|
88
|
+
* Call this when loading projects with old corner format
|
|
89
|
+
*/
|
|
90
|
+
export function migrateToCornerOffsets(position, corners) {
|
|
91
|
+
// Calculate what the natural corners would be
|
|
92
|
+
const naturalLeft = (position.x - position.width / 2) / 100;
|
|
93
|
+
const naturalRight = (position.x + position.width / 2) / 100;
|
|
94
|
+
const naturalTop = (position.y - position.height / 2) / 100;
|
|
95
|
+
const naturalBottom = (position.y + position.height / 2) / 100;
|
|
96
|
+
const widthNorm = position.width / 100;
|
|
97
|
+
const heightNorm = position.height / 100;
|
|
98
|
+
// Calculate offsets as percentage of overlay dimensions
|
|
99
|
+
return {
|
|
100
|
+
tl: {
|
|
101
|
+
dx: widthNorm > 0 ? ((corners.tl.x - naturalLeft) / widthNorm) * 100 : 0,
|
|
102
|
+
dy: heightNorm > 0 ? ((corners.tl.y - naturalTop) / heightNorm) * 100 : 0
|
|
103
|
+
},
|
|
104
|
+
tr: {
|
|
105
|
+
dx: widthNorm > 0 ? ((corners.tr.x - naturalRight) / widthNorm) * 100 : 0,
|
|
106
|
+
dy: heightNorm > 0 ? ((corners.tr.y - naturalTop) / heightNorm) * 100 : 0
|
|
107
|
+
},
|
|
108
|
+
br: {
|
|
109
|
+
dx: widthNorm > 0 ? ((corners.br.x - naturalRight) / widthNorm) * 100 : 0,
|
|
110
|
+
dy: heightNorm > 0 ? ((corners.br.y - naturalBottom) / heightNorm) * 100 : 0
|
|
111
|
+
},
|
|
112
|
+
bl: {
|
|
113
|
+
dx: widthNorm > 0 ? ((corners.bl.x - naturalLeft) / widthNorm) * 100 : 0,
|
|
114
|
+
dy: heightNorm > 0 ? ((corners.bl.y - naturalBottom) / heightNorm) * 100 : 0
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Recording Session Metadata
|
|
120
|
+
// ============================================================================
|
|
121
|
+
/**
|
|
122
|
+
* Session metadata for a recorded participation.
|
|
123
|
+
*
|
|
124
|
+
* Captures everything needed to re-publish the participant's recording
|
|
125
|
+
* as a new monetizable version with dynamic overlays and ads.
|
|
126
|
+
*
|
|
127
|
+
* Classify an overlay for recording: should it be burned into the video
|
|
128
|
+
* (static visual) or kept as dynamic metadata (interactive/monetizable)?
|
|
129
|
+
*
|
|
130
|
+
* Some overlays (e.g., clickable images) are BOTH — burned visually into
|
|
131
|
+
* the recording but also preserved as dynamic metadata so their click URL
|
|
132
|
+
* can be restored when re-publishing.
|
|
133
|
+
*/
|
|
134
|
+
export function classifyOverlayForRecording(overlay) {
|
|
135
|
+
switch (overlay.type) {
|
|
136
|
+
case 'cta':
|
|
137
|
+
return { burnable: false, dynamic: true };
|
|
138
|
+
case 'image': {
|
|
139
|
+
const content = overlay.content;
|
|
140
|
+
const hasClickUrl = !!(content.clickUrl?.trim());
|
|
141
|
+
// Images with click URLs are dynamic (ads/CTAs) — don't burn in
|
|
142
|
+
return { burnable: !hasClickUrl, dynamic: hasClickUrl };
|
|
143
|
+
}
|
|
144
|
+
// sprite-sequence and video: burnable in Phase 2 (not yet)
|
|
145
|
+
case 'sprite-sequence':
|
|
146
|
+
case 'video':
|
|
147
|
+
return { burnable: false, dynamic: false };
|
|
148
|
+
default:
|
|
149
|
+
// text, emoji, shape — always burn in
|
|
150
|
+
return { burnable: true, dynamic: false };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export const RECORDING_TIER_LIMITS = {
|
|
154
|
+
free: {
|
|
155
|
+
maxDurationMs: 3 * 60 * 1000, // 3 minutes
|
|
156
|
+
maxFileSize: 100 * 1024 * 1024, // 100 MB
|
|
157
|
+
label: 'Free (3 min)'
|
|
158
|
+
},
|
|
159
|
+
standard: {
|
|
160
|
+
maxDurationMs: 15 * 60 * 1000, // 15 minutes
|
|
161
|
+
maxFileSize: 300 * 1024 * 1024, // 300 MB
|
|
162
|
+
label: 'Standard (15 min)'
|
|
163
|
+
},
|
|
164
|
+
professional: {
|
|
165
|
+
maxDurationMs: 60 * 60 * 1000, // 60 minutes
|
|
166
|
+
maxFileSize: 500 * 1024 * 1024, // 500 MB
|
|
167
|
+
label: 'Professional (60 min)'
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Standard Ad Slot Templates (Default Set)
|
|
172
|
+
// ============================================================================
|
|
173
|
+
/**
|
|
174
|
+
* Standard ad slot templates following IAB guidelines
|
|
175
|
+
* These are the default templates that Admetise provides.
|
|
176
|
+
* Can be extended/modified via Admetise admin.
|
|
177
|
+
*/
|
|
178
|
+
export const STANDARD_AD_TEMPLATES = [
|
|
179
|
+
// Video Ad Templates
|
|
180
|
+
{
|
|
181
|
+
id: 'preroll-15',
|
|
182
|
+
name: 'Standard Preroll (15s)',
|
|
183
|
+
description: 'Video ad before content starts, up to 15 seconds',
|
|
184
|
+
type: 'preroll',
|
|
185
|
+
category: 'video',
|
|
186
|
+
duration: 15,
|
|
187
|
+
maxDuration: 15,
|
|
188
|
+
active: true,
|
|
189
|
+
sortOrder: 1,
|
|
190
|
+
icon: 'play-circle'
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'preroll-30',
|
|
194
|
+
name: 'Extended Preroll (30s)',
|
|
195
|
+
description: 'Video ad before content starts, up to 30 seconds',
|
|
196
|
+
type: 'preroll',
|
|
197
|
+
category: 'video',
|
|
198
|
+
duration: 30,
|
|
199
|
+
maxDuration: 30,
|
|
200
|
+
active: true,
|
|
201
|
+
sortOrder: 2,
|
|
202
|
+
icon: 'play-circle'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'midroll-15',
|
|
206
|
+
name: 'Standard Midroll (15s)',
|
|
207
|
+
description: 'Video ad during content, up to 15 seconds',
|
|
208
|
+
type: 'midroll',
|
|
209
|
+
category: 'video',
|
|
210
|
+
duration: 15,
|
|
211
|
+
maxDuration: 15,
|
|
212
|
+
active: true,
|
|
213
|
+
sortOrder: 10,
|
|
214
|
+
icon: 'pause-circle'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'midroll-30',
|
|
218
|
+
name: 'Extended Midroll (30s)',
|
|
219
|
+
description: 'Video ad during content, up to 30 seconds',
|
|
220
|
+
type: 'midroll',
|
|
221
|
+
category: 'video',
|
|
222
|
+
duration: 30,
|
|
223
|
+
maxDuration: 30,
|
|
224
|
+
active: true,
|
|
225
|
+
sortOrder: 11,
|
|
226
|
+
icon: 'pause-circle'
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'postroll-15',
|
|
230
|
+
name: 'Standard Postroll (15s)',
|
|
231
|
+
description: 'Video ad after content ends, up to 15 seconds',
|
|
232
|
+
type: 'postroll',
|
|
233
|
+
category: 'video',
|
|
234
|
+
duration: 15,
|
|
235
|
+
maxDuration: 15,
|
|
236
|
+
active: true,
|
|
237
|
+
sortOrder: 20,
|
|
238
|
+
icon: 'stop-circle'
|
|
239
|
+
},
|
|
240
|
+
// Display Overlay Templates (IAB Standard Sizes)
|
|
241
|
+
{
|
|
242
|
+
id: 'overlay-leaderboard',
|
|
243
|
+
name: 'Leaderboard Overlay (728×90)',
|
|
244
|
+
description: 'Bottom banner overlay, IAB standard leaderboard',
|
|
245
|
+
type: 'overlay',
|
|
246
|
+
category: 'display',
|
|
247
|
+
width: 728,
|
|
248
|
+
height: 90,
|
|
249
|
+
position: 'bottom-left',
|
|
250
|
+
iabSize: '728x90',
|
|
251
|
+
active: true,
|
|
252
|
+
sortOrder: 30,
|
|
253
|
+
icon: 'rectangle-horizontal'
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'overlay-medium-rectangle',
|
|
257
|
+
name: 'Medium Rectangle Overlay (300×250)',
|
|
258
|
+
description: 'Corner overlay, IAB standard medium rectangle',
|
|
259
|
+
type: 'overlay',
|
|
260
|
+
category: 'display',
|
|
261
|
+
width: 300,
|
|
262
|
+
height: 250,
|
|
263
|
+
position: 'bottom-right',
|
|
264
|
+
iabSize: '300x250',
|
|
265
|
+
active: true,
|
|
266
|
+
sortOrder: 31,
|
|
267
|
+
icon: 'square'
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'overlay-banner',
|
|
271
|
+
name: 'Banner Overlay (468×60)',
|
|
272
|
+
description: 'Compact bottom banner overlay',
|
|
273
|
+
type: 'overlay',
|
|
274
|
+
category: 'display',
|
|
275
|
+
width: 468,
|
|
276
|
+
height: 60,
|
|
277
|
+
position: 'bottom-left',
|
|
278
|
+
iabSize: '468x60',
|
|
279
|
+
active: true,
|
|
280
|
+
sortOrder: 32,
|
|
281
|
+
icon: 'rectangle-horizontal'
|
|
282
|
+
},
|
|
283
|
+
// Companion Ad Templates
|
|
284
|
+
{
|
|
285
|
+
id: 'companion-medium-rectangle',
|
|
286
|
+
name: 'Companion Medium Rectangle (300×250)',
|
|
287
|
+
description: 'Companion ad displayed alongside video player',
|
|
288
|
+
type: 'companion',
|
|
289
|
+
category: 'companion',
|
|
290
|
+
width: 300,
|
|
291
|
+
height: 250,
|
|
292
|
+
iabSize: '300x250',
|
|
293
|
+
active: true,
|
|
294
|
+
sortOrder: 40,
|
|
295
|
+
icon: 'layout-sidebar-right'
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 'companion-wide-skyscraper',
|
|
299
|
+
name: 'Companion Wide Skyscraper (160×600)',
|
|
300
|
+
description: 'Tall companion ad beside video player',
|
|
301
|
+
type: 'companion',
|
|
302
|
+
category: 'companion',
|
|
303
|
+
width: 160,
|
|
304
|
+
height: 600,
|
|
305
|
+
iabSize: '160x600',
|
|
306
|
+
active: true,
|
|
307
|
+
sortOrder: 41,
|
|
308
|
+
icon: 'layout-sidebar-right'
|
|
309
|
+
}
|
|
310
|
+
];
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Utility Functions
|
|
313
|
+
// ============================================================================
|
|
314
|
+
/**
|
|
315
|
+
* Find a template by ID
|
|
316
|
+
*
|
|
317
|
+
* @param templateId - The template ID to find
|
|
318
|
+
* @param templates - Array of templates (defaults to STANDARD_AD_TEMPLATES)
|
|
319
|
+
* @returns The template or undefined
|
|
320
|
+
*/
|
|
321
|
+
export function findTemplate(templateId, templates = STANDARD_AD_TEMPLATES) {
|
|
322
|
+
return templates.find((t) => t.id === templateId);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Maps an AdSlotPlacement to an AdZone using template data
|
|
326
|
+
*
|
|
327
|
+
* @param placement - The placement from Includu
|
|
328
|
+
* @param template - The template definition from Admetise
|
|
329
|
+
* @param fps - Frames per second of the video
|
|
330
|
+
* @returns AdZone for Admetise
|
|
331
|
+
*/
|
|
332
|
+
export function mapPlacementToZone(placement, template, fps) {
|
|
333
|
+
return {
|
|
334
|
+
id: placement.id,
|
|
335
|
+
templateId: template.id,
|
|
336
|
+
type: template.type,
|
|
337
|
+
enabled: true,
|
|
338
|
+
presetPosition: template.position,
|
|
339
|
+
width: template.width,
|
|
340
|
+
height: template.height,
|
|
341
|
+
triggerTime: placement.triggerFrame ? placement.triggerFrame / fps : undefined,
|
|
342
|
+
triggerPercent: placement.triggerPercent,
|
|
343
|
+
maxDuration: template.maxDuration,
|
|
344
|
+
centerPosition: placement.position,
|
|
345
|
+
scale: placement.scale,
|
|
346
|
+
rotation: placement.rotation,
|
|
347
|
+
rotationX: placement.rotationX,
|
|
348
|
+
rotationY: placement.rotationY,
|
|
349
|
+
rotationEnabled: placement.rotationEnabled,
|
|
350
|
+
perspective: placement.perspective
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* @deprecated Use mapPlacementToZone instead
|
|
355
|
+
* Maps an Includu AdSlot to an Admetise AdZone (legacy support)
|
|
356
|
+
*/
|
|
357
|
+
export function mapAdSlotToZone(slot, fps) {
|
|
358
|
+
return {
|
|
359
|
+
id: slot.id,
|
|
360
|
+
templateId: slot.templateId || `legacy-${slot.type}`,
|
|
361
|
+
type: slot.type,
|
|
362
|
+
enabled: true,
|
|
363
|
+
presetPosition: slot.position,
|
|
364
|
+
width: slot.width,
|
|
365
|
+
height: slot.height,
|
|
366
|
+
triggerTime: slot.triggerFrame ? slot.triggerFrame / fps : undefined,
|
|
367
|
+
triggerPercent: slot.triggerPercent,
|
|
368
|
+
maxDuration: slot.maxDuration
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Maps all placements from a manifest to AdZones
|
|
373
|
+
*
|
|
374
|
+
* @param manifest - Published manifest with monetization config
|
|
375
|
+
* @param templates - Available templates (defaults to STANDARD_AD_TEMPLATES)
|
|
376
|
+
* @returns Array of AdZones, or empty array if monetization disabled
|
|
377
|
+
*/
|
|
378
|
+
export function mapManifestToZones(manifest, templates = STANDARD_AD_TEMPLATES) {
|
|
379
|
+
if (!manifest.monetization?.enabled) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
// Prefer new placements field, fall back to legacy adSlots
|
|
383
|
+
if (manifest.monetization.placements?.length) {
|
|
384
|
+
return manifest.monetization.placements
|
|
385
|
+
.map((placement) => {
|
|
386
|
+
const template = findTemplate(placement.templateId, templates);
|
|
387
|
+
if (!template) {
|
|
388
|
+
console.warn(`Template not found: ${placement.templateId}`);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return mapPlacementToZone(placement, template, manifest.fps);
|
|
392
|
+
})
|
|
393
|
+
.filter((zone) => zone !== null);
|
|
394
|
+
}
|
|
395
|
+
// Legacy support for adSlots
|
|
396
|
+
if (manifest.monetization.adSlots?.length) {
|
|
397
|
+
return manifest.monetization.adSlots.map((slot) => mapAdSlotToZone(slot, manifest.fps));
|
|
398
|
+
}
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get templates grouped by category
|
|
403
|
+
*
|
|
404
|
+
* @param templates - Array of templates (defaults to STANDARD_AD_TEMPLATES)
|
|
405
|
+
* @returns Object with templates grouped by category
|
|
406
|
+
*/
|
|
407
|
+
export function getTemplatesByCategory(templates = STANDARD_AD_TEMPLATES) {
|
|
408
|
+
const result = {
|
|
409
|
+
video: [],
|
|
410
|
+
display: [],
|
|
411
|
+
companion: []
|
|
412
|
+
};
|
|
413
|
+
for (const template of templates.filter((t) => t.active)) {
|
|
414
|
+
result[template.category].push(template);
|
|
415
|
+
}
|
|
416
|
+
// Sort by sortOrder
|
|
417
|
+
for (const category of Object.keys(result)) {
|
|
418
|
+
result[category].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Get templates filtered by type
|
|
424
|
+
*
|
|
425
|
+
* @param type - The ad slot type to filter by
|
|
426
|
+
* @param templates - Array of templates (defaults to STANDARD_AD_TEMPLATES)
|
|
427
|
+
* @returns Filtered templates
|
|
428
|
+
*/
|
|
429
|
+
export function getTemplatesByType(type, templates = STANDARD_AD_TEMPLATES) {
|
|
430
|
+
return templates.filter((t) => t.active && t.type === type);
|
|
431
|
+
}
|
|
432
|
+
// ============================================================================
|
|
433
|
+
// Perspective Transform Utilities
|
|
434
|
+
// ============================================================================
|
|
435
|
+
export { computeMatrix3d, isValidQuad } from './perspective';
|
|
436
|
+
// ============================================================================
|
|
437
|
+
// Layer 2: 3D Composite Types
|
|
438
|
+
// ============================================================================
|
|
439
|
+
export * from './layer2';
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// Admetise Campaign Types (F1)
|
|
442
|
+
// ============================================================================
|
|
443
|
+
export * from './campaigns';
|
|
444
|
+
// ============================================================================
|
|
445
|
+
// Admetise Ad Serving Contracts (Player ↔ Admetise boundary)
|
|
446
|
+
// ============================================================================
|
|
447
|
+
export * from './adserving';
|
|
448
|
+
// ============================================================================
|
|
449
|
+
// Gausst Coordinate System Utilities
|
|
450
|
+
// ============================================================================
|
|
451
|
+
export * from './gausst';
|
|
452
|
+
/**
|
|
453
|
+
* Standard overlay template presets
|
|
454
|
+
* Designed for ease of use across all devices
|
|
455
|
+
*/
|
|
456
|
+
export const OVERLAY_TEMPLATE_PRESETS = [
|
|
457
|
+
// ==================== LOWER THIRDS ====================
|
|
458
|
+
{
|
|
459
|
+
id: 'lower-third-name',
|
|
460
|
+
name: 'Name Card',
|
|
461
|
+
description: 'Speaker name and title',
|
|
462
|
+
category: 'lower-thirds',
|
|
463
|
+
icon: '👤',
|
|
464
|
+
type: 'text',
|
|
465
|
+
position: { x: 25, y: 85, width: 40, height: 12 },
|
|
466
|
+
durationSeconds: 5,
|
|
467
|
+
textDefaults: {
|
|
468
|
+
text: 'Speaker Name',
|
|
469
|
+
fontSize: 2.5,
|
|
470
|
+
fontFamily: 'Inter, sans-serif',
|
|
471
|
+
fontWeight: 'bold',
|
|
472
|
+
color: '#ffffff',
|
|
473
|
+
textAlign: 'left',
|
|
474
|
+
background: {
|
|
475
|
+
color: 'rgba(0, 0, 0, 0.7)',
|
|
476
|
+
opacity: 1,
|
|
477
|
+
padding: { x: 12, y: 8 },
|
|
478
|
+
scale: 1,
|
|
479
|
+
borderRadius: 4
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
id: 'lower-third-subtitle',
|
|
485
|
+
name: 'Subtitle Bar',
|
|
486
|
+
description: 'Full-width subtitle or caption',
|
|
487
|
+
category: 'lower-thirds',
|
|
488
|
+
icon: '💬',
|
|
489
|
+
type: 'text',
|
|
490
|
+
position: { x: 50, y: 90, width: 80, height: 8 },
|
|
491
|
+
durationSeconds: 4,
|
|
492
|
+
textDefaults: {
|
|
493
|
+
text: 'Your subtitle text here',
|
|
494
|
+
fontSize: 2,
|
|
495
|
+
fontFamily: 'Inter, sans-serif',
|
|
496
|
+
fontWeight: 'normal',
|
|
497
|
+
color: '#ffffff',
|
|
498
|
+
textAlign: 'center',
|
|
499
|
+
background: {
|
|
500
|
+
color: 'rgba(0, 0, 0, 0.6)',
|
|
501
|
+
opacity: 1,
|
|
502
|
+
padding: { x: 8, y: 6 },
|
|
503
|
+
scale: 1,
|
|
504
|
+
borderRadius: 4
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
// ==================== TITLES ====================
|
|
509
|
+
{
|
|
510
|
+
id: 'title-centered',
|
|
511
|
+
name: 'Center Title',
|
|
512
|
+
description: 'Large centered title',
|
|
513
|
+
category: 'titles',
|
|
514
|
+
icon: '🎬',
|
|
515
|
+
type: 'text',
|
|
516
|
+
position: { x: 50, y: 50, width: 70, height: 15 },
|
|
517
|
+
durationSeconds: 3,
|
|
518
|
+
textDefaults: {
|
|
519
|
+
text: 'Your Title',
|
|
520
|
+
fontSize: 4,
|
|
521
|
+
fontFamily: 'Inter, sans-serif',
|
|
522
|
+
fontWeight: 'bold',
|
|
523
|
+
color: '#ffffff',
|
|
524
|
+
textAlign: 'center',
|
|
525
|
+
background: {
|
|
526
|
+
color: 'rgba(0, 0, 0, 0.5)',
|
|
527
|
+
opacity: 1,
|
|
528
|
+
padding: { x: 15, y: 10 },
|
|
529
|
+
scale: 1,
|
|
530
|
+
borderRadius: 8
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
id: 'title-chapter',
|
|
536
|
+
name: 'Chapter Title',
|
|
537
|
+
description: 'Section or chapter heading',
|
|
538
|
+
category: 'titles',
|
|
539
|
+
icon: '📖',
|
|
540
|
+
type: 'text',
|
|
541
|
+
position: { x: 50, y: 15, width: 50, height: 10 },
|
|
542
|
+
durationSeconds: 3,
|
|
543
|
+
textDefaults: {
|
|
544
|
+
text: 'Chapter 1',
|
|
545
|
+
fontSize: 3,
|
|
546
|
+
fontFamily: 'Inter, sans-serif',
|
|
547
|
+
fontWeight: 'bold',
|
|
548
|
+
color: '#ffffff',
|
|
549
|
+
textAlign: 'center',
|
|
550
|
+
background: {
|
|
551
|
+
color: 'rgba(229, 57, 53, 0.9)',
|
|
552
|
+
opacity: 1,
|
|
553
|
+
padding: { x: 12, y: 6 },
|
|
554
|
+
scale: 1,
|
|
555
|
+
borderRadius: 4
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
// ==================== BADGES ====================
|
|
560
|
+
{
|
|
561
|
+
id: 'badge-corner',
|
|
562
|
+
name: 'Corner Badge',
|
|
563
|
+
description: 'Small badge in corner',
|
|
564
|
+
category: 'badges',
|
|
565
|
+
icon: '🏷️',
|
|
566
|
+
type: 'text',
|
|
567
|
+
position: { x: 90, y: 10, width: 15, height: 6 },
|
|
568
|
+
durationSeconds: 0, // 0 = show for entire video
|
|
569
|
+
textDefaults: {
|
|
570
|
+
text: 'LIVE',
|
|
571
|
+
fontSize: 1.5,
|
|
572
|
+
fontFamily: 'Inter, sans-serif',
|
|
573
|
+
fontWeight: 'bold',
|
|
574
|
+
color: '#ffffff',
|
|
575
|
+
textAlign: 'center',
|
|
576
|
+
background: {
|
|
577
|
+
color: 'rgba(229, 57, 53, 1)',
|
|
578
|
+
opacity: 1,
|
|
579
|
+
padding: { x: 10, y: 5 },
|
|
580
|
+
scale: 1,
|
|
581
|
+
borderRadius: 4
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
id: 'badge-new',
|
|
587
|
+
name: 'NEW Badge',
|
|
588
|
+
description: 'Highlight new content',
|
|
589
|
+
category: 'badges',
|
|
590
|
+
icon: '✨',
|
|
591
|
+
type: 'text',
|
|
592
|
+
position: { x: 10, y: 10, width: 12, height: 5 },
|
|
593
|
+
durationSeconds: 5,
|
|
594
|
+
textDefaults: {
|
|
595
|
+
text: 'NEW',
|
|
596
|
+
fontSize: 1.2,
|
|
597
|
+
fontFamily: 'Inter, sans-serif',
|
|
598
|
+
fontWeight: 'bold',
|
|
599
|
+
color: '#000000',
|
|
600
|
+
textAlign: 'center',
|
|
601
|
+
background: {
|
|
602
|
+
color: 'rgba(255, 235, 59, 1)',
|
|
603
|
+
opacity: 1,
|
|
604
|
+
padding: { x: 8, y: 4 },
|
|
605
|
+
scale: 1,
|
|
606
|
+
borderRadius: 12
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
// ==================== CTA ====================
|
|
611
|
+
{
|
|
612
|
+
id: 'cta-subscribe',
|
|
613
|
+
name: 'Subscribe Button',
|
|
614
|
+
description: 'Call to action button',
|
|
615
|
+
category: 'cta',
|
|
616
|
+
icon: '🔔',
|
|
617
|
+
type: 'cta',
|
|
618
|
+
position: { x: 85, y: 85, width: 20, height: 8 },
|
|
619
|
+
durationSeconds: 10,
|
|
620
|
+
ctaDefaults: {
|
|
621
|
+
text: 'Subscribe',
|
|
622
|
+
url: '#subscribe',
|
|
623
|
+
style: {
|
|
624
|
+
backgroundColor: '#E53935',
|
|
625
|
+
textColor: '#ffffff',
|
|
626
|
+
borderRadius: 8,
|
|
627
|
+
fontSize: 1.2, // em units
|
|
628
|
+
fontWeight: 'bold',
|
|
629
|
+
padding: { x: 20, y: 12 }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
id: 'cta-learn-more',
|
|
635
|
+
name: 'Learn More',
|
|
636
|
+
description: 'Link to more info',
|
|
637
|
+
category: 'cta',
|
|
638
|
+
icon: '🔗',
|
|
639
|
+
type: 'cta',
|
|
640
|
+
position: { x: 50, y: 80, width: 25, height: 8 },
|
|
641
|
+
durationSeconds: 8,
|
|
642
|
+
ctaDefaults: {
|
|
643
|
+
text: 'Learn More',
|
|
644
|
+
url: '#',
|
|
645
|
+
style: {
|
|
646
|
+
backgroundColor: '#1976D2',
|
|
647
|
+
textColor: '#ffffff',
|
|
648
|
+
borderRadius: 24,
|
|
649
|
+
fontSize: 1.0, // em units
|
|
650
|
+
fontWeight: 'bold',
|
|
651
|
+
padding: { x: 24, y: 10 }
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: 'cta-picture-frame',
|
|
657
|
+
name: 'Clickable Picture Frame',
|
|
658
|
+
description: 'Clickable image overlay for promotions and links',
|
|
659
|
+
category: 'cta',
|
|
660
|
+
icon: '🖼️',
|
|
661
|
+
type: 'image',
|
|
662
|
+
position: { x: 50, y: 50, width: 30, height: 15 },
|
|
663
|
+
durationSeconds: 5,
|
|
664
|
+
imageDefaults: {
|
|
665
|
+
src: '',
|
|
666
|
+
alt: 'Clickable Picture Frame',
|
|
667
|
+
fit: 'contain',
|
|
668
|
+
opacity: 1,
|
|
669
|
+
borderRadius: 0,
|
|
670
|
+
clickUrl: '#'
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
// ==================== SOCIAL ====================
|
|
674
|
+
{
|
|
675
|
+
id: 'social-reaction',
|
|
676
|
+
name: 'Reaction',
|
|
677
|
+
description: 'Emoji reaction',
|
|
678
|
+
category: 'social',
|
|
679
|
+
icon: '😀',
|
|
680
|
+
type: 'emoji',
|
|
681
|
+
position: { x: 15, y: 50, width: 10, height: 10 },
|
|
682
|
+
durationSeconds: 2,
|
|
683
|
+
emojiDefaults: {
|
|
684
|
+
emoji: '👍',
|
|
685
|
+
size: 4
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
id: 'social-heart',
|
|
690
|
+
name: 'Heart',
|
|
691
|
+
description: 'Like/love reaction',
|
|
692
|
+
category: 'social',
|
|
693
|
+
icon: '❤️',
|
|
694
|
+
type: 'emoji',
|
|
695
|
+
position: { x: 85, y: 50, width: 10, height: 10 },
|
|
696
|
+
durationSeconds: 2,
|
|
697
|
+
emojiDefaults: {
|
|
698
|
+
emoji: '❤️',
|
|
699
|
+
size: 4
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
// ==================== GRAPHICS & LOGOS ====================
|
|
703
|
+
{
|
|
704
|
+
id: 'graphics-logo-corner',
|
|
705
|
+
name: 'Logo Bug',
|
|
706
|
+
description: 'Small logo in corner (watermark)',
|
|
707
|
+
category: 'graphics',
|
|
708
|
+
icon: '🏷️',
|
|
709
|
+
type: 'image',
|
|
710
|
+
position: { x: 90, y: 10, width: 12, height: 12 },
|
|
711
|
+
durationSeconds: 0, // 0 = full video duration
|
|
712
|
+
imageDefaults: {
|
|
713
|
+
src: '',
|
|
714
|
+
alt: 'Logo',
|
|
715
|
+
fit: 'contain',
|
|
716
|
+
opacity: 0.8,
|
|
717
|
+
borderRadius: 0
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 'graphics-watermark-center',
|
|
722
|
+
name: 'Center Watermark',
|
|
723
|
+
description: 'Semi-transparent centered logo',
|
|
724
|
+
category: 'graphics',
|
|
725
|
+
icon: '💧',
|
|
726
|
+
type: 'image',
|
|
727
|
+
position: { x: 50, y: 50, width: 30, height: 30 },
|
|
728
|
+
durationSeconds: 0,
|
|
729
|
+
imageDefaults: {
|
|
730
|
+
src: '',
|
|
731
|
+
alt: 'Watermark',
|
|
732
|
+
fit: 'contain',
|
|
733
|
+
opacity: 0.3,
|
|
734
|
+
borderRadius: 0
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
id: 'graphics-banner',
|
|
739
|
+
name: 'Image Banner',
|
|
740
|
+
description: 'Full-width image banner',
|
|
741
|
+
category: 'graphics',
|
|
742
|
+
icon: '🖼️',
|
|
743
|
+
type: 'image',
|
|
744
|
+
position: { x: 50, y: 90, width: 80, height: 15 },
|
|
745
|
+
durationSeconds: 5,
|
|
746
|
+
imageDefaults: {
|
|
747
|
+
src: '',
|
|
748
|
+
alt: 'Banner',
|
|
749
|
+
fit: 'contain',
|
|
750
|
+
opacity: 1,
|
|
751
|
+
borderRadius: 8
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
id: 'graphics-sponsor',
|
|
756
|
+
name: 'Sponsor Logo',
|
|
757
|
+
description: 'Sponsor or partner logo placement',
|
|
758
|
+
category: 'graphics',
|
|
759
|
+
icon: '🤝',
|
|
760
|
+
type: 'image',
|
|
761
|
+
position: { x: 10, y: 10, width: 15, height: 10 },
|
|
762
|
+
durationSeconds: 10,
|
|
763
|
+
imageDefaults: {
|
|
764
|
+
src: '',
|
|
765
|
+
alt: 'Sponsor',
|
|
766
|
+
fit: 'contain',
|
|
767
|
+
opacity: 1,
|
|
768
|
+
borderRadius: 4
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
id: 'social-picture-frame',
|
|
773
|
+
name: 'Picture Frame',
|
|
774
|
+
description: 'Image overlay for photos and graphics',
|
|
775
|
+
category: 'social',
|
|
776
|
+
icon: '🖼️',
|
|
777
|
+
type: 'image',
|
|
778
|
+
position: { x: 50, y: 50, width: 30, height: 15 },
|
|
779
|
+
durationSeconds: 5,
|
|
780
|
+
imageDefaults: {
|
|
781
|
+
src: '',
|
|
782
|
+
alt: 'Picture Frame',
|
|
783
|
+
fit: 'contain',
|
|
784
|
+
opacity: 1,
|
|
785
|
+
borderRadius: 0
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
// ==================== MOTION GRAPHICS ====================
|
|
789
|
+
{
|
|
790
|
+
id: 'motiongfx-sprite',
|
|
791
|
+
name: 'Sprite Animation',
|
|
792
|
+
description: 'Animated sprite sequence (pre-rendered effects, mascots, particles)',
|
|
793
|
+
category: 'motion-gfx',
|
|
794
|
+
icon: '🎬',
|
|
795
|
+
type: 'sprite-sequence',
|
|
796
|
+
position: { x: 50, y: 50, width: 20, height: 20 },
|
|
797
|
+
durationSeconds: 5,
|
|
798
|
+
spriteDefaults: {
|
|
799
|
+
spritesheetUrl: '',
|
|
800
|
+
frameWidth: 256,
|
|
801
|
+
frameHeight: 256,
|
|
802
|
+
frameCount: 1,
|
|
803
|
+
columns: 1,
|
|
804
|
+
framesPerSecond: 12,
|
|
805
|
+
loop: true,
|
|
806
|
+
opacity: 1
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
id: 'motiongfx-video',
|
|
811
|
+
name: 'Video Overlay',
|
|
812
|
+
description: 'Video with alpha channel (WebM VP9, animated effects, transitions)',
|
|
813
|
+
category: 'motion-gfx',
|
|
814
|
+
icon: '🎥',
|
|
815
|
+
type: 'video',
|
|
816
|
+
position: { x: 50, y: 50, width: 30, height: 30 },
|
|
817
|
+
durationSeconds: 5,
|
|
818
|
+
videoDefaults: {
|
|
819
|
+
videoUrl: '',
|
|
820
|
+
loop: true,
|
|
821
|
+
muted: true,
|
|
822
|
+
opacity: 1,
|
|
823
|
+
fit: 'contain'
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
];
|
|
827
|
+
/**
|
|
828
|
+
* Get overlay templates by category
|
|
829
|
+
*/
|
|
830
|
+
export function getOverlayTemplatesByCategory(category) {
|
|
831
|
+
return OVERLAY_TEMPLATE_PRESETS.filter((t) => t.category === category);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Find an overlay template by ID
|
|
835
|
+
*/
|
|
836
|
+
export function findOverlayTemplate(id) {
|
|
837
|
+
return OVERLAY_TEMPLATE_PRESETS.find((t) => t.id === id);
|
|
838
|
+
}
|
|
839
|
+
/** Category labels for display */
|
|
840
|
+
const CATEGORY_LABELS = {
|
|
841
|
+
'lower-thirds': 'Lower Thirds',
|
|
842
|
+
titles: 'Titles',
|
|
843
|
+
badges: 'Badges',
|
|
844
|
+
cta: 'Call to Action',
|
|
845
|
+
social: 'Social & Reactions',
|
|
846
|
+
graphics: 'Graphics & Logos',
|
|
847
|
+
'motion-gfx': 'Motion Graphics'
|
|
848
|
+
};
|
|
849
|
+
/**
|
|
850
|
+
* Get all overlay template categories with their templates
|
|
851
|
+
*/
|
|
852
|
+
export function getOverlayTemplateCategoriesWithTemplates() {
|
|
853
|
+
const categories = ['lower-thirds', 'titles', 'badges', 'cta', 'social', 'graphics'];
|
|
854
|
+
return categories.map((category) => ({
|
|
855
|
+
category,
|
|
856
|
+
label: CATEGORY_LABELS[category],
|
|
857
|
+
templates: getOverlayTemplatesByCategory(category)
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Get overlay template categories for the Overlays track (excludes branding categories)
|
|
862
|
+
*/
|
|
863
|
+
export function getOverlayTrackCategories() {
|
|
864
|
+
// Categories that go on the Overlays timeline track
|
|
865
|
+
const overlayCategories = ['lower-thirds', 'titles', 'badges', 'cta', 'social'];
|
|
866
|
+
return overlayCategories.map((category) => ({
|
|
867
|
+
category,
|
|
868
|
+
label: CATEGORY_LABELS[category],
|
|
869
|
+
templates: getOverlayTemplatesByCategory(category)
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Get branding template categories for the Branding track
|
|
874
|
+
*/
|
|
875
|
+
export function getBrandingTrackCategories() {
|
|
876
|
+
// Categories that go on the Branding timeline track
|
|
877
|
+
const brandingCategories = ['graphics'];
|
|
878
|
+
return brandingCategories.map((category) => ({
|
|
879
|
+
category,
|
|
880
|
+
label: CATEGORY_LABELS[category],
|
|
881
|
+
templates: getOverlayTemplatesByCategory(category)
|
|
882
|
+
}));
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Get Motion Graphics template categories for the Motion GFX track
|
|
886
|
+
*/
|
|
887
|
+
export function getMotionGfxTrackCategories() {
|
|
888
|
+
// Categories that go on the Motion GFX timeline track
|
|
889
|
+
const motionGfxCategories = ['motion-gfx'];
|
|
890
|
+
return motionGfxCategories.map((category) => ({
|
|
891
|
+
category,
|
|
892
|
+
label: CATEGORY_LABELS[category],
|
|
893
|
+
templates: getOverlayTemplatesByCategory(category)
|
|
894
|
+
}));
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* @deprecated Use getMotionGfxTrackCategories instead
|
|
898
|
+
*/
|
|
899
|
+
export function get3DTrackCategories() {
|
|
900
|
+
return getMotionGfxTrackCategories();
|
|
901
|
+
}
|
|
902
|
+
// ============================================================================
|
|
903
|
+
// Coordinate System (Three.js / Metric)
|
|
904
|
+
// ============================================================================
|
|
905
|
+
/**
|
|
906
|
+
* Coordinate system constants for Three.js video compositing
|
|
907
|
+
*
|
|
908
|
+
* Based on Gausst legacy code (Patent US8761580B2):
|
|
909
|
+
* - Y-up axis (Three.js default)
|
|
910
|
+
* - Metric units: 1 unit = 1 meter
|
|
911
|
+
* - Reference: 1920 video pixels = 1 meter
|
|
912
|
+
*
|
|
913
|
+
* @see docs/architecture/coordinate-system-design.md
|
|
914
|
+
*/
|
|
915
|
+
export const COORDINATE_SYSTEM = {
|
|
916
|
+
/**
|
|
917
|
+
* Reference scale: 1920 pixels = 1 meter in world space
|
|
918
|
+
* This ensures a standard 1920×1080 video is 1m × 0.5625m
|
|
919
|
+
*/
|
|
920
|
+
PIXELS_PER_METER: 1920,
|
|
921
|
+
/**
|
|
922
|
+
* Layer Z positions in meters
|
|
923
|
+
* - VIDEO: Background video plane
|
|
924
|
+
* - THREE_D_MIN/MAX: 3D tracked surfaces, webcam
|
|
925
|
+
* - HUD_MIN/MAX: HTML overlays via CSS2DRenderer
|
|
926
|
+
*/
|
|
927
|
+
LAYER_Z: {
|
|
928
|
+
VIDEO: 0,
|
|
929
|
+
THREE_D_MIN: 0.01,
|
|
930
|
+
THREE_D_MAX: 10,
|
|
931
|
+
HUD_MIN: 10.1,
|
|
932
|
+
HUD_MAX: 100
|
|
933
|
+
},
|
|
934
|
+
/**
|
|
935
|
+
* Camera defaults for orthographic rendering (video plane)
|
|
936
|
+
*/
|
|
937
|
+
CAMERA: {
|
|
938
|
+
NEAR: 0.01,
|
|
939
|
+
FAR: 1000,
|
|
940
|
+
POSITION_Z: 50 // Camera 50m from video plane
|
|
941
|
+
},
|
|
942
|
+
/**
|
|
943
|
+
* Orthographic camera settings for HUD overlays (Layer 3)
|
|
944
|
+
* Provides predictable 2D positioning for sprites, text, CTAs
|
|
945
|
+
*
|
|
946
|
+
* Bounds are set dynamically based on video dimensions.
|
|
947
|
+
* Objects are positioned in meters relative to center (0,0).
|
|
948
|
+
*/
|
|
949
|
+
HUD_CAMERA: {
|
|
950
|
+
/** Camera Z position (arbitrary for orthographic) */
|
|
951
|
+
POSITION_Z: 10,
|
|
952
|
+
/** Near clipping plane */
|
|
953
|
+
NEAR: 0.1,
|
|
954
|
+
/** Far clipping plane */
|
|
955
|
+
FAR: 100,
|
|
956
|
+
/** Default object Z position */
|
|
957
|
+
OBJECT_Z: 0
|
|
958
|
+
},
|
|
959
|
+
/**
|
|
960
|
+
* Perspective camera settings for webcam composite layer (Layer 2)
|
|
961
|
+
* Camera positioned at eye height for 3D scene placement
|
|
962
|
+
* Reserved for future webcam/chroma key compositing
|
|
963
|
+
*/
|
|
964
|
+
SCENE_CAMERA: {
|
|
965
|
+
/** Camera X position (centered) */
|
|
966
|
+
POSITION_X: 0,
|
|
967
|
+
/** Camera Y position (eye height) */
|
|
968
|
+
POSITION_Y: 1.2,
|
|
969
|
+
/** Camera Z position (distance from scene) */
|
|
970
|
+
POSITION_Z: 4,
|
|
971
|
+
/** Vertical field of view in degrees (~43mm equivalent) */
|
|
972
|
+
FOV: 47,
|
|
973
|
+
/** Near clipping plane */
|
|
974
|
+
NEAR: 0.1,
|
|
975
|
+
/** Far clipping plane */
|
|
976
|
+
FAR: 100,
|
|
977
|
+
/** Look-at target Y position */
|
|
978
|
+
LOOK_AT_Y: 1.2
|
|
979
|
+
},
|
|
980
|
+
/**
|
|
981
|
+
* Film gate constants (for future camera matching)
|
|
982
|
+
* Based on 35mm full-frame sensor
|
|
983
|
+
*/
|
|
984
|
+
FILM_GATE: {
|
|
985
|
+
HORIZONTAL_APERTURE: 1.417, // inches (36mm)
|
|
986
|
+
VERTICAL_APERTURE: 0.945 // inches (24mm)
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* Convert video pixel dimensions to world-space meters
|
|
991
|
+
*
|
|
992
|
+
* @param pixelWidth - Video width in pixels
|
|
993
|
+
* @param pixelHeight - Video height in pixels
|
|
994
|
+
* @returns World-space dimensions in meters
|
|
995
|
+
*/
|
|
996
|
+
export function pixelsToMeters(pixelWidth, pixelHeight) {
|
|
997
|
+
return {
|
|
998
|
+
width: pixelWidth / COORDINATE_SYSTEM.PIXELS_PER_METER,
|
|
999
|
+
height: pixelHeight / COORDINATE_SYSTEM.PIXELS_PER_METER
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Convert world-space meters to video pixels
|
|
1004
|
+
*
|
|
1005
|
+
* @param meterWidth - Width in meters
|
|
1006
|
+
* @param meterHeight - Height in meters
|
|
1007
|
+
* @returns Pixel dimensions
|
|
1008
|
+
*/
|
|
1009
|
+
export function metersToPixels(meterWidth, meterHeight) {
|
|
1010
|
+
return {
|
|
1011
|
+
width: meterWidth * COORDINATE_SYSTEM.PIXELS_PER_METER,
|
|
1012
|
+
height: meterHeight * COORDINATE_SYSTEM.PIXELS_PER_METER
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Convert overlay percentage position (0-100) to world-space meters
|
|
1017
|
+
*
|
|
1018
|
+
* @param percentX - X position as percentage (0-100, left to right)
|
|
1019
|
+
* @param percentY - Y position as percentage (0-100, top to bottom)
|
|
1020
|
+
* @param videoWidthMeters - Video width in meters
|
|
1021
|
+
* @param videoHeightMeters - Video height in meters
|
|
1022
|
+
* @returns World-space position {x, y} in meters, centered at origin
|
|
1023
|
+
*/
|
|
1024
|
+
export function percentToWorldSpace(percentX, percentY, videoWidthMeters, videoHeightMeters) {
|
|
1025
|
+
// Convert percentage (0-100) to normalized (-0.5 to 0.5)
|
|
1026
|
+
const normalizedX = (percentX - 50) / 100;
|
|
1027
|
+
const normalizedY = (50 - percentY) / 100; // Y inverted for screen coords
|
|
1028
|
+
return {
|
|
1029
|
+
x: normalizedX * videoWidthMeters,
|
|
1030
|
+
y: normalizedY * videoHeightMeters
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Convert world-space meters to overlay percentage position (0-100)
|
|
1035
|
+
*
|
|
1036
|
+
* @param worldX - X position in meters (origin = center)
|
|
1037
|
+
* @param worldY - Y position in meters (origin = center)
|
|
1038
|
+
* @param videoWidthMeters - Video width in meters
|
|
1039
|
+
* @param videoHeightMeters - Video height in meters
|
|
1040
|
+
* @returns Percentage position {x, y} (0-100)
|
|
1041
|
+
*/
|
|
1042
|
+
export function worldSpaceToPercent(worldX, worldY, videoWidthMeters, videoHeightMeters) {
|
|
1043
|
+
const normalizedX = worldX / videoWidthMeters;
|
|
1044
|
+
const normalizedY = worldY / videoHeightMeters;
|
|
1045
|
+
return {
|
|
1046
|
+
x: normalizedX * 100 + 50,
|
|
1047
|
+
y: 50 - normalizedY * 100 // Y inverted for screen coords
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Convert Gausst tracking packet to world-space transform
|
|
1052
|
+
*
|
|
1053
|
+
* @param packet - Raw tracking data from hardware
|
|
1054
|
+
* @returns Position in meters, rotation in radians, FOV in degrees
|
|
1055
|
+
*/
|
|
1056
|
+
export function trackingPacketToWorldSpace(packet) {
|
|
1057
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
1058
|
+
return {
|
|
1059
|
+
position: {
|
|
1060
|
+
x: packet.x / 100000, // mm×100 → meters
|
|
1061
|
+
y: packet.y / 100000,
|
|
1062
|
+
z: packet.z / 100000
|
|
1063
|
+
},
|
|
1064
|
+
rotation: {
|
|
1065
|
+
x: (packet.tilt / 1000) * DEG_TO_RAD, // Tilt (pitch)
|
|
1066
|
+
y: (-packet.pan / 1000) * DEG_TO_RAD, // Pan (yaw), negated per Gausst
|
|
1067
|
+
z: (packet.roll / 1000) * DEG_TO_RAD // Roll
|
|
1068
|
+
},
|
|
1069
|
+
fov: packet.fov / 1000
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Convert FOV to focal length (for matching real camera optics)
|
|
1074
|
+
*
|
|
1075
|
+
* Formula from Gausst: focalLength = (aperture / 2) / tan(fov / 2) * 25.4
|
|
1076
|
+
*
|
|
1077
|
+
* @param fovDegrees - Field of view in degrees
|
|
1078
|
+
* @param filmApertureInches - Horizontal film aperture (default: 1.417" = 36mm)
|
|
1079
|
+
* @returns Focal length in millimeters
|
|
1080
|
+
*/
|
|
1081
|
+
export function fovToFocalLength(fovDegrees, filmApertureInches = COORDINATE_SYSTEM.FILM_GATE.HORIZONTAL_APERTURE) {
|
|
1082
|
+
const fovRadians = fovDegrees * (Math.PI / 180);
|
|
1083
|
+
const apertureMM = filmApertureInches * 25.4;
|
|
1084
|
+
return (apertureMM / 2) / Math.tan(fovRadians / 2);
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Convert focal length to FOV
|
|
1088
|
+
*
|
|
1089
|
+
* @param focalLengthMM - Focal length in millimeters
|
|
1090
|
+
* @param filmApertureInches - Horizontal film aperture (default: 1.417" = 36mm)
|
|
1091
|
+
* @returns Field of view in degrees
|
|
1092
|
+
*/
|
|
1093
|
+
export function focalLengthToFov(focalLengthMM, filmApertureInches = COORDINATE_SYSTEM.FILM_GATE.HORIZONTAL_APERTURE) {
|
|
1094
|
+
const apertureMM = filmApertureInches * 25.4;
|
|
1095
|
+
const fovRadians = 2 * Math.atan((apertureMM / 2) / focalLengthMM);
|
|
1096
|
+
return fovRadians * (180 / Math.PI);
|
|
1097
|
+
}
|
|
1098
|
+
// ============================================================================
|
|
1099
|
+
// Layer 2 (WebGL/3D) Module Re-exports
|
|
1100
|
+
// ============================================================================
|
|
1101
|
+
// Shader exports
|
|
1102
|
+
export { webcamVertexShader, webcamFragmentShader, getDefaultPlayerUniforms, getDefaultEditorUniforms } from './layer2/shaders';
|
|
1103
|
+
// Webcam utility exports
|
|
1104
|
+
export { WEBCAM_BASE_HEIGHT_METERS, SILHOUETTE_HEIGHT_RATIO, getWebcamBaseDimensions, getWebcamMeshYOffset, getObjectEuler, getCameraEuler, GAM_PIXELS_TO_METERS, AD_TEMPLATE_DIMENSIONS, getAdDimensions, CHROMA_KEY_COLORS, getChromaKeyColorHex, featherToUV, featherEdgesToUV } from './layer2/webcam-utils';
|
|
1105
|
+
// ============================================================================
|
|
1106
|
+
// Segmentation Module
|
|
1107
|
+
// ============================================================================
|
|
1108
|
+
// NOTE: MediaPipeSegmenter is NOT exported from main entry point because
|
|
1109
|
+
// @mediapipe/tasks-vision requires browser APIs and breaks SSR.
|
|
1110
|
+
// Import directly from '@ume/contracts/segmentation' in browser contexts:
|
|
1111
|
+
// import { MediaPipeSegmenter } from '@ume/contracts/segmentation';
|
|
1112
|
+
// ============================================================================
|