Get tangled up in the Spaghetti Worm's web with HTML, CSS, and JS

Get tangled up in the Spaghetti Worm's web with HTML, CSS, and JS

So in this article, we're gonna create a Spaghetti Worm animation with canvas using HTML, CSS & JS. So let's start writing some code!

We are gonna start by writing basic HTML & then style it with CSS & at last implement JS.

HTML Code:

First, we are gonna create a Boilerplate for the HTML Code & link it with the CSS & JS files.

No alt text provided for this image

Inside the body, we're gonna add a canvas tag. After that, we're going to link the JS file below.

The output of the HTML code :

No alt text provided for this image

The output will just print a blank page.

Let's elevate the visual appeal with basic CSS styling and bring the worm to life with an exciting animation!

CSS Code :

No alt text provided for this image

Here we're setting up some properties for the HTML & Body elements to take up the full width and height of the viewport, with no margin or padding, and dark gray background color.

The overflow is also set to "hidden" to ensure that any content that exceeds the dimensions of the elements is not visible to the user.

The output of the CSS code :

No alt text provided for this image

The output will print a dark gray background with nothing on it.

Now let's add some JS to make the animation appear.

JS Code :

No alt text provided for this image

We're gonna start with window.onload function that gets executed when the webpage is fully loaded in the browser. In the function, we're setting the onload property of the window object to an anonymous function. This means that the function will be executed when the webpage has finished loading.

No alt text provided for this image

Here we're setting up a canvas element with a 2D rendering context and then calling a function called fitCanvas(). The getElementById() method is used to select the canvas element in the HTML document with the id "canvas", and the getContext() method is used to create a 2D rendering context for that canvas.

No alt text provided for this image

Then we're gonna create a function that takes in two arguments: vec and b.

vec is expected to be an object with an x and y property, representing a 2D vector.

b is expected to be a number representing a limit on the magnitude of the vector.

The function first calculates the magnitude of the vector (this.d) using the Pythagorean theorem. It then calculates the angle of the vector relative to the positive x-axis (this.ang) using the Math.atan2() function.

If the magnitude of the vector is greater than the limit (b), the function returns a new object with x and y properties that have been scaled down to a magnitude of b while maintaining the direction of the original vector. This is done using the formula for converting polar coordinates to Cartesian coordinates:

x = r * cos(theta)
y = r * sin(theta)

If the magnitude of the vector is less than or equal to the limit, the function simply returns a copy of the original vector object.

No alt text provided for this image

Then we're gonna create another function that takes in two arguments: a and m.

a is expected to be an object with an x and y property, representing a 2D vector. m is expected to be a number representing the desired magnitude of the vector.

The function first calculates the angle of the vector relative to the positive x-axis (this.ang) using the Math.atan2() function. It then returns a new object with x and y properties that have been scaled to the desired magnitude m while maintaining the direction of the original vector.

No alt text provided for this image

Then we're gonna initialize some variables:

  • mouse and last_mouse are objects with x and y properties, representing the current and previous positions of the mouse cursor, respectively. The initial values for both properties are false.
  • pi is a constant with the value of pi, calculated using Math.PI property.
  • seaMargin is a number representing the margin of the simulation space, which is set to 1000.
  • boids is an array of 100 elements. he new Array(100) constructor creates a new array object with a length of 100, but the array elements are not initialized and will be undefined until they are assigned a value. The purpose of this is to create an empty array with a fixed length, likely to be used to store instances of a "boid" object used in the simulation.
No alt text provided for this image

Then we're gonna create a class called "boid". A boid is a simulated bird or fish-like object that is part of a flock or school. The boid class has a constructor method that is called when a new instance of the class is created using the new keyword.

The constructor method initializes a number of properties for the boid object:

  • this.opacity - is a random value between 0.05 and 0.55, representing the opacity of the boid.
  • this.ang - is a random value between 0 and , representing the initial angle of the boid's velocity vector.
  • this.mf - is a fixed value of 0.1, representing the maximum force that can be applied to the boid.
  • this.ms - is a random value between 0.9 and 1, representing the maximum speed of the boid.
  • this.vm - is a random value between 0.5 and 1.5, representing the initial speed of the boid.
  • this.pos - is an object with x and y properties, representing the initial position of the boid. The x and y properties are set to random values between 0 and the width and height of the simulation space (w and h, respectively).
  • this.vel - is an object with x and y properties, representing the initial velocity vector of the boid. The x and y properties are calculated based on the this.vm and this.ang values using the formula for converting polar coordinates to Cartesian coordinates.
  • this.acc - is an object with x and y properties, representing the initial acceleration of the boid. The x and y properties are both set to 0, as the boid starts with no acceleration.
No alt text provided for this image

Then we're gonna define a method for the boid class. This method takes a parameter called other that represents another boid object.

The purpose of the flock method is to calculate the three rules of flocking behavior for the current boid object, based on the position and velocity of the other boid object:

  • Alignment: The boid should align its velocity with the average velocity of nearby boids.
  • Cohesion: The boid should move towards the center of mass of nearby boids.
  • Separation: The boid should move away from other boids that are too close.

The method initializes three objects (this.aa, this.ap, and this.as) to represent the three components of the flocking behavior. These objects have x and y properties that represent the total alignment, position, and separation of the boid with respect to other boids in the flock.

These objects will be updated in the flock method based on the position and velocity of the other boid object and then used to update the acceleration of the current boid object in the main simulation loop.

No alt text provided for this image

Then we're gonna start by setting the value of a property called "count" on the object referred to by the "this" keyword to 0.

  • for (var ot of other) {: This line begins the for...if loop, which iterates over the elements in the other iterable. On each iteration, the value of the current element is assigned to a variable called ot.
  • if (ot != this) {: This line checks whether the current element, ot, is not equal to the object that the current code is being executed in, which is referenced by the this keyword. If ot is not equal to this, then the code inside the block is executed.
  1. this.dis = Math.sqrt( Math.pow(ot.pos.x - this.pos.x, 2) + Math.pow(ot.pos.y - this.pos.y, 2) );: This line calculates the distance between the ot object and the this object using the Pythagorean theorem.
  • ot.pos.x and ot.pos.y are the x and y coordinates of the ot object.
  • this.pos.x and this.pos.y are the x and y coordinates of the this object. The formula calculates the distance between ot and this by subtracting the x and y coordinates of this from those of ot, squaring the differences, summing the squares, and taking the square root of the sum. The result is then assigned to the dis property of the this object. The loop continues iterating over the remaining elements in the other iterable until there are no more elements left.

In summary, we're calculating the distance between each object in the other iterable and the object that the current code is being executed in (this), and store the distance in the dis property of the this object.

No alt text provided for this image

Then we're gonna define a method called move.

  • this.acc = { x: 0, y: 0 };: This line sets the value of a property called acc on the object that this refers to. acc is an object with two properties, x, and y, which are both initialized to 0. in general terms, it's initializing the acc property of the object to a starting state, which in this case is 0 for both the x and y properties.

Based on the name of the method (move), this is initializing an acceleration vector that will be used to move the object. The acceleration vector would be represented by the acc property of the object, and it's being set to 0 in both the x and y directions at the beginning of each call to the move method.

No alt text provided for this image

These lines are part of a move method for an object. The purpose of this method is to update the position and velocity of the object based on its acceleration (acc).

The first six lines update the acceleration of the object by adding the x and y components of three different acceleration vectors (aa, ap, and as) to the x and y components of the acc object.

After that, these two lines update the position of the object by adding the x and y components of the velocity (vel) to the x and y components of the position (pos) respectively.

  • this.pos.x += this.vel.x;: This line updates the x property of the pos object by adding the x component of the vel object.
  • this.pos.y += this.vel.y;: This line updates the y property of pos by adding the y component of vel.

These last ones update the velocity of the object by adding the x and y components of the acceleration (acc) to the x and y components of the velocity (vel) respectively.

  • this.vel.x += this.acc.x;: This line updates the x component of the velocity (vel) by adding the x component of the acceleration (acc).
  • this.vel.y += this.acc.y;: This line updates the y component of vel by adding the y component of acc.

Overall, these lines work together to simulate the motion of an object that is subject to acceleration, updating its position and velocity over time.

No alt text provided for this image

These two lines update the velocity (vel) and angle (ang) properties of the object based on its current velocity.

  • this.vel = limit(this.vel, this.ms);: This line sets the vel property of the object to the result of a limit function that takes two arguments: the current velocity of the object (this.vel) and a maximum speed (this.ms). The limit function restricts the magnitude (i.e., the length) of the velocity vector to be no greater than the maximum speed. This ensures that the object doesn't move too quickly.
  • this.ang = Math.atan2(this.vel.y, this.vel.x);: This line sets the ang property of the object to the result of the Math.atan2 function, which calculates the angle (in radians) between the positive x-axis and the vector represented by the vel property of the object. In other words, it calculates the direction in which the object is moving. The y component of vel is passed as the first argument to Math.atan2 because the y-axis is inverted in many graphics systems.

These lines ensure that the object's velocity is limited to a maximum speed and that its angle property reflects its current direction of movement. This is important for simulating realistic motion and for making the object behave as expected in a game or other interactive application.

No alt text provided for this image

These lines check whether the object's position pos has moved outside of the visible area of the screen (whose width is given by w and height is given by h) and if so, they adjust the position to "wrap" it around to the opposite edge of the screen.

  • if (this.pos.x > w + seaMargin) {: This line checks whether the x-coordinate of the object's position (pos.x) has moved to the right of the visible screen area, plus a margin (seaMargin). If so, the object has moved off the right edge of the screen.
  • this.pos.x -= w + seaMargin;: This line adjusts the x-coordinate of the object's position by subtracting the width of the screen (w) plus the margin (seaMargin). This "wraps" the object around to the left side of the screen, as if it had exited the right side and re-entered from the left.
  • if (this.pos.x < -seaMargin) {: This line checks whether the x-coordinate of the object's position has moved to the left of the visible screen area, minus the margin. If so, the object has moved off the left edge of the screen.
  • this.pos.x += w + seaMargin;: This line adjusts the x-coordinate of the object's position by adding the width of the screen plus the margin. This "wraps" the object around to the right side of the screen, as if it had exited the left side and re-entered from the right.
  • if (this.pos.y > h + seaMargin) {: This line checks whether the y-coordinate of the object's position has moved below the visible screen area, plus the margin. If so, the object has moved off the bottom edge of the screen.
  • this.pos.y -= h + seaMargin;: This line adjusts the y-coordinate of the object's position by subtracting the height of the screen plus the margin. This "wraps" the object around to the top of the screen, as if it had exited the bottom and re-entered from the top.
  • if (this.pos.y < -seaMargin) {: This line checks whether the y-coordinate of the object's position has moved above the visible screen area, minus the margin. If so, the object has moved off the top edge of the screen.
  • this.pos.y += h + seaMargin;: This line adjusts the y-coordinate of the object's position by adding the height of the screen plus the margin. This "wraps" the object around to the bottom of the screen, as if it had exited the top and re-entered from the bottom.

These lines ensure that the object remains visible on the screen even if it moves off one edge and re-enters from the opposite edge. This is a common technique in games and other interactive applications, where objects may move freely in a larger virtual world that is larger than the visible screen area.

No alt text provided for this image

These are responsible for drawing the object on the canvas.

  • c.beginPath();: starts a new path to draw.
  • c.lineTo(x, y);: connects the previous point in the path to the specified (x, y) point with a straight line.
  • this.pos.x - this.opacity * 20 * Math.cos(this.ang), this.pos.y - this.opacity * 20 * Math.sin(this.ang): calculates the first (x, y) point of the line. The x-coordinate is calculated by subtracting the product of the opacity, 20, and the cosine of the angle from the object's x-position. The y-coordinate is calculated similarly but with the sine of the angle.
  • c.lineTo(x, y);: connects the first point to the second (x, y) point with a straight line.
  • this.pos.x + this.opacity * 20 * Math.cos(this.ang), this.pos.y + this.opacity * 20 * Math.sin(this.ang): calculates the second (x, y) point of the line. The x-coordinate is calculated by adding the product of the opacity, 20, and the cosine of the angle to the object's x-position. The y-coordinate is calculated similarly but with the sine of the angle.
  • c.strokeStyle = "rgba(255,255,255," + this.opacity + ")";: sets the stroke style of the path to a semi-transparent white color whose opacity is equal to the object's opacity.
  • c.lineWidth = this.opacity * 8;: sets the width of the stroke to a value proportional to the object's opacity.
  • c.lineCap = "round";: sets the style of the stroke cap to round, giving the ends of the line a rounded appearance.
  • c.stroke();: draws the path on the canvas using the specified stroke style, width, and cap style.
No alt text provided for this image

This is a class definition for a node used in a simulation. A node is an object with a position (x, y), a size, an index, and a length. The class has four methods: follow(), update(), avoid(), and show().

The follow() method takes in a target object and moves the node toward it. It calculates the distance between the node and the target, as well as the angle between them, and moves the node towards the target by a fraction of the distance in the direction of the angle.

The update() method takes in another node object and a radius. It calculates the angle between the two nodes and updates the position of the node by moving it along the angle at the given radius from the other node.

The avoid() method takes in another node object and a radius. If the distance between the two nodes is less than or equal to the radius, the method moves the node away from the other node by a small amount in the direction of the angle between them.

The show() method displays the node on a canvas by drawing a circle with the given position and size.

This is used to simulate a group of nodes moving around and interacting with each other based on their positions and distances.

No alt text provided for this image

Then we're gonna define a class which will be called rope. The constructor in that takes in four arguments: the x and y coordinates for the starting position of the rope, the number of nodes in the rope (n), and the radius of each node (r).

Inside the constructor, the class initializes several instance variables. The arr variable is an empty array, which will be used later to store the connections between the nodes in the rope. The tail variable is an array that stores the nodes in the rope.

The first node is created using the node class with its position calculated using polar coordinates with an angle of 0 radians (i.e., at the top of the rope).

The for loop then iterates over the remaining nodes in the rope (from 1 to n-1), and adds them to the tail array. Each new node is positioned based on the position of the previous node, with its angle and index in the rope calculated using the node class. The radius of each node is set to r / 2.

No alt text provided for this image

After that, we're gonna define some more additional methods in the rope class.

The follow method takes in a m object and calls the follow method of the first node in the rope's tail array, passing in the m object. The follow method is presumably defined in the node class and is responsible for updating the position of the node based on a target position.

The avoid method takes in an array arr of objects and a radius rad, and updates the positions of the nodes in the rope to avoid colliding with the objects in the arr array. The method uses nested loops to iterate over each node in the rope and each object in the arr array. For each node, it checks if the object is not the same as the node and if the object's x-coordinate is not the same as the mouse's x-coordinate. If these conditions are met, the avoid method of the node is called, passing in the object and radius.

The update method is responsible for updating the positions of the nodes in the rope. It first updates the position of each node (except the last one) by calling the update method of the node and passing in the next node in the array and the radius r. The update method is presumably defined in the node class and is responsible for calculating the new position of the node based on its current position, the position of the next node, and the radius.

After updating the positions of the nodes, the update method then updates the position of each node (except the first one) to be the same as the next node in the array. This is done by setting the x and y coordinates of the current node to be the same as the nx and ny coordinates of the previous node.

These methods are responsible for updating the position of the nodes in the rope based on external factors such as a target position or obstacles in the environment.

No alt text provided for this image

The show() method first sets the global composite operation to "lighter". This blending mode will blend the colors of overlapping pixels, resulting in a brighter appearance.

It then begins a new path by calling beginPath, and iterates over each node in the tail array using a for loop. For each node, it adds a line segment to the path by calling lineTo with the node's x and y coordinates.

After defining the path, the method sets the stroke style to the RGB color value (251, 195, 167) by setting the strokeStyle property of the canvas context. This color will be used to stroke the path when the stroke method is called.

The method then calls the show method of each node in the tail array, presumably to render any additional visual elements associated with the nodes.

Finally, the global composite operation is set back to "source-over". This will reset the blending mode to the default, where the colors of overlapping pixels are replaced by the color of the topmost pixel.

This method is responsible for rendering the rope on a canvas by drawing a path between each node and stroking it with a specific color. Additionally, it renders any visual elements associated with each node.

No alt text provided for this image

Now the tar class is representing a moving target/simulation. It has a constructor method that takes an x and y coordinate for the initial position of the target.

The constructor creates a pool array of 20 randomly positioned points and initializes the tx and ty properties to the first point in the array. The lasti property is also initialized to 0.

The perception property is set to 300, which likely represents the maximum distance at which the target can be detected by other game elements.

The move method updates the position of the target by calculating the distance and angle between the target's current position and its target position (tx and ty), and moving the target 2% of that distance toward the target position.

The changeTarget method selects a new target for the target to move towards. It first removes the last target from the pool array and adds a new random target. It then loops through the pool array to find the closest target within the perception distance.

If a suitable target is found, the tx and ty properties are updated to that target's position, and the lasti property is set to the index of that target in the pool array.

If no suitable target is found, the tx and ty properties are set to the position of the first point in the pool array, and lasti is set to 0.

No alt text provided for this image

For the tar class, we're gonna define a show method & inside that it first draws a circle on the canvas with the center at the x and y coordinates of the instance and a radius equal to the perception property of the instance. The circle is filled with a semi-transparent white color using fillStyle property, and the line width is set to 1 using the lineWidth property.

Next, a loop is used to draw a series of circles around the tar instance. The loop iterates through the pool array of the instance, and for each element, it draws a circle on the canvas using the arc method of the CanvasRenderingContext2D API.

The center of the circle is defined by the x and y coordinates of the element in the pool array, and the radius is calculated based on the distance of the element from the current instance using a formula involving the length of the pool array and a power function. The circle is filled with a light gray color using fillStyle property, and the line width is set to 1 using the lineWidth property.

No alt text provided for this image

Next, we're gonna initialize an empty p array. It sets a variable num to 16 and creates a new instance of the tar class with random x and y coordinates. Then, a loop is executed num times to create 16 new instances of the rope class.

Each rope instance is created with random x and y coordinates, a length value that is a random integer between 10 and 30, and a thickness value of 10. Finally, each new rope instance is pushed into the p array.

No alt text provided for this image

Then we're gonna initialize an array of boid objects called boids. It then loops through the array and assigns a new boid object to each index.

Essentially, it creates and initializes n number of boid objects, where n is the length of the boids array.

No alt text provided for this image

Then we're gonna use the function draw() method to create animation on the canvas. We're gonna create a simulation of boids, which are artificial lifeforms that exhibit flocking behavior.

Next, it displays a "target" object on the canvas and checks whether the mouse is hovering over it. If so, the target's coordinates are set to the mouse's position. Otherwise, if the target is close to its destination, it changes its destination randomly. Then, it calls the "follow" method on an array of "p" objects, which seem to be particle objects that follow the target.

Finally, it loops through the "p" array again and performs collision avoidance between each particle and the tails of the other particles. The "avoid" method appears to take an array of positions to avoid and a "radius" parameter that determines how far away the particle should stay from those positions. It then updates and displays the positions of the particles on the canvas.

Overall, this code seems to be simulating the flocking behavior of boids, with a target that the particles follow and collision avoidance between the particles.

No alt text provided for this image

Then we're gonna add an event listener to the ctx element for the "mousemove" event. When the user moves their mouse over the element, the function passed as the second argument will be executed. Inside the function, the coordinates of the mouse pointer are recorded relative to the top-left corner of the ctx element, and stored in the mouse object. The previous position of the mouse pointer is also stored in the last_mouse object.

This can be used in canvas applications to track the mouse pointer and perform actions based on its position. The mouse object can be used to update the position of graphical elements, and the last_mouse object can be used to calculate the velocity of the mouse pointer.

No alt text provided for this image

Then we're gonna add an event listener to the ctx element for the "mouseleave" event. When the user moves their mouse pointer outside of the ctx element, the function passed as the second argument will be executed.

Inside the function, the x and y properties of the mouse object are set to false. This is typically done to indicate that the mouse pointer is no longer within the bounds of the ctx element and to prevent any further processing that depends on the mouse position.

Without this line of code, the mouse object will continue to hold the last known position of the mouse pointer even after the user has left the ctx element. This could potentially cause unexpected behavior in the code, such as elements on the canvas continuing to follow the mouse pointer even after it has left the canvas.

No alt text provided for this image

Then we're gonna define a function called fitCanvas that resizes the ctx element to fit the window size of the browser.

Inside the function, the w and h variables are assigned the values of the innerWidth and innerHeight properties of the window object, respectively. These values represent the width and height of the browser window. The width and height attributes of the ctx element are then set to these values, effectively resizing the canvas to fit the browser window.

This code is commonly used in canvas applications to ensure that the canvas remains responsive and adjusts to changes in the browser window size. The fitCanvas function can be called on page load and on window resize events to keep the canvas sized correctly.

No alt text provided for this image

Then we're gonna define a function called loop that runs continuously using the requestAnimationFrame method.

Inside the function, the fitCanvas function is called to resize the canvas element to fit the window size. Then the draw function is called to update and render the canvas content. Finally, the window.requestAnimationFrame method is called with the loop function as its argument. This causes the loop function to be called again on the next available frame, effectively creating a loop that continuously updates the canvas content.

This is used to create animations or simulations that require continuous updates. By using requestAnimationFrame, the loop is synchronized with the browser's refresh rate, resulting in smoother and more efficient animations.

No alt text provided for this image

Lastly, we're gonna schedule a call to the loop function on the next available animation frame.

The requestAnimationFrame method is a built-in method provided by most modern browsers that allow for smoother and more efficient animations by synchronizing the loop with the browser's refresh rate. By using this method instead of a simple setInterval or setTimeout function, the animation loop will run at a consistent rate regardless of the system performance, resulting in a smoother animation and reduced battery usage.

The loop function is defined and it contains the main logic for updating and rendering the canvas content. By calling window.requestAnimationFrame(loop), the loop will run continuously, resulting in an animated or interactive canvas element.

Final Output :

Follow for more content ??

Nur Iman .

4th Year | BEng (Honours) in Mechanical Engineering at National University of Singapore| Double Specialisation in Robotics and Industry 4.0 | Minor in Computer Science

1 年

Very enlightening

回复

要查看或添加评论,请登录

Stelvin Saji的更多文章

社区洞察

其他会员也浏览了