GabGTool/scripttypo.js
2026-06-12 15:05:17 +02:00

675 lines
14 KiB
JavaScript

paper.install(window);
const debug = true;
// 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{
// ça c'est bizard, ça ne devrait pas fonctionner
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);
});