git.s-ol.nu leap-finger-scan / bdd0ffc
movable slices s-ol 2 years ago
2 changed file(s) with 157 addition(s) and 23 deletion(s). Raw diff Collapse all Expand all
00 import Leap from 'leapjs';
11 import * as T from 'three';
22 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
3 import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
34 import { saveAs } from 'file-saver';
45
56 const clamp = (val, min=0, max=1) => Math.min(max, Math.max(min, val));
1112 const $ = x => document.getElementById(x);
1213
1314 const canvas = $('main');
15 const renderer = new T.WebGLRenderer({ canvas });
1416
1517 const scene = new T.Scene();
1618 scene.background = new T.Color().setHSL(0.7, .95, .93);
3234
3335 const camera = new T.PerspectiveCamera(75, 1, 0.1, 1000);
3436 camera.position.set(-30, 400, 80);
35 const controls = new OrbitControls(camera, canvas);
36 // const context = canvas.getContext('webgl2', { alpha: false });
37 const renderer = new T.WebGLRenderer({ canvas }); // , context });
37
38 const orbit = new OrbitControls(camera, canvas);
39 const transform = new TransformControls(camera, canvas);
40 transform.addEventListener('dragging-changed', e => { orbit.enabled = !e.value });
41 scene.add(transform);
3842
3943 window.onresize = () => {
4044 renderer.setSize(canvas.offsetWidth, canvas.offsetHeight);
4145 camera.aspect = canvas.offsetWidth / canvas.offsetHeight;
4246 camera.updateProjectionMatrix();
43 controls.update();
47 orbit.update();
4448 };
4549 window.onresize();
4650
4751 class Finger extends T.Group {
4852 static tipGeom = new T.SphereBufferGeometry(4, 16, 16);
4953 static trackGeom = new T.SphereBufferGeometry(2, 4, 4);
54 static sliceMaterial = new T.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 });
5055 static MAX_TRACK = 1 << 10;
5156
52 constructor(hue) {
57 constructor(i) {
5358 super();
5459
55 const color = new T.Color().setHSL(hue || Math.random(), .8, .6);
56 const meshMaterial = new T.MeshLambertMaterial({ color });
57 const lineMaterial = new T.LineBasicMaterial({ color, linewidth: 2 });
58
59 this.tip = new T.Mesh(Finger.tipGeom, meshMaterial);
60 const color = new T.Color().setHSL(i / 5, .8, .6);
61 this.meshMaterial = new T.MeshLambertMaterial({ color });
62 this.lineMaterial = new T.LineBasicMaterial({ color, linewidth: 2 });
63
64 this.tip = new T.Mesh(Finger.tipGeom, this.meshMaterial);
6065 this.add(this.tip);
6166
6267 this.lineGeometry = new T.Geometry();
6368 this.lineGeometry.vertices.push(new T.Vector3(), new T.Vector3(), new T.Vector3());
64 this.skeleton = new T.Line(this.lineGeometry, lineMaterial);
69 this.skeleton = new T.Line(this.lineGeometry, this.lineMaterial);
6570 this.add(this.skeleton);
6671
67 this.trackers = new T.InstancedMesh(Finger.trackGeom.clone(), meshMaterial.clone(), Finger.MAX_TRACK);
72 this.trackers = new T.InstancedMesh(Finger.trackGeom.clone(), this.meshMaterial.clone(), Finger.MAX_TRACK);
6873 this.add(this.trackers);
6974
7075 this.dataPoints = [];
7176 this.track = false;
77
78 $('header').append(this.createControls(i));
79 }
80
81 createControls(i) {
82 const title = document.createElement('span');
83 title.innerText = `slice ${i}`;
84
85 const select = document.createElement('button');
86 select.innerText = 'select';
87 select.onclick = e => {
88 e.preventDefault();
89 this.selectSlice();
90 }
91
92 const remove = document.createElement('button');
93 remove.innerText = 'remove';
94 remove.onclick = e => {
95 e.preventDefault();
96 this.removeSlice();
97 }
98
99 const controls = document.createElement('div');
100 controls.append(title);
101 controls.append(select);
102 controls.append(remove);
103 return controls;
104 }
105
106 selectSlice() {
107 if (!this.slice) {
108 const average = this.dataPoints.reduce((sum, p) => sum.add(new T.Vector3(...p.dip)), new T.Vector3());
109 if (this.dataPoints.length)
110 average.divideScalar(this.dataPoints.length);
111
112 this.slice = new T.Mesh(new T.BoxGeometry(), Finger.sliceMaterial);
113 this.slice.scale.set(25, 5, 50);
114 this.slice.position.copy(average);
115 this.add(this.slice);
116 transform.attach(this.slice);
117 } else if (transform.object === this.slice) {
118 transform.detach();
119 } else {
120 transform.attach(this.slice);
121 }
122 }
123
124 updateSlice() {
125 const sliceTransform = this.slice.matrixWorld.getInverse();
126 const points = this.dataPoints.reduce((list, point) => {
127 const tip = new T.Vector3(...point.tip);
128
129 tip.applyMatrix4(sliceTransform);
130 if (Math.abs(tip.x) <= 1 && Math.abs(tip.y) <= 1 && Math.abs(tip.z) <= 1) {
131
132 }
133 }, []);
134 }
135
136 removeSlice() {
137 if (!this.slice)
138 return;
139
140 if (transform.object === this.slice)
141 transform.detach();
142
143 this.remove(this.slice);
144 this.slice = null;
72145 }
73146
74147 update(finger) {
90163 }
91164
92165 clear() {
166 this.removeSlice();
93167 this.dataPoints = [];
94168 for (let i = 0; i < Finger.MAX_TRACK; i++) {
95169 this.trackers.setMatrixAt(i, new T.Matrix4());
120194 scene.add(palm);
121195
122196 const fingers = [0,1,2,3,4].map(i => {
123 const finger = new Finger(i / 5);
197 const finger = new Finger(i);
124198 scene.add(finger);
125199 return finger;
126200 });
128202
129203 window.onkeydown = (e) => {
130204 if (e.key === 'r') {
131 e.preventDefault();
132205 fingers.forEach(finger => finger.clear());
133206 return;
134207 } else if (e.key === 's') {
135 e.preventDefault();
136208 const data = fingers.map(finger => finger.store());
137209 saveAs(new Blob([JSON.stringify(data)]), 'text/json');
138210 } else if (e.key === 'l') {
139 e.preventDefault();
140211 const input = document.createElement('input');
141212 input.type = 'file';
142213 input.click();
149220 }
150221 reader.readAsText(e.target.files[0]);
151222 }
223 } else if (e.key === 'Shift') {
224 transform.setMode('rotate');
225 } else if (e.key === 'Alt') {
226 transform.setMode('scale');
152227 }
153228
154229 const i = +e.key;
161236
162237 window.onkeyup = (e) => {
163238 const i = +e.key;
239
240 if (e.key === 'Shift' || e.key === 'Alt') {
241 transform.setMode('translate');
242 }
164243
165244 if (i > -1 && i < 6) {
166245 e.preventDefault();
190269 }, frame => {
191270 update(frame);
192271
193 controls.update();
272 orbit.update();
194273 renderer.render(scene, camera);
195274 });
196275
1313 width: 100vw;
1414 height: 100vh;
1515
16 overflow: hidden;
17
1618 flex-direction: column;
19
20 background: #212121;
21 color: #696969;
22 font-family: sans-serif;
1723 }
1824
1925 header {
2026 display: flex;
2127
28 min-height: 7em;
2229 justify-content: space-between;
30 flex-wrap: wrap;
2331
24 height: 150px;
2532 flex: 0 0 auto;
33 }
34
35 header > div {
36 display: flex;
37
38 flex-direction: column;
39 margin: 0.5em 1em;
40 }
41
42 header > div > span {
43 font-size: 2em;
44 }
45
46 button {
47 margin: 0.2em 0;
48 flex: 1;
49
50 border: 2px solid #696969;
51 color: #696969;
52 background: none;
53
54 transition: background 300ms, color 300ms;
55 }
56
57 button:hover {
58 background: #696969;
59 color: #212121;
60 }
61
62 key {
63 font-size: 0.8rem;
64 line-height: 0.8rem;
65 padding: 0.2rem;
66 font-family: monospace;
67
68 background: #696969;
69 color: #212121;
70 }
71
72 div {
73 padding: 0 0.5em 0.5em;
2674 }
2775
2876 #main {
2977 flex: 1 1 0;
78 width: auto !important;
79 height: auto !important;
3080 }
3181 </style>
3282 </head>
3383 <body>
34 <header>
35 <canvas id="graphs_a"></canvas>
36 <canvas id="graphs_b"></canvas>
37 <div id="controls"></div>
38 </header>
84 <header id="header"></header>
85 <div>
86 <b>hotkeys &mdash;</b>
87 <key>1-5</key> track fingers &mdash;
88 <key>shift</key> rotate slice &mdash;
89 <key>alt</key> scale slice &mdash;
90 <key>S</key> export data &mdash;
91 <key>L</key> import data &mdash;
92 <key>R</key> reset
93 </div>
3994 <canvas id="main"></canvas>
4095 </body>
4196 </html>