// // WaterHeartView.swift // SodaLive // // Created by klaus on 11/5/25. // import SwiftUI struct WaterHeartView: View { var progress: CGFloat = 0 // 0...1 var show: Bool = false // 하트 등장 여부 var phase: CGFloat = 0 // 파도 위상 (애니메이션) var body: some View { GeometryReader { geo in let size = min(geo.size.width, geo.size.height) ZStack { // 하트 외곽선 HeartShape() .stroke(lineWidth: size * 0.01) .foregroundStyle(Color(hex: "ff959a")) .opacity(show ? 1 : 0) // 물 채우는 레이어들 (마스크로 하트 내부만 보이도록) ZStack { // 아래 물(기본 채움) Rectangle() .fill(Color(hex: "ff959a")) .frame(height: geo.size.height * progress) .position(x: geo.size.width/2, y: geo.size.height - (geo.size.height * progress)/2) // 파도 라인 (상단에 얹히는 살짝 흔들리는 수면) WaveShape(progress: progress, phase: phase, amplitude: max(2, size * 0.015)) .fill(Color(hex: "ff959a")) } .mask(HeartShape()) .opacity(show ? 1 : 0) .animation(.easeInOut(duration: 0.2), value: show) } } .aspectRatio(1, contentMode: .fit) } } struct HeartShape: Shape { func path(in rect: CGRect) -> Path { let w = rect.width let h = rect.height let scale: CGFloat = 0.9 // 살짝 여백 let cx = rect.midX let cy = rect.midY let r = min(w, h) * 0.5 * scale func pt(_ nx: CGFloat, _ ny: CGFloat) -> CGPoint { CGPoint(x: cx + nx * r, y: cy + ny * r) } var p = Path() // 위쪽 파임 p.move(to: pt(0.0, -0.45)) // 오른쪽 반쪽: 둥근 윗볼 -> 어깨 -> 바닥 포인트 p.addCurve( to: pt(0.0, 0.65), // 바닥 포인트(최저점) control1: pt(0.62, -1.02), // 오른쪽 윗볼의 정점 쪽(더 둥글게/높게) control2: pt(1.22, -0.04) // 오른쪽 어깨(조금 위로, 더 바깥쪽) ) // 왼쪽 반쪽: 대칭 p.addCurve( to: pt(0.0, -0.45), // 시작점(위쪽 파임)으로 회귀 control1: pt(-1.22, -0.04), // 왼쪽 어깨 control2: pt(-0.62, -1.02) // 왼쪽 윗볼 ) p.closeSubpath() return p } } /// 물결 라인 (위로 채우는 면적의 상단을 살짝 흔들리게) struct WaveShape: Shape { /// 0...1 채움 비율 (1이면 가득) var progress: CGFloat /// 파도 진행(위상) var phase: CGFloat /// 진폭 var amplitude: CGFloat func path(in rect: CGRect) -> Path { let width = rect.width let height = rect.height // progress가 0이면 하단, 1이면 상단 let waterLevel = height * (1 - progress) var path = Path() path.move(to: CGPoint(x: 0, y: waterLevel)) // 부드러운 파형 let wavelength = width / 1.2 let step: CGFloat = 4 for x in stride(from: 0, through: width, by: step) { let relative = x / wavelength let y = waterLevel + sin(relative * 2 * .pi + phase) * amplitude path.addLine(to: CGPoint(x: x, y: y)) } // 바깥쪽 닫기 (아래쪽 영역 채움) path.addLine(to: CGPoint(x: width, y: height)) path.addLine(to: CGPoint(x: 0, y: height)) path.closeSubpath() return path } var animatableData: AnimatablePair { get { AnimatablePair(progress, phase) } set { progress = newValue.first phase = newValue.second } } } #Preview { WaterHeartView() }