paper.install(window); const debug = false; // canvas const canvas = document.getElementById("canvas-typo"); const ctx = canvas.getContext("2d"); canvas.width = window.innerWidth - 280; canvas.height = window.innerHeight; // pour exporter en png function exporterPNG() { redrawAll(); const link = document.createElement("a"); link.download = "dessin-typographique.png"; link.href = canvas.toDataURL("image/png"); link.click(); } /* bouton export (sécurisé) */ const exportBtn = document.getElementById("export"); if (exportBtn) { exportBtn.onclick = exporterPNG; } // img // const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const alphabet = "abcdefghijklmnopqrstuvwxyz"; const letterImages = {}; let imagesReady = false; let loadedImages = 0; alphabet.split("").forEach(letter => { const img = new Image(); img.onload = () => { loadedImages++; if (loadedImages === alphabet.length) { imagesReady = true; } }; img.src = `images/${letter.toLowerCase()}.svg`; letterImages[letter] = img; }); /* ================= STATE ================= */ let drawing = false; let currentPath = []; let drawings = []; let scheme = ""; let lettersMap = {}; let sequence = []; const lettersData = {}; async function loadLetter(letter) { const res = await fetch(`./letters/${letter}.json`); if (!res.ok) throw new Error(`Erreur chargement ${letter}: ${res.status}`); return await res.json(); } window.onload = async function() { paper.setup(canvas); // Charger toutes les lettres for (let char of alphabet) { lettersData[char] = await loadLetter(char); } generateSchema(); } // schémas function generateMiniScheme(length = 4) { const symbols = ["A"]; let result = ["A"]; for (let i = 1; i < length; i++) { let choices = [...symbols]; if (symbols.length < 10) { choices.push(String.fromCharCode(65 + symbols.length)); } const next = choices[Math.floor(Math.random() * choices.length)]; result.push(next); if (!symbols.includes(next)) { symbols.push(next); } } return result.join(""); } function generateFakeSentence() { const wordsCount = 3 + Math.floor(Math.random() * 4); let words = []; for (let i = 0; i < wordsCount; i++) { const len = 3 + Math.floor(Math.random() * 4); words.push(generateMiniScheme(len)); } return words.join(" "); } // lettres function generateLettersForScheme(text) { lettersMap = {}; const used = []; const words = text.split(" "); words.forEach(word => { const local = {}; const unique = [...new Set(word)]; unique.forEach(symbol => { let letter; do { letter = alphabet[Math.floor(Math.random() * alphabet.length)]; } while (used.includes(letter)); used.push(letter); local[symbol] = letter; }); Object.keys(local).forEach(symbol => { lettersMap[word + "_" + symbol] = local[symbol]; }); }); } // séquences function buildSequence() { sequence = []; const words = scheme.split(" "); words.forEach((word, wi) => { for (let char of word) { sequence.push(lettersMap[word + "_" + char]); } if (wi < words.length - 1) { sequence.push(" "); } }); } // affichage séquence + schéma function drawHeaderText() { if (!scheme || !sequence.length) return; ctx.save(); const text1 = "Schéma : " + scheme; const text2 = "Séquence : " + sequence.join(""); ctx.font = "11px Arial"; const w = Math.max( ctx.measureText(text1).width, ctx.measureText(text2).width ); const x = 10; const y = canvas.height - 10; ctx.fillStyle = "rgba(255,255,255,0.75)"; ctx.fillRect(x - 8, y - 36, w + 16, 36); ctx.fillStyle = "#000"; ctx.fillText(text1, x, y - 18); ctx.fillText(text2, x, y); ctx.restore(); } // lettres // Choisir le contour d'entrée : celui qui commence le plus à gauche // function getEntryPoint(contours) { // let entryPt = null; // let minX = Infinity; // contours.forEach(contour => { // if (!contour.points || contour.points.length === 0) return; // const firstPt = contour.points[0]; // if (firstPt.x < minX) { // minX = firstPt.x; // entryPt = new Point(firstPt.x, -firstPt.y); // } // }); // return entryPt; // } // Choisir le contour de sortie : celui qui termine le plus à droite // function getExitPoint(contours) { // console.log('getExitPoint', contours); // let exitPt = null; // console.log('Infinity', Infinity); // let maxX = -Infinity; // contours.forEach(contour => { // console.log('contour', contour); // if (!contour.points || contour.points.length === 0) return; // const lastPt = contour.points[contour.points.length - 1]; // if (lastPt.x > maxX) { // maxX = lastPt.x; // exitPt = new Point(lastPt.x, -lastPt.y); // } // }); // return exitPt; // } // Dessine la lettre et retourne les points d'entrée et de sortie optimisés function drawGlyph(data) { console.log('drawGlyph: data', data); // on chope la clef du layer 1 const layerKey = Object.keys(data.layers)[0]; const contours = data.layers[layerKey].glyph.path.contours; const group = new Group(); let pt; contours.forEach(contour => { const pts = contour.points; if (!pts || pts.length === 0) return; const path = new Path(); path.strokeColor = 'black'; let offCurves = []; let offset = new Point(pts[0].x, -pts[0].y); // path.moveTo(new Point(pts[0].x, -pts[0].y)); path.moveTo(new Point(0,0)); for (let i = 1; i < pts.length; i++) { const p = pts[i]; pt = new Point(p.x, -p.y).subtract(offset); if (p.type === "cubic") { offCurves.push(p); } else if (offCurves.length === 2) { path.cubicCurveTo( new Point(offCurves[0].x, -offCurves[0].y).subtract(offset), new Point(offCurves[1].x, -offCurves[1].y).subtract(offset), pt ); offCurves = []; } else if (offCurves.length === 1) { path.quadraticCurveTo( new Point(offCurves[0].x, -offCurves[0].y).subtract(offset), pt ); offCurves = []; } // else { // path.lineTo(pt); // } } path.closed = contour.isClosed; group.addChild(path); }); let scale = 0.05; lastPt = new Point(pt.x*scale, pt.y*scale); group.pivot = new Point(0,0); group.scale(scale); if (debug) { var repere_entree = new Path.Circle(new Point(0, 0), 5); repere_entree.strokeColor = 'red'; group.addChild(repere_entree); var repere_sortie = new Path.Circle(new Point(pt.x*scale, pt.y*scale), 7); repere_sortie.strokeColor = 'green'; group.addChild(repere_sortie); } return { group, lastPt }; } // function rotatePoint(point, angleDegrees) { // const angleRadians = angleDegrees * (Math.PI / 180); // Convertir en radians // const cos = Math.cos(angleRadians); // const sin = Math.sin(angleRadians); // const newX = point.x * cos - point.y * sin; // const newY = point.x * sin + point.y * cos; // return { x: newX, y: newY }; // } function getDist(p1, p2){ const dx = p2.x - p1.x; const dy = p2.y - p1.y; return Math.hypot(dx, dy); } function getAngle(p1, p2){ const dx = p2.x - p1.x; const dy = p2.y - p1.y; return Math.atan2(dy, dx); } function drawLetters(draw) { console.log("drawLetters: draw", draw); if (!imagesReady) return; const path = draw.path; const sequence = draw.sequence; console.log("path:",path,"sequence:",sequence); let letterIndex = 0; // let path_drew_len = 0; let path_index = 0; let previousLastPoint; let n = 0; let addLetters = true; // while(path_drew_len < path.length){ // while(path_index < path.length){ while(addLetters){ // while(n < 4){ // n++; let p1 = previousLastPoint ? previousLastPoint : path[0]; pos = new Point(p1); if (letterIndex >= sequence.length) { letterIndex = 0; } const letter = sequence[letterIndex]; console.log('sequence.length', sequence.length, 'letterIndex: ', letterIndex, "letter: ", letter); if (letter === " ") { // TODO "draw" a space }else{ let { group, lastPt } = drawGlyph(lettersData[letter]); group.position = pos; // get the distance btwn first and last point of the letter let letterDist = getDist(new Point(0,0), lastPt); // get the path point with closest point distance with first path point for (let i = path_index+1; i < path.length; i++) { let pathDist = getDist(pos, path[i]); if (pathDist > letterDist) { p2 = path[i]; if(debug){ let repere_target_point = new Path.Circle(new Point(p2), 5); repere_target_point.strokeColor = 'pink'; } // angle let pathAngle = getAngle(p1, p2); let letterPointsAngle = getAngle(new Point(0,0), lastPt); let angle = pathAngle-letterPointsAngle; // ctx.rotate(angle); group.rotate(angle * (180/Math.PI)); previousLastPoint = pos.add(lastPt.rotate(angle * (180/Math.PI))); path_index = i < path.length -1 ? i-1 : path.length ; addLetters = i < path.length -1; break; }else{ addLetters = false; } } } // path_index++; letterIndex++; // path_drew_len++; } } function drawLetters_old(draw) { console.log("drawLetters: draw", draw); if (!imagesReady) return; const path = draw.path; const sequence = draw.sequence; let letterIndex = 0; let remaining = 0; let previousRightPt; for (let i = 0; i < path.length - 1; i++) { // const p1 = path[i]; const p2 = path[i + 1]; const dx = p2.x - p1.x; const dy = p2.y - p1.y; const len = Math.hypot(dx, dy); let t = remaining / len; while (t <= 1) { const c = sequence[letterIndex]; console.log('letterIndex', letterIndex); if (c !== " ") { // TODO replace by own svg // const img = letterImages[c]; console.log('c', c, 'lettersData', lettersData); let { group, lastPt } = drawGlyph(lettersData[c]); console.log('group', group, 'lastPt', lastPt); // // Décaler la lettre pour que son point d'entrée coïncide avec la sortie précédente // Créer une ligature fluide // createLigature(previousRightPt, entryPt.add(offset)); let pos; if (previousRightPt) { // FOR ALL POINTS EXCEPT FOR THE FIRST // hopping to follow the line more or less pos = previousRightPt; }else{ // ONLY FOR THE FIRST POINT // TODO adatape x and y to connect letters const x = p1.x + dx * t; const y = p1.y + dy * t; pos = new Point(x,y); // ctx.translate(x, y); } group.position = pos; // angle sould be ok const angle = Math.atan2(dy, dx); // ctx.rotate(angle); group.rotate(angle * (180/Math.PI)); previousRightPt = pos.add(lastPt.rotate(angle * (180/Math.PI))); if (debug) { var repere_pos_group = new Path.Circle(pos, 2); repere_pos_group.strokeColor = 'blue'; var repere_prevrightpoint = new Path.Circle(previousRightPt, 2); repere_prevrightpoint.strokeColor = 'orange'; } t += (32 * 0.85) / len; } else { t += (32 * 1.8) / len; } letterIndex = (letterIndex + 1) % sequence.length; } remaining = (t - 1) * len; } } function drawPath(draw){ const path = draw.path; for (let i = 0; i < path.length - 1; i++) { let repere_path = new Path.Circle(new Point(path[i]), 2); repere_path.strokeColor = 'blue'; } } // l'ensenble/rendu global apres maj function redrawAll() { // ctx.clearRect(0, 0, canvas.width, canvas.height); paper.project.clear(); drawings.forEach(draw => { if (debug) { drawPath(draw); } drawLetters(draw); }); if (drawing && currentPath.length > 1) { ctx.strokeStyle = "#aaa"; ctx.lineWidth = 2; ctx.beginPath(); currentPath.forEach((p, i) => { if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y); }); ctx.stroke(); } drawHeaderText(); } // souris canvas.addEventListener("mousedown", e => { if (!sequence.length) { alert("Génère un schéma !"); return; } drawing = true; currentPath = [{ x: e.offsetX, y: e.offsetY }]; }); canvas.addEventListener("mousemove", e => { if (!drawing) return; currentPath.push({ x: e.offsetX, y: e.offsetY }); redrawAll(); }); canvas.addEventListener("mouseup", () => { drawing = false; if (currentPath.length > 1) { drawings.push({ path: currentPath, sequence: [...sequence], scheme }); } currentPath = []; redrawAll(); }); // formes function generateShape(type) { if (!sequence.length) return; const newDraw = { path: [], sequence: [...sequence], scheme }; const cx = canvas.width / 2; const cy = canvas.height / 2; if (type === "circle") { const r = 150; for (let a = 0; a < 360; a += 5) { newDraw.path.push({ x: cx + Math.cos(a * Math.PI / 180) * r, y: cy + Math.sin(a * Math.PI / 180) * r }); } } if (type === "spiral") { let r = 20; let a = 0; for (let i = 0; i < 300; i++) { newDraw.path.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r }); a += 0.2; r += 0.5; } } drawings.push(newDraw); redrawAll(); } // interactions : boutons etc document.getElementById("draw-scheme").addEventListener('click', generateSchema); function generateSchema(){ scheme = generateFakeSentence(); generateLettersForScheme(scheme); buildSequence(); redrawAll(); } // efface document.getElementById("clear").onclick = () => { drawings = []; currentPath = []; scheme = ""; sequence = []; lettersMap = {}; ctx.clearRect(0, 0, canvas.width, canvas.height); }; // selection forme document.getElementById("shape").addEventListener("change", e => { if (e.target.value !== "free") generateShape(e.target.value); });