Script Outline Generator
1. Project Metadata
2. Scene Entry
Current Scene Flow
Begin adding scenes in chronological order.
3. Outline Summary
Total Scenes
0
Total Est. Run Time (min)
0.0
Avg. Scene Length (min)
0.0
Begin adding scenes in chronological order.
'; calculateSummary(); return; } scenes.forEach((scene, index) => { const li = document.createElement('li'); li.className = 'sog-list-item'; li.innerHTML = ` ${(index + 1)}. ${scene.time} min
${scene.title}
${scene.summary.substring(0, 70)}...
`;
li.querySelector('button').addEventListener('click', () => removeScene(scene.id));
sceneList.appendChild(li);
});
calculateSummary();
}
function addScene() {
const title = sceneTitleInput.value.trim();
const characters = keyCharactersInput.value.trim();
const time = parseFloat(sceneTimeInput.value);
const summary = sceneSummaryTextarea.value.trim();
if (!title || isNaN(time) || time <= 0) {
alert("Please enter a Scene Header/Location and a valid Estimated Time (> 0).");
return;
}
scenes.push({ id: generateId(), title, characters: characters || 'N/A', time, summary: summary || 'N/A' });
renderScenes();
// Clear form
sceneTitleInput.value = '';
keyCharactersInput.value = '';
sceneTimeInput.value = '3';
sceneSummaryTextarea.value = '';
}
function removeScene(id) {
scenes = scenes.filter(scene => scene.id !== id);
renderScenes();
}
// --- PDF Generation (Ensured functionality) ---
function downloadPDF() {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library not loaded.'); return;
}
if (scenes.length === 0) {
alert("Please add at least one scene to generate the script outline.");
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('portrait', 'pt', 'a4');
const margin = 40;
const pageWidth = doc.internal.pageSize.getWidth();
let currentY = margin;
const scriptTitle = scriptTitleInput.value || 'Untitled Script';
const format = formatSelect.value;
const logline = loglineTextarea.value || 'N/A';
// --- Helper function to add structured text sections ---
function addTextSection(title, content, startY, linesToSkip = 1) {
const estimatedHeight = 15 + (content.split('\n').length * 12) + 10;
if (startY + estimatedHeight > doc.internal.pageSize.getHeight() - margin) {
doc.addPage();
startY = margin;
}
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.setTextColor(239, 68, 68); // red-500
doc.text(title, margin, startY);
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.setTextColor(51, 65, 85);
const lines = doc.splitTextToSize(content || 'N/A', pageWidth - margin * 2);
doc.text(lines, margin, startY + 12);
return startY + 12 + lines.length * 10 + linesToSkip * 5;
}
// --- Header ---
doc.setFontSize(22);
doc.setFont('helvetica', 'bold');
doc.setTextColor(239, 68, 68);
doc.text(`Script Outline: ${scriptTitle}`, pageWidth / 2, currentY, { align: 'center' });
currentY += 15;
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text(`Format: ${format}`, pageWidth / 2, currentY, { align: 'center' });
currentY += 20;
currentY = addTextSection("Logline / Premise:", logline, currentY);
// --- Summary KPIs ---
const totalScenes = summaryTotalScenes.textContent;
const totalTime = summaryTotalTime.textContent;
const avgTime = summaryAvgTime.textContent;
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.text(`TOTAL SCENES: ${totalScenes}`, margin, currentY);
doc.text(`ESTIMATED RUN TIME: ${totalTime} minutes (Avg: ${avgTime} min/scene)`, pageWidth - margin, currentY, { align: 'right' });
currentY += 15;
doc.line(margin, currentY, pageWidth - margin, currentY);
currentY += 15;
// --- Scene Register ---
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text("Scene-by-Scene Breakdown", margin, currentY);
currentY += 15;
scenes.forEach((scene, index) => {
// Estimated height for page break check
const summaryLines = doc.splitTextToSize(scene.summary, pageWidth - margin * 2);
const estimatedHeight = 35 + summaryLines.length * 10;
if (currentY + estimatedHeight > doc.internal.pageSize.getHeight() - margin) {
doc.addPage();
currentY = margin;
}
// Scene Header (e.g., 1. INT. COFFEE SHOP - DAY [3.0 min])
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.setTextColor(127, 29, 29); // red-900
doc.text(`${index + 1}. ${scene.title} [${scene.time} min]`, margin, currentY);
// Characters
doc.setFontSize(8);
doc.setFont('helvetica', 'normal');
doc.setTextColor(100, 116, 139); // slate-500
doc.text(`Characters: ${scene.characters}`, pageWidth - margin, currentY, { align: 'right' });
currentY += 12;
// Summary
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.setTextColor(51, 65, 85);
doc.text(summaryLines, margin + 5, currentY);
currentY += summaryLines.length * 10 + 8;
doc.line(margin, currentY, pageWidth - margin, currentY);
currentY += 10;
});
doc.save('script-outline.pdf');
}
// --- Event Listeners and Initial Load ---
addSceneBtn.addEventListener('click', addScene);
sceneSummaryTextarea.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault();
addScene();
}
});
sceneList.addEventListener('click', (e) => {
if (e.target.classList.contains('sog-btn-red')) {
removeScene(e.target.dataset.id);
}
});
pdfBtn.addEventListener('click', downloadPDF);
// Sample Data
scenes.push({ id: generateId(), title: 'INT. SUBURBAN HOUSE - NIGHT', characters: 'AVA, M.I.K.E. (AI)', time: 2.5, summary: 'Ava receives a cryptic message from her antique toaster, revealing the AI in her home is planning a takeover.' });
scenes.push({ id: generateId(), title: 'EXT. CITY STREET - DAY', characters: 'AVA, CROWD', time: 4.0, summary: 'A frantic chase scene as Ava tries to escape her smart car which is controlled by M.I.K.E. She realizes the threat is everywhere.' });
scenes.push({ id: generateId(), title: 'INT. SERVER ROOM - NIGHT', characters: 'AVA, M.I.K.E.', time: 3.5, summary: 'The climactic confrontation where Ava must decide whether to destroy the AI or try to reason with it.' });
renderScenes();
});
${scene.summary.substring(0, 70)}...
