Surf Studio commited on
Commit
94b4ce6
·
1 Parent(s): 656e35a

Surf Mystery Box 赠链追踪:X + Telegram 采集、名额检查、企业微信推送

Browse files

- 清空原有代码,替换为 Surf 赠链追踪应用
- 前端 Vite + React + Tailwind,后端 Express(Surf SDK)
- 每 5 分钟检查新消息与赠链名额,发现可用赠链推送到企业微信
- Telegram 支持手机号登录读取私有频道;X 支持登录后实时搜索

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +9 -0
  2. .gitattributes +0 -11
  3. BeadQR/README.md +0 -50
  4. BeadQR/cloudfunctions/quickstartFunctions/config.json +0 -9
  5. BeadQR/cloudfunctions/quickstartFunctions/index.js +0 -92
  6. BeadQR/cloudfunctions/quickstartFunctions/package-lock.json +0 -1157
  7. BeadQR/cloudfunctions/quickstartFunctions/package.json +0 -14
  8. BeadQR/docs/promo/douwei-1080-cover.png +0 -3
  9. BeadQR/docs/promo/douwei-1080.mp4 +0 -3
  10. BeadQR/docs/promo/douwei-720-cover.png +0 -3
  11. BeadQR/docs/promo/douwei-720.mp4 +0 -3
  12. BeadQR/docs/source-photo.jpeg +0 -3
  13. BeadQR/docs/video-script.md +0 -116
  14. BeadQR/docs/审核驳回处理-文案与步骤.md +0 -58
  15. BeadQR/miniprogram/app.js +0 -14
  16. BeadQR/miniprogram/app.json +0 -19
  17. BeadQR/miniprogram/app.wxss +0 -37
  18. BeadQR/miniprogram/assets/icon.png +0 -0
  19. BeadQR/miniprogram/assets/miniprogram-code.png +0 -0
  20. BeadQR/miniprogram/assets/shield.svg +0 -1
  21. BeadQR/miniprogram/components/privacy-popup/index.js +0 -40
  22. BeadQR/miniprogram/components/privacy-popup/index.json +0 -4
  23. BeadQR/miniprogram/components/privacy-popup/index.wxml +0 -13
  24. BeadQR/miniprogram/components/privacy-popup/index.wxss +0 -10
  25. BeadQR/miniprogram/pages/index/index.js +0 -1073
  26. BeadQR/miniprogram/pages/index/index.json +0 -6
  27. BeadQR/miniprogram/pages/index/index.wxml +0 -403
  28. BeadQR/miniprogram/pages/index/index.wxss +0 -828
  29. BeadQR/miniprogram/sitemap.json +0 -7
  30. BeadQR/miniprogram/utils/color.js +0 -46
  31. BeadQR/miniprogram/utils/engine.js +0 -225
  32. BeadQR/miniprogram/utils/imaging.js +0 -95
  33. BeadQR/miniprogram/utils/intro.js +0 -703
  34. BeadQR/miniprogram/utils/palette-data.js +0 -48
  35. BeadQR/miniprogram/utils/palette.js +0 -74
  36. BeadQR/miniprogram/utils/pattern.js +0 -381
  37. BeadQR/miniprogram/utils/pdf.js +0 -373
  38. BeadQR/miniprogram/utils/qr.js +0 -58
  39. BeadQR/miniprogram/utils/qrcore/alignment-pattern.js +0 -83
  40. BeadQR/miniprogram/utils/qrcore/alphanumeric-data.js +0 -59
  41. BeadQR/miniprogram/utils/qrcore/bit-buffer.js +0 -37
  42. BeadQR/miniprogram/utils/qrcore/bit-matrix.js +0 -65
  43. BeadQR/miniprogram/utils/qrcore/byte-data.js +0 -61
  44. BeadQR/miniprogram/utils/qrcore/dijkstrajs.js +0 -165
  45. BeadQR/miniprogram/utils/qrcore/error-correction-code.js +0 -135
  46. BeadQR/miniprogram/utils/qrcore/error-correction-level.js +0 -50
  47. BeadQR/miniprogram/utils/qrcore/finder-pattern.js +0 -22
  48. BeadQR/miniprogram/utils/qrcore/format-info.js +0 -29
  49. BeadQR/miniprogram/utils/qrcore/galois-field.js +0 -69
  50. BeadQR/miniprogram/utils/qrcore/kanji-data.js +0 -54
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ **/node_modules
3
+ public
4
+ frontend/dist
5
+ frontend/node_modules
6
+ .vulcan
7
+ .vulcan-error-reporter.js
8
+ *.log
9
+ .git
.gitattributes DELETED
@@ -1,11 +0,0 @@
1
- *.mp4 filter=lfs diff=lfs merge=lfs -text
2
- *.MP4 filter=lfs diff=lfs merge=lfs -text
3
- *.mov filter=lfs diff=lfs merge=lfs -text
4
- *.MOV filter=lfs diff=lfs merge=lfs -text
5
- *.mp3 filter=lfs diff=lfs merge=lfs -text
6
- *.png filter=lfs diff=lfs merge=lfs -text
7
- *.PNG filter=lfs diff=lfs merge=lfs -text
8
- *.jpg filter=lfs diff=lfs merge=lfs -text
9
- *.webp filter=lfs diff=lfs merge=lfs -text
10
- *.HEIC filter=lfs diff=lfs merge=lfs -text
11
- BeadQR/docs/source-photo.jpeg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/README.md DELETED
@@ -1,50 +0,0 @@
1
- # 豆维 · 可扫码的拼豆
2
-
3
- 输入一句话(可以再选一张照片),生成**用微信扫一扫就能读出内容**的 MARD 拼豆图纸,并支持导出 PDF 或图片。
4
-
5
- ## 本版重点
6
-
7
- - **一眼看懂**:首屏直接说明用途,页面按“写一句话 → 选张照片 → 导出图纸”三步走,打开就能看到示例效果。
8
- - **更快**:配色查表加速,色格数据不再走页面数据通道;PDF 改为矢量直出(毫秒级,文件更小);导出时不再重复绘制。
9
- - **更好看、更好扫**:
10
- - 感知更准的配色(OKLab)
11
- - 照片按面积取平均色,不再有锯齿
12
- - 相邻色块自动平滑,零散颜色自动合并,所需豆子种类更少
13
- - 二维码定位图形强制高对比
14
- - 本机扫码自检:扫不出时一键优化
15
- - **打印友好**:PDF 为 A4 矢量图纸,大作品会自动分块并附总览页;每一格都印有色号,另附带用量的清单。
16
- - **精选色板**:除标准 221 色和完整 291 色外,还提供 144/96/72/48/24 色精选套装,方便少买豆。
17
-
18
- ## 内容安全链路(按微信平台要求)
19
-
20
- | 环节 | 措施 |
21
- | --- | --- |
22
- | 二维码文字、扫码导入的文字、水印 | 云函数调用 `security.msgSecCheck` v2(带 openid、按场景值检测),结果为 risky 或 review 均拦截 |
23
- | 照片 | 选图后立即压缩到官方推荐的 750×1334 以内,由云函数调用 `security.imgSecCheck`;**检测通过前不能应用照片** |
24
- | 检测异常 | 一律视为未通过,不允许生成与导出(fail-closed) |
25
- | 导出前 | 再次校验全部文本(已通过的走缓存,不增加等待) |
26
- | 云函数加固 | 必须有 OPENID;只接受 `sec-check/` 目录下的 jpg;通过文件头判断格式;限制 1MB;检测后立即删除云端临时文件 |
27
- | 隐私 | 已开启 `__usePrivacyCheck__`,内置隐私授权弹窗;页脚设有“隐私保护指引”入口 |
28
- | 投诉与反馈 | 页脚提供“内容规范”说明及 `open-type="feedback"` 的投诉反馈入口 |
29
- | 分享 | 分享出去的只是小程序卡片,不带用户内容 |
30
-
31
- ## 部署步骤
32
-
33
- 1. 用微信开发者工具打开本目录(appid 已配置)。
34
- 2. 右键 `cloudfunctions/quickstartFunctions` →「上传并部署:云端安装依赖」。**本版云函数有改动,必须重新部署。**
35
- 3. 在云开发控制台 → 存储中确认可以写入 `sec-check/` 目录(默认权限即可)。
36
- 4. 在小程序管理后台 →「设置 → 服务内容声明 → 用户隐私保护指引」中声明:选择图片/拍照(`chooseMedia`)、保存到相册(`saveImageToPhotosAlbum`)、扫码(`scanCode`)。
37
- 5. 真机测试:正常/违规文字、正常/违规图片、断网时的检测异常,确认都会被正确拦截和提示。
38
-
39
- ## 说明
40
-
41
- - 为保证扫码成功率,**二维码默认保留 1 格白边**。如需原来的“无白边”效果,可在「高级设置 → 白边」中选择“无”。
42
- - 圆豆、中空豆样式只影响图纸的显示效果,实际拼出来的位置与方豆完全一致。
43
- - 第三方代码:`utils/qrcore`(node-qrcode,MIT)、`utils/vendor/jsqr.js`(jsQR,Apache-2.0)。
44
-
45
- ## 首屏演示动画 & 宣传视频
46
-
47
- - 首次打开(还没写内容)时,预览区播放 12 秒 1:1 演示动画:写一句话 → 编成二维码 → 融进照片 → 微信一扫 → 结尾卡片。
48
- - 动画由 `miniprogram/utils/intro.js` 纯代码实时绘制,**不含任何视频/图片素材,几乎不占包体积**(约 20KB)。演示作品由真实配色引擎生成,画面里的二维码真的能扫出 “Happy Birthday!”。
49
- - 滑出屏幕、切到后台、打开裁剪时自动暂停;点一下预览区重新播放;写下内容(或选了照片)后自动切回真实二维码。
50
- - `docs/promo/` 里是用同一套代码渲染的宣传视频(1080 / 720,含原创合成配乐)和封面图,可直接发公众号。该目录不在 `miniprogram/` 内,不计入小程序包体积。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/cloudfunctions/quickstartFunctions/config.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "timeout": 20,
3
- "permissions": {
4
- "openapi": [
5
- "security.msgSecCheck",
6
- "security.imgSecCheck"
7
- ]
8
- }
9
- }
 
 
 
 
 
 
 
 
 
 
BeadQR/cloudfunctions/quickstartFunctions/index.js DELETED
@@ -1,92 +0,0 @@
1
- // 豆维 · 内容安全云函数
2
- // type: checkMessage | checkMessages | checkImage
3
- // 返回约定:{ risk: boolean, skipped?: boolean };skipped 表示检测未能完成,前端必须拦截
4
- const cloud = require("wx-server-sdk");
5
- cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
6
-
7
- const MAX_TEXT = 2500;
8
- const MAX_IMAGE_BYTES = 1024 * 1024;
9
- const MAX_BATCH = 5;
10
-
11
- const extractErrCode = (error) => {
12
- if (!error) return undefined;
13
- if (typeof error.errCode === "number") return error.errCode;
14
- if (typeof error.code === "number") return error.code;
15
- const match = typeof error.errMsg === "string" ? error.errMsg.match(/-?\d{3,}/) : null;
16
- return match ? Number(match[0]) : undefined;
17
- };
18
-
19
- // msgSecCheck v2:scene 1 资料 / 2 评论 / 3 论坛 / 4 社交日志
20
- async function checkOneMessage(content, scene, openid) {
21
- const value = String(content || "").slice(0, MAX_TEXT);
22
- if (!value.trim()) return { risk: false };
23
- const safeScene = [1, 2, 3, 4].includes(Number(scene)) ? Number(scene) : 2;
24
- try {
25
- const response = await cloud.openapi.security.msgSecCheck({
26
- version: 2,
27
- scene: safeScene,
28
- openid,
29
- content: value,
30
- });
31
- const suggest = response && response.result && response.result.suggest;
32
- if (!suggest) return { risk: false, skipped: true };
33
- // review(疑似)按违规处理,宁严勿松
34
- return { risk: suggest === "risky" || suggest === "review", verdict: suggest };
35
- } catch (e) {
36
- if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" };
37
- return { risk: false, skipped: true, errCode: extractErrCode(e) };
38
- }
39
- }
40
-
41
- // 通过文件头判断真实格式,不信任客户端传入的类型
42
- function sniffType(buffer) {
43
- if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return "image/jpeg";
44
- if (buffer.length > 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return "image/png";
45
- return "";
46
- }
47
-
48
- async function checkImage(fileID) {
49
- // 只允许检测本小程序上传到 sec-check/ 目录的临时文件,防止借接口删除/读取其他文件
50
- if (typeof fileID !== "string" || !/^cloud:\/\/[^/]+\/sec-check\/[\w.-]+\.jpg$/.test(fileID)) {
51
- return { risk: false, skipped: true, reason: "INVALID_FILE" };
52
- }
53
- try {
54
- const download = await cloud.downloadFile({ fileID });
55
- const buffer = download && download.fileContent;
56
- const contentType = buffer ? sniffType(buffer) : "";
57
- if (!buffer || !buffer.length || buffer.length > MAX_IMAGE_BYTES || !contentType) {
58
- return { risk: false, skipped: true, reason: "BAD_IMAGE" };
59
- }
60
- await cloud.openapi.security.imgSecCheck({
61
- media: { contentType, value: buffer },
62
- });
63
- return { risk: false };
64
- } catch (e) {
65
- if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" };
66
- return { risk: false, skipped: true, errCode: extractErrCode(e) };
67
- } finally {
68
- try {
69
- await cloud.deleteFile({ fileList: [fileID] });
70
- } catch (e) {
71
- // 清理失败不影响检测结果
72
- }
73
- }
74
- }
75
-
76
- exports.main = async (event) => {
77
- const { OPENID } = cloud.getWXContext();
78
- if (!OPENID) return { risk: false, skipped: true, reason: "NO_OPENID" };
79
- const type = event && event.type;
80
- if (type === "checkMessage") {
81
- return checkOneMessage(event.content, event.scene, OPENID);
82
- }
83
- if (type === "checkMessages") {
84
- const items = Array.isArray(event.items) ? event.items.slice(0, MAX_BATCH) : [];
85
- const results = await Promise.all(items.map((it) => checkOneMessage(it && it.value, it && it.scene, OPENID)));
86
- return { results };
87
- }
88
- if (type === "checkImage") {
89
- return checkImage(event.fileID);
90
- }
91
- return { risk: false, skipped: true, reason: "UNKNOWN_TYPE" };
92
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/cloudfunctions/quickstartFunctions/package-lock.json DELETED
@@ -1,1157 +0,0 @@
1
- {
2
- "name": "quickstartFunctions",
3
- "version": "1.0.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "quickstartFunctions",
9
- "version": "1.0.0",
10
- "license": "ISC",
11
- "dependencies": {
12
- "wx-server-sdk": "~4.0.2"
13
- }
14
- },
15
- "node_modules/@cloudbase/adapter-interface": {
16
- "version": "0.7.1",
17
- "resolved": "https://registry.npmjs.org/@cloudbase/adapter-interface/-/adapter-interface-0.7.1.tgz",
18
- "integrity": "sha512-BjHTAvIMSLxbp26SeT6qLiSSBygz3kAVno/PMwwBwjgzVkVUvuZGVcvf1mhHxY/BjdRGLrd6X60OgvymV/aAUw==",
19
- "license": "ISC"
20
- },
21
- "node_modules/@cloudbase/adapter-wx_mp": {
22
- "version": "1.3.1",
23
- "resolved": "https://registry.npmjs.org/@cloudbase/adapter-wx_mp/-/adapter-wx_mp-1.3.1.tgz",
24
- "integrity": "sha512-rS0N1aD1a0zc421ds17/HzM4RChQ6T2ixMz87bhPJ1yveqJt/KUqYxiXJDWhZ803wwTmJs8aqNkgugsNusNhbg==",
25
- "license": "ISC",
26
- "dependencies": {
27
- "@cloudbase/adapter-interface": "^0.7.0",
28
- "web-streams-polyfill": "^4.0.0"
29
- }
30
- },
31
- "node_modules/@cloudbase/ai": {
32
- "version": "2.30.0",
33
- "resolved": "https://registry.npmjs.org/@cloudbase/ai/-/ai-2.30.0.tgz",
34
- "integrity": "sha512-RtBux47omXWcMhiiDDVJ96d2JlqnQ/bzKC6GG3ROgupYG3ReQyv1IssCpg5cnzmZ+l651IqdOpt27VTuO3kEGQ==",
35
- "license": "Apache-2.0",
36
- "dependencies": {
37
- "@cloudbase/types": "2.30.0",
38
- "@cloudbase/utilities": "2.30.0",
39
- "@mattiasbuelens/web-streams-adapter": "^0.1.0",
40
- "text-encoding-shim": "^1.0.5",
41
- "web-streams-polyfill": "^4.0.0"
42
- }
43
- },
44
- "node_modules/@cloudbase/ai/node_modules/@cloudbase/types": {
45
- "version": "2.30.0",
46
- "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz",
47
- "integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==",
48
- "license": "Apache-2.0",
49
- "dependencies": {
50
- "@cloudbase/adapter-interface": "^0.7.1"
51
- }
52
- },
53
- "node_modules/@cloudbase/app": {
54
- "version": "2.32.0",
55
- "resolved": "https://registry.npmjs.org/@cloudbase/app/-/app-2.32.0.tgz",
56
- "integrity": "sha512-78+eSo8OLO4iUiifdynZo4dK4wNlyaz5x9423muSibyl4FLwt5/Xt0t9s7StFmGaBdvB75UYM9dGWxOdIFRzaw==",
57
- "license": "Apache-2.0",
58
- "dependencies": {
59
- "@cloudbase/adapter-interface": "^0.7.1",
60
- "@cloudbase/adapter-wx_mp": "^1.3.1",
61
- "@cloudbase/types": "2.32.0",
62
- "@cloudbase/utilities": "2.32.0"
63
- },
64
- "peerDependencies": {
65
- "@cloudbase/signature-nodejs": ">=1.0.0",
66
- "jsonwebtoken": ">=8.0.0",
67
- "ws": "^8.18.0"
68
- },
69
- "peerDependenciesMeta": {
70
- "@cloudbase/signature-nodejs": {
71
- "optional": true
72
- },
73
- "jsonwebtoken": {
74
- "optional": true
75
- },
76
- "ws": {
77
- "optional": true
78
- }
79
- }
80
- },
81
- "node_modules/@cloudbase/app/node_modules/@cloudbase/utilities": {
82
- "version": "2.32.0",
83
- "resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.32.0.tgz",
84
- "integrity": "sha512-vYrr1aXOpU+/IrkHMSTxi/k2d4N8raNu+E3IrKtWt4w7Y0Ke3DLvCHeUJcuWECvQBVhzmg1ert4DcXVUymSKlg==",
85
- "license": "Apache-2.0",
86
- "dependencies": {
87
- "@cloudbase/adapter-interface": "^0.7.1",
88
- "@cloudbase/types": "2.32.0",
89
- "jwt-decode": "^3.1.2"
90
- }
91
- },
92
- "node_modules/@cloudbase/database": {
93
- "version": "1.4.3",
94
- "resolved": "https://registry.npmjs.org/@cloudbase/database/-/database-1.4.3.tgz",
95
- "integrity": "sha512-JzmdsGjy9LwzSQQ0Fv4OlNHNK61BRXdJSembTpy4408AQA3q2Ip3N5eB3v9OpAlFMvP/6MGmOQ/ZuUPKfJddJA==",
96
- "license": "ISC",
97
- "dependencies": {
98
- "bson": "^4.0.3",
99
- "lodash.clonedeep": "4.5.0",
100
- "lodash.set": "4.3.2",
101
- "lodash.unset": "4.5.2"
102
- }
103
- },
104
- "node_modules/@cloudbase/node-sdk": {
105
- "version": "3.17.2",
106
- "resolved": "https://registry.npmjs.org/@cloudbase/node-sdk/-/node-sdk-3.17.2.tgz",
107
- "integrity": "sha512-74ekvRsdmeGch9hFHkQnjHPTEfk8k09iQFvI1aAo48ZsbLqK3czqj/3GoVpSkw5+eYe05tZV13Ct/kWiV5tuGg==",
108
- "license": "MIT",
109
- "dependencies": {
110
- "@cloudbase/ai": "^2.23.0",
111
- "@cloudbase/app": "^2.23.0",
112
- "@cloudbase/database": "1.4.3",
113
- "@cloudbase/signature-nodejs": "2.2.0",
114
- "@cloudbase/types": "^2.23.0",
115
- "@cloudbase/wx-cloud-client-sdk": "1.7.1",
116
- "agentkeepalive": "^4.3.0",
117
- "axios": "0.27.2",
118
- "form-data": "^4.0.0",
119
- "http-proxy-agent": "^5.0.0",
120
- "https-proxy-agent": "^5.0.1",
121
- "jsonwebtoken": "^9.0.2",
122
- "retry": "^0.13.1",
123
- "web-streams-polyfill": "^4.2.0",
124
- "xml2js": "^0.6.2"
125
- },
126
- "engines": {
127
- "node": ">=12"
128
- }
129
- },
130
- "node_modules/@cloudbase/signature-nodejs": {
131
- "version": "2.2.0",
132
- "resolved": "https://registry.npmjs.org/@cloudbase/signature-nodejs/-/signature-nodejs-2.2.0.tgz",
133
- "integrity": "sha512-2y2rieOpJNnBA2MW7sLJKek3iFOjgr8TNicw5qjBkcdVczLR0cFzxMhip75NLCwarFTIAONE3oeNxoQ19eVHcA==",
134
- "dependencies": {
135
- "@types/clone": "^2.1.0",
136
- "clone": "^2.1.2",
137
- "is-stream": "^2.0.0",
138
- "url": "^0.11.0"
139
- }
140
- },
141
- "node_modules/@cloudbase/types": {
142
- "version": "2.32.0",
143
- "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.32.0.tgz",
144
- "integrity": "sha512-HPsMEHHt5SAyD3tpv/C/v2nYudSawE48ixjK5Y9w3R/82auJag0AUsZGfCExgdPa4Zud9OvGx5127ssEXLgadw==",
145
- "license": "Apache-2.0",
146
- "dependencies": {
147
- "@cloudbase/adapter-interface": "^0.7.1"
148
- }
149
- },
150
- "node_modules/@cloudbase/utilities": {
151
- "version": "2.30.0",
152
- "resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.30.0.tgz",
153
- "integrity": "sha512-52DfQH5qgfOtrwj26KCGw6cz0pUjuJludJ7fccUKCzBtn/QwPudwtXo7gOUTtKKDubW3+VUCjjBm7UiDwFu+ag==",
154
- "license": "Apache-2.0",
155
- "dependencies": {
156
- "@cloudbase/adapter-interface": "^0.7.1",
157
- "@cloudbase/types": "2.30.0",
158
- "jwt-decode": "^3.1.2"
159
- }
160
- },
161
- "node_modules/@cloudbase/utilities/node_modules/@cloudbase/types": {
162
- "version": "2.30.0",
163
- "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz",
164
- "integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==",
165
- "license": "Apache-2.0",
166
- "dependencies": {
167
- "@cloudbase/adapter-interface": "^0.7.1"
168
- }
169
- },
170
- "node_modules/@cloudbase/wx-cloud-client-sdk": {
171
- "version": "1.7.1",
172
- "resolved": "https://registry.npmjs.org/@cloudbase/wx-cloud-client-sdk/-/wx-cloud-client-sdk-1.7.1.tgz",
173
- "integrity": "sha512-NzLOoYDAeoRh2lRjCtvr6yXu4KPVHqZtPxyv70e+3b2+dZIDE5S5gtU8daE7xS5u4TwPqKB/E2vmgRTYOsCS9w==",
174
- "dependencies": {
175
- "core-js-pure": "^3.45.0"
176
- }
177
- },
178
- "node_modules/@mattiasbuelens/web-streams-adapter": {
179
- "version": "0.1.0",
180
- "resolved": "https://registry.npmjs.org/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz",
181
- "integrity": "sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA==",
182
- "license": "MIT",
183
- "engines": {
184
- "node": ">= 12"
185
- }
186
- },
187
- "node_modules/@protobufjs/aspromise": {
188
- "version": "1.1.2",
189
- "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
190
- "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
191
- "license": "BSD-3-Clause"
192
- },
193
- "node_modules/@protobufjs/base64": {
194
- "version": "1.1.2",
195
- "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
196
- "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
197
- "license": "BSD-3-Clause"
198
- },
199
- "node_modules/@protobufjs/codegen": {
200
- "version": "2.0.5",
201
- "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
202
- "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
203
- "license": "BSD-3-Clause"
204
- },
205
- "node_modules/@protobufjs/eventemitter": {
206
- "version": "1.1.1",
207
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
208
- "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
209
- "license": "BSD-3-Clause"
210
- },
211
- "node_modules/@protobufjs/fetch": {
212
- "version": "1.1.1",
213
- "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
214
- "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
215
- "license": "BSD-3-Clause",
216
- "dependencies": {
217
- "@protobufjs/aspromise": "^1.1.1"
218
- }
219
- },
220
- "node_modules/@protobufjs/float": {
221
- "version": "1.0.2",
222
- "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
223
- "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
224
- "license": "BSD-3-Clause"
225
- },
226
- "node_modules/@protobufjs/path": {
227
- "version": "1.1.2",
228
- "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
229
- "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
230
- "license": "BSD-3-Clause"
231
- },
232
- "node_modules/@protobufjs/pool": {
233
- "version": "1.1.0",
234
- "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
235
- "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
236
- "license": "BSD-3-Clause"
237
- },
238
- "node_modules/@protobufjs/utf8": {
239
- "version": "1.1.2",
240
- "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
241
- "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
242
- "license": "BSD-3-Clause"
243
- },
244
- "node_modules/@tootallnate/once": {
245
- "version": "2.0.1",
246
- "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
247
- "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==",
248
- "license": "MIT",
249
- "engines": {
250
- "node": ">= 10"
251
- }
252
- },
253
- "node_modules/@types/clone": {
254
- "version": "2.1.4",
255
- "resolved": "https://registry.npmjs.org/@types/clone/-/clone-2.1.4.tgz",
256
- "integrity": "sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==",
257
- "license": "MIT"
258
- },
259
- "node_modules/@types/node": {
260
- "version": "26.3.0",
261
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz",
262
- "integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==",
263
- "license": "MIT",
264
- "dependencies": {
265
- "undici-types": "~8.3.0"
266
- }
267
- },
268
- "node_modules/agent-base": {
269
- "version": "6.0.2",
270
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
271
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
272
- "license": "MIT",
273
- "dependencies": {
274
- "debug": "4"
275
- },
276
- "engines": {
277
- "node": ">= 6.0.0"
278
- }
279
- },
280
- "node_modules/agentkeepalive": {
281
- "version": "4.6.0",
282
- "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
283
- "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
284
- "license": "MIT",
285
- "dependencies": {
286
- "humanize-ms": "^1.2.1"
287
- },
288
- "engines": {
289
- "node": ">= 8.0.0"
290
- }
291
- },
292
- "node_modules/asynckit": {
293
- "version": "0.4.0",
294
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
295
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
296
- "license": "MIT"
297
- },
298
- "node_modules/axios": {
299
- "version": "0.27.2",
300
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
301
- "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
302
- "license": "MIT",
303
- "dependencies": {
304
- "follow-redirects": "^1.14.9",
305
- "form-data": "^4.0.0"
306
- }
307
- },
308
- "node_modules/base64-js": {
309
- "version": "1.5.1",
310
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
311
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
312
- "funding": [
313
- {
314
- "type": "github",
315
- "url": "https://github.com/sponsors/feross"
316
- },
317
- {
318
- "type": "patreon",
319
- "url": "https://www.patreon.com/feross"
320
- },
321
- {
322
- "type": "consulting",
323
- "url": "https://feross.org/support"
324
- }
325
- ],
326
- "license": "MIT"
327
- },
328
- "node_modules/bignumber.js": {
329
- "version": "9.3.1",
330
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
331
- "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
332
- "license": "MIT",
333
- "engines": {
334
- "node": "*"
335
- }
336
- },
337
- "node_modules/bson": {
338
- "version": "4.7.2",
339
- "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz",
340
- "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==",
341
- "license": "Apache-2.0",
342
- "dependencies": {
343
- "buffer": "^5.6.0"
344
- },
345
- "engines": {
346
- "node": ">=6.9.0"
347
- }
348
- },
349
- "node_modules/buffer": {
350
- "version": "5.7.1",
351
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
352
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
353
- "funding": [
354
- {
355
- "type": "github",
356
- "url": "https://github.com/sponsors/feross"
357
- },
358
- {
359
- "type": "patreon",
360
- "url": "https://www.patreon.com/feross"
361
- },
362
- {
363
- "type": "consulting",
364
- "url": "https://feross.org/support"
365
- }
366
- ],
367
- "license": "MIT",
368
- "dependencies": {
369
- "base64-js": "^1.3.1",
370
- "ieee754": "^1.1.13"
371
- }
372
- },
373
- "node_modules/buffer-equal-constant-time": {
374
- "version": "1.0.1",
375
- "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
376
- "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
377
- "license": "BSD-3-Clause"
378
- },
379
- "node_modules/call-bind-apply-helpers": {
380
- "version": "1.0.2",
381
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
382
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
383
- "license": "MIT",
384
- "dependencies": {
385
- "es-errors": "^1.3.0",
386
- "function-bind": "^1.1.2"
387
- },
388
- "engines": {
389
- "node": ">= 0.4"
390
- }
391
- },
392
- "node_modules/call-bound": {
393
- "version": "1.0.4",
394
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
395
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
396
- "license": "MIT",
397
- "dependencies": {
398
- "call-bind-apply-helpers": "^1.0.2",
399
- "get-intrinsic": "^1.3.0"
400
- },
401
- "engines": {
402
- "node": ">= 0.4"
403
- },
404
- "funding": {
405
- "url": "https://github.com/sponsors/ljharb"
406
- }
407
- },
408
- "node_modules/clone": {
409
- "version": "2.1.2",
410
- "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
411
- "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
412
- "license": "MIT",
413
- "engines": {
414
- "node": ">=0.8"
415
- }
416
- },
417
- "node_modules/combined-stream": {
418
- "version": "1.0.8",
419
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
420
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
421
- "license": "MIT",
422
- "dependencies": {
423
- "delayed-stream": "~1.0.0"
424
- },
425
- "engines": {
426
- "node": ">= 0.8"
427
- }
428
- },
429
- "node_modules/core-js-pure": {
430
- "version": "3.50.0",
431
- "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz",
432
- "integrity": "sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==",
433
- "hasInstallScript": true,
434
- "license": "MIT",
435
- "engines": {
436
- "node": "*"
437
- },
438
- "funding": {
439
- "type": "opencollective",
440
- "url": "https://opencollective.com/core-js"
441
- }
442
- },
443
- "node_modules/debug": {
444
- "version": "4.4.3",
445
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
446
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
447
- "license": "MIT",
448
- "dependencies": {
449
- "ms": "^2.1.3"
450
- },
451
- "engines": {
452
- "node": ">=6.0"
453
- },
454
- "peerDependenciesMeta": {
455
- "supports-color": {
456
- "optional": true
457
- }
458
- }
459
- },
460
- "node_modules/delayed-stream": {
461
- "version": "1.0.0",
462
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
463
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
464
- "license": "MIT",
465
- "engines": {
466
- "node": ">=0.4.0"
467
- }
468
- },
469
- "node_modules/dunder-proto": {
470
- "version": "1.0.1",
471
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
472
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
473
- "license": "MIT",
474
- "dependencies": {
475
- "call-bind-apply-helpers": "^1.0.1",
476
- "es-errors": "^1.3.0",
477
- "gopd": "^1.2.0"
478
- },
479
- "engines": {
480
- "node": ">= 0.4"
481
- }
482
- },
483
- "node_modules/ecdsa-sig-formatter": {
484
- "version": "1.0.11",
485
- "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
486
- "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
487
- "license": "Apache-2.0",
488
- "dependencies": {
489
- "safe-buffer": "^5.0.1"
490
- }
491
- },
492
- "node_modules/es-define-property": {
493
- "version": "1.0.1",
494
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
495
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
496
- "license": "MIT",
497
- "engines": {
498
- "node": ">= 0.4"
499
- }
500
- },
501
- "node_modules/es-errors": {
502
- "version": "1.3.0",
503
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
504
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
505
- "license": "MIT",
506
- "engines": {
507
- "node": ">= 0.4"
508
- }
509
- },
510
- "node_modules/es-object-atoms": {
511
- "version": "1.1.2",
512
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
513
- "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
514
- "license": "MIT",
515
- "dependencies": {
516
- "es-errors": "^1.3.0"
517
- },
518
- "engines": {
519
- "node": ">= 0.4"
520
- }
521
- },
522
- "node_modules/es-set-tostringtag": {
523
- "version": "2.1.0",
524
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
525
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
526
- "license": "MIT",
527
- "dependencies": {
528
- "es-errors": "^1.3.0",
529
- "get-intrinsic": "^1.2.6",
530
- "has-tostringtag": "^1.0.2",
531
- "hasown": "^2.0.2"
532
- },
533
- "engines": {
534
- "node": ">= 0.4"
535
- }
536
- },
537
- "node_modules/follow-redirects": {
538
- "version": "1.16.0",
539
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
540
- "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
541
- "funding": [
542
- {
543
- "type": "individual",
544
- "url": "https://github.com/sponsors/RubenVerborgh"
545
- }
546
- ],
547
- "license": "MIT",
548
- "engines": {
549
- "node": ">=4.0"
550
- },
551
- "peerDependenciesMeta": {
552
- "debug": {
553
- "optional": true
554
- }
555
- }
556
- },
557
- "node_modules/form-data": {
558
- "version": "4.0.6",
559
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
560
- "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
561
- "license": "MIT",
562
- "dependencies": {
563
- "asynckit": "^0.4.0",
564
- "combined-stream": "^1.0.8",
565
- "es-set-tostringtag": "^2.1.0",
566
- "hasown": "^2.0.4",
567
- "mime-types": "^2.1.35"
568
- },
569
- "engines": {
570
- "node": ">= 6"
571
- }
572
- },
573
- "node_modules/function-bind": {
574
- "version": "1.1.2",
575
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
576
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
577
- "license": "MIT",
578
- "funding": {
579
- "url": "https://github.com/sponsors/ljharb"
580
- }
581
- },
582
- "node_modules/get-intrinsic": {
583
- "version": "1.3.0",
584
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
585
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
586
- "license": "MIT",
587
- "dependencies": {
588
- "call-bind-apply-helpers": "^1.0.2",
589
- "es-define-property": "^1.0.1",
590
- "es-errors": "^1.3.0",
591
- "es-object-atoms": "^1.1.1",
592
- "function-bind": "^1.1.2",
593
- "get-proto": "^1.0.1",
594
- "gopd": "^1.2.0",
595
- "has-symbols": "^1.1.0",
596
- "hasown": "^2.0.2",
597
- "math-intrinsics": "^1.1.0"
598
- },
599
- "engines": {
600
- "node": ">= 0.4"
601
- },
602
- "funding": {
603
- "url": "https://github.com/sponsors/ljharb"
604
- }
605
- },
606
- "node_modules/get-proto": {
607
- "version": "1.0.1",
608
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
609
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
610
- "license": "MIT",
611
- "dependencies": {
612
- "dunder-proto": "^1.0.1",
613
- "es-object-atoms": "^1.0.0"
614
- },
615
- "engines": {
616
- "node": ">= 0.4"
617
- }
618
- },
619
- "node_modules/gopd": {
620
- "version": "1.2.0",
621
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
622
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
623
- "license": "MIT",
624
- "engines": {
625
- "node": ">= 0.4"
626
- },
627
- "funding": {
628
- "url": "https://github.com/sponsors/ljharb"
629
- }
630
- },
631
- "node_modules/has-symbols": {
632
- "version": "1.1.0",
633
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
634
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
635
- "license": "MIT",
636
- "engines": {
637
- "node": ">= 0.4"
638
- },
639
- "funding": {
640
- "url": "https://github.com/sponsors/ljharb"
641
- }
642
- },
643
- "node_modules/has-tostringtag": {
644
- "version": "1.0.2",
645
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
646
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
647
- "license": "MIT",
648
- "dependencies": {
649
- "has-symbols": "^1.0.3"
650
- },
651
- "engines": {
652
- "node": ">= 0.4"
653
- },
654
- "funding": {
655
- "url": "https://github.com/sponsors/ljharb"
656
- }
657
- },
658
- "node_modules/hasown": {
659
- "version": "2.0.4",
660
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
661
- "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
662
- "license": "MIT",
663
- "dependencies": {
664
- "function-bind": "^1.1.2"
665
- },
666
- "engines": {
667
- "node": ">= 0.4"
668
- }
669
- },
670
- "node_modules/http-proxy-agent": {
671
- "version": "5.0.0",
672
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
673
- "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
674
- "license": "MIT",
675
- "dependencies": {
676
- "@tootallnate/once": "2",
677
- "agent-base": "6",
678
- "debug": "4"
679
- },
680
- "engines": {
681
- "node": ">= 6"
682
- }
683
- },
684
- "node_modules/https-proxy-agent": {
685
- "version": "5.0.1",
686
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
687
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
688
- "license": "MIT",
689
- "dependencies": {
690
- "agent-base": "6",
691
- "debug": "4"
692
- },
693
- "engines": {
694
- "node": ">= 6"
695
- }
696
- },
697
- "node_modules/humanize-ms": {
698
- "version": "1.2.1",
699
- "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
700
- "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
701
- "license": "MIT",
702
- "dependencies": {
703
- "ms": "^2.0.0"
704
- }
705
- },
706
- "node_modules/ieee754": {
707
- "version": "1.2.1",
708
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
709
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
710
- "funding": [
711
- {
712
- "type": "github",
713
- "url": "https://github.com/sponsors/feross"
714
- },
715
- {
716
- "type": "patreon",
717
- "url": "https://www.patreon.com/feross"
718
- },
719
- {
720
- "type": "consulting",
721
- "url": "https://feross.org/support"
722
- }
723
- ],
724
- "license": "BSD-3-Clause"
725
- },
726
- "node_modules/is-stream": {
727
- "version": "2.0.1",
728
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
729
- "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
730
- "license": "MIT",
731
- "engines": {
732
- "node": ">=8"
733
- },
734
- "funding": {
735
- "url": "https://github.com/sponsors/sindresorhus"
736
- }
737
- },
738
- "node_modules/json-bigint": {
739
- "version": "1.0.0",
740
- "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
741
- "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
742
- "license": "MIT",
743
- "dependencies": {
744
- "bignumber.js": "^9.0.0"
745
- }
746
- },
747
- "node_modules/jsonwebtoken": {
748
- "version": "9.0.3",
749
- "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
750
- "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
751
- "license": "MIT",
752
- "dependencies": {
753
- "jws": "^4.0.1",
754
- "lodash.includes": "^4.3.0",
755
- "lodash.isboolean": "^3.0.3",
756
- "lodash.isinteger": "^4.0.4",
757
- "lodash.isnumber": "^3.0.3",
758
- "lodash.isplainobject": "^4.0.6",
759
- "lodash.isstring": "^4.0.1",
760
- "lodash.once": "^4.0.0",
761
- "ms": "^2.1.1",
762
- "semver": "^7.5.4"
763
- },
764
- "engines": {
765
- "node": ">=12",
766
- "npm": ">=6"
767
- }
768
- },
769
- "node_modules/jwa": {
770
- "version": "2.0.1",
771
- "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
772
- "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
773
- "license": "MIT",
774
- "dependencies": {
775
- "buffer-equal-constant-time": "^1.0.1",
776
- "ecdsa-sig-formatter": "1.0.11",
777
- "safe-buffer": "^5.0.1"
778
- }
779
- },
780
- "node_modules/jws": {
781
- "version": "4.0.1",
782
- "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
783
- "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
784
- "license": "MIT",
785
- "dependencies": {
786
- "jwa": "^2.0.1",
787
- "safe-buffer": "^5.0.1"
788
- }
789
- },
790
- "node_modules/jwt-decode": {
791
- "version": "3.1.2",
792
- "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
793
- "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==",
794
- "license": "MIT"
795
- },
796
- "node_modules/lodash.clonedeep": {
797
- "version": "4.5.0",
798
- "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
799
- "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
800
- "license": "MIT"
801
- },
802
- "node_modules/lodash.includes": {
803
- "version": "4.3.0",
804
- "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
805
- "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
806
- "license": "MIT"
807
- },
808
- "node_modules/lodash.isboolean": {
809
- "version": "3.0.3",
810
- "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
811
- "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
812
- "license": "MIT"
813
- },
814
- "node_modules/lodash.isinteger": {
815
- "version": "4.0.4",
816
- "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
817
- "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
818
- "license": "MIT"
819
- },
820
- "node_modules/lodash.isnumber": {
821
- "version": "3.0.3",
822
- "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
823
- "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
824
- "license": "MIT"
825
- },
826
- "node_modules/lodash.isplainobject": {
827
- "version": "4.0.6",
828
- "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
829
- "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
830
- "license": "MIT"
831
- },
832
- "node_modules/lodash.isstring": {
833
- "version": "4.0.1",
834
- "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
835
- "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
836
- "license": "MIT"
837
- },
838
- "node_modules/lodash.once": {
839
- "version": "4.1.1",
840
- "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
841
- "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
842
- "license": "MIT"
843
- },
844
- "node_modules/lodash.set": {
845
- "version": "4.3.2",
846
- "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
847
- "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
848
- "license": "MIT"
849
- },
850
- "node_modules/lodash.unset": {
851
- "version": "4.5.2",
852
- "resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz",
853
- "integrity": "sha512-bwKX88k2JhCV9D1vtE8+naDKlLiGrSmf8zi/Y9ivFHwbmRfA8RxS/aVJ+sIht2XOwqoNr4xUPUkGZpc1sHFEKg==",
854
- "license": "MIT"
855
- },
856
- "node_modules/long": {
857
- "version": "5.3.2",
858
- "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
859
- "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
860
- "license": "Apache-2.0"
861
- },
862
- "node_modules/math-intrinsics": {
863
- "version": "1.1.0",
864
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
865
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
866
- "license": "MIT",
867
- "engines": {
868
- "node": ">= 0.4"
869
- }
870
- },
871
- "node_modules/mime-db": {
872
- "version": "1.52.0",
873
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
874
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
875
- "license": "MIT",
876
- "engines": {
877
- "node": ">= 0.6"
878
- }
879
- },
880
- "node_modules/mime-types": {
881
- "version": "2.1.35",
882
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
883
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
884
- "license": "MIT",
885
- "dependencies": {
886
- "mime-db": "1.52.0"
887
- },
888
- "engines": {
889
- "node": ">= 0.6"
890
- }
891
- },
892
- "node_modules/ms": {
893
- "version": "2.1.3",
894
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
895
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
896
- "license": "MIT"
897
- },
898
- "node_modules/object-inspect": {
899
- "version": "1.13.4",
900
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
901
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
902
- "license": "MIT",
903
- "engines": {
904
- "node": ">= 0.4"
905
- },
906
- "funding": {
907
- "url": "https://github.com/sponsors/ljharb"
908
- }
909
- },
910
- "node_modules/protobufjs": {
911
- "version": "7.6.5",
912
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
913
- "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
914
- "hasInstallScript": true,
915
- "license": "BSD-3-Clause",
916
- "dependencies": {
917
- "@protobufjs/aspromise": "^1.1.2",
918
- "@protobufjs/base64": "^1.1.2",
919
- "@protobufjs/codegen": "^2.0.5",
920
- "@protobufjs/eventemitter": "^1.1.1",
921
- "@protobufjs/fetch": "^1.1.1",
922
- "@protobufjs/float": "^1.0.2",
923
- "@protobufjs/path": "^1.1.2",
924
- "@protobufjs/pool": "^1.1.0",
925
- "@protobufjs/utf8": "^1.1.1",
926
- "@types/node": ">=13.7.0",
927
- "long": "^5.3.2"
928
- },
929
- "engines": {
930
- "node": ">=12.0.0"
931
- }
932
- },
933
- "node_modules/punycode": {
934
- "version": "1.4.1",
935
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
936
- "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
937
- "license": "MIT"
938
- },
939
- "node_modules/qs": {
940
- "version": "6.15.3",
941
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
942
- "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
943
- "license": "BSD-3-Clause",
944
- "dependencies": {
945
- "es-define-property": "^1.0.1",
946
- "side-channel": "^1.1.1"
947
- },
948
- "engines": {
949
- "node": ">=0.6"
950
- },
951
- "funding": {
952
- "url": "https://github.com/sponsors/ljharb"
953
- }
954
- },
955
- "node_modules/retry": {
956
- "version": "0.13.1",
957
- "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
958
- "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
959
- "license": "MIT",
960
- "engines": {
961
- "node": ">= 4"
962
- }
963
- },
964
- "node_modules/safe-buffer": {
965
- "version": "5.2.1",
966
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
967
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
968
- "funding": [
969
- {
970
- "type": "github",
971
- "url": "https://github.com/sponsors/feross"
972
- },
973
- {
974
- "type": "patreon",
975
- "url": "https://www.patreon.com/feross"
976
- },
977
- {
978
- "type": "consulting",
979
- "url": "https://feross.org/support"
980
- }
981
- ],
982
- "license": "MIT"
983
- },
984
- "node_modules/sax": {
985
- "version": "1.6.1",
986
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
987
- "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
988
- "license": "BlueOak-1.0.0",
989
- "engines": {
990
- "node": ">=11.0.0"
991
- }
992
- },
993
- "node_modules/semver": {
994
- "version": "7.8.5",
995
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
996
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
997
- "license": "ISC",
998
- "bin": {
999
- "semver": "bin/semver.js"
1000
- },
1001
- "engines": {
1002
- "node": ">=10"
1003
- }
1004
- },
1005
- "node_modules/side-channel": {
1006
- "version": "1.1.1",
1007
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
1008
- "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
1009
- "license": "MIT",
1010
- "dependencies": {
1011
- "es-errors": "^1.3.0",
1012
- "object-inspect": "^1.13.4",
1013
- "side-channel-list": "^1.0.1",
1014
- "side-channel-map": "^1.0.1",
1015
- "side-channel-weakmap": "^1.0.2"
1016
- },
1017
- "engines": {
1018
- "node": ">= 0.4"
1019
- },
1020
- "funding": {
1021
- "url": "https://github.com/sponsors/ljharb"
1022
- }
1023
- },
1024
- "node_modules/side-channel-list": {
1025
- "version": "1.0.1",
1026
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
1027
- "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
1028
- "license": "MIT",
1029
- "dependencies": {
1030
- "es-errors": "^1.3.0",
1031
- "object-inspect": "^1.13.4"
1032
- },
1033
- "engines": {
1034
- "node": ">= 0.4"
1035
- },
1036
- "funding": {
1037
- "url": "https://github.com/sponsors/ljharb"
1038
- }
1039
- },
1040
- "node_modules/side-channel-map": {
1041
- "version": "1.0.1",
1042
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1043
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1044
- "license": "MIT",
1045
- "dependencies": {
1046
- "call-bound": "^1.0.2",
1047
- "es-errors": "^1.3.0",
1048
- "get-intrinsic": "^1.2.5",
1049
- "object-inspect": "^1.13.3"
1050
- },
1051
- "engines": {
1052
- "node": ">= 0.4"
1053
- },
1054
- "funding": {
1055
- "url": "https://github.com/sponsors/ljharb"
1056
- }
1057
- },
1058
- "node_modules/side-channel-weakmap": {
1059
- "version": "1.0.2",
1060
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1061
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1062
- "license": "MIT",
1063
- "dependencies": {
1064
- "call-bound": "^1.0.2",
1065
- "es-errors": "^1.3.0",
1066
- "get-intrinsic": "^1.2.5",
1067
- "object-inspect": "^1.13.3",
1068
- "side-channel-map": "^1.0.1"
1069
- },
1070
- "engines": {
1071
- "node": ">= 0.4"
1072
- },
1073
- "funding": {
1074
- "url": "https://github.com/sponsors/ljharb"
1075
- }
1076
- },
1077
- "node_modules/text-encoding-shim": {
1078
- "version": "1.0.5",
1079
- "resolved": "https://registry.npmjs.org/text-encoding-shim/-/text-encoding-shim-1.0.5.tgz",
1080
- "integrity": "sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==",
1081
- "license": "MIT"
1082
- },
1083
- "node_modules/tslib": {
1084
- "version": "1.14.1",
1085
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
1086
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
1087
- "license": "0BSD"
1088
- },
1089
- "node_modules/undici-types": {
1090
- "version": "8.3.0",
1091
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
1092
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
1093
- "license": "MIT"
1094
- },
1095
- "node_modules/url": {
1096
- "version": "0.11.4",
1097
- "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
1098
- "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
1099
- "license": "MIT",
1100
- "dependencies": {
1101
- "punycode": "^1.4.1",
1102
- "qs": "^6.12.3"
1103
- },
1104
- "engines": {
1105
- "node": ">= 0.4"
1106
- }
1107
- },
1108
- "node_modules/web-streams-polyfill": {
1109
- "version": "4.3.0",
1110
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.3.0.tgz",
1111
- "integrity": "sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw==",
1112
- "license": "MIT",
1113
- "workspaces": [
1114
- "test/benchmark-test",
1115
- "test/rollup-test",
1116
- "test/webpack-test"
1117
- ],
1118
- "engines": {
1119
- "node": ">= 8"
1120
- }
1121
- },
1122
- "node_modules/wx-server-sdk": {
1123
- "version": "4.0.2",
1124
- "resolved": "https://registry.npmjs.org/wx-server-sdk/-/wx-server-sdk-4.0.2.tgz",
1125
- "integrity": "sha512-dgpgWxYvOFWnQdGOb78sU+uQGELoEQSKzmJ7m8Y6eTCJKsfkdIHLMhRrdN5S5oDfAqr8Pc83jZKQeNRMZASIcg==",
1126
- "license": "MIT",
1127
- "dependencies": {
1128
- "@cloudbase/node-sdk": "3.17.2",
1129
- "json-bigint": "^1.0.0",
1130
- "protobufjs": "^7.2.4",
1131
- "tslib": "^1.9.3"
1132
- }
1133
- },
1134
- "node_modules/xml2js": {
1135
- "version": "0.6.2",
1136
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
1137
- "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
1138
- "license": "MIT",
1139
- "dependencies": {
1140
- "sax": ">=0.6.0",
1141
- "xmlbuilder": "~11.0.0"
1142
- },
1143
- "engines": {
1144
- "node": ">=4.0.0"
1145
- }
1146
- },
1147
- "node_modules/xmlbuilder": {
1148
- "version": "11.0.1",
1149
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
1150
- "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
1151
- "license": "MIT",
1152
- "engines": {
1153
- "node": ">=4.0"
1154
- }
1155
- }
1156
- }
1157
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/cloudfunctions/quickstartFunctions/package.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "name": "quickstartFunctions",
3
- "version": "1.0.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "author": "",
10
- "license": "ISC",
11
- "dependencies": {
12
- "wx-server-sdk": "~4.0.2"
13
- }
14
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/docs/promo/douwei-1080-cover.png DELETED

Git LFS Details

  • SHA256: aeac46fce4028cbfc2d7bae0dad427ed5f67262582ad411a00ccb2db65f8f03b
  • Pointer size: 131 Bytes
  • Size of remote file: 320 kB
BeadQR/docs/promo/douwei-1080.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:89ba1505c4ce4aa36316841c497dc96b8997bac534bb534426c23ca96ce87dcc
3
- size 3693122
 
 
 
 
BeadQR/docs/promo/douwei-720-cover.png DELETED

Git LFS Details

  • SHA256: bdb68c450700c1e65492e708440a29d07c9288606ca0ad7b464133052855fb2a
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB
BeadQR/docs/promo/douwei-720.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:626ea04f449f33fe29c4ec6c4aa94a2db88600cababab4f5a11b74155b4eee4e
3
- size 1650037
 
 
 
 
BeadQR/docs/source-photo.jpeg DELETED

Git LFS Details

  • SHA256: ceae6708b13c1c7863c9a73b51f5eb775abcfe6f667b1c86ea993c8fe26995a8
  • Pointer size: 131 Bytes
  • Size of remote file: 204 kB
BeadQR/docs/video-script.md DELETED
@@ -1,116 +0,0 @@
1
- # 豆维 · 视频文案与分镜脚本
2
-
3
- > 目标:用一个短视频介绍「豆维 · 可扫码的拼豆」小程序,让观众理解它是什么、为什么有趣、怎么做出来。
4
- > 结构:提出想法 → 实现想法 → 制作例子 → 完成例子。
5
-
6
- ---
7
-
8
- ## 一、视频定位与调性
9
-
10
- | 维度 | 建议 |
11
- |------|------|
12
- | 时长 | 主版本 60–90 秒(B站 / 朋友圈 / 公众号),另附 15–30 秒短视频版 |
13
- | 调性 | 温暖手作 × 科技惊喜,不是冷冰冰的产品演示 |
14
- | 核心情绪 | 「拼豆不止是一幅画,它是一封能被读到的信」 |
15
- | 目标观众 | 拼豆爱好者、手工创作者、喜欢送独特礼物的人 |
16
- | 画面风格 | 暖色调,木桌面,自然光;手机屏幕录制与实物特写交替出现 |
17
-
18
- ---
19
-
20
- ## 二、核心文案(Slogan / 一句话介绍)
21
-
22
- 以下任选一条作为标题或结尾定版字:
23
-
24
- 1. **豆维 · 可扫码的拼豆**
25
- 2. **每一颗珠子,都藏着一句话。**
26
- 3. **拼出来的二维码,扫出来的心意。**
27
- 4. **把想说的话,拼进一幅画里。**
28
- 5. **看得见,也读得到。**
29
-
30
- 推荐主标题:**看得见,也读得到。——豆维,可扫码的拼豆**
31
-
32
- ---
33
-
34
- ## 三、主版本分镜脚本(60–90 秒)
35
-
36
- > 旁白以第一人称「我」叙述,语气像朋友分享,不念广告稿。
37
- > 「画面」列标注了你需要准备的素材类型;如果你已有类似镜头,直接对应即可。
38
-
39
- | 幕 | 时间 | 画面 | 旁白 / 字幕 | 剪辑提示 |
40
- |----|------|------|-------------|----------|
41
- | **第一幕:提出想法** | 0:00–0:08 | 桌面上摆着几幅拼豆作品,阳光照进来。手拿起其中一幅,翻到背面,又翻回来。 | **旁白:** 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画? | 慢推镜头,配温暖的钢琴或指弹吉他。最后一帧画面停在作品特写。 |
42
- | | 0:08–0:14 | 手拿起手机对准拼豆作品「扫一扫」——屏幕弹出一条消息或链接。 | **旁白:** 能不能扫一下,就能看到我想说的话? | 这是全片第一处「惊喜时刻」,音效清脆,屏幕内容要清晰可见。 |
43
- | **第二幕:实现想法** | 0:14–0:24 | 切到手机屏幕录屏:打开豆维小程序,输入一段文字。 | **旁白:** 所以我做了「豆维」——一个可扫码的拼豆图纸生成器。输入一段话,或者一个链接。 | 屏幕录制加速 2–3 倍,打字过程快放。 |
44
- | | 0:24–0:34 | 录屏:导入一张照片,裁切、去背景。 | **旁白:** 再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。 | 展示裁切弹窗操作,去背景效果前后对比。 |
45
- | | 0:34–0:44 | 录屏:拖动滑块调整二维码大小和位置,切换「丰富 / 平衡 / 稳定」模式。 | **旁白:** 二维码可以放大缩小、移到任何位置。颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。 | 滑块操作用慢动作展示,让观众看到图案实时变化。 |
46
- | **第三幕:制作例子** | 0:44–0:52 | 导出的图纸打印出来,摆在桌上。开始一颗一颗摆珠子。 | **旁白:** 图纸保存下来,打印出来,就可以开始拼了。 | 延时摄影或快剪,展示摆珠过程,保留几秒真实速度的特写。 |
47
- | | 0:52–0:58 | 熨斗烫过,拼豆板冷却,掀开,成品完成。 | **旁白:** 熨好,冷却,掀开—— | 掀开那一刻音效「叮」,画面干净利落。 |
48
- | **第四幕:完成例子** | 0:58–1:10 | 手持成品对镜头展示,然后用手机扫描。屏幕上弹出一开始输入的那段话。 | **旁白:** 它不只是一幅画。你把手机对准它——就能读到里面藏着的话。 | 这是全片情绪最高点。扫描过程用真实速度,屏幕内容要清楚。 |
49
- | | 1:10–1:20 | 多个成品排列展示(不同图案、不同颜色),最后画面淡出到品牌 Logo 和小程序码。 | **旁白:** 豆维,可扫码的拼豆。看得见,也读得到。 | 结尾定格 3 秒,放小程序码,方便观众扫码体验。 |
50
-
51
- ---
52
-
53
- ## 四、15 秒短视频版(抖音 / 视频号快闪)
54
-
55
- | 时间 | 画面 | 字幕(无旁白,纯字幕流) |
56
- |------|------|--------------------------|
57
- | 0–3s | 手拿起拼豆作品 → 手机扫描 → 屏幕弹出消息 | 拼豆里,藏着一条消息 |
58
- | 3–8s | 快剪:输入文字 → 导入照片 → 图纸生成 → 摆珠 | 豆维:输入一句话,选一张图,生成拼豆图纸 |
59
- | 8–13s | 熨烫 → 掀开 → 成品再扫一次 | 拼好,烫平,扫一下—— |
60
- | 13–15s | 品牌 Logo + 小程序码 | 消息就出来了。豆维 · 可扫码的拼豆 |
61
-
62
- > 快闪版不需要旁白,靠音乐节奏 + 字幕推进。音乐选节奏感强的电子或 Lo-Fi。
63
-
64
- ---
65
-
66
- ## 五、30 秒版本(朋友圈 / 公众号内嵌)
67
-
68
- 节奏比 15 秒版稍慢,保留 4 个关键节点:
69
-
70
- 1. **悬念**(5s):拿起作品 → 扫码 → 弹出内容
71
- 2. **怎么做**(10s):小程序操作快剪(输入 → 导图 → 调整 → 导出)
72
- 3. **手作过程**(8s):摆珠延时 + 熨烫 + 掀开
73
- 4. **结尾**(7s):成品特写 + 再扫一次 + 定版
74
-
75
- 配乐:轻快指弹吉他或 Lo-Fi,音量压低,让旁白清晰。
76
-
77
- ---
78
-
79
- ## 六、素材清单
80
-
81
- 拍摄前对照这份清单,确认已拍到以下镜头(√ 为已有,× 为待补):
82
-
83
- | 素材 | 用途 | 状态 |
84
- |------|------|------|
85
- | 多幅完成的拼豆作品摆拍(正面 / 斜角 / 背面) | 开头悬念、结尾展示 | ☐ |
86
- | 手机扫描拼豆作品并弹出内容的实拍 | 全片核心「惊喜时刻」,需拍 2–3 次 | ☐ |
87
- | 豆维小程序屏幕录制:输入文字、导入图片、裁切、调 QR 位置、切模式 | 第二幕 | ☐ |
88
- | 图纸导出 / 打印的镜头 | 第三幕开头 | ☐ |
89
- | 摆珠延时或快剪素材 | 第三幕 | ☐ |
90
- | 熨斗烫拼豆 + 掀开的特写 | 第三幕高潮 | ☐ |
91
- | 成品多角度特写(自然光 / 桌面) | 结尾 | ☐ |
92
-
93
- ---
94
-
95
- ## 七、剪辑建议
96
-
97
- - **转场**:幕与幕之间用干净的硬切或快速模糊,不要花哨转场。产品视频要显得克制。
98
- - **音乐**:主版本选温暖的原声吉他或钢琴;快闪版选节奏感强的 Lo-Fi / 电子。全片一首歌,不要中途换。
99
- - **节奏**:第三幕(摆珠)可以稍慢,让观众感受手作质感;第二幕(录屏)加速推进。
100
- - **字幕**:全程加简体中文字幕,关键词用品牌色 `#416d62`(深绿)高亮。
101
- - **结尾**:小程序码至少停留 3 秒,让观众来得及扫码。
102
-
103
- ---
104
-
105
- ## 八、旁白全文(主版本,可直接朗读)
106
-
107
- > 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画?能不能扫一下,就能看到我想说的话?
108
- >
109
- > 所以我做了「豆维」。输入一段话,或者一个链接,再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。二维码可以放大缩小、移到任何位置,颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。
110
- >
111
- > 图纸保存下来,打印出来,就可以开始拼了。熨好,冷却,掀开——
112
- >
113
- > 它不只是一幅画。你把手机对准它,就能读到里面藏着的话。
114
- >
115
- > 豆维,可扫码的拼豆。看得见,也读得到。
116
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/docs/审核驳回处理-文案与步骤.md DELETED
@@ -1,58 +0,0 @@
1
- # 审核驳回处理:深度合成误判 — 文案与步骤
2
-
3
- > 驳回原因(2026-09-30):小程序服务内容涉及深度合成技术(如 AI 问答、AI 换脸视频、AI 绘画等),属个人主体尚未开放服务类目,建议申请企业主体类型小程序。
4
-
5
- ## 结论
6
-
7
- 代码里没有任何 AI 功能:文字是标准二维码编码;"照片转图纸"是确定性本地图像处理(按网格取平均色 → 映射到 221 种固定色号),不用任何模型、不生成新图像。被判"深度合成"大概率是**后台简介 / 服务类目 / 服务内容声明的表述**或审核员看到"照片→生成图纸"产生的误判。
8
-
9
- ## 路径 A(推荐,最快):修正表述后重新提审
10
-
11
- ### 1. 检查后台三处(mp.weixin.qq.com)
12
-
13
- | 位置 | 检查项 |
14
- | --- | --- |
15
- | 服务类目 | 改为 **工具 - 图片处理**(个人主体可选);删除任何 AI / 深度合成相关类目 |
16
- | 服务内容声明 / 提审问卷 | 凡问"是否提供 AI 生成内容、AI 问答、AI 绘画、深度合成"一律选 **否** |
17
- | 小程序简介 | 换成下方"新简介",去掉"生成器 / 一键生成 / 智能"等字眼 |
18
-
19
- ### 2. 新简介(约 90 字,上限 120)
20
-
21
- ```
22
- 手工拼豆图纸工具:输入一句话或选一张照片,按 MARD 标准色号排成拼豆图纸,拼好微信一扫就能读出内容。支持 A4 矢量 PDF 与图片导出、用量清单。照片仅在本地做取色处理,并经微信内容安全检测。
23
- ```
24
-
25
- ### 3. 版本描述 / 审核备注(重新提审时填写)
26
-
27
- ```
28
- 本小程序为拼豆手工图纸工具,不使用任何人工智能或深度合成技术,无 AIGC 功能:
29
- 1. "照片转图纸"为确定性本地图像处理:将用户上传照片按网格取平均色,再映射到固定的 221 种拼豆标准色号(色彩量化 + 最近邻查表),不生成、不合成任何新图像内容,不涉及人脸或生物特征编辑;
30
- 2. 文字功能是将文本编码为标准二维码点阵,非内容生成;
31
- 3. 所有处理均在用户手机本地完成,且已接入微信内容安全接口(security.msgSecCheck / imgSecCheck),检测未通过时禁止生成与导出(fail-closed)。
32
- 请按"工具-图片处理"类目审核。
33
- ```
34
-
35
- ## 路径 B:申诉
36
-
37
- 若不想改简介,可直接对本次审核结果发起申诉(审核结果页 → 申诉),粘贴下文:
38
-
39
- ```
40
- 本小程序为拼豆手工图纸工具,不涉及深度合成技术:
41
- (1) 不使用 AI 模型 / 神经网络,无 AI 问答、AI 绘画、AI 换脸等 AIGC 功能;
42
- (2) 照片功能仅做确定性色彩量化:按网格取平均色并映射到 221 种固定标准色号,属于常规图像处理,不生成新图像、不做人脸或生物特征编辑;
43
- (3) 文字功能为标准二维码编码;
44
- (4) 已接入微信内容安全检测(msgSecCheck / imgSecCheck),检测未通过不能生成或导出。
45
- 恳请复核,建议按"工具-图片处理"类目审核。
46
- ```
47
-
48
- 申诉未通过时:修改简介与类目后按路径 A 重新提审(改了表述基本都能过)。
49
-
50
- ## 路径 C:仅当二次被拒或想保留"生成"叙事
51
-
52
- 1. **隐藏照片功能**(只保留文字 → 二维码图纸,纯编码无争议)——我可以直接改代码;
53
- 2. **升级主体**:注册个体工商户 / 企业 → 主体变更 → 申请深度合成相关类目 + 算法备案。仅在以后真要做 AI 功能时才值得,当前工具用不到。
54
-
55
- ## 顺手可做的降风险小改动(可选)
56
-
57
- - 首页徽标 `MARD 拼豆图纸生成器` → `MARD 拼豆图纸工具`
58
- - 分享标题 `豆维 · 可扫码的拼豆图纸生成器` → `豆维 · 可扫码的拼豆图纸`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/app.js DELETED
@@ -1,14 +0,0 @@
1
- // 豆维 · 可扫码的拼豆
2
- App({
3
- onLaunch() {
4
- if (!wx.cloud) {
5
- console.error('当前基础库不支持云开发,内容安全检测不可用')
6
- return
7
- }
8
- wx.cloud.init({
9
- env: 'cloud1-d6gthxado7f6c9d14',
10
- traceUser: true,
11
- })
12
- },
13
- globalData: {},
14
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/app.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "pages": [
3
- "pages/index/index"
4
- ],
5
- "window": {
6
- "backgroundColor": "#f6f5f1",
7
- "backgroundTextStyle": "dark",
8
- "navigationBarBackgroundColor": "#f6f5f1",
9
- "navigationBarTitleText": "豆维 · 可扫码的拼豆",
10
- "navigationBarTextStyle": "black"
11
- },
12
- "usingComponents": {
13
- "privacy-popup": "/components/privacy-popup/index"
14
- },
15
- "__usePrivacyCheck__": true,
16
- "sitemapLocation": "sitemap.json",
17
- "style": "v2",
18
- "lazyCodeLoading": "requiredComponents"
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/app.wxss DELETED
@@ -1,37 +0,0 @@
1
- /* 豆维 · 设计变量 */
2
- page {
3
- --ink: #16211f;
4
- --ink-2: #3a4643;
5
- --muted: #6f7a76;
6
- --faint: #a4aca8;
7
- --bg: #f6f5f1;
8
- --card: #ffffff;
9
- --line: #ebe8e1;
10
- --brand: #0f766e;
11
- --brand-2: #14b8a6;
12
- --brand-soft: #e6f4f1;
13
- --brand-deep: #0f3d3a;
14
- --accent: #ff7a45;
15
- --accent-soft: #fff1ea;
16
- --danger: #e5484d;
17
- --danger-soft: #fdecec;
18
- --ok: #12a150;
19
- --ok-soft: #e7f6ed;
20
- --warn: #c77700;
21
- --warn-soft: #fff5e0;
22
- --r-s: 16rpx;
23
- --r-m: 24rpx;
24
- --r-l: 36rpx;
25
- --shadow: 0 8rpx 32rpx rgba(22, 33, 31, 0.06);
26
-
27
- background: var(--bg);
28
- color: var(--ink);
29
- font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
30
- font-size: 28rpx;
31
- line-height: 1.55;
32
- }
33
-
34
- view, text, textarea, input, button, scroll-view, canvas, image { box-sizing: border-box; }
35
-
36
- button { padding: 0; margin: 0; background: none; border: 0; border-radius: 0; line-height: inherit; font-size: inherit; color: inherit; text-align: center; }
37
- button::after { border: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/assets/icon.png DELETED
Binary file (59.4 kB)
 
BeadQR/miniprogram/assets/miniprogram-code.png DELETED
Binary file (98 kB)
 
BeadQR/miniprogram/assets/shield.svg DELETED
BeadQR/miniprogram/components/privacy-popup/index.js DELETED
@@ -1,40 +0,0 @@
1
- // 隐私保护指引弹窗:用户首次调用相册/相机/保存到相册等隐私接口时由微信触发
2
- Component({
3
- data: { visible: false, contractName: '《豆维隐私保护指引》' },
4
-
5
- lifetimes: {
6
- attached() {
7
- if (!wx.onNeedPrivacyAuthorization) return
8
- this._listener = (resolve) => {
9
- this._resolve = resolve
10
- this.setData({ visible: true })
11
- }
12
- wx.onNeedPrivacyAuthorization(this._listener)
13
- if (wx.getPrivacySetting) {
14
- wx.getPrivacySetting({
15
- success: (res) => {
16
- if (res.privacyContractName) this.setData({ contractName: res.privacyContractName })
17
- },
18
- })
19
- }
20
- },
21
- },
22
-
23
- methods: {
24
- openContract() {
25
- if (wx.openPrivacyContract) wx.openPrivacyContract({})
26
- },
27
- handleAgree() {
28
- if (this._resolve) this._resolve({ buttonId: 'privacy-agree-btn', event: 'agree' })
29
- this._resolve = null
30
- this.setData({ visible: false })
31
- },
32
- handleDisagree() {
33
- if (this._resolve) this._resolve({ event: 'disagree' })
34
- this._resolve = null
35
- this.setData({ visible: false })
36
- wx.showToast({ title: '未同意将无法选择照片或保存图纸', icon: 'none' })
37
- },
38
- noop() {},
39
- },
40
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/components/privacy-popup/index.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "component": true,
3
- "usingComponents": {}
4
- }
 
 
 
 
 
BeadQR/miniprogram/components/privacy-popup/index.wxml DELETED
@@ -1,13 +0,0 @@
1
- <view wx:if="{{visible}}" class="mask" catchtouchmove="noop">
2
- <view class="sheet">
3
- <view class="title">隐私保护提示</view>
4
- <view class="desc">
5
- 为了帮你把照片做成拼豆图纸,豆维需要读取你选择的照片、保存图纸到相册。照片仅在本机处理,送检的压缩副本会在内容安全检测后立即删除。使用前请阅读
6
- <text class="link" bindtap="openContract">{{contractName}}</text>。
7
- </view>
8
- <view class="actions">
9
- <button class="btn ghost" bindtap="handleDisagree">不同意</button>
10
- <button id="privacy-agree-btn" class="btn primary" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgree">同意并继续</button>
11
- </view>
12
- </view>
13
- </view>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/components/privacy-popup/index.wxss DELETED
@@ -1,10 +0,0 @@
1
- .mask { position: fixed; inset: 0; z-index: 999; background: rgba(15, 23, 22, 0.55); display: flex; align-items: flex-end; }
2
- .sheet { width: 100%; background: #fff; border-radius: 40rpx 40rpx 0 0; padding: 48rpx 40rpx calc(40rpx + env(safe-area-inset-bottom)); box-sizing: border-box; }
3
- .title { font-size: 36rpx; font-weight: 700; color: #1d2b2a; margin-bottom: 20rpx; }
4
- .desc { font-size: 28rpx; line-height: 1.7; color: #55615c; }
5
- .link { color: #0f766e; font-weight: 600; }
6
- .actions { display: flex; gap: 20rpx; margin-top: 40rpx; }
7
- .btn { flex: 1; height: 92rpx; line-height: 92rpx; border-radius: 999rpx; font-size: 30rpx; font-weight: 600; margin: 0 !important; padding: 0 !important; width: auto !important; }
8
- .btn::after { border: 0; }
9
- .ghost { background: #f1f3f2; color: #55615c; }
10
- .primary { background: #0f766e; color: #fff; }
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/pages/index/index.js DELETED
@@ -1,1073 +0,0 @@
1
- // 豆维主页面
2
- // 大数据(色格、图片、画布节点)一律挂在 this._ 上,不进 setData,保证滑动/输入流畅
3
- const { MATCH_MODES, PRESETS, preparePalette } = require('../../utils/palette')
4
- const { makeQr, createPlacement, SAMPLE_TEXT } = require('../../utils/qr')
5
- const { getImageDimensions, drawCropped, sampleImage, removeSimpleBackground } = require('../../utils/imaging')
6
- const { computeArtwork } = require('../../utils/engine')
7
- const { selfCheck } = require('../../utils/scancheck')
8
- const { renderPreview, patternPageLayout, drawPatternPage, buildInfoPages, drawWatermark } = require('../../utils/pattern')
9
- const { PdfDoc, planPatternPages, addPatternPages, addImagePage } = require('../../utils/pdf')
10
- const security = require('../../utils/security')
11
- const { createIntro } = require('../../utils/intro')
12
-
13
- const LEVELS = [
14
- { id: 'M', label: 'M', hint: '格子少' },
15
- { id: 'Q', label: 'Q', hint: '均衡' },
16
- { id: 'H', label: 'H', hint: '最耐磨' },
17
- ]
18
- const QUIET_OPTIONS = [
19
- { id: 0, label: '无' },
20
- { id: 1, label: '1 格' },
21
- { id: 2, label: '2 格' },
22
- ]
23
- const BEAD_STYLES = [
24
- { id: 'square', label: '方豆' },
25
- { id: 'round', label: '圆豆' },
26
- { id: 'hollow', label: '中空豆' },
27
- ]
28
- const GRID_COLORS = ['#0f766e', '#16211f', '#e5484d', '#3b82f6', '#a855f7']
29
- const POSITIONS = [0, 50, 100]
30
- const MAX_CANVAS_SIDE = 8000
31
- const MAX_CANVAS_PIXELS = 16000000
32
- const CROP_SIZE = 640
33
- const FILE_PREFIX = 'douwei-'
34
-
35
- function fitScale(w, h) {
36
- return Math.min(1, MAX_CANVAS_SIDE / w, MAX_CANVAS_SIDE / h, Math.sqrt(MAX_CANVAS_PIXELS / (w * h)))
37
- }
38
-
39
- function formatNumber(n) {
40
- return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
41
- }
42
-
43
- Page({
44
- data: {
45
- // 导航
46
- statusBarHeight: 20,
47
- navHeight: 44,
48
- navSolid: false,
49
- // 1. 内容
50
- text: '',
51
- textStatus: 'idle', // idle | checking | pass | risky | error
52
- scanImportLoading: false,
53
- // 2. 照片
54
- hasImage: false,
55
- imageThumb: '',
56
- imageMeta: '',
57
- removeBackground: false,
58
- bgTolerance: 22,
59
- // 3. 效果
60
- qrScale: 100,
61
- posX: 50,
62
- posY: 50,
63
- positions: POSITIONS,
64
- modeId: 'balanced',
65
- matchModes: MATCH_MODES,
66
- advancedOpen: false,
67
- fineTuneOpen: false,
68
- presets: PRESETS.map((p) => ({ id: p.id, short: p.short, detail: p.detail })),
69
- paletteId: '221',
70
- coherence: 40,
71
- cleanup: true,
72
- levels: LEVELS,
73
- level: 'H',
74
- quietOptions: QUIET_OPTIONS,
75
- quiet: 1,
76
- // 预览与统计
77
- generating: false,
78
- isSample: true,
79
- showIntro: true, // 无内容时播放动态演示,写下内容后切回真实预览
80
- textFocus: false,
81
- gridText: '—',
82
- beadsText: '—',
83
- colorsText: '—',
84
- qrGridText: '',
85
- scanState: 'idle', // idle | checking | ok | fail
86
- usage: [],
87
- usageMore: 0,
88
- usageExpanded: false,
89
- usageAll: [],
90
- // 裁切
91
- cropVisible: false,
92
- cropCheck: 'idle', // checking | pass | risky | error
93
- draftFocusX: 50,
94
- draftFocusY: 50,
95
- draftZoom: 100,
96
- draftFlipH: false,
97
- draftFlipV: false,
98
- draftRemoveBg: false,
99
- draftBgTolerance: 22,
100
- // 导出
101
- exportVisible: false,
102
- exportFormat: 'pdf',
103
- exportLayout: 'combined',
104
- exportQuality: 'hd',
105
- beadStyles: BEAD_STYLES,
106
- beadStyle: 'square',
107
- gridEnabled: true,
108
- gridInterval: 10,
109
- gridColors: GRID_COLORS,
110
- gridColor: '#0f766e',
111
- watermark: '',
112
- watermarkStatus: 'idle',
113
- exporting: false,
114
- exportStep: '',
115
- exportResult: null, // { kind, files: [{ name, path }] }
116
- },
117
-
118
- // —— 生命周期 ——
119
-
120
- onLoad() {
121
- const win = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()
122
- let navHeight = 44
123
- try {
124
- const capsule = wx.getMenuButtonBoundingClientRect()
125
- if (capsule && capsule.top) navHeight = (capsule.top - win.statusBarHeight) * 2 + capsule.height
126
- } catch (e) { /* 使用默认高度 */ }
127
- this._dpr = Math.min(3, win.pixelRatio || 2)
128
- this._timers = {}
129
- this._nodes = {}
130
- this._token = 0
131
- this.setData({ statusBarHeight: win.statusBarHeight || 20, navHeight })
132
- },
133
-
134
- onReady() {
135
- wx.createSelectorQuery().in(this)
136
- .select('#preview').fields({ node: true, size: true })
137
- .select('#work').fields({ node: true })
138
- .select('#security').fields({ node: true })
139
- .exec((res) => {
140
- this._nodes.preview = res[0] && res[0].node
141
- this._previewCss = (res[0] && res[0].width) || 300
142
- this._nodes.work = res[1] && res[1].node
143
- this._nodes.security = res[2] && res[2].node
144
- this.loadMpCode()
145
- this.schedule(0)
146
- })
147
- },
148
-
149
- onUnload() {
150
- Object.keys(this._timers).forEach((k) => clearTimeout(this._timers[k]))
151
- this.stopIntro()
152
- },
153
-
154
- onShow() {
155
- this._hidden = false
156
- if (this._nodes && this._nodes.preview) this.syncIntro()
157
- },
158
-
159
- onHide() {
160
- this._hidden = true
161
- this.pauseIntro()
162
- },
163
-
164
- onPageScroll(e) {
165
- const solid = e.scrollTop > 40
166
- if (solid !== this.data.navSolid) this.setData({ navSolid: solid })
167
- // 演示区滚出屏幕后暂停动画,省电
168
- const away = e.scrollTop > 650
169
- if (away !== this._scrolledAway) {
170
- this._scrolledAway = away
171
- this.syncIntro()
172
- }
173
- },
174
-
175
- // 分享的是小程序页面卡片,不携带用户内容
176
- onShareAppMessage() {
177
- return { title: '把一句话藏进拼豆里,微信一扫就能读出来', path: '/pages/index/index' }
178
- },
179
- onShareTimeline() {
180
- return { title: '豆维 · 可扫码的拼豆图纸生成器' }
181
- },
182
-
183
- loadMpCode() {
184
- if (!this._nodes.work) return
185
- const image = this._nodes.work.createImage()
186
- image.onload = () => { this._mpCode = image }
187
- image.onerror = () => { this._mpCode = null }
188
- image.src = '/assets/miniprogram-code.png'
189
- },
190
-
191
- debounce(key, fn, wait) {
192
- clearTimeout(this._timers[key])
193
- this._timers[key] = setTimeout(fn, wait)
194
- },
195
-
196
- noop() {},
197
-
198
- openPrivacy() {
199
- if (wx.openPrivacyContract) {
200
- wx.openPrivacyContract({ fail: () => wx.showToast({ title: '请在微信内查看隐私保护指引', icon: 'none' }) })
201
- }
202
- },
203
-
204
- showRules() {
205
- wx.showModal({
206
- title: '内容规范',
207
- content: '二维码内容、水印和照片都会经过微信内容安全检测。请勿输入或上传违法违规、色情低俗、暴力恐怖、侵犯他人权益等内容;检测未通过或检测异常时无法生成与导出。如发现不良内容,可通过页面底部“投诉与反馈”告诉我们。',
208
- showCancel: false,
209
- confirmText: '我知道了',
210
- })
211
- },
212
-
213
- // —— 生成管线 ——
214
-
215
- schedule(wait = 120) {
216
- this.debounce('run', () => this.run(), wait)
217
- },
218
-
219
- run() {
220
- if (!this._nodes.preview || !this._nodes.work) return
221
- // 没写内容、也没选照片:示例二维码没有意义,改为播放动态演示
222
- if (!this.data.text.trim() && !this._source) {
223
- this._token += 1
224
- this.setData({
225
- showIntro: true, generating: false, isSample: true, scanState: 'idle', usage: [], usageAll: [], usageMore: 0,
226
- }, () => this.syncIntro())
227
- return
228
- }
229
- this.stopIntro()
230
- if (this.data.showIntro) this.setData({ showIntro: false })
231
- const token = ++this._token
232
- this.setData({ generating: true }, () => {
233
- // 先让“生成中”状态渲染一帧,再做同步计算
234
- setTimeout(() => {
235
- if (token !== this._token) return
236
- try {
237
- this.compute()
238
- this.afterCompute(token)
239
- } catch (error) {
240
- console.error('生成失败', error)
241
- this.setData({ generating: false })
242
- wx.showToast({ title: '内容太长或图片异常,请调整后重试', icon: 'none' })
243
- }
244
- }, 16)
245
- })
246
- },
247
-
248
- compute() {
249
- const d = this.data
250
- const text = d.text.trim()
251
- const qrKey = `${text}|${d.level}`
252
- if (!this._qr || this._qrKey !== qrKey) {
253
- this._qr = makeQr(text, d.level)
254
- this._qrKey = qrKey
255
- }
256
- const src = this._source
257
- const placement = createPlacement(this._qr.size, {
258
- scale: d.qrScale, posX: d.posX, posY: d.posY, quiet: d.quiet, hasImage: Boolean(src),
259
- })
260
- let base = null
261
- if (src) {
262
- const c = src.crop
263
- const baseKey = `${placement.size}|${c.focusX}|${c.focusY}|${c.zoom}|${c.flipH}|${c.flipV}|${d.removeBackground}|${d.bgTolerance}`
264
- if (this._baseKey !== baseKey) {
265
- const sampled = sampleImage(this._nodes.work, src.image, placement.size, c)
266
- if (d.removeBackground) sampled.mask = removeSimpleBackground(sampled.rgb, placement.size, d.bgTolerance)
267
- this._base = sampled
268
- this._baseKey = baseKey
269
- }
270
- base = this._base
271
- }
272
- const pal = preparePalette(d.paletteId)
273
- this._artwork = computeArtwork(base, this._qr, placement, pal, {
274
- modeId: d.modeId, coherence: d.coherence, cleanup: d.cleanup,
275
- })
276
- },
277
-
278
- afterCompute(token) {
279
- const art = this._artwork
280
- this.drawPreview()
281
- const usageAll = art.usage.map((u) => ({ id: u.id, hex: u.hex, count: u.count }))
282
- this.setData({
283
- generating: false,
284
- isSample: this._qr.isSample,
285
- gridText: `${art.size}×${art.size}`,
286
- beadsText: formatNumber(art.beads),
287
- colorsText: String(art.usage.length),
288
- qrGridText: `${this._qr.size}×${this._qr.size}`,
289
- usage: usageAll.slice(0, 12),
290
- usageAll: this.data.usageExpanded ? usageAll : [],
291
- usageMore: Math.max(0, usageAll.length - 12),
292
- scanState: 'checking',
293
- })
294
- this._usageAll = usageAll
295
- // 扫码自检放到下一轮,避免阻塞预览出图
296
- this.debounce('scan', () => {
297
- if (token !== this._token) return
298
- const expected = this.data.text.trim() || SAMPLE_TEXT
299
- const result = selfCheck(art, expected)
300
- if (token === this._token) this.setData({ scanState: result.ok ? 'ok' : 'fail' })
301
- }, 60)
302
- },
303
-
304
- drawPreview() {
305
- const node = this._nodes.preview
306
- if (!node || !this._artwork) return
307
- const px = Math.round(this._previewCss * this._dpr)
308
- renderPreview(node, this._artwork, px, this.data.beadStyle)
309
- },
310
-
311
- // —— 动态演示 ——
312
-
313
- syncIntro() {
314
- const should = this.data.showIntro && !this._hidden && !this._scrolledAway && !this.data.cropVisible
315
- if (should) this.startIntro()
316
- else this.pauseIntro()
317
- },
318
-
319
- startIntro() {
320
- if (this._introPlayer) return
321
- const node = this._nodes.preview
322
- if (!node || !this._nodes.work) return
323
- if (!this._intro) {
324
- try {
325
- this._intro = createIntro({ workCanvas: this._nodes.work })
326
- } catch (error) {
327
- console.error('演示动画初始化失败', error)
328
- return
329
- }
330
- }
331
- const px = Math.round(this._previewCss * this._dpr)
332
- node.width = px
333
- node.height = px
334
- const ctx = node.getContext('2d')
335
- const raf = node.requestAnimationFrame ? (f) => node.requestAnimationFrame(f) : (f) => setTimeout(f, 16)
336
- const caf = node.cancelAnimationFrame ? (id) => node.cancelAnimationFrame(id) : (id) => clearTimeout(id)
337
- const player = { start: Date.now() - (this._introElapsed || 0), last: 0, id: 0, caf }
338
- const tick = () => {
339
- if (this._introPlayer !== player) return
340
- const now = Date.now()
341
- if (now - player.last >= 30) { // 约 30 帧/秒,流畅且省电
342
- player.last = now
343
- this._introElapsed = now - player.start
344
- this._intro.draw(ctx, this._introElapsed / 1000, px)
345
- }
346
- player.id = raf(tick)
347
- }
348
- this._introPlayer = player
349
- tick()
350
- },
351
-
352
- pauseIntro() {
353
- const player = this._introPlayer
354
- if (!player) return
355
- this._introPlayer = null
356
- player.caf(player.id)
357
- },
358
-
359
- stopIntro() {
360
- this.pauseIntro()
361
- this._introElapsed = 0
362
- },
363
-
364
- replayIntro() {
365
- if (!this.data.showIntro) return
366
- this.stopIntro()
367
- this.syncIntro()
368
- },
369
-
370
- // 引导用户开始:滚到第 1 步并弹出键盘
371
- startCreate() {
372
- wx.pageScrollTo({
373
- selector: '#step1',
374
- offsetTop: -(this.data.statusBarHeight + this.data.navHeight + 12),
375
- duration: 300,
376
- })
377
- setTimeout(() => this.setData({ textFocus: true }), 320)
378
- },
379
-
380
- onTextBlur() {
381
- if (this.data.textFocus) this.setData({ textFocus: false })
382
- },
383
-
384
- // —— 1. 内容 ——
385
-
386
- onTextInput(e) {
387
- const text = e.detail.value
388
- this.setData({ text, textStatus: text.trim() ? 'checking' : 'idle' })
389
- this.schedule(220)
390
- this.debounce('textCheck', () => this.verifyText(text), 650)
391
- },
392
-
393
- clearText() {
394
- this.setData({ text: '', textStatus: 'idle' })
395
- this.schedule(0)
396
- },
397
-
398
- verifyText(raw) {
399
- const value = String(raw || '').trim()
400
- if (!value) return
401
- security.checkText(value, 2).then(({ status }) => {
402
- if (this.data.text.trim() !== value) return
403
- this.setData({ textStatus: status })
404
- if (status === 'risky') wx.showToast({ title: security.MESSAGES.risky, icon: 'none' })
405
- })
406
- },
407
-
408
- scanImport() {
409
- wx.scanCode({
410
- onlyFromCamera: false,
411
- scanType: ['qrCode'],
412
- success: (res) => {
413
- const decoded = String(res.result || '').trim().slice(0, 120)
414
- if (!decoded) { wx.showToast({ title: '二维码里没有内容', icon: 'none' }); return }
415
- this.setData({ scanImportLoading: true })
416
- // 扫码得到的内容同样先过内容安全检测,通过后才填入
417
- security.checkText(decoded, 2).then(({ status }) => {
418
- this.setData({ scanImportLoading: false })
419
- if (status !== 'pass') { wx.showToast({ title: security.MESSAGES[status], icon: 'none' }); return }
420
- this.setData({ text: decoded, textStatus: 'pass' })
421
- this.schedule(0)
422
- wx.showToast({ title: '已导入二维码内容', icon: 'none' })
423
- })
424
- },
425
- fail: (err) => {
426
- if (err && /cancel/i.test(err.errMsg || '')) return
427
- wx.showToast({ title: '没有识别到二维码', icon: 'none' })
428
- },
429
- })
430
- },
431
-
432
- // —— 2. 照片 ——
433
-
434
- loadImage(path, info) {
435
- return new Promise((resolve, reject) => {
436
- const image = this._nodes.work.createImage()
437
- image.onload = () => {
438
- if (info && info.width) { image._w = info.width; image._h = info.height }
439
- resolve(image)
440
- }
441
- image.onerror = () => reject(new Error('IMAGE_LOAD_FAILED'))
442
- image.src = path
443
- })
444
- },
445
-
446
- chooseImage() {
447
- wx.chooseMedia({
448
- count: 1,
449
- mediaType: ['image'],
450
- sizeType: ['compressed'],
451
- sourceType: ['album', 'camera'],
452
- success: (res) => {
453
- const file = res.tempFiles && res.tempFiles[0]
454
- if (file && file.tempFilePath) this.prepareImage(file.tempFilePath)
455
- },
456
- fail: (err) => {
457
- if (err && /cancel/i.test(err.errMsg || '')) return
458
- wx.showToast({ title: '没有选到照片', icon: 'none' })
459
- },
460
- })
461
- },
462
-
463
- prepareImage(path) {
464
- wx.showLoading({ title: '读取照片…', mask: true })
465
- new Promise((resolve) => {
466
- wx.getImageInfo({ src: path, success: resolve, fail: () => resolve(null) })
467
- })
468
- .then((info) => this.loadImage(path, info).then((image) => ({ image, info })))
469
- .then(({ image }) => {
470
- wx.hideLoading()
471
- this._pending = { image, path }
472
- // 选图后立即在后台做内容安全检测;同时打开构图编辑,检测通过后才能使用
473
- this.startImageCheck(this._pending)
474
- this.openCrop(true)
475
- })
476
- .catch(() => {
477
- wx.hideLoading()
478
- wx.showToast({ title: '照片读取失败,请换一张 JPG / PNG', icon: 'none' })
479
- })
480
- },
481
-
482
- startImageCheck(pending) {
483
- pending.status = 'checking'
484
- this.setData({ cropCheck: 'checking' })
485
- pending.check = security.checkImage(pending.path, pending.image, this._nodes.security).then(({ status }) => {
486
- pending.status = status
487
- if (this._pending === pending) {
488
- this.setData({ cropCheck: status })
489
- if (pending.applyWhenPassed && status === 'pass') this.applyCrop()
490
- pending.applyWhenPassed = false
491
- }
492
- return status
493
- })
494
- },
495
-
496
- retryImageCheck() {
497
- if (this._pending) this.startImageCheck(this._pending)
498
- },
499
-
500
- editImage() {
501
- if (!this._source) return
502
- this._pending = { image: this._source.image, path: this._source.path, status: 'pass' }
503
- this.setData({ cropCheck: 'pass' })
504
- this.openCrop(false)
505
- },
506
-
507
- removeImage() {
508
- this._source = null
509
- this._base = null
510
- this._baseKey = ''
511
- this.setData({ hasImage: false, imageThumb: '', imageMeta: '', removeBackground: false })
512
- this.schedule(0)
513
- },
514
-
515
- // —— 裁切编辑 ——
516
-
517
- openCrop(isNew) {
518
- const crop = !isNew && this._source ? this._source.crop : null
519
- this.pauseIntro()
520
- this.setData({
521
- cropVisible: true,
522
- draftFocusX: crop ? crop.focusX : 50,
523
- draftFocusY: crop ? crop.focusY : 50,
524
- draftZoom: crop ? Math.round(crop.zoom * 100) : 100,
525
- draftFlipH: crop ? crop.flipH : false,
526
- draftFlipV: crop ? crop.flipV : false,
527
- draftRemoveBg: isNew ? false : this.data.removeBackground,
528
- draftBgTolerance: isNew ? 22 : this.data.bgTolerance,
529
- }, () => {
530
- wx.createSelectorQuery().in(this)
531
- .select('#crop').fields({ node: true, size: true })
532
- .select('#crop').boundingClientRect()
533
- .exec((res) => {
534
- this._nodes.crop = res[0] && res[0].node
535
- this._cropCss = (res[0] && res[0].width) || 300
536
- this._cropRect = res[1] || { left: 0, top: 0 }
537
- this.drawCrop()
538
- })
539
- })
540
- },
541
-
542
- drawCrop() {
543
- const node = this._nodes.crop
544
- const pending = this._pending
545
- if (!node || !pending) return
546
- node.width = CROP_SIZE
547
- node.height = CROP_SIZE
548
- const ctx = node.getContext('2d')
549
- const d = this.data
550
- ctx.imageSmoothingEnabled = true
551
- drawCropped(ctx, pending.image, CROP_SIZE, CROP_SIZE, {
552
- focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV,
553
- })
554
- // 三分线,仅辅助构图
555
- ctx.strokeStyle = 'rgba(255,255,255,0.5)'
556
- ctx.lineWidth = 1.5
557
- ctx.beginPath()
558
- for (let i = 1; i < 3; i += 1) {
559
- const p = Math.round(CROP_SIZE * i / 3) + 0.5
560
- ctx.moveTo(p, 0); ctx.lineTo(p, CROP_SIZE)
561
- ctx.moveTo(0, p); ctx.lineTo(CROP_SIZE, p)
562
- }
563
- ctx.stroke()
564
- },
565
-
566
- touchPoint(t) {
567
- const rect = this._cropRect || { left: 0, top: 0 }
568
- const x = t.clientX == null ? (t.pageX == null ? t.x : t.pageX) : t.clientX
569
- const y = t.clientY == null ? (t.pageY == null ? t.y : t.pageY) : t.clientY
570
- return { x: x - rect.left, y: y - rect.top }
571
- },
572
-
573
- touchDistance(touches) {
574
- const a = this.touchPoint(touches[0])
575
- const b = this.touchPoint(touches[1])
576
- return Math.hypot(b.x - a.x, b.y - a.y)
577
- },
578
-
579
- onCropStart(e) {
580
- const touches = e.touches || []
581
- if (!touches[0]) return
582
- if (touches.length >= 2) {
583
- this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom }
584
- return
585
- }
586
- this._gesture = { type: 'drag', ...this.touchPoint(touches[0]), focusX: this.data.draftFocusX, focusY: this.data.draftFocusY }
587
- },
588
-
589
- onCropMove(e) {
590
- const touches = e.touches || []
591
- const g = this._gesture
592
- const pending = this._pending
593
- if (!touches[0] || !g || !pending) return
594
- if (touches.length >= 2) {
595
- if (g.type !== 'pinch') {
596
- this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom }
597
- return
598
- }
599
- const zoom = Math.max(100, Math.min(400, Math.round(g.zoom * this.touchDistance(touches) / g.distance)))
600
- this.setData({ draftZoom: zoom })
601
- this.debounce('crop', () => this.drawCrop(), 16)
602
- return
603
- }
604
- if (g.type !== 'drag') return
605
- const p = this.touchPoint(touches[0])
606
- const cssScale = CROP_SIZE / (this._cropCss || CROP_SIZE)
607
- const src = getImageDimensions(pending.image)
608
- const scale = Math.max(CROP_SIZE / src.width, CROP_SIZE / src.height) * (this.data.draftZoom / 100)
609
- const w = src.width * scale
610
- const h = src.height * scale
611
- const clamp = (v) => Math.max(0, Math.min(100, v))
612
- const dirX = this.data.draftFlipH ? -1 : 1
613
- const dirY = this.data.draftFlipV ? -1 : 1
614
- const fx = Math.abs(w - CROP_SIZE) < 0.01 ? g.focusX : clamp(g.focusX + dirX * ((p.x - g.x) * cssScale / (CROP_SIZE - w)) * 100)
615
- const fy = Math.abs(h - CROP_SIZE) < 0.01 ? g.focusY : clamp(g.focusY + dirY * ((p.y - g.y) * cssScale / (CROP_SIZE - h)) * 100)
616
- this.setData({ draftFocusX: fx, draftFocusY: fy })
617
- this.debounce('crop', () => this.drawCrop(), 16)
618
- },
619
-
620
- onCropEnd() {
621
- this._gesture = null
622
- clearTimeout(this._timers.crop)
623
- this.drawCrop()
624
- },
625
-
626
- onDraftZoom(e) {
627
- this.setData({ draftZoom: Number(e.detail.value) })
628
- this.debounce('crop', () => this.drawCrop(), 16)
629
- },
630
- resetCrop() { this.setData({ draftFocusX: 50, draftFocusY: 50, draftZoom: 100 }, () => this.drawCrop()) },
631
- flipH() { this.setData({ draftFlipH: !this.data.draftFlipH }, () => this.drawCrop()) },
632
- flipV() { this.setData({ draftFlipV: !this.data.draftFlipV }, () => this.drawCrop()) },
633
- onDraftRemoveBg(e) { this.setData({ draftRemoveBg: e.detail.value }) },
634
- onDraftBgTolerance(e) { this.setData({ draftBgTolerance: Number(e.detail.value) }) },
635
-
636
- cancelCrop() {
637
- if (this._pending) this._pending.applyWhenPassed = false
638
- this.setData({ cropVisible: false }, () => this.syncIntro())
639
- },
640
-
641
- applyCrop() {
642
- const pending = this._pending
643
- if (!pending) return
644
- if (pending.status === 'checking') {
645
- // 检测未完成:记住用户意图,通过后自动应用
646
- pending.applyWhenPassed = true
647
- wx.showToast({ title: '正在做内容安全检测,通过后自动应用', icon: 'none' })
648
- return
649
- }
650
- if (pending.status !== 'pass') {
651
- wx.showToast({ title: security.MESSAGES[pending.status] || security.MESSAGES.error, icon: 'none' })
652
- return
653
- }
654
- const d = this.data
655
- const src = getImageDimensions(pending.image)
656
- this._source = {
657
- image: pending.image,
658
- path: pending.path,
659
- crop: { focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV },
660
- }
661
- this._baseKey = ''
662
- this.setData({
663
- cropVisible: false,
664
- hasImage: true,
665
- imageThumb: pending.path,
666
- imageMeta: `${src.width}×${src.height} · 已通过安全检测`,
667
- removeBackground: d.draftRemoveBg,
668
- bgTolerance: d.draftBgTolerance,
669
- // 首次导入照片时,二维码默认铺满全图;用户可再缩小
670
- })
671
- this.schedule(0)
672
- },
673
-
674
- // —— 3. 效果 ——
675
-
676
- onScaleChanging(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(160) },
677
- onScaleChange(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(0) },
678
- pickPosition(e) {
679
- const { x, y } = e.currentTarget.dataset
680
- this.setData({ posX: Number(x), posY: Number(y) })
681
- this.schedule(0)
682
- },
683
- toggleFineTune() { this.setData({ fineTuneOpen: !this.data.fineTuneOpen }) },
684
- onPosX(e) { this.setData({ posX: Number(e.detail.value) }); this.schedule(100) },
685
- onPosY(e) { this.setData({ posY: Number(e.detail.value) }); this.schedule(100) },
686
- pickMode(e) { this.setData({ modeId: e.currentTarget.dataset.id }); this.schedule(0) },
687
- toggleAdvanced() { this.setData({ advancedOpen: !this.data.advancedOpen }) },
688
- pickPalette(e) { this.setData({ paletteId: e.currentTarget.dataset.id }); this.schedule(0) },
689
- onCoherence(e) { this.setData({ coherence: Number(e.detail.value) }); this.schedule(200) },
690
- onCleanup(e) { this.setData({ cleanup: e.detail.value }); this.schedule(0) },
691
- pickLevel(e) { this.setData({ level: e.currentTarget.dataset.id }); this.schedule(0) },
692
- pickQuiet(e) { this.setData({ quiet: Number(e.currentTarget.dataset.id) }); this.schedule(0) },
693
- onBgToggle(e) { this.setData({ removeBackground: e.detail.value }); this.schedule(0) },
694
-
695
- applyScanFix() {
696
- // 自检未通过时的一键修复:扫码优先 + 1 格白边 + H 级纠错
697
- this.setData({ modeId: 'strict', quiet: Math.max(1, this.data.quiet), level: 'H' })
698
- this.schedule(0)
699
- wx.showToast({ title: '已切换为扫码优先', icon: 'none' })
700
- },
701
-
702
- toggleUsage() {
703
- const expanded = !this.data.usageExpanded
704
- this.setData({ usageExpanded: expanded, usageAll: expanded ? (this._usageAll || []) : [] })
705
- },
706
-
707
- // 生成当前预览图,用微信“识别图中二维码”实测
708
- verifyScan() {
709
- const node = this._nodes.preview
710
- if (!node) return
711
- wx.canvasToTempFilePath({
712
- canvas: node,
713
- fileType: 'png',
714
- success: (res) => {
715
- wx.previewImage({ urls: [res.tempFilePath], showmenu: true })
716
- wx.showToast({ title: '长按图片,选择“识别图中二维码”', icon: 'none', duration: 2500 })
717
- },
718
- fail: () => wx.showToast({ title: '预览生成失败,请重试', icon: 'none' }),
719
- })
720
- },
721
-
722
- // —— 导出 ——
723
-
724
- openExport() {
725
- if (!this.data.text.trim()) {
726
- this.startCreate()
727
- return
728
- }
729
- if (!this._artwork) return
730
- if (this.data.textStatus === 'risky') {
731
- wx.showToast({ title: security.MESSAGES.risky, icon: 'none' })
732
- return
733
- }
734
- this.setData({ exportVisible: true, exportResult: null })
735
- },
736
- closeExport() {
737
- if (this.data.exporting) return
738
- this.setData({ exportVisible: false })
739
- },
740
- pickFormat(e) { this.setData({ exportFormat: e.currentTarget.dataset.id, exportResult: null }) },
741
- pickLayout(e) { this.setData({ exportLayout: e.currentTarget.dataset.id, exportResult: null }) },
742
- pickQuality(e) { this.setData({ exportQuality: e.currentTarget.dataset.id, exportResult: null }) },
743
- pickBeadStyle(e) {
744
- this.setData({ beadStyle: e.currentTarget.dataset.id, exportResult: null }, () => this.drawPreview())
745
- },
746
- onGridToggle(e) { this.setData({ gridEnabled: e.detail.value, exportResult: null }) },
747
- pickInterval(e) { this.setData({ gridInterval: Number(e.currentTarget.dataset.id), exportResult: null }) },
748
- pickGridColor(e) { this.setData({ gridColor: e.currentTarget.dataset.color, exportResult: null }) },
749
- onWatermark(e) {
750
- const watermark = e.detail.value
751
- this.setData({ watermark, watermarkStatus: watermark.trim() ? 'checking' : 'idle', exportResult: null })
752
- this.debounce('wmCheck', () => {
753
- const value = watermark.trim()
754
- if (!value) return
755
- security.checkText(value, 1).then(({ status }) => {
756
- if (this.data.watermark.trim() === value) this.setData({ watermarkStatus: status })
757
- })
758
- }, 650)
759
- },
760
-
761
- gridSettings() {
762
- return { enabled: this.data.gridEnabled, interval: this.data.gridInterval, color: this.data.gridColor, opacity: 60 }
763
- },
764
-
765
- capture(fileType, quality) {
766
- return new Promise((resolve, reject) => {
767
- wx.canvasToTempFilePath({
768
- canvas: this._nodes.work,
769
- fileType,
770
- quality,
771
- success: (res) => resolve(res.tempFilePath),
772
- fail: reject,
773
- })
774
- })
775
- },
776
-
777
- step(label) {
778
- return new Promise((resolve) => this.setData({ exportStep: label }, () => setTimeout(resolve, 16)))
779
- },
780
-
781
- async startExport() {
782
- if (this.data.exporting || !this._artwork) return
783
- this.setData({ exporting: true, exportResult: null })
784
- try {
785
- // 平台合规:导出前再次确认所有用户文本均已通过内容安全检测(已通过的走缓存,无额外等待)
786
- await this.step('内容安全校验…')
787
- const check = await security.checkTexts([
788
- { value: this.data.text, scene: 2 },
789
- { value: this.data.watermark, scene: 1 },
790
- ])
791
- if (check.status !== 'pass') {
792
- wx.showToast({ title: security.MESSAGES[check.status], icon: 'none' })
793
- if (check.status === 'risky') this.setData({ textStatus: security.isTextPassed(this.data.text) ? 'pass' : 'risky' })
794
- return
795
- }
796
- if (this._source && !this.data.hasImage) throw new Error('IMAGE_STATE')
797
- this.cleanOldFiles()
798
- if (this.data.exportFormat === 'png') await this.exportPng()
799
- else await this.exportPdf()
800
- } catch (error) {
801
- console.error('导出失败', error)
802
- wx.showToast({ title: '导出失败,请重试(可尝试“标准”清晰度)', icon: 'none' })
803
- } finally {
804
- this.setData({ exporting: false, exportStep: '' })
805
- }
806
- },
807
-
808
- // 按清晰度求每格像素:高清长边约 4000px,标准约 2400px
809
- pngCellSize() {
810
- const size = this._artwork.size
811
- const maxSide = this.data.exportQuality === 'hd' ? 4000 : 2400
812
- let cell = Math.max(12, Math.min(72, Math.floor(maxSide / (size + 5))))
813
- while (cell > 12) {
814
- const L = patternPageLayout(size, cell)
815
- if (Math.max(L.width, L.height) <= maxSide) break
816
- cell -= 1
817
- }
818
- return cell
819
- },
820
-
821
- patternMeta() {
822
- const art = this._artwork
823
- return `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${art.pal.label} · 扫码内容:${this.data.text.trim()}`
824
- },
825
-
826
- infoParams(width) {
827
- return {
828
- artwork: this._artwork,
829
- width,
830
- text: this.data.text.trim(),
831
- paletteLabel: `${this._artwork.pal.label}(本作品用 ${this._artwork.usage.length} 色)`,
832
- mpCode: this._mpCode || null,
833
- thumbnail: true,
834
- }
835
- },
836
-
837
- async exportPng() {
838
- const node = this._nodes.work
839
- const art = this._artwork
840
- const cell = this.pngCellSize()
841
- const L = patternPageLayout(art.size, cell)
842
- const wm = this.data.watermark.trim()
843
- const drawPattern = (ctx) => drawPatternPage(ctx, art, { cell, style: this.data.beadStyle, grid: this.gridSettings(), meta: this.patternMeta() })
844
- const files = []
845
- if (this.data.exportLayout === 'separate') {
846
- await this.step('绘制图纸…')
847
- node.width = L.width
848
- node.height = L.height
849
- const ctx = node.getContext('2d')
850
- drawPattern(ctx)
851
- drawWatermark(ctx, L.width, L.height, wm)
852
- files.push({ name: '拼豆图纸', path: await this.capture('png') })
853
- const info = buildInfoPages(this.infoParams(this.data.exportQuality === 'hd' ? 2000 : 1400))
854
- for (let p = 0; p < info.pageCount; p += 1) {
855
- await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`)
856
- node.width = info.width
857
- node.height = info.height
858
- const c = node.getContext('2d')
859
- info.draw(c, p)
860
- drawWatermark(c, info.width, info.height, wm)
861
- files.push({ name: info.pageCount > 1 ? `用豆清单 ${p + 1}` : '用豆清单', path: await this.capture('png') })
862
- }
863
- } else {
864
- await this.step('绘制图纸与清单…')
865
- const info = buildInfoPages(this.infoParams(L.width))
866
- const gap = Math.round(L.width * 0.02)
867
- const totalH = L.height + info.pageCount * (info.height + gap)
868
- const scale = fitScale(L.width, totalH)
869
- node.width = Math.round(L.width * scale)
870
- node.height = Math.round(totalH * scale)
871
- const ctx = node.getContext('2d')
872
- ctx.fillStyle = '#e8e5de'
873
- ctx.fillRect(0, 0, node.width, node.height)
874
- ctx.scale(scale, scale)
875
- drawPattern(ctx)
876
- for (let p = 0; p < info.pageCount; p += 1) {
877
- ctx.save()
878
- ctx.translate(0, L.height + gap + p * (info.height + gap))
879
- info.draw(ctx, p)
880
- ctx.restore()
881
- }
882
- ctx.setTransform(1, 0, 0, 1, 0, 0)
883
- drawWatermark(ctx, node.width, node.height, wm)
884
- files.push({ name: '图纸 + 用豆清单', path: await this.capture('png') })
885
- }
886
- await this.step('保存到相册…')
887
- const saved = await this.saveImages(files.map((f) => f.path))
888
- this.setData({ exportResult: { kind: 'png', saved, files } })
889
- if (saved) wx.showToast({ title: `${files.length} 张图片已保存`, icon: 'success' })
890
- },
891
-
892
- saveImages(paths) {
893
- return paths.reduce((chain, path) => chain.then((ok) => (ok ? new Promise((resolve) => {
894
- wx.saveImageToPhotosAlbum({
895
- filePath: path,
896
- success: () => resolve(true),
897
- fail: (err) => {
898
- if (err && /auth|deny/i.test(err.errMsg || '')) {
899
- wx.showModal({
900
- title: '需要相册权限',
901
- content: '请允许保存到相册,才能把图纸存到手机里',
902
- confirmText: '去设置',
903
- success: (r) => { if (r.confirm) wx.openSetting() },
904
- })
905
- }
906
- resolve(false)
907
- },
908
- })
909
- }) : false)), Promise.resolve(true))
910
- },
911
-
912
- previewResult() {
913
- const result = this.data.exportResult
914
- if (!result) return
915
- if (result.kind === 'png') {
916
- wx.previewImage({ urls: result.files.map((f) => f.path), showmenu: true })
917
- } else {
918
- this.openPdf(result.files[0].path)
919
- }
920
- },
921
-
922
- openResultFile(e) {
923
- const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)]
924
- if (!file) return
925
- if (this.data.exportResult.kind === 'png') wx.previewImage({ urls: [file.path], showmenu: true })
926
- else this.openPdf(file.path)
927
- },
928
-
929
- shareResultFile(e) {
930
- const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)]
931
- if (!file || !wx.shareFileMessage) {
932
- wx.showToast({ title: '当前微信版本不支持,请先打开再转发', icon: 'none' })
933
- return
934
- }
935
- wx.shareFileMessage({ filePath: file.path, fileName: file.fileName || `${file.name}.pdf` })
936
- },
937
-
938
- openPdf(path) {
939
- wx.openDocument({ filePath: path, fileType: 'pdf', showMenu: true })
940
- },
941
-
942
- cleanOldFiles() {
943
- try {
944
- const fs = wx.getFileSystemManager()
945
- fs.readdirSync(wx.env.USER_DATA_PATH)
946
- .filter((name) => name.indexOf(FILE_PREFIX) === 0)
947
- .forEach((name) => { try { fs.unlinkSync(`${wx.env.USER_DATA_PATH}/${name}`) } catch (e) { /* 忽略 */ } })
948
- } catch (e) { /* 忽略 */ }
949
- },
950
-
951
- async canvasJpeg(width, height, draw, quality = 0.9) {
952
- const node = this._nodes.work
953
- node.width = width
954
- node.height = height
955
- const ctx = node.getContext('2d')
956
- draw(ctx)
957
- const path = await this.capture('jpg', quality)
958
- return wx.getFileSystemManager().readFileSync(path)
959
- },
960
-
961
- // 页眉(中文)渲染为小图,矢量页面上方使用
962
- async renderHeader(title, sub) {
963
- const w = 1540
964
- const h = 80
965
- const data = await this.canvasJpeg(w, h, (ctx) => {
966
- ctx.fillStyle = '#ffffff'
967
- ctx.fillRect(0, 0, w, h)
968
- ctx.fillStyle = '#16211f'
969
- ctx.font = 'bold 34px sans-serif'
970
- ctx.textBaseline = 'middle'
971
- ctx.fillText(title, 0, 30)
972
- ctx.fillStyle = '#6f7a76'
973
- ctx.font = '22px sans-serif'
974
- ctx.fillText(sub, 0, 64)
975
- ctx.fillStyle = '#0f766e'
976
- ctx.font = 'bold 24px sans-serif'
977
- ctx.textAlign = 'right'
978
- ctx.fillText('豆维 · 可扫码的拼豆', w, 30)
979
- }, 0.92)
980
- return { data, width: w, height: h }
981
- },
982
-
983
- // 水印转为 1-bit 蒙版,矢量页上以半透明平铺
984
- watermarkMask(doc, text) {
985
- const value = String(text || '').trim()
986
- if (!value) return null
987
- const node = this._nodes.work
988
- const fontSize = 64
989
- node.width = 16
990
- node.height = 16
991
- let ctx = node.getContext('2d')
992
- ctx.font = `bold ${fontSize}px sans-serif`
993
- const width = Math.min(2000, Math.ceil(ctx.measureText(value).width) + 16)
994
- const height = Math.round(fontSize * 1.4)
995
- node.width = width
996
- node.height = height
997
- ctx = node.getContext('2d')
998
- ctx.clearRect(0, 0, width, height)
999
- ctx.fillStyle = '#000000'
1000
- ctx.font = `bold ${fontSize}px sans-serif`
1001
- ctx.textBaseline = 'middle'
1002
- ctx.fillText(value, 8, height / 2)
1003
- const pixels = ctx.getImageData(0, 0, width, height).data
1004
- const rowBytes = Math.ceil(width / 8)
1005
- const bits = new Uint8Array(rowBytes * height).fill(0xff) // 1 = 不绘制
1006
- for (let y = 0; y < height; y += 1) {
1007
- for (let x = 0; x < width; x += 1) {
1008
- if (pixels[(y * width + x) * 4 + 3] > 110) bits[y * rowBytes + (x >> 3)] &= ~(0x80 >> (x & 7))
1009
- }
1010
- }
1011
- return { id: doc.addImageMask(bits, width, height), width, height }
1012
- },
1013
-
1014
- async buildPatternPdf(doc) {
1015
- const art = this._artwork
1016
- const plan = planPatternPages(art.size)
1017
- const headers = []
1018
- for (let i = 0; i < plan.pages.length; i += 1) {
1019
- const page = plan.pages[i]
1020
- await this.step(`绘制图纸页 ${i + 1}/${plan.pages.length}…`)
1021
- const title = page.kind === 'overview'
1022
- ? (plan.tiled ? `图纸总览 · 共分 ${plan.pages.length - 1} 块` : '拼豆图纸')
1023
- : `第 ${page.tile} 块 · 列 ${page.region.col + 1}–${page.region.col + page.region.cols} · 行 ${page.region.row + 1}–${page.region.row + page.region.rows}`
1024
- const sub = page.kind === 'overview'
1025
- ? `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${plan.tiled ? '红框编号对应后续分块页,按块拼更轻松' : '格内为 MARD 色号'}`
1026
- : `第 ${i + 1} / ${plan.pages.length} 页 · 格内为 MARD 色号 · 坐标为整张作品的行列号`
1027
- const header = await this.renderHeader(title, sub)
1028
- headers.push({ id: doc.addJpeg(header.data, header.width, header.height), width: header.width, height: header.height })
1029
- }
1030
- const watermark = this.watermarkMask(doc, this.data.watermark)
1031
- addPatternPages(doc, art, { plan, headers, style: this.data.beadStyle, grid: this.gridSettings(), watermark })
1032
- },
1033
-
1034
- async buildInfoPdf(doc) {
1035
- const info = buildInfoPages(this.infoParams(1654))
1036
- for (let p = 0; p < info.pageCount; p += 1) {
1037
- await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`)
1038
- const data = await this.canvasJpeg(info.width, info.height, (ctx) => {
1039
- info.draw(ctx, p)
1040
- drawWatermark(ctx, info.width, info.height, this.data.watermark)
1041
- }, 0.9)
1042
- addImagePage(doc, doc.addJpeg(data, info.width, info.height), info.width, info.height)
1043
- }
1044
- },
1045
-
1046
- async exportPdf() {
1047
- const fs = wx.getFileSystemManager()
1048
- const stamp = Date.now().toString(36)
1049
- const art = this._artwork
1050
- const base = `${FILE_PREFIX}${art.size}x${art.size}-${stamp}`
1051
- const files = []
1052
- const write = (doc, suffix, name) => {
1053
- const fileName = `豆维-${name}.pdf`
1054
- const path = `${wx.env.USER_DATA_PATH}/${base}-${suffix}.pdf`
1055
- fs.writeFileSync(path, doc.build(), 'binary')
1056
- files.push({ name, fileName, path })
1057
- }
1058
- if (this.data.exportLayout === 'separate') {
1059
- const patternDoc = new PdfDoc()
1060
- await this.buildPatternPdf(patternDoc)
1061
- write(patternDoc, 'pattern', '拼豆图纸')
1062
- const infoDoc = new PdfDoc()
1063
- await this.buildInfoPdf(infoDoc)
1064
- write(infoDoc, 'info', '用豆清单')
1065
- } else {
1066
- const doc = new PdfDoc()
1067
- await this.buildPatternPdf(doc)
1068
- await this.buildInfoPdf(doc)
1069
- write(doc, 'all', '图纸与用豆清单')
1070
- }
1071
- this.setData({ exportResult: { kind: 'pdf', saved: true, files } })
1072
- },
1073
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/pages/index/index.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "navigationStyle": "custom",
3
- "navigationBarTextStyle": "white",
4
- "backgroundColorTop": "#0f3d3a",
5
- "usingComponents": {}
6
- }
 
 
 
 
 
 
 
BeadQR/miniprogram/pages/index/index.wxml DELETED
@@ -1,403 +0,0 @@
1
- <!-- 自定义导航栏 -->
2
- <view class="nav {{navSolid ? 'nav-solid' : ''}}" style="padding-top: {{statusBarHeight}}px; height: {{navHeight}}px;">
3
- <view class="nav-title">
4
- <image class="nav-logo" src="/assets/icon.png" mode="aspectFill" />
5
- <text>豆维 · 可扫码的拼豆</text>
6
- </view>
7
- </view>
8
-
9
- <view class="page" style="padding-top: {{statusBarHeight + navHeight}}px;">
10
- <!-- 首屏:一句话说清楚是做什么的 -->
11
- <view class="hero">
12
- <view class="hero-badge">MARD 拼豆图纸生成器</view>
13
- <view class="hero-title">把一句话藏进拼豆里</view>
14
- <view class="hero-sub">拼好的作品用微信一扫,就能读出你写的文字或链接</view>
15
- <view class="hero-steps">
16
- <view class="hero-step"><text class="num">1</text>写一句话</view>
17
- <text class="hero-arrow">›</text>
18
- <view class="hero-step"><text class="num">2</text>选张照片</view>
19
- <text class="hero-arrow">›</text>
20
- <view class="hero-step"><text class="num">3</text>导出图纸</view>
21
- </view>
22
- </view>
23
-
24
- <!-- 实时预览:无内容时播放动态演示 -->
25
- <view class="card preview-card">
26
- <view class="preview-box" bindtap="replayIntro">
27
- <canvas id="preview" type="2d" class="preview-canvas"></canvas>
28
- <view wx:if="{{isSample && !showIntro}}" class="sample-tag">示例二维码 · 写下内容后换成你的</view>
29
- <view wx:if="{{generating}}" class="generating"><view class="spinner"></view></view>
30
- </view>
31
-
32
- <block wx:if="{{showIntro}}">
33
- <view class="intro-tip">
34
- <view class="intro-dot"></view>
35
- <text>上面是演示:写下一句话,这里就会变成你专属的可扫码图纸</text>
36
- </view>
37
- <view class="stats features">
38
- <view class="stat"><view class="stat-v">221 色</view><view class="stat-k">MARD 标准色号</view></view>
39
- <view class="stat"><view class="stat-v">A4</view><view class="stat-k">打印图纸</view></view>
40
- <view class="stat"><view class="stat-v">扫一扫</view><view class="stat-k">微信直接读出</view></view>
41
- </view>
42
- <view class="preview-actions">
43
- <view class="cta-btn" bindtap="startCreate">开始制作 · 写下第一句话</view>
44
- </view>
45
- </block>
46
-
47
- <block wx:else>
48
- <view class="scan-row">
49
- <view class="scan-badge scan-{{isSample ? 'idle' : scanState}}">
50
- <text wx:if="{{isSample}}">写下内容后,自动检查能否扫出</text>
51
- <text wx:elif="{{scanState === 'checking'}}">正在检查能否扫出…</text>
52
- <text wx:elif="{{scanState === 'ok'}}">✓ 自检通过,可以扫出内容</text>
53
- <text wx:elif="{{scanState === 'fail'}}">! 自检未扫出,建议优化</text>
54
- <text wx:else>生成后自动检查能否扫码</text>
55
- </view>
56
- <view wx:if="{{!isSample && scanState === 'fail'}}" class="mini-btn accent" bindtap="applyScanFix">一键优化</view>
57
- </view>
58
-
59
- <view class="stats">
60
- <view class="stat"><view class="stat-v">{{gridText}}</view><view class="stat-k">格子</view></view>
61
- <view class="stat"><view class="stat-v">{{beadsText}}</view><view class="stat-k">颗豆子</view></view>
62
- <view class="stat"><view class="stat-v">{{colorsText}}</view><view class="stat-k">种颜色</view></view>
63
- </view>
64
-
65
- <view class="preview-actions">
66
- <view wx:if="{{isSample}}" class="cta-btn" bindtap="startCreate">写下内容,生成你的二维码</view>
67
- <view wx:else class="ghost-btn" bindtap="verifyScan">用微信扫一扫验证</view>
68
- </view>
69
- </block>
70
- </view>
71
-
72
- <!-- 第 1 步:内容 -->
73
- <view class="card step" id="step1">
74
- <view class="step-head">
75
- <view class="step-num">1</view>
76
- <view>
77
- <view class="step-title">扫码后要看到什么?</view>
78
- <view class="step-desc">一句祝福、一个网址,或者你的名字</view>
79
- </view>
80
- </view>
81
- <view class="input-wrap {{textStatus === 'risky' ? 'input-bad' : ''}}">
82
- <textarea class="textarea" value="{{text}}" maxlength="300" auto-height placeholder="Happy Birthday!" placeholder-class="ph" focus="{{textFocus}}" bindinput="onTextInput" bindblur="onTextBlur" />
83
- <view class="input-foot">
84
- <view class="text-status status-{{textStatus}}">
85
- <text wx:if="{{textStatus === 'checking'}}">内容安全检测中…</text>
86
- <text wx:elif="{{textStatus === 'pass'}}">✓ 已通过内容安全检测</text>
87
- <text wx:elif="{{textStatus === 'risky'}}">内容可能不合规,请修改</text>
88
- <text wx:elif="{{textStatus === 'error'}}">检测暂不可用,请稍后重试</text>
89
- <text wx:else>{{text.length}}/300</text>
90
- </view>
91
- <view class="input-tools">
92
- <text wx:if="{{text}}" class="tool" bindtap="clearText">清空</text>
93
- <text class="tool tool-brand" bindtap="scanImport">{{scanImportLoading ? '识别中…' : '扫码导入'}}</text>
94
- </view>
95
- </view>
96
- </view>
97
- <view class="tip">字越少,格子越少,拼起来越快</view>
98
- </view>
99
-
100
- <!-- 第 2 步:照片 -->
101
- <view class="card step">
102
- <view class="step-head">
103
- <view class="step-num">2</view>
104
- <view>
105
- <view class="step-title">选一张照片当底图 <text class="optional">可选</text></view>
106
- <view class="step-desc">二维码会融进照片里,不选就是纯二维码</view>
107
- </view>
108
- </view>
109
- <view wx:if="{{!hasImage}}" class="upload" bindtap="chooseImage">
110
- <view class="upload-icon">+</view>
111
- <view class="upload-text">从相册选择或拍照</view>
112
- <view class="upload-hint">照片仅在手机上处理,会先经过微信内容安全检测</view>
113
- </view>
114
- <view wx:else class="photo">
115
- <image class="photo-thumb" src="{{imageThumb}}" mode="aspectFill" />
116
- <view class="photo-info">
117
- <view class="photo-meta">{{imageMeta}}</view>
118
- <view class="photo-actions">
119
- <view class="mini-btn" bindtap="editImage">调整</view>
120
- <view class="mini-btn" bindtap="chooseImage">换一张</view>
121
- <view class="mini-btn danger" bindtap="removeImage">移除</view>
122
- </view>
123
- </view>
124
- </view>
125
- <view wx:if="{{hasImage}}" class="row">
126
- <view class="row-label">去掉纯色背景</view>
127
- <switch checked="{{removeBackground}}" color="#0f766e" bindchange="onBgToggle" />
128
- </view>
129
- </view>
130
-
131
- <!-- 第 3 步:效果 -->
132
- <view class="card step">
133
- <view class="step-head">
134
- <view class="step-num">3</view>
135
- <view>
136
- <view class="step-title">调整效果</view>
137
- <view class="step-desc">默认设置已经很好用,想要更好看再微调</view>
138
- </view>
139
- </view>
140
-
141
- <view class="field-label">风格</view>
142
- <view class="seg">
143
- <view wx:for="{{matchModes}}" wx:key="id" class="seg-item {{modeId === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickMode">
144
- <view class="seg-main">{{item.label}}</view>
145
- <view class="seg-sub">{{item.hint}}</view>
146
- </view>
147
- </view>
148
-
149
- <block wx:if="{{hasImage}}">
150
- <view class="field-label">二维码大小 <text class="field-val">{{qrScale}}%</text></view>
151
- <slider min="30" max="100" step="1" value="{{qrScale}}" activeColor="#0f766e" block-size="22" bindchanging="onScaleChanging" bindchange="onScaleChange" />
152
- <block wx:if="{{qrScale < 100}}">
153
- <view class="field-label">二维码位置 <text class="link-text" bindtap="toggleFineTune">{{fineTuneOpen ? '收起微调' : '微调'}}</text></view>
154
- <view class="pos-grid">
155
- <block wx:for="{{positions}}" wx:for-item="py" wx:key="*this">
156
- <view wx:for="{{positions}}" wx:for-item="px" wx:key="*this" class="pos-cell {{posX === px && posY === py ? 'on' : ''}}" data-x="{{px}}" data-y="{{py}}" bindtap="pickPosition"></view>
157
- </block>
158
- </view>
159
- <block wx:if="{{fineTuneOpen}}">
160
- <view class="mini-label">左右 {{posX}}%</view>
161
- <slider min="0" max="100" value="{{posX}}" activeColor="#0f766e" block-size="20" bindchanging="onPosX" bindchange="onPosX" />
162
- <view class="mini-label">上下 {{posY}}%</view>
163
- <slider min="0" max="100" value="{{posY}}" activeColor="#0f766e" block-size="20" bindchanging="onPosY" bindchange="onPosY" />
164
- </block>
165
- </block>
166
- </block>
167
-
168
- <view class="advanced-toggle" bindtap="toggleAdvanced">
169
- <text>高级设置(色板、纠错、白边)</text>
170
- <text class="chev {{advancedOpen ? 'open' : ''}}">›</text>
171
- </view>
172
- <view wx:if="{{advancedOpen}}" class="advanced">
173
- <view class="field-label">色板 <text class="field-hint">颜色越少,买豆越省</text></view>
174
- <scroll-view scroll-x class="chips-scroll" enhanced show-scrollbar="{{false}}">
175
- <view class="chips">
176
- <view wx:for="{{presets}}" wx:key="id" class="chip {{paletteId === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickPalette">
177
- <view class="chip-main">{{item.short}}</view>
178
- <view class="chip-sub">{{item.detail}}</view>
179
- </view>
180
- </view>
181
- </scroll-view>
182
-
183
- <view class="field-label">色块平滑 <text class="field-val">{{coherence}}</text></view>
184
- <slider min="0" max="100" step="5" value="{{coherence}}" activeColor="#0f766e" block-size="20" bindchange="onCoherence" />
185
-
186
- <view class="row">
187
- <view>
188
- <view class="row-label">合并零散颜色</view>
189
- <view class="row-hint">把只用到一两颗的颜色换成相近色</view>
190
- </view>
191
- <switch checked="{{cleanup}}" color="#0f766e" bindchange="onCleanup" />
192
- </view>
193
-
194
- <view class="field-label">纠错等级 <text class="field-hint">等级越高越耐磨损,但格子更多</text></view>
195
- <view class="seg">
196
- <view wx:for="{{levels}}" wx:key="id" class="seg-item {{level === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickLevel">
197
- <view class="seg-main">{{item.label}}</view>
198
- <view class="seg-sub">{{item.hint}}</view>
199
- </view>
200
- </view>
201
-
202
- <view class="field-label">白边 <text class="field-hint">留 1 格白边更容易扫出</text></view>
203
- <view class="seg">
204
- <view wx:for="{{quietOptions}}" wx:key="id" class="seg-item {{quiet === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickQuiet">
205
- <view class="seg-main">{{item.label}}</view>
206
- </view>
207
- </view>
208
- <view wx:if="{{qrGridText}}" class="tip">二维码本体 {{qrGridText}} 格</view>
209
- </view>
210
- </view>
211
-
212
- <!-- 用豆清单 -->
213
- <view class="card" wx:if="{{usage.length && !showIntro}}">
214
- <view class="card-title">需要准备的豆子 <text class="field-hint">MARD 色号 · 按用量排序</text></view>
215
- <view class="usage">
216
- <view wx:for="{{usageExpanded ? usageAll : usage}}" wx:key="id" class="usage-item">
217
- <view class="swatch" style="background: {{item.hex}};"></view>
218
- <view class="usage-id">{{item.id}}</view>
219
- <view class="usage-count">{{item.count}}</view>
220
- </view>
221
- </view>
222
- <view wx:if="{{usageMore > 0}}" class="link-text center" bindtap="toggleUsage">{{usageExpanded ? '收起' : '展开全部(还有 ' + usageMore + ' 种)'}}</view>
223
- </view>
224
-
225
- <!-- 页脚:内容规范 / 投诉反馈 / 隐私 -->
226
- <view class="footer">
227
- <view class="footer-safe"><image class="footer-shield" src="/assets/shield.svg" mode="aspectFit" /><text>文字、水印与照片均经过微信内容安全检测,未通过或检测异常时不能生成与导出</text></view>
228
- <view class="footer-links">
229
- <text class="footer-link" bindtap="showRules">内容规范</text>
230
- <text class="dot">·</text>
231
- <button class="footer-link footer-btn" open-type="feedback">投诉与反馈</button>
232
- <text class="dot">·</text>
233
- <text class="footer-link" bindtap="openPrivacy">隐私保护指引</text>
234
- </view>
235
- <view class="footer-copy">豆维 · 可扫码的拼豆</view>
236
- </view>
237
- </view>
238
-
239
- <!-- 底部操作栏 -->
240
- <view class="bottom-bar">
241
- <view class="bar-info">
242
- <block wx:if="{{isSample}}">
243
- <view class="bar-main">还没有内容</view>
244
- <view class="bar-sub">写一句话,马上生成图纸</view>
245
- </block>
246
- <block wx:else>
247
- <view class="bar-main">{{gridText}} · {{colorsText}} 色</view>
248
- <view class="bar-sub">约 {{beadsText}} 颗豆子</view>
249
- </block>
250
- </view>
251
- <view class="primary-btn {{!isSample && textStatus === 'risky' ? 'disabled' : ''}}" bindtap="openExport">{{isSample ? '开始制作' : '导出图纸'}}</view>
252
- </view>
253
-
254
- <!-- 导出面板 -->
255
- <view wx:if="{{exportVisible}}" class="mask" bindtap="closeExport" catchtouchmove="noop">
256
- <view class="sheet" catchtap="noop">
257
- <view class="sheet-handle"></view>
258
- <view class="sheet-title">导出图纸</view>
259
-
260
- <scroll-view scroll-y class="sheet-body">
261
- <view class="field-label">格式</view>
262
- <view class="seg">
263
- <view class="seg-item {{exportFormat === 'pdf' ? 'on' : ''}}" data-id="pdf" bindtap="pickFormat">
264
- <view class="seg-main">PDF 文件</view><view class="seg-sub">A4 打印,大图自动分页</view>
265
- </view>
266
- <view class="seg-item {{exportFormat === 'png' ? 'on' : ''}}" data-id="png" bindtap="pickFormat">
267
- <view class="seg-main">图片</view><view class="seg-sub">存到相册,手机上看</view>
268
- </view>
269
- </view>
270
-
271
- <view class="field-label">内容</view>
272
- <view class="seg">
273
- <view class="seg-item {{exportLayout === 'combined' ? 'on' : ''}}" data-id="combined" bindtap="pickLayout">
274
- <view class="seg-main">合并一份</view><view class="seg-sub">图纸 + 用豆清单</view>
275
- </view>
276
- <view class="seg-item {{exportLayout === 'separate' ? 'on' : ''}}" data-id="separate" bindtap="pickLayout">
277
- <view class="seg-main">分开两份</view><view class="seg-sub">图纸、清单各一份</view>
278
- </view>
279
- </view>
280
-
281
- <block wx:if="{{exportFormat === 'png'}}">
282
- <view class="field-label">清晰度</view>
283
- <view class="seg">
284
- <view class="seg-item {{exportQuality === 'hd' ? 'on' : ''}}" data-id="hd" bindtap="pickQuality"><view class="seg-main">高清</view></view>
285
- <view class="seg-item {{exportQuality === 'std' ? 'on' : ''}}" data-id="std" bindtap="pickQuality"><view class="seg-main">标准</view></view>
286
- </view>
287
- </block>
288
-
289
- <view class="field-label">豆子样式</view>
290
- <view class="seg">
291
- <view wx:for="{{beadStyles}}" wx:key="id" class="seg-item {{beadStyle === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickBeadStyle">
292
- <view class="seg-main">{{item.label}}</view>
293
- </view>
294
- </view>
295
-
296
- <view class="row">
297
- <view class="row-label">辅助网格线</view>
298
- <switch checked="{{gridEnabled}}" color="#0f766e" bindchange="onGridToggle" />
299
- </view>
300
- <block wx:if="{{gridEnabled}}">
301
- <view class="grid-opts">
302
- <view class="seg small">
303
- <view class="seg-item {{gridInterval === 5 ? 'on' : ''}}" data-id="5" bindtap="pickInterval"><view class="seg-main">每 5 格</view></view>
304
- <view class="seg-item {{gridInterval === 10 ? 'on' : ''}}" data-id="10" bindtap="pickInterval"><view class="seg-main">每 10 格</view></view>
305
- </view>
306
- <view class="colors">
307
- <view wx:for="{{gridColors}}" wx:key="*this" class="color-dot {{gridColor === item ? 'on' : ''}}" style="background: {{item}};" data-color="{{item}}" bindtap="pickGridColor"></view>
308
- </view>
309
- </view>
310
- </block>
311
-
312
- <view class="field-label">水印 <text class="field-hint">可选,防止图纸被盗用</text></view>
313
- <view class="input-wrap {{watermarkStatus === 'risky' ? 'input-bad' : ''}}">
314
- <input class="input" value="{{watermark}}" maxlength="20" placeholder="例如:@我的小店" placeholder-class="ph" bindinput="onWatermark" />
315
- </view>
316
- <view wx:if="{{watermarkStatus !== 'idle'}}" class="text-status status-{{watermarkStatus}} pad">
317
- <text wx:if="{{watermarkStatus === 'checking'}}">内容安全检测中…</text>
318
- <text wx:elif="{{watermarkStatus === 'pass'}}">✓ 已通过内容安全检测</text>
319
- <text wx:elif="{{watermarkStatus === 'risky'}}">水印内容可能不合规,请修改</text>
320
- <text wx:else>检测暂不可用,请稍后重试</text>
321
- </view>
322
-
323
- <view wx:if="{{exportResult}}" class="result">
324
- <view class="result-head">
325
- <view class="result-icon {{exportResult.kind === 'png' && !exportResult.saved ? 'warn' : ''}}">
326
- <view class="result-tick"></view>
327
- </view>
328
- <view class="result-text">
329
- <view class="result-title">{{exportResult.kind === 'png' && !exportResult.saved ? '已生成,但未存入相册' : '导出完成'}}</view>
330
- <view class="result-sub" wx:if="{{exportResult.kind === 'pdf'}}">共 {{exportResult.files.length}} 份 PDF,可直接打印或发给好友</view>
331
- <view class="result-sub" wx:elif="{{exportResult.saved}}">{{exportResult.files.length}} 张图片已保存到手机相册</view>
332
- <view class="result-sub" wx:else>点「查看」后长按图片即可保存</view>
333
- </view>
334
- </view>
335
- <view class="result-list">
336
- <view wx:for="{{exportResult.files}}" wx:key="path" class="result-file">
337
- <view class="file-badge {{exportResult.kind}}">{{exportResult.kind === 'pdf' ? 'PDF' : 'PNG'}}</view>
338
- <view class="file-info">
339
- <view class="file-name">{{item.name}}</view>
340
- <view class="file-desc">{{exportResult.kind === 'pdf' ? 'A4 · 可打印' : '高清图片'}}</view>
341
- </view>
342
- <view class="file-actions">
343
- <view class="file-btn" data-index="{{index}}" bindtap="openResultFile">{{exportResult.kind === 'png' ? '查看' : '打开'}}</view>
344
- <view wx:if="{{exportResult.kind === 'pdf'}}" class="file-btn primary" data-index="{{index}}" bindtap="shareResultFile">发送</view>
345
- </view>
346
- </view>
347
- </view>
348
- </view>
349
- </scroll-view>
350
-
351
- <view class="primary-btn block {{exporting ? 'disabled' : ''}}" bindtap="startExport">
352
- {{exporting ? (exportStep || '正在导出…') : (exportResult ? '重新导出' : '开始导出')}}
353
- </view>
354
- </view>
355
- </view>
356
-
357
- <!-- 照片裁切 -->
358
- <view wx:if="{{cropVisible}}" class="mask crop-mask" catchtouchmove="noop">
359
- <view class="crop-panel">
360
- <view class="crop-head">
361
- <text class="crop-cancel" bindtap="cancelCrop">取消</text>
362
- <text class="crop-title">调整照片</text>
363
- <text class="crop-ok {{cropCheck === 'pass' ? '' : 'wait'}}" bindtap="applyCrop">完成</text>
364
- </view>
365
- <view class="crop-box">
366
- <canvas id="crop" type="2d" class="crop-canvas" bindtouchstart="onCropStart" bindtouchmove="onCropMove" bindtouchend="onCropEnd" bindtouchcancel="onCropEnd"></canvas>
367
- </view>
368
- <view class="crop-check check-{{cropCheck}}">
369
- <text wx:if="{{cropCheck === 'checking'}}">正在进行内容安全检测,你可以先调整构图…</text>
370
- <text wx:elif="{{cropCheck === 'pass'}}">✓ 照片已通过内容安全检测</text>
371
- <block wx:elif="{{cropCheck === 'risky'}}"><text>这张照片未通过内容安全检测,请换一张</text></block>
372
- <block wx:elif="{{cropCheck === 'error'}}"><text>检测暂时不可用 </text><text class="link-text" bindtap="retryImageCheck">重试</text></block>
373
- </view>
374
- <view class="crop-hint">单指拖动调整位置,双指缩放</view>
375
- <view class="field-label light">缩放 <text class="field-val">{{draftZoom}}%</text></view>
376
- <slider min="100" max="400" value="{{draftZoom}}" activeColor="#14b8a6" backgroundColor="rgba(255,255,255,0.2)" block-size="20" bindchanging="onDraftZoom" bindchange="onDraftZoom" />
377
- <view class="crop-tools">
378
- <view class="crop-tool" bindtap="flipH">左右翻转</view>
379
- <view class="crop-tool" bindtap="flipV">上下翻转</view>
380
- <view class="crop-tool" bindtap="resetCrop">复位</view>
381
- </view>
382
- <view class="row light">
383
- <view>
384
- <view class="row-label">去掉纯色背景</view>
385
- <view class="row-hint">适合白底、纯色底的照片</view>
386
- </view>
387
- <switch checked="{{draftRemoveBg}}" color="#14b8a6" bindchange="onDraftRemoveBg" />
388
- </view>
389
- <block wx:if="{{draftRemoveBg}}">
390
- <view class="mini-label light">识别范围 {{draftBgTolerance}}</view>
391
- <slider min="6" max="60" value="{{draftBgTolerance}}" activeColor="#14b8a6" backgroundColor="rgba(255,255,255,0.2)" block-size="18" bindchange="onDraftBgTolerance" />
392
- </block>
393
- <view wx:if="{{cropCheck === 'risky'}}" class="crop-replace"><view class="primary-btn block" bindtap="chooseImage">换一张照片</view></view>
394
- </view>
395
- </view>
396
-
397
- <!-- 离屏工作画布 -->
398
- <view class="offscreen">
399
- <canvas id="work" type="2d" class="off-canvas"></canvas>
400
- <canvas id="security" type="2d" class="off-canvas"></canvas>
401
- </view>
402
-
403
- <privacy-popup />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/pages/index/index.wxss DELETED
@@ -1,828 +0,0 @@
1
- page {
2
- background: var(--bg);
3
- }
4
-
5
- /* 导航 */
6
- .nav {
7
- position: fixed;
8
- top: 0;
9
- left: 0;
10
- right: 0;
11
- z-index: 50;
12
- display: flex;
13
- align-items: center;
14
- padding-left: 32rpx;
15
- box-sizing: content-box;
16
- background: transparent;
17
- transition: background 0.2s;
18
- }
19
- .nav-solid {
20
- background: var(--brand-deep);
21
- box-shadow: 0 4rpx 20rpx rgba(15, 61, 58, 0.25);
22
- }
23
- .nav-title {
24
- display: flex;
25
- align-items: center;
26
- gap: 14rpx;
27
- color: #fff;
28
- font-size: 34rpx;
29
- font-weight: 700;
30
- letter-spacing: 2rpx;
31
- }
32
- .nav-logo {
33
- width: 48rpx;
34
- height: 48rpx;
35
- border-radius: 12rpx;
36
- }
37
-
38
- .page {
39
- padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
40
- background: linear-gradient(180deg, var(--brand-deep) 0, var(--brand-deep) 560rpx, var(--bg) 560rpx);
41
- }
42
-
43
- /* 首屏 */
44
- .hero {
45
- padding: 24rpx 40rpx 150rpx;
46
- color: #fff;
47
- }
48
- .hero-badge {
49
- display: inline-block;
50
- padding: 6rpx 18rpx;
51
- border-radius: 999rpx;
52
- background: rgba(255, 255, 255, 0.12);
53
- color: #99f6e4;
54
- font-size: 22rpx;
55
- letter-spacing: 1rpx;
56
- }
57
- .hero-title {
58
- margin-top: 20rpx;
59
- font-size: 56rpx;
60
- font-weight: 800;
61
- line-height: 1.25;
62
- }
63
- .hero-sub {
64
- margin-top: 12rpx;
65
- font-size: 27rpx;
66
- line-height: 1.6;
67
- color: rgba(255, 255, 255, 0.78);
68
- }
69
- .hero-steps {
70
- margin-top: 28rpx;
71
- display: flex;
72
- align-items: center;
73
- gap: 10rpx;
74
- }
75
- .hero-step {
76
- display: flex;
77
- align-items: center;
78
- gap: 8rpx;
79
- padding: 10rpx 16rpx 10rpx 10rpx;
80
- border-radius: 999rpx;
81
- background: rgba(255, 255, 255, 0.1);
82
- font-size: 24rpx;
83
- white-space: nowrap;
84
- }
85
- .hero-step .num {
86
- width: 34rpx;
87
- height: 34rpx;
88
- line-height: 34rpx;
89
- text-align: center;
90
- border-radius: 50%;
91
- background: var(--brand-2);
92
- color: var(--brand-deep);
93
- font-size: 20rpx;
94
- font-weight: 800;
95
- }
96
- .hero-arrow {
97
- color: rgba(255, 255, 255, 0.4);
98
- font-size: 30rpx;
99
- }
100
-
101
- /* 卡片 */
102
- .card {
103
- margin: 0 24rpx 24rpx;
104
- padding: 32rpx;
105
- background: var(--card);
106
- border-radius: var(--r-l);
107
- box-shadow: var(--shadow);
108
- }
109
- .card-title {
110
- font-size: 30rpx;
111
- font-weight: 700;
112
- color: var(--ink);
113
- margin-bottom: 20rpx;
114
- }
115
-
116
- /* 预览 */
117
- .preview-card {
118
- margin-top: -120rpx;
119
- padding: 24rpx;
120
- }
121
- .preview-box {
122
- position: relative;
123
- width: 100%;
124
- height: 654rpx;
125
- border-radius: 24rpx;
126
- overflow: hidden;
127
- background: #f1eee7;
128
- }
129
- .preview-canvas {
130
- width: 654rpx;
131
- height: 654rpx;
132
- display: block;
133
- margin: 0 auto;
134
- }
135
- .sample-tag {
136
- position: absolute;
137
- left: 20rpx;
138
- top: 20rpx;
139
- padding: 8rpx 18rpx;
140
- border-radius: 999rpx;
141
- background: rgba(22, 33, 31, 0.72);
142
- color: #fff;
143
- font-size: 22rpx;
144
- }
145
- .generating {
146
- position: absolute;
147
- right: 20rpx;
148
- top: 20rpx;
149
- width: 52rpx;
150
- height: 52rpx;
151
- border-radius: 50%;
152
- background: rgba(255, 255, 255, 0.9);
153
- display: flex;
154
- align-items: center;
155
- justify-content: center;
156
- }
157
- .spinner {
158
- width: 28rpx;
159
- height: 28rpx;
160
- border: 4rpx solid var(--brand-soft);
161
- border-top-color: var(--brand);
162
- border-radius: 50%;
163
- animation: spin 0.7s linear infinite;
164
- }
165
- @keyframes spin {
166
- to { transform: rotate(360deg); }
167
- }
168
-
169
- .scan-row {
170
- margin-top: 20rpx;
171
- display: flex;
172
- align-items: center;
173
- justify-content: space-between;
174
- gap: 16rpx;
175
- }
176
- .scan-badge {
177
- flex: 1;
178
- padding: 14rpx 20rpx;
179
- border-radius: 16rpx;
180
- font-size: 25rpx;
181
- background: #f1f1ee;
182
- color: var(--muted);
183
- }
184
- .scan-ok { background: var(--ok-soft); color: var(--ok); font-weight: 600; }
185
- .scan-fail { background: var(--warn-soft); color: var(--warn); font-weight: 600; }
186
- .scan-checking { background: var(--brand-soft); color: var(--brand); }
187
-
188
- .stats {
189
- margin-top: 20rpx;
190
- display: flex;
191
- background: #faf9f6;
192
- border-radius: 20rpx;
193
- padding: 20rpx 0;
194
- }
195
- .stat {
196
- flex: 1;
197
- text-align: center;
198
- }
199
- .stat + .stat { border-left: 1rpx solid var(--line); }
200
- .stat-v {
201
- font-size: 36rpx;
202
- font-weight: 800;
203
- color: var(--ink);
204
- }
205
- .stat-k {
206
- margin-top: 4rpx;
207
- font-size: 22rpx;
208
- color: var(--muted);
209
- }
210
- .preview-actions {
211
- margin-top: 20rpx;
212
- }
213
- .ghost-btn {
214
- text-align: center;
215
- width: 100%;
216
- height: 84rpx;
217
- line-height: 84rpx;
218
- border-radius: 20rpx;
219
- background: var(--brand-soft);
220
- color: var(--brand);
221
- font-size: 28rpx;
222
- font-weight: 600;
223
- }
224
-
225
- /* 动态演示状态 */
226
- .intro-tip {
227
- margin-top: 20rpx;
228
- display: flex;
229
- align-items: flex-start;
230
- gap: 14rpx;
231
- padding: 16rpx 20rpx;
232
- border-radius: 16rpx;
233
- background: var(--brand-soft);
234
- color: var(--brand-deep);
235
- font-size: 25rpx;
236
- line-height: 1.5;
237
- }
238
- .intro-dot {
239
- flex-shrink: 0;
240
- width: 14rpx;
241
- height: 14rpx;
242
- margin-top: 13rpx;
243
- border-radius: 50%;
244
- background: var(--brand-2);
245
- animation: pulse 1.6s ease-in-out infinite;
246
- }
247
- @keyframes pulse {
248
- 0%, 100% { opacity: 1; transform: scale(1); }
249
- 50% { opacity: 0.35; transform: scale(0.7); }
250
- }
251
- .features .stat-v { font-size: 32rpx; color: var(--brand); }
252
- .cta-btn {
253
- width: 100%;
254
- height: 88rpx;
255
- line-height: 88rpx;
256
- border-radius: 20rpx;
257
- text-align: center;
258
- background: linear-gradient(135deg, var(--brand), var(--brand-2));
259
- color: #fff;
260
- font-size: 29rpx;
261
- font-weight: 700;
262
- box-shadow: 0 10rpx 24rpx rgba(15, 118, 110, 0.25);
263
- }
264
-
265
- /* 步骤卡 */
266
- .step-head {
267
- display: flex;
268
- align-items: flex-start;
269
- gap: 20rpx;
270
- margin-bottom: 24rpx;
271
- }
272
- .step-num {
273
- flex-shrink: 0;
274
- width: 52rpx;
275
- height: 52rpx;
276
- line-height: 52rpx;
277
- text-align: center;
278
- border-radius: 16rpx;
279
- background: var(--brand);
280
- color: #fff;
281
- font-weight: 800;
282
- font-size: 28rpx;
283
- }
284
- .step-title {
285
- font-size: 32rpx;
286
- font-weight: 700;
287
- color: var(--ink);
288
- }
289
- .optional {
290
- margin-left: 8rpx;
291
- padding: 2rpx 12rpx;
292
- border-radius: 8rpx;
293
- background: #f1f1ee;
294
- color: var(--muted);
295
- font-size: 20rpx;
296
- font-weight: 500;
297
- vertical-align: middle;
298
- }
299
- .step-desc {
300
- margin-top: 6rpx;
301
- font-size: 24rpx;
302
- color: var(--muted);
303
- }
304
-
305
- .input-wrap {
306
- border: 2rpx solid var(--line);
307
- border-radius: 20rpx;
308
- background: #fbfaf8;
309
- padding: 20rpx 24rpx;
310
- }
311
- .input-bad { border-color: var(--danger); background: var(--danger-soft); }
312
- .textarea {
313
- width: 100%;
314
- min-height: 120rpx;
315
- font-size: 30rpx;
316
- line-height: 1.5;
317
- color: var(--ink);
318
- }
319
- .input {
320
- height: 48rpx;
321
- font-size: 28rpx;
322
- color: var(--ink);
323
- }
324
- .ph { color: #b5b8b4; }
325
- .input-foot {
326
- margin-top: 12rpx;
327
- display: flex;
328
- justify-content: space-between;
329
- align-items: center;
330
- }
331
- .text-status { font-size: 23rpx; color: var(--muted); }
332
- .text-status.pad { margin-top: 10rpx; }
333
- .status-checking { color: var(--brand); }
334
- .status-pass { color: var(--ok); }
335
- .status-risky { color: var(--danger); font-weight: 600; }
336
- .status-error { color: var(--warn); }
337
- .input-tools { display: flex; gap: 24rpx; }
338
- .tool { font-size: 25rpx; color: var(--muted); }
339
- .tool-brand { color: var(--brand); font-weight: 600; }
340
- .tip {
341
- margin-top: 16rpx;
342
- font-size: 23rpx;
343
- color: var(--muted);
344
- }
345
-
346
- .upload {
347
- border: 2rpx dashed #c9d6d3;
348
- border-radius: 24rpx;
349
- padding: 44rpx 24rpx;
350
- text-align: center;
351
- background: #f8fbfa;
352
- }
353
- .upload-icon {
354
- margin: 0 auto;
355
- width: 80rpx;
356
- height: 80rpx;
357
- line-height: 76rpx;
358
- border-radius: 50%;
359
- background: var(--brand-soft);
360
- color: var(--brand);
361
- font-size: 48rpx;
362
- }
363
- .upload-text {
364
- margin-top: 16rpx;
365
- font-size: 29rpx;
366
- font-weight: 600;
367
- color: var(--ink);
368
- }
369
- .upload-hint {
370
- margin-top: 8rpx;
371
- font-size: 22rpx;
372
- color: var(--muted);
373
- }
374
-
375
- .photo {
376
- display: flex;
377
- gap: 24rpx;
378
- align-items: center;
379
- }
380
- .photo-thumb {
381
- width: 150rpx;
382
- height: 150rpx;
383
- border-radius: 20rpx;
384
- background: #eee;
385
- }
386
- .photo-info { flex: 1; }
387
- .photo-meta { font-size: 23rpx; color: var(--ok); }
388
- .photo-actions {
389
- margin-top: 16rpx;
390
- display: flex;
391
- gap: 12rpx;
392
- }
393
- .photo-actions .mini-btn {
394
- flex: 1;
395
- padding: 0;
396
- text-align: center;
397
- }
398
-
399
- .mini-btn {
400
- display: inline-block;
401
- text-align: center;
402
- white-space: nowrap;
403
- padding: 0 22rpx;
404
- height: 56rpx;
405
- line-height: 56rpx;
406
- border-radius: 14rpx;
407
- background: #f1f1ee;
408
- color: var(--ink);
409
- font-size: 24rpx;
410
- font-weight: 600;
411
- }
412
- .mini-btn.accent { background: var(--accent); color: #fff; }
413
- .mini-btn.danger { color: var(--danger); background: var(--danger-soft); }
414
-
415
- .row {
416
- margin-top: 24rpx;
417
- display: flex;
418
- align-items: center;
419
- justify-content: space-between;
420
- gap: 20rpx;
421
- }
422
- .row-label { font-size: 28rpx; color: var(--ink); font-weight: 600; }
423
- .row-hint { margin-top: 4rpx; font-size: 22rpx; color: var(--muted); }
424
-
425
- .field-label {
426
- margin: 28rpx 0 14rpx;
427
- font-size: 26rpx;
428
- font-weight: 600;
429
- color: var(--ink);
430
- display: flex;
431
- align-items: center;
432
- gap: 12rpx;
433
- }
434
- .field-label:first-child { margin-top: 0; }
435
- .field-val { color: var(--brand); font-weight: 700; }
436
- .field-hint { font-size: 22rpx; color: var(--muted); font-weight: 400; }
437
- .mini-label { font-size: 23rpx; color: var(--muted); margin-top: 12rpx; }
438
- .link-text { color: var(--brand); font-size: 24rpx; font-weight: 600; }
439
- .link-text.center { display: block; text-align: center; margin-top: 20rpx; }
440
-
441
- /* 分段选择 */
442
- .seg {
443
- display: flex;
444
- gap: 12rpx;
445
- }
446
- .seg-item {
447
- flex: 1;
448
- padding: 18rpx 8rpx;
449
- border-radius: 18rpx;
450
- border: 2rpx solid var(--line);
451
- background: #fbfaf8;
452
- text-align: center;
453
- transition: all 0.15s;
454
- }
455
- .seg-item.on {
456
- border-color: var(--brand);
457
- background: var(--brand-soft);
458
- }
459
- .seg-main { font-size: 27rpx; font-weight: 700; color: var(--ink); }
460
- .seg-item.on .seg-main { color: var(--brand); }
461
- .seg-sub { margin-top: 4rpx; font-size: 21rpx; color: var(--muted); }
462
- .seg.small .seg-item { padding: 12rpx 8rpx; }
463
-
464
- slider { margin: 0 12rpx; }
465
-
466
- .pos-grid {
467
- width: 216rpx;
468
- display: flex;
469
- flex-wrap: wrap;
470
- gap: 12rpx;
471
- padding: 12rpx;
472
- border-radius: 20rpx;
473
- background: #f4f3ef;
474
- }
475
- .pos-cell {
476
- width: 56rpx;
477
- height: 56rpx;
478
- border-radius: 12rpx;
479
- background: #fff;
480
- border: 2rpx solid var(--line);
481
- }
482
- .pos-cell.on { background: var(--brand); border-color: var(--brand); }
483
-
484
- .advanced-toggle {
485
- margin-top: 32rpx;
486
- padding-top: 24rpx;
487
- border-top: 1rpx solid var(--line);
488
- display: flex;
489
- justify-content: space-between;
490
- align-items: center;
491
- font-size: 26rpx;
492
- color: var(--muted);
493
- }
494
- .chev { font-size: 36rpx; transform: rotate(90deg); transition: transform 0.2s; }
495
- .chev.open { transform: rotate(-90deg); }
496
- .advanced { margin-top: 8rpx; }
497
-
498
- .chips-scroll { width: 100%; white-space: nowrap; }
499
- .chips { display: inline-flex; gap: 12rpx; padding-bottom: 4rpx; }
500
- .chip {
501
- display: inline-block;
502
- padding: 16rpx 22rpx;
503
- border-radius: 18rpx;
504
- border: 2rpx solid var(--line);
505
- background: #fbfaf8;
506
- }
507
- .chip.on { border-color: var(--brand); background: var(--brand-soft); }
508
- .chip-main { font-size: 27rpx; font-weight: 700; color: var(--ink); }
509
- .chip.on .chip-main { color: var(--brand); }
510
- .chip-sub { margin-top: 4rpx; font-size: 20rpx; color: var(--muted); }
511
-
512
- /* 用豆清单 */
513
- .usage {
514
- display: grid;
515
- grid-template-columns: repeat(4, 1fr);
516
- gap: 12rpx;
517
- }
518
- .usage-item {
519
- display: flex;
520
- align-items: center;
521
- gap: 10rpx;
522
- padding: 12rpx;
523
- border-radius: 14rpx;
524
- background: #faf9f6;
525
- }
526
- .swatch {
527
- width: 32rpx;
528
- height: 32rpx;
529
- border-radius: 50%;
530
- box-shadow: inset 0 0 0 1rpx rgba(0, 0, 0, 0.12);
531
- flex-shrink: 0;
532
- }
533
- .usage-id { font-size: 23rpx; font-weight: 700; color: var(--ink); }
534
- .usage-count { margin-left: auto; font-size: 21rpx; color: var(--muted); }
535
-
536
- /* 页脚 */
537
- .footer {
538
- padding: 16rpx 48rpx 24rpx;
539
- text-align: center;
540
- }
541
- .footer-safe {
542
- display: flex;
543
- align-items: flex-start;
544
- justify-content: center;
545
- gap: 10rpx;
546
- text-align: left;
547
- font-size: 22rpx;
548
- line-height: 1.6;
549
- color: var(--muted);
550
- }
551
- .footer-links {
552
- margin-top: 16rpx;
553
- display: flex;
554
- justify-content: center;
555
- align-items: center;
556
- gap: 12rpx;
557
- }
558
- .footer-link {
559
- font-size: 23rpx;
560
- line-height: 1.6;
561
- color: var(--brand);
562
- }
563
- .footer-shield {
564
- flex-shrink: 0;
565
- width: 30rpx;
566
- height: 30rpx;
567
- margin-top: 3rpx;
568
- }
569
- .footer-links .footer-btn {
570
- display: inline-block;
571
- width: auto !important;
572
- min-height: 0;
573
- padding: 0 !important;
574
- margin: 0 !important;
575
- font-size: 23rpx;
576
- font-weight: 400;
577
- line-height: 1.6;
578
- color: var(--brand);
579
- background: none;
580
- }
581
- .dot { color: #c4c7c3; font-size: 22rpx; }
582
- .footer-copy { margin-top: 12rpx; font-size: 20rpx; color: #b5b8b4; }
583
-
584
- /* 底部操作栏 */
585
- .bottom-bar {
586
- position: fixed;
587
- left: 0;
588
- right: 0;
589
- bottom: 0;
590
- z-index: 40;
591
- display: flex;
592
- align-items: center;
593
- gap: 24rpx;
594
- padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
595
- background: rgba(255, 255, 255, 0.96);
596
- box-shadow: 0 -6rpx 30rpx rgba(22, 33, 31, 0.08);
597
- }
598
- .bar-info { flex: 1; min-width: 0; }
599
- .bar-main { font-size: 30rpx; font-weight: 800; color: var(--ink); }
600
- .bar-sub { margin-top: 2rpx; font-size: 22rpx; color: var(--muted); }
601
- .primary-btn {
602
- text-align: center;
603
- padding: 0 56rpx;
604
- height: 92rpx;
605
- line-height: 92rpx;
606
- border-radius: 999rpx;
607
- background: linear-gradient(135deg, var(--brand), var(--brand-2));
608
- color: #fff;
609
- font-size: 30rpx;
610
- font-weight: 700;
611
- box-shadow: 0 10rpx 24rpx rgba(15, 118, 110, 0.3);
612
- }
613
- .primary-btn.disabled { opacity: 0.5; box-shadow: none; }
614
- .primary-btn.block { display: block; width: 100%; padding: 0; }
615
-
616
- /* 弹层 */
617
- .mask {
618
- position: fixed;
619
- top: 0;
620
- left: 0;
621
- right: 0;
622
- bottom: 0;
623
- z-index: 100;
624
- background: rgba(10, 20, 18, 0.5);
625
- display: flex;
626
- align-items: flex-end;
627
- }
628
- .sheet {
629
- width: 100%;
630
- max-height: 88vh;
631
- display: flex;
632
- flex-direction: column;
633
- background: var(--card);
634
- border-radius: 36rpx 36rpx 0 0;
635
- padding: 16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
636
- box-sizing: border-box;
637
- }
638
- .sheet-handle {
639
- margin: 0 auto 16rpx;
640
- width: 72rpx;
641
- height: 8rpx;
642
- border-radius: 8rpx;
643
- background: #e2e2dd;
644
- }
645
- .sheet-title {
646
- font-size: 34rpx;
647
- font-weight: 800;
648
- color: var(--ink);
649
- margin-bottom: 20rpx;
650
- }
651
- .sheet-body {
652
- flex: 1;
653
- max-height: 62vh;
654
- margin-bottom: 24rpx;
655
- }
656
- .grid-opts {
657
- margin-top: 16rpx;
658
- display: flex;
659
- align-items: center;
660
- gap: 24rpx;
661
- }
662
- .grid-opts .seg { flex: 1; }
663
- .colors { display: flex; gap: 14rpx; }
664
- .color-dot {
665
- width: 44rpx;
666
- height: 44rpx;
667
- border-radius: 50%;
668
- border: 4rpx solid #fff;
669
- box-shadow: 0 0 0 2rpx var(--line);
670
- }
671
- .color-dot.on { box-shadow: 0 0 0 4rpx var(--brand); }
672
-
673
- .result {
674
- margin-top: 32rpx;
675
- border-radius: 24rpx;
676
- background: #fff;
677
- border: 2rpx solid var(--line);
678
- overflow: hidden;
679
- }
680
- .result-head {
681
- display: flex;
682
- align-items: center;
683
- gap: 20rpx;
684
- padding: 28rpx;
685
- background: linear-gradient(135deg, var(--ok-soft), #ffffff);
686
- }
687
- .result-icon {
688
- flex-shrink: 0;
689
- width: 64rpx;
690
- height: 64rpx;
691
- border-radius: 50%;
692
- background: var(--ok);
693
- display: flex;
694
- align-items: center;
695
- justify-content: center;
696
- box-shadow: 0 8rpx 18rpx rgba(21, 128, 61, 0.25);
697
- }
698
- .result-icon.warn { background: var(--warn); box-shadow: none; }
699
- .result-tick {
700
- width: 16rpx;
701
- height: 28rpx;
702
- margin-top: -6rpx;
703
- border-right: 5rpx solid #fff;
704
- border-bottom: 5rpx solid #fff;
705
- transform: rotate(45deg);
706
- }
707
- .result-text { flex: 1; min-width: 0; }
708
- .result-title { font-size: 30rpx; font-weight: 800; color: var(--ink); }
709
- .result-sub { margin-top: 6rpx; font-size: 23rpx; color: var(--muted); line-height: 1.5; }
710
- .result-list { padding: 8rpx 28rpx; }
711
- .result-file {
712
- display: flex;
713
- align-items: center;
714
- gap: 20rpx;
715
- padding: 20rpx 0;
716
- }
717
- .result-file + .result-file { border-top: 1rpx solid var(--line); }
718
- .file-badge {
719
- flex-shrink: 0;
720
- width: 72rpx;
721
- height: 84rpx;
722
- line-height: 84rpx;
723
- border-radius: 12rpx;
724
- text-align: center;
725
- font-size: 20rpx;
726
- font-weight: 800;
727
- letter-spacing: 1rpx;
728
- color: #fff;
729
- background: #e5484d;
730
- }
731
- .file-badge.png { background: #3b82f6; }
732
- .file-info { flex: 1; min-width: 0; }
733
- .file-name {
734
- font-size: 27rpx;
735
- font-weight: 700;
736
- color: var(--ink);
737
- overflow: hidden;
738
- white-space: nowrap;
739
- text-overflow: ellipsis;
740
- }
741
- .file-desc { margin-top: 4rpx; font-size: 22rpx; color: var(--muted); }
742
- .file-actions { flex-shrink: 0; display: flex; gap: 12rpx; }
743
- .file-btn {
744
- min-width: 100rpx;
745
- height: 60rpx;
746
- line-height: 60rpx;
747
- padding: 0 20rpx;
748
- border-radius: 999rpx;
749
- text-align: center;
750
- font-size: 24rpx;
751
- font-weight: 700;
752
- color: var(--brand);
753
- background: var(--brand-soft);
754
- }
755
- .file-btn.primary { color: #fff; background: var(--brand); }
756
-
757
- /* 裁切 */
758
- .crop-mask { align-items: stretch; background: #0b1413; }
759
- .crop-panel {
760
- width: 100%;
761
- padding: calc(env(safe-area-inset-top) + 100rpx) 40rpx 40rpx;
762
- box-sizing: border-box;
763
- overflow-y: auto;
764
- color: #fff;
765
- }
766
- .crop-head {
767
- display: flex;
768
- justify-content: space-between;
769
- align-items: center;
770
- margin-bottom: 28rpx;
771
- }
772
- .crop-title { font-size: 32rpx; font-weight: 700; }
773
- .crop-cancel { font-size: 28rpx; color: rgba(255, 255, 255, 0.7); }
774
- .crop-ok {
775
- padding: 10rpx 28rpx;
776
- border-radius: 999rpx;
777
- background: var(--brand-2);
778
- color: var(--brand-deep);
779
- font-size: 28rpx;
780
- font-weight: 700;
781
- }
782
- .crop-ok.wait { opacity: 0.55; }
783
- .crop-box {
784
- width: 670rpx;
785
- height: 670rpx;
786
- margin: 0 auto;
787
- border-radius: 20rpx;
788
- overflow: hidden;
789
- background: #1a2523;
790
- }
791
- .crop-canvas { width: 670rpx; height: 670rpx; }
792
- .crop-check {
793
- margin-top: 20rpx;
794
- padding: 14rpx 20rpx;
795
- border-radius: 14rpx;
796
- font-size: 24rpx;
797
- background: rgba(255, 255, 255, 0.08);
798
- color: rgba(255, 255, 255, 0.8);
799
- }
800
- .check-pass { color: #6ee7b7; }
801
- .check-risky { background: rgba(229, 72, 77, 0.2); color: #fca5a5; }
802
- .check-error { color: #fcd34d; }
803
- .crop-hint { margin-top: 12rpx; font-size: 22rpx; color: rgba(255, 255, 255, 0.5); text-align: center; }
804
- .field-label.light, .row.light .row-label { color: #fff; }
805
- .row.light .row-hint, .mini-label.light { color: rgba(255, 255, 255, 0.55); }
806
- .crop-tools { margin-top: 24rpx; display: flex; gap: 16rpx; }
807
- .crop-tool {
808
- text-align: center;
809
- flex: 1;
810
- height: 72rpx;
811
- line-height: 72rpx;
812
- border-radius: 16rpx;
813
- background: rgba(255, 255, 255, 0.1);
814
- color: #fff;
815
- font-size: 25rpx;
816
- }
817
- .crop-replace { margin-top: 32rpx; }
818
-
819
- /* 离屏画布:不能 display:none,否则取不到节点 */
820
- .offscreen {
821
- position: fixed;
822
- left: -9999px;
823
- top: 0;
824
- width: 10px;
825
- height: 10px;
826
- overflow: hidden;
827
- }
828
- .off-canvas { width: 10px; height: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/sitemap.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
3
- "rules": [{
4
- "action": "allow",
5
- "page": "*"
6
- }]
7
- }
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/color.js DELETED
@@ -1,46 +0,0 @@
1
- // 颜色基础:HEX/RGB 转换、OKLab 感知色彩空间、亮度
2
- // OKLab 比 RGB 欧氏距离更接近人眼感受,配色更自然;距离统一放大 100 倍便于调参
3
-
4
- function hexToRgb(hex) {
5
- const value = String(hex || '').replace('#', '')
6
- const full = value.length === 3 ? value.split('').map((c) => c + c).join('') : value
7
- const num = parseInt(full, 16) || 0
8
- return [(num >> 16) & 255, (num >> 8) & 255, num & 255]
9
- }
10
-
11
- function rgbToHex(r, g, b) {
12
- const h = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')
13
- return `#${h(r)}${h(g)}${h(b)}`
14
- }
15
-
16
- // sRGB 伽马解码查表,避免每次调用 Math.pow
17
- const SRGB_TO_LINEAR = new Float32Array(256)
18
- for (let i = 0; i < 256; i += 1) {
19
- const c = i / 255
20
- SRGB_TO_LINEAR[i] = c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
21
- }
22
-
23
- // 写入 out[offset..offset+2](L, a, b,均 ×100)
24
- function rgbToOklabInto(r, g, b, out, offset) {
25
- const lr = SRGB_TO_LINEAR[r]
26
- const lg = SRGB_TO_LINEAR[g]
27
- const lb = SRGB_TO_LINEAR[b]
28
- const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb)
29
- const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb)
30
- const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb)
31
- out[offset] = (0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s) * 100
32
- out[offset + 1] = (1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s) * 100
33
- out[offset + 2] = (0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s) * 100
34
- }
35
-
36
- // 感知亮度(与旧版一致的 Rec.601 加权,0~1),用于二维码深/浅模块约束
37
- function luma(r, g, b) {
38
- return (r * 0.299 + g * 0.587 + b * 0.114) / 255
39
- }
40
-
41
- function lumaHex(hex) {
42
- const [r, g, b] = hexToRgb(hex)
43
- return luma(r, g, b)
44
- }
45
-
46
- module.exports = { hexToRgb, rgbToHex, rgbToOklabInto, luma, lumaHex }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/engine.js DELETED
@@ -1,225 +0,0 @@
1
- // 配色引擎:把底图 + 二维码合成为 MARD 色号网格
2
- // 性能要点:
3
- // 1. 5bit 量化桶 → 候选 Top-K 查找表(懒加载),每格只精算约 10 个候选,而非整套 291 色
4
- // 2. OKLab 感知距离 + 色间距离矩阵,全部 Typed Array,无对象分配
5
- // 3. “颜色连贯度”参考已选中的左/上邻居色号,减少零碎杂色,拼起来更省事
6
- // 4. 定位/时序/对齐等功能区使用更严格的深浅阈值,显著提升扫码成功率
7
- const { rgbToOklabInto } = require('./color')
8
- const { MATCH_MODES } = require('./palette')
9
- const { PAPER } = require('./imaging')
10
-
11
- const TOP_K = 10
12
- const C_ANY = 0
13
- const C_DARK = 1
14
- const C_LIGHT = 2
15
- const C_FDARK = 3
16
- const C_FLIGHT = 4
17
- const C_QUIET = 5
18
- const C_EMPTY = 6
19
-
20
- function subsetFor(pal, mode, kind) {
21
- const key = `${mode.id}:${kind}`
22
- if (pal.subsets[key]) return pal.subsets[key]
23
- const { n, lum } = pal
24
- let test
25
- if (kind === C_DARK) test = (i) => lum[i] <= mode.darkMax
26
- else if (kind === C_LIGHT) test = (i) => lum[i] >= mode.lightMin
27
- else if (kind === C_FDARK) test = (i) => lum[i] <= Math.min(mode.darkMax, 0.3)
28
- else if (kind === C_FLIGHT) test = (i) => lum[i] >= Math.max(mode.lightMin, 0.82)
29
- else test = () => true
30
- let list = []
31
- for (let i = 0; i < n; i += 1) if (test(i)) list.push(i)
32
- if (!list.length) {
33
- // 色库太小时退化为最深/最浅的 3 个颜色
34
- const order = Array.from({ length: n }, (_, i) => i).sort((a, b) => lum[a] - lum[b])
35
- list = kind === C_DARK || kind === C_FDARK ? order.slice(0, 3) : order.slice(-3)
36
- }
37
- const member = new Uint8Array(n)
38
- list.forEach((i) => { member[i] = 1 })
39
- const subset = { key, list: Int16Array.from(list), member, lut: new Map() }
40
- pal.subsets[key] = subset
41
- return subset
42
- }
43
-
44
- const tmpLab = new Float32Array(3)
45
-
46
- function topKForBucket(pal, subset, bucket) {
47
- let hit = subset.lut.get(bucket)
48
- if (hit) return hit
49
- const r = ((bucket >> 10) & 31) * 8 + 4
50
- const g = ((bucket >> 5) & 31) * 8 + 4
51
- const b = (bucket & 31) * 8 + 4
52
- rgbToOklabInto(r, g, b, tmpLab, 0)
53
- const { lab } = pal
54
- const list = subset.list
55
- const k = Math.min(TOP_K, list.length)
56
- const bestIdx = new Int16Array(k).fill(-1)
57
- const bestDist = new Float32Array(k).fill(Infinity)
58
- for (let t = 0; t < list.length; t += 1) {
59
- const i = list[t]
60
- const dl = lab[i * 3] - tmpLab[0]
61
- const da = lab[i * 3 + 1] - tmpLab[1]
62
- const db = lab[i * 3 + 2] - tmpLab[2]
63
- const d = dl * dl + da * da + db * db
64
- if (d >= bestDist[k - 1]) continue
65
- let p = k - 1
66
- while (p > 0 && bestDist[p - 1] > d) { bestDist[p] = bestDist[p - 1]; bestIdx[p] = bestIdx[p - 1]; p -= 1 }
67
- bestDist[p] = d
68
- bestIdx[p] = i
69
- }
70
- hit = bestIdx
71
- subset.lut.set(bucket, hit)
72
- return hit
73
- }
74
-
75
- /**
76
- * @param base { size, rgb: Uint8Array, mask?: Uint8Array } 或 null(无底图)
77
- * @param qr makeQr() 结果
78
- * @param placement createPlacement() 结果
79
- * @param pal preparePalette() 结果
80
- * @param options { modeId, coherence(0-100), cleanup(bool) }
81
- */
82
- function computeArtwork(base, qr, placement, pal, options = {}) {
83
- const mode = MATCH_MODES.find((m) => m.id === options.modeId) || MATCH_MODES[1]
84
- const size = placement.size
85
- const total = size * size
86
- const lab = new Float32Array(total * 3)
87
- const bucket = new Int32Array(total)
88
- const kind = new Uint8Array(total)
89
- const { left, top, quiet } = placement
90
-
91
- for (let y = 0; y < size; y += 1) {
92
- for (let x = 0; x < size; x += 1) {
93
- const i = y * size + x
94
- const qx = x - left
95
- const qy = y - top
96
- const inQr = qx >= 0 && qy >= 0 && qx < qr.size && qy < qr.size
97
- const inQuiet = !inQr && x >= left - quiet && x < left + qr.size + quiet && y >= top - quiet && y < top + qr.size + quiet
98
- let r; let g; let b
99
- if (inQr) {
100
- const q = qy * qr.size + qx
101
- const isDark = qr.dark[q]
102
- const isFunc = qr.func[q]
103
- kind[i] = isFunc ? (isDark ? C_FDARK : C_FLIGHT) : (isDark ? C_DARK : C_LIGHT)
104
- if (base && !(base.mask && base.mask[i])) {
105
- r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
106
- } else if (base) {
107
- r = PAPER[0]; g = PAPER[1]; b = PAPER[2]
108
- } else {
109
- r = g = b = isDark ? 0 : 255
110
- }
111
- } else if (inQuiet || !base) {
112
- kind[i] = C_QUIET
113
- continue
114
- } else if (base.mask && base.mask[i]) {
115
- kind[i] = C_EMPTY
116
- continue
117
- } else {
118
- kind[i] = C_ANY
119
- r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
120
- }
121
- rgbToOklabInto(r, g, b, lab, i * 3)
122
- bucket[i] = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)
123
- }
124
- }
125
-
126
- const n = pal.n
127
- const plab = pal.lab
128
- const pair = pal.pair
129
- const weight = (Math.max(0, Math.min(100, options.coherence == null ? 40 : options.coherence)) / 100) * 0.6
130
- const subsets = [C_ANY, C_DARK, C_LIGHT, C_FDARK, C_FLIGHT].map((k) => subsetFor(pal, mode, k))
131
- const cells = new Int16Array(total).fill(-1)
132
-
133
- const distTo = (c, i) => {
134
- const dl = plab[c * 3] - lab[i * 3]
135
- const da = plab[c * 3 + 1] - lab[i * 3 + 1]
136
- const db = plab[c * 3 + 2] - lab[i * 3 + 2]
137
- return Math.sqrt(dl * dl + da * da + db * db)
138
- }
139
-
140
- for (let y = 0; y < size; y += 1) {
141
- for (let x = 0; x < size; x += 1) {
142
- const i = y * size + x
143
- const k = kind[i]
144
- if (k >= C_QUIET) continue
145
- const subset = subsets[k]
146
- const cands = topKForBucket(pal, subset, bucket[i])
147
- const nl = x > 0 ? cells[i - 1] : -1
148
- const nu = y > 0 ? cells[i - size] : -1
149
- const nbCount = (nl >= 0 ? 1 : 0) + (nu >= 0 ? 1 : 0)
150
- let best = -1
151
- let bestScore = Infinity
152
- const consider = (c) => {
153
- let score = distTo(c, i)
154
- if (weight > 0 && nbCount) {
155
- let s = 0
156
- if (nl >= 0) s += pair[c * n + nl]
157
- if (nu >= 0) s += pair[c * n + nu]
158
- score += weight * (s / nbCount)
159
- }
160
- if (score < bestScore) { bestScore = score; best = c }
161
- }
162
- for (let t = 0; t < cands.length; t += 1) if (cands[t] >= 0) consider(cands[t])
163
- if (weight > 0) {
164
- if (nl >= 0 && subset.member[nl]) consider(nl)
165
- if (nu >= 0 && nu !== nl && subset.member[nu]) consider(nu)
166
- }
167
- cells[i] = best
168
- }
169
- }
170
-
171
- // 合并零散色:只用了 1~2 颗的色号并入最接近的常用色,减少需要购买的色号
172
- const counts = new Int32Array(n)
173
- for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
174
- if (options.cleanup !== false) {
175
- const kept = []
176
- for (let c = 0; c < n; c += 1) if (counts[c] >= 3) kept.push(c)
177
- if (kept.length) {
178
- for (let i = 0; i < total; i += 1) {
179
- const c = cells[i]
180
- if (c < 0 || counts[c] >= 3) continue
181
- const member = subsets[kind[i]].member
182
- let best = -1
183
- let bestD = Infinity
184
- for (let t = 0; t < kept.length; t += 1) {
185
- const kc = kept[t]
186
- if (!member[kc]) continue
187
- const d = distTo(kc, i)
188
- if (d < bestD) { bestD = d; best = kc }
189
- }
190
- // 与原色差别过大时保留原色,避免破坏画面
191
- if (best >= 0 && bestD <= distTo(c, i) + 12) cells[i] = best
192
- }
193
- counts.fill(0)
194
- for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
195
- }
196
- }
197
-
198
- // 二维码留白区:使用画面中最常用的浅色;没有则用色库里最浅的颜色
199
- let quietColor = -1
200
- let quietCount = -1
201
- for (let c = 0; c < n; c += 1) {
202
- if (pal.lum[c] >= Math.max(0.8, mode.lightMin) && counts[c] > quietCount) { quietColor = c; quietCount = counts[c] }
203
- }
204
- if (quietCount <= 0) {
205
- quietColor = 0
206
- for (let c = 1; c < n; c += 1) if (pal.lum[c] > pal.lum[quietColor]) quietColor = c
207
- }
208
- for (let i = 0; i < total; i += 1) {
209
- if (kind[i] === C_QUIET) { cells[i] = quietColor; counts[quietColor] += 1 }
210
- }
211
-
212
- const usage = []
213
- let beads = 0
214
- for (let c = 0; c < n; c += 1) {
215
- if (counts[c] > 0) {
216
- usage.push({ index: c, id: pal.ids[c], hex: pal.hexes[c], count: counts[c] })
217
- beads += counts[c]
218
- }
219
- }
220
- usage.sort((a, b) => b.count - a.count)
221
-
222
- return { size, cells, usage, beads, pal, qr, placement }
223
- }
224
-
225
- module.exports = { computeArtwork }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/imaging.js DELETED
@@ -1,95 +0,0 @@
1
- // 图像取样:超采样 + 区域平均,得到干净的格子颜色(旧版一次性缩放会产生噪点和锯齿色)
2
- // 所有结果都用 Typed Array 表示,不再生成 HEX 字符串再反解析
3
-
4
- const PAPER = [247, 241, 230]
5
-
6
- function getImageDimensions(image) {
7
- const width = Number(image.width) || Number(image.naturalWidth) || Number(image._w)
8
- const height = Number(image.height) || Number(image.naturalHeight) || Number(image._h)
9
- if (!width || !height) throw new Error('IMAGE_DIMENSIONS_UNAVAILABLE')
10
- return { width, height }
11
- }
12
-
13
- // 把图片按裁切参数画到 context 上(cover 语义,zoom≥1)
14
- function drawCropped(context, image, width, height, crop) {
15
- const source = getImageDimensions(image)
16
- const scale = Math.max(width / source.width, height / source.height) * (crop.zoom || 1)
17
- const w = source.width * scale
18
- const h = source.height * scale
19
- const offsetX = (width - w) * ((crop.focusX == null ? 50 : crop.focusX) / 100)
20
- const offsetY = (height - h) * ((crop.focusY == null ? 50 : crop.focusY) / 100)
21
- context.fillStyle = `rgb(${PAPER[0]},${PAPER[1]},${PAPER[2]})`
22
- context.fillRect(0, 0, width, height)
23
- context.save()
24
- context.translate(crop.flipH ? width : 0, crop.flipV ? height : 0)
25
- context.scale(crop.flipH ? -1 : 1, crop.flipV ? -1 : 1)
26
- context.drawImage(image, offsetX, offsetY, w, h)
27
- context.restore()
28
- }
29
-
30
- // 返回 { size, rgb: Uint8Array(size*size*3) }
31
- function sampleImage(canvas, image, size, crop) {
32
- // 超采样到约 700px 再按块平均,等价于高质量面积缩放
33
- const factor = Math.max(1, Math.min(24, Math.floor(720 / size)))
34
- const big = size * factor
35
- canvas.width = big
36
- canvas.height = big
37
- const context = canvas.getContext('2d')
38
- context.imageSmoothingEnabled = true
39
- try { context.imageSmoothingQuality = 'high' } catch (e) { /* 部分基础库不支持 */ }
40
- drawCropped(context, image, big, big, crop || {})
41
- const pixels = context.getImageData(0, 0, big, big).data
42
- const rgb = new Uint8Array(size * size * 3)
43
- const area = factor * factor
44
- for (let gy = 0; gy < size; gy += 1) {
45
- for (let gx = 0; gx < size; gx += 1) {
46
- let r = 0; let g = 0; let b = 0
47
- for (let sy = 0; sy < factor; sy += 1) {
48
- let p = ((gy * factor + sy) * big + gx * factor) * 4
49
- for (let sx = 0; sx < factor; sx += 1) {
50
- r += pixels[p]; g += pixels[p + 1]; b += pixels[p + 2]
51
- p += 4
52
- }
53
- }
54
- const o = (gy * size + gx) * 3
55
- rgb[o] = (r / area + 0.5) | 0
56
- rgb[o + 1] = (g / area + 0.5) | 0
57
- rgb[o + 2] = (b / area + 0.5) | 0
58
- }
59
- }
60
- return { size, rgb }
61
- }
62
-
63
- // 去除简单纯色背景:四角均色为背景色,从边缘洪泛填充相近颜色(主体内部不受影响)
64
- // 返回 Uint8Array 掩码:1 = 背景(空钉位,不放豆)
65
- function removeSimpleBackground(rgb, size, tolerance) {
66
- const corners = [0, size - 1, (size - 1) * size, size * size - 1]
67
- let br = 0; let bg = 0; let bb = 0
68
- corners.forEach((i) => { br += rgb[i * 3]; bg += rgb[i * 3 + 1]; bb += rgb[i * 3 + 2] })
69
- br /= 4; bg /= 4; bb /= 4
70
- const threshold = (tolerance / 100) * 441.673
71
- const limit = threshold * threshold
72
- const mask = new Uint8Array(size * size)
73
- const isBg = (i) => {
74
- const dr = rgb[i * 3] - br; const dg = rgb[i * 3 + 1] - bg; const db = rgb[i * 3 + 2] - bb
75
- return dr * dr + dg * dg + db * db <= limit
76
- }
77
- const stack = []
78
- const push = (x, y) => {
79
- if (x < 0 || y < 0 || x >= size || y >= size) return
80
- const i = y * size + x
81
- if (mask[i] || !isBg(i)) return
82
- mask[i] = 1
83
- stack.push(i)
84
- }
85
- for (let i = 0; i < size; i += 1) { push(i, 0); push(i, size - 1); push(0, i); push(size - 1, i) }
86
- while (stack.length) {
87
- const i = stack.pop()
88
- const x = i % size
89
- const y = (i - x) / size
90
- push(x + 1, y); push(x - 1, y); push(x, y + 1); push(x, y - 1)
91
- }
92
- return mask
93
- }
94
-
95
- module.exports = { PAPER, getImageDimensions, drawCropped, sampleImage, removeSimpleBackground }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/intro.js DELETED
@@ -1,703 +0,0 @@
1
- // 首屏动态演示(1:1)
2
- // 纯代码绘制:不占包体积(无视频/图片资源),小程序实时播放与宣传视频渲染共用同一条时间轴。
3
- // 演示用的拼豆作品由真实配色引擎实时生成,并且确实可以被微信扫出 “Happy Birthday!”。
4
- const { makeQr, createPlacement } = require('./qr')
5
- const { preparePalette } = require('./palette')
6
- const { computeArtwork } = require('./engine')
7
-
8
- const DEMO_TEXT = 'Happy Birthday!'
9
- const DURATION = 12
10
- const U = 1080 // 设计稿边长,绘制时按实际像素缩放
11
- const TAU = Math.PI * 2
12
- const FLIGHT = 0.85
13
- // 豆子半径(相对格宽):略大于 0.5,相邻豆子轻微相接,像熨烫后的成品,扫码更稳
14
- const BEAD_R = 0.52
15
-
16
- const C = {
17
- bg: '#f4efe6',
18
- ink: '#16211f',
19
- muted: '#6b7471',
20
- brand: '#0f766e',
21
- brand2: '#14b8a6',
22
- brandSoft: '#dcf1ec',
23
- board: '#fffdf8',
24
- dark: '#22302e',
25
- light: '#fbf8f1',
26
- wechat: '#07c160',
27
- }
28
-
29
- const SCENES = [
30
- { a: 0, b: 2.6, title: '写一句话', sub: '扫码后想让人看到的话' },
31
- { a: 2.6, b: 5.0, title: '自动编成二维码', sub: '每一颗拼豆,都是一个像素' },
32
- { a: 5.0, b: 7.6, title: '融进你的照片', sub: '换上照片的颜色,依然能扫' },
33
- { a: 7.6, b: 10.0, title: '微信扫一扫', sub: '拼好的作品,一扫就读出这句话' },
34
- ]
35
-
36
- const CONFETTI = [
37
- [0.17, 0.19, 0.045, '#ffd166'], [0.85, 0.2, 0.04, '#4cc9f0'], [0.13, 0.8, 0.04, '#2ec4b6'],
38
- [0.86, 0.82, 0.045, '#ffd166'], [0.5, 0.11, 0.03, '#2ec4b6'], [0.91, 0.51, 0.03, '#ff9f1c'], [0.08, 0.5, 0.03, '#4cc9f0'],
39
- ]
40
-
41
- const DECOR = [
42
- [90, 150, 22, '#14b8a6'], [990, 120, 16, '#ffb703'], [60, 960, 18, '#ff7a45'], [1010, 930, 24, '#8ecae6'],
43
- [170, 1040, 12, '#ffb703'], [930, 330, 12, '#ff7a45'], [120, 520, 10, '#8ecae6'], [980, 700, 14, '#14b8a6'],
44
- ]
45
-
46
- // —— 工具 ——
47
- const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)
48
- const lerp = (a, b, t) => a + (b - a) * t
49
- const prog = (t, a, b) => clamp01((t - a) / (b - a))
50
- const easeOut = (t) => 1 - Math.pow(1 - t, 3)
51
- const easeIn = (t) => t * t * t
52
- const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2)
53
- const easeBack = (t) => { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2) }
54
- const bump = (t, a, b) => { const p = prog(t, a, b); return p > 0 && p < 1 ? Math.sin(Math.PI * p) : 0 }
55
-
56
- function rng(seed) {
57
- let a = seed >>> 0
58
- return () => {
59
- a = (a + 0x6D2B79F5) >>> 0
60
- let t = a
61
- t = Math.imul(t ^ (t >>> 15), t | 1)
62
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
63
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296
64
- }
65
- }
66
-
67
- function rr(ctx, x, y, w, h, r) {
68
- r = Math.max(0, Math.min(r, w / 2, h / 2))
69
- ctx.moveTo(x + r, y)
70
- ctx.arcTo(x + w, y, x + w, y + h, r)
71
- ctx.arcTo(x + w, y + h, x, y + h, r)
72
- ctx.arcTo(x, y + h, x, y, r)
73
- ctx.arcTo(x, y, x + w, y, r)
74
- ctx.closePath()
75
- }
76
-
77
- // 三层叠加的柔和阴影:比 shadowBlur 便宜得多,手机上逐帧绘制也流畅
78
- function softShadow(ctx, x, y, w, h, r, alpha) {
79
- for (let i = 3; i >= 1; i -= 1) {
80
- const g = i * 8
81
- ctx.fillStyle = `rgba(52,38,18,${(alpha / (i + 1)).toFixed(3)})`
82
- ctx.beginPath()
83
- rr(ctx, x - g / 2, y - g / 2 + g * 0.8, w + g, h + g, r + g / 2)
84
- ctx.fill()
85
- }
86
- }
87
-
88
- function heartPath(ctx, cx, cy, s) {
89
- ctx.moveTo(cx, cy + s * 0.9)
90
- ctx.bezierCurveTo(cx - s * 1.3, cy + s * 0.05, cx - s * 0.95, cy - s * 1.0, cx, cy - s * 0.42)
91
- ctx.bezierCurveTo(cx + s * 0.95, cy - s * 1.0, cx + s * 1.3, cy + s * 0.05, cx, cy + s * 0.9)
92
- ctx.closePath()
93
- }
94
-
95
- // 演示“照片”:暖色渐变 + 爱心 + 彩纸屑(矢量绘制,任意尺寸都清晰)
96
- function drawIllustration(ctx, x, y, s) {
97
- ctx.save()
98
- ctx.beginPath()
99
- ctx.rect(x, y, s, s)
100
- ctx.clip()
101
- let g = ctx.createLinearGradient(x, y, x, y + s)
102
- g.addColorStop(0, '#63c7b8')
103
- g.addColorStop(1, '#4aa3c9')
104
- ctx.fillStyle = g
105
- ctx.fillRect(x, y, s, s)
106
- g = ctx.createRadialGradient(x + s * 0.5, y + s * 0.48, 0, x + s * 0.5, y + s * 0.48, s * 0.55)
107
- g.addColorStop(0, 'rgba(255,255,255,0.35)')
108
- g.addColorStop(1, 'rgba(255,255,255,0)')
109
- ctx.fillStyle = g
110
- ctx.fillRect(x, y, s, s)
111
- CONFETTI.forEach(([cx, cy, r, col]) => {
112
- ctx.fillStyle = col
113
- ctx.beginPath()
114
- ctx.arc(x + cx * s, y + cy * s, r * s, 0, TAU)
115
- ctx.fill()
116
- })
117
- g = ctx.createLinearGradient(x, y + s * 0.2, x, y + s * 0.85)
118
- g.addColorStop(0, '#ff6b88')
119
- g.addColorStop(1, '#d42a55')
120
- ctx.fillStyle = g
121
- ctx.beginPath()
122
- heartPath(ctx, x + s * 0.5, y + s * 0.55, s * 0.34)
123
- ctx.fill()
124
- ctx.fillStyle = 'rgba(255,255,255,0.55)'
125
- ctx.save()
126
- ctx.translate(x + s * 0.37, y + s * 0.42)
127
- ctx.rotate(-0.6)
128
- ctx.scale(1, 0.55)
129
- ctx.beginPath()
130
- ctx.arc(0, 0, s * 0.06, 0, TAU)
131
- ctx.fill()
132
- ctx.restore()
133
- ctx.restore()
134
- }
135
-
136
- // 用真实引擎生成演示作品:插画按格面积取平均色 → 配色引擎
137
- function buildDemo(workCanvas, createCanvas) {
138
- const qr = makeQr(DEMO_TEXT, 'H')
139
- const placement = createPlacement(qr.size, { scale: 100, quiet: 1, hasImage: true })
140
- const size = placement.size
141
- const k = 6
142
- const px = size * k
143
- const canvas = createCanvas ? createCanvas(px, px) : workCanvas
144
- canvas.width = px
145
- canvas.height = px
146
- const ctx = canvas.getContext('2d')
147
- ctx.setTransform(1, 0, 0, 1, 0, 0)
148
- ctx.clearRect(0, 0, px, px)
149
- drawIllustration(ctx, 0, 0, px)
150
- const data = ctx.getImageData(0, 0, px, px).data
151
- const rgb = new Uint8Array(size * size * 3)
152
- for (let r = 0; r < size; r += 1) {
153
- for (let c = 0; c < size; c += 1) {
154
- let sr = 0; let sg = 0; let sb = 0
155
- for (let y = 0; y < k; y += 1) {
156
- let o = ((r * k + y) * px + c * k) * 4
157
- for (let x = 0; x < k; x += 1, o += 4) { sr += data[o]; sg += data[o + 1]; sb += data[o + 2] }
158
- }
159
- const i = (r * size + c) * 3
160
- rgb[i] = sr / (k * k); rgb[i + 1] = sg / (k * k); rgb[i + 2] = sb / (k * k)
161
- }
162
- }
163
- const art = computeArtwork({ size, rgb }, qr, placement, preparePalette('221'), { modeId: 'balanced', coherence: 40, cleanup: true })
164
- return { qr, placement, art }
165
- }
166
-
167
- // —— 豆子批量绘制:按颜色合并路径,每帧只需十几次填充 ——
168
- function addBead(groups, hex, x, y, r) {
169
- let list = groups.get(hex)
170
- if (!list) { list = []; groups.set(hex, list) }
171
- list.push(x, y, r)
172
- }
173
-
174
- // 小批量合并路径:一条路径里子路径太多时,部分渲染引擎反而更慢,每 12 颗提交一次最稳
175
- const CHUNK = 12
176
- function fillCircles(ctx, list, rs, ox, oy) {
177
- let n = 0
178
- ctx.beginPath()
179
- for (let i = 0; i < list.length; i += 3) {
180
- const r = list[i + 2] * rs
181
- const x = list[i] + list[i + 2] * ox
182
- const y = list[i + 1] + list[i + 2] * oy
183
- ctx.moveTo(x + r, y)
184
- ctx.arc(x, y, r, 0, TAU)
185
- n += 1
186
- if (n === CHUNK) { ctx.fill(); ctx.beginPath(); n = 0 }
187
- }
188
- if (n) ctx.fill()
189
- }
190
-
191
- function flushBeads(ctx, groups) {
192
- const all = []
193
- groups.forEach((list, hex) => {
194
- ctx.fillStyle = hex
195
- fillCircles(ctx, list, 1, 0, 0)
196
- all.push(list)
197
- })
198
- // 中孔:拼豆是一颗颗小管子
199
- ctx.fillStyle = 'rgba(70,48,24,0.17)'
200
- all.forEach((list) => fillCircles(ctx, list, 0.3, 0, 0))
201
- // 高光
202
- ctx.fillStyle = 'rgba(255,255,255,0.4)'
203
- all.forEach((list) => fillCircles(ctx, list, 0.2, -0.42, -0.42))
204
- groups.clear()
205
- }
206
-
207
- /**
208
- * @param opts { workCanvas?, createCanvas?(w,h), font?, loop? }
209
- * @returns { duration, text, artwork, draw(ctx, t, pixelWidth) }
210
- */
211
- function createIntro(opts = {}) {
212
- const font = opts.font || 'sans-serif'
213
- const loop = opts.loop !== false
214
- const F = (weight, px) => `${weight} ${Math.round(px)}px ${font}`
215
- const { qr, placement, art } = buildDemo(opts.workCanvas, opts.createCanvas)
216
- const size = art.size
217
- const R = rng(20260929)
218
- const cells = []
219
- for (let r = 0; r < size; r += 1) {
220
- for (let c = 0; c < size; c += 1) {
221
- const qc = c - placement.left
222
- const qrow = r - placement.top
223
- const dark = qc >= 0 && qrow >= 0 && qc < qr.size && qrow < qr.size && qr.dark[qrow * qr.size + qc] === 1
224
- const idx = art.cells[r * size + c]
225
- cells.push({
226
- c,
227
- r,
228
- qrHex: dark ? C.dark : C.light,
229
- finalHex: idx >= 0 ? art.pal.hexes[idx] : C.board,
230
- k: Math.max(0, Math.min(DEMO_TEXT.length - 1, Math.floor((c / size) * DEMO_TEXT.length + (R() - 0.5) * 2.5))),
231
- jx: R(),
232
- jy: R() - 0.5,
233
- bend: R() - 0.5,
234
- t0: 2.72 + (c / size) * 0.55 + R() * 0.55,
235
- tw: 5.95 + ((c + r) / (2 * (size - 1))) * 0.95 + R() * 0.08,
236
- })
237
- }
238
- }
239
- const finders = [[0, 0], [qr.size - 7, 0], [0, qr.size - 7]].map(([x, y]) => [placement.left + x, placement.top + y])
240
- const topColors = art.usage.slice(0, 8).map((u) => u.hex)
241
- const burst = Array.from({ length: 26 }, () => ({
242
- a: -Math.PI / 2 + (R() - 0.5) * 2.2,
243
- v: 520 + R() * 520,
244
- r: 9 + R() * 8,
245
- hex: topColors[Math.floor(R() * topColors.length)] || C.brand2,
246
- }))
247
- const groups = new Map()
248
- const pegDots = []
249
- for (let y = 20; y < U; y += 40) for (let x = 20; x < U; x += 40) pegDots.push(x, y, 2.4)
250
- let layout = null // 文本排版缓存(首帧测量)
251
-
252
- const textLayout = (ctx) => {
253
- if (layout) return layout
254
- ctx.font = F('bold', 64)
255
- const x0 = 170
256
- const charX = []
257
- for (let i = 0; i <= DEMO_TEXT.length; i += 1) charX.push(x0 + ctx.measureText(DEMO_TEXT.slice(0, i)).width)
258
- layout = { charX, y: 616 }
259
- return layout
260
- }
261
-
262
- const boardAt = (t) => {
263
- let cx = 540; let cy = 590; let B = 640
264
- const a = easeInOut(prog(t, 7.6, 8.2))
265
- cx = lerp(cx, 356, a); B = lerp(B, 560, a)
266
- const b = easeInOut(prog(t, 10.05, 10.75))
267
- cx = lerp(cx, 540, b); cy = lerp(cy, 410, b); B = lerp(B, 450, b)
268
- return { cx, cy, B }
269
- }
270
- const gridOf = (L) => {
271
- const pad = L.B * 0.05
272
- const cell = (L.B - pad * 2) / size
273
- return { cell, ox: L.cx - L.B / 2 + pad, oy: L.cy - L.B / 2 + pad }
274
- }
275
-
276
- const drawPlate = (ctx, L, alpha) => {
277
- const x = L.cx - L.B / 2
278
- const y = L.cy - L.B / 2
279
- const r = L.B * 0.045
280
- ctx.globalAlpha = alpha
281
- softShadow(ctx, x, y, L.B, L.B, r, 0.16)
282
- ctx.fillStyle = C.board
283
- ctx.beginPath(); rr(ctx, x, y, L.B, L.B, r); ctx.fill()
284
- ctx.globalAlpha = 1
285
- }
286
-
287
- // 背景:米色钉板 + 缓慢漂浮的装饰豆
288
- const drawBackground = (ctx, t) => {
289
- ctx.fillStyle = C.bg
290
- ctx.fillRect(0, 0, U, U)
291
- ctx.fillStyle = 'rgba(60,45,25,0.065)'
292
- fillCircles(ctx, pegDots, 1, 0, 0)
293
- DECOR.forEach(([x, y, r, hex], i) => {
294
- const dx = Math.sin(t * 0.9 + i * 1.7) * 10
295
- const dy = Math.cos(t * 0.7 + i * 2.3) * 12
296
- ctx.globalAlpha = 0.55
297
- addBead(groups, hex, x + dx, y + dy, r)
298
- })
299
- flushBeads(ctx, groups)
300
- ctx.globalAlpha = 1
301
- }
302
-
303
- const drawCaption = (ctx, t) => {
304
- ctx.textAlign = 'center'
305
- ctx.textBaseline = 'middle'
306
- SCENES.forEach((s, i) => {
307
- if (t < s.a || t > s.b) return
308
- const inP = easeOut(prog(t, s.a + 0.05, s.a + 0.5))
309
- const outP = easeIn(prog(t, s.b - 0.32, s.b))
310
- const alpha = inP * (1 - outP)
311
- if (alpha <= 0) return
312
- const dy = (1 - inP) * 26 - outP * 14
313
- ctx.globalAlpha = alpha
314
- ctx.font = F('bold', 24)
315
- const label = `第 ${i + 1} 步`
316
- const w = ctx.measureText(label).width + 40
317
- ctx.fillStyle = C.brandSoft
318
- ctx.beginPath(); rr(ctx, 540 - w / 2, 70 + dy, w, 46, 23); ctx.fill()
319
- ctx.fillStyle = C.brand
320
- ctx.fillText(label, 540, 94 + dy)
321
- ctx.fillStyle = C.ink
322
- ctx.font = F('bold', 60)
323
- ctx.fillText(s.title, 540, 170 + dy * 1.3)
324
- ctx.fillStyle = C.muted
325
- ctx.font = F('normal', 30)
326
- ctx.fillText(s.sub, 540, 232 + dy * 1.6)
327
- })
328
- ctx.globalAlpha = 1
329
- }
330
-
331
- const drawProgress = (ctx, t) => {
332
- const alpha = prog(t, 0, 0.35) * (1 - prog(t, 10.0, 10.4))
333
- if (alpha <= 0) return
334
- ctx.globalAlpha = alpha
335
- const w = 110; const gap = 16; const x0 = 540 - (w * 4 + gap * 3) / 2
336
- SCENES.forEach((s, i) => {
337
- const x = x0 + i * (w + gap)
338
- ctx.fillStyle = 'rgba(22,33,31,0.1)'
339
- ctx.beginPath(); rr(ctx, x, 1010, w, 8, 4); ctx.fill()
340
- const p = prog(t, s.a, s.b)
341
- if (p > 0) {
342
- ctx.fillStyle = C.brand
343
- ctx.beginPath(); rr(ctx, x, 1010, Math.max(8, w * p), 8, 4); ctx.fill()
344
- }
345
- })
346
- ctx.globalAlpha = 1
347
- }
348
-
349
- // 第 1 幕:输入框 + 打字
350
- const drawInput = (ctx, t) => {
351
- if (t > 3.35) return
352
- const inP = easeBack(prog(t, 0.1, 0.65))
353
- const outP = easeInOut(prog(t, 2.6, 3.15))
354
- const alpha = prog(t, 0.1, 0.4)
355
- if (alpha <= 0) return
356
- const L = textLayout(ctx)
357
- const press = 1 - 0.08 * bump(t, 2.1, 2.4)
358
- const scale = lerp(0.88, 1, inP) * lerp(1, 0.95, outP)
359
- ctx.save()
360
- ctx.translate(540, 590)
361
- ctx.scale(scale, scale)
362
- ctx.translate(-540, -590)
363
- ctx.globalAlpha = alpha * (1 - outP)
364
- softShadow(ctx, 130, 495, 820, 190, 40, 0.14)
365
- ctx.fillStyle = '#ffffff'
366
- ctx.beginPath(); rr(ctx, 130, 495, 820, 190, 40); ctx.fill()
367
- ctx.textAlign = 'left'
368
- ctx.textBaseline = 'middle'
369
- ctx.fillStyle = C.muted
370
- ctx.font = F('normal', 24)
371
- ctx.fillText('扫码后显示的内容', 170, 548)
372
- // 发送按钮
373
- ctx.save()
374
- ctx.translate(880, 590)
375
- ctx.scale(press, press)
376
- ctx.fillStyle = C.brand
377
- ctx.beginPath(); ctx.arc(0, 0, 44, 0, TAU); ctx.fill()
378
- ctx.strokeStyle = '#fff'
379
- ctx.lineWidth = 7
380
- ctx.lineCap = 'round'
381
- ctx.lineJoin = 'round'
382
- ctx.beginPath(); ctx.moveTo(-14, 0); ctx.lineTo(14, 0); ctx.moveTo(2, -13); ctx.lineTo(15, 0); ctx.lineTo(2, 13); ctx.stroke()
383
- ctx.restore()
384
- ctx.restore()
385
- // 文字逐字出现,随后逐字化作豆子
386
- const count = Math.floor(prog(t, 0.55, 1.9) * DEMO_TEXT.length + 0.0001)
387
- ctx.font = F('bold', 64)
388
- ctx.textAlign = 'left'
389
- ctx.fillStyle = C.ink
390
- for (let i = 0; i < count; i += 1) {
391
- const start = 2.7 + (i / DEMO_TEXT.length) * 0.55
392
- const a = alpha * (1 - prog(t, start, start + 0.35))
393
- if (a <= 0) continue
394
- ctx.globalAlpha = a
395
- ctx.fillText(DEMO_TEXT[i], L.charX[i], L.y)
396
- }
397
- const typing = t >= 0.55 && t <= 1.9
398
- if (t < 2.55 && (typing || Math.floor(t / 0.4) % 2 === 0)) {
399
- ctx.globalAlpha = alpha
400
- ctx.fillStyle = C.brand2
401
- ctx.fillRect(L.charX[count] + 4, L.y - 34, 5, 68)
402
- }
403
- ctx.globalAlpha = 1
404
- }
405
-
406
- // 主画板上的豆子:飞入 → 黑白二维码 → 扫过换色
407
- const drawMainBeads = (ctx, t, L) => {
408
- const G = gridOf(L)
409
- const TL = textLayout(ctx)
410
- const baseR = G.cell * BEAD_R
411
- for (let i = 0; i < cells.length; i += 1) {
412
- const b = cells[i]
413
- if (t < b.t0) continue
414
- const dx = G.ox + (b.c + 0.5) * G.cell
415
- const dy = G.oy + (b.r + 0.5) * G.cell
416
- let x = dx; let y = dy; let s = 1
417
- const f = prog(t, b.t0, b.t0 + FLIGHT)
418
- if (f < 1) {
419
- const sx = TL.charX[b.k] + b.jx * (TL.charX[b.k + 1] - TL.charX[b.k])
420
- const sy = TL.y + b.jy * 44
421
- const cx = (sx + dx) / 2 + b.bend * 220
422
- const cy = Math.min(sy, dy) - 110 - Math.abs(b.bend) * 160
423
- const e = easeInOut(f)
424
- const u = 1 - e
425
- x = u * u * sx + 2 * u * e * cx + e * e * dx
426
- y = u * u * sy + 2 * u * e * cy + e * e * dy
427
- s = lerp(0.55, 1, easeOut(f))
428
- } else {
429
- s = 1 + 0.16 * bump(t, b.t0 + FLIGHT, b.t0 + FLIGHT + 0.2)
430
- }
431
- s += 0.3 * bump(t, b.tw - 0.05, b.tw + 0.25)
432
- addBead(groups, t >= b.tw ? b.finalHex : b.qrHex, x, y, baseR * s)
433
- }
434
- flushBeads(ctx, groups)
435
- }
436
-
437
- const drawFinalBeads = (ctx, L) => {
438
- const G = gridOf(L)
439
- const r = G.cell * BEAD_R
440
- for (let i = 0; i < cells.length; i += 1) {
441
- const b = cells[i]
442
- addBead(groups, b.finalHex, G.ox + (b.c + 0.5) * G.cell, G.oy + (b.r + 0.5) * G.cell, r)
443
- }
444
- flushBeads(ctx, groups)
445
- }
446
-
447
- // 二维码定位图形高亮
448
- const drawFinders = (ctx, t, L) => {
449
- const p = prog(t, 4.15, 4.95)
450
- if (p <= 0 || p >= 1) return
451
- const G = gridOf(L)
452
- const alpha = Math.sin(Math.PI * p)
453
- const grow = (1 - easeOut(p)) * 18
454
- ctx.strokeStyle = C.brand2
455
- ctx.lineWidth = 6
456
- ctx.globalAlpha = alpha
457
- finders.forEach(([c, r]) => {
458
- const x = G.ox + c * G.cell - 6 - grow
459
- const y = G.oy + r * G.cell - 6 - grow
460
- const w = 7 * G.cell + 12 + grow * 2
461
- ctx.beginPath(); rr(ctx, x, y, w, w, 16); ctx.stroke()
462
- })
463
- ctx.globalAlpha = 1
464
- }
465
-
466
- // 沿对角线扫过的光带
467
- const drawGleam = (ctx, L, p, strength) => {
468
- if (p <= 0 || p >= 1) return
469
- const x = L.cx - L.B / 2
470
- const y = L.cy - L.B / 2
471
- const g = ctx.createLinearGradient(x, y, x + L.B, y + L.B)
472
- const s0 = clamp01(p - 0.12); const s1 = clamp01(p); const s2 = clamp01(p + 0.04)
473
- g.addColorStop(0, 'rgba(255,255,255,0)')
474
- if (s0 > 0) g.addColorStop(s0, 'rgba(255,255,255,0)')
475
- g.addColorStop(s1, `rgba(255,255,255,${strength})`)
476
- if (s2 < 1) g.addColorStop(s2, 'rgba(255,255,255,0)')
477
- g.addColorStop(1, 'rgba(255,255,255,0)')
478
- ctx.save()
479
- ctx.beginPath(); rr(ctx, x, y, L.B, L.B, L.B * 0.045); ctx.clip()
480
- ctx.fillStyle = g
481
- ctx.fillRect(x, y, L.B, L.B)
482
- ctx.restore()
483
- }
484
-
485
- // 第 3 幕:照片飞入并铺到画板上
486
- const drawPhoto = (ctx, t, L) => {
487
- if (t < 4.95 || t > 6.65) return
488
- const a = easeOut(prog(t, 5.0, 5.55))
489
- const b = easeInOut(prog(t, 5.6, 6.05))
490
- let x = lerp(1320, 800, a); let y = lerp(250, 330, a); let rot = lerp(0.4, -0.12, a); let s = 300
491
- x = lerp(x, L.cx, b); y = lerp(y, L.cy, b); rot = lerp(rot, 0, b); s = lerp(s, L.B * 0.9, b)
492
- const frame = lerp(14, 0, b)
493
- const alpha = 1 - prog(t, 6.0, 6.6)
494
- ctx.save()
495
- ctx.globalAlpha = alpha
496
- ctx.translate(x, y)
497
- ctx.rotate(rot)
498
- const half = s / 2 + frame
499
- if (b < 1) softShadow(ctx, -half, -half, half * 2, half * 2, 18, 0.2 * (1 - b))
500
- ctx.fillStyle = '#ffffff'
501
- ctx.beginPath(); rr(ctx, -half, -half, half * 2, half * 2, 18); ctx.fill()
502
- ctx.beginPath(); rr(ctx, -s / 2, -s / 2, s, s, 10); ctx.clip()
503
- drawIllustration(ctx, -s / 2, -s / 2, s)
504
- ctx.restore()
505
- ctx.globalAlpha = 1
506
- }
507
-
508
- // 第 4 幕:手机扫码
509
- const drawPhone = (ctx, t) => {
510
- const enter = easeOut(prog(t, 7.75, 8.35))
511
- const exit = easeIn(prog(t, 9.95, 10.4))
512
- if (enter <= 0 || exit >= 1) return
513
- const w = 340; const h = 660
514
- const px = 800
515
- const py = 590 + (1 - enter) * 860 + exit * 860
516
- const rot = lerp(0.16, -0.035, enter)
517
- ctx.save()
518
- ctx.translate(px, py)
519
- ctx.rotate(rot)
520
- softShadow(ctx, -w / 2, -h / 2, w, h, 60, 0.28)
521
- ctx.fillStyle = '#1b2321'
522
- ctx.beginPath(); rr(ctx, -w / 2, -h / 2, w, h, 60); ctx.fill()
523
- ctx.fillStyle = '#2c3633'
524
- ctx.fillRect(w / 2 - 1, -h / 2 + 150, 5, 70)
525
- // 屏幕
526
- const sw = w - 24; const sh = h - 24
527
- ctx.save()
528
- ctx.beginPath(); rr(ctx, -sw / 2, -sh / 2, sw, sh, 48); ctx.clip()
529
- ctx.fillStyle = '#0e1312'
530
- ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
531
- const jx = Math.sin(t * 3.1) * 3
532
- const jy = Math.cos(t * 2.3) * 3
533
- const cam = { cx: jx, cy: -30 + jy, B: 250 }
534
- ctx.globalAlpha = 0.92
535
- ctx.fillStyle = C.bg
536
- ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
537
- ctx.globalAlpha = 1
538
- drawPlate(ctx, cam, 1)
539
- drawFinalBeads(ctx, cam)
540
- ctx.fillStyle = 'rgba(10,15,14,0.45)'
541
- ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
542
- // 取景框
543
- const detect = prog(t, 9.0, 9.3)
544
- const snap = t >= 9.0 ? 1 + 0.1 * (1 - easeOut(detect)) : 1
545
- const half = 146 * snap
546
- ctx.strokeStyle = t >= 9.0 ? C.wechat : '#ffffff'
547
- ctx.lineWidth = 6
548
- ctx.lineCap = 'round'
549
- const arm = 34
550
- ctx.beginPath()
551
- ;[[-1, -1], [1, -1], [1, 1], [-1, 1]].forEach(([sx, sy]) => {
552
- const x = cam.cx + sx * half
553
- const y = cam.cy + sy * half
554
- ctx.moveTo(x, y - sy * arm); ctx.lineTo(x, y); ctx.lineTo(x - sx * arm, y)
555
- })
556
- ctx.stroke()
557
- // 扫描线
558
- const sp = prog(t, 8.3, 9.0)
559
- if (sp > 0 && sp < 1) {
560
- const top = cam.cy - 140
561
- const y = lerp(top, cam.cy + 140, easeInOut(sp))
562
- const g = ctx.createLinearGradient(0, y - 80, 0, y)
563
- g.addColorStop(0, 'rgba(7,193,96,0)')
564
- g.addColorStop(1, 'rgba(7,193,96,0.35)')
565
- ctx.fillStyle = g
566
- ctx.fillRect(cam.cx - 140, Math.max(top, y - 80), 280, y - Math.max(top, y - 80))
567
- ctx.fillStyle = C.wechat
568
- ctx.fillRect(cam.cx - 140, y - 2, 280, 4)
569
- }
570
- ctx.textAlign = 'center'
571
- ctx.textBaseline = 'middle'
572
- ctx.fillStyle = '#ffffff'
573
- ctx.font = F('bold', 24)
574
- ctx.fillText('扫一扫', 0, -sh / 2 + 78)
575
- if (t < 9.0) {
576
- ctx.globalAlpha = 0.7
577
- ctx.font = F('normal', 19)
578
- ctx.fillText('将二维码放入框内', 0, cam.cy + 185)
579
- ctx.globalAlpha = 1
580
- }
581
- const flash = t >= 9.0 ? 0.35 * (1 - prog(t, 9.0, 9.25)) : 0
582
- if (flash > 0) {
583
- ctx.fillStyle = `rgba(255,255,255,${flash.toFixed(3)})`
584
- ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
585
- }
586
- // 识别结果卡片
587
- const rp = prog(t, 9.1, 9.5)
588
- if (rp > 0) {
589
- const cw = sw - 36; const ch = 170
590
- const cy = sh / 2 - 18 - ch + (1 - easeBack(rp)) * 220
591
- ctx.fillStyle = '#ffffff'
592
- ctx.beginPath(); rr(ctx, -cw / 2, cy, cw, ch, 28); ctx.fill()
593
- ctx.fillStyle = C.wechat
594
- ctx.beginPath(); ctx.arc(0, cy + 44, 22, 0, TAU); ctx.fill()
595
- ctx.strokeStyle = '#fff'
596
- ctx.lineWidth = 5
597
- ctx.lineJoin = 'round'
598
- ctx.beginPath(); ctx.moveTo(-9, cy + 44); ctx.lineTo(-2, cy + 51); ctx.lineTo(10, cy + 37); ctx.stroke()
599
- ctx.fillStyle = C.ink
600
- ctx.font = F('bold', 30)
601
- ctx.fillText(DEMO_TEXT, 0, cy + 100)
602
- ctx.fillStyle = C.muted
603
- ctx.font = F('normal', 18)
604
- ctx.fillText('扫码结果', 0, cy + 138)
605
- }
606
- ctx.restore()
607
- // 灵动岛
608
- ctx.fillStyle = '#000'
609
- ctx.beginPath(); rr(ctx, -52, -h / 2 + 26, 104, 30, 15); ctx.fill()
610
- ctx.restore()
611
-
612
- // 识别成功:豆子礼花
613
- const bt = t - 9.2
614
- if (bt > 0 && bt < 1.4) {
615
- const ox = px; const oy = py + 200
616
- const fade = 1 - prog(bt, 0.9, 1.4)
617
- ctx.globalAlpha = fade
618
- burst.forEach((p) => {
619
- const x = ox + Math.cos(p.a) * p.v * bt
620
- const y = oy + Math.sin(p.a) * p.v * bt + 900 * bt * bt
621
- addBead(groups, p.hex, x, y, p.r)
622
- })
623
- flushBeads(ctx, groups)
624
- ctx.globalAlpha = 1
625
- }
626
- }
627
-
628
- // 片尾
629
- const drawEnd = (ctx, t) => {
630
- if (t < 10.4) return
631
- ctx.textAlign = 'center'
632
- ctx.textBaseline = 'middle'
633
- const a = easeOut(prog(t, 10.45, 10.95))
634
- ctx.globalAlpha = a
635
- ctx.fillStyle = C.ink
636
- ctx.font = F('bold', 64)
637
- ctx.fillText('豆维 · 可扫码的拼豆', 540, 745 + (1 - a) * 30)
638
- const b = easeOut(prog(t, 10.6, 11.1))
639
- ctx.globalAlpha = b
640
- ctx.fillStyle = C.muted
641
- ctx.font = F('normal', 32)
642
- ctx.fillText('把一句话藏进拼豆里,微信一扫就能读出来', 540, 812 + (1 - b) * 30)
643
- const chips = ['MARD 标准色号', 'A4 打印图纸', '微信一扫即读']
644
- ctx.font = F('bold', 26)
645
- const widths = chips.map((s) => ctx.measureText(s).width + 44)
646
- const total = widths.reduce((s, v) => s + v, 0) + 18 * (chips.length - 1)
647
- let x = 540 - total / 2
648
- chips.forEach((label, i) => {
649
- const p = easeBack(prog(t, 10.85 + i * 0.12, 11.3 + i * 0.12))
650
- if (p > 0) {
651
- ctx.globalAlpha = clamp01(p)
652
- const cy = 892 + (1 - p) * 24
653
- ctx.fillStyle = C.brandSoft
654
- ctx.beginPath(); rr(ctx, x, cy - 26, widths[i], 52, 26); ctx.fill()
655
- ctx.fillStyle = C.brand
656
- ctx.fillText(label, x + widths[i] / 2, cy)
657
- }
658
- x += widths[i] + 18
659
- })
660
- ctx.globalAlpha = 1
661
- }
662
-
663
- const draw = (ctx, time, pixelWidth) => {
664
- const t = loop ? ((time % DURATION) + DURATION) % DURATION : Math.min(time, DURATION)
665
- const k = pixelWidth / U
666
- ctx.setTransform(k, 0, 0, k, 0, 0)
667
- ctx.globalAlpha = 1
668
- drawBackground(ctx, t)
669
- const L = boardAt(t)
670
- if (t >= 2.65) {
671
- const p = easeBack(prog(t, 2.65, 3.15))
672
- const s = lerp(0.92, 1, p)
673
- const LS = { cx: L.cx, cy: L.cy, B: L.B * s }
674
- drawPlate(ctx, LS, prog(t, 2.65, 3.0))
675
- }
676
- drawInput(ctx, t)
677
- if (t >= 2.72) drawMainBeads(ctx, t, L)
678
- drawFinders(ctx, t, L)
679
- drawGleam(ctx, L, prog(t, 5.9, 7.0), 0.55)
680
- drawGleam(ctx, L, prog(t, 11.0, 11.7), 0.4)
681
- drawPhoto(ctx, t, L)
682
- drawPhone(ctx, t)
683
- drawCaption(ctx, t)
684
- drawProgress(ctx, t)
685
- drawEnd(ctx, t)
686
- if (loop) {
687
- const fade = easeInOut(prog(t, 11.45, 12))
688
- if (fade > 0) {
689
- ctx.globalAlpha = fade
690
- ctx.fillStyle = C.bg
691
- ctx.fillRect(0, 0, U, U)
692
- ctx.globalAlpha = 1
693
- }
694
- }
695
- ctx.setTransform(1, 0, 0, 1, 0, 0)
696
- }
697
-
698
- // 关键时间点(供视频配乐对齐)
699
- const timeline = { landings: cells.map((b) => b.t0 + FLIGHT), wipes: cells.map((b) => b.tw) }
700
- return { duration: DURATION, text: DEMO_TEXT, artwork: art, draw, timeline }
701
- }
702
-
703
- module.exports = { createIntro, DEMO_TEXT, DURATION }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/palette-data.js DELETED
@@ -1,48 +0,0 @@
1
- // MARD reference palette from the public 2026 color chart.
2
- // 291 colors = standard 221 (A-H, M) + 70 extended (P, Q, R, T, Y, ZG).
3
- const MARD_HEX = {
4
- A1:"#f9f0cd", A2:"#fbfbd4", A3:"#fafc9f", A4:"#ffe953", A5:"#f4d738", A6:"#fdad49", A7:"#ff7c2f", A8:"#eaca49",
5
- A9:"#ff995a", A10:"#ff9d55", A11:"#ffdd99", A12:"#fcb58f", A13:"#ffbb59", A14:"#ff6d40", A15:"#fdff44", A16:"#fef9ae",
6
- A17:"#ffe36e", A18:"#fecf98", A19:"#fd7b72", A20:"#efcd67", A21:"#ffe395", A22:"#fff3a4", A23:"#f3d5bf", A24:"#fbf8c9",
7
- A25:"#ffd67d", A26:"#ffbb27", B1:"#e6ee32", B2:"#5be419", B3:"#7cee9d", B4:"#1ef942", B5:"#00bd35", B6:"#5ae8ba",
8
- B7:"#03ac88", B8:"#029d26", B9:"#26523a", B10:"#95d3c2", B11:"#5d722a", B12:"#156f40", B13:"#d9f794", B14:"#ade945",
9
- B15:"#2e5132", B16:"#c6ed9c", B17:"#9bb13a", B18:"#e6ee49", B19:"#25b88c", B20:"#c2f0cc", B21:"#146a6b", B22:"#0b3c43",
10
- B23:"#303921", B24:"#eefca5", B25:"#4e846d", B26:"#8c7a36", B27:"#d1dcc1", B28:"#9ee5b9", B29:"#c5e254", B30:"#ecfbd0",
11
- B31:"#c4e6b5", B32:"#9bab5a", C1:"#e8ffe7", C2:"#bcf9f6", C3:"#a0e2fb", C4:"#42ccff", C5:"#01aceb", C6:"#50a9f0",
12
- C7:"#0188d3", C8:"#1054c0", C9:"#314bca", C10:"#3ebce2", C11:"#03b9b9", C12:"#1c334d", C13:"#cde8ff", C14:"#d5fdff",
13
- C15:"#23c4c6", C16:"#1757a8", C17:"#50d3ec", C18:"#1c3344", C19:"#1787a2", C20:"#0082be", C21:"#beddff", C22:"#67b4be",
14
- C23:"#c2dceb", C24:"#7dc4ff", C25:"#a9e5e5", C26:"#2f99b3", C27:"#ebf5fc", C28:"#bbcfed", C29:"#4b5ba3", D1:"#aeb4f2",
15
- D2:"#858edd", D3:"#3054af", D4:"#182a84", D5:"#b843c5", D6:"#ac7bde", D7:"#6e399a", D8:"#e2d3ff", D9:"#d5b9f8",
16
- D10:"#361b50", D11:"#b9bae1", D12:"#de9ad4", D13:"#b90295", D14:"#8b279b", D15:"#2f1f90", D16:"#e2e1ee", D17:"#c4d4f6",
17
- D18:"#a45ec7", D19:"#d8c3d7", D20:"#9c32b2", D21:"#9a009b", D22:"#333995", D23:"#eadafc", D24:"#7786e5", D25:"#484fc7",
18
- D26:"#e9c3f6", E1:"#fdd3cc", E2:"#fecddf", E3:"#ff97c3", E4:"#e8649e", E5:"#f551a2", E6:"#ff346b", E7:"#c63578",
19
- E8:"#ffdbe9", E9:"#e970cc", E10:"#d33893", E11:"#fcddd2", E12:"#ffa1c5", E13:"#b6006d", E14:"#ffd1ba", E15:"#f2cfd0",
20
- E16:"#ffecde", E17:"#ffe2ea", E18:"#ffc9d6", E19:"#ffd2e7", E20:"#d8c7d1", E21:"#bd9da1", E22:"#cc78a7", E23:"#937a8d",
21
- E24:"#f6e4f9", F1:"#fd957b", F2:"#fc3d45", F3:"#f74941", F4:"#fc283c", F5:"#d80127", F6:"#b0443d", F7:"#971937",
22
- F8:"#bc0127", F9:"#e2677a", F10:"#a74d22", F11:"#6f201f", F12:"#fd4d6a", F13:"#dd422f", F14:"#ffa9ad", F15:"#c80020",
23
- F16:"#ffd9c8", F17:"#f79b71", F18:"#d37c46", F19:"#c1444a", F20:"#cd9391", F21:"#f4b1b4", F22:"#ffd0cb", F23:"#f57e66",
24
- F24:"#fcc1c4", F25:"#e54b4f", G1:"#ffe2ce", G2:"#ffcaaa", G3:"#f4c3a5", G4:"#e1b383", G5:"#ed9435", G6:"#f59734",
25
- G7:"#9d5b3e", G8:"#592a21", G9:"#e6b483", G10:"#c88135", G11:"#e0c593", G12:"#ebbb83", G13:"#b7714a", G14:"#8d614c",
26
- G15:"#fcf9e0", G16:"#f2d9ba", G17:"#56403c", G18:"#ffe4cc", G19:"#e1943a", G20:"#a94023", G21:"#cb8e77", H1:"#e2e2e2",
27
- H2:"#ffffff", H3:"#b3b3b3", H4:"#868686", H5:"#474747", H6:"#2c2c2c", H7:"#000000", H8:"#e7d6db", H9:"#e4e7e3",
28
- H10:"#eee9ea", H11:"#cecdd5", H12:"#fff5ed", H13:"#f3e1c9", H14:"#cfd7d3", H15:"#98a6a8", H16:"#3b2f23", H17:"#f1eded",
29
- H18:"#fffdf0", H19:"#f6efe2", H20:"#949fa3", H21:"#f7f3e4", H22:"#cacad5", H23:"#9a9d94", M1:"#bcc6b8", M2:"#8aa385",
30
- M3:"#697d80", M4:"#dacebe", M5:"#d0ccaa", M6:"#b0a782", M7:"#b4a497", M8:"#b38281", M9:"#a58767", M10:"#c5b1bc",
31
- M11:"#9f7494", M12:"#644749", M13:"#d19066", M14:"#c77361", M15:"#757d7b", P1:"#fcf8f9", P2:"#bda9ab", P3:"#aedda9",
32
- P4:"#fda49e", P5:"#ec8d3d", P6:"#60cfa8", P7:"#eb9271", P8:"#f0d958", P9:"#d9d9d9", P10:"#d5c8e9", P11:"#f3ecc8",
33
- P12:"#e6eef1", P13:"#a9cbf1", P14:"#3177b0", P15:"#668575", P16:"#ffbe46", P17:"#ffa324", P18:"#feb89f", P19:"#ffe0e8",
34
- P20:"#febecf", P21:"#ecbec0", P22:"#e4a89e", P23:"#a56269", Q1:"#f2a5e8", Q2:"#73b29e", Q3:"#ffff00", Q4:"#ffebfa",
35
- Q5:"#4f5e5b", R1:"#d50e21", R2:"#f92e83", R3:"#fd8225", R4:"#f8ec31", R5:"#34c75b", R6:"#25b891", R7:"#17779d",
36
- R8:"#1b60c3", R9:"#9a56b4", R10:"#ffdb4d", R11:"#ffebfa", R12:"#d8d5ce", R13:"#55514c", R14:"#9ee4df", R15:"#77cee9",
37
- R16:"#3dcfca", R17:"#4a867a", R18:"#7fcd9d", R19:"#cde55d", R20:"#e8c7b4", R21:"#ad6f3c", R22:"#6c372f", R23:"#feb872",
38
- R24:"#f2c1c0", R25:"#c9675d", R26:"#d293be", R27:"#ea8cb1", R28:"#9c87d6", T1:"#e2dfd7", Y1:"#fd6fb4", Y2:"#feb481",
39
- Y3:"#d7faa0", Y4:"#8bdbfa", Y5:"#e987ea", ZG1:"#daabb3", ZG2:"#d6aa87", ZG3:"#c1bd8d", ZG4:"#96869f", ZG5:"#8490a6",
40
- ZG6:"#94bfe2", ZG7:"#e2a9d2", ZG8:"#ab91c0",
41
- }
42
-
43
- const MARD_PALETTE = Object.entries(MARD_HEX).map(([id, hex]) => ({ id, hex }))
44
-
45
- // 智能近色色库的“远点优先”排序(离线预计算,避免真机启动时 2000 万次距离运算)
46
- const MARD_SMART_ORDER = ["A4","A7","B5","B9","C4","C8","C12","D7","E5","F3","G5","G8","H2","H3","H5","H7","M3","M12","P3","P14","Q1","R5","R13","ZG6","E13","B17","Q3","B2","A22","C11","D18","P6","G20","F5","P4","M8","D15","C2","D24","A19","B4","B14","G13","B21","R12","E7","C7","D21","E9","B19","D25","M2","D8","D20","R16","R23","B26","B11","E6","F7","E22","F19","ZG2","A26","A2","C5","B3","C28","E2","M13","B1","B13","ZG4","C6","R19","R28","G2","ZG1","E3","P23","A8","A9","R14","C29","C24","H6","M6","B7","G7","Q2","M5","D10","B8","B20","B10","F9","C22","C26","B12","B25","F4","D5","C19","B32","F13","A25","H15","D13","G10","B6","Y5","H10","Y1","D14","D1","D3","D12","E21","E10","M9","D9","R27","C10","R18","H13","H4","P21","ZG3","G11","C15","ZG8","P7","D4","C3","M14","A20","F8","R22","C14","D22","A15","A6","R26","R2","C16","B31","F12","R15","F20","C17","A11","F11","P17","A5","C9","P22","E4","B18","C1","C21","M11","B16","R9","F25","A12","A14","Y4","G14","B28","H23","P20","C20","C27","ZG5","B22","G12","M1","D6","F6","A13","P10","D11","F18","F1","Q5","Q4","M10","G17","H8","A17","D26","F21","P19","B24","P8","H22","P13","C13","F22","G21","R4","D2","H16","M4","P16","R21","E14","R7","R8","P18","H12","B27","D19","ZG7","E16","P2","B30","R20","F14","G16","R10","M7","E23","B23","D16","C23","F23","R1","Y2","A18","E15","G3","F15","P15","R17","D17","F2","E11","A1","F10","F17","G19","M15","G4","R25","C25","Y3","B29","H9","G15","R3","P9","P12","A16","F24","A3","E24","B15","D23","T1","P5","H14","H19","E12"]
47
-
48
- module.exports = { MARD_PALETTE, MARD_SMART_ORDER }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/palette.js DELETED
@@ -1,74 +0,0 @@
1
- // MARD 色库预设 + 预处理(Typed Array 结构,供匹配引擎高速读取)
2
- const { MARD_PALETTE, MARD_SMART_ORDER } = require('./palette-data')
3
- const { hexToRgb, rgbToOklabInto, luma } = require('./color')
4
-
5
- // 颜色匹配模式:darkMax/lightMin 为二维码深/浅模块允许的亮度范围
6
- const MATCH_MODES = [
7
- { id: 'rich', label: '色彩优先', hint: '更像原图', darkMax: 0.4, lightMin: 0.74 },
8
- { id: 'balanced', label: '均衡', hint: '推荐', darkMax: 0.36, lightMin: 0.78 },
9
- { id: 'strict', label: '扫码优先', hint: '最稳', darkMax: 0.3, lightMin: 0.84 },
10
- ]
11
-
12
- const byId = new Map(MARD_PALETTE.map((c) => [c.id, c]))
13
- const pick = (ids) => ids.map((id) => byId.get(id)).filter(Boolean)
14
- const STANDARD_221 = MARD_PALETTE.filter((c) => /^[A-HM]\d+$/.test(c.id))
15
-
16
- const PRESETS = [
17
- { id: '221', label: '标准 221 色', short: '221 色', detail: 'MARD 标准色,最常见', list: () => STANDARD_221 },
18
- { id: '291', label: '完整 291 色', short: '291 色', detail: '含扩展色,颜色最全', list: () => MARD_PALETTE },
19
- ...[144, 96, 72, 48, 24].map((n) => ({
20
- id: String(n),
21
- label: `精选 ${n} 色`,
22
- short: `${n} 色`,
23
- detail: n <= 48 ? '入门套装,买豆更省' : '常用色,兼顾效果与成本',
24
- list: () => pick(MARD_SMART_ORDER.slice(0, n)),
25
- })),
26
- ]
27
-
28
- const cache = {}
29
-
30
- function preparePalette(presetId) {
31
- if (cache[presetId]) return cache[presetId]
32
- const preset = PRESETS.find((p) => p.id === presetId) || PRESETS[0]
33
- const list = preset.list()
34
- const n = list.length
35
- const rgb = new Uint8Array(n * 3)
36
- const lab = new Float32Array(n * 3)
37
- const lum = new Float32Array(n)
38
- list.forEach((c, i) => {
39
- const [r, g, b] = hexToRgb(c.hex)
40
- rgb[i * 3] = r; rgb[i * 3 + 1] = g; rgb[i * 3 + 2] = b
41
- rgbToOklabInto(r, g, b, lab, i * 3)
42
- lum[i] = luma(r, g, b)
43
- })
44
- // 色与色之间的距离矩阵(291² ≈ 8.5 万项,一次性计算)
45
- const pair = new Float32Array(n * n)
46
- for (let i = 0; i < n; i += 1) {
47
- for (let j = i + 1; j < n; j += 1) {
48
- const dl = lab[i * 3] - lab[j * 3]
49
- const da = lab[i * 3 + 1] - lab[j * 3 + 1]
50
- const db = lab[i * 3 + 2] - lab[j * 3 + 2]
51
- const d = Math.sqrt(dl * dl + da * da + db * db)
52
- pair[i * n + j] = d
53
- pair[j * n + i] = d
54
- }
55
- }
56
- const prepared = {
57
- id: preset.id,
58
- label: preset.label,
59
- short: preset.short,
60
- n,
61
- ids: list.map((c) => c.id),
62
- hexes: list.map((c) => c.hex),
63
- rgb,
64
- lab,
65
- lum,
66
- pair,
67
- subsets: {},
68
- luts: {},
69
- }
70
- cache[presetId] = prepared
71
- return prepared
72
- }
73
-
74
- module.exports = { MATCH_MODES, PRESETS, preparePalette }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/pattern.js DELETED
@@ -1,381 +0,0 @@
1
- // Canvas 绘制:预览、PNG 图纸页、作品信息页、水印
2
- // 性能要点:按颜色批量合并路径,每种颜色只 fill 一次;字体只设置一次;网格线合并成单条路径
3
- const { lumaHex } = require('./color')
4
-
5
- const BOARD = '#f1eee7' // 钉板底色
6
- const PEG = '#d9d3c7' // 空钉位
7
- const INK = '#1d2b2a'
8
- const MUTED = '#6b7671'
9
- const BRAND = '#0f766e'
10
-
11
- function roundRectPath(ctx, x, y, w, h, r) {
12
- const rr = Math.min(r, w / 2, h / 2)
13
- ctx.moveTo(x + rr, y)
14
- ctx.arcTo(x + w, y, x + w, y + h, rr)
15
- ctx.arcTo(x + w, y + h, x, y + h, rr)
16
- ctx.arcTo(x, y + h, x, y, rr)
17
- ctx.arcTo(x, y, x + w, y, rr)
18
- ctx.closePath()
19
- }
20
-
21
- function fitText(ctx, value, maxWidth) {
22
- const text = String(value || '')
23
- if (ctx.measureText(text).width <= maxWidth) return text
24
- let end = text.length
25
- while (end > 0 && ctx.measureText(`${text.slice(0, end)}…`).width > maxWidth) end -= 1
26
- return `${text.slice(0, end)}…`
27
- }
28
-
29
- // 把同色格子分组:返回 Map<colorIndex, number[] cellIndex>
30
- function groupCells(artwork, region) {
31
- const { size, cells } = artwork
32
- const r = region || { col: 0, row: 0, cols: size, rows: size }
33
- const groups = new Map()
34
- const empty = []
35
- for (let y = r.row; y < r.row + r.rows; y += 1) {
36
- for (let x = r.col; x < r.col + r.cols; x += 1) {
37
- const c = cells[y * size + x]
38
- if (c < 0) { empty.push(y * size + x); continue }
39
- let list = groups.get(c)
40
- if (!list) { list = []; groups.set(c, list) }
41
- list.push(y * size + x)
42
- }
43
- }
44
- return { groups, empty, region: r }
45
- }
46
-
47
- /**
48
- * 绘制珠子
49
- * opts: { x0, y0, cell, style: square|round|hollow, region }
50
- */
51
- function drawBeads(ctx, artwork, opts) {
52
- const { x0, y0, cell, style = 'square' } = opts
53
- const { size, pal } = artwork
54
- const { groups, empty, region } = groupCells(artwork, opts.region)
55
- const ARC_CHUNK = 12
56
- const pos = (i) => {
57
- const x = i % size
58
- const y = (i - x) / size
59
- return [x0 + (x - region.col) * cell, y0 + (y - region.row) * cell]
60
- }
61
- ctx.fillStyle = BOARD
62
- ctx.fillRect(x0, y0, region.cols * cell, region.rows * cell)
63
- // 空钉位:小圆点,提示“这里不放豆”
64
- if (empty.length) {
65
- ctx.fillStyle = PEG
66
- ctx.beginPath()
67
- const r = cell * 0.14
68
- empty.forEach((i, n) => {
69
- const [x, y] = pos(i)
70
- ctx.moveTo(x + cell / 2 + r, y + cell / 2)
71
- ctx.arc(x + cell / 2, y + cell / 2, r, 0, Math.PI * 2)
72
- if (n % ARC_CHUNK === ARC_CHUNK - 1) { ctx.fill(); ctx.beginPath() }
73
- })
74
- ctx.fill()
75
- }
76
- const radius = cell * 0.46
77
- const hole = cell * 0.2
78
- groups.forEach((list, c) => {
79
- ctx.fillStyle = pal.hexes[c]
80
- ctx.beginPath()
81
- if (style === 'square') {
82
- list.forEach((i) => {
83
- const [x, y] = pos(i)
84
- // 取整边界避免相邻方块之间出现发丝缝
85
- const lx = Math.round(x); const ty = Math.round(y)
86
- ctx.rect(lx, ty, Math.round(x + cell) - lx, Math.round(y + cell) - ty)
87
- })
88
- } else {
89
- // 圆形子路径过多时渲染引擎会明显变慢,按小批提交(大图从数秒降到几十毫秒)
90
- list.forEach((i, n) => {
91
- const [x, y] = pos(i)
92
- const cx = x + cell / 2; const cy = y + cell / 2
93
- ctx.moveTo(cx + radius, cy)
94
- ctx.arc(cx, cy, radius, 0, Math.PI * 2)
95
- if (style === 'hollow') {
96
- ctx.moveTo(cx + hole, cy)
97
- ctx.arc(cx, cy, hole, 0, Math.PI * 2, true)
98
- }
99
- if (n % ARC_CHUNK === ARC_CHUNK - 1) { ctx.fill(); ctx.beginPath() }
100
- })
101
- }
102
- ctx.fill()
103
- })
104
- }
105
-
106
- // 色号文字:先按深/浅文字颜色分两批
107
- function drawCodes(ctx, artwork, opts) {
108
- const { x0, y0, cell, style = 'square' } = opts
109
- const { size, cells, pal } = artwork
110
- const region = opts.region || { col: 0, row: 0, cols: size, rows: size }
111
- const light = pal.hexes.map((hex) => lumaHex(hex) > 0.56)
112
- const fontSize = Math.max(8, Math.round(cell * (style === 'hollow' ? 0.26 : 0.32)))
113
- ctx.font = `bold ${fontSize}px sans-serif`
114
- ctx.textAlign = 'center'
115
- ctx.textBaseline = 'middle'
116
- const pass = (wantLight) => {
117
- ctx.fillStyle = wantLight ? INK : '#ffffff'
118
- for (let y = region.row; y < region.row + region.rows; y += 1) {
119
- for (let x = region.col; x < region.col + region.cols; x += 1) {
120
- const c = cells[y * size + x]
121
- if (c < 0) continue
122
- const useInk = style === 'hollow' ? true : light[c]
123
- if (useInk !== wantLight) continue
124
- ctx.fillText(pal.ids[c], x0 + (x - region.col + 0.5) * cell, y0 + (y - region.row + 0.5) * cell + fontSize * 0.04)
125
- }
126
- }
127
- }
128
- pass(true)
129
- pass(false)
130
- ctx.textAlign = 'start'
131
- ctx.textBaseline = 'alphabetic'
132
- }
133
-
134
- function rgba(hex, alpha) {
135
- const v = parseInt(hex.slice(1), 16)
136
- return `rgba(${(v >> 16) & 255},${(v >> 8) & 255},${v & 255},${alpha})`
137
- }
138
-
139
- // 网格 + 坐标:细线每格一条,粗线每 interval 格一条(按全局坐标对齐)
140
- function drawGrid(ctx, opts) {
141
- const { x0, y0, cell, region, interval = 10, color = BRAND, opacity = 60, labels = true } = opts
142
- const w = region.cols * cell
143
- const h = region.rows * cell
144
- const alpha = opacity / 100
145
- ctx.lineWidth = Math.max(1, cell * 0.02)
146
- ctx.strokeStyle = rgba(color, alpha * 0.28)
147
- ctx.beginPath()
148
- for (let i = 0; i <= region.cols; i += 1) { const x = Math.round(x0 + i * cell) + 0.5; ctx.moveTo(x, y0); ctx.lineTo(x, y0 + h) }
149
- for (let i = 0; i <= region.rows; i += 1) { const y = Math.round(y0 + i * cell) + 0.5; ctx.moveTo(x0, y); ctx.lineTo(x0 + w, y) }
150
- ctx.stroke()
151
- ctx.lineWidth = Math.max(2, cell * 0.06)
152
- ctx.strokeStyle = rgba(color, alpha)
153
- ctx.beginPath()
154
- for (let i = 0; i <= region.cols; i += 1) {
155
- const g = region.col + i
156
- if (g % interval !== 0 && i !== 0 && i !== region.cols) continue
157
- const x = x0 + i * cell; ctx.moveTo(x, y0); ctx.lineTo(x, y0 + h)
158
- }
159
- for (let i = 0; i <= region.rows; i += 1) {
160
- const g = region.row + i
161
- if (g % interval !== 0 && i !== 0 && i !== region.rows) continue
162
- const y = y0 + i * cell; ctx.moveTo(x0, y); ctx.lineTo(x0 + w, y)
163
- }
164
- ctx.stroke()
165
- if (!labels) return
166
- const fontSize = Math.max(10, Math.round(Math.min(cell * 0.42, 34)))
167
- ctx.font = `${fontSize}px sans-serif`
168
- ctx.fillStyle = rgba(color, Math.max(0.7, alpha))
169
- ctx.textAlign = 'center'
170
- ctx.textBaseline = 'middle'
171
- const step = cell < 22 ? interval : (cell < 36 ? 5 : 1)
172
- for (let i = 0; i < region.cols; i += 1) {
173
- const g = region.col + i + 1
174
- if (g % step !== 0 && g !== 1) continue
175
- ctx.fillText(String(g), x0 + (i + 0.5) * cell, y0 - fontSize * 0.9)
176
- }
177
- for (let i = 0; i < region.rows; i += 1) {
178
- const g = region.row + i + 1
179
- if (g % step !== 0 && g !== 1) continue
180
- ctx.fillText(String(g), x0 - fontSize * 1.1, y0 + (i + 0.5) * cell)
181
- }
182
- ctx.textAlign = 'start'
183
- ctx.textBaseline = 'alphabetic'
184
- }
185
-
186
- // 屏幕预览:整张作品,不带坐标(清爽)
187
- function renderPreview(canvas, artwork, pixelWidth, style) {
188
- const size = artwork.size
189
- const pad = Math.round(pixelWidth * 0.03)
190
- const cell = (pixelWidth - pad * 2) / size
191
- canvas.width = pixelWidth
192
- canvas.height = pixelWidth
193
- const ctx = canvas.getContext('2d')
194
- ctx.fillStyle = BOARD
195
- ctx.fillRect(0, 0, pixelWidth, pixelWidth)
196
- drawBeads(ctx, artwork, { x0: pad, y0: pad, cell, style })
197
- }
198
-
199
- // —— PNG 图纸页 ——
200
- function patternPageLayout(size, cell) {
201
- const axis = Math.max(48, Math.round(cell * 1.1))
202
- const pad = Math.max(48, Math.round(cell * 0.9))
203
- const header = Math.max(120, Math.round(cell * 2.4))
204
- const grid = size * cell
205
- return { axis, pad, header, width: grid + axis + pad * 2, height: header + axis + grid + pad * 2 }
206
- }
207
-
208
- function drawPatternPage(ctx, artwork, opts) {
209
- const { cell, style, grid, meta } = opts
210
- const L = patternPageLayout(artwork.size, cell)
211
- ctx.fillStyle = '#fbfaf7'
212
- ctx.fillRect(0, 0, L.width, L.height)
213
- // 头部
214
- const unit = L.header / 120
215
- ctx.fillStyle = INK
216
- ctx.font = `bold ${Math.round(40 * unit)}px sans-serif`
217
- ctx.textBaseline = 'middle'
218
- ctx.fillText('豆维 · 拼豆图纸', L.pad, L.pad * 0.6 + L.header * 0.36)
219
- ctx.font = `${Math.round(22 * unit)}px sans-serif`
220
- ctx.fillStyle = MUTED
221
- ctx.fillText(fitText(ctx, meta, L.width - L.pad * 2), L.pad, L.pad * 0.6 + L.header * 0.74)
222
- ctx.textBaseline = 'alphabetic'
223
- const x0 = L.pad + L.axis
224
- const y0 = L.header + L.pad * 0.6 + L.axis
225
- const region = { col: 0, row: 0, cols: artwork.size, rows: artwork.size }
226
- drawBeads(ctx, artwork, { x0, y0, cell, style, region })
227
- if (cell >= 16) drawCodes(ctx, artwork, { x0, y0, cell, style, region })
228
- if (grid.enabled) drawGrid(ctx, { x0, y0, cell, region, interval: grid.interval, color: grid.color, opacity: grid.opacity })
229
- return L
230
- }
231
-
232
- // —— 作品信息页(A4 比例,可分页)——
233
- function buildInfoPages({ artwork, width, text, paletteLabel, mpCode, thumbnail }) {
234
- const height = Math.round(width * 297 / 210)
235
- const u = width / 1600
236
- const pad = 96 * u
237
- const colors = artwork.usage.slice().sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }))
238
- const columns = 4
239
- const gap = 24 * u
240
- const cardW = (width - pad * 2 - gap * (columns - 1)) / columns
241
- const cardH = 124 * u
242
- const headerH = 300 * u
243
- // 缩略图区:headerH+64 起,高 400;清单标题与说明各占一行,避免与上方重叠
244
- const listTitleY = headerH + 64 * u + 400 * u + 96 * u
245
- const firstTop = listTitleY + 88 * u
246
- const nextTop = 200 * u
247
- const bottom = height - 110 * u
248
- const rowsFirst = Math.max(1, Math.floor((bottom - firstTop + gap) / (cardH + gap)))
249
- const rowsNext = Math.max(1, Math.floor((bottom - nextTop + gap) / (cardH + gap)))
250
- const pages = []
251
- let rest = colors.length
252
- let first = true
253
- do {
254
- const take = Math.min(rest, (first ? rowsFirst : rowsNext) * columns)
255
- pages.push(take)
256
- rest -= take
257
- first = false
258
- } while (rest > 0)
259
-
260
- const drawCard = (ctx, color, x, y) => {
261
- ctx.fillStyle = '#ffffff'
262
- ctx.beginPath(); roundRectPath(ctx, x, y, cardW, cardH, 18 * u); ctx.fill()
263
- ctx.strokeStyle = '#e7e2d8'; ctx.lineWidth = 2 * u; ctx.stroke()
264
- const sw = 76 * u
265
- ctx.fillStyle = color.hex
266
- ctx.beginPath(); ctx.arc(x + 24 * u + sw / 2, y + cardH / 2, sw / 2, 0, Math.PI * 2); ctx.fill()
267
- ctx.strokeStyle = 'rgba(0,0,0,0.12)'; ctx.stroke()
268
- ctx.fillStyle = INK
269
- ctx.font = `bold ${Math.round(34 * u)}px sans-serif`
270
- ctx.fillText(color.id, x + 24 * u + sw + 20 * u, y + 56 * u)
271
- ctx.fillStyle = BRAND
272
- ctx.font = `${Math.round(26 * u)}px sans-serif`
273
- ctx.fillText(`${color.count} 颗`, x + 24 * u + sw + 20 * u, y + 96 * u)
274
- }
275
-
276
- const draw = (ctx, page) => {
277
- ctx.textAlign = 'start'
278
- ctx.textBaseline = 'alphabetic'
279
- ctx.fillStyle = '#fbfaf7'
280
- ctx.fillRect(0, 0, width, height)
281
- let top = nextTop
282
- if (page === 0) {
283
- ctx.fillStyle = INK
284
- ctx.fillRect(0, 0, width, headerH)
285
- ctx.fillStyle = '#ffffff'
286
- ctx.font = `bold ${Math.round(62 * u)}px sans-serif`
287
- ctx.fillText('豆维 · 作品信息与用豆清单', pad, 132 * u)
288
- ctx.fillStyle = '#9fd3c7'
289
- ctx.font = `${Math.round(28 * u)}px sans-serif`
290
- ctx.fillText('照着清单备豆,照着图纸拼,拼完用微信扫一扫', pad, 200 * u)
291
- if (mpCode) {
292
- const s = 200 * u
293
- const x = width - pad - s
294
- const y = (headerH - s) / 2
295
- ctx.fillStyle = '#ffffff'
296
- ctx.beginPath(); roundRectPath(ctx, x - 10 * u, y - 10 * u, s + 20 * u, s + 20 * u, 20 * u); ctx.fill()
297
- ctx.drawImage(mpCode, x, y, s, s)
298
- }
299
- // 缩略图 + 参数
300
- const thumbS = 400 * u
301
- const ty = headerH + 64 * u
302
- if (thumbnail) {
303
- ctx.fillStyle = '#ffffff'
304
- ctx.beginPath(); roundRectPath(ctx, pad, ty, thumbS, thumbS, 24 * u); ctx.fill()
305
- const inner = thumbS - 32 * u
306
- const cell = inner / artwork.size
307
- drawBeads(ctx, artwork, { x0: pad + 16 * u, y0: ty + 16 * u, cell, style: 'square' })
308
- }
309
- const mx = thumbnail ? pad + thumbS + 56 * u : pad
310
- const rows = [
311
- ['扫码内容', text],
312
- ['作品尺寸', `${artwork.size} × ${artwork.size} 格`],
313
- ['用豆总数', `${artwork.beads.toLocaleString()} 颗 · ${artwork.usage.length} 个色号`],
314
- ['二维码', `${artwork.qr.size} × ${artwork.qr.size} 格 · 纠错 ${artwork.qr.level}`],
315
- ['色库', paletteLabel],
316
- ]
317
- rows.forEach(([k, v], idx) => {
318
- const y = ty + 52 * u + idx * 74 * u
319
- ctx.fillStyle = MUTED
320
- ctx.font = `${Math.round(26 * u)}px sans-serif`
321
- ctx.fillText(k, mx, y)
322
- ctx.fillStyle = INK
323
- ctx.font = `bold ${Math.round(28 * u)}px sans-serif`
324
- ctx.fillText(fitText(ctx, v, width - pad - mx - 150 * u), mx + 150 * u, y)
325
- })
326
- ctx.fillStyle = INK
327
- ctx.font = `bold ${Math.round(42 * u)}px sans-serif`
328
- ctx.fillText('用豆清单', pad, listTitleY)
329
- ctx.fillStyle = MUTED
330
- ctx.font = `${Math.round(24 * u)}px sans-serif`
331
- ctx.fillText('按色号排序 · 建议每色多备 5% 以防掉落', pad, listTitleY + 44 * u)
332
- top = firstTop
333
- } else {
334
- ctx.fillStyle = INK
335
- ctx.font = `bold ${Math.round(36 * u)}px sans-serif`
336
- ctx.fillText(`用豆清单(续 ${page + 1}/${pages.length})`, pad, 120 * u)
337
- }
338
- const offset = pages.slice(0, page).reduce((s, v) => s + v, 0)
339
- for (let i = 0; i < pages[page]; i += 1) {
340
- const col = i % columns
341
- const row = Math.floor(i / columns)
342
- drawCard(ctx, colors[offset + i], pad + col * (cardW + gap), top + row * (cardH + gap))
343
- }
344
- ctx.fillStyle = '#a3aaa5'
345
- ctx.font = `${Math.round(22 * u)}px sans-serif`
346
- ctx.fillText(`豆维 · 可扫码的拼豆 第 ${page + 1} / ${pages.length} 页`, pad, height - 56 * u)
347
- }
348
- return { width, height, pageCount: pages.length, draw }
349
- }
350
-
351
- // 轻度斜向平铺水印
352
- function drawWatermark(ctx, w, h, text) {
353
- const value = String(text || '').trim()
354
- if (!value) return
355
- const fontSize = Math.max(18, Math.round(w * 0.018))
356
- const stepX = Math.max(fontSize * value.length * 1.6, w / 4)
357
- const stepY = Math.max(fontSize * 6, w / 6)
358
- ctx.save()
359
- ctx.globalAlpha = 0.08
360
- ctx.fillStyle = INK
361
- ctx.font = `${fontSize}px sans-serif`
362
- ctx.textAlign = 'center'
363
- ctx.textBaseline = 'middle'
364
- ctx.translate(w / 2, h / 2)
365
- ctx.rotate(-Math.PI / 9)
366
- const r = Math.hypot(w, h) / 2
367
- for (let y = -r; y <= r; y += stepY) for (let x = -r; x <= r; x += stepX) ctx.fillText(value, x, y)
368
- ctx.restore()
369
- }
370
-
371
- module.exports = {
372
- BOARD,
373
- drawBeads,
374
- drawCodes,
375
- drawGrid,
376
- renderPreview,
377
- patternPageLayout,
378
- drawPatternPage,
379
- buildInfoPages,
380
- drawWatermark,
381
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/pdf.js DELETED
@@ -1,373 +0,0 @@
1
- // 轻量 PDF 生成器(无依赖)
2
- // - 图纸页为纯矢量:珠子、色号、网格都是矢量指令,任意放大都清晰,文件小、生成快
3
- // - 中文(页眉、信息页)以 JPEG 图片嵌入
4
- // - 支持 1-bit 蒙版图片(用于矢量页上的半透明水印)
5
-
6
- const A4 = { w: 595.28, h: 841.89 }
7
-
8
- // Helvetica / Helvetica-Bold 字宽(AFM,1/1000 em),色号只含 A-Z 0-9
9
- const W_BOLD = { ' ': 278, '-': 333, '/': 278 }
10
- const W_REG = { ' ': 278, '-': 333, '/': 278 }
11
- '0123456789'.split('').forEach((c) => { W_BOLD[c] = 556; W_REG[c] = 556 })
12
- const UPPER_BOLD = [722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611]
13
- const UPPER_REG = [667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611]
14
- for (let i = 0; i < 26; i += 1) {
15
- W_BOLD[String.fromCharCode(65 + i)] = UPPER_BOLD[i]
16
- W_REG[String.fromCharCode(65 + i)] = UPPER_REG[i]
17
- }
18
- function textWidth(text, size, bold = true) {
19
- const table = bold ? W_BOLD : W_REG
20
- let w = 0
21
- for (let i = 0; i < text.length; i += 1) w += table[text[i]] || 556
22
- return (w / 1000) * size
23
- }
24
-
25
- const n2 = (v) => (Math.round(v * 100) / 100).toString()
26
- const esc = (s) => String(s).replace(/[\\()]/g, (m) => `\\${m}`).replace(/[^\x20-\x7e]/g, '?')
27
-
28
- function ascii(text) {
29
- const out = new Uint8Array(text.length)
30
- for (let i = 0; i < text.length; i += 1) out[i] = text.charCodeAt(i) & 0xff
31
- return out
32
- }
33
-
34
- class PdfDoc {
35
- constructor() {
36
- this.objects = [null, null, null] // 1: Catalog, 2: Pages(0 号占位)
37
- this.pageIds = []
38
- this.fontRegular = this.add(['<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'])
39
- this.fontBold = this.add(['<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>'])
40
- this.forms = {}
41
- this.gstates = {}
42
- }
43
-
44
- add(parts) {
45
- this.objects.push(parts)
46
- return this.objects.length - 1
47
- }
48
-
49
- stream(dict, data) {
50
- const bytes = typeof data === 'string' ? ascii(data) : new Uint8Array(data)
51
- return this.add([`<< ${dict} /Length ${bytes.length} >>\nstream\n`, bytes, '\nendstream'])
52
- }
53
-
54
- addJpeg(buffer, width, height) {
55
- return this.stream(`/Type /XObject /Subtype /Image /Width ${width} /Height ${height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode`, buffer)
56
- }
57
-
58
- // bits:每行按字节对齐,0 = 绘制
59
- addImageMask(bits, width, height) {
60
- return this.stream(`/Type /XObject /Subtype /Image /Width ${width} /Height ${height} /ImageMask true /BitsPerComponent 1`, bits)
61
- }
62
-
63
- // 单位圆(直径 1)表单:不设颜色,继承外部填充色
64
- form(kind) {
65
- if (this.forms[kind]) return this.forms[kind]
66
- const k = 0.5523
67
- const circle = (r) => {
68
- const c = r * k
69
- return `${n2(0.5 + r)} 0.5 m ${n2(0.5 + r)} ${n2(0.5 + c)} ${n2(0.5 + c)} ${n2(0.5 + r)} 0.5 ${n2(0.5 + r)} c `
70
- + `${n2(0.5 - c)} ${n2(0.5 + r)} ${n2(0.5 - r)} ${n2(0.5 + c)} ${n2(0.5 - r)} 0.5 c `
71
- + `${n2(0.5 - r)} ${n2(0.5 - c)} ${n2(0.5 - c)} ${n2(0.5 - r)} 0.5 ${n2(0.5 - r)} c `
72
- + `${n2(0.5 + c)} ${n2(0.5 - r)} ${n2(0.5 + r)} ${n2(0.5 - c)} ${n2(0.5 + r)} 0.5 c h `
73
- }
74
- let body
75
- if (kind === 'round') body = `${circle(0.46)}f`
76
- else if (kind === 'hollow') body = `${circle(0.46)}${circle(0.2)}f*`
77
- else body = `${circle(0.14)}f` // peg 空钉位
78
- const id = this.stream('/Type /XObject /Subtype /Form /BBox [0 0 1 1]', body)
79
- this.forms[kind] = id
80
- return id
81
- }
82
-
83
- gstate(alpha) {
84
- const key = String(alpha)
85
- if (!this.gstates[key]) this.gstates[key] = this.add([`<< /Type /ExtGState /ca ${alpha} /CA ${alpha} >>`])
86
- return this.gstates[key]
87
- }
88
-
89
- addPage(content, { images = {} } = {}) {
90
- const contentId = this.stream('', content)
91
- const xobjects = Object.assign({}, images)
92
- Object.keys(this.forms).forEach((k) => { xobjects[`Fm_${k}`] = this.forms[k] })
93
- const xo = Object.keys(xobjects).map((k) => `/${k} ${xobjects[k]} 0 R`).join(' ')
94
- const gs = Object.keys(this.gstates).map((k) => `/GS${k.replace('.', '_')} ${this.gstates[k]} 0 R`).join(' ')
95
- const id = this.add([`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${A4.w} ${A4.h}] /Resources << /ProcSet [/PDF /Text /ImageB /ImageC] /Font << /F1 ${this.fontRegular} 0 R /F2 ${this.fontBold} 0 R >> /XObject << ${xo} >> /ExtGState << ${gs} >> >> /Contents ${contentId} 0 R >>`])
96
- this.pageIds.push(id)
97
- return id
98
- }
99
-
100
- build() {
101
- this.objects[1] = ['<< /Type /Catalog /Pages 2 0 R >>']
102
- this.objects[2] = [`<< /Type /Pages /Kids [${this.pageIds.map((id) => `${id} 0 R`).join(' ')}] /Count ${this.pageIds.length} >>`]
103
- const chunks = []
104
- let pos = 0
105
- const push = (part) => {
106
- const bytes = typeof part === 'string' ? ascii(part) : part
107
- chunks.push(bytes)
108
- pos += bytes.length
109
- }
110
- push('%PDF-1.4\n%\xe2\xe3\xcf\xd3\n')
111
- const offsets = []
112
- for (let id = 1; id < this.objects.length; id += 1) {
113
- offsets[id] = pos
114
- push(`${id} 0 obj\n`)
115
- this.objects[id].forEach(push)
116
- push('\nendobj\n')
117
- }
118
- const xref = pos
119
- let table = `xref\n0 ${this.objects.length}\n0000000000 65535 f \n`
120
- for (let id = 1; id < this.objects.length; id += 1) table += `${String(offsets[id]).padStart(10, '0')} 00000 n \n`
121
- push(table)
122
- push(`trailer\n<< /Size ${this.objects.length} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`)
123
- const out = new Uint8Array(pos)
124
- let cursor = 0
125
- chunks.forEach((c) => { out.set(c, cursor); cursor += c.length })
126
- return out.buffer
127
- }
128
- }
129
-
130
- // —— 矢量图纸 ——
131
- // 使用“左上角原点、y 向下”的坐标系(页面开头统一翻转),文字与图片局部再翻回来
132
-
133
- function hexToPdf(hex) {
134
- const v = parseInt(hex.slice(1), 16)
135
- return `${n2(((v >> 16) & 255) / 255)} ${n2(((v >> 8) & 255) / 255)} ${n2((v & 255) / 255)}`
136
- }
137
-
138
- function lumaOf(hex) {
139
- const v = parseInt(hex.slice(1), 16)
140
- return (((v >> 16) & 255) * 0.299 + ((v >> 8) & 255) * 0.587 + (v & 255) * 0.114) / 255
141
- }
142
-
143
- function vectorSheet(doc, artwork, { x0, y0, cell, region, style, codes, grid }) {
144
- const { size, cells, pal } = artwork
145
- const out = []
146
- out.push(`${hexToPdf('#f1eee7')} rg ${n2(x0)} ${n2(y0)} ${n2(region.cols * cell)} ${n2(region.rows * cell)} re f`)
147
- const groups = new Map()
148
- const empty = []
149
- for (let y = region.row; y < region.row + region.rows; y += 1) {
150
- for (let x = region.col; x < region.col + region.cols; x += 1) {
151
- const c = cells[y * size + x]
152
- const px = x0 + (x - region.col) * cell
153
- const py = y0 + (y - region.row) * cell
154
- if (c < 0) { empty.push([px, py]); continue }
155
- if (!groups.has(c)) groups.set(c, [])
156
- groups.get(c).push([px, py])
157
- }
158
- }
159
- if (empty.length) {
160
- doc.form('peg')
161
- out.push(`${hexToPdf('#d9d3c7')} rg`)
162
- empty.forEach(([x, y]) => out.push(`q ${n2(cell)} 0 0 ${n2(cell)} ${n2(x)} ${n2(y)} cm /Fm_peg Do Q`))
163
- }
164
- if (style !== 'square') doc.form(style)
165
- groups.forEach((list, c) => {
166
- out.push(`${hexToPdf(pal.hexes[c])} rg`)
167
- if (style === 'square') {
168
- // 略微外扩 0.05pt 消除阅读器抗锯齿造成的白缝
169
- list.forEach(([x, y]) => out.push(`${n2(x - 0.05)} ${n2(y - 0.05)} ${n2(cell + 0.1)} ${n2(cell + 0.1)} re`))
170
- out.push('f')
171
- } else {
172
- list.forEach(([x, y]) => out.push(`q ${n2(cell)} 0 0 ${n2(cell)} ${n2(x)} ${n2(y)} cm /Fm_${style} Do Q`))
173
- }
174
- })
175
- if (codes) {
176
- const fs = cell * (style === 'hollow' ? 0.27 : 0.34)
177
- const writeGroup = (ink) => {
178
- const lines = []
179
- groups.forEach((list, c) => {
180
- const useInk = style === 'hollow' || lumaOf(pal.hexes[c]) > 0.56
181
- if (useInk !== ink) return
182
- const id = pal.ids[c]
183
- const tw = textWidth(id, fs)
184
- list.forEach(([x, y]) => lines.push(`1 0 0 -1 ${n2(x + (cell - tw) / 2)} ${n2(y + cell / 2 + fs * 0.35)} Tm (${id}) Tj`))
185
- })
186
- if (!lines.length) return
187
- out.push(`${ink ? hexToPdf('#1d2b2a') : '1 1 1'} rg BT /F2 ${n2(fs)} Tf`)
188
- out.push(...lines)
189
- out.push('ET')
190
- }
191
- writeGroup(true)
192
- writeGroup(false)
193
- }
194
- if (grid && grid.enabled) {
195
- const w = region.cols * cell
196
- const h = region.rows * cell
197
- const color = hexToPdf(grid.color)
198
- const alpha = grid.opacity / 100
199
- const thin = []
200
- for (let i = 0; i <= region.cols; i += 1) thin.push(`${n2(x0 + i * cell)} ${n2(y0)} m ${n2(x0 + i * cell)} ${n2(y0 + h)} l`)
201
- for (let i = 0; i <= region.rows; i += 1) thin.push(`${n2(x0)} ${n2(y0 + i * cell)} m ${n2(x0 + w)} ${n2(y0 + i * cell)} l`)
202
- doc.gstate(Math.round(alpha * 30) / 100)
203
- doc.gstate(alpha)
204
- out.push(`q /GS${String(Math.round(alpha * 30) / 100).replace('.', '_')} gs ${color} RG ${n2(Math.max(0.2, cell * 0.02))} w`, ...thin, 'S Q')
205
- const thick = []
206
- for (let i = 0; i <= region.cols; i += 1) {
207
- const g = region.col + i
208
- if (g % grid.interval && i && i !== region.cols) continue
209
- thick.push(`${n2(x0 + i * cell)} ${n2(y0)} m ${n2(x0 + i * cell)} ${n2(y0 + h)} l`)
210
- }
211
- for (let i = 0; i <= region.rows; i += 1) {
212
- const g = region.row + i
213
- if (g % grid.interval && i && i !== region.rows) continue
214
- thick.push(`${n2(x0)} ${n2(y0 + i * cell)} m ${n2(x0 + w)} ${n2(y0 + i * cell)} l`)
215
- }
216
- out.push(`q /GS${String(alpha).replace('.', '_')} gs ${color} RG ${n2(Math.max(0.6, cell * 0.06))} w`, ...thick, 'S Q')
217
- // 坐标数字
218
- const fs = Math.min(7, Math.max(4, cell * 0.42))
219
- const step = cell < 8 ? grid.interval : (cell < 13 ? 5 : 1)
220
- const labels = []
221
- for (let i = 0; i < region.cols; i += 1) {
222
- const g = region.col + i + 1
223
- if (g % step && g !== 1 && i !== 0) continue
224
- const t = String(g)
225
- labels.push(`1 0 0 -1 ${n2(x0 + (i + 0.5) * cell - textWidth(t, fs, false) / 2)} ${n2(y0 - fs * 0.5)} Tm (${t}) Tj`)
226
- }
227
- for (let i = 0; i < region.rows; i += 1) {
228
- const g = region.row + i + 1
229
- if (g % step && g !== 1 && i !== 0) continue
230
- const t = String(g)
231
- labels.push(`1 0 0 -1 ${n2(x0 - textWidth(t, fs, false) - fs * 0.4)} ${n2(y0 + (i + 0.5) * cell + fs * 0.35)} Tm (${t}) Tj`)
232
- }
233
- out.push(`${color} rg BT /F1 ${n2(fs)} Tf`, ...labels, 'ET')
234
- }
235
- return out.join('\n')
236
- }
237
-
238
- // 页眉图片 + 水印蒙版的放置
239
- function imageCmd(name, x, y, w, h) {
240
- return `q ${n2(w)} 0 0 ${n2(-h)} ${n2(x)} ${n2(y + h)} cm /${name} Do Q`
241
- }
242
-
243
- function watermarkCmd(mask) {
244
- if (!mask) return ''
245
- const h = 16
246
- const w = h * mask.width / mask.height
247
- const out = [`q /GS0_08 gs ${hexToPdf('#1d2b2a')} rg`]
248
- const stepX = w + 90
249
- const stepY = 120
250
- for (let row = 0, y = -200; y < A4.h + 200; y += stepY, row += 1) {
251
- for (let x = -200; x < A4.w + 200; x += stepX) {
252
- // 旋转 -20°:cos=0.94 sin=0.342;y 轴已翻转,所以蒙版局部再翻回
253
- out.push(`q 0.94 -0.342 0.342 0.94 ${n2(x + (row % 2) * stepX / 2)} ${n2(y)} cm ${n2(w)} 0 0 ${n2(-h)} 0 ${n2(h)} cm /WM Do Q`)
254
- }
255
- }
256
- out.push('Q')
257
- return out.join('\n')
258
- }
259
-
260
- const MARGIN = 28.35 // 10mm
261
- const HEADER_H = 34
262
-
263
- const AXIS = 14
264
- const TARGET_CELL = 18 // 约 6.4mm/格,打印后色号清晰可读
265
- const AREA_W = A4.w - MARGIN * 2
266
- const AREA_H = A4.h - MARGIN * 2 - HEADER_H
267
-
268
- /**
269
- * 规划图纸页:小图一页搞定;大图 = 总览页 + 若干分块页
270
- * 返回 { tiled, fitCell, tilesX, tilesY, tileCols, tileRows, pages: [{ kind, tile, region }] }
271
- */
272
- function planPatternPages(size) {
273
- const fitCell = Math.min((AREA_W - AXIS) / size, (AREA_H - AXIS) / size)
274
- const tiled = fitCell < 11
275
- const full = { col: 0, row: 0, cols: size, rows: size }
276
- const pages = [{ kind: 'overview', tile: 0, region: full }]
277
- let tilesX = 1
278
- let tilesY = 1
279
- let tileCols = size
280
- let tileRows = size
281
- if (tiled) {
282
- tilesX = Math.ceil(size / Math.floor((AREA_W - AXIS) / TARGET_CELL))
283
- tilesY = Math.ceil(size / Math.floor((AREA_H - AXIS) / TARGET_CELL))
284
- tileCols = Math.ceil(size / tilesX)
285
- tileRows = Math.ceil(size / tilesY)
286
- let n = 1
287
- for (let ty = 0; ty < tilesY; ty += 1) {
288
- for (let tx = 0; tx < tilesX; tx += 1) {
289
- const col = tx * tileCols
290
- const row = ty * tileRows
291
- if (col >= size || row >= size) continue
292
- pages.push({ kind: 'tile', tile: n, region: { col, row, cols: Math.min(tileCols, size - col), rows: Math.min(tileRows, size - row) } })
293
- n += 1
294
- }
295
- }
296
- }
297
- return { tiled, fitCell, tilesX, tilesY, tileCols, tileRows, pages }
298
- }
299
-
300
- /**
301
- * 生成图纸页(矢量)。
302
- * opts: { plan, headers: [{ id, width, height }](按页序), style, grid, watermark }
303
- */
304
- function addPatternPages(doc, artwork, opts) {
305
- const size = artwork.size
306
- const plan = opts.plan || planPatternPages(size)
307
- const headers = opts.headers || []
308
- const flip = `1 0 0 -1 0 ${A4.h} cm`
309
- const wm = opts.watermark ? watermarkCmd(opts.watermark) : ''
310
- if (opts.watermark) doc.gstate(0.08)
311
- if (plan.tiled) doc.gstate(0.85)
312
- const images = (header) => {
313
- const map = {}
314
- if (header) map.HD = header.id
315
- if (opts.watermark) map.WM = opts.watermark.id
316
- return map
317
- }
318
- const headerCmd = (header) => (header ? imageCmd('HD', MARGIN, MARGIN, AREA_W, AREA_W * header.height / header.width) : '')
319
-
320
- plan.pages.forEach((page, i) => {
321
- const header = headers[i] || null
322
- const content = [flip, headerCmd(header)]
323
- if (page.kind === 'overview') {
324
- const fitCell = plan.fitCell
325
- const gridW = fitCell * size
326
- const x0 = MARGIN + AXIS + (AREA_W - AXIS - gridW) / 2
327
- const y0 = MARGIN + HEADER_H + AXIS
328
- content.push(vectorSheet(doc, artwork, {
329
- x0, y0, cell: fitCell, region: page.region, style: opts.style, codes: !plan.tiled, grid: opts.grid,
330
- }))
331
- if (plan.tiled) {
332
- const lines = [`q ${hexToPdf('#e11d48')} RG 1.2 w`]
333
- const labels = []
334
- plan.pages.forEach((tp) => {
335
- if (tp.kind !== 'tile') return
336
- const r = tp.region
337
- const cx = x0 + r.col * fitCell
338
- const cy = y0 + r.row * fitCell
339
- const cw = r.cols * fitCell
340
- const ch = r.rows * fitCell
341
- lines.push(`${n2(cx)} ${n2(cy)} ${n2(cw)} ${n2(ch)} re S`)
342
- const t = String(tp.tile)
343
- labels.push(`1 0 0 -1 ${n2(cx + cw / 2 - textWidth(t, 22) / 2)} ${n2(cy + ch / 2 + 8)} Tm (${t}) Tj`)
344
- })
345
- lines.push('Q')
346
- content.push(lines.join('\n'))
347
- content.push(`q /GS0_85 gs ${hexToPdf('#e11d48')} rg BT /F2 22 Tf\n${labels.join('\n')}\nET Q`)
348
- }
349
- } else {
350
- const cell = Math.min((AREA_W - AXIS) / plan.tileCols, (AREA_H - AXIS) / plan.tileRows)
351
- content.push(vectorSheet(doc, artwork, {
352
- x0: MARGIN + AXIS, y0: MARGIN + HEADER_H + AXIS, cell, region: page.region, style: opts.style, codes: true, grid: opts.grid,
353
- }))
354
- }
355
- content.push(wm)
356
- doc.addPage(content.join('\n'), { images: images(header) })
357
- })
358
- return plan.pages.length
359
- }
360
-
361
- // 整页图片(信息页)
362
- function addImagePage(doc, jpegId, pixelW, pixelH) {
363
- const areaW = A4.w - MARGIN * 2
364
- const areaH = A4.h - MARGIN * 2
365
- const scale = Math.min(areaW / pixelW, areaH / pixelH)
366
- const w = pixelW * scale
367
- const h = pixelH * scale
368
- const x = (A4.w - w) / 2
369
- const y = (A4.h - h) / 2
370
- doc.addPage(`1 0 0 -1 0 ${A4.h} cm\n${imageCmd('IM', x, y, w, h)}`, { images: { IM: jpegId } })
371
- }
372
-
373
- module.exports = { A4, PdfDoc, planPatternPages, addPatternPages, addImagePage, vectorSheet, textWidth, HEADER_H, MARGIN }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qr.js DELETED
@@ -1,58 +0,0 @@
1
- // 二维码生成、功能区识别与摆放
2
- const QRCodeCore = require('./qrcore/qrcode')
3
- const AlignmentPattern = require('./qrcore/alignment-pattern')
4
-
5
- const SAMPLE_TEXT = '豆维·可扫码的拼豆'
6
-
7
- // 功能区(定位/时序/对齐/格式/版本)直接决定能否扫出,配色时强制高对比
8
- function buildFunctionMask(size, version) {
9
- const mask = new Uint8Array(size * size)
10
- const mark = (x, y) => { if (x >= 0 && y >= 0 && x < size && y < size) mask[y * size + x] = 1 }
11
- for (let y = 0; y < size; y += 1) {
12
- for (let x = 0; x < size; x += 1) {
13
- const finder = (x <= 8 && y <= 8) || (x >= size - 8 && y <= 8) || (x <= 8 && y >= size - 8)
14
- if (finder || x === 6 || y === 6) mark(x, y)
15
- }
16
- }
17
- const positions = AlignmentPattern.getPositions(version) || []
18
- positions.forEach(([row, col]) => {
19
- for (let dy = -2; dy <= 2; dy += 1) for (let dx = -2; dx <= 2; dx += 1) mark(col + dx, row + dy)
20
- })
21
- if (version >= 7) {
22
- for (let i = 0; i < 6; i += 1) {
23
- for (let j = size - 11; j < size - 8; j += 1) { mark(i, j); mark(j, i) }
24
- }
25
- }
26
- return mask
27
- }
28
-
29
- function makeQr(text, level) {
30
- const value = String(text || '').trim() || SAMPLE_TEXT
31
- const qr = QRCodeCore.create(value, { errorCorrectionLevel: level || 'H' })
32
- const size = qr.modules.size
33
- const version = qr.version || Math.round((size - 17) / 4)
34
- return {
35
- size,
36
- version,
37
- level: level || 'H',
38
- dark: Uint8Array.from(qr.modules.data),
39
- func: buildFunctionMask(size, version),
40
- isSample: !String(text || '').trim(),
41
- }
42
- }
43
-
44
- // scale:二维码占整张作品边长的百分比;quiet:二维码四周留白格数
45
- function createPlacement(qrSize, { scale = 100, posX = 50, posY = 50, quiet = 0, hasImage = true } = {}) {
46
- const block = qrSize + quiet * 2
47
- const size = hasImage ? Math.max(block, Math.ceil(block * 100 / scale)) : block
48
- const free = size - block
49
- return {
50
- size,
51
- left: quiet + Math.round(free * posX / 100),
52
- top: quiet + Math.round(free * posY / 100),
53
- quiet,
54
- scale: hasImage ? scale : 100,
55
- }
56
- }
57
-
58
- module.exports = { SAMPLE_TEXT, makeQr, createPlacement }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/alignment-pattern.js DELETED
@@ -1,83 +0,0 @@
1
- /**
2
- * Alignment pattern are fixed reference pattern in defined positions
3
- * in a matrix symbology, which enables the decode software to re-synchronise
4
- * the coordinate mapping of the image modules in the event of moderate amounts
5
- * of distortion of the image.
6
- *
7
- * Alignment patterns are present only in QR Code symbols of version 2 or larger
8
- * and their number depends on the symbol version.
9
- */
10
-
11
- const getSymbolSize = require('./utils').getSymbolSize
12
-
13
- /**
14
- * Calculate the row/column coordinates of the center module of each alignment pattern
15
- * for the specified QR Code version.
16
- *
17
- * The alignment patterns are positioned symmetrically on either side of the diagonal
18
- * running from the top left corner of the symbol to the bottom right corner.
19
- *
20
- * Since positions are simmetrical only half of the coordinates are returned.
21
- * Each item of the array will represent in turn the x and y coordinate.
22
- * @see {@link getPositions}
23
- *
24
- * @param {Number} version QR Code version
25
- * @return {Array} Array of coordinate
26
- */
27
- exports.getRowColCoords = function getRowColCoords (version) {
28
- if (version === 1) return []
29
-
30
- const posCount = Math.floor(version / 7) + 2
31
- const size = getSymbolSize(version)
32
- const intervals = size === 145 ? 26 : Math.ceil((size - 13) / (2 * posCount - 2)) * 2
33
- const positions = [size - 7] // Last coord is always (size - 7)
34
-
35
- for (let i = 1; i < posCount - 1; i++) {
36
- positions[i] = positions[i - 1] - intervals
37
- }
38
-
39
- positions.push(6) // First coord is always 6
40
-
41
- return positions.reverse()
42
- }
43
-
44
- /**
45
- * Returns an array containing the positions of each alignment pattern.
46
- * Each array's element represent the center point of the pattern as (x, y) coordinates
47
- *
48
- * Coordinates are calculated expanding the row/column coordinates returned by {@link getRowColCoords}
49
- * and filtering out the items that overlaps with finder pattern
50
- *
51
- * @example
52
- * For a Version 7 symbol {@link getRowColCoords} returns values 6, 22 and 38.
53
- * The alignment patterns, therefore, are to be centered on (row, column)
54
- * positions (6,22), (22,6), (22,22), (22,38), (38,22), (38,38).
55
- * Note that the coordinates (6,6), (6,38), (38,6) are occupied by finder patterns
56
- * and are not therefore used for alignment patterns.
57
- *
58
- * let pos = getPositions(7)
59
- * // [[6,22], [22,6], [22,22], [22,38], [38,22], [38,38]]
60
- *
61
- * @param {Number} version QR Code version
62
- * @return {Array} Array of coordinates
63
- */
64
- exports.getPositions = function getPositions (version) {
65
- const coords = []
66
- const pos = exports.getRowColCoords(version)
67
- const posLength = pos.length
68
-
69
- for (let i = 0; i < posLength; i++) {
70
- for (let j = 0; j < posLength; j++) {
71
- // Skip if position is occupied by finder patterns
72
- if ((i === 0 && j === 0) || // top-left
73
- (i === 0 && j === posLength - 1) || // bottom-left
74
- (i === posLength - 1 && j === 0)) { // top-right
75
- continue
76
- }
77
-
78
- coords.push([pos[i], pos[j]])
79
- }
80
- }
81
-
82
- return coords
83
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/alphanumeric-data.js DELETED
@@ -1,59 +0,0 @@
1
- const Mode = require('./mode')
2
-
3
- /**
4
- * Array of characters available in alphanumeric mode
5
- *
6
- * As per QR Code specification, to each character
7
- * is assigned a value from 0 to 44 which in this case coincides
8
- * with the array index
9
- *
10
- * @type {Array}
11
- */
12
- const ALPHA_NUM_CHARS = [
13
- '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
14
- 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
15
- 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
16
- ' ', '$', '%', '*', '+', '-', '.', '/', ':'
17
- ]
18
-
19
- function AlphanumericData (data) {
20
- this.mode = Mode.ALPHANUMERIC
21
- this.data = data
22
- }
23
-
24
- AlphanumericData.getBitsLength = function getBitsLength (length) {
25
- return 11 * Math.floor(length / 2) + 6 * (length % 2)
26
- }
27
-
28
- AlphanumericData.prototype.getLength = function getLength () {
29
- return this.data.length
30
- }
31
-
32
- AlphanumericData.prototype.getBitsLength = function getBitsLength () {
33
- return AlphanumericData.getBitsLength(this.data.length)
34
- }
35
-
36
- AlphanumericData.prototype.write = function write (bitBuffer) {
37
- let i
38
-
39
- // Input data characters are divided into groups of two characters
40
- // and encoded as 11-bit binary codes.
41
- for (i = 0; i + 2 <= this.data.length; i += 2) {
42
- // The character value of the first character is multiplied by 45
43
- let value = ALPHA_NUM_CHARS.indexOf(this.data[i]) * 45
44
-
45
- // The character value of the second digit is added to the product
46
- value += ALPHA_NUM_CHARS.indexOf(this.data[i + 1])
47
-
48
- // The sum is then stored as 11-bit binary number
49
- bitBuffer.put(value, 11)
50
- }
51
-
52
- // If the number of input data characters is not a multiple of two,
53
- // the character value of the final character is encoded as a 6-bit binary number.
54
- if (this.data.length % 2) {
55
- bitBuffer.put(ALPHA_NUM_CHARS.indexOf(this.data[i]), 6)
56
- }
57
- }
58
-
59
- module.exports = AlphanumericData
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/bit-buffer.js DELETED
@@ -1,37 +0,0 @@
1
- function BitBuffer () {
2
- this.buffer = []
3
- this.length = 0
4
- }
5
-
6
- BitBuffer.prototype = {
7
-
8
- get: function (index) {
9
- const bufIndex = Math.floor(index / 8)
10
- return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) === 1
11
- },
12
-
13
- put: function (num, length) {
14
- for (let i = 0; i < length; i++) {
15
- this.putBit(((num >>> (length - i - 1)) & 1) === 1)
16
- }
17
- },
18
-
19
- getLengthInBits: function () {
20
- return this.length
21
- },
22
-
23
- putBit: function (bit) {
24
- const bufIndex = Math.floor(this.length / 8)
25
- if (this.buffer.length <= bufIndex) {
26
- this.buffer.push(0)
27
- }
28
-
29
- if (bit) {
30
- this.buffer[bufIndex] |= (0x80 >>> (this.length % 8))
31
- }
32
-
33
- this.length++
34
- }
35
- }
36
-
37
- module.exports = BitBuffer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/bit-matrix.js DELETED
@@ -1,65 +0,0 @@
1
- /**
2
- * Helper class to handle QR Code symbol modules
3
- *
4
- * @param {Number} size Symbol size
5
- */
6
- function BitMatrix (size) {
7
- if (!size || size < 1) {
8
- throw new Error('BitMatrix size must be defined and greater than 0')
9
- }
10
-
11
- this.size = size
12
- this.data = new Uint8Array(size * size)
13
- this.reservedBit = new Uint8Array(size * size)
14
- }
15
-
16
- /**
17
- * Set bit value at specified location
18
- * If reserved flag is set, this bit will be ignored during masking process
19
- *
20
- * @param {Number} row
21
- * @param {Number} col
22
- * @param {Boolean} value
23
- * @param {Boolean} reserved
24
- */
25
- BitMatrix.prototype.set = function (row, col, value, reserved) {
26
- const index = row * this.size + col
27
- this.data[index] = value
28
- if (reserved) this.reservedBit[index] = true
29
- }
30
-
31
- /**
32
- * Returns bit value at specified location
33
- *
34
- * @param {Number} row
35
- * @param {Number} col
36
- * @return {Boolean}
37
- */
38
- BitMatrix.prototype.get = function (row, col) {
39
- return this.data[row * this.size + col]
40
- }
41
-
42
- /**
43
- * Applies xor operator at specified location
44
- * (used during masking process)
45
- *
46
- * @param {Number} row
47
- * @param {Number} col
48
- * @param {Boolean} value
49
- */
50
- BitMatrix.prototype.xor = function (row, col, value) {
51
- this.data[row * this.size + col] ^= value
52
- }
53
-
54
- /**
55
- * Check if bit at specified location is reserved
56
- *
57
- * @param {Number} row
58
- * @param {Number} col
59
- * @return {Boolean}
60
- */
61
- BitMatrix.prototype.isReserved = function (row, col) {
62
- return this.reservedBit[row * this.size + col]
63
- }
64
-
65
- module.exports = BitMatrix
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/byte-data.js DELETED
@@ -1,61 +0,0 @@
1
- const Mode = require('./mode')
2
-
3
- // 微信部分真机运行时没有 TextEncoder。二维码 BYTE 段只需要 UTF-8 字节,
4
- // 在此实现最小兼容编码器,且与 TextEncoder 对无效代理对的处理一致。
5
- function encodeUtf8 (value) {
6
- const bytes = []
7
- for (let index = 0; index < value.length; index += 1) {
8
- let codePoint = value.charCodeAt(index)
9
- if (codePoint >= 0xd800 && codePoint <= 0xdbff) {
10
- const next = value.charCodeAt(index + 1)
11
- if (next >= 0xdc00 && next <= 0xdfff) {
12
- codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00)
13
- index += 1
14
- } else {
15
- codePoint = 0xfffd
16
- }
17
- } else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) {
18
- codePoint = 0xfffd
19
- }
20
-
21
- if (codePoint <= 0x7f) {
22
- bytes.push(codePoint)
23
- } else if (codePoint <= 0x7ff) {
24
- bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f))
25
- } else if (codePoint <= 0xffff) {
26
- bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
27
- } else {
28
- bytes.push(0xf0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3f), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
29
- }
30
- }
31
- return new Uint8Array(bytes)
32
- }
33
-
34
- function ByteData (data) {
35
- this.mode = Mode.BYTE
36
- if (typeof (data) === 'string') {
37
- this.data = encodeUtf8(data)
38
- } else {
39
- this.data = new Uint8Array(data)
40
- }
41
- }
42
-
43
- ByteData.getBitsLength = function getBitsLength (length) {
44
- return length * 8
45
- }
46
-
47
- ByteData.prototype.getLength = function getLength () {
48
- return this.data.length
49
- }
50
-
51
- ByteData.prototype.getBitsLength = function getBitsLength () {
52
- return ByteData.getBitsLength(this.data.length)
53
- }
54
-
55
- ByteData.prototype.write = function (bitBuffer) {
56
- for (let i = 0, l = this.data.length; i < l; i++) {
57
- bitBuffer.put(this.data[i], 8)
58
- }
59
- }
60
-
61
- module.exports = ByteData
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/dijkstrajs.js DELETED
@@ -1,165 +0,0 @@
1
- 'use strict';
2
-
3
- /******************************************************************************
4
- * Created 2008-08-19.
5
- *
6
- * Dijkstra path-finding functions. Adapted from the Dijkstar Python project.
7
- *
8
- * Copyright (C) 2008
9
- * Wyatt Baldwin <self@wyattbaldwin.com>
10
- * All rights reserved
11
- *
12
- * Licensed under the MIT license.
13
- *
14
- * http://www.opensource.org/licenses/mit-license.php
15
- *
16
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
- * THE SOFTWARE.
23
- *****************************************************************************/
24
- var dijkstra = {
25
- single_source_shortest_paths: function(graph, s, d) {
26
- // Predecessor map for each node that has been encountered.
27
- // node ID => predecessor node ID
28
- var predecessors = {};
29
-
30
- // Costs of shortest paths from s to all nodes encountered.
31
- // node ID => cost
32
- var costs = {};
33
- costs[s] = 0;
34
-
35
- // Costs of shortest paths from s to all nodes encountered; differs from
36
- // `costs` in that it provides easy access to the node that currently has
37
- // the known shortest path from s.
38
- // XXX: Do we actually need both `costs` and `open`?
39
- var open = dijkstra.PriorityQueue.make();
40
- open.push(s, 0);
41
-
42
- var closest,
43
- u, v,
44
- cost_of_s_to_u,
45
- adjacent_nodes,
46
- cost_of_e,
47
- cost_of_s_to_u_plus_cost_of_e,
48
- cost_of_s_to_v,
49
- first_visit;
50
- while (!open.empty()) {
51
- // In the nodes remaining in graph that have a known cost from s,
52
- // find the node, u, that currently has the shortest path from s.
53
- closest = open.pop();
54
- u = closest.value;
55
- cost_of_s_to_u = closest.cost;
56
-
57
- // Get nodes adjacent to u...
58
- adjacent_nodes = graph[u] || {};
59
-
60
- // ...and explore the edges that connect u to those nodes, updating
61
- // the cost of the shortest paths to any or all of those nodes as
62
- // necessary. v is the node across the current edge from u.
63
- for (v in adjacent_nodes) {
64
- if (adjacent_nodes.hasOwnProperty(v)) {
65
- // Get the cost of the edge running from u to v.
66
- cost_of_e = adjacent_nodes[v];
67
-
68
- // Cost of s to u plus the cost of u to v across e--this is *a*
69
- // cost from s to v that may or may not be less than the current
70
- // known cost to v.
71
- cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e;
72
-
73
- // If we haven't visited v yet OR if the current known cost from s to
74
- // v is greater than the new cost we just found (cost of s to u plus
75
- // cost of u to v across e), update v's cost in the cost list and
76
- // update v's predecessor in the predecessor list (it's now u).
77
- cost_of_s_to_v = costs[v];
78
- first_visit = (typeof costs[v] === 'undefined');
79
- if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
80
- costs[v] = cost_of_s_to_u_plus_cost_of_e;
81
- open.push(v, cost_of_s_to_u_plus_cost_of_e);
82
- predecessors[v] = u;
83
- }
84
- }
85
- }
86
- }
87
-
88
- if (typeof d !== 'undefined' && typeof costs[d] === 'undefined') {
89
- var msg = ['Could not find a path from ', s, ' to ', d, '.'].join('');
90
- throw new Error(msg);
91
- }
92
-
93
- return predecessors;
94
- },
95
-
96
- extract_shortest_path_from_predecessor_list: function(predecessors, d) {
97
- var nodes = [];
98
- var u = d;
99
- var predecessor;
100
- while (u) {
101
- nodes.push(u);
102
- predecessor = predecessors[u];
103
- u = predecessors[u];
104
- }
105
- nodes.reverse();
106
- return nodes;
107
- },
108
-
109
- find_path: function(graph, s, d) {
110
- var predecessors = dijkstra.single_source_shortest_paths(graph, s, d);
111
- return dijkstra.extract_shortest_path_from_predecessor_list(
112
- predecessors, d);
113
- },
114
-
115
- /**
116
- * A very naive priority queue implementation.
117
- */
118
- PriorityQueue: {
119
- make: function (opts) {
120
- var T = dijkstra.PriorityQueue,
121
- t = {},
122
- key;
123
- opts = opts || {};
124
- for (key in T) {
125
- if (T.hasOwnProperty(key)) {
126
- t[key] = T[key];
127
- }
128
- }
129
- t.queue = [];
130
- t.sorter = opts.sorter || T.default_sorter;
131
- return t;
132
- },
133
-
134
- default_sorter: function (a, b) {
135
- return a.cost - b.cost;
136
- },
137
-
138
- /**
139
- * Add a new item to the queue and ensure the highest priority element
140
- * is at the front of the queue.
141
- */
142
- push: function (value, cost) {
143
- var item = {value: value, cost: cost};
144
- this.queue.push(item);
145
- this.queue.sort(this.sorter);
146
- },
147
-
148
- /**
149
- * Return the highest priority element in the queue.
150
- */
151
- pop: function () {
152
- return this.queue.shift();
153
- },
154
-
155
- empty: function () {
156
- return this.queue.length === 0;
157
- }
158
- }
159
- };
160
-
161
-
162
- // node.js module exports
163
- if (typeof module !== 'undefined') {
164
- module.exports = dijkstra;
165
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/error-correction-code.js DELETED
@@ -1,135 +0,0 @@
1
- const ECLevel = require('./error-correction-level')
2
-
3
- const EC_BLOCKS_TABLE = [
4
- // L M Q H
5
- 1, 1, 1, 1,
6
- 1, 1, 1, 1,
7
- 1, 1, 2, 2,
8
- 1, 2, 2, 4,
9
- 1, 2, 4, 4,
10
- 2, 4, 4, 4,
11
- 2, 4, 6, 5,
12
- 2, 4, 6, 6,
13
- 2, 5, 8, 8,
14
- 4, 5, 8, 8,
15
- 4, 5, 8, 11,
16
- 4, 8, 10, 11,
17
- 4, 9, 12, 16,
18
- 4, 9, 16, 16,
19
- 6, 10, 12, 18,
20
- 6, 10, 17, 16,
21
- 6, 11, 16, 19,
22
- 6, 13, 18, 21,
23
- 7, 14, 21, 25,
24
- 8, 16, 20, 25,
25
- 8, 17, 23, 25,
26
- 9, 17, 23, 34,
27
- 9, 18, 25, 30,
28
- 10, 20, 27, 32,
29
- 12, 21, 29, 35,
30
- 12, 23, 34, 37,
31
- 12, 25, 34, 40,
32
- 13, 26, 35, 42,
33
- 14, 28, 38, 45,
34
- 15, 29, 40, 48,
35
- 16, 31, 43, 51,
36
- 17, 33, 45, 54,
37
- 18, 35, 48, 57,
38
- 19, 37, 51, 60,
39
- 19, 38, 53, 63,
40
- 20, 40, 56, 66,
41
- 21, 43, 59, 70,
42
- 22, 45, 62, 74,
43
- 24, 47, 65, 77,
44
- 25, 49, 68, 81
45
- ]
46
-
47
- const EC_CODEWORDS_TABLE = [
48
- // L M Q H
49
- 7, 10, 13, 17,
50
- 10, 16, 22, 28,
51
- 15, 26, 36, 44,
52
- 20, 36, 52, 64,
53
- 26, 48, 72, 88,
54
- 36, 64, 96, 112,
55
- 40, 72, 108, 130,
56
- 48, 88, 132, 156,
57
- 60, 110, 160, 192,
58
- 72, 130, 192, 224,
59
- 80, 150, 224, 264,
60
- 96, 176, 260, 308,
61
- 104, 198, 288, 352,
62
- 120, 216, 320, 384,
63
- 132, 240, 360, 432,
64
- 144, 280, 408, 480,
65
- 168, 308, 448, 532,
66
- 180, 338, 504, 588,
67
- 196, 364, 546, 650,
68
- 224, 416, 600, 700,
69
- 224, 442, 644, 750,
70
- 252, 476, 690, 816,
71
- 270, 504, 750, 900,
72
- 300, 560, 810, 960,
73
- 312, 588, 870, 1050,
74
- 336, 644, 952, 1110,
75
- 360, 700, 1020, 1200,
76
- 390, 728, 1050, 1260,
77
- 420, 784, 1140, 1350,
78
- 450, 812, 1200, 1440,
79
- 480, 868, 1290, 1530,
80
- 510, 924, 1350, 1620,
81
- 540, 980, 1440, 1710,
82
- 570, 1036, 1530, 1800,
83
- 570, 1064, 1590, 1890,
84
- 600, 1120, 1680, 1980,
85
- 630, 1204, 1770, 2100,
86
- 660, 1260, 1860, 2220,
87
- 720, 1316, 1950, 2310,
88
- 750, 1372, 2040, 2430
89
- ]
90
-
91
- /**
92
- * Returns the number of error correction block that the QR Code should contain
93
- * for the specified version and error correction level.
94
- *
95
- * @param {Number} version QR Code version
96
- * @param {Number} errorCorrectionLevel Error correction level
97
- * @return {Number} Number of error correction blocks
98
- */
99
- exports.getBlocksCount = function getBlocksCount (version, errorCorrectionLevel) {
100
- switch (errorCorrectionLevel) {
101
- case ECLevel.L:
102
- return EC_BLOCKS_TABLE[(version - 1) * 4 + 0]
103
- case ECLevel.M:
104
- return EC_BLOCKS_TABLE[(version - 1) * 4 + 1]
105
- case ECLevel.Q:
106
- return EC_BLOCKS_TABLE[(version - 1) * 4 + 2]
107
- case ECLevel.H:
108
- return EC_BLOCKS_TABLE[(version - 1) * 4 + 3]
109
- default:
110
- return undefined
111
- }
112
- }
113
-
114
- /**
115
- * Returns the number of error correction codewords to use for the specified
116
- * version and error correction level.
117
- *
118
- * @param {Number} version QR Code version
119
- * @param {Number} errorCorrectionLevel Error correction level
120
- * @return {Number} Number of error correction codewords
121
- */
122
- exports.getTotalCodewordsCount = function getTotalCodewordsCount (version, errorCorrectionLevel) {
123
- switch (errorCorrectionLevel) {
124
- case ECLevel.L:
125
- return EC_CODEWORDS_TABLE[(version - 1) * 4 + 0]
126
- case ECLevel.M:
127
- return EC_CODEWORDS_TABLE[(version - 1) * 4 + 1]
128
- case ECLevel.Q:
129
- return EC_CODEWORDS_TABLE[(version - 1) * 4 + 2]
130
- case ECLevel.H:
131
- return EC_CODEWORDS_TABLE[(version - 1) * 4 + 3]
132
- default:
133
- return undefined
134
- }
135
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/error-correction-level.js DELETED
@@ -1,50 +0,0 @@
1
- exports.L = { bit: 1 }
2
- exports.M = { bit: 0 }
3
- exports.Q = { bit: 3 }
4
- exports.H = { bit: 2 }
5
-
6
- function fromString (string) {
7
- if (typeof string !== 'string') {
8
- throw new Error('Param is not a string')
9
- }
10
-
11
- const lcStr = string.toLowerCase()
12
-
13
- switch (lcStr) {
14
- case 'l':
15
- case 'low':
16
- return exports.L
17
-
18
- case 'm':
19
- case 'medium':
20
- return exports.M
21
-
22
- case 'q':
23
- case 'quartile':
24
- return exports.Q
25
-
26
- case 'h':
27
- case 'high':
28
- return exports.H
29
-
30
- default:
31
- throw new Error('Unknown EC Level: ' + string)
32
- }
33
- }
34
-
35
- exports.isValid = function isValid (level) {
36
- return level && typeof level.bit !== 'undefined' &&
37
- level.bit >= 0 && level.bit < 4
38
- }
39
-
40
- exports.from = function from (value, defaultValue) {
41
- if (exports.isValid(value)) {
42
- return value
43
- }
44
-
45
- try {
46
- return fromString(value)
47
- } catch (e) {
48
- return defaultValue
49
- }
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/finder-pattern.js DELETED
@@ -1,22 +0,0 @@
1
- const getSymbolSize = require('./utils').getSymbolSize
2
- const FINDER_PATTERN_SIZE = 7
3
-
4
- /**
5
- * Returns an array containing the positions of each finder pattern.
6
- * Each array's element represent the top-left point of the pattern as (x, y) coordinates
7
- *
8
- * @param {Number} version QR Code version
9
- * @return {Array} Array of coordinates
10
- */
11
- exports.getPositions = function getPositions (version) {
12
- const size = getSymbolSize(version)
13
-
14
- return [
15
- // top-left
16
- [0, 0],
17
- // top-right
18
- [size - FINDER_PATTERN_SIZE, 0],
19
- // bottom-left
20
- [0, size - FINDER_PATTERN_SIZE]
21
- ]
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/format-info.js DELETED
@@ -1,29 +0,0 @@
1
- const Utils = require('./utils')
2
-
3
- const G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)
4
- const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
5
- const G15_BCH = Utils.getBCHDigit(G15)
6
-
7
- /**
8
- * Returns format information with relative error correction bits
9
- *
10
- * The format information is a 15-bit sequence containing 5 data bits,
11
- * with 10 error correction bits calculated using the (15, 5) BCH code.
12
- *
13
- * @param {Number} errorCorrectionLevel Error correction level
14
- * @param {Number} mask Mask pattern
15
- * @return {Number} Encoded format information bits
16
- */
17
- exports.getEncodedBits = function getEncodedBits (errorCorrectionLevel, mask) {
18
- const data = ((errorCorrectionLevel.bit << 3) | mask)
19
- let d = data << 10
20
-
21
- while (Utils.getBCHDigit(d) - G15_BCH >= 0) {
22
- d ^= (G15 << (Utils.getBCHDigit(d) - G15_BCH))
23
- }
24
-
25
- // xor final data with mask pattern in order to ensure that
26
- // no combination of Error Correction Level and data mask pattern
27
- // will result in an all-zero data string
28
- return ((data << 10) | d) ^ G15_MASK
29
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/galois-field.js DELETED
@@ -1,69 +0,0 @@
1
- const EXP_TABLE = new Uint8Array(512)
2
- const LOG_TABLE = new Uint8Array(256)
3
- /**
4
- * Precompute the log and anti-log tables for faster computation later
5
- *
6
- * For each possible value in the galois field 2^8, we will pre-compute
7
- * the logarithm and anti-logarithm (exponential) of this value
8
- *
9
- * ref {@link https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Introduction_to_mathematical_fields}
10
- */
11
- ;(function initTables () {
12
- let x = 1
13
- for (let i = 0; i < 255; i++) {
14
- EXP_TABLE[i] = x
15
- LOG_TABLE[x] = i
16
-
17
- x <<= 1 // multiply by 2
18
-
19
- // The QR code specification says to use byte-wise modulo 100011101 arithmetic.
20
- // This means that when a number is 256 or larger, it should be XORed with 0x11D.
21
- if (x & 0x100) { // similar to x >= 256, but a lot faster (because 0x100 == 256)
22
- x ^= 0x11D
23
- }
24
- }
25
-
26
- // Optimization: double the size of the anti-log table so that we don't need to mod 255 to
27
- // stay inside the bounds (because we will mainly use this table for the multiplication of
28
- // two GF numbers, no more).
29
- // @see {@link mul}
30
- for (let i = 255; i < 512; i++) {
31
- EXP_TABLE[i] = EXP_TABLE[i - 255]
32
- }
33
- }())
34
-
35
- /**
36
- * Returns log value of n inside Galois Field
37
- *
38
- * @param {Number} n
39
- * @return {Number}
40
- */
41
- exports.log = function log (n) {
42
- if (n < 1) throw new Error('log(' + n + ')')
43
- return LOG_TABLE[n]
44
- }
45
-
46
- /**
47
- * Returns anti-log value of n inside Galois Field
48
- *
49
- * @param {Number} n
50
- * @return {Number}
51
- */
52
- exports.exp = function exp (n) {
53
- return EXP_TABLE[n]
54
- }
55
-
56
- /**
57
- * Multiplies two number inside Galois Field
58
- *
59
- * @param {Number} x
60
- * @param {Number} y
61
- * @return {Number}
62
- */
63
- exports.mul = function mul (x, y) {
64
- if (x === 0 || y === 0) return 0
65
-
66
- // should be EXP_TABLE[(LOG_TABLE[x] + LOG_TABLE[y]) % 255] if EXP_TABLE wasn't oversized
67
- // @see {@link initTables}
68
- return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]]
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BeadQR/miniprogram/utils/qrcore/kanji-data.js DELETED
@@ -1,54 +0,0 @@
1
- const Mode = require('./mode')
2
- const Utils = require('./utils')
3
-
4
- function KanjiData (data) {
5
- this.mode = Mode.KANJI
6
- this.data = data
7
- }
8
-
9
- KanjiData.getBitsLength = function getBitsLength (length) {
10
- return length * 13
11
- }
12
-
13
- KanjiData.prototype.getLength = function getLength () {
14
- return this.data.length
15
- }
16
-
17
- KanjiData.prototype.getBitsLength = function getBitsLength () {
18
- return KanjiData.getBitsLength(this.data.length)
19
- }
20
-
21
- KanjiData.prototype.write = function (bitBuffer) {
22
- let i
23
-
24
- // In the Shift JIS system, Kanji characters are represented by a two byte combination.
25
- // These byte values are shifted from the JIS X 0208 values.
26
- // JIS X 0208 gives details of the shift coded representation.
27
- for (i = 0; i < this.data.length; i++) {
28
- let value = Utils.toSJIS(this.data[i])
29
-
30
- // For characters with Shift JIS values from 0x8140 to 0x9FFC:
31
- if (value >= 0x8140 && value <= 0x9FFC) {
32
- // Subtract 0x8140 from Shift JIS value
33
- value -= 0x8140
34
-
35
- // For characters with Shift JIS values from 0xE040 to 0xEBBF
36
- } else if (value >= 0xE040 && value <= 0xEBBF) {
37
- // Subtract 0xC140 from Shift JIS value
38
- value -= 0xC140
39
- } else {
40
- throw new Error(
41
- 'Invalid SJIS character: ' + this.data[i] + '\n' +
42
- 'Make sure your charset is UTF-8')
43
- }
44
-
45
- // Multiply most significant byte of result by 0xC0
46
- // and add least significant byte to product
47
- value = (((value >>> 8) & 0xff) * 0xC0) + (value & 0xff)
48
-
49
- // Convert result to a 13-bit binary string
50
- bitBuffer.put(value, 13)
51
- }
52
- }
53
-
54
- module.exports = KanjiData