Update app.py
Browse files
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{{
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 206 |
S.ctx.fillStyle='#444';
|
| 207 |
S.ctx.font='14px sans-serif';
|
| 208 |
S.ctx.textAlign='center';
|
| 209 |
-
S.ctx.fillText('ํ์๋ผ์ธ์ ๋ฏธ๋์ด๋ฅผ ์ถ๊ฐํ์ธ์',
|
| 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,
|
|
|
|
| 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||
|
| 559 |
-
var sh=el.videoHeight||el.naturalHeight||el.height||
|
| 560 |
-
|
| 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('ํ์๋ผ์ธ์ ๋ฏธ๋์ด๋ฅผ ์ถ๊ฐํ์ธ์',
|
| 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('์ฌ์ ์์น์ ๋ฏธ๋์ด๊ฐ ์์ต๋๋ค',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 631 |
var ctx=canvas.getContext('2d');
|
| 632 |
|
| 633 |
// ์คํ์คํฌ๋ฆฐ ์บ๋ฒ์ค (๋ ๋๋ง์ฉ - ๋๋ธ ๋ฒํผ๋ง)
|
| 634 |
var offCanvas=document.createElement('canvas');
|
| 635 |
-
offCanvas.width=
|
| 636 |
var offCtx=offCanvas.getContext('2d');
|
| 637 |
|
| 638 |
// ์ด๊ธฐ ๊ฒ์ ํ๋ฉด
|
| 639 |
ctx.fillStyle='#000';
|
| 640 |
-
ctx.fillRect(0,0,
|
| 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;
|
| 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,
|
| 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||
|
| 716 |
-
var sh=el.videoHeight||el.naturalHeight||el.height||
|
| 717 |
-
|
| 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){{
|