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) |