Des grilles pour tout

Tables aléatoires et générateurs divers.
Avatar de l’utilisateur
ZikDragon
Dragon d'argent
Messages : 81
Inscription : Lun 16 Sep 2024 08:19
Localisation :
Version de D&D préférée : ?
Univers de D&D préféré : ?
Race : ?
Classe : ?
Alignement : ?
Dieu :

Des grilles pour tout

Message par ZikDragon »

Bonjour tout le monde

Etant quelque peu obsédé par les cartes et leurs échelles lorsqu'il s'agit de voyages ou de donjons, je me suis attellé à un petit code HTML qui pour le moment parvient à calmer mes crises de cartographite aiguuuuuue :wizard: donc je le partage en espérant que ça pourra servir à quelqu'un.

Voici donc un petit code qui permet de placer un calque de grille, carré ou hexagone sur pointe ou sur coté, de dimensionner la grille en pixel ou en milimetres, de charger une image en fond pour la placer sous la grille et de pouvoir ajuster aussi bien la grille que l image sur les axes X et Y ainsi que de zoomer à la molette de la souris. Une présélection A4 , A3 et format libre ( en milimetres ) sont à disposition par le menu dédié. L'impression du fichier pdf est possible pour la page construite à l'écran. J'y ai également intégré un bouton pour afficher/masquer la grille, un choix de couleur et taille de trait en pixel, et une option de coordonées.
Le tout étant bien entendu utilisable hors ligne et entièrement gratuitement , en copiant tout simplement la totalité du code ci-dessous dans un editeur de texte (genre notepad) et de le sauvegarder en utilisant l'extension .HTML
Pour l'utiliser c 'est aussi simple que démarrer un raccourci vers une page internet

Code : Tout sélectionner

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Générateur de Grilles Pro</title>
    <style>
        :root {
            --bg-dark: #2c3e50;
            --bg-light: #34495e;
            --accent: #27ae60;
            --panel-bg: #ffffff;
        }

        body { 
            margin: 0; 
            background: var(--bg-dark); 
            display: flex; 
            flex-direction: column; 
            height: 100vh;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 
            overflow: hidden; 
        }

        header { 
            background: var(--panel-bg); 
            padding: 8px 15px; 
            display: flex; 
            gap: 15px; 
            flex-wrap: wrap; 
            z-index: 100; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
            align-items: center; 
        }

        #viewport {
            flex-grow: 1;
            background: var(--bg-light);
            overflow: auto;
            display: flex;
            justify-content: center;
            align-items: flex-start;
            padding: 40px;
        }

        .page-canvas {
            background: white;
            position: relative;
            box-shadow: 0 0 30px rgba(0,0,0,0.5);
            flex-shrink: 0;
            background-repeat: no-repeat;
            cursor: grab;
        }
        .page-canvas:active { cursor: grabbing; }

        #grid-svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
        .grid-shape { fill: none; vector-effect: non-scaling-stroke; }
        .grid-text { font-weight: bold; text-anchor: middle; dominant-baseline: middle; pointer-events: none; }
        
        .hide-grid .grid-shape, .hide-coords .grid-text { display: none; }

        .group { border-left: 2px solid #eee; padding-left: 10px; display: flex; gap: 8px; align-items: center; }
        label { font-size: 10px; font-weight: bold; color: #7f8c8d; text-transform: uppercase; display: block; }
        input[type="number"] { width: 50px; border: 1px solid #ddd; border-radius: 3px; }
        .val-display { font-size: 11px; font-weight: bold; min-width: 45px; display: inline-block; color: #2c3e50; }
        
        button { cursor: pointer; border: none; border-radius: 4px; transition: 0.2s; padding: 5px 10px; }
        .btn-main { background: var(--accent); color: white; padding: 8px 15px; font-weight: bold; }
        .btn-main:hover { background: #219150; }

        @media print { 
            @page { margin: 0; }
            body { background: white; }
            header { display: none !important; } 
            #viewport { padding: 0 !important; background: white !important; overflow: visible !important; display: block !important; } 
            .page-canvas { box-shadow: none !important; margin: 0 !important; border: none !important; } 
        }
    </style>
</head>
<body class="hide-coords">

<header id="controls">
    <div class="group">
        <div>
            <label>Format Page</label>
            <select id="paperFormat">
                <option value="210,297">A4</option>
                <option value="297,420">A3</option>
                <option value="custom">Perso</option>
            </select>
        </div>
        <div id="customDims" style="display:none">
            <input type="number" id="custW" value="210">×<input type="number" id="custH" value="297">
        </div>
    </div>

    <div class="group" style="background: #f9f9f9; padding-right: 10px; border-radius: 5px;">
        <select id="gridType">
            <option value="hex">Hex (Plat)</option>
            <option value="hex_pointy">Hex (Pointe)</option>
            <option value="sqr">Carré</option>
        </select>
        <div>
            <label>Taille Grille</label>
            <div style="display:flex; align-items:center; gap:5px;">
                <input type="range" id="size" min="5" max="400" value="60">
                <span class="val-display" id="sizeVal">60px</span>
                <select id="unit"><option value="px">px</option><option value="mm">mm</option></select>
            </div>
        </div>
        <div>
            <label>Trait</label>
            <input type="range" id="thick" min="0.5" max="10" step="0.5" value="1">
            <span class="val-display" id="thickVal">1px</span>
        </div>
        <input type="color" id="cColor" value="#000000">
    </div>

    <div class="group">
        <div style="font-size: 11px;">
            <label>Mode Souris</label>
            <input type="radio" name="mode" id="modeGrid" checked> Grille
            <input type="radio" name="mode" id="modeImg"> Image
        </div>
        <div>
            <label>Zoom Image</label>
            <input type="range" id="imgZoomRange" min="5" max="500" value="100">
            <span class="val-display" id="zoomVal">100%</span>
        </div>
        <input type="file" id="imgInp" accept="image/*" style="width: 120px; font-size: 10px;">
    </div>

    <div class="group">
        <button onclick="document.body.classList.toggle('hide-grid')" title="Voir/Cacher">👁️</button>
        <div style="text-align:center">
            <label>Num.</label>
            <input type="checkbox" id="showNb">
        </div>
    </div>

    <button onclick="window.print()" class="btn-main">IMPRIMER PDF</button>
</header>

<div id="viewport">
    <div class="page-canvas" id="page">
        <svg id="grid-svg" xmlns="http://www.w3.org/2000/svg"></svg>
    </div>
</div>

<script>
    const svgNS = "http://www.w3.org/2000/svg";
    const svgGrid = document.getElementById('grid-svg');
    const page = document.getElementById('page');
    const MM_TO_PX = 3.7795275591;

    let gX = 0, gY = 0, iX = 0, iY = 0, iZoom = 100;
    let isDragging = false, lastX, lastY;
    let imgNaturalWidth = 0;

    function updatePageSize() {
        const format = document.getElementById('paperFormat').value;
        const customDiv = document.getElementById('customDims');
        let w, h;

        if (format !== "custom") {
            const dims = format.split(',');
            w = dims[0]; h = dims[1];
            customDiv.style.display = "none";
        } else {
            w = document.getElementById('custW').value;
            h = document.getElementById('custH').value;
            customDiv.style.display = "inline-block";
        }
        page.style.width = (w * MM_TO_PX) + "px";
        page.style.height = (h * MM_TO_PX) + "px";
        drawGrid();
    }

    function drawGrid() {
        svgGrid.innerHTML = '';
        const rawSize = parseFloat(document.getElementById('size').value);
        const unit = document.getElementById('unit').value;
        const type = document.getElementById('gridType').value;
        const thick = document.getElementById('thick').value;
        const color = document.getElementById('cColor').value;
        const rect = page.getBoundingClientRect();

        const size = (unit === "mm") ? rawSize * MM_TO_PX : rawSize;
        
        document.getElementById('sizeVal').textContent = rawSize + unit;
        document.getElementById('thickVal').textContent = thick + "px";

        if (type === "hex") {
            // Hexagone à plat
            const h = size * 0.866025;
            const colW = size * 0.75;
            for (let c = -2; c < rect.width / colW + 2; c++) {
                for (let r = -2; r < rect.height / h + 2; r++) {
                    let px = c * colW + (gX % (colW * 2));
                    let py = r * h + (gY % h);
                    if (c % 2 !== 0) py += h / 2;
                    const poly = document.createElementNS(svgNS, "polygon");
                    poly.setAttribute("points", `${px + size*0.25},${py} ${px + size*0.75},${py} ${px + size},${py + h*0.5} ${px + size*0.75},${py + h} ${px + size*0.25},${py + h} ${px},${py + h*0.5}`);
                    renderElement(poly, color, thick, c, r, px + size*0.5, py + h*0.5, size);
                }
            }
        } else if (type === "hex_pointy") {
            // Hexagone sur pointe
            const w = size * 0.866025;
            const rowH = size * 0.75;
            for (let r = -2; r < rect.height / rowH + 2; r++) {
                for (let c = -2; c < rect.width / w + 2; c++) {
                    let px = c * w + (gX % w);
                    let py = r * rowH + (gY % (rowH * 2));
                    if (r % 2 !== 0) px += w / 2;
                    const poly = document.createElementNS(svgNS, "polygon");
                    poly.setAttribute("points", `${px + w*0.5},${py} ${px + w},${py + size*0.25} ${px + w},${py + size*0.75} ${px + w*0.5},${py + size} ${px},${py + size*0.75} ${px},${py + size*0.25}`);
                    renderElement(poly, color, thick, c, r, px + w*0.5, py + size*0.5, size);
                }
            }
        } else {
            for (let c = -1; c < rect.width / size + 1; c++) {
                for (let r = -1; r < rect.height / size + 1; r++) {
                    let px = c * size + (gX % size);
                    let py = r * size + (gY % size);
                    const rectEl = document.createElementNS(svgNS, "rect");
                    rectEl.setAttribute("x", px); rectEl.setAttribute("y", py);
                    rectEl.setAttribute("width", size); rectEl.setAttribute("height", size);
                    renderElement(rectEl, color, thick, c, r, px + size*0.5, py + size*0.5, size);
                }
            }
        }
    }

    function renderElement(el, color, thick, c, r, tx, ty, size) {
        el.setAttribute("class", "grid-shape");
        el.setAttribute("stroke", color);
        el.setAttribute("stroke-width", thick);
        svgGrid.appendChild(el);
        const text = document.createElementNS(svgNS, "text");
        text.setAttribute("x", tx); text.setAttribute("y", ty);
        text.setAttribute("class", "grid-text");
        text.setAttribute("fill", color);
        text.setAttribute("font-size", Math.max(size * 0.2, 5));
        text.textContent = `${c},${r}`;
        svgGrid.appendChild(text);
    }

    page.onmousedown = (e) => { if(e.button === 0) { isDragging = true; lastX = e.clientX; lastY = e.clientY; }};
    
    window.onmousemove = (e) => {
        if (!isDragging) return;
        const dx = e.clientX - lastX, dy = e.clientY - lastY;
        if (document.getElementById('modeGrid').checked) {
            gX += dx; gY += dy; drawGrid();
        } else {
            iX += dx; iY += dy; updateImgStyle();
        }
        lastX = e.clientX; lastY = e.clientY;
    };

    window.onmouseup = () => isDragging = false;

    page.onwheel = (e) => {
        e.preventDefault();
        if (document.getElementById('modeGrid').checked) {
            let s = document.getElementById('size');
            s.value = parseFloat(s.value) + (e.deltaY < 0 ? 2 : -2);
            drawGrid();
        } else {
            iZoom += (e.deltaY < 0 ? 5 : -5);
            iZoom = Math.max(5, iZoom);
            document.getElementById('imgZoomRange').value = iZoom;
            updateImgStyle();
        }
    };

    function updateImgStyle() {
        page.style.backgroundPosition = `${iX}px ${iY}px`;
        document.getElementById('zoomVal').textContent = iZoom + "%";
        if (imgNaturalWidth > 0) {
            page.style.backgroundSize = `${(imgNaturalWidth * (iZoom / 100))}px auto`;
        }
    }

    document.getElementById('paperFormat').onchange = updatePageSize;
    document.getElementById('gridType').onchange = drawGrid;
    document.getElementById('size').oninput = drawGrid;
    document.getElementById('unit').onchange = drawGrid;
    document.getElementById('thick').oninput = drawGrid;
    document.getElementById('cColor').oninput = drawGrid;
    document.getElementById('custW').oninput = updatePageSize;
    document.getElementById('custH').oninput = updatePageSize;
    document.getElementById('imgZoomRange').oninput = (e) => { iZoom = parseInt(e.target.value); updateImgStyle(); };
    document.getElementById('showNb').onchange = (e) => document.body.classList.toggle('hide-coords', !e.target.checked);

    document.getElementById('imgInp').onchange = (e) => {
        const file = e.target.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = (ev) => {
            const img = new Image();
            img.onload = () => {
                imgNaturalWidth = img.naturalWidth;
                page.style.backgroundImage = `url(${ev.target.result})`;
                iX = 0; iY = 0; iZoom = 100;
                document.getElementById('imgZoomRange').value = 100;
                updateImgStyle();
            };
            img.src = ev.target.result;
        };
        reader.readAsDataURL(file);
    };

    updatePageSize();
</script>
</body>
</html>
Voilà , j'espère avoir pu enfin apporter une petite contribution à ce donjon déjà très très bien garni :) bon jeu à toutes et tous
:wizard: Si ça sent le vieux, c'est pas nous, c'est les bouquins de D&D :wizard:
Avatar de l’utilisateur
szass
Staff - Façonneur de Donjons
Messages : 12861
Inscription : Jeu 29 Mars 2012 15:28
Localisation : Laboratoire du Donjon
Version de D&D préférée : AD&D2
Univers de D&D préféré : Planescape
Race : Githyanki
Classe : Illusionniste
Alignement : Chaotique Neutre
Dieu : Vlaakith CLVII
Mini Feuille de perso :
Feuilles de personnage ► Afficher le texte

Re: Des grilles pour tout

Message par szass »

Excellent :+1:
C'est le genre d'application très pratique (et bien foutu qui plus est) qui aurait toute sa place dans les applications du DDD si tu es d'accord. :)
Lolth tlu malla. Jal ultrinnan zhah xundus.
Avatar de l’utilisateur
MonsieurLOlo
Dragon d'argent
Messages : 16
Inscription : Mer 5 Avr 2023 14:51
Localisation : Marseille
Version de D&D préférée : AD&D2
Univers de D&D préféré : Ravenloft
Race : Elfe noir
Classe : Assassin
Alignement : Loyal Mauvais
Dieu : Lloth

Re: Des grilles pour tout

Message par MonsieurLOlo »

je partage ça, j'avais fais un post FB mais on sait jamais ! C'est une pépite

https://www.beta.arkanatools.com/papermap/
Avatar de l’utilisateur
ZikDragon
Dragon d'argent
Messages : 81
Inscription : Lun 16 Sep 2024 08:19
Localisation :
Version de D&D préférée : ?
Univers de D&D préféré : ?
Race : ?
Classe : ?
Alignement : ?
Dieu :

Re: Des grilles pour tout

Message par ZikDragon »

szass a écrit :
Dim 24 Mai 2026 19:36
Excellent :+1:
C'est le genre d'application très pratique (et bien foutu qui plus est) qui aurait toute sa place dans les applications du DDD si tu es d'accord. :)
Bien sur que je suis d'accord, je ne savais pas trop où la placer, mais je tenais à ce qu'elle profite à qui en veut :)
:wizard: Si ça sent le vieux, c'est pas nous, c'est les bouquins de D&D :wizard:
Avatar de l’utilisateur
szass
Staff - Façonneur de Donjons
Messages : 12861
Inscription : Jeu 29 Mars 2012 15:28
Localisation : Laboratoire du Donjon
Version de D&D préférée : AD&D2
Univers de D&D préféré : Planescape
Race : Githyanki
Classe : Illusionniste
Alignement : Chaotique Neutre
Dieu : Vlaakith CLVII
Mini Feuille de perso :
Feuilles de personnage ► Afficher le texte

Re: Des grilles pour tout

Message par szass »

Top, merci ZikDragon ;)
Je vais ajouter ça bientôt.

@MonsieurLOlo : je trouve que l'interface du générateur que tu indiques est un peu confuse et mal branlée. :oops:
Mais je n'ai peut-être pas tout compris.
- pas de pleine page (scrollbar et zoomwheel combinées ne font pas bon ménage :? )
- on ne peut pas déplacer l'image
- quand on change de format (A4, A3...) la zone n'est pas agrandie en conséquence
- on ne voit pas vraiment l'effet des grilles avant l'impression (juste au survol un petit bout).
- quand on clique sur la zone, cela ouvre le modal pour imprimer le pdf :aïe:
- je n'arrive pas à faire un truc propre lors de la génération du pdf

Comme je le disais, je n'ai peut-être pas tout saisi. :lol:
Mais selon moi une interface UI/UX bien faite doit être simple à prendre en main sans tutoriel, surtout quand il s'agit d'un petit programme avec peu de fonctionnalités.
Lolth tlu malla. Jal ultrinnan zhah xundus.
Avatar de l’utilisateur
szass
Staff - Façonneur de Donjons
Messages : 12861
Inscription : Jeu 29 Mars 2012 15:28
Localisation : Laboratoire du Donjon
Version de D&D préférée : AD&D2
Univers de D&D préféré : Planescape
Race : Githyanki
Classe : Illusionniste
Alignement : Chaotique Neutre
Dieu : Vlaakith CLVII
Mini Feuille de perso :
Feuilles de personnage ► Afficher le texte

Re: Des grilles pour tout

Message par szass »

Lolth tlu malla. Jal ultrinnan zhah xundus.
Avatar de l’utilisateur
ZikDragon
Dragon d'argent
Messages : 81
Inscription : Lun 16 Sep 2024 08:19
Localisation :
Version de D&D préférée : ?
Univers de D&D préféré : ?
Race : ?
Classe : ?
Alignement : ?
Dieu :

Re: Des grilles pour tout

Message par ZikDragon »

szass a écrit :
Lun 25 Mai 2026 07:20
C'est ajouté :
https://www.donjondudragon.fr/scripts/applications.html

:+1:
:applause: trop content d'avoir pu faire quelque chose pour le donjon :)
:wizard: Si ça sent le vieux, c'est pas nous, c'est les bouquins de D&D :wizard:
Avatar de l’utilisateur
MonsieurLOlo
Dragon d'argent
Messages : 16
Inscription : Mer 5 Avr 2023 14:51
Localisation : Marseille
Version de D&D préférée : AD&D2
Univers de D&D préféré : Ravenloft
Race : Elfe noir
Classe : Assassin
Alignement : Loyal Mauvais
Dieu : Lloth

Re: Des grilles pour tout

Message par MonsieurLOlo »

szass a écrit :
Lun 25 Mai 2026 06:58
Top, merci ZikDragon ;)
Je vais ajouter ça bientôt.

@MonsieurLOlo : je trouve que l'interface du générateur que tu indiques est un peu confuse et mal branlée. :oops:
Mais je n'ai peut-être pas tout compris.
- pas de pleine page (scrollbar et zoomwheel combinées ne font pas bon ménage :? )
- on ne peut pas déplacer l'image
- quand on change de format (A4, A3...) la zone n'est pas agrandie en conséquence
- on ne voit pas vraiment l'effet des grilles avant l'impression (juste au survol un petit bout).
- quand on clique sur la zone, cela ouvre le modal pour imprimer le pdf :aïe:
- je n'arrive pas à faire un truc propre lors de la génération du pdf

Comme je le disais, je n'ai peut-être pas tout saisi. :lol:
Mais selon moi une interface UI/UX bien faite doit être simple à prendre en main sans tutoriel, surtout quand il s'agit d'un petit programme avec peu de fonctionnalités.
Alors la video du tuto est explicite pourtant, une fois que tu choisit la taille de ton quadrillage, le soft fait tout tout seul, il découpe meme les salles du donjons, c'est vraiment orienté impression par contre il me semble
Avatar de l’utilisateur
szass
Staff - Façonneur de Donjons
Messages : 12861
Inscription : Jeu 29 Mars 2012 15:28
Localisation : Laboratoire du Donjon
Version de D&D préférée : AD&D2
Univers de D&D préféré : Planescape
Race : Githyanki
Classe : Illusionniste
Alignement : Chaotique Neutre
Dieu : Vlaakith CLVII
Mini Feuille de perso :
Feuilles de personnage ► Afficher le texte

Re: Des grilles pour tout

Message par szass »

Je ne vois pas de tuto sur la page :saispas:
Lolth tlu malla. Jal ultrinnan zhah xundus.
Avatar de l’utilisateur
MonsieurLOlo
Dragon d'argent
Messages : 16
Inscription : Mer 5 Avr 2023 14:51
Localisation : Marseille
Version de D&D préférée : AD&D2
Univers de D&D préféré : Ravenloft
Race : Elfe noir
Classe : Assassin
Alignement : Loyal Mauvais
Dieu : Lloth

Re: Des grilles pour tout

Message par MonsieurLOlo »

szass a écrit :
Lun 25 Mai 2026 12:05
Je ne vois pas de tuto sur la page :saispas:
Autan pour moi c'est sur son post FB

https://www.facebook.com/groups/3926785 ... 345377368/
Répondre

Revenir vers « Tables et générateurs »