hydra-synth
Version:
base synth for hydra-editor
475 lines (419 loc) • 12.7 kB
JavaScript
import Output from './output.js'
import loop from 'raf-loop'
import Source from './hydra-source.js'
import MouseTools from './lib/mouse.js'
import Audio from './lib/audio.js'
import VidRecorder from './lib/video-recorder.js'
import ArrayUtils from './lib/array-utils.js'
// import strudel from './lib/strudel.js'
import Sandbox from './eval-sandbox.js'
import Generator from './generator-factory.js'
import regl from 'regl'
// const window = global.window
const Mouse = MouseTools()
// to do: add ability to pass in certain uniforms and transforms
class HydraRenderer {
constructor ({
pb = null,
width = 1280,
height = 720,
numSources = 4,
numOutputs = 4,
makeGlobal = true,
autoLoop = true,
detectAudio = true,
enableStreamCapture = true,
canvas,
precision,
extendTransforms = {} // add your own functions on init
} = {}) {
ArrayUtils.init()
this.pb = pb
this.width = width
this.height = height
this.renderAll = false
this.detectAudio = detectAudio
this._initCanvas(canvas)
//global.window.test = 'hi'
// object that contains all properties that will be made available on the global context and during local evaluation
this.synth = {
time: 0,
bpm: 30,
width: this.width,
height: this.height,
fps: undefined,
stats: {
fps: 0
},
speed: 1,
mouse: Mouse,
render: this._render.bind(this),
setResolution: this.setResolution.bind(this),
update: (dt) => {},// user defined update function
hush: this.hush.bind(this),
tick: this.tick.bind(this)
}
if (makeGlobal) window.loadScript = this.loadScript
this.timeSinceLastUpdate = 0
this._time = 0 // for internal use, only to use for deciding when to render frames
// only allow valid precision options
let precisionOptions = ['lowp','mediump','highp']
if(precision && precisionOptions.includes(precision.toLowerCase())) {
this.precision = precision.toLowerCase()
//
// if(!precisionValid){
// console.warn('[hydra-synth warning]\nConstructor was provided an invalid floating point precision value of "' + precision + '". Using default value of "mediump" instead.')
// }
} else {
let isIOS =
(/iPad|iPhone|iPod/.test(navigator.platform) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
!window.MSStream;
this.precision = isIOS ? 'highp' : 'mediump'
}
this.extendTransforms = extendTransforms
// boolean to store when to save screenshot
this.saveFrame = false
// if stream capture is enabled, this object contains the capture stream
this.captureStream = null
this.generator = undefined
this._initRegl()
this._initOutputs(numOutputs)
this._initSources(numSources)
this._generateGlslTransforms()
this.synth.screencap = () => {
this.saveFrame = true
}
if (enableStreamCapture) {
try {
this.captureStream = this.canvas.captureStream(25)
// to do: enable capture stream of specific sources and outputs
this.synth.vidRecorder = new VidRecorder(this.captureStream)
} catch (e) {
console.warn('[hydra-synth warning]\nnew MediaSource() is not currently supported on iOS.')
console.error(e)
}
}
if(detectAudio) this._initAudio()
if(autoLoop) loop(this.tick.bind(this)).start()
// final argument is properties that the user can set, all others are treated as read-only
this.sandbox = new Sandbox(this.synth, makeGlobal, ['speed', 'update', 'bpm', 'fps'])
}
eval(code) {
this.sandbox.eval(code)
}
getScreenImage(callback) {
this.imageCallback = callback
this.saveFrame = true
}
hush() {
this.s.forEach((source) => {
source.clear()
})
this.o.forEach((output) => {
this.synth.solid(0, 0, 0, 0).out(output)
})
this.synth.render(this.o[0])
// this.synth.update = (dt) => {}
this.sandbox.set('update', (dt) => {})
}
loadScript(url = "") {
const p = new Promise((res, rej) => {
var script = document.createElement("script");
script.onload = function () {
console.log(`loaded script ${url}`);
res();
};
script.onerror = (err) => {
console.log(`error loading script ${url}`, "log-error");
res()
};
script.src = url;
document.head.appendChild(script);
});
return p;
}
setResolution(width, height) {
// console.log(width, height)
this.canvas.width = width
this.canvas.height = height
this.width = width // is this necessary?
this.height = height // ?
this.sandbox.set('width', width)
this.sandbox.set('height', height)
console.log(this.width)
this.o.forEach((output) => {
output.resize(width, height)
})
this.s.forEach((source) => {
source.resize(width, height)
})
this.regl._refresh()
console.log(this.canvas.width)
}
canvasToImage (callback) {
const a = document.createElement('a')
a.style.display = 'none'
let d = new Date()
a.download = `hydra-${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}-${d.getHours()}.${d.getMinutes()}.${d.getSeconds()}.png`
document.body.appendChild(a)
var self = this
this.canvas.toBlob( (blob) => {
if(self.imageCallback){
self.imageCallback(blob)
delete self.imageCallback
} else {
a.href = URL.createObjectURL(blob)
console.log(a.href)
a.click()
}
}, 'image/png')
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(a.href);
}, 300);
}
_initAudio () {
const that = this
this.synth.a = new Audio({
numBins: 4,
parentEl: this.canvas.parentNode
// changeListener: ({audio}) => {
// that.a = audio.bins.map((_, index) =>
// (scale = 1, offset = 0) => () => (audio.fft[index] * scale + offset)
// )
//
// if (that.makeGlobal) {
// that.a.forEach((a, index) => {
// const aname = `a${index}`
// window[aname] = a
// })
// }
// }
})
}
// create main output canvas and add to screen
_initCanvas (canvas) {
if (canvas) {
this.canvas = canvas
this.width = canvas.width
this.height = canvas.height
} else {
this.canvas = document.createElement('canvas')
this.canvas.width = this.width
this.canvas.height = this.height
this.canvas.style.width = '100%'
this.canvas.style.height = '100%'
this.canvas.style.imageRendering = 'pixelated'
document.body.appendChild(this.canvas)
}
}
_initRegl () {
this.regl = regl({
// profile: true,
canvas: this.canvas,
pixelRatio: 1//,
// extensions: [
// 'oes_texture_half_float',
// 'oes_texture_half_float_linear'
// ],
// optionalExtensions: [
// 'oes_texture_float',
// 'oes_texture_float_linear'
//]
})
// This clears the color buffer to black and the depth buffer to 1
this.regl.clear({
color: [0, 0, 0, 1]
})
this.renderAll = this.regl({
frag: `
precision ${this.precision} float;
varying vec2 uv;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform sampler2D tex3;
void main () {
vec2 st = vec2(1.0 - uv.x, uv.y);
st*= vec2(2);
vec2 q = floor(st).xy*(vec2(2.0, 1.0));
int quad = int(q.x) + int(q.y);
st.x += step(1., mod(st.y,2.0));
st.y += step(1., mod(st.x,2.0));
st = fract(st);
if(quad==0){
gl_FragColor = texture2D(tex0, st);
} else if(quad==1){
gl_FragColor = texture2D(tex1, st);
} else if (quad==2){
gl_FragColor = texture2D(tex2, st);
} else {
gl_FragColor = texture2D(tex3, st);
}
}
`,
vert: `
precision ${this.precision} float;
attribute vec2 position;
varying vec2 uv;
void main () {
uv = position;
gl_Position = vec4(1.0 - 2.0 * position, 0, 1);
}`,
attributes: {
position: [
[-2, 0],
[0, -2],
[2, 2]
]
},
uniforms: {
tex0: this.regl.prop('tex0'),
tex1: this.regl.prop('tex1'),
tex2: this.regl.prop('tex2'),
tex3: this.regl.prop('tex3')
},
count: 3,
depth: { enable: false }
})
this.renderFbo = this.regl({
frag: `
precision ${this.precision} float;
varying vec2 uv;
uniform vec2 resolution;
uniform sampler2D tex0;
void main () {
gl_FragColor = texture2D(tex0, vec2(1.0 - uv.x, uv.y));
}
`,
vert: `
precision ${this.precision} float;
attribute vec2 position;
varying vec2 uv;
void main () {
uv = position;
gl_Position = vec4(1.0 - 2.0 * position, 0, 1);
}`,
attributes: {
position: [
[-2, 0],
[0, -2],
[2, 2]
]
},
uniforms: {
tex0: this.regl.prop('tex0'),
resolution: this.regl.prop('resolution')
},
count: 3,
depth: { enable: false }
})
}
_initOutputs (numOutputs) {
const self = this
this.o = (Array(numOutputs)).fill().map((el, index) => {
var o = new Output({
regl: this.regl,
width: this.width,
height: this.height,
precision: this.precision,
label: `o${index}`
})
// o.render()
o.id = index
self.synth['o'+index] = o
return o
})
// set default output
this.output = this.o[0]
}
_initSources (numSources) {
this.s = []
for(var i = 0; i < numSources; i++) {
this.createSource(i)
}
}
createSource (i) {
let s = new Source({regl: this.regl, pb: this.pb, width: this.width, height: this.height, label: `s${i}`})
this.synth['s' + this.s.length] = s
this.s.push(s)
return s
}
_generateGlslTransforms () {
var self = this
this.generator = new Generator({
defaultOutput: this.o[0],
defaultUniforms: this.o[0].uniforms,
extendTransforms: this.extendTransforms,
changeListener: ({type, method, synth}) => {
if (type === 'add') {
self.synth[method] = synth.generators[method]
if(self.sandbox) self.sandbox.add(method)
} else if (type === 'remove') {
// what to do here? dangerously deleting window methods
//delete window[method]
}
// }
}
})
this.synth.setFunction = this.generator.setFunction.bind(this.generator)
}
_render (output) {
if (output) {
this.output = output
this.isRenderingAll = false
} else {
this.isRenderingAll = true
}
}
// dt in ms
tick (dt, uniforms) {
this.sandbox.tick()
if(this.detectAudio === true) this.synth.a.tick()
// let updateInterval = 1000/this.synth.fps // ms
this.sandbox.set('time', this.synth.time += dt * 0.001 * this.synth.speed)
this.timeSinceLastUpdate += dt
if(!this.synth.fps || this.timeSinceLastUpdate >= 1000/this.synth.fps) {
// console.log(1000/this.timeSinceLastUpdate)
this.synth.stats.fps = Math.ceil(1000/this.timeSinceLastUpdate)
if(this.synth.update) {
try { this.synth.update(this.timeSinceLastUpdate) } catch (e) { console.log(e) }
}
// console.log(this.synth.speed, this.synth.time)
for (let i = 0; i < this.s.length; i++) {
this.s[i].tick(this.synth.time)
}
// console.log(this.canvas.width, this.canvas.height)
for (let i = 0; i < this.o.length; i++) {
this.o[i].tick({
time: this.synth.time,
mouse: this.synth.mouse,
bpm: this.synth.bpm,
resolution: [this.canvas.width, this.canvas.height]
})
}
if (this.isRenderingAll) {
this.renderAll({
tex0: this.o[0].getCurrent(),
tex1: this.o[1].getCurrent(),
tex2: this.o[2].getCurrent(),
tex3: this.o[3].getCurrent(),
resolution: [this.canvas.width, this.canvas.height]
})
} else {
this.renderFbo({
tex0: this.output.getCurrent(),
resolution: [this.canvas.width, this.canvas.height]
})
}
this.timeSinceLastUpdate = 0
}
if(this.saveFrame === true) {
this.canvasToImage()
this.saveFrame = false
}
// this.regl.poll()
}
}
export default HydraRenderer