# Whiteboard

> 自由手绘表面，笔迹经过平滑处理，颜色与粗细均可配置，适合签名场景。

Source: https://crossfly.dev/zh/docs/ui/widgets/whiteboard — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# Whiteboard

`Whiteboard` 是一个**自由绘图**表面：用户拖动手指/指针，组件记录这些点，平滑笔迹（相邻点的加权平均），并将其渲染到内部的 `Image` 上。它是签名捕获、手写批注和小型草图的基础。每一笔完成后（在 `onTouchUp` 时）都会"烧录"到图像上，因此历史记录会累积而无需在每一帧重新处理所有点。

你可以控制画笔颜色（`setPenColor`）、粗细（`thick` 字段），并且可以清除（`clear()`）或导出/设置背景图像（`getImage`/`setImage`）。

> 🟢 · **包：** `dev.crossfly.ui` · **继承：** `Component` · **样式：** `ComponentStyle`（默认白色背景）

## 何时使用
- 捕获一个签名或一幅自由手绘。
- 在现有图像上进行批注（通过 `setImage` 传入）。

## 何时不要使用
- 显示/绘制结构化数据 → 使用 [`chartview`](/docs/ui/widgets/chartview)。

## 外观
与组件同尺寸的白色背景（`backColor`）。笔迹是平滑的线条，粗细为 `Styles.fontHeight/3`（粗）或 `Styles.fontHeight/10`（细），采用画笔颜色（alpha 被强制为不透明）。该组件具有 `hasPan(true)`，即捕获拖动手势以进行绘制。

## 完整示例（复制、粘贴并运行）
`Whiteboard` 是一个普通组件：用 `add(LAY_FILL, ...)` 直接放入屏幕以占据主体。此示例复用了基本用法（蓝色画笔、粗笔迹），并添加了一个清除按钮。

```java
package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;

/** 可执行的主界面。将 launcher 指向此类（classname）并运行：gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      Whiteboard board = new Whiteboard();
      board.setPenColor(0x0000FF);   // 蓝色（绘制时 alpha 被强制为不透明）
      board.thick = true;            // 粗描边

      Button limpar = new Button("Limpar")
         .onAction(c -> board.clear());
      add(LAY_AUTO, limpar);

      add(LAY_FILL, board);          // 占据屏幕主体
   }
}
```

> 下面的片段仅展示**核心部分**（放入 `onShow()` 内的内容）。要运行它们，请将其放入上面的 `MainWindow` 骨架中。

## 基本用法
```java
Whiteboard board = new Whiteboard();
board.setPenColor(0x0000FF);   // 蓝色（绘制时 alpha 被强制为不透明）
board.thick = true;            // 粗描边
container.add(LAY_FILL, board);

// 在其他地方，一个用于清除的按钮：
btnLimpar.onAction(c -> board.clear());
```

## 构造函数
| 构造函数 | 描述 |
|---|---|
| `Whiteboard()` | 创建具有白色背景和 `hasPan(true)` 的表面（可立即绘制）。 |

## 属性与自定义
| 属性 / 方法 | 类型 | 默认值 | 作用 |
|---|---|---|---|
| `setPenColor(int)` | `void` | `BLACK` | 设置画笔颜色（绘制时强制不透明 alpha）。 |
| `getPenColor()` | `int` | — | 返回当前画笔颜色。 |
| `thick` | `boolean`（字段） | `false` | `true` = 粗笔迹（`fontHeight/3`）；`false` = 细笔迹（`fontHeight/10`）。 |
| `clear()` | `void` | — | 清除图像和累积的点。 |
| `getImage()` | `Image` | `null` | 返回绘图图像（如果未绘制任何内容，则可能为 `null`）。 |
| `setImage(Image)` | `void` | — | 设置背景/基础图像（传入 `null` 表示空白表面）。`throws ImageException`。 |
| `Whiteboard.debugPoints` | `boolean` 静态 | `false` | 调试：显示点数/渲染时间。 |

## 回调与事件
`Whiteboard` 在内部处理手势；不暴露"笔迹完成"回调。

| 事件（内部） | 行为 |
|---|---|
| `onTouchDown(x,y,button)` | 开始一笔新笔迹并记录第一个点。 |
| `onPan(x,y,dx,dy)` | 在用户拖动时累积点并重绘。 |
| `onTouchUp(x,y,button)` | 结束笔迹并将绘图"烧录"到内部的 `Image` 上。 |

## 状态
- **绘制中**：在 `onTouchDown` 和 `onTouchUp` 之间，点位于一个临时缓冲区中，并在每次 `onPan` 时被平滑/渲染。
- **已固化**：在 `onTouchUp` 时，笔迹被写入 `Image` 并清空缓冲区（每帧成本不随笔迹总数增长）。

### 带颜色选择和导出的签名面板
```java
Whiteboard assinatura = new Whiteboard();
assinatura.thick = false;                 // 用于签名的细笔画
container.add(LAY_FILL, assinatura);

btnPreto.onAction(c -> assinatura.setPenColor(0x000000));
btnAzul.onAction (c -> assinatura.setPenColor(0x0000FF));
btnLimpar.onAction(c -> assinatura.clear());

// 确认后，取回生成的图像：
btnOk.onAction(c -> {
   Image desenho = assinatura.getImage();   // 如果什么都没画，可能为 null
   if (desenho != null)
      salvarAssinatura(desenho);
});
```

## 最佳实践
- 签名优先使用 `thick = false`（笔迹更精确），大幅草图使用 `thick = true`。
- 导出前务必检查 `getImage() != null`：如果用户未绘制任何内容，图像仍为 `null`。
- 提供一个 `clear()` 按钮 —— 没有按笔迹的"撤销"功能。

## 常见错误
- 传入带有显式 alpha 的颜色并期望透明：绘制**强制不透明 alpha**（`0xFF000000 | cor`）。
- 期望有笔迹回调：在交互后使用方法（`getImage`）；没有笔迹完成的 `onAction`。

## 另请参阅
- 领域指南：[`ui/`](/docs/ui) · 食谱：[`cookbook/`](/docs/cookbook)
- 概念（图像、图形基元）：[`concepts/`](/docs/concepts)
- 相关组件：[`chartview`](/docs/ui/widgets/chartview)
