Merge pull request #2 from jesstelford/physics
Return smoothed drag end velocity with event
This commit is contained in:
commit
8bda1fde5c
7 changed files with 177 additions and 12 deletions
|
@ -32,6 +32,7 @@ Emitted with the following info:
|
||||||
Emitted with the following info:
|
Emitted with the following info:
|
||||||
|
|
||||||
- `offset: {x, y, z}` - The offset from entity center to drag position.
|
- `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
|
- `depth` - the perpendicular distance from the screen to align the entity while
|
||||||
dragging
|
dragging
|
||||||
- `clientX` - the final mouse event's `clientX` value
|
- `clientX` - the final mouse event's `clientX` value
|
||||||
|
|
|
@ -12,22 +12,33 @@
|
||||||
h1 {
|
h1 {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
a {
|
a.demo-link {
|
||||||
color: #FAFAFA;
|
|
||||||
display: block;
|
display: block;
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
a {
|
||||||
|
color: #FAFAFA;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>A-Frame Click & Drag Component</h1>
|
<h1>A-Frame Click & Drag Component</h1>
|
||||||
<a href="basic/">Basic Demo</a>
|
<a class="demo-link" href="basic/">Basic Demo</a>
|
||||||
<p>Click + Drag entities on the screen. Note the plane cannot be dragged (it does not have the "click-drag" attribute).</p>
|
<p>Click + Drag entities on the screen. Note the plane cannot be dragged (it does not have the "click-drag" attribute).</p>
|
||||||
<p>Try the WASD keys to move around while dragging an entity!</p>
|
<p>Try the WASD keys to move around while dragging an entity!</p>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<a href="events/">Events Demo</a>
|
|
||||||
|
<a class="demo-link" href="events/">Events Demo</a>
|
||||||
<p>Events are fired for beginning to drag, ending a drag, and for each drag event in etween.</p>
|
<p>Events are fired for beginning to drag, ending a drag, and for each drag event in etween.</p>
|
||||||
<p>This example shows how those events can be used to "ghost" a dragged entity</p>
|
<p>This example shows how those events can be used to "ghost" a dragged entity.</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<a class="demo-link" href="physics/">Physics Demo</a>
|
||||||
|
<p>Calculating the velocity at the time of drag end.</p>
|
||||||
|
<p>Combined with a physics library (for example; <a href="https://github.com/donmccurdy/aframe-extras/tree/master/src/physics">aframe-extras physics</a>), we get some very nice interactions.</p>
|
||||||
|
<p>Try gently tossing the ball around / throwing it at the ground.</p>
|
||||||
|
|
||||||
<!-- GitHub Corner. -->
|
<!-- GitHub Corner. -->
|
||||||
<a href="https://github.com/jesstelford/aframe-click-drag-component" class="github-corner">
|
<a href="https://github.com/jesstelford/aframe-click-drag-component" class="github-corner">
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
import aframe from 'aframe';
|
import aframe from 'aframe';
|
||||||
|
import extras from 'aframe-extras';
|
||||||
import clickDragComponent from '../src/index';
|
import clickDragComponent from '../src/index';
|
||||||
|
|
||||||
|
extras.physics.registerAll(aframe);
|
||||||
clickDragComponent(aframe);
|
clickDragComponent(aframe);
|
||||||
|
|
||||||
|
|
42
examples/physics/index.html
Normal file
42
examples/physics/index.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>A-Frame Click & Drag Component - Events</title>
|
||||||
|
<script src="../build.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a-scene physics="debug: true">
|
||||||
|
|
||||||
|
<a-sphere
|
||||||
|
click-drag
|
||||||
|
dynamic-body="mass: 20"
|
||||||
|
position="0 3 -1"
|
||||||
|
radius="1.25"
|
||||||
|
color="#EF2D5E"
|
||||||
|
>
|
||||||
|
</a-sphere>
|
||||||
|
|
||||||
|
<a-plane static-body rotation="-90 0 0" width="200" height="200" color="#7BC8A4"></a-plane>
|
||||||
|
|
||||||
|
<a-sky color="#ECECEC"></a-sky>
|
||||||
|
|
||||||
|
<a-entity position="0 0 3.8">
|
||||||
|
<a-camera look-controls-enabled="false"></a-camera>
|
||||||
|
</a-entity>
|
||||||
|
<script>
|
||||||
|
var draggable = document.querySelector('[click-drag]');
|
||||||
|
draggable.addEventListener('dragstart', function(dragInfo) {
|
||||||
|
draggable.pause();
|
||||||
|
});
|
||||||
|
draggable.addEventListener('dragend', function(dragInfo) {
|
||||||
|
var x = dragInfo.detail.velocity.x;
|
||||||
|
var y = dragInfo.detail.velocity.y;
|
||||||
|
var z = dragInfo.detail.velocity.z;
|
||||||
|
|
||||||
|
draggable.play();
|
||||||
|
draggable.body.velocity.set(x, y, z);
|
||||||
|
console.log('drag end', dragInfo.detail.velocity);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</a-scene>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
package.json
12
package.json
|
@ -5,11 +5,11 @@
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"browser": "dist/aframe-click-drag-component.min.js",
|
"browser": "dist/aframe-click-drag-component.min.js",
|
||||||
"scripts": {
|
"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",
|
"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": "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 ]",
|
"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",
|
"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",
|
"preghpages": "npm run build-example && rm -rf gh-pages && mkdir gh-pages && cp -r examples/* gh-pages",
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aframe": "^0.3.0",
|
"aframe": "^0.3.0",
|
||||||
|
"aframe-extras": "^2.5.3",
|
||||||
"babel-cli": "^6.14.0",
|
"babel-cli": "^6.14.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||||
"babel-preset-es2015": "^6.9.0",
|
"babel-preset-es2015": "^6.9.0",
|
||||||
|
@ -55,12 +56,15 @@
|
||||||
"exorcist": "^0.4.0",
|
"exorcist": "^0.4.0",
|
||||||
"ghpages": "^0.0.8",
|
"ghpages": "^0.0.8",
|
||||||
"in-publish": "^2.0.0",
|
"in-publish": "^2.0.0",
|
||||||
|
"rollup-plugin-commonjs": "^5.0.4",
|
||||||
|
"rollup-plugin-node-resolve": "^2.0.0",
|
||||||
"rollupify": "^0.3.4",
|
"rollupify": "^0.3.4",
|
||||||
"uglify-js": "^2.7.3",
|
"uglify-js": "^2.7.3",
|
||||||
"uglifyify": "^3.0.3"
|
"uglifyify": "^3.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-equal": "^1.0.1"
|
"deep-equal": "^1.0.1",
|
||||||
|
"simple-statistics": "^2.1.0"
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
|
|
12
rollup.config.js
Normal file
12
rollup.config.js
Normal file
|
@ -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/**',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
97
src/index.js
97
src/index.js
|
@ -1,10 +1,14 @@
|
||||||
import deepEqual from 'deep-equal';
|
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 COMPONENT_NAME = 'click-drag';
|
||||||
const DRAG_START_EVENT = 'dragstart';
|
const DRAG_START_EVENT = 'dragstart';
|
||||||
const DRAG_MOVE_EVENT = 'dragmove';
|
const DRAG_MOVE_EVENT = 'dragmove';
|
||||||
const DRAG_END_EVENT = 'dragend';
|
const DRAG_END_EVENT = 'dragend';
|
||||||
|
|
||||||
|
const TIME_TO_KEEP_LOG = 300;
|
||||||
|
|
||||||
function cameraPositionToVec3(camera, vec3) {
|
function cameraPositionToVec3(camera, vec3) {
|
||||||
|
|
||||||
let element = camera;
|
let element = camera;
|
||||||
|
@ -339,6 +343,24 @@ const {initialize, tearDown} = (function closeOverInitAndTearDown() {
|
||||||
let removeDragListeners;
|
let removeDragListeners;
|
||||||
let draggedElement;
|
let draggedElement;
|
||||||
let dragInfo;
|
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}) {
|
function onMouseDown({clientX, clientY}) {
|
||||||
|
|
||||||
|
@ -347,7 +369,17 @@ const {initialize, tearDown} = (function closeOverInitAndTearDown() {
|
||||||
if (element) {
|
if (element) {
|
||||||
// Can only drag one item at a time, so no need to check if any
|
// Can only drag one item at a time, so no need to check if any
|
||||||
// listener is already set up
|
// 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;
|
draggedElement = element;
|
||||||
|
|
||||||
|
@ -359,12 +391,71 @@ const {initialize, tearDown} = (function closeOverInitAndTearDown() {
|
||||||
};
|
};
|
||||||
|
|
||||||
element.emit(DRAG_START_EVENT, dragInfo);
|
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}) {
|
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 && removeDragListeners(); // eslint-disable-line no-unused-expressions
|
||||||
removeDragListeners = undefined;
|
removeDragListeners = undefined;
|
||||||
}
|
}
|
||||||
|
@ -487,4 +578,4 @@ export default function aframeDraggableComponent(aframe, componentName = COMPONE
|
||||||
didMount(this, THREE, componentName);
|
didMount(this, THREE, componentName);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
Loading…
Reference in a new issue