From 4f2dd94e83dd9c3d87cb90dad9775eb1a9ad7b53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=BCrgensen?= <mail@dominiq.eu>
Date: Sat, 25 Dec 2021 21:36:33 +0100
Subject: [PATCH] Add support for multiple layer of particles.

---
 src/parallax-css-testpage/particles.js  | 280 ++++++++++--------------
 src/parallax-css-testpage/template.html |  70 +++---
 2 files changed, 162 insertions(+), 188 deletions(-)

diff --git a/src/parallax-css-testpage/particles.js b/src/parallax-css-testpage/particles.js
index 9b59090b1..6667cb1b6 100644
--- a/src/parallax-css-testpage/particles.js
+++ b/src/parallax-css-testpage/particles.js
@@ -1,23 +1,24 @@
 /*
  *  Particles.js
+ * 
+ *  Particle emitter and renderer.
  */
 
 const Debug = false
 const Static = true
 
-// Needed for fps calculation.
-let Then = Date.now() // Argh.. Global, mutable, variable..!
-
 
 //
 //  Helper
 //
 
 
-// Random Value without zero
 function randomValue(min, max) {
     const value = (Math.random() * (+max - +min) + +min)
-    return (value === 0) ? randomValue(min, max) : value
+    // Prevent values that can be possible boring
+    return (value === 0 || value === 1)
+        ? randomValue(min, max)
+        : value
 }
 
 function getNodeYOffset(node) {
@@ -30,38 +31,6 @@ function getNodeHeight(node) {
     return (dimensions) ? dimensions.height : window.innerHeight
 }
 
-function getScene(canvasId, height) {
-    const width  = window.innerWidth
-    const fov = 0.8
-    const perspective = width * fov
-    const yOffset = 0
-    const canvas = document.getElementById(canvasId)
-    const ctx = canvas.getContext('2d')
-
-    ctx.canvas.width = width
-    ctx.canvas.height = window.innerHeight
-
-    return { width, height, yOffset, perspective, ctx }
-}
-
-// function to2dParticle(scene, particle) {
-//     const perspective = scene.perspective
-//     const scale = perspective / (perspective + particle.position.z)
-//     const yOffset = scene.yOffset * scale
-//     const offset = particle.size.value * 0.5
-//     const x = particle.position.x - offset
-//     const y = (yOffset + (particle.position.y - offset))
-//     const size = particle.size.value
-//     // const y = (yOffset + (particle.position.y - offset)) * scale
-//     // const size = particle.size.value * scale
-//     return { x, y, size }
-// }
-
-function calcScrolledYPos(ctx, yPos, yOffset) {
-    const y = (yPos + yOffset)
-    return (y <= 0) ? ctx.canvas.height - y : y
-}
-
 
 //
 //  Data
@@ -72,10 +41,6 @@ function Point2D(x, y) {
     return { x, y }
 }
 
-function Point3D(x, y, z) {
-    return { x, y, z }
-}
-
 function MinMax(min, max) {
     return { min, max }
 }
@@ -90,16 +55,6 @@ function RandomPoint2D(minMaxX, minMaxY) {
     return Point2D(x, y)
 }
 
-function RandomPoint3D(minMaxX, minMaxY, minMaxZ) {
-    const x = randomValue(minMaxX.min, minMaxX.max)
-    const y = randomValue(minMaxY.min, minMaxY.max)
-    const z =
-        (typeof minMaxZ === 'number')
-            ? minMaxZ
-            : randomValue(minMaxZ.min, minMaxZ.max)
-    return Point3D(x, y, z)
-}
-
 function Particle({ position, direction, size, color }) {
     return { position, direction, size, color }
 }
@@ -132,8 +87,8 @@ function Update(scene, particle) {
         Object.assign({}, updateSizeStep, {
             size: Object.assign({}, updateSizeStep.size, {
                 value:
-                    // Todo: Animate puls effect with the help of Math.sin()
                     updateSizeStep.size.value + updateSizeStep.size.step * updateSizeStep.size.direction,
+                    // updateSizeStep.size.value
             })
         })
 
@@ -142,8 +97,8 @@ function Update(scene, particle) {
     const sizeOffset = particle.size.value * 0.5
     const y = particle.position.y
 
-    const top    = y - sizeOffset
-    const bottom = y + sizeOffset
+    const top    = y + sizeOffset
+    const bottom = y - sizeOffset
     const left   = particle.position.x - sizeOffset
     const right  = particle.position.x + sizeOffset
 
@@ -156,10 +111,11 @@ function Update(scene, particle) {
         Object.assign({}, updateSize, {
             direction:
                 Point2D(
-                    // Bounce of the right and left edges of the screen.
+                    // Bounce of the right and left edges of the scene
                     (isRightEdge || isLeftEdge)
                         ? updateSize.direction.x * -1
                         : updateSize.direction.x,
+                    // Bounce from the top and bottom edges of the scene
                     (isTopEdge || isBottomEdge)
                         ? particle.direction.y * -1
                         : particle.direction.y,
@@ -168,7 +124,7 @@ function Update(scene, particle) {
     const updatePosition =
         Object.assign({}, updateDirection, {
             position:
-                Point3D(
+                Point2D(
                     updateDirection.position.x + updateDirection.direction.x,
                     updateDirection.position.y + updateDirection.direction.y,
                     // (isTopEdge)
@@ -176,7 +132,6 @@ function Update(scene, particle) {
                     //     : (isBottomEdge)
                     //         ? -sizeOffset
                     //         : (updateDirection.position.y + updateDirection.direction.y),
-                    updateDirection.position.z
                 )
         })
 
@@ -186,19 +141,20 @@ function Update(scene, particle) {
 
 function Draw(scene, particle) {
     // Cache object access
-    const size = particle.size.value
+    const size     = particle.size.value
+    const position = particle.position
 
-    const perspective = scene.perspective
-    const scale = perspective / (perspective + particle.position.z)
-    const yOffset = scene.yOffset * scale
+    // Draw calculations
+    const yOffset = scene.yOffset * scene.depthScale
     const drawOffset = size * 0.5
-    const y = yOffset + (particle.position.y - drawOffset)
+    const y = yOffset + (position.y - drawOffset)
     const ctx = scene.ctx
-
     const isVisible = (size > 0) && (y < window.innerHeight) && (y > 0)
+
+    // Draw
     if (isVisible) {
         ctx.fillStyle = particle.color
-        ctx.fillRect(particle.position.x - drawOffset, y, size, size)
+        ctx.fillRect(position.x - drawOffset, y, size, size)
     }
 }
 
@@ -209,122 +165,126 @@ function Clear(scene) {
 function Render(scene, particles) {
     Clear(scene)
     particles.forEach(
-        function (particle) {
+        function doDraw(particle) {
             Draw(scene, particle)
         }
     )
 }
 
-function Animate({ scene, fpsInterval, particles, anchorNode }) {
-    scene.yOffset = getNodeYOffset(anchorNode)
-
-    // Update particle positions and register for the next frame
-    // to render. 
-    requestAnimationFrame(() => {
-        const updatedParticles = 
-            particles.map(function (p) {
-                return Update(scene, p)
-            })
-        Animate({ scene, fpsInterval, particles: updatedParticles, anchorNode })
-    }) 
-
-    // Render particles to canvas and do some fps calculations
-    const now = Date.now()
-    const elapsed = now - Then
-    if (elapsed > fpsInterval) {
-        Then = now - (elapsed % fpsInterval)
+function Animate(scene, particles) {
+    var fpsInterval = scene.fpsInterval
+    var now = Date.now()
+    var then = scene.then
+    var elapsed = now - then
+    
+    // Just render when we're reaching the configured fps
+    if (elapsed > scene.fpsInterval) {
+        scene.yOffset = getNodeYOffset(scene.anchorNode)
+        scene.then = now - (elapsed % fpsInterval)
         Render(scene, particles)
     }
+
+    // Update particle positions for the next render call
+    const updatedParticles = particles.map(function doUpdate(p) {
+        return Update(scene, p)
+    })
+
+    // Function to animate the next frame
+    return function () {
+        return Animate(scene, updatedParticles)
+    }
 }
 
+function CreateScene({ canvasId, contentId, depth, fps }) {
+    const contentNode = document.getElementById(contentId)
+    const height = getNodeHeight(contentNode)
 
-//
-//  Api
-//
+    const fpsInterval = 1000 / fps
+    const width  = window.innerWidth
+    const yOffset = 0
+    const canvas = document.getElementById(canvasId)
+    const ctx = canvas.getContext('2d')
+    const depthScaledHeight = height * (depth+1)
 
+    ctx.canvas.width = width
+    ctx.canvas.height = window.innerHeight
+
+    return {
+        width,
+        height: depthScaledHeight,
+        yOffset,
+        depthScale: depth,
+        ctx,
+        fps,
+        fpsInterval,
+        then: Date.now(),
+        anchorNode: contentNode
+    }
+}
 
-function Particles({
-    canvasId,
-    contentId,
-    fps,
+
+function CreateParticles(scene, {
     amount,
     color,
     size,
     speed,
-    depth,
     lifespan,
 }) {
-    const contentNode = document.getElementById(contentId)
-    const height = getNodeHeight(contentNode)
+    return Array.from(
+        { length: amount },
+        function createParticle() {
+            return Particle({
+                position:
+                    RandomPoint2D(
+                        MinMax(0, (scene.width - (size * 2))),
+                        MinMax(0, (scene.height - (size * 2))),
+                    ),
+                direction:
+                    RandomPoint2D(
+                        MinMax(-speed, speed),
+                        MinMax(-speed, speed)
+                    ),
+                size: {
+                    value: randomValue(-size, size),
+                    bound: MinMax(-size, size),
+                    step: size / ((lifespan) / scene.fps),
+                    direction:
+                        // 50% Chance for the particle floating in one
+                        // direction or the other.
+                        (randomValue(0, 100) < 50)
+                            ? -1
+                            : +1,
+                },
+                color
+            })
+        }
+    )
+}
 
-    // Init the canvas for rendering.
-    //
-    const scene  = getScene(canvasId, height)
-    if (!scene.ctx) {
-        console.error("Particles: Can't find Canvas ", canvasId)
-        return
-    }
 
- 
-    // Generate some particles
-    //
+//
+//  Api
+//
 
-    const z = depth * -1000
-    const particles =
-        (Debug)
-            ? (Static)
-                ? [ Particle({
-                        position:  Point3D(100, 500, z),
-                        direction: Point2D(0, 0),
-                        size: {
-                            value: 15,
-                            bound: MinMax(100, 100),
-                            step:  0,
-                            direction: 1
-                        },
-                        color: 'white'
-                }) ]
-                : [ Particle({
-                        position:  Point3D(100, 100, z),
-                        direction: Point2D(0, +1),
-                        size: {
-                            value: 100,
-                            bound: MinMax(100, 100),
-                            step:  0,
-                            direction: 1
-                        },
-                        color: 'white'
-                }) ]
-            : Array.from(
-                { length: amount },
-                () => Particle({
-                    position:
-                        RandomPoint3D(
-                            MinMax(0, (scene.width  - (size*2))),
-                            MinMax(0, (scene.height - (size*2))),
-                            z
-                        ),
-                    direction:
-                        RandomPoint2D(
-                            MinMax(-speed, speed),
-                            MinMax(-speed, speed)
-                        ),
-                    size: {
-                        value: randomValue(-size, size),
-                        bound: MinMax(-size, size),
-                        step:  size / ((lifespan) / fps),
-                        direction:
-                            (randomValue(0, 100) < 50)
-                                ? -1
-                                : +1,
-                    },
-                    color
+
+function Particles(configs) {
+    function doRenderParticles(rendererList) {
+        requestAnimationFrame(function () {
+            doRenderParticles(
+                rendererList.map(function doRender(render) {
+                    return render()
                 })
             )
-     
-    // Prepare Animation and animate
-    const fpsInterval = 1000 / fps
-    Then = Date.now()
-    Animate({ scene, fpsInterval, particles, anchorNode: contentNode })
-    return
-}
+        })
+    }
+
+    return function () {
+        doRenderParticles(
+            configs.map(function (particleConfig) {
+                var scene     = CreateScene(particleConfig.scene)
+                var particles = CreateParticles(scene, particleConfig.particles)
+                return Animate(scene, particles)
+            })
+        )
+    }
+}
\ No newline at end of file
diff --git a/src/parallax-css-testpage/template.html b/src/parallax-css-testpage/template.html
index 7819f5ee9..5fcfa853e 100644
--- a/src/parallax-css-testpage/template.html
+++ b/src/parallax-css-testpage/template.html
@@ -82,36 +82,50 @@
 
 
     <script>
-        var bgParticles = {
-            // Id of the canvas to render to
-            canvasId: 'bg-particles',
-
-            // Id of the content container, we're using it to get the 
-            // rendering height and it's the anchor to get the y offset
-            // to simulate scrolling.
-            contentId: 'content',
-            fps: 16,
-            amount: 120,
-            color: 'white',
-            size: 10,
-            speed: 0.2,
-            depth: -8,
-            lifespan: 4000 // in milliseconds
+        var fgParticleConfig = {
+            scene: {
+                // Id of the canvas to render to
+                canvasId: 'fg-particles',
+                // Id of the content container, we're using it to get the 
+                // rendering height and it's the anchor to get the y offset
+                // to simulate scrolling.
+                contentId: 'content',
+                fps: 30,
+                depth: 2,
+            },
+            particles: {
+                amount: 100,
+                color: 'white',
+                size: 30,
+                speed: 0.5,
+                lifespan: 3000 // in milliseconds
+            }
         }
-        Particles(bgParticles)
-
-        var fgParticles = {
-            canvasId: 'fg-particles',
-            contentId: 'content',
-            fps: 24,
-            amount: 20,
-            color: 'white',
-            size: 30,
-            speed: 0.5,
-            depth: 2,
-            lifespan: 4000 // in milliseconds
+        var bgParticleConfig = {
+            scene: {
+                // Id of the canvas to render to
+                canvasId: 'bg-particles',
+                // Id of the content container, we're using it to get the 
+                // rendering height and it's the anchor to get the y offset
+                // to simulate scrolling.
+                contentId: 'content',
+                fps: 16,
+                depth: 0.08,
+            },
+            particles: {
+                amount: 200,
+                color: 'white',
+                size: 10,
+                speed: 0.2,
+                lifespan: 4000 // in milliseconds
+            }
         }
-        // Particles(fgParticles)
+
+        var animate = Particles([
+            fgParticleConfig,
+            bgParticleConfig,
+        ])
+        animate()
     </script>
 
 </body>
-- 
GitLab