Mesmerizing Chinese Dragon

by typing10 in Circuits > Software

58 Views, 1 Favorites, 0 Comments

Mesmerizing Chinese Dragon

Screenshot 2025-01-13 204617.png

Chinese culture has always fascinated me, from the precise symbols of the language to the way their cities and buildings flow seamlessly along. The idea of a "chinese dragon" brings to mind a snakelike, red and yellow creature, flying peacefully through the sky. I wanted to try and create this mental scene in a mesmerizing animation. To give myself a bit of a challenge, I decided to do this in Javascript, a programming language not particularly well suited to animation. So let's buckle up and start animating!

Supplies

js.png

All you need for this project is a computer! Javascript runs in your browser, so you won't need to set anything up for the language itself. A basic understanding of the language is useful, but not strictly necessary. You can just copy-paste the code if you need, although I would encourage you to try and learn from it.

I'm going to be covering the idea behind the code first, which you can try and recreate yourself if you know Javascript, but if not, the last section has the code files themselves.

Skeleton

Screenshot 2025-01-13 205555.png

Every dragon needs a skeleton, and ours is no different. To capture the flowing, free spirit of the dragon, I decided to have its body follow the shape of a sine wave. This gives it an unearthly smoothness and makes the whole animation loop seamlessly. The basic design of the dragon's skeleton is based around this sine wave. All the points can be represented by a location on this wave. You can see this in the above image, where the circles are following the path of the wave.

Main Body

Screenshot 2025-01-13 205343.png

The main body of the dragon is created around our sine wave skeleton. By drawing lines between each point on the wave and offsetting them slightly, we get the dragon's main body and tail. The dragon is actually a bunch of line segments strung together, but they are so close together it appears seamless.

Scales

Screenshot 2025-01-13 205323.png

To give the dragon a little more detail, we will now add some scales. These follow the sine wave, just like the rest of the dragon, but they have no offset and therefore stay right in the middle. The scales are colored yellow to slightly contrast the red, but you can play around with the color if you like.

The Head

Screenshot 2025-01-13 205254.png

Now for the last part of the dragon - the head. Rather than trying to create the head using only Javascript (which would be a nightmare), I opted to create it using a paint app. Most computers come with one automatically installed, but you can also find some great online ones (which is where I created the head). If you don't want to make your own, you can just use the one I have here.

Clouds

Screenshot 2025-01-13 204617.png

As a finishing touch to really add to the atmosphere of the animation, let's add some clouds. My clouds are made with a single circular gradient (stored as an image and created in a paint app) that is randomly placed somewhere in a defined area. The whole area (and the points inside it) move across the screen, giving the idea that the dragon is flying. The clouds are semi-transparent, so they can be place in front of the dragon or behind it, whichever you want. All the cloud settings are adjustable in my code if you want to play around with them there.

Enjoy Your Animation!

If you've followed along and managed to create your own flying dragon, congratulations! If not, the code is available below. As mentioned before, you can control the cloud settings, but also the dragon's skeleton settings (things like wave speed and length). I thought this animation turned out pretty well for being coded in Javascript, and I felt like it was a good challenge. A lot of ideas had to be tinkered with and scrapped before this animation came into being, so I hope you enjoy it. Perhaps I'll make more Javascript animations, but until then, have a great rest of your day!

Downloads

Code and Files

dragon.png
cloud.png

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dragon</title>
<style>
body {
overflow:hidden;
margin:0px;
}
canvas {
background-image: linear-gradient(skyblue, rgb(41, 34, 171));
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="script.js"></script>
</body>
</html>

Javascript

const cloud = new Image(1, 1);
const dragon = new Image(1, 1);
cloud.src = 'cloud.png';
dragon.src = 'dragon.png';

class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
setDirection(dir) {
// dir is expected to be in radians
const length = this.length(); // Get the current length of the vector
this.x = length * Math.cos(dir); // Update x based on the new direction
this.y = length * Math.sin(dir); // Update y based on the new direction
}

setLength(n) {
const currentLength = this.length(); // Get the current length of the vector
if (currentLength !== 0) {
// Normalize the vector and then scale it to the new length
this.x = (this.x / currentLength) * n;
this.y = (this.y / currentLength) * n;
} else {
// If the current length is 0, just set it to the new length
this.x = n;
this.y = 0; // Arbitrarily set y to 0
}
}
setSlope(slope) {
// Calculate the angle from the slope
const angle = Math.atan(slope);
this.setDirection(angle); // Set the direction based on the calculated angle
}
add(v) {
this.x += v.x;
this.y += v.y;
}

length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
clone() {
return new Vector(this.x, this.y);
}
}
class Cloud {
constructor() {
this.pos = new Vector(window.innerWidth + 500 + (Math.random() * window.innerWidth + 500), Math.random() * window.innerHeight);
this.dir = this.pos.x > -499 ? -1 : 1;

this.points = [];
this.sizes = [];

this.speed = (Math.random() + 2) / 3 * 3;

this.isDead = false;
}
draw() {
if (this.points.length == 0) {
for (let i = 0; i < 5; i++) {
this.points.push(new Vector(Math.random() * 400, Math.random() * 200));
this.sizes.push((Math.random() + 2) / 3 * 500);
}
}

ctx.beginPath();
for (let i = 0; i < this.points.length; i++) {
ctx.drawImage(cloud, this.points[i].x + this.pos.x, this.points[i].y + this.pos.y, this.sizes[i], this.sizes[i]);
this.points[i].x += this.dir * this.speed;
}
ctx.closePath();

if (this.points[0].x + this.pos.x < -750) {
this.isDead = true;
}
}
}

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

const numSegments = 40;
const segmentRadius = 30;
const amplitude = 150;
const period = 360;
const length = 4; //number of full cycles (repeats every 2 * pi)
const numClouds = 10;

let speed = 1;
let t = 0;
let vector = new Vector(0, 0);
let oldVector = new Vector(0, 0);
let clouds = [];
function main() {
for (let s = 0; s < speed; s++) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

t++;

//clouds
while (clouds.length < numClouds) {
clouds.push(new Cloud());
}
for (let i = clouds.length - 1; i >= 0; i--) {
if (clouds[i].isDead) {
clouds.splice(i, 1);
}
clouds[i].draw();
}

//outline
ctx.lineWidth = 10;
ctx.strokeStyle = 'rgb(143, 3, 3)';
ctx.fillStyle = 'rgb(120, 0, 0)';
ctx.beginPath();
ctx.moveTo(((window.innerWidth - 300) * 0 / numSegments + 50), Math.sin(t * Math.PI * 2 / period + (length * 0 / numSegments)) * amplitude + window.innerHeight / 2);
for (let i = 0; i < numSegments; i++) {
ctx.lineTo(((window.innerWidth - 300) * i / numSegments + 50), Math.sin(t * Math.PI * 2 / period + (length * i / numSegments)) * amplitude + window.innerHeight / 2 + segmentRadius * (Math.min(1, Math.sqrt(i / 8))));
}
ctx.lineTo(((window.innerWidth - 300) + 75), Math.sin(t * Math.PI * 2 / period + (length)) * amplitude + window.innerHeight / 2 + segmentRadius);
for (let i = numSegments; i >= 0; i--) {
ctx.lineTo(((window.innerWidth - 300) * i / numSegments + 50), Math.sin(t * Math.PI * 2 / period + (length * i / numSegments)) * amplitude + window.innerHeight / 2 - segmentRadius * (Math.min(1, Math.sqrt(i / 8))));
}
ctx.closePath();
ctx.fill();
ctx.stroke();

//scales
ctx.strokeStyle = 'rgb(77, 3, 3)';
ctx.fillStyle = 'rgb(232, 204, 46)';
ctx.lineWidth = 3;
for (let i = 1; i < numSegments - 1; i += 2) {
ctx.beginPath();
ctx.arc(((window.innerWidth - 300) * i / numSegments + 50), Math.sin(t * Math.PI * 2 / period + (length * i / numSegments)) * amplitude + window.innerHeight / 2, segmentRadius / 5, 0, Math.PI * 2);
ctx.closePath();
ctx.stroke();
ctx.fill();
}

ctx.save();
ctx.translate(
((window.innerWidth - 300)) + segmentRadius * 2,
Math.sin(t * Math.PI * 2 / period + (length)) * amplitude + window.innerHeight / 2 - segmentRadius + segmentRadius * 0.7
);
ctx.rotate(Math.atan(Math.cos(t * Math.PI * 2 / period + length)));
ctx.beginPath();
ctx.drawImage(dragon, -segmentRadius * 2, -segmentRadius * 4, segmentRadius * 8, segmentRadius * 8);
ctx.closePath();
ctx.restore();

}
requestAnimationFrame(main);
}
main();