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 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 LogiqueAvant 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. 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 . C'est une surface de dessin sur laquelle JavaScript peut peindre des formes, des lignes et des images à très haute vitesse. 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). 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. 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 . 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 : Reptile Interactive Cursor Étape 2 : Le JavaScriptLe 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 InputCette 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 CanvasCette 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 SegmentLa 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 LimbSystemLa 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 LegSystemLa 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 CreatureLa 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éaturesCette 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 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 = 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 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