feat: implementa a Fabrica de Quadrinhos com geracao de PDF e video animado via FFmpeg
This commit is contained in:
+469
@@ -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
@@ -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;">×</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 -->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user