woscope/index.js

733 wiersze
23 KiB
JavaScript

'use strict';
let glslify = require('glslify');
let shadersDict = {
vsLine: glslify(__dirname + '/shaders/vsLine.glsl'),
fsLine: glslify(__dirname + '/shaders/fsLine.glsl'),
vsBlurTranspose: glslify(__dirname + '/shaders/vsBlurTranspose.glsl'),
fsBlurTranspose: glslify(__dirname + '/shaders/fsBlurTranspose.glsl'),
vsOutput: glslify(__dirname + '/shaders/vsOutput.glsl'),
fsOutput: glslify(__dirname + '/shaders/fsOutput.glsl'),
vsProgress: glslify(__dirname + '/shaders/vsProgress.glsl'),
fsProgress: glslify(__dirname + '/shaders/fsProgress.glsl'),
};
let defaultColor = [1/32, 1, 1/32, 1],
defaultBackground = [0, 0, 0, 1];
let audioCtx;
function axhr(url, callback, errorCallback, progress) {
let request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onprogress = progress;
request.onload = function() {
if (request.status >= 400) {
return errorCallback(`Error loading audio file - ${request.status} ${request.statusText}`);
}
audioCtx.decodeAudioData(request.response, function(buffer) {
callback(buffer);
}, function (e) {
errorCallback('Unable to decode audio data');
});
};
request.send();
}
module.exports = woscope;
function woscope(config) {
audioCtx = audioCtx || initAudioCtx(config.error);
let canvas = config.canvas,
gl = initGl(canvas, config.background, config.error),
audio = config.audio,
audioUrl = config.audioUrl || audio.currentSrc || audio.src,
live = (config.live === true) ? getLiveType() : config.live,
callback = config.callback || function () {};
let ctx = {
gl: gl,
destroy: destroy,
live: live,
swap: config.swap,
invert: config.invert,
sweep: config.sweep,
color: config.color,
color2: config.color2,
lineSize: (config.lineSize === undefined) ? 0.012 : config.lineSize,
lineShader: createShader(gl, shadersDict.vsLine, shadersDict.fsLine),
blurShader: createShader(gl, shadersDict.vsBlurTranspose, shadersDict.fsBlurTranspose),
outputShader: createShader(gl, shadersDict.vsOutput, shadersDict.fsOutput),
progressShader: createShader(gl, shadersDict.vsProgress, shadersDict.fsProgress),
progress: 0,
loaded: false,
nSamples: 2048,
bloom: config.bloom,
};
Object.assign(ctx, {
quadIndex: makeQuadIndex(ctx),
vertexIndex: makeVertexIndex(ctx),
outQuadArray: makeOutQuad(ctx),
scratchBuffer: new Float32Array(ctx.nSamples*8),
audioRamp: makeRamp(Math.ceil(ctx.nSamples / 3)),
});
Object.assign(ctx, makeFrameBuffer(ctx, canvas.width, canvas.height));
function destroy() {
// release GPU in Chrome
let ext = gl.getExtension('WEBGL_lose_context');
if (ext) {
ext.loseContext();
}
// disconnect web audio nodes
if (ctx.sourceNode) {
ctx.sourceNode.disconnect();
ctx.sourceNode.connect(audioCtx.destination);
}
// end loops, empty context object
loop = emptyContext;
progressLoop = emptyContext;
function emptyContext() {
Object.keys(ctx).forEach(function (val) { delete ctx[val]; });
}
}
let loop = function() {
draw(ctx, canvas, audio);
requestAnimationFrame(loop);
};
if (ctx.live) {
ctx.sourceNode = config.sourceNode || audioCtx.createMediaElementSource(audio);
let source = gainWorkaround(ctx.sourceNode, audio);
if (ctx.live === 'scriptProcessor') {
ctx.scriptNode = initScriptNode(ctx, source);
} else {
ctx.analysers = initAnalysers(ctx, source);
}
callback(ctx);
loop();
return ctx;
}
let progressLoop = function() {
if (ctx.loaded) {
return;
}
drawProgress(ctx, canvas);
requestAnimationFrame(progressLoop);
};
progressLoop();
axhr(audioUrl, function(buffer) {
ctx.audioData = prepareAudioData(ctx, buffer);
ctx.loaded = true;
callback(ctx);
loop();
},
config.error,
function (e) {
ctx.progress = e.total ? e.loaded / e.total : 1.0;
console.log('progress: ' + e.loaded + ' / ' + e.total);
});
return ctx;
}
function supportsAnalyserFloat() {
return typeof audioCtx.createAnalyser().getFloatTimeDomainData === 'function';
}
function getLiveType() {
return supportsAnalyserFloat() ? 'analyser' : 'scriptProcessor';
}
function initAudioCtx(errorCallback) {
try {
let AudioCtx = window.AudioContext || window.webkitAudioContext;
return new AudioCtx();
} catch(e) {
let message = 'Web Audio API is not supported in this browser';
if (errorCallback) {
errorCallback(message);
}
throw new Error(message);
}
}
function initGl(canvas, background, errorCallback) {
let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
let message = 'WebGL is not supported in this browser :(';
if (errorCallback) {
errorCallback(message);
}
throw new Error(message);
}
gl.clearColor.apply(gl, background || defaultBackground);
return gl;
}
function initAnalysers(ctx, sourceNode) {
ctx.audioData = {
sourceChannels: sourceNode.channelCount,
};
// Split the combined channels
// Note: Chrome channelSplitter upmixes mono (out.L = in.M, out.R = in.M),
// Firefox/Edge/Safari do not (out.L = in.M, out.R = 0) - as of Feb 2017
let channelSplitter = audioCtx.createChannelSplitter(2);
sourceNode.connect(channelSplitter);
let analysers = [0, 1].map(function (val, index) {
let analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
channelSplitter.connect(analyser, index, 0);
return analyser;
});
let channelMerger = audioCtx.createChannelMerger(2);
analysers.forEach(function(analyser, index) {
analyser.connect(channelMerger, 0, index);
});
// connect the source directly to the destination to avoid mono inconsistency
sourceNode.connect(audioCtx.destination);
// Edge/Safari require analyser nodes to be connected to a destination
muteOutput(channelMerger).connect(audioCtx.destination);
return analysers;
}
function initScriptNode(ctx, sourceNode) {
let samples = 1024;
let scriptNode = audioCtx.createScriptProcessor(samples, 2, 2);
sourceNode.connect(scriptNode);
let audioData = [
new Float32Array(ctx.nSamples),
new Float32Array(ctx.nSamples),
];
ctx.audioData = {
left: audioData[0],
right: audioData[1],
sampleRate: audioCtx.sampleRate,
sourceChannels: sourceNode.channelCount,
};
function processAudio(e) {
// scriptProcessor can distort output when resource-constrained,
// so output silence instead by leaving the outputBuffer empty
let inputBuffer = e.inputBuffer;
for (let i=0; i < inputBuffer.numberOfChannels; i++) {
let inputData = inputBuffer.getChannelData(i);
// append to audioData arrays
let channel = audioData[i];
// shift forward by x samples
channel.set(channel.subarray(inputBuffer.length));
// add new samples at end
channel.set(inputData, channel.length - inputBuffer.length);
}
}
scriptNode.onaudioprocess = processAudio;
// connect the source directly to the destination to avoid distortion
sourceNode.connect(audioCtx.destination);
// Edge/Safari require scriptProcessor nodes to be connected to a destination
scriptNode.connect(audioCtx.destination);
return scriptNode;
}
function gainWorkaround(node, audio) {
// Safari: createMediaElementSource causes output to ignore volume slider,
// so match gain to slider as a workaround
let gainNode;
if (audioCtx.constructor.name === 'webkitAudioContext') {
gainNode = audioCtx.createGain();
audio.onvolumechange = function () {
gainNode.gain.value = (audio.muted) ? 0 : audio.volume;
};
node.connect(gainNode);
return gainNode;
} else {
return node;
}
}
function muteOutput(node) {
let gainNode = audioCtx.createGain();
gainNode.gain.value = 0;
node.connect(gainNode);
return gainNode;
}
function createShader(gl, vsSource, fsSource) {
if (!supportsWebGl()) {
throw new Error('createShader: no WebGL context');
}
let vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vsSource);
gl.compileShader(vs);
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
let infoLog = gl.getShaderInfoLog(vs);
gl.deleteShader(vs);
throw new Error('createShader, vertex shader compilation:\n' + infoLog);
}
let fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fsSource);
gl.compileShader(fs);
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
let infoLog = gl.getShaderInfoLog(fs);
gl.deleteShader(vs);
gl.deleteShader(fs);
throw new Error('createShader, fragment shader compilation:\n' + infoLog);
}
let program = gl.createProgram();
gl.attachShader(program, vs);
gl.deleteShader(vs);
gl.attachShader(program, fs);
gl.deleteShader(fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
let infoLog = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error('createShader, linking:\n' + infoLog);
}
return program;
}
function makeQuadIndex(ctx) {
let gl = ctx.gl;
let index = new Int16Array(ctx.nSamples*4);
for (let i = index.length; i--; ) {
index[i] = i;
}
let vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, index, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return vbo;
}
function makeVertexIndex(ctx) {
let gl = ctx.gl;
let len = (ctx.nSamples-1)*2*3,
index = new Uint16Array(len);
for (let i = 0, pos = 0; i < len; ) {
index[i++] = pos;
index[i++] = pos+2;
index[i++] = pos+1;
index[i++] = pos+1;
index[i++] = pos+2;
index[i++] = pos+3;
pos += 4;
}
let vbo = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vbo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return vbo;
}
function makeOutQuad(ctx) {
let gl = ctx.gl;
let data = new Int16Array([
-1, -1, 0, 0,
-1, 1, 0, 1,
1, -1, 1, 0,
1, 1, 1, 1,
]);
let vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return vbo;
}
function makeTargetTexture(gl, width, height) {
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture(gl.TEXTURE_2D, null);
return texture;
}
function makeFrameBuffer(ctx, width, height) {
let gl = ctx.gl;
let frameBuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
frameBuffer.width = 1024;
frameBuffer.height = 1024;
return {
renderBuffer: gl.createRenderbuffer(),
frameBuffer: frameBuffer,
lineTexture: makeTargetTexture(gl, frameBuffer.width, frameBuffer.height),
blurTexture: makeTargetTexture(gl, frameBuffer.width, frameBuffer.height),
blurTexture2: makeTargetTexture(gl, frameBuffer.width, frameBuffer.height),
vbo: gl.createBuffer(),
vbo2: gl.createBuffer(),
};
}
function prepareAudioData(ctx, buffer) {
let left = buffer.getChannelData(0),
right = (buffer.numberOfChannels > 1) ? buffer.getChannelData(1) : left;
return {
left: left,
right: right,
sampleRate: buffer.sampleRate,
sourceChannels: buffer.numberOfChannels,
};
}
function makeRamp(len) {
// returns array of "len" length, values linearly increase from -1 to 1
let arr = new Float32Array(len),
dx = 2 / (len - 1);
for (let i = 0; i < len; i++) {
arr[i] = (i * dx) - 1;
}
return arr;
}
function loadWaveAtPosition(ctx, position) {
position = Math.max(0, position - 1/120);
position = Math.floor(position*ctx.audioData.sampleRate);
let end = Math.min(ctx.audioData.left.length, position + ctx.nSamples) - 1,
len = end - position + 1;
let left = ctx.audioData.left.subarray(position, end),
right = ctx.audioData.right.subarray(position, end);
channelRouter(ctx, len, left, right);
}
function loadWaveLive(ctx) {
let analyser0 = ctx.analysers[0],
analyser1 = ctx.analysers[1];
let len = Math.min(ctx.nSamples, analyser0.fftSize),
left = new Float32Array(analyser0.fftSize),
right = new Float32Array(analyser1.fftSize);
analyser0.getFloatTimeDomainData(left);
analyser1.getFloatTimeDomainData(right);
channelRouter(ctx, len, left, right);
}
function channelRouter(ctx, len, left, right) {
if (ctx.sweep && ctx.swap) {
loadChannelsInto(ctx, len, ctx.vbo, ctx.audioRamp, right);
loadChannelsInto(ctx, len, ctx.vbo2, ctx.audioRamp, left);
} else if (ctx.sweep) {
loadChannelsInto(ctx, len, ctx.vbo, ctx.audioRamp, left);
loadChannelsInto(ctx, len, ctx.vbo2, ctx.audioRamp, right);
} else if (ctx.swap) {
loadChannelsInto(ctx, len, ctx.vbo, right, left);
} else {
loadChannelsInto(ctx, len, ctx.vbo, left, right);
}
}
function loadChannelsInto(ctx, len, vbo, xAxis, yAxis) {
let gl = ctx.gl,
subArr = ctx.scratchBuffer;
for (let i = 0; i < len; i++) {
let t = i*8;
subArr[t] = subArr[t+2] = subArr[t+4] = subArr[t+6] = xAxis[i];
subArr[t+1] = subArr[t+3] = subArr[t+5] = subArr[t+7] = yAxis[i];
}
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, subArr, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
function supportsWebGl() {
// from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/webgl.js
let canvas = document.createElement('canvas'),
supports = 'probablySupportsContext' in canvas ? 'probablySupportsContext' : 'supportsContext';
if (supports in canvas) {
return canvas[supports]('webgl') || canvas[supports]('experimental-webgl');
}
return 'WebGLRenderingContext' in window;
}
function activateTargetTexture(ctx, texture) {
let gl = ctx.gl;
gl.bindRenderbuffer(gl.RENDERBUFFER, ctx.renderBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, ctx.frameBuffer.width, ctx.frameBuffer.height);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, ctx.renderBuffer);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
}
function drawProgress(ctx, canvas) {
let progress = ctx.progress;
let gl = ctx.gl;
let width = canvas.width,
height = canvas.height;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, width, height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(ctx.progressShader);
{
let tmpPos = gl.getUniformLocation(ctx.progressShader, 'uProgress');
if (tmpPos && tmpPos !== -1) {
gl.uniform1f(tmpPos, progress);
}
tmpPos = gl.getUniformLocation(ctx.progressShader, 'uColor');
if (tmpPos && tmpPos !== -1) {
gl.uniform4fv(tmpPos, ctx.color || defaultColor);
}
}
gl.bindBuffer(gl.ARRAY_BUFFER, ctx.outQuadArray);
let attribs = [];
{
let tmpAttr = gl.getAttribLocation(ctx.progressShader, 'aPos');
if (tmpAttr > -1) {
gl.enableVertexAttribArray(tmpAttr);
gl.vertexAttribPointer(tmpAttr, 2, gl.SHORT, false, 8, 0);
attribs.push(tmpAttr);
}
tmpAttr = gl.getAttribLocation(ctx.progressShader, 'aUV');
if (tmpAttr > -1) {
gl.enableVertexAttribArray(tmpAttr);
gl.vertexAttribPointer(tmpAttr, 2, gl.SHORT, false, 8, 4);
attribs.push(tmpAttr);
}
}
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
attribs.forEach(function(a){
gl.disableVertexAttribArray(a);
});
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(null);
}
function draw(ctx, canvas, audio) {
let gl = ctx.gl;
if (ctx.live) {
if (ctx.live === 'scriptProcessor') {
loadWaveAtPosition(ctx, 0);
} else {
loadWaveLive(ctx);
}
} else {
loadWaveAtPosition(ctx, audio.currentTime);
}
let width = canvas.width,
height = canvas.height;
if (!ctx.bloom) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, width, height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
drawLine(ctx, ctx.lineShader, ctx.vbo, ctx.color);
if (ctx.sweep) {
drawLine(ctx, ctx.lineShader, ctx.vbo2, ctx.color2);
}
} else {
gl.bindFramebuffer(gl.FRAMEBUFFER, ctx.frameBuffer);
activateTargetTexture(ctx, ctx.lineTexture);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, width, height);
drawLine(ctx, ctx.lineShader, ctx.vbo, ctx.color);
if (ctx.sweep) {
drawLine(ctx, ctx.lineShader, ctx.vbo2, ctx.color2);
}
{ // generate mipmap
gl.bindTexture(gl.TEXTURE_2D, ctx.lineTexture);
gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture(gl.TEXTURE_2D, null);
}
// downscale
activateTargetTexture(ctx, ctx.blurTexture2);
gl.viewport(0, 0, width/2, height/2);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
drawTexture(ctx, ctx.lineTexture, width, ctx.outputShader);
// blur x
activateTargetTexture(ctx, ctx.blurTexture);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
drawTexture(ctx, ctx.blurTexture2, width/2, ctx.blurShader);
// blur y
activateTargetTexture(ctx, ctx.blurTexture2);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
drawTexture(ctx, ctx.blurTexture, width/2, ctx.blurShader);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, width, height);
drawTexture(ctx, ctx.lineTexture, width, ctx.outputShader);
drawTexture(ctx, ctx.blurTexture2, width/2, ctx.outputShader, 0.5);
}
}
function drawLine(ctx, shader, vbo, color) {
let gl = ctx.gl;
gl.useProgram(shader);
{
let tmpPos = gl.getUniformLocation(shader, 'uInvert');
if (tmpPos && tmpPos !== -1) {
gl.uniform1f(tmpPos, (ctx.invert) ? -1 : 1);
}
tmpPos = gl.getUniformLocation(shader, 'uSize');
if (tmpPos && tmpPos !== -1) {
gl.uniform1f(tmpPos, ctx.lineSize);
}
tmpPos = gl.getUniformLocation(shader, 'uIntensity');
if (tmpPos && tmpPos !== -1) {
gl.uniform1f(tmpPos, 1);
}
tmpPos = gl.getUniformLocation(shader, 'uColor');
if (tmpPos && tmpPos !== -1) {
gl.uniform4fv(tmpPos, color || defaultColor);
}
}
let attribs = [];
{
gl.bindBuffer(gl.ARRAY_BUFFER, ctx.quadIndex);
let idxAttr = gl.getAttribLocation(shader, 'aIdx');
if (idxAttr > -1) {
gl.enableVertexAttribArray(idxAttr);
gl.vertexAttribPointer(idxAttr, 1, gl.SHORT, false, 2, 0);
attribs.push(idxAttr);
}
}
{
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
let tmpPos = gl.getAttribLocation(shader, 'aStart');
if (tmpPos > -1) {
gl.enableVertexAttribArray(tmpPos);
gl.vertexAttribPointer(tmpPos, 2, gl.FLOAT, false, 8, 0);
attribs.push(tmpPos);
}
tmpPos = gl.getAttribLocation(shader, 'aEnd');
if (tmpPos > -1) {
gl.enableVertexAttribArray(tmpPos);
gl.vertexAttribPointer(tmpPos, 2, gl.FLOAT, false, 8, 8*4);
attribs.push(tmpPos);
}
}
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ctx.vertexIndex);
gl.drawElements(gl.TRIANGLES, (ctx.nSamples-1)*2*3, gl.UNSIGNED_SHORT, 0);
gl.disable(gl.BLEND);
attribs.forEach(function(a) {
gl.disableVertexAttribArray(a);
});
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(null);
}
function drawTexture(ctx, texture, size, shader, alpha) {
let gl = ctx.gl;
alpha = alpha || 1;
gl.useProgram(shader);
let attribs = [];
gl.bindBuffer(gl.ARRAY_BUFFER, ctx.outQuadArray);
{
let tmpPos = gl.getAttribLocation(shader, 'aPos');
if (tmpPos > -1) {
gl.enableVertexAttribArray(tmpPos);
gl.vertexAttribPointer(tmpPos, 2, gl.SHORT, false, 8, 0);
attribs.push(tmpPos);
}
tmpPos = gl.getAttribLocation(shader, 'aST');
if (tmpPos > -1) {
gl.enableVertexAttribArray(tmpPos);
gl.vertexAttribPointer(tmpPos, 2, gl.SHORT, false, 8, 4);
attribs.push(tmpPos);
}
}
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
{
let tmpPos = gl.getUniformLocation(shader, 'uTexture');
if (tmpPos && tmpPos !== -1) {
gl.uniform1i(tmpPos, 0);
}
tmpPos = gl.getUniformLocation(shader, 'uSize');
if (tmpPos && tmpPos !== -1) {
gl.uniform1f(tmpPos, size);
}
tmpPos = gl.getUniformLocation(shader, 'uAlpha');
if (tmpPos && tmpPos !== -1) {
gl.uniform1f(tmpPos, alpha);
}
}
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.SRC_ALPHA);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
attribs.forEach(function(a) {
gl.disableVertexAttribArray(a);
});
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.useProgram(null);
}