'use strict' var createCamera = require('./camera.js') var createAxes = require('gl-axes3d') var axesRanges = require('gl-axes3d/properties') var createSpikes = require('gl-spikes3d') var createSelect = require('gl-select-static') var createFBO = require('gl-fbo') var drawTriangle = require('a-big-triangle') var mouseChange = require('mouse-change') var perspective = require('gl-mat4/perspective') var ortho = require('gl-mat4/ortho') var createShader = require('./lib/shader') var isMobile = require('is-mobile')({ tablet: true, featureDetect: true }) module.exports = { createScene: createScene, createCamera: createCamera } function MouseSelect() { this.mouse = [-1,-1] this.screen = null this.distance = Infinity this.index = null this.dataCoordinate = null this.dataPosition = null this.object = null this.data = null } function getContext(canvas, options) { var gl = null try { gl = canvas.getContext('webgl', options) if(!gl) { gl = canvas.getContext('experimental-webgl', options) } } catch(e) { return null } return gl } function roundUpPow10(x) { var y = Math.round(Math.log(Math.abs(x)) / Math.log(10)) if(y < 0) { var base = Math.round(Math.pow(10, -y)) return Math.ceil(x*base) / base } else if(y > 0) { var base = Math.round(Math.pow(10, y)) return Math.ceil(x/base) * base } return Math.ceil(x) } function defaultBool(x) { if(typeof x === 'boolean') { return x } return true } function createScene(options) { options = options || {} options.camera = options.camera || {} var canvas = options.canvas if(!canvas) { canvas = document.createElement('canvas') if(options.container) { var container = options.container container.appendChild(canvas) } else { document.body.appendChild(canvas) } } var gl = options.gl if(!gl) { if(options.glOptions) { isMobile = !!options.glOptions.preserveDrawingBuffer } gl = getContext(canvas, options.glOptions || { premultipliedAlpha: true, antialias: true, preserveDrawingBuffer: isMobile }) } if(!gl) { throw new Error('webgl not supported') } //Initial bounds var bounds = options.bounds || [[-10,-10,-10], [10,10,10]] //Create selection var selection = new MouseSelect() //Accumulation buffer var accumBuffer = createFBO(gl, gl.drawingBufferWidth, gl.drawingBufferHeight, { preferFloat: !isMobile }) var accumShader = createShader(gl) var isOrtho = (options.cameraObject && options.cameraObject._ortho === true) || (options.camera.projection && options.camera.projection.type === 'orthographic') || false //Create a camera var cameraOptions = { eye: options.camera.eye || [2,0,0], center: options.camera.center || [0,0,0], up: options.camera.up || [0,1,0], zoomMin: options.camera.zoomMax || 0.1, zoomMax: options.camera.zoomMin || 100, mode: options.camera.mode || 'turntable', _ortho: isOrtho } //Create axes var axesOptions = options.axes || {} var axes = createAxes(gl, axesOptions) axes.enable = !axesOptions.disable //Create spikes var spikeOptions = options.spikes || {} var spikes = createSpikes(gl, spikeOptions) //Object list is empty initially var objects = [] var pickBufferIds = [] var pickBufferCount = [] var pickBuffers = [] //Dirty flag, skip redraw if scene static var dirty = true var pickDirty = true var projection = new Array(16) var model = new Array(16) var cameraParams = { view: null, projection: projection, model: model, _ortho: false } var pickDirty = true var viewShape = [ gl.drawingBufferWidth, gl.drawingBufferHeight ] var camera = options.cameraObject || createCamera(canvas, cameraOptions) //Create scene object var scene = { gl: gl, contextLost: false, pixelRatio: options.pixelRatio || 1, canvas: canvas, selection: selection, camera: camera, axes: axes, axesPixels: null, spikes: spikes, bounds: bounds, objects: objects, shape: viewShape, aspect: options.aspectRatio || [1,1,1], pickRadius: options.pickRadius || 10, zNear: options.zNear || 0.01, zFar: options.zFar || 1000, fovy: options.fovy || Math.PI/4, clearColor: options.clearColor || [0,0,0,0], autoResize: defaultBool(options.autoResize), autoBounds: defaultBool(options.autoBounds), autoScale: !!options.autoScale, autoCenter: defaultBool(options.autoCenter), clipToBounds: defaultBool(options.clipToBounds), snapToData: !!options.snapToData, onselect: options.onselect || null, onrender: options.onrender || null, onclick: options.onclick || null, cameraParams: cameraParams, oncontextloss: null, mouseListener: null, _stopped: false, getAspectratio: function() { return { x: this.aspect[0], y: this.aspect[1], z: this.aspect[2] } }, setAspectratio: function(aspectratio) { this.aspect[0] = aspectratio.x this.aspect[1] = aspectratio.y this.aspect[2] = aspectratio.z pickDirty = true }, setBounds: function(axisIndex, range) { this.bounds[0][axisIndex] = range.min this.bounds[1][axisIndex] = range.max }, setClearColor: function(clearColor) { this.clearColor = clearColor }, clearRGBA: function() { this.gl.clearColor( this.clearColor[0], this.clearColor[1], this.clearColor[2], this.clearColor[3] ) this.gl.clear( this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT ) } } var pickShape = [ (gl.drawingBufferWidth/scene.pixelRatio)|0, (gl.drawingBufferHeight/scene.pixelRatio)|0 ] function resizeListener() { if(scene._stopped) { return } if(!scene.autoResize) { return } var parent = canvas.parentNode var width = 1 var height = 1 if(parent && parent !== document.body) { width = parent.clientWidth height = parent.clientHeight } else { width = window.innerWidth height = window.innerHeight } var nextWidth = Math.ceil(width * scene.pixelRatio)|0 var nextHeight = Math.ceil(height * scene.pixelRatio)|0 if(nextWidth !== canvas.width || nextHeight !== canvas.height) { canvas.width = nextWidth canvas.height = nextHeight var style = canvas.style style.position = style.position || 'absolute' style.left = '0px' style.top = '0px' style.width = width + 'px' style.height = height + 'px' dirty = true } } if(scene.autoResize) { resizeListener() } window.addEventListener('resize', resizeListener) function reallocPickIds() { var numObjs = objects.length var numPick = pickBuffers.length for(var i=0; i 0 && pickBufferCount[numPick-1] === 0) { pickBufferCount.pop() pickBuffers.pop().dispose() } } scene.update = function(options) { if(scene._stopped) { return } options = options || {} dirty = true pickDirty = true } scene.add = function(obj) { if(scene._stopped) { return } obj.axes = axes objects.push(obj) pickBufferIds.push(-1) dirty = true pickDirty = true reallocPickIds() } scene.remove = function(obj) { if(scene._stopped) { return } var idx = objects.indexOf(obj) if(idx < 0) { return } objects.splice(idx, 1) pickBufferIds.pop() dirty = true pickDirty = true reallocPickIds() } scene.dispose = function() { if(scene._stopped) { return } scene._stopped = true window.removeEventListener('resize', resizeListener) canvas.removeEventListener('webglcontextlost', checkContextLoss) scene.mouseListener.enabled = false if(scene.contextLost) { return } //Destroy objects axes.dispose() spikes.dispose() for(var i=0; i selection.distance) { continue } for(var j=0; j