├── README.md ├── gfx.js ├── index.js ├── package.json ├── plumbing ├── clock.js ├── keyboard.js ├── synth.js └── visual.js ├── server.js ├── sfx.js └── visuals ├── columns.js ├── sph.js └── text.json /README.md: -------------------------------------------------------------------------------- 1 | oregon-synth 2 | ============ 3 | WebGL powered synth 4 | -------------------------------------------------------------------------------- /gfx.js: -------------------------------------------------------------------------------- 1 | const MOVIE_TIME = 1e4 2 | 3 | module.exports = function (args) { 4 | const regl = args.regl 5 | const visuals = [ 6 | // require('./visuals/columns')(args), 7 | require('./visuals/sph')(args) 8 | ] 9 | 10 | let activeVisual = visuals[0] 11 | let visualTime = MOVIE_TIME 12 | regl.frame(({tick}) => { 13 | if (--visualTime < 0) { 14 | visualTime = MOVIE_TIME 15 | activeVisual = visuals[(Math.random() * visuals.length) | 0] 16 | } 17 | activeVisual() 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const regl = require('regl')({ 2 | extensions: ['OES_texture_float', 'OES_element_index_uint'], 3 | optionalExtensions: 'OES_texture_float_linear', 4 | pixelRatio: 1 5 | }) 6 | const keyboard = require('./plumbing/keyboard')() 7 | const audioContext = new window.AudioContext() 8 | 9 | require('./gfx')({ 10 | regl, 11 | keyboard 12 | }) 13 | 14 | require('./sfx')({ 15 | regl, 16 | audioContext, 17 | keyboard 18 | }) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oregon-synth", 3 | "version": "1.0.0", 4 | "description": "oregon-synth ============ WebGL powered synth", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mikolalysenko/oregon-synth.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/mikolalysenko/oregon-synth/issues" 17 | }, 18 | "homepage": "https://github.com/mikolalysenko/oregon-synth#readme", 19 | "dependencies": { 20 | "angle-normals": "^1.0.0", 21 | "column-mesh": "^1.0.1", 22 | "end-of-stream": "^1.1.0", 23 | "midi": "^0.9.5", 24 | "regl": "^1.0.0", 25 | "simplicial-complex": "^1.0.0", 26 | "sphere-mesh": "^0.2.2", 27 | "split2": "^2.1.0", 28 | "to2": "^1.0.0", 29 | "vectorize-text": "^3.0.2", 30 | "websocket-stream": "^3.2.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plumbing/clock.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | time: 0 3 | } 4 | -------------------------------------------------------------------------------- /plumbing/keyboard.js: -------------------------------------------------------------------------------- 1 | const wsock = require('websocket-stream') 2 | const to = require('to2') 3 | const split = require('split2') 4 | const onend = require('end-of-stream') 5 | const synthClock = require('./clock') 6 | 7 | const kmin = 24 8 | const kmax = 144 9 | const NUM_KEYS = kmax - kmin 10 | 11 | module.exports = function (clock) { 12 | const state = { 13 | time: 0, 14 | keys: Array(NUM_KEYS).fill(0), 15 | times: Array(NUM_KEYS * 2).fill(0), 16 | onChange: null 17 | } 18 | 19 | ;(function recon () { 20 | let stream = wsock('ws:///10.0.213.215:5000') 21 | onend(stream, recon) 22 | stream.pipe(split(JSON.parse)) 23 | .pipe(to.obj(write)) 24 | return recon 25 | 26 | function write (row, enc, next) { 27 | const keydown = (row.values[0] >> 4) & 1 28 | const k = row.values[1] - kmin 29 | state.time = synthClock.time 30 | state.keys[k] = keydown * row.values[2] / 128 31 | state.times[k * 2 + keydown] = state.time 32 | if (state.onChange) { 33 | state.onChange(state.keys) 34 | } 35 | next() 36 | } 37 | })() 38 | 39 | return state 40 | } 41 | -------------------------------------------------------------------------------- /plumbing/synth.js: -------------------------------------------------------------------------------- 1 | const clock = require('./clock') 2 | 3 | module.exports = function createSynth (options) { 4 | const regl = options.regl 5 | const context = options.audioContext 6 | const state = options.keyboard 7 | 8 | const NUM_KEYS = state.keys.length 9 | 10 | const shaderCode = options.shader 11 | const filterCode = options.filter 12 | const contextVars = options.context || {} 13 | 14 | const bufferSize = options.size || 1024 15 | const inputChannels = 0 16 | const outputChannels = 1 17 | const sampleRate = context.sampleRate 18 | 19 | clock.sampleRate = sampleRate 20 | 21 | const scriptNode = context.createScriptProcessor( 22 | bufferSize, 23 | inputChannels, 24 | outputChannels) 25 | 26 | const stateBuffer = Array(2).fill().map(() => 27 | regl.framebuffer({ 28 | color: regl.texture({ 29 | shape: [bufferSize, 1, 4], 30 | format: 'rgba', 31 | type: 'float', 32 | wrap: 'repeat', 33 | mag: regl.hasExtension('OES_texture_float_linear') 34 | ? 'linear' 35 | : 'nearest' 36 | }), 37 | depthStencil: false 38 | })) 39 | 40 | const outputBuffer = regl.framebuffer({ 41 | shape: [bufferSize, 1, 4], 42 | depthStencil: false 43 | }) 44 | 45 | const baseUniforms = { 46 | timeOffsetBase: regl.prop('timeOffsetBase'), 47 | timeOffsetScale: regl.prop('timeOffsetScale') 48 | } 49 | 50 | for (let i = 0; i < NUM_KEYS; ++i) { 51 | baseUniforms[`KEY_STATE[${i}]`] = (function (i) { 52 | var result = [0, 0, 0] 53 | return function () { 54 | result[0] = state.keys[i] 55 | result[1] = state.times[2 * i] 56 | result[2] = state.times[2 * i + 1] 57 | return result 58 | } 59 | })(i) 60 | } 61 | 62 | const generateSound = regl({ 63 | framebuffer: regl.prop('activeBuffer'), 64 | 65 | vert: ` 66 | precision highp float; 67 | attribute vec2 position; 68 | uniform float timeOffsetBase, timeOffsetScale; 69 | varying float offsetTime_; 70 | 71 | void main () { 72 | offsetTime_ = timeOffsetBase + timeOffsetScale * 0.5 * (position.x + 1.0); 73 | gl_Position = vec4(position, 0, 1); 74 | } 75 | `, 76 | 77 | frag: ` 78 | precision highp float; 79 | varying float offsetTime_; 80 | 81 | #define NUM_KEYS ${NUM_KEYS} 82 | uniform vec3 KEY_STATE[NUM_KEYS]; 83 | 84 | ${shaderCode} 85 | 86 | void main () { 87 | gl_FragColor = vec4(pcm(offsetTime_, KEY_STATE), 0, 0, 1); 88 | }`, 89 | 90 | context: contextVars, 91 | depth: { 92 | enable: false, 93 | mask: false 94 | }, 95 | attributes: { 96 | position: [ 97 | -4, 0, 98 | 4, 4, 99 | 4, -4 100 | ] 101 | }, 102 | uniforms: Object.assign(baseUniforms, options.uniforms || {}), 103 | count: 3, 104 | primitive: 'triangles', 105 | elements: null 106 | }) 107 | 108 | const filterSound = regl({ 109 | vert: ` 110 | precision highp float; 111 | attribute vec2 position; 112 | varying float shift; 113 | 114 | void main () { 115 | shift = 0.5 * (position.x + 1.0); 116 | gl_Position = vec4(position, 0, 1); 117 | } 118 | `, 119 | 120 | frag: ` 121 | precision highp float; 122 | varying float offsetTime_; 123 | 124 | #define NUM_KEYS ${NUM_KEYS} 125 | #define SAMPLE_COUNT ${bufferSize} 126 | uniform vec3 KEY_STATE[NUM_KEYS]; 127 | uniform sampler2D buffer[2]; 128 | varying float shift; 129 | 130 | float sample (float delay) { 131 | float t = shift - delay / float(SAMPLE_COUNT); 132 | return mix( 133 | texture2D(buffer[1], vec2(t, 0.0)).r, 134 | texture2D(buffer[0], vec2(t + 1.0, 0.0)).r, 135 | step(t, 0.0)); 136 | } 137 | 138 | ${filterCode} 139 | 140 | #define FLOAT_MAX 1.70141184e38 141 | #define FLOAT_MIN 1.17549435e-38 142 | 143 | lowp vec4 encode_float(highp float v) { 144 | highp float av = abs(v); 145 | 146 | //Handle special cases 147 | if(av < FLOAT_MIN) { 148 | return vec4(0.0, 0.0, 0.0, 0.0); 149 | } else if(v > FLOAT_MAX) { 150 | return vec4(127.0, 128.0, 0.0, 0.0) / 255.0; 151 | } else if(v < -FLOAT_MAX) { 152 | return vec4(255.0, 128.0, 0.0, 0.0) / 255.0; 153 | } 154 | 155 | highp vec4 c = vec4(0,0,0,0); 156 | 157 | //Compute exponent and mantissa 158 | highp float e = floor(log2(av)); 159 | highp float m = av * pow(2.0, -e) - 1.0; 160 | 161 | //Unpack mantissa 162 | c[1] = floor(128.0 * m); 163 | m -= c[1] / 128.0; 164 | c[2] = floor(32768.0 * m); 165 | m -= c[2] / 32768.0; 166 | c[3] = floor(8388608.0 * m); 167 | 168 | //Unpack exponent 169 | highp float ebias = e + 127.0; 170 | c[0] = floor(ebias / 2.0); 171 | ebias -= c[0] * 2.0; 172 | c[1] += floor(ebias) * 128.0; 173 | 174 | //Unpack sign bit 175 | c[0] += 128.0 * step(0.0, -v); 176 | 177 | //Scale back to range 178 | return c.abgr / 255.0; 179 | } 180 | 181 | void main () { 182 | gl_FragColor = encode_float(filter(KEY_STATE)); 183 | } 184 | `, 185 | 186 | context: contextVars, 187 | depth: { 188 | enable: false, 189 | mask: false 190 | }, 191 | attributes: { 192 | position: [ 193 | -4, 0, 194 | 4, 4, 195 | 4, -4 196 | ] 197 | }, 198 | uniforms: Object.assign( 199 | baseUniforms, 200 | options.uniforms || {}, { 201 | 'buffer[0]': regl.prop('prevBuffer'), 202 | 'buffer[1]': regl.prop('curBuffer') 203 | }), 204 | count: 3, 205 | primitive: 'triangles', 206 | elements: null 207 | }) 208 | 209 | const setFBO = regl({ 210 | framebuffer: outputBuffer 211 | }) 212 | 213 | let clockTime = 0 214 | let audioTick = 0 215 | scriptNode.onaudioprocess = function (event) { 216 | const outputBuffer = event.outputBuffer.getChannelData(0) 217 | 218 | generateSound({ 219 | timeOffsetBase: clockTime, 220 | timeOffsetScale: outputBuffer.length / sampleRate, 221 | activeBuffer: stateBuffer[audioTick % 2] 222 | }) 223 | 224 | setFBO(() => { 225 | filterSound({ 226 | prevBuffer: stateBuffer[(audioTick + 1) % 2], 227 | curBuffer: stateBuffer[audioTick % 2] 228 | }) 229 | regl.read({ 230 | data: new Uint8Array(outputBuffer.buffer) 231 | }) 232 | }) 233 | 234 | clockTime += outputBuffer.length / sampleRate 235 | audioTick = audioTick + 1 236 | 237 | clock.time = clockTime 238 | } 239 | 240 | return scriptNode 241 | } 242 | -------------------------------------------------------------------------------- /plumbing/visual.js: -------------------------------------------------------------------------------- 1 | let feedbackTextures 2 | 3 | module.exports = function ({ 4 | regl, 5 | keyboard, 6 | feedback, 7 | draw 8 | }) { 9 | const NUM_KEYS = keyboard.keys.length 10 | 11 | const commonUniforms = { 12 | 'time': ({tick}) => tick / 60.0 13 | } 14 | 15 | for (let i = 0; i < NUM_KEYS; ++i) { 16 | commonUniforms['keys[' + i + ']'] = (function (i) { 17 | var result = [0, 0, 0] 18 | return function () { 19 | result[0] = keyboard.keys[i] 20 | result[1] = keyboard.times[2 * i] 21 | result[2] = keyboard.times[2 * i + 1] 22 | return result 23 | } 24 | })(i) 25 | } 26 | 27 | const setupShaders = regl({ 28 | vert: ` 29 | precision highp float; 30 | 31 | attribute vec2 position; 32 | varying vec2 uv; 33 | 34 | void main () { 35 | uv = 0.5 * (position + 1.0); 36 | gl_Position = vec4(position, 0, 1); 37 | } 38 | `, 39 | attributes: { 40 | position: [ 41 | -4, 0, 42 | 4, 4, 43 | 4, -4 44 | ] 45 | }, 46 | uniforms: commonUniforms, 47 | context: { 48 | keyboard: keyboard 49 | }, 50 | count: 3 51 | }) 52 | 53 | if (!feedbackTextures) { 54 | feedbackTextures = Array(2).fill().map(() => 55 | regl.texture({ copy: true })) 56 | } 57 | 58 | let drawFeedback 59 | if (feedback) { 60 | drawFeedback = regl({ 61 | frag: ` 62 | precision highp float; 63 | 64 | #define NUM_KEYS ${NUM_KEYS} 65 | 66 | uniform sampler2D feedbackTexture[2]; 67 | uniform vec3 keys[NUM_KEYS]; 68 | uniform float time; 69 | uniform vec2 screenSize; 70 | varying vec2 uv; 71 | 72 | ${feedback} 73 | 74 | void main () { 75 | gl_FragColor = feedback(uv, time, keys, feedbackTexture); 76 | } 77 | `, 78 | 79 | uniforms: { 80 | 'feedbackTexture[0]': ({tick}) => feedbackTextures[tick % 2], 81 | 'feedbackTexture[1]': ({tick}) => feedbackTextures[(tick + 1) % 2], 82 | screenSize: ({viewportWidth, viewportHeight}) => [viewportWidth, viewportHeight] 83 | }, 84 | 85 | depth: { 86 | enable: false, 87 | mask: false 88 | }, 89 | 90 | blend: { 91 | enable: true, 92 | func: { 93 | srcRGB: 1, 94 | srcAlpha: 1, 95 | dst: 'one minus src alpha', 96 | dstAlpha: 1 97 | }, 98 | equation: 'add' 99 | } 100 | }) 101 | } 102 | 103 | return function () { 104 | setupShaders((context) => { 105 | if (draw) { 106 | draw(context) 107 | } 108 | if (feedback) { 109 | drawFeedback() 110 | feedbackTextures[context.tick % 2]({ 111 | copy: true 112 | }) 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var wsock = require('websocket-stream') 2 | var onend = require('end-of-stream') 3 | var http = require('http') 4 | 5 | var midi = require('midi') 6 | var input = new midi.input() 7 | 8 | var server = http.createServer(function (req, res) { 9 | res.statusCode = 404 10 | res.end('not found\n') 11 | }) 12 | server.listen(5000) 13 | 14 | var streams = [] 15 | wsock.createServer({ server: server }, function (stream) { 16 | streams.push(stream) 17 | onend(stream, function () { 18 | var ix = streams.indexOf(stream) 19 | streams.splice(ix, 1) 20 | }) 21 | }) 22 | 23 | input.on('message', function (dt, msg) { 24 | var row = { dt: dt, values: msg } 25 | for (var i = 0; i < streams.length; i++) { 26 | streams[i].write(JSON.stringify(row) + '\n') 27 | } 28 | console.log(row) 29 | }) 30 | input.openPort(1) 31 | -------------------------------------------------------------------------------- /sfx.js: -------------------------------------------------------------------------------- 1 | const createSynth = require('./plumbing/synth') 2 | 3 | module.exports = function ({regl, audioContext, keyboard}) { 4 | createSynth({ 5 | regl, 6 | audioContext, 7 | keyboard, 8 | shader: ` 9 | float pcm(float t, vec3 keys[NUM_KEYS]) { 10 | float result = 0.0; 11 | float k, x, m = 0.0; 12 | float tt = ${2.0 * Math.PI} * t; 13 | for (int i = 0; i < NUM_KEYS; ++i) { 14 | vec3 v = keys[i]; 15 | k = step(0.01, v.x); 16 | m += k; 17 | x = pow(2.0, float(i) / 12.0); 18 | result += k * ( 19 | sin(sin((t-v.y)*1.0*x/8.0+sin(t*4.0)*10.0)) 20 | ) * sin(tt*88.0*x+sin(t*2.0)/4.0) * 0.25; 21 | } 22 | return sqrt(abs(result)) * (result>0.0?1.0:-1.0); 23 | }`, 24 | filter: ` 25 | float filter(vec3 keys[NUM_KEYS]) { 26 | float result = sample(0.0); 27 | for (int i = 1; i < 40; i++) { 28 | result += sample(float(i)) * (exp(-0.25 * float(i * i)) / 4.0); 29 | } 30 | for (int i = 50; i < 80; ++i) { 31 | result += sample(float(i)) * 0.8 * exp(-0.1 * pow(float(i) - 65.0, 2.0)); 32 | } 33 | return result; 34 | } 35 | ` 36 | }).connect(audioContext.destination) 37 | } 38 | -------------------------------------------------------------------------------- /visuals/columns.js: -------------------------------------------------------------------------------- 1 | const column = require('column-mesh') 2 | const createVisual = require('../plumbing/visual') 3 | const mat4 = require('gl-mat4') 4 | const normals = require('angle-normals') 5 | 6 | const mesh = column({ 7 | radius: 2, 8 | height: 20 9 | }) 10 | 11 | module.exports = function ({regl, keyboard}) { 12 | const rmat = mat4.create() 13 | 14 | const drawColumn = regl({ 15 | frag: ` 16 | precision mediump float; 17 | varying vec3 vnormal, fposition; 18 | 19 | vec3 mod289(vec3 x) { 20 | return x - floor(x * (1.0 / 289.0)) * 289.0; 21 | } 22 | 23 | vec4 mod289(vec4 x) { 24 | return x - floor(x * (1.0 / 289.0)) * 289.0; 25 | } 26 | 27 | vec4 permute(vec4 x) { 28 | return mod289(((x*34.0)+1.0)*x); 29 | } 30 | 31 | vec4 taylorInvSqrt(vec4 r) 32 | { 33 | return 1.79284291400159 - 0.85373472095314 * r; 34 | } 35 | 36 | float snoise(vec3 v) 37 | { 38 | const vec2 C = vec2(1.0/6.0, 1.0/3.0) ; 39 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); 40 | 41 | // First corner 42 | vec3 i = floor(v + dot(v, C.yyy) ); 43 | vec3 x0 = v - i + dot(i, C.xxx) ; 44 | 45 | // Other corners 46 | vec3 g = step(x0.yzx, x0.xyz); 47 | vec3 l = 1.0 - g; 48 | vec3 i1 = min( g.xyz, l.zxy ); 49 | vec3 i2 = max( g.xyz, l.zxy ); 50 | 51 | // x0 = x0 - 0.0 + 0.0 * C.xxx; 52 | // x1 = x0 - i1 + 1.0 * C.xxx; 53 | // x2 = x0 - i2 + 2.0 * C.xxx; 54 | // x3 = x0 - 1.0 + 3.0 * C.xxx; 55 | vec3 x1 = x0 - i1 + C.xxx; 56 | vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y 57 | vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y 58 | 59 | // Permutations 60 | i = mod289(i); 61 | vec4 p = permute( permute( permute( 62 | i.z + vec4(0.0, i1.z, i2.z, 1.0 )) 63 | + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) 64 | + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); 65 | 66 | // Gradients: 7x7 points over a square, mapped onto an octahedron. 67 | // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) 68 | float n_ = 0.142857142857; // 1.0/7.0 69 | vec3 ns = n_ * D.wyz - D.xzx; 70 | 71 | vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) 72 | 73 | vec4 x_ = floor(j * ns.z); 74 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N) 75 | 76 | vec4 x = x_ *ns.x + ns.yyyy; 77 | vec4 y = y_ *ns.x + ns.yyyy; 78 | vec4 h = 1.0 - abs(x) - abs(y); 79 | 80 | vec4 b0 = vec4( x.xy, y.xy ); 81 | vec4 b1 = vec4( x.zw, y.zw ); 82 | 83 | //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; 84 | //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; 85 | vec4 s0 = floor(b0)*2.0 + 1.0; 86 | vec4 s1 = floor(b1)*2.0 + 1.0; 87 | vec4 sh = -step(h, vec4(0.0)); 88 | 89 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; 90 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; 91 | 92 | vec3 p0 = vec3(a0.xy,h.x); 93 | vec3 p1 = vec3(a0.zw,h.y); 94 | vec3 p2 = vec3(a1.xy,h.z); 95 | vec3 p3 = vec3(a1.zw,h.w); 96 | 97 | //Normalise gradients 98 | vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); 99 | p0 *= norm.x; 100 | p1 *= norm.y; 101 | p2 *= norm.z; 102 | p3 *= norm.w; 103 | 104 | // Mix final noise value 105 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); 106 | m = m * m; 107 | return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), 108 | dot(p2,x2), dot(p3,x3) ) ); 109 | } 110 | 111 | void main () { 112 | float intensity = 113 | (1.0 + sin( 114 | 3.0 * fposition.z + 115 | 0.5 * fposition.y + 116 | 3.0 * snoise(0.6 * fposition))) / 2.0; 117 | float light = max(dot(vnormal, vec3(sqrt(2.0), sqrt(2.0), 0) / 2.0), 0.0) + 0.1; 118 | intensity *= light; 119 | gl_FragColor = vec4(intensity, intensity, intensity, 1.0); 120 | } 121 | `, 122 | vert: ` 123 | precision mediump float; 124 | uniform mat4 projection, view, model; 125 | attribute vec3 position, normal; 126 | varying vec3 vnormal, fposition; 127 | void main () { 128 | vnormal = normal; 129 | fposition = position; 130 | gl_Position = projection * view * model * vec4(position, 1.0); 131 | } 132 | `, 133 | attributes: { 134 | position: mesh.positions, 135 | normal: normals(mesh.cells, mesh.positions) 136 | }, 137 | uniforms: { 138 | model: (context) => { 139 | var theta = context.time * 0.25 140 | return mat4.rotateY(rmat, mat4.identity(rmat), theta) 141 | }, 142 | view: ({tick}) => 143 | mat4.lookAt( 144 | mat4.create(), 145 | [0, 0, -40], 146 | [0, 0, 0], 147 | [0, 1, 0]), 148 | projection: ({viewportWidth, viewportHeight}) => 149 | mat4.perspective(mat4.create(), 150 | Math.PI / 4.0, 151 | viewportWidth / viewportHeight, 152 | 0.1, 153 | 1000.0) 154 | }, 155 | elements: mesh.cells 156 | }) 157 | 158 | return createVisual({ 159 | regl, 160 | keyboard, 161 | feedback: ` 162 | vec4 feedback(vec2 uv, float t, vec3 keys[NUM_KEYS], sampler2D image[2]) { 163 | return 0.8 * vec4(texture2D(image[1], uv).rgb, 0.5); 164 | } 165 | `, 166 | draw: () => { 167 | regl.clear({ 168 | color: [0, 0, 0, 1], 169 | depth: 1 170 | }) 171 | drawColumn() 172 | } 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /visuals/sph.js: -------------------------------------------------------------------------------- 1 | const createVisual = require('../plumbing/visual') 2 | const sphere = require('sphere-mesh')(20, 1) 3 | const mat4 = require('gl-mat4') 4 | const vectorizeText = require('vectorize-text') 5 | 6 | const text = require('./text.json') 7 | 8 | module.exports = function ({regl, keyboard}) { 9 | const NUM_KEYS = keyboard.keys.length 10 | 11 | const perspectiveMatrix = new Float32Array(16) 12 | const viewMatrix = new Float32Array(16) 13 | 14 | const textBuffers = text.map((str) => { 15 | const mesh = vectorizeText(str, { 16 | triangles: true 17 | }) 18 | return { 19 | elements: regl.elements(mesh.cells), 20 | positions: regl.buffer(mesh.positions) 21 | } 22 | }) 23 | 24 | const drawText = regl({ 25 | vert: ` 26 | precision highp float; 27 | attribute vec2 position; 28 | uniform vec3 offset; 29 | uniform float angle, scale; 30 | uniform mat4 projection; 31 | vec2 rotate (vec2 p) { 32 | float c = cos(angle); 33 | float s = sin(angle); 34 | return vec2( 35 | c * p.x - s * p.y, 36 | s * p.x + c * p.y); 37 | } 38 | void main () { 39 | gl_Position = projection * vec4( 40 | scale * rotate(vec2(position.x, -position.y)) + offset.xy, 41 | offset.z, 42 | 1); 43 | } 44 | `, 45 | 46 | frag: ` 47 | void main () { 48 | gl_FragColor = vec4(1, 1, 1, 1); 49 | } 50 | `, 51 | 52 | attributes: { 53 | position: (context, {symbol}) => 54 | textBuffers[symbol % textBuffers.length].positions 55 | }, 56 | uniforms: { 57 | offset: regl.prop('offset'), 58 | angle: regl.prop('angle'), 59 | scale: regl.prop('scale'), 60 | projection: ({viewportWidth, viewportHeight}) => 61 | mat4.perspective( 62 | perspectiveMatrix, 63 | Math.PI / 4.0, 64 | viewportWidth / viewportHeight, 65 | 0.01, 66 | 1000.0) 67 | }, 68 | elements: (context, {symbol}) => 69 | textBuffers[symbol % textBuffers.length].elements 70 | }) 71 | 72 | const drawLine = regl({ 73 | vert: ` 74 | precision highp float; 75 | attribute float x, key; 76 | varying float shift; 77 | void main () { 78 | shift = x; 79 | gl_Position = vec4(x, key - 0.5, 0, 1); 80 | } 81 | `, 82 | 83 | frag: ` 84 | precision highp float; 85 | varying float shift; 86 | void main () { 87 | gl_FragColor = vec4(0.2 * (1.0 - shift), shift, 1, 1); 88 | } 89 | `, 90 | 91 | attributes: { 92 | x: Array(NUM_KEYS).fill().map((_, i) => 2.0 * i / (NUM_KEYS - 1) - 1.0), 93 | key: regl.context('keyboard.keys') 94 | }, 95 | lineWidth: Math.min(8, regl.limits.lineWidthDims[1]), 96 | count: NUM_KEYS, 97 | primitive: 'line strip' 98 | }) 99 | 100 | const drawSphere = regl({ 101 | vert: ` 102 | precision highp float; 103 | attribute vec3 position; 104 | 105 | uniform mat4 projection, view; 106 | uniform vec3 keys[${NUM_KEYS}]; 107 | 108 | varying vec3 fragPos; 109 | 110 | void main () { 111 | float d = 1.0; 112 | for (int i = 0; i < ${NUM_KEYS}; ++i) { 113 | vec3 s = vec3( 114 | mod(float(37 * i), 53.0), 115 | mod(float(13 * i), 91.0), 116 | mod(float(23 * i), 89.0)) * position; 117 | float theta = length(s); 118 | vec3 V = normalize(vec3( 119 | sin(float(8 * i)), 120 | sin(float(11 * i) + 3.0), 121 | sin(float(5 * i) + 1.3))); 122 | d += keys[i].x * sin(theta) * dot(V, s) / theta; 123 | } 124 | 125 | fragPos = position; 126 | 127 | gl_Position = projection * view * vec4(d * position, 1); 128 | } 129 | `, 130 | 131 | frag: ` 132 | precision highp float; 133 | varying vec3 fragPos; 134 | void main () { 135 | vec3 color = fragPos; 136 | color.b += 1.5; 137 | float minP = min(min(color.x, color.y), color.z); 138 | float maxP = max(max(color.x, color.y), color.z); 139 | gl_FragColor = vec4((color - minP) / (maxP - minP), 1); 140 | } 141 | `, 142 | 143 | attributes: { 144 | position: sphere.positions 145 | }, 146 | 147 | uniforms: { 148 | projection: ({viewportWidth, viewportHeight}) => 149 | mat4.perspective( 150 | perspectiveMatrix, 151 | Math.PI / 4.0, 152 | viewportWidth / viewportHeight, 153 | 0.01, 154 | 1000.0), 155 | 156 | view: ({tick}) => { 157 | const t = 0.01 * tick 158 | return mat4.lookAt( 159 | viewMatrix, 160 | [5.0 * Math.cos(t), 5.0 * Math.sin(t), 5.0 * Math.cos(t)], 161 | [0, 0, 0], 162 | [0, Math.cos(0.1 * t), Math.sin(0.1 * t)]) 163 | } 164 | 165 | }, 166 | 167 | elements: sphere.cells // sc.skeleton(sphere.cells, 1) 168 | }) 169 | 170 | function generateText (text) { 171 | const angle = 2.0 * Math.PI * ((Math.random() * 4) | 0) / 4.0 172 | const velocity = [Math.cos(angle), Math.sin(angle)] 173 | const distance = 100.0 * Math.random() + 1.0 174 | const scale = 40.0 * Math.random() + 5.0 175 | const s = distance * scale 176 | const offset = [ 177 | -velocity[0] * s, 178 | -velocity[1] * s, 179 | -distance 180 | ] 181 | return { 182 | offset, 183 | velocity, 184 | lifetime: s * (4.0 + 4.0 * Math.random()), 185 | scale, 186 | angle, 187 | symbol: (Math.random() * textBuffers.length) | 0 188 | } 189 | } 190 | 191 | const textElements = Array(40).fill().map(() => generateText({})) 192 | 193 | return createVisual({ 194 | regl, 195 | keyboard, 196 | feedback: ` 197 | vec4 feedback(vec2 uv, float t, vec3 keys[NUM_KEYS], sampler2D image[2]) { 198 | vec4 result = texture2D(image[1], uv); 199 | vec2 d = uv - 0.5; 200 | float w = 1.0; 201 | for (int i = 0; i < NUM_KEYS; ++i) { 202 | float sx = 2.0 * cos(float(i) * 0.5); 203 | float sy = 2.0 * cos(float(i) * 0.5); 204 | float skew = pow(2.0 * cos(0.9 * float(i)), 10.0) / 1024.0; 205 | w += keys[i].x; 206 | result += keys[i].x * texture2D(image[0], uv + 207 | 0.1 * mat2( 208 | sx * skew, (1.0 - skew) * sy, 209 | (skew - 1.0) * sx, sy) * d); 210 | } 211 | return 0.8 * vec4(result.rgb / w, 0.5); 212 | } 213 | `, 214 | draw: () => { 215 | regl.clear({ 216 | color: [0, 0, 0, 1], 217 | depth: 1 218 | }) 219 | drawLine() 220 | drawSphere() 221 | drawText(textElements) 222 | 223 | for (let i = 0; i < textElements.length; ++i) { 224 | const text = textElements[i] 225 | for (let j = 0; j < 2; ++j) { 226 | text.offset[j] += text.velocity[j] 227 | } 228 | text.lifetime -= 1 229 | if (text.lifetime < 0) { 230 | textElements[i] = generateText() 231 | } 232 | } 233 | } 234 | }) 235 | } 236 | -------------------------------------------------------------------------------- /visuals/text.json: -------------------------------------------------------------------------------- 1 | [ 2 | "ポップアッcyberプ型のトースターは", 3 | "ポップアップトースター」とも呼ばれる", 4 | "縦置き式の箱型", 5 | "でスライSONYスした食パンが収まる幅と深さの溝があ", 6 | "そこに食パンを挿し電源スイ", 7 | "ッチを兼ねたレバーを押し下げる", 8 | "食パText Messageンは1-", 9 | "PEPSI 3分で焼け自動的に競りあがってくる", 10 | "熱源(発熱体)に調理物を平行に挟み込みながら表", 11 | "面に焼き目iPhone 6をつけ加熱調理する", 12 | "オーブン型に比して", 13 | "熱源が非常に近いという利点があり", 14 | "焼き上がATARIりが速くてパンに含まれる水分も", 15 | "逃がしにくい特長がある。1枚だけ焼けるもの、2枚焼けるもの、", 16 | "4枚焼けるも VHS", 17 | "のなどがある", 18 | "Cassette tape 食パンを入れずに通電すると内", 19 | "部の発熱体を傷めるため、複数の食パン" 20 | ] 21 | --------------------------------------------------------------------------------