I found this explanation of Robert Penner’s easing equations very useful – the most useful being the idea that one is feeding a timing value between 0 and 1 to the easing function to get back another value how far along the transformation in question you are – and that this value may be greater than 1 or less than zero. This is exactly what I want in my springy circles – for them to overshoot their target and oscillate a few times before settling at the final position.
Starting with KeyboardSpringyCircles, I started adding the logic to allow for non-linear tweens for movement. I got timing working by using the the millis() function of p5.js.
function SpringyCircle(){ //SpringyCircle object this.colour = color(random(100),50,100,50);; //random hue, saturation 50%, brightness 100%, alpha 50% this.radius = random(circleMinRadius,circleMaxRadius); this.position = createVector(random(windowWidth)/windowWidth,random(windowHeight)/windowHeight); this.startPosition = createVector(this.position.x, this.position.y); this.startPosition.y += 0.15; //want to start 15% of the screen down when the circle is interacted with this.durationOfTween = 1000; //1000 milliseconds for tween this.endPosition = createVector(this.position.x, this.position.y); //want to finish back where we started this.startTimeOfTween = -1; this.display = function(){ var milliseconds = millis(); var elapsedMillisSinceStartOfTween = milliseconds - this.startTimeOfTween; if(this.startTimeOfTween > 0 && elapsedMillisSinceStartOfTween < this.durationOfTween){ var changeBetweenStartAndEnd = this.endPosition.y - this.startPosition.y; var ratioOfTweenComplete = elapsedMillisSinceStartOfTween/this.durationOfTween; var changeUpToNow = changeBetweenStartAndEnd*this.bouncePast(ratioOfTweenComplete); this.position.y = this.startPosition.y + changeUpToNow; } var translatedX = this.position.x * windowWidth; var translatedY = this.position.y * windowHeight; fill(this.colour); ellipse(translatedX, translatedY, this.radius); // https://p5js.org/reference/#/p5/ellipse and https://p5js.org/reference/#/p5/ellipseMode } this.startTween = function(){ //move the position of the spring a bit print("Starting a tween"); this.startTimeOfTween = millis(); } this.bouncePast = function(howFarThroughTween){ //see https://github.com/jeremyckahn/shifty/blob/master/src/shifty.formulas.js //and http://upshots.org/actionscript/jsas-understanding-easing //and of course http://robertpenner.com/easing/ if (howFarThroughTween < (1 / 2.75)) { return (7.5625 * howFarThroughTween * howFarThroughTween); } else if (howFarThroughTween < (2 / 2.75)) { return 2 - (7.5625 * (howFarThroughTween -= (1.5 / 2.75)) * howFarThroughTween + 0.75); } else if (howFarThroughTween < (2.5 / 2.75)) { return 2 - (7.5625 * (howFarThroughTween -= (2.25 / 2.75)) * howFarThroughTween + 0.9375); } else { return 2 - (7.5625 * (howFarThroughTween -= (2.625 / 2.75)) * howFarThroughTween + 0.984375); } } }
One bug that caused me intense frustration was that my local webserver didn’t seem to be serving the latest code when I made an update. Eventually I tracked it down to Chrome caching files wherever possible – meaning that I had to use Command Shift R to force a reload of all the files being served – not just the HTML.
I also managed to repeated Vector copying values explicitly bug from late November, by doing writing:
this.startPosition = this.position;
Rather than:
this.startPosition = createVector(this.position.x, this.position.y);
At this stage I had the following interaction working:
Which was a good start, but I didn’t like the precise settling animation, so I decided to create a new external JS file with all the easing equations contained in one convenient place. This would allow me to use all the easing equations in other places in my code.
I created easing.p5.jgl.js and keyboard.p5.jgl.js to contain all my easing logic and keyboard logic respectively. After adding both references to my index.html file:
<script language="javascript" src="../libraries/keyboard.p5.jgl.js"></script> <script language="javascript" src="../libraries/easing.p5.jgl.js"></script> <script language="javascript" type="text/javascript" src="sketch.js"></script>
I began going through all the possible options for easing:
var ratioOfEaseComplete = elapsedMillisSinceStartOfEase/this.durationOfEase; // exhaustively trying all the different easing possibilities // var changeUpToNow = changeBetweenStartAndEnd*easeOutQuad(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutQuad(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInCubic(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutCubic(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutCubic(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInQuart(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutQuart(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutQuart(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInQuint(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutQuint(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutQuint(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInSine(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutSine(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutSine(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInExpo(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutExpo(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutExpo(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInCirc(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutCirc(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutCirc(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutBounce(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInBack(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeOutBack(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeInOutBack(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*elastic(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*swingFromTo(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*swingFrom(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*swingTo(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*bounce(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeFromTo(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeFrom(ratioOfEaseComplete); // var changeUpToNow = changeBetweenStartAndEnd*easeTo(ratioOfEaseComplete);
I broke this down to the following options, which felt like they were in the correct ballpark:
// var changeUpToNow = changeBetweenStartAndEnd*easeOutBounce(ratioOfEaseComplete); // easeOutBounce is good but not right // var changeUpToNow = changeBetweenStartAndEnd*easeOutBack(ratioOfEaseComplete); // easeOutBack is also good but not right // var changeUpToNow = changeBetweenStartAndEnd*elastic(ratioOfEaseComplete); // elastic is good // var changeUpToNow = changeBetweenStartAndEnd*swingTo(ratioOfEaseComplete); // swingTo is good // var changeUpToNow = changeBetweenStartAndEnd*bounce(ratioOfEaseComplete); // bounce is good // var changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete); // bouncePast is good, but feels abrupt at end
It was proving laborious to manually change the code every time I wanted to see how the particular easing function looked. Luckily, p5.js comes with a library called p5.dom:
The web is much more than just canvas and p5.dom makes it easy to interact with other HTML5 objects, including text, hyperlink, image, input, video, audio, and webcam.
p5.dom was even already included, but commented out in my index.html file, so I just removed the comment:
<script language="javascript" type="text/javascript" src="../libraries/p5.js"></script> <!-- uncomment lines below to include extra p5 libraries --> <script language="javascript" src="../libraries/p5.dom.js"></script> <!--<script language="javascript" src="../libraries/p5.sound.js"></script>--> <script language="javascript" src="../libraries/keyboard.p5.jgl.js"></script> <script language="javascript" src="../libraries/easing.p5.jgl.js"></script> <script language="javascript" type="text/javascript" src="sketch.js"></script>
I created a new select object:
sel = createSelect(); sel.position(10, 10); sel.option('easeOutBounce'); sel.option('easeOutBack'); sel.option('elastic'); sel.option('swingTo'); sel.option('bounce'); sel.option('bouncePast');
Following that I added some logic to link the select object to the selection of the particular easing function that I wanted, using the JavaScript switch statement:
var changeUpToNow = 0; var easeOption = sel.value(); switch(easeOption){ case 'easeOutBounce': changeUpToNow = changeBetweenStartAndEnd*easeOutBounce(ratioOfEaseComplete); break; case 'easeOutBack'): changeUpToNow = changeBetweenStartAndEnd*easeOutBack(ratioOfEaseComplete); break; case 'elastic'): changeUpToNow = changeBetweenStartAndEnd*elastic(ratioOfEaseComplete); break; case 'swingTo'): changeUpToNow = changeBetweenStartAndEnd*swingTo(ratioOfEaseComplete); break; case 'bounce'): changeUpToNow = changeBetweenStartAndEnd*bounce(ratioOfEaseComplete); break; case 'bouncePast'): changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete); break; default: changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete); break; }
Using the “Beyond the canvas” p5.js tutorial, I added an HTML text label to select object make it clearer for users. The demo can be tried here: KeyboardSpringyCirclesWithEaseSelect.
I decided that “elastic” was the closest to the original version, but still wasn’t satisfied with the result. I found this spring example on the Processing.js site and decided to port it to p5.js. After realising that I had to rewrite the code to use the static methods of the p5.vector class, I got to something that was much closer to the original version. I increase the number of circles to 100 as an added bonus. Give the updated KeyboardSpringyCircles demo a try.
I folded the updated spring code into KeyboardBouncingCircleGrid and MouseSpringyCircles too!