FC

فلش‌کارت ساز حرفه‌ای v3

Front / Back، ابزار متن پیشرفته، خروجی PNG / PDF و مدیریت لایه‌ها

Zoom
`; w.document.write(html); w.document.close(); } function printA6() { exportPDF(); } function updateTextToggles(layer) { $$('.mini-toggle [data-style]').forEach(btn => { const key = btn.dataset.style; btn.classList.toggle('active', !!layer?.[key]); }); $$('.mini-toggle [data-align]').forEach(btn => { btn.classList.toggle('active', layer?.align === btn.dataset.align); }); } function bindToolboxTabs() { dom.toolbox?.addEventListener('click', (e) => { const btn = e.target.closest('.tab-btn'); if (!btn) return; $$('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const tab = btn.dataset.tab; $$('.tab-content').forEach(p => p.classList.remove('active')); $('#' + tab + '-tab').classList.add('active'); }); } function bindCanvasInteractions() { dom.flashcard.addEventListener('click', (e) => { const layerEl = e.target.closest('.layer'); if (!layerEl) { state.selectedId = null; renderLayers(); return; } const id = layerEl.dataset.id; selectLayer(id); }); } function bindDragResize() { let start = null; dom.flashcard.addEventListener('pointerdown', (e) => { const handle = e.target.closest('.resize-handle'); const layerEl = e.target.closest('.layer'); if (!layerEl) return; const layer = getLayerById(layerEl.dataset.id); if (!layer || layer.locked) return; const rect = dom.flashcard.getBoundingClientRect(); start = { type: handle ? 'resize' : 'drag', layer, px: e.clientX, py: e.clientY, x: layer.x, y: layer.y, w: layer.w, h: layer.h, rect }; layerEl.setPointerCapture?.(e.pointerId); }); window.addEventListener('pointermove', (e) => { if (!start) return; const dx = e.clientX - start.px; const dy = e.clientY - start.py; const scale = start.rect.width / 1050; if (start.type === 'drag') { start.layer.x = start.x + dx / scale; start.layer.y = start.y + dy / scale; } else { start.layer.w = Math.max(20, start.w + dx / scale); start.layer.h = Math.max(20, start.h + dy / scale); } render(); state.selectedId = start.layer.id; renderLayers(); }); window.addEventListener('pointerup', () => { if (start) { saveHistory(); } start = null; }); } function bindTopbar() { onId('about-btn', 'click', () => { dom.aboutModal.classList.add('open'); }); onId('close-modal-btn', 'click', () => dom.aboutModal.classList.remove('open')); dom.aboutModal.addEventListener('click', (e) => { if (e.target === dom.aboutModal) dom.aboutModal.classList.remove('open'); }); onId('night-mode-btn', 'click', () => { setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'); }); onId('side-front-btn', 'click', () => setSide('front')); onId('side-back-btn', 'click', () => setSide('back')); onId('undo-btn', 'click', undo); onId('redo-btn', 'click', redo); onId('delete-btn', 'click', deleteSelected); onId('delete-all-btn', 'click', clearSide); onId('duplicate-side-btn', 'click', duplicateSide); onId('clear-side-btn', 'click', clearSide); $$('.zoom-group [data-zoom]').forEach(btn => { btn.addEventListener('click', () => setZoom(Number(btn.dataset.zoom))); }); } function bindInputs() { onId('add-text-btn', 'click', addText); onId('add-shape-btn', 'click', addShape); onId('add-image-btn', 'click', () => $('#image-upload').files.length && addImage($('#image-upload').files)); onId('flip-image-btn', 'click', () => { const layer = getLayerById(state.selectedId); if (layer && layer.type === 'image') { layer.flipX = !layer.flipX; saveHistory(); render(); } }); onId('duplicate-btn', 'click', duplicateSelected); onId('lock-btn', 'click', toggleLock); onId('export-png-btn', 'click', exportPNG); onId('export-pdf-btn', 'click', exportPDF); onId('print-btn', 'click', printA6); onId('text-input', 'dblclick', () => { const layer = getLayerById(state.selectedId); if (layer && layer.type === 'text') { layer.editable = !layer.editable; saveHistory(); render(); } }); onId('font-family', 'change', e => updateTextStyle('fontFamily', e.target.value)); onId('font-size', 'input', e => updateTextStyle('fontSize', Number(e.target.value))); onId('color-picker', 'input', e => updateTextStyle('color', e.target.value)); onId('text-bg-picker', 'input', e => updateTextStyle('textBg', e.target.value)); onId('text-opacity', 'input', e => updateTextStyle('opacity', Number(e.target.value))); onId('letter-spacing', 'input', e => updateTextStyle('letterSpacing', Number(e.target.value))); onId('line-height', 'input', e => updateTextStyle('lineHeight', Number(e.target.value))); onId('text-shadow', 'change', e => updateTextStyle('textShadow', e.target.value === 'soft' ? '0 3px 8px rgba(0,0,0,.18)' : e.target.value === 'hard' ? '2px 2px 0 rgba(0,0,0,.35)' : 'none')); onId('outline-size', 'input', e => updateTextStyle('outline', Number(e.target.value))); $$('.mini-toggle [data-style]').forEach(btn => { btn.addEventListener('click', () => { const layer = getLayerById(state.selectedId); if (!layer || layer.type !== 'text') return; const key = btn.dataset.style; layer[key] = !layer[key]; saveHistory(); render(); }); }); $$('.mini-toggle [data-align]').forEach(btn => { btn.addEventListener('click', () => { updateTextStyle('align', btn.dataset.align); }); }); onId('image-upload', 'change', e => addImage(e.target.files)); onId('image-radius', 'input', e => updateImageStyle('radius', Number(e.target.value))); onId('image-opacity', 'input', e => updateImageStyle('opacity', Number(e.target.value))); onId('image-border-color', 'input', e => updateImageStyle('border', e.target.value)); onId('image-border-width', 'input', e => updateImageStyle('borderWidth', Number(e.target.value))); onId('image-shadow', 'change', e => updateImageStyle('shadow', e.target.value === 'soft' ? '0 8px 22px rgba(0,0,0,.18)' : e.target.value === 'hard' ? '0 10px 0 rgba(0,0,0,.25)' : 'none')); onId('image-fit', 'change', e => updateImageStyle('fit', e.target.value)); onId('shape-fill', 'input', e => updateShapeStyle('fill', e.target.value)); onId('shape-border', 'input', e => updateShapeStyle('border', e.target.value)); onId('shape-border-width', 'input', e => updateShapeStyle('borderWidth', Number(e.target.value))); onId('shape-radius', 'input', e => updateShapeStyle('radius', Number(e.target.value))); onId('shape-opacity', 'input', e => updateShapeStyle('opacity', Number(e.target.value))); onId('shape-rotate', 'input', e => updateShapeStyle('rotate', Number(e.target.value))); onId('solid-bg-btn', 'click', () => setBgType('solid')); onId('gradient-bg-btn', 'click', () => setBgType('gradient')); onId('pattern-bg-btn', 'click', () => setBgType('pattern')); onId('white-bg-btn', 'click', () => { const side = currentSide(); side.backgroundType = 'solid'; side.background = '#ffffff'; saveHistory(); render(); }); onId('bg-color', 'input', e => { currentSide().background = e.target.value; applyBackground(); saveHistory(); }); onId('bg-color-2', 'input', e => { currentSide().background2 = e.target.value; applyBackground(); saveHistory(); }); onId('bg-gradient-type', 'change', e => { currentSide().backgroundGradientType = e.target.value; applyBackground(); saveHistory(); }); onId('bg-upload', 'change', e => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const side = currentSide(); side.backgroundType = 'image'; side.backgroundImage = reader.result; saveHistory(); render(); }; reader.readAsDataURL(file); }); onId('bg-repeat', 'change', e => { currentSide().backgroundRepeat = e.target.value; applyBackground(); saveHistory(); }); onId('bg-size', 'change', e => { currentSide().backgroundSize = e.target.value; applyBackground(); saveHistory(); }); onId('print-fit', 'change', e => state.printFit = e.target.value); } function bindLayerList() { dom.layersList.addEventListener('click', (e) => { const item = e.target.closest('.layer-item'); if (!item) return; const actBtn = e.target.closest('button[data-act]'); const id = item.dataset.id; state.selectedId = id; const side = currentSide(); const idx = side.layers.findIndex(l => l.id === id); const layer = side.layers[idx]; if (!layer) return; if (actBtn) { const act = actBtn.dataset.act; if (act === 'del') side.layers.splice(idx, 1); if (act === 'lock') layer.locked = !layer.locked; if (act === 'up' && idx < side.layers.length - 1) { [side.layers[idx], side.layers[idx + 1]] = [side.layers[idx + 1], side.layers[idx]]; } if (act === 'down' && idx > 0) { [side.layers[idx], side.layers[idx - 1]] = [side.layers[idx - 1], side.layers[idx]]; } saveHistory(); } render(); }); } function seed() { saveHistory(); saveHistory(); render(); } bindToolboxTabs(); bindTopbar(); bindInputs(); bindCanvasInteractions(); bindDragResize(); bindLayerList(); seed(); setZoom(1); setTheme('light'); setSide('front'); })();