geema / js /sequence-player.js
EricaLuvGemma's picture
Update js/sequence-player.js
c59d596 verified
Raw
History Blame Contribute Delete
7.92 kB
import { animationManifest } from "./animation-manifest.js";
export class SequencePlayer {
constructor(canvas, options = {}) {
if (!(canvas instanceof HTMLCanvasElement)) {
throw new TypeError(
"SequencePlayer requires a valid canvas element."
);
}
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
if (!this.ctx) {
throw new Error(
"The browser could not create a 2D canvas context."
);
}
this.basePath = options.basePath || "";
this.defaultFPS = options.fps || 12;
this.frames = [];
this.currentFrame = 0;
this.playing = false;
this.loop = true;
this.animationName = null;
this.animationFrameId = null;
this.lastFrameTime = 0;
this.frameDuration = 1000 / this.defaultFPS;
this.onComplete = null;
this.playRequestId = 0;
this.ctx.imageSmoothingEnabled = false;
this.resize();
window.addEventListener("resize", () => {
this.resize();
});
}
getAnimation(name) {
const animation = animationManifest[name];
if (!animation) {
throw new Error(
`Animation "${name}" does not exist in the manifest.`
);
}
if (
!Array.isArray(animation.frames) ||
animation.frames.length === 0
) {
throw new Error(
`Animation "${name}" does not contain any frames.`
);
}
return animation;
}
async play(name, options = {}) {
const requestId = ++this.playRequestId;
this.stop(false);
const animation = this.getAnimation(name);
this.animationName = name;
this.loop =
options.loop ??
animation.loop ??
true;
this.onComplete =
typeof options.onComplete === "function"
? options.onComplete
: null;
const fps =
options.fps ??
animation.fps ??
this.defaultFPS;
this.frameDuration = 1000 / fps;
const folder =
animation.path || name;
const loadedFrames = await this.loadFrames(
folder,
animation.frames
);
/*
* Ignore old loading requests if the user selected
* another animation before loading completed.
*/
if (requestId !== this.playRequestId) {
return;
}
this.frames = loadedFrames;
this.currentFrame = 0;
this.lastFrameTime = 0;
this.playing = true;
this.drawFrame(this.frames[0]);
this.animationFrameId = requestAnimationFrame(
(timestamp) => this.tick(timestamp)
);
}
async loadFrames(folder, filenames) {
const framePromises = filenames.map((filename) => {
const url = this.createFrameURL(
folder,
filename
);
return this.loadImage(url);
});
return Promise.all(framePromises);
}
createFrameURL(folder, filename) {
const cleanBasePath =
this.basePath.replace(/\/+$/, "");
const cleanFolder =
folder.replace(/^\/+|\/+$/g, "");
const cleanFilename =
filename.replace(/^\/+/, "");
return (
`${cleanBasePath}/` +
`${cleanFolder}/` +
`${cleanFilename}`
);
}
loadImage(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
resolve(image);
};
image.onerror = () => {
reject(
new Error(
`Could not load frame: ${url}`
)
);
};
image.src = url;
});
}
tick(timestamp) {
if (!this.playing) {
return;
}
if (this.lastFrameTime === 0) {
this.lastFrameTime = timestamp;
}
const elapsed =
timestamp - this.lastFrameTime;
if (elapsed >= this.frameDuration) {
this.lastFrameTime =
timestamp -
(elapsed % this.frameDuration);
this.advanceFrame();
}
if (this.playing) {
this.animationFrameId =
requestAnimationFrame(
(nextTimestamp) =>
this.tick(nextTimestamp)
);
}
}
advanceFrame() {
const nextFrame =
this.currentFrame + 1;
if (nextFrame >= this.frames.length) {
if (this.loop) {
this.currentFrame = 0;
} else {
this.finish();
return;
}
} else {
this.currentFrame = nextFrame;
}
this.drawFrame(
this.frames[this.currentFrame]
);
}
drawFrame(image) {
if (!image) {
return;
}
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
this.ctx.clearRect(
0,
0,
canvasWidth,
canvasHeight
);
const paddingRatio = 0.08;
const availableWidth =
canvasWidth *
(1 - paddingRatio * 2);
const availableHeight =
canvasHeight *
(1 - paddingRatio * 2);
const scale = Math.min(
availableWidth / image.width,
availableHeight / image.height
);
const drawWidth =
image.width * scale;
const drawHeight =
image.height * scale;
const drawX =
(canvasWidth - drawWidth) / 2;
const drawY =
(canvasHeight - drawHeight) / 2;
this.ctx.imageSmoothingEnabled = false;
this.ctx.drawImage(
image,
drawX,
drawY,
drawWidth,
drawHeight
);
}
resize() {
const rect =
this.canvas.getBoundingClientRect();
const pixelRatio =
window.devicePixelRatio || 1;
const width = Math.max(
1,
Math.floor(rect.width * pixelRatio)
);
const height = Math.max(
1,
Math.floor(rect.height * pixelRatio)
);
if (
this.canvas.width !== width ||
this.canvas.height !== height
) {
this.canvas.width = width;
this.canvas.height = height;
}
this.ctx.imageSmoothingEnabled = false;
if (this.frames.length > 0) {
this.drawFrame(
this.frames[this.currentFrame]
);
}
}
finish() {
this.playing = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(
this.animationFrameId
);
this.animationFrameId = null;
}
const completionCallback =
this.onComplete;
this.onComplete = null;
if (completionCallback) {
completionCallback();
}
}
stop(invalidateRequest = true) {
this.playing = false;
if (invalidateRequest) {
this.playRequestId++;
}
if (this.animationFrameId !== null) {
cancelAnimationFrame(
this.animationFrameId
);
this.animationFrameId = null;
}
this.frames = [];
this.currentFrame = 0;
this.lastFrameTime = 0;
this.onComplete = null;
}
}