简介
ME2D 是一个运行在 Windows 上的本地光标设计工具,把传统光标编辑器里那些"装 PNG、调 9 宫格热点、挨帧对齐"的脏活累活抽出来,交给你写一段简短的 Lua 脚本来描述——你写"画什么",ME2D 负责"怎么画到屏幕上"。
- WebView 前端:项目列表、代码编辑器、实时预览都在本地浏览器内核里渲染。
- Lua 沙箱:每帧用一段 Lua 描述当前画面,安全、可热重载。
- Python/Pillow 渲染管线:把 Lua 调用翻译成 Pillow 指令,输出 PNG 帧序列。
适用场景:自定义 Windows 光标(.cur / .ani)、主播/演示用的动态光标、需要根据系统状态(音量、主题色)变化的"活"光标。
创建一个项目
两种方式:
- 在 WebView 界面:左上角 "New Project" → 选类型 → 起名 → 自动生成
project.toml模板。 - 手动:在
projects/<your-name>/下新建project.toml与main.lua(或script.lua,由 toml 配置指定)。
数据流(animated / realtime):
- 调度器按
fps推一帧 → 给 Lua 沙箱当前frame编号。 - Lua 沙箱跑
on_render(frame),调用绘图 API 写到虚拟画布。 - Pillow 把虚拟画布栅格化,编码为预览 PNG。
- 前端通过 WebSocket 拿到 PNG,
<img>标签刷新src,形成"实时预览"。 - 缓存命中时跳过第 2~3 步,直接复用上一次的 PNG。
项目结构
projects/
└─ my-cursor/ ← 一个项目 = 一个文件夹
├─ project.toml ← 项目元数据
├─ main.lua ← 主脚本(可在 toml 里改名)
├─ assets/ ← 可选:图片等资源
│ └─ image.png
└─ .me2d_data/ ← 生成的预览与缓存(**不要**手动改)
├─ preview-0001.png
└─ cache.bin
project.toml 大致包含:
[project]
name = "my-cursor"
type = "animated" # static | animated | realtime
canvas = [32, 32] # 画布宽高(像素)
hotspot = [0, 0] # 热点坐标
[render]
fps = 30
total_frames = 60
loop = true
[script]
entry = "main.lua" # 入口脚本
项目类型
| 类型 | 何时使用 | 关键字段 |
|---|---|---|
static |
单帧光标,不需要动 | 只用 on_render(0) 一次 |
animated |
帧循环动画 | fps、total_frames |
realtime |
未来接入实时数据 | 同 animated,但允许更长的运行周期 |
切换类型:在 WebView 项目详情页直接改
type,或者编辑project.toml后保存。ME2D 会重置.me2d_data/下的缓存。
Lua API 参考
画布与坐标系
| 函数 | 说明 |
|---|---|
set_canvas(w, h) |
设置画布大小(像素)。在 on_setup 里调用一次即可。 |
set_hotspot(x, y) |
热点坐标,光标"瞄准"的点(Windows 鼠标点击的位置)。 |
坐标系:左上角 (0, 0),右下角 (w-1, h-1)。颜色用十六进制字符串: |
#rgb(3 位)→#33e等价#3333ee#rrggbb(6 位)→#33ebcb#rrggbbaa(8 位)→#33ebcb80(末尾两位是 alpha,00=透明,ff=不透明)
图像资源
| 函数 | 说明 |
|---|---|
load_png(key, path) |
加载 PNG 到 key,路径相对项目根目录。 |
add_image(key, x, y) |
在 (x, y) 贴一张已加载的图。 |
load_png("logo", "assets/image.png")
add_image("logo", 0, 0)
绘图原语
-- 矩形描边
draw_rect(x, y, w, h, color, [thickness=1])
-- 单像素
draw_pixel(x, y, color)
-- 直线
draw_line(x1, y1, x2, y2, color, [thickness=1])
-- 折线(仅描边,不闭合)
draw_path({{x=0,y=8},{x=8,y=0},{x=16,y=8}}, "#33ebcb", 1)
-- 圆(描边;thickness 缺省为 1)
draw_circle(cx, cy, r, color)
-- 多边形填充
fill_polygon({{x=4,y=4},{x=16,y=2},{x=20,y=18}}, "#33ebcb")
几何约定:
- 描边像素包含坐标本身(中心点位于坐标上)。
draw_circle默认 1 像素描边;如需实心圆可设thickness = r。- 颜色接受 alpha 通道(
#rrggbbaa),未指定时按不透明处理。
音频 API
local levels = get_audio_response(count)
返回一个长度为 count 的 Lua 表,元素是最近系统输出音频的归一化电平,范围 0.0 ~ 1.0。 行为细节:
- 读取的是 Windows 音频端点(系统默认输出设备的当前 session)。
- 静音 / 没有播放 → 全部接近
0.0(不会伪造一条假动画)。 - 端点不可用时(如权限被拒)→ 全部返回
0.0,并把警告写到print控制台。 典型用法:取第一项做"主电平":
local audio = get_audio_response(8)
print("main", audio[1])
主题与系统
get_win_theme_color_hex() -- 读 Windows 当前主题色,返回 "#rrggbb" 字符串
仅 Windows 上有效;其他平台返回 "#000000"。
输出与调试
print(...) -- 写到编辑器控制台
- 颜色:紫色(与普通日志区分)。
- 前缀:时间戳(毫秒精度)。
- 多参数:以 Tab 连接(标准 Lua
print风格)。 - 每次调用追加一行;不会自动换行之外的格式。
生命周期回调
| 回调 | 时机 | 用途 |
|---|---|---|
on_setup() |
脚本首次执行,且缓存失效时 | 初始化常量、加载资源、复位状态 |
on_render(frame) |
每帧调用一次,frame 从 0 开始 |
描述本帧画面 |
不写
on_setup不会报错;缓存命中时也不会重复执行。
帧间状态
on_render 每帧都被调用,函数内的 local 不会保留到下一帧。要保留值,用 chunk 级 local:
-- 这一层声明的 local 会成为 on_render 的 upvalue
local counter = 0
function on_render(frame)
counter = counter + 1
print("frame", counter)
end
或者用 on_setup 做初始化:
local smoothed
function on_setup()
smoothed = 0
end
function on_render(frame)
local audio = get_audio_response(8)
smoothed = smoothed * 0.7 + (audio[1] or 0) * 0.3
print("smoothed", smoothed)
end
重要:脚本保存即失效缓存,
on_setup会再跑一次,状态从头开始——这是"撤销"机制,不是 bug。
缓存机制
ME2D 用内容哈希判断脚本是否改动:
- 哈希未变 → 跳过 Lua 重跑 + 渲染,直接返回上一帧 PNG(CPU 几乎为 0)。
- 哈希变了 → 重新跑
on_setup+on_render,刷新预览。 因此: - 在编辑器里"光标闪烁"但没改字 → 不会触发重渲染。
- 加一个空格再删掉 → 会触发一次(因为哈希变了),再回到原值。
- 大型项目里善用 chunk 级
local来缓存昂贵计算(如加载的图像、查表)。
教程
Hello Cursor
最简 static 项目,画一个白点:
set_canvas(16, 16)
set_hotspot(0, 0)
draw_pixel(0, 0, "#ffffff")
做一个会动的小光标
set_canvas(32, 32)
set_hotspot(0, 0)
function on_render(frame)
-- 一条横向移动的青色竖线
local x = frame % 28 + 2
draw_line(x, 4, x, 28, "#33ebcb", 2)
end
跟着系统音量"呼吸"
set_canvas(32, 32)
set_hotspot(16, 16)
function on_render(frame)
local audio = get_audio_response(8)
local level = audio[1] or 0
local r = 4 + level * 12
draw_circle(16, 16, r, "#33ebcb80")
print("level", level)
end
跟随 Windows 主题色
set_canvas(24, 24)
set_hotspot(2, 2)
local theme = get_win_theme_color_hex()
function on_render(frame)
draw_rect(2, 2, 20, 20, theme, 0) -- thickness=0 = 实心填充
end
draw_rect的thickness=0在 ME2D 中表示"无描边 + 整个矩形填充",与默认 1 像素描边区分。
最佳实践
- 能 cache 就 cache:在
on_setup里load_png,避免每帧重读盘。 - 音频电平做平滑:
smoothed = smoothed*α + raw*(1-α),α越大越稳。 - 避免大对象:脚本里塞几百 KB 的字符串会让哈希计算变慢。
- 颜色 alpha 善用:半透明叠加比画多个图层便宜。
- 热点放准:写完跑一次,把鼠标移到屏幕四角,光标"瞄准"的点就是热点。
- 不要在
on_render里 IO:磁盘/网络访问是阻塞的,会拖慢整条管线。
故障排查
| 症状 | 可能原因 | 解决 |
|---|---|---|
| 预览一片黑 | set_canvas 没调用,或画在了画布外 |
加 set_canvas、检查坐标 |
| 光标点击位置偏 | set_hotspot 没设或设错 |
设为光标"瞄准"的像素 |
| 音频一直是 0 | 没有播放音频 / 输出端点静音 | 打开任意音频,确认系统混音器有信号 |
| 改完代码没反应 | 浏览器缓存 | 强刷(Ctrl+Shift+R);也可能是脚本哈希未变 |
编辑器 print 不显示 |
没保存 | 保存后 on_render 才会再跑 |
| 启动闪退 | Python / Pillow 版本不匹配 | 看 app.py 启动日志,重装依赖 |
术语表
| 术语 | 含义 |
|---|---|
| 画布 (Canvas) | Lua 脚本描述的虚拟绘图表面,最终被栅格化成 PNG。 |
| 热点 (Hotspot) | 光标图像上对应"鼠标点击位置"的像素坐标。 |
| chunk 级 local | 在脚本最外层(不在任何函数里)声明的 local 变量。 |
| upvalue | 闭包捕获的外层变量。on_render 能"记住" chunk 级 local 的本质。 |
| 哈希失效 | 脚本内容变化导致缓存键变化,触发重新渲染。 |
| 音频端点 | Windows 音频子系统的输出/输入设备抽象。 |
| WebView | 应用内嵌的浏览器内核,承载前端 UI。 |
| 栅格化 | 把矢量/脚本描述的图形转成像素位图的过程。 |
评论