diff --git a/Sources/OpenSwiftUICore/Graphic/Color/ConstantColor.swift b/Sources/OpenSwiftUICore/Graphic/Color/ConstantColor.swift index ae50d3541..a2311844f 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/ConstantColor.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/ConstantColor.swift @@ -162,34 +162,43 @@ extension Color.Resolved: CustomStringConvertible { extension Color.Resolved : Animatable { package static var legacyInterpolation: Bool = !isLinkedOnOrAfter(.v6) - public var animatableData: AnimatablePair>> { + public var animatableData: AnimatablePair>> { get { + let values: (Float, Float, Float, Float) if Self.legacyInterpolation { - // ResolvedGradient.Color.Space.convertIn(self) - _openSwiftUIUnimplementedFailure() + values = (linearRed, linearGreen, linearBlue, opacity) } else { - return AnimatablePair( - linearRed.scaled(by: .unitScale), + let color = ResolvedGradient.ColorSpace.perceptual.convertIn(self) + values = (color.r, color.g, color.b, color.a) + } + return AnimatablePair( + values.0.scaled(by: .unitScale), + AnimatablePair( + values.1.scaled(by: .unitScale), AnimatablePair( - linearGreen.scaled(by: .unitScale), - AnimatablePair( - linearBlue.scaled(by: .unitScale), - opacity.scaled(by: .unitScale) - ) + values.2.scaled(by: .unitScale), + values.3.scaled(by: .unitScale) ) ) - } + ) } set { + let values = newValue.scaled(by: .inverseUnitScale) if Self.legacyInterpolation { - // ResolvedGradient.Color.Space.convertOut(self) - _openSwiftUIUnimplementedFailure() + linearRed = values.first + linearGreen = values.second.first + linearBlue = values.second.second.first + opacity = values.second.second.second } else { - linearRed = newValue.first.scaled(by: .inverseUnitScale) - linearGreen = newValue.second.first.scaled(by: .inverseUnitScale) - linearBlue = newValue.second.second.first.scaled(by: .inverseUnitScale) - opacity = newValue.second.second.second.scaled(by: .inverseUnitScale) + self = ResolvedGradient.ColorSpace.perceptual.convertOut( + .init( + r: values.first, + g: values.second.first, + b: values.second.second.first, + a: values.second.second.second + ) + ) } } } diff --git a/Sources/OpenSwiftUICore/Graphic/Color/Paint.swift b/Sources/OpenSwiftUICore/Graphic/Color/Paint.swift index 41f18500a..dee08513b 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/Paint.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/Paint.swift @@ -7,6 +7,52 @@ package import Foundation +protocol Paint: ShapeStyle { + associatedtype ResolvedPaintType: ResolvedPaint + + func resolvePaint(in env: EnvironmentValues) -> ResolvedPaintType + func fallbackColor(in env: EnvironmentValues) -> Color? +} + +extension Paint { + public func _apply(to shape: inout _ShapeStyle_Shape) { + switch shape.operation { + case .prepareText: + shape.result = .none + case .resolveStyle(let name, let levels): + guard !levels.isEmpty else { return } + let resolvedPaint = resolvePaint(in: shape.environment) + let anyPaint: AnyResolvedPaint + if let bounds = shape.bounds { + anyPaint = _AnyResolvedPaint( + AnchoredResolvedPaint(resolvedPaint, bounds: bounds) + ) + } else { + anyPaint = _AnyResolvedPaint(resolvedPaint) + } + var style = ShapeStyle.Pack.Style(.paint(anyPaint)) + style.applyOpacity(shape.opacity(at: levels.lowerBound)) + shape.stylePack[name, levels.lowerBound] = style + case .fallbackColor: + if let color = fallbackColor(in: shape.environment) { + shape.result = .color(color) + } + default: + break + } + } +} + +extension Paint { + nonisolated public static func _makeView( + view: _GraphValue<_ShapeView>, + inputs: _ViewInputs + ) -> _ViewOutputs where S: Shape { + legacyMakeShapeView(view: view, inputs: inputs) + } +} + + // MARK: - ResolvedPaint package protocol ResolvedPaint: Equatable, Animatable, ProtobufEncodableMessage { @@ -55,6 +101,7 @@ package class AnyResolvedPaint: Equatable { final package class _AnyResolvedPaint

: AnyResolvedPaint where P: ResolvedPaint { package let paint: P + package init(_ paint: P) { self.paint = paint } @@ -103,6 +150,50 @@ final package class _AnyResolvedPaint

: AnyResolvedPaint where P: ResolvedPain extension AnyResolvedPaint: @unchecked Sendable {} extension _AnyResolvedPaint: @unchecked Sendable {} +// MARK: - AnchoredResolvedPaint + +package struct AnchoredResolvedPaint

: ResolvedPaint where P: ResolvedPaint { + var paint: P + var bounds: CGRect + + package init(_ paint: P, bounds: CGRect) { + self.paint = paint + self.bounds = bounds + } + + package func draw(path: Path, style: PathDrawingStyle, in context: GraphicsContext, bounds: CGRect?) { + let resolvedBounds: CGRect + if let bounds { + resolvedBounds = self.bounds.offsetBy(dx: bounds.origin.x, dy: bounds.origin.y) + } else { + resolvedBounds = self.bounds + } + paint.draw(path: path, style: style, in: context, bounds: resolvedBounds) + } + + package func encode(to encoder: inout ProtobufEncoder) throws { + try paint.encodePaint(to: &encoder) + try encoder.messageField(CodableResolvedPaint.Tag.anchorRect, bounds) + } + + package var animatableData: AnimatablePair, AnimatablePair>> { + get { AnimatablePair(paint.animatableData, bounds.animatableData) } + set { + paint.animatableData = newValue.first + bounds.animatableData = newValue.second + } + } + + package var isCALayerCompatible: Bool { paint.isCALayerCompatible } + + package var isClear: Bool { paint.isClear } + + package var isOpaque: Bool { paint.isOpaque } + + package static var leafProtobufTag: CodableResolvedPaint.Tag? { nil } +} + + // MARK: - ResolvedPaintVisitor package protocol ResolvedPaintVisitor { diff --git a/Sources/OpenSwiftUICore/Graphic/Gradient/AnyGradient.swift b/Sources/OpenSwiftUICore/Graphic/Gradient/AnyGradient.swift new file mode 100644 index 000000000..0cf039d88 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Gradient/AnyGradient.swift @@ -0,0 +1,135 @@ +// +// AnyGradient.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// ID: 50251084C79137239112ABA67F1FABAC +// Status: Compelete + +// MARK: - AnyGradient + +public struct AnyGradient: ShapeStyle, Hashable { + package var provider: AnyGradientBox + + package init(box: AnyGradientBox) { + self.provider = box + } + + package init

(provider: P) where P: GradientProvider { + self.provider = GradientBox(provider) + } + + public init(_ gradient: Gradient) { + self.provider = GradientBox(gradient) + } + + package func fallbackColor(in environment: EnvironmentValues) -> Color? { + provider.fallbackColor(in: environment) + } + + public func hash(into hasher: inout Hasher) { + provider.hash(into: &hasher) + } + + public func _apply(to shape: inout _ShapeStyle_Shape) { + provider.apply(to: &shape) + } + + package func resolve(in environment: EnvironmentValues) -> ResolvedGradient { + provider.resolve(in: environment) + } + + public static func == (lhs: AnyGradient, rhs: AnyGradient) -> Bool { + lhs.provider === rhs.provider ? true : lhs.provider.isEqual(to: rhs.provider) + } +} + +// MARK: - AnyGradientBox + +package class AnyGradientBox: AnyShapeStyleBox { + func resolve(in environment: EnvironmentValues) -> ResolvedGradient { + _openSwiftUIBaseClassAbstractMethod() + } + + func fallbackColor(in environment: EnvironmentValues) -> Color? { + _openSwiftUIBaseClassAbstractMethod() + } + + func hash(into: inout Hasher) { + _openSwiftUIBaseClassAbstractMethod() + } + + override package func apply(to shape: inout _ShapeStyle_Shape) { + _AnyLinearGradient( + gradient: AnyGradient(box: self), + startPoint: .top, + endPoint: .bottom + )._apply(to: &shape) + } +} + +// MARK: - GradientBox + +private class GradientBox

: AnyGradientBox where P: GradientProvider { + let base: P + + init(_ base: P) { + self.base = base + } + + override func resolve(in environment: EnvironmentValues) -> ResolvedGradient { + base.resolve(in: environment) + } + + override func fallbackColor(in environment: EnvironmentValues) -> Color? { + base.fallbackColor(in: environment) + } + + override func isEqual(to other: AnyShapeStyleBox) -> Bool { + guard let other = other as? GradientBox

else { return false } + return base == other.base + } + + override func hash(into hasher: inout Hasher) { + base.hash(into: &hasher) + } +} + +// MARK: - EitherGradient + +package enum EitherGradient: Hashable { + case gradient(Gradient) + case anyGradient(AnyGradient) + + package func fallbackColor(in environment: EnvironmentValues) -> Color? { + switch self { + case .gradient: nil + case .anyGradient(let anyGradient): + anyGradient.fallbackColor(in: environment) + } + } + + package func resolve(in environment: EnvironmentValues) -> ResolvedGradient { + switch self { + case .gradient(let gradient): + gradient.resolve(in: environment) + case .anyGradient(let anyGradient): + anyGradient.resolve(in: environment) + } + } + + package var constantColor: Color? { + switch self { + case .gradient(let gradient): + if gradient.stops.count == 0 { + Color.clear + } else if gradient.stops.count == 1 { + gradient.stops[0].color + } else { + nil + } + case .anyGradient: + nil + } + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Gradient/Gradient.swift b/Sources/OpenSwiftUICore/Graphic/Gradient/Gradient.swift new file mode 100644 index 000000000..df38ae4e2 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Gradient/Gradient.swift @@ -0,0 +1,475 @@ +// +// Gradient.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// ID: 3CA72A515D037D62EA3FD1FE1FD1F3CB +// Status: WIP + +public import Foundation +#if canImport(CoreGraphics) +import CoreGraphics +#endif + +// MARK: - GradientProvider + +package protocol GradientProvider: Hashable { + func resolve(in: EnvironmentValues) -> ResolvedGradient + func fallbackColor(in: EnvironmentValues) -> Color? +} + +extension GradientProvider { + package func fallbackColor(in: EnvironmentValues) -> Color? { + nil + } +} + +// MARK: - Gradient + +public struct Gradient: GradientProvider, ShapeStyle, Hashable { + public var stops: [Gradient.Stop] + + public init(stops: [Gradient.Stop]) { + self.stops = stops + } + + public struct Stop: Hashable { + public var color: Color + public var location: CGFloat + + public init(color: Color, location: CGFloat) { + self.color = color + self.location = location + } + + func resolve(in environment: EnvironmentValues) -> ResolvedGradient.Stop { + ResolvedGradient.Stop( + color: color.resolve(in: environment), + location: location, + interpolation: nil + ) + } + } + + public init(colors: [Color]) { + let count = colors.count + if count > 1 { + let step = 1.0 / CGFloat(colors.count - 1) + var stops: [Gradient.Stop] = [] + for (i, color) in colors.enumerated() { + stops.append(.init(color: color, location: CGFloat(i) * step)) + } + self.stops = stops + } else if count == 1 { + self.stops = [.init(color: colors[0], location: 0)] + } else { + self.stops = [] + } + } + + public func _apply(to shape: inout _ShapeStyle_Shape) { + LinearGradient( + gradient: self, + startPoint: .top, + endPoint: .bottom + )._apply(to: &shape) + } + + package func resolve(in environment: EnvironmentValues) -> ResolvedGradient { + ResolvedGradient(stops: stops.map{ $0.resolve(in: environment) }) + } +} + +@available(*, unavailable) +extension Gradient.Stop: Sendable {} + +// MARK: - ResolvedGradient + +package struct ResolvedGradient: Equatable { + var stops: [ResolvedGradient.Stop] + var colorSpace: ResolvedGradient.ColorSpace + + init(stops: [ResolvedGradient.Stop], colorSpace: ResolvedGradient.ColorSpace = .default) { + self.stops = stops + self.colorSpace = colorSpace + } + + init() { + self.stops = [] + self.colorSpace = .default + } + + mutating func multiplyOpacity(by opacity: Float) { + for i in stops.indices { + stops[i].color.opacity = stops[i].color.opacity * opacity + } + } + + #if canImport(CoreGraphics) + var cgGradient: CGGradient? { + _openSwiftUIUnimplementedFailure() + } + #endif + + var constantColor: Color.Resolved? { + guard !stops.isEmpty else { + return .clear + } + let color = stops[0].color + for stop in stops.dropFirst() { + guard stop.color == color else { + return nil + } + } + return color + } + + var hasInterpolations: Bool { + guard !stops.isEmpty else { return false } + return stops.contains{ $0.interpolation != nil } + } + + var interpolationsCount: Int { + guard !stops.isEmpty else { return 0 } + return stops.count { $0.interpolation != nil } + } + + var isClear: Bool { + guard !stops.isEmpty else { return true } + return stops.allSatisfy { $0.color.isClear } + } + + var isOpaque: Bool { + guard !stops.isEmpty else { return false } + return stops.allSatisfy { $0.color.isOpaque } + } + + package struct Stop: Equatable { + var color: Color.Resolved + var location: CGFloat + var interpolation: BezierTimingFunction? + + init(color: Color.Resolved, location: CGFloat, interpolation: BezierTimingFunction?) { + self.color = color + self.location = location + self.interpolation = interpolation + } + } +} + +extension ResolvedGradient: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + _openSwiftUIUnimplementedFailure() + } + + package init(from decoder: inout ProtobufDecoder) throws { + _openSwiftUIUnimplementedFailure() + } +} + +extension ResolvedGradient: CodableByProtobuf {} + +extension ResolvedGradient: Animatable { + package var animatableData: ResolvedGradientVector { + get { _SemanticFeature_v5.isEnabled ? ResolvedGradientVector(self) : .zero } + set { + guard _SemanticFeature_v5.isEnabled else { return } + stops.removeAll(keepingCapacity: true) + stops.reserveCapacity(newValue.stops.count) + for stop in newValue.stops { + stops.append( + Stop( + color: newValue.colorSpace.convertOut(stop.color), + location: stop.location, + interpolation: stop.interpolation + ) + ) + } + } + } +} + +extension ResolvedGradient.Stop: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + _openSwiftUIUnimplementedFailure() + } + + package init(from decoder: inout ProtobufDecoder) throws { + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - ResolvedGradient.ColorSpace + +extension ResolvedGradient { + package enum ColorSpace: UInt8, Hashable { + case device + case linear + case perceptual + + package static var `default`: ColorSpace { + _SemanticFeature_v4.isEnabled ? .perceptual : .device + } + + package func mix(_ lhs: Color.Resolved, _ rhs: Color.Resolved, by fraction: Float) -> Color.Resolved { + let lColor = convertIn(lhs) + let rColor = convertIn(rhs) + return convertOut(InterpolatableColor( + r: lColor.r * (1 - fraction) + rColor.r * fraction, + g: lColor.g * (1 - fraction) + rColor.g * fraction, + b: lColor.b * (1 - fraction) + rColor.b * fraction, + a: lColor.a * (1 - fraction) + rColor.a * fraction + )) + } + + package struct InterpolatableColor: Equatable { + var r: Float + var g: Float + var b: Float + var a: Float + } + + package func convertIn(_ color: Color.Resolved) -> InterpolatableColor { + var r: Float + var g: Float + var b: Float + switch self { + case .device: + r = color.red + g = color.green + b = color.blue + case .linear: + r = color.linearRed + g = color.linearGreen + b = color.linearBlue + case .perceptual: + r = 0.4122215 * color.linearRed + 0.5363325 * color.linearGreen + 0.05144599 * color.linearBlue + g = 0.2119035 * color.linearRed + 0.6806995 * color.linearGreen + 0.107397 * color.linearBlue + b = 0.088302463 * color.linearRed + 0.28171885 * color.linearGreen + 0.62997872 * color.linearBlue + r = pow(r, 1.0 / 3.0) + g = pow(g, 1.0 / 3.0) + b = pow(b, 1.0 / 3.0) + } + r = r * color.opacity + g = g * color.opacity + b = b * color.opacity + return InterpolatableColor(r: r, g: g, b: b, a: color.opacity) + } + + package func convertOut(_ color: InterpolatableColor) -> Color.Resolved { + var r = color.r + var g = color.g + var b = color.b + let a = color.a + if a != 0 { + r = r * (1.0 / a) + g = g * (1.0 / a) + b = b * (1.0 / a) + } + var resolved: Color.Resolved + switch self { + case .device: + resolved = Color.Resolved(red: r, green: g, blue: b, opacity: a) + case .linear: + resolved = Color.Resolved(linearRed: r, linearGreen: g, linearBlue: b, opacity: a) + case .perceptual: + let cubeR = r * r * r + let cubeG = g * g * g + let cubeB = b * b * b + r = 4.0767416954 * cubeR - 3.3077116013 * cubeG + 0.2309699357 * cubeB + g = -1.2684379816 * cubeR + 2.6097574234 * cubeG - 0.3413193822 * cubeB + b = -0.00419608643 * cubeR - 0.7034186125 * cubeG + 1.7076146603 * cubeB + resolved = Color.Resolved(red: r, green: g, blue: b, opacity: a) + } + return resolved + } + } +} + +extension ResolvedGradient.ColorSpace: ProtobufEnum {} + +// MARK: - ResolvedGradientVector + +package struct ResolvedGradientVector: VectorArithmetic { + fileprivate var stops: [ResolvedGradientVector.Stop] + package var colorSpace: ResolvedGradient.ColorSpace + + fileprivate struct Stop: Equatable { + var color: ResolvedGradient.ColorSpace.InterpolatableColor + var location: CGFloat + var interpolation: BezierTimingFunction? + } + + package init() { + self.stops = [] + self.colorSpace = .device + } + + package init(_ gradient: ResolvedGradient) { + self.stops = gradient.stops.map { + Stop( + color: gradient.colorSpace.convertIn($0.color), + location: $0.location, + interpolation: $0.interpolation + ) + } + self.colorSpace = gradient.colorSpace + } + + private mutating func add(_ other: ResolvedGradientVector, scaledBy scale: Double) { + let scale = Float(scale) + if stops.isEmpty { + if scale == 1 { + self = other + } else { + self.stops = other.stops.map { + Stop( + color: ResolvedGradient.ColorSpace.InterpolatableColor( + r: $0.color.r * scale, + g: $0.color.g * scale, + b: $0.color.b * scale, + a: $0.color.a * scale + ), + location: $0.location, + interpolation: nil + ) + } + self.colorSpace = other.colorSpace + } + return + } + + func mix( + stops: [ResolvedGradientVector.Stop], + count: Int, + at index: Int, + location: CGFloat + ) -> ResolvedGradientVector.Stop { + var color: ResolvedGradient.ColorSpace.InterpolatableColor + if index <= 0 { + color = stops[0].color + } else if index >= count { + color = stops[count - 1].color + } else { + let lLocation = stops[index - 1].location + let rLocation = stops[index].location + color = stops[index - 1].color + if lLocation != rLocation { + let t = Float((location - lLocation) / (rLocation - lLocation)) + let u = 1 - t + let rColor = stops[index].color + color = .init( + r: color.r * u + rColor.r * t, + g: color.g * u + rColor.g * t, + b: color.b * u + rColor.b * t, + a: color.a * u + rColor.a * t + ) + } + } + return .init(color: color, location: location, interpolation: nil) + } + + setColorSpace(other.colorSpace) + if stops.count == other.stops.count, zip(stops, other.stops).allSatisfy({ $0.location == $1.location }) { + for i in stops.indices { + stops[i].color.r += other.stops[i].color.r * scale + stops[i].color.g += other.stops[i].color.g * scale + stops[i].color.b += other.stops[i].color.b * scale + stops[i].color.a += other.stops[i].color.a * scale + + guard stops[i].interpolation != nil || other.stops[i].interpolation != nil else { continue } + let li = stops[i].interpolation ?? .linear + let ri = other.stops[i].interpolation ?? .linear + stops[i].interpolation = BezierTimingFunction( + p1: (li.p1x * scale + ri.p1x, li.p1y * scale + ri.p1y), + p2: (li.p2x * scale + ri.p2x, li.p2y * scale + ri.p2y) + ) + } + } else { + var result: [Stop] = [] + result.reserveCapacity(max(stops.count, other.stops.count)) + var i = 0 + var j = 0 + while i < stops.count || j < other.stops.count { + let lLocation = i < stops.count ? stops[i].location : .infinity + let rLocation = j < other.stops.count ? other.stops[j].location : .infinity + var stop: Stop + let color: ResolvedGradient.ColorSpace.InterpolatableColor + if lLocation == rLocation { + stop = stops[i] + color = other.stops[j].color + i += 1 + j += 1 + } else if lLocation < rLocation { + stop = stops[i] + color = mix(stops: other.stops, count: other.stops.count, at: j, location: lLocation).color + i += 1 + } else { + stop = mix(stops: stops, count: stops.count, at: i, location: rLocation) + color = other.stops[j].color + j += 1 + } + stop.color.r += color.r * scale + stop.color.g += color.g * scale + stop.color.b += color.b * scale + stop.color.a += color.a * scale + result.append(stop) + } + self.stops = result + } + } + + private mutating func setColorSpace(_ colorSpace: ResolvedGradient.ColorSpace) { + guard self.colorSpace != colorSpace else { return } + for i in stops.indices { + stops[i].color = colorSpace.convertIn(colorSpace.convertOut(stops[i].color)) + } + self.colorSpace = colorSpace + } + + package mutating func scale(by scale: Double) { + for i in stops.indices { + let color = stops[i].color + let colorScaled = ResolvedGradient.ColorSpace.InterpolatableColor( + r: color.r * Float(scale), + g: color.g * Float(scale), + b: color.b * Float(scale), + a: color.a * Float(scale) + ) + stops[i].color = colorScaled + } + } + + package var magnitudeSquared: Double { + guard !stops.isEmpty else { return 0 } + var result = 0.0 + for stop in stops { + let color = stop.color + let sum = color.r * color.r + color.g * color.g + color.b * color.b + color.a * color.a + result += Double(sum) + } + return result + } + + package static func + (_ lhs: ResolvedGradientVector, _ rhs: ResolvedGradientVector) -> ResolvedGradientVector { + var result = lhs + result.add(rhs, scaledBy: 1) + return result + } + + package static func - (_ lhs: ResolvedGradientVector, _ rhs: ResolvedGradientVector) -> ResolvedGradientVector { + var result = lhs + result.add(rhs, scaledBy: -1) + return result + } + + package static func += (_ lhs: inout ResolvedGradientVector, _ rhs: ResolvedGradientVector) { + lhs = lhs + rhs + } + + package static func -= (_ lhs: inout ResolvedGradientVector, _ rhs: ResolvedGradientVector) { + lhs = lhs - rhs + } + + package static let zero = ResolvedGradientVector() +} diff --git a/Sources/OpenSwiftUICore/Graphic/Gradient/GradientColorProvider.swift b/Sources/OpenSwiftUICore/Graphic/Gradient/GradientColorProvider.swift new file mode 100644 index 000000000..b96c2605b --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Gradient/GradientColorProvider.swift @@ -0,0 +1,48 @@ +// +// GradientColorProvider.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// ID: FE0C0CD2077F7DA45B2F6C579EB58E7B +// Status: Complete + +package import Foundation + +// MARK: - GradientColorProvider + +private struct GradientColorProvider: ColorProvider { + var base: EitherGradient + var location: CGFloat + + func resolve(in environment: EnvironmentValues) -> Color.Resolved { + let resolvedGradient = base.resolve(in: environment) + let stops = resolvedGradient.stops + let colorSpace = resolvedGradient.colorSpace + + guard !stops.isEmpty else { return .clear } + guard stops.count > 1 else { return stops[0].color } + + let index = stops.partitionPoint { $0.location >= location } + if index == 0 { return stops[0].color } + if index == stops.count { return stops[stops.count - 1].color } + let lhs = stops[index - 1] + let rhs = stops[index] + var t = (location - lhs.location) / (rhs.location - lhs.location) + if let interpolation = rhs.interpolation { + t = UnitCurve(interpolation).value(at: t) + } + return colorSpace.mix(lhs.color, rhs.color, by: Float(t)) + } +} + +extension AnyGradient { + package func color(at location: CGFloat) -> Color { + Color(provider: GradientColorProvider(base: .anyGradient(self), location: location)) + } +} + +extension Gradient { + package func color(at location: CGFloat) -> Color { + Color(provider: GradientColorProvider(base: .gradient(self), location: location)) + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Gradient/GradientColorSpace.swift b/Sources/OpenSwiftUICore/Graphic/Gradient/GradientColorSpace.swift index 00a598ff1..489bf9f5d 100644 --- a/Sources/OpenSwiftUICore/Graphic/Gradient/GradientColorSpace.swift +++ b/Sources/OpenSwiftUICore/Graphic/Gradient/GradientColorSpace.swift @@ -2,10 +2,14 @@ // GradientColorSpace.swift // OpenSwiftUICore // -// Status: Blocked by AnyGradient +// Audited for 6.5.4 +// ID: D9C66741AC30F809B56241FAEE383AD3 +// Status: Compelete import Foundation +// MARK: - Gradient.ColorSpace + extension Gradient { /// A method of interpolating between the colors in a gradient. public struct ColorSpace: Hashable, Sendable { @@ -21,13 +25,33 @@ extension Gradient { public static let perceptual: Gradient.ColorSpace = .init(base: .perceptual) } -// public func colorSpace(_ space: Gradient.ColorSpace) -> AnyGradient { -// _openSwiftUIUnimplementedFailure() -// } + public func colorSpace(_ space: Gradient.ColorSpace) -> AnyGradient { + AnyGradient( + provider: ColorSpaceGradientProvider(base: .gradient(self), + colorSpace: space.base + )) + } +} + +extension AnyGradient { + public func colorSpace(_ space: Gradient.ColorSpace) -> AnyGradient { + AnyGradient( + provider: ColorSpaceGradientProvider(base: .anyGradient(self), + colorSpace: space.base + )) + } } -//extension AnyGradient { -// public func colorSpace(_ space: Gradient.ColorSpace) -> AnyGradient { -// _openSwiftUIUnimplementedFailure() -// } -//} +private struct ColorSpaceGradientProvider: GradientProvider { + var base: EitherGradient + + var colorSpace: ResolvedGradient.ColorSpace + + func resolve(in environment: EnvironmentValues) -> ResolvedGradient { + base.resolve(in: environment) + } + + func fallbackColor(in environment: EnvironmentValues) -> Color? { + base.fallbackColor(in: environment) + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Gradient/LinearGradient.swift b/Sources/OpenSwiftUICore/Graphic/Gradient/LinearGradient.swift new file mode 100644 index 000000000..5f04e4633 --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Gradient/LinearGradient.swift @@ -0,0 +1,124 @@ +// +// LinearGradient.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP + +package import Foundation + +// MARK: - _AnyLinearGradient + +package struct _AnyLinearGradient: Paint { + var gradient: AnyGradient + var startPoint: UnitPoint + var endPoint: UnitPoint + + package init(gradient: AnyGradient, startPoint: UnitPoint, endPoint: UnitPoint) { + self.gradient = gradient + self.startPoint = startPoint + self.endPoint = endPoint + } + + @inline(__always) + func resolvePaint(in env: EnvironmentValues) -> LinearGradient._Paint { + LinearGradient._Paint( + gradient: gradient.resolve(in: env), + startPoint: startPoint, + endPoint: endPoint + ) + } + + @inline(__always) + func fallbackColor(in env: EnvironmentValues) -> Color? { + gradient.fallbackColor(in: env) + } +} + +// MARK: - LinearGradient + +@frozen +public struct LinearGradient: Paint, View { + var gradient: Gradient + var startPoint: UnitPoint + var endPoint: UnitPoint + + public init(gradient: Gradient, startPoint: UnitPoint, endPoint: UnitPoint) { + self.gradient = gradient + self.startPoint = startPoint + self.endPoint = endPoint + } + + package struct _Paint: ResolvedPaint { + var gradient: ResolvedGradient + var startPoint: UnitPoint + var endPoint: UnitPoint + + package static let leafProtobufTag: CodableResolvedPaint.Tag? = .linearGradient + + package func draw(path: Path, style: PathDrawingStyle, in context: GraphicsContext, bounds: CGRect?) { + let rect = bounds ?? path.boundingRect + context.draw( + path, + with: .gradient(gradient, .axial(startPoint.in(rect), endPoint.in(rect)), []), + style: style + ) + } + + package var isClear: Bool { + gradient.isClear + } + + package var isOpaque: Bool { + gradient.isOpaque + } + } + + package func resolvePaint(in env: EnvironmentValues) -> _Paint { + _Paint( + gradient: gradient.resolve(in: env), + startPoint: startPoint, + endPoint: endPoint + ) + } + + package func fallbackColor(in environment: EnvironmentValues) -> Color? { + guard !gradient.stops.isEmpty else { + return nil + } + return gradient.stops[0].color + } + + nonisolated public static func _makeView( + view: _GraphValue<_ShapeView>, + inputs: _ViewInputs + ) -> _ViewOutputs where S: Shape { + _ShapeView.makeView(view: view, inputs: inputs) + } +} + +extension LinearGradient._Paint: Animatable { + package var animatableData: AnimatablePair, AnimatablePair>, ResolvedGradientVector> { + get { + AnimatablePair( + AnimatablePair(startPoint.animatableData, endPoint.animatableData), + gradient.animatableData + ) + } + set { + startPoint.animatableData = newValue.first.first + endPoint.animatableData = newValue.first.second + gradient.animatableData = newValue.second + } + } +} + +extension LinearGradient._Paint: ProtobufMessage { + package init(from decoder: inout ProtobufDecoder) throws { + _openSwiftUIUnimplementedFailure() + } + + package func encode(to encoder: inout ProtobufEncoder) throws { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Gradient/ResolvedGradient.swift b/Sources/OpenSwiftUICore/Graphic/Gradient/ResolvedGradient.swift deleted file mode 100644 index c643c36bc..000000000 --- a/Sources/OpenSwiftUICore/Graphic/Gradient/ResolvedGradient.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ResolvedGradient.swift -// OpenSwiftUICore -// -// Audited for 6.0.87 -// Status: Empty - -package struct ResolvedGradient: Equatable { - package enum ColorSpace { - case device - case linear - case perceptual - - func mix(_ lhs: Color.Resolved, _ rhs: Color.Resolved, by fraction: Float) -> Color.Resolved { - _openSwiftUIUnimplementedFailure() - } - } -} - -// FIXME -public struct Gradient {} diff --git a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift index ae1bcf36a..a16c41875 100644 --- a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift +++ b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift @@ -512,14 +512,41 @@ public struct GraphicsContext { public static var inverse: ClipOptions { Self(rawValue: 1 << 0) } } + package enum GradientGeometry { + case axial(CGPoint, CGPoint) + case radial(CGPoint, CGFloat, CGFloat) + case radial2(CGPoint, CGFloat, CGPoint, CGFloat) + case elliptical(CGRect, CGFloat, CGFloat) + case conic(CGPoint, Angle) + case angular(CGPoint, Angle, Angle) + } + // FIXME package enum ResolvedShading: Sendable { case backdrop(Color.Resolved) case color(Color.Resolved) case sRGBColor(ORBColor) + // case shader(Shader.ResolvedShader, CGRect) + case gradient(ResolvedGradient, GradientGeometry, GradientOptions) + // case meshGradient(MeshGradient._Paint, CGRect) + // case tiledImage(GraphicsImage, origin: CGPoint, sourceRect: CGRect, scale: CGFloat) case style(_ShapeStyle_Pack.Style) case levels([GraphicsContext.ResolvedShading]) } + + package struct GradientOptions: OptionSet { + package let rawValue: UInt32 + + package init(rawValue: UInt32) { + self.rawValue = rawValue + } + + package static let `repeat`: GradientOptions = .init(rawValue: 1 << 0) + + package static let mirror: GradientOptions = .init(rawValue: 1 << 1) + + package static let linearColor: GradientOptions = .init(rawValue: 1 << 2) + } } extension GraphicsContext { diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/AnyShapeStyle.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/AnyShapeStyle.swift index 1466c7d2b..246bcdfa8 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/AnyShapeStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/AnyShapeStyle.swift @@ -36,8 +36,8 @@ public struct AnyShapeStyle: ShapeStyle { storage = style.storage } else if let color = style as? Color { storage = .init(box: color.provider) -// } else if let gradient = style as? AnyGradient { -// storage = .init(box: gradient.provider) + } else if let gradient = style as? AnyGradient { + storage = .init(box: gradient.provider) } else { storage = .init(box: ShapeStyleBox(style)) } diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift index 6bb4f7ab9..112486951 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyle.swift @@ -191,7 +191,25 @@ extension ShapeStyle { } } - // package func resolveGradient(in environment: EnvironmentValues, level: Int = 0) -> ResolvedGradient? + package func resolveGradient(in environment: EnvironmentValues, level: Int = 0) -> ResolvedGradient? { + let name = ShapeStyle.Name.foreground + var shape = _ShapeStyle_Shape( + operation: .resolveStyle(name: name, levels: level ..< level + 1), + environment: environment + ) + _apply(to: &shape) + let style = shape.stylePack[name, level] + switch style.fill { + case let .paint(paint): + guard var gradient = paint.resolvedGradient else { + return nil + } + gradient.multiplyOpacity(by: style.opacity) + return gradient + default: + return nil + } + } package func copyStyle(name: Name = .foreground, in env: EnvironmentValues, foregroundStyle: AnyShapeStyle? = nil) -> AnyShapeStyle { var shape = _ShapeStyle_Shape(operation: .copyStyle(name: name), environment: env, foregroundStyle: foregroundStyle) diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift index 1e23ccbdc..5c01a8fdc 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStylePack.swift @@ -493,8 +493,8 @@ extension _ShapeStyle_Pack.Fill: Animatable { case zero case color(Color.Resolved.AnimatableData) case vibrantColor(Color.ResolvedVibrant.AnimatableData) + case linearGradient(LinearGradient._Paint.AnimatableData) // TODO: Gradient + Shader - // case linearGradient(LinearGradient._Paint.AnimatableData) // case radialGradient(RadialGradient._Paint.AnimatableData) // case ellipticalGradient(EllipticalGradient._Paint.AnimatableData) // case angularGradient(AngularGradient._Paint.AnimatableData) @@ -538,9 +538,17 @@ extension _ShapeStyle_Pack.Fill: Animatable { default: break } case let .paint(anyPaint): - _ = anyPaint - // TODO: PaintSetVisitor for gradient/shader types - break + switch self { + case let .linearGradient(data): + var visitor = PaintSetVisitor( + data: data, + result: .color(.clear) + ) + anyPaint.visit(&visitor) + fill = visitor.result + // TODO: Handle gradient and shader paint types + default: break + } case .foregroundMaterial(var resolved, let materialStyle): switch self { case let .color(data): @@ -583,6 +591,13 @@ extension _ShapeStyle_Pack.Fill: Animatable { } lhsData += rhsData lhs = .vibrantColor(lhsData) + case let .linearGradient(rhsData): + guard case var .linearGradient(lhsData) = lhs else { + lhs = rhs + return + } + lhsData += rhsData + lhs = .linearGradient(lhsData) case let .colorMatrix(rhsData): guard case var .colorMatrix(lhsData) = lhs else { lhs = rhs @@ -597,26 +612,43 @@ extension _ShapeStyle_Pack.Fill: Animatable { switch rhs { case .zero: return case let .color(rhsData): - guard case var .color(lhsData) = lhs else { - lhs = rhs - return + switch lhs { + case var .color(lhsData): + lhsData -= rhsData + lhs = .color(lhsData) + case .zero: + lhs = .color(rhsData.scaled(by: -1)) + default: break } - lhsData -= rhsData - lhs = .color(lhsData) case let .vibrantColor(rhsData): - guard case var .vibrantColor(lhsData) = lhs else { - lhs = rhs - return + switch lhs { + case var .vibrantColor(lhsData): + lhsData -= rhsData + lhs = .vibrantColor(lhsData) + case .zero: + lhs = .vibrantColor(rhsData.scaled(by: -1)) + default: break + } + case let .linearGradient(rhsData): + switch lhs { + case var .linearGradient(lhsData): + lhsData -= rhsData + lhs = .linearGradient(lhsData) + case .zero: + lhs = .linearGradient(rhsData.scaled(by: -1)) + default: break } - lhsData -= rhsData - lhs = .vibrantColor(lhsData) case let .colorMatrix(rhsData): - guard case var .colorMatrix(lhsData) = lhs else { - lhs = rhs - return + switch lhs { + case var .colorMatrix(lhsData): + lhsData.subtract(rhsData) + lhs = .colorMatrix(lhsData) + case .zero: + var result = rhsData + result.scale(by: -1) + lhs = .colorMatrix(result) + default: break } - lhsData.subtract(rhsData) - lhs = .colorMatrix(lhsData) } } @@ -644,6 +676,9 @@ extension _ShapeStyle_Pack.Fill: Animatable { case var .vibrantColor(data): data.scale(by: rhs) self = .vibrantColor(data) + case var .linearGradient(data): + data.scale(by: rhs) + self = .linearGradient(data) case var .colorMatrix(data): data.scale(by: rhs) self = .colorMatrix(data) @@ -655,6 +690,7 @@ extension _ShapeStyle_Pack.Fill: Animatable { case .zero: return 0.0 case let .color(data): return data.magnitudeSquared case let .vibrantColor(data): return data.magnitudeSquared + case let .linearGradient(data): return data.magnitudeSquared case let .colorMatrix(data): return data.magnitudeSquared } } @@ -664,6 +700,7 @@ extension _ShapeStyle_Pack.Fill: Animatable { case (.zero, .zero): return true case let (.color(lhs), .color(rhs)): return lhs == rhs case let (.vibrantColor(lhs), .vibrantColor(rhs)): return lhs == rhs + case let (.linearGradient(lhs), .linearGradient(rhs)): return lhs == rhs case let (.colorMatrix(lhs), .colorMatrix(rhs)): return lhs == rhs default: return false } @@ -687,8 +724,20 @@ extension _ShapeStyle_Pack.Fill: Animatable { func visitPaint

(_ paint: P) where P: ResolvedPaint { if let color = paint as? Color.Resolved { result.pointee = .color(color.animatableData) + } else if let linearGradient = paint as? LinearGradient._Paint { + result.pointee = .linearGradient(linearGradient.animatableData) } - // TODO: Handle gradient and shader paint types + // else if let radialGradient = paint as? RadialGradient._Paint { + // result.pointee = .radialGradient(radialGradient.animatableData) + // } else if let ellipticalGradient = paint as? EllipticalGradient._Paint { + // result.pointee = .ellipticalGradient(ellipticalGradient.animatableData) + // } else if let angularGradient = paint as? AngularGradient._Paint { + // result.pointee = .angularGradient(angularGradient.animatableData) + // } else if let meshGradient = paint as? MeshGradient._Paint { + // result.pointee = .meshGradient(meshGradient.animatableData) + // } else if let shader = paint as? Shader.ResolvedShader { + // result.pointee = .shader(ShaderVectorData(shader)) + // } } } } diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift index fa340c2c1..382033504 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift @@ -190,6 +190,8 @@ package struct _ShapeStyle_RenderedShape { opacity = 1.0 } render(color: resolved) + case let .paint(paint): + render(paint: paint) default: _openSwiftUIUnimplementedFailure() } @@ -252,7 +254,18 @@ package struct _ShapeStyle_RenderedShape { } private mutating func render(paint: AnyResolvedPaint) { - _openSwiftUIUnimplementedFailure() + switch shape { + case let .path(path, fillStyle): + item.value = .content(DisplayList.Content( + .shape(path, paint, fillStyle), + seed: contentSeed + )) + case .text, .image, .alphaMask: + render(color: .white) + _openSwiftUIUnimplementedFailure() + case .empty: + _openSwiftUIUnimplementedFailure() + } } private mutating func renderVectorGlyph( diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleShape.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleShape.swift index 259144579..63ffd2944 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleShape.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleShape.swift @@ -14,11 +14,11 @@ public struct _ShapeStyle_Shape { package enum Operation { case prepareText(level: Int) case resolveStyle(name: _ShapeStyle_Name, levels: Range) - case multiLevel case fallbackColor(level: Int) case copyStyle(name: _ShapeStyle_Name) - case primaryStyle case modifyBackground(level: Int) + case multiLevel + case primaryStyle } package enum Result {