brand: update app icon, iOS assets, Android drawables, brand docs (Pass H)

This commit is contained in:
null 2026-06-25 14:34:27 -05:00
parent 450ddccd16
commit 334cb079fa
71 changed files with 95 additions and 310 deletions

View File

@ -56,7 +56,7 @@ object NotificationHelper {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setSmallIcon(R.drawable.ic_notification_closer)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel(true)

View File

@ -131,7 +131,7 @@ class PartnerNotificationManager @Inject constructor(
)
val notification = NotificationCompat.Builder(context, type.channelId)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setSmallIcon(R.drawable.ic_notification_closer)
.setContentTitle(type.title)
.setContentText(type.body)
.setAutoCancel(true)

View File

@ -1,5 +1,6 @@
package app.closer.ui.components
import app.closer.R
import app.closer.ui.theme.closerCardColor
import android.provider.Settings
import androidx.compose.animation.core.FastOutSlowInEasing
@ -8,7 +9,7 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -20,13 +21,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
@ -67,6 +64,14 @@ fun LoadingState(
fun CloserHeartLoader(
modifier: Modifier = Modifier,
size: Dp = 76.dp
) {
CloserMarkLoader(modifier = modifier, size = size)
}
@Composable
fun CloserMarkLoader(
modifier: Modifier = Modifier,
size: Dp = 76.dp
) {
val context = LocalContext.current
val reducedMotion = remember {
@ -76,16 +81,7 @@ fun CloserHeartLoader(
1f
) == 0f
}
val transition = rememberInfiniteTransition(label = "closerHeartLoader")
val animatedFill = transition.animateFloat(
initialValue = 0.08f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart
),
label = "closerHeartFill"
)
val transition = rememberInfiniteTransition(label = "closerMarkLoader")
val animatedPulse = transition.animateFloat(
initialValue = 0.96f,
targetValue = 1.04f,
@ -93,38 +89,13 @@ fun CloserHeartLoader(
animation = tween(durationMillis = 900, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "closerHeartPulse"
label = "closerMarkPulse"
)
val fillProgress = if (reducedMotion) 1f else animatedFill.value
val pulse = if (reducedMotion) 1f else animatedPulse.value
val shadowPath = remember {
PathParser().parsePathString(
"M54,89C48,82 25,65 20,50C15,35 23,22 37,22C45,22 51,26 54,33C57,26 63,22 71,22C85,22 93,35 88,50C83,65 60,82 54,89Z"
).toPath()
}
val leftPath = remember {
PathParser().parsePathString(
"M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35Z"
).toPath()
}
val rightPath = remember {
PathParser().parsePathString(
"M54,85C59,79 81,62 86,48C91,35 84,24 71,24C63,24 57,28 54,35Z"
).toPath()
}
val leftHighlight = remember {
PathParser().parsePathString(
"M27,42C28,32 34,27 42,27C48,27 52,30 54,35L54,41C47,36 37,36 27,42Z"
).toPath()
}
val rightHighlight = remember {
PathParser().parsePathString(
"M54,35C57,30 62,27 69,27C78,27 84,32 85,42C75,36 65,36 54,41Z"
).toPath()
}
Canvas(
Image(
painter = painterResource(R.drawable.closer_mark_loader),
contentDescription = null,
modifier = modifier
.size(size)
.graphicsLayer {
@ -132,22 +103,5 @@ fun CloserHeartLoader(
scaleY = pulse
}
.clearAndSetSemantics {}
) {
val scaleX = this.size.width / 108f
val scaleY = this.size.height / 108f
withTransform({
scale(scaleX = scaleX, scaleY = scaleY, pivot = Offset.Zero)
}) {
drawPath(shadowPath, color = Color(0xFF24122F).copy(alpha = 0.10f))
drawPath(leftPath, color = Color(0xFFF7C8E4).copy(alpha = 0.22f))
drawPath(rightPath, color = Color(0xFFD9B8FF).copy(alpha = 0.22f))
clipRect(top = 108f * (1f - fillProgress), bottom = 108f) {
drawPath(leftPath, color = Color(0xFFF7C8E4))
drawPath(rightPath, color = Color(0xFFD9B8FF))
drawPath(leftHighlight, color = Color(0xFFFFF4FA).copy(alpha = 0.68f))
drawPath(rightHighlight, color = Color(0xFFF3E8FF).copy(alpha = 0.52f))
}
}
}
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,33 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#24122F"
android:fillAlpha="0.38"
android:pathData="M54,89C48,82 25,65 20,50C15,35 23,22 37,22C45,22 51,26 54,33C57,26 63,22 71,22C85,22 93,35 88,50C83,65 60,82 54,89Z" />
<path
android:fillColor="#F7C8E4"
android:pathData="M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35Z" />
<path
android:fillColor="#D9B8FF"
android:pathData="M54,85C59,79 81,62 86,48C91,35 84,24 71,24C63,24 57,28 54,35Z" />
<path
android:fillColor="#FFF4FA"
android:fillAlpha="0.68"
android:pathData="M27,42C28,32 34,27 42,27C48,27 52,30 54,35L54,41C47,36 37,36 27,42Z" />
<path
android:fillColor="#F3E8FF"
android:fillAlpha="0.52"
android:pathData="M54,35C57,30 62,27 69,27C78,27 84,32 85,42C75,36 65,36 54,41Z" />
<path
android:fillColor="#6D2B55"
android:fillAlpha="0.22"
android:pathData="M22,48C29,62 45,75 54,85L54,89C45,79 27,65 22,48Z" />
<path
android:fillColor="#56306F"
android:fillAlpha="0.22"
android:pathData="M86,48C79,62 63,75 54,85L54,89C63,79 81,65 86,48Z" />
</vector>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/closer_launcher_foreground"
android:gravity="fill" />

View File

@ -1,10 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35C57,28 63,24 71,24C84,24 91,35 86,48C81,62 59,79 54,85Z" />
</vector>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/closer_launcher_monochrome"
android:gravity="fill" />

View File

@ -4,8 +4,8 @@ This is the working artwork asset set for Closer. It keeps the existing purple/p
and the 2D pastel couple illustration style, but gives every visual surface a clearer job.
The brand should feel like a private ritual for two people: warm, quiet, equal, and intentional.
The heart remains the compact brand mark. The couple artwork should become the primary visual
language anywhere there is enough space to show a human moment.
The compact brand mark is now the approved Closer C-heart-keyhole. The couple artwork should remain
the primary visual language anywhere there is enough space to show a human moment.
## Current Artwork Review
@ -33,19 +33,21 @@ Avoid:
Keep:
- `docs/store/sources/app-icon.svg` as the source of the compact heart mark.
- `docs/brand/sources/closer-approved-icon-source.png` as the visual source of truth for the compact
C-heart-keyhole mark.
- `docs/store/sources/app-icon.svg` as the source wrapper for the launcher/store icon.
- `iphone/Closer/Resources/illustration-couple-*.png` as the human brand style.
- `iphone/Closer/Resources/pack-art-*.png` as the category/pack art direction.
- `iphone/Closer/Resources/particle-heart.png` and `particle-petal.png` for celebration moments.
Improve:
- The heart is strong for launchers and tiny UI, but too generic when used alone at larger sizes.
Use couple illustrations for onboarding, paywall, empty states, store screenshots, and web/social.
- The mark is strong for launchers, auth, loaders, notification glyphs, and small privacy seals. Use
couple illustrations for onboarding, paywall, empty states, store screenshots, and web/social.
- Android currently has the launcher vectors but not the larger illustration library. Mirror the
iOS resources into Android when those screens start using raster art.
- Store feature graphics should show the heart plus one illustrated/private ritual scene, not only
cards and symbols.
- Store feature graphics should show the C-heart-keyhole plus one illustrated/private ritual scene,
not only cards and symbols.
### Notification Artwork
@ -63,18 +65,20 @@ This section is only about the artwork used to represent notifications, not noti
| Asset | Use | Source / Target |
| --- | --- | --- |
| Primary app mark | Launcher, favicon, small brand moments | `docs/store/sources/app-icon.svg` |
| Primary app mark | Launcher, favicon, small brand moments | `docs/brand/sources/closer-approved-icon-source.png` |
| Adaptive foreground | Android launcher layer | `app/src/main/res/drawable/ic_launcher_foreground.xml` |
| Adaptive background | Android launcher layer | `app/src/main/res/drawable/ic_launcher_background.xml` |
| Monochrome mark | Android themed icon, single-color use | `app/src/main/res/drawable/ic_launcher_monochrome.xml` |
| Notification glyph | Android/iOS notification art direction | Needed: monochrome heart/paired-card source |
| Notification glyph | Android notification small icon | `app/src/main/res/drawable-nodpi/ic_notification_closer.png` |
| Wordmark lockup | Website, store hero, press kit | Needed: `docs/brand/sources/closer-lockup.svg` |
| Horizontal logo | Email header, social headers | Needed: SVG + PNG exports |
| One-color logo | Legal docs, monochrome print, dark/light footer | Needed: SVG exports |
| Favicon set | Website/browser/PWA | Needed: 16, 32, 48, 180, 192, 512 px |
Logo rule: the heart mark should mean "two equal people meeting in the middle." Do not split it,
rotate it, add faces, add text inside it, or use it as a reaction emoji.
Logo rule: the mark should mean "Closer is the private space around your relationship." Keep the
approved pink upper C, lavender lower sweep, heart-shaped inner space, and true keyhole. Do not
redraw it as a generic `C`, turn the keyhole into a heart, add a key or lock shackle, add faces, add
text inside it, or use it as a reaction emoji.
### 2. App Icons
@ -86,9 +90,8 @@ rotate it, add faces, add text inside it, or use it as a reaction emoji.
Current status:
- Android and Play icon assets exist.
- iOS has an asset catalog folder but no visible app icon set in the checked file list. Add the
full iOS app icon set before TestFlight/App Store review.
- Android, Play, and iOS AppIcon assets exist and should be regenerated from the approved source
image after any future mark change.
### 3. Illustration Library
@ -158,7 +161,7 @@ These should be simple single-color vectors that work at 20-32 dp/pt.
| Asset | Spec | Direction |
| --- | --- | --- |
| Play feature graphic | 1024 x 500 PNG | Heart + one private ritual illustration + short promise |
| Play feature graphic | 1024 x 500 PNG | C-heart-keyhole + one private ritual illustration + short promise |
| Play screenshots | Up to 8 phone screenshots | Use product screens; omit login unless testing trust |
| App Store screenshots | iPhone 6.7", 6.5", 5.5" as needed | Mirror Play story |
| Social open graph | 1200 x 630 | Couple illustration + "A private space for two." |
@ -234,10 +237,9 @@ iphone/Closer/Resources/
## Priority Build List
1. Keep the current heart mark, but create wordmark/horizontal/one-color SVG lockups.
2. Add the missing iOS AppIcon set.
3. Mirror illustration PNGs into Android and start using them in onboarding, empty states, paywall,
1. Build wordmark/horizontal/one-color lockups around the approved C-heart-keyhole.
2. Mirror illustration PNGs into Android and start using them in onboarding, empty states, paywall,
and store screenshots.
4. Rework the Play feature graphic to include one couple/private-ritual illustration.
5. Add the new notification and privacy illustrations listed above.
6. Build a small custom glyph set for privacy/reveal/date/capsule concepts.
3. Rework the Play feature graphic to include one couple/private-ritual illustration.
4. Add the new notification and privacy illustrations listed above.
5. Build a small custom glyph set for privacy/reveal/date/capsule concepts.

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1254" height="1254" viewBox="0 0 1254 1254" role="img" aria-label="Closer C-heart-keyhole mark">
<image href="docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png" width="1254" height="1254" preserveAspectRatio="xMidYMid meet"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@ -11,14 +11,21 @@ requirements, see `docs/brand/asset-system.md`.
## Brand mark
The mark is one heart formed by two equal halves. Pink and lavender represent two people meeting at
the center; neither side visually dominates. Keep the mark intact and do not separate, rotate, add
text inside, or place it directly over a busy image.
The mark is the approved Closer `C-heart-keyhole`: a soft pink upper `C`, lavender lower sweep,
a heart-shaped inner space, and a true centered keyhole. Pink and lavender represent two people
meeting in one private space; the keyhole represents trust and privacy.
- Master mark source: `docs/brand/sources/closer-approved-icon-source.png`
- Transparent mark source: `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png`
- Launcher source: `docs/store/sources/app-icon.svg`
- Android adaptive layers: `app/src/main/res/drawable/ic_launcher_*`
- Android notification glyph: `app/src/main/res/drawable-nodpi/ic_notification_closer.png`
- Minimum clear space: one quarter of the mark's width on all sides.
- Minimum digital size: 24 px. At small sizes, use the solid monochrome mark.
- Minimum digital size: 24 px. At small sizes, use the solid monochrome C-heart-keyhole glyph.
Keep the mark visually faithful to the approved artwork. Do not redraw it as a generic `C`, close
the aperture into an `O`, add a padlock shackle, turn the keyhole into a heart, add a separate key,
or place text inside the icon.
## Core colors
@ -64,8 +71,9 @@ when the couple key is unavailable. These claims describe deployed behavior.
- Store graphics and screenshots should use the same purple/pink palette as the product.
- Lead with privacy and mutual connection before feature volume.
- The Play feature graphic should show the heart mark, the primary promise, and compact product cues
for private reveals, two-person use, and daily rituals. Do not turn it into a feature checklist.
- The Play feature graphic should show the C-heart-keyhole mark, the primary promise, and compact
product cues for private reveals, two-person use, and daily rituals. Do not turn it into a feature
checklist.
- Do not show intimate answer content, real email addresses, invite codes, or notification tokens.
- Use clean demo data and crop out development indicators before publishing.
- Re-export `docs/store/app-icon-512.png` and `docs/store/feature-graphic-1024x500.png` from the

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 477 KiB

View File

@ -1,17 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<filter id="shadow" x="-30%" y="-30%" width="160%" height="170%">
<feDropShadow dx="0" dy="18" stdDeviation="14" flood-color="#24122F" flood-opacity=".42"/>
</filter>
</defs>
<rect width="512" height="512" fill="#4A235F"/>
<path d="M0 0h512v198C411 236 314 232 222 207 124 180 52 125 0 64Z" fill="#B98AF4" opacity=".24"/>
<path d="M0 354c92-52 192-45 296-15 93 27 157 37 216 1v172H0Z" fill="#24122F" opacity=".36"/>
<path d="M378 0h134v512H352c6-74-5-148-34-219-34-82-27-171 60-293Z" fill="#B98AF4" opacity=".10"/>
<g filter="url(#shadow)">
<path d="M256 402c-25-28-130-111-153-177-24-62 13-116 73-116 38 0 65 19 80 49Z" fill="#F7C8E4"/>
<path d="M256 402c25-28 130-111 153-177 24-62-13-116-73-116-38 0-65 19-80 49Z" fill="#D9B8FF"/>
<path d="M122 199c7-45 36-69 78-69 27 0 46 11 56 28v28c-44-25-89-21-134 13Z" fill="#FFF4FA" opacity=".68"/>
<path d="M256 158c14-18 38-28 67-28 42 0 70 24 77 69-49-34-97-38-144-13Z" fill="#F3E8FF" opacity=".52"/>
</g>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="Closer app icon">
<image href="docs/brand/sources/closer-approved-icon-square.png" width="512" height="512" preserveAspectRatio="xMidYMid slice"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 266 B

View File

@ -13,17 +13,7 @@
<path d="M0 372c144-62 292-57 449-14 142 39 259 43 358 7 70-25 142-33 217-13v148H0Z" fill="#180C20" opacity=".34"/>
<path d="M900 0h124v500H820c31-76 34-154 8-236C800 174 812 88 900 0Z" fill="#F7C8E4" opacity=".08"/>
<g transform="translate(68 74)">
<rect x="0" y="0" width="312" height="312" rx="72" fill="#4A235F"/>
<path d="M0 0h312v120C250 143 192 141 136 126 76 109 32 76 0 38Z" fill="#B98AF4" opacity=".24"/>
<path d="M0 216c56-32 117-28 181-9 57 16 96 23 131 1v104H0Z" fill="#180C20" opacity=".28"/>
<g transform="translate(0 -4)" filter="url(#markShadow)">
<path d="M156 246c-15-17-79-68-93-109-15-38 8-71 45-71 23 0 40 12 48 30Z" fill="#F7C8E4"/>
<path d="M156 246c15-17 79-68 93-109 15-38-8-71-45-71-23 0-40 12-48 30Z" fill="#D9B8FF"/>
<path d="M74 121c4-28 22-42 48-42 16 0 28 7 34 17v17c-27-15-54-13-82 8Z" fill="#FFF4FA" opacity=".68"/>
<path d="M156 96c9-11 23-17 41-17 26 0 43 14 47 42-30-21-59-23-88-8Z" fill="#F3E8FF" opacity=".52"/>
</g>
</g>
<image x="68" y="74" width="312" height="312" href="docs/brand/sources/closer-approved-icon-square.png" preserveAspectRatio="xMidYMid slice" filter="url(#markShadow)"/>
<text x="408" y="142" fill="#FFF8FC" font-family="DejaVu Sans" font-size="72" font-weight="700">Closer</text>
<text x="412" y="198" fill="#F7C8E4" font-family="DejaVu Sans" font-size="26" font-weight="600">A private space</text>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -7,7 +7,7 @@ struct LoadingView: View {
var body: some View {
VStack(spacing: CloserSpacing.lg) {
CloserHeartLoader()
CloserMarkLoader()
Text(message)
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
@ -17,167 +17,44 @@ struct LoadingView: View {
}
struct CloserHeartLoader: View {
var size: CGFloat = 76
var body: some View {
CloserMarkLoader(size: size)
}
}
struct CloserMarkLoader: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var fillProgress: CGFloat = 0.08
@State private var isPulsing = false
var size: CGFloat = 76
var body: some View {
ZStack {
CloserHeartShape()
.fill(
LinearGradient(
colors: [
Color(hex: "F7C8E4").opacity(0.22),
Color(hex: "D9B8FF").opacity(0.22)
],
startPoint: .leading,
endPoint: .trailing
)
)
Image("closer-mark-loader")
.resizable()
.scaledToFit()
.frame(width: size, height: size)
.scaleEffect(reduceMotion ? 1 : (isPulsing ? 1.04 : 0.96))
.accessibilityHidden(true)
.onAppear {
guard !reduceMotion else {
isPulsing = false
return
}
HStack(spacing: 0) {
Color(hex: "F7C8E4")
Color(hex: "D9B8FF")
withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) {
isPulsing = true
}
}
.mask(CloserHeartShape())
.mask(alignment: .bottom) {
Rectangle()
.frame(height: size * (reduceMotion ? 1 : fillProgress))
.onChange(of: reduceMotion) { _, newValue in
if newValue {
isPulsing = false
}
}
CloserHeartHighlightShape(side: .left)
.fill(Color(hex: "FFF4FA").opacity(reduceMotion ? 0.68 : 0.68 * fillProgress))
CloserHeartHighlightShape(side: .right)
.fill(Color(hex: "F3E8FF").opacity(reduceMotion ? 0.52 : 0.52 * fillProgress))
}
.frame(width: size, height: size)
.scaleEffect(reduceMotion ? 1 : (isPulsing ? 1.04 : 0.96))
.accessibilityHidden(true)
.onAppear {
guard !reduceMotion else {
fillProgress = 1
isPulsing = false
return
}
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: false)) {
fillProgress = 1
}
withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) {
isPulsing = true
}
}
.onChange(of: reduceMotion) { _, newValue in
if newValue {
fillProgress = 1
isPulsing = false
}
}
}
}
private struct CloserHeartShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: point(54, 85, in: rect))
path.addCurve(
to: point(22, 48, in: rect),
control1: point(49, 79, in: rect),
control2: point(27, 62, in: rect)
)
path.addCurve(
to: point(37, 24, in: rect),
control1: point(17, 35, in: rect),
control2: point(24, 24, in: rect)
)
path.addCurve(
to: point(54, 35, in: rect),
control1: point(45, 24, in: rect),
control2: point(51, 28, in: rect)
)
path.addCurve(
to: point(71, 24, in: rect),
control1: point(57, 28, in: rect),
control2: point(63, 24, in: rect)
)
path.addCurve(
to: point(86, 48, in: rect),
control1: point(84, 24, in: rect),
control2: point(91, 35, in: rect)
)
path.addCurve(
to: point(54, 85, in: rect),
control1: point(81, 62, in: rect),
control2: point(59, 79, in: rect)
)
path.closeSubpath()
return path
}
}
private struct CloserHeartHighlightShape: Shape {
enum Side {
case left
case right
}
let side: Side
func path(in rect: CGRect) -> Path {
var path = Path()
switch side {
case .left:
path.move(to: point(27, 42, in: rect))
path.addCurve(
to: point(42, 27, in: rect),
control1: point(28, 32, in: rect),
control2: point(34, 27, in: rect)
)
path.addCurve(
to: point(54, 35, in: rect),
control1: point(48, 27, in: rect),
control2: point(52, 30, in: rect)
)
path.addLine(to: point(54, 41, in: rect))
path.addCurve(
to: point(27, 42, in: rect),
control1: point(47, 36, in: rect),
control2: point(37, 36, in: rect)
)
path.closeSubpath()
case .right:
path.move(to: point(54, 35, in: rect))
path.addCurve(
to: point(69, 27, in: rect),
control1: point(57, 30, in: rect),
control2: point(62, 27, in: rect)
)
path.addCurve(
to: point(85, 42, in: rect),
control1: point(78, 27, in: rect),
control2: point(84, 32, in: rect)
)
path.addCurve(
to: point(54, 41, in: rect),
control1: point(75, 36, in: rect),
control2: point(65, 36, in: rect)
)
path.closeSubpath()
}
return path
}
}
private func point(_ x: CGFloat, _ y: CGFloat, in rect: CGRect) -> CGPoint {
CGPoint(
x: rect.minX + rect.width * (x / 108),
y: rect.minY + rect.height * (y / 108)
)
}
// MARK: - Error View
struct ErrorView: View {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB