├── 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 | 
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
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 |
--------------------------------------------------------------------------------