web-dev-qa-db-ja.com

CAGradientLayer対角グラデーション

enter image description here

次のCAGradientLayerを使用します。

let layer = CAGradientLayer()
layer.colors = [
    UIColor.redColor().CGColor,
    UIColor.greenColor().CGColor,
    UIColor.blueColor().CGColor
]
layer.startPoint = CGPointMake(0, 1)
layer.endPoint = CGPointMake(1, 0)
layer.locations = [0.0, 0.6, 1.0]

しかし、レイヤーのboundsプロパティを設定すると、四角形のグラデーションが拡大されます。 Sketch 3アプリの画像のような結果が必要です(上記を参照)。

どうすればこれを達成できますか?

22
Evgeny

Update:CAGradientLayerの代わりにcontext.drawLinearGradient()を次のような 方法で使用します 。スケッチ/フォトショップと一致するグラデーションを描画します。

CAGradientLayerを絶対に使用する必要がある場合は、次の計算を使用する必要があります...


理解するのに少し時間がかかりましたが、注意深く観察した結果、CagradientLayerでのグラデーションのAppleの実装はかなり奇妙であることがわかりました。

  1. まず、ビューを正方形に変換します。
  2. 次に、開始/終了ポイントを使用してグラデーションを適用します。
  3. 真ん中の勾配は、この解像度では実際に90度の角度を形成します。
  4. 最後に、ビューを元のサイズに縮小します。

これは、中央のグラデーションが新しいサイズで90度の角度を形成しなくなることを意味します。これは、他のほぼすべてのペイントアプリケーション(スケッチ、Photoshopなど)の動作と矛盾します。

スケッチで機能するように始点/終点を実装する場合は、始点/終点を変換して、Appleがビューを圧縮するという事実を考慮に入れる必要があります。


実行する手順(図)

enter image description hereenter image description hereenter image description here

コード

_import UIKit

/// Last updated 4/3/17.
/// See https://stackoverflow.com/a/43176174 for more information.
public enum LinearGradientFixer {
  public static func fixPoints(start: CGPoint, end: CGPoint, bounds: CGSize) -> (CGPoint, CGPoint) {
    // Naming convention:
    // - a: point a
    // - ab: line segment from a to b
    // - abLine: line that passes through a and b
    // - lineAB: line that passes through A and B
    // - lineSegmentAB: line segment that passes from A to B

    if start.x == end.x || start.y == end.y {
      // Apple's implementation of horizontal and vertical gradients works just fine
      return (start, end)
    }

    // 1. Convert to absolute coordinates
    let startEnd = LineSegment(start, end)
    let ab = startEnd.multiplied(multipliers: (x: bounds.width, y: bounds.height))
    let a = ab.p1
    let b = ab.p2

    // 2. Calculate perpendicular bisector
    let cd = ab.perpendicularBisector

    // 3. Scale to square coordinates
    let multipliers = calculateMultipliers(bounds: bounds)
    let lineSegmentCD = cd.multiplied(multipliers: multipliers)

    // 4. Create scaled perpendicular bisector
    let lineSegmentEF = lineSegmentCD.perpendicularBisector

    // 5. Unscale back to rectangle
    let ef = lineSegmentEF.divided(divisors: multipliers)

    // 6. Extend line
    let efLine = ef.line

    // 7. Extend two lines from a and b parallel to cd
    let aParallelLine = Line(m: cd.slope, p: a)
    let bParallelLine = Line(m: cd.slope, p: b)

    // 8. Find the intersection of these lines
    let g = efLine.intersection(with: aParallelLine)
    let h = efLine.intersection(with: bParallelLine)

    if let g = g, let h = h {
      // 9. Convert to relative coordinates
      let gh = LineSegment(g, h)
      let result = gh.divided(divisors: (x: bounds.width, y: bounds.height))
      return (result.p1, result.p2)
    }
    return (start, end)
  }

  private static func unitTest() {
    let w = 320.0
    let h = 60.0
    let bounds = CGSize(width: w, height: h)
    let a = CGPoint(x: 138.5, y: 11.5)
    let b = CGPoint(x: 151.5, y: 53.5)
    let ab = LineSegment(a, b)
    let startEnd = ab.divided(divisors: (x: bounds.width, y: bounds.height))
    let start = startEnd.p1
    let end = startEnd.p2

    let points = fixPoints(start: start, end: end, bounds: bounds)

    let pointsSegment = LineSegment(points.0, points.1)
    let result = pointsSegment.multiplied(multipliers: (x: bounds.width, y: bounds.height))

    print(result.p1) // expected: (90.6119039567129, 26.3225059181603)
    print(result.p2) // expected: (199.388096043287, 38.6774940818397)
  }
}

private func calculateMultipliers(bounds: CGSize) -> (x: CGFloat, y: CGFloat) {
  if bounds.height <= bounds.width {
    return (x: 1, y: bounds.width/bounds.height)
  } else {
    return (x: bounds.height/bounds.width, y: 1)
  }
}

private struct LineSegment {
  let p1: CGPoint
  let p2: CGPoint

  init(_ p1: CGPoint, _ p2: CGPoint) {
    self.p1 = p1
    self.p2 = p2
  }

  init(p1: CGPoint, m: CGFloat, distance: CGFloat) {
    self.p1 = p1

    let line = Line(m: m, p: p1)
    let measuringPoint = line.point(x: p1.x + 1)
    let measuringDeltaH = LineSegment(p1, measuringPoint).distance

    let deltaX = distance/measuringDeltaH
    self.p2 = line.point(x: p1.x + deltaX)
  }

  var length: CGFloat {
    let dx = p2.x - p1.x
    let dy = p2.y - p1.y
    return sqrt(dx * dx + dy * dy)
  }
  var distance: CGFloat {
    return p1.x <= p2.x ? length : -length
  }
  var midpoint: CGPoint {
    return CGPoint(x: (p1.x + p2.x)/2, y: (p1.y + p2.y)/2)
  }
  var slope: CGFloat {
    return (p2.y-p1.y)/(p2.x-p1.x)
  }
  var perpendicularSlope: CGFloat {
    return -1/slope
  }
  var line: Line {
    return Line(p1, p2)
  }
  var perpendicularBisector: LineSegment {
    let p1 = LineSegment(p1: midpoint, m: perpendicularSlope, distance: -distance/2).p2
    let p2 = LineSegment(p1: midpoint, m: perpendicularSlope, distance: distance/2).p2
    return LineSegment(p1, p2)
  }

  func multiplied(multipliers: (x: CGFloat, y: CGFloat)) -> LineSegment {
    return LineSegment(
      CGPoint(x: p1.x * multipliers.x, y: p1.y * multipliers.y),
      CGPoint(x: p2.x * multipliers.x, y: p2.y * multipliers.y))
  }
  func divided(divisors: (x: CGFloat, y: CGFloat)) -> LineSegment {
    return multiplied(multipliers: (x: 1/divisors.x, y: 1/divisors.y))
  }
}

private struct Line {
  let m: CGFloat
  let b: CGFloat

  /// y = mx+b
  init(m: CGFloat, b: CGFloat) {
    self.m = m
    self.b = b
  }

  /// y-y1 = m(x-x1)
  init(m: CGFloat, p: CGPoint) {
    // y = m(x-x1) + y1
    // y = mx-mx1 + y1
    // y = mx + (y1 - mx1)
    // b = y1 - mx1
    self.m = m
    self.b = p.y - m*p.x
  }

  init(_ p1: CGPoint, _ p2: CGPoint) {
    self.init(m: LineSegment(p1, p2).slope, p: p1)
  }

  func y(x: CGFloat) -> CGFloat {
    return m*x + b
  }

  func point(x: CGFloat) -> CGPoint {
    return CGPoint(x: x, y: y(x: x))
  }

  func intersection(with line: Line) -> CGPoint? {
    // Line 1: y = mx + b
    // Line 2: y = nx + c
    // mx+b = nx+c
    // mx-nx = c-b
    // x(m-n) = c-b
    // x = (c-b)/(m-n)
    let n = line.m
    let c = line.b
    if m-n == 0 {
      // lines are parallel
      return nil
    }
    let x = (c-b)/(m-n)
    return point(x: x)
  }
}
_

長方形のサイズに関係なく機能することの証明

_size=320x60_、_gradient=[red@0,[email protected],blue@1]_、startPoint = (0,1)、およびendPoint = (1,0)のビューでこれを試しました。

スケッチ3:

enter image description here

上記のコードを使用して実際に生成されたiOSスクリーンショット:

enter image description here

緑の線の角度は100%正確に見えることに注意してください。違いは、赤と青のブレンド方法にあります。開始点と終了点を誤って計算しているためか、Appleがグラデーションをブレンドする方法とSketchがグラデーションをブレンドする方法の違いだけなのか、それが原因かどうかはわかりません。

38
Senseful

これがendPointを修正するための計算です

let width = bounds.width
let height = bounds.height
let dx = endPoint.x - startPoint.x
let dy = endPoint.y - startPoint.y
if width == 0 || height == 0 || width == height || dx == 0 || dy == 0 {
  return
}
let ux = dx * width / height
let uy = dy * height / width
let coef = (dx * ux + dy * uy) / (ux * ux + uy * uy)
endPoint = CGPoint(x: startPoint.x + coef * ux, y: startPoint.y + coef * uy)
1
Vadim Yelagin