/**
* représentation d'un segment : points, épaisseur, couleur
*/
class Segment {
/*
* Représente un segment.
* @param {object} conteneur_SVG - conteneur SVG
* @param {number[]} points - les 2 points du segment
* @param {string=} [couleur=black] - couleur, couleur nommée, hexadécimal, notation fonctionnelle rgb/rgba/hsl/hsla
* @param {number=} [epaisseur=1] - épaisseur du segment
*/
constructor ( conteneur_SVG, points = [], couleur = 'black', epaisseur = 1 ) {
// message d'erreur si le contexte SVG n'est pas défini
if ( conteneur_SVG === undefined ) {
console.error( 'Le contexte SVG du segment n\'est pas défini' );
}
// et si les points ne sont pas définis
if ( points === [] ) {
console.error( 'Les points du segment ne sont pas définis' );
}
// configuration de l'objet
this.conteneur = conteneur_SVG;
this.points = points;
this.element = null; // à ce stade on a pas dessiné l'élément
this.couleur = couleur;
this.epaisseur = epaisseur;
}
/**
* Vérifie si le segment peut prendre la suite de celui donné en argument
* si celui-ci a les mêmes propriétés (épaisseur, couleur, positions des points)
* @param {Segment} segment - segment à tester
* @returns {boolean} résultat du test de continuité
*/
suit ( segment ) {
// on teste déjà les paramètres couleur et épaisseur,
// si c'est pas bon on renvoie false
if ( this.couleur !== segment.couleur ||
this.epaisseur !== segment.epaisseur ) {
return false;
}
// on compare ensuite
// premier point du segment courant
let premier_point = JSON.stringify( this.points[0] );
// dernier point du segment passé en paramètre
let dernier_point = JSON.stringify( segment.points[ segment.points.length - 1 ] );
// si ces points et les paramètres sont compatibles
if ( dernier_point === premier_point ) {
// on retourne true
return true;
}
// sinon false
return false;
}
/**
* Concatène la liste de points du segment donné
* à la suite de ceux du segment courant et redessine celui-ci.
* Renvoie un booléen pour signifier la réussite de l'opération
* @param {Segment} segment - segment à fusionner
* @returns {boolean} réussite ou échec de la fusion
*/
fusionner_avec ( segment ) {
// on vérifie la compatibilité des points
//console.log( this, segment, segment.suit( this ) )
if ( !segment.suit( this ) ) {
//console.info( 'les segment ne sont pas compatibles');
return false;
}
// on récupère les points du segment donné
// dès le second point (index 1) et donc sans le premier (index 0)
this.points = this.points.concat( segment.points.slice(1) );
this.dessiner();
return true;
}
/**
* Dessine le segment.
*/
dessiner () {
// si une version précédente existe on la supprime
if ( this.element != null ) { this.effacer(); }
// on retrace le dessin
this.element = this.conteneur
.polyline( this.points )
.attr({
stroke: this.couleur,
'stroke-width': this.epaisseur,
fill: 'none',
});
}
/**
* Efface l'élément svg associé au segment.
*/
effacer () {
this.element.remove();
}
}
/**
* Représentation un traceur (tortue), lequel sera placé dans un conteneur SVG et figuré par un curseur.
*/
class Tortue {
/**
* créé et initialise une tortue
* @param {object} conteneur_SVG - conteneur SVG
* @property {number} epaisseur - épaisseur du tracé
* @property {string} couleur - couleur du tracé
* @example
* // création d'un contenu SVG
* const conteneur_SVG = creer_conteneur_dans( document.body );
* // création d'une tortue dans le conteneur
* const tortue = new Tortue( conteneur_SVG );
* // réglage de l'épaisseur du tracé
* tortue.epaisseur = 2;
* // réglage de la couleur du tracé
* tortue.couleur = 'rgb(0,255,0)';
* // rotation de 10° dans le sens horaire
* tortue.tourner( 10 )
* // déplacement de la tortue de 10 unités dans le sens où orientée
* tortue.avancer( 10 )
*/
constructor ( conteneur_SVG, couleur = 'black', epaisseur = 1, etat = true, angle = 0 ) {
// on récupère le contexte etc.
this.conteneur = conteneur_SVG;
this.config = {};
this.config.position = {
x: Math.round( innerWidth / 2 ),
y: Math.round( innerHeight / 2 ),
};
this.config.etat = etat;
this.config.angle = angle;
this.epaisseur = epaisseur;
this.couleur = couleur;
this.segment_courant = null;
this.curseur = new Curseur( this.conteneur, this.config.position.x, this.config.position.y );
this.maj_config();
}
/**
* Permet la mise à jour des variables accessibles en lecture.
*/
maj_config () {
this.x = this.config.position.x;
this.y = this.config.position.y;
this.angle = this.config.angle;
}
/**
* Lève le crayon : sortie du mode dessin.
*/
lever_crayon () {
this.config.etat = false;
}
/**
* Baisse le crayon : entrée en mode dessin.
*/
baisser_crayon () {
this.config.etat = true;
}
/**
* Permet de déplacer la tortue dans la direction où elle est orientée.
* @param {number} [distance=0] - longueur de laquelle on va avancer
*/
avancer ( distance = 0 ) {
// on teste la distance en entrée
if ( typeof distance !== 'number' || distance === 0 ){
console.error( 'avancer(n) prend un nombre > 0 en paramètre !');
return;
}
// on créé un objet correspondant à la nouvelle position que l'on calcule
const x = distance * Math.sin( this.config.angle * Math.PI / 180 ) + this.config.position.x;
const y = -distance * Math.cos( this.config.angle * Math.PI / 180 ) + this.config.position.y;
// on se déplace
this.aller_en( x, y );
}
/**
* Permet d'appliquer une rotation horaire à la tortue.
* @param {number} [angle_relatif = 0] - angle en degré qui sera ajouté à l'orientation courant
* @example
* // tourner de 90° horaires
* tortue.tourner( 90 )
* @example
* // tourner de 90° antihoraires
* tortue.tourner( -90 )
*/
tourner ( angle_relatif = 0 ) {
this.config.angle += angle_relatif;
this.curseur.definir_angle( this.config.angle );
this.maj_config();
}
/**
* Permet d'orienter la tortue à un certain angle à partir de midi.
* @param {number} [angle_absolu] - détermine en degrés horaires le positionnement angulaire à appliquer
* @example
* // orientation à midi 🕛
* tortue.orienter_a( 180 )
* @example
* // orientation à 6 heures 🕒
* tortue.orienter_a( 180 )
* @example
* // orientation à 9 heures 🕘
* tortue.orienter_a( 270 )
*/
orienter_a ( angle_absolu ) {
this.config.angle = angle_absolu || this.config.angle;
this.curseur.definir_angle( this.config.angle );
this.maj_config();
}
/**
* Permet de déplacer le traceur à une position donnée.
* @param {number} x - position en x
* @param {number} y - position en y
*/
aller_en ( x, y ) {
// si le dessin est actif, on trace un segment
if ( this.config.etat ) {
//console.info( 'mode dessin actif' );
// pour cela on créé un segment entre le point de départ et d'arrivée
let points = [
[ this.config.position.x, this.config.position.y ],
[ x, y ]
];
let segment = new Segment( this.conteneur, points, this.couleur, this.epaisseur );
// si un segment courant existe on tente la fusion
let fusion_reussie = false;
if ( this.segment_courant !== null ) {
fusion_reussie = this.segment_courant.fusionner_avec( segment );
}
// si pas de segment courant ou pas compatible, on trace
if ( !fusion_reussie ) {
//console.log('!fusion_reussie')
segment.dessiner();
this.segment_courant = segment;
}
// sinon on se déplace simplement
// et on met le segment courant à null
} else {
this.segment = null;
}
// mise à jour de la position
this.config.position.x = x;
this.config.position.y = y;
// déplacement du curseur
this.curseur.definir_origine( x, y );
// mise à jour des données en lecture seule
this.maj_config();
}
/**
* Trace le segment donné, éventuellement dans la continuité du précédente.
* @param {Segment} nouveau_segment - nouveau segment à tracer
*/
tracer_segment ( nouveau_segment ) {
// si le segment suit le précédent
if ( nouveau_segment.suit( this.segment_courant ) ) {
// on ajoute le dernier point au segment précédent
this.segment_courant.points.push( nouveau_segment.points[ 1 ] );
// on supprime l'élement SVG précédent
this.segment_courant.element.remove();
// on redessine une polyline avec tous les points
this.segment_courant.element = this.conteneur.polyline( this.segment_courant.points );
// s'il ne suit pas le précédent
} else {
// le nouveau segment devient le segment courant
this.segment_courant = nouveau_segment;
// on créé un objet line dont on garde la mémoire dans notre segment
this.segment_courant.element = this.conteneur.line( nouveau_segment.points );
}
}
/**
* Permet de placer une ligne de texte
* @param {string=} texte - texte à placer
* @param {number=} [x=this.config.position.x] - position en x
* @param {number=} [y=this.config.position.y] - position en y
* @example
* tortue.ecrire( 'bonjour' );
*/
ecrire ( texte, x = this.config.position.x, y = this.config.position.y ) {
let node = this.conteneur.text( texte ).node.firstChild
toto = node
let largeur = node.scrollWidth;
let hauteur = node.scrollHeight;
console.log( hauteur );
node.setAttribute( 'x', x - largeur / 2 );
node.setAttribute( 'dy', y + hauteur / 3 );
}
}
let toto;
/**
* Représentation visuelle d'un emplacement
* et d'une direction dans un conteneur SVG
*/
class Curseur {
/*
* Créé un curseur et l'implante dans un conteneur SVG
* @param {object} conteneur_SVG - élément SVG conteneur
* @param {number} [x=0] - position en x du curseur au sein du conteneur
* @param {number} [y=0] - position en y du curseur au sein du conteneur
* @param {string} [id=curseur] - id du groupe contenant le curseur
* @param {string} [couleur_croix=red] - couleur de la croix de positionnement, couleur nommée, hexadécimal, notation fonctionnelle rgb/rgba/hsl/hsla
* @param {string} [couleur_fleche=black] - couleur de la flèche d'orientation, couleur nommée, hexadécimal, notation fonctionnelle rgb/rgba/hsl/hsla
* @param {number} [taille_croix=2] - taille de la croix (origine → extrémité)
* @param {number} [longueur_fleche=10] - longueur de la flèche
* @param {number} [taille_fleche=4] - taille des extrémités de la flèche
*/
constructor (
conteneur_SVG,
x = 0,
y = 0,
id = 'curseur',
couleur_croix = 'red',
couleur_fleche = 'black',
taille_croix = 2,
longueur_fleche = 10,
taille_fleche = 4,
) {
// création d'un groupe et assignation d'un id
this.groupe = conteneur_SVG.group();
this.groupe.attr('id','curseur');
// dessin de la croix
this.croix = this.groupe.group();
this.croix.line(-taille_croix, -taille_croix, taille_croix, taille_croix);
this.croix.line(-taille_croix, taille_croix, taille_croix, -taille_croix);
// dessin de la flèche
this.fleche = this.groupe.group();
this.fleche.line(0, 0, 0, -longueur_fleche);
this.fleche.line(taille_fleche, -1.5*taille_fleche, 0, -longueur_fleche);
this.fleche.line(-taille_fleche, -1.5*taille_fleche, 0, -longueur_fleche);
// assignation de couleurs aux éléments
this.couleur_croix( couleur_croix );
this.couleur_fleche( couleur_fleche );
// positionnement
this.definir_origine( x, y );
}
/**
* (re)définit l'origine du curseur dans le conteneur svg
* supporte les objets, tableaux, nombres en entrée
* @param {(object|number[]|number)} a - position en x ou tableau/objet avec position en x et y
* @param {number} a.x - nouvelle position en x du curseur au sein du conteneur
* @param {number} a.y - nouvelle position en y du curseur au sein du conteneur
* @param {number=} b - position en y
*/
definir_origine ( a, b ) {
/* on va assigner aux variables x et y
la valeur correspondante selon a et b */
let x, y;
// si a est un objet de forme {x:0,y:0}
if ( typeof a === 'object' ) {
x = a.x;
y = a.y;
// si a est un tableau [x,y]
} else if ( Array.isArray( a ) ) {
x = a[0];
y = a[1];
// sinon on suppose que a et b sont de type Number
} else {
x = a;
y = b;
}
// on récupère l'état du groupe
let transform = this.groupe.transform();
// on reporte nos nouvelles valeurs
transform.e = x;
transform.f = y;
// on applique la transformation mise à jour
this.groupe.transform( transform );
}
/**
* todo
* @param {string} couleur - couleur SVG de la flèche
* @example
* // couleur nommée
* curseur.couleur = 'red';
* @example
* // notation hexadécimale
* curseur.couleur = '#rrggbb'
* @example
* // notation fonctionnelle rgb
* curseur.couleur = 'rgb(255, 158, 44)';
* @example
* // notation fonctionnelle rgba
* curseur.couleur = 'rgba(255, 158, 44, 0.5)';
* @example
* // notation fonctionnelle hsl
* curseur.couleur = 'hsl(255, 158, 44)';
* @example
* // notation fonctionnelle hsla
* curseur.couleur = 'hsla(255, 158, 44, 0.5)';
*/
/**
* modifie l'orientation du curseur en degrés par rapport à midi
* @param {number} angle_absolu - détermine en degrés horaires le positionnement angulaire à appliquer
* @example
* // orientation à midi 🕛
* curseur.definir_angle( 180 )
* @example
* // orientation à 6 heures 🕒
* curseur.definir_angle( 180 )
* @example
* // orientation à 9 heures 🕘
* curseur.definir_angle( 270 )
*/
definir_angle ( angle_absolu ) {
// si pas de paramètre en entrée
if ( angle_absolu === undefined ) { return; }
// on créé une matrice de transformation correspondant à notre angle
let matrix = new SVG.Matrix().transform( { rotate: angle_absolu } );
// on récupère la matrice actuelle
let transform = this.fleche.transform();
// on la modifie avec la nouvelle
transform.a = matrix.a;
transform.b = matrix.b;
transform.c = matrix.c;
transform.d = matrix.d;
// on applique la matrice modifiée
this.fleche.transform( transform );
}
/**
* fait tourner le curseur dans le sens horaire (si paramètre > 0)
* @param {number} [angle_relatif=0] - détermine en degrés horaires la rotation relative à appliquer
* @example
* // rotation horaire de 90 degrés
* curseur.rotation( 90 )
* @example
* // rotation antihoraire de 90 degrés
* curseur.rotation( -90 )
*/
rotation ( angle_relatif = 0 ) {
this.fleche.rotate( angle_relatif, 0, 0 );
}
/**
* modifie la couleur de la flèche du curseur
* @param {string} couleur_fleche - couleur de la flèche
*/
couleur_fleche ( couleur_fleche ) {
if ( couleur_fleche !== undefined ) {
this.fleche.stroke( { color: couleur_fleche } );
}
}
/**
* modifie la couleur de la croix du curseur
* @param {string} couleur_fleche - couleur de la croix
*/
couleur_croix ( couleur_croix ) {
if ( couleur_croix !== undefined ) {
this.croix.stroke( { color: couleur_croix } );
}
}
/**
* affiche le curseur
*/
afficher ( couleur_croix ) {
this.groupe.attr( 'display', null );
}
/**
* cache le curseur
*/
cacher ( couleur_croix ) {
this.groupe.attr( 'display', 'none' );
}
}
/**
* Permet de créer un conteneur SVG dans l'élément HTML donné.
* Ce conteneur pourra accueillir un traceur (tortue)
* @param {Element} conteneur - élément HTML ou placer le conteneur SVG
* @example
* const conteneur_SVG = creer_conteneur_dans( document.body );
* const tortue = new Tortue( conteneur_SVG );
* @return {SVG.Svg}
*/
function creer_conteneur_dans ( conteneur ) {
let element_svg = SVG().addTo( conteneur ).size( '100%', '100%' );
//element_svg.style.position = 'absolute';
return element_svg;
}
const conteneur_SVG = creer_conteneur_dans( document.body );
const tortue = new Tortue( conteneur_SVG );