diff --git a/README.md b/README.md index 53b16e6..522a601 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Emitted with the following info: Emitted with the following info: - `offset: {x, y, z}` - The offset from entity center to drag position. +- `velocity: {x, y, z}` - The smoothed velocity of the entity at dragend time. - `depth` - the perpendicular distance from the screen to align the entity while dragging - `clientX` - the final mouse event's `clientX` value diff --git a/examples/index.html b/examples/index.html index becc5f4..6076b54 100644 --- a/examples/index.html +++ b/examples/index.html @@ -12,22 +12,33 @@ h1 { font-weight: 300; } - a { - color: #FAFAFA; + a.demo-link { display: block; padding: 15px 0; } + a { + color: #FAFAFA; + }

A-Frame Click & Drag Component

- Basic Demo + Basic Demo

Click + Drag entities on the screen. Note the plane cannot be dragged (it does not have the "click-drag" attribute).

Try the WASD keys to move around while dragging an entity!

+
- Events Demo + + Events Demo

Events are fired for beginning to drag, ending a drag, and for each drag event in etween.

-

This example shows how those events can be used to "ghost" a dragged entity

+

This example shows how those events can be used to "ghost" a dragged entity.

+ +
+ + Physics Demo +

Calculating the velocity at the time of drag end.

+

Combined with a physics library (for example; aframe-extras physics), we get some very nice interactions.

+

Try gently tossing the ball around / throwing it at the ground.

diff --git a/examples/main.js b/examples/main.js index 9e5ebc8..cedfc9c 100644 --- a/examples/main.js +++ b/examples/main.js @@ -1,3 +1,7 @@ import aframe from 'aframe'; +import extras from 'aframe-extras'; import clickDragComponent from '../src/index'; + +extras.physics.registerAll(aframe); clickDragComponent(aframe); + diff --git a/examples/physics/index.html b/examples/physics/index.html new file mode 100644 index 0000000..a5e4a27 --- /dev/null +++ b/examples/physics/index.html @@ -0,0 +1,42 @@ + + + A-Frame Click & Drag Component - Events + + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index 269ecad..d229e16 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "main": "lib/index.js", "browser": "dist/aframe-click-drag-component.min.js", "scripts": { - "build-example": "browserify examples/main.js --debug --verbose -t babelify -t [envify --NODE_ENV development ] -o examples/build.js", + "build-example": "browserify examples/main.js --debug --verbose -g uglifyify -t [ rollupify --config rollup.config.js ] -t babelify -t [envify --NODE_ENV development ] -o examples/build.js", "build-lib": "mkdir -p lib && babel src/index.js -o lib/build.js", - "dist": "browserify src/index.js --verbose --debug --standalone registerAframeClickDragComponent -g uglifyify -t rollupify -t babelify -t [envify --NODE_ENV production ] | exorcist dist/out.map > dist/out.js && uglifyjs dist/out.js --screw-ie8 -c -m --in-source-map dist/out.map --source-map dist/aframe-click-drag-component.min.js.map --source-map-url aframe-click-drag-component.min.js.map > dist/aframe-click-drag-component.min.js && rm dist/out*", + "dist": "browserify src/index.js --verbose --debug --standalone registerAframeClickDragComponent -g uglifyify -t [ rollupify --config rollup.config.js ] -t babelify -t [envify --NODE_ENV production ] | exorcist dist/out.map > dist/out.js && uglifyjs dist/out.js --screw-ie8 -c -m --in-source-map dist/out.map --source-map dist/aframe-click-drag-component.min.js.map --source-map-url aframe-click-drag-component.min.js.map > dist/aframe-click-drag-component.min.js && rm dist/out*", "test": "npm run test:lint", - "test:lint": "eslint .", + "test:lint": "eslint ./src", "start": "budo examples/main.js:../build.js --serve build.js --dir examples --port 8000 --live --open -- --debug --verbose -t babelify -t [envify --NODE_ENV development ]", "prepublish": "in-publish && npm run dist && npm run build-lib || not-in-publish", "preghpages": "npm run build-example && rm -rf gh-pages && mkdir gh-pages && cp -r examples/* gh-pages", @@ -38,6 +38,7 @@ }, "devDependencies": { "aframe": "^0.3.0", + "aframe-extras": "^2.5.3", "babel-cli": "^6.14.0", "babel-plugin-transform-object-rest-spread": "^6.8.0", "babel-preset-es2015": "^6.9.0", @@ -55,12 +56,15 @@ "exorcist": "^0.4.0", "ghpages": "^0.0.8", "in-publish": "^2.0.0", + "rollup-plugin-commonjs": "^5.0.4", + "rollup-plugin-node-resolve": "^2.0.0", "rollupify": "^0.3.4", "uglify-js": "^2.7.3", "uglifyify": "^3.0.3" }, "dependencies": { - "deep-equal": "^1.0.1" + "deep-equal": "^1.0.1", + "simple-statistics": "^2.1.0" }, "babel": { "presets": [ diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..8d14c2c --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,12 @@ +module.exports = { + plugins: [ + require('rollup-plugin-node-resolve')({ + jsnext: true, + main: true, + browser: true, + }), + require('rollup-plugin-commonjs')({ + include: 'node_modules/**', + }), + ], +}; diff --git a/src/index.js b/src/index.js index 2d6ad01..2c1084f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,14 @@ import deepEqual from 'deep-equal'; +import linearRegression from 'simple-statistics/src/linear_regression'; +import linearRegressionLine from 'simple-statistics/src/linear_regression_line'; const COMPONENT_NAME = 'click-drag'; const DRAG_START_EVENT = 'dragstart'; const DRAG_MOVE_EVENT = 'dragmove'; const DRAG_END_EVENT = 'dragend'; +const TIME_TO_KEEP_LOG = 300; + function cameraPositionToVec3(camera, vec3) { let element = camera; @@ -339,6 +343,24 @@ const {initialize, tearDown} = (function closeOverInitAndTearDown() { let removeDragListeners; let draggedElement; let dragInfo; + const positionLog = []; + + function cleanUpPositionLog() { + const now = performance.now(); + while (positionLog.length && now - positionLog[0].time > TIME_TO_KEEP_LOG) { + // remove the first element; + positionLog.shift(); + } + } + + function onDragged({detail: {nextPosition}}) { + // Continuously clean up so we don't get huge logs built up + cleanUpPositionLog(); + positionLog.push({ + position: Object.assign({}, nextPosition), + time: performance.now(), + }); + } function onMouseDown({clientX, clientY}) { @@ -347,7 +369,17 @@ const {initialize, tearDown} = (function closeOverInitAndTearDown() { if (element) { // Can only drag one item at a time, so no need to check if any // listener is already set up - removeDragListeners = dragItem(THREE, element, offset, camera, depth, {clientX, clientY}); + let removeDragItemListeners = dragItem( + THREE, + element, + offset, + camera, + depth, + { + clientX, + clientY, + } + ); draggedElement = element; @@ -359,12 +391,71 @@ const {initialize, tearDown} = (function closeOverInitAndTearDown() { }; element.emit(DRAG_START_EVENT, dragInfo); + + element.addEventListener(DRAG_MOVE_EVENT, onDragged); + + removeDragListeners = _ => { + element.removeEventListener(DRAG_MOVE_EVENT, onDragged); + // eslint-disable-next-line no-unused-expressions + removeDragItemListeners && removeDragItemListeners(); + // in case this removal function gets called more than once + removeDragItemListeners = null; + }; } } + function fitLineToVelocity(dimension) { + + if (positionLog.length < 2) { + return 0; + } + + const velocities = positionLog + + // Pull out just the x, y, or z values + .map(log => ({time: log.time, value: log.position[dimension]})) + + // Then convert that into an array of array pairs [time, value] + .reduce((memo, log, index, collection) => { + + // skip the first item (we're looking for pairs) + if (index === 0) { + return memo; + } + + const deltaPosition = log.value - collection[index - 1].value; + const deltaTime = (log.time - collection[index - 1].time) / 1000; + + // The new value is the change in position + memo.push([log.time, deltaPosition / deltaTime]); + + return memo; + + }, []); + + // Calculate the line function + const lineFunction = linearRegressionLine(linearRegression(velocities)); + + // Calculate what the point was at the end of the line + // ie; the velocity at the time the drag stopped + return lineFunction(positionLog[positionLog.length - 1].time); + } + function onMouseUp({clientX, clientY}) { - draggedElement.emit(DRAG_END_EVENT, Object.assign({}, dragInfo, {clientX, clientY})); + cleanUpPositionLog(); + + const velocity = { + x: fitLineToVelocity('x'), + y: fitLineToVelocity('y'), + z: fitLineToVelocity('z'), + }; + + draggedElement.emit( + DRAG_END_EVENT, + Object.assign({}, dragInfo, {clientX, clientY, velocity}) + ); + removeDragListeners && removeDragListeners(); // eslint-disable-line no-unused-expressions removeDragListeners = undefined; } @@ -487,4 +578,4 @@ export default function aframeDraggableComponent(aframe, componentName = COMPONE didMount(this, THREE, componentName); }, }); -}; +}