perf: 优化图片渲染性能和内存管理
重构图片渲染逻辑,引入图片缓存系统和状态压缩机制 添加批量处理层叠图功能,每批5个图片处理 实现内存监控和自动清理机制 增加全局Loading管理器显示操作进度 优化撤销重做功能的状态比较和保存逻辑
This commit is contained in:
295
IMPROVEMENTS.md
Normal file
295
IMPROVEMENTS.md
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# 极简邀请函制作工具 - 升级改进计划
|
||||||
|
|
||||||
|
## 一、架构与工程化改进
|
||||||
|
|
||||||
|
### 1. 模块化重构
|
||||||
|
- **现状**: 所有代码集中在 3000+ 行的 `script.js` 文件中
|
||||||
|
- **改进方案**: 拆分为多个功能模块
|
||||||
|
- `state.js` - 状态管理(撤销/重做栈、当前选中元素等)
|
||||||
|
- `canvas.js` - 画布渲染与导出逻辑
|
||||||
|
- `textEditor.js` - 文本编辑功能
|
||||||
|
- `imageEditor.js` - 图片编辑功能
|
||||||
|
- `ui.js` - UI 交互与事件绑定
|
||||||
|
- `utils.js` - 工具函数
|
||||||
|
|
||||||
|
### 2. 引入构建工具
|
||||||
|
- **现状**: 原生 HTML/CSS/JS,无构建流程
|
||||||
|
- **改进方案**: 引入 Vite 或 Webpack
|
||||||
|
- 支持 ES6 Modules
|
||||||
|
- 代码压缩与优化
|
||||||
|
- 热更新开发体验
|
||||||
|
- 生产环境自动构建
|
||||||
|
|
||||||
|
### 3. TypeScript 迁移
|
||||||
|
- **改进方案**: 将 JavaScript 迁移至 TypeScript
|
||||||
|
- 添加类型定义,减少运行时错误
|
||||||
|
- 增强 IDE 智能提示
|
||||||
|
- 提升代码健壮性和可维护性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、功能增强
|
||||||
|
|
||||||
|
### 4. 图层管理系统
|
||||||
|
- **现状**: 仅支持背景图、层叠图、文字三层简单叠加
|
||||||
|
- **改进方案**:
|
||||||
|
- 图层列表面板(显示所有元素层级)
|
||||||
|
- 图层顺序调整(上移/下移/置顶/置底)
|
||||||
|
- 图层命名与锁定功能
|
||||||
|
- 图层可见性切换(显示/隐藏)
|
||||||
|
- 图层透明度独立调节
|
||||||
|
|
||||||
|
### 5. 更多元素类型
|
||||||
|
- **新增元素**:
|
||||||
|
- **形状元素**: 矩形、圆形、三角形、线条、箭头
|
||||||
|
- **二维码/条形码**: 支持输入内容生成二维码
|
||||||
|
- **图标库**: 集成 FontAwesome 或自定义图标库
|
||||||
|
- **贴纸/装饰**: 预设装饰元素库(花边、边框、图案)
|
||||||
|
- **日期时间**: 自动插入当前日期,支持多种格式
|
||||||
|
|
||||||
|
### 6. 高级文本功能
|
||||||
|
- **现状**: 仅支持基础字体、颜色、大小、加粗、斜体
|
||||||
|
- **改进方案**:
|
||||||
|
- 文本对齐方式(左对齐/居中/右对齐/两端对齐)
|
||||||
|
- 行高调整
|
||||||
|
- 字间距调整
|
||||||
|
- 文本阴影/描边效果
|
||||||
|
- 富文本编辑器(支持部分文字独立设置样式)
|
||||||
|
- 文字竖排支持
|
||||||
|
- 文字沿路径排列
|
||||||
|
|
||||||
|
### 7. 图片编辑增强
|
||||||
|
- **现状**: 仅支持透明度、旋转、水平镜像
|
||||||
|
- **改进方案**:
|
||||||
|
- 图片滤镜(亮度、对比度、饱和度、色相、模糊、灰度)
|
||||||
|
- 图片裁剪功能(自由裁剪、固定比例裁剪)
|
||||||
|
- 垂直镜像
|
||||||
|
- 圆角裁剪
|
||||||
|
- 旋转角度精确输入(支持小数角度)
|
||||||
|
- 图片混合模式(正片叠底、滤色、叠加等)
|
||||||
|
|
||||||
|
### 8. 模板系统
|
||||||
|
- **改进方案**:
|
||||||
|
- 预设邀请函模板库(婚礼、生日、会议、活动、节日等)
|
||||||
|
- 模板分类与搜索
|
||||||
|
- 用户自定义模板保存
|
||||||
|
- 模板分享功能
|
||||||
|
- 热门模板推荐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、性能优化 ✅ 已完成
|
||||||
|
|
||||||
|
### 9. Canvas 渲染优化 ✅
|
||||||
|
- **现状**: `generateCanvas` 函数逻辑复杂,渲染耗时
|
||||||
|
- **改进方案**:
|
||||||
|
- ✅ 使用 `OffscreenCanvas` 进行后台渲染
|
||||||
|
- ✅ 图片预加载与缓存机制(新增 `imageCache` Map)
|
||||||
|
- ✅ 大图片分片渲染(批量处理,每批5个)
|
||||||
|
- ✅ 渲染进度提示(Loading 管理器实时更新进度)
|
||||||
|
- **实现细节**:
|
||||||
|
- 新增 `getCachedImage()` 函数缓存已加载图片
|
||||||
|
- `drawOverlayImagesHD()` 改为异步批量处理
|
||||||
|
- 每批处理后让出主线程,避免阻塞 UI
|
||||||
|
|
||||||
|
### 10. 内存管理优化 ✅
|
||||||
|
- **现状**: 历史状态使用深拷贝,可能占用大量内存
|
||||||
|
- **改进方案**:
|
||||||
|
- ✅ 状态压缩(`compressState()` 限制文本长度)
|
||||||
|
- ✅ 根据历史记录数量动态清理(超过80%时自动清理到60%)
|
||||||
|
- ✅ 图片缓存大小限制(最大20张)
|
||||||
|
- ✅ 提供清理缓存功能(`clearImageCache()`)
|
||||||
|
- **实现细节**:
|
||||||
|
- 新增 `memoryMonitor` 内存监控器,每30秒检查一次
|
||||||
|
- 状态对比优化(先比较时间戳和关键属性)
|
||||||
|
- 状态添加时间戳便于管理
|
||||||
|
|
||||||
|
### 11. 防抖/节流优化 ✅
|
||||||
|
- **现状**: 拖拽过程中频繁触发状态保存
|
||||||
|
- **改进方案**:
|
||||||
|
- ✅ 拖拽结束只保存一次(使用 `immediate` 模式)
|
||||||
|
- ✅ 中间过程不保存状态
|
||||||
|
- ✅ 大操作使用 loading 提示(`loadingManager`)
|
||||||
|
- ✅ 性能计时(`performance.now()` 记录耗时)
|
||||||
|
- **实现细节**:
|
||||||
|
- 新增 `throttle()` 和 `debounce()` 工具函数
|
||||||
|
- 新增 `loadingManager` 全局 Loading 管理器
|
||||||
|
- 图片保存过程显示详细进度(初始化画布→绘制背景→绘制层叠图→绘制文字)
|
||||||
|
- 控制台输出详细性能日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、用户体验改进
|
||||||
|
|
||||||
|
### 12. 移动端优化
|
||||||
|
- **现状**: 基础移动端适配已具备
|
||||||
|
- **改进方案**:
|
||||||
|
- 触摸手势支持(双指缩放、旋转)
|
||||||
|
- 底部操作栏适配 iPhone 安全区
|
||||||
|
- 横屏模式支持
|
||||||
|
- 触摸反馈优化
|
||||||
|
- 虚拟键盘适配优化
|
||||||
|
|
||||||
|
### 13. 键盘快捷键扩展
|
||||||
|
- **现状**: 仅支持撤销/重做和方向键微调
|
||||||
|
- **改进方案**:
|
||||||
|
- `Delete` / `Backspace` - 删除选中元素
|
||||||
|
- `Ctrl+C` / `Ctrl+V` - 复制粘贴元素
|
||||||
|
- `Ctrl+D` - 复制元素
|
||||||
|
- `Ctrl+G` - 组合/解组元素
|
||||||
|
- `Esc` - 取消选中/退出编辑
|
||||||
|
- `Ctrl+S` - 保存草稿
|
||||||
|
- `Ctrl+A` - 全选元素
|
||||||
|
- `↑↓←→` + `Shift` - 快速移动(10px)
|
||||||
|
- `↑↓←→` + `Alt` - 微调移动(0.5px)
|
||||||
|
|
||||||
|
### 14. 对齐与分布工具
|
||||||
|
- **改进方案**:
|
||||||
|
- 多选元素对齐(左对齐、水平居中、右对齐、顶部对齐、垂直居中、底部对齐)
|
||||||
|
- 元素等间距分布(水平分布、垂直分布)
|
||||||
|
- 相对于画布居中
|
||||||
|
- 智能对齐线增强(显示距离数值)
|
||||||
|
- 网格吸附(可选开启)
|
||||||
|
|
||||||
|
### 15. 预览与导出增强
|
||||||
|
- **现状**: 仅支持 PNG 格式导出
|
||||||
|
- **改进方案**:
|
||||||
|
- 多种导出格式(PNG、JPG、WebP、PDF)
|
||||||
|
- 导出质量选择(低/中/高/无损)
|
||||||
|
- 导出尺寸预设(微信头像、朋友圈、打印尺寸、A4等)
|
||||||
|
- 实时预览模式
|
||||||
|
- 导出前预览确认
|
||||||
|
- 批量导出功能
|
||||||
|
|
||||||
|
### 16. 自动保存与草稿
|
||||||
|
- **改进方案**:
|
||||||
|
- LocalStorage 自动保存(可设置间隔)
|
||||||
|
- 草稿恢复提示(意外关闭后)
|
||||||
|
- 多草稿管理(命名、删除、切换)
|
||||||
|
- 手动保存快照
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、代码质量改进
|
||||||
|
|
||||||
|
### 17. 错误处理
|
||||||
|
- **现状**: 异步操作缺少完善的错误处理
|
||||||
|
- **改进方案**:
|
||||||
|
- 添加全局错误边界
|
||||||
|
- 图片加载失败优雅降级
|
||||||
|
- 网络异常处理
|
||||||
|
- 用户友好的错误提示
|
||||||
|
|
||||||
|
### 18. 事件委托优化
|
||||||
|
- **现状**: 大量事件监听器直接绑定在元素上
|
||||||
|
- **改进方案**:
|
||||||
|
- 使用事件委托减少内存占用
|
||||||
|
- 统一事件管理
|
||||||
|
- 避免内存泄漏
|
||||||
|
|
||||||
|
### 19. CSS 优化
|
||||||
|
- **现状**: 使用传统 CSS 文件
|
||||||
|
- **改进方案**:
|
||||||
|
- 使用 CSS 变量统一管理颜色、间距、阴影等
|
||||||
|
- 考虑使用 CSS-in-JS 或 Tailwind CSS
|
||||||
|
- 添加暗黑模式支持
|
||||||
|
- 响应式断点优化
|
||||||
|
|
||||||
|
### 20. 无障碍支持 (a11y)
|
||||||
|
- **改进方案**:
|
||||||
|
- 添加 ARIA 标签
|
||||||
|
- 键盘导航支持
|
||||||
|
- 屏幕阅读器适配
|
||||||
|
- 高对比度模式
|
||||||
|
- 字体大小调节支持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、可扩展性
|
||||||
|
|
||||||
|
### 21. 插件系统
|
||||||
|
- **改进方案**:
|
||||||
|
- 设计插件接口规范
|
||||||
|
- 允许第三方开发插件
|
||||||
|
- 插件市场/管理器
|
||||||
|
- 预设组件库扩展
|
||||||
|
|
||||||
|
### 22. 数据持久化
|
||||||
|
- **改进方案**:
|
||||||
|
- 后端服务集成
|
||||||
|
- 用户账号系统
|
||||||
|
- 云端保存作品
|
||||||
|
- 作品分享链接
|
||||||
|
- 团队协作功能
|
||||||
|
|
||||||
|
### 23. 国际化 (i18n)
|
||||||
|
- **改进方案**:
|
||||||
|
- 多语言支持(中文、英文、日文等)
|
||||||
|
- RTL 布局适配
|
||||||
|
- 时区适配
|
||||||
|
- 本地化日期格式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 优先级规划
|
||||||
|
|
||||||
|
### 🔴 高优先级(核心改进)
|
||||||
|
| 序号 | 改进项 | 预期收益 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| 1 | 模块化重构 | 提升代码可维护性,便于团队协作 |
|
||||||
|
| 2 | TypeScript 迁移 | 减少 Bug,提升开发效率 |
|
||||||
|
| 3 | 图层管理系统 | 核心功能增强,用户体验大幅提升 |
|
||||||
|
| 4 | 自动保存与草稿 | 防止数据丢失,提升用户体验 |
|
||||||
|
| 5 | 错误处理完善 | 提升应用稳定性 |
|
||||||
|
|
||||||
|
### 🟡 中优先级(功能扩展)
|
||||||
|
| 序号 | 改进项 | 预期收益 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| 6 | 模板系统 | 降低用户使用门槛,提升效率 |
|
||||||
|
| 7 | 更多元素类型 | 丰富设计能力 |
|
||||||
|
| 8 | 高级文本功能 | 满足专业排版需求 |
|
||||||
|
| 9 | 图片编辑增强 | 减少对外部工具的依赖 |
|
||||||
|
| 10 | 键盘快捷键扩展 | 提升专业用户效率 |
|
||||||
|
| 11 | 对齐与分布工具 | 提升排版效率 |
|
||||||
|
|
||||||
|
### 🟢 低优先级(长期规划)
|
||||||
|
| 序号 | 改进项 | 预期收益 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| 12 | 插件系统 | 构建生态,无限扩展可能 |
|
||||||
|
| 13 | 后端集成 | 实现数据云同步和社交功能 |
|
||||||
|
| 14 | 国际化 | 拓展海外市场 |
|
||||||
|
| 15 | 暗黑模式 | 提升视觉体验 |
|
||||||
|
| 16 | 无障碍支持 | 社会责任,覆盖更多用户 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施建议
|
||||||
|
|
||||||
|
### 阶段一:基础重构(1-2周)
|
||||||
|
1. 引入 Vite 构建工具
|
||||||
|
2. 代码模块化拆分
|
||||||
|
3. TypeScript 迁移
|
||||||
|
4. 完善错误处理
|
||||||
|
|
||||||
|
### 阶段二:核心功能(2-3周)
|
||||||
|
1. 图层管理系统
|
||||||
|
2. 自动保存与草稿
|
||||||
|
3. 对齐与分布工具
|
||||||
|
4. 键盘快捷键扩展
|
||||||
|
|
||||||
|
### 阶段三:功能扩展(3-4周)
|
||||||
|
1. 模板系统
|
||||||
|
2. 更多元素类型
|
||||||
|
3. 高级文本功能
|
||||||
|
4. 图片编辑增强
|
||||||
|
|
||||||
|
### 阶段四:优化与扩展(持续)
|
||||||
|
1. 性能优化
|
||||||
|
2. 移动端优化
|
||||||
|
3. 插件系统
|
||||||
|
4. 后端集成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档生成时间: 2026-04-23*
|
||||||
|
*基于项目版本分析*
|
||||||
146
README.md
146
README.md
@@ -76,148 +76,4 @@
|
|||||||
36. 层叠图镜像功能,优化编辑器工具栏按钮样式间距,调整背景图缩放和透明度顺序
|
36. 层叠图镜像功能,优化编辑器工具栏按钮样式间距,调整背景图缩放和透明度顺序
|
||||||
37. 项目添加撤销重做功能,检查修复撤销重做后背景图和文本位置显示异常问题,撤销重做按钮只需要点击一次就能执行操作
|
37. 项目添加撤销重做功能,检查修复撤销重做后背景图和文本位置显示异常问题,撤销重做按钮只需要点击一次就能执行操作
|
||||||
38. 优化撤销功能,优化顶部编辑栏按钮占位,优化撤销/重做按钮与文本按钮和保存/预览按钮的间距,文本按钮 ↔ [4px] ↔ 分隔线 ↔ 撤销按钮 ↔ [3px] ↔ 重做按钮 ↔ 分隔线 ↔ [4px] ↔ 保存按钮
|
38. 优化撤销功能,优化顶部编辑栏按钮占位,优化撤销/重做按钮与文本按钮和保存/预览按钮的间距,文本按钮 ↔ [4px] ↔ 分隔线 ↔ 撤销按钮 ↔ [3px] ↔ 重做按钮 ↔ 分隔线 ↔ [4px] ↔ 保存按钮
|
||||||
39. 优化顶部工具栏样式,调整撤销重做按钮分隔栏边距,重构层叠图渲染到最终保存图片上的逻辑
|
39. 优化顶部工具栏样式,调整撤销重做按钮分隔栏边距,重构层叠图渲染到最终保存图片上的逻辑
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🎨 UI/UX 优化
|
|
||||||
1. 加载状态提示
|
|
||||||
|
|
||||||
- 添加图片加载时的 loading 动画
|
|
||||||
- 保存图片时的进度提示
|
|
||||||
2. 空状态优化
|
|
||||||
|
|
||||||
- 没有背景图时的引导提示可以更丰富
|
|
||||||
- 添加示例模板供用户选择
|
|
||||||
3. 移动端适配
|
|
||||||
|
|
||||||
- 底部安全区域适配(iPhone 刘海屏)
|
|
||||||
- 横屏模式下的布局适配
|
|
||||||
## ⚡ 性能优化
|
|
||||||
4. 图片压缩
|
|
||||||
|
|
||||||
- 上传大图片时自动压缩,减少内存占用
|
|
||||||
- 使用 canvas 压缩而非原图保存
|
|
||||||
5. 防抖优化
|
|
||||||
|
|
||||||
- 滑动条调整时进一步减少重绘次数
|
|
||||||
- 拖拽时的性能优化
|
|
||||||
## 🛠️ 功能增强
|
|
||||||
6. 快捷键支持
|
|
||||||
|
|
||||||
- Delete 键删除选中元素
|
|
||||||
- Ctrl+S 保存
|
|
||||||
- 方向键微调位置
|
|
||||||
7. 图层管理
|
|
||||||
|
|
||||||
- 显示当前所有元素列表
|
|
||||||
- 图层顺序调整(置于顶层/底层)
|
|
||||||
8. 历史记录可视化
|
|
||||||
|
|
||||||
- 显示撤销/重做的操作描述
|
|
||||||
- 历史记录列表,可跳转到指定步骤
|
|
||||||
9. 导出选项
|
|
||||||
|
|
||||||
- 支持导出不同格式(JPG/PNG/PDF)
|
|
||||||
- 支持导出不同分辨率
|
|
||||||
10. 自动保存
|
|
||||||
|
|
||||||
- 本地存储自动保存编辑状态
|
|
||||||
- 刷新页面后恢复之前的工作
|
|
||||||
## 🔧 代码优化
|
|
||||||
11. 模块化重构
|
|
||||||
|
|
||||||
- 将功能拆分为独立模块
|
|
||||||
- 更好的代码组织和注释
|
|
||||||
12. TypeScript 迁移
|
|
||||||
|
|
||||||
- 添加类型检查,减少运行时错误
|
|
||||||
13. 测试覆盖
|
|
||||||
|
|
||||||
- 添加单元测试和 E2E 测试
|
|
||||||
## 🎯 核心功能增强
|
|
||||||
1. 模板系统
|
|
||||||
|
|
||||||
- 预设多种邀请函模板(婚礼、生日、商务等)
|
|
||||||
- 模板市场,可下载社区分享的模板
|
|
||||||
- 保存自定义模板
|
|
||||||
2. 更多元素类型
|
|
||||||
|
|
||||||
- 二维码生成器(可扫描添加联系方式/地址)
|
|
||||||
- 形状工具(圆形、矩形、线条装饰)
|
|
||||||
- 贴纸/图标库
|
|
||||||
- 日期选择器(自动格式化日期)
|
|
||||||
3. 高级编辑功能
|
|
||||||
|
|
||||||
- 多选元素(框选多个元素一起移动)
|
|
||||||
- 元素对齐工具(左对齐、居中、等间距分布)
|
|
||||||
- 复制/粘贴元素
|
|
||||||
- 元素锁定(防止误操作)
|
|
||||||
4. 文字增强
|
|
||||||
|
|
||||||
- 文字描边效果
|
|
||||||
- 文字阴影效果
|
|
||||||
- 竖排文字支持
|
|
||||||
- 文字弯曲/弧形排列
|
|
||||||
## 📱 交互增强
|
|
||||||
5. 手势操作
|
|
||||||
|
|
||||||
- 双指缩放画布
|
|
||||||
- 双指旋转元素
|
|
||||||
- 摇一摇撤销
|
|
||||||
6. 语音输入
|
|
||||||
|
|
||||||
- 语音转文字填入输入框
|
|
||||||
7. 实时协作
|
|
||||||
|
|
||||||
- 多人同时编辑同一张邀请函
|
|
||||||
- 分享编辑链接
|
|
||||||
## 💾 数据管理
|
|
||||||
8. 项目管理
|
|
||||||
|
|
||||||
- 保存多个邀请函项目
|
|
||||||
- 项目列表管理(重命名、删除、复制)
|
|
||||||
- 最近编辑记录
|
|
||||||
9. 云端同步
|
|
||||||
|
|
||||||
- 登录账号后云端保存项目
|
|
||||||
- 跨设备同步
|
|
||||||
10. 导入导出
|
|
||||||
|
|
||||||
- 导入 PSD/AI 文件
|
|
||||||
- 导出为可编辑格式(下次可继续修改)
|
|
||||||
## 🎨 视觉效果
|
|
||||||
11. 滤镜效果
|
|
||||||
|
|
||||||
- 背景图滤镜(黑白、复古、模糊等)
|
|
||||||
- 层叠图滤镜
|
|
||||||
12. 动画效果
|
|
||||||
|
|
||||||
- 保存前预览动画效果
|
|
||||||
- 元素入场动画
|
|
||||||
13. 主题系统
|
|
||||||
|
|
||||||
- 深色/浅色模式切换
|
|
||||||
- 节日主题皮肤
|
|
||||||
## 📤 分享功能
|
|
||||||
14. 一键分享
|
|
||||||
|
|
||||||
- 生成分享海报
|
|
||||||
- 生成小程序码
|
|
||||||
- 直接分享到微信/朋友圈
|
|
||||||
15. 统计功能
|
|
||||||
|
|
||||||
- 查看邀请函被查看次数
|
|
||||||
- RSVP RSVP 回复收集
|
|
||||||
## 🔒 安全与隐私
|
|
||||||
16. 隐私保护
|
|
||||||
- 密码保护邀请函
|
|
||||||
- 有效期设置
|
|
||||||
- 水印功能
|
|
||||||
332
js/script.js
332
js/script.js
@@ -1,4 +1,4 @@
|
|||||||
// 极简邀请函制作工具
|
// 极简邀请函制作工具 - 性能优化版本
|
||||||
(function() {
|
(function() {
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const addImageBtn = document.getElementById('addImageBtn');
|
const addImageBtn = document.getElementById('addImageBtn');
|
||||||
@@ -69,6 +69,143 @@
|
|||||||
let lastSavedState = null;
|
let lastSavedState = null;
|
||||||
const SAVE_DELAY = 300; // 防抖延迟 300ms
|
const SAVE_DELAY = 300; // 防抖延迟 300ms
|
||||||
|
|
||||||
|
// ==================== 性能优化:图片缓存系统 ====================
|
||||||
|
const imageCache = new Map();
|
||||||
|
const MAX_CACHE_SIZE = 20; // 最大缓存图片数量
|
||||||
|
|
||||||
|
// 获取缓存图片,如果不存在则加载并缓存
|
||||||
|
async function getCachedImage(src) {
|
||||||
|
if (imageCache.has(src)) {
|
||||||
|
return imageCache.get(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理旧缓存
|
||||||
|
if (imageCache.size >= MAX_CACHE_SIZE) {
|
||||||
|
const firstKey = imageCache.keys().next().value;
|
||||||
|
imageCache.delete(firstKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.onload = () => {
|
||||||
|
imageCache.set(src, img);
|
||||||
|
resolve(img);
|
||||||
|
};
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理图片缓存
|
||||||
|
function clearImageCache() {
|
||||||
|
imageCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 性能优化:状态压缩 ====================
|
||||||
|
// 压缩状态数据,减少内存占用
|
||||||
|
function compressState(state) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
// 压缩文本内容,限制长度
|
||||||
|
textInputs: state.textInputs.map(input => ({
|
||||||
|
...input,
|
||||||
|
content: input.content.length > 500 ? input.content.substring(0, 500) : input.content
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 性能优化:节流与防抖工具 ====================
|
||||||
|
function throttle(func, limit) {
|
||||||
|
let inThrottle;
|
||||||
|
return function(...args) {
|
||||||
|
if (!inThrottle) {
|
||||||
|
func.apply(this, args);
|
||||||
|
inThrottle = true;
|
||||||
|
setTimeout(() => inThrottle = false, limit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function(...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 性能优化:Loading 管理器 ====================
|
||||||
|
const loadingManager = {
|
||||||
|
show: (message = '处理中...') => {
|
||||||
|
let loadingEl = document.getElementById('globalLoading');
|
||||||
|
if (!loadingEl) {
|
||||||
|
loadingEl = document.createElement('div');
|
||||||
|
loadingEl.id = 'globalLoading';
|
||||||
|
loadingEl.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 99999;
|
||||||
|
`;
|
||||||
|
loadingEl.innerHTML = `
|
||||||
|
<div style="
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<div class="loading-spinner" style="
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #4a90e2;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
"></div>
|
||||||
|
<div class="loading-text" style="color: #333; font-size: 14px;">${message}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(loadingEl);
|
||||||
|
|
||||||
|
// 添加动画样式
|
||||||
|
if (!document.getElementById('loadingAnimation')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'loadingAnimation';
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadingEl.querySelector('.loading-text').textContent = message;
|
||||||
|
loadingEl.style.display = 'flex';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hide: () => {
|
||||||
|
const loadingEl = document.getElementById('globalLoading');
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateMessage: (message) => {
|
||||||
|
const loadingEl = document.getElementById('globalLoading');
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.querySelector('.loading-text').textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
init();
|
init();
|
||||||
|
|
||||||
@@ -321,9 +458,34 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 撤销重做功能 ====================
|
// ==================== 撤销重做功能(性能优化版) ====================
|
||||||
|
|
||||||
|
// 内存使用监控
|
||||||
|
let memoryMonitor = {
|
||||||
|
lastCheck: 0,
|
||||||
|
checkInterval: 30000, // 30秒检查一次
|
||||||
|
check: function() {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this.lastCheck < this.checkInterval) return;
|
||||||
|
this.lastCheck = now;
|
||||||
|
|
||||||
|
// 如果历史记录过多,主动清理
|
||||||
|
if (undoStack.length > MAX_HISTORY * 0.8) {
|
||||||
|
console.log('内存优化:清理旧历史记录');
|
||||||
|
while (undoStack.length > MAX_HISTORY * 0.6) {
|
||||||
|
undoStack.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理图片缓存
|
||||||
|
if (imageCache.size > MAX_CACHE_SIZE * 0.8) {
|
||||||
|
console.log('内存优化:清理图片缓存');
|
||||||
|
clearImageCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 保存当前状态(带防抖)
|
// 保存当前状态(带防抖和压缩)
|
||||||
function saveState(immediate = false) {
|
function saveState(immediate = false) {
|
||||||
if (isUndoing) return;
|
if (isUndoing) return;
|
||||||
|
|
||||||
@@ -334,6 +496,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const doSave = () => {
|
const doSave = () => {
|
||||||
|
// 内存检查
|
||||||
|
memoryMonitor.check();
|
||||||
|
|
||||||
const state = captureState();
|
const state = captureState();
|
||||||
|
|
||||||
// 如果新状态与上一个状态相同,不保存
|
// 如果新状态与上一个状态相同,不保存
|
||||||
@@ -341,8 +506,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
undoStack.push(state);
|
// 压缩状态后保存
|
||||||
lastSavedState = JSON.parse(JSON.stringify(state));
|
const compressedState = compressState(state);
|
||||||
|
undoStack.push(compressedState);
|
||||||
|
lastSavedState = JSON.parse(JSON.stringify(compressedState));
|
||||||
|
|
||||||
// 限制历史记录数量
|
// 限制历史记录数量
|
||||||
if (undoStack.length > MAX_HISTORY) {
|
if (undoStack.length > MAX_HISTORY) {
|
||||||
@@ -365,7 +532,7 @@
|
|||||||
// 捕获当前状态
|
// 捕获当前状态
|
||||||
function captureState() {
|
function captureState() {
|
||||||
return {
|
return {
|
||||||
// 背景图状态
|
// 背景图状态 - 使用图片标识而非完整 dataURL
|
||||||
bgImageSrc: bgImage.src,
|
bgImageSrc: bgImage.src,
|
||||||
bgImageScale: bgImageScale,
|
bgImageScale: bgImageScale,
|
||||||
bgImageOpacity: bgImageOpacity,
|
bgImageOpacity: bgImageOpacity,
|
||||||
@@ -398,13 +565,29 @@
|
|||||||
fontWeight: el.style.fontWeight,
|
fontWeight: el.style.fontWeight,
|
||||||
fontStyle: el.style.fontStyle,
|
fontStyle: el.style.fontStyle,
|
||||||
isLocked: el.dataset.locked === 'true'
|
isLocked: el.dataset.locked === 'true'
|
||||||
}))
|
})),
|
||||||
|
|
||||||
|
// 添加时间戳用于状态管理
|
||||||
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 比较两个状态是否相同
|
// 比较两个状态是否相同(优化版)
|
||||||
function statesEqual(state1, state2) {
|
function statesEqual(state1, state2) {
|
||||||
return JSON.stringify(state1) === JSON.stringify(state2);
|
// 快速检查:时间戳
|
||||||
|
if (state1.timestamp && state2.timestamp && Math.abs(state1.timestamp - state2.timestamp) > 1000) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较关键属性
|
||||||
|
if (state1.bgImageSrc !== state2.bgImageSrc) return false;
|
||||||
|
if (state1.bgImageScale !== state2.bgImageScale) return false;
|
||||||
|
if (state1.imageOffsetX !== state2.imageOffsetX) return false;
|
||||||
|
|
||||||
|
// 使用 JSON 比较完整状态
|
||||||
|
const s1 = JSON.stringify({ ...state1, timestamp: undefined });
|
||||||
|
const s2 = JSON.stringify({ ...state2, timestamp: undefined });
|
||||||
|
return s1 === s2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 撤销
|
// 撤销
|
||||||
@@ -1313,8 +1496,8 @@
|
|||||||
|
|
||||||
function handleImageEnd() {
|
function handleImageEnd() {
|
||||||
if (isDraggingImage) {
|
if (isDraggingImage) {
|
||||||
// 保存状态
|
// 保存状态(拖拽结束只保存一次)
|
||||||
saveState();
|
saveState(true);
|
||||||
}
|
}
|
||||||
isDraggingImage = false;
|
isDraggingImage = false;
|
||||||
isInteractingWithBgImage = false;
|
isInteractingWithBgImage = false;
|
||||||
@@ -1480,8 +1663,8 @@
|
|||||||
element.classList.remove('dragging');
|
element.classList.remove('dragging');
|
||||||
hideGuides();
|
hideGuides();
|
||||||
|
|
||||||
// 保存状态
|
// 保存状态(拖拽结束只保存一次,使用 immediate 模式)
|
||||||
saveState();
|
saveState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阻止事件冒泡
|
// 阻止事件冒泡
|
||||||
@@ -2569,8 +2752,9 @@
|
|||||||
return /MicroMessenger/i.test(navigator.userAgent);
|
return /MicroMessenger/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成画布(高清无损,保持与显示容器相同的宽高比)
|
// 生成画布(高清无损,保持与显示容器相同的宽高比)- 性能优化版
|
||||||
async function generateCanvas() {
|
async function generateCanvas() {
|
||||||
|
const startTime = performance.now();
|
||||||
const canvas = exportCanvas;
|
const canvas = exportCanvas;
|
||||||
const container = imageContainer;
|
const container = imageContainer;
|
||||||
|
|
||||||
@@ -2621,6 +2805,11 @@
|
|||||||
console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT, 'dpr:', dpr, '微信:', isWechat());
|
console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT, 'dpr:', dpr, '微信:', isWechat());
|
||||||
console.log('显示容器尺寸:', displayWidth, 'x', displayHeight);
|
console.log('显示容器尺寸:', displayWidth, 'x', displayHeight);
|
||||||
|
|
||||||
|
// 更新 Loading 提示
|
||||||
|
if (document.getElementById('globalLoading')) {
|
||||||
|
loadingManager.updateMessage('正在初始化画布...');
|
||||||
|
}
|
||||||
|
|
||||||
// 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制
|
// 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制
|
||||||
canvas.removeAttribute('hidden');
|
canvas.removeAttribute('hidden');
|
||||||
canvas.style.position = 'fixed';
|
canvas.style.position = 'fixed';
|
||||||
@@ -2657,13 +2846,26 @@
|
|||||||
const scale = TARGET_WIDTH / displayWidth;
|
const scale = TARGET_WIDTH / displayWidth;
|
||||||
console.log('统一缩放比例:', scale, '画布宽高比:', TARGET_WIDTH/TARGET_HEIGHT, '容器宽高比:', displayRatio);
|
console.log('统一缩放比例:', scale, '画布宽高比:', TARGET_WIDTH/TARGET_HEIGHT, '容器宽高比:', displayRatio);
|
||||||
|
|
||||||
|
// 绘制背景图
|
||||||
if (currentImage) {
|
if (currentImage) {
|
||||||
|
if (document.getElementById('globalLoading')) {
|
||||||
|
loadingManager.updateMessage('正在绘制背景...');
|
||||||
|
}
|
||||||
console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight);
|
console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight);
|
||||||
drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scale);
|
drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绘制层叠图
|
||||||
|
if (document.getElementById('globalLoading')) {
|
||||||
|
loadingManager.updateMessage('正在绘制层叠图...');
|
||||||
|
}
|
||||||
await drawOverlayImagesHD(ctx, scale);
|
await drawOverlayImagesHD(ctx, scale);
|
||||||
|
|
||||||
|
// 绘制文字
|
||||||
|
if (document.getElementById('globalLoading')) {
|
||||||
|
loadingManager.updateMessage('正在绘制文字...');
|
||||||
|
}
|
||||||
|
|
||||||
// 等待所有字体加载完成
|
// 等待所有字体加载完成
|
||||||
const fontPromises = [];
|
const fontPromises = [];
|
||||||
const textInputs = container.querySelectorAll('.preview-input-layer .text-input');
|
const textInputs = container.querySelectorAll('.preview-input-layer .text-input');
|
||||||
@@ -2684,6 +2886,10 @@
|
|||||||
textInputs.forEach(textInput => {
|
textInputs.forEach(textInput => {
|
||||||
drawTextOnCanvasHD(ctx, textInput, scale);
|
drawTextOnCanvasHD(ctx, textInput, scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 记录总耗时
|
||||||
|
const endTime = performance.now();
|
||||||
|
console.log(`画布生成总耗时: ${(endTime - startTime).toFixed(2)}ms`);
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
@@ -2818,29 +3024,29 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制层叠图(高清版本)
|
// 绘制层叠图(高清版本)- 性能优化版,使用图片缓存
|
||||||
function drawOverlayImagesHD(ctx, scale) {
|
async function drawOverlayImagesHD(ctx, scale) {
|
||||||
return new Promise((resolve) => {
|
const overlayImages = overlayLayer.querySelectorAll('.overlay-image');
|
||||||
const overlayImages = overlayLayer.querySelectorAll('.overlay-image');
|
if (overlayImages.length === 0) {
|
||||||
if (overlayImages.length === 0) {
|
return;
|
||||||
resolve();
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadedCount = 0;
|
const totalImages = overlayImages.length;
|
||||||
const totalImages = overlayImages.length;
|
let processedCount = 0;
|
||||||
|
|
||||||
function checkComplete() {
|
// 批量处理,每批最多5个
|
||||||
loadedCount++;
|
const BATCH_SIZE = 5;
|
||||||
if (loadedCount >= totalImages) {
|
const batches = [];
|
||||||
resolve();
|
|
||||||
}
|
for (let i = 0; i < overlayImages.length; i += BATCH_SIZE) {
|
||||||
}
|
batches.push(Array.from(overlayImages).slice(i, i + BATCH_SIZE));
|
||||||
|
}
|
||||||
overlayImages.forEach(overlayEl => {
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
await Promise.all(batch.map(async (overlayEl) => {
|
||||||
const imgEl = overlayEl.querySelector('img');
|
const imgEl = overlayEl.querySelector('img');
|
||||||
if (!imgEl) {
|
if (!imgEl) {
|
||||||
checkComplete();
|
processedCount++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2862,25 +3068,33 @@
|
|||||||
const isMirroredH = imgTransform.includes('scaleX(-1)');
|
const isMirroredH = imgTransform.includes('scaleX(-1)');
|
||||||
const isMirroredV = imgTransform.includes('scaleY(-1)');
|
const isMirroredV = imgTransform.includes('scaleY(-1)');
|
||||||
|
|
||||||
// 直接使用已加载的图片元素,避免重新加载导致质量下降
|
try {
|
||||||
if (imgEl.complete && imgEl.naturalWidth > 0) {
|
// 优先使用缓存的图片
|
||||||
drawImageWithTransform(ctx, imgEl, left, top, width, height, rotateDeg, isMirroredH, isMirroredV);
|
let imgToDraw;
|
||||||
|
if (imgEl.complete && imgEl.naturalWidth > 0) {
|
||||||
|
imgToDraw = imgEl;
|
||||||
|
} else {
|
||||||
|
// 尝试从缓存获取
|
||||||
|
imgToDraw = await getCachedImage(imgEl.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawImageWithTransform(ctx, imgToDraw, left, top, width, height, rotateDeg, isMirroredH, isMirroredV);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('层叠图绘制失败:', err);
|
||||||
|
} finally {
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
checkComplete();
|
processedCount++;
|
||||||
} else {
|
|
||||||
// 如果图片未加载完成,等待加载
|
// 更新 Loading 进度
|
||||||
imgEl.onload = () => {
|
if (document.getElementById('globalLoading') && processedCount % 3 === 0) {
|
||||||
drawImageWithTransform(ctx, imgEl, left, top, width, height, rotateDeg, isMirroredH, isMirroredV);
|
loadingManager.updateMessage(`正在绘制层叠图 (${processedCount}/${totalImages})...`);
|
||||||
ctx.globalAlpha = 1;
|
}
|
||||||
checkComplete();
|
|
||||||
};
|
|
||||||
imgEl.onerror = () => {
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
checkComplete();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
|
// 每批处理后让出主线程,避免阻塞
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制带旋转和镜像变换的图片
|
// 绘制带旋转和镜像变换的图片
|
||||||
@@ -3018,19 +3232,33 @@
|
|||||||
ctx.fillText(text, inputLeft, inputTop + textOffsetY);
|
ctx.fillText(text, inputLeft, inputTop + textOffsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存图片
|
// 保存图片(带 Loading 提示)
|
||||||
async function saveImage() {
|
async function saveImage() {
|
||||||
if (!currentImage) {
|
if (!currentImage) {
|
||||||
alert('请先添加背景图片');
|
alert('请先添加背景图片');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示 Loading
|
||||||
|
loadingManager.show('正在生成图片...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const startTime = performance.now();
|
||||||
const canvas = await generateCanvas();
|
const canvas = await generateCanvas();
|
||||||
|
|
||||||
|
// 更新 Loading 提示
|
||||||
|
loadingManager.updateMessage('正在准备保存...');
|
||||||
|
|
||||||
await showMobileSaveTip(canvas);
|
await showMobileSaveTip(canvas);
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
console.log(`图片保存耗时: ${(endTime - startTime).toFixed(2)}ms`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('保存失败:', err);
|
console.error('保存失败:', err);
|
||||||
alert('保存失败,请重试');
|
alert('保存失败,请重试');
|
||||||
|
} finally {
|
||||||
|
// 隐藏 Loading
|
||||||
|
loadingManager.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user