console
const MAX_WIDTH = 12;
const FPS = 60;
const getTimestamp = () => {
return (new Date()).getTime();
};
const random = (max = 1, signed = false) => {
return signed ? ((Math.random() - 0.5) * 2) * max : Math.random() * max;
};
const getPower = () => {
const power = (getTimestamp() - mouseStart) / 150;
return power > 30 ? 30 : power;
};
const shakeVolcano = (power) => {
$volcano.css({
left: random(power, true),
bottom: -1 * random(power)
});
$lava.css({
left: random(power, true),
top: 10 + (random(power) / 2),
width: random(power) + 260
});
};
const targetDelta = 1000 / FPS;
const stage = document.getElementById('stage');
const ctx = stage.getContext('2d');
const smoke = document.getElementById('smoke');
const ctx2 = smoke.getContext('2d');
let particles = [];
let AWESOME_MODE = false;
let mouseDown = false;
let isExploding = false;
let stageWidth = 0;
let stageHeight = 0;
let previousTimestamp = getTimestamp();
let previousRender = getTimestamp();
let previousPower = 0;
let mouseStart = getTimestamp();
let mouseEnd = getTimestamp();
let $volcano;
let $lava;
const generateParticles = (amount = 20, power) => {
for (let i = 0; i < amount; i++) {
particles.push(new Particle(power));
}
};
const loop = () => {
if (getTimestamp() - previousTimestamp < targetDelta) {
requestAnimationFrame(loop);
return;
}
ctx.globalCompositeOperation = 'lighter';
if (!AWESOME_MODE) {
ctx.clearRect(0, 0, stageWidth, stageHeight);
}
ctx2.clearRect(0, 0, stageWidth, stageHeight);
if (mouseDown) {
generateParticles(random(2) + 1, getPower() / 2);
shakeVolcano(getPower());
}
if (isExploding && previousPower > 0 && !mouseDown) {
shakeVolcano(previousPower);
previousPower -= 0.35;
if (previousPower < 1) {
isExploding = false;
}
}
if (random() < 0.3) {
generateParticles(1, 1);
}
if (random() < 0.08) {
particles.push(new Smoke());
}
particles.forEach((particle) => {
particle.animate();
particle.render();
});
particles = particles.filter(particle => {
if (
particle instanceof Smoke &&
particle.y + particle.width > 0
) {
return true;
} else if (
particle instanceof Particle &&
particle.y < stageHeight &&
particle.x > 0 - particle.width &&
particle.x < stageWidth + particle.width
) {
return true;
} else {
return false;
}
});
previousTimestamp = getTimestamp();
requestAnimationFrame(loop);
};
class Particle {
constructor(oPower) {
const power = oPower || random(5);
this.x = (stageWidth / 2) + random(80, true);
this.y = (stageHeight - 200) + random(20, true);
this.width = random(MAX_WIDTH) + 1;
this.red = Math.floor(210 + (this.width * 2));
this.green = Math.floor(90 + (this.width * 3));
this.blue = Math.floor(30 + (this.width * 2));
this.alpha = 1;
this.speed = power / (this.width * 0.13);
this.angle = random(45);
this.hasBounced = false;
this.velocityY = Math.abs(Math.sin(this.angle)) * this.speed;
this.velocityX = Math.cos(this.angle) * this.speed / 2;
this.xDirection = this.velocityX > 0;
if (Math.abs(this.velocityX) > 2) {
this.velocityX = this.velocityX / 2;
}
}
animate() {
this.x -= this.velocityX;
this.y -= this.velocityY;
this.velocityY -= this.width / (this.hasBounced ? 170 : 100);
if (
!this.hasBounced &&
random() < 0.1 &&
this.y > stageHeight - 150 &&
this.x > (stageWidth / 2) - 180 &&
this.x < (stageWidth / 2) + 180
) {
this.hasBounced = true;
this.velocityX = Math.sin(random(45)) + (random(2) * this.xDirection);
this.velocityY /= 8;
}
}
render() {
const colour = this.getColour();
ctx.beginPath();
ctx.arc(this.x, this.y, this.width, 0, Math.PI * 2, true);
ctx.lineWidth = this.width;
ctx.fillStyle = colour;
ctx.fill();
}
getColour(red, green, blue, alpha) {
return `rgba(${red || this.red}, ${green || this.green}, ${blue || this.blue}, ${alpha || this.alpha})`;
}
}
class Smoke {
constructor() {
this.x = (stageWidth / 2) + random(75, true);
this.y = stageHeight;
this.width = random(80) + 50;
this.red = 100;
this.green = 100;
this.blue = 100;
this.alpha = random() + 0.3;
this.speed = random(2) + 1;
}
animate() {
this.y -= this.speed;
}
render() {
const colour = this.getColour();
ctx2.beginPath();
ctx2.arc(this.x, this.y, this.width, 0, Math.PI * 2, true);
ctx2.lineWidth = this.width;
ctx2.fillStyle = colour;
ctx2.fill();
}
getColour(red, green, blue, alpha) {
return `rgba(${red || this.red}, ${green || this.green}, ${blue || this.blue}, ${alpha || this.alpha})`;
}
}
const updateCanvasSize = () => {
stageWidth = window.innerWidth;
stageHeight = window.innerHeight;
stage.width = stageWidth;
stage.height = stageHeight;
smoke.width = stageWidth;
smoke.height = stageHeight;
particles = [];
};
$(window).on('mousedown', (e) => {
mouseStart = getTimestamp();
mouseDown = true;
if (AWESOME_MODE) {
particles = [];
ctx.clearRect(0, 0, stageWidth, stageHeight);
}
});
$(window).on('mouseup', (e) => {
const power = getPower();
isExploding = true;
mouseDown = false;
mouseEnd = getTimestamp();
previousPower = power;
generateParticles((random(16) + 30) * power, power / 1.1);
setTimeout(() => {
generateParticles(14 * power, power / 1.4);
}, 100);
setTimeout(() => {
generateParticles(6 * power, power / 2);
}, 200);
setTimeout(() => {
generateParticles(4 * power, power / 2);
}, 400);
});
updateCanvasSize();
$(window).on('resize', updateCanvasSize);
$(() => {
$volcano = $('.volcano-container');
$lava = $volcano.find('.lava');
setTimeout(() => {
$('.toggle').fadeIn();
}, 6000);
$('.toggle').on('click', () => {
AWESOME_MODE = $('#awesome').is(':checked');
});
});
generateParticles(200, 8);
loop();
<canvas id="smoke"></canvas>
<canvas id="stage"></canvas>
<div class="volcano-container">
<div class="volcano"></div>
<div class="volcano flipped"></div>
<div class="lip">
<div class="lava"></div>
<div class="mask"></div>
</div>
</div>
<div class="toggle">
<label for="awesome">
<input type="checkbox" id="awesome" /> <strong>AWESOME MODE</strong>
</label>
</div>
$volcanoColour: #240904;
$lavaColour: #fa7510;
html,
body {
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
font-family: Helvetica, Arial;
color: #ffffff;
}
body {
background: linear-gradient(0deg, #aabbbb, #88aadd);
}
#stage, #smoke {
position: absolute;
top: 0;
left: 0;
}
#stage {
z-index: 20;
}
#smoke {
z-index: 5;
}
.volcano-container {
z-index: 10;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
.volcano {
position: absolute;
border: 380px solid transparent;
border-left: 140px solid transparent;
border-bottom: 200px solid $volcanoColour;
width: 0;
height: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
&.flipped {
transform: scaleX(-1);
}
}
.lip {
position: absolute;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: 300px;
height: 240px;
background-color: $volcanoColour;
border-radius: 200px;
.lava {
position: absolute;
left: 0;
right: 0;
top: 10px;
margin: auto;
width: 270px;
height: 230px;
background-color: $lavaColour;
border-radius: 200px;
clip-path: ellipse(120px 60px at top);
}
.mask {
position: absolute;
top: 80px;
left: 0;
right: 0;
margin: auto;
background-color: $volcanoColour;
width: 290px;
height: 300px;
}
}
.toggle {
display: none;
z-index: 100;
position: absolute;
top: 10px;
left: 10px;
}