محرر الفيديو الاحترافي لدمج الصور والموسيقى
بواسطة morbah |
|
1
محلل الاسكريبت والسيناريو
2
لوحة التحكم برفع صور المشاهد وتعديل المدد
3
غرفة المؤثرات والجماليات البصرية
يرجى رفع صور المشاهد لتنشيط الرندرة...
<!-- استدعاء مكتبة إصلاح ميتاداتا التوقيت للفيديو -->
<script src="https://cdn.jsdelivr.net/npm/fix-webm-duration@1.0.6/fix-webm-duration.js"></script>
<div class="pro-studio-container">
<div class="studio-header">
<div class="brand-tag">CINEMATIC STUDIO v11.0</div>
<h2>استوديو المونتاج السينمائي الذكي فائق السلاسة</h2>
<p class="subtitle font-cairo">تحليل تلقائي للمشاهد، حساب ذكي للمدد حسب طول النص، زوم متناوب، وقطع فوري فائق الاستقرار.</p>
</div>
<!-- خطوة 1: إدخال السيناريو وتفكيكه -->
<div class="studio-section">
<div class="section-title">
<span class="step-badge">1</span>
<h3>محلل الاسكريبت والسيناريو</h3>
</div>
<div class="script-input-area">
<label for="fullScript">ضع النص الكامل للفيديو هنا (يقبل "نهاية المشهد" أو "نهايه المشهد" بالتساوي):</label>
<textarea id="fullScript" placeholder="اكتب الاسكريبت هنا...">You’re waiting for the "perfect moment" to start, aren't you?
Waiting for the stars to align.
Waiting to feel "ready."
Here’s a cold truth:
That feeling is a lie.
The perfect moment is a myth designed to keep you comfortable.
نهايه المشهد 1
Look at your life right now.
Are you actually making progress, or are you just pretending?
You watch the videos.
You read the quotes.
You make the plans.
But when it’s time to actually sweat...
You find an excuse.
نهايه المشهد 2</textarea>
<!-- خيار تحديد طريقة حساب المدة -->
<div class="duration-method-box" style="margin-top: 15px;">
<label style="font-size: 13px; color: #94a3b8; display: block; margin-bottom: 8px;">طريقة تحديد مدة عرض المشاهد:</label>
<select id="durationMethod" style="background-color: #15192c; color: white; border: 1px solid #1e2540; padding: 10px; border-radius: 6px; font-size: 12px; outline: none; width: 100%; max-width: 350px;">
<option value="auto" selected>وضع تلقائي ذكي (حساب المدة تلقائياً بناءً على طول النص)</option>
<option value="manual">وضع يدوي ثابت (4 ثوانٍ افتراضية لكل مشهد)</option>
</select>
</div>
<button id="parseScriptBtn" class="btn-primary">تحليل النص وتوليد المشاهد تلقائياً</button>
</div>
</div>
<!-- خطوة 2: لوحة تحرير المشاهد الديناميكية (تظهر بعد التحليل) -->
<div class="studio-section" id="scenesEditingSection" style="display:none;">
<div class="section-title">
<span class="step-badge">2</span>
<h3>لوحة التحكم برفع صور المشاهد وتعديل المدد</h3>
</div>
<div id="scenesContainer" class="scenes-grid">
<!-- توليد المشاهد برمجياً وصورها المخصصة دون أي تعارض -->
</div>
</div>
<!-- خطوة 3: التعديلات الاحترافية والمؤثرات البصرية -->
<div class="studio-section">
<div class="section-title">
<span class="step-badge">3</span>
<h3>غرفة المؤثرات والجماليات البصرية</h3>
</div>
<div class="effects-grid">
<!-- أبعاد الفيديو المتقدمة -->
<div class="effect-card">
<label>أبعاد الفيديو (Aspect Ratio)</label>
<select id="aspectRatio">
<option value="9_16" selected>طولي للجوال Reels/TikTok (9:16)</option>
<option value="16_9">عرضي لليوتيوب (16:9)</option>
<option value="1_1">مربع للإنستقرام (1:1)</option>
</select>
</div>
<!-- اختيار الخطوط -->
<div class="effect-card">
<label>نوع خط النصوص</label>
<select id="textFont">
<option value="Arial, sans-serif">Kufi (Arial Simplified)</option>
<option value="'Times New Roman', serif">Classic Serif</option>
<option value="monospace">Digital Typewriter</option>
</select>
</div>
<!-- تأثيرات بصرية حية -->
<div class="effect-card checkbox-card">
<label><input type="checkbox" id="enableParticles" checked> تفعيل تأثير جزيئات الغبار النجمي المتطايرة</label>
<label><input type="checkbox" id="enableKenBurns" checked> تفعيل زوم تقريبي سينمائي متناوب (داخل وخارج)</label>
</div>
<!-- الملف الصوتي -->
<div class="effect-card">
<label>الصوت والخلفية الموسيقية</label>
<input type="file" id="audioInput" accept="audio/*">
<audio id="bgAudio" controls style="display:none; width:100%; margin-top:10px;"></audio>
</div>
</div>
</div>
<!-- منطقة الرندرة والتصدير المتقدم -->
<div class="render-zone">
<button id="generateBtn" disabled>البدء في تصدير الفيديو فائق النعومة</button>
<div class="progress-wrapper" id="progressWrapper">
<div class="progress-info">
<span id="progressStep">جاري تحضير المحرك...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressBarFill"></div>
</div>
</div>
<div id="status">يرجى رفع صور المشاهد لتنشيط الرندرة...</div>
</div>
<!-- شاشة العرض النهائية والتحميل -->
<div class="output-zone" id="outputZone" style="display:none;">
<div class="divider"></div>
<h3>شاشة العرض النهائية</h3>
<video id="previewVideo" controls></video>
<div class="download-action">
<a class="btn-download" id="downloadLink">تحميل الفيديو بجودتك المختارة MP4</a>
</div>
</div>
<canvas id="videoCanvas" style="display:none;"></canvas>
</div>
<style>
/* تصميم سينمائي متكامل واحترافي بمفهوم الاستوديو المظلم */
.pro-studio-container {
direction: rtl;
font-family: 'Segoe UI', Tahoma, Geneva, sans-serif;
background-color: #080a10;
color: #e2e8f0;
border-radius: 20px;
padding: 30px;
max-width: 1000px;
margin: 20px auto;
box-shadow: 0 20px 50px rgba(0,0,0,0.9);
border: 1px solid #1a2035;
}
.studio-header {
text-align: center;
margin-bottom: 35px;
}
.brand-tag {
display: inline-block;
background: linear-gradient(135deg, #00ffcc, #0055ff);
color: #080a10;
padding: 4px 15px;
font-size: 11px;
font-weight: bold;
border-radius: 20px;
margin-bottom: 12px;
}
.studio-header h2 {
font-size: 28px;
margin: 0;
background: linear-gradient(to left, #00ffcc, #00a2ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.studio-header .subtitle {
color: #94a3b8;
font-size: 13px;
margin-top: 8px;
}
.studio-section {
background-color: #111422;
padding: 24px;
border-radius: 14px;
border: 1px solid #1e2540;
margin-bottom: 25px;
}
.section-title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
border-bottom: 1px solid #1e2540;
padding-bottom: 12px;
}
.step-badge {
background: #00ffcc;
color: #080a10;
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 13px;
}
.section-title h3 {
margin: 0;
font-size: 16px;
color: #fff;
}
textarea {
width: 100%;
height: 180px;
background-color: #080a10;
border: 1px solid #1e2540;
color: #fff;
border-radius: 8px;
padding: 15px;
font-size: 13px;
line-height: 1.6;
outline: none;
resize: vertical;
box-sizing: border-box;
}
textarea:focus {
border-color: #00ffcc;
}
.btn-primary {
background: linear-gradient(135deg, #00ffcc, #0088ff);
color: #080a10;
border: none;
padding: 12px 30px;
font-size: 14px;
font-weight: bold;
border-radius: 8px;
cursor: pointer;
margin-top: 15px;
transition: 0.3s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,255,204,0.4);
}
.scenes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.scene-card {
background-color: #080a10;
border: 1px solid #1e2540;
border-radius: 10px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
}
.scene-card h4 {
margin: 0;
color: #00ffcc;
font-size: 14px;
border-bottom: 1px solid #1e2540;
padding-bottom: 6px;
}
.scene-card textarea {
height: 80px;
font-size: 11px;
}
.custom-file-upload {
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: #15192c;
border: 1px dashed #3a457a;
border-radius: 6px;
padding: 10px;
cursor: pointer;
}
.custom-file-upload input[type="file"] {
position: absolute;
left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer;
}
.upload-text {
font-size: 11px;
color: #00ffcc;
font-weight: bold;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
}
.img-preview {
height: 120px;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-color: #030407;
border-radius: 6px;
border: 1px solid #1e2540;
}
.scene-dur {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #94a3b8;
}
.scene-dur input {
width: 50px;
background: #111422;
color: white;
border: 1px solid #1e2540;
padding: 4px;
border-radius: 4px;
text-align: center;
}
.effects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.effect-card {
background-color: #080a10;
padding: 15px;
border-radius: 10px;
border: 1px solid #1e2540;
}
.effect-card label {
display: block;
font-size: 12px;
color: #94a3b8;
margin-bottom: 8px;
}
.effect-card select, .effect-card input[type="file"] {
width: 100%;
background-color: #111422;
color: #fff;
border: 1px solid #1e2540;
padding: 8px;
border-radius: 6px;
outline: none;
font-size: 12px;
box-sizing: border-box;
}
.checkbox-card {
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
}
.checkbox-card label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
margin: 0;
font-size: 11px;
}
.render-zone {
text-align: center;
margin-top: 30px;
background-color: #111422;
padding: 25px;
border-radius: 14px;
border: 1px solid #1e2540;
}
#generateBtn {
background: linear-gradient(135deg, #00ffcc, #0055ff);
color: #080a10;
border: none;
padding: 15px 45px;
font-size: 15px;
font-weight: bold;
border-radius: 30px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0,255,204,0.3);
transition: 0.3s;
}
#generateBtn:disabled {
background: #202438;
color: #5d637c;
cursor: not-allowed;
box-shadow: none;
}
.progress-wrapper {
margin-top: 20px;
display: none;
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
}
.progress-bar-bg {
width: 100%;
background-color: #080a10;
height: 10px;
border-radius: 10px;
overflow: hidden;
border: 1px solid #1e2540;
}
.progress-bar-fill {
width: 0%;
height: 100%;
background: linear-gradient(to right, #00ffcc, #0055ff);
border-radius: 10px;
}
#status {
margin-top: 12px;
font-size: 13px;
color: #94a3b8;
}
.output-zone {
margin-top: 30px;
text-align: center;
}
.divider {
border-top: 1px solid #1e2540;
margin-bottom: 20px;
}
#previewVideo {
max-width: 100%;
border-radius: 10px;
border: 1px solid #1e2540;
background-color: #000;
}
.btn-download {
display: inline-block;
background: linear-gradient(135deg, #00ffcc, #28a745);
color: #080a10 !important;
text-decoration: none;
padding: 14px 40px;
border-radius: 30px;
font-weight: bold;
font-size: 15px;
margin-top: 20px;
}
</style>
<script>
const fullScript = document.getElementById('fullScript');
const parseScriptBtn = document.getElementById('parseScriptBtn');
const scenesEditingSection = document.getElementById('scenesEditingSection');
const scenesContainer = document.getElementById('scenesContainer');
const generateBtn = document.getElementById('generateBtn');
const canvas = document.getElementById('videoCanvas');
const ctx = canvas.getContext('2d');
const status = document.getElementById('status');
const previewVideo = document.getElementById('previewVideo');
const downloadLink = document.getElementById('downloadLink');
const progressWrapper = document.getElementById('progressWrapper');
const progressBarFill = document.getElementById('progressBarFill');
const progressPercent = document.getElementById('progressPercent');
const progressStep = document.getElementById('progressStep');
const outputZone = document.getElementById('outputZone');
const audioInput = document.getElementById('audioInput');
const bgAudio = document.getElementById('bgAudio');
let parsedScenes = [];
let audioFile = null;
let particlesArray = [];
// دالة حساب المدة تلقائياً بناءً على طول النص (الوضع التلقائي الذكي)
function calculateAutoDuration(text) {
const words = text.trim().split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
// القاعدة: كل كلمة تأخذ 0.6 ثانية تقريباً للقراءة، مع حد أدنى 3 ثوانٍ وحد أقصى 15 ثانية للمشهد
const duration = Math.max(3, Math.min(15, Math.ceil(wordCount * 0.6 + 1.5)));
return duration;
}
// محلل الاسكريبت والسيناريو لإنشاء بطاقات تحرير المشاهد برمجياً وديناميكياً
parseScriptBtn.addEventListener('click', () => {
const text = fullScript.value;
// تحليل الاسكريبت ودعم هاء وتاء المشاهد المكتوبة برمتها
const chunks = text.split(/(?:نهاية|نهايه)\s*المشهد\s*\d*/gi);
scenesContainer.innerHTML = '';
parsedScenes = [];
const durationMethod = document.getElementById('durationMethod').value;
let count = 0;
chunks.forEach((chunk) => {
const cleanText = chunk.trim();
if (cleanText !== "") {
count++;
// حساب المدة تلقائياً أو وضعها كـ 4 ثوانٍ يدوية
let initialDuration = 4;
if (durationMethod === 'auto') {
initialDuration = calculateAutoDuration(cleanText);
}
const sceneData = {
id: count,
text: cleanText,
imgObj: new Image(),
imgLoaded: false,
duration: initialDuration
};
parsedScenes.push(sceneData);
const sceneCard = document.createElement('div');
sceneCard.className = 'scene-card';
sceneCard.innerHTML = `
<h4>المشهد ${count}</h4>
<textarea id="sceneText-${count}">${cleanText}</textarea>
<div class="custom-file-upload">
<!-- استخدام data-scene-id لضمان عدم حدوث تعارض في قنوات رفع الصور -->
<input type="file" class="scene-file-input" data-scene-id="${count}" id="sceneFile-${count}" accept="image/*">
<span class="upload-text" id="uploadText-${count}">اختر صورة المشهد</span>
</div>
<div id="scenePreview-${count}" class="img-preview" style="display:none;"></div>
<div class="scene-dur">
<label>المدة (ثوانٍ):</label>
<input type="number" id="sceneDuration-${count}" value="${initialDuration}" min="1" max="20">
</div>
`;
scenesContainer.appendChild(sceneCard);
}
});
scenesEditingSection.style.display = 'block';
status.innerText = `تم استخراج ${count} مشاهد. يرجى رفع صورة مخصصة لكل مشهد بالأسفل لتنشيط التصدير.`;
});
// نظام الـ Event Delegation المضمون 100% لإسناد الصورة المرفوعة لبطاقة المشهد الصحيحة بلا تداخل
scenesContainer.addEventListener('change', (e) => {
if (e.target.classList.contains('scene-file-input')) {
const fileInput = e.target;
const sceneId = parseInt(fileInput.getAttribute('data-scene-id'));
const file = fileInput.files[0];
if (file) {
const scene = parsedScenes.find(s => s.id === sceneId);
if (scene) {
const uploadTextSpan = document.getElementById(`uploadText-${sceneId}`);
const previewDiv = document.getElementById(`scenePreview-${sceneId}`);
uploadTextSpan.innerText = file.name;
const reader = new FileReader();
reader.onload = (ev) => {
scene.imgObj.onload = () => {
scene.imgLoaded = true;
previewDiv.style.backgroundImage = `url('${ev.target.result}')`;
previewDiv.style.display = 'block';
validateAllScenesReady();
};
scene.imgObj.src = ev.target.result;
};
reader.readAsDataURL(file);
}
}
}
});
audioInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
audioFile = file;
bgAudio.src = URL.createObjectURL(file);
bgAudio.style.display = 'block';
}
});
function validateAllScenesReady() {
const allReady = parsedScenes.every(s => s.imgLoaded);
if (allReady && parsedScenes.length > 0) {
generateBtn.disabled = false;
status.innerText = "كافة صور المشاهد مجهزة بالكامل! يمكنك الضغط على زر التصدير للرندرة.";
} else {
generateBtn.disabled = true;
}
}
function initParticles(w, h) {
particlesArray = [];
for (let i = 0; i < 40; i++) {
particlesArray.push({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * 1.5,
vy: -Math.random() * 1.2 - 0.4,
size: Math.random() * 2.5 + 1,
alpha: Math.random() * 0.6 + 0.2,
decay: Math.random() * 0.005 + 0.002
});
}
}
function drawParticles(targetCtx, w, h) {
targetCtx.save();
particlesArray.forEach((p) => {
p.x += p.vx;
p.y += p.vy;
p.alpha -= p.decay;
if (p.alpha <= 0 || p.y < 0 || p.x < 0 || p.x > w) {
p.x = Math.random() * w;
p.y = h;
p.alpha = Math.random() * 0.6 + 0.4;
p.vy = -Math.random() * 1.2 - 0.4;
}
targetCtx.fillStyle = `rgba(0, 255, 204, ${p.alpha})`;
targetCtx.beginPath();
targetCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
targetCtx.fill();
});
targetCtx.restore();
}
function preRenderImage(targetCtx, img, w, h) {
targetCtx.clearRect(0, 0, w, h);
targetCtx.fillStyle = "#000000";
targetCtx.fillRect(0, 0, w, h);
const canvasRatio = w / h;
const imgRatio = img.naturalWidth / img.naturalHeight;
let dW, dH, oX, oY;
if (imgRatio > canvasRatio) {
dW = w;
dH = w / imgRatio;
oX = 0;
oY = (h - dH) / 2;
} else {
dH = h;
dW = h * imgRatio;
oX = (w - dW) / 2;
oY = 0;
}
targetCtx.drawImage(img, oX, oY, dW, dH);
}
generateBtn.addEventListener('click', async () => {
progressStep.innerText = "جاري تهيئة وتحضير المشاهد للرندرة الفائقة...";
status.innerText = "بدأ محرك المونتاج التلقائي المعالجة المباشرة...";
generateBtn.disabled = true;
outputZone.style.display = 'none';
progressWrapper.style.display = 'block';
progressBarFill.style.width = '0%';
progressPercent.innerText = '0%';
const ratio = document.getElementById('aspectRatio').value;
let width, height;
if (ratio === '9_16') {
width = 1080; height = 1920;
} else if (ratio === '16_9') {
width = 1920; height = 1080;
} else {
width = 1080; height = 1080;
}
canvas.width = width;
canvas.height = height;
initParticles(width, height);
// تحديث القيم اللوجيستية لكل مشهد وتجهيز لوحات الرندرة الخلفية المعزولة
let totalDurationMs = 0;
parsedScenes.forEach((scene) => {
scene.text = document.getElementById(`sceneText-${scene.id}`).value.trim();
const durSec = parseFloat(document.getElementById(`sceneDuration-${scene.id}`).value) || 4;
scene.durationMs = durSec * 1000;
scene.startTime = totalDurationMs;
scene.endTime = totalDurationMs + scene.durationMs;
totalDurationMs += scene.durationMs;
scene.offCanvas = document.createElement('canvas');
scene.offCanvas.width = width;
scene.offCanvas.height = height;
const offCtx = scene.offCanvas.getContext('2d');
preRenderImage(offCtx, scene.imgObj, width, height);
});
// معدل 30 إطاراً لحماية العتاد واستقرار المخطط الزمني
const fps = 30;
const canvasStream = canvas.captureStream(fps);
let outputStream = canvasStream;
let audioContext = null;
let audioSource = null;
let audioDestination = null;
if (audioFile) {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioSource = audioContext.createMediaElementSource(bgAudio);
audioDestination = audioContext.createMediaStreamDestination();
audioSource.connect(audioDestination);
audioSource.connect(audioContext.destination);
const videoTrack = canvasStream.getVideoTracks()[0];
const audioTrack = audioDestination.stream.getAudioTracks()[0];
outputStream = new MediaStream([videoTrack, audioTrack]);
} catch (e) {
console.warn("تنبيه: فشل دمج الصوت، سيتم التصدير صامتاً:", e);
}
}
let options = { mimeType: 'video/mp4;codecs=avc1' };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options = { mimeType: 'video/webm;codecs=vp9' };
}
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options = { mimeType: 'video/webm' };
}
let mediaRecorder;
try {
mediaRecorder = new MediaRecorder(outputStream, options);
mediaRecorder.onerror = (e) => {
console.error("خطأ عتادي في مسجل الفيديو:", e);
status.innerText = "خطأ في تشفير ملف الفيديو عتادياً: " + e.error.name;
};
} catch (err) {
status.innerText = "الترميز غير مدعوم في هذا المتصفح: " + err.message;
generateBtn.disabled = false;
return;
}
const chunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
mediaRecorder.onstop = () => {
progressStep.innerText = "جاري معالجة وحقن الميتاداتا...";
status.innerText = "تجميع ملف الـ MP4 وإصلاح الميتاداتا لمنع مشكلة توقيت الجوال 0:00...";
if (audioFile) {
bgAudio.pause();
if (audioContext && audioContext.state !== 'closed') {
audioContext.close();
}
}
const buggyBlob = new Blob(chunks, { type: options.mimeType });
if (typeof ysFixWebmDuration === 'function') {
ysFixWebmDuration(buggyBlob, totalDurationMs, (fixedBlob) => {
finalizeVideo(fixedBlob);
});
} else {
finalizeVideo(buggyBlob);
}
};
function finalizeVideo(finalBlob) {
const videoURL = URL.createObjectURL(finalBlob);
previewVideo.src = videoURL;
outputZone.style.display = 'block';
if (ratio === '9_16') {
previewVideo.style.width = "340px";
previewVideo.style.height = "604px";
} else {
previewVideo.style.width = "640px";
previewVideo.style.height = "360px";
}
downloadLink.href = videoURL;
downloadLink.download = `cinematic_video_stable_${ratio}.mp4`;
status.innerText = "اكتمل التصدير بنجاح! تم تطبيق الرندرة المتسلسلة الآمنة والقطع المباشر الفوري.";
progressPercent.innerText = "100%";
progressBarFill.style.width = '100%';
progressStep.innerText = "اكتمل بنجاح!";
generateBtn.disabled = false;
}
if (audioFile) {
bgAudio.currentTime = 0;
bgAudio.play();
}
mediaRecorder.start();
const enableParticles = document.getElementById('enableParticles').checked;
const enableKenBurns = document.getElementById('enableKenBurns').checked;
const selectedFont = document.getElementById('textFont').value;
// الرندرة المتسلسلة الآمنة منخفضة التحميل العتادي تضمن انسيابية مطلقة
const frameIntervalMs = 1000 / fps;
const totalFrames = Math.ceil(totalDurationMs / frameIntervalMs);
let currentFrame = 0;
function renderFrameStep() {
try {
if (currentFrame >= totalFrames) {
mediaRecorder.stop();
return;
}
const elapsed = currentFrame * frameIntervalMs;
const progressPercentVal = Math.min((currentFrame / totalFrames) * 100, 100);
progressBarFill.style.width = `${progressPercentVal.toFixed(0)}%`;
progressPercent.innerText = `${progressPercentVal.toFixed(0)}%`;
progressStep.innerText = `جاري رندرة الإطارات الحتمية... (${progressPercentVal.toFixed(0)}%)`;
// رسم كادر المشهد الحالي بالتوقيت الحتمي
drawFrame(elapsed);
currentFrame++;
// الفاصل الزمني المجهري (18ms) المانع لتوقف وجمود الجافا سكريبت في الهواتف
setTimeout(() => {
requestAnimationFrame(renderFrameStep);
}, 18);
} catch (err) {
console.error("خطأ برمجي أثناء المونتاج المباشر:", err);
status.innerText = "خطأ برمجي أثناء الرندرة: " + err.message;
progressStep.innerText = "فشلت العملية";
generateBtn.disabled = false;
if (mediaRecorder && mediaRecorder.state !== "inactive") {
mediaRecorder.stop();
}
}
}
// دالة الرندرة المعيارية المنضبطة (القطع المباشر الفوري الحاسم والآمن)
function drawFrame(elapsed) {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
// البحث اللوجيستي الدقيق عن المشهد النشط
let currentSceneIndex = parsedScenes.findIndex(s => elapsed >= s.startTime && elapsed < s.endTime);
if (currentSceneIndex === -1) currentSceneIndex = parsedScenes.length - 1;
const currentScene = parsedScenes[currentSceneIndex];
const sceneElapsed = elapsed - currentScene.startTime;
// حساب أبعاد الزوم المتناوب بدقة سينمائية خطية تتماشى مع الفهرس المكتشف
let zoomScale = 1.0;
if (enableKenBurns) {
const progress = sceneElapsed / currentScene.durationMs;
if (currentSceneIndex % 2 === 0) {
zoomScale = 1.0 + progress * 0.12; // زوم للداخل للمشهد الفردي
} else {
zoomScale = 1.12 - progress * 0.12; // زوم للخارج للمشهد الزوجي
}
}
// رسم المشهد النشط مباشرة (قطع فوري ولحظي بدون تداخل أو تأثيرات تسبب بطء المعالجة)
ctx.save();
const zoomW = width * zoomScale;
const zoomH = height * zoomScale;
const zoomX = (width - zoomW) / 2;
const zoomY = (height - zoomH) / 2;
ctx.drawImage(currentScene.offCanvas, zoomX, zoomY, zoomW, zoomH);
ctx.restore();
if (enableParticles) {
drawParticles(ctx, width, height);
}
// كتابة ترجمة نصوص المشهد النشط سطراً تلو الآخر تلقائياً
const rawLines = currentScene.text.split('\n').map(l => l.trim()).filter(l => l !== "");
if (rawLines.length > 0) {
const lineDuration = currentScene.durationMs / rawLines.length;
const activeLineIndex = Math.floor(sceneElapsed / lineDuration);
const activeText = rawLines[activeLineIndex] || rawLines[rawLines.length - 1];
ctx.save();
ctx.direction = 'rtl';
ctx.fillStyle = "#ffffff";
const fontSize = (height / 1080) * 52;
ctx.font = `bold ${fontSize}px ${selectedFont}`;
ctx.textAlign = 'center';
let textX = width / 2;
let textY = height * 0.88;
ctx.shadowColor = "rgba(0, 0, 0, 0.9)";
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
ctx.fillText(activeText, textX, textY);
ctx.restore();
}
}
// بدء تشغيل الرندرة المتسلسلة
renderFrameStep();
});
</script>
انا مهتم بمجال التقنية والربح من الانترنت واتطلع لنشر المزيد من المقالات التي تفيدكم
تعليقات
إرسال تعليق