CoreAnimation et CGPath : faire une "pause"
Bonjour,
J'ai une subview (de type UIImageView) que je souhaite animer pour la déplacer en suivant un CGPath donné (courbe de bézier), tout en faisant en sorte que son orientation suive le path.
Jusque là , pas de problème, les CAKeyframeAnimations sont là pour ça, en définissant la propriété "path" de cette animation avec mon CGPath et mettant le rotationMode à kCAAnimationRotateAuto.
Mais là où ça se corse, c'est que j'aimerais faire une pause dans mon animation. En effet j'ai plusieurs vues à animer en même temps, dont les positions et orientations de chacune sont définies par un tableau de "CarPosInfo" (une classe perso qui définit entre autres la position et l'orientation de ma vue à un temps T donné, à une keyframe quoi).
Ainsi, je calcule mon CGPath de trajectoire pour chacune de mes vues en fonction du tableau de "CarPosInfo" qui lui est associé, ajustant mes controlPoints de mon CGPath de sorte que la tangente à ma courbe de bézier corresponde à l'orientation souhaitée de ma vue à ce point.
Ca marche bien, j'arrive à faire animer ma vue de sorte qu'elle suive la trajectoire avec la bonne orientation à l'endroit de mes points clés. Mais le souci vient des cas où j'ai, pour une de mes vues, un point clé identique entre 2 keyframes. Autrement dit, pendant que les autre vues continuent à s'animer, je veux qu'une de mes vues "fasse une pause" et s'arrête à l'endroit où elle est (pendant que les autres, elles, avancent d'une frame N à la frame N+1).
Si je garde mon principe actuel, j'ajoute une courbe (AddCurveToPoint) du point P... au même point P. Oui mais voilà , du coup lors de l'animation, le rotationMode kCAAnimationRotateAuto du coup n'aime pas, car il ne sait pas voir la tangente locale, forcément... (donc en résultat, ma vue est horizontale, même si localement elle devrait avoir une autre orientation).
Donc du coup je vois pas comment faire cette "pause".... Des idées ?
(Bon je sais pas si mon énoncé a été très bien exprimé, si vous avez besoin que j'éclaircisse...)
J'ai une subview (de type UIImageView) que je souhaite animer pour la déplacer en suivant un CGPath donné (courbe de bézier), tout en faisant en sorte que son orientation suive le path.
Jusque là , pas de problème, les CAKeyframeAnimations sont là pour ça, en définissant la propriété "path" de cette animation avec mon CGPath et mettant le rotationMode à kCAAnimationRotateAuto.
Mais là où ça se corse, c'est que j'aimerais faire une pause dans mon animation. En effet j'ai plusieurs vues à animer en même temps, dont les positions et orientations de chacune sont définies par un tableau de "CarPosInfo" (une classe perso qui définit entre autres la position et l'orientation de ma vue à un temps T donné, à une keyframe quoi).
Ainsi, je calcule mon CGPath de trajectoire pour chacune de mes vues en fonction du tableau de "CarPosInfo" qui lui est associé, ajustant mes controlPoints de mon CGPath de sorte que la tangente à ma courbe de bézier corresponde à l'orientation souhaitée de ma vue à ce point.
Ca marche bien, j'arrive à faire animer ma vue de sorte qu'elle suive la trajectoire avec la bonne orientation à l'endroit de mes points clés. Mais le souci vient des cas où j'ai, pour une de mes vues, un point clé identique entre 2 keyframes. Autrement dit, pendant que les autre vues continuent à s'animer, je veux qu'une de mes vues "fasse une pause" et s'arrête à l'endroit où elle est (pendant que les autres, elles, avancent d'une frame N à la frame N+1).
Si je garde mon principe actuel, j'ajoute une courbe (AddCurveToPoint) du point P... au même point P. Oui mais voilà , du coup lors de l'animation, le rotationMode kCAAnimationRotateAuto du coup n'aime pas, car il ne sait pas voir la tangente locale, forcément... (donc en résultat, ma vue est horizontale, même si localement elle devrait avoir une autre orientation).
Donc du coup je vois pas comment faire cette "pause".... Des idées ?
(Bon je sais pas si mon énoncé a été très bien exprimé, si vous avez besoin que j'éclaircisse...)
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
T=(P'-P)/||P'-P|| où P' est le point suivant normalement P
P(t) = P0*(1-t)^3+3*P1*t*(1-t)^2+3*P2*t^2*(1-t)+P3*t^3
Avec t entre 0 et 1, la courbe allant de P0 à P3 avec P1 et P2 comme points de contrôle.
Pour avoir la tangente, il suffit de dériver en t si mes cours de prépa ne sont pas trop loin
P'(t) = 3*(P3*t^2+P2*t*(2-3*t)+P1*(t-1)*(3*t-1)-P0*(1-t)^2)
En 0, ça donne 3*(P1-P0)
En 1, ça donne 3*(P3-P2)
En 1/2, ça donne (3/4)*((P3-P0)+(P2-P1))
Ce qui ne me semble pas illogique
Ainsi quand ton CGPath est au point A et que tu demandes CGPathAddCurveToPoint(Ca, Cb, , tu lui fournis le point de contrôle Ca au point A et le point de contrôle Cb au point B... Eh bien la demi-droite [A,Ca) est tangente à ta courbe au point A, et [B,Cb) est tangente à ta courbe au point B. Ce qui m'arrange, puisque c'est cette contrainte de tangence qui m'intéresse, pour imposer l'orientation de ma vue.
Non le souci c'est que je construis mon CGPath par des points clés définis par une position et un angle (point de départ défini par CGPathMoveToPoint, et points suivants définis par CGPathAddCurveToPoint). Je vais ainsi du point A, en partant d'une orientation de rA radians, au point B, avec une orientation de rB radians... puis continue vers le point C, orientation rC radians... quand ensuite je construit ma CAKeyframeAnimation en lui demandant d'utiliser ce path pour animer le mouvement, ça marche très bien.
Oui mais si le point B et le point C sont identiques, là ça déconne, car si ma vue reste bien alors au même endroit, son orientation part en c**ille.
Exemple pour mieux comprendre : Si j'ai ma vue en (0,10), orienté selon un angle a (disons grosso-modo vers la gauche), au moment T, et qu'au moment T+∆T je la veux en (0,20) un peu plus bas, même orientation a : alors la courbe de bézier de ma trajectoire va faire un S, partant vers la gauche, tournant dans le sens inverse des aiguilles d'une montre de 180°, puis dans l'autre sens de 180°, pour se retrouver au point B, encore orienté vers la gauche. Ok ?
Maintenant imaginons qu'on rapproche le point B non plus en (0,20) mais en (0,10+k)... au cas limite où k temps vers 0, on retrouve mon cas des points qui se confondent... Mais le problème c'est que même dans cet espace infinitésimal, ma courbe de bézier a cette forme de S. Certes elle est concentrée en un point unique, mais quand la CAKeyframeAnimation utilise ce path, il n'empêche qu'elle semble tout de même suivre cette forme en S, puisque ma vue même si elle reste sur place va quand même tourner à 180° dans un sens, puis dans l'autre, pour revenir à son orientation originale... dont j'aurais bien voulu qu'elle ne change jamais !
- prevPos est le point de départ,
- nextPos est le point d'arrivée à la "frame" suivante,
Et voilà donc comment je rajoute à mon CGPath la courbe allant de la position prevPos.position, orienté selon l'angle prevPos.angle, à la position nextPos.position, orienté à l'arrivée selon l'angle nextPos.angle :
--
Je pense que la solution la plus proche pour l'instant est celle de Philippe49, à savoir utiliser l'artifice qui consiste, si mes 2 points se confondent, à déplacer mon 2e points d'une valeur FLT_EPSILON avant de continuer mes calculs comme si de rien était.
PS : Si vous avez des conseils pour adapter mes coefficients L et k*L pour ajuster la courbe... Le but est d'avoir une trajectoire entre (position1, angle1) et (position2, angle2) la plus naturelle possible (mes vues représentent des voitures, la trajectoire celle de la voiture donc).
Quand tu fais un Bezierpath A,cp1,cp2,B, la tangente en B relie cp2 à B.
Si tu passes en B sans boucle, tu suis la direction de la tangente en prenant une curve B,B+epsilon*(B-cp2),B+epsilon*(B-cp2),B+2*epsilon*(B-cp2)
ou même faire un simple segment de raccordement. Si epsilon est assez petit, mais pas trop, à mon avis le point ne bougera pas, et la tangente sera la même.
2) Si tu veux raccorder deux keyframes
A ce moment là , il faut raccorder en B les formules par
lastB-lastCp2 positivement colinéaires à newCp2-newB
Oui je pense que c'est ce que je vais finir par faire... d'autant que ça me gênais quand même de déplacer ma vue d'un pixel pour ça... mais un déplacement de ε=FLT_EPSILON ne devrais pas se voir lors du rendu, tout en permettant à ce que l'animation ne fasse pas de boucle non voulue
Heu là j'ai pas tout compris... qu'est ce que tu appelles lastB/lastCp2 et newCp2/newB ?
C'est pas ce que je fais dans mon calcul (cf mon extrait de code post précédent) ?
Pour info voilà mon bout de code (CarPosInfo est une classe perso qui stocke essentiellement la position et l'angle de ma vue) : Le truc c'est que j'utilise donc une valeur [tt]eps[/tt], qui me permet de décaler mon point d'un petit delta (enfin epsilon plutôt ^^) pour éviter la boucle... mais si je met moins que 1.f comme valeur (je pensais en particulier à qqch comme 0.1 voire même FLT_EPSILON pensant qu'il faisait les calculs côté subpixels), bah ça marche pas, il faut un minimum de un pixel... du coup ça reste visible comme décalage, ça m'embête un peu (d'autant que ce n'est que pour l'animation, à la fin je remet la vraie position, donc s'il y a un décalage il est visible).
Donc je pense que pour le moment je vais garder cette solution, mais à terme je vais décomposer moi-même mon animation keyframe par keyframe. Ca m'embête un peu juste pour ce détail en plus ça va être un peu plus ch*ant à gérer mais bon.
D'autant que je vais aussi avoir à gérer le cas où l'on demande de changer de frame avant la fin de l'animation précédente : genre je demande de passer de la frame 1 à la 5, ça commence l'animation... et si je demande ensuite à passer à la frame 3 avant la fin de la première animation... ça doit interrompre la première animation et repartir de l'endroit où c'était rendu pour aller à la frame 3, et là au lieu de ça ça part en vrille, je pense que ça compose les animations... donc bon encore un truc à régler !
Interrompre une animation ? je doute, il faudrait agir dans le thread de cette animation. Enchaà®ner oui.
On peut chercher si on peut définir les ticks avec CAMediaTimingFunction, (comme NSAnimation avec les progressMarks) dans le non-documenté ?
[EDIT] Rien de bien enthousiasmant
Oui, évidemment, on le retrouve d'ailleurs dans mon calcul en t=0 et t=1... Mais c'est fortement imprécis, non ?
Tu n'as la tangente que sur les points donnés, et pas entre... Du coup si tu as des points trop espacés (ou même si tu fais un zoom sur ta courbe), ça va faire n'importe quoi.
Après, si tu n'as que des points très proches, je ne vois pas l'intérêt d'une courbe de Bézier, une approximation par segments est tout à fait correcte... et fonctionne même quand les points sont trop proches.
Donc tu définis ton circuit par des tableaux de positions / orientations pour créer le chemin de Bézier (je suppose un point avant et après chaque tournant...).
Mais en plein milieu d'un tournant, ton orientation doit être faussée non ?
L'utilisateur se positionne sur la "frame" 1, indique par toucher sur l'écran la position de la ou des voiture(s) et leur orientation au temps T=1, puis se positionne sur la frame 2 et fait la même chose... au fur et à mesure, la trajectoire estimée est tracée, et à la fin il peut lancer une animation pour voir la/les voiture(s) bouger de la première à la dernière frame.
Quand j'utilisateur change de frame, si c'est la première fois qu'il ton sur cette frame (création de la frame), je reprend la position et orientation de la frame précédente... mais s'il navigue de frame en frame pour revenir sur ses pas, réavancer, etc... c'est là que je rajoute une animation pour passer d'une frame à l'autre. De même, quand il a fini et qu'il veut animer le tout, je lance l'animation complète qui va de la frame 1 à la dernière frame.
Je tâche de sortir ça de mon projet et de faire un projet à part pour vous le poster, ça sera sans doute plus propre si vous voulez avoir une idée plu claire.
Et si au lieu de faire un "addCurveToPoint", tu fais un "addLineToPoint", ça ne résout pas le problème ?
Mais j'ai fini par régler le problème, je ne fais plus un gros path, mais décompose mon animation frame par frame et ne fait mes paths que de la frame N à la frame N+1.
Du coup si à un moment de mon animation complète, entre la frame N et la frame N+1, ma position ne change pas, bah j'anime simplement pas ma vue entre ces 2 frames, puis je reprend en faisant un CGPath pour la frame N+1 à N+2...
J'ai fait un projet séparé au propre où j'ai tout refactoré, ça commence à prendre forme, je poste ça ensuite pour info.
Description
Quand vous lancez, vous avez deux vues représentant des voitures A et B, et vous pouvez les déplacer par un touch, et changer leur orientation par un multitouch.
Ma classe SketchView représente la vue principale avec le plan, et contient 2 "CarView"s en subviews. CarPosInfo me sert pour stocker une position+orientation d'une voiture à un instant T (une CarView contient un tableau de 5 CarPosInfos car y'a maxi 5 frames)
Utilisation
Dans la toolbar en bas vous pouvez passer ensuite à la frame suivante (si c'est la première fois que vous allez dessus, les positions et orientations des voitures sont recopiées de la frame précédente), et modifier la position et orientation des voitures. La trajectoire commence alors à apparaà®tre, au fur et à mesure que vous rajoutez des frames et déplacez les voitures entre ces frames.
Ensuite si vous passez d'une frame à l'autre c'est animé en suivant la trajectoire, et avec le bouton "Play" vous pouvez animer toutes les frames de 0 à N (animation plus lente que la manuelle)
Conclusion
Donc ça a fini par marcher, il me reste juste un petit truc qui m'embête, c'est que mes CGPath vont de la position N à la position N+1, tant pour tracer la trajectoire que pour l'animation... du coup le temps de lancer l'animation, je dois repositionner temporairement mes vues en 0,0 (transform = Identity), et la restaurer à la fin de l'animation.
D'une part je trouve pas ça super propre, et de plus parfois on voit (oh c'est juste un flash très court mais quand même) la voiture en (0,0) juste avant qu'elle soit restaurée à la position de fin...
Je vois pas trop comment zapper ce problème, si vous avez une idée...
Bon tu peux lire le code, il est pas si méchant, mais bon, la flemme de le porter sur mac ^^
Bon dans mon programme c'était un peu plus complexe car d'une part je veux construire mon CGPath pour une animation CoreAnimation ensuite, et surtout je veux prendre en compte l'orientation de mes vues représentant mes voitures. Alors que pour par exemple un arbre genre B-Tree, les controlpoints sont tout trouvés, il suffit qu'ils soivent à la verticale des points à relier puisque ton path est principalement vertical dans son orientation.
D'autant que moi j'ai pas fini avec celui là
Bah oui, j'ai toujours un problème, c'est que (j'ai essayé sur un vrai iPhone et non plus dans le simulateur, et là c'est flagrant) à la fin de mon animation, je vois ma vue en (0,0) " pendant juste un cycle de runloop/drawing " avant qu'elle ait le temps de se remettre à la position finale. C'est bref, mais on voit quand même que pendant un cycle de rendu la vue est en (0,0), là où je l'avais mise au début de mon animation " pour que sa propriété "transform" ne gène pas l'animation si elle n'est pas l'identité (si je le fais pas j'ai des animations bizarres) " du coup à la fin de l'animation ça "saute" en (0,0) avant de re-sauter en (xFinal, yFinal)
Je me demande si mes calculs ne pourraient pas servir pour le coup
On y est presque, à un temps "t" (entre 0 et 1), on a le point et le vecteur tangente (vitesse) ; de là à trouver le contrôle point pour une sous-partie du path, il n'y a probablement pas loin.
Peut être -V/3... (enfin assez empirique comme supputation :P, mais en tout cas, c'est -V/qqch, qui peut être constant ou variable... mais le problème, c'est que le premier contrôle point subit peut-être une homothétie aussi...)