feat: implementa a Fabrica de Quadrinhos com geracao de PDF e video animado via FFmpeg

This commit is contained in:
2026-06-03 12:01:03 +00:00
parent 4690672245
commit 0dbf687ec9
7 changed files with 914 additions and 1 deletions
+469
View File
@@ -3451,6 +3451,475 @@ const initApp = () => {
}
});
}
// ============================================================
// FÁBRICA DE QUADRINHOS (Criação de histórias visuais pedagógicas)
// ============================================================
const fabricaQuadrinhosModal = document.getElementById('fabricaQuadrinhosModal');
const btnCloseComicsModal = document.getElementById('btnCloseComicsModal');
const barBtnComics = document.getElementById('barBtnComics');
const btnGenerateComics = document.getElementById('btnGenerateComics');
const comicsTemaInput = document.getElementById('comicsTemaInput');
const comicsGenerateLoader = document.getElementById('comicsGenerateLoader');
const comicsLoaderText = document.getElementById('comicsLoaderText');
const comicsLoaderProgress = document.getElementById('comicsLoaderProgress');
const comicsResultArea = document.getElementById('comicsResultArea');
const comicsResultTitle = document.getElementById('comicsResultTitle');
const comicsGridContainer = document.getElementById('comicsGridContainer');
const comicsCenarioSelect = document.getElementById('comicsCenarioSelect');
const comicsCenarioCustomInput = document.getElementById('comicsCenarioCustomInput');
// Export PDF / Vídeo
const btnExportComicsPDFPortrait = document.getElementById('btnExportComicsPDFPortrait');
const btnExportComicsPDFLandscape = document.getElementById('btnExportComicsPDFLandscape');
const btnCompileComicsVideo = document.getElementById('btnCompileComicsVideo');
const comicsVideoMusic = document.getElementById('comicsVideoMusic');
const comicsVideoDuration = document.getElementById('comicsVideoDuration');
const comicsVideoLoader = document.getElementById('comicsVideoLoader');
const comicsVideoResult = document.getElementById('comicsVideoResult');
const comicsVideoPlayer = document.getElementById('comicsVideoPlayer');
const comicsVideoDownloadBtn = document.getElementById('comicsVideoDownloadBtn');
let selectedComicsQty = 5;
let selectedComicsBubbles = true;
let selectedComicsRatio = '16:9';
let generatedPanelsData = [];
// Toggle ativo quantidade de quadros
document.querySelectorAll('.btn-comics-qty').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-qty').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsQty = parseInt(btn.dataset.qty);
});
});
// Toggle ativo falas/diálogos
document.querySelectorAll('.btn-comics-bubbles').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-bubbles').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsBubbles = btn.dataset.bubbles === 'true';
});
});
// Toggle ativo proporção visual
document.querySelectorAll('.btn-comics-ratio').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-ratio').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsRatio = btn.dataset.ratio;
});
});
// Toggle ativo personagens principais
document.querySelectorAll('.btn-comics-char').forEach(btn => {
btn.addEventListener('click', () => {
btn.classList.toggle('active');
});
});
// Exibir/ocultar cenário personalizado
if (comicsCenarioSelect) {
comicsCenarioSelect.addEventListener('change', () => {
if (comicsCenarioSelect.value === 'custom') {
comicsCenarioCustomInput.style.display = 'block';
} else {
comicsCenarioCustomInput.style.display = 'none';
}
});
}
// Abrir e fechar modal Fábrica de Quadrinhos
const openComicsModal = () => {
fabricaQuadrinhosModal.style.display = 'flex';
comicsTemaInput.value = '';
comicsResultArea.style.display = 'none';
comicsGenerateLoader.style.display = 'none';
comicsVideoResult.style.display = 'none';
comicsVideoLoader.style.display = 'none';
btnGenerateComics.disabled = false;
comicsLoaderProgress.style.width = '0%';
};
if (barBtnComics) {
barBtnComics.addEventListener('click', openComicsModal);
}
if (btnCloseComicsModal) {
btnCloseComicsModal.addEventListener('click', () => {
fabricaQuadrinhosModal.style.display = 'none';
if (comicsVideoPlayer) comicsVideoPlayer.pause();
});
}
// Fechar modal clicando fora
window.addEventListener('click', (e) => {
if (e.target === fabricaQuadrinhosModal) {
fabricaQuadrinhosModal.style.display = 'none';
if (comicsVideoPlayer) comicsVideoPlayer.pause();
}
});
// Helper para converter URL de imagem local em base64
const getBase64Image = async (imgUrl) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/jpeg'));
};
img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl));
img.src = imgUrl;
});
};
// Ação de geração da história e imagens
if (btnGenerateComics) {
btnGenerateComics.addEventListener('click', async () => {
const tema = comicsTemaInput.value.trim();
if (!tema) {
alert('Por favor, digite um tema para a história.');
return;
}
// Reunir personagens selecionados
const personagens = [];
document.querySelectorAll('.btn-comics-char.active').forEach(b => {
personagens.push(b.textContent.trim());
});
if (personagens.length === 0) {
alert('Selecione pelo menos um personagem principal.');
return;
}
// Definir cenário
let cenario = comicsCenarioSelect.value;
if (cenario === 'custom') {
cenario = comicsCenarioCustomInput.value.trim() || 'em um local bonito';
}
btnGenerateComics.disabled = true;
comicsGenerateLoader.style.display = 'flex';
comicsResultArea.style.display = 'none';
comicsVideoResult.style.display = 'none';
comicsLoaderProgress.style.width = '5%';
comicsLoaderText.textContent = 'Escrevendo o roteiro e diálogos dos quadrinhos...';
try {
// 1. Gerar Roteiro
const scriptResp = await fetch('/api/comics/generate-script', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tema,
quantidade: selectedComicsQty,
baloes: selectedComicsBubbles,
proporcao: selectedComicsRatio,
personagens,
cenario
})
});
if (!scriptResp.ok) {
const err = await scriptResp.json();
throw new Error(err.error || 'Erro ao planejar roteiro');
}
const scriptData = await scriptResp.json();
comicsLoaderProgress.style.width = '15%';
// 2. Gerar cada quadrinho sequencialmente
generatedPanelsData = [];
const totalPanels = scriptData.panels.length;
for (let i = 0; i < totalPanels; i++) {
const panel = scriptData.panels[i];
const progressPercent = 15 + Math.floor((i / totalPanels) * 80);
comicsLoaderProgress.style.width = `${progressPercent}%`;
comicsLoaderText.textContent = `Ilustrando quadrinho ${i + 1} de ${totalPanels}...`;
// Injetamos a proporção e estilo de forma reforçada no prompt
const fullPrompt = `${panel.image_prompt}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
const frameResp = await fetch('/api/comics/generate-frame', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: fullPrompt })
});
if (!frameResp.ok) {
const err = await frameResp.json();
throw new Error(`Erro no quadrinho ${i + 1}: ${err.error}`);
}
const frameData = await frameResp.json();
generatedPanelsData.push({
panel_number: panel.panel_number,
imageUrl: frameData.imageUrl,
dialogue: panel.dialogue || ''
});
}
// Renderizar quadrinhos na tela
comicsResultTitle.textContent = scriptData.title || 'Fábrica de Quadrinhos Camila';
comicsGridContainer.innerHTML = '';
generatedPanelsData.forEach(panel => {
const panelCard = document.createElement('div');
panelCard.className = 'comic-panel-card';
const imgContainer = document.createElement('div');
imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`;
const img = document.createElement('img');
img.className = 'comic-panel-image';
img.src = panel.imageUrl;
img.alt = `Quadro ${panel.panel_number}`;
imgContainer.appendChild(img);
// Selo do número
const badge = document.createElement('div');
badge.className = 'comic-panel-number-badge';
badge.textContent = `Quadro ${panel.panel_number}`;
imgContainer.appendChild(badge);
// Balão de fala absoluto por cima se habilitado e houver diálogo
if (selectedComicsBubbles && panel.dialogue) {
const bubble = document.createElement('div');
bubble.className = 'comic-speech-bubble';
bubble.textContent = panel.dialogue;
imgContainer.appendChild(bubble);
}
panelCard.appendChild(imgContainer);
// Legenda do texto abaixo
if (panel.dialogue) {
const caption = document.createElement('div');
caption.className = 'comic-caption-text';
caption.textContent = panel.dialogue;
panelCard.appendChild(caption);
}
comicsGridContainer.appendChild(panelCard);
});
comicsGenerateLoader.style.display = 'none';
comicsResultArea.style.display = 'flex';
btnGenerateComics.disabled = false;
} catch (err) {
console.error('Erro ao fabricar quadrinhos:', err);
alert('Erro ao fabricar quadrinhos: ' + err.message);
comicsGenerateLoader.style.display = 'none';
btnGenerateComics.disabled = false;
}
});
}
// Exportar PDF Retrato (1 por página)
if (btnExportComicsPDFPortrait) {
btnExportComicsPDFPortrait.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
btnExportComicsPDFPortrait.disabled = true;
btnExportComicsPDFPortrait.textContent = '⏳ Montando PDF...';
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'pt', 'a4');
const title = comicsResultTitle.textContent || 'História em Quadrinhos';
for (let i = 0; i < generatedPanelsData.length; i++) {
const panel = generatedPanelsData[i];
if (i > 0) doc.addPage();
// Título e Borda
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(2);
doc.rect(20, 20, 555, 802);
doc.setFont('helvetica', 'bold');
doc.setFontSize(16);
doc.setTextColor(30, 41, 59);
doc.text(title, 297, 50, { align: 'center' });
// Imagem
const base64Img = await getBase64Image(panel.imageUrl);
const imgWidth = 515;
const imgHeight = selectedComicsRatio === '16:9' ? 290 : 386;
doc.addImage(base64Img, 'JPEG', 40, 80, imgWidth, imgHeight);
// Caixa de diálogo
if (panel.dialogue) {
const textY = 100 + imgHeight;
doc.setFillColor(248, 250, 252);
doc.setDrawColor(226, 232, 240);
doc.rect(40, textY, 515, 80, 'FD');
doc.setFont('helvetica', 'normal');
doc.setFontSize(12);
doc.setTextColor(15, 23, 42);
const splitText = doc.splitTextToSize(panel.dialogue, 495);
doc.text(splitText, 50, textY + 25);
}
// Rodapé
doc.setFont('helvetica', 'italic');
doc.setFontSize(10);
doc.setTextColor(100, 116, 139);
doc.text(`Criado com Camila AI - Quadro ${panel.panel_number} de ${generatedPanelsData.length}`, 297, 810, { align: 'center' });
}
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}.pdf`);
} catch (error) {
console.error('Erro ao gerar PDF Retrato:', error);
alert('Erro ao gerar PDF: ' + error.message);
} finally {
btnExportComicsPDFPortrait.disabled = false;
btnExportComicsPDFPortrait.textContent = '📄 PDF (1 por página - Retrato)';
}
});
}
// Exportar PDF Paisagem (2 por página)
if (btnExportComicsPDFLandscape) {
btnExportComicsPDFLandscape.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
btnExportComicsPDFLandscape.disabled = true;
btnExportComicsPDFLandscape.textContent = '⏳ Montando PDF...';
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF('l', 'pt', 'a4');
const title = comicsResultTitle.textContent || 'História em Quadrinhos';
const totalPanels = generatedPanelsData.length;
const totalPages = Math.ceil(totalPanels / 2);
for (let page = 0; page < totalPages; page++) {
if (page > 0) doc.addPage();
// Desenhar moldura da página
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(2);
doc.rect(20, 20, 802, 555);
// Título
doc.setFont('helvetica', 'bold');
doc.setFontSize(16);
doc.setTextColor(30, 41, 59);
doc.text(`${title} - Página ${page + 1} de ${totalPages}`, 421, 45, { align: 'center' });
// Renderizar até dois quadrinhos lado a lado
for (let col = 0; col < 2; col++) {
const index = page * 2 + col;
if (index >= totalPanels) break;
const panel = generatedPanelsData[index];
const startX = col === 0 ? 40 : 426;
const cardWidth = 376;
const imgHeight = selectedComicsRatio === '16:9' ? 211 : 282;
// Imagem do quadrinho
const base64Img = await getBase64Image(panel.imageUrl);
doc.addImage(base64Img, 'JPEG', startX, 70, cardWidth, imgHeight);
// Borda do quadrinho
doc.setDrawColor(30, 41, 59);
doc.setLineWidth(1.5);
doc.rect(startX, 70, cardWidth, imgHeight);
// Legenda abaixo da imagem
if (panel.dialogue) {
const textY = 85 + imgHeight;
doc.setFillColor(248, 250, 252);
doc.setDrawColor(203, 213, 225);
doc.rect(startX, textY, cardWidth, 65, 'FD');
doc.setFont('helvetica', 'normal');
doc.setFontSize(10);
doc.setTextColor(15, 23, 42);
const splitText = doc.splitTextToSize(panel.dialogue, cardWidth - 20);
doc.text(splitText, startX + 10, textY + 18);
}
// Etiqueta do número
doc.setFillColor(254, 240, 138);
doc.rect(startX + 10, 80, 50, 20, 'F');
doc.setDrawColor(30, 41, 59);
doc.rect(startX + 10, 80, 50, 20);
doc.setFont('helvetica', 'bold');
doc.setFontSize(9);
doc.setTextColor(30, 41, 59);
doc.text(`Quadro ${panel.panel_number}`, startX + 35, 93, { align: 'center' });
}
}
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}_paisagem.pdf`);
} catch (error) {
console.error('Erro ao gerar PDF Paisagem:', error);
alert('Erro ao gerar PDF: ' + error.message);
} finally {
btnExportComicsPDFLandscape.disabled = false;
btnExportComicsPDFLandscape.textContent = '📄 PDF (2 por página - Paisagem)';
}
});
}
// Compilar Vídeo de Apresentação (FFmpeg)
if (btnCompileComicsVideo) {
btnCompileComicsVideo.addEventListener('click', async () => {
if (generatedPanelsData.length === 0) return;
btnCompileComicsVideo.disabled = true;
comicsVideoLoader.style.display = 'flex';
comicsVideoResult.style.display = 'none';
try {
const response = await fetch('/api/comics/generate-video', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
frames: generatedPanelsData,
musicSelection: comicsVideoMusic.value,
frameDuration: comicsVideoDuration.value
})
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || 'Erro na compilação do vídeo');
}
const data = await response.json();
comicsVideoPlayer.src = data.videoUrl;
comicsVideoDownloadBtn.href = data.videoUrl;
comicsVideoResult.style.display = 'flex';
comicsVideoLoader.style.display = 'none';
btnCompileComicsVideo.disabled = false;
} catch (err) {
console.error('Erro ao gerar vídeo dos quadrinhos:', err);
alert('Erro ao compilar o vídeo: ' + err.message);
comicsVideoLoader.style.display = 'none';
btnCompileComicsVideo.disabled = false;
}
});
}
};
// Player de áudio personalizado global para as mídias geradas
Binary file not shown.
Binary file not shown.
Binary file not shown.
+167 -1
View File
@@ -234,6 +234,9 @@
<button type="button" class="btn-prompt-mode mode-music" id="barBtnEstudio" style="border-color: rgba(168, 85, 247, 0.4); color: #c084fc; background: rgba(168, 85, 247, 0.05);" title="Abrir painel de Estúdio Musical completo">
<span class="mode-icon">🎹</span> Estúdio Musical
</button>
<button type="button" class="btn-prompt-mode mode-comics" id="barBtnComics" style="border-color: rgba(236, 72, 153, 0.4); color: #f472b6; background: rgba(236, 72, 153, 0.05);" title="Abrir Fábrica de Quadrinhos Pedagógicos">
<span class="mode-icon">🎨</span> Fábrica de Quadrinhos
</button>
<button type="button" class="btn-prompt-mode mode-music" data-mode="AUDIO" title="Compor e gerar uma música pedagógica">
<span class="mode-icon">🎵</span> Criar Letra de Música
</button>
@@ -729,13 +732,176 @@
<pre id="musicStudioLyricsArea" style="background: var(--bg-secondary); padding: 14px; border-radius: 10px; max-height: 180px; overflow-y: auto; font-family: inherit; font-size: 0.92rem; line-height: 1.6; white-space: pre-wrap; border: 1px solid var(--border-light); color: var(--text-primary); margin: 0; box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal: Fábrica de Quadrinhos (Criação de histórias pedagógicas visuais) -->
<div id="fabricaQuadrinhosModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content" style="max-width: 800px; width: 95%; max-height: 90vh; display: flex; flex-direction: column;">
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light);">
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🎨 Fábrica de Quadrinhos Camila AI</h3>
<button id="btnCloseComicsModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
<!-- TEMA -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">1. Tema da História</label>
<textarea id="comicsTemaInput" class="obs-textarea" placeholder="Ex: Lucas e Mariana aprendendo a compartilhar seus brinquedos..." style="margin-top: 4px; height: 60px; min-height: 60px;"></textarea>
</div>
<!-- OPÇÕES RÁPIDAS (QUANTIDADE, BALÕES, PROPORÇÃO) -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px;">
<!-- QUANTIDADE -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">2. Tamanho</label>
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 4px;">
<button type="button" class="btn-music-option btn-comics-qty active" data-qty="5">5 Quadros</button>
<button type="button" class="btn-music-option btn-comics-qty" data-qty="10">10 Quadros</button>
</div>
</div>
<!-- BALÕES DE FALAS -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">3. Falas/Diálogos</label>
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 4px;">
<button type="button" class="btn-music-option btn-comics-bubbles active" data-bubbles="true">Com Balões</button>
<button type="button" class="btn-music-option btn-comics-bubbles" data-bubbles="false">Sem Texto</button>
</div>
</div>
<!-- PROPORÇÃO -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">4. Formato Visual</label>
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 4px;">
<button type="button" class="btn-music-option btn-comics-ratio active" data-ratio="16:9">16:9 (HD)</button>
<button type="button" class="btn-music-option btn-comics-ratio" data-ratio="4:3">4:3 (Retrô)</button>
</div>
</div>
</div>
<!-- PERSONAGENS -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">5. Personagens Principais</label>
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px;">
<button type="button" class="btn-music-option btn-comics-char active" data-char="lucas" style="flex: 1; min-width: 120px;">🧒 Lucas (Criança)</button>
<button type="button" class="btn-music-option btn-comics-char" data-char="mariana" style="flex: 1; min-width: 120px;">👧 Mariana (Criança)</button>
<button type="button" class="btn-music-option btn-comics-char" data-char="animais" style="flex: 1; min-width: 120px;">🐾 Animais da Floresta</button>
<button type="button" class="btn-music-option btn-comics-char" data-char="professor" style="flex: 1; min-width: 120px;">👩‍🏫 Professora Camila</button>
</div>
</div>
<!-- CENÁRIO -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">6. Cenário da História</label>
<div style="display: flex; gap: 10px; margin-top: 4px;">
<select id="comicsCenarioSelect" class="obs-select" style="flex: 1;">
<option value="no parque de diversões colorido">No Parque de Diversões</option>
<option value="na sala de aula da escola infantil">Na Sala de Aula</option>
<option value="em uma floresta encantada cheia de flores">Na Floresta Encantada</option>
<option value="no zoológico interagindo com animais">No Zoológico</option>
<option value="num jardim ensolarado de uma casa">No Jardim de Casa</option>
<option value="custom">Outro cenário (descreva abaixo)...</option>
</select>
</div>
<input type="text" id="comicsCenarioCustomInput" class="obs-input" placeholder="Descreva o cenário personalizado..." style="margin-top: 8px; display: none;">
</div>
<!-- BOTÃO DE AÇÃO -->
<button id="btnGenerateComics" style="width: 100%; padding: 12px; background: var(--brand-pink, #db2777); color: white; border: none; border-radius: 10px; font-weight: 700; font-size: 1rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(219, 39, 119, 0.3); transition: all 0.2s;">
<span>🎨 Criar Quadrinhos Pedagógicos</span>
</button>
<!-- LOADING DE QUADRINHOS -->
<div id="comicsGenerateLoader" style="display: none; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 30px; background: var(--bg-secondary); border-radius: 12px; border: 1px dashed var(--border-light);">
<div class="media-spinner" style="border-top-color: var(--brand-pink, #db2777);"></div>
<p id="comicsLoaderText" style="color: var(--text-primary); font-weight: 600; font-size: 0.95rem; margin: 0; text-align: center;">Compondo a história pedagógica...</p>
<div style="width: 100%; max-width: 250px; height: 6px; background: var(--border-light); border-radius: 3px; overflow: hidden;">
<div id="comicsLoaderProgress" style="width: 0%; height: 100%; background: var(--brand-pink, #db2777); transition: width 0.4s ease;"></div>
</div>
</div>
<!-- RESULTADOS DA HISTÓRIA -->
<div id="comicsResultArea" style="display: none; flex-direction: column; gap: 20px;">
<h4 id="comicsResultTitle" style="font-family: 'Outfit', sans-serif; font-size: 1.15rem; color: var(--text-primary); margin: 0; text-align: center; font-weight: 700;">Título da História</h4>
<!-- CONTÊINER DOS QUADRINHOS -->
<div id="comicsGridContainer" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px;">
<!-- Gerado dinamicamente: quadrinhos com as imagens e balões de fala correspondentes -->
</div>
<!-- OPÇÕES DE EXPORTAÇÃO -->
<div style="background: var(--bg-secondary); padding: 16px; border-radius: 12px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 14px;">
<h5 style="margin: 0; font-size: 0.9rem; font-weight: 700; text-transform: uppercase; color: var(--text-secondary); letter-spacing: 0.5px;">📥 Opções de Exportação</h5>
<!-- DOWNLOAD DE PDF -->
<div style="display: flex; flex-direction: column; gap: 6px;">
<span style="font-size: 0.8rem; font-weight: 600; color: var(--text-primary);">Salvar como Documento PDF:</span>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
<button id="btnExportComicsPDFPortrait" class="btn-music-option" style="border-color: #10a37f; color: #10a37f; background: rgba(16, 163, 127, 0.05);">📄 PDF (1 por página - Retrato)</button>
<button id="btnExportComicsPDFLandscape" class="btn-music-option" style="border-color: #10a37f; color: #10a37f; background: rgba(16, 163, 127, 0.05);">📄 PDF (2 por página - Paisagem)</button>
</div>
</div>
<!-- APRESENTAÇÃO EM VÍDEO -->
<div style="display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--border-light); padding-top: 14px;">
<span style="font-size: 0.8rem; font-weight: 600; color: var(--text-primary);">Criar Apresentação de Vídeo Animada:</span>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="settings-group" style="gap: 4px;">
<label style="font-size: 0.72rem; color: var(--text-secondary); font-weight: 600;">Trilha Sonora de Fundo</label>
<select id="comicsVideoMusic" class="obs-select" style="padding: 6px 10px; font-size: 0.8rem;">
<option value="alegre">🎸 Trilha 1: Alegre e Divertida</option>
<option value="calma">🌙 Trilha 2: Calma e Relaxante</option>
<option value="aventura">🎮 Trilha 3: Aventura e Mistério</option>
</select>
</div>
<div class="settings-group" style="gap: 4px;">
<label style="font-size: 0.72rem; color: var(--text-secondary); font-weight: 600;">Duração por Quadrinho</label>
<select id="comicsVideoDuration" class="obs-select" style="padding: 6px 10px; font-size: 0.8rem;">
<option value="3">⏱️ 3 segundos</option>
<option value="5" selected>⏱️ 5 segundos</option>
<option value="8">⏱️ 8 segundos</option>
</select>
</div>
</div>
<button id="btnCompileComicsVideo" style="padding: 10px; background: var(--brand-pink, #db2777); color: white; border: none; border-radius: 8px; font-weight: 600; font-size: 0.88rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 6px; transition: all 0.2s;">
<span>🎬 Gerar Apresentação de Vídeo (MP4)</span>
</button>
<!-- Spinner do Vídeo -->
<div id="comicsVideoLoader" style="display: none; align-items: center; gap: 10px; justify-content: center; padding: 12px; background: var(--bg-primary); border-radius: 8px; border: 1px dashed var(--border-light);">
<div class="media-spinner" style="width: 20px; height: 20px; border-width: 2.5px; border-top-color: var(--brand-pink, #db2777);"></div>
<span style="font-size: 0.82rem; color: var(--text-secondary); font-weight: 500;">FFmpeg compilando a apresentação musical...</span>
</div>
<!-- Vídeo Player de Saída -->
<div id="comicsVideoResult" style="display: none; flex-direction: column; align-items: center; gap: 10px; margin-top: 10px;">
<video id="comicsVideoPlayer" controls style="width: 100%; max-width: 480px; border-radius: 8px; border: 1px solid var(--border-light); background: black;"></video>
<a id="comicsVideoDownloadBtn" href="" download="apresentacao_quadrinhos.mp4" class="btn-music-option" style="width: 100%; max-width: 480px; text-decoration: none; border-color: var(--brand-pink); color: var(--brand-pink); background: rgba(219, 39, 119, 0.05); justify-content: center;">
📥 Baixar Vídeo MP4
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bibliotecas externas para renderizar markdown e blocos de código -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- V2 cache-busting: version timestamp -->
+91
View File
@@ -3137,4 +3137,95 @@ code {
color: rgb(253, 224, 71);
}
/* ==========================================================================
FÁBRICA DE QUADRINHOS
========================================================================== */
.comic-panel-card {
position: relative;
border: 3px solid #1e293b;
background: var(--bg-secondary);
box-shadow: 4px 4px 0px #1e293b;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s;
}
.comic-panel-card:hover {
transform: translate(-2px, -2px);
box-shadow: 6px 6px 0px #1e293b;
}
.comic-panel-image-container {
position: relative;
width: 100%;
overflow: hidden;
background: var(--bg-tertiary);
border-bottom: 2px solid #1e293b;
}
.comic-panel-image-container.ratio-16-9 {
aspect-ratio: 16/9;
}
.comic-panel-image-container.ratio-4-3 {
aspect-ratio: 4/3;
}
.comic-panel-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.comic-speech-bubble {
position: absolute;
top: 10px;
left: 10px;
background: white;
border: 2px solid #1e293b;
border-radius: 12px;
padding: 6px 10px;
font-size: 0.78rem;
font-weight: 700;
color: #0f172a;
max-width: 80%;
box-shadow: 2px 2px 0px rgba(30, 41, 59, 0.15);
z-index: 10;
}
.comic-panel-number-badge {
position: absolute;
top: 10px;
right: 10px;
background: #fef08a; /* Amarelo claro */
color: #1e293b;
font-weight: 800;
font-size: 0.8rem;
padding: 2px 6px;
border: 2px solid #1e293b;
border-radius: 6px;
z-index: 12;
box-shadow: 1px 1px 0px #1e293b;
}
.comic-caption-text {
padding: 10px 14px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.82rem;
line-height: 1.4;
border-top: 1px solid var(--border-light);
font-weight: 500;
}
.btn-comics-qty.active, .btn-comics-bubbles.active, .btn-comics-ratio.active, .btn-comics-char.active {
background-color: rgba(219, 39, 119, 0.15);
border-color: #db2777;
color: #f472b6;
font-weight: 600;
box-shadow: 0 0 8px rgba(219, 39, 119, 0.2);
}