<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>XXY Oscilloscope</title>
<style>
audio { width: 100%; }
body{font: 12px Courier, Monospace; line-height:70%;}
canvas{margin-right: 10px;}
table {
border-spacing:0;
border-collapse: collapse;
}
#startOverlay {
display: none;
position: fixed;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.4);
color: rgb(255, 255, 255);
font-size: 10vh;
top: 0;
left: 0;
text-align: center;
padding-top: 45vh;
cursor: pointer;
}
</style>
</head>
<body bgcolor="silver" text="black" autocomplete="off">
<script>
var controls=
{
swapXY : false,
light : false,
sweepOn : false,
sweepMsDiv : 1,
sweepTriggerValue : 0,
signalGeneratorOn : false,
mainGain : 0.0,
exposureStops : 0.0,
audioVolume : 1.0,
hue : 125,
freezeImage: false,
disableFilter: false,
aValue : 1.0,
aExponent : 0.0,
bValue : 1.0,
bExponent :0.0,
invertXY : false,
grid : true,
persistence : 0,
xExpression : "sin(2*PI*a*t)*cos(2*PI*b*t)",
yExpression : "cos(2*PI*a*t)*cos(2*PI*b*t)",
}
Number.prototype.toFixedMinus = function(k)
{
if (this<0) return this.toFixed(k);
//else return '\xa0'+this.toFixed(k);
else return '+'+this.toFixed(k);
}
var toggleVisible = function(string)
{
var element = document.getElementById(string);
//console.log(element.style.display);
if (element.style.display == "none") element.style.display="block";
else element.style.display = "none";
}
</script>
<div id="startOverlay">START</div>
<table align="center">
<tbody><tr>
<td valign="top">
<canvas id="crtCanvas" width="584" height="584" style="z-index: 0;"></canvas>
<div id="canvasFailure" style="position: relative; z-index: 1; font:25px arial; top:-40px; color:lightgreen;"></div>
<a href="javascript:toggleVisible('sidebar'); window.onresize();">toggle sidebar</a>
</td>
<td id="sidebar" width="360" valign="top">
<p style="font-size:5px;"> </p>
<b id="title" style="font-size:26px;"> XXY OSCILLOSCOPE </b> <b id="samplerate">44.1kHz</b>
<a href="javascript:toggleVisible('introNotes');" style="float:right;margin-top:4px"><big>?</big></a>
<br>
<div id="introNotes" style="display:none">
<p style="line-height:110%">Version 1.0 (April 2017). Made
by <a href="http://venuspatrol.nfshost.com/">Neil Thapen</a>.<br>
Thanks to m1el and ompuco for inspiration.<br>
Line-drawing code adapted from
<a href="https://github.com/m1el/woscope">woscope</a> by m1el.<br>
<br>
This uses an upsampling filter to simulate a digital-analogue
converter between the computer and the oscilloscope. It turns sharp corners
in the signal into curves, loops or ringing artefacts.
If the page is running slowly, try disabling upsampling.
</p></div>
<br>
<hr noshade="" style="margin-top:10px">
<table>
<tbody><tr>
<td> <!--TOP LEFT CONTROL-->
<table>
<tbody><tr><td align="center">Gain</td></tr>
<tr><td><input id="mainGain" type="range" width="200" min="-1" max="4" value="0.0" step="0.05" oninput="controls.mainGain=mainGain.value; mainGainOutput.value=parseFloat(mainGain.value).toFixedMinus(2)+'Â '"></td>
<td> <output id="mainGainOutput">+0.00 </output></td>
</tr></tbody></table>
</td>
<td> <!--TOP RIGHT CONTROL-->
<table>
<tbody><tr><td align="center">Intensity</td></tr>
<tr><td><input id="exposure" type="range" width="200" min="-2" max="2" value="0.0" step="0.1" oninput="controls.exposureStops=this.value; exposureOutput.value=parseFloat(this.value).toFixedMinus(1)"></td>
<td> <output id="exposureOutput">+0.0</output></td>
</tr></tbody></table>
</td>
</tr><tr>
<td> <!--BOTTOM LEFT CONTROL-->
<table>
<tbody><tr><td align="center">Audio volume</td></tr>
<tr><td><input id="audioVolume" type="range" width="200" min="0" max="1" value="1.0" step="0.01" oninput="controls.audioVolume=this.value; audioVolumeOutput.value=parseFloat(this.value).toFixed(2)"></td>
<td> <output id="audioVolumeOutput">1.00</output></td>
</tr></tbody></table>
</td>
<td> <!--BOTTOM RIGHT CONTROL-->
<table>
<tbody><tr><td>
<input id="swapXY" type="checkbox" onchange="controls.swapXY=this.checked"> Swap x / y
</td></tr>
<tr><td>
<input id="invertXY" type="checkbox" onchange="controls.invertXY=this.checked"> Invert x and y
</td></tr>
<tr><td>
<input id="light" type="checkbox" onchange="controls.light=this.checked"> Graticule light
</td></tr>
</tbody></table>
</td>
</tr>
</tbody></table>
<hr noshade="">
<p><b style="font-size:18px">
<input id="sweepCheckbox" type="checkbox" onchange="controls.sweepOn=this.checked"> SWEEP</b>
<a href="javascript:toggleVisible('sweepNotes');" style="float:right;margin-top:3px"><big>?</big></a>
</p><table>
<tbody><tr>
<td>
<table>
<tbody><tr><td align="center">Trigger value</td></tr>
<tr><td><input id="trigger" type="range" width="200" min="-1" max="1" value="0.0" step="0.01" oninput="controls.sweepTriggerValue=this.value*Math.sqrt(Math.abs(this.value)); triggerOutput.value=parseFloat(controls.sweepTriggerValue).toFixedMinus(2)+'Â '"></td>
<td> <output id="triggerOutput">+0.00 </output></td>
</tr></tbody></table>
</td>
<td>
<table>
<tbody><tr><td align="center">Milliseconds/div</td></tr>
<tr><td><input id="msDiv" type="range" width="200" min="0" max="7" value="2" step="1" oninput="controls.sweepMsDiv=Math.pow(2, this.value-2); msDivOutput.value = controls.sweepMsDiv"></td>
<td> <output id="msDivOutput">1</output></td>
</tr></tbody></table>
</td>
</tr>
</tbody></table>
<div id="sweepNotes" style="display:none">
<p style="line-height:110%">
The trace moves to the right at a fixed speed. Once
off the screen, it restarts from the left as soon as <i>y</i>
moves above the trigger value.
</p></div>
<hr noshade="">
<p><b style="font-size:18px">
<input id="generatorCheckbox" type="checkbox" onchange="controls.signalGeneratorOn=this.checked; AudioSystem.connectMicrophone();"> SIGNAL GENERATOR</b>
<a href="javascript:toggleVisible('generatorNotes');" style="float:right;margin-top:3px"><big>?</big></a>
</p><div id="generatorNotes" style="display:none">
<p style="line-height:110%">
Enter mathematical expressions (in javascript). <br>
<i>t</i> is time and <i>n</i> is the number of samples so far.<br>
You can use <i>mx</i> and <i>my</i> for the signal from microphone, if it's active.
</p></div>
<p> x = <input type="text" size="37" id="xInput" value="" onkeydown="if (event.keyCode == 13) {UI.compile(); xNote.value=''; this.style.color='black';}" oninput="if (this.value != controls.xExpression) {xNote.value='*'; this.style.color='blue';} else {xNote.value=''; this.style.color='black';}">
<output id="xNote"> </output><br>
y = <input type="text" size="37" id="yInput" value="" onkeydown="if (event.keyCode == 13) {UI.compile(); yNote.value=''; this.style.color='black';}" oninput="if (this.value != controls.yExpression) {yNote.value='*'; this.style.color='blue';} else {yNote.value=''; this.style.color='black';}">
<output id="yNote"> </output><br>
</p><table border="0">
<tbody><tr>
<td width="155"></td> <td width="45"></td><td width="155"></td> <td width="45"></td>
</tr><tr>
<td align="right">Parameter a</td> <td></td>
<td><input id="aExponent" type="range" style="width:90%" min="0" max="3" value="0" step="1" oninput="controls.aExponent=this.value; aExponentOutput.value=[' x1',' x10','x100','x1000'][this.value]"></td>
<td> <output id="aExponentOutput"> x1</output></td>
</tr><tr>
<td colspan="3"><input id="aValue" type="range" style="width:95%" min="0.5" max="5.00" value="1.0" step="0.02" oninput="controls.aValue=this.value; aValueOutput.value=parseFloat(this.value).toFixed(2)"><br></td>
<td> <output id="aValueOutput">1.00</output></td>
</tr>
<tr><td height="5"></td></tr>
<tr>
<td align="right">Parameter b</td> <td></td>
<td><input id="bExponent" type="range" style="width:90%" min="0" max="3" value="0" step="1" oninput="controls.bExponent=this.value; bExponentOutput.value=[' x1',' x10','x100','x1000'][this.value]"></td>
<td> <output id="bExponentOutput"> x1</output></td>
</tr><tr>
<td colspan="3"><input id="bValue" type="range" style="width:95%" min="0.5" max="5.00" value="1.0" step="0.02" oninput="controls.bValue=this.value; bValueOutput.value=parseFloat(this.value).toFixed(2)"></td>
<td> <output id="bValueOutput">1.00</output></td></tr>
</tbody></table>
<hr noshade="">
<p><b style="font-size:18px">
<input type="checkbox" id="micCheckbox" onchange="if (this.checked) AudioSystem.tryToGetMicrophone(); else AudioSystem.disconnectMicrophone()"> MICROPHONE</b>
<output id="microphoneOutput"></output>
<a href="javascript:toggleVisible('micNotes');" style="float:right;margin-top:3px"><big>?</big></a>
</p><div id="micNotes" style="display:none">
<p style="line-height:110%">
<i>x</i> is the left channel, <i>y</i> the right.<br>
Unavailable in Safari. Only stereo in Chrome.<br>
<br>
To get audio from another program,
you can either physically connect your audio output to your audio input,
or use third party software,
such as <a href="http://vb-audio.pagesperso-orange.fr/Cable/">VB-CABLE</a> on Windows
or <a href="https://github.com/mattingalls/Soundflower">Soundflower</a> with
<a href="https://github.com/mLupine/SoundflowerBed">SoundflowerBed</a> on MacOS.
</p></div>
<hr noshade="">
<table>
<tbody><tr>
<td width="200"><b style="font-size:18px"> PLAY FILE<b></b></b></td>
<td width="200"><input id="audioFile" type="file" accept="audio/*"></td>
<td><a href="javascript:toggleVisible('fileNotes');"><big>?</big></a></td>
</tr>
</tbody></table>
<p><audio id="audioElement" controls=""></audio>
<script>
var file;
audioFile.onchange = function()
{
if (file) URL.revokeObjectURL(file)
var files = this.files;
file = URL.createObjectURL(files[0]);
audioElement.src = file;
audioElement.play();
};
</script>
</p><div id="fileNotes" style="display:none">
<p style="line-height:110%">
<i>x</i> is the left channel, <i>y</i> the right.<br>
Have a look at <a href="http://oscilloscopemusic.com/">oscilloscopemusic.com</a>
for music written to be displayed like this.
</p></div>
<hr noshade="">
<a href="javascript:toggleVisible('extraNotes');" style="float:right;margin-top:3px"><big>?</big></a>
<table><tbody><tr>
<td>
<table>
<tbody><tr><td align="center">Hue</td></tr>
<tr><td><input id="hue" type="range" width="200" min="0" max="359" value="125" step="1" oninput="controls.hue=this.value; hueOutput.value=this.value"></td>
<td width="30"> <output id="hueOutput">125</output></td>
</tr>
<tr><td align="center">Persistence</td></tr>
<tr><td><input id="persistence" type="range" width="200" min="-1" max="1" value="0" step="0.01" oninput="controls.persistence=this.value; persistenceOutput.value=parseFloat(this.value).toFixedMinus(1)"></td>
<td width="30"> <output id="persistenceOutput">0.00</output></td>
</tr>
</tbody></table>
</td>
<td>
<table>
<tbody><tr><td>
<input id="freeze" type="checkbox" onchange="controls.freezeImage=this.checked"> Freeze image
</td>
</tr>
<tr><td>
<input id="disableFilter" type="checkbox" onchange="controls.disableFilter=this.checked"> Disable upsampling
</td></tr>
<tr><td>
<input id="hideGrid" type="checkbox" onchange="controls.grid=!this.checked; if (Render) Render.screenTexture = Render.loadTexture('noise.jpg');"> Hide graticule
</td></tr>
</tbody></table>
</td>
</tr></tbody></table>
<input id="urlText" type="text" size="28" style="margin-top:5px" onclick="Controls.generateUrl()" value=" export current settings as a URL">
<a href="javascript:Controls.restoreDefaults();">[reset all]</a>
<div id="extraNotes" style="display:none">
<p style="line-height:110%">
To share your settings, click on the textbox, copy the URL that appears there, and
send it to someone else.
</p></div>
<script>
var Controls = {
generateUrl : function()
{
var locationString = location.toString();
var site = locationString.split('#')[0];
var text = this.getControlsArray().toString();
var hm = encodeURI(text);
urlText.value = site+'#'+hm;
urlText.select();
},
getControlsArray : function()
{
var a = [];
a.push(mainGain.value);
a.push(exposure.value);
//a.push(audioVolume.value);
a.push(0+swapXY.checked);
a.push(0+invertXY.checked);
a.push(0+light.checked);
a.push(0+sweepCheckbox.checked);
a.push(trigger.value);
a.push(msDiv.value);
a.push(0+generatorCheckbox.checked);
a.push(this.encodeString(xInput.value));
a.push(this.encodeString(yInput.value));
a.push(aExponent.value);
a.push(aValue.value);
a.push(bExponent.value);
a.push(bValue.value);
// don't try to record microphone status
a.push(hue.value);
a.push(persistence.value);
a.push(0+disableFilter.checked);
a.push(0+hideGrid.checked);
return a;
},
setupControls : function()
{
var locationString = location.toString();
if (!(locationString.includes('#'))) {
var stored = window.localStorage.getItem('controls');
if (!stored)
return;
locationString = '#' + stored;
}
var hash = locationString.split('#')[1];
var arrayString = decodeURI(hash);
var a = arrayString.split(',');
this.setupSlider(mainGain, a.shift());
this.setupSlider(exposure, a.shift());
//this.setupSlider(audioVolume, a.shift());
this.setupSlider(audioVolume, "0");
this.setupCheckbox(swapXY, a.shift());
this.setupCheckbox(invertXY, a.shift());
this.setupCheckbox(light, a.shift());
this.setupCheckbox(sweepCheckbox, a.shift());
this.setupSlider(trigger, a.shift());
this.setupSlider(msDiv, a.shift());
this.setupCheckbox(generatorCheckbox, a.shift());
this.setupString(xInput, a.shift());
this.setupString(yInput, a.shift());
this.setupSlider(aExponent, a.shift());
this.setupSlider(aValue, a.shift());
this.setupSlider(bExponent, a.shift());
this.setupSlider(bValue, a.shift());
this.setupSlider(hue, a.shift());
this.setupSlider(persistence, a.shift());
this.setupCheckbox(disableFilter, a.shift());
this.setupCheckbox(hideGrid, a.shift());
UI.compile();
},
encodeString : function(s)
{
s=s.replace(/ /g,"");
s=s.replace(/,/g,";");
return s;
},
decodeString : function(s)
{
s=s.replace(/;/g,",");
//now sanitize
var toSpaces = s.replace(/[(),+*-/=<>|&!.%]/g, " ");
var toSpaces = toSpaces.replace(/[0-9]/g, " ");
var words = toSpaces.split(' ');
var allowed = ["", "a", "b", "t", "n", "x", "y", "mx", "my", "E", "PI", "abs", "acos",
"asin", "atan", "ceil", "cos", "exp", "floor", "log", "max", "min", "pow", "random",
"round", "sin", "sqrt", "tan"];
for (var i=0; i<words.length; i++)
{
var found = false;
for (var j=0; j<allowed.length; j++)
{
if (words[i] == allowed[j]) found = true;
}
if (found == false) s="bad expression";
}
return s;
},
setupSlider : function(slider, s)
{
slider.value = parseFloat(s);
slider.oninput();
},
setupCheckbox : function(checkbox, s)
{
checkbox.checked = parseInt(s);
checkbox.onchange();
},
setupString : function(inp, s)
{
inp.value = this.decodeString(s);
},
restoreDefaults : function()
{
var locationString = location.toString();
var site = locationString.split('#')[0];
location = site;
}
}
// autosave controls every second
setInterval(function () {
var text = Controls.getControlsArray().toString();
var hm = encodeURI(text);
localStorage.setItem('controls', hm);
}, 1000);
</script>
<div id="extraNotes" style="display:none">
</div>
<!-- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -->
</td>
</tr>
</tbody></table>
<script id="vertex" type="x-shader">
attribute vec2 vertexPosition;
void main()
{
gl_Position = vec4(vertexPosition, 0.0, 1.0);
}
</script>
<script id="fragment" type="x-shader">
precision highp float;
uniform vec4 colour;
void main()
{
gl_FragColor = colour;
}
</script>
<!-- The Gaussian line-drawing code, the next two shaders, is adapted
from woscope by m1el : https://github.com/m1el/woscope -->
<script id="gaussianVertex" type="x-shader">
#define EPS 1E-6
uniform float uInvert;
uniform float uSize;
uniform float uNEdges;
uniform float uFadeAmount;
uniform float uIntensity;
uniform float uGain;
attribute vec2 aStart, aEnd;
attribute float aIdx;
varying vec4 uvl;
varying vec2 vTexCoord;
varying float vLen;
varying float vSize;
void main () {
float tang;
vec2 current;
// All points in quad contain the same data:
// segment start point and segment end point.
// We determine point position using its index.
float idx = mod(aIdx,4.0);
// `dir` vector is storing the normalized difference
// between end and start
vec2 dir = (aEnd-aStart)*uGain;
uvl.z = length(dir);
if (uvl.z > EPS)
{
dir = dir / uvl.z;
vSize = 0.006/pow(uvl.z,0.08);
}
else
{
// If the segment is too short, just draw a square
dir = vec2(1.0, 0.0);
vSize = 0.006/pow(EPS,0.08);
}
vSize = uSize;
vec2 norm = vec2(-dir.y, dir.x);
if (idx >= 2.0) {
current = aEnd*uGain;
tang = 1.0;
uvl.x = -vSize;
} else {
current = aStart*uGain;
tang = -1.0;
uvl.x = uvl.z + vSize;
}
// `side` corresponds to shift to the "right" or "left"
float side = (mod(idx, 2.0)-0.5)*2.0;
uvl.y = side * vSize;
uvl.w = uIntensity*mix(1.0-uFadeAmount, 1.0, floor(aIdx / 4.0 + 0.5)/uNEdges);
vec4 pos = vec4((current+(tang*dir+norm*side)*vSize)*uInvert,0.0,1.0);
gl_Position = pos;
vTexCoord = 0.5*pos.xy+0.5;
//float seed = floor(aIdx/4.0);
//seed = mod(sin(seed*seed), 7.0);
//if (mod(seed/2.0, 1.0)<0.5) gl_Position = vec4(10.0);
}
</script>
<script id="gaussianFragment" type="x-shader">
#define EPS 1E-6
#define TAU 6.283185307179586
#define TAUR 2.5066282746310002
#define SQRT2 1.4142135623730951
precision highp float;
uniform float uSize;
uniform float uIntensity;
uniform sampler2D uScreen;
varying float vSize;
varying vec4 uvl;
varying vec2 vTexCoord;
// A standard gaussian function, used for weighting samples
float gaussian(float x, float sigma)
{
return exp(-(x * x) / (2.0 * sigma * sigma)) / (TAUR * sigma);
}
// This approximates the error function, needed for the gaussian integral
float erf(float x)
{
float s = sign(x), a = abs(x);
x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
x *= x;
return s - s / (x * x);
}
void main (void)
{
float len = uvl.z;
vec2 xy = uvl.xy;
float brightness;
float sigma = vSize/5.0;
if (len < EPS)
{
// If the beam segment is too short, just calculate intensity at the position.
brightness = gaussian(length(xy), sigma);
}
else
{
// Otherwise, use analytical integral for accumulated intensity.
brightness = erf(xy.x/SQRT2/sigma) - erf((xy.x-len)/SQRT2/sigma);
brightness *= exp(-xy.y*xy.y/(2.0*sigma*sigma))/2.0/len;
}
brightness *= uvl.w;
gl_FragColor = 2.0 * texture2D(uScreen, vTexCoord) * brightness;
gl_FragColor.a = 1.0;
}
</script>
<script id="texturedVertex" type="x-shader">
precision highp float;
attribute vec2 aPos;
varying vec2 vTexCoord;
void main (void)
{
gl_Position = vec4(aPos, 0.0, 1.0);
vTexCoord = (0.5*aPos+0.5);
}
</script>
<script id="texturedVertexWithResize" type="x-shader">
precision highp float;
attribute vec2 aPos;
varying vec2 vTexCoord;
uniform float uResizeForCanvas;
void main (void)
{
gl_Position = vec4(aPos, 0.0, 1.0);
vTexCoord = (0.5*aPos+0.5)*uResizeForCanvas;
}
</script>
<script id="texturedFragment" type="x-shader">
precision highp float;
uniform sampler2D uTexture0;
varying vec2 vTexCoord;
void main (void)
{
gl_FragColor = texture2D(uTexture0, vTexCoord);
gl_FragColor.a= 1.0;
}
</script>
<script id="blurFragment" type="x-shader">
precision highp float;
uniform sampler2D uTexture0;
uniform vec2 uOffset;
varying vec2 vTexCoord;
void main (void)
{
vec4 sum = vec4(0.0);
sum += texture2D(uTexture0, vTexCoord - uOffset*8.0) * 0.000078;
sum += texture2D(uTexture0, vTexCoord - uOffset*7.0) * 0.000489;
sum += texture2D(uTexture0, vTexCoord - uOffset*6.0) * 0.002403;
sum += texture2D(uTexture0, vTexCoord - uOffset*5.0) * 0.009245;
sum += texture2D(uTexture0, vTexCoord - uOffset*4.0) * 0.027835;
sum += texture2D(uTexture0, vTexCoord - uOffset*3.0) * 0.065592;
sum += texture2D(uTexture0, vTexCoord - uOffset*2.0) * 0.12098;
sum += texture2D(uTexture0, vTexCoord - uOffset*1.0) * 0.17467;
sum += texture2D(uTexture0, vTexCoord + uOffset*0.0) * 0.19742;
sum += texture2D(uTexture0, vTexCoord + uOffset*1.0) * 0.17467;
sum += texture2D(uTexture0, vTexCoord + uOffset*2.0) * 0.12098;
sum += texture2D(uTexture0, vTexCoord + uOffset*3.0) * 0.065592;
sum += texture2D(uTexture0, vTexCoord + uOffset*4.0) * 0.027835;
sum += texture2D(uTexture0, vTexCoord + uOffset*5.0) * 0.009245;
sum += texture2D(uTexture0, vTexCoord + uOffset*6.0) * 0.002403;
sum += texture2D(uTexture0, vTexCoord + uOffset*7.0) * 0.000489;
sum += texture2D(uTexture0, vTexCoord + uOffset*8.0) * 0.000078;
gl_FragColor = sum;
}
</script>
<script id="outputVertex" type="x-shader">
precision highp float;
attribute vec2 aPos;
varying vec2 vTexCoord;
varying vec2 vTexCoordCanvas;
uniform float uResizeForCanvas;
void main (void)
{
gl_Position = vec4(aPos, 0.0, 1.0);
vTexCoord = (0.5*aPos+0.5);
vTexCoordCanvas = vTexCoord*uResizeForCanvas;
}
</script>
<script id="outputFragment" type="x-shader">
precision highp float;
uniform sampler2D uTexture0; //line
uniform sampler2D uTexture1; //tight glow
uniform sampler2D uTexture2; //big glow
uniform sampler2D uTexture3; //screen
uniform float uExposure;
uniform float graticuleLight;
uniform vec3 uColour;
varying vec2 vTexCoord;
varying vec2 vTexCoordCanvas;
void main (void)
{
vec4 line = texture2D(uTexture0, vTexCoordCanvas);
// r components have grid; g components do not.
vec4 screen = texture2D(uTexture3, vTexCoord);
vec4 tightGlow = texture2D(uTexture1, vTexCoord);
vec4 scatter = texture2D(uTexture2, vTexCoord)+0.35;
float light = line.r + 1.5*screen.g*screen.g*tightGlow.r;
light += 0.4*scatter.g * (2.0+1.0*screen.g + 0.5*screen.r);
float tlight = 1.0-pow(2.0, -uExposure*light);
float tlight2 = tlight*tlight*tlight;
gl_FragColor.rgb = mix(uColour, vec3(1.0), 0.3+tlight2*tlight2*0.5)*tlight;
gl_FragColor.rgb = mix(gl_FragColor.rgb, (vec3(0.7)+0.3*uColour)*screen.b, graticuleLight);
//gl_FragColor.rgb += 0.4*(vec3(0.7)+0.3*uColour)*screen.b;
gl_FragColor.a= 1.0;
}
</script>
<script src="./oscilloscope.js"></script>
<script src="./inject-electron.js"></script>
</body></html>