import { Canvas, loadImage } from 'skia-canvas'; import * as THREE from './three/Three'; import gl from 'gl'; // threejs内部使用了OffscreenCanvas (globalThis as any).OffscreenCanvas = (globalThis as any).OffscreenCanvas || Canvas; const cc = (a: T[][]): T[] => { const result: T[] = []; for (const x of a) { for (const e of x) result.push(e); } return result; }; class GLCanvas { ctx: ReturnType; width: number = 256; height: number = 192; resizeBuffer: number[]; constructor(width: number, height: number) { this.width = width; this.height = height; } getContext(type, options) { if (type === 'webgl') { this.ctx = this.ctx || gl(this.width, this.height, options); return this.ctx; } return null as WebGLRenderingContext; } addEventListener(evt: 'webglcontextlost') {} removeEventListener(evt: any) {} 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({ data: new Uint8ClampedArray(pixels), width: this.width, height: this.height }, 0, 0); return canvas.toBuffer('png'); } } export const renderGaugeImage = async (sourceURL: string | Buffer, gaugeURL: string | Buffer, baseY: number) => { const source = await loadImage(sourceURL); const gauge = await loadImage(gaugeURL); const { width: gaugeWidth, height: gaugeHeight } = gauge; const { width: srcWidth, height: srcHeight } = source; let width: number = 256; let height: number = 192; if (width !== srcWidth || height !== srcHeight) { if (Number.isFinite(gaugeWidth)) { width = gaugeWidth; } else { width = Math.round((height * srcWidth) / srcHeight); } } const camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0.1, 10); camera.position.set(0, 0, 1); camera.up.set(0, 1, 0); camera.lookAt(0, 0, 0); const ctx = new Canvas(gaugeWidth, gaugeHeight).getContext('2d'); ctx.drawImage(gauge, 0, 0); const { data: buff } = ctx.getImageData(0, 0, gaugeWidth, gaugeHeight); const xFactor = width / gaugeWidth; baseY = Math.round(Number.isFinite(baseY) ? baseY : gaugeHeight / 2); baseY = Math.max(0, Math.min(gaugeHeight - 1, baseY)); const propertyArray = Array(gaugeHeight) .fill(null) .map((_, y) => Array(gaugeWidth) .fill(null) .map((_, x) => ({ uv: [(x + 0.5) / gaugeWidth, 1 - (y + 0.5) / gaugeHeight], position: [(x - gaugeWidth / 2) * xFactor, (buff[(y * gaugeWidth + x) * 4] + buff[(y * gaugeWidth + x) * 4 + 2] / 256 - 128) / xFactor, 0], })) ); // integral X by K for (let y = baseY; y > 0; --y) { for (let x = 0; x < gaugeWidth; ++x) propertyArray[y - 1][x].position[0] = propertyArray[y][x].position[0] - ((buff[(y * gaugeWidth + x) * 4 + 1] - 128) * xFactor) / 127; } for (let y = baseY + 1; y < gaugeHeight; ++y) { for (let x = 0; x < gaugeWidth; ++x) propertyArray[y][x].position[0] = propertyArray[y - 1][x].position[0] + ((buff[((y - 1) * gaugeWidth + 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 geometry = new THREE.BufferGeometry(); geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); const faces = Array(gaugeHeight - 1) .fill(null) .map((_, y) => Array(gaugeWidth - 1) .fill(null) .map((_, x) => [ y * gaugeWidth + x, y * gaugeWidth + x + 1, (y + 1) * gaugeWidth + x, (y + 1) * gaugeWidth + x, (y + 1) * gaugeWidth + x + 1, y * gaugeWidth + x + 1, ]) ); const indices = cc(cc(faces)); geometry.setIndex(indices); const material = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide, map: new THREE.Texture(source), }); material.map.needsUpdate = true; material.needsUpdate = true; const scene = new THREE.Scene(); const plane = new THREE.Mesh(geometry, material); scene.add(plane); const glCanvas = new GLCanvas(width, height); const renderer = new THREE.WebGLRenderer({ antialias: true, canvas: glCanvas as any, alpha: false, }); renderer.render(scene, camera); renderer.dispose(); return { buffer: await glCanvas.toBuffer(), size: { width, height, }, }; };