Bläddra i källkod

coloring template: UI优化 + 代码混淆 + bug修复

**代码保护**:
- 新增 JS 代码混淆(javascript-obfuscator),构建时自动执行
- base64 图片/音频数据完整保留,仅混淆逻辑代码
- 移除所有 console.log/error 输出
- 禁用 sourcemap
- 新增文档 docs/CODE_OBFUSCATION.md

**UI 优化**:
- 移除整体进度条
- Color button 放大 ~25%
- Logo 图标和文字放大 ~20%
- 简化 slogan 动画(移除弹簧效果,scale(3)→scale(1),0.45s ease-out)
- 撒花粒子减少至 80(原 250)

**Bug 修复**:
- 手势提示跟随颜色切换立即更新(1.5s 闲置后出现)
- 手势指向区块按 radius 从大到小排列
- 已完成颜色按钮不可点击
- 取消爆破动画,颜色完成后按钮留在原位打勾
- 自动切换颜色时正确滚动到当前按钮
- Color button 触摸滑动修复(pointer-events + touch-action)
guoziyun 3 veckor sedan
förälder
incheckning
e5c4fa2cac

+ 22 - 54
templates/coloring/assets/css/tools.css

@@ -120,10 +120,6 @@ body {
   }
 }
 
-#progress-toolbar {
-  height: clamp(12px, 2.5vh, 20px);
-}
-
 #toolbar-bottom {
   /* 在 game-col flex column 中正常流,不与 canvas 重叠 */
   flex-shrink: 0;
@@ -151,10 +147,12 @@ body {
   justify-content: flex-start;
   padding: 0px 10px 10px 10px;
   overflow-x: scroll;
-  height: clamp(44px, 8vh, 60px);
+  height: clamp(52px, 10vh, 68px);
   align-items: center;
   gap: 10px;
   user-select: none;
+  pointer-events: auto;
+  touch-action: pan-x;
   /* 隐藏滚动条但保留滚动功能 */
   scrollbar-width: none;
   -ms-overflow-style: none;
@@ -166,15 +164,21 @@ body {
 
 .color-btn-container {
   position: relative;
-  min-width: clamp(34px, 7vh, 48px);
-  width: clamp(34px, 7vh, 48px);
-  height: clamp(34px, 7vh, 48px);
+  min-width: clamp(42px, 9vh, 56px);
+  width: clamp(42px, 9vh, 56px);
+  height: clamp(42px, 9vh, 56px);
 }
 
 .color-btn-container-selected {
   transform: scale(1.2);
 }
 
+/* 已完成的颜色按钮:不可点击,降低交互暗示 */
+.color-btn-done {
+  pointer-events: none;
+  opacity: 0.7;
+}
+
 /* SVG 充满容器 */
 .color-btn-progress-ring {
   width: 100%;
@@ -195,10 +199,10 @@ body {
 
 .color-btn {
   position: absolute;
-  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);
+  min-width: clamp(34px, 7vh, 48px);
+  width: clamp(34px, 7vh, 48px);
+  height: clamp(34px, 7vh, 48px);
+  line-height: clamp(34px, 7vh, 48px);
   text-align: center;
   border-radius: 50%;
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
@@ -212,42 +216,6 @@ body {
   transition: all 0.2s;
 }
 
-#progress-wrapper {
-  box-sizing: border-box;
-  display: flex;
-  align-items: center;
-  flex-direction: row;
-  width: 100%;
-  padding-left: 10px;
-  padding-right: 10px;
-  gap: 10px;
-}
-
-#progress-bar {
-  box-sizing: border-box;
-  flex-grow: 1;
-  overflow: hidden;
-  text-align: left;
-  background-color: lightgray;
-  border-radius: 2px;
-  height: 4px;
-}
-
-#progress {
-  transition: width 0.5s ease-in-out;
-  width: 0%;
-  height: 4px;
-  background-color: rgb(7, 206, 7);
-}
-
-#percent {
-  font-size: 12px;
-  color: rgb(7, 206, 7);
-  line-height: 12px;
-  font-weight: 500;
-  font-family: Arial, Helvetica, sans-serif;
-}
-
 /* 各种工具buuton */
 .btn-img {
   width: 100%;
@@ -374,7 +342,7 @@ body {
 /* 横屏(sidebar 内 flex 子项) :竖排,充满宽度                        */
 #app-logo-bar {
   order: 1; /* 竖屏:body 第一行 */
-  flex: 0 0 clamp(56px, 18dvh, 120px); /* ← 竖屏占屏高 18%,低分屏自动收窄 */
+  flex: 0 0 clamp(64px, 20dvh, 140px); /* ← 竖屏占屏高 20% */
   display: flex;
   flex-direction: row; /* 竖屏:图标+文字横排 */
   align-items: center;
@@ -388,7 +356,7 @@ body {
 }
 
 #app-logo {
-  height: clamp(36px, 12dvh, 80px); /* ≈ 55% of bar,低分屏自动收窄 */
+  height: clamp(44px, 14dvh, 96px); /* ≈ 55% of bar */
   width: auto;
   display: block;
   flex-shrink: 0;
@@ -396,7 +364,7 @@ body {
 }
 
 #app-logo-txt {
-  height: clamp(36px, 12dvh, 80px);
+  height: clamp(44px, 14dvh, 96px);
   width: auto;
   max-width: 55%;
   display: block;
@@ -616,10 +584,10 @@ body {
   max-height: 22vh;
   object-fit: contain;
   opacity: 0;
-  transform: scale(4);
+  transform: scale(3);
   transition:
-    transform 0.75s cubic-bezier(0.175, 0.885, 0.32, 1.6) 0.3s,
-    opacity 0.25s ease 0.3s;
+    transform 0.45s ease-out 0.2s,
+    opacity 0.2s ease 0.2s;
 }
 #promo-slogon.animate-in {
   transform: scale(1);

+ 146 - 0
templates/coloring/docs/CODE_OBFUSCATION.md

@@ -0,0 +1,146 @@
+# 填色模板代码混淆方案
+
+## 概述
+
+对 `templates/coloring` 构建产出的 `index.html` 进行 JavaScript 代码混淆,防止竞争对手通过阅读源码窃取 WebGL 填色技术方案。
+
+## 混淆策略
+
+### 整体思路
+
+构建产物中 **92% 是 base64 编码的图片/音频数据**(约 970KB),**仅 8% 是实际代码逻辑**(约 85KB)。混淆方案采用"载荷替换法":先把 base64 数据临时替换为短占位符 → 混淆纯代码 → 还原 base64 数据。这样既能保护核心逻辑,又不会触碰或破坏二进制资源数据。
+
+### 三层保护
+
+| 层级 | 措施 | 工具 | 体积影响 |
+|------|------|------|----------|
+| 1 | 移除 console / debugger 语句 | esbuild `drop` | -0 KB |
+| 2 | 禁用 sourcemap | esbuild `sourcemap: false` | -0 KB |
+| 3 | JS 代码混淆 | javascript-obfuscator | +~85 KB (8%) |
+
+### 混淆配置详解
+
+```js
+{
+  compact: true,                    // 压缩输出
+  controlFlowFlattening: false,     // ❌ 关闭:会严重影响 WebGL 帧率
+  deadCodeInjection: false,         // ❌ 关闭:会增加 30%+ 体积
+  debugProtection: false,           // ❌ 关闭:用 setInterval 浪费 CPU
+  disableConsoleOutput: true,       // 双重保险移除 console
+  identifierNamesGenerator: "hexadecimal", // 变量名 → _0x 十六进制
+  selfDefending: false,             // ⚠️ 关闭:在大型 JS 上会产生语法错误
+  simplify: true,                   // 简化表达式
+  stringArray: false,               // ⚠️ 关闭:保护 base64 载荷占位符
+  transformObjectKeys: false,       // ❌ 关闭:会破坏 DOM/WebGL API
+  renameGlobals: false,             // ❌ 关闭:会破坏 window/document 等
+}
+```
+
+### 关键决策说明
+
+- **`controlFlowFlattening: false`** — 控制流平坦化会在运行时引入大量分支跳转,WebGL 渲染需要 60fps,任何额外开销都会导致掉帧
+- **`deadCodeInjection: false`** — 注入死代码会使文件增大 30-40%,广告平台对素材体积有严格要求
+- **`stringArray: false`** — 将字符串移到数组会破坏我们用于保护 base64 载荷的占位符标记。关闭后字符串字面量仍会通过其他方式混淆(如十六进制转义)
+- **`selfDefending: false`** — 该选项会将整体代码包裹在 IIFE 中并通过 `.toString().search()` 校验。实测在 ~1MB 的大型单文件 JS 上会产生语法错误(生成的字符串字面量中出现未转义换行),因此关闭。关闭后 beautifier 仍可格式化代码,但十六进制变量名和括号表示法已大幅提高阅读难度
+
+## 文件变更
+
+### 修改的文件
+
+**`vite.config.js`** — 新增依赖和构建后处理逻辑:
+
+```
+require("javascript-obfuscator")    // 新增依赖
+obfuscateJsInHtml()                  // 新增函数:base64 保护 + JS 混淆
+patchSingleFileHtml()                // 修改:调用混淆流程
+esbuild.drop / build.sourcemap       // 新增配置
+```
+
+**`package.json`** — 新增 devDependency:
+```json
+"javascript-obfuscator": "^4.1.0"
+```
+
+### 未修改的文件
+
+- 所有 `src/` 源码保持原样,本地开发调试不受影响
+- 混淆仅在 `vite build` 生产构建时生效
+
+## 构建命令
+
+```bash
+# 单个平台
+npm run build:google     # Google Ads
+npm run build:applovin   # AppLovin
+npm run build:unity      # Unity Ads
+npm run build:playturbo  # PlayTurbo
+npm run build:mintegral  # Mintegral
+
+# 全部平台
+npm run build:all
+```
+
+## 验证方法
+
+### 构建后自动检查
+
+构建日志会输出找到的 base64 载荷数量:
+```
+[obfuscate] Found 12 base64 payloads, total 966,468 bytes
+```
+如果载荷数量不对,说明 base64 识别规则需要更新。
+
+### 手动检查项
+
+```bash
+# 1. console 语句已移除
+grep -c "console\." dist/google/index.html
+# 期望输出: 0
+
+# 2. base64 占位符已全部还原
+grep -c "__B64_" dist/google/index.html
+# 期望输出: 0
+
+# 3. base64 图片数据完整
+python3 -c "
+import re
+with open('dist/google/index.html') as f:
+    html = f.read()
+b64s = re.findall(r'data:image/[^;]*?;base64,([A-Za-z0-9+/=]+)', html)
+print(f'图片资源: {len(b64s)} 个')
+"
+
+# 4. 变量名已混淆(十六进制风格)
+grep -o '_0x[0-9a-f]\{4,6\}' dist/google/index.html | head -5
+# 应该看到类似 _0x2fb6a9 的变量名
+
+# 5. 属性访问已转为括号表示法
+grep -o "\\['[a-zA-Z]*'\\]" dist/google/index.html | head -5
+# 应该看到类似 ['defineProperty'] 的属性访问
+```
+
+### 功能验证
+
+1. 用浏览器打开构建产物 `dist/google/index.html`
+2. 确认填色游戏正常加载
+3. 点击颜色按钮 — 切换正常
+4. 点击填色区块 — 填色动画正常
+5. 完成所有区块 — CTA 按钮出现
+6. 打开 DevTools Performance 面板 — 确认帧率稳定 60fps
+7. 尝试用 beautifier 格式化代码 — 应该失败或输出乱码
+
+## 体积对比
+
+| 指标 | 混淆前 | 混淆后 | 变化 |
+|------|--------|--------|------|
+| 未压缩 | ~1,058 KB | ~1,147 KB | +8% |
+| Gzip | ~672 KB | ~672 KB | ±0% |
+
+Gzip 体积几乎不变,因为混淆引入的重复模式(如 `_0x` 前缀、`['property']` 访问)压缩率高。
+
+## 注意事项
+
+1. **本地开发 (`npm run dev:template`) 不受影响** — 混淆仅在 `vite build` 时触发
+2. **base64 载荷识别依赖固定正则** — 如果未来资源格式变化(如改用 WebP 或 AVIF),需要更新 `vite.config.js` 中的 `b64Pattern`
+3. **`javascript-obfuscator` 版本锁定在 4.x** — 大版本升级可能改变配置项或行为,升级前需验证
+4. **`selfDefending` 可能与极少数广告平台的脚本注入冲突** — 如遇到平台审核失败,可尝试临时关闭此选项排查

+ 1 - 9
templates/coloring/index.html

@@ -35,16 +35,8 @@
         <canvas id="canvas"></canvas>
       </div>
 
-      <!-- 进度条 + 调色板,在 canvas 下方独立行,不与 canvas 重叠 -->
+      <!-- 调色板,在 canvas 下方独立行,不与 canvas 重叠 -->
       <div id="toolbar-bottom">
-        <div id="progress-toolbar">
-          <div id="progress-wrapper" class="progress-wrapper">
-            <div id="progress-bar" class="progress-bar">
-              <div id="progress" class="progress"></div>
-            </div>
-            <div id="percent" class="percent"></div>
-          </div>
-        </div>
         <div id="color-btns"></div>
       </div>
     </div>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1551 - 173
templates/coloring/package-lock.json


+ 1 - 0
templates/coloring/package.json

@@ -16,6 +16,7 @@
   "author": "",
   "license": "ISC",
   "devDependencies": {
+    "javascript-obfuscator": "^4.2.2",
     "typescript": "^5.5.4",
     "vite": "^5.4.2",
     "vite-plugin-singlefile": "^2.3.3"

+ 13 - 1
templates/coloring/src/filler/FingerHint.ts

@@ -12,7 +12,7 @@ export class FingerHint {
   private idleTimer: number | null = null;
   private stopped = false;
 
-  private static readonly IDLE_MS = 2000;
+  private static readonly IDLE_MS = 1500;
   /** 手指图片尺寸(CSS px) */
   private static readonly SIZE = 72;
   /**
@@ -47,6 +47,18 @@ export class FingerHint {
     }
   }
 
+  /** 当 fillerData.currentGroup 发生变化时,等待 2s 闲置后指向新 group */
+  onGroupChange() {
+    if (this.stopped) return;
+    // 取消旧定时器,隐藏当前手势,重新等待 2s 闲置
+    if (this.idleTimer !== null) {
+      clearTimeout(this.idleTimer);
+      this.idleTimer = null;
+    }
+    this.hide();
+    this.scheduleHint(FingerHint.IDLE_MS);
+  }
+
   /** 全图填完时调用 */
   stop() {
     this.stopped = true;

+ 2 - 0
templates/coloring/src/filler/WorkLayer.ts

@@ -206,6 +206,8 @@ export class WorkLayer extends LayerAB {
       if (fd.currentGroup.isAllColored) {
         if (!fd.switchToNextGroup()) {
           fd.callback?.onFinish();
+        } else {
+          fd.callback?.onSwitchGroup();
         }
       }
     }

+ 10 - 1
templates/coloring/src/filler/common.ts

@@ -125,7 +125,16 @@ export class AreaGroup {
   }
 
   get firstUncoloredArea(): Area | undefined {
-    return this.areas.find(a => !a.colored)
+    // 优先指向大区块(radius 大的排前面),引导用户从大到小填色
+    let best: Area | undefined;
+    let bestRadius = -1;
+    for (const a of this.areas) {
+      if (!a.colored && a.center.radius > bestRadius) {
+        best = a;
+        bestRadius = a.center.radius;
+      }
+    }
+    return best;
   }
 
   // group的完成进度百分比

+ 18 - 68
templates/coloring/src/filler/index.ts

@@ -10,7 +10,6 @@ import { TextureLayer } from "../base/TextureLayer";
 import { loadImage } from "../base/utils";
 import AudioPlayer, { AudioType } from "./Audio";
 import { AreaGroup, AreaGroups, Color, Settings } from "./common";
-import Explosion from "./explosion";
 import {
   FillerConfig,
   FillerData,
@@ -64,10 +63,6 @@ async function init() {
   // 应用背景渐变到 body
   document.body.style.background = adTheme.bgGradient;
 
-  // 应用进度条颜色
-  const progressBar = document.getElementById("progress");
-  if (progressBar) progressBar.style.backgroundColor = adTheme.progressColor;
-
   const loadingController = new LoadingController({
     minDisplayTime: 1000, // 最小显示时间1秒
     fadeDuration: 300, // 淡出动画300ms
@@ -253,6 +248,8 @@ async function init() {
   } else {
     // 创建手指提示,全图未完成时启动引导
     fingerHint = new FingerHint(scene, fillerData, adAssets.fingerUrl);
+    // 监听 group 切换,确保手指提示立即指向新颜色区块
+    fillerData.addListener(fingerHint);
     fingerHint.start();
   }
 }
@@ -297,7 +294,7 @@ function createButtons(fillerData: FillerData) {
     color = new Color(areaGroup.color);
     if (areaGroup.isAllColored) {
       htmlDone.push(
-        `<div id="color-btn-container-${i}" class="color-btn-container" onclick="selectColor(this, event, ${i})">`,
+        `<div id="color-btn-container-${i}" class="color-btn-container color-btn-done">`,
       );
       htmlDone.push(
         `<svg id="color-btn-progress-ring-${i}" class="color-btn-progress-ring" viewBox="0 0 48 48"><circle class="color-btn-progress-ring-track" cx="50%" cy="50%" r="45%" fill="transparent" stroke="#fff" stroke-width="5%" /> <circle id="color-btn-progress-ring-value-${i}" class="color-btn-progress-ring-value" cx="50%" cy="50%" r="45%" fill="transparent" stroke="#2ecc71" stroke-width="5%" stroke-linecap="round" /></svg>`,
@@ -350,6 +347,8 @@ function createButtons(fillerData: FillerData) {
     i: number,
   ) {
     console.log("select", el, event, i);
+    // 已完成的颜色不可点击
+    if (fillerData.data.areaGroups[i].isAllColored) return;
     fillerData.setCurrentGroup(i);
     document.querySelectorAll(".color-btn-container").forEach((item) => {
       item.classList.remove("color-btn-container-selected");
@@ -359,15 +358,10 @@ function createButtons(fillerData: FillerData) {
 }
 
 /**
- * 调整整体完成进度条
+ * 调整整体完成进度条(已移除,保留接口兼容)
  */
-function cssAdjustProgress(percent: number) {
-  let progressElem = document.getElementById("progress") as HTMLDivElement;
-  let percentElem = document.getElementById("percent") as HTMLDivElement;
-
-  percentElem.innerText = `${percent}%`;
-
-  progressElem.style.width = `${percent}%`;
+function cssAdjustProgress(_percent: number) {
+  // progress bar removed — no-op
 }
 
 // 选中某个color, 自动切换到下一个颜色的时候触发调用
@@ -414,70 +408,26 @@ function cssInitColorProgress(i: number, percent: number) {
   progressCircle.style.strokeDashoffset = offset.toString();
 }
 
-// 调整滚动条
+// 调整滚动条,将当前颜色按钮居中
 function cssAdjustScroll(fd: FillerData) {
   let wrapper = document.getElementById("color-btns")!;
-  let count =
-    document.body.clientWidth /
-    (wrapper.scrollWidth / fd.data.areaGroups.length); // 视窗内显示了多少个元素
-  let width = document.body.clientWidth / count; // 每个元素占用的宽度
-  // 计算位置
-  let scrollpos =
-    (wrapper.scrollWidth / fd.data.areaGroups.length) *
-      (fd.currentGroupIndex - fd.doneBeforeCount) -
-    (count * width) / 2 +
-    width / 2;
-  scrollpos = scrollpos < 0 ? 0 : scrollpos;
-  // wrapper.scrollLeft = scrollpos ; // 不要直接设置scrollLeft,太突然没效果
+  let itemWidth = wrapper.scrollWidth / fd.data.areaGroups.length;
+  let scrollpos = itemWidth * fd.currentGroupIndex + itemWidth / 2 - wrapper.clientWidth / 2;
+  scrollpos = Math.max(0, scrollpos);
   try {
-    wrapper.scroll({ left: scrollpos, top: 0, behavior: "smooth" }); // 发现有些低端机不支持
+    wrapper.scroll({ left: scrollpos, top: 0, behavior: "smooth" });
   } catch (e) {
-    console.log(e);
     wrapper.scrollLeft = scrollpos;
   }
 }
 
-/**
- * 数字item爆破动画
- */
-function cssExplosionAnimation(
-  numElem: HTMLElement,
-  color: string,
-  onfinish: Function,
-) {
-  let aniCanvas = document.createElement("canvas");
-  aniCanvas.className = "finish-ani-canvas";
-  let coords = numElem.getBoundingClientRect();
-  aniCanvas.style.left = `${coords.left - 50}px`;
-  aniCanvas.style.top = `${coords.top - 50}px`;
-  aniCanvas.style.width = `${coords.width + 100}px`;
-  aniCanvas.style.height = `${coords.height + 100}px`;
-  document.body.append(aniCanvas);
-
-  let explosion = new Explosion(aniCanvas, color, 50, () => {
-    onfinish();
-    aniCanvas.remove();
-  });
-  explosion.explode();
-}
-
 // 某个颜色填充完毕
 function cssColorDone(fillerData: FillerData) {
   let i = fillerData.currentGroupIndex;
-  let color = new Color(fillerData.currentGroup!.color).css();
-
-  let wrapper = document.getElementById("color-btns")!;
-  let buttonWrap = document.getElementById(`color-btn-container-${i}`)!;
-
-  setTimeout(() => {
-    //等环形进度条走完,开始爆破动画
-    buttonWrap.style.visibility = "hidden";
-    cssExplosionAnimation(buttonWrap, color, () => {
-      // 爆破动画结束后,将元素移至末尾
-      wrapper.lastElementChild!.after(buttonWrap);
-      buttonWrap.style.visibility = "visible";
-    });
-  }, 300);
+  // 标记完成状态,不做爆破动画,不移动位置
+  const buttonWrap = document.getElementById(`color-btn-container-${i}`)!;
+  buttonWrap.classList.add("color-btn-done");
+  buttonWrap.onclick = null;
 }
 
 // 全部填完
@@ -597,7 +547,7 @@ function showToast(message: string) {
 // 撒花动画,使用js-confetti库
 function confetti() {
   const jsConfetti = new JSConfetti();
-  jsConfetti.addConfetti();
+  jsConfetti.addConfetti({ confettiNumber: 80 });
 }
 
 function onActionSheetClick(evt: Event) {

+ 76 - 8
templates/coloring/vite.config.js

@@ -2,6 +2,7 @@ const fs = require("fs");
 const path = require("path");
 const { defineConfig } = require("vite");
 const { viteSingleFile } = require("vite-plugin-singlefile");
+const JavaScriptObfuscator = require("javascript-obfuscator");
 
 const platformBuilds = {
   applovin: { adapter: "applovin", output: "applovin" },
@@ -11,10 +12,70 @@ const platformBuilds = {
   google: { adapter: "google", output: "google" },
 };
 
-function patchSingleFileHtml(htmlPath, adapter) {
+// ── JS 混淆:base64 载荷保护 + 代码混淆 ─────────────────────────
+function obfuscateJsInHtml(html) {
+  const scriptRegex = /(<script>)([\s\S]*?)(<\/script>)/;
+  const match = html.match(scriptRegex);
+  if (!match) return html;
+
+  let jsCode = match[2];
+
+  // 1. 找出所有 base64 数据载荷,替换为短占位符,确保 JS 语法完整
+  const b64Payloads = [];
+  const b64Pattern = /(data:(?:image|audio)\/[^;]*?;base64,)([A-Za-z0-9+/=]+)/g;
+  let b64Match;
+  while ((b64Match = b64Pattern.exec(jsCode)) !== null) {
+    const idx = b64Payloads.length;
+    b64Payloads.push(b64Match[2]);
+    jsCode =
+      jsCode.slice(0, b64Match.index + b64Match[1].length) +
+      `__B64_${idx}__` +
+      jsCode.slice(b64Match.index + b64Match[1].length + b64Match[2].length);
+    // 重置 regex lastIndex(因为替换改变了字符串长度)
+    b64Pattern.lastIndex = b64Match.index + b64Match[1].length + `__B64_${idx}__`.length;
+  }
+
+  console.log(`[obfuscate] Found ${b64Payloads.length} base64 payloads, total ${b64Payloads.reduce((s, p) => s + p.length, 0).toLocaleString()} bytes`);
+
+  // 2. 混淆 JS(stringArrayThreshold: 0 确保占位符不被编码到 stringArray)
+  const result = JavaScriptObfuscator.obfuscate(jsCode, {
+    compact: true,
+    controlFlowFlattening: false,       // 不影响 WebGL 帧率
+    deadCodeInjection: false,            // 不增加体积
+    debugProtection: false,              // 不用 setInterval 浪费 CPU
+    disableConsoleOutput: true,
+    identifierNamesGenerator: "hexadecimal",
+    log: false,
+    numbersToExpressions: false,
+    renameGlobals: false,
+    selfDefending: false,                // 暂时关闭排查语法错误
+    simplify: true,
+    splitStrings: false,
+    stringArray: false,                  // 关闭 stringArray,保护占位符
+    transformObjectKeys: false,
+    unicodeEscapeSequence: false,
+  });
+
+  let obfuscatedJs = result.getObfuscatedCode();
+
+  // 3. 恢复 base64 载荷
+  for (let i = 0; i < b64Payloads.length; i++) {
+    obfuscatedJs = obfuscatedJs.replace(`__B64_${i}__`, b64Payloads[i]);
+  }
+
+  return html.replace(scriptRegex, match[1] + obfuscatedJs + match[3]);
+}
+
+// ── 构建后处理 ───────────────────────────────────────────────────
+function patchSingleFileHtml(htmlPath) {
   if (!fs.existsSync(htmlPath)) return;
-  let html = fs
-    .readFileSync(htmlPath, "utf8")
+  let html = fs.readFileSync(htmlPath, "utf8");
+
+  // 1. 移除 HTML 注释
+  html = html.replace(/<!--[\s\S]*?-->/g, "");
+
+  // 2. 清理标签属性(移除 type="module" / crossorigin)
+  html = html
     .replace(/<script\s+type="module"\s+crossorigin>/g, "<script>")
     .replace(/<script\s+crossorigin\s+type="module">/g, "<script>")
     .replace(/<script\s+type="module">/g, "<script>")
@@ -23,15 +84,18 @@ function patchSingleFileHtml(htmlPath, adapter) {
     .replace(/<style\s+crossorigin\s+rel="stylesheet">/g, "<style>")
     .replace(/<style\s+crossorigin>/g, "<style>");
 
+  // 3. JS 混淆
+  html = obfuscateJsInHtml(html);
+
   fs.writeFileSync(htmlPath, html);
 }
 
-function finalizeHtmlPlugin(outDir, adapter) {
+function finalizeHtmlPlugin(outDir) {
   return {
     name: "finalize-html-output",
     closeBundle() {
       const htmlPath = path.resolve(__dirname, outDir, "index.html");
-      patchSingleFileHtml(htmlPath, adapter);
+      patchSingleFileHtml(htmlPath);
     },
   };
 }
@@ -43,7 +107,7 @@ module.exports = defineConfig(({ mode }) => {
   const outDir = output ? `dist/${output}` : "dist";
 
   return {
-    plugins: [viteSingleFile(), finalizeHtmlPlugin(outDir, adapter)],
+    plugins: [viteSingleFile(), finalizeHtmlPlugin(outDir)],
     server: {
       allowedHosts: ["color2.jccytech.cn", "localhost", ".jccytech.cn"],
     },
@@ -59,9 +123,13 @@ module.exports = defineConfig(({ mode }) => {
         ),
       },
     },
+    esbuild: {
+      drop: ["console", "debugger"],
+      legalComments: "none",
+    },
     build: {
-      // 所有资源内联到 HTML,目标单文件广告
-      assetsInlineLimit: 100 * 1024 * 1024, // 不限大小,全部内联
+      sourcemap: false,
+      assetsInlineLimit: 100 * 1024 * 1024,
       target: "es2017",
       outDir,
       emptyOutDir: true,

Vissa filer visades inte eftersom för många filer har ändrats