seawolf2357 commited on
Commit
43af6e2
ยท
verified ยท
1 Parent(s): 1a0b15a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -40
app.py CHANGED
@@ -44,8 +44,8 @@ body{{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#f5f5f7
44
  .media-item-icon{{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:18px}}
45
  .media-item-dur{{position:absolute;top:2px;right:2px;background:rgba(0,0,0,0.7);padding:1px 3px;border-radius:2px;font-size:8px;color:#fff}}
46
  .preview-area{{flex:1;display:flex;flex-direction:column;background:#1a1a1a;margin:6px;border-radius:8px;overflow:hidden}}
47
- .preview-box{{flex:1;display:flex;align-items:center;justify-content:center;background:#000}}
48
- #previewCanvas{{max-width:100%;max-height:100%;background:#000}}
49
  .controls{{height:45px;background:#222;display:flex;align-items:center;justify-content:center;gap:6px}}
50
  .ctrl-btn{{width:28px;height:28px;border:none;border-radius:50%;background:rgba(255,255,255,0.1);color:#fff;cursor:pointer;font-size:12px}}
51
  .ctrl-btn:hover{{background:rgba(255,255,255,0.2)}}
@@ -104,7 +104,19 @@ body{{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#f5f5f7
104
  <div class="editor">
105
  <div class="toolbar">
106
  <div class="toolbar-title">๐ŸŽฌ Video Editor</div>
107
- <div style="display:flex;gap:4px">
 
 
 
 
 
 
 
 
 
 
 
 
108
  <button class="btn btn-secondary" onclick="undo()">โ†ฉ ์‹คํ–‰์ทจ์†Œ</button>
109
  <button class="btn btn-success" onclick="exportVideo()">๐Ÿ“ฅ ๋‚ด๋ณด๋‚ด๊ธฐ</button>
110
  </div>
@@ -185,15 +197,46 @@ cancelled:false,
185
  els:{{}},
186
  canvas:null,
187
  ctx:null,
188
- lastClipId:null
 
 
 
 
 
 
 
 
 
189
  }};
190
 
191
  function init(){{
192
  S.canvas=document.getElementById('previewCanvas');
193
  S.ctx=S.canvas.getContext('2d');
 
194
  drawPlaceholder();
195
  }}
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  function id(){{return Math.random().toString(36).substr(2,9)}}
198
  function fmt(t){{if(!t||isNaN(t))t=0;var m=Math.floor(t/60),s=Math.floor(t%60),ms=Math.floor((t%1)*100);return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')+'.'+String(ms).padStart(2,'0')}}
199
  function r(n){{return Math.round(n*1000)/1000}}
@@ -201,12 +244,14 @@ function stat(m){{document.getElementById('status').textContent=m}}
201
  function save(){{S.history.push(JSON.stringify(S.clips));if(S.history.length>30)S.history.shift()}}
202
 
203
  function drawPlaceholder(){{
 
 
204
  S.ctx.fillStyle='#000';
205
- S.ctx.fillRect(0,0,640,360);
206
  S.ctx.fillStyle='#444';
207
  S.ctx.font='14px sans-serif';
208
  S.ctx.textAlign='center';
209
- S.ctx.fillText('ํƒ€์ž„๋ผ์ธ์— ๋ฏธ๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”',320,180);
210
  }}
211
 
212
  function addMedia(name,type,url,filePath){{
@@ -540,8 +585,12 @@ return null;
540
  function drawFrame(){{
541
  var t=S.time;
542
  var vc=getClipAt(t,'visual');
 
 
 
543
  S.ctx.fillStyle='#000';
544
- S.ctx.fillRect(0,0,640,360);
 
545
  if(vc){{
546
  var el=S.els[vc.mid];
547
  if(el){{
@@ -555,19 +604,16 @@ if(!S.playing&&!el.paused)el.pause();
555
  el.volume=S.muted?0:vc.vol;
556
  }}
557
  try{{
558
- var sw=el.videoWidth||el.naturalWidth||el.width||640;
559
- var sh=el.videoHeight||el.naturalHeight||el.height||360;
560
- var scale=Math.min(640/sw,360/sh);
561
- var dw=sw*scale,dh=sh*scale;
562
- var dx=(640-dw)/2,dy=(360-dh)/2;
563
- S.ctx.drawImage(el,dx,dy,dw,dh);
564
  }}catch(e){{}}
565
  }}
566
  }}else if(S.clips.length===0){{
567
  S.ctx.fillStyle='#444';
568
  S.ctx.font='14px sans-serif';
569
  S.ctx.textAlign='center';
570
- S.ctx.fillText('ํƒ€์ž„๋ผ์ธ์— ๋ฏธ๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”',320,180);
571
  }}
572
  var audioClips=S.clips.filter(function(c){{
573
  if(c.type!=='audio')return false;
@@ -596,7 +642,40 @@ if(!vc&&!audioClips.length&&S.clips.length>0){{
596
  S.ctx.fillStyle='#333';
597
  S.ctx.font='12px sans-serif';
598
  S.ctx.textAlign='center';
599
- S.ctx.fillText('์žฌ์ƒ ์œ„์น˜์— ๋ฏธ๋””์–ด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค',320,180);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  }}
601
  }}
602
 
@@ -625,19 +704,22 @@ doExport();
625
  }}
626
 
627
  async function doExport(){{
 
 
 
628
  // ๋ฉ”์ธ ์บ”๋ฒ„์Šค (MediaRecorder ์—ฐ๊ฒฐ์šฉ)
629
  var canvas=document.createElement('canvas');
630
- canvas.width=1280;canvas.height=720;
631
  var ctx=canvas.getContext('2d');
632
 
633
  // ์˜คํ”„์Šคํฌ๋ฆฐ ์บ”๋ฒ„์Šค (๋ Œ๋”๋ง์šฉ - ๋”๋ธ” ๋ฒ„ํผ๋ง)
634
  var offCanvas=document.createElement('canvas');
635
- offCanvas.width=1280;offCanvas.height=720;
636
  var offCtx=offCanvas.getContext('2d');
637
 
638
  // ์ดˆ๊ธฐ ๊ฒ€์€ ํ™”๋ฉด
639
  ctx.fillStyle='#000';
640
- ctx.fillRect(0,0,1280,720);
641
 
642
  var stream=canvas.captureStream(30);
643
 
@@ -654,16 +736,17 @@ var rec=new MediaRecorder(stream,opts);
654
  var chunks=[];
655
  rec.ondataavailable=function(e){{if(e.data.size>0)chunks.push(e.data)}};
656
 
657
- document.getElementById('exportMsg').textContent='๋…นํ™” ์ค‘...';
658
  rec.start(100);
659
 
660
  var dur=S.dur;
661
  var fps=30;
662
- var frameInterval=1000/fps; // 33.33ms
663
  var totalFrames=Math.ceil(dur*fps);
664
  var startTime=performance.now();
 
665
 
666
- // ์‹ค์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง (์ •ํ™•ํ•œ ํƒ€์ด๋ฐ)
667
  await new Promise(function(resolve){{
668
  var frameCount=0;
669
  var lastVideoEl=null;
@@ -673,7 +756,7 @@ function render(){{
673
  if(S.cancelled){{rec.stop();resolve();return}}
674
 
675
  var elapsed=performance.now()-startTime;
676
- var t=elapsed/1000; // ํ˜„์žฌ ์‹œ๊ฐ„ (์ดˆ)
677
 
678
  if(t>=dur){{
679
  rec.stop();
@@ -681,13 +764,11 @@ setTimeout(resolve,100);
681
  return;
682
  }}
683
 
684
- // ํ”„๋กœ๊ทธ๋ ˆ์Šค ์—…๋ฐ์ดํŠธ
685
  document.getElementById('exportBar').style.width=(t/dur*100)+'%';
686
  document.getElementById('exportMsg').textContent='๋…นํ™” ์ค‘... '+Math.round(t/dur*100)+'% ('+t.toFixed(1)+'/'+dur.toFixed(1)+'์ดˆ)';
687
 
688
- // ์˜คํ”„์Šคํฌ๋ฆฐ ์บ”๋ฒ„์Šค์— ๋ Œ๋”๋ง
689
  offCtx.fillStyle='#000';
690
- offCtx.fillRect(0,0,1280,720);
691
 
692
  var vc=getClipAt(t,'visual');
693
  if(vc){{
@@ -695,16 +776,12 @@ var el=S.els[vc.mid];
695
  if(el){{
696
  if(vc.type==='video'){{
697
  var clipT=t-vc.start+vc.ts;
698
-
699
- // ํด๋ฆฝ์ด ๋ฐ”๋€Œ๋ฉด seekํ•˜๊ณ  ์žฌ์ƒ
700
  if(lastClipId!==vc.id){{
701
  el.currentTime=clipT;
702
  el.play().catch(function(){{}});
703
  lastClipId=vc.id;
704
  lastVideoEl=el;
705
  }}
706
-
707
- // ์ด์ „ ๋น„๋””์˜ค ์ •์ง€
708
  if(lastVideoEl&&lastVideoEl!==el&&!lastVideoEl.paused){{
709
  lastVideoEl.pause();
710
  }}
@@ -712,31 +789,25 @@ lastVideoEl=el;
712
  }}
713
 
714
  try{{
715
- var sw=el.videoWidth||el.naturalWidth||el.width||1280;
716
- var sh=el.videoHeight||el.naturalHeight||el.height||720;
717
- var scale=Math.min(1280/sw,720/sh);
718
- var dw=sw*scale,dh=sh*scale;
719
- offCtx.drawImage(el,(1280-dw)/2,(720-dh)/2,dw,dh);
720
  }}catch(e){{}}
721
  }}
722
  }}else{{
723
- // ํด๋ฆฝ ์—†์œผ๋ฉด ์ด์ „ ๋น„๋””์˜ค ์ •์ง€
724
  if(lastVideoEl&&!lastVideoEl.paused){{
725
  lastVideoEl.pause();
726
  lastClipId=null;
727
  }}
728
  }}
729
 
730
- // ์™„์„ฑ๋œ ํ”„๋ ˆ์ž„์„ ๋ฉ”์ธ ์บ”๋ฒ„์Šค๋กœ ๋ณต์‚ฌ
731
  ctx.drawImage(offCanvas,0,0);
732
-
733
  requestAnimationFrame(render);
734
  }}
735
 
736
  requestAnimationFrame(render);
737
  }});
738
 
739
- // ๋ชจ๋“  ๋น„๋””์˜ค ์ •์ง€
740
  Object.keys(S.els).forEach(function(k){{
741
  var el=S.els[k];
742
  if(el&&el.pause)el.pause();
@@ -748,13 +819,13 @@ var webmBlob=new Blob(chunks,{{type:'video/webm'}});
748
  if(webmBlob.size<1000){{document.getElementById('exportMsg').textContent='๋…นํ™” ์‹คํŒจ';return}}
749
 
750
  document.getElementById('exportBar').style.width='100%';
751
- document.getElementById('exportMsg').textContent='์™„๋ฃŒ! ('+Math.round(webmBlob.size/1024/1024*10)/10+'MB) - ์•„๋ž˜์—์„œ ๋‹ค์šด๋กœ๋“œ';
752
 
753
  var downloadDiv=document.createElement('div');
754
  downloadDiv.style.marginTop='10px';
755
  var webmLink=document.createElement('a');
756
  webmLink.href=URL.createObjectURL(webmBlob);
757
- webmLink.download='video_'+Date.now()+'.webm';
758
  webmLink.className='btn btn-success';
759
  webmLink.textContent='๐Ÿ“ฅ WebM ๋‹ค์šด๋กœ๋“œ';
760
  webmLink.style.marginRight='5px';
@@ -781,6 +852,34 @@ downloadDiv.appendChild(copyBtn);
781
  document.querySelector('.modal-box').appendChild(downloadDiv);
782
  }}
783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  function cancelExport(){{S.cancelled=true;document.getElementById('exportModal').style.display='none'}}
785
 
786
  document.addEventListener('keydown',function(e){{
 
44
  .media-item-icon{{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:18px}}
45
  .media-item-dur{{position:absolute;top:2px;right:2px;background:rgba(0,0,0,0.7);padding:1px 3px;border-radius:2px;font-size:8px;color:#fff}}
46
  .preview-area{{flex:1;display:flex;flex-direction:column;background:#1a1a1a;margin:6px;border-radius:8px;overflow:hidden}}
47
+ .preview-box{{flex:1;display:flex;align-items:center;justify-content:center;background:#000;overflow:hidden}}
48
+ #previewCanvas{{background:#000;box-shadow:0 0 20px rgba(0,0,0,0.5)}}
49
  .controls{{height:45px;background:#222;display:flex;align-items:center;justify-content:center;gap:6px}}
50
  .ctrl-btn{{width:28px;height:28px;border:none;border-radius:50%;background:rgba(255,255,255,0.1);color:#fff;cursor:pointer;font-size:12px}}
51
  .ctrl-btn:hover{{background:rgba(255,255,255,0.2)}}
 
104
  <div class="editor">
105
  <div class="toolbar">
106
  <div class="toolbar-title">๐ŸŽฌ Video Editor</div>
107
+ <div style="display:flex;gap:8px;align-items:center">
108
+ <select id="ratioSelect" onchange="setRatio(this.value)" style="padding:4px 8px;border-radius:4px;border:1px solid #ddd;font-size:11px">
109
+ <option value="16:9">16:9 (๊ฐ€๋กœ)</option>
110
+ <option value="9:16">9:16 (์„ธ๋กœ)</option>
111
+ <option value="1:1">1:1 (์ •์‚ฌ๊ฐ)</option>
112
+ <option value="4:3">4:3</option>
113
+ <option value="4:5">4:5 (์ธ์Šคํƒ€)</option>
114
+ </select>
115
+ <select id="fillMode" onchange="setFillMode(this.value)" style="padding:4px 8px;border-radius:4px;border:1px solid #ddd;font-size:11px">
116
+ <option value="fit">๋งž์ถค (์—ฌ๋ฐฑ)</option>
117
+ <option value="fill">์ฑ„์šฐ๊ธฐ (ํ™•๋Œ€)</option>
118
+ <option value="blur">๋ธ”๋Ÿฌ ๋ฐฐ๊ฒฝ</option>
119
+ </select>
120
  <button class="btn btn-secondary" onclick="undo()">โ†ฉ ์‹คํ–‰์ทจ์†Œ</button>
121
  <button class="btn btn-success" onclick="exportVideo()">๐Ÿ“ฅ ๋‚ด๋ณด๋‚ด๊ธฐ</button>
122
  </div>
 
197
  els:{{}},
198
  canvas:null,
199
  ctx:null,
200
+ lastClipId:null,
201
+ ratio:'16:9',
202
+ fillMode:'fit',
203
+ ratioSizes:{{
204
+ '16:9':{{w:1280,h:720,pw:640,ph:360}},
205
+ '9:16':{{w:720,h:1280,pw:203,ph:360}},
206
+ '1:1':{{w:1080,h:1080,pw:360,ph:360}},
207
+ '4:3':{{w:1280,h:960,pw:480,ph:360}},
208
+ '4:5':{{w:1080,h:1350,pw:288,ph:360}}
209
+ }}
210
  }};
211
 
212
  function init(){{
213
  S.canvas=document.getElementById('previewCanvas');
214
  S.ctx=S.canvas.getContext('2d');
215
+ updateCanvasSize();
216
  drawPlaceholder();
217
  }}
218
 
219
+ function updateCanvasSize(){{
220
+ var size=S.ratioSizes[S.ratio];
221
+ S.canvas.width=size.pw;
222
+ S.canvas.height=size.ph;
223
+ S.canvas.style.width=size.pw+'px';
224
+ S.canvas.style.height=size.ph+'px';
225
+ }}
226
+
227
+ function setRatio(r){{
228
+ S.ratio=r;
229
+ updateCanvasSize();
230
+ drawFrame();
231
+ stat('ํ™”๋ฉด ๋น„์œจ: '+r);
232
+ }}
233
+
234
+ function setFillMode(m){{
235
+ S.fillMode=m;
236
+ drawFrame();
237
+ stat('์ฑ„์šฐ๊ธฐ ๋ชจ๋“œ: '+(m==='fit'?'๋งž์ถค (์—ฌ๋ฐฑ)':m==='fill'?'์ฑ„์šฐ๊ธฐ (ํ™•๋Œ€)':'๋ธ”๋Ÿฌ ๋ฐฐ๊ฒฝ'));
238
+ }}
239
+
240
  function id(){{return Math.random().toString(36).substr(2,9)}}
241
  function fmt(t){{if(!t||isNaN(t))t=0;var m=Math.floor(t/60),s=Math.floor(t%60),ms=Math.floor((t%1)*100);return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')+'.'+String(ms).padStart(2,'0')}}
242
  function r(n){{return Math.round(n*1000)/1000}}
 
244
  function save(){{S.history.push(JSON.stringify(S.clips));if(S.history.length>30)S.history.shift()}}
245
 
246
  function drawPlaceholder(){{
247
+ var size=S.ratioSizes[S.ratio];
248
+ var pw=size.pw,ph=size.ph;
249
  S.ctx.fillStyle='#000';
250
+ S.ctx.fillRect(0,0,pw,ph);
251
  S.ctx.fillStyle='#444';
252
  S.ctx.font='14px sans-serif';
253
  S.ctx.textAlign='center';
254
+ S.ctx.fillText('ํƒ€์ž„๋ผ์ธ์— ๋ฏธ๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”',pw/2,ph/2);
255
  }}
256
 
257
  function addMedia(name,type,url,filePath){{
 
585
  function drawFrame(){{
586
  var t=S.time;
587
  var vc=getClipAt(t,'visual');
588
+ var size=S.ratioSizes[S.ratio];
589
+ var pw=size.pw,ph=size.ph;
590
+
591
  S.ctx.fillStyle='#000';
592
+ S.ctx.fillRect(0,0,pw,ph);
593
+
594
  if(vc){{
595
  var el=S.els[vc.mid];
596
  if(el){{
 
604
  el.volume=S.muted?0:vc.vol;
605
  }}
606
  try{{
607
+ var sw=el.videoWidth||el.naturalWidth||el.width||pw;
608
+ var sh=el.videoHeight||el.naturalHeight||el.height||ph;
609
+ drawWithMode(S.ctx,el,sw,sh,pw,ph,S.fillMode);
 
 
 
610
  }}catch(e){{}}
611
  }}
612
  }}else if(S.clips.length===0){{
613
  S.ctx.fillStyle='#444';
614
  S.ctx.font='14px sans-serif';
615
  S.ctx.textAlign='center';
616
+ S.ctx.fillText('ํƒ€์ž„๋ผ์ธ์— ๋ฏธ๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”',pw/2,ph/2);
617
  }}
618
  var audioClips=S.clips.filter(function(c){{
619
  if(c.type!=='audio')return false;
 
642
  S.ctx.fillStyle='#333';
643
  S.ctx.font='12px sans-serif';
644
  S.ctx.textAlign='center';
645
+ S.ctx.fillText('์žฌ์ƒ ์œ„์น˜์— ๋ฏธ๋””์–ด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค',pw/2,ph/2);
646
+ }}
647
+ }}
648
+
649
+ function drawWithMode(ctx,el,sw,sh,dw,dh,mode){{
650
+ if(mode==='fill'){{
651
+ // ์ฑ„์šฐ๊ธฐ: ์บ”๋ฒ„์Šค๋ฅผ ๊ฝ‰ ์ฑ„์šฐ๋„๋ก ํ™•๋Œ€, ๋„˜์น˜๋Š” ๋ถ€๋ถ„ ์ž˜๋ฆผ
652
+ var scale=Math.max(dw/sw,dh/sh);
653
+ var nw=sw*scale,nh=sh*scale;
654
+ var ox=(dw-nw)/2,oy=(dh-nh)/2;
655
+ ctx.drawImage(el,ox,oy,nw,nh);
656
+ }}else if(mode==='blur'){{
657
+ // ๋ธ”๋Ÿฌ ๋ฐฐ๊ฒฝ: ๋ฐฐ๊ฒฝ์— ํ™•๋Œ€+๋ธ”๋Ÿฌ, ๊ทธ ์œ„์— ์›๋ณธ
658
+ // ๋จผ์ € ๋ฐฐ๊ฒฝ (ํ™•๋Œ€ํ•ด์„œ ๊ฝ‰ ์ฑ„์šฐ๊ธฐ)
659
+ ctx.filter='blur(20px)';
660
+ var scale=Math.max(dw/sw,dh/sh)*1.1;
661
+ var nw=sw*scale,nh=sh*scale;
662
+ var ox=(dw-nw)/2,oy=(dh-nh)/2;
663
+ ctx.drawImage(el,ox,oy,nw,nh);
664
+ ctx.filter='none';
665
+ // ์–ด๋‘ก๊ฒŒ
666
+ ctx.fillStyle='rgba(0,0,0,0.3)';
667
+ ctx.fillRect(0,0,dw,dh);
668
+ // ์›๋ณธ (fit)
669
+ var fitScale=Math.min(dw/sw,dh/sh);
670
+ var fw=sw*fitScale,fh=sh*fitScale;
671
+ var fx=(dw-fw)/2,fy=(dh-fh)/2;
672
+ ctx.drawImage(el,fx,fy,fw,fh);
673
+ }}else{{
674
+ // ๋งž์ถค (fit): ์›๋ณธ ๋น„์œจ ์œ ์ง€, ์—ฌ๋ฐฑ
675
+ var scale=Math.min(dw/sw,dh/sh);
676
+ var nw=sw*scale,nh=sh*scale;
677
+ var ox=(dw-nw)/2,oy=(dh-nh)/2;
678
+ ctx.drawImage(el,ox,oy,nw,nh);
679
  }}
680
  }}
681
 
 
704
  }}
705
 
706
  async function doExport(){{
707
+ var size=S.ratioSizes[S.ratio];
708
+ var exportW=size.w,exportH=size.h;
709
+
710
  // ๋ฉ”์ธ ์บ”๋ฒ„์Šค (MediaRecorder ์—ฐ๊ฒฐ์šฉ)
711
  var canvas=document.createElement('canvas');
712
+ canvas.width=exportW;canvas.height=exportH;
713
  var ctx=canvas.getContext('2d');
714
 
715
  // ์˜คํ”„์Šคํฌ๋ฆฐ ์บ”๋ฒ„์Šค (๋ Œ๋”๋ง์šฉ - ๋”๋ธ” ๋ฒ„ํผ๋ง)
716
  var offCanvas=document.createElement('canvas');
717
+ offCanvas.width=exportW;offCanvas.height=exportH;
718
  var offCtx=offCanvas.getContext('2d');
719
 
720
  // ์ดˆ๊ธฐ ๊ฒ€์€ ํ™”๋ฉด
721
  ctx.fillStyle='#000';
722
+ ctx.fillRect(0,0,exportW,exportH);
723
 
724
  var stream=canvas.captureStream(30);
725
 
 
736
  var chunks=[];
737
  rec.ondataavailable=function(e){{if(e.data.size>0)chunks.push(e.data)}};
738
 
739
+ document.getElementById('exportMsg').textContent='๋…นํ™” ์ค‘... ('+S.ratio+')';
740
  rec.start(100);
741
 
742
  var dur=S.dur;
743
  var fps=30;
744
+ var frameInterval=1000/fps;
745
  var totalFrames=Math.ceil(dur*fps);
746
  var startTime=performance.now();
747
+ var fillMode=S.fillMode;
748
 
749
+ // ์‹ค์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง
750
  await new Promise(function(resolve){{
751
  var frameCount=0;
752
  var lastVideoEl=null;
 
756
  if(S.cancelled){{rec.stop();resolve();return}}
757
 
758
  var elapsed=performance.now()-startTime;
759
+ var t=elapsed/1000;
760
 
761
  if(t>=dur){{
762
  rec.stop();
 
764
  return;
765
  }}
766
 
 
767
  document.getElementById('exportBar').style.width=(t/dur*100)+'%';
768
  document.getElementById('exportMsg').textContent='๋…นํ™” ์ค‘... '+Math.round(t/dur*100)+'% ('+t.toFixed(1)+'/'+dur.toFixed(1)+'์ดˆ)';
769
 
 
770
  offCtx.fillStyle='#000';
771
+ offCtx.fillRect(0,0,exportW,exportH);
772
 
773
  var vc=getClipAt(t,'visual');
774
  if(vc){{
 
776
  if(el){{
777
  if(vc.type==='video'){{
778
  var clipT=t-vc.start+vc.ts;
 
 
779
  if(lastClipId!==vc.id){{
780
  el.currentTime=clipT;
781
  el.play().catch(function(){{}});
782
  lastClipId=vc.id;
783
  lastVideoEl=el;
784
  }}
 
 
785
  if(lastVideoEl&&lastVideoEl!==el&&!lastVideoEl.paused){{
786
  lastVideoEl.pause();
787
  }}
 
789
  }}
790
 
791
  try{{
792
+ var sw=el.videoWidth||el.naturalWidth||el.width||exportW;
793
+ var sh=el.videoHeight||el.naturalHeight||el.height||exportH;
794
+ drawWithModeExport(offCtx,el,sw,sh,exportW,exportH,fillMode);
 
 
795
  }}catch(e){{}}
796
  }}
797
  }}else{{
 
798
  if(lastVideoEl&&!lastVideoEl.paused){{
799
  lastVideoEl.pause();
800
  lastClipId=null;
801
  }}
802
  }}
803
 
 
804
  ctx.drawImage(offCanvas,0,0);
 
805
  requestAnimationFrame(render);
806
  }}
807
 
808
  requestAnimationFrame(render);
809
  }});
810
 
 
811
  Object.keys(S.els).forEach(function(k){{
812
  var el=S.els[k];
813
  if(el&&el.pause)el.pause();
 
819
  if(webmBlob.size<1000){{document.getElementById('exportMsg').textContent='๋…นํ™” ์‹คํŒจ';return}}
820
 
821
  document.getElementById('exportBar').style.width='100%';
822
+ document.getElementById('exportMsg').textContent='์™„๋ฃŒ! ('+Math.round(webmBlob.size/1024/1024*10)/10+'MB, '+S.ratio+') - ์•„๋ž˜์—์„œ ๋‹ค์šด๋กœ๋“œ';
823
 
824
  var downloadDiv=document.createElement('div');
825
  downloadDiv.style.marginTop='10px';
826
  var webmLink=document.createElement('a');
827
  webmLink.href=URL.createObjectURL(webmBlob);
828
+ webmLink.download='video_'+S.ratio.replace(':','x')+'_'+Date.now()+'.webm';
829
  webmLink.className='btn btn-success';
830
  webmLink.textContent='๐Ÿ“ฅ WebM ๋‹ค์šด๋กœ๋“œ';
831
  webmLink.style.marginRight='5px';
 
852
  document.querySelector('.modal-box').appendChild(downloadDiv);
853
  }}
854
 
855
+ // ๋‚ด๋ณด๋‚ด๊ธฐ์šฉ ๊ทธ๋ฆฌ๊ธฐ ํ•จ์ˆ˜ (๋ธ”๋Ÿฌ ํšจ๊ณผ ํฌํ•จ)
856
+ function drawWithModeExport(ctx,el,sw,sh,dw,dh,mode){{
857
+ if(mode==='fill'){{
858
+ var scale=Math.max(dw/sw,dh/sh);
859
+ var nw=sw*scale,nh=sh*scale;
860
+ var ox=(dw-nw)/2,oy=(dh-nh)/2;
861
+ ctx.drawImage(el,ox,oy,nw,nh);
862
+ }}else if(mode==='blur'){{
863
+ ctx.filter='blur(20px)';
864
+ var scale=Math.max(dw/sw,dh/sh)*1.1;
865
+ var nw=sw*scale,nh=sh*scale;
866
+ var ox=(dw-nw)/2,oy=(dh-nh)/2;
867
+ ctx.drawImage(el,ox,oy,nw,nh);
868
+ ctx.filter='none';
869
+ ctx.fillStyle='rgba(0,0,0,0.3)';
870
+ ctx.fillRect(0,0,dw,dh);
871
+ var fitScale=Math.min(dw/sw,dh/sh);
872
+ var fw=sw*fitScale,fh=sh*fitScale;
873
+ var fx=(dw-fw)/2,fy=(dh-fh)/2;
874
+ ctx.drawImage(el,fx,fy,fw,fh);
875
+ }}else{{
876
+ var scale=Math.min(dw/sw,dh/sh);
877
+ var nw=sw*scale,nh=sh*scale;
878
+ var ox=(dw-nw)/2,oy=(dh-nh)/2;
879
+ ctx.drawImage(el,ox,oy,nw,nh);
880
+ }}
881
+ }}
882
+
883
  function cancelExport(){{S.cancelled=true;document.getElementById('exportModal').style.display='none'}}
884
 
885
  document.addEventListener('keydown',function(e){{