├── matcap.png
├── matcap1.jpg
├── matcap2.png
├── matcap3.png
├── .gitattributes
├── README.md
├── fragment-shader.js
├── index.html
├── LICENSE
├── vertex-shader.js
├── main.js
└── OrbitControls.js
/matcap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spite/makio-torus/HEAD/matcap.png
--------------------------------------------------------------------------------
/matcap1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spite/makio-torus/HEAD/matcap1.jpg
--------------------------------------------------------------------------------
/matcap2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spite/makio-torus/HEAD/matcap2.png
--------------------------------------------------------------------------------
/matcap3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spite/makio-torus/HEAD/matcap3.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ourobouros, the GPU version
2 |
3 | This is a GPU "port" of https://twitter.com/MAKIO135/status/1383163336905396225, a hic et nunc NFT by https://twitter.com/MAKIO135. The original NFT is a capture of https://observablehq.com/d/0f7ad63e053a2787?ui=classic, which was a bit sluggish. So we took it as a challenge to move it to the GPU, as an example of how to deal with parametric curves in a vertex shader.
4 |
5 | Here's the thread with some more details https://twitter.com/thespite/status/1383359844497825792
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fragment-shader.js:
--------------------------------------------------------------------------------
1 | const shader = `#version 300 es
2 | precision highp float;
3 |
4 | // texture uniforms.
5 | uniform sampler2D matCapMap;
6 |
7 | // varyings.
8 | in vec3 pos;
9 | in vec3 normal;
10 |
11 | // output.
12 | out vec4 color;
13 |
14 | void main() {
15 | // calculate matcap coordinates.
16 | vec3 n = normalize(normal);
17 | vec3 eye = normalize(pos.xyz);
18 | vec3 r = reflect( eye, normal );
19 | float m = 2. * sqrt( pow( r.x, 2. ) + pow( r.y, 2. ) + pow( r.z + 1., 2. ) );
20 | vec2 vN = r.xy / m + .5;
21 |
22 | // lookup matcap.
23 | vec3 mat = texture(matCapMap, vN).rgb;
24 |
25 | // return matcap.
26 | color = vec4(mat, 1.);
27 |
28 | // return normal.
29 | // color = vec4(.5 + .5 * n, 1.);
30 | }
31 | `;
32 |
33 | export { shader };
34 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ouroboros GPU
5 |
6 |
7 |
11 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 thespite and makio135
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 |
--------------------------------------------------------------------------------
/vertex-shader.js:
--------------------------------------------------------------------------------
1 | const shader = `#version 300 es
2 | precision highp float;
3 |
4 | // attributes.
5 | in vec3 position;
6 |
7 | // uniforms for vertex transformation.
8 | uniform mat4 projectionMatrix;
9 | uniform mat4 modelViewMatrix;
10 | uniform mat3 normalMatrix;
11 |
12 | // uniforms for the effect.
13 | uniform float SEGMENTS;
14 | uniform float SIDES;
15 | uniform float time;
16 | uniform float index;
17 |
18 | // varyings.
19 | out vec3 pos;
20 | out vec3 normal;
21 |
22 | const float PI = 3.1415926535897932384626433832795;
23 | const float TAU = 2. * PI;
24 |
25 | // creates a quaternion out of an axis vector and a rotation angle.
26 | vec4 quat(vec3 axis, float angle) {
27 | float halfAngle = angle / 2.;
28 | float s = sin( halfAngle );
29 |
30 | vec4 q = vec4(axis.x * s, axis.y * s, axis.z * s, cos( halfAngle ));
31 | return q;
32 | }
33 |
34 | // applies a quaternion q to a vec3 v.
35 | vec3 applyQuat( vec4 q, vec3 v ){
36 | return v + 2.0*cross(cross(v, q.xyz ) + q.w*v, q.xyz);
37 | }
38 |
39 | // returns the base point to generate a ring around.
40 | vec3 getBasePoint(float alpha) {
41 | float r = 17.;
42 | vec3 p = vec3(r * cos(alpha), 0., r * sin(alpha));
43 | vec3 dir = vec3(cos(alpha+PI/2.), 0., sin(alpha+PI/2.));
44 |
45 | float a = 2.*alpha;
46 | a += index/9. * TAU;
47 | p += applyQuat(quat(dir, a), vec3(0., 6., 0.));
48 | p.y += sin(alpha * 3.) * 4.;
49 | return p;
50 | }
51 |
52 | void main() {
53 | // get the base point, and calculate the orientation of the ring dir.
54 | float alpha = TAU * position.x / SEGMENTS;
55 | vec3 base = getBasePoint(alpha);
56 | vec3 prevBase = getBasePoint(alpha - TAU / SEGMENTS);
57 | vec3 dir = normalize(base - prevBase);
58 |
59 | // calculate the radius based on the effect.
60 | float beta = TAU * position.y / SIDES;
61 | float animStep = mod(3.*position.x + time, SEGMENTS) / SEGMENTS;
62 | float tubeRadius = max(0., pow(sin(alpha * 3. + (1. - time) * TAU) + 1.2, 2.)) * 0.3;
63 |
64 | // distribute each side of the ring around the base point.
65 | vec3 tubeDir = tubeRadius * vec3(0., 1., 0.);
66 | tubeDir = applyQuat(quat(dir, beta), tubeDir);
67 | vec3 newPosition = base + tubeDir;
68 |
69 | // the normal is the direction we pulled the vertex.
70 | normal = normalMatrix * normalize(tubeDir);
71 |
72 | // project the position.
73 | vec4 mvp = modelViewMatrix * vec4(newPosition, 1.);
74 | pos = mvp.xyz;
75 | gl_Position = projectionMatrix * mvp;
76 | }
77 | `;
78 |
79 | export { shader };
80 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | import {
2 | Scene,
3 | TextureLoader,
4 | Vector3,
5 | Mesh,
6 | WebGLRenderer,
7 | PerspectiveCamera,
8 | RawShaderMaterial,
9 | BufferAttribute,
10 | BufferGeometry,
11 | } from "./three.module.js";
12 | import { OrbitControls } from "./OrbitControls.js";
13 | import { shader as vertexShader } from "./vertex-shader.js";
14 | import { shader as fragmentShader } from "./fragment-shader.js";
15 |
16 | // Create renderer, camera, attach events, controls, etc.
17 | const renderer = new WebGLRenderer({ antialias: true });
18 | renderer.setSize(1, 1);
19 | document.body.append(renderer.domElement);
20 |
21 | const camera = new PerspectiveCamera(75, 1, 0.1, 150);
22 | camera.position.set(0, 20, 30).multiplyScalar(1.05);
23 | camera.lookAt(new Vector3(0, 0, 10));
24 |
25 | const controls = new OrbitControls(camera, renderer.domElement);
26 |
27 | const scene = new Scene();
28 |
29 | function onResize() {
30 | renderer.setSize(window.innerWidth, window.innerHeight);
31 | camera.aspect = window.innerWidth / window.innerHeight;
32 | camera.updateProjectionMatrix();
33 | }
34 |
35 | onResize();
36 |
37 | window.addEventListener("resize", onResize);
38 |
39 | // We generate a geometry that will hold information for the vertex shader
40 | // to generate the shape we want. It'll be a closed circle of SEGMENTS segments,
41 | // and each segment a ring of SIDES sides.
42 |
43 | const SIDES = 20;
44 | const SEGMENTS = 200;
45 | const geometry = new BufferGeometry();
46 |
47 | const indices = [];
48 | const vertices = new Float32Array(SIDES * SEGMENTS * 3);
49 |
50 | // We assign the values we need, instead of (x, y, z) values:
51 | // x is the segment number [0, SEGMENTS-1]
52 | // y is the side number [0, SIDES-1]
53 | // z is not used but we leave it empty.
54 | // We could use only a vec2, but BufferGeometry.computeBoundingSphere doesn't like it.
55 | let ptr = 0;
56 | for (let segment = 0; segment < SEGMENTS; segment++) {
57 | for (let side = 0; side < SIDES; side++) {
58 | vertices[ptr] = segment;
59 | vertices[ptr + 1] = side;
60 | vertices[ptr + 2] = 0;
61 | ptr += 3;
62 | }
63 | }
64 |
65 | // We generate the indices for each triangle.
66 | const MAX = SEGMENTS * SIDES;
67 | for (let segment = 0; segment < SEGMENTS + 1; segment++) {
68 | for (let f = 0; f < SIDES; f++) {
69 | const a = (segment * SIDES + ((f + 1) % SIDES)) % MAX;
70 | const b = (segment * SIDES + f) % MAX;
71 | const c = (segment * SIDES + f + SIDES) % MAX;
72 | const d = (segment * SIDES + ((f + 1) % SIDES) + SIDES) % MAX;
73 |
74 | indices.push(a, b, d);
75 | indices.push(b, c, d);
76 | }
77 | }
78 | geometry.setIndex(indices);
79 | geometry.setAttribute("position", new BufferAttribute(vertices, 3));
80 |
81 | // Load textures.
82 | const loader = new TextureLoader();
83 | const matcap1 = loader.load("matcap1.jpg");
84 | const matcap2 = loader.load("matcap2.png");
85 | const matcap3 = loader.load("matcap3.png");
86 |
87 | const TAU = 2 * Math.PI;
88 |
89 | // Generate a few meshes, rotated around the y axis.
90 | // We generate a new material for each because they have different matcaps,
91 | // and they have different index values for the animation.
92 | // It's still just a few draw calls, several orders of magnitude fewer than originally.
93 | const meshes = [];
94 | const PARTS = 9;
95 | for (let i = 0; i < PARTS; i++) {
96 | const geoMat = new RawShaderMaterial({
97 | uniforms: {
98 | SEGMENTS: { value: SEGMENTS },
99 | SIDES: { value: SIDES },
100 | matCapMap: {
101 | value: i % 3 === 0 ? matcap1 : i % 3 === 1 ? matcap2 : matcap3,
102 | },
103 | time: { value: 0 },
104 | index: { value: i },
105 | },
106 | vertexShader,
107 | fragmentShader,
108 | // wireframe: true,
109 | });
110 | const angle = (i * TAU) / PARTS;
111 | const t = new Mesh(geometry, geoMat);
112 | scene.add(t);
113 | meshes.push(t);
114 | }
115 |
116 | // Render. Assigns the time to each material every frame and draws.
117 | function render() {
118 | meshes.forEach((m) => {
119 | m.material.uniforms.time.value = 0.0005 * performance.now();
120 | });
121 | renderer.setAnimationLoop(render);
122 | renderer.render(scene, camera);
123 | }
124 |
125 | // Start rendering.
126 | render();
127 |
--------------------------------------------------------------------------------
/OrbitControls.js:
--------------------------------------------------------------------------------
1 | import {
2 | EventDispatcher,
3 | MOUSE,
4 | Quaternion,
5 | Spherical,
6 | TOUCH,
7 | Vector2,
8 | Vector3,
9 | } from "./three.module.js";
10 |
11 | // This set of controls performs orbiting, dollying (zooming), and panning.
12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
13 | //
14 | // Orbit - left mouse / touch: one-finger move
15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
17 |
18 | var OrbitControls = function (object, domElement) {
19 | if (domElement === undefined)
20 | console.warn(
21 | 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.'
22 | );
23 | if (domElement === document)
24 | console.error(
25 | 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'
26 | );
27 |
28 | this.object = object;
29 | this.domElement = domElement;
30 |
31 | // Set to false to disable this control
32 | this.enabled = true;
33 |
34 | // "target" sets the location of focus, where the object orbits around
35 | this.target = new Vector3();
36 |
37 | // How far you can dolly in and out ( PerspectiveCamera only )
38 | this.minDistance = 0;
39 | this.maxDistance = Infinity;
40 |
41 | // How far you can zoom in and out ( OrthographicCamera only )
42 | this.minZoom = 0;
43 | this.maxZoom = Infinity;
44 |
45 | // How far you can orbit vertically, upper and lower limits.
46 | // Range is 0 to Math.PI radians.
47 | this.minPolarAngle = 0; // radians
48 | this.maxPolarAngle = Math.PI; // radians
49 |
50 | // How far you can orbit horizontally, upper and lower limits.
51 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
52 | this.minAzimuthAngle = -Infinity; // radians
53 | this.maxAzimuthAngle = Infinity; // radians
54 |
55 | // Set to true to enable damping (inertia)
56 | // If damping is enabled, you must call controls.update() in your animation loop
57 | this.enableDamping = false;
58 | this.dampingFactor = 0.05;
59 |
60 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
61 | // Set to false to disable zooming
62 | this.enableZoom = true;
63 | this.zoomSpeed = 1.0;
64 |
65 | // Set to false to disable rotating
66 | this.enableRotate = true;
67 | this.rotateSpeed = 1.0;
68 |
69 | // Set to false to disable panning
70 | this.enablePan = true;
71 | this.panSpeed = 1.0;
72 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
73 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push
74 |
75 | // Set to true to automatically rotate around the target
76 | // If auto-rotate is enabled, you must call controls.update() in your animation loop
77 | this.autoRotate = false;
78 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
79 |
80 | // Set to false to disable use of the keys
81 | this.enableKeys = true;
82 |
83 | // The four arrow keys
84 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
85 |
86 | // Mouse buttons
87 | this.mouseButtons = {
88 | LEFT: MOUSE.ROTATE,
89 | MIDDLE: MOUSE.DOLLY,
90 | RIGHT: MOUSE.PAN,
91 | };
92 |
93 | // Touch fingers
94 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
95 |
96 | // for reset
97 | this.target0 = this.target.clone();
98 | this.position0 = this.object.position.clone();
99 | this.zoom0 = this.object.zoom;
100 |
101 | //
102 | // public methods
103 | //
104 |
105 | this.getPolarAngle = function () {
106 | return spherical.phi;
107 | };
108 |
109 | this.getAzimuthalAngle = function () {
110 | return spherical.theta;
111 | };
112 |
113 | this.saveState = function () {
114 | scope.target0.copy(scope.target);
115 | scope.position0.copy(scope.object.position);
116 | scope.zoom0 = scope.object.zoom;
117 | };
118 |
119 | this.reset = function () {
120 | scope.target.copy(scope.target0);
121 | scope.object.position.copy(scope.position0);
122 | scope.object.zoom = scope.zoom0;
123 |
124 | scope.object.updateProjectionMatrix();
125 | scope.dispatchEvent(changeEvent);
126 |
127 | scope.update();
128 |
129 | state = STATE.NONE;
130 | };
131 |
132 | // this method is exposed, but perhaps it would be better if we can make it private...
133 | this.update = (function () {
134 | var offset = new Vector3();
135 |
136 | // so camera.up is the orbit axis
137 | var quat = new Quaternion().setFromUnitVectors(
138 | object.up,
139 | new Vector3(0, 1, 0)
140 | );
141 | var quatInverse = quat.clone().invert();
142 |
143 | var lastPosition = new Vector3();
144 | var lastQuaternion = new Quaternion();
145 |
146 | var twoPI = 2 * Math.PI;
147 |
148 | return function update() {
149 | var position = scope.object.position;
150 |
151 | offset.copy(position).sub(scope.target);
152 |
153 | // rotate offset to "y-axis-is-up" space
154 | offset.applyQuaternion(quat);
155 |
156 | // angle from z-axis around y-axis
157 | spherical.setFromVector3(offset);
158 |
159 | if (scope.autoRotate && state === STATE.NONE) {
160 | rotateLeft(getAutoRotationAngle());
161 | }
162 |
163 | if (scope.enableDamping) {
164 | spherical.theta += sphericalDelta.theta * scope.dampingFactor;
165 | spherical.phi += sphericalDelta.phi * scope.dampingFactor;
166 | } else {
167 | spherical.theta += sphericalDelta.theta;
168 | spherical.phi += sphericalDelta.phi;
169 | }
170 |
171 | // restrict theta to be between desired limits
172 |
173 | var min = scope.minAzimuthAngle;
174 | var max = scope.maxAzimuthAngle;
175 |
176 | if (isFinite(min) && isFinite(max)) {
177 | if (min < -Math.PI) min += twoPI;
178 | else if (min > Math.PI) min -= twoPI;
179 |
180 | if (max < -Math.PI) max += twoPI;
181 | else if (max > Math.PI) max -= twoPI;
182 |
183 | if (min <= max) {
184 | spherical.theta = Math.max(min, Math.min(max, spherical.theta));
185 | } else {
186 | spherical.theta =
187 | spherical.theta > (min + max) / 2
188 | ? Math.max(min, spherical.theta)
189 | : Math.min(max, spherical.theta);
190 | }
191 | }
192 |
193 | // restrict phi to be between desired limits
194 | spherical.phi = Math.max(
195 | scope.minPolarAngle,
196 | Math.min(scope.maxPolarAngle, spherical.phi)
197 | );
198 |
199 | spherical.makeSafe();
200 |
201 | spherical.radius *= scale;
202 |
203 | // restrict radius to be between desired limits
204 | spherical.radius = Math.max(
205 | scope.minDistance,
206 | Math.min(scope.maxDistance, spherical.radius)
207 | );
208 |
209 | // move target to panned location
210 |
211 | if (scope.enableDamping === true) {
212 | scope.target.addScaledVector(panOffset, scope.dampingFactor);
213 | } else {
214 | scope.target.add(panOffset);
215 | }
216 |
217 | offset.setFromSpherical(spherical);
218 |
219 | // rotate offset back to "camera-up-vector-is-up" space
220 | offset.applyQuaternion(quatInverse);
221 |
222 | position.copy(scope.target).add(offset);
223 |
224 | scope.object.lookAt(scope.target);
225 |
226 | if (scope.enableDamping === true) {
227 | sphericalDelta.theta *= 1 - scope.dampingFactor;
228 | sphericalDelta.phi *= 1 - scope.dampingFactor;
229 |
230 | panOffset.multiplyScalar(1 - scope.dampingFactor);
231 | } else {
232 | sphericalDelta.set(0, 0, 0);
233 |
234 | panOffset.set(0, 0, 0);
235 | }
236 |
237 | scale = 1;
238 |
239 | // update condition is:
240 | // min(camera displacement, camera rotation in radians)^2 > EPS
241 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8
242 |
243 | if (
244 | zoomChanged ||
245 | lastPosition.distanceToSquared(scope.object.position) > EPS ||
246 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS
247 | ) {
248 | scope.dispatchEvent(changeEvent);
249 |
250 | lastPosition.copy(scope.object.position);
251 | lastQuaternion.copy(scope.object.quaternion);
252 | zoomChanged = false;
253 |
254 | return true;
255 | }
256 |
257 | return false;
258 | };
259 | })();
260 |
261 | this.dispose = function () {
262 | scope.domElement.removeEventListener("contextmenu", onContextMenu, false);
263 |
264 | scope.domElement.removeEventListener("pointerdown", onPointerDown, false);
265 | scope.domElement.removeEventListener("wheel", onMouseWheel, false);
266 |
267 | scope.domElement.removeEventListener("touchstart", onTouchStart, false);
268 | scope.domElement.removeEventListener("touchend", onTouchEnd, false);
269 | scope.domElement.removeEventListener("touchmove", onTouchMove, false);
270 |
271 | scope.domElement.ownerDocument.removeEventListener(
272 | "pointermove",
273 | onPointerMove,
274 | false
275 | );
276 | scope.domElement.ownerDocument.removeEventListener(
277 | "pointerup",
278 | onPointerUp,
279 | false
280 | );
281 |
282 | scope.domElement.removeEventListener("keydown", onKeyDown, false);
283 |
284 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
285 | };
286 |
287 | //
288 | // internals
289 | //
290 |
291 | var scope = this;
292 |
293 | var changeEvent = { type: "change" };
294 | var startEvent = { type: "start" };
295 | var endEvent = { type: "end" };
296 |
297 | var STATE = {
298 | NONE: -1,
299 | ROTATE: 0,
300 | DOLLY: 1,
301 | PAN: 2,
302 | TOUCH_ROTATE: 3,
303 | TOUCH_PAN: 4,
304 | TOUCH_DOLLY_PAN: 5,
305 | TOUCH_DOLLY_ROTATE: 6,
306 | };
307 |
308 | var state = STATE.NONE;
309 |
310 | var EPS = 0.000001;
311 |
312 | // current position in spherical coordinates
313 | var spherical = new Spherical();
314 | var sphericalDelta = new Spherical();
315 |
316 | var scale = 1;
317 | var panOffset = new Vector3();
318 | var zoomChanged = false;
319 |
320 | var rotateStart = new Vector2();
321 | var rotateEnd = new Vector2();
322 | var rotateDelta = new Vector2();
323 |
324 | var panStart = new Vector2();
325 | var panEnd = new Vector2();
326 | var panDelta = new Vector2();
327 |
328 | var dollyStart = new Vector2();
329 | var dollyEnd = new Vector2();
330 | var dollyDelta = new Vector2();
331 |
332 | function getAutoRotationAngle() {
333 | return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed;
334 | }
335 |
336 | function getZoomScale() {
337 | return Math.pow(0.95, scope.zoomSpeed);
338 | }
339 |
340 | function rotateLeft(angle) {
341 | sphericalDelta.theta -= angle;
342 | }
343 |
344 | function rotateUp(angle) {
345 | sphericalDelta.phi -= angle;
346 | }
347 |
348 | var panLeft = (function () {
349 | var v = new Vector3();
350 |
351 | return function panLeft(distance, objectMatrix) {
352 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix
353 | v.multiplyScalar(-distance);
354 |
355 | panOffset.add(v);
356 | };
357 | })();
358 |
359 | var panUp = (function () {
360 | var v = new Vector3();
361 |
362 | return function panUp(distance, objectMatrix) {
363 | if (scope.screenSpacePanning === true) {
364 | v.setFromMatrixColumn(objectMatrix, 1);
365 | } else {
366 | v.setFromMatrixColumn(objectMatrix, 0);
367 | v.crossVectors(scope.object.up, v);
368 | }
369 |
370 | v.multiplyScalar(distance);
371 |
372 | panOffset.add(v);
373 | };
374 | })();
375 |
376 | // deltaX and deltaY are in pixels; right and down are positive
377 | var pan = (function () {
378 | var offset = new Vector3();
379 |
380 | return function pan(deltaX, deltaY) {
381 | var element = scope.domElement;
382 |
383 | if (scope.object.isPerspectiveCamera) {
384 | // perspective
385 | var position = scope.object.position;
386 | offset.copy(position).sub(scope.target);
387 | var targetDistance = offset.length();
388 |
389 | // half of the fov is center to top of screen
390 | targetDistance *= Math.tan(((scope.object.fov / 2) * Math.PI) / 180.0);
391 |
392 | // we use only clientHeight here so aspect ratio does not distort speed
393 | panLeft(
394 | (2 * deltaX * targetDistance) / element.clientHeight,
395 | scope.object.matrix
396 | );
397 | panUp(
398 | (2 * deltaY * targetDistance) / element.clientHeight,
399 | scope.object.matrix
400 | );
401 | } else if (scope.object.isOrthographicCamera) {
402 | // orthographic
403 | panLeft(
404 | (deltaX * (scope.object.right - scope.object.left)) /
405 | scope.object.zoom /
406 | element.clientWidth,
407 | scope.object.matrix
408 | );
409 | panUp(
410 | (deltaY * (scope.object.top - scope.object.bottom)) /
411 | scope.object.zoom /
412 | element.clientHeight,
413 | scope.object.matrix
414 | );
415 | } else {
416 | // camera neither orthographic nor perspective
417 | console.warn(
418 | "WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."
419 | );
420 | scope.enablePan = false;
421 | }
422 | };
423 | })();
424 |
425 | function dollyOut(dollyScale) {
426 | if (scope.object.isPerspectiveCamera) {
427 | scale /= dollyScale;
428 | } else if (scope.object.isOrthographicCamera) {
429 | scope.object.zoom = Math.max(
430 | scope.minZoom,
431 | Math.min(scope.maxZoom, scope.object.zoom * dollyScale)
432 | );
433 | scope.object.updateProjectionMatrix();
434 | zoomChanged = true;
435 | } else {
436 | console.warn(
437 | "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."
438 | );
439 | scope.enableZoom = false;
440 | }
441 | }
442 |
443 | function dollyIn(dollyScale) {
444 | if (scope.object.isPerspectiveCamera) {
445 | scale *= dollyScale;
446 | } else if (scope.object.isOrthographicCamera) {
447 | scope.object.zoom = Math.max(
448 | scope.minZoom,
449 | Math.min(scope.maxZoom, scope.object.zoom / dollyScale)
450 | );
451 | scope.object.updateProjectionMatrix();
452 | zoomChanged = true;
453 | } else {
454 | console.warn(
455 | "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."
456 | );
457 | scope.enableZoom = false;
458 | }
459 | }
460 |
461 | //
462 | // event callbacks - update the object state
463 | //
464 |
465 | function handleMouseDownRotate(event) {
466 | rotateStart.set(event.clientX, event.clientY);
467 | }
468 |
469 | function handleMouseDownDolly(event) {
470 | dollyStart.set(event.clientX, event.clientY);
471 | }
472 |
473 | function handleMouseDownPan(event) {
474 | panStart.set(event.clientX, event.clientY);
475 | }
476 |
477 | function handleMouseMoveRotate(event) {
478 | rotateEnd.set(event.clientX, event.clientY);
479 |
480 | rotateDelta
481 | .subVectors(rotateEnd, rotateStart)
482 | .multiplyScalar(scope.rotateSpeed);
483 |
484 | var element = scope.domElement;
485 |
486 | rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height
487 |
488 | rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight);
489 |
490 | rotateStart.copy(rotateEnd);
491 |
492 | scope.update();
493 | }
494 |
495 | function handleMouseMoveDolly(event) {
496 | dollyEnd.set(event.clientX, event.clientY);
497 |
498 | dollyDelta.subVectors(dollyEnd, dollyStart);
499 |
500 | if (dollyDelta.y > 0) {
501 | dollyOut(getZoomScale());
502 | } else if (dollyDelta.y < 0) {
503 | dollyIn(getZoomScale());
504 | }
505 |
506 | dollyStart.copy(dollyEnd);
507 |
508 | scope.update();
509 | }
510 |
511 | function handleMouseMovePan(event) {
512 | panEnd.set(event.clientX, event.clientY);
513 |
514 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed);
515 |
516 | pan(panDelta.x, panDelta.y);
517 |
518 | panStart.copy(panEnd);
519 |
520 | scope.update();
521 | }
522 |
523 | function handleMouseUp(/*event*/) {
524 | // no-op
525 | }
526 |
527 | function handleMouseWheel(event) {
528 | if (event.deltaY < 0) {
529 | dollyIn(getZoomScale());
530 | } else if (event.deltaY > 0) {
531 | dollyOut(getZoomScale());
532 | }
533 |
534 | scope.update();
535 | }
536 |
537 | function handleKeyDown(event) {
538 | var needsUpdate = false;
539 |
540 | switch (event.keyCode) {
541 | case scope.keys.UP:
542 | pan(0, scope.keyPanSpeed);
543 | needsUpdate = true;
544 | break;
545 |
546 | case scope.keys.BOTTOM:
547 | pan(0, -scope.keyPanSpeed);
548 | needsUpdate = true;
549 | break;
550 |
551 | case scope.keys.LEFT:
552 | pan(scope.keyPanSpeed, 0);
553 | needsUpdate = true;
554 | break;
555 |
556 | case scope.keys.RIGHT:
557 | pan(-scope.keyPanSpeed, 0);
558 | needsUpdate = true;
559 | break;
560 | }
561 |
562 | if (needsUpdate) {
563 | // prevent the browser from scrolling on cursor keys
564 | event.preventDefault();
565 |
566 | scope.update();
567 | }
568 | }
569 |
570 | function handleTouchStartRotate(event) {
571 | if (event.touches.length == 1) {
572 | rotateStart.set(event.touches[0].pageX, event.touches[0].pageY);
573 | } else {
574 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
575 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
576 |
577 | rotateStart.set(x, y);
578 | }
579 | }
580 |
581 | function handleTouchStartPan(event) {
582 | if (event.touches.length == 1) {
583 | panStart.set(event.touches[0].pageX, event.touches[0].pageY);
584 | } else {
585 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
586 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
587 |
588 | panStart.set(x, y);
589 | }
590 | }
591 |
592 | function handleTouchStartDolly(event) {
593 | var dx = event.touches[0].pageX - event.touches[1].pageX;
594 | var dy = event.touches[0].pageY - event.touches[1].pageY;
595 |
596 | var distance = Math.sqrt(dx * dx + dy * dy);
597 |
598 | dollyStart.set(0, distance);
599 | }
600 |
601 | function handleTouchStartDollyPan(event) {
602 | if (scope.enableZoom) handleTouchStartDolly(event);
603 |
604 | if (scope.enablePan) handleTouchStartPan(event);
605 | }
606 |
607 | function handleTouchStartDollyRotate(event) {
608 | if (scope.enableZoom) handleTouchStartDolly(event);
609 |
610 | if (scope.enableRotate) handleTouchStartRotate(event);
611 | }
612 |
613 | function handleTouchMoveRotate(event) {
614 | if (event.touches.length == 1) {
615 | rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
616 | } else {
617 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
618 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
619 |
620 | rotateEnd.set(x, y);
621 | }
622 |
623 | rotateDelta
624 | .subVectors(rotateEnd, rotateStart)
625 | .multiplyScalar(scope.rotateSpeed);
626 |
627 | var element = scope.domElement;
628 |
629 | rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height
630 |
631 | rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight);
632 |
633 | rotateStart.copy(rotateEnd);
634 | }
635 |
636 | function handleTouchMovePan(event) {
637 | if (event.touches.length == 1) {
638 | panEnd.set(event.touches[0].pageX, event.touches[0].pageY);
639 | } else {
640 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
641 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
642 |
643 | panEnd.set(x, y);
644 | }
645 |
646 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed);
647 |
648 | pan(panDelta.x, panDelta.y);
649 |
650 | panStart.copy(panEnd);
651 | }
652 |
653 | function handleTouchMoveDolly(event) {
654 | var dx = event.touches[0].pageX - event.touches[1].pageX;
655 | var dy = event.touches[0].pageY - event.touches[1].pageY;
656 |
657 | var distance = Math.sqrt(dx * dx + dy * dy);
658 |
659 | dollyEnd.set(0, distance);
660 |
661 | dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed));
662 |
663 | dollyOut(dollyDelta.y);
664 |
665 | dollyStart.copy(dollyEnd);
666 | }
667 |
668 | function handleTouchMoveDollyPan(event) {
669 | if (scope.enableZoom) handleTouchMoveDolly(event);
670 |
671 | if (scope.enablePan) handleTouchMovePan(event);
672 | }
673 |
674 | function handleTouchMoveDollyRotate(event) {
675 | if (scope.enableZoom) handleTouchMoveDolly(event);
676 |
677 | if (scope.enableRotate) handleTouchMoveRotate(event);
678 | }
679 |
680 | function handleTouchEnd(/*event*/) {
681 | // no-op
682 | }
683 |
684 | //
685 | // event handlers - FSM: listen for events and reset state
686 | //
687 |
688 | function onPointerDown(event) {
689 | if (scope.enabled === false) return;
690 |
691 | switch (event.pointerType) {
692 | case "mouse":
693 | case "pen":
694 | onMouseDown(event);
695 | break;
696 |
697 | // TODO touch
698 | }
699 | }
700 |
701 | function onPointerMove(event) {
702 | if (scope.enabled === false) return;
703 |
704 | switch (event.pointerType) {
705 | case "mouse":
706 | case "pen":
707 | onMouseMove(event);
708 | break;
709 |
710 | // TODO touch
711 | }
712 | }
713 |
714 | function onPointerUp(event) {
715 | switch (event.pointerType) {
716 | case "mouse":
717 | case "pen":
718 | onMouseUp(event);
719 | break;
720 |
721 | // TODO touch
722 | }
723 | }
724 |
725 | function onMouseDown(event) {
726 | // Prevent the browser from scrolling.
727 | event.preventDefault();
728 |
729 | // Manually set the focus since calling preventDefault above
730 | // prevents the browser from setting it automatically.
731 |
732 | scope.domElement.focus ? scope.domElement.focus() : window.focus();
733 |
734 | var mouseAction;
735 |
736 | switch (event.button) {
737 | case 0:
738 | mouseAction = scope.mouseButtons.LEFT;
739 | break;
740 |
741 | case 1:
742 | mouseAction = scope.mouseButtons.MIDDLE;
743 | break;
744 |
745 | case 2:
746 | mouseAction = scope.mouseButtons.RIGHT;
747 | break;
748 |
749 | default:
750 | mouseAction = -1;
751 | }
752 |
753 | switch (mouseAction) {
754 | case MOUSE.DOLLY:
755 | if (scope.enableZoom === false) return;
756 |
757 | handleMouseDownDolly(event);
758 |
759 | state = STATE.DOLLY;
760 |
761 | break;
762 |
763 | case MOUSE.ROTATE:
764 | if (event.ctrlKey || event.metaKey || event.shiftKey) {
765 | if (scope.enablePan === false) return;
766 |
767 | handleMouseDownPan(event);
768 |
769 | state = STATE.PAN;
770 | } else {
771 | if (scope.enableRotate === false) return;
772 |
773 | handleMouseDownRotate(event);
774 |
775 | state = STATE.ROTATE;
776 | }
777 |
778 | break;
779 |
780 | case MOUSE.PAN:
781 | if (event.ctrlKey || event.metaKey || event.shiftKey) {
782 | if (scope.enableRotate === false) return;
783 |
784 | handleMouseDownRotate(event);
785 |
786 | state = STATE.ROTATE;
787 | } else {
788 | if (scope.enablePan === false) return;
789 |
790 | handleMouseDownPan(event);
791 |
792 | state = STATE.PAN;
793 | }
794 |
795 | break;
796 |
797 | default:
798 | state = STATE.NONE;
799 | }
800 |
801 | if (state !== STATE.NONE) {
802 | scope.domElement.ownerDocument.addEventListener(
803 | "pointermove",
804 | onPointerMove,
805 | false
806 | );
807 | scope.domElement.ownerDocument.addEventListener(
808 | "pointerup",
809 | onPointerUp,
810 | false
811 | );
812 |
813 | scope.dispatchEvent(startEvent);
814 | }
815 | }
816 |
817 | function onMouseMove(event) {
818 | if (scope.enabled === false) return;
819 |
820 | event.preventDefault();
821 |
822 | switch (state) {
823 | case STATE.ROTATE:
824 | if (scope.enableRotate === false) return;
825 |
826 | handleMouseMoveRotate(event);
827 |
828 | break;
829 |
830 | case STATE.DOLLY:
831 | if (scope.enableZoom === false) return;
832 |
833 | handleMouseMoveDolly(event);
834 |
835 | break;
836 |
837 | case STATE.PAN:
838 | if (scope.enablePan === false) return;
839 |
840 | handleMouseMovePan(event);
841 |
842 | break;
843 | }
844 | }
845 |
846 | function onMouseUp(event) {
847 | scope.domElement.ownerDocument.removeEventListener(
848 | "pointermove",
849 | onPointerMove,
850 | false
851 | );
852 | scope.domElement.ownerDocument.removeEventListener(
853 | "pointerup",
854 | onPointerUp,
855 | false
856 | );
857 |
858 | if (scope.enabled === false) return;
859 |
860 | handleMouseUp(event);
861 |
862 | scope.dispatchEvent(endEvent);
863 |
864 | state = STATE.NONE;
865 | }
866 |
867 | function onMouseWheel(event) {
868 | if (
869 | scope.enabled === false ||
870 | scope.enableZoom === false ||
871 | (state !== STATE.NONE && state !== STATE.ROTATE)
872 | )
873 | return;
874 |
875 | event.preventDefault();
876 | event.stopPropagation();
877 |
878 | scope.dispatchEvent(startEvent);
879 |
880 | handleMouseWheel(event);
881 |
882 | scope.dispatchEvent(endEvent);
883 | }
884 |
885 | function onKeyDown(event) {
886 | if (
887 | scope.enabled === false ||
888 | scope.enableKeys === false ||
889 | scope.enablePan === false
890 | )
891 | return;
892 |
893 | handleKeyDown(event);
894 | }
895 |
896 | function onTouchStart(event) {
897 | if (scope.enabled === false) return;
898 |
899 | event.preventDefault(); // prevent scrolling
900 |
901 | switch (event.touches.length) {
902 | case 1:
903 | switch (scope.touches.ONE) {
904 | case TOUCH.ROTATE:
905 | if (scope.enableRotate === false) return;
906 |
907 | handleTouchStartRotate(event);
908 |
909 | state = STATE.TOUCH_ROTATE;
910 |
911 | break;
912 |
913 | case TOUCH.PAN:
914 | if (scope.enablePan === false) return;
915 |
916 | handleTouchStartPan(event);
917 |
918 | state = STATE.TOUCH_PAN;
919 |
920 | break;
921 |
922 | default:
923 | state = STATE.NONE;
924 | }
925 |
926 | break;
927 |
928 | case 2:
929 | switch (scope.touches.TWO) {
930 | case TOUCH.DOLLY_PAN:
931 | if (scope.enableZoom === false && scope.enablePan === false) return;
932 |
933 | handleTouchStartDollyPan(event);
934 |
935 | state = STATE.TOUCH_DOLLY_PAN;
936 |
937 | break;
938 |
939 | case TOUCH.DOLLY_ROTATE:
940 | if (scope.enableZoom === false && scope.enableRotate === false)
941 | return;
942 |
943 | handleTouchStartDollyRotate(event);
944 |
945 | state = STATE.TOUCH_DOLLY_ROTATE;
946 |
947 | break;
948 |
949 | default:
950 | state = STATE.NONE;
951 | }
952 |
953 | break;
954 |
955 | default:
956 | state = STATE.NONE;
957 | }
958 |
959 | if (state !== STATE.NONE) {
960 | scope.dispatchEvent(startEvent);
961 | }
962 | }
963 |
964 | function onTouchMove(event) {
965 | if (scope.enabled === false) return;
966 |
967 | event.preventDefault(); // prevent scrolling
968 | event.stopPropagation();
969 |
970 | switch (state) {
971 | case STATE.TOUCH_ROTATE:
972 | if (scope.enableRotate === false) return;
973 |
974 | handleTouchMoveRotate(event);
975 |
976 | scope.update();
977 |
978 | break;
979 |
980 | case STATE.TOUCH_PAN:
981 | if (scope.enablePan === false) return;
982 |
983 | handleTouchMovePan(event);
984 |
985 | scope.update();
986 |
987 | break;
988 |
989 | case STATE.TOUCH_DOLLY_PAN:
990 | if (scope.enableZoom === false && scope.enablePan === false) return;
991 |
992 | handleTouchMoveDollyPan(event);
993 |
994 | scope.update();
995 |
996 | break;
997 |
998 | case STATE.TOUCH_DOLLY_ROTATE:
999 | if (scope.enableZoom === false && scope.enableRotate === false) return;
1000 |
1001 | handleTouchMoveDollyRotate(event);
1002 |
1003 | scope.update();
1004 |
1005 | break;
1006 |
1007 | default:
1008 | state = STATE.NONE;
1009 | }
1010 | }
1011 |
1012 | function onTouchEnd(event) {
1013 | if (scope.enabled === false) return;
1014 |
1015 | handleTouchEnd(event);
1016 |
1017 | scope.dispatchEvent(endEvent);
1018 |
1019 | state = STATE.NONE;
1020 | }
1021 |
1022 | function onContextMenu(event) {
1023 | if (scope.enabled === false) return;
1024 |
1025 | event.preventDefault();
1026 | }
1027 |
1028 | //
1029 |
1030 | scope.domElement.addEventListener("contextmenu", onContextMenu, false);
1031 |
1032 | scope.domElement.addEventListener("pointerdown", onPointerDown, false);
1033 | scope.domElement.addEventListener("wheel", onMouseWheel, false);
1034 |
1035 | scope.domElement.addEventListener("touchstart", onTouchStart, false);
1036 | scope.domElement.addEventListener("touchend", onTouchEnd, false);
1037 | scope.domElement.addEventListener("touchmove", onTouchMove, false);
1038 |
1039 | scope.domElement.addEventListener("keydown", onKeyDown, false);
1040 |
1041 | // force an update at start
1042 |
1043 | this.update();
1044 | };
1045 |
1046 | OrbitControls.prototype = Object.create(EventDispatcher.prototype);
1047 | OrbitControls.prototype.constructor = OrbitControls;
1048 |
1049 | // This set of controls performs orbiting, dollying (zooming), and panning.
1050 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
1051 | // This is very similar to OrbitControls, another set of touch behavior
1052 | //
1053 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
1054 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
1055 | // Pan - left mouse, or arrow keys / touch: one-finger move
1056 |
1057 | var MapControls = function (object, domElement) {
1058 | OrbitControls.call(this, object, domElement);
1059 |
1060 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
1061 |
1062 | this.mouseButtons.LEFT = MOUSE.PAN;
1063 | this.mouseButtons.RIGHT = MOUSE.ROTATE;
1064 |
1065 | this.touches.ONE = TOUCH.PAN;
1066 | this.touches.TWO = TOUCH.DOLLY_ROTATE;
1067 | };
1068 |
1069 | MapControls.prototype = Object.create(EventDispatcher.prototype);
1070 | MapControls.prototype.constructor = MapControls;
1071 |
1072 | export { OrbitControls, MapControls };
1073 |
--------------------------------------------------------------------------------