├── .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 | [](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 | }
--------------------------------------------------------------------------------