import Gui, { IGui } from './Gui' import Inspector, { IInspector } from './Inspector' import ToolsCommon from './Common' import * as MatterTypes from '@rozelin/matter-ts' const Matter = MatterTypes.default export interface IDemo { inline?: boolean example?: IDemoExample examples: IDemoExample[] resetOnOrientation: boolean preventZoom: boolean fullPage: boolean startExample: string | boolean appendTo: HTMLElement url?: string toolbar: { title: string | null url: string | null reset: boolean source: boolean inspector: boolean tools: boolean fullscreen: boolean exampleSelect: boolean } tools: { inspector: IInspector | boolean | null gui: IGui | boolean | null } dom: IDemoDom } export interface IDemoExample { id: string name: string init: (demo: IDemo) => IDemoExampleInstance instance: IDemoExampleInstance | null sourceLink?: string } export interface IDemoExampleInstance { engine: MatterTypes.Engine.IEngine render: MatterTypes.Render.IRender runner: MatterTypes.Runner.IRunner canvas: HTMLCanvasElement stop: () => void } export interface IDemoDom { root: HTMLElement | null title: HTMLElement | null header: HTMLElement | null exampleSelect: HTMLSelectElement | null buttonReset: HTMLButtonElement | null buttonSource: HTMLAnchorElement | null buttonTools: HTMLButtonElement | null buttonInspect: HTMLButtonElement | null buttonFullscreen: HTMLButtonElement | null } /** * A tool for for running and testing example scenes. * @module Demo */ export default class Demo { protected static _isIOS = window.navigator && /iPad|iPhone|iPod/.test(navigator.userAgent) && // @ts-ignore !window.MSStream protected static _matterLink = 'https://github.com/Rozelin-dc/matter-ts' /** * Creates a new demo instance. * See example for options and usage. * @function create * @param options */ public static create( options: MatterTypes.Common.DeepPartial = {} ): IDemo { const defaultDemo: Omit & { dom?: Partial } = { examples: [], resetOnOrientation: false, preventZoom: false, fullPage: false, startExample: true, appendTo: document.body, toolbar: { title: null, url: null, reset: true, source: false, inspector: false, tools: false, fullscreen: true, exampleSelect: false, }, tools: { inspector: null, gui: null, }, } const demo = Matter.Common.extend(defaultDemo, options) as unknown as IDemo if ( !options.toolbar || (demo.examples.length > 1 && options.toolbar && options.toolbar.exampleSelect !== false) ) { demo.toolbar.exampleSelect = true } if (Demo._isIOS) { demo.toolbar.fullscreen = false } demo.dom = Demo._createDom(demo) Demo._bindDom(demo) if (!demo.fullPage && demo.inline !== false) { demo.dom.root?.classList.add('matter-demo-inline') } if (demo.appendTo && demo.dom.root) { demo.appendTo.appendChild(demo.dom.root) } if (demo.startExample) { Demo.start(demo, demo.startExample) } return demo } /** * Starts a new demo instance by running the first or given example. * See example for options and usage. * @function start * @param demo * @param initalExampleId example to start (defaults to first) */ public static start(demo: IDemo, initialExampleId?: string | boolean): void { let exampleId = typeof initialExampleId === 'string' ? initialExampleId : demo.examples[0].id if (window.location.hash.length > 0) { exampleId = window.location.hash.slice(1) } Demo.setExampleById(demo, exampleId) } /** * Stops the currently running example in the demo. * This requires that the `example.init` function returned * an object specifiying a `stop` function. * @function stop * @param demo */ public static stop(demo: IDemo): void { if (demo.example && demo.example.instance) { demo.example.instance.stop() } } /** * Stops and restarts the currently running example. * @function reset * @param demo */ public static reset(demo: IDemo): void { // @ts-ignore Matter.Common._nextId = 0 // @ts-ignore Matter.Common._seed = 0 Demo.setExample(demo, demo.example) } /** * Starts the given example by its id. * Any running example will be stopped. * @function setExampleById * @param demo * @param exampleId */ public static setExampleById(demo: IDemo, exampleId: string): void { const example = demo.examples.filter((example) => { return example.id === exampleId })[0] Demo.setExample(demo, example) } /** * Starts the given example. * Any running example will be stopped. * @function setExample * @param demo * @param example */ public static setExample(demo: IDemo, example?: IDemoExample): void { if (!example) { Demo.setExample(demo, demo.examples[0]) return } const prevExample = demo.example let instance = prevExample?.instance if (instance) { instance.stop() if (instance.canvas) { instance.canvas.parentElement?.removeChild(instance.canvas) } } if (prevExample) { prevExample.instance = null } window.location.hash = example.id demo.example = example demo.example.instance = instance = example.init(demo) if (!instance.canvas && instance.render) { instance.canvas = instance.render.canvas } if (instance.canvas) { demo.dom.root?.appendChild(instance.canvas) } if (demo.dom.exampleSelect) { demo.dom.exampleSelect.value = example.id } if (demo.dom.buttonSource) { demo.dom.buttonSource.href = example.sourceLink || demo.url || '#' } setTimeout(function () { if (demo.tools.inspector) { Demo.setInspector(demo, true) } if (demo.tools.gui) { Demo.setGui(demo, true) } }, 500) } /** * Enables or disables the inspector tool. * If `enabled` a new `Inspector` instance will be created and the old one destroyed. * @function setInspector * @param demo * @param enabled */ public static setInspector(demo: IDemo, enabled: boolean): void { if (!enabled) { Demo._destroyTools(demo, true, false) demo.dom.root?.classList.toggle('matter-inspect-active', false) return } const instance = demo.example!.instance Demo._destroyTools(demo, true, false) demo.dom.root?.classList.toggle('matter-inspect-active', true) demo.tools.inspector = Inspector.create(instance!.engine, instance!.render) } /** * Enables or disables the Gui tool. * If `enabled` a new `Gui` instance will be created and the old one destroyed. * @function setGui * @param demo * @param enabled */ public static setGui(demo: IDemo, enabled: boolean): void { if (!enabled) { Demo._destroyTools(demo, false, true) demo.dom.root?.classList.toggle('matter-gui-active', false) return } const instance = demo.example!.instance Demo._destroyTools(demo, false, true) demo.dom.root?.classList.toggle('matter-gui-active', true) demo.tools.gui = Gui.create( instance!.engine, instance!.runner, instance!.render ) } public static _destroyTools( demo: IDemo, destroyInspector?: boolean, destroyGui?: boolean ): void { const inspector = demo.tools.inspector const gui = demo.tools.gui if (destroyInspector) { if (inspector && inspector !== true) { Inspector.destroy(inspector) } demo.tools.inspector = null } if (destroyGui) { if (gui && gui !== true) { Gui.destroy(gui) } demo.tools.gui = null } } public static _toggleFullscreen(demo: IDemo): void { let fullscreenElement = document.fullscreenElement || // @ts-ignore document.mozFullScreenElement || // @ts-ignore document.webkitFullscreenElement if (!fullscreenElement) { fullscreenElement = demo.dom.root if (fullscreenElement.requestFullscreen) { fullscreenElement.requestFullscreen() } else if (fullscreenElement.mozRequestFullScreen) { fullscreenElement.mozRequestFullScreen() } else if (fullscreenElement.webkitRequestFullscreen) { // @ts-ignore fullscreenElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT) } } else { if (document.exitFullscreen) { document.exitFullscreen() } else if ('mozCancelFullScreen' in document) { // @ts-ignore document.mozCancelFullScreen() } else if ('webkitExitFullscreen' in document) { // @ts-ignore document.webkitExitFullscreen() } } } public static _bindDom(demo: IDemo): void { const dom = demo.dom window.addEventListener('orientationchange', function () { setTimeout(() => { if (demo.resetOnOrientation) { Demo.reset(demo) } }, 300) }) if (demo.preventZoom) { document.body.addEventListener('gesturestart', function (event) { event.preventDefault() }) let allowTap = true let tapTimeout: NodeJS.Timeout document.body.addEventListener('touchstart', function (event) { if (!allowTap) { event.preventDefault() } allowTap = false clearTimeout(tapTimeout) tapTimeout = setTimeout(function () { allowTap = true }, 500) }) } if (dom.exampleSelect) { dom.exampleSelect.addEventListener('change', function () { const exampleId = this.options[this.selectedIndex].value Demo.setExampleById(demo, exampleId) }) } if (dom.buttonReset) { dom.buttonReset.addEventListener('click', function () { Demo.reset(demo) }) } if (dom.buttonInspect) { dom.buttonInspect.addEventListener('click', function () { const showInspector = !demo.tools.inspector Demo.setInspector(demo, showInspector) }) } if (dom.buttonTools) { dom.buttonTools.addEventListener('click', function () { const showGui = !demo.tools.gui Demo.setGui(demo, showGui) }) } if (dom.buttonFullscreen) { dom.buttonFullscreen.addEventListener('click', function () { Demo._toggleFullscreen(demo) }) const fullscreenChange = function () { const isFullscreen = document.fullscreen || // @ts-ignore document.webkitIsFullScreen || // @ts-ignore document.mozFullScreen document.body.classList.toggle('matter-is-fullscreen', isFullscreen) } document.addEventListener('webkitfullscreenchange', fullscreenChange) document.addEventListener('mozfullscreenchange', fullscreenChange) document.addEventListener('fullscreenchange', fullscreenChange) } } public static _createDom(options: IDemo): IDemoDom { // eslint-disable-next-line @typescript-eslint/no-var-requires const styles = require('../styles/demo.css') ToolsCommon.injectStyles(styles, 'matter-demo-style') const root = document.createElement('div') const exampleOptions = options.examples .map((example) => { return `` }) .join(' ') const preventZoomClass = options.preventZoom && Demo._isIOS ? 'prevent-zoom-ios' : '' root.innerHTML = ` ` const dom = { root: root.firstElementChild, title: root.querySelector('.matter-demo-title'), header: root.querySelector('.matter-header'), exampleSelect: root.querySelector('.matter-example-select'), buttonReset: root.querySelector('.matter-btn-reset'), buttonSource: root.querySelector('.matter-btn-source'), buttonTools: root.querySelector('.matter-btn-tools'), buttonInspect: root.querySelector('.matter-btn-inspect'), buttonFullscreen: root.querySelector('.matter-btn-fullscreen'), } as IDemoDom if (!options.toolbar.title && dom.title) { ToolsCommon.domRemove(dom.title) } if (!options.toolbar.exampleSelect && dom.exampleSelect?.parentElement) { ToolsCommon.domRemove(dom.exampleSelect.parentElement) } if (!options.toolbar.reset && dom.buttonReset) { ToolsCommon.domRemove(dom.buttonReset) } if (!options.toolbar.source && dom.buttonSource) { ToolsCommon.domRemove(dom.buttonSource) } if (!options.toolbar.inspector && dom.buttonInspect) { ToolsCommon.domRemove(dom.buttonInspect) } if (!options.toolbar.tools && dom.buttonTools) { ToolsCommon.domRemove(dom.buttonTools) } if (!options.toolbar.fullscreen && dom.buttonFullscreen) { ToolsCommon.domRemove(dom.buttonFullscreen) } return dom } }