[{"data":1,"prerenderedAt":1720},["ShallowReactive",2],{"blog-taxonomies":3,"blog-stats":507,"post-2025\u002F2025-12-29-记canvas画布项目开发":1083},[4,11,16,21,25,29,36,39,44,47,52,58,63,66,71,76,80,84,87,93,96,101,105,110,113,117,122,127,132,136,141,143,146,151,155,157,162,165,169,173,176,179,183,186,190,193,197,200,205,208,211,214,219,222,228,234,237,239,245,249,253,256,260,266,271,276,281,283,287,290,293,298,301,304,306,308,311,314,316,318,321,327,332,337,342,347,352,354,356,358,361,365,367,369,371,374,376,378,381,386,390,393,396,400,404,407,411,413,415,417,421,423,425,428,431,433,435,437,439,441,443,446,448,450,454,456,459,461,463,465,467,469,471,473,477,483,486,488,491,493,495,497,499,501,503,505],{"category":5,"tags":6},"笔记",[7,8,9,10],"QQ","插件","LiteLoader","美化",{"category":12,"tags":13},"其他",[14,15,10],"博客","IP签名",{"category":17,"tags":18},"杂谈",[19,20],"互联网","观点",{"category":5,"tags":22},[14,23,24],"GitHub","数据可视化",{"category":5,"tags":26},[14,27,28],"访问统计","不蒜子",{"category":30,"tags":31},"工具",[32,33,34,35],"Python","爬虫","选课脚本","广州大学",{"category":5,"tags":37},[38,5],"数学分析",{"category":40,"tags":41},"算法",[42,40,43],"数据结构","线性表",{"category":40,"tags":45},[42,46],"复习",{"category":5,"tags":48},[49,50,51],"高等代数","线性代数","知识点",{"category":5,"tags":53},[14,54,55,56,57],"Typora","PicGo","图床","Markdown",{"category":30,"tags":59},[60,61,62],"Jupyter","工具配置","中文",{"category":30,"tags":64},[65,61,10],"VSCode",{"category":30,"tags":67},[68,69,70,35],"路由器","OpenWrt","校园网",{"category":12,"tags":72},[73,74,75],"网络协议","Wireshark","DEVP2P",{"category":5,"tags":77},[73,74,78,79],"Lua","解码器",{"category":5,"tags":81},[82,5,83],"常微分方程","数学",{"category":5,"tags":85},[86,5,83],"概率论",{"category":30,"tags":88},[89,90,91,92],"Docker","WSL","Windows","磁盘清理",{"category":30,"tags":94},[32,33,95,35],"课程表",{"category":5,"tags":97},[14,98,99,100],"Hexo","Gitalk","评论系统",{"category":5,"tags":102},[14,103,98,104],"Obsidian","写作工具",{"category":12,"tags":106},[107,108,109],"课题研究","泊松分布","参数估计",{"category":12,"tags":111},[107,108,109,112],"组会",{"category":12,"tags":114},[109,115,108,116],"IM模型","统计学",{"category":17,"tags":118},[119,120,121],"软考","网络规划","信息系统项目管理",{"category":30,"tags":123},[124,125,126],"Arch Linux","AppImage","Linux",{"category":5,"tags":128},[129,130,131],"编程语言","计算机科学","内存管理",{"category":5,"tags":133},[134,135],"Canvas","前端",{"category":5,"tags":137},[14,138,139,140],"GitHub Actions","Vite","部署",{"category":5,"tags":142},[135],{"category":5,"tags":144},[135,145],"系统设计",{"category":5,"tags":147},[135,148,149,150],"安全","XSS","CSRF",{"category":152,"tags":153},"项目",[134,154],"PixiJS",{"category":152,"tags":156},null,{"category":40,"tags":158},[42,159,160,161],"栈","队列","JavaScript",{"category":40,"tags":163},[42,164,161],"哈希表",{"category":5,"tags":166},[167,161,168],"ES6","Set",{"category":40,"tags":170},[40,171,172],"动态规划","背包问题",{"category":40,"tags":174},[40,175],"滑动窗口",{"category":40,"tags":177},[42,178,161],"链表",{"category":40,"tags":180},[40,181,161,182],"ACM","面试",{"category":40,"tags":184},[40,161,185],"数据类型转换",{"category":40,"tags":187},[42,188,178,189],"顺序表","TypeScript",{"category":5,"tags":191},[135,154,134,192],"渲染",{"category":5,"tags":194},[135,195,134,196],"富文本编辑","wangEditor",{"category":40,"tags":198},[199,40,178],"力扣",{"category":5,"tags":201},[202,203,204,91],"DNS","加密","网络安全",{"category":5,"tags":206},[135,207],"Axios",{"category":5,"tags":209},[135,207,189,210],"接口封装",{"category":5,"tags":212},[135,213,145],"JWT",{"category":152,"tags":215},[134,216,217,154,218],"画布","React","项目开发",{"category":17,"tags":220},[221],"日志",{"category":5,"tags":223},[224,225,226,227],"Neo4j","图数据库","中心度分析","PageRank",{"category":12,"tags":229},[230,231,232,233],"论文","机器学习","随机森林","保险欺诈",{"category":5,"tags":235},[236],"人工智能",{"category":5,"tags":238},[231,40,32],{"category":12,"tags":240},[241,242,243,244],"共识算法","最长链","区块链","算法设计",{"category":5,"tags":246},[161,247,248,135],"闭包","作用域",{"category":5,"tags":250},[14,138,251,252],"RSS","自动化",{"category":30,"tags":254},[89,90,91,255],"安装",{"category":30,"tags":257},[258,91,259],"MSYS2","开发环境",{"category":5,"tags":261},[262,263,264,265],"团队协作","Git","Commit规范","代码审查",{"category":30,"tags":267},[268,269,270],"oh-my-posh","终端美化","CLI",{"category":5,"tags":272},[273,274,275],"上下文工程","Agent","RAG",{"category":5,"tags":277},[278,279,280],"前端工程化","技术选型","工程基建",{"category":5,"tags":282},[275],{"category":5,"tags":284},[285,286],"SDD","规格驱动开发",{"category":5,"tags":288},[289],"neovim",{"category":5,"tags":291},[292],"技术写作",{"category":5,"tags":294},[295,32,296,89,297],"FastAPI","后端","工程实践",{"category":152,"tags":299},[274,300],"Harness Engineering",{"category":5,"tags":302},[303],"RFC",{"category":152,"tags":305},[274,300,275],{"category":152,"tags":307},[274],{"category":40,"tags":309},[199,40,161,310],"两数之和",{"category":40,"tags":312},[199,40,161,313],"双指针",{"category":40,"tags":315},[199,40,161,178],{"category":40,"tags":317},[199,40,161,175],{"category":40,"tags":319},[199,40,161,320],"回溯",{"category":5,"tags":322},[323,324,325,326],"分布式处理与计算","Spark","Scala","RDD",{"category":5,"tags":328},[323,329,330,331],"线性回归","逻辑回归","监督学习",{"category":5,"tags":333},[323,334,335,336],"数值优化","牛顿法","收敛性",{"category":5,"tags":338},[323,339,340,341],"PCA","数据降维","正则化",{"category":5,"tags":343},[323,344,345,346],"聚类","层次聚类","K-Means",{"category":5,"tags":348},[323,349,350,351],"随机模拟","统计推断","EM算法",{"category":5,"tags":353},[24],{"category":5,"tags":355},[24],{"category":5,"tags":357},[24],{"category":40,"tags":359},[42,360],"图论",{"category":40,"tags":362},[42,363,364],"树","二叉树",{"category":40,"tags":366},[42,160],{"category":40,"tags":368},[42],{"category":40,"tags":370},[42,159],{"category":40,"tags":372},[42,40,373],"复杂度分析",{"category":40,"tags":375},[42,43,178],{"category":40,"tags":377},[42,43,188],{"category":40,"tags":379},[42,380],"基础概念",{"category":17,"tags":382},[383,384,385],"随记","照片","高中",{"category":17,"tags":387},[383,388,389],"团建","战地",{"category":17,"tags":391},[383,392],"生活",{"category":17,"tags":394},[383,395],"文学",{"category":17,"tags":397},[383,398,399],"SCP","模因",{"category":17,"tags":401},[383,402,403],"画集","艺术",{"category":17,"tags":405},[14,406,383],"写作",{"category":17,"tags":408},[383,409,410],"音乐","摇滚",{"category":17,"tags":412},[383,392],{"category":17,"tags":414},[383,409],{"category":17,"tags":416},[383,35,392],{"category":17,"tags":418},[383,419,420],"同人","手书",{"category":17,"tags":422},[383,392],{"category":17,"tags":424},[383,392],{"category":17,"tags":426},[383,427],"植物",{"category":17,"tags":429},[383,430],"读书",{"category":17,"tags":432},[383,392],{"category":17,"tags":434},[383,392],{"category":17,"tags":436},[383,392],{"category":17,"tags":438},[383,392],{"category":17,"tags":440},[383,392],{"category":17,"tags":442},[383,392],{"category":17,"tags":444},[14,445,383],"一周年",{"category":17,"tags":447},[383,392],{"category":17,"tags":449},[383,392],{"category":17,"tags":451},[35,452,453],"开源组织","SITE-193",{"category":17,"tags":455},[383,392],{"category":17,"tags":457},[383,458],"年末",{"category":17,"tags":460},[383,392],{"category":17,"tags":462},[383,392],{"category":17,"tags":464},[14,17],{"category":17,"tags":466},[383,392],{"category":17,"tags":468},[383,392],{"category":17,"tags":470},[383,392],{"category":17,"tags":472},[383,392],{"category":17,"tags":474},[383,475,476],"游戏","Control",{"category":30,"tags":478},[479,480,481,482],"Quadim","图像处理","四叉树","Rust",{"category":17,"tags":484},[14,485],"上线纪念",{"category":17,"tags":487},[383,395],{"category":30,"tags":489},[203,490],"测试",{"category":17,"tags":492},[383],{"category":17,"tags":494},[383,392],{"category":17,"tags":496},[383],{"category":5,"tags":498},[40],{"category":5,"tags":500},[40],{"category":5,"tags":502},[40],{"category":5,"tags":504},[40],{"category":5,"tags":506},[40],[508,512,516,520,524,528,532,536,540,544,548,552,556,560,564,568,572,576,579,583,587,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646,650,654,658,662,666,670,674,678,682,686,690,694,698,702,706,710,714,718,722,726,730,734,738,742,746,750,754,758,762,766,770,774,778,782,786,790,794,798,801,805,809,813,817,821,825,829,833,837,841,845,849,853,857,861,865,869,873,877,881,885,889,893,897,901,904,907,911,915,919,923,927,930,934,938,942,946,950,954,957,961,965,969,973,977,981,984,988,992,996,1000,1004,1007,1011,1015,1019,1023,1027,1031,1035,1039,1043,1047,1051,1055,1059,1063,1067,1071,1075,1079],{"path":509,"words":510,"published":511,"date":156},"\u002Fposts\u002F2024\u002F2024-06-25-liteloaderqqnt",1296,"2024-06-25 11:03:08",{"path":513,"words":514,"published":515,"date":156},"\u002Fposts\u002F2024\u002F2024-06-26-ip签名",0,"2024-06-26 22:53:53",{"path":517,"words":518,"published":519,"date":156},"\u002Fposts\u002F2024\u002F2024-06-29-何加盐中文互联网正在加速崩塌",4853,"2024-06-29 00:47:13",{"path":521,"words":522,"published":523,"date":156},"\u002Fposts\u002F2024\u002F2024-07-12-github-chart",270,"2024-07-12 20:02:50",{"path":525,"words":526,"published":527,"date":156},"\u002Fposts\u002F2024\u002F2024-07-14-访客数统计",192,"2024-07-24 01:34:28",{"path":529,"words":530,"published":531,"date":156},"\u002Fposts\u002F2024\u002F2024-07-26-广大选课脚本",110,"2024-07-26 01:34:28",{"path":533,"words":534,"published":535,"date":156},"\u002Fposts\u002F2024\u002F2024-07-26-数学分析笔记其二",14,"2024-07-26 23:54:02",{"path":537,"words":538,"published":539,"date":156},"\u002Fposts\u002F2024\u002F2024-07-26-数据结构复习其二",12856,"2024-07-26 02:48:08",{"path":541,"words":542,"published":543,"date":156},"\u002Fposts\u002F2024\u002F2024-07-26-数据结构复习相关",32856,"2024-07-26 02:24:14",{"path":545,"words":546,"published":547,"date":156},"\u002Fposts\u002F2024\u002F2024-07-26-线性代数与空间解析几何知识点全汇总",6110,"2024-07-26 03:27:55",{"path":549,"words":550,"published":551,"date":156},"\u002Fposts\u002F2024\u002F2024-07-31-typora-picgo-兰空图床打造markdown写作环境",311,"2024-07-31 12:56:23",{"path":553,"words":554,"published":555,"date":156},"\u002Fposts\u002F2024\u002F2024-09-02-jupyter配置中文",441,"2024-09-02 23:49:05",{"path":557,"words":558,"published":559,"date":156},"\u002Fposts\u002F2024\u002F2024-09-02-自定义vscode背景图片",100,"2024-09-02 23:39:00",{"path":561,"words":562,"published":563,"date":156},"\u002Fposts\u002F2024\u002F2024-09-03-制作gzhu校园网路由器",1423,"2024-09-03 23:43:18",{"path":565,"words":566,"published":567,"date":156},"\u002Fposts\u002F2024\u002F2024-09-26-09-25-2024",136,"2024-09-26 00:49:26",{"path":569,"words":570,"published":571,"date":156},"\u002Fposts\u002F2024\u002F2024-11-04-创建一个网络包解码器分析devp2p协议",1943,"2024-11-04 16:45:19",{"path":573,"words":574,"published":575,"date":156},"\u002Fposts\u002F2024\u002F2025-03-09-2024一些笔记常微分",16,"2024-12-09 22:51:00",{"path":577,"words":574,"published":578,"date":156},"\u002Fposts\u002F2024\u002F2025-03-09-2024一些笔记概率论","2025-03-09 22:51:00",{"path":580,"words":581,"published":582,"date":156},"\u002Fposts\u002F2025\u002F2025-03-01-windows下释放docker占用的wsl空间",164,"2025-03-01 13:54:15",{"path":584,"words":585,"published":586,"date":156},"\u002Fposts\u002F2025\u002F2025-03-01-爬虫实战-爬取广州大学课程表",774,"2025-03-01 14:55:48",{"path":588,"words":530,"published":589,"date":156},"\u002Fposts\u002F2025\u002F2025-04-03-hexo集成gitalk的问题","2025-04-03 14:26:34",{"path":591,"words":592,"published":593,"date":156},"\u002Fposts\u002F2025\u002F2025-09-22-记一次配置obsidian配合hexo写博客",92,"2025-09-22 16:47:52",{"path":595,"words":596,"published":597,"date":156},"\u002Fposts\u002F2025\u002F2025-10-24-10-20课题",204,"2025-10-24 08:15:01",{"path":599,"words":600,"published":601,"date":156},"\u002Fposts\u002F2025\u002F2025-11-02-组会朝花夕拾",501,"2025-11-02 18:56:59",{"path":603,"words":604,"published":605,"date":156},"\u002Fposts\u002F2025\u002F2025-11-05-基于随机加权推断模型im的参数估计算法",481,"2025-11-05 20:51:41",{"path":607,"words":608,"published":609,"date":156},"\u002Fposts\u002F2025\u002F2025-11-09-关于软考高项网规",884,"2025-11-09 19:54:52",{"path":611,"words":612,"published":613,"date":156},"\u002Fposts\u002F2025\u002F2025-11-17-arch-linux运行appimage相关",365,"2025-11-17 09:59:59",{"path":615,"words":616,"published":617,"date":156},"\u002Fposts\u002F2025\u002F2025-11-19-相关概念",2058,"2025-11-19 11:06:20",{"path":619,"words":620,"published":621,"date":156},"\u002Fposts\u002F2025\u002F2025-11-21-web可视化实践canvas",6393,"2025-11-21 14:50:48",{"path":623,"words":624,"published":625,"date":156},"\u002Fposts\u002F2025\u002F2025-11-22-使用-github-actions-自动部署基于vite的项目到-github-pages",272,"2025-11-23 01:59:24",{"path":627,"words":628,"published":629,"date":156},"\u002Fposts\u002F2025\u002F2025-11-22-关于前端包管理器npmpnpmyarn和bun",4890,"2025-11-22 21:21:15",{"path":631,"words":632,"published":633,"date":156},"\u002Fposts\u002F2025\u002F2025-11-23-undo-redo-机制具体实现",7839,"2025-11-23 19:15:00",{"path":635,"words":636,"published":637,"date":156},"\u002Fposts\u002F2025\u002F2025-11-27-前端安全-关于xss与crsf",1866,"2025-11-28 02:05:25",{"path":639,"words":640,"published":641,"date":156},"\u002Fposts\u002F2025\u002F2025-12-05-现代协同-2d-画布编辑器pixijs-v8",4285,"2025-12-05 02:00:16",{"path":643,"words":644,"published":645,"date":156},"\u002Fposts\u002F2025\u002F2025-12-07-杂谈-课题组系统设计",3710,"2025-12-07 21:09:33",{"path":647,"words":648,"published":649,"date":156},"\u002Fposts\u002F2025\u002F2025-12-25-javascript中的数组方法与栈stack和队列queue的实现",474,"2025-11-29 16:58:15",{"path":651,"words":652,"published":653,"date":156},"\u002Fposts\u002F2025\u002F2025-12-25-关于javascript-实现哈希表",189,"2025-11-27 11:31:39",{"path":655,"words":656,"published":657,"date":156},"\u002Fposts\u002F2025\u002F2025-12-25-关于javascript的set方法",332,"2025-12-25 15:26:18",{"path":659,"words":660,"published":661,"date":156},"\u002Fposts\u002F2025\u002F2025-12-25-关于动态规划(背包问题为例)",1238,"2025-11-17 18:24:04",{"path":663,"words":664,"published":665,"date":156},"\u002Fposts\u002F2025\u002F2025-12-25-关于滑动窗口",712,"2025-11-20 15:04:40",{"path":667,"words":668,"published":669,"date":156},"\u002Fposts\u002F2025\u002F2025-12-25-关于链表(javascript)",500,"2025-11-25 13:00:48",{"path":671,"words":672,"published":673,"date":156},"\u002Fposts\u002F2025\u002F2025-12-25-面试算法acm模式构建构建输入输出模板",2051,"2025-12-25 23:09:45",{"path":675,"words":676,"published":677,"date":156},"\u002Fposts\u002F2025\u002F2025-12-26-javascript-数字数组字符串的处理",483,"2025-12-26 11:20:25",{"path":679,"words":680,"published":681,"date":156},"\u002Fposts\u002F2025\u002F2025-12-26-javascripttypescript-的顺序表链表实现",446,"2025-12-26 12:16:12",{"path":683,"words":684,"published":685,"date":156},"\u002Fposts\u002F2025\u002F2025-12-27-前端画布设计vol-1-实现基础yuan素渲染和状态控制",2136,"2025-12-28 01:23:08",{"path":687,"words":688,"published":689,"date":156},"\u002Fposts\u002F2025\u002F2025-12-27-前端画布设计vol-2-实现富文本编辑",1838,"2025-12-28 02:58:47",{"path":691,"words":692,"published":693,"date":156},"\u002Fposts\u002F2025\u002F2025-12-27-算法刷题-关于链表操作",2135,"2025-12-27 16:12:42",{"path":695,"words":696,"published":697,"date":156},"\u002Fposts\u002F2025\u002F2025-12-27-配置dnscrypt-proxy实现加密dns服务windows",1899,"2025-11-15 01:06:37",{"path":699,"words":700,"published":701,"date":156},"\u002Fposts\u002F2025\u002F2025-12-28-前端-关于网络请求xhrajaxfetchaxios",7788,"2025-10-28 15:48:46",{"path":703,"words":704,"published":705,"date":156},"\u002Fposts\u002F2025\u002F2025-12-28-前端-接口封装与请求规范axios为例",2373,"2025-12-29 01:01:18",{"path":707,"words":708,"published":709,"date":156},"\u002Fposts\u002F2025\u002F2025-12-28-前端-身份验证管理-基于-jwt-token-的实现",2709,"2025-11-23 22:04:41",{"path":711,"words":712,"published":713,"date":156},"\u002Fposts\u002F2025\u002F2025-12-29-记canvas画布项目开发",6254,"2025-12-29 04:52:15",{"path":715,"words":716,"published":717,"date":156},"\u002Fposts\u002F2025\u002Fabout-site",116,"2025-03-31 16:44:24",{"path":719,"words":720,"published":721,"date":156},"\u002Fposts\u002F2025\u002F使用neo4j图数据科学库gds进行中心度分析",143,"2025-04-02 23:37:24",{"path":723,"words":724,"published":725,"date":156},"\u002Fposts\u002F2026\u002F2026-01-03-论文实训草稿",10445,"2026-01-03 14:10:49",{"path":727,"words":728,"published":729,"date":156},"\u002Fposts\u002F2026\u002F2026-01-06-人工智能导论",11461,"2026-01-07 03:25:49",{"path":731,"words":732,"published":733,"date":156},"\u002Fposts\u002F2026\u002F2026-01-07-机器学习相关算法",82,"2026-01-08 02:28:13",{"path":735,"words":736,"published":737,"date":156},"\u002Fposts\u002F2026\u002F2026-01-16-关于低复杂度最长链共识算法设计",4466,"2026-01-16 10:33:23",{"path":739,"words":740,"published":741,"date":156},"\u002Fposts\u002F2026\u002F2026-01-19-杂记2026-01-19",5754,"2026-01-20 01:03:26",{"path":743,"words":744,"published":745,"date":156},"\u002Fposts\u002F2026\u002F2026-01-22-github-action-自动同步博客到-github-主页",1069,"2026-01-22 16:31:47",{"path":747,"words":748,"published":749,"date":156},"\u002Fposts\u002F2026\u002F2026-03-01-windows-wsl安装docker",257,"2026-03-01 20:55:31",{"path":751,"words":752,"published":753,"date":156},"\u002Fposts\u002F2026\u002F2026-03-14-关于-msys2",701,"2026-03-15 03:05:04",{"path":755,"words":756,"published":757,"date":156},"\u002Fposts\u002F2026\u002F2026-03-28-团队项目协作规范随记",2419,"2026-03-29 05:33:50",{"path":759,"words":760,"published":761,"date":156},"\u002Fposts\u002F2026\u002F2026-03-29-oh-my-posh配置分享",104,"2026-03-29 16:34:43",{"path":763,"words":764,"published":765,"date":156},"\u002Fposts\u002F2026\u002F2026-06-13-从上下文工程到-agent-harness-engineering",8840,"2026-06-13 21:06:20",{"path":767,"words":768,"published":769,"date":156},"\u002Fposts\u002F2026\u002F2026-07-13-简谈前端基建质量标准与ai友好建设",3814,"2026-07-13 21:06:20",{"path":771,"words":772,"published":773,"date":156},"\u002Fposts\u002F2026\u002F2026-07-15-agentic-rag-系统实践",13357,"2026-07-15 21:06:20",{"path":775,"words":776,"published":777,"date":156},"\u002Fposts\u002F2026\u002F2026-07-18-简谈sdd规范驱动开发-copy",4476,"2026-07-18 11:06:20",{"path":779,"words":780,"published":781,"date":156},"\u002Fposts\u002F2026\u002F2026-07-20-个人neovim配置分享",366,"2026-07-22 13:46:50",{"path":783,"words":784,"published":785,"date":156},"\u002Fposts\u002F2026\u002F2026-07-20-关于技术写作-设计文档",6763,"2026-07-20 13:46:50",{"path":787,"words":788,"published":789,"date":156},"\u002Fposts\u002F2026\u002F2026-07-29-fastapi-工程实践随记",3417,"2026-07-29 04:58:34",{"path":791,"words":792,"published":793,"date":156},"\u002Fposts\u002Frfc_agentic_project\u002F2026-06-19-rfc-liskin_code_agent",3563,"2026-06-19 15:18:20",{"path":795,"words":796,"published":797,"date":156},"\u002Fposts\u002Frfc_agentic_project\u002F2026-07-25-rfc-glushkov_ogas",13461,"2026-07-28 23:46:50",{"path":799,"words":800,"published":793,"date":156},"\u002Fposts\u002Frfc_agentic_project\u002F2026-08-03-adr-ogas-arkhiv",2698,{"path":802,"words":803,"published":804,"date":156},"\u002Fposts\u002Frfc_agentic_project\u002Frag\u002F2026-08-12-arkhiv_rag-vol-1",4366,"2026-08-12 15:18:20",{"path":806,"words":807,"published":808,"date":156},"\u002Fposts\u002Fleetcode\u002F2025-12-25-力扣百题速练javascripttypescriptvol-1",3108,"2025-12-25 13:57:08",{"path":810,"words":811,"published":812,"date":156},"\u002Fposts\u002Fleetcode\u002F2025-12-26-力扣百题速练javascripttypescriptvol-2",3185,"2025-12-26 13:57:08",{"path":814,"words":815,"published":816,"date":156},"\u002Fposts\u002Fleetcode\u002F2025-12-28-力扣百题速练javascripttypescriptvol-3",2533,"2025-12-29 02:15:44",{"path":818,"words":819,"published":820,"date":156},"\u002Fposts\u002Fleetcode\u002F2026-01-17-力扣百题速练javascripttypescriptvol-4",3024,"2026-01-17 22:53:01",{"path":822,"words":823,"published":824,"date":156},"\u002Fposts\u002Fleetcode\u002F2026-01-19-力扣百题速练javascripttypescriptvol-5",293,"2026-01-19 15:15:12",{"path":826,"words":827,"published":828,"date":156},"\u002Fposts\u002F分布式处理与计算\u002F分布式处理与计算spark系统与scala语言",4320,"2026-07-06 14:10:49",{"path":830,"words":831,"published":832,"date":156},"\u002Fposts\u002F分布式处理与计算\u002F分布式处理与计算分类与回归分析",2081,"2026-07-07 12:40:29",{"path":834,"words":835,"published":836,"date":156},"\u002Fposts\u002F分布式处理与计算\u002F分布式处理与计算数值优化方法",2182,"2026-07-07 13:50:29",{"path":838,"words":839,"published":840,"date":156},"\u002Fposts\u002F分布式处理与计算\u002F分布式处理与计算数据降维",1583,"2026-07-07 21:50:29",{"path":842,"words":843,"published":844,"date":156},"\u002Fposts\u002F分布式处理与计算\u002F分布式处理与计算聚类分析",1882,"2026-07-07 16:50:29",{"path":846,"words":847,"published":848,"date":156},"\u002Fposts\u002F分布式处理与计算\u002F分布式处理与计算随机模拟与统计推断",2433,"2026-07-06 22:40:29",{"path":850,"words":851,"published":852,"date":156},"\u002Fposts\u002F数据可视化\u002F2026-06-14-数据可视化-基本图像",10011,"2026-06-14 20:18:20",{"path":854,"words":855,"published":856,"date":156},"\u002Fposts\u002F数据可视化\u002F2026-06-14-数据可视化-概论",10030,"2026-06-14 15:18:20",{"path":858,"words":859,"published":860,"date":156},"\u002Fposts\u002F数据可视化\u002F2026-06-15-数据可视化-可视化库",2032,"2026-06-15 20:18:20",{"path":862,"words":863,"published":864,"date":156},"\u002Fposts\u002F数据结构\u002F2025-09-29-数据结构-图",5451,"2025-04-15 20:48:06",{"path":866,"words":867,"published":868,"date":156},"\u002Fposts\u002F数据结构\u002F2025-09-29-数据结构-树",4211,"2025-04-30 02:19:06",{"path":870,"words":871,"published":872,"date":156},"\u002Fposts\u002F数据结构\u002F2025-09-29-数据结构-队列",921,"2025-04-11 15:30:08",{"path":874,"words":875,"published":876,"date":156},"\u002Fposts\u002F数据结构\u002F关于数据结构的一些想法",456,"2025-04-04 19:13:49",{"path":878,"words":879,"published":880,"date":156},"\u002Fposts\u002F数据结构\u002F数据结构-栈",920,"2025-04-18 00:17:37",{"path":882,"words":883,"published":884,"date":156},"\u002Fposts\u002F数据结构\u002F数据结构-算法复杂度",2153,"2025-04-05 16:09:55",{"path":886,"words":887,"published":888,"date":156},"\u002Fposts\u002F数据结构\u002F数据结构-线性表链表",4572,"2025-04-06 00:24:40",{"path":890,"words":891,"published":892,"date":156},"\u002Fposts\u002F数据结构\u002F数据结构-线性表顺序表",4191,"2025-04-05 22:09:59",{"path":894,"words":895,"published":896,"date":156},"\u002Fposts\u002F数据结构\u002F数据结构-绪论",2881,"2025-04-05 11:31:22",{"path":898,"words":899,"published":156,"date":900},"\u002Fposts\u002F杂记\u002F2023-12-26-2023年12月26日",7,"2023-12-26 23:51:33",{"path":902,"words":899,"published":156,"date":903},"\u002Fposts\u002F杂记\u002F2024-01-01-2024团建","2024-01-01 01:51:27",{"path":905,"words":581,"published":156,"date":906},"\u002Fposts\u002F杂记\u002F2024-01-09-随记","2024-01-09 20:50:35",{"path":908,"words":909,"published":156,"date":910},"\u002Fposts\u002F杂记\u002F2024-03-12-花名",5275,"2024-03-12 16:04:23",{"path":912,"words":913,"published":156,"date":914},"\u002Fposts\u002F杂记\u002F2024-04-15-逆模因",1353,"2024-04-15 00:26:16",{"path":916,"words":917,"published":156,"date":918},"\u002Fposts\u002F杂记\u002F2024-05-03-艺作",27,"2024-05-03 00:01:00",{"path":920,"words":921,"published":156,"date":922},"\u002Fposts\u002F杂记\u002F2024-05-21-关于博客",1720,"2024-02-21 23:21:11",{"path":924,"words":925,"published":156,"date":926},"\u002Fposts\u002F杂记\u002F2024-06-20-dnvrc维克多崔专访-2023",2036,"2024-06-20 00:28:39",{"path":928,"words":760,"published":156,"date":929},"\u002Fposts\u002F杂记\u002F2024-06-25-6月25日随记","2024-06-25 12:46:36",{"path":931,"words":932,"published":156,"date":933},"\u002Fposts\u002F杂记\u002F2024-06-25-歌名-想見星星",148,"2024-06-25 22:47:59",{"path":935,"words":936,"published":156,"date":937},"\u002Fposts\u002F杂记\u002F2024-07-11-桂花岗",6,"2024-07-11 00:28:17",{"path":939,"words":940,"published":156,"date":941},"\u002Fposts\u002F杂记\u002F2024-07-15-rolling-bocci",9,"2024-07-15 21:25:56",{"path":943,"words":944,"published":156,"date":945},"\u002Fposts\u002F杂记\u002F2024-07-22-七月九日",34,"2024-07-09 17:54:23",{"path":947,"words":948,"published":156,"date":949},"\u002Fposts\u002F杂记\u002F2024-07-22-六月随记",1052,"2024-07-22 01:03:19",{"path":951,"words":952,"published":156,"date":953},"\u002Fposts\u002F杂记\u002F2024-07-22-石菖蒲",41,"2024-07-10 01:26:05",{"path":955,"words":514,"published":156,"date":956},"\u002Fposts\u002F杂记\u002F2024-07-23-新书","2024-07-23 01:05:49",{"path":958,"words":959,"published":156,"date":960},"\u002Fposts\u002F杂记\u002F2024-07-29-年前",74,"2024-07-29 23:59:07",{"path":962,"words":963,"published":156,"date":964},"\u002Fposts\u002F杂记\u002F2024-07-30-周边",138,"2024-07-30 02:03:23",{"path":966,"words":967,"published":156,"date":968},"\u002Fposts\u002F杂记\u002F2024-07-31-2024-7-31日",185,"2024-07-31 01:21:52",{"path":970,"words":971,"published":156,"date":972},"\u002Fposts\u002F杂记\u002F2024-08-02-八月",1710,"2024-08-30 00:31:02",{"path":974,"words":975,"published":156,"date":976},"\u002Fposts\u002F杂记\u002F2024-09-01-九月",1484,"2024-09-26 16:30:15",{"path":978,"words":979,"published":156,"date":980},"\u002Fposts\u002F杂记\u002F2024-09-08-2024年9月8日",97,"2024-09-08 13:59:36",{"path":982,"words":514,"published":156,"date":983},"\u002Fposts\u002F杂记\u002F2024-09-19-博客站的一周年","2024-09-18 23:36:24",{"path":985,"words":986,"published":156,"date":987},"\u002Fposts\u002F杂记\u002F2024-10-03-十月",1028,"2024-10-31 23:54:27",{"path":989,"words":990,"published":156,"date":991},"\u002Fposts\u002F杂记\u002F2024-11-04-十一月",2134,"2024-11-30 00:04:39",{"path":993,"words":994,"published":156,"date":995},"\u002Fposts\u002F杂记\u002F2024-11-10-gzhu-site-193",80,"2024-11-10 19:19:12",{"path":997,"words":998,"published":156,"date":999},"\u002Fposts\u002F杂记\u002F2024-12-01-十二月",455,"2024-12-30 23:13:47",{"path":1001,"words":1002,"published":156,"date":1003},"\u002Fposts\u002F杂记\u002F2024-12-21-年末",23,"2024-12-21 23:21:49",{"path":1005,"words":522,"published":156,"date":1006},"\u002Fposts\u002F杂记\u002F2025-01-30-一月","2025-01-30 23:44:41",{"path":1008,"words":1009,"published":156,"date":1010},"\u002Fposts\u002F杂记\u002F2025-03-01-二月",224,"2025-02-28 00:11:08",{"path":1012,"words":1013,"published":156,"date":1014},"\u002Fposts\u002F杂记\u002F2025-03-01-博客杂谈",344,"2025-03-01 16:03:18",{"path":1016,"words":1017,"published":156,"date":1018},"\u002Fposts\u002F杂记\u002F2025-03-05-三月",2090,"2025-03-05 19:17:28",{"path":1020,"words":1021,"published":156,"date":1022},"\u002Fposts\u002F杂记\u002F2025-07-10-七月",361,"2025-07-10 22:04:32",{"path":1024,"words":1025,"published":156,"date":1026},"\u002Fposts\u002F杂记\u002F2025-09-21-九月",781,"2025-09-21 20:53:37",{"path":1028,"words":1029,"published":156,"date":1030},"\u002Fposts\u002F杂记\u002F2025-10-09-十月",591,"2025-10-09 00:35:07",{"path":1032,"words":1033,"published":156,"date":1034},"\u002Fposts\u002F杂记\u002Fcontrol",13,"2023-09-25 13:03:59",{"path":1036,"words":1037,"published":156,"date":1038},"\u002Fposts\u002F杂记\u002Fquadim四叉树图像处理",1094,"2023-10-21 11:27:06",{"path":1040,"words":1041,"published":156,"date":1042},"\u002Fposts\u002F杂记\u002Fsuccess",24,"2023-09-22 15:59:07",{"path":1044,"words":1045,"published":156,"date":1046},"\u002Fposts\u002F杂记\u002F三光年",295,"2023-11-18 04:18:40",{"path":1048,"words":1049,"published":156,"date":1050},"\u002Fposts\u002F杂记\u002F加密测试",961,"2023-11-21 14:24:01",{"path":1052,"words":1053,"published":156,"date":1054},"\u002Fposts\u002F杂记\u002F啊啊啊啊啊啊啊",54,"2023-09-20 17:29:33",{"path":1056,"words":1057,"published":156,"date":1058},"\u002Fposts\u002F杂记\u002F杂谈",124,"2023-09-25 23:11:32",{"path":1060,"words":1061,"published":156,"date":1062},"\u002Fposts\u002F杂记\u002F艹",25,"2023-10-21 17:40:43",{"path":1064,"words":1065,"published":1066,"date":156},"\u002Fposts\u002F神经网络\u002F神经网络与深度学习卷积神经网络",3168,"2026-06-07 11:23:08",{"path":1068,"words":1069,"published":1070,"date":156},"\u002Fposts\u002F神经网络\u002F神经网络与深度学习循环神经网络",2348,"2026-06-07 11:33:08",{"path":1072,"words":1073,"published":1074,"date":156},"\u002Fposts\u002F神经网络\u002F神经网络与深度学习改进学习方法",3360,"2026-06-07 10:13:08",{"path":1076,"words":1077,"published":1078,"date":156},"\u002Fposts\u002F神经网络\u002F神经网络与深度学习深度生成模型",2727,"2026-06-07 11:43:08",{"path":1080,"words":1081,"published":1082,"date":156},"\u002Fposts\u002F神经网络\u002F神经网络与深度学习神经网络基础",3205,"2026-06-07 10:03:08",{"id":1084,"title":1085,"abbrlink":1086,"body":1087,"category":152,"cover":156,"date":156,"description":1709,"extension":1710,"mathjax":1711,"meta":1712,"navigation":1711,"path":711,"published":713,"readingMinutes":1715,"seo":1716,"stem":1717,"sticky":156,"swiper_index":156,"tags":1718,"updated":156,"words":712,"__hash__":1719},"posts\u002Fposts\u002F2025\u002F2025-12-29-记canvas画布项目开发.md","2025-12-29-记canvas画布项目开发","2357",{"type":1088,"value":1089,"toc":1670},"minimark",[1090,1101,1104,1109,1114,1117,1121,1124,1127,1131,1134,1141,1147,1153,1159,1170,1176,1179,1186,1190,1193,1202,1222,1226,1229,1236,1239,1253,1257,1260,1264,1267,1271,1286,1295,1298,1304,1312,1318,1321,1325,1328,1332,1335,1339,1342,1345,1349,1352,1358,1361,1365,1372,1378,1384,1388,1395,1403,1406,1410,1413,1419,1422,1426,1430,1437,1443,1446,1449,1453,1456,1459,1465,1468,1474,1477,1483,1486,1493,1496,1500,1504,1507,1510,1513,1520,1523,1530,1536,1543,1547,1554,1560,1563,1567,1571,1574,1577,1583,1586,1592,1595,1599,1602,1608,1611,1615,1618,1622,1625,1632,1639,1643,1646,1649,1655,1658,1661,1664],[1091,1092,1093,1094],"p",{},"GitHub 仓库 ",[1095,1096,1100],"a",{"href":1097,"rel":1098},"https:\u002F\u002Fgithub.com\u002FZhongye1\u002FBDdraw_DEV?referrer=grok.com",[1099],"nofollow","https:\u002F\u002Fgithub.com\u002FZhongye1\u002FBDdraw_DEV",[1091,1102,1103],{},"项目把高性能渲染、无限画布、撤销重做、实时协作这几块比较硬的功能凑到了一起，所以下面的记录也主要围绕性能优化、状态管理、图形渲染、架构设计这几个方向展开，顺带聊一聊实际工程里踩过的坑。",[1105,1106,1108],"h3",{"id":1107},"_1-项目整体介绍与架构设计","1. 项目整体介绍与架构设计",[1110,1111,1113],"h4",{"id":1112},"项目的主要功能目标用户以及它解决了哪些实际问题","项目的主要功能、目标用户以及它解决了哪些实际问题？",[1091,1115,1116],{},"BDdraw_DEV 是一个基于 React + TypeScript + PixiJS 构建的现代化协同 2D 画布编辑器。它支持矩形、圆形、菱形、线条、箭头、画笔等多种基础图形的绘制，每种图形都能设置背景色、边框宽度、边框颜色等属性；除此之外还提供富文本编辑、图片插入与滤镜处理。在画布交互层面，它实现了无限画布的缩放与拖拽、minimap 缩略导航，以及元素的选择、分组、旋转和尺寸调整，并配套了撤销重做、快捷键、数据持久化、本地优先编辑和海量元素处理等能力。",[1110,1118,1120],{"id":1119},"项目架构是如何设计的为什么把-react-用于-ui-层zustand-用于状态管理pixijs-用于渲染层做这样的分离","项目架构是如何设计的？为什么把 React 用于 UI 层、Zustand 用于状态管理、PixiJS 用于渲染层做这样的分离？",[1091,1122,1123],{},"项目的核心是一套三层架构：React 负责 UI 层，Zustand 负责状态层，PixiJS 负责渲染层，整体走的是\"数据驱动视图\"的模式。之所以这么切，是因为这套分层能比较自然地撑起撤销重做、数据持久化和多人协同这些进阶功能。",[1091,1125,1126],{},"具体的好处可以从几个角度看。首先是解耦，渲染层、状态层和逻辑层彼此独立，维护和扩展时不会牵一发动全身。其次是便于协同，所有状态都集中在 Zustand Store 里管理，多人协同时有一个清晰的同步入口。再者是撤销重做容易实现，因为只要保存和恢复 Store 的快照就能完成完整的回退。最后是可持久化，集中管理的状态数据很容易序列化和反序列化，无论是本地保存还是网络传输都很顺。",[1110,1128,1130],{"id":1129},"项目的目录结构是怎么组织的这种模块化设计带来了哪些好处","项目的目录结构是怎么组织的？这种模块化设计带来了哪些好处？",[1091,1132,1133],{},"前端部分主要分成五个模块。",[1091,1135,1136,1140],{},[1137,1138,1139],"code",{},"src\u002Fapi"," 负责 API 客户端和类型定义，也就是前后端通信这一层。它内部又分为 types（API 类型定义）和 utils（API 工具函数），整体是对 API 服务的封装和客户端工具的集合。",[1091,1142,1143,1146],{},[1137,1144,1145],{},"src\u002Fcomponents"," 存放各种 React UI 组件，包括画布工具栏 canvas_toolbar、协作功能 collaboration、页面头部 header、属性面板 property-panel 以及富文本编辑器 richtext_editor。",[1091,1148,1149,1152],{},[1137,1150,1151],{},"src\u002Fhooks"," 是自定义 React Hooks，主要承担两类事情：一是状态管理，用简单的本地存储来保存用户偏好、UI 状态等；二是快捷键处理。",[1091,1154,1155,1158],{},[1137,1156,1157],{},"src\u002Flib"," 是工具库和核心功能模块，命令模式的实现就放在这里，包括 AddElementCommand.ts、RemoveElementCommand.ts 和 UndoRedoManager.ts，此外还有常量定义 constants.ts 和通用工具函数 utils.ts。",[1091,1160,1161,1164,1165,1169],{},[1137,1162,1163],{},"src\u002Fpages"," 是页面组件，分为认证相关的 auth、主页 home 和房间管理 room。其中最重的一块是 ",[1166,1167,1168],"strong",{},"canvas\u002FPixi_STM_modules","，也就是 Pixi.js 的状态管理模块，它内部再细分为 core（核心类和初始化逻辑）、interaction（拖拽、缩放、选择等交互处理）、utils（各项操作的封装）和 shared（共享类型定义）。",[1091,1171,1172,1175],{},[1137,1173,1174],{},"src\u002Fstores"," 是状态管理层，底层是 Yjs + IndexedDB 这套用于协同的复杂数据存储，专门存放画布元素数据，并支撑实时协同和离线编辑。它包含画布状态 canvasStore.ts、持久化状态 persistenceStore.ts 和主题状态 themeStore.ts。",[1091,1177,1178],{},"后端部分的设计相对常规，主要由四块构成：房间管理系统支持房间的创建、修改、删除和查询；用户认证系统提供登录、注册和权限验证；实时协作能力通过 collab.ts 实现；数据库相关的连接和操作则收敛在 db.ts 里。数据库在原型验证阶段用的是 sqlite，每个房间的画布数据存放在对应表的 content 字段中。",[1091,1180,1181],{},[1182,1183],"img",{"alt":1184,"src":1185},"","https:\u002F\u002Fpica.zhimg.com\u002F80\u002Fv2-466ea97d53037f5c8505d87a87c2e52e_720w.webp",[1110,1187,1189],{"id":1188},"项目中如何处理前端与后端ald_backend的交互","项目中如何处理前端与后端（ALD_Backend）的交互？",[1091,1191,1192],{},"项目是前后端分离的，常规交互走 REST API。前端用一层 TypeScript 封装的 API 模块统一管理所有 HTTP 请求，底层以 Axios 作为客户端，并配置了请求和响应拦截器来集中处理认证、错误和加载状态。不同环境的基础 URL 通过环境变量切换，同时定义了统一的响应格式和类型，保证了端到端的类型安全；而在需要实时协作的场景下，则改用 WebSocket 做双向通信。",[1091,1194,1195,1196,1201],{},"这里选 Axios 是因为它是一个基于 Promise 的网络请求库，浏览器和 Node.js 都能用，并且原生支持请求\u002F响应拦截、请求取消、并发请求和数据自动转换等能力，省去了不少自己造轮子的功夫。这部分的接口定义和封装细节，我整理在了另一篇博客里：",[1095,1197,1200],{"href":1198,"rel":1199},"https:\u002F\u002Fzhongye1.github.io\u002Fposts\u002F2025-12-28-%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0-%E6%8E%A5%E5%8F%A3%E5%B0%81%E8%A3%85%E4%B8%8E%E8%AF%B7%E6%B1%82%E8%A7%84%E8%8C%83axios%E4%B8%BA%E4%BE%8B\u002F",[1099],"前端-接口类型定义、Axios 封装与请求规范 | 笔记站","。",[1091,1203,1204,1205,1208,1209,1212,1213,1216,1217,1201],{},"身份验证这块用的是 JWT Token：通过 ",[1137,1206,1207],{},"setAuthToken"," 和 ",[1137,1210,1211],{},"clearAuthToken"," 来管理认证状态的写入与清除，并用 ",[1137,1214,1215],{},"onAuthenticate"," 钩子校验用户权限。具体实现可以看：",[1095,1218,1221],{"href":1219,"rel":1220},"https:\u002F\u002Fzhongye1.github.io\u002Fposts\u002F2025-12-28-%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0-%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81%E7%AE%A1%E7%90%86-%E5%9F%BA%E4%BA%8E-jwt-token-%E7%9A%84%E5%AE%9E%E7%8E%B0\u002F",[1099],"前端-身份验证管理-基于 JWT Token 的实现",[1110,1223,1225],{"id":1224},"实时协作部分是如何实现的","实时协作部分是如何实现的？",[1091,1227,1228],{},"实时协作整体由 Yjs、Hocuspocus 和 IndexedDB 三者配合完成",[1091,1230,1231,1232,1235],{},"前端（React）这边，用 Yjs 的 CRDT 数据结构来做多客户端的状态同步，通过 ",[1137,1233,1234],{},"HocuspocusProvider"," 连到后端的 WebSocket 服务器，再结合 IndexedDB 持久化实现离线编辑。后端（Bun）则用 Hocuspocus 作为 Yjs 的协作服务器，并实现了一个数据库扩展，把 Yjs 文档状态持久化到 SQLite，所有实时通信都走 WebSocket 协议。",[1091,1237,1238],{},"认证和权限是嵌在协作链路里的。WebSocket 连接本身需要 JWT Token 认证，服务器会校验用户是否有权访问某个房间，如果一个有效用户还不是房间成员，就自动把他加入房间。",[1091,1240,1241,1242,1245,1246,1249,1250,1252],{},"数据同步的具体路径是这样的：前端用 Yjs 的 ",[1137,1243,1244],{},"Y.Map"," 存放画布元素数据，通过 ",[1137,1247,1248],{},"IndexeddbPersistence"," 把它落到浏览器的 IndexedDB，再由 ",[1137,1251,1234],{}," 同步到服务器和其他客户端。在线和离线两种状态下行为不同——在线时数据实时同步到服务器，离线时数据先存在本地 IndexedDB，等重新连上网络后，本地的改动会借助 CRDT 自动合并回服务器，不需要人工介入。最后是用户在线状态：项目用 Yjs 的 Awareness 功能来跟踪在线用户，通过广播机制实时展示协作者的光标位置和选中状态，配合后端认证保证只有授权用户能进入协作。",[1105,1254,1256],{"id":1255},"_2-状态管理zustand","2. 状态管理（Zustand）",[1091,1258,1259],{},"Zustand 是项目里最核心的状态工具，最大的特点就是轻量、几乎没有样板代码。",[1110,1261,1263],{"id":1262},"为什么选-zustand而不是-redux-或-context-api在画布场景下它的优势在哪","为什么选 Zustand，而不是 Redux 或 Context API？在画布场景下它的优势在哪？",[1091,1265,1266],{},"主要是两点。一是 API 足够简洁，避开了 Redux 那套样板代码的负担——在 Redux 里你得分别定义 actions、reducers、store 好几块，而 Zustand 一个函数就能把 store 建起来。二是性能，Zustand 支持选择性订阅，组件只在它真正关心的那部分状态变化时才重渲染；相比之下 Context API 一旦状态更新就会触发所有子组件重渲染，在画布这种状态频繁变动的场景里差距会被放大。",[1110,1268,1270],{"id":1269},"如何用-zustand-管理画布元素状态又如何实现持久化","如何用 Zustand 管理画布元素状态？又如何实现持久化？",[1091,1272,1273,1274,1277,1278,1281,1282,1285],{},"画布元素状态由 ",[1137,1275,1276],{},"CanvasState"," 接口定义，其中 ",[1137,1279,1280],{},"elements"," 是一个 ",[1137,1283,1284],{},"Record\u003Cstring, CanvasElement>","，用来存放所有画布元素：",[1287,1288,1293],"pre",{"className":1289,"code":1291,"language":1292,"meta":1184},[1290],"language-ts","interface CanvasState {\n    elements: Record\u003Cstring, CanvasElement>;\n    selectedIds: string[];\n    \u002F\u002F ... 其他状态\n}\n","ts",[1137,1294,1291],{"__ignoreMap":1184},[1091,1296,1297],{},"不过要注意，因为要支持协同编辑，元素的增删改实际上是直接操作 Yjs 的共享数据类型来完成的，而不是直接改 Zustand：",[1287,1299,1302],{"className":1300,"code":1301,"language":1292,"meta":1184},[1290],"\u002F\u002F 添加元素\naddElement: (el) => {\n  currentYDoc?.transact(() => {\n    currentYElements?.set(el.id, el)\n  })\n},\n\n\u002F\u002F 更新元素\nupdateElement: (id, attrs) => {\n  currentYDoc?.transact(() => {\n    const oldEl = currentYElements?.get(id)\n    if (oldEl) {\n      currentYElements?.set(id, { ...oldEl, ...attrs })\n    }\n  })\n},\n\n\u002F\u002F 删除元素\nremoveElements: (ids) => {\n  currentYDoc?.transact(() => {\n    ids.forEach((id) => currentYElements?.delete(id))\n  })\n}\n",[1137,1303,1301],{"__ignoreMap":1184},[1091,1305,1306,1307,1208,1309,1311],{},"持久化这块也顺着协同的思路走，并没有用传统的 zustand-persist，而是直接用 Yjs 自带的 IndexedDB 持久化机制，把 ",[1137,1308,1248],{},[1137,1310,1234],{}," 组合起来：",[1287,1313,1316],{"className":1314,"code":1315,"language":1292,"meta":1184},[1290],"\u002F\u002F 在 persistenceStore.ts 中创建持久化提供者\nconst indexeddbProvider = new IndexeddbPersistence(\n    `canvas-local-db-${roomId}`,\n    yDoc,\n);\n",[1137,1317,1315],{"__ignoreMap":1184},[1091,1319,1320],{},"这么做有几个实打实的好处：Yjs 会自动接管 IndexedDB 的读写，不用手动管理；天然带离线支持，断网时数据照样留在本地；网络恢复后本地和远端会自动同步；而且 IndexedDB 是异步读写，不会卡住 UI 线程，应用的响应性得以保证。",[1110,1322,1324],{"id":1323},"多用户协作时zustand-和-yjs-crdt-怎么配合冲突和同步怎么处理","多用户协作时，Zustand 和 Yjs CRDT 怎么配合，冲突和同步怎么处理？",[1091,1326,1327],{},"分工很清楚：Zustand 在前端充当状态访问的接口层，Yjs 作为协同引擎负责多用户之间的数据同步和冲突解决，两者之间靠 Yjs 的 observe 机制连接起来——Yjs 的数据一变，就把变化同步进 Zustand 状态里，前端组件再据此更新。",[1105,1329,1331],{"id":1330},"_3-高性能渲染与-pixijs-集成","3. 高性能渲染与 PixiJS 集成",[1091,1333,1334],{},"PixiJS 的 WebGL 渲染是这个项目性能的命门，所以这块也写得细一些。",[1110,1336,1338],{"id":1337},"为什么用-pixijs而不是纯-canvas-或-svg它在做到-60-fps-和无限画布上起了什么作用","为什么用 PixiJS，而不是纯 Canvas 或 SVG？它在做到 60 FPS 和无限画布上起了什么作用？",[1091,1340,1341],{},"PixiJS 是一个基于 WebGL 的 2D 渲染引擎，性能上的优势来自能直接吃到 GPU 加速。和纯 Canvas API 比，它提供了更高层次的抽象，开发者不用手动去抠底层渲染细节，同时还能拿到更好的性能。和 SVG 比，差距主要体现在元素数量上——SVG 基于 DOM，元素一多 DOM 操作开销就急剧上升、性能直线下滑，而 PixiJS 直接在 GPU 层渲染，哪怕画布上有几千个元素也能保持流畅。",[1091,1343,1344],{},"无限画布这块则很依赖 pixi-viewport 插件，它能针对大规模场景做渲染优化，核心是视口裁剪（view culling）——只渲染当前可见区域内的元素，把屏幕外的开销直接省掉。",[1110,1346,1348],{"id":1347},"怎么用-pixi-viewport-实现无限画布的缩放平移和边界限制","怎么用 pixi-viewport 实现无限画布的缩放、平移和边界限制？",[1091,1350,1351],{},"这几个能力基本是靠 viewport 上链式挂载的几个插件搞定的：",[1287,1353,1356],{"className":1354,"code":1355,"language":1292,"meta":1184},[1290],"\u002F\u002F 在 Stage_InteractionHandler.ts 中实现视口功能\nviewport\n    .drag() \u002F\u002F 启用拖拽平移\n    .pinch() \u002F\u002F 启用双指缩放\n    .wheel() \u002F\u002F 启用滚轮缩放\n    .clamp({ direction: \"all\" }) \u002F\u002F 边界限制\n    .bounce(); \u002F\u002F 边界弹性效果\n",[1137,1357,1355],{"__ignoreMap":1184},[1091,1359,1360],{},"缩放由 pinch 和 wheel 两个插件提供，用户既能用双指手势也能用鼠标滚轮；平移由 drag 插件负责，直接拖画布即可；clamp 用来限制视口边界，防止用户把画面拖到内容之外的空白区。除此之外 viewport 还暴露了缩放级别限制、平滑动画等配置项，可以按需微调手感。",[1110,1362,1364],{"id":1363},"项目里怎么用-spritemap-缓存-pixijs-对象避免拖拽缩放时反复创建这对性能影响有多大","项目里怎么用 spriteMap 缓存 PixiJS 对象，避免拖拽\u002F缩放时反复创建？这对性能影响有多大？",[1091,1366,1367,1368,1371],{},"项目用一个叫 ",[1137,1369,1370],{},"spriteMap"," 的 Map 来缓存 PixiJS 对象，键是元素 ID，值是该元素对应的 PixiJS 显示对象，目的就是避免在拖拽、缩放这类高频操作里反复创建和销毁元素：",[1287,1373,1376],{"className":1374,"code":1375,"language":1292,"meta":1184},[1290],"\u002F\u002F 在 Pixi_stageManager.ts 中定义\nspriteMap private spriteMap: Map\u003Cstring, PIXI.DisplayObject> = new Map()\n",[1137,1377,1375],{"__ignoreMap":1184},[1091,1379,1380,1381,1383],{},"每当画布元素更新时，先去 ",[1137,1382,1370],{}," 里查有没有现成的显示对象，有就直接改属性，没有才新建。带来的收益主要是三方面：一是减少了对象创建和垃圾回收的开销，二是渲染效率更高，因为大多数情况只是改属性而非重建对象，三是对象的连续状态（比如动画状态、事件监听器）得以保留，不会因为重建而丢失。",[1110,1385,1387],{"id":1386},"图像滤镜blurfiltercolormatrixfilter和富文本htmltext在-pixijs-里怎么实现遇到过哪些渲染难题","图像滤镜（BlurFilter、ColorMatrixFilter）和富文本（HTMLText）在 PixiJS 里怎么实现，遇到过哪些渲染难题？",[1091,1389,1390,1391,1394],{},"滤镜直接用 PixiJS 的滤镜系统就行，BlurFilter、ColorMatrixFilter 这类滤镜实例化之后挂到 sprite 的 ",[1137,1392,1393],{},"filters"," 上即可：",[1287,1396,1401],{"className":1397,"code":1399,"language":1400,"meta":1184},[1398],"language-typescript","import { BlurFilter, ColorMatrixFilter } from \"pixi.js\";\n\n\u002F\u002F 为图像元素添加滤镜\nconst blurFilter = new BlurFilter();\nconst colorFilter = new ColorMatrixFilter();\n\nsprite.filters = [blurFilter, colorFilter];\n","typescript",[1137,1402,1399],{"__ignoreMap":1184},[1091,1404,1405],{},"富文本渲染则借助了 pixi-text-html 库，它能在 PixiJS 里渲染带 HTML 样式的文本，HTMLText 组件负责解析 HTML 标签并把格式化后的文本画出来。",[1110,1407,1409],{"id":1408},"小地图minimap怎么靠-cacheasbitmap-做到实时更新为什么要单独开一个-pixi-application","小地图（Minimap）怎么靠 cacheAsBitmap 做到实时更新？为什么要单独开一个 Pixi Application？",[1091,1411,1412],{},"小地图的性能优化主要靠 cacheAsBitmap 这个属性。它会把显示对象连同其子对象一次性渲染进一张内部纹理，之后的渲染只要画这张纹理就行，不必每帧重新计算所有子对象，性能提升很明显：",[1287,1414,1417],{"className":1415,"code":1416,"language":1292,"meta":1184},[1290],"stage.cacheAsBitmap = true;\n",[1137,1418,1416],{"__ignoreMap":1184},[1091,1420,1421],{},"至于为什么要给小地图单独开一个 Pixi Application 实例，原因有几条。一是性能隔离，小地图的渲染频率往往和主画布不一样，独立实例能各自控制各自的渲染节奏。二是交互独立，小地图常需要自己的交互逻辑，比如点一下就跳到画布对应位置。三是资源管理，独立实例能更干净地管理小地图相关的纹理和资源。四是缩放独立，小地图要始终维持一个固定比例的缩略图，单独的渲染上下文更容易实现这一点。",[1105,1423,1425],{"id":1424},"_4-撤销重做机制命令模式","4. 撤销\u002F重做机制（命令模式）",[1110,1427,1429],{"id":1428},"撤销重做是怎么实现的为什么选命令模式command-pattern","撤销\u002F重做是怎么实现的？为什么选命令模式（Command Pattern）？",[1091,1431,1432,1433,1436],{},"撤销重做走的是经典的命令模式。先定义一个 ",[1137,1434,1435],{},"Command"," 接口，包含 execute、undo、redo 三个方法：",[1287,1438,1441],{"className":1439,"code":1440,"language":1292,"meta":1184},[1290],"export interface Command {\n    execute(): void;\n    undo(): void;\n    redo(): void;\n}\n",[1137,1442,1440],{"__ignoreMap":1184},[1091,1444,1445],{},"然后为每类画布操作各写一个命令类，比如 AddElementCommand、RemoveElementCommand、UpdateElementCommand。每个命令类内部都保存了执行操作所需的信息，从而能在 undo 和 redo 时把状态准确地还原回去。",[1091,1447,1448],{},"选命令模式主要图三点。一是解耦，它把操作的执行者和发起者分开，想加新的操作类型时不用动现有代码。二是状态一致性，在协同编辑这种环境里，命令模式能保证每一步操作都能被精确撤销和重做，状态不会跑偏。三是易扩展，分组、取消分组这类新命令都能很自然地补进来。",[1110,1450,1452],{"id":1451},"每个命令如何存储前后状态这在内存和性能上要怎么权衡","每个命令如何存储前后状态，这在内存和性能上要怎么权衡？",[1091,1454,1455],{},"这里有个值得强调的设计取向：命令对象里存的是\"恢复操作所需的最小信息\"，而不是动不动就拍整份状态快照。三个典型命令各自的做法略有不同。",[1091,1457,1458],{},"AddElementCommand 是最省的，因为新增的逆操作就是删除，只需要元素 ID 就够；重做时直接重复一次添加即可：",[1287,1460,1463],{"className":1461,"code":1462,"language":1292,"meta":1184},[1290],"export class AddElementCommand implements Command {\n    constructor(private payload: { element: CanvasElement }) {}\n\n    execute = () => {\n        \u002F\u002F 添加元素到画布\n        useStore.getState().addElement(this.payload.element);\n    };\n\n    undo = () => {\n        \u002F\u002F 从画布移除元素，实现撤销\n        useStore.getState().removeElements([this.payload.element.id]);\n    };\n\n    redo = () => {\n        \u002F\u002F 重新添加元素，实现重做（与 execute 相同）\n        useStore.getState().addElement(this.payload.element);\n    };\n}\n",[1137,1464,1462],{"__ignoreMap":1184},[1091,1466,1467],{},"RemoveElementCommand 则相反，删除的逆操作是把元素原样加回来，所以它必须在执行删除的那一刻先把被删元素的完整数据存下来，撤销时才有东西可恢复：",[1287,1469,1472],{"className":1470,"code":1471,"language":1292,"meta":1184},[1290],"export class RemoveElementCommand implements Command {\n    private elementData: CanvasElement | null = null;\n\n    constructor(private payload: { element: CanvasElement }) {}\n\n    execute = () => {\n        \u002F\u002F 先保存被移除元素的完整数据（用于后续恢复）\n        this.elementData = { ...this.payload.element };\n        \u002F\u002F 执行移除\n        useStore.getState().removeElements([this.payload.element.id]);\n    };\n\n    undo = () => {\n        \u002F\u002F 使用保存的数据重新添加元素，实现撤销移除\n        if (this.elementData) {\n            useStore.getState().addElement(this.elementData);\n        }\n    };\n\n    redo = () => {\n        \u002F\u002F 重复移除操作，实现重做\n        if (this.elementData) {\n            useStore.getState().removeElements([this.elementData.id]);\n        }\n    };\n}\n",[1137,1473,1471],{"__ignoreMap":1184},[1091,1475,1476],{},"UpdateElementCommand 走的是\"只存差异\"的路子，在构造函数里就把将被修改的那几个属性的旧值记下来，撤销时拿旧值覆盖回去，重做时再应用一次新值：",[1287,1478,1481],{"className":1479,"code":1480,"language":1292,"meta":1184},[1290],"export class UpdateElementCommand implements Command {\n    private previousValues: Partial\u003CCanvasElement>;\n\n    constructor(\n        private elementId: string,\n        private newValues: Partial\u003CCanvasElement>,\n    ) {\n        \u002F\u002F 在构造函数中保存更新前的属性值（旧状态）\n        const currentState = useStore.getState().elements[this.elementId];\n        this.previousValues = {};\n        Object.keys(newValues).forEach((key) => {\n            this.previousValues[key as keyof CanvasElement] =\n                currentState[key as keyof CanvasElement];\n        });\n    }\n\n    execute = () => {\n        \u002F\u002F 应用新值\n        useStore.getState().updateElement(this.elementId, this.newValues);\n    };\n\n    undo = () => {\n        \u002F\u002F 恢复旧值，实现撤销\n        useStore.getState().updateElement(this.elementId, this.previousValues);\n    };\n\n    redo = () => {\n        \u002F\u002F 重新应用新值，实现重做（与 execute 相同）\n        useStore.getState().updateElement(this.elementId, this.newValues);\n    };\n}\n",[1137,1482,1480],{"__ignoreMap":1184},[1091,1484,1485],{},"把这三个放一起看，设计的核心原则就一句话：在命令对象里只存能独立完成 undo\u002Fredo 的那部分信息，而不是完整状态快照，这样撤销重做栈才能管得轻、跑得快。",[1091,1487,1488,1489,1492],{},"当然也有权衡。内存上，每个命令都要存恢复信息，命令一多就吃内存，尤其是那种存整份状态副本的 SnapshotCommand，在元素多的时候会很占地方。性能上，生成状态快照本身要花时间，画布元素一多，用 ",[1137,1490,1491],{},"structuredClone"," 深拷贝大对象会明显拖慢操作。所以项目里按操作类型分别用了不同的存储策略：增删只存元素本身，更新只存变更前的值和被改的属性，尽量把每个命令的体积压到最小。",[1091,1494,1495],{},"后续还有一串可以继续优化的方向：给历史栈设上限防止内存溢出；把连续的多个操作合并成一个批量命令、减少栈里的命令数；对图像这类大数据命令，在它不再需要时主动清掉内部引用；对拖拽这种高频操作用防抖把连续动作并成一条命令，减缓栈的增长；把创建复杂图形的多个步骤这类相关操作分组成一个逻辑撤销单元；给每条命令加上文字描述，让 UI 上能显示具体能撤销\u002F重做什么；把历史持久化到本地存储，刷新后也能恢复；甚至可以根据当前画布复杂度动态调整栈大小，元素多时用小栈、元素少时用大栈。",[1105,1497,1499],{"id":1498},"_5-交互与用户体验","5. 交互与用户体验",[1110,1501,1503],{"id":1502},"变换控件transform-controls的检测与处理","变换控件（Transform Controls）的检测与处理",[1091,1505,1506],{},"变换控件由 TF_controler_Renderer.ts 负责渲染，包括包住选中元素的边界框、四角和四边共 8 个缩放手柄，以及通常位于顶部或底部的 1 个旋转手柄。",[1091,1508,1509],{},"手柄的命中检测靠的是鼠标位置和手柄边界框之间的距离计算。鼠标进入某个手柄区域时，光标样式会相应变化——比如边角手柄显示对角箭头，旋转手柄显示旋转图标。每个手柄对应一种操作：8 个边角手柄用于非均匀缩放，是否保持宽高比取决于修饰键；旋转手柄则用于旋转选中元素，旋转锚点可能取整组的中心。",[1091,1511,1512],{},"真正的变换逻辑放在 Stage_InteractionHandler.ts 里。无论缩放还是旋转，思路都是遍历所有选中元素、算出各自的新尺寸或新角度，最后用一次批量更新提交，避免多次重渲染：",[1287,1514,1518],{"className":1515,"code":1517,"language":189,"meta":1184},[1516],"language-TypeScript","\u002F\u002F 处理缩放操作\nhandleScale(dx: number, dy: number, handleType: string) {\n  const updates: Record\u003Cstring, Partial\u003CCanvasElement>> = {};\n\n  selectedIds.forEach(id => {\n    const element = elements[id];\n    \u002F\u002F 根据手柄类型（e.g., 'top-left', 'bottom-right'）计算缩放比例和位置偏移\n    updates[id] = calculateNewDimensions(element, dx, dy, handleType);\n  });\n\n  \u002F\u002F 批量更新元素，避免多次重渲染\n  useStore.getState().batchUpdateElements(updates);\n}\n\n\u002F\u002F 处理旋转操作（示例）\nhandleRotate(deltaAngle: number, pivotPoint: { x: number; y: number }) {\n  const updates: Record\u003Cstring, Partial\u003CCanvasElement>> = {};\n\n  selectedIds.forEach(id => {\n    const element = elements[id];\n    updates[id] = calculateRotatedElement(element, deltaAngle, pivotPoint);\n  });\n\n  useStore.getState().batchUpdateElements(updates);\n}\n",[1137,1519,1517],{"__ignoreMap":1184},[1110,1521,1522],{"id":1522},"交互模式切换逻辑",[1091,1524,1525,1526,1529],{},"项目定义了多种交互模式，统一在 Stage_InteractionHandler.ts 里管理，并且保证同一时刻只有一种模式处于激活状态。模式的判定写在 ",[1137,1527,1528],{},"onPointerDown"," 里，本质是一串带优先级的条件分支：",[1287,1531,1534],{"className":1532,"code":1533,"language":189,"meta":1184},[1516],"onPointerDown = (event: PIXI.FederatedPointerEvent) => {\n  const { x, y } = this.viewport.toLocal(event.global);\n\n  \u002F\u002F 1. 优先检测变换手柄（最高优先级）\n  if (this.isOverTransformHandle(x, y)) {\n    this.currentMode = 'transforming';\n    this.startTransform(x, y, this.getCurrentHandleType());\n    return;\n  }\n\n  \u002F\u002F 2. 检测是否点击元素\n  const hitElementId = this.isOverElement(x, y);\n  if (hitElementId) {\n    if (event.data.originalEvent.shiftKey) {\n      \u002F\u002F Shift + 点击：多选切换\n      this.toggleSelection(hitElementId);\n    } else {\n      \u002F\u002F 普通点击：单选或重新开始选择\n      this.selectElement(hitElementId);\n    }\n    this.currentMode = 'dragging';\n    this.startDrag(x, y);\n    return;\n  }\n\n  \u002F\u002F 3. 空格键平移\n  if (event.data.originalEvent.code === 'Space') {\n    this.currentMode = 'panning';\n    this.startPan(event);\n    return;\n  }\n\n  \u002F\u002F 4. 默认：框选模式\n  this.currentMode = 'selecting';\n  this.startSelectionBox(x, y);\n};\n",[1137,1535,1533],{"__ignoreMap":1184},[1091,1537,1538,1539,1542],{},"整个优先级链是 ",[1166,1540,1541],{},"transforming > dragging > panning > selecting > idle","，这样能保证变换手柄永远最先响应，不会被底下的元素点击或框选抢走。",[1110,1544,1546],{"id":1545},"对齐指南alignment-guidelines的计算与绘制","对齐指南（Alignment Guidelines）的计算与绘制",[1091,1548,1549,1550,1553],{},"对齐指南由 guidelineUtils.ts 实现，作用是在拖拽元素时实时给出视觉参考线和吸附效果。核心函数 ",[1137,1551,1552],{},"detectAlignments"," 会拿正在移动的元素去和其余所有元素逐一比对，在左右边缘、水平中心等位置上判断它们的坐标差是否落在容差范围内，落在范围内就记一条对齐线：",[1287,1555,1558],{"className":1556,"code":1557,"language":189,"meta":1184},[1516],"\u002F\u002F 检测对齐位置\nfunction detectAlignments(\n  movingElements: CanvasElement[],\n  allElements: CanvasElement[],\n  tolerance: number = 5\n) {\n  const alignments = {\n    vertical: [] as { position: number; type: string }[],\n    horizontal: [] as { position: number; type: string }[],\n  };\n\n  movingElements.forEach(moving => {\n    allElements.forEach(element => {\n      if (movingElements.some(m => m.id === element.id)) return;\n\n      \u002F\u002F 左\u002F右边缘对齐\n      if (Math.abs(element.x - moving.x) \u003C tolerance) {\n        alignments.vertical.push({ position: element.x, type: 'left-edge' });\n      }\n      if (Math.abs(element.x + element.width - (moving.x + moving.width)) \u003C tolerance) {\n        alignments.vertical.push({ position: element.x + element.width, type: 'right-edge' });\n      }\n\n      \u002F\u002F 水平中心对齐\n      const movingCenterX = moving.x + moving.width \u002F 2;\n      const elementCenterX = element.x + element.width \u002F 2;\n      if (Math.abs(movingCenterX - elementCenterX) \u003C tolerance) {\n        alignments.vertical.push({ position: elementCenterX, type: 'center' });\n      }\n\n      \u002F\u002F 类似处理水平对齐（top\u002Fbottom\u002Fcenter）...\n    });\n  });\n\n  \u002F\u002F 等间距检测（可选扩展）\n  \u002F\u002F detectEqualSpacing(...);\n\n  return alignments;\n}\n",[1137,1559,1557],{"__ignoreMap":1184},[1091,1561,1562],{},"绘制和吸附是连在一起的。拖拽过程中每一帧都会调用 detectAlignments，一旦检测到对齐，就用 PixiJS 的 Graphics 画出虚线参考线，通常是带点透明度的蓝色或绿色；同时如果偏移量已经足够接近，就把元素位置自动吸附（snap）到精确的对齐点上，给到一种\"咔哒一下对齐\"的精准布局体验。",[1105,1564,1566],{"id":1565},"_6-性能优化与工程实践","6. 性能优化与工程实践",[1110,1568,1570],{"id":1569},"项目里具体做了哪些性能优化","项目里具体做了哪些性能优化？",[1091,1572,1573],{},"为了让复杂画布也能跑得顺，项目堆了好几层针对性优化。",[1091,1575,1576],{},"第一层是对象缓存，前面提过的 spriteMap 就是核心——更新元素时优先复用已有的 PixiJS 显示对象，只在首次出现时才创建并缓存：",[1287,1578,1581],{"className":1579,"code":1580,"language":1292,"meta":1184},[1290],"   \u002F\u002F 在 Pixi_stageManager.ts 中\n   private spriteMap: Map\u003Cstring, PIXI.DisplayObject> = new Map();\n\n   updateElement(id: string, attrs: Partial\u003CCanvasElement>) {\n     const sprite = this.spriteMap.get(id);\n     if (sprite) {\n       \u002F\u002F 重用现有对象，直接更新属性\n       Object.assign(sprite, attrs);\n     } else {\n       \u002F\u002F 首次创建并缓存\n       const newSprite = this.createSprite(attrs);\n       this.spriteMap.set(id, newSprite);\n       this.container.addChild(newSprite);\n     }\n   }\n",[1137,1582,1580],{"__ignoreMap":1184},[1091,1584,1585],{},"第二层是 WebGL 渲染优化，一方面吃满 PixiJS 的 GPU 加速，另一方面靠 pixi-viewport 的视口裁剪只渲染可见区域，把绘制调用压下来：",[1287,1587,1590],{"className":1588,"code":1589,"language":1292,"meta":1184},[1290],"\u002F\u002F viewport 配置示例\nconst viewport = new Viewport({\n    interaction: app.renderer.plugins.interaction,\n    cull: true, \u002F\u002F 启用视口裁剪\n});\n\nviewport.on(\"frame-end\", () => {\n    \u002F\u002F 帧结束时可执行额外优化，如清理不可见资源\n});\n",[1137,1591,1589],{"__ignoreMap":1184},[1091,1593,1594],{},"第三层是开发体验上的，借 Vite 的 HMR 做到改代码不刷新整页就能看到效果，迭代效率提升很明显。",[1110,1596,1598],{"id":1597},"typescript-在项目里起了什么作用","TypeScript 在项目里起了什么作用？",[1091,1600,1601],{},"最直接的是类型安全，通过严格的接口定义保证数据一致、把错误尽量提前到编译期暴露，比如核心的 CanvasElement 接口：",[1287,1603,1606],{"className":1604,"code":1605,"language":1292,"meta":1184},[1290],"export interface CanvasElement {\n    id: string;\n    type: ToolType;\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n    fill: string;\n    stroke: string;\n    strokeWidth: number;\n    alpha?: number;\n    points?: number[][];\n    rotation?: number;\n    \u002F\u002F 文本相关\n    text?: string;\n    fontSize?: number;\n    fontFamily?: string;\n    textAlign?: \"left\" | \"center\" | \"right\";\n    \u002F\u002F 图像相关\n    imageUrl?: string;\n    filter?: \"none\" | \"blur\" | \"brightness\" | \"grayscale\";\n    \u002F\u002F 分组相关\n    groupId?: string;\n}\n",[1137,1607,1605],{"__ignoreMap":1184},[1091,1609,1610],{},"除此之外还有几层好处：智能提示和类型推断让编码效率明显提高；大型重构时类型系统能快速框出受影响的范围，改起来心里有底；接口本身也充当了模块间的数据契约，让协作和维护更省心。",[1110,1612,1614],{"id":1613},"构建与部署是怎么做的","构建与部署是怎么做的？",[1091,1616,1617],{},"构建工具选了 Vite，看中的是它极快的开发服务器启动和构建速度、即时的 HMR、出色的构建性能与 Tree Shaking，以及对 TypeScript、JSX、CSS Modules 开箱即用的支持。部署上，项目根目录提供了 Dockerfile 和 docker-compose.yml 支持容器化，并用 GitHub Actions 串起了一条自动化流水线：代码检查 → 单元测试 → 构建产物 → 镜像推送 → 部署到目标环境。",[1110,1619,1621],{"id":1620},"样式一致性怎么保证","样式一致性怎么保证？",[1091,1623,1624],{},"项目同时用到了多套组件库，为了让外观统一，主要靠四招。一是用 TailwindCSS 建立统一的设计系统，把主色、辅色、间距等都收敛到配置里：",[1287,1626,1630],{"className":1627,"code":1629,"language":161,"meta":1184},[1628],"language-JavaScript","   \u002F\u002F tailwind.config.js\n   module.exports = {\n     theme: {\n       extend: {\n         colors: {\n           primary: colors.blue,\n           secondary: colors.gray,\n         },\n         spacing: {\n           '18': '4.5rem',\n           '88': '22rem',\n         },\n       },\n     },\n   };\n",[1137,1631,1629],{"__ignoreMap":1184},[1091,1633,1634,1635,1638],{},"二是配一套 CSS 变量系统，定义 ",[1137,1636,1637],{},"--color-primary"," 这类全局变量，保证所有组件引用的是同一个值。三是主题管理统一收口到 themeStore.ts，集中控制主题切换。四是对第三方库的组件做二次封装，把项目自己的样式和行为规范统一套上去，避免各家组件各说各话。",[1105,1640,1642],{"id":1641},"_7-开发过程中遇到的主要技术难点","7. 开发过程中遇到的主要技术难点",[1091,1644,1645],{},"整个开发里，最费劲的几块集中在实时协作、渲染同步和性能优化上。",[1091,1647,1648],{},"最棘手的是实时协作的冲突处理——多个用户同时改画布，很容易出现操作互相覆盖、状态不一致。解决思路是把冲突交给 Yjs 的 CRDT 算法来自动合并并发修改，整个过程不需要中央锁，就能保证最终一致性；实时通道则用 HocuspocusProvider 建立 WebSocket 连接来压低延迟。在 canvasStore.ts 里，靠 Yjs 的 observe 机制监听变更并同步回本地状态：",[1287,1650,1653],{"className":1651,"code":1652,"language":189,"meta":1184},[1516],"yElements.observe(() => {\n  useStore.setState({\n    elements: yElements.toJSON(),\n  });\n});\n",[1137,1654,1652],{"__ignoreMap":1184},[1091,1656,1657],{},"这里还额外做了一层锁定机制，专门防止在同步过程中往撤销\u002F重做栈里塞进无效命令，避免协同带来的历史污染——这是本地撤销系统和协同系统打架时很容易忽略的一个坑。",[1091,1659,1660],{},"第二个难点是 PixiJS 渲染层和 React\u002FZustand 状态层的实时一致性，元素一多就容易出现延迟或对不上。应对办法是专门写了 Pixi_stageManager.ts 当桥梁层，负责双向同步 React 状态和 PixiJS 显示对象，配合 spriteMap 缓存避免反复创建销毁，再用防抖限制过于频繁的同步，并做选择性更新，只重绘真正变化的元素。",[1091,1662,1663],{},"第三个是性能。元素数量一上来，渲染和交互性能都会明显掉。这块是组合拳：启用视口裁剪只渲染可见区域，引入对象池和缓存减少内存分配，用批量更新 batchUpdateElements 把多次状态变更合并、降低重渲染次数，并对静态元素启用 cacheAsBitmap，把内容烘焙成位图来省掉重复绘制：",[1287,1665,1668],{"className":1666,"code":1667,"language":189,"meta":1184},[1516],"\u002F\u002F 示例：针对静态元素启用位图缓存\nif (sprite.isStatic && !sprite.cacheAsBitmap) {\n  sprite.cacheAsBitmap = true;\n}\n",[1137,1669,1667],{"__ignoreMap":1184},{"title":1184,"searchDepth":1671,"depth":1671,"links":1672},4,[1673,1681,1686,1693,1697,1702,1708],{"id":1107,"depth":1674,"text":1108,"children":1675},3,[1676,1677,1678,1679,1680],{"id":1112,"depth":1671,"text":1113},{"id":1119,"depth":1671,"text":1120},{"id":1129,"depth":1671,"text":1130},{"id":1188,"depth":1671,"text":1189},{"id":1224,"depth":1671,"text":1225},{"id":1255,"depth":1674,"text":1256,"children":1682},[1683,1684,1685],{"id":1262,"depth":1671,"text":1263},{"id":1269,"depth":1671,"text":1270},{"id":1323,"depth":1671,"text":1324},{"id":1330,"depth":1674,"text":1331,"children":1687},[1688,1689,1690,1691,1692],{"id":1337,"depth":1671,"text":1338},{"id":1347,"depth":1671,"text":1348},{"id":1363,"depth":1671,"text":1364},{"id":1386,"depth":1671,"text":1387},{"id":1408,"depth":1671,"text":1409},{"id":1424,"depth":1674,"text":1425,"children":1694},[1695,1696],{"id":1428,"depth":1671,"text":1429},{"id":1451,"depth":1671,"text":1452},{"id":1498,"depth":1674,"text":1499,"children":1698},[1699,1700,1701],{"id":1502,"depth":1671,"text":1503},{"id":1522,"depth":1671,"text":1522},{"id":1545,"depth":1671,"text":1546},{"id":1565,"depth":1674,"text":1566,"children":1703},[1704,1705,1706,1707],{"id":1569,"depth":1671,"text":1570},{"id":1597,"depth":1671,"text":1598},{"id":1613,"depth":1671,"text":1614},{"id":1620,"depth":1671,"text":1621},{"id":1641,"depth":1674,"text":1642},"GitHub 仓库 https:\u002F\u002Fgithub.com\u002FZhongye1\u002FBDdraw_DEV","md",true,{"uuid":1713,"slots":1714},"ed0d9de0-e365-11f0-ab2a-31d302f5a901",{},21,{"title":1085,"description":1709},"posts\u002F2025\u002F2025-12-29-记canvas画布项目开发",[134,216,217,154,218],"k0jIulpVO30ElTmq7hZfokqHvxyFbQeXpx2Ylr038tI",1790443286667]