Highlight
iOS 14 通过
PKStroke、PKStrokePath、PKStrokePoint和PKInk打开PKDrawing内部数据,开发者可以按笔画检查、插值、识别、裁剪并程序化构造 PencilKit 绘图。
核心内容
在 iOS 14 之前,很多 App 接入 PencilKit 的方式很直接:放一个 PKCanvasView,让系统负责 Apple Pencil 的低延迟绘制,再把 PKDrawing 存下来。这个流程适合做白板、批注和草稿本。问题出在下一步:App 想知道用户画了什么,想按笔画回放,想把一段笔迹拆成字符,想根据识别结果修改绘图时,PKDrawing 里的结构不够透明。
这场 session 用一个练字 App 说明新能力的价值。App 先把一组小写字母拆成单个字母的 PencilKit drawing,再按用户输入合成练字模板;练习时,红点沿着下一笔的轨迹运动,用户写得接近模板就变绿,写偏了就重来。这个流程同时用到了三件事:程序化构造 drawing、按时间沿 stroke 动画、把用户笔画和模板做相似度匹配。
iOS 14 把 PKDrawing 拆开成 strokes、inks、paths 和 points。每个 stroke 代表用户画下的一条线,顺序保留了用户的书写过程;stroke 的 path 是一组三次均匀 B 样条控制点;point 记录位置、尺寸、角度、透明度、压力、倾角和时间偏移;mask 则描述像素橡皮擦裁掉后的可见范围。
这让 PencilKit 从一个画布控件进入数据层。开发者可以从绘图里取出 stroke,按距离或时间采样路径,处理被擦除后的可见片段,用 PKDrawing 的渲染能力生成图像,也可以从已有 strokes 组合出新的 drawing。
详细内容
从 PKDrawing.strokes 拆出新的 drawing
(03:37)演讲先展示了一个花朵 drawing。拆开后,每条 PKStroke 都对应用户画下的一条线,顺序就是绘制顺序。练字 demo 利用这一点,把一张包含小写字母的素材 drawing 切成多个字母,再用这些字母合成模板。
import PencilKit
func letterDrawing(from alphabetDrawing: PKDrawing,
strokes range: Range<Int>) -> PKDrawing {
let selectedStrokes = alphabetDrawing.strokes[range]
return PKDrawing(strokes: selectedStrokes)
}
关键点:
alphabetDrawing.strokes是组成 drawing 的PKStroke数组。Range<Int>对应素材 drawing 中某个字母占用的 stroke 区间。PKDrawing(strokes:)接收一组PKStroke,可以直接构造新的 drawing。- 这和 transcript 中“先取 strokes,再按字母切片,最后为每个字母创建新 drawing”的流程一致。
stroke 由 path、ink、transform、mask 组成
(04:57)PKStroke 的核心是 path。path 决定形状,ink 决定颜色和笔刷类型,transform 决定位置与方向,mask 描述被像素橡皮擦裁剪后的区域。renderBounds 会把 path、ink、transform 和 mask 的影响都算进去,适合做命中检测或局部刷新。
import UIKit
import PencilKit
func strokeSummary(_ stroke: PKStroke) -> (ink: PKInk,
bounds: CGRect,
hasMask: Bool) {
return (
ink: stroke.ink,
bounds: stroke.renderBounds,
hasMask: stroke.mask != nil
)
}
关键点:
stroke.ink提供这条线使用的 ink。stroke.renderBounds是渲染后的外接矩形,包含 stroke 宽度、transform 和 mask 的效果。stroke.mask只在 stroke 被 mask 裁剪时存在。- 这里没有读取 path 的控制点,因为控制点本身不在实际曲线上。
用插值点读取真实曲线
(06:11)PKStrokePath 是三次均匀 B 样条。path 里存的是控制点,直接遍历控制点会得到一组不在笔画曲线上的点。要得到曲线上的点,需要用 interpolatedPoints(in:by:),stride 可以按距离、时间或参数步长设置。
import UIKit
import PencilKit
func sampleLocations(from stroke: PKStroke,
every distance: CGFloat = 50) -> [CGPoint] {
return stroke.path
.interpolatedPoints(by: .distance(distance))
.map(\.location)
}
关键点:
stroke.path是PKStrokePath,它描述 stroke 的 B 样条路径。.distance(distance)表示按 drawing 坐标系里的点数采样。interpolatedPoints返回的是曲线上的PKStrokePoint。location才是后续绘制、识别或调试时常用的位置数据。
按时间推进路径动画
(08:54)参数值是浮点数,所以可以取 2.4、4.8 这类控制点之间的位置。演讲用这个能力解释练字 App 的红点动画:每帧取上一帧到当前帧的时间差,再把当前参数值沿 path 按同样时间推进,红点就会以原始书写速度移动。
import UIKit
import PencilKit
func nextMarkerPoint(on stroke: PKStroke,
currentValue: CGFloat,
deltaTime: TimeInterval) -> (value: CGFloat, location: CGPoint) {
let nextValue = stroke.path.parametricValue(currentValue,
offsetBy: .time(deltaTime))
let point = stroke.path.interpolatedPoint(at: nextValue)
return (value: nextValue, location: point.location)
}
关键点:
parametricValue(_:offsetBy:)根据时间、距离或参数步长推进 path 上的位置。.time(deltaTime)使用真实帧间隔,适合动画循环。interpolatedPoint(at:)用新的参数值取曲线上的PKStrokePoint。- 返回新的参数值,下一帧继续从这里推进。
处理被橡皮擦裁剪过的 stroke
(11:54)像素橡皮擦会让 stroke 带上 mask。mask 可能挖出洞,也可能把一条 stroke 分成多段。path 本身没有 mask 信息,所以按整条 path 采样会把已擦掉的部分也算进去。处理这类 stroke 时,应该用 maskedPathRanges 限定可见区间。
import UIKit
import PencilKit
func visibleLocationsInMaskedStroke(_ stroke: PKStroke,
every distance: CGFloat = 12) -> [CGPoint] {
return stroke.maskedPathRanges.flatMap { range in
stroke.path
.interpolatedPoints(in: range, by: .distance(distance))
.map(\.location)
}
}
关键点:
maskedPathRanges是ClosedRange<CGFloat>数组,范围使用 path 的参数值。- 每个 range 都代表 mask 裁剪后仍可见的一段 path。
- range 可能为 0 个,说明剩下的可见片段没有和 path spline 相交。
- 识别算法应基于这些可见区间采样,避免把已经擦掉的笔迹算入结果。
识别和渲染走两条入口
(14:09)演讲把 recognition 分成两个方向。基于 spline 的识别可以使用 maskedPathRanges、插值点、时间、压力等信息;基于图像的识别则使用 PKDrawing 的渲染 API 生成图片。
import UIKit
import PencilKit
func renderedImage(from drawing: PKDrawing, scale: CGFloat) -> UIImage {
return drawing.image(from: drawing.bounds, scale: scale)
}
关键点:
drawing.bounds是包含 drawing 内容的矩形。image(from:scale:)会把指定区域渲染成UIImage。- 这条路径适合图像识别或生成缩略图。
- 如果要比较笔画形状,应优先走 stroke path 和 point 数据,因为那里保留了时间、压力和 mask 信息。
核心启发
1. 练字评分 App
做什么:把标准字母或汉字保存成 PencilKit drawing,用户照着写时实时比较 stroke path。
为什么值得做:session 的 demo 已经展示了模板合成、红点动画和相似度评分这条完整链路。
怎么开始:先用 PKDrawing.strokes 把素材字形切成字符,再用 PKDrawing(strokes:) 组装模板;用户书写后,用 interpolatedPoints(in:by:) 采样模板和用户 stroke,比较路径距离与时间信息。
2. 笔记回放与教学标注
做什么:在数学推导、草图讲解或乐谱批注里,按用户原始书写速度回放每一笔。
为什么值得做:PKStrokePoint.timeOffset 和 parametricValue(_:offsetBy:) 能保留书写节奏,回放时不需要猜测每段路径的速度。
怎么开始:保存当前 stroke 的参数值,每帧用 .time(deltaTime) 推进,再用 interpolatedPoint(at:) 更新光标或高亮位置。
3. 智能橡皮擦后的识别
做什么:用户擦掉笔记的一部分后,识别算法只处理剩余可见笔迹。
为什么值得做:session 明确说明 pixel eraser 会产生 mask,直接读取完整 path 会包含已经擦掉的路径。
怎么开始:对带 mask 的 stroke 遍历 maskedPathRanges,只在这些 range 内插值采样;range 为空时跳过这条 stroke。
4. 手写内容缩略图与索引
做什么:为手写页面生成缩略图,同时把 stroke 采样结果送进自有识别或搜索索引。
为什么值得做:session 给出了两种识别入口:图像识别使用 PKDrawing 渲染结果,spline 识别使用 stroke、point、时间和压力。
怎么开始:用 drawing.image(from:scale:) 生成页面预览;需要更细粒度搜索时,按 stroke 采样 location、force 和 timeOffset,把结果交给识别流程。
关联 Session
- What’s new in PencilKit — iOS 14 PencilKit 更新入口,覆盖 PKCanvasView、PKToolPicker、drawingPolicy 与 stroke 访问入口。
- Meet Scribble for iPad — 讲解 Scribble 如何把 Apple Pencil 手写输入接入标准和自定义文本控件。
- Introducing PencilKit — WWDC19 的 PencilKit 入门 session,介绍 PKCanvasView、PKToolPicker 与在 app 中添加低延迟 Apple Pencil 绘图。
评论
GitHub Issues · utterances