SOURCE

console 命令行工具 X clear

                    
>
console
// Best viewed in Chrome

//=============================
// Consts
//=============================
const MAX_WIDTH = 12;
const FPS = 60;

//=============================
// Helpers
//=============================
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
  });
};

//=============================
// Main
//=============================
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;
    }
  }
  
  // constant particles
  if (random() < 0.3) {
    generateParticles(1, 1);
  }
  
  // smoke effects
  if (random() < 0.08) {
    particles.push(new Smoke());
  }
  
  // animate
  particles.forEach((particle) => {
    particle.animate();
    particle.render();
  });
  
  // remove out of bounds particles
  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;
    
    // add gravity
    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.shadowBlur = random(20);
    //ctx.shadowColor = 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.x += random(2, true);
    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})`;
  }
}

//=============================
// Setup
//=============================

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');
  });
});


//=============================
// Run it!
//=============================

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;
  //z-index: 50;
  
  &.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 for Firefox
  .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;
}