pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
729 lines (649 loc) • 27.3 kB
text/typescript
/*
Animation library for sprites
*/
//% color="#03AA74" weight=100 icon="\uf021" block="Animation"
//% groups='["Animate", "Advanced"]'
//% weight=5
namespace animation {
const stateNamespace = "__animation";
interface AnimationState {
animations: SpriteAnimation[];
}
export class Point {
public x: number;
public y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
//% fixedInstances blockId=animation_path block="path %pathString"
export class PathPreset {
constructor(public pathString: string) {
}
}
export class Path {
length: number;
protected args: number[];
protected currentCommand: string;
protected lastControlX: number;
protected lastControlY: number;
protected startX: number;
protected startY: number;
protected lastX: number;
protected lastY: number;
protected strIndex: number;
protected commandIndex: number;
constructor(protected path: string) {
this.strIndex = 0;
// Run through the path once to get the length and check for errors
this.length = 0;
while (this.strIndex < this.path.length) {
this.readNextCommand();
if (this.currentCommand) this.length++;
}
this.reset();
}
protected readNextCommand() {
if (this.strIndex >= this.path.length) {
this.currentCommand = undefined;
return;
}
this.currentCommand = this.readNextToken();
if (!this.currentCommand) return;
this.args = [];
const numArgs = Path.commandToArgCount(this.currentCommand);
if (numArgs === -1) throw "Unknown path command '" + this.currentCommand +"'";
for (let i = 0; i < numArgs; i++) {
this.args.push(parseFloat(this.readNextToken()))
}
for (const arg of this.args) {
if (Number.isNaN(arg)) throw "Invalid argument for path command '" + this.currentCommand + "'";
}
}
reset() {
this.args = undefined;
this.currentCommand = undefined;
this.lastControlX = undefined;
this.lastControlY = undefined;
this.startX = undefined;
this.startY = undefined;
this.lastX = undefined;
this.lastY = undefined;
this.strIndex = 0;
this.commandIndex = 0;
}
protected readNextToken() {
while (this.path.charCodeAt(this.strIndex) === 32 && this.strIndex < this.path.length) {
this.strIndex ++;
}
if (this.strIndex >= this.path.length) return undefined;
const tokenStart = this.strIndex;
while (this.path.charCodeAt(this.strIndex) !== 32 && this.strIndex < this.path.length) {
this.strIndex++;
}
return this.path.substr(tokenStart, this.strIndex - tokenStart);
}
private static commandToArgCount(command: string): number {
switch (command) {
case "M": // moveTo
case "m":
return 2;
case "L": // lineTo
case "l":
return 2;
case "H": // horizontalLineTo
case "h":
return 1;
case "V": // verticalLineTo
case "v":
return 1;
case "Q": // quadraticCurveTo
case "q":
return 4;
case "T": // smoothQuadraticCurveTo
case "t":
return 2;
case "C": // cubicCurveTo
case "c":
return 6;
case "S": // smoothCubicCurveTo
case "s":
return 4;
case "A": // arcTo
case "a":
return 7;
case "Z": // closePath
case "z":
return 0;
default:
return -1;
}
}
public run(interval: number, target: Sprite, runningTime: number): boolean {
const nodeIndex = Math.floor(runningTime / interval); // The current node
const nodeTime = runningTime % interval; // The time the current node has been animating
if (this.startX === undefined) {
this.startX = target.x;
this.startY = target.y;
this.lastX = target.x;
this.lastY = target.y;
this.commandIndex = 0;
this.readNextCommand();
}
while (this.commandIndex < nodeIndex) {
if (this.currentCommand) {
this.runCurrentCommand(target, interval, interval);
this.lastX = target.x;
this.lastY = target.y;
}
this.commandIndex++
this.readNextCommand();
}
if (nodeIndex >= this.length) {
return true;
}
this.runCurrentCommand(target, nodeTime, interval);
return false;
}
protected runCurrentCommand(target: Sprite, nodeTime: number, intervalTime: number) {
switch (this.currentCommand) {
case "M": // M x y
this.lastControlX = undefined;
this.lastControlY = undefined;
moveTo(
target,
nodeTime,
intervalTime,
this.args[0],
this.args[1]
);
break;
case "m": // m dx dy
this.lastControlX = undefined;
this.lastControlY = undefined;
moveTo(
target,
nodeTime,
intervalTime,
this.args[0] + this.lastX,
this.args[1] + this.lastY
);
break;
case "L": // L x y
this.lastControlX = undefined;
this.lastControlY = undefined;
lineTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0],
this.args[1]
);
break;
case "l": // l dx dy
this.lastControlX = undefined;
this.lastControlY = undefined;
lineTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0] + this.lastX,
this.args[1] + this.lastY
);
break;
case "H": // H x
this.lastControlX = undefined;
this.lastControlY = undefined;
lineTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0],
this.lastY
);
break;
case "h": // h dx
this.lastControlX = undefined;
this.lastControlY = undefined;
lineTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0] + this.lastX,
this.lastY
);
break;
case "V": // V y
this.lastControlX = undefined;
this.lastControlY = undefined;
lineTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.lastX,
this.args[0]
);
break;
case "v": // v dy
this.lastControlX = undefined;
this.lastControlY = undefined;
lineTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.lastX,
this.args[0] + this.lastY
);
break;
case "Q": // Q x1 y1 x2 y2
this.lastControlX = this.args[0];
this.lastControlY = this.args[1];
quadraticCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0],
this.args[1],
this.args[2],
this.args[3]
)
break;
case "q": // q dx1 dy1 dx2 dy2
this.lastControlX = this.args[0] + this.lastX;
this.lastControlY = this.args[1] + this.lastY;
quadraticCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0] + this.lastX,
this.args[1] + this.lastY,
this.args[2] + this.lastX,
this.args[3] + this.lastY
);
break;
case "T": // T x2 y2
this.ensureControlPoint();
quadraticCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.lastX + this.lastX - this.lastControlX,
this.lastY + this.lastY - this.lastControlY,
this.args[0],
this.args[1],
);
if (nodeTime === intervalTime) {
this.lastControlX = this.lastX + this.lastX - this.lastControlX;
this.lastControlY = this.lastY + this.lastY - this.lastControlY;
}
break;
case "t": // t dx2 dy2
this.ensureControlPoint();
quadraticCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.lastX + this.lastX - this.lastControlX,
this.lastY + this.lastY - this.lastControlY,
this.args[0] + this.lastX,
this.args[1] + this.lastY,
);
if (nodeTime === intervalTime) {
this.lastControlX = this.lastX + this.lastX - this.lastControlX;
this.lastControlY = this.lastY + this.lastY - this.lastControlY;
}
break;
case "C": // C x1 y1 x2 y2 x3 y3
this.lastControlX = this.args[2];
this.lastControlY = this.args[3];
cubicCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0],
this.args[1],
this.args[2],
this.args[3],
this.args[4],
this.args[5],
);
break;
case "c": // c dx1 dy1 dx2 dy2 dx3 dy3
this.lastControlX = this.args[2] + this.lastX;
this.lastControlY = this.args[3] + this.lastY;
cubicCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.args[0] + this.lastX,
this.args[1] + this.lastY,
this.args[2] + this.lastX,
this.args[3] + this.lastY,
this.args[4] + this.lastX,
this.args[5] + this.lastY,
);
break;
case "S": // S x2 y2 x3 y3
this.ensureControlPoint();
cubicCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.lastX + this.lastX - this.lastControlX,
this.lastY + this.lastY - this.lastControlY,
this.args[0],
this.args[1],
this.args[2],
this.args[3]
);
if (nodeTime === intervalTime) {
this.lastControlX = this.args[0];
this.lastControlY = this.args[1];
}
break;
case "s": // s dx2 dy2 dx3 dy3
this.ensureControlPoint();
cubicCurveTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.lastX + this.lastX - this.lastControlX,
this.lastY + this.lastY - this.lastControlY,
this.args[0] + this.lastX,
this.args[1] + this.lastY,
this.args[2] + this.lastX,
this.args[3] + this.lastY,
);
if (nodeTime === intervalTime) {
this.lastControlX = this.args[0] + this.lastX;
this.lastControlY = this.args[1] + this.lastY;
}
break;
case "Z": // Z
case "z": // z
this.lastControlX = undefined;
this.lastControlY = undefined;
lineTo(
target,
nodeTime,
intervalTime,
this.lastX,
this.lastY,
this.startX,
this.startY
);
break;
}
}
protected ensureControlPoint() {
if (this.lastControlX === undefined) throw "Invalid path command. S/s and T/t must follow either Q/q or C/c"
}
}
function moveTo(target: Sprite, nodeTime: number, interval: number, x: number, y: number) {
if (nodeTime >= interval) target.setPosition(x, y);
}
function lineTo(target: Sprite, nodeTime: number, interval: number, x0: number, y0: number, x1: number, y1: number) {
target.setPosition(
Math.round(((x1 - x0) / interval) * nodeTime) + x0,
Math.round(((y1 - y0) / interval) * nodeTime) + y0
);
}
function quadraticCurveTo(target: Sprite, nodeTime: number, interval: number, x0: number, y0: number, x1: number, y1: number, x2: number, y2: number) {
const progress = nodeTime / interval;
const diff = 1 - progress;
const a = diff * diff;
const b = 2 * diff * progress;
const c = progress * progress;
target.setPosition(
Math.round(a * x0 + b * x1 + c * x2),
Math.round(a * y0 + b * y1 + c * y2)
);
}
function cubicCurveTo(target: Sprite, nodeTime: number, interval: number, x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) {
const progress = nodeTime / interval;
const diff = 1 - progress;
const a = diff * diff * diff;
const b = 3 * diff * diff * progress;
const c = 3 * diff * progress * progress;
const d = progress * progress * progress;
target.setPosition(
Math.round(a * x0 + b * x1 + c * x2 + d * x3),
Math.round(a * y0 + b * y1 + c * y2 + d * y3)
);
}
export abstract class SpriteAnimation {
protected elapsedTime: number;
constructor(public sprite: Sprite, protected loop: boolean) {
this.elapsedTime = 0;
}
public init() {
let state: AnimationState = game.currentScene().data[stateNamespace];
// Register animation updates to fire when frames are rendered
if (!state) {
state = game.currentScene().data[stateNamespace] = {
animations: []
} as AnimationState;
game.eventContext().registerFrameHandler(scene.ANIMATION_UPDATE_PRIORITY, () => {
state.animations = state.animations.filter((anim: SpriteAnimation) => {
if (anim.sprite.flags & sprites.Flag.Destroyed)
return false;
return !anim.update(); // If update returns true, the animation is done and will be removed
});
});
}
// Remove any other animations of this type and attached to this sprite
state.animations = state.animations.filter((anim: SpriteAnimation) => {
return !(anim.sprite === this.sprite &&
((anim instanceof ImageAnimation && this instanceof ImageAnimation) ||
(anim instanceof MovementAnimation && this instanceof MovementAnimation)));
});
state.animations.push(this);
}
public update(): boolean {
// This should be implemented by subclasses
return false;
}
}
export class ImageAnimation extends SpriteAnimation {
private lastFrame: number;
constructor(sprite: Sprite, private frames: Image[], private frameInterval: number, loop?: boolean) {
super(sprite, loop);
this.lastFrame = -1;
}
public update(): boolean {
this.elapsedTime += game.eventContext().deltaTimeMillis;
const frameIndex = Math.floor(this.elapsedTime / this.frameInterval);
if (this.lastFrame != frameIndex && this.frames.length) {
if (!this.loop && frameIndex >= this.frames.length) {
return true;
}
const newImage = this.frames[frameIndex % this.frames.length];
if (this.sprite.image !== newImage) {
this.sprite.setImage(newImage);
}
}
this.lastFrame = frameIndex;
return false;
}
}
export class MovementAnimation extends SpriteAnimation {
protected startX: number;
protected startY: number;
constructor(sprite: Sprite, private path: Path, private nodeInterval: number, loop?: boolean) {
super(sprite, loop);
this.startX = sprite.x;
this.startY = sprite.y;
this.elapsedTime = 0;
}
public update(): boolean {
this.elapsedTime += game.eventContext().deltaTimeMillis;
let result = this.path.run(this.nodeInterval, this.sprite, this.elapsedTime);
if (result) {
if (!this.loop) return true;
this.elapsedTime = 0;
this.path.reset();
this.sprite.x = this.startX;
this.sprite.y = this.startY;
}
return false;
}
}
/**
* Create and run an image animation on a sprite
* @param frames the frames to animate through
* @param sprite the sprite to animate on
* @param frameInterval the time between changes, eg: 500
*/
//% blockId=run_image_animation
//% block="animate $sprite=variables_get(mySprite) frames $frames=animation_editor interval (ms) $frameInterval=timePicker loop $loop=toggleOnOff"
//% sprite.defl=mySprite
//% group="Animate"
//% weight=100
//% help=animation/run-image-animation
export function runImageAnimation(sprite: Sprite, frames: Image[], frameInterval?: number, loop?: boolean) {
const anim = new ImageAnimation(sprite, frames, frameInterval || 500, !!loop);
anim.init();
}
/**
* Create and run a movement animation on a sprite
* @param sprite the sprite to move
* @param pathString the SVG path to animate
* @param duration how long the animation should play for, eg: 500
*/
//% blockId=run_movement_animation
//% block="animate $sprite=variables_get(mySprite) with $pathString=animation_path for (ms) $duration=timePicker loop $loop=toggleOnOff"
//% sprite.defl=mySprite
//% duration.defl=2000
//% weight=80
//% group="Animate"
//% help=animation/run-movement-animation
export function runMovementAnimation(sprite: Sprite, pathString: string, duration?: number, loop?: boolean) {
const path = new Path(pathString);
const anim = new MovementAnimation(sprite, path, duration / path.length, !!loop);
anim.init();
}
export enum AnimationTypes {
//% block="all"
All,
//% block="frame"
ImageAnimation,
//% block="path"
MovementAnimation
}
/**
* Stop one type or all animations (simple and looping) on a sprite
* @param type the animation type to stop
* @param sprite the sprite to filter animations by
*/
//% blockId=stop_animations
//% block="stop %type animations on %sprite=variables_get(mySprite)"
//% sprite.defl=mySprite
//% group="Animate"
//% weight=60
//% help=animation/stop-animation
export function stopAnimation(type: AnimationTypes, sprite: Sprite) {
let state: AnimationState = game.currentScene().data[stateNamespace];
if (state && state.animations) {
state.animations = state.animations.filter((anim: SpriteAnimation) => {
if (anim.sprite === sprite) {
switch (type) {
case AnimationTypes.ImageAnimation:
if (anim instanceof ImageAnimation) return false;
break;
case AnimationTypes.MovementAnimation:
if (anim instanceof MovementAnimation) return false;
break;
case AnimationTypes.All:
return false;
}
}
return true;
});
}
if (type == AnimationTypes.All || type == AnimationTypes.ImageAnimation) {
//stop state based animation if any as well
sprite._action = -1
}
}
//% fixedInstance whenUsed block="fly to center"
export const flyToCenter = new PathPreset("L 80 60");
//% fixedInstance whenUsed block="shake"
export const shake = new PathPreset("m 4 -1 m 1 2 m -6 2 m -4 -8 m 8 8 m 2 -4 m -8 0 m 6 3 m -3 -2");
//% fixedInstance whenUsed block="bounce (right)"
export const bounceRight = new PathPreset("q 7 0 15 40 q 10 -30 15 -25 q 10 5 15 25 q 5 -25 10 0 q 4 -15 8 0 q 2 -10 4 0 q 1 -5 1 0 q 0 -2 1 0");
//% fixedInstance whenUsed block="bounce (left)"
export const bounceLeft = new PathPreset("q -7 0 -15 40 q -10 -30 -15 -25 q -10 5 -15 25 q -5 -25 -10 0 q -4 -15 -8 0 q -2 -10 -4 0 q -1 -5 -1 0 q 0 -2 -1 0");
//% fixedInstance whenUsed block="parachute (right)"
export const parachuteRight = new PathPreset("q 20 10 40 5 q 2 -2 0 0 q -15 10 -30 5 q -2 -2 0 0 q 10 10 20 5 q 2 -2 0 0 q -5 5 -10 3 q -1 -1 0 0 q 2 2 5 1 l 0 2 l 0 2 l 0 2");
//% fixedInstance whenUsed block="parachute (left)"
export const parachuteLeft = new PathPreset("q -20 10 -40 5 q -2 -2 0 0 q 15 10 30 5 q 2 -2 0 0 q -10 10 -20 5 q -2 -2 0 0 q 5 5 10 3 q 1 -1 0 0 q -2 2 -5 1 l 0 2 l 0 2 l 0 2");
//% fixedInstance whenUsed block="ease (right)"
export const easeRight = new PathPreset("h 5 h 10 h 20 h 30 h 20 h 10 h 5");
//% fixedInstance whenUsed block="ease (left)"
export const easeLeft = new PathPreset("h -5 h -10 h -20 h -30 h -20 h -10 h -5");
//% fixedInstance whenUsed block="ease (down)"
export const easeDown = new PathPreset("v 5 v 10 v 20 v 30 v 20 v 10 v 5");
//% fixedInstance whenUsed block="ease (up)"
export const easeUp = new PathPreset("v -5 v -10 v -20 v -30 v -20 v -10 v -5");
//% fixedInstance whenUsed block="wave (right)"
export const waveRight = new PathPreset("c 25 -15 15 -5 20 0");
//% fixedInstance whenUsed block="wave (left)"
export const waveLeft = new PathPreset("c -25 -15 -15 -5 -20 0");
//% fixedInstance whenUsed block="bobbing (in place)"
export const bobbing = new PathPreset("c 0 -20 0 20 0 0");
//% fixedInstance whenUsed block="bobbing (right)"
export const bobbingRight = new PathPreset("c 5 -20 15 20 20 0");
//% fixedInstance whenUsed block="bobbing (left)"
export const bobbingLeft = new PathPreset("c -5 -20 -15 20 -20 0");
/**
* Generates a path string for preset animation
* @param animationPath The preset path
*/
//% blockId=animation_path
//% block="%animationPath"
//% group="Animate"
//% blockHidden=1
export function animationPresets(animationPath: PathPreset) {
return animationPath.pathString;
}
//% blockId=animation_editor block="%frames"
//% shim=TD_ID
//% frames.fieldEditor="animation"
//% frames.fieldOptions.decompileLiterals="true"
//% frames.fieldOptions.filter="!tile !dialog !background"
//% weight=40
//% group="Animate" duplicateShadowOnDrag
//% help=animation/animation-frames
export function _animationFrames(frames: Image[]) {
return frames
}
}