소스 검색

fix(业务代码): 修复 replay 期间最后区块错误显示 hint 覆盖层

- HintLayer 新增 enabled 开关,draw() 添加禁用 guard,replay 前关闭 hint 渲染
- 根因:游戏结束时 currentGroup setter 未被调用,HintLayer mask 残留最后一组区域,
  replay 阶段 animatableMask 清空导致 WorkLayer 透明,hint 纹理透出
- index.ts 保留 hintLayer 引用并传入 cssOnFinish,replay 前执行 hintLayer.enabled = false
- 同步修复:初始化阶段预加载 promo 图片,避免动画播放时图片异步加载引发布局跳变
guoziyun 3 주 전
부모
커밋
f2a45411e4
2개의 변경된 파일137개의 추가작업 그리고 133개의 파일을 삭제
  1. 113 116
      src/filler/HintLayer.ts
  2. 24 17
      src/filler/index.ts

+ 113 - 116
src/filler/HintLayer.ts

@@ -1,16 +1,11 @@
-import { fillRectangle, rectangle, rectangleArray } from "../base/2d"
-import { LayerAB, Scene } from "../base/Scene"
-import { createProgram, createShader } from "../base/utils"
+import { fillRectangle, rectangle, rectangleArray } from "../base/2d";
+import { LayerAB, Scene } from "../base/Scene";
+import { createProgram, createShader } from "../base/utils";
 import { AreaGroup, Center, Color, createTexture, TexImage } from "./common";
 import { FillerData, FillerDataListener } from "./FillerData";
 import { Mask } from "./Mask";
 
-
-
-
 export class HintLayer extends LayerAB implements FillerDataListener {
-
-
   private vertexShaderCode = /*glsl*/ `
     attribute vec2 a_position;
     attribute vec2 a_texCoord;
@@ -78,106 +73,107 @@ export class HintLayer extends LayerAB implements FillerDataListener {
       }
     }
   
-  `
-
-
-
-  program: WebGLProgram
-
-
-  aPositionLoc: number
-  aTexcoordLoc: number
-  uMatrixLoc: WebGLUniformLocation
-  uScaleLoc: WebGLUniformLocation
-  uPixelSizeLoc: WebGLUniformLocation
-  uHintLoc: WebGLUniformLocation
-  uMaskLoc: WebGLUniformLocation
-  uColorDarkLoc: WebGLUniformLocation
-  uColorLightLoc: WebGLUniformLocation
+  `;
 
-  vertexBuffer: WebGLBuffer
-  texcoordBuffer: WebGLBuffer
+  program: WebGLProgram;
 
-  vertexArray: Float32Array
-  texCoordArray: Float32Array
+  aPositionLoc: number;
+  aTexcoordLoc: number;
+  uMatrixLoc: WebGLUniformLocation;
+  uScaleLoc: WebGLUniformLocation;
+  uPixelSizeLoc: WebGLUniformLocation;
+  uHintLoc: WebGLUniformLocation;
+  uMaskLoc: WebGLUniformLocation;
+  uColorDarkLoc: WebGLUniformLocation;
+  uColorLightLoc: WebGLUniformLocation;
 
+  vertexBuffer: WebGLBuffer;
+  texcoordBuffer: WebGLBuffer;
 
-  mask: Mask
-  hintTexture: WebGLTexture
+  vertexArray: Float32Array;
+  texCoordArray: Float32Array;
 
-  colorDarkArray = new Float32Array(4)
-  colorLightArray = new Float32Array(4)
+  mask: Mask;
+  hintTexture: WebGLTexture;
 
+  colorDarkArray = new Float32Array(4);
+  colorLightArray = new Float32Array(4);
 
   override dispose() {
-    this.mask.dispose()
-    let gl = this.scene.gl
-    gl.deleteProgram(this.program)
-    gl.deleteBuffer(this.vertexBuffer)
-    gl.deleteBuffer(this.texcoordBuffer)
-    gl.deleteTexture(this.hintTexture)
+    this.mask.dispose();
+    let gl = this.scene.gl;
+    gl.deleteProgram(this.program);
+    gl.deleteBuffer(this.vertexBuffer);
+    gl.deleteBuffer(this.texcoordBuffer);
+    gl.deleteTexture(this.hintTexture);
   }
 
-
   constructor(
     public readonly scene: Scene,
     private fillerData: FillerData,
   ) {
-    super()
+    super();
 
-    this.fillerData.addListener(this)
+    this.fillerData.addListener(this);
 
     const gl = scene.gl;
 
-    this.program = createProgram(gl,
+    this.program = createProgram(
+      gl,
       createShader(gl, gl.VERTEX_SHADER, this.vertexShaderCode)!,
-      createShader(gl, gl.FRAGMENT_SHADER, this.fragmentShaderCode)!)!
-
-    this.aPositionLoc = gl.getAttribLocation(this.program, "a_position")
-    this.aTexcoordLoc = gl.getAttribLocation(this.program, "a_texCoord")
-    this.uMatrixLoc = gl.getUniformLocation(this.program, "u_matrix")!
-
-    this.uScaleLoc = gl.getUniformLocation(this.program, "u_scale")!
-    this.uPixelSizeLoc = gl.getUniformLocation(this.program, "u_pixelSize")!
-    this.uHintLoc = gl.getUniformLocation(this.program, "u_hint")!
-    this.uMaskLoc = gl.getUniformLocation(this.program, "u_mask")!
-    this.uColorDarkLoc = gl.getUniformLocation(this.program, "u_colorDark")!
-    this.uColorLightLoc = gl.getUniformLocation(this.program, "u_colorLight")!
-
-
-    this.vertexArray = rectangleArray(0, 0, fillerData.width, fillerData.height)
-    this.texCoordArray = rectangleArray(0, 0, 1, 1)
-
+      createShader(gl, gl.FRAGMENT_SHADER, this.fragmentShaderCode)!,
+    )!;
+
+    this.aPositionLoc = gl.getAttribLocation(this.program, "a_position");
+    this.aTexcoordLoc = gl.getAttribLocation(this.program, "a_texCoord");
+    this.uMatrixLoc = gl.getUniformLocation(this.program, "u_matrix")!;
+
+    this.uScaleLoc = gl.getUniformLocation(this.program, "u_scale")!;
+    this.uPixelSizeLoc = gl.getUniformLocation(this.program, "u_pixelSize")!;
+    this.uHintLoc = gl.getUniformLocation(this.program, "u_hint")!;
+    this.uMaskLoc = gl.getUniformLocation(this.program, "u_mask")!;
+    this.uColorDarkLoc = gl.getUniformLocation(this.program, "u_colorDark")!;
+    this.uColorLightLoc = gl.getUniformLocation(this.program, "u_colorLight")!;
+
+    this.vertexArray = rectangleArray(
+      0,
+      0,
+      fillerData.width,
+      fillerData.height,
+    );
+    this.texCoordArray = rectangleArray(0, 0, 1, 1);
 
     this.vertexBuffer = gl.createBuffer()!;
     gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
     gl.bufferData(gl.ARRAY_BUFFER, this.vertexArray, gl.STATIC_DRAW);
 
-
     this.texcoordBuffer = gl.createBuffer()!;
     gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer);
     gl.bufferData(gl.ARRAY_BUFFER, this.texCoordArray, gl.STATIC_DRAW);
 
-
-    this.mask = new Mask(gl, fillerData)
-    this.hintTexture = this.createHintTexture(gl, fillerData.width, fillerData.height)
+    this.mask = new Mask(gl, fillerData);
+    this.hintTexture = this.createHintTexture(
+      gl,
+      fillerData.width,
+      fillerData.height,
+    );
 
     if (this.fillerData.currentGroup) {
-      this.setGroup(this.fillerData.currentGroup)
+      this.setGroup(this.fillerData.currentGroup);
     }
-
   }
 
-
-
-  createHintTexture(gl: WebGL2RenderingContext, width: number, height: number): WebGLTexture {
-
+  createHintTexture(
+    gl: WebGL2RenderingContext,
+    width: number,
+    height: number,
+  ): WebGLTexture {
     let texture = gl.createTexture()!;
     gl.bindTexture(gl.TEXTURE_2D, texture);
 
     const size = 10;
-    const nx = Math.floor(width / size)
-    const ny = Math.floor(height / size)
+    const nx = Math.floor(width / size);
+    const ny = Math.floor(height / size);
     const pixels = new Uint8Array(nx * ny);
     for (var y = 0; y < ny; y++) {
       let k = y % 2 == 0;
@@ -189,34 +185,44 @@ export class HintLayer extends LayerAB implements FillerDataListener {
     }
 
     gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
-    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, nx, ny, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, pixels);
+    gl.texImage2D(
+      gl.TEXTURE_2D,
+      0,
+      gl.LUMINANCE,
+      nx,
+      ny,
+      0,
+      gl.LUMINANCE,
+      gl.UNSIGNED_BYTE,
+      pixels,
+    );
     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
     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);
 
-    return texture
+    return texture;
   }
 
+  /** replay 阶段设为 false,隐藏 hint 覆盖层 */
+  public enabled: boolean = true;
 
   setGroup(group: AreaGroup) {
     for (var i = 0; i < group.areas.length; i++) {
-      this.mask.addArea(group.areas[i])
+      this.mask.addArea(group.areas[i]);
     }
-    this.mask.flush(true)
-    this.scene.invalidate()
+    this.mask.flush(true);
+    this.scene.invalidate();
   }
 
-
   onGroupChange(group: AreaGroup | null): void {
-    console.log('onGroupChange', group);
+    console.log("onGroupChange", group);
 
-    if (group != null) this.setGroup(group)
+    if (group != null) this.setGroup(group);
 
     this.fillerData.callback?.onSwitchGroup();
   }
 
-
   // 注释掉,不允许随意点击自动切换group
   // override tap(cx: number, cy: number, sx: number, sy: number): void {
   //   let area = this.fillerData.getArea(cx, cy, 40, this.fillerData.data.areaHash)
@@ -227,56 +233,47 @@ export class HintLayer extends LayerAB implements FillerDataListener {
   //   }
   // }
 
-
-
-
-
-
   override draw() {
+    if (!this.enabled) return;
 
     const gl = this.scene.gl;
     gl.useProgram(this.program);
 
     gl.enableVertexAttribArray(this.aPositionLoc);
     gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
-    gl.vertexAttribPointer(this.aPositionLoc, 2, gl.FLOAT, false, 0, 0)
+    gl.vertexAttribPointer(this.aPositionLoc, 2, gl.FLOAT, false, 0, 0);
 
     gl.enableVertexAttribArray(this.aTexcoordLoc);
     gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer);
-    gl.vertexAttribPointer(this.aTexcoordLoc, 2, gl.FLOAT, false, 0, 0)
-
-    gl.uniformMatrix4fv(this.uMatrixLoc, false, this.scene.drawMatrix)
-    gl.uniform1f(this.uScaleLoc, this.scene.userMat[0])
-    gl.uniform2f(this.uPixelSizeLoc, 1. / this.mask.width, 1. / this.mask.height)
-
-
-    new Color(this.fillerData.config.hintColorDark).fillFloatArray(this.colorDarkArray, 0)
-    new Color(this.fillerData.config.hintColorLight).fillFloatArray(this.colorLightArray, 0)
-    gl.uniform4fv(this.uColorDarkLoc, this.colorDarkArray)
-    gl.uniform4fv(this.uColorLightLoc, this.colorLightArray)
-
-    gl.uniform1i(this.uHintLoc, 0);  // texture unit 0
-    gl.uniform1i(this.uMaskLoc, 1);  // texture unit 1
-
-    gl.activeTexture(gl.TEXTURE0)
-    gl.bindTexture(gl.TEXTURE_2D, this.hintTexture)
-    gl.activeTexture(gl.TEXTURE1)
-    gl.bindTexture(gl.TEXTURE_2D, this.mask.texture)
+    gl.vertexAttribPointer(this.aTexcoordLoc, 2, gl.FLOAT, false, 0, 0);
+
+    gl.uniformMatrix4fv(this.uMatrixLoc, false, this.scene.drawMatrix);
+    gl.uniform1f(this.uScaleLoc, this.scene.userMat[0]);
+    gl.uniform2f(this.uPixelSizeLoc, 1 / this.mask.width, 1 / this.mask.height);
+
+    new Color(this.fillerData.config.hintColorDark).fillFloatArray(
+      this.colorDarkArray,
+      0,
+    );
+    new Color(this.fillerData.config.hintColorLight).fillFloatArray(
+      this.colorLightArray,
+      0,
+    );
+    gl.uniform4fv(this.uColorDarkLoc, this.colorDarkArray);
+    gl.uniform4fv(this.uColorLightLoc, this.colorLightArray);
+
+    gl.uniform1i(this.uHintLoc, 0); // texture unit 0
+    gl.uniform1i(this.uMaskLoc, 1); // texture unit 1
+
+    gl.activeTexture(gl.TEXTURE0);
+    gl.bindTexture(gl.TEXTURE_2D, this.hintTexture);
+    gl.activeTexture(gl.TEXTURE1);
+    gl.bindTexture(gl.TEXTURE_2D, this.mask.texture);
 
     gl.drawArrays(gl.TRIANGLES, 0, 6);
-
   }
 
-
-
-
-
   toString(): string {
-    return `HintLayer()`
+    return `HintLayer()`;
   }
-
 }
-
-
-
-

+ 24 - 17
src/filler/index.ts

@@ -86,6 +86,9 @@ async function init() {
   canvas.height = canvas.clientHeight * pixelRatio;
   let scene = new FillerScene(gl, pixelRatio);
 
+  // 确保 canvas 像素尺寸与实际 flex 布局一致(body.onload 时 dvh 可能还未最终结算)
+  syncCanvasSize();
+
   let audio = new AudioPlayer();
 
   // 加载设置项,学个新语法 ...
@@ -116,6 +119,11 @@ async function init() {
   (document.getElementById("app-logo-txt") as HTMLImageElement).src =
     logoTxtUrl;
 
+  // 预加载 promo 图片,避免动画播放时图片刚加载导致布局跳变
+  (document.getElementById("promo-coloring") as HTMLImageElement).src =
+    coloringPagesUrl;
+  (document.getElementById("promo-slogon") as HTMLImageElement).src = slogonUrl;
+
   let taskList: number[] = [];
 
   // fingerHint 先声明,init 末尾赋值后回调中就能使用
@@ -157,7 +165,7 @@ async function init() {
       },
       onFinish() {
         fingerHint?.stop();
-        cssOnFinish(scene, workLayer, audio);
+        cssOnFinish(scene, workLayer, hintLayer, audio);
       },
     },
   );
@@ -166,19 +174,8 @@ async function init() {
 
   console.log("resource", resource);
 
-  /** 根据横/竖屏设置 canvas 内容 padding(为 UI 元素留出空间) */
-  function updateContentPadding() {
-    const isLandscape = window.innerWidth > window.innerHeight;
-    if (isLandscape) {
-      // 横屏:右侧栏已占 42%,canvas 区域更方正,只需留出底部工具栏空间
-      scene.setContentPadding(new Padding(20, 90, 20, 140));
-    } else {
-      // 竖屏:顶部留 logo(140px),底部留工具栏+CTA(约 330px)
-      scene.setContentPadding(new Padding(50, 250, 50, 330));
-    }
-  }
-  updateContentPadding();
-  window.addEventListener("resize", updateContentPadding);
+  // canvas 区域与所有 UI 完全分离,只保留极小视觉边距
+  scene.setContentPadding(new Padding(20, 20, 20, 20));
 
   //let page = await loadImage('/webgl/page.png')
   //let page = await loadImage('/assets/resources/friend/page_gray.png')
@@ -187,6 +184,9 @@ async function init() {
   let height = fillerData.height;
   scene.setContentSize(width, height);
 
+  // // 最底层:透明背景,canvas 透出 body 渐变色,与整体 UI 融为一体
+  // scene.addLayer(new BackgroundLayer(scene, scene.width, scene.height, [0, 0, 0, 0]));
+
   if (resource.bg) {
     scene.addLayer(
       new BgLayer(scene, resource.bg, scene.width, scene.height, BgType.Repeat),
@@ -194,7 +194,8 @@ async function init() {
   }
   scene.addLayer(new BoxLayer(scene, 0, 0, width, height));
 
-  scene.addLayer(new HintLayer(scene, fillerData));
+  let hintLayer = new HintLayer(scene, fillerData);
+  scene.addLayer(hintLayer);
   scene.addLayer(new NumberLayer(scene, resource.numberImage, fillerData, 1));
 
   let workLayer = new WorkLayer(scene, fillerData);
@@ -235,7 +236,7 @@ async function init() {
   workLayer.initTask();
 
   if (fillerData.data.coloredPercent >= 100) {
-    cssOnFinish(scene, workLayer, null);
+    cssOnFinish(scene, workLayer, hintLayer, null);
   } else {
     // 创建手指提示,全图未完成时启动引导
     fingerHint = new FingerHint(scene, fillerData, fingerUrl);
@@ -464,8 +465,12 @@ function cssColorDone(fillerData: FillerData) {
 function cssOnFinish(
   scene: FillerScene,
   workLayer: WorkLayer,
+  hintLayer: HintLayer,
   audio: AudioPlayer | null,
 ) {
+  // 立即将 canvas 底色改为透明,让 body 渐变色从绘图区域透出,避免 replay/消失动画中出现局部白色块
+  scene.setClearTransparent();
+
   // 隐藏toolbar
   const toolbarBottom = document.getElementById(
     "toolbar-bottom",
@@ -491,6 +496,8 @@ function cssOnFinish(
           16,
         ),
       );
+      hintLayer.enabled = false; // replay 期间隐藏 hint 覆盖层
+      scene.invalidate();
       workLayer.replay().then(() => showPromoScreen());
     }, 500); // 过渡动画持续 0.5 秒
   }, 300); // 粒子爆破动画持续 0.3 秒
@@ -510,7 +517,7 @@ function showPromoScreen() {
     "promo-slogon",
   ) as HTMLImageElement;
 
-  // 预设图片资源
+  // 预设图片资源(已在 init 阶段预加载,此处保留赋值保证兼容性)
   promoColoring.src = coloringPagesUrl;
   promoSlogon.src = slogonUrl;