Last active
September 11, 2021 07:02
-
-
Save sharpicx/5ad8a8a8381a2509a9ea5172793e8444 to your computer and use it in GitHub Desktop.
fixed rain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
document.addEventListener("DOMContentLoaded", function() { | |
demo.init(); | |
window.addEventListener('resize', demo.resize); | |
}); | |
// demo namespace | |
var demo = { | |
// CUSTOMIZABLE PROPERTIES | |
// - physics speed multiplier: allows slowing down or speeding up simulation | |
speed: 1, | |
// - color of particles | |
color: { | |
r: '80', | |
g: '175', | |
b: '255', | |
a: '0.5' | |
}, | |
// END CUSTOMIZATION | |
// whether demo is running | |
started: false, | |
// canvas and associated context references | |
canvas: null, | |
ctx: null, | |
// viewport dimensions (DIPs) | |
width: 0, | |
height: 0, | |
// devicePixelRatio alias (should only be used for rendering, physics shouldn't care) | |
dpr: window.devicePixelRatio || 1, | |
// time since last drop | |
drop_time: 2, | |
// ideal time between drops (changed with mouse/finger) | |
drop_delay: 25, | |
// wind applied to rain (changed with mouse/finger) | |
wind: 0, | |
// color of rain (set in init) | |
rain_color: null, | |
rain_color_clear: null, | |
// rain particles | |
rain: [], | |
rain_pool: [], | |
// rain droplet (splash) particles | |
drops: [], | |
drop_pool: [] | |
}; | |
// demo initialization (should only run once) | |
demo.init = function() { | |
if (!demo.started) { | |
demo.started = true; | |
demo.canvas = document.getElementById('canvas'); | |
demo.ctx = demo.canvas.getContext('2d'); | |
var c = demo.color; | |
demo.rain_color = 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + c.a + ')'; | |
demo.rain_color_clear = 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',0)'; | |
demo.resize(); | |
Ticker.addListener(demo.step); | |
// | |
const gui = new dat.GUI(); | |
gui.add(demo, 'speed', 0.2, 2); | |
// fade out instructions after a few seconds | |
var instructions = document.getElementById('instructions'); | |
setTimeout(function() { | |
instructions.style.opacity = 0; | |
setTimeout(function(){ | |
instructions.parentNode.removeChild(instructions); | |
}, 2000); | |
}, 4000); | |
} | |
} | |
// (re)size canvas (clears all particles) | |
demo.resize = function() { | |
// localize common references | |
var rain = demo.rain; | |
var drops = demo.drops; | |
// recycle particles | |
for (var i = rain.length - 1; i >= 0; i--) { | |
rain.pop().recycle(); | |
} | |
for (var i = drops.length - 1; i >= 0; i--) { | |
drops.pop().recycle(); | |
} | |
// resize | |
demo.width = window.innerWidth; | |
demo.height = window.innerHeight; | |
demo.canvas.width = demo.width * demo.dpr; | |
demo.canvas.height = demo.height * demo.dpr; | |
} | |
demo.step = function(time, lag) { | |
// localize common references | |
var demo = window.demo; | |
var speed = demo.speed; | |
var width = demo.width; | |
var height = demo.height; | |
var wind = demo.wind; | |
var rain = demo.rain; | |
var rain_pool = demo.rain_pool; | |
var drops = demo.drops; | |
var drop_pool = demo.drop_pool; | |
// multiplier for physics | |
var multiplier = speed * lag; | |
// spawn drops | |
demo.drop_time += time * speed; | |
while (demo.drop_time > demo.drop_delay) { | |
demo.drop_time -= demo.drop_delay; | |
var new_rain = rain_pool.pop() || new Rain(); | |
new_rain.init(); | |
var wind_expand = Math.abs(height / new_rain.speed * wind); // expand spawn width as wind increases | |
var spawn_x = Math.random() * (width + wind_expand); | |
if (wind > 0) spawn_x -= wind_expand; | |
new_rain.x = spawn_x; | |
rain.push(new_rain); | |
} | |
// rain physics | |
for (var i = rain.length - 1; i >= 0; i--) { | |
var r = rain[i]; | |
r.y += r.speed * r.z * multiplier; | |
r.x += r.z * wind * multiplier; | |
// remove rain when out of view | |
if (r.y > height) { | |
// if rain reached bottom of view, show a splash | |
r.splash(); | |
} | |
// recycle rain | |
if (r.y > height + Rain.height * r.z || (wind < 0 && r.x < wind) || (wind > 0 && r.x > width + wind)) { | |
r.recycle(); | |
rain.splice(i, 1); | |
} | |
} | |
// splash drop physics | |
var drop_max_speed = Drop.max_speed; | |
for (var i = drops.length - 1; i >= 0; i--) { | |
var d = drops[i]; | |
d.x += d.speed_x * multiplier; | |
d.y += d.speed_y * multiplier; | |
// apply gravity - magic number 0.3 represents a faked gravity constant | |
d.speed_y += 0.3 * multiplier; | |
// apply wind (but scale back the force) | |
d.speed_x += wind / 25 * multiplier; | |
if (d.speed_x < -drop_max_speed) { | |
d.speed_x = -drop_max_speed; | |
}else if (d.speed_x > drop_max_speed) { | |
d.speed_x = drop_max_speed; | |
} | |
// recycle | |
if (d.y > height + d.radius) { | |
d.recycle(); | |
drops.splice(i, 1); | |
} | |
} | |
demo.draw(); | |
} | |
demo.draw = function() { | |
// localize common references | |
var demo = window.demo; | |
var width = demo.width; | |
var height = demo.height; | |
var dpr = demo.dpr; | |
var rain = demo.rain; | |
var drops = demo.drops; | |
var ctx = demo.ctx; | |
// start fresh | |
ctx.clearRect(0, 0, width*dpr, height*dpr); | |
// draw rain (trace all paths first, then stroke once) | |
ctx.beginPath(); | |
var rain_height = Rain.height * dpr; | |
for (var i = rain.length - 1; i >= 0; i--) { | |
var r = rain[i]; | |
var real_x = r.x * dpr; | |
var real_y = r.y * dpr; | |
ctx.moveTo(real_x, real_y); | |
// magic number 1.5 compensates for lack of trig in drawing angled rain | |
ctx.lineTo(real_x - demo.wind * r.z * dpr * 1.5, real_y - rain_height * r.z); | |
} | |
ctx.lineWidth = Rain.width * dpr; | |
ctx.strokeStyle = demo.rain_color; | |
ctx.stroke(); | |
// draw splash drops (just copy pre-rendered canvas) | |
for (var i = drops.length - 1; i >= 0; i--) { | |
var d = drops[i]; | |
var real_x = d.x * dpr - d.radius; | |
var real_y = d.y * dpr - d.radius; | |
ctx.drawImage(d.canvas, real_x, real_y); | |
} | |
} | |
// Rain definition | |
function Rain() { | |
this.x = 0; | |
this.y = 0; | |
this.z = 0; | |
this.speed = 25; | |
this.splashed = false; | |
} | |
Rain.width = 2; | |
Rain.height = 25; | |
Rain.prototype.init = function() { | |
this.y = Math.random() * -100; | |
this.z = Math.random() * 0.5 + 0.5; | |
this.splashed = false; | |
} | |
Rain.prototype.recycle = function() { | |
demo.rain_pool.push(this); | |
} | |
// recycle rain particle and create a burst of droplets | |
Rain.prototype.splash = function() { | |
if (!this.splashed) { | |
this.splashed = true; | |
var drops = demo.drops; | |
var drop_pool = demo.drop_pool; | |
for (var i=0; i<16; i++) { | |
var drop = drop_pool.pop() || new Drop(); | |
drops.push(drop); | |
drop.init(this.x); | |
} | |
} | |
} | |
// Droplet definition | |
function Drop() { | |
this.x = 0; | |
this.y = 0; | |
this.radius = Math.round(Math.random() * 2 + 1) * demo.dpr; | |
this.speed_x = 0; | |
this.speed_y = 0; | |
this.canvas = document.createElement('canvas'); | |
this.ctx = this.canvas.getContext('2d'); | |
// render once and cache | |
var diameter = this.radius * 2; | |
this.canvas.width = diameter; | |
this.canvas.height = diameter; | |
var grd = this.ctx.createRadialGradient(this.radius, this.radius , 1, this.radius, this.radius, this.radius); | |
grd.addColorStop(0, demo.rain_color); | |
grd.addColorStop(1, demo.rain_color_clear); | |
this.ctx.fillStyle = grd; | |
this.ctx.fillRect(0, 0, diameter, diameter); | |
} | |
Drop.max_speed = 2; | |
Drop.prototype.init = function(x) { | |
this.x = x; | |
this.y = demo.height; | |
var angle = Math.random() * Math.PI - (Math.PI * 0.5); | |
var speed = Math.random() * Drop.max_speed; | |
this.speed_x = Math.sin(angle) * speed; | |
this.speed_y = -Math.cos(angle) * speed; | |
} | |
Drop.prototype.recycle = function() { | |
demo.drop_pool.push(this); | |
} | |
// Frame ticker helper module | |
var Ticker = (function(){ | |
var PUBLIC_API = {}; | |
// public | |
// will call function reference repeatedly once registered, passing elapsed time and a lag multiplier as parameters | |
PUBLIC_API.addListener = function addListener(fn) { | |
if (typeof fn !== 'function') throw('Ticker.addListener() requires a function reference passed in.'); | |
listeners.push(fn); | |
// start frame-loop lazily | |
if (!started) { | |
started = true; | |
queueFrame(); | |
} | |
}; | |
// private | |
var started = false; | |
var last_timestamp = 0; | |
var listeners = []; | |
// queue up a new frame (calls frameHandler) | |
function queueFrame() { | |
if (window.requestAnimationFrame) { | |
requestAnimationFrame(frameHandler); | |
} else { | |
webkitRequestAnimationFrame(frameHandler); | |
} | |
} | |
function frameHandler(timestamp) { | |
var frame_time = timestamp - last_timestamp; | |
last_timestamp = timestamp; | |
// make sure negative time isn't reported (first frame can be whacky) | |
if (frame_time < 0) { | |
frame_time = 17; | |
} | |
// - cap minimum framerate to 15fps[~68ms] (assuming 60fps[~17ms] as 'normal') | |
else if (frame_time > 68) { | |
frame_time = 68; | |
} | |
// fire custom listeners | |
for (var i = 0, len = listeners.length; i < len; i++) { | |
listeners[i].call(window, frame_time, frame_time / 16.67); | |
} | |
// always queue another frame | |
queueFrame(); | |
} | |
return PUBLIC_API; | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment