diff --git a/landing/modeles/gabarits.mjs b/landing/modeles/gabarits.mjs new file mode 100644 index 0000000..4ab40d5 --- /dev/null +++ b/landing/modeles/gabarits.mjs @@ -0,0 +1,344 @@ +// Gabarits des modèles d'arbre généalogique à imprimer (page /arbre-genealogique-a-remplir/). +// Chaque gabarit renvoie une page HTML contenant un SVG en millimètres (viewBox = format A4). +// Les traits d'écriture sont ajustés dans le navigateur après chargement des polices +// (voir ajusterTraits dans generer.mjs) : chaque trait démarre juste après son libellé. + +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +const C = { + encre: '#2a2622', + a300: '#edb6a4', + a500: '#d2694a', + a700: '#93402a', +} + +// Logo Parenthèse (symbole + nom), repris tel quel de public/logo.svg +const LOGO_SRC = readFileSync(fileURLToPath(new URL('../public/logo.svg', import.meta.url)), 'utf8') +const LOGO_VIEWBOX = LOGO_SRC.match(/viewBox="([^"]+)"/)[1] +const LOGO_INNER = LOGO_SRC.replace(/^]*>/, '').replace(/<\/svg>\s*$/, '') +const LOGO_RATIO = 7.541 + +const esc = (s) => String(s).replace(/&/g, '&').replace(/ Math.round(v * 100) / 100 + +const STYLE = ` + .titre { font-family: 'Newsreader', serif; font-size: 9px; fill: ${C.encre}; } + .titre-i { font-family: 'Newsreader', serif; font-style: italic; font-size: 9px; fill: ${C.a700}; } + .sous { font-family: 'DM Sans', sans-serif; font-size: 3.1px; fill: ${C.encre}; fill-opacity: .72; } + .tag { font-family: 'DM Sans', sans-serif; font-weight: 500; font-size: 2.4px; letter-spacing: .35px; fill: ${C.a700}; text-transform: uppercase; } + .lab { font-family: 'DM Sans', sans-serif; font-size: 2.25px; fill: ${C.encre}; fill-opacity: .68; } + .role { font-family: 'DM Sans', sans-serif; font-weight: 500; font-size: 2.15px; fill: ${C.a700}; } + .sosa { font-family: 'DM Sans', sans-serif; font-size: 2px; fill: ${C.a500}; } + .pied { font-family: 'DM Sans', sans-serif; font-size: 2.3px; fill: ${C.encre}; fill-opacity: .6; } + .photo-lab { font-family: 'DM Sans', sans-serif; font-size: 2.1px; fill: ${C.encre}; fill-opacity: .35; letter-spacing: .3px; text-transform: uppercase; } + .bx { fill: #fff; stroke: ${C.encre}; stroke-opacity: .55; stroke-width: .3; } + .wl { stroke: ${C.encre}; stroke-opacity: .34; stroke-width: .2; } + .dot { stroke: ${C.encre}; stroke-opacity: .3; stroke-width: .2; stroke-dasharray: .35 .9; stroke-linecap: round; fill: none; } + .cn { stroke: ${C.a500}; stroke-width: .35; fill: none; stroke-linejoin: round; } + .regle { stroke: ${C.encre}; stroke-opacity: .16; stroke-width: .2; } + .photo { fill: #fff; stroke: ${C.encre}; stroke-opacity: .28; stroke-width: .2; stroke-dasharray: .8 .8; } +` + +function page({ W, H, body, titre = 'modèle' }) { + const orient = W > H ? 'landscape' : 'portrait' + return ` +${esc(titre)} + + + + +
+ + +${body} +
` +} + +// Un champ = libellé + trait d'écriture (le trait est recalé en JS après le libellé) +function champ(x, xEnd, y, label) { + return `${esc(label)}` +} + +function entete({ W, margin, tag, top = 14 }) { + const y = top + 9 + return ` + Arbre généalogique + de la famille + ${esc(tag)} + ` +} + +function pied({ W, H, margin, texte = 'Modèle gratuit à imprimer et à remplir' }) { + const h = 3.4 + const w = h * LOGO_RATIO + const y = H - 8 + return ` + ${LOGO_INNER} + ${esc(texte)}, parenthese.io` +} + +const ROLES = [ + ['Moi'], + ['Père', 'Mère'], + ['Grand-père paternel', 'Grand-mère paternelle', 'Grand-père maternel', 'Grand-mère maternelle'], + Array.from({ length: 8 }, (_, i) => (i % 2 ? 'Arrière-grand-mère' : 'Arrière-grand-père')), +] + +const CHAMPS = ['Prénom', 'Nom', 'Né(e) le', 'à', 'Décès'] + +// Boîte de personne : rôle + numéro Sosa en tête, photo optionnelle, puis les champs +function boite({ x, y, w, h, role, sosa, ls, photo }) { + const pad = 2.4 + let out = `` + out += `${esc(role)}` + out += `${sosa}` + let cy = y + 6 + if (photo) { + const pw = photo.w + const ph = photo.h + const px = x + (w - pw) / 2 + out += `` + out += `Photo` + cy += ph + 1.6 + } + CHAMPS.forEach((label, k) => { + out += champ(x + pad, x + w - pad, cy + (k + 1) * ls - 0.9, label) + }) + return out +} + +function hauteurBoite(ls, photo) { + return 6 + (photo ? photo.h + 1.6 : 0) + CHAMPS.length * ls + 1.4 +} + +// Arbre ascendant vertical : « Moi » en bas, les ancêtres au-dessus, côté paternel à gauche +function arbre({ W, H, margin, top, bottom, gens, gap, maxW, ls, photo, tag, texte }) { + const boxH = hauteurBoite(ls, photo) + const vgap = (bottom - top - gens * boxH) / (gens - 1) + const rowY = (g) => bottom - boxH - g * (boxH + vgap) + const availW = W - 2 * margin + const cx = (g, i) => margin + (availW / 2 ** g) * (i + 0.5) + let traits = '' + let boites = '' + for (let g = 0; g < gens; g++) { + const count = 2 ** g + const slot = availW / count + const w = Math.min(slot - gap, maxW[g]) + for (let i = 0; i < count; i++) { + boites += boite({ x: cx(g, i) - w / 2, y: rowY(g), w, h: boxH, role: ROLES[g][i], sosa: count + i, ls, photo }) + if (g < gens - 1) { + const yb = rowY(g + 1) + boxH + const yt = rowY(g) + const bar = (yb + yt) / 2 + const a = cx(g + 1, 2 * i) + const b = cx(g + 1, 2 * i + 1) + traits += `` + } + } + } + return page({ W, H, body: entete({ W, margin, tag }) + traits + boites + pied({ W, H, margin, texte }) }) +} + +export function troisGenerations() { + return arbre({ + W: 210, H: 297, margin: 14, top: 50, bottom: 276, gens: 3, gap: 4, + maxW: [84, 66, 42], ls: 8.2, tag: '3 générations', + }) +} + +export function quatreGenerations() { + return arbre({ + W: 297, H: 210, margin: 12, top: 47, bottom: 193, gens: 4, gap: 2.6, + maxW: [76, 64, 56, 32], ls: 5.3, tag: '4 générations', + }) +} + +export function troisGenerationsPhotos() { + return arbre({ + W: 210, H: 297, margin: 14, top: 46, bottom: 280, gens: 3, gap: 4, + maxW: [84, 66, 42], ls: 6.2, photo: { w: 23, h: 28 }, tag: '3 générations avec photos', + }) +} + +// ─── Éventail 5 générations ───────────────────────────────────────────────── + +const pol = (cx, cy, r, deg) => { + const a = (deg * Math.PI) / 180 + return [cx + r * Math.cos(a), cy - r * Math.sin(a)] +} + +function secteur(cx, cy, r0, r1, d0, d1) { + const [x0, y0] = pol(cx, cy, r1, d0) + const [x1, y1] = pol(cx, cy, r1, d1) + const [x2, y2] = pol(cx, cy, r0, d1) + const [x3, y3] = pol(cx, cy, r0, d0) + return `M${n(x0)} ${n(y0)} A${r1} ${r1} 0 0 1 ${n(x1)} ${n(y1)} L${n(x2)} ${n(y2)} A${r0} ${r0} 0 0 0 ${n(x3)} ${n(y3)} Z` +} + +function arc(cx, cy, r, d0, d1) { + const [x0, y0] = pol(cx, cy, r, d0) + const [x1, y1] = pol(cx, cy, r, d1) + return `M${n(x0)} ${n(y0)} A${r} ${r} 0 0 1 ${n(x1)} ${n(y1)}` +} + +export function cinqGenerationsEventail() { + const W = 297 + const H = 210 + const margin = 12 + const cx = W / 2 + const cy = 188 + const R = [0, 26, 55, 82, 108, 134] + const LEGENDES = [['Moi'], ['Parents'], ['Grands-', 'parents'], ['Arrière-grands-', 'parents'], ['Trisaïeuls']] + let body = '' + // anneaux de la génération 2 à 5 (Sosa 2 à 31), du père (gauche) à la mère (droite) + for (let g = 1; g <= 4; g++) { + const count = 2 ** g + const step = 180 / count + for (let i = 0; i < count; i++) { + const d0 = 180 - i * step + const d1 = 180 - (i + 1) * step + const fill = g % 2 ? `fill="${C.a300}" fill-opacity=".14"` : 'fill="#fff"' + body += `` + const depth = R[g + 1] - R[g] + if (g < 4) { + // lignes d'écriture en arcs + const lines = g === 1 ? 3 : 2 + for (let k = 1; k <= lines; k++) { + const r = R[g] + (depth * k) / (lines + 1) + const inset = (2.2 / r) * (180 / Math.PI) + body += `` + } + } else { + // dernière couronne : écriture dans le sens du rayon + for (let k = 1; k <= 2; k++) { + const d = d0 - (step * k) / 3 + const [x0, y0] = pol(cx, cy, R[g] + 2.2, d) + const [x1, y1] = pol(cx, cy, R[g + 1] - 5.5, d) + body += `` + } + } + // numéro Sosa près du bord extérieur + const [sx, sy] = pol(cx, cy, R[g + 1] - 2.6, (d0 + d1) / 2) + body += `${count + i}` + } + } + // demi-disque central (Moi) + body += `` + ;[cy - 15, cy - 9.5, cy - 4].forEach((y) => { + const half = Math.sqrt(R[1] ** 2 - (cy - y) ** 2) - 2.5 + body += `` + }) + body += `1` + // légende des couronnes sous la ligne de base, des deux côtés + LEGENDES.forEach((lines, g) => { + const mid = g === 0 ? 0 : (R[g] + R[g + 1]) / 2 + const xs = g === 0 ? [cx] : [cx - mid, cx + mid] + xs.forEach((x) => { + lines.forEach((t, k) => { + body += `${esc(t)}` + }) + }) + }) + body += `Numérotation Sosa. Le père d'une personne porte le double de son numéro, la mère le double plus un.` + return page({ W, H, body: entete({ W, margin, tag: '5 générations en éventail' }) + body + pied({ W, H, margin }) }) +} + +// ─── Arbre dessiné, 3 générations ─────────────────────────────────────────── + +// Branche effilée : on échantillonne une courbe de Bézier cubique et on décale ses normales +function branche(p0, p1, p2, p3, w0, w1) { + const N = 28 + const L = [] + const Rr = [] + for (let k = 0; k <= N; k++) { + const t = k / N + const u = 1 - t + const x = u ** 3 * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t ** 3 * p3[0] + const y = u ** 3 * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t ** 3 * p3[1] + const dx = 3 * u * u * (p1[0] - p0[0]) + 6 * u * t * (p2[0] - p1[0]) + 3 * t * t * (p3[0] - p2[0]) + const dy = 3 * u * u * (p1[1] - p0[1]) + 6 * u * t * (p2[1] - p1[1]) + 3 * t * t * (p3[1] - p2[1]) + const len = Math.hypot(dx, dy) || 1 + const hw = (w0 + (w1 - w0) * t) / 2 + L.push([x - (dy / len) * hw, y + (dx / len) * hw]) + Rr.push([x + (dy / len) * hw, y - (dx / len) * hw]) + } + const pts = [...L, ...Rr.reverse()] + return `` +} + +function feuille(x, y, angle, s) { + return `` +} + +export function arbreDessine() { + const W = 210 + const H = 297 + const margin = 14 + const ls = 7 + const bh = hauteurBoite(ls) + // positions des boîtes (centre x, haut y, largeur) + const moi = { x: 105, y: 238, w: 76 } + const parents = [{ x: 58, y: 150, w: 60 }, { x: 152, y: 150, w: 60 }] + const gps = [{ x: 35, y: 64, w: 42 }, { x: 81, y: 64, w: 42 }, { x: 129, y: 64, w: 42 }, { x: 175, y: 64, w: 42 }] + let dessin = '' + // tronc et branches, derrière les boîtes + // tronc = deux branches qui partent du pied et se séparent en douceur (pas de jonction visible) + dessin += branche([103, 266], [103, 200], [86, 194], [58, 172], 12, 5.5) + dessin += branche([107, 266], [107, 200], [124, 194], [152, 172], 12, 5.5) + const sous = [ + [[58, 170], [52, 140], [36, 132], [35, 100]], + [[58, 170], [64, 140], [80, 132], [81, 100]], + [[152, 170], [146, 140], [130, 132], [129, 100]], + [[152, 170], [158, 140], [174, 132], [175, 100]], + ] + sous.forEach(([a, b, c, d]) => { + dessin += branche(a, b, c, d, 5, 2.6) + }) + // rameaux secondaires qui sortent du cadre des boîtes + const rameaux = [ + [[35, 66], [30, 56], [22, 52], [16, 48]], + [[81, 66], [84, 56], [92, 52], [98, 47]], + [[129, 66], [126, 56], [118, 52], [112, 47]], + [[175, 66], [180, 56], [188, 52], [194, 48]], + [[58, 152], [50, 142], [30, 146], [22, 140]], + [[152, 152], [160, 142], [180, 146], [188, 140]], + ] + rameaux.forEach(([a, b, c, d]) => { + dessin += branche(a, b, c, d, 2.4, 0.6) + }) + const feuilles = [ + [16, 48, -150, 5], [18, 51, 160, 4.2], [22, 45, -120, 4.4], [98, 47, -40, 5], [95, 44, -80, 4.2], [100, 51, 10, 4], + [112, 47, -140, 5], [115, 44, -100, 4.2], [110, 51, 170, 4], [194, 48, -30, 5], [192, 51, 20, 4.2], [188, 45, -60, 4.4], + [22, 140, -160, 5], [25, 136, -110, 4.2], [20, 144, 150, 4], [188, 140, -20, 5], [185, 136, -70, 4.2], [190, 144, 30, 4], + ] + let feuillage = '' + feuilles.forEach(([x, y, a, s]) => { feuillage += feuille(x, y, a, s) }) + // sol + dessin += `` + let boites = '' + const roles = [ROLES[0], ROLES[1], ROLES[2]] + ;[[moi], parents, gps].forEach((row, g) => { + row.forEach((b, i) => { + boites += boite({ x: b.x - b.w / 2, y: b.y, w: b.w, h: bh, role: roles[g][i], sosa: 2 ** g + i, ls }) + }) + }) + // teintes opaques (a300 à 55 % et a500 à 50 % sur blanc) pour éviter les surimpressions aux jonctions + const body = entete({ W, margin, tag: '3 générations, arbre dessiné' }) + + `${dessin}${feuillage}` + + boites + pied({ W, H, margin }) + return page({ W, H, body }) +} + +export const MODELES = [ + { slug: 'arbre-genealogique-3-generations', build: troisGenerations, portrait: true }, + { slug: 'arbre-genealogique-4-generations', build: quatreGenerations, portrait: false }, + { slug: 'arbre-genealogique-5-generations-eventail', build: cinqGenerationsEventail, portrait: false }, + { slug: 'arbre-genealogique-avec-photos', build: troisGenerationsPhotos, portrait: true }, + { slug: 'arbre-genealogique-dessine', build: arbreDessine, portrait: true }, +] diff --git a/landing/modeles/generer.mjs b/landing/modeles/generer.mjs new file mode 100644 index 0000000..97f4c4d --- /dev/null +++ b/landing/modeles/generer.mjs @@ -0,0 +1,90 @@ +// Génère les modèles PDF A4 et leurs aperçus WebP pour /arbre-genealogique-a-remplir/. +// +// node modeles/generer.mjs (depuis landing/) +// +// Playwright n'est pas une dépendance de la landing : le script le cherche dans node_modules, +// puis dans le dossier indiqué par PLAYWRIGHT_DIR (ex. D:/Commercial/tools/web-qa/node_modules). +// Chromium est lancé via le canal « chrome » (Chrome installé sur la machine). +// Les polices (Newsreader, DM Sans) viennent de Google Fonts et sont embarquées dans le PDF. + +import { createRequire } from 'node:module' +import { mkdirSync, writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { MODELES } from './gabarits.mjs' + +const require = createRequire(import.meta.url) +function chargerPlaywright() { + const dirs = [process.env.PLAYWRIGHT_DIR, 'D:/Commercial/tools/web-qa/node_modules'].filter(Boolean) + try { return require('playwright') } catch {} + for (const dir of dirs) { + try { return require(require.resolve('playwright', { paths: [dir] })) } catch {} + } + throw new Error('Playwright introuvable. Définir PLAYWRIGHT_DIR vers un node_modules qui le contient.') +} + +const OUT = fileURLToPath(new URL('../public/arbre-genealogique-a-remplir/', import.meta.url)) +const APERCU_LARGEUR = 720 + +// Recale chaque trait d'écriture juste après son libellé, une fois les polices chargées +function ajusterTraits() { + document.querySelectorAll('g.f').forEach((g) => { + const t = g.querySelector('text') + const l = g.querySelector('line') + const b = t.getBBox() + l.setAttribute('x1', String(Math.round((b.x + b.width + 1.2) * 100) / 100)) + }) +} + +async function main() { + const { chromium } = chargerPlaywright() + mkdirSync(OUT, { recursive: true }) + const browser = await chromium.launch({ channel: 'chrome' }) + const ctx = await browser.newContext({ deviceScaleFactor: 2 }) + const page = await ctx.newPage() + const convert = await ctx.newPage() + const seul = process.argv[2] + + for (const m of MODELES) { + if (seul && !m.slug.includes(seul)) continue + const html = m.build() + const [wmm, hmm] = m.portrait ? [210, 297] : [297, 210] + await page.setViewportSize({ width: Math.ceil(wmm * 96 / 25.4), height: Math.ceil(hmm * 96 / 25.4) }) + await page.setContent(html, { waitUntil: 'networkidle' }) + const ok = await page.evaluate(async () => { + await Promise.all([ + document.fonts.load('400 12px "DM Sans"'), + document.fonts.load('500 12px "DM Sans"'), + document.fonts.load('400 12px "Newsreader"'), + document.fonts.load('italic 400 12px "Newsreader"'), + ]) + await document.fonts.ready + return document.fonts.check('400 12px "DM Sans"') && document.fonts.check('400 12px "Newsreader"') + }) + if (!ok) throw new Error(`Polices non chargées pour ${m.slug}`) + await page.evaluate(ajusterTraits) + + const pdf = `${OUT}${m.slug}.pdf` + await page.pdf({ path: pdf, preferCSSPageSize: true, printBackground: true }) + + // Aperçu : capture de la feuille, réduite et encodée en WebP par le canvas de Chromium + const png = await page.locator('.sheet').screenshot({ type: 'png' }) + const dataUrl = await convert.evaluate(async ({ b64, largeur }) => { + const img = new Image() + img.src = `data:image/png;base64,${b64}` + await img.decode() + const c = document.createElement('canvas') + c.width = largeur + c.height = Math.round(img.height * largeur / img.width) + const g = c.getContext('2d') + g.imageSmoothingQuality = 'high' + g.drawImage(img, 0, 0, c.width, c.height) + return { url: c.toDataURL('image/webp', 0.86), w: c.width, h: c.height } + }, { b64: png.toString('base64'), largeur: APERCU_LARGEUR }) + const webp = Buffer.from(dataUrl.url.split(',')[1], 'base64') + writeFileSync(`${OUT}${m.slug}.webp`, webp) + console.log(`${m.slug} pdf ok apercu ${dataUrl.w}x${dataUrl.h} ${Math.round(webp.length / 1024)} Ko`) + } + await browser.close() +} + +main().catch((e) => { console.error(e); process.exit(1) }) diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-3-generations.pdf b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-3-generations.pdf new file mode 100644 index 0000000..e03275a Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-3-generations.pdf differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-3-generations.webp b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-3-generations.webp new file mode 100644 index 0000000..2a45bf5 Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-3-generations.webp differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-4-generations.pdf b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-4-generations.pdf new file mode 100644 index 0000000..e44330b Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-4-generations.pdf differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-4-generations.webp b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-4-generations.webp new file mode 100644 index 0000000..7a1fc1c Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-4-generations.webp differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-5-generations-eventail.pdf b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-5-generations-eventail.pdf new file mode 100644 index 0000000..0cead2a Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-5-generations-eventail.pdf differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-5-generations-eventail.webp b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-5-generations-eventail.webp new file mode 100644 index 0000000..49f37fb Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-5-generations-eventail.webp differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-avec-photos.pdf b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-avec-photos.pdf new file mode 100644 index 0000000..2f0a9a3 Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-avec-photos.pdf differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-avec-photos.webp b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-avec-photos.webp new file mode 100644 index 0000000..c5580d7 Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-avec-photos.webp differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-dessine.pdf b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-dessine.pdf new file mode 100644 index 0000000..244578a Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-dessine.pdf differ diff --git a/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-dessine.webp b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-dessine.webp new file mode 100644 index 0000000..b5ddaf8 Binary files /dev/null and b/landing/public/arbre-genealogique-a-remplir/arbre-genealogique-dessine.webp differ diff --git a/landing/public/arbre-genealogique-a-remplir/index.html b/landing/public/arbre-genealogique-a-remplir/index.html new file mode 100644 index 0000000..c829d04 --- /dev/null +++ b/landing/public/arbre-genealogique-a-remplir/index.html @@ -0,0 +1,211 @@ + + + + + + + Arbre généalogique à remplir, modèles vierges gratuits + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ Modèles gratuits +

Arbre généalogique à remplir, modèles vierges gratuits

+

Cinq modèles à imprimer en A4, de trois à cinq générations. Téléchargez le PDF, imprimez-le, remplissez-le à la main avec la famille. Gratuit, sans inscription.

+ +

Les modèles à télécharger

+
+
+
Modèle d'arbre généalogique vierge sur 3 générations, sept cases à remplir, de l'enfant aux quatre grands-parents
+
+

3 générations

+

Vous, vos parents, vos quatre grands-parents. Le modèle pour commencer, avec de la place pour écrire.

+
+ Télécharger le PDF + A4 portrait +
+
+
+ +
+
Modèle d'arbre généalogique à remplir avec un cadre photo dans chaque case, sur 3 générations
+
+

3 générations avec photos

+

Les mêmes sept cases, chacune avec un cadre pour coller une photo d'identité ou dessiner un portrait.

+
+ Télécharger le PDF + A4 portrait +
+
+
+ +
+
Modèle d'arbre généalogique vierge sur 4 générations en format paysage, quinze cases jusqu'aux arrière-grands-parents
+
+

4 générations

+

Jusqu'aux huit arrière-grands-parents, en paysage. Quinze cases numérotées.

+
+ Télécharger le PDF + A4 paysage +
+
+
+ +
+
Modèle d'arbre généalogique en éventail sur 5 générations, trente et une cases numérotées en demi-cercle
+
+

5 générations en éventail

+

Trente et une personnes sur une page, jusqu'aux trisaïeuls. Le format le plus dense, à agrandir en A3 si vous écrivez gros.

+
+ Télécharger le PDF + A4 paysage +
+
+
+ +
+
Dessin d'arbre généalogique à remplir, un tronc et des branches qui relient sept cases sur 3 générations
+
+

Arbre dessiné, 3 générations

+

Un tronc et des branches relient les cases. Pour un arbre à afficher, à offrir ou à rendre en classe.

+
+ Télécharger le PDF + A4 portrait +
+
+
+
+ +
+

Les numéros dans les cases suivent la numérotation Sosa, la même dans tous les arbres. Vous êtes le 1, votre père le 2, votre mère le 3. Le père d'une personne porte le double de son numéro, la mère le double plus un.

+
+ +

Comment remplir un arbre généalogique

+ +

1. Commencez par vous

+

Remplissez la case 1 avec ce que vous savez sans chercher. Nom de naissance, date et lieu exacts, tels qu'ils figurent sur votre acte de naissance. C'est la seule case qui ne demande aucune question.

+ +

2. Remontez une génération à la fois

+

Vos parents, puis vos grands-parents, puis les leurs. Ne sautez pas de case, chaque génération sert à retrouver la suivante. Pour les femmes, écrivez le nom de naissance et non le nom d'épouse, c'est lui qui mène à leur famille.

+ +

3. Interrogez les plus âgés d'abord

+

Grands-parents, grands-oncles et grands-tantes savent ce qui n'est écrit nulle part. Venez avec le modèle imprimé, montrez les cases vides, et demandez qui manque. Le livret de famille, les faire-part et le dos des vieilles photos donnent souvent les dates exactes.

+ +

4. Quand une date manque

+

Écrivez ce que vous savez, « vers 1920 » ou « avant 1945 », suivi d'un point d'interrogation. Une estimation signalée vaut mieux qu'une case vide, elle se corrige plus tard. Les registres d'état civil anciens sont numérisés et consultables gratuitement sur le site des archives départementales, dans la plupart des départements. Les actes de naissance et de mariage y sont librement consultables après 75 ans.

+ +

5. Au crayon d'abord

+

Remplissez une première version au crayon à papier, le temps que les dates se confirment. Recopiez ensuite au propre sur un second tirage. Pour imprimer, choisissez « taille réelle » dans la fenêtre d'impression, pas « ajuster », afin que les marges restent intactes.

+ +

La même chose en ligne, avec les photos et les voix

+

Une feuille s'arrête au bord de la page et à ceux qui l'ont sous les yeux. Parenthèse est un arbre généalogique en ligne, gratuit, où chaque personne porte ses photos, des enregistrements de sa voix, des vidéos et les histoires qu'on raconte sur elle.

+

Frères, sœurs, oncles, tantes et cousins y ont leur place, sur autant de générations que vous en connaissez. Vous envoyez un lien, la famille consulte sans créer de compte, et ceux qui ont le mot de passe de contribution complètent ce qui manque. Pour les photos, le guide faire un arbre généalogique avec des photos explique comment les numériser et les ranger.

+ +
+

Votre arbre, en ligne et partagé

+

Un compte, un nom de famille, et vous invitez les vôtres par un lien.

+ Créer mon arbre +
+ +

Questions fréquentes

+
+
+ Combien de générations mettre dans un arbre généalogique ? +

Trois pour un premier arbre ou un travail d'école, quatre si vous connaissez vos arrière-grands-parents, cinq en éventail pour aller jusqu'aux trisaïeuls. Chaque génération double le nombre de cases, soit 2, 4, 8 puis 16 personnes.

+
+
+ Comment faire un arbre généalogique soi-même ? +

Imprimez un modèle vierge, remplissez votre case, puis celles de vos parents et grands-parents en interrogeant la famille. Le livret de famille donne les dates exactes, les archives départementales en ligne donnent les actes plus anciens.

+
+
+ Les modèles d'arbre généalogique sont-ils gratuits ? +

Oui. Les cinq modèles se téléchargent sans inscription ni adresse e-mail, et vous pouvez les imprimer et les partager autant que vous voulez.

+
+
+ Comment imprimer un arbre généalogique en A3 ? +

Ouvrez le PDF, choisissez le format A3 dans la fenêtre d'impression et l'option « ajuster à la page ». Le modèle s'agrandit sans perdre en netteté, ce qui donne plus de place pour écrire, surtout sur l'éventail à cinq générations.

+
+
+
+
+ + + + diff --git a/landing/public/sitemap.xml b/landing/public/sitemap.xml index d678b39..301db93 100644 --- a/landing/public/sitemap.xml +++ b/landing/public/sitemap.xml @@ -24,6 +24,12 @@ monthly 0.8 + + https://parenthese.io/arbre-genealogique-a-remplir/ + 2026-09-25 + monthly + 0.8 + https://parenthese.io/raconter-histoire-de-famille/ 2026-09-14 diff --git a/landing/src/App.jsx b/landing/src/App.jsx index 17f8480..8e3f108 100644 --- a/landing/src/App.jsx +++ b/landing/src/App.jsx @@ -902,6 +902,11 @@ const GUIDES = [ title: "Raconter l'histoire de sa famille", text: "Par qui commencer, les questions qui font parler, enregistrer plutôt qu'écrire, et où garder ce qu'on a recueilli.", }, + { + href: '/arbre-genealogique-a-remplir/', + title: 'Arbre généalogique à remplir', + text: "Cinq modèles vierges à imprimer en PDF, de trois à cinq générations, avec cases photo ou arbre dessiné. Et comment les remplir.", + }, ] export default function App() {