import Component from "@pencil.js/component";
import OffscreenCanvas from "@pencil.js/offscreen-canvas";
import KeyboardEvent from "@pencil.js/keyboard-event";
import MouseEvent from "@pencil.js/mouse-event";
import Position from "@pencil.js/position";
/**
* @module Scene
*/
const listenForEventsKey = Symbol("_listenForEvents");
/**
* Scene class
* @class
* @extends {module:OffscreenCanvas}
*/
export default class Scene extends OffscreenCanvas {
/**
* Scene constructor
* @param {HTMLElement} [container=document.body] - Container of the renderer
* @param {SceneOptions} [options] - Specific options
*/
constructor (container = window.document.body, options) {
if (container === window.document.body) {
container.style.margin = 0;
container.style.height = `${window.innerHeight}px`;
}
const measures = container.getBoundingClientRect();
if (container instanceof window.HTMLCanvasElement) {
super(null, 0, options);
this.ctx = container.getContext("2d");
}
else {
super(container, 0, options);
container.appendChild(this.ctx.canvas);
}
/**
* @type {Position}
*/
this.cursorPosition = new Position();
/**
* @type {Position}
*/
this.containerPosition = new Position(measures.left + window.scrollX, measures.top + window.scrollY);
/**
* @type {Boolean}
*/
this.isScene = true;
/**
* @type {Boolean}
*/
this.isLooped = false;
/**
* @type {Boolean}
*/
this.isClicked = false;
/**
* @type {Number}
*/
this.fps = 0;
/**
* @type {Number}
*/
this.lastTick = null;
this[listenForEventsKey](container);
}
/**
*
* @param {String} [cursor=Component.cursors.default] - Cursor string
* @return {Scene} Itself
*/
setCursor (cursor = Component.cursors.default) {
this.ctx.canvas.style.cursor = cursor;
return this;
}
/**
* Draw the whole scene
* @return {Scene} Itself
*/
render () {
const animationId = this.isLooped ? window.requestAnimationFrame(this.render.bind(this, undefined)) : null;
try {
super.render(this.ctx);
}
catch (error) {
window.cancelAnimationFrame(animationId);
this.stopLoop();
throw error;
}
const now = window.performance.now();
if (this.isLooped && this.lastTick) {
this.fps = 1000 / (now - this.lastTick);
}
this.lastTick = now;
return this;
}
/**
* Define if is hovered
* @return {Boolean}
*/
isHover () {
return this.options.shown;
}
/**
* Start to render the scene each frame
* @return {Scene} Itself
*/
startLoop () {
this.isLooped = true;
this.render();
return this;
}
/**
* Stop scene from being rendered
* @return {Scene} Itself
*/
stopLoop () {
this.isLooped = false;
this.fps = 0;
return this;
}
/**
* @inheritDoc
* @return {Scene} Itself
*/
hide () {
this.ctx.canvas.style.visibility = "hidden";
return super.hide();
}
/**
* @inheritDoc
* @return {Scene} Itself
*/
show () {
this.ctx.canvas.style.visibility = "";
return super.show();
}
/**
* @inheritDoc
* @param {Object} definition - Scene definition
* @return {Scene}
*/
static from (definition) {
return new Scene(undefined, definition.options);
}
/**
* Build a canvas and set it to fill the entire document.body
* @param {HTMLElement} container - Element holding the canvas
* @inheritDoc
*/
static getDrawingContext (container = window.document.body) {
if (container) {
const { scrollWidth, scrollHeight } = container;
const ctx = super.getDrawingContext(scrollWidth, scrollHeight);
ctx.canvas.style.display = "block";
ctx.canvas.style.position = "absolute";
return ctx;
}
return null;
}
/**
* @typedef {Object} SceneOptions
* @extends OffscreenCanvasOptions
* @prop {String} [cursor=Component.cursors.defaultOptions] - Cursor on hover
*/
/**
* @type {SceneOptions}
*/
static get defaultOptions () {
return {
...super.defaultOptions,
cursor: Component.cursors.default,
};
}
/**
* @typedef {Object} SceneEvents
* @extends ContainerEvent
* @prop {String} change -
*/
/**
* @type {SceneEvents}
*/
static get events () {
return {
...super.events,
change: "change-scene",
};
}
}
/**
* Bind events and call them on targets (can't be called twice)
* @param {HTMLElement} container - Container to bind event to
* @memberOf Scene#
*/
Scene.prototype[listenForEventsKey] = function listenForEvents (container) {
if (this.isReady) {
throw new EvalError("Can't rebind event a second time.");
}
let hovered = null;
let startPosition = null;
const mouseListeners = {
[MouseEvent.events.down]: (target, eventPosition) => {
target.isClicked = true;
startPosition = eventPosition;
},
[MouseEvent.events.move]: (target, eventPosition) => {
if (startPosition) {
target.isClicked = target.isClicked && eventPosition.distance(startPosition) < 10;
}
if (target !== hovered) {
if (hovered) {
hovered.isHovered = false;
if (!hovered.isAncestorOf(target)) {
hovered.fire(new MouseEvent(MouseEvent.events.leave, hovered, eventPosition));
}
}
hovered = target;
}
if (!target.isHovered) {
target.isHovered = true;
target.fire(new MouseEvent(MouseEvent.events.hover, target, eventPosition));
}
this.setCursor(target.options.cursor);
this.cursorPosition.set(eventPosition);
},
[MouseEvent.events.up]: (target, eventPosition, event) => {
startPosition = null;
if (target.isClicked) {
target.fire(new MouseEvent(MouseEvent.events.click, target, eventPosition, event));
target.isClicked = false;
}
},
[MouseEvent.events.wheel]: (target, eventPosition, event) => {
const mouseEvents = MouseEvent.events;
const events = event.deltaY > 0 ?
[mouseEvents.scrollDown, mouseEvents.zoomOut] :
[mouseEvents.scrollUp, mouseEvents.zoomIn];
target.fire(new MouseEvent(events[0], target, eventPosition, event))
.fire(new MouseEvent(events[1], target, eventPosition, event));
},
mouseout: (target, eventPosition, event) => {
target.fire(new MouseEvent(MouseEvent.events.leave, target, eventPosition, event));
},
mouseenter: (target, eventPosition, event) => {
target.fire(new MouseEvent(MouseEvent.events.hover, target, eventPosition, event));
},
[MouseEvent.events.contextMenu]: null,
[MouseEvent.events.doubleClick]: null,
};
Object.keys(mouseListeners).forEach((eventName) => {
container.addEventListener(eventName, (event) => {
if (this.options.shown) {
const eventPosition = (new Position(event.clientX, event.clientY))
.subtract(this.containerPosition)
.add(window.scrollX, window.scrollY);
const target = this.getTarget(eventPosition, this.ctx);
if (target) {
target.fire(new MouseEvent(eventName, target, eventPosition, event));
if (mouseListeners[eventName] instanceof Function) {
mouseListeners[eventName](target, eventPosition, event);
}
}
}
}, {
passive: false,
});
});
const keyboardListener = {
[KeyboardEvent.events.down]: null,
[KeyboardEvent.events.up]: null,
};
Object.keys(keyboardListener).forEach((eventName) => {
window.document.addEventListener(eventName, (event) => {
if (this.options.shown) {
this.fire(new KeyboardEvent(eventName, this, event));
if (keyboardListener[eventName] instanceof Function) {
keyboardListener[eventName](event);
}
}
});
});
};