diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bcdde66 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "standard" + ] +} \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..a16b34f --- /dev/null +++ b/app.js @@ -0,0 +1,35 @@ +// import Utilities from "./utilities.js" +import WrappedGL from './wrappedgl.js' +import FluidParticles from './fluidparticles.js' + +function concatenateWords (list) { + if (list.length === 0) { + return '' + } else if (list.length === 1) { + return "'" + list[0] + "'" + } else { + var result = '' + for (var i = 0; i < list.length; ++i) { + result += "'" + list[i] + "'" + if (i < list.length - 1) { + result += i < list.length - 2 ? ', ' : ' and ' + } + } + + return result + } +} + +WrappedGL.checkWebGLSupportWithExtensions(['ANGLE_instanced_arrays', 'WEBGL_depth_texture', 'OES_texture_float', 'OES_texture_float_linear', 'OES_texture_half_float', 'OES_texture_half_float_linear'], + function () { // we have webgl + document.getElementById('placeholder').outerHTML = document.getElementById('main').innerHTML + var fluidBox = new FluidParticles() + }, function (hasWebGL, unsupportedExtensions) { + document.getElementById('placeholder').outerHTML = document.getElementById('no-support').innerHTML + if (!hasWebGL) { // webgl not supported + document.getElementById('error').textContent = 'Unfortunately, your browser does not support WebGL' + } else { + document.getElementById('error').textContent = 'Unfortunately, your browser does not support the ' + concatenateWords(unsupportedExtensions) + ' WebGL extension' + (unsupportedExtensions.length > 1 ? 's.' : '.') + } + } +) diff --git a/boxeditor.js b/boxeditor.js index 98a41be..9fd9f6f 100644 --- a/boxeditor.js +++ b/boxeditor.js @@ -1,68 +1,74 @@ -var BoxEditor = (function () { - - //min and max are both number[3] - function AABB (min, max) { - this.min = [min[0], min[1], min[2]]; - this.max = [max[0], max[1], max[2]]; - } - - AABB.prototype.computeVolume = function () { - var volume = 1; - for (var i = 0; i < 3; ++i) { - volume *= (this.max[i] - this.min[i]); - } - return volume; - } - - AABB.prototype.computeSurfaceArea = function () { - var width = this.max[0] - this.min[0]; - var height = this.max[1] - this.min[1]; - var depth = this.max[2] - this.min[2]; - - return 2 * (width * height + width * depth + height * depth); +import Utilities from './utilities.js' + +// return { +// BoxEditor: BoxEditor, +// AABB: AABB, +// InteractionMode: InteractionMode +// } + +// min and max are both number[3] +export class AABB { + constructor (min, max) { + this.min = [min[0], min[1], min[2]] + this.max = [max[0], max[1], max[2]] + } + + computeVolume () { + var volume = 1 + for (var i = 0; i < 3; ++i) { + volume *= (this.max[i] - this.min[i]) } - - //returns new AABB with the same min and max (but not the same array references) - AABB.prototype.clone = function () { - return new AABB( - [this.min[0], this.min[1], this.min[2]], - [this.max[0], this.max[1], this.max[2]] - ); - } - - AABB.prototype.randomPoint = function () { //random point in this AABB - var point = []; - for (var i = 0; i < 3; ++i) { - point[i] = this.min[i] + Math.random() * (this.max[i] - this.min[i]); - } - return point; + return volume + } + + computeSurfaceArea () { + var width = this.max[0] - this.min[0] + var height = this.max[1] - this.min[1] + var depth = this.max[2] - this.min[2] + + return 2 * (width * height + width * depth + height * depth) + } + + // returns new AABB with the same min and max (but not the same array references) + clone () { + return new AABB( + [this.min[0], this.min[1], this.min[2]], + [this.max[0], this.max[1], this.max[2]] + ) + } + + randomPoint () { + var point = [] + for (var i = 0; i < 3; ++i) { + point[i] = this.min[i] + Math.random() * (this.max[i] - this.min[i]) } + return point + } +} - var InteractionMode = { - RESIZING: 0, - TRANSLATING: 1, +export const InteractionMode = { + RESIZING: 0, + TRANSLATING: 1, - DRAWING: 2, //whilst we're drawing a rectangle on a plane - EXTRUDING: 3 //whilst we're extruding that rectangle into a box - }; + DRAWING: 2, // whilst we're drawing a rectangle on a plane + EXTRUDING: 3 // whilst we're extruding that rectangle into a box +} - var STEP = 1.0; - +var STEP = 1.0 - function exclusiveAABBOverlap (a, b) { - return a.min[0] < b.max[0] && a.max[0] > b.min[0] && - a.min[1] < b.max[1] && a.max[1] > b.min[1] && - a.min[2] < b.max[2] && a.max[2] > b.min[2]; - } - - function inclusiveAABBOverlap (a, b) { - return a.min[0] <= b.max[0] && a.max[0] >= b.min[0] && - a.min[1] <= b.max[1] && a.max[1] >= b.min[1] && - a.min[2] <= b.max[2] && a.max[2] >= b.min[2]; - } +function exclusiveAABBOverlap (a, b) { + return a.min[0] < b.max[0] && a.max[0] > b.min[0] && + a.min[1] < b.max[1] && a.max[1] > b.min[1] && + a.min[2] < b.max[2] && a.max[2] > b.min[2] +} +function inclusiveAABBOverlap (a, b) { + return a.min[0] <= b.max[0] && a.max[0] >= b.min[0] && + a.min[1] <= b.max[1] && a.max[1] >= b.min[1] && + a.min[2] <= b.max[2] && a.max[2] >= b.min[2] +} - /* +/* if there is an intersection then this returns: { aabb: aabb, @@ -75,1021 +81,937 @@ var BoxEditor = (function () { side: -1 or 1 depending on which side the intersection happened on } - otherwise it returns null */ - function rayAABBIntersection (rayOrigin, rayDirection, aabb) { - //we see it as a series of clippings in t of the line in the AABB planes along each axis - //the part we are left with after clipping if successful is the region of the line within the AABB and thus we can extract the intersection - - //the part of the line we have clipped so far - var lowT = -Infinity; - var highT = Infinity; - - var intersectionAxis = 0; - - for (var i = 0; i < 3; ++i) { - var t1 = (aabb.min[i] - rayOrigin[i]) / rayDirection[i]; - var t2 = (aabb.max[i] - rayOrigin[i]) / rayDirection[i]; - //so between t1 and t2 we are within the aabb planes in this dimension - - //ensure t1 < t2 (swap if necessary) - if (t1 > t2) { - var temp = t1; - t1 = t2; - t2 = temp; - } - - //t1 and t2 now hold the lower and upper intersection t's respectively - - //the part of the line we just clipped for does not overlap the part previously clipped and thus there is no intersection - if (t2 < lowT || t1 > highT) return null; - - //further clip the line between the planes in this axis - if (t1 > lowT) { - lowT = t1; - - intersectionAxis = i; //if we needed to futher clip in this axis then this is the closest intersection axis - } - - if (t2 < highT) highT = t2; - } - - if (lowT > highT) return null; +function rayAABBIntersection (rayOrigin, rayDirection, aabb) { + // we see it as a series of clippings in t of the line in the AABB planes along each axis + // the part we are left with after clipping if successful is the region of the line within the AABB and thus we can extract the intersection - //if we've reached this far then there is an intersection + // the part of the line we have clipped so far + var lowT = -Infinity + var highT = Infinity - var intersection = []; - for (var i = 0; i < 3; ++i) { - intersection[i] = rayOrigin[i] + rayDirection[i] * lowT; - } + var intersectionAxis = 0 + for (var i = 0; i < 3; ++i) { + var t1 = (aabb.min[i] - rayOrigin[i]) / rayDirection[i] + var t2 = (aabb.max[i] - rayOrigin[i]) / rayDirection[i] + // so between t1 and t2 we are within the aabb planes in this dimension - return { - aabb: aabb, - t: lowT, - axis: intersectionAxis, - side: rayDirection[intersectionAxis] > 0 ? -1 : 1, - point: intersection - }; + // ensure t1 < t2 (swap if necessary) + if (t1 > t2) { + var temp = t1 + t1 = t2 + t2 = temp } - //finds the closest points between the line1 and line2 - //returns [closest point on line1, closest point on line2] - function closestPointsOnLines (line1Origin, line1Direction, line2Origin, line2Direction) { - var w0 = Utilities.subtractVectors([], line1Origin, line2Origin); - - var a = Utilities.dotVectors(line1Direction, line1Direction); - var b = Utilities.dotVectors(line1Direction, line2Direction); - var c = Utilities.dotVectors(line2Direction, line2Direction); - var d = Utilities.dotVectors(line1Direction, w0); - var e = Utilities.dotVectors(line2Direction, w0); + // t1 and t2 now hold the lower and upper intersection t's respectively + // the part of the line we just clipped for does not overlap the part previously clipped and thus there is no intersection + if (t2 < lowT || t1 > highT) return null - var t1 = (b * e - c * d) / (a * c - b * b); - var t2 = (a * e - b * d) / (a * c - b * b); + // further clip the line between the planes in this axis + if (t1 > lowT) { + lowT = t1 - return [ - Utilities.addVectors([], line1Origin, Utilities.multiplyVectorByScalar([], line1Direction, t1)), - Utilities.addVectors([], line2Origin, Utilities.multiplyVectorByScalar([], line2Direction, t2)) - ]; + intersectionAxis = i // if we needed to futher clip in this axis then this is the closest intersection axis } - //this defines the bounds of our editing space - //the grid starts at (0, 0, 0) - //gridSize is [width, height, depth] - //onChange is a callback that gets called anytime a box gets edited - function BoxEditor (canvas, wgl, projectionMatrix, camera, gridSize, onLoaded, onChange) { - this.canvas = canvas; - - this.wgl = wgl; - - this.gridWidth = gridSize[0]; - this.gridHeight = gridSize[1]; - this.gridDepth = gridSize[2]; - this.gridDimensions = [this.gridWidth, this.gridHeight, this.gridDepth]; - - this.projectionMatrix = projectionMatrix; - this.camera = camera; - - this.onChange = onChange; - - //the cube geometry is a 1x1 cube with the origin at the bottom left corner - - this.cubeVertexBuffer = wgl.createBuffer(); - wgl.bufferData(this.cubeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([ - // Front face - 0.0, 0.0, 1.0, - 1.0, 0.0, 1.0, - 1.0, 1.0, 1.0, - 0.0, 1.0, 1.0, - - // Back face - 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 1.0, 1.0, 0.0, - 1.0, 0.0, 0.0, - - // Top face - 0.0, 1.0, 0.0, - 0.0, 1.0, 1.0, - 1.0, 1.0, 1.0, - 1.0, 1.0, 0.0, - - // Bottom face - 0.0, 0.0, 0.0, - 1.0, 0.0, 0.0, - 1.0, 0.0, 1.0, - 0.0, 0.0, 1.0, - - // Right face - 1.0, 0.0, 0.0, - 1.0, 1.0, 0.0, - 1.0, 1.0, 1.0, - 1.0, 0.0, 1.0, - - // Left face - 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0, - 0.0, 1.0, 1.0, - 0.0, 1.0, 0.0 - ]), wgl.STATIC_DRAW); - - - - this.cubeIndexBuffer = wgl.createBuffer(); - wgl.bufferData(this.cubeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ - 0, 1, 2, 0, 2, 3, // front - 4, 5, 6, 4, 6, 7, // back - 8, 9, 10, 8, 10, 11, // top - 12, 13, 14, 12, 14, 15, // bottom - 16, 17, 18, 16, 18, 19, // right - 20, 21, 22, 20, 22, 23 // left - ]), wgl.STATIC_DRAW); - - - this.cubeWireframeVertexBuffer = wgl.createBuffer(); - wgl.bufferData(this.cubeWireframeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([ - 0.0, 0.0, 0.0, - 1.0, 0.0, 0.0, - 1.0, 1.0, 0.0, - 0.0, 1.0, 0.0, - - 0.0, 0.0, 1.0, - 1.0, 0.0, 1.0, - 1.0, 1.0, 1.0, - 0.0, 1.0, 1.0]), wgl.STATIC_DRAW); - - this.cubeWireframeIndexBuffer = wgl.createBuffer(); - wgl.bufferData(this.cubeWireframeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ - 0, 1, 1, 2, 2, 3, 3, 0, - 4, 5, 5, 6, 6, 7, 7, 4, - 0, 4, 1, 5, 2, 6, 3, 7 - ]), wgl.STATIC_DRAW); - - - //there's one grid vertex buffer for the planes normal to each axis - this.gridVertexBuffers = []; - - for (var axis = 0; axis < 3; ++axis) { - this.gridVertexBuffers[axis] = wgl.createBuffer(); - - var vertexData = []; - - - var points; //the points that make up this grid plane - - if (axis === 0) { - - points = [ - [0, 0, 0], - [0, this.gridHeight, 0], - [0, this.gridHeight, this.gridDepth], - [0, 0, this.gridDepth] - ]; - - } else if (axis === 1) { - points = [ - [0, 0, 0], - [this.gridWidth, 0, 0], - [this.gridWidth, 0, this.gridDepth], - [0, 0, this.gridDepth] - ]; - } else if (axis === 2) { - - points = [ - [0, 0, 0], - [this.gridWidth, 0, 0], - [this.gridWidth, this.gridHeight, 0], - [0, this.gridHeight, 0] - ]; - } - - - for (var i = 0; i < 4; ++i) { - vertexData.push(points[i][0]); - vertexData.push(points[i][1]); - vertexData.push(points[i][2]); - - vertexData.push(points[(i + 1) % 4][0]); - vertexData.push(points[(i + 1) % 4][1]); - vertexData.push(points[(i + 1) % 4][2]); - } - - - wgl.bufferData(this.gridVertexBuffers[axis], wgl.ARRAY_BUFFER, new Float32Array(vertexData), wgl.STATIC_DRAW); - } - - this.pointVertexBuffer = wgl.createBuffer(); - wgl.bufferData(this.pointVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0]), wgl.STATIC_DRAW); - - - this.quadVertexBuffer = wgl.createBuffer(); - wgl.bufferData(this.quadVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), wgl.STATIC_DRAW); - - - ///////////////////////////////////////////////// - // box state - - this.boxes = []; - - - //////////////////////////////////////////////// - // interaction stuff - - //mouse x and y are in [-1, 1] (clip space) - this.mouseX = 999; - this.mouseY = 999; - - this.keyPressed = []; //an array of booleans that maps a key code to whether or not it's pressed - for (var i = 0; i < 256; ++i) { - this.keyPressed[i] = false; - } - - /* - interactions: - click on a plane and hold down to begin drawing - when mouse is released we enter extrusion mode for new box - click again to create box - - click and drag on side of boxes to resize - - click and drag on side of boxes whilst holding shift to move - - - //while we're not interacting, this is null - //while we are interacting this contains an object - /* - - { - mode: the interaction mode, - - during resizing or translating or extrusion: - box: box we're currently manipulating, - axis: axis of plane we're manipulating: 0, 1 or 2 - side: side of plane we're manipulating: -1 or 1 - point: the point at which the interaction started - - - during translation we also have: - startMax: the starting max along the interaction axis - startMin: the starting min along the interaction axis - - - during drawing - box: box we're currently drawing - point: the point at which we started drawing - axis: the axis of the plane which we're drawing on - side: the side of the plane which we're drawin on - - } - */ - this.interactionState = null; - - - /////////////////////////////////// - // load programs - - - wgl.createProgramsFromFiles({ - backgroundProgram: { - vertexShader: 'shaders/background.vert', - fragmentShader: 'shaders/background.frag' - }, - boxProgram: { - vertexShader: 'shaders/box.vert', - fragmentShader: 'shaders/box.frag' - }, - boxWireframeProgram: { - vertexShader: 'shaders/boxwireframe.vert', - fragmentShader: 'shaders/boxwireframe.frag' - }, - gridProgram: { - vertexShader: 'shaders/grid.vert', - fragmentShader: 'shaders/grid.frag' - }, - pointProgram: { - vertexShader: 'shaders/point.vert', - fragmentShader: 'shaders/point.frag' - } - }, (function (programs) { - for (var programName in programs) { - this[programName] = programs[programName]; - } - - onLoaded(); - }).bind(this)); + if (t2 < highT) highT = t2 + } + + if (lowT > highT) return null + + // if we've reached this far then there is an intersection + + var intersection = [] + for (var i = 0; i < 3; ++i) { + intersection[i] = rayOrigin[i] + rayDirection[i] * lowT + } + + return { + aabb: aabb, + t: lowT, + axis: intersectionAxis, + side: rayDirection[intersectionAxis] > 0 ? -1 : 1, + point: intersection + } +} + +// finds the closest points between the line1 and line2 +// returns [closest point on line1, closest point on line2] +function closestPointsOnLines (line1Origin, line1Direction, line2Origin, line2Direction) { + var w0 = Utilities.subtractVectors([], line1Origin, line2Origin) + + var a = Utilities.dotVectors(line1Direction, line1Direction) + var b = Utilities.dotVectors(line1Direction, line2Direction) + var c = Utilities.dotVectors(line2Direction, line2Direction) + var d = Utilities.dotVectors(line1Direction, w0) + var e = Utilities.dotVectors(line2Direction, w0) + + var t1 = (b * e - c * d) / (a * c - b * b) + var t2 = (a * e - b * d) / (a * c - b * b) + + return [ + Utilities.addVectors([], line1Origin, Utilities.multiplyVectorByScalar([], line1Direction, t1)), + Utilities.addVectors([], line2Origin, Utilities.multiplyVectorByScalar([], line2Direction, t2)) + ] +} + +// this defines the bounds of our editing space +// the grid starts at (0, 0, 0) +// gridSize is [width, height, depth] +// onChange is a callback that gets called anytime a box gets edited +export class BoxEditor { + constructor (canvas, wgl, projectionMatrix, camera, gridSize, onLoaded, onChange) { + this.canvas = canvas + + this.wgl = wgl + + this.gridWidth = gridSize[0] + this.gridHeight = gridSize[1] + this.gridDepth = gridSize[2] + this.gridDimensions = [this.gridWidth, this.gridHeight, this.gridDepth] + + this.projectionMatrix = projectionMatrix + this.camera = camera + + this.onChange = onChange + + // the cube geometry is a 1x1 cube with the origin at the bottom left corner + this.cubeVertexBuffer = wgl.createBuffer() + wgl.bufferData(this.cubeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([ + // Front face + 0.0, 0.0, 1.0, + 1.0, 0.0, 1.0, + 1.0, 1.0, 1.0, + 0.0, 1.0, 1.0, + + // Back face + 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, 0.0, 0.0, + + // Top face + 0.0, 1.0, 0.0, + 0.0, 1.0, 1.0, + 1.0, 1.0, 1.0, + 1.0, 1.0, 0.0, + + // Bottom face + 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, + 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, + + // Right face + 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, 1.0, 1.0, + 1.0, 0.0, 1.0, + + // Left face + 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, + 0.0, 1.0, 1.0, + 0.0, 1.0, 0.0 + ]), wgl.STATIC_DRAW) + + this.cubeIndexBuffer = wgl.createBuffer() + wgl.bufferData(this.cubeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ + 0, 1, 2, 0, 2, 3, + 4, 5, 6, 4, 6, 7, + 8, 9, 10, 8, 10, 11, + 12, 13, 14, 12, 14, 15, + 16, 17, 18, 16, 18, 19, + 20, 21, 22, 20, 22, 23 // left + ]), wgl.STATIC_DRAW) + + this.cubeWireframeVertexBuffer = wgl.createBuffer() + wgl.bufferData(this.cubeWireframeVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([ + 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, + 0.0, 1.0, 0.0, + + 0.0, 0.0, 1.0, + 1.0, 0.0, 1.0, + 1.0, 1.0, 1.0, + 0.0, 1.0, 1.0 + ]), wgl.STATIC_DRAW) + + this.cubeWireframeIndexBuffer = wgl.createBuffer() + wgl.bufferData(this.cubeWireframeIndexBuffer, wgl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ + 0, 1, 1, 2, 2, 3, 3, 0, + 4, 5, 5, 6, 6, 7, 7, 4, + 0, 4, 1, 5, 2, 6, 3, 7 + ]), wgl.STATIC_DRAW) + + // there's one grid vertex buffer for the planes normal to each axis + this.gridVertexBuffers = [] + + for (var axis = 0; axis < 3; ++axis) { + this.gridVertexBuffers[axis] = wgl.createBuffer() + + var vertexData = [] + + var points // the points that make up this grid plane + + if (axis === 0) { + points = [ + [0, 0, 0], + [0, this.gridHeight, 0], + [0, this.gridHeight, this.gridDepth], + [0, 0, this.gridDepth] + ] + } else if (axis === 1) { + points = [ + [0, 0, 0], + [this.gridWidth, 0, 0], + [this.gridWidth, 0, this.gridDepth], + [0, 0, this.gridDepth] + ] + } else if (axis === 2) { + points = [ + [0, 0, 0], + [this.gridWidth, 0, 0], + [this.gridWidth, this.gridHeight, 0], + [0, this.gridHeight, 0] + ] + } + + for (var i = 0; i < 4; ++i) { + vertexData.push(points[i][0]) + vertexData.push(points[i][1]) + vertexData.push(points[i][2]) + + vertexData.push(points[(i + 1) % 4][0]) + vertexData.push(points[(i + 1) % 4][1]) + vertexData.push(points[(i + 1) % 4][2]) + } + + wgl.bufferData(this.gridVertexBuffers[axis], wgl.ARRAY_BUFFER, new Float32Array(vertexData), wgl.STATIC_DRAW) } - function quantize (x, step) { - return Math.round(x / step) * step; - } + this.pointVertexBuffer = wgl.createBuffer() + wgl.bufferData(this.pointVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0]), wgl.STATIC_DRAW) - function quantizeVector (v, step) { - for (var i = 0; i < v.length; ++i) { - v[i] = quantize(v[i], step); - } + this.quadVertexBuffer = wgl.createBuffer() + wgl.bufferData(this.quadVertexBuffer, wgl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), wgl.STATIC_DRAW) - return v; - } + /// ////////////////////////////////////////////// + // box state + this.boxes = [] - BoxEditor.prototype.onKeyDown = function (event) { - this.keyPressed[event.keyCode] = true; - } + /// ///////////////////////////////////////////// + // interaction stuff + // mouse x and y are in [-1, 1] (clip space) + this.mouseX = 999 + this.mouseY = 999 - BoxEditor.prototype.onKeyUp = function (event) { - this.keyPressed[event.keyCode] = false; + this.keyPressed = [] // an array of booleans that maps a key code to whether or not it's pressed + for (var i = 0; i < 256; ++i) { + this.keyPressed[i] = false } - BoxEditor.prototype.onMouseMove = function (event) { - event.preventDefault(); - - var position = Utilities.getMousePosition(event, this.canvas); - var normalizedX = position.x / this.canvas.width; - var normalizedY = position.y / this.canvas.height; - - this.mouseX = normalizedX * 2.0 - 1.0; - this.mouseY = (1.0 - normalizedY) * 2.0 - 1.0; - - - - if (this.interactionState !== null) { - this.onChange(); - - if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) { - var mouseRay = this.getMouseRay(); - - //so when we are dragging to make a box bigger or smaller, what we do is we extend a line out from the intersection point normal to the plane - - var dragLineOrigin = this.interactionState.point; - var dragLineDirection = [0, 0, 0]; - dragLineDirection[this.interactionState.axis] = 1.0; - - //then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged' - var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction); - var newCoordinate = closestPoints[0][this.interactionState.axis]; //the new coordinate for this box plane - newCoordinate = quantize(newCoordinate, STEP); - - var box = this.interactionState.box, - side = this.interactionState.side, - axis = this.interactionState.axis; - - //resize the box, clamping it to itself and the overall grid - if (side === -1) { - box.min[axis] = Math.max(Math.min(newCoordinate, box.max[axis]), 0); - } else if (side === 1) { - box.max[axis] = Math.min(Math.max(newCoordinate, box.min[axis]), this.gridDimensions[axis]); - } - - //collision detection - for (var i = 0; i < this.boxes.length; ++i) { - var otherBox = this.boxes[i]; - if (box !== otherBox) { //don't collide with self - if (exclusiveAABBOverlap(box, otherBox)) { - - //resolve collision - if (side === -1) { - box.min[axis] = otherBox.max[axis]; - } else if (side === 1) { - box.max[axis] = otherBox.min[axis]; - } - } - } - } - - } else if (this.interactionState.mode === InteractionMode.TRANSLATING) { - - var mouseRay = this.getMouseRay(); - - //so when we are translating a box, what we do is we extend a line out from the intersection point normal to the plane - - var dragLineOrigin = this.interactionState.point; - var dragLineDirection = [0, 0, 0]; - dragLineDirection[this.interactionState.axis] = 1.0; - - //then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged' - var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction); - var newCoordinate = closestPoints[0][this.interactionState.axis]; //the new coordinate for this box plane - newCoordinate = quantize(newCoordinate, STEP); - - var box = this.interactionState.box, - side = this.interactionState.side, - axis = this.interactionState.axis; - - - var length = this.interactionState.startMax - this.interactionState.startMin; //the length of the box along the translation axis - - if (side === -1) { - box.min[axis] = newCoordinate; - box.max[axis] = newCoordinate + length; - } else if (side === 1) { - box.max[axis] = newCoordinate; - box.min[axis] = newCoordinate - length; - } - - //clamp to boundaries - if (box.min[axis] < 0) { - box.min[axis] = 0; - box.max[axis] = length; - } - - if (box.max[axis] > this.gridDimensions[axis]) { - box.max[axis] = this.gridDimensions[axis]; - box.min[axis] = this.gridDimensions[axis] - length; - } - - - var translationDirection = 0; //is either -1 or 1 depending on which way we're pushing our box - //how we resolve collisions depends on our translation direction - if (side === -1) { - translationDirection = newCoordinate < this.interactionState.startMin ? -1 : 1; - } else if (side === 1) { - translationDirection = newCoordinate < this.interactionState.startMax ? -1 : 1; - } + /* + interactions: + click on a plane and hold down to begin drawing + when mouse is released we enter extrusion mode for new box + click again to create box - - var sweptBox = box.clone(); //we sweep out translating AABB for collision detection to prevent ghosting through boxes - //reset swept box to original box location before translation - sweptBox.min[axis] = this.interactionState.startMin; - sweptBox.max[axis] = this.interactionState.startMax; - - //sweep out the correct plane to where it has been translated to - if (translationDirection === 1) { - sweptBox.max[axis] = box.max[axis]; - } else if (translationDirection === -1) { - sweptBox.min[axis] = box.min[axis]; - } - - //collision detection - for (var i = 0; i < this.boxes.length; ++i) { - var otherBox = this.boxes[i]; - if (box !== otherBox) { //don't collide with self - if (exclusiveAABBOverlap(sweptBox, otherBox)) { - - //resolve collision - if (translationDirection === -1) { - box.min[axis] = otherBox.max[axis]; - box.max[axis] = otherBox.max[axis] + length; - } else if (translationDirection === 1) { - box.max[axis] = otherBox.min[axis]; - box.min[axis] = otherBox.min[axis] - length; - } - } - } - } + click and drag on side of boxes to resize - } else if (this.interactionState.mode === InteractionMode.DRAWING) { - - var mouseRay = this.getMouseRay(); + click and drag on side of boxes whilst holding shift to move - //get the mouse ray intersection with the drawing plane + //while we're not interacting, this is null + //while we are interacting this contains an object + /* - var axis = this.interactionState.axis, - side = this.interactionState.side, - startPoint = this.interactionState.point; + { + mode: the interaction mode, - var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; - var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis]; + during resizing or translating or extrusion: + box: box we're currently manipulating, + axis: axis of plane we're manipulating: 0, 1 or 2 + side: side of plane we're manipulating: -1 or 1 + point: the point at which the interaction started - if (t > 0) { //if the mouse ray misses the drawing plane then the box just stays the same size as it was before + during translation we also have: + startMax: the starting max along the interaction axis + startMin: the starting min along the interaction axis - var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t)); - quantizeVector(intersection, STEP); + during drawing + box: box we're currently drawing + point: the point at which we started drawing + axis: the axis of the plane which we're drawing on + side: the side of the plane which we're drawin on - for (var i = 0; i < 3; ++i) { - intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]); - intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]); } + */ + this.interactionState = null + + /// //////////////////////////////// + // load programs + wgl.createProgramsFromFiles({ + backgroundProgram: { + vertexShader: 'shaders/background.vert', + fragmentShader: 'shaders/background.frag' + }, + boxProgram: { + vertexShader: 'shaders/box.vert', + fragmentShader: 'shaders/box.frag' + }, + boxWireframeProgram: { + vertexShader: 'shaders/boxwireframe.vert', + fragmentShader: 'shaders/boxwireframe.frag' + }, + gridProgram: { + vertexShader: 'shaders/grid.vert', + fragmentShader: 'shaders/grid.frag' + }, + pointProgram: { + vertexShader: 'shaders/point.vert', + fragmentShader: 'shaders/point.frag' + } + }, function (programs) { + for (var programName in programs) { + this[programName] = programs[programName] + } + + onLoaded() + }.bind(this)) + } + + onKeyDown (event) { + this.keyPressed[event.keyCode] = true + } + + onKeyUp (event) { + this.keyPressed[event.keyCode] = false + } + + onMouseMove (event) { + event.preventDefault() + + var position = Utilities.getMousePosition(event, this.canvas) + var normalizedX = position.x / this.canvas.width + var normalizedY = position.y / this.canvas.height + + this.mouseX = normalizedX * 2.0 - 1.0 + this.mouseY = (1.0 - normalizedY) * 2.0 - 1.0 + + if (this.interactionState !== null) { + this.onChange() + + if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) { + var mouseRay = this.getMouseRay() + + // so when we are dragging to make a box bigger or smaller, what we do is we extend a line out from the intersection point normal to the plane + var dragLineOrigin = this.interactionState.point + var dragLineDirection = [0, 0, 0] + dragLineDirection[this.interactionState.axis] = 1.0 + + // then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged' + var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction) + var newCoordinate = closestPoints[0][this.interactionState.axis] // the new coordinate for this box plane + newCoordinate = quantize(newCoordinate, STEP) + + var box = this.interactionState.box + var side = this.interactionState.side + var axis = this.interactionState.axis + + // resize the box, clamping it to itself and the overall grid + if (side === -1) { + box.min[axis] = Math.max(Math.min(newCoordinate, box.max[axis]), 0) + } else if (side === 1) { + box.max[axis] = Math.min(Math.max(newCoordinate, box.min[axis]), this.gridDimensions[axis]) + } - var min = [Math.min(startPoint[0], intersection[0]), Math.min(startPoint[1], intersection[1]), Math.min(startPoint[2], intersection[2])]; - var max = [Math.max(startPoint[0], intersection[0]), Math.max(startPoint[1], intersection[1]), Math.max(startPoint[2], intersection[2])]; - - - var box = this.interactionState.box; - - var sweptBox = new AABB(min, max); //we sweep the box a bit into the grid to make sure it collides along the plane axis - if (this.interactionState.side === -1) { - sweptBox.max[this.interactionState.axis] = STEP * 0.1; - } else { - sweptBox.min[this.interactionState.axis] = this.gridDimensions[this.interactionState.axis] - STEP * 0.1; + // collision detection + for (var i = 0; i < this.boxes.length; ++i) { + var otherBox = this.boxes[i] + if (box !== otherBox) { // don't collide with self + if (exclusiveAABBOverlap(box, otherBox)) { + // resolve collision + if (side === -1) { + box.min[axis] = otherBox.max[axis] + } else if (side === 1) { + box.max[axis] = otherBox.min[axis] + } + } + } + } + } else if (this.interactionState.mode === InteractionMode.TRANSLATING) { + var mouseRay = this.getMouseRay() + + // so when we are translating a box, what we do is we extend a line out from the intersection point normal to the plane + var dragLineOrigin = this.interactionState.point + var dragLineDirection = [0, 0, 0] + dragLineDirection[this.interactionState.axis] = 1.0 + + // then we find the closest point between the mouse ray and this line and use that to determine how far we've 'dragged' + var closestPoints = closestPointsOnLines(dragLineOrigin, dragLineDirection, mouseRay.origin, mouseRay.direction) + var newCoordinate = closestPoints[0][this.interactionState.axis] // the new coordinate for this box plane + newCoordinate = quantize(newCoordinate, STEP) + + var box = this.interactionState.box + var side = this.interactionState.side + var axis = this.interactionState.axis + + var length = this.interactionState.startMax - this.interactionState.startMin // the length of the box along the translation axis + + if (side === -1) { + box.min[axis] = newCoordinate + box.max[axis] = newCoordinate + length + } else if (side === 1) { + box.max[axis] = newCoordinate + box.min[axis] = newCoordinate - length + } - } + // clamp to boundaries + if (box.min[axis] < 0) { + box.min[axis] = 0 + box.max[axis] = length + } - //collision detection - for (var i = 0; i < this.boxes.length; ++i) { - var otherBox = this.boxes[i]; - - if (box !== otherBox) { //don't collide with self - if (exclusiveAABBOverlap(sweptBox, otherBox)) { - - //we resolve along the axis with the smaller overlap and where the start point doesn't already overlap the other box in that axis - var smallestOverlap = 99999999; - var smallestOverlapAxis = -1; - - for (var axis = 0; axis < 3; ++axis) { - if (axis !== this.interactionState.axis) { //only resolve collisions in the drawing plane - var overlap = Math.min(max[axis], otherBox.max[axis]) - Math.max(min[axis], otherBox.min[axis]); - - if (overlap > 0 && overlap < smallestOverlap && (startPoint[axis] < otherBox.min[axis] || startPoint[axis] > otherBox.max[axis])) { - smallestOverlap = overlap; - smallestOverlapAxis = axis; - } - } - } - - if (intersection[smallestOverlapAxis] > startPoint[smallestOverlapAxis]) { //if we're resizing in the positive direction - max[smallestOverlapAxis] = otherBox.min[smallestOverlapAxis]; - } else { //if we're resizing in the negative direction - min[smallestOverlapAxis] = otherBox.max[smallestOverlapAxis]; - } - } - } - } + if (box.max[axis] > this.gridDimensions[axis]) { + box.max[axis] = this.gridDimensions[axis] + box.min[axis] = this.gridDimensions[axis] - length + } - this.interactionState.box.min = min; - this.interactionState.box.max = max; + var translationDirection = 0 // is either -1 or 1 depending on which way we're pushing our box - } - } + // how we resolve collisions depends on our translation direction + if (side === -1) { + translationDirection = newCoordinate < this.interactionState.startMin ? -1 : 1 + } else if (side === 1) { + translationDirection = newCoordinate < this.interactionState.startMax ? -1 : 1 } - this.camera.onMouseMove(event); - } + var sweptBox = box.clone() // we sweep out translating AABB for collision detection to prevent ghosting through boxes - //returns the closest box intersection data (same as rayAABBIntersection) for the given ray - //if there is no intersection it returns null - BoxEditor.prototype.getBoxIntersection = function (rayOrigin, rayDirection) { - //find the closest box that this collides with + // reset swept box to original box location before translation + sweptBox.min[axis] = this.interactionState.startMin + sweptBox.max[axis] = this.interactionState.startMax - var bestIntersectionSoFar = { - aabb: null, - t: Infinity + // sweep out the correct plane to where it has been translated to + if (translationDirection === 1) { + sweptBox.max[axis] = box.max[axis] + } else if (translationDirection === -1) { + sweptBox.min[axis] = box.min[axis] } + // collision detection for (var i = 0; i < this.boxes.length; ++i) { - var box = this.boxes[i]; - - var intersection = rayAABBIntersection(rayOrigin, rayDirection, box); + var otherBox = this.boxes[i] + if (box !== otherBox) { // don't collide with self + if (exclusiveAABBOverlap(sweptBox, otherBox)) { + // resolve collision + if (translationDirection === -1) { + box.min[axis] = otherBox.max[axis] + box.max[axis] = otherBox.max[axis] + length + } else if (translationDirection === 1) { + box.max[axis] = otherBox.min[axis] + box.min[axis] = otherBox.min[axis] - length + } + } + } + } + } else if (this.interactionState.mode === InteractionMode.DRAWING) { + var mouseRay = this.getMouseRay() + + // get the mouse ray intersection with the drawing plane + var axis = this.interactionState.axis + var side = this.interactionState.side + var startPoint = this.interactionState.point + + var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis] + var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis] + + if (t > 0) { // if the mouse ray misses the drawing plane then the box just stays the same size as it was before + var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t)) + quantizeVector(intersection, STEP) + + for (var i = 0; i < 3; ++i) { + intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]) + intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]) + } + + var min = [Math.min(startPoint[0], intersection[0]), Math.min(startPoint[1], intersection[1]), Math.min(startPoint[2], intersection[2])] + var max = [Math.max(startPoint[0], intersection[0]), Math.max(startPoint[1], intersection[1]), Math.max(startPoint[2], intersection[2])] + + var box = this.interactionState.box + + var sweptBox = new AABB(min, max) // we sweep the box a bit into the grid to make sure it collides along the plane axis + if (this.interactionState.side === -1) { + sweptBox.max[this.interactionState.axis] = STEP * 0.1 + } else { + sweptBox.min[this.interactionState.axis] = this.gridDimensions[this.interactionState.axis] - STEP * 0.1 + } + + // collision detection + for (var i = 0; i < this.boxes.length; ++i) { + var otherBox = this.boxes[i] + + if (box !== otherBox) { // don't collide with self + if (exclusiveAABBOverlap(sweptBox, otherBox)) { + // we resolve along the axis with the smaller overlap and where the start point doesn't already overlap the other box in that axis + var smallestOverlap = 99999999 + var smallestOverlapAxis = -1 + + for (var axis = 0; axis < 3; ++axis) { + if (axis !== this.interactionState.axis) { // only resolve collisions in the drawing plane + var overlap = Math.min(max[axis], otherBox.max[axis]) - Math.max(min[axis], otherBox.min[axis]) + + if (overlap > 0 && overlap < smallestOverlap && (startPoint[axis] < otherBox.min[axis] || startPoint[axis] > otherBox.max[axis])) { + smallestOverlap = overlap + smallestOverlapAxis = axis + } + } + } - if (intersection !== null) { //if there is an intersection - if (intersection.t < bestIntersectionSoFar.t) { //if this is closer than the best we've seen so far - bestIntersectionSoFar = intersection; + if (intersection[smallestOverlapAxis] > startPoint[smallestOverlapAxis]) { // if we're resizing in the positive direction + max[smallestOverlapAxis] = otherBox.min[smallestOverlapAxis] + } else { // if we're resizing in the negative direction + min[smallestOverlapAxis] = otherBox.max[smallestOverlapAxis] } + } } - } + } - if (bestIntersectionSoFar.aabb === null) { //if we didn't intersect any boxes - return null; - } else { - return bestIntersectionSoFar; + this.interactionState.box.min = min + this.interactionState.box.max = max } + } } - //tests for intersection with one of the bounding planes - /* - if there is an intersection returns - {axis, side, point} - otherwise, returns null - */ - BoxEditor.prototype.getBoundingPlaneIntersection = function (rayOrigin, rayDirection) { - //we try to intersect with the two planes on each axis in turn (as long as they are facing towards the camera) - //we assume we could only ever intersect with one of the planes so we break out as soon as we've found something - - for (var axis = 0; axis < 3; ++axis) { + this.camera.onMouseMove(event) + } - //now let's try intersecting with each side in turn - for (var side = -1; side <= 1; side += 2) { //goes between -1 and 1 (hackish! - - //first let's make sure the plane is front facing to the ray - var frontFacing = side === -1 ? rayDirection[axis] < 0 : rayDirection[axis] > 0; - if (frontFacing) { - var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; //the coordinate of the plane along this axis + // returns the closest box intersection data (same as rayAABBIntersection) for the given ray + // if there is no intersection it returns null + getBoxIntersection (rayOrigin, rayDirection) { + // find the closest box that this collides with + var bestIntersectionSoFar = { + aabb: null, + t: Infinity + } - var t = (planeCoordinate - rayOrigin[axis]) / rayDirection[axis]; + for (var i = 0; i < this.boxes.length; ++i) { + var box = this.boxes[i] + var intersection = rayAABBIntersection(rayOrigin, rayDirection, box) - if (t > 0) { - var intersection = Utilities.addVectors([], rayOrigin, Utilities.multiplyVectorByScalar([], rayDirection, t)); + if (intersection !== null) { // if there is an intersection + if (intersection.t < bestIntersectionSoFar.t) { // if this is closer than the best we've seen so far + bestIntersectionSoFar = intersection + } + } + } - //if we're still within the bounds of the grid - if (intersection[0] >= 0.0 && intersection[0] <= this.gridDimensions[0] && + if (bestIntersectionSoFar.aabb === null) { // if we didn't intersect any boxes + return null + } else { + return bestIntersectionSoFar + } + } + + // tests for intersection with one of the bounding planes + /* + if there is an intersection returns + {axis, side, point} + otherwise, returns null + */ + getBoundingPlaneIntersection (rayOrigin, rayDirection) { + // we try to intersect with the two planes on each axis in turn (as long as they are facing towards the camera) + // we assume we could only ever intersect with one of the planes so we break out as soon as we've found something + for (var axis = 0; axis < 3; ++axis) { + // now let's try intersecting with each side in turn + for (var side = -1; side <= 1; side += 2) { // goes between -1 and 1 (hackish! + // first let's make sure the plane is front facing to the ray + var frontFacing = side === -1 ? rayDirection[axis] < 0 : rayDirection[axis] > 0 + if (frontFacing) { + var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis] // the coordinate of the plane along this axis + + var t = (planeCoordinate - rayOrigin[axis]) / rayDirection[axis] + + if (t > 0) { + var intersection = Utilities.addVectors([], rayOrigin, Utilities.multiplyVectorByScalar([], rayDirection, t)) + + // if we're still within the bounds of the grid + if (intersection[0] >= 0.0 && intersection[0] <= this.gridDimensions[0] && intersection[1] >= 0.0 && intersection[1] <= this.gridDimensions[1] && intersection[2] >= 0.0 && intersection[2] <= this.gridDimensions[2]) { - - return { - axis: axis, - side: side, - point: intersection - } - } - } - } + return { + axis: axis, + side: side, + point: intersection + } } + } } - - return null; //no intersection found + } } + return null // no intersection found + } - BoxEditor.prototype.onMouseDown = function (event) { - event.preventDefault(); - - this.onMouseMove(event); - - if (!this.keyPressed[32]) { //if space isn't held down - - //we've finished extruding a box - if (this.interactionState !== null && this.interactionState.mode === InteractionMode.EXTRUDING) { - //delete zero volume boxes - if (this.interactionState.box.computeVolume() === 0) { - this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1); - } - this.interactionState = null; + onMouseDown (event) { + event.preventDefault() - this.onChange(); + this.onMouseMove(event) - return; - } else { - - var mouseRay = this.getMouseRay(); - - //find the closest box that this collides with - - var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction); - - - //if we've intersected at least one box then let's start manipulating that box - if (boxIntersection !== null) { - var intersection = boxIntersection; + if (!this.keyPressed[32]) { // if space isn't held down + // we've finished extruding a box + if (this.interactionState !== null && this.interactionState.mode === InteractionMode.EXTRUDING) { + // delete zero volume boxes + if (this.interactionState.box.computeVolume() === 0) { + this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1) + } + this.interactionState = null - if (this.keyPressed[16]) { //if we're holding shift we start to translate - this.interactionState = { - mode: InteractionMode.TRANSLATING, - box: intersection.aabb, - axis: intersection.axis, - side: intersection.side, - point: intersection.point, + this.onChange() - startMax: intersection.aabb.max[intersection.axis], - startMin: intersection.aabb.min[intersection.axis] - }; - } else { //otherwise we start resizing + return + } else { + var mouseRay = this.getMouseRay() - this.interactionState = { - mode: InteractionMode.RESIZING, - box: intersection.aabb, - axis: intersection.axis, - side: intersection.side, - point: intersection.point - }; - } - } + // find the closest box that this collides with + var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction) + // if we've intersected at least one box then let's start manipulating that box + if (boxIntersection !== null) { + var intersection = boxIntersection - //if we've not intersected any box then let's see if we should start the box creation process - if (boxIntersection === null) { - var mouseRay = this.getMouseRay(); + if (this.keyPressed[16]) { // if we're holding shift we start to translate + this.interactionState = { + mode: InteractionMode.TRANSLATING, + box: intersection.aabb, + axis: intersection.axis, + side: intersection.side, + point: intersection.point, - var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction); + startMax: intersection.aabb.max[intersection.axis], + startMin: intersection.aabb.min[intersection.axis] + } + } else { // otherwise we start resizing + this.interactionState = { + mode: InteractionMode.RESIZING, + box: intersection.aabb, + axis: intersection.axis, + side: intersection.side, + point: intersection.point + } + } + } - if (planeIntersection !== null) { //if we've hit one of the planes - //go into drawing mode - - var point = planeIntersection.point; - point[0] = quantize(point[0], STEP); - point[1] = quantize(point[1], STEP); - point[2] = quantize(point[2], STEP); + // if we've not intersected any box then let's see if we should start the box creation process + if (boxIntersection === null) { + var mouseRay = this.getMouseRay() - var newBox = new AABB(point, point); - this.boxes.push(newBox); + var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction) - this.interactionState = { - mode: InteractionMode.DRAWING, - box: newBox, - axis: planeIntersection.axis, - side: planeIntersection.side, - point: planeIntersection.point - }; - } + if (planeIntersection !== null) { // if we've hit one of the planes + // go into drawing mode + var point = planeIntersection.point + point[0] = quantize(point[0], STEP) + point[1] = quantize(point[1], STEP) + point[2] = quantize(point[2], STEP) - this.onChange(); - } + var newBox = new AABB(point, point) + this.boxes.push(newBox) + this.interactionState = { + mode: InteractionMode.DRAWING, + box: newBox, + axis: planeIntersection.axis, + side: planeIntersection.side, + point: planeIntersection.point } + } + this.onChange() } - - if (this.interactionState === null) { - this.camera.onMouseDown(event); - } + } + } + if (this.interactionState === null) { + this.camera.onMouseDown(event) } + } - BoxEditor.prototype.onMouseUp = function (event) { - event.preventDefault(); + onMouseUp (event) { + event.preventDefault() - if (this.interactionState !== null) { - if (this.interactionState.mode === InteractionMode.RESIZING) { //the end of a resize - //if we've resized to zero volume then we delete the box - if (this.interactionState.box.computeVolume() === 0) { - this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1); - } + if (this.interactionState !== null) { + if (this.interactionState.mode === InteractionMode.RESIZING) { // the end of a resize + // if we've resized to zero volume then we delete the box + if (this.interactionState.box.computeVolume() === 0) { + this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1) + } - this.interactionState = null; + this.interactionState = null + } else if (this.interactionState.mode === InteractionMode.TRANSLATING) { // the end of a translate + this.interactionState = null + } else if (this.interactionState.mode === InteractionMode.DRAWING) { // the end of a draw + // TODO: DRY this + if (this.interactionState.box.computeSurfaceArea() > 0) { // make sure we have something to extrude + var mouseRay = this.getMouseRay() + + var axis = this.interactionState.axis + var side = this.interactionState.side + var startPoint = this.interactionState.point + + var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis] + var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis] + + var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t)) + quantizeVector(intersection, STEP) + + // clamp extrusion point to grid and to box + for (var i = 0; i < 3; ++i) { + intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]) + intersection[i] = Utilities.clamp(intersection[i], this.interactionState.box.min[i], this.interactionState.box.max[i]) + } + + // go into extrusion mode + this.interactionState = { + mode: InteractionMode.EXTRUDING, + box: this.interactionState.box, + axis: this.interactionState.axis, + side: this.interactionState.side * -1, + point: intersection + } + } else { // otherwise delete the box we were editing and go straight back into regular mode + this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1) + this.interactionState = null + } + } - } else if (this.interactionState.mode === InteractionMode.TRANSLATING) { //the end of a translate - this.interactionState = null; - } else if (this.interactionState.mode === InteractionMode.DRAWING) { //the end of a draw - //TODO: DRY this + this.onChange() + } - if (this.interactionState.box.computeSurfaceArea() > 0) { //make sure we have something to extrude + if (this.interactionState === null) { + this.camera.onMouseUp(event) + } + } + + // returns an object + /* + { + origin: [x, y, z], + direction: [x, y, z] //normalized + } + */ + getMouseRay () { + var fov = 2.0 * Math.atan(1.0 / this.projectionMatrix[5]) + + var viewSpaceMouseRay = [ + this.mouseX * Math.tan(fov / 2.0) * (this.canvas.width / this.canvas.height), + this.mouseY * Math.tan(fov / 2.0), + -1.0 + ] + + var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix()) + var mouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix) + Utilities.normalizeVector(mouseRay, mouseRay) + + var rayOrigin = this.camera.getPosition() - var mouseRay = this.getMouseRay(); + return { + origin: rayOrigin, + direction: mouseRay + } + } - var axis = this.interactionState.axis, - side = this.interactionState.side, - startPoint = this.interactionState.point; + draw () { + var wgl = this.wgl - var planeCoordinate = side === -1 ? 0 : this.gridDimensions[axis]; - var t = (planeCoordinate - mouseRay.origin[axis]) / mouseRay.direction[axis]; + wgl.clear( + wgl.createClearState().bindFramebuffer(null).clearColor(0.9, 0.9, 0.9, 1.0), + wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT) - var intersection = Utilities.addVectors([], mouseRay.origin, Utilities.multiplyVectorByScalar([], mouseRay.direction, t)); - quantizeVector(intersection, STEP); - - //clamp extrusion point to grid and to box - for (var i = 0; i < 3; ++i) { - intersection[i] = Utilities.clamp(intersection[i], 0, this.gridDimensions[i]); - intersection[i] = Utilities.clamp(intersection[i], this.interactionState.box.min[i], this.interactionState.box.max[i]); - } + /// ////////////////////////////////////////// + // draw background + var backgroundDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) + .useProgram(this.backgroundProgram) - //go into extrusion mode - this.interactionState = { - mode: InteractionMode.EXTRUDING, - box: this.interactionState.box, - axis: this.interactionState.axis, - side: this.interactionState.side * -1, - point: intersection - }; + .vertexAttribPointer(this.quadVertexBuffer, this.backgroundProgram.getAttribLocation('a_position'), 2, wgl.FLOAT, wgl.FALSE, 0, 0) - } else { //otherwise delete the box we were editing and go straight back into regular mode - this.boxes.splice(this.boxes.indexOf(this.interactionState.box), 1); - this.interactionState = null; - } - } + wgl.drawArrays(backgroundDrawState, wgl.TRIANGLE_STRIP, 0, 4) - this.onChange(); - } + /// ////////////////////////////////////////// + // draw grid + for (var axis = 0; axis < 3; ++axis) { + for (var side = 0; side <= 1; ++side) { + var cameraPosition = this.camera.getPosition() + var planePosition = [this.gridWidth / 2, this.gridHeight / 2, this.gridDepth / 2] + planePosition[axis] = side === 0 ? 0 : this.gridDimensions[axis] - if (this.interactionState === null) { - this.camera.onMouseUp(event); - } - } + var cameraDirection = Utilities.subtractVectors([], planePosition, cameraPosition) + var gridDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) - //returns an object - /* - { - origin: [x, y, z], - direction: [x, y, z] //normalized - } - */ - BoxEditor.prototype.getMouseRay = function () { - var fov = 2.0 * Math.atan(1.0 / this.projectionMatrix[5]); + .useProgram(this.gridProgram) - var viewSpaceMouseRay = [ - this.mouseX * Math.tan(fov / 2.0) * (this.canvas.width / this.canvas.height), - this.mouseY * Math.tan(fov / 2.0), - -1.0]; + .vertexAttribPointer(this.gridVertexBuffers[axis], this.gridProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) - var inverseViewMatrix = Utilities.invertMatrix([], this.camera.getViewMatrix()); - var mouseRay = Utilities.transformDirectionByMatrix([], viewSpaceMouseRay, inverseViewMatrix); - Utilities.normalizeVector(mouseRay, mouseRay); + .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) + .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) + var translation = [0, 0, 0] + translation[axis] = side * this.gridDimensions[axis] - var rayOrigin = this.camera.getPosition(); + gridDrawState.uniform3f('u_translation', translation[0], translation[1], translation[2]) - return { - origin: rayOrigin, - direction: mouseRay - }; + if (side === 0 && cameraDirection[axis] <= 0 || side === 1 && cameraDirection[axis] >= 0) { + wgl.drawArrays(gridDrawState, wgl.LINES, 0, 8) + } + } } - BoxEditor.prototype.draw = function () { - var wgl = this.wgl; - - wgl.clear( - wgl.createClearState().bindFramebuffer(null).clearColor(0.9, 0.9, 0.9, 1.0), - wgl.COLOR_BUFFER_BIT | wgl.DEPTH_BUFFER_BIT); + /// //////////////////////////////////////////// + // draw boxes and point + var boxDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) - ///////////////////////////////////////////// - //draw background + .enable(wgl.DEPTH_TEST) + .enable(wgl.CULL_FACE) - var backgroundDrawState = wgl.createDrawState() - .bindFramebuffer(null) - .viewport(0, 0, this.canvas.width, this.canvas.height) - - .useProgram(this.backgroundProgram) - - .vertexAttribPointer(this.quadVertexBuffer, this.backgroundProgram.getAttribLocation('a_position'), 2, wgl.FLOAT, wgl.FALSE, 0, 0); + .useProgram(this.boxProgram) - wgl.drawArrays(backgroundDrawState, wgl.TRIANGLE_STRIP, 0, 4); + .vertexAttribPointer(this.cubeVertexBuffer, this.boxProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) + .bindIndexBuffer(this.cubeIndexBuffer) - ///////////////////////////////////////////// - //draw grid + .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) + .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) - for (var axis = 0; axis < 3; ++axis) { - for (var side = 0; side <= 1; ++side) { - var cameraPosition = this.camera.getPosition(); + .enable(wgl.POLYGON_OFFSET_FILL) + .polygonOffset(1, 1) - var planePosition = [this.gridWidth / 2, this.gridHeight / 2, this.gridDepth / 2]; - planePosition[axis] = side === 0 ? 0 : this.gridDimensions[axis]; - - var cameraDirection = Utilities.subtractVectors([], planePosition, cameraPosition); + var boxToHighlight = null + var sideToHighlight = null + var highlightColor = null - var gridDrawState = wgl.createDrawState() - .bindFramebuffer(null) - .viewport(0, 0, this.canvas.width, this.canvas.height) + if (this.interactionState !== null) { + if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) { + boxToHighlight = this.interactionState.box + sideToHighlight = [1.5, 1.5, 1.5] + sideToHighlight[this.interactionState.axis] = this.interactionState.side - .useProgram(this.gridProgram) + highlightColor = [0.75, 0.75, 0.75] + } + } else if (!this.keyPressed[32] && !this.camera.isMouseDown()) { // if we're not interacting with anything and we're not in camera mode + var mouseRay = this.getMouseRay() - .vertexAttribPointer(this.gridVertexBuffers[axis], this.gridProgram.getAttribLocation('a_vertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) + var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction) - .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) - .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()); + // if we're over a box, let's highlight the side we're hovering over + if (boxIntersection !== null) { + boxToHighlight = boxIntersection.aabb + sideToHighlight = [1.5, 1.5, 1.5] + sideToHighlight[boxIntersection.axis] = boxIntersection.side - var translation = [0, 0, 0]; - translation[axis] = side * this.gridDimensions[axis]; + highlightColor = [0.9, 0.9, 0.9] + } - gridDrawState.uniform3f('u_translation', translation[0], translation[1], translation[2]); - - - if (side === 0 && cameraDirection[axis] <= 0 || side === 1 && cameraDirection[axis] >= 0) { - wgl.drawArrays(gridDrawState, wgl.LINES, 0, 8); - } - } - } + // if we're not over a box but hovering over a bounding plane, let's draw a indicator point + if (boxIntersection === null && !this.keyPressed[32]) { + var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction) + if (planeIntersection !== null) { + var pointPosition = planeIntersection.point + quantizeVector(pointPosition, STEP) - /////////////////////////////////////////////// - //draw boxes and point + var rotation = [ + new Float32Array([0, 0, 1, 0, 1, 0, 1, 0, 0]), + new Float32Array([1, 0, 0, 0, 0, 1, 0, 1, 0]), + new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) + ][planeIntersection.axis] - var boxDrawState = wgl.createDrawState() + var pointDrawState = wgl.createDrawState() .bindFramebuffer(null) .viewport(0, 0, this.canvas.width, this.canvas.height) .enable(wgl.DEPTH_TEST) - .enable(wgl.CULL_FACE) - .useProgram(this.boxProgram) + .useProgram(this.pointProgram) - .vertexAttribPointer(this.cubeVertexBuffer, this.boxProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) - - .bindIndexBuffer(this.cubeIndexBuffer) + .vertexAttribPointer(this.pointVertexBuffer, this.pointProgram.getAttribLocation('a_position'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) - .enable(wgl.POLYGON_OFFSET_FILL) - .polygonOffset(1, 1); - - - var boxToHighlight = null, - sideToHighlight = null, - highlightColor = null; - - if (this.interactionState !== null) { - if (this.interactionState.mode === InteractionMode.RESIZING || this.interactionState.mode === InteractionMode.EXTRUDING) { - boxToHighlight = this.interactionState.box; - sideToHighlight = [1.5, 1.5, 1.5]; - sideToHighlight[this.interactionState.axis] = this.interactionState.side; - - highlightColor = [0.75, 0.75, 0.75]; - } - } else if (!this.keyPressed[32] && !this.camera.isMouseDown()) { //if we're not interacting with anything and we're not in camera mode - var mouseRay = this.getMouseRay(); - - var boxIntersection = this.getBoxIntersection(mouseRay.origin, mouseRay.direction); - - //if we're over a box, let's highlight the side we're hovering over - - if (boxIntersection !== null) { - boxToHighlight = boxIntersection.aabb; - sideToHighlight = [1.5, 1.5, 1.5]; - sideToHighlight[boxIntersection.axis] = boxIntersection.side; - - highlightColor = [0.9, 0.9, 0.9]; - } - + .uniform3f('u_position', pointPosition[0], pointPosition[1], pointPosition[2]) - //if we're not over a box but hovering over a bounding plane, let's draw a indicator point - if (boxIntersection === null && !this.keyPressed[32]) { - var planeIntersection = this.getBoundingPlaneIntersection(mouseRay.origin, mouseRay.direction); + .uniformMatrix3fv('u_rotation', false, rotation) - if (planeIntersection !== null) { - var pointPosition = planeIntersection.point; - quantizeVector(pointPosition, STEP); - - var rotation = [ - new Float32Array([0, 0, 1, 0, 1, 0, 1, 0, 0]), - new Float32Array([1, 0, 0, 0, 0, 1, 0, 1, 0]), - new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) - ][planeIntersection.axis]; - - var pointDrawState = wgl.createDrawState() - .bindFramebuffer(null) - .viewport(0, 0, this.canvas.width, this.canvas.height) - - .enable(wgl.DEPTH_TEST) - - .useProgram(this.pointProgram) - - .vertexAttribPointer(this.pointVertexBuffer, this.pointProgram.getAttribLocation('a_position'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) - - .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) - .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) - - .uniform3f('u_position', pointPosition[0], pointPosition[1], pointPosition[2]) - - .uniformMatrix3fv('u_rotation', false, rotation); - - wgl.drawArrays(pointDrawState, wgl.TRIANGLE_STRIP, 0, 4); - } - } + wgl.drawArrays(pointDrawState, wgl.TRIANGLE_STRIP, 0, 4) } - - for (var i = 0; i < this.boxes.length; ++i) { - var box = this.boxes[i]; + } + } - boxDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2]) - .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]); + for (var i = 0; i < this.boxes.length; ++i) { + var box = this.boxes[i] - if (box === boxToHighlight) { - boxDrawState.uniform3f('u_highlightSide', sideToHighlight[0], sideToHighlight[1], sideToHighlight[2]); - boxDrawState.uniform3f('u_highlightColor', highlightColor[0], highlightColor[1], highlightColor[2]); - } else { - boxDrawState.uniform3f('u_highlightSide', 1.5, 1.5, 1.5); - } + boxDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2]) + .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]) - wgl.drawElements(boxDrawState, wgl.TRIANGLES, 36, wgl.UNSIGNED_SHORT); - } + if (box === boxToHighlight) { + boxDrawState.uniform3f('u_highlightSide', sideToHighlight[0], sideToHighlight[1], sideToHighlight[2]) + boxDrawState.uniform3f('u_highlightColor', highlightColor[0], highlightColor[1], highlightColor[2]) + } else { + boxDrawState.uniform3f('u_highlightSide', 1.5, 1.5, 1.5) + } + wgl.drawElements(boxDrawState, wgl.TRIANGLES, 36, wgl.UNSIGNED_SHORT) + } + var boxWireframeDrawState = wgl.createDrawState() + .bindFramebuffer(null) + .viewport(0, 0, this.canvas.width, this.canvas.height) - var boxWireframeDrawState = wgl.createDrawState() - .bindFramebuffer(null) - .viewport(0, 0, this.canvas.width, this.canvas.height) + .enable(wgl.DEPTH_TEST) - .enable(wgl.DEPTH_TEST) + .useProgram(this.boxWireframeProgram) - .useProgram(this.boxWireframeProgram) + .vertexAttribPointer(this.cubeWireframeVertexBuffer, this.boxWireframeProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) - .vertexAttribPointer(this.cubeWireframeVertexBuffer, this.boxWireframeProgram.getAttribLocation('a_cubeVertexPosition'), 3, wgl.FLOAT, wgl.FALSE, 0, 0) + .bindIndexBuffer(this.cubeWireframeIndexBuffer) - .bindIndexBuffer(this.cubeWireframeIndexBuffer) + .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) + .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) - .uniformMatrix4fv('u_projectionMatrix', false, this.projectionMatrix) - .uniformMatrix4fv('u_viewMatrix', false, this.camera.getViewMatrix()) + for (var i = 0; i < this.boxes.length; ++i) { + var box = this.boxes[i] - - for (var i = 0; i < this.boxes.length; ++i) { - var box = this.boxes[i]; - - boxWireframeDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2]) - .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]); + boxWireframeDrawState.uniform3f('u_translation', box.min[0], box.min[1], box.min[2]) + .uniform3f('u_scale', box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]) - wgl.drawElements(boxWireframeDrawState, wgl.LINES, 24, wgl.UNSIGNED_SHORT); - } + wgl.drawElements(boxWireframeDrawState, wgl.LINES, 24, wgl.UNSIGNED_SHORT) + } + } +} +function quantize (x, step) { + return Math.round(x / step) * step +} - } +function quantizeVector (v, step) { + for (var i = 0; i < v.length; ++i) { + v[i] = quantize(v[i], step) + } - return { - BoxEditor: BoxEditor, - AABB: AABB, - InteractionMode: InteractionMode - }; -}()); + return v +} diff --git a/camera.js b/camera.js index 50168c2..37debbf 100644 --- a/camera.js +++ b/camera.js @@ -1,138 +1,134 @@ -'use strict' +import Utilities from './utilities.js' -var Camera = (function () { - var SENSITIVITY = 0.005; +var SENSITIVITY = 0.005 - var MIN_DISTANCE = 25.0; - var MAX_DISTANCE = 60.0; +var MIN_DISTANCE = 25.0 +var MAX_DISTANCE = 60.0 - function Camera (element, orbitPoint) { - this.element = element; - this.distance = 40.0; - this.orbitPoint = orbitPoint; +export default class Camera { + constructor (element, orbitPoint) { + this.element = element + this.distance = 40.0 + this.orbitPoint = orbitPoint - this.azimuth = 0.0, - this.elevation = 0.25 + this.azimuth = 0.0 + this.elevation = 0.25 - this.minElevation = -Math.PI / 4; - this.maxElevation = Math.PI / 4; + this.minElevation = -Math.PI / 4 + this.maxElevation = Math.PI / 4 - this.currentMouseX = 0, - this.currentMouseY = 0; + this.currentMouseX = 0 + this.currentMouseY = 0 - this.lastMouseX = 0, - this.lastMouseY = 0; + this.lastMouseX = 0 + this.lastMouseY = 0 - this.mouseDown = false; + this.mouseDown = false - this.viewMatrix = new Float32Array(16); + this.viewMatrix = new Float32Array(16) + this.recomputeViewMatrix() - this.recomputeViewMatrix(); + element.addEventListener('wheel', function (event) { + var scrollDelta = event.deltaY + this.distance += ((scrollDelta > 0) ? 1 : -1) * 2.0 + if (this.distance < MIN_DISTANCE) { this.distance = MIN_DISTANCE } + if (this.distance > MAX_DISTANCE) { this.distance = MAX_DISTANCE } - element.addEventListener('wheel', (function (event) { - var scrollDelta = event.deltaY; - this.distance += ((scrollDelta > 0) ? 1 : -1) * 2.0; + this.recomputeViewMatrix() + }.bind(this)) + } - if (this.distance < MIN_DISTANCE) this.distance = MIN_DISTANCE; - if (this.distance > MAX_DISTANCE) this.distance = MAX_DISTANCE; + recomputeViewMatrix () { + var xRotationMatrix = new Float32Array(16) + var yRotationMatrix = new Float32Array(16) + var distanceTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)) + var orbitTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)) - this.recomputeViewMatrix(); - }).bind(this)); - }; + Utilities.makeIdentityMatrix(this.viewMatrix) - Camera.prototype.recomputeViewMatrix = function () { - var xRotationMatrix = new Float32Array(16), - yRotationMatrix = new Float32Array(16), - distanceTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)), - orbitTranslationMatrix = Utilities.makeIdentityMatrix(new Float32Array(16)); + Utilities.makeXRotationMatrix(xRotationMatrix, this.elevation) + Utilities.makeYRotationMatrix(yRotationMatrix, this.azimuth) + distanceTranslationMatrix[14] = -this.distance + orbitTranslationMatrix[12] = -this.orbitPoint[0] + orbitTranslationMatrix[13] = -this.orbitPoint[1] + orbitTranslationMatrix[14] = -this.orbitPoint[2] - Utilities.makeIdentityMatrix(this.viewMatrix); + Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, orbitTranslationMatrix) + Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, yRotationMatrix) + Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, xRotationMatrix) + Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, distanceTranslationMatrix) + } - Utilities.makeXRotationMatrix(xRotationMatrix, this.elevation); - Utilities.makeYRotationMatrix(yRotationMatrix, this.azimuth); - distanceTranslationMatrix[14] = -this.distance; - orbitTranslationMatrix[12] = -this.orbitPoint[0]; - orbitTranslationMatrix[13] = -this.orbitPoint[1]; - orbitTranslationMatrix[14] = -this.orbitPoint[2]; + getPosition () { + var position = [ + this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.sin(-this.azimuth) + this.orbitPoint[0], + this.distance * Math.cos(Math.PI / 2 - this.elevation) + this.orbitPoint[1], + this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.cos(-this.azimuth) + this.orbitPoint[2] + ] - Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, orbitTranslationMatrix); - Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, yRotationMatrix); - Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, xRotationMatrix); - Utilities.premultiplyMatrix(this.viewMatrix, this.viewMatrix, distanceTranslationMatrix); - }; + return position + } - Camera.prototype.getPosition = function () { - var position = [ - this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.sin(-this.azimuth) + this.orbitPoint[0], - this.distance * Math.cos(Math.PI / 2 - this.elevation) + this.orbitPoint[1], - this.distance * Math.sin(Math.PI / 2 - this.elevation) * Math.cos(-this.azimuth) + this.orbitPoint[2] - ]; + isMouseDown () { + return this.mouseDown + } - return position; - }; + getViewMatrix () { + return this.viewMatrix + } - Camera.prototype.isMouseDown = function () { - return this.mouseDown; - }; + setBounds (minElevation, maxElevation) { + this.minElevation = minElevation + this.maxElevation = maxElevation - Camera.prototype.getViewMatrix = function () { - return this.viewMatrix; - }; + if (this.elevation > this.maxElevation) { this.elevation = this.maxElevation } + if (this.elevation < this.minElevation) { this.elevation = this.minElevation } - Camera.prototype.setBounds = function (minElevation, maxElevation) { - this.minElevation = minElevation; - this.maxElevation = maxElevation; + this.recomputeViewMatrix() + } - if (this.elevation > this.maxElevation) this.elevation = this.maxElevation; - if (this.elevation < this.minElevation) this.elevation = this.minElevation; + onMouseDown (event) { + event.preventDefault() - this.recomputeViewMatrix(); - }; + var x = Utilities.getMousePosition(event, this.element).x + var y = Utilities.getMousePosition(event, this.element).y - Camera.prototype.onMouseDown = function (event) { - event.preventDefault(); + this.mouseDown = true + this.lastMouseX = x + this.lastMouseY = y + } - var x = Utilities.getMousePosition(event, this.element).x; - var y = Utilities.getMousePosition(event, this.element).y; + onMouseUp (event) { + event.preventDefault() - this.mouseDown = true; - this.lastMouseX = x; - this.lastMouseY = y; - }; + this.mouseDown = false + } - Camera.prototype.onMouseUp = function (event) { - event.preventDefault(); + onMouseMove (event) { + event.preventDefault() - this.mouseDown = false; - }; + var x = Utilities.getMousePosition(event, this.element).x + var y = Utilities.getMousePosition(event, this.element).y - Camera.prototype.onMouseMove = function (event) { - event.preventDefault(); + if (this.mouseDown) { + this.currentMouseX = x + this.currentMouseY = y - var x = Utilities.getMousePosition(event, this.element).x; - var y = Utilities.getMousePosition(event, this.element).y; + var deltaAzimuth = (this.currentMouseX - this.lastMouseX) * SENSITIVITY + var deltaElevation = (this.currentMouseY - this.lastMouseY) * SENSITIVITY - if (this.mouseDown) { - this.currentMouseX = x; - this.currentMouseY = y; + this.azimuth += deltaAzimuth + this.elevation += deltaElevation - var deltaAzimuth = (this.currentMouseX - this.lastMouseX) * SENSITIVITY; - var deltaElevation = (this.currentMouseY - this.lastMouseY) * SENSITIVITY; + if (this.elevation > this.maxElevation) { this.elevation = this.maxElevation } + if (this.elevation < this.minElevation) { this.elevation = this.minElevation } - this.azimuth += deltaAzimuth; - this.elevation += deltaElevation; + this.recomputeViewMatrix() - if (this.elevation > this.maxElevation) this.elevation = this.maxElevation; - if (this.elevation < this.minElevation) this.elevation = this.minElevation; - - this.recomputeViewMatrix(); - - this.lastMouseX = this.currentMouseX; - this.lastMouseY = this.currentMouseY; - } - }; - - return Camera; -}()); + this.lastMouseX = this.currentMouseX + this.lastMouseY = this.currentMouseY + } + } +} diff --git a/fluid.feature.js b/fluid.feature.js new file mode 100644 index 0000000..1bc7073 --- /dev/null +++ b/fluid.feature.js @@ -0,0 +1,58 @@ +import { Water } from 'three/examples/jsm/objects/Water.js' + +// +// +// +// +// +// +// +// +// + +function concatenateWords (list) { + if (list.length === 0) { + return '' + } else if (list.length === 1) { + return "'" + list[0] + "'" + } else { + var result = '' + for (var i = 0; i < list.length; ++i) { + result += "'" + list[i] + "'" + if (i < list.length - 1) { + result += i < list.length - 2 ? ', ' : ' and ' + } + } + + return result + } +} + +WrappedGL.checkWebGLSupportWithExtensions(['ANGLE_instanced_arrays', 'WEBGL_depth_texture', 'OES_texture_float', 'OES_texture_float_linear', 'OES_texture_half_float', 'OES_texture_half_float_linear'], + function () { // we have webgl + document.getElementById('placeholder').outerHTML = document.getElementById('main').innerHTML + var fluidBox = new FluidParticles() + }, function (hasWebGL, unsupportedExtensions) { + document.getElementById('placeholder').outerHTML = document.getElementById('no-support').innerHTML + if (!hasWebGL) { // webgl not supported + document.getElementById('error').textContent = 'Unfortunately, your browser does not support WebGL' + } else { + document.getElementById('error').textContent = 'Unfortunately, your browser does not support the ' + concatenateWords(unsupportedExtensions) + ' WebGL extension' + (unsupportedExtensions.length > 1 ? 's.' : '.') + } + } +) + +export default { + props: { + oceanSide: { type: 'number', default: 2000 }, + size: { type: 'number', default: 1.0 }, + distortionScale: { type: 'number', default: 3.7 }, + alpha: { type: 'number', default: 1.0 }, + sunColor: { type: 'color', default: 0xffffff }, + waterColor: { type: 'color', default: 0x001e0f } + }, + + render ({ scene, THREE }) { + + } +} diff --git a/fluidparticles.js b/fluidparticles.js index 52c338a..9e655cb 100644 --- a/fluidparticles.js +++ b/fluidparticles.js @@ -1,377 +1,364 @@ -'use strict' - -var FluidParticles = (function () { - var FOV = Math.PI / 3; - - var State = { - EDITING: 0, - SIMULATING: 1 - }; - - var GRID_WIDTH = 40, - GRID_HEIGHT = 20, - GRID_DEPTH = 20; - - var PARTICLES_PER_CELL = 10; - - function FluidParticles () { - - var canvas = this.canvas = document.getElementById('canvas'); - var wgl = this.wgl = new WrappedGL(canvas); - - window.wgl = wgl; - - this.projectionMatrix = Utilities.makePerspectiveMatrix(new Float32Array(16), FOV, this.canvas.width / this.canvas.height, 0.1, 100.0); - this.camera = new Camera(this.canvas, [GRID_WIDTH / 2, GRID_HEIGHT / 3, GRID_DEPTH / 2]); - - var boxEditorLoaded = false, - simulatorRendererLoaded = false; - - this.boxEditor = new BoxEditor.BoxEditor(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], (function () { - boxEditorLoaded = true; - if (boxEditorLoaded && simulatorRendererLoaded) { - start.call(this); - } - }).bind(this), - (function () { - this.redrawUI(); - }).bind(this)); - - this.simulatorRenderer = new SimulatorRenderer(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], (function () { - simulatorRendererLoaded = true; - if (boxEditorLoaded && simulatorRendererLoaded) { - start.call(this); - } - }).bind(this)); - - function start(programs) { - this.state = State.EDITING; - - this.startButton = document.getElementById('start-button'); - - this.startButton.addEventListener('click', (function () { - if (this.state === State.EDITING) { - if (this.boxEditor.boxes.length > 0) { - this.startSimulation(); - } - this.redrawUI(); - } else if (this.state === State.SIMULATING) { - this.stopSimulation(); - this.redrawUI(); - } - }).bind(this)); - - this.currentPresetIndex = 0; - this.editedSinceLastPreset = false; //whether the user has edited the last set preset - var PRESETS = [ - //dam break - [ - new BoxEditor.AABB([0, 0, 0], [15, 20, 20]) - ], - - //block drop - [ - new BoxEditor.AABB([0, 0, 0], [40, 7, 20]), - new BoxEditor.AABB([12, 12, 5], [28, 20, 15]) - ], - - //double splash - [ - new BoxEditor.AABB([0, 0, 0], [10, 20, 15]), - new BoxEditor.AABB([30, 0, 5], [40, 20, 20]) - ], - - ]; - - this.presetButton = document.getElementById('preset-button'); - this.presetButton.addEventListener('click', (function () { - this.editedSinceLastPreset = false; - - this.boxEditor.boxes.length = 0; - - var preset = PRESETS[this.currentPresetIndex]; - for (var i = 0; i < preset.length; ++i) { - this.boxEditor.boxes.push(preset[i].clone()); - } - - this.currentPresetIndex = (this.currentPresetIndex + 1) % PRESETS.length; - - this.redrawUI(); - - }).bind(this)); - - - - //////////////////////////////////////////////////////// - // parameters/sliders +import Utilities from './utilities.js' +import Camera from './camera.js' +import { BoxEditor, AABB, InteractionMode } from './boxeditor.js' +import WrappedGL from './wrappedgl.js' +import SimulatorRenderer from './simulatorrenderer.js' +import Slider from './slider.js' + +var FOV = Math.PI / 3 + +var State = { + EDITING: 0, + SIMULATING: 1 +} + +var GRID_WIDTH = 40 +var GRID_HEIGHT = 20 +var GRID_DEPTH = 20 + +var PARTICLES_PER_CELL = 10 + +export default class FluidParticles { + constructor () { + var canvas = this.canvas = document.getElementById('canvas') + var wgl = this.wgl = new WrappedGL(canvas) + + window.wgl = wgl + + this.projectionMatrix = Utilities.makePerspectiveMatrix(new Float32Array(16), FOV, this.canvas.width / this.canvas.height, 0.1, 100.0) + this.camera = new Camera(this.canvas, [GRID_WIDTH / 2, GRID_HEIGHT / 3, GRID_DEPTH / 2]) + + var boxEditorLoaded = false + var simulatorRendererLoaded = false + + this.boxEditor = new BoxEditor(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], function () { + boxEditorLoaded = true + if (boxEditorLoaded && simulatorRendererLoaded) { + start.call(this) + } + }.bind(this), + function () { + this.redrawUI() + }.bind(this)) + + this.simulatorRenderer = new SimulatorRenderer(this.canvas, this.wgl, this.projectionMatrix, this.camera, [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH], function () { + simulatorRendererLoaded = true + if (boxEditorLoaded && simulatorRendererLoaded) { + start.call(this) + } + }.bind(this)) + + function start (programs) { + this.state = State.EDITING + + this.startButton = document.getElementById('start-button') + + this.startButton.addEventListener('click', function () { + if (this.state === State.EDITING) { + if (this.boxEditor.boxes.length > 0) { + this.startSimulation() + } + this.redrawUI() + } else if (this.state === State.SIMULATING) { + this.stopSimulation() + this.redrawUI() + } + }.bind(this)) + + this.currentPresetIndex = 0 + this.editedSinceLastPreset = false // whether the user has edited the last set preset + var PRESETS = [ + // dam break + [ + new AABB([0, 0, 0], [15, 20, 20]) + ], + + // block drop + [ + new AABB([0, 0, 0], [40, 7, 20]), + new AABB([12, 12, 5], [28, 20, 15]) + ], + + // double splash + [ + new AABB([0, 0, 0], [10, 20, 15]), + new AABB([30, 0, 5], [40, 20, 20]) + ] + ] + + this.presetButton = document.getElementById('preset-button') + this.presetButton.addEventListener('click', function () { + this.editedSinceLastPreset = false + + this.boxEditor.boxes.length = 0 + + var preset = PRESETS[this.currentPresetIndex] + for (var i = 0; i < preset.length; ++i) { + this.boxEditor.boxes.push(preset[i].clone()) + } - //using gridCellDensity ensures a linear relationship to particle count - this.gridCellDensity = 0.5; //simulation grid cell density per world space unit volume + this.currentPresetIndex = (this.currentPresetIndex + 1) % PRESETS.length - this.timeStep = 1.0 / 60.0; + this.redrawUI() + }.bind(this)) - this.densitySlider = new Slider(document.getElementById('density-slider'), this.gridCellDensity, 0.2, 3.0, (function (value) { - this.gridCellDensity = value; + /// ///////////////////////////////////////////////////// + // parameters/sliders + // using gridCellDensity ensures a linear relationship to particle count + this.gridCellDensity = 0.5 // simulation grid cell density per world space unit volume - this.redrawUI(); - }).bind(this)); + this.timeStep = 1.0 / 60.0 - this.flipnessSlider = new Slider(document.getElementById('fluidity-slider'), this.simulatorRenderer.simulator.flipness, 0.5, 0.99, (function (value) { - this.simulatorRenderer.simulator.flipness = value; - }).bind(this)); + this.densitySlider = new Slider(document.getElementById('density-slider'), this.gridCellDensity, 0.2, 3.0, function (value) { + this.gridCellDensity = value - this.speedSlider = new Slider(document.getElementById('speed-slider'), this.timeStep, 0.0, 1.0 / 60.0, (function (value) { - this.timeStep = value; - }).bind(this)); + this.redrawUI() + }.bind(this)) + this.flipnessSlider = new Slider(document.getElementById('fluidity-slider'), this.simulatorRenderer.simulator.flipness, 0.5, 0.99, function (value) { + this.simulatorRenderer.simulator.flipness = value + }.bind(this)) - this.redrawUI(); + this.speedSlider = new Slider(document.getElementById('speed-slider'), this.timeStep, 0.0, 1.0 / 60.0, function (value) { + this.timeStep = value + }.bind(this)) + this.redrawUI() - this.presetButton.click(); + this.presetButton.click() - /////////////////////////////////////////////////////// - // interaction state stuff + /// //////////////////////////////////////////////////// + // interaction state stuff + canvas.addEventListener('mousemove', this.onMouseMove.bind(this)) + canvas.addEventListener('mousedown', this.onMouseDown.bind(this)) + document.addEventListener('mouseup', this.onMouseUp.bind(this)) - canvas.addEventListener('mousemove', this.onMouseMove.bind(this)); - canvas.addEventListener('mousedown', this.onMouseDown.bind(this)); - document.addEventListener('mouseup', this.onMouseUp.bind(this)); + document.addEventListener('keydown', this.onKeyDown.bind(this)) + document.addEventListener('keyup', this.onKeyUp.bind(this)) - document.addEventListener('keydown', this.onKeyDown.bind(this)); - document.addEventListener('keyup', this.onKeyUp.bind(this)); + window.addEventListener('resize', this.onResize.bind(this)) + this.onResize() - window.addEventListener('resize', this.onResize.bind(this)); - this.onResize(); + /// ///////////////////////////////////////////////// + // start the update loop + var lastTime = 0 + var update = function (currentTime) { + var deltaTime = currentTime - lastTime || 0 + lastTime = currentTime + this.update(deltaTime) - //////////////////////////////////////////////////// - // start the update loop + requestAnimationFrame(update) + }.bind(this) + update() + } + } - var lastTime = 0; - var update = (function (currentTime) { - var deltaTime = currentTime - lastTime || 0; - lastTime = currentTime; + onResize (event) { + this.canvas.width = window.innerWidth + this.canvas.height = window.innerHeight + Utilities.makePerspectiveMatrix(this.projectionMatrix, FOV, this.canvas.width / this.canvas.height, 0.1, 100.0) - this.update(deltaTime); + this.simulatorRenderer.onResize(event) + } - requestAnimationFrame(update); - }).bind(this); - update(); + onMouseMove (event) { + event.preventDefault() + if (this.state === State.EDITING) { + this.boxEditor.onMouseMove(event) - } + if (this.boxEditor.interactionState !== null) { + this.editedSinceLastPreset = true + } + } else if (this.state === State.SIMULATING) { + this.simulatorRenderer.onMouseMove(event) } + } - FluidParticles.prototype.onResize = function (event) { - this.canvas.width = window.innerWidth; - this.canvas.height = window.innerHeight; - Utilities.makePerspectiveMatrix(this.projectionMatrix, FOV, this.canvas.width / this.canvas.height, 0.1, 100.0); + onMouseDown (event) { + event.preventDefault() - this.simulatorRenderer.onResize(event); + if (this.state === State.EDITING) { + this.boxEditor.onMouseDown(event) + } else if (this.state === State.SIMULATING) { + this.simulatorRenderer.onMouseDown(event) } + } - FluidParticles.prototype.onMouseMove = function (event) { - event.preventDefault(); - - if (this.state === State.EDITING) { - this.boxEditor.onMouseMove(event); - - if (this.boxEditor.interactionState !== null) { - this.editedSinceLastPreset = true; - } - } else if (this.state === State.SIMULATING) { - this.simulatorRenderer.onMouseMove(event); - } - }; - - FluidParticles.prototype.onMouseDown = function (event) { - event.preventDefault(); - - if (this.state === State.EDITING) { - this.boxEditor.onMouseDown(event); - } else if (this.state === State.SIMULATING) { - this.simulatorRenderer.onMouseDown(event); - } - }; - - FluidParticles.prototype.onMouseUp = function (event) { - event.preventDefault(); - - if (this.state === State.EDITING) { - this.boxEditor.onMouseUp(event); - } else if (this.state === State.SIMULATING) { - this.simulatorRenderer.onMouseUp(event); - } - }; - - FluidParticles.prototype.onKeyDown = function (event) { - if (this.state === State.EDITING) { - this.boxEditor.onKeyDown(event); - } - }; - - FluidParticles.prototype.onKeyUp = function (event) { - if (this.state === State.EDITING) { - this.boxEditor.onKeyUp(event); - } - }; - - //the UI elements are all created in the constructor, this just updates the DOM elements - //should be called every time state changes - FluidParticles.prototype.redrawUI = function () { - - var simulatingElements = document.querySelectorAll('.simulating-ui'); - var editingElements = document.querySelectorAll('.editing-ui'); + onMouseUp (event) { + event.preventDefault() + if (this.state === State.EDITING) { + this.boxEditor.onMouseUp(event) + } else if (this.state === State.SIMULATING) { + this.simulatorRenderer.onMouseUp(event) + } + } - if (this.state === State.SIMULATING) { - for (var i = 0; i < simulatingElements.length; ++i) { - simulatingElements[i].style.display = 'block'; - } + onKeyDown (event) { + if (this.state === State.EDITING) { + this.boxEditor.onKeyDown(event) + } + } - for (var i = 0; i < editingElements.length; ++i) { - editingElements[i].style.display = 'none'; - } + onKeyUp (event) { + if (this.state === State.EDITING) { + this.boxEditor.onKeyUp(event) + } + } + + // the UI elements are all created in the constructor, this just updates the DOM elements + // should be called every time state changes + redrawUI () { + var simulatingElements = document.querySelectorAll('.simulating-ui') + var editingElements = document.querySelectorAll('.editing-ui') + + if (this.state === State.SIMULATING) { + for (var i = 0; i < simulatingElements.length; ++i) { + simulatingElements[i].style.display = 'block' + } + + for (var i = 0; i < editingElements.length; ++i) { + editingElements[i].style.display = 'none' + } + + this.startButton.textContent = 'Edit' + this.startButton.className = 'start-button-active' + } else if (this.state === State.EDITING) { + for (var i = 0; i < simulatingElements.length; ++i) { + simulatingElements[i].style.display = 'none' + } + + for (var i = 0; i < editingElements.length; ++i) { + editingElements[i].style.display = 'block' + } + + document.getElementById('particle-count').innerHTML = this.getParticleCount().toFixed(0) + ' particles' + + if (this.boxEditor.boxes.length >= 2 || + this.boxEditor.boxes.length === 1 && + (this.boxEditor.interactionState === null || + this.boxEditor.interactionState.mode !== InteractionMode.EXTRUDING && + this.boxEditor.interactionState.mode !== InteractionMode.DRAWING) + ) { + this.startButton.className = 'start-button-active' + } else { + this.startButton.className = 'start-button-inactive' + } + + this.startButton.textContent = 'Start' + + if (this.editedSinceLastPreset) { + this.presetButton.innerHTML = 'Use Preset' + } else { + this.presetButton.innerHTML = 'Next Preset' + } + } + this.flipnessSlider.redraw() + this.densitySlider.redraw() + this.speedSlider.redraw() + } - this.startButton.textContent = 'Edit'; - this.startButton.className = 'start-button-active'; - } else if (this.state === State.EDITING) { - for (var i = 0; i < simulatingElements.length; ++i) { - simulatingElements[i].style.display = 'none'; - } + // compute the number of particles for the current boxes and grid density + getParticleCount () { + var boxEditor = this.boxEditor - for (var i = 0; i < editingElements.length; ++i) { - editingElements[i].style.display = 'block'; - } + var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity - document.getElementById('particle-count').innerHTML = this.getParticleCount().toFixed(0) + ' particles'; + // assuming x:y:z ratio of 2:1:1 + var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0)) + var gridResolutionZ = gridResolutionY * 1 + var gridResolutionX = gridResolutionY * 2 - if (this.boxEditor.boxes.length >= 2 || - this.boxEditor.boxes.length === 1 && (this.boxEditor.interactionState === null || this.boxEditor.interactionState.mode !== BoxEditor.InteractionMode.EXTRUDING && this.boxEditor.interactionState.mode !== BoxEditor.InteractionMode.DRAWING)) { - this.startButton.className = 'start-button-active'; - } else { - this.startButton.className = 'start-button-inactive'; - } + var totalGridCells = gridResolutionX * gridResolutionY * gridResolutionZ - this.startButton.textContent = 'Start'; + var totalVolume = 0 + var cumulativeVolume = [] // at index i, contains the total volume up to and including box i (so index 0 has volume of first box, last index has total volume) - if (this.editedSinceLastPreset) { - this.presetButton.innerHTML = 'Use Preset'; - } else { - this.presetButton.innerHTML = 'Next Preset'; - } - } + for (var i = 0; i < boxEditor.boxes.length; ++i) { + var box = boxEditor.boxes[i] + var volume = box.computeVolume() - this.flipnessSlider.redraw(); - this.densitySlider.redraw(); - this.speedSlider.redraw(); + totalVolume += volume + cumulativeVolume[i] = totalVolume } + var fractionFilled = totalVolume / (GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH) - //compute the number of particles for the current boxes and grid density - FluidParticles.prototype.getParticleCount = function () { - var boxEditor = this.boxEditor; - - var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity; - - //assuming x:y:z ratio of 2:1:1 - var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0)); - var gridResolutionZ = gridResolutionY * 1; - var gridResolutionX = gridResolutionY * 2; - - var totalGridCells = gridResolutionX * gridResolutionY * gridResolutionZ; + var desiredParticleCount = fractionFilled * totalGridCells * PARTICLES_PER_CELL // theoretical number of particles + return desiredParticleCount + } - var totalVolume = 0; - var cumulativeVolume = []; //at index i, contains the total volume up to and including box i (so index 0 has volume of first box, last index has total volume) + // begin simulation using boxes from box editor + // EDITING -> SIMULATING + startSimulation () { + this.state = State.SIMULATING - for (var i = 0; i < boxEditor.boxes.length; ++i) { - var box = boxEditor.boxes[i]; - var volume = box.computeVolume(); + var desiredParticleCount = this.getParticleCount() // theoretical number of particles + var particlesWidth = 512 // we fix particlesWidth + var particlesHeight = Math.ceil(desiredParticleCount / particlesWidth) // then we calculate the particlesHeight that produces the closest particle count - totalVolume += volume; - cumulativeVolume[i] = totalVolume; - } - - var fractionFilled = totalVolume / (GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH); + var particleCount = particlesWidth * particlesHeight + var particlePositions = [] - var desiredParticleCount = fractionFilled * totalGridCells * PARTICLES_PER_CELL; //theoretical number of particles + var boxEditor = this.boxEditor - return desiredParticleCount; + var totalVolume = 0 + for (var i = 0; i < boxEditor.boxes.length; ++i) { + totalVolume += boxEditor.boxes[i].computeVolume() } - //begin simulation using boxes from box editor - //EDITING -> SIMULATING - FluidParticles.prototype.startSimulation = function () { - this.state = State.SIMULATING; + var particlesCreatedSoFar = 0 + for (var i = 0; i < boxEditor.boxes.length; ++i) { + var box = boxEditor.boxes[i] - var desiredParticleCount = this.getParticleCount(); //theoretical number of particles - var particlesWidth = 512; //we fix particlesWidth - var particlesHeight = Math.ceil(desiredParticleCount / particlesWidth); //then we calculate the particlesHeight that produces the closest particle count + var particlesInBox = 0 + if (i < boxEditor.boxes.length - 1) { + particlesInBox = Math.floor(particleCount * box.computeVolume() / totalVolume) + } else { // for the last box we just use up all the remaining particles + particlesInBox = particleCount - particlesCreatedSoFar + } - var particleCount = particlesWidth * particlesHeight; - var particlePositions = []; - - var boxEditor = this.boxEditor; + for (var j = 0; j < particlesInBox; ++j) { + var position = box.randomPoint() + particlePositions.push(position) + } - var totalVolume = 0; - for (var i = 0; i < boxEditor.boxes.length; ++i) { - totalVolume += boxEditor.boxes[i].computeVolume(); - } - - var particlesCreatedSoFar = 0; - for (var i = 0; i < boxEditor.boxes.length; ++i) { - var box = boxEditor.boxes[i]; - - var particlesInBox = 0; - if (i < boxEditor.boxes.length - 1) { - particlesInBox = Math.floor(particleCount * box.computeVolume() / totalVolume); - } else { //for the last box we just use up all the remaining particles - particlesInBox = particleCount - particlesCreatedSoFar; - } - - for (var j = 0; j < particlesInBox; ++j) { - var position = box.randomPoint(); - particlePositions.push(position); - } - - particlesCreatedSoFar += particlesInBox; - } + particlesCreatedSoFar += particlesInBox + } - var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity; + var gridCells = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH * this.gridCellDensity - //assuming x:y:z ratio of 2:1:1 - var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0)); - var gridResolutionZ = gridResolutionY * 1; - var gridResolutionX = gridResolutionY * 2; + // assuming x:y:z ratio of 2:1:1 + var gridResolutionY = Math.ceil(Math.pow(gridCells / 2, 1.0 / 3.0)) + var gridResolutionZ = gridResolutionY * 1 + var gridResolutionX = gridResolutionY * 2 + var gridSize = [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH] + var gridResolution = [gridResolutionX, gridResolutionY, gridResolutionZ] - var gridSize = [GRID_WIDTH, GRID_HEIGHT, GRID_DEPTH]; - var gridResolution = [gridResolutionX, gridResolutionY, gridResolutionZ]; + var sphereRadius = 7.0 / gridResolutionX + this.simulatorRenderer.reset(particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, PARTICLES_PER_CELL, sphereRadius) - var sphereRadius = 7.0 / gridResolutionX; - this.simulatorRenderer.reset(particlesWidth, particlesHeight, particlePositions, gridSize, gridResolution, PARTICLES_PER_CELL, sphereRadius); + this.camera.setBounds(0, Math.PI / 2) + } - this.camera.setBounds(0, Math.PI / 2); - } + // go back to box editing + // SIMULATING -> EDITING + stopSimulation () { + this.state = State.EDITING - //go back to box editing - //SIMULATING -> EDITING - FluidParticles.prototype.stopSimulation = function () { - this.state = State.EDITING; + this.camera.setBounds(-Math.PI / 4, Math.PI / 4) + } - this.camera.setBounds(-Math.PI / 4, Math.PI / 4); + update () { + if (this.state === State.EDITING) { + this.boxEditor.draw() + } else if (this.state === State.SIMULATING) { + this.simulatorRenderer.update(this.timeStep) } - - FluidParticles.prototype.update = function () { - if (this.state === State.EDITING) { - this.boxEditor.draw(); - } else if (this.state === State.SIMULATING) { - this.simulatorRenderer.update(this.timeStep); - } - } - - return FluidParticles; -}()); - + } +} diff --git a/index.html b/index.html index d4c88f9..ec354cc 100644 --- a/index.html +++ b/index.html @@ -75,50 +75,7 @@
- - - - - - - - - - - - +