├── Sources ├── Resources │ └── gradient.jpg ├── Meshin │ ├── Meshin │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── MeshinApp.swift │ │ ├── Meshin.entitlements │ │ └── GradientSamplesView.swift │ └── Meshin.xcodeproj │ │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── swiftpm │ │ │ └── Package.resolved │ │ └── project.pbxproj └── MeshingKit │ ├── ParameterizedNoise.metal │ ├── ParameterizedNoiseView.swift │ ├── Color+Hex.swift │ ├── AnimationPattern.swift │ ├── GradientTemplate.swift │ ├── GradientExport.swift │ ├── MeshingKit.swift │ ├── PredefinedTemplate.swift │ ├── GradientTemplateSize2.swift │ ├── AnimatedMeshGradientView.swift │ ├── GradientTemplateSize4.swift │ └── GradientTemplateSize3.swift ├── scripts ├── setup-hooks.sh └── hooks │ └── pre-commit ├── .spi.yml ├── Package.swift ├── LICENSE ├── .swiftlint.yml ├── .gitignore ├── Tests └── MeshingKitTests │ ├── ExportTests.swift │ ├── PredefinedTemplateTests.swift │ └── MeshingKitTests.swift ├── CLAUDE.md ├── codemagic.yaml └── README.md /Sources/Resources/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rryam/MeshingKit/HEAD/Sources/Resources/gradient.jpg -------------------------------------------------------------------------------- /Sources/Meshin/Meshin/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin/MeshinApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshinApp.swift 3 | // Meshin 4 | // 5 | // Created by Rudrank Riyam on 10/19/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MeshinApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | GradientSamplesView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin/Meshin.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "af70fb9296517684576143a169e8d49dfcc3bc7d890d287ebb63bb9f31863317", 3 | "pins" : [ 4 | { 5 | "identity" : "inject", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/krzysztofzablocki/Inject", 8 | "state" : { 9 | "revision" : "728c56639ecb3df441d51d5bc6747329afabcfc9", 10 | "version" : "1.5.2" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /scripts/setup-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup script to configure Git hooks for MeshingKit 4 | # Run this once after cloning the repository 5 | 6 | echo "🔧 Setting up Git hooks for MeshingKit..." 7 | 8 | # Configure Git to use the tracked hooks directory 9 | git config core.hooksPath scripts/hooks 10 | 11 | if [ $? -eq 0 ]; then 12 | echo "✅ Git hooks configured successfully!" 13 | echo "" 14 | echo "The pre-commit hook will now run automatically on every commit." 15 | echo "It will check your Swift code with SwiftLint before allowing commits." 16 | else 17 | echo "❌ Failed to configure Git hooks" 18 | exit 1 19 | fi 20 | 21 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | metadata: 3 | authors: "Rudrank Riyam" 4 | builder: 5 | configs: 6 | - documentation_targets: ["MeshingKit"] 7 | - custom_build_commands: | 8 | #!/bin/zsh 9 | 10 | declare -a DESTINATIONS=( 11 | "platform=iOS Simulator,name=iPhone 17 Pro" 12 | "platform=macOS" 13 | ) 14 | 15 | for DESTINATION in "${DESTINATIONS[@]}" 16 | do 17 | echo "Building for destination: $DESTINATION" 18 | xcodebuild clean build \ 19 | -scheme MeshingKit \ 20 | -destination "$DESTINATION" \ 21 | -skipPackagePluginValidation \ 22 | -quiet 23 | done -------------------------------------------------------------------------------- /Sources/MeshingKit/ParameterizedNoise.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Shader.metal 3 | // MeshingShared 4 | // 5 | // Created by Rudrank Riyam on 8/9/24. 6 | // 7 | 8 | #include 9 | #include 10 | using namespace metal; 11 | 12 | [[ stitchable ]] 13 | half4 parameterizedNoise(float2 position, half4 color, float intensity, float frequency, float opacity) { 14 | float value = fract(cos(dot(position * frequency, float2(12.9898, 78.233))) * 43758.5453); 15 | 16 | float r = color.r * mix(1.0, value, intensity); 17 | float g = color.g * mix(1.0, value, intensity); 18 | float b = color.b * mix(1.0, value, intensity); 19 | 20 | return half4(r, g, b, color.a * opacity); 21 | } 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 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: "MeshingKit", 8 | platforms: [ 9 | .iOS(.v18), 10 | .macOS(.v15), 11 | .macCatalyst(.v18), 12 | .tvOS(.v18), 13 | .watchOS(.v11), 14 | .visionOS(.v2) 15 | ], 16 | products: [ 17 | .library( 18 | name: "MeshingKit", 19 | type: .static, 20 | targets: ["MeshingKit"]) 21 | ], 22 | targets: [ 23 | .target( 24 | name: "MeshingKit", 25 | resources: [ 26 | .process("ParameterizedNoise.metal") 27 | ] 28 | ), 29 | .testTarget( 30 | name: "MeshingKitTests", 31 | dependencies: ["MeshingKit"] 32 | ) 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rudrank Riyam 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 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # SwiftLint Configuration File 2 | 3 | # Disabled rules 4 | disabled_rules: 5 | - duplicate_imports 6 | - non_optional_string_data_conversion 7 | - todo 8 | - void_return 9 | 10 | # Enabled rules with custom configuration 11 | line_length: 12 | warning: 120 13 | error: 150 14 | 15 | file_length: 16 | warning: 400 17 | error: 600 18 | 19 | type_body_length: 20 | warning: 300 21 | error: 400 22 | 23 | identifier_name: 24 | min_length: 25 | warning: 3 26 | excluded: 27 | - a 28 | - r 29 | - g 30 | - b 31 | - x 32 | - y 33 | - id 34 | 35 | # Custom rules for trailing commas 36 | trailing_comma: 37 | mandatory_comma: false 38 | 39 | # Opening brace spacing 40 | opening_brace: 41 | ignore_multiline_function_signatures: true 42 | 43 | # Switch case alignment 44 | switch_case_alignment: 45 | indented_cases: false 46 | 47 | # Excluded files and directories 48 | excluded: 49 | - .build/ 50 | - .swiftpm/ 51 | - DerivedSources/ 52 | - "*.generated.swift" 53 | # Exclude template files - they contain many predefined templates 54 | - Sources/MeshingKit/GradientTemplateSize*.swift 55 | -------------------------------------------------------------------------------- /scripts/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Pre-commit hook to run SwiftLint on staged Swift files 4 | 5 | # Colors for output 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | NC='\033[0m' # No Color 10 | 11 | echo -e "${GREEN}Running SwiftLint pre-commit hook...${NC}" 12 | 13 | # Check if SwiftLint is installed 14 | if ! command -v swiftlint &> /dev/null; then 15 | echo -e "${YELLOW}Warning: SwiftLint is not installed.${NC}" 16 | echo "Install it with: brew install swiftlint" 17 | echo "Skipping SwiftLint check..." 18 | exit 0 19 | fi 20 | 21 | # Get list of staged Swift files 22 | STAGED_SWIFT_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.swift$') 23 | 24 | # If no Swift files are staged, exit successfully 25 | if [ -z "$STAGED_SWIFT_FILES" ]; then 26 | echo -e "${GREEN}No Swift files staged. Skipping SwiftLint check.${NC}" 27 | exit 0 28 | fi 29 | 30 | echo "Checking staged Swift files:" 31 | echo "$STAGED_SWIFT_FILES" | sed 's/^/ - /' 32 | 33 | # Run SwiftLint on staged files 34 | if echo "$STAGED_SWIFT_FILES" | xargs swiftlint lint; then 35 | echo -e "${GREEN}✓ SwiftLint passed!${NC}" 36 | exit 0 37 | else 38 | echo -e "${RED}✗ SwiftLint found issues. Please fix them before committing.${NC}" 39 | echo "" 40 | echo "You can run SwiftLint manually with:" 41 | echo " swiftlint lint" 42 | echo "" 43 | echo "Or to auto-fix some issues:" 44 | echo " swiftlint --fix" 45 | exit 1 46 | fi 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "scale" : "1x", 33 | "size" : "16x16" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "scale" : "2x", 38 | "size" : "16x16" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "32x32" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "32x32" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "128x128" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "128x128" 59 | }, 60 | { 61 | "idiom" : "mac", 62 | "scale" : "1x", 63 | "size" : "256x256" 64 | }, 65 | { 66 | "idiom" : "mac", 67 | "scale" : "2x", 68 | "size" : "256x256" 69 | }, 70 | { 71 | "idiom" : "mac", 72 | "scale" : "1x", 73 | "size" : "512x512" 74 | }, 75 | { 76 | "idiom" : "mac", 77 | "scale" : "2x", 78 | "size" : "512x512" 79 | } 80 | ], 81 | "info" : { 82 | "author" : "xcode", 83 | "version" : 1 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/MeshingKitTests/ExportTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import MeshingKit 3 | import SwiftUI 4 | 5 | @Suite("Export Tests") 6 | struct ExportTests { 7 | 8 | @Test("Export helpers generate snippets") 9 | func exportHelpersSnippets() { 10 | let template = GradientTemplateSize2.mysticTwilight 11 | let swiftUIStops = MeshingKit.swiftUIStopsSnippet(template: template) 12 | let swiftUIStopsWithAlpha = MeshingKit.swiftUIStopsSnippet( 13 | template: template, 14 | includeAlpha: true 15 | ) 16 | let cssStops = MeshingKit.cssLinearGradientSnippet(template: template) 17 | let cssStopsWithAlpha = MeshingKit.cssLinearGradientSnippet( 18 | template: template, 19 | includeAlpha: true 20 | ) 21 | 22 | #expect(swiftUIStops.contains("Color(hex:")) 23 | #expect(swiftUIStopsWithAlpha.contains("Color(hex: \"#FF")) 24 | #expect(cssStops.contains("linear-gradient(")) 25 | #expect(cssStops.contains("#")) 26 | #expect(cssStopsWithAlpha.contains("rgba(")) 27 | } 28 | 29 | @Test("Export helpers generate stops") 30 | func exportHelpersStops() { 31 | let template = GradientTemplateSize2.mysticTwilight 32 | let stops = MeshingKit.previewStops(template: template) 33 | 34 | #expect(stops.count == template.colors.count) 35 | #expect(isApproximatelyEqual(Double(stops.first?.location ?? 0), 0)) 36 | #expect(isApproximatelyEqual(Double(stops.last?.location ?? 0), 1)) 37 | } 38 | 39 | @Test("Snapshot helpers generate CGImage") 40 | func snapshotHelpersCGImage() async { 41 | let image = await MeshingKit.snapshotCGImage( 42 | template: .size2(.mysticTwilight), 43 | size: CGSize(width: 100, height: 100) 44 | ) 45 | 46 | #expect(image != nil) 47 | } 48 | } 49 | 50 | private func isApproximatelyEqual(_ lhs: Double, _ rhs: Double, tolerance: Double = 0.0001) 51 | -> Bool 52 | { 53 | abs(lhs - rhs) <= tolerance 54 | } 55 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | MeshingKit is a Swift library for creating mesh gradients in SwiftUI. It provides 68 predefined gradient templates (2x2, 3x3, 4x4 grids), animated gradients, and Metal shader-based noise effects. 8 | 9 | ## Build Commands 10 | 11 | ```bash 12 | # Swift Package Manager build 13 | swift build 14 | 15 | # Build for a specific target 16 | swift build --target MeshingKit 17 | 18 | # Xcode build for iOS Simulator 19 | xcodebuild build -project Sources/Meshin/Meshin.xcodeproj -scheme Meshin -destination "generic/platform=iOS Simulator" 20 | ``` 21 | 22 | ## Test Commands 23 | 24 | ```bash 25 | swift test --verbose 26 | swift test --enable-code-coverage 27 | ``` 28 | 29 | ## Lint 30 | 31 | ```bash 32 | swiftlint --strict 33 | ``` 34 | 35 | A pre-commit hook runs SwiftLint on staged files. Setup with `scripts/setup-hooks.sh`. 36 | 37 | ## Architecture 38 | 39 | **Main API:** `MeshingKit.swift` exposes static `gradient()` and `animatedGradient()` methods. 40 | 41 | **Template System:** 42 | - `GradientTemplate` protocol defines the mesh gradient structure 43 | - `PredefinedTemplate` enum wraps all 68 templates with metadata and search support 44 | - Templates are organized by grid size: `GradientTemplateSize2`, `GradientTemplateSize3`, `GradientTemplateSize4` 45 | 46 | **Key Views:** 47 | - `AnimatedMeshGradientView` - SwiftUI view for animated gradients 48 | - `ParameterizedNoiseView` - Metal shader noise effect 49 | 50 | **Search:** `PredefinedTemplate.find(by: token)` uses NaturalLanguage for lemmatization and camelCase splitting. 51 | 52 | ## Important Conventions 53 | 54 | - All public types conform to `Sendable` for concurrency safety 55 | - Tests use Swift Testing framework with `#expect` macro 56 | - Template files are excluded from SwiftLint (`.swiftlint.yml`) due to size 57 | - CI runs on Codemagic (see `codemagic.yaml`) 58 | 59 | ## Source Structure 60 | 61 | ``` 62 | Sources/MeshingKit/ # Main library 63 | Sources/Meshin/ # Demo app 64 | Tests/MeshingKitTests/ # Swift Testing suite 65 | scripts/ # Git hooks and setup 66 | ``` 67 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin/GradientSamplesView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MeshingKit 3 | import Inject 4 | 5 | /// A view that displays a list of gradient templates and allows full-screen viewing. 6 | struct GradientSamplesView: View { 7 | @ObserveInjection var inject 8 | @State private var selectedTemplate: PredefinedTemplate? 9 | 10 | var body: some View { 11 | NavigationStack { 12 | List { 13 | Section(header: Text("Size 2 Templates")) { 14 | ForEach(GradientTemplateSize2.allCases, id: \.self) { template in 15 | Button(template.name) { 16 | selectedTemplate = .size2(template) 17 | } 18 | } 19 | } 20 | 21 | Section(header: Text("Size 3 Templates")) { 22 | ForEach(GradientTemplateSize3.allCases, id: \.self) { template in 23 | Button(template.name) { 24 | selectedTemplate = .size3(template) 25 | } 26 | } 27 | } 28 | 29 | Section(header: Text("Size 4 Templates")) { 30 | ForEach(GradientTemplateSize4.allCases, id: \.self) { template in 31 | Button(template.name) { 32 | selectedTemplate = .size4(template) 33 | } 34 | } 35 | } 36 | } 37 | .navigationTitle("Gradient Templates") 38 | } 39 | .sheet(item: $selectedTemplate) { template in 40 | FullScreenGradientView(template: template) 41 | } 42 | .enableInjection() 43 | } 44 | } 45 | 46 | /// A view that displays a full-screen version of a selected gradient template. 47 | struct FullScreenGradientView: View { 48 | let template: PredefinedTemplate 49 | @Environment(\.dismiss) private var dismiss 50 | @State private var showAnimation: Bool = false 51 | 52 | var body: some View { 53 | ZStack { 54 | MeshingKit.animatedGradient(template, showAnimation: $showAnimation) 55 | 56 | VStack { 57 | Spacer() 58 | 59 | Toggle("Animate", isOn: $showAnimation) 60 | .padding() 61 | .background(.ultraThinMaterial, in: .rect) 62 | 63 | Button("Close") { 64 | dismiss() 65 | } 66 | .padding(.bottom) 67 | .buttonStyle(.borderedProminent) 68 | } 69 | } 70 | .ignoresSafeArea(edges: .all) 71 | } 72 | } 73 | 74 | struct GradientSamplesView_Previews: PreviewProvider { 75 | static var previews: some View { 76 | GradientSamplesView() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /codemagic.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | triggering: 3 | push: &events 4 | events: 5 | - push 6 | - pull_request 7 | 8 | workflows: 9 | meshingkit: 10 | name: MeshingKit Workflow 11 | environment: 12 | xcode: 26.0 13 | vars: 14 | XCODE_SCHEME: "MeshingKit" 15 | APP_ID: "MeshingKit" 16 | when: 17 | changeset: 18 | includes: 19 | - 'Sources' 20 | - 'Tests' 21 | - 'Package.swift' 22 | - '*.swift' 23 | triggering: 24 | <<: *events 25 | scripts: 26 | - name: Lint Swift Code 27 | script: | 28 | #!/bin/zsh 29 | 30 | echo "Running SwiftLint..." 31 | 32 | # Install swiftlint if not available 33 | if ! command -v swiftlint &> /dev/null; then 34 | echo "Installing SwiftLint..." 35 | brew install swiftlint 36 | fi 37 | 38 | # Run swiftlint with strict mode 39 | swiftlint --strict 40 | 41 | - name: Build Swift Package 42 | script: | 43 | #!/bin/zsh 44 | 45 | echo "Building MeshingKit..." 46 | 47 | # Basic swift build for the current platform 48 | swift build --target MeshingKit 49 | 50 | - name: Test Swift Package 51 | script: | 52 | #!/bin/zsh 53 | 54 | echo "Testing MeshingKit..." 55 | 56 | # Run tests with verbose output 57 | swift test --verbose 58 | 59 | # Generate code coverage report 60 | swift test --enable-code-coverage 61 | 62 | - name: Download Metal Toolchain 63 | script: | 64 | #!/bin/zsh 65 | xcodebuild -downloadComponent MetalToolchain 66 | 67 | - name: Build with Xcode (Multi-Platform) 68 | script: | 69 | #!/bin/zsh 70 | 71 | declare -a DESTINATIONS=( 72 | "platform=iOS Simulator,name=iPhone 17 Pro" 73 | "platform=macOS" 74 | ) 75 | 76 | for DESTINATION in "${DESTINATIONS[@]}" 77 | do 78 | echo "Building for destination: $DESTINATION" 79 | xcodebuild clean build \ 80 | -project Sources/Meshin/Meshin.xcodeproj \ 81 | -scheme Meshin \ 82 | -destination "$DESTINATION" \ 83 | -configuration Debug \ 84 | -skipPackagePluginValidation \ 85 | CODE_SIGN_IDENTITY="" \ 86 | CODE_SIGNING_REQUIRED=NO \ 87 | -quiet 88 | done 89 | 90 | -------------------------------------------------------------------------------- /Sources/MeshingKit/ParameterizedNoiseView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A view that applies a parameterized noise effect to a MeshGradient. 4 | /// 5 | /// Use `ParameterizedNoiseView` to add a customizable noise effect to a view (commonly a `MeshGradient`). 6 | /// The noise effect is controlled by three parameters: intensity, frequency, and opacity. 7 | /// 8 | /// Example usage: 9 | /// ```swift 10 | /// ParameterizedNoiseView(intensity: .constant(0.5), frequency: .constant(0.2), opacity: .constant(0.9)) { 11 | /// MeshingKit.gradientSize3(template: .cosmicAurora) 12 | /// } 13 | /// ``` 14 | /// 15 | /// - Important: This view requires iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, or visionOS 2.0 and later. 16 | public struct ParameterizedNoiseView: View { 17 | 18 | /// The view to which the noise effect is applied. 19 | let content: Content 20 | 21 | /// The intensity of the noise effect. 22 | /// 23 | /// Values typically range from 0 to 1, where 0 means no effect and 1 means maximum intensity. 24 | @Binding var intensity: Float 25 | 26 | /// The frequency of the noise pattern. 27 | /// 28 | /// Higher values create a finer, more detailed noise pattern, while lower values create a 29 | /// broader, more spread-out pattern. 30 | @Binding var frequency: Float 31 | 32 | /// The opacity of the noise effect. 33 | /// 34 | /// Values range from 0 to 1, where 0 is completely transparent and 1 is fully opaque. 35 | @Binding var opacity: Float 36 | 37 | /// Creates a new `ParameterizedNoiseView` with the specified parameters and MeshGradient. 38 | /// 39 | /// - Parameters: 40 | /// - intensity: A binding to the intensity of the noise effect. 41 | /// - frequency: A binding to the frequency of the noise pattern. 42 | /// - opacity: A binding to the opacity of the noise effect. 43 | /// - content: A closure that returns the view to which the noise effect will be applied. 44 | public init( 45 | intensity: Binding, frequency: Binding, 46 | opacity: Binding, @ViewBuilder content: () -> Content 47 | ) { 48 | self._intensity = intensity 49 | self._frequency = frequency 50 | self._opacity = opacity 51 | self.content = content() 52 | } 53 | 54 | /// The body of the view, applying the noise effect to the MeshGradient. 55 | public var body: some View { 56 | let clampedIntensity = min(max(intensity, 0), 1) 57 | let clampedFrequency = max(frequency, 0) 58 | let clampedOpacity = min(max(opacity, 0), 1) 59 | 60 | content 61 | .colorEffect( 62 | ShaderLibrary.parameterizedNoise( 63 | .float(clampedIntensity), 64 | .float(clampedFrequency), 65 | .float(clampedOpacity) 66 | ) 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/MeshingKit/Color+Hex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Hex.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 10/14/24. 6 | // 7 | 8 | import SwiftUI 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | 15 | struct RGBAComponents { 16 | let r: Double 17 | let g: Double 18 | let b: Double 19 | let a: Double 20 | } 21 | 22 | extension Color { 23 | 24 | /// Initializes a `Color` instance from a hexadecimal color string. 25 | /// 26 | /// This initializer supports the following hex formats: 27 | /// - "#RGB" (12-bit) 28 | /// - "#RRGGBB" (24-bit) 29 | /// - "#AARRGGBB" (32-bit with alpha) 30 | /// 31 | /// - Parameter hex: A string representing the color in hexadecimal format. 32 | /// The "#" prefix is optional. 33 | /// 34 | /// - Note: If an invalid hex string is provided, the color will default to opaque white. 35 | init(hex: String) { 36 | let hex = hex.trimmingCharacters( 37 | in: CharacterSet.alphanumerics.inverted) 38 | var int: UInt64 = 0 39 | let scanned = Scanner(string: hex).scanHexInt64(&int) 40 | let a: UInt64 41 | let r: UInt64 42 | let g: UInt64 43 | let b: UInt64 44 | switch (scanned, hex.count) { 45 | case (true, 3): // RGB (12-bit) 46 | (a, r, g, b) = ( 47 | 255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17 48 | ) 49 | case (true, 6): // RGB (24-bit) 50 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 51 | case (true, 8): // ARGB (32-bit) 52 | (a, r, g, b) = ( 53 | int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF 54 | ) 55 | default: 56 | (a, r, g, b) = (255, 255, 255, 255) 57 | } 58 | 59 | self.init( 60 | .sRGB, 61 | red: Double(r) / 255, 62 | green: Double(g) / 255, 63 | blue: Double(b) / 255, 64 | opacity: Double(a) / 255 65 | ) 66 | } 67 | 68 | func rgbaComponents() -> RGBAComponents? { 69 | #if canImport(UIKit) 70 | let platformColor = UIColor(self) 71 | var r: CGFloat = 0 72 | var g: CGFloat = 0 73 | var b: CGFloat = 0 74 | var a: CGFloat = 0 75 | guard platformColor.getRed(&r, green: &g, blue: &b, alpha: &a) else { 76 | return nil 77 | } 78 | return RGBAComponents(r: Double(r), g: Double(g), b: Double(b), a: Double(a)) 79 | #elseif canImport(AppKit) 80 | let platformColor = NSColor(self) 81 | let srgb = platformColor.usingColorSpace(.sRGB) ?? platformColor 82 | var r: CGFloat = 0 83 | var g: CGFloat = 0 84 | var b: CGFloat = 0 85 | var a: CGFloat = 0 86 | srgb.getRed(&r, green: &g, blue: &b, alpha: &a) 87 | return RGBAComponents(r: Double(r), g: Double(g), b: Double(b), a: Double(a)) 88 | #else 89 | return nil 90 | #endif 91 | } 92 | 93 | /// Returns a hex string for the color in sRGB space. 94 | /// 95 | /// - Parameter includeAlpha: When true, returns #AARRGGBB. Otherwise returns #RRGGBB. 96 | /// - Returns: A hex string if the color can be converted to sRGB. 97 | public func hexString(includeAlpha: Bool = false) -> String? { 98 | guard let components = rgbaComponents() else { 99 | return nil 100 | } 101 | 102 | let r = Int(round(components.r * 255)) 103 | let g = Int(round(components.g * 255)) 104 | let b = Int(round(components.b * 255)) 105 | let a = Int(round(components.a * 255)) 106 | 107 | if includeAlpha { 108 | return String(format: "#%02X%02X%02X%02X", a, r, g, b) 109 | } 110 | return String(format: "#%02X%02X%02X", r, g, b) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Tests/MeshingKitTests/PredefinedTemplateTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import MeshingKit 3 | import SwiftUI 4 | 5 | @Suite("PredefinedTemplate Tests") 6 | struct PredefinedTemplateTests { 7 | 8 | @Test("PredefinedTemplate tags include name tokens") 9 | func predefinedTemplateTags() { 10 | let template = PredefinedTemplate.size3(.auroraBorealis) 11 | #expect(template.tags.contains("aurora")) 12 | #expect(template.tags.contains("borealis")) 13 | } 14 | 15 | @Test("PredefinedTemplate moods derived from name") 16 | func predefinedTemplateMoods() { 17 | let template = PredefinedTemplate.size2(.arcticFrost) 18 | #expect(template.moods.contains(.cool)) 19 | } 20 | 21 | @Test("PredefinedTemplate find by query") 22 | func predefinedTemplateFind() { 23 | let results = PredefinedTemplate.find(by: "aurora") 24 | #expect(results.contains(.size3(.auroraBorealis))) 25 | } 26 | 27 | @Test("PredefinedTemplate find is case-insensitive") 28 | func predefinedTemplateFindCaseInsensitive() { 29 | let results = PredefinedTemplate.find(by: "Aurora") 30 | #expect(results.contains(.size3(.auroraBorealis))) 31 | } 32 | 33 | @Test("PredefinedTemplate find matches moods") 34 | func predefinedTemplateFindMoods() { 35 | let results = PredefinedTemplate.find(by: "cool") 36 | #expect(results.contains(.size2(.arcticFrost))) 37 | } 38 | 39 | @Test("PredefinedTemplate find respects limit") 40 | func predefinedTemplateFindLimit() { 41 | let results = PredefinedTemplate.find(by: "aurora", limit: 1) 42 | #expect(results.count == 1) 43 | } 44 | 45 | @Test("PredefinedTemplate find returns all for empty query") 46 | func predefinedTemplateFindEmptyQuery() { 47 | let results = PredefinedTemplate.find(by: " ") 48 | #expect(results.count == PredefinedTemplate.allCases.count) 49 | } 50 | 51 | @Test("CustomGradientTemplate creates valid template") 52 | func customGradientTemplateCreation() { 53 | let points: [SIMD2] = [ 54 | .init(x: 0.0, y: 0.0), .init(x: 1.0, y: 0.0), 55 | .init(x: 0.0, y: 1.0), .init(x: 1.0, y: 1.0) 56 | ] 57 | let colors: [Color] = [.red, .green, .blue, .yellow] 58 | 59 | let template = CustomGradientTemplate( 60 | name: "Test Template", 61 | size: 2, 62 | points: points, 63 | colors: colors, 64 | background: .black 65 | ) 66 | 67 | #expect(template.name == "Test Template") 68 | #expect(template.size == 2) 69 | #expect(template.points.count == 4) 70 | #expect(template.colors.count == 4) 71 | } 72 | 73 | @Test("CustomGradientTemplate validation reports errors") 74 | func customGradientTemplateValidation() { 75 | let points: [SIMD2] = [ 76 | .init(x: -0.1, y: 0.0), 77 | .init(x: 1.2, y: 1.1) 78 | ] 79 | let colors: [Color] = [.red] 80 | 81 | let errors = CustomGradientTemplate.validate( 82 | size: 2, 83 | points: points, 84 | colors: colors 85 | ) 86 | 87 | #expect(errors.contains(.pointsCount(expected: 4, actual: 2))) 88 | #expect(errors.contains(.colorsCount(expected: 4, actual: 1))) 89 | #expect(errors.contains(where: { error in 90 | if case .pointOutOfRange(index: 0, x: _, y: _) = error { return true } 91 | return false 92 | })) 93 | } 94 | 95 | @Test("CustomGradientTemplate validating initializer throws") 96 | func customGradientTemplateValidatingInitThrows() { 97 | let points: [SIMD2] = [ 98 | .init(x: 0.0, y: 0.0) 99 | ] 100 | let colors: [Color] = [.red] 101 | 102 | do { 103 | _ = try CustomGradientTemplate( 104 | validating: "Invalid", 105 | size: 2, 106 | points: points, 107 | colors: colors, 108 | background: .black 109 | ) 110 | #expect(Bool(false), "Expected validating initializer to throw") 111 | } catch let error as CustomGradientTemplate.ValidationErrors { 112 | #expect(!error.errors.isEmpty) 113 | } catch { 114 | #expect(Bool(false), "Unexpected error type: \(error)") 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/MeshingKit/AnimationPattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationPattern.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 10/29/25. 6 | 7 | import SwiftUI 8 | 9 | /// Defines how a point's position should be animated over time. 10 | public struct PointAnimation: Sendable { 11 | /// The index of the point in the gradient's position array. 12 | public let pointIndex: Int 13 | 14 | /// The axis to animate (x, y, or both). 15 | public let axis: Axis 16 | 17 | /// The amplitude of the animation (how far the point moves). 18 | public let amplitude: CGFloat 19 | 20 | /// The frequency multiplier (controls animation speed). 21 | public let frequency: CGFloat 22 | 23 | /// Defines the axis or axes along which a point can be animated. 24 | /// 25 | /// - `x`: Animate only along the horizontal axis. 26 | /// - `y`: Animate only along the vertical axis. 27 | /// - `both`: Animate along both axes simultaneously. 28 | public enum Axis: Sendable { 29 | case x, y, both 30 | } 31 | 32 | /// Creates a new point animation with the specified parameters. 33 | /// 34 | /// - Parameters: 35 | /// - pointIndex: The index of the point in the gradient's position array. 36 | /// - axis: The axis to animate (x, y, or both). 37 | /// - amplitude: The amplitude of the animation (how far the point moves). 38 | /// - frequency: The frequency multiplier (controls animation speed). 39 | public init( 40 | pointIndex: Int, 41 | axis: Axis, 42 | amplitude: CGFloat, 43 | frequency: CGFloat = 1.0 44 | ) { 45 | self.pointIndex = pointIndex 46 | self.axis = axis 47 | self.amplitude = amplitude 48 | self.frequency = frequency 49 | } 50 | 51 | /// Applies the animation to a point based on the current phase. 52 | func apply(to point: inout SIMD2, at phase: Double) { 53 | let value = Float(cos(phase * Double(frequency))) 54 | let amplitudeFloat = Float(amplitude) 55 | 56 | switch axis { 57 | case .x: 58 | point.x += amplitudeFloat * value 59 | case .y: 60 | point.y += amplitudeFloat * value 61 | case .both: 62 | point.x += amplitudeFloat * value 63 | point.y += amplitudeFloat * Float(sin(phase * Double(frequency))) 64 | } 65 | } 66 | } 67 | 68 | /// A collection of point animations that can be applied to a mesh gradient. 69 | public struct AnimationPattern: Sendable { 70 | 71 | /// The individual point animations in this pattern. 72 | public let animations: [PointAnimation] 73 | 74 | /// Creates a new animation pattern with the specified point animations. 75 | /// 76 | /// - Parameters: 77 | /// - animations: An array of `PointAnimation` objects defining the individual point animations. 78 | public init(animations: [PointAnimation]) { 79 | self.animations = animations 80 | } 81 | 82 | /// Creates a default animation pattern for a mesh gradient of the specified size. 83 | /// 84 | /// This method provides pre-configured animation patterns optimized for common grid sizes. 85 | /// For grid sizes 3 and 4, it returns a pattern with carefully tuned animations for 86 | /// various control points. For other sizes, it returns an empty pattern. 87 | /// 88 | /// - Parameter size: The grid size of the mesh gradient (e.g., 3 for a 3x3 grid). 89 | /// - Returns: An `AnimationPattern` with default animations for the specified grid size. 90 | /// 91 | /// - Note: Currently, default patterns are only available for grid sizes 3 and 4. 92 | public static func defaultPattern(forGridSize size: Int) -> AnimationPattern { 93 | switch size { 94 | case 3: 95 | return AnimationPattern(animations: [ 96 | PointAnimation(pointIndex: 1, axis: .x, amplitude: 0.4), 97 | PointAnimation( 98 | pointIndex: 3, axis: .y, amplitude: 0.3, frequency: 1.1), 99 | PointAnimation( 100 | pointIndex: 4, axis: .y, amplitude: -0.4, frequency: 0.9), 101 | PointAnimation( 102 | pointIndex: 4, axis: .x, amplitude: 0.2, frequency: 0.7), 103 | PointAnimation( 104 | pointIndex: 5, axis: .y, amplitude: -0.2, frequency: 0.9), 105 | PointAnimation( 106 | pointIndex: 7, axis: .x, amplitude: -0.4, frequency: 1.2) 107 | ]) 108 | case 4: 109 | return AnimationPattern(animations: [ 110 | // Edge points 111 | PointAnimation( 112 | pointIndex: 1, axis: .x, amplitude: 0.1, frequency: 0.7), 113 | PointAnimation( 114 | pointIndex: 2, axis: .x, amplitude: -0.1, frequency: 0.8), 115 | PointAnimation( 116 | pointIndex: 4, axis: .y, amplitude: 0.1, frequency: 0.9), 117 | PointAnimation( 118 | pointIndex: 7, axis: .y, amplitude: -0.1, frequency: 0.6), 119 | PointAnimation( 120 | pointIndex: 11, axis: .y, amplitude: -0.1, frequency: 1.2), 121 | PointAnimation( 122 | pointIndex: 13, axis: .x, amplitude: 0.1, frequency: 1.3), 123 | PointAnimation( 124 | pointIndex: 14, axis: .x, amplitude: -0.1, frequency: 1.4), 125 | 126 | // Inner points 127 | PointAnimation( 128 | pointIndex: 5, axis: .both, amplitude: 0.15, frequency: 0.8), 129 | PointAnimation( 130 | pointIndex: 6, axis: .both, amplitude: -0.15, frequency: 1.0 131 | ), 132 | PointAnimation( 133 | pointIndex: 9, axis: .both, amplitude: 0.15, frequency: 1.2), 134 | PointAnimation( 135 | pointIndex: 10, axis: .both, amplitude: -0.15, 136 | frequency: 1.4) 137 | ]) 138 | default: 139 | return AnimationPattern(animations: []) 140 | } 141 | } 142 | 143 | /// Applies all animations in the pattern to the points. 144 | func apply(to points: [SIMD2], at phase: Double) -> [SIMD2] { 145 | var result = points 146 | 147 | for animation in animations { 148 | let index = animation.pointIndex 149 | guard index < result.count else { continue } 150 | 151 | animation.apply(to: &result[index], at: phase) 152 | } 153 | 154 | return result 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Tests/MeshingKitTests/MeshingKitTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import MeshingKit 3 | import SwiftUI 4 | #if canImport(UIKit) 5 | import UIKit 6 | #elseif canImport(AppKit) 7 | import AppKit 8 | #endif 9 | 10 | @Suite("MeshingKit Tests") 11 | struct MeshingKitTests { 12 | 13 | @Test("Gradient creation from template") 14 | @MainActor 15 | func gradientCreation() { 16 | let template = GradientTemplateSize3.auroraBorealis 17 | 18 | // Verify the template has the correct size 19 | #expect(template.size == 3) 20 | 21 | // Verify the template has the expected number of points (3x3 = 9 points) 22 | #expect(template.points.count == 9) 23 | 24 | // Verify the template has the expected number of colors (3x3 = 9 colors) 25 | #expect(template.colors.count == 9) 26 | 27 | // Verify points are normalized (between 0.0 and 1.0) 28 | for point in template.points { 29 | #expect(point.x >= 0.0 && point.x <= 1.0) 30 | #expect(point.y >= 0.0 && point.y <= 1.0) 31 | } 32 | 33 | // Verify the gradient can be created without errors 34 | let gradient = MeshingKit.gradient(template: template) 35 | #expect(type(of: gradient) == MeshGradient.self) 36 | } 37 | 38 | @Test("Template counts validation") 39 | func templateCounts() { 40 | let size2Count = GradientTemplateSize2.allCases.count 41 | let size3Count = GradientTemplateSize3.allCases.count 42 | let size4Count = GradientTemplateSize4.allCases.count 43 | 44 | #expect(size2Count == 35) 45 | #expect(size3Count == 22) 46 | #expect(size4Count == 11) 47 | 48 | // Verify PredefinedTemplate.allCases matches the sum 49 | #expect(PredefinedTemplate.allCases.count == size2Count + size3Count + size4Count) 50 | } 51 | 52 | @Test("Hex color extension", arguments: [ 53 | "#FF0000", 54 | "#00FF00", 55 | "#0000FF", 56 | "#FFFFFF", 57 | "#000000" 58 | ]) 59 | func colorHexExtension(hexValue: String) { 60 | let color = Color(hex: hexValue) 61 | #expect(type(of: color) == Color.self) 62 | } 63 | 64 | @Test("Hex color extension handles short format") 65 | func hexColorShortFormat() { 66 | // 3-character hex (RGB) 67 | let color = Color(hex: "#F00") 68 | #expect(type(of: color) == Color.self) 69 | } 70 | 71 | @Test("Hex color extension handles alpha format") 72 | func hexColorAlphaFormat() { 73 | // 8-character hex (ARGB) 74 | let color = Color(hex: "#80FF0000") 75 | #expect(type(of: color) == Color.self) 76 | } 77 | 78 | @Test("Hex color extension handles invalid input") 79 | func hexColorInvalidInput() { 80 | let invalidColors = [ 81 | "not-a-hex", 82 | "#GGGGGG", 83 | "#12345" 84 | ] 85 | 86 | for hexValue in invalidColors { 87 | let color = Color(hex: hexValue) 88 | let rgba = resolvedRGBA(color) 89 | #expect(isApproximatelyEqual(rgba.r, 1.0)) 90 | #expect(isApproximatelyEqual(rgba.g, 1.0)) 91 | #expect(isApproximatelyEqual(rgba.b, 1.0)) 92 | #expect(isApproximatelyEqual(rgba.a, 1.0)) 93 | } 94 | } 95 | 96 | @Test("Hex color extension outputs hex string") 97 | func hexColorOutputsHexString() { 98 | let color = Color(hex: "#FF0000") 99 | #expect(color.hexString() == "#FF0000") 100 | #expect(color.hexString(includeAlpha: true) == "#FFFF0000") 101 | } 102 | 103 | private func validateTemplates( 104 | _ templates: [T], 105 | expectedSize: Int 106 | ) { 107 | let expectedCount = expectedSize * expectedSize 108 | for template in templates { 109 | #expect(template.size == expectedSize) 110 | #expect(template.points.count == expectedCount) 111 | #expect(template.colors.count == expectedCount) 112 | #expect(!template.name.isEmpty) 113 | 114 | for point in template.points { 115 | #expect(point.x >= 0.0 && point.x <= 1.0, "Point x=\(point.x) out of range in \(template.name)") 116 | #expect(point.y >= 0.0 && point.y <= 1.0, "Point y=\(point.y) out of range in \(template.name)") 117 | } 118 | } 119 | } 120 | 121 | @Test("All templates have valid structure") 122 | func allTemplatesValid() { 123 | validateTemplates(GradientTemplateSize2.allCases, expectedSize: 2) 124 | validateTemplates(GradientTemplateSize3.allCases, expectedSize: 3) 125 | validateTemplates(GradientTemplateSize4.allCases, expectedSize: 4) 126 | } 127 | 128 | @Test("PredefinedTemplate allCases contains all templates") 129 | func predefinedTemplateAllCases() { 130 | let allTemplates = PredefinedTemplate.allCases 131 | 132 | // Verify count 133 | #expect(allTemplates.count == 68) 134 | 135 | // Verify all IDs are unique 136 | let ids = allTemplates.map { $0.id } 137 | let uniqueIds = Set(ids) 138 | #expect(ids.count == uniqueIds.count, "Duplicate template IDs found") 139 | 140 | // Verify we have the correct count of templates from each size 141 | let counts = allTemplates.reduce(into: (size2: 0, size3: 0, size4: 0)) { counts, template in 142 | switch template { 143 | case .size2: counts.size2 += 1 144 | case .size3: counts.size3 += 1 145 | case .size4: counts.size4 += 1 146 | } 147 | } 148 | 149 | #expect(counts.size2 == 35) 150 | #expect(counts.size3 == 22) 151 | #expect(counts.size4 == 11) 152 | } 153 | } 154 | 155 | private struct RGBA { 156 | let r: Double 157 | let g: Double 158 | let b: Double 159 | let a: Double 160 | } 161 | 162 | private func resolvedRGBA(_ color: Color) -> RGBA { 163 | #if canImport(UIKit) 164 | let platformColor = UIColor(color) 165 | var r: CGFloat = 0 166 | var g: CGFloat = 0 167 | var b: CGFloat = 0 168 | var a: CGFloat = 0 169 | platformColor.getRed(&r, green: &g, blue: &b, alpha: &a) 170 | return RGBA(r: Double(r), g: Double(g), b: Double(b), a: Double(a)) 171 | #elseif canImport(AppKit) 172 | let platformColor = NSColor(color) 173 | let srgb = platformColor.usingColorSpace(.sRGB) ?? platformColor 174 | var r: CGFloat = 0 175 | var g: CGFloat = 0 176 | var b: CGFloat = 0 177 | var a: CGFloat = 0 178 | srgb.getRed(&r, green: &g, blue: &b, alpha: &a) 179 | return RGBA(r: Double(r), g: Double(g), b: Double(b), a: Double(a)) 180 | #else 181 | return RGBA(r: 0, g: 0, b: 0, a: 0) 182 | #endif 183 | } 184 | 185 | private func isApproximatelyEqual(_ lhs: Double, _ rhs: Double, tolerance: Double = 0.0001) 186 | -> Bool 187 | { 188 | abs(lhs - rhs) <= tolerance 189 | } 190 | -------------------------------------------------------------------------------- /Sources/MeshingKit/GradientTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientTemplate.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 10/14/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A protocol that defines a template for creating mesh gradients. 11 | /// 12 | /// `GradientTemplate` encapsulates all the necessary information to generate a mesh gradient, 13 | /// including its name, size, control points, colors, and background color. 14 | public protocol GradientTemplate: Sendable { 15 | 16 | /// The name of the gradient template. 17 | var name: String { get } 18 | 19 | /// The grid size of the gradient. 20 | /// 21 | /// For example, a size of `3` represents a 3×3 grid (9 control points/colors). 22 | var size: Int { get } 23 | 24 | /// An array of 2D points that define the control points of the gradient. 25 | /// 26 | /// Each point is represented as a `SIMD2` where: 27 | /// - The x-component represents the horizontal position (0.0 to 1.0). 28 | /// - The y-component represents the vertical position (0.0 to 1.0). 29 | var points: [SIMD2] { get } 30 | 31 | /// An array of colors associated with the control points. 32 | /// 33 | /// The colors in this array correspond to the points in the `points` array. 34 | var colors: [Color] { get } 35 | 36 | /// The background color of the gradient. 37 | /// 38 | /// This color is used as the base color for areas not directly affected by the control points. 39 | var background: Color { get } 40 | } 41 | 42 | /// A structure that implements the GradientTemplate protocol for custom gradients. 43 | public struct CustomGradientTemplate: GradientTemplate { 44 | 45 | /// The name of the gradient template. 46 | public let name: String 47 | 48 | /// The grid size of the gradient. 49 | /// 50 | /// For example, a size of `4` represents a 4×4 grid (16 control points/colors). 51 | public let size: Int 52 | 53 | /// An array of 2D points that define the control points of the gradient. 54 | /// 55 | /// Each point is represented as a `SIMD2` where: 56 | /// - The x-component represents the horizontal position (0.0 to 1.0). 57 | /// - The y-component represents the vertical position (0.0 to 1.0). 58 | public let points: [SIMD2] 59 | 60 | /// An array of colors associated with the control points. 61 | /// 62 | /// The colors in this array correspond to the points in the `points` array. 63 | public let colors: [Color] 64 | 65 | /// The background color of the gradient. 66 | /// 67 | /// This color is used as the base color for areas not directly affected by the control points. 68 | public let background: Color 69 | 70 | /// Validation issues for custom gradient templates. 71 | public enum ValidationError: Error, Equatable { 72 | case invalidSize(Int) 73 | case pointsCount(expected: Int, actual: Int) 74 | case colorsCount(expected: Int, actual: Int) 75 | case pointOutOfRange(index: Int, x: Float, y: Float) 76 | } 77 | 78 | /// A collection of validation errors for a custom gradient template. 79 | public struct ValidationErrors: Error, Equatable { 80 | public let errors: [ValidationError] 81 | 82 | public init(errors: [ValidationError]) { 83 | self.errors = errors 84 | } 85 | } 86 | 87 | /// Validates the provided template data without triggering a precondition. 88 | /// 89 | /// - Returns: An array of validation errors. Empty means the data is valid. 90 | public static func validate( 91 | size: Int, 92 | points: [SIMD2], 93 | colors: [Color] 94 | ) -> [ValidationError] { 95 | var errors: [ValidationError] = [] 96 | 97 | guard size > 0 else { 98 | errors.append(.invalidSize(size)) 99 | return errors 100 | } 101 | 102 | let expectedCount = size * size 103 | 104 | if points.count != expectedCount { 105 | errors.append(.pointsCount(expected: expectedCount, actual: points.count)) 106 | } 107 | 108 | if colors.count != expectedCount { 109 | errors.append(.colorsCount(expected: expectedCount, actual: colors.count)) 110 | } 111 | 112 | for (index, point) in points.enumerated() { 113 | if point.x < 0.0 || point.x > 1.0 || point.y < 0.0 || point.y > 1.0 { 114 | errors.append(.pointOutOfRange(index: index, x: point.x, y: point.y)) 115 | } 116 | } 117 | 118 | return errors 119 | } 120 | 121 | /// Validates the current template data without triggering a precondition. 122 | /// 123 | /// - Returns: An array of validation errors. Empty means the data is valid. 124 | public func validate() -> [ValidationError] { 125 | return Self.validate(size: size, points: points, colors: colors) 126 | } 127 | 128 | /// Creates a new custom gradient template with validation. 129 | /// 130 | /// - Throws: `ValidationErrors` if validation fails. 131 | public init( 132 | validating name: String, 133 | size: Int, 134 | points: [SIMD2], 135 | colors: [Color], 136 | background: Color 137 | ) throws { 138 | let errors = Self.validate(size: size, points: points, colors: colors) 139 | guard errors.isEmpty else { 140 | throw ValidationErrors(errors: errors) 141 | } 142 | 143 | self.name = name 144 | self.size = size 145 | self.points = points 146 | self.colors = colors 147 | self.background = background 148 | } 149 | 150 | /// Creates a new custom gradient template with the specified parameters. 151 | /// 152 | /// - Parameters: 153 | /// - name: A string that identifies the gradient template. 154 | /// - size: The grid size (width and height are equal). 155 | /// - points: An array of `SIMD2` values representing the control points. 156 | /// - colors: An array of `Color` values corresponding to each control point. 157 | /// - background: The base color of the gradient. 158 | /// 159 | /// - Note: The number of elements in `points` should match the number of elements in `colors`. 160 | /// - Precondition: `size > 0`, `points.count == size * size`, `colors.count == size * size`, 161 | /// and all points have coordinates in the range [0.0, 1.0]. 162 | public init( 163 | name: String, 164 | size: Int, 165 | points: [SIMD2], 166 | colors: [Color], 167 | background: Color 168 | ) { 169 | let expectedCount = size * size 170 | precondition(size > 0, "Gradient size must be greater than 0") 171 | precondition(points.count == expectedCount, 172 | "Expected \(expectedCount) points for size \(size), got \(points.count)") 173 | precondition(colors.count == expectedCount, 174 | "Expected \(expectedCount) colors for size \(size), got \(colors.count)") 175 | 176 | // Validate point ranges 177 | for (index, point) in points.enumerated() { 178 | precondition(point.x >= 0.0 && point.x <= 1.0, 179 | "Point at index \(index) has x coordinate \(point.x) outside valid range [0.0, 1.0]") 180 | precondition(point.y >= 0.0 && point.y <= 1.0, 181 | "Point at index \(index) has y coordinate \(point.y) outside valid range [0.0, 1.0]") 182 | } 183 | 184 | self.name = name 185 | self.size = size 186 | self.points = points 187 | self.colors = colors 188 | self.background = background 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Sources/MeshingKit/GradientExport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientExport.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 12/19/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(UIKit) 11 | import UIKit 12 | 13 | /// A platform-specific image type for cross-platform API compatibility. 14 | /// 15 | /// - On iOS, tvOS, and watchOS: Equivalent to `UIImage`. 16 | /// - On macOS: Equivalent to `NSImage`. 17 | public typealias PlatformImage = UIImage 18 | #elseif canImport(AppKit) 19 | import AppKit 20 | 21 | /// A platform-specific image type for cross-platform API compatibility. 22 | /// 23 | /// - On iOS, tvOS, and watchOS: Equivalent to `UIImage`. 24 | /// - On macOS: Equivalent to `NSImage`. 25 | public typealias PlatformImage = NSImage 26 | #endif 27 | 28 | public extension MeshingKit { 29 | /// Generates evenly spaced gradient stops for previewing template colors. 30 | static func previewStops(template: any GradientTemplate) -> [Gradient.Stop] { 31 | let colors = template.colors 32 | guard !colors.isEmpty else { return [] } 33 | 34 | let count = colors.count 35 | return colors.enumerated().map { index, color in 36 | let location = count == 1 ? 0.0 : Double(index) / Double(count - 1) 37 | return Gradient.Stop(color: color, location: location) 38 | } 39 | } 40 | 41 | /// Generates evenly spaced gradient stops for previewing predefined template colors. 42 | static func previewStops(template: PredefinedTemplate) -> [Gradient.Stop] { 43 | previewStops(template: template.baseTemplate) 44 | } 45 | 46 | /// Builds a SwiftUI snippet containing `Gradient.Stop` entries for a template. 47 | static func swiftUIStopsSnippet( 48 | template: any GradientTemplate, 49 | includeAlpha: Bool = false, 50 | precision: Int = 2 51 | ) -> String { 52 | let stops = previewStops(template: template) 53 | let format = "%.\(precision)f" 54 | let entries = stops.map { stop in 55 | let hex = stop.color.hexString(includeAlpha: includeAlpha) ?? "#FFFFFF" 56 | let location = String(format: format, stop.location) 57 | return ".init(color: Color(hex: \"\(hex)\"), location: \(location))" 58 | } 59 | 60 | return "[\(entries.joined(separator: ", "))]" 61 | } 62 | 63 | /// Builds a SwiftUI snippet containing `Gradient.Stop` entries for a predefined template. 64 | static func swiftUIStopsSnippet( 65 | template: PredefinedTemplate, 66 | includeAlpha: Bool = false, 67 | precision: Int = 2 68 | ) -> String { 69 | swiftUIStopsSnippet(template: template.baseTemplate, includeAlpha: includeAlpha, precision: precision) 70 | } 71 | 72 | /// Builds a CSS `linear-gradient` preview string for a template. 73 | static func cssLinearGradientSnippet( 74 | template: any GradientTemplate, 75 | angle: Double = 90, 76 | includeAlpha: Bool = false 77 | ) -> String { 78 | let stops = previewStops(template: template) 79 | let stopDescriptions = stops.map { stop in 80 | let percent = Int(round(stop.location * 100)) 81 | let color = cssColorString(for: stop.color, includeAlpha: includeAlpha) 82 | return "\(color) \(percent)%" 83 | } 84 | 85 | let angleString = String(format: "%.0f", angle) 86 | return "linear-gradient(\(angleString)deg, \(stopDescriptions.joined(separator: ", ")))" 87 | } 88 | 89 | /// Builds a CSS `linear-gradient` preview string for a predefined template. 90 | static func cssLinearGradientSnippet( 91 | template: PredefinedTemplate, 92 | angle: Double = 90, 93 | includeAlpha: Bool = false 94 | ) -> String { 95 | cssLinearGradientSnippet(template: template.baseTemplate, angle: angle, includeAlpha: includeAlpha) 96 | } 97 | 98 | /// Renders a mesh gradient template to a CGImage snapshot. 99 | @MainActor 100 | static func snapshotCGImage( 101 | template: any GradientTemplate, 102 | size: CGSize, 103 | scale: CGFloat = 1.0, 104 | smoothsColors: Bool = true 105 | ) -> CGImage? { 106 | let gradient = MeshingKit.gradient(template: template, smoothsColors: smoothsColors) 107 | .frame(width: size.width, height: size.height) 108 | 109 | let renderer = ImageRenderer(content: gradient) 110 | renderer.scale = scale 111 | renderer.proposedSize = ProposedViewSize(width: size.width, height: size.height) 112 | 113 | return renderer.cgImage 114 | } 115 | 116 | /// Renders a predefined template to a CGImage snapshot. 117 | @MainActor 118 | static func snapshotCGImage( 119 | template: PredefinedTemplate, 120 | size: CGSize, 121 | scale: CGFloat = 1.0, 122 | smoothsColors: Bool = true 123 | ) -> CGImage? { 124 | snapshotCGImage( 125 | template: template.baseTemplate, 126 | size: size, 127 | scale: scale, 128 | smoothsColors: smoothsColors 129 | ) 130 | } 131 | 132 | #if canImport(UIKit) 133 | /// Renders a mesh gradient template to a UIImage snapshot. 134 | @MainActor 135 | static func snapshotImage( 136 | template: any GradientTemplate, 137 | size: CGSize, 138 | scale: CGFloat = 1.0, 139 | smoothsColors: Bool = true 140 | ) -> UIImage? { 141 | guard let cgImage = snapshotCGImage( 142 | template: template, 143 | size: size, 144 | scale: scale, 145 | smoothsColors: smoothsColors 146 | ) else { 147 | return nil 148 | } 149 | return UIImage(cgImage: cgImage) 150 | } 151 | 152 | /// Renders a predefined template to a UIImage snapshot. 153 | @MainActor 154 | static func snapshotImage( 155 | template: PredefinedTemplate, 156 | size: CGSize, 157 | scale: CGFloat = 1.0, 158 | smoothsColors: Bool = true 159 | ) -> UIImage? { 160 | snapshotImage( 161 | template: template.baseTemplate, 162 | size: size, 163 | scale: scale, 164 | smoothsColors: smoothsColors 165 | ) 166 | } 167 | #elseif canImport(AppKit) 168 | /// Renders a mesh gradient template to an NSImage snapshot. 169 | @MainActor 170 | static func snapshotImage( 171 | template: any GradientTemplate, 172 | size: CGSize, 173 | scale: CGFloat = 1.0, 174 | smoothsColors: Bool = true 175 | ) -> NSImage? { 176 | guard let cgImage = snapshotCGImage( 177 | template: template, 178 | size: size, 179 | scale: scale, 180 | smoothsColors: smoothsColors 181 | ) else { 182 | return nil 183 | } 184 | return NSImage(cgImage: cgImage, size: size) 185 | } 186 | 187 | /// Renders a predefined template to an NSImage snapshot. 188 | @MainActor 189 | static func snapshotImage( 190 | template: PredefinedTemplate, 191 | size: CGSize, 192 | scale: CGFloat = 1.0, 193 | smoothsColors: Bool = true 194 | ) -> NSImage? { 195 | snapshotImage( 196 | template: template.baseTemplate, 197 | size: size, 198 | scale: scale, 199 | smoothsColors: smoothsColors 200 | ) 201 | } 202 | #endif 203 | } 204 | 205 | private extension MeshingKit { 206 | static func cssColorString(for color: Color, includeAlpha: Bool) -> String { 207 | if includeAlpha, let components = color.rgbaComponents() { 208 | let r = Int(round(components.r * 255)) 209 | let g = Int(round(components.g * 255)) 210 | let b = Int(round(components.b * 255)) 211 | let a = String(format: "%.2f", components.a) 212 | return "rgba(\(r), \(g), \(b), \(a))" 213 | } 214 | return color.hexString(includeAlpha: includeAlpha) ?? "#FFFFFF" 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Sources/MeshingKit/MeshingKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshingKit.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 10/14/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A type alias for an array of 2D points used in mesh gradients. 11 | /// 12 | /// Each point is represented as a `SIMD2` where: 13 | /// - The x-component represents the horizontal position (0.0 to 1.0). 14 | /// - The y-component represents the vertical position (0.0 to 1.0). 15 | public typealias MeshPoints = [SIMD2] 16 | 17 | /// A structure that provides utility functions for creating mesh gradients. 18 | public struct MeshingKit: Sendable { 19 | 20 | /// Creates a `MeshGradient` from a given `GradientTemplateSize3`. 21 | /// 22 | /// This function takes a `GradientTemplateSize3` and converts it into a `MeshGradient`, 23 | /// using the template's size, points, and colors. 24 | /// 25 | /// - Parameters: 26 | /// - template: A `GradientTemplateSize3` containing the gradient's specifications. 27 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 28 | /// - Returns: A `MeshGradient` instance created from the provided template. 29 | /// 30 | /// Example: 31 | /// ```swift 32 | /// let gradient = MeshingKit.gradientSize3(template: .auroraBorealis) 33 | /// ``` 34 | @MainActor public static func gradientSize3( 35 | template: GradientTemplateSize3, 36 | smoothsColors: Bool = true 37 | ) 38 | -> MeshGradient 39 | { 40 | gradient(template: template, smoothsColors: smoothsColors) 41 | } 42 | 43 | /// Creates a `MeshGradient` from a given `GradientTemplateSize2`. 44 | /// 45 | /// This function takes a `GradientTemplateSize2` and converts it into a `MeshGradient`, 46 | /// using the template's size, points, and colors. 47 | /// 48 | /// - Parameters: 49 | /// - template: A `GradientTemplateSize2` containing the gradient's specifications. 50 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 51 | /// - Returns: A `MeshGradient` instance created from the provided template. 52 | /// 53 | /// Example: 54 | /// ```swift 55 | /// let gradient = MeshingKit.gradientSize2(template: .mysticTwilight) 56 | /// ``` 57 | @MainActor public static func gradientSize2( 58 | template: GradientTemplateSize2, 59 | smoothsColors: Bool = true 60 | ) 61 | -> MeshGradient 62 | { 63 | gradient(template: template, smoothsColors: smoothsColors) 64 | } 65 | 66 | /// Creates a `MeshGradient` from a given `GradientTemplateSize4`. 67 | /// 68 | /// This function takes a `GradientTemplateSize4` and converts it into a `MeshGradient`, 69 | /// using the template's size, points, and colors. 70 | /// 71 | /// - Parameters: 72 | /// - template: A `GradientTemplateSize4` containing the gradient's specifications. 73 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 74 | /// - Returns: A `MeshGradient` instance created from the provided template. 75 | /// 76 | /// Example: 77 | /// ```swift 78 | /// let gradient = MeshingKit.gradientSize4(template: .cosmicNebula) 79 | /// ``` 80 | @MainActor public static func gradientSize4( 81 | template: GradientTemplateSize4, 82 | smoothsColors: Bool = true 83 | ) 84 | -> MeshGradient 85 | { 86 | gradient(template: template, smoothsColors: smoothsColors) 87 | } 88 | 89 | /// Creates a `MeshGradient` from a given `GradientTemplate`. 90 | /// 91 | /// This function takes any `GradientTemplate` and converts it into a `MeshGradient`, 92 | /// using the template's size, points, and colors. 93 | /// 94 | /// - Parameters: 95 | /// - template: A `GradientTemplate` containing the gradient's specifications. 96 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 97 | /// - Returns: A `MeshGradient` instance created from the provided template. 98 | /// 99 | /// Example: 100 | /// ```swift 101 | /// // Using with enum templates 102 | /// let gradient = MeshingKit.gradient(template: GradientTemplateSize3.auroraBorealis) 103 | /// 104 | /// // Using with custom template 105 | /// let customTemplate = CustomGradientTemplate(name: "Custom", size: 4, 106 | /// points: [...], colors: [...], background: .black) 107 | /// let gradient = MeshingKit.gradient(template: customTemplate) 108 | /// ``` 109 | @MainActor public static func gradient( 110 | template: GradientTemplate, 111 | smoothsColors: Bool = true 112 | ) 113 | -> MeshGradient 114 | { 115 | MeshGradient( 116 | width: template.size, 117 | height: template.size, 118 | locations: .points(template.points), 119 | colors: .colors(template.colors), 120 | background: template.background, 121 | smoothsColors: smoothsColors 122 | ) 123 | } 124 | 125 | /// Creates a `MeshGradient` from a predefined template. 126 | /// 127 | /// - Parameters: 128 | /// - template: The predefined template to use. 129 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 130 | /// - Returns: A `MeshGradient` instance created from the provided template. 131 | /// 132 | /// Example: 133 | /// ```swift 134 | /// let gradient = MeshingKit.gradient(template: .size3(.auroraBorealis)) 135 | /// ``` 136 | @MainActor public static func gradient( 137 | template: PredefinedTemplate, 138 | smoothsColors: Bool = true 139 | ) 140 | -> MeshGradient 141 | { 142 | gradient(template: template.baseTemplate, smoothsColors: smoothsColors) 143 | } 144 | 145 | /// Creates an animated `MeshGradient` view from any gradient template. 146 | /// 147 | /// - Parameters: 148 | /// - template: A gradient template to use. 149 | /// - showAnimation: A binding to control the animation's play/pause state. 150 | /// - animationSpeed: Controls the speed of the animation (default: 1.0). 151 | /// - animationPattern: Optional custom animation pattern to apply. 152 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 153 | /// - Returns: A view containing the animated `MeshGradient`. 154 | /// 155 | /// Example: 156 | /// ```swift 157 | /// struct ContentView: View { 158 | /// @State private var showAnimation = true 159 | /// 160 | /// var body: some View { 161 | /// MeshingKit.animatedGradient( 162 | /// .size3.intelligence, 163 | /// showAnimation: $showAnimation, 164 | /// animationSpeed: 1.5 165 | /// ) 166 | /// } 167 | /// } 168 | /// ``` 169 | @MainActor public static func animatedGradient( 170 | _ template: any GradientTemplate, 171 | showAnimation: Binding, 172 | animationSpeed: Double = 1.0, 173 | animationPattern: AnimationPattern? = nil, 174 | smoothsColors: Bool = true 175 | ) -> some View { 176 | AnimatedMeshGradientView( 177 | gridSize: template.size, 178 | showAnimation: showAnimation, 179 | positions: template.points, 180 | colors: template.colors, 181 | background: template.background, 182 | animationSpeed: animationSpeed, 183 | animationPattern: animationPattern, 184 | smoothsColors: smoothsColors 185 | ) 186 | } 187 | 188 | /// Creates an animated `MeshGradient` view from a predefined template. 189 | /// 190 | /// - Parameters: 191 | /// - template: A predefined template to use. 192 | /// - showAnimation: A binding to control the animation's play/pause state. 193 | /// - animationSpeed: Controls the speed of the animation (default: 1.0). 194 | /// - animationPattern: Optional custom animation pattern to apply. 195 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 196 | /// - Returns: A view containing the animated `MeshGradient`. 197 | /// 198 | /// Example: 199 | /// ```swift 200 | /// struct ContentView: View { 201 | /// @State private var showAnimation = true 202 | /// 203 | /// var body: some View { 204 | /// MeshingKit.animatedGradient( 205 | /// .size3(.intelligence), 206 | /// showAnimation: $showAnimation, 207 | /// animationSpeed: 1.5 208 | /// ) 209 | /// } 210 | /// } 211 | /// ``` 212 | @MainActor public static func animatedGradient( 213 | _ template: PredefinedTemplate, 214 | showAnimation: Binding, 215 | animationSpeed: Double = 1.0, 216 | animationPattern: AnimationPattern? = nil, 217 | smoothsColors: Bool = true 218 | ) -> some View { 219 | animatedGradient( 220 | template.baseTemplate, 221 | showAnimation: showAnimation, 222 | animationSpeed: animationSpeed, 223 | animationPattern: animationPattern, 224 | smoothsColors: smoothsColors 225 | ) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Sources/MeshingKit/PredefinedTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PredefinedTemplate.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 2/25/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | #if canImport(NaturalLanguage) 11 | import NaturalLanguage 12 | #endif 13 | 14 | /// A type representing predefined gradient templates of different sizes. 15 | public enum PredefinedTemplate: Identifiable, CaseIterable, Equatable { 16 | case size2(GradientTemplateSize2) 17 | case size3(GradientTemplateSize3) 18 | case size4(GradientTemplateSize4) 19 | 20 | /// All predefined templates across all sizes. 21 | /// 22 | /// This property provides access to all templates in a single collection for easy iteration. 23 | public static var allCases: [PredefinedTemplate] { 24 | GradientTemplateSize2.allCases.map(PredefinedTemplate.size2) 25 | + GradientTemplateSize3.allCases.map(PredefinedTemplate.size3) 26 | + GradientTemplateSize4.allCases.map(PredefinedTemplate.size4) 27 | } 28 | 29 | /// A unique identifier for the template. 30 | /// 31 | /// The identifier is constructed from the template size and the template's raw value, 32 | /// ensuring uniqueness across all predefined templates. 33 | public var id: String { 34 | switch self { 35 | case .size2(let template): return "size2_\(template.rawValue)" 36 | case .size3(let template): return "size3_\(template.rawValue)" 37 | case .size4(let template): return "size4_\(template.rawValue)" 38 | } 39 | } 40 | 41 | /// Returns the underlying base template for this predefined template. 42 | var baseTemplate: any GradientTemplate { 43 | switch self { 44 | case .size2(let template): return template 45 | case .size3(let template): return template 46 | case .size4(let template): return template 47 | } 48 | } 49 | } 50 | 51 | /// Describes the mood of a gradient template for browsing and search. 52 | public enum TemplateMood: String, CaseIterable, Sendable { 53 | case aquatic 54 | case bright 55 | case cool 56 | case cosmic 57 | case dark 58 | case earthy 59 | case fiery 60 | case vibrant 61 | case warm 62 | } 63 | 64 | /// Metadata for a predefined template. 65 | public struct TemplateMetadata: Sendable { 66 | public let name: String 67 | public let tags: [String] 68 | public let moods: [TemplateMood] 69 | public let palette: [Color] 70 | public let background: Color 71 | 72 | public init( 73 | name: String, 74 | tags: [String], 75 | moods: [TemplateMood], 76 | palette: [Color], 77 | background: Color 78 | ) { 79 | self.name = name 80 | self.tags = tags 81 | self.moods = moods 82 | self.palette = palette 83 | self.background = background 84 | } 85 | } 86 | 87 | public extension PredefinedTemplate { 88 | /// Template metadata computed on demand. 89 | package var metadataValue: TemplateMetadata { 90 | let nameTokens = Self.normalizedTokens(from: rawName) 91 | let moodList = Self.moods(for: nameTokens) 92 | let moodTokens = moodList.map(\.rawValue) 93 | let tags = Self.uniqueTokens(nameTokens + moodTokens) 94 | 95 | return TemplateMetadata( 96 | name: template.name, 97 | tags: tags, 98 | moods: moodList, 99 | palette: template.colors, 100 | background: template.background 101 | ) 102 | } 103 | 104 | /// The underlying template for this predefined case. 105 | package var template: any GradientTemplate { 106 | switch self { 107 | case .size2(let specificTemplate): return specificTemplate 108 | case .size3(let specificTemplate): return specificTemplate 109 | case .size4(let specificTemplate): return specificTemplate 110 | } 111 | } 112 | 113 | /// A user-facing name for the template. 114 | package var name: String { 115 | template.name 116 | } 117 | 118 | /// The palette colors for the template. 119 | package var palette: [Color] { 120 | template.colors 121 | } 122 | 123 | /// The background color for the template. 124 | package var background: Color { 125 | template.background 126 | } 127 | 128 | /// Tags derived from the template name and mood. 129 | package var tags: [String] { 130 | metadataValue.tags 131 | } 132 | 133 | /// Moods derived from the template name. 134 | package var moods: [TemplateMood] { 135 | metadataValue.moods 136 | } 137 | 138 | /// Combined metadata for the template. 139 | package var metadata: TemplateMetadata { 140 | metadataValue 141 | } 142 | 143 | /// Finds templates that best match the query. 144 | /// 145 | /// - Parameters: 146 | /// - query: Search terms (tags or mood keywords). 147 | /// - limit: Optional limit for the number of results. 148 | /// - Returns: Templates ordered by best match. 149 | static func find(by query: String, limit: Int? = nil) -> [PredefinedTemplate] { 150 | let queryTokens = normalizedTokens(from: query) 151 | guard !queryTokens.isEmpty else { 152 | return allCases 153 | } 154 | 155 | let results = allCases.compactMap { template -> (score: Int, template: PredefinedTemplate)? in 156 | let tokens = template.searchTokens 157 | let score = matchScore(for: queryTokens, in: tokens) 158 | return score > 0 ? (score, template) : nil 159 | } 160 | .sorted { lhs, rhs in 161 | if lhs.score == rhs.score { 162 | return lhs.template.id < rhs.template.id 163 | } 164 | return lhs.score > rhs.score 165 | } 166 | .map { $0.template } 167 | 168 | if let limit { 169 | return Array(results.prefix(limit)) 170 | } 171 | return results 172 | } 173 | } 174 | 175 | private extension PredefinedTemplate { 176 | var rawName: String { 177 | switch self { 178 | case .size2(let specificTemplate): return specificTemplate.rawValue 179 | case .size3(let specificTemplate): return specificTemplate.rawValue 180 | case .size4(let specificTemplate): return specificTemplate.rawValue 181 | } 182 | } 183 | 184 | var searchTokens: [String] { 185 | tags 186 | } 187 | 188 | static func moods(for tokens: [String]) -> [TemplateMood] { 189 | var matched: [TemplateMood] = [] 190 | 191 | for (mood, keywords) in moodKeywords where tokens.contains(where: keywords.contains) { 192 | matched.append(mood) 193 | } 194 | 195 | return matched 196 | } 197 | 198 | static func matchScore(for queryTokens: [String], in tokens: [String]) -> Int { 199 | var score = 0 200 | 201 | for query in queryTokens { 202 | if tokens.contains(query) { 203 | score += 3 204 | continue 205 | } 206 | 207 | if tokens.contains(where: { $0.hasPrefix(query) }) { 208 | score += 2 209 | continue 210 | } 211 | 212 | if tokens.contains(where: { $0.contains(query) }) { 213 | score += 1 214 | } 215 | } 216 | 217 | return score 218 | } 219 | 220 | static func normalizedTokens(from string: String) -> [String] { 221 | let rawTokens = basicTokens(from: string) 222 | #if canImport(NaturalLanguage) 223 | return rawTokens.map { lemmatize($0) } 224 | #else 225 | return rawTokens 226 | #endif 227 | } 228 | 229 | static func basicTokens(from string: String) -> [String] { 230 | let components = string 231 | .components(separatedBy: CharacterSet.alphanumerics.inverted) 232 | .filter { !$0.isEmpty } 233 | 234 | let splitTokens = components.flatMap { splitCamelCase($0) } 235 | return splitTokens.map { $0.lowercased() } 236 | } 237 | 238 | static func splitCamelCase(_ string: String) -> [String] { 239 | var tokens: [String] = [] 240 | var current = "" 241 | 242 | for character in string { 243 | if character.isUppercase, !current.isEmpty { 244 | tokens.append(current) 245 | current = "" 246 | } 247 | current.append(character) 248 | } 249 | 250 | if !current.isEmpty { 251 | tokens.append(current) 252 | } 253 | 254 | return tokens 255 | } 256 | 257 | static func uniqueTokens(_ tokens: [String]) -> [String] { 258 | var seen: Set = [] 259 | var result: [String] = [] 260 | 261 | for token in tokens where seen.insert(token).inserted { 262 | result.append(token) 263 | } 264 | 265 | return result 266 | } 267 | 268 | static let moodKeywords: [TemplateMood: Set] = [ 269 | .aquatic: ["ocean", "sea", "lagoon", "breeze", "mist"], 270 | .bright: ["sunrise", "morning", "glow", "dawn"], 271 | .cool: ["arctic", "frost", "winter", "ice", "mint"], 272 | .cosmic: ["cosmic", "aurora", "nebula", "galaxy", "starry"], 273 | .dark: ["midnight", "night", "shadow"], 274 | .earthy: ["forest", "meadow", "jungle", "dunes", "desert"], 275 | .fiery: ["ember", "lava", "volcanic", "fire", "blaze"], 276 | .vibrant: ["neon", "electric", "citrus"], 277 | .warm: ["sunset", "golden", "autumn", "crimson"] 278 | ] 279 | 280 | #if canImport(NaturalLanguage) 281 | static func lemmatize(_ token: String) -> String { 282 | let tagger = NLTagger(tagSchemes: [.lemma]) 283 | tagger.string = token 284 | let (tag, _) = tagger.tag(at: token.startIndex, unit: .word, scheme: .lemma) 285 | if let lemma = tag?.rawValue, !lemma.isEmpty, lemma != token { 286 | return lemma 287 | } 288 | return token.lowercased() 289 | } 290 | #endif 291 | } 292 | -------------------------------------------------------------------------------- /Sources/MeshingKit/GradientTemplateSize2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientTemplateSize2.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 10/14/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration of predefined gradient templates with a 2x2 grid size. 11 | public enum GradientTemplateSize2: String, CaseIterable, GradientTemplate { 12 | case mysticTwilight 13 | case tropicalParadise 14 | case cherryBlossom 15 | case arcticFrost 16 | case goldenSunrise 17 | case emeraldForest 18 | case desertMirage 19 | case midnightGalaxy 20 | case autumnHarvest 21 | case oceanBreeze 22 | case lavenderDreams 23 | case citrusBurst 24 | case northernLights 25 | case strawberryLemonade 26 | case deepSea 27 | case cottonCandy 28 | case volcanicAsh 29 | case springMeadow 30 | case cosmicDust 31 | case peacockFeathers 32 | case crimsonSunset 33 | case enchantedForest 34 | case blueberryMuffin 35 | case saharaDunes 36 | case grapeSoda 37 | case frostyWinter 38 | case dragonFire 39 | case mermaidLagoon 40 | case chocolateTruffle 41 | case neonNights 42 | case fieryEmbers 43 | case morningDew 44 | case starryNight 45 | case auroraBorealis 46 | case sunsetBlaze 47 | 48 | /// The name of the gradient template. 49 | public var name: String { 50 | rawValue.capitalized 51 | } 52 | 53 | /// The size of the gradient, representing both width and height in pixels. 54 | public var size: Int { 55 | 2 56 | } 57 | 58 | /// An array of 2D points that define the control points of the gradient. 59 | public var points: [SIMD2] { 60 | [ 61 | .init(x: 0.000, y: 0.000), 62 | .init(x: 1.000, y: 0.000), 63 | .init(x: 0.000, y: 1.000), 64 | .init(x: 1.000, y: 1.000) 65 | ] 66 | } 67 | 68 | /// An array of colors associated with the control points. 69 | public var colors: [Color] { 70 | switch self { 71 | case .mysticTwilight: 72 | return [ 73 | Color(hex: "#4B0082"), Color(hex: "#8A2BE2"), 74 | Color(hex: "#9400D3"), Color(hex: "#4169E1") 75 | ] 76 | case .tropicalParadise: 77 | return [ 78 | Color(hex: "#00FA9A"), Color(hex: "#1E90FF"), 79 | Color(hex: "#FFD700"), Color(hex: "#FF6347") 80 | ] 81 | case .cherryBlossom: 82 | return [ 83 | Color(hex: "#FFB7C5"), Color(hex: "#FF69B4"), 84 | Color(hex: "#FFC0CB"), Color(hex: "#DB7093") 85 | ] 86 | case .arcticFrost: 87 | return [ 88 | Color(hex: "#E0FFFF"), Color(hex: "#B0E0E6"), 89 | Color(hex: "#87CEEB"), Color(hex: "#4682B4") 90 | ] 91 | case .goldenSunrise: 92 | return [ 93 | Color(hex: "#FFA500"), Color(hex: "#FF8C00"), 94 | Color(hex: "#FF4500"), Color(hex: "#FF6347") 95 | ] 96 | case .emeraldForest: 97 | return [ 98 | Color(hex: "#00FF00"), Color(hex: "#32CD32"), 99 | Color(hex: "#008000"), Color(hex: "#006400") 100 | ] 101 | case .desertMirage: 102 | return [ 103 | Color(hex: "#DEB887"), Color(hex: "#D2691E"), 104 | Color(hex: "#CD853F"), Color(hex: "#8B4513") 105 | ] 106 | case .midnightGalaxy: 107 | return [ 108 | Color(hex: "#191970"), Color(hex: "#483D8B"), 109 | Color(hex: "#6A5ACD"), Color(hex: "#9370DB") 110 | ] 111 | case .autumnHarvest: 112 | return [ 113 | Color(hex: "#D2691E"), Color(hex: "#FF7F50"), 114 | Color(hex: "#CD5C5C"), Color(hex: "#8B0000") 115 | ] 116 | case .oceanBreeze: 117 | return [ 118 | Color(hex: "#00CED1"), Color(hex: "#20B2AA"), 119 | Color(hex: "#48D1CC"), Color(hex: "#40E0D0") 120 | ] 121 | case .lavenderDreams: 122 | return [ 123 | Color(hex: "#9370DB"), Color(hex: "#8A2BE2"), 124 | Color(hex: "#9932CC"), Color(hex: "#BA55D3") 125 | ] 126 | case .citrusBurst: 127 | return [ 128 | Color(hex: "#FFD700"), Color(hex: "#FFA500"), 129 | Color(hex: "#FF8C00"), Color(hex: "#FF7F50") 130 | ] 131 | case .northernLights: 132 | return [ 133 | Color(hex: "#00FF00"), Color(hex: "#00FFFF"), 134 | Color(hex: "#FF00FF"), Color(hex: "#4B0082") 135 | ] 136 | case .strawberryLemonade: 137 | return [ 138 | Color(hex: "#FFB6C1"), Color(hex: "#FFC0CB"), 139 | Color(hex: "#FAFAD2"), Color(hex: "#FFFFE0") 140 | ] 141 | case .deepSea: 142 | return [ 143 | Color(hex: "#191970"), Color(hex: "#00008B"), 144 | Color(hex: "#0000CD"), Color(hex: "#4169E1") 145 | ] 146 | case .cottonCandy: 147 | return [ 148 | Color(hex: "#FF69B4"), Color(hex: "#FFB6C1"), 149 | Color(hex: "#E6E6FA"), Color(hex: "#B0E0E6") 150 | ] 151 | case .volcanicAsh: 152 | return [ 153 | Color(hex: "#2F4F4F"), Color(hex: "#696969"), 154 | Color(hex: "#778899"), Color(hex: "#A9A9A9") 155 | ] 156 | case .springMeadow: 157 | return [ 158 | Color(hex: "#98FB98"), Color(hex: "#00FA9A"), 159 | Color(hex: "#7FFF00"), Color(hex: "#32CD32") 160 | ] 161 | case .cosmicDust: 162 | return [ 163 | Color(hex: "#4B0082"), Color(hex: "#8A2BE2"), 164 | Color(hex: "#9932CC"), Color(hex: "#E6E6FA") 165 | ] 166 | case .peacockFeathers: 167 | return [ 168 | Color(hex: "#1E90FF"), Color(hex: "#00CED1"), 169 | Color(hex: "#20B2AA"), Color(hex: "#008080") 170 | ] 171 | case .crimsonSunset: 172 | return [ 173 | Color(hex: "#FF4500"), Color(hex: "#FF6347"), 174 | Color(hex: "#FF7F50"), Color(hex: "#FFA07A") 175 | ] 176 | case .enchantedForest: 177 | return [ 178 | Color(hex: "#006400"), Color(hex: "#008000"), 179 | Color(hex: "#2E8B57"), Color(hex: "#3CB371") 180 | ] 181 | case .blueberryMuffin: 182 | return [ 183 | Color(hex: "#1E90FF"), Color(hex: "#6495ED"), 184 | Color(hex: "#87CEFA"), Color(hex: "#B0E0E6") 185 | ] 186 | case .saharaDunes: 187 | return [ 188 | Color(hex: "#D2691E"), Color(hex: "#CD853F"), 189 | Color(hex: "#DEB887"), Color(hex: "#FFDAB9") 190 | ] 191 | case .grapeSoda: 192 | return [ 193 | Color(hex: "#4B0082"), Color(hex: "#8A2BE2"), 194 | Color(hex: "#9932CC"), Color(hex: "#BA55D3") 195 | ] 196 | case .frostyWinter: 197 | return [ 198 | Color(hex: "#E0FFFF"), Color(hex: "#B0E0E6"), 199 | Color(hex: "#AFEEEE"), Color(hex: "#E6E6FA") 200 | ] 201 | case .dragonFire: 202 | return [ 203 | Color(hex: "#FF4500"), Color(hex: "#FF6347"), 204 | Color(hex: "#FF7F50"), Color(hex: "#FFA500") 205 | ] 206 | case .mermaidLagoon: 207 | return [ 208 | Color(hex: "#00CED1"), Color(hex: "#48D1CC"), 209 | Color(hex: "#40E0D0"), Color(hex: "#7FFFD4") 210 | ] 211 | case .chocolateTruffle: 212 | return [ 213 | Color(hex: "#8B4513"), Color(hex: "#A0522D"), 214 | Color(hex: "#CD853F"), Color(hex: "#D2691E") 215 | ] 216 | case .neonNights: 217 | return [ 218 | Color(hex: "#FF00FF"), Color(hex: "#00FFFF"), 219 | Color(hex: "#FF1493"), Color(hex: "#00FF00") 220 | ] 221 | case .fieryEmbers: 222 | return [ 223 | Color(hex: "#FF6347"), Color(hex: "#FF4500"), 224 | Color(hex: "#FF7F50"), Color(hex: "#FFA500") 225 | ] 226 | case .morningDew: 227 | return [ 228 | Color(hex: "#98FB98"), Color(hex: "#00FA9A"), 229 | Color(hex: "#7FFF00"), Color(hex: "#32CD32") 230 | ] 231 | case .starryNight: 232 | return [ 233 | Color(hex: "#191970"), Color(hex: "#483D8B"), 234 | Color(hex: "#6A5ACD"), Color(hex: "#9370DB") 235 | ] 236 | case .auroraBorealis: 237 | return [ 238 | Color(hex: "#00FF00"), Color(hex: "#00FFFF"), 239 | Color(hex: "#FF00FF"), Color(hex: "#4B0082") 240 | ] 241 | case .sunsetBlaze: 242 | return [ 243 | Color(hex: "#FF4500"), Color(hex: "#FF6347"), 244 | Color(hex: "#FF7F50"), Color(hex: "#FFA07A") 245 | ] 246 | } 247 | } 248 | 249 | /// The background color of the gradient. 250 | public var background: Color { 251 | switch self { 252 | case .mysticTwilight: 253 | return Color(hex: "#1A0033") 254 | case .tropicalParadise: 255 | return Color(hex: "#006644") 256 | case .cherryBlossom: 257 | return Color(hex: "#FFF0F5") 258 | case .arcticFrost: 259 | return Color(hex: "#F0FFFF") 260 | case .goldenSunrise: 261 | return Color(hex: "#FFD700") 262 | case .emeraldForest: 263 | return Color(hex: "#004D40") 264 | case .desertMirage: 265 | return Color(hex: "#F4A460") 266 | case .midnightGalaxy: 267 | return Color(hex: "#000033") 268 | case .autumnHarvest: 269 | return Color(hex: "#8B4513") 270 | case .oceanBreeze: 271 | return Color(hex: "#E0FFFF") 272 | case .lavenderDreams: 273 | return Color(hex: "#E6E6FA") 274 | case .citrusBurst: 275 | return Color(hex: "#FFF700") 276 | case .northernLights: 277 | return Color(hex: "#000033") 278 | case .strawberryLemonade: 279 | return Color(hex: "#FFFACD") 280 | case .deepSea: 281 | return Color(hex: "#000080") 282 | case .cottonCandy: 283 | return Color(hex: "#FFBCD9") 284 | case .volcanicAsh: 285 | return Color(hex: "#1C1C1C") 286 | case .springMeadow: 287 | return Color(hex: "#90EE90") 288 | case .cosmicDust: 289 | return Color(hex: "#2D2D2D") 290 | case .peacockFeathers: 291 | return Color(hex: "#00A86B") 292 | case .crimsonSunset: 293 | return Color(hex: "#DC143C") 294 | case .enchantedForest: 295 | return Color(hex: "#228B22") 296 | case .blueberryMuffin: 297 | return Color(hex: "#4169E1") 298 | case .saharaDunes: 299 | return Color(hex: "#F4A460") 300 | case .grapeSoda: 301 | return Color(hex: "#8E4585") 302 | case .frostyWinter: 303 | return Color(hex: "#F0F8FF") 304 | case .dragonFire: 305 | return Color(hex: "#8B0000") 306 | case .mermaidLagoon: 307 | return Color(hex: "#20B2AA") 308 | case .chocolateTruffle: 309 | return Color(hex: "#3C2A21") 310 | case .neonNights: 311 | return Color(hex: "#000000") 312 | case .fieryEmbers: 313 | return Color(hex: "#FF6347") 314 | case .morningDew: 315 | return Color(hex: "#98FB98") 316 | case .starryNight: 317 | return Color(hex: "#191970") 318 | case .auroraBorealis: 319 | return Color(hex: "#000033") 320 | case .sunsetBlaze: 321 | return Color(hex: "#FF4500") 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /Sources/MeshingKit/AnimatedMeshGradientView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatedMeshGradientView.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 10/19/24. 6 | // 7 | 8 | import SwiftUI 9 | import simd 10 | 11 | /// Animation constants for mesh gradient animations. 12 | private enum AnimationConstants { 13 | /// Animation frame rate (frames per second). 14 | static let frameRate: Double = 120 15 | 16 | // Grid size 3 animation constants 17 | enum GridSize3 { 18 | static let centerX: Float = 0.5 19 | static let centerY: Float = 0.5 20 | static let amplitude1: Float = 0.4 21 | static let amplitude2: Float = 0.3 22 | static let amplitude3: Float = 0.2 23 | static let frequency1: Double = 1.0 24 | static let frequency2: Double = 1.1 25 | static let frequency3: Double = 0.9 26 | static let frequency4: Double = 0.7 27 | static let frequency5: Double = 1.2 28 | } 29 | 30 | // Grid size 4 animation constants 31 | enum GridSize4 { 32 | static let phaseDivider: Double = 2.0 33 | static let position1: Float = 0.33 34 | static let position2: Float = 0.67 35 | static let position3: Float = 0.37 36 | static let edgeAmplitude: Float = 0.1 37 | static let innerAmplitude: Float = 0.15 38 | static let frequency1: Double = 0.7 39 | static let frequency2: Double = 0.8 40 | static let frequency3: Double = 0.9 41 | static let frequency4: Double = 0.6 42 | static let frequency5: Double = 1.2 43 | static let frequency6: Double = 1.3 44 | static let frequency7: Double = 1.4 45 | static let frequency8: Double = 1.5 46 | static let frequency9: Double = 1.0 47 | static let frequency10: Double = 1.1 48 | } 49 | } 50 | 51 | /// A view that displays an animated mesh gradient. 52 | public struct AnimatedMeshGradientView: View { 53 | /// The size of the gradient grid (e.g., 3 for a 3x3 grid). 54 | var gridSize: Int 55 | 56 | /// A binding that controls whether the animation is currently playing. 57 | @Binding var showAnimation: Bool 58 | 59 | /// An array of 2D points that define the control points of the gradient. 60 | /// 61 | /// Each point is represented as a `SIMD2` where coordinates range from 0.0 to 1.0. 62 | var positions: [SIMD2] 63 | 64 | /// An array of colors associated with the control points. 65 | /// 66 | /// The colors in this array correspond to the points in the `positions` array. 67 | var colors: [Color] 68 | 69 | /// The background color of the gradient. 70 | /// 71 | /// This color is used as the base color for areas not directly affected by the control points. 72 | var background: Color 73 | 74 | /// The speed multiplier for the animation. 75 | /// 76 | /// A value of 1.0 represents normal speed, 2.0 is twice as fast, and 0.5 is half speed. 77 | var animationSpeed: Double 78 | 79 | /// Optional animation pattern for point-based animations. 80 | /// 81 | /// When provided, this pattern is applied to `positions` each frame. 82 | var animationPattern: AnimationPattern? 83 | 84 | /// Whether the gradient should smooth between colors. 85 | /// 86 | /// Defaults to `true` for softer transitions. 87 | var smoothsColors: Bool 88 | 89 | /// Creates a new animated mesh gradient view with the specified parameters. 90 | /// 91 | /// - Parameters: 92 | /// - gridSize: The size of the gradient grid (e.g., 3 for a 3x3 grid). 93 | /// - showAnimation: A binding that controls whether the animation is currently playing. 94 | /// - positions: An array of 2D points that define the control points of the gradient. 95 | /// - colors: An array of colors associated with the control points. 96 | /// - background: The background color of the gradient. 97 | /// - animationSpeed: The speed multiplier for the animation (default: 1.0). 98 | /// - animationPattern: Optional custom animation pattern to apply. 99 | /// - smoothsColors: Whether the gradient should smooth between colors (default: `true`). 100 | public init( 101 | gridSize: Int, 102 | showAnimation: Binding, 103 | positions: [SIMD2], 104 | colors: [Color], 105 | background: Color, 106 | animationSpeed: Double = 1.0, 107 | animationPattern: AnimationPattern? = nil, 108 | smoothsColors: Bool = true 109 | ) { 110 | self.gridSize = gridSize 111 | self._showAnimation = showAnimation 112 | self.positions = positions 113 | self.colors = colors 114 | self.background = background 115 | self.animationSpeed = animationSpeed 116 | self.animationPattern = animationPattern 117 | self.smoothsColors = smoothsColors 118 | } 119 | 120 | /// The body of the view, displaying an animated mesh gradient. 121 | public var body: some View { 122 | TimelineView( 123 | .animation(minimumInterval: 1 / AnimationConstants.frameRate, paused: !showAnimation) 124 | ) { phase in 125 | MeshGradient( 126 | width: gridSize, 127 | height: gridSize, 128 | locations: .points(animatedPositions(for: phase.date)), 129 | colors: .colors(colors), 130 | background: background, 131 | smoothsColors: smoothsColors 132 | ) 133 | .ignoresSafeArea() 134 | } 135 | } 136 | 137 | private func animatedPositions(for date: Date) -> [SIMD2] { 138 | let adjustedTimeInterval = 139 | date.timeIntervalSinceReferenceDate * animationSpeed 140 | 141 | if let animationPattern, gridSize >= 3 { 142 | let animated = animationPattern.apply(to: positions, at: adjustedTimeInterval) 143 | return clampedToUnitSquare(animated) 144 | } 145 | 146 | switch gridSize { 147 | case 3: 148 | return animatedPositionsForGridSize3( 149 | phase: adjustedTimeInterval, positions: positions) 150 | case 4: 151 | return animatedPositionsForGridSize4( 152 | phase: adjustedTimeInterval, positions: positions) 153 | default: 154 | return positions 155 | } 156 | } 157 | 158 | private func animatedPositionsForGridSize3( 159 | phase: Double, positions: [SIMD2] 160 | ) -> [SIMD2] { 161 | guard positions.count >= 9 else { return positions } 162 | var animatedPositions = positions 163 | 164 | animatedPositions[1].x = AnimationConstants.GridSize3.centerX 165 | + AnimationConstants.GridSize3.amplitude1 166 | * Float(cos(phase * AnimationConstants.GridSize3.frequency1)) 167 | animatedPositions[3].y = AnimationConstants.GridSize3.centerY 168 | + AnimationConstants.GridSize3.amplitude2 169 | * Float(cos(phase * AnimationConstants.GridSize3.frequency2)) 170 | animatedPositions[4].y = AnimationConstants.GridSize3.centerY 171 | - AnimationConstants.GridSize3.amplitude1 172 | * Float(cos(phase * AnimationConstants.GridSize3.frequency3)) 173 | animatedPositions[4].x = AnimationConstants.GridSize3.centerX 174 | + AnimationConstants.GridSize3.amplitude3 175 | * Float(cos(phase * AnimationConstants.GridSize3.frequency4)) 176 | animatedPositions[5].y = AnimationConstants.GridSize3.centerY 177 | - AnimationConstants.GridSize3.amplitude3 178 | * Float(cos(phase * AnimationConstants.GridSize3.frequency3)) 179 | animatedPositions[7].x = AnimationConstants.GridSize3.centerX 180 | - AnimationConstants.GridSize3.amplitude1 181 | * Float(cos(phase * AnimationConstants.GridSize3.frequency5)) 182 | 183 | return animatedPositions 184 | } 185 | 186 | private func animatedPositionsForGridSize4( 187 | phase: Double, positions: [SIMD2] 188 | ) -> [SIMD2] { 189 | guard positions.count >= 16 else { return positions } 190 | let adjustedPhase = phase / AnimationConstants.GridSize4.phaseDivider 191 | var animatedPositions = positions 192 | 193 | animateGridSize4Edges(&animatedPositions, phase: adjustedPhase) 194 | animateGridSize4InnerPoints(&animatedPositions, phase: adjustedPhase) 195 | 196 | return animatedPositions 197 | } 198 | 199 | private func animateGridSize4Edges( 200 | _ positions: inout [SIMD2], phase: Double 201 | ) { 202 | // Top edge 203 | positions[1].x = AnimationConstants.GridSize4.position1 204 | + AnimationConstants.GridSize4.edgeAmplitude 205 | * Float(cos(phase * AnimationConstants.GridSize4.frequency1)) 206 | positions[2].x = AnimationConstants.GridSize4.position2 207 | - AnimationConstants.GridSize4.edgeAmplitude 208 | * Float(cos(phase * AnimationConstants.GridSize4.frequency2)) 209 | // Left edge 210 | positions[4].y = AnimationConstants.GridSize4.position1 211 | + AnimationConstants.GridSize4.edgeAmplitude 212 | * Float(cos(phase * AnimationConstants.GridSize4.frequency3)) 213 | positions[7].y = AnimationConstants.GridSize4.position3 214 | - AnimationConstants.GridSize4.edgeAmplitude 215 | * Float(cos(phase * AnimationConstants.GridSize4.frequency4)) 216 | // Bottom edge 217 | positions[11].y = AnimationConstants.GridSize4.position2 218 | - AnimationConstants.GridSize4.edgeAmplitude 219 | * Float(cos(phase * AnimationConstants.GridSize4.frequency5)) 220 | // Right edge 221 | positions[13].x = AnimationConstants.GridSize4.position1 222 | + AnimationConstants.GridSize4.edgeAmplitude 223 | * Float(cos(phase * AnimationConstants.GridSize4.frequency6)) 224 | positions[14].x = AnimationConstants.GridSize4.position2 225 | - AnimationConstants.GridSize4.edgeAmplitude 226 | * Float(cos(phase * AnimationConstants.GridSize4.frequency7)) 227 | } 228 | 229 | private func animateGridSize4InnerPoints( 230 | _ positions: inout [SIMD2], phase: Double 231 | ) { 232 | positions[5].x = AnimationConstants.GridSize4.position1 233 | + AnimationConstants.GridSize4.innerAmplitude 234 | * Float(cos(phase * AnimationConstants.GridSize4.frequency2)) 235 | positions[5].y = AnimationConstants.GridSize4.position1 236 | + AnimationConstants.GridSize4.innerAmplitude 237 | * Float(cos(phase * AnimationConstants.GridSize4.frequency3)) 238 | positions[6].x = AnimationConstants.GridSize4.position2 239 | - AnimationConstants.GridSize4.innerAmplitude 240 | * Float(cos(phase * AnimationConstants.GridSize4.frequency9)) 241 | positions[6].y = AnimationConstants.GridSize4.position1 242 | + AnimationConstants.GridSize4.innerAmplitude 243 | * Float(cos(phase * AnimationConstants.GridSize4.frequency10)) 244 | positions[9].x = AnimationConstants.GridSize4.position1 245 | + AnimationConstants.GridSize4.innerAmplitude 246 | * Float(cos(phase * AnimationConstants.GridSize4.frequency5)) 247 | positions[9].y = AnimationConstants.GridSize4.position2 248 | - AnimationConstants.GridSize4.innerAmplitude 249 | * Float(cos(phase * AnimationConstants.GridSize4.frequency6)) 250 | positions[10].x = AnimationConstants.GridSize4.position2 251 | - AnimationConstants.GridSize4.innerAmplitude 252 | * Float(cos(phase * AnimationConstants.GridSize4.frequency7)) 253 | positions[10].y = AnimationConstants.GridSize4.position2 254 | - AnimationConstants.GridSize4.innerAmplitude 255 | * Float(cos(phase * AnimationConstants.GridSize4.frequency8)) 256 | } 257 | 258 | private func clampedToUnitSquare(_ positions: [SIMD2]) -> [SIMD2] { 259 | let lowerBound = SIMD2.zero 260 | let upperBound = SIMD2(repeating: 1.0) 261 | return positions.map { point in 262 | simd_clamp(point, lowerBound, upperBound) 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MeshingKit 2 | 3 | ![Gradient](Sources/Resources/gradient.jpg) 4 | 5 | ![Swift](https://img.shields.io/badge/Swift-6.2-orange.svg) 6 | ![Build Status](https://github.com/rryam/MeshingKit/workflows/Build/badge.svg) 7 | ![Platforms](https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS%20%7C%20tvOS%20%7C%20watchOS%20%7C%20visionOS-blue.svg) 8 | ![License](https://img.shields.io/badge/License-MIT-green.svg) 9 | ![SPM](https://img.shields.io/badge/SPM-compatible-brightgreen.svg) 10 | 11 | MeshingKit provides an easy way to create mesh gradients in SwiftUI with predefined gradient templates to directly render beautiful, gorgeous gradients! 12 | 13 | ## Support 14 | 15 | Love this project? Check out my books to explore more of AI and iOS development: 16 | - [Exploring AI for iOS Development](https://academy.rudrank.com/product/ai) 17 | - [Exploring AI-Assisted Coding for iOS Development](https://academy.rudrank.com/product/ai-assisted-coding) 18 | 19 | Your support helps to keep this project growing! 20 | 21 | ## Meshing 22 | 23 | MeshingKit is based on [Meshing](https://apps.apple.com/in/app/ai-mesh-gradient-tool-meshing/id6567933550), an AI Mesh Gradient Tool. 24 | 25 | ## Features 26 | 27 | - Create beautiful mesh gradients with customizable control points and colors 28 | - Animate gradients with smooth, configurable transitions 29 | - 68 predefined gradient templates: 30 | - 35 templates with 2x2 grid size 31 | - 22 templates with 3x3 grid size 32 | - 11 templates with 4x4 grid size 33 | - Easily extendable for custom gradients 34 | - Works across all Apple platforms (iOS, macOS, tvOS, watchOS, visionOS) 35 | 36 | ## Requirements 37 | 38 | - iOS 18.0+, macOS 15.0+, tvOS 18.0+, watchOS 11.0+, visionOS 2.0+ 39 | - Swift 6.2+ 40 | - Xcode 16.0+ 41 | 42 | ## Installation 43 | 44 | ### Swift Package Manager 45 | 46 | Add MeshingKit to your project using Swift Package Manager. In Xcode, go to File > Swift Packages > Add Package Dependency and enter the following URL: 47 | 48 | ```swift 49 | dependencies: [ 50 | .package(url: "https://github.com/rryam/MeshingKit.git", from: "2.4.0") 51 | ] 52 | ``` 53 | 54 | ## Usage 55 | 56 | To use a predefined gradient template: 57 | 58 | ```swift 59 | import SwiftUI 60 | import MeshingKit 61 | 62 | struct ContentView: View { 63 | var body: some View { 64 | // Using PredefinedTemplate enum (recommended) 65 | MeshingKit.gradient(template: .size3(.cosmicAurora)) 66 | .frame(width: 300, height: 300) 67 | 68 | // Or using specific size methods 69 | MeshingKit.gradientSize3(template: .cosmicAurora) 70 | .frame(width: 300, height: 300) 71 | } 72 | } 73 | ``` 74 | 75 | ### Using PredefinedTemplate Enum 76 | 77 | The `PredefinedTemplate` enum provides a unified way to access all gradient templates: 78 | 79 | ```swift 80 | let gradient = MeshingKit.gradient(template: .size2(.mysticTwilight)) 81 | let gradient3 = MeshingKit.gradient(template: .size3(.auroraBorealis)) 82 | let gradient4 = MeshingKit.gradient(template: .size4(.cosmicNebula)) 83 | ``` 84 | 85 | ## Animated Gradient Views 86 | 87 | To create an animated gradient view: 88 | 89 | ```swift 90 | import SwiftUI 91 | import MeshingKit 92 | 93 | struct AnimatedGradientView: View { 94 | @State private var showAnimation = true 95 | 96 | var body: some View { 97 | MeshingKit.animatedGradient( 98 | .size3(.cosmicAurora), 99 | showAnimation: $showAnimation, 100 | animationSpeed: 1.5 101 | ) 102 | .frame(width: 300, height: 300) 103 | .padding() 104 | 105 | // Toggle animation 106 | Toggle("Animate Gradient", isOn: $showAnimation) 107 | .padding() 108 | } 109 | } 110 | ``` 111 | 112 | > **Note:** Animation is only available for 3x3 and 4x4 grid templates. 2x2 templates cannot be animated because all four points are corner points that must remain fixed at the edges of the gradient. 113 | 114 | ## Custom Animation Patterns 115 | 116 | MeshingKit provides advanced animation control through `AnimationPattern` and `PointAnimation` structures: 117 | 118 | ```swift 119 | import SwiftUI 120 | import MeshingKit 121 | 122 | struct CustomAnimationView: View { 123 | @State private var showAnimation = true 124 | 125 | var body: some View { 126 | // Use default animation pattern 127 | MeshingKit.animatedGradient( 128 | .size3(.cosmicAurora), 129 | showAnimation: $showAnimation, 130 | animationSpeed: 1.0 131 | ) 132 | .frame(width: 300, height: 300) 133 | } 134 | } 135 | ``` 136 | 137 | ### Creating Custom Animation Patterns 138 | 139 | You can create custom animations by defining specific point movements: 140 | 141 | ```swift 142 | // Create custom point animations 143 | let pointAnimations = [ 144 | PointAnimation(pointIndex: 1, axis: .x, amplitude: 0.3, frequency: 1.2), 145 | PointAnimation(pointIndex: 4, axis: .both, amplitude: 0.2, frequency: 0.8), 146 | PointAnimation(pointIndex: 7, axis: .y, amplitude: -0.4, frequency: 1.5) 147 | ] 148 | 149 | let customPattern = AnimationPattern(animations: pointAnimations) 150 | 151 | // Apply the pattern to an animated gradient 152 | MeshingKit.animatedGradient( 153 | .size3(.cosmicAurora), 154 | showAnimation: $showAnimation, 155 | animationSpeed: 1.0, 156 | animationPattern: customPattern 157 | ) 158 | ``` 159 | 160 | **Animation Parameters:** 161 | - `pointIndex`: Index of the point to animate in the gradient's position array 162 | - `axis`: Which axis to animate (`.x`, `.y`, or `.both`) 163 | - `amplitude`: How far the point moves from its original position 164 | - `frequency`: Speed multiplier for the animation (default: 1.0) 165 | 166 | ## Noise Effect with Gradients 167 | 168 | You can add a noise effect to your gradients using the ParameterizedNoiseView: 169 | 170 | ```swift 171 | import SwiftUI 172 | import MeshingKit 173 | 174 | struct NoiseEffectGradientView: View { 175 | @State private var intensity: Float = 0.5 176 | @State private var frequency: Float = 0.2 177 | @State private var opacity: Float = 0.9 178 | 179 | var body: some View { 180 | ParameterizedNoiseView(intensity: $intensity, frequency: $frequency, opacity: $opacity) { 181 | MeshingKit.gradientSize3(template: .cosmicAurora) 182 | } 183 | .frame(width: 300, height: 300) 184 | 185 | // Controls for adjusting the noise effect 186 | VStack { 187 | Slider(value: $intensity, in: 0...1) { 188 | Text("Intensity") 189 | } 190 | .padding() 191 | 192 | Slider(value: $frequency, in: 0...1) { 193 | Text("Frequency") 194 | } 195 | .padding() 196 | 197 | Slider(value: $opacity, in: 0...1) { 198 | Text("Opacity") 199 | } 200 | .padding() 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | ## Available Gradient Templates 207 | 208 | MeshingKit provides 68 predefined gradient templates organized by grid size: 209 | 210 | ### Exploring Templates Programmatically 211 | 212 | You can explore all available templates using the `CaseIterable` conformance: 213 | 214 | ```swift 215 | // List all 3x3 templates 216 | for template in GradientTemplateSize3.allCases { 217 | print(template.name) 218 | } 219 | 220 | // Get total count of templates for each size 221 | let size2Count = GradientTemplateSize2.allCases.count 222 | let size3Count = GradientTemplateSize3.allCases.count 223 | let size4Count = GradientTemplateSize4.allCases.count 224 | ``` 225 | 226 | ### Searching Templates 227 | 228 | You can search across template names, tags, and moods using `PredefinedTemplate.find(by:)`: 229 | 230 | ```swift 231 | // Find templates by keyword 232 | let matches = PredefinedTemplate.find(by: "aurora") 233 | 234 | // Inspect metadata 235 | if let first = matches.first { 236 | print(first.tags) 237 | print(first.moods) 238 | print(first.palette) 239 | } 240 | ``` 241 | 242 | ### Popular Template Examples 243 | 244 | **2x2 Grid Templates (35 total):** 245 | - mysticTwilight, tropicalParadise, cherryBlossom, arcticFrost 246 | - goldenSunrise, emeraldForest, desertMirage, midnightGalaxy 247 | - autumnHarvest 248 | 249 | **3x3 Grid Templates (22 total):** 250 | - intelligence, auroraBorealis, sunsetGlow, oceanDepths 251 | - neonNight, autumnLeaves, cosmicAurora, lavaFlow 252 | - etherealMist, tropicalParadise, midnightGalaxy, desertMirage 253 | - frostedCrystal, enchantedForest, rubyFusion, goldenSunrise 254 | - cosmicNebula, arcticAurora, volcanicEmber, mintBreeze 255 | - twilightSerenade, saharaDunes 256 | 257 | **4x4 Grid Templates (11 total):** 258 | - auroraBorealis, sunsetHorizon, mysticForest, cosmicNebula 259 | - coralReef, etherealTwilight, volcanicOasis, arcticFrost 260 | - jungleMist, desertMirage, neonMetropolis 261 | 262 | ### Finding Templates by Name 263 | 264 | Since templates follow `camelCase` naming, you can easily find them: 265 | 266 | ```swift 267 | // Create a gradient from any template name 268 | let template = GradientTemplateSize3.auroraBorealis 269 | let gradient = MeshingKit.gradient(template: template) 270 | ``` 271 | 272 | ## Custom Gradients 273 | 274 | Create custom gradients by defining your own `GradientTemplate`: 275 | 276 | ```swift 277 | let customTemplate = CustomGradientTemplate( 278 | name: "Custom Gradient", 279 | size: 3, 280 | points: [ 281 | .init(x: 0.0, y: 0.0), .init(x: 0.5, y: 0.0), .init(x: 1.0, y: 0.0), 282 | .init(x: 0.0, y: 0.5), .init(x: 0.5, y: 0.5), .init(x: 1.0, y: 0.5), 283 | .init(x: 0.0, y: 1.0), .init(x: 0.5, y: 1.0), .init(x: 1.0, y: 1.0) 284 | ], 285 | colors: [ 286 | Color.red, Color.orange, Color.yellow, 287 | Color.green, Color.blue, Color.indigo, 288 | Color.purple, Color.pink, Color.white 289 | ], 290 | background: Color.black 291 | ) 292 | 293 | let customGradient = MeshingKit.gradient(template: customTemplate) 294 | ``` 295 | 296 | ## Advanced Animation Examples 297 | 298 | ### Speed Control and Pausing 299 | 300 | ```swift 301 | struct AdvancedAnimationView: View { 302 | @State private var showAnimation = true 303 | @State private var animationSpeed: Double = 1.0 304 | 305 | var body: some View { 306 | VStack { 307 | MeshingKit.animatedGradient( 308 | .size4(.cosmicNebula), 309 | showAnimation: $showAnimation, 310 | animationSpeed: animationSpeed 311 | ) 312 | .frame(width: 400, height: 400) 313 | 314 | // Animation controls 315 | VStack { 316 | Toggle("Enable Animation", isOn: $showAnimation) 317 | 318 | Slider(value: $animationSpeed, in: 0.1...3.0) { 319 | Text("Animation Speed: \(animationSpeed, specifier: "%.1f")x") 320 | } 321 | } 322 | .padding() 323 | } 324 | } 325 | } 326 | ``` 327 | 328 | ### Combining Animation with Noise Effects 329 | 330 | ```swift 331 | struct AnimatedNoiseGradientView: View { 332 | @State private var showAnimation = true 333 | @State private var intensity: Float = 0.3 334 | @State private var frequency: Float = 0.2 335 | 336 | var body: some View { 337 | ParameterizedNoiseView( 338 | intensity: $intensity, 339 | frequency: $frequency, 340 | opacity: .constant(0.8) 341 | ) { 342 | MeshingKit.animatedGradient( 343 | .size3(.auroraBorealis), 344 | showAnimation: $showAnimation, 345 | animationSpeed: 1.2 346 | ) 347 | } 348 | .frame(width: 300, height: 300) 349 | } 350 | } 351 | ``` 352 | 353 | ## Hex Color Initialization 354 | 355 | There is an extension on `Color` that allows to initialise colors using hexadecimal strings: 356 | 357 | ```swift 358 | let color = Color(hex: "#FF5733") 359 | ``` 360 | 361 | This extension supports various hex formats: 362 | 363 | - "#RGB" (12-bit) 364 | - "#RRGGBB" (24-bit) 365 | - "#AARRGGBB" (32-bit with alpha) 366 | 367 | ## Export Helpers 368 | 369 | MeshingKit includes helpers to export previews and snippets for design tools: 370 | 371 | ```swift 372 | // Snapshot a mesh gradient (CGImage) 373 | let image = MeshingKit.snapshotCGImage( 374 | template: GradientTemplateSize3.auroraBorealis, 375 | size: CGSize(width: 600, height: 600) 376 | ) 377 | 378 | // Generate SwiftUI Gradient.Stop snippet 379 | let swiftUIStops = MeshingKit.swiftUIStopsSnippet( 380 | template: GradientTemplateSize3.auroraBorealis 381 | ) 382 | 383 | // Generate CSS linear-gradient preview 384 | let css = MeshingKit.cssLinearGradientSnippet( 385 | template: GradientTemplateSize3.auroraBorealis 386 | ) 387 | ``` 388 | 389 | ## Contributing 390 | 391 | Contributions to MeshingKit are welcome! Please feel free to submit a Pull Request. 392 | 393 | ## License 394 | 395 | MeshingKit is available under the MIT license. See the LICENSE file for more info. 396 | -------------------------------------------------------------------------------- /Sources/MeshingKit/GradientTemplateSize4.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientTemplateSize4.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 3/22/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration of predefined gradient templates with a 4x4 grid size. 11 | public enum GradientTemplateSize4: String, CaseIterable, GradientTemplate { 12 | case auroraBorealis 13 | case sunsetHorizon 14 | case mysticForest 15 | case cosmicNebula 16 | case coralReef 17 | case etherealTwilight 18 | case volcanicOasis 19 | case arcticFrost 20 | case jungleMist 21 | case desertMirage 22 | case neonMetropolis 23 | 24 | /// The name of the gradient template. 25 | public var name: String { 26 | rawValue.capitalized 27 | } 28 | 29 | /// The size of the gradient, representing both width and height in pixels. 30 | public var size: Int { 31 | 4 32 | } 33 | 34 | /// An array of 2D points that define the control points of the gradient. 35 | public var points: [SIMD2] { 36 | switch self { 37 | case .auroraBorealis: 38 | return [ 39 | .init(x: 0.000, y: 0.000), .init(x: 0.263, y: 0.000), 40 | .init(x: 0.680, y: 0.000), .init(x: 1.000, y: 0.000), 41 | .init(x: 0.000, y: 0.244), .init(x: 0.565, y: 0.340), 42 | .init(x: 0.815, y: 0.689), .init(x: 1.000, y: 0.147), 43 | .init(x: 0.000, y: 0.715), .init(x: 0.289, y: 0.418), 44 | .init(x: 0.594, y: 0.766), .init(x: 1.000, y: 0.650), 45 | .init(x: 0.000, y: 1.000), .init(x: 0.244, y: 1.000), 46 | .init(x: 0.672, y: 1.000), .init(x: 1.000, y: 1.000) 47 | ] 48 | case .sunsetHorizon: 49 | return [ 50 | .init(x: 0.000, y: 0.000), .init(x: 0.300, y: 0.000), 51 | .init(x: 0.700, y: 0.000), .init(x: 1.000, y: 0.000), 52 | .init(x: 0.000, y: 0.250), .init(x: 0.352, y: 0.641), 53 | .init(x: 0.609, y: 0.131), .init(x: 1.000, y: 0.200), 54 | .init(x: 0.000, y: 0.700), .init(x: 0.584, y: 0.764), 55 | .init(x: 0.790, y: 0.210), .init(x: 1.000, y: 0.750), 56 | .init(x: 0.000, y: 1.000), .init(x: 0.300, y: 1.000), 57 | .init(x: 0.700, y: 1.000), .init(x: 1.000, y: 1.000) 58 | ] 59 | case .mysticForest: 60 | return [ 61 | .init(x: 0.000, y: 0.000), .init(x: 0.350, y: 0.000), 62 | .init(x: 0.650, y: 0.000), .init(x: 1.000, y: 0.000), 63 | .init(x: 0.000, y: 0.400), .init(x: 0.181, y: 0.471), 64 | .init(x: 0.882, y: 0.225), .init(x: 1.000, y: 0.300), 65 | .init(x: 0.000, y: 0.600), .init(x: 0.290, y: 0.546), 66 | .init(x: 0.634, y: 0.238), .init(x: 1.000, y: 0.861), 67 | .init(x: 0.000, y: 1.000), .init(x: 0.350, y: 1.000), 68 | .init(x: 0.650, y: 1.000), .init(x: 1.000, y: 1.000) 69 | ] 70 | case .cosmicNebula: 71 | return [ 72 | .init(x: 0.000, y: 0.000), .init(x: 0.200, y: 0.000), 73 | .init(x: 0.800, y: 0.000), .init(x: 1.000, y: 0.000), 74 | .init(x: 0.000, y: 0.447), .init(x: 0.253, y: 0.317), 75 | .init(x: 0.300, y: 0.175), .init(x: 1.000, y: 0.404), 76 | .init(x: 0.000, y: 0.520), .init(x: 0.459, y: 0.666), 77 | .init(x: 0.741, y: 0.429), .init(x: 1.000, y: 0.784), 78 | .init(x: 0.000, y: 1.000), .init(x: 0.465, y: 1.000), 79 | .init(x: 0.616, y: 1.000), .init(x: 1.000, y: 1.000) 80 | ] 81 | case .coralReef: 82 | return [ 83 | .init(x: 0.000, y: 0.000), .init(x: 0.400, y: 0.000), 84 | .init(x: 0.600, y: 0.000), .init(x: 1.000, y: 0.000), 85 | .init(x: 0.000, y: 0.300), .init(x: 0.708, y: 0.589), 86 | .init(x: 0.844, y: 0.343), .init(x: 1.000, y: 0.400), 87 | .init(x: 0.000, y: 0.700), .init(x: 0.232, y: 0.362), 88 | .init(x: 0.716, y: 0.892), .init(x: 1.000, y: 0.600), 89 | .init(x: 0.000, y: 1.000), .init(x: 0.400, y: 1.000), 90 | .init(x: 0.600, y: 1.000), .init(x: 1.000, y: 1.000) 91 | ] 92 | case .etherealTwilight: 93 | return [ 94 | .init(x: 0.000, y: 0.000), .init(x: 0.333, y: 0.000), 95 | .init(x: 0.667, y: 0.000), .init(x: 1.000, y: 0.000), 96 | .init(x: 0.000, y: 0.333), .init(x: 0.421, y: 0.512), 97 | .init(x: 0.739, y: 0.187), .init(x: 1.000, y: 0.333), 98 | .init(x: 0.000, y: 0.667), .init(x: 0.176, y: 0.845), 99 | .init(x: 0.623, y: 0.401), .init(x: 1.000, y: 0.667), 100 | .init(x: 0.000, y: 1.000), .init(x: 0.333, y: 1.000), 101 | .init(x: 0.667, y: 1.000), .init(x: 1.000, y: 1.000) 102 | ] 103 | case .volcanicOasis: 104 | return [ 105 | .init(x: 0.000, y: 0.000), .init(x: 0.333, y: 0.000), 106 | .init(x: 0.667, y: 0.000), .init(x: 1.000, y: 0.000), 107 | .init(x: 0.000, y: 0.333), .init(x: 0.218, y: 0.456), 108 | .init(x: 0.789, y: 0.123), .init(x: 1.000, y: 0.333), 109 | .init(x: 0.000, y: 0.667), .init(x: 0.567, y: 0.901), 110 | .init(x: 0.345, y: 0.234), .init(x: 1.000, y: 0.667), 111 | .init(x: 0.000, y: 1.000), .init(x: 0.333, y: 1.000), 112 | .init(x: 0.667, y: 1.000), .init(x: 1.000, y: 1.000) 113 | ] 114 | case .arcticFrost: 115 | return [ 116 | .init(x: 0.000, y: 0.000), .init(x: 0.333, y: 0.000), 117 | .init(x: 0.667, y: 0.000), .init(x: 1.000, y: 0.000), 118 | .init(x: 0.000, y: 0.333), .init(x: 0.678, y: 0.543), 119 | .init(x: 0.234, y: 0.876), .init(x: 1.000, y: 0.333), 120 | .init(x: 0.000, y: 0.667), .init(x: 0.432, y: 0.321), 121 | .init(x: 0.901, y: 0.765), .init(x: 1.000, y: 0.667), 122 | .init(x: 0.000, y: 1.000), .init(x: 0.333, y: 1.000), 123 | .init(x: 0.667, y: 1.000), .init(x: 1.000, y: 1.000) 124 | ] 125 | case .jungleMist: 126 | return [ 127 | .init(x: 0.000, y: 0.000), .init(x: 0.333, y: 0.000), 128 | .init(x: 0.667, y: 0.000), .init(x: 1.000, y: 0.000), 129 | .init(x: 0.000, y: 0.333), .init(x: 0.123, y: 0.789), 130 | .init(x: 0.876, y: 0.432), .init(x: 1.000, y: 0.333), 131 | .init(x: 0.000, y: 0.667), .init(x: 0.654, y: 0.210), 132 | .init(x: 0.345, y: 0.678), .init(x: 1.000, y: 0.667), 133 | .init(x: 0.000, y: 1.000), .init(x: 0.333, y: 1.000), 134 | .init(x: 0.667, y: 1.000), .init(x: 1.000, y: 1.000) 135 | ] 136 | case .desertMirage: 137 | return [ 138 | .init(x: 0.000, y: 0.000), .init(x: 0.333, y: 0.000), 139 | .init(x: 0.667, y: 0.000), .init(x: 1.000, y: 0.000), 140 | .init(x: 0.000, y: 0.333), .init(x: 0.789, y: 0.234), 141 | .init(x: 0.456, y: 0.901), .init(x: 1.000, y: 0.333), 142 | .init(x: 0.000, y: 0.667), .init(x: 0.321, y: 0.567), 143 | .init(x: 0.765, y: 0.123), .init(x: 1.000, y: 0.667), 144 | .init(x: 0.000, y: 1.000), .init(x: 0.333, y: 1.000), 145 | .init(x: 0.667, y: 1.000), .init(x: 1.000, y: 1.000) 146 | ] 147 | case .neonMetropolis: 148 | return [ 149 | .init(x: 0.000, y: 0.000), .init(x: 0.333, y: 0.000), 150 | .init(x: 0.667, y: 0.000), .init(x: 1.000, y: 0.000), 151 | .init(x: 0.000, y: 0.333), .init(x: 0.543, y: 0.210), 152 | .init(x: 0.876, y: 0.789), .init(x: 1.000, y: 0.333), 153 | .init(x: 0.000, y: 0.667), .init(x: 0.234, y: 0.678), 154 | .init(x: 0.765, y: 0.345), .init(x: 1.000, y: 0.667), 155 | .init(x: 0.000, y: 1.000), .init(x: 0.333, y: 1.000), 156 | .init(x: 0.667, y: 1.000), .init(x: 1.000, y: 1.000) 157 | ] 158 | } 159 | } 160 | 161 | /// An array of colors associated with the control points. 162 | public var colors: [Color] { 163 | switch self { 164 | case .auroraBorealis: 165 | return [ 166 | Color(hex: "#00264d"), Color(hex: "#004080"), 167 | Color(hex: "#0059b3"), Color(hex: "#0073e6"), 168 | Color(hex: "#1a8cff"), Color(hex: "#4da6ff"), 169 | Color(hex: "#80bfff"), Color(hex: "#b3d9ff"), 170 | Color(hex: "#00ff80"), Color(hex: "#33ff99"), 171 | Color(hex: "#66ffb3"), Color(hex: "#99ffcc"), 172 | Color(hex: "#004d40"), Color(hex: "#00665c"), 173 | Color(hex: "#008577"), Color(hex: "#00a693") 174 | ] 175 | case .sunsetHorizon: 176 | return [ 177 | Color(hex: "#ff6600"), Color(hex: "#ff8533"), 178 | Color(hex: "#ffa366"), Color(hex: "#ffc199"), 179 | Color(hex: "#ffb3ba"), Color(hex: "#ff99a7"), 180 | Color(hex: "#ff8093"), Color(hex: "#ff6680"), 181 | Color(hex: "#ff4d6a"), Color(hex: "#ff3357"), 182 | Color(hex: "#ff1a44"), Color(hex: "#ff0030"), 183 | Color(hex: "#cc0026"), Color(hex: "#990026"), 184 | Color(hex: "#660026"), Color(hex: "#330026") 185 | ] 186 | case .mysticForest: 187 | return [ 188 | Color(hex: "#004d00"), Color(hex: "#006600"), 189 | Color(hex: "#008000"), Color(hex: "#009900"), 190 | Color(hex: "#00b300"), Color(hex: "#00cc00"), 191 | Color(hex: "#00e600"), Color(hex: "#00ff00"), 192 | Color(hex: "#33ff33"), Color(hex: "#66ff66"), 193 | Color(hex: "#99ff99"), Color(hex: "#ccffcc"), 194 | Color(hex: "#004000"), Color(hex: "#005900"), 195 | Color(hex: "#007300"), Color(hex: "#008c00") 196 | ] 197 | case .cosmicNebula: 198 | return [ 199 | Color(hex: "#1a1a33"), Color(hex: "#33334d"), 200 | Color(hex: "#4d4d66"), Color(hex: "#666680"), 201 | Color(hex: "#8080b3"), Color(hex: "#9999cc"), 202 | Color(hex: "#b3b3e6"), Color(hex: "#ccccff"), 203 | Color(hex: "#ff99ff"), Color(hex: "#ff66ff"), 204 | Color(hex: "#ff33ff"), Color(hex: "#ff00ff"), 205 | Color(hex: "#cc00cc"), Color(hex: "#990099"), 206 | Color(hex: "#660066"), Color(hex: "#330033") 207 | ] 208 | case .coralReef: 209 | return [ 210 | Color(hex: "#004d66"), Color(hex: "#006680"), 211 | Color(hex: "#008099"), Color(hex: "#0099b3"), 212 | Color(hex: "#00b3cc"), Color(hex: "#00cce6"), 213 | Color(hex: "#00e6ff"), Color(hex: "#1affff"), 214 | Color(hex: "#ff6666"), Color(hex: "#ff8080"), 215 | Color(hex: "#ff9999"), Color(hex: "#ffb3b3"), 216 | Color(hex: "#ffcc00"), Color(hex: "#ffe600"), 217 | Color(hex: "#ffff1a"), Color(hex: "#ffff4d") 218 | ] 219 | case .etherealTwilight: 220 | return [ 221 | Color(hex: "#2e0059"), Color(hex: "#420080"), 222 | Color(hex: "#5600a6"), Color(hex: "#6a00cc"), 223 | Color(hex: "#7f00f2"), Color(hex: "#9933ff"), 224 | Color(hex: "#b366ff"), Color(hex: "#cc99ff"), 225 | Color(hex: "#ff66b3"), Color(hex: "#ff99cc"), 226 | Color(hex: "#ffcce6"), Color(hex: "#fff0f5"), 227 | Color(hex: "#ff3300"), Color(hex: "#ff6600"), 228 | Color(hex: "#ff9900"), Color(hex: "#ffcc00") 229 | ] 230 | case .volcanicOasis: 231 | return [ 232 | Color(hex: "#660000"), Color(hex: "#990000"), 233 | Color(hex: "#cc0000"), Color(hex: "#ff0000"), 234 | Color(hex: "#ff3300"), Color(hex: "#ff6600"), 235 | Color(hex: "#ff9900"), Color(hex: "#ffcc00"), 236 | Color(hex: "#00cc66"), Color(hex: "#00e677"), 237 | Color(hex: "#00ff88"), Color(hex: "#66ffb3"), 238 | Color(hex: "#003366"), Color(hex: "#004080"), 239 | Color(hex: "#004d99"), Color(hex: "#0059b3") 240 | ] 241 | case .arcticFrost: 242 | return [ 243 | Color(hex: "#ffffff"), Color(hex: "#f0f8ff"), 244 | Color(hex: "#e6f2ff"), Color(hex: "#ccebff"), 245 | Color(hex: "#b3e0ff"), Color(hex: "#99d6ff"), 246 | Color(hex: "#80ccff"), Color(hex: "#66c2ff"), 247 | Color(hex: "#4db8ff"), Color(hex: "#33adff"), 248 | Color(hex: "#1aa3ff"), Color(hex: "#0099ff"), 249 | Color(hex: "#0080d6"), Color(hex: "#0066cc"), 250 | Color(hex: "#004db3"), Color(hex: "#003399") 251 | ] 252 | case .jungleMist: 253 | return [ 254 | Color(hex: "#264d00"), Color(hex: "#336600"), 255 | Color(hex: "#408000"), Color(hex: "#4d9900"), 256 | Color(hex: "#59b300"), Color(hex: "#66cc00"), 257 | Color(hex: "#73e600"), Color(hex: "#80ff00"), 258 | Color(hex: "#b3ff66"), Color(hex: "#ccff99"), 259 | Color(hex: "#e6ffcc"), Color(hex: "#f2fff2"), 260 | Color(hex: "#006666"), Color(hex: "#008080"), 261 | Color(hex: "#009999"), Color(hex: "#00b3b3") 262 | ] 263 | case .desertMirage: 264 | return [ 265 | Color(hex: "#fff2d9"), Color(hex: "#ffedcc"), 266 | Color(hex: "#ffe6b3"), Color(hex: "#ffdf99"), 267 | Color(hex: "#ffd480"), Color(hex: "#ffcc66"), 268 | Color(hex: "#ffc34d"), Color(hex: "#ffbb33"), 269 | Color(hex: "#ff9900"), Color(hex: "#ff8000"), 270 | Color(hex: "#ff6600"), Color(hex: "#ff4d00"), 271 | Color(hex: "#ff3300"), Color(hex: "#ff1a00"), 272 | Color(hex: "#ff0000"), Color(hex: "#cc0000") 273 | ] 274 | case .neonMetropolis: 275 | return [ 276 | Color(hex: "#1a0033"), Color(hex: "#330066"), 277 | Color(hex: "#4d0099"), Color(hex: "#6600cc"), 278 | Color(hex: "#8000ff"), Color(hex: "#9933ff"), 279 | Color(hex: "#b366ff"), Color(hex: "#cc99ff"), 280 | Color(hex: "#00ff00"), Color(hex: "#33ff33"), 281 | Color(hex: "#66ff66"), Color(hex: "#99ff99"), 282 | Color(hex: "#ff0066"), Color(hex: "#ff3399"), 283 | Color(hex: "#ff66cc"), Color(hex: "#ff99ff") 284 | ] 285 | } 286 | } 287 | 288 | /// The background color of the gradient. 289 | public var background: Color { 290 | switch self { 291 | case .auroraBorealis: 292 | return Color(hex: "#001a33") 293 | case .sunsetHorizon: 294 | return Color(hex: "#660000") 295 | case .mysticForest: 296 | return Color(hex: "#002600") 297 | case .cosmicNebula: 298 | return Color(hex: "#0d0d1a") 299 | case .coralReef: 300 | return Color(hex: "#00334d") 301 | case .etherealTwilight: 302 | return Color(hex: "#1a0033") 303 | case .volcanicOasis: 304 | return Color(hex: "#330000") 305 | case .arcticFrost: 306 | return Color(hex: "#e6f3ff") 307 | case .jungleMist: 308 | return Color(hex: "#1a3300") 309 | case .desertMirage: 310 | return Color(hex: "#ffe6b3") 311 | case .neonMetropolis: 312 | return Color(hex: "#000000") 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /Sources/Meshin/Meshin.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0A2DF7D82CC3D03400920005 /* MeshingKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0A2DF7D72CC3D03400920005 /* MeshingKit */; }; 11 | 0A2DF7DB2CC3D07D00920005 /* Inject in Frameworks */ = {isa = PBXBuildFile; productRef = 0A2DF7DA2CC3D07D00920005 /* Inject */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 0ACF11BF2CC3C99F008D8E5C /* Meshin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshin.app; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | /* End PBXFileReference section */ 17 | 18 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 19 | 0ACF11C12CC3C99F008D8E5C /* Meshin */ = { 20 | isa = PBXFileSystemSynchronizedRootGroup; 21 | path = Meshin; 22 | sourceTree = ""; 23 | }; 24 | /* End PBXFileSystemSynchronizedRootGroup section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | 0ACF11BC2CC3C99F008D8E5C /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | 0A2DF7D82CC3D03400920005 /* MeshingKit in Frameworks */, 32 | 0A2DF7DB2CC3D07D00920005 /* Inject in Frameworks */, 33 | ); 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXFrameworksBuildPhase section */ 37 | 38 | /* Begin PBXGroup section */ 39 | 0ACF11B62CC3C99F008D8E5C = { 40 | isa = PBXGroup; 41 | children = ( 42 | 0ACF11C12CC3C99F008D8E5C /* Meshin */, 43 | 0ACF11C02CC3C99F008D8E5C /* Products */, 44 | ); 45 | sourceTree = ""; 46 | }; 47 | 0ACF11C02CC3C99F008D8E5C /* Products */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 0ACF11BF2CC3C99F008D8E5C /* Meshin.app */, 51 | ); 52 | name = Products; 53 | sourceTree = ""; 54 | }; 55 | /* End PBXGroup section */ 56 | 57 | /* Begin PBXNativeTarget section */ 58 | 0ACF11BE2CC3C99F008D8E5C /* Meshin */ = { 59 | isa = PBXNativeTarget; 60 | buildConfigurationList = 0ACF11CE2CC3C9A1008D8E5C /* Build configuration list for PBXNativeTarget "Meshin" */; 61 | buildPhases = ( 62 | 0ACF11BB2CC3C99F008D8E5C /* Sources */, 63 | 0ACF11BC2CC3C99F008D8E5C /* Frameworks */, 64 | 0ACF11BD2CC3C99F008D8E5C /* Resources */, 65 | ); 66 | buildRules = ( 67 | ); 68 | dependencies = ( 69 | ); 70 | fileSystemSynchronizedGroups = ( 71 | 0ACF11C12CC3C99F008D8E5C /* Meshin */, 72 | ); 73 | name = Meshin; 74 | packageProductDependencies = ( 75 | 0A2DF7D72CC3D03400920005 /* MeshingKit */, 76 | 0A2DF7DA2CC3D07D00920005 /* Inject */, 77 | ); 78 | productName = Meshin; 79 | productReference = 0ACF11BF2CC3C99F008D8E5C /* Meshin.app */; 80 | productType = "com.apple.product-type.application"; 81 | }; 82 | /* End PBXNativeTarget section */ 83 | 84 | /* Begin PBXProject section */ 85 | 0ACF11B72CC3C99F008D8E5C /* Project object */ = { 86 | isa = PBXProject; 87 | attributes = { 88 | BuildIndependentTargetsInParallel = 1; 89 | LastSwiftUpdateCheck = 1610; 90 | LastUpgradeCheck = 1610; 91 | TargetAttributes = { 92 | 0ACF11BE2CC3C99F008D8E5C = { 93 | CreatedOnToolsVersion = 16.1; 94 | }; 95 | }; 96 | }; 97 | buildConfigurationList = 0ACF11BA2CC3C99F008D8E5C /* Build configuration list for PBXProject "Meshin" */; 98 | developmentRegion = en; 99 | hasScannedForEncodings = 0; 100 | knownRegions = ( 101 | en, 102 | Base, 103 | ); 104 | mainGroup = 0ACF11B62CC3C99F008D8E5C; 105 | minimizedProjectReferenceProxies = 1; 106 | packageReferences = ( 107 | 0A2DF7D62CC3D03400920005 /* XCRemoteSwiftPackageReference "MeshingKit" */, 108 | 0A2DF7D92CC3D07D00920005 /* XCRemoteSwiftPackageReference "Inject" */, 109 | ); 110 | preferredProjectObjectVersion = 77; 111 | productRefGroup = 0ACF11C02CC3C99F008D8E5C /* Products */; 112 | projectDirPath = ""; 113 | projectRoot = ""; 114 | targets = ( 115 | 0ACF11BE2CC3C99F008D8E5C /* Meshin */, 116 | ); 117 | }; 118 | /* End PBXProject section */ 119 | 120 | /* Begin PBXResourcesBuildPhase section */ 121 | 0ACF11BD2CC3C99F008D8E5C /* Resources */ = { 122 | isa = PBXResourcesBuildPhase; 123 | buildActionMask = 2147483647; 124 | files = ( 125 | ); 126 | runOnlyForDeploymentPostprocessing = 0; 127 | }; 128 | /* End PBXResourcesBuildPhase section */ 129 | 130 | /* Begin PBXSourcesBuildPhase section */ 131 | 0ACF11BB2CC3C99F008D8E5C /* Sources */ = { 132 | isa = PBXSourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXSourcesBuildPhase section */ 139 | 140 | /* Begin XCBuildConfiguration section */ 141 | 0ACF11CC2CC3C9A1008D8E5C /* Debug */ = { 142 | isa = XCBuildConfiguration; 143 | buildSettings = { 144 | ALWAYS_SEARCH_USER_PATHS = NO; 145 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 146 | CLANG_ANALYZER_NONNULL = YES; 147 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 148 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 149 | CLANG_ENABLE_MODULES = YES; 150 | CLANG_ENABLE_OBJC_ARC = YES; 151 | CLANG_ENABLE_OBJC_WEAK = YES; 152 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 153 | CLANG_WARN_BOOL_CONVERSION = YES; 154 | CLANG_WARN_COMMA = YES; 155 | CLANG_WARN_CONSTANT_CONVERSION = YES; 156 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 157 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 158 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 159 | CLANG_WARN_EMPTY_BODY = YES; 160 | CLANG_WARN_ENUM_CONVERSION = YES; 161 | CLANG_WARN_INFINITE_RECURSION = YES; 162 | CLANG_WARN_INT_CONVERSION = YES; 163 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 164 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 165 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 166 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 167 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 168 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 169 | CLANG_WARN_STRICT_PROTOTYPES = YES; 170 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 171 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 172 | CLANG_WARN_UNREACHABLE_CODE = YES; 173 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 174 | COPY_PHASE_STRIP = NO; 175 | DEBUG_INFORMATION_FORMAT = dwarf; 176 | ENABLE_STRICT_OBJC_MSGSEND = YES; 177 | ENABLE_TESTABILITY = YES; 178 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 179 | GCC_C_LANGUAGE_STANDARD = gnu17; 180 | GCC_DYNAMIC_NO_PIC = NO; 181 | GCC_NO_COMMON_BLOCKS = YES; 182 | GCC_OPTIMIZATION_LEVEL = 0; 183 | GCC_PREPROCESSOR_DEFINITIONS = ( 184 | "DEBUG=1", 185 | "$(inherited)", 186 | ); 187 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 188 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 189 | GCC_WARN_UNDECLARED_SELECTOR = YES; 190 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 191 | GCC_WARN_UNUSED_FUNCTION = YES; 192 | GCC_WARN_UNUSED_VARIABLE = YES; 193 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 194 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 195 | MTL_FAST_MATH = YES; 196 | ONLY_ACTIVE_ARCH = YES; 197 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 198 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 199 | }; 200 | name = Debug; 201 | }; 202 | 0ACF11CD2CC3C9A1008D8E5C /* Release */ = { 203 | isa = XCBuildConfiguration; 204 | buildSettings = { 205 | ALWAYS_SEARCH_USER_PATHS = NO; 206 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 207 | CLANG_ANALYZER_NONNULL = YES; 208 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 209 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 210 | CLANG_ENABLE_MODULES = YES; 211 | CLANG_ENABLE_OBJC_ARC = YES; 212 | CLANG_ENABLE_OBJC_WEAK = YES; 213 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 214 | CLANG_WARN_BOOL_CONVERSION = YES; 215 | CLANG_WARN_COMMA = YES; 216 | CLANG_WARN_CONSTANT_CONVERSION = YES; 217 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 218 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 219 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 220 | CLANG_WARN_EMPTY_BODY = YES; 221 | CLANG_WARN_ENUM_CONVERSION = YES; 222 | CLANG_WARN_INFINITE_RECURSION = YES; 223 | CLANG_WARN_INT_CONVERSION = YES; 224 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 225 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 226 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 227 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 228 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 229 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 230 | CLANG_WARN_STRICT_PROTOTYPES = YES; 231 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 232 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 233 | CLANG_WARN_UNREACHABLE_CODE = YES; 234 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 235 | COPY_PHASE_STRIP = NO; 236 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 237 | ENABLE_NS_ASSERTIONS = NO; 238 | ENABLE_STRICT_OBJC_MSGSEND = YES; 239 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 240 | GCC_C_LANGUAGE_STANDARD = gnu17; 241 | GCC_NO_COMMON_BLOCKS = YES; 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 249 | MTL_ENABLE_DEBUG_INFO = NO; 250 | MTL_FAST_MATH = YES; 251 | SWIFT_COMPILATION_MODE = wholemodule; 252 | }; 253 | name = Release; 254 | }; 255 | 0ACF11CF2CC3C9A1008D8E5C /* Debug */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 259 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 260 | CODE_SIGN_ENTITLEMENTS = Meshin/Meshin.entitlements; 261 | CODE_SIGN_STYLE = Automatic; 262 | CURRENT_PROJECT_VERSION = 1; 263 | DEVELOPMENT_ASSET_PATHS = "\"Meshin/Preview Content\""; 264 | DEVELOPMENT_TEAM = YQZQG7N4WG; 265 | ENABLE_HARDENED_RUNTIME = YES; 266 | ENABLE_PREVIEWS = YES; 267 | GENERATE_INFOPLIST_FILE = YES; 268 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 269 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 270 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 271 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 272 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 273 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 274 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 275 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 276 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 277 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 278 | IPHONEOS_DEPLOYMENT_TARGET = 18.1; 279 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 280 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 281 | MACOSX_DEPLOYMENT_TARGET = 15.1; 282 | MARKETING_VERSION = 1.0; 283 | PRODUCT_BUNDLE_IDENTIFIER = com.rudrankriyam.Meshin; 284 | PRODUCT_NAME = "$(TARGET_NAME)"; 285 | SDKROOT = auto; 286 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 287 | SWIFT_EMIT_LOC_STRINGS = YES; 288 | SWIFT_VERSION = 5.0; 289 | TARGETED_DEVICE_FAMILY = "1,2,7"; 290 | XROS_DEPLOYMENT_TARGET = 2.1; 291 | }; 292 | name = Debug; 293 | }; 294 | 0ACF11D02CC3C9A1008D8E5C /* Release */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 298 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 299 | CODE_SIGN_ENTITLEMENTS = Meshin/Meshin.entitlements; 300 | CODE_SIGN_STYLE = Automatic; 301 | CURRENT_PROJECT_VERSION = 1; 302 | DEVELOPMENT_ASSET_PATHS = "\"Meshin/Preview Content\""; 303 | DEVELOPMENT_TEAM = YQZQG7N4WG; 304 | ENABLE_HARDENED_RUNTIME = YES; 305 | ENABLE_PREVIEWS = YES; 306 | GENERATE_INFOPLIST_FILE = YES; 307 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 308 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 309 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 310 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 311 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 312 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 313 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 314 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 315 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 316 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 317 | IPHONEOS_DEPLOYMENT_TARGET = 18.1; 318 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 319 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 320 | MACOSX_DEPLOYMENT_TARGET = 15.1; 321 | MARKETING_VERSION = 1.0; 322 | PRODUCT_BUNDLE_IDENTIFIER = com.rudrankriyam.Meshin; 323 | PRODUCT_NAME = "$(TARGET_NAME)"; 324 | SDKROOT = auto; 325 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 326 | SWIFT_EMIT_LOC_STRINGS = YES; 327 | SWIFT_VERSION = 5.0; 328 | TARGETED_DEVICE_FAMILY = "1,2,7"; 329 | XROS_DEPLOYMENT_TARGET = 2.1; 330 | }; 331 | name = Release; 332 | }; 333 | /* End XCBuildConfiguration section */ 334 | 335 | /* Begin XCConfigurationList section */ 336 | 0ACF11BA2CC3C99F008D8E5C /* Build configuration list for PBXProject "Meshin" */ = { 337 | isa = XCConfigurationList; 338 | buildConfigurations = ( 339 | 0ACF11CC2CC3C9A1008D8E5C /* Debug */, 340 | 0ACF11CD2CC3C9A1008D8E5C /* Release */, 341 | ); 342 | defaultConfigurationIsVisible = 0; 343 | defaultConfigurationName = Release; 344 | }; 345 | 0ACF11CE2CC3C9A1008D8E5C /* Build configuration list for PBXNativeTarget "Meshin" */ = { 346 | isa = XCConfigurationList; 347 | buildConfigurations = ( 348 | 0ACF11CF2CC3C9A1008D8E5C /* Debug */, 349 | 0ACF11D02CC3C9A1008D8E5C /* Release */, 350 | ); 351 | defaultConfigurationIsVisible = 0; 352 | defaultConfigurationName = Release; 353 | }; 354 | /* End XCConfigurationList section */ 355 | 356 | /* Begin XCLocalSwiftPackageReference section */ 357 | 0A2DF7D62CC3D03400920005 /* XCLocalSwiftPackageReference "MeshingKit" */ = { 358 | isa = XCLocalSwiftPackageReference; 359 | relativePath = "../.."; 360 | }; 361 | /* End XCLocalSwiftPackageReference section */ 362 | 363 | /* Begin XCRemoteSwiftPackageReference section */ 364 | 0A2DF7D92CC3D07D00920005 /* XCRemoteSwiftPackageReference "Inject" */ = { 365 | isa = XCRemoteSwiftPackageReference; 366 | repositoryURL = "https://github.com/krzysztofzablocki/Inject"; 367 | requirement = { 368 | kind = upToNextMajorVersion; 369 | minimumVersion = 1.5.2; 370 | }; 371 | }; 372 | /* End XCRemoteSwiftPackageReference section */ 373 | 374 | /* Begin XCSwiftPackageProductDependency section */ 375 | 0A2DF7D72CC3D03400920005 /* MeshingKit */ = { 376 | isa = XCSwiftPackageProductDependency; 377 | package = 0A2DF7D62CC3D03400920005 /* XCLocalSwiftPackageReference "MeshingKit" */; 378 | productName = MeshingKit; 379 | }; 380 | 0A2DF7DA2CC3D07D00920005 /* Inject */ = { 381 | isa = XCSwiftPackageProductDependency; 382 | package = 0A2DF7D92CC3D07D00920005 /* XCRemoteSwiftPackageReference "Inject" */; 383 | productName = Inject; 384 | }; 385 | /* End XCSwiftPackageProductDependency section */ 386 | }; 387 | rootObject = 0ACF11B72CC3C99F008D8E5C /* Project object */; 388 | } 389 | -------------------------------------------------------------------------------- /Sources/MeshingKit/GradientTemplateSize3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientTemplateSize3.swift 3 | // MeshingKit 4 | // 5 | // Created by Rudrank Riyam on 10/14/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration of predefined gradient templates with a 3x3 grid size. 11 | public enum GradientTemplateSize3: String, CaseIterable, GradientTemplate { 12 | case intelligence 13 | case auroraBorealis 14 | case sunsetGlow 15 | case oceanDepths 16 | case neonNight 17 | case autumnLeaves 18 | case cosmicAurora 19 | case lavaFlow 20 | case etherealMist 21 | case tropicalParadise 22 | case midnightGalaxy 23 | case desertMirage 24 | case frostedCrystal 25 | case enchantedForest 26 | case rubyFusion 27 | case goldenSunrise 28 | case cosmicNebula 29 | case arcticAurora 30 | case volcanicEmber 31 | case mintBreeze 32 | case twilightSerenade 33 | case saharaDunes 34 | 35 | /// The name of the gradient template. 36 | public var name: String { 37 | rawValue.capitalized 38 | } 39 | 40 | /// The size of the gradient, representing both width and height in pixels. 41 | public var size: Int { 42 | 3 43 | } 44 | 45 | /// An array of 2D points that define the control points of the gradient. 46 | public var points: [SIMD2] { 47 | switch self { 48 | case .intelligence: 49 | return [ 50 | .init(x: 0.000, y: 0.000), .init(x: 0.400, y: 0.000), 51 | .init(x: 1.000, y: 0.000), 52 | .init(x: 0.000, y: 0.450), .init(x: 0.653, y: 0.670), 53 | .init(x: 1.000, y: 0.200), 54 | .init(x: 0.000, y: 1.000), .init(x: 0.550, y: 1.000), 55 | .init(x: 1.000, y: 1.000) 56 | ] 57 | case .auroraBorealis: 58 | return [ 59 | .init(x: 0.000, y: 0.000), .init(x: 0.400, y: 0.000), 60 | .init(x: 1.000, y: 0.000), 61 | .init(x: 0.000, y: 0.450), .init(x: 0.900, y: 0.700), 62 | .init(x: 1.000, y: 0.200), 63 | .init(x: 0.000, y: 1.000), .init(x: 0.550, y: 1.000), 64 | .init(x: 1.000, y: 1.000) 65 | ] 66 | case .sunsetGlow: 67 | return [ 68 | .init(x: 0.000, y: 0.000), .init(x: 0.100, y: 0.000), 69 | .init(x: 1.000, y: 0.000), 70 | .init(x: 0.000, y: 0.537), .init(x: 0.182, y: 0.794), 71 | .init(x: 1.000, y: 0.148), 72 | .init(x: 0.000, y: 1.000), .init(x: 0.900, y: 1.000), 73 | .init(x: 1.000, y: 1.000) 74 | ] 75 | case .oceanDepths: 76 | return [ 77 | .init(x: 0.000, y: 0.000), .init(x: 0.497, y: 0.000), 78 | .init(x: 1.000, y: 0.000), 79 | .init(x: 0.000, y: 0.213), .init(x: 0.670, y: 0.930), 80 | .init(x: 1.000, y: 0.091), 81 | .init(x: 0.000, y: 1.000), .init(x: 0.490, y: 1.000), 82 | .init(x: 1.000, y: 1.000) 83 | ] 84 | case .neonNight: 85 | return [ 86 | .init(x: 0.000, y: 0.000), .init(x: 0.200, y: 0.000), 87 | .init(x: 1.000, y: 0.000), 88 | .init(x: 0.000, y: 0.596), .init(x: 0.807, y: 0.295), 89 | .init(x: 1.000, y: 0.200), 90 | .init(x: 0.000, y: 1.000), .init(x: 0.800, y: 1.000), 91 | .init(x: 1.000, y: 1.000) 92 | ] 93 | case .autumnLeaves: 94 | return [ 95 | .init(x: 0.000, y: 0.000), .init(x: 0.300, y: 0.000), 96 | .init(x: 1.000, y: 0.000), 97 | .init(x: 0.000, y: 0.700), .init(x: 0.172, y: 0.154), 98 | .init(x: 1.000, y: 0.300), 99 | .init(x: 0.000, y: 1.000), .init(x: 0.700, y: 1.000), 100 | .init(x: 1.000, y: 1.000) 101 | ] 102 | case .cosmicAurora: 103 | return [ 104 | .init(x: 0.000, y: 0.000), .init(x: 0.161, y: 0.000), 105 | .init(x: 1.000, y: 0.000), 106 | .init(x: 0.000, y: 0.326), .init(x: 0.263, y: 0.882), 107 | .init(x: 1.000, y: 0.142), 108 | .init(x: 0.000, y: 1.000), .init(x: 0.600, y: 1.000), 109 | .init(x: 1.000, y: 1.000) 110 | ] 111 | case .lavaFlow: 112 | return [ 113 | .init(x: 0.000, y: 0.000), .init(x: 0.737, y: 0.000), 114 | .init(x: 1.000, y: 0.000), 115 | .init(x: 0.000, y: 0.177), .init(x: 0.703, y: 0.809), 116 | .init(x: 1.000, y: 0.503), 117 | .init(x: 0.000, y: 1.000), .init(x: 0.502, y: 1.000), 118 | .init(x: 1.000, y: 1.000) 119 | ] 120 | case .etherealMist: 121 | return [ 122 | .init(x: 0.000, y: 0.000), .init(x: 0.850, y: 0.000), 123 | .init(x: 1.000, y: 0.000), 124 | .init(x: 0.000, y: 0.150), .init(x: 0.920, y: 0.080), 125 | .init(x: 1.000, y: 0.850), 126 | .init(x: 0.000, y: 1.000), .init(x: 0.150, y: 1.000), 127 | .init(x: 1.000, y: 1.000) 128 | ] 129 | case .tropicalParadise: 130 | return [ 131 | .init(x: 0.000, y: 0.000), .init(x: 0.400, y: 0.000), 132 | .init(x: 1.000, y: 0.000), 133 | .init(x: 0.000, y: 0.600), .init(x: 0.250, y: 0.750), 134 | .init(x: 1.000, y: 0.400), 135 | .init(x: 0.000, y: 1.000), .init(x: 0.950, y: 1.000), 136 | .init(x: 1.000, y: 1.000) 137 | ] 138 | case .midnightGalaxy: 139 | return [ 140 | .init(x: 0.000, y: 0.000), .init(x: 0.100, y: 0.000), 141 | .init(x: 1.000, y: 0.000), 142 | .init(x: 0.000, y: 0.900), .init(x: 0.800, y: 0.200), 143 | .init(x: 1.000, y: 0.100), 144 | .init(x: 0.000, y: 1.000), .init(x: 0.900, y: 1.000), 145 | .init(x: 1.000, y: 1.000) 146 | ] 147 | case .desertMirage: 148 | return [ 149 | .init(x: 0.000, y: 0.000), .init(x: 0.300, y: 0.000), 150 | .init(x: 1.000, y: 0.000), 151 | .init(x: 0.000, y: 0.700), .init(x: 0.600, y: 0.400), 152 | .init(x: 1.000, y: 0.300), 153 | .init(x: 0.000, y: 1.000), .init(x: 0.700, y: 1.000), 154 | .init(x: 1.000, y: 1.000) 155 | ] 156 | case .frostedCrystal: 157 | return [ 158 | .init(x: 0.000, y: 0.000), .init(x: 0.400, y: 0.000), 159 | .init(x: 1.000, y: 0.000), 160 | .init(x: 0.000, y: 0.600), .init(x: 0.200, y: 0.200), 161 | .init(x: 1.000, y: 0.400), 162 | .init(x: 0.000, y: 1.000), .init(x: 0.600, y: 1.000), 163 | .init(x: 1.000, y: 1.000) 164 | ] 165 | case .enchantedForest: 166 | return [ 167 | .init(x: 0.000, y: 0.000), .init(x: 0.300, y: 0.000), 168 | .init(x: 1.000, y: 0.000), 169 | .init(x: 0.000, y: 0.700), .init(x: 0.500, y: 0.300), 170 | .init(x: 1.000, y: 0.300), 171 | .init(x: 0.000, y: 1.000), .init(x: 0.700, y: 1.000), 172 | .init(x: 1.000, y: 1.000) 173 | ] 174 | case .rubyFusion: 175 | return [ 176 | .init(x: 0.000, y: 0.000), .init(x: 0.200, y: 0.000), 177 | .init(x: 1.000, y: 0.000), 178 | .init(x: 0.000, y: 0.800), .init(x: 0.400, y: 0.600), 179 | .init(x: 1.000, y: 0.200), 180 | .init(x: 0.000, y: 1.000), .init(x: 0.800, y: 1.000), 181 | .init(x: 1.000, y: 1.000) 182 | ] 183 | case .goldenSunrise: 184 | return [ 185 | .init(x: 0.000, y: 0.000), .init(x: 0.600, y: 0.000), 186 | .init(x: 1.000, y: 0.000), 187 | .init(x: 0.000, y: 0.400), .init(x: 0.700, y: 0.300), 188 | .init(x: 1.000, y: 0.600), 189 | .init(x: 0.000, y: 1.000), .init(x: 0.400, y: 1.000), 190 | .init(x: 1.000, y: 1.000) 191 | ] 192 | case .cosmicNebula: 193 | return [ 194 | .init(x: 0.000, y: 0.000), .init(x: 0.500, y: 0.000), 195 | .init(x: 1.000, y: 0.000), 196 | .init(x: 0.000, y: 0.500), .init(x: 0.750, y: 0.250), 197 | .init(x: 1.000, y: 0.500), 198 | .init(x: 0.000, y: 1.000), .init(x: 0.500, y: 1.000), 199 | .init(x: 1.000, y: 1.000) 200 | ] 201 | case .arcticAurora: 202 | return [ 203 | .init(x: 0.000, y: 0.000), .init(x: 0.300, y: 0.000), 204 | .init(x: 1.000, y: 0.000), 205 | .init(x: 0.000, y: 0.700), .init(x: 0.600, y: 0.400), 206 | .init(x: 1.000, y: 0.300), 207 | .init(x: 0.000, y: 1.000), .init(x: 0.700, y: 1.000), 208 | .init(x: 1.000, y: 1.000) 209 | ] 210 | case .volcanicEmber: 211 | return [ 212 | .init(x: 0.000, y: 0.000), .init(x: 0.200, y: 0.000), 213 | .init(x: 1.000, y: 0.000), 214 | .init(x: 0.000, y: 0.800), .init(x: 0.500, y: 0.500), 215 | .init(x: 1.000, y: 0.200), 216 | .init(x: 0.000, y: 1.000), .init(x: 0.800, y: 1.000), 217 | .init(x: 1.000, y: 1.000) 218 | ] 219 | case .mintBreeze: 220 | return [ 221 | .init(x: 0.000, y: 0.000), .init(x: 0.400, y: 0.000), 222 | .init(x: 1.000, y: 0.000), 223 | .init(x: 0.000, y: 0.600), .init(x: 0.720, y: 0.860), 224 | .init(x: 1.000, y: 0.130), 225 | .init(x: 0.000, y: 1.000), .init(x: 0.600, y: 1.000), 226 | .init(x: 1.000, y: 1.000) 227 | ] 228 | case .twilightSerenade: 229 | return [ 230 | .init(x: 0.000, y: 0.000), .init(x: 0.300, y: 0.000), 231 | .init(x: 1.000, y: 0.000), 232 | .init(x: 0.000, y: 0.230), .init(x: 0.220, y: 0.770), 233 | .init(x: 1.000, y: 0.210), 234 | .init(x: 0.000, y: 1.000), .init(x: 0.700, y: 1.000), 235 | .init(x: 1.000, y: 1.000) 236 | ] 237 | case .saharaDunes: 238 | return [ 239 | .init(x: 0.000, y: 0.000), .init(x: 0.400, y: 0.000), 240 | .init(x: 1.000, y: 0.000), 241 | .init(x: 0.000, y: 0.600), .init(x: 0.700, y: 0.300), 242 | .init(x: 1.000, y: 0.400), 243 | .init(x: 0.000, y: 1.000), .init(x: 0.600, y: 1.000), 244 | .init(x: 1.000, y: 1.000) 245 | ] 246 | } 247 | } 248 | 249 | /// An array of colors associated with the control points. 250 | public var colors: [Color] { 251 | switch self { 252 | case .intelligence: 253 | return [ 254 | Color(hex: "#1BB1F9"), Color(hex: "#648EF2"), 255 | Color(hex: "#AE6FEE"), 256 | Color(hex: "#9B79F1"), Color(hex: "#ED50EB"), 257 | Color(hex: "#F65490"), 258 | Color(hex: "#F74A6B"), Color(hex: "#F47F3E"), 259 | Color(hex: "#ED8D02") 260 | ] 261 | case .auroraBorealis: 262 | return [ 263 | Color(hex: "#0073e6"), Color(hex: "#4da6ff"), 264 | Color(hex: "#b3d9ff"), 265 | Color(hex: "#00ff80"), Color(hex: "#66ffb3"), 266 | Color(hex: "#99ffcc"), 267 | Color(hex: "#004d40"), Color(hex: "#008577"), 268 | Color(hex: "#00a693") 269 | ] 270 | case .sunsetGlow: 271 | return [ 272 | Color(hex: "#F29933"), Color(hex: "#E66666"), 273 | Color(hex: "#B3337F"), 274 | Color(hex: "#CC4D80"), Color(hex: "#99194D"), 275 | Color(hex: "#660D33"), 276 | Color(hex: "#4D0D26"), Color(hex: "#330D1A"), 277 | Color(hex: "#1A0D0D") 278 | ] 279 | case .oceanDepths: 280 | return [ 281 | Color(hex: "#1A4D80"), Color(hex: "#0D3366"), 282 | Color(hex: "#00264D"), 283 | Color(hex: "#264D8C"), Color(hex: "#1A4073"), 284 | Color(hex: "#0D3366"), 285 | Color(hex: "#336699"), Color(hex: "#264D8C"), 286 | Color(hex: "#1A4D80") 287 | ] 288 | case .neonNight: 289 | return [ 290 | Color(hex: "#FF0080"), Color(hex: "#00FF80"), 291 | Color(hex: "#0080FF"), 292 | Color(hex: "#FF8000"), Color(hex: "#8000FF"), 293 | Color(hex: "#00FFFF"), 294 | Color(hex: "#FF00FF"), Color(hex: "#FFFF00"), 295 | Color(hex: "#80FF80") 296 | ] 297 | case .autumnLeaves: 298 | return [ 299 | Color(hex: "#CC4D00"), Color(hex: "#E66619"), 300 | Color(hex: "#B33300"), 301 | Color(hex: "#993319"), Color(hex: "#801910"), 302 | Color(hex: "#661A00"), 303 | Color(hex: "#4D0D00"), Color(hex: "#33190D"), 304 | Color(hex: "#1A0D00") 305 | ] 306 | case .cosmicAurora: 307 | return [ 308 | Color(hex: "#008050"), Color(hex: "#199966"), 309 | Color(hex: "#33B380"), 310 | Color(hex: "#4DCC99"), Color(hex: "#66E6B3"), 311 | Color(hex: "#80FFCC"), 312 | Color(hex: "#1A334D"), Color(hex: "#335266"), 313 | Color(hex: "#4D7080") 314 | ] 315 | case .lavaFlow: 316 | return [ 317 | Color(hex: "#FF0000"), Color(hex: "#E61A00"), 318 | Color(hex: "#CC3300"), 319 | Color(hex: "#B34D00"), Color(hex: "#996600"), 320 | Color(hex: "#808000"), 321 | Color(hex: "#660000"), Color(hex: "#4D0000"), 322 | Color(hex: "#330000") 323 | ] 324 | case .etherealMist: 325 | return [ 326 | Color(hex: "#F0F8FF"), Color(hex: "#E6F0FF"), 327 | Color(hex: "#D9E6FF"), 328 | Color(hex: "#CCE0FF"), Color(hex: "#B3D1FF"), 329 | Color(hex: "#99C2FF"), 330 | Color(hex: "#80B3FF"), Color(hex: "#66A3FF"), 331 | Color(hex: "#4D94FF") 332 | ] 333 | case .tropicalParadise: 334 | return [ 335 | Color(hex: "#00FF99"), Color(hex: "#33CC99"), 336 | Color(hex: "#66CC66"), 337 | Color(hex: "#99CC33"), Color(hex: "#CCCC00"), 338 | Color(hex: "#FFCC00"), 339 | Color(hex: "#FF9900"), Color(hex: "#FF6600"), 340 | Color(hex: "#FF3300") 341 | ] 342 | case .midnightGalaxy: 343 | return [ 344 | Color(hex: "#000066"), Color(hex: "#330066"), 345 | Color(hex: "#660066"), 346 | Color(hex: "#990066"), Color(hex: "#CC0066"), 347 | Color(hex: "#FF0066"), 348 | Color(hex: "#FF3399"), Color(hex: "#FF66CC"), 349 | Color(hex: "#FF99CC") 350 | ] 351 | case .desertMirage: 352 | return [ 353 | Color(hex: "#FFD699"), Color(hex: "#FFCC66"), 354 | Color(hex: "#FFC14D"), 355 | Color(hex: "#FFB833"), Color(hex: "#FFAD1A"), 356 | Color(hex: "#FFA500"), 357 | Color(hex: "#E69900"), Color(hex: "#CC8800"), 358 | Color(hex: "#B37700") 359 | ] 360 | case .frostedCrystal: 361 | return [ 362 | Color(hex: "#F0FAFF"), Color(hex: "#D6EBFF"), 363 | Color(hex: "#B8DCFF"), 364 | Color(hex: "#9ACDFF"), Color(hex: "#7CBEFF"), 365 | Color(hex: "#5EAFFF"), 366 | Color(hex: "#40A0FF"), Color(hex: "#2291FF"), 367 | Color(hex: "#0482FF") 368 | ] 369 | case .enchantedForest: 370 | return [ 371 | Color(hex: "#0A3A0A"), Color(hex: "#145214"), 372 | Color(hex: "#1E6A1E"), 373 | Color(hex: "#288228"), Color(hex: "#329B32"), 374 | Color(hex: "#3CB43C"), 375 | Color(hex: "#46CD46"), Color(hex: "#50E650"), 376 | Color(hex: "#5AFF5A") 377 | ] 378 | case .rubyFusion: 379 | return [ 380 | Color(hex: "#660000"), Color(hex: "#990000"), 381 | Color(hex: "#CC0000"), 382 | Color(hex: "#FF0000"), Color(hex: "#FF3333"), 383 | Color(hex: "#FF6666"), 384 | Color(hex: "#FF9999"), Color(hex: "#FFCCCC"), 385 | Color(hex: "#FFFFFF") 386 | ] 387 | case .goldenSunrise: 388 | return [ 389 | Color(hex: "#FFA500"), Color(hex: "#FFB52E"), 390 | Color(hex: "#FFC55C"), 391 | Color(hex: "#FFD58A"), Color(hex: "#FFE4B8"), 392 | Color(hex: "#FFF4E6"), 393 | Color(hex: "#FFFAF0"), Color(hex: "#FFFDF7"), 394 | Color(hex: "#FFFFFF") 395 | ] 396 | case .cosmicNebula: 397 | return [ 398 | Color(hex: "#000066"), Color(hex: "#3300CC"), 399 | Color(hex: "#6600FF"), 400 | Color(hex: "#9900FF"), Color(hex: "#CC00FF"), 401 | Color(hex: "#FF00FF"), 402 | Color(hex: "#FF33CC"), Color(hex: "#FF6699"), 403 | Color(hex: "#FF99CC") 404 | ] 405 | case .arcticAurora: 406 | return [ 407 | Color(hex: "#00264D"), Color(hex: "#004C99"), 408 | Color(hex: "#0072E6"), 409 | Color(hex: "#00A3FF"), Color(hex: "#33B8FF"), 410 | Color(hex: "#66CCFF"), 411 | Color(hex: "#99E0FF"), Color(hex: "#CCF2FF"), 412 | Color(hex: "#FFFFFF") 413 | ] 414 | case .volcanicEmber: 415 | return [ 416 | Color(hex: "#660000"), Color(hex: "#990000"), 417 | Color(hex: "#CC0000"), 418 | Color(hex: "#FF3300"), Color(hex: "#FF6600"), 419 | Color(hex: "#FF9900"), 420 | Color(hex: "#FFCC00"), Color(hex: "#FFFF00"), 421 | Color(hex: "#FFFFCC") 422 | ] 423 | case .mintBreeze: 424 | return [ 425 | Color(hex: "#CCFFE6"), Color(hex: "#99FFCC"), 426 | Color(hex: "#66FFB3"), 427 | Color(hex: "#33FF99"), Color(hex: "#00FF80"), 428 | Color(hex: "#00CC66"), 429 | Color(hex: "#009949"), Color(hex: "#006633"), 430 | Color(hex: "#00331A") 431 | ] 432 | case .twilightSerenade: 433 | return [ 434 | Color(hex: "#330066"), Color(hex: "#4D0099"), 435 | Color(hex: "#6600CC"), 436 | Color(hex: "#8000FF"), Color(hex: "#9933FF"), 437 | Color(hex: "#B266FF"), 438 | Color(hex: "#CC99FF"), Color(hex: "#E6CCFF"), 439 | Color(hex: "#FFFFFF") 440 | ] 441 | case .saharaDunes: 442 | return [ 443 | Color(hex: "#E6B366"), Color(hex: "#D9914D"), 444 | Color(hex: "#CC6E33"), 445 | Color(hex: "#BF4C1A"), Color(hex: "#B32900"), 446 | Color(hex: "#992200"), 447 | Color(hex: "#801A00"), Color(hex: "#661400"), 448 | Color(hex: "#4D0F00") 449 | ] 450 | } 451 | } 452 | 453 | /// The background color of the gradient. 454 | public var background: Color { 455 | switch self { 456 | case .intelligence: 457 | return Color(hex: "#1BB1F9") 458 | case .auroraBorealis: 459 | return Color(hex: "#001a33") 460 | case .sunsetGlow: 461 | return Color(hex: "#1A0D26") 462 | case .oceanDepths: 463 | return Color(hex: "#0D1A33") 464 | case .neonNight: 465 | return Color(hex: "#0D001A") 466 | case .autumnLeaves: 467 | return Color(hex: "#33190D") 468 | case .cosmicAurora: 469 | return Color(hex: "#000919") 470 | case .lavaFlow: 471 | return Color(hex: "#330000") 472 | case .etherealMist: 473 | return Color(hex: "#E6F0FF") 474 | case .tropicalParadise: 475 | return Color(hex: "#006633") 476 | case .midnightGalaxy: 477 | return Color(hex: "#000033") 478 | case .desertMirage: 479 | return Color(hex: "#FFE6CC") 480 | case .frostedCrystal: 481 | return Color(hex: "#E0F0FF") 482 | case .enchantedForest: 483 | return Color(hex: "#0A2A0A") 484 | case .rubyFusion: 485 | return Color(hex: "#330000") 486 | case .goldenSunrise: 487 | return Color(hex: "#FFD700") 488 | case .cosmicNebula: 489 | return Color(hex: "#000033") 490 | case .arcticAurora: 491 | return Color(hex: "#001433") 492 | case .volcanicEmber: 493 | return Color(hex: "#330000") 494 | case .mintBreeze: 495 | return Color(hex: "#E0FFF0") 496 | case .twilightSerenade: 497 | return Color(hex: "#1A0033") 498 | case .saharaDunes: 499 | return Color(hex: "#F2D6A2") 500 | } 501 | } 502 | } 503 | --------------------------------------------------------------------------------