Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweens #112

Open
Baegus opened this issue Oct 8, 2024 · 3 comments
Open

Tweens #112

Baegus opened this issue Oct 8, 2024 · 3 comments

Comments

@Baegus
Copy link

Baegus commented Oct 8, 2024

Is there something like a tween system planned? Something that could modify values in time, with easings etc.?

@EthanSuperior
Copy link
Contributor

EthanSuperior commented Oct 8, 2024

You can add Tweens by hooking into the new Plugin system with something like this

 * Tween - A class for creating tweens for animations and other properties.
 * 
 * This class allows you to interpolate values over a specified duration,
 * executing a callback function with the interpolated value at each step.
 * 
 * @example
 * // Create a tween that logs from 0 to 1 over 1200 frames (20 seconds)
 * const tween1 = new Tween(value => console.log(value), 1200);
 * 
 * // Create a tween from 0 to 10 over 300 frames (5 seconds)
 * const tween2 = new Tween(value => console.log(value), 10, 300);
 * 
 * // Create a tween that animates from 5 to 15 300 frames (5 seconds)
 * const tween3 = new Tween(value => console.log(value), 5, 15, 300);
 */

class Tween {
    static children = new Set();
	
    static {
        engineAddPlugin(() =>
			Tween.children.forEach((tween) =>
				--tween.life
					? tween.fn((tween.life * tween.diff) / tween.duration + tween.end)
					: Tween.children.delete(tween) && tween.fn(tween.end)
			)
		);
    }

    /**
     * Creates a new Tween instance. The fn and duration arguments are required.
     * 
     * @param {function} fn - The function to call with the interpolated value.
     * @param {number} [start] - The starting value of the tween.
     * @param {number} [end] - The ending value of the tween.
     * @param {number} [duration] - The duration of the tween in frames.
     */
    constructor(fn, start, end, duration) {
		end ?? ([duration, end, start] = [start, 1, 0]);
		duration ?? ([duration, end, start] = [end, start, 0]);
		this.fn = fn;
		this.end = end;
		this.diff = start - end;
		this.life = this.duration = duration;
		Tween.children.add(this);
		this.fn(start);
    }
}

@KilledByAPixel
Copy link
Owner

Very cool, I am definitely planning on adding something like this either as a plugin or directly to the engine. Will use yours to help me get started when I do it.

@EthanSuperior
Copy link
Contributor

EthanSuperior commented Oct 10, 2024

Had some extra time so I did some more work, might be a bit more helpful now.

class Tween {
	static active = [];

	constructor(fn, start, end, duration) {
		// These two lines are to reverse the order of optional params
		// Otherwise just use constructor(fn, duration, end=1, start=0)
		end ?? ([start, end, duration] = [0, 1, start]);
		duration ?? ([start, end, duration] = [0, start, end]);

		// Properties for the Tween to function
		this.duration = duration;
		this.life = this.duration;
		this.start = start;
		this.end = end;
		this.delta = this.end - this.start;
		this.fn = fn;
		// Callback for when Tween is completed
		this.then = (f) => ((this.then = f), this);
		this.setEase = (f) => ((this.ease = f), this);
		Tween.active.push(this);
		this.fn(this.interp(this.duration));
	}
	ease = (t) => t;
	interp(life) {
		const y = this.ease((this.duration - life) / this.duration);
		return y * this.delta + this.start;
	}

	static update() {
		// for(let t,i=0;i<Tween.living.length;i++)
		// 	--(t=Tween.living[i]).life?t.fn(t.curr):(t.fn(t.end),Tween.living.splice(i--,1),t.then());
		for (let i = 0; i < Tween.active.length; i++) {
			const twn = Tween.active[i];
			if (--twn.life) twn.fn(twn.interp(twn.life));
			else {
				twn.fn(twn.interp(0));
				Tween.active.splice(i--, 1);
				twn.then();
			}
		}
	}

	static Loop = function (n) {
		function repeat() {
			new Tween(this.fn, this.start, this.end, this.duration)
				.setEase(this.ease)
				.then(Tween.Loop(n));
		}
		if (--n == 0) return () => {};
		else if (n) return repeat;
		else Tween.Loop(Infinity).call(this);
	};

	static PingPong = function (n) {
		function repeat() {
			new Tween(this.fn, this.end, this.start, this.duration)
				.setEase(this.ease)
				.then(Tween.PingPong(n));
		}
		if (--n == 0) return () => {};
		else if (n) return repeat;
		else Tween.PingPong(Infinity).call(this);
	};
}
class Ease {
	static LINEAR = (x) => x;
	static POWER = (n) => (x) => x ** n;
	static SINE = (x) => 1 - Math.cos(x * (Math.PI / 2));
	static CIRC = (x) => 1 - Math.sqrt(1 - x * x);
	static EXPO = (x) => 2 ** (10 * x - 10);
	static BACK = (x) => x * x * (2.70158 * x - 1.70158);
	static ELASTIC = (x) =>
		-(2 ** (10 * x - 10)) * Math.sin(((37 - 40 * x) * Math.PI) / 6);
	static SPRING = (x) =>
		1 -
		(Math.sin(Math.PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
			Math.pow(x, 2.2) +
			(1 - x)) *
			(1.0 + 1.2 * x);
	static BOUNCE = (x) => {
		const bounceOut = (x) => {
			if (x < 4 / 11) return 7.5625 * x * x;
			if (x < 8 / 11) return bounceOut(x - 6 / 11) + 0.75;
			if (x < 10 / 11) return bounceOut(x - 9 / 11) + 0.9375;
			return bounceOut(x - 10.5 / 11) + 0.984375;
		};
		return Ease.OUT(bounceOut)(x);
	};
	static BEZIER = (x1, y1, x2, y2) => {
		const curve = (t) => {
			const u = 1 - t;
			const c1 = 3 * u * u * t;
			const c2 = 3 * u * t * t;
			const t3 = t ** 3;
			return [c1 * x1 + c2 * x2 + t3, c1 * y1 + c2 * y2 + t3];
		};
		return (x) => {
			for (let i = 0, t0 = 0, t1 = 1; i < 128; i++) {
				const tMid = (t0 + t1) / 2;
				const [bx, by] = curve(tMid);
				if (Math.abs(bx - x) < 1e-5) return by;
				else if (bx < x) t0 = tMid;
				else t1 = tMid;
			}
			return curve((t0 + t1) / 2)[1];
		};
	};
	static IN = (x) => x;
	static OUT = (f) => (x) => 1 - f(1 - x);
	static IN_OUT = (f) => Piecewise(f, Ease.OUT(f));
	static PIECEWISE = (...fns) => {
		const n = fns.length;
		return (x) => {
			let i = (x * n - 1e-9) >> 0;
			return (fns[i]((x - i / n) * n) + i) / n;
		};
	};
}

And some Examples to demo them a bit better:

Example 1: Countdown Timer

const txt = (t) => drawTextScreen(t, mainCanvasSize.scale(0.5), 80);
new Tween((t) => txt(t >> 0), 10, 1, 600).then(() => txt("BOOM!"));

Example 2: Chaining Tweens

const obj = new EngineObject();
new Tween((t) => (obj.pos.x = t), -10, 10, 600).then = () =>
	new Tween((t) => (obj.size = new Vector2(t, t)), 1, 0, 60);

Example 3: Looping

const obj = new EngineObject(new Vector2(0, -1));
const obj2 = new EngineObject(new Vector2(0, 1));
new Tween((t) => (obj.pos.x = t), -10, 10, 60).then(Tween.PingPong);
new Tween((t) => (obj2.pos.x = t), -10, 10, 60).then(Tween.Loop(5));

Example 4: Easing

const obj = new EngineObject(new Vector2(3, 3));
new Tween((t) => (obj.pos.y = t), 12, -10, 120)
	.then(Tween.Loop)
	.setEase(Ease.OUT(Ease.BOUNCE));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants