2020年8月6日 星期四

使用 Javascript 用 webGL 畫線

參考資料:   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>

沒有留言:

張貼留言

使用 pcie 轉接器連接 nvme SSD

之前 AM4 主機板使用 pcie ssd, 但主機板故障了沒辦法上網, 只好翻出以前買的 FM2 舊主機板, 想辦法讓老主機復活, 但舊主機板沒有 nvme 的界面, 因此上網買了 pcie 轉接器用來連接 nvme ssd, 遺憾的是 grub2 bootloader 無法識...