Comment modifier l'origine d'une NSAffineTransform?
Bonjour,
Je souhaite me faire un logiciel d'animation en démultipliant avec des transformations affines des NSImage (s) : des itérations, des fractales, etc.
Aucune des trois solutions proposées par les frameworks ne me convient vraiment :
- les NSAffineTransform ont pour origine celle de la NSView. J'aimerais pouvoir choisir plutôt le centre du rectangle de l'image comme origine de la transformation. (tout en gardant le rectangle de la NSView inchangé pour que le dessin s'y affiche, d'où l'impossibilité de modifier l'origine de la NSView). Le problème se pose à l'identique avec Quartz.
J'ai du mal à comprendre les méthodes de "Converting Coordinate Values" en particulier.
- sinon il y a les filtres CIIImage, et le filtre "Perspective transforme". Je ne trouve pas la doc des paramètres à intégrer à mon code. J'ai déjà utilisé ces filtres pourtant... Par ailleurs, lorsque je la teste dans la démo "CoreImageFunHouse", La transformation a l'air de déborder du rectangle d'origine de l'image. J'espère que dans la NSView globale, l'affichage sera correct. le savez-vous?
Dernier point, la structure de la matrice d'une NSAffineTransform est privée : pas moyen de calculer les valeurs et de réécrire la structure apparemment (le compilateur veut pas!). Y a t-il une astuce pour contourner le problème? (pareil avec Quartz apparemment)
Merci beaucoup si vous avez des idées. Je m'en vais essayer les filtres en farfouillant d'abord dans la doc. (mais où donc l'ont-ils rangé?)
Réponses
- Une matrice de transformation de type "translation" pour déplacer le centre de ton dessin pour que ce soit lui le nouveau (0,0)
- Une matrice de rotation autour de ce (0,0)
- Une matrice de translation inverse de la première pour re-déplacer le point (0,0) au centre
Attention petit rappel mathématique la composition de matrices (ou de fonctions) se fait dans un ordre qui peut paraà®tre contraire à l'ordre naturel suggéré par l'écriture, ainsi tout comme en maths f(g(x)) consiste à d'abord appliquer la fonction g() à x avant d'appliquer f() au résultat (et pas d'abord f puis g), il faut de même faire attention à composer les NSAffineTransform dans le bon sens (il y a déjà des questions sur le sujet sur le forum tu devrais trouver plus de détails en faisant une recherche ici)
Quant à ta dernière question, tu peux tout à fait modifier les composantes de la matrice de ta NSAffineTransform : voir ici dans la doc.
Ah oui, très bien l'idée d'utiliser deux translations. J'avais essayé en modifiant l'origine du rectangle de la photo, mais sans succès... J'm'en vais tester cela.
Par contre, lorsque j'ai fait :
le compilateur n'est pas content du tout : il barre de rouge et me dit que l'accès à la structure est privé! D'où ma question...
Merci AliGator en tous les cas, j'essaie cela.
(effectivement, l'ordre des transformations sera aussi une bonne prise de tête. L'idée d'écrire soit-même la matrice ferait gagner du temps).
Essaye ceci à la place, ça devrait mieux marcher :
Ou encore en utilisant directement la syntaxe d'initialisation des structs via les noms des champs de la struct (syntaxe C pour créer des structs C) :
Je ne suis pas trop d'accord. Car en utilisant la transformStruct, encore faut-il savoir "lire dans la matrice" (c'est le cas de le dire pour le coup ) et comprendre de suite ce que tu cherches à faire en voyant les différents calculs que tu fais à la main.
De plus, je ne suis même pas sûr que les valeurs que tu as mises dans le code de ton précédent post sont bonnes (le calcul des bonnes valeurs pour tX et tY n'est pas si simple vu qu'après rotation en (0,0) le point qui était avant au centre de l'image n'est plus au centre, donc normalement tX et tY aussi dépendend du sinus et cosinus de l'angle de rotation... Bref, ça peut vite être prise de tête et calculs de maths à faire à la main qui non seulement peuvent être source d'erreur mais en plus vont être difficiles à relire.
Alors que NSAffineTransform a une API ObjC, propre et explicite (encore + que son équivalent CoreGraphics qu'est CGAffineTransform), où écrire ce genre de chose est bien plus lisible que d'écrire directement des résultats calculés dans la matrice je trouve :
Dans cet exemple, l'utilisation de "translateXBy:YBy:" et "rotateByDegrees:" me semble beaucoup plus clair, où le seul truc qui peut être choquant c'est qu'il faut penser à lire les opérations de bas en haut pour avoir l'ordre d'application, mais à part ça c'est quand même bien plus lisible que des cos et sin et maths partout !
Merci une nouvelle fois AliGator
Je préfère travailler directement sur la matrice parce qu'en plus, on peut déformer le rectangle, le "tordre". J'utilisais cela dans des générateurs d'IFS. J'appelle cette torsion "cisaillement", je ne connais pas le terme mathématique.
La matrice fonctionne très bien. Cela donne :
Merci pour le coup de main. En plus la photo se "tord" d'elle même , pas besoin de CIImage. Tant mieux, je préfère travailler avec Cocoa.
Je n'ai pas encore réussi à modifier l'origine du plan. En bas à gauche, ce n'est vraiment pas joli. Je suis dessus en ce moment.
Et sinon t'es pas obligé de récupérer la NSAffineTransformStruct de ta nouvelle NSAffineTransform alors que tu la modifie entièrement finalement (redéfinis la valeur de tous les champs) donc tu t'en fiches de la valeur initiale. Tu peux directement écrire "NSAffineTransformStruct matrix;" tout court pour déclarer la valable avant de changer les valeurs de ses champs m11,m12,m21,m22,tX et tY. Ou encore utiliser la syntaxe que j'ai démontrée dans mon post précédent.
Pour l'origine, ce serait par là :
[self translateOriginToPoint:unPoint];
6.2831852 = 2*PI.
En ce moement, j'en suis aux essais, aux tests préliminaires, pour voir ce qui marche.
On fera plus propre lorsqu'on entrera "dans le dur" (la version complète)
Merci encore AliGator.
Tu appliques cette transformation à ton contexte de dessin, puis tu fais touted les opérations de dessin de ton image comme tu les ferais dans ce nouveau repère, puis tu appliques la transformation inverse a la fin pour revenir dans le repère OS X avec l'origine en bas et Y vers le haut.
En termes mathématiques, cela revient à appliquer la matrice R = T*M*T' où T est la transformation qui inverse le repère, M est la transformation qui va déformer ton image, exprimée dans ce nouveau repère dans lequel tu es plus a l'aise, et T' la transformation inverse de T qui restaure le repère d'origine.
Et le résultat R est du coup au final la transformation M mais exprimée dans le repère OSX.
C'est la formule classique de changement de repère qui est d'ailleurs la même que ce que j'ai utilisé dans les exemples précédents de rotation autour d'un point arbitraire, ou T changeait le repère pour mettre le centre de rotation a l'origine du nouveau repère, M faisait la rotation, et T' restaurait le repère d'origine.
Bah là c'est pareil, exactement le même principe.
Et tu es tout ce qu'il faut dans l'API pour concatener des NSAffineTransform !
Et sinon j'avais bien deviné que ce magic number était la valeur de 2 PI ça va je la connais quand même 😄 mais ma remarque était plutôt pour te rappeler que les Magic Numbers c'est le mal et à éviter et qu'en plus il existe déjà une constante M_PI qui existe en standard donc autant l'utiliser.
double angleEnRadians = angleEnDegres * M_PI/180.0;
"double angleEnRadians = angleEnDegres * M_PI/180.0;"
Ah oui, on peut simplifier le calcul!
Alors, je souhaite faire des NSImage qui s'affichent dans des NSRect qui, eux-mêmes sont transformés par des NSAffineTransform (tournés, tordus, etc.) La fonction de transformation d'origine marche bien : je définis n'importe quel point sur le plan comme centre de la rotation par exemple.
La dernière chose qui me manque serait de "réinitialiser" le plan de façon à pouvoir afficher une autre image et ses transformations (autres bien évidemment) ensuite.
On peut faire :
[self translateOriginToPoint:self.bounds.origin];
mais cela semble être décevant : les affichages suivants se révèlent être aléatoires. parfois cela s'affiche, parfois non... De même la fonction "insert" de NSAffineTransform est insatisfaisante, sans doute à cause des itérations de la transformation.
Je ne trouve pas, ni dans les fonctions de NSView, ni dans celle de NSAffineTransform une fonction qui "reset" le plan affine, et remet l'origine à 0 et les transformations à aucune. Je vais essayer un "concat" avec une matrice qui ne modifie rien pour voir, et je vous tiens au courant.
Le plus propre est comme je l'ai expliqué plus haut de toujours appliquer la transformation inverse (NSAffineTransform a certainement une méthode qui retourne pour toi la transform inverse d'ailleurs, non ?) pour revenir à l'état initial.
Mais sinon les techniques les plus courantes pour revenir à l'état d'avant c'est directement de sauver l'état du contexte et le restaurer ensuite.
Je ne sais pas sur quoi tu dessines (NSBitmapImageRep ? NSImage avec lockFocus appelé dessus ? CGContextRef ? Autre ?) mais les contextes graphiques ont en général une méthode push et pop ou save et restore ou ce genre de nom pour faire ça.
En fait, il faut effectivement réécrire la matrice de la transformation pour que les dessins suivants ne subissent pas la dernière transfo en cours (matrice (1,0,0 // 0,1,0). En ce qui concerne l'origine, changer le point d'origine du plan produit l'effet escompté.
Par contre, il m'arrive quelque chose d'étonnant : si je ne change pas le point d'origine pour la première transformation, ou si il est égal à (0,0), les dessins dessinés après
continuent de subir la transformation du plan, alors que si les valeurs du point sont différentes de (0,0), les dessins après sont "normaux". Du coup, je bricole avec un "leurre", en mettant un changement d'origine inutile "pour le cas où" le point d'origine serait le point nul.
Voici le code complet de mon test ; il fonctionne, et produit l'effet souhaité. Avec des sliders, je modifie les valeurs de rotation, translation, homothétie et cisaillement de la première transfo. Celle-ci est réitérée pour des effets de répétition. Puis je dessine un carré test avec quelques rotations : ils doivent toujours être dessinés au même endroit quelle que soit la transformation précédente. Ce code marche, mais il y a cette bizarrerie!
Qui dit mieux?
Merci mille fois AliGator en tous les cas pour tes idées et tes posts.
Par ce que là , c'est comme si après avoir fait un schéma sur un papier, tu gommais tout ce que tu avais écrit sur le papier, pour pouvoir repartir d'une feuille vierge,... au lieu de tout simplement prendre une nouvelle feuille vierge directement !
Car là ce que tu en fais de ta matrix et de ta xform après c'est juste faire autr chose de complètement indépendant (c'est pas appliquer la transformation inverse ou changer la matrice de transformation du contexte graphique courant ou quoi, c'est juste changer la valeur d'une variable.
C'est comme si tu avais une variable x, tu faisais des calculs dedans donc elle se terminait a une valeur de 42, et ensuite si tu voulais faire un autre calcul (genre 3*5) tu faisais "x=0" (remise à zéro de ta matrice pour la remettre à la matrice identité) avant de faire "x += 3*5" (concaténation de la matrice précédente avec la nouvelle matrice) au lieu de faire directement x=3*5 ou d'utiliser une autre variable pour faire y+=3*5
Et sinon, je t'ai déjà donné d'autres astuces et pistes dans les messages plus haut que tu n'as pas intégré à ton code, comme utiliser [ul=https://developer.apple.com/library/prerelease/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSGraphicsContext_Class/index.html#//apple_ref/occ/instm/NSGraphicsContext/saveGraphicsState]saveGraphicsState[/url] au début de ton code, faire toutes les transformations que tu veux, et utiliser la méthode "restoreGraphicsState" plus tard pour restaurer l'état (y compris le repère et la matrice de transformation active) à ce qui avait été sauvegardé.
Merci AliGator une nouvelle fois. Tu écris :
Le problème est que justement, je ne veux pas de page blanche, mais continuer à dessiner sur la précédente. Mon bout de code fonctionne de ce point de vue.
En ce qui concerne la NSGraphicContext, il m'a semblé que cette classe traite des polices de caractères, des couleurs, des types de tracés, mais pas à proprement parlé de transformation du plan. J'ai lu ceci :
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaDrawingGuide/GraphicsContexts/GraphicsContexts.html#//apple_ref/doc/uid/TP40003290-CH203-BCIJFBJJ
Je reviendrai sur ce sujet plus tard sans doute. Là , je commence à travailler sur mon algo d'IFS : je souhaite démultiplier une image dans une fractale!
A suivre...
Ce que je voulais dire c'est que dans ton code, tu réutilises la variable "matrix" mais fait des pieds et des mains pour changer ses valeurs m11, m12, ... tout ça pour pouvoir la réutiliser en repartant d'une matrice identité, au lieu de directement en utiliser une autre. C'est comme si tu avais une variable "x" que tu avais utilisée (dans ton cas matrix), et que tu avais une méthode qui faisait "x += 30" (les méthodes "translateXBy:YBy:" & co que tu veux utiliser à la fin) et du coup tu voulais absolument utiliser x et était obligé de faire un "x=0" avant de pouvoir utiliser "x += 30" pour qu'il valle 30 à la fin... au lieu d'utiliser une autre variable "y" qui est déjà initialisée à 0. Sauf que dans cet exemple x et y c'est que des entiers donc ça va c'est pas méchant de devoir faire un "x=0" plutôt que d'utiliser une nouvelle variable "y", mais dans le cas de ta "matrix", ça t'oblige à remettre tous les 6 champs de la structure aux bonnes valeurs pour retomber sur la matrice identité... aucun intérêt.
Donc non je persiste, ça serait bien plus simple d'utiliser une nouvelle variable NSAffineTransformStruct pou la fin de ton code que t'embêter à modifier l'ancienne pour la remettre à l'identité. (Ca ne changera pas que tu continuera à dessiner sur l'image précédente, ça c'est autre chose, là je te parle des variables que tu manipules pour créer tes matrices que tu vas appliquer à ton contexte de dessin)
NSGraphicContext manipule également la matrice de transformation courante. C'est d'ailleurs le contexte graphique qui est modifié quand tu appelles "[monAffineTransform concat]" : cela doit bien concaténer la matrice "monAffineTransform" à quelque chose, et ce quelque chose, c'est la matrice de transformation courante du NSGraphicContext justement, qui garde à tout moment en mémoire quel est la matrice/transformation actuellement appliquée pour l'appliquer sur les futures opérations de dessin faites sur ce contexte.
J'ai enfin compris ce que tu voulais dire. Des fois, cela prend du temps à monter au cerveau!
Effectivement, il est plus simple de crée une nouvelle NSAffineTransform que de réécrire l'ancienne...
Par contre, la double définition du point d'origine demeure nécessaire.
Je vais encore essayer de comprendre la classe NSGraphicContext. Logiquement en effet, elle devrait répondre à mes besoins. Je cherche encore.
Merci une nouvelle fois AliGator en tous les cas