Snowfall made with native WebGL shader
A Pen by Travis Wagner on CodePen.
// img#snowflake(src="https://cdn.jsdelivr.net/gh/trvswgnr/cdn/snowflake.png") | |
#snow |
const holder = document.querySelector('#snow'); | |
const count = 3000; | |
let wind = { | |
current: 0, | |
force: 0.01, | |
target: 0.1, | |
min: 0.1, | |
max: 0.1, | |
easing: 0.005 | |
}; | |
// Create a function to load the image | |
function loadImage(url) { | |
return new Promise((resolve, reject) => { | |
if (url.startsWith("data")) { | |
resolve(url); | |
} | |
const img = new Image(); | |
img.crossOrigin = "anonymous"; // Enable CORS if needed | |
img.onload = () => { | |
// Create a canvas to convert the image to a data URL | |
const canvas = document.createElement('canvas'); | |
canvas.width = img.width; | |
canvas.height = img.height; | |
const ctx = canvas.getContext('2d'); | |
ctx.drawImage(img, 0, 0); | |
// Get the data URL | |
const dataUrl = canvas.toDataURL('image/png'); | |
resolve(dataUrl); | |
}; | |
img.onerror = () => { | |
reject(new Error(`Failed to load image: ${url}`)); | |
}; | |
img.src = url; | |
}); | |
} | |
// Initialize the snow effect | |
async function initSnow(imageUrl) { | |
try { | |
// Load and convert the image | |
const textureDataUrl = await loadImage(imageUrl); | |
// Create the shader program | |
const snow = new ShaderProgram(holder, { | |
depthTest: false, | |
texture: textureDataUrl, | |
uniforms: { | |
worldSize: { type: 'vec3', value: [0, 0, 0] }, | |
gravity: { type: 'float', value: 100 }, | |
wind: { type: 'float', value: 0 }, | |
}, | |
buffers: { | |
size: { size: 1, data: [] }, | |
rotation: { size: 3, data: [] }, | |
speed: { size: 3, data: [] }, | |
}, | |
vertex: ` | |
precision highp float; | |
attribute vec4 a_position; | |
attribute vec4 a_color; | |
attribute vec3 a_rotation; | |
attribute vec3 a_speed; | |
attribute float a_size; | |
uniform float u_time; | |
uniform vec2 u_mousemove; | |
uniform vec2 u_resolution; | |
uniform mat4 u_projection; | |
uniform vec3 u_worldSize; | |
uniform float u_gravity; | |
uniform float u_wind; | |
varying vec4 v_color; | |
varying float v_rotation; | |
void main() { | |
v_color = a_color; | |
v_rotation = a_rotation.x + u_time * a_rotation.y; | |
vec3 pos = a_position.xyz; | |
pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x; | |
pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y; | |
pos.x += sin(u_time * a_speed.z) * a_rotation.z; | |
pos.z += cos(u_time * a_speed.z) * a_rotation.z; | |
gl_Position = u_projection * vec4(pos.xyz, a_position.w); | |
gl_PointSize = (a_size / gl_Position.w) * 100.0; | |
}`, | |
fragment: ` | |
precision highp float; | |
uniform sampler2D u_texture; | |
varying vec4 v_color; | |
varying float v_rotation; | |
void main() { | |
vec2 rotated = vec2( | |
cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5, | |
cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5 | |
); | |
vec4 snowflake = texture2D(u_texture, rotated); | |
gl_FragColor = vec4(snowflake.rgb, snowflake.a * v_color.a); | |
}`, | |
onResize(w, h, dpi) { | |
const position = [], color = [], size = [], rotation = [], speed = []; | |
const height = 110; | |
const width = w / h * height; | |
const depth = 80; | |
Array.from({ length: w / h * count }, snowflake => { | |
position.push( | |
-width + Math.random() * width * 2, | |
-height + Math.random() * height * 2, | |
Math.random() * depth * 2 | |
); | |
speed.push( | |
1 + Math.random(), | |
1 + Math.random(), | |
Math.random() * 10 | |
); | |
rotation.push( | |
Math.random() * 2 * Math.PI, | |
Math.random() * 20, | |
Math.random() * 10 | |
); | |
color.push( | |
1, | |
1, | |
1, | |
0.1 + Math.random() * 0.2 | |
); | |
size.push( | |
5 * Math.random() * 5 * (h * dpi / 1000) | |
); | |
}); | |
this.uniforms.worldSize = [width, height, depth]; | |
this.buffers.position = position; | |
this.buffers.color = color; | |
this.buffers.rotation = rotation; | |
this.buffers.size = size; | |
this.buffers.speed = speed; | |
}, | |
onUpdate(delta) { | |
wind.force += (wind.target - wind.force) * wind.easing; | |
wind.current += wind.force * (delta * 0.2); | |
this.uniforms.wind = wind.current; | |
if (Math.random() > 0.995) { | |
wind.target = (wind.min + Math.random() * (wind.max - wind.min)) * (Math.random() > 0.5 ? -1 : 1); | |
} | |
}, | |
}); | |
} catch (error) { | |
console.error('Failed to initialize snow effect:', error); | |
} | |
} | |
// Usage example: | |
// initSnow('https://cdn.jsdelivr.net/gh/trvswgnr/cdn/snowflake.png'); | |
initSnow("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAQAAABuvaSwAAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAE7SURBVCjPnZMvcoNAGMWhdJgGhkYkw1CBSyaCGURcTDSGih4gFwiOE0RGIivTK2C5ArKVkXE9w+tvKS2223nm8b3f7n77B0fOr1zdIU/3o7zh252ICXQHzNdMgUIU4PxhiPszYJrTgIHmWijWE4pxcypmwDj/hD4o0lKp1sqUowyXUolIRvwbNuijEq201V6FnlGB21JJSAw+wKZXn/GJNtqp1EGValThSiobkgiC3p2hhYDlVgQvOuqkRq+owR2p7EiWELRiYJ+NpCxZEp71plYdanFnKiVJCuEb2OOIFmxmz7InXcB6faAed6FyIFlDzOQ5QxMxey/osmG+Xlfd0BXXUqlIMggaMXDIqebsv6bTTu+An+iG66jUJDlE+A/Yog2rDVodndWlWF231UOyfKJWj9/yt/rjD/sF+2xJiwvIrtcAAAASdEVYdEVYSUY6T3JpZW50YXRpb24AMYRY7O8AAAAASUVORK5CYII=") |
<script src="https://codepen.io/bsehovac/pen/mddZWPw.js"></script> |
Snowfall made with native WebGL shader
A Pen by Travis Wagner on CodePen.
#snow | |
display: block | |
position: fixed | |
left: 0 | |
top: 0 | |
right: 0 | |
bottom: 0 | |
background-color: #000000 | |
background-image: linear-gradient(to bottom, #000000, #050505) |