diff --git a/TrackWeight.xcodeproj/project.pbxproj b/TrackWeight.xcodeproj/project.pbxproj index a343c4f..153c11d 100644 --- a/TrackWeight.xcodeproj/project.pbxproj +++ b/TrackWeight.xcodeproj/project.pbxproj @@ -333,7 +333,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 9ZRLG6277G; + DEVELOPMENT_TEAM = BQ65WXL7J3; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -365,6 +365,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 9ZRLG6277G; + "DEVELOPMENT_TEAM[sdk=macosx*]" = BQ65WXL7J3; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/TrackWeight/HomeView.swift b/TrackWeight/HomeView.swift index 5dadf96..a3901c5 100644 --- a/TrackWeight/HomeView.swift +++ b/TrackWeight/HomeView.swift @@ -6,18 +6,22 @@ import SwiftUI struct HomeView: View { - let onBegin: () -> Void + @State private var isHovering: Bool = false + @State private var isPressing: Bool = false + + let onBegin: () -> Void + var body: some View { VStack(spacing: 40) { Spacer() - + // Title section VStack(spacing: 15) { - Image(systemName: "scalemass") + Image(systemName: "scalemass.fill") .font(.system(size: 80, weight: .ultraLight)) .foregroundStyle(Color.blue) - + Text("TrackWeight") .font(.system(size: 48, weight: .bold, design: .rounded)) .foregroundStyle( @@ -28,78 +32,100 @@ struct HomeView: View { ) ) } - - // Description section - VStack(spacing: 20) { + + // Description and Limitations section + VStack(spacing: 30) { Text("Transform your MacBook trackpad into a precision scale using Apple's private MultitouchSupport framework to read pressure values with gram-level accuracy.") - .font(.system(size: 18, weight: .medium)) + .font(.system(size: 19, weight: .medium)) .foregroundStyle(Color.primary) .multilineTextAlignment(.center) - .frame(maxWidth: 550) - - // Limitations section - VStack(spacing: 12) { + .frame(maxWidth: 600) + .padding(.horizontal, 20) + + // Limitations section container but with glassmorphism effect this do not work as i thought + VStack(spacing: 15) { Text("Important Limitations") - .font(.system(size: 16, weight: .semibold)) + .font(.system(size: 17, weight: .semibold)) .foregroundStyle(Color.orange) - - VStack(spacing: 8) { + + VStack(alignment: .leading, spacing: 10) { LimitationRow( icon: "hand.point.up.left", - text: "Requires finger contact for capacitive detection" + text: "Requires finger contact for capacitive detection. Objects must be non-conductive and have sufficient surface area." ) LimitationRow( icon: "chart.line.downtrend.xyaxis", - text: "May experience pressure drift when placing objects" + text: "May experience slight pressure drift when placing objects due to temperature or contact changes. Calibration is recommended." ) LimitationRow( - icon: "cube.fill", - text: "Metal/magnetic objects may not work" + icon: "cube.box.fill", + text: "Metal or magnetic objects may interfere with the trackpad's sensors and may not register accurately or at all." ) } + .padding(.top, 5) } .padding(.horizontal, 30) - .padding(.vertical, 20) + .padding(.vertical, 25) .background( - RoundedRectangle(cornerRadius: 15) - .foregroundColor(Color.orange.opacity(0.05)) - .overlay( - RoundedRectangle(cornerRadius: 15) - .stroke(Color.orange.opacity(0.2), lineWidth: 1) - ) + RoundedRectangle(cornerRadius: 20) + .fill(.ultraThinMaterial) ) - .frame(maxWidth: 500) - } - - Spacer() - - // Begin button - Button(action: onBegin) { - HStack(spacing: 10) { - Text("Begin") - .font(.system(size: 18, weight: .semibold)) - Image(systemName: "arrow.right") - .font(.system(size: 16, weight: .semibold)) - } - .foregroundStyle(Color.white) - .frame(width: 140, height: 50) - .background( + .overlay( RoundedRectangle(cornerRadius: 25) - .fill( - LinearGradient( - colors: [.blue, .teal], - startPoint: .leading, - endPoint: .trailing - ) - ) - .shadow(color: .blue.opacity(0.3), radius: 10, x: 0, y: 5) + .stroke(Color.white.opacity(0.1), lineWidth: 1) ) + .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) + .frame(maxWidth: 550) + .padding(.horizontal, 20) } - .buttonStyle(.plain) - .scaleEffect(1.0) - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: true) - .padding(.vertical, 10) + + // Begin button + Button(action: { + self.isPressing = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.isPressing = false + onBegin() + } + }) { + HStack(spacing: 10) { + Text("Begin") + .font(.system(size: 18, weight: .semibold)) + Image(systemName: "scalemass.fill") + .font(.system(size: 16, weight: .semibold)) + // Icon movement on hover + .offset(x: isHovering ? 3 : 0) + } + .foregroundStyle(.white) + .frame(width: 140, height: 50) + .background( + RoundedRectangle(cornerRadius: 20) + .fill( + LinearGradient( + + colors: isPressing ? [.init(red: 0.35, green: 0.13, blue: 0.71), .init(red: 0.49, green: 0.23, blue: 0.98)] : // Darker on press + (isHovering ? [.init(red: 0.49, green: 0.23, blue: 0.98), .init(red: 0.65, green: 0.54, blue: 0.98)] : // Brighter on hover + [.blue, .teal]), + startPoint: .leading, + endPoint: .trailing + ) + ) + ) + + .shadow(color: Color.black.opacity(isPressing ? 0.15 : (isHovering ? 0.35 : 0.2)), + radius: isPressing ? 8 : (isHovering ? 25 : 15), + x: 0, + y: isPressing ? 2 : (isHovering ? 8 : 4)) + } + .buttonStyle(.plain) + .padding(.vertical, 10) + .scaleEffect(isPressing ? 0.98 : (isHovering ? 1.02 : 1.0)) + .offset(y: isHovering ? -3 : 0) + .animation(.spring(response: 0.3, dampingFraction: 0.8), value: isHovering) + .animation(.easeOut(duration: 0.1), value: isPressing) + .onHover { hover in + self.isHovering = hover + } Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -110,24 +136,52 @@ struct HomeView: View { struct LimitationRow: View { let icon: String let text: String + + @State private var isRowHovering: Bool = false + var body: some View { - HStack(spacing: 12) { + HStack(alignment: .top, spacing: 10) { + Image(systemName: icon) - .font(.system(size: 14, weight: .medium)) + .font(.system(size: 18, weight: .medium)) .foregroundStyle(Color.orange) - .frame(width: 20) + .frame(width: 25) + Text(text) - .font(.system(size: 14, weight: .medium)) + .font(.system(size: 15, weight: .regular)) .foregroundStyle(Color.secondary) - .multilineTextAlignment(.leading) - - Spacer() + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.vertical, 5) + .padding(.horizontal, 10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(isRowHovering ? Color.white.opacity(0.08) : Color.clear) + ) + .cornerRadius(10) + .scaleEffect(isRowHovering ? 1.01 : 1.0) + .shadow(color: Color.black.opacity(isRowHovering ? 0.1 : 0), + radius: isRowHovering ? 5 : 0, + x: 0, + y: isRowHovering ? 2 : 0) + + .animation(.easeOut(duration: 0.2), value: isRowHovering) + .onHover { hover in + isRowHovering = hover } } } #Preview { - HomeView(onBegin: {}) + ZStack { + LinearGradient(gradient: Gradient(colors: [.purple, .blue, .green]), startPoint: .topLeading, endPoint: .bottomTrailing) + .edgesIgnoringSafeArea(.all) + + HomeView(onBegin: { + print("Begin button tapped from preview!") + }) + } } diff --git a/TrackWeight/ScaleView.swift b/TrackWeight/ScaleView.swift index 97d9fb5..ffddf41 100644 --- a/TrackWeight/ScaleView.swift +++ b/TrackWeight/ScaleView.swift @@ -4,82 +4,59 @@ // import SwiftUI +import AppKit + struct ScaleView: View { @StateObject private var viewModel = ScaleViewModel() @State private var scaleCompression: CGFloat = 0 - @State private var displayShake = false - @State private var particleOffset: CGFloat = 0 @State private var keyMonitor: Any? - + var body: some View { GeometryReader { geometry in ZStack { - // Animated gradient background -// LinearGradient( -// colors: [ -// Color(red: 0.95, green: 0.97, blue: 1.0), -// Color(red: 0.85, green: 0.92, blue: 0.98) -// ], -// startPoint: .topLeading, -// endPoint: .bottomTrailing -// ) -// .ignoresSafeArea() - VStack(spacing: geometry.size.height * 0.06) { - // Title with subtitle directly underneath + // Header VStack(spacing: 8) { Text("Track Weight") .font(.system(size: min(max(geometry.size.width * 0.05, 24), 42), weight: .bold, design: .rounded)) .foregroundStyle( - LinearGradient( - colors: [.blue, .teal, .cyan], - startPoint: .leading, - endPoint: .trailing - ) + LinearGradient(colors: [.blue, .teal, .cyan], startPoint: .leading, endPoint: .trailing) ) .minimumScaleFactor(0.7) .lineLimit(1) - + Text("Place your finger on the trackpad to begin") .font(.system(size: min(max(geometry.size.width * 0.022, 14), 18), weight: .medium)) - .foregroundStyle(.gray) + .foregroundStyle(.gray.opacity(0.7)) .multilineTextAlignment(.center) .frame(maxWidth: geometry.size.width * 0.8) .opacity(viewModel.hasTouch ? 0 : 1) .animation(.easeInOut(duration: 0.5), value: viewModel.hasTouch) } - .frame(height: max(geometry.size.height * 0.15, 80)) // Fixed height for title + subtitle - .frame(maxWidth: .infinity) // Ensure full width for centering - + .frame(height: max(geometry.size.height * 0.15, 80)) + .frame(maxWidth: .infinity) + Spacer() - - // Cartoon Digital Scale - responsive size - HStack { - Spacer() - CartoonScaleView( - weight: viewModel.currentWeight, - hasTouch: viewModel.hasTouch, - compression: $scaleCompression, - displayShake: $displayShake, - scaleFactor: min(geometry.size.width / 700, geometry.size.height / 500) - ) - Spacer() - } - + + FitnessScaleView( + weight: viewModel.currentWeight, + hasTouch: viewModel.hasTouch, + compression: $scaleCompression, + scaleFactor: min(geometry.size.width / 700, geometry.size.height / 550) // Adjusted scale + ) + Spacer() - - // Fixed container for button to prevent jumping + + // Controls VStack(spacing: 10) { if viewModel.hasTouch { Text("Press spacebar or click to zero") .font(.system(size: min(max(geometry.size.width * 0.018, 12), 16), weight: .medium)) - .foregroundStyle(.gray) + .foregroundStyle(.gray.opacity(0.7)) } - - Button(action: { - viewModel.zeroScale() - }) { + + Button(action: viewModel.zeroScale) { HStack(spacing: 8) { Image(systemName: "arrow.clockwise") .font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold)) @@ -87,37 +64,34 @@ struct ScaleView: View { .font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold)) } .foregroundStyle(.white) - .frame(width: min(max(geometry.size.width * 0.2, 140), 180), + .frame(width: min(max(geometry.size.width * 0.2, 140), 180), height: min(max(geometry.size.height * 0.08, 40), 55)) .background( RoundedRectangle(cornerRadius: 25) - .fill( - LinearGradient( - colors: [.blue, .teal], - startPoint: .leading, - endPoint: .trailing - ) - ) + .fill(LinearGradient(colors: [.blue, .teal], startPoint: .leading, endPoint: .trailing)) ) + .shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4) } .buttonStyle(.plain) .opacity(viewModel.hasTouch ? 1 : 0) .scaleEffect(viewModel.hasTouch ? 1 : 0.8) .animation(.spring(response: 0.4, dampingFraction: 0.8), value: viewModel.hasTouch) } - .frame(height: min(max(geometry.size.height * 0.15, 80), 100)) // Fixed space for button + instruction - .frame(maxWidth: .infinity) // Ensure full width for centering + .frame(height: min(max(geometry.size.height * 0.15, 80), 100)) + .frame(maxWidth: .infinity) } .padding(.horizontal, max(geometry.size.width * 0.05, 20)) .padding(.vertical, max(geometry.size.height * 0.03, 20)) - .frame(maxWidth: .infinity, maxHeight: .infinity) // Ensure the VStack takes full available space + .frame(maxWidth: .infinity, maxHeight: .infinity) } } .focusable() .modifier(FocusEffectModifier()) .onChange(of: viewModel.currentWeight) { newWeight in + // The `compression` value drives all the animations. + // It's a value from 0.0 (no weight) to 0.2 (max weight). withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { - scaleCompression = CGFloat(min(newWeight / 100.0, 0.2)) + scaleCompression = CGFloat(min(Float(newWeight) / 200.0, 0.2)) } } .onAppear { @@ -129,7 +103,7 @@ struct ScaleView: View { removeKeyMonitoring() } } - + private func setupKeyMonitoring() { keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in // Space key code is 49 @@ -139,7 +113,7 @@ struct ScaleView: View { return event } } - + private func removeKeyMonitoring() { if let monitor = keyMonitor { NSEvent.removeMonitor(monitor) @@ -148,133 +122,140 @@ struct ScaleView: View { } } -struct CartoonScaleView: View { +// MARK: - Fitness Character View (with Tray) +struct FitnessScaleView: View { let weight: Float let hasTouch: Bool - @Binding var compression: CGFloat - @Binding var displayShake: Bool + @Binding var compression: CGFloat // Value from 0.0 to 0.2 let scaleFactor: CGFloat - + var body: some View { - VStack(spacing: 0) { - // Scale platform (top) - responsive to weight - RoundedRectangle(cornerRadius: 8) - .fill( - LinearGradient( - colors: [.gray.opacity(0.3), .gray.opacity(0.6)], - startPoint: .top, - endPoint: .bottom - ) - ) - .frame(width: 200 * scaleFactor, height: 12 * scaleFactor) - .offset(y: compression * 15) + // This offset will move the arms and tray down as weight increases + let trayOffset = compression * 200 * scaleFactor + + ZStack { + // MARK: - Body, Face, and Legs + VStack(spacing: 0) { + // Main Body + ZStack { + RoundedRectangle(cornerRadius: 20 * scaleFactor) + .fill(LinearGradient(colors: [Color(red: 0.98, green: 0.98, blue: 1.0), Color(red: 0.88, green: 0.88, blue: 0.92)], startPoint: .top, endPoint: .bottom)) + .frame(width: 180 * scaleFactor, height: 160 * scaleFactor) + .shadow(color: .black.opacity(0.2), radius: 10, x: 0, y: 5) + + // Face + VStack(spacing: 10 * scaleFactor) { + HStack(spacing: 30 * scaleFactor) { + EyeView(size: 20 * scaleFactor, isHappy: weight > 5) + EyeView(size: 20 * scaleFactor, isHappy: weight > 5) + } + + Group { + if weight > 5 { + let smileWidth = 35 * scaleFactor + CGFloat(min(weight, 100) / 5) * scaleFactor + Path { path in + path.move(to: .zero) + path.addQuadCurve(to: CGPoint(x: smileWidth, y: 0), control: CGPoint(x: smileWidth / 2, y: 15 * scaleFactor)) + } + .stroke(.black, style: StrokeStyle(lineWidth: 4 * scaleFactor, lineCap: .round)) + .frame(width: smileWidth) + } else { + Rectangle().fill(.black).frame(width: 20 * scaleFactor, height: 3 * scaleFactor) + } + } + .animation(.spring(response: 0.3, dampingFraction: 0.5), value: weight) + .frame(height: 20 * scaleFactor) + } + } + .offset(y: 50 * scaleFactor) // Move body down to make room for tray + + // Legs + HStack(spacing: 100 * scaleFactor) { + RoundedRectangle(cornerRadius: 4, style: .continuous).fill(.gray.opacity(0.7)).frame(width: 35 * scaleFactor, height: 20 * scaleFactor) + RoundedRectangle(cornerRadius: 4, style: .continuous).fill(.gray.opacity(0.7)).frame(width: 35 * scaleFactor, height: 20 * scaleFactor) + } + .offset(y: 45 * scaleFactor) + } - // Scale body + // MARK: - Arms ZStack { - // Main body - RoundedRectangle(cornerRadius: 20) - .fill( - LinearGradient( - colors: [ - Color(red: 0.95, green: 0.95, blue: 0.97), - Color(red: 0.85, green: 0.85, blue: 0.90) - ], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .frame(width: 250 * scaleFactor, height: 150 * scaleFactor) - .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 8) + // Left Arm + ArmView(scaleFactor: scaleFactor) + .rotationEffect(.degrees(180)) // Flip it + .offset(x: -80 * scaleFactor, y: 30 * scaleFactor + trayOffset) + + // Right Arm + ArmView(scaleFactor: scaleFactor) + .rotationEffect(.degrees(180)) + .offset(x: 80 * scaleFactor, y: 30 * scaleFactor + trayOffset) + } + + // MARK: - Tray and Display + ZStack { + // Tray + RoundedRectangle(cornerRadius: 15 * scaleFactor) + .fill(Color(red: 0.85, green: 0.85, blue: 0.90)) + .frame(width: 220 * scaleFactor, height: 80 * scaleFactor) + .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: -3) - // Display screen - RoundedRectangle(cornerRadius: 12) + // Digital Display on the tray + RoundedRectangle(cornerRadius: 8 * scaleFactor) .fill(.black) - .frame(width: 180 * scaleFactor, height: 60 * scaleFactor) - .offset(y: -10) + .frame(width: 110 * scaleFactor, height: 40 * scaleFactor) .overlay( - RoundedRectangle(cornerRadius: 12) - .fill( - LinearGradient( - colors: [.teal.opacity(0.8), .blue.opacity(0.6)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .frame(width: 176 * scaleFactor, height: 56 * scaleFactor) - .offset(y: -10) + RoundedRectangle(cornerRadius: 8 * scaleFactor) + .fill(LinearGradient(colors: [.teal.opacity(0.8), .blue.opacity(0.6)], startPoint: .top, endPoint: .bottom)) + .padding(2 * scaleFactor) ) - // Weight display - VStack(spacing: 2) { - Text(String(format: "%.1f", weight)) - .font(.system(size: 32 * scaleFactor, weight: .bold, design: .monospaced)) - .foregroundStyle(.white) - .shadow(color: .teal, radius: hasTouch ? 2 : 0) - .animation(.easeInOut(duration: 0.2), value: weight) - - Text("grams") - .font(.system(size: 12 * scaleFactor, weight: .medium)) - .foregroundStyle(.white.opacity(0.8)) - } - .offset(y: -10) - - // Status indicator - simple and clean - if hasTouch { - Circle() - .fill(.teal) - .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) - .offset(x: 90 * scaleFactor, y: -50 * scaleFactor) - } - - // Fun face on the scale - positioned below the display screen - VStack(spacing: 8 * scaleFactor) { - // Eyes - HStack(spacing: 15 * scaleFactor) { - Circle() - .fill(.black) - .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) - Circle() - .fill(.black) - .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) - } - - // Responsive mouth expression - Group { - if hasTouch && weight > 5 { - // Happy mouth when weighing something substantial - Path { path in - path.move(to: CGPoint(x: 0, y: 0)) - path.addQuadCurve(to: CGPoint(x: 20, y: 0), control: CGPoint(x: 0, y: 15)) - } - .stroke(.black, lineWidth: 2 * scaleFactor) - .frame(width: 20 * scaleFactor, height: 10 * scaleFactor) - } else { - // Neutral mouth - Rectangle() - .fill(.black) - .frame(width: 12 * scaleFactor, height: 2 * scaleFactor) - } - } - .animation(.easeInOut(duration: 0.3), value: weight > 5) - } - .offset(y: 60 * scaleFactor) // Position well below the display screen + Text(String(format: "%.1f", weight)) + .font(.system(size: 22 * scaleFactor, weight: .heavy, design: .monospaced)) + .foregroundColor(.white) + .shadow(color: .teal, radius: hasTouch ? 4 : 0) } + .offset(y: -40 * scaleFactor + trayOffset) // Move tray up and animate with offset + } + } +} + + +// MARK: - Helper Views +struct ArmView: View { + let scaleFactor: CGFloat + + var body: some View { + ZStack { + // Main arm part + RoundedRectangle(cornerRadius: 10 * scaleFactor) + .fill(Color(red: 0.9, green: 0.9, blue: 0.92)) + .frame(width: 30 * scaleFactor, height: 80 * scaleFactor) - // Scale legs - HStack(spacing: 140 * scaleFactor) { - ForEach(0..<2, id: \.self) { _ in - RoundedRectangle(cornerRadius: 4) - .fill(.gray.opacity(0.7)) - .frame(width: 12 * scaleFactor, height: 25 * scaleFactor) - .offset(y: compression * 3) - } + // Hand/Holder part + RoundedRectangle(cornerRadius: 8 * scaleFactor) + .fill(Color(red: 0.85, green: 0.85, blue: 0.90)) + .frame(width: 40 * scaleFactor, height: 20 * scaleFactor) + .offset(y: -40 * scaleFactor) + } + } +} + +struct EyeView: View { + let size: CGFloat + var isHappy: Bool = false + + var body: some View { + ZStack { + Circle().fill(.black).frame(width: size, height: size) + if isHappy { + Circle().fill(.white).frame(width: size * 0.4, height: size * 0.4).offset(x: -size * 0.15, y: -size * 0.15) + } else { + Circle().fill(.white).frame(width: size * 0.2, height: size * 0.2).offset(x: size * 0.2, y: -size * 0.2) } - .offset(y: -5) } - .animation(.spring(response: 0.4, dampingFraction: 0.8), value: compression) } } +// MARK: - Modifiers struct FocusEffectModifier: ViewModifier { func body(content: Content) -> some View { if #available(macOS 14.0, *) { @@ -285,6 +266,7 @@ struct FocusEffectModifier: ViewModifier { } } +// MARK: - Preview #Preview { ScaleView() }