UNPKG

@spearwolf/twopoint5d

Version:

a library to create 2.5d realtime graphics and pixelart with three.js

288 lines 11.7 kB
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