├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── base ├── dev.html ├── index.html ├── index.js ├── line-utils.js ├── template.hbs └── theme.css ├── compress.sh ├── dev.sh ├── expanded ├── frag.glsl ├── gl-line-2d.js ├── index.js └── vert.glsl ├── factor.js ├── native └── index.js ├── package.json ├── projected ├── frag.glsl ├── gl-line-3d.js ├── index.js └── vert.glsl └── triangles └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | **/bundle.js 6 | .bundle.js 7 | bundle.min.js 8 | common.min.js 9 | index.html 10 | !base/index.html -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webgl-lines 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | Some interactive content for [a blog post](http://mattdesl.svbtle.com/drawing-lines-is-hard). 6 | 7 | Demos: 8 | 9 | - [native](http://mattdesl.github.io/webgl-lines/native/) - rendering with `gl.LINES` 10 | - [triangles](http://mattdesl.github.io/webgl-lines/triangles/) - triangulated stroke 11 | - [expanded](http://mattdesl.github.io/webgl-lines/expanded/) - expanded in a vertex shader 12 | - [projected](http://mattdesl.github.io/webgl-lines/projected/) - screen space projected lines 13 | 14 | ## running demos 15 | 16 | First you need to git clone and install dependencies: 17 | 18 | ```sh 19 | git clone https://github.com/mattdesl/webgl-lines.git 20 | cd webgl-lines 21 | npm install 22 | ``` 23 | 24 | To start developing a demo, use one of the following: 25 | 26 | ``` 27 | npm run native 28 | npm run triangles 29 | npm run expanded 30 | npm run projected 31 | ``` 32 | 33 | And open `localhost:9966/[demo]`, for example `localhost:9966/native`. 34 | 35 | ## production 36 | 37 | For the bundle splitting and uglify step, use `npm run build-all`. 38 | 39 | ## License 40 | 41 | MIT, see [LICENSE.md](http://github.com/mattdesl/webgl-lines/blob/master/LICENSE.md) for details. 42 | -------------------------------------------------------------------------------- /base/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /base/index.js: -------------------------------------------------------------------------------- 1 | const ContextWebGL = require('webgl-context') 2 | const Context2D = require('2d-context') 3 | const fit = require('canvas-fit') 4 | const loop = require('raf-loop') 5 | const touches = require('touches') 6 | const minstache = require('minstache') 7 | const domify = require('domify') 8 | const xtend = require('xtend') 9 | const marked = require('marked') 10 | const classes = require('dom-classes') 11 | const template = require('fs').readFileSync(`${__dirname}/template.hbs`, 'utf8') 12 | 13 | const DPR = window.devicePixelRatio 14 | 15 | module.exports = function(render, opt) { 16 | opt = opt || {} 17 | 18 | let isWebGL = opt.context === 'webgl' 19 | let context = (isWebGL ? ContextWebGL : Context2D)(opt) 20 | if (!context) 21 | return fallback(opt) 22 | 23 | let resize = () => { 24 | fit(context.canvas, window, DPR) 25 | renderRetina(0) 26 | } 27 | 28 | context.canvas.oncontextmenu = function() { 29 | return false 30 | } 31 | 32 | window.addEventListener('resize', resize, false) 33 | process.nextTick(resize) 34 | 35 | let engine = loop(renderRetina) 36 | 37 | touches(document, { filtered: true }) 38 | .on('start', (ev) => { 39 | ev.preventDefault() 40 | engine.start() 41 | }) 42 | .on('end', () => { engine.stop() }) 43 | 44 | require('domready')(() => { 45 | document.body.appendChild(context.canvas) 46 | info(opt) 47 | }) 48 | 49 | function renderRetina(dt) { 50 | if (!isWebGL) { //scale the context... 51 | let { width, height } = context.canvas 52 | context.clearRect(0, 0, width, height) 53 | context.save() 54 | context.scale(DPR, DPR) 55 | } else { //draw WebGL buffer 56 | let gl = context 57 | let width = gl.drawingBufferWidth 58 | let height = gl.drawingBufferHeight 59 | gl.clearColor(0, 0, 0, 0) 60 | gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT) 61 | gl.viewport(0, 0, width, height) 62 | } 63 | render(dt) 64 | if (!isWebGL) 65 | context.restore() 66 | } 67 | 68 | return context 69 | } 70 | 71 | function fallback(opt) { 72 | info(xtend(opt, { error: true, description: 'sorry, this demo needs WebGL to run!' })) 73 | } 74 | 75 | function info(opt) { 76 | require('domready')(() => { 77 | opt = xtend({ name: '', description: '' }, opt) 78 | opt.name = opt.name.replace(/[\\\/]+/g, '') 79 | opt.description = marked(opt.description) 80 | let element = domify(minstache(template, opt)) 81 | if (opt.error) 82 | classes.add(element, 'error') 83 | document.body.appendChild(element) 84 | }) 85 | } -------------------------------------------------------------------------------- /base/line-utils.js: -------------------------------------------------------------------------------- 1 | //we need to duplicate vertex attributes to expand in the shader 2 | //and mirror the normals 3 | module.exports.duplicate = function duplicate(nestedArray, mirror) { 4 | var out = [] 5 | nestedArray.forEach(x => { 6 | let x1 = mirror ? -x : x 7 | out.push(x1, x) 8 | }) 9 | return out 10 | } 11 | 12 | //counter-clockwise indices but prepared for duplicate vertices 13 | module.exports.createIndices = function createIndices(length) { 14 | let indices = new Uint16Array(length * 6) 15 | let c = 0, index = 0 16 | for (let j=0; j 2 |
{{!description}}
3 | -------------------------------------------------------------------------------- /base/theme.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto Slab', 'Georgia', serif; 3 | -webkit-font-smoothing: antialiased; 4 | font-size: 12px; 5 | background: hsl(0, 0%, 95%); 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | div { 11 | padding: 0; 12 | margin: 0; 13 | line-height: 0px; 14 | } 15 | 16 | p { 17 | line-height: 1; 18 | } 19 | 20 | canvas { 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | } 25 | 26 | .content { 27 | pointer-events: none; 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | margin: 5px 15px; 32 | } 33 | 34 | .name { 35 | /*font-family: 'Roboto', 'Helvetica', sans-serif;*/ 36 | font-size: 24px; 37 | line-height: 5px; 38 | margin-top: 10px; 39 | } 40 | 41 | .description { 42 | margin-left: 0px; 43 | } 44 | 45 | em { 46 | color: rgb(90, 90, 90); 47 | /*color: rgb(205, 73, 73);*/ 48 | display: inline-block; 49 | margin-bottom: 22px; 50 | font-size: 14px; 51 | font-style: normal; 52 | } 53 | em::after { 54 | content: ' '; 55 | width: 40px; 56 | height: 1px; 57 | display: block; 58 | position: relative; 59 | top: 13px; 60 | left: 0; 61 | background: rgb(202, 202, 202); 62 | } 63 | 64 | .error > .description { 65 | color: rgb(205, 58, 58); 66 | } 67 | 68 | code { 69 | background: white; 70 | padding: 3px 5px; 71 | border-bottom: 1px solid rgb(202, 202, 202); 72 | } -------------------------------------------------------------------------------- /compress.sh: -------------------------------------------------------------------------------- 1 | # 1. Copy production index.html 2 | # 2. Uglify the bundle 3 | rm -rf **/bundle.min.js 4 | for var in "$@" 5 | do 6 | cp base/index.html $var/index.html && \ 7 | uglifyjs $var/bundle.js > $var/bundle.min.js 8 | done 9 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | # 1. copy dev HTML template into folder 2 | # 2. run watchify server with live reload 3 | rm -rf $1/bundle.js $1/bundle.min.js \ 4 | && cp base/dev.html $1/index.html \ 5 | && ./node_modules/.bin/budo $1/index.js -o $1/bundle.js -v -d --live | garnish -v 6 | -------------------------------------------------------------------------------- /expanded/frag.glsl: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | #endif 4 | 5 | uniform vec3 color; 6 | uniform float inner; 7 | varying float edge; 8 | 9 | const vec3 color2 = vec3(0.8); 10 | 11 | void main() { 12 | float v = 1.0 - abs(edge); 13 | v = smoothstep(0.65, 0.7, v*inner); 14 | gl_FragColor = mix(vec4(color, 1.0), vec4(0.0), v); 15 | } 16 | -------------------------------------------------------------------------------- /expanded/gl-line-2d.js: -------------------------------------------------------------------------------- 1 | const getNormals = require('polyline-normals') 2 | const createBuffer = require('gl-buffer') 3 | const createVAO = require('gl-vao') 4 | // const createElements = require('quad-indices') 5 | const pack = require('array-pack-2d') 6 | const identity = require('gl-mat4/identity') 7 | const lineUtils = require('../base/line-utils') 8 | 9 | const glslify = require('glslify') 10 | const createShader = glslify({ 11 | vertex: './vert.glsl', 12 | fragment: './frag.glsl' 13 | }) 14 | 15 | module.exports = function(gl, opt) { 16 | let shader = createShader(gl) 17 | shader.bind() 18 | shader.attributes.position.location = 0 19 | shader.attributes.normal.location = 1 20 | shader.attributes.miter.location = 2 21 | 22 | let indexBuffer = emptyBuffer(Uint16Array, gl.ELEMENT_ARRAY_BUFFER) 23 | let positionBuffer = emptyBuffer() 24 | let normalBuffer = emptyBuffer() 25 | let miterBuffer = emptyBuffer() 26 | let count = 0 27 | let vao = createVAO(gl) 28 | 29 | //in real-world you wouldn't want to create so 30 | //many typed arrays per frame 31 | function update(path, closed) { 32 | let tags = getNormals(path, closed) 33 | 34 | //and update our VAO 35 | if (closed) { 36 | path = path.slice() 37 | path.push(path[0]) 38 | tags.push(tags[0]) 39 | } 40 | 41 | let normals = tags.map(x => x[0]) 42 | let miters = tags.map(x => x[1]) 43 | count = (path.length-1) * 6 44 | 45 | //our vertex attributes (positions, normals) need to be duplicated 46 | //the only difference is that one has a negative miter length 47 | normals = lineUtils.duplicate(normals) 48 | miters = lineUtils.duplicate(miters, true) 49 | let positions = lineUtils.duplicate(path) 50 | let indexUint16 = lineUtils.createIndices(path.length) 51 | 52 | //now update the buffers with float/short data 53 | positionBuffer.update(pack(positions)) 54 | normalBuffer.update(pack(normals)) 55 | miterBuffer.update(pack(miters)) 56 | indexBuffer.update(indexUint16) 57 | 58 | vao.update([ 59 | { buffer: positionBuffer, size: 2 }, 60 | { buffer: normalBuffer, size: 2 }, 61 | { buffer: miterBuffer, size: 1 } 62 | ], indexBuffer) 63 | } 64 | 65 | //default uniforms 66 | let model = identity([]) 67 | let projection = identity([]) 68 | let view = identity([]) 69 | let thickness = 1 70 | let inner = 0 71 | let color = [1,1,1] 72 | 73 | return { 74 | update, 75 | model, 76 | view, 77 | projection, 78 | thickness, 79 | color, 80 | inner, 81 | 82 | draw() { 83 | shader.bind() 84 | shader.uniforms.model = this.model 85 | shader.uniforms.view = this.view 86 | shader.uniforms.projection = this.projection 87 | shader.uniforms.color = this.color 88 | shader.uniforms.thickness = this.thickness 89 | shader.uniforms.inner = this.inner 90 | 91 | vao.bind() 92 | vao.draw(gl.TRIANGLES, count) 93 | vao.unbind() 94 | } 95 | } 96 | 97 | function emptyBuffer(arrayType, type) { 98 | arrayType = arrayType || Float32Array 99 | return createBuffer(gl, new arrayType(), type || gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW) 100 | } 101 | } -------------------------------------------------------------------------------- /expanded/index.js: -------------------------------------------------------------------------------- 1 | const createLine = require('./gl-line-2d') 2 | const curve = require('adaptive-bezier-curve') 3 | const mat4 = require('gl-mat4') 4 | 5 | let gl = require('../base')(render, { 6 | name: __dirname, 7 | context: 'webgl', 8 | description: ` 9 | _touch to animate_ 10 | 2D lines expanded in a vertex shader 11 | ` 12 | }) 13 | 14 | let time = 0 15 | let projection = mat4.create() 16 | let identity = mat4.create() 17 | let rotation = mat4.create() 18 | let view = mat4.create() 19 | 20 | mat4.translate(view, view, [0.5, 0.0, -3]) 21 | mat4.scale(rotation, rotation, [0.5, 0.5, 0.5]) 22 | mat4.rotateY(rotation, rotation, -0.5) 23 | let line = createLine(gl) 24 | 25 | function render(dt) { 26 | time += dt/1000 27 | let width = gl.drawingBufferWidth 28 | let height = gl.drawingBufferHeight 29 | 30 | gl.disable(gl.DEPTH_TEST) 31 | gl.disable(gl.CULL_FACE) 32 | gl.enable(gl.BLEND) 33 | 34 | line.color = [0.2, 0.2, 0.2] 35 | gl.clear(gl.DEPTH_BUFFER_BIT) 36 | mat4.rotateY(rotation, rotation, 0.01) 37 | drawBox(width, height) 38 | drawCurve(width, height) 39 | } 40 | 41 | //draw a thick-lined rectangle in 3D space 42 | function drawBox(width, height) { 43 | mat4.perspective(projection, Math.PI/4, width/height, 0, 100) 44 | 45 | let path = [ 46 | [-1, -1], [1, -1], 47 | [1, 1], [-1, 1] 48 | ] 49 | 50 | line.update(path, true) 51 | line.model = rotation 52 | line.view = view 53 | line.projection = projection 54 | line.thickness = 0.2 55 | line.inner = 0 56 | line.draw() 57 | } 58 | 59 | //draw a bezier curve in 2D orthographic space, 60 | function drawCurve(width, height) { 61 | //top-left ortho projection 62 | mat4.ortho(projection, 0, width, height, 0, 0, 1) 63 | line.projection = projection 64 | //reset others to identity 65 | line.model = identity 66 | line.view = identity 67 | 68 | //get a bezier curve 69 | let x = width/4, y = height/2 70 | let off = 200 71 | let len = 100 72 | let path = curve( 73 | [x-len, y], 74 | [x, y+off], 75 | [x+len/2, y-off], 76 | [x+len, y] 77 | ) 78 | 79 | //also add in sharp edges to demonstrate miter joins 80 | path.push([x+len+50, y+25]) 81 | path.unshift([x-len-50, y+25]) 82 | 83 | line.update(path) 84 | line.thickness = (Math.sin(time)/2+0.5) * 30 + 10 85 | line.inner = Math.sin(time)/2+0.5 86 | line.draw() 87 | } -------------------------------------------------------------------------------- /expanded/vert.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 position; 2 | attribute vec2 normal; 3 | attribute float miter; 4 | uniform mat4 projection; 5 | uniform mat4 model; 6 | uniform mat4 view; 7 | uniform float thickness; 8 | varying float edge; 9 | 10 | void main() { 11 | edge = sign(miter); 12 | vec2 pointPos = position.xy + vec2(normal * thickness/2.0 * miter); 13 | gl_Position = projection * view * model * vec4(pointPos, 0.0, 1.0); 14 | gl_PointSize = 1.0; 15 | } 16 | -------------------------------------------------------------------------------- /factor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var path = require('path') 3 | var browserify = require('browserify') 4 | var fs = require('fs') 5 | var uglify = require('uglify-stream') 6 | 7 | var folders = ['native', 'triangles', 'expanded', 'projected'] 8 | var entries = folders.map(function(f) { 9 | return ['./', f, '/index.js'].join('') 10 | }) 11 | 12 | var bundles = folders.map(function(f) { 13 | var file = path.join('./', f, '/bundle.js') 14 | return fs.createWriteStream(file) 15 | }) 16 | 17 | var b = browserify(entries) 18 | b.plugin('factor-bundle', { outputs: bundles }) 19 | b.bundle() 20 | .pipe(minify()) 21 | .pipe(fs.createWriteStream('./build/common.min.js')) 22 | 23 | function minify() { 24 | return uglify({ compress: false, mangle: false }) 25 | } -------------------------------------------------------------------------------- /native/index.js: -------------------------------------------------------------------------------- 1 | const Mesh = require('bunny') 2 | const Geometry = require('gl-geometry') 3 | const mat4 = require('gl-mat4') 4 | const wireframe = require('gl-wireframe') 5 | const premult = require('premultiplied-rgba') 6 | 7 | 8 | const rescale = require('rescale-vertices') 9 | const boundingBox = require('vertices-bounding-box') 10 | const quantize = require('quantize-vertices') 11 | 12 | //some per-mesh properties 13 | const lines = [20, 4, 0.5] 14 | const levels = [1, 3, 4] 15 | const colors = [ 16 | [0.3, 0.3, 0.3, 1] 17 | ].map(premult) 18 | 19 | let description = ` 20 | _touch to rotate_ 21 | rendering with \`gl.LINES\` 22 | ` 23 | 24 | //create GL context 25 | let gl = require('../base')(render, { 26 | context: 'webgl', 27 | name: __dirname, 28 | description 29 | }) 30 | 31 | let projection = mat4.create() 32 | let model = mat4.create() 33 | let view = mat4.create() 34 | 35 | mat4.translate(view, view, [1, -5, -20]) 36 | mat4.rotateY(model, model, 0.6) 37 | 38 | let shader = require('gl-basic-shader')(gl) 39 | 40 | //render mesh with edges 41 | Mesh.cells = wireframe(Mesh.cells) 42 | 43 | //create meshes 44 | let meshes = levels.map(x => { 45 | //quantize the mesh & use edges instead of triangles 46 | let { positions, cells } = Mesh 47 | let bb = boundingBox(Mesh.positions) 48 | positions = quantize(Mesh.positions, x) 49 | positions = rescale(positions, bb) 50 | 51 | //create WebGL buffers 52 | let geometry = Geometry(gl) 53 | geometry.attr('position', positions) 54 | geometry.faces(cells) 55 | 56 | return { positions, cells, geometry } 57 | }) 58 | 59 | function render(dt) { 60 | let width = gl.drawingBufferWidth 61 | let height = gl.drawingBufferHeight 62 | 63 | gl.enable(gl.BLEND) 64 | gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) 65 | 66 | mat4.perspective(projection, Math.PI/4, width / height, 0.01, 1000) 67 | mat4.rotateY(model, model, 0.8 * dt/1000) 68 | 69 | gl.enable(gl.DEPTH_TEST) 70 | gl.enable(gl.CULL_FACE) 71 | shader.bind() 72 | shader.uniforms.projection = projection 73 | shader.uniforms.view = view 74 | shader.uniforms.model = model 75 | 76 | //draw meshes 77 | meshes.forEach((mesh, index, list) => { 78 | gl.lineWidth(lines[index % lines.length]) 79 | shader.uniforms.tint = colors[index % colors.length] 80 | 81 | let geometry = mesh.geometry 82 | geometry.bind(shader) 83 | geometry.draw(gl.LINES) 84 | geometry.unbind() 85 | }) 86 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-lines", 3 | "version": "1.0.0", 4 | "description": "some interactive content for a blog post", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "private": true, 8 | "author": { 9 | "name": "Matt DesLauriers", 10 | "email": "dave.des@gmail.com", 11 | "url": "https://github.com/mattdesl" 12 | }, 13 | "dependencies": { 14 | "2d-context": "^1.2.0", 15 | "adaptive-bezier-curve": "^1.0.3", 16 | "arc-to": "^1.0.1", 17 | "array-pack-2d": "^0.1.0", 18 | "bunny": "^1.0.1", 19 | "canvas-fit": "^1.2.0", 20 | "clamp": "^1.0.1", 21 | "color-style": "^1.0.0", 22 | "dom-classes": "0.0.1", 23 | "domify": "^1.3.1", 24 | "domready": "^1.0.7", 25 | "dtype": "^0.1.0", 26 | "extrude-polyline": "^1.0.6", 27 | "garnish": "^2.1.0", 28 | "gl-basic-shader": "^1.2.2", 29 | "gl-buffer": "^2.1.1", 30 | "gl-geometry": "^1.0.3", 31 | "gl-mat4": "^1.1.2", 32 | "gl-shader-core": "^2.2.0", 33 | "gl-vao": "^1.2.0", 34 | "gl-vec3": "^1.0.3", 35 | "gl-wireframe": "^1.0.1", 36 | "glslify": "1.6.1", 37 | "lodash.throttle": "^3.0.1", 38 | "marked": "^0.3.3", 39 | "minstache": "^1.2.0", 40 | "polyline-normals": "^2.0.2", 41 | "premultiplied-rgba": "^1.0.1", 42 | "quantize-vertices": "^1.0.2", 43 | "raf-loop": "^1.0.1", 44 | "randf": "^1.0.0", 45 | "rescale-vertices": "^1.0.0", 46 | "teapot": "^1.0.0", 47 | "touches": "^1.0.3", 48 | "vectors": "^0.1.0", 49 | "vertices-bounding-box": "^1.0.0", 50 | "webgl-context": "^2.1.1", 51 | "xtend": "^4.0.0" 52 | }, 53 | "devDependencies": { 54 | "babelify": "^5.0.3", 55 | "brfs": "^1.4.0", 56 | "browserify": "^9.0.3", 57 | "budo": "^1.2.0", 58 | "factor-bundle": "^2.3.3", 59 | "open-url": "^2.0.2", 60 | "uglify-js": "^2.4.16", 61 | "uglify-stream": "^1.1.0", 62 | "watchify": "git://github.com/mattdesl/watchify#fast" 63 | }, 64 | "scripts": { 65 | "native": "./dev.sh native", 66 | "triangles": "./dev.sh triangles", 67 | "expanded": "./dev.sh expanded", 68 | "projected": "./dev.sh projected", 69 | "build-all": "mkdir -p build && ./factor.js && ./compress.sh native triangles expanded projected" 70 | }, 71 | "keywords": [], 72 | "repository": { 73 | "type": "git", 74 | "url": "git://github.com/mattdesl/webgl-lines.git" 75 | }, 76 | "homepage": "https://github.com/mattdesl/webgl-lines", 77 | "bugs": { 78 | "url": "https://github.com/mattdesl/webgl-lines/issues" 79 | }, 80 | "browserify": { 81 | "transform": [ 82 | "babelify", 83 | "brfs", 84 | "glslify" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /projected/frag.glsl: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | #endif 4 | 5 | uniform vec3 color; 6 | 7 | void main() { 8 | gl_FragColor = vec4(color, 1.0); 9 | } 10 | -------------------------------------------------------------------------------- /projected/gl-line-3d.js: -------------------------------------------------------------------------------- 1 | const getNormals = require('polyline-normals') 2 | const createBuffer = require('gl-buffer') 3 | const createVAO = require('gl-vao') 4 | // const createElements = require('quad-indices') 5 | const pack = require('array-pack-2d') 6 | const identity = require('gl-mat4/identity') 7 | const { duplicate, createIndices } = require('../base/line-utils') 8 | const clamp = require('clamp') 9 | 10 | const glslify = require('glslify') 11 | const createShader = glslify({ 12 | vertex: './vert.glsl', 13 | fragment: './frag.glsl' 14 | }) 15 | 16 | module.exports = function(gl, opt) { 17 | let shader = createShader(gl) 18 | shader.bind() 19 | shader.attributes.position.location = 0 20 | shader.attributes.direction.location = 1 21 | shader.attributes.next.location = 2 22 | shader.attributes.previous.location = 3 23 | 24 | //each vertex has the following attribs: 25 | // vec3 position //current point on line 26 | // vec3 previous //previous point on line 27 | // vec3 next //next point on line 28 | // float direction //a sign, -1 or 1 29 | 30 | //we submit two vertices per point so that 31 | //we can expand them away from each other 32 | let indexBuffer = emptyBuffer(Uint16Array, gl.ELEMENT_ARRAY_BUFFER) 33 | let positionBuffer = emptyBuffer() 34 | let previousBuffer = emptyBuffer() 35 | let nextBuffer = emptyBuffer() 36 | let directionBuffer = emptyBuffer() 37 | let count = 0 38 | let vao = createVAO(gl) 39 | 40 | //in real-world you wouldn't want to create so 41 | //many typed arrays per frame 42 | function update(path) { 43 | //ensure 3 component vectors 44 | if (path.length > 0 && path[0].length !== 3) { 45 | path = path.map(point => { 46 | let [x, y, z] = point 47 | return [x || 0, y || 0, z || 0] 48 | }) 49 | } 50 | 51 | count = (path.length-1) * 6 52 | 53 | //each pair has a mirrored direction 54 | let direction = duplicate(path.map(x => 1), true) 55 | //now get the positional data for each vertex 56 | let positions = duplicate(path) 57 | let previous = duplicate(path.map(relative(-1))) 58 | let next = duplicate(path.map(relative(+1))) 59 | let indexUint16 = createIndices(path.length) 60 | 61 | //now update the buffers with float/short data 62 | positionBuffer.update(pack(positions)) 63 | previousBuffer.update(pack(previous)) 64 | nextBuffer.update(pack(next)) 65 | directionBuffer.update(pack(direction)) 66 | indexBuffer.update(indexUint16) 67 | 68 | vao.update([ 69 | { buffer: positionBuffer, size: 3 }, 70 | { buffer: directionBuffer, size: 1 }, 71 | { buffer: nextBuffer, size: 3 }, 72 | { buffer: previousBuffer, size: 3 } 73 | ], indexBuffer) 74 | } 75 | 76 | //default uniforms 77 | let model = identity([]) 78 | let projection = identity([]) 79 | let view = identity([]) 80 | let thickness = 1 81 | let aspect = 1 82 | let miter = 0 83 | let color = [1,1,1] 84 | 85 | return { 86 | update, 87 | model, 88 | view, 89 | projection, 90 | thickness, 91 | color, 92 | miter, 93 | aspect, 94 | 95 | draw() { 96 | shader.bind() 97 | shader.uniforms.model = this.model 98 | shader.uniforms.view = this.view 99 | shader.uniforms.projection = this.projection 100 | shader.uniforms.color = this.color 101 | shader.uniforms.thickness = this.thickness 102 | shader.uniforms.aspect = this.aspect 103 | shader.uniforms.miter = this.miter 104 | 105 | vao.bind() 106 | vao.draw(gl.TRIANGLES, count) 107 | vao.unbind() 108 | } 109 | } 110 | 111 | function emptyBuffer(arrayType, type) { 112 | arrayType = arrayType || Float32Array 113 | return createBuffer(gl, new arrayType(), type || gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW) 114 | } 115 | } 116 | 117 | function relative(offset) { 118 | return (point, index, list) => { 119 | index = clamp(index + offset, 0, list.length-1) 120 | return list[index] 121 | } 122 | } -------------------------------------------------------------------------------- /projected/index.js: -------------------------------------------------------------------------------- 1 | const createLine = require('./gl-line-3d') 2 | const curve = require('adaptive-bezier-curve') 3 | const mat4 = require('gl-mat4') 4 | const transformMat4 = require('gl-vec3/transformMat4') 5 | const arc = require('arc-to') 6 | 7 | let description = `_touch to animate paths_ 8 | 3D lines expanded in screen-space 9 | miter join computed in vertex shader` 10 | 11 | let gl = require('../base')(render, { 12 | name: __dirname, 13 | context: 'webgl', 14 | description: description 15 | }) 16 | 17 | let time = 0 18 | let projection = mat4.create() 19 | let identity = mat4.create() 20 | let rotation = mat4.create() 21 | let left = mat4.create() 22 | let leftRotation = mat4.create() 23 | let view = mat4.create() 24 | 25 | mat4.translate(view, view, [0.0, 0.0, -3]) 26 | mat4.translate(left, left, [-0.25, 0.25, 0.0]) 27 | mat4.scale(left, left, [0.5, 0.5, 0.5]) 28 | mat4.scale(rotation, rotation, [0.75, 0.75, 0.75]) 29 | 30 | let line = createLine(gl) 31 | 32 | let spin = mat4.create() 33 | 34 | 35 | function render(dt) { 36 | time += dt/1000 37 | let width = gl.drawingBufferWidth 38 | let height = gl.drawingBufferHeight 39 | 40 | gl.enable(gl.DEPTH_TEST) 41 | gl.disable(gl.CULL_FACE) 42 | 43 | mat4.perspective(projection, Math.PI/4, width/height, 0, 1000) 44 | line.aspect = width/height 45 | 46 | drawMitered() 47 | drawSpinning() 48 | } 49 | 50 | function drawSpinning() { 51 | mat4.rotateY(rotation, rotation, 0.01) 52 | mat4.identity(spin) 53 | 54 | //first create a circle with a small radius 55 | let path = arc(0, 0, 1, 0, Math.PI*1.5, false, 256) 56 | path = path.map((point, i) => { 57 | let [x, y, z] = point 58 | let v3 = [x||0, y||0, z||0] 59 | mat4.rotateY(spin, spin, Math.sin(x/10 * Math.sin(time/1))) 60 | transformMat4(v3, v3, spin) 61 | return v3 62 | }) 63 | 64 | line.color = [0.2, 0.2, 0.2] 65 | line.projection = projection 66 | line.model = rotation 67 | line.view = view 68 | line.update(path) 69 | line.thickness = 0.21 70 | line.miter = 0 71 | line.draw() 72 | } 73 | 74 | function drawMitered() { 75 | mat4.identity(leftRotation) 76 | mat4.multiply(leftRotation, leftRotation, left) 77 | mat4.rotateY(leftRotation, leftRotation, Math.sin(time)*0.8) 78 | 79 | let path = [ 80 | [0, -1], [1, -1], 81 | [0, 0], [1, 0], 82 | [0.25, -0.75] 83 | ] 84 | line.projection = projection 85 | 86 | line.model = leftRotation 87 | line.color = [0.8, 0.8, 0.8] 88 | line.view = view 89 | line.update(path) 90 | line.thickness = 0.1 91 | line.miter = 1 92 | line.draw() 93 | } 94 | -------------------------------------------------------------------------------- /projected/vert.glsl: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | attribute float direction; 3 | attribute vec3 next; 4 | attribute vec3 previous; 5 | uniform mat4 projection; 6 | uniform mat4 model; 7 | uniform mat4 view; 8 | uniform float aspect; 9 | 10 | uniform float thickness; 11 | uniform int miter; 12 | 13 | void main() { 14 | vec2 aspectVec = vec2(aspect, 1.0); 15 | mat4 projViewModel = projection * view * model; 16 | vec4 previousProjected = projViewModel * vec4(previous, 1.0); 17 | vec4 currentProjected = projViewModel * vec4(position, 1.0); 18 | vec4 nextProjected = projViewModel * vec4(next, 1.0); 19 | 20 | //get 2D screen space with W divide and aspect correction 21 | vec2 currentScreen = currentProjected.xy / currentProjected.w * aspectVec; 22 | vec2 previousScreen = previousProjected.xy / previousProjected.w * aspectVec; 23 | vec2 nextScreen = nextProjected.xy / nextProjected.w * aspectVec; 24 | 25 | float len = thickness; 26 | float orientation = direction; 27 | 28 | //starting point uses (next - current) 29 | vec2 dir = vec2(0.0); 30 | if (currentScreen == previousScreen) { 31 | dir = normalize(nextScreen - currentScreen); 32 | } 33 | //ending point uses (current - previous) 34 | else if (currentScreen == nextScreen) { 35 | dir = normalize(currentScreen - previousScreen); 36 | } 37 | //somewhere in middle, needs a join 38 | else { 39 | //get directions from (C - B) and (B - A) 40 | vec2 dirA = normalize((currentScreen - previousScreen)); 41 | if (miter == 1) { 42 | vec2 dirB = normalize((nextScreen - currentScreen)); 43 | //now compute the miter join normal and length 44 | vec2 tangent = normalize(dirA + dirB); 45 | vec2 perp = vec2(-dirA.y, dirA.x); 46 | vec2 miter = vec2(-tangent.y, tangent.x); 47 | dir = tangent; 48 | len = thickness / dot(miter, perp); 49 | } else { 50 | dir = dirA; 51 | } 52 | } 53 | vec2 normal = vec2(-dir.y, dir.x); 54 | normal *= len/2.0; 55 | normal.x /= aspect; 56 | 57 | vec4 offset = vec4(normal * orientation, 0.0, 1.0); 58 | gl_Position = currentProjected + offset; 59 | gl_PointSize = 1.0; 60 | } 61 | -------------------------------------------------------------------------------- /triangles/index.js: -------------------------------------------------------------------------------- 1 | const MAX_POINTS = 8, MIN_DIST = 20 2 | const distance = require('vectors/dist')(2) 3 | const throttle = require('lodash.throttle') 4 | const random = require('randf') 5 | const curve = require('adaptive-bezier-curve') 6 | 7 | let stroke = require('extrude-polyline')({ 8 | thickness: 20, 9 | join: 'miter', 10 | miterLimit: 2, 11 | cap: 'square' 12 | }) 13 | 14 | let context = require('../base')(render, { 15 | name: __dirname, 16 | description: `_toch and drag to draw_ 17 | rendering with triangles ` 18 | }) 19 | 20 | let canvas = context.canvas 21 | let colors = [ 22 | '#4f4f4f', 23 | '#767676', 24 | '#d9662d', 25 | '#d98a2d' 26 | ] 27 | 28 | let path = getInitialPath() 29 | let lastPosition = path[path.length-1] 30 | 31 | function render(dt) { 32 | let { width, height } = canvas 33 | context.clearRect(0, 0, width, height) 34 | 35 | let mesh = stroke.build(path) 36 | mesh.cells.forEach((cell, i) => { 37 | let [ f0, f1, f2 ] = cell 38 | let v0 = mesh.positions[f0], 39 | v1 = mesh.positions[f1], 40 | v2 = mesh.positions[f2] 41 | context.beginPath() 42 | context.lineTo(v0[0], v0[1]) 43 | context.lineTo(v1[0], v1[1]) 44 | context.lineTo(v2[0], v2[1]) 45 | context.fillStyle = colors[i % colors.length] 46 | context.fill() 47 | }) 48 | } 49 | 50 | let adder = throttle(addPoint, 30) 51 | let dragging = false 52 | 53 | require('touches')(document, { filtered: true }) 54 | .on('move', adder) 55 | .on('start', () => { //clear path on click 56 | path.length = 0 57 | stroke.thickness = random(10, 30) 58 | dragging = true 59 | }) 60 | .on('end', () => { 61 | dragging = false 62 | }) 63 | 64 | function addPoint(ev, position) { 65 | if (!dragging) 66 | return 67 | //limit our path by distance and capacity 68 | if (distance(position, lastPosition) < MIN_DIST) 69 | return 70 | if (path.length > MAX_POINTS) 71 | path.shift() 72 | path.push(position) 73 | lastPosition = position 74 | } 75 | 76 | //gets a nice curved stroke to start off 77 | function getInitialPath() { 78 | let width2 = window.innerWidth/2 79 | let height2 = window.innerHeight/2 80 | let len = Math.min(100, window.innerWidth*0.25), off = 100 81 | return curve( 82 | [width2-len, height2], 83 | [width2-len/2, height2-off], 84 | [width2+len/2, height2+off], 85 | [width2+len, height2], 86 | 0.1 //scaling factor 87 | ).slice(0, MAX_POINTS) 88 | } --------------------------------------------------------------------------------