@7ots/cli 0.1.1 → 0.1.4
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/.env.example +17 -1
- package/cli/7ots.mjs +144 -36
- package/cli/lib/assistant.mjs +347 -0
- package/cli/lib/brain.mjs +10 -7
- package/cli/lib/config.mjs +8 -1
- package/cli/lib/i18n.mjs +84 -11
- package/cli/lib/install.mjs +29 -1
- package/cli/lib/pet-core.mjs +3 -2
- package/cli/lib/pet-server.mjs +133 -13
- package/cli/lib/prompts.mjs +199 -7
- package/cli/lib/terminal.mjs +27 -6
- package/cli/pet/electron/main.cjs +96 -32
- package/cli/pet/pet.html +59 -6
- package/cli/pet/settings.html +430 -0
- package/dist/7ots.esm.js +370 -142
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +370 -142
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +198 -6
- package/package.json +1 -1
- package/server/channels/apuchat.mjs +8 -5
- package/server/channels/apumail.mjs +13 -3
- package/server/channels/index.mjs +5 -3
- package/server/platform/apuchat.mjs +266 -0
- package/server/platform/auth.mjs +39 -10
- package/server/platform/connect.mjs +347 -0
- package/server/platform/db.mjs +55 -0
- package/server/platform/look.mjs +20 -0
- package/server/platform/orquesta.mjs +499 -0
- package/server/platform/routes.mjs +57 -8
- package/server/platform/runtime.mjs +75 -15
- package/server/platform/store.mjs +130 -9
- package/server/server.mjs +14 -3
- package/src/avatar/AvatarStage.js +54 -9
- package/src/avatar/three.js +62 -0
- package/src/character/AvatarEditor.js +174 -50
- package/src/character/Character.js +164 -26
- package/src/character/motion.js +95 -34
- package/src/character/parts.js +43 -12
- package/src/character/skin.js +412 -0
- package/src/character/styles.js +1429 -0
- package/src/character3d/Character3D.js +1693 -0
- package/src/character3d/body.js +154 -0
- package/src/character3d/fx.js +323 -0
- package/src/character3d/index.js +281 -0
- package/src/character3d/materials.js +429 -0
- package/src/character3d/stage.js +187 -0
- package/src/i18n/messages/character.js +18 -42
- package/src/i18n/messages/platform.js +135 -0
- package/src/identity/schema.js +16 -8
- package/src/index.js +1 -0
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* const ed = createAvatarEditor(div, { value: identity.look, onChange: (look) => … });
|
|
5
5
|
* <ots-avatar-editor></ots-avatar-editor> → el.value = {...}; el.addEventListener('change', e => e.detail)
|
|
6
6
|
*
|
|
7
|
-
* Valor: { kind: 'character'|'
|
|
7
|
+
* Valor: { kind: 'character'|'plush'|'image', character: spec, material: 'plush'|'vinyl', avatar, image }
|
|
8
|
+
* (un 'realistic' heredado se abre como 'character'; `avatar` solo se conserva).
|
|
8
9
|
*
|
|
9
10
|
* - Personaje 2D: estilos, forma (se deforma arrastrando sus puntos, con simetría), color,
|
|
10
11
|
* acabado, ojos, boca, cara y accesorios (arrastrables). Deshacer/rehacer, aleatorio,
|
|
11
12
|
* exportar SVG/JSON, vista previa hablando/con ánimos/gestos.
|
|
12
|
-
* -
|
|
13
|
+
* - Personaje 3D: la MISMA spec y las mismas pestañas, con vista previa 3D (src/character3d, se
|
|
14
|
+
* arrastra para girarlo) y material peluche/vinilo. Los puntos arrastrables son del 2D: en 3D se
|
|
15
|
+
* ocultan (forma, ojos y boca se cambian desde las pestañas).
|
|
13
16
|
* - Imagen: retrato fijo.
|
|
14
17
|
*
|
|
15
18
|
* Idioma: atributo `lang` (o la propiedad nativa `el.lang`, u `opts.locale` en createAvatarEditor).
|
|
@@ -20,18 +23,19 @@
|
|
|
20
23
|
|
|
21
24
|
import {
|
|
22
25
|
normalizeCharacter, renderCharacter, createCharacter, applyPreset, randomCharacter, layout, bodyPoints,
|
|
23
|
-
SHAPES, EYES, BROWS, MOUTHS, CHEEKS, PATTERNS, FINISHES, ACCESSORIES, ACCESSORY_GROUPS, PRESETS, BODY_POINTS,
|
|
26
|
+
SHAPES, EYES, BROWS, MOUTHS, CHEEKS, PATTERNS, FINISHES, TEXTURES, ACCESSORIES, ACCESSORY_GROUPS, PRESETS, BODY_POINTS,
|
|
24
27
|
CHARACTER_MOODS, CHARACTER_GESTURES, CHARACTER_MORPHS, partLabel,
|
|
25
28
|
} from './Character.js';
|
|
26
29
|
import { translator, detectLocale, getLocale, onLocaleChange, formatNumber } from '../i18n/index.js';
|
|
30
|
+
import { STYLES as ART_STYLES } from './styles.js';
|
|
31
|
+
import { loadCharacter3D } from '../avatar/three.js';
|
|
27
32
|
|
|
28
33
|
const VIEW = [-20, -50, 240, 270];
|
|
29
34
|
const AXIS = 100;
|
|
30
35
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
];
|
|
37
|
+
const KINDS = ['character', 'plush', 'image'];
|
|
38
|
+
const MATERIALS = ['plush', 'vinyl'];
|
|
35
39
|
|
|
36
40
|
const TABS = ['styles', 'shape', 'color', 'eyes', 'mouth', 'face', 'acc'];
|
|
37
41
|
const GESTURES = CHARACTER_GESTURES;
|
|
@@ -45,7 +49,7 @@ const CSS = `
|
|
|
45
49
|
--ae-bg:#ffffff;--ae-soft:#f4f4fa;--ae-line:#e4e4ee;--ae-fg:#17171f;--ae-muted:#6b6f82;--ae-p:#6d5dfc;--ae-p-soft:#6d5dfc1f;--ae-stage:radial-gradient(circle at 50% 35%,#ffffff,#eceaf7)}
|
|
46
50
|
@media (prefers-color-scheme:dark){:host{--ae-bg:#17171e;--ae-soft:#1f1f28;--ae-line:#2c2c38;--ae-fg:#ececf3;--ae-muted:#9a9db0;--ae-stage:radial-gradient(circle at 50% 35%,#2a2a36,#15151c)}}
|
|
47
51
|
:host([theme=light]){--ae-bg:#ffffff;--ae-soft:#f4f4fa;--ae-line:#e4e4ee;--ae-fg:#17171f;--ae-muted:#6b6f82;--ae-stage:radial-gradient(circle at 50% 35%,#ffffff,#eceaf7)}
|
|
48
|
-
:host([theme=dark]){
|
|
52
|
+
:host([theme=dark]){color-scheme:dark;--ae-bg:#17171e;--ae-soft:#1f1f28;--ae-line:#2c2c38;--ae-fg:#ececf3;--ae-muted:#9a9db0;--ae-stage:radial-gradient(circle at 50% 35%,#2a2a36,#15151c)}
|
|
49
53
|
*{box-sizing:border-box}
|
|
50
54
|
.wrap{background:var(--ae-bg);border:1px solid var(--ae-line);border-radius:16px;overflow:hidden}
|
|
51
55
|
.kinds{display:flex;gap:4px;padding:10px;border-bottom:1px solid var(--ae-line);flex-wrap:wrap}
|
|
@@ -57,6 +61,7 @@ const CSS = `
|
|
|
57
61
|
@media (max-width:760px){.left{border-right:0;border-bottom:1px solid var(--ae-line)}}
|
|
58
62
|
.stage{position:relative;aspect-ratio:240/270;max-height:420px;margin:0 auto;width:100%;background:var(--ae-stage);touch-action:none;user-select:none}
|
|
59
63
|
.stage>div,.stage>svg{position:absolute;inset:0;width:100%;height:100%}
|
|
64
|
+
.stage .ch3{cursor:grab}
|
|
60
65
|
.ov{overflow:visible}
|
|
61
66
|
.h{cursor:grab;fill:#fff;stroke:var(--ae-p);stroke-width:2.2;outline:none}
|
|
62
67
|
.h:hover,.h:focus-visible{fill:var(--ae-p)}
|
|
@@ -69,7 +74,8 @@ const CSS = `
|
|
|
69
74
|
.btn:hover{border-color:var(--ae-p)}
|
|
70
75
|
.btn:disabled{opacity:.4;cursor:default}
|
|
71
76
|
.btn.p{background:var(--ae-p);border-color:var(--ae-p);color:#fff}
|
|
72
|
-
select.btn{padding:
|
|
77
|
+
select.btn{padding:6px 28px 6px 10px;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239a9db0' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;background-size:14px}
|
|
78
|
+
select.btn option{background:var(--ae-soft);color:var(--ae-fg)}
|
|
73
79
|
.right{display:flex;flex-direction:column;min-width:0}
|
|
74
80
|
.tabs{display:flex;gap:2px;padding:8px 8px 0;overflow-x:auto;border-bottom:1px solid var(--ae-line);scrollbar-width:thin}
|
|
75
81
|
.tabs button{border-radius:9px 9px 0 0;white-space:nowrap;padding:8px 11px}
|
|
@@ -81,6 +87,10 @@ select.btn{padding:5px 8px}
|
|
|
81
87
|
.opt span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
|
82
88
|
.opt:hover{border-color:var(--ae-line)}
|
|
83
89
|
.opt[aria-pressed=true]{border-color:var(--ae-p);background:var(--ae-p-soft);color:var(--ae-fg)}
|
|
90
|
+
.arts{grid-template-columns:none;grid-template-rows:repeat(2,auto);grid-auto-flow:column;grid-auto-columns:minmax(78px,86px);overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x proximity;padding-bottom:6px;scrollbar-width:thin}
|
|
91
|
+
.arts .opt{scroll-snap-align:start}
|
|
92
|
+
.arts i{width:100%;aspect-ratio:1;display:block;border-radius:8px;background:var(--ae-line);opacity:.5}
|
|
93
|
+
.arts img{width:100%;aspect-ratio:1;display:block}
|
|
84
94
|
h4{margin:16px 0 8px;font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--ae-muted);font-weight:600}
|
|
85
95
|
h4:first-child{margin-top:0}
|
|
86
96
|
.row{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px 14px}
|
|
@@ -98,6 +108,8 @@ input[type=text],input[type=url]{font:inherit;width:100%;padding:7px 9px;border-
|
|
|
98
108
|
.card{border:1px solid var(--ae-line);border-radius:12px;padding:12px;background:var(--ae-soft)}
|
|
99
109
|
.sel-acc{margin-top:12px}
|
|
100
110
|
.hidden{display:none!important}
|
|
111
|
+
:host([simple]) [data-kind=image],:host([simple]) [data-bar=export]{display:none!important}
|
|
112
|
+
:host([simple]) .wrap{border:0;border-radius:0;background:transparent}
|
|
101
113
|
.alt{padding:18px;display:grid;gap:14px;max-width:640px}
|
|
102
114
|
.alt img{max-width:160px;border-radius:14px;border:1px solid var(--ae-line)}
|
|
103
115
|
`;
|
|
@@ -106,10 +118,11 @@ const SWATCHES = ['#a0714f', '#f1c27d', '#fcd9b6', '#e0a97e', '#8d5524', '#ef444
|
|
|
106
118
|
|
|
107
119
|
/** Valor completo del editor a partir de un `look` de identidad (o parcial). */
|
|
108
120
|
export function normalizeEditorValue(v = {}) {
|
|
109
|
-
const kind =
|
|
121
|
+
const kind = KINDS.includes(v.kind) ? v.kind : 'character'; // 'realistic' (heredado) → character
|
|
110
122
|
return {
|
|
111
123
|
kind,
|
|
112
124
|
character: normalizeCharacter(v.character || {}),
|
|
125
|
+
material: MATERIALS.includes(v.material) ? v.material : 'plush',
|
|
113
126
|
avatar: { url: typeof v.avatar?.url === 'string' ? v.avatar.url : '', body: v.avatar?.body === 'M' ? 'M' : 'F' },
|
|
114
127
|
image: typeof v.image === 'string' ? v.image : '',
|
|
115
128
|
};
|
|
@@ -117,12 +130,15 @@ export function normalizeEditorValue(v = {}) {
|
|
|
117
130
|
|
|
118
131
|
/**
|
|
119
132
|
* @param {HTMLElement} container
|
|
120
|
-
* @param {{ value?: object, onChange?: (v:object)=>void,
|
|
133
|
+
* @param {{ value?: object, onChange?: (v:object)=>void, plush?: boolean, simple?: boolean, theme?: 'light'|'dark', locale?: string }} [opts]
|
|
134
|
+
* plush: false oculta el tipo «Personaje 3D» (se acepta también el antiguo `realistic: false`).
|
|
121
135
|
* locale: idioma de la interfaz ('es', 'en', 'pt'…); se puede cambiar luego con `el.lang = 'en'`.
|
|
122
136
|
*/
|
|
123
137
|
export function createAvatarEditor(container, opts = {}) {
|
|
124
138
|
const el = document.createElement('ots-avatar-editor');
|
|
125
|
-
if (opts.realistic === false) el.setAttribute('no-
|
|
139
|
+
if (opts.plush === false || opts.realistic === false) el.setAttribute('no-plush', '');
|
|
140
|
+
if (opts.simple) el.setAttribute('simple', ''); // sin selector de tipo ni exportar (p. ej. la portada)
|
|
141
|
+
if (opts.theme) el.setAttribute('theme', opts.theme);
|
|
126
142
|
if (opts.locale) el.setAttribute('lang', opts.locale);
|
|
127
143
|
if (opts.value) el.value = opts.value;
|
|
128
144
|
if (opts.onChange) el.addEventListener('change', (e) => opts.onChange(e.detail));
|
|
@@ -215,8 +231,10 @@ export class AvatarEditor extends Base {
|
|
|
215
231
|
<div class="left">
|
|
216
232
|
<div class="stage">
|
|
217
233
|
<div class="ch"></div>
|
|
234
|
+
<div class="ch3 hidden"></div>
|
|
218
235
|
<svg class="ov" viewBox="${VIEW.join(' ')}"></svg>
|
|
219
236
|
</div>
|
|
237
|
+
<div class="bar hidden" data-bar="material"></div>
|
|
220
238
|
<div class="bar" data-bar="play"></div>
|
|
221
239
|
<div class="bar" data-bar="export"></div>
|
|
222
240
|
</div>
|
|
@@ -225,7 +243,6 @@ export class AvatarEditor extends Base {
|
|
|
225
243
|
<div class="panel" role="tabpanel"></div>
|
|
226
244
|
</div>
|
|
227
245
|
</div>
|
|
228
|
-
<div class="alt hidden" data-view="realistic"></div>
|
|
229
246
|
<div class="alt hidden" data-view="image"></div>
|
|
230
247
|
</div>`;
|
|
231
248
|
this.$ = (s) => root.querySelector(s);
|
|
@@ -240,8 +257,8 @@ export class AvatarEditor extends Base {
|
|
|
240
257
|
this._ov.addEventListener('pointerdown', (e) => this._onDown(e));
|
|
241
258
|
this._ov.addEventListener('keydown', (e) => this._onHandleKey(e));
|
|
242
259
|
this._ov.addEventListener('wheel', (e) => this._onWheel(e), { passive: false });
|
|
243
|
-
this.$('.stage').addEventListener('pointermove', (e) => { if (!this._drag) this.
|
|
244
|
-
this.$('.stage').addEventListener('pointerleave', () => this.
|
|
260
|
+
this.$('.stage').addEventListener('pointermove', (e) => { if (!this._drag) this._pv.lookAtPoint(e.clientX, e.clientY); });
|
|
261
|
+
this.$('.stage').addEventListener('pointerleave', () => this._pv.lookAt(0, 0));
|
|
245
262
|
this.addEventListener('keydown', (e) => {
|
|
246
263
|
if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'z') return;
|
|
247
264
|
const t = e.composedPath()[0];
|
|
@@ -253,9 +270,55 @@ export class AvatarEditor extends Base {
|
|
|
253
270
|
}
|
|
254
271
|
|
|
255
272
|
disconnectedCallback() {
|
|
273
|
+
this._artIO?.disconnect();
|
|
256
274
|
this._speech?.stop?.();
|
|
257
275
|
this._offLocale?.();
|
|
258
276
|
this._offLocale = null;
|
|
277
|
+
cancelAnimationFrame(this._raf3);
|
|
278
|
+
this._raf3 = 0;
|
|
279
|
+
this._ch3?.destroy();
|
|
280
|
+
this._ch3 = null;
|
|
281
|
+
this._ch3P = null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ───────────── vista previa 3D ─────────────
|
|
285
|
+
|
|
286
|
+
/** Vista previa activa: la 3D en «Personaje 3D» (si cargó), si no la 2D. Misma API. */
|
|
287
|
+
get _pv() {
|
|
288
|
+
return this._v.kind === 'plush' && this._ch3 ? this._ch3 : this._ch;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Carga (una vez) el ot 3D en la vista previa. Si falla, se queda la 2D con un aviso. */
|
|
292
|
+
_mount3d() {
|
|
293
|
+
if (this._ch3 || this._ch3P) return;
|
|
294
|
+
const p = (this._ch3P = loadCharacter3D()
|
|
295
|
+
.then((m) => {
|
|
296
|
+
if (!this.isConnected || this._ch3P !== p) return;
|
|
297
|
+
this._ch3 = m.createCharacter3D(this.$('.ch3'), this._v.character, {
|
|
298
|
+
material: this._v.material, orbit: true, stage: { key: 'overlay', zIndex: 2147483001, maxDpr: 1.5 },
|
|
299
|
+
});
|
|
300
|
+
if (this._ch3.fallback) throw new Error('webgl');
|
|
301
|
+
this._ch3.mood(this.$('[data-a=mood]')?.value || 'neutral', { fx: false });
|
|
302
|
+
this._renderKind();
|
|
303
|
+
})
|
|
304
|
+
.catch((err) => {
|
|
305
|
+
console.warn('[7ots] vista previa 3D no disponible:', err?.message || err);
|
|
306
|
+
this._ch3?.destroy?.();
|
|
307
|
+
this._ch3 = null;
|
|
308
|
+
this._failed3d = true;
|
|
309
|
+
if (!this.isConnected) return;
|
|
310
|
+
this._renderKind();
|
|
311
|
+
this._msg?.(this._t('editor.plush.unavailable'));
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Actualiza la vista 3D como mucho una vez por frame (reconstruir la geometría cuesta). */
|
|
316
|
+
_sync3d() {
|
|
317
|
+
if (!this._ch3 || this._raf3) return;
|
|
318
|
+
this._raf3 = requestAnimationFrame(() => {
|
|
319
|
+
this._raf3 = 0;
|
|
320
|
+
this._ch3?.update(this._v.character, { material: this._v.material });
|
|
321
|
+
});
|
|
259
322
|
}
|
|
260
323
|
|
|
261
324
|
// ───────────── estado ─────────────
|
|
@@ -269,6 +332,7 @@ export class AvatarEditor extends Base {
|
|
|
269
332
|
mut(next);
|
|
270
333
|
this._v.character = normalizeCharacter(next);
|
|
271
334
|
this._ch.update(this._v.character);
|
|
335
|
+
this._sync3d();
|
|
272
336
|
this._drawOverlay();
|
|
273
337
|
if (commit) this._commit(panel);
|
|
274
338
|
}
|
|
@@ -314,6 +378,7 @@ export class AvatarEditor extends Base {
|
|
|
314
378
|
|
|
315
379
|
_renderAll() {
|
|
316
380
|
this._ch.update(this._c);
|
|
381
|
+
this._sync3d();
|
|
317
382
|
this._renderKind();
|
|
318
383
|
this._renderPanel();
|
|
319
384
|
this._drawOverlay();
|
|
@@ -325,7 +390,7 @@ export class AvatarEditor extends Base {
|
|
|
325
390
|
const t = this._t;
|
|
326
391
|
const kinds = this.$('.kinds');
|
|
327
392
|
kinds.setAttribute('aria-label', t('editor.kinds.label'));
|
|
328
|
-
kinds.innerHTML =
|
|
393
|
+
kinds.innerHTML = KINDS.filter((k) => k !== 'plush' || !(this.hasAttribute('no-plush') || this.hasAttribute('no-realistic')))
|
|
329
394
|
.map((k) => `<button type="button" data-kind="${k}">${esc(t(`editor.kinds.${k}`))}</button>`)
|
|
330
395
|
.join('');
|
|
331
396
|
this._ov.setAttribute('aria-label', t('editor.controls'));
|
|
@@ -341,6 +406,9 @@ export class AvatarEditor extends Base {
|
|
|
341
406
|
<select class="btn" data-a="gesture" aria-label="${esc(t('editor.gesture'))}"><option value="">${esc(t('editor.gesturePick'))}</option>${GESTURES.map((g) => `<option value="${g}">${esc(partLabel(t, 'gestures', g))}</option>`).join('')}</select>
|
|
342
407
|
<select class="btn" data-a="morph" aria-label="${esc(t('editor.morph'))}"><option value="">${esc(t('editor.morphPick'))}</option>${MORPHS.map((m) => `<option value="${m}">${esc(partLabel(t, 'morphs', m))}</option>`).join('')}</select>`;
|
|
343
408
|
this.$('[data-a=mood]').value = mood;
|
|
409
|
+
this.$('[data-bar=material]').innerHTML = `<span class="hint" style="margin:0">${esc(t('editor.plush.material'))}</span>
|
|
410
|
+
<span class="chips">${MATERIALS.map((m) => `<button type="button" class="chip" data-material="${m}">${esc(t(`editor.plush.materials.${m}`))}</button>`).join('')}</span>
|
|
411
|
+
<span class="hint" style="margin:0">${esc(t('editor.plush.hint'))}</span>`;
|
|
344
412
|
this.$('[data-bar=export]').innerHTML = `
|
|
345
413
|
<button class="btn" type="button" data-a="svg">⬇ SVG</button>
|
|
346
414
|
<button class="btn" type="button" data-a="png">⬇ PNG</button>
|
|
@@ -350,11 +418,6 @@ export class AvatarEditor extends Base {
|
|
|
350
418
|
this.$('.tabs').innerHTML = TABS.map((id) => `<button type="button" role="tab" data-tab="${id}">${esc(t(`editor.tabs.${id}`))}</button>`).join('');
|
|
351
419
|
}
|
|
352
420
|
|
|
353
|
-
/** Texto del catálogo escapado, con variables que ya son HTML (p. ej. `{name}` → <b>…</b>). */
|
|
354
|
-
_html(key, html = {}) {
|
|
355
|
-
return esc(this._t(key)).replace(/\{(\w+)\}/g, (m, k) => html[k] ?? m);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
421
|
_syncBar() {
|
|
359
422
|
this.$('[data-a=undo]').disabled = !this._past.length;
|
|
360
423
|
this.$('[data-a=redo]').disabled = !this._future.length;
|
|
@@ -362,30 +425,20 @@ export class AvatarEditor extends Base {
|
|
|
362
425
|
|
|
363
426
|
_renderKind() {
|
|
364
427
|
const k = this._v.kind;
|
|
428
|
+
const plush = k === 'plush';
|
|
365
429
|
this.$$('[data-kind]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.kind === k)));
|
|
366
|
-
|
|
367
|
-
|
|
430
|
+
// «Personaje 3D» comparte la vista del 2D (mismas pestañas); solo cambia la vista previa
|
|
431
|
+
this.$$('[data-view]').forEach((v) => v.classList.toggle('hidden', v.dataset.view !== (plush ? 'character' : k)));
|
|
432
|
+
if (plush && !this._failed3d) this._mount3d();
|
|
433
|
+
const show3d = plush && !!this._ch3;
|
|
434
|
+
this.$('.ch').classList.toggle('hidden', show3d);
|
|
435
|
+
this.$('.ch3').classList.toggle('hidden', !show3d);
|
|
436
|
+
this.$('.ov').classList.toggle('hidden', plush);
|
|
437
|
+
this.$('[data-bar=material]').classList.toggle('hidden', !plush);
|
|
438
|
+
this.$$('[data-material]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.material === this._v.material)));
|
|
368
439
|
if (k === 'image') this._renderImage();
|
|
369
440
|
}
|
|
370
441
|
|
|
371
|
-
_renderRealistic() {
|
|
372
|
-
const a = this._v.avatar;
|
|
373
|
-
const t = this._t;
|
|
374
|
-
this.$('[data-view=realistic]').innerHTML = `
|
|
375
|
-
<div><h4>${esc(t('editor.realistic.title'))}</h4>
|
|
376
|
-
<p class="hint" style="margin:0">${esc(t('editor.realistic.intro'))}</p></div>
|
|
377
|
-
<label class="f">${esc(t('editor.realistic.url'))}<input type="url" data-m="avatar.url" placeholder="https://…/avatar.glb" value="${esc(a.url)}"></label>
|
|
378
|
-
<div><h4>${esc(t('editor.realistic.body'))}</h4><div class="chips">
|
|
379
|
-
<button type="button" class="chip" data-m-body="F" aria-pressed="${a.body === 'F'}">${esc(t('editor.realistic.bodyF'))}</button>
|
|
380
|
-
<button type="button" class="chip" data-m-body="M" aria-pressed="${a.body === 'M'}">${esc(t('editor.realistic.bodyM'))}</button></div></div>
|
|
381
|
-
<div><h4>${esc(t('editor.realistic.samples'))}</h4><div class="chips">${SAMPLE_3D.map((s, i) => `<button type="button" class="chip" data-sample="${i}" aria-pressed="${a.url === s.url}">${esc(s.label)}</button>`).join('')}</div></div>
|
|
382
|
-
<div class="card"><strong>${esc(t('editor.realistic.ownTitle'))}</strong>
|
|
383
|
-
<p class="hint">· ${this._html('editor.realistic.ownMpfb', { name: '<b>MPFB2 / MakeHuman</b>' })}<br>
|
|
384
|
-
· ${this._html('editor.realistic.ownVroid', { name: '<b>VRoid Studio</b>' })}<br>
|
|
385
|
-
· ${esc(t('editor.realistic.ownCommercial'))}</p>
|
|
386
|
-
<p class="hint">${esc(t('editor.realistic.fallback'))}</p></div>`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
442
|
_renderImage() {
|
|
390
443
|
const img = this._v.image;
|
|
391
444
|
const t = this._t;
|
|
@@ -409,11 +462,66 @@ export class AvatarEditor extends Base {
|
|
|
409
462
|
return `<div class="grid">${items
|
|
410
463
|
.map((it) => {
|
|
411
464
|
const label = esc(partLabel(this._t, kind, it.id));
|
|
412
|
-
return `<button type="button" class="opt" ${attr}="${it.id}" aria-pressed="${pressed(it)}" title="${label}">${renderCharacter(mk(it), { open: 0 }, { viewBox: view })}<span>${label}</span></button>`;
|
|
465
|
+
return `<button type="button" class="opt" ${attr}="${it.id}" aria-pressed="${pressed(it)}" title="${label}">${renderCharacter({ ...mk(it), style: 'none' }, { open: 0 }, { viewBox: view })}<span>${label}</span></button>`;
|
|
413
466
|
})
|
|
414
467
|
.join('')}</div>`;
|
|
415
468
|
}
|
|
416
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Tira de estilos artísticos (styles.js): el personaje actual en cada estilo, con su fondo.
|
|
472
|
+
* Son muchos SVG con filtros: cada miniatura se rasteriza una vez como <img> estática, solo
|
|
473
|
+
* cuando entra en vista, y se recuerda mientras el personaje (salvo el estilo) no cambie.
|
|
474
|
+
*/
|
|
475
|
+
_artStyles() {
|
|
476
|
+
const cur = this._c.style || 'none';
|
|
477
|
+
const opts = ART_STYLES.map((st) => {
|
|
478
|
+
const label = esc(partLabel(this._t, 'styles', st.id));
|
|
479
|
+
const src = this._artThumbs?.get(st.id);
|
|
480
|
+
return `<button type="button" class="opt" data-artstyle="${st.id}" aria-pressed="${st.id === cur}" title="${label}">${src ? `<img alt="" src="${src}">` : '<i></i>'}<span>${label}</span></button>`;
|
|
481
|
+
});
|
|
482
|
+
return `<h4>${esc(this._t('editor.artStyle.title'))}</h4><div class="grid arts">${opts.join('')}</div><p class="hint">${esc(this._t('editor.artStyle.hint'))}</p>`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Pinta las miniaturas pendientes de la tira según van apareciendo. */
|
|
486
|
+
_lazyArtThumbs() {
|
|
487
|
+
this._artIO?.disconnect();
|
|
488
|
+
const strip = this.$('.arts');
|
|
489
|
+
if (!strip) return;
|
|
490
|
+
const { style, ...rest } = this._c;
|
|
491
|
+
const view = this._thumbView('full');
|
|
492
|
+
const key = JSON.stringify(rest) + view;
|
|
493
|
+
if (this._artKey !== key) {
|
|
494
|
+
this._artKey = key;
|
|
495
|
+
this._artThumbs = new Map();
|
|
496
|
+
strip.querySelectorAll('img').forEach((img) => img.replaceWith(document.createElement('i')));
|
|
497
|
+
}
|
|
498
|
+
const fill = (btn) => {
|
|
499
|
+
const ph = btn.querySelector('i');
|
|
500
|
+
if (!ph) return;
|
|
501
|
+
const id = btn.dataset.artstyle;
|
|
502
|
+
let src = this._artThumbs.get(id);
|
|
503
|
+
if (!src) {
|
|
504
|
+
src = `data:image/svg+xml,${encodeURIComponent(renderCharacter({ ...rest, style: id }, { open: 0 }, { viewBox: view }))}`;
|
|
505
|
+
this._artThumbs.set(id, src);
|
|
506
|
+
}
|
|
507
|
+
const img = document.createElement('img');
|
|
508
|
+
img.alt = '';
|
|
509
|
+
img.src = src;
|
|
510
|
+
ph.replaceWith(img);
|
|
511
|
+
};
|
|
512
|
+
const todo = [...strip.querySelectorAll('.opt')].filter((b) => b.querySelector('i'));
|
|
513
|
+
if (!todo.length) return;
|
|
514
|
+
if (typeof IntersectionObserver !== 'function') return todo.forEach(fill);
|
|
515
|
+
this._artIO = new IntersectionObserver((entries, io) => {
|
|
516
|
+
for (const e of entries) {
|
|
517
|
+
if (!e.isIntersecting) continue;
|
|
518
|
+
io.unobserve(e.target);
|
|
519
|
+
fill(e.target);
|
|
520
|
+
}
|
|
521
|
+
}, { root: strip, rootMargin: '0px 90px' });
|
|
522
|
+
todo.forEach((b) => this._artIO.observe(b));
|
|
523
|
+
}
|
|
524
|
+
|
|
417
525
|
_range(path, key, min, max, step, value) {
|
|
418
526
|
return `<label class="f">${esc(this._t(key))}<input type="range" data-k="${path}" min="${min}" max="${max}" step="${step}" value="${value}"></label>`;
|
|
419
527
|
}
|
|
@@ -432,7 +540,7 @@ export class AvatarEditor extends Base {
|
|
|
432
540
|
let h = '';
|
|
433
541
|
switch (this._tab) {
|
|
434
542
|
case 'styles':
|
|
435
|
-
h =
|
|
543
|
+
h = `${this._artStyles()}<h4>${T('editor.styles.title')}</h4>${this._grid('presets', PRESETS, c.preset, 'data-preset', (p) => ({ preset: p.id }), full)}
|
|
436
544
|
<p class="hint">${T('editor.styles.hint')}</p>
|
|
437
545
|
<label class="c" style="margin-top:8px"><input type="checkbox" data-keep-body ${this._keepBody ? 'checked' : ''}> ${T('editor.styles.keepBody')}</label>`;
|
|
438
546
|
break;
|
|
@@ -451,6 +559,7 @@ export class AvatarEditor extends Base {
|
|
|
451
559
|
h = `<h4>${T('editor.color.body')}</h4><div class="sw">${SWATCHES.map((s) => `<button type="button" data-swatch="${s}" style="background:${s}" title="${s}" aria-label="${T('editor.color.swatch', { color: s })}"></button>`).join('')}</div>
|
|
452
560
|
<div class="row" style="margin-top:10px">${this._color('body.color', 'editor.color.custom', c.body.color)}</div>
|
|
453
561
|
<h4>${T('editor.color.finish')}</h4>${this._grid('finishes', FINISHES, c.finish, 'data-finish', (f) => with_({ finish: f.id }), full)}
|
|
562
|
+
<h4>${T('editor.color.texture')}</h4>${this._grid('textures', TEXTURES, c.texture, 'data-texture', (x) => with_({ texture: x.id }), full)}
|
|
454
563
|
<h4>${T('editor.color.outline')}</h4><div class="chips">${OUTLINES.map((v) => `<button type="button" class="chip" data-outline="${v}" aria-pressed="${c.outline === v}">${T(`editor.color.outlines.${v}`)}</button>`).join('')}</div>
|
|
455
564
|
<h4>${T('editor.color.pattern')}</h4>${this._grid('patterns', PATTERNS, c.body.pattern, 'data-pattern', (p) => with_({ body: { ...c.body, pattern: p.id } }), full)}
|
|
456
565
|
<div class="row" style="margin-top:10px">${this._color('body.patternColor', 'editor.color.patternColor', c.body.patternColor)}</div>`;
|
|
@@ -491,7 +600,13 @@ export class AvatarEditor extends Base {
|
|
|
491
600
|
break;
|
|
492
601
|
}
|
|
493
602
|
}
|
|
603
|
+
const scroll = this.$('.arts')?.scrollLeft || 0;
|
|
494
604
|
this.$('.panel').innerHTML = h;
|
|
605
|
+
const strip = this.$('.arts');
|
|
606
|
+
if (strip) {
|
|
607
|
+
strip.scrollLeft = scroll;
|
|
608
|
+
this._lazyArtThumbs();
|
|
609
|
+
} else this._artIO?.disconnect();
|
|
495
610
|
}
|
|
496
611
|
|
|
497
612
|
_accEditor() {
|
|
@@ -669,7 +784,10 @@ export class AvatarEditor extends Base {
|
|
|
669
784
|
if (!b) return;
|
|
670
785
|
const d = b.dataset;
|
|
671
786
|
const c = this._c;
|
|
672
|
-
if (d.kind)
|
|
787
|
+
if (d.kind) {
|
|
788
|
+
this._setMeta((v) => (v.kind = d.kind));
|
|
789
|
+
return this._drawOverlay();
|
|
790
|
+
}
|
|
673
791
|
if (d.tab) {
|
|
674
792
|
this._tab = d.tab;
|
|
675
793
|
this._renderPanel();
|
|
@@ -677,9 +795,11 @@ export class AvatarEditor extends Base {
|
|
|
677
795
|
return;
|
|
678
796
|
}
|
|
679
797
|
if (d.preset) return this._set((n) => Object.assign(n, applyPreset(n, d.preset, this._keepBody ? ['body', 'finish'] : [])));
|
|
798
|
+
if (d.artstyle) return this._set((n) => { n.style = d.artstyle; });
|
|
680
799
|
if (d.shape) return this._set((n) => { n.body.shape = d.shape; n.body.points = null; });
|
|
681
800
|
if (d.swatch) return this._set((n) => { n.body.color = d.swatch; });
|
|
682
801
|
if (d.finish) return this._set((n) => { n.finish = d.finish; });
|
|
802
|
+
if (d.texture) return this._set((n) => { n.texture = d.texture; });
|
|
683
803
|
if (d.outline) return this._set((n) => { n.outline = d.outline; });
|
|
684
804
|
if (d.pattern) return this._set((n) => { n.body.pattern = d.pattern; });
|
|
685
805
|
if (d.eyes) return this._set((n) => { n.eyes.type = d.eyes; });
|
|
@@ -705,8 +825,10 @@ export class AvatarEditor extends Base {
|
|
|
705
825
|
this._renderPanel();
|
|
706
826
|
return this._drawOverlay();
|
|
707
827
|
}
|
|
708
|
-
if (d.
|
|
709
|
-
|
|
828
|
+
if (d.material) {
|
|
829
|
+
this._setMeta((v) => (v.material = d.material));
|
|
830
|
+
return this._sync3d();
|
|
831
|
+
}
|
|
710
832
|
const i = this._sel;
|
|
711
833
|
switch (d.a) {
|
|
712
834
|
case 'undo': return this._undo();
|
|
@@ -745,14 +867,17 @@ export class AvatarEditor extends Base {
|
|
|
745
867
|
|
|
746
868
|
_onInput(e, final) {
|
|
747
869
|
const t = e.target;
|
|
748
|
-
if (t.dataset?.a === 'mood' && final)
|
|
870
|
+
if (t.dataset?.a === 'mood' && final) {
|
|
871
|
+
this._ch.mood(t.value);
|
|
872
|
+
return this._ch3?.mood(t.value);
|
|
873
|
+
}
|
|
749
874
|
if (t.dataset?.a === 'gesture' && final) {
|
|
750
|
-
if (t.value) this.
|
|
875
|
+
if (t.value) this._pv.gesture(t.value);
|
|
751
876
|
t.value = '';
|
|
752
877
|
return;
|
|
753
878
|
}
|
|
754
879
|
if (t.dataset?.a === 'morph' && final) {
|
|
755
|
-
if (t.value) this.
|
|
880
|
+
if (t.value) this._pv.morph(t.value);
|
|
756
881
|
t.value = '';
|
|
757
882
|
return;
|
|
758
883
|
}
|
|
@@ -764,7 +889,6 @@ export class AvatarEditor extends Base {
|
|
|
764
889
|
if (!final) return;
|
|
765
890
|
return this._setMeta((v) => {
|
|
766
891
|
if (t.dataset.m === 'image') v.image = t.value.trim();
|
|
767
|
-
else v.avatar.url = t.value.trim();
|
|
768
892
|
});
|
|
769
893
|
}
|
|
770
894
|
const k = t.dataset?.k;
|
|
@@ -785,7 +909,7 @@ export class AvatarEditor extends Base {
|
|
|
785
909
|
_talk() {
|
|
786
910
|
// Lip-sync por visemas a partir de una frase de ejemplo (en el idioma del editor).
|
|
787
911
|
this._speech?.stop?.();
|
|
788
|
-
this._speech = this.
|
|
912
|
+
this._speech = this._pv.speak(this._t('editor.talkSample'));
|
|
789
913
|
}
|
|
790
914
|
|
|
791
915
|
_exportSVG() {
|