@exabytellc/canvas
Version:
EB react canvas!
234 lines • 6.58 kB
JavaScript
import CanimeObj from "./_CanimeObj";
export default class Canime {
#canvas;
#context;
#lastTime = 0;
#fps = 0;
#deltaTime = 0;
#data = {
centerX: 0,
centerY: 0,
maxDim: 0,
minDim: 0,
mouseX: 0,
mouseY: 0
};
#events = [];
#fpsTimer = 0;
#animationFrame;
// settings
fpsInfinite = true;
fpsLimit = 60;
sizeAuto = false;
interactive = false;
resolutionWidth = 800;
resolutionHeight = 600;
// on events (to be overridden)
onInitiate;
onRender;
constructor(canvas, {
fpsInfinite = true,
fpsLimit = 60,
sizeAuto = false,
interactive = false,
resolutionWidth = 800,
resolutionHeight = 600,
onInitiate,
onRender
} = {}) {
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error("Invalid canvas element");
}
this.#canvas = canvas;
this.#context = this.#canvas.getContext("2d");
if (!this.#context) {
throw new Error("Failed to get canvas context");
}
window.addEventListener('resize', this.#debounce(() => this.#resize(), 100));
this.fpsInfinite = fpsInfinite;
this.fpsLimit = fpsLimit;
this.sizeAuto = sizeAuto;
this.interactive = interactive;
this.resolutionWidth = resolutionWidth;
this.resolutionHeight = resolutionHeight;
this.onInitiate = onInitiate;
this.onRender = onRender;
}
start() {
this.#initiate();
this.#render(0);
}
stop() {
if (this.#animationFrame) {
cancelAnimationFrame(this.#animationFrame);
this.#animationFrame = null;
}
}
restart() {
this.stop();
this.start();
}
#initiate() {
this.#resizeCanvas(true);
if (this.interactive) this.#initializeEventListeners();
if (this.onInitiate) this.onInitiate(this.#objParams());
}
#resizeCanvas(init = false) {
if (this.sizeAuto) {
this.#canvas.width = this.#canvas.offsetWidth;
this.#canvas.height = this.#canvas.offsetHeight;
} else if (init) {
this.#canvas.width = this.resolutionWidth;
this.#canvas.height = this.resolutionHeight;
}
this.#data.centerX = this.#canvas.width / 2;
this.#data.centerY = this.#canvas.height / 2;
this.#data.maxDim = Math.max(this.#canvas.width, this.#canvas.height);
this.#data.minDim = Math.min(this.#canvas.width, this.#canvas.height);
}
#resize() {
if (this.sizeAuto) {
this.restart();
}
}
#render(timestamp) {
const deltaTime = timestamp - this.#lastTime;
this.#lastTime = timestamp;
this.#fps = 1000 / deltaTime;
if (!this.fpsInfinite && this.fpsLimit) {
const nextFrame = 1000 / this.fpsLimit;
if (this.#fpsTimer > nextFrame) {
this.#animate(deltaTime);
this.#fpsTimer = 0;
} else {
this.#fpsTimer += deltaTime;
}
} else {
this.#animate(deltaTime);
}
this.#animationFrame = requestAnimationFrame(this.#render.bind(this));
}
#animate(deltaTime) {
this.#deltaTime = deltaTime;
if (this.onRender) this.onRender(this.#objParams());
}
#initiateObj(obj, data) {
if (obj?.initiate) {
obj.initiate(this.#objParams(data), obj);
}
}
#renderObj(obj, data) {
if (obj?.render) {
obj.render(this.#objParams(data), obj);
}
}
#drawSave(draw) {
this.#context.save();
if (draw) {
draw(this.#context);
}
this.#context.restore();
}
#clearCanvas() {
this.#context.clearRect(0, 0, this.#canvas.width, this.#canvas.height);
}
#objParams(extra = {}) {
return {
fps: this.#fps,
deltaTime: this.#deltaTime,
ctx: this.#context,
canvas: this.#canvas,
width: this.#canvas.width,
height: this.#canvas.height,
clearCanvas: this.#clearCanvas.bind(this),
drawSave: this.#drawSave.bind(this),
listenEvent: this.#listenEvent.bind(this),
initiateObj: this.#initiateObj.bind(this),
renderObj: this.#renderObj.bind(this),
...this.#data,
...(extra ?? {})
};
}
#debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
#listenEvent(event, callback, options) {
this.#events.push({
event,
callback,
options
});
this.#canvas.addEventListener(event, callback, options);
}
#initializeEventListeners() {
this.#canvas.removeEventListener('click', this.#handleClick.bind(this));
this.#canvas.removeEventListener('contextmenu', this.#handleContextmenu.bind(this));
this.#canvas.removeEventListener('mousedown', this.#handleMouseDown.bind(this));
this.#canvas.removeEventListener('mousemove', this.#handleMouseMove.bind(this));
this.#canvas.removeEventListener('mouseup', this.#handleMouseUp.bind(this));
window.removeEventListener('keydown', this.#handleKeyDown.bind(this));
window.removeEventListener('keyup', this.#handleKeyUp.bind(this));
this.#canvas.addEventListener('click', this.#handleClick.bind(this));
this.#canvas.addEventListener('contextmenu', this.#handleContextmenu.bind(this));
this.#canvas.addEventListener('mousedown', this.#handleMouseDown.bind(this));
this.#canvas.addEventListener('mousemove', this.#handleMouseMove.bind(this));
this.#canvas.addEventListener('mouseup', this.#handleMouseUp.bind(this));
window.addEventListener('keydown', this.#handleKeyDown.bind(this));
window.addEventListener('keyup', this.#handleKeyUp.bind(this));
}
#handleClick(event) {
this.#triggerEvents('click', event);
}
#handleContextmenu(event) {
event.preventDefault();
this.#triggerEvents('contextmenu', event);
return false;
}
#handleMouseDown(event) {
this.#triggerEvents('mousedown', event);
}
#handleMouseMove(event) {
const {
offsetX,
offsetY
} = event;
this.#data.mouseX = offsetX;
this.#data.mouseY = offsetY;
this.#triggerEvents('mousemove', event);
}
#handleMouseUp(event) {
this.#triggerEvents('mouseup', event);
}
#handleKeyDown(event) {
this.#triggerEvents('keydown', event);
}
#handleKeyUp(event) {
this.#triggerEvents('keyup', event);
}
#triggerEvents(eventType, event) {
if (this.#animationFrame) {
for (const {
event: registeredEvent,
callback,
options
} of this.#events) {
if (registeredEvent === eventType) {
callback(event);
}
}
}
}
static createObj({
onInitiate,
onRender
}) {
return new CanimeObj({
onInitiate,
onRender
});
}
}