diff --git a/Tiny.xcodeproj/project.pbxproj b/Tiny.xcodeproj/project.pbxproj index 18efa44..97285b3 100644 --- a/Tiny.xcodeproj/project.pbxproj +++ b/Tiny.xcodeproj/project.pbxproj @@ -467,7 +467,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = YPC2WUCUT5; + DEVELOPMENT_TEAM = 6PN64DQKLX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Tiny/Info.plist; @@ -508,7 +508,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = YPC2WUCUT5; + DEVELOPMENT_TEAM = 6PN64DQKLX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Tiny/Info.plist; diff --git a/Tiny/Assets.xcassets/background.imageset/Contents.json b/Tiny/Assets.xcassets/background.imageset/Contents.json new file mode 100644 index 0000000..c305661 --- /dev/null +++ b/Tiny/Assets.xcassets/background.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Illustration35 4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tiny/Assets.xcassets/background.imageset/Illustration35 4.png b/Tiny/Assets.xcassets/background.imageset/Illustration35 4.png new file mode 100644 index 0000000..0d2037b Binary files /dev/null and b/Tiny/Assets.xcassets/background.imageset/Illustration35 4.png differ diff --git a/Tiny/Assets.xcassets/orbLightYellow.colorset/Contents.json b/Tiny/Assets.xcassets/orbLightYellow.colorset/Contents.json new file mode 100644 index 0000000..eea90fd --- /dev/null +++ b/Tiny/Assets.xcassets/orbLightYellow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x46", + "green" : "0xF5", + "red" : "0xF5" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x46", + "green" : "0xF5", + "red" : "0xF5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tiny/Assets.xcassets/orbOrange.colorset/Contents.json b/Tiny/Assets.xcassets/orbOrange.colorset/Contents.json new file mode 100644 index 0000000..d6fa551 --- /dev/null +++ b/Tiny/Assets.xcassets/orbOrange.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x45", + "green" : "0xAC", + "red" : "0xF0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x45", + "green" : "0xAC", + "red" : "0xF0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tiny/Components/BokehEffectView.swift b/Tiny/Components/BokehEffectView.swift index 61e1b74..2142cfe 100644 --- a/Tiny/Components/BokehEffectView.swift +++ b/Tiny/Components/BokehEffectView.swift @@ -1,3 +1,10 @@ +// +//  BokehEffectView.swift +//  Tiny +// +//  Created by Destu Cikal Ramdani on 12/11/25. +// + import SwiftUI internal import Combine @@ -6,39 +13,53 @@ struct BokehEffectView: View { private var pulseOpacity: Double { let clampedAmplitude = min(max(Double(amplitude), 0.0), 1.0) - return 0.2 + (clampedAmplitude * 0.8) // Make it more visible + return 0.15 + (clampedAmplitude * 0.6) } - + private var pulseScale: CGFloat { - return 1.0 + CGFloat(amplitude) * 0.2 + return 1.0 + CGFloat(amplitude) * 0.3 } var body: some View { ZStack { + // Layer 1: The Base Glow (Shifted Left) + // This is the bottom layer, slightly less bright. + Circle() + .fill(Color.orbLightYellow) + .frame(width: 30) + .opacity(pulseOpacity * 0.5) + .scaleEffect(pulseScale * 1) + .offset(x: -2) + + // Layer 2: The Core/Highlight (Shifted Right) + // *** We apply .blendMode(.screen) here to brighten the overlap *** Circle() - .fill(Color.yellow.opacity(0.8)) // Keeping this one + .fill(Color.orbLightYellow) .frame(width: 30) - .opacity(pulseOpacity) - .scaleEffect(pulseScale) + .opacity(pulseOpacity * 0.5) + .scaleEffect(pulseScale * 1.1) + .offset(x: 1, y: -2) + .blendMode( + .hardLight + ) // Makes the overlapping area brighter (like adding light) } - .blur(radius: 2) - .animation(.easeInOut(duration: 0.15), value: amplitude) + .blur(radius: 1) + .animation(.easeInOut(duration: 0.2), value: amplitude) } } #Preview { - // Example of how to use the view with a dummy binding struct PreviewWrapper: View { @State private var dummyAmplitude: Float = 0.5 let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect() var body: some View { ZStack { - Color.black + Color.black.ignoresSafeArea() BokehEffectView(amplitude: $dummyAmplitude) .onReceive(timer) { _ in withAnimation { - dummyAmplitude = Float.random(in: 0.1...0.8) + dummyAmplitude = Float.random(in: 0.2...0.9) } } } diff --git a/Tiny/Orb/AnimatedOrbView.swift b/Tiny/Orb/AnimatedOrbView.swift index 1b771c2..aa129f7 100644 --- a/Tiny/Orb/AnimatedOrbView.swift +++ b/Tiny/Orb/AnimatedOrbView.swift @@ -1,35 +1,70 @@ // AnimatedOrbView.swift // Tiny // -// Portions of this file are derived from “Orb” by Siddhant Mehta +// Portions of this file are derived from "Orb" by Siddhant Mehta // Copyright (c) 2024 Siddhant Mehta // Licensed under the MIT License. // See: https://github.com/metasidd/Orb/blob/main/LICENSE // -// Modifications made by Destu Cikal Ramdani on 2025-11-14. +// Modifications made by Destu Cikal Ramdani on 2025-11-18. // import SwiftUI +import SpriteKit struct AnimatedOrbView: View { + @StateObject private var physicsController = OrbPhysicsController() + private let configuration = OrbConfiguration( - backgroundColors: [.orange, .orange, .clear], // Custom gradient colors - glowColor: .white, // Glow effect color - coreGlowIntensity: 0.3, // Increased intensity for more visibility - showBackground: true, // Toggle background visibility - showWavyBlobs: true, // Toggle organic movement elements - showParticles: false, // Toggle particle effects - showGlowEffects: true, // Toggle glow effects - showShadow: true, // Toggle shadow effects - speed: 30, // Increased speed for more dynamic wiggle + backgroundColors: [.orange, .orbOrange, .clear], + glowColor: .white.opacity(0.1), + coreGlowIntensity: 0.1, + showBackground: true, + showWavyBlobs: true, + showParticles: false, + showGlowEffects: false, + showShadow: false, + speed: 30 ) var body: some View { - OrbView(configuration: configuration) - .frame(width: 200, height: 200) + ZStack { + + // 1. The Ring (behind) + Circle() + .stroke( + AngularGradient( + colors: [ + .orbOrange.opacity(0.8), + .orbOrange.opacity(2), + .orbOrange.opacity(0.6) + ], + center: .center, + startAngle: .degrees(0), + endAngle: .degrees(360) + ), + lineWidth: 1.5 + ) + .blur(radius: 5) + .frame(width: 235, height: 235) + + // 2. The Orb (in front) + OrbView(configuration: configuration) + .frame(width: 200, height: 200) + } + // animate the ring AND the orb together as one unit. + .scaleEffect(x: physicsController.scaleX, y: physicsController.scaleY) + .offset(x: physicsController.offsetX, y: physicsController.offsetY) + .rotationEffect(.degrees(physicsController.rotation)) + .onAppear { + physicsController.startPhysics() + } } } #Preview { - AnimatedOrbView() + ZStack { + Color.black.ignoresSafeArea() + AnimatedOrbView() + } } diff --git a/Tiny/Orb/OrbConfiguration.swift b/Tiny/Orb/OrbConfiguration.swift index 42316a2..1fc3cd6 100644 --- a/Tiny/Orb/OrbConfiguration.swift +++ b/Tiny/Orb/OrbConfiguration.swift @@ -51,7 +51,7 @@ public struct OrbConfiguration { } public init( - backgroundColors: [Color] = [.green, .blue, .pink], + backgroundColors: [Color] = [.orange, .orange, .clear], glowColor: Color = .white, coreGlowIntensity: Double = 1.0, showBackground: Bool = true, diff --git a/Tiny/Orb/OrbPhysicsController.swift b/Tiny/Orb/OrbPhysicsController.swift new file mode 100644 index 0000000..76ac903 --- /dev/null +++ b/Tiny/Orb/OrbPhysicsController.swift @@ -0,0 +1,65 @@ +// +// OrbPhysicsController.swift +// Tiny +// +// Created by Destu Cikal Ramdani on 18/11/25. +// + +import Foundation +import SwiftUI +internal import Combine + +class OrbPhysicsController: ObservableObject { + @Published var offsetX: CGFloat = 0 + @Published var offsetY: CGFloat = 0 + @Published var scaleX: CGFloat = 1.0 + @Published var scaleY: CGFloat = 1.0 + @Published var rotation: Double = 0 + + private var time: Double = 0.0 + private var displayLink: CADisplayLink? + + func startPhysics() { + displayLink = CADisplayLink(target: self, selector: #selector(update)) + displayLink?.add(to: .main, forMode: .common) + } + + @objc private func update() { + // Increment time. A smaller number means a slower, more "amniotic" feel. + time += 0.05 + + // --- Tunable Parameters --- + let driftAmount: CGFloat = 10.0 // How far it wanders from the center + let wobbleAmount: CGFloat = 0.1 // How much it "jiggles" + let rotationAmount: Double = 5.0 // How much it slowly turns + let smoothing: CGFloat = 0.05 // How "heavy" or "watery" the motion feels + // ------------------------- + + // 1. Organic "Floating" (Offset) + // We use two different sine waves for each axis. + // This creates a complex, non-linear drift. + let targetOffsetX = (sin(time * 0.3) * driftAmount) + (cos(time * 0.7) * driftAmount * 0.5) + let targetOffsetY = (cos(time * 0.2) * driftAmount) + (sin(time * 0.5) * driftAmount * 0.5) + + // 2. Asymmetric "Wobble" (Scale) + // X and Y now scale independently, using different wave combinations. + let baseScale: CGFloat = 1.0 + let targetScaleX = baseScale + (sin(time * 0.8) * wobbleAmount) + (cos(time * 0.3) * wobbleAmount * 0.5) + let targetScaleY = baseScale + (cos(time * 0.6) * wobbleAmount) + (sin(time * 0.2) * wobbleAmount * 0.5) + + // 3. Slow "Turning" (Rotation) + // A very slow rotation frequency for a gentle turn. + let targetRotation = sin(time * 0.15) * rotationAmount + + // 4. Smoothly apply all changes (the "liquid" feel) + offsetX += (targetOffsetX - offsetX) * smoothing + offsetY += (targetOffsetY - offsetY) * smoothing + scaleX += (targetScaleX - scaleX) * smoothing + scaleY += (targetScaleY - scaleY) * smoothing + rotation += (targetRotation - rotation) * smoothing + } + + deinit { + displayLink?.invalidate() + } +} diff --git a/Tiny/Orb/OrbView.swift b/Tiny/Orb/OrbView.swift index 68aec77..728ceba 100644 --- a/Tiny/Orb/OrbView.swift +++ b/Tiny/Orb/OrbView.swift @@ -70,7 +70,7 @@ public struct OrbView: View { private var background: some View { LinearGradient(colors: config.backgroundColors, - startPoint: .bottom, + startPoint: .bottomLeading, endPoint: .top) } @@ -107,7 +107,7 @@ public struct OrbView: View { GeometryReader { geometry in let size = min(geometry.size.width, geometry.size.height) - RotatingGlowView(color: .white.opacity(0.75), + RotatingGlowView(color: .white.opacity(0.4), rotationSpeed: config.speed * 1.5, direction: .clockwise) .mask { diff --git a/Tiny/Views/OrbLiveListenView.swift b/Tiny/Views/OrbLiveListenView.swift index 51a1e0a..95cafdc 100644 --- a/Tiny/Views/OrbLiveListenView.swift +++ b/Tiny/Views/OrbLiveListenView.swift @@ -2,6 +2,7 @@ import SwiftUI struct OrbLiveListenView: View { @StateObject var heartbeatSoundManager = HeartbeatSoundManager() + @StateObject private var physicsController = OrbPhysicsController() @State private var isListening = false @State private var animateOrb = false @State private var showShareSheet = false @@ -12,6 +13,12 @@ struct OrbLiveListenView: View { // Background Color.black.edgesIgnoringSafeArea(.all) + Image("background") + .resizable() + .scaleEffect(isListening ? 1.2 : 1.0) // zoom in + .animation(.easeInOut(duration: 1.2), value: isListening) + .ignoresSafeArea() + // Share Button VStack { HStack { @@ -48,6 +55,13 @@ struct OrbLiveListenView: View { AnimatedOrbView() if isListening { BokehEffectView(amplitude: $heartbeatSoundManager.blinkAmplitude) + .scaleEffect(x: physicsController.scaleX, y: physicsController.scaleY) + .offset(x: physicsController.offsetX, y: physicsController.offsetY) + .rotationEffect(.degrees(physicsController.rotation)) + .onAppear { + physicsController.startPhysics() + } + .frame(width: 18, height: 18) } } .frame(width: 200, height: 200)