RD5 GOLD WEB EDITOR
📄
📄 2-RB4-3CC.HTML
📄 222.JPG
📄 3CC-BASIC1.HTML
📄 Microsoft Paint.exe
📄 R2C-3.HTML
📄 R2C-4.HTML
📄 RB4-3C.HTML
📄 RB4-3CC.HTML
📄 RB4-RETRO.HTML
📄 RB4-RETRO.html
📄 RD5-3CC.html
📄 RL-4.html
📄 TARS.html
📄 TES10.mp3
📄 TES11.mp3
📄 TRISHA-OK.html
📄 TRISHA-WIN.HTML
📄 Tiny-demo.html
📄 Tinybj7-blindado.html
📄 Tinybj7-publica.html
📄 aplicaciones-rd5.html
📄 app1.html
📄 app2.html
📄 app3.html
📄 appb1.html
📄 appb2.html
📄 appb3.html
📄 appb4.html
📄 appb5.html
📄 appb6.html
📄 c-julio.JPG
📄 caratula.gif
📄 caratula02.gif
📄 caratula2.gif
📄 chokurei2.jpg
📄 columna-julio.html
📄 columna-stella.html
📄 columna-stellab.html
📄 demos.html
📄 df3.html
📄 dni.gif
📄 donaciones.gif
📄 ecualizador.html
📄 ed-podcast.JPG
📄 ed-radio.JPG
📄 editorv.html
📄 editorv1.html
📄 editorv2.html
📄 fondo5d.jpg
📄 fresi-clip.html
📄 gold.php
📄 gold62.php
📄 gold93.php
📄 googlefd161ffccea5f6ab.html
📄 gw.php
📄 index-anterior.html
📄 index.html
📄 index2.html
📄 logo.png
📄 logo.webp
📄 magazine.html
📄 manager3.php
📄 mic2.jpg
📄 mini1.html
📄 minib2a.html
📄 mn.php
📄 muestra.JPG
📄 p.html
📄 pm.html
📄 pm2.html
📄 pod-rd5.html
📄 podcast-stops.gif
📄 podcast2.mp3
📄 pp.html
📄 promo-tinybj7.html
📄 radio.html
📄 radio_gold.html
📄 rd5-fresia-4 canvashtml
📄 rd5-fresia-ok.html
📄 rd5-fresia.html
📄 rd5-plus-anterior.html
📄 rd5-plus.html
📄 rd5-plus2.html
📄 rd5-radioplayer.html
📄 rd5-tv.html
📄 rd5.html
📄 sst.html
📄 st.html
📄 starclip.html
📄 stella-2.JPG
📄 stella.JPG
📄 stt.html
📄 t-rd5.html.html
📄 t-rd6.html
📄 t-rd7.html
📄 t-tiny.html
📄 tes0.jpg
📄 tes1.jpg
📄 tes2.jpg
📄 tes4.jpg
📄 tes4.mp3
📄 tes5.jpg
📄 tes7.jpg
📄 tes8.jpg
📄 tienda.html
📄 tiendaaps.html
📄 tiendapps.html
📄 tiny-bj6.html
📄 tiny-bj7-full.html
📄 tiny-bj7.html
📄 tiny3-bj2.html
📄 tinybj7-blindado.html
📄 tinydemo.html
📄 titulo.jpg
📄 tnw2.html
📄 trisha-basic.html
📄 trisha-cafe.html
📄 trisha-mini.html
📄 tv-pantalla.php
📄 tv.html
📄 xdni.html
CÓDIGO (OSCURO GOLD)
VISTA PREVIA
💾 GUARDAR
Tema: Monokai
Tema: Dracula
Tema: Material
Tema: Blackboard
Tema: Abyss
Monospace
Courier
12px
14px
16px
18px
20px
22px
24px
26px
28px
30px
32px
↷ Rehacer
↶ Deshacer
🔍 BUSCAR
+ REPRO RADIO
SIG.
X
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>EDITOR_V5 + RENDER</title> <div style="font-size:20px; padding:10px;">🎬 STAR-CLIP V4</div> <hr style="border:none; border-top:1px solid WHITE; margin:0;"> </head> <body style="margin:0; background:#000; color:#fff; font-family:monospace; height:100vh; display:flex; flex-direction:column; overflow:hidden;"> <div style="display:flex; height:300px; border-bottom:1px solid #333; background:#050505; flex-shrink:0;"> <div id="monitor" style="width:300px; height:300px; background:#000; display:flex; align-items:center; justify-content:center; border-right:1px solid #333; flex-shrink:0; position:relative; overflow:hidden;"> <video id="mainVideo" style="max-width:100%; max-height:100%; display:none;"></video> <div id="imageDisplay" style="width:100%; height:100%; display:none; align-items:center; justify-content:center; overflow:hidden;"></div> </div> <div id="clipBar" style="flex:1; display:flex; align-items:center; gap:10px; padding:0 20px; overflow-x:auto; background:#080808;"></div> </div> <div id="moduleControls" style="display:flex; align-items:flex-start; gap:15px; padding:20px; background:#000; overflow:visible; flex-wrap:wrap;"></div> <div id="secondControlRow" style="display:flex; gap:10px; padding:0 20px 15px 20px; flex-wrap:wrap; border-top:1px solid #222;"></div> <!-- Barra de progreso de render --> <div id="renderOverlay" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.85); z-index:99999; flex-direction:column; align-items:center; justify-content:center; gap:20px;"> <div style="font-size:18px; color:#0f0; font-family:monospace;">RENDERIZANDO...</div> <div style="font-size:13px; color:#aaa; font-family:monospace;" id="renderStatus">Iniciando...</div> <div style="width:500px; max-width:90vw; background:#222; height:20px; border:1px solid #555; position:relative;"> <div id="renderBar" style="height:100%; background:#0f0; width:0%; transition:width 0.3s;"></div> </div> <div id="renderPercent" style="font-size:14px; color:#fff; font-family:monospace;">0%</div> </div> <!-- Canvas oculto para render --> <canvas id="renderCanvas" width="700" height="350" style="display:none;"></canvas> <input type="file" id="globalInput" multiple accept="video/*,image/*" style="display:none;"> <script> const BTN_STYLE = "background:#111; border:1px solid #eee; color:#fff; padding:10px 15px; cursor:pointer; font-family:monospace; font-size:12px; flex-shrink:0;"; window.Editor = { clips: [], currentIndex: 0, isPlaying: false, draggedIdx: null, vs: { scaleX: 1, zoom: 100, posX: 0, posY: 0, rotate: 0, translateX: 0, translateY: 0, brightness: 100, contrast: 100, blur: 0, flashBoost: 0, opacity: 1, crossfade: 1 }, _getEls() { return [ document.getElementById("mainVideo"), document.querySelector("#imageDisplay img") ].filter(Boolean); }, applyTransform() { const s = this.vs; const t = `scaleX(${s.scaleX}) scale(${s.zoom/100}) translate(${s.posX}%,${s.posY}%) rotate(${s.rotate}deg) translateX(${s.translateX}%) translateY(${s.translateY}%)`; this._getEls().forEach(el => { el.style.transformOrigin = "center center"; el.style.transform = t; }); }, applyFilter() { const s = this.vs; const b = Math.min(500, s.brightness + s.flashBoost); this._getEls().forEach(el => { el.style.filter = `brightness(${b}%) contrast(${s.contrast}%) blur(${s.blur}px)`; }); }, applyOpacity() { const op = Math.max(0, Math.min(1, this.vs.opacity * this.vs.crossfade)); this._getEls().forEach(el => { el.style.opacity = op; }); }, resetVisual(clip) { this.vs = { scaleX: clip.mirror ? -1 : 1, zoom: clip.zoom || 100, posX: clip.posX || 0, posY: clip.posY || 0, rotate: clip.rotate || 0, translateX: 0, translateY: 0, brightness: clip.bri || 100, contrast: clip.con || 100, blur: 0, flashBoost: 0, opacity: 1, crossfade: 1 }; this.applyTransform(); this.applyFilter(); this.applyOpacity(); }, register(mod) { if (mod.init) mod.init(); }, refreshUI() { const bar = document.getElementById("clipBar"); bar.innerHTML = ""; this.clips.forEach((c, i) => { const d = document.createElement("div"); d.draggable = true; d.style.cssText = `min-width:120px; height:80px; background:#111; border:${this.currentIndex==i?'2px solid #fff':'1px solid #444'}; cursor:move; flex-shrink:0; position:relative; overflow:hidden;`; if (c.thumb) d.innerHTML = `<img src="${c.thumb}" style="width:100%;height:100%;object-fit:cover;pointer-events:none;">`; d.ondragstart = () => { this.draggedIdx = i; d.style.opacity="0.5"; }; d.ondragover = e => e.preventDefault(); d.ondrop = e => { e.preventDefault(); const dc=this.clips.splice(this.draggedIdx,1)[0]; this.clips.splice(i,0,dc); this.refreshUI(); }; d.ondragend = () => { d.style.opacity="1"; }; const x = document.createElement("button"); x.innerText="×"; x.style.cssText="position:absolute;top:2px;right:2px;background:red;color:white;border:none;cursor:pointer;z-index:10;font-weight:bold;padding:2px 6px;"; x.onclick = e => { e.stopPropagation(); this.clips.splice(i,1); this.currentIndex=Math.max(0,this.currentIndex-1); this.refreshUI(); }; d.onclick = () => this.loadClip(i); d.appendChild(x); bar.appendChild(d); }); }, loadClip(i) { if (i < 0 || i >= this.clips.length) { this.isPlaying=false; document.getElementById("mainVideo").pause(); return; } this.currentIndex = i; const clip = this.clips[i]; const v = document.getElementById("mainVideo"); const imgCont = document.getElementById("imageDisplay"); clearTimeout(window.nextTimer); v.pause(); v.style.display="none"; imgCont.style.display="none"; this.refreshUI(); this.resetVisual(clip); document.dispatchEvent(new CustomEvent("clipChanged", { detail: clip })); if (clip.type.startsWith("video")) { v.style.display="block"; v.src = URL.createObjectURL(clip.file); v.volume = (clip.vol!==undefined ? clip.vol : 100) / 100; v.playbackRate = clip.playbackRate || 1; v.currentTime = clip.start || 0; v.onloadedmetadata = () => { if (this.isPlaying) v.play(); }; } else { imgCont.style.display="flex"; imgCont.innerHTML = `<img src="${URL.createObjectURL(clip.file)}" style="max-width:100%;max-height:100%;object-fit:contain;">`; this.applyTransform(); this.applyFilter(); this.applyOpacity(); if (this.isPlaying) window.nextTimer = setTimeout(() => this.loadClip(this.currentIndex+1), 3000); } } }; // ============================================= // SECUENCIA AUTOMÁTICA (PREVIEW) // ============================================= const mainVid = document.getElementById("mainVideo"); mainVid.ontimeupdate = function() { const clip = Editor.clips[Editor.currentIndex]; if (clip && clip.end && this.currentTime >= clip.end) { if (Editor.isPlaying) Editor.loadClip(Editor.currentIndex+1); else { this.pause(); this.currentTime = clip.start||0; } } }; mainVid.onended = function() { if (Editor.isPlaying) Editor.loadClip(Editor.currentIndex+1); }; // ============================================= // MÓDULO: SUBIR + PLAY // ============================================= Editor.register({ init() { const ctrl = document.getElementById("moduleControls"); const add = (txt,fn) => { const b=document.createElement("button"); b.innerText=txt; b.style.cssText=BTN_STYLE; b.onclick=fn; ctrl.appendChild(b); }; add("SUBIR ARCHIVOS", () => document.getElementById("globalInput").click()); add("PLAY / STOP", () => { Editor.isPlaying = !Editor.isPlaying; if (Editor.isPlaying) Editor.loadClip(Editor.currentIndex); else document.getElementById("mainVideo").pause(); }); document.getElementById("globalInput").onchange = e => { for (let f of e.target.files) { const obj = { file:f, type:f.type, bri:100, con:100, vol:100, start:0, end:0, zoom:100, posX:0, posY:0, mirror:false, rotate:0, playbackRate:1 }; Editor.clips.push(obj); if (f.type.startsWith("video")) { const v=document.createElement("video"); v.src=URL.createObjectURL(f); v.onloadeddata = () => { obj.end=v.duration; v.currentTime=0.5; v.onseeked = () => { const c=document.createElement("canvas"); c.width=160; c.height=90; c.getContext("2d").drawImage(v,0,0,160,90); obj.thumb=c.toDataURL(); Editor.refreshUI(); }; }; } else { obj.thumb=URL.createObjectURL(f); Editor.refreshUI(); } } }; }}); // ============================================= // MÓDULO: VOLUMEN // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="VOLUMEN"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;padding:15px;border:1px solid #fff;flex-direction:column;z-index:2000;min-width:140px;"; pop.innerHTML=`<div>NIVEL: <span id="v-val" style="color:#0f0;">100</span>%</div><input type="range" id="v-slid" min="0" max="100" value="100" style="width:100%">`; const s=pop.querySelector('#v-slid'),t=pop.querySelector('#v-val'); s.oninput=()=>{ t.innerText=s.value; const c=Editor.clips[Editor.currentIndex]; if(c)c.vol=s.value; document.getElementById("mainVideo").volume=s.value/100; }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ s.value=e.detail.vol||100; t.innerText=s.value; }); wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: RECORTAR // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="RECORTAR"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;padding:15px;border:1px solid #fff;flex-direction:column;z-index:2000;min-width:180px;"; pop.innerHTML=`<div>IN:<span id="ti-v" style="color:#0f0;">0</span>s | OUT:<span id="to-v" style="color:#0f0;">10</span>s</div><input type="range" id="ti-s" min="0" max="300" value="0" style="width:100%"><input type="range" id="to-s" min="0" max="300" value="10" style="width:100%;margin-top:5px;">`; const is=pop.querySelector('#ti-s'),os=pop.querySelector('#to-s'),iv=pop.querySelector('#ti-v'),ov=pop.querySelector('#to-v'); const upd=()=>{ const c=Editor.clips[Editor.currentIndex]; if(!c)return; if(parseFloat(is.value)>=parseFloat(os.value))is.value=os.value-0.5; iv.innerText=is.value; ov.innerText=os.value; c.start=parseFloat(is.value); c.end=parseFloat(os.value); document.getElementById("mainVideo").currentTime=c.start; }; pop.querySelectorAll('input').forEach(i=>i.oninput=upd); btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const c=e.detail; is.value=c.start||0; os.value=c.end||10; iv.innerText=is.value; ov.innerText=os.value; }); wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: FILTROS // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="FILTROS"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:2000;flex-direction:column;min-width:160px;"; pop.innerHTML=`<div>BRILLO: <span id="v-b" style="color:#0f0;">100</span>%</div><input type="range" id="s-b" min="0" max="200" value="100" style="width:100%;margin-bottom:10px;cursor:pointer;"><div>CONTRASTE: <span id="v-c" style="color:#0f0;">100</span>%</div><input type="range" id="s-c" min="0" max="200" value="100" style="width:100%;cursor:pointer;"><button id="res-f" style="margin-top:10px;background:#333;color:#fff;border:1px solid #555;padding:4px;cursor:pointer;font-size:10px;">RESET</button>`; const sb=pop.querySelector('#s-b'),sc=pop.querySelector('#s-c'),vb=pop.querySelector('#v-b'),vc=pop.querySelector('#v-c'); const aplicar=()=>{ const clip=Editor.clips[Editor.currentIndex]; if(!clip)return; clip.bri=sb.value; clip.con=sc.value; vb.innerText=sb.value; vc.innerText=sc.value; Editor.vs.brightness=parseFloat(sb.value); Editor.vs.contrast=parseFloat(sc.value); Editor.applyFilter(); }; sb.oninput=aplicar; sc.oninput=aplicar; pop.querySelector('#res-f').onclick=()=>{ sb.value=100; sc.value=100; aplicar(); }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const c=e.detail; sb.value=c.bri||100; sc.value=c.con||100; vb.innerText=sb.value; vc.innerText=sc.value; }); wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: FADES // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="FADES"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:5000;flex-direction:column;min-width:200px;"; pop.innerHTML=`<div>ENTRADA: <span id="vf-in">0</span>s</div><input type="range" id="sf-in" min="0" max="5" step="0.1" value="0" style="width:100%"><div style="margin-top:10px;">SALIDA: <span id="vf-out">0</span>s</div><input type="range" id="sf-out" min="0" max="5" step="0.1" value="0" style="width:100%">`; const sIn=pop.querySelector('#sf-in'),sOut=pop.querySelector('#sf-out'),vIn=pop.querySelector('#vf-in'),vOut=pop.querySelector('#vf-out'); const act=()=>{ const clip=Editor.clips[Editor.currentIndex]; if(!clip)return; vIn.innerText=sIn.value; vOut.innerText=sOut.value; clip.fadeIn=parseFloat(sIn.value); clip.fadeOut=parseFloat(sOut.value); }; sIn.oninput=act; sOut.oninput=act; let rafFade; const loopFade=()=>{ cancelAnimationFrame(rafFade); const clip=Editor.clips[Editor.currentIndex]; if(!clip||!Editor.isPlaying)return; const isVid=clip.type.startsWith("video"); const vid=document.getElementById("mainVideo"); const dur=isVid?(clip.end||vid.duration||10):3; const cur=isVid?vid.currentTime:(performance.now()-(window.imgStart||0))/1000; let op=1; if(clip.fadeIn>0&&cur<clip.fadeIn) op=cur/clip.fadeIn; else if(clip.fadeOut>0&&(dur-cur)<clip.fadeOut) op=(dur-cur)/clip.fadeOut; Editor.vs.opacity=Math.max(0,Math.min(1,op)); Editor.applyOpacity(); rafFade=requestAnimationFrame(loopFade); }; document.addEventListener("clipChanged",e=>{ const c=e.detail; sIn.value=c.fadeIn||0; sOut.value=c.fadeOut||0; vIn.innerText=sIn.value; vOut.innerText=sOut.value; window.imgStart=performance.now(); Editor.vs.opacity=1; Editor.applyOpacity(); if(Editor.isPlaying)loopFade(); }); btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: ZOOM/POS // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="ZOOM/POS"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;color:#fff;padding:10px;border:1px solid #fff;width:180px;z-index:10000;flex-direction:column;"; pop.innerHTML=`<div>ZOOM: <b id="vz">100</b>%</div><input type="range" id="sz" min="100" max="500" value="100" style="width:100%"><div style="margin-top:8px;">EJE X: <b id="vx">0</b>%</div><input type="range" id="sx" min="-100" max="100" value="0" style="width:100%"><div style="margin-top:8px;">EJE Y: <b id="vy">0</b>%</div><input type="range" id="sy" min="-100" max="100" value="0" style="width:100%"><button id="resetZoom" style="margin-top:10px;background:#444;border:none;color:white;padding:5px;cursor:pointer;">RESET</button>`; const sz=pop.querySelector('#sz'),vz=pop.querySelector('#vz'),sx=pop.querySelector('#sx'),vx=pop.querySelector('#vx'),sy=pop.querySelector('#sy'),vy=pop.querySelector('#vy'); const ap=()=>{ const c=Editor.clips[Editor.currentIndex]; if(!c)return; vz.innerText=sz.value; vx.innerText=sx.value; vy.innerText=sy.value; c.zoom=sz.value; c.posX=sx.value; c.posY=sy.value; Editor.vs.zoom=parseFloat(sz.value); Editor.vs.posX=parseFloat(sx.value); Editor.vs.posY=parseFloat(sy.value); Editor.applyTransform(); }; sz.oninput=sx.oninput=sy.oninput=ap; pop.querySelector('#resetZoom').onclick=()=>{ sz.value=100; sx.value=0; sy.value=0; ap(); }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const c=e.detail; sz.value=c.zoom||100; sx.value=c.posX||0; sy.value=c.posY||0; vz.innerText=sz.value; vx.innerText=sx.value; vy.innerText=sy.value; }); wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: ESPEJO // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="ESPEJO"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:2000;flex-direction:column;min-width:140px;"; pop.innerHTML=`<div style="margin-bottom:10px;font-size:11px;text-align:center;">VOLTEAR HORIZONTAL</div><button id="toggle-mirror" style="width:100%;background:#333;color:#fff;border:1px solid #555;padding:8px;cursor:pointer;font-size:10px;">DESACTIVADO</button>`; const tBtn=pop.querySelector('#toggle-mirror'); const ap=state=>{ Editor.vs.scaleX=state?-1:1; Editor.applyTransform(); tBtn.innerText=state?"ACTIVADO":"DESACTIVADO"; tBtn.style.background=state?"#0f0":"#333"; tBtn.style.color=state?"#000":"#fff"; }; tBtn.onclick=()=>{ const clip=Editor.clips[Editor.currentIndex]; if(!clip)return; clip.mirror=!clip.mirror; ap(clip.mirror); }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>ap(!!e.detail.mirror)); wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: VELOCIDAD // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="VELOCIDAD"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:2000;flex-direction:column;min-width:160px;"; pop.innerHTML=`<div>VELOCIDAD: <span id="v-rate" style="color:#0f0;">1.0</span>x</div><input type="range" id="s-rate" min="0.25" max="2" step="0.25" value="1" style="width:100%;cursor:pointer;margin-top:5px;">`; const slider=pop.querySelector('#s-rate'),vt=pop.querySelector('#v-rate'); slider.oninput=()=>{ const clip=Editor.clips[Editor.currentIndex]; if(!clip||clip.type.startsWith("image"))return; clip.playbackRate=slider.value; vt.innerText=slider.value; document.getElementById("mainVideo").playbackRate=slider.value; }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const clip=e.detail; if(clip.type.startsWith("video")){slider.disabled=false;slider.value=clip.playbackRate||1;vt.innerText=slider.value;}else{slider.disabled=true;vt.innerText="N/A";} }); wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: ROTAR // ============================================= Editor.register({ init() { const ctrl=document.getElementById("moduleControls"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="ROTAR"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;top:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:2000;flex-direction:column;min-width:120px;text-align:center;"; pop.innerHTML=`<div style="margin-bottom:10px;font-size:10px;">SENTIDO HORARIO</div><button id="btn-rot" style="width:100%;background:#333;color:#fff;border:1px solid #555;padding:10px;cursor:pointer;font-size:12px;">↻ +90°</button><div id="v-rot" style="margin-top:10px;font-size:10px;color:#0f0;">0°</div>`; const rotBtn=pop.querySelector('#btn-rot'),vt=pop.querySelector('#v-rot'); const ap=()=>{ const clip=Editor.clips[Editor.currentIndex]; if(!clip)return; vt.innerText=(clip.rotate||0)+"°"; Editor.vs.rotate=clip.rotate||0; Editor.applyTransform(); }; rotBtn.onclick=()=>{ const clip=Editor.clips[Editor.currentIndex]; if(!clip)return; clip.rotate=((clip.rotate||0)+90)%360; ap(); }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",()=>ap()); wrap.append(btn,pop); ctrl.appendChild(wrap); }}); // ============================================= // MÓDULO: MEZCLA // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="MEZCLA"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;bottom:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:3000;flex-direction:column;min-width:160px;"; pop.innerHTML=`<div style="color:#aaa;font-size:11px;margin-bottom:8px;">TIEMPO DE MEZCLA</div><span id="v-cross" style="color:#0f0;font-size:14px;">0.0</span>s<input type="range" id="s-cross" min="0" max="3" step="0.1" value="0" style="width:100%;cursor:pointer;margin-top:5px;">`; const slider=pop.querySelector('#s-cross'),vt=pop.querySelector('#v-cross'); setInterval(()=>{ const vid=document.getElementById("mainVideo"); const clip=Editor.clips[Editor.currentIndex]; if(!vid||!clip||!clip.crossFade||clip.crossFade<=0){if(Editor.vs.crossfade!==1){Editor.vs.crossfade=1;Editor.applyOpacity();}return;} const t=vid.currentTime,d=vid.duration||clip.end||10,f=clip.crossFade; let cf=1; if(t<f)cf=t/f; else if(t>d-f)cf=(d-t)/f; Editor.vs.crossfade=Math.max(0,Math.min(1,cf)); Editor.applyOpacity(); },30); slider.oninput=()=>{ const clip=Editor.clips[Editor.currentIndex]; if(clip){clip.crossFade=parseFloat(slider.value);vt.innerText=clip.crossFade.toFixed(1);} }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ slider.value=e.detail.crossFade||0; vt.innerText=parseFloat(slider.value).toFixed(1); }); wrap.append(btn,pop); row.appendChild(wrap); }}); // ============================================= // MÓDULO: BARRIDO // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="BARRIDO"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;bottom:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:3000;flex-direction:column;min-width:160px;"; pop.innerHTML=`<div style="color:#aaa;font-size:11px;margin-bottom:8px;">TIEMPO DE BARRIDO</div><span id="v-wipe" style="color:#0f0;font-size:14px;">0.0</span>s<input type="range" id="s-wipe" min="0" max="2" step="0.1" value="0" style="width:100%;cursor:pointer;margin-top:5px;"><button id="test-wipe" style="margin-top:12px;background:#444;color:#fff;border:none;padding:5px;cursor:pointer;font-size:10px;">PROBAR</button>`; const slider=pop.querySelector('#s-wipe'),vt=pop.querySelector('#v-wipe'); const ej=t=>{ if(t<=0)return; Editor.vs.translateX=-100; Editor.applyTransform(); const els=Editor._getEls(); els.forEach(el=>{void el.offsetWidth;el.style.transition=`transform ${t}s cubic-bezier(0.25,0.46,0.45,0.94)`;}); Editor.vs.translateX=0; Editor.applyTransform(); setTimeout(()=>els.forEach(el=>el.style.transition=""),t*1000+100); }; slider.oninput=()=>{ const c=Editor.clips[Editor.currentIndex]; if(c){c.wipeTime=parseFloat(slider.value);vt.innerText=c.wipeTime.toFixed(1);} }; pop.querySelector('#test-wipe').onclick=()=>ej(parseFloat(slider.value)); btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const c=e.detail; slider.value=c.wipeTime||0; vt.innerText=parseFloat(slider.value).toFixed(1); if(c.wipeTime>0)setTimeout(()=>ej(c.wipeTime),100); }); wrap.append(btn,pop); row.appendChild(wrap); }}); // ============================================= // MÓDULO: CAÍDA // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="CAÍDA"; btn.style.cssText=BTN_STYLE; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;bottom:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:3000;flex-direction:column;min-width:160px;"; pop.innerHTML=`<div style="color:#aaa;font-size:11px;margin-bottom:8px;">CAÍDA DESDE ARRIBA</div><span id="v-drop" style="color:#0f0;font-size:14px;">0.0</span>s<input type="range" id="s-drop" min="0" max="2" step="0.1" value="0" style="width:100%;cursor:pointer;margin-top:5px;"><button id="test-drop" style="margin-top:12px;background:#444;color:#fff;border:none;padding:5px;cursor:pointer;font-size:10px;">PROBAR</button>`; const slider=pop.querySelector('#s-drop'),vt=pop.querySelector('#v-drop'); const ej=t=>{ if(t<=0)return; Editor.vs.translateY=-100; Editor.applyTransform(); const els=Editor._getEls(); els.forEach(el=>{void el.offsetWidth;el.style.transition=`transform ${t}s ease-out`;}); Editor.vs.translateY=0; Editor.applyTransform(); setTimeout(()=>els.forEach(el=>el.style.transition=""),t*1000+100); }; slider.oninput=()=>{ const c=Editor.clips[Editor.currentIndex]; if(c){c.dropTime=parseFloat(slider.value);vt.innerText=c.dropTime.toFixed(1);} }; pop.querySelector('#test-drop').onclick=()=>ej(parseFloat(slider.value)); btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const c=e.detail; slider.value=c.dropTime||0; vt.innerText=parseFloat(slider.value).toFixed(1); if(c.dropTime>0)setTimeout(()=>ej(c.dropTime),100); }); wrap.append(btn,pop); row.appendChild(wrap); }}); // ============================================= // MÓDULO: FLASH // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="FLASH"; btn.style.cssText=BTN_STYLE+"background:#777;border-color:#fff;"; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;bottom:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:3000;flex-direction:column;min-width:160px;"; pop.innerHTML=`<div style="color:#aaa;font-size:11px;margin-bottom:8px;">DURACIÓN DEL DESTELLO</div><span id="v-flash" style="color:#0f0;font-size:14px;">0.0</span>s<input type="range" id="s-flash" min="0" max="1.5" step="0.1" value="0" style="width:100%;cursor:pointer;margin-top:5px;"><button id="test-flash" style="margin-top:12px;background:#fff;color:#000;border:none;padding:8px;cursor:pointer;font-size:10px;font-weight:bold;">Disparar Flash</button>`; const slider=pop.querySelector('#s-flash'),vt=pop.querySelector('#v-flash'); const ej=t=>{ if(t<=0)return; Editor.vs.flashBoost=400; Editor.applyFilter(); const els=Editor._getEls(); els.forEach(el=>{void el.offsetWidth;el.style.transition=`filter ${t}s ease-out`;}); Editor.vs.flashBoost=0; Editor.applyFilter(); setTimeout(()=>els.forEach(el=>el.style.transition=""),t*1000+100); }; slider.oninput=()=>{ const c=Editor.clips[Editor.currentIndex]; if(c){c.flashTime=parseFloat(slider.value);vt.innerText=c.flashTime.toFixed(1);} }; pop.querySelector('#test-flash').onclick=()=>ej(parseFloat(slider.value)); btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const c=e.detail; slider.value=c.flashTime||0; vt.innerText=parseFloat(slider.value).toFixed(1); if(c.flashTime>0)setTimeout(()=>ej(c.flashTime),50); }); wrap.append(btn,pop); row.appendChild(wrap); }}); // ============================================= // MÓDULO: PIXELAR // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="PIXELAR"; btn.style.cssText=BTN_STYLE+"background:#442255;border-color:#663377;"; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;bottom:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:4000;flex-direction:column;min-width:160px;"; pop.innerHTML=`<div style="color:#aaa;font-size:10px;margin-bottom:8px;">DURACIÓN PIXELADO</div><span id="v-pix" style="color:#0f0;">0.0</span>s<input type="range" id="s-pix" min="0" max="2" step="0.1" value="0" style="width:100%;cursor:pointer;margin-top:5px;"><button id="test-pix" style="margin-top:12px;background:#663377;color:#fff;border:none;padding:8px;cursor:pointer;font-size:10px;font-weight:bold;">PROBAR</button>`; const slider=pop.querySelector('#s-pix'),vt=pop.querySelector('#v-pix'); const ej=t=>{ if(t<=0)return; Editor.vs.blur=30; Editor.applyFilter(); const els=Editor._getEls(); els.forEach(el=>{void el.offsetWidth;el.style.transition=`filter ${t}s ease-out`;}); Editor.vs.blur=0; Editor.applyFilter(); setTimeout(()=>els.forEach(el=>el.style.transition=""),t*1000+100); }; slider.oninput=()=>{ const c=Editor.clips[Editor.currentIndex]; if(c){c.pixTime=parseFloat(slider.value);vt.innerText=c.pixTime.toFixed(1);} }; pop.querySelector('#test-pix').onclick=()=>ej(parseFloat(slider.value)); btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; document.addEventListener("clipChanged",e=>{ const c=e.detail; slider.value=c.pixTime||0; vt.innerText=parseFloat(slider.value).toFixed(1); if(c.pixTime>0)setTimeout(()=>ej(c.pixTime),100); }); wrap.append(btn,pop); row.appendChild(wrap); }}); // ============================================= // MÓDULO: IRIS & GLOW // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const ej_iris=t=>{ if(t<=0)return; Editor._getEls().forEach(el=>{el.style.transition="none";el.style.clipPath="circle(0% at 50% 50%)";void el.offsetWidth;el.style.transition=`clip-path ${t}s ease-out`;el.style.clipPath="circle(100% at 50% 50%)";setTimeout(()=>{el.style.clipPath="none";el.style.transition="";},t*1000+100);}); }; const ej_glow=t=>{ if(t<=0)return; Editor.vs.flashBoost=200;Editor.vs.blur=15;Editor.applyFilter(); const els=Editor._getEls(); els.forEach(el=>{void el.offsetWidth;el.style.transition=`filter ${t}s ease-in-out`;}); Editor.vs.flashBoost=0;Editor.vs.blur=0;Editor.applyFilter(); setTimeout(()=>els.forEach(el=>el.style.transition=""),t*1000+100); }; const crearMod=(label,bg,key,fn)=>{ const wrap=document.createElement("div"); wrap.style.position="relative"; wrap.innerHTML=`<button style="${BTN_STYLE}background:${bg};">${label}</button><div class="pop" style="display:none;position:absolute;bottom:45px;left:0;background:#111;padding:15px;border:1px solid #fff;z-index:5000;flex-direction:column;min-width:150px;"><div style="font-size:10px;color:#aaa;margin-bottom:5px;">${label} (Seg)</div><input type="range" class="sv" min="0" max="2" step="0.1" value="0" style="width:100%;"><button class="tb" style="margin-top:10px;background:${bg};color:#fff;border:none;padding:5px;cursor:pointer;">PROBAR</button></div>`; wrap.querySelector('button').onclick=e=>{ e.stopPropagation(); const p=wrap.querySelector('.pop'); p.style.display=p.style.display==="none"?"flex":"none"; }; wrap.querySelector('.sv').oninput=()=>{ const c=Editor.clips[Editor.currentIndex]; if(c)c[key]=parseFloat(wrap.querySelector('.sv').value); }; wrap.querySelector('.tb').onclick=()=>fn(parseFloat(wrap.querySelector('.sv').value)); document.addEventListener("clipChanged",e=>{ const val=e.detail[key]||0; wrap.querySelector('.sv').value=val; if(val>0)setTimeout(()=>fn(val),50); }); row.appendChild(wrap); }; crearMod("IRIS","#500","irisTime",ej_iris); crearMod("GLOW","#550","glowTime",ej_glow); }}); // ============================================= // MÓDULO: TITULADOR // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const monitor=document.getElementById("monitor"); const old=document.getElementById("video-overlay-text"); if(old)old.remove(); const ot=document.createElement("div"); ot.id="video-overlay-text"; ot.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;font-family:'Arial Black',sans-serif;font-size:30px;text-align:center;text-shadow:3px 3px 0 #000,-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000;pointer-events:none;z-index:999;display:none;line-height:1.2;"; monitor.appendChild(ot); const wrap=document.createElement("div"); wrap.style.position="relative"; const btn=document.createElement("button"); btn.innerText="TEXTO"; btn.style.cssText=BTN_STYLE+"background:#003366;color:#fff;border-color:#00ffff;"; const pop=document.createElement("div"); pop.style.cssText="display:none;position:absolute;bottom:45px;left:0;background:#111;padding:15px;border:1px solid #00ffff;z-index:5000;flex-direction:column;min-width:220px;gap:8px;"; pop.innerHTML=`<input type="text" id="t-input" placeholder="Escribe aquí..." style="width:100%;background:#000;color:#fff;border:1px solid #0af;padding:5px;"><div style="font-size:9px;color:#0af;">POSICIÓN X / Y</div><input type="range" id="t-x" min="0" max="100" value="50" style="width:100%;"><input type="range" id="t-y" min="0" max="100" value="50" style="width:100%;"><div style="font-size:9px;color:#0af;">TAMAÑO</div><input type="range" id="t-s" min="10" max="150" value="30" style="width:100%;"><button id="t-btn-show" style="margin-top:5px;background:#0af;color:#000;border:none;padding:5px;font-weight:bold;cursor:pointer;">ACTUALIZAR</button>`; const rf=()=>{ const txt=pop.querySelector("#t-input").value,x=pop.querySelector("#t-x").value,y=pop.querySelector("#t-y").value,s=pop.querySelector("#t-s").value; ot.innerText=txt; ot.style.left=x+"%";ot.style.top=y+"%";ot.style.fontSize=s+"px";ot.style.display=txt.trim()!==""?"block":"none"; const c=Editor.clips[Editor.currentIndex]; if(c){c.t_text=txt;c.t_x=x;c.t_y=y;c.t_s=s;} }; btn.onclick=e=>{ e.stopPropagation(); pop.style.display=pop.style.display==="none"?"flex":"none"; }; pop.querySelector("#t-btn-show").onclick=rf; pop.querySelectorAll("input").forEach(i=>i.oninput=rf); document.addEventListener("clipChanged",e=>{ const c=e.detail; pop.querySelector("#t-input").value=c.t_text||""; pop.querySelector("#t-x").value=c.t_x||50; pop.querySelector("#t-y").value=c.t_y||50; pop.querySelector("#t-s").value=c.t_s||30; rf(); }); wrap.append(btn,pop); row.appendChild(wrap); }}); // ============================================= // MÓDULO: FULLSCREEN // ============================================= Editor.register({ init() { const row=document.getElementById("secondControlRow"); const btn=document.createElement("button"); btn.innerText="FULLSCREEN"; btn.style.cssText=BTN_STYLE+"background:#111;color:#0f0;border-color:#0f0;font-weight:bold;"; btn.onclick=e=>{ e.stopPropagation(); const c=document.getElementById("monitor"); if(!document.fullscreenElement){(c.requestFullscreen||c.webkitRequestFullscreen).call(c);}else{document.exitFullscreen&&document.exitFullscreen();} }; document.addEventListener("fullscreenchange",()=>{ const v=document.getElementById("mainVideo"); if(document.fullscreenElement){v.style.width="100vw";v.style.height="100vh";v.style.objectFit="contain";btn.innerText="SALIR FS";}else{v.style.width="";v.style.height="";v.style.objectFit="";btn.innerText="FULLSCREEN";} }); row.appendChild(btn); }}); // ============================================= // MÓDULO: EXPORTAR VIDEO (RENDER ENGINE) // ============================================= Editor.register({ init() { const row = document.getElementById("secondControlRow"); const btn = document.createElement("button"); btn.innerText = "⬇ EXPORTAR VIDEO"; btn.style.cssText = BTN_STYLE + "background:#003300;color:#0f0;border-color:#0f0;font-weight:bold;font-size:13px;"; btn.onclick = () => startRender(); row.appendChild(btn); }}); // ============================================= // MOTOR DE RENDER // ============================================= async function startRender() { if (Editor.clips.length === 0) { alert("No hay clips para renderizar."); return; } const overlay = document.getElementById("renderOverlay"); const barEl = document.getElementById("renderBar"); const statusEl = document.getElementById("renderStatus"); const percentEl = document.getElementById("renderPercent"); overlay.style.display = "flex"; const canvas = document.getElementById("renderCanvas"); const ctx = canvas.getContext("2d"); const W = canvas.width, H = canvas.height; const FPS = 24; // Calcular duración total para progreso let totalDuration = 0; Editor.clips.forEach(c => { totalDuration += c.type.startsWith("video") ? ((c.end||0) - (c.start||0)) : 3; }); let totalFramesRendered = 0; const totalFrames = Math.ceil(totalDuration * FPS); const setProgress = (pct, msg) => { barEl.style.width = pct + "%"; percentEl.innerText = Math.round(pct) + "%"; if (msg) statusEl.innerText = msg; }; // Audio let audioCtx, audioDest; try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); audioDest = audioCtx.createMediaStreamDestination(); } catch(e) { console.warn("AudioContext no disponible:", e); } // captureStream(30) = MediaRecorder samplea el canvas a exactamente 30fps constantes. // Nosotros solo nos encargamos de dibujar lo más rápido posible con rVFC. const canvasStream = canvas.captureStream(30); const tracks = [...canvasStream.getVideoTracks()]; if (audioDest) tracks.push(...audioDest.stream.getAudioTracks()); const finalStream = new MediaStream(tracks); const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus') ? 'video/webm;codecs=vp9,opus' : 'video/webm'; const recorder = new MediaRecorder(finalStream, { mimeType, videoBitsPerSecond: 12000000 }); const chunks = []; recorder.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); }; recorder.start(100); // Solo actualizar barra de progreso — MediaRecorder samplea solo por su cuenta const commitFrame = () => { totalFramesRendered++; setProgress(Math.min(99, (totalFramesRendered / totalFrames) * 100)); }; // Función auxiliar: dibujar un frame en el canvas con todos los efectos del clip function drawFrame(source, clip, progress) { ctx.clearRect(0, 0, W, H); ctx.fillStyle = "#000"; ctx.fillRect(0, 0, W, H); const bri = clip.bri || 100; const con = clip.con || 100; const rot = (clip.rotate || 0) * Math.PI / 180; const scaleX = clip.mirror ? -1 : 1; const zoom = (clip.zoom || 100) / 100; const posX = ((clip.posX || 0) / 100) * W; const posY = ((clip.posY || 0) / 100) * H; // Duración del clip const clipDur = clip.type.startsWith("video") ? ((clip.end||10)-(clip.start||0)) : 3; // Fade in/out — globalAlpha let alpha = 1; const fi = clip.fadeIn || 0; const fo = clip.fadeOut || 0; const elapsed = progress * clipDur; if (fi > 0 && elapsed < fi) alpha = elapsed / fi; else if (fo > 0 && (clipDur - elapsed) < fo) alpha = (clipDur - elapsed) / fo; // Crossfade al inicio/fin del clip const cf = clip.crossFade || 0; if (cf > 0) { if (elapsed < cf) alpha = Math.min(alpha, elapsed / cf); else if ((clipDur-elapsed) < cf) alpha = Math.min(alpha, (clipDur-elapsed) / cf); } // Transición BARRIDO: translateX interpolado al inicio const wipeT = clip.wipeTime || 0; let wipeOffsetX = 0; if (wipeT > 0 && elapsed < wipeT) wipeOffsetX = (1 - elapsed/wipeT) * -W; // Transición CAÍDA: translateY interpolado al inicio const dropT = clip.dropTime || 0; let dropOffsetY = 0; if (dropT > 0 && elapsed < dropT) dropOffsetY = (1 - elapsed/dropT) * -H; // Transición IRIS: clip circular creciente const irisT = clip.irisTime || 0; let irisProgress = 1; if (irisT > 0 && elapsed < irisT) irisProgress = elapsed / irisT; // Flash: boost de brillo al inicio del clip const flashT = clip.flashTime || 0; let flashBoost = 0; if (flashT > 0 && elapsed < flashT) flashBoost = (1 - elapsed/flashT) * 400; // Pixelar: blur al inicio del clip const pixT = clip.pixTime || 0; let blurVal = 0; if (pixT > 0 && elapsed < pixT) blurVal = (1 - elapsed/pixT) * 20; // Glow const glowT = clip.glowTime || 0; let glowBoost = 0, glowBlur = 0; if (glowT > 0 && elapsed < glowT) { glowBoost = (1-elapsed/glowT)*200; glowBlur = (1-elapsed/glowT)*15; } // Aplicar filtros CSS al canvas context const totalBri = Math.min(500, bri + flashBoost + glowBoost); const totalBlur = blurVal + glowBlur; ctx.filter = `brightness(${totalBri}%) contrast(${con}%)${totalBlur > 0 ? ` blur(${totalBlur.toFixed(1)}px)` : ''}`; ctx.globalAlpha = Math.max(0, Math.min(1, alpha)); // Aplicar IRIS clip path si está activo if (irisT > 0 && irisProgress < 1) { ctx.save(); ctx.beginPath(); const radius = irisProgress * Math.sqrt(W*W + H*H); ctx.arc(W/2, H/2, radius, 0, Math.PI*2); ctx.clip(); } // Transformaciones geométricas ctx.save(); ctx.translate(W/2 + posX + wipeOffsetX, H/2 + posY + dropOffsetY); ctx.rotate(rot); ctx.scale(scaleX * zoom, zoom); // Dimensiones del source const sw = source.videoWidth || source.naturalWidth || W; const sh = source.videoHeight || source.naturalHeight || H; const aspect = sw / sh; let dw, dh; if (aspect > W/H) { dw = W; dh = W/aspect; } else { dh = H; dw = H*aspect; } ctx.drawImage(source, -dw/2, -dh/2, dw, dh); ctx.restore(); // Restaurar iris clip if (irisT > 0 && irisProgress < 1) ctx.restore(); // Resetear filtros para el texto (no queremos filtros en el overlay) ctx.filter = "none"; ctx.globalAlpha = Math.max(0, Math.min(1, alpha)); // Overlay de texto if (clip.t_text && clip.t_text.trim() !== "") { const fs = parseFloat(clip.t_s || 30) * (W / 300); const tx = ((clip.t_x || 50) / 100) * W; const ty = ((clip.t_y || 50) / 100) * H; ctx.font = `bold ${fs}px Arial Black, sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.shadowColor = "#000"; ctx.shadowBlur = 4; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3; ctx.fillStyle = "#fff"; ctx.fillText(clip.t_text, tx, ty); ctx.shadowColor = "transparent"; } ctx.globalAlpha = 1; ctx.filter = "none"; } // ── RENDER VIDEO: reproducción real + requestVideoFrameCallback ── // El video corre a velocidad normal. Por cada frame decodificado: // 1. lo dibujamos en canvas 2. llamamos requestFrame() para grabarlo // Sin seeks → rápido. Sin timers → perfectamente fluido. function renderVideoClip(clip, clipIndex, totalClips) { return new Promise((resolve) => { const vid = document.createElement("video"); vid.src = URL.createObjectURL(clip.file); vid.playbackRate = clip.playbackRate || 1; vid.crossOrigin = "anonymous"; // Conectar audio al mixer de grabación if (audioCtx && audioDest) { try { const src = audioCtx.createMediaElementSource(vid); const gain = audioCtx.createGain(); gain.gain.value = (clip.vol !== undefined ? parseFloat(clip.vol) : 100) / 100; src.connect(gain); gain.connect(audioDest); } catch(e) {} } const startT = clip.start || 0; const endT = clip.end || 0; const clipDur = Math.max(0.033, endT - startT); let done = false; const finish = () => { if (done) return; done = true; vid.pause(); vid.src = ""; resolve(); }; vid.onloadedmetadata = () => { vid.currentTime = startT; }; vid.onseeked = () => { if (done) return; statusEl.innerText = `Clip ${clipIndex+1}/${totalClips}: renderizando video...`; vid.play().catch(finish); // requestVideoFrameCallback: dispara exactamente cuando el decodificador // produce un frame nuevo — es el callback más preciso disponible en browsers const useRVFC = typeof vid.requestVideoFrameCallback === "function"; const onFrame = () => { if (done) return; const elapsed = Math.max(0, vid.currentTime - startT); const progress = Math.min(1, elapsed / clipDur); drawFrame(vid, clip, progress); commitFrame(); // graba exactamente este frame en MediaRecorder if (vid.currentTime >= endT - 0.04 || vid.ended) { finish(); return; } if (useRVFC) vid.requestVideoFrameCallback(onFrame); }; if (useRVFC) { vid.requestVideoFrameCallback(onFrame); } else { // Fallback para browsers sin rVFC (Safari < 15, Firefox sin flag) const rafLoop = () => { if (!done) { onFrame(); requestAnimationFrame(rafLoop); } }; requestAnimationFrame(rafLoop); } }; // Seguro: si el video termina antes de llegar al fin del clip vid.onended = finish; vid.onerror = finish; }); } // ── RENDER IMAGEN: dibuja la imagen a 30fps durante 3 segundos ── function renderImageClip(clip, clipIndex, totalClips) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { statusEl.innerText = `Clip ${clipIndex+1}/${totalClips}: renderizando imagen...`; const totalF = Math.ceil(3 * FPS); let f = 0; const loop = () => { if (f >= totalF) { resolve(); return; } drawFrame(img, clip, f / totalF); commitFrame(); f++; requestAnimationFrame(loop); }; requestAnimationFrame(loop); }; img.onerror = resolve; img.src = URL.createObjectURL(clip.file); }); } // ── LOOP PRINCIPAL ── for (let i = 0; i < Editor.clips.length; i++) { const clip = Editor.clips[i]; if (clip.type.startsWith("video")) { await renderVideoClip(clip, i, Editor.clips.length); } else { await renderImageClip(clip, i, Editor.clips.length); } } // ── FINALIZAR ── setProgress(99, "Finalizando..."); await new Promise(r => setTimeout(r, 400)); // dar tiempo al último chunk recorder.stop(); recorder.onstop = () => { setProgress(100, "Listo. Descargando..."); const blob = new Blob(chunks, { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "video_exportado.webm"; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 5000); setTimeout(() => { overlay.style.display = "none"; }, 1500); if (audioCtx) audioCtx.close().catch(() => {}); }; } // Cerrar popups al click fuera document.addEventListener("click", () => { document.querySelectorAll('#moduleControls div>div,#secondControlRow div>div').forEach(p => { if (p.style.display==="flex") p.style.display="none"; }); }); </script> <label for="mensaje"> <center>TEXTO PARA LA TITULADORA<br></label><width="650" > <FONT COLOR="WHITE"> <textarea id="mensaje" name="mensaje" rows="5" > </textarea> </body> </html>