├── .gitignore
├── 086-005a.py
├── README.md
├── crosshairs.png
├── dependencies
├── DDSLoader.js
├── MTLLoader.js
├── OBJLoader.js
├── OrbitControls.js
├── compatibility.js
├── jquery-3.1.0.min.js
├── jquery-ui.min.css
├── jquery-ui.min.js
├── jsfeat-min.js
├── jsfeat.js
├── socket.io-1.3.5.js
├── three.js
├── three.min.js
└── vr
│ ├── VRControls.js
│ ├── VREffect.js
│ ├── ViveController.js
│ └── WebVR.js
├── index.html
├── js
├── controls.js
├── fitness.js
├── main.js
├── optimize.js
└── webcam.js
├── node
├── nodeServer.js
├── stepper_angles.py
└── test.py
├── projectionAR.png
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | data/
2 | node/node_modules
3 | node_modules
4 |
5 | motionPlannerDebugFile.txt
6 | virtualmachineids.vmp
7 |
8 | *.pyc
9 |
--------------------------------------------------------------------------------
/086-005a.py:
--------------------------------------------------------------------------------
1 | from pygestalt import nodes
2 | from pygestalt import utilities
3 | from pygestalt.utilities import notice
4 | from pygestalt import functions as functions
5 | from pygestalt import packets as packets
6 | from pygestalt import core
7 | import time
8 | import math
9 |
10 | class virtualNode(nodes.baseStandardGestaltNode):
11 | def init(self, **kwargs):
12 | pass
13 |
14 | def initParameters(self):
15 | #ADC parameters
16 | self.ADCRefVoltage = 5.0 #voltage divider to ref pin is 5V -> 10K -> REF -> 5K -> GND
17 | self.motorSenseResistor = 0.1 #ohms
18 |
19 | #stepping parameters
20 | self.maxSteps = 255 #one byte
21 | self.clockFrequency = 18432000.0 #Hz
22 | self.timeBaseTicks = 921.0 #clock cycles, this works out to around 20KHz
23 | self.timeBasePeriod = self.timeBaseTicks / self.clockFrequency #seconds
24 | self.uStepsPerStep = 1048576 #uSteps
25 |
26 | #axes parameters
27 | self.numberOfAxes = 1 #three axis driver
28 |
29 | def initFunctions(self):
30 | pass
31 |
32 | def initPackets(self):
33 | self.getReferenceVoltageResponsePacket = packets.packet(template = [packets.pInteger('voltage',2)])
34 |
35 | self.spinRequestPacket = packets.packet(template = [packets.pInteger('majorSteps',1),
36 | packets.pInteger('directions',1),
37 | packets.pInteger('steps', 1),
38 | packets.pInteger('accel',1),
39 | packets.pInteger('accelSteps',1),
40 | packets.pInteger('decelSteps',1),
41 | packets.pInteger('sync', 1)]) #if >0, indicates that move is synchronous
42 |
43 | self.spinStatusPacket = packets.packet(template = [packets.pInteger('statusCode',1),
44 | packets.pInteger('currentKey',1),
45 | packets.pInteger('stepsRemaining',1),
46 | packets.pInteger('readPosition',1),
47 | packets.pInteger('writePosition',1)])
48 |
49 | self.setVelocityPacket = packets.packet(template = [packets.pInteger('velocity',2)])
50 |
51 |
52 | def initPorts(self):
53 | #get reference voltage
54 | self.bindPort(port = 20, outboundFunction = self.getReferenceVoltageRequest, inboundPacket = self.getReferenceVoltageResponsePacket)
55 |
56 | #enable drivers
57 | self.bindPort(port = 21, outboundFunction = self.enableRequest)
58 |
59 | #disable drivers
60 | self.bindPort(port = 22, outboundFunction = self.disableRequest)
61 |
62 | #move
63 | self.bindPort(port = 23, outboundFunction = self.spinRequest, outboundPacket = self.spinRequestPacket,
64 | inboundPacket = self.spinStatusPacket)
65 |
66 | #set velocity
67 | self.bindPort(port = 24, outboundFunction = self.setVelocityRequest, outboundPacket = self.setVelocityPacket)
68 |
69 | #spin status
70 | self.bindPort(port = 26, outboundFunction = self.spinStatusRequest, inboundPacket = self.spinStatusPacket)
71 |
72 | #sync
73 | self.bindPort(port = 30, outboundFunction = self.syncRequest)
74 |
75 |
76 | #----- API FUNCTIONS --------------------
77 | def setMotorCurrent(self, setCurrent):
78 | if setCurrent < 0 or setCurrent > 2.0:
79 | notice(self, "Motor current must be between 0 and 2.0 amps. " + str(setCurrent) + " was requested.")
80 | setCurrent = round(float(setCurrent), 1)
81 | while True:
82 | motorVoltage = self.getReferenceVoltage()
83 | motorCurrent = round(motorVoltage /(8.0*self.motorSenseResistor),2) #amps
84 | if round(motorCurrent,1) > setCurrent:
85 | notice(self, "Motor current: " + str(motorCurrent) + "A / " + "Desired: " + str(setCurrent) + "A. Turn trimmer CW.")
86 | elif round(motorCurrent,1) < setCurrent:
87 | notice(self, "Motor current: " + str(motorCurrent) + "A / " + "Desired: " + str(setCurrent) + "A. Turn trimmer CCW.")
88 | else:
89 | notice(self, "Motor current set to: " + str(motorCurrent) + "A")
90 | break;
91 | time.sleep(0.5)
92 |
93 |
94 | def getReferenceVoltage(self):
95 | ADCReading = self.getReferenceVoltageRequest()
96 | if ADCReading:
97 | return self.ADCRefVoltage * ADCReading / 1024.0
98 | else:
99 | return False
100 |
101 | def enableMotorsRequest(self):
102 | return self.enableRequest()
103 |
104 | def disableMotorsRequest(self):
105 | return self.disableRequest()
106 |
107 | #----- SERVICE ROUTINES -----------------
108 | class enableRequest(functions.serviceRoutine):
109 | class actionObject(core.actionObject):
110 | def init(self):
111 | self.setPacket({})
112 | self.commitAndRelease()
113 | self.waitForChannelAccess()
114 | if self.transmitPersistent():
115 | notice(self.virtualNode, "Stepper motor enabled.")
116 | return True
117 | else:
118 | notice(self.virtualNode, "Stepper motor could not be enabled.")
119 | return False
120 |
121 | class disableRequest(functions.serviceRoutine):
122 | class actionObject(core.actionObject):
123 | def init(self):
124 | self.setPacket({})
125 | self.commitAndRelease()
126 | self.waitForChannelAccess()
127 | if self.transmitPersistent():
128 | notice(self.virtualNode, "Stepper motor disabled.")
129 | return True
130 | else:
131 | notice(self.virtualNode, "Stepper motor could not be disabled.")
132 | return False
133 |
134 | class getReferenceVoltageRequest(functions.serviceRoutine):
135 | class actionObject(core.actionObject):
136 | def init(self):
137 | self.setPacket({})
138 | self.commitAndRelease()
139 | self.waitForChannelAccess()
140 | if self.transmitPersistent(): return self.getPacket()['voltage']
141 | else: return False
142 |
143 | class spinStatusRequest(functions.serviceRoutine):
144 | class actionObject(core.actionObject):
145 | def init(self):
146 | self.setPacket({})
147 | self.commitAndRelease()
148 | self.waitForChannelAccess()
149 | if self.transmitPersistent():
150 | return self.getPacket()
151 | else:
152 | notice(self.virtualNode, 'unable to get status from node.')
153 |
154 | class setVelocityRequest(functions.serviceRoutine):
155 | class actionObject(core.actionObject):
156 | def init(self, velocity):
157 | #velocity is in steps/sec
158 | velocity = int(round((velocity * self.virtualNode.uStepsPerStep * self.virtualNode.timeBasePeriod)/16.0,0)) #convert into 16*uSteps/timebase
159 | print velocity
160 | self.setPacket({'velocity': velocity})
161 | self.commitAndRelease()
162 | self.waitForChannelAccess()
163 | if self.transmitPersistent():
164 | return True
165 | else:
166 | notice(self.virtualNode, 'unable to set velocity.')
167 | return False
168 |
169 | class spinRequest(functions.serviceRoutine):
170 | class actionObject(core.actionObject):
171 | def init(self, axesSteps, accelSteps = 0, decelSteps = 0, accelRate = 0, external = False, sync = False, majorSteps = None):
172 | # axesSteps: a list containing the number of steps which each axis of the node should take in synchrony.
173 | # accelSteps: number of virtual major axis steps during which acceleration should occur.
174 | # deccelSteps: number of virtual major axis steps during which deceleration should occur.
175 | # accelRate: accel/decel rate in steps/sec^2
176 | # external: indicates whether the actionObject will be commited to its interface externally.
177 | # When False, the actionObject will self commit and release.
178 | # When True, the actionObject will prepare a packet but will need to be commited and release externally.
179 | # sync: indicates that this actionObject will be synchronized externally and that parameters might change. This will prevent it from spawning.
180 | # majorSteps: this parameter is only called internally when a request calls for too many steps for one actionObject and the actionObject needs to spawn.
181 | if type(axesSteps) != list and type(axesSteps) != tuple: axesSteps = [axesSteps]
182 | self.axesSteps = [int(axis) for axis in axesSteps] #list of steps to take in each axis e.g. [x, y, z]. May be provided as a tuple.
183 | self.accelSteps = accelSteps
184 | self.decelSteps = decelSteps
185 | self.accelRate = accelRate
186 | self.external = external
187 | self.sync = sync
188 | self.actionSequence = False #start out with no action sequence
189 | self.sequenceMajorSteps = None #if this actionObject becomes an actionSequence parent, this will store the list of major axes
190 |
191 | #calculate virtual major axis
192 | if majorSteps: #if provided externally, use that.
193 | self.majorSteps = majorSteps
194 | else: #generate majorSteps from provided axes.
195 | self.majorSteps = max([abs(axis) for axis in self.axesSteps])
196 |
197 | if self.majorSteps > self.virtualNode.maxSteps: #check if step request exceeds maximum length
198 | if not sync: #no other anticipated recalculations, so go ahead and generate actionSequence
199 | #need to generate an actionSequence with multiple new actionObjects
200 | self.actionSequence = self.actionSequenceGen()
201 | return self.actionSequence
202 | else: #the majorSteps has not yet been synchronized, so cannot generate an action sequence yet.
203 | return self
204 |
205 | else: #step request is executable by this action object. Either initial request met this criteria, or this actionObject was initialized by actionSequence().
206 | #calculate directions
207 | directions = [1 if axis>0 else 0 for axis in self.axesSteps]
208 | directionMultiplexer = [2**b for b in range(self.virtualNode.numberOfAxes-1, -1, -1)] #generates [2^n, 2^n-1,...,2^0]
209 | self.directionByte = sum(map(lambda x,y: x*y, directionMultiplexer, directions)) #directionByte is each term of directionMultiplexer multiplied by the directions and then summed.
210 |
211 | if external or sync: #actionObject is being managed by an external function and shouldn't auto-commit
212 | return self
213 | else:
214 | #set packet
215 | accelRate = self.tbAccelRate(self.accelRate) #convert accel rate into timebase
216 | self.setPacket({'majorSteps':self.majorSteps, 'directions':self.directionByte, 'steps':abs(self.axesSteps[0]), 'accel':accelRate,
217 | 'accelSteps':accelSteps, 'decelSteps':decelSteps})
218 |
219 | self.commitAndRelease()
220 | self.waitForChannelAccess()
221 | moveQueued = False
222 | while not moveQueued:
223 | if self.transmitPersistent():
224 | responsePacket = self.getPacket()
225 | moveQueued = bool(responsePacket['statusCode']) #False if move was not queued
226 | time.sleep(0.02)
227 | else:
228 | notice(self.virtualNode, "got no response to spin request")
229 | return False
230 | return responsePacket
231 |
232 | def syncPush(self):
233 | '''Stores this actionObject's sync tokens to the provided syncToken.'''
234 | if self.sync:
235 | self.sync.push('majorSteps', self.majorSteps)
236 |
237 | def syncPull(self):
238 | '''Actually does the synchronization based on the tokens stored in self.sync.'''
239 | if self.sync:
240 | self.majorSteps = int(max(self.sync.pull('majorSteps')))
241 | if self.majorSteps > self.virtualNode.maxSteps:
242 | self.actionSequence = self.actionSequenceGen()
243 | return self.actionSequence
244 | else:
245 | return self
246 |
247 |
248 | def channelAccess(self):
249 | '''This gets called when channel access is granted.
250 | To prevent double-transmission, only transmit if external is True.'''
251 | if self.external:
252 | #set packet
253 | accelRate = self.tbAccelRate(self.accelRate) #convert accel rate into timebase
254 | self.setPacket({'majorSteps':self.majorSteps, 'directions':self.directionByte, 'steps':abs(self.axesSteps[0]), 'accel':accelRate,
255 | 'accelSteps':self.accelSteps, 'decelSteps':self.decelSteps, 'sync':int(bool(self.sync))})
256 | #since this node was instantiated under external control, it did not auto-transmit.
257 | moveQueued = False
258 | while not moveQueued:
259 | if self.transmitPersistent():
260 | responsePacket = self.getPacket()
261 | print responsePacket
262 | moveQueued = bool(responsePacket['statusCode']) #False if move was not queued, meaning buffer is full
263 | if not moveQueued: time.sleep(0.02) #wait before retransmitting
264 | else:
265 | notice(self.virtualNode, "got no response to spin request")
266 |
267 |
268 | def splitNumber(self, value, segments):
269 | '''Returns a list of integers (length segments) whose sum is value.
270 |
271 | This algorithm should produce similar results to the bresenham algorithm without needing to iterate.'''
272 | #e.g. splitNumber(800, 7)
273 | integers = [int(value/segments) for i in range(segments)] #[114, 114, 114, 114, 114, 114, 114]
274 | remainder = value - sum(integers) #2
275 | distributedRemainder = remainder/float(segments) #0.285...
276 | remainderSequence = [0] + [distributedRemainder*i - round(distributedRemainder*i,0) for i in range(1, 1+segments)] #[0.285, -0.428, -0.142, 0.142, 0.428, -0.285, 0.0]
277 | extraSteps = map(lambda x,y: 1 if x>y else 0, remainderSequence[:-1], remainderSequence[1:]) #[0, 1, 0, 0, 0, 1, 0] passes negative steps
278 | return map(lambda x,y: x+y, integers, extraSteps) #[114, 115, 114, 114, 114, 115, 114]
279 |
280 | def fillFront(self, fillNumber, binSizes):
281 | '''Fills a series of bins whose size are specified by fillList, starting with the first bin.'''
282 | filledBins = [0 for bin in binSizes]
283 | for binIndex, binSize in enumerate(binSizes):
284 | if fillNumber > binSize:
285 | filledBins[binIndex] = binSize
286 | fillNumber -= binSize
287 | else:
288 | filledBins[binIndex] = fillNumber
289 | fillNumber = 0
290 | return filledBins
291 |
292 | def fillBack(self, fillNumber, binSizes):
293 | '''Fills a series of bins whose size are specified by fillList, starting with the last bin.'''
294 | reversedBinSizes = list(binSizes) #makes a copy
295 | reversedBinSizes.reverse()
296 | filledBins = self.fillFront(fillNumber, reversedBinSizes)
297 | filledBins.reverse()
298 | return filledBins
299 |
300 |
301 | def actionSequenceGen(self): #need to generate an action sequence to accommodate the request
302 | '''Returns an actionSequence which contains a sequential set of actionObjects.'''
303 | segments = int(math.ceil(self.majorSteps/float(self.virtualNode.maxSteps))) #number of actionObjects needed to address requested number of steps
304 | axesSteps = zip(*[self.splitNumber(axisSteps, segments) for axisSteps in self.axesSteps]) #a list of axesSteps lists which are divided into the necessary number of segments
305 | majorSteps = self.splitNumber(self.majorSteps, segments) #a list of majorSteps which are divided into the necessary number of segments
306 | self.sequenceMajorSteps = majorSteps #store at the actionObject level for when parameters need to be updated later.
307 | accelSteps = self.fillFront(self.accelSteps, majorSteps) #a list of accelSteps which fills bins of maximum size majorSteps starting from the front
308 | decelSteps = self.fillBack(self.decelSteps, majorSteps) #a list of decelSteps which fills bins of maximum size majorSteps starting from the back
309 | accelRate = [self.accelRate for segment in range(segments)]
310 | external = [self.external for segment in range(segments)]
311 | sync = [self.sync for segment in range(segments)]
312 |
313 | return self.__actionSequence__(axesSteps, accelSteps, decelSteps, accelRate, external, sync, majorSteps)
314 |
315 | def update(self, accelSteps = 0, decelSteps = 0, accelRate = 0):
316 | '''Updates the acceleration and deceleration parameters for the spin actionObject.'''
317 |
318 | if self.actionSequence: #this actionObject is the parent of an actionSequence. Update the whole sequence
319 | accelSteps = self.fillFront(accelSteps, self.sequenceMajorSteps)
320 | decelSteps = self.fillBack(decelSteps, self.sequenceMajorSteps)
321 | accelRates = [accelRate for majorSteps in self.sequenceMajorSteps]
322 |
323 | for actionObject, args in zip(self.actionSequence.actionObjects, zip(accelSteps, decelSteps, accelRates)):
324 | actionObject.update(*args)
325 | else:
326 | self.accelSteps = accelSteps
327 | self.decelSteps = decelSteps
328 | self.accelRate = accelRate
329 |
330 |
331 | def tbAccelRate(self, accelRate):
332 | '''Converts acceleration from steps/sec^2 to uSteps/timeBase^2.'''
333 | return int(round(accelRate * self.virtualNode.uStepsPerStep * self.virtualNode.timeBasePeriod * self.virtualNode.timeBasePeriod,0)) #uSteps/timePeriod^2
334 |
335 | class syncRequest(functions.serviceRoutine):
336 | class actionObject(core.actionObject):
337 | def init(self):
338 | self.mode = 'multicast'
339 | def channelAccess(self):
340 | self.transmit()
341 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Projection Mapping Alignment
2 |
3 |
4 |
5 |
6 | A WebGL app that breaks out many of the threejs camera parameters to fine tune projection mapping alignment to a physical model. Sends G-code to a rotary stage (via a node server) to rotate the physical and virtual models together. I began to also incorporate some closed loop optimization into the alignment process via a webcam - though that work was never quite finished. Part of a collaboration with Jeff Koons studio. Live demo here.
--------------------------------------------------------------------------------
/crosshairs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amandaghassaei/ProjectionMappingAlignment/1e617c63ff7527b509eee8a75d2ec52e67865cae/crosshairs.png
--------------------------------------------------------------------------------
/dependencies/DDSLoader.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @author mrdoob / http://mrdoob.com/
3 | */
4 |
5 | THREE.DDSLoader = function () {
6 |
7 | this._parser = THREE.DDSLoader.parse;
8 |
9 | };
10 |
11 | THREE.DDSLoader.prototype = Object.create( THREE.CompressedTextureLoader.prototype );
12 | THREE.DDSLoader.prototype.constructor = THREE.DDSLoader;
13 |
14 | THREE.DDSLoader.parse = function ( buffer, loadMipmaps ) {
15 |
16 | var dds = { mipmaps: [], width: 0, height: 0, format: null, mipmapCount: 1 };
17 |
18 | // Adapted from @toji's DDS utils
19 | // https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js
20 |
21 | // All values and structures referenced from:
22 | // http://msdn.microsoft.com/en-us/library/bb943991.aspx/
23 |
24 | var DDS_MAGIC = 0x20534444;
25 |
26 | var DDSD_CAPS = 0x1,
27 | DDSD_HEIGHT = 0x2,
28 | DDSD_WIDTH = 0x4,
29 | DDSD_PITCH = 0x8,
30 | DDSD_PIXELFORMAT = 0x1000,
31 | DDSD_MIPMAPCOUNT = 0x20000,
32 | DDSD_LINEARSIZE = 0x80000,
33 | DDSD_DEPTH = 0x800000;
34 |
35 | var DDSCAPS_COMPLEX = 0x8,
36 | DDSCAPS_MIPMAP = 0x400000,
37 | DDSCAPS_TEXTURE = 0x1000;
38 |
39 | var DDSCAPS2_CUBEMAP = 0x200,
40 | DDSCAPS2_CUBEMAP_POSITIVEX = 0x400,
41 | DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800,
42 | DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000,
43 | DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000,
44 | DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000,
45 | DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000,
46 | DDSCAPS2_VOLUME = 0x200000;
47 |
48 | var DDPF_ALPHAPIXELS = 0x1,
49 | DDPF_ALPHA = 0x2,
50 | DDPF_FOURCC = 0x4,
51 | DDPF_RGB = 0x40,
52 | DDPF_YUV = 0x200,
53 | DDPF_LUMINANCE = 0x20000;
54 |
55 | function fourCCToInt32( value ) {
56 |
57 | return value.charCodeAt( 0 ) +
58 | ( value.charCodeAt( 1 ) << 8 ) +
59 | ( value.charCodeAt( 2 ) << 16 ) +
60 | ( value.charCodeAt( 3 ) << 24 );
61 |
62 | }
63 |
64 | function int32ToFourCC( value ) {
65 |
66 | return String.fromCharCode(
67 | value & 0xff,
68 | ( value >> 8 ) & 0xff,
69 | ( value >> 16 ) & 0xff,
70 | ( value >> 24 ) & 0xff
71 | );
72 |
73 | }
74 |
75 | function loadARGBMip( buffer, dataOffset, width, height ) {
76 |
77 | var dataLength = width * height * 4;
78 | var srcBuffer = new Uint8Array( buffer, dataOffset, dataLength );
79 | var byteArray = new Uint8Array( dataLength );
80 | var dst = 0;
81 | var src = 0;
82 | for ( var y = 0; y < height; y ++ ) {
83 |
84 | for ( var x = 0; x < width; x ++ ) {
85 |
86 | var b = srcBuffer[ src ]; src ++;
87 | var g = srcBuffer[ src ]; src ++;
88 | var r = srcBuffer[ src ]; src ++;
89 | var a = srcBuffer[ src ]; src ++;
90 | byteArray[ dst ] = r; dst ++; //r
91 | byteArray[ dst ] = g; dst ++; //g
92 | byteArray[ dst ] = b; dst ++; //b
93 | byteArray[ dst ] = a; dst ++; //a
94 |
95 | }
96 |
97 | }
98 | return byteArray;
99 |
100 | }
101 |
102 | var FOURCC_DXT1 = fourCCToInt32( "DXT1" );
103 | var FOURCC_DXT3 = fourCCToInt32( "DXT3" );
104 | var FOURCC_DXT5 = fourCCToInt32( "DXT5" );
105 | var FOURCC_ETC1 = fourCCToInt32( "ETC1" );
106 |
107 | var headerLengthInt = 31; // The header length in 32 bit ints
108 |
109 | // Offsets into the header array
110 |
111 | var off_magic = 0;
112 |
113 | var off_size = 1;
114 | var off_flags = 2;
115 | var off_height = 3;
116 | var off_width = 4;
117 |
118 | var off_mipmapCount = 7;
119 |
120 | var off_pfFlags = 20;
121 | var off_pfFourCC = 21;
122 | var off_RGBBitCount = 22;
123 | var off_RBitMask = 23;
124 | var off_GBitMask = 24;
125 | var off_BBitMask = 25;
126 | var off_ABitMask = 26;
127 |
128 | var off_caps = 27;
129 | var off_caps2 = 28;
130 | var off_caps3 = 29;
131 | var off_caps4 = 30;
132 |
133 | // Parse header
134 |
135 | var header = new Int32Array( buffer, 0, headerLengthInt );
136 |
137 | if ( header[ off_magic ] !== DDS_MAGIC ) {
138 |
139 | console.error( 'THREE.DDSLoader.parse: Invalid magic number in DDS header.' );
140 | return dds;
141 |
142 | }
143 |
144 | if ( ! header[ off_pfFlags ] & DDPF_FOURCC ) {
145 |
146 | console.error( 'THREE.DDSLoader.parse: Unsupported format, must contain a FourCC code.' );
147 | return dds;
148 |
149 | }
150 |
151 | var blockBytes;
152 |
153 | var fourCC = header[ off_pfFourCC ];
154 |
155 | var isRGBAUncompressed = false;
156 |
157 | switch ( fourCC ) {
158 |
159 | case FOURCC_DXT1:
160 |
161 | blockBytes = 8;
162 | dds.format = THREE.RGB_S3TC_DXT1_Format;
163 | break;
164 |
165 | case FOURCC_DXT3:
166 |
167 | blockBytes = 16;
168 | dds.format = THREE.RGBA_S3TC_DXT3_Format;
169 | break;
170 |
171 | case FOURCC_DXT5:
172 |
173 | blockBytes = 16;
174 | dds.format = THREE.RGBA_S3TC_DXT5_Format;
175 | break;
176 |
177 | case FOURCC_ETC1:
178 |
179 | blockBytes = 8;
180 | dds.format = THREE.RGB_ETC1_Format;
181 | break;
182 |
183 | default:
184 |
185 | if ( header[ off_RGBBitCount ] === 32
186 | && header[ off_RBitMask ] & 0xff0000
187 | && header[ off_GBitMask ] & 0xff00
188 | && header[ off_BBitMask ] & 0xff
189 | && header[ off_ABitMask ] & 0xff000000 ) {
190 |
191 | isRGBAUncompressed = true;
192 | blockBytes = 64;
193 | dds.format = THREE.RGBAFormat;
194 |
195 | } else {
196 |
197 | console.error( 'THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC( fourCC ) );
198 | return dds;
199 |
200 | }
201 | }
202 |
203 | dds.mipmapCount = 1;
204 |
205 | if ( header[ off_flags ] & DDSD_MIPMAPCOUNT && loadMipmaps !== false ) {
206 |
207 | dds.mipmapCount = Math.max( 1, header[ off_mipmapCount ] );
208 |
209 | }
210 |
211 | var caps2 = header[ off_caps2 ];
212 | dds.isCubemap = caps2 & DDSCAPS2_CUBEMAP ? true : false;
213 | if ( dds.isCubemap && (
214 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEX ) ||
215 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX ) ||
216 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEY ) ||
217 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY ) ||
218 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ ) ||
219 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ )
220 | ) ) {
221 |
222 | console.error( 'THREE.DDSLoader.parse: Incomplete cubemap faces' );
223 | return dds;
224 |
225 | }
226 |
227 | dds.width = header[ off_width ];
228 | dds.height = header[ off_height ];
229 |
230 | var dataOffset = header[ off_size ] + 4;
231 |
232 | // Extract mipmaps buffers
233 |
234 | var faces = dds.isCubemap ? 6 : 1;
235 |
236 | for ( var face = 0; face < faces; face ++ ) {
237 |
238 | var width = dds.width;
239 | var height = dds.height;
240 |
241 | for ( var i = 0; i < dds.mipmapCount; i ++ ) {
242 |
243 | if ( isRGBAUncompressed ) {
244 |
245 | var byteArray = loadARGBMip( buffer, dataOffset, width, height );
246 | var dataLength = byteArray.length;
247 |
248 | } else {
249 |
250 | var dataLength = Math.max( 4, width ) / 4 * Math.max( 4, height ) / 4 * blockBytes;
251 | var byteArray = new Uint8Array( buffer, dataOffset, dataLength );
252 |
253 | }
254 |
255 | var mipmap = { "data": byteArray, "width": width, "height": height };
256 | dds.mipmaps.push( mipmap );
257 |
258 | dataOffset += dataLength;
259 |
260 | width = Math.max( width >> 1, 1 );
261 | height = Math.max( height >> 1, 1 );
262 |
263 | }
264 |
265 | }
266 |
267 | return dds;
268 |
269 | };
270 |
--------------------------------------------------------------------------------
/dependencies/MTLLoader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Loads a Wavefront .mtl file specifying materials
3 | *
4 | * @author angelxuanchang
5 | */
6 |
7 | THREE.MTLLoader = function( manager ) {
8 |
9 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
10 |
11 | };
12 |
13 | THREE.MTLLoader.prototype = {
14 |
15 | constructor: THREE.MTLLoader,
16 |
17 | load: function ( url, onLoad, onProgress, onError ) {
18 |
19 | var scope = this;
20 |
21 | var loader = new THREE.XHRLoader( this.manager );
22 | loader.setPath( this.path );
23 | loader.load( url, function ( text ) {
24 |
25 | onLoad( scope.parse( text ) );
26 |
27 | }, onProgress, onError );
28 |
29 | },
30 |
31 | setPath: function ( value ) {
32 |
33 | this.path = value;
34 |
35 | },
36 |
37 | setBaseUrl: function( value ) {
38 |
39 | // TODO: Merge with setPath()? Or rename to setTexturePath?
40 |
41 | this.baseUrl = value;
42 |
43 | },
44 |
45 | setCrossOrigin: function ( value ) {
46 |
47 | this.crossOrigin = value;
48 |
49 | },
50 |
51 | setMaterialOptions: function ( value ) {
52 |
53 | this.materialOptions = value;
54 |
55 | },
56 |
57 | /**
58 | * Parses loaded MTL file
59 | * @param text - Content of MTL file
60 | * @return {THREE.MTLLoader.MaterialCreator}
61 | */
62 | parse: function ( text ) {
63 |
64 | var lines = text.split( "\n" );
65 | var info = {};
66 | var delimiter_pattern = /\s+/;
67 | var materialsInfo = {};
68 |
69 | for ( var i = 0; i < lines.length; i ++ ) {
70 |
71 | var line = lines[ i ];
72 | line = line.trim();
73 |
74 | if ( line.length === 0 || line.charAt( 0 ) === '#' ) {
75 |
76 | // Blank line or comment ignore
77 | continue;
78 |
79 | }
80 |
81 | var pos = line.indexOf( ' ' );
82 |
83 | var key = ( pos >= 0 ) ? line.substring( 0, pos ) : line;
84 | key = key.toLowerCase();
85 |
86 | var value = ( pos >= 0 ) ? line.substring( pos + 1 ) : "";
87 | value = value.trim();
88 |
89 | if ( key === "newmtl" ) {
90 |
91 | // New material
92 |
93 | info = { name: value };
94 | materialsInfo[ value ] = info;
95 |
96 | } else if ( info ) {
97 |
98 | if ( key === "ka" || key === "kd" || key === "ks" ) {
99 |
100 | var ss = value.split( delimiter_pattern, 3 );
101 | info[ key ] = [ parseFloat( ss[ 0 ] ), parseFloat( ss[ 1 ] ), parseFloat( ss[ 2 ] ) ];
102 |
103 | } else {
104 |
105 | info[ key ] = value;
106 |
107 | }
108 |
109 | }
110 |
111 | }
112 |
113 | var materialCreator = new THREE.MTLLoader.MaterialCreator( this.baseUrl, this.materialOptions );
114 | materialCreator.setCrossOrigin( this.crossOrigin );
115 | materialCreator.setManager( this.manager );
116 | materialCreator.setMaterials( materialsInfo );
117 | return materialCreator;
118 |
119 | }
120 |
121 | };
122 |
123 | /**
124 | * Create a new THREE-MTLLoader.MaterialCreator
125 | * @param baseUrl - Url relative to which textures are loaded
126 | * @param options - Set of options on how to construct the materials
127 | * side: Which side to apply the material
128 | * THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide
129 | * wrap: What type of wrapping to apply for textures
130 | * THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping
131 | * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255
132 | * Default: false, assumed to be already normalized
133 | * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's
134 | * Default: false
135 | * @constructor
136 | */
137 |
138 | THREE.MTLLoader.MaterialCreator = function( baseUrl, options ) {
139 |
140 | this.baseUrl = baseUrl;
141 | this.options = options;
142 | this.materialsInfo = {};
143 | this.materials = {};
144 | this.materialsArray = [];
145 | this.nameLookup = {};
146 |
147 | this.side = ( this.options && this.options.side ) ? this.options.side : THREE.FrontSide;
148 | this.wrap = ( this.options && this.options.wrap ) ? this.options.wrap : THREE.RepeatWrapping;
149 |
150 | };
151 |
152 | THREE.MTLLoader.MaterialCreator.prototype = {
153 |
154 | constructor: THREE.MTLLoader.MaterialCreator,
155 |
156 | setCrossOrigin: function ( value ) {
157 |
158 | this.crossOrigin = value;
159 |
160 | },
161 |
162 | setManager: function ( value ) {
163 |
164 | this.manager = value;
165 |
166 | },
167 |
168 | setMaterials: function( materialsInfo ) {
169 |
170 | this.materialsInfo = this.convert( materialsInfo );
171 | this.materials = {};
172 | this.materialsArray = [];
173 | this.nameLookup = {};
174 |
175 | },
176 |
177 | convert: function( materialsInfo ) {
178 |
179 | if ( ! this.options ) return materialsInfo;
180 |
181 | var converted = {};
182 |
183 | for ( var mn in materialsInfo ) {
184 |
185 | // Convert materials info into normalized form based on options
186 |
187 | var mat = materialsInfo[ mn ];
188 |
189 | var covmat = {};
190 |
191 | converted[ mn ] = covmat;
192 |
193 | for ( var prop in mat ) {
194 |
195 | var save = true;
196 | var value = mat[ prop ];
197 | var lprop = prop.toLowerCase();
198 |
199 | switch ( lprop ) {
200 |
201 | case 'kd':
202 | case 'ka':
203 | case 'ks':
204 |
205 | // Diffuse color (color under white light) using RGB values
206 |
207 | if ( this.options && this.options.normalizeRGB ) {
208 |
209 | value = [ value[ 0 ] / 255, value[ 1 ] / 255, value[ 2 ] / 255 ];
210 |
211 | }
212 |
213 | if ( this.options && this.options.ignoreZeroRGBs ) {
214 |
215 | if ( value[ 0 ] === 0 && value[ 1 ] === 0 && value[ 1 ] === 0 ) {
216 |
217 | // ignore
218 |
219 | save = false;
220 |
221 | }
222 |
223 | }
224 |
225 | break;
226 |
227 | default:
228 |
229 | break;
230 | }
231 |
232 | if ( save ) {
233 |
234 | covmat[ lprop ] = value;
235 |
236 | }
237 |
238 | }
239 |
240 | }
241 |
242 | return converted;
243 |
244 | },
245 |
246 | preload: function () {
247 |
248 | for ( var mn in this.materialsInfo ) {
249 |
250 | this.create( mn );
251 |
252 | }
253 |
254 | },
255 |
256 | getIndex: function( materialName ) {
257 |
258 | return this.nameLookup[ materialName ];
259 |
260 | },
261 |
262 | getAsArray: function() {
263 |
264 | var index = 0;
265 |
266 | for ( var mn in this.materialsInfo ) {
267 |
268 | this.materialsArray[ index ] = this.create( mn );
269 | this.nameLookup[ mn ] = index;
270 | index ++;
271 |
272 | }
273 |
274 | return this.materialsArray;
275 |
276 | },
277 |
278 | create: function ( materialName ) {
279 |
280 | if ( this.materials[ materialName ] === undefined ) {
281 |
282 | this.createMaterial_( materialName );
283 |
284 | }
285 |
286 | return this.materials[ materialName ];
287 |
288 | },
289 |
290 | createMaterial_: function ( materialName ) {
291 |
292 | // Create material
293 |
294 | var mat = this.materialsInfo[ materialName ];
295 | var params = {
296 |
297 | name: materialName,
298 | side: this.side
299 |
300 | };
301 |
302 | for ( var prop in mat ) {
303 |
304 | var value = mat[ prop ];
305 |
306 | if ( value === '' ) continue;
307 |
308 | switch ( prop.toLowerCase() ) {
309 |
310 | // Ns is material specular exponent
311 |
312 | case 'kd':
313 |
314 | // Diffuse color (color under white light) using RGB values
315 |
316 | params[ 'color' ] = new THREE.Color().fromArray( value );
317 |
318 | break;
319 |
320 | case 'ks':
321 |
322 | // Specular color (color when light is reflected from shiny surface) using RGB values
323 | params[ 'specular' ] = new THREE.Color().fromArray( value );
324 |
325 | break;
326 |
327 | case 'map_kd':
328 |
329 | // Diffuse texture map
330 |
331 | params[ 'map' ] = this.loadTexture( this.baseUrl + value );
332 | params[ 'map' ].wrapS = this.wrap;
333 | params[ 'map' ].wrapT = this.wrap;
334 |
335 | break;
336 |
337 | case 'ns':
338 |
339 | // The specular exponent (defines the focus of the specular highlight)
340 | // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
341 |
342 | params[ 'shininess' ] = parseFloat( value );
343 |
344 | break;
345 |
346 | case 'd':
347 |
348 | if ( value < 1 ) {
349 |
350 | params[ 'opacity' ] = value;
351 | params[ 'transparent' ] = true;
352 |
353 | }
354 |
355 | break;
356 |
357 | case 'Tr':
358 |
359 | if ( value > 0 ) {
360 |
361 | params[ 'opacity' ] = 1 - value;
362 | params[ 'transparent' ] = true;
363 |
364 | }
365 |
366 | break;
367 |
368 | case 'map_bump':
369 | case 'bump':
370 |
371 | // Bump texture map
372 |
373 | if ( params[ 'bumpMap' ] ) break; // Avoid loading twice.
374 |
375 | params[ 'bumpMap' ] = this.loadTexture( this.baseUrl + value );
376 | params[ 'bumpMap' ].wrapS = this.wrap;
377 | params[ 'bumpMap' ].wrapT = this.wrap;
378 |
379 | break;
380 |
381 | default:
382 | break;
383 |
384 | }
385 |
386 | }
387 |
388 | this.materials[ materialName ] = new THREE.MeshPhongMaterial( params );
389 | return this.materials[ materialName ];
390 |
391 | },
392 |
393 |
394 | loadTexture: function ( url, mapping, onLoad, onProgress, onError ) {
395 |
396 | var texture;
397 | var loader = THREE.Loader.Handlers.get( url );
398 | if (loader === null) loader = new THREE.DDSLoader();
399 | var manager = ( this.manager !== undefined ) ? this.manager : THREE.DefaultLoadingManager;
400 |
401 | if ( loader === null ) {
402 |
403 | loader = new THREE.TextureLoader( manager );
404 |
405 | }
406 |
407 | if ( loader.setCrossOrigin ) loader.setCrossOrigin( this.crossOrigin );
408 | texture = loader.load( url, onLoad, onProgress, onError );
409 |
410 | if ( mapping !== undefined ) texture.mapping = mapping;
411 |
412 | return texture;
413 |
414 | }
415 |
416 | };
417 |
418 | THREE.EventDispatcher.prototype.apply( THREE.MTLLoader.prototype );
419 |
--------------------------------------------------------------------------------
/dependencies/OBJLoader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author mrdoob / http://mrdoob.com/
3 | */
4 |
5 | THREE.OBJLoader = function ( manager ) {
6 |
7 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
8 |
9 | this.materials = null;
10 |
11 | this.regexp = {
12 | // v float float float
13 | vertex_pattern : /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
14 | // vn float float float
15 | normal_pattern : /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
16 | // vt float float
17 | uv_pattern : /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
18 | // f vertex vertex vertex
19 | face_vertex : /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/,
20 | // f vertex/uv vertex/uv vertex/uv
21 | face_vertex_uv : /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/,
22 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal
23 | face_vertex_uv_normal : /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/,
24 | // f vertex//normal vertex//normal vertex//normal
25 | face_vertex_normal : /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/,
26 | // o object_name | g group_name
27 | object_pattern : /^[og]\s*(.+)?/,
28 | // s boolean
29 | smoothing_pattern : /^s\s+(\d+|on|off)/,
30 | // mtllib file_reference
31 | material_library_pattern : /^mtllib /,
32 | // usemtl material_name
33 | material_use_pattern : /^usemtl /
34 | };
35 |
36 | };
37 |
38 | THREE.OBJLoader.prototype = {
39 |
40 | constructor: THREE.OBJLoader,
41 |
42 | load: function ( url, onLoad, onProgress, onError ) {
43 |
44 | var scope = this;
45 |
46 | var loader = new THREE.XHRLoader( scope.manager );
47 | loader.setPath( this.path );
48 | loader.load( url, function ( text ) {
49 |
50 | onLoad( scope.parse( text ) );
51 |
52 | }, onProgress, onError );
53 |
54 | },
55 |
56 | setPath: function ( value ) {
57 |
58 | this.path = value;
59 |
60 | },
61 |
62 | setMaterials: function ( materials ) {
63 |
64 | this.materials = materials;
65 |
66 | },
67 |
68 | _createParserState : function () {
69 |
70 | var state = {
71 | objects : [],
72 | object : {},
73 |
74 | vertices : [],
75 | normals : [],
76 | uvs : [],
77 |
78 | materialLibraries : [],
79 |
80 | startObject: function ( name, fromDeclaration ) {
81 |
82 | // If the current object (initial from reset) is not from a g/o declaration in the parsed
83 | // file. We need to use it for the first parsed g/o to keep things in sync.
84 | if ( this.object && this.object.fromDeclaration === false ) {
85 |
86 | this.object.name = name;
87 | this.object.fromDeclaration = ( fromDeclaration !== false );
88 | return;
89 |
90 | }
91 |
92 | var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined );
93 |
94 | if ( this.object && typeof this.object._finalize === 'function' ) {
95 |
96 | this.object._finalize( true );
97 |
98 | }
99 |
100 | this.object = {
101 | name : name || '',
102 | fromDeclaration : ( fromDeclaration !== false ),
103 |
104 | geometry : {
105 | vertices : [],
106 | normals : [],
107 | uvs : []
108 | },
109 | materials : [],
110 | smooth : true,
111 |
112 | startMaterial : function( name, libraries ) {
113 |
114 | var previous = this._finalize( false );
115 |
116 | // New usemtl declaration overwrites an inherited material, except if faces were declared
117 | // after the material, then it must be preserved for proper MultiMaterial continuation.
118 | if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) {
119 |
120 | this.materials.splice( previous.index, 1 );
121 |
122 | }
123 |
124 | var material = {
125 | index : this.materials.length,
126 | name : name || '',
127 | mtllib : ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ),
128 | smooth : ( previous !== undefined ? previous.smooth : this.smooth ),
129 | groupStart : ( previous !== undefined ? previous.groupEnd : 0 ),
130 | groupEnd : -1,
131 | groupCount : -1,
132 | inherited : false,
133 |
134 | clone : function( index ) {
135 | var cloned = {
136 | index : ( typeof index === 'number' ? index : this.index ),
137 | name : this.name,
138 | mtllib : this.mtllib,
139 | smooth : this.smooth,
140 | groupStart : 0,
141 | groupEnd : -1,
142 | groupCount : -1,
143 | inherited : false
144 | };
145 | cloned.clone = this.clone.bind(cloned);
146 | return cloned;
147 | }
148 | };
149 |
150 | this.materials.push( material );
151 |
152 | return material;
153 |
154 | },
155 |
156 | currentMaterial : function() {
157 |
158 | if ( this.materials.length > 0 ) {
159 | return this.materials[ this.materials.length - 1 ];
160 | }
161 |
162 | return undefined;
163 |
164 | },
165 |
166 | _finalize : function( end ) {
167 |
168 | var lastMultiMaterial = this.currentMaterial();
169 | if ( lastMultiMaterial && lastMultiMaterial.groupEnd === -1 ) {
170 |
171 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
172 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
173 | lastMultiMaterial.inherited = false;
174 |
175 | }
176 |
177 | // Ignore objects tail materials if no face declarations followed them before a new o/g started.
178 | if ( end && this.materials.length > 1 ) {
179 |
180 | for ( var mi = this.materials.length - 1; mi >= 0; mi-- ) {
181 | if ( this.materials[mi].groupCount <= 0 ) {
182 | this.materials.splice( mi, 1 );
183 | }
184 | }
185 |
186 | }
187 |
188 | // Guarantee at least one empty material, this makes the creation later more straight forward.
189 | if ( end && this.materials.length === 0 ) {
190 |
191 | this.materials.push({
192 | name : '',
193 | smooth : this.smooth
194 | });
195 |
196 | }
197 |
198 | return lastMultiMaterial;
199 |
200 | }
201 | };
202 |
203 | // Inherit previous objects material.
204 | // Spec tells us that a declared material must be set to all objects until a new material is declared.
205 | // If a usemtl declaration is encountered while this new object is being parsed, it will
206 | // overwrite the inherited material. Exception being that there was already face declarations
207 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation.
208 |
209 | if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function" ) {
210 |
211 | var declared = previousMaterial.clone( 0 );
212 | declared.inherited = true;
213 | this.object.materials.push( declared );
214 |
215 | }
216 |
217 | this.objects.push( this.object );
218 |
219 | },
220 |
221 | finalize : function() {
222 |
223 | if ( this.object && typeof this.object._finalize === 'function' ) {
224 |
225 | this.object._finalize( true );
226 |
227 | }
228 |
229 | },
230 |
231 | parseVertexIndex: function ( value, len ) {
232 |
233 | var index = parseInt( value, 10 );
234 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
235 |
236 | },
237 |
238 | parseNormalIndex: function ( value, len ) {
239 |
240 | var index = parseInt( value, 10 );
241 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
242 |
243 | },
244 |
245 | parseUVIndex: function ( value, len ) {
246 |
247 | var index = parseInt( value, 10 );
248 | return ( index >= 0 ? index - 1 : index + len / 2 ) * 2;
249 |
250 | },
251 |
252 | addVertex: function ( a, b, c ) {
253 |
254 | var src = this.vertices;
255 | var dst = this.object.geometry.vertices;
256 |
257 | dst.push( src[ a + 0 ] );
258 | dst.push( src[ a + 1 ] );
259 | dst.push( src[ a + 2 ] );
260 | dst.push( src[ b + 0 ] );
261 | dst.push( src[ b + 1 ] );
262 | dst.push( src[ b + 2 ] );
263 | dst.push( src[ c + 0 ] );
264 | dst.push( src[ c + 1 ] );
265 | dst.push( src[ c + 2 ] );
266 |
267 | },
268 |
269 | addVertexLine: function ( a ) {
270 |
271 | var src = this.vertices;
272 | var dst = this.object.geometry.vertices;
273 |
274 | dst.push( src[ a + 0 ] );
275 | dst.push( src[ a + 1 ] );
276 | dst.push( src[ a + 2 ] );
277 |
278 | },
279 |
280 | addNormal : function ( a, b, c ) {
281 |
282 | var src = this.normals;
283 | var dst = this.object.geometry.normals;
284 |
285 | dst.push( src[ a + 0 ] );
286 | dst.push( src[ a + 1 ] );
287 | dst.push( src[ a + 2 ] );
288 | dst.push( src[ b + 0 ] );
289 | dst.push( src[ b + 1 ] );
290 | dst.push( src[ b + 2 ] );
291 | dst.push( src[ c + 0 ] );
292 | dst.push( src[ c + 1 ] );
293 | dst.push( src[ c + 2 ] );
294 |
295 | },
296 |
297 | addUV: function ( a, b, c ) {
298 |
299 | var src = this.uvs;
300 | var dst = this.object.geometry.uvs;
301 |
302 | dst.push( src[ a + 0 ] );
303 | dst.push( src[ a + 1 ] );
304 | dst.push( src[ b + 0 ] );
305 | dst.push( src[ b + 1 ] );
306 | dst.push( src[ c + 0 ] );
307 | dst.push( src[ c + 1 ] );
308 |
309 | },
310 |
311 | addUVLine: function ( a ) {
312 |
313 | var src = this.uvs;
314 | var dst = this.object.geometry.uvs;
315 |
316 | dst.push( src[ a + 0 ] );
317 | dst.push( src[ a + 1 ] );
318 |
319 | },
320 |
321 | addFace: function ( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) {
322 |
323 | var vLen = this.vertices.length;
324 |
325 | var ia = this.parseVertexIndex( a, vLen );
326 | var ib = this.parseVertexIndex( b, vLen );
327 | var ic = this.parseVertexIndex( c, vLen );
328 | var id;
329 |
330 | if ( d === undefined ) {
331 |
332 | this.addVertex( ia, ib, ic );
333 |
334 | } else {
335 |
336 | id = this.parseVertexIndex( d, vLen );
337 |
338 | this.addVertex( ia, ib, id );
339 | this.addVertex( ib, ic, id );
340 |
341 | }
342 |
343 | if ( ua !== undefined ) {
344 |
345 | var uvLen = this.uvs.length;
346 |
347 | ia = this.parseUVIndex( ua, uvLen );
348 | ib = this.parseUVIndex( ub, uvLen );
349 | ic = this.parseUVIndex( uc, uvLen );
350 |
351 | if ( d === undefined ) {
352 |
353 | this.addUV( ia, ib, ic );
354 |
355 | } else {
356 |
357 | id = this.parseUVIndex( ud, uvLen );
358 |
359 | this.addUV( ia, ib, id );
360 | this.addUV( ib, ic, id );
361 |
362 | }
363 |
364 | }
365 |
366 | if ( na !== undefined ) {
367 |
368 | // Normals are many times the same. If so, skip function call and parseInt.
369 | var nLen = this.normals.length;
370 | ia = this.parseNormalIndex( na, nLen );
371 |
372 | ib = na === nb ? ia : this.parseNormalIndex( nb, nLen );
373 | ic = na === nc ? ia : this.parseNormalIndex( nc, nLen );
374 |
375 | if ( d === undefined ) {
376 |
377 | this.addNormal( ia, ib, ic );
378 |
379 | } else {
380 |
381 | id = this.parseNormalIndex( nd, nLen );
382 |
383 | this.addNormal( ia, ib, id );
384 | this.addNormal( ib, ic, id );
385 |
386 | }
387 |
388 | }
389 |
390 | },
391 |
392 | addLineGeometry: function ( vertices, uvs ) {
393 |
394 | this.object.geometry.type = 'Line';
395 |
396 | var vLen = this.vertices.length;
397 | var uvLen = this.uvs.length;
398 |
399 | for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) {
400 |
401 | this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) );
402 |
403 | }
404 |
405 | for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) {
406 |
407 | this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) );
408 |
409 | }
410 |
411 | }
412 |
413 | };
414 |
415 | state.startObject( '', false );
416 |
417 | return state;
418 |
419 | },
420 |
421 | parse: function ( text ) {
422 |
423 | console.time( 'OBJLoader' );
424 |
425 | var state = this._createParserState();
426 |
427 | if ( text.indexOf( '\r\n' ) !== - 1 ) {
428 |
429 | // This is faster than String.split with regex that splits on both
430 | text = text.replace( /\r\n/g, '\n' );
431 |
432 | }
433 |
434 | if ( text.indexOf( '\\\n' ) !== - 1) {
435 |
436 | // join lines separated by a line continuation character (\)
437 | text = text.replace( /\\\n/g, '' );
438 |
439 | }
440 |
441 | var lines = text.split( '\n' );
442 | var line = '', lineFirstChar = '', lineSecondChar = '';
443 | var lineLength = 0;
444 | var result = [];
445 |
446 | // Faster to just trim left side of the line. Use if available.
447 | var trimLeft = ( typeof ''.trimLeft === 'function' );
448 |
449 | for ( var i = 0, l = lines.length; i < l; i ++ ) {
450 |
451 | line = lines[ i ];
452 |
453 | line = trimLeft ? line.trimLeft() : line.trim();
454 |
455 | lineLength = line.length;
456 |
457 | if ( lineLength === 0 ) continue;
458 |
459 | lineFirstChar = line.charAt( 0 );
460 |
461 | // @todo invoke passed in handler if any
462 | if ( lineFirstChar === '#' ) continue;
463 |
464 | if ( lineFirstChar === 'v' ) {
465 |
466 | lineSecondChar = line.charAt( 1 );
467 |
468 | if ( lineSecondChar === ' ' && ( result = this.regexp.vertex_pattern.exec( line ) ) !== null ) {
469 |
470 | // 0 1 2 3
471 | // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
472 |
473 | state.vertices.push(
474 | parseFloat( result[ 1 ] ),
475 | parseFloat( result[ 2 ] ),
476 | parseFloat( result[ 3 ] )
477 | );
478 |
479 | } else if ( lineSecondChar === 'n' && ( result = this.regexp.normal_pattern.exec( line ) ) !== null ) {
480 |
481 | // 0 1 2 3
482 | // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
483 |
484 | state.normals.push(
485 | parseFloat( result[ 1 ] ),
486 | parseFloat( result[ 2 ] ),
487 | parseFloat( result[ 3 ] )
488 | );
489 |
490 | } else if ( lineSecondChar === 't' && ( result = this.regexp.uv_pattern.exec( line ) ) !== null ) {
491 |
492 | // 0 1 2
493 | // ["vt 0.1 0.2", "0.1", "0.2"]
494 |
495 | state.uvs.push(
496 | parseFloat( result[ 1 ] ),
497 | parseFloat( result[ 2 ] )
498 | );
499 |
500 | } else {
501 |
502 | throw new Error( "Unexpected vertex/normal/uv line: '" + line + "'" );
503 |
504 | }
505 |
506 | } else if ( lineFirstChar === "f" ) {
507 |
508 | if ( ( result = this.regexp.face_vertex_uv_normal.exec( line ) ) !== null ) {
509 |
510 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal
511 | // 0 1 2 3 4 5 6 7 8 9 10 11 12
512 | // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined]
513 |
514 | state.addFace(
515 | result[ 1 ], result[ 4 ], result[ 7 ], result[ 10 ],
516 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ],
517 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ]
518 | );
519 |
520 | } else if ( ( result = this.regexp.face_vertex_uv.exec( line ) ) !== null ) {
521 |
522 | // f vertex/uv vertex/uv vertex/uv
523 | // 0 1 2 3 4 5 6 7 8
524 | // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined]
525 |
526 | state.addFace(
527 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ],
528 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ]
529 | );
530 |
531 | } else if ( ( result = this.regexp.face_vertex_normal.exec( line ) ) !== null ) {
532 |
533 | // f vertex//normal vertex//normal vertex//normal
534 | // 0 1 2 3 4 5 6 7 8
535 | // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined]
536 |
537 | state.addFace(
538 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ],
539 | undefined, undefined, undefined, undefined,
540 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ]
541 | );
542 |
543 | } else if ( ( result = this.regexp.face_vertex.exec( line ) ) !== null ) {
544 |
545 | // f vertex vertex vertex
546 | // 0 1 2 3 4
547 | // ["f 1 2 3", "1", "2", "3", undefined]
548 |
549 | state.addFace(
550 | result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ]
551 | );
552 |
553 | } else {
554 |
555 | throw new Error( "Unexpected face line: '" + line + "'" );
556 |
557 | }
558 |
559 | } else if ( lineFirstChar === "l" ) {
560 |
561 | var lineParts = line.substring( 1 ).trim().split( " " );
562 | var lineVertices = [], lineUVs = [];
563 |
564 | if ( line.indexOf( "/" ) === - 1 ) {
565 |
566 | lineVertices = lineParts;
567 |
568 | } else {
569 |
570 | for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) {
571 |
572 | var parts = lineParts[ li ].split( "/" );
573 |
574 | if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] );
575 | if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] );
576 |
577 | }
578 |
579 | }
580 | state.addLineGeometry( lineVertices, lineUVs );
581 |
582 | } else if ( ( result = this.regexp.object_pattern.exec( line ) ) !== null ) {
583 |
584 | // o object_name
585 | // or
586 | // g group_name
587 |
588 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
589 | // var name = result[ 0 ].substr( 1 ).trim();
590 | var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 );
591 |
592 | state.startObject( name );
593 |
594 | } else if ( this.regexp.material_use_pattern.test( line ) ) {
595 |
596 | // material
597 |
598 | state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries );
599 |
600 | } else if ( this.regexp.material_library_pattern.test( line ) ) {
601 |
602 | // mtl file
603 |
604 | state.materialLibraries.push( line.substring( 7 ).trim() );
605 |
606 | } else if ( ( result = this.regexp.smoothing_pattern.exec( line ) ) !== null ) {
607 |
608 | // smooth shading
609 |
610 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry,
611 | // but does not define a usemtl for each face set.
612 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups).
613 | // This requires some care to not create extra material on each smooth value for "normal" obj files.
614 | // where explicit usemtl defines geometry groups.
615 | // Example asset: examples/models/obj/cerberus/Cerberus.obj
616 |
617 | var value = result[ 1 ].trim().toLowerCase();
618 | state.object.smooth = ( value === '1' || value === 'on' );
619 |
620 | var material = state.object.currentMaterial();
621 | if ( material ) {
622 |
623 | material.smooth = state.object.smooth;
624 |
625 | }
626 |
627 | } else {
628 |
629 | // Handle null terminated files without exception
630 | if ( line === '\0' ) continue;
631 |
632 | throw new Error( "Unexpected line: '" + line + "'" );
633 |
634 | }
635 |
636 | }
637 |
638 | state.finalize();
639 |
640 | var container = new THREE.Group();
641 | container.materialLibraries = [].concat( state.materialLibraries );
642 |
643 | for ( var i = 0, l = state.objects.length; i < l; i ++ ) {
644 |
645 | var object = state.objects[ i ];
646 | var geometry = object.geometry;
647 | var materials = object.materials;
648 | var isLine = ( geometry.type === 'Line' );
649 |
650 | // Skip o/g line declarations that did not follow with any faces
651 | if ( geometry.vertices.length === 0 ) continue;
652 |
653 | var buffergeometry = new THREE.BufferGeometry();
654 |
655 | buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) );
656 |
657 | if ( geometry.normals.length > 0 ) {
658 |
659 | buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) );
660 |
661 | } else {
662 |
663 | buffergeometry.computeVertexNormals();
664 |
665 | }
666 |
667 | if ( geometry.uvs.length > 0 ) {
668 |
669 | buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) );
670 |
671 | }
672 |
673 | // Create materials
674 |
675 | var createdMaterials = [];
676 |
677 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) {
678 |
679 | var sourceMaterial = materials[mi];
680 | var material = undefined;
681 |
682 | if ( this.materials !== null ) {
683 |
684 | material = this.materials.create( sourceMaterial.name );
685 |
686 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
687 | if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) {
688 |
689 | var materialLine = new THREE.LineBasicMaterial();
690 | materialLine.copy( material );
691 | material = materialLine;
692 |
693 | }
694 |
695 | }
696 |
697 | if ( ! material ) {
698 |
699 | material = ( ! isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial() );
700 | material.name = sourceMaterial.name;
701 |
702 | }
703 |
704 | material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading;
705 |
706 | createdMaterials.push(material);
707 |
708 | }
709 |
710 | // Create mesh
711 |
712 | var mesh;
713 |
714 | if ( createdMaterials.length > 1 ) {
715 |
716 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) {
717 |
718 | var sourceMaterial = materials[mi];
719 | buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi );
720 |
721 | }
722 |
723 | var multiMaterial = new THREE.MultiMaterial( createdMaterials );
724 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, multiMaterial ) : new THREE.LineSegments( buffergeometry, multiMaterial ) );
725 |
726 | } else {
727 |
728 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ) : new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ) );
729 | }
730 |
731 | mesh.name = object.name;
732 |
733 | container.add( mesh );
734 |
735 | }
736 |
737 | console.timeEnd( 'OBJLoader' );
738 |
739 | return container;
740 |
741 | }
742 |
743 | };
744 |
--------------------------------------------------------------------------------
/dependencies/OrbitControls.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author qiao / https://github.com/qiao
3 | * @author mrdoob / http://mrdoob.com
4 | * @author alteredq / http://alteredqualia.com/
5 | * @author WestLangley / http://github.com/WestLangley
6 | * @author erich666 / http://erichaines.com
7 | */
8 |
9 | // This set of controls performs orbiting, dollying (zooming), and panning.
10 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
11 | //
12 | // Orbit - left mouse / touch: one finger move
13 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
14 | // Pan - right mouse, or arrow keys / touch: three finter swipe
15 |
16 | THREE.OrbitControls = function ( object, domElement ) {
17 |
18 | this.object = object;
19 |
20 | this.domElement = ( domElement !== undefined ) ? domElement : document;
21 |
22 | // Set to false to disable this control
23 | this.enabled = true;
24 |
25 | // "target" sets the location of focus, where the object orbits around
26 | this.target = new THREE.Vector3();
27 |
28 | // How far you can dolly in and out ( PerspectiveCamera only )
29 | this.minDistance = 0;
30 | this.maxDistance = Infinity;
31 |
32 | // How far you can zoom in and out ( OrthographicCamera only )
33 | this.minZoom = 0;
34 | this.maxZoom = Infinity;
35 |
36 | // How far you can orbit vertically, upper and lower limits.
37 | // Range is 0 to Math.PI radians.
38 | this.minPolarAngle = 0; // radians
39 | this.maxPolarAngle = Math.PI; // radians
40 |
41 | // How far you can orbit horizontally, upper and lower limits.
42 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
43 | this.minAzimuthAngle = - Infinity; // radians
44 | this.maxAzimuthAngle = Infinity; // radians
45 |
46 | // Set to true to enable damping (inertia)
47 | // If damping is enabled, you must call controls.update() in your animation loop
48 | this.enableDamping = false;
49 | this.dampingFactor = 0.25;
50 |
51 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
52 | // Set to false to disable zooming
53 | this.enableZoom = true;
54 | this.zoomSpeed = 1.0;
55 |
56 | // Set to false to disable rotating
57 | this.enableRotate = true;
58 | this.rotateSpeed = 1.0;
59 |
60 | // Set to false to disable panning
61 | this.enablePan = true;
62 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push
63 |
64 | // Set to true to automatically rotate around the target
65 | // If auto-rotate is enabled, you must call controls.update() in your animation loop
66 | this.autoRotate = false;
67 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
68 |
69 | // Set to false to disable use of the keys
70 | this.enableKeys = true;
71 |
72 | // The four arrow keys
73 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
74 |
75 | // Mouse buttons
76 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
77 |
78 | // for reset
79 | this.target0 = this.target.clone();
80 | this.position0 = this.object.position.clone();
81 | this.zoom0 = this.object.zoom;
82 |
83 | this.setTarget = function(target){//three vector
84 | this.target = target;
85 | this.target0 = this.target.clone();
86 | };
87 |
88 | //
89 | // public methods
90 | //
91 |
92 | this.getPolarAngle = function () {
93 |
94 | return spherical.phi;
95 |
96 | };
97 |
98 | this.getAzimuthalAngle = function () {
99 |
100 | return spherical.theta;
101 |
102 | };
103 |
104 | this.reset = function () {
105 |
106 | scope.target.copy( scope.target0 );
107 | scope.object.position.copy( scope.position0 );
108 | scope.object.zoom = scope.zoom0;
109 |
110 | //scope.object.updateProjectionMatrix();
111 | //scope.dispatchEvent( changeEvent );
112 |
113 | scope.update();
114 |
115 | state = STATE.NONE;
116 |
117 | };
118 |
119 | // this method is exposed, but perhaps it would be better if we can make it private...
120 | this.update = function() {
121 |
122 | var offset = new THREE.Vector3();
123 |
124 | // so camera.up is the orbit axis
125 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
126 | var quatInverse = quat.clone().inverse();
127 |
128 | var lastPosition = new THREE.Vector3();
129 | var lastQuaternion = new THREE.Quaternion();
130 |
131 | return function update () {
132 |
133 | var position = scope.object.position;
134 |
135 | offset.copy( position ).sub( scope.target );
136 |
137 | // rotate offset to "y-axis-is-up" space
138 | offset.applyQuaternion( quat );
139 |
140 | // angle from z-axis around y-axis
141 | spherical.setFromVector3( offset );
142 |
143 | if ( scope.autoRotate && state === STATE.NONE ) {
144 |
145 | rotateLeft( getAutoRotationAngle() );
146 |
147 | }
148 |
149 | spherical.theta += sphericalDelta.theta;
150 | spherical.phi += sphericalDelta.phi;
151 |
152 | // restrict theta to be between desired limits
153 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );
154 |
155 | // restrict phi to be between desired limits
156 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
157 |
158 | spherical.makeSafe();
159 |
160 |
161 | spherical.radius *= scale;
162 |
163 | // restrict radius to be between desired limits
164 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
165 |
166 | // move target to panned location
167 | scope.target.add( panOffset );
168 |
169 | offset.setFromSpherical( spherical );
170 |
171 | // rotate offset back to "camera-up-vector-is-up" space
172 | offset.applyQuaternion( quatInverse );
173 |
174 | position.copy( scope.target ).add( offset );
175 |
176 | scope.object.lookAt( scope.target );
177 |
178 | if ( scope.enableDamping === true ) {
179 |
180 | sphericalDelta.theta *= ( 1 - scope.dampingFactor );
181 | sphericalDelta.phi *= ( 1 - scope.dampingFactor );
182 |
183 | } else {
184 |
185 | sphericalDelta.set( 0, 0, 0 );
186 |
187 | }
188 |
189 | scale = 1;
190 | panOffset.set( 0, 0, 0 );
191 |
192 | // update condition is:
193 | // min(camera displacement, camera rotation in radians)^2 > EPS
194 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8
195 |
196 | if ( zoomChanged ||
197 | lastPosition.distanceToSquared( scope.object.position ) > EPS ||
198 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
199 |
200 | scope.dispatchEvent( changeEvent );
201 |
202 | lastPosition.copy( scope.object.position );
203 | lastQuaternion.copy( scope.object.quaternion );
204 | zoomChanged = false;
205 |
206 | return true;
207 |
208 | }
209 |
210 | return false;
211 |
212 | };
213 |
214 | }();
215 |
216 | this.dispose = function() {
217 |
218 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
219 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
220 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
221 |
222 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
223 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
224 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
225 |
226 | document.removeEventListener( 'mousemove', onMouseMove, false );
227 | document.removeEventListener( 'mouseup', onMouseUp, false );
228 |
229 | window.removeEventListener( 'keydown', onKeyDown, false );
230 |
231 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
232 |
233 | };
234 |
235 | //
236 | // internals
237 | //
238 |
239 | var scope = this;
240 |
241 | var changeEvent = { type: 'change' };
242 | var startEvent = { type: 'start' };
243 | var endEvent = { type: 'end' };
244 |
245 | var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
246 |
247 | var state = STATE.NONE;
248 |
249 | var EPS = 0.000001;
250 |
251 | // current position in spherical coordinates
252 | var spherical = new THREE.Spherical();
253 | var sphericalDelta = new THREE.Spherical();
254 |
255 | var scale = 1;
256 | var panOffset = new THREE.Vector3();
257 | var zoomChanged = false;
258 |
259 | var rotateStart = new THREE.Vector2();
260 | var rotateEnd = new THREE.Vector2();
261 | var rotateDelta = new THREE.Vector2();
262 |
263 | var panStart = new THREE.Vector2();
264 | var panEnd = new THREE.Vector2();
265 | var panDelta = new THREE.Vector2();
266 |
267 | var dollyStart = new THREE.Vector2();
268 | var dollyEnd = new THREE.Vector2();
269 | var dollyDelta = new THREE.Vector2();
270 |
271 | function getAutoRotationAngle() {
272 |
273 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
274 |
275 | }
276 |
277 | function getZoomScale() {
278 |
279 | return Math.pow( 0.95, scope.zoomSpeed );
280 |
281 | }
282 |
283 | function rotateLeft( angle ) {
284 |
285 | sphericalDelta.theta -= angle;
286 |
287 | }
288 |
289 | function rotateUp( angle ) {
290 |
291 | sphericalDelta.phi -= angle;
292 |
293 | }
294 |
295 | var panLeft = function() {
296 |
297 | var v = new THREE.Vector3();
298 |
299 | return function panLeft( distance, objectMatrix ) {
300 |
301 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
302 | v.multiplyScalar( - distance );
303 |
304 | panOffset.add( v );
305 |
306 | };
307 |
308 | }();
309 |
310 | var panUp = function() {
311 |
312 | var v = new THREE.Vector3();
313 |
314 | return function panUp( distance, objectMatrix ) {
315 |
316 | v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix
317 | v.multiplyScalar( distance );
318 |
319 | panOffset.add( v );
320 |
321 | };
322 |
323 | }();
324 |
325 | // deltaX and deltaY are in pixels; right and down are positive
326 | var pan = function() {
327 |
328 | var offset = new THREE.Vector3();
329 |
330 | return function pan ( deltaX, deltaY ) {
331 |
332 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
333 |
334 | if ( scope.object instanceof THREE.PerspectiveCamera ) {
335 |
336 | // perspective
337 | var position = scope.object.position;
338 | offset.copy( position ).sub( scope.target );
339 | var targetDistance = offset.length();
340 |
341 | // half of the fov is center to top of screen
342 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
343 |
344 | // we actually don't use screenWidth, since perspective camera is fixed to screen height
345 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
346 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
347 |
348 | } else if ( scope.object instanceof THREE.OrthographicCamera ) {
349 |
350 | // orthographic
351 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
352 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
353 |
354 | } else {
355 |
356 | // camera neither orthographic nor perspective
357 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
358 | scope.enablePan = false;
359 |
360 | }
361 |
362 | };
363 |
364 | }();
365 |
366 | function dollyIn( dollyScale ) {
367 |
368 | if ( scope.object instanceof THREE.PerspectiveCamera ) {
369 |
370 | scale /= dollyScale;
371 |
372 | } else if ( scope.object instanceof THREE.OrthographicCamera ) {
373 |
374 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
375 | scope.object.updateProjectionMatrix();
376 | zoomChanged = true;
377 |
378 | } else {
379 |
380 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
381 | scope.enableZoom = false;
382 |
383 | }
384 |
385 | }
386 |
387 | function dollyOut( dollyScale ) {
388 |
389 | if ( scope.object instanceof THREE.PerspectiveCamera ) {
390 |
391 | scale *= dollyScale;
392 |
393 | } else if ( scope.object instanceof THREE.OrthographicCamera ) {
394 |
395 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
396 | scope.object.updateProjectionMatrix();
397 | zoomChanged = true;
398 |
399 | } else {
400 |
401 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
402 | scope.enableZoom = false;
403 |
404 | }
405 |
406 | }
407 |
408 | //
409 | // event callbacks - update the object state
410 | //
411 |
412 | function handleMouseDownRotate( event ) {
413 |
414 | //console.log( 'handleMouseDownRotate' );
415 |
416 | rotateStart.set( event.clientX, event.clientY );
417 |
418 | }
419 |
420 | function handleMouseDownDolly( event ) {
421 |
422 | //console.log( 'handleMouseDownDolly' );
423 |
424 | dollyStart.set( event.clientX, event.clientY );
425 |
426 | }
427 |
428 | function handleMouseDownPan( event ) {
429 |
430 | //console.log( 'handleMouseDownPan' );
431 |
432 | panStart.set( event.clientX, event.clientY );
433 |
434 | }
435 |
436 | function handleMouseMoveRotate( event ) {
437 |
438 | //console.log( 'handleMouseMoveRotate' );
439 |
440 | rotateEnd.set( event.clientX, event.clientY );
441 | rotateDelta.subVectors( rotateEnd, rotateStart );
442 |
443 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
444 |
445 | // rotating across whole screen goes 360 degrees around
446 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
447 |
448 | // rotating up and down along whole screen attempts to go 360, but limited to 180
449 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
450 |
451 | rotateStart.copy( rotateEnd );
452 |
453 | scope.update();
454 |
455 | }
456 |
457 | function handleMouseMoveDolly( event ) {
458 |
459 | //console.log( 'handleMouseMoveDolly' );
460 |
461 | dollyEnd.set( event.clientX, event.clientY );
462 |
463 | dollyDelta.subVectors( dollyEnd, dollyStart );
464 |
465 | if ( dollyDelta.y > 0 ) {
466 |
467 | dollyIn( getZoomScale() );
468 |
469 | } else if ( dollyDelta.y < 0 ) {
470 |
471 | dollyOut( getZoomScale() );
472 |
473 | }
474 |
475 | dollyStart.copy( dollyEnd );
476 |
477 | scope.update();
478 |
479 | }
480 |
481 | function handleMouseMovePan( event ) {
482 |
483 | //console.log( 'handleMouseMovePan' );
484 |
485 | panEnd.set( event.clientX, event.clientY );
486 |
487 | panDelta.subVectors( panEnd, panStart );
488 |
489 | pan( panDelta.x, panDelta.y );
490 |
491 | panStart.copy( panEnd );
492 |
493 | scope.update();
494 |
495 | }
496 |
497 | function handleMouseUp( event ) {
498 |
499 | //console.log( 'handleMouseUp' );
500 |
501 | }
502 |
503 | function handleMouseWheel( event ) {
504 |
505 | //console.log( 'handleMouseWheel' );
506 |
507 | if ( event.deltaY < 0 ) {
508 |
509 | dollyOut( getZoomScale() );
510 |
511 | } else if ( event.deltaY > 0 ) {
512 |
513 | dollyIn( getZoomScale() );
514 |
515 | }
516 |
517 | scope.update();
518 |
519 | }
520 |
521 | function handleKeyDown( event ) {
522 |
523 | //console.log( 'handleKeyDown' );
524 |
525 | switch ( event.keyCode ) {
526 |
527 | case scope.keys.UP:
528 | pan( 0, scope.keyPanSpeed );
529 | scope.update();
530 | break;
531 |
532 | case scope.keys.BOTTOM:
533 | pan( 0, - scope.keyPanSpeed );
534 | scope.update();
535 | break;
536 |
537 | case scope.keys.LEFT:
538 | pan( scope.keyPanSpeed, 0 );
539 | scope.update();
540 | break;
541 |
542 | case scope.keys.RIGHT:
543 | pan( - scope.keyPanSpeed, 0 );
544 | scope.update();
545 | break;
546 |
547 | }
548 |
549 | }
550 |
551 | function handleTouchStartRotate( event ) {
552 |
553 | //console.log( 'handleTouchStartRotate' );
554 |
555 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
556 |
557 | }
558 |
559 | function handleTouchStartDolly( event ) {
560 |
561 | //console.log( 'handleTouchStartDolly' );
562 |
563 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
564 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
565 |
566 | var distance = Math.sqrt( dx * dx + dy * dy );
567 |
568 | dollyStart.set( 0, distance );
569 |
570 | }
571 |
572 | function handleTouchStartPan( event ) {
573 |
574 | //console.log( 'handleTouchStartPan' );
575 |
576 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
577 |
578 | }
579 |
580 | function handleTouchMoveRotate( event ) {
581 |
582 | //console.log( 'handleTouchMoveRotate' );
583 |
584 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
585 | rotateDelta.subVectors( rotateEnd, rotateStart );
586 |
587 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
588 |
589 | // rotating across whole screen goes 360 degrees around
590 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
591 |
592 | // rotating up and down along whole screen attempts to go 360, but limited to 180
593 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
594 |
595 | rotateStart.copy( rotateEnd );
596 |
597 | scope.update();
598 |
599 | }
600 |
601 | function handleTouchMoveDolly( event ) {
602 |
603 | //console.log( 'handleTouchMoveDolly' );
604 |
605 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
606 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
607 |
608 | var distance = Math.sqrt( dx * dx + dy * dy );
609 |
610 | dollyEnd.set( 0, distance );
611 |
612 | dollyDelta.subVectors( dollyEnd, dollyStart );
613 |
614 | if ( dollyDelta.y > 0 ) {
615 |
616 | dollyOut( getZoomScale() );
617 |
618 | } else if ( dollyDelta.y < 0 ) {
619 |
620 | dollyIn( getZoomScale() );
621 |
622 | }
623 |
624 | dollyStart.copy( dollyEnd );
625 |
626 | scope.update();
627 |
628 | }
629 |
630 | function handleTouchMovePan( event ) {
631 |
632 | //console.log( 'handleTouchMovePan' );
633 |
634 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
635 |
636 | panDelta.subVectors( panEnd, panStart );
637 |
638 | pan( panDelta.x, panDelta.y );
639 |
640 | panStart.copy( panEnd );
641 |
642 | scope.update();
643 |
644 | }
645 |
646 | function handleTouchEnd( event ) {
647 |
648 | //console.log( 'handleTouchEnd' );
649 |
650 | }
651 |
652 | //
653 | // event handlers - FSM: listen for events and reset state
654 | //
655 |
656 | function onMouseDown( event ) {
657 |
658 | if ( scope.enabled === false ) return;
659 |
660 | event.preventDefault();
661 |
662 | if ( event.button === scope.mouseButtons.ORBIT ) {
663 |
664 | if ( scope.enableRotate === false ) return;
665 |
666 | handleMouseDownRotate( event );
667 |
668 | state = STATE.ROTATE;
669 |
670 | } else if ( event.button === scope.mouseButtons.ZOOM ) {
671 |
672 | if ( scope.enableZoom === false ) return;
673 |
674 | handleMouseDownDolly( event );
675 |
676 | state = STATE.DOLLY;
677 |
678 | } else if ( event.button === scope.mouseButtons.PAN ) {
679 |
680 | if ( scope.enablePan === false ) return;
681 |
682 | handleMouseDownPan( event );
683 |
684 | state = STATE.PAN;
685 |
686 | }
687 |
688 | if ( state !== STATE.NONE ) {
689 |
690 | document.addEventListener( 'mousemove', onMouseMove, false );
691 | document.addEventListener( 'mouseup', onMouseUp, false );
692 |
693 | scope.dispatchEvent( startEvent );
694 |
695 | }
696 |
697 | }
698 |
699 | function onMouseMove( event ) {
700 |
701 | if ( scope.enabled === false ) return;
702 |
703 | event.preventDefault();
704 |
705 | if ( state === STATE.ROTATE ) {
706 |
707 | if ( scope.enableRotate === false ) return;
708 |
709 | handleMouseMoveRotate( event );
710 |
711 | } else if ( state === STATE.DOLLY ) {
712 |
713 | if ( scope.enableZoom === false ) return;
714 |
715 | handleMouseMoveDolly( event );
716 |
717 | } else if ( state === STATE.PAN ) {
718 |
719 | if ( scope.enablePan === false ) return;
720 |
721 | handleMouseMovePan( event );
722 |
723 | }
724 |
725 | }
726 |
727 | function onMouseUp( event ) {
728 |
729 | if ( scope.enabled === false ) return;
730 |
731 | handleMouseUp( event );
732 |
733 | document.removeEventListener( 'mousemove', onMouseMove, false );
734 | document.removeEventListener( 'mouseup', onMouseUp, false );
735 |
736 | scope.dispatchEvent( endEvent );
737 |
738 | state = STATE.NONE;
739 |
740 | }
741 |
742 | function onMouseWheel( event ) {
743 |
744 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
745 |
746 | event.preventDefault();
747 | event.stopPropagation();
748 |
749 | handleMouseWheel( event );
750 |
751 | scope.dispatchEvent( startEvent ); // not sure why these are here...
752 | scope.dispatchEvent( endEvent );
753 |
754 | }
755 |
756 | function onKeyDown( event ) {
757 |
758 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
759 |
760 | handleKeyDown( event );
761 |
762 | }
763 |
764 | function onTouchStart( event ) {
765 |
766 | if ( scope.enabled === false ) return;
767 |
768 | switch ( event.touches.length ) {
769 |
770 | case 1: // one-fingered touch: rotate
771 |
772 | if ( scope.enableRotate === false ) return;
773 |
774 | handleTouchStartRotate( event );
775 |
776 | state = STATE.TOUCH_ROTATE;
777 |
778 | break;
779 |
780 | case 2: // two-fingered touch: dolly
781 |
782 | if ( scope.enableZoom === false ) return;
783 |
784 | handleTouchStartDolly( event );
785 |
786 | state = STATE.TOUCH_DOLLY;
787 |
788 | break;
789 |
790 | case 3: // three-fingered touch: pan
791 |
792 | if ( scope.enablePan === false ) return;
793 |
794 | handleTouchStartPan( event );
795 |
796 | state = STATE.TOUCH_PAN;
797 |
798 | break;
799 |
800 | default:
801 |
802 | state = STATE.NONE;
803 |
804 | }
805 |
806 | if ( state !== STATE.NONE ) {
807 |
808 | scope.dispatchEvent( startEvent );
809 |
810 | }
811 |
812 | }
813 |
814 | function onTouchMove( event ) {
815 |
816 | if ( scope.enabled === false ) return;
817 |
818 | event.preventDefault();
819 | event.stopPropagation();
820 |
821 | switch ( event.touches.length ) {
822 |
823 | case 1: // one-fingered touch: rotate
824 |
825 | if ( scope.enableRotate === false ) return;
826 | if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?...
827 |
828 | handleTouchMoveRotate( event );
829 |
830 | break;
831 |
832 | case 2: // two-fingered touch: dolly
833 |
834 | if ( scope.enableZoom === false ) return;
835 | if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?...
836 |
837 | handleTouchMoveDolly( event );
838 |
839 | break;
840 |
841 | case 3: // three-fingered touch: pan
842 |
843 | if ( scope.enablePan === false ) return;
844 | if ( state !== STATE.TOUCH_PAN ) return; // is this needed?...
845 |
846 | handleTouchMovePan( event );
847 |
848 | break;
849 |
850 | default:
851 |
852 | state = STATE.NONE;
853 |
854 | }
855 |
856 | }
857 |
858 | function onTouchEnd( event ) {
859 |
860 | if ( scope.enabled === false ) return;
861 |
862 | handleTouchEnd( event );
863 |
864 | scope.dispatchEvent( endEvent );
865 |
866 | state = STATE.NONE;
867 |
868 | }
869 |
870 | function onContextMenu( event ) {
871 |
872 | event.preventDefault();
873 |
874 | }
875 |
876 | //
877 |
878 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
879 |
880 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
881 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false );
882 |
883 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
884 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
885 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
886 |
887 | window.addEventListener( 'keydown', onKeyDown, false );
888 |
889 | // force an update at start
890 |
891 | this.update();
892 |
893 | };
894 |
895 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
896 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
897 |
898 | Object.defineProperties( THREE.OrbitControls.prototype, {
899 |
900 | center: {
901 |
902 | get: function () {
903 |
904 | console.warn( 'THREE.OrbitControls: .center has been renamed to .target' );
905 | return this.target;
906 |
907 | }
908 |
909 | },
910 |
911 | // backward compatibility
912 |
913 | noZoom: {
914 |
915 | get: function () {
916 |
917 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
918 | return ! this.enableZoom;
919 |
920 | },
921 |
922 | set: function ( value ) {
923 |
924 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
925 | this.enableZoom = ! value;
926 |
927 | }
928 |
929 | },
930 |
931 | noRotate: {
932 |
933 | get: function () {
934 |
935 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
936 | return ! this.enableRotate;
937 |
938 | },
939 |
940 | set: function ( value ) {
941 |
942 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
943 | this.enableRotate = ! value;
944 |
945 | }
946 |
947 | },
948 |
949 | noPan: {
950 |
951 | get: function () {
952 |
953 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
954 | return ! this.enablePan;
955 |
956 | },
957 |
958 | set: function ( value ) {
959 |
960 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
961 | this.enablePan = ! value;
962 |
963 | }
964 |
965 | },
966 |
967 | noKeys: {
968 |
969 | get: function () {
970 |
971 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
972 | return ! this.enableKeys;
973 |
974 | },
975 |
976 | set: function ( value ) {
977 |
978 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
979 | this.enableKeys = ! value;
980 |
981 | }
982 |
983 | },
984 |
985 | staticMoving : {
986 |
987 | get: function () {
988 |
989 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
990 | return ! this.enableDamping;
991 |
992 | },
993 |
994 | set: function ( value ) {
995 |
996 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
997 | this.enableDamping = ! value;
998 |
999 | }
1000 |
1001 | },
1002 |
1003 | dynamicDampingFactor : {
1004 |
1005 | get: function () {
1006 |
1007 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
1008 | return this.dampingFactor;
1009 |
1010 | },
1011 |
1012 | set: function ( value ) {
1013 |
1014 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
1015 | this.dampingFactor = value;
1016 |
1017 | }
1018 |
1019 | }
1020 |
1021 | } );
1022 |
--------------------------------------------------------------------------------
/dependencies/compatibility.js:
--------------------------------------------------------------------------------
1 | /**
2 | * this code is from all around the web :)
3 | * if u want to put some credits u are welcome!
4 | */
5 | var compatibility = (function() {
6 | var lastTime = 0,
7 | isLittleEndian = true,
8 |
9 | URL = window.URL || window.webkitURL,
10 |
11 | requestAnimationFrame = function(callback, element) {
12 | var requestAnimationFrame =
13 | window.requestAnimationFrame ||
14 | window.webkitRequestAnimationFrame ||
15 | window.mozRequestAnimationFrame ||
16 | window.oRequestAnimationFrame ||
17 | window.msRequestAnimationFrame ||
18 | function(callback, element) {
19 | var currTime = new Date().getTime();
20 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
21 | var id = window.setTimeout(function() {
22 | callback(currTime + timeToCall);
23 | }, timeToCall);
24 | lastTime = currTime + timeToCall;
25 | return id;
26 | };
27 |
28 | return requestAnimationFrame.call(window, callback, element);
29 | },
30 |
31 | cancelAnimationFrame = function(id) {
32 | var cancelAnimationFrame = window.cancelAnimationFrame ||
33 | function(id) {
34 | clearTimeout(id);
35 | };
36 | return cancelAnimationFrame.call(window, id);
37 | },
38 |
39 | getUserMedia = function(options, success, error) {
40 | var getUserMedia =
41 | window.navigator.getUserMedia ||
42 | window.navigator.mozGetUserMedia ||
43 | window.navigator.webkitGetUserMedia ||
44 | window.navigator.msGetUserMedia ||
45 | function(options, success, error) {
46 | error();
47 | };
48 |
49 | return getUserMedia.call(window.navigator, options, success, error);
50 | },
51 |
52 | detectEndian = function() {
53 | var buf = new ArrayBuffer(8);
54 | var data = new Uint32Array(buf);
55 | data[0] = 0xff000000;
56 | isLittleEndian = true;
57 | if (buf[0] === 0xff) {
58 | isLittleEndian = false;
59 | }
60 | return isLittleEndian;
61 | };
62 |
63 | return {
64 | URL: URL,
65 | requestAnimationFrame: requestAnimationFrame,
66 | cancelAnimationFrame: cancelAnimationFrame,
67 | getUserMedia: getUserMedia,
68 | detectEndian: detectEndian,
69 | isLittleEndian: isLittleEndian
70 | };
71 | })();
--------------------------------------------------------------------------------
/dependencies/jquery-ui.min.css:
--------------------------------------------------------------------------------
1 | /*! jQuery UI - v1.12.0 - 2016-08-13
2 | * http://jqueryui.com
3 | * Includes: core.css, slider.css, theme.css
4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
5 | * Copyright jQuery Foundation and other contributors; Licensed MIT */
6 |
7 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon,.ui-state-default .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666}
--------------------------------------------------------------------------------
/dependencies/vr/VRControls.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author dmarcos / https://github.com/dmarcos
3 | * @author mrdoob / http://mrdoob.com
4 | */
5 |
6 | THREE.VRControls = function ( object, onError ) {
7 |
8 | var scope = this;
9 |
10 | var vrDisplay, vrDisplays;
11 |
12 | var standingMatrix = new THREE.Matrix4();
13 |
14 | function gotVRDisplays( displays ) {
15 |
16 | vrDisplays = displays;
17 |
18 | for ( var i = 0; i < displays.length; i ++ ) {
19 |
20 | if ( ( 'VRDisplay' in window && displays[ i ] instanceof VRDisplay ) ||
21 | ( 'PositionSensorVRDevice' in window && displays[ i ] instanceof PositionSensorVRDevice ) ) {
22 |
23 | vrDisplay = displays[ i ];
24 | break; // We keep the first we encounter
25 |
26 | }
27 |
28 | }
29 |
30 | if ( vrDisplay === undefined ) {
31 |
32 | if ( onError ) onError( 'VR input not available.' );
33 |
34 | }
35 |
36 | }
37 |
38 | if ( navigator.getVRDisplays ) {
39 |
40 | navigator.getVRDisplays().then( gotVRDisplays );
41 |
42 | } else if ( navigator.getVRDevices ) {
43 |
44 | // Deprecated API.
45 | navigator.getVRDevices().then( gotVRDisplays );
46 |
47 | }
48 |
49 | // the Rift SDK returns the position in meters
50 | // this scale factor allows the user to define how meters
51 | // are converted to scene units.
52 |
53 | this.scale = 1;
54 |
55 | // If true will use "standing space" coordinate system where y=0 is the
56 | // floor and x=0, z=0 is the center of the room.
57 | this.standing = false;
58 |
59 | // Distance from the users eyes to the floor in meters. Used when
60 | // standing=true but the VRDisplay doesn't provide stageParameters.
61 | this.userHeight = 1.6;
62 |
63 | this.getVRDisplay = function () {
64 |
65 | return vrDisplay;
66 |
67 | };
68 |
69 | this.getVRDisplays = function () {
70 |
71 | return vrDisplays;
72 |
73 | };
74 |
75 | this.getStandingMatrix = function () {
76 |
77 | return standingMatrix;
78 |
79 | };
80 |
81 | this.update = function () {
82 |
83 | if ( vrDisplay ) {
84 |
85 | if ( vrDisplay.getPose ) {
86 |
87 | var pose = vrDisplay.getPose();
88 |
89 | if ( pose.orientation !== null ) {
90 |
91 | object.quaternion.fromArray( pose.orientation );
92 |
93 | }
94 |
95 | if ( pose.position !== null ) {
96 |
97 | object.position.fromArray( pose.position );
98 |
99 | } else {
100 |
101 | object.position.set( 0, 0, 0 );
102 |
103 | }
104 |
105 | } else {
106 |
107 | // Deprecated API.
108 | var state = vrDisplay.getState();
109 |
110 | if ( state.orientation !== null ) {
111 |
112 | object.quaternion.copy( state.orientation );
113 |
114 | }
115 |
116 | if ( state.position !== null ) {
117 |
118 | object.position.copy( state.position );
119 |
120 | } else {
121 |
122 | object.position.set( 0, 0, 0 );
123 |
124 | }
125 |
126 | }
127 |
128 | if ( this.standing ) {
129 |
130 | if ( vrDisplay.stageParameters ) {
131 |
132 | object.updateMatrix();
133 |
134 | standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform );
135 | object.applyMatrix( standingMatrix );
136 |
137 | } else {
138 |
139 | object.position.setY( object.position.y + this.userHeight );
140 |
141 | }
142 |
143 | }
144 |
145 | object.position.multiplyScalar( scope.scale );
146 |
147 | }
148 |
149 | };
150 |
151 | this.resetPose = function () {
152 |
153 | if ( vrDisplay ) {
154 |
155 | if ( vrDisplay.resetPose !== undefined ) {
156 |
157 | vrDisplay.resetPose();
158 |
159 | } else if ( vrDisplay.resetSensor !== undefined ) {
160 |
161 | // Deprecated API.
162 | vrDisplay.resetSensor();
163 |
164 | } else if ( vrDisplay.zeroSensor !== undefined ) {
165 |
166 | // Really deprecated API.
167 | vrDisplay.zeroSensor();
168 |
169 | }
170 |
171 | }
172 |
173 | };
174 |
175 | this.resetSensor = function () {
176 |
177 | console.warn( 'THREE.VRControls: .resetSensor() is now .resetPose().' );
178 | this.resetPose();
179 |
180 | };
181 |
182 | this.zeroSensor = function () {
183 |
184 | console.warn( 'THREE.VRControls: .zeroSensor() is now .resetPose().' );
185 | this.resetPose();
186 |
187 | };
188 |
189 | this.dispose = function () {
190 |
191 | vrDisplay = null;
192 |
193 | };
194 |
195 | };
196 |
--------------------------------------------------------------------------------
/dependencies/vr/VREffect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author dmarcos / https://github.com/dmarcos
3 | * @author mrdoob / http://mrdoob.com
4 | *
5 | * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html
6 | *
7 | * Firefox: http://mozvr.com/downloads/
8 | * Chromium: https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list
9 | *
10 | */
11 |
12 | THREE.VREffect = function ( renderer, onError ) {
13 |
14 | var isWebVR1 = true;
15 |
16 | var vrDisplay, vrDisplays;
17 | var eyeTranslationL = new THREE.Vector3();
18 | var eyeTranslationR = new THREE.Vector3();
19 | var renderRectL, renderRectR;
20 | var eyeFOVL, eyeFOVR;
21 |
22 | function gotVRDisplays( displays ) {
23 |
24 | vrDisplays = displays;
25 |
26 | for ( var i = 0; i < displays.length; i ++ ) {
27 |
28 | if ( 'VRDisplay' in window && displays[ i ] instanceof VRDisplay ) {
29 |
30 | vrDisplay = displays[ i ];
31 | isWebVR1 = true;
32 | break; // We keep the first we encounter
33 |
34 | } else if ( 'HMDVRDevice' in window && displays[ i ] instanceof HMDVRDevice ) {
35 |
36 | vrDisplay = displays[ i ];
37 | isWebVR1 = false;
38 | break; // We keep the first we encounter
39 |
40 | }
41 |
42 | }
43 |
44 | if ( vrDisplay === undefined ) {
45 |
46 | if ( onError ) onError( 'HMD not available' );
47 |
48 | }
49 |
50 | }
51 |
52 | if ( navigator.getVRDisplays ) {
53 |
54 | navigator.getVRDisplays().then( gotVRDisplays );
55 |
56 | } else if ( navigator.getVRDevices ) {
57 |
58 | // Deprecated API.
59 | navigator.getVRDevices().then( gotVRDisplays );
60 |
61 | }
62 |
63 | //
64 |
65 | this.isPresenting = false;
66 | this.scale = 1;
67 |
68 | var scope = this;
69 |
70 | var rendererSize = renderer.getSize();
71 | var rendererPixelRatio = renderer.getPixelRatio();
72 |
73 | this.getVRDisplay = function () {
74 |
75 | return vrDisplay;
76 |
77 | };
78 |
79 | this.getVRDisplays = function () {
80 |
81 | return vrDisplays;
82 |
83 | };
84 |
85 | this.setSize = function ( width, height ) {
86 |
87 | rendererSize = { width: width, height: height };
88 |
89 | if ( scope.isPresenting ) {
90 |
91 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
92 | renderer.setPixelRatio( 1 );
93 |
94 | if ( isWebVR1 ) {
95 |
96 | renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false );
97 |
98 | } else {
99 |
100 | renderer.setSize( eyeParamsL.renderRect.width * 2, eyeParamsL.renderRect.height, false );
101 |
102 | }
103 |
104 | } else {
105 |
106 | renderer.setPixelRatio( rendererPixelRatio );
107 | renderer.setSize( width, height );
108 |
109 | }
110 |
111 | };
112 |
113 | // fullscreen
114 |
115 | var canvas = renderer.domElement;
116 | var requestFullscreen;
117 | var exitFullscreen;
118 | var fullscreenElement;
119 | var leftBounds = [ 0.0, 0.0, 0.5, 1.0 ];
120 | var rightBounds = [ 0.5, 0.0, 0.5, 1.0 ];
121 |
122 | function onFullscreenChange() {
123 |
124 | var wasPresenting = scope.isPresenting;
125 | scope.isPresenting = vrDisplay !== undefined && ( vrDisplay.isPresenting || ( ! isWebVR1 && document[ fullscreenElement ] instanceof window.HTMLElement ) );
126 |
127 | if ( scope.isPresenting ) {
128 |
129 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
130 | var eyeWidth, eyeHeight;
131 |
132 | if ( isWebVR1 ) {
133 |
134 | eyeWidth = eyeParamsL.renderWidth;
135 | eyeHeight = eyeParamsL.renderHeight;
136 |
137 | if ( vrDisplay.getLayers ) {
138 |
139 | var layers = vrDisplay.getLayers();
140 | if ( layers.length ) {
141 |
142 | leftBounds = layers[0].leftBounds || [ 0.0, 0.0, 0.5, 1.0 ];
143 | rightBounds = layers[0].rightBounds || [ 0.5, 0.0, 0.5, 1.0 ];
144 |
145 | }
146 |
147 | }
148 |
149 | } else {
150 |
151 | eyeWidth = eyeParamsL.renderRect.width;
152 | eyeHeight = eyeParamsL.renderRect.height;
153 |
154 | }
155 |
156 | if ( !wasPresenting ) {
157 |
158 | rendererPixelRatio = renderer.getPixelRatio();
159 | rendererSize = renderer.getSize();
160 |
161 | renderer.setPixelRatio( 1 );
162 | renderer.setSize( eyeWidth * 2, eyeHeight, false );
163 |
164 | }
165 |
166 | } else if ( wasPresenting ) {
167 |
168 | renderer.setPixelRatio( rendererPixelRatio );
169 | renderer.setSize( rendererSize.width, rendererSize.height );
170 |
171 | }
172 |
173 | }
174 |
175 | if ( canvas.requestFullscreen ) {
176 |
177 | requestFullscreen = 'requestFullscreen';
178 | fullscreenElement = 'fullscreenElement';
179 | exitFullscreen = 'exitFullscreen';
180 | document.addEventListener( 'fullscreenchange', onFullscreenChange, false );
181 |
182 | } else if ( canvas.mozRequestFullScreen ) {
183 |
184 | requestFullscreen = 'mozRequestFullScreen';
185 | fullscreenElement = 'mozFullScreenElement';
186 | exitFullscreen = 'mozCancelFullScreen';
187 | document.addEventListener( 'mozfullscreenchange', onFullscreenChange, false );
188 |
189 | } else {
190 |
191 | requestFullscreen = 'webkitRequestFullscreen';
192 | fullscreenElement = 'webkitFullscreenElement';
193 | exitFullscreen = 'webkitExitFullscreen';
194 | document.addEventListener( 'webkitfullscreenchange', onFullscreenChange, false );
195 |
196 | }
197 |
198 | window.addEventListener( 'vrdisplaypresentchange', onFullscreenChange, false );
199 |
200 | this.setFullScreen = function ( boolean ) {
201 |
202 | return new Promise( function ( resolve, reject ) {
203 |
204 | if ( vrDisplay === undefined ) {
205 |
206 | reject( new Error( 'No VR hardware found.' ) );
207 | return;
208 |
209 | }
210 |
211 | if ( scope.isPresenting === boolean ) {
212 |
213 | resolve();
214 | return;
215 |
216 | }
217 |
218 | if ( isWebVR1 ) {
219 |
220 | if ( boolean ) {
221 |
222 | resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) );
223 |
224 | } else {
225 |
226 | resolve( vrDisplay.exitPresent() );
227 |
228 | }
229 |
230 | } else {
231 |
232 | if ( canvas[ requestFullscreen ] ) {
233 |
234 | canvas[ boolean ? requestFullscreen : exitFullscreen ]( { vrDisplay: vrDisplay } );
235 | resolve();
236 |
237 | } else {
238 |
239 | console.error( 'No compatible requestFullscreen method found.' );
240 | reject( new Error( 'No compatible requestFullscreen method found.' ) );
241 |
242 | }
243 |
244 | }
245 |
246 | } );
247 |
248 | };
249 |
250 | this.requestPresent = function () {
251 |
252 | return this.setFullScreen( true );
253 |
254 | };
255 |
256 | this.exitPresent = function () {
257 |
258 | return this.setFullScreen( false );
259 |
260 | };
261 |
262 | this.requestAnimationFrame = function ( f ) {
263 |
264 | if ( isWebVR1 && vrDisplay !== undefined ) {
265 |
266 | return vrDisplay.requestAnimationFrame( f );
267 |
268 | } else {
269 |
270 | return window.requestAnimationFrame( f );
271 |
272 | }
273 |
274 | };
275 |
276 | this.cancelAnimationFrame = function ( h ) {
277 |
278 | if ( isWebVR1 && vrDisplay !== undefined ) {
279 |
280 | vrDisplay.cancelAnimationFrame( h );
281 |
282 | } else {
283 |
284 | window.cancelAnimationFrame( h );
285 |
286 | }
287 |
288 | };
289 |
290 | this.submitFrame = function () {
291 |
292 | if ( isWebVR1 && vrDisplay !== undefined && scope.isPresenting ) {
293 |
294 | vrDisplay.submitFrame();
295 |
296 | }
297 |
298 | };
299 |
300 | this.autoSubmitFrame = true;
301 |
302 | // render
303 |
304 | var cameraL = new THREE.PerspectiveCamera();
305 | cameraL.layers.enable( 1 );
306 |
307 | var cameraR = new THREE.PerspectiveCamera();
308 | cameraR.layers.enable( 2 );
309 |
310 | this.render = function ( scene, camera, renderTarget, forceClear ) {
311 |
312 | if ( vrDisplay && scope.isPresenting ) {
313 |
314 | var autoUpdate = scene.autoUpdate;
315 |
316 | if ( autoUpdate ) {
317 |
318 | scene.updateMatrixWorld();
319 | scene.autoUpdate = false;
320 |
321 | }
322 |
323 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
324 | var eyeParamsR = vrDisplay.getEyeParameters( 'right' );
325 |
326 | if ( isWebVR1 ) {
327 |
328 | eyeTranslationL.fromArray( eyeParamsL.offset );
329 | eyeTranslationR.fromArray( eyeParamsR.offset );
330 | eyeFOVL = eyeParamsL.fieldOfView;
331 | eyeFOVR = eyeParamsR.fieldOfView;
332 |
333 | } else {
334 |
335 | eyeTranslationL.copy( eyeParamsL.eyeTranslation );
336 | eyeTranslationR.copy( eyeParamsR.eyeTranslation );
337 | eyeFOVL = eyeParamsL.recommendedFieldOfView;
338 | eyeFOVR = eyeParamsR.recommendedFieldOfView;
339 |
340 | }
341 |
342 | if ( Array.isArray( scene ) ) {
343 |
344 | console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' );
345 | scene = scene[ 0 ];
346 |
347 | }
348 |
349 | // When rendering we don't care what the recommended size is, only what the actual size
350 | // of the backbuffer is.
351 | var size = renderer.getSize();
352 | renderRectL = {
353 | x: Math.round( size.width * leftBounds[ 0 ] ),
354 | y: Math.round( size.height * leftBounds[ 1 ] ),
355 | width: Math.round( size.width * leftBounds[ 2 ] ),
356 | height: Math.round(size.height * leftBounds[ 3 ] )
357 | };
358 | renderRectR = {
359 | x: Math.round( size.width * rightBounds[ 0 ] ),
360 | y: Math.round( size.height * rightBounds[ 1 ] ),
361 | width: Math.round( size.width * rightBounds[ 2 ] ),
362 | height: Math.round(size.height * rightBounds[ 3 ] )
363 | };
364 |
365 | if ( renderTarget ) {
366 |
367 | renderer.setRenderTarget( renderTarget );
368 | renderTarget.scissorTest = true;
369 |
370 | } else {
371 |
372 | renderer.setScissorTest( true );
373 |
374 | }
375 |
376 | if ( renderer.autoClear || forceClear ) renderer.clear();
377 |
378 | if ( camera.parent === null ) camera.updateMatrixWorld();
379 |
380 | cameraL.projectionMatrix = fovToProjection( eyeFOVL, true, camera.near, camera.far );
381 | cameraR.projectionMatrix = fovToProjection( eyeFOVR, true, camera.near, camera.far );
382 |
383 | camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale );
384 | camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale );
385 |
386 | var scale = this.scale;
387 | cameraL.translateOnAxis( eyeTranslationL, scale );
388 | cameraR.translateOnAxis( eyeTranslationR, scale );
389 |
390 |
391 | // render left eye
392 | if ( renderTarget ) {
393 |
394 | renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
395 | renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
396 |
397 | } else {
398 |
399 | renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
400 | renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
401 |
402 | }
403 | renderer.render( scene, cameraL, renderTarget, forceClear );
404 |
405 | // render right eye
406 | if ( renderTarget ) {
407 |
408 | renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
409 | renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
410 |
411 | } else {
412 |
413 | renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
414 | renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
415 |
416 | }
417 | renderer.render( scene, cameraR, renderTarget, forceClear );
418 |
419 | if ( renderTarget ) {
420 |
421 | renderTarget.viewport.set( 0, 0, size.width, size.height );
422 | renderTarget.scissor.set( 0, 0, size.width, size.height );
423 | renderTarget.scissorTest = false;
424 | renderer.setRenderTarget( null );
425 |
426 | } else {
427 |
428 | renderer.setScissorTest( false );
429 |
430 | }
431 |
432 | if ( autoUpdate ) {
433 |
434 | scene.autoUpdate = true;
435 |
436 | }
437 |
438 | if ( scope.autoSubmitFrame ) {
439 |
440 | scope.submitFrame();
441 |
442 | }
443 |
444 | return;
445 |
446 | }
447 |
448 | // Regular render mode if not HMD
449 |
450 | renderer.render( scene, camera, renderTarget, forceClear );
451 |
452 | };
453 |
454 | //
455 |
456 | function fovToNDCScaleOffset( fov ) {
457 |
458 | var pxscale = 2.0 / ( fov.leftTan + fov.rightTan );
459 | var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5;
460 | var pyscale = 2.0 / ( fov.upTan + fov.downTan );
461 | var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5;
462 | return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] };
463 |
464 | }
465 |
466 | function fovPortToProjection( fov, rightHanded, zNear, zFar ) {
467 |
468 | rightHanded = rightHanded === undefined ? true : rightHanded;
469 | zNear = zNear === undefined ? 0.01 : zNear;
470 | zFar = zFar === undefined ? 10000.0 : zFar;
471 |
472 | var handednessScale = rightHanded ? - 1.0 : 1.0;
473 |
474 | // start with an identity matrix
475 | var mobj = new THREE.Matrix4();
476 | var m = mobj.elements;
477 |
478 | // and with scale/offset info for normalized device coords
479 | var scaleAndOffset = fovToNDCScaleOffset( fov );
480 |
481 | // X result, map clip edges to [-w,+w]
482 | m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ];
483 | m[ 0 * 4 + 1 ] = 0.0;
484 | m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale;
485 | m[ 0 * 4 + 3 ] = 0.0;
486 |
487 | // Y result, map clip edges to [-w,+w]
488 | // Y offset is negated because this proj matrix transforms from world coords with Y=up,
489 | // but the NDC scaling has Y=down (thanks D3D?)
490 | m[ 1 * 4 + 0 ] = 0.0;
491 | m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ];
492 | m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale;
493 | m[ 1 * 4 + 3 ] = 0.0;
494 |
495 | // Z result (up to the app)
496 | m[ 2 * 4 + 0 ] = 0.0;
497 | m[ 2 * 4 + 1 ] = 0.0;
498 | m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale;
499 | m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar );
500 |
501 | // W result (= Z in)
502 | m[ 3 * 4 + 0 ] = 0.0;
503 | m[ 3 * 4 + 1 ] = 0.0;
504 | m[ 3 * 4 + 2 ] = handednessScale;
505 | m[ 3 * 4 + 3 ] = 0.0;
506 |
507 | mobj.transpose();
508 |
509 | return mobj;
510 |
511 | }
512 |
513 | function fovToProjection( fov, rightHanded, zNear, zFar ) {
514 |
515 | var DEG2RAD = Math.PI / 180.0;
516 |
517 | var fovPort = {
518 | upTan: Math.tan( fov.upDegrees * DEG2RAD ),
519 | downTan: Math.tan( fov.downDegrees * DEG2RAD ),
520 | leftTan: Math.tan( fov.leftDegrees * DEG2RAD ),
521 | rightTan: Math.tan( fov.rightDegrees * DEG2RAD )
522 | };
523 |
524 | return fovPortToProjection( fovPort, rightHanded, zNear, zFar );
525 |
526 | }
527 |
528 | };
529 |
--------------------------------------------------------------------------------
/dependencies/vr/ViveController.js:
--------------------------------------------------------------------------------
1 | THREE.ViveController = function ( id ) {
2 |
3 | THREE.Object3D.call( this );
4 |
5 | var gamepad;
6 |
7 | this.getGamepad = function () { return gamepad; };
8 | this.matrixAutoUpdate = false;
9 | this.standingMatrix = new THREE.Matrix4();
10 |
11 | var scope = this;
12 |
13 | function update() {
14 |
15 | requestAnimationFrame( update );
16 |
17 | gamepad = navigator.getGamepads()[ id ];
18 |
19 | if ( gamepad !== undefined && gamepad.pose !== null ) {
20 |
21 | var pose = gamepad.pose;
22 |
23 | scope.position.fromArray( pose.position );
24 | scope.quaternion.fromArray( pose.orientation );
25 | scope.matrix.compose( scope.position, scope.quaternion, scope.scale );
26 | scope.matrix.multiplyMatrices( scope.standingMatrix, scope.matrix );
27 | scope.matrixWorldNeedsUpdate = true;
28 |
29 | scope.visible = true;
30 |
31 | } else {
32 |
33 | scope.visible = false;
34 |
35 | }
36 |
37 | }
38 |
39 | update();
40 |
41 | };
42 |
43 | THREE.ViveController.prototype = Object.create( THREE.Object3D.prototype );
44 | THREE.ViveController.prototype.constructor = THREE.ViveController;
45 |
--------------------------------------------------------------------------------
/dependencies/vr/WebVR.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author mrdoob / http://mrdoob.com
3 | * Based on @tojiro's vr-samples-utils.js
4 | */
5 |
6 | var WEBVR = {
7 |
8 | isLatestAvailable: function () {
9 |
10 | return navigator.getVRDisplays !== undefined;
11 |
12 | },
13 |
14 | isAvailable: function () {
15 |
16 | return navigator.getVRDisplays !== undefined || navigator.getVRDevices !== undefined;
17 |
18 | },
19 |
20 | getMessage: function () {
21 |
22 | var message;
23 |
24 | if ( navigator.getVRDisplays ) {
25 |
26 | navigator.getVRDisplays().then( function ( displays ) {
27 |
28 | if ( displays.length === 0 ) message = 'WebVR supported, but no VRDisplays found.';
29 |
30 | } );
31 |
32 | } else if ( navigator.getVRDevices ) {
33 |
34 | message = 'Your browser supports WebVR but not the latest version. See webvr.info for more info.';
35 |
36 | } else {
37 |
38 | message = 'Your browser does not support WebVR. See webvr.info for assistance.';
39 |
40 | }
41 |
42 | if ( message !== undefined ) {
43 |
44 | var container = document.createElement( 'div' );
45 | container.style.position = 'absolute';
46 | container.style.left = '0';
47 | container.style.top = '0';
48 | container.style.right = '0';
49 | container.style.zIndex = '999';
50 | container.align = 'center';
51 |
52 | var error = document.createElement( 'div' );
53 | error.style.fontFamily = 'sans-serif';
54 | error.style.fontSize = '16px';
55 | error.style.fontStyle = 'normal';
56 | error.style.lineHeight = '26px';
57 | error.style.backgroundColor = '#fff';
58 | error.style.color = '#000';
59 | error.style.padding = '10px 20px';
60 | error.style.margin = '50px';
61 | error.style.display = 'inline-block';
62 | error.innerHTML = message;
63 | container.appendChild( error );
64 |
65 | return container;
66 |
67 | }
68 |
69 | },
70 |
71 | getButton: function ( effect ) {
72 |
73 | var button = document.createElement( 'button' );
74 | button.style.position = 'absolute';
75 | button.style.left = 'calc(50% - 50px)';
76 | button.style.bottom = '20px';
77 | button.style.width = '100px';
78 | button.style.border = '0';
79 | button.style.padding = '8px';
80 | button.style.cursor = 'pointer';
81 | button.style.backgroundColor = '#000';
82 | button.style.color = '#fff';
83 | button.style.fontFamily = 'sans-serif';
84 | button.style.fontSize = '13px';
85 | button.style.fontStyle = 'normal';
86 | button.style.textAlign = 'center';
87 | button.style.zIndex = '999';
88 | button.textContent = 'ENTER VR';
89 | button.onclick = function() {
90 |
91 | effect.isPresenting ? effect.exitPresent() : effect.requestPresent();
92 |
93 | };
94 |
95 | window.addEventListener( 'vrdisplaypresentchange', function ( event ) {
96 |
97 | button.textContent = effect.isPresenting ? 'EXIT VR' : 'ENTER VR';
98 |
99 | }, false );
100 |
101 | return button;
102 |
103 | }
104 |
105 | };
106 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |