├── .gitignore
├── LICENSE.txt
├── README.md
├── bower.json
├── img
├── glyphicons-174-play.png
├── glyphicons-182-download-alt.png
├── glyphicons-186-screenshot.png
└── glyphicons-433-plus.png
├── index.html
└── js
├── Actuator.js
├── ActuatorControls.js
├── App.js
├── Controls.js
├── LeapHandHelper.js
├── MainControls.js
├── Workbench.js
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components
2 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Brian Peiris
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Perambulate
2 |
3 | Perambulate is a physics-based, VR construction game inspired by sodaplay.
4 |
5 | Use actuators to build machines and set them loose.
6 |
7 | To try Perambulate, you need a [WebVR compatible browser](http://webvr.info/) and a Leap Motion
8 | controller attached to your HMD.
9 |
10 | Perambulate was developed in [Firefox Nightly](http://mozvr.com/) with an Oculus Rift DK2 but
11 | it should work with Chrome WebVR builds and with a DK1.
12 |
13 | ## Usage and Tips
14 |
15 | Watch the video to get an idea of how Perambulate works: http://youtu.be/qLC8L-58Juk
16 |
17 | Bend the middle and ring fingers on your left hand to display the control panels :metal:.
18 |
19 | The main control panel appears when your palm is facing up. The actuator control panel appears when your palm is facing down. The main control panel lets you add new actuators, which appear in the center of the environment, activate the actuator animations and enable gravity. The actuator control panel lets you change the amplitude (the y-axis) and the phase (the x-axis) of the actuator's animation. Select actuators to control by poking them with your index finger.
20 |
21 | Be deliberate with your hand movements. The pose and grab detection isn't very intelligent and the Leap Motion can be very glitchy.
22 |
23 | Get the most out of your Leap Motion by following the VR troubleshooting guide: http://blog.leapmotion.com/troubleshooting-guide-vr-tracking/
24 |
25 | ## Known Issues
26 |
27 | The app can sometimes start in a bad state. You'll notice that lights are much darker or that only one eye is lit up in VR mode. Just refresh once or twice until it starts correctly.
28 |
29 | Your construction can sometimes go crazy if two actuators join at weird angles or when you don't mean to. This can probably be improved but it's partly due to the Leap Motion's glitches. See the link to the troubleshooting guide above.
30 |
31 | The slider widget on the actuator control panel kinda sucks. It doesn't behave as you'd expect and doesn't like being angled in odd positions. I'm going to have to submit some patches to Leap Motion's widget library.
32 |
33 | ## Attributions
34 |
35 | - Three.js by mrdoob et al. - https://github.com/mrdoob/three.js
36 | - Cannon.js by schteppe - https://github.com/schteppe/cannon.js
37 | - WebVR boilerplate by borismus - https://github.com/borismus/webvr-boilerplate
38 | - LeapJS library, plugins and widgets - https://github.com/leapmotion/leapjs
39 | - Glam by tparisi - https://github.com/tparisi/glam
40 | - Backbone and Underscore by jasenkas et al. - https://github.com/jashkenas/underscore
41 | - Icons by http://glyphicons.com/
42 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Perambulate",
3 | "dependencies": {
4 | "cannon.js": "schteppe/cannon.js#v0.6.2",
5 | "leapjs": "4f50e58ba916d65fba48b3f39566047ec659a9f6",
6 | "leapjs-plugins": "158dbd7fdf6d53d21df515445d68fc0909c1d126",
7 | "leapjs-widgets": "leapmotion/leapjs-widgets#1b9a9c11ff041fd8048cd58ae9c827ecc546a4db",
8 | "three.js": "brianpeiris/three.js#c9fd6070edd0ce9e4156d23d7e954eb7bb43c3d0",
9 | "glam": "brianpeiris/glam#579c0adef531d8d7e31d589f2d4c314a264b577f",
10 | "webvr-boilerplate": "borismus/webvr-boilerplate#95af06476e62621fb2c677f3ca05d5e0dfffa408",
11 | "backbone": "~1.1.2"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/img/glyphicons-174-play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brianpeiris/Perambulate/467366950e6bc9661f9464b17529f513f0ed67df/img/glyphicons-174-play.png
--------------------------------------------------------------------------------
/img/glyphicons-182-download-alt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brianpeiris/Perambulate/467366950e6bc9661f9464b17529f513f0ed67df/img/glyphicons-182-download-alt.png
--------------------------------------------------------------------------------
/img/glyphicons-186-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brianpeiris/Perambulate/467366950e6bc9661f9464b17529f513f0ed67df/img/glyphicons-186-screenshot.png
--------------------------------------------------------------------------------
/img/glyphicons-433-plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brianpeiris/Perambulate/467366950e6bc9661f9464b17529f513f0ed67df/img/glyphicons-433-plus.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
30 |
31 |
32 | LOADING...
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
81 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/js/Actuator.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 | var MESH_SCALE = 2;
4 | var WIDTH = 0.0125;
5 | var HEIGHT = 0.05;
6 |
7 | var Actuator = function (options) {
8 | this.options = options || {};
9 | this.amplitude = (
10 | this.options.amplitude === undefined ? 0 : this.options.amplitude);
11 | this.phase = 0;
12 |
13 | this.actuators = [];
14 | this.topPivotPoints = [];
15 | this.bottomPivotPoints = [];
16 |
17 | this.shape = new CANNON.Box(new CANNON.Vec3(
18 | WIDTH, HEIGHT, WIDTH));
19 | this.body = new CANNON.Body({mass: 1, position: this.options.position});
20 | this.body.addShape(this.shape);
21 | this.body.linearDamping = 0.5;
22 | this.body.angularDamping = 0.5;
23 |
24 | var geometry = new THREE.BoxGeometry(
25 | WIDTH * MESH_SCALE,
26 | HEIGHT * MESH_SCALE,
27 | WIDTH * MESH_SCALE
28 | );
29 | this.material = new THREE.MeshLambertMaterial({
30 | color: this.options.color
31 | });
32 | this.mesh = new THREE.Mesh(geometry, this.material);
33 | };
34 |
35 | var CONSTRAINT_OFFSET = 0.7;
36 |
37 | Actuator.prototype.stepBody = function (elapsed) {
38 | this.mesh.position.copy(this.body.position);
39 | this.mesh.quaternion.copy(this.body.quaternion);
40 | };
41 |
42 | var RATE = Math.PI / (1 * 1000);
43 | Actuator.prototype.stepActuation = function (elapsed) {
44 | this.scale = 1 + (Math.sin(
45 | elapsed * RATE + this.phase * Math.PI
46 | ) + 1) / 2 * this.amplitude;
47 | this.mesh.scale.setY(this.scale);
48 |
49 | var bodyHeight = HEIGHT * this.scale;
50 | var pivotPosition = bodyHeight + HEIGHT * CONSTRAINT_OFFSET;
51 |
52 | if (this.topJoinMesh) {
53 | this.topJoinMesh.scale.setY(1 / this.scale);
54 | }
55 | if (this.bottomJoinMesh) {
56 | this.bottomJoinMesh.scale.setY(1 / this.scale);
57 | }
58 |
59 | this.shape.halfExtents.y = bodyHeight;
60 |
61 | this.topPivotPoints.forEach(function (topPivotPoint) {
62 | topPivotPoint.y = pivotPosition;
63 | });
64 |
65 | this.bottomPivotPoints.forEach(function (bottomPivotPoint) {
66 | bottomPivotPoint.y = -pivotPosition;
67 | });
68 |
69 | this.shape.updateConvexPolyhedronRepresentation();
70 | };
71 |
72 | var JOIN_POINT_POS = HEIGHT + HEIGHT * CONSTRAINT_OFFSET;
73 | var TOP_JOIN_POINT = new CANNON.Vec3(0, JOIN_POINT_POS, 0);
74 | var BOTTOM_JOIN_POINT = new CANNON.Vec3(0, -JOIN_POINT_POS, 0);
75 |
76 | Actuator.prototype.joinToLocation = function (
77 | actuator,
78 | joinedPivotPoints,
79 | thisTop, otherTop
80 | ) {
81 | if (joinedPivotPoints.length === 0) {
82 | var joinMesh = new THREE.Mesh(
83 | new THREE.SphereGeometry(HEIGHT * CONSTRAINT_OFFSET),
84 | new THREE.MeshLambertMaterial({color: 'white'})
85 | );
86 | var joinMeshY = thisTop ? JOIN_POINT_POS : -JOIN_POINT_POS;
87 | joinMesh.position.set(0, joinMeshY, 0);
88 | if (thisTop) {
89 | this.topJoinMesh = joinMesh;
90 | }
91 | else {
92 | this.bottomJoinMesh = joinMesh;
93 | }
94 | this.mesh.add(joinMesh);
95 | }
96 | var pivotA = thisTop ? TOP_JOIN_POINT : BOTTOM_JOIN_POINT;
97 | var pivotB = otherTop ? TOP_JOIN_POINT : BOTTOM_JOIN_POINT;
98 | var actuatorConstraint = new CANNON.PointToPointConstraint(
99 | this.body,
100 | pivotA,
101 | actuator.body,
102 | pivotB
103 | );
104 | joinedPivotPoints.push(actuatorConstraint.pivotA);
105 | (otherTop ? actuator.topPivotPoints : actuator.bottomPivotPoints).push(
106 | actuatorConstraint.pivotB);
107 | return actuatorConstraint;
108 | };
109 |
110 | var closerToTop = function (quaternion, contactPosition) {
111 | var distanceToTop = contactPosition.distanceTo(
112 | quaternion.vmult(TOP_JOIN_POINT));
113 | var distanceToBottom = contactPosition.distanceTo(
114 | quaternion.vmult(BOTTOM_JOIN_POINT));
115 | return distanceToTop < distanceToBottom;
116 | };
117 |
118 | Actuator.prototype.joinTo = function (actuator, contact) {
119 | this.actuators.push(actuator);
120 | var thisTop = closerToTop(contact.bi.quaternion, contact.ri);
121 | var otherTop = closerToTop(contact.bj.quaternion, contact.rj);
122 | if (thisTop) {
123 | return this.joinToLocation(
124 | actuator, this.topPivotPoints,
125 | thisTop, otherTop);
126 | }
127 | else {
128 | return this.joinToLocation(
129 | actuator, this.bottomPivotPoints,
130 | thisTop, otherTop);
131 | }
132 | };
133 |
134 | Actuator.prototype.isJoinedTo = function (actuator) {
135 | return this.actuators.indexOf(actuator) !== -1;
136 | };
137 |
138 | window.Actuator = Actuator;
139 | })();
140 |
--------------------------------------------------------------------------------
/js/ActuatorControls.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var ActuatorControls = function (scene, leapController) {
3 | Controls.call(this, scene, leapController, '#actuatorControls', false);
4 |
5 | this.xmax = 0.2;
6 | this.ymax = 0.07;
7 |
8 | this.actuatorSettingButton = this.createPlane(
9 | '.actuatorSetting', 'actuatorSettingReleased', false);
10 | };
11 | ActuatorControls.prototype = Object.create(Controls.prototype);
12 |
13 | ActuatorControls.prototype.createPlane = function (selector, eventName, locking) {
14 | var plane = new InteractablePlane(
15 | document.querySelector(selector).glam.object.visuals[0].object,
16 | this.leapController,
17 | {
18 | damping: 0.06,
19 | moveX: true,
20 | moveY: true,
21 | moveZ: false
22 | }
23 | );
24 | plane.name = 'foo';
25 | plane.movementConstraints.y = function (y) {
26 | if (y < 0) { return 0; }
27 | if (y > this.ymax) { return this.ymax; }
28 | return y;
29 | }.bind(this);
30 | plane.movementConstraints.x = function (x) {
31 | if (x < 0) { return 0; }
32 | if (x > this.xmax) { return this.xmax; }
33 | return x;
34 | }.bind(this);
35 | plane.on('release', function (plane) {
36 | if (this.visible) {
37 | console.log(plane.mesh.position.x, this.xmax);
38 | console.log(plane.mesh.position.y, this.ymax);
39 | this.trigger(
40 | eventName,
41 | plane.mesh.position.x / this.xmax,
42 | plane.mesh.position.y / this.ymax
43 | );
44 | }
45 | }.bind(this));
46 | return plane;
47 | };
48 |
49 | ActuatorControls.prototype.setX = function (x) {
50 | this.actuatorSettingButton.mesh.position.setX(x * this.xmax);
51 | this.actuatorSettingButton.lastPosition.setX(x * this.xmax);
52 | };
53 |
54 | ActuatorControls.prototype.setY = function (y) {
55 | this.actuatorSettingButton.mesh.position.setY(y * this.ymax);
56 | this.actuatorSettingButton.lastPosition.setY(y * this.ymax);
57 | };
58 |
59 | window.ActuatorControls = ActuatorControls;
60 | }());
61 |
62 |
--------------------------------------------------------------------------------
/js/App.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | var App = function (width, height, scene, camera, renderer) {
5 | this.width = width;
6 | this.height = height;
7 | this.scene = scene;
8 | this.renderer = renderer;
9 | };
10 | _.extend(App.prototype, Backbone.Events);
11 |
12 | App.prototype.init = function () {
13 | this._initThree();
14 | this._initVR();
15 | this._initCannon();
16 | this._initLeap();
17 | this.workbench = new Workbench(
18 | this.world, this.scene, Leap.loopController);
19 | this.trigger('initialized');
20 | this._animate();
21 | };
22 |
23 | App.prototype._initThree = function () {
24 | this.camera = new THREE.PerspectiveCamera(
25 | 75, this.width / this.height, 0.1, 100 );
26 | };
27 |
28 | App.prototype._initVR = function () {
29 | this.controls = new THREE.VRControls(this.camera);
30 |
31 | this.effect = new THREE.VREffect(this.renderer);
32 | this.effect.setSize(this.width, this.height);
33 |
34 | this.manager = new WebVRManager(this.renderer, this.effect);
35 | };
36 |
37 | App.prototype.zeroSensor = function () {
38 | if (this.controls) {
39 | this.controls.zeroSensor();
40 | }
41 | };
42 |
43 | App.prototype._initCannon = function () {
44 | this.world = new CANNON.World();
45 | this.world.gravity.set(0, 0, 0);
46 | this.world.broadphase = new CANNON.NaiveBroadphase();
47 | this.world.solver.iterations = 10;
48 |
49 | var groundShape = new CANNON.Plane();
50 | this.groundBody = new CANNON.Body({
51 | mass: 0
52 | });
53 | this.groundBody.position.set(0, -0.5, 0);
54 | this.groundBody.quaternion.setFromAxisAngle(
55 | new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
56 | this.groundBody.addShape(groundShape);
57 | this.world.add(this.groundBody);
58 | };
59 |
60 | App.prototype._initLeap = function () {
61 | Leap.loop({
62 | optimizeHMD: true
63 | });
64 | Leap.loopController.use('transform', {
65 | vr: true,
66 | effectiveParent: this.camera,
67 | });
68 | Leap.loopController.use('boneHand', {
69 | scene: this.scene
70 | });
71 | };
72 |
73 | App.prototype._animate = function (timestamp) {
74 | timestamp = timestamp || 0;
75 | if (!this.start) { this.start = timestamp; }
76 | var elapsed = timestamp - this.start;
77 |
78 | this.controls.update();
79 |
80 | this.workbench.update(elapsed);
81 |
82 | var TIMESTEP = 1 / 60;
83 | this.world.step(TIMESTEP, elapsed);
84 |
85 | this.manager.render(this.scene, this.camera);
86 |
87 | requestAnimationFrame(this._animate.bind(this));
88 | };
89 |
90 | App.prototype.resizeView = function (width, height) {
91 | this.width = width;
92 | this.height = height;
93 | this.camera.aspect = this.width / this.height;
94 | this.camera.updateProjectionMatrix();
95 | this.effect.setSize(this.width, this.height);
96 | };
97 |
98 | window.App = App;
99 | }());
100 |
--------------------------------------------------------------------------------
/js/Controls.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var Controls = function (scene, leapController, controlsSelector, poseUp) {
3 | this.scene = scene;
4 | this.leapController = leapController;
5 | this.visible = false;
6 | this.poseUp = poseUp;
7 | this.lastDetected = new Date();
8 | this.lastPoseState = false;
9 |
10 | var controlsEl = document.querySelector(controlsSelector);
11 | this.controlBase = controlsEl.glam.object.transform;
12 | this.controlObject = controlsEl.glam.object.getChild(0).getChild(0);
13 |
14 | this.toggleControls(this.visible);
15 |
16 | this.leapController.on('frame', this.showControls.bind(this));
17 | };
18 | _.extend(Controls.prototype, Backbone.Events);
19 |
20 | Controls.prototype.createButton = function (selector, eventName, locking) {
21 | var button = new PushButton(
22 | new InteractablePlane(
23 | document.querySelector(selector).glam.object.visuals[0].object,
24 | this.leapController),
25 | {
26 | locking: locking,
27 | longThrow: -0.01,
28 | shortThrow: -0.005
29 | }
30 | );
31 | button.on('press', function (mesh) {
32 | if (this.visible) {
33 | this.trigger(eventName, button.pressed);
34 | }
35 | }.bind(this));
36 | button.on('release', function (mesh) {
37 | if (this.visible) {
38 | this.trigger(eventName, button.pressed);
39 | }
40 | }.bind(this));
41 | return button;
42 | };
43 |
44 | Controls.prototype.poseDetected = function (handHelper, hand) {
45 | return (
46 | (this.poseUp ? hand.palmNormal[1] > 0.2 : hand.palmNormal[1] < -0.2) &&
47 | handHelper.fingerStraight(hand.indexFinger) &&
48 | (
49 | handHelper.fingerBent(hand.middleFinger) ||
50 | handHelper.fingerBent(hand.ringFinger)
51 | ) &&
52 | handHelper.fingerStraight(hand.pinky)
53 | );
54 | };
55 |
56 | Controls.prototype.toggleVisuals = function (object, visible) {
57 | object.visuals[0].visible = visible;
58 | for (
59 | var i = 0, child = object.getChild(i);
60 | child !== null;
61 | i++, child = object.getChild(i)
62 | ) {
63 | this.toggleVisuals(child, visible);
64 | }
65 | };
66 |
67 | Controls.prototype.toggleControls = function (visible) {
68 | this.visible = visible;
69 | this.toggleVisuals(this.controlObject, visible);
70 | };
71 |
72 | POSE_GLIMPSE_THRESHOLD = 50;
73 | Controls.prototype.showControls = function (frame) {
74 | var hand;
75 | for (var i = 0; i < frame.hands.length; i++) {
76 | if (frame.hands[i].type === 'left') {
77 | hand = frame.hands[i];
78 | }
79 | }
80 | if (!hand) { return; }
81 |
82 | var handHelper = new LeapHandHelper(hand);
83 | var poseDetected = this.poseDetected(handHelper, hand);
84 | if (
85 | this.lastPoseState !== poseDetected &&
86 | (new Date() - this.lastDetected) > POSE_GLIMPSE_THRESHOLD
87 | ) {
88 | if (!poseDetected) {
89 | this.visible = !this.visible;
90 | this.toggleControls(this.visible);
91 | this.controlBase.position.copy(handHelper.palmPosition);
92 | this.controlBase.quaternion.copy(handHelper.palmQuaternion);
93 | this.lastDetected = new Date();
94 | }
95 | this.lastPoseState = poseDetected;
96 | }
97 | };
98 | window.Controls = Controls;
99 | }());
100 |
101 |
--------------------------------------------------------------------------------
/js/LeapHandHelper.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var LeapHandHelper = function (hand) {
3 | this.palmPosition = new THREE.Vector3().fromArray(hand.palmPosition);
4 | this.palmQuaternion = this.getPalmQuaternion(hand);
5 | };
6 |
7 | var getMatrixFromArray = function (arr) {
8 | var matrix = new THREE.Matrix4();
9 | matrix.set(
10 | arr[0], arr[1], arr[2], 0,
11 | arr[4], arr[5], arr[6], 0,
12 | arr[8], arr[9], arr[10], 0,
13 | 0, 0, 0, 0
14 | );
15 | return matrix;
16 | };
17 |
18 | var ROTATION_OFFSET = new THREE.Quaternion().setFromAxisAngle(
19 | new THREE.Vector3(0, 0, 1), -Math.PI / 2);
20 |
21 | LeapHandHelper.prototype.getPalmQuaternion = function (hand) {
22 | var quaternion = new THREE.Quaternion().setFromRotationMatrix(
23 | getMatrixFromArray(hand.indexFinger.metacarpal.matrix()));
24 | quaternion.inverse();
25 | quaternion.multiply(ROTATION_OFFSET);
26 | return quaternion;
27 | };
28 |
29 | var getBoneCenter = function (bone) {
30 | return new THREE.Vector3().fromArray(bone.center());
31 | };
32 |
33 | var getFingerPositionDetails = function (finger) {
34 | var metacarpalPosition = getBoneCenter(finger.metacarpal);
35 | var proximalPosition = getBoneCenter(finger.proximal);
36 | var boneLength = finger.metacarpal.length + finger.proximal.length;
37 | var distance = metacarpalPosition.distanceTo(proximalPosition);
38 | return {distance: distance, boneLength: boneLength};
39 | };
40 |
41 | LeapHandHelper.prototype.fingerStraight = function (finger) {
42 | var details = getFingerPositionDetails(finger);
43 | var threshold = details.boneLength * 0.479;
44 | return details.distance > threshold;
45 | };
46 |
47 | LeapHandHelper.prototype.fingerBent = function (finger) {
48 | var details = getFingerPositionDetails(finger);
49 | var threshold = details.boneLength * 0.453;
50 | return details.distance < threshold;
51 | };
52 |
53 | window.LeapHandHelper = LeapHandHelper;
54 | }());
55 |
--------------------------------------------------------------------------------
/js/MainControls.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var MainControls = function (scene, leapController) {
3 | Controls.call(this, scene, leapController, '#mainControls', true);
4 |
5 | this.addActuatorButton = this.createButton(
6 | '.addActuator', 'addActuatorPressed', false);
7 | this.activateActuatorsButton = this.createButton(
8 | '.activateActuators', 'activateActuatorsPressed', true);
9 | this.enableGravityButton = this.createButton(
10 | '.enableGravity', 'enableGravityPressed', true);
11 | };
12 | MainControls.prototype = Object.create(Controls.prototype);
13 | window.MainControls = MainControls;
14 | }());
15 |
--------------------------------------------------------------------------------
/js/Workbench.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var Workbench = function (world, scene, leapController) {
3 | this.world = world;
4 | this.scene = scene;
5 | this.leapController = leapController;
6 | this.actuatorsActivated = false;
7 |
8 | this.actuators = [];
9 | this.bodyActuatorMap = {};
10 |
11 | this.selectedActuator = null;
12 | this.heldActuators = {};
13 |
14 | this.addActuator('white', new CANNON.Vec3(0.1, 0, -0.2));
15 | this.addActuator('white', new CANNON.Vec3(-0.1, 0, -0.2));
16 |
17 | this.leapController.on('frame', this.interact.bind(this));
18 |
19 | this.mainControls = new MainControls(this.scene, this.leapController);
20 | this.mainControls.on('addActuatorPressed', function () {
21 | this.addActuator('white', new CANNON.Vec3(0, 0, -0.2));
22 | }.bind(this));
23 | this.mainControls.on('activateActuatorsPressed', function (pressed) {
24 | this.actuatorsActivated = pressed;
25 | }.bind(this));
26 | this.mainControls.on('enableGravityPressed', function (pressed) {
27 | this.world.gravity.set(0, pressed ? -1.8 : 0, 0);
28 | }.bind(this));
29 |
30 | this.actuatorControls = new ActuatorControls(this.scene, this.leapController);
31 | this.actuatorControls.on('actuatorSettingReleased', function (x, y) {
32 | this.selectedActuator.phase = x;
33 | this.selectedActuator.amplitude = y;
34 | }.bind(this));
35 | };
36 |
37 | Workbench.prototype.addActuator = function (color, position) {
38 | var actuator = new Actuator({
39 | amplitude: 0,
40 | position: position,
41 | color: color
42 | });
43 | this.world.add(actuator.body);
44 | this.scene.add(actuator.mesh);
45 | this.actuators.push(actuator);
46 | this.bodyActuatorMap[actuator.body.id] = actuator;
47 | actuator.body.addEventListener(
48 | 'collide', this.joinActuators.bind(this));
49 | };
50 |
51 | Workbench.prototype.joinActuators = function (event) {
52 | var target = this.bodyActuatorMap[event.contact.bi.id];
53 | var body = this.bodyActuatorMap[event.contact.bj.id];
54 | if (!target || !body) { return; }
55 | if (target.isJoinedTo(body) || body.isJoinedTo(target)) { return; }
56 | this.world.addConstraint(target.joinTo(body, event.contact));
57 | };
58 |
59 | Workbench.prototype.getClosestActuator = function (position) {
60 | var closest = null;
61 | var closestDistance;
62 | this.actuators.forEach(function (actuator) {
63 | var currentDistance = position.distanceTo(actuator.body.position);
64 | if (!closest || currentDistance < closestDistance) {
65 | closest = actuator;
66 | closestDistance = currentDistance;
67 | }
68 | });
69 | return closest;
70 | };
71 |
72 | var ZERO = new CANNON.Vec3(0, 0, 0);
73 | var YAXIS = new CANNON.Vec3(0, 1, 0);
74 | Workbench.prototype.createHinge = function (hand, currentActuator) {
75 | var constraintBody =
76 | this.heldActuators[hand.type + 'constraintBody'] =
77 | new CANNON.Body();
78 | var constraint =
79 | this.heldActuators[hand.type + 'constraint'] =
80 | new CANNON.HingeConstraint(
81 | constraintBody, currentActuator.body,
82 | {
83 | pivotA: ZERO,
84 | axisA: YAXIS,
85 | pivotB: ZERO,
86 | axisB: YAXIS
87 | }
88 | );
89 | this.world.addConstraint(constraint);
90 | return constraintBody;
91 | };
92 |
93 |
94 | var POSITION_OFFSET = new THREE.Vector3(0.015, -0.005, -0.01);
95 | var GRAB_STRENGTH_THRESHOLD = 0.8;
96 | var GRAB_DISTANCE_THRESHOLD = 0.1;
97 | var SELECTION_DISTANCE_THRESHOLD = 0.05;
98 | Workbench.prototype.interact = function (frame) {
99 | for (var i = 0; i < frame.hands.length; i++) {
100 | var hand = frame.hands[i];
101 | var currentActuator = this.heldActuators[hand.type];
102 | var constraintBody =
103 | this.heldActuators[hand.type + 'constraintBody'];
104 | var handHelper = new LeapHandHelper(hand);
105 |
106 | var closestActuator = this.getClosestActuator(
107 | handHelper.palmPosition);
108 | var distanceToPalm = (
109 | closestActuator &&
110 | closestActuator.body.position.distanceTo(
111 | handHelper.palmPosition));
112 | if (
113 | !currentActuator &&
114 | hand.grabStrength > GRAB_STRENGTH_THRESHOLD &&
115 | closestActuator && distanceToPalm < GRAB_DISTANCE_THRESHOLD
116 | ) {
117 | this.heldActuators[hand.type] =
118 | currentActuator = closestActuator;
119 | if (currentActuator) {
120 | constraintBody = this.createHinge(hand, currentActuator);
121 | }
122 | }
123 |
124 | if (hand.grabStrength <= GRAB_STRENGTH_THRESHOLD) {
125 | if (currentActuator) {
126 | this.world.removeConstraint(
127 | this.heldActuators[hand.type + 'constraint']);
128 | this.actuators.forEach(function (actuator) {
129 | actuator.body.velocity.setZero();
130 | }.bind(this));
131 | }
132 | this.heldActuators[hand.type] = currentActuator = null;
133 | this.heldActuators[hand.type + 'constraintBody'] =
134 | constraintBody = null;
135 | this.heldActuators[hand.type + 'constraint'] = null;
136 |
137 | if (closestActuator && this.selectedActuator !== closestActuator) {
138 | var distalPosition = new THREE.Vector3().fromArray(
139 | hand.indexFinger.distal.center());
140 | var distanceToClosest = distalPosition.distanceTo(
141 | closestActuator.body.position);
142 | if (distanceToClosest < SELECTION_DISTANCE_THRESHOLD) {
143 | if (this.selectedActuator) {
144 | this.selectedActuator.mesh.material.color.setHex(0xffffff);
145 | }
146 | this.selectedActuator = closestActuator;
147 | this.selectedActuator.mesh.material.color.setHex(0xff0000);
148 | this.actuatorControls.setX(this.selectedActuator.phase);
149 | this.actuatorControls.setY(this.selectedActuator.amplitude);
150 | }
151 | }
152 | }
153 |
154 | if (currentActuator) {
155 | var actuatorQuaternion = handHelper.palmQuaternion.clone();
156 | constraintBody.quaternion.copy(actuatorQuaternion);
157 |
158 | var rotatedOffset = POSITION_OFFSET.clone().applyQuaternion(
159 | handHelper.palmQuaternion);
160 | var actuatorPosition = handHelper.palmPosition.clone().add(
161 | rotatedOffset);
162 | constraintBody.position.copy(actuatorPosition);
163 | }
164 | }
165 | };
166 |
167 |
168 | Workbench.prototype.update = function (elapsed) {
169 | this.actuators.forEach(function (actuator) {
170 | actuator.stepBody(elapsed);
171 | if (this.actuatorsActivated) {
172 | actuator.stepActuation(elapsed);
173 | }
174 | else {
175 | actuator.stepActuation(0);
176 | }
177 | }.bind(this));
178 | };
179 | window.Workbench = Workbench;
180 | }());
181 |
--------------------------------------------------------------------------------
/js/index.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var startApp = function () {
3 | var app = new App(
4 | window.innerWidth, window.innerHeight,
5 | glam.Graphics.instance.scene,
6 | glam.Graphics.instance.camera,
7 | glam.Graphics.instance.renderer
8 | );
9 |
10 | window.addEventListener('resize', function () {
11 | app.resizeView(window.innerWidth, window.innerHeight);
12 | });
13 |
14 | window.addEventListener('keydown', function (event) {
15 | if (event.keyCode === 'Z'.charCodeAt(0) ) {
16 | app.zeroSensor();
17 | }
18 | });
19 |
20 | app.on('initialized', function () {
21 | window.document.querySelector('.loading-indicator').style.display = 'none';
22 | });
23 |
24 | app.init();
25 | };
26 |
27 | var glamReadyIntervalId = setInterval(function () {
28 | if (glam.DOM.isReady) {
29 | clearInterval(glamReadyIntervalId);
30 | glam.DOM.viewers.glamRoot.app.controllerScript.controls.enabled = false;
31 | glam.Application.instance.runloop = function () {};
32 | startApp();
33 | }
34 | }, 10);
35 | }());
36 |
--------------------------------------------------------------------------------