-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathplugins3d-loader.js
More file actions
590 lines (525 loc) · 21.9 KB
/
Copy pathplugins3d-loader.js
File metadata and controls
590 lines (525 loc) · 21.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
/**
* @module plugins3d-loader
* @description
* Consumer-only runtime for UV rendering.
*
* Plugin loading and UI button registration are handled by ghostyles-manager.
* This module only listens to app events and, when the currently active plugin
* exports `paintUV`, renders it on the 3D mesh overlay.
*/
import { state } from './state.js';
import { setLog } from './utils.js';
import { createUvRenderer } from './ghostyle3d-uv-renderer.js';
import { clearActiveEffect } from './dom.js';
import { asErrorLabel, syncMirror, syncSize } from './utils.js';
import { t } from './i18n.js';
const runtime = {
initialized: false,
canvas: null,
overlayEl: null,
panel: null,
video: null,
ctx: null,
events: null,
renderer: null,
activePluginId: null,
paramValues: new Map()
};
/**
* Writes loader messages to the shared Ghostmaxxing log under the 3D plugin
* source, so unavailable runtime operations are attributed to this module.
*
* @param {string} message - Message to show in the application log.
* @returns {void}
* @see reloadPlugins3d - Reports that 3D plugin reloading is delegated to `ghostyles-manager`.
*/
function log3d(message) {
setLog(message, 'plugins3d');
}
/**
* Removes a 3D ghostyle from the active UI path after its `paintUV` renderer
* throws, then broadcasts the same effect-change events used by normal plugin
* toggles. The user-facing Italian log message means the plugin threw during
* `paintUV`, and this handler prevents that plugin from breaking subsequent
* mesh frames.
*
* @param {object|null|undefined} entry - Loaded ghostyle entry that failed.
* @param {*} err - Error or thrown value raised by the plugin renderer.
* @returns {void}
* @see initPlugins3dLoader - Called by the `landmarks3d` event listener when UV rendering fails.
*/
function deactivateBroken3dPlugin(entry, err) {
const pluginId = entry?.id || runtime.activePluginId;
if (!pluginId) return;
setLog(t('plugin_runtime_error_log', { id: pluginId, message: asErrorLabel(err), hook: 'paintUV' }), pluginId);
console.error(`[plugins3d] paintUV errore in ${entry?.name || pluginId}:`, err);
const btn = document.querySelector(`[data-effect="${pluginId}"]`);
if (btn) btn.classList.remove('active');
if (state.activeEffect === pluginId) {
const previous = pluginId;
clearActiveEffect();
runtime.activePluginId = null;
runtime.events.dispatchEvent(new CustomEvent('effectChanged', {
detail: { activeEffect: null, previous }
}));
runtime.events.dispatchEvent(new CustomEvent('effectChanged3d', {
detail: { active: null, previous }
}));
}
}
/**
* Guards public 3D control APIs so callers cannot manipulate plugin buttons
* before the loader has captured its DOM nodes and event bus. The thrown
* Italian message means `initPlugins3dLoader()` has not been called yet.
*
* @returns {void}
* @throws {Error} When the 3D loader runtime has not been initialized.
* @see activateEffect3d - Verifies initialization before activating a 3D effect.
* @see deactivateEffect3d - Verifies initialization before deactivating a 3D effect.
* @see toggleEffect3d - Verifies initialization before toggling a 3D effect.
* @see reloadPlugins3d - Verifies initialization before reporting reload availability.
*/
function requireInit() {
if (!runtime.initialized) {
throw new Error(t('plugins3d_not_initialized_error'));
}
}
/**
* Clears the 3D plugin canvas between mesh frames or when no UV-capable plugin
* remains active, avoiding stale painted makeup on the overlay.
*
* @returns {void}
* @see syncActiveEntry - Clears the overlay when the active effect has no `paintUV` renderer.
* @see initPlugins3dLoader - Clears the overlay before drawing each `landmarks3d` frame.
*/
function clearCanvas() {
runtime.ctx.clearRect(0, 0, runtime.canvas.width, runtime.canvas.height);
}
/**
* Normalizes a 3D ghostyle parameter value to the supported project schema
* (`range`, `bool`, `select`, or RGB `color`) before it is stored for `paintUV`.
* This keeps panel input values, plugin defaults, and renderer parameters in
* the same shape regardless of browser form serialization.
*
* @param {object} p - Plugin parameter definition.
* @param {string} p.type - Supported parameter control type.
* @param {*} p.default - Default value declared by the ghostyle.
* @param {number} [p.step] - Numeric slider increment.
* @param {number} [p.min] - Numeric slider minimum.
* @param {number} [p.max] - Numeric slider maximum.
* @param {Array<*>} [p.options] - Selectable values for `select` parameters.
* @param {*} value - Raw value from the plugin manifest or parameter control.
* @returns {*} Coerced value ready to pass to the active ghostyle.
* @see ensureParamValues - Coerces initial plugin defaults.
* @see createParamRow - Coerces values read from parameter panel controls.
*/
function coerceParam(p, value) {
if (p.type === 'range') {
let v = (typeof value === 'number') ? value : parseFloat(value);
if (!Number.isFinite(v)) v = p.default;
const step = (typeof p.step === 'number') ? p.step : 0.01;
if (Number.isInteger(step) && step >= 1) v = Math.round(v);
if (typeof p.min === 'number') v = Math.max(p.min, v);
if (typeof p.max === 'number') v = Math.min(p.max, v);
return v;
}
if (p.type === 'bool') return Boolean(value);
if (p.type === 'select') {
const opts = Array.isArray(p.options) ? p.options : [];
return opts.includes(value) ? value : p.default;
}
if (p.type === 'color') {
if (Array.isArray(value) && value.length === 3) {
return value.map((c) => {
const n = Math.round(Number(c));
return Math.max(0, Math.min(255, Number.isFinite(n) ? n : 0));
});
}
if (typeof value === 'string') {
const short = /^#([0-9a-fA-F]{3})$/.exec(value);
if (short) {
const c = short[1];
return [
parseInt(c[0] + c[0], 16),
parseInt(c[1] + c[1], 16),
parseInt(c[2] + c[2], 16)
];
}
if (/^#[0-9a-fA-F]{6}$/.test(value)) {
return [
parseInt(value.slice(1, 3), 16),
parseInt(value.slice(3, 5), 16),
parseInt(value.slice(5, 7), 16)
];
}
}
return value === p.default ? [0, 0, 0] : coerceParam(p, p.default);
}
return value;
}
/**
* Converts a stored RGB array from the 3D parameter map into a browser color
* input value, clamping invalid plugin-provided components to black-safe
* channel values.
*
* @param {number[]} rgb - RGB triplet stored for a color parameter.
* @returns {string} Hex color string used by the parameter panel.
* @see createParamRow - Initializes and updates color controls for 3D ghostyle parameters.
*/
function rgbToHex(rgb) {
if (!Array.isArray(rgb) || rgb.length < 3) return '#000000';
return '#' + rgb.slice(0, 3).map((c) => {
const n = Math.max(0, Math.min(255, Math.round(Number(c) || 0)));
return n.toString(16).padStart(2, '0');
}).join('');
}
/**
* Creates the per-plugin parameter state used by `paintUV`, seeded from the
* ghostyle module defaults only once per plugin so user adjustments survive
* ordinary active-effect refreshes.
*
* @param {string} id - Loaded ghostyle identifier.
* @param {object} module - Ghostyle module that may expose configurable UV parameters.
* @param {Array<object>} [module.params] - Parameter definitions declared by the ghostyle.
* @returns {void}
* @see syncActiveEntry - Ensures parameters exist whenever a UV-capable ghostyle becomes active.
*/
function ensureParamValues(id, module) {
if (!Array.isArray(module.params) || module.params.length === 0) {
runtime.paramValues.delete(id);
return;
}
if (runtime.paramValues.has(id)) return;
const values = {};
for (const p of module.params) values[p.name] = coerceParam(p, p.default);
runtime.paramValues.set(id, values);
}
/**
* Publishes the parameter panel height into the `--pp-h` CSS variable so the
* rest of the lab UI can reserve space when 3D ghostyle controls are visible.
*
* @returns {void}
* @see hideParamsPanel - Resets the CSS variable when controls are hidden.
* @see renderParamsPanel - Updates the CSS variable after rendering controls.
*/
function syncPanelHeightVar() {
const h = runtime.panel.classList.contains('visible') ? runtime.panel.offsetHeight + 12 : 0;
document.documentElement.style.setProperty('--pp-h', h + 'px');
}
/**
* Builds one row in the floating 3D parameter panel for a ghostyle manifest
* parameter, wiring browser controls back into the runtime value map that is
* later passed to `paintUV`.
*
* @param {string} pluginId - Loaded ghostyle identifier whose values should be edited.
* @param {object} p - Parameter definition from the ghostyle module.
* @param {string} p.name - Runtime parameter key passed to `paintUV`.
* @param {string} [p.label] - Visible label used in the panel.
* @param {string} p.type - Supported parameter control type.
* @param {number} [p.min] - Numeric slider minimum.
* @param {number} [p.max] - Numeric slider maximum.
* @param {number} [p.step] - Numeric slider increment.
* @param {Array<*>} [p.options] - Selectable values for `select` parameters.
* @returns {HTMLDivElement|null} Parameter row, or null when the type cannot be rendered.
* @see renderParamsPanel - Creates rows for the active 3D ghostyle parameter list.
*/
function createParamRow(pluginId, p) {
const values = runtime.paramValues.get(pluginId);
if (!values) return null;
const row = document.createElement('div');
row.className = 'pp-row';
const label = document.createElement('label');
label.className = 'pp-label';
label.textContent = p.label || p.name;
row.appendChild(label);
const ctrlWrap = document.createElement('div');
ctrlWrap.className = 'pp-control';
if (p.type === 'range') {
const input = document.createElement('input');
input.type = 'range';
input.min = String(p.min);
input.max = String(p.max);
input.step = String(p.step || 0.01);
input.value = String(values[p.name]);
const valueLabel = document.createElement('span');
valueLabel.className = 'pp-value';
const fmt = (v) => (Number(p.step) >= 1 ? String(v) : Number(v).toFixed(2));
valueLabel.textContent = fmt(values[p.name]);
input.addEventListener('input', () => {
const v = coerceParam(p, input.value);
values[p.name] = v;
valueLabel.textContent = fmt(v);
});
ctrlWrap.appendChild(input);
row.appendChild(ctrlWrap);
row.appendChild(valueLabel);
return row;
}
if (p.type === 'bool') {
const input = document.createElement('input');
input.type = 'checkbox';
input.checked = Boolean(values[p.name]);
input.addEventListener('input', () => {
values[p.name] = coerceParam(p, input.checked);
});
ctrlWrap.appendChild(input);
row.appendChild(ctrlWrap);
return row;
}
if (p.type === 'select') {
const select = document.createElement('select');
for (const opt of (p.options || [])) {
const o = document.createElement('option');
o.value = String(opt);
o.textContent = String(opt);
if (opt === values[p.name]) o.selected = true;
select.appendChild(o);
}
select.addEventListener('input', () => {
values[p.name] = coerceParam(p, select.value);
});
ctrlWrap.appendChild(select);
row.appendChild(ctrlWrap);
return row;
}
if (p.type === 'color') {
const input = document.createElement('input');
input.type = 'color';
input.value = rgbToHex(values[p.name]);
const valueLabel = document.createElement('span');
valueLabel.className = 'pp-value';
valueLabel.textContent = rgbToHex(values[p.name]);
input.addEventListener('input', () => {
values[p.name] = coerceParam(p, input.value);
valueLabel.textContent = rgbToHex(values[p.name]);
});
ctrlWrap.appendChild(input);
row.appendChild(ctrlWrap);
row.appendChild(valueLabel);
return row;
}
return null;
}
/**
* Hides and empties the 3D parameter panel when no UV-capable ghostyle is
* active, or when the active ghostyle has no configurable parameters.
*
* @returns {void}
* @see renderParamsPanel - Delegates here for plugins without parameters.
* @see syncActiveEntry - Hides controls when the active effect is not a 3D renderer.
*/
function hideParamsPanel() {
runtime.panel.classList.remove('visible');
runtime.panel.setAttribute('aria-hidden', 'true');
runtime.panel.innerHTML = '';
syncPanelHeightVar();
}
/**
* Renders the floating parameter panel for the active UV ghostyle. The existing
* Italian heading "Parametri" is the visible label for plugin controls and is
* generated here together with the active plugin name.
*
* @param {object|null|undefined} entry - Active loaded ghostyle entry.
* @returns {void}
* @see syncActiveEntry - Refreshes the panel whenever the active effect changes.
*/
function renderParamsPanel(entry) {
runtime.panel.innerHTML = '';
if (!entry || !Array.isArray(entry.module.params) || entry.module.params.length === 0) {
hideParamsPanel();
return;
}
const title = document.createElement('div');
title.className = 'pp-header';
title.textContent = t('params_heading', { name: entry.name });
runtime.panel.appendChild(title);
for (const p of entry.module.params) {
const row = createParamRow(entry.id, p);
if (row) runtime.panel.appendChild(row);
}
runtime.panel.classList.add('visible');
runtime.panel.classList.remove('collapsed');
runtime.panel.setAttribute('aria-hidden', 'false');
requestAnimationFrame(syncPanelHeightVar);
}
/**
* Resolves the current global active effect to a loaded ghostyle entry only
* when that module exports the `paintUV` hook required by the 3D mesh overlay.
*
* @returns {object|null} Active UV-capable ghostyle entry.
* @see syncActiveEntry - Uses this lookup to align runtime state with `state.activeEffect`.
*/
function getActivePaintEntry() {
const activeId = state.activeEffect;
if (!activeId) return null;
const entry = state.loadedGhostyles.get(activeId);
if (!entry || !entry.module || typeof entry.module.paintUV !== 'function') return null;
return entry;
}
/**
* Synchronizes loader state after a ghostyle effect change by selecting the
* active `paintUV` entry, preparing its parameters, refreshing controls, and
* notifying listeners through `effectChanged3d`.
*
* @returns {void}
* @see initPlugins3dLoader - Called during initialization and by the `effectChanged` event listener.
*/
function syncActiveEntry() {
const prev = runtime.activePluginId;
const entry = getActivePaintEntry();
runtime.activePluginId = entry ? entry.id : null;
if (entry) {
ensureParamValues(entry.id, entry.module);
renderParamsPanel(entry);
} else {
hideParamsPanel();
clearCanvas();
}
if (prev !== runtime.activePluginId) {
runtime.events.dispatchEvent(new CustomEvent('effectChanged3d', {
detail: { active: runtime.activePluginId, previous: prev }
}));
}
}
/**
* Returns the active 3D ghostyle id tracked by this loader, or null when the
* current active effect is not a UV-capable plugin.
*
* @returns {string|null} Active 3D plugin id.
* @see main - Exposed as `window.gstmxx.getActiveEffect3d`.
* @see hasActivePlugin - Checks whether 2D or 3D compositing should run.
* @see hasActivePlugin3d - Checks active plugin state before 3D efficacy compositing.
*/
export function getActiveEffect3d() {
return runtime.activePluginId;
}
/**
* Activates a UV-capable ghostyle by delegating to its existing effect button,
* keeping 3D activation on the same UI path as regular ghostyle toggles.
*
* @param {string} id - Loaded ghostyle id to activate.
* @returns {boolean} True when a matching clickable 3D effect button was triggered.
* @see main - Exposed as `window.gstmxx.activateEffect3d`.
* @see toggleEffect3d - Uses this when the requested 3D plugin is not already active.
*/
export function activateEffect3d(id) {
requireInit();
const entry = state.loadedGhostyles.get(id);
if (!entry || typeof entry.module?.paintUV !== 'function') return false;
const btn = document.querySelector(`[data-effect="${id}"]`);
if (!btn || typeof btn.click !== 'function') return false;
btn.click();
return true;
}
/**
* Deactivates the current UV-capable ghostyle by clicking the active effect
* button, preserving the unified active-effect behavior owned by the UI.
*
* @returns {boolean} True when the active 3D effect button was triggered.
* @see main - Exposed as `window.gstmxx.deactivateEffect3d`.
* @see toggleEffect3d - Uses this when the requested 3D plugin is already active.
*/
export function deactivateEffect3d() {
requireInit();
if (!state.activeEffect || state.activeEffect !== runtime.activePluginId) return false;
const btn = document.querySelector(`[data-effect="${state.activeEffect}"]`);
if (!btn || typeof btn.click !== 'function') return false;
btn.click();
return true;
}
/**
* Toggles a UV-capable ghostyle through the shared effect-button workflow,
* activating it when inactive and deactivating it when it is already selected.
*
* @param {string} id - Loaded ghostyle id to toggle.
* @returns {boolean} True when a matching UI button was triggered.
* @see main - Exposed as `window.gstmxx.toggleEffect3d`.
*/
export function toggleEffect3d(id) {
requireInit();
if (runtime.activePluginId === id) return deactivateEffect3d();
return activateEffect3d(id);
}
/**
* Compatibility hook for older 3D plugin callers. In the current project,
* plugin discovery and reloads are handled by `ghostyles-manager`, so this
* function logs that 3D-specific reload is not available and returns false.
*
* @returns {boolean} Always false because 3D plugins are managed by `ghostyles-manager`.
* @see main - Exposed as `window.gstmxx.reloadPlugins3d`.
*/
export function reloadPlugins3d() {
requireInit();
log3d('reloadPlugins3d non disponibile: i plugin sono gestiti da ghostyles-manager.');
return false;
}
/**
* Initializes the 3D plugin runtime by binding the overlay canvas, parameter
* panel, app event bus, and UV renderer. It then listens for active-effect,
* compositing, and MediaPipe landmark events so loaded ghostyles with `paintUV`
* can draw onto the mesh overlay without owning plugin loading themselves.
*
* @param {object} [options={}] - DOM id and renderer overrides used by startup and tests.
* @param {string} [options.canvasId='mesh3dOverlay'] - Canvas receiving rendered UV makeup.
* @param {string} [options.overlayId='overlay'] - Main overlay whose size and mirror transform are followed.
* @param {string} [options.panelId='plugin3dParamsPanel'] - Floating panel for active plugin parameters.
* @param {string} [options.videoId='video'] - Webcam video element required by the runtime.
* @param {string} [options.baseUrl] - Base URL for resolving the canonical UV data file.
* @param {string} [options.uvPath] - Explicit URL for `face_canonical_uv.json`.
* @param {Function} [options.getFaceLandmarker] - Provider for the MediaPipe FaceLandmarker instance.
* @returns {object} Initialized singleton runtime.
* @throws {Error} When the required DOM elements are missing.
* @see main - Called during application startup before `ghostyles-manager` loads unified ghostyles.
*/
export function initPlugins3dLoader(options = {}) {
if (runtime.initialized) return runtime;
runtime.canvas = document.getElementById(options.canvasId || 'mesh3dOverlay');
runtime.overlayEl = document.getElementById(options.overlayId || 'overlay');
runtime.panel = document.getElementById(options.panelId || 'plugin3dParamsPanel');
runtime.video = document.getElementById(options.videoId || 'video');
if (!runtime.canvas || !runtime.overlayEl || !runtime.panel || !runtime.video) {
throw new Error(t('plugins3d_missing_dom_error'));
}
runtime.ctx = runtime.canvas.getContext('2d');
runtime.events = state.gstmxxEvents;
const relurl = options.baseUrl || window.location.pathname.split('/').slice(0, -1).join('/');
const uvPath = options.uvPath || (relurl + '/data/face_canonical_uv.json');
runtime.renderer = createUvRenderer({
uvPath,
getFaceLandmarker: options.getFaceLandmarker || (() => (window.gstmxx && window.gstmxx.FaceLandmarker) || null),
log: (message) => setLog(message, 'uv-renderer')
});
runtime.renderer.ensureLoaded();
runtime.events.addEventListener('effectChanged', () => {
syncActiveEntry();
});
runtime.events.addEventListener('beforeEfficacyComposite', (e) => {
const detail = e.detail || {};
if (!runtime.activePluginId || !detail.canvas || !detail.ctx) return;
detail.ctx.drawImage(runtime.canvas, 0, 0, detail.canvas.width, detail.canvas.height);
});
runtime.events.addEventListener('beforeEfficacyComposite3d', (e) => {
const detail = e.detail || {};
if (!runtime.activePluginId || !detail.canvas || !detail.ctx) return;
detail.ctx.drawImage(runtime.canvas, 0, 0, detail.canvas.width, detail.canvas.height);
});
runtime.events.addEventListener('landmarks3d', (e) => {
const landmarks = e.detail && e.detail.landmarks;
syncSize(runtime.canvas, runtime.overlayEl);
syncMirror(runtime.canvas, runtime.overlayEl);
clearCanvas();
if (!runtime.activePluginId || !landmarks) return;
const entry = state.loadedGhostyles.get(runtime.activePluginId);
if (!entry || typeof entry.module?.paintUV !== 'function') return;
try {
runtime.ctx.save();
runtime.renderer.render(entry.module, runtime.ctx, landmarks, runtime.paramValues.get(runtime.activePluginId) || {});
runtime.ctx.restore();
} catch (err) {
deactivateBroken3dPlugin(entry, err);
}
});
runtime.initialized = true;
syncActiveEntry();
return runtime;
}