苹果铅笔入门教程

来源:互联网 发布:ubuntu wordpress 安装 编辑:程序博客网 时间:2024/04/28 01:42
  • 原文链接 : Apple Pencil Tutorial: Getting Started
  • 原文作者 : Caroline Begbie
  • 译者 : kmyhy

有许多人都在为拥有了新的 iPad Pro 以及上面附带的苹果铅笔而兴奋不已。

如果你像我一样,体会到了用笔涂鸦的好处,就会恨不得在你所有的 App 中支持苹果铅笔特性。

自从我购买了第一代的 iPad 开始,我就在等待这样的设备出现了。就如我的涂鸦作品中所看到的,我不是伦布兰特(译者注:荷兰画家),但我仍然可以用苹果铅笔记录我所想。我不禁在心中幻想,如果艺术家们使用苹果铅笔作画,他们又能创造出多么令人惊异的作品?

在本教程中,你将学习如何在 App 中支持苹果铅笔。有几个关键的知识点:

  • 如何控制力量

  • 如何更加精确

  • 如何绘制阴影

  • 如何使用橡皮擦

  • 如何通过预判和实际绘制来提升体验

最终,你将了解如何将苹果铅笔集成到你的 App 中!

前提

要学习本教程,你需要具备:

  • 一个 iPad Pro 和一只苹果铅笔。无法在模拟器中测试苹果铅笔。同时,苹果铅笔无法在老的 iPad 设备是运行,只支持新的 iPad Pro。

  • Xcode 7.1 及以上,iOS 9.1 及以上。

  • 对 Core Graphics 有一定的了解。你需要知道什么是上下文,如何创建上下文以及如何绘制线条。你可以看一下我们的 Core Graphics 教程第一部分 —— 这将大大加快你的学习速度。这个 App 会提醒你去喝水 :]

开始

通过本教程,你会编写一个 App,叫做 Scribble。这是一个简单的 App,功能是允许你使用响应式 UI 比如压力感应和阴影绘制进行涂鸦。

下载并浏览 Scribble 项目。在你的 iPad Pro 上运行这个程序,使用苹果铅笔或者手指,在屏幕上画画,在画的时候让你的手保持放松。

你会发现,和老式的 iPad 不同,将手掌放到屏幕上是没有反应的,你必须小心地让手的大部分面积保持悬空,因为只有小面积的接触被视作触摸。

晃动 iPad 可以清空屏幕 —— 就像一块神奇画板!

实际上,Scribble 只是一个简单的 App,它由一块 cavas (画布)视图构成,cavas 负责俘获来自于苹果铅笔或手指的触摸。它通过不停地刷新画面来显示你的触摸。

请看一眼 CanvasView.swift 中的代码。

最核心的代码位于 touchesMoved(_:withEvent:) 方法,这个方法在用户与 canvas 视图产生交互时触发。在这个方法中,创建了一个 Core Graphics 上下文并将图形绘制到图形上下文中。

touchesMoved(_:withEvent:) 方法会调用 drawStroke(_:touch:) 方法,后者会在图形上下文中绘制凑够上一次触摸到当前触摸之间的线段。

touchesMoved(_:withEvent:) 方法将图形上下文中画好的图形用于更新 canvas 视图。

看到了吧?是不是很简单?:]

第一次用苹果铅笔进行绘图

哪怕是在数位环境中,用手指来作画也并非易事。而苹果铅笔则更接近于以往传统的作画方式,就像用纸和笔在作画一样。

现在,准备使用苹果铅笔的第一个特性 —— 力量。当笔尖在屏幕上用力压的时候,笔画变得更粗了。这个特性无法在使用手指时获得,这个技巧我们随后将会学到。

力量的大小保存在 touch.force 中。一个 1.0 的力是一个力量中等的触摸,为了获得适当的笔画粗细,我们需要乘以一定的系数。请接着往下看……

打开 CanvasView.swift,在类的顶部加入一个常量声明:

private let forceSensitivity: CGFloat = 4.0

你可以修改 forceSensitivity 的值,可以让笔画粗细对压力的敏感度增强或者减弱。

找到 lineWidthForDrawing(_:touch:)。这个方法用于计算笔画的粗细。在返回语句之前,加入:

if touch.force > 0 {  lineWidth = touch.force * forceSensitivity}

在这段代码中,我们用 touch.force 乘以 forceSensitivity 来计算笔画宽度,但请注意,这只对苹果铅笔有效而对手指无效。如果你使用手指作画,则 touch.force 应当为 0,这样你就不会改变笔画的宽度。

编译运行程序。用苹果铅笔胡乱画一些内容,你会注意到笔画的粗细会根据笔尖在屏幕上所施加的力道的大小而变:

让线条更光滑

在画的时候你可能注意到了,线条上有一些尖锐的突出点,而不是光滑曲线。在苹果铅笔出现之前,为了更像传统作画我们必须做一些很复杂的事情,比如将笔画转换成样条曲线。但有了苹果铅笔之后,这种方法完全没有必要了。

苹果告诉我们,iPad Pro 对一个触摸每秒扫描 120 次,但当苹果铅笔放到屏幕上时,这个扫描频率立即放大了 2 倍,即每秒 240 次。

iPad Pro 的屏幕刷新率是 60 Hz,即每秒 60 次。也就是说,120 Hz 的扫描速度理论上在每一次刷新都能够识别出 2 个触摸。如果在需要处理大量事务的情况下,某一帧中的触摸有可能会丢失一些,因为主线程阻塞导致来不及处理。

试着快速画一个圆。它应该是圆的,但结果更像是一个多边形:

苹果用“合并的触摸”来解决这个问题。基本上,它们会将丢失的触摸放到一个新的 UIEvent 数组中,你可以用 coalescedTouchesForTouch(_:) 方法来访问。

找到 CanvasView.swift 中的 touchesMoved(_:withEvent:) 方法,将这一句:

drawStroke(context, touch: touch)

替换为:

// 1var touches = [UITouch]()// 2if let coalescedTouches = event?.coalescedTouchesForTouch(touch) {  touches = coalescedTouches} else {  touches.append(touch)}// 3print(touches.count)// 4for touch in touches {  drawStroke(context, touch: touch)}

让我们来过一遍这些代码。

  1. 首先,创建一个数组,用于保存我们要处理的的所有触摸。

  2. 检查“合并的触摸”,如果不为空,我们将“合并的触摸”添加到新数组中。如果为空,我们只需要添加一个触摸到数组。

  3. 用一个 Log 语句打印 touches 数组的长度。

  4. 最终,将原来只调用一次 drawStroke(_:touch:) 方法替换为针对 touches 数组中的每一个触摸都调用一次 drawStroke(_:touch:) 方法。

运行程序,用苹果铅笔随意绘制一些圆,你会发现笔画光滑顺溜,粗细控制由心:

注意控制台中的输出。你会看到当你使用笔作画时,所接收到的触摸要比用手指作画时要多得多。

此外,通过使用“合并的触摸”,用笔绘制的圆要更圆滑,因为 iPad Pro 一旦侦测到你使用的是笔,扫描的次数会增加一倍。

倾斜

现在你已经爱上了这种在 App 中流畅作画的方式。但是,如果你看过关于苹果铅笔的评论,应该还记得它有一个像铅笔一样的绘制阴影的功能。而这只需要用户倾斜笔尖。但随即你就会发现阴影并不会自动出现 —— 这还需要我们的开发者编写代码它才会出现 :]

高度、方位角和单位向量

本节,我将介绍如何识别倾斜。而在下一节,你将支持简单的阴影绘制功能。

在使用苹果铅笔时,你可以在 3 个维度上进行旋转。上、下方向的旋转,我们称之为 Altitude(高度),而水平方向上的旋转称之为 Azimuth(方位角):

在 iOS 9.1 中,UITouch 有一个新的属性 altitudeAngle,这个属性专门用于苹果铅笔。这是一个以弧度为单位的角度。当平放于 iPad 表面时,这个 altitude 为 0。当笔直立在屏幕表面时,altitude 为 π/2。要知道 2π 为一个整圆,因此 π/2 就是 90 度。

在 UITouch 上有两个新方法,用于获取 azimuth:azimuthAngleInView(_:) 以及 azimuthUnitVectorInView(_:)。性能代价最小的是 azimuthUnitVectorInView(_:) 方法,但这两个方法都很有用。要用哪一个方法,取决于你需要计算什么。

你会看到如何使用 azimuth 的单位向量。注,一个单位向量代表一个从原点(0,0) 指向某个方向的长度为 1 的向量:

你可以来试一试,在 touchesMoved(_:withEvent:) 方法中,在 guard 语句后面添加如下代码:

print(touch.azimuthUnitVectorInView(self))

运行程序。将 iPad 置为横屏模式 —— Scribble 只支持横屏,以便我们将注意力集中在苹果铅笔 —— 手握苹果铅笔,将笔尖放在屏幕左边,然后向右边倾斜。

在控制台中,你可能无法看到非常精确的值,但这个向量应该近似于: x 方向为 1,y 方向为 0,即(1,0)。然后沿反时针方向旋转 90 度,直到笔尖指向 iPad 的下方。这个方向的值应该约等于(0,-1)。

注意,x 方向是 cosine(余弦),y 方向是 sine(正弦)。例如,如果你向上图一样握笔 —— 从原来的水平方向逆时针旋转 45 度 —— 则单位向量为(cos(45),sine(-45)) 或者 (0.7071,-0.7071)。

注意:如果你对向量数学了解不多,则最好先预习一下。这个教程 Sprite Kit 游戏中的三角学也许对你有所帮助。

了解完苹果铅笔的方向的改变和它笔尖的指向的关系之后,将上述打印语句从代码中删除。

绘制阴影

现在,你已经知道如何计算出笔的倾斜,是时候在 Scribble 中添加一些阴影了。

如果笔处于一种自然作图的角度,你通过力道大小来决定笔画的粗细,当用户将笔倾斜,你就可以用力道大小来决定阴影的透明度。

你可以通过笔画的方向以及执笔的方向来计算线条的厚薄。

如果你还不太明白,你可以找一张纸和一只铅笔,试着将笔倾斜到一边进行描绘,这样笔尖和纸张的接触面积将被最大化。当你向笔倾斜的方向进行绘制时,阴影是细的。但当你以放笔的方向成 90 度方向绘制时,笔划将最粗:

使用纹理

事情的第一步是修改线条所使用的纹理,以使它更接近于真正的铅笔。初始项目的资源集中包含了一张图片 PencilTexture ,我们将用它做纹理。

在 CanvasView 头部加入属性声明:

private var pencilTexture = UIColor(patternImage: UIImage(named: "PencilTexture")!)

这将允许我们在作图中把 pencilTexture 作为颜色一样使用,而不是现在所使用的默认的红色。

在 drawStoke(_:touch:) 方法中找到如下语句:

drawColor.setStroke()

修改为:

pencilTexture.setStroke()

运行程序。哈!你的线条现在已经和真正的铅笔很像了:

注意:在本教程,我们使用纹理的方式十分简单。在真正的绘画类 App 中,笔触的实现是非常复杂的,但作为入门,上面的这个方法就足够了。

为了明确笔要旋转到什么程度才足以构成阴影,请在 CanvasView 的开始加入常量声明:

private let tiltThreshold = π/6  // 30º

因为每个人握笔姿势的差异,这个值不一定适合于你,你可以改变这个值。

注意:同时按下 Option 键和 p 键可以输入 π 字符。π 是 CanvasView.swift 中定义的常量,等于 CGFloat(M_PI)。

在编写图形程序时,在弧度和度之间来回转换是非常频繁的。关于度和弧度之间的关系,请看来自于维基百科的这张图片。

接着,在 drawStroke(_:touch:) 方法中找到这句:

let lineWidth = lineWidthForDrawing(context, touch: touch)

修改为:

var lineWidth:CGFloatif touch.altitudeAngle < tiltThreshold {  lineWidth = lineWidthForShading(context, touch: touch)} else {  lineWidth = lineWidthForDrawing(context, touch: touch)}

这里,我们增加了一个片段,检查笔的倾角(altitud)是否大于 π/6 即 30 度。如果是,调用 shading 方法,否则调用 drawing 方法。

然后,在 CanvasView 中增加这个方法:

private func lineWidthForShading(context: CGContext?, touch: UITouch) -> CGFloat {  // 1  let previousLocation = touch.previousLocationInView(self)  let location = touch.locationInView(self)  // 2 - vector1 is the pencil direction  let vector1 = touch.azimuthUnitVectorInView(self)  // 3 - vector2 is the stroke direction  let vector2 = CGPoint(x: location.x - previousLocation.x, y: location.y - previousLocation.y)  // 4 - Angle difference between the two vectors  var angle = abs(atan2(vector2.y, vector2.x) - atan2(vector1.dy, vector1.dx))  // 5  if angle > π {    angle = 2 * π - angle  }  if angle > π / 2 {    angle = π - angle  }  // 6  let minAngle: CGFloat = 0  let maxAngle = π / 2  let normalizedAngle = (angle - minAngle) / (maxAngle - minAngle)  // 7  let maxLineWidth: CGFloat = 60  var lineWidth = maxLineWidth * normalizedAngle  return lineWidth}

因为数学计算比较复杂,我们来捋一捋这些代码:

  1. 分别将上一个触摸和当前侦测到的触摸保存到局部变量。

  2. 保存笔的方位角向量。

  3. 将当前笔画的方向向量保存到局部变量。

  4. 计算两个方向向量之差。

  5. 将上一步的计算结果限制到 0-90 度之间。如果角度为 90 度,笔画为最粗。注意所有的角度都是弧度,π/2 为 90 度。

  6. 将上一步的结果进行归一化,即转换为 0-1 之间的值,1 表示 90 度。

  7. 将最大线宽(60)乘以前面得到单位向量,即得到相应的阴影宽度。

注意:无论你是否使用笔,下面的公式都是适用的:
求向量的角度:角度 = atan2(对边,邻边)
归一化:归一化 = (值 - 最小值)/(最大值 - 最小值)

运行程序。以下图所示角度握笔,你就可以进行描影法作图了。不要改变角度,画出一张描影图。

观察随着笔画方向的变化,线条发生了粗细不同的变化。以这种方式实现的描影图,虽然看起来斑斑点点,但我们却看到了无限的可能。

用方位角改变宽度

还有一件事情需要做:如果你用真实的铅笔以90度角作画,线条会变得比你用其他角度画要窄。但是,如果你试图用苹果铅笔这样做,线条的宽度不会改变。

除了方位角,你还应该在计算线条宽度时考虑进铅笔的高度。

在 CanvasView 类中增加常量声明:

private let minLineWidth: CGFloat = 5

这是描影图线条的最小宽度 —— 你可以根据自己的喜好修改它 :]

在 lineWidthForShading(_:touch:) 方法最后,返回语句之前,加入代码:

// 1    let minAltitudeAngle: CGFloat = 0.25let maxAltitudeAngle = tiltThreshold// 2let altitudeAngle = touch.altitudeAngle < minAltitudeAngle ? minAltitudeAngle : touch.altitudeAngle// 3let normalizedAltitude = 1 - ((altitudeAngle - minAltitudeAngle) / (maxAltitudeAngle - minAltitudeAngle))// 4lineWidth = lineWidth * normalizedAltitude + minLineWidth

注意:请确认在 lineWidthForShading(_:touch:) 方法中添加代码,而不是在 lineWidthForDrawing(_:touch:) 方法中。

这段代码分别做了以下事情:

  1. 理论上,笔的最小高度是 0,表明笔被平放在 iPad 上,同时笔尖没有接触到屏幕,但这样的话,altitude 根本是无法获得的。实际的最小值应当是 0.2 左右,但这里将它定为 0.25。

  2. 如果 altitude 小于最小值,用最小值代替。

  3. 就像我们在前面做的,将 altitude 归一化为 0-1 之间的值。

  4. 最终,将用 azimuth(方位角)算出的线宽乘以上面归一化向量的值,然后加上最小线宽。

运行程序,当你使用描影作图时,改变比的高度,线条会变窄或宽。加入高度之后,会让你画出的线条变得更加连续和平滑:

改变透明度

本节最后一个任务是随着力道的大小改变纹理的透明度,让描影图看起来更加真实。

在 lineWidthForShading(_:touch:) 方法返回语句之前,加入:

let minForce: CGFloat = 0.0let maxForce: CGFloat = 5let normalizedAlpha = (touch.force - minForce) / (maxForce - minForce)CGContextSetAlpha(context, normalizedAlpha)

看过之前介绍的代码,这里的代码其作用应当是显而易见的了。我们简单地读取了 force 属性,并对它进行归一化,然后用它来设置图形上下文的 alpha 值。

运行程序,用不同的压力进行作画:

手指 vs 苹果铅笔

如果你和我一样,这时已经画错了好几个地方了,这时候你可能会想将这些地方擦掉。

在本节,你将看到手指和苹果铅笔用起来有什么不同。具体地说,你将修改 App,让手指来扮演一个忠实的橡皮擦。

要区分当前触摸的是手指还是苹果铅笔非常简单 —— 直接使用 UITouch 的 type 属性。

在 CanvasView 头部加入一个属性,用于保存橡皮擦的颜色。你可以在 canvas 上画出背景色,以这种方式来充当橡皮擦的“擦除”功能。这法子真不错,嗯?:]

private var eraserColor: UIColor {  return backgroundColor ?? UIColor.whiteColor()}

这里我们将 eraseColor 设置为视图的背景色,如果背景色为空,则将它设置为白色。接着在 drawStoke(_:touch:) 方法中找到下列代码:

if touch.altitudeAngle < tiltThreshold {  lineWidth = lineWidthForShading(context, touch: touch)} else {  lineWidth = lineWidthForDrawing(context, touch: touch)}pencilTexture.setStroke()

替换为:

if touch.type == .Stylus {  if touch.altitudeAngle < tiltThreshold {    lineWidth = lineWidthForShading(context, touch: touch)  } else {    lineWidth = lineWidthForDrawing(context, touch: touch)  }  pencilTexture.setStroke()} else {  lineWidth = 20  eraserColor.setStroke()}

这里我们增加了一个判断,检查触摸的类型是手指还是笔,如果是手指,我们修改线条的宽度并应用橡皮擦颜色进行绘制。

运行程序,现在你可以用手指来修理不整齐的毛边或者擦去任何东西!

在模拟手指上的压力

你知道在 iOS 8 中可以模拟出指尖触摸在屏幕上的压力吗?UITouch 有一个属性 majorRaiuds,顾名思义,可以表示触摸的大小。在前面的代码块中找到这行代码:

lineWidth = 20

替换为:

lineWidth = touch.majorRadius / 2

运行程序。用描影法画出一块颜色,然后分别用指尖和指腹擦出粗细不同的痕迹:

在你已经习惯了实用苹果铅笔涂鸦之后,是不是感觉到用手指作画更加别扭和痛苦?:]

减少延迟

你可能以为在笔尖从 iPad 上面划过的一瞬间就会立即画出线条。实际不是这样 —— 这只是想象而已,以为在触摸和绘制线条之间会存在延迟。苹果有一个专门对付这个问题的概念,叫做:触摸预判。

这简直让人难以置信,全知的苹果可以知道笔或者手指即将画到哪里。这个预判就保存在 UIEvent 的一个数组里。你可以提前绘制这些预判的触摸。是不是很酷?:]

在开始使用预判触摸之前,需要解决一点小小的技术障碍。现在,我们在图形上下文中的绘图操作,会立即显示在 canvas 视图中。

你需要将预判触摸绘制到 canvas,但当真实的触摸到来时,应该撤销预判的触摸。

例如,如果你根据预判绘制了一个 S 形曲线,但后来发现实际绘制时的方向跟预判不符,则预判是错误的,应当被撤销。下图演示了这个问题。S 形曲线用红色表示,预判的触摸显示为蓝色。

要解决这个问题,你需要:

  • 增加一个 UIImage 属性,名为 drawingImage,用于保存图形上下文中的真实的 —— 而非预判的 —— 触摸。

  • 在触摸移动事件处理方法中,将 drawingImage 绘制到图形上下文。

  • 将真实的触摸绘制到图形上下文,同时将其保存到 drawingImage 属性,而不是 canvasView.image。

  • 将预判的触摸绘制到图形上下文。

  • 图形上下文绘制完预判的触摸后,将被赋给 canvasView.image,并显示给用户。

通过这种方式,预判的触摸并不会绘制到 drawingImage 中,每当一个触摸事件到达,所绘制的预判的触摸都会被删掉。

家庭作业:删除预判的笔画

为了确保在每个笔画的最后或者用户取消绘制后,预判触摸会被正确地撤销,我们布置了一个家庭作业。

在 CanvasView 顶部增加一个 UIImage 属性,用于保存真实的绘图 —— 不包含任何预判的触摸:

private var drawingImage: UIImage?

接着,在 touchesMove(_:withEvent:) 方法中找到这句:

image?.drawInRect(bounds)

替换为:

drawingImage?.drawInRect(bounds)

这里,我们将 drawingImage 绘制到了图形上下文,这样 image 就不会立即显示到 canvas 视图。这将导致上一次触摸事件绘制的预判被覆盖。

然后,在 touchesMoved(_:withEvent:) 方法最后,找到这些语句:

image = UIGraphicsGetImageFromCurrentImageContext()UIGraphicsEndImageContext()

在上述语句之前加上:

// 1drawingImage = UIGraphicsGetImageFromCurrentImageContext()// 2if let predictedTouches = event?.predictedTouchesForTouch(touch) {  for touch in predictedTouches {    drawStroke(context, touch: touch)  }}

上面的代码完成了以下工作:

  1. 将图形上下文保存到 drawingImage,其中包含了刚刚绘制的新的笔画,但不包含预判的笔画。

  2. 就像我们在“合并的触摸”中所做的,我们读取预判触摸数组,然后对每个预判触摸进行笔画的绘制。

然后增加两个方法:

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {  image = drawingImage}override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {  image = drawingImage}

这两个方法在笔画结束时会被调用。当一个触摸结束或者取消,我们将 drawingImage 赋给 image 属性,这样就取消了所有绘制到 canvas 上的预判的触摸。

最后还有一件事情:你要在“摇晃” iPad 时清除 canvas 以及 drawingImage 属性。

在 CanvasView.swift 的 clearCanvas(animated:) 方法,找到位于动画闭包中的这一句:

self.image = nil

在这一句之后加入:

self.drawingImage = nil

就在同一个方法的稍后一点,找到这句:

image = nil

同样在后面加上:

drawingImage = nil

这样,你就清除了曾经画过的所有图形。

运行程序,胡乱画一些字和线条。你会注意到,在绘制时会将苹果预判的触摸也绘制出来,这样就大大降低了延迟感。你可能要仔细观察才能发现,因为这种现象并不明显。:]

注意:当你运行本教程最后的第二个代码示例时,你可以直观地看到预判触摸的效果。在代码中,你可以将预判触摸的纹理替换成蓝色。

苹果的预判触摸算法给了我们一个惊喜。由于这样的小玩意存在,在苹果平台上进行开发变成了一件让人心情愉快的事情。

接下来做什么

恭喜你!你完成了一个简单的绘画类 App,它能让你用苹果铅笔进行涂鸦或者艺术创作。:] 你可以下载完整的项目来预览最终效果。

你完成了一件来不起的创举,同时学到了如下知识:

  • 绘制自然的光滑曲线和形状

  • 使用高度和方位角

  • 实现画图和描影法作图

  • 添加和使用纹理

  • 添加橡皮擦功能

  • 使用预判以及何时撤销它们

我也提供了另一个项目示例,它有几个按钮,用于打开/关闭“合并的触摸”和“预判的触摸”,以让你对二者有一个更直观的了解。

苹果的 WWDC 的视频 on Touches上专门有一节关于在 60 Hz 的帧率下“合并的触摸”和“预判的触摸”的内容。你可以观看这个视频,视频中描述了如何改善延迟,这种方法在 iOS 8 上能提升 4 帧而 iOS 9.1 上能提升 1.5 帧。内容非常之详实!

FlexMonkey(即 Simon Gladman) 用苹果铅笔制作了一些非常棒的东东,绝不仅限于用它来作画。你可以看一看他的博客,尤其是 Pencil Synthesizer 和 FurrySketch。

我希望你能喜欢这篇苹果铅笔教程 —— 我乐于看到有越来越多的 App 集成这个超酷的设备。如果你有疑问或建议,请加入下面的讨论!

0 0
原创粉丝点击