web-dev-qa-db-fra.com

Dessinez des segments à partir d'un cercle ou d'un beignet

J'ai essayé de trouver un moyen de dessiner des segments comme illustré dans l'image suivante:

enter image description here

J'aimerais:

  1. dessiner le segment
  2. inclure des dégradés
  3. inclure des ombres
  4. animer le dessin d'un angle de 0 à n

J'ai essayé de le faire avec CGContextAddArc et des appels similaires, mais sans aller très loin.

Quelqu'un peut-il aider?

71
Chris Baxter

Votre question comporte de nombreuses parties.

Obtenir le chemin

La création du chemin pour un tel segment ne devrait pas être trop difficile. Il y a deux arcs et deux lignes droites. J'ai expliqué précédemment comment vous pouvez briser un chemin comme ça donc je ne le ferai pas ici. Au lieu de cela, je vais être fantaisiste et créer le chemin en caressant un autre chemin. Vous pouvez bien sûr lire le détail et construire le chemin vous-même. L'arc dont je parle caressant est l'arc orange à l'intérieur du résultat final en pointillés gris.

Path to be stroked

Pour caresser le chemin, nous en avons d'abord besoin. c'est aussi simple que se déplacer vers le point de départ et dessiner un arc autour du centre de l'angle actuel à l'angle que vous voulez que le segment couvre.

CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
                  startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
             centerPoint.x, centerPoint.y,
             radius,
             startAngle,
             endAngle,
             YES);

Ensuite, lorsque vous avez ce chemin (l'arc unique), vous pouvez créer le nouveau segment en le caressant avec une certaine largeur. Le chemin résultant va avoir les deux lignes droites et les deux arcs. Le coup se produit à partir du centre sur une distance égale vers l'intérieur et vers l'extérieur.

CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
    CGPathCreateCopyByStrokingPath(arc, NULL,
                                   lineWidth,
                                   kCGLineCapButt,
                                   kCGLineJoinMiter, // the default
                                   10); // 10 is default miter limit

Dessin

La prochaine étape est le dessin et il y a généralement deux choix principaux: Core Graphics dans drawRect: ou formez des calques avec Core Animation. Core Graphics va vous donner le dessin le plus puissant mais Core Animation va vous donner les meilleures performances d'animation. Puisque les chemins sont impliqués, Cora Animation pure ne fonctionnera pas. Vous vous retrouverez avec des objets étranges. Nous pouvons cependant utiliser une combinaison de calques et de Core Graphics en dessinant le contexte graphique du calque.

Remplir et caresser le segment

Nous avons déjà la forme de base, mais avant d'y ajouter des dégradés et des ombres, je vais faire un remplissage et un trait de base (vous avez un trait noir dans votre image).

CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke);

Cela mettra quelque chose comme ça à l'écran

Filled and stroked shape

Ajout d'ombres

Je vais changer l'ordre et faire l'ombre avant le dégradé. Pour dessiner l'ombre, nous devons configurer une ombre pour le contexte et dessiner remplir la forme pour la dessiner avec l'ombre. Ensuite, nous devons restaurer le contexte (avant l'ombre) et recommencer la forme.

CGColorRef shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor;
CGContextSaveGState(c);
CGContextSetShadowWithColor(c,
                            CGSizeMake(0, 2), // Offset
                            3.0,              // Radius
                            shadowColor);
CGContextFillPath(c);
CGContextRestoreGState(c);

// Note that filling the path "consumes it" so we add it again
CGContextAddPath(c, strokedArc);
CGContextStrokePath(c);

À ce stade, le résultat est quelque chose comme ça

enter image description here

Dessiner le dégradé

Pour le dégradé, nous avons besoin d'une couche de dégradé. Je fais ici un dégradé de deux couleurs très simple, mais vous pouvez le personnaliser à votre guise. Pour créer le dégradé, nous devons obtenir les couleurs et l'espace colorimétrique approprié. Ensuite, nous pouvons dessiner le dégradé au-dessus du remplissage (mais avant le trait). Nous devons également masquer le gradient sur le même chemin qu'auparavant. Pour ce faire, nous coupons le chemin.

CGFloat colors [] = {
    0.75, 1.0, // light gray   (fully opaque)
    0.90, 1.0  // lighter gray (fully opaque)
};

CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceGray(); // gray colors want gray color space
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;

CGContextSaveGState(c);
CGContextAddPath(c, strokedArc);
CGContextClip(c);

CGRect boundingBox = CGPathGetBoundingBox(strokedArc);
CGPoint gradientStart = CGPointMake(0, CGRectGetMinY(boundingBox));
CGPoint gradientEnd   = CGPointMake(0, CGRectGetMaxY(boundingBox));

CGContextDrawLinearGradient(c, gradient, gradientStart, gradientEnd, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(c);

Ceci termine le dessin car nous avons actuellement ce résultat

Masked gradient

Animation

En ce qui concerne l'animation de la forme, elle a tout a été écrit avant: Animation de tranches de tarte à l'aide d'un CALayer personnalisé . Si vous essayez de faire le dessin en animant simplement la propriété du chemin, vous verrez une déformation vraiment géniale du chemin pendant l'animation. L'ombre et le dégradé ont été laissés intacts à des fins d'illustration dans l'image ci-dessous.

Funky warping of path

Je vous suggère de prendre le code de dessin que j'ai publié dans cette réponse et de l'adopter dans le code d'animation de cet article. Ensuite, vous devriez vous retrouver avec ce que vous demandez.


Pour référence: le même dessin utilisant Core Animation

Forme unie

CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = [UIColor lightGrayColor].CGColor;
segment.strokeColor = [UIColor blackColor].CGColor;
segment.lineWidth = 1.0;
segment.path = strokedArc;

[self.view.layer addSublayer:segment];

Ajout d'ombres

Le calque possède des propriétés liées aux ombres que vous pouvez personnaliser. Cependant vous devez définir la propriété shadowPath pour de meilleures performances.

segment.shadowColor = [UIColor blackColor].CGColor;
segment.shadowOffset = CGSizeMake(0, 2);
segment.shadowOpacity = 0.75;
segment.shadowRadius = 3.0;
segment.shadowPath = segment.path; // Important for performance

Dessiner le dégradé

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.colors = @[(id)[UIColor colorWithWhite:0.75 alpha:1.0].CGColor,  // light gray
                    (id)[UIColor colorWithWhite:0.90 alpha:1.0].CGColor]; // lighter gray
gradient.frame = CGPathGetBoundingBox(segment.path);

Si nous dessinions le dégradé maintenant, ce serait au-dessus de la forme et non à l'intérieur. Non, nous ne pouvons pas avoir un remplissage dégradé de la forme (je sais que vous y pensiez). Nous devons masquer le dégradé pour qu'il sorte du segment. Pour ce faire, nous créons un autre calque pour être le masque de ce segment. Il doit être une autre couche, la documentation est claire que le comportement est "indéfini" si le masque fait partie de la hiérarchie des couches. Étant donné que le système de coordonnées du masque va être le même que celui des sous-couches au dégradé, nous devrons traduire la forme du segment avant de le définir.

CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
                                                                 -CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
                                               &translation);
gradient.mask = mask;
180
David Rönnqvist

Tout ce dont vous avez besoin est couvert dans le Guide de programmation Quartz 2D . Je vous suggère de le parcourir.

Cependant, il peut être difficile de tout rassembler, alors je vais vous guider. Nous allons écrire une fonction qui prend une taille et renvoie une image qui ressemble à peu près à l'un de vos segments:

arc with outline, gradient, and shadow

Nous commençons la définition de la fonction comme ceci:

static UIImage *imageWithSize(CGSize size) {

Nous aurons besoin d'une constante pour l'épaisseur du segment:

    static CGFloat const kThickness = 20;

et une constante pour la largeur de la ligne décrivant le segment:

    static CGFloat const kLineWidth = 1;

et une constante pour la taille de l'ombre:

    static CGFloat const kShadowWidth = 8;

Ensuite, nous devons créer un contexte d'image dans lequel dessiner:

    UIGraphicsBeginImageContextWithOptions(size, NO, 0); {

J'ai mis une accolade gauche à la fin de cette ligne parce que j'aime un niveau d'indentation supplémentaire pour me rappeler d'appeler UIGraphicsEndImageContext plus tard.

Étant donné que de nombreuses fonctions que nous devons appeler sont des fonctions Core Graphics (aka Quartz 2D), et non des fonctions UIKit, nous devons obtenir le CGContext:

        CGContextRef gc = UIGraphicsGetCurrentContext();

Nous sommes maintenant prêts à vraiment commencer. D'abord, nous ajoutons un arc au chemin. L'arc longe le centre du segment que nous voulons dessiner:

        CGContextAddArc(gc, size.width / 2, size.height / 2,
            (size.width - kThickness - kLineWidth) / 2,
            -M_PI / 4, -3 * M_PI / 4, YES);

Nous allons maintenant demander à Core Graphics de remplacer le chemin d'accès par une version "barrée" qui décrit le chemin d'accès. Nous définissons d'abord l'épaisseur du trait sur l'épaisseur que nous voulons que le segment ait:

        CGContextSetLineWidth(gc, kThickness);

et nous définissons le style de la ligne sur "bout", nous aurons donc des extrémités carrées :

        CGContextSetLineCap(gc, kCGLineCapButt);

Ensuite, nous pouvons demander à Core Graphics de remplacer le chemin par une version caressée:

        CGContextReplacePathWithStrokedPath(gc);

Pour remplir ce chemin avec un dégradé linéaire, nous devons dire à Core Graphics de couper toutes les opérations à l'intérieur du chemin. Cela entraînera la réinitialisation du chemin par Core Graphics, mais nous aurons besoin du chemin plus tard pour tracer la ligne noire autour de l'Edge. Nous allons donc copier le chemin ici:

        CGPathRef path = CGContextCopyPath(gc);

Puisque nous voulons que le segment projette une ombre, nous allons définir les paramètres d'ombre avant de faire un dessin:

        CGContextSetShadowWithColor(gc,
            CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2,
            [UIColor colorWithWhite:0 alpha:0.3].CGColor);

Nous allons à la fois remplir le segment (avec un dégradé) et le caresser (pour dessiner le contour noir). Nous voulons une seule ombre pour les deux opérations. Nous disons à Core Graphics qu'en commençant une couche de transparence:

        CGContextBeginTransparencyLayer(gc, 0); {

J'ai mis une accolade gauche à la fin de cette ligne parce que j'aime avoir un niveau d'indentation supplémentaire pour me rappeler d'appeler CGContextEndTransparencyLayer plus tard.

Étant donné que nous allons modifier la région de découpage du contexte pour le remplissage, mais nous ne voulons pas découper lorsque nous tracerons le contour plus tard, nous devons enregistrer l'état graphique:

            CGContextSaveGState(gc); {

J'ai mis une accolade gauche à la fin de cette ligne parce que j'aime avoir un niveau d'indentation supplémentaire pour me rappeler d'appeler CGContextRestoreGState plus tard.

Pour remplir le chemin avec un dégradé, nous devons créer un objet dégradé:

                CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
                CGGradientRef gradient = CGGradientCreateWithColors(rgb, (__bridge CFArrayRef)@[
                    (__bridge id)[UIColor grayColor].CGColor,
                    (__bridge id)[UIColor whiteColor].CGColor
                ], (CGFloat[]){ 0.0f, 1.0f });
                CGColorSpaceRelease(rgb);

Nous devons également déterminer un point de départ et un point d'arrivée pour le gradient. Nous utiliserons le cadre de délimitation du chemin:

                CGRect bbox = CGContextGetPathBoundingBox(gc);
                CGPoint start = bbox.Origin;
                CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox));

et nous forcerons le dégradé à être dessiné horizontalement ou verticalement, selon la plus longue des deux:

                if (bbox.size.width > bbox.size.height) {
                    end.y = start.y;
                } else {
                    end.x = start.x;
                }

Maintenant, nous avons enfin tout ce dont nous avons besoin pour dessiner le dégradé. Commençons par couper le chemin:

                CGContextClip(gc);

Ensuite, nous dessinons le gradient:

                CGContextDrawLinearGradient(gc, gradient, start, end, 0);

Ensuite, nous pouvons libérer le dégradé et restaurer l'état graphique enregistré:

                CGGradientRelease(gradient);
            } CGContextRestoreGState(gc);

Lorsque nous avons appelé CGContextClip, Core Graphics réinitialisait le chemin du contexte. Le chemin ne fait pas partie de l'état graphique enregistré; c'est pourquoi nous en avons fait une copie plus tôt. Il est maintenant temps d'utiliser cette copie pour définir à nouveau le chemin dans le contexte:

            CGContextAddPath(gc, path);
            CGPathRelease(path);

Maintenant, nous pouvons tracer le chemin, pour dessiner le contour noir du segment:

            CGContextSetLineWidth(gc, kLineWidth);
            CGContextSetLineJoin(gc, kCGLineJoinMiter);
            [[UIColor blackColor] setStroke];
            CGContextStrokePath(gc);

Ensuite, nous demandons à Core Graphics de terminer la couche de transparence. Cela lui fera regarder ce que nous avons dessiné et ajouter l'ombre en dessous:

        } CGContextEndTransparencyLayer(gc);

Maintenant, nous avons tous fini de dessiner. Nous demandons à UIKit de créer un UIImage à partir du contexte de l'image, puis de détruire le contexte et de renvoyer l'image:

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

Vous pouvez trouver le code tous ensemble dans ce Gist .

39
rob mayoff

Ceci est une version Swift de la réponse de Rob Mayoff. Voyez à quel point cette langue est plus efficace! Cela pourrait être le contenu d'un fichier MView.Swift:

import UIKit

class MView: UIView {

    var size = CGSize.zero

    override init(frame: CGRect) {
    super.init(frame: frame)
    size = frame.size
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var niceImage: UIImage {

        let kThickness = CGFloat(20)
        let kLineWidth = CGFloat(1)
        let kShadowWidth = CGFloat(8)

        UIGraphicsBeginImageContextWithOptions(size, false, 0)

            let gc = UIGraphicsGetCurrentContext()!
            gc.addArc(center: CGPoint(x: size.width/2, y: size.height/2),
                   radius: (size.width - kThickness - kLineWidth)/2,
                   startAngle: -45°,
                   endAngle: -135°,
                   clockwise: true)

            gc.setLineWidth(kThickness)
            gc.setLineCap(.butt)
            gc.replacePathWithStrokedPath()

            let path = gc.path!

            gc.setShadow(
                offset: CGSize(width: 0, height: kShadowWidth/2),
                blur: kShadowWidth/2,
                color: UIColor.gray.cgColor
            )

            gc.beginTransparencyLayer(auxiliaryInfo: nil)

                gc.saveGState()

                    let rgb = CGColorSpaceCreateDeviceRGB()

                    let gradient = CGGradient(
                        colorsSpace: rgb,
                        colors: [UIColor.gray.cgColor, UIColor.white.cgColor] as CFArray,
                        locations: [CGFloat(0), CGFloat(1)])!

                    let bbox = path.boundingBox
                    let startP = bbox.Origin
                    var endP = CGPoint(x: bbox.maxX, y: bbox.maxY);
                    if (bbox.size.width > bbox.size.height) {
                        endP.y = startP.y
                    } else {
                        endP.x = startP.x
                    }

                    gc.clip()

                    gc.drawLinearGradient(gradient, start: startP, end: endP,
                                          options: CGGradientDrawingOptions(rawValue: 0))

                gc.restoreGState()

                gc.addPath(path)

                gc.setLineWidth(kLineWidth)
                gc.setLineJoin(.miter)
                UIColor.black.setStroke()
                gc.strokePath()

            gc.endTransparencyLayer()


        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    }

    override func draw(_ rect: CGRect) {
        niceImage.draw(at:.zero)
    }
}

Appelez-le à partir d'un viewController comme celui-ci:

let vi = MView(frame: self.view.bounds)
self.view.addSubview(vi)

Pour faire les conversions de degrés en radians, j'ai créé l'opérateur de suffixe °. Vous pouvez donc maintenant utiliser par exemple 45 ° et cela fait la conversion de 45 degrés en radians. Cet exemple concerne les Ints, étendez-les également pour les types Float si vous en avez besoin:

postfix operator °

protocol IntegerInitializable: ExpressibleByIntegerLiteral {
  init (_: Int)
}

extension Int: IntegerInitializable {
  postfix public static func °(lhs: Int) -> CGFloat {
    return CGFloat(lhs) * .pi / 180
  }
}

Mettez ce code dans un utilitaire Swift.

4
t1ser