Browse Source

fix: 优化 coloring 模板多屏适配与 Google clickTag

- 优化窄高屏和平板竖屏下 canvas/game-area 布局
- 修复横屏 CTA 按钮在右侧栏中的宽度约束
- WebGL bestFit 变更后同步 userMat,避免视口重算后内容偏移
- 手指提示挂载到 game-area 内部并使用 canvas 局部坐标定位
- canvas resize 后刷新手指提示位置
- index.html 增加 Google HTML5 clickTag 声明
- Google adapter fallback 优先使用 window.clickTag
guoziyun 2 weeks ago
parent
commit
a3a3e9b0a1

+ 139 - 7
templates/coloring/assets/css/tools.css

@@ -80,6 +80,89 @@ body {
   z-index: 1;
 }
 
+/*
+ * 高竖屏:填色内容本身是方形素材,不能让 game-area 吃满所有剩余高度,
+ * 否则可见画面会悬在一个过高的透明 canvas 中,调色按钮看起来离画面很远。
+ * 这里把 canvas 区域收敛为接近屏宽的正方形,并把 CTA 用 auto margin 推到底部。
+ */
+@media (orientation: portrait) and (min-height: 700px) and (max-width: 599px) {
+  #game-col {
+    flex: 0 0 auto;
+    width: 100%;
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  #game-area {
+    flex: 0 0 auto;
+    width: calc(100vw - 32px);
+    height: calc(100vw - 32px);
+    max-width: 430px;
+    max-height: 430px;
+    margin: 0 auto;
+  }
+
+  #toolbar-bottom {
+    flex: 0 0 auto;
+  }
+
+  #color-btns {
+    box-sizing: border-box;
+    height: clamp(56px, 9dvh, 72px);
+    padding: 0 10px 8px 10px;
+  }
+
+  #cta-btn-wrapper {
+    margin-top: 0;
+  }
+}
+
+/* 平板竖屏:给方形绘图区更多空间,但保留 logo / 调色板 / CTA 的安全高度 */
+@media (orientation: portrait) and (min-width: 600px) {
+  #game-col {
+    flex: 0 0 auto;
+    width: 100%;
+    margin-top: auto;
+    margin-bottom: auto;
+  }
+
+  #game-area {
+    flex: 0 0 auto;
+    width: min(
+      calc(100vw - 64px),
+      calc(
+        100vh - clamp(64px, 20vh, 140px) - clamp(58px, 7vh, 76px) -
+          clamp(64px, 7vh, 82px) - 24px
+      ),
+      760px
+    );
+    height: min(
+      calc(100vw - 64px),
+      calc(
+        100vh - clamp(64px, 20vh, 140px) - clamp(58px, 7vh, 76px) -
+          clamp(64px, 7vh, 82px) - 24px
+      ),
+      760px
+    );
+    margin: 0 auto;
+  }
+
+  #toolbar-bottom {
+    flex: 0 0 auto;
+  }
+
+  #color-btns {
+    box-sizing: border-box;
+    height: clamp(64px, 7vh, 82px);
+    padding: 0 16px 10px 16px;
+  }
+
+  #cta-btn-wrapper {
+    flex-basis: clamp(58px, 7vh, 76px);
+    margin-top: 0;
+  }
+}
+
 /* ── 竖屏:sidebar 用 display:contents,子项直接排入 body ── */
 #sidebar {
   display: contents;
@@ -109,7 +192,7 @@ body {
     align-items: center;
     justify-content: flex-start;
     gap: 0;
-    width: clamp(140px, 36%, 260px);
+    width: clamp(160px, 40%, 300px);
     height: 100%;
     background: transparent;
     padding: clamp(8px, 1.5vh, 16px) clamp(10px, 2vw, 16px);
@@ -450,19 +533,23 @@ body {
     flex: 3 3 0;
     min-height: 0;
     order: 10;
-    width: 90%;
+    width: 96%;
+    max-width: 180px;
     padding: 0;
+    margin: 0 auto;
     position: static;
     z-index: auto;
+    box-sizing: border-box;
   }
 
-  #cta-btn {
+  #cta-btn-wrapper #cta-btn {
     width: 100%;
-    max-width: none;
+    max-width: 100%;
+    box-sizing: border-box;
     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);
+    font-size: clamp(12px, 3.2vh, 16px);
+    letter-spacing: clamp(0.5px, 0.5vw, 2px);
+    padding: 0 8px;
     white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
@@ -541,6 +628,51 @@ body {
   animation: cta-highlight-pulse 0.8s ease-in-out infinite;
 }
 
+@media (orientation: landscape) {
+  #cta-btn-wrapper {
+    width: 96%;
+    max-width: 180px;
+    margin: 0 auto;
+  }
+
+  #cta-btn-wrapper #cta-btn {
+    width: 100% !important;
+    max-width: 100% !important;
+    min-width: 0;
+  }
+}
+
+/* 放在通用规则之后:平板竖屏先预留 logo/CTA,再让 game-col 吃掉中间剩余空间 */
+@media (orientation: portrait) and (min-width: 600px) {
+  #game-col {
+    flex: 1 1 auto;
+    min-height: 0;
+    width: 100%;
+    margin: 0;
+  }
+
+  #game-area {
+    flex: 1 1 auto;
+    min-height: 0;
+    width: 100%;
+    height: auto;
+    max-width: none;
+    max-height: none;
+    margin: 0;
+  }
+
+  #color-btns {
+    box-sizing: border-box;
+    height: clamp(64px, 7vh, 82px);
+    padding: 0 16px 10px 16px;
+  }
+
+  #cta-btn-wrapper {
+    flex: 0 0 clamp(58px, 7vh, 76px);
+    margin-top: 0;
+  }
+}
+
 /* ── 宣传界面 ──────────────────────────────────────────── */
 /* 在 game-area 内 position:absolute 展开,填满整个填色区域   */
 /* logo-bar(z:200) 和 cta-wrapper(z:200) 叠在其上             */

+ 5 - 0
templates/coloring/index.html

@@ -4,6 +4,11 @@
     <meta charset="UTF-8" />
     <title>Coloring Page Paint On Line | Coloring Game</title>
 
+    <!-- Google HTML5 click tag fallback; platform builds may override this value. -->
+    <script>
+      var clickTag = "https://play.google.com/store/apps/details?id=com.pcoloring.art.puzzle.color.by.number";
+    </script>
+
     <script type="module" src="./src/filler/index.ts"></script>
     <meta
       name="viewport"

+ 10 - 8
templates/coloring/src/base/Scene.ts

@@ -118,11 +118,13 @@ export class Scene implements Disposable {
       this.width - this.padding.right - this.padding.left,
       this.height - this.padding.top - this.padding.bottom,
     );
+    // 初始填色阶段优先让内容接近占满宽度;高度不足时再按高度适配。
+    const fitRate = this.width <= this.height ? 0.96 : 0.88;
     const scale = Math.min(
-      box.width / this.contentWidth,
+      (this.width * fitRate) / this.contentWidth,
       box.height / this.contentHeight,
     );
-    const tx = box.center.x - (this.contentWidth * scale) / 2;
+    const tx = this.width / 2 - (this.contentWidth * scale) / 2;
     const ty = box.center.y - (this.contentHeight * scale) / 2;
     m4.identity(this.bestFitMat);
     this.bestFitMat[0] = scale;
@@ -130,11 +132,11 @@ export class Scene implements Disposable {
     this.bestFitMat[12] = tx;
     this.bestFitMat[13] = ty;
 
-    if (!this.isBestMatSet) {
-      m4.copy(this.bestFitMat, this.userMat);
-      this.invalidate();
-      this.isBestMatSet = true;
-    }
+    // H5 Validator / iframe 中 dvh、flex 最终结算可能晚于 Scene 构造。
+    // 每次 viewport/content 变化时同步 userMat,避免内容矩阵停留在旧尺寸。
+    m4.copy(this.bestFitMat, this.userMat);
+    this.invalidate();
+    this.isBestMatSet = true;
   }
 
   updateResultMat() {
@@ -155,7 +157,7 @@ export class Scene implements Disposable {
     // this.resultMat[12] = tx
     // this.resultMat[13] = ty
 
-    let rate = this.width > this.height ? 0.7 : 0.8;
+    let rate = this.width > this.height ? 0.7 : 0.92;
     let scaleX = (this.width * rate) / this.contentWidth;
     let scaleY = (this.height * rate) / this.contentHeight;
     let scale = Math.min(scaleX, scaleY);

+ 24 - 16
templates/coloring/src/filler/FingerHint.ts

@@ -28,7 +28,8 @@ export class FingerHint {
     fingerUrl: string,
   ) {
     this.el = this.createEl(fingerUrl);
-    document.body.appendChild(this.el);
+    const gameArea = document.getElementById("game-area");
+    (gameArea || document.body).appendChild(this.el);
     this.injectStyle();
   }
 
@@ -39,6 +40,12 @@ export class FingerHint {
     this.scheduleHint(800);
   }
 
+  /** 画布尺寸或矩阵变化后调用,重新按最新 userMat 定位当前提示 */
+  refresh() {
+    if (this.stopped || this.el.style.display === "none") return;
+    this.showHint();
+  }
+
   /** 每次用户有交互动作(填色成功/失败)时调用 */
   onUserInteraction() {
     this.hide();
@@ -73,7 +80,11 @@ export class FingerHint {
 
   private scheduleHint(delay: number) {
     if (this.idleTimer !== null) clearTimeout(this.idleTimer);
-    this.idleTimer = window.setTimeout(() => this.showHint(), delay);
+    this.idleTimer = window.setTimeout(() => {
+      requestAnimationFrame(() => {
+        requestAnimationFrame(() => this.showHint());
+      });
+    }, delay);
   }
 
   private showHint() {
@@ -83,36 +94,33 @@ export class FingerHint {
     const area = group.firstUncoloredArea;
     if (!area) return;
 
-    const [cssX, cssY] = this.contentToScreen(area.center.x, area.center.y);
+    const [cssX, cssY] = this.contentToCanvasCss(area.center.x, area.center.y);
     this.show(cssX, cssY);
   }
 
   /**
-   * 将内容坐标(图片像素空间)转换为 CSS 页面坐标(视口 px)
+   * 将内容坐标(图片像素空间)转换为 canvas 内 CSS 坐标。
    *
    * Scene 的 userMat 是列主序矩阵,对简单 scale+translate 变换:
    *   physX = cx * mat[0] + cy * mat[4] + mat[12]
    *   physY = cx * mat[1] + cy * mat[5] + mat[13]
-   * 再除以 devicePixelRatio 得到 canvas 内 CSS px;
-   * 最后加上 canvas 元素的视口偏移,以匹配 position:fixed 坐标系
+   * 再除以 canvas 实际像素尺寸 / CSS 尺寸,得到 canvas 内 CSS px。
+   * 手指 DOM 挂在 #game-area 内部,因此无需再叠加 viewport 偏移
    */
-  private contentToScreen(cx: number, cy: number): [number, number] {
+  private contentToCanvasCss(cx: number, cy: number): [number, number] {
     const mat = this.scene.userMat;
-    const ratio = this.scene.ratio;
     const physX = cx * mat[0] + cy * mat[4] + mat[12];
     const physY = cx * mat[1] + cy * mat[5] + mat[13];
-    // canvas 内 CSS 像素
-    const canvasCssX = physX / ratio;
-    const canvasCssY = physY / ratio;
-    // 加上 canvas 在视口中的偏移,转换为 position:fixed 坐标
-    const canvas = this.scene.gl.canvas as HTMLElement;
+    const canvas = this.scene.gl.canvas as HTMLCanvasElement;
     const rect = canvas.getBoundingClientRect();
-    return [canvasCssX + rect.left, canvasCssY + rect.top];
+    const scaleX = canvas.width / rect.width;
+    const scaleY = canvas.height / rect.height;
+    return [physX / scaleX, physY / scaleY];
   }
 
   private show(cssX: number, cssY: number) {
     const size = FingerHint.SIZE;
-    // 将指尖对齐到目标坐标
+    // 将指尖严格对齐到目标坐标;不要 clamp 容器位置,否则会破坏指尖与内容坐标的对应关系。
     this.el.style.left = `${cssX - size * FingerHint.TIP_X}px`;
     this.el.style.top = `${cssY - size * FingerHint.TIP_Y}px`;
     this.el.style.display = "block";
@@ -129,7 +137,7 @@ export class FingerHint {
     const el = document.createElement("div");
     el.id = "finger-hint";
     el.style.cssText = `
-      position: fixed;
+      position: absolute;
       width: ${size}px;
       height: ${size}px;
       pointer-events: none;

+ 1 - 1
templates/coloring/src/filler/ad-platform/adapters/google.ts

@@ -30,7 +30,7 @@ export function createGoogleAdapter(): AdPlatformAdapter {
         window.ExitApi.exit();
       } else {
         // 非 Google 广告环境下的 fallback(如本地调试)
-        window.open(_storeUrl, "_blank");
+        window.open(window.clickTag || _storeUrl, "_blank");
       }
     },
 

+ 1 - 0
templates/coloring/src/filler/ad-platform/types.ts

@@ -30,6 +30,7 @@ declare global {
       addEventListener: (event: string, cb: () => void) => void;
     };
     install?: () => void;
+    clickTag?: string;
     gameReady?: () => void;
     gameEnd?: () => void;
     gameRetry?: () => void;

+ 3 - 4
templates/coloring/src/filler/index.ts

@@ -83,6 +83,7 @@ async function init() {
     premultipliedAlpha: false,
   }) as WebGL2RenderingContext;
   let pixelRatio = window.devicePixelRatio;
+  let fingerHint: FingerHint | null = null;
 
   /** 根据 game-area 的实际渲染尺寸同步 canvas 像素尺寸 */
   function syncCanvasSize() {
@@ -93,6 +94,7 @@ async function init() {
       canvas.width = w;
       canvas.height = h;
       scene?.updateViewport();
+      fingerHint?.refresh();
     }
   }
 
@@ -136,9 +138,6 @@ async function init() {
 
   let taskList: number[] = [];
 
-  // fingerHint 先声明,init 末尾赋值后回调中就能使用
-  let fingerHint: FingerHint | null = null;
-
   let fillerData = new FillerData(
     new FillerConfig(settings),
     resource,
@@ -189,7 +188,7 @@ async function init() {
 
   // canvas 区域与所有 UI 完全分离,只保留适当视觉边距
   // 乘以 pixelRatio 转换为设备像素,确保跨 DPI 设备视觉一致
-  const pad = 20 * pixelRatio;
+  const pad = 8 * pixelRatio;
   scene.setContentPadding(new Padding(pad, pad, pad, pad));
 
   //let page = await loadImage('/webgl/page.png')