/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import Phaser from "phaser" import TransitionImage from "phaser3-rex-plugins/plugins/transitionimage.js" import DissolvePipelinePlugin from "phaser3-rex-plugins/plugins/dissolvepipeline-plugin" type BGMap = Record // id, url type TransitionConfig = Partial & { key?: string } type TransitionGenerator = (scene: Phaser.Scene, duration: number) => TransitionConfig class BGImage extends TransitionImage { constructor(scene, x, y, texture?, config?) { super(scene, x, y, texture, undefined, config) // ... } } interface SceneListener { onSceneReady?: () => void } class BG extends Phaser.Scene { protected bgMap: BGMap protected image: TransitionImage protected listener: SceneListener constructor( imagesToLoad: BGMap, listener?: SceneListener, sceneConfig: Partial = {} ) { super({ key: "backgrounds", plugins: { input: false, }, ...sceneConfig, }) this.bgMap = imagesToLoad if (listener) { this.listener = listener } } preload() { // load all of the images in the bgMap for (let key in this.bgMap) { const val = this.bgMap[key] if (typeof val === "string") { this.load.image(key, val) } else { this.load.texture(key, val) } } } create() { this.image = new BGImage(this, this.getWidth() / 2, this.getHeight() / 2, "__WHITE") this.add.existing(this.image) this.autoResize() if (this.listener?.onSceneReady) { this.listener.onSceneReady() } this.image.on("complete", () => { this.autoResize() let anyImage = this.image as any anyImage.frontImage.tint = anyImage.backImage.tintTopLeft }) } transition(to: string, config: TransitionConfig = {}) { this.image.transit({ key: to, ...config, onStart: (parent, currentImage, nextImage, t) => { if (config.onStart) { config.onStart(parent, currentImage, nextImage, t) } this.autoResize(currentImage) this.autoResize(nextImage) }, }) } getWidth() { return this.sys.renderer.width } getHeight() { return this.sys.renderer.height } getImage() { return this.image } autoResize(image?: Phaser.GameObjects.Container | Phaser.GameObjects.Image) { let targetImage = image ?? (this.image as any).frontImage targetImage.setX(this.getWidth() / 2).setY(this.getHeight() / 2) const containerWidth = this.getWidth() const containerHeight = this.getHeight() const imgRatio = targetImage.height / targetImage.width // original img ratio const containerRatio = containerHeight / containerWidth // container ratio let finalWidth let finalHeight if (containerRatio > imgRatio) { finalHeight = containerHeight finalWidth = containerHeight / imgRatio } else { finalWidth = containerWidth finalHeight = containerWidth * imgRatio } // doing the scaling targetImage.displayWidth = finalWidth targetImage.displayHeight = finalHeight } } type PhaserBGEngineConfig = Phaser.Types.Core.GameConfig & { autoResizeBGs?: boolean autoResizeGame?: boolean domElement?: HTMLElement // dom element to attach this entire game to. sceneConfig?: Partial onLoaded?: () => void // fired when the engine is loaded } const defaultBGEngineConfig = { type: Phaser.AUTO, scale: { mode: Phaser.Scale.RESIZE, }, autoResizeBGs: true, autoResizeGame: false, } const defaultTransitionConfig: TransitionConfig = { duration: 500, onStart: () => {}, onProgress: (parent, currentImage, nextImage, t) => { parent.setChildLocalAlpha(currentImage, 1 - t).setChildLocalAlpha(nextImage, t) }, onComplete: (parent, currentImage, nextImage, t) => { parent.setChildLocalAlpha(currentImage, 1) }, mask: undefined, dir: "out", ease: "Linear", } export class PhaserBGEngine implements SceneListener { protected game: Phaser.Game protected bg: BG protected isActive: boolean = false protected timeout: number = 500 protected config: PhaserBGEngineConfig constructor(imagesToLoad: BGMap, config: PhaserBGEngineConfig = {}, existingGame?: Phaser.Game) { const mergedConfig = { ...defaultBGEngineConfig, ...config } this.config = mergedConfig this.bg = new BG(imagesToLoad, this, config.sceneConfig) if (existingGame) { this.game = existingGame } else { this.game = new Phaser.Game(mergedConfig) } if (mergedConfig.domElement) { this.addToDOM(mergedConfig.domElement) } this.game.scene.add("backgrounds", this.bg, true) } onSceneReady() { this.isActive = true if ((this.config.autoResizeGame || this.config.autoResizeBGs) && !this.config.domElement) { this.resize(window.innerWidth, window.innerHeight) window.addEventListener("resize", () => { this.resize(window.innerWidth, window.innerHeight) }) } if (this.config.onLoaded) { this.config.onLoaded() } } /** * Transitions to a new image for the background. * @param to the key for the image, which should be taken from the imagesToLoad that was fed to the constructor * @param config can be manually created, or generated from an imported TransitionGenerator to perform various shader effects. */ transition(to: string, config: TransitionConfig = {}) { const mergedConfig = { ...defaultTransitionConfig, ...config, onStart: function (parent, currentImage, nextImage, t) { // reset the color tint unless we're trying to transition colors...this is why we run this // before config.start nextImage.tint = 0xffffff if (config.onStart) { config.onStart(parent, currentImage, nextImage, t) } }, } if (!this.isActive) { const self = this setTimeout(() => { if (!this.isActive) { // fail silently. We might want to instead try again later. console.warn( "Attempted to transition before the scene was ready. Please use a preloader if possible." ) return } self.bg.transition(to, mergedConfig) }, this.timeout) return } this.bg.transition(to, mergedConfig) } transitionColor(color: number, config: TransitionConfig = {}) { const mergedConfig: TransitionConfig = { ...config, onStart: function (parent, currentImage, nextImage, t) { if (config.onStart) { config.onStart(parent, currentImage, nextImage, t) } nextImage.tint = color }, } this.transition("__WHITE", mergedConfig) } get width() { return this.bg.getWidth() } get height() { return this.bg.getHeight() } get phaser() { return this.game } get scene() { return this.bg } get image() { return this.bg.getImage() } resize(width: number, height: number) { if (this.config.autoResizeGame) { this.game.scale.resize(width, height) } if (this.config.autoResizeBGs) { this.bg.autoResize() } } addToDOM(element: HTMLElement) { element.appendChild(this.game.canvas) } } export const CrossfadeTransition: TransitionGenerator = (scene, duration) => { return { duration, ease: "Linear", dir: "out", onStart: function (parent, currentImage, nextImage, t) {}, onProgress: function (parent, currentImage, nextImage, t) { parent.setChildLocalAlpha(currentImage, 1 - t).setChildLocalAlpha(nextImage, t) }, onComplete: function (parent, currentImage, nextImage, t) { parent.setChildLocalAlpha(currentImage, 1) }, } } export const DissolveTransition: TransitionGenerator = (scene, duration) => { let postFxPlugin = scene.plugins.get("rexdissolvepipelineplugin") as DissolvePipelinePlugin if (!postFxPlugin) { scene.plugins.install("rexdissolvepipelineplugin", DissolvePipelinePlugin) postFxPlugin = scene.plugins.get("rexdissolvepipelineplugin") as DissolvePipelinePlugin } return { duration, ease: "Linear", dir: "out", onStart: function (parent, currentImage, nextImage, t) { postFxPlugin.add(currentImage) }, onProgress: function (parent, currentImage, nextImage, t) { postFxPlugin.get(currentImage)[0].setProgress(t) }, onComplete: function (parent, currentImage, nextImage, t) { postFxPlugin.remove(currentImage) }, } }