首页归档 👤 登录

MD test-ME2D Cursor Design Engine Wiki

简介 ME2D 是一个运行在 Windows 上的本地光标设计工具,把传统光标编辑器里那些"装 PNG、调 9 宫格热点、挨帧对齐"的脏活累活抽出来,交给你写一段简短的 Lua 脚本来描述——你写"画什么",ME2D 负责"怎么画到屏幕上"。 WebView 前端:项目列表、代码编辑器、实时预览都在

简介

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.tomlmain.lua(或 script.lua,由 toml 配置指定)。

数据流(animated / realtime):

  1. 调度器按 fps 推一帧 → 给 Lua 沙箱当前 frame 编号。
  2. Lua 沙箱跑 on_render(frame),调用绘图 API 写到虚拟画布。
  3. Pillow 把虚拟画布栅格化,编码为预览 PNG。
  4. 前端通过 WebSocket 拿到 PNG,<img> 标签刷新 src,形成"实时预览"。
  5. 缓存命中时跳过第 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 帧循环动画 fpstotal_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_rectthickness=0 在 ME2D 中表示"无描边 + 整个矩形填充",与默认 1 像素描边区分。


最佳实践

  • 能 cache 就 cache:在 on_setupload_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。
栅格化 把矢量/脚本描述的图形转成像素位图的过程。

CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
转载请注明出处,不得用于商业用途,衍生作品须采用相同许可协议。

评论