Experiment 3: Bouncy Balls

Sections in this Article:

Balls! Bringing it Together

If you're here, well done - we're almost done! As in some of the earlier experiments, this one is going to be to be wrapped into a re-useable web-compopnent [cause I like this relatively new technology!]. An advantage to this is it's very easy to create many instances of the ball experiement on a single page as follows:

Your browser does not support Web Components :-(
Your browser does not support Web Components :-(
Your browser does not support Web Components :-(

This is where we'll be at the end of this section. For this experiment, I can't really see much use for this other than it's cool as it keeps everything neatly together and promotes re-use! It must be a good thing right? So how do we do it?

The Web Component

To keep it really simple, the most basic way to define a web component is as follows:

class BouncyBalls extends HTMLElement {

    constructor() {
        super();
    }

    connectedCallback() {
        // do magic stuff here!
    }

} 
customElements.define('mfx-bouncy-balls', BouncyBalls );

That’s really all the browser really needs for “mfx-bouncy-balls” elements in the DOM to be offloaded to a class named BouncyBalls . Of course there's much more to Web Components than this, but the intention is to keep it simple.

Filling all the blanks and hooking up the Balls class from earlier, here’s the final listing for the web-component:

WebComponent.js
1class BouncyBalls extends HTMLElement {
2
3    _shadow = null;     // reference to the shaow dom
4    _ballArea = null;   // reference to the svg
5
6    _balls = new Array();   // array of balls
7    _me = null;
8
9     
10    // default size of the svg; will be modifed on component resize
11     _bounds = {
12         width: 300,
13         height: 200
14    }
15
16    constructor() {
17        super();
18    }
19
20    connectedCallback() {
21        this._shadow = this.attachShadow({mode: 'open'});
22        this.render();
23        
24        // kick off a ball at the start
25        this.newBall(this);
26
27        let self = this;
28        // start the animation timer
29        window.setInterval( () => {
30            // update the boundaries just in case there has been a resize 
31            self._bounds.width = self._ballArea.clientWidth || self._ballArea.parentNode.clientWidth;
32            self._bounds.height = self._ballArea.clientHeight || self._ballArea.parentNode.clientHeight;
33
34            // now update all of the balls
35            self._balls.forEach( kvp => {
36               // get the ball and corresponding svg element
37               let theBall = kvp.value;
38               let theCircle = kvp.key;
39
40               // update the ball position
41               theBall.Update(self._bounds);
42                              
43               // now update position of the circle in the svg that represents the ball
44               theCircle.style.cx = theBall.x;
45               theCircle.style.cy = theBall.y;
46            });
47        }, 10 ); // timer interval
48
49    }
50
51    // create a new ball instance and add
52    newBall(self) {
53
54        // create the svg object
55        let elm = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
56        
57        self._ballArea.appendChild(elm);
58        const minSize = 4;
59        const maxSize = 30-minSize;
60        let radius= minSize + Math.random()*maxSize;
61        let bounceLoss = 0.98-0.6*(radius/maxSize); // bigger ones don't bounce as well!
62        let ball = new Ball( radius, 0.1, bounceLoss, self._bounds );    
63
64        elm.setAttribute("cx", ball.x);
65        elm.setAttribute("cy", ball.y);
66        elm.setAttribute("r", ball.radius);
67        elm.setAttribute("fill", "#ffffff");
68        elm.setAttribute("stroke", "#d0d0d0");
69        elm.setAttribute("stroke-width", "1px");
70        
71        self._ballArea.appendChild(elm);
72        
73        // need to remember or 'pair' the ball object with its corresponding svg circle
74        let wpr = new KeyValuePair(elm, ball);
75        self._balls.push(wpr);
76    }
77
78    render() {
79        if(!this._shadow) return; // not ready yet, don't render
80        let html="";
81
82        // center div in a div; refer to: https://www.freecodecamp.org/news/how-to-center-anything-with-css-align-a-div-text-and-more/
83        html+='<div style="position: relative; background-color: #867ade; width: 100%; height: 100%; display: inline-block; user-select: none;">';
84        html+='  <div style="width: 90%; height: 90%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 12px; font-family:\'Franklin Gothic Medium\', \'Arial Narrow\', Arial, sans-serif;">';
85        html+='    <svg id="ball-screen" style="background-color: #483aaa; width: 100% ;height: 100%; ">';
86        html+='      <text x="10" y="20" font-size="1em" style="fill: white; user-select: none; ">CLICK/TOUCH TO THROW BALL IN</text>';
87        html+='  </div>';
88        html+='</div>';
89    
90        this._shadow.innerHTML = html;
91
92        // cache references
93        this._ballArea = this._shadow.querySelector("#ball-screen");
94
95        // set-up the click handler to throw in a new ball...
96        let self = this; // little trick so we can access the instance of this object
97        this.addEventListener("click", () => { self.newBall(self); } );         
98    }
99} 
100customElements.define('mfx-bouncy-balls', BouncyBalls );

We'll break this down shortly, but there's is one additional class used that hasn't been not mentioned yet:

keyvaluepair.js
1class KeyValuePair {
2   key = null;
3   value = null;
4   
5   constructor(key, value) {
6      this.key = key;
7      this.value = value;
8   }
9}

This is a very simple [and crude] class that has one purpose, to establish a relationship or, one to one mapping of two objects that I've probably incorrectly referred to as the key and pair. It effectively wraps the two items together and the reason this is needed in this example, is to define a one to one mapping for each Ball object and its corresponding graphic representation, a circle in an svg.

Note: When this experiment was first conceived, the graphic element was a field of the Ball class. While this kept things simple, it coupled the Ball to the svg circle element and limited the re-useability of the Ball class. After some consideration, the experiment was refactored into the form presented here.

Continue on reading to An Alternative Engine...