├── .gitignore ├── Files └── bezier.gif ├── Package.swift ├── README.md └── Sources ├── AnimatableBezierPath ├── BezierPath.swift ├── MetalBezierView.swift ├── NativeColor.swift ├── Samples │ └── Hello.swift ├── Shaders │ ├── bezier_shaders.metal │ └── circle_shader.metal └── SwiftUI │ ├── AnimatableBezierView.swift │ └── MetalViewWrapper.swift └── _BezierCHeaders ├── PleasingSwiftPM.c └── include └── RendererTypes.h /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Files/bezier.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patskovn/AnimatableBezierPath/97e5163182959f0ea4dd461efd6975583ac16b5b/Files/bezier.gif -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AnimatableBezierPath", 8 | platforms: [.macOS(.v10_15), .iOS(.v13), .macCatalyst(.v13)], 9 | products: [ 10 | .library(name: "AnimatableBezierPath", targets: ["AnimatableBezierPath"]), 11 | .library(name: "_BezierCHeaders", targets: ["_BezierCHeaders"]) 12 | ], 13 | targets: [ 14 | .target(name: "AnimatableBezierPath", dependencies: ["_BezierCHeaders"]), 15 | .target(name: "_BezierCHeaders") 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnimatableBezierPath 2 | 3 | Metal-based implementation of bezier curve that you fill with gradient along path and animate. 4 | 5 | Here how it looks. 6 | 7 | ![Mesh gradient gif](Files/bezier.gif) 8 | 9 | ## How to generate mesh 10 | 11 | It is as simple as native UIKit API. Ideally you should ask your designer to create SVG file with single continuous bezier curve, export it and convert it with online converter [like this one](http://svg-converter.kyome.io/) or manually. 12 | You can find "Hello" example [here](Sources/AnimatableBezierPath/Samples/Hello.swift). 13 | 14 | Here how it looks like: 15 | 16 | ```swift 17 | public var helloBezierPath: BezierPath = { 18 | // Initiate bezier path with `viewBox` and desired `lineWidth` 19 | var shape = BezierPath(viewBox: .init(width: 1032, height: 322), lineWidth: 120) 20 | 21 | // Start your curve via moving to initial position 22 | shape.move(to: CGPoint(x: 1, y: 303.38)) 23 | 24 | // And then you should draw it relatively to your `viewBox` 25 | shape.addCurve(to: CGPoint(x: 109.8, y: 227.73), controlPoint1: CGPoint(x: 28.31, y: 287.4), controlPoint2: CGPoint(x: 88.3, y: 249.9)) 26 | shape.addCurve(to: CGPoint(x: 210.23, y: 48.3), controlPoint1: CGPoint(x: 164.86, y: 164.55), controlPoint2: CGPoint(x: 202.46, y: 117.58)) 27 | 28 | return shape 29 | }() 30 | ``` 31 | 32 | And to draw it you need to use provided `AnimatableBezierView` 33 | 34 | ```swift 35 | struct HelloView: View { 36 | 37 | @State var animationPercent: Float = 0 38 | 39 | var body: some View { 40 | AnimatableBezierView(path: helloBezierPath, colors: helloColors, animationPercent: animationPercent) 41 | .onAppear { 42 | withAnimation(.easeInOut(duration: 3)) { 43 | animationPercent = 1 44 | } 45 | } 46 | } 47 | 48 | } 49 | ``` 50 | 51 | ## Add via SwiftPM 52 | 53 | ```swift 54 | dependencies: [ 55 | .package(url: "https://github.com/Nekitosss/AnimatableBezierPath", from: "1.0.0"), 56 | ], 57 | ``` 58 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/BezierPath.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #if canImport(UIKit) 24 | import UIKit 25 | let defaultScale = UIScreen.main.scale 26 | #elseif canImport(AppKit) 27 | import AppKit 28 | let defaultScale = NSScreen.main?.backingScaleFactor ?? 1 29 | #endif 30 | 31 | import _BezierCHeaders 32 | 33 | public struct BezierPath { 34 | public var parameters: [BezierParameters] 35 | 36 | private let scale: Float 37 | private let viewBox: CGSize 38 | private let lineWidth: CGFloat 39 | 40 | private var startingPoint: CGPoint? 41 | 42 | /// Creates bezier path according to parameters. Usually you should provide empty parameters and use `addCurve` api to fill it in. 43 | /// - Parameters: 44 | /// - parameters: Array of bezier parameters to draw. For ideal picture this should be a single curve with mirrored control point on the vertices points. 45 | /// - scale: Scale of the screen 46 | /// - viewBox: Size for bounding box of a curve 47 | /// - lineWidth: This one is self-explanatory 48 | init(parameters: [BezierParameters] = [], scale: Float = Float(defaultScale), viewBox: CGSize, lineWidth: CGFloat) { 49 | self.parameters = parameters 50 | self.scale = scale 51 | self.viewBox = viewBox 52 | self.lineWidth = lineWidth 53 | } 54 | 55 | func normalizedLineWidth(viewSize: CGSize) -> Float { 56 | Float(lineWidth / viewSize.width) 57 | } 58 | 59 | func normalizedParameters(viewSize: CGSize) -> [BezierParameters] { 60 | let viewBox = simd_float2(Float(self.viewBox.width), Float(self.viewBox.height)) 61 | let viewSize = simd_float2(Float(viewSize.width), Float(viewSize.height)) 62 | let shift = simd_float2(abs(viewSize.x - viewBox.x * scale) / 2, abs(viewSize.y - viewBox.y * scale) / 2) / viewSize 63 | 64 | func toMetal(_ v: simd_float2) -> simd_float2 { 65 | .init((2 * v.x) - 1, (v.y * -2) + 1) * 0.9 66 | } 67 | 68 | return parameters 69 | .map { BezierParameters(a: $0.a / viewSize * scale + shift, b: $0.b / viewSize * scale + shift, p1: $0.p1 / viewSize * scale + shift, p2: $0.p2 / viewSize * scale + shift) } 70 | .map { BezierParameters(a: toMetal($0.a), b: toMetal($0.b), p1: toMetal($0.p1), p2: toMetal($0.p2)) } 71 | } 72 | 73 | /// You may use that method but ideally you shouldn't. Prefer `addCurve` API 74 | public mutating func move(to point: CGPoint) { 75 | if let last = parameters.last { 76 | parameters.append(BezierParameters(a: last.b, b: point.f2, p1: 2 * last.b - last.p2, p2: (last.b + point.f2) / 2)) 77 | } else { 78 | startingPoint = point 79 | } 80 | } 81 | 82 | public mutating func addCurve(to b: CGPoint, controlPoint1 p1: CGPoint, controlPoint2 p2: CGPoint) { 83 | if let last = parameters.last { 84 | parameters.append(BezierParameters(a: last.b, b: b.f2, p1: p1.f2, p2: p2.f2)) 85 | } else if let a = startingPoint { 86 | parameters.append(BezierParameters(a: a.f2, b: b.f2, p1: p1.f2, p2: p2.f2)) 87 | } 88 | } 89 | 90 | /// This method mirrors last connection control point to achieve smooth connection. 91 | public mutating func normalizeLastConnection() { 92 | guard parameters.count > 2 else { return } 93 | parameters[parameters.count - 1].p1 = 2 * parameters[parameters.count - 2].b - parameters[parameters.count - 2].p2 94 | } 95 | } 96 | 97 | private extension CGPoint { 98 | var f2: simd_float2 { 99 | .init(Float(x), Float(y)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/MetalBezierView.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | import Metal 24 | import MetalKit 25 | import _BezierCHeaders 26 | 27 | public class MetalBezierView: MTKView { 28 | 29 | /// This represents amount of triangles used to draw single bezier curve. 30 | /// By single it means not whole `BezierPath` struct, but single `BezierParameters`. 31 | /// The bigger value here the "smoother" the curve is 32 | private let trianglesPerBezierCurve: UInt32 33 | 34 | private var commandQueue: MTLCommandQueue? 35 | private var bezierPipelineDescriptor = MTLRenderPipelineDescriptor() 36 | private var circlePipelineDescriptor = MTLRenderPipelineDescriptor() 37 | 38 | /// Array of indices for triangles that we are manupulate to draw certain percent of whole curve 39 | private let indices: [UInt16] 40 | private let indicesBuffer: MTLBuffer? 41 | private var msaaColorTexture: MTLTexture? 42 | 43 | var params: BezierPath 44 | var animationPercent: Float = 0 45 | var colors: [simd_float4] { 46 | didSet { 47 | guard !colors.isEmpty else { 48 | fatalError("Bruh I really need colors here") 49 | } 50 | } 51 | } 52 | 53 | init(params: BezierPath, colors: [simd_float4], device: MTLDevice? = MTLCreateSystemDefaultDevice(), trianglesPerBezierCurve: UInt32 = 300) { 54 | guard !colors.isEmpty else { 55 | fatalError("Bruh I really need colors here") 56 | } 57 | 58 | self.params = params 59 | self.colors = colors 60 | self.trianglesPerBezierCurve = trianglesPerBezierCurve 61 | self.indices = stride(from: 0, to: UInt16(trianglesPerBezierCurve), by: 1).reduce(into: []) { 62 | $0 += [$1, $1 + 1, $1 + 2] 63 | } 64 | 65 | self.indicesBuffer = device?.makeBuffer(bytes: indices, 66 | length: MemoryLayout.size * indices.count, 67 | options: .storageModeShared)! 68 | 69 | super.init(frame: .zero, device: device) 70 | 71 | framebufferOnly = true 72 | colorPixelFormat = .bgra8Unorm 73 | clearColor = MTLClearColor(red: 0.0, green: 0, blue: 0, alpha: 0) 74 | preferredFramesPerSecond = 60 75 | 76 | // Run with 4x MSAA 77 | sampleCount = 4 78 | 79 | // Bloody API differences -_- 80 | #if canImport(AppKit) 81 | layer?.isOpaque = false 82 | #else 83 | layer.isOpaque = false 84 | #endif 85 | 86 | 87 | guard let device else { return } 88 | self.device = device 89 | commandQueue = device.makeCommandQueue() 90 | 91 | // We should use `.module` here, because this is distributed via swift package manager 92 | guard let library = try? device.makeDefaultLibrary(bundle: .module) else { return } 93 | initializeMetalPipeline(from: library) 94 | } 95 | 96 | 97 | /// Unpack & compile all shaders from the library 98 | private func initializeMetalPipeline(from library: MTLLibrary) { 99 | bezierPipelineDescriptor.vertexFunction = library.makeFunction(name: "bezier_vertex") 100 | bezierPipelineDescriptor.fragmentFunction = library.makeFunction(name: "bezier_fragment") 101 | bezierPipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat 102 | 103 | // Run with 4x MSAA: 104 | bezierPipelineDescriptor.sampleCount = self.sampleCount 105 | bezierPipelineDescriptor.rasterSampleCount = self.sampleCount 106 | 107 | circlePipelineDescriptor.vertexFunction = library.makeFunction(name: "circle_vertex") 108 | circlePipelineDescriptor.fragmentFunction = library.makeFunction(name: "circle_fragment") 109 | circlePipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat 110 | 111 | // Run with 4x MSAA: 112 | circlePipelineDescriptor.sampleCount = sampleCount 113 | circlePipelineDescriptor.rasterSampleCount = sampleCount 114 | } 115 | 116 | required init(coder: NSCoder) { 117 | fatalError("Storyboards are for lamers") 118 | } 119 | 120 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 121 | msaaColorTexture = nil 122 | } 123 | 124 | /// Creates our custom texture that we will use for rendering. 125 | /// We need that because MTKView's own texture supports MSAA only for one render pipeline but we have two: circles and bezier path. 126 | /// So we creating our custom texture. 127 | private func makeMSAARenderTextureIfNeeded() { 128 | let drawableWidth = Int(drawableSize.width) 129 | let drawableHeight = Int(drawableSize.height) 130 | 131 | if msaaColorTexture == nil || 132 | msaaColorTexture?.width != drawableWidth || 133 | msaaColorTexture?.height != drawableHeight || 134 | msaaColorTexture?.sampleCount != sampleCount 135 | { 136 | let textureDescriptor = MTLTextureDescriptor() 137 | textureDescriptor.textureType = .type2DMultisample 138 | textureDescriptor.sampleCount = sampleCount 139 | textureDescriptor.pixelFormat = colorPixelFormat 140 | textureDescriptor.width = drawableWidth 141 | textureDescriptor.height = drawableHeight 142 | textureDescriptor.storageMode = .private 143 | textureDescriptor.usage = .renderTarget 144 | 145 | msaaColorTexture = device?.makeTexture(descriptor: textureDescriptor) 146 | msaaColorTexture?.label = "MSAA x\(sampleCount) Color" 147 | } 148 | } 149 | 150 | private func renderPassDescriptor(colorTexture: MTLTexture) -> MTLRenderPassDescriptor { 151 | let renderPassDescriptor = MTLRenderPassDescriptor() 152 | 153 | renderPassDescriptor.colorAttachments[0].texture = msaaColorTexture 154 | renderPassDescriptor.colorAttachments[0].resolveTexture = colorTexture 155 | renderPassDescriptor.colorAttachments[0].loadAction = .clear 156 | renderPassDescriptor.colorAttachments[0].clearColor = clearColor 157 | renderPassDescriptor.colorAttachments[0].storeAction = .storeAndMultisampleResolve 158 | 159 | return renderPassDescriptor 160 | } 161 | 162 | 163 | public override func draw(_ rect: CGRect) { 164 | guard rect.size != .zero else { return } 165 | 166 | makeMSAARenderTextureIfNeeded() 167 | 168 | guard let commandBuffer = commandQueue?.makeCommandBuffer(), 169 | let currentDrawable 170 | else { return } 171 | 172 | let path = self.params 173 | let lineWidth: Float = path.normalizedLineWidth(viewSize: drawableSize) 174 | let bezierCurve = path.normalizedParameters(viewSize: drawableSize) 175 | 176 | var circles: [CircleVertex] = [] 177 | if animationPercent.isZero { 178 | circles = [] 179 | } else { 180 | circles = [ 181 | CircleVertex(center: bezierCurve[0].a, 182 | color: colors.first!, 183 | radius: lineWidth / 2), 184 | CircleVertex(center: getClosingCircleCenter(animationPercent: animationPercent, vectors: bezierCurve), 185 | color: getClosingCircleColor(animationPercent: animationPercent, gradientColors: colors), 186 | radius: lineWidth / 2), 187 | ] 188 | } 189 | 190 | let bezierPassDescriptor = self.renderPassDescriptor(colorTexture: currentDrawable.texture) 191 | render(bezierCurve: bezierCurve, commandBuffer: commandBuffer, renderPassDescriptor: bezierPassDescriptor, colors: colors, lineWidth: lineWidth) 192 | 193 | let circlePassDescriptor = self.renderPassDescriptor(colorTexture: currentDrawable.texture) 194 | circlePassDescriptor.colorAttachments[0].loadAction = .load 195 | render(circles: circles, commandBuffer: commandBuffer, renderPassDescriptor: circlePassDescriptor) 196 | 197 | commandBuffer.present(currentDrawable) 198 | commandBuffer.commit() 199 | } 200 | 201 | private func render(circles: [CircleVertex], commandBuffer: MTLCommandBuffer, renderPassDescriptor: MTLRenderPassDescriptor) { 202 | guard let device, 203 | !circles.isEmpty, 204 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor), 205 | let pipelineState = try? device.makeRenderPipelineState(descriptor: circlePipelineDescriptor) 206 | else { return } 207 | 208 | var circles = circles 209 | let paramBuffer = device.makeBuffer(bytes: &circles, length: MemoryLayout.size * circles.count) 210 | 211 | renderEncoder.setRenderPipelineState(pipelineState) 212 | renderEncoder.setVertexBuffer(paramBuffer, offset: 0, index: 0) 213 | 214 | for i in stride(from: 0, to: 720, by: 1) { 215 | renderEncoder.drawPrimitives(type: .triangle, vertexStart: i, vertexCount: 3, instanceCount: circles.count) 216 | } 217 | 218 | renderEncoder.endEncoding() 219 | } 220 | 221 | 222 | /// This method uses exactly the same algorithm that is inside the shader to determine proper circle position in canvas 223 | private func getClosingCircleCenter(animationPercent: Float, vectors: [BezierParameters]) -> simd_float2 { 224 | if animationPercent == 0 { 225 | return vectors[0].a 226 | } 227 | let chunkSize = 1 / Float(vectors.count) 228 | let vectorNumber = Int(ceil(animationPercent / chunkSize)) 229 | 230 | let vector = vectors[vectorNumber - 1] 231 | let delta = animationPercent - Float(vectorNumber - 1) * chunkSize 232 | let t = delta / chunkSize 233 | let nt = 1 - t 234 | 235 | return pow(nt, 3) * vector.a + 3 * pow(nt, 2) * t * vector.p1 + 3 * nt * pow(t, 2) * vector.p2 + pow(t, 3) * vector.b 236 | } 237 | 238 | /// This method uses kinda the same algorithm that is inside the shader to determine proper circle color. 239 | /// It is a bit easier because we use `animationPercent` to determine color and don't need additional calculations 240 | private func getClosingCircleColor(animationPercent: Float, gradientColors: [simd_float4]) -> simd_float4 { 241 | guard animationPercent != 1 else { 242 | return gradientColors.last! 243 | } 244 | 245 | let colorT = animationPercent 246 | 247 | let stopLength = Float(gradientColors.count - 1) 248 | var colorValueRatio = colorT / (1 / stopLength) 249 | let bucket = Int(colorValueRatio) 250 | 251 | let startColor = gradientColors[bucket] 252 | let endColor = gradientColors[bucket + 1] 253 | 254 | colorValueRatio = colorValueRatio - floor(colorValueRatio) 255 | return startColor + colorValueRatio * (endColor - startColor) 256 | } 257 | 258 | private func render(bezierCurve: [BezierParameters], commandBuffer: MTLCommandBuffer, renderPassDescriptor: MTLRenderPassDescriptor, colors: [simd_float4], lineWidth: Float) { 259 | guard let device, 260 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor), 261 | let pipelineState = try? device.makeRenderPipelineState(descriptor: bezierPipelineDescriptor), 262 | let indicesBuffer 263 | else { return } 264 | 265 | var bezierCurve = bezierCurve 266 | let paramBuffer = device.makeBuffer(bytes: &bezierCurve, 267 | length: MemoryLayout.size * bezierCurve.count, 268 | options: .storageModeShared)! 269 | 270 | var gradientColors: [simd_float4] = colors 271 | var globalParams = GlobalParameters(lineWidth: lineWidth, 272 | elementsPerInstance: trianglesPerBezierCurve, 273 | gradientStepsCount: UInt32(gradientColors.count), 274 | vectorsCount: UInt32(bezierCurve.count), 275 | filledPercent: animationPercent) 276 | 277 | let globalParamBuffer = device.makeBuffer(bytes: &globalParams, 278 | length: MemoryLayout.size, 279 | options: .storageModeShared) 280 | let gradientColorsBuffer = device.makeBuffer(bytes: &gradientColors, 281 | length: MemoryLayout.size * gradientColors.count, 282 | options: .storageModeShared) 283 | 284 | renderEncoder.setRenderPipelineState(pipelineState) 285 | 286 | renderEncoder.setVertexBuffer(paramBuffer, offset: 0, index: 0) 287 | renderEncoder.setVertexBuffer(globalParamBuffer, offset: 0, index: 1) 288 | renderEncoder.setVertexBuffer(gradientColorsBuffer, offset: 0, index: 2) 289 | 290 | let totalAmountOfIndices = bezierCurve.count * indices.count 291 | 292 | for i in stride(from: 0, to: bezierCurve.count, by: 1) { 293 | let indicesCount = calculateIndicesCountToDraw(totalAmountOfIndices: totalAmountOfIndices, 294 | animationPercent: animationPercent, 295 | vertexIndex: i, 296 | totalVerticesCount: bezierCurve.count) 297 | 298 | if indicesCount != 0 { 299 | renderEncoder.drawIndexedPrimitives(type: .triangle, 300 | indexCount: indicesCount, 301 | indexType: .uint16, 302 | indexBuffer: indicesBuffer, 303 | indexBufferOffset: 0, 304 | instanceCount: 1, 305 | baseVertex: 0, 306 | baseInstance: i) 307 | } 308 | } 309 | 310 | 311 | renderEncoder.endEncoding() 312 | } 313 | 314 | private func calculateIndicesCountToDraw(totalAmountOfIndices: Int, animationPercent: Float, vertexIndex: Int, totalVerticesCount: Int) -> Int { 315 | let totalAmountOfIndices = Float(totalAmountOfIndices) 316 | let totalVerticesCount = Float(totalVerticesCount) 317 | 318 | let totalDrawnVerticesOnThatStep = (vertexIndex + 1) * indices.count 319 | 320 | let currentPercentage = Float(totalDrawnVerticesOnThatStep) / totalAmountOfIndices 321 | guard currentPercentage > animationPercent else { 322 | return indices.count 323 | } 324 | 325 | let percentDiff = currentPercentage - animationPercent 326 | if percentDiff > 1 / totalVerticesCount { 327 | return 0 328 | } else { 329 | // In theory we will always have value <= indices.count, but it is worth to check and play safe 330 | return min(indices.count, Int(ceil((1 / totalVerticesCount - percentDiff) * totalAmountOfIndices))) 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/NativeColor.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | import SwiftUI 24 | import simd 25 | 26 | #if canImport(UIKit) 27 | import UIKit 28 | public typealias NativeColor = UIColor 29 | #elseif canImport(AppKit) 30 | import AppKit 31 | public typealias NativeColor = NSColor 32 | #endif 33 | 34 | public struct BezierColor { 35 | let value: simd_float4 36 | 37 | public init(_ value: simd_float4) { 38 | self.value = value 39 | } 40 | 41 | public init(_ r: Float, _ g: Float, _ b: Float, _ a: Float) { 42 | self.init(.init(r, g, b, a)) 43 | } 44 | 45 | public init(_ nativeColor: NativeColor) { 46 | let (r, g, b, a) = nativeColor.components 47 | self.init(.init(r, g, b, a)) 48 | } 49 | 50 | @available(iOS 14.0, macOS 10.16, *) 51 | public init(_ swiftUIColor: Color) { 52 | self.init(NativeColor(swiftUIColor)) 53 | } 54 | } 55 | 56 | extension NativeColor { 57 | var components: (red: Float, green: Float, blue: Float, opacity: Float) { 58 | var r: CGFloat = 0 59 | var g: CGFloat = 0 60 | var b: CGFloat = 0 61 | var o: CGFloat = 0 62 | 63 | 64 | self.getRed(&r, green: &g, blue: &b, alpha: &o) 65 | 66 | return (Float(r), Float(g), Float(b), Float(o)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/Samples/Hello.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | import CoreGraphics 24 | import simd 25 | 26 | public var helloColors: [BezierColor] = [ 27 | .init(21/255, 123/255, 147/255, 1), 28 | .init(253/255, 211/255, 93/255, 1), 29 | .init(244/255, 83/255, 67/255, 1), 30 | .init(149/255, 117/255, 179/255, 1), 31 | .init(108/255, 153/255, 223/255, 1), 32 | .init(127/255, 182/255, 221/255, 1), 33 | ] 34 | 35 | public var helloBezierPath: BezierPath = { 36 | var shape = BezierPath(viewBox: .init(width: 1032, height: 322), lineWidth: 120) 37 | shape.move(to: CGPoint(x: 1, y: 303.38)) 38 | shape.addCurve(to: CGPoint(x: 109.8, y: 227.73), controlPoint1: CGPoint(x: 28.31, y: 287.4), controlPoint2: CGPoint(x: 88.3, y: 249.9)) 39 | 40 | shape.move(to: CGPoint(x: 146.8, y: 183.76)) 41 | shape.addCurve(to: CGPoint(x: 210.23, y: 48.3), controlPoint1: CGPoint(x: 164.86, y: 164.55), controlPoint2: CGPoint(x: 202.46, y: 117.58)) 42 | shape.addCurve(to: CGPoint(x: 142.83, y: 32.47), controlPoint1: CGPoint(x: 216.39, y: -6.67), controlPoint2: CGPoint(x: 160.85, y: -16.06)) 43 | shape.addCurve(to: CGPoint(x: 112.88, y: 253.68), controlPoint1: CGPoint(x: 128.3, y: 71.61), controlPoint2: CGPoint(x: 121.25, y: 194.9)) 44 | 45 | 46 | shape.move(to: CGPoint(x: 99.23, y: 319.65)) 47 | shape.addCurve(to: CGPoint(x: 142.39, y: 188.59), controlPoint1: CGPoint(x: 104.37, y: 285.49), controlPoint2: CGPoint(x: 118.61, y: 216.74)) 48 | shape.addCurve(to: CGPoint(x: 225.64, y: 201.35), controlPoint1: CGPoint(x: 168.53, y: 157.66), controlPoint2: CGPoint(x: 219.46, y: 150.09)) 49 | shape.addCurve(to: CGPoint(x: 229.61, y: 310.85), controlPoint1: CGPoint(x: 230.05, y: 237.85), controlPoint2: CGPoint(x: 205.38, y: 292.38)) 50 | shape.addCurve(to: CGPoint(x: 340.17, y: 296.34), controlPoint1: CGPoint(x: 253.83, y: 329.32), controlPoint2: CGPoint(x: 314.62, y: 313.49)) 51 | 52 | 53 | shape.addCurve(to: CGPoint(x: 415.49, y: 214.74), controlPoint1: CGPoint(x: 369.24, y: 280.83), controlPoint2: CGPoint(x: 404.09, y: 254.76)) 54 | shape.normalizeLastConnection() 55 | shape.addCurve(to: CGPoint(x: 340.17, y: 178.48), controlPoint1: CGPoint(x: 430.19, y: 163.12), controlPoint2: CGPoint(x: 373.15, y: 140.67)) 56 | shape.normalizeLastConnection() 57 | shape.addCurve(to: CGPoint(x: 345.45, y: 293.74), controlPoint1: CGPoint(x: 315.06, y: 207.26), controlPoint2: CGPoint(x: 310.21, y: 267.15)) 58 | 59 | shape.addCurve(to: CGPoint(x: 508.43, y: 270.39), controlPoint1: CGPoint(x: 359.55, y: 316.13), controlPoint2: CGPoint(x: 438.66, y: 343.57)) 60 | shape.normalizeLastConnection() 61 | 62 | 63 | shape.addCurve(to: CGPoint(x: 596.96, y: 124.82), controlPoint1: CGPoint(x: 540.44, y: 232.43), controlPoint2: CGPoint(x: 582.87, y: 175.14)) 64 | shape.addCurve(to: CGPoint(x: 594.32, y: 6.08), controlPoint1: CGPoint(x: 614.58, y: 61.94), controlPoint2: CGPoint(x: 618.11, y: 17.52)) 65 | shape.normalizeLastConnection() 66 | shape.addCurve(to: CGPoint(x: 520.76, y: 101.27), controlPoint1: CGPoint(x: 567.89, y: -6.62), controlPoint2: CGPoint(x: 535.3, y: 28.71)) 67 | shape.addCurve(to: CGPoint(x: 508.43, y: 218.06), controlPoint1: CGPoint(x: 510.52, y: 152.39), controlPoint2: CGPoint(x: 508.43, y: 179.36)) 68 | 69 | 70 | shape.addCurve(to: CGPoint(x: 573.62, y: 320.97), controlPoint1: CGPoint(x: 507.69, y: 252.36), controlPoint2: CGPoint(x: 517.24, y: 319.56)) 71 | shape.addCurve(to: CGPoint(x: 705.76, y: 231.25), controlPoint1: CGPoint(x: 639.25, y: 322.61), controlPoint2: CGPoint(x: 685.35, y: 261.6)) 72 | shape.normalizeLastConnection() 73 | 74 | shape.addCurve(to: CGPoint(x: 767.43, y: 91.4), controlPoint1: CGPoint(x: 724.7, y: 201.79), controlPoint2: CGPoint(x: 754.8, y: 141.69)) 75 | shape.addCurve(to: CGPoint(x: 754.21, y: 6.52), controlPoint1: CGPoint(x: 781.96, y: 33.54), controlPoint2: CGPoint(x: 774.22, y: 13.38)) 76 | shape.normalizeLastConnection() 77 | shape.addCurve(to: CGPoint(x: 687.26, y: 91.4), controlPoint1: CGPoint(x: 729.11, y: -2.08), controlPoint2: CGPoint(x: 700.92, y: 32.03)) 78 | shape.addCurve(to: CGPoint(x: 677.57, y: 262.92), controlPoint1: CGPoint(x: 674.8, y: 145.6), controlPoint2: CGPoint(x: 661.72, y: 217.18)) 79 | 80 | 81 | shape.addCurve(to: CGPoint(x: 739.24, y: 320.53), controlPoint1: CGPoint(x: 674.93, y: 273.67), controlPoint2: CGPoint(x: 703.5, y: 319.05)) 82 | shape.normalizeLastConnection() 83 | shape.addCurve(to: CGPoint(x: 841.43, y: 220.26), controlPoint1: CGPoint(x: 786.37, y: 322.48), controlPoint2: CGPoint(x: 819.85, y: 286.67)) 84 | shape.normalizeLastConnection() 85 | 86 | 87 | shape.addCurve(to: CGPoint(x: 905.3, y: 160.01), controlPoint1: CGPoint(x: 848.04, y: 196.95), controlPoint2: CGPoint(x: 867.95, y: 160.01)) 88 | shape.addCurve(to: CGPoint(x: 967.4, y: 235.65), controlPoint1: CGPoint(x: 951.81, y: 317.63), controlPoint2: CGPoint(x: 966.82, y: 208.5)) 89 | shape.normalizeLastConnection() 90 | 91 | shape.addCurve(to: CGPoint(x: 896.49, y: 320.09), controlPoint1: CGPoint(x: 830.86, y: 267.31), controlPoint2: CGPoint(x: 951.81, y: 317.63)) 92 | shape.normalizeLastConnection() 93 | shape.addCurve(to: CGPoint(x: 841.43, y: 220.26), controlPoint1: CGPoint(x: 786.37, y: 322.48), controlPoint2: CGPoint(x: 829, y: 266)) 94 | shape.normalizeLastConnection() 95 | 96 | shape.addCurve(to: CGPoint(x: 905.3, y: 160.01), controlPoint1: CGPoint(x: 848.04, y: 196.95), controlPoint2: CGPoint(x: 867.95, y: 160.01)) 97 | shape.addCurve(to: CGPoint(x: 981.94, y: 181.56), controlPoint1: CGPoint(x: 951.99, y: 160.01), controlPoint2: CGPoint(x: 948.46, y: 182.44)) 98 | shape.addCurve(to: CGPoint(x: 1037, y: 153.41), controlPoint1: CGPoint(x: 1008.72, y: 180.85), controlPoint2: CGPoint(x: 1029.81, y: 162.5)) 99 | 100 | return shape 101 | }() 102 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/Shaders/bezier_shaders.metal: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | 24 | #include 25 | #include 26 | 27 | #import "../../_BezierCHeaders/include/RendererTypes.h" 28 | 29 | using namespace metal; 30 | 31 | 32 | struct VertexOut { 33 | float4 pos[[position]]; 34 | float4 color; 35 | }; 36 | 37 | vertex VertexOut bezier_vertex(constant BezierParameters *allParams[[buffer(0)]], 38 | constant GlobalParameters& globalParams[[buffer(1)]], 39 | constant float4 *gradientColors[[buffer(2)]], 40 | uint vertexId[[vertex_id]], 41 | uint instanceId[[instance_id]]) 42 | { 43 | // Calculating what percent of BezierParameters we are currently drawing 44 | float t = (float) vertexId / globalParams.elementsPerInstance; 45 | 46 | 47 | // Calculating what percent of whole BezierPath we are currently drawing 48 | float numberOfItems = globalParams.vectorsCount; 49 | float tItemStep = 1 / numberOfItems; 50 | float globalT = tItemStep * (float(instanceId) + t); 51 | 52 | 53 | // Then we calculating color in current point 54 | float stopLength = globalParams.gradientStepsCount - 1; 55 | float colorValueRatio = globalT / (1 / stopLength); 56 | uint bucket = colorValueRatio; 57 | 58 | float4 startColor = gradientColors[bucket]; 59 | float4 endColor = gradientColors[bucket + 1]; 60 | 61 | colorValueRatio = colorValueRatio - float(int(colorValueRatio)); 62 | float4 resultColor = startColor + colorValueRatio * (endColor - startColor); 63 | 64 | 65 | // And lastly we calculating triangle position on canvas 66 | BezierParameters params = allParams[instanceId]; 67 | 68 | float2 a = params.a; 69 | float2 b = params.b; 70 | 71 | // We premultiply several values though I doubt it actually does anything performance-wise: 72 | float2 p1 = params.p1 * 3.0; 73 | float2 p2 = params.p2 * 3.0; 74 | 75 | float nt = 1.0f - t; 76 | 77 | float nt_2 = nt * nt; 78 | float nt_3 = nt_2 * nt; 79 | 80 | float t_2 = t * t; 81 | float t_3 = t_2 * t; 82 | 83 | // Calculate a single point in this Bezier curve: 84 | float2 point = a * nt_3 + p1 * nt_2 * t + p2 * nt * t_2 + b * t_3; 85 | 86 | // Calculate the tangent so we can produce a triangle (to achieve a line width greater than 1): 87 | float2 tangent = -3 * a * nt_2 + p1 * (1 - 4 * t + 3 * t_2) + p2 * (2 * t - 3 * t_2) + 3 * b * t_2; 88 | 89 | tangent = normalize(float2(-tangent.y, tangent.x)); 90 | 91 | VertexOut vo; 92 | 93 | 94 | // This is a little trick to avoid conditional code. We need to determine which side of the 95 | // triangle we are processing, so as to calculate the correct "side" of the curve, so we just 96 | // check for odd vs. even vertexId values to determine that: 97 | float branch = vertexId % 2; 98 | float lineWidth = (1 - (branch * 2.0)) * globalParams.lineWidth; 99 | 100 | // Combine the point with the tangent and lineWidth to achieve a properly oriented 101 | // triangle for this point in the curve: 102 | vo.pos.xy = point + (tangent * lineWidth / 2); 103 | vo.pos.zw = float2(0, 1); 104 | vo.color = resultColor; 105 | 106 | return vo; 107 | } 108 | 109 | fragment half4 bezier_fragment(VertexOut params[[stage_in]]) 110 | { 111 | return half4(params.color); 112 | } 113 | 114 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/Shaders/circle_shader.metal: -------------------------------------------------------------------------------- 1 | // 2 | // circle_shader.metal 3 | // ios_metal_bezier_renderer 4 | // 5 | // Created by Nikita Patskov on 05/11/2022. 6 | // Copyright © 2022 Eldad Eilam. All rights reserved. 7 | // 8 | 9 | #include 10 | #include 11 | 12 | #import "../../_BezierCHeaders/include/RendererTypes.h" 13 | 14 | using namespace metal; 15 | 16 | struct VertexOut { 17 | vector_float4 position [[position]]; 18 | vector_float4 color; 19 | }; 20 | 21 | #define M_PI 3.1415926535897932384626433832795 22 | 23 | float rads(float i){ 24 | return (M_PI * i) / 180; 25 | } 26 | 27 | vertex VertexOut circle_vertex(constant CircleVertex *circles[[buffer(0)]], 28 | uint vertexId[[vertex_id]], 29 | uint instanceId[[instance_id]]) { 30 | 31 | CircleVertex circleVertex = circles[instanceId]; 32 | VertexOut output; 33 | float i = vertexId; 34 | 35 | float branch = (vertexId + 1) % 2; 36 | 37 | output.position.x = circleVertex.center.x + branch * cos(rads(i/2)) * circleVertex.radius; 38 | output.position.y = circleVertex.center.y + branch * sin(rads(i/2)) * circleVertex.radius; 39 | 40 | output.position.zw = float2(0, 1); 41 | 42 | output.color = circleVertex.color; 43 | 44 | return output; 45 | } 46 | 47 | fragment float4 circle_fragment(VertexOut interpolated [[stage_in]]){ 48 | return interpolated.color; 49 | } 50 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/SwiftUI/AnimatableBezierView.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | import SwiftUI 24 | 25 | public struct AnimatableBezierView: View { 26 | 27 | private let path: BezierPath 28 | private let colors: [BezierColor] 29 | private let animationPercent: Float 30 | 31 | public init(path: BezierPath, colors: [BezierColor], animationPercent: Float = 1) { 32 | self.path = path 33 | self.colors = colors 34 | self.animationPercent = animationPercent 35 | } 36 | 37 | public var body: some View { 38 | Color.clear 39 | .modifier(AnimatableMetalViewModifier(animationPercent: animationPercent, path: path, colors: colors)) 40 | } 41 | } 42 | 43 | // AnimatableModifier is deprecated. But combination of Modifier+Animatable doesn't work for modifiers outside of module. 44 | // So we have to stick to deprecated protocol to make animation work for library users. 45 | public struct AnimatableMetalViewModifier: AnimatableModifier { 46 | 47 | private var animationPercent: Float 48 | private let path: BezierPath 49 | private let colors: [BezierColor] 50 | 51 | public init(animationPercent: Float, path: BezierPath, colors: [BezierColor]) { 52 | self.animationPercent = animationPercent 53 | self.path = path 54 | self.colors = colors 55 | } 56 | 57 | public var animatableData: Float { 58 | get { animationPercent } 59 | set { animationPercent = newValue } 60 | } 61 | 62 | public func body(content: Content) -> some View { 63 | MetalViewMapper(animationPercent: animationPercent, path: path, colors: colors) 64 | } 65 | } 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sources/AnimatableBezierPath/SwiftUI/MetalViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | import SwiftUI 24 | import simd 25 | 26 | #if canImport(UIKit) 27 | 28 | import UIKit 29 | 30 | public struct MetalViewMapper: UIViewRepresentable { 31 | private let animationPercent: Float 32 | private let path: BezierPath 33 | private let colors: [simd_float4] 34 | 35 | public init(animationPercent: Float, path: BezierPath, colors: [BezierColor]) { 36 | self.animationPercent = animationPercent 37 | self.path = path 38 | self.colors = colors.map(\.value) 39 | } 40 | 41 | public func makeUIView(context: Context) -> MetalBezierView { 42 | let view = MetalBezierView(params: helloBezierPath, colors: colors) 43 | view.animationPercent = animationPercent 44 | return view 45 | } 46 | 47 | public func updateUIView(_ view: MetalBezierView, context: Context) { 48 | view.animationPercent = animationPercent 49 | view.params = path 50 | view.colors = colors 51 | } 52 | } 53 | 54 | #elseif canImport(AppKit) 55 | 56 | import AppKit 57 | 58 | public struct MetalViewMapper: NSViewRepresentable { 59 | private let animationPercent: Float 60 | private let path: BezierPath 61 | private let colors: [simd_float4] 62 | 63 | public init(animationPercent: Float, path: BezierPath, colors: [BezierColor]) { 64 | self.animationPercent = animationPercent 65 | self.path = path 66 | self.colors = colors.map(\.value) 67 | } 68 | 69 | public func makeNSView(context: Context) -> MetalBezierView { 70 | let view = MetalBezierView(params: helloBezierPath, colors: colors) 71 | view.animationPercent = animationPercent 72 | return view 73 | } 74 | 75 | public func updateNSView(_ view: MetalBezierView, context: Context) { 76 | view.animationPercent = animationPercent 77 | view.params = path 78 | view.colors = colors 79 | } 80 | } 81 | 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/_BezierCHeaders/PleasingSwiftPM.c: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /Sources/_BezierCHeaders/include/RendererTypes.h: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #ifndef RendererTypes_h 24 | #define RendererTypes_h 25 | 26 | 27 | #include 28 | 29 | struct GlobalParameters 30 | { 31 | float lineWidth; 32 | uint elementsPerInstance; 33 | uint gradientStepsCount; 34 | uint vectorsCount; 35 | float filledPercent; 36 | }; 37 | 38 | 39 | 40 | // BezierParameters represent a per-curve buffer specifying curve parameters. Note that 41 | // even though the vertex shader is obviously called per-vertex, it actually uses the same 42 | // BezierParameters instance (identified through the instance_id) for all vertexes in a given 43 | // curve. 44 | struct BezierParameters 45 | { 46 | simd_float2 a; 47 | simd_float2 b; 48 | simd_float2 p1; 49 | simd_float2 p2; 50 | }; 51 | 52 | struct CircleVertex 53 | { 54 | simd_float2 center; 55 | simd_float4 color; 56 | float radius; 57 | }; 58 | 59 | #endif /* RendererTypes_h */ 60 | --------------------------------------------------------------------------------