diff --git a/Marks.xcodeproj/project.pbxproj b/Marks.xcodeproj/project.pbxproj index 9974c22..40ca20f 100644 --- a/Marks.xcodeproj/project.pbxproj +++ b/Marks.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 1F1CB72BBCFFB33B6533D5C9 /* LibraryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E093C878E702891C64D21FD5 /* LibraryListRow.swift */; }; 212F713DCC289C48087B79AE /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7728D15C17219ABFF3EFFE /* Log.swift */; }; 22C814FD55D29B88D227C987 /* SpotlightBookmarkSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41DDBB04346F3BF06DE233D2 /* SpotlightBookmarkSearch.swift */; }; + 2F80DFB7298B733965FE04F5 /* MarksMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC8E834AB663A823D77BCB1 /* MarksMark.swift */; }; 337E8272EEB3B10FD0868F76 /* IngestedSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC9BBF1006495D75DE4A232 /* IngestedSource.swift */; }; 3528AF5CB690BBCCF337581B /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA428181B35885F7D9F4D55 /* Bookmark.swift */; }; 4153FBF538C1D3F4BC96E4C5 /* LibraryKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C9BFD9C0DD3876CC32B3A /* LibraryKit.swift */; }; @@ -138,6 +139,7 @@ 23F172EC9977CD5C51B228B9 /* MarksWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = MarksWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 240DBB87940F8D255A812EB2 /* BookmarkActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkActions.swift; sourceTree = ""; }; 2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightRetrievalTests.swift; sourceTree = ""; }; + 3EC8E834AB663A823D77BCB1 /* MarksMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarksMark.swift; sourceTree = ""; }; 41DDBB04346F3BF06DE233D2 /* SpotlightBookmarkSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightBookmarkSearch.swift; sourceTree = ""; }; 47CB3AAED5B64809B06A9650 /* RecentPodcastsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentPodcastsWidget.swift; sourceTree = ""; }; 49685B8F3FEC72E8CF75843E /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; @@ -257,6 +259,7 @@ 1BF2B010DADCCFBC282D37F0 /* LibraryGridView.swift */, AA5C9BFD9C0DD3876CC32B3A /* LibraryKit.swift */, E093C878E702891C64D21FD5 /* LibraryListRow.swift */, + 3EC8E834AB663A823D77BCB1 /* MarksMark.swift */, ); path = Library; sourceTree = ""; @@ -600,6 +603,7 @@ 41F00F4E7FFC1C0ACF71E398 /* MarksApp.swift in Sources */, E10B0B4EC9580342830EC0D2 /* MarksAppIntents.swift in Sources */, 8BD3FA025C082654D55374A2 /* MarksAuth.swift in Sources */, + 2F80DFB7298B733965FE04F5 /* MarksMark.swift in Sources */, 14E1B3CE58D36BFF1A2199C1 /* OnboardingView.swift in Sources */, 773C174263D08416248D0675 /* PodcastGenerationManager.swift in Sources */, 0479C0AA16E3AFDBD4414AD0 /* PodcastIndex.swift in Sources */, diff --git a/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon-Dark.png b/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon-Dark.png new file mode 100644 index 0000000..c964e63 Binary files /dev/null and b/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon-Dark.png differ diff --git a/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon-Tinted.png b/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon-Tinted.png new file mode 100644 index 0000000..d86980f Binary files /dev/null and b/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon-Tinted.png differ diff --git a/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon.png index 1332dcb..d13f5b4 100644 Binary files a/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon.png and b/Marks/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/Marks/Assets.xcassets/AppIcon.appiconset/Contents.json b/Marks/Assets.xcassets/AppIcon.appiconset/Contents.json index cefcc87..e02c1e5 100644 --- a/Marks/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Marks/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -5,6 +5,30 @@ "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "AppIcon-Dark.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "AppIcon-Tinted.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { diff --git a/Marks/Views/Library/MarksMark.swift b/Marks/Views/Library/MarksMark.swift new file mode 100644 index 0000000..310d0a2 --- /dev/null +++ b/Marks/Views/Library/MarksMark.swift @@ -0,0 +1,166 @@ +import SwiftUI + +// MARK: - The mark +// +// One path, four tails. +// +// The brand mark is a single closed shape rather than a stack of separate +// ribbons, and that is the whole design decision. A logo gets tested in two +// places that have nothing to do with how it looks on a slide: at 16pt in a +// Spotlight result, and in iOS 26's tinted and monochrome icon modes, where the +// palette is thrown away and only the silhouette survives. Every arrangement of +// N separate ribbons collapses into a blob under both tests, because the shapes +// were only ever told apart by colour. A scalloped baseline is told apart by +// its outline, so it reads the same tinted, monochrome, and 16pt tall. +// +// Geometry is defined in a canonical 88 x 98 box and fitted to whatever rect it +// is handed, so the proportions can't drift with the container. + +struct ScallopMark: Shape { + /// Downward points along the baseline. Four is the drawn default: three + /// reads as a plain banner, and five starts to look like bunting. + var tails: Int = 4 + + /// How far the notches cut up from the baseline, as a fraction of height. + /// Shallower than this and the tails read as damage rather than intent. + var notchDepth: CGFloat = 16.0 / 98.0 + + /// Top-corner radius, as a fraction of width. + var capRadius: CGFloat = 10.0 / 88.0 + + /// Canonical proportions of the drawn mark. Everything else derives from + /// this, including the app icon artwork — see scripts/appicon. + static let aspectRatio: CGFloat = 88.0 / 98.0 + + func path(in rect: CGRect) -> Path { + // Fit the canonical box into `rect` without distorting it, so callers + // can hand this any frame and still get the drawn proportions. + let height = min(rect.height, rect.width / Self.aspectRatio) + let width = height * Self.aspectRatio + let minX = rect.midX - width / 2 + let minY = rect.midY - height / 2 + let maxX = minX + width + let maxY = minY + height + + let radius = min(capRadius * width, min(width, height) / 2) + let depth = notchDepth * height + let points = max(2, tails) + let step = width / CGFloat(points - 1) + + var path = Path() + path.move(to: CGPoint(x: minX + radius, y: minY)) + // Top edge, right cap, then straight down the right side. + path.addArc( + tangent1End: CGPoint(x: maxX, y: minY), + tangent2End: CGPoint(x: maxX, y: minY + radius), + radius: radius + ) + path.addLine(to: CGPoint(x: maxX, y: maxY)) + // The tails, cut right to left so the winding stays consistent with + // the caps above. + for i in 1.. MarksMark { + MarksMark(bands: [color]) + } + + var body: some View { + ScallopMark(tails: tails) + .fill(bandFill) + .aspectRatio(ScallopMark.aspectRatio, contentMode: .fit) + } + + /// Hard-stopped, not blended: the bands are three inks laid side by side, + /// not a gradient. Doubling the stop at each boundary is what kills the + /// ramp SwiftUI would otherwise interpolate. + private var bandFill: LinearGradient { + let stops = bands.enumerated().flatMap { index, color in + [ + Gradient.Stop(color: color, location: Double(index) / Double(bands.count)), + Gradient.Stop(color: color, location: Double(index + 1) / Double(bands.count)), + ] + } + return LinearGradient(stops: stops, startPoint: .leading, endPoint: .trailing) + } +} + +// MARK: - Previews + +#Preview("Size ladder") { + VStack(spacing: 28) { + HStack(alignment: .bottom, spacing: 22) { + ForEach([128, 64, 40, 24, 16], id: \.self) { size in + MarksMark().frame(height: CGFloat(size)) + } + } + // The test the shape was chosen to pass: no colour, same mark. + HStack(alignment: .bottom, spacing: 22) { + ForEach([128, 64, 40, 24, 16], id: \.self) { size in + MarksMark.monochrome().frame(height: CGFloat(size)) + } + } + HStack(spacing: 22) { + ForEach(3..<6) { tails in + MarksMark(tails: tails).frame(height: 72) + } + } + } + .padding(40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Paper.sheet) +} + +#Preview("Lockup") { + HStack(spacing: 18) { + MarksMark().frame(height: 64) + VStack(alignment: .leading, spacing: 6) { + Text("Marks").font(.system(size: 46, design: .serif)) + Text("EVERYTHING YOU SAVED") + .font(PaperType.micro) + .tracking(3) + .foregroundStyle(Paper.tertiary) + } + } + .foregroundStyle(Paper.ink) + .padding(40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Paper.sheet) +} diff --git a/docs/logo-c1-variants.html b/docs/logo-c1-variants.html new file mode 100644 index 0000000..277588c --- /dev/null +++ b/docs/logo-c1-variants.html @@ -0,0 +1,322 @@ + + + + +Marks — C1 development + + + + +
+

C1 — three ribbons, developed

+

C1 had the most robust silhouette of the seven and the least ownership: three bookmark ribbons is a shape any read-later app could reach for. Ownability is the thing to attack, so each variant below adds one specific idea — a rhythm, a structure, a colour rule — and the caption says whether it actually buys anything.

+
+ + + +
+ 01 +

Ten developments

+

Shown in the app's own palette (navy / vermilion / yellow from Paper.swatchPairs) rather than the reference cyan-violet, since that was the conclusion of the last sheet. The ownable? line is the honest verdict, not a summary.

+
+
+ +
+ 02 +

Palette

+

The three-swatch column is the interesting one. The app already hashes bookmarks into a twelve-swatch palette — a mark built from three of those swatches is describing the app's actual data model, which is ownership no competitor can copy without copying the system underneath it.

+
+
+ +
+ 03 +

Size ladder

+

128 / 64 / 40 / 24 / 16 px. C1's whole argument is that it survives this; the developments have to survive it too or they've bought ownership with legibility.

+
+
+ +
+ 04 +

App icon

+

Squircle at 118 and 58 px, mark at ~62% of the tile.

+
+
+
+ +
+ 05 +

Lockups

+
+
+ +
+ 06 +

Pattern

+

The test C1 is most likely to win: a repeating field is where a simple silhouette beats a clever one.

+
+
+ + + + + diff --git a/docs/logo-variants.html b/docs/logo-variants.html new file mode 100644 index 0000000..897627d --- /dev/null +++ b/docs/logo-variants.html @@ -0,0 +1,447 @@ + + + + +Marks — logo variants + + + + +
+

Marks — logo variants

+

Five geometries derived from the reference (letter + box files + bookmark ribbon), each drawn once and re-coloured through the app's existing Paper palette. Geometry lives in <symbol>; colour is injected with CSS custom properties, so any cell in the matrix is a real, buildable combination.

+
+ + + + + +
+ 01 +

Seven geometries

+

Shown in the reference palette (cyan #22C7F0 / violet #8B4DEE) with multiply overlap, so they're directly comparable to the original.

+
+ +
+
+

A1 · Binder-B

+

Faithful reconstruction. Ribbon stem carries the notch; three bars go wide / narrow / wide to make the B. Closest to the source.

+
+ +
+
+

A2 · Ribbon-B

+

Every terminal becomes a swallowtail. More internally consistent, but the bowls stop reading as bowls — the B gets weaker.

+
+ +
+
+

B1 · Slab-M, tailed

+

Retargeted to the app's actual name. A real M — counters are wedges, apex at the cap line — with a swallowtail on each of the three feet.

+
+ +
+
+

B2 · Slab-M, flat

+

Same letter, tails removed. Sturdier and more type-like, but only the rounded cap is left carrying the bookmark idea — probably too little.

+
+ +
+
+

B3 · Ribbon knockout

+

Single ribbon, letter knocked out. One colour, one shape — the only variant that's trivially correct in iOS 26 tinted and mono icon modes.

+
+ +
+
+

C1 · Three ribbons

+

Drops the letterform entirely. Stepped heights do the work. Most robust silhouette, least ownable — many apps could use this.

+
+ +
+
+

C2 · Monoline

+

Binder-B as strokes. Sits in the same register as the app's serif/paper styling instead of fighting it. Fails below ~28px.

+
+ +
+
+ + +
+ 02 +

Palette matrix

+

Each geometry through the reference palette and five pairs drawn from Paper.swatchPairs in LibraryKit.swift. The Dark column uses the palette's own dark-mode counterparts, not dimmed light values.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ReferenceNavy + Verm.Ink + Verm.Yellow + Verm.Blue + ForestInk onlyDark
A1 Binder-B
A2 Ribbon-B
B1 Slab-M deep
B2 Slab-M shallow
B3 Knockout
C1 Three ribbons
C2 Monoline
+
+ + +
+ 03 +

Size ladder

+

128 / 64 / 40 / 24 / 16 px. 40px is the Settings-row and tab-bar size; 16px is the favicon / Spotlight-result size. Anything that turns to mush at 24 is a wordmark, not a mark.

+ +
+
A1 Binder-B
+
128
+
64
+
40
+
24
+
16
+
+ +
+
B1 Slab-M deep
+
128
+
64
+
40
+
24
+
16
+
+ +
+
B3 Knockout
+
128
+
64
+
40
+
24
+
16
+
+ +
+
C1 Three ribbons
+
128
+
64
+
40
+
24
+
16
+
+ +
+
C2 Monoline
+
128
+
64
+
40
+
24
+
16
+
+
+ + +
+ 04 +

App icon

+

iOS squircle at 120pt and 60pt. The mark is set at ~62% of the tile — Apple's grid wants generous margins, and every one of these was drawn edge-to-edge in its own box.

+
+
A1 · navy
+
B1 · yellow
+
B3 · ink
+
C1 · blue
+
A1 · dark
+
A1 · reference
+
+
+
60pt
+
60pt
+
60pt
+
60pt
+
60pt
+
60pt
+
+
+ + +
+ 05 +

Lockups

+

Wordmark set in the app's own voice — serif for the name, mono letterspaced for the descriptor — rather than the reference's geometric sans, which belongs to a different product.

+ +
+ +
Marks
Everything you saved
+
+ +
+ +
Marks
Everything you saved
+
+ +
+ +
Marks
Everything you saved
+
+ +
+ +
Marks
Everything you saved
+
+
+ + +
+ 06 +

Pattern test

+

The reference proves itself by tiling. Same test here — if the mark only works alone, it isn't a system.

+
+
+ + + + + diff --git a/scripts/appicon/appicon.html b/scripts/appicon/appicon.html new file mode 100644 index 0000000..aedd51e --- /dev/null +++ b/scripts/appicon/appicon.html @@ -0,0 +1,72 @@ + + + + + +Marks app icon + + + +
+ + + diff --git a/scripts/appicon/generate.sh b/scripts/appicon/generate.sh new file mode 100755 index 0000000..81906e2 --- /dev/null +++ b/scripts/appicon/generate.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Renders the three app-icon variants from appicon.html into the asset catalog. +# +# scripts/appicon/generate.sh +# +# Chrome is the rasteriser because it is the only thing on a stock Mac that +# renders SVG the same way the browser preview sheets in docs/ do. The PNGs it +# writes are build output — edit appicon.html, not them. +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +OUT="$HERE/../../Marks/Assets.xcassets/AppIcon.appiconset" +CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + +[ -x "$CHROME" ] || { echo "Google Chrome not found at $CHROME" >&2; exit 1; } +command -v magick >/dev/null || { echo "ImageMagick (magick) not found" >&2; exit 1; } + +render() { + local variant=$1 name=$2 + "$CHROME" --headless=new --disable-gpu --hide-scrollbars \ + --default-background-color=00000000 \ + --screenshot="$OUT/$name" --window-size=1024,1024 \ + --virtual-time-budget=2000 \ + "file://$HERE/appicon.html?v=$variant" >/dev/null 2>&1 + echo " $name" +} + +echo "rendering app icon ->" +render light AppIcon.png +render dark AppIcon-Dark.png +render tinted AppIcon-Tinted.png + +# The light icon is the one shipped as the home-screen artwork, and iOS rejects +# an alpha channel there. The dark and tinted variants are composited by the +# system over a background it supplies, so they must keep theirs. +magick "$OUT/AppIcon.png" -background '#F8F5EF' -alpha remove -alpha off "$OUT/AppIcon.png" + +echo "done"