diff --git a/.gitattributes b/.gitattributes
index 39f1d1e12b04589c6c4e3c00b70dfe2274137a3a..a74d77e6610c22b12664e552221bbae5fa312f78 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -52,3 +52,7 @@ img/memes.png filter=lfs diff=lfs merge=lfs -text
普通拼豆作品展示.MOV filter=lfs diff=lfs merge=lfs -text
烫好了,还没有揭开烫纸,用微信二维码扫一扫,成功扫码.MP4 filter=lfs diff=lfs merge=lfs -text
用烫豆机对拼好的作品烫.MOV filter=lfs diff=lfs merge=lfs -text
+BeadQR/docs/promo/douwei-1080-cover.png filter=lfs diff=lfs merge=lfs -text
+BeadQR/docs/promo/douwei-1080.mp4 filter=lfs diff=lfs merge=lfs -text
+BeadQR/docs/promo/douwei-720-cover.png filter=lfs diff=lfs merge=lfs -text
+BeadQR/docs/promo/douwei-720.mp4 filter=lfs diff=lfs merge=lfs -text
diff --git a/BeadQR/README.md b/BeadQR/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..58e1c1be66ddca4b41d2a437f4899b49d3837beb
--- /dev/null
+++ b/BeadQR/README.md
@@ -0,0 +1,50 @@
+# 豆维 · 可扫码的拼豆
+
+输入一句话(可以再选一张照片),生成**用微信扫一扫就能读出内容**的 MARD 拼豆图纸,并支持导出 PDF 或图片。
+
+## 本版重点
+
+- **一眼看懂**:首屏直接说明用途,页面按“写一句话 → 选张照片 → 导出图纸”三步走,打开就能看到示例效果。
+- **更快**:配色查表加速,色格数据不再走页面数据通道;PDF 改为矢量直出(毫秒级,文件更小);导出时不再重复绘制。
+- **更好看、更好扫**:
+ - 感知更准的配色(OKLab)
+ - 照片按面积取平均色,不再有锯齿
+ - 相邻色块自动平滑,零散颜色自动合并,所需豆子种类更少
+ - 二维码定位图形强制高对比
+ - 本机扫码自检:扫不出时一键优化
+- **打印友好**:PDF 为 A4 矢量图纸,大作品会自动分块并附总览页;每一格都印有色号,另附带用量的清单。
+- **精选色板**:除标准 221 色和完整 291 色外,还提供 144/96/72/48/24 色精选套装,方便少买豆。
+
+## 内容安全链路(按微信平台要求)
+
+| 环节 | 措施 |
+| --- | --- |
+| 二维码文字、扫码导入的文字、水印 | 云函数调用 `security.msgSecCheck` v2(带 openid、按场景值检测),结果为 risky 或 review 均拦截 |
+| 照片 | 选图后立即压缩到官方推荐的 750×1334 以内,由云函数调用 `security.imgSecCheck`;**检测通过前不能应用照片** |
+| 检测异常 | 一律视为未通过,不允许生成与导出(fail-closed) |
+| 导出前 | 再次校验全部文本(已通过的走缓存,不增加等待) |
+| 云函数加固 | 必须有 OPENID;只接受 `sec-check/` 目录下的 jpg;通过文件头判断格式;限制 1MB;检测后立即删除云端临时文件 |
+| 隐私 | 已开启 `__usePrivacyCheck__`,内置隐私授权弹窗;页脚设有“隐私保护指引”入口 |
+| 投诉与反馈 | 页脚提供“内容规范”说明及 `open-type="feedback"` 的投诉反馈入口 |
+| 分享 | 分享出去的只是小程序卡片,不带用户内容 |
+
+## 部署步骤
+
+1. 用微信开发者工具打开本目录(appid 已配置)。
+2. 右键 `cloudfunctions/quickstartFunctions` →「上传并部署:云端安装依赖」。**本版云函数有改动,必须重新部署。**
+3. 在云开发控制台 → 存储中确认可以写入 `sec-check/` 目录(默认权限即可)。
+4. 在小程序管理后台 →「设置 → 服务内容声明 → 用户隐私保护指引」中声明:选择图片/拍照(`chooseMedia`)、保存到相册(`saveImageToPhotosAlbum`)、扫码(`scanCode`)。
+5. 真机测试:正常/违规文字、正常/违规图片、断网时的检测异常,确认都会被正确拦截和提示。
+
+## 说明
+
+- 为保证扫码成功率,**二维码默认保留 1 格白边**。如需原来的“无白边”效果,可在「高级设置 → 白边」中选择“无”。
+- 圆豆、中空豆样式只影响图纸的显示效果,实际拼出来的位置与方豆完全一致。
+- 第三方代码:`utils/qrcore`(node-qrcode,MIT)、`utils/vendor/jsqr.js`(jsQR,Apache-2.0)。
+
+## 首屏演示动画 & 宣传视频
+
+- 首次打开(还没写内容)时,预览区播放 12 秒 1:1 演示动画:写一句话 → 编成二维码 → 融进照片 → 微信一扫 → 结尾卡片。
+- 动画由 `miniprogram/utils/intro.js` 纯代码实时绘制,**不含任何视频/图片素材,几乎不占包体积**(约 20KB)。演示作品由真实配色引擎生成,画面里的二维码真的能扫出 “Happy Birthday!”。
+- 滑出屏幕、切到后台、打开裁剪时自动暂停;点一下预览区重新播放;写下内容(或选了照片)后自动切回真实二维码。
+- `docs/promo/` 里是用同一套代码渲染的宣传视频(1080 / 720,含原创合成配乐)和封面图,可直接发公众号。该目录不在 `miniprogram/` 内,不计入小程序包体积。
diff --git a/BeadQR/cloudfunctions/quickstartFunctions/config.json b/BeadQR/cloudfunctions/quickstartFunctions/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..a1d2eac6ed64d91b41f32dd6a796a2fb91328985
--- /dev/null
+++ b/BeadQR/cloudfunctions/quickstartFunctions/config.json
@@ -0,0 +1,9 @@
+{
+ "timeout": 20,
+ "permissions": {
+ "openapi": [
+ "security.msgSecCheck",
+ "security.imgSecCheck"
+ ]
+ }
+}
diff --git a/BeadQR/cloudfunctions/quickstartFunctions/index.js b/BeadQR/cloudfunctions/quickstartFunctions/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..24bdb9b2b2dba5b4e592e6b61c46ec3124794dd7
--- /dev/null
+++ b/BeadQR/cloudfunctions/quickstartFunctions/index.js
@@ -0,0 +1,92 @@
+// 豆维 · 内容安全云函数
+// type: checkMessage | checkMessages | checkImage
+// 返回约定:{ risk: boolean, skipped?: boolean };skipped 表示检测未能完成,前端必须拦截
+const cloud = require("wx-server-sdk");
+cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
+
+const MAX_TEXT = 2500;
+const MAX_IMAGE_BYTES = 1024 * 1024;
+const MAX_BATCH = 5;
+
+const extractErrCode = (error) => {
+ if (!error) return undefined;
+ if (typeof error.errCode === "number") return error.errCode;
+ if (typeof error.code === "number") return error.code;
+ const match = typeof error.errMsg === "string" ? error.errMsg.match(/-?\d{3,}/) : null;
+ return match ? Number(match[0]) : undefined;
+};
+
+// msgSecCheck v2:scene 1 资料 / 2 评论 / 3 论坛 / 4 社交日志
+async function checkOneMessage(content, scene, openid) {
+ const value = String(content || "").slice(0, MAX_TEXT);
+ if (!value.trim()) return { risk: false };
+ const safeScene = [1, 2, 3, 4].includes(Number(scene)) ? Number(scene) : 2;
+ try {
+ const response = await cloud.openapi.security.msgSecCheck({
+ version: 2,
+ scene: safeScene,
+ openid,
+ content: value,
+ });
+ const suggest = response && response.result && response.result.suggest;
+ if (!suggest) return { risk: false, skipped: true };
+ // review(疑似)按违规处理,宁严勿松
+ return { risk: suggest === "risky" || suggest === "review", verdict: suggest };
+ } catch (e) {
+ if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" };
+ return { risk: false, skipped: true, errCode: extractErrCode(e) };
+ }
+}
+
+// 通过文件头判断真实格式,不信任客户端传入的类型
+function sniffType(buffer) {
+ if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return "image/jpeg";
+ if (buffer.length > 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return "image/png";
+ return "";
+}
+
+async function checkImage(fileID) {
+ // 只允许检测本小程序上传到 sec-check/ 目录的临时文件,防止借接口删除/读取其他文件
+ if (typeof fileID !== "string" || !/^cloud:\/\/[^/]+\/sec-check\/[\w.-]+\.jpg$/.test(fileID)) {
+ return { risk: false, skipped: true, reason: "INVALID_FILE" };
+ }
+ try {
+ const download = await cloud.downloadFile({ fileID });
+ const buffer = download && download.fileContent;
+ const contentType = buffer ? sniffType(buffer) : "";
+ if (!buffer || !buffer.length || buffer.length > MAX_IMAGE_BYTES || !contentType) {
+ return { risk: false, skipped: true, reason: "BAD_IMAGE" };
+ }
+ await cloud.openapi.security.imgSecCheck({
+ media: { contentType, value: buffer },
+ });
+ return { risk: false };
+ } catch (e) {
+ if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" };
+ return { risk: false, skipped: true, errCode: extractErrCode(e) };
+ } finally {
+ try {
+ await cloud.deleteFile({ fileList: [fileID] });
+ } catch (e) {
+ // 清理失败不影响检测结果
+ }
+ }
+}
+
+exports.main = async (event) => {
+ const { OPENID } = cloud.getWXContext();
+ if (!OPENID) return { risk: false, skipped: true, reason: "NO_OPENID" };
+ const type = event && event.type;
+ if (type === "checkMessage") {
+ return checkOneMessage(event.content, event.scene, OPENID);
+ }
+ if (type === "checkMessages") {
+ const items = Array.isArray(event.items) ? event.items.slice(0, MAX_BATCH) : [];
+ const results = await Promise.all(items.map((it) => checkOneMessage(it && it.value, it && it.scene, OPENID)));
+ return { results };
+ }
+ if (type === "checkImage") {
+ return checkImage(event.fileID);
+ }
+ return { risk: false, skipped: true, reason: "UNKNOWN_TYPE" };
+};
diff --git a/BeadQR/cloudfunctions/quickstartFunctions/package-lock.json b/BeadQR/cloudfunctions/quickstartFunctions/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..26d0d9d6e69b043888fa78b3e1a003670970cf1c
--- /dev/null
+++ b/BeadQR/cloudfunctions/quickstartFunctions/package-lock.json
@@ -0,0 +1,1157 @@
+{
+ "name": "quickstartFunctions",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "quickstartFunctions",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "wx-server-sdk": "~4.0.2"
+ }
+ },
+ "node_modules/@cloudbase/adapter-interface": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@cloudbase/adapter-interface/-/adapter-interface-0.7.1.tgz",
+ "integrity": "sha512-BjHTAvIMSLxbp26SeT6qLiSSBygz3kAVno/PMwwBwjgzVkVUvuZGVcvf1mhHxY/BjdRGLrd6X60OgvymV/aAUw==",
+ "license": "ISC"
+ },
+ "node_modules/@cloudbase/adapter-wx_mp": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@cloudbase/adapter-wx_mp/-/adapter-wx_mp-1.3.1.tgz",
+ "integrity": "sha512-rS0N1aD1a0zc421ds17/HzM4RChQ6T2ixMz87bhPJ1yveqJt/KUqYxiXJDWhZ803wwTmJs8aqNkgugsNusNhbg==",
+ "license": "ISC",
+ "dependencies": {
+ "@cloudbase/adapter-interface": "^0.7.0",
+ "web-streams-polyfill": "^4.0.0"
+ }
+ },
+ "node_modules/@cloudbase/ai": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/ai/-/ai-2.30.0.tgz",
+ "integrity": "sha512-RtBux47omXWcMhiiDDVJ96d2JlqnQ/bzKC6GG3ROgupYG3ReQyv1IssCpg5cnzmZ+l651IqdOpt27VTuO3kEGQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cloudbase/types": "2.30.0",
+ "@cloudbase/utilities": "2.30.0",
+ "@mattiasbuelens/web-streams-adapter": "^0.1.0",
+ "text-encoding-shim": "^1.0.5",
+ "web-streams-polyfill": "^4.0.0"
+ }
+ },
+ "node_modules/@cloudbase/ai/node_modules/@cloudbase/types": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz",
+ "integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cloudbase/adapter-interface": "^0.7.1"
+ }
+ },
+ "node_modules/@cloudbase/app": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/app/-/app-2.32.0.tgz",
+ "integrity": "sha512-78+eSo8OLO4iUiifdynZo4dK4wNlyaz5x9423muSibyl4FLwt5/Xt0t9s7StFmGaBdvB75UYM9dGWxOdIFRzaw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cloudbase/adapter-interface": "^0.7.1",
+ "@cloudbase/adapter-wx_mp": "^1.3.1",
+ "@cloudbase/types": "2.32.0",
+ "@cloudbase/utilities": "2.32.0"
+ },
+ "peerDependencies": {
+ "@cloudbase/signature-nodejs": ">=1.0.0",
+ "jsonwebtoken": ">=8.0.0",
+ "ws": "^8.18.0"
+ },
+ "peerDependenciesMeta": {
+ "@cloudbase/signature-nodejs": {
+ "optional": true
+ },
+ "jsonwebtoken": {
+ "optional": true
+ },
+ "ws": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cloudbase/app/node_modules/@cloudbase/utilities": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.32.0.tgz",
+ "integrity": "sha512-vYrr1aXOpU+/IrkHMSTxi/k2d4N8raNu+E3IrKtWt4w7Y0Ke3DLvCHeUJcuWECvQBVhzmg1ert4DcXVUymSKlg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cloudbase/adapter-interface": "^0.7.1",
+ "@cloudbase/types": "2.32.0",
+ "jwt-decode": "^3.1.2"
+ }
+ },
+ "node_modules/@cloudbase/database": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@cloudbase/database/-/database-1.4.3.tgz",
+ "integrity": "sha512-JzmdsGjy9LwzSQQ0Fv4OlNHNK61BRXdJSembTpy4408AQA3q2Ip3N5eB3v9OpAlFMvP/6MGmOQ/ZuUPKfJddJA==",
+ "license": "ISC",
+ "dependencies": {
+ "bson": "^4.0.3",
+ "lodash.clonedeep": "4.5.0",
+ "lodash.set": "4.3.2",
+ "lodash.unset": "4.5.2"
+ }
+ },
+ "node_modules/@cloudbase/node-sdk": {
+ "version": "3.17.2",
+ "resolved": "https://registry.npmjs.org/@cloudbase/node-sdk/-/node-sdk-3.17.2.tgz",
+ "integrity": "sha512-74ekvRsdmeGch9hFHkQnjHPTEfk8k09iQFvI1aAo48ZsbLqK3czqj/3GoVpSkw5+eYe05tZV13Ct/kWiV5tuGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@cloudbase/ai": "^2.23.0",
+ "@cloudbase/app": "^2.23.0",
+ "@cloudbase/database": "1.4.3",
+ "@cloudbase/signature-nodejs": "2.2.0",
+ "@cloudbase/types": "^2.23.0",
+ "@cloudbase/wx-cloud-client-sdk": "1.7.1",
+ "agentkeepalive": "^4.3.0",
+ "axios": "0.27.2",
+ "form-data": "^4.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "jsonwebtoken": "^9.0.2",
+ "retry": "^0.13.1",
+ "web-streams-polyfill": "^4.2.0",
+ "xml2js": "^0.6.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cloudbase/signature-nodejs": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/signature-nodejs/-/signature-nodejs-2.2.0.tgz",
+ "integrity": "sha512-2y2rieOpJNnBA2MW7sLJKek3iFOjgr8TNicw5qjBkcdVczLR0cFzxMhip75NLCwarFTIAONE3oeNxoQ19eVHcA==",
+ "dependencies": {
+ "@types/clone": "^2.1.0",
+ "clone": "^2.1.2",
+ "is-stream": "^2.0.0",
+ "url": "^0.11.0"
+ }
+ },
+ "node_modules/@cloudbase/types": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.32.0.tgz",
+ "integrity": "sha512-HPsMEHHt5SAyD3tpv/C/v2nYudSawE48ixjK5Y9w3R/82auJag0AUsZGfCExgdPa4Zud9OvGx5127ssEXLgadw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cloudbase/adapter-interface": "^0.7.1"
+ }
+ },
+ "node_modules/@cloudbase/utilities": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.30.0.tgz",
+ "integrity": "sha512-52DfQH5qgfOtrwj26KCGw6cz0pUjuJludJ7fccUKCzBtn/QwPudwtXo7gOUTtKKDubW3+VUCjjBm7UiDwFu+ag==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cloudbase/adapter-interface": "^0.7.1",
+ "@cloudbase/types": "2.30.0",
+ "jwt-decode": "^3.1.2"
+ }
+ },
+ "node_modules/@cloudbase/utilities/node_modules/@cloudbase/types": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz",
+ "integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cloudbase/adapter-interface": "^0.7.1"
+ }
+ },
+ "node_modules/@cloudbase/wx-cloud-client-sdk": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@cloudbase/wx-cloud-client-sdk/-/wx-cloud-client-sdk-1.7.1.tgz",
+ "integrity": "sha512-NzLOoYDAeoRh2lRjCtvr6yXu4KPVHqZtPxyv70e+3b2+dZIDE5S5gtU8daE7xS5u4TwPqKB/E2vmgRTYOsCS9w==",
+ "dependencies": {
+ "core-js-pure": "^3.45.0"
+ }
+ },
+ "node_modules/@mattiasbuelens/web-streams-adapter": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz",
+ "integrity": "sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
+ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
+ "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@types/clone": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@types/clone/-/clone-2.1.4.tgz",
+ "integrity": "sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "26.3.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz",
+ "integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+ "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.14.9",
+ "form-data": "^4.0.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bson": {
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz",
+ "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "buffer": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.50.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz",
+ "integrity": "sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jwt-decode": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
+ "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.set": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+ "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.unset": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz",
+ "integrity": "sha512-bwKX88k2JhCV9D1vtE8+naDKlLiGrSmf8zi/Y9ivFHwbmRfA8RxS/aVJ+sIht2XOwqoNr4xUPUkGZpc1sHFEKg==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/protobufjs": {
+ "version": "7.6.5",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
+ "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.1",
+ "@protobufjs/fetch": "^1.1.1",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.3.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.15.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
+ "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/text-encoding-shim": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/text-encoding-shim/-/text-encoding-shim-1.0.5.tgz",
+ "integrity": "sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "license": "MIT"
+ },
+ "node_modules/url": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
+ "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^1.4.1",
+ "qs": "^6.12.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.3.0.tgz",
+ "integrity": "sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw==",
+ "license": "MIT",
+ "workspaces": [
+ "test/benchmark-test",
+ "test/rollup-test",
+ "test/webpack-test"
+ ],
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wx-server-sdk": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/wx-server-sdk/-/wx-server-sdk-4.0.2.tgz",
+ "integrity": "sha512-dgpgWxYvOFWnQdGOb78sU+uQGELoEQSKzmJ7m8Y6eTCJKsfkdIHLMhRrdN5S5oDfAqr8Pc83jZKQeNRMZASIcg==",
+ "license": "MIT",
+ "dependencies": {
+ "@cloudbase/node-sdk": "3.17.2",
+ "json-bigint": "^1.0.0",
+ "protobufjs": "^7.2.4",
+ "tslib": "^1.9.3"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ }
+ }
+}
diff --git a/BeadQR/cloudfunctions/quickstartFunctions/package.json b/BeadQR/cloudfunctions/quickstartFunctions/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..63727cda9c2e7d038ad78f36ae111268eadd9258
--- /dev/null
+++ b/BeadQR/cloudfunctions/quickstartFunctions/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "quickstartFunctions",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "wx-server-sdk": "~4.0.2"
+ }
+}
diff --git a/BeadQR/docs/promo/douwei-1080-cover.png b/BeadQR/docs/promo/douwei-1080-cover.png
new file mode 100644
index 0000000000000000000000000000000000000000..f08e99fd0f86d17f8467e6b059637e3b989fc1ec
--- /dev/null
+++ b/BeadQR/docs/promo/douwei-1080-cover.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aeac46fce4028cbfc2d7bae0dad427ed5f67262582ad411a00ccb2db65f8f03b
+size 319823
diff --git a/BeadQR/docs/promo/douwei-1080.mp4 b/BeadQR/docs/promo/douwei-1080.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..8ad3a7bd6f7099c1305b7b42b367c9c7db85cdbb
--- /dev/null
+++ b/BeadQR/docs/promo/douwei-1080.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:89ba1505c4ce4aa36316841c497dc96b8997bac534bb534426c23ca96ce87dcc
+size 3693122
diff --git a/BeadQR/docs/promo/douwei-720-cover.png b/BeadQR/docs/promo/douwei-720-cover.png
new file mode 100644
index 0000000000000000000000000000000000000000..ccaf575a9b83c1b8b0eb3255f0e3a37673424a67
--- /dev/null
+++ b/BeadQR/docs/promo/douwei-720-cover.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bdb68c450700c1e65492e708440a29d07c9288606ca0ad7b464133052855fb2a
+size 195403
diff --git a/BeadQR/docs/promo/douwei-720.mp4 b/BeadQR/docs/promo/douwei-720.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..b95f5525b899e8a0144b1b7da6234149348de007
--- /dev/null
+++ b/BeadQR/docs/promo/douwei-720.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:626ea04f449f33fe29c4ec6c4aa94a2db88600cababab4f5a11b74155b4eee4e
+size 1650037
diff --git a/BeadQR/docs/video-script.md b/BeadQR/docs/video-script.md
new file mode 100644
index 0000000000000000000000000000000000000000..bc42e2b8af7f963da3dc1c90067b2aa461061fcc
--- /dev/null
+++ b/BeadQR/docs/video-script.md
@@ -0,0 +1,116 @@
+# 豆维 · 视频文案与分镜脚本
+
+> 目标:用一个短视频介绍「豆维 · 可扫码的拼豆」小程序,让观众理解它是什么、为什么有趣、怎么做出来。
+> 结构:提出想法 → 实现想法 → 制作例子 → 完成例子。
+
+---
+
+## 一、视频定位与调性
+
+| 维度 | 建议 |
+|------|------|
+| 时长 | 主版本 60–90 秒(B站 / 朋友圈 / 公众号),另附 15–30 秒短视频版 |
+| 调性 | 温暖手作 × 科技惊喜,不是冷冰冰的产品演示 |
+| 核心情绪 | 「拼豆不止是一幅画,它是一封能被读到的信」 |
+| 目标观众 | 拼豆爱好者、手工创作者、喜欢送独特礼物的人 |
+| 画面风格 | 暖色调,木桌面,自然光;手机屏幕录制与实物特写交替出现 |
+
+---
+
+## 二、核心文案(Slogan / 一句话介绍)
+
+以下任选一条作为标题或结尾定版字:
+
+1. **豆维 · 可扫码的拼豆**
+2. **每一颗珠子,都藏着一句话。**
+3. **拼出来的二维码,扫出来的心意。**
+4. **把想说的话,拼进一幅画里。**
+5. **看得见,也读得到。**
+
+推荐主标题:**看得见,也读得到。——豆维,可扫码的拼豆**
+
+---
+
+## 三、主版本分镜脚本(60–90 秒)
+
+> 旁白以第一人称「我」叙述,语气像朋友分享,不念广告稿。
+> 「画面」列标注了你需要准备的素材类型;如果你已有类似镜头,直接对应即可。
+
+| 幕 | 时间 | 画面 | 旁白 / 字幕 | 剪辑提示 |
+|----|------|------|-------------|----------|
+| **第一幕:提出想法** | 0:00–0:08 | 桌面上摆着几幅拼豆作品,阳光照进来。手拿起其中一幅,翻到背面,又翻回来。 | **旁白:** 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画? | 慢推镜头,配温暖的钢琴或指弹吉他。最后一帧画面停在作品特写。 |
+| | 0:08–0:14 | 手拿起手机对准拼豆作品「扫一扫」——屏幕弹出一条消息或链接。 | **旁白:** 能不能扫一下,就能看到我想说的话? | 这是全片第一处「惊喜时刻」,音效清脆,屏幕内容要清晰可见。 |
+| **第二幕:实现想法** | 0:14–0:24 | 切到手机屏幕录屏:打开豆维小程序,输入一段文字。 | **旁白:** 所以我做了「豆维」——一个可扫码的拼豆图纸生成器。输入一段话,或者一个链接。 | 屏幕录制加速 2–3 倍,打字过程快放。 |
+| | 0:24–0:34 | 录屏:导入一张照片,裁切、去背景。 | **旁白:** 再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。 | 展示裁切弹窗操作,去背景效果前后对比。 |
+| | 0:34–0:44 | 录屏:拖动滑块调整二维码大小和位置,切换「丰富 / 平衡 / 稳定」模式。 | **旁白:** 二维码可以放大缩小、移到任何位置。颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。 | 滑块操作用慢动作展示,让观众看到图案实时变化。 |
+| **第三幕:制作例子** | 0:44–0:52 | 导出的图纸打印出来,摆在桌上。开始一颗一颗摆珠子。 | **旁白:** 图纸保存下来,打印出来,就可以开始拼了。 | 延时摄影或快剪,展示摆珠过程,保留几秒真实速度的特写。 |
+| | 0:52–0:58 | 熨斗烫过,拼豆板冷却,掀开,成品完成。 | **旁白:** 熨好,冷却,掀开—— | 掀开那一刻音效「叮」,画面干净利落。 |
+| **第四幕:完成例子** | 0:58–1:10 | 手持成品对镜头展示,然后用手机扫描。屏幕上弹出一开始输入的那段话。 | **旁白:** 它不只是一幅画。你把手机对准它——就能读到里面藏着的话。 | 这是全片情绪最高点。扫描过程用真实速度,屏幕内容要清楚。 |
+| | 1:10–1:20 | 多个成品排列展示(不同图案、不同颜色),最后画面淡出到品牌 Logo 和小程序码。 | **旁白:** 豆维,可扫码的拼豆。看得见,也读得到。 | 结尾定格 3 秒,放小程序码,方便观众扫码体验。 |
+
+---
+
+## 四、15 秒短视频版(抖音 / 视频号快闪)
+
+| 时间 | 画面 | 字幕(无旁白,纯字幕流) |
+|------|------|--------------------------|
+| 0–3s | 手拿起拼豆作品 → 手机扫描 → 屏幕弹出消息 | 拼豆里,藏着一条消息 |
+| 3–8s | 快剪:输入文字 → 导入照片 → 图纸生成 → 摆珠 | 豆维:输入一句话,选一张图,生成拼豆图纸 |
+| 8–13s | 熨烫 → 掀开 → 成品再扫一次 | 拼好,烫平,扫一下—— |
+| 13–15s | 品牌 Logo + 小程序码 | 消息就出来了。豆维 · 可扫码的拼豆 |
+
+> 快闪版不需要旁白,靠音乐节奏 + 字幕推进。音乐选节奏感强的电子或 Lo-Fi。
+
+---
+
+## 五、30 秒版本(朋友圈 / 公众号内嵌)
+
+节奏比 15 秒版稍慢,保留 4 个关键节点:
+
+1. **悬念**(5s):拿起作品 → 扫码 → 弹出内容
+2. **怎么做**(10s):小程序操作快剪(输入 → 导图 → 调整 → 导出)
+3. **手作过程**(8s):摆珠延时 + 熨烫 + 掀开
+4. **结尾**(7s):成品特写 + 再扫一次 + 定版
+
+配乐:轻快指弹吉他或 Lo-Fi,音量压低,让旁白清晰。
+
+---
+
+## 六、素材清单
+
+拍摄前对照这份清单,确认已拍到以下镜头(√ 为已有,× 为待补):
+
+| 素材 | 用途 | 状态 |
+|------|------|------|
+| 多幅完成的拼豆作品摆拍(正面 / 斜角 / 背面) | 开头悬念、结尾展示 | ☐ |
+| 手机扫描拼豆作品并弹出内容的实拍 | 全片核心「惊喜时刻」,需拍 2–3 次 | ☐ |
+| 豆维小程序屏幕录制:输入文字、导入图片、裁切、调 QR 位置、切模式 | 第二幕 | ☐ |
+| 图纸导出 / 打印的镜头 | 第三幕开头 | ☐ |
+| 摆珠延时或快剪素材 | 第三幕 | ☐ |
+| 熨斗烫拼豆 + 掀开的特写 | 第三幕高潮 | ☐ |
+| 成品多角度特写(自然光 / 桌面) | 结尾 | ☐ |
+
+---
+
+## 七、剪辑建议
+
+- **转场**:幕与幕之间用干净的硬切或快速模糊,不要花哨转场。产品视频要显得克制。
+- **音乐**:主版本选温暖的原声吉他或钢琴;快闪版选节奏感强的 Lo-Fi / 电子。全片一首歌,不要中途换。
+- **节奏**:第三幕(摆珠)可以稍慢,让观众感受手作质感;第二幕(录屏)加速推进。
+- **字幕**:全程加简体中文字幕,关键词用品牌色 `#416d62`(深绿)高亮。
+- **结尾**:小程序码至少停留 3 秒,让观众来得及扫码。
+
+---
+
+## 八、旁白全文(主版本,可直接朗读)
+
+> 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画?能不能扫一下,就能看到我想说的话?
+>
+> 所以我做了「豆维」。输入一段话,或者一个链接,再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。二维码可以放大缩小、移到任何位置,颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。
+>
+> 图纸保存下来,打印出来,就可以开始拼了。熨好,冷却,掀开——
+>
+> 它不只是一幅画。你把手机对准它,就能读到里面藏着的话。
+>
+> 豆维,可扫码的拼豆。看得见,也读得到。
+
diff --git "a/BeadQR/docs/\345\256\241\346\240\270\351\251\263\345\233\236\345\244\204\347\220\206-\346\226\207\346\241\210\344\270\216\346\255\245\351\252\244.md" "b/BeadQR/docs/\345\256\241\346\240\270\351\251\263\345\233\236\345\244\204\347\220\206-\346\226\207\346\241\210\344\270\216\346\255\245\351\252\244.md"
new file mode 100644
index 0000000000000000000000000000000000000000..32154ecf4a6048b2035090856a2d3e244d5fcf13
--- /dev/null
+++ "b/BeadQR/docs/\345\256\241\346\240\270\351\251\263\345\233\236\345\244\204\347\220\206-\346\226\207\346\241\210\344\270\216\346\255\245\351\252\244.md"
@@ -0,0 +1,58 @@
+# 审核驳回处理:深度合成误判 — 文案与步骤
+
+> 驳回原因(2026-09-30):小程序服务内容涉及深度合成技术(如 AI 问答、AI 换脸视频、AI 绘画等),属个人主体尚未开放服务类目,建议申请企业主体类型小程序。
+
+## 结论
+
+代码里没有任何 AI 功能:文字是标准二维码编码;"照片转图纸"是确定性本地图像处理(按网格取平均色 → 映射到 221 种固定色号),不用任何模型、不生成新图像。被判"深度合成"大概率是**后台简介 / 服务类目 / 服务内容声明的表述**或审核员看到"照片→生成图纸"产生的误判。
+
+## 路径 A(推荐,最快):修正表述后重新提审
+
+### 1. 检查后台三处(mp.weixin.qq.com)
+
+| 位置 | 检查项 |
+| --- | --- |
+| 服务类目 | 改为 **工具 - 图片处理**(个人主体可选);删除任何 AI / 深度合成相关类目 |
+| 服务内容声明 / 提审问卷 | 凡问"是否提供 AI 生成内容、AI 问答、AI 绘画、深度合成"一律选 **否** |
+| 小程序简介 | 换成下方"新简介",去掉"生成器 / 一键生成 / 智能"等字眼 |
+
+### 2. 新简介(约 90 字,上限 120)
+
+```
+手工拼豆图纸工具:输入一句话或选一张照片,按 MARD 标准色号排成拼豆图纸,拼好微信一扫就能读出内容。支持 A4 矢量 PDF 与图片导出、用量清单。照片仅在本地做取色处理,并经微信内容安全检测。
+```
+
+### 3. 版本描述 / 审核备注(重新提审时填写)
+
+```
+本小程序为拼豆手工图纸工具,不使用任何人工智能或深度合成技术,无 AIGC 功能:
+1. "照片转图纸"为确定性本地图像处理:将用户上传照片按网格取平均色,再映射到固定的 221 种拼豆标准色号(色彩量化 + 最近邻查表),不生成、不合成任何新图像内容,不涉及人脸或生物特征编辑;
+2. 文字功能是将文本编码为标准二维码点阵,非内容生成;
+3. 所有处理均在用户手机本地完成,且已接入微信内容安全接口(security.msgSecCheck / imgSecCheck),检测未通过时禁止生成与导出(fail-closed)。
+请按"工具-图片处理"类目审核。
+```
+
+## 路径 B:申诉
+
+若不想改简介,可直接对本次审核结果发起申诉(审核结果页 → 申诉),粘贴下文:
+
+```
+本小程序为拼豆手工图纸工具,不涉及深度合成技术:
+(1) 不使用 AI 模型 / 神经网络,无 AI 问答、AI 绘画、AI 换脸等 AIGC 功能;
+(2) 照片功能仅做确定性色彩量化:按网格取平均色并映射到 221 种固定标准色号,属于常规图像处理,不生成新图像、不做人脸或生物特征编辑;
+(3) 文字功能为标准二维码编码;
+(4) 已接入微信内容安全检测(msgSecCheck / imgSecCheck),检测未通过不能生成或导出。
+恳请复核,建议按"工具-图片处理"类目审核。
+```
+
+申诉未通过时:修改简介与类目后按路径 A 重新提审(改了表述基本都能过)。
+
+## 路径 C:仅当二次被拒或想保留"生成"叙事
+
+1. **隐藏照片功能**(只保留文字 → 二维码图纸,纯编码无争议)——我可以直接改代码;
+2. **升级主体**:注册个体工商户 / 企业 → 主体变更 → 申请深度合成相关类目 + 算法备案。仅在以后真要做 AI 功能时才值得,当前工具用不到。
+
+## 顺手可做的降风险小改动(可选)
+
+- 首页徽标 `MARD 拼豆图纸生成器` → `MARD 拼豆图纸工具`
+- 分享标题 `豆维 · 可扫码的拼豆图纸生成器` → `豆维 · 可扫码的拼豆图纸`
diff --git a/BeadQR/miniprogram/app.js b/BeadQR/miniprogram/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..77c44453096b7460e86e268a5079a6b79c45d25d
--- /dev/null
+++ b/BeadQR/miniprogram/app.js
@@ -0,0 +1,14 @@
+// 豆维 · 可扫码的拼豆
+App({
+ onLaunch() {
+ if (!wx.cloud) {
+ console.error('当前基础库不支持云开发,内容安全检测不可用')
+ return
+ }
+ wx.cloud.init({
+ env: 'cloud1-d6gthxado7f6c9d14',
+ traceUser: true,
+ })
+ },
+ globalData: {},
+})
diff --git a/BeadQR/miniprogram/app.json b/BeadQR/miniprogram/app.json
new file mode 100644
index 0000000000000000000000000000000000000000..c65531905dd809351665a2e54b7406068e24fe14
--- /dev/null
+++ b/BeadQR/miniprogram/app.json
@@ -0,0 +1,19 @@
+{
+ "pages": [
+ "pages/index/index"
+ ],
+ "window": {
+ "backgroundColor": "#f6f5f1",
+ "backgroundTextStyle": "dark",
+ "navigationBarBackgroundColor": "#f6f5f1",
+ "navigationBarTitleText": "豆维 · 可扫码的拼豆",
+ "navigationBarTextStyle": "black"
+ },
+ "usingComponents": {
+ "privacy-popup": "/components/privacy-popup/index"
+ },
+ "__usePrivacyCheck__": true,
+ "sitemapLocation": "sitemap.json",
+ "style": "v2",
+ "lazyCodeLoading": "requiredComponents"
+}
diff --git a/BeadQR/miniprogram/app.wxss b/BeadQR/miniprogram/app.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..284c483d7c36686985f29a1abfdb8de7b83aead0
--- /dev/null
+++ b/BeadQR/miniprogram/app.wxss
@@ -0,0 +1,37 @@
+/* 豆维 · 设计变量 */
+page {
+ --ink: #16211f;
+ --ink-2: #3a4643;
+ --muted: #6f7a76;
+ --faint: #a4aca8;
+ --bg: #f6f5f1;
+ --card: #ffffff;
+ --line: #ebe8e1;
+ --brand: #0f766e;
+ --brand-2: #14b8a6;
+ --brand-soft: #e6f4f1;
+ --brand-deep: #0f3d3a;
+ --accent: #ff7a45;
+ --accent-soft: #fff1ea;
+ --danger: #e5484d;
+ --danger-soft: #fdecec;
+ --ok: #12a150;
+ --ok-soft: #e7f6ed;
+ --warn: #c77700;
+ --warn-soft: #fff5e0;
+ --r-s: 16rpx;
+ --r-m: 24rpx;
+ --r-l: 36rpx;
+ --shadow: 0 8rpx 32rpx rgba(22, 33, 31, 0.06);
+
+ background: var(--bg);
+ color: var(--ink);
+ font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
+ font-size: 28rpx;
+ line-height: 1.55;
+}
+
+view, text, textarea, input, button, scroll-view, canvas, image { box-sizing: border-box; }
+
+button { padding: 0; margin: 0; background: none; border: 0; border-radius: 0; line-height: inherit; font-size: inherit; color: inherit; text-align: center; }
+button::after { border: 0; }
diff --git a/BeadQR/miniprogram/assets/icon.png b/BeadQR/miniprogram/assets/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d10be4c5227e54d35f75210df674fac3218ff05
Binary files /dev/null and b/BeadQR/miniprogram/assets/icon.png differ
diff --git a/BeadQR/miniprogram/assets/miniprogram-code.png b/BeadQR/miniprogram/assets/miniprogram-code.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a295d47d3f983270485c2eefceb296ce2729810
Binary files /dev/null and b/BeadQR/miniprogram/assets/miniprogram-code.png differ
diff --git a/BeadQR/miniprogram/assets/shield.svg b/BeadQR/miniprogram/assets/shield.svg
new file mode 100644
index 0000000000000000000000000000000000000000..50456c42007effc88842ab4b474c62b6b64af781
--- /dev/null
+++ b/BeadQR/miniprogram/assets/shield.svg
@@ -0,0 +1 @@
+
diff --git a/BeadQR/miniprogram/components/privacy-popup/index.js b/BeadQR/miniprogram/components/privacy-popup/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e5d889e8cec37f4235f72742b66926a17825115
--- /dev/null
+++ b/BeadQR/miniprogram/components/privacy-popup/index.js
@@ -0,0 +1,40 @@
+// 隐私保护指引弹窗:用户首次调用相册/相机/保存到相册等隐私接口时由微信触发
+Component({
+ data: { visible: false, contractName: '《豆维隐私保护指引》' },
+
+ lifetimes: {
+ attached() {
+ if (!wx.onNeedPrivacyAuthorization) return
+ this._listener = (resolve) => {
+ this._resolve = resolve
+ this.setData({ visible: true })
+ }
+ wx.onNeedPrivacyAuthorization(this._listener)
+ if (wx.getPrivacySetting) {
+ wx.getPrivacySetting({
+ success: (res) => {
+ if (res.privacyContractName) this.setData({ contractName: res.privacyContractName })
+ },
+ })
+ }
+ },
+ },
+
+ methods: {
+ openContract() {
+ if (wx.openPrivacyContract) wx.openPrivacyContract({})
+ },
+ handleAgree() {
+ if (this._resolve) this._resolve({ buttonId: 'privacy-agree-btn', event: 'agree' })
+ this._resolve = null
+ this.setData({ visible: false })
+ },
+ handleDisagree() {
+ if (this._resolve) this._resolve({ event: 'disagree' })
+ this._resolve = null
+ this.setData({ visible: false })
+ wx.showToast({ title: '未同意将无法选择照片或保存图纸', icon: 'none' })
+ },
+ noop() {},
+ },
+})
diff --git a/BeadQR/miniprogram/components/privacy-popup/index.json b/BeadQR/miniprogram/components/privacy-popup/index.json
new file mode 100644
index 0000000000000000000000000000000000000000..a89ef4dbeefa01f5cd7971973aa4db6498d139f7
--- /dev/null
+++ b/BeadQR/miniprogram/components/privacy-popup/index.json
@@ -0,0 +1,4 @@
+{
+ "component": true,
+ "usingComponents": {}
+}
diff --git a/BeadQR/miniprogram/components/privacy-popup/index.wxml b/BeadQR/miniprogram/components/privacy-popup/index.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..52eb9a5d528a586383b94527a8a6814bea849ea0
--- /dev/null
+++ b/BeadQR/miniprogram/components/privacy-popup/index.wxml
@@ -0,0 +1,13 @@
+
+
+ 隐私保护提示
+
+ 为了帮你把照片做成拼豆图纸,豆维需要读取你选择的照片、保存图纸到相册。照片仅在本机处理,送检的压缩副本会在内容安全检测后立即删除。使用前请阅读
+ {{contractName}}。
+
+
+
+
+
+
+
diff --git a/BeadQR/miniprogram/components/privacy-popup/index.wxss b/BeadQR/miniprogram/components/privacy-popup/index.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..0310cb844f9df69a543c3477b2353f044a03cd4a
--- /dev/null
+++ b/BeadQR/miniprogram/components/privacy-popup/index.wxss
@@ -0,0 +1,10 @@
+.mask { position: fixed; inset: 0; z-index: 999; background: rgba(15, 23, 22, 0.55); display: flex; align-items: flex-end; }
+.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; }
+.title { font-size: 36rpx; font-weight: 700; color: #1d2b2a; margin-bottom: 20rpx; }
+.desc { font-size: 28rpx; line-height: 1.7; color: #55615c; }
+.link { color: #0f766e; font-weight: 600; }
+.actions { display: flex; gap: 20rpx; margin-top: 40rpx; }
+.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; }
+.btn::after { border: 0; }
+.ghost { background: #f1f3f2; color: #55615c; }
+.primary { background: #0f766e; color: #fff; }
diff --git a/BeadQR/miniprogram/pages/index/index.js b/BeadQR/miniprogram/pages/index/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..59dd120e7362c0a310d02be21431a930cfd65ba3
--- /dev/null
+++ b/BeadQR/miniprogram/pages/index/index.js
@@ -0,0 +1,1073 @@
+// 豆维主页面
+// 大数据(色格、图片、画布节点)一律挂在 this._ 上,不进 setData,保证滑动/输入流畅
+const { MATCH_MODES, PRESETS, preparePalette } = require('../../utils/palette')
+const { makeQr, createPlacement, SAMPLE_TEXT } = require('../../utils/qr')
+const { getImageDimensions, drawCropped, sampleImage, removeSimpleBackground } = require('../../utils/imaging')
+const { computeArtwork } = require('../../utils/engine')
+const { selfCheck } = require('../../utils/scancheck')
+const { renderPreview, patternPageLayout, drawPatternPage, buildInfoPages, drawWatermark } = require('../../utils/pattern')
+const { PdfDoc, planPatternPages, addPatternPages, addImagePage } = require('../../utils/pdf')
+const security = require('../../utils/security')
+const { createIntro } = require('../../utils/intro')
+
+const LEVELS = [
+ { id: 'M', label: 'M', hint: '格子少' },
+ { id: 'Q', label: 'Q', hint: '均衡' },
+ { id: 'H', label: 'H', hint: '最耐磨' },
+]
+const QUIET_OPTIONS = [
+ { id: 0, label: '无' },
+ { id: 1, label: '1 格' },
+ { id: 2, label: '2 格' },
+]
+const BEAD_STYLES = [
+ { id: 'square', label: '方豆' },
+ { id: 'round', label: '圆豆' },
+ { id: 'hollow', label: '中空豆' },
+]
+const GRID_COLORS = ['#0f766e', '#16211f', '#e5484d', '#3b82f6', '#a855f7']
+const POSITIONS = [0, 50, 100]
+const MAX_CANVAS_SIDE = 8000
+const MAX_CANVAS_PIXELS = 16000000
+const CROP_SIZE = 640
+const FILE_PREFIX = 'douwei-'
+
+function fitScale(w, h) {
+ return Math.min(1, MAX_CANVAS_SIDE / w, MAX_CANVAS_SIDE / h, Math.sqrt(MAX_CANVAS_PIXELS / (w * h)))
+}
+
+function formatNumber(n) {
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+}
+
+Page({
+ data: {
+ // 导航
+ statusBarHeight: 20,
+ navHeight: 44,
+ navSolid: false,
+ // 1. 内容
+ text: '',
+ textStatus: 'idle', // idle | checking | pass | risky | error
+ scanImportLoading: false,
+ // 2. 照片
+ hasImage: false,
+ imageThumb: '',
+ imageMeta: '',
+ removeBackground: false,
+ bgTolerance: 22,
+ // 3. 效果
+ qrScale: 100,
+ posX: 50,
+ posY: 50,
+ positions: POSITIONS,
+ modeId: 'balanced',
+ matchModes: MATCH_MODES,
+ advancedOpen: false,
+ fineTuneOpen: false,
+ presets: PRESETS.map((p) => ({ id: p.id, short: p.short, detail: p.detail })),
+ paletteId: '221',
+ coherence: 40,
+ cleanup: true,
+ levels: LEVELS,
+ level: 'H',
+ quietOptions: QUIET_OPTIONS,
+ quiet: 1,
+ // 预览与统计
+ generating: false,
+ isSample: true,
+ showIntro: true, // 无内容时播放动态演示,写下内容后切回真实预览
+ textFocus: false,
+ gridText: '—',
+ beadsText: '—',
+ colorsText: '—',
+ qrGridText: '',
+ scanState: 'idle', // idle | checking | ok | fail
+ usage: [],
+ usageMore: 0,
+ usageExpanded: false,
+ usageAll: [],
+ // 裁切
+ cropVisible: false,
+ cropCheck: 'idle', // checking | pass | risky | error
+ draftFocusX: 50,
+ draftFocusY: 50,
+ draftZoom: 100,
+ draftFlipH: false,
+ draftFlipV: false,
+ draftRemoveBg: false,
+ draftBgTolerance: 22,
+ // 导出
+ exportVisible: false,
+ exportFormat: 'pdf',
+ exportLayout: 'combined',
+ exportQuality: 'hd',
+ beadStyles: BEAD_STYLES,
+ beadStyle: 'square',
+ gridEnabled: true,
+ gridInterval: 10,
+ gridColors: GRID_COLORS,
+ gridColor: '#0f766e',
+ watermark: '',
+ watermarkStatus: 'idle',
+ exporting: false,
+ exportStep: '',
+ exportResult: null, // { kind, files: [{ name, path }] }
+ },
+
+ // —— 生命周期 ——
+
+ onLoad() {
+ const win = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()
+ let navHeight = 44
+ try {
+ const capsule = wx.getMenuButtonBoundingClientRect()
+ if (capsule && capsule.top) navHeight = (capsule.top - win.statusBarHeight) * 2 + capsule.height
+ } catch (e) { /* 使用默认高度 */ }
+ this._dpr = Math.min(3, win.pixelRatio || 2)
+ this._timers = {}
+ this._nodes = {}
+ this._token = 0
+ this.setData({ statusBarHeight: win.statusBarHeight || 20, navHeight })
+ },
+
+ onReady() {
+ wx.createSelectorQuery().in(this)
+ .select('#preview').fields({ node: true, size: true })
+ .select('#work').fields({ node: true })
+ .select('#security').fields({ node: true })
+ .exec((res) => {
+ this._nodes.preview = res[0] && res[0].node
+ this._previewCss = (res[0] && res[0].width) || 300
+ this._nodes.work = res[1] && res[1].node
+ this._nodes.security = res[2] && res[2].node
+ this.loadMpCode()
+ this.schedule(0)
+ })
+ },
+
+ onUnload() {
+ Object.keys(this._timers).forEach((k) => clearTimeout(this._timers[k]))
+ this.stopIntro()
+ },
+
+ onShow() {
+ this._hidden = false
+ if (this._nodes && this._nodes.preview) this.syncIntro()
+ },
+
+ onHide() {
+ this._hidden = true
+ this.pauseIntro()
+ },
+
+ onPageScroll(e) {
+ const solid = e.scrollTop > 40
+ if (solid !== this.data.navSolid) this.setData({ navSolid: solid })
+ // 演示区滚出屏幕后暂停动画,省电
+ const away = e.scrollTop > 650
+ if (away !== this._scrolledAway) {
+ this._scrolledAway = away
+ this.syncIntro()
+ }
+ },
+
+ // 分享的是小程序页面卡片,不携带用户内容
+ onShareAppMessage() {
+ return { title: '把一句话藏进拼豆里,微信一扫就能读出来', path: '/pages/index/index' }
+ },
+ onShareTimeline() {
+ return { title: '豆维 · 可扫码的拼豆图纸生成器' }
+ },
+
+ loadMpCode() {
+ if (!this._nodes.work) return
+ const image = this._nodes.work.createImage()
+ image.onload = () => { this._mpCode = image }
+ image.onerror = () => { this._mpCode = null }
+ image.src = '/assets/miniprogram-code.png'
+ },
+
+ debounce(key, fn, wait) {
+ clearTimeout(this._timers[key])
+ this._timers[key] = setTimeout(fn, wait)
+ },
+
+ noop() {},
+
+ openPrivacy() {
+ if (wx.openPrivacyContract) {
+ wx.openPrivacyContract({ fail: () => wx.showToast({ title: '请在微信内查看隐私保护指引', icon: 'none' }) })
+ }
+ },
+
+ showRules() {
+ wx.showModal({
+ title: '内容规范',
+ content: '二维码内容、水印和照片都会经过微信内容安全检测。请勿输入或上传违法违规、色情低俗、暴力恐怖、侵犯他人权益等内容;检测未通过或检测异常时无法生成与导出。如发现不良内容,可通过页面底部“投诉与反馈”告诉我们。',
+ showCancel: false,
+ confirmText: '我知道了',
+ })
+ },
+
+ // —— 生成管线 ——
+
+ schedule(wait = 120) {
+ this.debounce('run', () => this.run(), wait)
+ },
+
+ run() {
+ if (!this._nodes.preview || !this._nodes.work) return
+ // 没写内容、也没选照片:示例二维码没有意义,改为播放动态演示
+ if (!this.data.text.trim() && !this._source) {
+ this._token += 1
+ this.setData({
+ showIntro: true, generating: false, isSample: true, scanState: 'idle', usage: [], usageAll: [], usageMore: 0,
+ }, () => this.syncIntro())
+ return
+ }
+ this.stopIntro()
+ if (this.data.showIntro) this.setData({ showIntro: false })
+ const token = ++this._token
+ this.setData({ generating: true }, () => {
+ // 先让“生成中”状态渲染一帧,再做同步计算
+ setTimeout(() => {
+ if (token !== this._token) return
+ try {
+ this.compute()
+ this.afterCompute(token)
+ } catch (error) {
+ console.error('生成失败', error)
+ this.setData({ generating: false })
+ wx.showToast({ title: '内容太长或图片异常,请调整后重试', icon: 'none' })
+ }
+ }, 16)
+ })
+ },
+
+ compute() {
+ const d = this.data
+ const text = d.text.trim()
+ const qrKey = `${text}|${d.level}`
+ if (!this._qr || this._qrKey !== qrKey) {
+ this._qr = makeQr(text, d.level)
+ this._qrKey = qrKey
+ }
+ const src = this._source
+ const placement = createPlacement(this._qr.size, {
+ scale: d.qrScale, posX: d.posX, posY: d.posY, quiet: d.quiet, hasImage: Boolean(src),
+ })
+ let base = null
+ if (src) {
+ const c = src.crop
+ const baseKey = `${placement.size}|${c.focusX}|${c.focusY}|${c.zoom}|${c.flipH}|${c.flipV}|${d.removeBackground}|${d.bgTolerance}`
+ if (this._baseKey !== baseKey) {
+ const sampled = sampleImage(this._nodes.work, src.image, placement.size, c)
+ if (d.removeBackground) sampled.mask = removeSimpleBackground(sampled.rgb, placement.size, d.bgTolerance)
+ this._base = sampled
+ this._baseKey = baseKey
+ }
+ base = this._base
+ }
+ const pal = preparePalette(d.paletteId)
+ this._artwork = computeArtwork(base, this._qr, placement, pal, {
+ modeId: d.modeId, coherence: d.coherence, cleanup: d.cleanup,
+ })
+ },
+
+ afterCompute(token) {
+ const art = this._artwork
+ this.drawPreview()
+ const usageAll = art.usage.map((u) => ({ id: u.id, hex: u.hex, count: u.count }))
+ this.setData({
+ generating: false,
+ isSample: this._qr.isSample,
+ gridText: `${art.size}×${art.size}`,
+ beadsText: formatNumber(art.beads),
+ colorsText: String(art.usage.length),
+ qrGridText: `${this._qr.size}×${this._qr.size}`,
+ usage: usageAll.slice(0, 12),
+ usageAll: this.data.usageExpanded ? usageAll : [],
+ usageMore: Math.max(0, usageAll.length - 12),
+ scanState: 'checking',
+ })
+ this._usageAll = usageAll
+ // 扫码自检放到下一轮,避免阻塞预览出图
+ this.debounce('scan', () => {
+ if (token !== this._token) return
+ const expected = this.data.text.trim() || SAMPLE_TEXT
+ const result = selfCheck(art, expected)
+ if (token === this._token) this.setData({ scanState: result.ok ? 'ok' : 'fail' })
+ }, 60)
+ },
+
+ drawPreview() {
+ const node = this._nodes.preview
+ if (!node || !this._artwork) return
+ const px = Math.round(this._previewCss * this._dpr)
+ renderPreview(node, this._artwork, px, this.data.beadStyle)
+ },
+
+ // —— 动态演示 ——
+
+ syncIntro() {
+ const should = this.data.showIntro && !this._hidden && !this._scrolledAway && !this.data.cropVisible
+ if (should) this.startIntro()
+ else this.pauseIntro()
+ },
+
+ startIntro() {
+ if (this._introPlayer) return
+ const node = this._nodes.preview
+ if (!node || !this._nodes.work) return
+ if (!this._intro) {
+ try {
+ this._intro = createIntro({ workCanvas: this._nodes.work })
+ } catch (error) {
+ console.error('演示动画初始化失败', error)
+ return
+ }
+ }
+ const px = Math.round(this._previewCss * this._dpr)
+ node.width = px
+ node.height = px
+ const ctx = node.getContext('2d')
+ const raf = node.requestAnimationFrame ? (f) => node.requestAnimationFrame(f) : (f) => setTimeout(f, 16)
+ const caf = node.cancelAnimationFrame ? (id) => node.cancelAnimationFrame(id) : (id) => clearTimeout(id)
+ const player = { start: Date.now() - (this._introElapsed || 0), last: 0, id: 0, caf }
+ const tick = () => {
+ if (this._introPlayer !== player) return
+ const now = Date.now()
+ if (now - player.last >= 30) { // 约 30 帧/秒,流畅且省电
+ player.last = now
+ this._introElapsed = now - player.start
+ this._intro.draw(ctx, this._introElapsed / 1000, px)
+ }
+ player.id = raf(tick)
+ }
+ this._introPlayer = player
+ tick()
+ },
+
+ pauseIntro() {
+ const player = this._introPlayer
+ if (!player) return
+ this._introPlayer = null
+ player.caf(player.id)
+ },
+
+ stopIntro() {
+ this.pauseIntro()
+ this._introElapsed = 0
+ },
+
+ replayIntro() {
+ if (!this.data.showIntro) return
+ this.stopIntro()
+ this.syncIntro()
+ },
+
+ // 引导用户开始:滚到第 1 步并弹出键盘
+ startCreate() {
+ wx.pageScrollTo({
+ selector: '#step1',
+ offsetTop: -(this.data.statusBarHeight + this.data.navHeight + 12),
+ duration: 300,
+ })
+ setTimeout(() => this.setData({ textFocus: true }), 320)
+ },
+
+ onTextBlur() {
+ if (this.data.textFocus) this.setData({ textFocus: false })
+ },
+
+ // —— 1. 内容 ——
+
+ onTextInput(e) {
+ const text = e.detail.value
+ this.setData({ text, textStatus: text.trim() ? 'checking' : 'idle' })
+ this.schedule(220)
+ this.debounce('textCheck', () => this.verifyText(text), 650)
+ },
+
+ clearText() {
+ this.setData({ text: '', textStatus: 'idle' })
+ this.schedule(0)
+ },
+
+ verifyText(raw) {
+ const value = String(raw || '').trim()
+ if (!value) return
+ security.checkText(value, 2).then(({ status }) => {
+ if (this.data.text.trim() !== value) return
+ this.setData({ textStatus: status })
+ if (status === 'risky') wx.showToast({ title: security.MESSAGES.risky, icon: 'none' })
+ })
+ },
+
+ scanImport() {
+ wx.scanCode({
+ onlyFromCamera: false,
+ scanType: ['qrCode'],
+ success: (res) => {
+ const decoded = String(res.result || '').trim().slice(0, 120)
+ if (!decoded) { wx.showToast({ title: '二维码里没有内容', icon: 'none' }); return }
+ this.setData({ scanImportLoading: true })
+ // 扫码得到的内容同样先过内容安全检测,通过后才填入
+ security.checkText(decoded, 2).then(({ status }) => {
+ this.setData({ scanImportLoading: false })
+ if (status !== 'pass') { wx.showToast({ title: security.MESSAGES[status], icon: 'none' }); return }
+ this.setData({ text: decoded, textStatus: 'pass' })
+ this.schedule(0)
+ wx.showToast({ title: '已导入二维码内容', icon: 'none' })
+ })
+ },
+ fail: (err) => {
+ if (err && /cancel/i.test(err.errMsg || '')) return
+ wx.showToast({ title: '没有识别到二维码', icon: 'none' })
+ },
+ })
+ },
+
+ // —— 2. 照片 ——
+
+ loadImage(path, info) {
+ return new Promise((resolve, reject) => {
+ const image = this._nodes.work.createImage()
+ image.onload = () => {
+ if (info && info.width) { image._w = info.width; image._h = info.height }
+ resolve(image)
+ }
+ image.onerror = () => reject(new Error('IMAGE_LOAD_FAILED'))
+ image.src = path
+ })
+ },
+
+ chooseImage() {
+ wx.chooseMedia({
+ count: 1,
+ mediaType: ['image'],
+ sizeType: ['compressed'],
+ sourceType: ['album', 'camera'],
+ success: (res) => {
+ const file = res.tempFiles && res.tempFiles[0]
+ if (file && file.tempFilePath) this.prepareImage(file.tempFilePath)
+ },
+ fail: (err) => {
+ if (err && /cancel/i.test(err.errMsg || '')) return
+ wx.showToast({ title: '没有选到照片', icon: 'none' })
+ },
+ })
+ },
+
+ prepareImage(path) {
+ wx.showLoading({ title: '读取照片…', mask: true })
+ new Promise((resolve) => {
+ wx.getImageInfo({ src: path, success: resolve, fail: () => resolve(null) })
+ })
+ .then((info) => this.loadImage(path, info).then((image) => ({ image, info })))
+ .then(({ image }) => {
+ wx.hideLoading()
+ this._pending = { image, path }
+ // 选图后立即在后台做内容安全检测;同时打开构图编辑,检测通过后才能使用
+ this.startImageCheck(this._pending)
+ this.openCrop(true)
+ })
+ .catch(() => {
+ wx.hideLoading()
+ wx.showToast({ title: '照片读取失败,请换一张 JPG / PNG', icon: 'none' })
+ })
+ },
+
+ startImageCheck(pending) {
+ pending.status = 'checking'
+ this.setData({ cropCheck: 'checking' })
+ pending.check = security.checkImage(pending.path, pending.image, this._nodes.security).then(({ status }) => {
+ pending.status = status
+ if (this._pending === pending) {
+ this.setData({ cropCheck: status })
+ if (pending.applyWhenPassed && status === 'pass') this.applyCrop()
+ pending.applyWhenPassed = false
+ }
+ return status
+ })
+ },
+
+ retryImageCheck() {
+ if (this._pending) this.startImageCheck(this._pending)
+ },
+
+ editImage() {
+ if (!this._source) return
+ this._pending = { image: this._source.image, path: this._source.path, status: 'pass' }
+ this.setData({ cropCheck: 'pass' })
+ this.openCrop(false)
+ },
+
+ removeImage() {
+ this._source = null
+ this._base = null
+ this._baseKey = ''
+ this.setData({ hasImage: false, imageThumb: '', imageMeta: '', removeBackground: false })
+ this.schedule(0)
+ },
+
+ // —— 裁切编辑 ——
+
+ openCrop(isNew) {
+ const crop = !isNew && this._source ? this._source.crop : null
+ this.pauseIntro()
+ this.setData({
+ cropVisible: true,
+ draftFocusX: crop ? crop.focusX : 50,
+ draftFocusY: crop ? crop.focusY : 50,
+ draftZoom: crop ? Math.round(crop.zoom * 100) : 100,
+ draftFlipH: crop ? crop.flipH : false,
+ draftFlipV: crop ? crop.flipV : false,
+ draftRemoveBg: isNew ? false : this.data.removeBackground,
+ draftBgTolerance: isNew ? 22 : this.data.bgTolerance,
+ }, () => {
+ wx.createSelectorQuery().in(this)
+ .select('#crop').fields({ node: true, size: true })
+ .select('#crop').boundingClientRect()
+ .exec((res) => {
+ this._nodes.crop = res[0] && res[0].node
+ this._cropCss = (res[0] && res[0].width) || 300
+ this._cropRect = res[1] || { left: 0, top: 0 }
+ this.drawCrop()
+ })
+ })
+ },
+
+ drawCrop() {
+ const node = this._nodes.crop
+ const pending = this._pending
+ if (!node || !pending) return
+ node.width = CROP_SIZE
+ node.height = CROP_SIZE
+ const ctx = node.getContext('2d')
+ const d = this.data
+ ctx.imageSmoothingEnabled = true
+ drawCropped(ctx, pending.image, CROP_SIZE, CROP_SIZE, {
+ focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV,
+ })
+ // 三分线,仅辅助构图
+ ctx.strokeStyle = 'rgba(255,255,255,0.5)'
+ ctx.lineWidth = 1.5
+ ctx.beginPath()
+ for (let i = 1; i < 3; i += 1) {
+ const p = Math.round(CROP_SIZE * i / 3) + 0.5
+ ctx.moveTo(p, 0); ctx.lineTo(p, CROP_SIZE)
+ ctx.moveTo(0, p); ctx.lineTo(CROP_SIZE, p)
+ }
+ ctx.stroke()
+ },
+
+ touchPoint(t) {
+ const rect = this._cropRect || { left: 0, top: 0 }
+ const x = t.clientX == null ? (t.pageX == null ? t.x : t.pageX) : t.clientX
+ const y = t.clientY == null ? (t.pageY == null ? t.y : t.pageY) : t.clientY
+ return { x: x - rect.left, y: y - rect.top }
+ },
+
+ touchDistance(touches) {
+ const a = this.touchPoint(touches[0])
+ const b = this.touchPoint(touches[1])
+ return Math.hypot(b.x - a.x, b.y - a.y)
+ },
+
+ onCropStart(e) {
+ const touches = e.touches || []
+ if (!touches[0]) return
+ if (touches.length >= 2) {
+ this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom }
+ return
+ }
+ this._gesture = { type: 'drag', ...this.touchPoint(touches[0]), focusX: this.data.draftFocusX, focusY: this.data.draftFocusY }
+ },
+
+ onCropMove(e) {
+ const touches = e.touches || []
+ const g = this._gesture
+ const pending = this._pending
+ if (!touches[0] || !g || !pending) return
+ if (touches.length >= 2) {
+ if (g.type !== 'pinch') {
+ this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom }
+ return
+ }
+ const zoom = Math.max(100, Math.min(400, Math.round(g.zoom * this.touchDistance(touches) / g.distance)))
+ this.setData({ draftZoom: zoom })
+ this.debounce('crop', () => this.drawCrop(), 16)
+ return
+ }
+ if (g.type !== 'drag') return
+ const p = this.touchPoint(touches[0])
+ const cssScale = CROP_SIZE / (this._cropCss || CROP_SIZE)
+ const src = getImageDimensions(pending.image)
+ const scale = Math.max(CROP_SIZE / src.width, CROP_SIZE / src.height) * (this.data.draftZoom / 100)
+ const w = src.width * scale
+ const h = src.height * scale
+ const clamp = (v) => Math.max(0, Math.min(100, v))
+ const dirX = this.data.draftFlipH ? -1 : 1
+ const dirY = this.data.draftFlipV ? -1 : 1
+ const fx = Math.abs(w - CROP_SIZE) < 0.01 ? g.focusX : clamp(g.focusX + dirX * ((p.x - g.x) * cssScale / (CROP_SIZE - w)) * 100)
+ const fy = Math.abs(h - CROP_SIZE) < 0.01 ? g.focusY : clamp(g.focusY + dirY * ((p.y - g.y) * cssScale / (CROP_SIZE - h)) * 100)
+ this.setData({ draftFocusX: fx, draftFocusY: fy })
+ this.debounce('crop', () => this.drawCrop(), 16)
+ },
+
+ onCropEnd() {
+ this._gesture = null
+ clearTimeout(this._timers.crop)
+ this.drawCrop()
+ },
+
+ onDraftZoom(e) {
+ this.setData({ draftZoom: Number(e.detail.value) })
+ this.debounce('crop', () => this.drawCrop(), 16)
+ },
+ resetCrop() { this.setData({ draftFocusX: 50, draftFocusY: 50, draftZoom: 100 }, () => this.drawCrop()) },
+ flipH() { this.setData({ draftFlipH: !this.data.draftFlipH }, () => this.drawCrop()) },
+ flipV() { this.setData({ draftFlipV: !this.data.draftFlipV }, () => this.drawCrop()) },
+ onDraftRemoveBg(e) { this.setData({ draftRemoveBg: e.detail.value }) },
+ onDraftBgTolerance(e) { this.setData({ draftBgTolerance: Number(e.detail.value) }) },
+
+ cancelCrop() {
+ if (this._pending) this._pending.applyWhenPassed = false
+ this.setData({ cropVisible: false }, () => this.syncIntro())
+ },
+
+ applyCrop() {
+ const pending = this._pending
+ if (!pending) return
+ if (pending.status === 'checking') {
+ // 检测未完成:记住用户意图,通过后自动应用
+ pending.applyWhenPassed = true
+ wx.showToast({ title: '正在做内容安全检测,通过后自动应用', icon: 'none' })
+ return
+ }
+ if (pending.status !== 'pass') {
+ wx.showToast({ title: security.MESSAGES[pending.status] || security.MESSAGES.error, icon: 'none' })
+ return
+ }
+ const d = this.data
+ const src = getImageDimensions(pending.image)
+ this._source = {
+ image: pending.image,
+ path: pending.path,
+ crop: { focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV },
+ }
+ this._baseKey = ''
+ this.setData({
+ cropVisible: false,
+ hasImage: true,
+ imageThumb: pending.path,
+ imageMeta: `${src.width}×${src.height} · 已通过安全检测`,
+ removeBackground: d.draftRemoveBg,
+ bgTolerance: d.draftBgTolerance,
+ // 首次导入照片时,二维码默认铺满全图;用户可再缩小
+ })
+ this.schedule(0)
+ },
+
+ // —— 3. 效果 ——
+
+ onScaleChanging(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(160) },
+ onScaleChange(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(0) },
+ pickPosition(e) {
+ const { x, y } = e.currentTarget.dataset
+ this.setData({ posX: Number(x), posY: Number(y) })
+ this.schedule(0)
+ },
+ toggleFineTune() { this.setData({ fineTuneOpen: !this.data.fineTuneOpen }) },
+ onPosX(e) { this.setData({ posX: Number(e.detail.value) }); this.schedule(100) },
+ onPosY(e) { this.setData({ posY: Number(e.detail.value) }); this.schedule(100) },
+ pickMode(e) { this.setData({ modeId: e.currentTarget.dataset.id }); this.schedule(0) },
+ toggleAdvanced() { this.setData({ advancedOpen: !this.data.advancedOpen }) },
+ pickPalette(e) { this.setData({ paletteId: e.currentTarget.dataset.id }); this.schedule(0) },
+ onCoherence(e) { this.setData({ coherence: Number(e.detail.value) }); this.schedule(200) },
+ onCleanup(e) { this.setData({ cleanup: e.detail.value }); this.schedule(0) },
+ pickLevel(e) { this.setData({ level: e.currentTarget.dataset.id }); this.schedule(0) },
+ pickQuiet(e) { this.setData({ quiet: Number(e.currentTarget.dataset.id) }); this.schedule(0) },
+ onBgToggle(e) { this.setData({ removeBackground: e.detail.value }); this.schedule(0) },
+
+ applyScanFix() {
+ // 自检未通过时的一键修复:扫码优先 + 1 格白边 + H 级纠错
+ this.setData({ modeId: 'strict', quiet: Math.max(1, this.data.quiet), level: 'H' })
+ this.schedule(0)
+ wx.showToast({ title: '已切换为扫码优先', icon: 'none' })
+ },
+
+ toggleUsage() {
+ const expanded = !this.data.usageExpanded
+ this.setData({ usageExpanded: expanded, usageAll: expanded ? (this._usageAll || []) : [] })
+ },
+
+ // 生成当前预览图,用微信“识别图中二维码”实测
+ verifyScan() {
+ const node = this._nodes.preview
+ if (!node) return
+ wx.canvasToTempFilePath({
+ canvas: node,
+ fileType: 'png',
+ success: (res) => {
+ wx.previewImage({ urls: [res.tempFilePath], showmenu: true })
+ wx.showToast({ title: '长按图片,选择“识别图中二维码”', icon: 'none', duration: 2500 })
+ },
+ fail: () => wx.showToast({ title: '预览生成失败,请重试', icon: 'none' }),
+ })
+ },
+
+ // —— 导出 ——
+
+ openExport() {
+ if (!this.data.text.trim()) {
+ this.startCreate()
+ return
+ }
+ if (!this._artwork) return
+ if (this.data.textStatus === 'risky') {
+ wx.showToast({ title: security.MESSAGES.risky, icon: 'none' })
+ return
+ }
+ this.setData({ exportVisible: true, exportResult: null })
+ },
+ closeExport() {
+ if (this.data.exporting) return
+ this.setData({ exportVisible: false })
+ },
+ pickFormat(e) { this.setData({ exportFormat: e.currentTarget.dataset.id, exportResult: null }) },
+ pickLayout(e) { this.setData({ exportLayout: e.currentTarget.dataset.id, exportResult: null }) },
+ pickQuality(e) { this.setData({ exportQuality: e.currentTarget.dataset.id, exportResult: null }) },
+ pickBeadStyle(e) {
+ this.setData({ beadStyle: e.currentTarget.dataset.id, exportResult: null }, () => this.drawPreview())
+ },
+ onGridToggle(e) { this.setData({ gridEnabled: e.detail.value, exportResult: null }) },
+ pickInterval(e) { this.setData({ gridInterval: Number(e.currentTarget.dataset.id), exportResult: null }) },
+ pickGridColor(e) { this.setData({ gridColor: e.currentTarget.dataset.color, exportResult: null }) },
+ onWatermark(e) {
+ const watermark = e.detail.value
+ this.setData({ watermark, watermarkStatus: watermark.trim() ? 'checking' : 'idle', exportResult: null })
+ this.debounce('wmCheck', () => {
+ const value = watermark.trim()
+ if (!value) return
+ security.checkText(value, 1).then(({ status }) => {
+ if (this.data.watermark.trim() === value) this.setData({ watermarkStatus: status })
+ })
+ }, 650)
+ },
+
+ gridSettings() {
+ return { enabled: this.data.gridEnabled, interval: this.data.gridInterval, color: this.data.gridColor, opacity: 60 }
+ },
+
+ capture(fileType, quality) {
+ return new Promise((resolve, reject) => {
+ wx.canvasToTempFilePath({
+ canvas: this._nodes.work,
+ fileType,
+ quality,
+ success: (res) => resolve(res.tempFilePath),
+ fail: reject,
+ })
+ })
+ },
+
+ step(label) {
+ return new Promise((resolve) => this.setData({ exportStep: label }, () => setTimeout(resolve, 16)))
+ },
+
+ async startExport() {
+ if (this.data.exporting || !this._artwork) return
+ this.setData({ exporting: true, exportResult: null })
+ try {
+ // 平台合规:导出前再次确认所有用户文本均已通过内容安全检测(已通过的走缓存,无额外等待)
+ await this.step('内容安全校验…')
+ const check = await security.checkTexts([
+ { value: this.data.text, scene: 2 },
+ { value: this.data.watermark, scene: 1 },
+ ])
+ if (check.status !== 'pass') {
+ wx.showToast({ title: security.MESSAGES[check.status], icon: 'none' })
+ if (check.status === 'risky') this.setData({ textStatus: security.isTextPassed(this.data.text) ? 'pass' : 'risky' })
+ return
+ }
+ if (this._source && !this.data.hasImage) throw new Error('IMAGE_STATE')
+ this.cleanOldFiles()
+ if (this.data.exportFormat === 'png') await this.exportPng()
+ else await this.exportPdf()
+ } catch (error) {
+ console.error('导出失败', error)
+ wx.showToast({ title: '导出失败,请重试(可尝试“标准”清晰度)', icon: 'none' })
+ } finally {
+ this.setData({ exporting: false, exportStep: '' })
+ }
+ },
+
+ // 按清晰度求每格像素:高清长边约 4000px,标准约 2400px
+ pngCellSize() {
+ const size = this._artwork.size
+ const maxSide = this.data.exportQuality === 'hd' ? 4000 : 2400
+ let cell = Math.max(12, Math.min(72, Math.floor(maxSide / (size + 5))))
+ while (cell > 12) {
+ const L = patternPageLayout(size, cell)
+ if (Math.max(L.width, L.height) <= maxSide) break
+ cell -= 1
+ }
+ return cell
+ },
+
+ patternMeta() {
+ const art = this._artwork
+ return `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${art.pal.label} · 扫码内容:${this.data.text.trim()}`
+ },
+
+ infoParams(width) {
+ return {
+ artwork: this._artwork,
+ width,
+ text: this.data.text.trim(),
+ paletteLabel: `${this._artwork.pal.label}(本作品用 ${this._artwork.usage.length} 色)`,
+ mpCode: this._mpCode || null,
+ thumbnail: true,
+ }
+ },
+
+ async exportPng() {
+ const node = this._nodes.work
+ const art = this._artwork
+ const cell = this.pngCellSize()
+ const L = patternPageLayout(art.size, cell)
+ const wm = this.data.watermark.trim()
+ const drawPattern = (ctx) => drawPatternPage(ctx, art, { cell, style: this.data.beadStyle, grid: this.gridSettings(), meta: this.patternMeta() })
+ const files = []
+ if (this.data.exportLayout === 'separate') {
+ await this.step('绘制图纸…')
+ node.width = L.width
+ node.height = L.height
+ const ctx = node.getContext('2d')
+ drawPattern(ctx)
+ drawWatermark(ctx, L.width, L.height, wm)
+ files.push({ name: '拼豆图纸', path: await this.capture('png') })
+ const info = buildInfoPages(this.infoParams(this.data.exportQuality === 'hd' ? 2000 : 1400))
+ for (let p = 0; p < info.pageCount; p += 1) {
+ await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`)
+ node.width = info.width
+ node.height = info.height
+ const c = node.getContext('2d')
+ info.draw(c, p)
+ drawWatermark(c, info.width, info.height, wm)
+ files.push({ name: info.pageCount > 1 ? `用豆清单 ${p + 1}` : '用豆清单', path: await this.capture('png') })
+ }
+ } else {
+ await this.step('绘制图纸与清单…')
+ const info = buildInfoPages(this.infoParams(L.width))
+ const gap = Math.round(L.width * 0.02)
+ const totalH = L.height + info.pageCount * (info.height + gap)
+ const scale = fitScale(L.width, totalH)
+ node.width = Math.round(L.width * scale)
+ node.height = Math.round(totalH * scale)
+ const ctx = node.getContext('2d')
+ ctx.fillStyle = '#e8e5de'
+ ctx.fillRect(0, 0, node.width, node.height)
+ ctx.scale(scale, scale)
+ drawPattern(ctx)
+ for (let p = 0; p < info.pageCount; p += 1) {
+ ctx.save()
+ ctx.translate(0, L.height + gap + p * (info.height + gap))
+ info.draw(ctx, p)
+ ctx.restore()
+ }
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
+ drawWatermark(ctx, node.width, node.height, wm)
+ files.push({ name: '图纸 + 用豆清单', path: await this.capture('png') })
+ }
+ await this.step('保存到相册…')
+ const saved = await this.saveImages(files.map((f) => f.path))
+ this.setData({ exportResult: { kind: 'png', saved, files } })
+ if (saved) wx.showToast({ title: `${files.length} 张图片已保存`, icon: 'success' })
+ },
+
+ saveImages(paths) {
+ return paths.reduce((chain, path) => chain.then((ok) => (ok ? new Promise((resolve) => {
+ wx.saveImageToPhotosAlbum({
+ filePath: path,
+ success: () => resolve(true),
+ fail: (err) => {
+ if (err && /auth|deny/i.test(err.errMsg || '')) {
+ wx.showModal({
+ title: '需要相册权限',
+ content: '请允许保存到相册,才能把图纸存到手机里',
+ confirmText: '去设置',
+ success: (r) => { if (r.confirm) wx.openSetting() },
+ })
+ }
+ resolve(false)
+ },
+ })
+ }) : false)), Promise.resolve(true))
+ },
+
+ previewResult() {
+ const result = this.data.exportResult
+ if (!result) return
+ if (result.kind === 'png') {
+ wx.previewImage({ urls: result.files.map((f) => f.path), showmenu: true })
+ } else {
+ this.openPdf(result.files[0].path)
+ }
+ },
+
+ openResultFile(e) {
+ const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)]
+ if (!file) return
+ if (this.data.exportResult.kind === 'png') wx.previewImage({ urls: [file.path], showmenu: true })
+ else this.openPdf(file.path)
+ },
+
+ shareResultFile(e) {
+ const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)]
+ if (!file || !wx.shareFileMessage) {
+ wx.showToast({ title: '当前微信版本不支持,请先打开再转发', icon: 'none' })
+ return
+ }
+ wx.shareFileMessage({ filePath: file.path, fileName: file.fileName || `${file.name}.pdf` })
+ },
+
+ openPdf(path) {
+ wx.openDocument({ filePath: path, fileType: 'pdf', showMenu: true })
+ },
+
+ cleanOldFiles() {
+ try {
+ const fs = wx.getFileSystemManager()
+ fs.readdirSync(wx.env.USER_DATA_PATH)
+ .filter((name) => name.indexOf(FILE_PREFIX) === 0)
+ .forEach((name) => { try { fs.unlinkSync(`${wx.env.USER_DATA_PATH}/${name}`) } catch (e) { /* 忽略 */ } })
+ } catch (e) { /* 忽略 */ }
+ },
+
+ async canvasJpeg(width, height, draw, quality = 0.9) {
+ const node = this._nodes.work
+ node.width = width
+ node.height = height
+ const ctx = node.getContext('2d')
+ draw(ctx)
+ const path = await this.capture('jpg', quality)
+ return wx.getFileSystemManager().readFileSync(path)
+ },
+
+ // 页眉(中文)渲染为小图,矢量页面上方使用
+ async renderHeader(title, sub) {
+ const w = 1540
+ const h = 80
+ const data = await this.canvasJpeg(w, h, (ctx) => {
+ ctx.fillStyle = '#ffffff'
+ ctx.fillRect(0, 0, w, h)
+ ctx.fillStyle = '#16211f'
+ ctx.font = 'bold 34px sans-serif'
+ ctx.textBaseline = 'middle'
+ ctx.fillText(title, 0, 30)
+ ctx.fillStyle = '#6f7a76'
+ ctx.font = '22px sans-serif'
+ ctx.fillText(sub, 0, 64)
+ ctx.fillStyle = '#0f766e'
+ ctx.font = 'bold 24px sans-serif'
+ ctx.textAlign = 'right'
+ ctx.fillText('豆维 · 可扫码的拼豆', w, 30)
+ }, 0.92)
+ return { data, width: w, height: h }
+ },
+
+ // 水印转为 1-bit 蒙版,矢量页上以半透明平铺
+ watermarkMask(doc, text) {
+ const value = String(text || '').trim()
+ if (!value) return null
+ const node = this._nodes.work
+ const fontSize = 64
+ node.width = 16
+ node.height = 16
+ let ctx = node.getContext('2d')
+ ctx.font = `bold ${fontSize}px sans-serif`
+ const width = Math.min(2000, Math.ceil(ctx.measureText(value).width) + 16)
+ const height = Math.round(fontSize * 1.4)
+ node.width = width
+ node.height = height
+ ctx = node.getContext('2d')
+ ctx.clearRect(0, 0, width, height)
+ ctx.fillStyle = '#000000'
+ ctx.font = `bold ${fontSize}px sans-serif`
+ ctx.textBaseline = 'middle'
+ ctx.fillText(value, 8, height / 2)
+ const pixels = ctx.getImageData(0, 0, width, height).data
+ const rowBytes = Math.ceil(width / 8)
+ const bits = new Uint8Array(rowBytes * height).fill(0xff) // 1 = 不绘制
+ for (let y = 0; y < height; y += 1) {
+ for (let x = 0; x < width; x += 1) {
+ if (pixels[(y * width + x) * 4 + 3] > 110) bits[y * rowBytes + (x >> 3)] &= ~(0x80 >> (x & 7))
+ }
+ }
+ return { id: doc.addImageMask(bits, width, height), width, height }
+ },
+
+ async buildPatternPdf(doc) {
+ const art = this._artwork
+ const plan = planPatternPages(art.size)
+ const headers = []
+ for (let i = 0; i < plan.pages.length; i += 1) {
+ const page = plan.pages[i]
+ await this.step(`绘制图纸页 ${i + 1}/${plan.pages.length}…`)
+ const title = page.kind === 'overview'
+ ? (plan.tiled ? `图纸总览 · 共分 ${plan.pages.length - 1} 块` : '拼豆图纸')
+ : `第 ${page.tile} 块 · 列 ${page.region.col + 1}–${page.region.col + page.region.cols} · 行 ${page.region.row + 1}–${page.region.row + page.region.rows}`
+ const sub = page.kind === 'overview'
+ ? `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${plan.tiled ? '红框编号对应后续分块页,按块拼更轻松' : '格内为 MARD 色号'}`
+ : `第 ${i + 1} / ${plan.pages.length} 页 · 格内为 MARD 色号 · 坐标为整张作品的行列号`
+ const header = await this.renderHeader(title, sub)
+ headers.push({ id: doc.addJpeg(header.data, header.width, header.height), width: header.width, height: header.height })
+ }
+ const watermark = this.watermarkMask(doc, this.data.watermark)
+ addPatternPages(doc, art, { plan, headers, style: this.data.beadStyle, grid: this.gridSettings(), watermark })
+ },
+
+ async buildInfoPdf(doc) {
+ const info = buildInfoPages(this.infoParams(1654))
+ for (let p = 0; p < info.pageCount; p += 1) {
+ await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`)
+ const data = await this.canvasJpeg(info.width, info.height, (ctx) => {
+ info.draw(ctx, p)
+ drawWatermark(ctx, info.width, info.height, this.data.watermark)
+ }, 0.9)
+ addImagePage(doc, doc.addJpeg(data, info.width, info.height), info.width, info.height)
+ }
+ },
+
+ async exportPdf() {
+ const fs = wx.getFileSystemManager()
+ const stamp = Date.now().toString(36)
+ const art = this._artwork
+ const base = `${FILE_PREFIX}${art.size}x${art.size}-${stamp}`
+ const files = []
+ const write = (doc, suffix, name) => {
+ const fileName = `豆维-${name}.pdf`
+ const path = `${wx.env.USER_DATA_PATH}/${base}-${suffix}.pdf`
+ fs.writeFileSync(path, doc.build(), 'binary')
+ files.push({ name, fileName, path })
+ }
+ if (this.data.exportLayout === 'separate') {
+ const patternDoc = new PdfDoc()
+ await this.buildPatternPdf(patternDoc)
+ write(patternDoc, 'pattern', '拼豆图纸')
+ const infoDoc = new PdfDoc()
+ await this.buildInfoPdf(infoDoc)
+ write(infoDoc, 'info', '用豆清单')
+ } else {
+ const doc = new PdfDoc()
+ await this.buildPatternPdf(doc)
+ await this.buildInfoPdf(doc)
+ write(doc, 'all', '图纸与用豆清单')
+ }
+ this.setData({ exportResult: { kind: 'pdf', saved: true, files } })
+ },
+})
diff --git a/BeadQR/miniprogram/pages/index/index.json b/BeadQR/miniprogram/pages/index/index.json
new file mode 100644
index 0000000000000000000000000000000000000000..3a164a5de25f9e3973d37a973c87fb807ef30625
--- /dev/null
+++ b/BeadQR/miniprogram/pages/index/index.json
@@ -0,0 +1,6 @@
+{
+ "navigationStyle": "custom",
+ "navigationBarTextStyle": "white",
+ "backgroundColorTop": "#0f3d3a",
+ "usingComponents": {}
+}
diff --git a/BeadQR/miniprogram/pages/index/index.wxml b/BeadQR/miniprogram/pages/index/index.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..7234757764c44cf005156ba6875b418f5495f838
--- /dev/null
+++ b/BeadQR/miniprogram/pages/index/index.wxml
@@ -0,0 +1,403 @@
+
+
+
+
+ 豆维 · 可扫码的拼豆
+
+
+
+
+
+
+ MARD 拼豆图纸生成器
+ 把一句话藏进拼豆里
+ 拼好的作品用微信一扫,就能读出你写的文字或链接
+
+ 1写一句话
+ ›
+ 2选张照片
+ ›
+ 3导出图纸
+
+
+
+
+
+
+
+ 示例二维码 · 写下内容后换成你的
+
+
+
+
+
+
+ 上面是演示:写下一句话,这里就会变成你专属的可扫码图纸
+
+
+ 221 色MARD 标准色号
+ A4打印图纸
+ 扫一扫微信直接读出
+
+
+ 开始制作 · 写下第一句话
+
+
+
+
+
+
+ 写下内容后,自动检查能否扫出
+ 正在检查能否扫出…
+ ✓ 自检通过,可以扫出内容
+ ! 自检未扫出,建议优化
+ 生成后自动检查能否扫码
+
+ 一键优化
+
+
+
+ {{gridText}}格子
+ {{beadsText}}颗豆子
+ {{colorsText}}种颜色
+
+
+
+ 写下内容,生成你的二维码
+ 用微信扫一扫验证
+
+
+
+
+
+
+
+ 1
+
+ 扫码后要看到什么?
+ 一句祝福、一个网址,或者你的名字
+
+
+
+
+
+
+ 字越少,格子越少,拼起来越快
+
+
+
+
+
+ 2
+
+ 选一张照片当底图 可选
+ 二维码会融进照片里,不选就是纯二维码
+
+
+
+ +
+ 从相册选择或拍照
+ 照片仅在手机上处理,会先经过微信内容安全检测
+
+
+
+
+ {{imageMeta}}
+
+ 调整
+ 换一张
+ 移除
+
+
+
+
+ 去掉纯色背景
+
+
+
+
+
+
+
+ 3
+
+ 调整效果
+ 默认设置已经很好用,想要更好看再微调
+
+
+
+ 风格
+
+
+ {{item.label}}
+ {{item.hint}}
+
+
+
+
+ 二维码大小 {{qrScale}}%
+
+
+ 二维码位置 {{fineTuneOpen ? '收起微调' : '微调'}}
+
+
+
+
+
+
+ 左右 {{posX}}%
+
+ 上下 {{posY}}%
+
+
+
+
+
+
+ 高级设置(色板、纠错、白边)
+ ›
+
+
+ 色板 颜色越少,买豆越省
+
+
+
+ {{item.short}}
+ {{item.detail}}
+
+
+
+
+ 色块平滑 {{coherence}}
+
+
+
+
+ 合并零散颜色
+ 把只用到一两颗的颜色换成相近色
+
+
+
+
+ 纠错等级 等级越高越耐磨损,但格子更多
+
+
+ {{item.label}}
+ {{item.hint}}
+
+
+
+ 白边 留 1 格白边更容易扫出
+
+
+ {{item.label}}
+
+
+ 二维码本体 {{qrGridText}} 格
+
+
+
+
+
+ 需要准备的豆子 MARD 色号 · 按用量排序
+
+
+
+ {{item.id}}
+ {{item.count}}
+
+
+ {{usageExpanded ? '收起' : '展开全部(还有 ' + usageMore + ' 种)'}}
+
+
+
+
+
+
+
+
+
+
+ 还没有内容
+ 写一句话,马上生成图纸
+
+
+ {{gridText}} · {{colorsText}} 色
+ 约 {{beadsText}} 颗豆子
+
+
+ {{isSample ? '开始制作' : '导出图纸'}}
+
+
+
+
+
+
+ 导出图纸
+
+
+ 格式
+
+
+ PDF 文件A4 打印,大图自动分页
+
+
+ 图片存到相册,手机上看
+
+
+
+ 内容
+
+
+ 合并一份图纸 + 用豆清单
+
+
+ 分开两份图纸、清单各一份
+
+
+
+
+ 清晰度
+
+ 高清
+ 标准
+
+
+
+ 豆子样式
+
+
+ {{item.label}}
+
+
+
+
+ 辅助网格线
+
+
+
+
+
+ 每 5 格
+ 每 10 格
+
+
+
+
+
+
+
+ 水印 可选,防止图纸被盗用
+
+
+
+
+ 内容安全检测中…
+ ✓ 已通过内容安全检测
+ 水印内容可能不合规,请修改
+ 检测暂不可用,请稍后重试
+
+
+
+
+
+
+
+
+ {{exportResult.kind === 'png' && !exportResult.saved ? '已生成,但未存入相册' : '导出完成'}}
+ 共 {{exportResult.files.length}} 份 PDF,可直接打印或发给好友
+ {{exportResult.files.length}} 张图片已保存到手机相册
+ 点「查看」后长按图片即可保存
+
+
+
+
+ {{exportResult.kind === 'pdf' ? 'PDF' : 'PNG'}}
+
+ {{item.name}}
+ {{exportResult.kind === 'pdf' ? 'A4 · 可打印' : '高清图片'}}
+
+
+ {{exportResult.kind === 'png' ? '查看' : '打开'}}
+ 发送
+
+
+
+
+
+
+
+ {{exporting ? (exportStep || '正在导出…') : (exportResult ? '重新导出' : '开始导出')}}
+
+
+
+
+
+
+
+
+ 取消
+ 调整照片
+ 完成
+
+
+
+
+
+ 正在进行内容安全检测,你可以先调整构图…
+ ✓ 照片已通过内容安全检测
+ 这张照片未通过内容安全检测,请换一张
+ 检测暂时不可用 重试
+
+ 单指拖动调整位置,双指缩放
+ 缩放 {{draftZoom}}%
+
+
+ 左右翻转
+ 上下翻转
+ 复位
+
+
+
+ 去掉纯色背景
+ 适合白底、纯色底的照片
+
+
+
+
+ 识别范围 {{draftBgTolerance}}
+
+
+ 换一张照片
+
+
+
+
+
+
+
+
+
+
diff --git a/BeadQR/miniprogram/pages/index/index.wxss b/BeadQR/miniprogram/pages/index/index.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..6fc68c6752807090b71567e745a932adfc29ad3a
--- /dev/null
+++ b/BeadQR/miniprogram/pages/index/index.wxss
@@ -0,0 +1,828 @@
+page {
+ background: var(--bg);
+}
+
+/* 导航 */
+.nav {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 50;
+ display: flex;
+ align-items: center;
+ padding-left: 32rpx;
+ box-sizing: content-box;
+ background: transparent;
+ transition: background 0.2s;
+}
+.nav-solid {
+ background: var(--brand-deep);
+ box-shadow: 0 4rpx 20rpx rgba(15, 61, 58, 0.25);
+}
+.nav-title {
+ display: flex;
+ align-items: center;
+ gap: 14rpx;
+ color: #fff;
+ font-size: 34rpx;
+ font-weight: 700;
+ letter-spacing: 2rpx;
+}
+.nav-logo {
+ width: 48rpx;
+ height: 48rpx;
+ border-radius: 12rpx;
+}
+
+.page {
+ padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
+ background: linear-gradient(180deg, var(--brand-deep) 0, var(--brand-deep) 560rpx, var(--bg) 560rpx);
+}
+
+/* 首屏 */
+.hero {
+ padding: 24rpx 40rpx 150rpx;
+ color: #fff;
+}
+.hero-badge {
+ display: inline-block;
+ padding: 6rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(255, 255, 255, 0.12);
+ color: #99f6e4;
+ font-size: 22rpx;
+ letter-spacing: 1rpx;
+}
+.hero-title {
+ margin-top: 20rpx;
+ font-size: 56rpx;
+ font-weight: 800;
+ line-height: 1.25;
+}
+.hero-sub {
+ margin-top: 12rpx;
+ font-size: 27rpx;
+ line-height: 1.6;
+ color: rgba(255, 255, 255, 0.78);
+}
+.hero-steps {
+ margin-top: 28rpx;
+ display: flex;
+ align-items: center;
+ gap: 10rpx;
+}
+.hero-step {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 10rpx 16rpx 10rpx 10rpx;
+ border-radius: 999rpx;
+ background: rgba(255, 255, 255, 0.1);
+ font-size: 24rpx;
+ white-space: nowrap;
+}
+.hero-step .num {
+ width: 34rpx;
+ height: 34rpx;
+ line-height: 34rpx;
+ text-align: center;
+ border-radius: 50%;
+ background: var(--brand-2);
+ color: var(--brand-deep);
+ font-size: 20rpx;
+ font-weight: 800;
+}
+.hero-arrow {
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 30rpx;
+}
+
+/* 卡片 */
+.card {
+ margin: 0 24rpx 24rpx;
+ padding: 32rpx;
+ background: var(--card);
+ border-radius: var(--r-l);
+ box-shadow: var(--shadow);
+}
+.card-title {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: var(--ink);
+ margin-bottom: 20rpx;
+}
+
+/* 预览 */
+.preview-card {
+ margin-top: -120rpx;
+ padding: 24rpx;
+}
+.preview-box {
+ position: relative;
+ width: 100%;
+ height: 654rpx;
+ border-radius: 24rpx;
+ overflow: hidden;
+ background: #f1eee7;
+}
+.preview-canvas {
+ width: 654rpx;
+ height: 654rpx;
+ display: block;
+ margin: 0 auto;
+}
+.sample-tag {
+ position: absolute;
+ left: 20rpx;
+ top: 20rpx;
+ padding: 8rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(22, 33, 31, 0.72);
+ color: #fff;
+ font-size: 22rpx;
+}
+.generating {
+ position: absolute;
+ right: 20rpx;
+ top: 20rpx;
+ width: 52rpx;
+ height: 52rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.9);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.spinner {
+ width: 28rpx;
+ height: 28rpx;
+ border: 4rpx solid var(--brand-soft);
+ border-top-color: var(--brand);
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+}
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.scan-row {
+ margin-top: 20rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16rpx;
+}
+.scan-badge {
+ flex: 1;
+ padding: 14rpx 20rpx;
+ border-radius: 16rpx;
+ font-size: 25rpx;
+ background: #f1f1ee;
+ color: var(--muted);
+}
+.scan-ok { background: var(--ok-soft); color: var(--ok); font-weight: 600; }
+.scan-fail { background: var(--warn-soft); color: var(--warn); font-weight: 600; }
+.scan-checking { background: var(--brand-soft); color: var(--brand); }
+
+.stats {
+ margin-top: 20rpx;
+ display: flex;
+ background: #faf9f6;
+ border-radius: 20rpx;
+ padding: 20rpx 0;
+}
+.stat {
+ flex: 1;
+ text-align: center;
+}
+.stat + .stat { border-left: 1rpx solid var(--line); }
+.stat-v {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: var(--ink);
+}
+.stat-k {
+ margin-top: 4rpx;
+ font-size: 22rpx;
+ color: var(--muted);
+}
+.preview-actions {
+ margin-top: 20rpx;
+}
+.ghost-btn {
+ text-align: center;
+ width: 100%;
+ height: 84rpx;
+ line-height: 84rpx;
+ border-radius: 20rpx;
+ background: var(--brand-soft);
+ color: var(--brand);
+ font-size: 28rpx;
+ font-weight: 600;
+}
+
+/* 动态演示状态 */
+.intro-tip {
+ margin-top: 20rpx;
+ display: flex;
+ align-items: flex-start;
+ gap: 14rpx;
+ padding: 16rpx 20rpx;
+ border-radius: 16rpx;
+ background: var(--brand-soft);
+ color: var(--brand-deep);
+ font-size: 25rpx;
+ line-height: 1.5;
+}
+.intro-dot {
+ flex-shrink: 0;
+ width: 14rpx;
+ height: 14rpx;
+ margin-top: 13rpx;
+ border-radius: 50%;
+ background: var(--brand-2);
+ animation: pulse 1.6s ease-in-out infinite;
+}
+@keyframes pulse {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.35; transform: scale(0.7); }
+}
+.features .stat-v { font-size: 32rpx; color: var(--brand); }
+.cta-btn {
+ width: 100%;
+ height: 88rpx;
+ line-height: 88rpx;
+ border-radius: 20rpx;
+ text-align: center;
+ background: linear-gradient(135deg, var(--brand), var(--brand-2));
+ color: #fff;
+ font-size: 29rpx;
+ font-weight: 700;
+ box-shadow: 0 10rpx 24rpx rgba(15, 118, 110, 0.25);
+}
+
+/* 步骤卡 */
+.step-head {
+ display: flex;
+ align-items: flex-start;
+ gap: 20rpx;
+ margin-bottom: 24rpx;
+}
+.step-num {
+ flex-shrink: 0;
+ width: 52rpx;
+ height: 52rpx;
+ line-height: 52rpx;
+ text-align: center;
+ border-radius: 16rpx;
+ background: var(--brand);
+ color: #fff;
+ font-weight: 800;
+ font-size: 28rpx;
+}
+.step-title {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: var(--ink);
+}
+.optional {
+ margin-left: 8rpx;
+ padding: 2rpx 12rpx;
+ border-radius: 8rpx;
+ background: #f1f1ee;
+ color: var(--muted);
+ font-size: 20rpx;
+ font-weight: 500;
+ vertical-align: middle;
+}
+.step-desc {
+ margin-top: 6rpx;
+ font-size: 24rpx;
+ color: var(--muted);
+}
+
+.input-wrap {
+ border: 2rpx solid var(--line);
+ border-radius: 20rpx;
+ background: #fbfaf8;
+ padding: 20rpx 24rpx;
+}
+.input-bad { border-color: var(--danger); background: var(--danger-soft); }
+.textarea {
+ width: 100%;
+ min-height: 120rpx;
+ font-size: 30rpx;
+ line-height: 1.5;
+ color: var(--ink);
+}
+.input {
+ height: 48rpx;
+ font-size: 28rpx;
+ color: var(--ink);
+}
+.ph { color: #b5b8b4; }
+.input-foot {
+ margin-top: 12rpx;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.text-status { font-size: 23rpx; color: var(--muted); }
+.text-status.pad { margin-top: 10rpx; }
+.status-checking { color: var(--brand); }
+.status-pass { color: var(--ok); }
+.status-risky { color: var(--danger); font-weight: 600; }
+.status-error { color: var(--warn); }
+.input-tools { display: flex; gap: 24rpx; }
+.tool { font-size: 25rpx; color: var(--muted); }
+.tool-brand { color: var(--brand); font-weight: 600; }
+.tip {
+ margin-top: 16rpx;
+ font-size: 23rpx;
+ color: var(--muted);
+}
+
+.upload {
+ border: 2rpx dashed #c9d6d3;
+ border-radius: 24rpx;
+ padding: 44rpx 24rpx;
+ text-align: center;
+ background: #f8fbfa;
+}
+.upload-icon {
+ margin: 0 auto;
+ width: 80rpx;
+ height: 80rpx;
+ line-height: 76rpx;
+ border-radius: 50%;
+ background: var(--brand-soft);
+ color: var(--brand);
+ font-size: 48rpx;
+}
+.upload-text {
+ margin-top: 16rpx;
+ font-size: 29rpx;
+ font-weight: 600;
+ color: var(--ink);
+}
+.upload-hint {
+ margin-top: 8rpx;
+ font-size: 22rpx;
+ color: var(--muted);
+}
+
+.photo {
+ display: flex;
+ gap: 24rpx;
+ align-items: center;
+}
+.photo-thumb {
+ width: 150rpx;
+ height: 150rpx;
+ border-radius: 20rpx;
+ background: #eee;
+}
+.photo-info { flex: 1; }
+.photo-meta { font-size: 23rpx; color: var(--ok); }
+.photo-actions {
+ margin-top: 16rpx;
+ display: flex;
+ gap: 12rpx;
+}
+.photo-actions .mini-btn {
+ flex: 1;
+ padding: 0;
+ text-align: center;
+}
+
+.mini-btn {
+ display: inline-block;
+ text-align: center;
+ white-space: nowrap;
+ padding: 0 22rpx;
+ height: 56rpx;
+ line-height: 56rpx;
+ border-radius: 14rpx;
+ background: #f1f1ee;
+ color: var(--ink);
+ font-size: 24rpx;
+ font-weight: 600;
+}
+.mini-btn.accent { background: var(--accent); color: #fff; }
+.mini-btn.danger { color: var(--danger); background: var(--danger-soft); }
+
+.row {
+ margin-top: 24rpx;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20rpx;
+}
+.row-label { font-size: 28rpx; color: var(--ink); font-weight: 600; }
+.row-hint { margin-top: 4rpx; font-size: 22rpx; color: var(--muted); }
+
+.field-label {
+ margin: 28rpx 0 14rpx;
+ font-size: 26rpx;
+ font-weight: 600;
+ color: var(--ink);
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+.field-label:first-child { margin-top: 0; }
+.field-val { color: var(--brand); font-weight: 700; }
+.field-hint { font-size: 22rpx; color: var(--muted); font-weight: 400; }
+.mini-label { font-size: 23rpx; color: var(--muted); margin-top: 12rpx; }
+.link-text { color: var(--brand); font-size: 24rpx; font-weight: 600; }
+.link-text.center { display: block; text-align: center; margin-top: 20rpx; }
+
+/* 分段选择 */
+.seg {
+ display: flex;
+ gap: 12rpx;
+}
+.seg-item {
+ flex: 1;
+ padding: 18rpx 8rpx;
+ border-radius: 18rpx;
+ border: 2rpx solid var(--line);
+ background: #fbfaf8;
+ text-align: center;
+ transition: all 0.15s;
+}
+.seg-item.on {
+ border-color: var(--brand);
+ background: var(--brand-soft);
+}
+.seg-main { font-size: 27rpx; font-weight: 700; color: var(--ink); }
+.seg-item.on .seg-main { color: var(--brand); }
+.seg-sub { margin-top: 4rpx; font-size: 21rpx; color: var(--muted); }
+.seg.small .seg-item { padding: 12rpx 8rpx; }
+
+slider { margin: 0 12rpx; }
+
+.pos-grid {
+ width: 216rpx;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12rpx;
+ padding: 12rpx;
+ border-radius: 20rpx;
+ background: #f4f3ef;
+}
+.pos-cell {
+ width: 56rpx;
+ height: 56rpx;
+ border-radius: 12rpx;
+ background: #fff;
+ border: 2rpx solid var(--line);
+}
+.pos-cell.on { background: var(--brand); border-color: var(--brand); }
+
+.advanced-toggle {
+ margin-top: 32rpx;
+ padding-top: 24rpx;
+ border-top: 1rpx solid var(--line);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 26rpx;
+ color: var(--muted);
+}
+.chev { font-size: 36rpx; transform: rotate(90deg); transition: transform 0.2s; }
+.chev.open { transform: rotate(-90deg); }
+.advanced { margin-top: 8rpx; }
+
+.chips-scroll { width: 100%; white-space: nowrap; }
+.chips { display: inline-flex; gap: 12rpx; padding-bottom: 4rpx; }
+.chip {
+ display: inline-block;
+ padding: 16rpx 22rpx;
+ border-radius: 18rpx;
+ border: 2rpx solid var(--line);
+ background: #fbfaf8;
+}
+.chip.on { border-color: var(--brand); background: var(--brand-soft); }
+.chip-main { font-size: 27rpx; font-weight: 700; color: var(--ink); }
+.chip.on .chip-main { color: var(--brand); }
+.chip-sub { margin-top: 4rpx; font-size: 20rpx; color: var(--muted); }
+
+/* 用豆清单 */
+.usage {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 12rpx;
+}
+.usage-item {
+ display: flex;
+ align-items: center;
+ gap: 10rpx;
+ padding: 12rpx;
+ border-radius: 14rpx;
+ background: #faf9f6;
+}
+.swatch {
+ width: 32rpx;
+ height: 32rpx;
+ border-radius: 50%;
+ box-shadow: inset 0 0 0 1rpx rgba(0, 0, 0, 0.12);
+ flex-shrink: 0;
+}
+.usage-id { font-size: 23rpx; font-weight: 700; color: var(--ink); }
+.usage-count { margin-left: auto; font-size: 21rpx; color: var(--muted); }
+
+/* 页脚 */
+.footer {
+ padding: 16rpx 48rpx 24rpx;
+ text-align: center;
+}
+.footer-safe {
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 10rpx;
+ text-align: left;
+ font-size: 22rpx;
+ line-height: 1.6;
+ color: var(--muted);
+}
+.footer-links {
+ margin-top: 16rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 12rpx;
+}
+.footer-link {
+ font-size: 23rpx;
+ line-height: 1.6;
+ color: var(--brand);
+}
+.footer-shield {
+ flex-shrink: 0;
+ width: 30rpx;
+ height: 30rpx;
+ margin-top: 3rpx;
+}
+.footer-links .footer-btn {
+ display: inline-block;
+ width: auto !important;
+ min-height: 0;
+ padding: 0 !important;
+ margin: 0 !important;
+ font-size: 23rpx;
+ font-weight: 400;
+ line-height: 1.6;
+ color: var(--brand);
+ background: none;
+}
+.dot { color: #c4c7c3; font-size: 22rpx; }
+.footer-copy { margin-top: 12rpx; font-size: 20rpx; color: #b5b8b4; }
+
+/* 底部操作栏 */
+.bottom-bar {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 40;
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+ padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
+ background: rgba(255, 255, 255, 0.96);
+ box-shadow: 0 -6rpx 30rpx rgba(22, 33, 31, 0.08);
+}
+.bar-info { flex: 1; min-width: 0; }
+.bar-main { font-size: 30rpx; font-weight: 800; color: var(--ink); }
+.bar-sub { margin-top: 2rpx; font-size: 22rpx; color: var(--muted); }
+.primary-btn {
+ text-align: center;
+ padding: 0 56rpx;
+ height: 92rpx;
+ line-height: 92rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(135deg, var(--brand), var(--brand-2));
+ color: #fff;
+ font-size: 30rpx;
+ font-weight: 700;
+ box-shadow: 0 10rpx 24rpx rgba(15, 118, 110, 0.3);
+}
+.primary-btn.disabled { opacity: 0.5; box-shadow: none; }
+.primary-btn.block { display: block; width: 100%; padding: 0; }
+
+/* 弹层 */
+.mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 100;
+ background: rgba(10, 20, 18, 0.5);
+ display: flex;
+ align-items: flex-end;
+}
+.sheet {
+ width: 100%;
+ max-height: 88vh;
+ display: flex;
+ flex-direction: column;
+ background: var(--card);
+ border-radius: 36rpx 36rpx 0 0;
+ padding: 16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
+ box-sizing: border-box;
+}
+.sheet-handle {
+ margin: 0 auto 16rpx;
+ width: 72rpx;
+ height: 8rpx;
+ border-radius: 8rpx;
+ background: #e2e2dd;
+}
+.sheet-title {
+ font-size: 34rpx;
+ font-weight: 800;
+ color: var(--ink);
+ margin-bottom: 20rpx;
+}
+.sheet-body {
+ flex: 1;
+ max-height: 62vh;
+ margin-bottom: 24rpx;
+}
+.grid-opts {
+ margin-top: 16rpx;
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+}
+.grid-opts .seg { flex: 1; }
+.colors { display: flex; gap: 14rpx; }
+.color-dot {
+ width: 44rpx;
+ height: 44rpx;
+ border-radius: 50%;
+ border: 4rpx solid #fff;
+ box-shadow: 0 0 0 2rpx var(--line);
+}
+.color-dot.on { box-shadow: 0 0 0 4rpx var(--brand); }
+
+.result {
+ margin-top: 32rpx;
+ border-radius: 24rpx;
+ background: #fff;
+ border: 2rpx solid var(--line);
+ overflow: hidden;
+}
+.result-head {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ padding: 28rpx;
+ background: linear-gradient(135deg, var(--ok-soft), #ffffff);
+}
+.result-icon {
+ flex-shrink: 0;
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: var(--ok);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 18rpx rgba(21, 128, 61, 0.25);
+}
+.result-icon.warn { background: var(--warn); box-shadow: none; }
+.result-tick {
+ width: 16rpx;
+ height: 28rpx;
+ margin-top: -6rpx;
+ border-right: 5rpx solid #fff;
+ border-bottom: 5rpx solid #fff;
+ transform: rotate(45deg);
+}
+.result-text { flex: 1; min-width: 0; }
+.result-title { font-size: 30rpx; font-weight: 800; color: var(--ink); }
+.result-sub { margin-top: 6rpx; font-size: 23rpx; color: var(--muted); line-height: 1.5; }
+.result-list { padding: 8rpx 28rpx; }
+.result-file {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ padding: 20rpx 0;
+}
+.result-file + .result-file { border-top: 1rpx solid var(--line); }
+.file-badge {
+ flex-shrink: 0;
+ width: 72rpx;
+ height: 84rpx;
+ line-height: 84rpx;
+ border-radius: 12rpx;
+ text-align: center;
+ font-size: 20rpx;
+ font-weight: 800;
+ letter-spacing: 1rpx;
+ color: #fff;
+ background: #e5484d;
+}
+.file-badge.png { background: #3b82f6; }
+.file-info { flex: 1; min-width: 0; }
+.file-name {
+ font-size: 27rpx;
+ font-weight: 700;
+ color: var(--ink);
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+.file-desc { margin-top: 4rpx; font-size: 22rpx; color: var(--muted); }
+.file-actions { flex-shrink: 0; display: flex; gap: 12rpx; }
+.file-btn {
+ min-width: 100rpx;
+ height: 60rpx;
+ line-height: 60rpx;
+ padding: 0 20rpx;
+ border-radius: 999rpx;
+ text-align: center;
+ font-size: 24rpx;
+ font-weight: 700;
+ color: var(--brand);
+ background: var(--brand-soft);
+}
+.file-btn.primary { color: #fff; background: var(--brand); }
+
+/* 裁切 */
+.crop-mask { align-items: stretch; background: #0b1413; }
+.crop-panel {
+ width: 100%;
+ padding: calc(env(safe-area-inset-top) + 100rpx) 40rpx 40rpx;
+ box-sizing: border-box;
+ overflow-y: auto;
+ color: #fff;
+}
+.crop-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 28rpx;
+}
+.crop-title { font-size: 32rpx; font-weight: 700; }
+.crop-cancel { font-size: 28rpx; color: rgba(255, 255, 255, 0.7); }
+.crop-ok {
+ padding: 10rpx 28rpx;
+ border-radius: 999rpx;
+ background: var(--brand-2);
+ color: var(--brand-deep);
+ font-size: 28rpx;
+ font-weight: 700;
+}
+.crop-ok.wait { opacity: 0.55; }
+.crop-box {
+ width: 670rpx;
+ height: 670rpx;
+ margin: 0 auto;
+ border-radius: 20rpx;
+ overflow: hidden;
+ background: #1a2523;
+}
+.crop-canvas { width: 670rpx; height: 670rpx; }
+.crop-check {
+ margin-top: 20rpx;
+ padding: 14rpx 20rpx;
+ border-radius: 14rpx;
+ font-size: 24rpx;
+ background: rgba(255, 255, 255, 0.08);
+ color: rgba(255, 255, 255, 0.8);
+}
+.check-pass { color: #6ee7b7; }
+.check-risky { background: rgba(229, 72, 77, 0.2); color: #fca5a5; }
+.check-error { color: #fcd34d; }
+.crop-hint { margin-top: 12rpx; font-size: 22rpx; color: rgba(255, 255, 255, 0.5); text-align: center; }
+.field-label.light, .row.light .row-label { color: #fff; }
+.row.light .row-hint, .mini-label.light { color: rgba(255, 255, 255, 0.55); }
+.crop-tools { margin-top: 24rpx; display: flex; gap: 16rpx; }
+.crop-tool {
+ text-align: center;
+ flex: 1;
+ height: 72rpx;
+ line-height: 72rpx;
+ border-radius: 16rpx;
+ background: rgba(255, 255, 255, 0.1);
+ color: #fff;
+ font-size: 25rpx;
+}
+.crop-replace { margin-top: 32rpx; }
+
+/* 离屏画布:不能 display:none,否则取不到节点 */
+.offscreen {
+ position: fixed;
+ left: -9999px;
+ top: 0;
+ width: 10px;
+ height: 10px;
+ overflow: hidden;
+}
+.off-canvas { width: 10px; height: 10px; }
diff --git a/BeadQR/miniprogram/sitemap.json b/BeadQR/miniprogram/sitemap.json
new file mode 100644
index 0000000000000000000000000000000000000000..27b2b26d379bc328ebf87b99d643ac2d374e0730
--- /dev/null
+++ b/BeadQR/miniprogram/sitemap.json
@@ -0,0 +1,7 @@
+{
+ "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
+ "rules": [{
+ "action": "allow",
+ "page": "*"
+ }]
+}
\ No newline at end of file
diff --git a/BeadQR/miniprogram/utils/color.js b/BeadQR/miniprogram/utils/color.js
new file mode 100644
index 0000000000000000000000000000000000000000..96622d027f9a3bf80ad933d587fd03d20e819714
--- /dev/null
+++ b/BeadQR/miniprogram/utils/color.js
@@ -0,0 +1,46 @@
+// 颜色基础:HEX/RGB 转换、OKLab 感知色彩空间、亮度
+// OKLab 比 RGB 欧氏距离更接近人眼感受,配色更自然;距离统一放大 100 倍便于调参
+
+function hexToRgb(hex) {
+ const value = String(hex || '').replace('#', '')
+ const full = value.length === 3 ? value.split('').map((c) => c + c).join('') : value
+ const num = parseInt(full, 16) || 0
+ return [(num >> 16) & 255, (num >> 8) & 255, num & 255]
+}
+
+function rgbToHex(r, g, b) {
+ const h = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')
+ return `#${h(r)}${h(g)}${h(b)}`
+}
+
+// sRGB 伽马解码查表,避免每次调用 Math.pow
+const SRGB_TO_LINEAR = new Float32Array(256)
+for (let i = 0; i < 256; i += 1) {
+ const c = i / 255
+ SRGB_TO_LINEAR[i] = c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
+}
+
+// 写入 out[offset..offset+2](L, a, b,均 ×100)
+function rgbToOklabInto(r, g, b, out, offset) {
+ const lr = SRGB_TO_LINEAR[r]
+ const lg = SRGB_TO_LINEAR[g]
+ const lb = SRGB_TO_LINEAR[b]
+ const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb)
+ const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb)
+ const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb)
+ out[offset] = (0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s) * 100
+ out[offset + 1] = (1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s) * 100
+ out[offset + 2] = (0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s) * 100
+}
+
+// 感知亮度(与旧版一致的 Rec.601 加权,0~1),用于二维码深/浅模块约束
+function luma(r, g, b) {
+ return (r * 0.299 + g * 0.587 + b * 0.114) / 255
+}
+
+function lumaHex(hex) {
+ const [r, g, b] = hexToRgb(hex)
+ return luma(r, g, b)
+}
+
+module.exports = { hexToRgb, rgbToHex, rgbToOklabInto, luma, lumaHex }
diff --git a/BeadQR/miniprogram/utils/engine.js b/BeadQR/miniprogram/utils/engine.js
new file mode 100644
index 0000000000000000000000000000000000000000..c519e60276bf694327e1dd07f4aedbcc247e60bb
--- /dev/null
+++ b/BeadQR/miniprogram/utils/engine.js
@@ -0,0 +1,225 @@
+// 配色引擎:把底图 + 二维码合成为 MARD 色号网格
+// 性能要点:
+// 1. 5bit 量化桶 → 候选 Top-K 查找表(懒加载),每格只精算约 10 个候选,而非整套 291 色
+// 2. OKLab 感知距离 + 色间距离矩阵,全部 Typed Array,无对象分配
+// 3. “颜色连贯度”参考已选中的左/上邻居色号,减少零碎杂色,拼起来更省事
+// 4. 定位/时序/对齐等功能区使用更严格的深浅阈值,显著提升扫码成功率
+const { rgbToOklabInto } = require('./color')
+const { MATCH_MODES } = require('./palette')
+const { PAPER } = require('./imaging')
+
+const TOP_K = 10
+const C_ANY = 0
+const C_DARK = 1
+const C_LIGHT = 2
+const C_FDARK = 3
+const C_FLIGHT = 4
+const C_QUIET = 5
+const C_EMPTY = 6
+
+function subsetFor(pal, mode, kind) {
+ const key = `${mode.id}:${kind}`
+ if (pal.subsets[key]) return pal.subsets[key]
+ const { n, lum } = pal
+ let test
+ if (kind === C_DARK) test = (i) => lum[i] <= mode.darkMax
+ else if (kind === C_LIGHT) test = (i) => lum[i] >= mode.lightMin
+ else if (kind === C_FDARK) test = (i) => lum[i] <= Math.min(mode.darkMax, 0.3)
+ else if (kind === C_FLIGHT) test = (i) => lum[i] >= Math.max(mode.lightMin, 0.82)
+ else test = () => true
+ let list = []
+ for (let i = 0; i < n; i += 1) if (test(i)) list.push(i)
+ if (!list.length) {
+ // 色库太小时退化为最深/最浅的 3 个颜色
+ const order = Array.from({ length: n }, (_, i) => i).sort((a, b) => lum[a] - lum[b])
+ list = kind === C_DARK || kind === C_FDARK ? order.slice(0, 3) : order.slice(-3)
+ }
+ const member = new Uint8Array(n)
+ list.forEach((i) => { member[i] = 1 })
+ const subset = { key, list: Int16Array.from(list), member, lut: new Map() }
+ pal.subsets[key] = subset
+ return subset
+}
+
+const tmpLab = new Float32Array(3)
+
+function topKForBucket(pal, subset, bucket) {
+ let hit = subset.lut.get(bucket)
+ if (hit) return hit
+ const r = ((bucket >> 10) & 31) * 8 + 4
+ const g = ((bucket >> 5) & 31) * 8 + 4
+ const b = (bucket & 31) * 8 + 4
+ rgbToOklabInto(r, g, b, tmpLab, 0)
+ const { lab } = pal
+ const list = subset.list
+ const k = Math.min(TOP_K, list.length)
+ const bestIdx = new Int16Array(k).fill(-1)
+ const bestDist = new Float32Array(k).fill(Infinity)
+ for (let t = 0; t < list.length; t += 1) {
+ const i = list[t]
+ const dl = lab[i * 3] - tmpLab[0]
+ const da = lab[i * 3 + 1] - tmpLab[1]
+ const db = lab[i * 3 + 2] - tmpLab[2]
+ const d = dl * dl + da * da + db * db
+ if (d >= bestDist[k - 1]) continue
+ let p = k - 1
+ while (p > 0 && bestDist[p - 1] > d) { bestDist[p] = bestDist[p - 1]; bestIdx[p] = bestIdx[p - 1]; p -= 1 }
+ bestDist[p] = d
+ bestIdx[p] = i
+ }
+ hit = bestIdx
+ subset.lut.set(bucket, hit)
+ return hit
+}
+
+/**
+ * @param base { size, rgb: Uint8Array, mask?: Uint8Array } 或 null(无底图)
+ * @param qr makeQr() 结果
+ * @param placement createPlacement() 结果
+ * @param pal preparePalette() 结果
+ * @param options { modeId, coherence(0-100), cleanup(bool) }
+ */
+function computeArtwork(base, qr, placement, pal, options = {}) {
+ const mode = MATCH_MODES.find((m) => m.id === options.modeId) || MATCH_MODES[1]
+ const size = placement.size
+ const total = size * size
+ const lab = new Float32Array(total * 3)
+ const bucket = new Int32Array(total)
+ const kind = new Uint8Array(total)
+ const { left, top, quiet } = placement
+
+ for (let y = 0; y < size; y += 1) {
+ for (let x = 0; x < size; x += 1) {
+ const i = y * size + x
+ const qx = x - left
+ const qy = y - top
+ const inQr = qx >= 0 && qy >= 0 && qx < qr.size && qy < qr.size
+ const inQuiet = !inQr && x >= left - quiet && x < left + qr.size + quiet && y >= top - quiet && y < top + qr.size + quiet
+ let r; let g; let b
+ if (inQr) {
+ const q = qy * qr.size + qx
+ const isDark = qr.dark[q]
+ const isFunc = qr.func[q]
+ kind[i] = isFunc ? (isDark ? C_FDARK : C_FLIGHT) : (isDark ? C_DARK : C_LIGHT)
+ if (base && !(base.mask && base.mask[i])) {
+ r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
+ } else if (base) {
+ r = PAPER[0]; g = PAPER[1]; b = PAPER[2]
+ } else {
+ r = g = b = isDark ? 0 : 255
+ }
+ } else if (inQuiet || !base) {
+ kind[i] = C_QUIET
+ continue
+ } else if (base.mask && base.mask[i]) {
+ kind[i] = C_EMPTY
+ continue
+ } else {
+ kind[i] = C_ANY
+ r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
+ }
+ rgbToOklabInto(r, g, b, lab, i * 3)
+ bucket[i] = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)
+ }
+ }
+
+ const n = pal.n
+ const plab = pal.lab
+ const pair = pal.pair
+ const weight = (Math.max(0, Math.min(100, options.coherence == null ? 40 : options.coherence)) / 100) * 0.6
+ const subsets = [C_ANY, C_DARK, C_LIGHT, C_FDARK, C_FLIGHT].map((k) => subsetFor(pal, mode, k))
+ const cells = new Int16Array(total).fill(-1)
+
+ const distTo = (c, i) => {
+ const dl = plab[c * 3] - lab[i * 3]
+ const da = plab[c * 3 + 1] - lab[i * 3 + 1]
+ const db = plab[c * 3 + 2] - lab[i * 3 + 2]
+ return Math.sqrt(dl * dl + da * da + db * db)
+ }
+
+ for (let y = 0; y < size; y += 1) {
+ for (let x = 0; x < size; x += 1) {
+ const i = y * size + x
+ const k = kind[i]
+ if (k >= C_QUIET) continue
+ const subset = subsets[k]
+ const cands = topKForBucket(pal, subset, bucket[i])
+ const nl = x > 0 ? cells[i - 1] : -1
+ const nu = y > 0 ? cells[i - size] : -1
+ const nbCount = (nl >= 0 ? 1 : 0) + (nu >= 0 ? 1 : 0)
+ let best = -1
+ let bestScore = Infinity
+ const consider = (c) => {
+ let score = distTo(c, i)
+ if (weight > 0 && nbCount) {
+ let s = 0
+ if (nl >= 0) s += pair[c * n + nl]
+ if (nu >= 0) s += pair[c * n + nu]
+ score += weight * (s / nbCount)
+ }
+ if (score < bestScore) { bestScore = score; best = c }
+ }
+ for (let t = 0; t < cands.length; t += 1) if (cands[t] >= 0) consider(cands[t])
+ if (weight > 0) {
+ if (nl >= 0 && subset.member[nl]) consider(nl)
+ if (nu >= 0 && nu !== nl && subset.member[nu]) consider(nu)
+ }
+ cells[i] = best
+ }
+ }
+
+ // 合并零散色:只用了 1~2 颗的色号并入最接近的常用色,减少需要购买的色号
+ const counts = new Int32Array(n)
+ for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
+ if (options.cleanup !== false) {
+ const kept = []
+ for (let c = 0; c < n; c += 1) if (counts[c] >= 3) kept.push(c)
+ if (kept.length) {
+ for (let i = 0; i < total; i += 1) {
+ const c = cells[i]
+ if (c < 0 || counts[c] >= 3) continue
+ const member = subsets[kind[i]].member
+ let best = -1
+ let bestD = Infinity
+ for (let t = 0; t < kept.length; t += 1) {
+ const kc = kept[t]
+ if (!member[kc]) continue
+ const d = distTo(kc, i)
+ if (d < bestD) { bestD = d; best = kc }
+ }
+ // 与原色差别过大时保留原色,避免破坏画面
+ if (best >= 0 && bestD <= distTo(c, i) + 12) cells[i] = best
+ }
+ counts.fill(0)
+ for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
+ }
+ }
+
+ // 二维码留白区:使用画面中最常用的浅色;没有则用色库里最浅的颜色
+ let quietColor = -1
+ let quietCount = -1
+ for (let c = 0; c < n; c += 1) {
+ if (pal.lum[c] >= Math.max(0.8, mode.lightMin) && counts[c] > quietCount) { quietColor = c; quietCount = counts[c] }
+ }
+ if (quietCount <= 0) {
+ quietColor = 0
+ for (let c = 1; c < n; c += 1) if (pal.lum[c] > pal.lum[quietColor]) quietColor = c
+ }
+ for (let i = 0; i < total; i += 1) {
+ if (kind[i] === C_QUIET) { cells[i] = quietColor; counts[quietColor] += 1 }
+ }
+
+ const usage = []
+ let beads = 0
+ for (let c = 0; c < n; c += 1) {
+ if (counts[c] > 0) {
+ usage.push({ index: c, id: pal.ids[c], hex: pal.hexes[c], count: counts[c] })
+ beads += counts[c]
+ }
+ }
+ usage.sort((a, b) => b.count - a.count)
+
+ return { size, cells, usage, beads, pal, qr, placement }
+}
+
+module.exports = { computeArtwork }
diff --git a/BeadQR/miniprogram/utils/imaging.js b/BeadQR/miniprogram/utils/imaging.js
new file mode 100644
index 0000000000000000000000000000000000000000..16608b11a75931b0d41fd2758a92f449091a5854
--- /dev/null
+++ b/BeadQR/miniprogram/utils/imaging.js
@@ -0,0 +1,95 @@
+// 图像取样:超采样 + 区域平均,得到干净的格子颜色(旧版一次性缩放会产生噪点和锯齿色)
+// 所有结果都用 Typed Array 表示,不再生成 HEX 字符串再反解析
+
+const PAPER = [247, 241, 230]
+
+function getImageDimensions(image) {
+ const width = Number(image.width) || Number(image.naturalWidth) || Number(image._w)
+ const height = Number(image.height) || Number(image.naturalHeight) || Number(image._h)
+ if (!width || !height) throw new Error('IMAGE_DIMENSIONS_UNAVAILABLE')
+ return { width, height }
+}
+
+// 把图片按裁切参数画到 context 上(cover 语义,zoom≥1)
+function drawCropped(context, image, width, height, crop) {
+ const source = getImageDimensions(image)
+ const scale = Math.max(width / source.width, height / source.height) * (crop.zoom || 1)
+ const w = source.width * scale
+ const h = source.height * scale
+ const offsetX = (width - w) * ((crop.focusX == null ? 50 : crop.focusX) / 100)
+ const offsetY = (height - h) * ((crop.focusY == null ? 50 : crop.focusY) / 100)
+ context.fillStyle = `rgb(${PAPER[0]},${PAPER[1]},${PAPER[2]})`
+ context.fillRect(0, 0, width, height)
+ context.save()
+ context.translate(crop.flipH ? width : 0, crop.flipV ? height : 0)
+ context.scale(crop.flipH ? -1 : 1, crop.flipV ? -1 : 1)
+ context.drawImage(image, offsetX, offsetY, w, h)
+ context.restore()
+}
+
+// 返回 { size, rgb: Uint8Array(size*size*3) }
+function sampleImage(canvas, image, size, crop) {
+ // 超采样到约 700px 再按块平均,等价于高质量面积缩放
+ const factor = Math.max(1, Math.min(24, Math.floor(720 / size)))
+ const big = size * factor
+ canvas.width = big
+ canvas.height = big
+ const context = canvas.getContext('2d')
+ context.imageSmoothingEnabled = true
+ try { context.imageSmoothingQuality = 'high' } catch (e) { /* 部分基础库不支持 */ }
+ drawCropped(context, image, big, big, crop || {})
+ const pixels = context.getImageData(0, 0, big, big).data
+ const rgb = new Uint8Array(size * size * 3)
+ const area = factor * factor
+ for (let gy = 0; gy < size; gy += 1) {
+ for (let gx = 0; gx < size; gx += 1) {
+ let r = 0; let g = 0; let b = 0
+ for (let sy = 0; sy < factor; sy += 1) {
+ let p = ((gy * factor + sy) * big + gx * factor) * 4
+ for (let sx = 0; sx < factor; sx += 1) {
+ r += pixels[p]; g += pixels[p + 1]; b += pixels[p + 2]
+ p += 4
+ }
+ }
+ const o = (gy * size + gx) * 3
+ rgb[o] = (r / area + 0.5) | 0
+ rgb[o + 1] = (g / area + 0.5) | 0
+ rgb[o + 2] = (b / area + 0.5) | 0
+ }
+ }
+ return { size, rgb }
+}
+
+// 去除简单纯色背景:四角均色为背景色,从边缘洪泛填充相近颜色(主体内部不受影响)
+// 返回 Uint8Array 掩码:1 = 背景(空钉位,不放豆)
+function removeSimpleBackground(rgb, size, tolerance) {
+ const corners = [0, size - 1, (size - 1) * size, size * size - 1]
+ let br = 0; let bg = 0; let bb = 0
+ corners.forEach((i) => { br += rgb[i * 3]; bg += rgb[i * 3 + 1]; bb += rgb[i * 3 + 2] })
+ br /= 4; bg /= 4; bb /= 4
+ const threshold = (tolerance / 100) * 441.673
+ const limit = threshold * threshold
+ const mask = new Uint8Array(size * size)
+ const isBg = (i) => {
+ const dr = rgb[i * 3] - br; const dg = rgb[i * 3 + 1] - bg; const db = rgb[i * 3 + 2] - bb
+ return dr * dr + dg * dg + db * db <= limit
+ }
+ const stack = []
+ const push = (x, y) => {
+ if (x < 0 || y < 0 || x >= size || y >= size) return
+ const i = y * size + x
+ if (mask[i] || !isBg(i)) return
+ mask[i] = 1
+ stack.push(i)
+ }
+ for (let i = 0; i < size; i += 1) { push(i, 0); push(i, size - 1); push(0, i); push(size - 1, i) }
+ while (stack.length) {
+ const i = stack.pop()
+ const x = i % size
+ const y = (i - x) / size
+ push(x + 1, y); push(x - 1, y); push(x, y + 1); push(x, y - 1)
+ }
+ return mask
+}
+
+module.exports = { PAPER, getImageDimensions, drawCropped, sampleImage, removeSimpleBackground }
diff --git a/BeadQR/miniprogram/utils/intro.js b/BeadQR/miniprogram/utils/intro.js
new file mode 100644
index 0000000000000000000000000000000000000000..d8cf6fea218e501859c403a42bb2ecc597ca6ede
--- /dev/null
+++ b/BeadQR/miniprogram/utils/intro.js
@@ -0,0 +1,703 @@
+// 首屏动态演示(1:1)
+// 纯代码绘制:不占包体积(无视频/图片资源),小程序实时播放与宣传视频渲染共用同一条时间轴。
+// 演示用的拼豆作品由真实配色引擎实时生成,并且确实可以被微信扫出 “Happy Birthday!”。
+const { makeQr, createPlacement } = require('./qr')
+const { preparePalette } = require('./palette')
+const { computeArtwork } = require('./engine')
+
+const DEMO_TEXT = 'Happy Birthday!'
+const DURATION = 12
+const U = 1080 // 设计稿边长,绘制时按实际像素缩放
+const TAU = Math.PI * 2
+const FLIGHT = 0.85
+// 豆子半径(相对格宽):略大于 0.5,相邻豆子轻微相接,像熨烫后的成品,扫码更稳
+const BEAD_R = 0.52
+
+const C = {
+ bg: '#f4efe6',
+ ink: '#16211f',
+ muted: '#6b7471',
+ brand: '#0f766e',
+ brand2: '#14b8a6',
+ brandSoft: '#dcf1ec',
+ board: '#fffdf8',
+ dark: '#22302e',
+ light: '#fbf8f1',
+ wechat: '#07c160',
+}
+
+const SCENES = [
+ { a: 0, b: 2.6, title: '写一句话', sub: '扫码后想让人看到的话' },
+ { a: 2.6, b: 5.0, title: '自动编成二维码', sub: '每一颗拼豆,都是一个像素' },
+ { a: 5.0, b: 7.6, title: '融进你的照片', sub: '换上照片的颜色,依然能扫' },
+ { a: 7.6, b: 10.0, title: '微信扫一扫', sub: '拼好的作品,一扫就读出这句话' },
+]
+
+const CONFETTI = [
+ [0.17, 0.19, 0.045, '#ffd166'], [0.85, 0.2, 0.04, '#4cc9f0'], [0.13, 0.8, 0.04, '#2ec4b6'],
+ [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'],
+]
+
+const DECOR = [
+ [90, 150, 22, '#14b8a6'], [990, 120, 16, '#ffb703'], [60, 960, 18, '#ff7a45'], [1010, 930, 24, '#8ecae6'],
+ [170, 1040, 12, '#ffb703'], [930, 330, 12, '#ff7a45'], [120, 520, 10, '#8ecae6'], [980, 700, 14, '#14b8a6'],
+]
+
+// —— 工具 ——
+const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)
+const lerp = (a, b, t) => a + (b - a) * t
+const prog = (t, a, b) => clamp01((t - a) / (b - a))
+const easeOut = (t) => 1 - Math.pow(1 - t, 3)
+const easeIn = (t) => t * t * t
+const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2)
+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) }
+const bump = (t, a, b) => { const p = prog(t, a, b); return p > 0 && p < 1 ? Math.sin(Math.PI * p) : 0 }
+
+function rng(seed) {
+ let a = seed >>> 0
+ return () => {
+ a = (a + 0x6D2B79F5) >>> 0
+ let t = a
+ t = Math.imul(t ^ (t >>> 15), t | 1)
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
+ }
+}
+
+function rr(ctx, x, y, w, h, r) {
+ r = Math.max(0, Math.min(r, w / 2, h / 2))
+ ctx.moveTo(x + r, y)
+ ctx.arcTo(x + w, y, x + w, y + h, r)
+ ctx.arcTo(x + w, y + h, x, y + h, r)
+ ctx.arcTo(x, y + h, x, y, r)
+ ctx.arcTo(x, y, x + w, y, r)
+ ctx.closePath()
+}
+
+// 三层叠加的柔和阴影:比 shadowBlur 便宜得多,手机上逐帧绘制也流畅
+function softShadow(ctx, x, y, w, h, r, alpha) {
+ for (let i = 3; i >= 1; i -= 1) {
+ const g = i * 8
+ ctx.fillStyle = `rgba(52,38,18,${(alpha / (i + 1)).toFixed(3)})`
+ ctx.beginPath()
+ rr(ctx, x - g / 2, y - g / 2 + g * 0.8, w + g, h + g, r + g / 2)
+ ctx.fill()
+ }
+}
+
+function heartPath(ctx, cx, cy, s) {
+ ctx.moveTo(cx, cy + s * 0.9)
+ ctx.bezierCurveTo(cx - s * 1.3, cy + s * 0.05, cx - s * 0.95, cy - s * 1.0, cx, cy - s * 0.42)
+ ctx.bezierCurveTo(cx + s * 0.95, cy - s * 1.0, cx + s * 1.3, cy + s * 0.05, cx, cy + s * 0.9)
+ ctx.closePath()
+}
+
+// 演示“照片”:暖色渐变 + 爱心 + 彩纸屑(矢量绘制,任意尺寸都清晰)
+function drawIllustration(ctx, x, y, s) {
+ ctx.save()
+ ctx.beginPath()
+ ctx.rect(x, y, s, s)
+ ctx.clip()
+ let g = ctx.createLinearGradient(x, y, x, y + s)
+ g.addColorStop(0, '#63c7b8')
+ g.addColorStop(1, '#4aa3c9')
+ ctx.fillStyle = g
+ ctx.fillRect(x, y, s, s)
+ g = ctx.createRadialGradient(x + s * 0.5, y + s * 0.48, 0, x + s * 0.5, y + s * 0.48, s * 0.55)
+ g.addColorStop(0, 'rgba(255,255,255,0.35)')
+ g.addColorStop(1, 'rgba(255,255,255,0)')
+ ctx.fillStyle = g
+ ctx.fillRect(x, y, s, s)
+ CONFETTI.forEach(([cx, cy, r, col]) => {
+ ctx.fillStyle = col
+ ctx.beginPath()
+ ctx.arc(x + cx * s, y + cy * s, r * s, 0, TAU)
+ ctx.fill()
+ })
+ g = ctx.createLinearGradient(x, y + s * 0.2, x, y + s * 0.85)
+ g.addColorStop(0, '#ff6b88')
+ g.addColorStop(1, '#d42a55')
+ ctx.fillStyle = g
+ ctx.beginPath()
+ heartPath(ctx, x + s * 0.5, y + s * 0.55, s * 0.34)
+ ctx.fill()
+ ctx.fillStyle = 'rgba(255,255,255,0.55)'
+ ctx.save()
+ ctx.translate(x + s * 0.37, y + s * 0.42)
+ ctx.rotate(-0.6)
+ ctx.scale(1, 0.55)
+ ctx.beginPath()
+ ctx.arc(0, 0, s * 0.06, 0, TAU)
+ ctx.fill()
+ ctx.restore()
+ ctx.restore()
+}
+
+// 用真实引擎生成演示作品:插画按格面积取平均色 → 配色引擎
+function buildDemo(workCanvas, createCanvas) {
+ const qr = makeQr(DEMO_TEXT, 'H')
+ const placement = createPlacement(qr.size, { scale: 100, quiet: 1, hasImage: true })
+ const size = placement.size
+ const k = 6
+ const px = size * k
+ const canvas = createCanvas ? createCanvas(px, px) : workCanvas
+ canvas.width = px
+ canvas.height = px
+ const ctx = canvas.getContext('2d')
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
+ ctx.clearRect(0, 0, px, px)
+ drawIllustration(ctx, 0, 0, px)
+ const data = ctx.getImageData(0, 0, px, px).data
+ const rgb = new Uint8Array(size * size * 3)
+ for (let r = 0; r < size; r += 1) {
+ for (let c = 0; c < size; c += 1) {
+ let sr = 0; let sg = 0; let sb = 0
+ for (let y = 0; y < k; y += 1) {
+ let o = ((r * k + y) * px + c * k) * 4
+ for (let x = 0; x < k; x += 1, o += 4) { sr += data[o]; sg += data[o + 1]; sb += data[o + 2] }
+ }
+ const i = (r * size + c) * 3
+ rgb[i] = sr / (k * k); rgb[i + 1] = sg / (k * k); rgb[i + 2] = sb / (k * k)
+ }
+ }
+ const art = computeArtwork({ size, rgb }, qr, placement, preparePalette('221'), { modeId: 'balanced', coherence: 40, cleanup: true })
+ return { qr, placement, art }
+}
+
+// —— 豆子批量绘制:按颜色合并路径,每帧只需十几次填充 ——
+function addBead(groups, hex, x, y, r) {
+ let list = groups.get(hex)
+ if (!list) { list = []; groups.set(hex, list) }
+ list.push(x, y, r)
+}
+
+// 小批量合并路径:一条路径里子路径太多时,部分渲染引擎反而更慢,每 12 颗提交一次最稳
+const CHUNK = 12
+function fillCircles(ctx, list, rs, ox, oy) {
+ let n = 0
+ ctx.beginPath()
+ for (let i = 0; i < list.length; i += 3) {
+ const r = list[i + 2] * rs
+ const x = list[i] + list[i + 2] * ox
+ const y = list[i + 1] + list[i + 2] * oy
+ ctx.moveTo(x + r, y)
+ ctx.arc(x, y, r, 0, TAU)
+ n += 1
+ if (n === CHUNK) { ctx.fill(); ctx.beginPath(); n = 0 }
+ }
+ if (n) ctx.fill()
+}
+
+function flushBeads(ctx, groups) {
+ const all = []
+ groups.forEach((list, hex) => {
+ ctx.fillStyle = hex
+ fillCircles(ctx, list, 1, 0, 0)
+ all.push(list)
+ })
+ // 中孔:拼豆是一颗颗小管子
+ ctx.fillStyle = 'rgba(70,48,24,0.17)'
+ all.forEach((list) => fillCircles(ctx, list, 0.3, 0, 0))
+ // 高光
+ ctx.fillStyle = 'rgba(255,255,255,0.4)'
+ all.forEach((list) => fillCircles(ctx, list, 0.2, -0.42, -0.42))
+ groups.clear()
+}
+
+/**
+ * @param opts { workCanvas?, createCanvas?(w,h), font?, loop? }
+ * @returns { duration, text, artwork, draw(ctx, t, pixelWidth) }
+ */
+function createIntro(opts = {}) {
+ const font = opts.font || 'sans-serif'
+ const loop = opts.loop !== false
+ const F = (weight, px) => `${weight} ${Math.round(px)}px ${font}`
+ const { qr, placement, art } = buildDemo(opts.workCanvas, opts.createCanvas)
+ const size = art.size
+ const R = rng(20260929)
+ const cells = []
+ for (let r = 0; r < size; r += 1) {
+ for (let c = 0; c < size; c += 1) {
+ const qc = c - placement.left
+ const qrow = r - placement.top
+ const dark = qc >= 0 && qrow >= 0 && qc < qr.size && qrow < qr.size && qr.dark[qrow * qr.size + qc] === 1
+ const idx = art.cells[r * size + c]
+ cells.push({
+ c,
+ r,
+ qrHex: dark ? C.dark : C.light,
+ finalHex: idx >= 0 ? art.pal.hexes[idx] : C.board,
+ k: Math.max(0, Math.min(DEMO_TEXT.length - 1, Math.floor((c / size) * DEMO_TEXT.length + (R() - 0.5) * 2.5))),
+ jx: R(),
+ jy: R() - 0.5,
+ bend: R() - 0.5,
+ t0: 2.72 + (c / size) * 0.55 + R() * 0.55,
+ tw: 5.95 + ((c + r) / (2 * (size - 1))) * 0.95 + R() * 0.08,
+ })
+ }
+ }
+ const finders = [[0, 0], [qr.size - 7, 0], [0, qr.size - 7]].map(([x, y]) => [placement.left + x, placement.top + y])
+ const topColors = art.usage.slice(0, 8).map((u) => u.hex)
+ const burst = Array.from({ length: 26 }, () => ({
+ a: -Math.PI / 2 + (R() - 0.5) * 2.2,
+ v: 520 + R() * 520,
+ r: 9 + R() * 8,
+ hex: topColors[Math.floor(R() * topColors.length)] || C.brand2,
+ }))
+ const groups = new Map()
+ const pegDots = []
+ for (let y = 20; y < U; y += 40) for (let x = 20; x < U; x += 40) pegDots.push(x, y, 2.4)
+ let layout = null // 文本排版缓存(首帧测量)
+
+ const textLayout = (ctx) => {
+ if (layout) return layout
+ ctx.font = F('bold', 64)
+ const x0 = 170
+ const charX = []
+ for (let i = 0; i <= DEMO_TEXT.length; i += 1) charX.push(x0 + ctx.measureText(DEMO_TEXT.slice(0, i)).width)
+ layout = { charX, y: 616 }
+ return layout
+ }
+
+ const boardAt = (t) => {
+ let cx = 540; let cy = 590; let B = 640
+ const a = easeInOut(prog(t, 7.6, 8.2))
+ cx = lerp(cx, 356, a); B = lerp(B, 560, a)
+ const b = easeInOut(prog(t, 10.05, 10.75))
+ cx = lerp(cx, 540, b); cy = lerp(cy, 410, b); B = lerp(B, 450, b)
+ return { cx, cy, B }
+ }
+ const gridOf = (L) => {
+ const pad = L.B * 0.05
+ const cell = (L.B - pad * 2) / size
+ return { cell, ox: L.cx - L.B / 2 + pad, oy: L.cy - L.B / 2 + pad }
+ }
+
+ const drawPlate = (ctx, L, alpha) => {
+ const x = L.cx - L.B / 2
+ const y = L.cy - L.B / 2
+ const r = L.B * 0.045
+ ctx.globalAlpha = alpha
+ softShadow(ctx, x, y, L.B, L.B, r, 0.16)
+ ctx.fillStyle = C.board
+ ctx.beginPath(); rr(ctx, x, y, L.B, L.B, r); ctx.fill()
+ ctx.globalAlpha = 1
+ }
+
+ // 背景:米色钉板 + 缓慢漂浮的装饰豆
+ const drawBackground = (ctx, t) => {
+ ctx.fillStyle = C.bg
+ ctx.fillRect(0, 0, U, U)
+ ctx.fillStyle = 'rgba(60,45,25,0.065)'
+ fillCircles(ctx, pegDots, 1, 0, 0)
+ DECOR.forEach(([x, y, r, hex], i) => {
+ const dx = Math.sin(t * 0.9 + i * 1.7) * 10
+ const dy = Math.cos(t * 0.7 + i * 2.3) * 12
+ ctx.globalAlpha = 0.55
+ addBead(groups, hex, x + dx, y + dy, r)
+ })
+ flushBeads(ctx, groups)
+ ctx.globalAlpha = 1
+ }
+
+ const drawCaption = (ctx, t) => {
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ SCENES.forEach((s, i) => {
+ if (t < s.a || t > s.b) return
+ const inP = easeOut(prog(t, s.a + 0.05, s.a + 0.5))
+ const outP = easeIn(prog(t, s.b - 0.32, s.b))
+ const alpha = inP * (1 - outP)
+ if (alpha <= 0) return
+ const dy = (1 - inP) * 26 - outP * 14
+ ctx.globalAlpha = alpha
+ ctx.font = F('bold', 24)
+ const label = `第 ${i + 1} 步`
+ const w = ctx.measureText(label).width + 40
+ ctx.fillStyle = C.brandSoft
+ ctx.beginPath(); rr(ctx, 540 - w / 2, 70 + dy, w, 46, 23); ctx.fill()
+ ctx.fillStyle = C.brand
+ ctx.fillText(label, 540, 94 + dy)
+ ctx.fillStyle = C.ink
+ ctx.font = F('bold', 60)
+ ctx.fillText(s.title, 540, 170 + dy * 1.3)
+ ctx.fillStyle = C.muted
+ ctx.font = F('normal', 30)
+ ctx.fillText(s.sub, 540, 232 + dy * 1.6)
+ })
+ ctx.globalAlpha = 1
+ }
+
+ const drawProgress = (ctx, t) => {
+ const alpha = prog(t, 0, 0.35) * (1 - prog(t, 10.0, 10.4))
+ if (alpha <= 0) return
+ ctx.globalAlpha = alpha
+ const w = 110; const gap = 16; const x0 = 540 - (w * 4 + gap * 3) / 2
+ SCENES.forEach((s, i) => {
+ const x = x0 + i * (w + gap)
+ ctx.fillStyle = 'rgba(22,33,31,0.1)'
+ ctx.beginPath(); rr(ctx, x, 1010, w, 8, 4); ctx.fill()
+ const p = prog(t, s.a, s.b)
+ if (p > 0) {
+ ctx.fillStyle = C.brand
+ ctx.beginPath(); rr(ctx, x, 1010, Math.max(8, w * p), 8, 4); ctx.fill()
+ }
+ })
+ ctx.globalAlpha = 1
+ }
+
+ // 第 1 幕:输入框 + 打字
+ const drawInput = (ctx, t) => {
+ if (t > 3.35) return
+ const inP = easeBack(prog(t, 0.1, 0.65))
+ const outP = easeInOut(prog(t, 2.6, 3.15))
+ const alpha = prog(t, 0.1, 0.4)
+ if (alpha <= 0) return
+ const L = textLayout(ctx)
+ const press = 1 - 0.08 * bump(t, 2.1, 2.4)
+ const scale = lerp(0.88, 1, inP) * lerp(1, 0.95, outP)
+ ctx.save()
+ ctx.translate(540, 590)
+ ctx.scale(scale, scale)
+ ctx.translate(-540, -590)
+ ctx.globalAlpha = alpha * (1 - outP)
+ softShadow(ctx, 130, 495, 820, 190, 40, 0.14)
+ ctx.fillStyle = '#ffffff'
+ ctx.beginPath(); rr(ctx, 130, 495, 820, 190, 40); ctx.fill()
+ ctx.textAlign = 'left'
+ ctx.textBaseline = 'middle'
+ ctx.fillStyle = C.muted
+ ctx.font = F('normal', 24)
+ ctx.fillText('扫码后显示的内容', 170, 548)
+ // 发送按钮
+ ctx.save()
+ ctx.translate(880, 590)
+ ctx.scale(press, press)
+ ctx.fillStyle = C.brand
+ ctx.beginPath(); ctx.arc(0, 0, 44, 0, TAU); ctx.fill()
+ ctx.strokeStyle = '#fff'
+ ctx.lineWidth = 7
+ ctx.lineCap = 'round'
+ ctx.lineJoin = 'round'
+ ctx.beginPath(); ctx.moveTo(-14, 0); ctx.lineTo(14, 0); ctx.moveTo(2, -13); ctx.lineTo(15, 0); ctx.lineTo(2, 13); ctx.stroke()
+ ctx.restore()
+ ctx.restore()
+ // 文字逐字出现,随后逐字化作豆子
+ const count = Math.floor(prog(t, 0.55, 1.9) * DEMO_TEXT.length + 0.0001)
+ ctx.font = F('bold', 64)
+ ctx.textAlign = 'left'
+ ctx.fillStyle = C.ink
+ for (let i = 0; i < count; i += 1) {
+ const start = 2.7 + (i / DEMO_TEXT.length) * 0.55
+ const a = alpha * (1 - prog(t, start, start + 0.35))
+ if (a <= 0) continue
+ ctx.globalAlpha = a
+ ctx.fillText(DEMO_TEXT[i], L.charX[i], L.y)
+ }
+ const typing = t >= 0.55 && t <= 1.9
+ if (t < 2.55 && (typing || Math.floor(t / 0.4) % 2 === 0)) {
+ ctx.globalAlpha = alpha
+ ctx.fillStyle = C.brand2
+ ctx.fillRect(L.charX[count] + 4, L.y - 34, 5, 68)
+ }
+ ctx.globalAlpha = 1
+ }
+
+ // 主画板上的豆子:飞入 → 黑白二维码 → 扫过换色
+ const drawMainBeads = (ctx, t, L) => {
+ const G = gridOf(L)
+ const TL = textLayout(ctx)
+ const baseR = G.cell * BEAD_R
+ for (let i = 0; i < cells.length; i += 1) {
+ const b = cells[i]
+ if (t < b.t0) continue
+ const dx = G.ox + (b.c + 0.5) * G.cell
+ const dy = G.oy + (b.r + 0.5) * G.cell
+ let x = dx; let y = dy; let s = 1
+ const f = prog(t, b.t0, b.t0 + FLIGHT)
+ if (f < 1) {
+ const sx = TL.charX[b.k] + b.jx * (TL.charX[b.k + 1] - TL.charX[b.k])
+ const sy = TL.y + b.jy * 44
+ const cx = (sx + dx) / 2 + b.bend * 220
+ const cy = Math.min(sy, dy) - 110 - Math.abs(b.bend) * 160
+ const e = easeInOut(f)
+ const u = 1 - e
+ x = u * u * sx + 2 * u * e * cx + e * e * dx
+ y = u * u * sy + 2 * u * e * cy + e * e * dy
+ s = lerp(0.55, 1, easeOut(f))
+ } else {
+ s = 1 + 0.16 * bump(t, b.t0 + FLIGHT, b.t0 + FLIGHT + 0.2)
+ }
+ s += 0.3 * bump(t, b.tw - 0.05, b.tw + 0.25)
+ addBead(groups, t >= b.tw ? b.finalHex : b.qrHex, x, y, baseR * s)
+ }
+ flushBeads(ctx, groups)
+ }
+
+ const drawFinalBeads = (ctx, L) => {
+ const G = gridOf(L)
+ const r = G.cell * BEAD_R
+ for (let i = 0; i < cells.length; i += 1) {
+ const b = cells[i]
+ addBead(groups, b.finalHex, G.ox + (b.c + 0.5) * G.cell, G.oy + (b.r + 0.5) * G.cell, r)
+ }
+ flushBeads(ctx, groups)
+ }
+
+ // 二维码定位图形高亮
+ const drawFinders = (ctx, t, L) => {
+ const p = prog(t, 4.15, 4.95)
+ if (p <= 0 || p >= 1) return
+ const G = gridOf(L)
+ const alpha = Math.sin(Math.PI * p)
+ const grow = (1 - easeOut(p)) * 18
+ ctx.strokeStyle = C.brand2
+ ctx.lineWidth = 6
+ ctx.globalAlpha = alpha
+ finders.forEach(([c, r]) => {
+ const x = G.ox + c * G.cell - 6 - grow
+ const y = G.oy + r * G.cell - 6 - grow
+ const w = 7 * G.cell + 12 + grow * 2
+ ctx.beginPath(); rr(ctx, x, y, w, w, 16); ctx.stroke()
+ })
+ ctx.globalAlpha = 1
+ }
+
+ // 沿对角线扫过的光带
+ const drawGleam = (ctx, L, p, strength) => {
+ if (p <= 0 || p >= 1) return
+ const x = L.cx - L.B / 2
+ const y = L.cy - L.B / 2
+ const g = ctx.createLinearGradient(x, y, x + L.B, y + L.B)
+ const s0 = clamp01(p - 0.12); const s1 = clamp01(p); const s2 = clamp01(p + 0.04)
+ g.addColorStop(0, 'rgba(255,255,255,0)')
+ if (s0 > 0) g.addColorStop(s0, 'rgba(255,255,255,0)')
+ g.addColorStop(s1, `rgba(255,255,255,${strength})`)
+ if (s2 < 1) g.addColorStop(s2, 'rgba(255,255,255,0)')
+ g.addColorStop(1, 'rgba(255,255,255,0)')
+ ctx.save()
+ ctx.beginPath(); rr(ctx, x, y, L.B, L.B, L.B * 0.045); ctx.clip()
+ ctx.fillStyle = g
+ ctx.fillRect(x, y, L.B, L.B)
+ ctx.restore()
+ }
+
+ // 第 3 幕:照片飞入并铺到画板上
+ const drawPhoto = (ctx, t, L) => {
+ if (t < 4.95 || t > 6.65) return
+ const a = easeOut(prog(t, 5.0, 5.55))
+ const b = easeInOut(prog(t, 5.6, 6.05))
+ let x = lerp(1320, 800, a); let y = lerp(250, 330, a); let rot = lerp(0.4, -0.12, a); let s = 300
+ 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)
+ const frame = lerp(14, 0, b)
+ const alpha = 1 - prog(t, 6.0, 6.6)
+ ctx.save()
+ ctx.globalAlpha = alpha
+ ctx.translate(x, y)
+ ctx.rotate(rot)
+ const half = s / 2 + frame
+ if (b < 1) softShadow(ctx, -half, -half, half * 2, half * 2, 18, 0.2 * (1 - b))
+ ctx.fillStyle = '#ffffff'
+ ctx.beginPath(); rr(ctx, -half, -half, half * 2, half * 2, 18); ctx.fill()
+ ctx.beginPath(); rr(ctx, -s / 2, -s / 2, s, s, 10); ctx.clip()
+ drawIllustration(ctx, -s / 2, -s / 2, s)
+ ctx.restore()
+ ctx.globalAlpha = 1
+ }
+
+ // 第 4 幕:手机扫码
+ const drawPhone = (ctx, t) => {
+ const enter = easeOut(prog(t, 7.75, 8.35))
+ const exit = easeIn(prog(t, 9.95, 10.4))
+ if (enter <= 0 || exit >= 1) return
+ const w = 340; const h = 660
+ const px = 800
+ const py = 590 + (1 - enter) * 860 + exit * 860
+ const rot = lerp(0.16, -0.035, enter)
+ ctx.save()
+ ctx.translate(px, py)
+ ctx.rotate(rot)
+ softShadow(ctx, -w / 2, -h / 2, w, h, 60, 0.28)
+ ctx.fillStyle = '#1b2321'
+ ctx.beginPath(); rr(ctx, -w / 2, -h / 2, w, h, 60); ctx.fill()
+ ctx.fillStyle = '#2c3633'
+ ctx.fillRect(w / 2 - 1, -h / 2 + 150, 5, 70)
+ // 屏幕
+ const sw = w - 24; const sh = h - 24
+ ctx.save()
+ ctx.beginPath(); rr(ctx, -sw / 2, -sh / 2, sw, sh, 48); ctx.clip()
+ ctx.fillStyle = '#0e1312'
+ ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
+ const jx = Math.sin(t * 3.1) * 3
+ const jy = Math.cos(t * 2.3) * 3
+ const cam = { cx: jx, cy: -30 + jy, B: 250 }
+ ctx.globalAlpha = 0.92
+ ctx.fillStyle = C.bg
+ ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
+ ctx.globalAlpha = 1
+ drawPlate(ctx, cam, 1)
+ drawFinalBeads(ctx, cam)
+ ctx.fillStyle = 'rgba(10,15,14,0.45)'
+ ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
+ // 取景框
+ const detect = prog(t, 9.0, 9.3)
+ const snap = t >= 9.0 ? 1 + 0.1 * (1 - easeOut(detect)) : 1
+ const half = 146 * snap
+ ctx.strokeStyle = t >= 9.0 ? C.wechat : '#ffffff'
+ ctx.lineWidth = 6
+ ctx.lineCap = 'round'
+ const arm = 34
+ ctx.beginPath()
+ ;[[-1, -1], [1, -1], [1, 1], [-1, 1]].forEach(([sx, sy]) => {
+ const x = cam.cx + sx * half
+ const y = cam.cy + sy * half
+ ctx.moveTo(x, y - sy * arm); ctx.lineTo(x, y); ctx.lineTo(x - sx * arm, y)
+ })
+ ctx.stroke()
+ // 扫描线
+ const sp = prog(t, 8.3, 9.0)
+ if (sp > 0 && sp < 1) {
+ const top = cam.cy - 140
+ const y = lerp(top, cam.cy + 140, easeInOut(sp))
+ const g = ctx.createLinearGradient(0, y - 80, 0, y)
+ g.addColorStop(0, 'rgba(7,193,96,0)')
+ g.addColorStop(1, 'rgba(7,193,96,0.35)')
+ ctx.fillStyle = g
+ ctx.fillRect(cam.cx - 140, Math.max(top, y - 80), 280, y - Math.max(top, y - 80))
+ ctx.fillStyle = C.wechat
+ ctx.fillRect(cam.cx - 140, y - 2, 280, 4)
+ }
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ ctx.fillStyle = '#ffffff'
+ ctx.font = F('bold', 24)
+ ctx.fillText('扫一扫', 0, -sh / 2 + 78)
+ if (t < 9.0) {
+ ctx.globalAlpha = 0.7
+ ctx.font = F('normal', 19)
+ ctx.fillText('将二维码放入框内', 0, cam.cy + 185)
+ ctx.globalAlpha = 1
+ }
+ const flash = t >= 9.0 ? 0.35 * (1 - prog(t, 9.0, 9.25)) : 0
+ if (flash > 0) {
+ ctx.fillStyle = `rgba(255,255,255,${flash.toFixed(3)})`
+ ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
+ }
+ // 识别结果卡片
+ const rp = prog(t, 9.1, 9.5)
+ if (rp > 0) {
+ const cw = sw - 36; const ch = 170
+ const cy = sh / 2 - 18 - ch + (1 - easeBack(rp)) * 220
+ ctx.fillStyle = '#ffffff'
+ ctx.beginPath(); rr(ctx, -cw / 2, cy, cw, ch, 28); ctx.fill()
+ ctx.fillStyle = C.wechat
+ ctx.beginPath(); ctx.arc(0, cy + 44, 22, 0, TAU); ctx.fill()
+ ctx.strokeStyle = '#fff'
+ ctx.lineWidth = 5
+ ctx.lineJoin = 'round'
+ ctx.beginPath(); ctx.moveTo(-9, cy + 44); ctx.lineTo(-2, cy + 51); ctx.lineTo(10, cy + 37); ctx.stroke()
+ ctx.fillStyle = C.ink
+ ctx.font = F('bold', 30)
+ ctx.fillText(DEMO_TEXT, 0, cy + 100)
+ ctx.fillStyle = C.muted
+ ctx.font = F('normal', 18)
+ ctx.fillText('扫码结果', 0, cy + 138)
+ }
+ ctx.restore()
+ // 灵动岛
+ ctx.fillStyle = '#000'
+ ctx.beginPath(); rr(ctx, -52, -h / 2 + 26, 104, 30, 15); ctx.fill()
+ ctx.restore()
+
+ // 识别成功:豆子礼花
+ const bt = t - 9.2
+ if (bt > 0 && bt < 1.4) {
+ const ox = px; const oy = py + 200
+ const fade = 1 - prog(bt, 0.9, 1.4)
+ ctx.globalAlpha = fade
+ burst.forEach((p) => {
+ const x = ox + Math.cos(p.a) * p.v * bt
+ const y = oy + Math.sin(p.a) * p.v * bt + 900 * bt * bt
+ addBead(groups, p.hex, x, y, p.r)
+ })
+ flushBeads(ctx, groups)
+ ctx.globalAlpha = 1
+ }
+ }
+
+ // 片尾
+ const drawEnd = (ctx, t) => {
+ if (t < 10.4) return
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ const a = easeOut(prog(t, 10.45, 10.95))
+ ctx.globalAlpha = a
+ ctx.fillStyle = C.ink
+ ctx.font = F('bold', 64)
+ ctx.fillText('豆维 · 可扫码的拼豆', 540, 745 + (1 - a) * 30)
+ const b = easeOut(prog(t, 10.6, 11.1))
+ ctx.globalAlpha = b
+ ctx.fillStyle = C.muted
+ ctx.font = F('normal', 32)
+ ctx.fillText('把一句话藏进拼豆里,微信一扫就能读出来', 540, 812 + (1 - b) * 30)
+ const chips = ['MARD 标准色号', 'A4 打印图纸', '微信一扫即读']
+ ctx.font = F('bold', 26)
+ const widths = chips.map((s) => ctx.measureText(s).width + 44)
+ const total = widths.reduce((s, v) => s + v, 0) + 18 * (chips.length - 1)
+ let x = 540 - total / 2
+ chips.forEach((label, i) => {
+ const p = easeBack(prog(t, 10.85 + i * 0.12, 11.3 + i * 0.12))
+ if (p > 0) {
+ ctx.globalAlpha = clamp01(p)
+ const cy = 892 + (1 - p) * 24
+ ctx.fillStyle = C.brandSoft
+ ctx.beginPath(); rr(ctx, x, cy - 26, widths[i], 52, 26); ctx.fill()
+ ctx.fillStyle = C.brand
+ ctx.fillText(label, x + widths[i] / 2, cy)
+ }
+ x += widths[i] + 18
+ })
+ ctx.globalAlpha = 1
+ }
+
+ const draw = (ctx, time, pixelWidth) => {
+ const t = loop ? ((time % DURATION) + DURATION) % DURATION : Math.min(time, DURATION)
+ const k = pixelWidth / U
+ ctx.setTransform(k, 0, 0, k, 0, 0)
+ ctx.globalAlpha = 1
+ drawBackground(ctx, t)
+ const L = boardAt(t)
+ if (t >= 2.65) {
+ const p = easeBack(prog(t, 2.65, 3.15))
+ const s = lerp(0.92, 1, p)
+ const LS = { cx: L.cx, cy: L.cy, B: L.B * s }
+ drawPlate(ctx, LS, prog(t, 2.65, 3.0))
+ }
+ drawInput(ctx, t)
+ if (t >= 2.72) drawMainBeads(ctx, t, L)
+ drawFinders(ctx, t, L)
+ drawGleam(ctx, L, prog(t, 5.9, 7.0), 0.55)
+ drawGleam(ctx, L, prog(t, 11.0, 11.7), 0.4)
+ drawPhoto(ctx, t, L)
+ drawPhone(ctx, t)
+ drawCaption(ctx, t)
+ drawProgress(ctx, t)
+ drawEnd(ctx, t)
+ if (loop) {
+ const fade = easeInOut(prog(t, 11.45, 12))
+ if (fade > 0) {
+ ctx.globalAlpha = fade
+ ctx.fillStyle = C.bg
+ ctx.fillRect(0, 0, U, U)
+ ctx.globalAlpha = 1
+ }
+ }
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
+ }
+
+ // 关键时间点(供视频配乐对齐)
+ const timeline = { landings: cells.map((b) => b.t0 + FLIGHT), wipes: cells.map((b) => b.tw) }
+ return { duration: DURATION, text: DEMO_TEXT, artwork: art, draw, timeline }
+}
+
+module.exports = { createIntro, DEMO_TEXT, DURATION }
diff --git a/BeadQR/miniprogram/utils/palette-data.js b/BeadQR/miniprogram/utils/palette-data.js
new file mode 100644
index 0000000000000000000000000000000000000000..03c30718d7d65fbea2a205a75fabd95cbeb2e398
--- /dev/null
+++ b/BeadQR/miniprogram/utils/palette-data.js
@@ -0,0 +1,48 @@
+// MARD reference palette from the public 2026 color chart.
+// 291 colors = standard 221 (A-H, M) + 70 extended (P, Q, R, T, Y, ZG).
+const MARD_HEX = {
+ A1:"#f9f0cd", A2:"#fbfbd4", A3:"#fafc9f", A4:"#ffe953", A5:"#f4d738", A6:"#fdad49", A7:"#ff7c2f", A8:"#eaca49",
+ A9:"#ff995a", A10:"#ff9d55", A11:"#ffdd99", A12:"#fcb58f", A13:"#ffbb59", A14:"#ff6d40", A15:"#fdff44", A16:"#fef9ae",
+ A17:"#ffe36e", A18:"#fecf98", A19:"#fd7b72", A20:"#efcd67", A21:"#ffe395", A22:"#fff3a4", A23:"#f3d5bf", A24:"#fbf8c9",
+ A25:"#ffd67d", A26:"#ffbb27", B1:"#e6ee32", B2:"#5be419", B3:"#7cee9d", B4:"#1ef942", B5:"#00bd35", B6:"#5ae8ba",
+ B7:"#03ac88", B8:"#029d26", B9:"#26523a", B10:"#95d3c2", B11:"#5d722a", B12:"#156f40", B13:"#d9f794", B14:"#ade945",
+ B15:"#2e5132", B16:"#c6ed9c", B17:"#9bb13a", B18:"#e6ee49", B19:"#25b88c", B20:"#c2f0cc", B21:"#146a6b", B22:"#0b3c43",
+ B23:"#303921", B24:"#eefca5", B25:"#4e846d", B26:"#8c7a36", B27:"#d1dcc1", B28:"#9ee5b9", B29:"#c5e254", B30:"#ecfbd0",
+ B31:"#c4e6b5", B32:"#9bab5a", C1:"#e8ffe7", C2:"#bcf9f6", C3:"#a0e2fb", C4:"#42ccff", C5:"#01aceb", C6:"#50a9f0",
+ C7:"#0188d3", C8:"#1054c0", C9:"#314bca", C10:"#3ebce2", C11:"#03b9b9", C12:"#1c334d", C13:"#cde8ff", C14:"#d5fdff",
+ C15:"#23c4c6", C16:"#1757a8", C17:"#50d3ec", C18:"#1c3344", C19:"#1787a2", C20:"#0082be", C21:"#beddff", C22:"#67b4be",
+ C23:"#c2dceb", C24:"#7dc4ff", C25:"#a9e5e5", C26:"#2f99b3", C27:"#ebf5fc", C28:"#bbcfed", C29:"#4b5ba3", D1:"#aeb4f2",
+ D2:"#858edd", D3:"#3054af", D4:"#182a84", D5:"#b843c5", D6:"#ac7bde", D7:"#6e399a", D8:"#e2d3ff", D9:"#d5b9f8",
+ D10:"#361b50", D11:"#b9bae1", D12:"#de9ad4", D13:"#b90295", D14:"#8b279b", D15:"#2f1f90", D16:"#e2e1ee", D17:"#c4d4f6",
+ D18:"#a45ec7", D19:"#d8c3d7", D20:"#9c32b2", D21:"#9a009b", D22:"#333995", D23:"#eadafc", D24:"#7786e5", D25:"#484fc7",
+ D26:"#e9c3f6", E1:"#fdd3cc", E2:"#fecddf", E3:"#ff97c3", E4:"#e8649e", E5:"#f551a2", E6:"#ff346b", E7:"#c63578",
+ E8:"#ffdbe9", E9:"#e970cc", E10:"#d33893", E11:"#fcddd2", E12:"#ffa1c5", E13:"#b6006d", E14:"#ffd1ba", E15:"#f2cfd0",
+ E16:"#ffecde", E17:"#ffe2ea", E18:"#ffc9d6", E19:"#ffd2e7", E20:"#d8c7d1", E21:"#bd9da1", E22:"#cc78a7", E23:"#937a8d",
+ E24:"#f6e4f9", F1:"#fd957b", F2:"#fc3d45", F3:"#f74941", F4:"#fc283c", F5:"#d80127", F6:"#b0443d", F7:"#971937",
+ F8:"#bc0127", F9:"#e2677a", F10:"#a74d22", F11:"#6f201f", F12:"#fd4d6a", F13:"#dd422f", F14:"#ffa9ad", F15:"#c80020",
+ F16:"#ffd9c8", F17:"#f79b71", F18:"#d37c46", F19:"#c1444a", F20:"#cd9391", F21:"#f4b1b4", F22:"#ffd0cb", F23:"#f57e66",
+ F24:"#fcc1c4", F25:"#e54b4f", G1:"#ffe2ce", G2:"#ffcaaa", G3:"#f4c3a5", G4:"#e1b383", G5:"#ed9435", G6:"#f59734",
+ G7:"#9d5b3e", G8:"#592a21", G9:"#e6b483", G10:"#c88135", G11:"#e0c593", G12:"#ebbb83", G13:"#b7714a", G14:"#8d614c",
+ G15:"#fcf9e0", G16:"#f2d9ba", G17:"#56403c", G18:"#ffe4cc", G19:"#e1943a", G20:"#a94023", G21:"#cb8e77", H1:"#e2e2e2",
+ H2:"#ffffff", H3:"#b3b3b3", H4:"#868686", H5:"#474747", H6:"#2c2c2c", H7:"#000000", H8:"#e7d6db", H9:"#e4e7e3",
+ H10:"#eee9ea", H11:"#cecdd5", H12:"#fff5ed", H13:"#f3e1c9", H14:"#cfd7d3", H15:"#98a6a8", H16:"#3b2f23", H17:"#f1eded",
+ H18:"#fffdf0", H19:"#f6efe2", H20:"#949fa3", H21:"#f7f3e4", H22:"#cacad5", H23:"#9a9d94", M1:"#bcc6b8", M2:"#8aa385",
+ M3:"#697d80", M4:"#dacebe", M5:"#d0ccaa", M6:"#b0a782", M7:"#b4a497", M8:"#b38281", M9:"#a58767", M10:"#c5b1bc",
+ M11:"#9f7494", M12:"#644749", M13:"#d19066", M14:"#c77361", M15:"#757d7b", P1:"#fcf8f9", P2:"#bda9ab", P3:"#aedda9",
+ P4:"#fda49e", P5:"#ec8d3d", P6:"#60cfa8", P7:"#eb9271", P8:"#f0d958", P9:"#d9d9d9", P10:"#d5c8e9", P11:"#f3ecc8",
+ P12:"#e6eef1", P13:"#a9cbf1", P14:"#3177b0", P15:"#668575", P16:"#ffbe46", P17:"#ffa324", P18:"#feb89f", P19:"#ffe0e8",
+ P20:"#febecf", P21:"#ecbec0", P22:"#e4a89e", P23:"#a56269", Q1:"#f2a5e8", Q2:"#73b29e", Q3:"#ffff00", Q4:"#ffebfa",
+ Q5:"#4f5e5b", R1:"#d50e21", R2:"#f92e83", R3:"#fd8225", R4:"#f8ec31", R5:"#34c75b", R6:"#25b891", R7:"#17779d",
+ R8:"#1b60c3", R9:"#9a56b4", R10:"#ffdb4d", R11:"#ffebfa", R12:"#d8d5ce", R13:"#55514c", R14:"#9ee4df", R15:"#77cee9",
+ R16:"#3dcfca", R17:"#4a867a", R18:"#7fcd9d", R19:"#cde55d", R20:"#e8c7b4", R21:"#ad6f3c", R22:"#6c372f", R23:"#feb872",
+ R24:"#f2c1c0", R25:"#c9675d", R26:"#d293be", R27:"#ea8cb1", R28:"#9c87d6", T1:"#e2dfd7", Y1:"#fd6fb4", Y2:"#feb481",
+ Y3:"#d7faa0", Y4:"#8bdbfa", Y5:"#e987ea", ZG1:"#daabb3", ZG2:"#d6aa87", ZG3:"#c1bd8d", ZG4:"#96869f", ZG5:"#8490a6",
+ ZG6:"#94bfe2", ZG7:"#e2a9d2", ZG8:"#ab91c0",
+}
+
+const MARD_PALETTE = Object.entries(MARD_HEX).map(([id, hex]) => ({ id, hex }))
+
+// 智能近色色库的“远点优先”排序(离线预计算,避免真机启动时 2000 万次距离运算)
+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"]
+
+module.exports = { MARD_PALETTE, MARD_SMART_ORDER }
diff --git a/BeadQR/miniprogram/utils/palette.js b/BeadQR/miniprogram/utils/palette.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4c42ce2d3f3846eb7b9e57dea88c2746e91b455
--- /dev/null
+++ b/BeadQR/miniprogram/utils/palette.js
@@ -0,0 +1,74 @@
+// MARD 色库预设 + 预处理(Typed Array 结构,供匹配引擎高速读取)
+const { MARD_PALETTE, MARD_SMART_ORDER } = require('./palette-data')
+const { hexToRgb, rgbToOklabInto, luma } = require('./color')
+
+// 颜色匹配模式:darkMax/lightMin 为二维码深/浅模块允许的亮度范围
+const MATCH_MODES = [
+ { id: 'rich', label: '色彩优先', hint: '更像原图', darkMax: 0.4, lightMin: 0.74 },
+ { id: 'balanced', label: '均衡', hint: '推荐', darkMax: 0.36, lightMin: 0.78 },
+ { id: 'strict', label: '扫码优先', hint: '最稳', darkMax: 0.3, lightMin: 0.84 },
+]
+
+const byId = new Map(MARD_PALETTE.map((c) => [c.id, c]))
+const pick = (ids) => ids.map((id) => byId.get(id)).filter(Boolean)
+const STANDARD_221 = MARD_PALETTE.filter((c) => /^[A-HM]\d+$/.test(c.id))
+
+const PRESETS = [
+ { id: '221', label: '标准 221 色', short: '221 色', detail: 'MARD 标准色,最常见', list: () => STANDARD_221 },
+ { id: '291', label: '完整 291 色', short: '291 色', detail: '含扩展色,颜色最全', list: () => MARD_PALETTE },
+ ...[144, 96, 72, 48, 24].map((n) => ({
+ id: String(n),
+ label: `精选 ${n} 色`,
+ short: `${n} 色`,
+ detail: n <= 48 ? '入门套装,买豆更省' : '常用色,兼顾效果与成本',
+ list: () => pick(MARD_SMART_ORDER.slice(0, n)),
+ })),
+]
+
+const cache = {}
+
+function preparePalette(presetId) {
+ if (cache[presetId]) return cache[presetId]
+ const preset = PRESETS.find((p) => p.id === presetId) || PRESETS[0]
+ const list = preset.list()
+ const n = list.length
+ const rgb = new Uint8Array(n * 3)
+ const lab = new Float32Array(n * 3)
+ const lum = new Float32Array(n)
+ list.forEach((c, i) => {
+ const [r, g, b] = hexToRgb(c.hex)
+ rgb[i * 3] = r; rgb[i * 3 + 1] = g; rgb[i * 3 + 2] = b
+ rgbToOklabInto(r, g, b, lab, i * 3)
+ lum[i] = luma(r, g, b)
+ })
+ // 色与色之间的距离矩阵(291² ≈ 8.5 万项,一次性计算)
+ const pair = new Float32Array(n * n)
+ for (let i = 0; i < n; i += 1) {
+ for (let j = i + 1; j < n; j += 1) {
+ const dl = lab[i * 3] - lab[j * 3]
+ const da = lab[i * 3 + 1] - lab[j * 3 + 1]
+ const db = lab[i * 3 + 2] - lab[j * 3 + 2]
+ const d = Math.sqrt(dl * dl + da * da + db * db)
+ pair[i * n + j] = d
+ pair[j * n + i] = d
+ }
+ }
+ const prepared = {
+ id: preset.id,
+ label: preset.label,
+ short: preset.short,
+ n,
+ ids: list.map((c) => c.id),
+ hexes: list.map((c) => c.hex),
+ rgb,
+ lab,
+ lum,
+ pair,
+ subsets: {},
+ luts: {},
+ }
+ cache[presetId] = prepared
+ return prepared
+}
+
+module.exports = { MATCH_MODES, PRESETS, preparePalette }
diff --git a/BeadQR/miniprogram/utils/pattern.js b/BeadQR/miniprogram/utils/pattern.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc570ad3b7ec7f0a2d3a3dc71041bd8d0944169c
--- /dev/null
+++ b/BeadQR/miniprogram/utils/pattern.js
@@ -0,0 +1,381 @@
+// Canvas 绘制:预览、PNG 图纸页、作品信息页、水印
+// 性能要点:按颜色批量合并路径,每种颜色只 fill 一次;字体只设置一次;网格线合并成单条路径
+const { lumaHex } = require('./color')
+
+const BOARD = '#f1eee7' // 钉板底色
+const PEG = '#d9d3c7' // 空钉位
+const INK = '#1d2b2a'
+const MUTED = '#6b7671'
+const BRAND = '#0f766e'
+
+function roundRectPath(ctx, x, y, w, h, r) {
+ const rr = Math.min(r, w / 2, h / 2)
+ ctx.moveTo(x + rr, y)
+ ctx.arcTo(x + w, y, x + w, y + h, rr)
+ ctx.arcTo(x + w, y + h, x, y + h, rr)
+ ctx.arcTo(x, y + h, x, y, rr)
+ ctx.arcTo(x, y, x + w, y, rr)
+ ctx.closePath()
+}
+
+function fitText(ctx, value, maxWidth) {
+ const text = String(value || '')
+ if (ctx.measureText(text).width <= maxWidth) return text
+ let end = text.length
+ while (end > 0 && ctx.measureText(`${text.slice(0, end)}…`).width > maxWidth) end -= 1
+ return `${text.slice(0, end)}…`
+}
+
+// 把同色格子分组:返回 Map
+function groupCells(artwork, region) {
+ const { size, cells } = artwork
+ const r = region || { col: 0, row: 0, cols: size, rows: size }
+ const groups = new Map()
+ const empty = []
+ for (let y = r.row; y < r.row + r.rows; y += 1) {
+ for (let x = r.col; x < r.col + r.cols; x += 1) {
+ const c = cells[y * size + x]
+ if (c < 0) { empty.push(y * size + x); continue }
+ let list = groups.get(c)
+ if (!list) { list = []; groups.set(c, list) }
+ list.push(y * size + x)
+ }
+ }
+ return { groups, empty, region: r }
+}
+
+/**
+ * 绘制珠子
+ * opts: { x0, y0, cell, style: square|round|hollow, region }
+ */
+function drawBeads(ctx, artwork, opts) {
+ const { x0, y0, cell, style = 'square' } = opts
+ const { size, pal } = artwork
+ const { groups, empty, region } = groupCells(artwork, opts.region)
+ const ARC_CHUNK = 12
+ const pos = (i) => {
+ const x = i % size
+ const y = (i - x) / size
+ return [x0 + (x - region.col) * cell, y0 + (y - region.row) * cell]
+ }
+ ctx.fillStyle = BOARD
+ ctx.fillRect(x0, y0, region.cols * cell, region.rows * cell)
+ // 空钉位:小圆点,提示“这里不放豆”
+ if (empty.length) {
+ ctx.fillStyle = PEG
+ ctx.beginPath()
+ const r = cell * 0.14
+ empty.forEach((i, n) => {
+ const [x, y] = pos(i)
+ ctx.moveTo(x + cell / 2 + r, y + cell / 2)
+ ctx.arc(x + cell / 2, y + cell / 2, r, 0, Math.PI * 2)
+ if (n % ARC_CHUNK === ARC_CHUNK - 1) { ctx.fill(); ctx.beginPath() }
+ })
+ ctx.fill()
+ }
+ const radius = cell * 0.46
+ const hole = cell * 0.2
+ groups.forEach((list, c) => {
+ ctx.fillStyle = pal.hexes[c]
+ ctx.beginPath()
+ if (style === 'square') {
+ list.forEach((i) => {
+ const [x, y] = pos(i)
+ // 取整边界避免相邻方块之间出现发丝缝
+ const lx = Math.round(x); const ty = Math.round(y)
+ ctx.rect(lx, ty, Math.round(x + cell) - lx, Math.round(y + cell) - ty)
+ })
+ } else {
+ // 圆形子路径过多时渲染引擎会明显变慢,按小批提交(大图从数秒降到几十毫秒)
+ list.forEach((i, n) => {
+ const [x, y] = pos(i)
+ const cx = x + cell / 2; const cy = y + cell / 2
+ ctx.moveTo(cx + radius, cy)
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2)
+ if (style === 'hollow') {
+ ctx.moveTo(cx + hole, cy)
+ ctx.arc(cx, cy, hole, 0, Math.PI * 2, true)
+ }
+ if (n % ARC_CHUNK === ARC_CHUNK - 1) { ctx.fill(); ctx.beginPath() }
+ })
+ }
+ ctx.fill()
+ })
+}
+
+// 色号文字:先按深/浅文字颜色分两批
+function drawCodes(ctx, artwork, opts) {
+ const { x0, y0, cell, style = 'square' } = opts
+ const { size, cells, pal } = artwork
+ const region = opts.region || { col: 0, row: 0, cols: size, rows: size }
+ const light = pal.hexes.map((hex) => lumaHex(hex) > 0.56)
+ const fontSize = Math.max(8, Math.round(cell * (style === 'hollow' ? 0.26 : 0.32)))
+ ctx.font = `bold ${fontSize}px sans-serif`
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ const pass = (wantLight) => {
+ ctx.fillStyle = wantLight ? INK : '#ffffff'
+ for (let y = region.row; y < region.row + region.rows; y += 1) {
+ for (let x = region.col; x < region.col + region.cols; x += 1) {
+ const c = cells[y * size + x]
+ if (c < 0) continue
+ const useInk = style === 'hollow' ? true : light[c]
+ if (useInk !== wantLight) continue
+ ctx.fillText(pal.ids[c], x0 + (x - region.col + 0.5) * cell, y0 + (y - region.row + 0.5) * cell + fontSize * 0.04)
+ }
+ }
+ }
+ pass(true)
+ pass(false)
+ ctx.textAlign = 'start'
+ ctx.textBaseline = 'alphabetic'
+}
+
+function rgba(hex, alpha) {
+ const v = parseInt(hex.slice(1), 16)
+ return `rgba(${(v >> 16) & 255},${(v >> 8) & 255},${v & 255},${alpha})`
+}
+
+// 网格 + 坐标:细线每格一条,粗线每 interval 格一条(按全局坐标对齐)
+function drawGrid(ctx, opts) {
+ const { x0, y0, cell, region, interval = 10, color = BRAND, opacity = 60, labels = true } = opts
+ const w = region.cols * cell
+ const h = region.rows * cell
+ const alpha = opacity / 100
+ ctx.lineWidth = Math.max(1, cell * 0.02)
+ ctx.strokeStyle = rgba(color, alpha * 0.28)
+ ctx.beginPath()
+ 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) }
+ 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) }
+ ctx.stroke()
+ ctx.lineWidth = Math.max(2, cell * 0.06)
+ ctx.strokeStyle = rgba(color, alpha)
+ ctx.beginPath()
+ for (let i = 0; i <= region.cols; i += 1) {
+ const g = region.col + i
+ if (g % interval !== 0 && i !== 0 && i !== region.cols) continue
+ const x = x0 + i * cell; ctx.moveTo(x, y0); ctx.lineTo(x, y0 + h)
+ }
+ for (let i = 0; i <= region.rows; i += 1) {
+ const g = region.row + i
+ if (g % interval !== 0 && i !== 0 && i !== region.rows) continue
+ const y = y0 + i * cell; ctx.moveTo(x0, y); ctx.lineTo(x0 + w, y)
+ }
+ ctx.stroke()
+ if (!labels) return
+ const fontSize = Math.max(10, Math.round(Math.min(cell * 0.42, 34)))
+ ctx.font = `${fontSize}px sans-serif`
+ ctx.fillStyle = rgba(color, Math.max(0.7, alpha))
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ const step = cell < 22 ? interval : (cell < 36 ? 5 : 1)
+ for (let i = 0; i < region.cols; i += 1) {
+ const g = region.col + i + 1
+ if (g % step !== 0 && g !== 1) continue
+ ctx.fillText(String(g), x0 + (i + 0.5) * cell, y0 - fontSize * 0.9)
+ }
+ for (let i = 0; i < region.rows; i += 1) {
+ const g = region.row + i + 1
+ if (g % step !== 0 && g !== 1) continue
+ ctx.fillText(String(g), x0 - fontSize * 1.1, y0 + (i + 0.5) * cell)
+ }
+ ctx.textAlign = 'start'
+ ctx.textBaseline = 'alphabetic'
+}
+
+// 屏幕预览:整张作品,不带坐标(清爽)
+function renderPreview(canvas, artwork, pixelWidth, style) {
+ const size = artwork.size
+ const pad = Math.round(pixelWidth * 0.03)
+ const cell = (pixelWidth - pad * 2) / size
+ canvas.width = pixelWidth
+ canvas.height = pixelWidth
+ const ctx = canvas.getContext('2d')
+ ctx.fillStyle = BOARD
+ ctx.fillRect(0, 0, pixelWidth, pixelWidth)
+ drawBeads(ctx, artwork, { x0: pad, y0: pad, cell, style })
+}
+
+// —— PNG 图纸页 ——
+function patternPageLayout(size, cell) {
+ const axis = Math.max(48, Math.round(cell * 1.1))
+ const pad = Math.max(48, Math.round(cell * 0.9))
+ const header = Math.max(120, Math.round(cell * 2.4))
+ const grid = size * cell
+ return { axis, pad, header, width: grid + axis + pad * 2, height: header + axis + grid + pad * 2 }
+}
+
+function drawPatternPage(ctx, artwork, opts) {
+ const { cell, style, grid, meta } = opts
+ const L = patternPageLayout(artwork.size, cell)
+ ctx.fillStyle = '#fbfaf7'
+ ctx.fillRect(0, 0, L.width, L.height)
+ // 头部
+ const unit = L.header / 120
+ ctx.fillStyle = INK
+ ctx.font = `bold ${Math.round(40 * unit)}px sans-serif`
+ ctx.textBaseline = 'middle'
+ ctx.fillText('豆维 · 拼豆图纸', L.pad, L.pad * 0.6 + L.header * 0.36)
+ ctx.font = `${Math.round(22 * unit)}px sans-serif`
+ ctx.fillStyle = MUTED
+ ctx.fillText(fitText(ctx, meta, L.width - L.pad * 2), L.pad, L.pad * 0.6 + L.header * 0.74)
+ ctx.textBaseline = 'alphabetic'
+ const x0 = L.pad + L.axis
+ const y0 = L.header + L.pad * 0.6 + L.axis
+ const region = { col: 0, row: 0, cols: artwork.size, rows: artwork.size }
+ drawBeads(ctx, artwork, { x0, y0, cell, style, region })
+ if (cell >= 16) drawCodes(ctx, artwork, { x0, y0, cell, style, region })
+ if (grid.enabled) drawGrid(ctx, { x0, y0, cell, region, interval: grid.interval, color: grid.color, opacity: grid.opacity })
+ return L
+}
+
+// —— 作品信息页(A4 比例,可分页)——
+function buildInfoPages({ artwork, width, text, paletteLabel, mpCode, thumbnail }) {
+ const height = Math.round(width * 297 / 210)
+ const u = width / 1600
+ const pad = 96 * u
+ const colors = artwork.usage.slice().sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }))
+ const columns = 4
+ const gap = 24 * u
+ const cardW = (width - pad * 2 - gap * (columns - 1)) / columns
+ const cardH = 124 * u
+ const headerH = 300 * u
+ // 缩略图区:headerH+64 起,高 400;清单标题与说明各占一行,避免与上方重叠
+ const listTitleY = headerH + 64 * u + 400 * u + 96 * u
+ const firstTop = listTitleY + 88 * u
+ const nextTop = 200 * u
+ const bottom = height - 110 * u
+ const rowsFirst = Math.max(1, Math.floor((bottom - firstTop + gap) / (cardH + gap)))
+ const rowsNext = Math.max(1, Math.floor((bottom - nextTop + gap) / (cardH + gap)))
+ const pages = []
+ let rest = colors.length
+ let first = true
+ do {
+ const take = Math.min(rest, (first ? rowsFirst : rowsNext) * columns)
+ pages.push(take)
+ rest -= take
+ first = false
+ } while (rest > 0)
+
+ const drawCard = (ctx, color, x, y) => {
+ ctx.fillStyle = '#ffffff'
+ ctx.beginPath(); roundRectPath(ctx, x, y, cardW, cardH, 18 * u); ctx.fill()
+ ctx.strokeStyle = '#e7e2d8'; ctx.lineWidth = 2 * u; ctx.stroke()
+ const sw = 76 * u
+ ctx.fillStyle = color.hex
+ ctx.beginPath(); ctx.arc(x + 24 * u + sw / 2, y + cardH / 2, sw / 2, 0, Math.PI * 2); ctx.fill()
+ ctx.strokeStyle = 'rgba(0,0,0,0.12)'; ctx.stroke()
+ ctx.fillStyle = INK
+ ctx.font = `bold ${Math.round(34 * u)}px sans-serif`
+ ctx.fillText(color.id, x + 24 * u + sw + 20 * u, y + 56 * u)
+ ctx.fillStyle = BRAND
+ ctx.font = `${Math.round(26 * u)}px sans-serif`
+ ctx.fillText(`${color.count} 颗`, x + 24 * u + sw + 20 * u, y + 96 * u)
+ }
+
+ const draw = (ctx, page) => {
+ ctx.textAlign = 'start'
+ ctx.textBaseline = 'alphabetic'
+ ctx.fillStyle = '#fbfaf7'
+ ctx.fillRect(0, 0, width, height)
+ let top = nextTop
+ if (page === 0) {
+ ctx.fillStyle = INK
+ ctx.fillRect(0, 0, width, headerH)
+ ctx.fillStyle = '#ffffff'
+ ctx.font = `bold ${Math.round(62 * u)}px sans-serif`
+ ctx.fillText('豆维 · 作品信息与用豆清单', pad, 132 * u)
+ ctx.fillStyle = '#9fd3c7'
+ ctx.font = `${Math.round(28 * u)}px sans-serif`
+ ctx.fillText('照着清单备豆,照着图纸拼,拼完用微信扫一扫', pad, 200 * u)
+ if (mpCode) {
+ const s = 200 * u
+ const x = width - pad - s
+ const y = (headerH - s) / 2
+ ctx.fillStyle = '#ffffff'
+ ctx.beginPath(); roundRectPath(ctx, x - 10 * u, y - 10 * u, s + 20 * u, s + 20 * u, 20 * u); ctx.fill()
+ ctx.drawImage(mpCode, x, y, s, s)
+ }
+ // 缩略图 + 参数
+ const thumbS = 400 * u
+ const ty = headerH + 64 * u
+ if (thumbnail) {
+ ctx.fillStyle = '#ffffff'
+ ctx.beginPath(); roundRectPath(ctx, pad, ty, thumbS, thumbS, 24 * u); ctx.fill()
+ const inner = thumbS - 32 * u
+ const cell = inner / artwork.size
+ drawBeads(ctx, artwork, { x0: pad + 16 * u, y0: ty + 16 * u, cell, style: 'square' })
+ }
+ const mx = thumbnail ? pad + thumbS + 56 * u : pad
+ const rows = [
+ ['扫码内容', text],
+ ['作品尺寸', `${artwork.size} × ${artwork.size} 格`],
+ ['用豆总数', `${artwork.beads.toLocaleString()} 颗 · ${artwork.usage.length} 个色号`],
+ ['二维码', `${artwork.qr.size} × ${artwork.qr.size} 格 · 纠错 ${artwork.qr.level}`],
+ ['色库', paletteLabel],
+ ]
+ rows.forEach(([k, v], idx) => {
+ const y = ty + 52 * u + idx * 74 * u
+ ctx.fillStyle = MUTED
+ ctx.font = `${Math.round(26 * u)}px sans-serif`
+ ctx.fillText(k, mx, y)
+ ctx.fillStyle = INK
+ ctx.font = `bold ${Math.round(28 * u)}px sans-serif`
+ ctx.fillText(fitText(ctx, v, width - pad - mx - 150 * u), mx + 150 * u, y)
+ })
+ ctx.fillStyle = INK
+ ctx.font = `bold ${Math.round(42 * u)}px sans-serif`
+ ctx.fillText('用豆清单', pad, listTitleY)
+ ctx.fillStyle = MUTED
+ ctx.font = `${Math.round(24 * u)}px sans-serif`
+ ctx.fillText('按色号排序 · 建议每色多备 5% 以防掉落', pad, listTitleY + 44 * u)
+ top = firstTop
+ } else {
+ ctx.fillStyle = INK
+ ctx.font = `bold ${Math.round(36 * u)}px sans-serif`
+ ctx.fillText(`用豆清单(续 ${page + 1}/${pages.length})`, pad, 120 * u)
+ }
+ const offset = pages.slice(0, page).reduce((s, v) => s + v, 0)
+ for (let i = 0; i < pages[page]; i += 1) {
+ const col = i % columns
+ const row = Math.floor(i / columns)
+ drawCard(ctx, colors[offset + i], pad + col * (cardW + gap), top + row * (cardH + gap))
+ }
+ ctx.fillStyle = '#a3aaa5'
+ ctx.font = `${Math.round(22 * u)}px sans-serif`
+ ctx.fillText(`豆维 · 可扫码的拼豆 第 ${page + 1} / ${pages.length} 页`, pad, height - 56 * u)
+ }
+ return { width, height, pageCount: pages.length, draw }
+}
+
+// 轻度斜向平铺水印
+function drawWatermark(ctx, w, h, text) {
+ const value = String(text || '').trim()
+ if (!value) return
+ const fontSize = Math.max(18, Math.round(w * 0.018))
+ const stepX = Math.max(fontSize * value.length * 1.6, w / 4)
+ const stepY = Math.max(fontSize * 6, w / 6)
+ ctx.save()
+ ctx.globalAlpha = 0.08
+ ctx.fillStyle = INK
+ ctx.font = `${fontSize}px sans-serif`
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ ctx.translate(w / 2, h / 2)
+ ctx.rotate(-Math.PI / 9)
+ const r = Math.hypot(w, h) / 2
+ for (let y = -r; y <= r; y += stepY) for (let x = -r; x <= r; x += stepX) ctx.fillText(value, x, y)
+ ctx.restore()
+}
+
+module.exports = {
+ BOARD,
+ drawBeads,
+ drawCodes,
+ drawGrid,
+ renderPreview,
+ patternPageLayout,
+ drawPatternPage,
+ buildInfoPages,
+ drawWatermark,
+}
diff --git a/BeadQR/miniprogram/utils/pdf.js b/BeadQR/miniprogram/utils/pdf.js
new file mode 100644
index 0000000000000000000000000000000000000000..34805d1053fe171327a272f9993ecb4d7bdd5906
--- /dev/null
+++ b/BeadQR/miniprogram/utils/pdf.js
@@ -0,0 +1,373 @@
+// 轻量 PDF 生成器(无依赖)
+// - 图纸页为纯矢量:珠子、色号、网格都是矢量指令,任意放大都清晰,文件小、生成快
+// - 中文(页眉、信息页)以 JPEG 图片嵌入
+// - 支持 1-bit 蒙版图片(用于矢量页上的半透明水印)
+
+const A4 = { w: 595.28, h: 841.89 }
+
+// Helvetica / Helvetica-Bold 字宽(AFM,1/1000 em),色号只含 A-Z 0-9
+const W_BOLD = { ' ': 278, '-': 333, '/': 278 }
+const W_REG = { ' ': 278, '-': 333, '/': 278 }
+'0123456789'.split('').forEach((c) => { W_BOLD[c] = 556; W_REG[c] = 556 })
+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]
+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]
+for (let i = 0; i < 26; i += 1) {
+ W_BOLD[String.fromCharCode(65 + i)] = UPPER_BOLD[i]
+ W_REG[String.fromCharCode(65 + i)] = UPPER_REG[i]
+}
+function textWidth(text, size, bold = true) {
+ const table = bold ? W_BOLD : W_REG
+ let w = 0
+ for (let i = 0; i < text.length; i += 1) w += table[text[i]] || 556
+ return (w / 1000) * size
+}
+
+const n2 = (v) => (Math.round(v * 100) / 100).toString()
+const esc = (s) => String(s).replace(/[\\()]/g, (m) => `\\${m}`).replace(/[^\x20-\x7e]/g, '?')
+
+function ascii(text) {
+ const out = new Uint8Array(text.length)
+ for (let i = 0; i < text.length; i += 1) out[i] = text.charCodeAt(i) & 0xff
+ return out
+}
+
+class PdfDoc {
+ constructor() {
+ this.objects = [null, null, null] // 1: Catalog, 2: Pages(0 号占位)
+ this.pageIds = []
+ this.fontRegular = this.add(['<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'])
+ this.fontBold = this.add(['<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>'])
+ this.forms = {}
+ this.gstates = {}
+ }
+
+ add(parts) {
+ this.objects.push(parts)
+ return this.objects.length - 1
+ }
+
+ stream(dict, data) {
+ const bytes = typeof data === 'string' ? ascii(data) : new Uint8Array(data)
+ return this.add([`<< ${dict} /Length ${bytes.length} >>\nstream\n`, bytes, '\nendstream'])
+ }
+
+ addJpeg(buffer, width, height) {
+ return this.stream(`/Type /XObject /Subtype /Image /Width ${width} /Height ${height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode`, buffer)
+ }
+
+ // bits:每行按字节对齐,0 = 绘制
+ addImageMask(bits, width, height) {
+ return this.stream(`/Type /XObject /Subtype /Image /Width ${width} /Height ${height} /ImageMask true /BitsPerComponent 1`, bits)
+ }
+
+ // 单位圆(直径 1)表单:不设颜色,继承外部填充色
+ form(kind) {
+ if (this.forms[kind]) return this.forms[kind]
+ const k = 0.5523
+ const circle = (r) => {
+ const c = r * k
+ 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 `
+ + `${n2(0.5 - c)} ${n2(0.5 + r)} ${n2(0.5 - r)} ${n2(0.5 + c)} ${n2(0.5 - r)} 0.5 c `
+ + `${n2(0.5 - r)} ${n2(0.5 - c)} ${n2(0.5 - c)} ${n2(0.5 - r)} 0.5 ${n2(0.5 - r)} c `
+ + `${n2(0.5 + c)} ${n2(0.5 - r)} ${n2(0.5 + r)} ${n2(0.5 - c)} ${n2(0.5 + r)} 0.5 c h `
+ }
+ let body
+ if (kind === 'round') body = `${circle(0.46)}f`
+ else if (kind === 'hollow') body = `${circle(0.46)}${circle(0.2)}f*`
+ else body = `${circle(0.14)}f` // peg 空钉位
+ const id = this.stream('/Type /XObject /Subtype /Form /BBox [0 0 1 1]', body)
+ this.forms[kind] = id
+ return id
+ }
+
+ gstate(alpha) {
+ const key = String(alpha)
+ if (!this.gstates[key]) this.gstates[key] = this.add([`<< /Type /ExtGState /ca ${alpha} /CA ${alpha} >>`])
+ return this.gstates[key]
+ }
+
+ addPage(content, { images = {} } = {}) {
+ const contentId = this.stream('', content)
+ const xobjects = Object.assign({}, images)
+ Object.keys(this.forms).forEach((k) => { xobjects[`Fm_${k}`] = this.forms[k] })
+ const xo = Object.keys(xobjects).map((k) => `/${k} ${xobjects[k]} 0 R`).join(' ')
+ const gs = Object.keys(this.gstates).map((k) => `/GS${k.replace('.', '_')} ${this.gstates[k]} 0 R`).join(' ')
+ 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 >>`])
+ this.pageIds.push(id)
+ return id
+ }
+
+ build() {
+ this.objects[1] = ['<< /Type /Catalog /Pages 2 0 R >>']
+ this.objects[2] = [`<< /Type /Pages /Kids [${this.pageIds.map((id) => `${id} 0 R`).join(' ')}] /Count ${this.pageIds.length} >>`]
+ const chunks = []
+ let pos = 0
+ const push = (part) => {
+ const bytes = typeof part === 'string' ? ascii(part) : part
+ chunks.push(bytes)
+ pos += bytes.length
+ }
+ push('%PDF-1.4\n%\xe2\xe3\xcf\xd3\n')
+ const offsets = []
+ for (let id = 1; id < this.objects.length; id += 1) {
+ offsets[id] = pos
+ push(`${id} 0 obj\n`)
+ this.objects[id].forEach(push)
+ push('\nendobj\n')
+ }
+ const xref = pos
+ let table = `xref\n0 ${this.objects.length}\n0000000000 65535 f \n`
+ for (let id = 1; id < this.objects.length; id += 1) table += `${String(offsets[id]).padStart(10, '0')} 00000 n \n`
+ push(table)
+ push(`trailer\n<< /Size ${this.objects.length} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`)
+ const out = new Uint8Array(pos)
+ let cursor = 0
+ chunks.forEach((c) => { out.set(c, cursor); cursor += c.length })
+ return out.buffer
+ }
+}
+
+// —— 矢量图纸 ——
+// 使用“左上角原点、y 向下”的坐标系(页面开头统一翻转),文字与图片局部再翻回来
+
+function hexToPdf(hex) {
+ const v = parseInt(hex.slice(1), 16)
+ return `${n2(((v >> 16) & 255) / 255)} ${n2(((v >> 8) & 255) / 255)} ${n2((v & 255) / 255)}`
+}
+
+function lumaOf(hex) {
+ const v = parseInt(hex.slice(1), 16)
+ return (((v >> 16) & 255) * 0.299 + ((v >> 8) & 255) * 0.587 + (v & 255) * 0.114) / 255
+}
+
+function vectorSheet(doc, artwork, { x0, y0, cell, region, style, codes, grid }) {
+ const { size, cells, pal } = artwork
+ const out = []
+ out.push(`${hexToPdf('#f1eee7')} rg ${n2(x0)} ${n2(y0)} ${n2(region.cols * cell)} ${n2(region.rows * cell)} re f`)
+ const groups = new Map()
+ const empty = []
+ for (let y = region.row; y < region.row + region.rows; y += 1) {
+ for (let x = region.col; x < region.col + region.cols; x += 1) {
+ const c = cells[y * size + x]
+ const px = x0 + (x - region.col) * cell
+ const py = y0 + (y - region.row) * cell
+ if (c < 0) { empty.push([px, py]); continue }
+ if (!groups.has(c)) groups.set(c, [])
+ groups.get(c).push([px, py])
+ }
+ }
+ if (empty.length) {
+ doc.form('peg')
+ out.push(`${hexToPdf('#d9d3c7')} rg`)
+ empty.forEach(([x, y]) => out.push(`q ${n2(cell)} 0 0 ${n2(cell)} ${n2(x)} ${n2(y)} cm /Fm_peg Do Q`))
+ }
+ if (style !== 'square') doc.form(style)
+ groups.forEach((list, c) => {
+ out.push(`${hexToPdf(pal.hexes[c])} rg`)
+ if (style === 'square') {
+ // 略微外扩 0.05pt 消除阅读器抗锯齿造成的白缝
+ list.forEach(([x, y]) => out.push(`${n2(x - 0.05)} ${n2(y - 0.05)} ${n2(cell + 0.1)} ${n2(cell + 0.1)} re`))
+ out.push('f')
+ } else {
+ list.forEach(([x, y]) => out.push(`q ${n2(cell)} 0 0 ${n2(cell)} ${n2(x)} ${n2(y)} cm /Fm_${style} Do Q`))
+ }
+ })
+ if (codes) {
+ const fs = cell * (style === 'hollow' ? 0.27 : 0.34)
+ const writeGroup = (ink) => {
+ const lines = []
+ groups.forEach((list, c) => {
+ const useInk = style === 'hollow' || lumaOf(pal.hexes[c]) > 0.56
+ if (useInk !== ink) return
+ const id = pal.ids[c]
+ const tw = textWidth(id, fs)
+ 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`))
+ })
+ if (!lines.length) return
+ out.push(`${ink ? hexToPdf('#1d2b2a') : '1 1 1'} rg BT /F2 ${n2(fs)} Tf`)
+ out.push(...lines)
+ out.push('ET')
+ }
+ writeGroup(true)
+ writeGroup(false)
+ }
+ if (grid && grid.enabled) {
+ const w = region.cols * cell
+ const h = region.rows * cell
+ const color = hexToPdf(grid.color)
+ const alpha = grid.opacity / 100
+ const thin = []
+ 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`)
+ 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`)
+ doc.gstate(Math.round(alpha * 30) / 100)
+ doc.gstate(alpha)
+ 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')
+ const thick = []
+ for (let i = 0; i <= region.cols; i += 1) {
+ const g = region.col + i
+ if (g % grid.interval && i && i !== region.cols) continue
+ thick.push(`${n2(x0 + i * cell)} ${n2(y0)} m ${n2(x0 + i * cell)} ${n2(y0 + h)} l`)
+ }
+ for (let i = 0; i <= region.rows; i += 1) {
+ const g = region.row + i
+ if (g % grid.interval && i && i !== region.rows) continue
+ thick.push(`${n2(x0)} ${n2(y0 + i * cell)} m ${n2(x0 + w)} ${n2(y0 + i * cell)} l`)
+ }
+ out.push(`q /GS${String(alpha).replace('.', '_')} gs ${color} RG ${n2(Math.max(0.6, cell * 0.06))} w`, ...thick, 'S Q')
+ // 坐标数字
+ const fs = Math.min(7, Math.max(4, cell * 0.42))
+ const step = cell < 8 ? grid.interval : (cell < 13 ? 5 : 1)
+ const labels = []
+ for (let i = 0; i < region.cols; i += 1) {
+ const g = region.col + i + 1
+ if (g % step && g !== 1 && i !== 0) continue
+ const t = String(g)
+ 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`)
+ }
+ for (let i = 0; i < region.rows; i += 1) {
+ const g = region.row + i + 1
+ if (g % step && g !== 1 && i !== 0) continue
+ const t = String(g)
+ 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`)
+ }
+ out.push(`${color} rg BT /F1 ${n2(fs)} Tf`, ...labels, 'ET')
+ }
+ return out.join('\n')
+}
+
+// 页眉图片 + 水印蒙版的放置
+function imageCmd(name, x, y, w, h) {
+ return `q ${n2(w)} 0 0 ${n2(-h)} ${n2(x)} ${n2(y + h)} cm /${name} Do Q`
+}
+
+function watermarkCmd(mask) {
+ if (!mask) return ''
+ const h = 16
+ const w = h * mask.width / mask.height
+ const out = [`q /GS0_08 gs ${hexToPdf('#1d2b2a')} rg`]
+ const stepX = w + 90
+ const stepY = 120
+ for (let row = 0, y = -200; y < A4.h + 200; y += stepY, row += 1) {
+ for (let x = -200; x < A4.w + 200; x += stepX) {
+ // 旋转 -20°:cos=0.94 sin=0.342;y 轴已翻转,所以蒙版局部再翻回
+ 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`)
+ }
+ }
+ out.push('Q')
+ return out.join('\n')
+}
+
+const MARGIN = 28.35 // 10mm
+const HEADER_H = 34
+
+const AXIS = 14
+const TARGET_CELL = 18 // 约 6.4mm/格,打印后色号清晰可读
+const AREA_W = A4.w - MARGIN * 2
+const AREA_H = A4.h - MARGIN * 2 - HEADER_H
+
+/**
+ * 规划图纸页:小图一页搞定;大图 = 总览页 + 若干分块页
+ * 返回 { tiled, fitCell, tilesX, tilesY, tileCols, tileRows, pages: [{ kind, tile, region }] }
+ */
+function planPatternPages(size) {
+ const fitCell = Math.min((AREA_W - AXIS) / size, (AREA_H - AXIS) / size)
+ const tiled = fitCell < 11
+ const full = { col: 0, row: 0, cols: size, rows: size }
+ const pages = [{ kind: 'overview', tile: 0, region: full }]
+ let tilesX = 1
+ let tilesY = 1
+ let tileCols = size
+ let tileRows = size
+ if (tiled) {
+ tilesX = Math.ceil(size / Math.floor((AREA_W - AXIS) / TARGET_CELL))
+ tilesY = Math.ceil(size / Math.floor((AREA_H - AXIS) / TARGET_CELL))
+ tileCols = Math.ceil(size / tilesX)
+ tileRows = Math.ceil(size / tilesY)
+ let n = 1
+ for (let ty = 0; ty < tilesY; ty += 1) {
+ for (let tx = 0; tx < tilesX; tx += 1) {
+ const col = tx * tileCols
+ const row = ty * tileRows
+ if (col >= size || row >= size) continue
+ pages.push({ kind: 'tile', tile: n, region: { col, row, cols: Math.min(tileCols, size - col), rows: Math.min(tileRows, size - row) } })
+ n += 1
+ }
+ }
+ }
+ return { tiled, fitCell, tilesX, tilesY, tileCols, tileRows, pages }
+}
+
+/**
+ * 生成图纸页(矢量)。
+ * opts: { plan, headers: [{ id, width, height }](按页序), style, grid, watermark }
+ */
+function addPatternPages(doc, artwork, opts) {
+ const size = artwork.size
+ const plan = opts.plan || planPatternPages(size)
+ const headers = opts.headers || []
+ const flip = `1 0 0 -1 0 ${A4.h} cm`
+ const wm = opts.watermark ? watermarkCmd(opts.watermark) : ''
+ if (opts.watermark) doc.gstate(0.08)
+ if (plan.tiled) doc.gstate(0.85)
+ const images = (header) => {
+ const map = {}
+ if (header) map.HD = header.id
+ if (opts.watermark) map.WM = opts.watermark.id
+ return map
+ }
+ const headerCmd = (header) => (header ? imageCmd('HD', MARGIN, MARGIN, AREA_W, AREA_W * header.height / header.width) : '')
+
+ plan.pages.forEach((page, i) => {
+ const header = headers[i] || null
+ const content = [flip, headerCmd(header)]
+ if (page.kind === 'overview') {
+ const fitCell = plan.fitCell
+ const gridW = fitCell * size
+ const x0 = MARGIN + AXIS + (AREA_W - AXIS - gridW) / 2
+ const y0 = MARGIN + HEADER_H + AXIS
+ content.push(vectorSheet(doc, artwork, {
+ x0, y0, cell: fitCell, region: page.region, style: opts.style, codes: !plan.tiled, grid: opts.grid,
+ }))
+ if (plan.tiled) {
+ const lines = [`q ${hexToPdf('#e11d48')} RG 1.2 w`]
+ const labels = []
+ plan.pages.forEach((tp) => {
+ if (tp.kind !== 'tile') return
+ const r = tp.region
+ const cx = x0 + r.col * fitCell
+ const cy = y0 + r.row * fitCell
+ const cw = r.cols * fitCell
+ const ch = r.rows * fitCell
+ lines.push(`${n2(cx)} ${n2(cy)} ${n2(cw)} ${n2(ch)} re S`)
+ const t = String(tp.tile)
+ labels.push(`1 0 0 -1 ${n2(cx + cw / 2 - textWidth(t, 22) / 2)} ${n2(cy + ch / 2 + 8)} Tm (${t}) Tj`)
+ })
+ lines.push('Q')
+ content.push(lines.join('\n'))
+ content.push(`q /GS0_85 gs ${hexToPdf('#e11d48')} rg BT /F2 22 Tf\n${labels.join('\n')}\nET Q`)
+ }
+ } else {
+ const cell = Math.min((AREA_W - AXIS) / plan.tileCols, (AREA_H - AXIS) / plan.tileRows)
+ content.push(vectorSheet(doc, artwork, {
+ x0: MARGIN + AXIS, y0: MARGIN + HEADER_H + AXIS, cell, region: page.region, style: opts.style, codes: true, grid: opts.grid,
+ }))
+ }
+ content.push(wm)
+ doc.addPage(content.join('\n'), { images: images(header) })
+ })
+ return plan.pages.length
+}
+
+// 整页图片(信息页)
+function addImagePage(doc, jpegId, pixelW, pixelH) {
+ const areaW = A4.w - MARGIN * 2
+ const areaH = A4.h - MARGIN * 2
+ const scale = Math.min(areaW / pixelW, areaH / pixelH)
+ const w = pixelW * scale
+ const h = pixelH * scale
+ const x = (A4.w - w) / 2
+ const y = (A4.h - h) / 2
+ doc.addPage(`1 0 0 -1 0 ${A4.h} cm\n${imageCmd('IM', x, y, w, h)}`, { images: { IM: jpegId } })
+}
+
+module.exports = { A4, PdfDoc, planPatternPages, addPatternPages, addImagePage, vectorSheet, textWidth, HEADER_H, MARGIN }
diff --git a/BeadQR/miniprogram/utils/qr.js b/BeadQR/miniprogram/utils/qr.js
new file mode 100644
index 0000000000000000000000000000000000000000..ca09a381bd088091e12070039e5409846e62593c
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qr.js
@@ -0,0 +1,58 @@
+// 二维码生成、功能区识别与摆放
+const QRCodeCore = require('./qrcore/qrcode')
+const AlignmentPattern = require('./qrcore/alignment-pattern')
+
+const SAMPLE_TEXT = '豆维·可扫码的拼豆'
+
+// 功能区(定位/时序/对齐/格式/版本)直接决定能否扫出,配色时强制高对比
+function buildFunctionMask(size, version) {
+ const mask = new Uint8Array(size * size)
+ const mark = (x, y) => { if (x >= 0 && y >= 0 && x < size && y < size) mask[y * size + x] = 1 }
+ for (let y = 0; y < size; y += 1) {
+ for (let x = 0; x < size; x += 1) {
+ const finder = (x <= 8 && y <= 8) || (x >= size - 8 && y <= 8) || (x <= 8 && y >= size - 8)
+ if (finder || x === 6 || y === 6) mark(x, y)
+ }
+ }
+ const positions = AlignmentPattern.getPositions(version) || []
+ positions.forEach(([row, col]) => {
+ for (let dy = -2; dy <= 2; dy += 1) for (let dx = -2; dx <= 2; dx += 1) mark(col + dx, row + dy)
+ })
+ if (version >= 7) {
+ for (let i = 0; i < 6; i += 1) {
+ for (let j = size - 11; j < size - 8; j += 1) { mark(i, j); mark(j, i) }
+ }
+ }
+ return mask
+}
+
+function makeQr(text, level) {
+ const value = String(text || '').trim() || SAMPLE_TEXT
+ const qr = QRCodeCore.create(value, { errorCorrectionLevel: level || 'H' })
+ const size = qr.modules.size
+ const version = qr.version || Math.round((size - 17) / 4)
+ return {
+ size,
+ version,
+ level: level || 'H',
+ dark: Uint8Array.from(qr.modules.data),
+ func: buildFunctionMask(size, version),
+ isSample: !String(text || '').trim(),
+ }
+}
+
+// scale:二维码占整张作品边长的百分比;quiet:二维码四周留白格数
+function createPlacement(qrSize, { scale = 100, posX = 50, posY = 50, quiet = 0, hasImage = true } = {}) {
+ const block = qrSize + quiet * 2
+ const size = hasImage ? Math.max(block, Math.ceil(block * 100 / scale)) : block
+ const free = size - block
+ return {
+ size,
+ left: quiet + Math.round(free * posX / 100),
+ top: quiet + Math.round(free * posY / 100),
+ quiet,
+ scale: hasImage ? scale : 100,
+ }
+}
+
+module.exports = { SAMPLE_TEXT, makeQr, createPlacement }
diff --git a/BeadQR/miniprogram/utils/qrcore/alignment-pattern.js b/BeadQR/miniprogram/utils/qrcore/alignment-pattern.js
new file mode 100644
index 0000000000000000000000000000000000000000..63c3154e976ca3894f16ebe60e9ead0f448c7ee1
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/alignment-pattern.js
@@ -0,0 +1,83 @@
+/**
+ * Alignment pattern are fixed reference pattern in defined positions
+ * in a matrix symbology, which enables the decode software to re-synchronise
+ * the coordinate mapping of the image modules in the event of moderate amounts
+ * of distortion of the image.
+ *
+ * Alignment patterns are present only in QR Code symbols of version 2 or larger
+ * and their number depends on the symbol version.
+ */
+
+const getSymbolSize = require('./utils').getSymbolSize
+
+/**
+ * Calculate the row/column coordinates of the center module of each alignment pattern
+ * for the specified QR Code version.
+ *
+ * The alignment patterns are positioned symmetrically on either side of the diagonal
+ * running from the top left corner of the symbol to the bottom right corner.
+ *
+ * Since positions are simmetrical only half of the coordinates are returned.
+ * Each item of the array will represent in turn the x and y coordinate.
+ * @see {@link getPositions}
+ *
+ * @param {Number} version QR Code version
+ * @return {Array} Array of coordinate
+ */
+exports.getRowColCoords = function getRowColCoords (version) {
+ if (version === 1) return []
+
+ const posCount = Math.floor(version / 7) + 2
+ const size = getSymbolSize(version)
+ const intervals = size === 145 ? 26 : Math.ceil((size - 13) / (2 * posCount - 2)) * 2
+ const positions = [size - 7] // Last coord is always (size - 7)
+
+ for (let i = 1; i < posCount - 1; i++) {
+ positions[i] = positions[i - 1] - intervals
+ }
+
+ positions.push(6) // First coord is always 6
+
+ return positions.reverse()
+}
+
+/**
+ * Returns an array containing the positions of each alignment pattern.
+ * Each array's element represent the center point of the pattern as (x, y) coordinates
+ *
+ * Coordinates are calculated expanding the row/column coordinates returned by {@link getRowColCoords}
+ * and filtering out the items that overlaps with finder pattern
+ *
+ * @example
+ * For a Version 7 symbol {@link getRowColCoords} returns values 6, 22 and 38.
+ * The alignment patterns, therefore, are to be centered on (row, column)
+ * positions (6,22), (22,6), (22,22), (22,38), (38,22), (38,38).
+ * Note that the coordinates (6,6), (6,38), (38,6) are occupied by finder patterns
+ * and are not therefore used for alignment patterns.
+ *
+ * let pos = getPositions(7)
+ * // [[6,22], [22,6], [22,22], [22,38], [38,22], [38,38]]
+ *
+ * @param {Number} version QR Code version
+ * @return {Array} Array of coordinates
+ */
+exports.getPositions = function getPositions (version) {
+ const coords = []
+ const pos = exports.getRowColCoords(version)
+ const posLength = pos.length
+
+ for (let i = 0; i < posLength; i++) {
+ for (let j = 0; j < posLength; j++) {
+ // Skip if position is occupied by finder patterns
+ if ((i === 0 && j === 0) || // top-left
+ (i === 0 && j === posLength - 1) || // bottom-left
+ (i === posLength - 1 && j === 0)) { // top-right
+ continue
+ }
+
+ coords.push([pos[i], pos[j]])
+ }
+ }
+
+ return coords
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/alphanumeric-data.js b/BeadQR/miniprogram/utils/qrcore/alphanumeric-data.js
new file mode 100644
index 0000000000000000000000000000000000000000..29ddf50c3ad1f02beb0f8dc79f6b10e3d207ea36
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/alphanumeric-data.js
@@ -0,0 +1,59 @@
+const Mode = require('./mode')
+
+/**
+ * Array of characters available in alphanumeric mode
+ *
+ * As per QR Code specification, to each character
+ * is assigned a value from 0 to 44 which in this case coincides
+ * with the array index
+ *
+ * @type {Array}
+ */
+const ALPHA_NUM_CHARS = [
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ ' ', '$', '%', '*', '+', '-', '.', '/', ':'
+]
+
+function AlphanumericData (data) {
+ this.mode = Mode.ALPHANUMERIC
+ this.data = data
+}
+
+AlphanumericData.getBitsLength = function getBitsLength (length) {
+ return 11 * Math.floor(length / 2) + 6 * (length % 2)
+}
+
+AlphanumericData.prototype.getLength = function getLength () {
+ return this.data.length
+}
+
+AlphanumericData.prototype.getBitsLength = function getBitsLength () {
+ return AlphanumericData.getBitsLength(this.data.length)
+}
+
+AlphanumericData.prototype.write = function write (bitBuffer) {
+ let i
+
+ // Input data characters are divided into groups of two characters
+ // and encoded as 11-bit binary codes.
+ for (i = 0; i + 2 <= this.data.length; i += 2) {
+ // The character value of the first character is multiplied by 45
+ let value = ALPHA_NUM_CHARS.indexOf(this.data[i]) * 45
+
+ // The character value of the second digit is added to the product
+ value += ALPHA_NUM_CHARS.indexOf(this.data[i + 1])
+
+ // The sum is then stored as 11-bit binary number
+ bitBuffer.put(value, 11)
+ }
+
+ // If the number of input data characters is not a multiple of two,
+ // the character value of the final character is encoded as a 6-bit binary number.
+ if (this.data.length % 2) {
+ bitBuffer.put(ALPHA_NUM_CHARS.indexOf(this.data[i]), 6)
+ }
+}
+
+module.exports = AlphanumericData
diff --git a/BeadQR/miniprogram/utils/qrcore/bit-buffer.js b/BeadQR/miniprogram/utils/qrcore/bit-buffer.js
new file mode 100644
index 0000000000000000000000000000000000000000..5b41d3d920ed97d04aabdc7d115302709ca42ae5
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/bit-buffer.js
@@ -0,0 +1,37 @@
+function BitBuffer () {
+ this.buffer = []
+ this.length = 0
+}
+
+BitBuffer.prototype = {
+
+ get: function (index) {
+ const bufIndex = Math.floor(index / 8)
+ return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) === 1
+ },
+
+ put: function (num, length) {
+ for (let i = 0; i < length; i++) {
+ this.putBit(((num >>> (length - i - 1)) & 1) === 1)
+ }
+ },
+
+ getLengthInBits: function () {
+ return this.length
+ },
+
+ putBit: function (bit) {
+ const bufIndex = Math.floor(this.length / 8)
+ if (this.buffer.length <= bufIndex) {
+ this.buffer.push(0)
+ }
+
+ if (bit) {
+ this.buffer[bufIndex] |= (0x80 >>> (this.length % 8))
+ }
+
+ this.length++
+ }
+}
+
+module.exports = BitBuffer
diff --git a/BeadQR/miniprogram/utils/qrcore/bit-matrix.js b/BeadQR/miniprogram/utils/qrcore/bit-matrix.js
new file mode 100644
index 0000000000000000000000000000000000000000..439138b99a746c0f22b432c7bfad608965fea3e9
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/bit-matrix.js
@@ -0,0 +1,65 @@
+/**
+ * Helper class to handle QR Code symbol modules
+ *
+ * @param {Number} size Symbol size
+ */
+function BitMatrix (size) {
+ if (!size || size < 1) {
+ throw new Error('BitMatrix size must be defined and greater than 0')
+ }
+
+ this.size = size
+ this.data = new Uint8Array(size * size)
+ this.reservedBit = new Uint8Array(size * size)
+}
+
+/**
+ * Set bit value at specified location
+ * If reserved flag is set, this bit will be ignored during masking process
+ *
+ * @param {Number} row
+ * @param {Number} col
+ * @param {Boolean} value
+ * @param {Boolean} reserved
+ */
+BitMatrix.prototype.set = function (row, col, value, reserved) {
+ const index = row * this.size + col
+ this.data[index] = value
+ if (reserved) this.reservedBit[index] = true
+}
+
+/**
+ * Returns bit value at specified location
+ *
+ * @param {Number} row
+ * @param {Number} col
+ * @return {Boolean}
+ */
+BitMatrix.prototype.get = function (row, col) {
+ return this.data[row * this.size + col]
+}
+
+/**
+ * Applies xor operator at specified location
+ * (used during masking process)
+ *
+ * @param {Number} row
+ * @param {Number} col
+ * @param {Boolean} value
+ */
+BitMatrix.prototype.xor = function (row, col, value) {
+ this.data[row * this.size + col] ^= value
+}
+
+/**
+ * Check if bit at specified location is reserved
+ *
+ * @param {Number} row
+ * @param {Number} col
+ * @return {Boolean}
+ */
+BitMatrix.prototype.isReserved = function (row, col) {
+ return this.reservedBit[row * this.size + col]
+}
+
+module.exports = BitMatrix
diff --git a/BeadQR/miniprogram/utils/qrcore/byte-data.js b/BeadQR/miniprogram/utils/qrcore/byte-data.js
new file mode 100644
index 0000000000000000000000000000000000000000..b94666ae13ee529fb92de3e07df2235439118a97
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/byte-data.js
@@ -0,0 +1,61 @@
+const Mode = require('./mode')
+
+// 微信部分真机运行时没有 TextEncoder。二维码 BYTE 段只需要 UTF-8 字节,
+// 在此实现最小兼容编码器,且与 TextEncoder 对无效代理对的处理一致。
+function encodeUtf8 (value) {
+ const bytes = []
+ for (let index = 0; index < value.length; index += 1) {
+ let codePoint = value.charCodeAt(index)
+ if (codePoint >= 0xd800 && codePoint <= 0xdbff) {
+ const next = value.charCodeAt(index + 1)
+ if (next >= 0xdc00 && next <= 0xdfff) {
+ codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00)
+ index += 1
+ } else {
+ codePoint = 0xfffd
+ }
+ } else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) {
+ codePoint = 0xfffd
+ }
+
+ if (codePoint <= 0x7f) {
+ bytes.push(codePoint)
+ } else if (codePoint <= 0x7ff) {
+ bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f))
+ } else if (codePoint <= 0xffff) {
+ bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
+ } else {
+ bytes.push(0xf0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3f), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
+ }
+ }
+ return new Uint8Array(bytes)
+}
+
+function ByteData (data) {
+ this.mode = Mode.BYTE
+ if (typeof (data) === 'string') {
+ this.data = encodeUtf8(data)
+ } else {
+ this.data = new Uint8Array(data)
+ }
+}
+
+ByteData.getBitsLength = function getBitsLength (length) {
+ return length * 8
+}
+
+ByteData.prototype.getLength = function getLength () {
+ return this.data.length
+}
+
+ByteData.prototype.getBitsLength = function getBitsLength () {
+ return ByteData.getBitsLength(this.data.length)
+}
+
+ByteData.prototype.write = function (bitBuffer) {
+ for (let i = 0, l = this.data.length; i < l; i++) {
+ bitBuffer.put(this.data[i], 8)
+ }
+}
+
+module.exports = ByteData
diff --git a/BeadQR/miniprogram/utils/qrcore/dijkstrajs.js b/BeadQR/miniprogram/utils/qrcore/dijkstrajs.js
new file mode 100644
index 0000000000000000000000000000000000000000..4f83f1f25913a94273a788e24185d8a18e0a7396
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/dijkstrajs.js
@@ -0,0 +1,165 @@
+'use strict';
+
+/******************************************************************************
+ * Created 2008-08-19.
+ *
+ * Dijkstra path-finding functions. Adapted from the Dijkstar Python project.
+ *
+ * Copyright (C) 2008
+ * Wyatt Baldwin
+ * All rights reserved
+ *
+ * Licensed under the MIT license.
+ *
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *****************************************************************************/
+var dijkstra = {
+ single_source_shortest_paths: function(graph, s, d) {
+ // Predecessor map for each node that has been encountered.
+ // node ID => predecessor node ID
+ var predecessors = {};
+
+ // Costs of shortest paths from s to all nodes encountered.
+ // node ID => cost
+ var costs = {};
+ costs[s] = 0;
+
+ // Costs of shortest paths from s to all nodes encountered; differs from
+ // `costs` in that it provides easy access to the node that currently has
+ // the known shortest path from s.
+ // XXX: Do we actually need both `costs` and `open`?
+ var open = dijkstra.PriorityQueue.make();
+ open.push(s, 0);
+
+ var closest,
+ u, v,
+ cost_of_s_to_u,
+ adjacent_nodes,
+ cost_of_e,
+ cost_of_s_to_u_plus_cost_of_e,
+ cost_of_s_to_v,
+ first_visit;
+ while (!open.empty()) {
+ // In the nodes remaining in graph that have a known cost from s,
+ // find the node, u, that currently has the shortest path from s.
+ closest = open.pop();
+ u = closest.value;
+ cost_of_s_to_u = closest.cost;
+
+ // Get nodes adjacent to u...
+ adjacent_nodes = graph[u] || {};
+
+ // ...and explore the edges that connect u to those nodes, updating
+ // the cost of the shortest paths to any or all of those nodes as
+ // necessary. v is the node across the current edge from u.
+ for (v in adjacent_nodes) {
+ if (adjacent_nodes.hasOwnProperty(v)) {
+ // Get the cost of the edge running from u to v.
+ cost_of_e = adjacent_nodes[v];
+
+ // Cost of s to u plus the cost of u to v across e--this is *a*
+ // cost from s to v that may or may not be less than the current
+ // known cost to v.
+ cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e;
+
+ // If we haven't visited v yet OR if the current known cost from s to
+ // v is greater than the new cost we just found (cost of s to u plus
+ // cost of u to v across e), update v's cost in the cost list and
+ // update v's predecessor in the predecessor list (it's now u).
+ cost_of_s_to_v = costs[v];
+ first_visit = (typeof costs[v] === 'undefined');
+ if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
+ costs[v] = cost_of_s_to_u_plus_cost_of_e;
+ open.push(v, cost_of_s_to_u_plus_cost_of_e);
+ predecessors[v] = u;
+ }
+ }
+ }
+ }
+
+ if (typeof d !== 'undefined' && typeof costs[d] === 'undefined') {
+ var msg = ['Could not find a path from ', s, ' to ', d, '.'].join('');
+ throw new Error(msg);
+ }
+
+ return predecessors;
+ },
+
+ extract_shortest_path_from_predecessor_list: function(predecessors, d) {
+ var nodes = [];
+ var u = d;
+ var predecessor;
+ while (u) {
+ nodes.push(u);
+ predecessor = predecessors[u];
+ u = predecessors[u];
+ }
+ nodes.reverse();
+ return nodes;
+ },
+
+ find_path: function(graph, s, d) {
+ var predecessors = dijkstra.single_source_shortest_paths(graph, s, d);
+ return dijkstra.extract_shortest_path_from_predecessor_list(
+ predecessors, d);
+ },
+
+ /**
+ * A very naive priority queue implementation.
+ */
+ PriorityQueue: {
+ make: function (opts) {
+ var T = dijkstra.PriorityQueue,
+ t = {},
+ key;
+ opts = opts || {};
+ for (key in T) {
+ if (T.hasOwnProperty(key)) {
+ t[key] = T[key];
+ }
+ }
+ t.queue = [];
+ t.sorter = opts.sorter || T.default_sorter;
+ return t;
+ },
+
+ default_sorter: function (a, b) {
+ return a.cost - b.cost;
+ },
+
+ /**
+ * Add a new item to the queue and ensure the highest priority element
+ * is at the front of the queue.
+ */
+ push: function (value, cost) {
+ var item = {value: value, cost: cost};
+ this.queue.push(item);
+ this.queue.sort(this.sorter);
+ },
+
+ /**
+ * Return the highest priority element in the queue.
+ */
+ pop: function () {
+ return this.queue.shift();
+ },
+
+ empty: function () {
+ return this.queue.length === 0;
+ }
+ }
+};
+
+
+// node.js module exports
+if (typeof module !== 'undefined') {
+ module.exports = dijkstra;
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/error-correction-code.js b/BeadQR/miniprogram/utils/qrcore/error-correction-code.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a4aeae92af8819c272370434d614b8e086ea8bc
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/error-correction-code.js
@@ -0,0 +1,135 @@
+const ECLevel = require('./error-correction-level')
+
+const EC_BLOCKS_TABLE = [
+// L M Q H
+ 1, 1, 1, 1,
+ 1, 1, 1, 1,
+ 1, 1, 2, 2,
+ 1, 2, 2, 4,
+ 1, 2, 4, 4,
+ 2, 4, 4, 4,
+ 2, 4, 6, 5,
+ 2, 4, 6, 6,
+ 2, 5, 8, 8,
+ 4, 5, 8, 8,
+ 4, 5, 8, 11,
+ 4, 8, 10, 11,
+ 4, 9, 12, 16,
+ 4, 9, 16, 16,
+ 6, 10, 12, 18,
+ 6, 10, 17, 16,
+ 6, 11, 16, 19,
+ 6, 13, 18, 21,
+ 7, 14, 21, 25,
+ 8, 16, 20, 25,
+ 8, 17, 23, 25,
+ 9, 17, 23, 34,
+ 9, 18, 25, 30,
+ 10, 20, 27, 32,
+ 12, 21, 29, 35,
+ 12, 23, 34, 37,
+ 12, 25, 34, 40,
+ 13, 26, 35, 42,
+ 14, 28, 38, 45,
+ 15, 29, 40, 48,
+ 16, 31, 43, 51,
+ 17, 33, 45, 54,
+ 18, 35, 48, 57,
+ 19, 37, 51, 60,
+ 19, 38, 53, 63,
+ 20, 40, 56, 66,
+ 21, 43, 59, 70,
+ 22, 45, 62, 74,
+ 24, 47, 65, 77,
+ 25, 49, 68, 81
+]
+
+const EC_CODEWORDS_TABLE = [
+// L M Q H
+ 7, 10, 13, 17,
+ 10, 16, 22, 28,
+ 15, 26, 36, 44,
+ 20, 36, 52, 64,
+ 26, 48, 72, 88,
+ 36, 64, 96, 112,
+ 40, 72, 108, 130,
+ 48, 88, 132, 156,
+ 60, 110, 160, 192,
+ 72, 130, 192, 224,
+ 80, 150, 224, 264,
+ 96, 176, 260, 308,
+ 104, 198, 288, 352,
+ 120, 216, 320, 384,
+ 132, 240, 360, 432,
+ 144, 280, 408, 480,
+ 168, 308, 448, 532,
+ 180, 338, 504, 588,
+ 196, 364, 546, 650,
+ 224, 416, 600, 700,
+ 224, 442, 644, 750,
+ 252, 476, 690, 816,
+ 270, 504, 750, 900,
+ 300, 560, 810, 960,
+ 312, 588, 870, 1050,
+ 336, 644, 952, 1110,
+ 360, 700, 1020, 1200,
+ 390, 728, 1050, 1260,
+ 420, 784, 1140, 1350,
+ 450, 812, 1200, 1440,
+ 480, 868, 1290, 1530,
+ 510, 924, 1350, 1620,
+ 540, 980, 1440, 1710,
+ 570, 1036, 1530, 1800,
+ 570, 1064, 1590, 1890,
+ 600, 1120, 1680, 1980,
+ 630, 1204, 1770, 2100,
+ 660, 1260, 1860, 2220,
+ 720, 1316, 1950, 2310,
+ 750, 1372, 2040, 2430
+]
+
+/**
+ * Returns the number of error correction block that the QR Code should contain
+ * for the specified version and error correction level.
+ *
+ * @param {Number} version QR Code version
+ * @param {Number} errorCorrectionLevel Error correction level
+ * @return {Number} Number of error correction blocks
+ */
+exports.getBlocksCount = function getBlocksCount (version, errorCorrectionLevel) {
+ switch (errorCorrectionLevel) {
+ case ECLevel.L:
+ return EC_BLOCKS_TABLE[(version - 1) * 4 + 0]
+ case ECLevel.M:
+ return EC_BLOCKS_TABLE[(version - 1) * 4 + 1]
+ case ECLevel.Q:
+ return EC_BLOCKS_TABLE[(version - 1) * 4 + 2]
+ case ECLevel.H:
+ return EC_BLOCKS_TABLE[(version - 1) * 4 + 3]
+ default:
+ return undefined
+ }
+}
+
+/**
+ * Returns the number of error correction codewords to use for the specified
+ * version and error correction level.
+ *
+ * @param {Number} version QR Code version
+ * @param {Number} errorCorrectionLevel Error correction level
+ * @return {Number} Number of error correction codewords
+ */
+exports.getTotalCodewordsCount = function getTotalCodewordsCount (version, errorCorrectionLevel) {
+ switch (errorCorrectionLevel) {
+ case ECLevel.L:
+ return EC_CODEWORDS_TABLE[(version - 1) * 4 + 0]
+ case ECLevel.M:
+ return EC_CODEWORDS_TABLE[(version - 1) * 4 + 1]
+ case ECLevel.Q:
+ return EC_CODEWORDS_TABLE[(version - 1) * 4 + 2]
+ case ECLevel.H:
+ return EC_CODEWORDS_TABLE[(version - 1) * 4 + 3]
+ default:
+ return undefined
+ }
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/error-correction-level.js b/BeadQR/miniprogram/utils/qrcore/error-correction-level.js
new file mode 100644
index 0000000000000000000000000000000000000000..07433d6bff7692d37f5747976545150a8893f0da
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/error-correction-level.js
@@ -0,0 +1,50 @@
+exports.L = { bit: 1 }
+exports.M = { bit: 0 }
+exports.Q = { bit: 3 }
+exports.H = { bit: 2 }
+
+function fromString (string) {
+ if (typeof string !== 'string') {
+ throw new Error('Param is not a string')
+ }
+
+ const lcStr = string.toLowerCase()
+
+ switch (lcStr) {
+ case 'l':
+ case 'low':
+ return exports.L
+
+ case 'm':
+ case 'medium':
+ return exports.M
+
+ case 'q':
+ case 'quartile':
+ return exports.Q
+
+ case 'h':
+ case 'high':
+ return exports.H
+
+ default:
+ throw new Error('Unknown EC Level: ' + string)
+ }
+}
+
+exports.isValid = function isValid (level) {
+ return level && typeof level.bit !== 'undefined' &&
+ level.bit >= 0 && level.bit < 4
+}
+
+exports.from = function from (value, defaultValue) {
+ if (exports.isValid(value)) {
+ return value
+ }
+
+ try {
+ return fromString(value)
+ } catch (e) {
+ return defaultValue
+ }
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/finder-pattern.js b/BeadQR/miniprogram/utils/qrcore/finder-pattern.js
new file mode 100644
index 0000000000000000000000000000000000000000..4613c041a972ea603b56deda6b85488c44ba1443
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/finder-pattern.js
@@ -0,0 +1,22 @@
+const getSymbolSize = require('./utils').getSymbolSize
+const FINDER_PATTERN_SIZE = 7
+
+/**
+ * Returns an array containing the positions of each finder pattern.
+ * Each array's element represent the top-left point of the pattern as (x, y) coordinates
+ *
+ * @param {Number} version QR Code version
+ * @return {Array} Array of coordinates
+ */
+exports.getPositions = function getPositions (version) {
+ const size = getSymbolSize(version)
+
+ return [
+ // top-left
+ [0, 0],
+ // top-right
+ [size - FINDER_PATTERN_SIZE, 0],
+ // bottom-left
+ [0, size - FINDER_PATTERN_SIZE]
+ ]
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/format-info.js b/BeadQR/miniprogram/utils/qrcore/format-info.js
new file mode 100644
index 0000000000000000000000000000000000000000..11fe9efaacdd292fb39698e341e0df3ef8fe95e7
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/format-info.js
@@ -0,0 +1,29 @@
+const Utils = require('./utils')
+
+const G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)
+const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
+const G15_BCH = Utils.getBCHDigit(G15)
+
+/**
+ * Returns format information with relative error correction bits
+ *
+ * The format information is a 15-bit sequence containing 5 data bits,
+ * with 10 error correction bits calculated using the (15, 5) BCH code.
+ *
+ * @param {Number} errorCorrectionLevel Error correction level
+ * @param {Number} mask Mask pattern
+ * @return {Number} Encoded format information bits
+ */
+exports.getEncodedBits = function getEncodedBits (errorCorrectionLevel, mask) {
+ const data = ((errorCorrectionLevel.bit << 3) | mask)
+ let d = data << 10
+
+ while (Utils.getBCHDigit(d) - G15_BCH >= 0) {
+ d ^= (G15 << (Utils.getBCHDigit(d) - G15_BCH))
+ }
+
+ // xor final data with mask pattern in order to ensure that
+ // no combination of Error Correction Level and data mask pattern
+ // will result in an all-zero data string
+ return ((data << 10) | d) ^ G15_MASK
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/galois-field.js b/BeadQR/miniprogram/utils/qrcore/galois-field.js
new file mode 100644
index 0000000000000000000000000000000000000000..8125179c058e1b130c551dc04c12e3292311e751
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/galois-field.js
@@ -0,0 +1,69 @@
+const EXP_TABLE = new Uint8Array(512)
+const LOG_TABLE = new Uint8Array(256)
+/**
+ * Precompute the log and anti-log tables for faster computation later
+ *
+ * For each possible value in the galois field 2^8, we will pre-compute
+ * the logarithm and anti-logarithm (exponential) of this value
+ *
+ * ref {@link https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Introduction_to_mathematical_fields}
+ */
+;(function initTables () {
+ let x = 1
+ for (let i = 0; i < 255; i++) {
+ EXP_TABLE[i] = x
+ LOG_TABLE[x] = i
+
+ x <<= 1 // multiply by 2
+
+ // The QR code specification says to use byte-wise modulo 100011101 arithmetic.
+ // This means that when a number is 256 or larger, it should be XORed with 0x11D.
+ if (x & 0x100) { // similar to x >= 256, but a lot faster (because 0x100 == 256)
+ x ^= 0x11D
+ }
+ }
+
+ // Optimization: double the size of the anti-log table so that we don't need to mod 255 to
+ // stay inside the bounds (because we will mainly use this table for the multiplication of
+ // two GF numbers, no more).
+ // @see {@link mul}
+ for (let i = 255; i < 512; i++) {
+ EXP_TABLE[i] = EXP_TABLE[i - 255]
+ }
+}())
+
+/**
+ * Returns log value of n inside Galois Field
+ *
+ * @param {Number} n
+ * @return {Number}
+ */
+exports.log = function log (n) {
+ if (n < 1) throw new Error('log(' + n + ')')
+ return LOG_TABLE[n]
+}
+
+/**
+ * Returns anti-log value of n inside Galois Field
+ *
+ * @param {Number} n
+ * @return {Number}
+ */
+exports.exp = function exp (n) {
+ return EXP_TABLE[n]
+}
+
+/**
+ * Multiplies two number inside Galois Field
+ *
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Number}
+ */
+exports.mul = function mul (x, y) {
+ if (x === 0 || y === 0) return 0
+
+ // should be EXP_TABLE[(LOG_TABLE[x] + LOG_TABLE[y]) % 255] if EXP_TABLE wasn't oversized
+ // @see {@link initTables}
+ return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]]
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/kanji-data.js b/BeadQR/miniprogram/utils/qrcore/kanji-data.js
new file mode 100644
index 0000000000000000000000000000000000000000..a1d9a6922d8e91cd90d89ea301d2531145b129f2
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/kanji-data.js
@@ -0,0 +1,54 @@
+const Mode = require('./mode')
+const Utils = require('./utils')
+
+function KanjiData (data) {
+ this.mode = Mode.KANJI
+ this.data = data
+}
+
+KanjiData.getBitsLength = function getBitsLength (length) {
+ return length * 13
+}
+
+KanjiData.prototype.getLength = function getLength () {
+ return this.data.length
+}
+
+KanjiData.prototype.getBitsLength = function getBitsLength () {
+ return KanjiData.getBitsLength(this.data.length)
+}
+
+KanjiData.prototype.write = function (bitBuffer) {
+ let i
+
+ // In the Shift JIS system, Kanji characters are represented by a two byte combination.
+ // These byte values are shifted from the JIS X 0208 values.
+ // JIS X 0208 gives details of the shift coded representation.
+ for (i = 0; i < this.data.length; i++) {
+ let value = Utils.toSJIS(this.data[i])
+
+ // For characters with Shift JIS values from 0x8140 to 0x9FFC:
+ if (value >= 0x8140 && value <= 0x9FFC) {
+ // Subtract 0x8140 from Shift JIS value
+ value -= 0x8140
+
+ // For characters with Shift JIS values from 0xE040 to 0xEBBF
+ } else if (value >= 0xE040 && value <= 0xEBBF) {
+ // Subtract 0xC140 from Shift JIS value
+ value -= 0xC140
+ } else {
+ throw new Error(
+ 'Invalid SJIS character: ' + this.data[i] + '\n' +
+ 'Make sure your charset is UTF-8')
+ }
+
+ // Multiply most significant byte of result by 0xC0
+ // and add least significant byte to product
+ value = (((value >>> 8) & 0xff) * 0xC0) + (value & 0xff)
+
+ // Convert result to a 13-bit binary string
+ bitBuffer.put(value, 13)
+ }
+}
+
+module.exports = KanjiData
diff --git a/BeadQR/miniprogram/utils/qrcore/mask-pattern.js b/BeadQR/miniprogram/utils/qrcore/mask-pattern.js
new file mode 100644
index 0000000000000000000000000000000000000000..f5f8900eb19c94a30b74d795177f1d3fa32184d9
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/mask-pattern.js
@@ -0,0 +1,234 @@
+/**
+ * Data mask pattern reference
+ * @type {Object}
+ */
+exports.Patterns = {
+ PATTERN000: 0,
+ PATTERN001: 1,
+ PATTERN010: 2,
+ PATTERN011: 3,
+ PATTERN100: 4,
+ PATTERN101: 5,
+ PATTERN110: 6,
+ PATTERN111: 7
+}
+
+/**
+ * Weighted penalty scores for the undesirable features
+ * @type {Object}
+ */
+const PenaltyScores = {
+ N1: 3,
+ N2: 3,
+ N3: 40,
+ N4: 10
+}
+
+/**
+ * Check if mask pattern value is valid
+ *
+ * @param {Number} mask Mask pattern
+ * @return {Boolean} true if valid, false otherwise
+ */
+exports.isValid = function isValid (mask) {
+ return mask != null && mask !== '' && !isNaN(mask) && mask >= 0 && mask <= 7
+}
+
+/**
+ * Returns mask pattern from a value.
+ * If value is not valid, returns undefined
+ *
+ * @param {Number|String} value Mask pattern value
+ * @return {Number} Valid mask pattern or undefined
+ */
+exports.from = function from (value) {
+ return exports.isValid(value) ? parseInt(value, 10) : undefined
+}
+
+/**
+* Find adjacent modules in row/column with the same color
+* and assign a penalty value.
+*
+* Points: N1 + i
+* i is the amount by which the number of adjacent modules of the same color exceeds 5
+*/
+exports.getPenaltyN1 = function getPenaltyN1 (data) {
+ const size = data.size
+ let points = 0
+ let sameCountCol = 0
+ let sameCountRow = 0
+ let lastCol = null
+ let lastRow = null
+
+ for (let row = 0; row < size; row++) {
+ sameCountCol = sameCountRow = 0
+ lastCol = lastRow = null
+
+ for (let col = 0; col < size; col++) {
+ let module = data.get(row, col)
+ if (module === lastCol) {
+ sameCountCol++
+ } else {
+ if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5)
+ lastCol = module
+ sameCountCol = 1
+ }
+
+ module = data.get(col, row)
+ if (module === lastRow) {
+ sameCountRow++
+ } else {
+ if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5)
+ lastRow = module
+ sameCountRow = 1
+ }
+ }
+
+ if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5)
+ if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5)
+ }
+
+ return points
+}
+
+/**
+ * Find 2x2 blocks with the same color and assign a penalty value
+ *
+ * Points: N2 * (m - 1) * (n - 1)
+ */
+exports.getPenaltyN2 = function getPenaltyN2 (data) {
+ const size = data.size
+ let points = 0
+
+ for (let row = 0; row < size - 1; row++) {
+ for (let col = 0; col < size - 1; col++) {
+ const last = data.get(row, col) +
+ data.get(row, col + 1) +
+ data.get(row + 1, col) +
+ data.get(row + 1, col + 1)
+
+ if (last === 4 || last === 0) points++
+ }
+ }
+
+ return points * PenaltyScores.N2
+}
+
+/**
+ * Find 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column,
+ * preceded or followed by light area 4 modules wide
+ *
+ * Points: N3 * number of pattern found
+ */
+exports.getPenaltyN3 = function getPenaltyN3 (data) {
+ const size = data.size
+ let points = 0
+ let bitsCol = 0
+ let bitsRow = 0
+
+ for (let row = 0; row < size; row++) {
+ bitsCol = bitsRow = 0
+ for (let col = 0; col < size; col++) {
+ bitsCol = ((bitsCol << 1) & 0x7FF) | data.get(row, col)
+ if (col >= 10 && (bitsCol === 0x5D0 || bitsCol === 0x05D)) points++
+
+ bitsRow = ((bitsRow << 1) & 0x7FF) | data.get(col, row)
+ if (col >= 10 && (bitsRow === 0x5D0 || bitsRow === 0x05D)) points++
+ }
+ }
+
+ return points * PenaltyScores.N3
+}
+
+/**
+ * Calculate proportion of dark modules in entire symbol
+ *
+ * Points: N4 * k
+ *
+ * k is the rating of the deviation of the proportion of dark modules
+ * in the symbol from 50% in steps of 5%
+ */
+exports.getPenaltyN4 = function getPenaltyN4 (data) {
+ let darkCount = 0
+ const modulesCount = data.data.length
+
+ for (let i = 0; i < modulesCount; i++) darkCount += data.data[i]
+
+ const k = Math.abs(Math.ceil((darkCount * 100 / modulesCount) / 5) - 10)
+
+ return k * PenaltyScores.N4
+}
+
+/**
+ * Return mask value at given position
+ *
+ * @param {Number} maskPattern Pattern reference value
+ * @param {Number} i Row
+ * @param {Number} j Column
+ * @return {Boolean} Mask value
+ */
+function getMaskAt (maskPattern, i, j) {
+ switch (maskPattern) {
+ case exports.Patterns.PATTERN000: return (i + j) % 2 === 0
+ case exports.Patterns.PATTERN001: return i % 2 === 0
+ case exports.Patterns.PATTERN010: return j % 3 === 0
+ case exports.Patterns.PATTERN011: return (i + j) % 3 === 0
+ case exports.Patterns.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0
+ case exports.Patterns.PATTERN101: return (i * j) % 2 + (i * j) % 3 === 0
+ case exports.Patterns.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 === 0
+ case exports.Patterns.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 === 0
+
+ default: throw new Error('bad maskPattern:' + maskPattern)
+ }
+}
+
+/**
+ * Apply a mask pattern to a BitMatrix
+ *
+ * @param {Number} pattern Pattern reference number
+ * @param {BitMatrix} data BitMatrix data
+ */
+exports.applyMask = function applyMask (pattern, data) {
+ const size = data.size
+
+ for (let col = 0; col < size; col++) {
+ for (let row = 0; row < size; row++) {
+ if (data.isReserved(row, col)) continue
+ data.xor(row, col, getMaskAt(pattern, row, col))
+ }
+ }
+}
+
+/**
+ * Returns the best mask pattern for data
+ *
+ * @param {BitMatrix} data
+ * @return {Number} Mask pattern reference number
+ */
+exports.getBestMask = function getBestMask (data, setupFormatFunc) {
+ const numPatterns = Object.keys(exports.Patterns).length
+ let bestPattern = 0
+ let lowerPenalty = Infinity
+
+ for (let p = 0; p < numPatterns; p++) {
+ setupFormatFunc(p)
+ exports.applyMask(p, data)
+
+ // Calculate penalty
+ const penalty =
+ exports.getPenaltyN1(data) +
+ exports.getPenaltyN2(data) +
+ exports.getPenaltyN3(data) +
+ exports.getPenaltyN4(data)
+
+ // Undo previously applied mask
+ exports.applyMask(p, data)
+
+ if (penalty < lowerPenalty) {
+ lowerPenalty = penalty
+ bestPattern = p
+ }
+ }
+
+ return bestPattern
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/mode.js b/BeadQR/miniprogram/utils/qrcore/mode.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd95a6ef72b2414042cb9cf73fd7f4fe7ce054fe
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/mode.js
@@ -0,0 +1,167 @@
+const VersionCheck = require('./version-check')
+const Regex = require('./regex')
+
+/**
+ * Numeric mode encodes data from the decimal digit set (0 - 9)
+ * (byte values 30HEX to 39HEX).
+ * Normally, 3 data characters are represented by 10 bits.
+ *
+ * @type {Object}
+ */
+exports.NUMERIC = {
+ id: 'Numeric',
+ bit: 1 << 0,
+ ccBits: [10, 12, 14]
+}
+
+/**
+ * Alphanumeric mode encodes data from a set of 45 characters,
+ * i.e. 10 numeric digits (0 - 9),
+ * 26 alphabetic characters (A - Z),
+ * and 9 symbols (SP, $, %, *, +, -, ., /, :).
+ * Normally, two input characters are represented by 11 bits.
+ *
+ * @type {Object}
+ */
+exports.ALPHANUMERIC = {
+ id: 'Alphanumeric',
+ bit: 1 << 1,
+ ccBits: [9, 11, 13]
+}
+
+/**
+ * In byte mode, data is encoded at 8 bits per character.
+ *
+ * @type {Object}
+ */
+exports.BYTE = {
+ id: 'Byte',
+ bit: 1 << 2,
+ ccBits: [8, 16, 16]
+}
+
+/**
+ * The Kanji mode efficiently encodes Kanji characters in accordance with
+ * the Shift JIS system based on JIS X 0208.
+ * The Shift JIS values are shifted from the JIS X 0208 values.
+ * JIS X 0208 gives details of the shift coded representation.
+ * Each two-byte character value is compacted to a 13-bit binary codeword.
+ *
+ * @type {Object}
+ */
+exports.KANJI = {
+ id: 'Kanji',
+ bit: 1 << 3,
+ ccBits: [8, 10, 12]
+}
+
+/**
+ * Mixed mode will contain a sequences of data in a combination of any of
+ * the modes described above
+ *
+ * @type {Object}
+ */
+exports.MIXED = {
+ bit: -1
+}
+
+/**
+ * Returns the number of bits needed to store the data length
+ * according to QR Code specifications.
+ *
+ * @param {Mode} mode Data mode
+ * @param {Number} version QR Code version
+ * @return {Number} Number of bits
+ */
+exports.getCharCountIndicator = function getCharCountIndicator (mode, version) {
+ if (!mode.ccBits) throw new Error('Invalid mode: ' + mode)
+
+ if (!VersionCheck.isValid(version)) {
+ throw new Error('Invalid version: ' + version)
+ }
+
+ if (version >= 1 && version < 10) return mode.ccBits[0]
+ else if (version < 27) return mode.ccBits[1]
+ return mode.ccBits[2]
+}
+
+/**
+ * Returns the most efficient mode to store the specified data
+ *
+ * @param {String} dataStr Input data string
+ * @return {Mode} Best mode
+ */
+exports.getBestModeForData = function getBestModeForData (dataStr) {
+ if (Regex.testNumeric(dataStr)) return exports.NUMERIC
+ else if (Regex.testAlphanumeric(dataStr)) return exports.ALPHANUMERIC
+ else if (Regex.testKanji(dataStr)) return exports.KANJI
+ else return exports.BYTE
+}
+
+/**
+ * Return mode name as string
+ *
+ * @param {Mode} mode Mode object
+ * @returns {String} Mode name
+ */
+exports.toString = function toString (mode) {
+ if (mode && mode.id) return mode.id
+ throw new Error('Invalid mode')
+}
+
+/**
+ * Check if input param is a valid mode object
+ *
+ * @param {Mode} mode Mode object
+ * @returns {Boolean} True if valid mode, false otherwise
+ */
+exports.isValid = function isValid (mode) {
+ return mode && mode.bit && mode.ccBits
+}
+
+/**
+ * Get mode object from its name
+ *
+ * @param {String} string Mode name
+ * @returns {Mode} Mode object
+ */
+function fromString (string) {
+ if (typeof string !== 'string') {
+ throw new Error('Param is not a string')
+ }
+
+ const lcStr = string.toLowerCase()
+
+ switch (lcStr) {
+ case 'numeric':
+ return exports.NUMERIC
+ case 'alphanumeric':
+ return exports.ALPHANUMERIC
+ case 'kanji':
+ return exports.KANJI
+ case 'byte':
+ return exports.BYTE
+ default:
+ throw new Error('Unknown mode: ' + string)
+ }
+}
+
+/**
+ * Returns mode from a value.
+ * If value is not a valid mode, returns defaultValue
+ *
+ * @param {Mode|String} value Encoding mode
+ * @param {Mode} defaultValue Fallback value
+ * @return {Mode} Encoding mode
+ */
+exports.from = function from (value, defaultValue) {
+ if (exports.isValid(value)) {
+ return value
+ }
+
+ try {
+ return fromString(value)
+ } catch (e) {
+ return defaultValue
+ }
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/numeric-data.js b/BeadQR/miniprogram/utils/qrcore/numeric-data.js
new file mode 100644
index 0000000000000000000000000000000000000000..5223991c6a7378509b5ab9701fb31e28c68c6faf
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/numeric-data.js
@@ -0,0 +1,43 @@
+const Mode = require('./mode')
+
+function NumericData (data) {
+ this.mode = Mode.NUMERIC
+ this.data = data.toString()
+}
+
+NumericData.getBitsLength = function getBitsLength (length) {
+ return 10 * Math.floor(length / 3) + ((length % 3) ? ((length % 3) * 3 + 1) : 0)
+}
+
+NumericData.prototype.getLength = function getLength () {
+ return this.data.length
+}
+
+NumericData.prototype.getBitsLength = function getBitsLength () {
+ return NumericData.getBitsLength(this.data.length)
+}
+
+NumericData.prototype.write = function write (bitBuffer) {
+ let i, group, value
+
+ // The input data string is divided into groups of three digits,
+ // and each group is converted to its 10-bit binary equivalent.
+ for (i = 0; i + 3 <= this.data.length; i += 3) {
+ group = this.data.substr(i, 3)
+ value = parseInt(group, 10)
+
+ bitBuffer.put(value, 10)
+ }
+
+ // If the number of input digits is not an exact multiple of three,
+ // the final one or two digits are converted to 4 or 7 bits respectively.
+ const remainingNum = this.data.length - i
+ if (remainingNum > 0) {
+ group = this.data.substr(i)
+ value = parseInt(group, 10)
+
+ bitBuffer.put(value, remainingNum * 3 + 1)
+ }
+}
+
+module.exports = NumericData
diff --git a/BeadQR/miniprogram/utils/qrcore/polynomial.js b/BeadQR/miniprogram/utils/qrcore/polynomial.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6ed9c2e9173b5e494d488e99b72e03eb55fcd2f
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/polynomial.js
@@ -0,0 +1,62 @@
+const GF = require('./galois-field')
+
+/**
+ * Multiplies two polynomials inside Galois Field
+ *
+ * @param {Uint8Array} p1 Polynomial
+ * @param {Uint8Array} p2 Polynomial
+ * @return {Uint8Array} Product of p1 and p2
+ */
+exports.mul = function mul (p1, p2) {
+ const coeff = new Uint8Array(p1.length + p2.length - 1)
+
+ for (let i = 0; i < p1.length; i++) {
+ for (let j = 0; j < p2.length; j++) {
+ coeff[i + j] ^= GF.mul(p1[i], p2[j])
+ }
+ }
+
+ return coeff
+}
+
+/**
+ * Calculate the remainder of polynomials division
+ *
+ * @param {Uint8Array} divident Polynomial
+ * @param {Uint8Array} divisor Polynomial
+ * @return {Uint8Array} Remainder
+ */
+exports.mod = function mod (divident, divisor) {
+ let result = new Uint8Array(divident)
+
+ while ((result.length - divisor.length) >= 0) {
+ const coeff = result[0]
+
+ for (let i = 0; i < divisor.length; i++) {
+ result[i] ^= GF.mul(divisor[i], coeff)
+ }
+
+ // remove all zeros from buffer head
+ let offset = 0
+ while (offset < result.length && result[offset] === 0) offset++
+ result = result.slice(offset)
+ }
+
+ return result
+}
+
+/**
+ * Generate an irreducible generator polynomial of specified degree
+ * (used by Reed-Solomon encoder)
+ *
+ * @param {Number} degree Degree of the generator polynomial
+ * @return {Uint8Array} Buffer containing polynomial coefficients
+ */
+exports.generateECPolynomial = function generateECPolynomial (degree) {
+ let poly = new Uint8Array([1])
+ for (let i = 0; i < degree; i++) {
+ poly = exports.mul(poly, new Uint8Array([1, GF.exp(i)]))
+ }
+
+ return poly
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/qrcode.js b/BeadQR/miniprogram/utils/qrcore/qrcode.js
new file mode 100644
index 0000000000000000000000000000000000000000..f76df54b3c595e831d70c189e446479dbe8ac75a
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/qrcode.js
@@ -0,0 +1,495 @@
+const Utils = require('./utils')
+const ECLevel = require('./error-correction-level')
+const BitBuffer = require('./bit-buffer')
+const BitMatrix = require('./bit-matrix')
+const AlignmentPattern = require('./alignment-pattern')
+const FinderPattern = require('./finder-pattern')
+const MaskPattern = require('./mask-pattern')
+const ECCode = require('./error-correction-code')
+const ReedSolomonEncoder = require('./reed-solomon-encoder')
+const Version = require('./version')
+const FormatInfo = require('./format-info')
+const Mode = require('./mode')
+const Segments = require('./segments')
+
+/**
+ * QRCode for JavaScript
+ *
+ * modified by Ryan Day for nodejs support
+ * Copyright (c) 2011 Ryan Day
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+//---------------------------------------------------------------------
+// QRCode for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word "QR Code" is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+*/
+
+/**
+ * Add finder patterns bits to matrix
+ *
+ * @param {BitMatrix} matrix Modules matrix
+ * @param {Number} version QR Code version
+ */
+function setupFinderPattern (matrix, version) {
+ const size = matrix.size
+ const pos = FinderPattern.getPositions(version)
+
+ for (let i = 0; i < pos.length; i++) {
+ const row = pos[i][0]
+ const col = pos[i][1]
+
+ for (let r = -1; r <= 7; r++) {
+ if (row + r <= -1 || size <= row + r) continue
+
+ for (let c = -1; c <= 7; c++) {
+ if (col + c <= -1 || size <= col + c) continue
+
+ if ((r >= 0 && r <= 6 && (c === 0 || c === 6)) ||
+ (c >= 0 && c <= 6 && (r === 0 || r === 6)) ||
+ (r >= 2 && r <= 4 && c >= 2 && c <= 4)) {
+ matrix.set(row + r, col + c, true, true)
+ } else {
+ matrix.set(row + r, col + c, false, true)
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Add timing pattern bits to matrix
+ *
+ * Note: this function must be called before {@link setupAlignmentPattern}
+ *
+ * @param {BitMatrix} matrix Modules matrix
+ */
+function setupTimingPattern (matrix) {
+ const size = matrix.size
+
+ for (let r = 8; r < size - 8; r++) {
+ const value = r % 2 === 0
+ matrix.set(r, 6, value, true)
+ matrix.set(6, r, value, true)
+ }
+}
+
+/**
+ * Add alignment patterns bits to matrix
+ *
+ * Note: this function must be called after {@link setupTimingPattern}
+ *
+ * @param {BitMatrix} matrix Modules matrix
+ * @param {Number} version QR Code version
+ */
+function setupAlignmentPattern (matrix, version) {
+ const pos = AlignmentPattern.getPositions(version)
+
+ for (let i = 0; i < pos.length; i++) {
+ const row = pos[i][0]
+ const col = pos[i][1]
+
+ for (let r = -2; r <= 2; r++) {
+ for (let c = -2; c <= 2; c++) {
+ if (r === -2 || r === 2 || c === -2 || c === 2 ||
+ (r === 0 && c === 0)) {
+ matrix.set(row + r, col + c, true, true)
+ } else {
+ matrix.set(row + r, col + c, false, true)
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Add version info bits to matrix
+ *
+ * @param {BitMatrix} matrix Modules matrix
+ * @param {Number} version QR Code version
+ */
+function setupVersionInfo (matrix, version) {
+ const size = matrix.size
+ const bits = Version.getEncodedBits(version)
+ let row, col, mod
+
+ for (let i = 0; i < 18; i++) {
+ row = Math.floor(i / 3)
+ col = i % 3 + size - 8 - 3
+ mod = ((bits >> i) & 1) === 1
+
+ matrix.set(row, col, mod, true)
+ matrix.set(col, row, mod, true)
+ }
+}
+
+/**
+ * Add format info bits to matrix
+ *
+ * @param {BitMatrix} matrix Modules matrix
+ * @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level
+ * @param {Number} maskPattern Mask pattern reference value
+ */
+function setupFormatInfo (matrix, errorCorrectionLevel, maskPattern) {
+ const size = matrix.size
+ const bits = FormatInfo.getEncodedBits(errorCorrectionLevel, maskPattern)
+ let i, mod
+
+ for (i = 0; i < 15; i++) {
+ mod = ((bits >> i) & 1) === 1
+
+ // vertical
+ if (i < 6) {
+ matrix.set(i, 8, mod, true)
+ } else if (i < 8) {
+ matrix.set(i + 1, 8, mod, true)
+ } else {
+ matrix.set(size - 15 + i, 8, mod, true)
+ }
+
+ // horizontal
+ if (i < 8) {
+ matrix.set(8, size - i - 1, mod, true)
+ } else if (i < 9) {
+ matrix.set(8, 15 - i - 1 + 1, mod, true)
+ } else {
+ matrix.set(8, 15 - i - 1, mod, true)
+ }
+ }
+
+ // fixed module
+ matrix.set(size - 8, 8, 1, true)
+}
+
+/**
+ * Add encoded data bits to matrix
+ *
+ * @param {BitMatrix} matrix Modules matrix
+ * @param {Uint8Array} data Data codewords
+ */
+function setupData (matrix, data) {
+ const size = matrix.size
+ let inc = -1
+ let row = size - 1
+ let bitIndex = 7
+ let byteIndex = 0
+
+ for (let col = size - 1; col > 0; col -= 2) {
+ if (col === 6) col--
+
+ while (true) {
+ for (let c = 0; c < 2; c++) {
+ if (!matrix.isReserved(row, col - c)) {
+ let dark = false
+
+ if (byteIndex < data.length) {
+ dark = (((data[byteIndex] >>> bitIndex) & 1) === 1)
+ }
+
+ matrix.set(row, col - c, dark)
+ bitIndex--
+
+ if (bitIndex === -1) {
+ byteIndex++
+ bitIndex = 7
+ }
+ }
+ }
+
+ row += inc
+
+ if (row < 0 || size <= row) {
+ row -= inc
+ inc = -inc
+ break
+ }
+ }
+ }
+}
+
+/**
+ * Create encoded codewords from data input
+ *
+ * @param {Number} version QR Code version
+ * @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level
+ * @param {ByteData} data Data input
+ * @return {Uint8Array} Buffer containing encoded codewords
+ */
+function createData (version, errorCorrectionLevel, segments) {
+ // Prepare data buffer
+ const buffer = new BitBuffer()
+
+ segments.forEach(function (data) {
+ // prefix data with mode indicator (4 bits)
+ buffer.put(data.mode.bit, 4)
+
+ // Prefix data with character count indicator.
+ // The character count indicator is a string of bits that represents the
+ // number of characters that are being encoded.
+ // The character count indicator must be placed after the mode indicator
+ // and must be a certain number of bits long, depending on the QR version
+ // and data mode
+ // @see {@link Mode.getCharCountIndicator}.
+ buffer.put(data.getLength(), Mode.getCharCountIndicator(data.mode, version))
+
+ // add binary data sequence to buffer
+ data.write(buffer)
+ })
+
+ // Calculate required number of bits
+ const totalCodewords = Utils.getSymbolTotalCodewords(version)
+ const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel)
+ const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8
+
+ // Add a terminator.
+ // If the bit string is shorter than the total number of required bits,
+ // a terminator of up to four 0s must be added to the right side of the string.
+ // If the bit string is more than four bits shorter than the required number of bits,
+ // add four 0s to the end.
+ if (buffer.getLengthInBits() + 4 <= dataTotalCodewordsBits) {
+ buffer.put(0, 4)
+ }
+
+ // If the bit string is fewer than four bits shorter, add only the number of 0s that
+ // are needed to reach the required number of bits.
+
+ // After adding the terminator, if the number of bits in the string is not a multiple of 8,
+ // pad the string on the right with 0s to make the string's length a multiple of 8.
+ while (buffer.getLengthInBits() % 8 !== 0) {
+ buffer.putBit(0)
+ }
+
+ // Add pad bytes if the string is still shorter than the total number of required bits.
+ // Extend the buffer to fill the data capacity of the symbol corresponding to
+ // the Version and Error Correction Level by adding the Pad Codewords 11101100 (0xEC)
+ // and 00010001 (0x11) alternately.
+ const remainingByte = (dataTotalCodewordsBits - buffer.getLengthInBits()) / 8
+ for (let i = 0; i < remainingByte; i++) {
+ buffer.put(i % 2 ? 0x11 : 0xEC, 8)
+ }
+
+ return createCodewords(buffer, version, errorCorrectionLevel)
+}
+
+/**
+ * Encode input data with Reed-Solomon and return codewords with
+ * relative error correction bits
+ *
+ * @param {BitBuffer} bitBuffer Data to encode
+ * @param {Number} version QR Code version
+ * @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level
+ * @return {Uint8Array} Buffer containing encoded codewords
+ */
+function createCodewords (bitBuffer, version, errorCorrectionLevel) {
+ // Total codewords for this QR code version (Data + Error correction)
+ const totalCodewords = Utils.getSymbolTotalCodewords(version)
+
+ // Total number of error correction codewords
+ const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel)
+
+ // Total number of data codewords
+ const dataTotalCodewords = totalCodewords - ecTotalCodewords
+
+ // Total number of blocks
+ const ecTotalBlocks = ECCode.getBlocksCount(version, errorCorrectionLevel)
+
+ // Calculate how many blocks each group should contain
+ const blocksInGroup2 = totalCodewords % ecTotalBlocks
+ const blocksInGroup1 = ecTotalBlocks - blocksInGroup2
+
+ const totalCodewordsInGroup1 = Math.floor(totalCodewords / ecTotalBlocks)
+
+ const dataCodewordsInGroup1 = Math.floor(dataTotalCodewords / ecTotalBlocks)
+ const dataCodewordsInGroup2 = dataCodewordsInGroup1 + 1
+
+ // Number of EC codewords is the same for both groups
+ const ecCount = totalCodewordsInGroup1 - dataCodewordsInGroup1
+
+ // Initialize a Reed-Solomon encoder with a generator polynomial of degree ecCount
+ const rs = new ReedSolomonEncoder(ecCount)
+
+ let offset = 0
+ const dcData = new Array(ecTotalBlocks)
+ const ecData = new Array(ecTotalBlocks)
+ let maxDataSize = 0
+ const buffer = new Uint8Array(bitBuffer.buffer)
+
+ // Divide the buffer into the required number of blocks
+ for (let b = 0; b < ecTotalBlocks; b++) {
+ const dataSize = b < blocksInGroup1 ? dataCodewordsInGroup1 : dataCodewordsInGroup2
+
+ // extract a block of data from buffer
+ dcData[b] = buffer.slice(offset, offset + dataSize)
+
+ // Calculate EC codewords for this data block
+ ecData[b] = rs.encode(dcData[b])
+
+ offset += dataSize
+ maxDataSize = Math.max(maxDataSize, dataSize)
+ }
+
+ // Create final data
+ // Interleave the data and error correction codewords from each block
+ const data = new Uint8Array(totalCodewords)
+ let index = 0
+ let i, r
+
+ // Add data codewords
+ for (i = 0; i < maxDataSize; i++) {
+ for (r = 0; r < ecTotalBlocks; r++) {
+ if (i < dcData[r].length) {
+ data[index++] = dcData[r][i]
+ }
+ }
+ }
+
+ // Apped EC codewords
+ for (i = 0; i < ecCount; i++) {
+ for (r = 0; r < ecTotalBlocks; r++) {
+ data[index++] = ecData[r][i]
+ }
+ }
+
+ return data
+}
+
+/**
+ * Build QR Code symbol
+ *
+ * @param {String} data Input string
+ * @param {Number} version QR Code version
+ * @param {ErrorCorretionLevel} errorCorrectionLevel Error level
+ * @param {MaskPattern} maskPattern Mask pattern
+ * @return {Object} Object containing symbol data
+ */
+function createSymbol (data, version, errorCorrectionLevel, maskPattern) {
+ let segments
+
+ if (Array.isArray(data)) {
+ segments = Segments.fromArray(data)
+ } else if (typeof data === 'string') {
+ let estimatedVersion = version
+
+ if (!estimatedVersion) {
+ const rawSegments = Segments.rawSplit(data)
+
+ // Estimate best version that can contain raw splitted segments
+ estimatedVersion = Version.getBestVersionForData(rawSegments, errorCorrectionLevel)
+ }
+
+ // Build optimized segments
+ // If estimated version is undefined, try with the highest version
+ segments = Segments.fromString(data, estimatedVersion || 40)
+ } else {
+ throw new Error('Invalid data')
+ }
+
+ // Get the min version that can contain data
+ const bestVersion = Version.getBestVersionForData(segments, errorCorrectionLevel)
+
+ // If no version is found, data cannot be stored
+ if (!bestVersion) {
+ throw new Error('The amount of data is too big to be stored in a QR Code')
+ }
+
+ // If not specified, use min version as default
+ if (!version) {
+ version = bestVersion
+
+ // Check if the specified version can contain the data
+ } else if (version < bestVersion) {
+ throw new Error('\n' +
+ 'The chosen QR Code version cannot contain this amount of data.\n' +
+ 'Minimum version required to store current data is: ' + bestVersion + '.\n'
+ )
+ }
+
+ const dataBits = createData(version, errorCorrectionLevel, segments)
+
+ // Allocate matrix buffer
+ const moduleCount = Utils.getSymbolSize(version)
+ const modules = new BitMatrix(moduleCount)
+
+ // Add function modules
+ setupFinderPattern(modules, version)
+ setupTimingPattern(modules)
+ setupAlignmentPattern(modules, version)
+
+ // Add temporary dummy bits for format info just to set them as reserved.
+ // This is needed to prevent these bits from being masked by {@link MaskPattern.applyMask}
+ // since the masking operation must be performed only on the encoding region.
+ // These blocks will be replaced with correct values later in code.
+ setupFormatInfo(modules, errorCorrectionLevel, 0)
+
+ if (version >= 7) {
+ setupVersionInfo(modules, version)
+ }
+
+ // Add data codewords
+ setupData(modules, dataBits)
+
+ if (isNaN(maskPattern)) {
+ // Find best mask pattern
+ maskPattern = MaskPattern.getBestMask(modules,
+ setupFormatInfo.bind(null, modules, errorCorrectionLevel))
+ }
+
+ // Apply mask pattern
+ MaskPattern.applyMask(maskPattern, modules)
+
+ // Replace format info bits with correct values
+ setupFormatInfo(modules, errorCorrectionLevel, maskPattern)
+
+ return {
+ modules: modules,
+ version: version,
+ errorCorrectionLevel: errorCorrectionLevel,
+ maskPattern: maskPattern,
+ segments: segments
+ }
+}
+
+/**
+ * QR Code
+ *
+ * @param {String | Array} data Input data
+ * @param {Object} options Optional configurations
+ * @param {Number} options.version QR Code version
+ * @param {String} options.errorCorrectionLevel Error correction level
+ * @param {Function} options.toSJISFunc Helper func to convert utf8 to sjis
+ */
+exports.create = function create (data, options) {
+ if (typeof data === 'undefined' || data === '') {
+ throw new Error('No input text')
+ }
+
+ let errorCorrectionLevel = ECLevel.M
+ let version
+ let mask
+
+ if (typeof options !== 'undefined') {
+ // Use higher error correction level as default
+ errorCorrectionLevel = ECLevel.from(options.errorCorrectionLevel, ECLevel.M)
+ version = Version.from(options.version)
+ mask = MaskPattern.from(options.maskPattern)
+
+ if (options.toSJISFunc) {
+ Utils.setToSJISFunction(options.toSJISFunc)
+ }
+ }
+
+ return createSymbol(data, version, errorCorrectionLevel, mask)
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/reed-solomon-encoder.js b/BeadQR/miniprogram/utils/qrcore/reed-solomon-encoder.js
new file mode 100644
index 0000000000000000000000000000000000000000..c19842279cd54c8a09cd821aaeb36ff2c1229101
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/reed-solomon-encoder.js
@@ -0,0 +1,56 @@
+const Polynomial = require('./polynomial')
+
+function ReedSolomonEncoder (degree) {
+ this.genPoly = undefined
+ this.degree = degree
+
+ if (this.degree) this.initialize(this.degree)
+}
+
+/**
+ * Initialize the encoder.
+ * The input param should correspond to the number of error correction codewords.
+ *
+ * @param {Number} degree
+ */
+ReedSolomonEncoder.prototype.initialize = function initialize (degree) {
+ // create an irreducible generator polynomial
+ this.degree = degree
+ this.genPoly = Polynomial.generateECPolynomial(this.degree)
+}
+
+/**
+ * Encodes a chunk of data
+ *
+ * @param {Uint8Array} data Buffer containing input data
+ * @return {Uint8Array} Buffer containing encoded data
+ */
+ReedSolomonEncoder.prototype.encode = function encode (data) {
+ if (!this.genPoly) {
+ throw new Error('Encoder not initialized')
+ }
+
+ // Calculate EC for this data block
+ // extends data size to data+genPoly size
+ const paddedData = new Uint8Array(data.length + this.degree)
+ paddedData.set(data)
+
+ // The error correction codewords are the remainder after dividing the data codewords
+ // by a generator polynomial
+ const remainder = Polynomial.mod(paddedData, this.genPoly)
+
+ // return EC data blocks (last n byte, where n is the degree of genPoly)
+ // If coefficients number in remainder are less than genPoly degree,
+ // pad with 0s to the left to reach the needed number of coefficients
+ const start = this.degree - remainder.length
+ if (start > 0) {
+ const buff = new Uint8Array(this.degree)
+ buff.set(remainder, start)
+
+ return buff
+ }
+
+ return remainder
+}
+
+module.exports = ReedSolomonEncoder
diff --git a/BeadQR/miniprogram/utils/qrcore/regex.js b/BeadQR/miniprogram/utils/qrcore/regex.js
new file mode 100644
index 0000000000000000000000000000000000000000..9dd13a4231a1a88d66652ac1249a316c0265c9d8
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/regex.js
@@ -0,0 +1,31 @@
+const numeric = '[0-9]+'
+const alphanumeric = '[A-Z $%*+\\-./:]+'
+let kanji = '(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|' +
+ '[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|' +
+ '[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|' +
+ '[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+'
+kanji = kanji.replace(/u/g, '\\u')
+
+const byte = '(?:(?![A-Z0-9 $%*+\\-./:]|' + kanji + ')(?:.|[\r\n]))+'
+
+exports.KANJI = new RegExp(kanji, 'g')
+exports.BYTE_KANJI = new RegExp('[^A-Z0-9 $%*+\\-./:]+', 'g')
+exports.BYTE = new RegExp(byte, 'g')
+exports.NUMERIC = new RegExp(numeric, 'g')
+exports.ALPHANUMERIC = new RegExp(alphanumeric, 'g')
+
+const TEST_KANJI = new RegExp('^' + kanji + '$')
+const TEST_NUMERIC = new RegExp('^' + numeric + '$')
+const TEST_ALPHANUMERIC = new RegExp('^[A-Z0-9 $%*+\\-./:]+$')
+
+exports.testKanji = function testKanji (str) {
+ return TEST_KANJI.test(str)
+}
+
+exports.testNumeric = function testNumeric (str) {
+ return TEST_NUMERIC.test(str)
+}
+
+exports.testAlphanumeric = function testAlphanumeric (str) {
+ return TEST_ALPHANUMERIC.test(str)
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/segments.js b/BeadQR/miniprogram/utils/qrcore/segments.js
new file mode 100644
index 0000000000000000000000000000000000000000..9254178d4e2d899092cb452724dee66fe90cdf39
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/segments.js
@@ -0,0 +1,330 @@
+const Mode = require('./mode')
+const NumericData = require('./numeric-data')
+const AlphanumericData = require('./alphanumeric-data')
+const ByteData = require('./byte-data')
+const KanjiData = require('./kanji-data')
+const Regex = require('./regex')
+const Utils = require('./utils')
+const dijkstra = require('./dijkstrajs')
+
+/**
+ * Returns UTF8 byte length
+ *
+ * @param {String} str Input string
+ * @return {Number} Number of byte
+ */
+function getStringByteLength (str) {
+ return unescape(encodeURIComponent(str)).length
+}
+
+/**
+ * Get a list of segments of the specified mode
+ * from a string
+ *
+ * @param {Mode} mode Segment mode
+ * @param {String} str String to process
+ * @return {Array} Array of object with segments data
+ */
+function getSegments (regex, mode, str) {
+ const segments = []
+ let result
+
+ while ((result = regex.exec(str)) !== null) {
+ segments.push({
+ data: result[0],
+ index: result.index,
+ mode: mode,
+ length: result[0].length
+ })
+ }
+
+ return segments
+}
+
+/**
+ * Extracts a series of segments with the appropriate
+ * modes from a string
+ *
+ * @param {String} dataStr Input string
+ * @return {Array} Array of object with segments data
+ */
+function getSegmentsFromString (dataStr) {
+ const numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr)
+ const alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr)
+ let byteSegs
+ let kanjiSegs
+
+ if (Utils.isKanjiModeEnabled()) {
+ byteSegs = getSegments(Regex.BYTE, Mode.BYTE, dataStr)
+ kanjiSegs = getSegments(Regex.KANJI, Mode.KANJI, dataStr)
+ } else {
+ byteSegs = getSegments(Regex.BYTE_KANJI, Mode.BYTE, dataStr)
+ kanjiSegs = []
+ }
+
+ const segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs)
+
+ return segs
+ .sort(function (s1, s2) {
+ return s1.index - s2.index
+ })
+ .map(function (obj) {
+ return {
+ data: obj.data,
+ mode: obj.mode,
+ length: obj.length
+ }
+ })
+}
+
+/**
+ * Returns how many bits are needed to encode a string of
+ * specified length with the specified mode
+ *
+ * @param {Number} length String length
+ * @param {Mode} mode Segment mode
+ * @return {Number} Bit length
+ */
+function getSegmentBitsLength (length, mode) {
+ switch (mode) {
+ case Mode.NUMERIC:
+ return NumericData.getBitsLength(length)
+ case Mode.ALPHANUMERIC:
+ return AlphanumericData.getBitsLength(length)
+ case Mode.KANJI:
+ return KanjiData.getBitsLength(length)
+ case Mode.BYTE:
+ return ByteData.getBitsLength(length)
+ }
+}
+
+/**
+ * Merges adjacent segments which have the same mode
+ *
+ * @param {Array} segs Array of object with segments data
+ * @return {Array} Array of object with segments data
+ */
+function mergeSegments (segs) {
+ return segs.reduce(function (acc, curr) {
+ const prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null
+ if (prevSeg && prevSeg.mode === curr.mode) {
+ acc[acc.length - 1].data += curr.data
+ return acc
+ }
+
+ acc.push(curr)
+ return acc
+ }, [])
+}
+
+/**
+ * Generates a list of all possible nodes combination which
+ * will be used to build a segments graph.
+ *
+ * Nodes are divided by groups. Each group will contain a list of all the modes
+ * in which is possible to encode the given text.
+ *
+ * For example the text '12345' can be encoded as Numeric, Alphanumeric or Byte.
+ * The group for '12345' will contain then 3 objects, one for each
+ * possible encoding mode.
+ *
+ * Each node represents a possible segment.
+ *
+ * @param {Array} segs Array of object with segments data
+ * @return {Array} Array of object with segments data
+ */
+function buildNodes (segs) {
+ const nodes = []
+ for (let i = 0; i < segs.length; i++) {
+ const seg = segs[i]
+
+ switch (seg.mode) {
+ case Mode.NUMERIC:
+ nodes.push([seg,
+ { data: seg.data, mode: Mode.ALPHANUMERIC, length: seg.length },
+ { data: seg.data, mode: Mode.BYTE, length: seg.length }
+ ])
+ break
+ case Mode.ALPHANUMERIC:
+ nodes.push([seg,
+ { data: seg.data, mode: Mode.BYTE, length: seg.length }
+ ])
+ break
+ case Mode.KANJI:
+ nodes.push([seg,
+ { data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
+ ])
+ break
+ case Mode.BYTE:
+ nodes.push([
+ { data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
+ ])
+ }
+ }
+
+ return nodes
+}
+
+/**
+ * Builds a graph from a list of nodes.
+ * All segments in each node group will be connected with all the segments of
+ * the next group and so on.
+ *
+ * At each connection will be assigned a weight depending on the
+ * segment's byte length.
+ *
+ * @param {Array} nodes Array of object with segments data
+ * @param {Number} version QR Code version
+ * @return {Object} Graph of all possible segments
+ */
+function buildGraph (nodes, version) {
+ const table = {}
+ const graph = { start: {} }
+ let prevNodeIds = ['start']
+
+ for (let i = 0; i < nodes.length; i++) {
+ const nodeGroup = nodes[i]
+ const currentNodeIds = []
+
+ for (let j = 0; j < nodeGroup.length; j++) {
+ const node = nodeGroup[j]
+ const key = '' + i + j
+
+ currentNodeIds.push(key)
+ table[key] = { node: node, lastCount: 0 }
+ graph[key] = {}
+
+ for (let n = 0; n < prevNodeIds.length; n++) {
+ const prevNodeId = prevNodeIds[n]
+
+ if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) {
+ graph[prevNodeId][key] =
+ getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) -
+ getSegmentBitsLength(table[prevNodeId].lastCount, node.mode)
+
+ table[prevNodeId].lastCount += node.length
+ } else {
+ if (table[prevNodeId]) table[prevNodeId].lastCount = node.length
+
+ graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) +
+ 4 + Mode.getCharCountIndicator(node.mode, version) // switch cost
+ }
+ }
+ }
+
+ prevNodeIds = currentNodeIds
+ }
+
+ for (let n = 0; n < prevNodeIds.length; n++) {
+ graph[prevNodeIds[n]].end = 0
+ }
+
+ return { map: graph, table: table }
+}
+
+/**
+ * Builds a segment from a specified data and mode.
+ * If a mode is not specified, the more suitable will be used.
+ *
+ * @param {String} data Input data
+ * @param {Mode | String} modesHint Data mode
+ * @return {Segment} Segment
+ */
+function buildSingleSegment (data, modesHint) {
+ let mode
+ const bestMode = Mode.getBestModeForData(data)
+
+ mode = Mode.from(modesHint, bestMode)
+
+ // Make sure data can be encoded
+ if (mode !== Mode.BYTE && mode.bit < bestMode.bit) {
+ throw new Error('"' + data + '"' +
+ ' cannot be encoded with mode ' + Mode.toString(mode) +
+ '.\n Suggested mode is: ' + Mode.toString(bestMode))
+ }
+
+ // Use Mode.BYTE if Kanji support is disabled
+ if (mode === Mode.KANJI && !Utils.isKanjiModeEnabled()) {
+ mode = Mode.BYTE
+ }
+
+ switch (mode) {
+ case Mode.NUMERIC:
+ return new NumericData(data)
+
+ case Mode.ALPHANUMERIC:
+ return new AlphanumericData(data)
+
+ case Mode.KANJI:
+ return new KanjiData(data)
+
+ case Mode.BYTE:
+ return new ByteData(data)
+ }
+}
+
+/**
+ * Builds a list of segments from an array.
+ * Array can contain Strings or Objects with segment's info.
+ *
+ * For each item which is a string, will be generated a segment with the given
+ * string and the more appropriate encoding mode.
+ *
+ * For each item which is an object, will be generated a segment with the given
+ * data and mode.
+ * Objects must contain at least the property "data".
+ * If property "mode" is not present, the more suitable mode will be used.
+ *
+ * @param {Array} array Array of objects with segments data
+ * @return {Array} Array of Segments
+ */
+exports.fromArray = function fromArray (array) {
+ return array.reduce(function (acc, seg) {
+ if (typeof seg === 'string') {
+ acc.push(buildSingleSegment(seg, null))
+ } else if (seg.data) {
+ acc.push(buildSingleSegment(seg.data, seg.mode))
+ }
+
+ return acc
+ }, [])
+}
+
+/**
+ * Builds an optimized sequence of segments from a string,
+ * which will produce the shortest possible bitstream.
+ *
+ * @param {String} data Input string
+ * @param {Number} version QR Code version
+ * @return {Array} Array of segments
+ */
+exports.fromString = function fromString (data, version) {
+ const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled())
+
+ const nodes = buildNodes(segs)
+ const graph = buildGraph(nodes, version)
+ const path = dijkstra.find_path(graph.map, 'start', 'end')
+
+ const optimizedSegs = []
+ for (let i = 1; i < path.length - 1; i++) {
+ optimizedSegs.push(graph.table[path[i]].node)
+ }
+
+ return exports.fromArray(mergeSegments(optimizedSegs))
+}
+
+/**
+ * Splits a string in various segments with the modes which
+ * best represent their content.
+ * The produced segments are far from being optimized.
+ * The output of this function is only used to estimate a QR Code version
+ * which may contain the data.
+ *
+ * @param {string} data Input string
+ * @return {Array} Array of segments
+ */
+exports.rawSplit = function rawSplit (data) {
+ return exports.fromArray(
+ getSegmentsFromString(data, Utils.isKanjiModeEnabled())
+ )
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/utils.js b/BeadQR/miniprogram/utils/qrcore/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a0b87ba5ad14a058d81ebc9d9439f87f271a2e7
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/utils.js
@@ -0,0 +1,63 @@
+let toSJISFunction
+const CODEWORDS_COUNT = [
+ 0, // Not used
+ 26, 44, 70, 100, 134, 172, 196, 242, 292, 346,
+ 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085,
+ 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185,
+ 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706
+]
+
+/**
+ * Returns the QR Code size for the specified version
+ *
+ * @param {Number} version QR Code version
+ * @return {Number} size of QR code
+ */
+exports.getSymbolSize = function getSymbolSize (version) {
+ if (!version) throw new Error('"version" cannot be null or undefined')
+ if (version < 1 || version > 40) throw new Error('"version" should be in range from 1 to 40')
+ return version * 4 + 17
+}
+
+/**
+ * Returns the total number of codewords used to store data and EC information.
+ *
+ * @param {Number} version QR Code version
+ * @return {Number} Data length in bits
+ */
+exports.getSymbolTotalCodewords = function getSymbolTotalCodewords (version) {
+ return CODEWORDS_COUNT[version]
+}
+
+/**
+ * Encode data with Bose-Chaudhuri-Hocquenghem
+ *
+ * @param {Number} data Value to encode
+ * @return {Number} Encoded value
+ */
+exports.getBCHDigit = function (data) {
+ let digit = 0
+
+ while (data !== 0) {
+ digit++
+ data >>>= 1
+ }
+
+ return digit
+}
+
+exports.setToSJISFunction = function setToSJISFunction (f) {
+ if (typeof f !== 'function') {
+ throw new Error('"toSJISFunc" is not a valid function.')
+ }
+
+ toSJISFunction = f
+}
+
+exports.isKanjiModeEnabled = function () {
+ return typeof toSJISFunction !== 'undefined'
+}
+
+exports.toSJIS = function toSJIS (kanji) {
+ return toSJISFunction(kanji)
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/version-check.js b/BeadQR/miniprogram/utils/qrcore/version-check.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd32b5aced810559e5ef974d483cb0660996737e
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/version-check.js
@@ -0,0 +1,9 @@
+/**
+ * Check if QR Code version is valid
+ *
+ * @param {Number} version QR Code version
+ * @return {Boolean} true if valid version, false otherwise
+ */
+exports.isValid = function isValid (version) {
+ return !isNaN(version) && version >= 1 && version <= 40
+}
diff --git a/BeadQR/miniprogram/utils/qrcore/version.js b/BeadQR/miniprogram/utils/qrcore/version.js
new file mode 100644
index 0000000000000000000000000000000000000000..32b846615722615db5b68f422bb46eae885246c6
--- /dev/null
+++ b/BeadQR/miniprogram/utils/qrcore/version.js
@@ -0,0 +1,163 @@
+const Utils = require('./utils')
+const ECCode = require('./error-correction-code')
+const ECLevel = require('./error-correction-level')
+const Mode = require('./mode')
+const VersionCheck = require('./version-check')
+
+// Generator polynomial used to encode version information
+const G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0)
+const G18_BCH = Utils.getBCHDigit(G18)
+
+function getBestVersionForDataLength (mode, length, errorCorrectionLevel) {
+ for (let currentVersion = 1; currentVersion <= 40; currentVersion++) {
+ if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, mode)) {
+ return currentVersion
+ }
+ }
+
+ return undefined
+}
+
+function getReservedBitsCount (mode, version) {
+ // Character count indicator + mode indicator bits
+ return Mode.getCharCountIndicator(mode, version) + 4
+}
+
+function getTotalBitsFromDataArray (segments, version) {
+ let totalBits = 0
+
+ segments.forEach(function (data) {
+ const reservedBits = getReservedBitsCount(data.mode, version)
+ totalBits += reservedBits + data.getBitsLength()
+ })
+
+ return totalBits
+}
+
+function getBestVersionForMixedData (segments, errorCorrectionLevel) {
+ for (let currentVersion = 1; currentVersion <= 40; currentVersion++) {
+ const length = getTotalBitsFromDataArray(segments, currentVersion)
+ if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, Mode.MIXED)) {
+ return currentVersion
+ }
+ }
+
+ return undefined
+}
+
+/**
+ * Returns version number from a value.
+ * If value is not a valid version, returns defaultValue
+ *
+ * @param {Number|String} value QR Code version
+ * @param {Number} defaultValue Fallback value
+ * @return {Number} QR Code version number
+ */
+exports.from = function from (value, defaultValue) {
+ if (VersionCheck.isValid(value)) {
+ return parseInt(value, 10)
+ }
+
+ return defaultValue
+}
+
+/**
+ * Returns how much data can be stored with the specified QR code version
+ * and error correction level
+ *
+ * @param {Number} version QR Code version (1-40)
+ * @param {Number} errorCorrectionLevel Error correction level
+ * @param {Mode} mode Data mode
+ * @return {Number} Quantity of storable data
+ */
+exports.getCapacity = function getCapacity (version, errorCorrectionLevel, mode) {
+ if (!VersionCheck.isValid(version)) {
+ throw new Error('Invalid QR Code version')
+ }
+
+ // Use Byte mode as default
+ if (typeof mode === 'undefined') mode = Mode.BYTE
+
+ // Total codewords for this QR code version (Data + Error correction)
+ const totalCodewords = Utils.getSymbolTotalCodewords(version)
+
+ // Total number of error correction codewords
+ const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel)
+
+ // Total number of data codewords
+ const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8
+
+ if (mode === Mode.MIXED) return dataTotalCodewordsBits
+
+ const usableBits = dataTotalCodewordsBits - getReservedBitsCount(mode, version)
+
+ // Return max number of storable codewords
+ switch (mode) {
+ case Mode.NUMERIC:
+ return Math.floor((usableBits / 10) * 3)
+
+ case Mode.ALPHANUMERIC:
+ return Math.floor((usableBits / 11) * 2)
+
+ case Mode.KANJI:
+ return Math.floor(usableBits / 13)
+
+ case Mode.BYTE:
+ default:
+ return Math.floor(usableBits / 8)
+ }
+}
+
+/**
+ * Returns the minimum version needed to contain the amount of data
+ *
+ * @param {Segment} data Segment of data
+ * @param {Number} [errorCorrectionLevel=H] Error correction level
+ * @param {Mode} mode Data mode
+ * @return {Number} QR Code version
+ */
+exports.getBestVersionForData = function getBestVersionForData (data, errorCorrectionLevel) {
+ let seg
+
+ const ecl = ECLevel.from(errorCorrectionLevel, ECLevel.M)
+
+ if (Array.isArray(data)) {
+ if (data.length > 1) {
+ return getBestVersionForMixedData(data, ecl)
+ }
+
+ if (data.length === 0) {
+ return 1
+ }
+
+ seg = data[0]
+ } else {
+ seg = data
+ }
+
+ return getBestVersionForDataLength(seg.mode, seg.getLength(), ecl)
+}
+
+/**
+ * Returns version information with relative error correction bits
+ *
+ * The version information is included in QR Code symbols of version 7 or larger.
+ * It consists of an 18-bit sequence containing 6 data bits,
+ * with 12 error correction bits calculated using the (18, 6) Golay code.
+ *
+ * @param {Number} version QR Code version
+ * @return {Number} Encoded version info bits
+ */
+exports.getEncodedBits = function getEncodedBits (version) {
+ if (!VersionCheck.isValid(version) || version < 7) {
+ throw new Error('Invalid QR Code version')
+ }
+
+ let d = version << 12
+
+ while (Utils.getBCHDigit(d) - G18_BCH >= 0) {
+ d ^= (G18 << (Utils.getBCHDigit(d) - G18_BCH))
+ }
+
+ return (version << 12) | d
+}
diff --git a/BeadQR/miniprogram/utils/scancheck.js b/BeadQR/miniprogram/utils/scancheck.js
new file mode 100644
index 0000000000000000000000000000000000000000..b09829aa94c3019160125d11eb6510be3c48f21b
--- /dev/null
+++ b/BeadQR/miniprogram/utils/scancheck.js
@@ -0,0 +1,36 @@
+// 本地扫码自检:把生成的色号网格渲染成像素,用 jsQR 解码并比对内容
+// jsQR 比微信扫码更“挑剔”,自检通过基本可以放心;未通过时提示用户调整
+const jsQR = require('./vendor/jsqr')
+const { hexToRgb } = require('./color')
+
+function selfCheck(artwork, expectedText) {
+ const { size, cells, pal } = artwork
+ const px = size > 90 ? 4 : 5
+ const margin = 4
+ const dim = (size + margin * 2) * px
+ const data = new Uint8ClampedArray(dim * dim * 4).fill(255)
+ const colors = pal.hexes.map(hexToRgb)
+ for (let y = 0; y < size; y += 1) {
+ for (let x = 0; x < size; x += 1) {
+ const c = cells[y * size + x]
+ if (c < 0) continue // 空钉位视为白底
+ const [r, g, b] = colors[c]
+ for (let dy = 0; dy < px; dy += 1) {
+ let o = (((y + margin) * px + dy) * dim + (x + margin) * px) * 4
+ for (let dx = 0; dx < px; dx += 1) {
+ data[o] = r; data[o + 1] = g; data[o + 2] = b
+ o += 4
+ }
+ }
+ }
+ }
+ try {
+ const result = jsQR(data, dim, dim, { inversionAttempts: 'dontInvert' })
+ if (!result) return { ok: false, reason: 'unreadable' }
+ return { ok: result.data === expectedText, reason: result.data === expectedText ? '' : 'mismatch' }
+ } catch (e) {
+ return { ok: false, reason: 'error' }
+ }
+}
+
+module.exports = { selfCheck }
diff --git a/BeadQR/miniprogram/utils/security.js b/BeadQR/miniprogram/utils/security.js
new file mode 100644
index 0000000000000000000000000000000000000000..e7e78d8934ac4f2c621017910fab917142d2ba27
--- /dev/null
+++ b/BeadQR/miniprogram/utils/security.js
@@ -0,0 +1,134 @@
+// 内容安全链路(微信平台规范)
+// 文本:security.msgSecCheck v2(二维码内容 / 扫码导入内容 / 水印)
+// 图片:先按官方建议压缩到 750×1334 内、1MB 内的 JPG,再上传云存储由云函数调用 security.imgSecCheck
+// 约定返回:{ status: 'pass' | 'risky' | 'error' }
+// - risky:命中违规,必须拦截
+// - error:检测服务不可用,同样拦截(禁止静默放行)
+// 通过结果按内容缓存,导出时无需重复请求,显著提升导出速度
+
+const FUNCTION_NAME = 'quickstartFunctions'
+const MAX_W = 750
+const MAX_H = 1334
+const textCache = new Map() // value -> 'pass' | 'risky'
+const pending = new Map() // value -> Promise
+
+function callFunction(data) {
+ return new Promise((resolve) => {
+ if (!wx.cloud) { resolve(null); return }
+ wx.cloud.callFunction({
+ name: FUNCTION_NAME,
+ data,
+ success: (res) => resolve((res && res.result) || null),
+ fail: () => resolve(null),
+ })
+ })
+}
+
+function normalize(result) {
+ if (!result) return 'error'
+ if (result.risk) return 'risky'
+ if (result.skipped) return 'error'
+ return 'pass'
+}
+
+function checkText(content, scene = 2) {
+ const value = String(content || '').trim().slice(0, 2500)
+ if (!value) return Promise.resolve({ status: 'pass' })
+ const cached = textCache.get(value)
+ if (cached) return Promise.resolve({ status: cached, cached: true })
+ if (pending.has(value)) return pending.get(value)
+ const task = callFunction({ type: 'checkMessage', content: value, scene }).then((result) => {
+ const status = normalize(result)
+ if (status !== 'error') textCache.set(value, status)
+ pending.delete(value)
+ return { status }
+ })
+ pending.set(value, task)
+ return task
+}
+
+// 导出前一次性校验多段文本(已缓存的直接复用,未缓存的合并为一次云函数调用)
+function checkTexts(items) {
+ const list = items.map((it) => ({ value: String(it.value || '').trim().slice(0, 2500), scene: it.scene || 2 })).filter((it) => it.value)
+ const results = list.map((it) => textCache.get(it.value))
+ if (results.includes('risky')) return Promise.resolve({ status: 'risky' })
+ const missing = list.filter((it, i) => !results[i])
+ if (!missing.length) return Promise.resolve({ status: 'pass' })
+ return callFunction({ type: 'checkMessages', items: missing }).then((res) => {
+ if (!res || !Array.isArray(res.results)) return { status: 'error' }
+ let status = 'pass'
+ res.results.forEach((r, i) => {
+ const s = normalize(r)
+ if (s !== 'error') textCache.set(missing[i].value, s)
+ if (s === 'risky') status = 'risky'
+ else if (s === 'error' && status === 'pass') status = 'error'
+ })
+ return { status }
+ })
+}
+
+function isTextPassed(value) {
+ const v = String(value || '').trim()
+ return !v || textCache.get(v) === 'pass'
+}
+
+// 按官方尺寸建议压缩(同时加快上传)
+function compressForCheck(canvas, image, path) {
+ return new Promise((resolve) => {
+ if (!canvas || !image) { resolve(path); return }
+ try {
+ const w0 = image.width || image._w
+ const h0 = image.height || image._h
+ const scale = Math.min(1, MAX_W / w0, MAX_H / h0)
+ canvas.width = Math.max(1, Math.round(w0 * scale))
+ canvas.height = Math.max(1, Math.round(h0 * scale))
+ const ctx = canvas.getContext('2d')
+ ctx.fillStyle = '#ffffff'
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
+ wx.canvasToTempFilePath({
+ canvas,
+ fileType: 'jpg',
+ quality: 0.82,
+ destWidth: canvas.width,
+ destHeight: canvas.height,
+ success: (res) => resolve(res.tempFilePath),
+ fail: () => resolve(path),
+ })
+ } catch (e) {
+ resolve(path)
+ }
+ })
+}
+
+function upload(filePath) {
+ return new Promise((resolve) => {
+ if (!wx.cloud) { resolve(''); return }
+ wx.cloud.uploadFile({
+ cloudPath: `sec-check/${Date.now()}-${Math.random().toString(36).slice(2, 10)}.jpg`,
+ filePath,
+ success: (res) => resolve((res && res.fileID) || ''),
+ fail: () => resolve(''),
+ })
+ })
+}
+
+// image:已加载的 CanvasImage(用于压缩);path:本地临时路径
+function checkImage(path, image, canvas) {
+ if (!path) return Promise.resolve({ status: 'error' })
+ return compressForCheck(canvas, image, path)
+ .then(upload)
+ .then((fileID) => {
+ if (!fileID) return { status: 'error' }
+ // 云函数检测完毕后会立即删除该临时文件
+ return callFunction({ type: 'checkImage', fileID }).then((result) => ({ status: normalize(result) }))
+ })
+ .catch(() => ({ status: 'error' }))
+}
+
+const MESSAGES = {
+ risky: '内容含违规信息,请修改后再试',
+ error: '安全检测暂不可用,请检查网络后重试',
+}
+
+module.exports = { checkText, checkTexts, isTextPassed, checkImage, MESSAGES }
diff --git a/BeadQR/miniprogram/utils/vendor/jsqr.js b/BeadQR/miniprogram/utils/vendor/jsqr.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b99db54d44f69a7262e1900adf7fcb4b54f6e65
--- /dev/null
+++ b/BeadQR/miniprogram/utils/vendor/jsqr.js
@@ -0,0 +1,2 @@
+/*! jsQR 1.4.0 | Apache-2.0 | https://github.com/cozmo/jsQR — 用于本地扫码自检,不联网 */
+(function(y,g){typeof exports=="object"&&typeof module=="object"?module.exports=g():typeof define=="function"&&define.amd?define([],g):typeof exports=="object"?exports.jsQR=g():y.jsQR=g()})(typeof self<"u"?self:exports,function(){return function(G){var y={};function g(p){if(y[p])return y[p].exports;var m=y[p]={i:p,l:!1,exports:{}};return G[p].call(m.exports,m,m.exports,g),m.l=!0,m.exports}return g.m=G,g.c=y,g.d=function(p,m,l){g.o(p,m)||Object.defineProperty(p,m,{configurable:!1,enumerable:!0,get:l})},g.n=function(p){var m=p&&p.__esModule?function(){return p.default}:function(){return p};return g.d(m,"a",m),m},g.o=function(p,m){return Object.prototype.hasOwnProperty.call(p,m)},g.p="",g(g.s=3)}([function(G,y,g){"use strict";Object.defineProperty(y,"__esModule",{value:!0});var p=function(){function m(l,s){this.width=s,this.height=l.length/s,this.data=l}return m.createEmpty=function(l,s){return new m(new Uint8ClampedArray(l*s),l)},m.prototype.get=function(l,s){return l<0||l>=this.width||s<0||s>=this.height?!1:!!this.data[s*this.width+l]},m.prototype.set=function(l,s,c){this.data[s*this.width+l]=c?1:0},m.prototype.setRegion=function(l,s,c,o,E){for(var r=s;r=this.size&&(r=(r^this.primitive)&this.size-1);for(var a=0;a1&&c[0]===0){for(var E=1;EE.length&&(c=[E,o],o=c[0],E=c[1]);for(var r=new Uint8ClampedArray(E.length),a=E.length-o.length,F=0;Fa?a:E}var c=function(){function E(r,a){this.width=r,this.data=new Uint8ClampedArray(r*a)}return E.prototype.get=function(r,a){return this.data[a*this.width+r]},E.prototype.set=function(r,a,F){this.data[a*this.width+r]=F},E}();function o(E,r,a,F){if(E.length!==r*a*4)throw new Error("Malformed data passed to binarizer.");for(var d=new c(r,a),D=0;D0&&n>0)){var w=(C.get(n,t-1)+2*C.get(n-1,t)+C.get(n-1,t-1))/4;u6&&(e.setRegion(B-11,0,3,6,!0),e.setRegion(0,B-11,6,3,!0)),e}function F(x,B,e){for(var C=r[e.dataMask],t=x.height,n=a(B),k=[],u=0,f=0,M=!0,b=t-1;b>0;b-=2){b===6&&b--;for(var w=0;w=0;t--)for(var n=B-9;n>=B-11;n--)C=o(x.get(n,t),C);for(var k=0,n=5;n>=0;n--)for(var t=B-9;t>=B-11;t--)k=o(x.get(n,t),k);for(var u=1/0,f,M=0,b=s.VERSIONS;M=0;C--)C!==6&&(B=o(x.get(8,C),B));for(var t=x.height,n=0,C=t-1;C>=t-7;C--)n=o(x.get(8,C),n);for(var e=t-8;e1)for(var w=C.ecBlocks[0].numBlocks,I=C.ecBlocks[1].numBlocks,u=0;u0;)for(var h=0,N=t;h=3;){var B=d.readBits(10);if(B>=1e3)throw new Error("Invalid numeric value above 999");var e=Math.floor(B/100),C=Math.floor(B/10)%10,t=B%10;i.push(48+e,48+C,48+t),v+=e.toString()+C.toString()+t.toString(),x-=3}if(x===2){var B=d.readBits(7);if(B>=100)throw new Error("Invalid numeric value above 99");var e=Math.floor(B/10),C=B%10;i.push(48+e,48+C),v+=e.toString()+C.toString()}else if(x===1){var B=d.readBits(4);if(B>=10)throw new Error("Invalid numeric value above 9");i.push(48+B),v+=B.toString()}return{bytes:i,text:v}}var o=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function E(d,D){for(var i=[],v="",A=[9,11,13][D],x=d.readBits(A);x>=2;){var B=d.readBits(11),e=Math.floor(B/45),C=B%45;i.push(o[e].charCodeAt(0),o[C].charCodeAt(0)),v+=o[e]+o[C],x-=2}if(x===1){var e=d.readBits(6);i.push(o[e].charCodeAt(0)),v+=o[e]}return{bytes:i,text:v}}function r(d,D){for(var i=[],v="",A=[8,16,16][D],x=d.readBits(A),B=0;B>8,C&255),v+=String.fromCharCode(m.shiftJISTable[C])}return{bytes:i,text:v}}function F(d,D){for(var i,v,A,x,B=new p.BitStream(d),e=D<=9?0:D<=26?1:2,C={text:"",bytes:[],chunks:[],version:D};B.available()>=4;){var t=B.readBits(4);if(t===s.Terminator)return C;if(t===s.ECI)B.readBits(1)===0?C.chunks.push({type:l.ECI,assignmentNumber:B.readBits(7)}):B.readBits(1)===0?C.chunks.push({type:l.ECI,assignmentNumber:B.readBits(14)}):B.readBits(1)===0?C.chunks.push({type:l.ECI,assignmentNumber:B.readBits(21)}):C.chunks.push({type:l.ECI,assignmentNumber:-1});else if(t===s.Numeric){var n=c(B,e);C.text+=n.text,(i=C.bytes).push.apply(i,n.bytes),C.chunks.push({type:l.Numeric,text:n.text})}else if(t===s.Alphanumeric){var k=E(B,e);C.text+=k.text,(v=C.bytes).push.apply(v,k.bytes),C.chunks.push({type:l.Alphanumeric,text:k.text})}else if(t===s.Byte){var u=r(B,e);C.text+=u.text,(A=C.bytes).push.apply(A,u.bytes),C.chunks.push({type:l.Byte,bytes:u.bytes,text:u.text})}else if(t===s.Kanji){var f=a(B,e);C.text+=f.text,(x=C.bytes).push.apply(x,f.bytes),C.chunks.push({type:l.Kanji,bytes:f.bytes,text:f.text})}}if(B.available()===0||B.readBits(B.available())===0)return C}y.decode=F},function(G,y,g){"use strict";Object.defineProperty(y,"__esModule",{value:!0});var p=function(){function m(l){this.byteOffset=0,this.bitOffset=0,this.bytes=l}return m.prototype.readBits=function(l){if(l<1||l>32||l>this.available())throw new Error("Cannot read "+l.toString()+" bits");var s=0;if(this.bitOffset>0){var c=8-this.bitOffset,o=l>8-o<>E,l-=o,this.bitOffset+=o,this.bitOffset===8&&(this.bitOffset=0,this.byteOffset++)}if(l>0){for(;l>=8;)s=s<<8|this.bytes[this.byteOffset]&255,this.byteOffset++,l-=8;if(l>0){var E=8-l,r=255>>E<>E,this.bitOffset+=l}}return s},m.prototype.available=function(){return 8*(this.bytes.length-this.byteOffset)-this.bitOffset},m}();y.BitStream=p},function(G,y,g){"use strict";Object.defineProperty(y,"__esModule",{value:!0}),y.shiftJISTable={32:32,33:33,34:34,35:35,36:36,37:37,38:38,39:39,40:40,41:41,42:42,43:43,44:44,45:45,46:46,47:47,48:48,49:49,50:50,51:51,52:52,53:53,54:54,55:55,56:56,57:57,58:58,59:59,60:60,61:61,62:62,63:63,64:64,65:65,66:66,67:67,68:68,69:69,70:70,71:71,72:72,73:73,74:74,75:75,76:76,77:77,78:78,79:79,80:80,81:81,82:82,83:83,84:84,85:85,86:86,87:87,88:88,89:89,90:90,91:91,92:165,93:93,94:94,95:95,96:96,97:97,98:98,99:99,100:100,101:101,102:102,103:103,104:104,105:105,106:106,107:107,108:108,109:109,110:110,111:111,112:112,113:113,114:114,115:115,116:116,117:117,118:118,119:119,120:120,121:121,122:122,123:123,124:124,125:125,126:8254,33088:12288,33089:12289,33090:12290,33091:65292,33092:65294,33093:12539,33094:65306,33095:65307,33096:65311,33097:65281,33098:12443,33099:12444,33100:180,33101:65344,33102:168,33103:65342,33104:65507,33105:65343,33106:12541,33107:12542,33108:12445,33109:12446,33110:12291,33111:20189,33112:12293,33113:12294,33114:12295,33115:12540,33116:8213,33117:8208,33118:65295,33119:92,33120:12316,33121:8214,33122:65372,33123:8230,33124:8229,33125:8216,33126:8217,33127:8220,33128:8221,33129:65288,33130:65289,33131:12308,33132:12309,33133:65339,33134:65341,33135:65371,33136:65373,33137:12296,33138:12297,33139:12298,33140:12299,33141:12300,33142:12301,33143:12302,33144:12303,33145:12304,33146:12305,33147:65291,33148:8722,33149:177,33150:215,33152:247,33153:65309,33154:8800,33155:65308,33156:65310,33157:8806,33158:8807,33159:8734,33160:8756,33161:9794,33162:9792,33163:176,33164:8242,33165:8243,33166:8451,33167:65509,33168:65284,33169:162,33170:163,33171:65285,33172:65283,33173:65286,33174:65290,33175:65312,33176:167,33177:9734,33178:9733,33179:9675,33180:9679,33181:9678,33182:9671,33183:9670,33184:9633,33185:9632,33186:9651,33187:9650,33188:9661,33189:9660,33190:8251,33191:12306,33192:8594,33193:8592,33194:8593,33195:8595,33196:12307,33208:8712,33209:8715,33210:8838,33211:8839,33212:8834,33213:8835,33214:8746,33215:8745,33224:8743,33225:8744,33226:172,33227:8658,33228:8660,33229:8704,33230:8707,33242:8736,33243:8869,33244:8978,33245:8706,33246:8711,33247:8801,33248:8786,33249:8810,33250:8811,33251:8730,33252:8765,33253:8733,33254:8757,33255:8747,33256:8748,33264:8491,33265:8240,33266:9839,33267:9837,33268:9834,33269:8224,33270:8225,33271:182,33276:9711,33359:65296,33360:65297,33361:65298,33362:65299,33363:65300,33364:65301,33365:65302,33366:65303,33367:65304,33368:65305,33376:65313,33377:65314,33378:65315,33379:65316,33380:65317,33381:65318,33382:65319,33383:65320,33384:65321,33385:65322,33386:65323,33387:65324,33388:65325,33389:65326,33390:65327,33391:65328,33392:65329,33393:65330,33394:65331,33395:65332,33396:65333,33397:65334,33398:65335,33399:65336,33400:65337,33401:65338,33409:65345,33410:65346,33411:65347,33412:65348,33413:65349,33414:65350,33415:65351,33416:65352,33417:65353,33418:65354,33419:65355,33420:65356,33421:65357,33422:65358,33423:65359,33424:65360,33425:65361,33426:65362,33427:65363,33428:65364,33429:65365,33430:65366,33431:65367,33432:65368,33433:65369,33434:65370,33439:12353,33440:12354,33441:12355,33442:12356,33443:12357,33444:12358,33445:12359,33446:12360,33447:12361,33448:12362,33449:12363,33450:12364,33451:12365,33452:12366,33453:12367,33454:12368,33455:12369,33456:12370,33457:12371,33458:12372,33459:12373,33460:12374,33461:12375,33462:12376,33463:12377,33464:12378,33465:12379,33466:12380,33467:12381,33468:12382,33469:12383,33470:12384,33471:12385,33472:12386,33473:12387,33474:12388,33475:12389,33476:12390,33477:12391,33478:12392,33479:12393,33480:12394,33481:12395,33482:12396,33483:12397,33484:12398,33485:12399,33486:12400,33487:12401,33488:12402,33489:12403,33490:12404,33491:12405,33492:12406,33493:12407,33494:12408,33495:12409,33496:12410,33497:12411,33498:12412,33499:12413,33500:12414,33501:12415,33502:12416,33503:12417,33504:12418,33505:12419,33506:12420,33507:12421,33508:12422,33509:12423,33510:12424,33511:12425,33512:12426,33513:12427,33514:12428,33515:12429,33516:12430,33517:12431,33518:12432,33519:12433,33520:12434,33521:12435,33600:12449,33601:12450,33602:12451,33603:12452,33604:12453,33605:12454,33606:12455,33607:12456,33608:12457,33609:12458,33610:12459,33611:12460,33612:12461,33613:12462,33614:12463,33615:12464,33616:12465,33617:12466,33618:12467,33619:12468,33620:12469,33621:12470,33622:12471,33623:12472,33624:12473,33625:12474,33626:12475,33627:12476,33628:12477,33629:12478,33630:12479,33631:12480,33632:12481,33633:12482,33634:12483,33635:12484,33636:12485,33637:12486,33638:12487,33639:12488,33640:12489,33641:12490,33642:12491,33643:12492,33644:12493,33645:12494,33646:12495,33647:12496,33648:12497,33649:12498,33650:12499,33651:12500,33652:12501,33653:12502,33654:12503,33655:12504,33656:12505,33657:12506,33658:12507,33659:12508,33660:12509,33661:12510,33662:12511,33664:12512,33665:12513,33666:12514,33667:12515,33668:12516,33669:12517,33670:12518,33671:12519,33672:12520,33673:12521,33674:12522,33675:12523,33676:12524,33677:12525,33678:12526,33679:12527,33680:12528,33681:12529,33682:12530,33683:12531,33684:12532,33685:12533,33686:12534,33695:913,33696:914,33697:915,33698:916,33699:917,33700:918,33701:919,33702:920,33703:921,33704:922,33705:923,33706:924,33707:925,33708:926,33709:927,33710:928,33711:929,33712:931,33713:932,33714:933,33715:934,33716:935,33717:936,33718:937,33727:945,33728:946,33729:947,33730:948,33731:949,33732:950,33733:951,33734:952,33735:953,33736:954,33737:955,33738:956,33739:957,33740:958,33741:959,33742:960,33743:961,33744:963,33745:964,33746:965,33747:966,33748:967,33749:968,33750:969,33856:1040,33857:1041,33858:1042,33859:1043,33860:1044,33861:1045,33862:1025,33863:1046,33864:1047,33865:1048,33866:1049,33867:1050,33868:1051,33869:1052,33870:1053,33871:1054,33872:1055,33873:1056,33874:1057,33875:1058,33876:1059,33877:1060,33878:1061,33879:1062,33880:1063,33881:1064,33882:1065,33883:1066,33884:1067,33885:1068,33886:1069,33887:1070,33888:1071,33904:1072,33905:1073,33906:1074,33907:1075,33908:1076,33909:1077,33910:1105,33911:1078,33912:1079,33913:1080,33914:1081,33915:1082,33916:1083,33917:1084,33918:1085,33920:1086,33921:1087,33922:1088,33923:1089,33924:1090,33925:1091,33926:1092,33927:1093,33928:1094,33929:1095,33930:1096,33931:1097,33932:1098,33933:1099,33934:1100,33935:1101,33936:1102,33937:1103,33951:9472,33952:9474,33953:9484,33954:9488,33955:9496,33956:9492,33957:9500,33958:9516,33959:9508,33960:9524,33961:9532,33962:9473,33963:9475,33964:9487,33965:9491,33966:9499,33967:9495,33968:9507,33969:9523,33970:9515,33971:9531,33972:9547,33973:9504,33974:9519,33975:9512,33976:9527,33977:9535,33978:9501,33979:9520,33980:9509,33981:9528,33982:9538,34975:20124,34976:21782,34977:23043,34978:38463,34979:21696,34980:24859,34981:25384,34982:23030,34983:36898,34984:33909,34985:33564,34986:31312,34987:24746,34988:25569,34989:28197,34990:26093,34991:33894,34992:33446,34993:39925,34994:26771,34995:22311,34996:26017,34997:25201,34998:23451,34999:22992,35e3:34427,35001:39156,35002:32098,35003:32190,35004:39822,35005:25110,35006:31903,35007:34999,35008:23433,35009:24245,35010:25353,35011:26263,35012:26696,35013:38343,35014:38797,35015:26447,35016:20197,35017:20234,35018:20301,35019:20381,35020:20553,35021:22258,35022:22839,35023:22996,35024:23041,35025:23561,35026:24799,35027:24847,35028:24944,35029:26131,35030:26885,35031:28858,35032:30031,35033:30064,35034:31227,35035:32173,35036:32239,35037:32963,35038:33806,35039:34915,35040:35586,35041:36949,35042:36986,35043:21307,35044:20117,35045:20133,35046:22495,35047:32946,35048:37057,35049:30959,35050:19968,35051:22769,35052:28322,35053:36920,35054:31282,35055:33576,35056:33419,35057:39983,35058:20801,35059:21360,35060:21693,35061:21729,35062:22240,35063:23035,35064:24341,35065:39154,35066:28139,35067:32996,35068:34093,35136:38498,35137:38512,35138:38560,35139:38907,35140:21515,35141:21491,35142:23431,35143:28879,35144:32701,35145:36802,35146:38632,35147:21359,35148:40284,35149:31418,35150:19985,35151:30867,35152:33276,35153:28198,35154:22040,35155:21764,35156:27421,35157:34074,35158:39995,35159:23013,35160:21417,35161:28006,35162:29916,35163:38287,35164:22082,35165:20113,35166:36939,35167:38642,35168:33615,35169:39180,35170:21473,35171:21942,35172:23344,35173:24433,35174:26144,35175:26355,35176:26628,35177:27704,35178:27891,35179:27945,35180:29787,35181:30408,35182:31310,35183:38964,35184:33521,35185:34907,35186:35424,35187:37613,35188:28082,35189:30123,35190:30410,35191:39365,35192:24742,35193:35585,35194:36234,35195:38322,35196:27022,35197:21421,35198:20870,35200:22290,35201:22576,35202:22852,35203:23476,35204:24310,35205:24616,35206:25513,35207:25588,35208:27839,35209:28436,35210:28814,35211:28948,35212:29017,35213:29141,35214:29503,35215:32257,35216:33398,35217:33489,35218:34199,35219:36960,35220:37467,35221:40219,35222:22633,35223:26044,35224:27738,35225:29989,35226:20985,35227:22830,35228:22885,35229:24448,35230:24540,35231:25276,35232:26106,35233:27178,35234:27431,35235:27572,35236:29579,35237:32705,35238:35158,35239:40236,35240:40206,35241:40644,35242:23713,35243:27798,35244:33659,35245:20740,35246:23627,35247:25014,35248:33222,35249:26742,35250:29281,35251:20057,35252:20474,35253:21368,35254:24681,35255:28201,35256:31311,35257:38899,35258:19979,35259:21270,35260:20206,35261:20309,35262:20285,35263:20385,35264:20339,35265:21152,35266:21487,35267:22025,35268:22799,35269:23233,35270:23478,35271:23521,35272:31185,35273:26247,35274:26524,35275:26550,35276:27468,35277:27827,35278:28779,35279:29634,35280:31117,35281:31166,35282:31292,35283:31623,35284:33457,35285:33499,35286:33540,35287:33655,35288:33775,35289:33747,35290:34662,35291:35506,35292:22057,35293:36008,35294:36838,35295:36942,35296:38686,35297:34442,35298:20420,35299:23784,35300:25105,35301:29273,35302:30011,35303:33253,35304:33469,35305:34558,35306:36032,35307:38597,35308:39187,35309:39381,35310:20171,35311:20250,35312:35299,35313:22238,35314:22602,35315:22730,35316:24315,35317:24555,35318:24618,35319:24724,35320:24674,35321:25040,35322:25106,35323:25296,35324:25913,35392:39745,35393:26214,35394:26800,35395:28023,35396:28784,35397:30028,35398:30342,35399:32117,35400:33445,35401:34809,35402:38283,35403:38542,35404:35997,35405:20977,35406:21182,35407:22806,35408:21683,35409:23475,35410:23830,35411:24936,35412:27010,35413:28079,35414:30861,35415:33995,35416:34903,35417:35442,35418:37799,35419:39608,35420:28012,35421:39336,35422:34521,35423:22435,35424:26623,35425:34510,35426:37390,35427:21123,35428:22151,35429:21508,35430:24275,35431:25313,35432:25785,35433:26684,35434:26680,35435:27579,35436:29554,35437:30906,35438:31339,35439:35226,35440:35282,35441:36203,35442:36611,35443:37101,35444:38307,35445:38548,35446:38761,35447:23398,35448:23731,35449:27005,35450:38989,35451:38990,35452:25499,35453:31520,35454:27179,35456:27263,35457:26806,35458:39949,35459:28511,35460:21106,35461:21917,35462:24688,35463:25324,35464:27963,35465:28167,35466:28369,35467:33883,35468:35088,35469:36676,35470:19988,35471:39993,35472:21494,35473:26907,35474:27194,35475:38788,35476:26666,35477:20828,35478:31427,35479:33970,35480:37340,35481:37772,35482:22107,35483:40232,35484:26658,35485:33541,35486:33841,35487:31909,35488:21e3,35489:33477,35490:29926,35491:20094,35492:20355,35493:20896,35494:23506,35495:21002,35496:21208,35497:21223,35498:24059,35499:21914,35500:22570,35501:23014,35502:23436,35503:23448,35504:23515,35505:24178,35506:24185,35507:24739,35508:24863,35509:24931,35510:25022,35511:25563,35512:25954,35513:26577,35514:26707,35515:26874,35516:27454,35517:27475,35518:27735,35519:28450,35520:28567,35521:28485,35522:29872,35523:29976,35524:30435,35525:30475,35526:31487,35527:31649,35528:31777,35529:32233,35530:32566,35531:32752,35532:32925,35533:33382,35534:33694,35535:35251,35536:35532,35537:36011,35538:36996,35539:37969,35540:38291,35541:38289,35542:38306,35543:38501,35544:38867,35545:39208,35546:33304,35547:20024,35548:21547,35549:23736,35550:24012,35551:29609,35552:30284,35553:30524,35554:23721,35555:32747,35556:36107,35557:38593,35558:38929,35559:38996,35560:39e3,35561:20225,35562:20238,35563:21361,35564:21916,35565:22120,35566:22522,35567:22855,35568:23305,35569:23492,35570:23696,35571:24076,35572:24190,35573:24524,35574:25582,35575:26426,35576:26071,35577:26082,35578:26399,35579:26827,35580:26820,35648:27231,35649:24112,35650:27589,35651:27671,35652:27773,35653:30079,35654:31048,35655:23395,35656:31232,35657:32e3,35658:24509,35659:35215,35660:35352,35661:36020,35662:36215,35663:36556,35664:36637,35665:39138,35666:39438,35667:39740,35668:20096,35669:20605,35670:20736,35671:22931,35672:23452,35673:25135,35674:25216,35675:25836,35676:27450,35677:29344,35678:30097,35679:31047,35680:32681,35681:34811,35682:35516,35683:35696,35684:25516,35685:33738,35686:38816,35687:21513,35688:21507,35689:21931,35690:26708,35691:27224,35692:35440,35693:30759,35694:26485,35695:40653,35696:21364,35697:23458,35698:33050,35699:34384,35700:36870,35701:19992,35702:20037,35703:20167,35704:20241,35705:21450,35706:21560,35707:23470,35708:24339,35709:24613,35710:25937,35712:26429,35713:27714,35714:27762,35715:27875,35716:28792,35717:29699,35718:31350,35719:31406,35720:31496,35721:32026,35722:31998,35723:32102,35724:26087,35725:29275,35726:21435,35727:23621,35728:24040,35729:25298,35730:25312,35731:25369,35732:28192,35733:34394,35734:35377,35735:36317,35736:37624,35737:28417,35738:31142,35739:39770,35740:20136,35741:20139,35742:20140,35743:20379,35744:20384,35745:20689,35746:20807,35747:31478,35748:20849,35749:20982,35750:21332,35751:21281,35752:21375,35753:21483,35754:21932,35755:22659,35756:23777,35757:24375,35758:24394,35759:24623,35760:24656,35761:24685,35762:25375,35763:25945,35764:27211,35765:27841,35766:29378,35767:29421,35768:30703,35769:33016,35770:33029,35771:33288,35772:34126,35773:37111,35774:37857,35775:38911,35776:39255,35777:39514,35778:20208,35779:20957,35780:23597,35781:26241,35782:26989,35783:23616,35784:26354,35785:26997,35786:29577,35787:26704,35788:31873,35789:20677,35790:21220,35791:22343,35792:24062,35793:37670,35794:26020,35795:27427,35796:27453,35797:29748,35798:31105,35799:31165,35800:31563,35801:32202,35802:33465,35803:33740,35804:34943,35805:35167,35806:35641,35807:36817,35808:37329,35809:21535,35810:37504,35811:20061,35812:20534,35813:21477,35814:21306,35815:29399,35816:29590,35817:30697,35818:33510,35819:36527,35820:39366,35821:39368,35822:39378,35823:20855,35824:24858,35825:34398,35826:21936,35827:31354,35828:20598,35829:23507,35830:36935,35831:38533,35832:20018,35833:27355,35834:37351,35835:23633,35836:23624,35904:25496,35905:31391,35906:27795,35907:38772,35908:36705,35909:31402,35910:29066,35911:38536,35912:31874,35913:26647,35914:32368,35915:26705,35916:37740,35917:21234,35918:21531,35919:34219,35920:35347,35921:32676,35922:36557,35923:37089,35924:21350,35925:34952,35926:31041,35927:20418,35928:20670,35929:21009,35930:20804,35931:21843,35932:22317,35933:29674,35934:22411,35935:22865,35936:24418,35937:24452,35938:24693,35939:24950,35940:24935,35941:25001,35942:25522,35943:25658,35944:25964,35945:26223,35946:26690,35947:28179,35948:30054,35949:31293,35950:31995,35951:32076,35952:32153,35953:32331,35954:32619,35955:33550,35956:33610,35957:34509,35958:35336,35959:35427,35960:35686,35961:36605,35962:38938,35963:40335,35964:33464,35965:36814,35966:39912,35968:21127,35969:25119,35970:25731,35971:28608,35972:38553,35973:26689,35974:20625,35975:27424,35976:27770,35977:28500,35978:31348,35979:32080,35980:34880,35981:35363,35982:26376,35983:20214,35984:20537,35985:20518,35986:20581,35987:20860,35988:21048,35989:21091,35990:21927,35991:22287,35992:22533,35993:23244,35994:24314,35995:25010,35996:25080,35997:25331,35998:25458,35999:26908,36e3:27177,36001:29309,36002:29356,36003:29486,36004:30740,36005:30831,36006:32121,36007:30476,36008:32937,36009:35211,36010:35609,36011:36066,36012:36562,36013:36963,36014:37749,36015:38522,36016:38997,36017:39443,36018:40568,36019:20803,36020:21407,36021:21427,36022:24187,36023:24358,36024:28187,36025:28304,36026:29572,36027:29694,36028:32067,36029:33335,36030:35328,36031:35578,36032:38480,36033:20046,36034:20491,36035:21476,36036:21628,36037:22266,36038:22993,36039:23396,36040:24049,36041:24235,36042:24359,36043:25144,36044:25925,36045:26543,36046:28246,36047:29392,36048:31946,36049:34996,36050:32929,36051:32993,36052:33776,36053:34382,36054:35463,36055:36328,36056:37431,36057:38599,36058:39015,36059:40723,36060:20116,36061:20114,36062:20237,36063:21320,36064:21577,36065:21566,36066:23087,36067:24460,36068:24481,36069:24735,36070:26791,36071:27278,36072:29786,36073:30849,36074:35486,36075:35492,36076:35703,36077:37264,36078:20062,36079:39881,36080:20132,36081:20348,36082:20399,36083:20505,36084:20502,36085:20809,36086:20844,36087:21151,36088:21177,36089:21246,36090:21402,36091:21475,36092:21521,36160:21518,36161:21897,36162:22353,36163:22434,36164:22909,36165:23380,36166:23389,36167:23439,36168:24037,36169:24039,36170:24055,36171:24184,36172:24195,36173:24218,36174:24247,36175:24344,36176:24658,36177:24908,36178:25239,36179:25304,36180:25511,36181:25915,36182:26114,36183:26179,36184:26356,36185:26477,36186:26657,36187:26775,36188:27083,36189:27743,36190:27946,36191:28009,36192:28207,36193:28317,36194:30002,36195:30343,36196:30828,36197:31295,36198:31968,36199:32005,36200:32024,36201:32094,36202:32177,36203:32789,36204:32771,36205:32943,36206:32945,36207:33108,36208:33167,36209:33322,36210:33618,36211:34892,36212:34913,36213:35611,36214:36002,36215:36092,36216:37066,36217:37237,36218:37489,36219:30783,36220:37628,36221:38308,36222:38477,36224:38917,36225:39321,36226:39640,36227:40251,36228:21083,36229:21163,36230:21495,36231:21512,36232:22741,36233:25335,36234:28640,36235:35946,36236:36703,36237:40633,36238:20811,36239:21051,36240:21578,36241:22269,36242:31296,36243:37239,36244:40288,36245:40658,36246:29508,36247:28425,36248:33136,36249:29969,36250:24573,36251:24794,36252:39592,36253:29403,36254:36796,36255:27492,36256:38915,36257:20170,36258:22256,36259:22372,36260:22718,36261:23130,36262:24680,36263:25031,36264:26127,36265:26118,36266:26681,36267:26801,36268:28151,36269:30165,36270:32058,36271:33390,36272:39746,36273:20123,36274:20304,36275:21449,36276:21766,36277:23919,36278:24038,36279:24046,36280:26619,36281:27801,36282:29811,36283:30722,36284:35408,36285:37782,36286:35039,36287:22352,36288:24231,36289:25387,36290:20661,36291:20652,36292:20877,36293:26368,36294:21705,36295:22622,36296:22971,36297:23472,36298:24425,36299:25165,36300:25505,36301:26685,36302:27507,36303:28168,36304:28797,36305:37319,36306:29312,36307:30741,36308:30758,36309:31085,36310:25998,36311:32048,36312:33756,36313:35009,36314:36617,36315:38555,36316:21092,36317:22312,36318:26448,36319:32618,36320:36001,36321:20916,36322:22338,36323:38442,36324:22586,36325:27018,36326:32948,36327:21682,36328:23822,36329:22524,36330:30869,36331:40442,36332:20316,36333:21066,36334:21643,36335:25662,36336:26152,36337:26388,36338:26613,36339:31364,36340:31574,36341:32034,36342:37679,36343:26716,36344:39853,36345:31545,36346:21273,36347:20874,36348:21047,36416:23519,36417:25334,36418:25774,36419:25830,36420:26413,36421:27578,36422:34217,36423:38609,36424:30352,36425:39894,36426:25420,36427:37638,36428:39851,36429:30399,36430:26194,36431:19977,36432:20632,36433:21442,36434:23665,36435:24808,36436:25746,36437:25955,36438:26719,36439:29158,36440:29642,36441:29987,36442:31639,36443:32386,36444:34453,36445:35715,36446:36059,36447:37240,36448:39184,36449:26028,36450:26283,36451:27531,36452:20181,36453:20180,36454:20282,36455:20351,36456:21050,36457:21496,36458:21490,36459:21987,36460:22235,36461:22763,36462:22987,36463:22985,36464:23039,36465:23376,36466:23629,36467:24066,36468:24107,36469:24535,36470:24605,36471:25351,36472:25903,36473:23388,36474:26031,36475:26045,36476:26088,36477:26525,36478:27490,36480:27515,36481:27663,36482:29509,36483:31049,36484:31169,36485:31992,36486:32025,36487:32043,36488:32930,36489:33026,36490:33267,36491:35222,36492:35422,36493:35433,36494:35430,36495:35468,36496:35566,36497:36039,36498:36060,36499:38604,36500:39164,36501:27503,36502:20107,36503:20284,36504:20365,36505:20816,36506:23383,36507:23546,36508:24904,36509:25345,36510:26178,36511:27425,36512:28363,36513:27835,36514:29246,36515:29885,36516:30164,36517:30913,36518:31034,36519:32780,36520:32819,36521:33258,36522:33940,36523:36766,36524:27728,36525:40575,36526:24335,36527:35672,36528:40235,36529:31482,36530:36600,36531:23437,36532:38635,36533:19971,36534:21489,36535:22519,36536:22833,36537:23241,36538:23460,36539:24713,36540:28287,36541:28422,36542:30142,36543:36074,36544:23455,36545:34048,36546:31712,36547:20594,36548:26612,36549:33437,36550:23649,36551:34122,36552:32286,36553:33294,36554:20889,36555:23556,36556:25448,36557:36198,36558:26012,36559:29038,36560:31038,36561:32023,36562:32773,36563:35613,36564:36554,36565:36974,36566:34503,36567:37034,36568:20511,36569:21242,36570:23610,36571:26451,36572:28796,36573:29237,36574:37196,36575:37320,36576:37675,36577:33509,36578:23490,36579:24369,36580:24825,36581:20027,36582:21462,36583:23432,36584:25163,36585:26417,36586:27530,36587:29417,36588:29664,36589:31278,36590:33131,36591:36259,36592:37202,36593:39318,36594:20754,36595:21463,36596:21610,36597:23551,36598:25480,36599:27193,36600:32172,36601:38656,36602:22234,36603:21454,36604:21608,36672:23447,36673:23601,36674:24030,36675:20462,36676:24833,36677:25342,36678:27954,36679:31168,36680:31179,36681:32066,36682:32333,36683:32722,36684:33261,36685:33311,36686:33936,36687:34886,36688:35186,36689:35728,36690:36468,36691:36655,36692:36913,36693:37195,36694:37228,36695:38598,36696:37276,36697:20160,36698:20303,36699:20805,36700:21313,36701:24467,36702:25102,36703:26580,36704:27713,36705:28171,36706:29539,36707:32294,36708:37325,36709:37507,36710:21460,36711:22809,36712:23487,36713:28113,36714:31069,36715:32302,36716:31899,36717:22654,36718:29087,36719:20986,36720:34899,36721:36848,36722:20426,36723:23803,36724:26149,36725:30636,36726:31459,36727:33308,36728:39423,36729:20934,36730:24490,36731:26092,36732:26991,36733:27529,36734:28147,36736:28310,36737:28516,36738:30462,36739:32020,36740:24033,36741:36981,36742:37255,36743:38918,36744:20966,36745:21021,36746:25152,36747:26257,36748:26329,36749:28186,36750:24246,36751:32210,36752:32626,36753:26360,36754:34223,36755:34295,36756:35576,36757:21161,36758:21465,36759:22899,36760:24207,36761:24464,36762:24661,36763:37604,36764:38500,36765:20663,36766:20767,36767:21213,36768:21280,36769:21319,36770:21484,36771:21736,36772:21830,36773:21809,36774:22039,36775:22888,36776:22974,36777:23100,36778:23477,36779:23558,36780:23567,36781:23569,36782:23578,36783:24196,36784:24202,36785:24288,36786:24432,36787:25215,36788:25220,36789:25307,36790:25484,36791:25463,36792:26119,36793:26124,36794:26157,36795:26230,36796:26494,36797:26786,36798:27167,36799:27189,36800:27836,36801:28040,36802:28169,36803:28248,36804:28988,36805:28966,36806:29031,36807:30151,36808:30465,36809:30813,36810:30977,36811:31077,36812:31216,36813:31456,36814:31505,36815:31911,36816:32057,36817:32918,36818:33750,36819:33931,36820:34121,36821:34909,36822:35059,36823:35359,36824:35388,36825:35412,36826:35443,36827:35937,36828:36062,36829:37284,36830:37478,36831:37758,36832:37912,36833:38556,36834:38808,36835:19978,36836:19976,36837:19998,36838:20055,36839:20887,36840:21104,36841:22478,36842:22580,36843:22732,36844:23330,36845:24120,36846:24773,36847:25854,36848:26465,36849:26454,36850:27972,36851:29366,36852:30067,36853:31331,36854:33976,36855:35698,36856:37304,36857:37664,36858:22065,36859:22516,36860:39166,36928:25325,36929:26893,36930:27542,36931:29165,36932:32340,36933:32887,36934:33394,36935:35302,36936:39135,36937:34645,36938:36785,36939:23611,36940:20280,36941:20449,36942:20405,36943:21767,36944:23072,36945:23517,36946:23529,36947:24515,36948:24910,36949:25391,36950:26032,36951:26187,36952:26862,36953:27035,36954:28024,36955:28145,36956:30003,36957:30137,36958:30495,36959:31070,36960:31206,36961:32051,36962:33251,36963:33455,36964:34218,36965:35242,36966:35386,36967:36523,36968:36763,36969:36914,36970:37341,36971:38663,36972:20154,36973:20161,36974:20995,36975:22645,36976:22764,36977:23563,36978:29978,36979:23613,36980:33102,36981:35338,36982:36805,36983:38499,36984:38765,36985:31525,36986:35535,36987:38920,36988:37218,36989:22259,36990:21416,36992:36887,36993:21561,36994:22402,36995:24101,36996:25512,36997:27700,36998:28810,36999:30561,37e3:31883,37001:32736,37002:34928,37003:36930,37004:37204,37005:37648,37006:37656,37007:38543,37008:29790,37009:39620,37010:23815,37011:23913,37012:25968,37013:26530,37014:36264,37015:38619,37016:25454,37017:26441,37018:26905,37019:33733,37020:38935,37021:38592,37022:35070,37023:28548,37024:25722,37025:23544,37026:19990,37027:28716,37028:30045,37029:26159,37030:20932,37031:21046,37032:21218,37033:22995,37034:24449,37035:24615,37036:25104,37037:25919,37038:25972,37039:26143,37040:26228,37041:26866,37042:26646,37043:27491,37044:28165,37045:29298,37046:29983,37047:30427,37048:31934,37049:32854,37050:22768,37051:35069,37052:35199,37053:35488,37054:35475,37055:35531,37056:36893,37057:37266,37058:38738,37059:38745,37060:25993,37061:31246,37062:33030,37063:38587,37064:24109,37065:24796,37066:25114,37067:26021,37068:26132,37069:26512,37070:30707,37071:31309,37072:31821,37073:32318,37074:33034,37075:36012,37076:36196,37077:36321,37078:36447,37079:30889,37080:20999,37081:25305,37082:25509,37083:25666,37084:25240,37085:35373,37086:31363,37087:31680,37088:35500,37089:38634,37090:32118,37091:33292,37092:34633,37093:20185,37094:20808,37095:21315,37096:21344,37097:23459,37098:23554,37099:23574,37100:24029,37101:25126,37102:25159,37103:25776,37104:26643,37105:26676,37106:27849,37107:27973,37108:27927,37109:26579,37110:28508,37111:29006,37112:29053,37113:26059,37114:31359,37115:31661,37116:32218,37184:32330,37185:32680,37186:33146,37187:33307,37188:33337,37189:34214,37190:35438,37191:36046,37192:36341,37193:36984,37194:36983,37195:37549,37196:37521,37197:38275,37198:39854,37199:21069,37200:21892,37201:28472,37202:28982,37203:20840,37204:31109,37205:32341,37206:33203,37207:31950,37208:22092,37209:22609,37210:23720,37211:25514,37212:26366,37213:26365,37214:26970,37215:29401,37216:30095,37217:30094,37218:30990,37219:31062,37220:31199,37221:31895,37222:32032,37223:32068,37224:34311,37225:35380,37226:38459,37227:36961,37228:40736,37229:20711,37230:21109,37231:21452,37232:21474,37233:20489,37234:21930,37235:22766,37236:22863,37237:29245,37238:23435,37239:23652,37240:21277,37241:24803,37242:24819,37243:25436,37244:25475,37245:25407,37246:25531,37248:25805,37249:26089,37250:26361,37251:24035,37252:27085,37253:27133,37254:28437,37255:29157,37256:20105,37257:30185,37258:30456,37259:31379,37260:31967,37261:32207,37262:32156,37263:32865,37264:33609,37265:33624,37266:33900,37267:33980,37268:34299,37269:35013,37270:36208,37271:36865,37272:36973,37273:37783,37274:38684,37275:39442,37276:20687,37277:22679,37278:24974,37279:33235,37280:34101,37281:36104,37282:36896,37283:20419,37284:20596,37285:21063,37286:21363,37287:24687,37288:25417,37289:26463,37290:28204,37291:36275,37292:36895,37293:20439,37294:23646,37295:36042,37296:26063,37297:32154,37298:21330,37299:34966,37300:20854,37301:25539,37302:23384,37303:23403,37304:23562,37305:25613,37306:26449,37307:36956,37308:20182,37309:22810,37310:22826,37311:27760,37312:35409,37313:21822,37314:22549,37315:22949,37316:24816,37317:25171,37318:26561,37319:33333,37320:26965,37321:38464,37322:39364,37323:39464,37324:20307,37325:22534,37326:23550,37327:32784,37328:23729,37329:24111,37330:24453,37331:24608,37332:24907,37333:25140,37334:26367,37335:27888,37336:28382,37337:32974,37338:33151,37339:33492,37340:34955,37341:36024,37342:36864,37343:36910,37344:38538,37345:40667,37346:39899,37347:20195,37348:21488,37349:22823,37350:31532,37351:37261,37352:38988,37353:40441,37354:28381,37355:28711,37356:21331,37357:21828,37358:23429,37359:25176,37360:25246,37361:25299,37362:27810,37363:28655,37364:29730,37365:35351,37366:37944,37367:28609,37368:35582,37369:33592,37370:20967,37371:34552,37372:21482,37440:21481,37441:20294,37442:36948,37443:36784,37444:22890,37445:33073,37446:24061,37447:31466,37448:36799,37449:26842,37450:35895,37451:29432,37452:40008,37453:27197,37454:35504,37455:20025,37456:21336,37457:22022,37458:22374,37459:25285,37460:25506,37461:26086,37462:27470,37463:28129,37464:28251,37465:28845,37466:30701,37467:31471,37468:31658,37469:32187,37470:32829,37471:32966,37472:34507,37473:35477,37474:37723,37475:22243,37476:22727,37477:24382,37478:26029,37479:26262,37480:27264,37481:27573,37482:30007,37483:35527,37484:20516,37485:30693,37486:22320,37487:24347,37488:24677,37489:26234,37490:27744,37491:30196,37492:31258,37493:32622,37494:33268,37495:34584,37496:36933,37497:39347,37498:31689,37499:30044,37500:31481,37501:31569,37502:33988,37504:36880,37505:31209,37506:31378,37507:33590,37508:23265,37509:30528,37510:20013,37511:20210,37512:23449,37513:24544,37514:25277,37515:26172,37516:26609,37517:27880,37518:34411,37519:34935,37520:35387,37521:37198,37522:37619,37523:39376,37524:27159,37525:28710,37526:29482,37527:33511,37528:33879,37529:36015,37530:19969,37531:20806,37532:20939,37533:21899,37534:23541,37535:24086,37536:24115,37537:24193,37538:24340,37539:24373,37540:24427,37541:24500,37542:25074,37543:25361,37544:26274,37545:26397,37546:28526,37547:29266,37548:30010,37549:30522,37550:32884,37551:33081,37552:33144,37553:34678,37554:35519,37555:35548,37556:36229,37557:36339,37558:37530,37559:38263,37560:38914,37561:40165,37562:21189,37563:25431,37564:30452,37565:26389,37566:27784,37567:29645,37568:36035,37569:37806,37570:38515,37571:27941,37572:22684,37573:26894,37574:27084,37575:36861,37576:37786,37577:30171,37578:36890,37579:22618,37580:26626,37581:25524,37582:27131,37583:20291,37584:28460,37585:26584,37586:36795,37587:34086,37588:32180,37589:37716,37590:26943,37591:28528,37592:22378,37593:22775,37594:23340,37595:32044,37596:29226,37597:21514,37598:37347,37599:40372,37600:20141,37601:20302,37602:20572,37603:20597,37604:21059,37605:35998,37606:21576,37607:22564,37608:23450,37609:24093,37610:24213,37611:24237,37612:24311,37613:24351,37614:24716,37615:25269,37616:25402,37617:25552,37618:26799,37619:27712,37620:30855,37621:31118,37622:31243,37623:32224,37624:33351,37625:35330,37626:35558,37627:36420,37628:36883,37696:37048,37697:37165,37698:37336,37699:40718,37700:27877,37701:25688,37702:25826,37703:25973,37704:28404,37705:30340,37706:31515,37707:36969,37708:37841,37709:28346,37710:21746,37711:24505,37712:25764,37713:36685,37714:36845,37715:37444,37716:20856,37717:22635,37718:22825,37719:23637,37720:24215,37721:28155,37722:32399,37723:29980,37724:36028,37725:36578,37726:39003,37727:28857,37728:20253,37729:27583,37730:28593,37731:3e4,37732:38651,37733:20814,37734:21520,37735:22581,37736:22615,37737:22956,37738:23648,37739:24466,37740:26007,37741:26460,37742:28193,37743:30331,37744:33759,37745:36077,37746:36884,37747:37117,37748:37709,37749:30757,37750:30778,37751:21162,37752:24230,37753:22303,37754:22900,37755:24594,37756:20498,37757:20826,37758:20908,37760:20941,37761:20992,37762:21776,37763:22612,37764:22616,37765:22871,37766:23445,37767:23798,37768:23947,37769:24764,37770:25237,37771:25645,37772:26481,37773:26691,37774:26812,37775:26847,37776:30423,37777:28120,37778:28271,37779:28059,37780:28783,37781:29128,37782:24403,37783:30168,37784:31095,37785:31561,37786:31572,37787:31570,37788:31958,37789:32113,37790:21040,37791:33891,37792:34153,37793:34276,37794:35342,37795:35588,37796:35910,37797:36367,37798:36867,37799:36879,37800:37913,37801:38518,37802:38957,37803:39472,37804:38360,37805:20685,37806:21205,37807:21516,37808:22530,37809:23566,37810:24999,37811:25758,37812:27934,37813:30643,37814:31461,37815:33012,37816:33796,37817:36947,37818:37509,37819:23776,37820:40199,37821:21311,37822:24471,37823:24499,37824:28060,37825:29305,37826:30563,37827:31167,37828:31716,37829:27602,37830:29420,37831:35501,37832:26627,37833:27233,37834:20984,37835:31361,37836:26932,37837:23626,37838:40182,37839:33515,37840:23493,37841:37193,37842:28702,37843:22136,37844:23663,37845:24775,37846:25958,37847:27788,37848:35930,37849:36929,37850:38931,37851:21585,37852:26311,37853:37389,37854:22856,37855:37027,37856:20869,37857:20045,37858:20970,37859:34201,37860:35598,37861:28760,37862:25466,37863:37707,37864:26978,37865:39348,37866:32260,37867:30071,37868:21335,37869:26976,37870:36575,37871:38627,37872:27741,37873:20108,37874:23612,37875:24336,37876:36841,37877:21250,37878:36049,37879:32905,37880:34425,37881:24319,37882:26085,37883:20083,37884:20837,37952:22914,37953:23615,37954:38894,37955:20219,37956:22922,37957:24525,37958:35469,37959:28641,37960:31152,37961:31074,37962:23527,37963:33905,37964:29483,37965:29105,37966:24180,37967:24565,37968:25467,37969:25754,37970:29123,37971:31896,37972:20035,37973:24316,37974:20043,37975:22492,37976:22178,37977:24745,37978:28611,37979:32013,37980:33021,37981:33075,37982:33215,37983:36786,37984:35223,37985:34468,37986:24052,37987:25226,37988:25773,37989:35207,37990:26487,37991:27874,37992:27966,37993:29750,37994:30772,37995:23110,37996:32629,37997:33453,37998:39340,37999:20467,38e3:24259,38001:25309,38002:25490,38003:25943,38004:26479,38005:30403,38006:29260,38007:32972,38008:32954,38009:36649,38010:37197,38011:20493,38012:22521,38013:23186,38014:26757,38016:26995,38017:29028,38018:29437,38019:36023,38020:22770,38021:36064,38022:38506,38023:36889,38024:34687,38025:31204,38026:30695,38027:33833,38028:20271,38029:21093,38030:21338,38031:25293,38032:26575,38033:27850,38034:30333,38035:31636,38036:31893,38037:33334,38038:34180,38039:36843,38040:26333,38041:28448,38042:29190,38043:32283,38044:33707,38045:39361,38046:40614,38047:20989,38048:31665,38049:30834,38050:31672,38051:32903,38052:31560,38053:27368,38054:24161,38055:32908,38056:30033,38057:30048,38058:20843,38059:37474,38060:28300,38061:30330,38062:37271,38063:39658,38064:20240,38065:32624,38066:25244,38067:31567,38068:38309,38069:40169,38070:22138,38071:22617,38072:34532,38073:38588,38074:20276,38075:21028,38076:21322,38077:21453,38078:21467,38079:24070,38080:25644,38081:26001,38082:26495,38083:27710,38084:27726,38085:29256,38086:29359,38087:29677,38088:30036,38089:32321,38090:33324,38091:34281,38092:36009,38093:31684,38094:37318,38095:29033,38096:38930,38097:39151,38098:25405,38099:26217,38100:30058,38101:30436,38102:30928,38103:34115,38104:34542,38105:21290,38106:21329,38107:21542,38108:22915,38109:24199,38110:24444,38111:24754,38112:25161,38113:25209,38114:25259,38115:26e3,38116:27604,38117:27852,38118:30130,38119:30382,38120:30865,38121:31192,38122:32203,38123:32631,38124:32933,38125:34987,38126:35513,38127:36027,38128:36991,38129:38750,38130:39131,38131:27147,38132:31800,38133:20633,38134:23614,38135:24494,38136:26503,38137:27608,38138:29749,38139:30473,38140:32654,38208:40763,38209:26570,38210:31255,38211:21305,38212:30091,38213:39661,38214:24422,38215:33181,38216:33777,38217:32920,38218:24380,38219:24517,38220:30050,38221:31558,38222:36924,38223:26727,38224:23019,38225:23195,38226:32016,38227:30334,38228:35628,38229:20469,38230:24426,38231:27161,38232:27703,38233:28418,38234:29922,38235:31080,38236:34920,38237:35413,38238:35961,38239:24287,38240:25551,38241:30149,38242:31186,38243:33495,38244:37672,38245:37618,38246:33948,38247:34541,38248:39981,38249:21697,38250:24428,38251:25996,38252:27996,38253:28693,38254:36007,38255:36051,38256:38971,38257:25935,38258:29942,38259:19981,38260:20184,38261:22496,38262:22827,38263:23142,38264:23500,38265:20904,38266:24067,38267:24220,38268:24598,38269:25206,38270:25975,38272:26023,38273:26222,38274:28014,38275:29238,38276:31526,38277:33104,38278:33178,38279:33433,38280:35676,38281:36e3,38282:36070,38283:36212,38284:38428,38285:38468,38286:20398,38287:25771,38288:27494,38289:33310,38290:33889,38291:34154,38292:37096,38293:23553,38294:26963,38295:39080,38296:33914,38297:34135,38298:20239,38299:21103,38300:24489,38301:24133,38302:26381,38303:31119,38304:33145,38305:35079,38306:35206,38307:28149,38308:24343,38309:25173,38310:27832,38311:20175,38312:29289,38313:39826,38314:20998,38315:21563,38316:22132,38317:22707,38318:24996,38319:25198,38320:28954,38321:22894,38322:31881,38323:31966,38324:32027,38325:38640,38326:25991,38327:32862,38328:19993,38329:20341,38330:20853,38331:22592,38332:24163,38333:24179,38334:24330,38335:26564,38336:20006,38337:34109,38338:38281,38339:38491,38340:31859,38341:38913,38342:20731,38343:22721,38344:30294,38345:30887,38346:21029,38347:30629,38348:34065,38349:31622,38350:20559,38351:22793,38352:29255,38353:31687,38354:32232,38355:36794,38356:36820,38357:36941,38358:20415,38359:21193,38360:23081,38361:24321,38362:38829,38363:20445,38364:33303,38365:37610,38366:22275,38367:25429,38368:27497,38369:29995,38370:35036,38371:36628,38372:31298,38373:21215,38374:22675,38375:24917,38376:25098,38377:26286,38378:27597,38379:31807,38380:33769,38381:20515,38382:20472,38383:21253,38384:21574,38385:22577,38386:22857,38387:23453,38388:23792,38389:23791,38390:23849,38391:24214,38392:25265,38393:25447,38394:25918,38395:26041,38396:26379,38464:27861,38465:27873,38466:28921,38467:30770,38468:32299,38469:32990,38470:33459,38471:33804,38472:34028,38473:34562,38474:35090,38475:35370,38476:35914,38477:37030,38478:37586,38479:39165,38480:40179,38481:40300,38482:20047,38483:20129,38484:20621,38485:21078,38486:22346,38487:22952,38488:24125,38489:24536,38490:24537,38491:25151,38492:26292,38493:26395,38494:26576,38495:26834,38496:20882,38497:32033,38498:32938,38499:33192,38500:35584,38501:35980,38502:36031,38503:37502,38504:38450,38505:21536,38506:38956,38507:21271,38508:20693,38509:21340,38510:22696,38511:25778,38512:26420,38513:29287,38514:30566,38515:31302,38516:37350,38517:21187,38518:27809,38519:27526,38520:22528,38521:24140,38522:22868,38523:26412,38524:32763,38525:20961,38526:30406,38528:25705,38529:30952,38530:39764,38531:40635,38532:22475,38533:22969,38534:26151,38535:26522,38536:27598,38537:21737,38538:27097,38539:24149,38540:33180,38541:26517,38542:39850,38543:26622,38544:40018,38545:26717,38546:20134,38547:20451,38548:21448,38549:25273,38550:26411,38551:27819,38552:36804,38553:20397,38554:32365,38555:40639,38556:19975,38557:24930,38558:28288,38559:28459,38560:34067,38561:21619,38562:26410,38563:39749,38564:24051,38565:31637,38566:23724,38567:23494,38568:34588,38569:28234,38570:34001,38571:31252,38572:33032,38573:22937,38574:31885,38575:27665,38576:30496,38577:21209,38578:22818,38579:28961,38580:29279,38581:30683,38582:38695,38583:40289,38584:26891,38585:23167,38586:23064,38587:20901,38588:21517,38589:21629,38590:26126,38591:30431,38592:36855,38593:37528,38594:40180,38595:23018,38596:29277,38597:28357,38598:20813,38599:26825,38600:32191,38601:32236,38602:38754,38603:40634,38604:25720,38605:27169,38606:33538,38607:22916,38608:23391,38609:27611,38610:29467,38611:30450,38612:32178,38613:32791,38614:33945,38615:20786,38616:26408,38617:40665,38618:30446,38619:26466,38620:21247,38621:39173,38622:23588,38623:25147,38624:31870,38625:36016,38626:21839,38627:24758,38628:32011,38629:38272,38630:21249,38631:20063,38632:20918,38633:22812,38634:29242,38635:32822,38636:37326,38637:24357,38638:30690,38639:21380,38640:24441,38641:32004,38642:34220,38643:35379,38644:36493,38645:38742,38646:26611,38647:34222,38648:37971,38649:24841,38650:24840,38651:27833,38652:30290,38720:35565,38721:36664,38722:21807,38723:20305,38724:20778,38725:21191,38726:21451,38727:23461,38728:24189,38729:24736,38730:24962,38731:25558,38732:26377,38733:26586,38734:28263,38735:28044,38736:29494,38737:29495,38738:30001,38739:31056,38740:35029,38741:35480,38742:36938,38743:37009,38744:37109,38745:38596,38746:34701,38747:22805,38748:20104,38749:20313,38750:19982,38751:35465,38752:36671,38753:38928,38754:20653,38755:24188,38756:22934,38757:23481,38758:24248,38759:25562,38760:25594,38761:25793,38762:26332,38763:26954,38764:27096,38765:27915,38766:28342,38767:29076,38768:29992,38769:31407,38770:32650,38771:32768,38772:33865,38773:33993,38774:35201,38775:35617,38776:36362,38777:36965,38778:38525,38779:39178,38780:24958,38781:25233,38782:27442,38784:27779,38785:28020,38786:32716,38787:32764,38788:28096,38789:32645,38790:34746,38791:35064,38792:26469,38793:33713,38794:38972,38795:38647,38796:27931,38797:32097,38798:33853,38799:37226,38800:20081,38801:21365,38802:23888,38803:27396,38804:28651,38805:34253,38806:34349,38807:35239,38808:21033,38809:21519,38810:23653,38811:26446,38812:26792,38813:29702,38814:29827,38815:30178,38816:35023,38817:35041,38818:37324,38819:38626,38820:38520,38821:24459,38822:29575,38823:31435,38824:33870,38825:25504,38826:30053,38827:21129,38828:27969,38829:28316,38830:29705,38831:30041,38832:30827,38833:31890,38834:38534,38835:31452,38836:40845,38837:20406,38838:24942,38839:26053,38840:34396,38841:20102,38842:20142,38843:20698,38844:20001,38845:20940,38846:23534,38847:26009,38848:26753,38849:28092,38850:29471,38851:30274,38852:30637,38853:31260,38854:31975,38855:33391,38856:35538,38857:36988,38858:37327,38859:38517,38860:38936,38861:21147,38862:32209,38863:20523,38864:21400,38865:26519,38866:28107,38867:29136,38868:29747,38869:33256,38870:36650,38871:38563,38872:40023,38873:40607,38874:29792,38875:22593,38876:28057,38877:32047,38878:39006,38879:20196,38880:20278,38881:20363,38882:20919,38883:21169,38884:23994,38885:24604,38886:29618,38887:31036,38888:33491,38889:37428,38890:38583,38891:38646,38892:38666,38893:40599,38894:40802,38895:26278,38896:27508,38897:21015,38898:21155,38899:28872,38900:35010,38901:24265,38902:24651,38903:24976,38904:28451,38905:29001,38906:31806,38907:32244,38908:32879,38976:34030,38977:36899,38978:37676,38979:21570,38980:39791,38981:27347,38982:28809,38983:36034,38984:36335,38985:38706,38986:21172,38987:23105,38988:24266,38989:24324,38990:26391,38991:27004,38992:27028,38993:28010,38994:28431,38995:29282,38996:29436,38997:31725,38998:32769,38999:32894,39e3:34635,39001:37070,39002:20845,39003:40595,39004:31108,39005:32907,39006:37682,39007:35542,39008:20525,39009:21644,39010:35441,39011:27498,39012:36036,39013:33031,39014:24785,39015:26528,39016:40434,39017:20121,39018:20120,39019:39952,39020:35435,39021:34241,39022:34152,39023:26880,39024:28286,39025:30871,39026:33109,39071:24332,39072:19984,39073:19989,39074:20010,39075:20017,39076:20022,39077:20028,39078:20031,39079:20034,39080:20054,39081:20056,39082:20098,39083:20101,39084:35947,39085:20106,39086:33298,39087:24333,39088:20110,39089:20126,39090:20127,39091:20128,39092:20130,39093:20144,39094:20147,39095:20150,39096:20174,39097:20173,39098:20164,39099:20166,39100:20162,39101:20183,39102:20190,39103:20205,39104:20191,39105:20215,39106:20233,39107:20314,39108:20272,39109:20315,39110:20317,39111:20311,39112:20295,39113:20342,39114:20360,39115:20367,39116:20376,39117:20347,39118:20329,39119:20336,39120:20369,39121:20335,39122:20358,39123:20374,39124:20760,39125:20436,39126:20447,39127:20430,39128:20440,39129:20443,39130:20433,39131:20442,39132:20432,39133:20452,39134:20453,39135:20506,39136:20520,39137:20500,39138:20522,39139:20517,39140:20485,39141:20252,39142:20470,39143:20513,39144:20521,39145:20524,39146:20478,39147:20463,39148:20497,39149:20486,39150:20547,39151:20551,39152:26371,39153:20565,39154:20560,39155:20552,39156:20570,39157:20566,39158:20588,39159:20600,39160:20608,39161:20634,39162:20613,39163:20660,39164:20658,39232:20681,39233:20682,39234:20659,39235:20674,39236:20694,39237:20702,39238:20709,39239:20717,39240:20707,39241:20718,39242:20729,39243:20725,39244:20745,39245:20737,39246:20738,39247:20758,39248:20757,39249:20756,39250:20762,39251:20769,39252:20794,39253:20791,39254:20796,39255:20795,39256:20799,39257:20800,39258:20818,39259:20812,39260:20820,39261:20834,39262:31480,39263:20841,39264:20842,39265:20846,39266:20864,39267:20866,39268:22232,39269:20876,39270:20873,39271:20879,39272:20881,39273:20883,39274:20885,39275:20886,39276:20900,39277:20902,39278:20898,39279:20905,39280:20906,39281:20907,39282:20915,39283:20913,39284:20914,39285:20912,39286:20917,39287:20925,39288:20933,39289:20937,39290:20955,39291:20960,39292:34389,39293:20969,39294:20973,39296:20976,39297:20981,39298:20990,39299:20996,39300:21003,39301:21012,39302:21006,39303:21031,39304:21034,39305:21038,39306:21043,39307:21049,39308:21071,39309:21060,39310:21067,39311:21068,39312:21086,39313:21076,39314:21098,39315:21108,39316:21097,39317:21107,39318:21119,39319:21117,39320:21133,39321:21140,39322:21138,39323:21105,39324:21128,39325:21137,39326:36776,39327:36775,39328:21164,39329:21165,39330:21180,39331:21173,39332:21185,39333:21197,39334:21207,39335:21214,39336:21219,39337:21222,39338:39149,39339:21216,39340:21235,39341:21237,39342:21240,39343:21241,39344:21254,39345:21256,39346:30008,39347:21261,39348:21264,39349:21263,39350:21269,39351:21274,39352:21283,39353:21295,39354:21297,39355:21299,39356:21304,39357:21312,39358:21318,39359:21317,39360:19991,39361:21321,39362:21325,39363:20950,39364:21342,39365:21353,39366:21358,39367:22808,39368:21371,39369:21367,39370:21378,39371:21398,39372:21408,39373:21414,39374:21413,39375:21422,39376:21424,39377:21430,39378:21443,39379:31762,39380:38617,39381:21471,39382:26364,39383:29166,39384:21486,39385:21480,39386:21485,39387:21498,39388:21505,39389:21565,39390:21568,39391:21548,39392:21549,39393:21564,39394:21550,39395:21558,39396:21545,39397:21533,39398:21582,39399:21647,39400:21621,39401:21646,39402:21599,39403:21617,39404:21623,39405:21616,39406:21650,39407:21627,39408:21632,39409:21622,39410:21636,39411:21648,39412:21638,39413:21703,39414:21666,39415:21688,39416:21669,39417:21676,39418:21700,39419:21704,39420:21672,39488:21675,39489:21698,39490:21668,39491:21694,39492:21692,39493:21720,39494:21733,39495:21734,39496:21775,39497:21780,39498:21757,39499:21742,39500:21741,39501:21754,39502:21730,39503:21817,39504:21824,39505:21859,39506:21836,39507:21806,39508:21852,39509:21829,39510:21846,39511:21847,39512:21816,39513:21811,39514:21853,39515:21913,39516:21888,39517:21679,39518:21898,39519:21919,39520:21883,39521:21886,39522:21912,39523:21918,39524:21934,39525:21884,39526:21891,39527:21929,39528:21895,39529:21928,39530:21978,39531:21957,39532:21983,39533:21956,39534:21980,39535:21988,39536:21972,39537:22036,39538:22007,39539:22038,39540:22014,39541:22013,39542:22043,39543:22009,39544:22094,39545:22096,39546:29151,39547:22068,39548:22070,39549:22066,39550:22072,39552:22123,39553:22116,39554:22063,39555:22124,39556:22122,39557:22150,39558:22144,39559:22154,39560:22176,39561:22164,39562:22159,39563:22181,39564:22190,39565:22198,39566:22196,39567:22210,39568:22204,39569:22209,39570:22211,39571:22208,39572:22216,39573:22222,39574:22225,39575:22227,39576:22231,39577:22254,39578:22265,39579:22272,39580:22271,39581:22276,39582:22281,39583:22280,39584:22283,39585:22285,39586:22291,39587:22296,39588:22294,39589:21959,39590:22300,39591:22310,39592:22327,39593:22328,39594:22350,39595:22331,39596:22336,39597:22351,39598:22377,39599:22464,39600:22408,39601:22369,39602:22399,39603:22409,39604:22419,39605:22432,39606:22451,39607:22436,39608:22442,39609:22448,39610:22467,39611:22470,39612:22484,39613:22482,39614:22483,39615:22538,39616:22486,39617:22499,39618:22539,39619:22553,39620:22557,39621:22642,39622:22561,39623:22626,39624:22603,39625:22640,39626:27584,39627:22610,39628:22589,39629:22649,39630:22661,39631:22713,39632:22687,39633:22699,39634:22714,39635:22750,39636:22715,39637:22712,39638:22702,39639:22725,39640:22739,39641:22737,39642:22743,39643:22745,39644:22744,39645:22757,39646:22748,39647:22756,39648:22751,39649:22767,39650:22778,39651:22777,39652:22779,39653:22780,39654:22781,39655:22786,39656:22794,39657:22800,39658:22811,39659:26790,39660:22821,39661:22828,39662:22829,39663:22834,39664:22840,39665:22846,39666:31442,39667:22869,39668:22864,39669:22862,39670:22874,39671:22872,39672:22882,39673:22880,39674:22887,39675:22892,39676:22889,39744:22904,39745:22913,39746:22941,39747:20318,39748:20395,39749:22947,39750:22962,39751:22982,39752:23016,39753:23004,39754:22925,39755:23001,39756:23002,39757:23077,39758:23071,39759:23057,39760:23068,39761:23049,39762:23066,39763:23104,39764:23148,39765:23113,39766:23093,39767:23094,39768:23138,39769:23146,39770:23194,39771:23228,39772:23230,39773:23243,39774:23234,39775:23229,39776:23267,39777:23255,39778:23270,39779:23273,39780:23254,39781:23290,39782:23291,39783:23308,39784:23307,39785:23318,39786:23346,39787:23248,39788:23338,39789:23350,39790:23358,39791:23363,39792:23365,39793:23360,39794:23377,39795:23381,39796:23386,39797:23387,39798:23397,39799:23401,39800:23408,39801:23411,39802:23413,39803:23416,39804:25992,39805:23418,39806:23424,39808:23427,39809:23462,39810:23480,39811:23491,39812:23495,39813:23497,39814:23508,39815:23504,39816:23524,39817:23526,39818:23522,39819:23518,39820:23525,39821:23531,39822:23536,39823:23542,39824:23539,39825:23557,39826:23559,39827:23560,39828:23565,39829:23571,39830:23584,39831:23586,39832:23592,39833:23608,39834:23609,39835:23617,39836:23622,39837:23630,39838:23635,39839:23632,39840:23631,39841:23409,39842:23660,39843:23662,39844:20066,39845:23670,39846:23673,39847:23692,39848:23697,39849:23700,39850:22939,39851:23723,39852:23739,39853:23734,39854:23740,39855:23735,39856:23749,39857:23742,39858:23751,39859:23769,39860:23785,39861:23805,39862:23802,39863:23789,39864:23948,39865:23786,39866:23819,39867:23829,39868:23831,39869:23900,39870:23839,39871:23835,39872:23825,39873:23828,39874:23842,39875:23834,39876:23833,39877:23832,39878:23884,39879:23890,39880:23886,39881:23883,39882:23916,39883:23923,39884:23926,39885:23943,39886:23940,39887:23938,39888:23970,39889:23965,39890:23980,39891:23982,39892:23997,39893:23952,39894:23991,39895:23996,39896:24009,39897:24013,39898:24019,39899:24018,39900:24022,39901:24027,39902:24043,39903:24050,39904:24053,39905:24075,39906:24090,39907:24089,39908:24081,39909:24091,39910:24118,39911:24119,39912:24132,39913:24131,39914:24128,39915:24142,39916:24151,39917:24148,39918:24159,39919:24162,39920:24164,39921:24135,39922:24181,39923:24182,39924:24186,39925:40636,39926:24191,39927:24224,39928:24257,39929:24258,39930:24264,39931:24272,39932:24271,4e4:24278,40001:24291,40002:24285,40003:24282,40004:24283,40005:24290,40006:24289,40007:24296,40008:24297,40009:24300,40010:24305,40011:24307,40012:24304,40013:24308,40014:24312,40015:24318,40016:24323,40017:24329,40018:24413,40019:24412,40020:24331,40021:24337,40022:24342,40023:24361,40024:24365,40025:24376,40026:24385,40027:24392,40028:24396,40029:24398,40030:24367,40031:24401,40032:24406,40033:24407,40034:24409,40035:24417,40036:24429,40037:24435,40038:24439,40039:24451,40040:24450,40041:24447,40042:24458,40043:24456,40044:24465,40045:24455,40046:24478,40047:24473,40048:24472,40049:24480,40050:24488,40051:24493,40052:24508,40053:24534,40054:24571,40055:24548,40056:24568,40057:24561,40058:24541,40059:24755,40060:24575,40061:24609,40062:24672,40064:24601,40065:24592,40066:24617,40067:24590,40068:24625,40069:24603,40070:24597,40071:24619,40072:24614,40073:24591,40074:24634,40075:24666,40076:24641,40077:24682,40078:24695,40079:24671,40080:24650,40081:24646,40082:24653,40083:24675,40084:24643,40085:24676,40086:24642,40087:24684,40088:24683,40089:24665,40090:24705,40091:24717,40092:24807,40093:24707,40094:24730,40095:24708,40096:24731,40097:24726,40098:24727,40099:24722,40100:24743,40101:24715,40102:24801,40103:24760,40104:24800,40105:24787,40106:24756,40107:24560,40108:24765,40109:24774,40110:24757,40111:24792,40112:24909,40113:24853,40114:24838,40115:24822,40116:24823,40117:24832,40118:24820,40119:24826,40120:24835,40121:24865,40122:24827,40123:24817,40124:24845,40125:24846,40126:24903,40127:24894,40128:24872,40129:24871,40130:24906,40131:24895,40132:24892,40133:24876,40134:24884,40135:24893,40136:24898,40137:24900,40138:24947,40139:24951,40140:24920,40141:24921,40142:24922,40143:24939,40144:24948,40145:24943,40146:24933,40147:24945,40148:24927,40149:24925,40150:24915,40151:24949,40152:24985,40153:24982,40154:24967,40155:25004,40156:24980,40157:24986,40158:24970,40159:24977,40160:25003,40161:25006,40162:25036,40163:25034,40164:25033,40165:25079,40166:25032,40167:25027,40168:25030,40169:25018,40170:25035,40171:32633,40172:25037,40173:25062,40174:25059,40175:25078,40176:25082,40177:25076,40178:25087,40179:25085,40180:25084,40181:25086,40182:25088,40183:25096,40184:25097,40185:25101,40186:25100,40187:25108,40188:25115,40256:25118,40257:25121,40258:25130,40259:25134,40260:25136,40261:25138,40262:25139,40263:25153,40264:25166,40265:25182,40266:25187,40267:25179,40268:25184,40269:25192,40270:25212,40271:25218,40272:25225,40273:25214,40274:25234,40275:25235,40276:25238,40277:25300,40278:25219,40279:25236,40280:25303,40281:25297,40282:25275,40283:25295,40284:25343,40285:25286,40286:25812,40287:25288,40288:25308,40289:25292,40290:25290,40291:25282,40292:25287,40293:25243,40294:25289,40295:25356,40296:25326,40297:25329,40298:25383,40299:25346,40300:25352,40301:25327,40302:25333,40303:25424,40304:25406,40305:25421,40306:25628,40307:25423,40308:25494,40309:25486,40310:25472,40311:25515,40312:25462,40313:25507,40314:25487,40315:25481,40316:25503,40317:25525,40318:25451,40320:25449,40321:25534,40322:25577,40323:25536,40324:25542,40325:25571,40326:25545,40327:25554,40328:25590,40329:25540,40330:25622,40331:25652,40332:25606,40333:25619,40334:25638,40335:25654,40336:25885,40337:25623,40338:25640,40339:25615,40340:25703,40341:25711,40342:25718,40343:25678,40344:25898,40345:25749,40346:25747,40347:25765,40348:25769,40349:25736,40350:25788,40351:25818,40352:25810,40353:25797,40354:25799,40355:25787,40356:25816,40357:25794,40358:25841,40359:25831,40360:33289,40361:25824,40362:25825,40363:25260,40364:25827,40365:25839,40366:25900,40367:25846,40368:25844,40369:25842,40370:25850,40371:25856,40372:25853,40373:25880,40374:25884,40375:25861,40376:25892,40377:25891,40378:25899,40379:25908,40380:25909,40381:25911,40382:25910,40383:25912,40384:30027,40385:25928,40386:25942,40387:25941,40388:25933,40389:25944,40390:25950,40391:25949,40392:25970,40393:25976,40394:25986,40395:25987,40396:35722,40397:26011,40398:26015,40399:26027,40400:26039,40401:26051,40402:26054,40403:26049,40404:26052,40405:26060,40406:26066,40407:26075,40408:26073,40409:26080,40410:26081,40411:26097,40412:26482,40413:26122,40414:26115,40415:26107,40416:26483,40417:26165,40418:26166,40419:26164,40420:26140,40421:26191,40422:26180,40423:26185,40424:26177,40425:26206,40426:26205,40427:26212,40428:26215,40429:26216,40430:26207,40431:26210,40432:26224,40433:26243,40434:26248,40435:26254,40436:26249,40437:26244,40438:26264,40439:26269,40440:26305,40441:26297,40442:26313,40443:26302,40444:26300,40512:26308,40513:26296,40514:26326,40515:26330,40516:26336,40517:26175,40518:26342,40519:26345,40520:26352,40521:26357,40522:26359,40523:26383,40524:26390,40525:26398,40526:26406,40527:26407,40528:38712,40529:26414,40530:26431,40531:26422,40532:26433,40533:26424,40534:26423,40535:26438,40536:26462,40537:26464,40538:26457,40539:26467,40540:26468,40541:26505,40542:26480,40543:26537,40544:26492,40545:26474,40546:26508,40547:26507,40548:26534,40549:26529,40550:26501,40551:26551,40552:26607,40553:26548,40554:26604,40555:26547,40556:26601,40557:26552,40558:26596,40559:26590,40560:26589,40561:26594,40562:26606,40563:26553,40564:26574,40565:26566,40566:26599,40567:27292,40568:26654,40569:26694,40570:26665,40571:26688,40572:26701,40573:26674,40574:26702,40576:26803,40577:26667,40578:26713,40579:26723,40580:26743,40581:26751,40582:26783,40583:26767,40584:26797,40585:26772,40586:26781,40587:26779,40588:26755,40589:27310,40590:26809,40591:26740,40592:26805,40593:26784,40594:26810,40595:26895,40596:26765,40597:26750,40598:26881,40599:26826,40600:26888,40601:26840,40602:26914,40603:26918,40604:26849,40605:26892,40606:26829,40607:26836,40608:26855,40609:26837,40610:26934,40611:26898,40612:26884,40613:26839,40614:26851,40615:26917,40616:26873,40617:26848,40618:26863,40619:26920,40620:26922,40621:26906,40622:26915,40623:26913,40624:26822,40625:27001,40626:26999,40627:26972,40628:27e3,40629:26987,40630:26964,40631:27006,40632:26990,40633:26937,40634:26996,40635:26941,40636:26969,40637:26928,40638:26977,40639:26974,40640:26973,40641:27009,40642:26986,40643:27058,40644:27054,40645:27088,40646:27071,40647:27073,40648:27091,40649:27070,40650:27086,40651:23528,40652:27082,40653:27101,40654:27067,40655:27075,40656:27047,40657:27182,40658:27025,40659:27040,40660:27036,40661:27029,40662:27060,40663:27102,40664:27112,40665:27138,40666:27163,40667:27135,40668:27402,40669:27129,40670:27122,40671:27111,40672:27141,40673:27057,40674:27166,40675:27117,40676:27156,40677:27115,40678:27146,40679:27154,40680:27329,40681:27171,40682:27155,40683:27204,40684:27148,40685:27250,40686:27190,40687:27256,40688:27207,40689:27234,40690:27225,40691:27238,40692:27208,40693:27192,40694:27170,40695:27280,40696:27277,40697:27296,40698:27268,40699:27298,40700:27299,40768:27287,40769:34327,40770:27323,40771:27331,40772:27330,40773:27320,40774:27315,40775:27308,40776:27358,40777:27345,40778:27359,40779:27306,40780:27354,40781:27370,40782:27387,40783:27397,40784:34326,40785:27386,40786:27410,40787:27414,40788:39729,40789:27423,40790:27448,40791:27447,40792:30428,40793:27449,40794:39150,40795:27463,40796:27459,40797:27465,40798:27472,40799:27481,40800:27476,40801:27483,40802:27487,40803:27489,40804:27512,40805:27513,40806:27519,40807:27520,40808:27524,40809:27523,40810:27533,40811:27544,40812:27541,40813:27550,40814:27556,40815:27562,40816:27563,40817:27567,40818:27570,40819:27569,40820:27571,40821:27575,40822:27580,40823:27590,40824:27595,40825:27603,40826:27615,40827:27628,40828:27627,40829:27635,40830:27631,40832:40638,40833:27656,40834:27667,40835:27668,40836:27675,40837:27684,40838:27683,40839:27742,40840:27733,40841:27746,40842:27754,40843:27778,40844:27789,40845:27802,40846:27777,40847:27803,40848:27774,40849:27752,40850:27763,40851:27794,40852:27792,40853:27844,40854:27889,40855:27859,40856:27837,40857:27863,40858:27845,40859:27869,40860:27822,40861:27825,40862:27838,40863:27834,40864:27867,40865:27887,40866:27865,40867:27882,40868:27935,40869:34893,40870:27958,40871:27947,40872:27965,40873:27960,40874:27929,40875:27957,40876:27955,40877:27922,40878:27916,40879:28003,40880:28051,40881:28004,40882:27994,40883:28025,40884:27993,40885:28046,40886:28053,40887:28644,40888:28037,40889:28153,40890:28181,40891:28170,40892:28085,40893:28103,40894:28134,40895:28088,40896:28102,40897:28140,40898:28126,40899:28108,40900:28136,40901:28114,40902:28101,40903:28154,40904:28121,40905:28132,40906:28117,40907:28138,40908:28142,40909:28205,40910:28270,40911:28206,40912:28185,40913:28274,40914:28255,40915:28222,40916:28195,40917:28267,40918:28203,40919:28278,40920:28237,40921:28191,40922:28227,40923:28218,40924:28238,40925:28196,40926:28415,40927:28189,40928:28216,40929:28290,40930:28330,40931:28312,40932:28361,40933:28343,40934:28371,40935:28349,40936:28335,40937:28356,40938:28338,40939:28372,40940:28373,40941:28303,40942:28325,40943:28354,40944:28319,40945:28481,40946:28433,40947:28748,40948:28396,40949:28408,40950:28414,40951:28479,40952:28402,40953:28465,40954:28399,40955:28466,40956:28364,161:65377,162:65378,163:65379,164:65380,165:65381,166:65382,167:65383,168:65384,169:65385,170:65386,171:65387,172:65388,173:65389,174:65390,175:65391,176:65392,177:65393,178:65394,179:65395,180:65396,181:65397,182:65398,183:65399,184:65400,185:65401,186:65402,187:65403,188:65404,189:65405,190:65406,191:65407,192:65408,193:65409,194:65410,195:65411,196:65412,197:65413,198:65414,199:65415,200:65416,201:65417,202:65418,203:65419,204:65420,205:65421,206:65422,207:65423,208:65424,209:65425,210:65426,211:65427,212:65428,213:65429,214:65430,215:65431,216:65432,217:65433,218:65434,219:65435,220:65436,221:65437,222:65438,223:65439,57408:28478,57409:28435,57410:28407,57411:28550,57412:28538,57413:28536,57414:28545,57415:28544,57416:28527,57417:28507,57418:28659,57419:28525,57420:28546,57421:28540,57422:28504,57423:28558,57424:28561,57425:28610,57426:28518,57427:28595,57428:28579,57429:28577,57430:28580,57431:28601,57432:28614,57433:28586,57434:28639,57435:28629,57436:28652,57437:28628,57438:28632,57439:28657,57440:28654,57441:28635,57442:28681,57443:28683,57444:28666,57445:28689,57446:28673,57447:28687,57448:28670,57449:28699,57450:28698,57451:28532,57452:28701,57453:28696,57454:28703,57455:28720,57456:28734,57457:28722,57458:28753,57459:28771,57460:28825,57461:28818,57462:28847,57463:28913,57464:28844,57465:28856,57466:28851,57467:28846,57468:28895,57469:28875,57470:28893,57472:28889,57473:28937,57474:28925,57475:28956,57476:28953,57477:29029,57478:29013,57479:29064,57480:29030,57481:29026,57482:29004,57483:29014,57484:29036,57485:29071,57486:29179,57487:29060,57488:29077,57489:29096,57490:29100,57491:29143,57492:29113,57493:29118,57494:29138,57495:29129,57496:29140,57497:29134,57498:29152,57499:29164,57500:29159,57501:29173,57502:29180,57503:29177,57504:29183,57505:29197,57506:29200,57507:29211,57508:29224,57509:29229,57510:29228,57511:29232,57512:29234,57513:29243,57514:29244,57515:29247,57516:29248,57517:29254,57518:29259,57519:29272,57520:29300,57521:29310,57522:29314,57523:29313,57524:29319,57525:29330,57526:29334,57527:29346,57528:29351,57529:29369,57530:29362,57531:29379,57532:29382,57533:29380,57534:29390,57535:29394,57536:29410,57537:29408,57538:29409,57539:29433,57540:29431,57541:20495,57542:29463,57543:29450,57544:29468,57545:29462,57546:29469,57547:29492,57548:29487,57549:29481,57550:29477,57551:29502,57552:29518,57553:29519,57554:40664,57555:29527,57556:29546,57557:29544,57558:29552,57559:29560,57560:29557,57561:29563,57562:29562,57563:29640,57564:29619,57565:29646,57566:29627,57567:29632,57568:29669,57569:29678,57570:29662,57571:29858,57572:29701,57573:29807,57574:29733,57575:29688,57576:29746,57577:29754,57578:29781,57579:29759,57580:29791,57581:29785,57582:29761,57583:29788,57584:29801,57585:29808,57586:29795,57587:29802,57588:29814,57589:29822,57590:29835,57591:29854,57592:29863,57593:29898,57594:29903,57595:29908,57596:29681,57664:29920,57665:29923,57666:29927,57667:29929,57668:29934,57669:29938,57670:29936,57671:29937,57672:29944,57673:29943,57674:29956,57675:29955,57676:29957,57677:29964,57678:29966,57679:29965,57680:29973,57681:29971,57682:29982,57683:29990,57684:29996,57685:30012,57686:30020,57687:30029,57688:30026,57689:30025,57690:30043,57691:30022,57692:30042,57693:30057,57694:30052,57695:30055,57696:30059,57697:30061,57698:30072,57699:30070,57700:30086,57701:30087,57702:30068,57703:30090,57704:30089,57705:30082,57706:30100,57707:30106,57708:30109,57709:30117,57710:30115,57711:30146,57712:30131,57713:30147,57714:30133,57715:30141,57716:30136,57717:30140,57718:30129,57719:30157,57720:30154,57721:30162,57722:30169,57723:30179,57724:30174,57725:30206,57726:30207,57728:30204,57729:30209,57730:30192,57731:30202,57732:30194,57733:30195,57734:30219,57735:30221,57736:30217,57737:30239,57738:30247,57739:30240,57740:30241,57741:30242,57742:30244,57743:30260,57744:30256,57745:30267,57746:30279,57747:30280,57748:30278,57749:30300,57750:30296,57751:30305,57752:30306,57753:30312,57754:30313,57755:30314,57756:30311,57757:30316,57758:30320,57759:30322,57760:30326,57761:30328,57762:30332,57763:30336,57764:30339,57765:30344,57766:30347,57767:30350,57768:30358,57769:30355,57770:30361,57771:30362,57772:30384,57773:30388,57774:30392,57775:30393,57776:30394,57777:30402,57778:30413,57779:30422,57780:30418,57781:30430,57782:30433,57783:30437,57784:30439,57785:30442,57786:34351,57787:30459,57788:30472,57789:30471,57790:30468,57791:30505,57792:30500,57793:30494,57794:30501,57795:30502,57796:30491,57797:30519,57798:30520,57799:30535,57800:30554,57801:30568,57802:30571,57803:30555,57804:30565,57805:30591,57806:30590,57807:30585,57808:30606,57809:30603,57810:30609,57811:30624,57812:30622,57813:30640,57814:30646,57815:30649,57816:30655,57817:30652,57818:30653,57819:30651,57820:30663,57821:30669,57822:30679,57823:30682,57824:30684,57825:30691,57826:30702,57827:30716,57828:30732,57829:30738,57830:31014,57831:30752,57832:31018,57833:30789,57834:30862,57835:30836,57836:30854,57837:30844,57838:30874,57839:30860,57840:30883,57841:30901,57842:30890,57843:30895,57844:30929,57845:30918,57846:30923,57847:30932,57848:30910,57849:30908,57850:30917,57851:30922,57852:30956,57920:30951,57921:30938,57922:30973,57923:30964,57924:30983,57925:30994,57926:30993,57927:31001,57928:31020,57929:31019,57930:31040,57931:31072,57932:31063,57933:31071,57934:31066,57935:31061,57936:31059,57937:31098,57938:31103,57939:31114,57940:31133,57941:31143,57942:40779,57943:31146,57944:31150,57945:31155,57946:31161,57947:31162,57948:31177,57949:31189,57950:31207,57951:31212,57952:31201,57953:31203,57954:31240,57955:31245,57956:31256,57957:31257,57958:31264,57959:31263,57960:31104,57961:31281,57962:31291,57963:31294,57964:31287,57965:31299,57966:31319,57967:31305,57968:31329,57969:31330,57970:31337,57971:40861,57972:31344,57973:31353,57974:31357,57975:31368,57976:31383,57977:31381,57978:31384,57979:31382,57980:31401,57981:31432,57982:31408,57984:31414,57985:31429,57986:31428,57987:31423,57988:36995,57989:31431,57990:31434,57991:31437,57992:31439,57993:31445,57994:31443,57995:31449,57996:31450,57997:31453,57998:31457,57999:31458,58e3:31462,58001:31469,58002:31472,58003:31490,58004:31503,58005:31498,58006:31494,58007:31539,58008:31512,58009:31513,58010:31518,58011:31541,58012:31528,58013:31542,58014:31568,58015:31610,58016:31492,58017:31565,58018:31499,58019:31564,58020:31557,58021:31605,58022:31589,58023:31604,58024:31591,58025:31600,58026:31601,58027:31596,58028:31598,58029:31645,58030:31640,58031:31647,58032:31629,58033:31644,58034:31642,58035:31627,58036:31634,58037:31631,58038:31581,58039:31641,58040:31691,58041:31681,58042:31692,58043:31695,58044:31668,58045:31686,58046:31709,58047:31721,58048:31761,58049:31764,58050:31718,58051:31717,58052:31840,58053:31744,58054:31751,58055:31763,58056:31731,58057:31735,58058:31767,58059:31757,58060:31734,58061:31779,58062:31783,58063:31786,58064:31775,58065:31799,58066:31787,58067:31805,58068:31820,58069:31811,58070:31828,58071:31823,58072:31808,58073:31824,58074:31832,58075:31839,58076:31844,58077:31830,58078:31845,58079:31852,58080:31861,58081:31875,58082:31888,58083:31908,58084:31917,58085:31906,58086:31915,58087:31905,58088:31912,58089:31923,58090:31922,58091:31921,58092:31918,58093:31929,58094:31933,58095:31936,58096:31941,58097:31938,58098:31960,58099:31954,58100:31964,58101:31970,58102:39739,58103:31983,58104:31986,58105:31988,58106:31990,58107:31994,58108:32006,58176:32002,58177:32028,58178:32021,58179:32010,58180:32069,58181:32075,58182:32046,58183:32050,58184:32063,58185:32053,58186:32070,58187:32115,58188:32086,58189:32078,58190:32114,58191:32104,58192:32110,58193:32079,58194:32099,58195:32147,58196:32137,58197:32091,58198:32143,58199:32125,58200:32155,58201:32186,58202:32174,58203:32163,58204:32181,58205:32199,58206:32189,58207:32171,58208:32317,58209:32162,58210:32175,58211:32220,58212:32184,58213:32159,58214:32176,58215:32216,58216:32221,58217:32228,58218:32222,58219:32251,58220:32242,58221:32225,58222:32261,58223:32266,58224:32291,58225:32289,58226:32274,58227:32305,58228:32287,58229:32265,58230:32267,58231:32290,58232:32326,58233:32358,58234:32315,58235:32309,58236:32313,58237:32323,58238:32311,58240:32306,58241:32314,58242:32359,58243:32349,58244:32342,58245:32350,58246:32345,58247:32346,58248:32377,58249:32362,58250:32361,58251:32380,58252:32379,58253:32387,58254:32213,58255:32381,58256:36782,58257:32383,58258:32392,58259:32393,58260:32396,58261:32402,58262:32400,58263:32403,58264:32404,58265:32406,58266:32398,58267:32411,58268:32412,58269:32568,58270:32570,58271:32581,58272:32588,58273:32589,58274:32590,58275:32592,58276:32593,58277:32597,58278:32596,58279:32600,58280:32607,58281:32608,58282:32616,58283:32617,58284:32615,58285:32632,58286:32642,58287:32646,58288:32643,58289:32648,58290:32647,58291:32652,58292:32660,58293:32670,58294:32669,58295:32666,58296:32675,58297:32687,58298:32690,58299:32697,58300:32686,58301:32694,58302:32696,58303:35697,58304:32709,58305:32710,58306:32714,58307:32725,58308:32724,58309:32737,58310:32742,58311:32745,58312:32755,58313:32761,58314:39132,58315:32774,58316:32772,58317:32779,58318:32786,58319:32792,58320:32793,58321:32796,58322:32801,58323:32808,58324:32831,58325:32827,58326:32842,58327:32838,58328:32850,58329:32856,58330:32858,58331:32863,58332:32866,58333:32872,58334:32883,58335:32882,58336:32880,58337:32886,58338:32889,58339:32893,58340:32895,58341:32900,58342:32902,58343:32901,58344:32923,58345:32915,58346:32922,58347:32941,58348:20880,58349:32940,58350:32987,58351:32997,58352:32985,58353:32989,58354:32964,58355:32986,58356:32982,58357:33033,58358:33007,58359:33009,58360:33051,58361:33065,58362:33059,58363:33071,58364:33099,58432:38539,58433:33094,58434:33086,58435:33107,58436:33105,58437:33020,58438:33137,58439:33134,58440:33125,58441:33126,58442:33140,58443:33155,58444:33160,58445:33162,58446:33152,58447:33154,58448:33184,58449:33173,58450:33188,58451:33187,58452:33119,58453:33171,58454:33193,58455:33200,58456:33205,58457:33214,58458:33208,58459:33213,58460:33216,58461:33218,58462:33210,58463:33225,58464:33229,58465:33233,58466:33241,58467:33240,58468:33224,58469:33242,58470:33247,58471:33248,58472:33255,58473:33274,58474:33275,58475:33278,58476:33281,58477:33282,58478:33285,58479:33287,58480:33290,58481:33293,58482:33296,58483:33302,58484:33321,58485:33323,58486:33336,58487:33331,58488:33344,58489:33369,58490:33368,58491:33373,58492:33370,58493:33375,58494:33380,58496:33378,58497:33384,58498:33386,58499:33387,58500:33326,58501:33393,58502:33399,58503:33400,58504:33406,58505:33421,58506:33426,58507:33451,58508:33439,58509:33467,58510:33452,58511:33505,58512:33507,58513:33503,58514:33490,58515:33524,58516:33523,58517:33530,58518:33683,58519:33539,58520:33531,58521:33529,58522:33502,58523:33542,58524:33500,58525:33545,58526:33497,58527:33589,58528:33588,58529:33558,58530:33586,58531:33585,58532:33600,58533:33593,58534:33616,58535:33605,58536:33583,58537:33579,58538:33559,58539:33560,58540:33669,58541:33690,58542:33706,58543:33695,58544:33698,58545:33686,58546:33571,58547:33678,58548:33671,58549:33674,58550:33660,58551:33717,58552:33651,58553:33653,58554:33696,58555:33673,58556:33704,58557:33780,58558:33811,58559:33771,58560:33742,58561:33789,58562:33795,58563:33752,58564:33803,58565:33729,58566:33783,58567:33799,58568:33760,58569:33778,58570:33805,58571:33826,58572:33824,58573:33725,58574:33848,58575:34054,58576:33787,58577:33901,58578:33834,58579:33852,58580:34138,58581:33924,58582:33911,58583:33899,58584:33965,58585:33902,58586:33922,58587:33897,58588:33862,58589:33836,58590:33903,58591:33913,58592:33845,58593:33994,58594:33890,58595:33977,58596:33983,58597:33951,58598:34009,58599:33997,58600:33979,58601:34010,58602:34e3,58603:33985,58604:33990,58605:34006,58606:33953,58607:34081,58608:34047,58609:34036,58610:34071,58611:34072,58612:34092,58613:34079,58614:34069,58615:34068,58616:34044,58617:34112,58618:34147,58619:34136,58620:34120,58688:34113,58689:34306,58690:34123,58691:34133,58692:34176,58693:34212,58694:34184,58695:34193,58696:34186,58697:34216,58698:34157,58699:34196,58700:34203,58701:34282,58702:34183,58703:34204,58704:34167,58705:34174,58706:34192,58707:34249,58708:34234,58709:34255,58710:34233,58711:34256,58712:34261,58713:34269,58714:34277,58715:34268,58716:34297,58717:34314,58718:34323,58719:34315,58720:34302,58721:34298,58722:34310,58723:34338,58724:34330,58725:34352,58726:34367,58727:34381,58728:20053,58729:34388,58730:34399,58731:34407,58732:34417,58733:34451,58734:34467,58735:34473,58736:34474,58737:34443,58738:34444,58739:34486,58740:34479,58741:34500,58742:34502,58743:34480,58744:34505,58745:34851,58746:34475,58747:34516,58748:34526,58749:34537,58750:34540,58752:34527,58753:34523,58754:34543,58755:34578,58756:34566,58757:34568,58758:34560,58759:34563,58760:34555,58761:34577,58762:34569,58763:34573,58764:34553,58765:34570,58766:34612,58767:34623,58768:34615,58769:34619,58770:34597,58771:34601,58772:34586,58773:34656,58774:34655,58775:34680,58776:34636,58777:34638,58778:34676,58779:34647,58780:34664,58781:34670,58782:34649,58783:34643,58784:34659,58785:34666,58786:34821,58787:34722,58788:34719,58789:34690,58790:34735,58791:34763,58792:34749,58793:34752,58794:34768,58795:38614,58796:34731,58797:34756,58798:34739,58799:34759,58800:34758,58801:34747,58802:34799,58803:34802,58804:34784,58805:34831,58806:34829,58807:34814,58808:34806,58809:34807,58810:34830,58811:34770,58812:34833,58813:34838,58814:34837,58815:34850,58816:34849,58817:34865,58818:34870,58819:34873,58820:34855,58821:34875,58822:34884,58823:34882,58824:34898,58825:34905,58826:34910,58827:34914,58828:34923,58829:34945,58830:34942,58831:34974,58832:34933,58833:34941,58834:34997,58835:34930,58836:34946,58837:34967,58838:34962,58839:34990,58840:34969,58841:34978,58842:34957,58843:34980,58844:34992,58845:35007,58846:34993,58847:35011,58848:35012,58849:35028,58850:35032,58851:35033,58852:35037,58853:35065,58854:35074,58855:35068,58856:35060,58857:35048,58858:35058,58859:35076,58860:35084,58861:35082,58862:35091,58863:35139,58864:35102,58865:35109,58866:35114,58867:35115,58868:35137,58869:35140,58870:35131,58871:35126,58872:35128,58873:35148,58874:35101,58875:35168,58876:35166,58944:35174,58945:35172,58946:35181,58947:35178,58948:35183,58949:35188,58950:35191,58951:35198,58952:35203,58953:35208,58954:35210,58955:35219,58956:35224,58957:35233,58958:35241,58959:35238,58960:35244,58961:35247,58962:35250,58963:35258,58964:35261,58965:35263,58966:35264,58967:35290,58968:35292,58969:35293,58970:35303,58971:35316,58972:35320,58973:35331,58974:35350,58975:35344,58976:35340,58977:35355,58978:35357,58979:35365,58980:35382,58981:35393,58982:35419,58983:35410,58984:35398,58985:35400,58986:35452,58987:35437,58988:35436,58989:35426,58990:35461,58991:35458,58992:35460,58993:35496,58994:35489,58995:35473,58996:35493,58997:35494,58998:35482,58999:35491,59e3:35524,59001:35533,59002:35522,59003:35546,59004:35563,59005:35571,59006:35559,59008:35556,59009:35569,59010:35604,59011:35552,59012:35554,59013:35575,59014:35550,59015:35547,59016:35596,59017:35591,59018:35610,59019:35553,59020:35606,59021:35600,59022:35607,59023:35616,59024:35635,59025:38827,59026:35622,59027:35627,59028:35646,59029:35624,59030:35649,59031:35660,59032:35663,59033:35662,59034:35657,59035:35670,59036:35675,59037:35674,59038:35691,59039:35679,59040:35692,59041:35695,59042:35700,59043:35709,59044:35712,59045:35724,59046:35726,59047:35730,59048:35731,59049:35734,59050:35737,59051:35738,59052:35898,59053:35905,59054:35903,59055:35912,59056:35916,59057:35918,59058:35920,59059:35925,59060:35938,59061:35948,59062:35960,59063:35962,59064:35970,59065:35977,59066:35973,59067:35978,59068:35981,59069:35982,59070:35988,59071:35964,59072:35992,59073:25117,59074:36013,59075:36010,59076:36029,59077:36018,59078:36019,59079:36014,59080:36022,59081:36040,59082:36033,59083:36068,59084:36067,59085:36058,59086:36093,59087:36090,59088:36091,59089:36100,59090:36101,59091:36106,59092:36103,59093:36111,59094:36109,59095:36112,59096:40782,59097:36115,59098:36045,59099:36116,59100:36118,59101:36199,59102:36205,59103:36209,59104:36211,59105:36225,59106:36249,59107:36290,59108:36286,59109:36282,59110:36303,59111:36314,59112:36310,59113:36300,59114:36315,59115:36299,59116:36330,59117:36331,59118:36319,59119:36323,59120:36348,59121:36360,59122:36361,59123:36351,59124:36381,59125:36382,59126:36368,59127:36383,59128:36418,59129:36405,59130:36400,59131:36404,59132:36426,59200:36423,59201:36425,59202:36428,59203:36432,59204:36424,59205:36441,59206:36452,59207:36448,59208:36394,59209:36451,59210:36437,59211:36470,59212:36466,59213:36476,59214:36481,59215:36487,59216:36485,59217:36484,59218:36491,59219:36490,59220:36499,59221:36497,59222:36500,59223:36505,59224:36522,59225:36513,59226:36524,59227:36528,59228:36550,59229:36529,59230:36542,59231:36549,59232:36552,59233:36555,59234:36571,59235:36579,59236:36604,59237:36603,59238:36587,59239:36606,59240:36618,59241:36613,59242:36629,59243:36626,59244:36633,59245:36627,59246:36636,59247:36639,59248:36635,59249:36620,59250:36646,59251:36659,59252:36667,59253:36665,59254:36677,59255:36674,59256:36670,59257:36684,59258:36681,59259:36678,59260:36686,59261:36695,59262:36700,59264:36706,59265:36707,59266:36708,59267:36764,59268:36767,59269:36771,59270:36781,59271:36783,59272:36791,59273:36826,59274:36837,59275:36834,59276:36842,59277:36847,59278:36999,59279:36852,59280:36869,59281:36857,59282:36858,59283:36881,59284:36885,59285:36897,59286:36877,59287:36894,59288:36886,59289:36875,59290:36903,59291:36918,59292:36917,59293:36921,59294:36856,59295:36943,59296:36944,59297:36945,59298:36946,59299:36878,59300:36937,59301:36926,59302:36950,59303:36952,59304:36958,59305:36968,59306:36975,59307:36982,59308:38568,59309:36978,59310:36994,59311:36989,59312:36993,59313:36992,59314:37002,59315:37001,59316:37007,59317:37032,59318:37039,59319:37041,59320:37045,59321:37090,59322:37092,59323:25160,59324:37083,59325:37122,59326:37138,59327:37145,59328:37170,59329:37168,59330:37194,59331:37206,59332:37208,59333:37219,59334:37221,59335:37225,59336:37235,59337:37234,59338:37259,59339:37257,59340:37250,59341:37282,59342:37291,59343:37295,59344:37290,59345:37301,59346:37300,59347:37306,59348:37312,59349:37313,59350:37321,59351:37323,59352:37328,59353:37334,59354:37343,59355:37345,59356:37339,59357:37372,59358:37365,59359:37366,59360:37406,59361:37375,59362:37396,59363:37420,59364:37397,59365:37393,59366:37470,59367:37463,59368:37445,59369:37449,59370:37476,59371:37448,59372:37525,59373:37439,59374:37451,59375:37456,59376:37532,59377:37526,59378:37523,59379:37531,59380:37466,59381:37583,59382:37561,59383:37559,59384:37609,59385:37647,59386:37626,59387:37700,59388:37678,59456:37657,59457:37666,59458:37658,59459:37667,59460:37690,59461:37685,59462:37691,59463:37724,59464:37728,59465:37756,59466:37742,59467:37718,59468:37808,59469:37804,59470:37805,59471:37780,59472:37817,59473:37846,59474:37847,59475:37864,59476:37861,59477:37848,59478:37827,59479:37853,59480:37840,59481:37832,59482:37860,59483:37914,59484:37908,59485:37907,59486:37891,59487:37895,59488:37904,59489:37942,59490:37931,59491:37941,59492:37921,59493:37946,59494:37953,59495:37970,59496:37956,59497:37979,59498:37984,59499:37986,59500:37982,59501:37994,59502:37417,59503:38e3,59504:38005,59505:38007,59506:38013,59507:37978,59508:38012,59509:38014,59510:38017,59511:38015,59512:38274,59513:38279,59514:38282,59515:38292,59516:38294,59517:38296,59518:38297,59520:38304,59521:38312,59522:38311,59523:38317,59524:38332,59525:38331,59526:38329,59527:38334,59528:38346,59529:28662,59530:38339,59531:38349,59532:38348,59533:38357,59534:38356,59535:38358,59536:38364,59537:38369,59538:38373,59539:38370,59540:38433,59541:38440,59542:38446,59543:38447,59544:38466,59545:38476,59546:38479,59547:38475,59548:38519,59549:38492,59550:38494,59551:38493,59552:38495,59553:38502,59554:38514,59555:38508,59556:38541,59557:38552,59558:38549,59559:38551,59560:38570,59561:38567,59562:38577,59563:38578,59564:38576,59565:38580,59566:38582,59567:38584,59568:38585,59569:38606,59570:38603,59571:38601,59572:38605,59573:35149,59574:38620,59575:38669,59576:38613,59577:38649,59578:38660,59579:38662,59580:38664,59581:38675,59582:38670,59583:38673,59584:38671,59585:38678,59586:38681,59587:38692,59588:38698,59589:38704,59590:38713,59591:38717,59592:38718,59593:38724,59594:38726,59595:38728,59596:38722,59597:38729,59598:38748,59599:38752,59600:38756,59601:38758,59602:38760,59603:21202,59604:38763,59605:38769,59606:38777,59607:38789,59608:38780,59609:38785,59610:38778,59611:38790,59612:38795,59613:38799,59614:38800,59615:38812,59616:38824,59617:38822,59618:38819,59619:38835,59620:38836,59621:38851,59622:38854,59623:38856,59624:38859,59625:38876,59626:38893,59627:40783,59628:38898,59629:31455,59630:38902,59631:38901,59632:38927,59633:38924,59634:38968,59635:38948,59636:38945,59637:38967,59638:38973,59639:38982,59640:38991,59641:38987,59642:39019,59643:39023,59644:39024,59712:39025,59713:39028,59714:39027,59715:39082,59716:39087,59717:39089,59718:39094,59719:39108,59720:39107,59721:39110,59722:39145,59723:39147,59724:39171,59725:39177,59726:39186,59727:39188,59728:39192,59729:39201,59730:39197,59731:39198,59732:39204,59733:39200,59734:39212,59735:39214,59736:39229,59737:39230,59738:39234,59739:39241,59740:39237,59741:39248,59742:39243,59743:39249,59744:39250,59745:39244,59746:39253,59747:39319,59748:39320,59749:39333,59750:39341,59751:39342,59752:39356,59753:39391,59754:39387,59755:39389,59756:39384,59757:39377,59758:39405,59759:39406,59760:39409,59761:39410,59762:39419,59763:39416,59764:39425,59765:39439,59766:39429,59767:39394,59768:39449,59769:39467,59770:39479,59771:39493,59772:39490,59773:39488,59774:39491,59776:39486,59777:39509,59778:39501,59779:39515,59780:39511,59781:39519,59782:39522,59783:39525,59784:39524,59785:39529,59786:39531,59787:39530,59788:39597,59789:39600,59790:39612,59791:39616,59792:39631,59793:39633,59794:39635,59795:39636,59796:39646,59797:39647,59798:39650,59799:39651,59800:39654,59801:39663,59802:39659,59803:39662,59804:39668,59805:39665,59806:39671,59807:39675,59808:39686,59809:39704,59810:39706,59811:39711,59812:39714,59813:39715,59814:39717,59815:39719,59816:39720,59817:39721,59818:39722,59819:39726,59820:39727,59821:39730,59822:39748,59823:39747,59824:39759,59825:39757,59826:39758,59827:39761,59828:39768,59829:39796,59830:39827,59831:39811,59832:39825,59833:39830,59834:39831,59835:39839,59836:39840,59837:39848,59838:39860,59839:39872,59840:39882,59841:39865,59842:39878,59843:39887,59844:39889,59845:39890,59846:39907,59847:39906,59848:39908,59849:39892,59850:39905,59851:39994,59852:39922,59853:39921,59854:39920,59855:39957,59856:39956,59857:39945,59858:39955,59859:39948,59860:39942,59861:39944,59862:39954,59863:39946,59864:39940,59865:39982,59866:39963,59867:39973,59868:39972,59869:39969,59870:39984,59871:40007,59872:39986,59873:40006,59874:39998,59875:40026,59876:40032,59877:40039,59878:40054,59879:40056,59880:40167,59881:40172,59882:40176,59883:40201,59884:40200,59885:40171,59886:40195,59887:40198,59888:40234,59889:40230,59890:40367,59891:40227,59892:40223,59893:40260,59894:40213,59895:40210,59896:40257,59897:40255,59898:40254,59899:40262,59900:40264,59968:40285,59969:40286,59970:40292,59971:40273,59972:40272,59973:40281,59974:40306,59975:40329,59976:40327,59977:40363,59978:40303,59979:40314,59980:40346,59981:40356,59982:40361,59983:40370,59984:40388,59985:40385,59986:40379,59987:40376,59988:40378,59989:40390,59990:40399,59991:40386,59992:40409,59993:40403,59994:40440,59995:40422,59996:40429,59997:40431,59998:40445,59999:40474,6e4:40475,60001:40478,60002:40565,60003:40569,60004:40573,60005:40577,60006:40584,60007:40587,60008:40588,60009:40594,60010:40597,60011:40593,60012:40605,60013:40613,60014:40617,60015:40632,60016:40618,60017:40621,60018:38753,60019:40652,60020:40654,60021:40655,60022:40656,60023:40660,60024:40668,60025:40670,60026:40669,60027:40672,60028:40677,60029:40680,60030:40687,60032:40692,60033:40694,60034:40695,60035:40697,60036:40699,60037:40700,60038:40701,60039:40711,60040:40712,60041:30391,60042:40725,60043:40737,60044:40748,60045:40766,60046:40778,60047:40786,60048:40788,60049:40803,60050:40799,60051:40800,60052:40801,60053:40806,60054:40807,60055:40812,60056:40810,60057:40823,60058:40818,60059:40822,60060:40853,60061:40860,60062:40864,60063:22575,60064:27079,60065:36953,60066:29796,60067:20956,60068:29081}},function(G,y,g){"use strict";Object.defineProperty(y,"__esModule",{value:!0});var p=g(1),m=g(2);function l(E,r,a,F){var d;r.degree()=F/2;){var x=D,B=v;if(D=i,v=A,D.isZero())return null;i=x;for(var e=E.zero,C=D.getCoefficient(D.degree()),t=E.inverse(C);i.degree()>=D.degree()&&!i.isZero();){var n=i.degree()-D.degree(),k=E.multiply(i.getCoefficient(i.degree()),t);e=e.addOrSubtract(E.buildMonomial(n,k)),i=i.addOrSubtract(D.multiplyByMonomial(n,k))}if(A=e.multiplyPoly(v).addOrSubtract(B),i.degree()>=D.degree())return null}var u=A.getCoefficient(0);if(u===0)return null;var f=E.inverse(u);return[A.multiply(f),i.multiply(f)]}function s(E,r){var a=r.degree();if(a===1)return[r.getCoefficient(1)];for(var F=new Array(a),d=0,D=1;D=k&&u>=f?(e=[x,A,B],M=e[0],b=e[1],w=e[2]):f>=u&&f>=k?(C=[A,x,B],M=C[0],b=C[1],w=C[2]):(t=[A,B,x],M=t[0],b=t[1],w=t[2]),(w.x-b.x)*(M.y-b.y)-(w.y-b.y)*(M.x-b.x)<0&&(n=[w,M],M=n[0],w=n[1]),{bottomLeft:M,topLeft:b,topRight:w}}function E(A,x,B,e){var C=(c(a(A,B,e,5))/7+c(a(A,x,e,5))/7+c(a(B,A,e,5))/7+c(a(x,A,e,5))/7)/4;if(C<1)throw new Error("Invalid module size");var t=Math.round(s(A,x)/C),n=Math.round(s(A,B)/C),k=Math.floor((t+n)/2)+7;switch(k%4){case 0:k++;break;case 2:k--;break}return{dimension:k,moduleSize:C}}function r(A,x,B,e){var C=[{x:Math.floor(A.x),y:Math.floor(A.y)}],t=Math.abs(x.y-A.y)>Math.abs(x.x-A.x),n,k,u,f;t?(n=Math.floor(A.y),k=Math.floor(A.x),u=Math.floor(x.y),f=Math.floor(x.x)):(n=Math.floor(A.x),k=Math.floor(A.y),u=Math.floor(x.x),f=Math.floor(x.y));for(var M=Math.abs(u-n),b=Math.abs(f-k),w=Math.floor(-M/2),I=n0){if(O===f)break;O+=h,w-=M}}for(var z=[],L=0;L=U.bottom.startX&&Y<=U.bottom.endX||H>=U.bottom.startX&&Y<=U.bottom.endX||Y<=U.bottom.startX&&H>=U.bottom.endX&&L[2]/(U.bottom.endX-U.bottom.startX)m});Z.length>0?Z[0].bottom=Q:B.push({top:Q,bottom:Q})}if(x0){var $=X-L[4],J=$-L[3],Q={startX:J,y:P,endX:$},Z=C.filter(function(R){return J>=R.bottom.startX&&J<=R.bottom.endX||$>=R.bottom.startX&&J<=R.bottom.endX||J<=R.bottom.startX&&$>=R.bottom.endX&&L[2]/(R.bottom.endX-R.bottom.startX)m});Z.length>0?Z[0].bottom=Q:C.push({top:Q,bottom:Q})}}},j=-1;j<=A.width;j++)K(j);x.push.apply(x,B.filter(function(X){return X.bottom.y!==P&&X.bottom.y-X.top.y>=2})),B=B.filter(function(X){return X.bottom.y===P}),e.push.apply(e,C.filter(function(X){return X.bottom.y!==P})),C=C.filter(function(X){return X.bottom.y===P})},n=0;n<=A.height;n++)t(n);x.push.apply(x,B.filter(function(P){return P.bottom.y-P.top.y>=2})),e.push.apply(e,C);var k=x.filter(function(P){return P.bottom.y-P.top.y>=2}).map(function(P){var T=(P.top.startX+P.top.endX+P.bottom.startX+P.bottom.endX)/4,z=(P.top.y+P.bottom.y+1)/2;if(A.get(Math.round(T),Math.round(z))){var L=[P.top.endX-P.top.startX,P.bottom.endX-P.bottom.startX,P.bottom.y-P.top.y+1],K=c(L)/L.length,j=d({x:Math.round(T),y:Math.round(z)},[1,1,3,1,1],A);return{score:j,x:T,y:z,size:K}}}).filter(function(P){return!!P}).sort(function(P,T){return P.score-T.score}).map(function(P,T,z){if(T>p)return null;var L=z.filter(function(j,X){return T!==X}).map(function(j){return{x:j.x,y:j.y,score:j.score+Math.pow(j.size-P.size,2)/P.size,size:j.size}}).sort(function(j,X){return j.score-X.score});if(L.length<2)return null;var K=P.score+L[0].score+L[1].score;return{points:[P].concat(L.slice(0,2)),score:K}}).filter(function(P){return!!P}).sort(function(P,T){return P.score-T.score});if(k.length===0)return null;var u=o(k[0].points[0],k[0].points[1],k[0].points[2]),f=u.topRight,M=u.topLeft,b=u.bottomLeft,w=v(A,e,f,M,b),I=[];w&&I.push({alignmentPattern:{x:w.alignmentPattern.x,y:w.alignmentPattern.y},bottomLeft:{x:b.x,y:b.y},dimension:w.dimension,topLeft:{x:M.x,y:M.y},topRight:{x:f.x,y:f.y}});var h=D(A,f),N=D(A,M),S=D(A,b),O=v(A,e,h,N,S);return O&&I.push({alignmentPattern:{x:O.alignmentPattern.x,y:O.alignmentPattern.y},bottomLeft:{x:S.x,y:S.y},topLeft:{x:N.x,y:N.y},topRight:{x:h.x,y:h.y},dimension:O.dimension}),I.length===0?null:I}y.locate=i;function v(A,x,B,e,C){var t,n,k;try{t=E(e,B,C,A),n=t.dimension,k=t.moduleSize}catch{return null}var u={x:B.x-e.x+C.x,y:B.y-e.y+C.y},f=(s(e,C)+s(e,B))/2/k,M=1-3/f,b={x:e.x+M*(u.x-e.x),y:e.y+M*(u.y-e.y)},w=x.map(function(h){var N=(h.top.startX+h.top.endX+h.bottom.startX+h.bottom.endX)/4,S=(h.top.y+h.bottom.y+1)/2;if(A.get(Math.floor(N),Math.floor(S))){var O=[h.top.endX-h.top.startX,h.bottom.endX-h.bottom.startX,h.bottom.y-h.top.y+1],P=c(O)/O.length,T=d({x:Math.floor(N),y:Math.floor(S)},[1,1,1],A),z=T+s({x:N,y:S},b);return{x:N,y:S,score:z}}}).filter(function(h){return!!h}).sort(function(h,N){return h.score-N.score}),I=f>=15&&w.length?w[0]:b;return{alignmentPattern:I,dimension:n}}}]).default});
diff --git a/BeadQR/project.config.json b/BeadQR/project.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..df30fa1c2487b304fdb0a1be0bf84f00ad5cba08
--- /dev/null
+++ b/BeadQR/project.config.json
@@ -0,0 +1,20 @@
+{
+ "description": "豆维 · 可扫码的拼豆",
+ "appid": "wx17a895fdcb5de908",
+ "compileType": "miniprogram",
+ "libVersion": "3.17.1",
+ "projectname": "豆维",
+ "miniprogramRoot": "miniprogram/",
+ "cloudfunctionRoot": "cloudfunctions/",
+ "setting": {
+ "es6": true,
+ "enhance": true,
+ "postcss": true,
+ "minified": true,
+ "urlCheck": false,
+ "ignoreUploadUnusedFiles": true
+ },
+ "packOptions": {
+ "ignore": []
+ }
+}
diff --git a/BeadQR/project.private.config.json b/BeadQR/project.private.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..63c8bf65b255d79e5a650d4f5086a86def72ec20
--- /dev/null
+++ b/BeadQR/project.private.config.json
@@ -0,0 +1,25 @@
+{
+ "setting": {
+ "compileHotReLoad": true,
+ "urlCheck": false,
+ "coverView": true,
+ "lazyloadPlaceholderEnable": false,
+ "skylineRenderEnable": false,
+ "preloadBackgroundData": false,
+ "autoAudits": false,
+ "useApiHook": true,
+ "useApiHostProcess": true,
+ "showShadowRootInWxmlPanel": true,
+ "useStaticServer": false,
+ "useLanDebug": false,
+ "showES6CompileOption": false,
+ "checkInvalidKey": true,
+ "ignoreDevUnusedFiles": true,
+ "bigPackageSizeSupport": false,
+ "useIsolateContext": true
+ },
+ "condition": {},
+ "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
+ "projectname": "BeadQR",
+ "libVersion": "3.17.1"
+}
\ No newline at end of file
diff --git a/BeadQR/uploadCloudFunction.sh b/BeadQR/uploadCloudFunction.sh
new file mode 100644
index 0000000000000000000000000000000000000000..df311b39a9e52abb5444e47a84208134df1495b2
--- /dev/null
+++ b/BeadQR/uploadCloudFunction.sh
@@ -0,0 +1 @@
+${installPath} cloud functions deploy --e ${envId} --n quickstartFunctions --r --project ${projectPath}
\ No newline at end of file