參考資料: https://www.tutorialspoint.com/webgl/index.htm
<html><head><meta charset='utf-8'>
<script>
class PixelRGBA { // float32[4] RGBA, 1 pixel color
constructor(r, g, b, α = 1) { this.color = new Float32Array([r, g, b, α]); }
}
class ColorLine extends PixelRGBA {// default α = 1
constructor(nXnY = 512, r = 8, g = 0, b = 0, isSolid = true, aspect = 1.0) {
super(r, g, b);
this.pixels = nXnY;// , nX = nY = nXnY, number of X, Y
this.isSolid= isSolid;
this.visible = true;
this.lineID = 0;// ID allocated by ScopeWebGL addLine()
this.eXoY = new Float32Array(nXnY << 1);// even X, odd Y for (x, y)
this.viewStart = 0;
this.η = 0;// 0 <= η < this.pixels
// keep const λ = 2 = N * Δx, so that x begin with -1 to be in range [-1, 1]
const λ = 2.0;// x range [-1, 1], max length = 2, to be a float number
const N = isSolid ? nXnY : nXnY >> 3;// line pixels,to be a interger number
const Δx = λ / N;// x interval, will be a float number
const Δy = aspect * Δx;// y dependent on x for vertical/horizontal line
for (let i = 0, n = 0, x = -1.0, y = -aspect; i < N; i ++, n += 2) {
this.eXoY[n] = x; // even: x coordinate [-1, 1], Δx = 2 ÷ nXnY
this.eXoY[n + 1] = y;// odd: ycoordinate [-1, 1],Δy = aspect * Δx
x += Δx;
y += Δy;
}// line.eXoY is Float32Array, 4 bytes/float, 8 bytes per (x,y)
this.vertical = (x) => { // set vertical line @x
for (let i = 0, n = 0; i < this.pixels; i ++, n += 2) {
this.eXoY[n] = x;// todo: validate
}
return this;// to be chain this member
}
this.horizon = (y) => { // set horizon line@y
for (let i = 0, n = 1; i < this.pixels; i ++, n += 2) {
this.eXoY[n] = y;// todo: validate
}
return this;// to be chain this member
}
this.injectFrame = (data) => { // to inject a frame of data at tail
if (data && (data.length > 0)) {
let moveEnd = this.pixels - data.length;
if (moveEnd < 0) moveEnd = 0;
const start = data.length * 2;
for (let i = 0, n = 1; i < moveEnd; i ++, n += 2) {
this.eXoY[n] = this.eXoY[start + n];// only copy y at odd
}
for (let i = 0, n = (moveEnd * 2) + 1; i < data.length; i ++, n += 2){
this.eXoY[n] = data[i];// only copy y at odd
}
}
return this;// to be chain this member
}
}
// amplitude(n, y) { this.eXoY[n * 2 + 1] = y; }
get y() {
let n = this.η + this.viewStart ;
if (n >= this.pixels) n = this.pixels - 1;
return this.eXoY[(n << 1) + 1];
} // read only
/*set y(tempy) {// write only
let n = this.η + this.viewStart ;
if (0 > n || n >= this.pixels) return;
this.eXoY[(n << 1) + 1] = tempy;
}*/
get position() { return this.η - (this.pixels >> 1); } // read only, refer to center point
set position(n){ // to set position @n sample, 0<= n < pixels
if(n >= this.pixels) n = this.pixels - 1;// saturation
else if(n < 0) n = 0;
this.η = n;// 0<= η < pixels
} // write only
get x() { return this.eXoY[this.η << 1]; }// read only
set x(tempx){// to set position @x [-1.0<= x <=1.0] mapto [0 - pixels]
let n = Math.floor((tempx + 1) * this.pixels) >> 1;// level shift
this.position = n;
} // write only
}
class ScopeLineGL {// https://www.tutorialspoint.com/webgl/index.htm
constructor() {
const gpuVertexSource =`
uniform mat2 scaling;
attribute vec2 coordinates;
uniform vec2 translation;
void main(void) {
gl_Position = vec4(scaling * coordinates + translation, 0, 1.0);
}
`;
const gpuShaderSource =`
precision mediump float;
uniform highp vec4 color;
void main(void) {
gl_FragColor = color;
}
`;
//const filterGain = document.createElement('span');// to output text
const yscaleInput = document.createElement('input');// to input gain value
const xcursorInput= document.createElement('input');// to input level trigger value
const ycursorInput= document.createElement('input');// to input level trigger value
const xscaleInput = document.createElement('input');// to input level trigger value
const spanBreak = document.createElement('br'); // spanBreak.cloneNode(true));
const divScope = document.createElement('div');// to group following elements
const canvas = document.createElement('canvas');// to display oscilloscope
const coordinatex = document.getElementById('coordinatex');
const edgeToggle = document.getElementById('edgeToggle');
const xcursorToggle= document.getElementById('xcursorToggle');
const ycursorToggle= document.getElementById('ycursorToggle');
this.resetToDefault= document.getElementById('resetToDefault');
document.body.style='background-color:black;';
document.body.append(yscaleInput);
document.body.append(spanBreak);
document.body.append(xcursorInput);
document.body.append(divScope);
divScope.append(ycursorInput);
divScope.append(spanBreak.cloneNode());
divScope.append(canvas);
divScope.append(spanBreak.cloneNode());
divScope.append(xscaleInput);
const pixelRatio = window.devicePixelRatio || 1;
this.width = 2048;
this.height= 256;
this.aspectratio = this.width / this.height;
canvas.width = Math.round(this.width * pixelRatio);// pixels@foreground, to fix center point?
canvas.height= Math.round(this.height* pixelRatio);// canvas.getContext('2d');
const webgl = canvas.getContext('webgl', {antialias: true, transparent: false});//'experimental-webgl'
webgl.enable(webgl.DEPTH_TEST);
webgl.clear(webgl.COLOR_BUFFER_BIT || webgl.DEPTH_BUFFER_BIT);
webgl.viewport(0, 0, canvas.width, canvas.height);
const gpuProgram = webgl.createProgram();
const vertexCode = webgl.createShader(webgl.VERTEX_SHADER);
const fragmentCode = webgl.createShader(webgl.FRAGMENT_SHADER);
webgl.shaderSource(vertexCode, gpuVertexSource);
webgl.shaderSource(fragmentCode, gpuShaderSource);
webgl.compileShader(vertexCode);
webgl.compileShader(fragmentCode);
webgl.attachShader(gpuProgram, vertexCode);
webgl.attachShader(gpuProgram, fragmentCode);
webgl.linkProgram(gpuProgram);
this.clear = () => webgl.clear(webgl.COLOR_BUFFER_BIT || webgl.DEPTH_BUFFER_BIT);
this.lines = [];// to store all lines, todo: use map { } , not array []
this.lineID = 0;// serial number to be allocated
this.clientWidth = canvas.width; // scaled by drag
this.clientHeight = canvas.height;// scaled by drag
this.disableLine = (id) => { // mark as invisible
if(id < this.lineID) this.lines[id].visible = false;
return this;
}
this.addLine = (line) => {
line.lineID = this.lineID ++;
this.lines.push(line);
return line.lineID;
}
this.addLine(new ColorLine(this.width, 10, 10, 10).horizon(0));// solid grey horizontal line at x=0
this.addLine(new ColorLine(this.height, 10, 10, 10, true, this.aspectratio).vertical(0));// solid grey vertical line at y=0
this.triggerLineID1 = this.addLine(new ColorLine(this.width, 10, 0, 0, false));// dash red trigger line;
this.triggerLineID2 = this.addLine(new ColorLine(this.width, 10, 0, 0, false));// dash red trigger line;
this.timeLineID1 = this.addLine(new ColorLine(this.height, 0, 10, 0, true, this.aspectratio));// solid green time line;
this.timeLineID2 = this.addLine(new ColorLine(this.height, 0, 10, 0, true, this.aspectratio));// solid green time line;
this.scopeLineID = this.addLine(new ColorLine(this.width, 10, 10, 0));// slop = 1, solid yellow line
const abs = (v) => v < 0 ? -v : v;// 取絕對值
const round = (f, d = 2) => {// default 小數 2 位
let dot = 10 ** d;
return Math.floor(f * dot + 0.5) / dot;
}
const tText = (uS) => abs(uS) >= 1e6 ? `${round(uS / 1e6)}sec` :
abs(uS) >= 1e3 ? `${round(uS / 1e3)} ms` :
`${round(uS, 0)} uS`;// 單位四捨五入, 小數點 2 位
const reportCoordinate = () => {
const scopeLine = this.lines[this.scopeLineID];
const fs = this.sampleRate;
const Δx = abs(this.lines[this.timeLineID1].eXoY[0] - this.lines[this.timeLineID2].eXoY[0]);
const Δy = abs(this.lines[this.triggerLineID1].eXoY[1] - this.lines[this.triggerLineID2].eXoY[1]);
const tΔx = round(scopeLine.pixels * Δx * 5e5 / fs, 0); // uS, [-1, 1] ↕↔
const ΔHz = (tΔx == 0) ? 'Hz' : `${round(1e6 / tΔx, 0)} Hz`;
const n = scopeLine.position;// refer to O, may be a negative number
const nΔt = n * 1e6 / fs;// uS unit
let y = round(scopeLine.y);
if (y > 0) y = `+${y}`;
coordinatey.innerHTML =`<span>↕@y= ${round(this.triggerLevel)} v, Δy= ${round(Δy)} v</span>`;
coordinatex.innerHTML =`<span'>↔@fs= ${fs} Hz, y= ${y} v<br>n= ${n}, t= ${tText(nΔt)}<br>Δt= ${ΔHz}<sup>-1</sup> = ${tText(tΔx)}</span>`;
}
const scaleTable = [16, 8, 4, 2, 1, 1.0/2, 1.0/4, 1.0/8, 1.0/16];
yscaleInput.type = 'range';
yscaleInput.min = 0;
yscaleInput.max = scaleTable.length - 1;
xscaleInput.type = 'range';// rotate 180 degreen
xscaleInput.min = 0;
xscaleInput.max = scaleTable.length - 1;
ycursorInput.type = 'range';// rotate 90 degreen
xcursorInput.type = 'range';
// to calculate global offset
const ox0 = yscaleInput.clientWidth;// pixels
const oy0 = ycursorInput.clientHeight;// pixels
const oTranslate = `translate(0, ${oy0 - ox0}px)`;
const rotate90deg= 'transform-origin:0 100%; transform: rotate(90deg)';//clockwise
const rotate180deg='transform-origin:50% 50%; transform: rotate(180deg)';
xcursorInput.style= `width:${this.clientWidth}; ${rotate180deg} translate(${-ox0}px, 0)`;
ycursorInput.style= `width:${this.clientHeight}; ${rotate90deg} ${oTranslate};`;
yscaleInput.style= rotate90deg;
xscaleInput.style= rotate180deg;
const xoffset = ox0 * 2 / this.clientWidth; // Δx = 2.0 ÷ nX;
const yoffset = 0;
this.xcursorID = false;
this.ycursorID = false;
canvas.style.border= '1px solid';// 外框 1 點
// canvas.style= `transform-origin:0% 0%; transform: translate(${ox0}px, 0)`;
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.useProgram(gpuProgram);
const gpuColor = webgl.getUniformLocation(gpuProgram, 'color');
const gpuScaling = webgl.getUniformLocation(gpuProgram,'scaling');
const coordinates = webgl.getAttribLocation(gpuProgram,'coordinates');
const gpuTranslate= webgl.getUniformLocation(gpuProgram,'translation');
webgl.uniform2fv(gpuTranslate, new Float32Array([xoffset, yoffset]));
webgl.vertexAttribPointer(coordinates, 2, webgl.FLOAT, false, 0, 0);
webgl.enableVertexAttribArray(coordinates);
const xyDraw = (eXoY, pixels, color = PixelRGBA(10,10,10), isSolid = true) => {
if (line) { // lambda can bind this & webgl to useProgram ;// webgl.useProgram(gpuProgram);
webgl.uniform4fv(gpuColor, color);// transfer color to GPU
webgl.uniformMatrix2fv(gpuScaling, false, new Float32Array([
this.xscale, 0,
0, this.yscale
])); // transfer a matrix for global scaler to GPU
webgl.bufferData(webgl.ARRAY_BUFFER, eXoY, webgl.STREAM_DRAW);// stream the position (x,y)
webgl.drawArrays(isSolid ? webgl.LINE_STRIP : webgl.LINES, 0, pixels);// line render
// webgl.drawArrays(isSolid ? webgl.POINTS : webgl.LINES, 0, viewPixels);
}
}
this.draw = (line, eXoY = line.eXoY, pixels = line.pixels) =>
xyDraw(eXoY, pixels, line.color, line.isSolid);
edgeToggle.onclick = () => {
this.positiveTrigger = ! this.positiveTrigger;// toggle
edgeToggle.innerHTML = `${this.positiveTrigger ? '↑正緣':'↓負緣'}觸發`;
}
// cursor y to capture position as triggerLevel and show horizontal line
ycursorInput.oninput = () => { // direction inverse
// console.log(ycursorInput.value);
const temp = - ycursorInput.value * 2 / this.height;// mirror and scale down to [-1, 1]
this.triggerLevel = temp;
this.lines[this.ycursorID ? this.triggerLineID1 : this.triggerLineID2].horizon(temp);
reportCoordinate();// todo: report y only
}
// cursor x to capture position as time@x and show vertical line
xcursorInput.oninput = () => { // direction inverse
// console.log(xcursorInput.value);
const temp = - xcursorInput.value * 2 / this.width;// mirror and scale down to [-1, 1]
this.lines[this.scopeLineID].x = temp;
this.lines[this.xcursorID ? this.timeLineID1 : this.timeLineID2].vertical(temp);
reportCoordinate();// todo: report x only
}
// toggle y cursor ID
ycursorToggle.onclick = () => {
this.ycursorID = ! this.ycursorID;// toggle
ycursorInput.oninput();// to sync with y-cursor
}
// toggle xcursor ID
xcursorToggle.onclick = () => {
this.xcursorID = ! this.xcursorID;// toggle
xcursorInput.oninput();// to sync with x-cursor
}
// amplitude scale
yscaleInput.oninput = () => {// inverse by scaleTable
this.yscale = scaleTable[yscaleInput.value];
const temp = 1.0 / this.yscale;// to syn with _gpuYscale
ycursorInput.step = temp;
ycursorInput.min = -temp * this.height / 2;// Δy = 2/this.height
ycursorInput.max = temp * this.height / 2;
ycursorInput.oninput();// to sync with y-cursor
};
// time scale
xscaleInput.oninput = () => {// inverse by scaleTable
this.xscale = scaleTable[xscaleInput.value];
const temp = 1.0 / this.xscale;// to syn with _gpuXscale
xcursorInput.step = temp;
xcursorInput.min = -temp * this.width / 2;// Δx = 2/this.width
xcursorInput.max = temp * this.width / 2;
xcursorInput.oninput(); // to sync with x-cursor
}
this.resetToDefault.onclick = () => {
yscaleInput.value = 5; // 0.5X => use table to invese direction
xscaleInput.value = 4; // 1X => use table to invese direction
ycursorInput.value= 0;// +:down, 0:original@center, -:up
xcursorInput.value= 0;// 1:left, 0:original@center, -1:right
this.positiveTrigger = false;
this.triggerLevel = 0.0;
this.lines[this.triggerLineID1].horizon(0);
this.lines[this.triggerLineID2].horizon(0);
this.lines[this.timeLineID1].vertical(0);
this.lines[this.timeLineID2].vertical(0);
this.lines[this.scopeLineID].viewStart = 0;
edgeToggle.onclick();
yscaleInput.oninput();
xscaleInput.oninput();
}
this.xcursorInput = xcursorInput;// to update cursor infomation realtime
this.resetToDefault.onclick();
}// end of constructor
get render () { // getter can bind this
this.lines.forEach( (line) => { if (line.visible) {
if(line.lineID == this.scopeLineID) { // scopeLine need to be trigger
const level = this.triggerLevel;
const positive = this.positiveTrigger;
const halfPoints = line.pixels >> 1; // begin from center point
let n = halfPoints * 2 + 1;// index of center y
let py = line.eXoY[n];// store first point
n += 2; // go ahead next point, to skip x coordinate
for(let i = 1; i < halfPoints; i ++, n += 2) {
let y = line.eXoY[n];// trigger seraching
if (positive && y > py &&
py < level && level <= y) {
line.viewStart = i; // positive trigger
break;
} else if (! positive && y < py &&
py > level && level >= y) {
line.viewStart = i; // negative trigger
break;
}
py = y; // keep previously value to check edge
}
}
if(line.viewStart == 0) this.draw(line);
else {
const eXoY = new Float32Array(line.eXoY);//to keep same x coordinate
const pixels = line.pixels - line.viewStart;// pixels to move
const indexShift= line.viewStart << 1; // index shift
let n = 1;// to update y coordinate only, it begins at 1
for(let i = 0; i < pixels; i ++, n += 2) {
eXoY[n] = line.eXoY[n + indexShift];
}
this.draw(line, eXoY, pixels);// pixels has been shrink
// for(let i = pixels; i < line.pixels; i ++, n += 2) eXoY[n] = 0;
// this.draw(line, eXoY);
}
}});
}
set triggerLevel(level) {
if(-1<= level && level <=1) this._triggerLevel = level;
}
get triggerLevel() { return this._triggerLevel || 0.0; } // to prevent Null Pointer Exception
get sampleRate() { return this._sampleRate || 1; } // to prevent Null Pointer Exception
set sampleRate (fs) {// fs per second
if(fs >= 1) {
this._sampleRate = fs;
this.resetToDefault.onclick();
this.render;
}
}
}
</script>
</head>
<body>
<button id='resetToDefault' style="float: left;">重設 Reset</button>
<button id='ycursorToggle' style="float: right; color: red">標記相對振幅 y 起始點</button>
<div id='coordinatey' style='font-size: 48px; text-align:left; color:red'>coordinatey</div>
<button id='xcursorToggle' style="float: right; color:green">標記相對時間 t 起始點</button>
<button id='edgeToggle' style="float: left;">↑ 邊緣觸發</button>
<div id='coordinatex' style='font-size: 48px; text-align:left; color:green'>coordinatex</div>
<script>
const oscScope= new ScopeLineGL();
const line = oscScope.lines[oscScope.scopeLineID];
const data = new Float32Array(128);
const randomSingal = () => {
for(let i = 0; i < data.length ; i ++) {
data[i] = 2 * Math.random() - 1;
}
line.injectFrame(data);
oscScope.render;
requestAnimationFrame(randomSingal);
}
randomSingal();
</script>
</body>
</html>
2020年8月6日 星期四
使用 Javascript 用 webGL 畫線
訂閱:
張貼留言 (Atom)
Linux mint 玩 waydroid 一些心得
1. 目前使用 linux mint 22.1 作業系統可以順利跑起來, 可上官網去下載, 並安裝到硬碟. 2. 安裝 waydroid 可上網站 https://docs.waydro.id 參考看看: https://docs.waydro.id/usage/inst...
-
1. 上 Firebase 網站 註冊啟用 Firebase : https://firebase.google.com/ 2. 進入 Firebase Console 註冊開啟新專案 : https://console.firebase.google.com/...
-
Flutter 讓人很容易短時間就能開發出 app, 關鍵在於他開發了很多種小部件稱為 Widget, 將Widget 組合起來就是一個 app. 所謂 部 件(Widget)就是一個可以呈現在螢幕上的視覺系 物 件,幾乎可以說 Flutter 每個物件都是 部 件. 開發者透...
沒有留言:
張貼留言