2017-02-16 1 views
2

J'ai essayé de comprendre comment dessiner l'effet de vague de Siri dans iOS et j'ai rencontré this un excellent référentiel. Le résultat final ressemble à ceci:Dessiner l'effet WaveForm de Siri

enter image description here

Cependant, j'ai du mal à comprendre ce qui se passe avec le code qui génère le waves.I peut générer une onde sinusoïdale unique statique mais cela, je ne semble comprendre.

En particulier lorsque l'on calcule la valeur de y, pourquoi doit-il être:

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

Code Source:

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

Méthode

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 
} 

Les deux pour boucles semblent très mathématique, je n'ai aucune idée de ce qui se passe dans ther e. Merci d'avance.

+0

Je ne sais pas quelle est la question ici – Abizern

+0

Je voulais une brève explication du code dans la méthode 'draw (rect)' mais la question est vague. Donc je demande cela ... pourquoi est-ce que let = scaling * maxAmplitude * normedAmplitude * sin (CGFloat (2 * M_PI) * self.frequency * (x/self.bounds.width) + self.phase) + self.bounds. height/2.0' –

Répondre

1

Voici une répartition de la boucle la plus interne, qui passe par x pour dessiner la forme d'onde. Je vais obtenir un peu plus de détails dans mes explications dans l'espoir que quelques informations supplémentaires pourraient être utiles aux autres.

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

Les itère en boucle à travers la largeur de la UIView par un incrément density. Cela permet le contrôle de deux propriétés: (1) la "résolution" de la forme d'onde et (2) combien de temps il dépense pour générer le UIBezierPath qui est dessiné. Régler simplement density sur 2 (dans ViewController.swift) permet de réduire le nombre de calculs de moitié et de créer un chemin avec la moitié d'autant d'éléments à dessiner. Augmenter density par un ordre de grandeur (10) peut sembler trop, mais vous auriez du mal à remarquer une différence visuelle. Essayez de définir la valeur sur 100 si vous voulez voir une onde triangulaire.

Side note: en raison de l'utilisation de stride(from:to:by:) si la largeur de la vue ne sont pas divisibles par density, la forme d'onde peut s'arrêter du côté droit de la vue, donc + self.density a été ajouté.

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

Avez-vous remarqué comment la forme d'onde semble être attachée à un point d'ancrage des deux côtés de l'écran? C'est ce que fait cette mise à l'échelle parabolique. Pour y voir plus clair, vous pouvez plug this formula dans la fonctionnalité graphique de Google pour obtenir ceci:

enter image description here

Dans cette plage, y suit une courbe, oui, mais remarquez comment y commence à 0, monte à exactement 1,0 à le centre, puis redescend à 0. Plus précisément, il le fait dans la plage de x de 0 à 1. C'est la clé parce que nous allons cartographier cette courbe à la largeur de la vue, où le bord gauche de l'écran correspond à x=0 et le bord droit de l'écran correspond à x=1.Si nous mappons cette courbe sur notre forme d'onde à l'écran et que nous l'utilisons pour redimensionner l'amplitude (amplitude: la taille de la forme d'onde par rapport à sa ligne centrale), vous verrez que les extrémités gauche et droite de la forme d'onde aurait une amplitude de 0 (nos points d'ancrage) avec la taille de la forme d'onde augmentant graduellement à la taille normale (1,0) au centre.

Pour voir l'effet complet de cette mise à l'échelle, essayez de remplacer cette ligne par let scaling = CGFloat(1.0).

À ce stade, nous sommes prêts à tracer la forme d'onde. Voici la ligne de code originale que le PO demandait à propos de:

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

C'est beaucoup à prendre en même temps. Ce code fait exactement la même chose, mais je me suis cassé à part dans des variables temporaires avec des noms appropriés pour aider à comprendre ce qui se passe:

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 

D'accord, nous allons aborder ce un peu à la fois. Nous allons commencer par unitWidth. Rappelez-vous quand j'ai mentionné que nous allions cartographier la courbe à la largeur de notre écran? C'est ce que ce calcul unitWidth fait: comme x va de 0 à self.bounds.width, unitWidth vont de 0 à 1.

up est wave suivante. Il est important de noter que cette valeur est destinée à calculer une onde sinusoïdale. Notez que la fonction sin fonctionne en Radians, ce qui signifie que la période complète d'une onde sinusoïdale va de 0 à 2π, donc nous allons commencer là (CGFloat(2 * M_PI)).

Ensuite, nous appliquons notre unitWidth à wave qui détermine où, dans l'onde sinusoïdale, nous voulons être pour une position donnée x dans la vue. Pensez-y comme ceci: Sur le côté gauche de la vue, unitWidth est 0, donc cette multiplication se traduit par 0 (le début d'une onde sinusoïdale.) Sur le côté droit de la vue, unitWidth est 1,0 (nous donnant la pleine valeur 2π - la fin de l'onde sinusoïdale.) Si nous sommes au milieu de la vue, unitWidth sera de 0,5, ce qui nous donnerait la moitié de la période sinusoïdale complète. Et tout entre les deux. C'est ce qu'on appelle l'interpolation. Il est important de comprendre que nous ne bougeons pas la vague sinusoïdale, nous la traversons.

Ensuite, nous appliquons self.frequency à wave. Cela met à l'échelle l'onde sinusoïdale de telle sorte que les valeurs plus élevées ont plus de collines et de vallées. Une fréquence est 1 ne ferait rien et nous suivrons l'onde sinusoïdale naturelle. Mais c'est ennuyeux, donc la fréquence est augmentée un peu (1.5) afin de donner un meilleur aspect visuel. Comme le sel, ajustez au goût. Ici, il est à 3 fois la fréquence:

enter image description here

Jusqu'à présent, nous avons défini comment notre onde sinusoïdale regardera par rapport à l'idée que nous nous inspirons à. Notre prochaine tâche est de lui donner du mouvement. À cette fin, nous ajouterons self.phase à wave. Ceci est appelé «phase» car une phase est une période distincte dans la forme d'onde. En changeant continuellement self.phase pour chaque image de l'animation, le dessin commencera à une position différente dans la forme d'onde, ce qui lui donnera l'impression de dépasser l'écran. Enfin, nous utilisons wavePosition pour calculer la valeur réelle de l'onde sinusoïdale (let waveUnitValue = sin(wavePosition)). J'ai appelé cela waveUnitValue car le résultat de sin() est une valeur comprise entre -1 et +1.Si l'on a dessiné comme-est, notre vague serait assez ennuyeux, ressemblant à une ligne presque plat:

enter image description here

"J'ai besoin ... besoin d'amplitude"

- Personne

Notre amplitude commence par l'application d'un maxAmplitude-waveUnitValue, étirer verticalement. Pourquoi commencer avec le maximum? Si l'on revient à ce calcul de la variable scaling, on se rappellera que c'est une valeur unitaire - une valeur qui va de 0 à 1 - ce qui signifie qu'elle ne peut que réduire l'amplitude (ou la laisser inchangée) mais pas augmente-le.

Et c'est exactement ce que nous ferons ensuite, appliquez notre valeur scaling. Cela fait que notre forme d'onde a une amplitude de 0 aux extrémités, augmentant graduellement jusqu'à l'amplitude complète au centre. Sans cela, nous aurions quelque chose qui ressemble à ceci:

enter image description here

Enfin, nous avons normedAmplitude. Si vous suivez le code, vous verrez que la fonction drawWave est appelée dans une boucle afin de dessiner plusieurs vagues dans la vue (c'est ici que ces formes d'onde secondaires ou «d'ombre» entrent en jeu.) Le normedAmplitude est utilisé pour sélectionner amplitude différente pour chacune des formes d'onde dessinées dans le cadre de l'effet global.

Il est intéressant de noter que normedAmplitude peut devenir négatif, ce qui permet de renverser les formes d'onde verticalement, en remplissant les espaces vides de la forme d'onde. Essayez de changer l'utilisation de normedAmplitude dans le code original abs(normedAmplitude) et vous verrez quelque chose comme ça (combiné avec l'exemple de fréquence 3x pour mettre en évidence la différence):

enter image description here

La dernière étape consiste à centrer la forme d'onde dans la vue (amplitude + self.bounds.height/2.0), qui devient la valeur finale y que nous utiliserons pour dessiner la forme d'onde.

Donc, euh. C'est tout.

+0

Merci pour la réponse incroyable. –