├── .gitignore ├── LICENSE ├── README.md ├── demo.js ├── example.js ├── gif ├── hexagon.gif ├── square.gif └── triangle.gif ├── index.html ├── index.js ├── package.json ├── shaders ├── demo.frag ├── demo.vert ├── example.frag └── example.vert ├── shapes ├── circle.js ├── heart.js ├── hexagon.js ├── square.js └── triangle.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | bundle.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeremy Freeman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extrude 2 | 3 | Use extrusion to turn a 2d shape into a 3d mesh. Extrusion is the process of "pulling" a 2d shape through space to make it 3d. This module contains a single function that accepts a collection of 2d points, and returns a 3d mesh in the form of a [`simplicial complex`](https://github.com/mikolalysenko/simplicial-complex), a data structure that works well with the [`stack.gl`](http://stack.gl/) ecosystem. The implementation uses seidel's algorithm to triangulate the top and bottom faces, and simple triangulated rectangles for the sides. 4 | 5 | View a [demo](http://freeman-lab.github.io/extrude). 6 | 7 | 8 |  9 | 10 | [](https://github.com/feross/standard) 11 | 12 | 13 | ## install 14 | 15 | To use in your project 16 | 17 | ```javascript 18 | npm install extrude 19 | ``` 20 | 21 | To see an example, clone this repo, then call 22 | 23 | ```javascript 24 | npm install 25 | npm start 26 | ``` 27 | and it should open a browser with a floating square. You can also try 28 | 29 | ```javascript 30 | npm run demo 31 | ``` 32 | for a demo with several shapes. 33 | 34 | ## example 35 | 36 | Assuming you already have a stack.gl context `gl`, make a cube like this! 37 | 38 | ```javascript 39 | var extrude = require('extrude') 40 | 41 | var points = [[-1, -1], [1, -1], [1, 1], [-1, 1]] 42 | var cube = extrude(points, {bottom: -1, top: 1}) 43 | 44 | var geometry = require('gl-geometry')(gl) 45 | geometry.attr('position', cube.positions) 46 | geometry.faces(cube.cells) 47 | ``` 48 | 49 | See [`example.js`](example.js) for a complete end-to-end example. 50 | 51 | ## usage 52 | 53 | #### `complex = extrude(points, opts)` 54 | 55 | Create a simplicial complex from a set of points. 56 | 57 | `points` should be a list in the form `[[x, y], [x, y], ...]` 58 | 59 | `complex` has two attributes: 60 | - `complex.position` : array of 3d vertices `[[x, y, z], [x, y, z], ...]` 61 | - `complex.cells` : array of tuples that index into the vertices `[[i, j, k], [i, j, k], ...]` 62 | 63 | `opts` can include the following options: 64 | - `opts.bottom` : bottom of the extruded object `default: 0` 65 | - `opts.top` : top of the extruded object `default: 1` 66 | - `opts.closed` : whether to close the top and bottom of the mesh `default: true` 67 | 68 | If `top` and `bottom` are equal it will result in a single-sided 3d surface. 69 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | var Geometry = require('gl-geometry') 2 | var Shader = require('gl-shader') 3 | var context = require('gl-context') 4 | var mat4 = require('gl-mat4') 5 | var vignette = require('gl-vignette-background') 6 | var glslify = require('glslify') 7 | var orbit = require('canvas-orbit-camera') 8 | var fit = require('canvas-fit') 9 | var unindex = require('unindex-mesh') 10 | var reindex = require('mesh-reindex') 11 | var eye = require('eye-vector') 12 | var time = require('right-now') 13 | var normals = require('normals') 14 | var css = require('dom-css') 15 | var extrude = require('./index.js') 16 | var soundcloud = require('soundcloud-badge') 17 | var issafari = require('is-safari') 18 | var ismobile = require('is-mobile') 19 | var Analyser = require('web-audio-analyser') 20 | 21 | var canvas = document.body.appendChild(document.createElement('canvas')) 22 | css(canvas, {zIndex: -1000}) 23 | var gl = context(canvas, render) 24 | 25 | var message 26 | 27 | if (!ismobile() & issafari) { 28 | message = document.body.appendChild(document.createElement('div')) 29 | css(message, { 30 | position: 'absolute', left: '4%', top: '3%', 31 | color: 'white', fontFamily: 'GlacialIndifferenceRegular' 32 | }) 33 | message.innerHTML = 'for music view in Chrome or Firefox' 34 | } 35 | 36 | if (ismobile()) { 37 | message = document.body.appendChild(document.createElement('div')) 38 | css(message, { 39 | position: 'absolute', left: '4%', top: '3%', 40 | color: 'white', fontFamily: 'GlacialIndifferenceRegular', 41 | fontSize: 30 42 | }) 43 | message.innerHTML = 'for music view on Desktop' 44 | } 45 | 46 | if (!ismobile() & !issafari) { 47 | var audio = new Audio() 48 | audio.crossOrigin = 'Anonymous' 49 | var analyser = Analyser(audio) 50 | 51 | soundcloud({ 52 | client_id: 'cc4fb3b1e4b84004455321ad04a16580', 53 | song: 'https://soundcloud.com/constellation-records/cst025_track05', 54 | dark: false, 55 | getFonts: true 56 | }, function (err, src, data, div) { 57 | if (err) throw err 58 | audio.src = src 59 | audio.loop = true 60 | audio.addEventListener('canplay', function () { 61 | audio.play() 62 | }, false) 63 | }) 64 | } 65 | 66 | var camera = orbit(canvas) 67 | 68 | var shapes = ['triangle', 'square', 'hexagon', 'circle', 'heart'] 69 | var options = document.body.appendChild(document.createElement('div')) 70 | css(options, {position: 'absolute', right: '6%', top: '2%'}) 71 | 72 | var link = document.body.appendChild(document.createElement('div')) 73 | css(link, {position: 'absolute', right: '6%', bottom: '4.5%', textDecoration: 'none'}) 74 | link.innerHTML = 'github' 75 | link.addEventListener('click', function () { 76 | window.location.href = 'http://github.com/freeman-lab/extrude' 77 | }) 78 | var type = { 79 | fontFamily: 'GlacialIndifferenceRegular', 80 | borderBottom: 'solid 3px rgb(20,20,20)', 81 | borderLeft: 'solid 3px rgba(0, 0, 0, 0)', 82 | paddingBottom: 2, 83 | paddingLeft: 8, 84 | transition: '0.15s', 85 | width: '100%', 86 | cursor: 'pointer' 87 | } 88 | css(link, type) 89 | mouseover(link) 90 | mouseout(link) 91 | 92 | function mouseover (el) { 93 | el.addEventListener('mouseover', function () { 94 | css(el, {borderLeft: 'solid 3px rgb(20,20,20)'}) 95 | }) 96 | } 97 | 98 | function mouseout (el) { 99 | el.addEventListener('mouseout', function () { 100 | css(el, {borderLeft: 'solid 3px rgba(0, 0, 0, 0)'}) 101 | }) 102 | } 103 | 104 | var items = [] 105 | shapes.forEach(function (shape, i) { 106 | items[i] = options.appendChild(document.createElement('div')) 107 | items[i].innerHTML = shape 108 | css(items[i], type) 109 | mouseover(items[i]) 110 | mouseout(items[i]) 111 | items[i].addEventListener('click', function () { 112 | selection = i 113 | reload() 114 | }) 115 | }) 116 | 117 | function resize () { 118 | var w = Math.sqrt(window.innerWidth * 20) 119 | var s = Math.sqrt(window.innerWidth * 1.2) 120 | if (ismobile()) { 121 | w *= 1.4 122 | s *= 1.4 123 | } 124 | var m = window.innerWidth * 0.01 125 | css(options, {width: w}) 126 | css(link, {width: w, fontSize: s}) 127 | items.forEach(function (item) { 128 | css(item, {fontSize: s, marginBottom: m}) 129 | }) 130 | } 131 | 132 | resize() 133 | window.addEventListener('resize', resize, false) 134 | window.addEventListener('resize', fit(canvas), false) 135 | 136 | camera.lookAt([3, 3, 4], [0, 0, 0], [1, 0, 0]) 137 | 138 | var selection = 2 139 | var flattened, geometry, shape 140 | 141 | reload() 142 | 143 | function reload () { 144 | if (selection === 0) { 145 | shape = require('./shapes/triangle.js') 146 | } 147 | 148 | if (selection === 1) { 149 | shape = require('./shapes/square.js') 150 | } 151 | 152 | if (selection === 2) { 153 | shape = require('./shapes/hexagon.js') 154 | } 155 | 156 | if (selection === 3) { 157 | shape = require('./shapes/circle.js') 158 | } 159 | 160 | if (selection === 4) { 161 | shape = require('./shapes/heart.js') 162 | } 163 | 164 | var complex = extrude(shape.points, {top: 0.5, bottom: -0.5, closed: true}) 165 | 166 | geometry = Geometry(gl) 167 | flattened = unindex(complex.positions, complex.cells) 168 | complex = reindex(flattened) 169 | complex.normals = normals.vertexNormals(complex.cells, complex.positions) 170 | geometry.attr('position', complex.positions) 171 | geometry.attr('normal', complex.normals) 172 | geometry.faces(complex.cells) 173 | } 174 | 175 | var shader = Shader(gl, 176 | glslify('./shaders/demo.vert'), 177 | glslify('./shaders/demo.frag') 178 | ) 179 | 180 | var projection = mat4.create() 181 | var view = mat4.create() 182 | 183 | var background = vignette(gl) 184 | 185 | var rotate = 0.005 186 | var freq, scale 187 | 188 | function render () { 189 | var width = gl.drawingBufferWidth 190 | var height = gl.drawingBufferHeight 191 | 192 | var now = time() * 0.001 193 | var axis = Math.sin(now) * 2 194 | 195 | var aspect = width / height 196 | var fov = Math.PI / 4 197 | var near = 0.01 198 | var far = 1000 199 | mat4.perspective(projection, fov, aspect, near, far) 200 | 201 | if (!ismobile() & !issafari) { 202 | freq = analyser.frequencies().reduce(function (x, y) { return x + y }) 203 | rotate = freq / 1000000 204 | } 205 | 206 | camera.rotate([0, 0, 0], [axis * rotate, -rotate, 0]) 207 | 208 | camera.view(view) 209 | camera.tick() 210 | 211 | gl.viewport(0, 0, width, height) 212 | gl.clear(gl.COLOR_BUFFER_BIT) 213 | gl.disable(gl.DEPTH_TEST) 214 | 215 | if (ismobile()) { 216 | scale = [0.00038 * width, 0.00026 * height] 217 | } else { 218 | scale = [0.0007 * width, 0.0007 * height] 219 | } 220 | 221 | background.style({ 222 | scale: scale, 223 | smoothing: [-0.4, 0.6], 224 | aspect: aspect, 225 | color1: [0.95, 0.95, 0.95], 226 | color2: shape.colors[0], 227 | coloredNoise: false, 228 | noiseAlpha: 0.2, 229 | offset: [0, 0] 230 | }) 231 | 232 | background.draw() 233 | 234 | gl.enable(gl.DEPTH_TEST) 235 | 236 | geometry.bind(shader) 237 | shader.uniforms.projection = projection 238 | shader.uniforms.view = view 239 | shader.uniforms.eye = eye(view) 240 | shader.uniforms.color1 = shape.colors[0] 241 | shader.uniforms.color2 = shape.colors[1] 242 | geometry.draw(gl.TRIANGLES) 243 | geometry.unbind() 244 | } 245 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var Geometry = require('gl-geometry') 2 | var Shader = require('gl-shader') 3 | var context = require('gl-context') 4 | var mat4 = require('gl-mat4') 5 | var glslify = require('glslify') 6 | var orbit = require('canvas-orbit-camera') 7 | var fit = require('canvas-fit') 8 | var unindex = require('unindex-mesh') 9 | var reindex = require('mesh-reindex') 10 | var eye = require('eye-vector') 11 | var time = require('right-now') 12 | var normals = require('normals') 13 | var extrude = require('./index.js') 14 | 15 | var canvas = document.body.appendChild(document.createElement('canvas')) 16 | var camera = orbit(canvas) 17 | var gl = context(canvas, render) 18 | 19 | window.addEventListener('resize', fit(canvas), false) 20 | camera.lookAt([3, 3, 4], [0, 0, 0], [1, 0, 0]) 21 | 22 | var points = [[-1, -1], [1, -1], [1, 1], [-1, 1]] 23 | 24 | var complex = extrude(points, {top: 1, bottom: -1, closed: true}) 25 | 26 | var geometry = Geometry(gl) 27 | 28 | var flattened = unindex(complex.positions, complex.cells) 29 | complex = reindex(flattened) 30 | complex.normals = normals.vertexNormals(complex.cells, complex.positions) 31 | geometry.attr('position', complex.positions) 32 | geometry.attr('normal', complex.normals) 33 | geometry.faces(complex.cells) 34 | 35 | var shader = Shader(gl, 36 | glslify('./shaders/example.vert'), 37 | glslify('./shaders/example.frag') 38 | ) 39 | 40 | var projection = mat4.create() 41 | var view = mat4.create() 42 | 43 | function render () { 44 | var width = gl.drawingBufferWidth 45 | var height = gl.drawingBufferHeight 46 | 47 | var now = time() * 0.001 48 | var axis = Math.sin(now) * 2 49 | 50 | var aspect = width / height 51 | var fov = Math.PI / 4 52 | var near = 0.01 53 | var far = 1000 54 | mat4.perspective(projection, fov, aspect, near, far) 55 | 56 | camera.rotate([0, 0, 0], [axis * 0.005, -0.005, 0]) 57 | camera.view(view) 58 | camera.tick() 59 | 60 | gl.viewport(0, 0, width, height) 61 | gl.enable(gl.DEPTH_TEST) 62 | 63 | geometry.bind(shader) 64 | shader.uniforms.projection = projection 65 | shader.uniforms.view = view 66 | shader.uniforms.eye = eye(view) 67 | geometry.draw(gl.TRIANGLES) 68 | geometry.unbind() 69 | } 70 | -------------------------------------------------------------------------------- /gif/hexagon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeman-lab/extrude/4345ea1cb03b4c764e60b81b49cd74a282d27e8b/gif/hexagon.gif -------------------------------------------------------------------------------- /gif/square.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeman-lab/extrude/4345ea1cb03b4c764e60b81b49cd74a282d27e8b/gif/square.gif -------------------------------------------------------------------------------- /gif/triangle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeman-lab/extrude/4345ea1cb03b4c764e60b81b49cd74a282d27e8b/gif/triangle.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |