/**
* @module snowfall
*/
const vec2 = require('./vec2')
const { lerp } = require('./math')
const appContainer = document.querySelector('#snow-container')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let gravity = vec2.create(0, 0.7)
let wind = vec2.create(0, 0)
let density = 200
let snowflakes = []
let bg = '#0d0014'
let primary = '#8d90b7'
let secondary = '#ffffff'
let amplitude = 1.0
let frequency = 0.02
let fadeIn = false
let scroll = false
let paused = false
/**
* @param {Object} config - A config, possibly from the Visual Config Editor.
* @param {string} [config.bg = '#0d0014'] - A hex string representing the
* Background Colour of the canvas.
* @param {string} [config.primary = '#8d90b7'] - A hex string representing the
* colour of the snowflakes in the foreground.
* @param {string} [config.secondary = '#ffffff'] - A hex string representing
* the colour of the snowflakes in the background.
* @param {number} [config.density = 200] - A number representing the required
* density of snowflakes on screen. Note, this is not the actual number of
* snowflakes.
* @param {Boolean} [config.fadeIn = false] - Should the snowflakes grow in size
* when the app starts or should they begin at their full size?
* @param {Boolean} [config.scroll = false] - Should the snowflakes scroll when
* the user scrolls up and down the page?
*
* @param {Object} config.wave - Configure the wave motion of the snowflakes.
* @param {number} [config.wave.frequency = 0.02] - The frequency of the wave
* the snowflakes follow.
* @param {number} [config.wave.amplitude = 1.0] - The amplitude of the wave the
* snowflakes follow.
*
* @param {Object} config.gravity - Configure the gravity of the simulation.
* @param {number} [config.gravity.angle = 90] - The angle of gravity, in
* degrees.
* @param {number} [config.gravity.strength = 0.7] - The strength of gravity.
*
* @param {Object} config.wind - Configure the wind.
* @param {number} [config.wind.angle = 0] - The angle of the wind, in degrees.
* @param {number} [config.wind.strength = 0] - The strength of the wind.
*/
function start(config = {}) {
if (config.bg !== undefined) {
bg = config.bg
}
if (config.primary !== undefined) {
primary = config.primary
}
if (config.secondary !== undefined) {
secondary = config.secondary
}
if (config.density !== undefined) {
density = config.density
}
if (config.fadeIn !== undefined) {
fadeIn = config.fadeIn
}
if (config.scroll !== undefined) {
scroll = config.scroll
}
if (config.wave !== undefined) {
if (config.wave.amplitude !== undefined) {
amplitude = config.wave.amplitude
}
if (config.wave.frequency !== undefined) {
frequency = config.wave.frequency
}
}
if (config.gravity !== undefined) {
if (
config.gravity.angle !== undefined &&
config.gravity.strength !== undefined
) {
setGravity(config.gravity.angle, config.gravity.strength)
}
if (
config.gravity.angle !== undefined &&
config.gravity.strength === undefined
) {
setGravity(config.gravity.angle, 0.7)
}
if (
config.gravity.angle === undefined &&
config.gravity.strength !== undefined
) {
setGravity(90, config.gravity.strength)
}
}
if (config.wind !== undefined) {
if (config.wind.angle !== undefined && config.wind.strength !== undefined) {
setWind(config.wind.angle, config.wind.strength)
}
if (config.wind.angle !== undefined && config.wind.strength === undefined) {
setWind(config.wind.angle, 0.0)
}
if (config.wind.angle === undefined && config.wind.strength !== undefined) {
setWind(0.0, config.wind.strength)
}
}
canvas.width = appContainer.offsetWidth
canvas.height = appContainer.offsetHeight
appContainer.appendChild(canvas)
snowflakes = makeSnowflakes(requiredSnowflakes())
window.onresize = onResize
window.requestAnimationFrame(onEnterFrame)
}
/**
* Set the background colour
*
* @param {string} colour - The background colour of the Canvas
*/
function setBackground(col) {
bg = col
}
/**
* Sets the colour of the Snowflakes in the foreground
*
* @param {string} colour - A Hex string representing the colour of the
* foreground snow.
*/
function setPrimary(col) {
primary = col
}
/**
* Sets the colour of the Snowflakes in the background
*
* @param {string} colour - A Hex string representing the colour of the
* background snow.
*/
function setSecondary(col) {
secondary = col
}
/**
* Set the density of the Snowflakes. This is the number of snowflakes on screen
* at a resolution of 1280 x 1080, but this number is scaled up and down at
* higher and lower resolutions respectively to give a consistent look when
* resizing.
*
* Setting this restarts the simulation.
*
* @param {number} density - A number representing the density of snowflakes.
*/
function setDensity(den) {
density = den
restart()
}
/**
* Should the snowflakes grow in size from nothing until they reach their full
* size? It happens pretty quickly.
*
* Setting this restarts the simulation.
*
* @param {Boolean} value - Yes or no?
*/
function setFade(val) {
fadeIn = val
restart()
}
/**
* Should the snowflakes scroll up and down the page as the User scrolls?
* @param {Boolean} value - Yes or no?
*/
function setScroll(val) {
scroll = val
}
/**
* Set the Amplitude of the Wave motion of the Snowflakes
*
* @param {number} amplitude - The Amplitude to set
*/
function setAmplitude(num) {
amplitude = num
}
/**
* Set the Frequency of the Wave motion of the Snowflakes.
*
* @param {number} frequency - The frequency to set
*/
function setFrequency(freq) {
frequency = freq
}
/**
* Set the angle and strength of gravity in the simulation.
*
* @param {number} angle - The angle of gravity, in degrees
* @param {number} strength - The strength of the gravity
*/
function setGravity(degrees, strength) {
gravity = vec2.fromDegrees(degrees)
gravity.multiplyScalar(strength)
}
/**
* Set the angle and strength of the wind in the simulation.
*
* @param {number} angle - The angle of the wind, in degrees
* @param {number} strength - The strength of the wind
*/
function setWind(degrees, strength) {
wind = vec2.fromDegrees(degrees)
wind.multiplyScalar(strength)
}
/**
* Set this to true to prevent the update.
* Set it to true to continue from where we left off.
*
* @param {boolean} pause - If the simulation should be halted or not
*/
function setPaused(pause) {
paused = pause
}
/**
* Pause/unpause the snowfall update loop
*/
function togglePaused() {
paused = !paused
}
function onResize() {
canvas.width = appContainer.offsetWidth
canvas.height = appContainer.offsetHeight
snowflakes = makeSnowflakes(requiredSnowflakes())
}
function onEnterFrame() {
if (!paused) {
update()
render()
}
window.requestAnimationFrame(onEnterFrame)
}
let t = 0
const w = vec2.create(0, 0)
const g = vec2.create(0, 0)
function update() {
snowflakes.forEach(snowflake => {
// add the wind
w.x = wind.x
w.y = wind.y
w.multiplyScalar(snowflake.size + snowflake.random)
snowflake.pos.add(w)
// add gravity
g.x = gravity.x
g.y = gravity.y
g.multiplyScalar(snowflake.size + snowflake.random)
snowflake.pos.add(g)
// add the wave motion
const phase = snowflake.noise
let sine = vec2.create(amplitude * Math.sin(frequency * t + phase), 0)
snowflake.pos.add(sine)
// wrap the snowflakes when they move off screen
if (snowflake.pos.x > canvas.width) {
snowflake.pos.x = 0
}
if (snowflake.pos.x < 0) {
snowflake.pos.x = canvas.width
}
if (snowflake.pos.y > canvas.height) {
snowflake.pos.y = snowflake.pos.y - canvas.height
snowflake.pos.x = Math.random() * canvas.width
}
if (snowflake.pos.y < 0) {
snowflake.pos.y = canvas.height - snowflake.pos.y
snowflake.pos.x = Math.random() * canvas.width
}
if (snowflake.renderedSize < snowflake.size) {
snowflake.renderedSize = lerp(
snowflake.renderedSize,
snowflake.size,
0.025
)
}
})
previousPageYOffset = window.pageYOffset
t += 1
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (bg) {
ctx.fillStyle = bg
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
const bgSize = 7
const foreground = snowflakes.filter(x => x.size >= bgSize)
const background = snowflakes.filter(x => x.size < bgSize)
ctx.fillStyle = primary
background.forEach(snowflake => {
ctx.beginPath()
drawCircle(snowflake.pos, snowflake.renderedSize)
ctx.fill()
})
ctx.fillStyle = secondary
foreground.forEach(snowflake => {
ctx.beginPath()
drawCircle(snowflake.pos, snowflake.renderedSize)
ctx.fill()
})
}
function makeSnowflakes(num) {
let result = []
while (num--) {
const size = 3 + Math.random() * 5
const renderedSize = fadeIn === true ? 0 : size
result.push({
pos: vec2.create(
Math.random() * canvas.width,
Math.random() * canvas.height
),
size,
renderedSize,
// Random value, just to add some uncertainty
noise: Math.random() * 10,
amplitude: Math.random() * 2,
frequency: Math.random() * 0.01,
random: Math.random()
})
}
return result
}
// This function figures out how many snowflakes we should use for our given
// canvas size.
//
// Just setting a fixed number of snowflakes would give an uneven distribution
// of snowflakes across different screen sizes, for example.
function requiredSnowflakes() {
const tenEightyPee = 1920 * 1080
const thisScreen = canvas.width * canvas.height
const snowflakeCount = Math.round(density * (thisScreen / tenEightyPee))
return snowflakeCount
}
function drawCircle(position, radius) {
ctx.arc(position.x, position.y, radius, 0, 2 * Math.PI, false)
}
function restart() {
snowflakes = makeSnowflakes(requiredSnowflakes())
}
module.exports = {
setAmplitude,
setBackground,
setDensity,
setFade,
setScroll,
setFrequency,
setGravity,
setPrimary,
setSecondary,
setWind,
setPaused,
togglePaused,
start
}