让 Scratch真正成为生产力工具
面板任意停靠,几个角色的工作区同时打开,积木在角色之间直接拖;一个作品好几个世界,两万块积木照样丝滑。
Scratch 的界面是给「一个小作品」设计的
一张舞台、一列角色、一个工作区,够用而且好懂。但当作品长到几十个角色、几万块积木、要分好几关的时候,这套界面会开始拦着你。
可以同时打开好几个角色的工作区、造型区、声音区,并且能把积木从一个角色的工作区直接拖到另一个角色的工作区里。积木栏全局只有一个,独立成面板。
面板即窗口
整个界面是一套完整的停靠系统,不是固定三栏。任意面板都能拖成标签、拖进任何一格、拖出来变成浮动窗口。点下面的布局切一切。
几个角色的工作区同时打开,积木直接拖过去
同一页可以开好几个积木工作区,它们靠事件互相重放保持一致。从 A 角色的工作区把积木拖进 B 角色的工作区,拖动时另一边显示跟随指针的影子。
- 1积木栏全局只有一个,是能拖到屏幕任何地方的独立面板,不再假设它贴着工作区左边。
- 2造型区、声音区也能开多个,对照着改两个角色的造型。
- 3悬停铺开:鼠标放上去,长积木完整铺出来,不会被面板边缘截断。
一个作品,好几个世界
原来菜单、第一关、Boss 战、结算全挤在一张舞台里,每个角色都得自己判断「我这一关该不该出现」。场景把这件事拆开:每个场景是一个完整的小作品,自己的舞台、角色、脚本、造型、声音、变量。
卸载
不在内存里。纹理和解码后的音频都放掉了,存盘一个字节都不丢。
休眠
装着但没在跑。送给它的广播不会丢,排队等它下次开始。
在场 / 后台
在跑。可以在画面上,也可以在后台先把下一关算着,前台放过场动画。
全作品的造型和声音,一张表
它不是新的存储层,而是给已经按 md5 存着的素材一个作品级身份。树 / 网格 / 详情三栏,一个条目,多个用法。
角色1、Pico、小兵 都在用它。改动要影响哪些?
共用与分叉的规则是明确的
复制角色、复制场景、把造型拖给别的角色,共用同一份;复制造型、「改为独立副本」,独立一份。
改之前问一次
在画板里改一个被多处共用的造型,会问:全部更新,还是只改这个角色。
未被引用的也保存
「未使用」筛选能看到并清理,造型条、声音条、角色列表上有角标,一眼看出共用了 N 处。
积木栏是平台能力,不是扩展的许可
折叠、可扩展参数、自制积木的形状,这些都是编辑器给的,第三方扩展一句声明不写也一样能用。下面三个都能点。这里的每一块积木都是 gandi-blocks 渲染出来的。
可扩展参数 expandable
积木上那对 ➕➖。一块积木带几个参数由用户说了算,扩展在 getInfo() 里写一段 expandable 就有了,不用碰 Blockly。
命令块、报告块、布尔块、C 型积木、带参 hat 都支持。
可折叠分组 fold
分类 → 组 → 组里的小节,三级。任何一条标题都能点、能收;扩展写的 |fold-closed 只决定初始状态。
折叠是用户的权利,不是扩展的许可。
自制积木 procedures
三种形状:指令 / 报告 / 布尔。参数可以是下拉菜单,不只是输入框。改形状会自动重建所有调用块,并把用户插进去的积木搬过去。
表单是纯 React 的对话框,不是在对话框里再开一个 Blockly 工作区。
不是 SVG DOM,是 Canvas 2D
旧编辑器撞的墙和积木编辑器是同一堵:几千个 <text> 一动就要重排。所以这里场景图是不可变的、命中测试自己在文档坐标里做、视口就是一个矩阵。无限画布,平移缩放是矩阵乘法。
造型就是帧
时间轴上每一格是一个造型。洋葱皮把前后帧淡色叠上来,按旋转中心对齐;补间在帧与帧之间自动插值。画出来的曲线和播放实际用的曲线逐点相同。
不是「剪一段波形」,是一套完整的音频工作台
四种形态:波形 / 频谱 / 编谱 / 音色。多轨编排、钢琴卷帘、混音台、合成器与 SoundFont;编好的谱子能变成积木,积木也能读回谱子。
按它真正被显示的尺寸测量和绘制
4:3、16:9 或自定义尺寸;三种动态尺寸模式:固定 / 宽度跟面板长 / 高度跟面板长。做宽银幕或竖屏不用改积木。
- 1页内全屏:在页面内部铺满窗口,不问浏览器要全屏,所以舞台不会被拉糊。
- 2切换场景时交叉淡入淡出,按缓冲尺寸抓帧。
- 3变量监视器、提问框层级固定,扩展加的覆盖层不会压住它们。
代码挂在作品上,跟着 .sb3 一起走
JavaScript / Python / JSON / GLSL,Monaco 编辑器。终端接管 runtime.logSystem 六个口子,扩展的 console.log 在这里看得到,出错会自己弹出来。
- ⌘Quick Open,按名字跳文件。
- >_xterm 终端,扩展日志不再散落在浏览器控制台里。
贴一个集市页面地址就能装
背后走 CCW 的公开接口取真实 js 地址;另有 ScratchX 兼容层。装进 VM 之后,再出现在积木栏里。
Scratch.gandi:给扩展的宿主接口
想在舞台上放 <iframe> 的扩展,过去只能满页面遍历去认哪个 canvas 是舞台,再猜它的父节点插进去;每换一次布局这套猜法就断一次。现在直接要一层:画布不动、监视器不动,你也不用知道编辑器的 DOM 长什么样。
| 选项 | 默认 | 含义 |
|---|---|---|
| scale | false | 内部坐标系固定成舞台原生尺寸,缩放交给编辑器 |
| interactive | false | 接收鼠标事件(会挡住那块舞台,所以默认穿透) |
| name | 无 | 写到 data-overlay 上,方便排查 |
编辑器没提供舞台服务时(无头播放器、单测、别家编辑器),这几个方法返回 null 并且只抱怨一次,不抛异常。
// 问功能本身,能力探测不会随版本失效 if (typeof Scratch.gandi?.stage?.addOverlay === 'function') { const layer = Scratch.gandi.stage.addOverlay({ scale: true, interactive: true, name: 'my-hud' }); layer.appendChild(myIframe); } // 折叠 + 可扩展参数,在 getInfo() 里声明就有 blocks: [ '---基础', { opcode: 'foo', /* … */ }, '---高级用法|fold-closed', { opcode: 'switchToWithParams', text: '切换到场景 [SCENE],参数 [EXPAND]', expandable: { slot: [ { name: 'KEY', type: ArgumentType.STRING, defaultValue: '关卡' }, { text: '=' }, { name: 'VALUE', type: ArgumentType.STRING, defaultValue: '1' } ], between: ',', min: 1, max: 10 } } ]
两种插件,同一套契约 {id, name, setup}。内置插件跟编辑器一起构建发版,设置里一个开关;外部插件填一个网址装进来。Patcher 提供 before / after 钩子并支持 unpatch,多个扩展 patch 同一个方法不再层层套娃。
编辑几万块积木不卡,运行一万个克隆体不掉帧
这是投入最大的一块。所有数字都是同一台机器、同一套脚本、前后两棵源码树背靠背测出来的,方法和复现步骤在仓库里的两份报告里。
几万块积木的工作区
真实项目,单个角色 19899 个积木 · 帧时间中位数一万个克隆体,各有各的线程
全部绘制 · scratch-vm 编译器 + scratch-render 批量绘制积木编辑器 gandi-blocks
- 视口裁剪:看不见的脚本从文档里移走,不是 display:none,Blink 对隐藏的 SVG 子树照样跑文字排版
- 文字层:字段的文字画在工作区共享的一层上,不再是每个字段一个 <text>
- 平移缩放跑在合成层上,手势期间不重排;文字测量用 canvas,不触发布局刷新
- 连接点数据库早退:没动的连接点不再做两次 O(n) splice
运行时 scratch-vm · scratch-render
- 数值变量槽:只写过数字的变量挪进 V8 会拆箱的小对象
- 自制积木内联、常量参数特化、常量折叠,语义是 Scratch 的不是 JS 的
- startHats 按 key 直接找线程,线程表增量维护
- 批量绘制:一万个同造型克隆体变成一次 draw call
打开 47 MB 的作品
- 5 个角色、1941 个 SVG 造型、73 段音频共 47 分钟、25600 个积木
- 从选文件到能干活:50 秒以上 → 4.7 ~ 5.6 秒
- 关键是造型等到有人要看它再变成图片:一个角色同时只穿一件造型,另外 1935 件从头到尾没人看
诚实的一处退步
- 三千个克隆体的生成从 89 ms 变成 111 ms:变量提升成槽要定义访问器
- 一次性成本,之后每帧省 0.68 ms,二十帧就回本
- 到一万个克隆体时生成反而快 2.2 倍,因为原来那个二次方的 hat 扫描在主导
打开浏览器就能用
不用装任何东西。打开下面的地址,拖一个 .sb3 进去,或者从零开始。源码整理好之后会开源,敬请期待。