core2d
Version:
Multiplatform 2D interaction engine
471 lines (378 loc) • 10.9 kB
JavaScript
;
import { ACL } from "./ACL.mjs";
import { Color } from "./Color.mjs";
import { CompositeOperations } from "./CompositeOperations.mjs";
import { Frame } from "./Frame.mjs";
import { Input } from "./Input.mjs";
import { Point } from "./Point.mjs";
import { Rect } from "./Rect.mjs";
import { RenderableList } from "./RenderableList.mjs";
import { Scene } from "./Scene.mjs";
import { Sound } from "./Sound.mjs";
import { Sprite } from "./Sprite.mjs";
import { Static } from "./Static.mjs";
const CANVAS_ELEMENT = "canvas";
const CONTEXT = "2d";
const DEFAULT_FRAME_TIME = 16;
const DEGRADATION_TOLERANCE = 10;
export const Engine = (() => {
const RENDER_STRATEGY = {
DEFAULT: () => {
ACL.window.requestAnimationFrame(render);
},
DEGRADED: () => {
render();
},
};
const RENDER_STRATEGIES = [RENDER_STRATEGY.DEFAULT, RENDER_STRATEGY.DEGRADED];
let _autoScale = true;
let _canvas =
Static.getElement("app") || Static.getElements(CANVAS_ELEMENT)[0];
let _context = _canvas.getContext(CONTEXT);
let _degraded = 0;
let _everyOther = true;
let _frameTime = DEFAULT_FRAME_TIME;
let _fullScreen = false;
let _height = _canvas.height;
let _imageCache = {};
let _keepAspect = false;
let _input = new Input();
let _lastRender;
let _name = "Game";
let _realHeight = _canvas.height;
let _realWidth = _canvas.width;
let _renderableLists = [];
let _renderStrategy = 0;
let _scene;
let _sound = new Sound();
let _transition;
let _width = _canvas.width;
class Engine {
static init(scene) {
_scene = scene;
boot(_canvas, _context);
}
static get bottom() {
return _height - 1;
}
static get center() {
return new Point(this.centerX, this.centerY);
}
static get centerX() {
return Math.floor(_width / 2);
}
static get centerY() {
return Math.floor(_height / 2);
}
static get height() {
return _height;
}
static getName() {
return _name;
}
static get offsetLeft() {
return _canvas.offsetLeft;
}
static get offsetTop() {
return _canvas.offsetTop;
}
static get right() {
return _width - 1;
}
static get everyOther() {
return _everyOther;
}
static get frameTime() {
return _frameTime;
}
static get realHeight() {
return _realHeight;
}
static get realWidth() {
return _realWidth;
}
static get width() {
return _width;
}
static addControllerDevice(device) {
_input.addController(device);
}
static clear() {
_context.clearRect(0, 0, _width, _height);
}
static colorize(
image,
fillStyle,
compositeOperation = CompositeOperations.SOURCE_IN
) {
const input = Static.getImage(image);
const output = ACL.document.createElement(CANVAS_ELEMENT);
output.width = input.width;
output.height = input.height;
const context = output.getContext(CONTEXT);
context.drawImage(input, 0, 0);
context.globalCompositeOperation = compositeOperation;
context.fillStyle = fillStyle;
context.fillRect(0, 0, output.width, output.height);
return output;
}
static fadeOut() {
_sound.fadeOut();
}
static flip(image) {
return invert(image, false, true);
}
static getController(id) {
return _input.getController(id);
}
static get controller() {
return this.getController();
}
static getPointer(id) {
return _input.getPointer(id);
}
static get pointer() {
return this.getPointer();
}
static load(namespace) {
const container = localStorage[namespace ?? Engine.getName()];
let result;
try {
result = container && JSON.parse(container);
} catch (error) {
console.log("Could not load saved game: " + error);
}
return result;
}
static mirror(image) {
return invert(image, true, false);
}
static mute() {
_sound.mute();
}
static paint(renderable, index = 0) {
if (index >= _renderableLists.length) {
for (let i = _renderableLists.length; i <= index; ++i) {
_renderableLists.push(new RenderableList());
}
}
_renderableLists[index].add(renderable);
}
static play(id, volume) {
_sound.play(id, volume);
}
static playTheme(name) {
_sound.playTheme(name);
}
static random(max) {
return Math.floor(Math.random() * (max + 1));
}
static rotate(image, degrees) {
const input = Static.getImage(image);
degrees = degrees % 360;
if (degrees == 0) {
return input;
}
const output = ACL.document.createElement(CANVAS_ELEMENT);
output.width = input.width;
output.height = input.height;
const context = output.getContext(CONTEXT);
context.translate(output.width / 2, output.height / 2);
context.rotate(Static.toRadians(degrees));
context.drawImage(input, -input.width / 2, -input.height / 2);
return output;
}
static save(data, namespace) {
try {
localStorage[namespace || Engine.getName()] =
data && JSON.stringify(data);
} catch (error) {
console.log("Could not save current game: " + error);
}
}
static setAutoScale(customAutoScale = true) {
_autoScale = customAutoScale;
}
static setFrameTime(frameTime) {
_frameTime = frameTime ?? DEFAULT_FRAME_TIME;
}
static setFullScreen(customFullScreen = true) {
_fullScreen = customFullScreen;
}
static setKeepAspect(customKeepAspect = true) {
_keepAspect = customKeepAspect;
}
static setName(name) {
_name = name;
document && (ACL.document.title = _name);
}
static stopTheme() {
_sound.stopTheme();
}
// factories
static animation(frames) {
return new Animation(frames);
}
static frame(image, duration) {
return new Frame(image, duration);
}
static image(id, isMirror, isFlip) {
if (isFlip && isMirror) {
if (_imageCache[id] && _imageCache[id].flipMirror) {
return _imageCache[id].flipMirror;
}
_imageCache[id] = _imageCache[id] || {};
_imageCache[id].flipMirror = invert(Static.getElement(id), true, true);
return _imageCache[id].flip;
}
if (isFlip) {
if (_imageCache[id] && _imageCache[id].flip) {
return _imageCache[id].flip;
}
_imageCache[id] = _imageCache[id] || {};
_imageCache[id].flip = Engine.flip(Static.getElement(id));
return _imageCache[id].flip;
}
if (isMirror) {
if (_imageCache[id] && _imageCache[id].mirror) {
return _imageCache[id].mirror;
}
_imageCache[id] = _imageCache[id] || {};
_imageCache[id].mirror = Engine.mirror(Static.getElement(id));
return _imageCache[id].mirror;
}
return Static.getElement(id);
}
static point(x, y) {
return new Point(x, y);
}
static rect(x, y, width, height) {
return new Rect(x, y, width, height);
}
static scene() {
return new Scene();
}
static sprite(scene) {
return new Sprite(scene);
}
}
function boot(canvas, context) {
_canvas.style.transform = "translateZ(0)";
addEventListener("blur", focus, false);
addEventListener("click", focus, false);
addEventListener("focus", focus, false);
addEventListener("load", focus, false);
addEventListener("resize", scale, false);
// focus(); // TODO: test all platforms before finally removing it
scale();
const images = Static.getElements("img");
const total = images.length;
let complete = 0;
for (let i = 0; i < images.length; ++i) {
const IMAGE = images[i];
if (IMAGE.complete) {
++complete;
}
}
context.fillStyle = Color.Blue;
context.fillRect(0, 0, (canvas.width * complete) / total, canvas.height);
if (complete < total) {
setTimeout(() => {
boot(canvas, context);
}, 100);
return;
}
initScene();
_lastRender = Date.now();
loop();
}
function focus() {
ACL.window.focus();
if (_fullScreen && _canvas.requestFullscreen) {
_canvas.requestFullscreen().catch((error) => {
console.warn("Could not request full screen", error);
});
}
}
function initScene() {
if (!_scene) {
throw new Error("Could not get the next scene");
}
_scene.scene = new Point();
_scene.height = _scene.height || _height;
_scene.width = _scene.width || _width;
_scene.init();
}
function invert(image, isMirror, isFlip) {
const input = Static.getImage(image);
const output = ACL.document.createElement(CANVAS_ELEMENT);
output.width = input.width;
output.height = input.height;
const context = output.getContext(CONTEXT);
context.translate(isMirror ? output.width : 0, isFlip ? output.height : 0);
context.scale(isMirror ? -1 : 1, isFlip ? -1 : 1);
context.drawImage(input, 0, 0);
return output;
}
function loop() {
_everyOther = !_everyOther;
_input.update();
if (_transition != null) {
if (_transition.sync()) {
_transition = null;
initScene();
} else {
Engine.paint(_transition);
}
} else {
if (_scene.sync()) {
_transition = _scene.transition;
if (_transition) {
_transition.scene = _scene;
_transition.init();
}
_scene = _scene.next;
if (!_transition) {
initScene();
}
}
}
_sound.update();
RENDER_STRATEGIES[_renderStrategy]();
}
function render() {
for (let i = 0; i < _renderableLists.length; ++i) {
_renderableLists[i].render(_context);
}
const timeout = _frameTime + _lastRender - Date.now();
if (timeout < 0 && ++_degraded > DEGRADATION_TOLERANCE) {
if (++_renderStrategy > RENDER_STRATEGIES.length - 1) {
_renderStrategy = 0;
}
}
setTimeout(loop, timeout);
_lastRender = Date.now();
}
function scale() {
if (!_autoScale) {
return;
}
let width, height;
if (_keepAspect) {
let proportion = ACL.window.innerWidth / _canvas.width;
if (ACL.window.innerHeight < _canvas.height * proportion) {
proportion = ACL.window.innerHeight / _canvas.height;
}
width = _canvas.width * proportion;
height = _canvas.height * proportion;
} else {
width = ACL.window.innerWidth;
height = ACL.window.innerHeight;
}
_realWidth = width;
_realHeight = height;
_canvas.style.width = width + "px";
_canvas.style.height = height + "px";
}
return Engine;
})();