Inspect, modify, and construct PencilKit drawings
Graphics & Games 进阶 22m

检查、修改和构建 PencilKit 绘图

Inspect, modify, and construct PencilKit drawings

2020年6月25日

在 Apple 官方观看视频

一句话判断

PencilKit 终于开放了对绘图内容的程序化访问和修改能力——如果你的笔记或绘图应用还在用”黑盒”方式处理 PKDrawing,这场 Session 会改变你的开发思路。

这场 Session 讲了什么

iOS 14 对 PencilKit 做了重大升级,核心变化是引入了 PKStrokePKStrokePathPKInk 等新类型,让开发者可以逐笔画地读取和修改绘图数据。在此之前,PKDrawing 对开发者来说基本是个不透明的容器——你能显示它、能序列化它,但无法访问其中的具体笔画内容。

Session 详细介绍了新的 strokes API。每个 PKStroke 包含一条 PKStrokePath(笔画的点序列)和一个 PKInk(笔刷类型和颜色)。你可以遍历 drawing 中的所有 stroke,检查它们的属性,甚至创建全新的 stroke 来程序化生成绘图内容。这意味着你可以实现诸如笔画颜色批量替换、图形识别后的智能修正、导出为自定义矢量格式等高级功能。

除了 stroke 级别的操作,Session 还演示了如何利用新 API 构建更智能的绘图工具。例如,你可以检测用户画的是不是近似直线或圆形,然后自动吸附为标准图形;或者在用户完成标注后,用程序生成高亮区域。

值得深挖的点

PKStrokePath 的点数据结构

PKStrokePath 由一系列 PKStrokePoint 组成,每个点携带 x/y 坐标、时间戳、压力值和仰角(azimuth)信息。这些数据足够精确地还原 Apple Pencil 的完整输入轨迹。对于做手写识别或签名验证的开发者来说,这些原始数据是金矿。值得注意的是,点数据的坐标系统与 PKCanvasView 的 bounds 对应,所以做坐标变换时要注意 scale 和 offset。

程序化构建绘图的实际应用场景

Session 展示了几个很有启发性的用例:模板填充(在预设位置生成标准图形)、批注辅助(检测用户意图后自动生成标注框)、以及绘图数据导出(将 PencilKit 数据转换为 SVG 或自定义格式)。这些场景此前只能靠截图或私有 API 实现,现在终于有了官方途径。

代码片段

import PencilKit

// 遍历 PKDrawing 中的所有笔画并分析
func analyzeDrawing(_ drawing: PKDrawing) {
    for (index, stroke) in drawing.strokes.enumerated() {
        let ink = stroke.ink
        print("笔画 \(index): 工具=\(ink.tool), 颜色=\(ink.color)")
        
        // 获取笔画路径中的所有点
        let path = stroke.path
        print("  包含 \(path.count) 个采样点")
        
        // 计算笔画的边界框
        if let firstPoint = path.first,
           let lastPoint = path.last {
            print("  起点坐标: (\(firstPoint.x), \(firstPoint.y))")
            print("  终点坐标: (\(lastPoint.x), \(lastPoint.y))")
        }
    }
}
// 程序化创建新的笔画并添加到绘图中
func createHighlightStroke(in drawing: PKDrawing, 
                           rect: CGRect) -> PKDrawing {
    // 创建高亮笔刷(黄色半透明)
    let highlightInk = PKInk(.pen, color: UIColor.yellow.withAlphaComponent(0.3))
    
    // 构建矩形高亮区域的点序列
    var points: [PKStrokePoint] = []
    let corners = [
        CGPoint(x: rect.minX, y: rect.minY),
        CGPoint(x: rect.maxX, y: rect.minY),
        CGPoint(x: rect.maxX, y: rect.maxY),
        CGPoint(x: rect.minX, y: rect.maxY),
        CGPoint(x: rect.minX, y: rect.minY),
    ]
    
    for corner in corners {
        points.append(PKStrokePoint(
            location: corner,
            timeOffset: 0,
            size: CGSize(width: 20, height: 20),  // 笔刷粗细
            opacity: 0.3,
            force: 1.0,
            azimuth: CGFloat.pi / 4,
            altitude: CGFloat.pi / 4
        ))
    }
    
    // 创建笔画路径和笔画
    let path = PKStrokePath(controlPoints: points, creationDate: Date())
    let newStroke = PKStroke(ink: highlightInk, path: path)
    
    // 将新笔画添加到已有绘图中
    var existingStrokes = drawing.strokes
    existingStrokes.append(newStroke)
    return PKDrawing(strokes: existingStrokes)
}
// 批量替换绘图中的颜色
func replaceColor(in drawing: PKDrawing,
                  from originalColor: UIColor,
                  to newColor: UIColor) -> PKDrawing {
    let newStrokes = drawing.strokes.map { stroke -> PKStroke in
        // 检查颜色是否匹配(近似比较)
        if stroke.ink.color.isApproximatelyEqual(to: originalColor) {
            // 用新颜色创建相同的笔刷和路径
            let newInk = PKInk(stroke.ink.tool, color: newColor)
            return PKStroke(ink: newInk, path: stroke.path)
        }
        return stroke
    }
    return PKDrawing(strokes: newStrokes)
}

最佳实践

  • 在处理大量笔画数据时注意性能,strokes 数组的 map/filter 操作会创建新的 PKDrawing 实例,建议批量操作而非逐笔修改
  • 保存自定义绘图数据时,可以同时保留原始 PKDrawing 的序列化数据和你的解析结果,以便未来兼容
  • 使用 PKStrokePoint 的时间戳信息可以实现笔画回放动画,但要注意时间偏移是相对于笔画起点的
  • 在 Apple Pencil 之外也考虑手指输入场景,force 值在手指输入时可能为 0

还有什么值得关注

  • PKInk 的 tool 类型包括 .pen.pencil.marker,每种工具的渲染效果不同,程序化生成时选择合适的类型很重要
  • Session 提到 Scribble 功能(全系统手写输入识别)底层也依赖类似的笔画数据结构,虽然 Scribble API 本身不直接暴露笔画级别操作
  • 新的 stroke API 与 PKCanvasView 的 tool picker 配合使用时,开发者可以在工具切换时做自定义的笔画后处理
WWDC 2020