[RESOLU] UIBezierPath avec ou sans UIGraphicsGetCurrentContext

busterTheobusterTheo Membre
décembre 2017 modifié dans Xcode et Developer Tools #1

Bonjour à  tous,


Je travaille sur la déformation d'un masque (fait à  partir d'un UIBezierPath) sur une image.


Je dois donc faire à  chaque fois que je déplace un point, un self.setNeedsDisplay dans le UIView qui contient le path.


 


Cette mise à  jour du path avant de refaire le masque, est trop longue. Car je compte faire cela avec un pan sur une poignée qui pourrait déplacer le point


 


Peut-être quelqu'un a-t-il une idée...


 


Voici quelques bouts de code :


 


 



import UIKit

class PathDent11: UIView {
var bezierPath = UIBezierPath()

var strokeColor = UIColor(red: 255/255, green: 0/255, blue: 0/255, alpha: 1.0)

var scale = CGFloat(0.4)
var translation = CGSize(width: 15, height: 0)

var xScaleDepart = CGFloat(1.0)
var yScaleDepart = CGFloat(1.0)

var premierRecadrage = true

var p1Transfert = CGPoint(x: 78.56, y: 0.28).deplacePath()
var cp1Transfert = CGPoint(x: 82.88, y: -0.04).deplacePath()
var cp2Transfert = CGPoint(x: 80.72, y: 0.12).deplacePath()

override init(frame: CGRect) {
super.init(frame: frame)

self.backgroundColor = UIColor.clear
}

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

override func draw(_ rect: CGRect) {
if premierRecadrage == true {
let p1 = p1Transfert
let cp1 = cp1Transfert
let cp2 = cp2Transfert

bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 85.04, y: -0.2).deplacePath())

bezierPath.addCurve(to: p1, controlPoint1: cp1, controlPoint2: cp2)

bezierPath.addCurve(to: CGPoint(x: 62, y: 4.36).deplacePath(), controlPoint1: CGPoint(x: 73.03, y: 1.32).deplacePath(), controlPoint2: CGPoint(x: 66.83, y: 2.3).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 25.04, y: 33.64).deplacePath(), controlPoint1: CGPoint(x: 46.28, y: 11.05).deplacePath(), controlPoint2: CGPoint(x: 35.01, y: 21.27).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 16.16, y: 43).deplacePath(), controlPoint1: CGPoint(x: 22.08, y: 36.76).deplacePath(), controlPoint2: CGPoint(x: 19.12, y: 39.88).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 5.84, y: 81.64).deplacePath(), controlPoint1: CGPoint(x: 9.92, y: 53.35).deplacePath(), controlPoint2: CGPoint(x: 8.32, y: 67.56).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 4.64, y: 102.52).deplacePath(), controlPoint1: CGPoint(x: 5.44, y: 88.6).deplacePath(), controlPoint2: CGPoint(x: 5.04, y: 95.56).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 2.96, y: 132.76).deplacePath(), controlPoint1: CGPoint(x: 4.08, y: 112.6).deplacePath(), controlPoint2: CGPoint(x: 3.52, y: 122.68).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 13.52, y: 232.6).deplacePath(), controlPoint1: CGPoint(x: -2.33, y: 165.34).deplacePath(), controlPoint2: CGPoint(x: -1.46, y: 212.37).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 129.2, y: 251.56).deplacePath(), controlPoint1: CGPoint(x: 28.03, y: 252.19).deplacePath(), controlPoint2: CGPoint(x: 93.64, y: 257.37).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 164.48, y: 251.56).deplacePath(), controlPoint1: CGPoint(x: 141.22, y: 249.59).deplacePath(), controlPoint2: CGPoint(x: 153.36, y: 254.49).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 187.04, y: 143.8).deplacePath(), controlPoint1: CGPoint(x: 198.12, y: 242.69).deplacePath(), controlPoint2: CGPoint(x: 181.16, y: 180.68).deplacePath())
bezierPath.addLine(to: CGPoint(x: 187.04, y: 129.4).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 178.64, y: 86.92).deplacePath(), controlPoint1: CGPoint(x: 190.33, y: 107.94).deplacePath(), controlPoint2: CGPoint(x: 182.23, y: 101.28).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 111.92, y: 5.8).deplacePath(), controlPoint1: CGPoint(x: 166.4, y: 37.91).deplacePath(), controlPoint2: CGPoint(x: 160.92, y: 19.1).deplacePath())
bezierPath.addCurve(to: CGPoint(x: 85.04, y: -0.2).deplacePath(), controlPoint1: CGPoint(x: 103.43, y: 3.49).deplacePath(), controlPoint2: CGPoint(x: 95.75, y: -0.19).deplacePath())
bezierPath.close()

xScaleDepart = baseDentWidth / bezierPath.bounds.size.width
yScaleDepart = baseDentHeight / bezierPath.bounds.size.height

affineTransformScale(xScaleDepart, yScale: yScaleDepart)

premierRecadrage = false

UIColor.clear.setFill()
bezierPath.fill()
strokeColor.setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
} else {
print("premierRecadrage = false")
UIColor.clear.setFill()
bezierPath.fill()
strokeColor.setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
}
}
ETC


pathDent.p1Transfert = CGPoint(x: 78.56, y:30.28).deplacePath()
pathDent.cp1Transfert = CGPoint(x: 82.88, y: -0.04).deplacePath()
pathDent.cp2Transfert = CGPoint(x: 80.72, y: 0.12).deplacePath()

pathDent.premierRecadrage = true
pathDent.setNeedsDisplay()

DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
imageViewDent.layer.mask = nil
masqueLayer.path = pathDent.bezierPath.cgPath

masqueLayer.frame = pathDent.bezierPath.bounds

masqueLayer.borderWidth = 1
masqueLayer.borderColor = UIColor(red: 0/255, green: 0/255, blue: 255/255, alpha: 1.0).cgColor

masqueLayer.frame.size.width = pathDent.frame.size.width
masqueLayer.frame.size.height = pathDent.frame.size.height

masqueLayer.frame.origin.x = masqueLayer.frame.origin.x - TaillePoignees
masqueLayer.frame.origin.y = masqueLayer.frame.origin.y - TaillePoignees

imageViewDent.layer.mask = masqueLayer
}

J'ai déjà  travailler sur une autre façon de traiter les paths, avec un contexte, et ça me semblait plus rapide mais plus fastidieux... Qu'est-ce qui est le mieux ?


 


Voici l'autre façon (un bout) :



import UIKit

class Curve: UIView {

override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.yellow
}

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

// Mark: - Les variables

var startCurveFloatX: CGFloat = 0.0
var startCurveFloatY: CGFloat = 0.0

var aCurveP1FloatX: CGFloat = 0.0
var aCurveP1FloatY: CGFloat = 0.0

var aCurveP2FloatX: CGFloat = 0.0
var aCurveP2FloatY: CGFloat = 0.0

var aEndCurveFloatX: CGFloat = 0.0
var aEndCurveFloatY: CGFloat = 0.0

// Mark: - La méthode drawRect

override func draw(_ myRect: CGRect) {

let startCurveX = startCurveFloatX
let startCurveY = startCurveFloatY

let aCurveP1X = aCurveP1FloatX
let aCurveP1Y = aCurveP1FloatY
let aCurveP2X = aCurveP2FloatX
let aCurveP2Y = aCurveP2FloatY

let aEndCurveX = aEndCurveFloatX
let aEndCurveY = aEndCurveFloatY

var Path: UIBezierPath = UIBezierPath(rect:myRect)
let context = UIGraphicsGetCurrentContext()
context?.setLineWidth(2.0)
context?.setStrokeColor(UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor)
context?.move(to: CGPoint(x: startCurveX, y: startCurveY))
context?.addCurve(to: CGPoint(x: aEndCurveX, y: aEndCurveY), control1: CGPoint(x: aCurveP1X, y: aCurveP1Y), control2: CGPoint(x: aCurveP2X, y: aCurveP2Y))
context?.strokePath()
}
}


Puis un pan :



case .changed:
let recognizerX = recognizer.location(in: self.view).x as CGFloat
let recognizerY = recognizer.location(in: self.view).y as CGFloat

pointStart.frame.origin.x = recognizerX
pointStart.frame.origin.y = recognizerY

curve.startCurveFloatX = recognizerX + startCurveX - pointStartX
curve.startCurveFloatY = recognizerY + startCurveY - pointStartY

pointP1.frame.origin.x = recognizerX
pointP1.frame.origin.y = recognizerY + pointP1Y - pointStartY

curve.aCurveP1FloatX = pointP1.frame.origin.x + aCurveP1X - pointP1X
curve.aCurveP1FloatY = pointP1.frame.origin.y + aCurveP1Y - pointP1Y

curve.setNeedsDisplay()

Merci d'avance


 


 


 


 


 



Réponses

  • Désolé,


    je relis ça : oubli.


  • CéroceCéroce Membre, Modérateur
    novembre 2017 modifié #3

    Cette mise à  jour du path avant de refaire le masque, est trop longue. Car je compte faire cela avec un pan sur une poignée qui pourrait déplacer le point


    Ce que tu fais est super lourd sur iOS. Sache que tout le dessin de CoreGraphics est effectué par le CPU, puis doit être transféré dans une bitmap (texture) utilisée par une CALayer pour l'afficher avec le GPU.

    UIView utilise toujours une CALayer qui conserve son contenu sous forme de texture. C'est ce qui permet à  iOS d'effectuer les animations de rotation, déplacement, réduction ou opacité de façon performante.

    La bonne technique est de ne dessiner qu'une fois, puis ensuite travailler avec la CALayer, sans forcer à  rafraà®chir le tracé, si possible. Note tout de même qu'il y a une limite: on travaille alors en bitmap et non en vectoriel, donc ça peut être un peu pixelisé si tu étires le masque.
     
    Jette un oe“il à  CAShapeLayer, ça peut aider pour ce genre de choses.
    Vois aussi si tu ne peux pas assembler ton dessin à  partir de plusieurs éléments.
     

    J'ai déjà  travaillé sur une autre façon de traiter les paths, avec un contexte, et ça me semblait plus rapide mais plus fastidieux... Qu'est-ce qui est le mieux ?


    UIBezierPath permet seulement de stocker les points qui constituent la courbe. Quand tu demandes à  le dessiner, ça dessine dans le CGContext courant; quand tu es dans la méthode drawRect(), tu peux justement obtenir le CGContext courant avec la fonction UIGraphicsGetCurrentContext().

    ça signifie aussi qu'on peut conserver une instance de UIBezierPath et dessiner à  nouveau avec.

    Au final, UIBezierPath s'appuie de toute façon sur CGContext. Travailler directement avec UIBezierPath fait gagner un peu de performances parce qu'on se passe de la couche objet, mais c'est négligeable par rapport à  la lourdeur de l'opération que tu demandes.
  • Ben finalement, je ne trouve pas la réponse.


     


    Je retiens juste du post, c'est que les CALayer c'est mieux que un contexte,


    ​mais moi, je n'utilise pas de layer (sauf pour faire le masque) et je me pose la question : avec contexte ou pas ?


     


    ​J'utilise un layer ensuite seulement pour faire le masque, mais ce principe (sans contexte) est trop lent.


  •  


     


    La bonne technique est de ne dessiner qu'une fois, puis ensuite travailler avec la CALayer, sans forcer à  rafraà®chir le tracé, si possible. Note tout de même qu'il y a une limite: on travaille alors en bitmap et non en vectoriel, donc ça peut être un peu pixelisé si tu étires le masque.

    Ah, merci Céroce, je vais travailler là -dessus, et je reviens sur ce post plus tard pour faire le bilan de ces recherches.


     


    En gros je fais mon path sur un layer, sans contexte, et quand j'applique le masque (avec imageViewDent.layer.mask = masqueLayer), ce sera automatique.., c'est ça le bon plan ?


  • CéroceCéroce Membre, Modérateur
    novembre 2017 modifié #6

    En gros je fais mon path sur un layer, sans contexte

    Il y a toujours un contexte. Dans les méthodes de dessin de UIBezierPath, il est juste implicite, c'est le contexte courant.

    Personnellement, j'irais dessiner dans une CAShapeLayer, qui prend un CGPath, c'est assez direct. Peut-être, en fixant la propriété UIView.layerClass.
    Travailler avec CoreAnimation est toujours complexe, mais tu n'as guère le choix ici.
  • Heu, ok, pas tout compris, mais j'enchaà®ne...


  • busterTheobusterTheo Membre
    novembre 2017 modifié #8

    Dans :



     


     


    En gros je fais mon path sur un layer, sans contexte

    J'entend, je fais ça :



    let context = UIGraphicsGetCurrentContext()
    context?.setLineWidth(2.0)
    context?.setStrokeColor(UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor)
    context?.move(to: CGPoint(x: startCurveX, y: startCurveY))
    context?.addCurve(to: CGPoint(x: aEndCurveX, y: aEndCurveY), control1: CGPoint(x: aCurveP1X, y: aCurveP1Y), control2: CGPoint(x: aCurveP2X, y: aCurveP2Y))
    context?.strokePath()

    ou ça : ?



    bezierPath = UIBezierPath()
    bezierPath.move(to: CGPoint(x: 85.04, y: -0.2).deplacePath())

    bezierPath.addCurve(to: p1, controlPoint1: cp1, controlPoint2: cp2)

    bezierPath.addCurve(to: CGPoint(x: 62, y: 4.36).deplacePath(), controlPoint1: CGPoint(x: 73.03, y: 1.32).deplacePath(), controlPoint2: CGPoint(x: 66.83, y: 2.3).deplacePath())

  • DrakenDraken Membre
    novembre 2017 modifié #9
  • Ok Draken, j'ai regardé (en travers) le lien :


    Apparemment cela parle d'avantage de transformation de path en pixels, et moi, c'est plutôt utiliser un path pour en faire un masque déformable via la modificaion du path avant la re-fabrication du masque.


     


    Ou bien, j'ai mal lu, et donc je m'y remettrais plus sérieusement.


     


    Merci d'avance, et désolé...


  • Tu ne peux pas montrer quelques exemples graphiques plutôt que des lignes de code ?


    A quoi ressemble l'image déformable ? A quoi ressemble les masques de déformation ?

  • DrakenDraken Membre
    novembre 2017 modifié #12

     


    Bonjour à  tous,


    Je travaille sur la déformation d'un masque (fait à  partir d'un UIBezierPath) sur une image.


    Je dois donc faire à  chaque fois que je déplace un point, un self.setNeedsDisplay dans le UIView qui contient le path.


     


    Cette mise à  jour du path avant de refaire le masque, est trop longue. Car je compte faire cela avec un pan sur une poignée qui pourrait déplacer le point


     


    Peut-être quelqu'un a-t-il une idée...



     


    a


    Dessiner le masque en temps réel n'est peut-être pas la solution. Tu devrais essayer d'afficher juste son CONTOUR, avec des lignes pointillés. Et recréer le masque à  la fin de l'événement pan, quand le doigt quitte l'écran.


     


    C'est ce qu'on faisait à  l'aube de la préhistoire quand les cartes graphiques n'étaient pas assez performantes pour déplacer les fenêtres en temps réel.

  • busterTheobusterTheo Membre
    novembre 2017 modifié #13

    Ah ça, c'est une p.. de bonne idée  :D


     


     



     


     


    Dessiner le masque en temps réel n'est peut-être pas la solution. Tu devrais essayer d'afficher juste son CONTOUR, avec des lignes pointillés. Et recréer le masque à  la fin de l'événement pan, quand le doigt quitte l'écran.

    Mais c'est re-dessiner le path qui prend du temps, pas le masque.

  • DrakenDraken Membre
    novembre 2017 modifié #14


     


    Mais c'est re-dessiner le path qui prend du temps, pas le masque.




     


    Dessiner juste le contour du path doit être beaucoup plus rapide qu'un remplissage de tous les pixels. A essayer ..


     


    Une autre idée à  tester : relier les couples de point du masque par des paths individuels.


     


    Exemple : un masque défini par 4 points : A, B, C, D


     


    4 Paths :


    A-B


    B-C


    C-D


    D-A


     


    Si tu agis sur la position du point B, il faut redessiner les paths des lignes A-B et B-C. C'est plus économique en temps de calcul (et en nombre de pixels) que tout dessiner.

  • Ah ouais Draken, super idée, encore une. Merci...


  • Hey, purée, désolé mais j'ai rien compris au truc...


  • Il y a quelques temps j'ai écrit un rapport de stage qui montrait comment j'avais fait pour accélérer drastiquement l'affichage de contrôles customs sur iOS. Y est abordé notamment comment utiliser les layers de manière relativement avancée et la gestion des masques. 


    C'était assez efficient, j'étais passé de ~60-70% d'utilisation CPU lors de l'utilisation du contrôle à  ~15-20% sur un iPad mini 1.


     


    Par contre je préviens: c'est du texte écrit au kilomètre avec une partie non relue (par erreur) et je n'ai pas eu le temps ni l'envie de le faire par après.


     


    Le code est en Swift 3 ou 2.2 au pire, je ne sais plus.


     


  • Je ne connais pas grand chose à  Swift !


    Quelle est donc cette opération deplacePath dans les lignes :



    CGPoint(x: 62, y: 4.36).deplacePath()

    Merci de me tuyauter car je ne trouve pas ça dans la doc.




  •  


    Par contre je préviens: c'est du texte écrit au kilomètre avec une partie non relue (par erreur) et je n'ai pas eu le temps ni l'envie de le faire par après.


     




    C'est parce que tu manques de maturité, pierre ..

  • Merci pour le rapport, ouais, y'a d'la lecture... cool


     


    Pour :



    CGPoint(x: 62, y: 4.36).deplacePath()

    c'est une méthode que j'ai créé (extension CGPoint) pour déplacer le path dans sa view.


    Mais je l'ai supprimé, j'utilise plutôt ça :



    func affineTransformTranslation(_ translationX: CGFloat, y: CGFloat) {
    bezierPath.apply(CGAffineTransform(translationX: translationX, y: y))
    let newPath = bezierPath
    bezierPath = newPath
    self.setNeedsDisplay()
    }

    Et l'appel avec ça :



    let TranslatePathX = 2 + (cadre.frame.size.width - self.frame.size.width)/2
    let TranslatePathY = 2 + (cadre.frame.size.height - self.frame.size.height)/2
    affineTransformTranslation(TranslatePathX, y: TranslatePathY)

    Voilà , si ça peut t'aider...


  • CéroceCéroce Membre, Modérateur

    C'est toujours aussi inefficace. À quoi bon déplacer le BézierPath, et donc redessiner, alors qu'on peut déplacer la CALayer (ou la UIView) qui contient ce path ?


  • Ah super, Céroce, t'as bien raison, je n'y avais carrément pas pensé.


     


    Et t'aurais pas une idée du même type pour éviter de scaler le path, mais plutôt le layer ?




  •  


    Et t'aurais pas une idée du même type pour éviter de scaler le path, mais plutôt le layer ?




     


    Le layer doit avoir une propriété .transform permettant de réaliser des transformations affine, comme le scale.

  • DrakenDraken Membre
    novembre 2017 modifié #25

    La technique est expliqué dans le lien sur les Layers que je t'ai donné plus haut. 


     


    Mise en oeuvre :



    import UIKit

    class ViewController: UIViewController {

    override func viewDidLoad() {
    super.viewDidLoad()

    // Création du Layer
    let vuePourLayer = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
    self.view.addSubview(vuePourLayer)
    vuePourLayer.center = self.view.center
    let layer = vuePourLayer.layer
    layer.backgroundColor = UIColor.blue.cgColor

    // Scale Layer
    let scaleX:CGFloat = 1.0
    let scaleY:CGFloat = 0.7
    let scaleZ:CGFloat = 1.0
    layer.transform = CATransform3DMakeScale(scaleX, scaleY, scaleZ)

    }
    }


  • CéroceCéroce Membre, Modérateur
    novembre 2017 modifié #26


    Le layer doit avoir une propriété .transform permettant de réaliser des transformations affines, comme le scale.




    Tu peux aussi utiliser la méthode CALayer.setAffineTransform(), qui est plus facile à  utiliser.


     


    L'inconvénient est que l'image va être étirée. C'est ce que j'expliquais en disant que la layer est adossée à  une texture. En fait, la texture est une image bitmap, et elle va seulement être agrandie en utilisant une interpolation bi-cubique, ce qui est très facile pour le GPU.


     


    Dans l'absolu, ce serait donc mieux de redessiner, mais ça peut être lourd. Si c'est seulement une dent, ça ne devrait pas poser de problèmes de performances.


    Je pense que tu as compris que l'idée est de dessiner plein de petits éléments plutôt qu'un gros bloc, et profiter du "cache" graphique offert par les CALayers.


     


    UIView utilise toujours une CALayer, et utiliser UIView suffit dans bien des cas. Il ne faut plonger dans Core Animation que quand on ne peut pas faire autrement, parce que c'est une API délicate à  maà®triser.


  • Bonjour, désolé pour l'absence, j'ai justement regardé tout ça.


    J'ai fait plein d'essais, et je reste finalement sur le bezierPath, sans déplacer le layer, car c'est trop compliqué à  gérer ensuite, car j'ai plusieurs vues à  ajuster ensemble (frame, bounds, etc). Bref...


    Par contre, ça m'a permis d'approfondir les layer, c'est top.


     


    Je propose de mettre résolu. Encore merci pour votre aide.


     


    En ce qui me concerne, je n'ai pas encore tout compris, et ce qui me trouble, c'est que l'on retrouve les même méthodes (avec des différences notoires) sur les paths, les views, les layer, etc. Et certaines fonctionnent dans un de ces contextes (sans jeu de mot) et d'autres, non.


     


    ça viendra avec le temps.


     


    En attendant je vais poster un sacré casse-tête.


    Voici le lien..


  • CéroceCéroce Membre, Modérateur


    En ce qui me concerne, je n'ai pas encore tout compris, et ce qui me trouble, c'est que l'on retrouve les même méthodes (avec des différences notoires) sur les paths, les views, les layer, etc. Et certaines fonctionnent dans un de ces contextes (sans jeu de mot) et d'autres, non.




     


    C'est l'essence de l'informatique: tant qu'on n'a pas une puissance infinie à  disposition, on est obligé à  des compromis.


     


    Core Graphics dessine grâce au CPU, ce qui permet une grande variété de tracés, aussi bien à  l'écran qu'à  l'impression.


    L'inconvénient est que c'est lent, parce que ça n'exploite pas le GPU. 


     


    Core Animation utilise le GPU, mais est aussi limitée dans ce qu'elle sait dessiner. Notamment, elle reste très axée sur le graphisme bitmap, parce que c'est ainsi que le GPU travaille.


     


    UIView simplifie l'utilisation de Core Animation, en couvrant 90% des besoins, mais parfois ça ne suffit pas.


     


    Si tu veux t'amuser, tu peux aussi programmer directement avec OpenGL ES ou Metal!

  • DrakenDraken Membre
    décembre 2017 modifié #30


     


    Si tu veux t'amuser, tu peux aussi programmer directement avec OpenGL ES ou Metal!




     


    Sans aller à  une telle extrémité, on peut imaginer d'insérer une SKView (une vue SpriteKit) sur l'écran, et dessiner la forme avec un SKShapeNode (un sprite 2D dont les contours peuvent être définis avec un path).


Connectez-vous ou Inscrivez-vous pour répondre.