# canvas 动效画布

`canvas` 是给游戏、动画和自绘界面用的控件。自绘事件和动画接口与 [plus 控件](plus.html) 相同（`onDrawBackground`、`onDrawForeground`、`onDrawComplete`、`startAnimation`、`onAnimation`，鼠标键盘事件也一样），plus 的自绘代码把 `cls="plus"` 换成 `cls="canvas"` 一般就能运行。

| | plus | canvas |
|---|---|---|
| 画布 | 每次重绘新建内存位图，伽马校正合成 | 常驻的 PARGB 内存位图（GDI+ 最快的格式），默认合成 |
| 动画节拍 | 窗口定时器，帧间隔不稳 | 跟随显示器刷新（60Hz 屏幕 16.7 毫秒，高刷屏更快） |
| 功能 | 文本、图标、皮肤、边框、圆角、进度条…… | 只有画布、鼠标键盘事件与动画 |
| 透明 | 剪切父窗口背景 | 不透明；`orphanWindow(true)` 后为透明分层窗口 |

见缝插针小游戏实测（200% DPI，880×1360 像素）：plus 每帧绘图 24 毫秒、帧间隔约 31 毫秒；canvas 每帧 5 毫秒、帧间隔 16.7 毫秒。带整屏半透明遮罩的结算画面 plus 约 10 帧/秒，canvas 仍是 60 帧/秒。

游戏、动画和大量自绘一律用 canvas；按钮、图标文字、贴图等普通界面控件用 plus。canvas 上不要叠放其他控件，按钮直接画在画布上更好看（命中检测自己做）；确实需要叠放时，对上层控件调用 `orphanWindow(true)`。

> aardio 窗体设计器控件控工具箱控件排序：指针（用于在设计器上拖动控件）,static,plus,canvas... ， 第 3 个控件就是 canvas 。在控件工具箱中点选 canvas，然后在窗体设计器上拖动选好位置放置控件即可。canvas 在设计器中默认会设为白色背景。

## 最小示例

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="canvas";right=599;bottom=399)
winform.add(
canvas={cls="canvas";left=0;top=0;right=600;bottom=400;bgcolor=0x2A1A12;anchors={left=1;top=1;right=1;bottom=1};z=1}
)
/*}}*/

var x = 0;
var brush = gdip.solidBrush(0xFF4CC9F0);

winform.canvas.onDrawForeground = function(graphics,rc){
	var dpi = winform.dpiScale(1);
	graphics.scale(dpi,dpi); //之后按 100% DPI 的逻辑坐标绘图
	graphics.fillCircle(brush,x,200,30);
}

winform.canvas.onAnimation = function(prevState,beginning,change,timestamp,duration,deltaMs){
	x = (timestamp * 0.2) % 600; //按时间推进，不要按帧计数
	return true; //返回 null 才停止（返回 false 不停止）
}

winform.onDestroy = function(){ brush.delete(); }

winform.canvas.startAnimation(); //省略间隔：每帧回调
winform.show();
win.loopMessage();
```

范例都在 `~/examples/canvas/` 目录：`balls.aardio`（弹跳小球）、`game/`（见缝插针等 10 个小游戏）、`weather-card.aardio`（动画天气卡片）、`clock.aardio`（透明分层窗口做的桌面时钟）；`~/examples/plus/av/celebrate.aardio` 是盖在控件上面的透明特效层。

## 绘图

- 每帧先用底色铺满画布，再依次回调 `onDrawBackground`、`onDrawForeground`、`onDrawComplete`。底色为 `bgcolor`，未指定时为父窗口的背景色，`orphanWindow(true)` 后为全透明。`onDrawBackground` 画在底色上面，返回值不影响绘图（与 plus 相同）。
- `bgcolor` 与 plus 一样用 0xBBGGRR 格式。背景只是一种纯色时直接指定 `bgcolor`，不必写 `onDrawBackground`。
- `rc` 是设备像素。游戏状态、碰撞建议用逻辑坐标，绘图时 `graphics.scale(dpi,dpi)`，鼠标坐标除以 dpi。
- 画板默认抗锯齿，文字默认是不对齐像素的抗锯齿（移动的文字不抖），贴图默认双线性插值。可以在回调里改。
- `graphics.compositingQuality` 默认为 0（线性混合），plus 是 2（伽马混合）。伽马混合下低 alpha 的半透明与细线明显更亮更清晰，但代价正比于混合流程覆盖的像素数：细线只贵 2 倍，整屏半透明遮罩贵十几倍。因此不要全局设为 2，静态背景应当缓存成位图并在建立缓存时开 2（见 [GDI+ 绘图与性能优化](https://www.aardio.com/zh-cn/docs/library-guide/std/gdip/_.html)）。
- 画刷、画笔、字体、位图创建一次反复用，窗口销毁时释放。不变的背景画进位图缓存。
- 缓存位图用 PARGB 格式（`gdip.bitmap(w,h,0xE200B)`），与画布同尺寸的缓存背景用 SourceCopy 贴上去：`graphics.compositingMode = 1` 后 `drawImageRect`，画完改回 0。1520×1000 像素实测 SourceCopy 0.73 毫秒，默认混合 2.2 毫秒。
- canvas 没有 plus 的边框、圆角、文本等属性，这些都在回调里自己画。例如圆角卡片：`onDrawBackground` 里用 `setClipPath` 把内容裁剪到圆角矩形内，`onDrawForeground` 最后 `resetClip` 再描边（见 `weather-card.aardio`）。

## 动画

- `startAnimation()` 立即回调一次 `onAnimation`，之后每次显示器刷新回调一次，回调后自动重绘。后台线程等系统合成器刷新后通知界面线程，不用窗口定时器。
- `timestamp` 是动画已运行的毫秒数，`deltaMs` 是距上一帧的毫秒数，都是高精度小数。高刷屏上回调更频繁，按时间推进才能在所有屏幕上速度一致。
- 窗口隐藏或最小化时暂停回调，恢复后的 `deltaMs` 包含暂停的时长，游戏应限制单帧最大步长，例如 `math.min(deltaMs,50)`。
- `startAnimation(interval)` 的间隔小于 25 毫秒时每帧回调；大于等于 25 毫秒时按刷新周期的整数倍降频，例如 50 在 60Hz 屏幕上每 3 帧回调一次，适合不需要流畅的动画。
- 回调里的异常会停止动画，由定时器异步报告（跨线程消息与 WM_PAINT 里不能直接抛出异常），之后可以再次 `startAnimation`。

## 鼠标与键盘

事件由 `win.ui.tracker` 提供，与 plus 启用 `notify` 后相同：`onMouseDown`、`onMouseUp`、`onMouseClick`、`onMouseMove`、`onMouseDrag`、`onMouseWheel`、`onKeyDown`、`onKeyUp`、`onFocusGot`、`onFocusLost` 等。`notify` 默认为 true，一般不用写；设为 false 时鼠标穿透到下层窗口（例如父窗体），适合纯装饰的背景动画，运行时也可以修改。点击画布会自动获得键盘焦点。

画布默认接收方向键与字符键；要接收回车、Tab、Esc 设置 `winform.canvas.dlgCode = 4/*_DLGC_WANTALLKEYS*/`。`cursor` 属性可以换鼠标指针，例如 `"hand"`。

## 透明特效层

调用 `orphanWindow(true)` 后画布转为透明分层窗口，背景默认全透明，可以盖在其他控件上面画粒子。鼠标要穿透到下面的控件时加上 `_WS_EX_TRANSPARENT`：

```aardio
winform.add(fx={cls="canvas";left=0;top=0;right=600;bottom=400;anchors={left=1;top=1;right=1;bottom=1};z=99});
winform.fx.orphanWindow(true); //在 winform.show() 之前调用
winform.fx.modifyStyleEx(,0x20/*_WS_EX_TRANSPARENT*/);
```

分层模式下文字不要用 ClearType（默认的抗锯齿即可）。

分层模式也能做任意形状的窗口：`clock.aardio` 的画布铺满一个无边框窗体后调用 `orphanWindow(true)`，表盘缓存成位图，每帧只重画指针，圆形之外全透明；窗体本身用 `win.ui.layered` 变成全透明，只负责定位和拖动。

## 截图与无界面测试

`snap()` 立即绘制一帧并返回副本（PARGB 格式的 `gdip.bitmap`），不依赖窗口显示，隐藏的窗口也能截图，适合无界面测试。用完要 `delete`。

```aardio
var bmp = winform.canvas.snap();
var argb = bmp.getPixel(10,10);
bmp.delete();
```

## 录制视频

画布的内存布局与 `sys.mediaFoundation` 视频写入器要求的帧一致（PARGB、自上而下、步幅为宽×4）。在 `onDrawComplete` 里把 `owner.bitmap` 传给写入器即可录下每一帧（画布尺寸必须与视频一致）：

```aardio
winform.canvas.onDrawComplete = function(graphics,rc,hMemBitmap){
	if(writer) writer.writeFrame(owner.bitmap);
}
```

`owner.bitmap` 在画布尺寸改变时会重建，不要长期保存或删除它。

## 音效

游戏音效用 [sys.soundPool](https://www.aardio.com/zh-cn/docs/library/sys/soundPool/)：启动时载入，之后一句 `play(名字)`，多个声音可以同时响，不卡画面。
