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(); +});