From c4c72e45d2b719bc86520b56249ef4eedaa9568c Mon Sep 17 00:00:00 2001 From: singlecoder Date: Tue, 8 Jun 2021 20:32:25 +0800 Subject: [PATCH] feat: add ortho control for camera (#278) --- packages/controls/src/OrthoControl.ts | 127 +++++++++++++++++++ packages/controls/src/index.ts | 1 + packages/controls/tests/OrthoControl.test.ts | 39 ++++++ 3 files changed, 167 insertions(+) create mode 100644 packages/controls/src/OrthoControl.ts create mode 100644 packages/controls/tests/OrthoControl.test.ts diff --git a/packages/controls/src/OrthoControl.ts b/packages/controls/src/OrthoControl.ts new file mode 100644 index 0000000000..cdc8e0ed70 --- /dev/null +++ b/packages/controls/src/OrthoControl.ts @@ -0,0 +1,127 @@ +import { Camera, Entity, Logger, Script, Vector2, Vector3 } from "oasis-engine"; + +/** + * The camera's 2D controller, can zoom and pan. + */ +export class OrthoControl extends Script { + cameraEntity: Entity; + camera: Camera; + + private _zoomSpeed: number = 1.0; + private _zoomScale: number = 1.0; + private _zoomScaleUnit: number = 25.0; + private _zoomMinSize: number = 0.0; + private _zoomMaxSize: number = Infinity; + private _isPanStart: boolean = false; + private _panStartPos: Vector3 = new Vector3(); + private _panStart: Vector2 = new Vector2(); + private _panEnd: Vector2 = new Vector2(); + private _panDelta: Vector2 = new Vector2(); + + /** + * The zoom speed. + */ + get zoomSpeed(): number { + return this._zoomSpeed; + } + + set zoomSpeed(value: number) { + this._zoomSpeed = value; + } + + constructor(entity: Entity) { + super(entity); + + this.cameraEntity = entity; + this.camera = entity.getComponent(Camera); + } + + onUpdate(dt: number): void { + if (this._zoomScale !== 1) { + const { camera } = this; + const sizeDiff = this._zoomScaleUnit * (this._zoomScale - 1); + const size = camera.orthographicSize + sizeDiff; + camera.orthographicSize = Math.max(this._zoomMinSize, Math.min(this._zoomMaxSize, size)); + this._zoomScale = 1; + } + + if (this._isPanStart) { + const { _panStart: panStart, _panEnd: panEnd } = this; + const panDelta = this._panDelta; + Vector2.subtract(panEnd, panStart, panDelta); + if (panDelta.x === 0 && panDelta.y === 0) { + return ; + } + this._handlePan(); + panEnd.cloneTo(panStart); + } + } + + /** + * Zoom in. + */ + zoomIn(): void { + this._zoomScale *= this._getZoomScale(); + } + + /** + * Zoom out. + */ + zoomOut(): void { + this._zoomScale /= this._getZoomScale(); + } + + /** + * Start pan. + * @param x - The x-axis coordinate (unit: pixel) + * @param y - The y-axis coordinate (unit: pixel) + */ + panStart(x: number, y: number): void { + if (!this.enabled) return; + + this.cameraEntity.transform.position.cloneTo(this._panStartPos); + this._panStart.setValue(x, y); + this._panEnd.setValue(x, y); + this._isPanStart = true; + } + + /** + * Panning. + * @param x - The x-axis coordinate (unit: pixel) + * @param y - The y-axis coordinate (unit: pixel) + * + * @remarks Make sure to call panStart before calling panMove. + */ + panMove(x: number, y: number): void { + if (!this.enabled) return; + if (!this._isPanStart) { + Logger.warn("Make sure to call panStart before calling panMove"); + } + this._panEnd.setValue(x, y); + } + + /** + * End pan. + */ + panEnd(): void { + if (!this.enabled) return; + this._isPanStart = false; + } + + private _getZoomScale(): number { + return Math.pow(0.95, this.zoomSpeed); + } + + private _handlePan(): void { + const { width, height } = this.engine.canvas; + const { x, y } = this._panDelta; + const { camera } = this; + const doubleOrthographicSize = camera.orthographicSize * 4; + const width3D = doubleOrthographicSize * camera.aspectRatio; + const height3D = doubleOrthographicSize; + const pos = this._panStartPos; + pos.x -= (x * width3D) / width; + pos.y += (y * height3D) / height; + this.cameraEntity.transform.position = pos; + } +} diff --git a/packages/controls/src/index.ts b/packages/controls/src/index.ts index fb51fff610..16f610636c 100644 --- a/packages/controls/src/index.ts +++ b/packages/controls/src/index.ts @@ -1,2 +1,3 @@ export { FreeControl } from "./FreeControl"; export { OrbitControl } from "./OrbitControl"; +export { OrthoControl } from "./OrthoControl"; diff --git a/packages/controls/tests/OrthoControl.test.ts b/packages/controls/tests/OrthoControl.test.ts new file mode 100644 index 0000000000..eed0f2486b --- /dev/null +++ b/packages/controls/tests/OrthoControl.test.ts @@ -0,0 +1,39 @@ +import { Camera, Entity, WebGLEngine } from "oasis-engine"; +import { OrthoControl } from "../src/OrthoControl"; + +const canvasDOM = document.createElement("canvas"); +canvasDOM.width = 1024; +canvasDOM.height = 1024; + +describe(" Ortho Control", () => { + let entity: Entity; + let camera: Camera; + let cameraControl: OrthoControl; + + beforeAll(() => { + const engine = new WebGLEngine(canvasDOM); + entity = engine.sceneManager.activeScene.createRootEntity(); + camera = entity.addComponent(Camera); + cameraControl = entity.addComponent(OrthoControl); + }); + + it("test zoom", () => { + cameraControl.zoomIn(); + cameraControl.onUpdate(0); + expect(camera.orthographicSize).toEqual(8.749999999999998); + cameraControl.zoomOut(); + cameraControl.onUpdate(0); + expect(camera.orthographicSize).toEqual(10.065789473684207); + }); + + it("test pan", () => { + cameraControl.panStart(0, 0); + cameraControl.panMove(2, 0); + cameraControl.onUpdate(0); + cameraControl.panEnd(); + + const pos = entity.transform.position; + expect(pos.x).toEqual(-0.07863898026315787); + expect(pos.y).toEqual(0); + }); +});