@spearwolf/twopoint5d
Version:
a library to create 2.5d realtime graphics and pixelart with three.js
288 lines • 11.7 kB
JavaScript
import { emit, eventize, off, on, once, retain, retainClear } from '@spearwolf/eventize';
import { WebGLRenderer } from 'three';
import { Chronometer } from './Chronometer.js';
import { DisplayStateMachine } from './DisplayStateMachine.js';
import { Stylesheets } from './Stylesheets.js';
import { getContentAreaSize, getHorizontalInnerMargin, getIsContentBox, getVerticalInnerMargin } from './styleUtils.js';
let canvasMaxResolutionWarningWasShown = false;
function showCanvasMaxResolutionWarning(w, h) {
if (!canvasMaxResolutionWarningWasShown) {
console.warn(`Oops, the canvas width or height should not bigger than ${Display.MaxResolution} pixels (${w}x${h} was requested).`, 'If you need more, please set Display.MaxResolution before you create a Display!');
canvasMaxResolutionWarningWasShown = true;
}
}
export class Display {
static { this.MaxResolution = 8192; }
static { this.CssRulesPrefixContainer = 'twopoint5d-container'; }
static { this.CssRulesPrefixDisplay = 'twopoint5d-canvas'; }
static { this.CssRulesPrefixFullscreen = 'twopoint5d-canvas--fullscreen'; }
#chronometer;
#stateMachine;
#rafID;
#lastResizeHash;
#fullscreenCssRules;
#fullscreenCssRulesMustBeRemoved;
constructor(domElementOrRenderer, options) {
this.#chronometer = new Chronometer(0);
this.#stateMachine = new DisplayStateMachine();
this.#rafID = -1;
this.#lastResizeHash = '';
this.#fullscreenCssRulesMustBeRemoved = false;
this.pixelZoom = 0;
this.styleImageRendering = undefined;
this.width = 0;
this.height = 0;
this.frameNo = 0;
this.#emitEvent = (eventName) => {
emit(this, eventName, this.getEventArgs());
};
eventize(this);
retain(this, ['init', 'start', 'resize']);
this.#chronometer.stop();
this.resizeToCallback = options?.resizeTo;
this.styleSheetRoot = options?.styleSheetRoot ?? document.head;
if (domElementOrRenderer instanceof WebGLRenderer || domElementOrRenderer.isWebGPURenderer) {
this.renderer = domElementOrRenderer;
this.resizeToElement = this.renderer.domElement;
}
else if (domElementOrRenderer instanceof HTMLElement) {
let canvas;
if (domElementOrRenderer.tagName === 'CANVAS') {
canvas = domElementOrRenderer;
}
else {
const container = document.createElement('div');
Stylesheets.addRule(container, Display.CssRulesPrefixContainer, 'display:block;width:100%;height:100%;margin:0;padding:0;border:0;line-height:0;font-size:0;', this.styleSheetRoot);
domElementOrRenderer.appendChild(container);
canvas = document.createElement('canvas');
container.appendChild(canvas);
}
this.resizeToElement = domElementOrRenderer;
if (options?.webgpu) {
console.warn('WebGPU is not supported yet. If you want to use WebGPU, please create a WebGPURenderer instance and pass it to the :renderer option!');
}
this.renderer = new WebGLRenderer({
canvas,
precision: 'highp',
preserveDrawingBuffer: false,
stencil: false,
alpha: true,
antialias: true,
powerPreference: 'high-performance',
...options,
});
}
const { domElement: canvas } = this.renderer;
Stylesheets.addRule(canvas, Display.CssRulesPrefixDisplay, 'touch-action: none;', this.styleSheetRoot);
canvas.setAttribute('touch-action', 'none');
this.resizeToElement = options?.resizeToElement ?? this.resizeToElement;
this.resizeToAttributeEl = options?.resizeToAttributeEl ?? canvas;
this.resize();
on(this.#stateMachine, {
[DisplayStateMachine.Init]: async () => {
if (this.renderer.isWebGPURenderer) {
await this.renderer.init();
}
this.#emitEvent('init');
},
[DisplayStateMachine.Restart]: () => this.#emitEvent('restart'),
[DisplayStateMachine.Start]: () => {
this.#chronometer.start();
this.#chronometer.update();
const renderFrame = (now) => {
if (!this.pause) {
this.renderFrame(now);
}
this.#rafID = window.requestAnimationFrame(renderFrame);
};
this.#rafID = window.requestAnimationFrame(renderFrame);
this.#emitEvent('start');
},
[DisplayStateMachine.Pause]: () => {
window.cancelAnimationFrame(this.#rafID);
this.#chronometer.stop();
retainClear(this, 'start');
this.#emitEvent('pause');
},
});
if (typeof document !== 'undefined') {
const onDocVisibilityChange = () => {
this.#stateMachine.documentIsVisible = !document.hidden;
};
document.addEventListener('visibilitychange', onDocVisibilityChange, false);
once(this, 'dispose', () => {
document.removeEventListener('visibilitychange', onDocVisibilityChange, false);
});
onDocVisibilityChange();
}
}
get now() {
return this.#chronometer.time;
}
get deltaTime() {
return this.#chronometer.deltaTime;
}
get pause() {
return this.#stateMachine.state === DisplayStateMachine.PAUSED;
}
set pause(pause) {
this.#stateMachine.pausedByUser = pause;
}
get pixelRatio() {
if (this.pixelZoom > 0) {
return 1.0;
}
return window.devicePixelRatio ?? 1;
}
resize() {
let wPx = 300;
let hPx = 150;
const canvasElement = this.renderer.domElement;
let sizeRefElement = this.resizeToElement;
let fullscreenCssRulesMustBeRemoved = this.#fullscreenCssRulesMustBeRemoved;
if (this.resizeToAttributeEl.hasAttribute('resize-to')) {
const resizeTo = this.resizeToAttributeEl.getAttribute('resize-to').trim();
if (resizeTo.match(/^:?(fullscreen|window)$/)) {
wPx = window.innerWidth;
hPx = window.innerHeight;
sizeRefElement = undefined;
let fullscreenCssRules = this.#fullscreenCssRules;
if (!fullscreenCssRules) {
fullscreenCssRules = Stylesheets.installRule(Display.CssRulesPrefixFullscreen, `position:fixed;top:0;left:0;`, this.styleSheetRoot);
this.#fullscreenCssRules = fullscreenCssRules;
}
if (fullscreenCssRulesMustBeRemoved) {
fullscreenCssRulesMustBeRemoved = false;
}
else {
canvasElement.classList.add(fullscreenCssRules);
this.#fullscreenCssRulesMustBeRemoved = true;
}
}
else if (resizeTo === 'self') {
sizeRefElement = this.resizeToElement ?? canvasElement;
}
else if (resizeTo) {
sizeRefElement = document.querySelector(resizeTo) ?? this.resizeToElement ?? canvasElement;
}
}
if (fullscreenCssRulesMustBeRemoved) {
if (this.#fullscreenCssRules) {
canvasElement.classList.remove(this.#fullscreenCssRules);
}
this.#fullscreenCssRulesMustBeRemoved = false;
}
if (this.resizeToCallback) {
const size = this.resizeToCallback(this);
if (size) {
wPx = size[0];
hPx = size[1];
}
}
else if (sizeRefElement) {
const area = getContentAreaSize(sizeRefElement);
wPx = area.width;
hPx = area.height;
}
let cssWidth = wPx;
let cssHeight = hPx;
const canvasStyle = getComputedStyle(canvasElement, null);
const canvasIsContentBox = getIsContentBox(canvasStyle);
const canvasHorizontalInnerMargin = getHorizontalInnerMargin(canvasStyle);
const canvasVerticalInnerMargin = getVerticalInnerMargin(canvasStyle);
if (canvasIsContentBox && canvasElement !== sizeRefElement) {
wPx -= canvasHorizontalInnerMargin;
hPx -= canvasVerticalInnerMargin;
cssWidth -= canvasHorizontalInnerMargin;
cssHeight -= canvasVerticalInnerMargin;
}
else if (!canvasIsContentBox && canvasElement === sizeRefElement) {
cssWidth += canvasHorizontalInnerMargin;
cssHeight += canvasVerticalInnerMargin;
}
if (wPx < 0) {
wPx = 0;
}
if (hPx < 0) {
hPx = 0;
}
if (cssWidth < 0) {
cssWidth = 0;
}
if (cssHeight < 0) {
cssHeight = 0;
}
if (wPx > Display.MaxResolution) {
wPx = Display.MaxResolution;
showCanvasMaxResolutionWarning(wPx, hPx);
}
if (hPx > Display.MaxResolution) {
hPx = Display.MaxResolution;
showCanvasMaxResolutionWarning(wPx, hPx);
}
const { pixelRatio, pixelZoom } = this;
const resizeHash = `${wPx}|${cssWidth}x${hPx}|${cssHeight}x${pixelRatio},${pixelZoom}`;
if (resizeHash !== this.#lastResizeHash) {
this.#lastResizeHash = resizeHash;
if (pixelZoom > 0) {
this.width = wPx / pixelZoom;
this.height = hPx / pixelZoom;
}
else {
this.width = wPx;
this.height = hPx;
}
this.width = Math.floor(this.width);
this.height = Math.floor(this.height);
this.renderer.setPixelRatio(pixelRatio);
this.renderer.setSize(this.width, this.height, false);
canvasElement.style.width = `${cssWidth}px`;
canvasElement.style.height = `${cssHeight}px`;
canvasElement.style.imageRendering = this.styleImageRendering ?? (pixelZoom > 0 ? 'pixelated' : 'auto');
if (this.frameNo > 0) {
this.#emitEvent('resize');
}
}
}
renderFrame(now = window.performance.now()) {
this.#chronometer.update(now / 1000);
this.resize();
if (this.frameNo === 0) {
this.#emitEvent('resize');
}
this.#emitEvent('frame');
++this.frameNo;
}
async start(beforeStartCallback) {
if (typeof beforeStartCallback === 'function') {
await beforeStartCallback(this.getEventArgs());
}
this.#stateMachine.pausedByUser = false;
this.#stateMachine.start();
return this;
}
stop() {
this.#stateMachine.pausedByUser = true;
}
dispose() {
this.stop();
emit(this, 'dispose');
off(this);
this.renderer?.dispose();
delete this.renderer;
}
getEventArgs() {
return {
display: this,
renderer: this.renderer,
width: this.width,
height: this.height,
pixelRatio: this.pixelRatio,
now: this.now,
deltaTime: this.deltaTime,
frameNo: this.frameNo,
};
}
#emitEvent;
}
//# sourceMappingURL=Display.js.map