export class Charm { constructor(renderingEngine = PIXI) { if (renderingEngine === undefined) throw new Error("Please assign a rendering engine in the constructor before using charm.js"); //Find out which rendering engine is being used (the default is Pixi) this.renderer = ""; //If the `renderingEngine` is Pixi, set up Pixi object aliases if (renderingEngine.ParticleContainer && renderingEngine.Sprite) { this.renderer = "pixi"; } //An array to store the global tweens this.globalTweens = []; //An object that stores all the easing formulas this.easingFormulas = { //Linear linear(x) { return x; }, //Smoothstep smoothstep(x) { return x * x * (3 - 2 * x); }, smoothstepSquared(x) { return Math.pow((x * x * (3 - 2 * x)), 2); }, smoothstepCubed(x) { return Math.pow((x * x * (3 - 2 * x)), 3); }, //Acceleration acceleration(x) { return x * x; }, accelerationCubed(x) { return Math.pow(x * x, 3); }, //Deceleration deceleration(x) { return 1 - Math.pow(1 - x, 2); }, decelerationCubed(x) { return 1 - Math.pow(1 - x, 3); }, //Sine sine(x) { return Math.sin(x * Math.PI / 2); }, sineSquared(x) { return Math.pow(Math.sin(x * Math.PI / 2), 2); }, sineCubed(x) { return Math.pow(Math.sin(x * Math.PI / 2), 2); }, inverseSine(x) { return 1 - Math.sin((1 - x) * Math.PI / 2); }, inverseSineSquared(x) { return 1 - Math.pow(Math.sin((1 - x) * Math.PI / 2), 2); }, inverseSineCubed(x) { return 1 - Math.pow(Math.sin((1 - x) * Math.PI / 2), 3); }, //Spline spline(t, p0, p1, p2, p3) { return 0.5 * ( (2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t * t * t ); }, //Bezier curve cubicBezier(t, a, b, c, d) { let t2 = t * t; let t3 = t2 * t; return a + (-a * 3 + t * (3 * a - a * t)) * t + (3 * b + t * (-6 * b + b * 3 * t)) * t + (c * 3 - c * 3 * t) * t2 + d * t3; } }; //Add `scaleX` and `scaleY` properties to Pixi sprites this._addScaleProperties = (sprite) => { if (this.renderer === "pixi") { if (!("scaleX" in sprite) && ("scale" in sprite) && ("x" in sprite.scale)) { Object.defineProperty( sprite, "scaleX", { get() { return sprite.scale.x }, set(value) { sprite.scale.x = value } } ); } if (!("scaleY" in sprite) && ("scale" in sprite) && ("y" in sprite.scale)) { Object.defineProperty( sprite, "scaleY", { get() { return sprite.scale.y }, set(value) { sprite.scale.y = value } } ); } } }; } //The low level `tweenProperty` function is used as the foundation //for the the higher level tween methods. tweenProperty( sprite, //Sprite object property, //String property startValue, //Tween start value endValue, //Tween end value totalFrames, //Duration in frames type = "smoothstep", //The easing type yoyo = false, //Yoyo? delayBeforeRepeat = 0 //Delay in frames before repeating ) { //Create the tween object let o = {}; //If the tween is a bounce type (a spline), set the //start and end magnitude values let typeArray = type.split(" "); if (typeArray[0] === "bounce") { o.startMagnitude = parseInt(typeArray[1]); o.endMagnitude = parseInt(typeArray[2]); } //Use `o.start` to make a new tween using the current //end point values o.start = (startValue, endValue) => { //Clone the start and end values so that any possible references to sprite //properties are converted to ordinary numbers o.startValue = JSON.parse(JSON.stringify(startValue)); o.endValue = JSON.parse(JSON.stringify(endValue)); o.playing = true; o.totalFrames = totalFrames; o.frameCounter = 0; //Add the tween to the global `tweens` array. The `tweens` array is //updated on each frame this.globalTweens.push(o); }; //Call `o.start` to start the tween o.start(startValue, endValue); //The `update` method will be called on each frame by the game loop. //This is what makes the tween move o.update = () => { let time, curvedTime; if (o.playing) { //If the elapsed frames are less than the total frames, //use the tweening formulas to move the sprite if (o.frameCounter < o.totalFrames) { //Find the normalized value let normalizedTime = o.frameCounter / o.totalFrames; //Select the correct easing function from the //`ease` object’s library of easing functions //If it's not a spline, use one of the ordinary easing functions if (typeArray[0] !== "bounce") { curvedTime = this.easingFormulas[type](normalizedTime); } //If it's a spline, use the `spline` function and apply the //2 additional `type` array values as the spline's start and //end points else { curvedTime = this.easingFormulas.spline(normalizedTime, o.startMagnitude, 0, 1, o.endMagnitude); } //Interpolate the sprite's property based on the curve sprite[property] = (o.endValue * curvedTime) + (o.startValue * (1 - curvedTime)); o.frameCounter += 1; } //When the tween has finished playing, run the end tasks else { sprite[property] = o.endValue; o.end(); } } }; //The `end` method will be called when the tween is finished o.end = () => { //Set `playing` to `false` o.playing = false; //Call the tween's `onComplete` method, if it's been assigned if (o.onComplete) o.onComplete(); //Remove the tween from the `tweens` array this.globalTweens.splice(this.globalTweens.indexOf(o), 1); //If the tween's `yoyo` property is `true`, create a new tween //using the same values, but use the current tween's `startValue` //as the next tween's `endValue` if (yoyo) { this.wait(delayBeforeRepeat).then(() => { o.start(o.endValue, o.startValue); }); } }; //Pause and play methods o.play = () => o.playing = true; o.pause = () => o.playing = false; //Return the tween object return o; } //`makeTween` is a general low-level method for making complex tweens //out of multiple `tweenProperty` functions. Its one argument, //`tweensToAdd` is an array containing multiple `tweenProperty` calls makeTween(tweensToAdd) { //Create an object to manage the tweens let o = {}; //Create a `tweens` array to store the new tweens o.tweens = []; //Make a new tween for each array tweensToAdd.forEach(tweenPropertyArguments => { //Use the tween property arguments to make a new tween let newTween = this.tweenProperty(...tweenPropertyArguments); //Push the new tween into this object's internal `tweens` array o.tweens.push(newTween); }); //Add a counter to keep track of the //number of tweens that have completed their actions let completionCounter = 0; //`o.completed` will be called each time one of the tweens //finishes o.completed = () => { //Add 1 to the `completionCounter` completionCounter += 1; //If all tweens have finished, call the user-defined `onComplete` //method, if it's been assigned. Reset the `completionCounter` if (completionCounter === o.tweens.length) { if (o.onComplete) o.onComplete(); completionCounter = 0; } }; //Add `onComplete` methods to all tweens o.tweens.forEach(tween => { tween.onComplete = () => o.completed(); }); //Add pause and play methods to control all the tweens o.pause = () => { o.tweens.forEach(tween => { tween.playing = false; }); }; o.play = () => { o.tweens.forEach(tween => { tween.playing = true; }); }; //Return the tween object return o; } /* High level tween methods */ //1. Simple tweens //`fadeOut` fadeOut(sprite, frames = 60) { return this.tweenProperty( sprite, "alpha", sprite.alpha, 0, frames, "sine" ); } //`fadeIn` fadeIn(sprite, frames = 60) { return this.tweenProperty( sprite, "alpha", sprite.alpha, 1, frames, "sine" ); } //`pulse` //Fades the sprite in and out at a steady rate. //Set the `minAlpha` to something greater than 0 if you //don't want the sprite to fade away completely pulse(sprite, frames = 60, minAlpha = 0) { return this.tweenProperty( sprite, "alpha", sprite.alpha, minAlpha, frames, "smoothstep", true ); } //2. Complex tweens slide( sprite, endX, endY, frames = 60, type = "smoothstep", yoyo = false, delayBeforeRepeat = 0 ) { return this.makeTween([ //Create the x axis tween [sprite, "x", sprite.x, endX, frames, type, yoyo, delayBeforeRepeat], //Create the y axis tween [sprite, "y", sprite.y, endY, frames, type, yoyo, delayBeforeRepeat] ]); } breathe( sprite, endScaleX = 0.8, endScaleY = 0.8, frames = 60, yoyo = true, delayBeforeRepeat = 0 ) { //Add `scaleX` and `scaleY` properties to Pixi sprites this._addScaleProperties(sprite); return this.makeTween([ //Create the scaleX tween [ sprite, "scaleX", sprite.scaleX, endScaleX, frames, "smoothstepSquared", yoyo, delayBeforeRepeat ], //Create the scaleY tween [ sprite, "scaleY", sprite.scaleY, endScaleY, frames, "smoothstepSquared", yoyo, delayBeforeRepeat ] ]); } scale(sprite, endScaleX = 0.5, endScaleY = 0.5, frames = 60) { //Add `scaleX` and `scaleY` properties to Pixi sprites this._addScaleProperties(sprite); return this.makeTween([ //Create the scaleX tween [ sprite, "scaleX", sprite.scaleX, endScaleX, frames, "smoothstep", false ], //Create the scaleY tween [ sprite, "scaleY", sprite.scaleY, endScaleY, frames, "smoothstep", false ] ]); } strobe( sprite, scaleFactor = 1.3, startMagnitude = 10, endMagnitude = 20, frames = 10, yoyo = true, delayBeforeRepeat = 0 ) { let bounce = "bounce " + startMagnitude + " " + endMagnitude; //Add `scaleX` and `scaleY` properties to Pixi sprites this._addScaleProperties(sprite); return this.makeTween([ //Create the scaleX tween [ sprite, "scaleX", sprite.scaleX, scaleFactor, frames, bounce, yoyo, delayBeforeRepeat ], //Create the scaleY tween [ sprite, "scaleY", sprite.scaleY, scaleFactor, frames, bounce, yoyo, delayBeforeRepeat ] ]); } wobble( sprite, scaleFactorX = 1.2, scaleFactorY = 1.2, frames = 10, xStartMagnitude = 10, xEndMagnitude = 10, yStartMagnitude = -10, yEndMagnitude = -10, friction = 0.98, yoyo = true, delayBeforeRepeat = 0 ) { let bounceX = "bounce " + xStartMagnitude + " " + xEndMagnitude; let bounceY = "bounce " + yStartMagnitude + " " + yEndMagnitude; //Add `scaleX` and `scaleY` properties to Pixi sprites this._addScaleProperties(sprite); let o = this.makeTween([ //Create the scaleX tween [ sprite, "scaleX", sprite.scaleX, scaleFactorX, frames, bounceX, yoyo, delayBeforeRepeat ], //Create the scaleY tween [ sprite, "scaleY", sprite.scaleY, scaleFactorY, frames, bounceY, yoyo, delayBeforeRepeat ] ]); //Add some friction to the `endValue` at the end of each tween o.tweens.forEach(tween => { tween.onComplete = () => { //Add friction if the `endValue` is greater than 1 if (tween.endValue > 1) { tween.endValue *= friction; //Set the `endValue` to 1 when the effect is finished and //remove the tween from the global `tweens` array if (tween.endValue <= 1) { tween.endValue = 1; this.removeTween(tween); } } }; }); return o; } //3. Motion path tweens followCurve( sprite, pointsArray, totalFrames, type = "smoothstep", yoyo = false, delayBeforeRepeat = 0 ) { //Create the tween object let o = {}; //If the tween is a bounce type (a spline), set the //start and end magnitude values let typeArray = type.split(" "); if (typeArray[0] === "bounce") { o.startMagnitude = parseInt(typeArray[1]); o.endMagnitude = parseInt(typeArray[2]); } //Use `tween.start` to make a new tween using the current //end point values o.start = (pointsArray) => { o.playing = true; o.totalFrames = totalFrames; o.frameCounter = 0; //Clone the points array o.pointsArray = JSON.parse(JSON.stringify(pointsArray)); //Add the tween to the `globalTweens` array. The `globalTweens` array is //updated on each frame this.globalTweens.push(o); }; //Call `tween.start` to start the first tween o.start(pointsArray); //The `update` method will be called on each frame by the game loop. //This is what makes the tween move o.update = () => { let normalizedTime, curvedTime, p = o.pointsArray; if (o.playing) { //If the elapsed frames are less than the total frames, //use the tweening formulas to move the sprite if (o.frameCounter < o.totalFrames) { //Find the normalized value normalizedTime = o.frameCounter / o.totalFrames; //Select the correct easing function //If it's not a spline, use one of the ordinary tween //functions if (typeArray[0] !== "bounce") { curvedTime = this.easingFormulas[type](normalizedTime); } //If it's a spline, use the `spline` function and apply the //2 additional `type` array values as the spline's start and //end points else { //curve = tweenFunction.spline(n, type[1], 0, 1, type[2]); curvedTime = this.easingFormulas.spline(normalizedTime, o.startMagnitude, 0, 1, o.endMagnitude); } //Apply the Bezier curve to the sprite's position sprite.x = this.easingFormulas.cubicBezier(curvedTime, p[0][0], p[1][0], p[2][0], p[3][0]); sprite.y = this.easingFormulas.cubicBezier(curvedTime, p[0][1], p[1][1], p[2][1], p[3][1]); //Add one to the `elapsedFrames` o.frameCounter += 1; } //When the tween has finished playing, run the end tasks else { //sprite[property] = o.endValue; o.end(); } } }; //The `end` method will be called when the tween is finished o.end = () => { //Set `playing` to `false` o.playing = false; //Call the tween's `onComplete` method, if it's been //assigned if (o.onComplete) o.onComplete(); //Remove the tween from the global `tweens` array this.globalTweens.splice(this.globalTweens.indexOf(o), 1); //If the tween's `yoyo` property is `true`, reverse the array and //use it to create a new tween if (yoyo) { this.wait(delayBeforeRepeat).then(() => { o.pointsArray = o.pointsArray.reverse(); o.start(o.pointsArray); }); } }; //Pause and play methods o.pause = () => { o.playing = false; }; o.play = () => { o.playing = true; }; //Return the tween object return o; } walkPath( sprite, //The sprite originalPathArray, //A 2D array of waypoints totalFrames = 300, //The duration, in frames type = "smoothstep", //The easing type loop = false, //Should the animation loop? yoyo = false, //Shoud the direction reverse? delayBetweenSections = 0 //Delay, in milliseconds, between sections ) { //Clone the path array so that any possible references to sprite //properties are converted into ordinary numbers let pathArray = JSON.parse(JSON.stringify(originalPathArray)); //Figure out the duration, in frames, of each path section by //dividing the `totalFrames` by the length of the `pathArray` let frames = totalFrames / pathArray.length; //Set the current point to 0, which will be the first waypoint let currentPoint = 0; //The `makePath` function creates a single tween between two points and //then schedules the next path to be made after it let makePath = (currentPoint) => { //Use the `makeTween` function to tween the sprite's //x and y position let tween = this.makeTween([ //Create the x axis tween between the first x value in the //current point and the x value in the following point [ sprite, "x", pathArray[currentPoint][0], pathArray[currentPoint + 1][0], frames, type ], //Create the y axis tween in the same way [ sprite, "y", pathArray[currentPoint][1], pathArray[currentPoint + 1][1], frames, type ] ]); //When the tween is complete, advance the `currentPoint` by one. //Add an optional delay between path segments, and then make the //next connecting path tween.onComplete = () => { //Advance to the next point currentPoint += 1; //If the sprite hasn't reached the end of the //path, tween the sprite to the next point if (currentPoint < pathArray.length - 1) { this.wait(delayBetweenSections).then(() => { tween = makePath(currentPoint); }); } //If we've reached the end of the path, optionally //loop and yoyo it else { //Reverse the path if `loop` is `true` if (loop) { //Reverse the array if `yoyo` is `true` if (yoyo) pathArray.reverse(); //Optionally wait before restarting this.wait(delayBetweenSections).then(() => { //Reset the `currentPoint` to 0 so that we can //restart at the first point currentPoint = 0; //Set the sprite to the first point sprite.x = pathArray[0][0]; sprite.y = pathArray[0][1]; //Make the first new path tween = makePath(currentPoint); //... and so it continues! }); } } }; //Return the path tween to the main function return tween; }; //Make the first path using the internal `makePath` function (below) let tween = makePath(currentPoint); //Pass the tween back to the main program return tween; } walkCurve( sprite, //The sprite pathArray, //2D array of Bezier curves totalFrames = 300, //The duration, in frames type = "smoothstep", //The easing type loop = false, //Should the animation loop? yoyo = false, //Should the direction reverse? delayBeforeContinue = 0 //Delay, in milliseconds, between sections ) { //Divide the `totalFrames` into sections for each part of the path let frames = totalFrames / pathArray.length; //Set the current curve to 0, which will be the first one let currentCurve = 0; //The `makePath` function let makePath = (currentCurve) => { //Use the custom `followCurve` function to make //a sprite follow a curve let tween = this.followCurve( sprite, pathArray[currentCurve], frames, type ); //When the tween is complete, advance the `currentCurve` by one. //Add an optional delay between path segments, and then make the //next path tween.onComplete = () => { currentCurve += 1; if (currentCurve < pathArray.length) { this.wait(delayBeforeContinue).then(() => { tween = makePath(currentCurve); }); } //If we've reached the end of the path, optionally //loop and reverse it else { if (loop) { if (yoyo) { //Reverse order of the curves in the `pathArray` pathArray.reverse(); //Reverse the order of the points in each curve pathArray.forEach(curveArray => curveArray.reverse()); } //After an optional delay, reset the sprite to the //beginning of the path and make the next new path this.wait(delayBeforeContinue).then(() => { currentCurve = 0; sprite.x = pathArray[0][0]; sprite.y = pathArray[0][1]; tween = makePath(currentCurve); }); } } }; //Return the path tween to the main function return tween; }; //Make the first path let tween = makePath(currentCurve); //Pass the tween back to the main program return tween; } //4. Utilities /* The `wait` method lets you set up a timed sequence of events wait(1000) .then(() => console.log("One")) .then(() => wait(1000)) .then(() => console.log("Two")) .then(() => wait(1000)) .then(() => console.log("Three")) */ wait(duration = 0) { return new Promise((resolve, reject) => { setTimeout(resolve, duration); }); } //A utility to remove tweens from the game removeTween(tweenObject) { //Remove the tween if `tweenObject` doesn't have any nested //tween objects if (!tweenObject.tweens) { tweenObject.pause(); //array.splice(-1,1) will always remove last elemnt of array, so this //extra check prevents that (Thank you, MCumic10! https://github.com/kittykatattack/charm/issues/5) if (this.globalTweens.indexOf(tweenObject) != -1) { this.globalTweens.splice(this.globalTweens.indexOf(tweenObject), 1); } //Otherwise, remove the nested tween objects } else { tweenObject.pause(); tweenObject.tweens.forEach(element => { this.globalTweens.splice(this.globalTweens.indexOf(element), 1); }); } } update() { //Update all the tween objects in the `globalTweens` array if (this.globalTweens.length > 0) { for (let i = this.globalTweens.length - 1; i >= 0; i--) { let tween = this.globalTweens[i]; if (tween) tween.update(); } } } }