Skip to content

palette-driven graphic primitive (v1.4)#4

Merged
2dubu merged 11 commits intomainfrom
feature/v1.4-palette-graphic
May 3, 2026
Merged

palette-driven graphic primitive (v1.4)#4
2dubu merged 11 commits intomainfrom
feature/v1.4-palette-graphic

Conversation

@2dubu
Copy link
Copy Markdown
Owner

@2dubu 2dubu commented May 3, 2026

Summary

  • Add PaletteGraphic (SwiftUI) and PaletteGraphicView (UIKit) — the first palette-driven gradient + grain primitive in PaletteKit. Both views share the same Core Image / Core Graphics pipeline so output is pixel-equivalent across platforms.
  • New Configuration struct exposes four orthogonal axes: direction (linear / radial), linearStart/linearEnd (any UnitPoint), colorCount (2…5 with cumulative bisection), swatchStrategy (vibrant / contrast / muted), grain (none / subtle / standard / heavy). All defaults sized so PaletteGraphic(palette:swatches:) renders sensibly without further customisation.
  • New public CardPalette + SwatchStrategy color resolver — picks center / edge / background / accent PaletteColor values from a Palette + SwatchMap so consumers can compose palette-themed UI without re-implementing the lookup logic.
  • Internal NSCache memoization in the renderer (countLimit 32) — repeated SwiftUI body invalidations with the same inputs return instantly. Bumps paletteKitVersion to 1.4.0.

What's changed

Library (Sources/PaletteKit/)

  • New: Card/PaletteGraphic.swift — public SwiftUI View + nested Configuration + GradientDirection / ColorCount / GrainStyle enums. makeImage(size:scale:) provides a UIKit-friendly snapshot path that bypasses SwiftUI hosting. Wrapped in #if canImport(SwiftUI) && canImport(UIKit) for macOS dev-target compatibility.
  • New: Card/PaletteGraphicView.swift — public UIView pair, @MainActor, final. Renders the same pipeline directly into CALayer.contents (no UIHostingController inside). Property setters trigger re-render on next layout pass; display scale changes observed via the iOS 17+ UITraitChangeObservable API. snapshotImage(scale:) exposes the pipeline as a UIImage.
  • New: Card/PaletteGraphicRenderer.swift — internal CI/CG renderer. CGGradient (CG-based for arbitrary N stops + per-direction control) → CIImage → grain composite → CIContext.createCGImage. Cumulative-bisection color stop selection (positions 0.5 → 0.25 → 0.75) so raising colorCount adds one color at a time. Chroma filter (oklch.c >= min(first.c, last.c) * 0.5) prevents low-saturation outliers from intruding mid-gradient.
  • New: Card/CardPalette.swift — public CardPalette struct + SwatchStrategy enum. Conforms to Sendable, Equatable, Hashable for SwiftUI diffing and .onChange(of:) use.
  • paletteKitVersion = "1.4.0".

Demo app (Examples/PaletteKitDemo/)

  • New "Generate Graphic" entry on the result screen pushes into the Graphic Lab — interactive playground exposing every configuration axis (Direction · Axis · Colors · Strategy · Shape · Grain) on the user's actual extracted palette.
  • Lab includes a swatch chip bar (center/edge/bg with hex values) and a Share PNG action that captures the configured graphic via CardExport.snapshot (composition-aware, includes any clipShape applied at the call site).
  • Removes the demo-internal duplicates of PaletteGraphic, PaletteGraphicView, PaletteGraphicRenderer, and CardPalette now that they ship from PaletteKit. Lab UI behaviour unchanged; only the import boundary moved.

Docs

  • New PaletteKit.docc/Tutorials/Card.md — walks through SwiftUI + UIKit usage, configuration axes, shape clipping, and performance characteristics.
  • README: new ## What's new in 1.4 section above 1.3, install snippet bumped to from: "1.4.0", Roadmap marks v1.4 shipped (PaletteGraphic primitive) and keeps v2.0 entry (observe() + PaletteKitInsights).

Audit notes

Phase 1 brainstorming explored 4-tier shader strategies (LinearGradient / MeshGradient / MSL .colorEffect / MetalKit MTKView) plus 3 alternatives (SwiftUI Canvas / Core Image / pure SwiftUI composition). Side-by-side captures vs Arc Browser cards converged on Core Image (CGGradient + CIRandomGenerator + multiply blend) — naturally smooth grain, pixel-equivalent across SwiftUI + UIKit, and validated against Arc tone at ~90% match (Linear × Contrast × Subtle).

Cumulative bisection chosen over even-spaced indices because even spacing produced an odd/even parity bug — the middle pool's central index appears at colorCount=3,5 but is skipped at 4, so toggling stops felt jarring. Cumulative reads as "+1 color per increment" without disturbing earlier picks.

Chroma filter added after observing that monotone source images (Arc graphic crop) leak gray-blue shadow colors into mid-gradient when only luminance is filtered. The filter collapses to ~0 for .muted strategy (low-chroma anchors), naturally disabling itself when neutrality is the intent.

Net-new in iOS/Swift ecosystem: color-thief / Android Palette / node-vibrant / ColorPaletteCodable / swift-vibrant all stop at color extraction. PaletteGraphic is the first public iOS implementation that takes a palette → renders a graphic. Arc Browser's card system itself is closed source; the renderer here converges on the same standardised recipe (feTurbulence equivalent via CIRandomGenerator + radial gradient stops) used across the web ecosystem.

Display P3 fidelity intentionally deferred — current pipeline uses CGColorSpaceCreateDeviceRGB (sRGB on Apple devices). Wide-gamut palette colors may clip; re-evaluation flagged for a follow-up release.

Test plan

  • swift test — 43/43 macOS tests pass (Card module adds 17 new: 8 CardPaletteTests, 5 PaletteGraphicRendererTests, 5 PaletteGraphicConfigurationTests; iOS sim runs all incl. the UIKit-guarded ones).
  • xcodebuild build for Examples/PaletteKitDemo — demo app builds cleanly against the public PaletteKit Card module.
  • xcodebuild docbuild -scheme PaletteKitBUILD DOCUMENTATION SUCCEEDED. Tutorial cross-references resolve.
  • iOS Simulator smoke (iPhone 17 Pro): app launches, photo picker → palette extraction works, "Generate Graphic" entry pushes into Graphic Lab, all pickers (Direction · Axis · Colors · Strategy · Shape · Grain) toggle without crash, Share PNG produces a UIImage and presents the share sheet.
  • Cumulative property verified by PaletteGraphicRendererTests.cumulativePropertystops_at(N) first/last anchors match stops_at(N+1) for all 2 ≤ N ≤ 4.
  • NSCache identity verified — second makeCGImage call with same inputs returns the same CGImage instance (first === second).
  • (Reviewer) Visual sanity on a few palettes — vibrant photo, monotone photo, low-chroma photo.

2dubu added 10 commits May 3, 2026 22:12
Demo-internal prototype of PaletteGraphic + PaletteGraphicView +
Renderer + CardPalette built during the v1.4 brainstorming session.
Lives in Examples/PaletteKitDemo/PaletteKitDemo/Card/ for now;
upcoming commits extract these to Sources/PaletteKit/Card/ as the
public v1.4 module.

ContentView gains a "Generate Graphic" entry that pushes into the
Graphic Lab for interactive exploration of every configuration axis.

.gitignore: add .superpowers/ for visual-companion brainstorm assets.
Public color resolution helper used by PaletteGraphic and available to
consumers building their own palette-driven layouts. Exposes vibrant /
contrast / muted strategies; resolves center/edge/background/accent
PaletteColor values with explicit fallback chains.

Foundation for the upcoming PaletteGraphic primitive.
- Per-property DocC on center/edge/background/accent/strategy
- Equatable + Hashable conformance for SwiftUI diffing / .onChange use
- Test for equality + hash consistency
- Rename test closure helper to avoid loop-variable shadow
- Inline comments on `lightest` precomputation and SwatchStrategy rawValues
Shared CIContext + NSCache-memoized renderer used by both PaletteGraphic
(SwiftUI) and PaletteGraphicView (UIKit, next commits). Exposes
resolveStopColors as internal-but-tested for the cumulative-bisection
color flow, and makeCGImage for full-pipeline output.

Tests are committed alongside but won't compile until Task 3 adds
PaletteGraphic.Configuration.
Public SwiftUI primitive that renders a palette-driven gradient + grain
graphic. Configuration captures the four orthogonal axes (direction +
linearStart/End + colorCount + swatchStrategy + grain) with sensible
defaults; init takes the configuration as a single argument so all
options are visible at the call site.

makeImage(size:scale:) provides a UIKit-friendly snapshot path that
bypasses SwiftUI hosting.

Wraps the file in #if canImport(SwiftUI) && canImport(UIKit) so the
macOS dev-target SwiftPM build stays green.
UIView subclass that renders the same pipeline as PaletteGraphic
(SwiftUI) directly into CALayer contents. Pure UIKit, no
UIHostingController inside, so UIKit-only consumers don't pay SwiftUI
hosting cost.

Property setters trigger re-render on next layout pass; display scale
changes observed via the iOS 17+ UITraitChangeObservable API.
snapshotImage(scale:) exposes the same pipeline as a UIImage.

File wrapped in #if canImport(UIKit) for macOS dev-target compatibility.
Removes the demo-internal duplicates of PaletteGraphic, PaletteGraphicView,
PaletteGraphicRenderer, and CardPalette now that those types ship from
PaletteKit. Lab UI behaviour unchanged; only the import boundary moved.
Short human-readable description of each direction's flow ("diagonal
flow" / "off-center radial"). Used by the demo's footer label and
available to consumers for picker subtitles or accessibility text.
Walks through SwiftUI + UIKit usage, Configuration axes, shape clipping
options, and performance characteristics of the new Card module.
Adds "What's new in 1.4" section, bumps install snippet to 1.4.0, and
marks v1.4 shipped on the roadmap.
@2dubu 2dubu self-assigned this May 3, 2026
PaletteGraphicView.snapshotImage(scale:): clamp the resolved scale to
a minimum of 1. When the view is invoked before being attached to a
window, traitCollection.displayScale can be 0, which would otherwise
produce a 1x1 placeholder image — easy to miss in offscreen rendering
and share-export paths.

Demo CardLabView: replace the AnyView-cast switch in graphicArea and
captureAndShare with @ViewBuilder Group + switch and per-branch
CardExport.snapshot calls. Removes the per-body heap allocation on
every picker toggle without changing the rendered output.
@2dubu 2dubu merged commit 533a073 into main May 3, 2026
1 check passed
@2dubu 2dubu deleted the feature/v1.4-palette-graphic branch May 3, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant