diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2732d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +gltumble.js +gltumble.min.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21c0720 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Philip Rideout + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2f9b46 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +

gltumble

+ +[![badge]](https://travis-ci.org/prideout/gltumble) + +This library provides a math class called `Trackball` that allows users to spin (or "tumble") an +object by dragging with a mouse, trackpad, or touch screen. It optionally applies inertia such that +the object continues to spin if you flick it. + +The trackball does not do anything with quaternions. It simply applies Y rotation followed by X +rotation. It avoids rotation about the Z axis, and here's why: + +**When I'm at a museum, I often walk around an *objet d'art*, or stand on my tiptoes to see its top, +or crouch to see its underside. However, I very rarely tilt my head to the side, since that doesn't +reveal anything new. The only times I tilt my head are when I'm expressing confusion, or trying to +read vertical text.** + +Personally, I like this constraint. If you find it to be too limiting, try another package such +as [trackball-controller]. + +- [Interactive Demo] using [Filament] and WebGL 2.0 +- The demo [source code] is a single JS file. + +Note that `gltumble` emulates the behavior used by [sketchfab.com]. + +## Example + +```js +const trackball = new Trackball({foo: bar}); +const mat = trackball.getMatrix(); +trackball.execute(); +console.info(`The first component has ${nverts} vertices.`); +``` + +## Install + +Install with NPM (`npm install gltumble`) or Yarn (`yarn add gltumble`), then: + +```js +import Trackball from 'gltumble'; +``` + +Or use one of the following two CDN builds. + +```html + + +``` + +## API Reference + +#### new Trackball(options) + +Constructs a trackball, given an optional configuration dictionary. + +#### trackball.touch(...) + +TBD. + +#### trackball.getTransform(...) + +TBD. + +[badge]: https://travis-ci.org/prideout/.svg?branch=master "Build Status" +[glMatrix]: http://glmatrix.net +[Interactive Demo]: https://prideout.net/gltumble +[Filament]: https://github.com/google/filament +[source code]: https://github.com/prideout/knotess/blob/master/docs/demo.js +[trackball-controller]: https://github.com/wwwtyro/trackball-controller +[sketchfab.com]: https://sketchfab.com/models/bde956d410d4483da4126f1b0c80a06b diff --git a/docs/Turntable.js b/docs/Turntable.js new file mode 100644 index 0000000..9d055ae --- /dev/null +++ b/docs/Turntable.js @@ -0,0 +1,180 @@ +var GIZA = GIZA || {}; + +// Restricts rotation to Spin (Y axis) and Tilt (X axis) +GIZA.Turntable = function(config) { + + var M3 = GIZA.Matrix3; + var V3 = GIZA.Vector3; + var V2 = GIZA.Vector2; + + // Allow clients to skip the "new" + if (!(this instanceof GIZA.Turntable)) { + return new GIZA.Turntable(config); + } + + var defaults = { + startSpin: 0.001, // radians per second + allowTilt: true, + allowSpin: true, + spinFriction: 0.125, // 0 means no friction (infinite spin) while 1 means no inertia + epsilon: 3, // distance (in pixels) to wait before deciding if a drag is a Tilt or a Spin + radiansPerPixel: V2.make(0.01, 0.01), + canvas: GIZA.canvas, + trackpad: true, // if true, compensate for the delay on trackpads that occur between touchup and mouseup + lockAxes: false, // if true, don't allow simultaneous spin + tilt + homeTilt: 0.25, + //bounceTilt: false, // if true, returns the tilt to the "home" angle after a mouse release + //boundSpin: false, // if true, returns to the startSpin state after a mouse release + }; + + this.config = config = GIZA.merge(defaults, config || {}); + + // diagram please! + var state = { + Resting: 0, + SpinningStart: 1, + SpinningInertia: 2, + DraggingInit: 3, + DraggingSpin: 4, + DraggingTilt: 5, + ReturningHome: 6, + }; + + var startPosition = V2.make(0, 0); + var currentPosition = V2.make(0, 0); + + // TODO make these into a "positionHistory" + var previousPosition = currentPosition.slice(0); + var previous2Position = currentPosition.slice(0); + + var currentSpin = 0; + var currentTilt = config.homeTilt; + var currentState = config.startSpin ? + state.SpinningStart : state.Resting; + var previousTime = null; + var inertiaSpeed = 0; + var initialInertia = 0.125; + var turntable = this; + + GIZA.mousedown(function(position, modifiers) { + turntable.startDrag(position); + }); + + GIZA.mouseup(function(position, modifiers) { + turntable.endDrag(position); + }); + + GIZA.mousemove(function(position, modifiers) { + if (modifiers.button) { + turntable.updateDrag(position); + } + }); + + GIZA.drawHooks.push(function(time) { + if (previousTime == null) { + previousTime = time; + } + var deltaTime = time - previousTime; + previousTime = time; + + var isSpinning = currentState == state.DraggingSpin || + (currentState == state.DraggingInit && !config.lockAxes); + + if (currentState == state.SpinningStart) { + currentSpin += config.startSpin * deltaTime; + } else if (currentState == state.SpinningInertia) { + currentSpin += inertiaSpeed * deltaTime; + inertiaSpeed *= (1 - config.spinFriction); + if (Math.abs(inertiaSpeed) < 0.0001) { + currentState = state.Resting; + } + + // Some trackpads have an intentional delay between fingers-up + // and the time we receive the mouseup event. To accomodate this, + // we execute inertia even while we think the mouse is still down. + // This behavior can be disabled with the "trackpad" config option. + } else if (config.trackpad && isSpinning && + V2.equivalent(currentPosition, previous2Position, 0)) { + currentSpin += inertiaSpeed * deltaTime; + inertiaSpeed *= (1 - config.spinFriction); + } + + previous2Position = previousPosition.slice(0); + previousPosition = currentPosition.slice(0); + }); + + this.startDrag = function(position) { + startPosition = position.slice(0); + currentPosition = position.slice(0); + currentState = state.DraggingInit; + }; + + this.updateDrag = function(position) { + var delta = V2.subtract(position, startPosition); + + // If we haven't decided yet, decide if we're spinning or tilting. + if (currentState == state.DraggingInit && config.lockAxes) { + if (Math.abs(delta[0]) > config.epsilon && config.allowSpin) { + currentState = state.DraggingSpin; + } else if (Math.abs(delta[1]) > config.epsilon && config.allowTilt) { + currentState = state.DraggingTilt; + } else { + return; + } + } + + var previousSpin = this.getAngles()[0]; + currentPosition = position.slice(0); + + // This is purely for trackpads: + var spinDelta = this.getAngles()[0] - previousSpin; + inertiaSpeed = initialInertia * spinDelta; + }; + + this.getAngles = function() { + var delta = V2.subtract(currentPosition, startPosition); + var spin = currentSpin; + var tilt = currentTilt; + if (currentState == state.DraggingSpin) { + var radians = config.radiansPerPixel[0] * delta[0]; + spin += radians; + } else if (currentState == state.DraggingTilt) { + var radians = config.radiansPerPixel[1] * delta[1]; + tilt += radians; + } else if (!config.lockAxes && currentState == state.DraggingInit) { + spin += config.radiansPerPixel[0] * delta[0]; + tilt += config.radiansPerPixel[1] * delta[1]; + } + return [spin, tilt]; + }; + + this.getRotation = function() { + var r = this.getAngles(); + var spin = M3.rotationY(r[0]); + var tilt = M3.rotationX(r[1]); + return M3.multiply(tilt, spin); + }; + + // When releasing the mouse, capture the current rotation and change + // the state machine back to 'Resting' or 'SpinningInertia'. + this.endDrag = function(position) { + var previousSpin = this.getAngles()[0]; + currentPosition = position.slice(0); + var r = this.getAngles(); + currentSpin = r[0]; + currentTilt = r[1]; + + var spinDelta = currentSpin - previousSpin; + + if (config.spinFriction == 1) { + currentState = state.Resting; + } else { + currentState = state.SpinningInertia; + inertiaSpeed = initialInertia * spinDelta; + } + + }; + +}; + +GIZA.Turntable.INFINITE = 1000; // not sure if I'll use this \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..d40f691 --- /dev/null +++ b/index.js @@ -0,0 +1,19 @@ +import { vec3, quat } from 'gl-matrix'; + +const TWOPI = 2 * Math.PI; + +export default class Trackball { + constructor() { + this.defaults = Object.freeze({ + startSpin: 0.001, // radians per second + allowTilt: true, + allowSpin: true, + spinFriction: 0.125, // 0 means no friction (infinite spin) while 1 means no inertia + epsilon: 3, // distance (in pixels) to wait before deciding if a drag is a Tilt or a Spin + radiansPerPixel: V2.make(0.01, 0.01), + trackpad: true, // if true, compensate for the delay on trackpads that occur between touchup and mouseup + lockAxes: false, // if true, don't allow simultaneous spin + tilt + homeTilt: 0.25, + }); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5646414 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "gltumble", + "version": "0.0.0", + "description": "Spin an object by dragging with a mouse, trackpad, or touch screen", + "main": "gltumble.js", + "module": "index.js", + "jsdelivr": "gltumble.min.js", + "unpkg": "gltumble.min.js", + "dependencies": { + "gl-matrix": "^2.8.1" + }, + "devDependencies": { + "c8": "^3.2.0", + "eslint": "^5.6.0", + "eslint-config-mourner": "^3.0.0", + "esm": "^3.0.84", + "rollup": "^0.66.2", + "rollup-plugin-buble": "^0.19.2", + "rollup-plugin-node-resolve": "^3.4.0", + "rollup-plugin-terser": "^3.0.0", + "tape": "^4.9.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/prideout/gltumble.git" + }, + "scripts": { + "disable__lint": "eslint index.js test.js rollup.config.js", + "disable__pretest": "npm run lint", + "test": "node -r esm test.js", + "disable__cov": "c8 node -r esm test.js && c8 report -r html", + "build": "rollup -c", + "clean": "rm gltumble.*", + "watch": "rollup -cw", + "disable__prepublishOnly": "npm test && npm run build" + }, + "files": [ + "gltumble.js", + "gltumble.min.js" + ], + "eslintConfig": { + "extends": "mourner", + "rules": { + "no-sequences": 0 + } + }, + "author": "Philip Rideout", + "license": "MIT" +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..9984acb --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,20 @@ +import {terser} from 'rollup-plugin-terser'; +import buble from 'rollup-plugin-buble'; +import resolve from 'rollup-plugin-node-resolve'; + +const config = (file, plugins) => ({ + input: 'index.js', + output: { + name: 'gltumble', + format: 'umd', + file + }, + plugins +}); + +const bubleConfig = {transforms: {dangerousForOf: true}}; + +export default [ + config('gltumble.js', [resolve(), buble(bubleConfig)]), + config('gltumble.min.js', [resolve(), terser(), buble(bubleConfig)]) +]; diff --git a/test.js b/test.js new file mode 100644 index 0000000..51916ea --- /dev/null +++ b/test.js @@ -0,0 +1,8 @@ +import test from 'tape'; +import Trackball from './index.js'; + +test('smoke', (t) => { + const trackball = new Trackball(data); + t.notSame(trackball, null); + t.end(); +});