Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
b630996db2
commit
3ece45d215
8
PRD.md
8
PRD.md
@ -22,8 +22,16 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
|
|||||||
- **Safe area handling** with proper Dynamic Island avoidance on iPhone and full-width layout on iPad
|
- **Safe area handling** with proper Dynamic Island avoidance on iPhone and full-width layout on iPad
|
||||||
- **Full-screen mode** with status bar hiding and tab bar expansion
|
- **Full-screen mode** with status bar hiding and tab bar expansion
|
||||||
- **Orientation-aware spacing** for optimal layout in all orientations
|
- **Orientation-aware spacing** for optimal layout in all orientations
|
||||||
|
- **Modern iOS 18+ Animations**:
|
||||||
|
- **Selectable Animation Styles**: Choose from effective animation styles including None, Spring, Bounce, and Glitch.
|
||||||
|
- **Numeric Text Transitions**: Smooth scrolling transitions for digits using `.contentTransition(.numericText())` (available in most styles).
|
||||||
|
- **Phase-Based Digit Animations**: Dynamic scale, vertical offset, and jitter effects when digits change.
|
||||||
|
- **Glitch Effect**: High-energy digital jitter with random offsets and rapid opacity shifts.
|
||||||
|
- **Dynamic Glow Pulsing**: Glow intensity and blur radius pulse during digit transitions for enhanced visual feedback.
|
||||||
|
- **Breathing Colon Effect**: Subtle opacity pulsing for colon separators to add life to the display.
|
||||||
|
|
||||||
### 2. Clock Customization
|
### 2. Clock Customization
|
||||||
|
- **Selectable digit animation styles**: Choose from None, Spring, Bounce, and Glitch
|
||||||
- **Color customization**: User-selectable digit colors with color picker
|
- **Color customization**: User-selectable digit colors with color picker
|
||||||
- **Background color**: Customizable background with color picker
|
- **Background color**: Customizable background with color picker
|
||||||
- **Glow effects**: Adjustable glow intensity (0-100%)
|
- **Glow effects**: Adjustable glow intensity (0-100%)
|
||||||
|
|||||||
@ -28,6 +28,8 @@ TheNoiseClock is a distraction-free digital clock with built-in white noise and
|
|||||||
- Custom fonts, weights, and designs with live preview
|
- Custom fonts, weights, and designs with live preview
|
||||||
- Glow and opacity controls for low-light comfort
|
- Glow and opacity controls for low-light comfort
|
||||||
- Clock tab hides the status bar for a distraction-free display
|
- Clock tab hides the status bar for a distraction-free display
|
||||||
|
- Selectable animation styles: None, Spring, Bounce, and Glitch
|
||||||
|
- Modern iOS 18+ animations: numeric transitions, phase-based bounces, glitch effects, and breathing colons
|
||||||
|
|
||||||
**White Noise**
|
**White Noise**
|
||||||
- Multiple ambient categories and curated sound packs
|
- Multiple ambient categories and curated sound packs
|
||||||
|
|||||||
@ -41,6 +41,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
var fontFamily: FontFamily = .system
|
var fontFamily: FontFamily = .system
|
||||||
var fontWeight: Font.Weight = .bold
|
var fontWeight: Font.Weight = .bold
|
||||||
var fontDesign: Font.Design = .rounded
|
var fontDesign: Font.Design = .rounded
|
||||||
|
var digitAnimationStyle: DigitAnimationStyle = .spring
|
||||||
|
|
||||||
// MARK: - Overlay Settings
|
// MARK: - Overlay Settings
|
||||||
var showBattery: Bool = true
|
var showBattery: Bool = true
|
||||||
@ -78,6 +79,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
case fontFamily
|
case fontFamily
|
||||||
case fontWeight
|
case fontWeight
|
||||||
case fontDesign
|
case fontDesign
|
||||||
|
case digitAnimationStyle
|
||||||
case showBattery
|
case showBattery
|
||||||
case showDate
|
case showDate
|
||||||
case dateFormat
|
case dateFormat
|
||||||
@ -125,6 +127,10 @@ class ClockStyle: Codable, Equatable {
|
|||||||
let decoded = Font.Design(rawValue: fontDesignString) {
|
let decoded = Font.Design(rawValue: fontDesignString) {
|
||||||
self.fontDesign = decoded
|
self.fontDesign = decoded
|
||||||
}
|
}
|
||||||
|
if let animationStyleRaw = try container.decodeIfPresent(String.self, forKey: .digitAnimationStyle),
|
||||||
|
let decoded = DigitAnimationStyle(rawValue: animationStyleRaw) {
|
||||||
|
self.digitAnimationStyle = decoded
|
||||||
|
}
|
||||||
self.showBattery = try container.decodeIfPresent(Bool.self, forKey: .showBattery) ?? self.showBattery
|
self.showBattery = try container.decodeIfPresent(Bool.self, forKey: .showBattery) ?? self.showBattery
|
||||||
self.showDate = try container.decodeIfPresent(Bool.self, forKey: .showDate) ?? self.showDate
|
self.showDate = try container.decodeIfPresent(Bool.self, forKey: .showDate) ?? self.showDate
|
||||||
self.dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) ?? self.dateFormat
|
self.dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) ?? self.dateFormat
|
||||||
@ -157,6 +163,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
try container.encode(fontFamily.rawValue, forKey: .fontFamily)
|
try container.encode(fontFamily.rawValue, forKey: .fontFamily)
|
||||||
try container.encode(fontWeight.rawValue, forKey: .fontWeight)
|
try container.encode(fontWeight.rawValue, forKey: .fontWeight)
|
||||||
try container.encode(fontDesign.rawValue, forKey: .fontDesign)
|
try container.encode(fontDesign.rawValue, forKey: .fontDesign)
|
||||||
|
try container.encode(digitAnimationStyle.rawValue, forKey: .digitAnimationStyle)
|
||||||
try container.encode(showBattery, forKey: .showBattery)
|
try container.encode(showBattery, forKey: .showBattery)
|
||||||
try container.encode(showDate, forKey: .showDate)
|
try container.encode(showDate, forKey: .showDate)
|
||||||
try container.encode(dateFormat, forKey: .dateFormat)
|
try container.encode(dateFormat, forKey: .dateFormat)
|
||||||
@ -449,6 +456,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
lhs.fontFamily == rhs.fontFamily &&
|
lhs.fontFamily == rhs.fontFamily &&
|
||||||
lhs.fontWeight == rhs.fontWeight &&
|
lhs.fontWeight == rhs.fontWeight &&
|
||||||
lhs.fontDesign == rhs.fontDesign &&
|
lhs.fontDesign == rhs.fontDesign &&
|
||||||
|
lhs.digitAnimationStyle == rhs.digitAnimationStyle &&
|
||||||
lhs.showBattery == rhs.showBattery &&
|
lhs.showBattery == rhs.showBattery &&
|
||||||
lhs.showDate == rhs.showDate &&
|
lhs.showDate == rhs.showDate &&
|
||||||
lhs.dateFormat == rhs.dateFormat &&
|
lhs.dateFormat == rhs.dateFormat &&
|
||||||
|
|||||||
@ -105,6 +105,7 @@ class ClockViewModel {
|
|||||||
style.nightModeEndTime = newStyle.nightModeEndTime
|
style.nightModeEndTime = newStyle.nightModeEndTime
|
||||||
style.ambientLightThreshold = newStyle.ambientLightThreshold
|
style.ambientLightThreshold = newStyle.ambientLightThreshold
|
||||||
style.autoBrightness = newStyle.autoBrightness
|
style.autoBrightness = newStyle.autoBrightness
|
||||||
|
style.digitAnimationStyle = newStyle.digitAnimationStyle
|
||||||
style.dateFormat = newStyle.dateFormat
|
style.dateFormat = newStyle.dateFormat
|
||||||
style.respectFocusModes = newStyle.respectFocusModes
|
style.respectFocusModes = newStyle.respectFocusModes
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,8 @@ struct ClockDisplayContainer: View {
|
|||||||
fontWeight: style.fontWeight,
|
fontWeight: style.fontWeight,
|
||||||
fontDesign: style.fontDesign,
|
fontDesign: style.fontDesign,
|
||||||
forceHorizontalMode: style.forceHorizontalMode,
|
forceHorizontalMode: style.forceHorizontalMode,
|
||||||
isDisplayMode: isDisplayMode
|
isDisplayMode: isDisplayMode,
|
||||||
|
animationStyle: style.digitAnimationStyle
|
||||||
)
|
)
|
||||||
.padding(.top, topSpacing)
|
.padding(.top, topSpacing)
|
||||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||||
|
|||||||
@ -35,6 +35,12 @@ struct ColonView: View {
|
|||||||
}
|
}
|
||||||
.fixedSize(horizontal: true, vertical: true)
|
.fixedSize(horizontal: true, vertical: true)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
.phaseAnimator([0, 1]) { content, phase in
|
||||||
|
content
|
||||||
|
.opacity(phase == 1 ? 1.0 : 0.6)
|
||||||
|
} animation: { _ in
|
||||||
|
.easeInOut(duration: 1.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ struct DigitView: View {
|
|||||||
let digitColor: Color
|
let digitColor: Color
|
||||||
let glowIntensity: Double
|
let glowIntensity: Double
|
||||||
let isDisplayMode: Bool
|
let isDisplayMode: Bool
|
||||||
|
let animationStyle: DigitAnimationStyle
|
||||||
@Binding var fontSize: CGFloat
|
@Binding var fontSize: CGFloat
|
||||||
|
|
||||||
init(digit: String,
|
init(digit: String,
|
||||||
@ -32,7 +33,8 @@ struct DigitView: View {
|
|||||||
opacity: Double = 1,
|
opacity: Double = 1,
|
||||||
glowIntensity: Double = 0,
|
glowIntensity: Double = 0,
|
||||||
fontSize: Binding<CGFloat>,
|
fontSize: Binding<CGFloat>,
|
||||||
isDisplayMode: Bool = false) {
|
isDisplayMode: Bool = false,
|
||||||
|
animationStyle: DigitAnimationStyle = .spring) {
|
||||||
self.digit = (digit.count == 1 && "0123456789".contains(digit)) ? digit : "0"
|
self.digit = (digit.count == 1 && "0123456789".contains(digit)) ? digit : "0"
|
||||||
self.fontName = fontName
|
self.fontName = fontName
|
||||||
self.weight = weight
|
self.weight = weight
|
||||||
@ -41,6 +43,7 @@ struct DigitView: View {
|
|||||||
self.digitColor = digitColor
|
self.digitColor = digitColor
|
||||||
self.glowIntensity = glowIntensity
|
self.glowIntensity = glowIntensity
|
||||||
self.isDisplayMode = isDisplayMode
|
self.isDisplayMode = isDisplayMode
|
||||||
|
self.animationStyle = animationStyle
|
||||||
self._fontSize = fontSize
|
self._fontSize = fontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,12 +68,14 @@ struct DigitView: View {
|
|||||||
.foregroundColor(digitColor)
|
.foregroundColor(digitColor)
|
||||||
.blur(radius: glowRadius)
|
.blur(radius: glowRadius)
|
||||||
.opacity(glowOpacity)
|
.opacity(glowOpacity)
|
||||||
|
.modifier(GlowAnimationModifier(style: animationStyle, digit: digit, glowRadius: glowRadius, glowOpacity: glowOpacity))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainText: some View {
|
private var mainText: some View {
|
||||||
baseText
|
baseText
|
||||||
.foregroundColor(digitColor)
|
.foregroundColor(digitColor)
|
||||||
.opacity(opacity)
|
.opacity(opacity)
|
||||||
|
.modifier(DigitAnimationModifier(style: animationStyle, digit: digit, fontSize: fontSize))
|
||||||
.debugBorder(Self.debugShowBorders, color: .orange, label: "Text")
|
.debugBorder(Self.debugShowBorders, color: .orange, label: "Text")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +89,85 @@ struct DigitView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Animation Modifiers
|
||||||
|
|
||||||
|
private struct DigitAnimationModifier: ViewModifier {
|
||||||
|
let style: DigitAnimationStyle
|
||||||
|
let digit: String
|
||||||
|
let fontSize: CGFloat
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
switch style {
|
||||||
|
case .none:
|
||||||
|
content
|
||||||
|
case .spring:
|
||||||
|
content
|
||||||
|
.contentTransition(.numericText())
|
||||||
|
.animation(.snappy(duration: 0.35), value: digit)
|
||||||
|
.phaseAnimator([0, 1], trigger: digit) { content, phase in
|
||||||
|
content
|
||||||
|
.scaleEffect(phase == 1 ? 1.05 : 1.0)
|
||||||
|
.offset(y: phase == 1 ? -fontSize * 0.02 : 0)
|
||||||
|
} animation: { _ in
|
||||||
|
.spring(duration: 0.3, bounce: 0.4)
|
||||||
|
}
|
||||||
|
case .bounce:
|
||||||
|
content
|
||||||
|
.contentTransition(.numericText())
|
||||||
|
.animation(.bouncy(duration: 0.4), value: digit)
|
||||||
|
.phaseAnimator([0, 1], trigger: digit) { content, phase in
|
||||||
|
content
|
||||||
|
.scaleEffect(phase == 1 ? 1.1 : 1.0)
|
||||||
|
} animation: { _ in
|
||||||
|
.spring(duration: 0.4, bounce: 0.5)
|
||||||
|
}
|
||||||
|
case .glitch:
|
||||||
|
content
|
||||||
|
.contentTransition(.numericText())
|
||||||
|
.phaseAnimator([0, 1, 2, 0], trigger: digit) { content, phase in
|
||||||
|
content
|
||||||
|
.offset(x: phase == 1 ? -fontSize * 0.05 : (phase == 2 ? fontSize * 0.05 : 0))
|
||||||
|
.opacity(phase == 1 || phase == 2 ? 0.7 : 1.0)
|
||||||
|
.scaleEffect(phase == 1 ? 1.02 : (phase == 2 ? 0.98 : 1.0))
|
||||||
|
} animation: { _ in
|
||||||
|
.interactiveSpring(response: 0.1, dampingFraction: 0.8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct GlowAnimationModifier: ViewModifier {
|
||||||
|
let style: DigitAnimationStyle
|
||||||
|
let digit: String
|
||||||
|
let glowRadius: CGFloat
|
||||||
|
let glowOpacity: Double
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
switch style {
|
||||||
|
case .none:
|
||||||
|
content
|
||||||
|
case .spring, .bounce:
|
||||||
|
content
|
||||||
|
.phaseAnimator([0, 1], trigger: digit) { content, phase in
|
||||||
|
content
|
||||||
|
.blur(radius: glowRadius * (phase == 1 ? 1.5 : 1.0))
|
||||||
|
.opacity(glowOpacity * (phase == 1 ? 1.2 : 1.0))
|
||||||
|
} animation: { _ in
|
||||||
|
.easeInOut(duration: 0.3)
|
||||||
|
}
|
||||||
|
case .glitch:
|
||||||
|
content
|
||||||
|
.phaseAnimator([0, 1, 2, 0], trigger: digit) { content, phase in
|
||||||
|
content
|
||||||
|
.opacity(phase == 1 || phase == 2 ? 0.4 : 1.0)
|
||||||
|
.blur(radius: glowRadius * (phase == 1 || phase == 2 ? 2.0 : 1.0))
|
||||||
|
} animation: { _ in
|
||||||
|
.easeInOut(duration: 0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
#Preview {
|
#Preview {
|
||||||
@Previewable @State var sharedFontSize: CGFloat = 2000
|
@Previewable @State var sharedFontSize: CGFloat = 2000
|
||||||
@ -98,7 +182,8 @@ struct DigitView: View {
|
|||||||
weight: weight,
|
weight: weight,
|
||||||
design: design,
|
design: design,
|
||||||
glowIntensity: glowIntensity,
|
glowIntensity: glowIntensity,
|
||||||
fontSize: $sharedFontSize)
|
fontSize: $sharedFontSize,
|
||||||
|
animationStyle: .spring)
|
||||||
.border(Color.black)
|
.border(Color.black)
|
||||||
|
|
||||||
DigitView(digit: "1",
|
DigitView(digit: "1",
|
||||||
@ -106,7 +191,8 @@ struct DigitView: View {
|
|||||||
weight: weight,
|
weight: weight,
|
||||||
design: design,
|
design: design,
|
||||||
glowIntensity: glowIntensity,
|
glowIntensity: glowIntensity,
|
||||||
fontSize: $sharedFontSize)
|
fontSize: $sharedFontSize,
|
||||||
|
animationStyle: .spring)
|
||||||
.border(Color.black)
|
.border(Color.black)
|
||||||
|
|
||||||
Text(":")
|
Text(":")
|
||||||
@ -118,7 +204,8 @@ struct DigitView: View {
|
|||||||
weight: weight,
|
weight: weight,
|
||||||
design: design,
|
design: design,
|
||||||
glowIntensity: glowIntensity,
|
glowIntensity: glowIntensity,
|
||||||
fontSize: $sharedFontSize)
|
fontSize: $sharedFontSize,
|
||||||
|
animationStyle: .spring)
|
||||||
.border(Color.black)
|
.border(Color.black)
|
||||||
|
|
||||||
DigitView(digit: "5",
|
DigitView(digit: "5",
|
||||||
@ -126,7 +213,8 @@ struct DigitView: View {
|
|||||||
weight: weight,
|
weight: weight,
|
||||||
design: design,
|
design: design,
|
||||||
glowIntensity: glowIntensity,
|
glowIntensity: glowIntensity,
|
||||||
fontSize: $sharedFontSize)
|
fontSize: $sharedFontSize,
|
||||||
|
animationStyle: .spring)
|
||||||
.border(Color.black)
|
.border(Color.black)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,21 @@ struct AdvancedAppearanceSection: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||||
|
Text("Digit Animation")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
|
||||||
|
Picker("Digit Animation", selection: $style.digitAnimationStyle) {
|
||||||
|
ForEach(DigitAnimationStyle.allCases, id: \.self) { animation in
|
||||||
|
Text(animation.displayName).tag(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.tint(AppAccent.primary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Randomize Color",
|
title: "Randomize Color",
|
||||||
subtitle: "Shift the color every minute",
|
subtitle: "Shift the color every minute",
|
||||||
|
|||||||
@ -36,6 +36,7 @@ struct TimeDisplayView: View {
|
|||||||
let fontDesign: Font.Design
|
let fontDesign: Font.Design
|
||||||
let forceHorizontalMode: Bool
|
let forceHorizontalMode: Bool
|
||||||
let isDisplayMode: Bool
|
let isDisplayMode: Bool
|
||||||
|
let animationStyle: DigitAnimationStyle
|
||||||
@State var fontSize: CGFloat = 100
|
@State var fontSize: CGFloat = 100
|
||||||
@State private var lastCalculatedContainerSize: CGSize = .zero
|
@State private var lastCalculatedContainerSize: CGSize = .zero
|
||||||
|
|
||||||
@ -119,12 +120,12 @@ struct TimeDisplayView: View {
|
|||||||
return Group {
|
return Group {
|
||||||
if portrait {
|
if portrait {
|
||||||
VStack(alignment: .center, spacing: 0) {
|
VStack(alignment: .center, spacing: 0) {
|
||||||
TimeSegment(text: hour, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode)
|
TimeSegment(text: hour, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode, animationStyle: animationStyle)
|
||||||
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: true)
|
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: true)
|
||||||
TimeSegment(text: minute, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode)
|
TimeSegment(text: minute, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode, animationStyle: animationStyle)
|
||||||
if showSeconds {
|
if showSeconds {
|
||||||
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: true)
|
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: true)
|
||||||
TimeSegment(text: secondsText, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode)
|
TimeSegment(text: secondsText, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode, animationStyle: animationStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity) // Center horizontally in portrait
|
.frame(maxWidth: .infinity) // Center horizontally in portrait
|
||||||
@ -140,16 +141,16 @@ struct TimeDisplayView: View {
|
|||||||
let segmentWidth = fixedDigitWidth * 2 // Each segment has 2 digits
|
let segmentWidth = fixedDigitWidth * 2 // Each segment has 2 digits
|
||||||
|
|
||||||
HStack(alignment: .center, spacing: 0) {
|
HStack(alignment: .center, spacing: 0) {
|
||||||
TimeSegment(text: hour, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode)
|
TimeSegment(text: hour, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode, animationStyle: animationStyle)
|
||||||
.frame(width: segmentWidth)
|
.frame(width: segmentWidth)
|
||||||
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: false)
|
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: false)
|
||||||
.frame(width: dotDiameter)
|
.frame(width: dotDiameter)
|
||||||
TimeSegment(text: minute, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode)
|
TimeSegment(text: minute, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode, animationStyle: animationStyle)
|
||||||
.frame(width: segmentWidth)
|
.frame(width: segmentWidth)
|
||||||
if showSeconds {
|
if showSeconds {
|
||||||
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: false)
|
ColonView(dotDiameter: dotDiameter, spacing: dotSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight, isHorizontal: false)
|
||||||
.frame(width: dotDiameter)
|
.frame(width: dotDiameter)
|
||||||
TimeSegment(text: secondsText, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode)
|
TimeSegment(text: secondsText, fontSize: $fontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign, isDisplayMode: isDisplayMode, animationStyle: animationStyle)
|
||||||
.frame(width: segmentWidth)
|
.frame(width: segmentWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,7 +339,8 @@ struct TimeDisplayView: View {
|
|||||||
fontWeight: .medium,
|
fontWeight: .medium,
|
||||||
fontDesign: .default,
|
fontDesign: .default,
|
||||||
forceHorizontalMode: false,
|
forceHorizontalMode: false,
|
||||||
isDisplayMode: false
|
isDisplayMode: false,
|
||||||
|
animationStyle: .spring
|
||||||
)
|
)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ struct TimeSegment: View {
|
|||||||
let fontWeight: Font.Weight
|
let fontWeight: Font.Weight
|
||||||
let fontDesign: Font.Design
|
let fontDesign: Font.Design
|
||||||
let isDisplayMode: Bool
|
let isDisplayMode: Bool
|
||||||
|
let animationStyle: DigitAnimationStyle
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// Use fixed digit width based on widest digit ("8") for stable layout
|
// Use fixed digit width based on widest digit ("8") for stable layout
|
||||||
@ -45,7 +46,8 @@ struct TimeSegment: View {
|
|||||||
opacity: clampedOpacity,
|
opacity: clampedOpacity,
|
||||||
glowIntensity: glowIntensity,
|
glowIntensity: glowIntensity,
|
||||||
fontSize: $fontSize,
|
fontSize: $fontSize,
|
||||||
isDisplayMode: isDisplayMode
|
isDisplayMode: isDisplayMode,
|
||||||
|
animationStyle: animationStyle
|
||||||
)
|
)
|
||||||
.frame(width: fixedDigitWidth) // Fixed width for stability
|
.frame(width: fixedDigitWidth) // Fixed width for stability
|
||||||
.debugBorder(Self.debugShowBorders, color: .red, label: "D\(index)")
|
.debugBorder(Self.debugShowBorders, color: .red, label: "D\(index)")
|
||||||
@ -72,7 +74,8 @@ struct TimeSegment: View {
|
|||||||
fontFamily: .system,
|
fontFamily: .system,
|
||||||
fontWeight: .regular,
|
fontWeight: .regular,
|
||||||
fontDesign: .default,
|
fontDesign: .default,
|
||||||
isDisplayMode: true
|
isDisplayMode: true,
|
||||||
|
animationStyle: .spring
|
||||||
)
|
)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// DigitAnimationStyle.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by AI Agent on 2/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Available animation styles for clock digits
|
||||||
|
public enum DigitAnimationStyle: String, CaseIterable, Codable {
|
||||||
|
case none
|
||||||
|
case spring
|
||||||
|
case bounce
|
||||||
|
case glitch
|
||||||
|
|
||||||
|
public var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .none: return "None"
|
||||||
|
case .spring: return "Spring"
|
||||||
|
case .bounce: return "Bounce"
|
||||||
|
case .glitch: return "Glitch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -241,7 +241,8 @@ private struct TestContentView: View {
|
|||||||
fontWeight: fontWeight,
|
fontWeight: fontWeight,
|
||||||
fontDesign: fontDesign,
|
fontDesign: fontDesign,
|
||||||
forceHorizontalMode: true,
|
forceHorizontalMode: true,
|
||||||
isDisplayMode: false)
|
isDisplayMode: false,
|
||||||
|
animationStyle: .spring)
|
||||||
|
|
||||||
TimeSegment(
|
TimeSegment(
|
||||||
text: digit,
|
text: digit,
|
||||||
@ -252,7 +253,8 @@ private struct TestContentView: View {
|
|||||||
fontFamily: font, // FontFamily
|
fontFamily: font, // FontFamily
|
||||||
fontWeight: fontWeight,
|
fontWeight: fontWeight,
|
||||||
fontDesign: fontDesign,
|
fontDesign: fontDesign,
|
||||||
isDisplayMode: false
|
isDisplayMode: false,
|
||||||
|
animationStyle: .spring
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.frame(width: 400, height: 200)
|
.frame(width: 400, height: 200)
|
||||||
@ -318,7 +320,8 @@ private struct FontSampleCell: View {
|
|||||||
fontFamily: font, // FontFamily
|
fontFamily: font, // FontFamily
|
||||||
fontWeight: weight,
|
fontWeight: weight,
|
||||||
fontDesign: design,
|
fontDesign: design,
|
||||||
isDisplayMode: false)
|
isDisplayMode: false,
|
||||||
|
animationStyle: .spring)
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
.border(Color.black)
|
.border(Color.black)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user