绘制Siri的WaveForm效果

5 graphics core spectrum ios swift

我一直试图了解如何在iOS中绘制Siri的波形效果并遇到了这个伟大的存储库.最终结果如下:

在此输入图像描述

但是我很难理解生成wave的代码发生了什么.我可以生成一个静态正弦波,但是,我似乎并不理解.

特别是当我们计算y的值时,为什么它必须是:

let y = scaling * maxAmplitude * normedAmplitude * sin(CGFloat(2 * M_PI) * self.frequency * (x / self.bounds.width) + self.phase) + self.bounds.height/2.0

源代码:

 //MARK : Properties 


let density : CGFloat =        1
let frequency : CGFloat =      1.5
var phase :CGFloat =           0
var phaseShift:CGFloat =      -0.15
var numberOfWaves:Int =        6
var primaryLineWidth:CGFloat = 1.5
var idleAmplitude:CGFloat =    0.01
var waveColor:UIColor =        UIColor.white
var amplitude:CGFloat =        1.0 {
    didSet {
        amplitude = max(amplitude, self.idleAmplitude)
        self.setNeedsDisplay()
    }
}
Run Code Online (Sandbox Code Playgroud)

方法

  override open func draw(_ rect: CGRect) {
    // Convenience function to draw the wave
    func drawWave(_ index:Int, maxAmplitude:CGFloat, normedAmplitude:CGFloat) {
        let path = UIBezierPath()
        let mid = self.bounds.width/2.0

        path.lineWidth = index == 0 ? self.primaryLineWidth : self.secondaryLineWidth

        for x in Swift.stride(from:0, to:self.bounds.width + self.density, by:self.density) {
            // Parabolic scaling
            let scaling = -pow(1 / mid * (x - mid), 2) + 1

  // The confusing part /////////////////////////////////////////
            let y = scaling * maxAmplitude * normedAmplitude *
     sin(CGFloat(2 * M_PI) * self.frequency * (x / self.bounds.width) + self.phase)  
+ self.bounds.height/2.0

  //////////////////////////////////////////////////////////////////
            if x == 0 {
                path.move(to: CGPoint(x:x, y:y))
            } else {
                path.addLine(to: CGPoint(x:x, y:y))
            }
        }

        path.stroke()
    }

    let context = UIGraphicsGetCurrentContext()
    context?.setAllowsAntialiasing(true)

    self.backgroundColor?.set()
    context?.fill(rect)

    let halfHeight = self.bounds.height / 2.0
    let maxAmplitude = halfHeight - self.primaryLineWidth

    for i in 0 ..< self.numberOfWaves {
        let progress = 1.0 - CGFloat(i) / CGFloat(self.numberOfWaves)
        let normedAmplitude = (1.5 * progress - 0.8) * self.amplitude
        let multiplier = min(1.0, (progress/3.0*2.0) + (1.0/3.0))
        self.waveColor.withAlphaComponent(multiplier * self.waveColor.cgColor.alpha).set()
        drawWave(i, maxAmplitude: maxAmplitude, normedAmplitude: normedAmplitude)
    }
    self.phase += self.phaseShift
}
Run Code Online (Sandbox Code Playgroud)

两个for循环看起来都很数学,我不知道那里发生了什么.提前致谢.

pau*_*uln 6

这是最内部循环的细分,循环x绘制波形.我将在我的解释中稍微详细一点,希望一些额外的信息可能对其他人有用.

        for x in Swift.stride(from:0, to:self.bounds.width + self.density, by:self.density)
        {
Run Code Online (Sandbox Code Playgroud)

循环以density递增的方式迭代UIView的宽度.这允许控制两个属性:(1)波形的"分辨率"和(2)生成UIBezierPath绘制的时间长度.简单地设置density2(in ViewController.swift)会将计算次数减半,并生成一个路径,其中要绘制的元素数量减半.增加density一个完整的数量级(10)可能看起来太多了,但你会很难注意到视觉差异.100如果要查看三角波,请尝试将值设置为.

旁注:由于使用了stride(from:to:by:)如果视图的宽度不能被整除density,波形可能会停留在视图的右侧,因此+ self.density添加了.

            // Parabolic scaling
            let scaling = -pow(1 / mid * (x - mid), 2) + 1
Run Code Online (Sandbox Code Playgroud)

您是否注意到波形似乎如何附加到屏幕两侧的锚点?这就是抛物线缩放所做的事情.要更清楚地看到它,您可以将此公式插入Google的图形功能中以获取此信息:

在此输入图像描述

在该范围内,y遵循曲线,是的,但注意如何y从0开始,在中心上升到正好1.0,然后回落到0.更具体地说,它在x0到1 的范围内这样做.这是关键,因为我们将此曲线映射到视图的宽度,屏幕的左边缘映射到该视图,屏幕x=0的右边缘映射到x=1.

如果我们将此曲线映射到我们的屏幕波形并使用它来缩放幅度(幅度:波形相对于其中心线的大小),您将看到波形的左右端点将具有幅度为0(我们的锚点),波形的大小逐渐增加到中心的全尺寸(1.0).

要查看此缩放的完整效果,请尝试将该行更改为let scaling = CGFloat(1.0).

此时,我们已准备好绘制波形图.这是OP询问的原始代码行:

 let y = scaling * maxAmplitude * normedAmplitude *
 sin(CGFloat(2 * M_PI) * self.frequency * (x / self.bounds.width) + self.phase)  
 + self.bounds.height/2.0
Run Code Online (Sandbox Code Playgroud)

这一切都很重要.这段代码做了同样的事情,但我把它拆分成具有适当名称的临时变量,以帮助理解正在发生的事情:

let unitWidth = x / self.bounds.width

var wave = CGFloat(2 * M_PI)
wave *= unitWidth
wave *= self.frequency

let wavePosition = wave + self.phase

let waveUnitValue = sin(wavePosition)

var amplitude = waveUnitValue * maxAmplitude
amplitude *= scaling
amplitude *= normedAmplitude

let y = amplitude + self.bounds.height/2.0
Run Code Online (Sandbox Code Playgroud)

好吧,让我们一次解决这个问题.我们先从开始unitWidth.还记得当我提到我们要将曲线映射到屏幕的宽度时吗?这就是unitWidth计算的作用:x范围从0到0 self.bounds.width,unitWidth范围从0到1.

接下来是wave.重要的是要注意,该值旨在用于计算正弦波.请注意,该sin函数在Radians中起作用,这意味着正弦波的整个周期范围为0到2π,因此我们将从那里开始(CGFloat(2 * M_PI)).

然后我们应用我们unitWidthwave确定在x视图中给定位置的正弦波内的位置.想象一下:沿着视图的左侧,unitWidth是0,所以这个乘法结果为0(正弦波的开始.)沿着视图的右侧,unitWidth是1.0(给我们全值2π - 正弦波的结束.)如果我们在视图的中间,unitWidth将是0.5,这将使我们在完整的正弦波周期中途.以及介于两者之间的一切.这称为插值.重要的是要理解我们没有移动正弦波,我们正在踩踏它.

接下来,我们申请self.frequencywave.这会缩放正弦波,使得较高的值具有更多的山丘和山谷.频率为1不会做任何事情,我们将遵循自然的正弦波.但这很无聊,所以频率增加了一点(1.5),以提供更好的视觉外观.像盐一样,适应口味.这是频率的3倍:

在此输入图像描述

到目前为止,我们已经定义了我们的正弦波相对于我们所绘制的视图的看法.我们的下一个任务是提出动议.为此,我们将添加self.phasewave.这称为"相位",因为相位是波形内的不同周期.通过连续更改self.phase动画的每个帧,绘图将从波形内的不同位置开始,使其看起来移过屏幕.

最后,我们wavePosition用来计算实际的正弦波值(let waveUnitValue = sin(wavePosition)).我之所以这样称呼waveUnitValue是因为sin()的结果是一个从-1到+1的值.如果我们按原样绘制它,我们的波浪将非常无聊,类似于一条平坦的线条:

在此输入图像描述

"我有需要......需要振幅"

- 没有人

我们amplitude开始通过应用maxAmplitudewaveUnitValue垂直拉伸.为什么从最大值开始?如果我们回到scaling变量的计算,我们会被提醒这是一个单位值 - 一个从0到1的值 - 这意味着它只能减小幅度(或保持不变)但不增加它.

而这正是我们接下来要做的,应用我们的scaling价值.这导致我们的波形在末端具有0的幅度,在中心逐渐增加到全幅度.没有这个,我们会有这样的东西:

在此输入图像描述

最后,我们有normedAmplitude.如果您遵循代码,您将看到drawWave在循环中调用该函数以便将多个波绘制到视图中(这是那些次级或"阴影"波形进入的位置.)normedAmplitude用于选择不同的幅度对于作为整体效果的一部分绘制的每个波形.

值得注意的是,normedAmplitude可以为负,这允许阴影波形垂直翻转,填充波形的空白区域.尝试更改normedAmplitude原始代码中的使用,abs(normedAmplitude)您将看到类似的内容(结合3倍频率示例以突出显示差异):

在此输入图像描述

最后一步是将波形置于视图(amplitude + self.bounds.height/2.0)中心,这将成为y我们用于绘制波形的最终值.

所以,嗯.而已.

  • 感谢您的精彩回答。 (3认同)