Last active
February 14, 2025 03:20
-
-
Save alexjlockwood/9522ae937060b41a957ac05e0c6a76db to your computer and use it in GitHub Desktop.
A cute lil happy valentines day animation in SwiftUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
/// Happy Valentines Day! | |
struct HappyValentinesDayView: View { | |
@State private var progress: CGFloat = 0 | |
var body: some View { | |
AnimatedStrokePath(progress: progress) | |
.trim(from: 0, to: progress) | |
.stroke(.red, style: .init(lineWidth: 6, lineCap: .round, lineJoin: .round)) | |
.animation(.linear(duration: 8), value: progress) | |
.frame(width: .infinity) | |
.aspectRatio(kViewportWidth / kViewportHeight, contentMode: .fit) | |
.onAppear { progress = 1 } | |
} | |
private struct AnimatedStrokePath: Shape { | |
var progress: CGFloat | |
func path(in rect: CGRect) -> Path { | |
var path = Path() | |
/// Converts a point from viewport coordinates to device pixel coordinates. | |
func fromViewport(_ point: CGPoint) -> CGPoint { | |
return .init( | |
x: point.x * rect.width / kViewportWidth, | |
y: point.y * rect.height / kViewportHeight | |
) | |
} | |
for command in kPathCommands { | |
switch command { | |
case .move(let end): | |
path.move(to: fromViewport(end)) | |
case .curve(let cp1, let cp2, let end): | |
path.addCurve( | |
to: fromViewport(end), | |
control1: fromViewport(cp1), | |
control2: fromViewport(cp2) | |
) | |
} | |
} | |
return path | |
} | |
} | |
} | |
private enum PathCommand { | |
case move(end: CGPoint) | |
case curve(cp1: CGPoint, cp2: CGPoint, end: CGPoint) | |
} | |
/// An extremely primitive SVG path parser (please don't copy/paste this into production). | |
private func parseSVGPath(_ pathData: String) -> [PathCommand] { | |
var commands: [PathCommand] = [] | |
let components = pathData.split(whereSeparator: { $0.isWhitespace }) | |
var index = 0 | |
while index < components.count { | |
let command = components[index] | |
index += 1 | |
switch command { | |
case "M": | |
let x = components[index].toFloat() | |
let y = components[index + 1].toFloat() | |
commands.append(.move(end: .init(x: x, y: y))) | |
index += 2 | |
case "C": | |
let cx1 = components[index].toFloat() | |
let cy1 = components[index + 1].toFloat() | |
let cx2 = components[index + 2].toFloat() | |
let cy2 = components[index + 3].toFloat() | |
let x = components[index + 4].toFloat() | |
let y = components[index + 5].toFloat() | |
commands.append( | |
.curve( | |
cp1: .init(x: cx1, y: cy1), | |
cp2: .init(x: cx2, y: cy2), | |
end: .init(x: x, y: y) | |
) | |
) | |
index += 6 | |
default: | |
break | |
} | |
} | |
return commands | |
} | |
private extension String.SubSequence { | |
func toFloat() -> CGFloat { | |
return CGFloat(Double(self) ?? 0) | |
} | |
} | |
/// Values taken from the viewBox of the SVG. | |
private let kViewportWidth: CGFloat = 3038 | |
private let kViewportHeight: CGFloat = 1551 | |
/// Drawn in Figma as a single stroked VectorNode and exported as an SVG. Then imported | |
/// the SVG into https://shapeshifter.design to re-order the incorrect paths/subpaths | |
/// in the SVG string. Then re-exported and copy/pasted into here. | |
private let kPathCommands = parseSVGPath( | |
"M 1074 21 C 1022.92 106.782 981.854 183.96 953.454 250.459 M 953.454 250.459 C 906.44 360.545 894.128 441.363 928.499 483.5 M 840 255.5 C 882.872 253.996 919.775 252.396 953.454 250.459 M 953.454 250.459 C 1038.3 245.578 1102.68 238.552 1190.5 225.5 M 1310 43 C 1240.53 138.805 1207.39 192.892 1166 290.5 C 1148.78 358.39 1161.4 376.346 1225 373.5 M 1470.5 196 C 1465.49 186.765 1440.09 154.393 1400.5 172.5 C 1360.91 190.607 1227.16 316.309 1267 372 C 1306.84 427.691 1433.68 231.301 1439.5 223.756 C 1445.32 216.212 1369.62 383.767 1446 396.5 M 1517 157.5 C 1544.71 152.835 1565.52 145.99 1582.5 152 C 1599.48 158.01 1557.62 241.778 1541.5 275 C 1498.06 372.429 1502.96 367.601 1484.5 413 C 1469.8 451.299 1465.67 472.764 1456 511.5 C 1450.2 540.301 1448.3 551.842 1465 566.5 C 1480 570 1489.94 567.76 1509 554.5 M 1558 247.5 C 1586.86 218.112 1623.5 178 1667 178 C 1698.5 178 1702.5 190 1703 216.5 C 1703.5 243 1685 284.5 1669.5 307 C 1621.18 364.842 1588.03 380.518 1517.5 377 M 1753.5 158 C 1753.5 158 1769.53 150.216 1797.5 152 C 1825.47 153.783 1799 221 1799 221 L 1688.31 483 L 1669.38 522 C 1669.38 522 1630.35 603.764 1622.5 616 C 1597.5 662.677 1579.97 684.609 1536.5 709 C 1507.37 726.923 1479.5 734 1457.5 730 C 1435.5 726 1406.5 703 1394.5 659 C 1386.95 608.98 1394.5 572.5 1429.5 537.5 C 1469.5 504.5 1518.68 490.081 1566 492.5 M 1566 492.5 C 1594.55 492.855 1617.06 493.831 1636 495.686 M 1566 492.5 L 1636 495.686 M 1636 495.686 C 1639.09 495.988 1649 497 1656 498 C 1663 499 1699.5 508.735 1699.5 508.735 C 1699.5 508.735 1720.5 515 1736.5 524 C 1752.5 533 1796.88 562.357 1831 619.5 C 1856.5 672 1858 730 1850.5 761 C 1843 792 1826.65 824.345 1800.5 849 C 1750.82 883.226 1701.5 892 1649.5 873 C 1597.5 854 1582.02 840.006 1572.5 822 C 1562.98 803.994 1553.22 779.97 1570 757 C 1586.78 734.03 1608 746.5 1618.5 755 C 1629 763.5 1627.5 774 1636 774 C 1644.5 774 1648.5 740.5 1666.5 731.5 C 1684.5 722.5 1708.5 735 1715.5 759.5 C 1722.5 784 1688.5 840 1688.5 840 M 1790.5 253.5 C 1829.99 215.789 1885 172 1912.5 176.5 C 1940 181 1949 202.5 1947.5 228 C 1946 253.5 1921.67 294.052 1897.5 327 C 1841.5 377.507 1806.48 383.598 1740.5 373 M 2011 161 C 1994.12 261.607 1906 421.5 1981 425 C 2056 428.5 2155.87 157.897 2163.5 168 C 2171.13 178.103 2154.5 337 2106.5 432.5 C 2058.5 528 1984.5 609 1928.5 646.5 C 1872.5 684 1771.52 706.437 1588.5 707 C 1503.28 704.97 1372.3 692.415 1312.5 662 C 1252.7 631.586 1216.23 591.402 1208.5 534 C 1200.77 476.598 1237 461 1251 458 C 1265 455 1283.5 513.5 1288.5 511.5 C 1293.5 509.5 1311 446 1340 455 C 1369 464 1372.86 531.607 1326.5 606 M 178.5 1040 C 211.918 1032.62 242.5 1040 251 1061.5 C 259.5 1083 247.5 1115.5 230 1143.5 C 212.5 1171.5 199 1187 167.5 1207 C 136 1227 107.5 1235 78.5 1221.5 C 49.5 1208 22.5 1172 21 1096.5 C 19.5 1021 83.343 939.222 123.5 907 C 168.473 865.829 266.501 820.056 337.5 847.5 C 401.36 872.185 419.87 931.503 396.5 1016 C 365.168 1088.95 341.506 1127.22 299 1213 C 269.97 1269.53 250 1331 295.5 1353 C 341 1375 437.514 1289.03 498.5 1207 C 582.5 1087 599.064 1006.87 512.5 959 M 766.5 1124.5 C 766.5 1124.5 754.307 1113.98 719.002 1110.5 C 683.698 1107.02 634 1147 607 1198 C 580 1249 545.505 1300.11 590.502 1324 C 635.5 1347.89 731.497 1173.78 741.502 1164 C 751.507 1154.22 722.606 1195.54 709.5 1231.5 C 698.952 1260.45 696.671 1281.48 698.501 1291 C 700.33 1300.52 739.832 1282.19 741.502 1279 M 911.5 904 C 853.364 1001.92 828.866 1058.16 799.5 1161 C 778.214 1276.29 799.417 1317.2 883.5 1357 M 920.5 1191 C 961.06 1204.02 988 1202 1027.5 1182 C 1067 1162 1078.88 1113.31 1061.5 1092.5 C 1044.12 1071.69 1007.5 1073 979 1104 C 950.5 1135 909.478 1182.84 897 1274.5 C 884.521 1366.16 1024.4 1282.53 1048.5 1244 M 1141.5 1094 C 1112.99 1175.51 1073.59 1291.9 1079.5 1297 C 1085.41 1302.1 1181.29 1116.59 1219.5 1116 C 1257.71 1115.41 1215.57 1175.34 1213.5 1229 C 1202.63 1282.43 1220.38 1322.16 1236.5 1322 M 1419.5 844 C 1408.87 848.346 1319.07 1044.97 1282.5 1171 C 1245.93 1297.03 1258.28 1422.7 1366 1423 M 1302.5 979 C 1392.14 923.201 1430.77 915.752 1473.5 956 M 1436.5 1096 C 1405.15 1169.38 1379.78 1248.23 1377.5 1287.5 C 1375.22 1326.77 1436.5 1277.5 1436.5 1277.5 M 1436.5 1020.5 C 1436.5 1005 1441 1005 1454.5 1005 C 1468 1005 1473.55 1005.67 1473.5 1022 C 1473.45 1038.33 1467.46 1038.97 1457 1039.5 C 1446.54 1040.03 1436.5 1036 1436.5 1020.5 M 1523.5 1097 C 1496.22 1184.57 1455.88 1289.5 1461.5 1289 C 1467.12 1288.5 1549.6 1114.08 1596.5 1125 C 1643.41 1135.92 1532.75 1293.04 1604.5 1324 M 1681.5 1193.5 C 1724.94 1201.71 1735 1205.5 1779.5 1186 C 1824 1166.5 1857 1104.5 1807.5 1081 C 1758 1057.5 1704.5 1135 1685.5 1170 C 1666.5 1205 1624 1281.1 1670.5 1311 C 1717 1340.9 1814.5 1231 1814.5 1231 M 1889.5 901 C 1919.88 938.14 1916.67 959.047 1874.5 1003 M 2090 1121 C 2090 1121 2093.23 1081.89 2039.5 1092.5 C 1985.77 1103.11 1941 1159 1968.5 1195 C 1996 1231 2043 1203 2061.5 1235.5 C 2080 1268 2059.41 1286.11 2034.5 1302 C 2009.59 1317.89 1968.5 1319 1953 1302 C 1937.5 1285 1937 1255.5 1937 1255.5 M 2330.5 1005 C 2322.7 975.806 2325 946 2341 927 C 2357 908 2377.5 911.5 2392.5 917 C 2407.5 922.5 2398 973 2403.5 975 C 2409 977 2439.5 955 2453.5 967 C 2467.5 979 2449 1021 2403.5 1046 C 2358 1071 2297.1 1056.85 2255.5 1020.5 C 2213.9 984.153 2227.38 938.386 2243 912 C 2258.62 885.614 2280.5 865 2326 848.5 C 2371.5 832 2444.48 847.632 2498 884.5 C 2551.52 921.368 2578.97 979.317 2589 1046 C 2599.03 1112.68 2598.66 1203.56 2556.5 1289 C 2514.34 1374.44 2425.81 1410.83 2296 1399 C 2319.167 1290.667 2342.333 1182.333 2365.5 1074 M 2839.5 1137 C 2839.5 1137 2815.5 1105 2776.5 1106 C 2737.5 1107 2671.74 1200.58 2660 1220 C 2648.26 1239.42 2611.18 1293.5 2648.09 1318 C 2658.58 1324.96 2676.4 1322.83 2694 1309 C 2732.6 1270.5 2799 1162.5 2802 1165 C 2805 1167.5 2743.92 1278.71 2755.5 1290 C 2767.08 1301.29 2805 1276 2805 1276 M 2900.5 1108 C 2860.03 1181.49 2843.18 1272.33 2864.5 1286 C 2885.82 1299.67 2951.48 1214.16 2955.5 1207 C 2963.92 1192 2996.4 1107.62 3014.5 1108 C 3037.55 1109.14 2931.78 1359.59 2917 1387 C 2902.22 1414.41 2871.13 1467.31 2794 1503.5 C 2716.87 1539.69 2648.09 1536.5 2596.5 1513 C 2544.91 1489.5 2533 1465.5 2530.5 1434 C 2528 1402.5 2545.5 1374 2567.5 1374 C 2589.5 1374 2589.5 1407 2602.5 1407 C 2615.5 1407 2622.43 1365.91 2643.5 1369 C 2664.57 1372.09 2665 1407 2657 1425.5 C 2649 1444 2623.5 1477 2623.5 1477" | |
) | |
#Preview { | |
HappyValentinesDayView() | |
.padding(56) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment