game_actor_camera_camera.js

import Actor from "../actor.js";
import Vec2 from "../../vector/vec2.js";
import CameraShake from "./effect/shake_effect.js";
import Color from "../../../util/color/color.js";
import ColorGradient from "../../../util/color/color_gradient.js";
import FrameTask from "../../../util/task/frame_task.js";

/**
 * @module Camera
 * @fileoverview Contains Camera class.
 */

/**
 * @class
 * @extends Actor
 * Used to draw a space. A camera is and will behave like an actor.
 */
class Camera extends Actor {
    /**
     * Create a new camera.
     * @param {Vec2} pos Initial position
     * @param {Actor|null} toFollow Actor to follow/track (optional)
     */
    constructor(pos = new Vec2(), toFollow = null) {
        super({
            pos: pos
        });

        this.zoom = 0;
        this.zoomMin = 0;
        this.zoomMax = 4;

        this.toFollow = toFollow;

        this.speed = 2;
        this.followDistance = 150;

        this.shake = null;

        this.filter = null;
    }


    //
    // Getters
    //

    /**
     * Get the positional offset of the relative to the game canvas
     * and camera. Used for drawing the world and querying mouse position.
     * @returns {Vec2} Positional offset of game canvas and camera
     */
    getOffset() {
        let offset = new Vec2($$.width, $$.height).divv(2).sub(this._pos);
        if (this.shake !== null)
            offset = offset.add(this.shake.val);

        return offset;
    }

    /**
     * Get the positional offset of the relative to the game canvas
     * and camera, while taking magnification into account. Similar to getOffset().
     * @returns {Vec2} Positional offset of game canvas and camera, with the zoom taken into account
     */
    getOffsetWithZoom() {
        let offset = this._pos.sub(new Vec2($$.width, $$.height).divv(this.calcZoom()).divv(2));
        if (this.shake !== null)
            offset = offset.add(this.shake.val);

        return offset;
    }

    /**
     * Get the actual value of the zoom.
     * @returns {number} Actual zoom value
     */
    calcZoom() {
        return Math.pow(2, this.zoom);
    }


    //
    // Update & Draw
    //

    /**
     * Update the camera.
     * @param {number} elapsed Time since last update in seconds
     */
    update(elapsed) {
        if (this.toFollow != null)
            this.doFollow(elapsed);

        if (this.shake !== null)
            this.shake.update(elapsed);

        if (this.zoom > this.zoomMax)
            this.zoom = this.zoomMax;
        else if (this.zoom < this.zoomMin)
            this.zoom = this.zoomMin;
    }

    /**
     * Draw debug information for the camera (if enabled).
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw on
     * @param {boolean} showID Whether to show chunk actor ID
     * @param {boolean} showDot Whether to show the dot in the center
     */
    drawDebug(ctx, showID = false, showDot = true) {
        super.drawDebug(ctx, showID, showDot);

        if ($$.debug.actors && this.zoom !== 0) {
            ctx.fillStyle = $$.colors.debug_body;
            ctx.textAlign = "center";

            const zoom = this.calcZoom();
            ctx.font = "bold 16px " + $$.fonts.debug;

            ctx.save();
            ctx.translate(this._pos.x, this._pos.y);
            ctx.scale(1 / zoom, 1 / zoom);

            let z = this.calcZoom();

            ctx.fillText("Zoom: " + z.toFixed(2) + "x", 0, $$.height / 2 - 38);

            const pos = this.pos.floor();
            ctx.fillText("Pos: (" + pos.x + "," + pos.y + ")", 0, $$.height / 2 - 16);

            ctx.restore();
        }
    }

    /**
     * Draw the current filter over the game, if there is one.
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw on
     */
    drawFilter(ctx) {
        if (this.filter === null)
            return;

        ctx.fillStyle = this.filter.get();
        ctx.fillRect(0, 0, $$.width, $$.height);
    }

    /**
     * Do the translation of the camera in following the
     * actor, if one is set.
     * @param {number} elapsed Time elapsed since last update
     */
    doFollow(elapsed) {
        const x = this.toFollow.pos.x - this._pos.x;
        const y = this.toFollow.pos.y - this._pos.y;

        const xs = Math.abs(x) / this.followDistance * this.speed + 4;
        const ys = Math.abs(y) / this.followDistance * this.speed + 4;

        if (Math.abs(x) > 0.1)
            this._pos.x += x * elapsed * xs;
        if (Math.abs(y) > 0.1)
            this._pos.y += y * elapsed * ys;
    }


    //
    // Effects
    //

    /**
     * Initialize a new camera shake effect.
     * @param {number} strength Intensity of the effect
     * @param {number} length Duration of the effect
     */
    doShake(strength, length = 1) {
        this.shake = new CameraShake(this, strength, length);
    }

    /**
     * Add a fade effect.
     * @param {Color} from Color to transition from
     * @param {Color} to Color to transition to
     * @param {number} duration Length of fade effect
     * @param {function} after Function to run when complete
     * @private
     */
    _doFade(from, to, duration, after = () => {}) {
        const gradient = new ColorGradient({
            0: from,
            1: to
        });

        let timer = 0;

        $$.tasks.add(new FrameTask({
            during: elapsed => {
                timer += elapsed;

                let progress = timer / duration;
                if (progress > duration)
                    progress = duration;

                this.filter = gradient.get(progress);
            },
            after: () => {
                this.filter = null;
                after();
            },
            duration: duration
        }));
    }

    /**
     * Add a fade out effect.
     * @param {Color} color Color to fade out to (black by default)
     * @param {number} duration Length of fade effect
     * @param {function} after Function to run when complete
     */
    fadeOut({
        color = Color.BLACK,
        duration = 1,
        after = () => {}
    } = {}) {
        this._doFade(Color.CLEAR, color, duration, after);
    }

    /**
     * Add a fade in effect.
     * @param {Color} color Color to fade in to (black by default)
     * @param {number} duration Length of fade effect
     * @param {function} after Function to run when complete
     */
    fadeIn({
       color = Color.BLACK,
       duration = 1,
       after = () => {}
    } = {}) {
        this._doFade(color, Color.CLEAR, duration, after);
    }
}

export default Camera;