
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.
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.
- 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. - 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 <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 !