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;