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:
|
|
|
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...