svg-logo.js

/**
 * 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 );