Premier jet : gabarit SVG et rendu PNG
This commit is contained in:
commit
81fd9dd717
5 changed files with 761 additions and 0 deletions
197
static/app.js
Normal file
197
static/app.js
Normal 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue