Thousands of particles that create waves of light. So beautiful.
A Pen by Karl Lindberg on CodePen.
Thousands of particles that create waves of light. So beautiful.
A Pen by Karl Lindberg on CodePen.
| <canvas></canvas> | |
| <footer> | |
| Made with 🐳 | |
| </footer> |
| // Do you like rainbow waves? | |
| const rainbow = false; | |
| // Need more performance? | |
| const HD = true; | |
| const canvas = document.querySelector("canvas"); | |
| const background = document.querySelector(".background"); | |
| const bar = document.querySelector(".progress"); | |
| const initialize = vertices => { | |
| const pixelRatio = HD ? window.devicePixelRatio : 1; | |
| const rows = HD ? 150 : 100; | |
| const multiplier = rows * rows; | |
| const duration = 0.4; | |
| const geometry = [{ x: 0, y: 0, z: 0 }]; | |
| const pointSize = (HD ? 6 : 2).toFixed(1); | |
| let step = 0.004; | |
| const size = 5; | |
| const attributes = [ | |
| { | |
| name: "aPositionStart", | |
| data: (i, total) => [ | |
| size - ((i % rows) / rows + 0.5 / rows) * (size * 2), | |
| -1, | |
| (size - (Math.floor(i / rows) / rows + 0.5 / rows) * size * 2) * -1 | |
| ] | |
| }, | |
| { | |
| name: "aControlPointOne", | |
| data: i => [ | |
| size - ((i % rows) / rows + 0.5 / rows) * (size * 2), | |
| -0.5 + getRandom(0.2), | |
| (size - (Math.floor(i / rows) / rows + 0.5 / rows) * size * 2) * -1 | |
| ] | |
| }, | |
| { | |
| name: "aControlPointTwo", | |
| data: i => [ | |
| size - ((i % rows) / rows + 0.5 / rows) * (size * 2), | |
| -0.5 + getRandom(0.2), | |
| (size - (Math.floor(i / rows) / rows + 0.5 / rows) * size * 2) * -1 | |
| ] | |
| }, | |
| { | |
| name: "aPositionEnd", | |
| data: i => [ | |
| size - ((i % rows) / rows + 0.5 / rows) * (size * 2), | |
| -1, | |
| (size - (Math.floor(i / rows) / rows + 0.5 / rows) * size * 2) * -1 | |
| ] | |
| }, | |
| { | |
| name: "aOffset", | |
| data: i => [i * ((1 - duration) / (multiplier - 1))] | |
| }, | |
| { | |
| name: "aColor", | |
| data: (i, total) => getHSL( | |
| rainbow ? i / total * 1.0 : 0.5 + i / total * 0.4, | |
| 0.5, | |
| 0.5 | |
| ) | |
| } | |
| ]; | |
| const uniforms = [ | |
| { | |
| name: "uProgress", | |
| type: "float", | |
| value: 0.8 | |
| } | |
| ]; | |
| const vertexShader = ` | |
| attribute vec3 aPositionStart; | |
| attribute vec3 aControlPointOne; | |
| attribute vec3 aControlPointTwo; | |
| attribute vec3 aPositionEnd; | |
| attribute float aOffset; | |
| attribute vec3 aColor; | |
| uniform float uProgress; | |
| uniform mat4 uMVP; | |
| varying vec3 vColor; | |
| vec3 bezier4(vec3 a, vec3 b, vec3 c, vec3 d, float t) { | |
| return mix(mix(mix(a, b, t), mix(b, c, t), t), mix(mix(b, c, t), mix(c, d, t), t), t); | |
| } | |
| float easeInOutQuint(float t){ | |
| return t < 0.5 ? 16.0 * t * t * t * t * t : 1.0 + 16.0 * (--t) * t * t * t * t; | |
| } | |
| void main () { | |
| float tProgress = easeInOutQuint(min(1.0, max(0.0, (uProgress - aOffset)) / ${duration})); | |
| vec3 newPosition = bezier4(aPositionStart, aControlPointOne, aControlPointTwo, aPositionEnd, tProgress); | |
| gl_PointSize = ${pointSize} + ((newPosition.y + 1.0) * 80.0); | |
| gl_Position = uMVP * vec4(newPosition, 1.0); | |
| vColor = aColor; | |
| } | |
| `; | |
| const fragmentShader = ` | |
| precision mediump float; | |
| varying vec3 vColor; | |
| void main() { | |
| vec2 pc = 2.0 * gl_PointCoord - 1.0; | |
| gl_FragColor = vec4(vColor, 1.0 - dot(pc, pc)); | |
| } | |
| `; | |
| const onSetup = gl => { | |
| gl.blendFunc(gl.SRC_ALPHA, gl.ONE); | |
| gl.enable(gl.BLEND); | |
| }; | |
| const onRepeat = () => { | |
| rotateY(uniforms[uniforms.length - 1].value, 0.002); | |
| if (uniforms[0].value < 0) { | |
| uniforms[0].value = 1; | |
| } | |
| uniforms[0].value -= step; | |
| }; | |
| // const diff = (a, b) => Math.abs(a - b); | |
| // const ratio = window.innerWidth / window.innerHeight; | |
| // const halfWidth = window.innerWidth / 2; | |
| // const halfHeight = window.innerHeight / 2; | |
| // window.addEventListener('mousemove', (e) => { | |
| // uniforms[0].value = (((e.clientX - halfWidth) / halfWidth) * ratio).toFixed(4); | |
| // uniforms[1].value = (((e.clientY - halfHeight) / halfHeight)).toFixed(4) * -1; | |
| // }); | |
| const options = { | |
| onSetup, | |
| onRepeat, | |
| pixelRatio | |
| }; | |
| starlings( | |
| canvas, | |
| geometry, | |
| multiplier, | |
| attributes, | |
| uniforms, | |
| vertexShader, | |
| fragmentShader, | |
| options | |
| ); | |
| }; | |
| const getRandom = value => Math.random() * value - value / 2; | |
| const rotateY = (matrix, angle) => { | |
| const sin = Math.sin(angle); | |
| const cos = Math.cos(angle); | |
| const clone = JSON.parse(JSON.stringify(matrix)); | |
| matrix[0] = clone[0] * cos - clone[8] * sin; | |
| matrix[1] = clone[1] * cos - clone[9] * sin; | |
| matrix[2] = clone[2] * cos - clone[10] * sin; | |
| matrix[3] = clone[3] * cos - clone[11] * sin; | |
| matrix[8] = clone[0] * sin + clone[8] * cos; | |
| matrix[9] = clone[1] * sin + clone[9] * cos; | |
| matrix[10] = clone[2] * sin + clone[10] * cos; | |
| matrix[11] = clone[3] * sin + clone[11] * cos; | |
| }; | |
| const h2r = (p, q, t) => { | |
| if (t < 0) t += 1; | |
| if (t > 1) t -= 1; | |
| if (t < 1 / 6) return p + (q - p) * 6 * t; | |
| if (t < 1 / 2) return q; | |
| if (t < 2 / 3) return p + (q - p) * 6 * (2 / 3 - t); | |
| return p; | |
| }; | |
| const getHSL = (h, s, l) => { | |
| h = (h % 1 + 1) % 1; | |
| s = Math.max(0, Math.min(1, s)); | |
| l = Math.max(0, Math.min(1, l)); | |
| if (s === 0) return [l, l, l]; | |
| const p = l <= 0.5 ? l * (1 + s) : l + s - l * s; | |
| const q = 2 * l - p; | |
| return [h2r(q, p, h + 1 / 3), h2r(q, p, h), h2r(q, p, h - 1 / 3)]; | |
| }; | |
| initialize(); |
| <script src="https://use-the-platform.com/starlings@1.2.0/dist/starlings.min.js"></script> |
| html, | |
| body { | |
| width: 100%; | |
| height: 100%; | |
| margin: 0; | |
| overflow: hidden; | |
| background: black; | |
| } | |
| canvas { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| * { | |
| user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| footer { | |
| position: fixed; | |
| right: 0; | |
| top: 0; | |
| left: 0; | |
| padding: 10px 10px; | |
| text-align: right; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, | |
| Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; | |
| font-size: 14px; | |
| color: #fff; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| a { | |
| display: inline-block; | |
| margin-left: 2px; | |
| padding: 2px 4px; | |
| color: rgba(52, 52, 54, 1); | |
| text-decoration: none; | |
| background-color: #fcd000; | |
| border-radius: 4px; | |
| opacity: 1; | |
| transition: opacity 0.2s; | |
| &:hover { | |
| opacity: 0.6; | |
| } | |
| } |
💾