HTML Canvas + JavaScript : Fabriquez un curseur reptile qui bouge et réagit

Un tutoriel complet et détaillé pour créer un curseur interactif en forme de reptile avec HTML Canvas et JavaScript. Apprenez l'animation, la physique
Créer un Curseur Reptile Interactif avec JavaScript

Oubliez le curseur ennuyeux et sans âme qui hante nos écrans depuis des décennies. Et si, d'un coup de baguette magique, on pouvait le transformer en une créature vivante, organique, qui ondule et suit chacun de vos mouvements avec une grâce reptilienne ? Et si on pouvait insuffler de la vie et de la personnalité à l'élément le plus fondamental de l'interaction utilisateur ?

C'est exactement ce que nous allons faire aujourd'hui. Ce n'est pas un simple effet CSS, c'est une véritable simulation. Nous allons construire, pas à pas, un curseur reptile interactif en utilisant la puissance du <canvas> HTML et la flexibilité du JavaScript. Nous allons créer une créature segmentée qui "chasse" votre souris, dont le corps s'étire et se contracte de manière fluide, donnant l'illusion d'un être vivant explorant votre page. C'est un projet incroyablement amusant qui vous fera plonger dans les concepts d'animation, de physique simple et de programmation orientée objet.

Les outils du créateur : Pour cette aventure, nous n'avons besoin que de deux choses : un fichier HTML pour accueillir notre toile, et un fichier JavaScript pour y peindre la vie. C'est la preuve ultime que le navigateur est un terrain de jeu créatif sans limites.

Comprendre la Logique

Avant d'écrire la moindre ligne de code, il est crucial de comprendre la "magie" derrière le mouvement de notre créature. Comment un simple curseur peut-il se comporter comme un serpent ou un lézard ? La réponse se trouve dans la décomposition du problème en concepts simples.

  1. La Toile (Le Canvas) : Au lieu de manipuler des dizaines d'éléments div (ce qui serait un cauchemar pour les performances), nous allons utiliser une seule balise <canvas>. C'est une surface de dessin sur laquelle JavaScript peut peindre des formes, des lignes et des images à très haute vitesse.
  2. Les Blocs de construction (Les Segments) : Notre reptile n'est pas un objet unique. Il est composé d'une chaîne de petits "segments" (pensez aux vertèbres d'un serpent). Chaque segment est un objet en JavaScript qui a sa propre position (x, y).
  3. La Physique du "suiveur" : C'est le cœur de l'illusion. Le mouvement n'est pas calculé pour la créature entière, mais pour chaque segment individuellement. La règle est simple : chaque segment essaie de suivre celui qui le précède. Le premier segment (la "tête") suit la souris. Le deuxième segment suit la tête, le troisième suit le deuxième, et ainsi de suite. Ce comportement en chaîne crée l'ondulation naturelle.
  4. La boucle d'Animation: Pour créer l'illusion du mouvement, il ne suffit pas de calculer les positions une seule fois. Il faut le faire en continu, très rapidement. On utilisera requestAnimationFrame, une fonction JavaScript qui crée une boucle se synchronisant avec le taux de rafraîchissement de l'écran (généralement 60 fois par seconde). À chaque "image", on efface la toile, on recalcule la position de chaque segment, et on redessine la créature à sa nouvelle place.

Étape 1 : Mettre en place notre monde (HTML & CSS)

Commençons par la base. Notre HTML est d'une simplicité biblique. Il nous faut juste une page avec un <canvas>. Le CSS, quant à lui, s'assurera que notre toile remplisse tout l'écran et qu'il n'y ait pas de marges ou de barres de défilement disgracieuses.

Créez un fichier index.html :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reptile Interactive Cursor</title>
</head>
<body>
    <script  src="./script.js"></script>
</body>
</html>

Étape 2 : Le JavaScript

Le code JavaScript va créer une créature animée complexe qui suit le mouvement de la souris...Il simule un système corporel segmenté avec des articulations, des membres et un mouvement réaliste grâce à des calculs basés sur la physique et des contraintes d’angle. La créature aura des pattes, des tentacules et une queue, et sa structure est construite à l’aide des classes Segment, LimbSystem et Creature. Les mouvements sont contrôlés par l’entrée de la souris, et différentes configurations La configuration finale dessine une créature ressemblant à un lézard qui rampe vers le curseur.

Gestion des entrées : la classe Input

Cette section initialise un objet global Input pour gérer les événements du clavier et de la souris. Il maintient l'état de chaque touche et bouton de la souris (enfoncé ou relâché) et met à jour les coordonnées de la souris.


var Input = {
    keys: [],
    mouse: {
      left: false,
      right: false,
      middle: false,
      x: 0,
      y: 0
    }
  };
  for (var i = 0; i < 230; i++) {
    Input.keys.push(false);
  }
  document.addEventListener("keydown", function(event) {
    Input.keys[event.keyCode] = true;
  });
  document.addEventListener("keyup", function(event) {
    Input.keys[event.keyCode] = false;
  });
  document.addEventListener("mousedown", function(event) {
    if ((event.button = 0)) {
      Input.mouse.left = true;
    }
    if ((event.button = 1)) {
      Input.mouse.middle = true;
    }
    if ((event.button = 2)) {
      Input.mouse.right = true;
    }
  });
  document.addEventListener("mouseup", function(event) {
    if ((event.button = 0)) {
      Input.mouse.left = false;
    }
    if ((event.button = 1)) {
      Input.mouse.middle = false;
    }
    if ((event.button = 2)) {
      Input.mouse.right = false;
    }
  });
  document.addEventListener("mousemove", function(event) {
    Input.mouse.x = event.clientX;
    Input.mouse.y = event.clientY;
  });

Configuration du Canvas

Cette partie du code initialise le canvas HTML5 qui sera utilisé pour dessiner la créature. Il crée un élément canvas, l'ajoute au corps du document, ajuste sa taille pour qu'il corresponde à la fenêtre du navigateur et obtient le contexte de rendu 2D.


  //Sets up canvas
  var canvas = document.createElement("canvas");
  document.body.appendChild(canvas);
  canvas.width = Math.max(window.innerWidth, window.innerWidth);
  
  //canvas.height = Math.max(window.innerWidth, window.innerWidth);
  
  canvas.height = window.innerHeight;
  canvas.style.position = "absolute";
  canvas.style.left = "0px";
  canvas.style.top = "0px";
  document.body.style.overflow = "hidden";
  var ctx = canvas.getContext("2d");

La classe Segment

La classe Segment est la pierre angulaire de la structure de la créature. Chaque segment représente une partie du corps connectée à un parent et peut avoir des enfants. Elle gère sa taille, son angle relatif par rapport au parent, son angle absolu par rapport à l'axe X, sa plage de mouvement et sa rigidité. Les méthodes updateRelative, draw et follow gèrent la mise à jour de sa position, son rendu et son comportement de suivi.


  //Necessary classes
  var segmentCount = 0;
  class Segment {
    constructor(parent, size, angle, range, stiffness) {
      segmentCount++;
      this.isSegment = true;
      this.parent = parent; //Segment which this one is connected to
      if (typeof parent.children == "object") {
        parent.children.push(this);
      }
      this.children = []; //Segments connected to this segment
      this.size = size; //Distance from parent
      this.relAngle = angle; //Angle relative to parent
      this.defAngle = angle; //Default angle relative to parent
      this.absAngle = parent.absAngle + angle; //Angle relative to x-axis
      this.range = range; //Difference between maximum and minimum angles
      this.stiffness = stiffness; //How closely it conforms to default angle
      this.updateRelative(false, true);
    }
    updateRelative(iter, flex) {
      this.relAngle =
        this.relAngle -
        2 *
          Math.PI *
          Math.floor((this.relAngle - this.defAngle) / 2 / Math.PI + 1 / 2);
      if (flex) {
        //    this.relAngle=this.range/
        //        (1+Math.exp(-4*(this.relAngle-this.defAngle)/
        //        (this.stiffness*this.range)))
        //        -this.range/2+this.defAngle;
        this.relAngle = Math.min(
          this.defAngle + this.range / 2,
          Math.max(
            this.defAngle - this.range / 2,
            (this.relAngle - this.defAngle) / this.stiffness + this.defAngle
          )
        );
      }
      this.absAngle = this.parent.absAngle + this.relAngle;
      this.x = this.parent.x + Math.cos(this.absAngle) * this.size; //Position
      this.y = this.parent.y + Math.sin(this.absAngle) * this.size; //Position
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].updateRelative(iter, flex);
        }
      }
    }
    draw(iter) {
      ctx.beginPath();
      ctx.moveTo(this.parent.x, this.parent.y);
      ctx.lineTo(this.x, this.y);
      ctx.stroke();
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].draw(true);
        }
      }
    }
    follow(iter) {
      var x = this.parent.x;
      var y = this.parent.y;
      var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
      this.x = x + this.size * (this.x - x) / dist;
      this.y = y + this.size * (this.y - y) / dist;
      this.absAngle = Math.atan2(this.y - y, this.x - x);
      this.relAngle = this.absAngle - this.parent.absAngle;
      this.updateRelative(false, true);
      //this.draw();
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].follow(true);
        }
      }
    }
  }

La classe LimbSystem

La classe LimbSystem représente un système de membres, comme un tentacule ou une patte. Elle est composée d'une série de segments qui se terminent par un segment "end". Elle permet de contrôler le mouvement de ce membre vers une position cible, en ajustant les angles des segments pour atteindre la cible.


  class LimbSystem {
    constructor(end, length, speed, creature) {
      this.end = end;
      this.length = Math.max(1, length);
      this.creature = creature;
      this.speed = speed;
      creature.systems.push(this);
      this.nodes = [];
      var node = end;
      for (var i = 0; i < length; i++) {
        this.nodes.unshift(node);
        //node.stiffness=1;
        node = node.parent;
        if (!node.isSegment) {
          this.length = i + 1;
          break;
        }
      }
      this.hip = this.nodes.parent;
    }
    moveTo(x, y) {
      this.nodes.updateRelative(true, true);
      var dist = ((x - this.end.x) ** 2 + (y - this.end.y) ** 2) ** 0.5;
      var len = Math.max(0, dist - this.speed);
      for (var i = this.nodes.length - 1; i >= 0; i--) {
        var node = this.nodes[i];
        var ang = Math.atan2(node.y - y, node.x - x);
        node.x = x + len * Math.cos(ang);
        node.y = y + len * Math.sin(ang);
        x = node.x;
        y = node.y;
        len = node.size;
      }
      for (var i = 0; i < this.nodes.length; i++) {
        var node = this.nodes[i];
        node.absAngle = Math.atan2(
          node.y - node.parent.y,
          node.x - node.parent.x
        );
        node.relAngle = node.absAngle - node.parent.absAngle;
        for (var ii = 0; ii < node.children.length; ii++) {
          var childNode = node.children[ii];
          if (!this.nodes.includes(childNode)) {
            childNode.updateRelative(true, false);
          }
        }
      }
      //this.nodes.updateRelative(true,false)
    }
    update() {
      this.moveTo(Input.mouse.x, Input.mouse.y);
    }
  }

La classe LegSystem

La classe LegSystem étend LimbSystem et ajoute des fonctionnalités spécifiques aux pattes, comme la marche. Elle définit un point de destination (goalX, goalY) que la patte essaie d'atteindre. Elle implémente une logique pour que la patte se déplace en plusieurs étapes : attendre, avancer vers une nouvelle position, puis revenir à une position de repos.


  class LegSystem extends LimbSystem {
    constructor(end, length, speed, creature) {
      super(end, length, speed, creature);
      this.goalX = end.x;
      this.goalY = end.y;
      this.step = 0; //0 stand still, 1 move forward,2 move towards foothold
      this.forwardness = 0;
  
      //For foot goal placement
      this.reach =
        0.9 *
        ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** 0.5;
      var relAngle =
        this.creature.absAngle -
        Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x);
      relAngle -= 2 * Math.PI * Math.floor(relAngle / 2 / Math.PI + 1 / 2);
      this.swing = -relAngle + (2 * (relAngle < 0) - 1) * Math.PI / 2;
      this.swingOffset = this.creature.absAngle - this.hip.absAngle;
      //this.swing*=(2*(relAngle>0)-1);
    }
    update(x, y) {
      this.moveTo(this.goalX, this.goalY);
      //this.nodes.follow(true,true)
      if (this.step == 0) {
        var dist =
          ((this.end.x - this.goalX) ** 2 + (this.end.y - this.goalY) ** 2) **
          0.5;
        if (dist > 1) {
          this.step = 1;
          //this.goalX=x;
          //this.goalY=y;
          this.goalX =
            this.hip.x +
            this.reach *
              Math.cos(this.swing + this.hip.absAngle + this.swingOffset) +
            (2 * Math.random() - 1) * this.reach / 2;
          this.goalY =
            this.hip.y +
            this.reach *
              Math.sin(this.swing + this.hip.absAngle + this.swingOffset) +
            (2 * Math.random() - 1) * this.reach / 2;
        }
      } else if (this.step == 1) {
        var theta =
          Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x) -
          this.hip.absAngle;
        var dist =
          ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) **
          0.5;
        var forwardness2 = dist * Math.cos(theta);
        var dF = this.forwardness - forwardness2;
        this.forwardness = forwardness2;
        if (dF * dF < 1) {
          this.step = 0;
          this.goalX = this.hip.x + (this.end.x - this.hip.x);
          this.goalY = this.hip.y + (this.end.y - this.hip.y);
        }
      }
      //  ctx.strokeStyle='blue';
      //  ctx.beginPath();
      //  ctx.moveTo(this.end.x,this.end.y);
      //  ctx.lineTo(this.hip.x+this.reach*Math.cos(this.swing+this.hip.absAngle+this.swingOffset),
      //        this.hip.y+this.reach*Math.sin(this.swing+this.hip.absAngle+this.swingOffset));
      //  ctx.stroke();
      //  ctx.strokeStyle='black';
    }
  }

La classe Creature

La classe Creature représente la créature principale. Elle possède une position, un angle, une vitesse de déplacement et de rotation, ainsi que des paramètres pour contrôler son accélération, son frottement et sa résistance. Elle contient une collection de segments enfants et de systèmes de membres (LimbSystem ou LegSystem). La méthode follow gère le mouvement de la créature vers une cible (généralement la souris), en calculant les forces et les vitesses. La méthode draw s'occupe de dessiner la créature sur le canvas.


  class Creature {
    constructor(
      x,
      y,
      angle,
      fAccel,
      fFric,
      fRes,
      fThresh,
      rAccel,
      rFric,
      rRes,
      rThresh
    ) {
      this.x = x; //Starting position
      this.y = y;
      this.absAngle = angle; //Staring angle
      this.fSpeed = 0; //Forward speed
      this.fAccel = fAccel; //Force when moving forward
      this.fFric = fFric; //Friction against forward motion
      this.fRes = fRes; //Resistance to motion
      this.fThresh = fThresh; //minimum distance to target to keep moving forward
      this.rSpeed = 0; //Rotational speed
      this.rAccel = rAccel; //Force when rotating
      this.rFric = rFric; //Friction against rotation
      this.rRes = rRes; //Resistance to rotation
      this.rThresh = rThresh; //Maximum angle difference before rotation
      this.children = [];
      this.systems = [];
    }
    follow(x, y) {
      var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
      var angle = Math.atan2(y - this.y, x - this.x);
      //Update forward
      var accel = this.fAccel;
      if (this.systems.length > 0) {
        var sum = 0;
        for (var i = 0; i < this.systems.length; i++) {
          sum += this.systems[i].step == 0;
        }
        accel *= sum / this.systems.length;
      }
      this.fSpeed += accel * (dist > this.fThresh);
      this.fSpeed *= 1 - this.fRes;
      this.speed = Math.max(0, this.fSpeed - this.fFric);
      //Update rotation
      var dif = this.absAngle - angle;
      dif -= 2 * Math.PI * Math.floor(dif / (2 * Math.PI) + 1 / 2);
      if (Math.abs(dif) > this.rThresh && dist > this.fThresh) {
        this.rSpeed -= this.rAccel * (2 * (dif > 0) - 1);
      }
      this.rSpeed *= 1 - this.rRes;
      if (Math.abs(this.rSpeed) > this.rFric) {
        this.rSpeed -= this.rFric * (2 * (this.rSpeed > 0) - 1);
      } else {
        this.rSpeed = 0;
      }
  
      //Update position
      this.absAngle += this.rSpeed;
      this.absAngle -=
        2 * Math.PI * Math.floor(this.absAngle / (2 * Math.PI) + 1 / 2);
      this.x += this.speed * Math.cos(this.absAngle);
      this.y += this.speed * Math.sin(this.absAngle);
      this.absAngle += Math.PI;
      for (var i = 0; i < this.children.length; i++) {
        this.children[i].follow(true, true);
      }
      for (var i = 0; i < this.systems.length; i++) {
        this.systems[i].update(x, y);
      }
      this.absAngle -= Math.PI;
      this.draw(true);
    }
    draw(iter) {
      var r = 4;
      ctx.beginPath();
      ctx.arc(
        this.x,
        this.y,
        r,
        Math.PI / 4 + this.absAngle,
        7 * Math.PI / 4 + this.absAngle
      );
      ctx.moveTo(
        this.x + r * Math.cos(7 * Math.PI / 4 + this.absAngle),
        this.y + r * Math.sin(7 * Math.PI / 4 + this.absAngle)
      );
      ctx.lineTo(
        this.x + r * Math.cos(this.absAngle) * 2 ** 0.5,
        this.y + r * Math.sin(this.absAngle) * 2 ** 0.5
      );
      ctx.lineTo(
        this.x + r * Math.cos(Math.PI / 4 + this.absAngle),
        this.y + r * Math.sin(Math.PI / 4 + this.absAngle)
      );
      ctx.stroke();
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].draw(true);
        }
      }
    }
  }

Fonctions de Configuration des Créatures

Cette section contient plusieurs fonctions setupSimple, setupTentacle, setupArm, setupTestSquid et setupLizard. Chacune d'elles permet de configurer et d'initialiser différents types de créatures avec des structures et des comportements variés en utilisant les classes Segment, LimbSystem et Creature. Elles définissent les paramètres de chaque créature, comme le nombre de segments, leur taille, leur rigidité, ainsi que la vitesse et la réactivité globale de la créature. Ces fonctions appellent ensuite setInterval pour animer la créature en rafraîchissant le canvas à intervalles réguliers.


  //Initializes and animates
  var critter;
  function setupSimple() {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    var critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      12,
      1,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var node = critter;
    //(parent,size,angle,range,stiffness)
    for (var i = 0; i < 128; i++) {
      var node = new Segment(node, 8, 0, 3.14159 / 2, 1);
    }
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(Input.mouse.x, Input.mouse.y);
    }, 33);
  }
  function setupTentacle() {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      12,
      1,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var node = critter;
    //(parent,size,angle,range,stiffness)
    for (var i = 0; i < 32; i++) {
      var node = new Segment(node, 8, 0, 2, 1);
    }
    //(end,length,speed,creature)
    var tentacle = new LimbSystem(node, 32, 8, critter);
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(canvas.width / 2, canvas.height / 2);
      ctx.beginPath();
      ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283);
      ctx.fill();
    }, 33);
  }
  function setupArm() {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    var critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      12,
      1,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var node = critter;
    //(parent,size,angle,range,stiffness)
    for (var i = 0; i < 3; i++) {
      var node = new Segment(node, 80, 0, 3.1416, 1);
    }
    var tentacle = new LimbSystem(node, 3, critter);
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(canvas.width / 2, canvas.height / 2);
    }, 33);
    ctx.beginPath();
    ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283);
    ctx.fill();
  }
  
  function setupTestSquid(size, legs) {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      size * 10,
      size * 3,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var legNum = legs;
    var jointNum = 32;
    for (var i = 0; i < legNum; i++) {
      var node = critter;
      var ang = Math.PI / 2 * (i / (legNum - 1) - 0.5);
      for (var ii = 0; ii < jointNum; ii++) {
        var node = new Segment(
          node,
          size * 64 / jointNum,
          ang * (ii == 0),
          3.1416,
          1.2
        );
      }
      //(end,length,speed,creature,dist)
      var leg = new LegSystem(node, jointNum, size * 30, critter);
    }
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(Input.mouse.x, Input.mouse.y);
    }, 33);
  }
  function setupLizard(size, legs, tail) {
    var s = size;
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      s * 10,
      s * 2,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var spinal = critter;
    //(parent,size,angle,range,stiffness)
    //Neck
    for (var i = 0; i < 6; i++) {
      spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1);
      for (var ii = -1; ii <= 1; ii += 2) {
        var node = new Segment(spinal, s * 3, ii, 0.1, 2);
        for (var iii = 0; iii < 3; iii++) {
          node = new Segment(node, s * 0.1, -ii * 0.1, 0.1, 2);
        }
      }
    }
    //Torso and legs
    for (var i = 0; i < legs; i++) {
      if (i > 0) {
        //Vertebrae and ribs
        for (var ii = 0; ii < 6; ii++) {
          spinal = new Segment(spinal, s * 4, 0, 1.571, 1.5);
          for (var iii = -1; iii <= 1; iii += 2) {
            var node = new Segment(spinal, s * 3, iii * 1.571, 0.1, 1.5);
            for (var iv = 0; iv < 3; iv++) {
              node = new Segment(node, s * 3, -iii * 0.3, 0.1, 2);
            }
          }
        }
      }
      //Legs and shoulders
      for (var ii = -1; ii <= 1; ii += 2) {
        var node = new Segment(spinal, s * 12, ii * 0.785, 0, 8); //Hip
        node = new Segment(node, s * 16, -ii * 0.785, 6.28, 1); //Humerus
        node = new Segment(node, s * 16, ii * 1.571, 3.1415, 2); //Forearm
        for (
          var iii = 0;
          iii < 4;
          iii++ //fingers
        ) {
          new Segment(node, s * 4, (iii / 3 - 0.5) * 1.571, 0.1, 4);
        }
        new LegSystem(node, 3, s * 12, critter, 4);
      }
    }
    //Tail
    for (var i = 0; i < tail; i++) {
      spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1);
      for (var ii = -1; ii <= 1; ii += 2) {
        var node = new Segment(spinal, s * 3, ii, 0.1, 2);
        for (var iii = 0; iii < 3; iii++) {
          node = new Segment(node, s * 3 * (tail - i) / tail, -ii * 0.1, 0.1, 2);
        }
      }
    }
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(Input.mouse.x, Input.mouse.y);
    }, 33);
  }

Paramètres visuels et Lancement de l'Animation

Cette dernière section définit le style du canvas (couleur de fond et couleur des lignes) et appelle l'une des fonctions de configuration (setupLizard dans cet exemple) pour créer et démarrer l'animation de la créature. Les lignes commentées montrent d'autres configurations possibles de créatures.


  canvas.style.backgroundColor = "black";
  ctx.strokeStyle = "white";
  //setupSimple();//Just the very basic string
  //setupTentacle();//Tentacle that reaches for mouse
  //setupLizard(.5,100,128);//Literal centipede
  //setupSquid(2,8);//Spidery thing
  var legNum = Math.floor(1 + Math.random() * 12);
  setupLizard(
    8 / Math.sqrt(legNum),
    legNum,
    Math.floor(4 + Math.random() * legNum * 8)
  );

Voici le code complet

Voici le code complet et ici deux sections : la partie HTML et la partie JS.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reptile Interactive Cursor</title>
</head>
<body>
    <script  src="./script.js"></script>
</body>
</html>
var Input = {
    keys: [],
    mouse: {
      left: false,
      right: false,
      middle: false,
      x: 0,
      y: 0
    }
  };
  for (var i = 0; i < 230; i++) {
    Input.keys.push(false);
  }
  document.addEventListener("keydown", function(event) {
    Input.keys[event.keyCode] = true;
  });
  document.addEventListener("keyup", function(event) {
    Input.keys[event.keyCode] = false;
  });
  document.addEventListener("mousedown", function(event) {
    if ((event.button = 0)) {
      Input.mouse.left = true;
    }
    if ((event.button = 1)) {
      Input.mouse.middle = true;
    }
    if ((event.button = 2)) {
      Input.mouse.right = true;
    }
  });
  document.addEventListener("mouseup", function(event) {
    if ((event.button = 0)) {
      Input.mouse.left = false;
    }
    if ((event.button = 1)) {
      Input.mouse.middle = false;
    }
    if ((event.button = 2)) {
      Input.mouse.right = false;
    }
  });
  document.addEventListener("mousemove", function(event) {
    Input.mouse.x = event.clientX;
    Input.mouse.y = event.clientY;
  });
  //Sets up canvas
  var canvas = document.createElement("canvas");
  document.body.appendChild(canvas);
  canvas.width = Math.max(window.innerWidth, window.innerWidth);
  
  //canvas.height = Math.max(window.innerWidth, window.innerWidth);
  
  canvas.height = window.innerHeight;
  canvas.style.position = "absolute";
  canvas.style.left = "0px";
  canvas.style.top = "0px";
  document.body.style.overflow = "hidden";
  var ctx = canvas.getContext("2d");
  //Necessary classes
  var segmentCount = 0;
  class Segment {
    constructor(parent, size, angle, range, stiffness) {
      segmentCount++;
      this.isSegment = true;
      this.parent = parent; //Segment which this one is connected to
      if (typeof parent.children == "object") {
        parent.children.push(this);
      }
      this.children = []; //Segments connected to this segment
      this.size = size; //Distance from parent
      this.relAngle = angle; //Angle relative to parent
      this.defAngle = angle; //Default angle relative to parent
      this.absAngle = parent.absAngle + angle; //Angle relative to x-axis
      this.range = range; //Difference between maximum and minimum angles
      this.stiffness = stiffness; //How closely it conforms to default angle
      this.updateRelative(false, true);
    }
    updateRelative(iter, flex) {
      this.relAngle =
        this.relAngle -
        2 *
          Math.PI *
          Math.floor((this.relAngle - this.defAngle) / 2 / Math.PI + 1 / 2);
      if (flex) {
        //    this.relAngle=this.range/
        //        (1+Math.exp(-4*(this.relAngle-this.defAngle)/
        //        (this.stiffness*this.range)))
        //        -this.range/2+this.defAngle;
        this.relAngle = Math.min(
          this.defAngle + this.range / 2,
          Math.max(
            this.defAngle - this.range / 2,
            (this.relAngle - this.defAngle) / this.stiffness + this.defAngle
          )
        );
      }
      this.absAngle = this.parent.absAngle + this.relAngle;
      this.x = this.parent.x + Math.cos(this.absAngle) * this.size; //Position
      this.y = this.parent.y + Math.sin(this.absAngle) * this.size; //Position
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].updateRelative(iter, flex);
        }
      }
    }
    draw(iter) {
      ctx.beginPath();
      ctx.moveTo(this.parent.x, this.parent.y);
      ctx.lineTo(this.x, this.y);
      ctx.stroke();
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].draw(true);
        }
      }
    }
    follow(iter) {
      var x = this.parent.x;
      var y = this.parent.y;
      var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
      this.x = x + this.size * (this.x - x) / dist;
      this.y = y + this.size * (this.y - y) / dist;
      this.absAngle = Math.atan2(this.y - y, this.x - x);
      this.relAngle = this.absAngle - this.parent.absAngle;
      this.updateRelative(false, true);
      //this.draw();
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].follow(true);
        }
      }
    }
  }
  class LimbSystem {
    constructor(end, length, speed, creature) {
      this.end = end;
      this.length = Math.max(1, length);
      this.creature = creature;
      this.speed = speed;
      creature.systems.push(this);
      this.nodes = [];
      var node = end;
      for (var i = 0; i < length; i++) {
        this.nodes.unshift(node);
        //node.stiffness=1;
        node = node.parent;
        if (!node.isSegment) {
          this.length = i + 1;
          break;
        }
      }
      this.hip = this.nodes[0].parent;
    }
    moveTo(x, y) {
      this.nodes[0].updateRelative(true, true);
      var dist = ((x - this.end.x) ** 2 + (y - this.end.y) ** 2) ** 0.5;
      var len = Math.max(0, dist - this.speed);
      for (var i = this.nodes.length - 1; i >= 0; i--) {
        var node = this.nodes[i];
        var ang = Math.atan2(node.y - y, node.x - x);
        node.x = x + len * Math.cos(ang);
        node.y = y + len * Math.sin(ang);
        x = node.x;
        y = node.y;
        len = node.size;
      }
      for (var i = 0; i < this.nodes.length; i++) {
        var node = this.nodes[i];
        node.absAngle = Math.atan2(
          node.y - node.parent.y,
          node.x - node.parent.x
        );
        node.relAngle = node.absAngle - node.parent.absAngle;
        for (var ii = 0; ii < node.children.length; ii++) {
          var childNode = node.children[ii];
          if (!this.nodes.includes(childNode)) {
            childNode.updateRelative(true, false);
          }
        }
      }
      //this.nodes[0].updateRelative(true,false)
    }
    update() {
      this.moveTo(Input.mouse.x, Input.mouse.y);
    }
  }
  class LegSystem extends LimbSystem {
    constructor(end, length, speed, creature) {
      super(end, length, speed, creature);
      this.goalX = end.x;
      this.goalY = end.y;
      this.step = 0; //0 stand still, 1 move forward,2 move towards foothold
      this.forwardness = 0;
  
      //For foot goal placement
      this.reach =
        0.9 *
        ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** 0.5;
      var relAngle =
        this.creature.absAngle -
        Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x);
      relAngle -= 2 * Math.PI * Math.floor(relAngle / 2 / Math.PI + 1 / 2);
      this.swing = -relAngle + (2 * (relAngle < 0) - 1) * Math.PI / 2;
      this.swingOffset = this.creature.absAngle - this.hip.absAngle;
      //this.swing*=(2*(relAngle>0)-1);
    }
    update(x, y) {
      this.moveTo(this.goalX, this.goalY);
      //this.nodes[0].follow(true,true)
      if (this.step == 0) {
        var dist =
          ((this.end.x - this.goalX) ** 2 + (this.end.y - this.goalY) ** 2) **
          0.5;
        if (dist > 1) {
          this.step = 1;
          //this.goalX=x;
          //this.goalY=y;
          this.goalX =
            this.hip.x +
            this.reach *
              Math.cos(this.swing + this.hip.absAngle + this.swingOffset) +
            (2 * Math.random() - 1) * this.reach / 2;
          this.goalY =
            this.hip.y +
            this.reach *
              Math.sin(this.swing + this.hip.absAngle + this.swingOffset) +
            (2 * Math.random() - 1) * this.reach / 2;
        }
      } else if (this.step == 1) {
        var theta =
          Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x) -
          this.hip.absAngle;
        var dist =
          ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) **
          0.5;
        var forwardness2 = dist * Math.cos(theta);
        var dF = this.forwardness - forwardness2;
        this.forwardness = forwardness2;
        if (dF * dF < 1) {
          this.step = 0;
          this.goalX = this.hip.x + (this.end.x - this.hip.x);
          this.goalY = this.hip.y + (this.end.y - this.hip.y);
        }
      }
      //  ctx.strokeStyle='blue';
      //  ctx.beginPath();
      //  ctx.moveTo(this.end.x,this.end.y);
      //  ctx.lineTo(this.hip.x+this.reach*Math.cos(this.swing+this.hip.absAngle+this.swingOffset),
      //        this.hip.y+this.reach*Math.sin(this.swing+this.hip.absAngle+this.swingOffset));
      //  ctx.stroke();
      //  ctx.strokeStyle='black';
    }
  }
  class Creature {
    constructor(
      x,
      y,
      angle,
      fAccel,
      fFric,
      fRes,
      fThresh,
      rAccel,
      rFric,
      rRes,
      rThresh
    ) {
      this.x = x; //Starting position
      this.y = y;
      this.absAngle = angle; //Staring angle
      this.fSpeed = 0; //Forward speed
      this.fAccel = fAccel; //Force when moving forward
      this.fFric = fFric; //Friction against forward motion
      this.fRes = fRes; //Resistance to motion
      this.fThresh = fThresh; //minimum distance to target to keep moving forward
      this.rSpeed = 0; //Rotational speed
      this.rAccel = rAccel; //Force when rotating
      this.rFric = rFric; //Friction against rotation
      this.rRes = rRes; //Resistance to rotation
      this.rThresh = rThresh; //Maximum angle difference before rotation
      this.children = [];
      this.systems = [];
    }
    follow(x, y) {
      var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
      var angle = Math.atan2(y - this.y, x - this.x);
      //Update forward
      var accel = this.fAccel;
      if (this.systems.length > 0) {
        var sum = 0;
        for (var i = 0; i < this.systems.length; i++) {
          sum += this.systems[i].step == 0;
        }
        accel *= sum / this.systems.length;
      }
      this.fSpeed += accel * (dist > this.fThresh);
      this.fSpeed *= 1 - this.fRes;
      this.speed = Math.max(0, this.fSpeed - this.fFric);
      //Update rotation
      var dif = this.absAngle - angle;
      dif -= 2 * Math.PI * Math.floor(dif / (2 * Math.PI) + 1 / 2);
      if (Math.abs(dif) > this.rThresh && dist > this.fThresh) {
        this.rSpeed -= this.rAccel * (2 * (dif > 0) - 1);
      }
      this.rSpeed *= 1 - this.rRes;
      if (Math.abs(this.rSpeed) > this.rFric) {
        this.rSpeed -= this.rFric * (2 * (this.rSpeed > 0) - 1);
      } else {
        this.rSpeed = 0;
      }
  
      //Update position
      this.absAngle += this.rSpeed;
      this.absAngle -=
        2 * Math.PI * Math.floor(this.absAngle / (2 * Math.PI) + 1 / 2);
      this.x += this.speed * Math.cos(this.absAngle);
      this.y += this.speed * Math.sin(this.absAngle);
      this.absAngle += Math.PI;
      for (var i = 0; i < this.children.length; i++) {
        this.children[i].follow(true, true);
      }
      for (var i = 0; i < this.systems.length; i++) {
        this.systems[i].update(x, y);
      }
      this.absAngle -= Math.PI;
      this.draw(true);
    }
    draw(iter) {
      var r = 4;
      ctx.beginPath();
      ctx.arc(
        this.x,
        this.y,
        r,
        Math.PI / 4 + this.absAngle,
        7 * Math.PI / 4 + this.absAngle
      );
      ctx.moveTo(
        this.x + r * Math.cos(7 * Math.PI / 4 + this.absAngle),
        this.y + r * Math.sin(7 * Math.PI / 4 + this.absAngle)
      );
      ctx.lineTo(
        this.x + r * Math.cos(this.absAngle) * 2 ** 0.5,
        this.y + r * Math.sin(this.absAngle) * 2 ** 0.5
      );
      ctx.lineTo(
        this.x + r * Math.cos(Math.PI / 4 + this.absAngle),
        this.y + r * Math.sin(Math.PI / 4 + this.absAngle)
      );
      ctx.stroke();
      if (iter) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].draw(true);
        }
      }
    }
  }
  //Initializes and animates
  var critter;
  function setupSimple() {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    var critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      12,
      1,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var node = critter;
    //(parent,size,angle,range,stiffness)
    for (var i = 0; i < 128; i++) {
      var node = new Segment(node, 8, 0, 3.14159 / 2, 1);
    }
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(Input.mouse.x, Input.mouse.y);
    }, 33);
  }
  function setupTentacle() {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      12,
      1,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var node = critter;
    //(parent,size,angle,range,stiffness)
    for (var i = 0; i < 32; i++) {
      var node = new Segment(node, 8, 0, 2, 1);
    }
    //(end,length,speed,creature)
    var tentacle = new LimbSystem(node, 32, 8, critter);
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(canvas.width / 2, canvas.height / 2);
      ctx.beginPath();
      ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283);
      ctx.fill();
    }, 33);
  }
  function setupArm() {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    var critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      12,
      1,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var node = critter;
    //(parent,size,angle,range,stiffness)
    for (var i = 0; i < 3; i++) {
      var node = new Segment(node, 80, 0, 3.1416, 1);
    }
    var tentacle = new LimbSystem(node, 3, critter);
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(canvas.width / 2, canvas.height / 2);
    }, 33);
    ctx.beginPath();
    ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283);
    ctx.fill();
  }
  
  function setupTestSquid(size, legs) {
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      size * 10,
      size * 3,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var legNum = legs;
    var jointNum = 32;
    for (var i = 0; i < legNum; i++) {
      var node = critter;
      var ang = Math.PI / 2 * (i / (legNum - 1) - 0.5);
      for (var ii = 0; ii < jointNum; ii++) {
        var node = new Segment(
          node,
          size * 64 / jointNum,
          ang * (ii == 0),
          3.1416,
          1.2
        );
      }
      //(end,length,speed,creature,dist)
      var leg = new LegSystem(node, jointNum, size * 30, critter);
    }
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(Input.mouse.x, Input.mouse.y);
    }, 33);
  }
  function setupLizard(size, legs, tail) {
    var s = size;
    //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh)
    critter = new Creature(
      window.innerWidth / 2,
      window.innerHeight / 2,
      0,
      s * 10,
      s * 2,
      0.5,
      16,
      0.5,
      0.085,
      0.5,
      0.3
    );
    var spinal = critter;
    //(parent,size,angle,range,stiffness)
    //Neck
    for (var i = 0; i < 6; i++) {
      spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1);
      for (var ii = -1; ii <= 1; ii += 2) {
        var node = new Segment(spinal, s * 3, ii, 0.1, 2);
        for (var iii = 0; iii < 3; iii++) {
          node = new Segment(node, s * 0.1, -ii * 0.1, 0.1, 2);
        }
      }
    }
    //Torso and legs
    for (var i = 0; i < legs; i++) {
      if (i > 0) {
        //Vertebrae and ribs
        for (var ii = 0; ii < 6; ii++) {
          spinal = new Segment(spinal, s * 4, 0, 1.571, 1.5);
          for (var iii = -1; iii <= 1; iii += 2) {
            var node = new Segment(spinal, s * 3, iii * 1.571, 0.1, 1.5);
            for (var iv = 0; iv < 3; iv++) {
              node = new Segment(node, s * 3, -iii * 0.3, 0.1, 2);
            }
          }
        }
      }
      //Legs and shoulders
      for (var ii = -1; ii <= 1; ii += 2) {
        var node = new Segment(spinal, s * 12, ii * 0.785, 0, 8); //Hip
        node = new Segment(node, s * 16, -ii * 0.785, 6.28, 1); //Humerus
        node = new Segment(node, s * 16, ii * 1.571, 3.1415, 2); //Forearm
        for (
          var iii = 0;
          iii < 4;
          iii++ //fingers
        ) {
          new Segment(node, s * 4, (iii / 3 - 0.5) * 1.571, 0.1, 4);
        }
        new LegSystem(node, 3, s * 12, critter, 4);
      }
    }
    //Tail
    for (var i = 0; i < tail; i++) {
      spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1);
      for (var ii = -1; ii <= 1; ii += 2) {
        var node = new Segment(spinal, s * 3, ii, 0.1, 2);
        for (var iii = 0; iii < 3; iii++) {
          node = new Segment(node, s * 3 * (tail - i) / tail, -ii * 0.1, 0.1, 2);
        }
      }
    }
    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      critter.follow(Input.mouse.x, Input.mouse.y);
    }, 33);
  }
  canvas.style.backgroundColor = "black";
  ctx.strokeStyle = "white";
  //setupSimple();//Just the very basic string
  //setupTentacle();//Tentacle that reaches for mouse
  //setupLizard(.5,100,128);//Literal centipede
  //setupSquid(2,8);//Spidery thing
  var legNum = Math.floor(1 + Math.random() * 12);
  setupLizard(
    8 / Math.sqrt(legNum),
    legNum,
    Math.floor(4 + Math.random() * legNum * 8)
  );

Conclusion : Une créature est née

Et voilà ! Avec ces quelques blocs de code, vous avez transcendé un simple curseur pour créer une entité dynamique qui répond à vos actions. Vous avez touché du doigt les principes fondamentaux de l'animation programmatique : la gestion d'états, la boucle de rendu et la simulation de comportements simples qui, mis bout à bout, créent une illusion de vie complexe.

Ce projet n'est qu'un point de départ. Imaginez les possibilités : vous pouvez changer les couleurs, la longueur, le nombre de segments. Vous pourriez même ajouter des "pattes" qui seraient d'autres chaînes de segments attachées au corps principal !

Post a Comment

N'hésitez pas de mentionner votre nom d'utilisateur Telegram à la fin de votre message pour recevoir des cadeaux de la part des modérateurs. Merci de commenter