diff options
Diffstat (limited to 'client/util')
| -rw-r--r-- | client/util/index.js | 122 | ||||
| -rw-r--r-- | client/util/math.js | 51 | ||||
| -rw-r--r-- | client/util/vendor/DRACOLoader.js | 518 | ||||
| -rw-r--r-- | client/util/vendor/geometryHelper.js | 106 | ||||
| -rw-r--r-- | client/util/vendor/oktween.js | 159 |
5 files changed, 956 insertions, 0 deletions
diff --git a/client/util/index.js b/client/util/index.js new file mode 100644 index 00000000..d0db0d98 --- /dev/null +++ b/client/util/index.js @@ -0,0 +1,122 @@ +/* Mobile check */ + +export const isiPhone = !!((navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i))) +export const isiPad = !!(navigator.userAgent.match(/iPad/i)) +export const isAndroid = !!(navigator.userAgent.match(/Android/i)) +export const isMobile = isiPhone || isiPad || isAndroid +export const isDesktop = !isMobile + +export const toArray = a => Array.prototype.slice.apply(a) +export const choice = a => a[Math.floor(Math.random() * a.length)] + +const htmlClassList = document.body.parentNode.classList +htmlClassList.add(isDesktop ? 'desktop' : 'mobile') + +/* Default image dimensions */ + +export const widths = { + th: 160, + sm: 320, + md: 640, + lg: 1280, +} + +/* Formatting functions */ + +const acronyms = 'id url cc sa fp md5 sha256'.split(' ').map(s => '_' + s) +const acronymsUpperCase = acronyms.map(s => s.toUpperCase()) + +export const formatName = s => { + acronyms.forEach((acronym, i) => s = s.replace(acronym, acronymsUpperCase[i])) + return s.replace(/_/g, ' ') +} + +// Use to pad frame numbers with zeroes +export const pad = (n, m) => { + let s = String(n || 0) + while (s.length < m) { + s = '0' + s + } + return s +} + +export const courtesyS = (n, s) => n + ' ' + (n === 1 ? s : s + 's') + +export const padSeconds = n => n < 10 ? '0' + n : n + +export const timestamp = (n = 0, fps = 25) => { + n /= fps + let s = padSeconds(Math.round(n) % 60) + n = Math.floor(n / 60) + if (n > 60) { + return Math.floor(n / 60) + ':' + padSeconds(n % 60) + ':' + s + } + return (n % 60) + ':' + s +} + +export const percent = n => (n * 100).toFixed(1) + '%' +export const px = (n, w) => Math.round(n * w) + 'px' +export const clamp = (n, a, b) => n < a ? a : n < b ? n : b + +/* URLs */ + +export const preloadImage = opt => { + let { verified, hash, frame, url } = opt + if (hash && frame) { + url = imageUrl(verified, hash, frame, 'md') + } + const image = new Image() + let loaded = false + image.onload = () => { + if (loaded) return + loaded = true + image.onload = null + } + // console.log(img.src) + image.crossOrigin = 'anonymous' + image.src = url + if (image.complete) { + image.onload() + } +} + +/* AJAX */ + +export const get = (uri, data) => { + let headers = { + Accept: 'application/json, application/xml, text/play, text/html, *.*', + } + let opt = { + method: 'GET', + body: data, + headers, + // credentials: 'include', + } + // console.log(headers) + // headers['X-CSRFToken'] = csrftoken + return fetch(uri, opt).then(res => res.json()) +} + +export const post = (uri, data) => { + let headers + if (data instanceof FormData) { + headers = { + Accept: 'application/json, application/xml, text/play, text/html, *.*', + } + } else { + headers = { + Accept: 'application/json, application/xml, text/play, text/html, *.*', + 'Content-Type': 'application/json; charset=utf-8', + } + data = JSON.stringify(data) + } + let opt = { + method: 'POST', + body: data, + headers, + // credentials: 'include', + } + // console.log(headers) + // headers['X-CSRFToken'] = csrftoken + return fetch(uri, opt).then(res => res.json()) +} diff --git a/client/util/math.js b/client/util/math.js new file mode 100644 index 00000000..064d37c6 --- /dev/null +++ b/client/util/math.js @@ -0,0 +1,51 @@ +export const mod = (n,m) => n-(m * Math.floor(n/m)) +export const clamp = (n,a,b) => n<a?a:n<b?n:b +export const norm = (n,a,b) => (n-a) / (b-a) +export const lerp = (n,a,b) => (b-a)*n+a +export const mix = (n,a,b) => a*(1-n)+b*n +export const randint = (n) => Math.floor(Math.random()*n) +export const randrange = (a,b) => Math.random() * (b-a) + a +export const randsign = () => Math.random() >= 0.5 ? -1 : 1 +export const choice = (a) => a[ Math.floor(Math.random() * a.length) ] +export const angle = (x0,y0,x1,y1) => Math.atan2(y1-y0, x1-x0) +export const dist = (x0,y0,x1,y1) => Math.sqrt(Math.pow(x1-x0, 2) + Math.pow(y1-y0, 2)) +export const xor = (a,b) => { a=!!a; b=!!b; return (a||b) && !(a&&b) } +export const quantize = (a,b) => Math.floor(a/b)*b +export const shuffle = (a) => { + for (var i = a.length; i > 0; i--){ + var r = randint(i) + var swap = a[i-1] + a[i-1] = a[r] + a[r] = swap + } + return a +} +// returns a gaussian random function with the given mean and stdev. +export function gaussian(mean, stdev) { + let y2; + let use_last = false; + return () => { + let y1; + if (use_last) { + y1 = y2; + use_last = false; + } + else { + let x1, x2, w; + do { + x1 = 2.0 * Math.random() - 1.0; + x2 = 2.0 * Math.random() - 1.0; + w = x1 * x1 + x2 * x2; + } while( w >= 1.0); + w = Math.sqrt((-2.0 * Math.log(w))/w); + y1 = x1 * w; + y2 = x2 * w; + use_last = true; + } + + let retval = mean + stdev * y1; + if (retval > 0) + return retval; + return -retval; + } +} diff --git a/client/util/vendor/DRACOLoader.js b/client/util/vendor/DRACOLoader.js new file mode 100644 index 00000000..67c18dcf --- /dev/null +++ b/client/util/vendor/DRACOLoader.js @@ -0,0 +1,518 @@ +// Copyright 2016 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import * as THREE from 'three'; + +/** + * @param {THREE.LoadingManager} manager + */ +const DRACOLoader = function(manager) { + this.timeLoaded = 0; + this.manager = manager || THREE.DefaultLoadingManager; + this.materials = null; + this.verbosity = 0; + this.attributeOptions = {}; + this.drawMode = THREE.TrianglesDrawMode; + // Native Draco attribute type to Three.JS attribute type. + this.nativeAttributeMap = { + 'position' : 'POSITION', + 'normal' : 'NORMAL', + 'color' : 'COLOR', + 'uv' : 'TEX_COORD' + }; +}; + +DRACOLoader.prototype = { + + constructor: DRACOLoader, + + load: function(url, onLoad, onProgress, onError) { + var scope = this; + var loader = new THREE.FileLoader(scope.manager); + loader.setPath(this.path); + loader.setResponseType('arraybuffer'); + if (this.crossOrigin !== undefined) { + loader.crossOrigin = this.crossOrigin; + } + loader.load(url, function(blob) { + scope.decodeDracoFile(blob, onLoad); + }, onProgress, onError); + }, + + setPath: function(value) { + this.path = value; + }, + + setCrossOrigin: function(value) { + this.crossOrigin = value; + }, + + setVerbosity: function(level) { + this.verbosity = level; + }, + + /** + * Sets desired mode for generated geometry indices. + * Can be either: + * THREE.TrianglesDrawMode + * THREE.TriangleStripDrawMode + */ + setDrawMode: function(drawMode) { + this.drawMode = drawMode; + }, + + /** + * Skips dequantization for a specific attribute. + * |attributeName| is the THREE.js name of the given attribute type. + * The only currently supported |attributeName| is 'position', more may be + * added in future. + */ + setSkipDequantization: function(attributeName, skip) { + var skipDequantization = true; + if (typeof skip !== 'undefined') + skipDequantization = skip; + this.getAttributeOptions(attributeName).skipDequantization = + skipDequantization; + }, + + /** + * |attributeUniqueIdMap| specifies attribute unique id for an attribute in + * the geometry to be decoded. The name of the attribute must be one of the + * supported attribute type in Three.JS, including: + * 'position', + * 'color', + * 'normal', + * 'uv', + * 'uv2', + * 'skinIndex', + * 'skinWeight'. + * The format is: + * attributeUniqueIdMap[attributeName] = attributeId + */ + decodeDracoFile: function(rawBuffer, callback, attributeUniqueIdMap, + attributeTypeMap) { + var scope = this; + DRACOLoader.getDecoderModule() + .then( function ( module ) { + scope.decodeDracoFileInternal( rawBuffer, module.decoder, callback, + attributeUniqueIdMap || {}, attributeTypeMap || {}); + }); + }, + + decodeDracoFileInternal: function(rawBuffer, dracoDecoder, callback, + attributeUniqueIdMap, attributeTypeMap) { + /* + * Here is how to use Draco Javascript decoder and get the geometry. + */ + var buffer = new dracoDecoder.DecoderBuffer(); + buffer.Init(new Int8Array(rawBuffer), rawBuffer.byteLength); + var decoder = new dracoDecoder.Decoder(); + + /* + * Determine what type is this file: mesh or point cloud. + */ + var geometryType = decoder.GetEncodedGeometryType(buffer); + if (geometryType == dracoDecoder.TRIANGULAR_MESH) { + if (this.verbosity > 0) { + console.log('Loaded a mesh.'); + } + } else if (geometryType == dracoDecoder.POINT_CLOUD) { + if (this.verbosity > 0) { + console.log('Loaded a point cloud.'); + } + } else { + var errorMsg = 'THREE.DRACOLoader: Unknown geometry type.' + console.error(errorMsg); + throw new Error(errorMsg); + } + callback(this.convertDracoGeometryTo3JS(dracoDecoder, decoder, + geometryType, buffer, attributeUniqueIdMap, attributeTypeMap)); + }, + + addAttributeToGeometry: function(dracoDecoder, decoder, dracoGeometry, + attributeName, attributeType, attribute, + geometry, geometryBuffer) { + if (attribute.ptr === 0) { + var errorMsg = 'THREE.DRACOLoader: No attribute ' + attributeName; + console.error(errorMsg); + throw new Error(errorMsg); + } + + var numComponents = attribute.num_components(); + var numPoints = dracoGeometry.num_points(); + var numValues = numPoints * numComponents; + var attributeData; + var TypedBufferAttribute; + + switch ( attributeType ) { + + case Float32Array: + attributeData = new dracoDecoder.DracoFloat32Array(); + decoder.GetAttributeFloatForAllPoints( + dracoGeometry, attribute, attributeData); + geometryBuffer[ attributeName ] = new Float32Array( numValues ); + TypedBufferAttribute = THREE.Float32BufferAttribute; + break; + + case Int8Array: + attributeData = new dracoDecoder.DracoInt8Array(); + decoder.GetAttributeInt8ForAllPoints( + dracoGeometry, attribute, attributeData ); + geometryBuffer[ attributeName ] = new Int8Array( numValues ); + TypedBufferAttribute = THREE.Int8BufferAttribute; + break; + + case Int16Array: + attributeData = new dracoDecoder.DracoInt16Array(); + decoder.GetAttributeInt16ForAllPoints( + dracoGeometry, attribute, attributeData); + geometryBuffer[ attributeName ] = new Int16Array( numValues ); + TypedBufferAttribute = THREE.Int16BufferAttribute; + break; + + case Int32Array: + attributeData = new dracoDecoder.DracoInt32Array(); + decoder.GetAttributeInt32ForAllPoints( + dracoGeometry, attribute, attributeData); + geometryBuffer[ attributeName ] = new Int32Array( numValues ); + TypedBufferAttribute = THREE.Int32BufferAttribute; + break; + + case Uint8Array: + attributeData = new dracoDecoder.DracoUInt8Array(); + decoder.GetAttributeUInt8ForAllPoints( + dracoGeometry, attribute, attributeData); + geometryBuffer[ attributeName ] = new Uint8Array( numValues ); + TypedBufferAttribute = THREE.Uint8BufferAttribute; + break; + + case Uint16Array: + attributeData = new dracoDecoder.DracoUInt16Array(); + decoder.GetAttributeUInt16ForAllPoints( + dracoGeometry, attribute, attributeData); + geometryBuffer[ attributeName ] = new Uint16Array( numValues ); + TypedBufferAttribute = THREE.Uint16BufferAttribute; + break; + + case Uint32Array: + attributeData = new dracoDecoder.DracoUInt32Array(); + decoder.GetAttributeUInt32ForAllPoints( + dracoGeometry, attribute, attributeData); + geometryBuffer[ attributeName ] = new Uint32Array( numValues ); + TypedBufferAttribute = THREE.Uint32BufferAttribute; + break; + + default: + var errorMsg = 'THREE.DRACOLoader: Unexpected attribute type.'; + console.error( errorMsg ); + throw new Error( errorMsg ); + + } + + // Copy data from decoder. + for (var i = 0; i < numValues; i++) { + geometryBuffer[attributeName][i] = attributeData.GetValue(i); + } + // Add attribute to THREEJS geometry for rendering. + geometry.addAttribute(attributeName, + new TypedBufferAttribute(geometryBuffer[attributeName], + numComponents)); + dracoDecoder.destroy(attributeData); + }, + + convertDracoGeometryTo3JS: function(dracoDecoder, decoder, geometryType, + buffer, attributeUniqueIdMap, + attributeTypeMap) { + if (this.getAttributeOptions('position').skipDequantization === true) { + decoder.SkipAttributeTransform(dracoDecoder.POSITION); + } + var dracoGeometry; + var decodingStatus; + const start_time = performance.now(); + if (geometryType === dracoDecoder.TRIANGULAR_MESH) { + dracoGeometry = new dracoDecoder.Mesh(); + decodingStatus = decoder.DecodeBufferToMesh(buffer, dracoGeometry); + } else { + dracoGeometry = new dracoDecoder.PointCloud(); + decodingStatus = + decoder.DecodeBufferToPointCloud(buffer, dracoGeometry); + } + if (!decodingStatus.ok() || dracoGeometry.ptr == 0) { + var errorMsg = 'THREE.DRACOLoader: Decoding failed: '; + errorMsg += decodingStatus.error_msg(); + console.error(errorMsg); + dracoDecoder.destroy(decoder); + dracoDecoder.destroy(dracoGeometry); + throw new Error(errorMsg); + } + + var decode_end = performance.now(); + dracoDecoder.destroy(buffer); + /* + * Example on how to retrieve mesh and attributes. + */ + var numFaces; + if (geometryType == dracoDecoder.TRIANGULAR_MESH) { + numFaces = dracoGeometry.num_faces(); + if (this.verbosity > 0) { + console.log('Number of faces loaded: ' + numFaces.toString()); + } + } else { + numFaces = 0; + } + + var numPoints = dracoGeometry.num_points(); + var numAttributes = dracoGeometry.num_attributes(); + if (this.verbosity > 0) { + console.log('Number of points loaded: ' + numPoints.toString()); + console.log('Number of attributes loaded: ' + + numAttributes.toString()); + } + + // Verify if there is position attribute. + var posAttId = decoder.GetAttributeId(dracoGeometry, + dracoDecoder.POSITION); + if (posAttId == -1) { + var errorMsg = 'THREE.DRACOLoader: No position attribute found.'; + console.error(errorMsg); + dracoDecoder.destroy(decoder); + dracoDecoder.destroy(dracoGeometry); + throw new Error(errorMsg); + } + var posAttribute = decoder.GetAttribute(dracoGeometry, posAttId); + + // Structure for converting to THREEJS geometry later. + var geometryBuffer = {}; + // Import data to Three JS geometry. + var geometry = new THREE.BufferGeometry(); + + // Add native Draco attribute type to geometry. + for (var attributeName in this.nativeAttributeMap) { + // The native attribute type is only used when no unique Id is + // provided. For example, loading .drc files. + if (attributeUniqueIdMap[attributeName] === undefined) { + var attId = decoder.GetAttributeId(dracoGeometry, + dracoDecoder[this.nativeAttributeMap[attributeName]]); + if (attId !== -1) { + if (this.verbosity > 0) { + console.log('Loaded ' + attributeName + ' attribute.'); + } + var attribute = decoder.GetAttribute(dracoGeometry, attId); + this.addAttributeToGeometry(dracoDecoder, decoder, dracoGeometry, + attributeName, Float32Array, attribute, geometry, geometryBuffer); + } + } + } + + // Add attributes of user specified unique id. E.g. GLTF models. + for (var attributeName in attributeUniqueIdMap) { + var attributeType = attributeTypeMap[attributeName] || Float32Array; + var attributeId = attributeUniqueIdMap[attributeName]; + var attribute = decoder.GetAttributeByUniqueId(dracoGeometry, + attributeId); + this.addAttributeToGeometry(dracoDecoder, decoder, dracoGeometry, + attributeName, attributeType, attribute, geometry, geometryBuffer); + } + + // For mesh, we need to generate the faces. + if (geometryType == dracoDecoder.TRIANGULAR_MESH) { + if (this.drawMode === THREE.TriangleStripDrawMode) { + var stripsArray = new dracoDecoder.DracoInt32Array(); + var numStrips = decoder.GetTriangleStripsFromMesh( + dracoGeometry, stripsArray); + geometryBuffer.indices = new Uint32Array(stripsArray.size()); + for (var i = 0; i < stripsArray.size(); ++i) { + geometryBuffer.indices[i] = stripsArray.GetValue(i); + } + dracoDecoder.destroy(stripsArray); + } else { + var numIndices = numFaces * 3; + geometryBuffer.indices = new Uint32Array(numIndices); + var ia = new dracoDecoder.DracoInt32Array(); + for (var i = 0; i < numFaces; ++i) { + decoder.GetFaceFromMesh(dracoGeometry, i, ia); + var index = i * 3; + geometryBuffer.indices[index] = ia.GetValue(0); + geometryBuffer.indices[index + 1] = ia.GetValue(1); + geometryBuffer.indices[index + 2] = ia.GetValue(2); + } + dracoDecoder.destroy(ia); + } + } + + geometry.drawMode = this.drawMode; + if (geometryType == dracoDecoder.TRIANGULAR_MESH) { + geometry.setIndex(new(geometryBuffer.indices.length > 65535 ? + THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute) + (geometryBuffer.indices, 1)); + } + var posTransform = new dracoDecoder.AttributeQuantizationTransform(); + if (posTransform.InitFromAttribute(posAttribute)) { + // Quantized attribute. Store the quantization parameters into the + // THREE.js attribute. + geometry.attributes['position'].isQuantized = true; + geometry.attributes['position'].maxRange = posTransform.range(); + geometry.attributes['position'].numQuantizationBits = + posTransform.quantization_bits(); + geometry.attributes['position'].minValues = new Float32Array(3); + for (var i = 0; i < 3; ++i) { + geometry.attributes['position'].minValues[i] = + posTransform.min_value(i); + } + } + dracoDecoder.destroy(posTransform); + dracoDecoder.destroy(decoder); + dracoDecoder.destroy(dracoGeometry); + + this.decode_time = decode_end - start_time; + this.import_time = performance.now() - decode_end; + + if (this.verbosity > 0) { + console.log('Decode time: ' + this.decode_time); + console.log('Import time: ' + this.import_time); + } + return geometry; + }, + + isVersionSupported: function(version, callback) { + DRACOLoader.getDecoderModule() + .then( function ( module ) { + callback( module.decoder.isVersionSupported( version ) ); + }); + }, + + getAttributeOptions: function(attributeName) { + if (typeof this.attributeOptions[attributeName] === 'undefined') + this.attributeOptions[attributeName] = {}; + return this.attributeOptions[attributeName]; + } +}; + +DRACOLoader.decoderPath = './'; +DRACOLoader.decoderConfig = {}; +DRACOLoader.decoderModulePromise = null; + +/** + * Sets the base path for decoder source files. + * @param {string} path + */ +DRACOLoader.setDecoderPath = function ( path ) { + DRACOLoader.decoderPath = path; +}; + +/** + * Sets decoder configuration and releases singleton decoder module. Module + * will be recreated with the next decoding call. + * @param {Object} config + */ +DRACOLoader.setDecoderConfig = function ( config ) { + var wasmBinary = DRACOLoader.decoderConfig.wasmBinary; + DRACOLoader.decoderConfig = config || {}; + DRACOLoader.releaseDecoderModule(); + + // Reuse WASM binary. + if ( wasmBinary ) DRACOLoader.decoderConfig.wasmBinary = wasmBinary; +}; + +/** + * Releases the singleton DracoDecoderModule instance. Module will be recreated + * with the next decoding call. + */ +DRACOLoader.releaseDecoderModule = function () { + DRACOLoader.decoderModulePromise = null; +}; + +/** + * Gets WebAssembly or asm.js singleton instance of DracoDecoderModule + * after testing for browser support. Returns Promise that resolves when + * module is available. + * @return {Promise<{decoder: DracoDecoderModule}>} + */ +DRACOLoader.getDecoderModule = function () { + var scope = this; + var path = DRACOLoader.decoderPath; + var config = DRACOLoader.decoderConfig; + var promise = DRACOLoader.decoderModulePromise; + + if ( promise ) return promise; + + // Load source files. + if ( typeof DracoDecoderModule !== 'undefined' ) { + // Loaded externally. + promise = Promise.resolve(); + } else if ( typeof WebAssembly !== 'object' || config.type === 'js' ) { + // Load with asm.js. + promise = DRACOLoader._loadScript( path + 'draco_decoder.js' ); + } else { + // Load with WebAssembly. + config.wasmBinaryFile = path + 'draco_decoder.wasm'; + promise = DRACOLoader._loadScript( path + 'draco_wasm_wrapper.js' ) + .then( function () { + return DRACOLoader._loadArrayBuffer( config.wasmBinaryFile ); + } ) + .then( function ( wasmBinary ) { + config.wasmBinary = wasmBinary; + } ); + } + + // Wait for source files, then create and return a decoder. + promise = promise.then( function () { + return new Promise( function ( resolve ) { + config.onModuleLoaded = function ( decoder ) { + scope.timeLoaded = performance.now(); + // Module is Promise-like. Wrap before resolving to avoid loop. + resolve( { decoder: decoder } ); + }; + DracoDecoderModule( config ); + } ); + } ); + + DRACOLoader.decoderModulePromise = promise; + return promise; +}; + +/** + * @param {string} src + * @return {Promise} + */ +DRACOLoader._loadScript = function ( src ) { + var prevScript = document.getElementById( 'decoder_script' ); + if ( prevScript !== null ) { + prevScript.parentNode.removeChild( prevScript ); + } + var head = document.getElementsByTagName( 'head' )[ 0 ]; + var script = document.createElement( 'script' ); + script.id = 'decoder_script'; + script.type = 'text/javascript'; + script.src = src; + return new Promise( function ( resolve ) { + script.onload = resolve; + head.appendChild( script ); + }); +}; + +/** + * @param {string} src + * @return {Promise} + */ +DRACOLoader._loadArrayBuffer = function ( src ) { + var loader = new THREE.FileLoader(); + loader.setResponseType( 'arraybuffer' ); + return new Promise( function( resolve, reject ) { + loader.load( src, resolve, undefined, reject ); + }); +}; + +export default DRACOLoader diff --git a/client/util/vendor/geometryHelper.js b/client/util/vendor/geometryHelper.js new file mode 100644 index 00000000..7f699722 --- /dev/null +++ b/client/util/vendor/geometryHelper.js @@ -0,0 +1,106 @@ +// Copyright 2017 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import * as THREE from 'three'; + +/** + * @fileoverview Helper class implementing various utilities for THREE.js + * geometry. + */ + +function GeometryHelper() {} + +GeometryHelper.prototype = { + constructor: GeometryHelper, + + // Computes vertex normals on THREE.js buffer geometry, even when the mesh + // uses triangle strip connectivity. + computeVertexNormals: function (bufferGeometry) { + if (bufferGeometry.drawMode === THREE.TrianglesDrawMode) { + bufferGeometry.computeVertexNormals(); + return; + } else if (bufferGeometry.drawMode === THREE.TriangleStripDrawMode) { + if (bufferGeometry.attributes.position === undefined) { + return; + } + const inPositions = bufferGeometry.attributes.position.array; + if (bufferGeometry.attributes.normal === undefined) { + bufferGeometry.addAttribute( + 'normal', + new THREE.BufferAttribute(new Float32Array(inPositions.length), + 3)); + } else { + // Reset existing normals to zero. + const array = bufferGeometry.attributes.normal.array; + for (let i = 0; i < array.length; ++i) { + array[ i ] = 0; + } + } + let outNormals = bufferGeometry.attributes.normal.array; + + let pos0 = new THREE.Vector3(); + let pos1 = new THREE.Vector3(); + let pos2 = new THREE.Vector3(); + let posDif0 = new THREE.Vector3(), posDif1 = new THREE.Vector3(); + let localNormal = new THREE.Vector3(); + + const stripIndices = bufferGeometry.index.array; + for (let i = 2; i < stripIndices.length; ++i) { + let index0 = stripIndices[i - 2] * 3; + let index1 = stripIndices[i - 1] * 3; + let index2 = stripIndices[i] * 3; + // Skip degenerate triangles. + if (index0 === index1 || index0 === index2 || index1 === index2) { + continue; + } + if ((i & 1) !== 0) { + // Swap index 1 and 0 on odd indexed triangles. + const tmpIndex = index1; + index1 = index2; + index2 = tmpIndex; + } + + // Get position values. + pos0.fromArray(inPositions, index0); + pos1.fromArray(inPositions, index1); + pos2.fromArray(inPositions, index2); + + // Position differences + posDif0.subVectors(pos2, pos0); + posDif1.subVectors(pos1, pos0); + + // Weighted normal. + localNormal.crossVectors(posDif1, posDif0); + + // Update normals on vertices + outNormals[index0] += localNormal.x; + outNormals[index0 + 1] += localNormal.y; + outNormals[index0 + 2] += localNormal.z; + + outNormals[index1] += localNormal.x; + outNormals[index1 + 1] += localNormal.y; + outNormals[index1 + 2] += localNormal.z; + + outNormals[index2] += localNormal.x; + outNormals[index2 + 1] += localNormal.y; + outNormals[index2 + 2] += localNormal.z; + } + bufferGeometry.normalizeNormals(); + bufferGeometry.attributes.normal.needsUpdate = true; + } + }, +}; + +export default GeometryHelper diff --git a/client/util/vendor/oktween.js b/client/util/vendor/oktween.js new file mode 100644 index 00000000..cbf5d835 --- /dev/null +++ b/client/util/vendor/oktween.js @@ -0,0 +1,159 @@ +/* + oktween.add({ + obj: el.style, + units: "px", + from: { left: 0 }, + to: { left: 100 }, + duration: 1000, + easing: oktween.easing.circ_out, + update: function(obj){ + console.log(obj.left) + } + finished: function(){ + console.log("done") + } + }) +*/ + +import { lerp } from '../../util/math' + +const oktween = {} +let tweens = [] + +let last_t = 0 +let id = 0 + +oktween.speed = 1 +oktween.add = (tween) => { + tween.id = id++ + tween.obj = tween.obj || {} + if (tween.easing) { + if (typeof tween.easing === "string") { + tween.easing = oktween.easing[tween.easing] + } + } else { + tween.easing = oktween.easing.linear + } + if (!('from' in tween) && !('to' in tween)) { + tween.keys = [] + } else if (!('from' in tween)) { + tween.from = {} + tween.keys = Object.keys(tween.to) + tween.keys.forEach(function(prop) { + tween.from[prop] = parseFloat(tween.obj[prop]) + }) + } else { + tween.keys = Object.keys(tween.from) + } + tween.delay = tween.delay || 0 + tween.start = last_t + tween.delay + tween.done = false + tween.after = tween.after || [] + tween.then = (fn) => { tween.after.push(fn); return tween } + tween.tick = 0 + tween.skip = tween.skip || 1 + tween.dt = 0 + tweens.push(tween) + return tween +} +oktween.update = (t) => { + let done = false + requestAnimationFrame(oktween.update) + last_t = t * oktween.speed + if (tweens.length === 0) return + tweens.forEach((tween, i) => { + const dt = Math.min(1.0, (t - tween.start) / tween.duration) + tween.tick++ + if (dt < 0 || (dt < 1 && (tween.tick % tween.skip != 0))) return + const ddt = tween.easing(dt) + tween.dt = ddt + tween.keys.forEach((prop) => { + let val = lerp(ddt, tween.from[prop], tween.to[prop]) + if (tween.round) val = Math.round(val) + if (tween.units) val = (Math.round(val)) + tween.units + tween.obj[prop] = val + }) + tween.update && tween.update(tween.obj, dt) + if (dt == 1) { + tween.finished && tween.finished(tween) + if (tween.after.length) { + var twn = tween.after.shift() + twn.obj = twn.obj || tween.obj + twn.after = tween.after + oktween.add(twn) + } + if (tween.loop) { + tween.start = t + tween.delay + } + else { + done = true + tween.done = true + } + } + }) + if (done) { + tweens = tweens.filter(function(tween){ return ! tween.done }) + } +} + +requestAnimationFrame(oktween.update) + +oktween.easing = { + linear: (t) => { + return t + }, + circ_out: (t) => { + return Math.sqrt(1 - (t = t - 1) * t) + }, + circ_in: (t) => { + return -(Math.sqrt(1 - (t * t)) - 1) + }, + circ_in_out: (t) => { + return ((t*=2) < 1) ? -0.5 * (Math.sqrt(1 - t * t) - 1) : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1) + }, + quad_in: (n) => { + return Math.pow(n, 2) + }, + quad_out: (n) => { + return n * (n - 2) * -1 + }, + quad_in_out: (n) => { + n = n * 2 + if (n < 1) { return Math.pow(n, 2) / 2 } + return -1 * ((--n) * (n - 2) - 1) / 2 + }, + cubic_bezier: (mX1, mY1, mX2, mY2) => { + function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1 } + function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1 } + function C(aA1) { return 3.0 * aA1 } + + // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + function CalcBezier(aT, aA1, aA2) { + return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT + } + + // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + function GetSlope(aT, aA1, aA2) { + return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1) + } + + function GetTForX(aX) { + // Newton raphson iteration + let aGuessT = aX + for (let i = 0; i < 10; ++i) { + const currentSlope = GetSlope(aGuessT, mX1, mX2) + if (currentSlope == 0.0) return aGuessT + const currentX = CalcBezier(aGuessT, mX1, mX2) - aX + aGuessT -= currentX / currentSlope + } + return aGuessT + } + + return function (aX) { + if (mX1 == mY1 && mX2 == mY2) return aX // linear + return CalcBezier(aX, mY1, mY2) + } + } +} + +export default oktween |
