Spaces:
Running
Running
上传 BeadQR 微信小程序项目源码
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +4 -0
- BeadQR/README.md +50 -0
- BeadQR/cloudfunctions/quickstartFunctions/config.json +9 -0
- BeadQR/cloudfunctions/quickstartFunctions/index.js +92 -0
- BeadQR/cloudfunctions/quickstartFunctions/package-lock.json +1157 -0
- BeadQR/cloudfunctions/quickstartFunctions/package.json +14 -0
- BeadQR/docs/promo/douwei-1080-cover.png +3 -0
- BeadQR/docs/promo/douwei-1080.mp4 +3 -0
- BeadQR/docs/promo/douwei-720-cover.png +3 -0
- BeadQR/docs/promo/douwei-720.mp4 +3 -0
- BeadQR/docs/video-script.md +116 -0
- BeadQR/docs/审核驳回处理-文案与步骤.md +58 -0
- BeadQR/miniprogram/app.js +14 -0
- BeadQR/miniprogram/app.json +19 -0
- BeadQR/miniprogram/app.wxss +37 -0
- BeadQR/miniprogram/assets/icon.png +0 -0
- BeadQR/miniprogram/assets/miniprogram-code.png +0 -0
- BeadQR/miniprogram/assets/shield.svg +1 -0
- BeadQR/miniprogram/components/privacy-popup/index.js +40 -0
- BeadQR/miniprogram/components/privacy-popup/index.json +4 -0
- BeadQR/miniprogram/components/privacy-popup/index.wxml +13 -0
- BeadQR/miniprogram/components/privacy-popup/index.wxss +10 -0
- BeadQR/miniprogram/pages/index/index.js +1073 -0
- BeadQR/miniprogram/pages/index/index.json +6 -0
- BeadQR/miniprogram/pages/index/index.wxml +403 -0
- BeadQR/miniprogram/pages/index/index.wxss +828 -0
- BeadQR/miniprogram/sitemap.json +7 -0
- BeadQR/miniprogram/utils/color.js +46 -0
- BeadQR/miniprogram/utils/engine.js +225 -0
- BeadQR/miniprogram/utils/imaging.js +95 -0
- BeadQR/miniprogram/utils/intro.js +703 -0
- BeadQR/miniprogram/utils/palette-data.js +48 -0
- BeadQR/miniprogram/utils/palette.js +74 -0
- BeadQR/miniprogram/utils/pattern.js +381 -0
- BeadQR/miniprogram/utils/pdf.js +373 -0
- BeadQR/miniprogram/utils/qr.js +58 -0
- BeadQR/miniprogram/utils/qrcore/alignment-pattern.js +83 -0
- BeadQR/miniprogram/utils/qrcore/alphanumeric-data.js +59 -0
- BeadQR/miniprogram/utils/qrcore/bit-buffer.js +37 -0
- BeadQR/miniprogram/utils/qrcore/bit-matrix.js +65 -0
- BeadQR/miniprogram/utils/qrcore/byte-data.js +61 -0
- BeadQR/miniprogram/utils/qrcore/dijkstrajs.js +165 -0
- BeadQR/miniprogram/utils/qrcore/error-correction-code.js +135 -0
- BeadQR/miniprogram/utils/qrcore/error-correction-level.js +50 -0
- BeadQR/miniprogram/utils/qrcore/finder-pattern.js +22 -0
- BeadQR/miniprogram/utils/qrcore/format-info.js +29 -0
- BeadQR/miniprogram/utils/qrcore/galois-field.js +69 -0
- BeadQR/miniprogram/utils/qrcore/kanji-data.js +54 -0
- BeadQR/miniprogram/utils/qrcore/mask-pattern.js +234 -0
- BeadQR/miniprogram/utils/qrcore/mode.js +167 -0
.gitattributes
CHANGED
|
@@ -52,3 +52,7 @@ img/memes.png filter=lfs diff=lfs merge=lfs -text
|
|
| 52 |
普通拼豆作品展示.MOV filter=lfs diff=lfs merge=lfs -text
|
| 53 |
烫好了,还没有揭开烫纸,用微信二维码扫一扫,成功扫码.MP4 filter=lfs diff=lfs merge=lfs -text
|
| 54 |
用烫豆机对拼好的作品烫.MOV filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
普通拼豆作品展示.MOV filter=lfs diff=lfs merge=lfs -text
|
| 53 |
烫好了,还没有揭开烫纸,用微信二维码扫一扫,成功扫码.MP4 filter=lfs diff=lfs merge=lfs -text
|
| 54 |
用烫豆机对拼好的作品烫.MOV filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
BeadQR/docs/promo/douwei-1080-cover.png filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
BeadQR/docs/promo/douwei-1080.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
BeadQR/docs/promo/douwei-720-cover.png filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
BeadQR/docs/promo/douwei-720.mp4 filter=lfs diff=lfs merge=lfs -text
|
BeadQR/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 豆维 · 可扫码的拼豆
|
| 2 |
+
|
| 3 |
+
输入一句话(可以再选一张照片),生成**用微信扫一扫就能读出内容**的 MARD 拼豆图纸,并支持导出 PDF 或图片。
|
| 4 |
+
|
| 5 |
+
## 本版重点
|
| 6 |
+
|
| 7 |
+
- **一眼看懂**:首屏直接说明用途,页面按“写一句话 → 选张照片 → 导出图纸”三步走,打开就能看到示例效果。
|
| 8 |
+
- **更快**:配色查表加速,色格数据不再走页面数据通道;PDF 改为矢量直出(毫秒级,文件更小);导出时不再重复绘制。
|
| 9 |
+
- **更好看、更好扫**:
|
| 10 |
+
- 感知更准的配色(OKLab)
|
| 11 |
+
- 照片按面积取平均色,不再有锯齿
|
| 12 |
+
- 相邻色块自动平滑,零散颜色自动合并,所需豆子种类更少
|
| 13 |
+
- 二维码定位图形强制高对比
|
| 14 |
+
- 本机扫码自检:扫不出时一键优化
|
| 15 |
+
- **打印友好**:PDF 为 A4 矢量图纸,大作品会自动分块并附总览页;每一格都印有色号,另附带用量的清单。
|
| 16 |
+
- **精选色板**:除标准 221 色和完整 291 色外,还提供 144/96/72/48/24 色精选套装,方便少买豆。
|
| 17 |
+
|
| 18 |
+
## 内容安全链路(按微信平台要求)
|
| 19 |
+
|
| 20 |
+
| 环节 | 措施 |
|
| 21 |
+
| --- | --- |
|
| 22 |
+
| 二维码文字、扫码导入的文字、水印 | 云函数调用 `security.msgSecCheck` v2(带 openid、按场景值检测),结果为 risky 或 review 均拦截 |
|
| 23 |
+
| 照片 | 选图后立即压缩到官方推荐的 750×1334 以内,由云函数调用 `security.imgSecCheck`;**检测通过前不能应用照片** |
|
| 24 |
+
| 检测异常 | 一律视为未通过,不允许生成与导出(fail-closed) |
|
| 25 |
+
| 导出前 | 再次校验全部文本(已通过的走缓存,不增加等待) |
|
| 26 |
+
| 云函数加固 | 必须有 OPENID;只接受 `sec-check/` 目录下的 jpg;通过文件头判断格式;限制 1MB;检测后立即删除云端临时文件 |
|
| 27 |
+
| 隐私 | 已开启 `__usePrivacyCheck__`,内置隐私授权弹窗;页脚设有“隐私保护指引”入口 |
|
| 28 |
+
| 投诉与反馈 | 页脚提供“内容规范”说明及 `open-type="feedback"` 的投诉反馈入口 |
|
| 29 |
+
| 分享 | 分享出去的只是小程序卡片,不带用户内容 |
|
| 30 |
+
|
| 31 |
+
## 部署步骤
|
| 32 |
+
|
| 33 |
+
1. 用微信开发者工具打开本目录(appid 已配置)。
|
| 34 |
+
2. 右键 `cloudfunctions/quickstartFunctions` →「上传并部署:云端安装依赖」。**本版云函数有改动,必须重新部署。**
|
| 35 |
+
3. 在云开发控制台 → 存储中确认可以写入 `sec-check/` 目录(默认权限即可)。
|
| 36 |
+
4. 在小程序管理后台 →「设置 → 服务内容声明 → 用户隐私保护指引」中声明:选择图片/拍照(`chooseMedia`)、保存到相册(`saveImageToPhotosAlbum`)、扫码(`scanCode`)。
|
| 37 |
+
5. 真机测试:正常/违规文字、正常/违规图片、断网时的检测异常,确认都会被正确拦截和提示。
|
| 38 |
+
|
| 39 |
+
## 说明
|
| 40 |
+
|
| 41 |
+
- 为保证扫码成功率,**二维码默认保留 1 格白边**。如需原来的“无白边”效果,可在「高级设置 → 白边」中选择“无”。
|
| 42 |
+
- 圆豆、中空豆样式只影响图纸的显示效果,实际拼出来的位置与方豆完全一致。
|
| 43 |
+
- 第三方代码:`utils/qrcore`(node-qrcode,MIT)、`utils/vendor/jsqr.js`(jsQR,Apache-2.0)。
|
| 44 |
+
|
| 45 |
+
## 首屏演示动画 & 宣传视频
|
| 46 |
+
|
| 47 |
+
- 首次打开(还没写内容)时,预览区播放 12 秒 1:1 演示动画:写一句话 → 编成二维码 → 融进照片 → 微信一扫 → 结尾卡片。
|
| 48 |
+
- 动画由 `miniprogram/utils/intro.js` 纯代码实时绘制,**不含任何视频/图片素材,几乎不占包体积**(约 20KB)。演示作品由真实配色引擎生成,画面里的二维码真的能扫出 “Happy Birthday!”。
|
| 49 |
+
- 滑出屏幕、切到后台、打开裁剪时自动暂停;点一下预览区重新播放;写下内容(或选了照片)后自动切回真实二维码。
|
| 50 |
+
- `docs/promo/` 里是用同一套代码渲染的宣传视频(1080 / 720,含原创合成配乐)和封面图,可直接发公众号。该目录不在 `miniprogram/` 内,不计入小程序包体积。
|
BeadQR/cloudfunctions/quickstartFunctions/config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timeout": 20,
|
| 3 |
+
"permissions": {
|
| 4 |
+
"openapi": [
|
| 5 |
+
"security.msgSecCheck",
|
| 6 |
+
"security.imgSecCheck"
|
| 7 |
+
]
|
| 8 |
+
}
|
| 9 |
+
}
|
BeadQR/cloudfunctions/quickstartFunctions/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 豆维 · 内容安全云函数
|
| 2 |
+
// type: checkMessage | checkMessages | checkImage
|
| 3 |
+
// 返回约定:{ risk: boolean, skipped?: boolean };skipped 表示检测未能完成,前端必须拦截
|
| 4 |
+
const cloud = require("wx-server-sdk");
|
| 5 |
+
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
|
| 6 |
+
|
| 7 |
+
const MAX_TEXT = 2500;
|
| 8 |
+
const MAX_IMAGE_BYTES = 1024 * 1024;
|
| 9 |
+
const MAX_BATCH = 5;
|
| 10 |
+
|
| 11 |
+
const extractErrCode = (error) => {
|
| 12 |
+
if (!error) return undefined;
|
| 13 |
+
if (typeof error.errCode === "number") return error.errCode;
|
| 14 |
+
if (typeof error.code === "number") return error.code;
|
| 15 |
+
const match = typeof error.errMsg === "string" ? error.errMsg.match(/-?\d{3,}/) : null;
|
| 16 |
+
return match ? Number(match[0]) : undefined;
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
// msgSecCheck v2:scene 1 资料 / 2 评论 / 3 论坛 / 4 社交日志
|
| 20 |
+
async function checkOneMessage(content, scene, openid) {
|
| 21 |
+
const value = String(content || "").slice(0, MAX_TEXT);
|
| 22 |
+
if (!value.trim()) return { risk: false };
|
| 23 |
+
const safeScene = [1, 2, 3, 4].includes(Number(scene)) ? Number(scene) : 2;
|
| 24 |
+
try {
|
| 25 |
+
const response = await cloud.openapi.security.msgSecCheck({
|
| 26 |
+
version: 2,
|
| 27 |
+
scene: safeScene,
|
| 28 |
+
openid,
|
| 29 |
+
content: value,
|
| 30 |
+
});
|
| 31 |
+
const suggest = response && response.result && response.result.suggest;
|
| 32 |
+
if (!suggest) return { risk: false, skipped: true };
|
| 33 |
+
// review(疑似)按违规处理,宁严勿松
|
| 34 |
+
return { risk: suggest === "risky" || suggest === "review", verdict: suggest };
|
| 35 |
+
} catch (e) {
|
| 36 |
+
if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" };
|
| 37 |
+
return { risk: false, skipped: true, errCode: extractErrCode(e) };
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// 通过文件头判断真实格式,不信任客户端传入的类型
|
| 42 |
+
function sniffType(buffer) {
|
| 43 |
+
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return "image/jpeg";
|
| 44 |
+
if (buffer.length > 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return "image/png";
|
| 45 |
+
return "";
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
async function checkImage(fileID) {
|
| 49 |
+
// 只允许检测本小程序上传到 sec-check/ 目录的临时文件,防止借接口删除/读取其他文件
|
| 50 |
+
if (typeof fileID !== "string" || !/^cloud:\/\/[^/]+\/sec-check\/[\w.-]+\.jpg$/.test(fileID)) {
|
| 51 |
+
return { risk: false, skipped: true, reason: "INVALID_FILE" };
|
| 52 |
+
}
|
| 53 |
+
try {
|
| 54 |
+
const download = await cloud.downloadFile({ fileID });
|
| 55 |
+
const buffer = download && download.fileContent;
|
| 56 |
+
const contentType = buffer ? sniffType(buffer) : "";
|
| 57 |
+
if (!buffer || !buffer.length || buffer.length > MAX_IMAGE_BYTES || !contentType) {
|
| 58 |
+
return { risk: false, skipped: true, reason: "BAD_IMAGE" };
|
| 59 |
+
}
|
| 60 |
+
await cloud.openapi.security.imgSecCheck({
|
| 61 |
+
media: { contentType, value: buffer },
|
| 62 |
+
});
|
| 63 |
+
return { risk: false };
|
| 64 |
+
} catch (e) {
|
| 65 |
+
if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" };
|
| 66 |
+
return { risk: false, skipped: true, errCode: extractErrCode(e) };
|
| 67 |
+
} finally {
|
| 68 |
+
try {
|
| 69 |
+
await cloud.deleteFile({ fileList: [fileID] });
|
| 70 |
+
} catch (e) {
|
| 71 |
+
// 清理失败不影响检测结果
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
exports.main = async (event) => {
|
| 77 |
+
const { OPENID } = cloud.getWXContext();
|
| 78 |
+
if (!OPENID) return { risk: false, skipped: true, reason: "NO_OPENID" };
|
| 79 |
+
const type = event && event.type;
|
| 80 |
+
if (type === "checkMessage") {
|
| 81 |
+
return checkOneMessage(event.content, event.scene, OPENID);
|
| 82 |
+
}
|
| 83 |
+
if (type === "checkMessages") {
|
| 84 |
+
const items = Array.isArray(event.items) ? event.items.slice(0, MAX_BATCH) : [];
|
| 85 |
+
const results = await Promise.all(items.map((it) => checkOneMessage(it && it.value, it && it.scene, OPENID)));
|
| 86 |
+
return { results };
|
| 87 |
+
}
|
| 88 |
+
if (type === "checkImage") {
|
| 89 |
+
return checkImage(event.fileID);
|
| 90 |
+
}
|
| 91 |
+
return { risk: false, skipped: true, reason: "UNKNOWN_TYPE" };
|
| 92 |
+
};
|
BeadQR/cloudfunctions/quickstartFunctions/package-lock.json
ADDED
|
@@ -0,0 +1,1157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "quickstartFunctions",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "quickstartFunctions",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"license": "ISC",
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"wx-server-sdk": "~4.0.2"
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
"node_modules/@cloudbase/adapter-interface": {
|
| 16 |
+
"version": "0.7.1",
|
| 17 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/adapter-interface/-/adapter-interface-0.7.1.tgz",
|
| 18 |
+
"integrity": "sha512-BjHTAvIMSLxbp26SeT6qLiSSBygz3kAVno/PMwwBwjgzVkVUvuZGVcvf1mhHxY/BjdRGLrd6X60OgvymV/aAUw==",
|
| 19 |
+
"license": "ISC"
|
| 20 |
+
},
|
| 21 |
+
"node_modules/@cloudbase/adapter-wx_mp": {
|
| 22 |
+
"version": "1.3.1",
|
| 23 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/adapter-wx_mp/-/adapter-wx_mp-1.3.1.tgz",
|
| 24 |
+
"integrity": "sha512-rS0N1aD1a0zc421ds17/HzM4RChQ6T2ixMz87bhPJ1yveqJt/KUqYxiXJDWhZ803wwTmJs8aqNkgugsNusNhbg==",
|
| 25 |
+
"license": "ISC",
|
| 26 |
+
"dependencies": {
|
| 27 |
+
"@cloudbase/adapter-interface": "^0.7.0",
|
| 28 |
+
"web-streams-polyfill": "^4.0.0"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"node_modules/@cloudbase/ai": {
|
| 32 |
+
"version": "2.30.0",
|
| 33 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/ai/-/ai-2.30.0.tgz",
|
| 34 |
+
"integrity": "sha512-RtBux47omXWcMhiiDDVJ96d2JlqnQ/bzKC6GG3ROgupYG3ReQyv1IssCpg5cnzmZ+l651IqdOpt27VTuO3kEGQ==",
|
| 35 |
+
"license": "Apache-2.0",
|
| 36 |
+
"dependencies": {
|
| 37 |
+
"@cloudbase/types": "2.30.0",
|
| 38 |
+
"@cloudbase/utilities": "2.30.0",
|
| 39 |
+
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
| 40 |
+
"text-encoding-shim": "^1.0.5",
|
| 41 |
+
"web-streams-polyfill": "^4.0.0"
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"node_modules/@cloudbase/ai/node_modules/@cloudbase/types": {
|
| 45 |
+
"version": "2.30.0",
|
| 46 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz",
|
| 47 |
+
"integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==",
|
| 48 |
+
"license": "Apache-2.0",
|
| 49 |
+
"dependencies": {
|
| 50 |
+
"@cloudbase/adapter-interface": "^0.7.1"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"node_modules/@cloudbase/app": {
|
| 54 |
+
"version": "2.32.0",
|
| 55 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/app/-/app-2.32.0.tgz",
|
| 56 |
+
"integrity": "sha512-78+eSo8OLO4iUiifdynZo4dK4wNlyaz5x9423muSibyl4FLwt5/Xt0t9s7StFmGaBdvB75UYM9dGWxOdIFRzaw==",
|
| 57 |
+
"license": "Apache-2.0",
|
| 58 |
+
"dependencies": {
|
| 59 |
+
"@cloudbase/adapter-interface": "^0.7.1",
|
| 60 |
+
"@cloudbase/adapter-wx_mp": "^1.3.1",
|
| 61 |
+
"@cloudbase/types": "2.32.0",
|
| 62 |
+
"@cloudbase/utilities": "2.32.0"
|
| 63 |
+
},
|
| 64 |
+
"peerDependencies": {
|
| 65 |
+
"@cloudbase/signature-nodejs": ">=1.0.0",
|
| 66 |
+
"jsonwebtoken": ">=8.0.0",
|
| 67 |
+
"ws": "^8.18.0"
|
| 68 |
+
},
|
| 69 |
+
"peerDependenciesMeta": {
|
| 70 |
+
"@cloudbase/signature-nodejs": {
|
| 71 |
+
"optional": true
|
| 72 |
+
},
|
| 73 |
+
"jsonwebtoken": {
|
| 74 |
+
"optional": true
|
| 75 |
+
},
|
| 76 |
+
"ws": {
|
| 77 |
+
"optional": true
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"node_modules/@cloudbase/app/node_modules/@cloudbase/utilities": {
|
| 82 |
+
"version": "2.32.0",
|
| 83 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.32.0.tgz",
|
| 84 |
+
"integrity": "sha512-vYrr1aXOpU+/IrkHMSTxi/k2d4N8raNu+E3IrKtWt4w7Y0Ke3DLvCHeUJcuWECvQBVhzmg1ert4DcXVUymSKlg==",
|
| 85 |
+
"license": "Apache-2.0",
|
| 86 |
+
"dependencies": {
|
| 87 |
+
"@cloudbase/adapter-interface": "^0.7.1",
|
| 88 |
+
"@cloudbase/types": "2.32.0",
|
| 89 |
+
"jwt-decode": "^3.1.2"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"node_modules/@cloudbase/database": {
|
| 93 |
+
"version": "1.4.3",
|
| 94 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/database/-/database-1.4.3.tgz",
|
| 95 |
+
"integrity": "sha512-JzmdsGjy9LwzSQQ0Fv4OlNHNK61BRXdJSembTpy4408AQA3q2Ip3N5eB3v9OpAlFMvP/6MGmOQ/ZuUPKfJddJA==",
|
| 96 |
+
"license": "ISC",
|
| 97 |
+
"dependencies": {
|
| 98 |
+
"bson": "^4.0.3",
|
| 99 |
+
"lodash.clonedeep": "4.5.0",
|
| 100 |
+
"lodash.set": "4.3.2",
|
| 101 |
+
"lodash.unset": "4.5.2"
|
| 102 |
+
}
|
| 103 |
+
},
|
| 104 |
+
"node_modules/@cloudbase/node-sdk": {
|
| 105 |
+
"version": "3.17.2",
|
| 106 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/node-sdk/-/node-sdk-3.17.2.tgz",
|
| 107 |
+
"integrity": "sha512-74ekvRsdmeGch9hFHkQnjHPTEfk8k09iQFvI1aAo48ZsbLqK3czqj/3GoVpSkw5+eYe05tZV13Ct/kWiV5tuGg==",
|
| 108 |
+
"license": "MIT",
|
| 109 |
+
"dependencies": {
|
| 110 |
+
"@cloudbase/ai": "^2.23.0",
|
| 111 |
+
"@cloudbase/app": "^2.23.0",
|
| 112 |
+
"@cloudbase/database": "1.4.3",
|
| 113 |
+
"@cloudbase/signature-nodejs": "2.2.0",
|
| 114 |
+
"@cloudbase/types": "^2.23.0",
|
| 115 |
+
"@cloudbase/wx-cloud-client-sdk": "1.7.1",
|
| 116 |
+
"agentkeepalive": "^4.3.0",
|
| 117 |
+
"axios": "0.27.2",
|
| 118 |
+
"form-data": "^4.0.0",
|
| 119 |
+
"http-proxy-agent": "^5.0.0",
|
| 120 |
+
"https-proxy-agent": "^5.0.1",
|
| 121 |
+
"jsonwebtoken": "^9.0.2",
|
| 122 |
+
"retry": "^0.13.1",
|
| 123 |
+
"web-streams-polyfill": "^4.2.0",
|
| 124 |
+
"xml2js": "^0.6.2"
|
| 125 |
+
},
|
| 126 |
+
"engines": {
|
| 127 |
+
"node": ">=12"
|
| 128 |
+
}
|
| 129 |
+
},
|
| 130 |
+
"node_modules/@cloudbase/signature-nodejs": {
|
| 131 |
+
"version": "2.2.0",
|
| 132 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/signature-nodejs/-/signature-nodejs-2.2.0.tgz",
|
| 133 |
+
"integrity": "sha512-2y2rieOpJNnBA2MW7sLJKek3iFOjgr8TNicw5qjBkcdVczLR0cFzxMhip75NLCwarFTIAONE3oeNxoQ19eVHcA==",
|
| 134 |
+
"dependencies": {
|
| 135 |
+
"@types/clone": "^2.1.0",
|
| 136 |
+
"clone": "^2.1.2",
|
| 137 |
+
"is-stream": "^2.0.0",
|
| 138 |
+
"url": "^0.11.0"
|
| 139 |
+
}
|
| 140 |
+
},
|
| 141 |
+
"node_modules/@cloudbase/types": {
|
| 142 |
+
"version": "2.32.0",
|
| 143 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.32.0.tgz",
|
| 144 |
+
"integrity": "sha512-HPsMEHHt5SAyD3tpv/C/v2nYudSawE48ixjK5Y9w3R/82auJag0AUsZGfCExgdPa4Zud9OvGx5127ssEXLgadw==",
|
| 145 |
+
"license": "Apache-2.0",
|
| 146 |
+
"dependencies": {
|
| 147 |
+
"@cloudbase/adapter-interface": "^0.7.1"
|
| 148 |
+
}
|
| 149 |
+
},
|
| 150 |
+
"node_modules/@cloudbase/utilities": {
|
| 151 |
+
"version": "2.30.0",
|
| 152 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.30.0.tgz",
|
| 153 |
+
"integrity": "sha512-52DfQH5qgfOtrwj26KCGw6cz0pUjuJludJ7fccUKCzBtn/QwPudwtXo7gOUTtKKDubW3+VUCjjBm7UiDwFu+ag==",
|
| 154 |
+
"license": "Apache-2.0",
|
| 155 |
+
"dependencies": {
|
| 156 |
+
"@cloudbase/adapter-interface": "^0.7.1",
|
| 157 |
+
"@cloudbase/types": "2.30.0",
|
| 158 |
+
"jwt-decode": "^3.1.2"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"node_modules/@cloudbase/utilities/node_modules/@cloudbase/types": {
|
| 162 |
+
"version": "2.30.0",
|
| 163 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz",
|
| 164 |
+
"integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==",
|
| 165 |
+
"license": "Apache-2.0",
|
| 166 |
+
"dependencies": {
|
| 167 |
+
"@cloudbase/adapter-interface": "^0.7.1"
|
| 168 |
+
}
|
| 169 |
+
},
|
| 170 |
+
"node_modules/@cloudbase/wx-cloud-client-sdk": {
|
| 171 |
+
"version": "1.7.1",
|
| 172 |
+
"resolved": "https://registry.npmjs.org/@cloudbase/wx-cloud-client-sdk/-/wx-cloud-client-sdk-1.7.1.tgz",
|
| 173 |
+
"integrity": "sha512-NzLOoYDAeoRh2lRjCtvr6yXu4KPVHqZtPxyv70e+3b2+dZIDE5S5gtU8daE7xS5u4TwPqKB/E2vmgRTYOsCS9w==",
|
| 174 |
+
"dependencies": {
|
| 175 |
+
"core-js-pure": "^3.45.0"
|
| 176 |
+
}
|
| 177 |
+
},
|
| 178 |
+
"node_modules/@mattiasbuelens/web-streams-adapter": {
|
| 179 |
+
"version": "0.1.0",
|
| 180 |
+
"resolved": "https://registry.npmjs.org/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz",
|
| 181 |
+
"integrity": "sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA==",
|
| 182 |
+
"license": "MIT",
|
| 183 |
+
"engines": {
|
| 184 |
+
"node": ">= 12"
|
| 185 |
+
}
|
| 186 |
+
},
|
| 187 |
+
"node_modules/@protobufjs/aspromise": {
|
| 188 |
+
"version": "1.1.2",
|
| 189 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
| 190 |
+
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
| 191 |
+
"license": "BSD-3-Clause"
|
| 192 |
+
},
|
| 193 |
+
"node_modules/@protobufjs/base64": {
|
| 194 |
+
"version": "1.1.2",
|
| 195 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
| 196 |
+
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
| 197 |
+
"license": "BSD-3-Clause"
|
| 198 |
+
},
|
| 199 |
+
"node_modules/@protobufjs/codegen": {
|
| 200 |
+
"version": "2.0.5",
|
| 201 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
| 202 |
+
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
|
| 203 |
+
"license": "BSD-3-Clause"
|
| 204 |
+
},
|
| 205 |
+
"node_modules/@protobufjs/eventemitter": {
|
| 206 |
+
"version": "1.1.1",
|
| 207 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
|
| 208 |
+
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
|
| 209 |
+
"license": "BSD-3-Clause"
|
| 210 |
+
},
|
| 211 |
+
"node_modules/@protobufjs/fetch": {
|
| 212 |
+
"version": "1.1.1",
|
| 213 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
|
| 214 |
+
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
|
| 215 |
+
"license": "BSD-3-Clause",
|
| 216 |
+
"dependencies": {
|
| 217 |
+
"@protobufjs/aspromise": "^1.1.1"
|
| 218 |
+
}
|
| 219 |
+
},
|
| 220 |
+
"node_modules/@protobufjs/float": {
|
| 221 |
+
"version": "1.0.2",
|
| 222 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
| 223 |
+
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
| 224 |
+
"license": "BSD-3-Clause"
|
| 225 |
+
},
|
| 226 |
+
"node_modules/@protobufjs/path": {
|
| 227 |
+
"version": "1.1.2",
|
| 228 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
| 229 |
+
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
| 230 |
+
"license": "BSD-3-Clause"
|
| 231 |
+
},
|
| 232 |
+
"node_modules/@protobufjs/pool": {
|
| 233 |
+
"version": "1.1.0",
|
| 234 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
| 235 |
+
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
| 236 |
+
"license": "BSD-3-Clause"
|
| 237 |
+
},
|
| 238 |
+
"node_modules/@protobufjs/utf8": {
|
| 239 |
+
"version": "1.1.2",
|
| 240 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
|
| 241 |
+
"integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
|
| 242 |
+
"license": "BSD-3-Clause"
|
| 243 |
+
},
|
| 244 |
+
"node_modules/@tootallnate/once": {
|
| 245 |
+
"version": "2.0.1",
|
| 246 |
+
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
|
| 247 |
+
"integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==",
|
| 248 |
+
"license": "MIT",
|
| 249 |
+
"engines": {
|
| 250 |
+
"node": ">= 10"
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"node_modules/@types/clone": {
|
| 254 |
+
"version": "2.1.4",
|
| 255 |
+
"resolved": "https://registry.npmjs.org/@types/clone/-/clone-2.1.4.tgz",
|
| 256 |
+
"integrity": "sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==",
|
| 257 |
+
"license": "MIT"
|
| 258 |
+
},
|
| 259 |
+
"node_modules/@types/node": {
|
| 260 |
+
"version": "26.3.0",
|
| 261 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz",
|
| 262 |
+
"integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==",
|
| 263 |
+
"license": "MIT",
|
| 264 |
+
"dependencies": {
|
| 265 |
+
"undici-types": "~8.3.0"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"node_modules/agent-base": {
|
| 269 |
+
"version": "6.0.2",
|
| 270 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
| 271 |
+
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
| 272 |
+
"license": "MIT",
|
| 273 |
+
"dependencies": {
|
| 274 |
+
"debug": "4"
|
| 275 |
+
},
|
| 276 |
+
"engines": {
|
| 277 |
+
"node": ">= 6.0.0"
|
| 278 |
+
}
|
| 279 |
+
},
|
| 280 |
+
"node_modules/agentkeepalive": {
|
| 281 |
+
"version": "4.6.0",
|
| 282 |
+
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
| 283 |
+
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
| 284 |
+
"license": "MIT",
|
| 285 |
+
"dependencies": {
|
| 286 |
+
"humanize-ms": "^1.2.1"
|
| 287 |
+
},
|
| 288 |
+
"engines": {
|
| 289 |
+
"node": ">= 8.0.0"
|
| 290 |
+
}
|
| 291 |
+
},
|
| 292 |
+
"node_modules/asynckit": {
|
| 293 |
+
"version": "0.4.0",
|
| 294 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
| 295 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
| 296 |
+
"license": "MIT"
|
| 297 |
+
},
|
| 298 |
+
"node_modules/axios": {
|
| 299 |
+
"version": "0.27.2",
|
| 300 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
| 301 |
+
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
| 302 |
+
"license": "MIT",
|
| 303 |
+
"dependencies": {
|
| 304 |
+
"follow-redirects": "^1.14.9",
|
| 305 |
+
"form-data": "^4.0.0"
|
| 306 |
+
}
|
| 307 |
+
},
|
| 308 |
+
"node_modules/base64-js": {
|
| 309 |
+
"version": "1.5.1",
|
| 310 |
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
| 311 |
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
| 312 |
+
"funding": [
|
| 313 |
+
{
|
| 314 |
+
"type": "github",
|
| 315 |
+
"url": "https://github.com/sponsors/feross"
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"type": "patreon",
|
| 319 |
+
"url": "https://www.patreon.com/feross"
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
"type": "consulting",
|
| 323 |
+
"url": "https://feross.org/support"
|
| 324 |
+
}
|
| 325 |
+
],
|
| 326 |
+
"license": "MIT"
|
| 327 |
+
},
|
| 328 |
+
"node_modules/bignumber.js": {
|
| 329 |
+
"version": "9.3.1",
|
| 330 |
+
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
| 331 |
+
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
| 332 |
+
"license": "MIT",
|
| 333 |
+
"engines": {
|
| 334 |
+
"node": "*"
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
"node_modules/bson": {
|
| 338 |
+
"version": "4.7.2",
|
| 339 |
+
"resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz",
|
| 340 |
+
"integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==",
|
| 341 |
+
"license": "Apache-2.0",
|
| 342 |
+
"dependencies": {
|
| 343 |
+
"buffer": "^5.6.0"
|
| 344 |
+
},
|
| 345 |
+
"engines": {
|
| 346 |
+
"node": ">=6.9.0"
|
| 347 |
+
}
|
| 348 |
+
},
|
| 349 |
+
"node_modules/buffer": {
|
| 350 |
+
"version": "5.7.1",
|
| 351 |
+
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
| 352 |
+
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
| 353 |
+
"funding": [
|
| 354 |
+
{
|
| 355 |
+
"type": "github",
|
| 356 |
+
"url": "https://github.com/sponsors/feross"
|
| 357 |
+
},
|
| 358 |
+
{
|
| 359 |
+
"type": "patreon",
|
| 360 |
+
"url": "https://www.patreon.com/feross"
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"type": "consulting",
|
| 364 |
+
"url": "https://feross.org/support"
|
| 365 |
+
}
|
| 366 |
+
],
|
| 367 |
+
"license": "MIT",
|
| 368 |
+
"dependencies": {
|
| 369 |
+
"base64-js": "^1.3.1",
|
| 370 |
+
"ieee754": "^1.1.13"
|
| 371 |
+
}
|
| 372 |
+
},
|
| 373 |
+
"node_modules/buffer-equal-constant-time": {
|
| 374 |
+
"version": "1.0.1",
|
| 375 |
+
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
| 376 |
+
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
| 377 |
+
"license": "BSD-3-Clause"
|
| 378 |
+
},
|
| 379 |
+
"node_modules/call-bind-apply-helpers": {
|
| 380 |
+
"version": "1.0.2",
|
| 381 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 382 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 383 |
+
"license": "MIT",
|
| 384 |
+
"dependencies": {
|
| 385 |
+
"es-errors": "^1.3.0",
|
| 386 |
+
"function-bind": "^1.1.2"
|
| 387 |
+
},
|
| 388 |
+
"engines": {
|
| 389 |
+
"node": ">= 0.4"
|
| 390 |
+
}
|
| 391 |
+
},
|
| 392 |
+
"node_modules/call-bound": {
|
| 393 |
+
"version": "1.0.4",
|
| 394 |
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
| 395 |
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
| 396 |
+
"license": "MIT",
|
| 397 |
+
"dependencies": {
|
| 398 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 399 |
+
"get-intrinsic": "^1.3.0"
|
| 400 |
+
},
|
| 401 |
+
"engines": {
|
| 402 |
+
"node": ">= 0.4"
|
| 403 |
+
},
|
| 404 |
+
"funding": {
|
| 405 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 406 |
+
}
|
| 407 |
+
},
|
| 408 |
+
"node_modules/clone": {
|
| 409 |
+
"version": "2.1.2",
|
| 410 |
+
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
| 411 |
+
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
| 412 |
+
"license": "MIT",
|
| 413 |
+
"engines": {
|
| 414 |
+
"node": ">=0.8"
|
| 415 |
+
}
|
| 416 |
+
},
|
| 417 |
+
"node_modules/combined-stream": {
|
| 418 |
+
"version": "1.0.8",
|
| 419 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
| 420 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
| 421 |
+
"license": "MIT",
|
| 422 |
+
"dependencies": {
|
| 423 |
+
"delayed-stream": "~1.0.0"
|
| 424 |
+
},
|
| 425 |
+
"engines": {
|
| 426 |
+
"node": ">= 0.8"
|
| 427 |
+
}
|
| 428 |
+
},
|
| 429 |
+
"node_modules/core-js-pure": {
|
| 430 |
+
"version": "3.50.0",
|
| 431 |
+
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz",
|
| 432 |
+
"integrity": "sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==",
|
| 433 |
+
"hasInstallScript": true,
|
| 434 |
+
"license": "MIT",
|
| 435 |
+
"engines": {
|
| 436 |
+
"node": "*"
|
| 437 |
+
},
|
| 438 |
+
"funding": {
|
| 439 |
+
"type": "opencollective",
|
| 440 |
+
"url": "https://opencollective.com/core-js"
|
| 441 |
+
}
|
| 442 |
+
},
|
| 443 |
+
"node_modules/debug": {
|
| 444 |
+
"version": "4.4.3",
|
| 445 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 446 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 447 |
+
"license": "MIT",
|
| 448 |
+
"dependencies": {
|
| 449 |
+
"ms": "^2.1.3"
|
| 450 |
+
},
|
| 451 |
+
"engines": {
|
| 452 |
+
"node": ">=6.0"
|
| 453 |
+
},
|
| 454 |
+
"peerDependenciesMeta": {
|
| 455 |
+
"supports-color": {
|
| 456 |
+
"optional": true
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
},
|
| 460 |
+
"node_modules/delayed-stream": {
|
| 461 |
+
"version": "1.0.0",
|
| 462 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
| 463 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
| 464 |
+
"license": "MIT",
|
| 465 |
+
"engines": {
|
| 466 |
+
"node": ">=0.4.0"
|
| 467 |
+
}
|
| 468 |
+
},
|
| 469 |
+
"node_modules/dunder-proto": {
|
| 470 |
+
"version": "1.0.1",
|
| 471 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 472 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 473 |
+
"license": "MIT",
|
| 474 |
+
"dependencies": {
|
| 475 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 476 |
+
"es-errors": "^1.3.0",
|
| 477 |
+
"gopd": "^1.2.0"
|
| 478 |
+
},
|
| 479 |
+
"engines": {
|
| 480 |
+
"node": ">= 0.4"
|
| 481 |
+
}
|
| 482 |
+
},
|
| 483 |
+
"node_modules/ecdsa-sig-formatter": {
|
| 484 |
+
"version": "1.0.11",
|
| 485 |
+
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
| 486 |
+
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
| 487 |
+
"license": "Apache-2.0",
|
| 488 |
+
"dependencies": {
|
| 489 |
+
"safe-buffer": "^5.0.1"
|
| 490 |
+
}
|
| 491 |
+
},
|
| 492 |
+
"node_modules/es-define-property": {
|
| 493 |
+
"version": "1.0.1",
|
| 494 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 495 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 496 |
+
"license": "MIT",
|
| 497 |
+
"engines": {
|
| 498 |
+
"node": ">= 0.4"
|
| 499 |
+
}
|
| 500 |
+
},
|
| 501 |
+
"node_modules/es-errors": {
|
| 502 |
+
"version": "1.3.0",
|
| 503 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 504 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 505 |
+
"license": "MIT",
|
| 506 |
+
"engines": {
|
| 507 |
+
"node": ">= 0.4"
|
| 508 |
+
}
|
| 509 |
+
},
|
| 510 |
+
"node_modules/es-object-atoms": {
|
| 511 |
+
"version": "1.1.2",
|
| 512 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
| 513 |
+
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
| 514 |
+
"license": "MIT",
|
| 515 |
+
"dependencies": {
|
| 516 |
+
"es-errors": "^1.3.0"
|
| 517 |
+
},
|
| 518 |
+
"engines": {
|
| 519 |
+
"node": ">= 0.4"
|
| 520 |
+
}
|
| 521 |
+
},
|
| 522 |
+
"node_modules/es-set-tostringtag": {
|
| 523 |
+
"version": "2.1.0",
|
| 524 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
| 525 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
| 526 |
+
"license": "MIT",
|
| 527 |
+
"dependencies": {
|
| 528 |
+
"es-errors": "^1.3.0",
|
| 529 |
+
"get-intrinsic": "^1.2.6",
|
| 530 |
+
"has-tostringtag": "^1.0.2",
|
| 531 |
+
"hasown": "^2.0.2"
|
| 532 |
+
},
|
| 533 |
+
"engines": {
|
| 534 |
+
"node": ">= 0.4"
|
| 535 |
+
}
|
| 536 |
+
},
|
| 537 |
+
"node_modules/follow-redirects": {
|
| 538 |
+
"version": "1.16.0",
|
| 539 |
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
| 540 |
+
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
| 541 |
+
"funding": [
|
| 542 |
+
{
|
| 543 |
+
"type": "individual",
|
| 544 |
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
| 545 |
+
}
|
| 546 |
+
],
|
| 547 |
+
"license": "MIT",
|
| 548 |
+
"engines": {
|
| 549 |
+
"node": ">=4.0"
|
| 550 |
+
},
|
| 551 |
+
"peerDependenciesMeta": {
|
| 552 |
+
"debug": {
|
| 553 |
+
"optional": true
|
| 554 |
+
}
|
| 555 |
+
}
|
| 556 |
+
},
|
| 557 |
+
"node_modules/form-data": {
|
| 558 |
+
"version": "4.0.6",
|
| 559 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
| 560 |
+
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
| 561 |
+
"license": "MIT",
|
| 562 |
+
"dependencies": {
|
| 563 |
+
"asynckit": "^0.4.0",
|
| 564 |
+
"combined-stream": "^1.0.8",
|
| 565 |
+
"es-set-tostringtag": "^2.1.0",
|
| 566 |
+
"hasown": "^2.0.4",
|
| 567 |
+
"mime-types": "^2.1.35"
|
| 568 |
+
},
|
| 569 |
+
"engines": {
|
| 570 |
+
"node": ">= 6"
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"node_modules/function-bind": {
|
| 574 |
+
"version": "1.1.2",
|
| 575 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 576 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 577 |
+
"license": "MIT",
|
| 578 |
+
"funding": {
|
| 579 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 580 |
+
}
|
| 581 |
+
},
|
| 582 |
+
"node_modules/get-intrinsic": {
|
| 583 |
+
"version": "1.3.0",
|
| 584 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 585 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 586 |
+
"license": "MIT",
|
| 587 |
+
"dependencies": {
|
| 588 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 589 |
+
"es-define-property": "^1.0.1",
|
| 590 |
+
"es-errors": "^1.3.0",
|
| 591 |
+
"es-object-atoms": "^1.1.1",
|
| 592 |
+
"function-bind": "^1.1.2",
|
| 593 |
+
"get-proto": "^1.0.1",
|
| 594 |
+
"gopd": "^1.2.0",
|
| 595 |
+
"has-symbols": "^1.1.0",
|
| 596 |
+
"hasown": "^2.0.2",
|
| 597 |
+
"math-intrinsics": "^1.1.0"
|
| 598 |
+
},
|
| 599 |
+
"engines": {
|
| 600 |
+
"node": ">= 0.4"
|
| 601 |
+
},
|
| 602 |
+
"funding": {
|
| 603 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 604 |
+
}
|
| 605 |
+
},
|
| 606 |
+
"node_modules/get-proto": {
|
| 607 |
+
"version": "1.0.1",
|
| 608 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 609 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 610 |
+
"license": "MIT",
|
| 611 |
+
"dependencies": {
|
| 612 |
+
"dunder-proto": "^1.0.1",
|
| 613 |
+
"es-object-atoms": "^1.0.0"
|
| 614 |
+
},
|
| 615 |
+
"engines": {
|
| 616 |
+
"node": ">= 0.4"
|
| 617 |
+
}
|
| 618 |
+
},
|
| 619 |
+
"node_modules/gopd": {
|
| 620 |
+
"version": "1.2.0",
|
| 621 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 622 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 623 |
+
"license": "MIT",
|
| 624 |
+
"engines": {
|
| 625 |
+
"node": ">= 0.4"
|
| 626 |
+
},
|
| 627 |
+
"funding": {
|
| 628 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 629 |
+
}
|
| 630 |
+
},
|
| 631 |
+
"node_modules/has-symbols": {
|
| 632 |
+
"version": "1.1.0",
|
| 633 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 634 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 635 |
+
"license": "MIT",
|
| 636 |
+
"engines": {
|
| 637 |
+
"node": ">= 0.4"
|
| 638 |
+
},
|
| 639 |
+
"funding": {
|
| 640 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 641 |
+
}
|
| 642 |
+
},
|
| 643 |
+
"node_modules/has-tostringtag": {
|
| 644 |
+
"version": "1.0.2",
|
| 645 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
| 646 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
| 647 |
+
"license": "MIT",
|
| 648 |
+
"dependencies": {
|
| 649 |
+
"has-symbols": "^1.0.3"
|
| 650 |
+
},
|
| 651 |
+
"engines": {
|
| 652 |
+
"node": ">= 0.4"
|
| 653 |
+
},
|
| 654 |
+
"funding": {
|
| 655 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 656 |
+
}
|
| 657 |
+
},
|
| 658 |
+
"node_modules/hasown": {
|
| 659 |
+
"version": "2.0.4",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
| 661 |
+
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
| 662 |
+
"license": "MIT",
|
| 663 |
+
"dependencies": {
|
| 664 |
+
"function-bind": "^1.1.2"
|
| 665 |
+
},
|
| 666 |
+
"engines": {
|
| 667 |
+
"node": ">= 0.4"
|
| 668 |
+
}
|
| 669 |
+
},
|
| 670 |
+
"node_modules/http-proxy-agent": {
|
| 671 |
+
"version": "5.0.0",
|
| 672 |
+
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
| 673 |
+
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
|
| 674 |
+
"license": "MIT",
|
| 675 |
+
"dependencies": {
|
| 676 |
+
"@tootallnate/once": "2",
|
| 677 |
+
"agent-base": "6",
|
| 678 |
+
"debug": "4"
|
| 679 |
+
},
|
| 680 |
+
"engines": {
|
| 681 |
+
"node": ">= 6"
|
| 682 |
+
}
|
| 683 |
+
},
|
| 684 |
+
"node_modules/https-proxy-agent": {
|
| 685 |
+
"version": "5.0.1",
|
| 686 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
| 687 |
+
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
| 688 |
+
"license": "MIT",
|
| 689 |
+
"dependencies": {
|
| 690 |
+
"agent-base": "6",
|
| 691 |
+
"debug": "4"
|
| 692 |
+
},
|
| 693 |
+
"engines": {
|
| 694 |
+
"node": ">= 6"
|
| 695 |
+
}
|
| 696 |
+
},
|
| 697 |
+
"node_modules/humanize-ms": {
|
| 698 |
+
"version": "1.2.1",
|
| 699 |
+
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
| 700 |
+
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
| 701 |
+
"license": "MIT",
|
| 702 |
+
"dependencies": {
|
| 703 |
+
"ms": "^2.0.0"
|
| 704 |
+
}
|
| 705 |
+
},
|
| 706 |
+
"node_modules/ieee754": {
|
| 707 |
+
"version": "1.2.1",
|
| 708 |
+
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
| 709 |
+
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
| 710 |
+
"funding": [
|
| 711 |
+
{
|
| 712 |
+
"type": "github",
|
| 713 |
+
"url": "https://github.com/sponsors/feross"
|
| 714 |
+
},
|
| 715 |
+
{
|
| 716 |
+
"type": "patreon",
|
| 717 |
+
"url": "https://www.patreon.com/feross"
|
| 718 |
+
},
|
| 719 |
+
{
|
| 720 |
+
"type": "consulting",
|
| 721 |
+
"url": "https://feross.org/support"
|
| 722 |
+
}
|
| 723 |
+
],
|
| 724 |
+
"license": "BSD-3-Clause"
|
| 725 |
+
},
|
| 726 |
+
"node_modules/is-stream": {
|
| 727 |
+
"version": "2.0.1",
|
| 728 |
+
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
| 729 |
+
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
| 730 |
+
"license": "MIT",
|
| 731 |
+
"engines": {
|
| 732 |
+
"node": ">=8"
|
| 733 |
+
},
|
| 734 |
+
"funding": {
|
| 735 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 736 |
+
}
|
| 737 |
+
},
|
| 738 |
+
"node_modules/json-bigint": {
|
| 739 |
+
"version": "1.0.0",
|
| 740 |
+
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
| 741 |
+
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
| 742 |
+
"license": "MIT",
|
| 743 |
+
"dependencies": {
|
| 744 |
+
"bignumber.js": "^9.0.0"
|
| 745 |
+
}
|
| 746 |
+
},
|
| 747 |
+
"node_modules/jsonwebtoken": {
|
| 748 |
+
"version": "9.0.3",
|
| 749 |
+
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
| 750 |
+
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
| 751 |
+
"license": "MIT",
|
| 752 |
+
"dependencies": {
|
| 753 |
+
"jws": "^4.0.1",
|
| 754 |
+
"lodash.includes": "^4.3.0",
|
| 755 |
+
"lodash.isboolean": "^3.0.3",
|
| 756 |
+
"lodash.isinteger": "^4.0.4",
|
| 757 |
+
"lodash.isnumber": "^3.0.3",
|
| 758 |
+
"lodash.isplainobject": "^4.0.6",
|
| 759 |
+
"lodash.isstring": "^4.0.1",
|
| 760 |
+
"lodash.once": "^4.0.0",
|
| 761 |
+
"ms": "^2.1.1",
|
| 762 |
+
"semver": "^7.5.4"
|
| 763 |
+
},
|
| 764 |
+
"engines": {
|
| 765 |
+
"node": ">=12",
|
| 766 |
+
"npm": ">=6"
|
| 767 |
+
}
|
| 768 |
+
},
|
| 769 |
+
"node_modules/jwa": {
|
| 770 |
+
"version": "2.0.1",
|
| 771 |
+
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
| 772 |
+
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
| 773 |
+
"license": "MIT",
|
| 774 |
+
"dependencies": {
|
| 775 |
+
"buffer-equal-constant-time": "^1.0.1",
|
| 776 |
+
"ecdsa-sig-formatter": "1.0.11",
|
| 777 |
+
"safe-buffer": "^5.0.1"
|
| 778 |
+
}
|
| 779 |
+
},
|
| 780 |
+
"node_modules/jws": {
|
| 781 |
+
"version": "4.0.1",
|
| 782 |
+
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
| 783 |
+
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
| 784 |
+
"license": "MIT",
|
| 785 |
+
"dependencies": {
|
| 786 |
+
"jwa": "^2.0.1",
|
| 787 |
+
"safe-buffer": "^5.0.1"
|
| 788 |
+
}
|
| 789 |
+
},
|
| 790 |
+
"node_modules/jwt-decode": {
|
| 791 |
+
"version": "3.1.2",
|
| 792 |
+
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
| 793 |
+
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==",
|
| 794 |
+
"license": "MIT"
|
| 795 |
+
},
|
| 796 |
+
"node_modules/lodash.clonedeep": {
|
| 797 |
+
"version": "4.5.0",
|
| 798 |
+
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
| 799 |
+
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
| 800 |
+
"license": "MIT"
|
| 801 |
+
},
|
| 802 |
+
"node_modules/lodash.includes": {
|
| 803 |
+
"version": "4.3.0",
|
| 804 |
+
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
| 805 |
+
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
| 806 |
+
"license": "MIT"
|
| 807 |
+
},
|
| 808 |
+
"node_modules/lodash.isboolean": {
|
| 809 |
+
"version": "3.0.3",
|
| 810 |
+
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
| 811 |
+
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
| 812 |
+
"license": "MIT"
|
| 813 |
+
},
|
| 814 |
+
"node_modules/lodash.isinteger": {
|
| 815 |
+
"version": "4.0.4",
|
| 816 |
+
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
| 817 |
+
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
| 818 |
+
"license": "MIT"
|
| 819 |
+
},
|
| 820 |
+
"node_modules/lodash.isnumber": {
|
| 821 |
+
"version": "3.0.3",
|
| 822 |
+
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
| 823 |
+
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
| 824 |
+
"license": "MIT"
|
| 825 |
+
},
|
| 826 |
+
"node_modules/lodash.isplainobject": {
|
| 827 |
+
"version": "4.0.6",
|
| 828 |
+
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
| 829 |
+
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
| 830 |
+
"license": "MIT"
|
| 831 |
+
},
|
| 832 |
+
"node_modules/lodash.isstring": {
|
| 833 |
+
"version": "4.0.1",
|
| 834 |
+
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
| 835 |
+
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
| 836 |
+
"license": "MIT"
|
| 837 |
+
},
|
| 838 |
+
"node_modules/lodash.once": {
|
| 839 |
+
"version": "4.1.1",
|
| 840 |
+
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
| 841 |
+
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
| 842 |
+
"license": "MIT"
|
| 843 |
+
},
|
| 844 |
+
"node_modules/lodash.set": {
|
| 845 |
+
"version": "4.3.2",
|
| 846 |
+
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
| 847 |
+
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
|
| 848 |
+
"license": "MIT"
|
| 849 |
+
},
|
| 850 |
+
"node_modules/lodash.unset": {
|
| 851 |
+
"version": "4.5.2",
|
| 852 |
+
"resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz",
|
| 853 |
+
"integrity": "sha512-bwKX88k2JhCV9D1vtE8+naDKlLiGrSmf8zi/Y9ivFHwbmRfA8RxS/aVJ+sIht2XOwqoNr4xUPUkGZpc1sHFEKg==",
|
| 854 |
+
"license": "MIT"
|
| 855 |
+
},
|
| 856 |
+
"node_modules/long": {
|
| 857 |
+
"version": "5.3.2",
|
| 858 |
+
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
| 859 |
+
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
| 860 |
+
"license": "Apache-2.0"
|
| 861 |
+
},
|
| 862 |
+
"node_modules/math-intrinsics": {
|
| 863 |
+
"version": "1.1.0",
|
| 864 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 865 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 866 |
+
"license": "MIT",
|
| 867 |
+
"engines": {
|
| 868 |
+
"node": ">= 0.4"
|
| 869 |
+
}
|
| 870 |
+
},
|
| 871 |
+
"node_modules/mime-db": {
|
| 872 |
+
"version": "1.52.0",
|
| 873 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 874 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 875 |
+
"license": "MIT",
|
| 876 |
+
"engines": {
|
| 877 |
+
"node": ">= 0.6"
|
| 878 |
+
}
|
| 879 |
+
},
|
| 880 |
+
"node_modules/mime-types": {
|
| 881 |
+
"version": "2.1.35",
|
| 882 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 883 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 884 |
+
"license": "MIT",
|
| 885 |
+
"dependencies": {
|
| 886 |
+
"mime-db": "1.52.0"
|
| 887 |
+
},
|
| 888 |
+
"engines": {
|
| 889 |
+
"node": ">= 0.6"
|
| 890 |
+
}
|
| 891 |
+
},
|
| 892 |
+
"node_modules/ms": {
|
| 893 |
+
"version": "2.1.3",
|
| 894 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 895 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 896 |
+
"license": "MIT"
|
| 897 |
+
},
|
| 898 |
+
"node_modules/object-inspect": {
|
| 899 |
+
"version": "1.13.4",
|
| 900 |
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
| 901 |
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
| 902 |
+
"license": "MIT",
|
| 903 |
+
"engines": {
|
| 904 |
+
"node": ">= 0.4"
|
| 905 |
+
},
|
| 906 |
+
"funding": {
|
| 907 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 908 |
+
}
|
| 909 |
+
},
|
| 910 |
+
"node_modules/protobufjs": {
|
| 911 |
+
"version": "7.6.5",
|
| 912 |
+
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
|
| 913 |
+
"integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
|
| 914 |
+
"hasInstallScript": true,
|
| 915 |
+
"license": "BSD-3-Clause",
|
| 916 |
+
"dependencies": {
|
| 917 |
+
"@protobufjs/aspromise": "^1.1.2",
|
| 918 |
+
"@protobufjs/base64": "^1.1.2",
|
| 919 |
+
"@protobufjs/codegen": "^2.0.5",
|
| 920 |
+
"@protobufjs/eventemitter": "^1.1.1",
|
| 921 |
+
"@protobufjs/fetch": "^1.1.1",
|
| 922 |
+
"@protobufjs/float": "^1.0.2",
|
| 923 |
+
"@protobufjs/path": "^1.1.2",
|
| 924 |
+
"@protobufjs/pool": "^1.1.0",
|
| 925 |
+
"@protobufjs/utf8": "^1.1.1",
|
| 926 |
+
"@types/node": ">=13.7.0",
|
| 927 |
+
"long": "^5.3.2"
|
| 928 |
+
},
|
| 929 |
+
"engines": {
|
| 930 |
+
"node": ">=12.0.0"
|
| 931 |
+
}
|
| 932 |
+
},
|
| 933 |
+
"node_modules/punycode": {
|
| 934 |
+
"version": "1.4.1",
|
| 935 |
+
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
| 936 |
+
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
| 937 |
+
"license": "MIT"
|
| 938 |
+
},
|
| 939 |
+
"node_modules/qs": {
|
| 940 |
+
"version": "6.15.3",
|
| 941 |
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
| 942 |
+
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
| 943 |
+
"license": "BSD-3-Clause",
|
| 944 |
+
"dependencies": {
|
| 945 |
+
"es-define-property": "^1.0.1",
|
| 946 |
+
"side-channel": "^1.1.1"
|
| 947 |
+
},
|
| 948 |
+
"engines": {
|
| 949 |
+
"node": ">=0.6"
|
| 950 |
+
},
|
| 951 |
+
"funding": {
|
| 952 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 953 |
+
}
|
| 954 |
+
},
|
| 955 |
+
"node_modules/retry": {
|
| 956 |
+
"version": "0.13.1",
|
| 957 |
+
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
| 958 |
+
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
| 959 |
+
"license": "MIT",
|
| 960 |
+
"engines": {
|
| 961 |
+
"node": ">= 4"
|
| 962 |
+
}
|
| 963 |
+
},
|
| 964 |
+
"node_modules/safe-buffer": {
|
| 965 |
+
"version": "5.2.1",
|
| 966 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
| 967 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
| 968 |
+
"funding": [
|
| 969 |
+
{
|
| 970 |
+
"type": "github",
|
| 971 |
+
"url": "https://github.com/sponsors/feross"
|
| 972 |
+
},
|
| 973 |
+
{
|
| 974 |
+
"type": "patreon",
|
| 975 |
+
"url": "https://www.patreon.com/feross"
|
| 976 |
+
},
|
| 977 |
+
{
|
| 978 |
+
"type": "consulting",
|
| 979 |
+
"url": "https://feross.org/support"
|
| 980 |
+
}
|
| 981 |
+
],
|
| 982 |
+
"license": "MIT"
|
| 983 |
+
},
|
| 984 |
+
"node_modules/sax": {
|
| 985 |
+
"version": "1.6.1",
|
| 986 |
+
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
| 987 |
+
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
| 988 |
+
"license": "BlueOak-1.0.0",
|
| 989 |
+
"engines": {
|
| 990 |
+
"node": ">=11.0.0"
|
| 991 |
+
}
|
| 992 |
+
},
|
| 993 |
+
"node_modules/semver": {
|
| 994 |
+
"version": "7.8.5",
|
| 995 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
| 996 |
+
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
| 997 |
+
"license": "ISC",
|
| 998 |
+
"bin": {
|
| 999 |
+
"semver": "bin/semver.js"
|
| 1000 |
+
},
|
| 1001 |
+
"engines": {
|
| 1002 |
+
"node": ">=10"
|
| 1003 |
+
}
|
| 1004 |
+
},
|
| 1005 |
+
"node_modules/side-channel": {
|
| 1006 |
+
"version": "1.1.1",
|
| 1007 |
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
| 1008 |
+
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
| 1009 |
+
"license": "MIT",
|
| 1010 |
+
"dependencies": {
|
| 1011 |
+
"es-errors": "^1.3.0",
|
| 1012 |
+
"object-inspect": "^1.13.4",
|
| 1013 |
+
"side-channel-list": "^1.0.1",
|
| 1014 |
+
"side-channel-map": "^1.0.1",
|
| 1015 |
+
"side-channel-weakmap": "^1.0.2"
|
| 1016 |
+
},
|
| 1017 |
+
"engines": {
|
| 1018 |
+
"node": ">= 0.4"
|
| 1019 |
+
},
|
| 1020 |
+
"funding": {
|
| 1021 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1022 |
+
}
|
| 1023 |
+
},
|
| 1024 |
+
"node_modules/side-channel-list": {
|
| 1025 |
+
"version": "1.0.1",
|
| 1026 |
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
| 1027 |
+
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
| 1028 |
+
"license": "MIT",
|
| 1029 |
+
"dependencies": {
|
| 1030 |
+
"es-errors": "^1.3.0",
|
| 1031 |
+
"object-inspect": "^1.13.4"
|
| 1032 |
+
},
|
| 1033 |
+
"engines": {
|
| 1034 |
+
"node": ">= 0.4"
|
| 1035 |
+
},
|
| 1036 |
+
"funding": {
|
| 1037 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1038 |
+
}
|
| 1039 |
+
},
|
| 1040 |
+
"node_modules/side-channel-map": {
|
| 1041 |
+
"version": "1.0.1",
|
| 1042 |
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
| 1043 |
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
| 1044 |
+
"license": "MIT",
|
| 1045 |
+
"dependencies": {
|
| 1046 |
+
"call-bound": "^1.0.2",
|
| 1047 |
+
"es-errors": "^1.3.0",
|
| 1048 |
+
"get-intrinsic": "^1.2.5",
|
| 1049 |
+
"object-inspect": "^1.13.3"
|
| 1050 |
+
},
|
| 1051 |
+
"engines": {
|
| 1052 |
+
"node": ">= 0.4"
|
| 1053 |
+
},
|
| 1054 |
+
"funding": {
|
| 1055 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1056 |
+
}
|
| 1057 |
+
},
|
| 1058 |
+
"node_modules/side-channel-weakmap": {
|
| 1059 |
+
"version": "1.0.2",
|
| 1060 |
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
| 1061 |
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
| 1062 |
+
"license": "MIT",
|
| 1063 |
+
"dependencies": {
|
| 1064 |
+
"call-bound": "^1.0.2",
|
| 1065 |
+
"es-errors": "^1.3.0",
|
| 1066 |
+
"get-intrinsic": "^1.2.5",
|
| 1067 |
+
"object-inspect": "^1.13.3",
|
| 1068 |
+
"side-channel-map": "^1.0.1"
|
| 1069 |
+
},
|
| 1070 |
+
"engines": {
|
| 1071 |
+
"node": ">= 0.4"
|
| 1072 |
+
},
|
| 1073 |
+
"funding": {
|
| 1074 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1075 |
+
}
|
| 1076 |
+
},
|
| 1077 |
+
"node_modules/text-encoding-shim": {
|
| 1078 |
+
"version": "1.0.5",
|
| 1079 |
+
"resolved": "https://registry.npmjs.org/text-encoding-shim/-/text-encoding-shim-1.0.5.tgz",
|
| 1080 |
+
"integrity": "sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==",
|
| 1081 |
+
"license": "MIT"
|
| 1082 |
+
},
|
| 1083 |
+
"node_modules/tslib": {
|
| 1084 |
+
"version": "1.14.1",
|
| 1085 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
| 1086 |
+
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
| 1087 |
+
"license": "0BSD"
|
| 1088 |
+
},
|
| 1089 |
+
"node_modules/undici-types": {
|
| 1090 |
+
"version": "8.3.0",
|
| 1091 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
| 1092 |
+
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
| 1093 |
+
"license": "MIT"
|
| 1094 |
+
},
|
| 1095 |
+
"node_modules/url": {
|
| 1096 |
+
"version": "0.11.4",
|
| 1097 |
+
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
|
| 1098 |
+
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
|
| 1099 |
+
"license": "MIT",
|
| 1100 |
+
"dependencies": {
|
| 1101 |
+
"punycode": "^1.4.1",
|
| 1102 |
+
"qs": "^6.12.3"
|
| 1103 |
+
},
|
| 1104 |
+
"engines": {
|
| 1105 |
+
"node": ">= 0.4"
|
| 1106 |
+
}
|
| 1107 |
+
},
|
| 1108 |
+
"node_modules/web-streams-polyfill": {
|
| 1109 |
+
"version": "4.3.0",
|
| 1110 |
+
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.3.0.tgz",
|
| 1111 |
+
"integrity": "sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw==",
|
| 1112 |
+
"license": "MIT",
|
| 1113 |
+
"workspaces": [
|
| 1114 |
+
"test/benchmark-test",
|
| 1115 |
+
"test/rollup-test",
|
| 1116 |
+
"test/webpack-test"
|
| 1117 |
+
],
|
| 1118 |
+
"engines": {
|
| 1119 |
+
"node": ">= 8"
|
| 1120 |
+
}
|
| 1121 |
+
},
|
| 1122 |
+
"node_modules/wx-server-sdk": {
|
| 1123 |
+
"version": "4.0.2",
|
| 1124 |
+
"resolved": "https://registry.npmjs.org/wx-server-sdk/-/wx-server-sdk-4.0.2.tgz",
|
| 1125 |
+
"integrity": "sha512-dgpgWxYvOFWnQdGOb78sU+uQGELoEQSKzmJ7m8Y6eTCJKsfkdIHLMhRrdN5S5oDfAqr8Pc83jZKQeNRMZASIcg==",
|
| 1126 |
+
"license": "MIT",
|
| 1127 |
+
"dependencies": {
|
| 1128 |
+
"@cloudbase/node-sdk": "3.17.2",
|
| 1129 |
+
"json-bigint": "^1.0.0",
|
| 1130 |
+
"protobufjs": "^7.2.4",
|
| 1131 |
+
"tslib": "^1.9.3"
|
| 1132 |
+
}
|
| 1133 |
+
},
|
| 1134 |
+
"node_modules/xml2js": {
|
| 1135 |
+
"version": "0.6.2",
|
| 1136 |
+
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
| 1137 |
+
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
| 1138 |
+
"license": "MIT",
|
| 1139 |
+
"dependencies": {
|
| 1140 |
+
"sax": ">=0.6.0",
|
| 1141 |
+
"xmlbuilder": "~11.0.0"
|
| 1142 |
+
},
|
| 1143 |
+
"engines": {
|
| 1144 |
+
"node": ">=4.0.0"
|
| 1145 |
+
}
|
| 1146 |
+
},
|
| 1147 |
+
"node_modules/xmlbuilder": {
|
| 1148 |
+
"version": "11.0.1",
|
| 1149 |
+
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
| 1150 |
+
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
| 1151 |
+
"license": "MIT",
|
| 1152 |
+
"engines": {
|
| 1153 |
+
"node": ">=4.0"
|
| 1154 |
+
}
|
| 1155 |
+
}
|
| 1156 |
+
}
|
| 1157 |
+
}
|
BeadQR/cloudfunctions/quickstartFunctions/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "quickstartFunctions",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 8 |
+
},
|
| 9 |
+
"author": "",
|
| 10 |
+
"license": "ISC",
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"wx-server-sdk": "~4.0.2"
|
| 13 |
+
}
|
| 14 |
+
}
|
BeadQR/docs/promo/douwei-1080-cover.png
ADDED
|
Git LFS Details
|
BeadQR/docs/promo/douwei-1080.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:89ba1505c4ce4aa36316841c497dc96b8997bac534bb534426c23ca96ce87dcc
|
| 3 |
+
size 3693122
|
BeadQR/docs/promo/douwei-720-cover.png
ADDED
|
Git LFS Details
|
BeadQR/docs/promo/douwei-720.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:626ea04f449f33fe29c4ec6c4aa94a2db88600cababab4f5a11b74155b4eee4e
|
| 3 |
+
size 1650037
|
BeadQR/docs/video-script.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 豆维 · 视频文案与分镜脚本
|
| 2 |
+
|
| 3 |
+
> 目标:用一个短视频介绍「豆维 · 可扫码的拼豆」小程序,让观众理解它是什么、为什么有趣、怎么做出来。
|
| 4 |
+
> 结构:提出想法 → 实现想法 → 制作例子 → 完成例子。
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 一、视频定位与调性
|
| 9 |
+
|
| 10 |
+
| 维度 | 建议 |
|
| 11 |
+
|------|------|
|
| 12 |
+
| 时长 | 主版本 60–90 秒(B站 / 朋友圈 / 公众号),另附 15–30 秒短视频版 |
|
| 13 |
+
| 调性 | 温暖手作 × 科技惊喜,不是冷冰冰的产品演示 |
|
| 14 |
+
| 核心情绪 | 「拼豆不止是一幅画,它是一封能被读到的信」 |
|
| 15 |
+
| 目标观众 | 拼豆爱好者、手工创作者、喜欢送独特礼物的人 |
|
| 16 |
+
| 画面风格 | 暖色调,木桌面,自然光;手机屏幕录制与实物特写交替出现 |
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 二、核心文案(Slogan / 一句话介绍)
|
| 21 |
+
|
| 22 |
+
以下任选一条作为标题或结尾定版字:
|
| 23 |
+
|
| 24 |
+
1. **豆维 · 可扫码的拼豆**
|
| 25 |
+
2. **每一颗珠子,都藏着一句话。**
|
| 26 |
+
3. **拼出来的二维码,扫出来的心意。**
|
| 27 |
+
4. **把想说的话,拼进一幅画里。**
|
| 28 |
+
5. **看得见,也读得到。**
|
| 29 |
+
|
| 30 |
+
推荐主标题:**看得见,也读得到。——豆维,可扫码的拼豆**
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 三、主版本分镜脚本(60–90 秒)
|
| 35 |
+
|
| 36 |
+
> 旁白以第一人称「我」叙述,语气像朋友分享,不念广告稿。
|
| 37 |
+
> 「画面」列标注了你需要准备的素材类型;如果你已有类似镜头,直接对应即可。
|
| 38 |
+
|
| 39 |
+
| 幕 | 时间 | 画面 | 旁白 / 字幕 | 剪辑提示 |
|
| 40 |
+
|----|------|------|-------------|----------|
|
| 41 |
+
| **第一幕:提出想法** | 0:00–0:08 | 桌面上摆着几幅拼豆作品,阳光照进来。手拿起其中一幅,翻到背面,又翻回来。 | **旁白:** 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画? | 慢推镜头,配温暖的钢琴或指弹吉他。最后一帧画面停在作品特写。 |
|
| 42 |
+
| | 0:08–0:14 | 手拿起手机对准拼豆作品「扫一扫」——屏幕弹出一条消息或链接。 | **旁白:** 能不能扫一下,就能看到我想说的话? | 这是全片第一处「惊喜时刻」,音效清脆,屏幕内容要清晰可见。 |
|
| 43 |
+
| **第二幕:实现想法** | 0:14–0:24 | 切到手机屏幕录屏:打开豆维小程序,输入一段文字。 | **旁白:** 所以我做了「豆维」——一个可扫码的拼豆图纸生成器。输入一段话,或者一个链接。 | 屏幕录制加速 2–3 倍,打字过程快放。 |
|
| 44 |
+
| | 0:24–0:34 | 录屏:导入一张照片,裁切、去背景。 | **旁白:** 再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。 | 展示裁切弹窗操作,去背景效果前后对比。 |
|
| 45 |
+
| | 0:34–0:44 | 录屏:拖动滑块调整二维码大小和位置,切换「丰富 / 平衡 / 稳定」模式。 | **旁白:** 二维码可以放大缩小、移到任何位置。颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。 | 滑块操作用慢动作展示,让观众看到图案实时变化。 |
|
| 46 |
+
| **第三幕:制作例子** | 0:44–0:52 | 导出的图纸打印出来,摆在桌上。开始一颗一颗摆珠子。 | **旁白:** 图纸保存下来,打印出来,就可以开始拼了。 | 延时摄影或快剪,展示摆珠过程,保留几秒真实速度的特写。 |
|
| 47 |
+
| | 0:52–0:58 | 熨斗烫过,拼豆板冷却,掀开,成品完成。 | **旁白:** 熨好,冷却,掀开—— | 掀开那一刻音效「叮」,画面干净利落。 |
|
| 48 |
+
| **第四幕:完成例子** | 0:58–1:10 | 手持成品对镜头展示,然后用手机扫描。屏幕上弹出一开始输入的那段话。 | **旁白:** 它不只是一幅画。你把手机对准它——就能读到里面藏着的话。 | 这是全片情绪最高点。扫描过程用真实速度,屏幕内容要清楚。 |
|
| 49 |
+
| | 1:10–1:20 | 多个成品排列展示(不同图案、不同颜色),最后画面淡出到品牌 Logo 和小程序码。 | **旁白:** 豆维,可扫码的拼豆。看得见,也读得到。 | 结尾定格 3 秒,放小程序码,方便观众扫码体验。 |
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## 四、15 秒短视频版(抖音 / 视频号快闪)
|
| 54 |
+
|
| 55 |
+
| 时间 | 画面 | 字幕(无旁白,纯字幕流) |
|
| 56 |
+
|------|------|--------------------------|
|
| 57 |
+
| 0–3s | 手拿起拼豆作品 → 手机扫描 → 屏幕弹出消息 | 拼豆里,藏着一条消息 |
|
| 58 |
+
| 3–8s | 快剪:输入文字 → 导入照片 → 图纸生成 → 摆珠 | 豆维:输入一句话,选一张图,生成拼豆图纸 |
|
| 59 |
+
| 8–13s | 熨烫 → 掀开 → 成品再扫一次 | 拼好,烫平,扫一下—— |
|
| 60 |
+
| 13–15s | 品牌 Logo + 小程序码 | 消息就出来了。豆维 · 可扫码的拼豆 |
|
| 61 |
+
|
| 62 |
+
> 快闪版不需要旁白,靠音乐节奏 + 字幕推进。音乐选节奏感强的电子或 Lo-Fi。
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## 五、30 秒版本(朋友圈 / 公众号内嵌)
|
| 67 |
+
|
| 68 |
+
节奏比 15 秒版稍慢,保留 4 个关键节点:
|
| 69 |
+
|
| 70 |
+
1. **悬念**(5s):拿起作品 → 扫码 → 弹出内容
|
| 71 |
+
2. **怎么做**(10s):小程序操作快剪(输入 → 导图 → 调整 → 导出)
|
| 72 |
+
3. **手作过程**(8s):摆珠延时 + 熨烫 + 掀开
|
| 73 |
+
4. **结尾**(7s):成品特写 + 再扫一次 + 定版
|
| 74 |
+
|
| 75 |
+
配乐:轻快指弹吉他或 Lo-Fi,音量压低,让旁白清晰。
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## 六、素材清单
|
| 80 |
+
|
| 81 |
+
拍摄前对照这份清单,确认已拍到以下镜头(√ 为已有,× 为待补):
|
| 82 |
+
|
| 83 |
+
| 素材 | 用途 | 状态 |
|
| 84 |
+
|------|------|------|
|
| 85 |
+
| 多幅完成的拼豆作品摆拍(正面 / 斜角 / 背面) | 开头悬念、结尾展示 | ☐ |
|
| 86 |
+
| 手机扫描拼豆作品并弹出内容的实拍 | 全片核心「惊喜时刻」,需拍 2–3 次 | ☐ |
|
| 87 |
+
| 豆维小程序屏幕录制:输入文字、导入图片、裁切、调 QR 位置、切模式 | 第二幕 | ☐ |
|
| 88 |
+
| 图纸导出 / 打印的镜头 | 第三幕开头 | ☐ |
|
| 89 |
+
| 摆珠延时或快剪素材 | 第三幕 | ☐ |
|
| 90 |
+
| 熨斗烫拼豆 + 掀开的特写 | 第三幕高潮 | ☐ |
|
| 91 |
+
| 成品多角度特写(自然光 / 桌面) | 结尾 | ☐ |
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## 七、剪辑建议
|
| 96 |
+
|
| 97 |
+
- **转场**:幕与幕之间用干净的硬切或快速模糊,不要花哨转场。产品视频要显得克制。
|
| 98 |
+
- **音乐**:主版本选温暖的原声吉他或钢琴;快闪版选节奏感强的 Lo-Fi / 电子。全片一首歌,不要中途换。
|
| 99 |
+
- **节奏**:第三幕(摆珠)可以稍慢,让观众感受手作质感;第二幕(录屏)加速推进。
|
| 100 |
+
- **字幕**:全程加简体中文字幕,关键词用品牌色 `#416d62`(深绿)高亮。
|
| 101 |
+
- **结尾**:小程序码至少停留 3 秒,让观众来得及扫码。
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 八、旁白全文(主版本,可直接朗读)
|
| 106 |
+
|
| 107 |
+
> 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画?能不能扫一下,就能看到我想说的话?
|
| 108 |
+
>
|
| 109 |
+
> 所以我做了「豆维」。输入一段话,或者一个链接,再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。二维码可以放大缩小、移到任何位置,颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。
|
| 110 |
+
>
|
| 111 |
+
> 图纸保存下来,打印出来,就可以开始拼了。熨好,冷却,掀开——
|
| 112 |
+
>
|
| 113 |
+
> 它不只是一幅画。你把手机对准它,就能读到里面藏着的话。
|
| 114 |
+
>
|
| 115 |
+
> 豆维,可扫码的拼豆。看得见,也读得到。
|
| 116 |
+
|
BeadQR/docs/审核驳回处理-文案与步骤.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 审核驳回处理:深度合成误判 — 文案与步骤
|
| 2 |
+
|
| 3 |
+
> 驳回原因(2026-09-30):小程序服务内容涉及深度合成技术(如 AI 问答、AI 换脸视频、AI 绘画等),属个人主体尚未开放服务类目,建议申请企业主体类型小程序。
|
| 4 |
+
|
| 5 |
+
## 结论
|
| 6 |
+
|
| 7 |
+
代码里没有任何 AI 功能:文字是标准二维码编码;"照片转图纸"是确定性本地图像处理(按网格取平均色 → 映射到 221 种固定色号),不用任何模型、不生成新图像。被判"深度合成"大概率是**后台简介 / 服务类目 / 服务内容声明的表述**或审核员看到"照片→生成图纸"产生的误判。
|
| 8 |
+
|
| 9 |
+
## 路径 A(推荐,最快):修正表述后重新提审
|
| 10 |
+
|
| 11 |
+
### 1. 检查后台三处(mp.weixin.qq.com)
|
| 12 |
+
|
| 13 |
+
| 位置 | 检查项 |
|
| 14 |
+
| --- | --- |
|
| 15 |
+
| 服务类目 | 改为 **工具 - 图片处理**(个人主体可选);删除任何 AI / 深度合成相关类目 |
|
| 16 |
+
| 服务内容声明 / 提审问卷 | 凡问"是否提供 AI 生成内容、AI 问答、AI 绘画、深度合成"一律选 **否** |
|
| 17 |
+
| 小程序简介 | 换成下方"新简介",去掉"生成器 / 一键生成 / 智能"等字眼 |
|
| 18 |
+
|
| 19 |
+
### 2. 新简介(约 90 字,上限 120)
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
手工拼豆图纸工具:输入一句话或选一张照片,按 MARD 标准色号排成拼豆图纸,拼好微信一扫就能读出内容。支持 A4 矢量 PDF 与图片导出、用量清单。照片仅在本地做取色处理,并经微信内容安全检测。
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### 3. 版本描述 / 审核备注(重新提审时填写)
|
| 26 |
+
|
| 27 |
+
```
|
| 28 |
+
本小程序为拼豆手工图纸工具,不使用任何人工智能或深度合成技术,无 AIGC 功能:
|
| 29 |
+
1. "照片转图纸"为确定性本地图像处理:将用户上传照片按网格取平均色,再映射到固定的 221 种拼豆标准色号(色彩量化 + 最近邻查表),不生成、不合成任何新图像内容,不涉及人脸或生物特征编辑;
|
| 30 |
+
2. 文字功能是将文本编码为标准二维码点阵,非内容生成;
|
| 31 |
+
3. 所有处理均在用户手机本地完成,且已接入微信内容安全接口(security.msgSecCheck / imgSecCheck),检测未通过时禁止生成与导出(fail-closed)。
|
| 32 |
+
请按"工具-图片处理"类目审核。
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## 路径 B:申诉
|
| 36 |
+
|
| 37 |
+
若不想改简介,可直接对本次审核结果发起申诉(审核结果页 → 申诉),粘贴下文:
|
| 38 |
+
|
| 39 |
+
```
|
| 40 |
+
本小程序为拼豆手工图纸工具,不涉及深度合成技术:
|
| 41 |
+
(1) 不使用 AI 模型 / 神经网络,无 AI 问答、AI 绘画、AI 换脸等 AIGC 功能;
|
| 42 |
+
(2) 照片功能仅做确定性色彩量化:按网格取平均色并映射到 221 种固定标准色号,属于常规图像处理,不生成新图像、不做人脸或生物特征编辑;
|
| 43 |
+
(3) 文字功能为标准二维码编码;
|
| 44 |
+
(4) 已接入微信内容安全检测(msgSecCheck / imgSecCheck),检测未通过不能生成或导出。
|
| 45 |
+
恳请复核,建议按"工具-图片处理"类目审核。
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
申诉未通过时:修改简介与类目后按路径 A 重新提审(改了表述基本都能过)。
|
| 49 |
+
|
| 50 |
+
## 路径 C:仅当二次被拒或想保留"生成"叙事
|
| 51 |
+
|
| 52 |
+
1. **隐藏照片功能**(只保留文字 → 二维码图纸,纯编码无争议)——我可以直接改代码;
|
| 53 |
+
2. **升级主体**:注册个体工商户 / 企业 → 主体变更 → 申请深度合成相关类目 + 算法备案。仅在以后真要做 AI 功能时才值得,当前工具用不到。
|
| 54 |
+
|
| 55 |
+
## 顺手可做的降风险小改动(可选)
|
| 56 |
+
|
| 57 |
+
- 首页徽标 `MARD 拼豆图纸生成器` → `MARD 拼豆图纸工具`
|
| 58 |
+
- 分享标题 `豆维 · 可扫码的拼豆图纸生成器` → `豆维 · 可扫码的拼豆图纸`
|
BeadQR/miniprogram/app.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 豆维 · 可扫码的拼豆
|
| 2 |
+
App({
|
| 3 |
+
onLaunch() {
|
| 4 |
+
if (!wx.cloud) {
|
| 5 |
+
console.error('当前基础库不支持云开发,内容安全检测不可用')
|
| 6 |
+
return
|
| 7 |
+
}
|
| 8 |
+
wx.cloud.init({
|
| 9 |
+
env: 'cloud1-d6gthxado7f6c9d14',
|
| 10 |
+
traceUser: true,
|
| 11 |
+
})
|
| 12 |
+
},
|
| 13 |
+
globalData: {},
|
| 14 |
+
})
|
BeadQR/miniprogram/app.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"pages": [
|
| 3 |
+
"pages/index/index"
|
| 4 |
+
],
|
| 5 |
+
"window": {
|
| 6 |
+
"backgroundColor": "#f6f5f1",
|
| 7 |
+
"backgroundTextStyle": "dark",
|
| 8 |
+
"navigationBarBackgroundColor": "#f6f5f1",
|
| 9 |
+
"navigationBarTitleText": "豆维 · 可扫码的拼豆",
|
| 10 |
+
"navigationBarTextStyle": "black"
|
| 11 |
+
},
|
| 12 |
+
"usingComponents": {
|
| 13 |
+
"privacy-popup": "/components/privacy-popup/index"
|
| 14 |
+
},
|
| 15 |
+
"__usePrivacyCheck__": true,
|
| 16 |
+
"sitemapLocation": "sitemap.json",
|
| 17 |
+
"style": "v2",
|
| 18 |
+
"lazyCodeLoading": "requiredComponents"
|
| 19 |
+
}
|
BeadQR/miniprogram/app.wxss
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* 豆维 · 设计变量 */
|
| 2 |
+
page {
|
| 3 |
+
--ink: #16211f;
|
| 4 |
+
--ink-2: #3a4643;
|
| 5 |
+
--muted: #6f7a76;
|
| 6 |
+
--faint: #a4aca8;
|
| 7 |
+
--bg: #f6f5f1;
|
| 8 |
+
--card: #ffffff;
|
| 9 |
+
--line: #ebe8e1;
|
| 10 |
+
--brand: #0f766e;
|
| 11 |
+
--brand-2: #14b8a6;
|
| 12 |
+
--brand-soft: #e6f4f1;
|
| 13 |
+
--brand-deep: #0f3d3a;
|
| 14 |
+
--accent: #ff7a45;
|
| 15 |
+
--accent-soft: #fff1ea;
|
| 16 |
+
--danger: #e5484d;
|
| 17 |
+
--danger-soft: #fdecec;
|
| 18 |
+
--ok: #12a150;
|
| 19 |
+
--ok-soft: #e7f6ed;
|
| 20 |
+
--warn: #c77700;
|
| 21 |
+
--warn-soft: #fff5e0;
|
| 22 |
+
--r-s: 16rpx;
|
| 23 |
+
--r-m: 24rpx;
|
| 24 |
+
--r-l: 36rpx;
|
| 25 |
+
--shadow: 0 8rpx 32rpx rgba(22, 33, 31, 0.06);
|
| 26 |
+
|
| 27 |
+
background: var(--bg);
|
| 28 |
+
color: var(--ink);
|
| 29 |
+
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
|
| 30 |
+
font-size: 28rpx;
|
| 31 |
+
line-height: 1.55;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
view, text, textarea, input, button, scroll-view, canvas, image { box-sizing: border-box; }
|
| 35 |
+
|
| 36 |
+
button { padding: 0; margin: 0; background: none; border: 0; border-radius: 0; line-height: inherit; font-size: inherit; color: inherit; text-align: center; }
|
| 37 |
+
button::after { border: 0; }
|
BeadQR/miniprogram/assets/icon.png
ADDED
|
|
BeadQR/miniprogram/assets/miniprogram-code.png
ADDED
|
BeadQR/miniprogram/assets/shield.svg
ADDED
|
|
BeadQR/miniprogram/components/privacy-popup/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 隐私保护指引弹窗:用户首次调用相册/相机/保存到相册等隐私接口时由微信触发
|
| 2 |
+
Component({
|
| 3 |
+
data: { visible: false, contractName: '《豆维隐私保护指引》' },
|
| 4 |
+
|
| 5 |
+
lifetimes: {
|
| 6 |
+
attached() {
|
| 7 |
+
if (!wx.onNeedPrivacyAuthorization) return
|
| 8 |
+
this._listener = (resolve) => {
|
| 9 |
+
this._resolve = resolve
|
| 10 |
+
this.setData({ visible: true })
|
| 11 |
+
}
|
| 12 |
+
wx.onNeedPrivacyAuthorization(this._listener)
|
| 13 |
+
if (wx.getPrivacySetting) {
|
| 14 |
+
wx.getPrivacySetting({
|
| 15 |
+
success: (res) => {
|
| 16 |
+
if (res.privacyContractName) this.setData({ contractName: res.privacyContractName })
|
| 17 |
+
},
|
| 18 |
+
})
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
|
| 23 |
+
methods: {
|
| 24 |
+
openContract() {
|
| 25 |
+
if (wx.openPrivacyContract) wx.openPrivacyContract({})
|
| 26 |
+
},
|
| 27 |
+
handleAgree() {
|
| 28 |
+
if (this._resolve) this._resolve({ buttonId: 'privacy-agree-btn', event: 'agree' })
|
| 29 |
+
this._resolve = null
|
| 30 |
+
this.setData({ visible: false })
|
| 31 |
+
},
|
| 32 |
+
handleDisagree() {
|
| 33 |
+
if (this._resolve) this._resolve({ event: 'disagree' })
|
| 34 |
+
this._resolve = null
|
| 35 |
+
this.setData({ visible: false })
|
| 36 |
+
wx.showToast({ title: '未同意将无法选择照片或保存图纸', icon: 'none' })
|
| 37 |
+
},
|
| 38 |
+
noop() {},
|
| 39 |
+
},
|
| 40 |
+
})
|
BeadQR/miniprogram/components/privacy-popup/index.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"component": true,
|
| 3 |
+
"usingComponents": {}
|
| 4 |
+
}
|
BeadQR/miniprogram/components/privacy-popup/index.wxml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<view wx:if="{{visible}}" class="mask" catchtouchmove="noop">
|
| 2 |
+
<view class="sheet">
|
| 3 |
+
<view class="title">隐私保护提示</view>
|
| 4 |
+
<view class="desc">
|
| 5 |
+
为了帮你把照片做成拼豆图纸,豆维需要读取你选择的照片、保存图纸到相册。照片仅在本机处理,送检的压缩副本会在内容安全检测后立即删除。使用前请阅读
|
| 6 |
+
<text class="link" bindtap="openContract">{{contractName}}</text>。
|
| 7 |
+
</view>
|
| 8 |
+
<view class="actions">
|
| 9 |
+
<button class="btn ghost" bindtap="handleDisagree">不同意</button>
|
| 10 |
+
<button id="privacy-agree-btn" class="btn primary" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgree">同意并继续</button>
|
| 11 |
+
</view>
|
| 12 |
+
</view>
|
| 13 |
+
</view>
|
BeadQR/miniprogram/components/privacy-popup/index.wxss
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.mask { position: fixed; inset: 0; z-index: 999; background: rgba(15, 23, 22, 0.55); display: flex; align-items: flex-end; }
|
| 2 |
+
.sheet { width: 100%; background: #fff; border-radius: 40rpx 40rpx 0 0; padding: 48rpx 40rpx calc(40rpx + env(safe-area-inset-bottom)); box-sizing: border-box; }
|
| 3 |
+
.title { font-size: 36rpx; font-weight: 700; color: #1d2b2a; margin-bottom: 20rpx; }
|
| 4 |
+
.desc { font-size: 28rpx; line-height: 1.7; color: #55615c; }
|
| 5 |
+
.link { color: #0f766e; font-weight: 600; }
|
| 6 |
+
.actions { display: flex; gap: 20rpx; margin-top: 40rpx; }
|
| 7 |
+
.btn { flex: 1; height: 92rpx; line-height: 92rpx; border-radius: 999rpx; font-size: 30rpx; font-weight: 600; margin: 0 !important; padding: 0 !important; width: auto !important; }
|
| 8 |
+
.btn::after { border: 0; }
|
| 9 |
+
.ghost { background: #f1f3f2; color: #55615c; }
|
| 10 |
+
.primary { background: #0f766e; color: #fff; }
|
BeadQR/miniprogram/pages/index/index.js
ADDED
|
@@ -0,0 +1,1073 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 豆维主页面
|
| 2 |
+
// 大数据(色格、图片、画布节点)一律挂在 this._ 上,不进 setData,保证滑动/输入流畅
|
| 3 |
+
const { MATCH_MODES, PRESETS, preparePalette } = require('../../utils/palette')
|
| 4 |
+
const { makeQr, createPlacement, SAMPLE_TEXT } = require('../../utils/qr')
|
| 5 |
+
const { getImageDimensions, drawCropped, sampleImage, removeSimpleBackground } = require('../../utils/imaging')
|
| 6 |
+
const { computeArtwork } = require('../../utils/engine')
|
| 7 |
+
const { selfCheck } = require('../../utils/scancheck')
|
| 8 |
+
const { renderPreview, patternPageLayout, drawPatternPage, buildInfoPages, drawWatermark } = require('../../utils/pattern')
|
| 9 |
+
const { PdfDoc, planPatternPages, addPatternPages, addImagePage } = require('../../utils/pdf')
|
| 10 |
+
const security = require('../../utils/security')
|
| 11 |
+
const { createIntro } = require('../../utils/intro')
|
| 12 |
+
|
| 13 |
+
const LEVELS = [
|
| 14 |
+
{ id: 'M', label: 'M', hint: '格子少' },
|
| 15 |
+
{ id: 'Q', label: 'Q', hint: '均衡' },
|
| 16 |
+
{ id: 'H', label: 'H', hint: '最耐磨' },
|
| 17 |
+
]
|
| 18 |
+
const QUIET_OPTIONS = [
|
| 19 |
+
{ id: 0, label: '无' },
|
| 20 |
+
{ id: 1, label: '1 格' },
|
| 21 |
+
{ id: 2, label: '2 格' },
|
| 22 |
+
]
|
| 23 |
+
const BEAD_STYLES = [
|
| 24 |
+
{ id: 'square', label: '方豆' },
|
| 25 |
+
{ id: 'round', label: '圆豆' },
|
| 26 |
+
{ id: 'hollow', label: '中空豆' },
|
| 27 |
+
]
|
| 28 |
+
const GRID_COLORS = ['#0f766e', '#16211f', '#e5484d', '#3b82f6', '#a855f7']
|
| 29 |
+
const POSITIONS = [0, 50, 100]
|
| 30 |
+
const MAX_CANVAS_SIDE = 8000
|
| 31 |
+
const MAX_CANVAS_PIXELS = 16000000
|
| 32 |
+
const CROP_SIZE = 640
|
| 33 |
+
const FILE_PREFIX = 'douwei-'
|
| 34 |
+
|
| 35 |
+
function fitScale(w, h) {
|
| 36 |
+
return Math.min(1, MAX_CANVAS_SIDE / w, MAX_CANVAS_SIDE / h, Math.sqrt(MAX_CANVAS_PIXELS / (w * h)))
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function formatNumber(n) {
|
| 40 |
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
Page({
|
| 44 |
+
data: {
|
| 45 |
+
// 导航
|
| 46 |
+
statusBarHeight: 20,
|
| 47 |
+
navHeight: 44,
|
| 48 |
+
navSolid: false,
|
| 49 |
+
// 1. 内容
|
| 50 |
+
text: '',
|
| 51 |
+
textStatus: 'idle', // idle | checking | pass | risky | error
|
| 52 |
+
scanImportLoading: false,
|
| 53 |
+
// 2. 照片
|
| 54 |
+
hasImage: false,
|
| 55 |
+
imageThumb: '',
|
| 56 |
+
imageMeta: '',
|
| 57 |
+
removeBackground: false,
|
| 58 |
+
bgTolerance: 22,
|
| 59 |
+
// 3. 效果
|
| 60 |
+
qrScale: 100,
|
| 61 |
+
posX: 50,
|
| 62 |
+
posY: 50,
|
| 63 |
+
positions: POSITIONS,
|
| 64 |
+
modeId: 'balanced',
|
| 65 |
+
matchModes: MATCH_MODES,
|
| 66 |
+
advancedOpen: false,
|
| 67 |
+
fineTuneOpen: false,
|
| 68 |
+
presets: PRESETS.map((p) => ({ id: p.id, short: p.short, detail: p.detail })),
|
| 69 |
+
paletteId: '221',
|
| 70 |
+
coherence: 40,
|
| 71 |
+
cleanup: true,
|
| 72 |
+
levels: LEVELS,
|
| 73 |
+
level: 'H',
|
| 74 |
+
quietOptions: QUIET_OPTIONS,
|
| 75 |
+
quiet: 1,
|
| 76 |
+
// 预览与统计
|
| 77 |
+
generating: false,
|
| 78 |
+
isSample: true,
|
| 79 |
+
showIntro: true, // 无内容时播放动态演示,写下内容后切回真实预览
|
| 80 |
+
textFocus: false,
|
| 81 |
+
gridText: '—',
|
| 82 |
+
beadsText: '—',
|
| 83 |
+
colorsText: '—',
|
| 84 |
+
qrGridText: '',
|
| 85 |
+
scanState: 'idle', // idle | checking | ok | fail
|
| 86 |
+
usage: [],
|
| 87 |
+
usageMore: 0,
|
| 88 |
+
usageExpanded: false,
|
| 89 |
+
usageAll: [],
|
| 90 |
+
// 裁切
|
| 91 |
+
cropVisible: false,
|
| 92 |
+
cropCheck: 'idle', // checking | pass | risky | error
|
| 93 |
+
draftFocusX: 50,
|
| 94 |
+
draftFocusY: 50,
|
| 95 |
+
draftZoom: 100,
|
| 96 |
+
draftFlipH: false,
|
| 97 |
+
draftFlipV: false,
|
| 98 |
+
draftRemoveBg: false,
|
| 99 |
+
draftBgTolerance: 22,
|
| 100 |
+
// 导出
|
| 101 |
+
exportVisible: false,
|
| 102 |
+
exportFormat: 'pdf',
|
| 103 |
+
exportLayout: 'combined',
|
| 104 |
+
exportQuality: 'hd',
|
| 105 |
+
beadStyles: BEAD_STYLES,
|
| 106 |
+
beadStyle: 'square',
|
| 107 |
+
gridEnabled: true,
|
| 108 |
+
gridInterval: 10,
|
| 109 |
+
gridColors: GRID_COLORS,
|
| 110 |
+
gridColor: '#0f766e',
|
| 111 |
+
watermark: '',
|
| 112 |
+
watermarkStatus: 'idle',
|
| 113 |
+
exporting: false,
|
| 114 |
+
exportStep: '',
|
| 115 |
+
exportResult: null, // { kind, files: [{ name, path }] }
|
| 116 |
+
},
|
| 117 |
+
|
| 118 |
+
// —— 生命周期 ——
|
| 119 |
+
|
| 120 |
+
onLoad() {
|
| 121 |
+
const win = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()
|
| 122 |
+
let navHeight = 44
|
| 123 |
+
try {
|
| 124 |
+
const capsule = wx.getMenuButtonBoundingClientRect()
|
| 125 |
+
if (capsule && capsule.top) navHeight = (capsule.top - win.statusBarHeight) * 2 + capsule.height
|
| 126 |
+
} catch (e) { /* 使用默认高度 */ }
|
| 127 |
+
this._dpr = Math.min(3, win.pixelRatio || 2)
|
| 128 |
+
this._timers = {}
|
| 129 |
+
this._nodes = {}
|
| 130 |
+
this._token = 0
|
| 131 |
+
this.setData({ statusBarHeight: win.statusBarHeight || 20, navHeight })
|
| 132 |
+
},
|
| 133 |
+
|
| 134 |
+
onReady() {
|
| 135 |
+
wx.createSelectorQuery().in(this)
|
| 136 |
+
.select('#preview').fields({ node: true, size: true })
|
| 137 |
+
.select('#work').fields({ node: true })
|
| 138 |
+
.select('#security').fields({ node: true })
|
| 139 |
+
.exec((res) => {
|
| 140 |
+
this._nodes.preview = res[0] && res[0].node
|
| 141 |
+
this._previewCss = (res[0] && res[0].width) || 300
|
| 142 |
+
this._nodes.work = res[1] && res[1].node
|
| 143 |
+
this._nodes.security = res[2] && res[2].node
|
| 144 |
+
this.loadMpCode()
|
| 145 |
+
this.schedule(0)
|
| 146 |
+
})
|
| 147 |
+
},
|
| 148 |
+
|
| 149 |
+
onUnload() {
|
| 150 |
+
Object.keys(this._timers).forEach((k) => clearTimeout(this._timers[k]))
|
| 151 |
+
this.stopIntro()
|
| 152 |
+
},
|
| 153 |
+
|
| 154 |
+
onShow() {
|
| 155 |
+
this._hidden = false
|
| 156 |
+
if (this._nodes && this._nodes.preview) this.syncIntro()
|
| 157 |
+
},
|
| 158 |
+
|
| 159 |
+
onHide() {
|
| 160 |
+
this._hidden = true
|
| 161 |
+
this.pauseIntro()
|
| 162 |
+
},
|
| 163 |
+
|
| 164 |
+
onPageScroll(e) {
|
| 165 |
+
const solid = e.scrollTop > 40
|
| 166 |
+
if (solid !== this.data.navSolid) this.setData({ navSolid: solid })
|
| 167 |
+
// 演示区滚出屏幕后暂停动画,省电
|
| 168 |
+
const away = e.scrollTop > 650
|
| 169 |
+
if (away !== this._scrolledAway) {
|
| 170 |
+
this._scrolledAway = away
|
| 171 |
+
this.syncIntro()
|
| 172 |
+
}
|
| 173 |
+
},
|
| 174 |
+
|
| 175 |
+
// 分享的是小程序页面卡片,不携带用户内容
|
| 176 |
+
onShareAppMessage() {
|
| 177 |
+
return { title: '把一句话藏进拼豆里,微信一扫就能读出来', path: '/pages/index/index' }
|
| 178 |
+
},
|
| 179 |
+
onShareTimeline() {
|
| 180 |
+
return { title: '豆维 · 可扫码的拼豆图纸生成器' }
|
| 181 |
+
},
|
| 182 |
+
|
| 183 |
+
loadMpCode() {
|
| 184 |
+
if (!this._nodes.work) return
|
| 185 |
+
const image = this._nodes.work.createImage()
|
| 186 |
+
image.onload = () => { this._mpCode = image }
|
| 187 |
+
image.onerror = () => { this._mpCode = null }
|
| 188 |
+
image.src = '/assets/miniprogram-code.png'
|
| 189 |
+
},
|
| 190 |
+
|
| 191 |
+
debounce(key, fn, wait) {
|
| 192 |
+
clearTimeout(this._timers[key])
|
| 193 |
+
this._timers[key] = setTimeout(fn, wait)
|
| 194 |
+
},
|
| 195 |
+
|
| 196 |
+
noop() {},
|
| 197 |
+
|
| 198 |
+
openPrivacy() {
|
| 199 |
+
if (wx.openPrivacyContract) {
|
| 200 |
+
wx.openPrivacyContract({ fail: () => wx.showToast({ title: '请在微信内查看隐私保护指引', icon: 'none' }) })
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
|
| 204 |
+
showRules() {
|
| 205 |
+
wx.showModal({
|
| 206 |
+
title: '内容规范',
|
| 207 |
+
content: '二维码内容、水印和照片都会经过微信内容安全检测。请勿输入或上传违法违规、色情低俗、暴力恐怖、侵犯他人权益等内容;检测未通过或检测异常时无法生成与导出。如发现不良内容,可通过页面底部“投诉与反馈”告诉我们。',
|
| 208 |
+
showCancel: false,
|
| 209 |
+
confirmText: '我知道了',
|
| 210 |
+
})
|
| 211 |
+
},
|
| 212 |
+
|
| 213 |
+
// —— 生成管线 ——
|
| 214 |
+
|
| 215 |
+
schedule(wait = 120) {
|
| 216 |
+
this.debounce('run', () => this.run(), wait)
|
| 217 |
+
},
|
| 218 |
+
|
| 219 |
+
run() {
|
| 220 |
+
if (!this._nodes.preview || !this._nodes.work) return
|
| 221 |
+
// 没写内容、也没选照片:示例二维码没有意义,改为播放动态演示
|
| 222 |
+
if (!this.data.text.trim() && !this._source) {
|
| 223 |
+
this._token += 1
|
| 224 |
+
this.setData({
|
| 225 |
+
showIntro: true, generating: false, isSample: true, scanState: 'idle', usage: [], usageAll: [], usageMore: 0,
|
| 226 |
+
}, () => this.syncIntro())
|
| 227 |
+
return
|
| 228 |
+
}
|
| 229 |
+
this.stopIntro()
|
| 230 |
+
if (this.data.showIntro) this.setData({ showIntro: false })
|
| 231 |
+
const token = ++this._token
|
| 232 |
+
this.setData({ generating: true }, () => {
|
| 233 |
+
// 先让“生成中”状态渲染一帧,再做同步计算
|
| 234 |
+
setTimeout(() => {
|
| 235 |
+
if (token !== this._token) return
|
| 236 |
+
try {
|
| 237 |
+
this.compute()
|
| 238 |
+
this.afterCompute(token)
|
| 239 |
+
} catch (error) {
|
| 240 |
+
console.error('生成失败', error)
|
| 241 |
+
this.setData({ generating: false })
|
| 242 |
+
wx.showToast({ title: '内容太长或图片异常,请调整后重试', icon: 'none' })
|
| 243 |
+
}
|
| 244 |
+
}, 16)
|
| 245 |
+
})
|
| 246 |
+
},
|
| 247 |
+
|
| 248 |
+
compute() {
|
| 249 |
+
const d = this.data
|
| 250 |
+
const text = d.text.trim()
|
| 251 |
+
const qrKey = `${text}|${d.level}`
|
| 252 |
+
if (!this._qr || this._qrKey !== qrKey) {
|
| 253 |
+
this._qr = makeQr(text, d.level)
|
| 254 |
+
this._qrKey = qrKey
|
| 255 |
+
}
|
| 256 |
+
const src = this._source
|
| 257 |
+
const placement = createPlacement(this._qr.size, {
|
| 258 |
+
scale: d.qrScale, posX: d.posX, posY: d.posY, quiet: d.quiet, hasImage: Boolean(src),
|
| 259 |
+
})
|
| 260 |
+
let base = null
|
| 261 |
+
if (src) {
|
| 262 |
+
const c = src.crop
|
| 263 |
+
const baseKey = `${placement.size}|${c.focusX}|${c.focusY}|${c.zoom}|${c.flipH}|${c.flipV}|${d.removeBackground}|${d.bgTolerance}`
|
| 264 |
+
if (this._baseKey !== baseKey) {
|
| 265 |
+
const sampled = sampleImage(this._nodes.work, src.image, placement.size, c)
|
| 266 |
+
if (d.removeBackground) sampled.mask = removeSimpleBackground(sampled.rgb, placement.size, d.bgTolerance)
|
| 267 |
+
this._base = sampled
|
| 268 |
+
this._baseKey = baseKey
|
| 269 |
+
}
|
| 270 |
+
base = this._base
|
| 271 |
+
}
|
| 272 |
+
const pal = preparePalette(d.paletteId)
|
| 273 |
+
this._artwork = computeArtwork(base, this._qr, placement, pal, {
|
| 274 |
+
modeId: d.modeId, coherence: d.coherence, cleanup: d.cleanup,
|
| 275 |
+
})
|
| 276 |
+
},
|
| 277 |
+
|
| 278 |
+
afterCompute(token) {
|
| 279 |
+
const art = this._artwork
|
| 280 |
+
this.drawPreview()
|
| 281 |
+
const usageAll = art.usage.map((u) => ({ id: u.id, hex: u.hex, count: u.count }))
|
| 282 |
+
this.setData({
|
| 283 |
+
generating: false,
|
| 284 |
+
isSample: this._qr.isSample,
|
| 285 |
+
gridText: `${art.size}×${art.size}`,
|
| 286 |
+
beadsText: formatNumber(art.beads),
|
| 287 |
+
colorsText: String(art.usage.length),
|
| 288 |
+
qrGridText: `${this._qr.size}×${this._qr.size}`,
|
| 289 |
+
usage: usageAll.slice(0, 12),
|
| 290 |
+
usageAll: this.data.usageExpanded ? usageAll : [],
|
| 291 |
+
usageMore: Math.max(0, usageAll.length - 12),
|
| 292 |
+
scanState: 'checking',
|
| 293 |
+
})
|
| 294 |
+
this._usageAll = usageAll
|
| 295 |
+
// 扫码自检放到下一轮,避免阻塞预览出图
|
| 296 |
+
this.debounce('scan', () => {
|
| 297 |
+
if (token !== this._token) return
|
| 298 |
+
const expected = this.data.text.trim() || SAMPLE_TEXT
|
| 299 |
+
const result = selfCheck(art, expected)
|
| 300 |
+
if (token === this._token) this.setData({ scanState: result.ok ? 'ok' : 'fail' })
|
| 301 |
+
}, 60)
|
| 302 |
+
},
|
| 303 |
+
|
| 304 |
+
drawPreview() {
|
| 305 |
+
const node = this._nodes.preview
|
| 306 |
+
if (!node || !this._artwork) return
|
| 307 |
+
const px = Math.round(this._previewCss * this._dpr)
|
| 308 |
+
renderPreview(node, this._artwork, px, this.data.beadStyle)
|
| 309 |
+
},
|
| 310 |
+
|
| 311 |
+
// —— 动态演示 ——
|
| 312 |
+
|
| 313 |
+
syncIntro() {
|
| 314 |
+
const should = this.data.showIntro && !this._hidden && !this._scrolledAway && !this.data.cropVisible
|
| 315 |
+
if (should) this.startIntro()
|
| 316 |
+
else this.pauseIntro()
|
| 317 |
+
},
|
| 318 |
+
|
| 319 |
+
startIntro() {
|
| 320 |
+
if (this._introPlayer) return
|
| 321 |
+
const node = this._nodes.preview
|
| 322 |
+
if (!node || !this._nodes.work) return
|
| 323 |
+
if (!this._intro) {
|
| 324 |
+
try {
|
| 325 |
+
this._intro = createIntro({ workCanvas: this._nodes.work })
|
| 326 |
+
} catch (error) {
|
| 327 |
+
console.error('演示动画初始化失败', error)
|
| 328 |
+
return
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
const px = Math.round(this._previewCss * this._dpr)
|
| 332 |
+
node.width = px
|
| 333 |
+
node.height = px
|
| 334 |
+
const ctx = node.getContext('2d')
|
| 335 |
+
const raf = node.requestAnimationFrame ? (f) => node.requestAnimationFrame(f) : (f) => setTimeout(f, 16)
|
| 336 |
+
const caf = node.cancelAnimationFrame ? (id) => node.cancelAnimationFrame(id) : (id) => clearTimeout(id)
|
| 337 |
+
const player = { start: Date.now() - (this._introElapsed || 0), last: 0, id: 0, caf }
|
| 338 |
+
const tick = () => {
|
| 339 |
+
if (this._introPlayer !== player) return
|
| 340 |
+
const now = Date.now()
|
| 341 |
+
if (now - player.last >= 30) { // 约 30 帧/秒,流畅且省电
|
| 342 |
+
player.last = now
|
| 343 |
+
this._introElapsed = now - player.start
|
| 344 |
+
this._intro.draw(ctx, this._introElapsed / 1000, px)
|
| 345 |
+
}
|
| 346 |
+
player.id = raf(tick)
|
| 347 |
+
}
|
| 348 |
+
this._introPlayer = player
|
| 349 |
+
tick()
|
| 350 |
+
},
|
| 351 |
+
|
| 352 |
+
pauseIntro() {
|
| 353 |
+
const player = this._introPlayer
|
| 354 |
+
if (!player) return
|
| 355 |
+
this._introPlayer = null
|
| 356 |
+
player.caf(player.id)
|
| 357 |
+
},
|
| 358 |
+
|
| 359 |
+
stopIntro() {
|
| 360 |
+
this.pauseIntro()
|
| 361 |
+
this._introElapsed = 0
|
| 362 |
+
},
|
| 363 |
+
|
| 364 |
+
replayIntro() {
|
| 365 |
+
if (!this.data.showIntro) return
|
| 366 |
+
this.stopIntro()
|
| 367 |
+
this.syncIntro()
|
| 368 |
+
},
|
| 369 |
+
|
| 370 |
+
// 引导用户开始:滚到第 1 步并弹出键盘
|
| 371 |
+
startCreate() {
|
| 372 |
+
wx.pageScrollTo({
|
| 373 |
+
selector: '#step1',
|
| 374 |
+
offsetTop: -(this.data.statusBarHeight + this.data.navHeight + 12),
|
| 375 |
+
duration: 300,
|
| 376 |
+
})
|
| 377 |
+
setTimeout(() => this.setData({ textFocus: true }), 320)
|
| 378 |
+
},
|
| 379 |
+
|
| 380 |
+
onTextBlur() {
|
| 381 |
+
if (this.data.textFocus) this.setData({ textFocus: false })
|
| 382 |
+
},
|
| 383 |
+
|
| 384 |
+
// —— 1. 内容 ——
|
| 385 |
+
|
| 386 |
+
onTextInput(e) {
|
| 387 |
+
const text = e.detail.value
|
| 388 |
+
this.setData({ text, textStatus: text.trim() ? 'checking' : 'idle' })
|
| 389 |
+
this.schedule(220)
|
| 390 |
+
this.debounce('textCheck', () => this.verifyText(text), 650)
|
| 391 |
+
},
|
| 392 |
+
|
| 393 |
+
clearText() {
|
| 394 |
+
this.setData({ text: '', textStatus: 'idle' })
|
| 395 |
+
this.schedule(0)
|
| 396 |
+
},
|
| 397 |
+
|
| 398 |
+
verifyText(raw) {
|
| 399 |
+
const value = String(raw || '').trim()
|
| 400 |
+
if (!value) return
|
| 401 |
+
security.checkText(value, 2).then(({ status }) => {
|
| 402 |
+
if (this.data.text.trim() !== value) return
|
| 403 |
+
this.setData({ textStatus: status })
|
| 404 |
+
if (status === 'risky') wx.showToast({ title: security.MESSAGES.risky, icon: 'none' })
|
| 405 |
+
})
|
| 406 |
+
},
|
| 407 |
+
|
| 408 |
+
scanImport() {
|
| 409 |
+
wx.scanCode({
|
| 410 |
+
onlyFromCamera: false,
|
| 411 |
+
scanType: ['qrCode'],
|
| 412 |
+
success: (res) => {
|
| 413 |
+
const decoded = String(res.result || '').trim().slice(0, 120)
|
| 414 |
+
if (!decoded) { wx.showToast({ title: '二维码里没有内容', icon: 'none' }); return }
|
| 415 |
+
this.setData({ scanImportLoading: true })
|
| 416 |
+
// 扫码得到的内容同样先过内容安全检测,通过后才填入
|
| 417 |
+
security.checkText(decoded, 2).then(({ status }) => {
|
| 418 |
+
this.setData({ scanImportLoading: false })
|
| 419 |
+
if (status !== 'pass') { wx.showToast({ title: security.MESSAGES[status], icon: 'none' }); return }
|
| 420 |
+
this.setData({ text: decoded, textStatus: 'pass' })
|
| 421 |
+
this.schedule(0)
|
| 422 |
+
wx.showToast({ title: '已导入二维码内容', icon: 'none' })
|
| 423 |
+
})
|
| 424 |
+
},
|
| 425 |
+
fail: (err) => {
|
| 426 |
+
if (err && /cancel/i.test(err.errMsg || '')) return
|
| 427 |
+
wx.showToast({ title: '没有识别到二维码', icon: 'none' })
|
| 428 |
+
},
|
| 429 |
+
})
|
| 430 |
+
},
|
| 431 |
+
|
| 432 |
+
// —— 2. 照片 ——
|
| 433 |
+
|
| 434 |
+
loadImage(path, info) {
|
| 435 |
+
return new Promise((resolve, reject) => {
|
| 436 |
+
const image = this._nodes.work.createImage()
|
| 437 |
+
image.onload = () => {
|
| 438 |
+
if (info && info.width) { image._w = info.width; image._h = info.height }
|
| 439 |
+
resolve(image)
|
| 440 |
+
}
|
| 441 |
+
image.onerror = () => reject(new Error('IMAGE_LOAD_FAILED'))
|
| 442 |
+
image.src = path
|
| 443 |
+
})
|
| 444 |
+
},
|
| 445 |
+
|
| 446 |
+
chooseImage() {
|
| 447 |
+
wx.chooseMedia({
|
| 448 |
+
count: 1,
|
| 449 |
+
mediaType: ['image'],
|
| 450 |
+
sizeType: ['compressed'],
|
| 451 |
+
sourceType: ['album', 'camera'],
|
| 452 |
+
success: (res) => {
|
| 453 |
+
const file = res.tempFiles && res.tempFiles[0]
|
| 454 |
+
if (file && file.tempFilePath) this.prepareImage(file.tempFilePath)
|
| 455 |
+
},
|
| 456 |
+
fail: (err) => {
|
| 457 |
+
if (err && /cancel/i.test(err.errMsg || '')) return
|
| 458 |
+
wx.showToast({ title: '没有选到照片', icon: 'none' })
|
| 459 |
+
},
|
| 460 |
+
})
|
| 461 |
+
},
|
| 462 |
+
|
| 463 |
+
prepareImage(path) {
|
| 464 |
+
wx.showLoading({ title: '读取照片…', mask: true })
|
| 465 |
+
new Promise((resolve) => {
|
| 466 |
+
wx.getImageInfo({ src: path, success: resolve, fail: () => resolve(null) })
|
| 467 |
+
})
|
| 468 |
+
.then((info) => this.loadImage(path, info).then((image) => ({ image, info })))
|
| 469 |
+
.then(({ image }) => {
|
| 470 |
+
wx.hideLoading()
|
| 471 |
+
this._pending = { image, path }
|
| 472 |
+
// 选图后立即在后台做内容安全检测;同时打开构图编辑,检测通过后才能使用
|
| 473 |
+
this.startImageCheck(this._pending)
|
| 474 |
+
this.openCrop(true)
|
| 475 |
+
})
|
| 476 |
+
.catch(() => {
|
| 477 |
+
wx.hideLoading()
|
| 478 |
+
wx.showToast({ title: '照片读取失败,请换一张 JPG / PNG', icon: 'none' })
|
| 479 |
+
})
|
| 480 |
+
},
|
| 481 |
+
|
| 482 |
+
startImageCheck(pending) {
|
| 483 |
+
pending.status = 'checking'
|
| 484 |
+
this.setData({ cropCheck: 'checking' })
|
| 485 |
+
pending.check = security.checkImage(pending.path, pending.image, this._nodes.security).then(({ status }) => {
|
| 486 |
+
pending.status = status
|
| 487 |
+
if (this._pending === pending) {
|
| 488 |
+
this.setData({ cropCheck: status })
|
| 489 |
+
if (pending.applyWhenPassed && status === 'pass') this.applyCrop()
|
| 490 |
+
pending.applyWhenPassed = false
|
| 491 |
+
}
|
| 492 |
+
return status
|
| 493 |
+
})
|
| 494 |
+
},
|
| 495 |
+
|
| 496 |
+
retryImageCheck() {
|
| 497 |
+
if (this._pending) this.startImageCheck(this._pending)
|
| 498 |
+
},
|
| 499 |
+
|
| 500 |
+
editImage() {
|
| 501 |
+
if (!this._source) return
|
| 502 |
+
this._pending = { image: this._source.image, path: this._source.path, status: 'pass' }
|
| 503 |
+
this.setData({ cropCheck: 'pass' })
|
| 504 |
+
this.openCrop(false)
|
| 505 |
+
},
|
| 506 |
+
|
| 507 |
+
removeImage() {
|
| 508 |
+
this._source = null
|
| 509 |
+
this._base = null
|
| 510 |
+
this._baseKey = ''
|
| 511 |
+
this.setData({ hasImage: false, imageThumb: '', imageMeta: '', removeBackground: false })
|
| 512 |
+
this.schedule(0)
|
| 513 |
+
},
|
| 514 |
+
|
| 515 |
+
// —— 裁切编辑 ——
|
| 516 |
+
|
| 517 |
+
openCrop(isNew) {
|
| 518 |
+
const crop = !isNew && this._source ? this._source.crop : null
|
| 519 |
+
this.pauseIntro()
|
| 520 |
+
this.setData({
|
| 521 |
+
cropVisible: true,
|
| 522 |
+
draftFocusX: crop ? crop.focusX : 50,
|
| 523 |
+
draftFocusY: crop ? crop.focusY : 50,
|
| 524 |
+
draftZoom: crop ? Math.round(crop.zoom * 100) : 100,
|
| 525 |
+
draftFlipH: crop ? crop.flipH : false,
|
| 526 |
+
draftFlipV: crop ? crop.flipV : false,
|
| 527 |
+
draftRemoveBg: isNew ? false : this.data.removeBackground,
|
| 528 |
+
draftBgTolerance: isNew ? 22 : this.data.bgTolerance,
|
| 529 |
+
}, () => {
|
| 530 |
+
wx.createSelectorQuery().in(this)
|
| 531 |
+
.select('#crop').fields({ node: true, size: true })
|
| 532 |
+
.select('#crop').boundingClientRect()
|
| 533 |
+
.exec((res) => {
|
| 534 |
+
this._nodes.crop = res[0] && res[0].node
|
| 535 |
+
this._cropCss = (res[0] && res[0].width) || 300
|
| 536 |
+
this._cropRect = res[1] || { left: 0, top: 0 }
|
| 537 |
+
this.drawCrop()
|
| 538 |
+
})
|
| 539 |
+
})
|
| 540 |
+
},
|
| 541 |
+
|
| 542 |
+
drawCrop() {
|
| 543 |
+
const node = this._nodes.crop
|
| 544 |
+
const pending = this._pending
|
| 545 |
+
if (!node || !pending) return
|
| 546 |
+
node.width = CROP_SIZE
|
| 547 |
+
node.height = CROP_SIZE
|
| 548 |
+
const ctx = node.getContext('2d')
|
| 549 |
+
const d = this.data
|
| 550 |
+
ctx.imageSmoothingEnabled = true
|
| 551 |
+
drawCropped(ctx, pending.image, CROP_SIZE, CROP_SIZE, {
|
| 552 |
+
focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV,
|
| 553 |
+
})
|
| 554 |
+
// 三分线,仅辅助构图
|
| 555 |
+
ctx.strokeStyle = 'rgba(255,255,255,0.5)'
|
| 556 |
+
ctx.lineWidth = 1.5
|
| 557 |
+
ctx.beginPath()
|
| 558 |
+
for (let i = 1; i < 3; i += 1) {
|
| 559 |
+
const p = Math.round(CROP_SIZE * i / 3) + 0.5
|
| 560 |
+
ctx.moveTo(p, 0); ctx.lineTo(p, CROP_SIZE)
|
| 561 |
+
ctx.moveTo(0, p); ctx.lineTo(CROP_SIZE, p)
|
| 562 |
+
}
|
| 563 |
+
ctx.stroke()
|
| 564 |
+
},
|
| 565 |
+
|
| 566 |
+
touchPoint(t) {
|
| 567 |
+
const rect = this._cropRect || { left: 0, top: 0 }
|
| 568 |
+
const x = t.clientX == null ? (t.pageX == null ? t.x : t.pageX) : t.clientX
|
| 569 |
+
const y = t.clientY == null ? (t.pageY == null ? t.y : t.pageY) : t.clientY
|
| 570 |
+
return { x: x - rect.left, y: y - rect.top }
|
| 571 |
+
},
|
| 572 |
+
|
| 573 |
+
touchDistance(touches) {
|
| 574 |
+
const a = this.touchPoint(touches[0])
|
| 575 |
+
const b = this.touchPoint(touches[1])
|
| 576 |
+
return Math.hypot(b.x - a.x, b.y - a.y)
|
| 577 |
+
},
|
| 578 |
+
|
| 579 |
+
onCropStart(e) {
|
| 580 |
+
const touches = e.touches || []
|
| 581 |
+
if (!touches[0]) return
|
| 582 |
+
if (touches.length >= 2) {
|
| 583 |
+
this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom }
|
| 584 |
+
return
|
| 585 |
+
}
|
| 586 |
+
this._gesture = { type: 'drag', ...this.touchPoint(touches[0]), focusX: this.data.draftFocusX, focusY: this.data.draftFocusY }
|
| 587 |
+
},
|
| 588 |
+
|
| 589 |
+
onCropMove(e) {
|
| 590 |
+
const touches = e.touches || []
|
| 591 |
+
const g = this._gesture
|
| 592 |
+
const pending = this._pending
|
| 593 |
+
if (!touches[0] || !g || !pending) return
|
| 594 |
+
if (touches.length >= 2) {
|
| 595 |
+
if (g.type !== 'pinch') {
|
| 596 |
+
this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom }
|
| 597 |
+
return
|
| 598 |
+
}
|
| 599 |
+
const zoom = Math.max(100, Math.min(400, Math.round(g.zoom * this.touchDistance(touches) / g.distance)))
|
| 600 |
+
this.setData({ draftZoom: zoom })
|
| 601 |
+
this.debounce('crop', () => this.drawCrop(), 16)
|
| 602 |
+
return
|
| 603 |
+
}
|
| 604 |
+
if (g.type !== 'drag') return
|
| 605 |
+
const p = this.touchPoint(touches[0])
|
| 606 |
+
const cssScale = CROP_SIZE / (this._cropCss || CROP_SIZE)
|
| 607 |
+
const src = getImageDimensions(pending.image)
|
| 608 |
+
const scale = Math.max(CROP_SIZE / src.width, CROP_SIZE / src.height) * (this.data.draftZoom / 100)
|
| 609 |
+
const w = src.width * scale
|
| 610 |
+
const h = src.height * scale
|
| 611 |
+
const clamp = (v) => Math.max(0, Math.min(100, v))
|
| 612 |
+
const dirX = this.data.draftFlipH ? -1 : 1
|
| 613 |
+
const dirY = this.data.draftFlipV ? -1 : 1
|
| 614 |
+
const fx = Math.abs(w - CROP_SIZE) < 0.01 ? g.focusX : clamp(g.focusX + dirX * ((p.x - g.x) * cssScale / (CROP_SIZE - w)) * 100)
|
| 615 |
+
const fy = Math.abs(h - CROP_SIZE) < 0.01 ? g.focusY : clamp(g.focusY + dirY * ((p.y - g.y) * cssScale / (CROP_SIZE - h)) * 100)
|
| 616 |
+
this.setData({ draftFocusX: fx, draftFocusY: fy })
|
| 617 |
+
this.debounce('crop', () => this.drawCrop(), 16)
|
| 618 |
+
},
|
| 619 |
+
|
| 620 |
+
onCropEnd() {
|
| 621 |
+
this._gesture = null
|
| 622 |
+
clearTimeout(this._timers.crop)
|
| 623 |
+
this.drawCrop()
|
| 624 |
+
},
|
| 625 |
+
|
| 626 |
+
onDraftZoom(e) {
|
| 627 |
+
this.setData({ draftZoom: Number(e.detail.value) })
|
| 628 |
+
this.debounce('crop', () => this.drawCrop(), 16)
|
| 629 |
+
},
|
| 630 |
+
resetCrop() { this.setData({ draftFocusX: 50, draftFocusY: 50, draftZoom: 100 }, () => this.drawCrop()) },
|
| 631 |
+
flipH() { this.setData({ draftFlipH: !this.data.draftFlipH }, () => this.drawCrop()) },
|
| 632 |
+
flipV() { this.setData({ draftFlipV: !this.data.draftFlipV }, () => this.drawCrop()) },
|
| 633 |
+
onDraftRemoveBg(e) { this.setData({ draftRemoveBg: e.detail.value }) },
|
| 634 |
+
onDraftBgTolerance(e) { this.setData({ draftBgTolerance: Number(e.detail.value) }) },
|
| 635 |
+
|
| 636 |
+
cancelCrop() {
|
| 637 |
+
if (this._pending) this._pending.applyWhenPassed = false
|
| 638 |
+
this.setData({ cropVisible: false }, () => this.syncIntro())
|
| 639 |
+
},
|
| 640 |
+
|
| 641 |
+
applyCrop() {
|
| 642 |
+
const pending = this._pending
|
| 643 |
+
if (!pending) return
|
| 644 |
+
if (pending.status === 'checking') {
|
| 645 |
+
// 检测未完成:记住用户意图,通过后自动应用
|
| 646 |
+
pending.applyWhenPassed = true
|
| 647 |
+
wx.showToast({ title: '正在做内容安全检测,通过后自动应用', icon: 'none' })
|
| 648 |
+
return
|
| 649 |
+
}
|
| 650 |
+
if (pending.status !== 'pass') {
|
| 651 |
+
wx.showToast({ title: security.MESSAGES[pending.status] || security.MESSAGES.error, icon: 'none' })
|
| 652 |
+
return
|
| 653 |
+
}
|
| 654 |
+
const d = this.data
|
| 655 |
+
const src = getImageDimensions(pending.image)
|
| 656 |
+
this._source = {
|
| 657 |
+
image: pending.image,
|
| 658 |
+
path: pending.path,
|
| 659 |
+
crop: { focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV },
|
| 660 |
+
}
|
| 661 |
+
this._baseKey = ''
|
| 662 |
+
this.setData({
|
| 663 |
+
cropVisible: false,
|
| 664 |
+
hasImage: true,
|
| 665 |
+
imageThumb: pending.path,
|
| 666 |
+
imageMeta: `${src.width}×${src.height} · 已通过安全检测`,
|
| 667 |
+
removeBackground: d.draftRemoveBg,
|
| 668 |
+
bgTolerance: d.draftBgTolerance,
|
| 669 |
+
// 首次导入照片时,二维码默认铺满全图;用户可再缩小
|
| 670 |
+
})
|
| 671 |
+
this.schedule(0)
|
| 672 |
+
},
|
| 673 |
+
|
| 674 |
+
// —— 3. 效果 ——
|
| 675 |
+
|
| 676 |
+
onScaleChanging(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(160) },
|
| 677 |
+
onScaleChange(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(0) },
|
| 678 |
+
pickPosition(e) {
|
| 679 |
+
const { x, y } = e.currentTarget.dataset
|
| 680 |
+
this.setData({ posX: Number(x), posY: Number(y) })
|
| 681 |
+
this.schedule(0)
|
| 682 |
+
},
|
| 683 |
+
toggleFineTune() { this.setData({ fineTuneOpen: !this.data.fineTuneOpen }) },
|
| 684 |
+
onPosX(e) { this.setData({ posX: Number(e.detail.value) }); this.schedule(100) },
|
| 685 |
+
onPosY(e) { this.setData({ posY: Number(e.detail.value) }); this.schedule(100) },
|
| 686 |
+
pickMode(e) { this.setData({ modeId: e.currentTarget.dataset.id }); this.schedule(0) },
|
| 687 |
+
toggleAdvanced() { this.setData({ advancedOpen: !this.data.advancedOpen }) },
|
| 688 |
+
pickPalette(e) { this.setData({ paletteId: e.currentTarget.dataset.id }); this.schedule(0) },
|
| 689 |
+
onCoherence(e) { this.setData({ coherence: Number(e.detail.value) }); this.schedule(200) },
|
| 690 |
+
onCleanup(e) { this.setData({ cleanup: e.detail.value }); this.schedule(0) },
|
| 691 |
+
pickLevel(e) { this.setData({ level: e.currentTarget.dataset.id }); this.schedule(0) },
|
| 692 |
+
pickQuiet(e) { this.setData({ quiet: Number(e.currentTarget.dataset.id) }); this.schedule(0) },
|
| 693 |
+
onBgToggle(e) { this.setData({ removeBackground: e.detail.value }); this.schedule(0) },
|
| 694 |
+
|
| 695 |
+
applyScanFix() {
|
| 696 |
+
// 自检未通过时的一键修复:扫码优先 + 1 格白边 + H 级纠错
|
| 697 |
+
this.setData({ modeId: 'strict', quiet: Math.max(1, this.data.quiet), level: 'H' })
|
| 698 |
+
this.schedule(0)
|
| 699 |
+
wx.showToast({ title: '已切换为扫码优先', icon: 'none' })
|
| 700 |
+
},
|
| 701 |
+
|
| 702 |
+
toggleUsage() {
|
| 703 |
+
const expanded = !this.data.usageExpanded
|
| 704 |
+
this.setData({ usageExpanded: expanded, usageAll: expanded ? (this._usageAll || []) : [] })
|
| 705 |
+
},
|
| 706 |
+
|
| 707 |
+
// 生成当前预览图,用微信“识别图中二维码”实测
|
| 708 |
+
verifyScan() {
|
| 709 |
+
const node = this._nodes.preview
|
| 710 |
+
if (!node) return
|
| 711 |
+
wx.canvasToTempFilePath({
|
| 712 |
+
canvas: node,
|
| 713 |
+
fileType: 'png',
|
| 714 |
+
success: (res) => {
|
| 715 |
+
wx.previewImage({ urls: [res.tempFilePath], showmenu: true })
|
| 716 |
+
wx.showToast({ title: '长按图片,选择“识别图中二维码”', icon: 'none', duration: 2500 })
|
| 717 |
+
},
|
| 718 |
+
fail: () => wx.showToast({ title: '预览生成失败,请重试', icon: 'none' }),
|
| 719 |
+
})
|
| 720 |
+
},
|
| 721 |
+
|
| 722 |
+
// —— 导出 ——
|
| 723 |
+
|
| 724 |
+
openExport() {
|
| 725 |
+
if (!this.data.text.trim()) {
|
| 726 |
+
this.startCreate()
|
| 727 |
+
return
|
| 728 |
+
}
|
| 729 |
+
if (!this._artwork) return
|
| 730 |
+
if (this.data.textStatus === 'risky') {
|
| 731 |
+
wx.showToast({ title: security.MESSAGES.risky, icon: 'none' })
|
| 732 |
+
return
|
| 733 |
+
}
|
| 734 |
+
this.setData({ exportVisible: true, exportResult: null })
|
| 735 |
+
},
|
| 736 |
+
closeExport() {
|
| 737 |
+
if (this.data.exporting) return
|
| 738 |
+
this.setData({ exportVisible: false })
|
| 739 |
+
},
|
| 740 |
+
pickFormat(e) { this.setData({ exportFormat: e.currentTarget.dataset.id, exportResult: null }) },
|
| 741 |
+
pickLayout(e) { this.setData({ exportLayout: e.currentTarget.dataset.id, exportResult: null }) },
|
| 742 |
+
pickQuality(e) { this.setData({ exportQuality: e.currentTarget.dataset.id, exportResult: null }) },
|
| 743 |
+
pickBeadStyle(e) {
|
| 744 |
+
this.setData({ beadStyle: e.currentTarget.dataset.id, exportResult: null }, () => this.drawPreview())
|
| 745 |
+
},
|
| 746 |
+
onGridToggle(e) { this.setData({ gridEnabled: e.detail.value, exportResult: null }) },
|
| 747 |
+
pickInterval(e) { this.setData({ gridInterval: Number(e.currentTarget.dataset.id), exportResult: null }) },
|
| 748 |
+
pickGridColor(e) { this.setData({ gridColor: e.currentTarget.dataset.color, exportResult: null }) },
|
| 749 |
+
onWatermark(e) {
|
| 750 |
+
const watermark = e.detail.value
|
| 751 |
+
this.setData({ watermark, watermarkStatus: watermark.trim() ? 'checking' : 'idle', exportResult: null })
|
| 752 |
+
this.debounce('wmCheck', () => {
|
| 753 |
+
const value = watermark.trim()
|
| 754 |
+
if (!value) return
|
| 755 |
+
security.checkText(value, 1).then(({ status }) => {
|
| 756 |
+
if (this.data.watermark.trim() === value) this.setData({ watermarkStatus: status })
|
| 757 |
+
})
|
| 758 |
+
}, 650)
|
| 759 |
+
},
|
| 760 |
+
|
| 761 |
+
gridSettings() {
|
| 762 |
+
return { enabled: this.data.gridEnabled, interval: this.data.gridInterval, color: this.data.gridColor, opacity: 60 }
|
| 763 |
+
},
|
| 764 |
+
|
| 765 |
+
capture(fileType, quality) {
|
| 766 |
+
return new Promise((resolve, reject) => {
|
| 767 |
+
wx.canvasToTempFilePath({
|
| 768 |
+
canvas: this._nodes.work,
|
| 769 |
+
fileType,
|
| 770 |
+
quality,
|
| 771 |
+
success: (res) => resolve(res.tempFilePath),
|
| 772 |
+
fail: reject,
|
| 773 |
+
})
|
| 774 |
+
})
|
| 775 |
+
},
|
| 776 |
+
|
| 777 |
+
step(label) {
|
| 778 |
+
return new Promise((resolve) => this.setData({ exportStep: label }, () => setTimeout(resolve, 16)))
|
| 779 |
+
},
|
| 780 |
+
|
| 781 |
+
async startExport() {
|
| 782 |
+
if (this.data.exporting || !this._artwork) return
|
| 783 |
+
this.setData({ exporting: true, exportResult: null })
|
| 784 |
+
try {
|
| 785 |
+
// 平台合规:导出前再次确认所有用户文本均已通过内容安全检测(已通过的走缓存,无额外等待)
|
| 786 |
+
await this.step('内容安全校验…')
|
| 787 |
+
const check = await security.checkTexts([
|
| 788 |
+
{ value: this.data.text, scene: 2 },
|
| 789 |
+
{ value: this.data.watermark, scene: 1 },
|
| 790 |
+
])
|
| 791 |
+
if (check.status !== 'pass') {
|
| 792 |
+
wx.showToast({ title: security.MESSAGES[check.status], icon: 'none' })
|
| 793 |
+
if (check.status === 'risky') this.setData({ textStatus: security.isTextPassed(this.data.text) ? 'pass' : 'risky' })
|
| 794 |
+
return
|
| 795 |
+
}
|
| 796 |
+
if (this._source && !this.data.hasImage) throw new Error('IMAGE_STATE')
|
| 797 |
+
this.cleanOldFiles()
|
| 798 |
+
if (this.data.exportFormat === 'png') await this.exportPng()
|
| 799 |
+
else await this.exportPdf()
|
| 800 |
+
} catch (error) {
|
| 801 |
+
console.error('导出失败', error)
|
| 802 |
+
wx.showToast({ title: '导出失败,请重试(可尝试“标准”清晰度)', icon: 'none' })
|
| 803 |
+
} finally {
|
| 804 |
+
this.setData({ exporting: false, exportStep: '' })
|
| 805 |
+
}
|
| 806 |
+
},
|
| 807 |
+
|
| 808 |
+
// 按清晰度求每格像素:高清长边约 4000px,标准约 2400px
|
| 809 |
+
pngCellSize() {
|
| 810 |
+
const size = this._artwork.size
|
| 811 |
+
const maxSide = this.data.exportQuality === 'hd' ? 4000 : 2400
|
| 812 |
+
let cell = Math.max(12, Math.min(72, Math.floor(maxSide / (size + 5))))
|
| 813 |
+
while (cell > 12) {
|
| 814 |
+
const L = patternPageLayout(size, cell)
|
| 815 |
+
if (Math.max(L.width, L.height) <= maxSide) break
|
| 816 |
+
cell -= 1
|
| 817 |
+
}
|
| 818 |
+
return cell
|
| 819 |
+
},
|
| 820 |
+
|
| 821 |
+
patternMeta() {
|
| 822 |
+
const art = this._artwork
|
| 823 |
+
return `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${art.pal.label} · 扫码内容:${this.data.text.trim()}`
|
| 824 |
+
},
|
| 825 |
+
|
| 826 |
+
infoParams(width) {
|
| 827 |
+
return {
|
| 828 |
+
artwork: this._artwork,
|
| 829 |
+
width,
|
| 830 |
+
text: this.data.text.trim(),
|
| 831 |
+
paletteLabel: `${this._artwork.pal.label}(本作品用 ${this._artwork.usage.length} 色)`,
|
| 832 |
+
mpCode: this._mpCode || null,
|
| 833 |
+
thumbnail: true,
|
| 834 |
+
}
|
| 835 |
+
},
|
| 836 |
+
|
| 837 |
+
async exportPng() {
|
| 838 |
+
const node = this._nodes.work
|
| 839 |
+
const art = this._artwork
|
| 840 |
+
const cell = this.pngCellSize()
|
| 841 |
+
const L = patternPageLayout(art.size, cell)
|
| 842 |
+
const wm = this.data.watermark.trim()
|
| 843 |
+
const drawPattern = (ctx) => drawPatternPage(ctx, art, { cell, style: this.data.beadStyle, grid: this.gridSettings(), meta: this.patternMeta() })
|
| 844 |
+
const files = []
|
| 845 |
+
if (this.data.exportLayout === 'separate') {
|
| 846 |
+
await this.step('绘制图纸…')
|
| 847 |
+
node.width = L.width
|
| 848 |
+
node.height = L.height
|
| 849 |
+
const ctx = node.getContext('2d')
|
| 850 |
+
drawPattern(ctx)
|
| 851 |
+
drawWatermark(ctx, L.width, L.height, wm)
|
| 852 |
+
files.push({ name: '拼豆图纸', path: await this.capture('png') })
|
| 853 |
+
const info = buildInfoPages(this.infoParams(this.data.exportQuality === 'hd' ? 2000 : 1400))
|
| 854 |
+
for (let p = 0; p < info.pageCount; p += 1) {
|
| 855 |
+
await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`)
|
| 856 |
+
node.width = info.width
|
| 857 |
+
node.height = info.height
|
| 858 |
+
const c = node.getContext('2d')
|
| 859 |
+
info.draw(c, p)
|
| 860 |
+
drawWatermark(c, info.width, info.height, wm)
|
| 861 |
+
files.push({ name: info.pageCount > 1 ? `用豆清单 ${p + 1}` : '用豆清单', path: await this.capture('png') })
|
| 862 |
+
}
|
| 863 |
+
} else {
|
| 864 |
+
await this.step('绘制图纸与清单…')
|
| 865 |
+
const info = buildInfoPages(this.infoParams(L.width))
|
| 866 |
+
const gap = Math.round(L.width * 0.02)
|
| 867 |
+
const totalH = L.height + info.pageCount * (info.height + gap)
|
| 868 |
+
const scale = fitScale(L.width, totalH)
|
| 869 |
+
node.width = Math.round(L.width * scale)
|
| 870 |
+
node.height = Math.round(totalH * scale)
|
| 871 |
+
const ctx = node.getContext('2d')
|
| 872 |
+
ctx.fillStyle = '#e8e5de'
|
| 873 |
+
ctx.fillRect(0, 0, node.width, node.height)
|
| 874 |
+
ctx.scale(scale, scale)
|
| 875 |
+
drawPattern(ctx)
|
| 876 |
+
for (let p = 0; p < info.pageCount; p += 1) {
|
| 877 |
+
ctx.save()
|
| 878 |
+
ctx.translate(0, L.height + gap + p * (info.height + gap))
|
| 879 |
+
info.draw(ctx, p)
|
| 880 |
+
ctx.restore()
|
| 881 |
+
}
|
| 882 |
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
| 883 |
+
drawWatermark(ctx, node.width, node.height, wm)
|
| 884 |
+
files.push({ name: '图纸 + 用豆清单', path: await this.capture('png') })
|
| 885 |
+
}
|
| 886 |
+
await this.step('保存到相册…')
|
| 887 |
+
const saved = await this.saveImages(files.map((f) => f.path))
|
| 888 |
+
this.setData({ exportResult: { kind: 'png', saved, files } })
|
| 889 |
+
if (saved) wx.showToast({ title: `${files.length} 张图片已保存`, icon: 'success' })
|
| 890 |
+
},
|
| 891 |
+
|
| 892 |
+
saveImages(paths) {
|
| 893 |
+
return paths.reduce((chain, path) => chain.then((ok) => (ok ? new Promise((resolve) => {
|
| 894 |
+
wx.saveImageToPhotosAlbum({
|
| 895 |
+
filePath: path,
|
| 896 |
+
success: () => resolve(true),
|
| 897 |
+
fail: (err) => {
|
| 898 |
+
if (err && /auth|deny/i.test(err.errMsg || '')) {
|
| 899 |
+
wx.showModal({
|
| 900 |
+
title: '需要相册权限',
|
| 901 |
+
content: '请允许保存到相册,才能把图纸存到手机里',
|
| 902 |
+
confirmText: '去设置',
|
| 903 |
+
success: (r) => { if (r.confirm) wx.openSetting() },
|
| 904 |
+
})
|
| 905 |
+
}
|
| 906 |
+
resolve(false)
|
| 907 |
+
},
|
| 908 |
+
})
|
| 909 |
+
}) : false)), Promise.resolve(true))
|
| 910 |
+
},
|
| 911 |
+
|
| 912 |
+
previewResult() {
|
| 913 |
+
const result = this.data.exportResult
|
| 914 |
+
if (!result) return
|
| 915 |
+
if (result.kind === 'png') {
|
| 916 |
+
wx.previewImage({ urls: result.files.map((f) => f.path), showmenu: true })
|
| 917 |
+
} else {
|
| 918 |
+
this.openPdf(result.files[0].path)
|
| 919 |
+
}
|
| 920 |
+
},
|
| 921 |
+
|
| 922 |
+
openResultFile(e) {
|
| 923 |
+
const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)]
|
| 924 |
+
if (!file) return
|
| 925 |
+
if (this.data.exportResult.kind === 'png') wx.previewImage({ urls: [file.path], showmenu: true })
|
| 926 |
+
else this.openPdf(file.path)
|
| 927 |
+
},
|
| 928 |
+
|
| 929 |
+
shareResultFile(e) {
|
| 930 |
+
const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)]
|
| 931 |
+
if (!file || !wx.shareFileMessage) {
|
| 932 |
+
wx.showToast({ title: '当前微信版本不支持,请先打开再转发', icon: 'none' })
|
| 933 |
+
return
|
| 934 |
+
}
|
| 935 |
+
wx.shareFileMessage({ filePath: file.path, fileName: file.fileName || `${file.name}.pdf` })
|
| 936 |
+
},
|
| 937 |
+
|
| 938 |
+
openPdf(path) {
|
| 939 |
+
wx.openDocument({ filePath: path, fileType: 'pdf', showMenu: true })
|
| 940 |
+
},
|
| 941 |
+
|
| 942 |
+
cleanOldFiles() {
|
| 943 |
+
try {
|
| 944 |
+
const fs = wx.getFileSystemManager()
|
| 945 |
+
fs.readdirSync(wx.env.USER_DATA_PATH)
|
| 946 |
+
.filter((name) => name.indexOf(FILE_PREFIX) === 0)
|
| 947 |
+
.forEach((name) => { try { fs.unlinkSync(`${wx.env.USER_DATA_PATH}/${name}`) } catch (e) { /* 忽略 */ } })
|
| 948 |
+
} catch (e) { /* 忽略 */ }
|
| 949 |
+
},
|
| 950 |
+
|
| 951 |
+
async canvasJpeg(width, height, draw, quality = 0.9) {
|
| 952 |
+
const node = this._nodes.work
|
| 953 |
+
node.width = width
|
| 954 |
+
node.height = height
|
| 955 |
+
const ctx = node.getContext('2d')
|
| 956 |
+
draw(ctx)
|
| 957 |
+
const path = await this.capture('jpg', quality)
|
| 958 |
+
return wx.getFileSystemManager().readFileSync(path)
|
| 959 |
+
},
|
| 960 |
+
|
| 961 |
+
// 页眉(中文)渲染为小图,矢量页面上方使用
|
| 962 |
+
async renderHeader(title, sub) {
|
| 963 |
+
const w = 1540
|
| 964 |
+
const h = 80
|
| 965 |
+
const data = await this.canvasJpeg(w, h, (ctx) => {
|
| 966 |
+
ctx.fillStyle = '#ffffff'
|
| 967 |
+
ctx.fillRect(0, 0, w, h)
|
| 968 |
+
ctx.fillStyle = '#16211f'
|
| 969 |
+
ctx.font = 'bold 34px sans-serif'
|
| 970 |
+
ctx.textBaseline = 'middle'
|
| 971 |
+
ctx.fillText(title, 0, 30)
|
| 972 |
+
ctx.fillStyle = '#6f7a76'
|
| 973 |
+
ctx.font = '22px sans-serif'
|
| 974 |
+
ctx.fillText(sub, 0, 64)
|
| 975 |
+
ctx.fillStyle = '#0f766e'
|
| 976 |
+
ctx.font = 'bold 24px sans-serif'
|
| 977 |
+
ctx.textAlign = 'right'
|
| 978 |
+
ctx.fillText('豆维 · 可扫码的拼豆', w, 30)
|
| 979 |
+
}, 0.92)
|
| 980 |
+
return { data, width: w, height: h }
|
| 981 |
+
},
|
| 982 |
+
|
| 983 |
+
// 水印转为 1-bit 蒙版,矢量页上以半透明平铺
|
| 984 |
+
watermarkMask(doc, text) {
|
| 985 |
+
const value = String(text || '').trim()
|
| 986 |
+
if (!value) return null
|
| 987 |
+
const node = this._nodes.work
|
| 988 |
+
const fontSize = 64
|
| 989 |
+
node.width = 16
|
| 990 |
+
node.height = 16
|
| 991 |
+
let ctx = node.getContext('2d')
|
| 992 |
+
ctx.font = `bold ${fontSize}px sans-serif`
|
| 993 |
+
const width = Math.min(2000, Math.ceil(ctx.measureText(value).width) + 16)
|
| 994 |
+
const height = Math.round(fontSize * 1.4)
|
| 995 |
+
node.width = width
|
| 996 |
+
node.height = height
|
| 997 |
+
ctx = node.getContext('2d')
|
| 998 |
+
ctx.clearRect(0, 0, width, height)
|
| 999 |
+
ctx.fillStyle = '#000000'
|
| 1000 |
+
ctx.font = `bold ${fontSize}px sans-serif`
|
| 1001 |
+
ctx.textBaseline = 'middle'
|
| 1002 |
+
ctx.fillText(value, 8, height / 2)
|
| 1003 |
+
const pixels = ctx.getImageData(0, 0, width, height).data
|
| 1004 |
+
const rowBytes = Math.ceil(width / 8)
|
| 1005 |
+
const bits = new Uint8Array(rowBytes * height).fill(0xff) // 1 = 不绘制
|
| 1006 |
+
for (let y = 0; y < height; y += 1) {
|
| 1007 |
+
for (let x = 0; x < width; x += 1) {
|
| 1008 |
+
if (pixels[(y * width + x) * 4 + 3] > 110) bits[y * rowBytes + (x >> 3)] &= ~(0x80 >> (x & 7))
|
| 1009 |
+
}
|
| 1010 |
+
}
|
| 1011 |
+
return { id: doc.addImageMask(bits, width, height), width, height }
|
| 1012 |
+
},
|
| 1013 |
+
|
| 1014 |
+
async buildPatternPdf(doc) {
|
| 1015 |
+
const art = this._artwork
|
| 1016 |
+
const plan = planPatternPages(art.size)
|
| 1017 |
+
const headers = []
|
| 1018 |
+
for (let i = 0; i < plan.pages.length; i += 1) {
|
| 1019 |
+
const page = plan.pages[i]
|
| 1020 |
+
await this.step(`绘制图纸页 ${i + 1}/${plan.pages.length}…`)
|
| 1021 |
+
const title = page.kind === 'overview'
|
| 1022 |
+
? (plan.tiled ? `图纸总览 · 共分 ${plan.pages.length - 1} 块` : '拼豆图纸')
|
| 1023 |
+
: `第 ${page.tile} 块 · 列 ${page.region.col + 1}–${page.region.col + page.region.cols} · 行 ${page.region.row + 1}–${page.region.row + page.region.rows}`
|
| 1024 |
+
const sub = page.kind === 'overview'
|
| 1025 |
+
? `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${plan.tiled ? '红框编号对应后续分块页,按块拼更轻松' : '格内为 MARD 色号'}`
|
| 1026 |
+
: `第 ${i + 1} / ${plan.pages.length} 页 · 格内为 MARD 色号 · 坐标为整张作品的行列号`
|
| 1027 |
+
const header = await this.renderHeader(title, sub)
|
| 1028 |
+
headers.push({ id: doc.addJpeg(header.data, header.width, header.height), width: header.width, height: header.height })
|
| 1029 |
+
}
|
| 1030 |
+
const watermark = this.watermarkMask(doc, this.data.watermark)
|
| 1031 |
+
addPatternPages(doc, art, { plan, headers, style: this.data.beadStyle, grid: this.gridSettings(), watermark })
|
| 1032 |
+
},
|
| 1033 |
+
|
| 1034 |
+
async buildInfoPdf(doc) {
|
| 1035 |
+
const info = buildInfoPages(this.infoParams(1654))
|
| 1036 |
+
for (let p = 0; p < info.pageCount; p += 1) {
|
| 1037 |
+
await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`)
|
| 1038 |
+
const data = await this.canvasJpeg(info.width, info.height, (ctx) => {
|
| 1039 |
+
info.draw(ctx, p)
|
| 1040 |
+
drawWatermark(ctx, info.width, info.height, this.data.watermark)
|
| 1041 |
+
}, 0.9)
|
| 1042 |
+
addImagePage(doc, doc.addJpeg(data, info.width, info.height), info.width, info.height)
|
| 1043 |
+
}
|
| 1044 |
+
},
|
| 1045 |
+
|
| 1046 |
+
async exportPdf() {
|
| 1047 |
+
const fs = wx.getFileSystemManager()
|
| 1048 |
+
const stamp = Date.now().toString(36)
|
| 1049 |
+
const art = this._artwork
|
| 1050 |
+
const base = `${FILE_PREFIX}${art.size}x${art.size}-${stamp}`
|
| 1051 |
+
const files = []
|
| 1052 |
+
const write = (doc, suffix, name) => {
|
| 1053 |
+
const fileName = `豆维-${name}.pdf`
|
| 1054 |
+
const path = `${wx.env.USER_DATA_PATH}/${base}-${suffix}.pdf`
|
| 1055 |
+
fs.writeFileSync(path, doc.build(), 'binary')
|
| 1056 |
+
files.push({ name, fileName, path })
|
| 1057 |
+
}
|
| 1058 |
+
if (this.data.exportLayout === 'separate') {
|
| 1059 |
+
const patternDoc = new PdfDoc()
|
| 1060 |
+
await this.buildPatternPdf(patternDoc)
|
| 1061 |
+
write(patternDoc, 'pattern', '拼豆图纸')
|
| 1062 |
+
const infoDoc = new PdfDoc()
|
| 1063 |
+
await this.buildInfoPdf(infoDoc)
|
| 1064 |
+
write(infoDoc, 'info', '用豆清单')
|
| 1065 |
+
} else {
|
| 1066 |
+
const doc = new PdfDoc()
|
| 1067 |
+
await this.buildPatternPdf(doc)
|
| 1068 |
+
await this.buildInfoPdf(doc)
|
| 1069 |
+
write(doc, 'all', '图纸与用豆清单')
|
| 1070 |
+
}
|
| 1071 |
+
this.setData({ exportResult: { kind: 'pdf', saved: true, files } })
|
| 1072 |
+
},
|
| 1073 |
+
})
|
BeadQR/miniprogram/pages/index/index.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"navigationStyle": "custom",
|
| 3 |
+
"navigationBarTextStyle": "white",
|
| 4 |
+
"backgroundColorTop": "#0f3d3a",
|
| 5 |
+
"usingComponents": {}
|
| 6 |
+
}
|
BeadQR/miniprogram/pages/index/index.wxml
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- 自定义导航栏 -->
|
| 2 |
+
<view class="nav {{navSolid ? 'nav-solid' : ''}}" style="padding-top: {{statusBarHeight}}px; height: {{navHeight}}px;">
|
| 3 |
+
<view class="nav-title">
|
| 4 |
+
<image class="nav-logo" src="/assets/icon.png" mode="aspectFill" />
|
| 5 |
+
<text>豆维 · 可扫码的拼豆</text>
|
| 6 |
+
</view>
|
| 7 |
+
</view>
|
| 8 |
+
|
| 9 |
+
<view class="page" style="padding-top: {{statusBarHeight + navHeight}}px;">
|
| 10 |
+
<!-- 首屏:一句话说清楚是做什么的 -->
|
| 11 |
+
<view class="hero">
|
| 12 |
+
<view class="hero-badge">MARD 拼豆图纸生成器</view>
|
| 13 |
+
<view class="hero-title">把一句话藏进拼豆里</view>
|
| 14 |
+
<view class="hero-sub">拼好的作品用微信一扫,就能读出你写的文字或链接</view>
|
| 15 |
+
<view class="hero-steps">
|
| 16 |
+
<view class="hero-step"><text class="num">1</text>写一句话</view>
|
| 17 |
+
<text class="hero-arrow">›</text>
|
| 18 |
+
<view class="hero-step"><text class="num">2</text>选张照片</view>
|
| 19 |
+
<text class="hero-arrow">›</text>
|
| 20 |
+
<view class="hero-step"><text class="num">3</text>导出图纸</view>
|
| 21 |
+
</view>
|
| 22 |
+
</view>
|
| 23 |
+
|
| 24 |
+
<!-- 实时预览:无内容时播放动态演示 -->
|
| 25 |
+
<view class="card preview-card">
|
| 26 |
+
<view class="preview-box" bindtap="replayIntro">
|
| 27 |
+
<canvas id="preview" type="2d" class="preview-canvas"></canvas>
|
| 28 |
+
<view wx:if="{{isSample && !showIntro}}" class="sample-tag">示例二维码 · 写下内容后换成你的</view>
|
| 29 |
+
<view wx:if="{{generating}}" class="generating"><view class="spinner"></view></view>
|
| 30 |
+
</view>
|
| 31 |
+
|
| 32 |
+
<block wx:if="{{showIntro}}">
|
| 33 |
+
<view class="intro-tip">
|
| 34 |
+
<view class="intro-dot"></view>
|
| 35 |
+
<text>上面是演示:写下一句话,这里就会变成你专属的可扫码图纸</text>
|
| 36 |
+
</view>
|
| 37 |
+
<view class="stats features">
|
| 38 |
+
<view class="stat"><view class="stat-v">221 色</view><view class="stat-k">MARD 标准色号</view></view>
|
| 39 |
+
<view class="stat"><view class="stat-v">A4</view><view class="stat-k">打印图纸</view></view>
|
| 40 |
+
<view class="stat"><view class="stat-v">扫一扫</view><view class="stat-k">微信直接读出</view></view>
|
| 41 |
+
</view>
|
| 42 |
+
<view class="preview-actions">
|
| 43 |
+
<view class="cta-btn" bindtap="startCreate">开始制作 · 写下第一句话</view>
|
| 44 |
+
</view>
|
| 45 |
+
</block>
|
| 46 |
+
|
| 47 |
+
<block wx:else>
|
| 48 |
+
<view class="scan-row">
|
| 49 |
+
<view class="scan-badge scan-{{isSample ? 'idle' : scanState}}">
|
| 50 |
+
<text wx:if="{{isSample}}">写下内容后,自动检查能否扫出</text>
|
| 51 |
+
<text wx:elif="{{scanState === 'checking'}}">正在检查能否扫出…</text>
|
| 52 |
+
<text wx:elif="{{scanState === 'ok'}}">✓ 自检通过,可以扫出内容</text>
|
| 53 |
+
<text wx:elif="{{scanState === 'fail'}}">! 自检未扫出,建议优化</text>
|
| 54 |
+
<text wx:else>生成后自动检查能否扫码</text>
|
| 55 |
+
</view>
|
| 56 |
+
<view wx:if="{{!isSample && scanState === 'fail'}}" class="mini-btn accent" bindtap="applyScanFix">一键优化</view>
|
| 57 |
+
</view>
|
| 58 |
+
|
| 59 |
+
<view class="stats">
|
| 60 |
+
<view class="stat"><view class="stat-v">{{gridText}}</view><view class="stat-k">格子</view></view>
|
| 61 |
+
<view class="stat"><view class="stat-v">{{beadsText}}</view><view class="stat-k">颗豆子</view></view>
|
| 62 |
+
<view class="stat"><view class="stat-v">{{colorsText}}</view><view class="stat-k">种颜色</view></view>
|
| 63 |
+
</view>
|
| 64 |
+
|
| 65 |
+
<view class="preview-actions">
|
| 66 |
+
<view wx:if="{{isSample}}" class="cta-btn" bindtap="startCreate">写下内容,生成你的二维码</view>
|
| 67 |
+
<view wx:else class="ghost-btn" bindtap="verifyScan">用微信扫一扫验证</view>
|
| 68 |
+
</view>
|
| 69 |
+
</block>
|
| 70 |
+
</view>
|
| 71 |
+
|
| 72 |
+
<!-- 第 1 步:内容 -->
|
| 73 |
+
<view class="card step" id="step1">
|
| 74 |
+
<view class="step-head">
|
| 75 |
+
<view class="step-num">1</view>
|
| 76 |
+
<view>
|
| 77 |
+
<view class="step-title">扫码后要看到什么?</view>
|
| 78 |
+
<view class="step-desc">一句祝福、一个网址,或者你的名字</view>
|
| 79 |
+
</view>
|
| 80 |
+
</view>
|
| 81 |
+
<view class="input-wrap {{textStatus === 'risky' ? 'input-bad' : ''}}">
|
| 82 |
+
<textarea class="textarea" value="{{text}}" maxlength="300" auto-height placeholder="Happy Birthday!" placeholder-class="ph" focus="{{textFocus}}" bindinput="onTextInput" bindblur="onTextBlur" />
|
| 83 |
+
<view class="input-foot">
|
| 84 |
+
<view class="text-status status-{{textStatus}}">
|
| 85 |
+
<text wx:if="{{textStatus === 'checking'}}">内容安全检测中…</text>
|
| 86 |
+
<text wx:elif="{{textStatus === 'pass'}}">✓ 已通过内容安全检测</text>
|
| 87 |
+
<text wx:elif="{{textStatus === 'risky'}}">内容可能不合规,请修改</text>
|
| 88 |
+
<text wx:elif="{{textStatus === 'error'}}">检测暂不可用,请稍后重试</text>
|
| 89 |
+
<text wx:else>{{text.length}}/300</text>
|
| 90 |
+
</view>
|
| 91 |
+
<view class="input-tools">
|
| 92 |
+
<text wx:if="{{text}}" class="tool" bindtap="clearText">清空</text>
|
| 93 |
+
<text class="tool tool-brand" bindtap="scanImport">{{scanImportLoading ? '识别中…' : '扫码导入'}}</text>
|
| 94 |
+
</view>
|
| 95 |
+
</view>
|
| 96 |
+
</view>
|
| 97 |
+
<view class="tip">字越少,格子越少,拼起来越快</view>
|
| 98 |
+
</view>
|
| 99 |
+
|
| 100 |
+
<!-- 第 2 步:照片 -->
|
| 101 |
+
<view class="card step">
|
| 102 |
+
<view class="step-head">
|
| 103 |
+
<view class="step-num">2</view>
|
| 104 |
+
<view>
|
| 105 |
+
<view class="step-title">选一张照片当底图 <text class="optional">可选</text></view>
|
| 106 |
+
<view class="step-desc">二维码会融进照片里,不选就是纯二维码</view>
|
| 107 |
+
</view>
|
| 108 |
+
</view>
|
| 109 |
+
<view wx:if="{{!hasImage}}" class="upload" bindtap="chooseImage">
|
| 110 |
+
<view class="upload-icon">+</view>
|
| 111 |
+
<view class="upload-text">从相册选择或拍照</view>
|
| 112 |
+
<view class="upload-hint">照片仅在手机上处理,会先经过微信内容安全检测</view>
|
| 113 |
+
</view>
|
| 114 |
+
<view wx:else class="photo">
|
| 115 |
+
<image class="photo-thumb" src="{{imageThumb}}" mode="aspectFill" />
|
| 116 |
+
<view class="photo-info">
|
| 117 |
+
<view class="photo-meta">{{imageMeta}}</view>
|
| 118 |
+
<view class="photo-actions">
|
| 119 |
+
<view class="mini-btn" bindtap="editImage">调整</view>
|
| 120 |
+
<view class="mini-btn" bindtap="chooseImage">换一张</view>
|
| 121 |
+
<view class="mini-btn danger" bindtap="removeImage">移除</view>
|
| 122 |
+
</view>
|
| 123 |
+
</view>
|
| 124 |
+
</view>
|
| 125 |
+
<view wx:if="{{hasImage}}" class="row">
|
| 126 |
+
<view class="row-label">去掉纯色背景</view>
|
| 127 |
+
<switch checked="{{removeBackground}}" color="#0f766e" bindchange="onBgToggle" />
|
| 128 |
+
</view>
|
| 129 |
+
</view>
|
| 130 |
+
|
| 131 |
+
<!-- 第 3 步:效果 -->
|
| 132 |
+
<view class="card step">
|
| 133 |
+
<view class="step-head">
|
| 134 |
+
<view class="step-num">3</view>
|
| 135 |
+
<view>
|
| 136 |
+
<view class="step-title">调整效果</view>
|
| 137 |
+
<view class="step-desc">默认设置已经很好用,想要更好看再微调</view>
|
| 138 |
+
</view>
|
| 139 |
+
</view>
|
| 140 |
+
|
| 141 |
+
<view class="field-label">风格</view>
|
| 142 |
+
<view class="seg">
|
| 143 |
+
<view wx:for="{{matchModes}}" wx:key="id" class="seg-item {{modeId === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickMode">
|
| 144 |
+
<view class="seg-main">{{item.label}}</view>
|
| 145 |
+
<view class="seg-sub">{{item.hint}}</view>
|
| 146 |
+
</view>
|
| 147 |
+
</view>
|
| 148 |
+
|
| 149 |
+
<block wx:if="{{hasImage}}">
|
| 150 |
+
<view class="field-label">二维码大小 <text class="field-val">{{qrScale}}%</text></view>
|
| 151 |
+
<slider min="30" max="100" step="1" value="{{qrScale}}" activeColor="#0f766e" block-size="22" bindchanging="onScaleChanging" bindchange="onScaleChange" />
|
| 152 |
+
<block wx:if="{{qrScale < 100}}">
|
| 153 |
+
<view class="field-label">二维码位置 <text class="link-text" bindtap="toggleFineTune">{{fineTuneOpen ? '收起微调' : '微调'}}</text></view>
|
| 154 |
+
<view class="pos-grid">
|
| 155 |
+
<block wx:for="{{positions}}" wx:for-item="py" wx:key="*this">
|
| 156 |
+
<view wx:for="{{positions}}" wx:for-item="px" wx:key="*this" class="pos-cell {{posX === px && posY === py ? 'on' : ''}}" data-x="{{px}}" data-y="{{py}}" bindtap="pickPosition"></view>
|
| 157 |
+
</block>
|
| 158 |
+
</view>
|
| 159 |
+
<block wx:if="{{fineTuneOpen}}">
|
| 160 |
+
<view class="mini-label">左右 {{posX}}%</view>
|
| 161 |
+
<slider min="0" max="100" value="{{posX}}" activeColor="#0f766e" block-size="20" bindchanging="onPosX" bindchange="onPosX" />
|
| 162 |
+
<view class="mini-label">上下 {{posY}}%</view>
|
| 163 |
+
<slider min="0" max="100" value="{{posY}}" activeColor="#0f766e" block-size="20" bindchanging="onPosY" bindchange="onPosY" />
|
| 164 |
+
</block>
|
| 165 |
+
</block>
|
| 166 |
+
</block>
|
| 167 |
+
|
| 168 |
+
<view class="advanced-toggle" bindtap="toggleAdvanced">
|
| 169 |
+
<text>高级设置(色板、纠错、白边)</text>
|
| 170 |
+
<text class="chev {{advancedOpen ? 'open' : ''}}">›</text>
|
| 171 |
+
</view>
|
| 172 |
+
<view wx:if="{{advancedOpen}}" class="advanced">
|
| 173 |
+
<view class="field-label">色板 <text class="field-hint">颜色越少,买豆越省</text></view>
|
| 174 |
+
<scroll-view scroll-x class="chips-scroll" enhanced show-scrollbar="{{false}}">
|
| 175 |
+
<view class="chips">
|
| 176 |
+
<view wx:for="{{presets}}" wx:key="id" class="chip {{paletteId === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickPalette">
|
| 177 |
+
<view class="chip-main">{{item.short}}</view>
|
| 178 |
+
<view class="chip-sub">{{item.detail}}</view>
|
| 179 |
+
</view>
|
| 180 |
+
</view>
|
| 181 |
+
</scroll-view>
|
| 182 |
+
|
| 183 |
+
<view class="field-label">色块平滑 <text class="field-val">{{coherence}}</text></view>
|
| 184 |
+
<slider min="0" max="100" step="5" value="{{coherence}}" activeColor="#0f766e" block-size="20" bindchange="onCoherence" />
|
| 185 |
+
|
| 186 |
+
<view class="row">
|
| 187 |
+
<view>
|
| 188 |
+
<view class="row-label">合并零散颜色</view>
|
| 189 |
+
<view class="row-hint">把只用到一两颗的颜色换成相近色</view>
|
| 190 |
+
</view>
|
| 191 |
+
<switch checked="{{cleanup}}" color="#0f766e" bindchange="onCleanup" />
|
| 192 |
+
</view>
|
| 193 |
+
|
| 194 |
+
<view class="field-label">纠错等级 <text class="field-hint">等级越高越耐磨损,但格子更多</text></view>
|
| 195 |
+
<view class="seg">
|
| 196 |
+
<view wx:for="{{levels}}" wx:key="id" class="seg-item {{level === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickLevel">
|
| 197 |
+
<view class="seg-main">{{item.label}}</view>
|
| 198 |
+
<view class="seg-sub">{{item.hint}}</view>
|
| 199 |
+
</view>
|
| 200 |
+
</view>
|
| 201 |
+
|
| 202 |
+
<view class="field-label">白边 <text class="field-hint">留 1 格白边更容易扫出</text></view>
|
| 203 |
+
<view class="seg">
|
| 204 |
+
<view wx:for="{{quietOptions}}" wx:key="id" class="seg-item {{quiet === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickQuiet">
|
| 205 |
+
<view class="seg-main">{{item.label}}</view>
|
| 206 |
+
</view>
|
| 207 |
+
</view>
|
| 208 |
+
<view wx:if="{{qrGridText}}" class="tip">二维码本体 {{qrGridText}} 格</view>
|
| 209 |
+
</view>
|
| 210 |
+
</view>
|
| 211 |
+
|
| 212 |
+
<!-- 用豆清单 -->
|
| 213 |
+
<view class="card" wx:if="{{usage.length && !showIntro}}">
|
| 214 |
+
<view class="card-title">需要准备的豆子 <text class="field-hint">MARD 色号 · 按用量排序</text></view>
|
| 215 |
+
<view class="usage">
|
| 216 |
+
<view wx:for="{{usageExpanded ? usageAll : usage}}" wx:key="id" class="usage-item">
|
| 217 |
+
<view class="swatch" style="background: {{item.hex}};"></view>
|
| 218 |
+
<view class="usage-id">{{item.id}}</view>
|
| 219 |
+
<view class="usage-count">{{item.count}}</view>
|
| 220 |
+
</view>
|
| 221 |
+
</view>
|
| 222 |
+
<view wx:if="{{usageMore > 0}}" class="link-text center" bindtap="toggleUsage">{{usageExpanded ? '收起' : '展开全部(还有 ' + usageMore + ' 种)'}}</view>
|
| 223 |
+
</view>
|
| 224 |
+
|
| 225 |
+
<!-- 页脚:内容规范 / 投诉反馈 / 隐私 -->
|
| 226 |
+
<view class="footer">
|
| 227 |
+
<view class="footer-safe"><image class="footer-shield" src="/assets/shield.svg" mode="aspectFit" /><text>文字、水印与照片均经过微信内容安全检测,未通过或检测异常时不能生成与导出</text></view>
|
| 228 |
+
<view class="footer-links">
|
| 229 |
+
<text class="footer-link" bindtap="showRules">内容规范</text>
|
| 230 |
+
<text class="dot">·</text>
|
| 231 |
+
<button class="footer-link footer-btn" open-type="feedback">投诉与反馈</button>
|
| 232 |
+
<text class="dot">·</text>
|
| 233 |
+
<text class="footer-link" bindtap="openPrivacy">隐私保护指引</text>
|
| 234 |
+
</view>
|
| 235 |
+
<view class="footer-copy">豆维 · 可扫码的拼豆</view>
|
| 236 |
+
</view>
|
| 237 |
+
</view>
|
| 238 |
+
|
| 239 |
+
<!-- 底部操作栏 -->
|
| 240 |
+
<view class="bottom-bar">
|
| 241 |
+
<view class="bar-info">
|
| 242 |
+
<block wx:if="{{isSample}}">
|
| 243 |
+
<view class="bar-main">还没有内容</view>
|
| 244 |
+
<view class="bar-sub">写一句话,马上生成图纸</view>
|
| 245 |
+
</block>
|
| 246 |
+
<block wx:else>
|
| 247 |
+
<view class="bar-main">{{gridText}} · {{colorsText}} 色</view>
|
| 248 |
+
<view class="bar-sub">约 {{beadsText}} 颗豆子</view>
|
| 249 |
+
</block>
|
| 250 |
+
</view>
|
| 251 |
+
<view class="primary-btn {{!isSample && textStatus === 'risky' ? 'disabled' : ''}}" bindtap="openExport">{{isSample ? '开始制作' : '导出图纸'}}</view>
|
| 252 |
+
</view>
|
| 253 |
+
|
| 254 |
+
<!-- 导出面板 -->
|
| 255 |
+
<view wx:if="{{exportVisible}}" class="mask" bindtap="closeExport" catchtouchmove="noop">
|
| 256 |
+
<view class="sheet" catchtap="noop">
|
| 257 |
+
<view class="sheet-handle"></view>
|
| 258 |
+
<view class="sheet-title">导出图纸</view>
|
| 259 |
+
|
| 260 |
+
<scroll-view scroll-y class="sheet-body">
|
| 261 |
+
<view class="field-label">格式</view>
|
| 262 |
+
<view class="seg">
|
| 263 |
+
<view class="seg-item {{exportFormat === 'pdf' ? 'on' : ''}}" data-id="pdf" bindtap="pickFormat">
|
| 264 |
+
<view class="seg-main">PDF 文件</view><view class="seg-sub">A4 打印,大图自动分页</view>
|
| 265 |
+
</view>
|
| 266 |
+
<view class="seg-item {{exportFormat === 'png' ? 'on' : ''}}" data-id="png" bindtap="pickFormat">
|
| 267 |
+
<view class="seg-main">图片</view><view class="seg-sub">存到相册,手机上看</view>
|
| 268 |
+
</view>
|
| 269 |
+
</view>
|
| 270 |
+
|
| 271 |
+
<view class="field-label">内容</view>
|
| 272 |
+
<view class="seg">
|
| 273 |
+
<view class="seg-item {{exportLayout === 'combined' ? 'on' : ''}}" data-id="combined" bindtap="pickLayout">
|
| 274 |
+
<view class="seg-main">合并一份</view><view class="seg-sub">图纸 + 用豆清单</view>
|
| 275 |
+
</view>
|
| 276 |
+
<view class="seg-item {{exportLayout === 'separate' ? 'on' : ''}}" data-id="separate" bindtap="pickLayout">
|
| 277 |
+
<view class="seg-main">分开两份</view><view class="seg-sub">图纸、清单各一份</view>
|
| 278 |
+
</view>
|
| 279 |
+
</view>
|
| 280 |
+
|
| 281 |
+
<block wx:if="{{exportFormat === 'png'}}">
|
| 282 |
+
<view class="field-label">清晰度</view>
|
| 283 |
+
<view class="seg">
|
| 284 |
+
<view class="seg-item {{exportQuality === 'hd' ? 'on' : ''}}" data-id="hd" bindtap="pickQuality"><view class="seg-main">高清</view></view>
|
| 285 |
+
<view class="seg-item {{exportQuality === 'std' ? 'on' : ''}}" data-id="std" bindtap="pickQuality"><view class="seg-main">标准</view></view>
|
| 286 |
+
</view>
|
| 287 |
+
</block>
|
| 288 |
+
|
| 289 |
+
<view class="field-label">豆子样式</view>
|
| 290 |
+
<view class="seg">
|
| 291 |
+
<view wx:for="{{beadStyles}}" wx:key="id" class="seg-item {{beadStyle === item.id ? 'on' : ''}}" data-id="{{item.id}}" bindtap="pickBeadStyle">
|
| 292 |
+
<view class="seg-main">{{item.label}}</view>
|
| 293 |
+
</view>
|
| 294 |
+
</view>
|
| 295 |
+
|
| 296 |
+
<view class="row">
|
| 297 |
+
<view class="row-label">辅助网格线</view>
|
| 298 |
+
<switch checked="{{gridEnabled}}" color="#0f766e" bindchange="onGridToggle" />
|
| 299 |
+
</view>
|
| 300 |
+
<block wx:if="{{gridEnabled}}">
|
| 301 |
+
<view class="grid-opts">
|
| 302 |
+
<view class="seg small">
|
| 303 |
+
<view class="seg-item {{gridInterval === 5 ? 'on' : ''}}" data-id="5" bindtap="pickInterval"><view class="seg-main">每 5 格</view></view>
|
| 304 |
+
<view class="seg-item {{gridInterval === 10 ? 'on' : ''}}" data-id="10" bindtap="pickInterval"><view class="seg-main">每 10 格</view></view>
|
| 305 |
+
</view>
|
| 306 |
+
<view class="colors">
|
| 307 |
+
<view wx:for="{{gridColors}}" wx:key="*this" class="color-dot {{gridColor === item ? 'on' : ''}}" style="background: {{item}};" data-color="{{item}}" bindtap="pickGridColor"></view>
|
| 308 |
+
</view>
|
| 309 |
+
</view>
|
| 310 |
+
</block>
|
| 311 |
+
|
| 312 |
+
<view class="field-label">水印 <text class="field-hint">可选,防止图纸被盗用</text></view>
|
| 313 |
+
<view class="input-wrap {{watermarkStatus === 'risky' ? 'input-bad' : ''}}">
|
| 314 |
+
<input class="input" value="{{watermark}}" maxlength="20" placeholder="例如:@我的小店" placeholder-class="ph" bindinput="onWatermark" />
|
| 315 |
+
</view>
|
| 316 |
+
<view wx:if="{{watermarkStatus !== 'idle'}}" class="text-status status-{{watermarkStatus}} pad">
|
| 317 |
+
<text wx:if="{{watermarkStatus === 'checking'}}">内容安全检测中…</text>
|
| 318 |
+
<text wx:elif="{{watermarkStatus === 'pass'}}">✓ 已通过内容安全检测</text>
|
| 319 |
+
<text wx:elif="{{watermarkStatus === 'risky'}}">水印内容可能不合规,请修改</text>
|
| 320 |
+
<text wx:else>检测暂不可用,请稍后重试</text>
|
| 321 |
+
</view>
|
| 322 |
+
|
| 323 |
+
<view wx:if="{{exportResult}}" class="result">
|
| 324 |
+
<view class="result-head">
|
| 325 |
+
<view class="result-icon {{exportResult.kind === 'png' && !exportResult.saved ? 'warn' : ''}}">
|
| 326 |
+
<view class="result-tick"></view>
|
| 327 |
+
</view>
|
| 328 |
+
<view class="result-text">
|
| 329 |
+
<view class="result-title">{{exportResult.kind === 'png' && !exportResult.saved ? '已生成,但未存入相册' : '导出完成'}}</view>
|
| 330 |
+
<view class="result-sub" wx:if="{{exportResult.kind === 'pdf'}}">共 {{exportResult.files.length}} 份 PDF,可直接打印或发给好友</view>
|
| 331 |
+
<view class="result-sub" wx:elif="{{exportResult.saved}}">{{exportResult.files.length}} 张图片已保存到手机相册</view>
|
| 332 |
+
<view class="result-sub" wx:else>点「查看」后长按图片即可保存</view>
|
| 333 |
+
</view>
|
| 334 |
+
</view>
|
| 335 |
+
<view class="result-list">
|
| 336 |
+
<view wx:for="{{exportResult.files}}" wx:key="path" class="result-file">
|
| 337 |
+
<view class="file-badge {{exportResult.kind}}">{{exportResult.kind === 'pdf' ? 'PDF' : 'PNG'}}</view>
|
| 338 |
+
<view class="file-info">
|
| 339 |
+
<view class="file-name">{{item.name}}</view>
|
| 340 |
+
<view class="file-desc">{{exportResult.kind === 'pdf' ? 'A4 · 可打印' : '高清图片'}}</view>
|
| 341 |
+
</view>
|
| 342 |
+
<view class="file-actions">
|
| 343 |
+
<view class="file-btn" data-index="{{index}}" bindtap="openResultFile">{{exportResult.kind === 'png' ? '查看' : '打开'}}</view>
|
| 344 |
+
<view wx:if="{{exportResult.kind === 'pdf'}}" class="file-btn primary" data-index="{{index}}" bindtap="shareResultFile">发送</view>
|
| 345 |
+
</view>
|
| 346 |
+
</view>
|
| 347 |
+
</view>
|
| 348 |
+
</view>
|
| 349 |
+
</scroll-view>
|
| 350 |
+
|
| 351 |
+
<view class="primary-btn block {{exporting ? 'disabled' : ''}}" bindtap="startExport">
|
| 352 |
+
{{exporting ? (exportStep || '正在导出…') : (exportResult ? '重新导出' : '开始导出')}}
|
| 353 |
+
</view>
|
| 354 |
+
</view>
|
| 355 |
+
</view>
|
| 356 |
+
|
| 357 |
+
<!-- 照片裁切 -->
|
| 358 |
+
<view wx:if="{{cropVisible}}" class="mask crop-mask" catchtouchmove="noop">
|
| 359 |
+
<view class="crop-panel">
|
| 360 |
+
<view class="crop-head">
|
| 361 |
+
<text class="crop-cancel" bindtap="cancelCrop">取消</text>
|
| 362 |
+
<text class="crop-title">调整照片</text>
|
| 363 |
+
<text class="crop-ok {{cropCheck === 'pass' ? '' : 'wait'}}" bindtap="applyCrop">完成</text>
|
| 364 |
+
</view>
|
| 365 |
+
<view class="crop-box">
|
| 366 |
+
<canvas id="crop" type="2d" class="crop-canvas" bindtouchstart="onCropStart" bindtouchmove="onCropMove" bindtouchend="onCropEnd" bindtouchcancel="onCropEnd"></canvas>
|
| 367 |
+
</view>
|
| 368 |
+
<view class="crop-check check-{{cropCheck}}">
|
| 369 |
+
<text wx:if="{{cropCheck === 'checking'}}">正在进行内容安全检测,你可以先调整构图…</text>
|
| 370 |
+
<text wx:elif="{{cropCheck === 'pass'}}">✓ 照片已通过内容安全检测</text>
|
| 371 |
+
<block wx:elif="{{cropCheck === 'risky'}}"><text>这张照片未通过内容安全检测,请换一张</text></block>
|
| 372 |
+
<block wx:elif="{{cropCheck === 'error'}}"><text>检测暂时不可用 </text><text class="link-text" bindtap="retryImageCheck">重试</text></block>
|
| 373 |
+
</view>
|
| 374 |
+
<view class="crop-hint">单指拖动调整位置,双指缩放</view>
|
| 375 |
+
<view class="field-label light">缩放 <text class="field-val">{{draftZoom}}%</text></view>
|
| 376 |
+
<slider min="100" max="400" value="{{draftZoom}}" activeColor="#14b8a6" backgroundColor="rgba(255,255,255,0.2)" block-size="20" bindchanging="onDraftZoom" bindchange="onDraftZoom" />
|
| 377 |
+
<view class="crop-tools">
|
| 378 |
+
<view class="crop-tool" bindtap="flipH">左右翻转</view>
|
| 379 |
+
<view class="crop-tool" bindtap="flipV">上下翻转</view>
|
| 380 |
+
<view class="crop-tool" bindtap="resetCrop">复位</view>
|
| 381 |
+
</view>
|
| 382 |
+
<view class="row light">
|
| 383 |
+
<view>
|
| 384 |
+
<view class="row-label">去掉纯色背景</view>
|
| 385 |
+
<view class="row-hint">适合白底、纯色底的照片</view>
|
| 386 |
+
</view>
|
| 387 |
+
<switch checked="{{draftRemoveBg}}" color="#14b8a6" bindchange="onDraftRemoveBg" />
|
| 388 |
+
</view>
|
| 389 |
+
<block wx:if="{{draftRemoveBg}}">
|
| 390 |
+
<view class="mini-label light">识别范围 {{draftBgTolerance}}</view>
|
| 391 |
+
<slider min="6" max="60" value="{{draftBgTolerance}}" activeColor="#14b8a6" backgroundColor="rgba(255,255,255,0.2)" block-size="18" bindchange="onDraftBgTolerance" />
|
| 392 |
+
</block>
|
| 393 |
+
<view wx:if="{{cropCheck === 'risky'}}" class="crop-replace"><view class="primary-btn block" bindtap="chooseImage">换一张照片</view></view>
|
| 394 |
+
</view>
|
| 395 |
+
</view>
|
| 396 |
+
|
| 397 |
+
<!-- 离屏工作画布 -->
|
| 398 |
+
<view class="offscreen">
|
| 399 |
+
<canvas id="work" type="2d" class="off-canvas"></canvas>
|
| 400 |
+
<canvas id="security" type="2d" class="off-canvas"></canvas>
|
| 401 |
+
</view>
|
| 402 |
+
|
| 403 |
+
<privacy-popup />
|
BeadQR/miniprogram/pages/index/index.wxss
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
page {
|
| 2 |
+
background: var(--bg);
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
/* 导航 */
|
| 6 |
+
.nav {
|
| 7 |
+
position: fixed;
|
| 8 |
+
top: 0;
|
| 9 |
+
left: 0;
|
| 10 |
+
right: 0;
|
| 11 |
+
z-index: 50;
|
| 12 |
+
display: flex;
|
| 13 |
+
align-items: center;
|
| 14 |
+
padding-left: 32rpx;
|
| 15 |
+
box-sizing: content-box;
|
| 16 |
+
background: transparent;
|
| 17 |
+
transition: background 0.2s;
|
| 18 |
+
}
|
| 19 |
+
.nav-solid {
|
| 20 |
+
background: var(--brand-deep);
|
| 21 |
+
box-shadow: 0 4rpx 20rpx rgba(15, 61, 58, 0.25);
|
| 22 |
+
}
|
| 23 |
+
.nav-title {
|
| 24 |
+
display: flex;
|
| 25 |
+
align-items: center;
|
| 26 |
+
gap: 14rpx;
|
| 27 |
+
color: #fff;
|
| 28 |
+
font-size: 34rpx;
|
| 29 |
+
font-weight: 700;
|
| 30 |
+
letter-spacing: 2rpx;
|
| 31 |
+
}
|
| 32 |
+
.nav-logo {
|
| 33 |
+
width: 48rpx;
|
| 34 |
+
height: 48rpx;
|
| 35 |
+
border-radius: 12rpx;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.page {
|
| 39 |
+
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
| 40 |
+
background: linear-gradient(180deg, var(--brand-deep) 0, var(--brand-deep) 560rpx, var(--bg) 560rpx);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* 首屏 */
|
| 44 |
+
.hero {
|
| 45 |
+
padding: 24rpx 40rpx 150rpx;
|
| 46 |
+
color: #fff;
|
| 47 |
+
}
|
| 48 |
+
.hero-badge {
|
| 49 |
+
display: inline-block;
|
| 50 |
+
padding: 6rpx 18rpx;
|
| 51 |
+
border-radius: 999rpx;
|
| 52 |
+
background: rgba(255, 255, 255, 0.12);
|
| 53 |
+
color: #99f6e4;
|
| 54 |
+
font-size: 22rpx;
|
| 55 |
+
letter-spacing: 1rpx;
|
| 56 |
+
}
|
| 57 |
+
.hero-title {
|
| 58 |
+
margin-top: 20rpx;
|
| 59 |
+
font-size: 56rpx;
|
| 60 |
+
font-weight: 800;
|
| 61 |
+
line-height: 1.25;
|
| 62 |
+
}
|
| 63 |
+
.hero-sub {
|
| 64 |
+
margin-top: 12rpx;
|
| 65 |
+
font-size: 27rpx;
|
| 66 |
+
line-height: 1.6;
|
| 67 |
+
color: rgba(255, 255, 255, 0.78);
|
| 68 |
+
}
|
| 69 |
+
.hero-steps {
|
| 70 |
+
margin-top: 28rpx;
|
| 71 |
+
display: flex;
|
| 72 |
+
align-items: center;
|
| 73 |
+
gap: 10rpx;
|
| 74 |
+
}
|
| 75 |
+
.hero-step {
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
gap: 8rpx;
|
| 79 |
+
padding: 10rpx 16rpx 10rpx 10rpx;
|
| 80 |
+
border-radius: 999rpx;
|
| 81 |
+
background: rgba(255, 255, 255, 0.1);
|
| 82 |
+
font-size: 24rpx;
|
| 83 |
+
white-space: nowrap;
|
| 84 |
+
}
|
| 85 |
+
.hero-step .num {
|
| 86 |
+
width: 34rpx;
|
| 87 |
+
height: 34rpx;
|
| 88 |
+
line-height: 34rpx;
|
| 89 |
+
text-align: center;
|
| 90 |
+
border-radius: 50%;
|
| 91 |
+
background: var(--brand-2);
|
| 92 |
+
color: var(--brand-deep);
|
| 93 |
+
font-size: 20rpx;
|
| 94 |
+
font-weight: 800;
|
| 95 |
+
}
|
| 96 |
+
.hero-arrow {
|
| 97 |
+
color: rgba(255, 255, 255, 0.4);
|
| 98 |
+
font-size: 30rpx;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* 卡片 */
|
| 102 |
+
.card {
|
| 103 |
+
margin: 0 24rpx 24rpx;
|
| 104 |
+
padding: 32rpx;
|
| 105 |
+
background: var(--card);
|
| 106 |
+
border-radius: var(--r-l);
|
| 107 |
+
box-shadow: var(--shadow);
|
| 108 |
+
}
|
| 109 |
+
.card-title {
|
| 110 |
+
font-size: 30rpx;
|
| 111 |
+
font-weight: 700;
|
| 112 |
+
color: var(--ink);
|
| 113 |
+
margin-bottom: 20rpx;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* 预览 */
|
| 117 |
+
.preview-card {
|
| 118 |
+
margin-top: -120rpx;
|
| 119 |
+
padding: 24rpx;
|
| 120 |
+
}
|
| 121 |
+
.preview-box {
|
| 122 |
+
position: relative;
|
| 123 |
+
width: 100%;
|
| 124 |
+
height: 654rpx;
|
| 125 |
+
border-radius: 24rpx;
|
| 126 |
+
overflow: hidden;
|
| 127 |
+
background: #f1eee7;
|
| 128 |
+
}
|
| 129 |
+
.preview-canvas {
|
| 130 |
+
width: 654rpx;
|
| 131 |
+
height: 654rpx;
|
| 132 |
+
display: block;
|
| 133 |
+
margin: 0 auto;
|
| 134 |
+
}
|
| 135 |
+
.sample-tag {
|
| 136 |
+
position: absolute;
|
| 137 |
+
left: 20rpx;
|
| 138 |
+
top: 20rpx;
|
| 139 |
+
padding: 8rpx 18rpx;
|
| 140 |
+
border-radius: 999rpx;
|
| 141 |
+
background: rgba(22, 33, 31, 0.72);
|
| 142 |
+
color: #fff;
|
| 143 |
+
font-size: 22rpx;
|
| 144 |
+
}
|
| 145 |
+
.generating {
|
| 146 |
+
position: absolute;
|
| 147 |
+
right: 20rpx;
|
| 148 |
+
top: 20rpx;
|
| 149 |
+
width: 52rpx;
|
| 150 |
+
height: 52rpx;
|
| 151 |
+
border-radius: 50%;
|
| 152 |
+
background: rgba(255, 255, 255, 0.9);
|
| 153 |
+
display: flex;
|
| 154 |
+
align-items: center;
|
| 155 |
+
justify-content: center;
|
| 156 |
+
}
|
| 157 |
+
.spinner {
|
| 158 |
+
width: 28rpx;
|
| 159 |
+
height: 28rpx;
|
| 160 |
+
border: 4rpx solid var(--brand-soft);
|
| 161 |
+
border-top-color: var(--brand);
|
| 162 |
+
border-radius: 50%;
|
| 163 |
+
animation: spin 0.7s linear infinite;
|
| 164 |
+
}
|
| 165 |
+
@keyframes spin {
|
| 166 |
+
to { transform: rotate(360deg); }
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.scan-row {
|
| 170 |
+
margin-top: 20rpx;
|
| 171 |
+
display: flex;
|
| 172 |
+
align-items: center;
|
| 173 |
+
justify-content: space-between;
|
| 174 |
+
gap: 16rpx;
|
| 175 |
+
}
|
| 176 |
+
.scan-badge {
|
| 177 |
+
flex: 1;
|
| 178 |
+
padding: 14rpx 20rpx;
|
| 179 |
+
border-radius: 16rpx;
|
| 180 |
+
font-size: 25rpx;
|
| 181 |
+
background: #f1f1ee;
|
| 182 |
+
color: var(--muted);
|
| 183 |
+
}
|
| 184 |
+
.scan-ok { background: var(--ok-soft); color: var(--ok); font-weight: 600; }
|
| 185 |
+
.scan-fail { background: var(--warn-soft); color: var(--warn); font-weight: 600; }
|
| 186 |
+
.scan-checking { background: var(--brand-soft); color: var(--brand); }
|
| 187 |
+
|
| 188 |
+
.stats {
|
| 189 |
+
margin-top: 20rpx;
|
| 190 |
+
display: flex;
|
| 191 |
+
background: #faf9f6;
|
| 192 |
+
border-radius: 20rpx;
|
| 193 |
+
padding: 20rpx 0;
|
| 194 |
+
}
|
| 195 |
+
.stat {
|
| 196 |
+
flex: 1;
|
| 197 |
+
text-align: center;
|
| 198 |
+
}
|
| 199 |
+
.stat + .stat { border-left: 1rpx solid var(--line); }
|
| 200 |
+
.stat-v {
|
| 201 |
+
font-size: 36rpx;
|
| 202 |
+
font-weight: 800;
|
| 203 |
+
color: var(--ink);
|
| 204 |
+
}
|
| 205 |
+
.stat-k {
|
| 206 |
+
margin-top: 4rpx;
|
| 207 |
+
font-size: 22rpx;
|
| 208 |
+
color: var(--muted);
|
| 209 |
+
}
|
| 210 |
+
.preview-actions {
|
| 211 |
+
margin-top: 20rpx;
|
| 212 |
+
}
|
| 213 |
+
.ghost-btn {
|
| 214 |
+
text-align: center;
|
| 215 |
+
width: 100%;
|
| 216 |
+
height: 84rpx;
|
| 217 |
+
line-height: 84rpx;
|
| 218 |
+
border-radius: 20rpx;
|
| 219 |
+
background: var(--brand-soft);
|
| 220 |
+
color: var(--brand);
|
| 221 |
+
font-size: 28rpx;
|
| 222 |
+
font-weight: 600;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
/* 动态演示状态 */
|
| 226 |
+
.intro-tip {
|
| 227 |
+
margin-top: 20rpx;
|
| 228 |
+
display: flex;
|
| 229 |
+
align-items: flex-start;
|
| 230 |
+
gap: 14rpx;
|
| 231 |
+
padding: 16rpx 20rpx;
|
| 232 |
+
border-radius: 16rpx;
|
| 233 |
+
background: var(--brand-soft);
|
| 234 |
+
color: var(--brand-deep);
|
| 235 |
+
font-size: 25rpx;
|
| 236 |
+
line-height: 1.5;
|
| 237 |
+
}
|
| 238 |
+
.intro-dot {
|
| 239 |
+
flex-shrink: 0;
|
| 240 |
+
width: 14rpx;
|
| 241 |
+
height: 14rpx;
|
| 242 |
+
margin-top: 13rpx;
|
| 243 |
+
border-radius: 50%;
|
| 244 |
+
background: var(--brand-2);
|
| 245 |
+
animation: pulse 1.6s ease-in-out infinite;
|
| 246 |
+
}
|
| 247 |
+
@keyframes pulse {
|
| 248 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 249 |
+
50% { opacity: 0.35; transform: scale(0.7); }
|
| 250 |
+
}
|
| 251 |
+
.features .stat-v { font-size: 32rpx; color: var(--brand); }
|
| 252 |
+
.cta-btn {
|
| 253 |
+
width: 100%;
|
| 254 |
+
height: 88rpx;
|
| 255 |
+
line-height: 88rpx;
|
| 256 |
+
border-radius: 20rpx;
|
| 257 |
+
text-align: center;
|
| 258 |
+
background: linear-gradient(135deg, var(--brand), var(--brand-2));
|
| 259 |
+
color: #fff;
|
| 260 |
+
font-size: 29rpx;
|
| 261 |
+
font-weight: 700;
|
| 262 |
+
box-shadow: 0 10rpx 24rpx rgba(15, 118, 110, 0.25);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
/* 步骤卡 */
|
| 266 |
+
.step-head {
|
| 267 |
+
display: flex;
|
| 268 |
+
align-items: flex-start;
|
| 269 |
+
gap: 20rpx;
|
| 270 |
+
margin-bottom: 24rpx;
|
| 271 |
+
}
|
| 272 |
+
.step-num {
|
| 273 |
+
flex-shrink: 0;
|
| 274 |
+
width: 52rpx;
|
| 275 |
+
height: 52rpx;
|
| 276 |
+
line-height: 52rpx;
|
| 277 |
+
text-align: center;
|
| 278 |
+
border-radius: 16rpx;
|
| 279 |
+
background: var(--brand);
|
| 280 |
+
color: #fff;
|
| 281 |
+
font-weight: 800;
|
| 282 |
+
font-size: 28rpx;
|
| 283 |
+
}
|
| 284 |
+
.step-title {
|
| 285 |
+
font-size: 32rpx;
|
| 286 |
+
font-weight: 700;
|
| 287 |
+
color: var(--ink);
|
| 288 |
+
}
|
| 289 |
+
.optional {
|
| 290 |
+
margin-left: 8rpx;
|
| 291 |
+
padding: 2rpx 12rpx;
|
| 292 |
+
border-radius: 8rpx;
|
| 293 |
+
background: #f1f1ee;
|
| 294 |
+
color: var(--muted);
|
| 295 |
+
font-size: 20rpx;
|
| 296 |
+
font-weight: 500;
|
| 297 |
+
vertical-align: middle;
|
| 298 |
+
}
|
| 299 |
+
.step-desc {
|
| 300 |
+
margin-top: 6rpx;
|
| 301 |
+
font-size: 24rpx;
|
| 302 |
+
color: var(--muted);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.input-wrap {
|
| 306 |
+
border: 2rpx solid var(--line);
|
| 307 |
+
border-radius: 20rpx;
|
| 308 |
+
background: #fbfaf8;
|
| 309 |
+
padding: 20rpx 24rpx;
|
| 310 |
+
}
|
| 311 |
+
.input-bad { border-color: var(--danger); background: var(--danger-soft); }
|
| 312 |
+
.textarea {
|
| 313 |
+
width: 100%;
|
| 314 |
+
min-height: 120rpx;
|
| 315 |
+
font-size: 30rpx;
|
| 316 |
+
line-height: 1.5;
|
| 317 |
+
color: var(--ink);
|
| 318 |
+
}
|
| 319 |
+
.input {
|
| 320 |
+
height: 48rpx;
|
| 321 |
+
font-size: 28rpx;
|
| 322 |
+
color: var(--ink);
|
| 323 |
+
}
|
| 324 |
+
.ph { color: #b5b8b4; }
|
| 325 |
+
.input-foot {
|
| 326 |
+
margin-top: 12rpx;
|
| 327 |
+
display: flex;
|
| 328 |
+
justify-content: space-between;
|
| 329 |
+
align-items: center;
|
| 330 |
+
}
|
| 331 |
+
.text-status { font-size: 23rpx; color: var(--muted); }
|
| 332 |
+
.text-status.pad { margin-top: 10rpx; }
|
| 333 |
+
.status-checking { color: var(--brand); }
|
| 334 |
+
.status-pass { color: var(--ok); }
|
| 335 |
+
.status-risky { color: var(--danger); font-weight: 600; }
|
| 336 |
+
.status-error { color: var(--warn); }
|
| 337 |
+
.input-tools { display: flex; gap: 24rpx; }
|
| 338 |
+
.tool { font-size: 25rpx; color: var(--muted); }
|
| 339 |
+
.tool-brand { color: var(--brand); font-weight: 600; }
|
| 340 |
+
.tip {
|
| 341 |
+
margin-top: 16rpx;
|
| 342 |
+
font-size: 23rpx;
|
| 343 |
+
color: var(--muted);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.upload {
|
| 347 |
+
border: 2rpx dashed #c9d6d3;
|
| 348 |
+
border-radius: 24rpx;
|
| 349 |
+
padding: 44rpx 24rpx;
|
| 350 |
+
text-align: center;
|
| 351 |
+
background: #f8fbfa;
|
| 352 |
+
}
|
| 353 |
+
.upload-icon {
|
| 354 |
+
margin: 0 auto;
|
| 355 |
+
width: 80rpx;
|
| 356 |
+
height: 80rpx;
|
| 357 |
+
line-height: 76rpx;
|
| 358 |
+
border-radius: 50%;
|
| 359 |
+
background: var(--brand-soft);
|
| 360 |
+
color: var(--brand);
|
| 361 |
+
font-size: 48rpx;
|
| 362 |
+
}
|
| 363 |
+
.upload-text {
|
| 364 |
+
margin-top: 16rpx;
|
| 365 |
+
font-size: 29rpx;
|
| 366 |
+
font-weight: 600;
|
| 367 |
+
color: var(--ink);
|
| 368 |
+
}
|
| 369 |
+
.upload-hint {
|
| 370 |
+
margin-top: 8rpx;
|
| 371 |
+
font-size: 22rpx;
|
| 372 |
+
color: var(--muted);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.photo {
|
| 376 |
+
display: flex;
|
| 377 |
+
gap: 24rpx;
|
| 378 |
+
align-items: center;
|
| 379 |
+
}
|
| 380 |
+
.photo-thumb {
|
| 381 |
+
width: 150rpx;
|
| 382 |
+
height: 150rpx;
|
| 383 |
+
border-radius: 20rpx;
|
| 384 |
+
background: #eee;
|
| 385 |
+
}
|
| 386 |
+
.photo-info { flex: 1; }
|
| 387 |
+
.photo-meta { font-size: 23rpx; color: var(--ok); }
|
| 388 |
+
.photo-actions {
|
| 389 |
+
margin-top: 16rpx;
|
| 390 |
+
display: flex;
|
| 391 |
+
gap: 12rpx;
|
| 392 |
+
}
|
| 393 |
+
.photo-actions .mini-btn {
|
| 394 |
+
flex: 1;
|
| 395 |
+
padding: 0;
|
| 396 |
+
text-align: center;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.mini-btn {
|
| 400 |
+
display: inline-block;
|
| 401 |
+
text-align: center;
|
| 402 |
+
white-space: nowrap;
|
| 403 |
+
padding: 0 22rpx;
|
| 404 |
+
height: 56rpx;
|
| 405 |
+
line-height: 56rpx;
|
| 406 |
+
border-radius: 14rpx;
|
| 407 |
+
background: #f1f1ee;
|
| 408 |
+
color: var(--ink);
|
| 409 |
+
font-size: 24rpx;
|
| 410 |
+
font-weight: 600;
|
| 411 |
+
}
|
| 412 |
+
.mini-btn.accent { background: var(--accent); color: #fff; }
|
| 413 |
+
.mini-btn.danger { color: var(--danger); background: var(--danger-soft); }
|
| 414 |
+
|
| 415 |
+
.row {
|
| 416 |
+
margin-top: 24rpx;
|
| 417 |
+
display: flex;
|
| 418 |
+
align-items: center;
|
| 419 |
+
justify-content: space-between;
|
| 420 |
+
gap: 20rpx;
|
| 421 |
+
}
|
| 422 |
+
.row-label { font-size: 28rpx; color: var(--ink); font-weight: 600; }
|
| 423 |
+
.row-hint { margin-top: 4rpx; font-size: 22rpx; color: var(--muted); }
|
| 424 |
+
|
| 425 |
+
.field-label {
|
| 426 |
+
margin: 28rpx 0 14rpx;
|
| 427 |
+
font-size: 26rpx;
|
| 428 |
+
font-weight: 600;
|
| 429 |
+
color: var(--ink);
|
| 430 |
+
display: flex;
|
| 431 |
+
align-items: center;
|
| 432 |
+
gap: 12rpx;
|
| 433 |
+
}
|
| 434 |
+
.field-label:first-child { margin-top: 0; }
|
| 435 |
+
.field-val { color: var(--brand); font-weight: 700; }
|
| 436 |
+
.field-hint { font-size: 22rpx; color: var(--muted); font-weight: 400; }
|
| 437 |
+
.mini-label { font-size: 23rpx; color: var(--muted); margin-top: 12rpx; }
|
| 438 |
+
.link-text { color: var(--brand); font-size: 24rpx; font-weight: 600; }
|
| 439 |
+
.link-text.center { display: block; text-align: center; margin-top: 20rpx; }
|
| 440 |
+
|
| 441 |
+
/* 分段选择 */
|
| 442 |
+
.seg {
|
| 443 |
+
display: flex;
|
| 444 |
+
gap: 12rpx;
|
| 445 |
+
}
|
| 446 |
+
.seg-item {
|
| 447 |
+
flex: 1;
|
| 448 |
+
padding: 18rpx 8rpx;
|
| 449 |
+
border-radius: 18rpx;
|
| 450 |
+
border: 2rpx solid var(--line);
|
| 451 |
+
background: #fbfaf8;
|
| 452 |
+
text-align: center;
|
| 453 |
+
transition: all 0.15s;
|
| 454 |
+
}
|
| 455 |
+
.seg-item.on {
|
| 456 |
+
border-color: var(--brand);
|
| 457 |
+
background: var(--brand-soft);
|
| 458 |
+
}
|
| 459 |
+
.seg-main { font-size: 27rpx; font-weight: 700; color: var(--ink); }
|
| 460 |
+
.seg-item.on .seg-main { color: var(--brand); }
|
| 461 |
+
.seg-sub { margin-top: 4rpx; font-size: 21rpx; color: var(--muted); }
|
| 462 |
+
.seg.small .seg-item { padding: 12rpx 8rpx; }
|
| 463 |
+
|
| 464 |
+
slider { margin: 0 12rpx; }
|
| 465 |
+
|
| 466 |
+
.pos-grid {
|
| 467 |
+
width: 216rpx;
|
| 468 |
+
display: flex;
|
| 469 |
+
flex-wrap: wrap;
|
| 470 |
+
gap: 12rpx;
|
| 471 |
+
padding: 12rpx;
|
| 472 |
+
border-radius: 20rpx;
|
| 473 |
+
background: #f4f3ef;
|
| 474 |
+
}
|
| 475 |
+
.pos-cell {
|
| 476 |
+
width: 56rpx;
|
| 477 |
+
height: 56rpx;
|
| 478 |
+
border-radius: 12rpx;
|
| 479 |
+
background: #fff;
|
| 480 |
+
border: 2rpx solid var(--line);
|
| 481 |
+
}
|
| 482 |
+
.pos-cell.on { background: var(--brand); border-color: var(--brand); }
|
| 483 |
+
|
| 484 |
+
.advanced-toggle {
|
| 485 |
+
margin-top: 32rpx;
|
| 486 |
+
padding-top: 24rpx;
|
| 487 |
+
border-top: 1rpx solid var(--line);
|
| 488 |
+
display: flex;
|
| 489 |
+
justify-content: space-between;
|
| 490 |
+
align-items: center;
|
| 491 |
+
font-size: 26rpx;
|
| 492 |
+
color: var(--muted);
|
| 493 |
+
}
|
| 494 |
+
.chev { font-size: 36rpx; transform: rotate(90deg); transition: transform 0.2s; }
|
| 495 |
+
.chev.open { transform: rotate(-90deg); }
|
| 496 |
+
.advanced { margin-top: 8rpx; }
|
| 497 |
+
|
| 498 |
+
.chips-scroll { width: 100%; white-space: nowrap; }
|
| 499 |
+
.chips { display: inline-flex; gap: 12rpx; padding-bottom: 4rpx; }
|
| 500 |
+
.chip {
|
| 501 |
+
display: inline-block;
|
| 502 |
+
padding: 16rpx 22rpx;
|
| 503 |
+
border-radius: 18rpx;
|
| 504 |
+
border: 2rpx solid var(--line);
|
| 505 |
+
background: #fbfaf8;
|
| 506 |
+
}
|
| 507 |
+
.chip.on { border-color: var(--brand); background: var(--brand-soft); }
|
| 508 |
+
.chip-main { font-size: 27rpx; font-weight: 700; color: var(--ink); }
|
| 509 |
+
.chip.on .chip-main { color: var(--brand); }
|
| 510 |
+
.chip-sub { margin-top: 4rpx; font-size: 20rpx; color: var(--muted); }
|
| 511 |
+
|
| 512 |
+
/* 用豆清单 */
|
| 513 |
+
.usage {
|
| 514 |
+
display: grid;
|
| 515 |
+
grid-template-columns: repeat(4, 1fr);
|
| 516 |
+
gap: 12rpx;
|
| 517 |
+
}
|
| 518 |
+
.usage-item {
|
| 519 |
+
display: flex;
|
| 520 |
+
align-items: center;
|
| 521 |
+
gap: 10rpx;
|
| 522 |
+
padding: 12rpx;
|
| 523 |
+
border-radius: 14rpx;
|
| 524 |
+
background: #faf9f6;
|
| 525 |
+
}
|
| 526 |
+
.swatch {
|
| 527 |
+
width: 32rpx;
|
| 528 |
+
height: 32rpx;
|
| 529 |
+
border-radius: 50%;
|
| 530 |
+
box-shadow: inset 0 0 0 1rpx rgba(0, 0, 0, 0.12);
|
| 531 |
+
flex-shrink: 0;
|
| 532 |
+
}
|
| 533 |
+
.usage-id { font-size: 23rpx; font-weight: 700; color: var(--ink); }
|
| 534 |
+
.usage-count { margin-left: auto; font-size: 21rpx; color: var(--muted); }
|
| 535 |
+
|
| 536 |
+
/* 页脚 */
|
| 537 |
+
.footer {
|
| 538 |
+
padding: 16rpx 48rpx 24rpx;
|
| 539 |
+
text-align: center;
|
| 540 |
+
}
|
| 541 |
+
.footer-safe {
|
| 542 |
+
display: flex;
|
| 543 |
+
align-items: flex-start;
|
| 544 |
+
justify-content: center;
|
| 545 |
+
gap: 10rpx;
|
| 546 |
+
text-align: left;
|
| 547 |
+
font-size: 22rpx;
|
| 548 |
+
line-height: 1.6;
|
| 549 |
+
color: var(--muted);
|
| 550 |
+
}
|
| 551 |
+
.footer-links {
|
| 552 |
+
margin-top: 16rpx;
|
| 553 |
+
display: flex;
|
| 554 |
+
justify-content: center;
|
| 555 |
+
align-items: center;
|
| 556 |
+
gap: 12rpx;
|
| 557 |
+
}
|
| 558 |
+
.footer-link {
|
| 559 |
+
font-size: 23rpx;
|
| 560 |
+
line-height: 1.6;
|
| 561 |
+
color: var(--brand);
|
| 562 |
+
}
|
| 563 |
+
.footer-shield {
|
| 564 |
+
flex-shrink: 0;
|
| 565 |
+
width: 30rpx;
|
| 566 |
+
height: 30rpx;
|
| 567 |
+
margin-top: 3rpx;
|
| 568 |
+
}
|
| 569 |
+
.footer-links .footer-btn {
|
| 570 |
+
display: inline-block;
|
| 571 |
+
width: auto !important;
|
| 572 |
+
min-height: 0;
|
| 573 |
+
padding: 0 !important;
|
| 574 |
+
margin: 0 !important;
|
| 575 |
+
font-size: 23rpx;
|
| 576 |
+
font-weight: 400;
|
| 577 |
+
line-height: 1.6;
|
| 578 |
+
color: var(--brand);
|
| 579 |
+
background: none;
|
| 580 |
+
}
|
| 581 |
+
.dot { color: #c4c7c3; font-size: 22rpx; }
|
| 582 |
+
.footer-copy { margin-top: 12rpx; font-size: 20rpx; color: #b5b8b4; }
|
| 583 |
+
|
| 584 |
+
/* 底部操作栏 */
|
| 585 |
+
.bottom-bar {
|
| 586 |
+
position: fixed;
|
| 587 |
+
left: 0;
|
| 588 |
+
right: 0;
|
| 589 |
+
bottom: 0;
|
| 590 |
+
z-index: 40;
|
| 591 |
+
display: flex;
|
| 592 |
+
align-items: center;
|
| 593 |
+
gap: 24rpx;
|
| 594 |
+
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
| 595 |
+
background: rgba(255, 255, 255, 0.96);
|
| 596 |
+
box-shadow: 0 -6rpx 30rpx rgba(22, 33, 31, 0.08);
|
| 597 |
+
}
|
| 598 |
+
.bar-info { flex: 1; min-width: 0; }
|
| 599 |
+
.bar-main { font-size: 30rpx; font-weight: 800; color: var(--ink); }
|
| 600 |
+
.bar-sub { margin-top: 2rpx; font-size: 22rpx; color: var(--muted); }
|
| 601 |
+
.primary-btn {
|
| 602 |
+
text-align: center;
|
| 603 |
+
padding: 0 56rpx;
|
| 604 |
+
height: 92rpx;
|
| 605 |
+
line-height: 92rpx;
|
| 606 |
+
border-radius: 999rpx;
|
| 607 |
+
background: linear-gradient(135deg, var(--brand), var(--brand-2));
|
| 608 |
+
color: #fff;
|
| 609 |
+
font-size: 30rpx;
|
| 610 |
+
font-weight: 700;
|
| 611 |
+
box-shadow: 0 10rpx 24rpx rgba(15, 118, 110, 0.3);
|
| 612 |
+
}
|
| 613 |
+
.primary-btn.disabled { opacity: 0.5; box-shadow: none; }
|
| 614 |
+
.primary-btn.block { display: block; width: 100%; padding: 0; }
|
| 615 |
+
|
| 616 |
+
/* 弹层 */
|
| 617 |
+
.mask {
|
| 618 |
+
position: fixed;
|
| 619 |
+
top: 0;
|
| 620 |
+
left: 0;
|
| 621 |
+
right: 0;
|
| 622 |
+
bottom: 0;
|
| 623 |
+
z-index: 100;
|
| 624 |
+
background: rgba(10, 20, 18, 0.5);
|
| 625 |
+
display: flex;
|
| 626 |
+
align-items: flex-end;
|
| 627 |
+
}
|
| 628 |
+
.sheet {
|
| 629 |
+
width: 100%;
|
| 630 |
+
max-height: 88vh;
|
| 631 |
+
display: flex;
|
| 632 |
+
flex-direction: column;
|
| 633 |
+
background: var(--card);
|
| 634 |
+
border-radius: 36rpx 36rpx 0 0;
|
| 635 |
+
padding: 16rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
| 636 |
+
box-sizing: border-box;
|
| 637 |
+
}
|
| 638 |
+
.sheet-handle {
|
| 639 |
+
margin: 0 auto 16rpx;
|
| 640 |
+
width: 72rpx;
|
| 641 |
+
height: 8rpx;
|
| 642 |
+
border-radius: 8rpx;
|
| 643 |
+
background: #e2e2dd;
|
| 644 |
+
}
|
| 645 |
+
.sheet-title {
|
| 646 |
+
font-size: 34rpx;
|
| 647 |
+
font-weight: 800;
|
| 648 |
+
color: var(--ink);
|
| 649 |
+
margin-bottom: 20rpx;
|
| 650 |
+
}
|
| 651 |
+
.sheet-body {
|
| 652 |
+
flex: 1;
|
| 653 |
+
max-height: 62vh;
|
| 654 |
+
margin-bottom: 24rpx;
|
| 655 |
+
}
|
| 656 |
+
.grid-opts {
|
| 657 |
+
margin-top: 16rpx;
|
| 658 |
+
display: flex;
|
| 659 |
+
align-items: center;
|
| 660 |
+
gap: 24rpx;
|
| 661 |
+
}
|
| 662 |
+
.grid-opts .seg { flex: 1; }
|
| 663 |
+
.colors { display: flex; gap: 14rpx; }
|
| 664 |
+
.color-dot {
|
| 665 |
+
width: 44rpx;
|
| 666 |
+
height: 44rpx;
|
| 667 |
+
border-radius: 50%;
|
| 668 |
+
border: 4rpx solid #fff;
|
| 669 |
+
box-shadow: 0 0 0 2rpx var(--line);
|
| 670 |
+
}
|
| 671 |
+
.color-dot.on { box-shadow: 0 0 0 4rpx var(--brand); }
|
| 672 |
+
|
| 673 |
+
.result {
|
| 674 |
+
margin-top: 32rpx;
|
| 675 |
+
border-radius: 24rpx;
|
| 676 |
+
background: #fff;
|
| 677 |
+
border: 2rpx solid var(--line);
|
| 678 |
+
overflow: hidden;
|
| 679 |
+
}
|
| 680 |
+
.result-head {
|
| 681 |
+
display: flex;
|
| 682 |
+
align-items: center;
|
| 683 |
+
gap: 20rpx;
|
| 684 |
+
padding: 28rpx;
|
| 685 |
+
background: linear-gradient(135deg, var(--ok-soft), #ffffff);
|
| 686 |
+
}
|
| 687 |
+
.result-icon {
|
| 688 |
+
flex-shrink: 0;
|
| 689 |
+
width: 64rpx;
|
| 690 |
+
height: 64rpx;
|
| 691 |
+
border-radius: 50%;
|
| 692 |
+
background: var(--ok);
|
| 693 |
+
display: flex;
|
| 694 |
+
align-items: center;
|
| 695 |
+
justify-content: center;
|
| 696 |
+
box-shadow: 0 8rpx 18rpx rgba(21, 128, 61, 0.25);
|
| 697 |
+
}
|
| 698 |
+
.result-icon.warn { background: var(--warn); box-shadow: none; }
|
| 699 |
+
.result-tick {
|
| 700 |
+
width: 16rpx;
|
| 701 |
+
height: 28rpx;
|
| 702 |
+
margin-top: -6rpx;
|
| 703 |
+
border-right: 5rpx solid #fff;
|
| 704 |
+
border-bottom: 5rpx solid #fff;
|
| 705 |
+
transform: rotate(45deg);
|
| 706 |
+
}
|
| 707 |
+
.result-text { flex: 1; min-width: 0; }
|
| 708 |
+
.result-title { font-size: 30rpx; font-weight: 800; color: var(--ink); }
|
| 709 |
+
.result-sub { margin-top: 6rpx; font-size: 23rpx; color: var(--muted); line-height: 1.5; }
|
| 710 |
+
.result-list { padding: 8rpx 28rpx; }
|
| 711 |
+
.result-file {
|
| 712 |
+
display: flex;
|
| 713 |
+
align-items: center;
|
| 714 |
+
gap: 20rpx;
|
| 715 |
+
padding: 20rpx 0;
|
| 716 |
+
}
|
| 717 |
+
.result-file + .result-file { border-top: 1rpx solid var(--line); }
|
| 718 |
+
.file-badge {
|
| 719 |
+
flex-shrink: 0;
|
| 720 |
+
width: 72rpx;
|
| 721 |
+
height: 84rpx;
|
| 722 |
+
line-height: 84rpx;
|
| 723 |
+
border-radius: 12rpx;
|
| 724 |
+
text-align: center;
|
| 725 |
+
font-size: 20rpx;
|
| 726 |
+
font-weight: 800;
|
| 727 |
+
letter-spacing: 1rpx;
|
| 728 |
+
color: #fff;
|
| 729 |
+
background: #e5484d;
|
| 730 |
+
}
|
| 731 |
+
.file-badge.png { background: #3b82f6; }
|
| 732 |
+
.file-info { flex: 1; min-width: 0; }
|
| 733 |
+
.file-name {
|
| 734 |
+
font-size: 27rpx;
|
| 735 |
+
font-weight: 700;
|
| 736 |
+
color: var(--ink);
|
| 737 |
+
overflow: hidden;
|
| 738 |
+
white-space: nowrap;
|
| 739 |
+
text-overflow: ellipsis;
|
| 740 |
+
}
|
| 741 |
+
.file-desc { margin-top: 4rpx; font-size: 22rpx; color: var(--muted); }
|
| 742 |
+
.file-actions { flex-shrink: 0; display: flex; gap: 12rpx; }
|
| 743 |
+
.file-btn {
|
| 744 |
+
min-width: 100rpx;
|
| 745 |
+
height: 60rpx;
|
| 746 |
+
line-height: 60rpx;
|
| 747 |
+
padding: 0 20rpx;
|
| 748 |
+
border-radius: 999rpx;
|
| 749 |
+
text-align: center;
|
| 750 |
+
font-size: 24rpx;
|
| 751 |
+
font-weight: 700;
|
| 752 |
+
color: var(--brand);
|
| 753 |
+
background: var(--brand-soft);
|
| 754 |
+
}
|
| 755 |
+
.file-btn.primary { color: #fff; background: var(--brand); }
|
| 756 |
+
|
| 757 |
+
/* 裁切 */
|
| 758 |
+
.crop-mask { align-items: stretch; background: #0b1413; }
|
| 759 |
+
.crop-panel {
|
| 760 |
+
width: 100%;
|
| 761 |
+
padding: calc(env(safe-area-inset-top) + 100rpx) 40rpx 40rpx;
|
| 762 |
+
box-sizing: border-box;
|
| 763 |
+
overflow-y: auto;
|
| 764 |
+
color: #fff;
|
| 765 |
+
}
|
| 766 |
+
.crop-head {
|
| 767 |
+
display: flex;
|
| 768 |
+
justify-content: space-between;
|
| 769 |
+
align-items: center;
|
| 770 |
+
margin-bottom: 28rpx;
|
| 771 |
+
}
|
| 772 |
+
.crop-title { font-size: 32rpx; font-weight: 700; }
|
| 773 |
+
.crop-cancel { font-size: 28rpx; color: rgba(255, 255, 255, 0.7); }
|
| 774 |
+
.crop-ok {
|
| 775 |
+
padding: 10rpx 28rpx;
|
| 776 |
+
border-radius: 999rpx;
|
| 777 |
+
background: var(--brand-2);
|
| 778 |
+
color: var(--brand-deep);
|
| 779 |
+
font-size: 28rpx;
|
| 780 |
+
font-weight: 700;
|
| 781 |
+
}
|
| 782 |
+
.crop-ok.wait { opacity: 0.55; }
|
| 783 |
+
.crop-box {
|
| 784 |
+
width: 670rpx;
|
| 785 |
+
height: 670rpx;
|
| 786 |
+
margin: 0 auto;
|
| 787 |
+
border-radius: 20rpx;
|
| 788 |
+
overflow: hidden;
|
| 789 |
+
background: #1a2523;
|
| 790 |
+
}
|
| 791 |
+
.crop-canvas { width: 670rpx; height: 670rpx; }
|
| 792 |
+
.crop-check {
|
| 793 |
+
margin-top: 20rpx;
|
| 794 |
+
padding: 14rpx 20rpx;
|
| 795 |
+
border-radius: 14rpx;
|
| 796 |
+
font-size: 24rpx;
|
| 797 |
+
background: rgba(255, 255, 255, 0.08);
|
| 798 |
+
color: rgba(255, 255, 255, 0.8);
|
| 799 |
+
}
|
| 800 |
+
.check-pass { color: #6ee7b7; }
|
| 801 |
+
.check-risky { background: rgba(229, 72, 77, 0.2); color: #fca5a5; }
|
| 802 |
+
.check-error { color: #fcd34d; }
|
| 803 |
+
.crop-hint { margin-top: 12rpx; font-size: 22rpx; color: rgba(255, 255, 255, 0.5); text-align: center; }
|
| 804 |
+
.field-label.light, .row.light .row-label { color: #fff; }
|
| 805 |
+
.row.light .row-hint, .mini-label.light { color: rgba(255, 255, 255, 0.55); }
|
| 806 |
+
.crop-tools { margin-top: 24rpx; display: flex; gap: 16rpx; }
|
| 807 |
+
.crop-tool {
|
| 808 |
+
text-align: center;
|
| 809 |
+
flex: 1;
|
| 810 |
+
height: 72rpx;
|
| 811 |
+
line-height: 72rpx;
|
| 812 |
+
border-radius: 16rpx;
|
| 813 |
+
background: rgba(255, 255, 255, 0.1);
|
| 814 |
+
color: #fff;
|
| 815 |
+
font-size: 25rpx;
|
| 816 |
+
}
|
| 817 |
+
.crop-replace { margin-top: 32rpx; }
|
| 818 |
+
|
| 819 |
+
/* 离屏画布:不能 display:none,否则取不到节点 */
|
| 820 |
+
.offscreen {
|
| 821 |
+
position: fixed;
|
| 822 |
+
left: -9999px;
|
| 823 |
+
top: 0;
|
| 824 |
+
width: 10px;
|
| 825 |
+
height: 10px;
|
| 826 |
+
overflow: hidden;
|
| 827 |
+
}
|
| 828 |
+
.off-canvas { width: 10px; height: 10px; }
|
BeadQR/miniprogram/sitemap.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
| 3 |
+
"rules": [{
|
| 4 |
+
"action": "allow",
|
| 5 |
+
"page": "*"
|
| 6 |
+
}]
|
| 7 |
+
}
|
BeadQR/miniprogram/utils/color.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 颜色基础:HEX/RGB 转换、OKLab 感知色彩空间、亮度
|
| 2 |
+
// OKLab 比 RGB 欧氏距离更接近人眼感受,配色更自然;距离统一放大 100 倍便于调参
|
| 3 |
+
|
| 4 |
+
function hexToRgb(hex) {
|
| 5 |
+
const value = String(hex || '').replace('#', '')
|
| 6 |
+
const full = value.length === 3 ? value.split('').map((c) => c + c).join('') : value
|
| 7 |
+
const num = parseInt(full, 16) || 0
|
| 8 |
+
return [(num >> 16) & 255, (num >> 8) & 255, num & 255]
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
function rgbToHex(r, g, b) {
|
| 12 |
+
const h = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')
|
| 13 |
+
return `#${h(r)}${h(g)}${h(b)}`
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// sRGB 伽马解码查表,避免每次调用 Math.pow
|
| 17 |
+
const SRGB_TO_LINEAR = new Float32Array(256)
|
| 18 |
+
for (let i = 0; i < 256; i += 1) {
|
| 19 |
+
const c = i / 255
|
| 20 |
+
SRGB_TO_LINEAR[i] = c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// 写入 out[offset..offset+2](L, a, b,均 ×100)
|
| 24 |
+
function rgbToOklabInto(r, g, b, out, offset) {
|
| 25 |
+
const lr = SRGB_TO_LINEAR[r]
|
| 26 |
+
const lg = SRGB_TO_LINEAR[g]
|
| 27 |
+
const lb = SRGB_TO_LINEAR[b]
|
| 28 |
+
const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb)
|
| 29 |
+
const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb)
|
| 30 |
+
const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb)
|
| 31 |
+
out[offset] = (0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s) * 100
|
| 32 |
+
out[offset + 1] = (1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s) * 100
|
| 33 |
+
out[offset + 2] = (0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s) * 100
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// 感知亮度(与旧版一致的 Rec.601 加权,0~1),用于二维码深/浅模块约束
|
| 37 |
+
function luma(r, g, b) {
|
| 38 |
+
return (r * 0.299 + g * 0.587 + b * 0.114) / 255
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function lumaHex(hex) {
|
| 42 |
+
const [r, g, b] = hexToRgb(hex)
|
| 43 |
+
return luma(r, g, b)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
module.exports = { hexToRgb, rgbToHex, rgbToOklabInto, luma, lumaHex }
|
BeadQR/miniprogram/utils/engine.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 配色引擎:把底图 + 二维码合成为 MARD 色号网格
|
| 2 |
+
// 性能要点:
|
| 3 |
+
// 1. 5bit 量化桶 → 候选 Top-K 查找表(懒加载),每格只精算约 10 个候选,而非整套 291 色
|
| 4 |
+
// 2. OKLab 感知距离 + 色间距离矩阵,全部 Typed Array,无对象分配
|
| 5 |
+
// 3. “颜色连贯度”参考已选中的左/上邻居色号,减少零碎杂色,拼起来更省事
|
| 6 |
+
// 4. 定位/时序/对齐等功能区使用更严格的深浅阈值,显著提升扫码成功率
|
| 7 |
+
const { rgbToOklabInto } = require('./color')
|
| 8 |
+
const { MATCH_MODES } = require('./palette')
|
| 9 |
+
const { PAPER } = require('./imaging')
|
| 10 |
+
|
| 11 |
+
const TOP_K = 10
|
| 12 |
+
const C_ANY = 0
|
| 13 |
+
const C_DARK = 1
|
| 14 |
+
const C_LIGHT = 2
|
| 15 |
+
const C_FDARK = 3
|
| 16 |
+
const C_FLIGHT = 4
|
| 17 |
+
const C_QUIET = 5
|
| 18 |
+
const C_EMPTY = 6
|
| 19 |
+
|
| 20 |
+
function subsetFor(pal, mode, kind) {
|
| 21 |
+
const key = `${mode.id}:${kind}`
|
| 22 |
+
if (pal.subsets[key]) return pal.subsets[key]
|
| 23 |
+
const { n, lum } = pal
|
| 24 |
+
let test
|
| 25 |
+
if (kind === C_DARK) test = (i) => lum[i] <= mode.darkMax
|
| 26 |
+
else if (kind === C_LIGHT) test = (i) => lum[i] >= mode.lightMin
|
| 27 |
+
else if (kind === C_FDARK) test = (i) => lum[i] <= Math.min(mode.darkMax, 0.3)
|
| 28 |
+
else if (kind === C_FLIGHT) test = (i) => lum[i] >= Math.max(mode.lightMin, 0.82)
|
| 29 |
+
else test = () => true
|
| 30 |
+
let list = []
|
| 31 |
+
for (let i = 0; i < n; i += 1) if (test(i)) list.push(i)
|
| 32 |
+
if (!list.length) {
|
| 33 |
+
// 色库太小时退化为最深/最浅的 3 个颜色
|
| 34 |
+
const order = Array.from({ length: n }, (_, i) => i).sort((a, b) => lum[a] - lum[b])
|
| 35 |
+
list = kind === C_DARK || kind === C_FDARK ? order.slice(0, 3) : order.slice(-3)
|
| 36 |
+
}
|
| 37 |
+
const member = new Uint8Array(n)
|
| 38 |
+
list.forEach((i) => { member[i] = 1 })
|
| 39 |
+
const subset = { key, list: Int16Array.from(list), member, lut: new Map() }
|
| 40 |
+
pal.subsets[key] = subset
|
| 41 |
+
return subset
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const tmpLab = new Float32Array(3)
|
| 45 |
+
|
| 46 |
+
function topKForBucket(pal, subset, bucket) {
|
| 47 |
+
let hit = subset.lut.get(bucket)
|
| 48 |
+
if (hit) return hit
|
| 49 |
+
const r = ((bucket >> 10) & 31) * 8 + 4
|
| 50 |
+
const g = ((bucket >> 5) & 31) * 8 + 4
|
| 51 |
+
const b = (bucket & 31) * 8 + 4
|
| 52 |
+
rgbToOklabInto(r, g, b, tmpLab, 0)
|
| 53 |
+
const { lab } = pal
|
| 54 |
+
const list = subset.list
|
| 55 |
+
const k = Math.min(TOP_K, list.length)
|
| 56 |
+
const bestIdx = new Int16Array(k).fill(-1)
|
| 57 |
+
const bestDist = new Float32Array(k).fill(Infinity)
|
| 58 |
+
for (let t = 0; t < list.length; t += 1) {
|
| 59 |
+
const i = list[t]
|
| 60 |
+
const dl = lab[i * 3] - tmpLab[0]
|
| 61 |
+
const da = lab[i * 3 + 1] - tmpLab[1]
|
| 62 |
+
const db = lab[i * 3 + 2] - tmpLab[2]
|
| 63 |
+
const d = dl * dl + da * da + db * db
|
| 64 |
+
if (d >= bestDist[k - 1]) continue
|
| 65 |
+
let p = k - 1
|
| 66 |
+
while (p > 0 && bestDist[p - 1] > d) { bestDist[p] = bestDist[p - 1]; bestIdx[p] = bestIdx[p - 1]; p -= 1 }
|
| 67 |
+
bestDist[p] = d
|
| 68 |
+
bestIdx[p] = i
|
| 69 |
+
}
|
| 70 |
+
hit = bestIdx
|
| 71 |
+
subset.lut.set(bucket, hit)
|
| 72 |
+
return hit
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/**
|
| 76 |
+
* @param base { size, rgb: Uint8Array, mask?: Uint8Array } 或 null(无底图)
|
| 77 |
+
* @param qr makeQr() 结果
|
| 78 |
+
* @param placement createPlacement() 结果
|
| 79 |
+
* @param pal preparePalette() 结果
|
| 80 |
+
* @param options { modeId, coherence(0-100), cleanup(bool) }
|
| 81 |
+
*/
|
| 82 |
+
function computeArtwork(base, qr, placement, pal, options = {}) {
|
| 83 |
+
const mode = MATCH_MODES.find((m) => m.id === options.modeId) || MATCH_MODES[1]
|
| 84 |
+
const size = placement.size
|
| 85 |
+
const total = size * size
|
| 86 |
+
const lab = new Float32Array(total * 3)
|
| 87 |
+
const bucket = new Int32Array(total)
|
| 88 |
+
const kind = new Uint8Array(total)
|
| 89 |
+
const { left, top, quiet } = placement
|
| 90 |
+
|
| 91 |
+
for (let y = 0; y < size; y += 1) {
|
| 92 |
+
for (let x = 0; x < size; x += 1) {
|
| 93 |
+
const i = y * size + x
|
| 94 |
+
const qx = x - left
|
| 95 |
+
const qy = y - top
|
| 96 |
+
const inQr = qx >= 0 && qy >= 0 && qx < qr.size && qy < qr.size
|
| 97 |
+
const inQuiet = !inQr && x >= left - quiet && x < left + qr.size + quiet && y >= top - quiet && y < top + qr.size + quiet
|
| 98 |
+
let r; let g; let b
|
| 99 |
+
if (inQr) {
|
| 100 |
+
const q = qy * qr.size + qx
|
| 101 |
+
const isDark = qr.dark[q]
|
| 102 |
+
const isFunc = qr.func[q]
|
| 103 |
+
kind[i] = isFunc ? (isDark ? C_FDARK : C_FLIGHT) : (isDark ? C_DARK : C_LIGHT)
|
| 104 |
+
if (base && !(base.mask && base.mask[i])) {
|
| 105 |
+
r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
|
| 106 |
+
} else if (base) {
|
| 107 |
+
r = PAPER[0]; g = PAPER[1]; b = PAPER[2]
|
| 108 |
+
} else {
|
| 109 |
+
r = g = b = isDark ? 0 : 255
|
| 110 |
+
}
|
| 111 |
+
} else if (inQuiet || !base) {
|
| 112 |
+
kind[i] = C_QUIET
|
| 113 |
+
continue
|
| 114 |
+
} else if (base.mask && base.mask[i]) {
|
| 115 |
+
kind[i] = C_EMPTY
|
| 116 |
+
continue
|
| 117 |
+
} else {
|
| 118 |
+
kind[i] = C_ANY
|
| 119 |
+
r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
|
| 120 |
+
}
|
| 121 |
+
rgbToOklabInto(r, g, b, lab, i * 3)
|
| 122 |
+
bucket[i] = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
const n = pal.n
|
| 127 |
+
const plab = pal.lab
|
| 128 |
+
const pair = pal.pair
|
| 129 |
+
const weight = (Math.max(0, Math.min(100, options.coherence == null ? 40 : options.coherence)) / 100) * 0.6
|
| 130 |
+
const subsets = [C_ANY, C_DARK, C_LIGHT, C_FDARK, C_FLIGHT].map((k) => subsetFor(pal, mode, k))
|
| 131 |
+
const cells = new Int16Array(total).fill(-1)
|
| 132 |
+
|
| 133 |
+
const distTo = (c, i) => {
|
| 134 |
+
const dl = plab[c * 3] - lab[i * 3]
|
| 135 |
+
const da = plab[c * 3 + 1] - lab[i * 3 + 1]
|
| 136 |
+
const db = plab[c * 3 + 2] - lab[i * 3 + 2]
|
| 137 |
+
return Math.sqrt(dl * dl + da * da + db * db)
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
for (let y = 0; y < size; y += 1) {
|
| 141 |
+
for (let x = 0; x < size; x += 1) {
|
| 142 |
+
const i = y * size + x
|
| 143 |
+
const k = kind[i]
|
| 144 |
+
if (k >= C_QUIET) continue
|
| 145 |
+
const subset = subsets[k]
|
| 146 |
+
const cands = topKForBucket(pal, subset, bucket[i])
|
| 147 |
+
const nl = x > 0 ? cells[i - 1] : -1
|
| 148 |
+
const nu = y > 0 ? cells[i - size] : -1
|
| 149 |
+
const nbCount = (nl >= 0 ? 1 : 0) + (nu >= 0 ? 1 : 0)
|
| 150 |
+
let best = -1
|
| 151 |
+
let bestScore = Infinity
|
| 152 |
+
const consider = (c) => {
|
| 153 |
+
let score = distTo(c, i)
|
| 154 |
+
if (weight > 0 && nbCount) {
|
| 155 |
+
let s = 0
|
| 156 |
+
if (nl >= 0) s += pair[c * n + nl]
|
| 157 |
+
if (nu >= 0) s += pair[c * n + nu]
|
| 158 |
+
score += weight * (s / nbCount)
|
| 159 |
+
}
|
| 160 |
+
if (score < bestScore) { bestScore = score; best = c }
|
| 161 |
+
}
|
| 162 |
+
for (let t = 0; t < cands.length; t += 1) if (cands[t] >= 0) consider(cands[t])
|
| 163 |
+
if (weight > 0) {
|
| 164 |
+
if (nl >= 0 && subset.member[nl]) consider(nl)
|
| 165 |
+
if (nu >= 0 && nu !== nl && subset.member[nu]) consider(nu)
|
| 166 |
+
}
|
| 167 |
+
cells[i] = best
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// 合并零散色:只用了 1~2 颗的色号并入最接近的常用色,减少需要购买的色号
|
| 172 |
+
const counts = new Int32Array(n)
|
| 173 |
+
for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
|
| 174 |
+
if (options.cleanup !== false) {
|
| 175 |
+
const kept = []
|
| 176 |
+
for (let c = 0; c < n; c += 1) if (counts[c] >= 3) kept.push(c)
|
| 177 |
+
if (kept.length) {
|
| 178 |
+
for (let i = 0; i < total; i += 1) {
|
| 179 |
+
const c = cells[i]
|
| 180 |
+
if (c < 0 || counts[c] >= 3) continue
|
| 181 |
+
const member = subsets[kind[i]].member
|
| 182 |
+
let best = -1
|
| 183 |
+
let bestD = Infinity
|
| 184 |
+
for (let t = 0; t < kept.length; t += 1) {
|
| 185 |
+
const kc = kept[t]
|
| 186 |
+
if (!member[kc]) continue
|
| 187 |
+
const d = distTo(kc, i)
|
| 188 |
+
if (d < bestD) { bestD = d; best = kc }
|
| 189 |
+
}
|
| 190 |
+
// 与原色差别过大时保留原色,避免破坏画面
|
| 191 |
+
if (best >= 0 && bestD <= distTo(c, i) + 12) cells[i] = best
|
| 192 |
+
}
|
| 193 |
+
counts.fill(0)
|
| 194 |
+
for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// 二维码留白区:使用画面中最常用的浅色;没有则用色库里最浅的颜色
|
| 199 |
+
let quietColor = -1
|
| 200 |
+
let quietCount = -1
|
| 201 |
+
for (let c = 0; c < n; c += 1) {
|
| 202 |
+
if (pal.lum[c] >= Math.max(0.8, mode.lightMin) && counts[c] > quietCount) { quietColor = c; quietCount = counts[c] }
|
| 203 |
+
}
|
| 204 |
+
if (quietCount <= 0) {
|
| 205 |
+
quietColor = 0
|
| 206 |
+
for (let c = 1; c < n; c += 1) if (pal.lum[c] > pal.lum[quietColor]) quietColor = c
|
| 207 |
+
}
|
| 208 |
+
for (let i = 0; i < total; i += 1) {
|
| 209 |
+
if (kind[i] === C_QUIET) { cells[i] = quietColor; counts[quietColor] += 1 }
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
const usage = []
|
| 213 |
+
let beads = 0
|
| 214 |
+
for (let c = 0; c < n; c += 1) {
|
| 215 |
+
if (counts[c] > 0) {
|
| 216 |
+
usage.push({ index: c, id: pal.ids[c], hex: pal.hexes[c], count: counts[c] })
|
| 217 |
+
beads += counts[c]
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
usage.sort((a, b) => b.count - a.count)
|
| 221 |
+
|
| 222 |
+
return { size, cells, usage, beads, pal, qr, placement }
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
module.exports = { computeArtwork }
|
BeadQR/miniprogram/utils/imaging.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 图像取样:超采样 + 区域平均,得到干净的格子颜色(旧版一次性缩放会产生噪点和锯齿色)
|
| 2 |
+
// 所有结果都用 Typed Array 表示,不再生成 HEX 字符串再反解析
|
| 3 |
+
|
| 4 |
+
const PAPER = [247, 241, 230]
|
| 5 |
+
|
| 6 |
+
function getImageDimensions(image) {
|
| 7 |
+
const width = Number(image.width) || Number(image.naturalWidth) || Number(image._w)
|
| 8 |
+
const height = Number(image.height) || Number(image.naturalHeight) || Number(image._h)
|
| 9 |
+
if (!width || !height) throw new Error('IMAGE_DIMENSIONS_UNAVAILABLE')
|
| 10 |
+
return { width, height }
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// 把图片按裁切参数画到 context 上(cover 语义,zoom≥1)
|
| 14 |
+
function drawCropped(context, image, width, height, crop) {
|
| 15 |
+
const source = getImageDimensions(image)
|
| 16 |
+
const scale = Math.max(width / source.width, height / source.height) * (crop.zoom || 1)
|
| 17 |
+
const w = source.width * scale
|
| 18 |
+
const h = source.height * scale
|
| 19 |
+
const offsetX = (width - w) * ((crop.focusX == null ? 50 : crop.focusX) / 100)
|
| 20 |
+
const offsetY = (height - h) * ((crop.focusY == null ? 50 : crop.focusY) / 100)
|
| 21 |
+
context.fillStyle = `rgb(${PAPER[0]},${PAPER[1]},${PAPER[2]})`
|
| 22 |
+
context.fillRect(0, 0, width, height)
|
| 23 |
+
context.save()
|
| 24 |
+
context.translate(crop.flipH ? width : 0, crop.flipV ? height : 0)
|
| 25 |
+
context.scale(crop.flipH ? -1 : 1, crop.flipV ? -1 : 1)
|
| 26 |
+
context.drawImage(image, offsetX, offsetY, w, h)
|
| 27 |
+
context.restore()
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// 返回 { size, rgb: Uint8Array(size*size*3) }
|
| 31 |
+
function sampleImage(canvas, image, size, crop) {
|
| 32 |
+
// 超采样到约 700px 再按块平均,等价于高质量面积缩放
|
| 33 |
+
const factor = Math.max(1, Math.min(24, Math.floor(720 / size)))
|
| 34 |
+
const big = size * factor
|
| 35 |
+
canvas.width = big
|
| 36 |
+
canvas.height = big
|
| 37 |
+
const context = canvas.getContext('2d')
|
| 38 |
+
context.imageSmoothingEnabled = true
|
| 39 |
+
try { context.imageSmoothingQuality = 'high' } catch (e) { /* 部分基础库不支持 */ }
|
| 40 |
+
drawCropped(context, image, big, big, crop || {})
|
| 41 |
+
const pixels = context.getImageData(0, 0, big, big).data
|
| 42 |
+
const rgb = new Uint8Array(size * size * 3)
|
| 43 |
+
const area = factor * factor
|
| 44 |
+
for (let gy = 0; gy < size; gy += 1) {
|
| 45 |
+
for (let gx = 0; gx < size; gx += 1) {
|
| 46 |
+
let r = 0; let g = 0; let b = 0
|
| 47 |
+
for (let sy = 0; sy < factor; sy += 1) {
|
| 48 |
+
let p = ((gy * factor + sy) * big + gx * factor) * 4
|
| 49 |
+
for (let sx = 0; sx < factor; sx += 1) {
|
| 50 |
+
r += pixels[p]; g += pixels[p + 1]; b += pixels[p + 2]
|
| 51 |
+
p += 4
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
const o = (gy * size + gx) * 3
|
| 55 |
+
rgb[o] = (r / area + 0.5) | 0
|
| 56 |
+
rgb[o + 1] = (g / area + 0.5) | 0
|
| 57 |
+
rgb[o + 2] = (b / area + 0.5) | 0
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
return { size, rgb }
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// 去除简单纯色背景:四角均色为背景色,从边缘洪泛填充相近颜色(主体内部不受影响)
|
| 64 |
+
// 返回 Uint8Array 掩码:1 = 背景(空钉位,不放豆)
|
| 65 |
+
function removeSimpleBackground(rgb, size, tolerance) {
|
| 66 |
+
const corners = [0, size - 1, (size - 1) * size, size * size - 1]
|
| 67 |
+
let br = 0; let bg = 0; let bb = 0
|
| 68 |
+
corners.forEach((i) => { br += rgb[i * 3]; bg += rgb[i * 3 + 1]; bb += rgb[i * 3 + 2] })
|
| 69 |
+
br /= 4; bg /= 4; bb /= 4
|
| 70 |
+
const threshold = (tolerance / 100) * 441.673
|
| 71 |
+
const limit = threshold * threshold
|
| 72 |
+
const mask = new Uint8Array(size * size)
|
| 73 |
+
const isBg = (i) => {
|
| 74 |
+
const dr = rgb[i * 3] - br; const dg = rgb[i * 3 + 1] - bg; const db = rgb[i * 3 + 2] - bb
|
| 75 |
+
return dr * dr + dg * dg + db * db <= limit
|
| 76 |
+
}
|
| 77 |
+
const stack = []
|
| 78 |
+
const push = (x, y) => {
|
| 79 |
+
if (x < 0 || y < 0 || x >= size || y >= size) return
|
| 80 |
+
const i = y * size + x
|
| 81 |
+
if (mask[i] || !isBg(i)) return
|
| 82 |
+
mask[i] = 1
|
| 83 |
+
stack.push(i)
|
| 84 |
+
}
|
| 85 |
+
for (let i = 0; i < size; i += 1) { push(i, 0); push(i, size - 1); push(0, i); push(size - 1, i) }
|
| 86 |
+
while (stack.length) {
|
| 87 |
+
const i = stack.pop()
|
| 88 |
+
const x = i % size
|
| 89 |
+
const y = (i - x) / size
|
| 90 |
+
push(x + 1, y); push(x - 1, y); push(x, y + 1); push(x, y - 1)
|
| 91 |
+
}
|
| 92 |
+
return mask
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
module.exports = { PAPER, getImageDimensions, drawCropped, sampleImage, removeSimpleBackground }
|
BeadQR/miniprogram/utils/intro.js
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 首屏动态演示(1:1)
|
| 2 |
+
// 纯代码绘制:不占包体积(无视频/图片资源),小程序实时播放与宣传视频渲染共用同一条时间轴。
|
| 3 |
+
// 演示用的拼豆作品由真实配色引擎实时生成,并且确实可以被微信扫出 “Happy Birthday!”。
|
| 4 |
+
const { makeQr, createPlacement } = require('./qr')
|
| 5 |
+
const { preparePalette } = require('./palette')
|
| 6 |
+
const { computeArtwork } = require('./engine')
|
| 7 |
+
|
| 8 |
+
const DEMO_TEXT = 'Happy Birthday!'
|
| 9 |
+
const DURATION = 12
|
| 10 |
+
const U = 1080 // 设计稿边长,绘制时按实际像素缩放
|
| 11 |
+
const TAU = Math.PI * 2
|
| 12 |
+
const FLIGHT = 0.85
|
| 13 |
+
// 豆子半径(相对格宽):略大于 0.5,相邻豆子轻微相接,像熨烫后的成品,扫码更稳
|
| 14 |
+
const BEAD_R = 0.52
|
| 15 |
+
|
| 16 |
+
const C = {
|
| 17 |
+
bg: '#f4efe6',
|
| 18 |
+
ink: '#16211f',
|
| 19 |
+
muted: '#6b7471',
|
| 20 |
+
brand: '#0f766e',
|
| 21 |
+
brand2: '#14b8a6',
|
| 22 |
+
brandSoft: '#dcf1ec',
|
| 23 |
+
board: '#fffdf8',
|
| 24 |
+
dark: '#22302e',
|
| 25 |
+
light: '#fbf8f1',
|
| 26 |
+
wechat: '#07c160',
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const SCENES = [
|
| 30 |
+
{ a: 0, b: 2.6, title: '写一句话', sub: '扫码后想让人看到的话' },
|
| 31 |
+
{ a: 2.6, b: 5.0, title: '自动编成二维码', sub: '每一颗拼豆,都是一个像素' },
|
| 32 |
+
{ a: 5.0, b: 7.6, title: '融进你的照片', sub: '换上照片的颜色,依然能扫' },
|
| 33 |
+
{ a: 7.6, b: 10.0, title: '微信扫一扫', sub: '拼好的作品,一扫就读出这句话' },
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
const CONFETTI = [
|
| 37 |
+
[0.17, 0.19, 0.045, '#ffd166'], [0.85, 0.2, 0.04, '#4cc9f0'], [0.13, 0.8, 0.04, '#2ec4b6'],
|
| 38 |
+
[0.86, 0.82, 0.045, '#ffd166'], [0.5, 0.11, 0.03, '#2ec4b6'], [0.91, 0.51, 0.03, '#ff9f1c'], [0.08, 0.5, 0.03, '#4cc9f0'],
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
const DECOR = [
|
| 42 |
+
[90, 150, 22, '#14b8a6'], [990, 120, 16, '#ffb703'], [60, 960, 18, '#ff7a45'], [1010, 930, 24, '#8ecae6'],
|
| 43 |
+
[170, 1040, 12, '#ffb703'], [930, 330, 12, '#ff7a45'], [120, 520, 10, '#8ecae6'], [980, 700, 14, '#14b8a6'],
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
// —— 工具 ——
|
| 47 |
+
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)
|
| 48 |
+
const lerp = (a, b, t) => a + (b - a) * t
|
| 49 |
+
const prog = (t, a, b) => clamp01((t - a) / (b - a))
|
| 50 |
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3)
|
| 51 |
+
const easeIn = (t) => t * t * t
|
| 52 |
+
const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2)
|
| 53 |
+
const easeBack = (t) => { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2) }
|
| 54 |
+
const bump = (t, a, b) => { const p = prog(t, a, b); return p > 0 && p < 1 ? Math.sin(Math.PI * p) : 0 }
|
| 55 |
+
|
| 56 |
+
function rng(seed) {
|
| 57 |
+
let a = seed >>> 0
|
| 58 |
+
return () => {
|
| 59 |
+
a = (a + 0x6D2B79F5) >>> 0
|
| 60 |
+
let t = a
|
| 61 |
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
| 62 |
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
| 63 |
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function rr(ctx, x, y, w, h, r) {
|
| 68 |
+
r = Math.max(0, Math.min(r, w / 2, h / 2))
|
| 69 |
+
ctx.moveTo(x + r, y)
|
| 70 |
+
ctx.arcTo(x + w, y, x + w, y + h, r)
|
| 71 |
+
ctx.arcTo(x + w, y + h, x, y + h, r)
|
| 72 |
+
ctx.arcTo(x, y + h, x, y, r)
|
| 73 |
+
ctx.arcTo(x, y, x + w, y, r)
|
| 74 |
+
ctx.closePath()
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// 三层叠加的柔和阴影:比 shadowBlur 便宜得多,手机上逐帧绘制也流畅
|
| 78 |
+
function softShadow(ctx, x, y, w, h, r, alpha) {
|
| 79 |
+
for (let i = 3; i >= 1; i -= 1) {
|
| 80 |
+
const g = i * 8
|
| 81 |
+
ctx.fillStyle = `rgba(52,38,18,${(alpha / (i + 1)).toFixed(3)})`
|
| 82 |
+
ctx.beginPath()
|
| 83 |
+
rr(ctx, x - g / 2, y - g / 2 + g * 0.8, w + g, h + g, r + g / 2)
|
| 84 |
+
ctx.fill()
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
function heartPath(ctx, cx, cy, s) {
|
| 89 |
+
ctx.moveTo(cx, cy + s * 0.9)
|
| 90 |
+
ctx.bezierCurveTo(cx - s * 1.3, cy + s * 0.05, cx - s * 0.95, cy - s * 1.0, cx, cy - s * 0.42)
|
| 91 |
+
ctx.bezierCurveTo(cx + s * 0.95, cy - s * 1.0, cx + s * 1.3, cy + s * 0.05, cx, cy + s * 0.9)
|
| 92 |
+
ctx.closePath()
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// 演示“照片”:暖色渐变 + 爱心 + 彩纸屑(矢量绘制,任意尺寸都清晰)
|
| 96 |
+
function drawIllustration(ctx, x, y, s) {
|
| 97 |
+
ctx.save()
|
| 98 |
+
ctx.beginPath()
|
| 99 |
+
ctx.rect(x, y, s, s)
|
| 100 |
+
ctx.clip()
|
| 101 |
+
let g = ctx.createLinearGradient(x, y, x, y + s)
|
| 102 |
+
g.addColorStop(0, '#63c7b8')
|
| 103 |
+
g.addColorStop(1, '#4aa3c9')
|
| 104 |
+
ctx.fillStyle = g
|
| 105 |
+
ctx.fillRect(x, y, s, s)
|
| 106 |
+
g = ctx.createRadialGradient(x + s * 0.5, y + s * 0.48, 0, x + s * 0.5, y + s * 0.48, s * 0.55)
|
| 107 |
+
g.addColorStop(0, 'rgba(255,255,255,0.35)')
|
| 108 |
+
g.addColorStop(1, 'rgba(255,255,255,0)')
|
| 109 |
+
ctx.fillStyle = g
|
| 110 |
+
ctx.fillRect(x, y, s, s)
|
| 111 |
+
CONFETTI.forEach(([cx, cy, r, col]) => {
|
| 112 |
+
ctx.fillStyle = col
|
| 113 |
+
ctx.beginPath()
|
| 114 |
+
ctx.arc(x + cx * s, y + cy * s, r * s, 0, TAU)
|
| 115 |
+
ctx.fill()
|
| 116 |
+
})
|
| 117 |
+
g = ctx.createLinearGradient(x, y + s * 0.2, x, y + s * 0.85)
|
| 118 |
+
g.addColorStop(0, '#ff6b88')
|
| 119 |
+
g.addColorStop(1, '#d42a55')
|
| 120 |
+
ctx.fillStyle = g
|
| 121 |
+
ctx.beginPath()
|
| 122 |
+
heartPath(ctx, x + s * 0.5, y + s * 0.55, s * 0.34)
|
| 123 |
+
ctx.fill()
|
| 124 |
+
ctx.fillStyle = 'rgba(255,255,255,0.55)'
|
| 125 |
+
ctx.save()
|
| 126 |
+
ctx.translate(x + s * 0.37, y + s * 0.42)
|
| 127 |
+
ctx.rotate(-0.6)
|
| 128 |
+
ctx.scale(1, 0.55)
|
| 129 |
+
ctx.beginPath()
|
| 130 |
+
ctx.arc(0, 0, s * 0.06, 0, TAU)
|
| 131 |
+
ctx.fill()
|
| 132 |
+
ctx.restore()
|
| 133 |
+
ctx.restore()
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// 用真实引擎生成演示作品:插画按格面积取平均色 → 配色引擎
|
| 137 |
+
function buildDemo(workCanvas, createCanvas) {
|
| 138 |
+
const qr = makeQr(DEMO_TEXT, 'H')
|
| 139 |
+
const placement = createPlacement(qr.size, { scale: 100, quiet: 1, hasImage: true })
|
| 140 |
+
const size = placement.size
|
| 141 |
+
const k = 6
|
| 142 |
+
const px = size * k
|
| 143 |
+
const canvas = createCanvas ? createCanvas(px, px) : workCanvas
|
| 144 |
+
canvas.width = px
|
| 145 |
+
canvas.height = px
|
| 146 |
+
const ctx = canvas.getContext('2d')
|
| 147 |
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
| 148 |
+
ctx.clearRect(0, 0, px, px)
|
| 149 |
+
drawIllustration(ctx, 0, 0, px)
|
| 150 |
+
const data = ctx.getImageData(0, 0, px, px).data
|
| 151 |
+
const rgb = new Uint8Array(size * size * 3)
|
| 152 |
+
for (let r = 0; r < size; r += 1) {
|
| 153 |
+
for (let c = 0; c < size; c += 1) {
|
| 154 |
+
let sr = 0; let sg = 0; let sb = 0
|
| 155 |
+
for (let y = 0; y < k; y += 1) {
|
| 156 |
+
let o = ((r * k + y) * px + c * k) * 4
|
| 157 |
+
for (let x = 0; x < k; x += 1, o += 4) { sr += data[o]; sg += data[o + 1]; sb += data[o + 2] }
|
| 158 |
+
}
|
| 159 |
+
const i = (r * size + c) * 3
|
| 160 |
+
rgb[i] = sr / (k * k); rgb[i + 1] = sg / (k * k); rgb[i + 2] = sb / (k * k)
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
const art = computeArtwork({ size, rgb }, qr, placement, preparePalette('221'), { modeId: 'balanced', coherence: 40, cleanup: true })
|
| 164 |
+
return { qr, placement, art }
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// —— 豆子批量绘制:按颜色合并路径,每帧只需十几次填充 ——
|
| 168 |
+
function addBead(groups, hex, x, y, r) {
|
| 169 |
+
let list = groups.get(hex)
|
| 170 |
+
if (!list) { list = []; groups.set(hex, list) }
|
| 171 |
+
list.push(x, y, r)
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// 小批量合并路径:一条路径里子路径太多时,部分渲染引擎反而更慢,每 12 颗提交一次最稳
|
| 175 |
+
const CHUNK = 12
|
| 176 |
+
function fillCircles(ctx, list, rs, ox, oy) {
|
| 177 |
+
let n = 0
|
| 178 |
+
ctx.beginPath()
|
| 179 |
+
for (let i = 0; i < list.length; i += 3) {
|
| 180 |
+
const r = list[i + 2] * rs
|
| 181 |
+
const x = list[i] + list[i + 2] * ox
|
| 182 |
+
const y = list[i + 1] + list[i + 2] * oy
|
| 183 |
+
ctx.moveTo(x + r, y)
|
| 184 |
+
ctx.arc(x, y, r, 0, TAU)
|
| 185 |
+
n += 1
|
| 186 |
+
if (n === CHUNK) { ctx.fill(); ctx.beginPath(); n = 0 }
|
| 187 |
+
}
|
| 188 |
+
if (n) ctx.fill()
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function flushBeads(ctx, groups) {
|
| 192 |
+
const all = []
|
| 193 |
+
groups.forEach((list, hex) => {
|
| 194 |
+
ctx.fillStyle = hex
|
| 195 |
+
fillCircles(ctx, list, 1, 0, 0)
|
| 196 |
+
all.push(list)
|
| 197 |
+
})
|
| 198 |
+
// 中孔:拼豆是一颗颗小管子
|
| 199 |
+
ctx.fillStyle = 'rgba(70,48,24,0.17)'
|
| 200 |
+
all.forEach((list) => fillCircles(ctx, list, 0.3, 0, 0))
|
| 201 |
+
// 高光
|
| 202 |
+
ctx.fillStyle = 'rgba(255,255,255,0.4)'
|
| 203 |
+
all.forEach((list) => fillCircles(ctx, list, 0.2, -0.42, -0.42))
|
| 204 |
+
groups.clear()
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/**
|
| 208 |
+
* @param opts { workCanvas?, createCanvas?(w,h), font?, loop? }
|
| 209 |
+
* @returns { duration, text, artwork, draw(ctx, t, pixelWidth) }
|
| 210 |
+
*/
|
| 211 |
+
function createIntro(opts = {}) {
|
| 212 |
+
const font = opts.font || 'sans-serif'
|
| 213 |
+
const loop = opts.loop !== false
|
| 214 |
+
const F = (weight, px) => `${weight} ${Math.round(px)}px ${font}`
|
| 215 |
+
const { qr, placement, art } = buildDemo(opts.workCanvas, opts.createCanvas)
|
| 216 |
+
const size = art.size
|
| 217 |
+
const R = rng(20260929)
|
| 218 |
+
const cells = []
|
| 219 |
+
for (let r = 0; r < size; r += 1) {
|
| 220 |
+
for (let c = 0; c < size; c += 1) {
|
| 221 |
+
const qc = c - placement.left
|
| 222 |
+
const qrow = r - placement.top
|
| 223 |
+
const dark = qc >= 0 && qrow >= 0 && qc < qr.size && qrow < qr.size && qr.dark[qrow * qr.size + qc] === 1
|
| 224 |
+
const idx = art.cells[r * size + c]
|
| 225 |
+
cells.push({
|
| 226 |
+
c,
|
| 227 |
+
r,
|
| 228 |
+
qrHex: dark ? C.dark : C.light,
|
| 229 |
+
finalHex: idx >= 0 ? art.pal.hexes[idx] : C.board,
|
| 230 |
+
k: Math.max(0, Math.min(DEMO_TEXT.length - 1, Math.floor((c / size) * DEMO_TEXT.length + (R() - 0.5) * 2.5))),
|
| 231 |
+
jx: R(),
|
| 232 |
+
jy: R() - 0.5,
|
| 233 |
+
bend: R() - 0.5,
|
| 234 |
+
t0: 2.72 + (c / size) * 0.55 + R() * 0.55,
|
| 235 |
+
tw: 5.95 + ((c + r) / (2 * (size - 1))) * 0.95 + R() * 0.08,
|
| 236 |
+
})
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
const finders = [[0, 0], [qr.size - 7, 0], [0, qr.size - 7]].map(([x, y]) => [placement.left + x, placement.top + y])
|
| 240 |
+
const topColors = art.usage.slice(0, 8).map((u) => u.hex)
|
| 241 |
+
const burst = Array.from({ length: 26 }, () => ({
|
| 242 |
+
a: -Math.PI / 2 + (R() - 0.5) * 2.2,
|
| 243 |
+
v: 520 + R() * 520,
|
| 244 |
+
r: 9 + R() * 8,
|
| 245 |
+
hex: topColors[Math.floor(R() * topColors.length)] || C.brand2,
|
| 246 |
+
}))
|
| 247 |
+
const groups = new Map()
|
| 248 |
+
const pegDots = []
|
| 249 |
+
for (let y = 20; y < U; y += 40) for (let x = 20; x < U; x += 40) pegDots.push(x, y, 2.4)
|
| 250 |
+
let layout = null // 文本排版缓存(首帧测量)
|
| 251 |
+
|
| 252 |
+
const textLayout = (ctx) => {
|
| 253 |
+
if (layout) return layout
|
| 254 |
+
ctx.font = F('bold', 64)
|
| 255 |
+
const x0 = 170
|
| 256 |
+
const charX = []
|
| 257 |
+
for (let i = 0; i <= DEMO_TEXT.length; i += 1) charX.push(x0 + ctx.measureText(DEMO_TEXT.slice(0, i)).width)
|
| 258 |
+
layout = { charX, y: 616 }
|
| 259 |
+
return layout
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
const boardAt = (t) => {
|
| 263 |
+
let cx = 540; let cy = 590; let B = 640
|
| 264 |
+
const a = easeInOut(prog(t, 7.6, 8.2))
|
| 265 |
+
cx = lerp(cx, 356, a); B = lerp(B, 560, a)
|
| 266 |
+
const b = easeInOut(prog(t, 10.05, 10.75))
|
| 267 |
+
cx = lerp(cx, 540, b); cy = lerp(cy, 410, b); B = lerp(B, 450, b)
|
| 268 |
+
return { cx, cy, B }
|
| 269 |
+
}
|
| 270 |
+
const gridOf = (L) => {
|
| 271 |
+
const pad = L.B * 0.05
|
| 272 |
+
const cell = (L.B - pad * 2) / size
|
| 273 |
+
return { cell, ox: L.cx - L.B / 2 + pad, oy: L.cy - L.B / 2 + pad }
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
const drawPlate = (ctx, L, alpha) => {
|
| 277 |
+
const x = L.cx - L.B / 2
|
| 278 |
+
const y = L.cy - L.B / 2
|
| 279 |
+
const r = L.B * 0.045
|
| 280 |
+
ctx.globalAlpha = alpha
|
| 281 |
+
softShadow(ctx, x, y, L.B, L.B, r, 0.16)
|
| 282 |
+
ctx.fillStyle = C.board
|
| 283 |
+
ctx.beginPath(); rr(ctx, x, y, L.B, L.B, r); ctx.fill()
|
| 284 |
+
ctx.globalAlpha = 1
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// 背景:米色钉板 + 缓慢漂浮的装饰豆
|
| 288 |
+
const drawBackground = (ctx, t) => {
|
| 289 |
+
ctx.fillStyle = C.bg
|
| 290 |
+
ctx.fillRect(0, 0, U, U)
|
| 291 |
+
ctx.fillStyle = 'rgba(60,45,25,0.065)'
|
| 292 |
+
fillCircles(ctx, pegDots, 1, 0, 0)
|
| 293 |
+
DECOR.forEach(([x, y, r, hex], i) => {
|
| 294 |
+
const dx = Math.sin(t * 0.9 + i * 1.7) * 10
|
| 295 |
+
const dy = Math.cos(t * 0.7 + i * 2.3) * 12
|
| 296 |
+
ctx.globalAlpha = 0.55
|
| 297 |
+
addBead(groups, hex, x + dx, y + dy, r)
|
| 298 |
+
})
|
| 299 |
+
flushBeads(ctx, groups)
|
| 300 |
+
ctx.globalAlpha = 1
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
const drawCaption = (ctx, t) => {
|
| 304 |
+
ctx.textAlign = 'center'
|
| 305 |
+
ctx.textBaseline = 'middle'
|
| 306 |
+
SCENES.forEach((s, i) => {
|
| 307 |
+
if (t < s.a || t > s.b) return
|
| 308 |
+
const inP = easeOut(prog(t, s.a + 0.05, s.a + 0.5))
|
| 309 |
+
const outP = easeIn(prog(t, s.b - 0.32, s.b))
|
| 310 |
+
const alpha = inP * (1 - outP)
|
| 311 |
+
if (alpha <= 0) return
|
| 312 |
+
const dy = (1 - inP) * 26 - outP * 14
|
| 313 |
+
ctx.globalAlpha = alpha
|
| 314 |
+
ctx.font = F('bold', 24)
|
| 315 |
+
const label = `第 ${i + 1} 步`
|
| 316 |
+
const w = ctx.measureText(label).width + 40
|
| 317 |
+
ctx.fillStyle = C.brandSoft
|
| 318 |
+
ctx.beginPath(); rr(ctx, 540 - w / 2, 70 + dy, w, 46, 23); ctx.fill()
|
| 319 |
+
ctx.fillStyle = C.brand
|
| 320 |
+
ctx.fillText(label, 540, 94 + dy)
|
| 321 |
+
ctx.fillStyle = C.ink
|
| 322 |
+
ctx.font = F('bold', 60)
|
| 323 |
+
ctx.fillText(s.title, 540, 170 + dy * 1.3)
|
| 324 |
+
ctx.fillStyle = C.muted
|
| 325 |
+
ctx.font = F('normal', 30)
|
| 326 |
+
ctx.fillText(s.sub, 540, 232 + dy * 1.6)
|
| 327 |
+
})
|
| 328 |
+
ctx.globalAlpha = 1
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
const drawProgress = (ctx, t) => {
|
| 332 |
+
const alpha = prog(t, 0, 0.35) * (1 - prog(t, 10.0, 10.4))
|
| 333 |
+
if (alpha <= 0) return
|
| 334 |
+
ctx.globalAlpha = alpha
|
| 335 |
+
const w = 110; const gap = 16; const x0 = 540 - (w * 4 + gap * 3) / 2
|
| 336 |
+
SCENES.forEach((s, i) => {
|
| 337 |
+
const x = x0 + i * (w + gap)
|
| 338 |
+
ctx.fillStyle = 'rgba(22,33,31,0.1)'
|
| 339 |
+
ctx.beginPath(); rr(ctx, x, 1010, w, 8, 4); ctx.fill()
|
| 340 |
+
const p = prog(t, s.a, s.b)
|
| 341 |
+
if (p > 0) {
|
| 342 |
+
ctx.fillStyle = C.brand
|
| 343 |
+
ctx.beginPath(); rr(ctx, x, 1010, Math.max(8, w * p), 8, 4); ctx.fill()
|
| 344 |
+
}
|
| 345 |
+
})
|
| 346 |
+
ctx.globalAlpha = 1
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
// 第 1 幕:输入框 + 打字
|
| 350 |
+
const drawInput = (ctx, t) => {
|
| 351 |
+
if (t > 3.35) return
|
| 352 |
+
const inP = easeBack(prog(t, 0.1, 0.65))
|
| 353 |
+
const outP = easeInOut(prog(t, 2.6, 3.15))
|
| 354 |
+
const alpha = prog(t, 0.1, 0.4)
|
| 355 |
+
if (alpha <= 0) return
|
| 356 |
+
const L = textLayout(ctx)
|
| 357 |
+
const press = 1 - 0.08 * bump(t, 2.1, 2.4)
|
| 358 |
+
const scale = lerp(0.88, 1, inP) * lerp(1, 0.95, outP)
|
| 359 |
+
ctx.save()
|
| 360 |
+
ctx.translate(540, 590)
|
| 361 |
+
ctx.scale(scale, scale)
|
| 362 |
+
ctx.translate(-540, -590)
|
| 363 |
+
ctx.globalAlpha = alpha * (1 - outP)
|
| 364 |
+
softShadow(ctx, 130, 495, 820, 190, 40, 0.14)
|
| 365 |
+
ctx.fillStyle = '#ffffff'
|
| 366 |
+
ctx.beginPath(); rr(ctx, 130, 495, 820, 190, 40); ctx.fill()
|
| 367 |
+
ctx.textAlign = 'left'
|
| 368 |
+
ctx.textBaseline = 'middle'
|
| 369 |
+
ctx.fillStyle = C.muted
|
| 370 |
+
ctx.font = F('normal', 24)
|
| 371 |
+
ctx.fillText('扫码后显示的内容', 170, 548)
|
| 372 |
+
// 发送按钮
|
| 373 |
+
ctx.save()
|
| 374 |
+
ctx.translate(880, 590)
|
| 375 |
+
ctx.scale(press, press)
|
| 376 |
+
ctx.fillStyle = C.brand
|
| 377 |
+
ctx.beginPath(); ctx.arc(0, 0, 44, 0, TAU); ctx.fill()
|
| 378 |
+
ctx.strokeStyle = '#fff'
|
| 379 |
+
ctx.lineWidth = 7
|
| 380 |
+
ctx.lineCap = 'round'
|
| 381 |
+
ctx.lineJoin = 'round'
|
| 382 |
+
ctx.beginPath(); ctx.moveTo(-14, 0); ctx.lineTo(14, 0); ctx.moveTo(2, -13); ctx.lineTo(15, 0); ctx.lineTo(2, 13); ctx.stroke()
|
| 383 |
+
ctx.restore()
|
| 384 |
+
ctx.restore()
|
| 385 |
+
// 文字逐字出现,随后逐字化作豆子
|
| 386 |
+
const count = Math.floor(prog(t, 0.55, 1.9) * DEMO_TEXT.length + 0.0001)
|
| 387 |
+
ctx.font = F('bold', 64)
|
| 388 |
+
ctx.textAlign = 'left'
|
| 389 |
+
ctx.fillStyle = C.ink
|
| 390 |
+
for (let i = 0; i < count; i += 1) {
|
| 391 |
+
const start = 2.7 + (i / DEMO_TEXT.length) * 0.55
|
| 392 |
+
const a = alpha * (1 - prog(t, start, start + 0.35))
|
| 393 |
+
if (a <= 0) continue
|
| 394 |
+
ctx.globalAlpha = a
|
| 395 |
+
ctx.fillText(DEMO_TEXT[i], L.charX[i], L.y)
|
| 396 |
+
}
|
| 397 |
+
const typing = t >= 0.55 && t <= 1.9
|
| 398 |
+
if (t < 2.55 && (typing || Math.floor(t / 0.4) % 2 === 0)) {
|
| 399 |
+
ctx.globalAlpha = alpha
|
| 400 |
+
ctx.fillStyle = C.brand2
|
| 401 |
+
ctx.fillRect(L.charX[count] + 4, L.y - 34, 5, 68)
|
| 402 |
+
}
|
| 403 |
+
ctx.globalAlpha = 1
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// 主画板上的豆子:飞入 → 黑白二维码 → 扫过换色
|
| 407 |
+
const drawMainBeads = (ctx, t, L) => {
|
| 408 |
+
const G = gridOf(L)
|
| 409 |
+
const TL = textLayout(ctx)
|
| 410 |
+
const baseR = G.cell * BEAD_R
|
| 411 |
+
for (let i = 0; i < cells.length; i += 1) {
|
| 412 |
+
const b = cells[i]
|
| 413 |
+
if (t < b.t0) continue
|
| 414 |
+
const dx = G.ox + (b.c + 0.5) * G.cell
|
| 415 |
+
const dy = G.oy + (b.r + 0.5) * G.cell
|
| 416 |
+
let x = dx; let y = dy; let s = 1
|
| 417 |
+
const f = prog(t, b.t0, b.t0 + FLIGHT)
|
| 418 |
+
if (f < 1) {
|
| 419 |
+
const sx = TL.charX[b.k] + b.jx * (TL.charX[b.k + 1] - TL.charX[b.k])
|
| 420 |
+
const sy = TL.y + b.jy * 44
|
| 421 |
+
const cx = (sx + dx) / 2 + b.bend * 220
|
| 422 |
+
const cy = Math.min(sy, dy) - 110 - Math.abs(b.bend) * 160
|
| 423 |
+
const e = easeInOut(f)
|
| 424 |
+
const u = 1 - e
|
| 425 |
+
x = u * u * sx + 2 * u * e * cx + e * e * dx
|
| 426 |
+
y = u * u * sy + 2 * u * e * cy + e * e * dy
|
| 427 |
+
s = lerp(0.55, 1, easeOut(f))
|
| 428 |
+
} else {
|
| 429 |
+
s = 1 + 0.16 * bump(t, b.t0 + FLIGHT, b.t0 + FLIGHT + 0.2)
|
| 430 |
+
}
|
| 431 |
+
s += 0.3 * bump(t, b.tw - 0.05, b.tw + 0.25)
|
| 432 |
+
addBead(groups, t >= b.tw ? b.finalHex : b.qrHex, x, y, baseR * s)
|
| 433 |
+
}
|
| 434 |
+
flushBeads(ctx, groups)
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
const drawFinalBeads = (ctx, L) => {
|
| 438 |
+
const G = gridOf(L)
|
| 439 |
+
const r = G.cell * BEAD_R
|
| 440 |
+
for (let i = 0; i < cells.length; i += 1) {
|
| 441 |
+
const b = cells[i]
|
| 442 |
+
addBead(groups, b.finalHex, G.ox + (b.c + 0.5) * G.cell, G.oy + (b.r + 0.5) * G.cell, r)
|
| 443 |
+
}
|
| 444 |
+
flushBeads(ctx, groups)
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// 二维码定位图形高亮
|
| 448 |
+
const drawFinders = (ctx, t, L) => {
|
| 449 |
+
const p = prog(t, 4.15, 4.95)
|
| 450 |
+
if (p <= 0 || p >= 1) return
|
| 451 |
+
const G = gridOf(L)
|
| 452 |
+
const alpha = Math.sin(Math.PI * p)
|
| 453 |
+
const grow = (1 - easeOut(p)) * 18
|
| 454 |
+
ctx.strokeStyle = C.brand2
|
| 455 |
+
ctx.lineWidth = 6
|
| 456 |
+
ctx.globalAlpha = alpha
|
| 457 |
+
finders.forEach(([c, r]) => {
|
| 458 |
+
const x = G.ox + c * G.cell - 6 - grow
|
| 459 |
+
const y = G.oy + r * G.cell - 6 - grow
|
| 460 |
+
const w = 7 * G.cell + 12 + grow * 2
|
| 461 |
+
ctx.beginPath(); rr(ctx, x, y, w, w, 16); ctx.stroke()
|
| 462 |
+
})
|
| 463 |
+
ctx.globalAlpha = 1
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
// 沿对角线扫过的光带
|
| 467 |
+
const drawGleam = (ctx, L, p, strength) => {
|
| 468 |
+
if (p <= 0 || p >= 1) return
|
| 469 |
+
const x = L.cx - L.B / 2
|
| 470 |
+
const y = L.cy - L.B / 2
|
| 471 |
+
const g = ctx.createLinearGradient(x, y, x + L.B, y + L.B)
|
| 472 |
+
const s0 = clamp01(p - 0.12); const s1 = clamp01(p); const s2 = clamp01(p + 0.04)
|
| 473 |
+
g.addColorStop(0, 'rgba(255,255,255,0)')
|
| 474 |
+
if (s0 > 0) g.addColorStop(s0, 'rgba(255,255,255,0)')
|
| 475 |
+
g.addColorStop(s1, `rgba(255,255,255,${strength})`)
|
| 476 |
+
if (s2 < 1) g.addColorStop(s2, 'rgba(255,255,255,0)')
|
| 477 |
+
g.addColorStop(1, 'rgba(255,255,255,0)')
|
| 478 |
+
ctx.save()
|
| 479 |
+
ctx.beginPath(); rr(ctx, x, y, L.B, L.B, L.B * 0.045); ctx.clip()
|
| 480 |
+
ctx.fillStyle = g
|
| 481 |
+
ctx.fillRect(x, y, L.B, L.B)
|
| 482 |
+
ctx.restore()
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
// 第 3 幕:照片飞入并铺到画板上
|
| 486 |
+
const drawPhoto = (ctx, t, L) => {
|
| 487 |
+
if (t < 4.95 || t > 6.65) return
|
| 488 |
+
const a = easeOut(prog(t, 5.0, 5.55))
|
| 489 |
+
const b = easeInOut(prog(t, 5.6, 6.05))
|
| 490 |
+
let x = lerp(1320, 800, a); let y = lerp(250, 330, a); let rot = lerp(0.4, -0.12, a); let s = 300
|
| 491 |
+
x = lerp(x, L.cx, b); y = lerp(y, L.cy, b); rot = lerp(rot, 0, b); s = lerp(s, L.B * 0.9, b)
|
| 492 |
+
const frame = lerp(14, 0, b)
|
| 493 |
+
const alpha = 1 - prog(t, 6.0, 6.6)
|
| 494 |
+
ctx.save()
|
| 495 |
+
ctx.globalAlpha = alpha
|
| 496 |
+
ctx.translate(x, y)
|
| 497 |
+
ctx.rotate(rot)
|
| 498 |
+
const half = s / 2 + frame
|
| 499 |
+
if (b < 1) softShadow(ctx, -half, -half, half * 2, half * 2, 18, 0.2 * (1 - b))
|
| 500 |
+
ctx.fillStyle = '#ffffff'
|
| 501 |
+
ctx.beginPath(); rr(ctx, -half, -half, half * 2, half * 2, 18); ctx.fill()
|
| 502 |
+
ctx.beginPath(); rr(ctx, -s / 2, -s / 2, s, s, 10); ctx.clip()
|
| 503 |
+
drawIllustration(ctx, -s / 2, -s / 2, s)
|
| 504 |
+
ctx.restore()
|
| 505 |
+
ctx.globalAlpha = 1
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
// 第 4 幕:手机扫码
|
| 509 |
+
const drawPhone = (ctx, t) => {
|
| 510 |
+
const enter = easeOut(prog(t, 7.75, 8.35))
|
| 511 |
+
const exit = easeIn(prog(t, 9.95, 10.4))
|
| 512 |
+
if (enter <= 0 || exit >= 1) return
|
| 513 |
+
const w = 340; const h = 660
|
| 514 |
+
const px = 800
|
| 515 |
+
const py = 590 + (1 - enter) * 860 + exit * 860
|
| 516 |
+
const rot = lerp(0.16, -0.035, enter)
|
| 517 |
+
ctx.save()
|
| 518 |
+
ctx.translate(px, py)
|
| 519 |
+
ctx.rotate(rot)
|
| 520 |
+
softShadow(ctx, -w / 2, -h / 2, w, h, 60, 0.28)
|
| 521 |
+
ctx.fillStyle = '#1b2321'
|
| 522 |
+
ctx.beginPath(); rr(ctx, -w / 2, -h / 2, w, h, 60); ctx.fill()
|
| 523 |
+
ctx.fillStyle = '#2c3633'
|
| 524 |
+
ctx.fillRect(w / 2 - 1, -h / 2 + 150, 5, 70)
|
| 525 |
+
// 屏幕
|
| 526 |
+
const sw = w - 24; const sh = h - 24
|
| 527 |
+
ctx.save()
|
| 528 |
+
ctx.beginPath(); rr(ctx, -sw / 2, -sh / 2, sw, sh, 48); ctx.clip()
|
| 529 |
+
ctx.fillStyle = '#0e1312'
|
| 530 |
+
ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
|
| 531 |
+
const jx = Math.sin(t * 3.1) * 3
|
| 532 |
+
const jy = Math.cos(t * 2.3) * 3
|
| 533 |
+
const cam = { cx: jx, cy: -30 + jy, B: 250 }
|
| 534 |
+
ctx.globalAlpha = 0.92
|
| 535 |
+
ctx.fillStyle = C.bg
|
| 536 |
+
ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
|
| 537 |
+
ctx.globalAlpha = 1
|
| 538 |
+
drawPlate(ctx, cam, 1)
|
| 539 |
+
drawFinalBeads(ctx, cam)
|
| 540 |
+
ctx.fillStyle = 'rgba(10,15,14,0.45)'
|
| 541 |
+
ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
|
| 542 |
+
// 取景框
|
| 543 |
+
const detect = prog(t, 9.0, 9.3)
|
| 544 |
+
const snap = t >= 9.0 ? 1 + 0.1 * (1 - easeOut(detect)) : 1
|
| 545 |
+
const half = 146 * snap
|
| 546 |
+
ctx.strokeStyle = t >= 9.0 ? C.wechat : '#ffffff'
|
| 547 |
+
ctx.lineWidth = 6
|
| 548 |
+
ctx.lineCap = 'round'
|
| 549 |
+
const arm = 34
|
| 550 |
+
ctx.beginPath()
|
| 551 |
+
;[[-1, -1], [1, -1], [1, 1], [-1, 1]].forEach(([sx, sy]) => {
|
| 552 |
+
const x = cam.cx + sx * half
|
| 553 |
+
const y = cam.cy + sy * half
|
| 554 |
+
ctx.moveTo(x, y - sy * arm); ctx.lineTo(x, y); ctx.lineTo(x - sx * arm, y)
|
| 555 |
+
})
|
| 556 |
+
ctx.stroke()
|
| 557 |
+
// 扫描线
|
| 558 |
+
const sp = prog(t, 8.3, 9.0)
|
| 559 |
+
if (sp > 0 && sp < 1) {
|
| 560 |
+
const top = cam.cy - 140
|
| 561 |
+
const y = lerp(top, cam.cy + 140, easeInOut(sp))
|
| 562 |
+
const g = ctx.createLinearGradient(0, y - 80, 0, y)
|
| 563 |
+
g.addColorStop(0, 'rgba(7,193,96,0)')
|
| 564 |
+
g.addColorStop(1, 'rgba(7,193,96,0.35)')
|
| 565 |
+
ctx.fillStyle = g
|
| 566 |
+
ctx.fillRect(cam.cx - 140, Math.max(top, y - 80), 280, y - Math.max(top, y - 80))
|
| 567 |
+
ctx.fillStyle = C.wechat
|
| 568 |
+
ctx.fillRect(cam.cx - 140, y - 2, 280, 4)
|
| 569 |
+
}
|
| 570 |
+
ctx.textAlign = 'center'
|
| 571 |
+
ctx.textBaseline = 'middle'
|
| 572 |
+
ctx.fillStyle = '#ffffff'
|
| 573 |
+
ctx.font = F('bold', 24)
|
| 574 |
+
ctx.fillText('扫一扫', 0, -sh / 2 + 78)
|
| 575 |
+
if (t < 9.0) {
|
| 576 |
+
ctx.globalAlpha = 0.7
|
| 577 |
+
ctx.font = F('normal', 19)
|
| 578 |
+
ctx.fillText('将二维码放入框内', 0, cam.cy + 185)
|
| 579 |
+
ctx.globalAlpha = 1
|
| 580 |
+
}
|
| 581 |
+
const flash = t >= 9.0 ? 0.35 * (1 - prog(t, 9.0, 9.25)) : 0
|
| 582 |
+
if (flash > 0) {
|
| 583 |
+
ctx.fillStyle = `rgba(255,255,255,${flash.toFixed(3)})`
|
| 584 |
+
ctx.fillRect(-sw / 2, -sh / 2, sw, sh)
|
| 585 |
+
}
|
| 586 |
+
// 识别结果卡片
|
| 587 |
+
const rp = prog(t, 9.1, 9.5)
|
| 588 |
+
if (rp > 0) {
|
| 589 |
+
const cw = sw - 36; const ch = 170
|
| 590 |
+
const cy = sh / 2 - 18 - ch + (1 - easeBack(rp)) * 220
|
| 591 |
+
ctx.fillStyle = '#ffffff'
|
| 592 |
+
ctx.beginPath(); rr(ctx, -cw / 2, cy, cw, ch, 28); ctx.fill()
|
| 593 |
+
ctx.fillStyle = C.wechat
|
| 594 |
+
ctx.beginPath(); ctx.arc(0, cy + 44, 22, 0, TAU); ctx.fill()
|
| 595 |
+
ctx.strokeStyle = '#fff'
|
| 596 |
+
ctx.lineWidth = 5
|
| 597 |
+
ctx.lineJoin = 'round'
|
| 598 |
+
ctx.beginPath(); ctx.moveTo(-9, cy + 44); ctx.lineTo(-2, cy + 51); ctx.lineTo(10, cy + 37); ctx.stroke()
|
| 599 |
+
ctx.fillStyle = C.ink
|
| 600 |
+
ctx.font = F('bold', 30)
|
| 601 |
+
ctx.fillText(DEMO_TEXT, 0, cy + 100)
|
| 602 |
+
ctx.fillStyle = C.muted
|
| 603 |
+
ctx.font = F('normal', 18)
|
| 604 |
+
ctx.fillText('扫码结果', 0, cy + 138)
|
| 605 |
+
}
|
| 606 |
+
ctx.restore()
|
| 607 |
+
// 灵动岛
|
| 608 |
+
ctx.fillStyle = '#000'
|
| 609 |
+
ctx.beginPath(); rr(ctx, -52, -h / 2 + 26, 104, 30, 15); ctx.fill()
|
| 610 |
+
ctx.restore()
|
| 611 |
+
|
| 612 |
+
// 识别成功:豆子礼花
|
| 613 |
+
const bt = t - 9.2
|
| 614 |
+
if (bt > 0 && bt < 1.4) {
|
| 615 |
+
const ox = px; const oy = py + 200
|
| 616 |
+
const fade = 1 - prog(bt, 0.9, 1.4)
|
| 617 |
+
ctx.globalAlpha = fade
|
| 618 |
+
burst.forEach((p) => {
|
| 619 |
+
const x = ox + Math.cos(p.a) * p.v * bt
|
| 620 |
+
const y = oy + Math.sin(p.a) * p.v * bt + 900 * bt * bt
|
| 621 |
+
addBead(groups, p.hex, x, y, p.r)
|
| 622 |
+
})
|
| 623 |
+
flushBeads(ctx, groups)
|
| 624 |
+
ctx.globalAlpha = 1
|
| 625 |
+
}
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
// 片尾
|
| 629 |
+
const drawEnd = (ctx, t) => {
|
| 630 |
+
if (t < 10.4) return
|
| 631 |
+
ctx.textAlign = 'center'
|
| 632 |
+
ctx.textBaseline = 'middle'
|
| 633 |
+
const a = easeOut(prog(t, 10.45, 10.95))
|
| 634 |
+
ctx.globalAlpha = a
|
| 635 |
+
ctx.fillStyle = C.ink
|
| 636 |
+
ctx.font = F('bold', 64)
|
| 637 |
+
ctx.fillText('豆维 · 可扫码的拼豆', 540, 745 + (1 - a) * 30)
|
| 638 |
+
const b = easeOut(prog(t, 10.6, 11.1))
|
| 639 |
+
ctx.globalAlpha = b
|
| 640 |
+
ctx.fillStyle = C.muted
|
| 641 |
+
ctx.font = F('normal', 32)
|
| 642 |
+
ctx.fillText('把一句话藏进拼豆里,微信一扫就能读出来', 540, 812 + (1 - b) * 30)
|
| 643 |
+
const chips = ['MARD 标准色号', 'A4 打印图纸', '微信一扫即读']
|
| 644 |
+
ctx.font = F('bold', 26)
|
| 645 |
+
const widths = chips.map((s) => ctx.measureText(s).width + 44)
|
| 646 |
+
const total = widths.reduce((s, v) => s + v, 0) + 18 * (chips.length - 1)
|
| 647 |
+
let x = 540 - total / 2
|
| 648 |
+
chips.forEach((label, i) => {
|
| 649 |
+
const p = easeBack(prog(t, 10.85 + i * 0.12, 11.3 + i * 0.12))
|
| 650 |
+
if (p > 0) {
|
| 651 |
+
ctx.globalAlpha = clamp01(p)
|
| 652 |
+
const cy = 892 + (1 - p) * 24
|
| 653 |
+
ctx.fillStyle = C.brandSoft
|
| 654 |
+
ctx.beginPath(); rr(ctx, x, cy - 26, widths[i], 52, 26); ctx.fill()
|
| 655 |
+
ctx.fillStyle = C.brand
|
| 656 |
+
ctx.fillText(label, x + widths[i] / 2, cy)
|
| 657 |
+
}
|
| 658 |
+
x += widths[i] + 18
|
| 659 |
+
})
|
| 660 |
+
ctx.globalAlpha = 1
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
const draw = (ctx, time, pixelWidth) => {
|
| 664 |
+
const t = loop ? ((time % DURATION) + DURATION) % DURATION : Math.min(time, DURATION)
|
| 665 |
+
const k = pixelWidth / U
|
| 666 |
+
ctx.setTransform(k, 0, 0, k, 0, 0)
|
| 667 |
+
ctx.globalAlpha = 1
|
| 668 |
+
drawBackground(ctx, t)
|
| 669 |
+
const L = boardAt(t)
|
| 670 |
+
if (t >= 2.65) {
|
| 671 |
+
const p = easeBack(prog(t, 2.65, 3.15))
|
| 672 |
+
const s = lerp(0.92, 1, p)
|
| 673 |
+
const LS = { cx: L.cx, cy: L.cy, B: L.B * s }
|
| 674 |
+
drawPlate(ctx, LS, prog(t, 2.65, 3.0))
|
| 675 |
+
}
|
| 676 |
+
drawInput(ctx, t)
|
| 677 |
+
if (t >= 2.72) drawMainBeads(ctx, t, L)
|
| 678 |
+
drawFinders(ctx, t, L)
|
| 679 |
+
drawGleam(ctx, L, prog(t, 5.9, 7.0), 0.55)
|
| 680 |
+
drawGleam(ctx, L, prog(t, 11.0, 11.7), 0.4)
|
| 681 |
+
drawPhoto(ctx, t, L)
|
| 682 |
+
drawPhone(ctx, t)
|
| 683 |
+
drawCaption(ctx, t)
|
| 684 |
+
drawProgress(ctx, t)
|
| 685 |
+
drawEnd(ctx, t)
|
| 686 |
+
if (loop) {
|
| 687 |
+
const fade = easeInOut(prog(t, 11.45, 12))
|
| 688 |
+
if (fade > 0) {
|
| 689 |
+
ctx.globalAlpha = fade
|
| 690 |
+
ctx.fillStyle = C.bg
|
| 691 |
+
ctx.fillRect(0, 0, U, U)
|
| 692 |
+
ctx.globalAlpha = 1
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
// 关键时间点(供视频配乐对齐)
|
| 699 |
+
const timeline = { landings: cells.map((b) => b.t0 + FLIGHT), wipes: cells.map((b) => b.tw) }
|
| 700 |
+
return { duration: DURATION, text: DEMO_TEXT, artwork: art, draw, timeline }
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
module.exports = { createIntro, DEMO_TEXT, DURATION }
|
BeadQR/miniprogram/utils/palette-data.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// MARD reference palette from the public 2026 color chart.
|
| 2 |
+
// 291 colors = standard 221 (A-H, M) + 70 extended (P, Q, R, T, Y, ZG).
|
| 3 |
+
const MARD_HEX = {
|
| 4 |
+
A1:"#f9f0cd", A2:"#fbfbd4", A3:"#fafc9f", A4:"#ffe953", A5:"#f4d738", A6:"#fdad49", A7:"#ff7c2f", A8:"#eaca49",
|
| 5 |
+
A9:"#ff995a", A10:"#ff9d55", A11:"#ffdd99", A12:"#fcb58f", A13:"#ffbb59", A14:"#ff6d40", A15:"#fdff44", A16:"#fef9ae",
|
| 6 |
+
A17:"#ffe36e", A18:"#fecf98", A19:"#fd7b72", A20:"#efcd67", A21:"#ffe395", A22:"#fff3a4", A23:"#f3d5bf", A24:"#fbf8c9",
|
| 7 |
+
A25:"#ffd67d", A26:"#ffbb27", B1:"#e6ee32", B2:"#5be419", B3:"#7cee9d", B4:"#1ef942", B5:"#00bd35", B6:"#5ae8ba",
|
| 8 |
+
B7:"#03ac88", B8:"#029d26", B9:"#26523a", B10:"#95d3c2", B11:"#5d722a", B12:"#156f40", B13:"#d9f794", B14:"#ade945",
|
| 9 |
+
B15:"#2e5132", B16:"#c6ed9c", B17:"#9bb13a", B18:"#e6ee49", B19:"#25b88c", B20:"#c2f0cc", B21:"#146a6b", B22:"#0b3c43",
|
| 10 |
+
B23:"#303921", B24:"#eefca5", B25:"#4e846d", B26:"#8c7a36", B27:"#d1dcc1", B28:"#9ee5b9", B29:"#c5e254", B30:"#ecfbd0",
|
| 11 |
+
B31:"#c4e6b5", B32:"#9bab5a", C1:"#e8ffe7", C2:"#bcf9f6", C3:"#a0e2fb", C4:"#42ccff", C5:"#01aceb", C6:"#50a9f0",
|
| 12 |
+
C7:"#0188d3", C8:"#1054c0", C9:"#314bca", C10:"#3ebce2", C11:"#03b9b9", C12:"#1c334d", C13:"#cde8ff", C14:"#d5fdff",
|
| 13 |
+
C15:"#23c4c6", C16:"#1757a8", C17:"#50d3ec", C18:"#1c3344", C19:"#1787a2", C20:"#0082be", C21:"#beddff", C22:"#67b4be",
|
| 14 |
+
C23:"#c2dceb", C24:"#7dc4ff", C25:"#a9e5e5", C26:"#2f99b3", C27:"#ebf5fc", C28:"#bbcfed", C29:"#4b5ba3", D1:"#aeb4f2",
|
| 15 |
+
D2:"#858edd", D3:"#3054af", D4:"#182a84", D5:"#b843c5", D6:"#ac7bde", D7:"#6e399a", D8:"#e2d3ff", D9:"#d5b9f8",
|
| 16 |
+
D10:"#361b50", D11:"#b9bae1", D12:"#de9ad4", D13:"#b90295", D14:"#8b279b", D15:"#2f1f90", D16:"#e2e1ee", D17:"#c4d4f6",
|
| 17 |
+
D18:"#a45ec7", D19:"#d8c3d7", D20:"#9c32b2", D21:"#9a009b", D22:"#333995", D23:"#eadafc", D24:"#7786e5", D25:"#484fc7",
|
| 18 |
+
D26:"#e9c3f6", E1:"#fdd3cc", E2:"#fecddf", E3:"#ff97c3", E4:"#e8649e", E5:"#f551a2", E6:"#ff346b", E7:"#c63578",
|
| 19 |
+
E8:"#ffdbe9", E9:"#e970cc", E10:"#d33893", E11:"#fcddd2", E12:"#ffa1c5", E13:"#b6006d", E14:"#ffd1ba", E15:"#f2cfd0",
|
| 20 |
+
E16:"#ffecde", E17:"#ffe2ea", E18:"#ffc9d6", E19:"#ffd2e7", E20:"#d8c7d1", E21:"#bd9da1", E22:"#cc78a7", E23:"#937a8d",
|
| 21 |
+
E24:"#f6e4f9", F1:"#fd957b", F2:"#fc3d45", F3:"#f74941", F4:"#fc283c", F5:"#d80127", F6:"#b0443d", F7:"#971937",
|
| 22 |
+
F8:"#bc0127", F9:"#e2677a", F10:"#a74d22", F11:"#6f201f", F12:"#fd4d6a", F13:"#dd422f", F14:"#ffa9ad", F15:"#c80020",
|
| 23 |
+
F16:"#ffd9c8", F17:"#f79b71", F18:"#d37c46", F19:"#c1444a", F20:"#cd9391", F21:"#f4b1b4", F22:"#ffd0cb", F23:"#f57e66",
|
| 24 |
+
F24:"#fcc1c4", F25:"#e54b4f", G1:"#ffe2ce", G2:"#ffcaaa", G3:"#f4c3a5", G4:"#e1b383", G5:"#ed9435", G6:"#f59734",
|
| 25 |
+
G7:"#9d5b3e", G8:"#592a21", G9:"#e6b483", G10:"#c88135", G11:"#e0c593", G12:"#ebbb83", G13:"#b7714a", G14:"#8d614c",
|
| 26 |
+
G15:"#fcf9e0", G16:"#f2d9ba", G17:"#56403c", G18:"#ffe4cc", G19:"#e1943a", G20:"#a94023", G21:"#cb8e77", H1:"#e2e2e2",
|
| 27 |
+
H2:"#ffffff", H3:"#b3b3b3", H4:"#868686", H5:"#474747", H6:"#2c2c2c", H7:"#000000", H8:"#e7d6db", H9:"#e4e7e3",
|
| 28 |
+
H10:"#eee9ea", H11:"#cecdd5", H12:"#fff5ed", H13:"#f3e1c9", H14:"#cfd7d3", H15:"#98a6a8", H16:"#3b2f23", H17:"#f1eded",
|
| 29 |
+
H18:"#fffdf0", H19:"#f6efe2", H20:"#949fa3", H21:"#f7f3e4", H22:"#cacad5", H23:"#9a9d94", M1:"#bcc6b8", M2:"#8aa385",
|
| 30 |
+
M3:"#697d80", M4:"#dacebe", M5:"#d0ccaa", M6:"#b0a782", M7:"#b4a497", M8:"#b38281", M9:"#a58767", M10:"#c5b1bc",
|
| 31 |
+
M11:"#9f7494", M12:"#644749", M13:"#d19066", M14:"#c77361", M15:"#757d7b", P1:"#fcf8f9", P2:"#bda9ab", P3:"#aedda9",
|
| 32 |
+
P4:"#fda49e", P5:"#ec8d3d", P6:"#60cfa8", P7:"#eb9271", P8:"#f0d958", P9:"#d9d9d9", P10:"#d5c8e9", P11:"#f3ecc8",
|
| 33 |
+
P12:"#e6eef1", P13:"#a9cbf1", P14:"#3177b0", P15:"#668575", P16:"#ffbe46", P17:"#ffa324", P18:"#feb89f", P19:"#ffe0e8",
|
| 34 |
+
P20:"#febecf", P21:"#ecbec0", P22:"#e4a89e", P23:"#a56269", Q1:"#f2a5e8", Q2:"#73b29e", Q3:"#ffff00", Q4:"#ffebfa",
|
| 35 |
+
Q5:"#4f5e5b", R1:"#d50e21", R2:"#f92e83", R3:"#fd8225", R4:"#f8ec31", R5:"#34c75b", R6:"#25b891", R7:"#17779d",
|
| 36 |
+
R8:"#1b60c3", R9:"#9a56b4", R10:"#ffdb4d", R11:"#ffebfa", R12:"#d8d5ce", R13:"#55514c", R14:"#9ee4df", R15:"#77cee9",
|
| 37 |
+
R16:"#3dcfca", R17:"#4a867a", R18:"#7fcd9d", R19:"#cde55d", R20:"#e8c7b4", R21:"#ad6f3c", R22:"#6c372f", R23:"#feb872",
|
| 38 |
+
R24:"#f2c1c0", R25:"#c9675d", R26:"#d293be", R27:"#ea8cb1", R28:"#9c87d6", T1:"#e2dfd7", Y1:"#fd6fb4", Y2:"#feb481",
|
| 39 |
+
Y3:"#d7faa0", Y4:"#8bdbfa", Y5:"#e987ea", ZG1:"#daabb3", ZG2:"#d6aa87", ZG3:"#c1bd8d", ZG4:"#96869f", ZG5:"#8490a6",
|
| 40 |
+
ZG6:"#94bfe2", ZG7:"#e2a9d2", ZG8:"#ab91c0",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const MARD_PALETTE = Object.entries(MARD_HEX).map(([id, hex]) => ({ id, hex }))
|
| 44 |
+
|
| 45 |
+
// 智能近色色库的“远点优先”排序(离线预计算,避免真机启动时 2000 万次距离运算)
|
| 46 |
+
const MARD_SMART_ORDER = ["A4","A7","B5","B9","C4","C8","C12","D7","E5","F3","G5","G8","H2","H3","H5","H7","M3","M12","P3","P14","Q1","R5","R13","ZG6","E13","B17","Q3","B2","A22","C11","D18","P6","G20","F5","P4","M8","D15","C2","D24","A19","B4","B14","G13","B21","R12","E7","C7","D21","E9","B19","D25","M2","D8","D20","R16","R23","B26","B11","E6","F7","E22","F19","ZG2","A26","A2","C5","B3","C28","E2","M13","B1","B13","ZG4","C6","R19","R28","G2","ZG1","E3","P23","A8","A9","R14","C29","C24","H6","M6","B7","G7","Q2","M5","D10","B8","B20","B10","F9","C22","C26","B12","B25","F4","D5","C19","B32","F13","A25","H15","D13","G10","B6","Y5","H10","Y1","D14","D1","D3","D12","E21","E10","M9","D9","R27","C10","R18","H13","H4","P21","ZG3","G11","C15","ZG8","P7","D4","C3","M14","A20","F8","R22","C14","D22","A15","A6","R26","R2","C16","B31","F12","R15","F20","C17","A11","F11","P17","A5","C9","P22","E4","B18","C1","C21","M11","B16","R9","F25","A12","A14","Y4","G14","B28","H23","P20","C20","C27","ZG5","B22","G12","M1","D6","F6","A13","P10","D11","F18","F1","Q5","Q4","M10","G17","H8","A17","D26","F21","P19","B24","P8","H22","P13","C13","F22","G21","R4","D2","H16","M4","P16","R21","E14","R7","R8","P18","H12","B27","D19","ZG7","E16","P2","B30","R20","F14","G16","R10","M7","E23","B23","D16","C23","F23","R1","Y2","A18","E15","G3","F15","P15","R17","D17","F2","E11","A1","F10","F17","G19","M15","G4","R25","C25","Y3","B29","H9","G15","R3","P9","P12","A16","F24","A3","E24","B15","D23","T1","P5","H14","H19","E12"]
|
| 47 |
+
|
| 48 |
+
module.exports = { MARD_PALETTE, MARD_SMART_ORDER }
|
BeadQR/miniprogram/utils/palette.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// MARD 色库预设 + 预处理(Typed Array 结构,供匹配引擎高速读取)
|
| 2 |
+
const { MARD_PALETTE, MARD_SMART_ORDER } = require('./palette-data')
|
| 3 |
+
const { hexToRgb, rgbToOklabInto, luma } = require('./color')
|
| 4 |
+
|
| 5 |
+
// 颜色匹配模式:darkMax/lightMin 为二维码深/浅模块允许的亮度范围
|
| 6 |
+
const MATCH_MODES = [
|
| 7 |
+
{ id: 'rich', label: '色彩优先', hint: '更像原图', darkMax: 0.4, lightMin: 0.74 },
|
| 8 |
+
{ id: 'balanced', label: '均衡', hint: '推荐', darkMax: 0.36, lightMin: 0.78 },
|
| 9 |
+
{ id: 'strict', label: '扫码优先', hint: '最稳', darkMax: 0.3, lightMin: 0.84 },
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
const byId = new Map(MARD_PALETTE.map((c) => [c.id, c]))
|
| 13 |
+
const pick = (ids) => ids.map((id) => byId.get(id)).filter(Boolean)
|
| 14 |
+
const STANDARD_221 = MARD_PALETTE.filter((c) => /^[A-HM]\d+$/.test(c.id))
|
| 15 |
+
|
| 16 |
+
const PRESETS = [
|
| 17 |
+
{ id: '221', label: '标准 221 色', short: '221 色', detail: 'MARD 标准色,最常见', list: () => STANDARD_221 },
|
| 18 |
+
{ id: '291', label: '完整 291 色', short: '291 色', detail: '含扩展色,颜色最全', list: () => MARD_PALETTE },
|
| 19 |
+
...[144, 96, 72, 48, 24].map((n) => ({
|
| 20 |
+
id: String(n),
|
| 21 |
+
label: `精选 ${n} 色`,
|
| 22 |
+
short: `${n} 色`,
|
| 23 |
+
detail: n <= 48 ? '入门套装,买豆更省' : '常用色,兼顾效果与成本',
|
| 24 |
+
list: () => pick(MARD_SMART_ORDER.slice(0, n)),
|
| 25 |
+
})),
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
const cache = {}
|
| 29 |
+
|
| 30 |
+
function preparePalette(presetId) {
|
| 31 |
+
if (cache[presetId]) return cache[presetId]
|
| 32 |
+
const preset = PRESETS.find((p) => p.id === presetId) || PRESETS[0]
|
| 33 |
+
const list = preset.list()
|
| 34 |
+
const n = list.length
|
| 35 |
+
const rgb = new Uint8Array(n * 3)
|
| 36 |
+
const lab = new Float32Array(n * 3)
|
| 37 |
+
const lum = new Float32Array(n)
|
| 38 |
+
list.forEach((c, i) => {
|
| 39 |
+
const [r, g, b] = hexToRgb(c.hex)
|
| 40 |
+
rgb[i * 3] = r; rgb[i * 3 + 1] = g; rgb[i * 3 + 2] = b
|
| 41 |
+
rgbToOklabInto(r, g, b, lab, i * 3)
|
| 42 |
+
lum[i] = luma(r, g, b)
|
| 43 |
+
})
|
| 44 |
+
// 色与色之间的距离矩阵(291² ≈ 8.5 万项,一次性计算)
|
| 45 |
+
const pair = new Float32Array(n * n)
|
| 46 |
+
for (let i = 0; i < n; i += 1) {
|
| 47 |
+
for (let j = i + 1; j < n; j += 1) {
|
| 48 |
+
const dl = lab[i * 3] - lab[j * 3]
|
| 49 |
+
const da = lab[i * 3 + 1] - lab[j * 3 + 1]
|
| 50 |
+
const db = lab[i * 3 + 2] - lab[j * 3 + 2]
|
| 51 |
+
const d = Math.sqrt(dl * dl + da * da + db * db)
|
| 52 |
+
pair[i * n + j] = d
|
| 53 |
+
pair[j * n + i] = d
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
const prepared = {
|
| 57 |
+
id: preset.id,
|
| 58 |
+
label: preset.label,
|
| 59 |
+
short: preset.short,
|
| 60 |
+
n,
|
| 61 |
+
ids: list.map((c) => c.id),
|
| 62 |
+
hexes: list.map((c) => c.hex),
|
| 63 |
+
rgb,
|
| 64 |
+
lab,
|
| 65 |
+
lum,
|
| 66 |
+
pair,
|
| 67 |
+
subsets: {},
|
| 68 |
+
luts: {},
|
| 69 |
+
}
|
| 70 |
+
cache[presetId] = prepared
|
| 71 |
+
return prepared
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
module.exports = { MATCH_MODES, PRESETS, preparePalette }
|
BeadQR/miniprogram/utils/pattern.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Canvas 绘制:预览、PNG 图纸页、作品信息页、水印
|
| 2 |
+
// 性能要点:按颜色批量合并路径,每种颜色只 fill 一次;字体只设置一次;网格线合并成单条路径
|
| 3 |
+
const { lumaHex } = require('./color')
|
| 4 |
+
|
| 5 |
+
const BOARD = '#f1eee7' // 钉板底色
|
| 6 |
+
const PEG = '#d9d3c7' // 空钉位
|
| 7 |
+
const INK = '#1d2b2a'
|
| 8 |
+
const MUTED = '#6b7671'
|
| 9 |
+
const BRAND = '#0f766e'
|
| 10 |
+
|
| 11 |
+
function roundRectPath(ctx, x, y, w, h, r) {
|
| 12 |
+
const rr = Math.min(r, w / 2, h / 2)
|
| 13 |
+
ctx.moveTo(x + rr, y)
|
| 14 |
+
ctx.arcTo(x + w, y, x + w, y + h, rr)
|
| 15 |
+
ctx.arcTo(x + w, y + h, x, y + h, rr)
|
| 16 |
+
ctx.arcTo(x, y + h, x, y, rr)
|
| 17 |
+
ctx.arcTo(x, y, x + w, y, rr)
|
| 18 |
+
ctx.closePath()
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function fitText(ctx, value, maxWidth) {
|
| 22 |
+
const text = String(value || '')
|
| 23 |
+
if (ctx.measureText(text).width <= maxWidth) return text
|
| 24 |
+
let end = text.length
|
| 25 |
+
while (end > 0 && ctx.measureText(`${text.slice(0, end)}…`).width > maxWidth) end -= 1
|
| 26 |
+
return `${text.slice(0, end)}…`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// 把同色格子分组:返回 Map<colorIndex, number[] cellIndex>
|
| 30 |
+
function groupCells(artwork, region) {
|
| 31 |
+
const { size, cells } = artwork
|
| 32 |
+
const r = region || { col: 0, row: 0, cols: size, rows: size }
|
| 33 |
+
const groups = new Map()
|
| 34 |
+
const empty = []
|
| 35 |
+
for (let y = r.row; y < r.row + r.rows; y += 1) {
|
| 36 |
+
for (let x = r.col; x < r.col + r.cols; x += 1) {
|
| 37 |
+
const c = cells[y * size + x]
|
| 38 |
+
if (c < 0) { empty.push(y * size + x); continue }
|
| 39 |
+
let list = groups.get(c)
|
| 40 |
+
if (!list) { list = []; groups.set(c, list) }
|
| 41 |
+
list.push(y * size + x)
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
return { groups, empty, region: r }
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* 绘制珠子
|
| 49 |
+
* opts: { x0, y0, cell, style: square|round|hollow, region }
|
| 50 |
+
*/
|
| 51 |
+
function drawBeads(ctx, artwork, opts) {
|
| 52 |
+
const { x0, y0, cell, style = 'square' } = opts
|
| 53 |
+
const { size, pal } = artwork
|
| 54 |
+
const { groups, empty, region } = groupCells(artwork, opts.region)
|
| 55 |
+
const ARC_CHUNK = 12
|
| 56 |
+
const pos = (i) => {
|
| 57 |
+
const x = i % size
|
| 58 |
+
const y = (i - x) / size
|
| 59 |
+
return [x0 + (x - region.col) * cell, y0 + (y - region.row) * cell]
|
| 60 |
+
}
|
| 61 |
+
ctx.fillStyle = BOARD
|
| 62 |
+
ctx.fillRect(x0, y0, region.cols * cell, region.rows * cell)
|
| 63 |
+
// 空钉位:小圆点,提示“这里不放豆”
|
| 64 |
+
if (empty.length) {
|
| 65 |
+
ctx.fillStyle = PEG
|
| 66 |
+
ctx.beginPath()
|
| 67 |
+
const r = cell * 0.14
|
| 68 |
+
empty.forEach((i, n) => {
|
| 69 |
+
const [x, y] = pos(i)
|
| 70 |
+
ctx.moveTo(x + cell / 2 + r, y + cell / 2)
|
| 71 |
+
ctx.arc(x + cell / 2, y + cell / 2, r, 0, Math.PI * 2)
|
| 72 |
+
if (n % ARC_CHUNK === ARC_CHUNK - 1) { ctx.fill(); ctx.beginPath() }
|
| 73 |
+
})
|
| 74 |
+
ctx.fill()
|
| 75 |
+
}
|
| 76 |
+
const radius = cell * 0.46
|
| 77 |
+
const hole = cell * 0.2
|
| 78 |
+
groups.forEach((list, c) => {
|
| 79 |
+
ctx.fillStyle = pal.hexes[c]
|
| 80 |
+
ctx.beginPath()
|
| 81 |
+
if (style === 'square') {
|
| 82 |
+
list.forEach((i) => {
|
| 83 |
+
const [x, y] = pos(i)
|
| 84 |
+
// 取整边界避免相邻方块之间出现发丝缝
|
| 85 |
+
const lx = Math.round(x); const ty = Math.round(y)
|
| 86 |
+
ctx.rect(lx, ty, Math.round(x + cell) - lx, Math.round(y + cell) - ty)
|
| 87 |
+
})
|
| 88 |
+
} else {
|
| 89 |
+
// 圆形子路径过多时渲染引擎会明显变慢,按小批提交(大图从数秒降到几十毫秒)
|
| 90 |
+
list.forEach((i, n) => {
|
| 91 |
+
const [x, y] = pos(i)
|
| 92 |
+
const cx = x + cell / 2; const cy = y + cell / 2
|
| 93 |
+
ctx.moveTo(cx + radius, cy)
|
| 94 |
+
ctx.arc(cx, cy, radius, 0, Math.PI * 2)
|
| 95 |
+
if (style === 'hollow') {
|
| 96 |
+
ctx.moveTo(cx + hole, cy)
|
| 97 |
+
ctx.arc(cx, cy, hole, 0, Math.PI * 2, true)
|
| 98 |
+
}
|
| 99 |
+
if (n % ARC_CHUNK === ARC_CHUNK - 1) { ctx.fill(); ctx.beginPath() }
|
| 100 |
+
})
|
| 101 |
+
}
|
| 102 |
+
ctx.fill()
|
| 103 |
+
})
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// 色号文字:先按深/浅文字颜色分两批
|
| 107 |
+
function drawCodes(ctx, artwork, opts) {
|
| 108 |
+
const { x0, y0, cell, style = 'square' } = opts
|
| 109 |
+
const { size, cells, pal } = artwork
|
| 110 |
+
const region = opts.region || { col: 0, row: 0, cols: size, rows: size }
|
| 111 |
+
const light = pal.hexes.map((hex) => lumaHex(hex) > 0.56)
|
| 112 |
+
const fontSize = Math.max(8, Math.round(cell * (style === 'hollow' ? 0.26 : 0.32)))
|
| 113 |
+
ctx.font = `bold ${fontSize}px sans-serif`
|
| 114 |
+
ctx.textAlign = 'center'
|
| 115 |
+
ctx.textBaseline = 'middle'
|
| 116 |
+
const pass = (wantLight) => {
|
| 117 |
+
ctx.fillStyle = wantLight ? INK : '#ffffff'
|
| 118 |
+
for (let y = region.row; y < region.row + region.rows; y += 1) {
|
| 119 |
+
for (let x = region.col; x < region.col + region.cols; x += 1) {
|
| 120 |
+
const c = cells[y * size + x]
|
| 121 |
+
if (c < 0) continue
|
| 122 |
+
const useInk = style === 'hollow' ? true : light[c]
|
| 123 |
+
if (useInk !== wantLight) continue
|
| 124 |
+
ctx.fillText(pal.ids[c], x0 + (x - region.col + 0.5) * cell, y0 + (y - region.row + 0.5) * cell + fontSize * 0.04)
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
pass(true)
|
| 129 |
+
pass(false)
|
| 130 |
+
ctx.textAlign = 'start'
|
| 131 |
+
ctx.textBaseline = 'alphabetic'
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function rgba(hex, alpha) {
|
| 135 |
+
const v = parseInt(hex.slice(1), 16)
|
| 136 |
+
return `rgba(${(v >> 16) & 255},${(v >> 8) & 255},${v & 255},${alpha})`
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// 网格 + 坐标:细线每格一条,粗线每 interval 格一条(按全局坐标对齐)
|
| 140 |
+
function drawGrid(ctx, opts) {
|
| 141 |
+
const { x0, y0, cell, region, interval = 10, color = BRAND, opacity = 60, labels = true } = opts
|
| 142 |
+
const w = region.cols * cell
|
| 143 |
+
const h = region.rows * cell
|
| 144 |
+
const alpha = opacity / 100
|
| 145 |
+
ctx.lineWidth = Math.max(1, cell * 0.02)
|
| 146 |
+
ctx.strokeStyle = rgba(color, alpha * 0.28)
|
| 147 |
+
ctx.beginPath()
|
| 148 |
+
for (let i = 0; i <= region.cols; i += 1) { const x = Math.round(x0 + i * cell) + 0.5; ctx.moveTo(x, y0); ctx.lineTo(x, y0 + h) }
|
| 149 |
+
for (let i = 0; i <= region.rows; i += 1) { const y = Math.round(y0 + i * cell) + 0.5; ctx.moveTo(x0, y); ctx.lineTo(x0 + w, y) }
|
| 150 |
+
ctx.stroke()
|
| 151 |
+
ctx.lineWidth = Math.max(2, cell * 0.06)
|
| 152 |
+
ctx.strokeStyle = rgba(color, alpha)
|
| 153 |
+
ctx.beginPath()
|
| 154 |
+
for (let i = 0; i <= region.cols; i += 1) {
|
| 155 |
+
const g = region.col + i
|
| 156 |
+
if (g % interval !== 0 && i !== 0 && i !== region.cols) continue
|
| 157 |
+
const x = x0 + i * cell; ctx.moveTo(x, y0); ctx.lineTo(x, y0 + h)
|
| 158 |
+
}
|
| 159 |
+
for (let i = 0; i <= region.rows; i += 1) {
|
| 160 |
+
const g = region.row + i
|
| 161 |
+
if (g % interval !== 0 && i !== 0 && i !== region.rows) continue
|
| 162 |
+
const y = y0 + i * cell; ctx.moveTo(x0, y); ctx.lineTo(x0 + w, y)
|
| 163 |
+
}
|
| 164 |
+
ctx.stroke()
|
| 165 |
+
if (!labels) return
|
| 166 |
+
const fontSize = Math.max(10, Math.round(Math.min(cell * 0.42, 34)))
|
| 167 |
+
ctx.font = `${fontSize}px sans-serif`
|
| 168 |
+
ctx.fillStyle = rgba(color, Math.max(0.7, alpha))
|
| 169 |
+
ctx.textAlign = 'center'
|
| 170 |
+
ctx.textBaseline = 'middle'
|
| 171 |
+
const step = cell < 22 ? interval : (cell < 36 ? 5 : 1)
|
| 172 |
+
for (let i = 0; i < region.cols; i += 1) {
|
| 173 |
+
const g = region.col + i + 1
|
| 174 |
+
if (g % step !== 0 && g !== 1) continue
|
| 175 |
+
ctx.fillText(String(g), x0 + (i + 0.5) * cell, y0 - fontSize * 0.9)
|
| 176 |
+
}
|
| 177 |
+
for (let i = 0; i < region.rows; i += 1) {
|
| 178 |
+
const g = region.row + i + 1
|
| 179 |
+
if (g % step !== 0 && g !== 1) continue
|
| 180 |
+
ctx.fillText(String(g), x0 - fontSize * 1.1, y0 + (i + 0.5) * cell)
|
| 181 |
+
}
|
| 182 |
+
ctx.textAlign = 'start'
|
| 183 |
+
ctx.textBaseline = 'alphabetic'
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// 屏幕预览:整张作品,不带坐标(清爽)
|
| 187 |
+
function renderPreview(canvas, artwork, pixelWidth, style) {
|
| 188 |
+
const size = artwork.size
|
| 189 |
+
const pad = Math.round(pixelWidth * 0.03)
|
| 190 |
+
const cell = (pixelWidth - pad * 2) / size
|
| 191 |
+
canvas.width = pixelWidth
|
| 192 |
+
canvas.height = pixelWidth
|
| 193 |
+
const ctx = canvas.getContext('2d')
|
| 194 |
+
ctx.fillStyle = BOARD
|
| 195 |
+
ctx.fillRect(0, 0, pixelWidth, pixelWidth)
|
| 196 |
+
drawBeads(ctx, artwork, { x0: pad, y0: pad, cell, style })
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// —— PNG 图纸页 ——
|
| 200 |
+
function patternPageLayout(size, cell) {
|
| 201 |
+
const axis = Math.max(48, Math.round(cell * 1.1))
|
| 202 |
+
const pad = Math.max(48, Math.round(cell * 0.9))
|
| 203 |
+
const header = Math.max(120, Math.round(cell * 2.4))
|
| 204 |
+
const grid = size * cell
|
| 205 |
+
return { axis, pad, header, width: grid + axis + pad * 2, height: header + axis + grid + pad * 2 }
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
function drawPatternPage(ctx, artwork, opts) {
|
| 209 |
+
const { cell, style, grid, meta } = opts
|
| 210 |
+
const L = patternPageLayout(artwork.size, cell)
|
| 211 |
+
ctx.fillStyle = '#fbfaf7'
|
| 212 |
+
ctx.fillRect(0, 0, L.width, L.height)
|
| 213 |
+
// 头部
|
| 214 |
+
const unit = L.header / 120
|
| 215 |
+
ctx.fillStyle = INK
|
| 216 |
+
ctx.font = `bold ${Math.round(40 * unit)}px sans-serif`
|
| 217 |
+
ctx.textBaseline = 'middle'
|
| 218 |
+
ctx.fillText('豆维 · 拼豆图纸', L.pad, L.pad * 0.6 + L.header * 0.36)
|
| 219 |
+
ctx.font = `${Math.round(22 * unit)}px sans-serif`
|
| 220 |
+
ctx.fillStyle = MUTED
|
| 221 |
+
ctx.fillText(fitText(ctx, meta, L.width - L.pad * 2), L.pad, L.pad * 0.6 + L.header * 0.74)
|
| 222 |
+
ctx.textBaseline = 'alphabetic'
|
| 223 |
+
const x0 = L.pad + L.axis
|
| 224 |
+
const y0 = L.header + L.pad * 0.6 + L.axis
|
| 225 |
+
const region = { col: 0, row: 0, cols: artwork.size, rows: artwork.size }
|
| 226 |
+
drawBeads(ctx, artwork, { x0, y0, cell, style, region })
|
| 227 |
+
if (cell >= 16) drawCodes(ctx, artwork, { x0, y0, cell, style, region })
|
| 228 |
+
if (grid.enabled) drawGrid(ctx, { x0, y0, cell, region, interval: grid.interval, color: grid.color, opacity: grid.opacity })
|
| 229 |
+
return L
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// —— 作品信息页(A4 比例,可分页)——
|
| 233 |
+
function buildInfoPages({ artwork, width, text, paletteLabel, mpCode, thumbnail }) {
|
| 234 |
+
const height = Math.round(width * 297 / 210)
|
| 235 |
+
const u = width / 1600
|
| 236 |
+
const pad = 96 * u
|
| 237 |
+
const colors = artwork.usage.slice().sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }))
|
| 238 |
+
const columns = 4
|
| 239 |
+
const gap = 24 * u
|
| 240 |
+
const cardW = (width - pad * 2 - gap * (columns - 1)) / columns
|
| 241 |
+
const cardH = 124 * u
|
| 242 |
+
const headerH = 300 * u
|
| 243 |
+
// 缩略图区:headerH+64 起,高 400;清单标题与说明各占一行,避免与上方重叠
|
| 244 |
+
const listTitleY = headerH + 64 * u + 400 * u + 96 * u
|
| 245 |
+
const firstTop = listTitleY + 88 * u
|
| 246 |
+
const nextTop = 200 * u
|
| 247 |
+
const bottom = height - 110 * u
|
| 248 |
+
const rowsFirst = Math.max(1, Math.floor((bottom - firstTop + gap) / (cardH + gap)))
|
| 249 |
+
const rowsNext = Math.max(1, Math.floor((bottom - nextTop + gap) / (cardH + gap)))
|
| 250 |
+
const pages = []
|
| 251 |
+
let rest = colors.length
|
| 252 |
+
let first = true
|
| 253 |
+
do {
|
| 254 |
+
const take = Math.min(rest, (first ? rowsFirst : rowsNext) * columns)
|
| 255 |
+
pages.push(take)
|
| 256 |
+
rest -= take
|
| 257 |
+
first = false
|
| 258 |
+
} while (rest > 0)
|
| 259 |
+
|
| 260 |
+
const drawCard = (ctx, color, x, y) => {
|
| 261 |
+
ctx.fillStyle = '#ffffff'
|
| 262 |
+
ctx.beginPath(); roundRectPath(ctx, x, y, cardW, cardH, 18 * u); ctx.fill()
|
| 263 |
+
ctx.strokeStyle = '#e7e2d8'; ctx.lineWidth = 2 * u; ctx.stroke()
|
| 264 |
+
const sw = 76 * u
|
| 265 |
+
ctx.fillStyle = color.hex
|
| 266 |
+
ctx.beginPath(); ctx.arc(x + 24 * u + sw / 2, y + cardH / 2, sw / 2, 0, Math.PI * 2); ctx.fill()
|
| 267 |
+
ctx.strokeStyle = 'rgba(0,0,0,0.12)'; ctx.stroke()
|
| 268 |
+
ctx.fillStyle = INK
|
| 269 |
+
ctx.font = `bold ${Math.round(34 * u)}px sans-serif`
|
| 270 |
+
ctx.fillText(color.id, x + 24 * u + sw + 20 * u, y + 56 * u)
|
| 271 |
+
ctx.fillStyle = BRAND
|
| 272 |
+
ctx.font = `${Math.round(26 * u)}px sans-serif`
|
| 273 |
+
ctx.fillText(`${color.count} 颗`, x + 24 * u + sw + 20 * u, y + 96 * u)
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
const draw = (ctx, page) => {
|
| 277 |
+
ctx.textAlign = 'start'
|
| 278 |
+
ctx.textBaseline = 'alphabetic'
|
| 279 |
+
ctx.fillStyle = '#fbfaf7'
|
| 280 |
+
ctx.fillRect(0, 0, width, height)
|
| 281 |
+
let top = nextTop
|
| 282 |
+
if (page === 0) {
|
| 283 |
+
ctx.fillStyle = INK
|
| 284 |
+
ctx.fillRect(0, 0, width, headerH)
|
| 285 |
+
ctx.fillStyle = '#ffffff'
|
| 286 |
+
ctx.font = `bold ${Math.round(62 * u)}px sans-serif`
|
| 287 |
+
ctx.fillText('豆维 · 作品信息与用豆清单', pad, 132 * u)
|
| 288 |
+
ctx.fillStyle = '#9fd3c7'
|
| 289 |
+
ctx.font = `${Math.round(28 * u)}px sans-serif`
|
| 290 |
+
ctx.fillText('照着清单备豆,照着图纸拼,拼完用微信扫一扫', pad, 200 * u)
|
| 291 |
+
if (mpCode) {
|
| 292 |
+
const s = 200 * u
|
| 293 |
+
const x = width - pad - s
|
| 294 |
+
const y = (headerH - s) / 2
|
| 295 |
+
ctx.fillStyle = '#ffffff'
|
| 296 |
+
ctx.beginPath(); roundRectPath(ctx, x - 10 * u, y - 10 * u, s + 20 * u, s + 20 * u, 20 * u); ctx.fill()
|
| 297 |
+
ctx.drawImage(mpCode, x, y, s, s)
|
| 298 |
+
}
|
| 299 |
+
// 缩略图 + 参数
|
| 300 |
+
const thumbS = 400 * u
|
| 301 |
+
const ty = headerH + 64 * u
|
| 302 |
+
if (thumbnail) {
|
| 303 |
+
ctx.fillStyle = '#ffffff'
|
| 304 |
+
ctx.beginPath(); roundRectPath(ctx, pad, ty, thumbS, thumbS, 24 * u); ctx.fill()
|
| 305 |
+
const inner = thumbS - 32 * u
|
| 306 |
+
const cell = inner / artwork.size
|
| 307 |
+
drawBeads(ctx, artwork, { x0: pad + 16 * u, y0: ty + 16 * u, cell, style: 'square' })
|
| 308 |
+
}
|
| 309 |
+
const mx = thumbnail ? pad + thumbS + 56 * u : pad
|
| 310 |
+
const rows = [
|
| 311 |
+
['扫码内容', text],
|
| 312 |
+
['作品尺寸', `${artwork.size} × ${artwork.size} 格`],
|
| 313 |
+
['用豆总数', `${artwork.beads.toLocaleString()} 颗 · ${artwork.usage.length} 个色号`],
|
| 314 |
+
['二维码', `${artwork.qr.size} × ${artwork.qr.size} 格 · 纠错 ${artwork.qr.level}`],
|
| 315 |
+
['色库', paletteLabel],
|
| 316 |
+
]
|
| 317 |
+
rows.forEach(([k, v], idx) => {
|
| 318 |
+
const y = ty + 52 * u + idx * 74 * u
|
| 319 |
+
ctx.fillStyle = MUTED
|
| 320 |
+
ctx.font = `${Math.round(26 * u)}px sans-serif`
|
| 321 |
+
ctx.fillText(k, mx, y)
|
| 322 |
+
ctx.fillStyle = INK
|
| 323 |
+
ctx.font = `bold ${Math.round(28 * u)}px sans-serif`
|
| 324 |
+
ctx.fillText(fitText(ctx, v, width - pad - mx - 150 * u), mx + 150 * u, y)
|
| 325 |
+
})
|
| 326 |
+
ctx.fillStyle = INK
|
| 327 |
+
ctx.font = `bold ${Math.round(42 * u)}px sans-serif`
|
| 328 |
+
ctx.fillText('用豆清单', pad, listTitleY)
|
| 329 |
+
ctx.fillStyle = MUTED
|
| 330 |
+
ctx.font = `${Math.round(24 * u)}px sans-serif`
|
| 331 |
+
ctx.fillText('按色号排序 · 建议每色多备 5% 以防掉落', pad, listTitleY + 44 * u)
|
| 332 |
+
top = firstTop
|
| 333 |
+
} else {
|
| 334 |
+
ctx.fillStyle = INK
|
| 335 |
+
ctx.font = `bold ${Math.round(36 * u)}px sans-serif`
|
| 336 |
+
ctx.fillText(`用豆清单(续 ${page + 1}/${pages.length})`, pad, 120 * u)
|
| 337 |
+
}
|
| 338 |
+
const offset = pages.slice(0, page).reduce((s, v) => s + v, 0)
|
| 339 |
+
for (let i = 0; i < pages[page]; i += 1) {
|
| 340 |
+
const col = i % columns
|
| 341 |
+
const row = Math.floor(i / columns)
|
| 342 |
+
drawCard(ctx, colors[offset + i], pad + col * (cardW + gap), top + row * (cardH + gap))
|
| 343 |
+
}
|
| 344 |
+
ctx.fillStyle = '#a3aaa5'
|
| 345 |
+
ctx.font = `${Math.round(22 * u)}px sans-serif`
|
| 346 |
+
ctx.fillText(`豆维 · 可扫码的拼豆 第 ${page + 1} / ${pages.length} 页`, pad, height - 56 * u)
|
| 347 |
+
}
|
| 348 |
+
return { width, height, pageCount: pages.length, draw }
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
// 轻度斜向平铺水印
|
| 352 |
+
function drawWatermark(ctx, w, h, text) {
|
| 353 |
+
const value = String(text || '').trim()
|
| 354 |
+
if (!value) return
|
| 355 |
+
const fontSize = Math.max(18, Math.round(w * 0.018))
|
| 356 |
+
const stepX = Math.max(fontSize * value.length * 1.6, w / 4)
|
| 357 |
+
const stepY = Math.max(fontSize * 6, w / 6)
|
| 358 |
+
ctx.save()
|
| 359 |
+
ctx.globalAlpha = 0.08
|
| 360 |
+
ctx.fillStyle = INK
|
| 361 |
+
ctx.font = `${fontSize}px sans-serif`
|
| 362 |
+
ctx.textAlign = 'center'
|
| 363 |
+
ctx.textBaseline = 'middle'
|
| 364 |
+
ctx.translate(w / 2, h / 2)
|
| 365 |
+
ctx.rotate(-Math.PI / 9)
|
| 366 |
+
const r = Math.hypot(w, h) / 2
|
| 367 |
+
for (let y = -r; y <= r; y += stepY) for (let x = -r; x <= r; x += stepX) ctx.fillText(value, x, y)
|
| 368 |
+
ctx.restore()
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
module.exports = {
|
| 372 |
+
BOARD,
|
| 373 |
+
drawBeads,
|
| 374 |
+
drawCodes,
|
| 375 |
+
drawGrid,
|
| 376 |
+
renderPreview,
|
| 377 |
+
patternPageLayout,
|
| 378 |
+
drawPatternPage,
|
| 379 |
+
buildInfoPages,
|
| 380 |
+
drawWatermark,
|
| 381 |
+
}
|
BeadQR/miniprogram/utils/pdf.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 轻量 PDF 生成器(无依赖)
|
| 2 |
+
// - 图纸页为纯矢量:珠子、色号、网格都是矢量指令,任意放大都清晰,文件小、生成快
|
| 3 |
+
// - 中文(页眉、信息页)以 JPEG 图片嵌入
|
| 4 |
+
// - 支持 1-bit 蒙版图片(用于矢量页上的半透明水印)
|
| 5 |
+
|
| 6 |
+
const A4 = { w: 595.28, h: 841.89 }
|
| 7 |
+
|
| 8 |
+
// Helvetica / Helvetica-Bold 字宽(AFM,1/1000 em),色号只含 A-Z 0-9
|
| 9 |
+
const W_BOLD = { ' ': 278, '-': 333, '/': 278 }
|
| 10 |
+
const W_REG = { ' ': 278, '-': 333, '/': 278 }
|
| 11 |
+
'0123456789'.split('').forEach((c) => { W_BOLD[c] = 556; W_REG[c] = 556 })
|
| 12 |
+
const UPPER_BOLD = [722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611]
|
| 13 |
+
const UPPER_REG = [667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611]
|
| 14 |
+
for (let i = 0; i < 26; i += 1) {
|
| 15 |
+
W_BOLD[String.fromCharCode(65 + i)] = UPPER_BOLD[i]
|
| 16 |
+
W_REG[String.fromCharCode(65 + i)] = UPPER_REG[i]
|
| 17 |
+
}
|
| 18 |
+
function textWidth(text, size, bold = true) {
|
| 19 |
+
const table = bold ? W_BOLD : W_REG
|
| 20 |
+
let w = 0
|
| 21 |
+
for (let i = 0; i < text.length; i += 1) w += table[text[i]] || 556
|
| 22 |
+
return (w / 1000) * size
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const n2 = (v) => (Math.round(v * 100) / 100).toString()
|
| 26 |
+
const esc = (s) => String(s).replace(/[\\()]/g, (m) => `\\${m}`).replace(/[^\x20-\x7e]/g, '?')
|
| 27 |
+
|
| 28 |
+
function ascii(text) {
|
| 29 |
+
const out = new Uint8Array(text.length)
|
| 30 |
+
for (let i = 0; i < text.length; i += 1) out[i] = text.charCodeAt(i) & 0xff
|
| 31 |
+
return out
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
class PdfDoc {
|
| 35 |
+
constructor() {
|
| 36 |
+
this.objects = [null, null, null] // 1: Catalog, 2: Pages(0 号占位)
|
| 37 |
+
this.pageIds = []
|
| 38 |
+
this.fontRegular = this.add(['<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'])
|
| 39 |
+
this.fontBold = this.add(['<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>'])
|
| 40 |
+
this.forms = {}
|
| 41 |
+
this.gstates = {}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
add(parts) {
|
| 45 |
+
this.objects.push(parts)
|
| 46 |
+
return this.objects.length - 1
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
stream(dict, data) {
|
| 50 |
+
const bytes = typeof data === 'string' ? ascii(data) : new Uint8Array(data)
|
| 51 |
+
return this.add([`<< ${dict} /Length ${bytes.length} >>\nstream\n`, bytes, '\nendstream'])
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
addJpeg(buffer, width, height) {
|
| 55 |
+
return this.stream(`/Type /XObject /Subtype /Image /Width ${width} /Height ${height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode`, buffer)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// bits:每行按字节对齐,0 = 绘制
|
| 59 |
+
addImageMask(bits, width, height) {
|
| 60 |
+
return this.stream(`/Type /XObject /Subtype /Image /Width ${width} /Height ${height} /ImageMask true /BitsPerComponent 1`, bits)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// 单位圆(直径 1)表单:不设颜色,继承外部填充色
|
| 64 |
+
form(kind) {
|
| 65 |
+
if (this.forms[kind]) return this.forms[kind]
|
| 66 |
+
const k = 0.5523
|
| 67 |
+
const circle = (r) => {
|
| 68 |
+
const c = r * k
|
| 69 |
+
return `${n2(0.5 + r)} 0.5 m ${n2(0.5 + r)} ${n2(0.5 + c)} ${n2(0.5 + c)} ${n2(0.5 + r)} 0.5 ${n2(0.5 + r)} c `
|
| 70 |
+
+ `${n2(0.5 - c)} ${n2(0.5 + r)} ${n2(0.5 - r)} ${n2(0.5 + c)} ${n2(0.5 - r)} 0.5 c `
|
| 71 |
+
+ `${n2(0.5 - r)} ${n2(0.5 - c)} ${n2(0.5 - c)} ${n2(0.5 - r)} 0.5 ${n2(0.5 - r)} c `
|
| 72 |
+
+ `${n2(0.5 + c)} ${n2(0.5 - r)} ${n2(0.5 + r)} ${n2(0.5 - c)} ${n2(0.5 + r)} 0.5 c h `
|
| 73 |
+
}
|
| 74 |
+
let body
|
| 75 |
+
if (kind === 'round') body = `${circle(0.46)}f`
|
| 76 |
+
else if (kind === 'hollow') body = `${circle(0.46)}${circle(0.2)}f*`
|
| 77 |
+
else body = `${circle(0.14)}f` // peg 空钉位
|
| 78 |
+
const id = this.stream('/Type /XObject /Subtype /Form /BBox [0 0 1 1]', body)
|
| 79 |
+
this.forms[kind] = id
|
| 80 |
+
return id
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
gstate(alpha) {
|
| 84 |
+
const key = String(alpha)
|
| 85 |
+
if (!this.gstates[key]) this.gstates[key] = this.add([`<< /Type /ExtGState /ca ${alpha} /CA ${alpha} >>`])
|
| 86 |
+
return this.gstates[key]
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
addPage(content, { images = {} } = {}) {
|
| 90 |
+
const contentId = this.stream('', content)
|
| 91 |
+
const xobjects = Object.assign({}, images)
|
| 92 |
+
Object.keys(this.forms).forEach((k) => { xobjects[`Fm_${k}`] = this.forms[k] })
|
| 93 |
+
const xo = Object.keys(xobjects).map((k) => `/${k} ${xobjects[k]} 0 R`).join(' ')
|
| 94 |
+
const gs = Object.keys(this.gstates).map((k) => `/GS${k.replace('.', '_')} ${this.gstates[k]} 0 R`).join(' ')
|
| 95 |
+
const id = this.add([`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${A4.w} ${A4.h}] /Resources << /ProcSet [/PDF /Text /ImageB /ImageC] /Font << /F1 ${this.fontRegular} 0 R /F2 ${this.fontBold} 0 R >> /XObject << ${xo} >> /ExtGState << ${gs} >> >> /Contents ${contentId} 0 R >>`])
|
| 96 |
+
this.pageIds.push(id)
|
| 97 |
+
return id
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
build() {
|
| 101 |
+
this.objects[1] = ['<< /Type /Catalog /Pages 2 0 R >>']
|
| 102 |
+
this.objects[2] = [`<< /Type /Pages /Kids [${this.pageIds.map((id) => `${id} 0 R`).join(' ')}] /Count ${this.pageIds.length} >>`]
|
| 103 |
+
const chunks = []
|
| 104 |
+
let pos = 0
|
| 105 |
+
const push = (part) => {
|
| 106 |
+
const bytes = typeof part === 'string' ? ascii(part) : part
|
| 107 |
+
chunks.push(bytes)
|
| 108 |
+
pos += bytes.length
|
| 109 |
+
}
|
| 110 |
+
push('%PDF-1.4\n%\xe2\xe3\xcf\xd3\n')
|
| 111 |
+
const offsets = []
|
| 112 |
+
for (let id = 1; id < this.objects.length; id += 1) {
|
| 113 |
+
offsets[id] = pos
|
| 114 |
+
push(`${id} 0 obj\n`)
|
| 115 |
+
this.objects[id].forEach(push)
|
| 116 |
+
push('\nendobj\n')
|
| 117 |
+
}
|
| 118 |
+
const xref = pos
|
| 119 |
+
let table = `xref\n0 ${this.objects.length}\n0000000000 65535 f \n`
|
| 120 |
+
for (let id = 1; id < this.objects.length; id += 1) table += `${String(offsets[id]).padStart(10, '0')} 00000 n \n`
|
| 121 |
+
push(table)
|
| 122 |
+
push(`trailer\n<< /Size ${this.objects.length} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`)
|
| 123 |
+
const out = new Uint8Array(pos)
|
| 124 |
+
let cursor = 0
|
| 125 |
+
chunks.forEach((c) => { out.set(c, cursor); cursor += c.length })
|
| 126 |
+
return out.buffer
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// —— 矢量图纸 ——
|
| 131 |
+
// 使用“左上角原点、y 向下”的坐标系(页面开头统一翻转),文字与图片局部再翻回来
|
| 132 |
+
|
| 133 |
+
function hexToPdf(hex) {
|
| 134 |
+
const v = parseInt(hex.slice(1), 16)
|
| 135 |
+
return `${n2(((v >> 16) & 255) / 255)} ${n2(((v >> 8) & 255) / 255)} ${n2((v & 255) / 255)}`
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
function lumaOf(hex) {
|
| 139 |
+
const v = parseInt(hex.slice(1), 16)
|
| 140 |
+
return (((v >> 16) & 255) * 0.299 + ((v >> 8) & 255) * 0.587 + (v & 255) * 0.114) / 255
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
function vectorSheet(doc, artwork, { x0, y0, cell, region, style, codes, grid }) {
|
| 144 |
+
const { size, cells, pal } = artwork
|
| 145 |
+
const out = []
|
| 146 |
+
out.push(`${hexToPdf('#f1eee7')} rg ${n2(x0)} ${n2(y0)} ${n2(region.cols * cell)} ${n2(region.rows * cell)} re f`)
|
| 147 |
+
const groups = new Map()
|
| 148 |
+
const empty = []
|
| 149 |
+
for (let y = region.row; y < region.row + region.rows; y += 1) {
|
| 150 |
+
for (let x = region.col; x < region.col + region.cols; x += 1) {
|
| 151 |
+
const c = cells[y * size + x]
|
| 152 |
+
const px = x0 + (x - region.col) * cell
|
| 153 |
+
const py = y0 + (y - region.row) * cell
|
| 154 |
+
if (c < 0) { empty.push([px, py]); continue }
|
| 155 |
+
if (!groups.has(c)) groups.set(c, [])
|
| 156 |
+
groups.get(c).push([px, py])
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
if (empty.length) {
|
| 160 |
+
doc.form('peg')
|
| 161 |
+
out.push(`${hexToPdf('#d9d3c7')} rg`)
|
| 162 |
+
empty.forEach(([x, y]) => out.push(`q ${n2(cell)} 0 0 ${n2(cell)} ${n2(x)} ${n2(y)} cm /Fm_peg Do Q`))
|
| 163 |
+
}
|
| 164 |
+
if (style !== 'square') doc.form(style)
|
| 165 |
+
groups.forEach((list, c) => {
|
| 166 |
+
out.push(`${hexToPdf(pal.hexes[c])} rg`)
|
| 167 |
+
if (style === 'square') {
|
| 168 |
+
// 略微外扩 0.05pt 消除阅读器抗锯齿造成的白缝
|
| 169 |
+
list.forEach(([x, y]) => out.push(`${n2(x - 0.05)} ${n2(y - 0.05)} ${n2(cell + 0.1)} ${n2(cell + 0.1)} re`))
|
| 170 |
+
out.push('f')
|
| 171 |
+
} else {
|
| 172 |
+
list.forEach(([x, y]) => out.push(`q ${n2(cell)} 0 0 ${n2(cell)} ${n2(x)} ${n2(y)} cm /Fm_${style} Do Q`))
|
| 173 |
+
}
|
| 174 |
+
})
|
| 175 |
+
if (codes) {
|
| 176 |
+
const fs = cell * (style === 'hollow' ? 0.27 : 0.34)
|
| 177 |
+
const writeGroup = (ink) => {
|
| 178 |
+
const lines = []
|
| 179 |
+
groups.forEach((list, c) => {
|
| 180 |
+
const useInk = style === 'hollow' || lumaOf(pal.hexes[c]) > 0.56
|
| 181 |
+
if (useInk !== ink) return
|
| 182 |
+
const id = pal.ids[c]
|
| 183 |
+
const tw = textWidth(id, fs)
|
| 184 |
+
list.forEach(([x, y]) => lines.push(`1 0 0 -1 ${n2(x + (cell - tw) / 2)} ${n2(y + cell / 2 + fs * 0.35)} Tm (${id}) Tj`))
|
| 185 |
+
})
|
| 186 |
+
if (!lines.length) return
|
| 187 |
+
out.push(`${ink ? hexToPdf('#1d2b2a') : '1 1 1'} rg BT /F2 ${n2(fs)} Tf`)
|
| 188 |
+
out.push(...lines)
|
| 189 |
+
out.push('ET')
|
| 190 |
+
}
|
| 191 |
+
writeGroup(true)
|
| 192 |
+
writeGroup(false)
|
| 193 |
+
}
|
| 194 |
+
if (grid && grid.enabled) {
|
| 195 |
+
const w = region.cols * cell
|
| 196 |
+
const h = region.rows * cell
|
| 197 |
+
const color = hexToPdf(grid.color)
|
| 198 |
+
const alpha = grid.opacity / 100
|
| 199 |
+
const thin = []
|
| 200 |
+
for (let i = 0; i <= region.cols; i += 1) thin.push(`${n2(x0 + i * cell)} ${n2(y0)} m ${n2(x0 + i * cell)} ${n2(y0 + h)} l`)
|
| 201 |
+
for (let i = 0; i <= region.rows; i += 1) thin.push(`${n2(x0)} ${n2(y0 + i * cell)} m ${n2(x0 + w)} ${n2(y0 + i * cell)} l`)
|
| 202 |
+
doc.gstate(Math.round(alpha * 30) / 100)
|
| 203 |
+
doc.gstate(alpha)
|
| 204 |
+
out.push(`q /GS${String(Math.round(alpha * 30) / 100).replace('.', '_')} gs ${color} RG ${n2(Math.max(0.2, cell * 0.02))} w`, ...thin, 'S Q')
|
| 205 |
+
const thick = []
|
| 206 |
+
for (let i = 0; i <= region.cols; i += 1) {
|
| 207 |
+
const g = region.col + i
|
| 208 |
+
if (g % grid.interval && i && i !== region.cols) continue
|
| 209 |
+
thick.push(`${n2(x0 + i * cell)} ${n2(y0)} m ${n2(x0 + i * cell)} ${n2(y0 + h)} l`)
|
| 210 |
+
}
|
| 211 |
+
for (let i = 0; i <= region.rows; i += 1) {
|
| 212 |
+
const g = region.row + i
|
| 213 |
+
if (g % grid.interval && i && i !== region.rows) continue
|
| 214 |
+
thick.push(`${n2(x0)} ${n2(y0 + i * cell)} m ${n2(x0 + w)} ${n2(y0 + i * cell)} l`)
|
| 215 |
+
}
|
| 216 |
+
out.push(`q /GS${String(alpha).replace('.', '_')} gs ${color} RG ${n2(Math.max(0.6, cell * 0.06))} w`, ...thick, 'S Q')
|
| 217 |
+
// 坐标数字
|
| 218 |
+
const fs = Math.min(7, Math.max(4, cell * 0.42))
|
| 219 |
+
const step = cell < 8 ? grid.interval : (cell < 13 ? 5 : 1)
|
| 220 |
+
const labels = []
|
| 221 |
+
for (let i = 0; i < region.cols; i += 1) {
|
| 222 |
+
const g = region.col + i + 1
|
| 223 |
+
if (g % step && g !== 1 && i !== 0) continue
|
| 224 |
+
const t = String(g)
|
| 225 |
+
labels.push(`1 0 0 -1 ${n2(x0 + (i + 0.5) * cell - textWidth(t, fs, false) / 2)} ${n2(y0 - fs * 0.5)} Tm (${t}) Tj`)
|
| 226 |
+
}
|
| 227 |
+
for (let i = 0; i < region.rows; i += 1) {
|
| 228 |
+
const g = region.row + i + 1
|
| 229 |
+
if (g % step && g !== 1 && i !== 0) continue
|
| 230 |
+
const t = String(g)
|
| 231 |
+
labels.push(`1 0 0 -1 ${n2(x0 - textWidth(t, fs, false) - fs * 0.4)} ${n2(y0 + (i + 0.5) * cell + fs * 0.35)} Tm (${t}) Tj`)
|
| 232 |
+
}
|
| 233 |
+
out.push(`${color} rg BT /F1 ${n2(fs)} Tf`, ...labels, 'ET')
|
| 234 |
+
}
|
| 235 |
+
return out.join('\n')
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// 页眉图片 + 水印蒙版的放置
|
| 239 |
+
function imageCmd(name, x, y, w, h) {
|
| 240 |
+
return `q ${n2(w)} 0 0 ${n2(-h)} ${n2(x)} ${n2(y + h)} cm /${name} Do Q`
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
function watermarkCmd(mask) {
|
| 244 |
+
if (!mask) return ''
|
| 245 |
+
const h = 16
|
| 246 |
+
const w = h * mask.width / mask.height
|
| 247 |
+
const out = [`q /GS0_08 gs ${hexToPdf('#1d2b2a')} rg`]
|
| 248 |
+
const stepX = w + 90
|
| 249 |
+
const stepY = 120
|
| 250 |
+
for (let row = 0, y = -200; y < A4.h + 200; y += stepY, row += 1) {
|
| 251 |
+
for (let x = -200; x < A4.w + 200; x += stepX) {
|
| 252 |
+
// 旋转 -20°:cos=0.94 sin=0.342;y 轴已翻转,所以蒙版局部再翻回
|
| 253 |
+
out.push(`q 0.94 -0.342 0.342 0.94 ${n2(x + (row % 2) * stepX / 2)} ${n2(y)} cm ${n2(w)} 0 0 ${n2(-h)} 0 ${n2(h)} cm /WM Do Q`)
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
out.push('Q')
|
| 257 |
+
return out.join('\n')
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
const MARGIN = 28.35 // 10mm
|
| 261 |
+
const HEADER_H = 34
|
| 262 |
+
|
| 263 |
+
const AXIS = 14
|
| 264 |
+
const TARGET_CELL = 18 // 约 6.4mm/格,打印后色号清晰可读
|
| 265 |
+
const AREA_W = A4.w - MARGIN * 2
|
| 266 |
+
const AREA_H = A4.h - MARGIN * 2 - HEADER_H
|
| 267 |
+
|
| 268 |
+
/**
|
| 269 |
+
* 规划图纸页:小图一页搞定;大图 = 总览页 + 若干分块页
|
| 270 |
+
* 返回 { tiled, fitCell, tilesX, tilesY, tileCols, tileRows, pages: [{ kind, tile, region }] }
|
| 271 |
+
*/
|
| 272 |
+
function planPatternPages(size) {
|
| 273 |
+
const fitCell = Math.min((AREA_W - AXIS) / size, (AREA_H - AXIS) / size)
|
| 274 |
+
const tiled = fitCell < 11
|
| 275 |
+
const full = { col: 0, row: 0, cols: size, rows: size }
|
| 276 |
+
const pages = [{ kind: 'overview', tile: 0, region: full }]
|
| 277 |
+
let tilesX = 1
|
| 278 |
+
let tilesY = 1
|
| 279 |
+
let tileCols = size
|
| 280 |
+
let tileRows = size
|
| 281 |
+
if (tiled) {
|
| 282 |
+
tilesX = Math.ceil(size / Math.floor((AREA_W - AXIS) / TARGET_CELL))
|
| 283 |
+
tilesY = Math.ceil(size / Math.floor((AREA_H - AXIS) / TARGET_CELL))
|
| 284 |
+
tileCols = Math.ceil(size / tilesX)
|
| 285 |
+
tileRows = Math.ceil(size / tilesY)
|
| 286 |
+
let n = 1
|
| 287 |
+
for (let ty = 0; ty < tilesY; ty += 1) {
|
| 288 |
+
for (let tx = 0; tx < tilesX; tx += 1) {
|
| 289 |
+
const col = tx * tileCols
|
| 290 |
+
const row = ty * tileRows
|
| 291 |
+
if (col >= size || row >= size) continue
|
| 292 |
+
pages.push({ kind: 'tile', tile: n, region: { col, row, cols: Math.min(tileCols, size - col), rows: Math.min(tileRows, size - row) } })
|
| 293 |
+
n += 1
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
return { tiled, fitCell, tilesX, tilesY, tileCols, tileRows, pages }
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
/**
|
| 301 |
+
* 生成图纸页(矢量)。
|
| 302 |
+
* opts: { plan, headers: [{ id, width, height }](按页序), style, grid, watermark }
|
| 303 |
+
*/
|
| 304 |
+
function addPatternPages(doc, artwork, opts) {
|
| 305 |
+
const size = artwork.size
|
| 306 |
+
const plan = opts.plan || planPatternPages(size)
|
| 307 |
+
const headers = opts.headers || []
|
| 308 |
+
const flip = `1 0 0 -1 0 ${A4.h} cm`
|
| 309 |
+
const wm = opts.watermark ? watermarkCmd(opts.watermark) : ''
|
| 310 |
+
if (opts.watermark) doc.gstate(0.08)
|
| 311 |
+
if (plan.tiled) doc.gstate(0.85)
|
| 312 |
+
const images = (header) => {
|
| 313 |
+
const map = {}
|
| 314 |
+
if (header) map.HD = header.id
|
| 315 |
+
if (opts.watermark) map.WM = opts.watermark.id
|
| 316 |
+
return map
|
| 317 |
+
}
|
| 318 |
+
const headerCmd = (header) => (header ? imageCmd('HD', MARGIN, MARGIN, AREA_W, AREA_W * header.height / header.width) : '')
|
| 319 |
+
|
| 320 |
+
plan.pages.forEach((page, i) => {
|
| 321 |
+
const header = headers[i] || null
|
| 322 |
+
const content = [flip, headerCmd(header)]
|
| 323 |
+
if (page.kind === 'overview') {
|
| 324 |
+
const fitCell = plan.fitCell
|
| 325 |
+
const gridW = fitCell * size
|
| 326 |
+
const x0 = MARGIN + AXIS + (AREA_W - AXIS - gridW) / 2
|
| 327 |
+
const y0 = MARGIN + HEADER_H + AXIS
|
| 328 |
+
content.push(vectorSheet(doc, artwork, {
|
| 329 |
+
x0, y0, cell: fitCell, region: page.region, style: opts.style, codes: !plan.tiled, grid: opts.grid,
|
| 330 |
+
}))
|
| 331 |
+
if (plan.tiled) {
|
| 332 |
+
const lines = [`q ${hexToPdf('#e11d48')} RG 1.2 w`]
|
| 333 |
+
const labels = []
|
| 334 |
+
plan.pages.forEach((tp) => {
|
| 335 |
+
if (tp.kind !== 'tile') return
|
| 336 |
+
const r = tp.region
|
| 337 |
+
const cx = x0 + r.col * fitCell
|
| 338 |
+
const cy = y0 + r.row * fitCell
|
| 339 |
+
const cw = r.cols * fitCell
|
| 340 |
+
const ch = r.rows * fitCell
|
| 341 |
+
lines.push(`${n2(cx)} ${n2(cy)} ${n2(cw)} ${n2(ch)} re S`)
|
| 342 |
+
const t = String(tp.tile)
|
| 343 |
+
labels.push(`1 0 0 -1 ${n2(cx + cw / 2 - textWidth(t, 22) / 2)} ${n2(cy + ch / 2 + 8)} Tm (${t}) Tj`)
|
| 344 |
+
})
|
| 345 |
+
lines.push('Q')
|
| 346 |
+
content.push(lines.join('\n'))
|
| 347 |
+
content.push(`q /GS0_85 gs ${hexToPdf('#e11d48')} rg BT /F2 22 Tf\n${labels.join('\n')}\nET Q`)
|
| 348 |
+
}
|
| 349 |
+
} else {
|
| 350 |
+
const cell = Math.min((AREA_W - AXIS) / plan.tileCols, (AREA_H - AXIS) / plan.tileRows)
|
| 351 |
+
content.push(vectorSheet(doc, artwork, {
|
| 352 |
+
x0: MARGIN + AXIS, y0: MARGIN + HEADER_H + AXIS, cell, region: page.region, style: opts.style, codes: true, grid: opts.grid,
|
| 353 |
+
}))
|
| 354 |
+
}
|
| 355 |
+
content.push(wm)
|
| 356 |
+
doc.addPage(content.join('\n'), { images: images(header) })
|
| 357 |
+
})
|
| 358 |
+
return plan.pages.length
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
// 整页图片(信息页)
|
| 362 |
+
function addImagePage(doc, jpegId, pixelW, pixelH) {
|
| 363 |
+
const areaW = A4.w - MARGIN * 2
|
| 364 |
+
const areaH = A4.h - MARGIN * 2
|
| 365 |
+
const scale = Math.min(areaW / pixelW, areaH / pixelH)
|
| 366 |
+
const w = pixelW * scale
|
| 367 |
+
const h = pixelH * scale
|
| 368 |
+
const x = (A4.w - w) / 2
|
| 369 |
+
const y = (A4.h - h) / 2
|
| 370 |
+
doc.addPage(`1 0 0 -1 0 ${A4.h} cm\n${imageCmd('IM', x, y, w, h)}`, { images: { IM: jpegId } })
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
module.exports = { A4, PdfDoc, planPatternPages, addPatternPages, addImagePage, vectorSheet, textWidth, HEADER_H, MARGIN }
|
BeadQR/miniprogram/utils/qr.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 二维码生成、功能区识别与摆放
|
| 2 |
+
const QRCodeCore = require('./qrcore/qrcode')
|
| 3 |
+
const AlignmentPattern = require('./qrcore/alignment-pattern')
|
| 4 |
+
|
| 5 |
+
const SAMPLE_TEXT = '豆维·可扫码的拼豆'
|
| 6 |
+
|
| 7 |
+
// 功能区(定位/时序/对齐/格式/版本)直接决定能否扫出,配色时强制高对比
|
| 8 |
+
function buildFunctionMask(size, version) {
|
| 9 |
+
const mask = new Uint8Array(size * size)
|
| 10 |
+
const mark = (x, y) => { if (x >= 0 && y >= 0 && x < size && y < size) mask[y * size + x] = 1 }
|
| 11 |
+
for (let y = 0; y < size; y += 1) {
|
| 12 |
+
for (let x = 0; x < size; x += 1) {
|
| 13 |
+
const finder = (x <= 8 && y <= 8) || (x >= size - 8 && y <= 8) || (x <= 8 && y >= size - 8)
|
| 14 |
+
if (finder || x === 6 || y === 6) mark(x, y)
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
const positions = AlignmentPattern.getPositions(version) || []
|
| 18 |
+
positions.forEach(([row, col]) => {
|
| 19 |
+
for (let dy = -2; dy <= 2; dy += 1) for (let dx = -2; dx <= 2; dx += 1) mark(col + dx, row + dy)
|
| 20 |
+
})
|
| 21 |
+
if (version >= 7) {
|
| 22 |
+
for (let i = 0; i < 6; i += 1) {
|
| 23 |
+
for (let j = size - 11; j < size - 8; j += 1) { mark(i, j); mark(j, i) }
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
return mask
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
function makeQr(text, level) {
|
| 30 |
+
const value = String(text || '').trim() || SAMPLE_TEXT
|
| 31 |
+
const qr = QRCodeCore.create(value, { errorCorrectionLevel: level || 'H' })
|
| 32 |
+
const size = qr.modules.size
|
| 33 |
+
const version = qr.version || Math.round((size - 17) / 4)
|
| 34 |
+
return {
|
| 35 |
+
size,
|
| 36 |
+
version,
|
| 37 |
+
level: level || 'H',
|
| 38 |
+
dark: Uint8Array.from(qr.modules.data),
|
| 39 |
+
func: buildFunctionMask(size, version),
|
| 40 |
+
isSample: !String(text || '').trim(),
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// scale:二维码占整张作品边长的百分比;quiet:二维码四周留白格数
|
| 45 |
+
function createPlacement(qrSize, { scale = 100, posX = 50, posY = 50, quiet = 0, hasImage = true } = {}) {
|
| 46 |
+
const block = qrSize + quiet * 2
|
| 47 |
+
const size = hasImage ? Math.max(block, Math.ceil(block * 100 / scale)) : block
|
| 48 |
+
const free = size - block
|
| 49 |
+
return {
|
| 50 |
+
size,
|
| 51 |
+
left: quiet + Math.round(free * posX / 100),
|
| 52 |
+
top: quiet + Math.round(free * posY / 100),
|
| 53 |
+
quiet,
|
| 54 |
+
scale: hasImage ? scale : 100,
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
module.exports = { SAMPLE_TEXT, makeQr, createPlacement }
|
BeadQR/miniprogram/utils/qrcore/alignment-pattern.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Alignment pattern are fixed reference pattern in defined positions
|
| 3 |
+
* in a matrix symbology, which enables the decode software to re-synchronise
|
| 4 |
+
* the coordinate mapping of the image modules in the event of moderate amounts
|
| 5 |
+
* of distortion of the image.
|
| 6 |
+
*
|
| 7 |
+
* Alignment patterns are present only in QR Code symbols of version 2 or larger
|
| 8 |
+
* and their number depends on the symbol version.
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
const getSymbolSize = require('./utils').getSymbolSize
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Calculate the row/column coordinates of the center module of each alignment pattern
|
| 15 |
+
* for the specified QR Code version.
|
| 16 |
+
*
|
| 17 |
+
* The alignment patterns are positioned symmetrically on either side of the diagonal
|
| 18 |
+
* running from the top left corner of the symbol to the bottom right corner.
|
| 19 |
+
*
|
| 20 |
+
* Since positions are simmetrical only half of the coordinates are returned.
|
| 21 |
+
* Each item of the array will represent in turn the x and y coordinate.
|
| 22 |
+
* @see {@link getPositions}
|
| 23 |
+
*
|
| 24 |
+
* @param {Number} version QR Code version
|
| 25 |
+
* @return {Array} Array of coordinate
|
| 26 |
+
*/
|
| 27 |
+
exports.getRowColCoords = function getRowColCoords (version) {
|
| 28 |
+
if (version === 1) return []
|
| 29 |
+
|
| 30 |
+
const posCount = Math.floor(version / 7) + 2
|
| 31 |
+
const size = getSymbolSize(version)
|
| 32 |
+
const intervals = size === 145 ? 26 : Math.ceil((size - 13) / (2 * posCount - 2)) * 2
|
| 33 |
+
const positions = [size - 7] // Last coord is always (size - 7)
|
| 34 |
+
|
| 35 |
+
for (let i = 1; i < posCount - 1; i++) {
|
| 36 |
+
positions[i] = positions[i - 1] - intervals
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
positions.push(6) // First coord is always 6
|
| 40 |
+
|
| 41 |
+
return positions.reverse()
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Returns an array containing the positions of each alignment pattern.
|
| 46 |
+
* Each array's element represent the center point of the pattern as (x, y) coordinates
|
| 47 |
+
*
|
| 48 |
+
* Coordinates are calculated expanding the row/column coordinates returned by {@link getRowColCoords}
|
| 49 |
+
* and filtering out the items that overlaps with finder pattern
|
| 50 |
+
*
|
| 51 |
+
* @example
|
| 52 |
+
* For a Version 7 symbol {@link getRowColCoords} returns values 6, 22 and 38.
|
| 53 |
+
* The alignment patterns, therefore, are to be centered on (row, column)
|
| 54 |
+
* positions (6,22), (22,6), (22,22), (22,38), (38,22), (38,38).
|
| 55 |
+
* Note that the coordinates (6,6), (6,38), (38,6) are occupied by finder patterns
|
| 56 |
+
* and are not therefore used for alignment patterns.
|
| 57 |
+
*
|
| 58 |
+
* let pos = getPositions(7)
|
| 59 |
+
* // [[6,22], [22,6], [22,22], [22,38], [38,22], [38,38]]
|
| 60 |
+
*
|
| 61 |
+
* @param {Number} version QR Code version
|
| 62 |
+
* @return {Array} Array of coordinates
|
| 63 |
+
*/
|
| 64 |
+
exports.getPositions = function getPositions (version) {
|
| 65 |
+
const coords = []
|
| 66 |
+
const pos = exports.getRowColCoords(version)
|
| 67 |
+
const posLength = pos.length
|
| 68 |
+
|
| 69 |
+
for (let i = 0; i < posLength; i++) {
|
| 70 |
+
for (let j = 0; j < posLength; j++) {
|
| 71 |
+
// Skip if position is occupied by finder patterns
|
| 72 |
+
if ((i === 0 && j === 0) || // top-left
|
| 73 |
+
(i === 0 && j === posLength - 1) || // bottom-left
|
| 74 |
+
(i === posLength - 1 && j === 0)) { // top-right
|
| 75 |
+
continue
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
coords.push([pos[i], pos[j]])
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return coords
|
| 83 |
+
}
|
BeadQR/miniprogram/utils/qrcore/alphanumeric-data.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Mode = require('./mode')
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Array of characters available in alphanumeric mode
|
| 5 |
+
*
|
| 6 |
+
* As per QR Code specification, to each character
|
| 7 |
+
* is assigned a value from 0 to 44 which in this case coincides
|
| 8 |
+
* with the array index
|
| 9 |
+
*
|
| 10 |
+
* @type {Array}
|
| 11 |
+
*/
|
| 12 |
+
const ALPHA_NUM_CHARS = [
|
| 13 |
+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
| 14 |
+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
| 15 |
+
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
| 16 |
+
' ', '$', '%', '*', '+', '-', '.', '/', ':'
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
function AlphanumericData (data) {
|
| 20 |
+
this.mode = Mode.ALPHANUMERIC
|
| 21 |
+
this.data = data
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
AlphanumericData.getBitsLength = function getBitsLength (length) {
|
| 25 |
+
return 11 * Math.floor(length / 2) + 6 * (length % 2)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
AlphanumericData.prototype.getLength = function getLength () {
|
| 29 |
+
return this.data.length
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
AlphanumericData.prototype.getBitsLength = function getBitsLength () {
|
| 33 |
+
return AlphanumericData.getBitsLength(this.data.length)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
AlphanumericData.prototype.write = function write (bitBuffer) {
|
| 37 |
+
let i
|
| 38 |
+
|
| 39 |
+
// Input data characters are divided into groups of two characters
|
| 40 |
+
// and encoded as 11-bit binary codes.
|
| 41 |
+
for (i = 0; i + 2 <= this.data.length; i += 2) {
|
| 42 |
+
// The character value of the first character is multiplied by 45
|
| 43 |
+
let value = ALPHA_NUM_CHARS.indexOf(this.data[i]) * 45
|
| 44 |
+
|
| 45 |
+
// The character value of the second digit is added to the product
|
| 46 |
+
value += ALPHA_NUM_CHARS.indexOf(this.data[i + 1])
|
| 47 |
+
|
| 48 |
+
// The sum is then stored as 11-bit binary number
|
| 49 |
+
bitBuffer.put(value, 11)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// If the number of input data characters is not a multiple of two,
|
| 53 |
+
// the character value of the final character is encoded as a 6-bit binary number.
|
| 54 |
+
if (this.data.length % 2) {
|
| 55 |
+
bitBuffer.put(ALPHA_NUM_CHARS.indexOf(this.data[i]), 6)
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
module.exports = AlphanumericData
|
BeadQR/miniprogram/utils/qrcore/bit-buffer.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function BitBuffer () {
|
| 2 |
+
this.buffer = []
|
| 3 |
+
this.length = 0
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
BitBuffer.prototype = {
|
| 7 |
+
|
| 8 |
+
get: function (index) {
|
| 9 |
+
const bufIndex = Math.floor(index / 8)
|
| 10 |
+
return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) === 1
|
| 11 |
+
},
|
| 12 |
+
|
| 13 |
+
put: function (num, length) {
|
| 14 |
+
for (let i = 0; i < length; i++) {
|
| 15 |
+
this.putBit(((num >>> (length - i - 1)) & 1) === 1)
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
|
| 19 |
+
getLengthInBits: function () {
|
| 20 |
+
return this.length
|
| 21 |
+
},
|
| 22 |
+
|
| 23 |
+
putBit: function (bit) {
|
| 24 |
+
const bufIndex = Math.floor(this.length / 8)
|
| 25 |
+
if (this.buffer.length <= bufIndex) {
|
| 26 |
+
this.buffer.push(0)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
if (bit) {
|
| 30 |
+
this.buffer[bufIndex] |= (0x80 >>> (this.length % 8))
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
this.length++
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
module.exports = BitBuffer
|
BeadQR/miniprogram/utils/qrcore/bit-matrix.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Helper class to handle QR Code symbol modules
|
| 3 |
+
*
|
| 4 |
+
* @param {Number} size Symbol size
|
| 5 |
+
*/
|
| 6 |
+
function BitMatrix (size) {
|
| 7 |
+
if (!size || size < 1) {
|
| 8 |
+
throw new Error('BitMatrix size must be defined and greater than 0')
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
this.size = size
|
| 12 |
+
this.data = new Uint8Array(size * size)
|
| 13 |
+
this.reservedBit = new Uint8Array(size * size)
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Set bit value at specified location
|
| 18 |
+
* If reserved flag is set, this bit will be ignored during masking process
|
| 19 |
+
*
|
| 20 |
+
* @param {Number} row
|
| 21 |
+
* @param {Number} col
|
| 22 |
+
* @param {Boolean} value
|
| 23 |
+
* @param {Boolean} reserved
|
| 24 |
+
*/
|
| 25 |
+
BitMatrix.prototype.set = function (row, col, value, reserved) {
|
| 26 |
+
const index = row * this.size + col
|
| 27 |
+
this.data[index] = value
|
| 28 |
+
if (reserved) this.reservedBit[index] = true
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Returns bit value at specified location
|
| 33 |
+
*
|
| 34 |
+
* @param {Number} row
|
| 35 |
+
* @param {Number} col
|
| 36 |
+
* @return {Boolean}
|
| 37 |
+
*/
|
| 38 |
+
BitMatrix.prototype.get = function (row, col) {
|
| 39 |
+
return this.data[row * this.size + col]
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Applies xor operator at specified location
|
| 44 |
+
* (used during masking process)
|
| 45 |
+
*
|
| 46 |
+
* @param {Number} row
|
| 47 |
+
* @param {Number} col
|
| 48 |
+
* @param {Boolean} value
|
| 49 |
+
*/
|
| 50 |
+
BitMatrix.prototype.xor = function (row, col, value) {
|
| 51 |
+
this.data[row * this.size + col] ^= value
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Check if bit at specified location is reserved
|
| 56 |
+
*
|
| 57 |
+
* @param {Number} row
|
| 58 |
+
* @param {Number} col
|
| 59 |
+
* @return {Boolean}
|
| 60 |
+
*/
|
| 61 |
+
BitMatrix.prototype.isReserved = function (row, col) {
|
| 62 |
+
return this.reservedBit[row * this.size + col]
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
module.exports = BitMatrix
|
BeadQR/miniprogram/utils/qrcore/byte-data.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Mode = require('./mode')
|
| 2 |
+
|
| 3 |
+
// 微信部分真机运行时没有 TextEncoder。二维码 BYTE 段只需要 UTF-8 字节,
|
| 4 |
+
// 在此实现最小兼容编码器,且与 TextEncoder 对无效代理对的处理一致。
|
| 5 |
+
function encodeUtf8 (value) {
|
| 6 |
+
const bytes = []
|
| 7 |
+
for (let index = 0; index < value.length; index += 1) {
|
| 8 |
+
let codePoint = value.charCodeAt(index)
|
| 9 |
+
if (codePoint >= 0xd800 && codePoint <= 0xdbff) {
|
| 10 |
+
const next = value.charCodeAt(index + 1)
|
| 11 |
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
| 12 |
+
codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00)
|
| 13 |
+
index += 1
|
| 14 |
+
} else {
|
| 15 |
+
codePoint = 0xfffd
|
| 16 |
+
}
|
| 17 |
+
} else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) {
|
| 18 |
+
codePoint = 0xfffd
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if (codePoint <= 0x7f) {
|
| 22 |
+
bytes.push(codePoint)
|
| 23 |
+
} else if (codePoint <= 0x7ff) {
|
| 24 |
+
bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f))
|
| 25 |
+
} else if (codePoint <= 0xffff) {
|
| 26 |
+
bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
|
| 27 |
+
} else {
|
| 28 |
+
bytes.push(0xf0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3f), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
return new Uint8Array(bytes)
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function ByteData (data) {
|
| 35 |
+
this.mode = Mode.BYTE
|
| 36 |
+
if (typeof (data) === 'string') {
|
| 37 |
+
this.data = encodeUtf8(data)
|
| 38 |
+
} else {
|
| 39 |
+
this.data = new Uint8Array(data)
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
ByteData.getBitsLength = function getBitsLength (length) {
|
| 44 |
+
return length * 8
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
ByteData.prototype.getLength = function getLength () {
|
| 48 |
+
return this.data.length
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
ByteData.prototype.getBitsLength = function getBitsLength () {
|
| 52 |
+
return ByteData.getBitsLength(this.data.length)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
ByteData.prototype.write = function (bitBuffer) {
|
| 56 |
+
for (let i = 0, l = this.data.length; i < l; i++) {
|
| 57 |
+
bitBuffer.put(this.data[i], 8)
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
module.exports = ByteData
|
BeadQR/miniprogram/utils/qrcore/dijkstrajs.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict';
|
| 2 |
+
|
| 3 |
+
/******************************************************************************
|
| 4 |
+
* Created 2008-08-19.
|
| 5 |
+
*
|
| 6 |
+
* Dijkstra path-finding functions. Adapted from the Dijkstar Python project.
|
| 7 |
+
*
|
| 8 |
+
* Copyright (C) 2008
|
| 9 |
+
* Wyatt Baldwin <self@wyattbaldwin.com>
|
| 10 |
+
* All rights reserved
|
| 11 |
+
*
|
| 12 |
+
* Licensed under the MIT license.
|
| 13 |
+
*
|
| 14 |
+
* http://www.opensource.org/licenses/mit-license.php
|
| 15 |
+
*
|
| 16 |
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 17 |
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 18 |
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 19 |
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 20 |
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 21 |
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 22 |
+
* THE SOFTWARE.
|
| 23 |
+
*****************************************************************************/
|
| 24 |
+
var dijkstra = {
|
| 25 |
+
single_source_shortest_paths: function(graph, s, d) {
|
| 26 |
+
// Predecessor map for each node that has been encountered.
|
| 27 |
+
// node ID => predecessor node ID
|
| 28 |
+
var predecessors = {};
|
| 29 |
+
|
| 30 |
+
// Costs of shortest paths from s to all nodes encountered.
|
| 31 |
+
// node ID => cost
|
| 32 |
+
var costs = {};
|
| 33 |
+
costs[s] = 0;
|
| 34 |
+
|
| 35 |
+
// Costs of shortest paths from s to all nodes encountered; differs from
|
| 36 |
+
// `costs` in that it provides easy access to the node that currently has
|
| 37 |
+
// the known shortest path from s.
|
| 38 |
+
// XXX: Do we actually need both `costs` and `open`?
|
| 39 |
+
var open = dijkstra.PriorityQueue.make();
|
| 40 |
+
open.push(s, 0);
|
| 41 |
+
|
| 42 |
+
var closest,
|
| 43 |
+
u, v,
|
| 44 |
+
cost_of_s_to_u,
|
| 45 |
+
adjacent_nodes,
|
| 46 |
+
cost_of_e,
|
| 47 |
+
cost_of_s_to_u_plus_cost_of_e,
|
| 48 |
+
cost_of_s_to_v,
|
| 49 |
+
first_visit;
|
| 50 |
+
while (!open.empty()) {
|
| 51 |
+
// In the nodes remaining in graph that have a known cost from s,
|
| 52 |
+
// find the node, u, that currently has the shortest path from s.
|
| 53 |
+
closest = open.pop();
|
| 54 |
+
u = closest.value;
|
| 55 |
+
cost_of_s_to_u = closest.cost;
|
| 56 |
+
|
| 57 |
+
// Get nodes adjacent to u...
|
| 58 |
+
adjacent_nodes = graph[u] || {};
|
| 59 |
+
|
| 60 |
+
// ...and explore the edges that connect u to those nodes, updating
|
| 61 |
+
// the cost of the shortest paths to any or all of those nodes as
|
| 62 |
+
// necessary. v is the node across the current edge from u.
|
| 63 |
+
for (v in adjacent_nodes) {
|
| 64 |
+
if (adjacent_nodes.hasOwnProperty(v)) {
|
| 65 |
+
// Get the cost of the edge running from u to v.
|
| 66 |
+
cost_of_e = adjacent_nodes[v];
|
| 67 |
+
|
| 68 |
+
// Cost of s to u plus the cost of u to v across e--this is *a*
|
| 69 |
+
// cost from s to v that may or may not be less than the current
|
| 70 |
+
// known cost to v.
|
| 71 |
+
cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e;
|
| 72 |
+
|
| 73 |
+
// If we haven't visited v yet OR if the current known cost from s to
|
| 74 |
+
// v is greater than the new cost we just found (cost of s to u plus
|
| 75 |
+
// cost of u to v across e), update v's cost in the cost list and
|
| 76 |
+
// update v's predecessor in the predecessor list (it's now u).
|
| 77 |
+
cost_of_s_to_v = costs[v];
|
| 78 |
+
first_visit = (typeof costs[v] === 'undefined');
|
| 79 |
+
if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
|
| 80 |
+
costs[v] = cost_of_s_to_u_plus_cost_of_e;
|
| 81 |
+
open.push(v, cost_of_s_to_u_plus_cost_of_e);
|
| 82 |
+
predecessors[v] = u;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if (typeof d !== 'undefined' && typeof costs[d] === 'undefined') {
|
| 89 |
+
var msg = ['Could not find a path from ', s, ' to ', d, '.'].join('');
|
| 90 |
+
throw new Error(msg);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
return predecessors;
|
| 94 |
+
},
|
| 95 |
+
|
| 96 |
+
extract_shortest_path_from_predecessor_list: function(predecessors, d) {
|
| 97 |
+
var nodes = [];
|
| 98 |
+
var u = d;
|
| 99 |
+
var predecessor;
|
| 100 |
+
while (u) {
|
| 101 |
+
nodes.push(u);
|
| 102 |
+
predecessor = predecessors[u];
|
| 103 |
+
u = predecessors[u];
|
| 104 |
+
}
|
| 105 |
+
nodes.reverse();
|
| 106 |
+
return nodes;
|
| 107 |
+
},
|
| 108 |
+
|
| 109 |
+
find_path: function(graph, s, d) {
|
| 110 |
+
var predecessors = dijkstra.single_source_shortest_paths(graph, s, d);
|
| 111 |
+
return dijkstra.extract_shortest_path_from_predecessor_list(
|
| 112 |
+
predecessors, d);
|
| 113 |
+
},
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* A very naive priority queue implementation.
|
| 117 |
+
*/
|
| 118 |
+
PriorityQueue: {
|
| 119 |
+
make: function (opts) {
|
| 120 |
+
var T = dijkstra.PriorityQueue,
|
| 121 |
+
t = {},
|
| 122 |
+
key;
|
| 123 |
+
opts = opts || {};
|
| 124 |
+
for (key in T) {
|
| 125 |
+
if (T.hasOwnProperty(key)) {
|
| 126 |
+
t[key] = T[key];
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
t.queue = [];
|
| 130 |
+
t.sorter = opts.sorter || T.default_sorter;
|
| 131 |
+
return t;
|
| 132 |
+
},
|
| 133 |
+
|
| 134 |
+
default_sorter: function (a, b) {
|
| 135 |
+
return a.cost - b.cost;
|
| 136 |
+
},
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Add a new item to the queue and ensure the highest priority element
|
| 140 |
+
* is at the front of the queue.
|
| 141 |
+
*/
|
| 142 |
+
push: function (value, cost) {
|
| 143 |
+
var item = {value: value, cost: cost};
|
| 144 |
+
this.queue.push(item);
|
| 145 |
+
this.queue.sort(this.sorter);
|
| 146 |
+
},
|
| 147 |
+
|
| 148 |
+
/**
|
| 149 |
+
* Return the highest priority element in the queue.
|
| 150 |
+
*/
|
| 151 |
+
pop: function () {
|
| 152 |
+
return this.queue.shift();
|
| 153 |
+
},
|
| 154 |
+
|
| 155 |
+
empty: function () {
|
| 156 |
+
return this.queue.length === 0;
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
// node.js module exports
|
| 163 |
+
if (typeof module !== 'undefined') {
|
| 164 |
+
module.exports = dijkstra;
|
| 165 |
+
}
|
BeadQR/miniprogram/utils/qrcore/error-correction-code.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const ECLevel = require('./error-correction-level')
|
| 2 |
+
|
| 3 |
+
const EC_BLOCKS_TABLE = [
|
| 4 |
+
// L M Q H
|
| 5 |
+
1, 1, 1, 1,
|
| 6 |
+
1, 1, 1, 1,
|
| 7 |
+
1, 1, 2, 2,
|
| 8 |
+
1, 2, 2, 4,
|
| 9 |
+
1, 2, 4, 4,
|
| 10 |
+
2, 4, 4, 4,
|
| 11 |
+
2, 4, 6, 5,
|
| 12 |
+
2, 4, 6, 6,
|
| 13 |
+
2, 5, 8, 8,
|
| 14 |
+
4, 5, 8, 8,
|
| 15 |
+
4, 5, 8, 11,
|
| 16 |
+
4, 8, 10, 11,
|
| 17 |
+
4, 9, 12, 16,
|
| 18 |
+
4, 9, 16, 16,
|
| 19 |
+
6, 10, 12, 18,
|
| 20 |
+
6, 10, 17, 16,
|
| 21 |
+
6, 11, 16, 19,
|
| 22 |
+
6, 13, 18, 21,
|
| 23 |
+
7, 14, 21, 25,
|
| 24 |
+
8, 16, 20, 25,
|
| 25 |
+
8, 17, 23, 25,
|
| 26 |
+
9, 17, 23, 34,
|
| 27 |
+
9, 18, 25, 30,
|
| 28 |
+
10, 20, 27, 32,
|
| 29 |
+
12, 21, 29, 35,
|
| 30 |
+
12, 23, 34, 37,
|
| 31 |
+
12, 25, 34, 40,
|
| 32 |
+
13, 26, 35, 42,
|
| 33 |
+
14, 28, 38, 45,
|
| 34 |
+
15, 29, 40, 48,
|
| 35 |
+
16, 31, 43, 51,
|
| 36 |
+
17, 33, 45, 54,
|
| 37 |
+
18, 35, 48, 57,
|
| 38 |
+
19, 37, 51, 60,
|
| 39 |
+
19, 38, 53, 63,
|
| 40 |
+
20, 40, 56, 66,
|
| 41 |
+
21, 43, 59, 70,
|
| 42 |
+
22, 45, 62, 74,
|
| 43 |
+
24, 47, 65, 77,
|
| 44 |
+
25, 49, 68, 81
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
const EC_CODEWORDS_TABLE = [
|
| 48 |
+
// L M Q H
|
| 49 |
+
7, 10, 13, 17,
|
| 50 |
+
10, 16, 22, 28,
|
| 51 |
+
15, 26, 36, 44,
|
| 52 |
+
20, 36, 52, 64,
|
| 53 |
+
26, 48, 72, 88,
|
| 54 |
+
36, 64, 96, 112,
|
| 55 |
+
40, 72, 108, 130,
|
| 56 |
+
48, 88, 132, 156,
|
| 57 |
+
60, 110, 160, 192,
|
| 58 |
+
72, 130, 192, 224,
|
| 59 |
+
80, 150, 224, 264,
|
| 60 |
+
96, 176, 260, 308,
|
| 61 |
+
104, 198, 288, 352,
|
| 62 |
+
120, 216, 320, 384,
|
| 63 |
+
132, 240, 360, 432,
|
| 64 |
+
144, 280, 408, 480,
|
| 65 |
+
168, 308, 448, 532,
|
| 66 |
+
180, 338, 504, 588,
|
| 67 |
+
196, 364, 546, 650,
|
| 68 |
+
224, 416, 600, 700,
|
| 69 |
+
224, 442, 644, 750,
|
| 70 |
+
252, 476, 690, 816,
|
| 71 |
+
270, 504, 750, 900,
|
| 72 |
+
300, 560, 810, 960,
|
| 73 |
+
312, 588, 870, 1050,
|
| 74 |
+
336, 644, 952, 1110,
|
| 75 |
+
360, 700, 1020, 1200,
|
| 76 |
+
390, 728, 1050, 1260,
|
| 77 |
+
420, 784, 1140, 1350,
|
| 78 |
+
450, 812, 1200, 1440,
|
| 79 |
+
480, 868, 1290, 1530,
|
| 80 |
+
510, 924, 1350, 1620,
|
| 81 |
+
540, 980, 1440, 1710,
|
| 82 |
+
570, 1036, 1530, 1800,
|
| 83 |
+
570, 1064, 1590, 1890,
|
| 84 |
+
600, 1120, 1680, 1980,
|
| 85 |
+
630, 1204, 1770, 2100,
|
| 86 |
+
660, 1260, 1860, 2220,
|
| 87 |
+
720, 1316, 1950, 2310,
|
| 88 |
+
750, 1372, 2040, 2430
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Returns the number of error correction block that the QR Code should contain
|
| 93 |
+
* for the specified version and error correction level.
|
| 94 |
+
*
|
| 95 |
+
* @param {Number} version QR Code version
|
| 96 |
+
* @param {Number} errorCorrectionLevel Error correction level
|
| 97 |
+
* @return {Number} Number of error correction blocks
|
| 98 |
+
*/
|
| 99 |
+
exports.getBlocksCount = function getBlocksCount (version, errorCorrectionLevel) {
|
| 100 |
+
switch (errorCorrectionLevel) {
|
| 101 |
+
case ECLevel.L:
|
| 102 |
+
return EC_BLOCKS_TABLE[(version - 1) * 4 + 0]
|
| 103 |
+
case ECLevel.M:
|
| 104 |
+
return EC_BLOCKS_TABLE[(version - 1) * 4 + 1]
|
| 105 |
+
case ECLevel.Q:
|
| 106 |
+
return EC_BLOCKS_TABLE[(version - 1) * 4 + 2]
|
| 107 |
+
case ECLevel.H:
|
| 108 |
+
return EC_BLOCKS_TABLE[(version - 1) * 4 + 3]
|
| 109 |
+
default:
|
| 110 |
+
return undefined
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Returns the number of error correction codewords to use for the specified
|
| 116 |
+
* version and error correction level.
|
| 117 |
+
*
|
| 118 |
+
* @param {Number} version QR Code version
|
| 119 |
+
* @param {Number} errorCorrectionLevel Error correction level
|
| 120 |
+
* @return {Number} Number of error correction codewords
|
| 121 |
+
*/
|
| 122 |
+
exports.getTotalCodewordsCount = function getTotalCodewordsCount (version, errorCorrectionLevel) {
|
| 123 |
+
switch (errorCorrectionLevel) {
|
| 124 |
+
case ECLevel.L:
|
| 125 |
+
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 0]
|
| 126 |
+
case ECLevel.M:
|
| 127 |
+
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 1]
|
| 128 |
+
case ECLevel.Q:
|
| 129 |
+
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 2]
|
| 130 |
+
case ECLevel.H:
|
| 131 |
+
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 3]
|
| 132 |
+
default:
|
| 133 |
+
return undefined
|
| 134 |
+
}
|
| 135 |
+
}
|
BeadQR/miniprogram/utils/qrcore/error-correction-level.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
exports.L = { bit: 1 }
|
| 2 |
+
exports.M = { bit: 0 }
|
| 3 |
+
exports.Q = { bit: 3 }
|
| 4 |
+
exports.H = { bit: 2 }
|
| 5 |
+
|
| 6 |
+
function fromString (string) {
|
| 7 |
+
if (typeof string !== 'string') {
|
| 8 |
+
throw new Error('Param is not a string')
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const lcStr = string.toLowerCase()
|
| 12 |
+
|
| 13 |
+
switch (lcStr) {
|
| 14 |
+
case 'l':
|
| 15 |
+
case 'low':
|
| 16 |
+
return exports.L
|
| 17 |
+
|
| 18 |
+
case 'm':
|
| 19 |
+
case 'medium':
|
| 20 |
+
return exports.M
|
| 21 |
+
|
| 22 |
+
case 'q':
|
| 23 |
+
case 'quartile':
|
| 24 |
+
return exports.Q
|
| 25 |
+
|
| 26 |
+
case 'h':
|
| 27 |
+
case 'high':
|
| 28 |
+
return exports.H
|
| 29 |
+
|
| 30 |
+
default:
|
| 31 |
+
throw new Error('Unknown EC Level: ' + string)
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
exports.isValid = function isValid (level) {
|
| 36 |
+
return level && typeof level.bit !== 'undefined' &&
|
| 37 |
+
level.bit >= 0 && level.bit < 4
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
exports.from = function from (value, defaultValue) {
|
| 41 |
+
if (exports.isValid(value)) {
|
| 42 |
+
return value
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
try {
|
| 46 |
+
return fromString(value)
|
| 47 |
+
} catch (e) {
|
| 48 |
+
return defaultValue
|
| 49 |
+
}
|
| 50 |
+
}
|
BeadQR/miniprogram/utils/qrcore/finder-pattern.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const getSymbolSize = require('./utils').getSymbolSize
|
| 2 |
+
const FINDER_PATTERN_SIZE = 7
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Returns an array containing the positions of each finder pattern.
|
| 6 |
+
* Each array's element represent the top-left point of the pattern as (x, y) coordinates
|
| 7 |
+
*
|
| 8 |
+
* @param {Number} version QR Code version
|
| 9 |
+
* @return {Array} Array of coordinates
|
| 10 |
+
*/
|
| 11 |
+
exports.getPositions = function getPositions (version) {
|
| 12 |
+
const size = getSymbolSize(version)
|
| 13 |
+
|
| 14 |
+
return [
|
| 15 |
+
// top-left
|
| 16 |
+
[0, 0],
|
| 17 |
+
// top-right
|
| 18 |
+
[size - FINDER_PATTERN_SIZE, 0],
|
| 19 |
+
// bottom-left
|
| 20 |
+
[0, size - FINDER_PATTERN_SIZE]
|
| 21 |
+
]
|
| 22 |
+
}
|
BeadQR/miniprogram/utils/qrcore/format-info.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Utils = require('./utils')
|
| 2 |
+
|
| 3 |
+
const G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)
|
| 4 |
+
const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
|
| 5 |
+
const G15_BCH = Utils.getBCHDigit(G15)
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Returns format information with relative error correction bits
|
| 9 |
+
*
|
| 10 |
+
* The format information is a 15-bit sequence containing 5 data bits,
|
| 11 |
+
* with 10 error correction bits calculated using the (15, 5) BCH code.
|
| 12 |
+
*
|
| 13 |
+
* @param {Number} errorCorrectionLevel Error correction level
|
| 14 |
+
* @param {Number} mask Mask pattern
|
| 15 |
+
* @return {Number} Encoded format information bits
|
| 16 |
+
*/
|
| 17 |
+
exports.getEncodedBits = function getEncodedBits (errorCorrectionLevel, mask) {
|
| 18 |
+
const data = ((errorCorrectionLevel.bit << 3) | mask)
|
| 19 |
+
let d = data << 10
|
| 20 |
+
|
| 21 |
+
while (Utils.getBCHDigit(d) - G15_BCH >= 0) {
|
| 22 |
+
d ^= (G15 << (Utils.getBCHDigit(d) - G15_BCH))
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// xor final data with mask pattern in order to ensure that
|
| 26 |
+
// no combination of Error Correction Level and data mask pattern
|
| 27 |
+
// will result in an all-zero data string
|
| 28 |
+
return ((data << 10) | d) ^ G15_MASK
|
| 29 |
+
}
|
BeadQR/miniprogram/utils/qrcore/galois-field.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const EXP_TABLE = new Uint8Array(512)
|
| 2 |
+
const LOG_TABLE = new Uint8Array(256)
|
| 3 |
+
/**
|
| 4 |
+
* Precompute the log and anti-log tables for faster computation later
|
| 5 |
+
*
|
| 6 |
+
* For each possible value in the galois field 2^8, we will pre-compute
|
| 7 |
+
* the logarithm and anti-logarithm (exponential) of this value
|
| 8 |
+
*
|
| 9 |
+
* ref {@link https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Introduction_to_mathematical_fields}
|
| 10 |
+
*/
|
| 11 |
+
;(function initTables () {
|
| 12 |
+
let x = 1
|
| 13 |
+
for (let i = 0; i < 255; i++) {
|
| 14 |
+
EXP_TABLE[i] = x
|
| 15 |
+
LOG_TABLE[x] = i
|
| 16 |
+
|
| 17 |
+
x <<= 1 // multiply by 2
|
| 18 |
+
|
| 19 |
+
// The QR code specification says to use byte-wise modulo 100011101 arithmetic.
|
| 20 |
+
// This means that when a number is 256 or larger, it should be XORed with 0x11D.
|
| 21 |
+
if (x & 0x100) { // similar to x >= 256, but a lot faster (because 0x100 == 256)
|
| 22 |
+
x ^= 0x11D
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Optimization: double the size of the anti-log table so that we don't need to mod 255 to
|
| 27 |
+
// stay inside the bounds (because we will mainly use this table for the multiplication of
|
| 28 |
+
// two GF numbers, no more).
|
| 29 |
+
// @see {@link mul}
|
| 30 |
+
for (let i = 255; i < 512; i++) {
|
| 31 |
+
EXP_TABLE[i] = EXP_TABLE[i - 255]
|
| 32 |
+
}
|
| 33 |
+
}())
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Returns log value of n inside Galois Field
|
| 37 |
+
*
|
| 38 |
+
* @param {Number} n
|
| 39 |
+
* @return {Number}
|
| 40 |
+
*/
|
| 41 |
+
exports.log = function log (n) {
|
| 42 |
+
if (n < 1) throw new Error('log(' + n + ')')
|
| 43 |
+
return LOG_TABLE[n]
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Returns anti-log value of n inside Galois Field
|
| 48 |
+
*
|
| 49 |
+
* @param {Number} n
|
| 50 |
+
* @return {Number}
|
| 51 |
+
*/
|
| 52 |
+
exports.exp = function exp (n) {
|
| 53 |
+
return EXP_TABLE[n]
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Multiplies two number inside Galois Field
|
| 58 |
+
*
|
| 59 |
+
* @param {Number} x
|
| 60 |
+
* @param {Number} y
|
| 61 |
+
* @return {Number}
|
| 62 |
+
*/
|
| 63 |
+
exports.mul = function mul (x, y) {
|
| 64 |
+
if (x === 0 || y === 0) return 0
|
| 65 |
+
|
| 66 |
+
// should be EXP_TABLE[(LOG_TABLE[x] + LOG_TABLE[y]) % 255] if EXP_TABLE wasn't oversized
|
| 67 |
+
// @see {@link initTables}
|
| 68 |
+
return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]]
|
| 69 |
+
}
|
BeadQR/miniprogram/utils/qrcore/kanji-data.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Mode = require('./mode')
|
| 2 |
+
const Utils = require('./utils')
|
| 3 |
+
|
| 4 |
+
function KanjiData (data) {
|
| 5 |
+
this.mode = Mode.KANJI
|
| 6 |
+
this.data = data
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
KanjiData.getBitsLength = function getBitsLength (length) {
|
| 10 |
+
return length * 13
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
KanjiData.prototype.getLength = function getLength () {
|
| 14 |
+
return this.data.length
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
KanjiData.prototype.getBitsLength = function getBitsLength () {
|
| 18 |
+
return KanjiData.getBitsLength(this.data.length)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
KanjiData.prototype.write = function (bitBuffer) {
|
| 22 |
+
let i
|
| 23 |
+
|
| 24 |
+
// In the Shift JIS system, Kanji characters are represented by a two byte combination.
|
| 25 |
+
// These byte values are shifted from the JIS X 0208 values.
|
| 26 |
+
// JIS X 0208 gives details of the shift coded representation.
|
| 27 |
+
for (i = 0; i < this.data.length; i++) {
|
| 28 |
+
let value = Utils.toSJIS(this.data[i])
|
| 29 |
+
|
| 30 |
+
// For characters with Shift JIS values from 0x8140 to 0x9FFC:
|
| 31 |
+
if (value >= 0x8140 && value <= 0x9FFC) {
|
| 32 |
+
// Subtract 0x8140 from Shift JIS value
|
| 33 |
+
value -= 0x8140
|
| 34 |
+
|
| 35 |
+
// For characters with Shift JIS values from 0xE040 to 0xEBBF
|
| 36 |
+
} else if (value >= 0xE040 && value <= 0xEBBF) {
|
| 37 |
+
// Subtract 0xC140 from Shift JIS value
|
| 38 |
+
value -= 0xC140
|
| 39 |
+
} else {
|
| 40 |
+
throw new Error(
|
| 41 |
+
'Invalid SJIS character: ' + this.data[i] + '\n' +
|
| 42 |
+
'Make sure your charset is UTF-8')
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Multiply most significant byte of result by 0xC0
|
| 46 |
+
// and add least significant byte to product
|
| 47 |
+
value = (((value >>> 8) & 0xff) * 0xC0) + (value & 0xff)
|
| 48 |
+
|
| 49 |
+
// Convert result to a 13-bit binary string
|
| 50 |
+
bitBuffer.put(value, 13)
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
module.exports = KanjiData
|
BeadQR/miniprogram/utils/qrcore/mask-pattern.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Data mask pattern reference
|
| 3 |
+
* @type {Object}
|
| 4 |
+
*/
|
| 5 |
+
exports.Patterns = {
|
| 6 |
+
PATTERN000: 0,
|
| 7 |
+
PATTERN001: 1,
|
| 8 |
+
PATTERN010: 2,
|
| 9 |
+
PATTERN011: 3,
|
| 10 |
+
PATTERN100: 4,
|
| 11 |
+
PATTERN101: 5,
|
| 12 |
+
PATTERN110: 6,
|
| 13 |
+
PATTERN111: 7
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Weighted penalty scores for the undesirable features
|
| 18 |
+
* @type {Object}
|
| 19 |
+
*/
|
| 20 |
+
const PenaltyScores = {
|
| 21 |
+
N1: 3,
|
| 22 |
+
N2: 3,
|
| 23 |
+
N3: 40,
|
| 24 |
+
N4: 10
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* Check if mask pattern value is valid
|
| 29 |
+
*
|
| 30 |
+
* @param {Number} mask Mask pattern
|
| 31 |
+
* @return {Boolean} true if valid, false otherwise
|
| 32 |
+
*/
|
| 33 |
+
exports.isValid = function isValid (mask) {
|
| 34 |
+
return mask != null && mask !== '' && !isNaN(mask) && mask >= 0 && mask <= 7
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Returns mask pattern from a value.
|
| 39 |
+
* If value is not valid, returns undefined
|
| 40 |
+
*
|
| 41 |
+
* @param {Number|String} value Mask pattern value
|
| 42 |
+
* @return {Number} Valid mask pattern or undefined
|
| 43 |
+
*/
|
| 44 |
+
exports.from = function from (value) {
|
| 45 |
+
return exports.isValid(value) ? parseInt(value, 10) : undefined
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* Find adjacent modules in row/column with the same color
|
| 50 |
+
* and assign a penalty value.
|
| 51 |
+
*
|
| 52 |
+
* Points: N1 + i
|
| 53 |
+
* i is the amount by which the number of adjacent modules of the same color exceeds 5
|
| 54 |
+
*/
|
| 55 |
+
exports.getPenaltyN1 = function getPenaltyN1 (data) {
|
| 56 |
+
const size = data.size
|
| 57 |
+
let points = 0
|
| 58 |
+
let sameCountCol = 0
|
| 59 |
+
let sameCountRow = 0
|
| 60 |
+
let lastCol = null
|
| 61 |
+
let lastRow = null
|
| 62 |
+
|
| 63 |
+
for (let row = 0; row < size; row++) {
|
| 64 |
+
sameCountCol = sameCountRow = 0
|
| 65 |
+
lastCol = lastRow = null
|
| 66 |
+
|
| 67 |
+
for (let col = 0; col < size; col++) {
|
| 68 |
+
let module = data.get(row, col)
|
| 69 |
+
if (module === lastCol) {
|
| 70 |
+
sameCountCol++
|
| 71 |
+
} else {
|
| 72 |
+
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5)
|
| 73 |
+
lastCol = module
|
| 74 |
+
sameCountCol = 1
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
module = data.get(col, row)
|
| 78 |
+
if (module === lastRow) {
|
| 79 |
+
sameCountRow++
|
| 80 |
+
} else {
|
| 81 |
+
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5)
|
| 82 |
+
lastRow = module
|
| 83 |
+
sameCountRow = 1
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5)
|
| 88 |
+
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return points
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Find 2x2 blocks with the same color and assign a penalty value
|
| 96 |
+
*
|
| 97 |
+
* Points: N2 * (m - 1) * (n - 1)
|
| 98 |
+
*/
|
| 99 |
+
exports.getPenaltyN2 = function getPenaltyN2 (data) {
|
| 100 |
+
const size = data.size
|
| 101 |
+
let points = 0
|
| 102 |
+
|
| 103 |
+
for (let row = 0; row < size - 1; row++) {
|
| 104 |
+
for (let col = 0; col < size - 1; col++) {
|
| 105 |
+
const last = data.get(row, col) +
|
| 106 |
+
data.get(row, col + 1) +
|
| 107 |
+
data.get(row + 1, col) +
|
| 108 |
+
data.get(row + 1, col + 1)
|
| 109 |
+
|
| 110 |
+
if (last === 4 || last === 0) points++
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return points * PenaltyScores.N2
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/**
|
| 118 |
+
* Find 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column,
|
| 119 |
+
* preceded or followed by light area 4 modules wide
|
| 120 |
+
*
|
| 121 |
+
* Points: N3 * number of pattern found
|
| 122 |
+
*/
|
| 123 |
+
exports.getPenaltyN3 = function getPenaltyN3 (data) {
|
| 124 |
+
const size = data.size
|
| 125 |
+
let points = 0
|
| 126 |
+
let bitsCol = 0
|
| 127 |
+
let bitsRow = 0
|
| 128 |
+
|
| 129 |
+
for (let row = 0; row < size; row++) {
|
| 130 |
+
bitsCol = bitsRow = 0
|
| 131 |
+
for (let col = 0; col < size; col++) {
|
| 132 |
+
bitsCol = ((bitsCol << 1) & 0x7FF) | data.get(row, col)
|
| 133 |
+
if (col >= 10 && (bitsCol === 0x5D0 || bitsCol === 0x05D)) points++
|
| 134 |
+
|
| 135 |
+
bitsRow = ((bitsRow << 1) & 0x7FF) | data.get(col, row)
|
| 136 |
+
if (col >= 10 && (bitsRow === 0x5D0 || bitsRow === 0x05D)) points++
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return points * PenaltyScores.N3
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* Calculate proportion of dark modules in entire symbol
|
| 145 |
+
*
|
| 146 |
+
* Points: N4 * k
|
| 147 |
+
*
|
| 148 |
+
* k is the rating of the deviation of the proportion of dark modules
|
| 149 |
+
* in the symbol from 50% in steps of 5%
|
| 150 |
+
*/
|
| 151 |
+
exports.getPenaltyN4 = function getPenaltyN4 (data) {
|
| 152 |
+
let darkCount = 0
|
| 153 |
+
const modulesCount = data.data.length
|
| 154 |
+
|
| 155 |
+
for (let i = 0; i < modulesCount; i++) darkCount += data.data[i]
|
| 156 |
+
|
| 157 |
+
const k = Math.abs(Math.ceil((darkCount * 100 / modulesCount) / 5) - 10)
|
| 158 |
+
|
| 159 |
+
return k * PenaltyScores.N4
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* Return mask value at given position
|
| 164 |
+
*
|
| 165 |
+
* @param {Number} maskPattern Pattern reference value
|
| 166 |
+
* @param {Number} i Row
|
| 167 |
+
* @param {Number} j Column
|
| 168 |
+
* @return {Boolean} Mask value
|
| 169 |
+
*/
|
| 170 |
+
function getMaskAt (maskPattern, i, j) {
|
| 171 |
+
switch (maskPattern) {
|
| 172 |
+
case exports.Patterns.PATTERN000: return (i + j) % 2 === 0
|
| 173 |
+
case exports.Patterns.PATTERN001: return i % 2 === 0
|
| 174 |
+
case exports.Patterns.PATTERN010: return j % 3 === 0
|
| 175 |
+
case exports.Patterns.PATTERN011: return (i + j) % 3 === 0
|
| 176 |
+
case exports.Patterns.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0
|
| 177 |
+
case exports.Patterns.PATTERN101: return (i * j) % 2 + (i * j) % 3 === 0
|
| 178 |
+
case exports.Patterns.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 === 0
|
| 179 |
+
case exports.Patterns.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 === 0
|
| 180 |
+
|
| 181 |
+
default: throw new Error('bad maskPattern:' + maskPattern)
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/**
|
| 186 |
+
* Apply a mask pattern to a BitMatrix
|
| 187 |
+
*
|
| 188 |
+
* @param {Number} pattern Pattern reference number
|
| 189 |
+
* @param {BitMatrix} data BitMatrix data
|
| 190 |
+
*/
|
| 191 |
+
exports.applyMask = function applyMask (pattern, data) {
|
| 192 |
+
const size = data.size
|
| 193 |
+
|
| 194 |
+
for (let col = 0; col < size; col++) {
|
| 195 |
+
for (let row = 0; row < size; row++) {
|
| 196 |
+
if (data.isReserved(row, col)) continue
|
| 197 |
+
data.xor(row, col, getMaskAt(pattern, row, col))
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/**
|
| 203 |
+
* Returns the best mask pattern for data
|
| 204 |
+
*
|
| 205 |
+
* @param {BitMatrix} data
|
| 206 |
+
* @return {Number} Mask pattern reference number
|
| 207 |
+
*/
|
| 208 |
+
exports.getBestMask = function getBestMask (data, setupFormatFunc) {
|
| 209 |
+
const numPatterns = Object.keys(exports.Patterns).length
|
| 210 |
+
let bestPattern = 0
|
| 211 |
+
let lowerPenalty = Infinity
|
| 212 |
+
|
| 213 |
+
for (let p = 0; p < numPatterns; p++) {
|
| 214 |
+
setupFormatFunc(p)
|
| 215 |
+
exports.applyMask(p, data)
|
| 216 |
+
|
| 217 |
+
// Calculate penalty
|
| 218 |
+
const penalty =
|
| 219 |
+
exports.getPenaltyN1(data) +
|
| 220 |
+
exports.getPenaltyN2(data) +
|
| 221 |
+
exports.getPenaltyN3(data) +
|
| 222 |
+
exports.getPenaltyN4(data)
|
| 223 |
+
|
| 224 |
+
// Undo previously applied mask
|
| 225 |
+
exports.applyMask(p, data)
|
| 226 |
+
|
| 227 |
+
if (penalty < lowerPenalty) {
|
| 228 |
+
lowerPenalty = penalty
|
| 229 |
+
bestPattern = p
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
return bestPattern
|
| 234 |
+
}
|
BeadQR/miniprogram/utils/qrcore/mode.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const VersionCheck = require('./version-check')
|
| 2 |
+
const Regex = require('./regex')
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Numeric mode encodes data from the decimal digit set (0 - 9)
|
| 6 |
+
* (byte values 30HEX to 39HEX).
|
| 7 |
+
* Normally, 3 data characters are represented by 10 bits.
|
| 8 |
+
*
|
| 9 |
+
* @type {Object}
|
| 10 |
+
*/
|
| 11 |
+
exports.NUMERIC = {
|
| 12 |
+
id: 'Numeric',
|
| 13 |
+
bit: 1 << 0,
|
| 14 |
+
ccBits: [10, 12, 14]
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Alphanumeric mode encodes data from a set of 45 characters,
|
| 19 |
+
* i.e. 10 numeric digits (0 - 9),
|
| 20 |
+
* 26 alphabetic characters (A - Z),
|
| 21 |
+
* and 9 symbols (SP, $, %, *, +, -, ., /, :).
|
| 22 |
+
* Normally, two input characters are represented by 11 bits.
|
| 23 |
+
*
|
| 24 |
+
* @type {Object}
|
| 25 |
+
*/
|
| 26 |
+
exports.ALPHANUMERIC = {
|
| 27 |
+
id: 'Alphanumeric',
|
| 28 |
+
bit: 1 << 1,
|
| 29 |
+
ccBits: [9, 11, 13]
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* In byte mode, data is encoded at 8 bits per character.
|
| 34 |
+
*
|
| 35 |
+
* @type {Object}
|
| 36 |
+
*/
|
| 37 |
+
exports.BYTE = {
|
| 38 |
+
id: 'Byte',
|
| 39 |
+
bit: 1 << 2,
|
| 40 |
+
ccBits: [8, 16, 16]
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* The Kanji mode efficiently encodes Kanji characters in accordance with
|
| 45 |
+
* the Shift JIS system based on JIS X 0208.
|
| 46 |
+
* The Shift JIS values are shifted from the JIS X 0208 values.
|
| 47 |
+
* JIS X 0208 gives details of the shift coded representation.
|
| 48 |
+
* Each two-byte character value is compacted to a 13-bit binary codeword.
|
| 49 |
+
*
|
| 50 |
+
* @type {Object}
|
| 51 |
+
*/
|
| 52 |
+
exports.KANJI = {
|
| 53 |
+
id: 'Kanji',
|
| 54 |
+
bit: 1 << 3,
|
| 55 |
+
ccBits: [8, 10, 12]
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Mixed mode will contain a sequences of data in a combination of any of
|
| 60 |
+
* the modes described above
|
| 61 |
+
*
|
| 62 |
+
* @type {Object}
|
| 63 |
+
*/
|
| 64 |
+
exports.MIXED = {
|
| 65 |
+
bit: -1
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/**
|
| 69 |
+
* Returns the number of bits needed to store the data length
|
| 70 |
+
* according to QR Code specifications.
|
| 71 |
+
*
|
| 72 |
+
* @param {Mode} mode Data mode
|
| 73 |
+
* @param {Number} version QR Code version
|
| 74 |
+
* @return {Number} Number of bits
|
| 75 |
+
*/
|
| 76 |
+
exports.getCharCountIndicator = function getCharCountIndicator (mode, version) {
|
| 77 |
+
if (!mode.ccBits) throw new Error('Invalid mode: ' + mode)
|
| 78 |
+
|
| 79 |
+
if (!VersionCheck.isValid(version)) {
|
| 80 |
+
throw new Error('Invalid version: ' + version)
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if (version >= 1 && version < 10) return mode.ccBits[0]
|
| 84 |
+
else if (version < 27) return mode.ccBits[1]
|
| 85 |
+
return mode.ccBits[2]
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Returns the most efficient mode to store the specified data
|
| 90 |
+
*
|
| 91 |
+
* @param {String} dataStr Input data string
|
| 92 |
+
* @return {Mode} Best mode
|
| 93 |
+
*/
|
| 94 |
+
exports.getBestModeForData = function getBestModeForData (dataStr) {
|
| 95 |
+
if (Regex.testNumeric(dataStr)) return exports.NUMERIC
|
| 96 |
+
else if (Regex.testAlphanumeric(dataStr)) return exports.ALPHANUMERIC
|
| 97 |
+
else if (Regex.testKanji(dataStr)) return exports.KANJI
|
| 98 |
+
else return exports.BYTE
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Return mode name as string
|
| 103 |
+
*
|
| 104 |
+
* @param {Mode} mode Mode object
|
| 105 |
+
* @returns {String} Mode name
|
| 106 |
+
*/
|
| 107 |
+
exports.toString = function toString (mode) {
|
| 108 |
+
if (mode && mode.id) return mode.id
|
| 109 |
+
throw new Error('Invalid mode')
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* Check if input param is a valid mode object
|
| 114 |
+
*
|
| 115 |
+
* @param {Mode} mode Mode object
|
| 116 |
+
* @returns {Boolean} True if valid mode, false otherwise
|
| 117 |
+
*/
|
| 118 |
+
exports.isValid = function isValid (mode) {
|
| 119 |
+
return mode && mode.bit && mode.ccBits
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/**
|
| 123 |
+
* Get mode object from its name
|
| 124 |
+
*
|
| 125 |
+
* @param {String} string Mode name
|
| 126 |
+
* @returns {Mode} Mode object
|
| 127 |
+
*/
|
| 128 |
+
function fromString (string) {
|
| 129 |
+
if (typeof string !== 'string') {
|
| 130 |
+
throw new Error('Param is not a string')
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
const lcStr = string.toLowerCase()
|
| 134 |
+
|
| 135 |
+
switch (lcStr) {
|
| 136 |
+
case 'numeric':
|
| 137 |
+
return exports.NUMERIC
|
| 138 |
+
case 'alphanumeric':
|
| 139 |
+
return exports.ALPHANUMERIC
|
| 140 |
+
case 'kanji':
|
| 141 |
+
return exports.KANJI
|
| 142 |
+
case 'byte':
|
| 143 |
+
return exports.BYTE
|
| 144 |
+
default:
|
| 145 |
+
throw new Error('Unknown mode: ' + string)
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Returns mode from a value.
|
| 151 |
+
* If value is not a valid mode, returns defaultValue
|
| 152 |
+
*
|
| 153 |
+
* @param {Mode|String} value Encoding mode
|
| 154 |
+
* @param {Mode} defaultValue Fallback value
|
| 155 |
+
* @return {Mode} Encoding mode
|
| 156 |
+
*/
|
| 157 |
+
exports.from = function from (value, defaultValue) {
|
| 158 |
+
if (exports.isValid(value)) {
|
| 159 |
+
return value
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
try {
|
| 163 |
+
return fromString(value)
|
| 164 |
+
} catch (e) {
|
| 165 |
+
return defaultValue
|
| 166 |
+
}
|
| 167 |
+
}
|