import { Rect } from "./2d"; import { Animator, Interpolator } from "./Animator"; import { Gesture } from "./Gesture"; import { m4 } from "./m4"; export interface Disposable { dispose(): void; } export interface Layer { preDraw(): void; draw(): void; tap(cx: number, cy: number, sx: number, sy: number): void; scale(scale: number): void; dispose(): void; } export class LayerAB implements Layer { preDraw(): void {} draw(): void {} tap(cx: number, cy: number, sx: number, sy: number): void {} scale(scale: number): void {} dispose(): void {} } export class Padding { constructor( readonly left: number, readonly top: number, readonly right: number, readonly bottom: number, ) {} equals(other: Padding) { return ( this.left == other.left && this.top == other.top && this.right == other.right && this.bottom == other.bottom ); } } export class Scene implements Disposable { layers: Array = []; testLayers: Array = []; animators: Array = []; userMat: m4.Matrix4 = m4.identity(); bestFitMat: m4.Matrix4 = m4.identity(); projectionMat: m4.Matrix4 = m4.identity(); resultMat: m4.Matrix4 = m4.identity(); width: number = 0; height: number = 0; contentWidth: number = 0; contentHeight: number = 0; padding = new Padding(0, 0, 0, 0); pendingDraw = false; // clearColorValue: [number, number, number, number] = [1, 1, 1, 1]; // 默认白色 clearColorValue: [number, number, number, number] = [0, 0, 0, 0]; // 默认透明 /** 游戏完成后调用,将 canvas 底色改为透明,让 body 渐变色透出 */ setClearTransparent() { this.clearColorValue = [0, 0, 0, 0]; this.invalidate(); } constructor( readonly gl: WebGL2RenderingContext, public readonly ratio: number, public animationFrameProvider: AnimationFrameProvider = window, public interact: boolean = true, ) { this.updateViewport(); let self = this; if (window && interact) { new Gesture(gl.canvas as HTMLElement, { // drag: self.drag.bind(self), // zoom: self.scaleAt.bind(self), tap: self.tap.bind(self), }); } m4.identity(); } updateViewport() { console.log("viewport update."); const gl = this.gl; const canvas = gl.canvas as HTMLCanvasElement; //canvas.width = canvas.clientWidth * this.ratio //canvas.height = canvas.clientHeight * this.ratio this.invalidate(); if (canvas.width != this.width || canvas.height != this.height) { this.width = canvas.width; this.height = canvas.height; m4.projection(this.width, this.height, this.projectionMat); this.updateBestFit(); this.updateResultMat(); } } private isBestMatSet: boolean = false; private updateBestFit() { if (this.contentWidth == 0 || this.width == 0) return; const box = new Rect( this.padding.left, this.padding.top, this.width - this.padding.right - this.padding.left, this.height - this.padding.top - this.padding.bottom, ); const scale = Math.min( box.width / this.contentWidth, box.height / this.contentHeight, ); const tx = box.center.x - (this.contentWidth * scale) / 2; const ty = box.center.y - (this.contentHeight * scale) / 2; m4.identity(this.bestFitMat); this.bestFitMat[0] = scale; this.bestFitMat[5] = scale; this.bestFitMat[12] = tx; this.bestFitMat[13] = ty; if (!this.isBestMatSet) { m4.copy(this.bestFitMat, this.userMat); this.invalidate(); this.isBestMatSet = true; } } updateResultMat() { if (this.contentWidth == 0 || this.width == 0) return; const box = new Rect( this.padding.left, this.padding.top, this.width - this.padding.right - this.padding.left, this.height - this.padding.top - this.padding.bottom, ); // const scale = Math.min(box.width / this.contentWidth, box.height / this.contentHeight) // const tx = box.center.x - this.contentWidth * scale / 2 // const ty = box.center.y - this.contentHeight * scale / 2 // m4.identity(this.resultMat) // this.resultMat[0] = scale // this.resultMat[5] = scale // this.resultMat[12] = tx // this.resultMat[13] = ty let rate = this.width > this.height ? 0.7 : 0.8; let scaleX = (this.width * rate) / this.contentWidth; let scaleY = (this.height * rate) / this.contentHeight; let scale = Math.min(scaleX, scaleY); const tx = box.center.x - (this.contentWidth * scale) / 2; const ty = box.center.y - (this.contentHeight * scale) / 2; m4.identity(this.resultMat); this.resultMat[0] = scale; this.resultMat[5] = scale; this.resultMat[12] = tx; this.resultMat[13] = ty; } setContentPadding(padding: Padding) { if (!this.padding.equals(padding)) { this.padding = padding; this.updateBestFit(); this.updateResultMat(); } } setContentSize(width: number, height: number) { if (width != this.contentWidth || height != this.contentWidth) { this.contentWidth = width; this.contentHeight = height; this.updateBestFit(); this.updateResultMat(); } } addLayer(layer: Layer) { this.layers.push(layer); this.invalidate(); } invalidate() { if (this.pendingDraw) return; this.pendingDraw = true; //requestAnimationFrame(() => { this.animationFrameProvider.requestAnimationFrame(() => { this.draw(); }); } draw() { this.animators.forEach((a) => a.update()); this.layers.forEach((l) => l.preDraw()); const gl = this.gl; gl.viewport(0, 0, this.width, this.height); gl.clearColor(...this.clearColorValue); gl.clear(gl.COLOR_BUFFER_BIT); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.BLEND); gl.disable(gl.DEPTH_TEST); this.layers.forEach((l) => l.draw()); this.pendingDraw = false; this.animators = this.animators.filter((a) => !a.removable()); if (this.animators.length > 0) { this.invalidate(); } } get drawMatrix(): m4.Matrix4 { return m4.multiply(this.projectionMat, this.userMat); } drag(dx: number, dy: number) { let t = m4.translation(dx * this.ratio, dy * this.ratio, 0); this.userMat = m4.multiply(t, this.userMat); this.invalidate(); } scaleAt(scale: number, focusX: number, focusY: number) { focusX *= this.ratio; focusY *= this.ratio; let t = m4.scaleAt(scale, scale, focusX, focusY); this.userMat = m4.multiply(t, this.userMat); this.layers.forEach((l) => l.scale(this.userMat[0])); this.invalidate(); } tap(x: number, y: number) { let sx = x * this.ratio; let sy = y * this.ratio; let [cx, cy] = m4.transformPoint( m4.inverse(this.userMat), new Float32Array([sx, sy, 0]), ); this.layers.forEach((l) => l.tap(cx, cy, sx, sy)); } addAnimator(animator: Animator) { this.animators.push(animator); this.invalidate(); } addTestLayer(layer: Layer) { this.testLayers.push(layer); if (this.testLayers.length <= 1) this.layers.push(layer); this.invalidate(); } toggleTestLayer() { if (this.testLayers.length == 0) return; let testLayerIndex = this.testLayers.findIndex((l) => { return this.layers.indexOf(l) >= 0; }); let next = (testLayerIndex + 1) % this.testLayers.length; let nextLayer = this.testLayers[next]; if (testLayerIndex >= 0) { this.layers = this.layers.filter( (l) => l != this.testLayers[testLayerIndex], ); } console.log(`toggleTestLayer, layer=${nextLayer}`); this.layers.push(nextLayer); this.invalidate(); } setScale(scale: number) { m4.scaling(scale, scale, 1, this.userMat); this.invalidate(); } updateUserMat(mat: m4.Matrix4) { m4.copy(mat, this.userMat); this.invalidate(); } matrixAnimationTo( endMat: m4.Matrix4, duration: number, interpolator?: Interpolator, onEnd?: Function, ) { const startMat = m4.copy(this.userMat); const mat = new Float32Array(16); const animator = new Animator( duration, () => { m4.lerp(startMat, endMat, mat, animator.value()); this.updateUserMat(mat); }, () => { onEnd?.(); }, interpolator, ); this.addAnimator(animator); } resetToBestFit() { this.matrixAnimationTo(this.bestFitMat, 600); } dispose() { while (this.layers.length > 0) { let layer = this.layers.pop(); layer?.dispose(); } } }