675 lines
14 KiB
JavaScript
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);
|
|
});
|
|
|