├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── index.html ├── index.js ├── package.json ├── src ├── audio.js ├── background.js ├── colors.js ├── engine.js ├── shader.js ├── shaders │ ├── point.frag │ └── point.vert └── video.js └── video └── nyc.mp4 /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules/ 3 | !src/node_modules 4 | *.log 5 | .DS_Store 6 | bundle.js 7 | img/ 8 | video/ 9 | audio/ 10 | !video/nyc.mp4 -------------------------------------------------------------------------------- /.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 | # ink 2 | 3 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 4 | 5 | Experiments with particles, video, and SoundCloud. 6 | 7 | ## Usage 8 | 9 | [![NPM](https://nodei.co/npm/ink.png)](https://www.npmjs.com/package/ink) 10 | 11 | ## License 12 | 13 | MIT, see [LICENSE.md](http://github.com/mattdesl/ink/blob/master/LICENSE.md) for details. 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sketch 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const gl = require('webgl-context')() 2 | const canvas = gl.canvas 3 | const dpr = Math.min(1.5, window.devicePixelRatio) 4 | const fit = require('canvas-fit')(canvas, window, dpr) 5 | const loop = require('raf-loop') 6 | const engine = require('./src/engine')(gl) 7 | const background = require('./src/background')(gl) 8 | 9 | require('domready')(() => { 10 | window.addEventListener('resize', fit, false) 11 | document.body.appendChild(canvas) 12 | loop(draw).start() 13 | }) 14 | 15 | function draw(dt) { 16 | const { drawingBufferWidth: width, drawingBufferHeight: height } = gl 17 | 18 | gl.viewport(0, 0, width, height) 19 | 20 | gl.disable(gl.DEPTH_TEST) 21 | gl.disable(gl.CULL_FACE) 22 | gl.disable(gl.BLEND) 23 | 24 | gl.clearColor(0, 0, 0, 1) 25 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 26 | 27 | background(dt) 28 | 29 | // gl.enable(gl.BLEND) 30 | // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 31 | engine(dt) 32 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ink", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "array-range": "^1.0.1", 14 | "canvas-fit": "^1.4.0", 15 | "dom-events": "^0.1.1", 16 | "domready": "^1.0.7", 17 | "gl-audio-analyser": "^1.0.0", 18 | "gl-basic-shader": "^1.3.0", 19 | "gl-buffer": "^2.1.1", 20 | "gl-geometry": "^1.0.3", 21 | "gl-mat4": "^1.1.2", 22 | "gl-shader": "^4.0.1", 23 | "gl-texture2d": "^2.0.8", 24 | "gl-vao": "^1.2.0", 25 | "gl-vec3": "^1.0.3", 26 | "gl-vignette-background": "^1.0.4", 27 | "glsl-aastep": "^1.0.1", 28 | "glsl-easings": "^1.0.0", 29 | "glsl-luma": "^1.0.1", 30 | "glsl-noise": "0.0.0", 31 | "glslify": "^2.1.2", 32 | "hex-rgb": "^1.0.0", 33 | "icosphere": "^1.0.0", 34 | "img": "^1.0.0", 35 | "raf-loop": "^1.0.1", 36 | "randf": "^1.0.0", 37 | "soundcloud-badge": "^1.0.0", 38 | "webgl-context": "^2.1.2" 39 | }, 40 | "devDependencies": { 41 | "babelify": "^6.0.2", 42 | "browserify": "^9.0.8", 43 | "budo": "^5.1.4", 44 | "garnish": "^3.2.1", 45 | "uglify-js": "^2.4.20", 46 | "watchify": "^3.1.1" 47 | }, 48 | "scripts": { 49 | "build": "browserify index.js -t babelify -t glslify | uglifyjs -cm > bundle.js", 50 | "start": "budo index.js:bundle.js --live -- -t babelify -t glslify | garnish" 51 | }, 52 | "keywords": [], 53 | "repository": { 54 | "type": "git", 55 | "url": "git://github.com/mattdesl/ink.git" 56 | }, 57 | "homepage": "https://github.com/mattdesl/ink", 58 | "bugs": { 59 | "url": "https://github.com/mattdesl/ink/issues" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/audio.js: -------------------------------------------------------------------------------- 1 | import badge from 'soundcloud-badge' 2 | import Analyser from 'gl-audio-analyser' 3 | 4 | module.exports = function(gl) { 5 | const audio = new Audio() 6 | const analyser = Analyser(gl, audio) 7 | 8 | badge({ 9 | client_id: 'b95f61a90da961736c03f659c03cb0cc', 10 | song: 'https://soundcloud.com/kartell/sg-lewis-no-less-kartell-remix-1', 11 | dark: false, 12 | getFonts: true 13 | }, function(err, src, json, div) { 14 | if (err) throw err 15 | 16 | //temp fix since CORS are not enabled on SoundCloud :( 17 | audio.crossOrigin = 'Anonymous' 18 | audio.src = src 19 | audio.loop = true 20 | audio.addEventListener('canplay', function() { 21 | console.log("Playing audio...") 22 | audio.play() 23 | }, false) 24 | }) 25 | 26 | return function waveform(shader) { 27 | shader.uniforms.iWaveform = analyser.bindWaveform(1) 28 | } 29 | } -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import create from 'gl-vignette-background' 2 | import { rgb } from './colors' 3 | 4 | module.exports = function(gl) { 5 | const bg = create(gl) 6 | 7 | //optional styling 8 | bg.style({ 9 | scale: [ 1, 1 ], 10 | aspect: 1, 11 | color1: rgb('#e4d3b6'), 12 | color2: rgb('#6f614a'), 13 | smoothing: [ -0.5, 1.0 ], 14 | noiseAlpha: 0.08, 15 | coloredNoise: true, 16 | offset: [ -0.05, -0.15 ] 17 | }) 18 | 19 | return function() { 20 | bg.style({ aspect: gl.drawingBufferWidth / gl.drawingBufferHeight }) 21 | bg.draw() 22 | } 23 | } -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | import hex from 'hex-rgb' 2 | 3 | export const rgb = (str) => hex(str).map(x => x/255) 4 | export const rgba = (str, a=1) => [...rgb(str), a] 5 | export default hex 6 | -------------------------------------------------------------------------------- /src/engine.js: -------------------------------------------------------------------------------- 1 | import createGeom from 'gl-geometry' 2 | import range from 'array-range' 3 | import rand from 'randf' 4 | import { rgb } from './colors' 5 | import { length, random as rvec3 } from 'gl-vec3' 6 | import icosphere from 'icosphere' 7 | 8 | import createTexture from 'gl-texture2d' 9 | import createShader from './shader' 10 | import createVideo from './video' 11 | import createAudio from './audio' 12 | import loadImage from 'img' 13 | 14 | const lineTint = rgb('#be360d') 15 | const pointTint = rgb('#000') 16 | const sphere = icosphere(3) 17 | 18 | //get N random 3D unit vectors 19 | const N = 30000 20 | const unit = () => rand(-1, 1) 21 | const unitVector = () => range(3).map(unit) 22 | const sphereVector = () => rvec3([]) 23 | 24 | const pointPositions = range(N).map(sphereVector) 25 | const pointOffsets = range(N).map(unitVector) 26 | const linePositions = sphere.positions 27 | const lineOffsets = pointOffsets 28 | //.map(x => x.map(y => y*2.5)) 29 | 30 | module.exports = function(gl) { 31 | const points = createGeom(gl) 32 | .attr('position', pointPositions) 33 | .attr('offset', pointOffsets) 34 | 35 | const lines = createGeom(gl) 36 | .attr('position', linePositions) 37 | .attr('offset', lineOffsets) 38 | // .faces(sphere.cells) 39 | 40 | const program = createShader(gl) 41 | const audio = createAudio(gl) 42 | 43 | let texture 44 | const updateVideo = createVideo(gl, (err, tex) => { 45 | texture = tex 46 | }) 47 | // loadImage('img/road.jpg', function(err, image) { 48 | // if (err) 49 | // console.error(err) 50 | // texture = createTexture(gl, image) 51 | // texture.filter = [ gl.LINEAR, gl.LINEAR ] 52 | // texture.wrap = [ gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE ] 53 | // }) 54 | 55 | return function(dt) { 56 | if (!texture) 57 | return 58 | 59 | 60 | // gl.lineWidth(3) 61 | 62 | program.bind(dt) 63 | audio(program.shader) 64 | texture.bind(0) 65 | texture.update() 66 | 67 | program.tint(lineTint) 68 | lines.bind(program.shader) 69 | lines.draw(gl.LINE_LOOP) 70 | lines.unbind() 71 | 72 | program.tint(pointTint) 73 | points.bind(program.shader) 74 | points.draw(gl.POINTS) 75 | points.unbind() 76 | } 77 | } -------------------------------------------------------------------------------- /src/shader.js: -------------------------------------------------------------------------------- 1 | import mat4 from 'gl-mat4' 2 | import createShader from 'gl-shader' 3 | 4 | const projection = mat4.create() 5 | const model = mat4.create() 6 | const view = mat4.create() 7 | 8 | // needed for AST 9 | const glslify = require('glslify') 10 | 11 | module.exports = function(gl) { 12 | const vert = glslify('./shaders/point.vert') 13 | const frag = glslify('./shaders/point.frag') 14 | 15 | var images = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) 16 | if (images <= 0) { 17 | alert("Sorry, this demo isn't ready for your device yet!") 18 | console.error("no vertex texture image units") 19 | } 20 | 21 | const shader = createShader(gl, vert, frag) 22 | 23 | let time = 0 24 | let rotation = Math.PI/2 25 | let resolution = [gl.drawingBufferWidth, gl.drawingBufferHeight] 26 | let translation = [0, 0, -5] 27 | 28 | function bind(dt) { 29 | time += dt / 1000 30 | 31 | const width = gl.drawingBufferWidth 32 | const height = gl.drawingBufferHeight 33 | const aspect = width / height 34 | 35 | mat4.perspective(projection, Math.PI/4, aspect, 1, 1000) 36 | 37 | mat4.identity(model) 38 | mat4.translate(model, model, translation) 39 | mat4.rotateY(model, model, Math.PI/2) 40 | mat4.rotateX(model, model, rotation) 41 | // mat4.rotateZ(model, model, rotation) 42 | rotation += dt * 0.000025 43 | 44 | shader.bind() 45 | shader.uniforms.iGlobalTime = time 46 | shader.uniforms.iResolution = resolution 47 | shader.uniforms.iChannel0 = 0 48 | shader.uniforms.projection = projection 49 | shader.uniforms.model = model 50 | shader.uniforms.view = view 51 | } 52 | 53 | return { 54 | shader: shader, 55 | bind: bind, 56 | tint: (rgb) => { shader.uniforms.tint = rgb } 57 | } 58 | } -------------------------------------------------------------------------------- /src/shaders/point.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec2 iResolution; 4 | uniform vec3 tint; 5 | uniform sampler2D iChannel0; 6 | varying vec2 uv; 7 | 8 | #pragma glslify: luma = require('glsl-luma') 9 | // #pragma glslify: audio = require('gl-audio-analyser') 10 | 11 | void main() { 12 | float dist = length(uv - 0.5); 13 | dist = smoothstep(0.4, 0.5, dist); 14 | 15 | vec3 color = texture2D(iChannel0, uv + vec2(0.0, 0.05)).rgb; 16 | 17 | // color += max(0., +amplitude) * vec3(0.5, 1.0, 0.2); 18 | // color += max(0., -amplitude) * vec3(0.9, 0.1, 0.4); 19 | 20 | 21 | if (tint == vec3(0.0)) { 22 | gl_FragColor.rgb = vec3(luma(color)); 23 | } 24 | else { 25 | gl_FragColor.rgb = tint; 26 | } 27 | gl_FragColor.a = 1.0; 28 | // gl_FragColor = vec4(1.0) * tint; 29 | } 30 | -------------------------------------------------------------------------------- /src/shaders/point.vert: -------------------------------------------------------------------------------- 1 | #pragma glslify: noise = require('glsl-noise/simplex/4d') 2 | #pragma glslify: audio = require('gl-audio-analyser') 3 | #pragma glslify: ease = require('glsl-easings/sine-in-out') 4 | 5 | #define POINT_SIZE 2.2 6 | 7 | attribute vec4 position; 8 | attribute vec3 offset; 9 | 10 | uniform sampler2D iWaveform; 11 | uniform mat4 projection; 12 | uniform mat4 view; 13 | uniform mat4 model; 14 | uniform float iGlobalTime; 15 | 16 | varying vec2 uv; 17 | 18 | void main() { 19 | vec3 vpos = position.xyz; 20 | vec3 mult = offset; 21 | 22 | 23 | float audioCoord = vpos.x; 24 | float amplitude = max(0.0, audio(iWaveform, audioCoord)); 25 | amplitude = smoothstep(0.0, 3.0, amplitude*3.0); 26 | amplitude = ease(amplitude); 27 | 28 | vec4 npos = vec4(vpos, sin(iGlobalTime * sin(2.85 * mult.x))); 29 | npos.x += amplitude*0.3; 30 | 31 | float fade = 1.0; 32 | 33 | mat4 projModelView = projection * view * model; 34 | vpos += noise(npos * 0.45) * mult; 35 | 36 | 37 | 38 | vec4 projected = projModelView * vec4(vpos, 1.0); 39 | float anim = sin(iGlobalTime*0.1)*0.5+0.5; 40 | float dist = length(position.zzz); 41 | // dist = smoothstep(, 0.0, dist); 42 | gl_Position = projected; 43 | 44 | vec2 screenPos = projected.xy / projected.w; 45 | vec2 sUv = screenPos.xy * 0.5 + 0.5; 46 | uv = sUv; 47 | uv.y = 1.0 - sUv.y; 48 | 49 | gl_PointSize = dist * POINT_SIZE + amplitude*0.05; 50 | // uv *= 1.5; 51 | } -------------------------------------------------------------------------------- /src/video.js: -------------------------------------------------------------------------------- 1 | import createTexture from 'gl-texture2d' 2 | import loadImage from 'img' 3 | import events from 'dom-events' 4 | 5 | module.exports = function(gl, cb) { 6 | const element = document.createElement('video') 7 | element.setAttribute('loop', true) 8 | element.setAttribute('muted', 'muted') 9 | addSource('video/mp4', 'video/nyc.mp4') 10 | 11 | events.on(element, 'canplay', ev => { 12 | const texture = createTexture(gl, element) 13 | texture.update = update.bind(null, texture) 14 | element.play() 15 | cb(null, texture) 16 | }) 17 | 18 | function addSource(type, path) { 19 | var source = document.createElement('source') 20 | source.src = path 21 | source.type = type 22 | return element.appendChild(source) 23 | } 24 | 25 | function update(texture) { 26 | texture.setPixels(element) 27 | } 28 | } -------------------------------------------------------------------------------- /video/nyc.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/ink/bb1c5e859594b1848e1fae80c0b7a8bd6e3191ad/video/nyc.mp4 --------------------------------------------------------------------------------