Tuto : Mask et Clipping d'une image via le layer

Philippe49Philippe49 Membre
juin 2010 modifié dans Actualités #1
Jeudi 13 Août 2009, SDK iPhone 3.0, XCode 3.1
Voici un petit tuto pour masquer ou clipper les images. J'espère qu'il vous sera utile.

L'objet est de réaliser l'image ci-dessous :

• Une première solution est de réaliser le clipping dans la méthode pour dessiner un CALayer. Une première solution bis est de réaliser la même chose dans une UIView. On reportera dans drawRect: ce qui ici est fait dans drawInContext:
• Une seconde solution est d'utiliser la property mask d'un CALayer.
• Enfin, on ajoute un masque à  une UIImageView.

N'est pas abordé dans cet article la solution qui consiste à  reconstruire une image, ni celle qui consiste à  sous-classer UIView. Merci d'avance des retours ...

Réponses

  • Philippe49Philippe49 Membre
    août 2009 modifié #2
    Le projet :

    1) Créer un projet de type Window-Based Application, appelée ClippedImage.
    2) Ouvrir IB, et ne rien faire d'autre que mettre le fond de la window en noir.
    3) Importer le framework QuartzCore
    4) Revenir à  XCode, et créer une classe ClippedLayer dont l'interface ClippedLayer.h est
    <br />#import &lt;Foundation/Foundation.h&gt;<br />#import &lt;QuartzCore/QuartzCore.h&gt;<br />@interface ClippedLayer : CALayer {<br />}<br />@end
    


    A ce stade, on peut compiler correctement ... pour voir le magnifique écran noir.
  • Philippe49Philippe49 Membre
    août 2009 modifié #3
    La création du ClippedLayer qui va contenir l'image
    rq: Pour ne pas alourdir le tuto, je le fais dans applicationDidFinishLaunching:, ce qui dans une situation d'utilisation ne sera peut-être pas le meilleur endroit

    Importer l'interface de ClippedLayer.h. Cela importe en même temps <QuartzCore/QuartzCore.h> qui se trouve en #import de ClippedLayer.h.

    #import &quot;ClippedImageAppDelegate.h&quot;<br />#import &quot;ClippedLayer.h&quot;<br />@implementation ClippedImageAppDelegate<br />@synthesize window;<br />- (void)dealloc {<br />	[window release];<br />	[super dealloc];<br />}<br />
    


    On traduit volontiers layer par calque au sens du dessinateur. Ainsi, comme une vue, un CALayer possède une frame. Le pendant de la property center d'une view est la property position d'un layer. Néammoins, si vous en avez besoin, la signification de la property position peut être changée grâce à  la property anchorPoint.

    Importer l'image qui servira de contenu au layer

    - (void)applicationDidFinishLaunching:(UIApplication *)application { <br />	UIImage * image=[UIImage imageNamed:@&quot;de-stael-nicolas-bateaux.jpg&quot;];&nbsp;  <br />	// Création et géométrie du layer<br />	ClippedLayer * layer=[[ClippedLayer alloc] init];<br />	CGRect frame;<br />	frame.size=image.size;<br />	layer.frame=frame;<br />	layer.position=[window layer].position;<br />...<br />}<br />
    
  • Philippe49Philippe49 Membre
    août 2009 modifié #4
    Matériel pour le dessin du layer

    1) Certaine options sont disponibles dans la doc de CALayer. On utilise par exemple la bordure rouge. Comme tout cela c'est du CoreGraphics, la couleur est une CGColor. Un moyen simple est d'utiliser les méthodes de UIColor,  mais CGColorRef fournit également des possibilités exploitables.

    <br />	// On définit la bordure rouge (voir la doc de CALayer pour d&#39;autres options)<br />	layer.borderWidth=3.;<br />	UIColor * borderColor=[UIColor redColor];	<br />	layer.borderColor=borderColor.CGColor;<br />
    


    2) On définit le contenu du layer comme étant l'image. La property contents est de type CGImeRef, type CoreGraphics qui est utilisé par QuartzCore. On passe par UIImage pour simplifier, et on en prend la CGImageRef associée par la property CGImage.

    // et on définit le contenu du layer<br />	layer.contents=(id) image.CGImage;
    


  • Philippe49Philippe49 Membre
    18:48 modifié #5
    Ajouter le layer sur l'écran

    Enfin on ajoute le layer dans l'arborescence des layers, en utilisant window.layer qui est racine de cette arborescence.

    // Installation<br />	[window.layer&nbsp; addSublayer:layer];<br />	[layer display];<br /><br />&nbsp; // Override point for customization after application launch<br />	[window makeKeyAndVisible];<br />}<br />
    




    A ce stade, cela ne peut marcher puisque le code de ClippedLayer n'est pas encore installé. Par contre, on peut obtenir le résultat ci-dessous, en faisant ces petites transformations :

    /*ClippedLayer*/ CALayer * layer=[[ClippedLayer alloc] init];<br />	...<br />//	[layer display];<br />
    
  • Philippe49Philippe49 Membre
    août 2009 modifié #6
    Code de clipping du layer

    Le message [layer display] a pour effet d'appeler la méthode - (void)drawInContext:(CGContextRef)ctx sur le ClippedLayer. C'est là  que l'on fait le clipping.

    En macro, on met des valeurs numériques.
    <br />#import &quot;ClippedLayer.h&quot;<br /><br />#define DELTA 20.<br />#define WIDTH (CGRectGetWidth(self.frame))<br />#define HEIGHT (CGRectGetHeight(self.frame))<br /><br />@implementation ClippedLayer<br />
    


    Je définis ensuite la matrice de transformation qui retourne l'image. Cela est imposé par le système de coordonnées de l'écran de l'iPhone, qui est inversé par rapport au regard de l'utilisateur. CGContextConcatCTM() combine la Current Transform Matrix actuelle (ici l'identité) avec celle passée en argument.
    <br />- (void)drawInContext:(CGContextRef)ctx {<br />	// Symétrie axiale par rapport à  l&#39;horizontale médiane du layer<br />	CGAffineTransform transform=CGAffineTransformMake(1.,0.,0.,-1.,0.,HEIGHT);<br />	CGContextConcatCTM(ctx, transform);<br />
    



    On trace ensuite le polygone dans le contexte graphique
    // Le polygone<br />	CGContextBeginPath(ctx);<br />	CGContextMoveToPoint(ctx,DELTA,DELTA);<br />	CGContextAddLineToPoint(ctx,0.,HEIGHT);<br />	CGContextAddLineToPoint(ctx,WIDTH,HEIGHT/2.);<br />	CGContextAddLineToPoint(ctx,WIDTH*0.4,HEIGHT*0.3);	<br />	CGContextClosePath(ctx);<br />
    


    On applique le clipping
    // clipper<br />	CGContextClip(ctx);	<br />
    


    On dessine l'image
    // dessiner l&#39;image<br />	CGContextDrawImage(ctx, self.bounds, (CGImageRef) self.contents);<br />}
    



    Build , Run & Enjoy
  • Philippe49Philippe49 Membre
    août 2009 modifié #7


    Seconde solution : utiliser la property mask

    mask

    An optional layer whose alpha channel is used as a mask to select between the layer's background and the result of compositing the layer's contents with its filtered background.

    @property(retain) CALayer *mask
    Discussion
    Defaults to nil.

    Special Considerations
    When setting the mask to a new layer, the new layer's superlayer must first be set to nil, otherwise the behavior is undefined.

    Availability
    Available in iPhone OS 3.0b and later.
  • Philippe49Philippe49 Membre
    18:48 modifié #8
    Le projet :

    1) Créer un projet de type Window-Based Application, appelée ClippedImage.
    2) Ouvrir IB, et ne rien faire d'autre que mettre le fond de la window en noir.
    3) Importer le framework QuartzCore
    4) Importer dans les ressources les images ci-dessous


    <br />#import &lt;Foundation/Foundation.h&gt;<br />#import &lt;QuartzCore/QuartzCore.h&gt;<br />@interface ClippedLayer : CALayer {<br />}<br />@end
    

  • Philippe49Philippe49 Membre
    septembre 2009 modifié #9
    Dans le code du delegate de l'application, on commence par prendre des références sur l'image représentée, et sur le mask à  appliquer

    #import &quot;MaskedImageAppDelegate.h&quot;<br />#import &lt;QuartzCore/QuartzCore.h&gt;<br /><br />@implementation MaskedImageAppDelegate<br />@synthesize window;<br />- (void)applicationDidFinishLaunching:(UIApplication *)application {&nbsp; &nbsp; <br />	UIImage * image=[UIImage imageNamed:@&quot;de-stael-nicolas-bateaux.jpg&quot;];<br />	UIImage * maskImage=[UIImage imageNamed:@&quot;mask.png&quot;];<br />
    


    Puis on crée le layer principal, sa géométrie, son contenu. (Les commentaires sur ce code sont faits plus haut)
    // Création et géométrie du layer<br />	CALayer * layer=[CALayer layer];&nbsp; &nbsp; // retained with addSublayer<br />	CGRect frame;<br />	frame.size=image.size;<br />	layer.frame=frame;<br />	layer.position=[window layer].position;<br /><br />	// graphisme associé<br />	layer.contents=(id) image.CGImage;<br />	layer.borderWidth=3.;<br />	UIColor * borderColor=[UIColor redColor];	<br />	layer.borderColor=borderColor.CGColor;<br />
    


  • Philippe49Philippe49 Membre
    septembre 2009 modifié #10
    Puis on crée le masque, et on l'installe. Il n'est pas nécessaire de préciser la position qui doit être mise à  jour automatiquement. 

    // mask layer<br />	CALayer * maskLayer=[CALayer layer];<br />	maskLayer.frame=CGRectMake(0.,0.,CGRectGetWidth(frame),CGRectGetHeight(frame));<br />	maskLayer.contents=(id)maskImage.CGImage;<br />	layer.mask=maskLayer;<br />
    


    On ajoute le layer à  la subview, et le tour est joué
    // Installation<br />	[window.layer&nbsp; addSublayer:layer];<br />	<br />	<br />&nbsp; // Override point for customization after application launch<br />	[window makeKeyAndVisible];<br />}<br />
    
  • Philippe49Philippe49 Membre
    août 2009 modifié #11



    Ajouter un masque à  une UIImageView
  • Philippe49Philippe49 Membre
    septembre 2009 modifié #12
    Quand à  ajouter un masque à  une image view existante, cela peut se faire par l'intermédiaire de son layer

    - (void)applicationDidFinishLaunching:(UIApplication *)application {&nbsp; &nbsp; <br />	CALayer * maskLayer=[[CALayer alloc] init];<br />	maskLayer.frame=CGRectMake(0.,0.,CGRectGetWidth(imageView.frame),CGRectGetHeight(imageView.frame));	<br />	UIImage * mask=[UIImage imageNamed:@&quot;mask2.png&quot;];<br />	maskLayer.contents=(id)mask.CGImage;<br />	imageView.layer.mask=maskLayer;<br /><br />&nbsp; // Override point for customization after application launch<br />	[window makeKeyAndVisible];<br />}<br />
    


    [EDIT] Une nuance : la doc sur la property mask d'un layer indique :
    Special Considerations
    When setting the mask to a new layer, the new layer's superlayer must first be set to nil, otherwise the behavior is undefined.




    Avec un masque dont alpha varie en gradient :

  • MalaMala Membre, Modérateur
    18:48 modifié #13
    D'expérience, je déconseillerais l'usage de la propriété mask. Elle est pratique mais beaucoup plus gourmande en ressources.  :brule:
  • Philippe49Philippe49 Membre
    août 2009 modifié #14
    Merci pour ce retour d'expérience, on serait tenté de sauter sur la facilité d'utilisation de cette property mask .
  • PommeATotoPommeAToto Membre
    18:48 modifié #15
    Bonjour,

    J'ai également vu que c'était assez gourmand en ressources, notamment pour animer ce masque.
    Que me conseilleriez-vous comme pistes ?

    Merci par avance
  • Philippe49Philippe49 Membre
    18:48 modifié #16
    Quel est le type d'animation que tu veux réaliser : déplacement du masque, distorsion, rotation, .... ?
  • PommeATotoPommeAToto Membre
    18:48 modifié #17
    Bonjour,

    Je souhaite réaliser un scale du masque.

    Pour l'instant, j'ai quelque chose qui marche pas trop mal, en utilisant un layer et en lui mettant un masque, puis en créant une CAAnimation sur ce masque avec la clé "transform.scale".
    Je tourne à  14-20 fps en faisant ce masque sur une image de la taille de l'écran.

    Le seul gros souci c'est que je souhaite appliquer ensuite encore cette anim sur d'autres layers que je superpose, mais là  ça lag pas mal.

    Merci

    +
  • Philippe49Philippe49 Membre
    18:48 modifié #18
    Tu as essayé une CAAnimationGroup ?
  • PommeATotoPommeAToto Membre
    18:48 modifié #19
    En fait, à  chaque fois que l'on tape l'écran, j'ajoute un élément et j'aimerais qu'il apparaisse avec ce zoom sur le scale.
    Je me sers d'un group pour une autre animation, et elle ne pose aucun problème. J'ai juste un gros temps de calcul ^^
  • Philippe49Philippe49 Membre
    18:48 modifié #20
    J'essaierais à  l'étape N+1 de cumuler les masques des étapes 1->N en un seul masque, ou un seul clipping en CGPath. Cela aurait l'avantage de ne toujours traiter que 2 layers ou clipping simultanément. A tester ... 

    J'ai testé le cumul des CGPath sur une UIView, et ça lag à  partir d'une cinquantaine sur le device. Je n'ai pas essayé le cumul dans une image pour en faire un mask, où on prend du temps pour faire l'image, mais où le display devrait être assez court.
  • PommeATotoPommeAToto Membre
    18:48 modifié #21
    En fait, je peux enlever le mask, je ne sais pas pourquoi je n'y ai pas pensé...
    Je vais regarder de ce côté là .
    Après, je tenterai le cumul des masques, ça sonne bien ^^
    Merci pour m'avoir aiguillé !
Connectez-vous ou Inscrivez-vous pour répondre.