Przeglądaj źródła

style(业务代码): 低分屏响应式布局优化,工具栏/按钮/clamp 自适应缩放

- toolbar/logo/CTA/color-btn 全部使用 clamp() 替代固定 px/dvh
- 低分屏下 canvas 可用高度提升约 30%,正常屏视觉不变
- CTA 横屏添加 overflow/letter-spacing/padding 防文字溢出
- CTA border-radius 改为 999px 适配可变高度 pill 形状
- 修复颜色按钮进度环 viewBox 坐标系计算,换容器尺寸不再出错
- NumberLayer 新增 enabled 属性,replay 期间隐藏数字覆盖层
guoziyun 3 tygodni temu
rodzic
commit
c2b058f230
3 zmienionych plików z 156 dodań i 160 usunięć
  1. 22 17
      assets/css/tools.css
  2. 116 126
      src/filler/NumberLayer.ts
  3. 18 17
      src/filler/index.ts

+ 22 - 17
assets/css/tools.css

@@ -121,7 +121,7 @@ body {
 }
 
 #progress-toolbar {
-  height: 20px;
+  height: clamp(12px, 2.5vh, 20px);
 }
 
 #toolbar-bottom {
@@ -151,7 +151,7 @@ body {
   justify-content: flex-start;
   padding: 0px 10px 10px 10px;
   overflow-x: scroll;
-  height: 60px;
+  height: clamp(44px, 8vh, 60px);
   align-items: center;
   gap: 10px;
   user-select: none;
@@ -166,9 +166,9 @@ body {
 
 .color-btn-container {
   position: relative;
-  min-width: 48px;
-  width: 48px;
-  height: 48px;
+  min-width: clamp(34px, 7vh, 48px);
+  width: clamp(34px, 7vh, 48px);
+  height: clamp(34px, 7vh, 48px);
 }
 
 .color-btn-container-selected {
@@ -195,10 +195,10 @@ body {
 
 .color-btn {
   position: absolute;
-  min-width: 40px;
-  width: 40px;
-  height: 40px;
-  line-height: 40px;
+  min-width: clamp(28px, 5.5vh, 40px);
+  width: clamp(28px, 5.5vh, 40px);
+  height: clamp(28px, 5.5vh, 40px);
+  line-height: clamp(28px, 5.5vh, 40px);
   text-align: center;
   border-radius: 50%;
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
@@ -374,7 +374,7 @@ body {
 /* 横屏(sidebar 内 flex 子项) :竖排,充满宽度                        */
 #app-logo-bar {
   order: 1; /* 竖屏:body 第一行 */
-  flex: 0 0 18dvh; /* ← 竖屏固定占屏高 18% */
+  flex: 0 0 clamp(56px, 18dvh, 120px); /* ← 竖屏占屏高 18%,低分屏自动收窄 */
   display: flex;
   flex-direction: row; /* 竖屏:图标+文字横排 */
   align-items: center;
@@ -388,7 +388,7 @@ body {
 }
 
 #app-logo {
-  height: 12dvh; /* ≈ 55% of 20dvh bar */
+  height: clamp(36px, 12dvh, 80px); /* ≈ 55% of bar,低分屏自动收窄 */
   width: auto;
   display: block;
   flex-shrink: 0;
@@ -396,7 +396,7 @@ body {
 }
 
 #app-logo-txt {
-  height: 12dvh;
+  height: clamp(36px, 12dvh, 80px);
   width: auto;
   max-width: 55%;
   display: block;
@@ -460,7 +460,7 @@ body {
 /* 横屏(sidebar 内 flex 子项) :在 spacer 之后贴底              */
 #cta-btn-wrapper {
   order: 10; /* 竖屏:body 最末行 */
-  flex: 0 0 14dvh; /* ← 竖屏固定占屏高 14%(含按钮+上下间距) */
+  flex: 0 0 clamp(44px, 14dvh, 90px); /* ← 竖屏占屏高 14%,低分屏自动收窄 */
   display: flex;
   align-items: center;
   justify-content: center;
@@ -488,20 +488,25 @@ body {
     max-width: none;
     height: clamp(42px, 10vh, 54px);
     font-size: clamp(13px, 3.5vh, 17px);
+    letter-spacing: clamp(1px, 0.8vw, 3px);
+    padding: 0 clamp(8px, 2vw, 24px);
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
   }
 }
 
 #cta-btn {
   pointer-events: auto;
   width: min(300px, 80vw);
-  height: 44px;
+  height: clamp(34px, 7vh, 48px);
   border: none;
-  border-radius: 22px;
+  border-radius: 999px;
   background: linear-gradient(135deg, #ff5f1f 0%, #ffb300 100%);
   color: #fff;
-  font-size: 18px;
+  font-size: clamp(14px, 3vh, 18px);
   font-weight: 800;
-  letter-spacing: 3px;
+  letter-spacing: clamp(1px, 0.5vw, 3px);
   font-family: Arial, Helvetica, sans-serif;
   cursor: pointer;
   /* 立体感阴影 */

+ 116 - 126
src/filler/NumberLayer.ts

@@ -1,15 +1,10 @@
-import { fillRectangle } from "../base/2d"
-import { LayerAB, Scene } from "../base/Scene"
-import { createProgram, createShader } from "../base/utils"
+import { fillRectangle } from "../base/2d";
+import { LayerAB, Scene } from "../base/Scene";
+import { createProgram, createShader } from "../base/utils";
 import { Center, createTexture, TexImage } from "./common";
 import { FillerData } from "./FillerData";
 
-
-
-
 export class NumberLayer extends LayerAB {
-
-
   private vertexShaderCode = /*glsl*/ `
     attribute vec2 a_position;
     attribute vec2 a_texCoord;
@@ -33,36 +28,33 @@ export class NumberLayer extends LayerAB {
       gl_FragColor = color;
     }
   
-  `
-
-
-
-  program: WebGLProgram
+  `;
 
+  program: WebGLProgram;
 
-  aPositionLoc: number
-  aTexcoordLoc: number
-  uMatrixLoc: WebGLUniformLocation
-  uScaleLoc: WebGLUniformLocation
-  uTexSizeLoc: WebGLUniformLocation
+  aPositionLoc: number;
+  aTexcoordLoc: number;
+  uMatrixLoc: WebGLUniformLocation;
+  uScaleLoc: WebGLUniformLocation;
+  uTexSizeLoc: WebGLUniformLocation;
 
-  vertexBuffer: WebGLBuffer
-  texcoordBuffer: WebGLBuffer
+  vertexBuffer: WebGLBuffer;
+  texcoordBuffer: WebGLBuffer;
 
-  vertexArray: Float32Array
-  texCoordArray: Float32Array
+  vertexArray: Float32Array;
+  texCoordArray: Float32Array;
 
-  texture: WebGLTexture
+  texture: WebGLTexture;
 
-  digits: number
-  centers: Array<Center>
+  digits: number;
+  centers: Array<Center>;
 
   dispose() {
-    let gl = this.scene.gl
-    gl.deleteProgram(this.program)
-    gl.deleteBuffer(this.vertexBuffer)
-    gl.deleteBuffer(this.texcoordBuffer)
-    gl.deleteTexture(this.texture)
+    let gl = this.scene.gl;
+    gl.deleteProgram(this.program);
+    gl.deleteBuffer(this.vertexBuffer);
+    gl.deleteBuffer(this.texcoordBuffer);
+    gl.deleteTexture(this.texture);
   }
 
   constructor(
@@ -71,185 +63,183 @@ export class NumberLayer extends LayerAB {
     private fillerData: FillerData,
     private bestFitScale: number,
   ) {
-    super()
+    super();
 
     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)!)!
+      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.uTexSizeLoc = gl.getUniformLocation(this.program, "u_texSize")!
+    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.uTexSizeLoc = gl.getUniformLocation(this.program, "u_texSize")!;
 
+    this.texture = createTexture(gl, image, gl.LINEAR);
 
-    this.texture = createTexture(gl, image, gl.LINEAR)
-
-
-    const centers: Array<Center> = []
-    const ratio = image.width / 10. / image.height
-    const textHeightRatio = Array(4)
+    const centers: Array<Center> = [];
+    const ratio = image.width / 10 / image.height;
+    const textHeightRatio = Array(4);
     for (var i = 0; i < 4; i++) {
-      textHeightRatio[i] = 2. / Math.sqrt(i * i * ratio * ratio + 1)
+      textHeightRatio[i] = 2 / Math.sqrt(i * i * ratio * ratio + 1);
     }
 
+    const magnify = 1.2;
+    const minify = 0.8;
+    var digits = 0;
 
-    const magnify = 1.2
-    const minify = 0.8
-    var digits = 0
-
-
-    const config = fillerData.config
+    const config = fillerData.config;
 
     //const maxScale = fillerData.config.maxScale
 
-    const areaGroups = fillerData.data.areaGroups
-
+    const areaGroups = fillerData.data.areaGroups;
 
     for (var i = 0; i < areaGroups.length; i++) {
-      var group = areaGroups[i]
+      var group = areaGroups[i];
       for (var j = 0; j < group.areas.length; j++) {
-        var area = group.areas[j]
+        var area = group.areas[j];
 
         // 剪枝, 如果已经上色,直接跳过
-        if (area.colored) continue
+        if (area.colored) continue;
 
-        digits += area.center.label!.length
-        centers.push(area.center)
-        var fontHeight = area.center.radius * textHeightRatio[area.center.label!.length]
+        digits += area.center.label!.length;
+        centers.push(area.center);
+        var fontHeight =
+          area.center.radius * textHeightRatio[area.center.label!.length];
 
         // 让文字大小线性放大或者缩小
-        var k = (fontHeight * config.maxScale) / config.visibleFontSize
+        var k = (fontHeight * config.maxScale) / config.visibleFontSize;
         if (k <= 1) {
-          k *= magnify
+          k *= magnify;
         } else if (k > 1 && k < 1 / minify) {
-          k = ((1 / minify - magnify) / (1 / minify - 1)) * (k - 1) + magnify
+          k = ((1 / minify - magnify) / (1 / minify - 1)) * (k - 1) + magnify;
         } else if (k >= 1 / minify) {
-          k = minify * k + (1 / minify - 1)
+          k = minify * k + (1 / minify - 1);
         }
-        fontHeight = k * config.visibleFontSize / config.maxScale
+        fontHeight = (k * config.visibleFontSize) / config.maxScale;
 
         if (fontHeight * this.bestFitScale > config.visibleFontSize * 1.2) {
-          fontHeight = config.visibleFontSize * 1.2 / bestFitScale
+          fontHeight = (config.visibleFontSize * 1.2) / bestFitScale;
         }
 
-        area.center.fontHeight = fontHeight
+        area.center.fontHeight = fontHeight;
       }
     }
 
+    centers.sort((a, b) => b.fontHeight - a.fontHeight);
 
-    centers.sort((a, b) => b.fontHeight - a.fontHeight)
+    this.digits = digits;
+    this.centers = centers;
 
-    this.digits = digits
-    this.centers = centers
-
-    this.vertexArray = new Float32Array(digits * 12)
-    this.texCoordArray = new Float32Array(digits * 12)
-    var offset = 0
+    this.vertexArray = new Float32Array(digits * 12);
+    this.texCoordArray = new Float32Array(digits * 12);
+    var offset = 0;
 
     for (var i = 0; i < centers.length; i++) {
-      var center = centers[i]
-      this.fillVertexArray(this.vertexArray, this.texCoordArray, offset, center, ratio)
-      offset += 12 * center.label.length
-      center.offset = offset / 2
+      var center = centers[i];
+      this.fillVertexArray(
+        this.vertexArray,
+        this.texCoordArray,
+        offset,
+        center,
+        ratio,
+      );
+      offset += 12 * center.label.length;
+      center.offset = offset / 2;
     }
 
-
     //fillRectangle(this.vertexArray, 0, 0, 0, width, height )
     //fillRectangle(this.texCoordArray, 0, 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);
-
   }
 
-
-
+  /** replay 阶段设为 false,隐藏 hint 覆盖层 */
+  public enabled: boolean = true;
 
   override draw() {
+    if (!this.enabled) return;
 
-    var offset = 0
-    const maxScale = this.fillerData.config.maxScale - 0.2
-    const scale = this.scene.userMat[0]
+    var offset = 0;
+    const maxScale = this.fillerData.config.maxScale - 0.2;
+    const scale = this.scene.userMat[0];
 
     if (scale > maxScale) {
-      offset = this.vertexArray.length / 2
+      offset = this.vertexArray.length / 2;
     } else {
       for (var i = this.centers.length - 1; i >= 0; i--) {
-        if (this.centers[i].fontHeight * scale > this.fillerData.config.visibleFontSize) {
-          offset = this.centers[i].offset
-          break
+        if (
+          this.centers[i].fontHeight * scale >
+          this.fillerData.config.visibleFontSize
+        ) {
+          offset = this.centers[i].offset;
+          break;
         }
       }
-
     }
 
-
     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.vertexAttribPointer(this.aTexcoordLoc, 2, gl.FLOAT, false, 0, 0);
 
-    gl.uniformMatrix4fv(this.uMatrixLoc, false, this.scene.drawMatrix)
+    gl.uniformMatrix4fv(this.uMatrixLoc, false, this.scene.drawMatrix);
 
-    gl.uniform1f(this.uScaleLoc, this.scene.userMat[0])
+    gl.uniform1f(this.uScaleLoc, this.scene.userMat[0]);
     //gl.uniform2f(this.uTexSizeLoc, 1. / this.texWidth, 1. / this.texHeight)
 
-    gl.activeTexture(gl.TEXTURE0)
-    gl.bindTexture(gl.TEXTURE_2D, this.texture)
+    gl.activeTexture(gl.TEXTURE0);
+    gl.bindTexture(gl.TEXTURE_2D, this.texture);
 
     gl.drawArrays(gl.TRIANGLES, 0, offset);
-
   }
 
-
-  fillVertexArray(vertexArray: Float32Array, texCoordArray: Float32Array, offset: number, center: Center, ratio: number) {
-
-    const text = center.label
-    const size = text.length
-    const cellWidth = center.fontHeight * ratio
-    const cellHeight = center.fontHeight
-    const width = cellWidth * size
-    const left = center.x - width / 2
-    const top = center.y - cellHeight / 2
+  fillVertexArray(
+    vertexArray: Float32Array,
+    texCoordArray: Float32Array,
+    offset: number,
+    center: Center,
+    ratio: number,
+  ) {
+    const text = center.label;
+    const size = text.length;
+    const cellWidth = center.fontHeight * ratio;
+    const cellHeight = center.fontHeight;
+    const width = cellWidth * size;
+    const left = center.x - width / 2;
+    const top = center.y - cellHeight / 2;
 
     for (var i = 0; i < size; i++) {
-      fillRectangle(vertexArray, offset + 12 * i, left + i * cellWidth, top, cellWidth, cellHeight)
-      const index = parseInt(text.substring(i, i + 1))
-      fillRectangle(texCoordArray, offset + 12 * i, index * 0.1, 0, 0.1, 1)
+      fillRectangle(
+        vertexArray,
+        offset + 12 * i,
+        left + i * cellWidth,
+        top,
+        cellWidth,
+        cellHeight,
+      );
+      const index = parseInt(text.substring(i, i + 1));
+      fillRectangle(texCoordArray, offset + 12 * i, index * 0.1, 0, 0.1, 1);
     }
-
   }
 
-
-
-
-
-
-
   toString(): string {
-    return `NumberLayer()`
+    return `NumberLayer()`;
   }
-
 }
-
-
-
-

+ 18 - 17
src/filler/index.ts

@@ -166,7 +166,7 @@ async function init() {
       onFinish() {
         fingerHint?.stop();
         adPlatform.onGameEnd();
-        cssOnFinish(scene, workLayer, hintLayer, audio);
+        cssOnFinish(scene, workLayer, hintLayer, numberLayer, audio);
       },
     },
   );
@@ -197,7 +197,9 @@ async function init() {
 
   let hintLayer = new HintLayer(scene, fillerData);
   scene.addLayer(hintLayer);
-  scene.addLayer(new NumberLayer(scene, resource.numberImage, fillerData, 1));
+
+  let numberLayer = new NumberLayer(scene, resource.numberImage, fillerData, 1);
+  scene.addLayer(numberLayer);
 
   let workLayer = new WorkLayer(scene, fillerData);
 
@@ -238,7 +240,7 @@ async function init() {
 
   if (fillerData.data.coloredPercent >= 100) {
     adPlatform.onGameEnd();
-    cssOnFinish(scene, workLayer, hintLayer, null);
+    cssOnFinish(scene, workLayer, hintLayer, numberLayer, null);
   } else {
     // 创建手指提示,全图未完成时启动引导
     fingerHint = new FingerHint(scene, fillerData, fingerUrl);
@@ -360,16 +362,19 @@ function cssSelectColor(i: number) {
 }
 
 // 设置color button的进度,即areagroup的进度
+// SVG viewBox 常量,所有strokeDasharray/offset计算均在此坐标系内
+const COLOR_BTN_VIEWBOX = 48;
+const COLOR_BTN_RING_RATIO = 0.45;
+const COLOR_BTN_RADIUS = COLOR_BTN_VIEWBOX * COLOR_BTN_RING_RATIO; // 21.6
+const COLOR_BTN_CIRCUMFERENCE = 2 * Math.PI * COLOR_BTN_RADIUS;
+
 function cssSetColorProgress(i: number, percent: number) {
-  const container = document.getElementById(`color-btn-container-${i}`)!;
   const progressCircle = document.getElementById(
     `color-btn-progress-ring-value-${i}`,
   )!;
 
-  const containerWidth = container.clientWidth;
-  const radius = containerWidth * 0.45;
-  const circumference = 2 * Math.PI * radius;
-  const offset = circumference - (percent * circumference) / 100;
+  const offset =
+    COLOR_BTN_CIRCUMFERENCE - (percent * COLOR_BTN_CIRCUMFERENCE) / 100;
   progressCircle.style.strokeDashoffset = offset.toString();
 
   if (percent >= 100) {
@@ -381,19 +386,13 @@ function cssSetColorProgress(i: number, percent: number) {
 
 // 初始化color button进度
 function cssInitColorProgress(i: number, percent: number) {
-  const container = document.getElementById(`color-btn-container-${i}`)!;
   const progressCircle = document.getElementById(
     `color-btn-progress-ring-value-${i}`,
   )!;
 
-  // 通过容器尺寸计算 SVG 半径
-  const containerWidth = container.clientWidth;
-  const radius = containerWidth * 0.45; // 对应 r="45%"
-
-  // 计算周长并设置属性
-  const circumference = 2 * Math.PI * radius;
-  const offset = circumference - (percent * circumference) / 100;
-  progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;
+  const offset =
+    COLOR_BTN_CIRCUMFERENCE - (percent * COLOR_BTN_CIRCUMFERENCE) / 100;
+  progressCircle.style.strokeDasharray = `${COLOR_BTN_CIRCUMFERENCE} ${COLOR_BTN_CIRCUMFERENCE}`;
   progressCircle.style.strokeDashoffset = offset.toString();
 }
 
@@ -468,6 +467,7 @@ function cssOnFinish(
   scene: FillerScene,
   workLayer: WorkLayer,
   hintLayer: HintLayer,
+  numberLayer: NumberLayer,
   audio: AudioPlayer | null,
 ) {
   // 立即将 canvas 底色改为透明,让 body 渐变色从绘图区域透出,避免 replay/消失动画中出现局部白色块
@@ -499,6 +499,7 @@ function cssOnFinish(
         ),
       );
       hintLayer.enabled = false; // replay 期间隐藏 hint 覆盖层
+      numberLayer.enabled = false; // replay 期间隐藏 number 覆盖层
       scene.invalidate();
       workLayer.replay().then(() => showPromoScreen());
     }, 500); // 过渡动画持续 0.5 秒