2017-02-16 26 views
2

Я пытался понять, как рисовать эффект Сири в iOS и натолкнулся на отличный репозиторий this. Окончательный результат выглядит следующим образом:Рисование эффекта WaveForm от Siri

enter image description here

Однако я трудно понять, что происходит с кодом, который генерирует waves.I может генерировать один статический синусоиду, но это, я не вполне понятно.

Особенно, когда мы вычисляем значение у, почему это должно быть:

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() 
    } 
} 

Метод

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 
} 

И для петли кажутся очень математическими, я не знаю, что происходит в е. Спасибо заранее.

+0

Я не уверен, что вопрос здесь ... – Abizern

+0

Я хотел короткое объяснение кода внутри метода 'draw (rect)', но вопрос неясен. Поэтому я спрашиваю об этом .. почему «let y = масштабирование * maxAmplitude * normedAmplitude * sin (CGFloat (2 * M_PI) * self.frequency * (x/self.bounds.width) + self.phase) + self.bounds. высота/2.0' –

ответ

1

Вот пробой самой внутренней петли, которая проходит через x, чтобы нарисовать форму волны. Я подробно расскажу о своем объяснении в надежде, что некоторые дополнительные сведения могут быть полезны другим.

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

В итерации цикла через ширину UIView путем приращения density. Это позволяет контролировать два свойства: (1) «разрешение» формы волны и (2) сколько времени он проводит, генерируя UIBezierPath, который нарисован. Просто установка density на 2ViewController.swift) сократит количество вычислений пополам, а также создаст путь с половиной количества элементов для рисования. Увеличение density на полный порядок (10) может показаться чересчур большим, но вам будет трудно заметить визуальную разницу. Попробуйте установить значение 100, если вы хотите увидеть треугольную волну.

Примечание стороны: за счет использования stride(from:to:by:), если ширина точка зрения не является равномерно делится на density, сигнал может не доходить на правой стороне точки зрения, так + self.density был добавлен.

  // Parabolic scaling 
      let scaling = -pow(1/mid * (x - mid), 2) + 1 

Вы заметили, как форма волны, кажется, прикреплен к точке крепления на обеих сторонах экрана? Вот что делает это параболическое масштабирование. Чтобы увидеть это более ясно, вы можете plug this formula в функциональности графиков Google, чтобы получить это:

enter image description here

В этом диапазоне, y повторяет кривую, да, но обратите внимание, как y начинается с 0, поднимается ровно 1,0 в центр, затем падает до 0. Более конкретно, он делает это в диапазоне x от 0 до 1. Это ключ, потому что мы будем сопоставлять эту кривую с шириной вида, где левый край экрана карты до x=0, а правый край экрана соответствует x=1.

Если мы сопоставим эту кривую с нашей экранной формой волны и используем ее для масштабирования амплитуды (амплитуда: размер формы волны относительно ее центральной линии), вы увидите, что левая и правая конечные точки формы волны будет иметь амплитуду 0 (наши опорные точки) с размером формы волны, постепенно увеличивающейся до полноразмерного (1.0) в центре.

Чтобы увидеть полный эффект этого масштабирования, попробуйте изменить эту строку на let scaling = CGFloat(1.0).

На этом этапе мы готовы нарисовать форму волны. Вот исходная строка кода, о которой спрашивал ОП:

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

Это много, чтобы взять все сразу. Этот код делает то же точную вещь, но я разбил его на части во временные переменные с соответствующими именами, чтобы помочь в понимании того, что происходит:

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 

Хорошо, давайте решать этот один бит за один раз. Начнем с unitWidth. Помните, когда я упоминал, что мы собираемся отображать кривую на ширину экрана? Это то, что делает это unitWidth расчет: в x диапазоне от 0 до self.bounds.width, unitWidth будет находиться в диапазоне от 0 до 1.

Далее идет wave. Важно отметить, что это значение предназначено для расчета синусоидальной волны. Обратите внимание, что функция sin работает в Radians, что означает, что полный период синусоидальной волны будет находиться в диапазоне от 0 до 2π, поэтому мы начнем там (CGFloat(2 * M_PI)).

Затем мы применяем наш unitWidth к wave, который определяет, где, в пределах синусоиды, мы хотим быть для данной позиции x на вид. Подумайте об этом так: по левой стороне обзора unitWidth равно 0, поэтому это умножение приводит к 0 (начало синусоиды). В правой части окна unitWidth равно 1.0 (дает нам полную значение 2π - конец синусоидальной волны.) Если мы находимся в середине обзора, то unitWidth будет 0,5, что даст нам полпути через весь период синусоиды. И все между ними. Это называется интерполяцией. Важно понимать, что мы не двигаем синусоидальную волну, мы ее преодолеваем.

Далее мы применяем self.frequency к wave. Это масштабирует синусоидальную волну, так что более высокие значения имеют больше холмов и долин. Частота 1 не будет делать ничего, и мы будем следовать естественной синусоидальной волне. Но это скучно, поэтому частота увеличивается немного (1,5), чтобы улучшить внешний вид. Как соль, приспосабливайтесь к вкусу. Здесь на 3x частоте:

enter image description here

До сих пор мы определили, как наша синусоида будет выглядеть по сравнению с точки зрения, что мы рисуем его. Наша следующая задача - дать ему движение. С этой целью мы добавим self.phase в wave. Это называется «фазой», потому что фаза представляет собой отчетливый период внутри формы волны. Постоянно изменяя self.phase для каждого кадра анимации, рисунок будет начинаться в другом положении в пределах формы волны, заставляя его перемещаться по экрану.

Для расчета фактического значения синусоидальной волны (let waveUnitValue = sin(wavePosition)) используется wavePosition. Я назвал это waveUnitValue, потому что результат sin() - это значение, которое варьируется от -1 до +1.Если мы рисовали его как есть, наша волна будет довольно скучной, напоминающей почти плоская линия:

enter image description here

«У меня есть потребность ... потребность в амплитуду»

- Nobody

Наши amplitude начинается с применения maxAmplitude к waveUnitValue, растягивая его по вертикали. Зачем начинать с максимума? Если мы вернемся к этому вычислению переменной scaling, нам следует напомнить, что это единичное значение - значение, которое находится в диапазоне от 0 до 1, что означает, что оно может уменьшить амплитуду (или оставить ее неизменной), но не увеличьте его.

И это именно то, что мы будем делать дальше, примените наше значение scaling. Это приводит к тому, что наш сигнал имеет амплитуду 0 на концах, постепенно увеличиваясь до полной амплитуды в центре. Без этого мы бы что-то, что выглядит следующим образом:

enter image description here

Наконец, мы имеем normedAmplitude. Если вы выполните код, вы увидите, что функция drawWave вызывается в цикле для того, чтобы нарисовать несколько волн в виде (в этом случае появляются те вторичные или «тени»). normedAmplitude используется для выбора различной амплитуды для каждой из осциллограмм, нарисованных как часть общего эффекта.

Интересно отметить, что нормированная амплитуда может быть отрицательной, что позволяет отражать формы тени в вертикальном направлении, заполняя пустые пространства формы сигнала. Попробуйте изменить использование normedAmplitude в исходном коде abs(normedAmplitude) и вы увидите что-то вроде этого (в сочетании с примером частоты 3x, чтобы подчеркнуть разницу):

enter image description here

Последний шаг заключается в центровки сигнала на вид (amplitude + self.bounds.height/2.0), который становится окончательным значением y, которое мы будем использовать для рисования формы волны.

Итак, um. Вот и все.

+0

Спасибо за удивительный ответ. –