diff --git a/examples/get-started/pure-js/orthographic/README.md b/examples/get-started/pure-js/orthographic/README.md
new file mode 100644
index 00000000000..10e01a9df78
--- /dev/null
+++ b/examples/get-started/pure-js/orthographic/README.md
@@ -0,0 +1,17 @@
+## Example: Use deck.gl with OrthographicView
+
+Uses [Vite](https://vitejs.dev/) to bundle and serve files.
+
+## Usage
+
+To install dependencies:
+
+```bash
+npm install
+# or
+yarn
+```
+
+Commands:
+* `npm start` is the development target, to serve the app and hot reload.
+* `npm run build` is the production target, to create the final bundle and write to disk.
diff --git a/examples/get-started/pure-js/orthographic/app.js b/examples/get-started/pure-js/orthographic/app.js
new file mode 100644
index 00000000000..7a828da36cf
--- /dev/null
+++ b/examples/get-started/pure-js/orthographic/app.js
@@ -0,0 +1,36 @@
+/* global document */
+import {Deck, OrthographicView} from '@deck.gl/core';
+import {ScatterplotLayer} from '@deck.gl/layers';
+
+const INITIAL_VIEW_STATE = {
+ target: [0, 0, 0],
+ zoom: 1
+};
+
+const data = [];
+for (let x = -50; x <= 50; x += 5) {
+ for (let y = -50; y <= 50; y += 5) {
+ data.push({position: [x, y], color: [(x + 50) * 2.5, (y + 50) * 2.5, 128]});
+ }
+}
+
+new Deck({
+ parent: document.body,
+ views: new OrthographicView({
+ controller: {
+ dragRotate: true
+ }
+ }),
+ initialViewState: INITIAL_VIEW_STATE,
+ controller: true,
+ layers: [
+ new ScatterplotLayer({
+ id: 'scatterplot',
+ data,
+ getPosition: d => d.position,
+ getFillColor: d => d.color,
+ getRadius: 2,
+ radiusUnits: 'pixels'
+ })
+ ]
+});
diff --git a/examples/get-started/pure-js/orthographic/index.html b/examples/get-started/pure-js/orthographic/index.html
new file mode 100644
index 00000000000..93a62a3e5ba
--- /dev/null
+++ b/examples/get-started/pure-js/orthographic/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+ deck.gl OrthographicView Example
+
+
+
+
+
+
diff --git a/examples/get-started/pure-js/orthographic/package.json b/examples/get-started/pure-js/orthographic/package.json
new file mode 100644
index 00000000000..81f0f9a28d2
--- /dev/null
+++ b/examples/get-started/pure-js/orthographic/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "deck.gl-example-orthographic",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "scripts": {
+ "start": "vite --open",
+ "start-local": "vite --config ../../../vite.config.local.mjs",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@deck.gl/core": "^9.0.0",
+ "@deck.gl/layers": "^9.0.0"
+ },
+ "devDependencies": {
+ "vite": "^4.0.0"
+ }
+}
diff --git a/modules/core/src/controllers/orthographic-controller.ts b/modules/core/src/controllers/orthographic-controller.ts
index 9749a42ab66..eacc52b9c05 100644
--- a/modules/core/src/controllers/orthographic-controller.ts
+++ b/modules/core/src/controllers/orthographic-controller.ts
@@ -150,25 +150,65 @@ export class OrthographicState extends ViewState<
});
}
- /**
- * Start rotating
- */
- rotateStart(): OrthographicState {
- return this._getUpdatedState({});
+ rotateStart({pos}: {pos: [number, number]}): OrthographicState {
+ const {width, height, rotationOrbit = 0} = this.getViewportProps();
+ const centerX = width / 2;
+ const centerY = height / 2;
+ const startAngle = Math.atan2(centerY - pos[1], pos[0] - centerX);
+
+ return this._getUpdatedState({
+ startRotatePos: pos,
+ startRotationOrbit: rotationOrbit,
+ startRotationX: startAngle
+ }) as OrthographicState;
}
- /**
- * Rotate
- */
- rotate(): OrthographicState {
- return this;
+ rotate({
+ pos,
+ deltaAngleX = 0
+ }: {
+ pos?: [number, number];
+ deltaAngleX?: number;
+ deltaAngleY?: number;
+ }): OrthographicState {
+ const {startRotatePos, startRotationOrbit, startRotationX} = this.getState();
+ const {width, height, rotationOrbit = 0} = this.getViewportProps();
+
+ if (!startRotatePos || startRotationOrbit === undefined || startRotationX === undefined) {
+ return this;
+ }
+
+ let newRotationOrbit: number;
+
+ if (pos) {
+ const centerX = width / 2;
+ const centerY = height / 2;
+ const currentAngle = Math.atan2(centerY - pos[1], pos[0] - centerX);
+ let deltaAngle = currentAngle - startRotationX;
+
+ if (deltaAngle > Math.PI) {
+ deltaAngle -= 2 * Math.PI;
+ } else if (deltaAngle < -Math.PI) {
+ deltaAngle += 2 * Math.PI;
+ }
+
+ const sensitivity = 1.5;
+ newRotationOrbit = startRotationOrbit - (deltaAngle * 180 * sensitivity) / Math.PI;
+ } else {
+ newRotationOrbit = rotationOrbit + deltaAngleX;
+ }
+
+ return this._getUpdatedState({
+ rotationOrbit: newRotationOrbit
+ }) as OrthographicState;
}
- /**
- * End rotating
- */
rotateEnd(): OrthographicState {
- return this._getUpdatedState({});
+ return this._getUpdatedState({
+ startRotatePos: null,
+ startRotationOrbit: null,
+ startRotationX: null
+ }) as OrthographicState;
}
// shortest path between two view states
@@ -366,13 +406,14 @@ export class OrthographicState extends ViewState<
return props;
}
+
}
export default class OrthographicController extends Controller {
ControllerState = OrthographicState;
transition = {
transitionDuration: 300,
- transitionInterpolator: new LinearInterpolator(['target', 'zoomX', 'zoomY'])
+ transitionInterpolator: new LinearInterpolator(['target', 'zoomX', 'zoomY', 'rotationOrbit'])
};
dragMode: 'pan' | 'rotate' = 'pan';
@@ -380,9 +421,4 @@ export default class OrthographicController extends Controller {
+ t.ok(OrthographicViewport, 'OrthographicViewport import ok');
+ t.end();
+});
+
+test('OrthographicViewport#constructor - default props', t => {
+ const viewport = new OrthographicViewport({
+ width: 800,
+ height: 600
+ });
+
+ t.ok(viewport instanceof OrthographicViewport, 'Created OrthographicViewport');
+ t.deepEqual(viewport.target, [0, 0, 0], 'default target');
+ t.is(viewport.zoomX, 0, 'default zoomX');
+ t.is(viewport.zoomY, 0, 'default zoomY');
+ t.is(viewport.flipY, true, 'default flipY');
+ t.is(viewport.rotationOrbit, 0, 'default rotationOrbit');
+ t.end();
+});
+
+test('OrthographicViewport#constructor - custom props', t => {
+ const viewport = new OrthographicViewport({
+ width: 800,
+ height: 600,
+ target: [100, 200, 0],
+ zoom: 2,
+ flipY: false,
+ rotationOrbit: 45
+ });
+
+ t.ok(viewport instanceof OrthographicViewport, 'Created OrthographicViewport');
+ t.deepEqual(viewport.target, [100, 200, 0], 'custom target');
+ t.is(viewport.zoomX, 2, 'custom zoomX');
+ t.is(viewport.zoomY, 2, 'custom zoomY');
+ t.is(viewport.flipY, false, 'custom flipY');
+ t.is(viewport.rotationOrbit, 45, 'custom rotationOrbit');
+ t.end();
+});
+
+test('OrthographicViewport#rotationOrbit - viewMatrix changes with rotation', t => {
+ const viewport0 = new OrthographicViewport({
+ width: 800,
+ height: 600,
+ rotationOrbit: 0
+ });
+
+ const viewport90 = new OrthographicViewport({
+ width: 800,
+ height: 600,
+ rotationOrbit: 90
+ });
+
+ const viewport180 = new OrthographicViewport({
+ width: 800,
+ height: 600,
+ rotationOrbit: 180
+ });
+
+ t.notDeepEqual(
+ viewport0.viewMatrix,
+ viewport90.viewMatrix,
+ 'viewMatrix differs with 90 degree rotation'
+ );
+ t.notDeepEqual(
+ viewport0.viewMatrix,
+ viewport180.viewMatrix,
+ 'viewMatrix differs with 180 degree rotation'
+ );
+ t.notDeepEqual(
+ viewport90.viewMatrix,
+ viewport180.viewMatrix,
+ 'viewMatrix differs between 90 and 180 degree rotation'
+ );
+ t.end();
+});
+
+test('OrthographicViewport#rotationOrbit - project/unproject consistency', t => {
+ const viewport = new OrthographicViewport({
+ width: 800,
+ height: 600,
+ target: [0, 0, 0],
+ zoom: 0,
+ rotationOrbit: 45
+ });
+
+ const worldPos = [100, 100, 0];
+ const screenPos = viewport.project(worldPos);
+ const unprojected = viewport.unproject(screenPos);
+
+ t.ok(
+ Math.abs(unprojected[0] - worldPos[0]) < 0.001 &&
+ Math.abs(unprojected[1] - worldPos[1]) < 0.001,
+ 'project/unproject roundtrip consistent with rotation'
+ );
+ t.end();
+});
+
+test('OrthographicViewport#rotationOrbit - independent zoom axes', t => {
+ const viewport = new OrthographicViewport({
+ width: 800,
+ height: 600,
+ zoom: [1, 2],
+ rotationOrbit: 30
+ });
+
+ t.is(viewport.zoomX, 1, 'independent zoomX');
+ t.is(viewport.zoomY, 2, 'independent zoomY');
+ t.is(viewport.rotationOrbit, 30, 'rotationOrbit with independent zoom');
+ t.end();
+});