Reference Source

src/parallax.mjs

import { Logger } from './vendor/logger.min.mjs';
import { Layer } from './layer.mjs'

class ParallaxSingleton {
	/**
	 * The version of the module.
	 */
	version = "VERSION_REPLACE_ME";
    /** The logger module this module uses to log errors / logs
     * @private
     * @type {Object}
     */
    logger = new Logger();
    /**
     * The layer class.
     * @type {Layer}
     */
    Layer = Layer;
    /**
     * An set of instances that use the parallax system.
     * @private
     * @type {Set}
     */
    instances = new Set();
    /**
     * Weakmap to store info on instances used in this module.
     * @private
     * @type {WeakMap}
     */
    instanceWeakMap = new WeakMap();
    /**
     * The last position of the camera.
     * @private
     * @type {Object}
     */
    lastCamPos = { 
        x: 0,
        y: 0
    }
    /**
     * @private
     */
	constructor() {
        this.logger.registerType('Parallax-Module', '#ff6600');
	}
    /**
     * Adds an instance to the parallax system.
     * Call this first and then add your instance to the map.
     * @param {Object} pInstance - The instance to add to the parallax system.
     * @param {Object} pConfig - The parallax info that tells this module how to control this instance.
     * @prop {number} pConfig.x - The x multiplier for this instance. Controls how fast or slow this instance moves. -Infinity to Infinity. 1 to move with camera.
     * @prop {number} pConfig.y - The y multiplier for this instance. Controls how fast or slow this instance moves. -Infinity to Infinity. 1 to move with camera.
     * @prop {boolean} pConfig.loop - Whether this instance will loop endlessly.
     * @param {number} [pX] - The x position this instance will start at.
     * @param {number} [pY] - The y position this instance will start at.
     * @param {string} [pMap] - The map this instance will start at.
     * 
     * ## The following is how the speed of the parallax multipliers are factored in.  
     (x | y) < 1 = faster behind the camera eg: (-> Player goes this way = Instance goes this way <-)  
     (x | y) > 1 faster against the camera eg: (-> Player goes this way = Instance goes this way ->)  
     (x | y) = 0 = static to the camera eg: (-> Player goes this way = Instance does nothing, and moves with the camera)   
     (x | y) = 1 = moves with the camera eg: (-> Player goes this way = Instance goes this way -> at position of camera)  
     */
    add(pInstance, pConfig, pX, pY, pMap) {
        if (!pInstance) {
            this.logger.prefix('Parallax-Module').error('No pInstance passed!');
            return;
        }

        if (pConfig instanceof Object) {
            if (!this.instances.has(pInstance)) {
                const x = typeof pX === 'number' ? pX : pInstance.x;
                const y = typeof pY === 'number' ? pY : pInstance.y;
                const map = typeof pMap === 'string' ? pMap : pInstance.mapName;
                // Clone the parallax object
                const parallaxInfo = { ...pConfig };
                this.init(pInstance, parallaxInfo, x, y, map);
                // Set the parallax info to the instance
                this.instanceWeakMap.set(pInstance, parallaxInfo);
                this.instances.add(pInstance);
            }
        } else {
            this.logger.prefix('Parallax-Module').error('No pConfig passed or invalid type found!');
        }
    }
    /**
     * Initializes this instance.
     * @param {Object} pInstance - The instance to initialize.
     * @param {Object} pConfig - The parallax info that tells this module how to control this instance.
     * @prop {number} pConfig.x - The x multiplier for this instance. Controls how fast or slow this instance moves. -Infinity to Infinity. 0 to move with camera.
     * @prop {number} pConfig.y - The y multiplier for this instance. Controls how fast or slow this instance moves. -Infinity to Infinity. 0 to move with camera.
     * @prop {boolean} pConfig.loop - Whether this instance will loop endlessly.
     * @param {number} pX - The x position this parallax will start at.
     * @param {number} pY - The y position this parallax will start at.
     * @param {string} pMap - The map this instance will start at.
     * @private
     */
    init(pInstance, pConfig, pX, pY, pMap) {
        if (!VYLO) {
            this.logger.prefix('Parallax-Module').error('VYLO not found! This module depends on the VYLO object being in the global name space.');
            return;
        }

        // If this instance is set to loop, then it needs a left and right clone
        if (pConfig.loop) {
            // Create a left and right clone
            const left = VYLO.newDiob('MapObject');
            const right = VYLO.newDiob('MapObject');

            // Make the left and right clone particle look the same as the initial instance
            left.setAppearance(pInstance);
            right.setAppearance(pInstance);

            // Force the renderer to render it, as if its placed offscreen its not rendered.
            left.setPos(pX, pY, pMap);
            right.setPos(pX, pY, pMap);

            // Position the left clone
            left.x = pX - pInstance.icon.width;
            left.y = pY;
            // Position the right clone
            right.x = pX + pInstance.icon.width;
            right.y = pY;
            // Store the clones in a temporary array
            const children = [left, right];
            // Loop the clones and store their relative positions to the main instance
            children.forEach((pChild) => {
                pChild.relativeX = pChild.x - pX;
                pChild.relativeY = pChild.y - pY;
            });

            // Do not mutate event if one is found. Call alongside it.
            const oldRelocatedEvent = pInstance.onRelocated;
            // When the main instance moves, move the clones with their relative position to it.
            if (typeof oldRelocatedEvent === 'function') {
                pInstance.onRelocated = (pX, pY) => {
                    oldRelocatedEvent.call(pInstance, pX, pY);
                    this.handleOnRelocated(pInstance, children);
                }
            } else {
                pInstance.onRelocated = (pX, pY) => {
                    this.handleOnRelocated(pInstance, children);
                }
            }
        }
        const viewEye = VYLO.Client.getViewEye();
        let lastCamX = 0;
        let lastCamY = 0;
        if (viewEye) {
            lastCamX = viewEye.x;
            lastCamY = viewEye.y;
        }
        this.lastCamPos.x = lastCamX;
        this.lastCamPos.y = lastCamY;
        pInstance.setPos(pX, pY, pMap);
    }
    /**
     * Removes an instance to the parallax system.
     * @param {Object} pInstance - The instance to remove to the parallax system.
     */
    remove(pInstance) {
        if (!pInstance) {
            this.logger.prefix('Parallax-Module').error('No pInstance passed!');
            return;
        }

        if (this.instances.has(pInstance)) {
            this.instances.delete(pInstance);
            this.instanceWeakMap.delete(pInstance);
        }
    }
    /**
     * Updates the parallax system.
     * @param {number} pCameraX - The x position of the camera.
     * @param {number} pCameraY - The y position of the camera.
     */
    update(pCameraX = 0, pCameraY = 0) {
        // The camera's x position.
        let cameraX = pCameraX;
        // The camera's x position.
        let cameraY = pCameraY;
        this.instances.forEach((pInstance) => {
            const parallaxInfo = this.instanceWeakMap.get(pInstance);

            // Move the instance with the camera if the parallax is set to 0
            const isBackgroundX = parallaxInfo.x === 0;
            const isBackgroundY = parallaxInfo.y === 0;

            // Position to set the instance to.
            let x;
            let y;

            if (isBackgroundX) {
                x = cameraX - pInstance.icon.width / 2;
            } else {
                let deltaX = cameraX - this.lastCamPos.x;
                let distX = deltaX * parallaxInfo.x;
                x = pInstance.x + distX;
            }

            if (isBackgroundY) {
                y = cameraY - pInstance.icon.height / 2;
            } else {
                let deltaY = cameraY - this.lastCamPos.y;
                let distY = deltaY * parallaxInfo.y;
                y = pInstance.y + distY;
            }

            // Set the position
            pInstance.x = x;
            pInstance.y = y;
            
            // Logic cannot be ran on background instances as they should not loop
            if (!isBackgroundX && !isBackgroundY) {
                if (parallaxInfo.loop) {
                    // The start pos + total width
                    const rightEnd = pInstance.x + pInstance.icon.width;
                    // The start pos - total width / 2
                    const leftEnd = pInstance.x - pInstance.icon.width / 6;
                    if (cameraX > rightEnd) {
                        pInstance.x += pInstance.icon.width;
                    } else if (cameraX < leftEnd) {
                        pInstance.x -= pInstance.icon.width;
                    }
                }
            }
        });
        this.lastCamPos.x = cameraX;
        this.lastCamPos.y = cameraY;
    }
    /**
     * Handles the onRelocated event for instances. Moves their children in relativity to their position.
     * @param {Diob | MapObject} pInstance - The instance to handle the event for.
     * @param {MapObject[]} pChildren - An array of children belonging to the instance.
     */
    handleOnRelocated(pInstance, pChildren) {
        // Update the children's position when the parent moves
        pChildren.forEach((pChild) => {
            pChild.x = pInstance.x + pChild.relativeX;
            pChild.y = pInstance.y + pChild.relativeY;
            pChild.mapName = pInstance.mapName;
        });
    }
}

const Parallax = new ParallaxSingleton();
export { Parallax };