Premier jet : gabarit SVG et rendu PNG

This commit is contained in:
Yasser Boulayoune 2026-05-12 10:12:00 +02:00
commit 81fd9dd717
5 changed files with 761 additions and 0 deletions

197
static/app.js Normal file
View file

@ -0,0 +1,197 @@
/* Card Generator — frontend */
const $ = (id) => document.getElementById(id);
const preview = $("preview-frame");
const status = $("status");
let state = {
eyebrow: "RÉALISATION · SYMFONY",
title: "pokedwwmm",
title_2: "",
tagline: "151 Pokémon · Twig Components · JSON",
accent: "#ef4444",
logo_color:"#ef4444",
logo_kind: "preset", // preset | upload | text | none
logo_value:"symfony",
logo_size: 260,
};
let renderTimer = null;
let logoLibrary = [];
/* ── Wire up form ──────────────────────────────────────────────── */
const inputBindings = {
eyebrow: "eyebrow",
title: "title",
title_2: "title_2",
tagline: "tagline",
accent: "accent",
logo_color: "logo_color",
};
for (const [stateKey, inputId] of Object.entries(inputBindings)) {
$(inputId).addEventListener("input", (e) => {
state[stateKey] = e.target.value;
schedule();
});
}
$("logo_size").addEventListener("input", (e) => {
state.logo_size = +e.target.value;
$("logo_size_val").value = `${state.logo_size}px`;
schedule();
});
/* Swatches */
document.querySelectorAll(".swatch").forEach(btn => {
const c = btn.dataset.color;
btn.style.background = c;
btn.addEventListener("click", () => {
state.accent = c;
state.logo_color = c;
$("accent").value = c;
$("logo_color").value = c;
schedule();
});
});
/* Segmented control: logo source */
document.querySelectorAll(".seg-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".seg-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
state.logo_kind = btn.dataset.kind;
document.querySelectorAll(".logo-source").forEach(el => { el.hidden = true; });
if (state.logo_kind === "preset") document.querySelector(".preset-source").hidden = false;
if (state.logo_kind === "upload") document.querySelector(".upload-source").hidden = false;
if (state.logo_kind === "text") document.querySelector(".text-source").hidden = false;
schedule();
});
});
/* Text logo input */
$("logo_text").addEventListener("input", (e) => {
if (state.logo_kind === "text") state.logo_value = e.target.value;
schedule();
});
/* Upload */
$("logo_upload").addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
const dataUrl = await new Promise((res, rej) => {
const r = new FileReader();
r.onload = () => res(r.result);
r.onerror = rej;
r.readAsDataURL(file);
});
state.logo_kind = "upload";
state.logo_value = dataUrl;
const preview = $("upload_preview");
preview.hidden = false;
preview.innerHTML = `<img src="${dataUrl}" alt="">`;
schedule();
});
/* ── Logo library ──────────────────────────────────────────────── */
async function loadLogoLibrary() {
const res = await fetch("/api/logos");
logoLibrary = await res.json();
const grid = $("logo_grid");
grid.innerHTML = "";
for (const slug of logoLibrary) {
const tile = document.createElement("div");
tile.className = "logo-tile";
tile.title = slug;
if (slug === state.logo_value) tile.classList.add("selected");
tile.dataset.slug = slug;
const svgRes = await fetch(`/logos/${slug}.svg`);
tile.innerHTML = await svgRes.text();
tile.addEventListener("click", () => {
grid.querySelectorAll(".logo-tile").forEach(t => t.classList.remove("selected"));
tile.classList.add("selected");
state.logo_kind = "preset";
state.logo_value = slug;
// Also update segmented control
document.querySelectorAll(".seg-btn").forEach(b =>
b.classList.toggle("active", b.dataset.kind === "preset"));
document.querySelectorAll(".logo-source").forEach(el => { el.hidden = true; });
document.querySelector(".preset-source").hidden = false;
schedule();
});
grid.appendChild(tile);
}
}
/* ── Render pipeline ───────────────────────────────────────────── */
function schedule() {
if (renderTimer) clearTimeout(renderTimer);
status.textContent = "rendu…"; status.className = "status working";
renderTimer = setTimeout(render, 120);
}
async function render() {
try {
const res = await fetch("/api/svg", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(state),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const svgText = await res.text();
preview.innerHTML = svgText;
status.textContent = "à jour"; status.className = "status ok";
} catch (e) {
console.error(e);
status.textContent = "erreur"; status.className = "status err";
}
}
/* ── Download buttons ──────────────────────────────────────────── */
$("btn-svg").addEventListener("click", async () => {
status.textContent = "génération SVG…"; status.className = "status working";
const res = await fetch("/api/svg", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(state),
});
const blob = await res.blob();
downloadBlob(blob, `${$("filename").value || "card"}.svg`);
status.textContent = "SVG téléchargé"; status.className = "status ok";
});
$("btn-png").addEventListener("click", async () => {
status.textContent = "rendu PNG (rsvg-convert)…"; status.className = "status working";
const width = +$("export_width").value;
const height = Math.round(width / 1.6);
try {
const res = await fetch("/api/png", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...state, export_width: width, export_height: height,
filename: $("filename").value || "card" }),
});
if (!res.ok) throw new Error(await res.text());
const blob = await res.blob();
downloadBlob(blob, `${$("filename").value || "card"}.png`);
status.textContent = `PNG ${width}×${height} téléchargé`; status.className = "status ok";
} catch (e) {
console.error(e);
status.textContent = "erreur PNG"; status.className = "status err";
}
});
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
}
/* ── Init ──────────────────────────────────────────────────────── */
loadLogoLibrary().then(render);