Creating a Snake Monster Following Cursor
Introduction:
The project creates a skeletal structure that moves dynamically and interactively. The structure, resembling a lizard or a creature with a segmented body and limbs, reacts to user inputs like mouse movements and keyboard events. The design uses canvas-based animation in JavaScript, providing a visually engaging and responsive experience.
Card Structure:
The structure consists of multiple segments connected in a chain-like manner. The main body is composed of a spine with articulated segments that bend and flex, while smaller branches resemble limbs extending from the main body. Each limb has smaller segments that add detail, like claws or fingers.
HTML:
<body>
<script src="./script.js"></script>
<script src=></script>
</body>
</html>
JS:
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)
);
If Your Project is Not Working or Has Any Problem Don’t Worry, Just Click on Below Download Assets File And You Will Be Able To Get Full Control Of Our Projects , Then Customize And Use it For Your Coding Journey. Let the coding adventure begin!
By Downloading this assets You Will Be Able to create an Snake Monster Following Cursor HTML & JS Which Can Be Used For Several Projects i.e. A Flutter App Using Vs Code or For Android App Using Android Studio And Also For Custom Web Development.
Conclusion:
This skeletal animation effectively demonstrates how JavaScript and HTML canvas can create interactive and visually captivating effects. The articulated, dynamic movements provide an engaging experience for users, simulating the lifelike motion of a segmented creature. The simple yet striking design showcases how minimalistic elements can be used to create complex visual interactions.
Remember, the key to mastering web development is practice and experimentation. Feel free to customize the colors, or even extend the functionality to create your own unique navigation. Happy coding!