/* global cv */ import { Canvas, Image, loadImage, ImageData } from 'skia-canvas'; // threejs内部使用了OffscreenCanvas //(globalThis as any).OffscreenCanvas = (globalThis as any).OffscreenCanvas || Canvas; globalThis.ImageData = ImageData; import createContext from 'gl'; import * as SHADER_SOURCE from '../../src/pages/playground/scripts/shaders'; //const cc = (a: T[][]): T[] => a.flat(1); // This is slower! const cc = (a: T[][]): T[] => { const result: T[] = []; for (const x of a) { for (const e of x) result.push(e); } return result; }; type RenderContext = ReturnType; class GLCanvas { ctx: RenderContext; _width: number = 256; _height: number = 192; resizeBuffer: number[]; constructor(context: RenderContext) { this.ctx = context; } get width() { return this._width; } set width(width: number) { this._width = width; const ext = this.ctx.getExtension('STACKGL_resize_drawingbuffer'); ext.resize(width, this.height); } get height() { return this._height; } set height(height: number) { this._height = height; const ext = this.ctx.getExtension('STACKGL_resize_drawingbuffer'); ext.resize(this.width, height); } /*// @ts-ignore getContext(type, options) { if (type === 'webgl') { this.ctx = createContext(200, 300, options); return this.ctx; } return null as WebGLRenderingContext; }*/ addEventListener(evt: 'webglcontextlost') {} async toBuffer() { const pixels = new Uint8Array(this.width * this.height * 4); this.ctx.readPixels(0, 0, this.width, this.height, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, pixels); const canvas = new Canvas(this.width, this.height); const ctx = canvas.getContext('2d'); ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels), this.width, this.height), 0, 0); return canvas.toBuffer('png'); } } interface GaugeRendererInitOptions { source: HTMLImageElement; gauge: HTMLImageElement; } const gl = createContext(512, 192, { antialias: true }); export default class GaugeRenderer { source: Image; // base64 string gauge: Image; canvas: GLCanvas; program: WebGLProgram; texture: WebGLTexture; pos: WebGLBuffer; uv: WebGLBuffer; ib: WebGLBuffer; primitiveCount: number; width: number = 256; height: number = 192; constructor(options: GaugeRendererInitOptions) { this.source = options.source; this.gauge = options.gauge; this.canvas = new GLCanvas(gl); gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT); gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); gl.getExtension('OES_element_index_uint'); // initial program this.program = gl.createProgram(); const vsShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vsShader, SHADER_SOURCE.vs); gl.compileShader(vsShader); const logVs = gl.getShaderInfoLog(vsShader); logVs && console.warn('vs log:', logVs); const fsShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fsShader, SHADER_SOURCE.fs); gl.compileShader(fsShader); const logFs = gl.getShaderInfoLog(fsShader); logFs && console.warn('fs log:', logFs); gl.attachShader(this.program, vsShader); gl.attachShader(this.program, fsShader); gl.linkProgram(this.program); const logProgram = gl.getProgramInfoLog(this.program); logProgram && console.warn('program log:', logProgram); gl.deleteShader(vsShader); gl.deleteShader(fsShader); const { name: nameModelView } = gl.getActiveUniform(this.program, 0); const modelMat = gl.getUniformLocation(this.program, nameModelView); const { name: nameProj } = gl.getActiveUniform(this.program, 1); const projMat = gl.getUniformLocation(this.program, nameProj); const { name: nameUV } = gl.getActiveUniform(this.program, 2); const uvMat = gl.getUniformLocation(this.program, nameUV); const { name: nameDiffuse } = gl.getActiveUniform(this.program, 3); const diffuse = gl.getUniformLocation(this.program, nameDiffuse); const { name: nameOpacity } = gl.getActiveUniform(this.program, 4); const opacity = gl.getUniformLocation(this.program, nameOpacity); const { name: nameMap } = gl.getActiveUniform(this.program, 5); const map = gl.getUniformLocation(this.program, nameMap); gl.useProgram(this.program); gl.uniformMatrix4fv( projMat, false, //new Float32Array([0.0026385225355625153, 0, 0, 0, 0, -0.010416666977107525, 0, 0, 0, 0, -0.20202019810676575, 0, 0, 0, -1.0202020406723022, 1]) new Float32Array([0.002739726100116968, 0, 0, 0, 0, 0.010416666977107525, 0, 0, 0, 0, -0.20202019810676575, 0, 0, 0, -1.0202020406723022, 1]) ); gl.uniformMatrix4fv(modelMat, false, new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -1, 1])); gl.uniformMatrix3fv(uvMat, false, new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1])); gl.uniform3f(diffuse, 1, 1, 1); gl.uniform1f(opacity, 1); gl.uniform1i(map, 0); // texture this.texture = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.pixelStorei(37440, true); gl.pixelStorei(37441, false); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4); gl.pixelStorei(37443, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 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.disable(gl.CULL_FACE); gl.depthMask(true); gl.colorMask(true, true, true, true); gl.disable(gl.STENCIL_TEST); gl.disable(gl.POLYGON_OFFSET_FILL); gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE); // buffers this.pos = gl.createBuffer(); this.uv = gl.createBuffer(); this.ib = gl.createBuffer(); const iPos = gl.getAttribLocation(this.program, 'position'); const iUV = gl.getAttribLocation(this.program, 'uv'); //console.log('indices:', iPos, iUV); gl.enableVertexAttribArray(iPos); gl.bindBuffer(gl.ARRAY_BUFFER, this.pos); gl.vertexAttribPointer(iPos, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(iUV); gl.bindBuffer(gl.ARRAY_BUFFER, this.uv); gl.vertexAttribPointer(iUV, 2, gl.FLOAT, false, 0, 0); } updateMaterial({ width = null, sw = this.width, sh = this.height } = {}) { if (sw !== this.width || sh !== this.height) { if (Number.isFinite(width)) { this.width = width; } else { this.width = Math.round((this.height * sw) / sh); } this.canvas.width = this.width; this.canvas.height = this.height; gl.viewport(0, 0, this.width, this.height); const projMat = gl.getUniformLocation(this.program, 'projectionMatrix'); gl.uniformMatrix4fv( projMat, false, new Float32Array([2 / this.width, 0, 0, 0, 0, 2 / this.height, 0, 0, 0, 0, -0.20202019810676575, 0, 0, 0, -1.0202020406723022, 1]) ); } // image to canvas const sourceCanvas = new Canvas(this.source.width, this.source.height); sourceCanvas.getContext('2d').drawImage(this.source, 0, 0); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sourceCanvas as any); gl.generateMipmap(gl.TEXTURE_2D); } updateGeometry(baseY = null) { const { width, height } = this.gauge; const canvas = new Canvas(width, height); const ctx = canvas.getContext('2d'); ctx.drawImage(this.gauge, 0, 0); const { data: buffer } = ctx.getImageData(0, 0, width, height); const xFactor = this.width / width; baseY = Math.round(Number.isFinite(baseY) ? baseY : height / 2); baseY = Math.max(0, Math.min(height - 1, baseY)); const propertyArray = Array(height) .fill(null) .map((_, y) => Array(width) .fill(null) .map((_, x) => ({ uv: [(x + 0.5) / width, 1 - (y + 0.5) / height], position: [(x - width / 2) * xFactor, (buffer[(y * width + x) * 4] + buffer[(y * width + x) * 4 + 2] / 256 - 128) / xFactor, 0], })) ); // integral X by K for (let y = baseY; y > 0; --y) { for (let x = 0; x < width; ++x) propertyArray[y - 1][x].position[0] = propertyArray[y][x].position[0] - ((buffer[(y * width + x) * 4 + 1] - 128) * xFactor) / 127; } for (let y = baseY + 1; y < height; ++y) { for (let x = 0; x < width; ++x) propertyArray[y][x].position[0] = propertyArray[y - 1][x].position[0] + ((buffer[((y - 1) * width + x) * 4 + 1] - 128) * xFactor) / 127; } const uvs = cc(cc(propertyArray).map((p) => p.uv)); const positions = cc(cc(propertyArray).map((p) => p.position)); const faces = Array(height - 1) .fill(null) .map((_, y) => Array(width - 1) .fill(null) .map((_, x) => [y * width + x, y * width + x + 1, (y + 1) * width + x, (y + 1) * width + x, (y + 1) * width + x + 1, y * width + x + 1]) ); const indices = cc(cc(faces)); gl.bindBuffer(gl.ARRAY_BUFFER, this.pos); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, this.uv); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvs), gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ib); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW); this.primitiveCount = indices.length; } render() { gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); //gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ib); gl.drawElements(gl.TRIANGLES, this.primitiveCount, gl.UNSIGNED_INT, 0); return this.canvas.toBuffer(); } dispose() { gl.deleteBuffer(this.pos); gl.deleteBuffer(this.uv); gl.deleteBuffer(this.ib); gl.deleteProgram(this.program); gl.deleteTexture(this.texture); } } const gaugeRenderer = new GaugeRenderer({ source: new Image(), gauge: new Image(), }); export const renderGaugeImage = async (sourceURL: string | Buffer, gaugeURL: string | Buffer, baseY?: number) => { const source = await loadImage(sourceURL); const gauge = await loadImage(gaugeURL); gaugeRenderer.source = source; gaugeRenderer.gauge = gauge; gaugeRenderer.updateMaterial({ width: gauge.width, sw: source.width, sh: source.height, }); gaugeRenderer.updateGeometry(baseY); console.log(process.memoryUsage().heapUsed); return { buffer: await gaugeRenderer.render(), size: { width: gaugeRenderer.width, height: gaugeRenderer.height, }, }; }; // renderGaugeImage('./images/source.png', './images/gauge.png');