UNPKG

@exabytellc/canvas

Version:
234 lines 6.58 kB
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 }); } }