Back to Components
Reptile Interactive Cursor Using HTML5 Canvas & Pure JavaScript | Realistic Creature Movement Animation
Component

Reptile Interactive Cursor Using HTML5 Canvas & Pure JavaScript | Realistic Creature Movement Animation

CodewithLord
October 19, 2025

This project showcases an interactive reptile-like cursor animation built entirely with HTML5 Canvas and JavaScript — no libraries or CSS animations required.

🧠 Description

This project showcases an interactive reptile-like cursor animation built entirely with HTML5 Canvas and JavaScript — no libraries or CSS animations required. The code simulates a realistic creature (like a lizard or tentacle) that dynamically follows your mouse movements.

It uses an advanced procedural animation system based on:

Segment-based joints (Segment class) to form flexible limbs and body parts

Inverse Kinematics (IK) via LimbSystem and LegSystem classes for natural motion

Creature physics (acceleration, rotation, friction) handled by the Creature class

The entire system updates and redraws the creature in real-time using requestAnimationFrame-like logic within timed intervals. As you move the mouse, the reptile crawls and adapts its direction fluidly — creating a lifelike interactive cursor effect that’s perfect for experimental animations, websites, or game prototypes.


💻 HTML Code


1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Reptile-Interactive-Cursor - CodewithLord</title> 7</head> 8<body> 9 <script src="script.js"></script> 10</body> 11</html>


Javascipt Code


1// Input handling 2 3var Input = { 4 keys: [], 5 mouse: { 6 left: false, 7 right: false, 8 middle: false, 9 x: 0, 10 y: 0 11 } 12 }; 13 14// initialize key states 15 16 for (var i = 0; i < 230; i++) { 17 Input.keys.push(false); 18 } 19 document.addEventListener("keydown", function(event) { 20 Input.keys[event.keyCode] = true; 21 }); 22 document.addEventListener("keyup", function(event) { 23 Input.keys[event.keyCode] = false; 24 }); 25 document.addEventListener("mousedown", function(event) { 26// use strict equality, not assignment 27 if ((event.button = 0)) { 28 Input.mouse.left = true; 29 } 30 if ((event.button = 1)) { 31 Input.mouse.middle = true; 32 } 33 if ((event.button = 2)) { 34 Input.mouse.right = true; 35 } 36 }); 37 document.addEventListener("mouseup", function(event) { 38 if ((event.button = 0)) { 39 Input.mouse.left = false; 40 } 41 if ((event.button = 1)) { 42 Input.mouse.middle = false; 43 } 44 if ((event.button = 2)) { 45 Input.mouse.right = false; 46 } 47 }); 48 document.addEventListener("mousemove", function(event) { 49 Input.mouse.x = event.clientX; 50 Input.mouse.y = event.clientY; 51 }); 52 53 //Sets up canvas 54 55 var canvas = document.createElement("canvas"); 56 document.body.appendChild(canvas); 57 canvas.width = Math.max(window.innerWidth, window.innerWidth); 58 59 60 canvas.height = window.innerHeight; 61 canvas.style.position = "absolute"; 62 canvas.style.left = "0px"; 63 canvas.style.top = "0px"; 64 document.body.style.overflow = "hidden"; 65 var ctx = canvas.getContext("2d"); 66 67// Segment class (joint) 68 69 var segmentCount = 0; 70 class Segment { 71 constructor(parent, size, angle, range, stiffness) { 72 segmentCount++; 73 this.isSegment = true; 74 this.parent = parent; //Segment which this one is connected to 75 if (typeof parent.children == "object") { 76 parent.children.push(this); 77 } 78 this.children = []; //Segments connected to this segment 79 this.size = size; //Distance from parent 80 this.relAngle = angle; //Angle relative to parent 81 this.defAngle = angle; //Default angle relative to parent 82 this.absAngle = parent.absAngle + angle; //Angle relative to x-axis 83 this.range = range; //Difference between maximum and minimum angles 84 this.stiffness = stiffness; //How closely it conforms to default angle 85 this.updateRelative(false, true); 86 } 87 updateRelative(iter, flex) { 88// normalize relative angle around defAngle 89 this.relAngle = 90 this.relAngle - 91 2 * 92 Math.PI * 93 Math.floor((this.relAngle - this.defAngle) / 2 / Math.PI + 1 / 2); 94 if (flex) { 95 // apply stiffness-limited angle toward defAngle 96 this.relAngle = Math.min( 97 this.defAngle + this.range / 2, 98 Math.max( 99 this.defAngle - this.range / 2, 100 (this.relAngle - this.defAngle) / this.stiffness + this.defAngle 101 ) 102 ); 103 } 104 this.absAngle = this.parent.absAngle + this.relAngle; 105 this.x = this.parent.x + Math.cos(this.absAngle) * this.size; //Position 106 this.y = this.parent.y + Math.sin(this.absAngle) * this.size; //Position 107 if (iter) { 108 for (var i = 0; i < this.children.length; i++) { 109 this.children[i].updateRelative(iter, flex); 110 } 111 } 112 } 113 draw(iter) { 114 ctx.beginPath(); 115 ctx.moveTo(this.parent.x, this.parent.y); 116 ctx.lineTo(this.x, this.y); 117 ctx.stroke(); 118 if (iter) { 119 for (var i = 0; i < this.children.length; i++) { 120 this.children[i].draw(true); 121 } 122 } 123 } 124 follow(iter) { 125 var x = this.parent.x; 126 var y = this.parent.y; 127 var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5; 128 this.x = x + this.size * (this.x - x) / dist; 129 this.y = y + this.size * (this.y - y) / dist; 130 this.absAngle = Math.atan2(this.y - y, this.x - x); 131 this.relAngle = this.absAngle - this.parent.absAngle; 132 this.updateRelative(false, true); 133 134 //this.draw(); 135 136 if (iter) { 137 for (var i = 0; i < this.children.length; i++) { 138 this.children[i].follow(true); 139 } 140 } 141 } 142 } 143 144// LimbSystem (IK-like limb) 145 146 class LimbSystem { 147 constructor(end, length, speed, creature) { 148 this.end = end; 149 this.length = Math.max(1, length); 150 this.creature = creature; 151 this.speed = speed; 152 creature.systems.push(this); 153 this.nodes = []; 154 var node = end; 155 for (var i = 0; i < length; i++) { 156 this.nodes.unshift(node); 157 //node.stiffness=1; 158 node = node.parent; 159 if (!node.isSegment) { 160 this.length = i + 1; 161 break; 162 } 163 } 164 this.hip = this.nodes[0].parent; 165 } 166 moveTo(x, y) { 167 this.nodes[0].updateRelative(true, true); 168 var dist = ((x - this.end.x) ** 2 + (y - this.end.y) ** 2) ** 0.5; 169 var len = Math.max(0, dist - this.speed); 170 for (var i = this.nodes.length - 1; i >= 0; i--) { 171 var node = this.nodes[i]; 172 var ang = Math.atan2(node.y - y, node.x - x); 173 node.x = x + len * Math.cos(ang); 174 node.y = y + len * Math.sin(ang); 175 x = node.x; 176 y = node.y; 177 len = node.size; 178 } 179 for (var i = 0; i < this.nodes.length; i++) { 180 var node = this.nodes[i]; 181 node.absAngle = Math.atan2( 182 node.y - node.parent.y, 183 node.x - node.parent.x 184 ); 185 node.relAngle = node.absAngle - node.parent.absAngle; 186 for (var ii = 0; ii < node.children.length; ii++) { 187 var childNode = node.children[ii]; 188 if (!this.nodes.includes(childNode)) { 189 childNode.updateRelative(true, false); 190 } 191 } 192 } 193 //this.nodes[0].updateRelative(true,false) 194 } 195 update() { 196 this.moveTo(Input.mouse.x, Input.mouse.y); 197 } 198 } 199 200// LegSystem (specialized limb) 201 202 class LegSystem extends LimbSystem { 203 constructor(end, length, speed, creature) { 204 super(end, length, speed, creature); 205 this.goalX = end.x; 206 this.goalY = end.y; 207 this.step = 0; //0 stand still, 1 move forward,2 move towards foothold 208 this.forwardness = 0; 209 210 //For foot goal placement 211 this.reach = 212 0.9 * 213 ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** 0.5; 214 var relAngle = 215 this.creature.absAngle - 216 Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x); 217 relAngle -= 2 * Math.PI * Math.floor(relAngle / 2 / Math.PI + 1 / 2); 218 this.swing = -relAngle + (2 * (relAngle < 0) - 1) * Math.PI / 2; 219 this.swingOffset = this.creature.absAngle - this.hip.absAngle; 220 //this.swing*=(2*(relAngle>0)-1); 221 } 222 update(x, y) { 223 this.moveTo(this.goalX, this.goalY); 224 //this.nodes[0].follow(true,true) 225 if (this.step == 0) { 226 var dist = 227 ((this.end.x - this.goalX) ** 2 + (this.end.y - this.goalY) ** 2) ** 228 0.5; 229 if (dist > 1) { 230 this.step = 1; 231 //this.goalX=x; 232 //this.goalY=y; 233 this.goalX = 234 this.hip.x + 235 this.reach * 236 Math.cos(this.swing + this.hip.absAngle + this.swingOffset) + 237 (2 * Math.random() - 1) * this.reach / 2; 238 this.goalY = 239 this.hip.y + 240 this.reach * 241 Math.sin(this.swing + this.hip.absAngle + this.swingOffset) + 242 (2 * Math.random() - 1) * this.reach / 2; 243 } 244 } else if (this.step == 1) { 245 var theta = 246 Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x) - 247 this.hip.absAngle; 248 var dist = 249 ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** 250 0.5; 251 var forwardness2 = dist * Math.cos(theta); 252 var dF = this.forwardness - forwardness2; 253 this.forwardness = forwardness2; 254 if (dF * dF < 1) { 255 this.step = 0; 256 this.goalX = this.hip.x + (this.end.x - this.hip.x); 257 this.goalY = this.hip.y + (this.end.y - this.hip.y); 258 } 259 } 260 261 } 262 } 263 264// Creature class (root) 265 266 class Creature { 267 constructor( 268 x, 269 y, 270 angle, 271 fAccel, 272 fFric, 273 fRes, 274 fThresh, 275 rAccel, 276 rFric, 277 rRes, 278 rThresh 279 ) { 280 this.x = x; //Starting position 281 this.y = y; 282 this.absAngle = angle; //Staring angle 283 this.fSpeed = 0; //Forward speed 284 this.fAccel = fAccel; //Force when moving forward 285 this.fFric = fFric; //Friction against forward motion 286 this.fRes = fRes; //Resistance to motion 287 this.fThresh = fThresh; //minimum distance to target to keep moving forward 288 this.rSpeed = 0; //Rotational speed 289 this.rAccel = rAccel; //Force when rotating 290 this.rFric = rFric; //Friction against rotation 291 this.rRes = rRes; //Resistance to rotation 292 this.rThresh = rThresh; //Maximum angle difference before rotation 293 this.children = []; 294 this.systems = []; 295 } 296 follow(x, y) { 297 var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5; 298 var angle = Math.atan2(y - this.y, x - this.x); 299 //Update forward 300 var accel = this.fAccel; 301 if (this.systems.length > 0) { 302 var sum = 0; 303 for (var i = 0; i < this.systems.length; i++) { 304 sum += this.systems[i].step == 0; 305 } 306 accel *= sum / this.systems.length; 307 } 308 this.fSpeed += accel * (dist > this.fThresh); 309 this.fSpeed *= 1 - this.fRes; 310 this.speed = Math.max(0, this.fSpeed - this.fFric); 311 //Update rotation 312 var dif = this.absAngle - angle; 313 dif -= 2 * Math.PI * Math.floor(dif / (2 * Math.PI) + 1 / 2); 314 if (Math.abs(dif) > this.rThresh && dist > this.fThresh) { 315 this.rSpeed -= this.rAccel * (2 * (dif > 0) - 1); 316 } 317 this.rSpeed *= 1 - this.rRes; 318 if (Math.abs(this.rSpeed) > this.rFric) { 319 this.rSpeed -= this.rFric * (2 * (this.rSpeed > 0) - 1); 320 } else { 321 this.rSpeed = 0; 322 } 323 324 //Update position 325 this.absAngle += this.rSpeed; 326 this.absAngle -= 327 2 * Math.PI * Math.floor(this.absAngle / (2 * Math.PI) + 1 / 2); 328 this.x += this.speed * Math.cos(this.absAngle); 329 this.y += this.speed * Math.sin(this.absAngle); 330 this.absAngle += Math.PI; 331 for (var i = 0; i < this.children.length; i++) { 332 this.children[i].follow(true, true); 333 } 334 for (var i = 0; i < this.systems.length; i++) { 335 this.systems[i].update(x, y); 336 } 337 this.absAngle -= Math.PI; 338 this.draw(true); 339 } 340 draw(iter) { 341 var r = 4; 342 ctx.beginPath(); 343 ctx.arc( 344 this.x, 345 this.y, 346 r, 347 Math.PI / 4 + this.absAngle, 348 7 * Math.PI / 4 + this.absAngle 349 ); 350 ctx.moveTo( 351 this.x + r * Math.cos(7 * Math.PI / 4 + this.absAngle), 352 this.y + r * Math.sin(7 * Math.PI / 4 + this.absAngle) 353 ); 354 ctx.lineTo( 355 this.x + r * Math.cos(this.absAngle) * 2 ** 0.5, 356 this.y + r * Math.sin(this.absAngle) * 2 ** 0.5 357 ); 358 ctx.lineTo( 359 this.x + r * Math.cos(Math.PI / 4 + this.absAngle), 360 this.y + r * Math.sin(Math.PI / 4 + this.absAngle) 361 ); 362 ctx.stroke(); 363 if (iter) { 364 for (var i = 0; i < this.children.length; i++) { 365 this.children[i].draw(true); 366 } 367 } 368 } 369 } 370 371// Setup helpers (examples) 372 373 var critter; 374 function setupSimple() { 375 // create creature and a long chain of segments 376 var critter = new Creature( 377 window.innerWidth / 2, 378 window.innerHeight / 2, 379 0, 380 12, 381 1, 382 0.5, 383 16, 384 0.5, 385 0.085, 386 0.5, 387 0.3 388 ); 389 var node = critter; 390 //(parent,size,angle,range,stiffness) 391 for (var i = 0; i < 128; i++) { 392 var node = new Segment(node, 8, 0, 3.14159 / 2, 1); 393 } 394 setInterval(function() { 395 ctx.clearRect(0, 0, canvas.width, canvas.height); 396 critter.follow(Input.mouse.x, Input.mouse.y); 397 }, 33); 398 } 399 function setupTentacle() { 400 //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) 401 critter = new Creature( 402 window.innerWidth / 2, 403 window.innerHeight / 2, 404 0, 405 12, 406 1, 407 0.5, 408 16, 409 0.5, 410 0.085, 411 0.5, 412 0.3 413 ); 414 var node = critter; 415 //(parent,size,angle,range,stiffness) 416 for (var i = 0; i < 32; i++) { 417 var node = new Segment(node, 8, 0, 2, 1); 418 } 419 //(end,length,speed,creature) 420 var tentacle = new LimbSystem(node, 32, 8, critter); 421 setInterval(function() { 422 ctx.clearRect(0, 0, canvas.width, canvas.height); 423 critter.follow(canvas.width / 2, canvas.height / 2); 424 ctx.beginPath(); 425 ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283); 426 ctx.fill(); 427 }, 33); 428 } 429 function setupArm() { 430 //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) 431 var critter = new Creature( 432 window.innerWidth / 2, 433 window.innerHeight / 2, 434 0, 435 12, 436 1, 437 0.5, 438 16, 439 0.5, 440 0.085, 441 0.5, 442 0.3 443 ); 444 var node = critter; 445 //(parent,size,angle,range,stiffness) 446 for (var i = 0; i < 3; i++) { 447 var node = new Segment(node, 80, 0, 3.1416, 1); 448 } 449 var tentacle = new LimbSystem(node, 3, critter); 450 setInterval(function() { 451 ctx.clearRect(0, 0, canvas.width, canvas.height); 452 critter.follow(canvas.width / 2, canvas.height / 2); 453 }, 33); 454 ctx.beginPath(); 455 ctx.arc(Input.mouse.x, Input.mouse.y, 2, 0, 6.283); 456 ctx.fill(); 457 } 458 459 function setupTestSquid(size, legs) { 460 //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) 461 critter = new Creature( 462 window.innerWidth / 2, 463 window.innerHeight / 2, 464 0, 465 size * 10, 466 size * 3, 467 0.5, 468 16, 469 0.5, 470 0.085, 471 0.5, 472 0.3 473 ); 474 var legNum = legs; 475 var jointNum = 32; 476 for (var i = 0; i < legNum; i++) { 477 var node = critter; 478 var ang = Math.PI / 2 * (i / (legNum - 1) - 0.5); 479 for (var ii = 0; ii < jointNum; ii++) { 480 var node = new Segment( 481 node, 482 size * 64 / jointNum, 483 ang * (ii == 0), 484 3.1416, 485 1.2 486 ); 487 } 488 //(end,length,speed,creature,dist) 489 var leg = new LegSystem(node, jointNum, size * 30, critter); 490 } 491 setInterval(function() { 492 ctx.clearRect(0, 0, canvas.width, canvas.height); 493 critter.follow(Input.mouse.x, Input.mouse.y); 494 }, 33); 495 } 496 function setupLizard(size, legs, tail) { 497 var s = size; 498 //(x,y,angle,fAccel,fFric,fRes,fThresh,rAccel,rFric,rRes,rThresh) 499 critter = new Creature( 500 window.innerWidth / 2, 501 window.innerHeight / 2, 502 0, 503 s * 10, 504 s * 2, 505 0.5, 506 16, 507 0.5, 508 0.085, 509 0.5, 510 0.3 511 ); 512 var spinal = critter; 513 //(parent,size,angle,range,stiffness) 514 //Neck 515 for (var i = 0; i < 6; i++) { 516 spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1); 517 for (var ii = -1; ii <= 1; ii += 2) { 518 var node = new Segment(spinal, s * 3, ii, 0.1, 2); 519 for (var iii = 0; iii < 3; iii++) { 520 node = new Segment(node, s * 0.1, -ii * 0.1, 0.1, 2); 521 } 522 } 523 } 524 //Torso and legs 525 for (var i = 0; i < legs; i++) { 526 if (i > 0) { 527 //Vertebrae and ribs 528 for (var ii = 0; ii < 6; ii++) { 529 spinal = new Segment(spinal, s * 4, 0, 1.571, 1.5); 530 for (var iii = -1; iii <= 1; iii += 2) { 531 var node = new Segment(spinal, s * 3, iii * 1.571, 0.1, 1.5); 532 for (var iv = 0; iv < 3; iv++) { 533 node = new Segment(node, s * 3, -iii * 0.3, 0.1, 2); 534 } 535 } 536 } 537 } 538 //Legs and shoulders 539 for (var ii = -1; ii <= 1; ii += 2) { 540 var node = new Segment(spinal, s * 12, ii * 0.785, 0, 8); //Hip 541 node = new Segment(node, s * 16, -ii * 0.785, 6.28, 1); //Humerus 542 node = new Segment(node, s * 16, ii * 1.571, 3.1415, 2); //Forearm 543 for ( 544 var iii = 0; 545 iii < 4; 546 iii++ //fingers 547 ) { 548 new Segment(node, s * 4, (iii / 3 - 0.5) * 1.571, 0.1, 4); 549 } 550 new LegSystem(node, 3, s * 12, critter, 4); 551 } 552 } 553 //Tail 554 for (var i = 0; i < tail; i++) { 555 spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1); 556 for (var ii = -1; ii <= 1; ii += 2) { 557 var node = new Segment(spinal, s * 3, ii, 0.1, 2); 558 for (var iii = 0; iii < 3; iii++) { 559 node = new Segment(node, s * 3 * (tail - i) / tail, -ii * 0.1, 0.1, 2); 560 } 561 } 562 } 563 setInterval(function() { 564 ctx.clearRect(0, 0, canvas.width, canvas.height); 565 critter.follow(Input.mouse.x, Input.mouse.y); 566 }, 33); 567 } 568 569// create creature and a long chain of segments 570 571 canvas.style.backgroundColor = "black"; 572 ctx.strokeStyle = "white"; 573 574 var legNum = Math.floor(1 + Math.random() * 12); 575 setupLizard( 576 8 / Math.sqrt(legNum), 577 legNum, 578 Math.floor(4 + Math.random() * legNum * 8) 579 ); 580

⚙️ JavaScript Explanation

The JavaScript file handles the entire logic for the reptile animation. It begins by defining an input system that tracks keyboard and mouse events, including the state of keys, mouse buttons, and the current mouse position on the screen. This allows the creature to respond interactively as the user moves the cursor.

A canvas element is dynamically created and appended to the document body. Its width and height are set to match the window dimensions, ensuring a full-screen experience. The context from this canvas is used for drawing lines, arcs, and other elements representing the creature.

The foundation of movement is handled by the Segment class. Each segment represents a small part of the reptile’s body or limb. These segments are connected in a parent-child hierarchy to form chains that can bend, rotate, and stretch realistically. Each segment maintains its own angle, length, and stiffness, controlling how naturally it flexes or returns to its default position.

To make the body parts behave like limbs, the LimbSystem class is introduced. It organizes a group of connected segments into a movable structure that can target a specific point. The movement is calculated using inverse kinematics, where each joint adjusts its position to keep the overall limb pointing towards a target — in this case, the mouse cursor. The LegSystem extends this logic further, allowing legs to behave independently, stepping naturally when the creature moves.

The Creature class acts as the main controller. It defines the reptile’s body position, direction, and speed. It applies physics such as acceleration, resistance, and friction to make movements appear smooth and organic. The creature continuously adjusts its angle to face the cursor and moves forward in that direction, while its limbs follow dynamically.

Multiple setup functions allow different types of creatures to be created — for example, simple chains, tentacles, arms, squids, or full lizards. Each setup initializes a different structure of segments and limb systems. The setupLizard function, which runs by default, creates a full reptile with a spine, legs, and tail. The tail and limbs are made up of multiple connected segments to simulate realistic motion.

The animation is handled using a timed interval that continuously clears the canvas, updates positions, and redraws every frame. The mouse coordinates guide the creature’s movement, making it appear as though a reptile is crawling toward and following the user’s cursor in real time.

Love this component?

Explore more components and build amazing UIs.

View All Components