diff --git a/examples/src/examples/camera/multi.example.mjs b/examples/src/examples/camera/multi.example.mjs
new file mode 100644
index 00000000000..48a3df02dd7
--- /dev/null
+++ b/examples/src/examples/camera/multi.example.mjs
@@ -0,0 +1,113 @@
+// @config DESCRIPTION
(WASDQE) Move
(LMB) Orbit, (RMB) Fly
(Scroll Wheel) zoom
(MMB / Hold Shift) Pan
(F) Focus
+// @config HIDDEN
+import * as pc from 'playcanvas';
+import { deviceType, rootPath } from 'examples/utils';
+
+const canvas = document.getElementById('application-canvas');
+if (!(canvas instanceof HTMLCanvasElement)) {
+ throw new Error('No canvas found');
+}
+window.focus();
+
+const gfxOptions = {
+ deviceTypes: [deviceType],
+ glslangUrl: rootPath + '/static/lib/glslang/glslang.js',
+ twgslUrl: rootPath + '/static/lib/twgsl/twgsl.js'
+};
+
+const assets = {
+ helipad: new pc.Asset(
+ 'helipad-env-atlas',
+ 'texture',
+ { url: rootPath + '/static/assets/cubemaps/helipad-env-atlas.png' },
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
+ ),
+ script: new pc.Asset('script', 'script', { url: rootPath + '/static/scripts/camera/multi-camera.js' }),
+ statue: new pc.Asset('statue', 'container', { url: rootPath + '/static/assets/models/statue.glb' })
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+
+createOptions.componentSystems = [
+ pc.RenderComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem,
+ pc.ScriptComponentSystem
+];
+createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler];
+
+const app = new pc.AppBase(canvas);
+app.init(createOptions);
+
+// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
+app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
+app.setCanvasResolution(pc.RESOLUTION_AUTO);
+
+// Ensure canvas is resized when window changes size
+const resize = () => app.resizeCanvas();
+window.addEventListener('resize', resize);
+app.on('destroy', () => {
+ window.removeEventListener('resize', resize);
+});
+
+await new Promise((resolve) => {
+ new pc.AssetListLoader(Object.values(assets), app.assets).load(resolve);
+});
+
+/**
+ * @param {pc.Entity} focus - The entity to focus the camera on.
+ * @returns {pc.Entity} The multi-camera entity.
+ */
+function createMultiCamera(focus) {
+ const camera = new pc.Entity();
+ camera.addComponent('camera');
+
+ const multiCamera = new pc.Entity();
+ multiCamera.addComponent('script');
+ if (!multiCamera.script) {
+ throw new Error('Script component not found');
+ }
+
+ /** @type {any} */
+ const script = multiCamera.script.create('multiCamera', {
+ attributes: {
+ camera
+ }
+ });
+
+ // wait until after canvas resized to focus on entity
+ const resize = new ResizeObserver(() => {
+ script.focusOnEntity(focus, true);
+ resize.disconnect();
+ });
+ resize.observe(canvas ?? document.body);
+
+ return multiCamera;
+}
+
+app.start();
+
+app.scene.ambientLight.set(0.4, 0.4, 0.4);
+
+app.scene.skyboxMip = 1;
+app.scene.skyboxIntensity = 0.4;
+app.scene.envAtlas = assets.helipad.resource;
+
+// Create a directional light
+const light = new pc.Entity();
+light.addComponent('light');
+light.setLocalEulerAngles(45, 30, 0);
+app.root.addChild(light);
+
+const statue = assets.statue.resource.instantiateRenderEntity();
+statue.setLocalPosition(0, -0.5, 0);
+app.root.addChild(statue);
+
+const multiCamera = createMultiCamera(statue);
+app.root.addChild(multiCamera);
+
+export { app };
diff --git a/examples/thumbnails/camera_multi_large.webp b/examples/thumbnails/camera_multi_large.webp
new file mode 100644
index 00000000000..7ea680395b5
Binary files /dev/null and b/examples/thumbnails/camera_multi_large.webp differ
diff --git a/examples/thumbnails/camera_multi_small.webp b/examples/thumbnails/camera_multi_small.webp
new file mode 100644
index 00000000000..af47f8c3719
Binary files /dev/null and b/examples/thumbnails/camera_multi_small.webp differ
diff --git a/scripts/camera/multi-camera.js b/scripts/camera/multi-camera.js
new file mode 100644
index 00000000000..768ea1e07a7
--- /dev/null
+++ b/scripts/camera/multi-camera.js
@@ -0,0 +1,754 @@
+(() => {
+ const { createScript, BoundingBox, Vec2, Vec3, Ray, Plane, math } = pc;
+
+ const tmpVa = new Vec2();
+ const tmpV1 = new Vec3();
+ const tmpV2 = new Vec3();
+ const tmpR1 = new Ray();
+ const tmpP1 = new Plane();
+
+ const PASSIVE = { passive: false };
+
+ const LOOK_MAX_ANGLE = 90;
+
+ function calcEntityAABB(bbox, entity) {
+ bbox.center.set(0, 0, 0);
+ bbox.halfExtents.set(0, 0, 0);
+ entity.findComponents('render').forEach((render) => {
+ render.meshInstances.forEach((mi) => {
+ bbox.add(mi.aabb);
+ });
+ });
+ return bbox;
+ }
+
+ class BaseCamera {
+ /**
+ * @type {Entity}
+ */
+ entity;
+
+ /**
+ * @type {HTMLElement}
+ */
+ target = document.documentElement;
+
+ /**
+ * @type {number}
+ */
+ sceneSize = 100;
+
+ /**
+ * @type {number}
+ */
+ lookSensitivity = 0.2;
+
+ /**
+ * @type {number}
+ */
+ lookDamping = 0.97;
+
+ /**
+ * @type {number}
+ */
+ moveDamping = 0.98;
+
+ /**
+ * @type {Entity}
+ * @protected
+ */
+ _camera = null;
+
+ /**
+ * @type {Vec3}
+ * @protected
+ */
+ _origin = new Vec3(0, 1, 0);
+
+ /**
+ * @type {Vec3}
+ * @protected
+ */
+ _position = new Vec3();
+
+ /**
+ * @type {Vec2}
+ * @protected
+ */
+ _dir = new Vec2();
+
+ /**
+ * @type {Vec3}
+ * @protected
+ */
+ _angles = new Vec3();
+
+ /**
+ * @param {Entity} entity - The entity to attach the camera to.
+ * @param {HTMLElement} target - The target element to listen for pointer events.
+ * @param {Record} options - The options for the camera.
+ */
+ constructor(entity, target, options = {}) {
+ this.entity = entity;
+ this.target = target;
+ this.sceneSize = options.sceneSize ?? this.sceneSize;
+ this.lookSensitivity = options.lookSensitivity ?? this.lookSensitivity;
+ this.lookDamping = options.lookDamping ?? this.lookDamping;
+ this.moveDamping = options.moveDamping ?? this.moveDamping;
+
+ this._onPointerDown = this._onPointerDown.bind(this);
+ this._onPointerMove = this._onPointerMove.bind(this);
+ this._onPointerUp = this._onPointerUp.bind(this);
+ }
+
+ /**
+ * @param {number} dt - The delta time in seconds.
+ * @private
+ */
+ _smoothLook(dt) {
+ const lerpRate = 1 - Math.pow(this.lookDamping, dt * 1000);
+ this._angles.x = math.lerp(this._angles.x, this._dir.x, lerpRate);
+ this._angles.y = math.lerp(this._angles.y, this._dir.y, lerpRate);
+ this.entity.setEulerAngles(this._angles);
+ }
+
+ /**
+ * @param {number} dt - The delta time in seconds.
+ * @private
+ */
+ _smoothMove(dt) {
+ this._position.lerp(this._position, this._origin, 1 - Math.pow(this.moveDamping, dt * 1000));
+ this.entity.setPosition(this._position);
+ }
+
+ /**
+ * @param {MouseEvent} event - The mouse event.
+ * @private
+ */
+ _onContextMenu(event) {
+ event.preventDefault();
+ }
+
+ /**
+ * @param {PointerEvent} event - The pointer event.
+ * @protected
+ * @abstract
+ */
+ _onPointerDown(event) {}
+
+ /**
+ * @param {PointerEvent} event - The pointer move event.
+ * @protected
+ * @abstract
+ */
+ _onPointerMove(event) {}
+
+ /**
+ * @param {PointerEvent} event - The pointer event.
+ * @protected
+ * @abstract
+ */
+ _onPointerUp(event) {}
+
+ /**
+ * @param {PointerEvent} event - The pointer move event.
+ * @protected
+ */
+ _look(event) {
+ if (event.target !== this.target) {
+ return;
+ }
+ const movementX = event.movementX || 0;
+ const movementY = event.movementY || 0;
+ this._dir.x = math.clamp(this._dir.x - movementY * this.lookSensitivity, -LOOK_MAX_ANGLE, LOOK_MAX_ANGLE);
+ this._dir.y -= movementX * this.lookSensitivity;
+ }
+
+ /**
+ * @param {Entity} camera - The camera entity to attach.
+ */
+ attach(camera) {
+ this._camera = camera;
+ this._camera.setLocalEulerAngles(0, 0, 0);
+
+ window.addEventListener('pointerdown', this._onPointerDown);
+ window.addEventListener('pointermove', this._onPointerMove);
+ window.addEventListener('pointerup', this._onPointerUp);
+ window.addEventListener('contextmenu', this._onContextMenu);
+
+ this.entity.addChild(camera);
+ }
+
+ detach() {
+ window.removeEventListener('pointermove', this._onPointerMove);
+ window.removeEventListener('pointerdown', this._onPointerDown);
+ window.removeEventListener('pointerup', this._onPointerUp);
+ window.removeEventListener('contextmenu', this._onContextMenu);
+
+ this.entity.removeChild(this._camera);
+ this._camera = null;
+
+ this._dir.x = this._angles.x;
+ this._dir.y = this._angles.y;
+
+ this._origin.copy(this._position);
+ }
+
+ /**
+ * @param {number} dt - The delta time in seconds.
+ */
+ update(dt) {
+ if (!this._camera) {
+ return;
+ }
+
+ this._smoothLook(dt);
+ this._smoothMove(dt);
+ }
+ }
+
+ class MultiCamera extends BaseCamera {
+ /**
+ * @type {number}
+ */
+ focusFov = 75;
+
+ /**
+ * @type {number}
+ */
+ lookSensitivity = 0.2;
+
+ /**
+ * @type {number}
+ */
+ lookDamping = 0.97;
+
+ /**
+ * @type {number}
+ */
+ moveDamping = 0.98;
+
+ /**
+ * @type {number}
+ */
+ pinchSpeed = 5;
+
+ /**
+ * @type {number}
+ */
+ wheelSpeed = 0.005;
+
+ /**
+ * @type {number}
+ */
+ zoomMin = 0.001;
+
+ /**
+ * @type {number}
+ */
+ zoomMax = 10;
+
+ /**
+ * @type {number}
+ */
+ zoomScaleMin = 0.01;
+
+ /**
+ * @type {number}
+ */
+ moveSpeed = 2;
+
+ /**
+ * @type {number}
+ */
+ sprintSpeed = 4;
+
+ /**
+ * @type {number}
+ */
+ crouchSpeed = 1;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _zoom = 0;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _cameraDist = 0;
+
+ /**
+ * @type {Map}
+ * @private
+ */
+ _pointerEvents = new Map();
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _lastPinchDist = -1;
+
+ /**
+ * @type {Vec2}
+ * @private
+ */
+ _lastPosition = new Vec2();
+
+ /**
+ * @type {boolean}
+ */
+ _panning = false;
+
+ /**
+ * @type {boolean}
+ */
+ _flying = false;
+
+ /**
+ * @type {Record}
+ * @private
+ */
+ _key = {
+ forward: false,
+ backward: false,
+ left: false,
+ right: false,
+ up: false,
+ down: false,
+ sprint: false,
+ crouch: false
+ };
+
+ /**
+ * @param {Entity} entity - The entity to attach the camera to.
+ * @param {HTMLElement} target - The target element to listen for pointer events.
+ * @param {Record} options - The options for the camera.
+ */
+ constructor(entity, target, options = {}) {
+ super(entity, target, options);
+
+ this.pinchSpeed = options.pinchSpeed ?? this.pinchSpeed;
+ this.wheelSpeed = options.wheelSpeed ?? this.wheelSpeed;
+ this.zoomMin = options.zoomMin ?? this.zoomMin;
+ this.zoomMax = options.zoomMax ?? this.zoomMax;
+ this.moveSpeed = options.moveSpeed ?? this.moveSpeed;
+ this.sprintSpeed = options.sprintSpeed ?? this.sprintSpeed;
+ this.crouchSpeed = options.crouchSpeed ?? this.crouchSpeed;
+
+ this._onWheel = this._onWheel.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ this._onKeyUp = this._onKeyUp.bind(this);
+ }
+
+ /**
+ * @param {PointerEvent} event - The pointer event.
+ * @protected
+ */
+ _onPointerDown(event) {
+ if (!this._camera) {
+ return;
+ }
+ this._pointerEvents.set(event.pointerId, event);
+ if (this._pointerEvents.size === 2) {
+ this._lastPinchDist = this._getPinchDist();
+ this._getMidPoint(this._lastPosition);
+ this._panning = true;
+ }
+ if (event.shiftKey || event.button === 1) {
+ this._lastPosition.set(event.clientX, event.clientY);
+ this._panning = true;
+ }
+ if (event.button === 2) {
+ this._zoom = this._cameraDist;
+ this._origin.copy(this._camera.getPosition());
+ this._position.copy(this._origin);
+ this._camera.setLocalPosition(0, 0, 0);
+ this._flying = true;
+ }
+ }
+
+ /**
+ * @param {PointerEvent} event - The pointer event.
+ * @protected
+ */
+ _onPointerMove(event) {
+ if (this._pointerEvents.size === 0) {
+ return;
+ }
+
+ this._pointerEvents.set(event.pointerId, event);
+
+ if (this._pointerEvents.size === 1) {
+ if (this._panning) {
+ // mouse pan
+ this._handlePan(tmpVa.set(event.clientX, event.clientY));
+ } else {
+ super._look(event);
+ }
+ return;
+ }
+
+ if (this._pointerEvents.size === 2) {
+ // touch pan
+ this._handlePan(this._getMidPoint(tmpVa));
+
+ // pinch zoom
+ const pinchDist = this._getPinchDist();
+ if (this._lastPinchDist > 0) {
+ this._handleZoom((this._lastPinchDist - pinchDist) * this.pinchSpeed);
+ }
+ this._lastPinchDist = pinchDist;
+ }
+ }
+
+ /**
+ * @param {PointerEvent} event - The pointer event.
+ * @protected
+ */
+ _onPointerUp(event) {
+ this._pointerEvents.delete(event.pointerId);
+ if (this._pointerEvents.size < 2) {
+ this._lastPinchDist = -1;
+ this._panning = false;
+ }
+ if (this._panning) {
+ this._panning = false;
+ }
+ if (this._flying) {
+ tmpV1.copy(this.entity.forward).mulScalar(this._zoom);
+ this._origin.add(tmpV1);
+ this._position.add(tmpV1);
+ this._flying = false;
+ }
+ }
+
+ /**
+ * @param {WheelEvent} event - The wheel event.
+ * @private
+ */
+ _onWheel(event) {
+ event.preventDefault();
+ this._handleZoom(event.deltaY);
+ }
+
+ /**
+ * @param {KeyboardEvent} event - The keyboard event.
+ * @private
+ */
+ _onKeyDown(event) {
+ event.stopPropagation();
+ switch (event.key.toLowerCase()) {
+ case 'w':
+ this._key.forward = true;
+ break;
+ case 's':
+ this._key.backward = true;
+ break;
+ case 'a':
+ this._key.left = true;
+ break;
+ case 'd':
+ this._key.right = true;
+ break;
+ case 'q':
+ this._key.up = true;
+ break;
+ case 'e':
+ this._key.down = true;
+ break;
+ case 'shift':
+ this._key.sprint = true;
+ break;
+ case 'control':
+ this._key.crouch = true;
+ break;
+ }
+ }
+
+ /**
+ * @param {KeyboardEvent} event - The keyboard event.
+ * @private
+ */
+ _onKeyUp(event) {
+ event.stopPropagation();
+ switch (event.key.toLowerCase()) {
+ case 'w':
+ this._key.forward = false;
+ break;
+ case 's':
+ this._key.backward = false;
+ break;
+ case 'a':
+ this._key.left = false;
+ break;
+ case 'd':
+ this._key.right = false;
+ break;
+ case 'q':
+ this._key.up = false;
+ break;
+ case 'e':
+ this._key.down = false;
+ break;
+ case 'shift':
+ this._key.sprint = false;
+ break;
+ case 'control':
+ this._key.crouch = false;
+ break;
+ }
+ }
+
+ /**
+ * @param {number} dt - The time delta.
+ * @private
+ */
+ _handleMove(dt) {
+ tmpV1.set(0, 0, 0);
+ if (this._key.forward) {
+ tmpV1.add(this.entity.forward);
+ }
+ if (this._key.backward) {
+ tmpV1.sub(this.entity.forward);
+ }
+ if (this._key.left) {
+ tmpV1.sub(this.entity.right);
+ }
+ if (this._key.right) {
+ tmpV1.add(this.entity.right);
+ }
+ if (this._key.up) {
+ tmpV1.add(this.entity.up);
+ }
+ if (this._key.down) {
+ tmpV1.sub(this.entity.up);
+ }
+ tmpV1.normalize();
+ const speed = this._key.crouch ? this.crouchSpeed : this._key.sprint ? this.sprintSpeed : this.moveSpeed;
+ tmpV1.mulScalar(this.sceneSize * speed * dt);
+ this._origin.add(tmpV1);
+ }
+
+ /**
+ * @param {Vec2} out - The output vector.
+ * @returns {Vec2} The mid point.
+ * @private
+ */
+ _getMidPoint(out) {
+ const [a, b] = this._pointerEvents.values();
+ const dx = a.clientX - b.clientX;
+ const dy = a.clientY - b.clientY;
+ return out.set(b.clientX + dx * 0.5, b.clientY + dy * 0.5);
+ }
+
+ /**
+ * @returns {number} The pinch distance.
+ * @private
+ */
+ _getPinchDist() {
+ const [a, b] = this._pointerEvents.values();
+ const dx = a.clientX - b.clientX;
+ const dy = a.clientY - b.clientY;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ /**
+ * @param {Vec2} pos - The position.
+ * @param {Vec3} point - The output point.
+ * @private
+ */
+ _screenToWorldPan(pos, point) {
+ const mouseW = this._camera.camera.screenToWorld(pos.x, pos.y, 1);
+ const cameraPos = this._camera.getPosition();
+
+ const focusDirScaled = tmpV1.copy(this.entity.forward).mulScalar(this._zoom);
+ const focalPos = tmpV2.add2(cameraPos, focusDirScaled);
+ const planeNormal = focusDirScaled.mulScalar(-1).normalize();
+
+ const plane = tmpP1.setFromPointNormal(focalPos, planeNormal);
+ const ray = tmpR1.set(cameraPos, mouseW.sub(cameraPos).normalize());
+
+ plane.intersectsRay(ray, point);
+ }
+
+ /**
+ * @param {Vec2} pos - The position.
+ * @private
+ */
+ _handlePan(pos) {
+ const start = new Vec3();
+ const end = new Vec3();
+
+ this._screenToWorldPan(this._lastPosition, start);
+ this._screenToWorldPan(pos, end);
+
+ tmpV1.sub2(start, end);
+ this._origin.add(tmpV1);
+
+ this._lastPosition.copy(pos);
+ }
+
+ /**
+ * @param {number} delta - The delta.
+ * @private
+ */
+ _handleZoom(delta) {
+ const min = this._camera.camera.nearClip + this.zoomMin * this.sceneSize;
+ const max = this.zoomMax * this.sceneSize;
+ const scale = math.clamp(this._zoom / (max - min), this.zoomScaleMin, 1);
+ this._zoom += delta * this.wheelSpeed * this.sceneSize * scale;
+ this._zoom = math.clamp(this._zoom, min, max);
+ }
+
+ /**
+ * @returns {number} The zoom.
+ * @private
+ */
+ _calcZoom() {
+ const camera = this._camera.camera;
+ const d1 = Math.tan(0.5 * this.focusFov * math.DEG_TO_RAD);
+ const d2 = Math.tan(0.5 * camera.fov * math.DEG_TO_RAD);
+
+ const scale = (d1 / d2) * (1 / camera.aspectRatio);
+ return scale * this.sceneSize + this.sceneSize;
+ }
+
+ /**
+ * @param {Vec3} point - The point to focus on.
+ * @param {Vec3} [start] - The start point.
+ * @param {boolean} [snap] - Whether to snap the focus.
+ */
+ focus(point, start, snap = false) {
+ if (!this._camera) {
+ return;
+ }
+
+ this._origin.copy(point);
+ if (snap) {
+ this._position.copy(point);
+ }
+ this._camera.setPosition(start);
+ this._camera.setLocalEulerAngles(0, 0, 0);
+
+ if (!start) {
+ return;
+ }
+
+ tmpV1.sub2(start, point);
+ const elev = Math.atan2(tmpV1.y, tmpV1.z) * math.RAD_TO_DEG;
+ const azim = Math.atan2(tmpV1.x, tmpV1.z) * math.RAD_TO_DEG;
+ this._dir.set(-elev, -azim);
+ if (snap) {
+ this._angles.copy(this._dir);
+ }
+
+ this._zoom = tmpV1.length();
+ }
+
+ /**
+ * @param {Entity} entity - The entity to focus on.
+ * @param {boolean} [snap] - Whether to snap the focus.
+ */
+ focusOnEntity(entity, snap = false) {
+ const bbox = calcEntityAABB(new BoundingBox(), entity);
+ this.sceneSize = bbox.halfExtents.length();
+ this.focus(bbox.center, undefined, snap);
+ this._zoom = this._calcZoom();
+ if (snap) {
+ this._cameraDist = this._zoom;
+ }
+ }
+
+ /**
+ * @param {Entity} camera - The camera entity to attach.
+ */
+ attach(camera) {
+ super.attach(camera);
+ this._camera.setPosition(0, 0, 0);
+ this._camera.setLocalEulerAngles(0, 0, 0);
+
+ window.addEventListener('wheel', this._onWheel, PASSIVE);
+ window.addEventListener('keydown', this._onKeyDown, false);
+ window.addEventListener('keyup', this._onKeyUp, false);
+ }
+
+ detach() {
+ super.detach();
+
+ window.removeEventListener('wheel', this._onWheel, PASSIVE);
+ window.removeEventListener('keydown', this._onKeyDown, false);
+ window.removeEventListener('keyup', this._onKeyUp, false);
+
+ this._pointerEvents.clear();
+ this._lastPinchDist = -1;
+ this._panning = false;
+ this._key = {
+ forward: false,
+ backward: false,
+ left: false,
+ right: false,
+ up: false,
+ down: false,
+ sprint: false,
+ crouch: false
+ };
+ }
+
+ /**
+ * @param {number} dt - The delta time in seconds.
+ */
+ update(dt) {
+ if (!this._camera) {
+ return;
+ }
+
+ if (!this._flying) {
+ this._cameraDist = math.lerp(this._cameraDist, this._zoom, 1 - Math.pow(this.moveDamping, dt * 1000));
+ this._camera.setLocalPosition(0, 0, this._cameraDist);
+ }
+
+ this._handleMove(dt);
+
+ super.update(dt);
+ }
+ }
+
+ const MultiCameraScript = createScript('multiCamera');
+
+ MultiCameraScript.attributes.add('camera', { type: 'entity' });
+
+ MultiCameraScript.prototype.initialize = function () {
+ this.multiCamera = new MultiCamera(this.entity, this.app.graphicsDevice.canvas, {
+ name: 'multi-camera'
+ });
+ this.multiCamera.attach(this.camera);
+
+ this._onKeyDown = this._onKeyDown.bind(this);
+ window.addEventListener('keydown', this._onKeyDown, false);
+
+ this.on('destroy', () => {
+ this.multiCamera.detach();
+ window.removeEventListener('keydown', this._onKeyDown, false);
+ });
+
+ };
+
+ MultiCameraScript.prototype._onKeyDown = function (event) {
+ if (event.key === 'f' && this.focus) {
+ this.focusOnEntity(this.focus);
+ }
+ };
+
+ MultiCameraScript.prototype.focusOnEntity = function (entity, snap = false) {
+ this.focus = entity;
+ this.multiCamera.focusOnEntity(this.focus, snap);
+ };
+
+ MultiCameraScript.prototype.update = function (dt) {
+ this.multiCamera.update(dt);
+ };
+})();