Capas independientes, projectM en el mapping y catalogo de la galeria
Mapping - Los clips salian del reves: mapper.js activaba UNPACK_FLIP_Y_WEBGL, pero el modelo va con origen arriba-izquierda y texImage2D ya sube la primera fila en t=0, asi que el flip la mandaba al final. Comprobado renderizando el mapper real en Chromium headless con una fuente mitad roja / mitad azul. - projectM se puede usar YA en una capa: los .milk de data/presets-projectm se traducen a Butterchurn en el navegador al elegirlos (milkdrop-preset- converter). La capa MilkDrop gana 'biblioteca' (butter | projectm) sin cambiar su firma, para no recrear el contexto WebGL al cambiar de una a otra. Medido: 100/100 presets de una muestra convierten, 84/84 de los que llevan shaders warp/comp; ~7 ms por preset. - Cada capa tiene su pestana arriba y su propia vista, y '+ CAPA' crea una y entra en ella: con varias capas, ir y volver a MAPPING no era viable. - Dos capas MilkDrop sin preset ya no salen identicas (cogian el indice 0): cada instancia elige uno al azar y lo escribe en el estado. - Una capa nueva ya no nace en la fuente "motor", que es el lienzo del motor activo y hacia que dos capas ensenaran lo mismo. Galeria de visuales - scripts/catalogar-visuales.py: ficha de cada clip (pelicula, personajes, duracion) y, sobre todo, si es una silueta de verdad y cuanta figura tiene. Distingue silueta de corte crudo por el negro puro del fondo: 0,63-0,90 frente a 0,04-0,06, sin zona gris. - El panel lista los clips agrupados por pelicula, con nombre legible y aviso de los que casi no tienen figura, en vez del nombre del archivo. - Soporte de siluetas con canal alfa (.webm VP9): transparencia de verdad, sin recorte por luminancia. El shader del mapper ya la respeta. Documentacion - docs/visuales.md nuevo; pendiente.md al dia con lo hecho y lo que queda. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5d5223db9f
commit
f017246f0e
30 changed files with 3771 additions and 292 deletions
|
|
@ -1,26 +1,34 @@
|
|||
/*
|
||||
* FOSFENO :: Mapper (projection mapping en el navegador) — Fase 2
|
||||
* FOSFENO :: Mapper (projection mapping en el navegador) — Fase 3
|
||||
* --------------------------------------------------------------------------
|
||||
* Coge el canvas del motor activo como TEXTURA y lo pinta deformado sobre
|
||||
* SUPERFICIES con MALLA (rejilla n x n deformable => superficies curvas, no
|
||||
* solo keystone de 4 esquinas) y aplica MASCARAS (cuadrilateros negros que
|
||||
* Pinta CAPAS deformadas sobre superficies con MALLA (rejilla n x n => curvas,
|
||||
* no solo keystone de 4 esquinas) y aplica MASCARAS (cuadrilateros negros que
|
||||
* tapan zonas / recortan derrames de luz).
|
||||
*
|
||||
* Cada superficie es una CAPA con su propia fuente: el motor activo, una
|
||||
* instancia propia de MilkDrop, un shader, un clip o la camara (las crea
|
||||
* layers.js). Asi se puede tener MilkDrop a la derecha, otro preset a la
|
||||
* izquierda y un video en el centro, cada uno encajado en su sitio.
|
||||
*
|
||||
* Modelo (0..1, origen arriba-izquierda; se guarda en data/mapping.json):
|
||||
* surface = { id, n, points:[[x,y] x (n+1)^2] } // rejilla n x n celdas
|
||||
* mask = { id, corners:[[x,y] x4] } // TL,TR,BR,BL, negro
|
||||
* surface = { id, n, points:[[x,y] x (n+1)^2],
|
||||
* nombre, visible, opacidad, mezcla:"normal"|"sumar",
|
||||
* fuente:{ tipo, ... } } // ver layers.js
|
||||
* mask = { id, corners:[[x,y] x4] } // TL,TR,BR,BL, negro
|
||||
* mapping = { enabled, edit, surfaces:[...], masks:[...] }
|
||||
*
|
||||
* Compatibilidad: superficies antiguas { corners:[...] } se migran a n=1.
|
||||
* Compatibilidad: superficies antiguas { corners:[...] } se migran a n=1, y
|
||||
* las que no llevan fuente se pintan con el motor activo, como siempre.
|
||||
*
|
||||
* Render: cada celda de la malla se dibuja como un parche con homografia
|
||||
* (cuadrado unidad -> celda) sobre una sub-malla fina => perspectiva correcta.
|
||||
*
|
||||
* API global:
|
||||
* FosMapper.init({ onChange }) arranca
|
||||
* FosMapper.setSource(canvas) motor activo (o null)
|
||||
* FosMapper.setMapping(m) estado de mapping
|
||||
* onChange({surfaces, masks}) al editar (arrastrar), para guardar
|
||||
* FosMapper.init({ onChange, resolver }) arranca
|
||||
* FosMapper.setSource(canvas) lienzo del motor activo (fuente "motor")
|
||||
* FosMapper.setMapping(m) estado de mapping
|
||||
* onChange({surfaces, masks}) al editar (arrastrar), para guardar
|
||||
* resolver(surface) -> { el, clave } con la textura de la capa
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
|
@ -32,11 +40,15 @@
|
|||
const M = { enabled: false, edit: false, surfaces: [], masks: [] };
|
||||
let source = null;
|
||||
let onChange = null;
|
||||
let resolver = null; // surface -> { el, clave } (lo pone layers.js)
|
||||
let aviso = function () {}; // report() del escenario: nada se queda callado
|
||||
|
||||
let outputEl, editEl, gl, ectx;
|
||||
let prog, aUV, uH, uTex, uOff, uScale; // programa de textura
|
||||
let progF, aPos, uColor, fillBuf; // programa de relleno (mascaras)
|
||||
let tex, gridBuf, gridIdx, gridCount, dpr = 1;
|
||||
let prog, aUV, uH, uTex, uOff, uScale, uAlpha; // programa de textura
|
||||
let progF, aPos, uColor, fillBuf; // programa de relleno (mascaras)
|
||||
let gridBuf, gridIdx, gridCount, dpr = 1;
|
||||
const texturas = new Map(); // clave de fuente -> { tex, subida, visto }
|
||||
let frame = 0;
|
||||
|
||||
let selected = null; // { type:'surface'|'mask', id }
|
||||
let drag = null; // { type, id, pt|null, sx, sy, orig }
|
||||
|
|
@ -46,16 +58,39 @@
|
|||
const sidx = (c, r, n) => r * (n + 1) + c;
|
||||
|
||||
// ------------------------------------------------------------------ modelo
|
||||
/* Normaliza una superficie: geometria a formato malla y ajustes de capa con
|
||||
sus valores por defecto. Lo que no venga (superficies de antes de las
|
||||
capas) se rellena para que el resto del codigo no tenga que preguntar. */
|
||||
function migrate(s) {
|
||||
const capa = {
|
||||
id: s.id,
|
||||
nombre: s.nombre || '',
|
||||
visible: s.visible !== false,
|
||||
opacidad: typeof s.opacidad === 'number' ? s.opacidad : 1,
|
||||
mezcla: s.mezcla === 'sumar' ? 'sumar' : 'normal',
|
||||
fuente: s.fuente || { tipo: 'motor' },
|
||||
};
|
||||
if (Array.isArray(s.points) && s.n) {
|
||||
return { id: s.id, n: s.n, points: s.points.map((p) => [p[0], p[1]]) };
|
||||
}
|
||||
if (Array.isArray(s.corners)) { // formato Fase 1 -> n=1
|
||||
capa.n = s.n; capa.points = s.points.map((p) => [p[0], p[1]]);
|
||||
} else if (Array.isArray(s.corners)) { // formato Fase 1 -> n=1
|
||||
const c = s.corners;
|
||||
return { id: s.id, n: 1, points: [[c[0][0], c[0][1]], [c[1][0], c[1][1]],
|
||||
[c[3][0], c[3][1]], [c[2][0], c[2][1]]] }; // TL,TR,BL,BR (row-major)
|
||||
capa.n = 1;
|
||||
capa.points = [[c[0][0], c[0][1]], [c[1][0], c[1][1]],
|
||||
[c[3][0], c[3][1]], [c[2][0], c[2][1]]]; // TL,TR,BL,BR (row-major)
|
||||
} else {
|
||||
capa.n = 1;
|
||||
capa.points = [[0.3, 0.3], [0.7, 0.3], [0.3, 0.7], [0.7, 0.7]];
|
||||
}
|
||||
return { id: s.id, n: 1, points: [[0.3, 0.3], [0.7, 0.3], [0.3, 0.7], [0.7, 0.7]] };
|
||||
return capa;
|
||||
}
|
||||
|
||||
/* Mascaras saneadas. Una sin esquinas (mapping.json a medio escribir, de
|
||||
otra version o tocado a mano) no puede tumbar el escenario entero: se
|
||||
descarta y punto. */
|
||||
function migrateMasks(lista) {
|
||||
return (Array.isArray(lista) ? lista : [])
|
||||
.filter((k) => k && Array.isArray(k.corners) && k.corners.length === 4)
|
||||
.map((k) => ({ id: k.id, corners: k.corners.map((p) => [p[0], p[1]]) }));
|
||||
}
|
||||
|
||||
// punto [x,y] a coordenada normalizada (u,v) de la superficie (bilineal por celda)
|
||||
|
|
@ -74,7 +109,11 @@
|
|||
const pts = [];
|
||||
for (let r = 0; r <= newN; r++)
|
||||
for (let c = 0; c <= newN; c++) pts.push(sampleSurf(s, c / newN, r / newN));
|
||||
return { id: s.id, n: newN, points: pts };
|
||||
// Solo cambia la malla: los ajustes de la capa (fuente, nombre, opacidad)
|
||||
// se conservan tal cual.
|
||||
const out = migrate(s);
|
||||
out.n = newN; out.points = pts;
|
||||
return out;
|
||||
}
|
||||
|
||||
// homografia cuadrado unidad -> cuadrilatero (Heckbert), mat3 por columnas
|
||||
|
|
@ -117,7 +156,11 @@
|
|||
function initGL() {
|
||||
gl = outputEl.getContext('webgl', { alpha: false, antialias: true })
|
||||
|| outputEl.getContext('experimental-webgl');
|
||||
if (!gl) { console.error('[FOSFENO] mapper: sin WebGL'); return false; }
|
||||
if (!gl) {
|
||||
aviso('error', 'El mapping necesita WebGL y este navegador no lo tiene: '
|
||||
+ 'las visuales saldran sin deformar.');
|
||||
return false;
|
||||
}
|
||||
|
||||
prog = linkProg(
|
||||
`attribute vec2 aUV; uniform mat3 uH; uniform vec2 uOff; uniform vec2 uScale;
|
||||
|
|
@ -126,12 +169,15 @@
|
|||
gl_Position = vec4(d.x*2.0-1.0, 1.0-d.y*2.0, 0.0, 1.0);
|
||||
vUV = uOff + aUV*uScale; }`,
|
||||
`precision mediump float; varying vec2 vUV; uniform sampler2D uTex;
|
||||
void main(){ gl_FragColor = texture2D(uTex, vUV); }`);
|
||||
uniform float uAlpha;
|
||||
void main(){ vec4 c = texture2D(uTex, vUV);
|
||||
gl_FragColor = vec4(c.rgb, c.a * uAlpha); }`);
|
||||
aUV = gl.getAttribLocation(prog, 'aUV');
|
||||
uH = gl.getUniformLocation(prog, 'uH');
|
||||
uTex = gl.getUniformLocation(prog, 'uTex');
|
||||
uOff = gl.getUniformLocation(prog, 'uOff');
|
||||
uScale = gl.getUniformLocation(prog, 'uScale');
|
||||
uAlpha = gl.getUniformLocation(prog, 'uAlpha');
|
||||
|
||||
progF = linkProg(
|
||||
`attribute vec2 aPos; void main(){ gl_Position = vec4(aPos,0.0,1.0); }`,
|
||||
|
|
@ -158,16 +204,53 @@
|
|||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gridIdx);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(idx), gl.STATIC_DRAW);
|
||||
|
||||
tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||
/* SIN voltear la textura al subirla. El modelo va con el origen
|
||||
arriba-izquierda (vUV.y = 0 es el borde de arriba de la superficie) y
|
||||
texImage2D ya sube la primera fila de la imagen en t = 0. Poniendo
|
||||
UNPACK_FLIP_Y a true, t = 0 pasaba a ser la ultima fila y todo salia
|
||||
del reves — se notaba sobre todo con los clips de video. */
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
gl.enable(gl.BLEND);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Una textura por fuente, subida una sola vez por fotograma: si dos
|
||||
superficies comparten el mismo clip, la imagen no viaja dos veces. */
|
||||
function texturaDe(src) {
|
||||
const el = src.el;
|
||||
if (!el) return null;
|
||||
const w = el.videoWidth || el.naturalWidth || el.width;
|
||||
const h = el.videoHeight || el.naturalHeight || el.height;
|
||||
if (!w || !h) return null;
|
||||
if (el.tagName === 'VIDEO' && el.readyState < 2) return null;
|
||||
let e = texturas.get(src.clave);
|
||||
if (!e) {
|
||||
e = { tex: gl.createTexture(), subida: -1, visto: -1 };
|
||||
gl.bindTexture(gl.TEXTURE_2D, e.tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
texturas.set(src.clave, e);
|
||||
}
|
||||
e.visto = frame;
|
||||
if (e.subida !== frame) {
|
||||
e.subida = frame;
|
||||
gl.bindTexture(gl.TEXTURE_2D, e.tex);
|
||||
try { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el); }
|
||||
catch (err) { return null; }
|
||||
}
|
||||
return e.tex;
|
||||
}
|
||||
|
||||
/* Tira las texturas de fuentes que ya no se usan (capas borradas). */
|
||||
function limpiarTexturas() {
|
||||
if (frame % 300) return;
|
||||
texturas.forEach((e, clave) => {
|
||||
if (frame - e.visto > 120) { gl.deleteTexture(e.tex); texturas.delete(clave); }
|
||||
});
|
||||
}
|
||||
|
||||
function drawSurfaceGL(s) {
|
||||
const n = s.n;
|
||||
const P = (c, r) => s.points[sidx(c, r, n)];
|
||||
|
|
@ -182,32 +265,63 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Pantalla entera: lo que se ve cuando el mapping esta activo pero aun no
|
||||
// hay ninguna superficie creada (pasada directa del motor).
|
||||
const PANTALLA = { id: '__pantalla__', n: 1, visible: true, opacidad: 1,
|
||||
mezcla: 'normal', fuente: { tipo: 'motor' },
|
||||
points: [[0, 0], [1, 0], [0, 1], [1, 1]] };
|
||||
|
||||
// La fuente de una capa: la resuelve layers.js; si no hay resolver (o la
|
||||
// capa es del motor), tira del lienzo del motor activo.
|
||||
function fuenteDeCapa(capa) {
|
||||
if (resolver) {
|
||||
const r = resolver(capa);
|
||||
if (r && r.el) return r;
|
||||
if (capa.fuente && capa.fuente.tipo !== 'motor') return null;
|
||||
}
|
||||
return (source && source.width) ? { el: source, clave: 'motor' } : null;
|
||||
}
|
||||
|
||||
function renderGL() {
|
||||
frame++;
|
||||
gl.viewport(0, 0, outputEl.width, outputEl.height);
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
if (!source || !source.width) return;
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
try { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source); }
|
||||
catch (e) { return; }
|
||||
|
||||
// superficies
|
||||
// superficies (cada una con su propia fuente)
|
||||
gl.useProgram(prog);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, gridBuf);
|
||||
gl.enableVertexAttribArray(aUV);
|
||||
gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gridIdx);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.uniform1i(uTex, 0);
|
||||
if (!M.surfaces.length) {
|
||||
drawSurfaceGL({ n: 1, points: [[0, 0], [1, 0], [0, 1], [1, 1]] }); // pasada directa
|
||||
} else {
|
||||
for (const s of M.surfaces) drawSurfaceGL(migrate(s));
|
||||
}
|
||||
|
||||
// mascaras (cuadrilateros negros encima)
|
||||
// M.surfaces ya viene normalizado de setMapping / onDown / onDblClick:
|
||||
// no hace falta migrar otra vez en cada fotograma (eran cientos de
|
||||
// arrays nuevos por segundo, justo en la maquina que menos lo aguanta).
|
||||
const lista = M.surfaces.length ? M.surfaces : [PANTALLA];
|
||||
for (const capa of lista) {
|
||||
if (!capa.visible || capa.opacidad <= 0) continue;
|
||||
if (capa.fuente.tipo === 'negro') continue;
|
||||
const src = fuenteDeCapa(capa);
|
||||
if (!src) continue;
|
||||
const t = texturaDe(src);
|
||||
if (!t) continue;
|
||||
gl.bindTexture(gl.TEXTURE_2D, t);
|
||||
gl.uniform1f(uAlpha, capa.opacidad);
|
||||
// "sumar" apila luz (las zonas oscuras dejan ver la capa de abajo);
|
||||
// "normal" tapa lo que haya debajo.
|
||||
if (capa.mezcla === 'sumar') gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
|
||||
else gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
drawSurfaceGL(capa);
|
||||
}
|
||||
limpiarTexturas();
|
||||
|
||||
// mascaras (cuadrilateros negros encima). Vuelve a mezcla normal: si la
|
||||
// ultima capa iba en "sumar", un negro sumado no taparia nada.
|
||||
if (M.masks.length) {
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.useProgram(progF);
|
||||
gl.uniform4f(uColor, 0, 0, 0, 1);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, fillBuf);
|
||||
|
|
@ -225,6 +339,28 @@
|
|||
}
|
||||
|
||||
// ------------------------------------------------------------------ editor
|
||||
const NOMBRE_FUENTE = {
|
||||
motor: 'motor activo', butter: 'MilkDrop', shader: 'shader',
|
||||
video: 'clip', camara: 'camara', negro: 'negro',
|
||||
};
|
||||
function etiquetaFuente(f) {
|
||||
f = f || {};
|
||||
const base = NOMBRE_FUENTE[f.tipo] || 'motor activo';
|
||||
if (f.tipo === 'butter' && f.preset) return base + ' · ' + f.preset;
|
||||
if (f.tipo === 'video' && f.archivo) return base + ' · ' + f.archivo;
|
||||
return base;
|
||||
}
|
||||
|
||||
/* Rotulo centrado que no se sale de la pantalla: una capa pegada al borde
|
||||
tendria el nombre cortado por la mitad. */
|
||||
function rotulo(txt, x, y, w) {
|
||||
let t = txt;
|
||||
while (t.length > 6 && ectx.measureText(t).width > w - 24) t = t.slice(0, -2);
|
||||
if (t !== txt) t = t.slice(0, -1) + '…';
|
||||
const media = ectx.measureText(t).width / 2;
|
||||
ectx.fillText(t, Math.min(Math.max(x, media + 10), w - media - 10), y);
|
||||
}
|
||||
|
||||
function drawEditor() {
|
||||
const w = window.innerWidth, h = window.innerHeight;
|
||||
ectx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
|
@ -232,9 +368,9 @@
|
|||
ectx.font = '600 14px system-ui, sans-serif';
|
||||
ectx.textAlign = 'center'; ectx.textBaseline = 'middle';
|
||||
|
||||
// superficies (malla)
|
||||
M.surfaces.forEach((raw, i) => {
|
||||
const s = migrate(raw); const n = s.n;
|
||||
// superficies (malla). Ya vienen normalizadas de setMapping.
|
||||
M.surfaces.forEach((s, i) => {
|
||||
const n = s.n;
|
||||
const sel = selected && selected.type === 'surface' && selected.id === s.id;
|
||||
const col = sel ? '#00e5ff' : '#b4ff00';
|
||||
const P = (c, r) => { const p = s.points[sidx(c, r, n)]; return [p[0] * w, p[1] * h]; };
|
||||
|
|
@ -242,9 +378,16 @@
|
|||
ectx.strokeStyle = col; ectx.lineWidth = sel ? 2 : 1.3;
|
||||
for (let r = 0; r <= n; r++) { ectx.beginPath(); for (let c = 0; c <= n; c++) { const p = P(c, r); c ? ectx.lineTo(p[0], p[1]) : ectx.moveTo(p[0], p[1]); } ectx.stroke(); }
|
||||
for (let c = 0; c <= n; c++) { ectx.beginPath(); for (let r = 0; r <= n; r++) { const p = P(c, r); r ? ectx.lineTo(p[0], p[1]) : ectx.moveTo(p[0], p[1]); } ectx.stroke(); }
|
||||
// numero
|
||||
const cc = P(n / 2 | 0, n / 2 | 0);
|
||||
ectx.fillStyle = col; ectx.fillText(String(i + 1), cc[0], cc[1]);
|
||||
// nombre de la capa y su fuente, en el centro de verdad: el medio de
|
||||
// las cuatro esquinas (con n=1, P(n/2|0, n/2|0) caia en la esquina).
|
||||
const esq = [P(0, 0), P(n, 0), P(n, n), P(0, n)];
|
||||
const cc = [(esq[0][0] + esq[1][0] + esq[2][0] + esq[3][0]) / 4,
|
||||
(esq[0][1] + esq[1][1] + esq[2][1] + esq[3][1]) / 4];
|
||||
ectx.fillStyle = col;
|
||||
rotulo(s.nombre || ('Capa ' + (i + 1)), cc[0], cc[1] - 9, w);
|
||||
ectx.font = '12px system-ui, sans-serif';
|
||||
rotulo(etiquetaFuente(s.fuente), cc[0], cc[1] + 10, w);
|
||||
ectx.font = '600 14px system-ui, sans-serif';
|
||||
// tiradores (todos los puntos)
|
||||
for (let r = 0; r <= n; r++) for (let c = 0; c <= n; c++) {
|
||||
const p = P(c, r);
|
||||
|
|
@ -364,7 +507,9 @@
|
|||
function onUp() { if (drag && dragged) emitNow(); drag = null; }
|
||||
|
||||
function defaultSurface(cx, cy, s) {
|
||||
return { id: 's' + Math.floor(performance.now()) + '_' + Math.floor(Math.random() * 1e4), n: 1,
|
||||
return { id: 's' + Math.floor(performance.now()) + '_' + Math.floor(Math.random() * 1e4),
|
||||
n: 1, nombre: '', visible: true, opacidad: 1, mezcla: 'normal',
|
||||
fuente: { tipo: 'motor' },
|
||||
points: [[clamp01(cx - s), clamp01(cy - s)], [clamp01(cx + s), clamp01(cy - s)],
|
||||
[clamp01(cx - s), clamp01(cy + s)], [clamp01(cx + s), clamp01(cy + s)]] };
|
||||
}
|
||||
|
|
@ -394,7 +539,7 @@
|
|||
function emitNow() {
|
||||
if (emitTimer) { clearTimeout(emitTimer); emitTimer = null; }
|
||||
if (onChange) onChange({
|
||||
surfaces: M.surfaces.map((s) => { const m = migrate(s); return { id: m.id, n: m.n, points: m.points }; }),
|
||||
surfaces: M.surfaces.map(migrate), // geometria + ajustes de la capa
|
||||
masks: M.masks.map((m) => ({ id: m.id, corners: m.corners })),
|
||||
});
|
||||
}
|
||||
|
|
@ -422,10 +567,24 @@
|
|||
init(opts) {
|
||||
opts = opts || {};
|
||||
onChange = opts.onChange || null;
|
||||
resolver = opts.resolver || null;
|
||||
aviso = opts.report || function () {};
|
||||
outputEl = document.getElementById('output');
|
||||
editEl = document.getElementById('mapedit');
|
||||
if (!outputEl || !editEl) { console.error('[FOSFENO] mapper: faltan #output/#mapedit'); return; }
|
||||
if (!outputEl || !editEl) {
|
||||
aviso('error', 'Falta el lienzo del mapping en la pagina del '
|
||||
+ 'escenario: recarga el escenario.');
|
||||
return;
|
||||
}
|
||||
ectx = editEl.getContext('2d');
|
||||
// Si el navegador tira este contexto (demasiadas capas a la vez), el
|
||||
// mapping se queda negro en silencio: que al menos se sepa por que.
|
||||
outputEl.addEventListener('webglcontextlost', (e) => {
|
||||
e.preventDefault();
|
||||
aviso('error', 'El navegador ha cerrado el contexto grafico del '
|
||||
+ 'mapping (demasiadas capas a la vez). Quita alguna capa de '
|
||||
+ 'MilkDrop o de shader y recarga el escenario.');
|
||||
});
|
||||
if (!initGL()) return;
|
||||
editEl.addEventListener('mousedown', onDown);
|
||||
window.addEventListener('mousemove', onMove);
|
||||
|
|
@ -440,8 +599,10 @@
|
|||
M.enabled = !!m.enabled;
|
||||
M.edit = !!m.edit;
|
||||
if (!drag) { // no piso la geometria mientras arrastro aqui
|
||||
if (Array.isArray(m.surfaces)) M.surfaces = m.surfaces.map(migrate);
|
||||
M.masks = Array.isArray(m.masks) ? m.masks.map((k) => ({ id: k.id, corners: k.corners.map((p) => [p[0], p[1]]) })) : [];
|
||||
if (Array.isArray(m.surfaces)) {
|
||||
M.surfaces = m.surfaces.filter((s) => s && s.id).map(migrate);
|
||||
}
|
||||
M.masks = migrateMasks(m.masks);
|
||||
if (selected) {
|
||||
const ok = selected.type === 'surface' ? findSurf(selected.id) >= 0 : findMask(selected.id) >= 0;
|
||||
if (!ok) selected = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue