feat: add physical surfaces, brushed metal and silkscreen label
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// A shape with a fill, layered drop shadows, layered inset shadows and a hairline edge: a physical part.
|
||||
public struct PhysicalSurface: View {
|
||||
private let fill: AnyShapeStyle
|
||||
private let cornerRadius: CGFloat
|
||||
private let shadows: [ShadowLayer]
|
||||
private let edge: Color?
|
||||
@Environment(\.colorScheme) private var scheme
|
||||
|
||||
public init(fill: AnyShapeStyle, cornerRadius: CGFloat, shadows: [ShadowLayer], edge: Color? = Palette.lineSeamDark) {
|
||||
self.fill = fill
|
||||
self.cornerRadius = cornerRadius
|
||||
self.shadows = shadows
|
||||
self.edge = edge
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .circular)
|
||||
ZStack {
|
||||
shadows.filter { !$0.inset }.reduce(AnyView(shape.fill(fill))) { view, layer in
|
||||
AnyView(view.shadow(color: layer.color(scheme), radius: layer.blur / 2, x: layer.x, y: layer.y))
|
||||
}
|
||||
ForEach(Array(shadows.filter(\.inset).enumerated()), id: \.offset) { _, layer in
|
||||
InsetShadow(layer: layer, cornerRadius: cornerRadius)
|
||||
}
|
||||
if let edge {
|
||||
shape.strokeBorder(edge, lineWidth: Border.hairline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A shadow cast inwards from the edge of a rounded rectangle.
|
||||
struct InsetShadow: View {
|
||||
let layer: ShadowLayer
|
||||
let cornerRadius: CGFloat
|
||||
@Environment(\.colorScheme) private var scheme
|
||||
|
||||
var body: some View {
|
||||
Canvas { context, size in
|
||||
let rect = CGRect(origin: .zero, size: size)
|
||||
context.clip(to: Path(roundedRect: rect, cornerRadius: cornerRadius))
|
||||
context.drawLayer { inner in
|
||||
inner.addFilter(.blur(radius: layer.blur / 2))
|
||||
let hole = rect.insetBy(dx: layer.spread, dy: layer.spread).offsetBy(dx: layer.x, dy: layer.y)
|
||||
var frame = Path(rect.insetBy(dx: -size.width, dy: -size.height))
|
||||
frame.addPath(Path(roundedRect: hole, cornerRadius: max(0, cornerRadius - layer.spread)))
|
||||
inner.fill(frame, with: .color(layer.color(scheme)), style: FillStyle(eoFill: true))
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Faceplate of matt plastic, sitting above the chassis.
|
||||
public struct PanelBackground: View {
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
PhysicalSurface(fill: AnyShapeStyle(Palette.surfacePanel), cornerRadius: Radius.none, shadows: Shadows.panel)
|
||||
}
|
||||
}
|
||||
|
||||
/// A recessed area (input fields, display windows).
|
||||
public struct WellBackground: View {
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
PhysicalSurface(fill: AnyShapeStyle(Palette.surfaceWell), cornerRadius: Radius.none, shadows: Shadows.well)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dark display glass for the counter.
|
||||
public struct DisplayBackground: View {
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
PhysicalSurface(fill: AnyShapeStyle(Palette.surfaceDisplay), cornerRadius: Radius.none, shadows: Shadows.well)
|
||||
}
|
||||
}
|
||||
|
||||
/// Cassette-label cream, for lists of transcripts and dictionary entries.
|
||||
public struct PaperBackground: View {
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
PhysicalSurface(fill: AnyShapeStyle(Palette.surfacePaper), cornerRadius: Radius.none, shadows: Shadows.well)
|
||||
}
|
||||
}
|
||||
|
||||
/// An engraved groove: a dark line with a light line below it.
|
||||
public struct SeamDivider: View {
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Rectangle().fill(Palette.lineSeamDark).frame(height: Border.hairline)
|
||||
Rectangle().fill(Palette.lineSeamLight).frame(height: Border.hairline)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Small capitals silkscreen label.
|
||||
public struct SilkLabel: View {
|
||||
private let text: String
|
||||
private let style: TextStyle
|
||||
|
||||
public init(_ text: String, style: TextStyle = Typography.label) {
|
||||
self.text = text
|
||||
self.style = style
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Text(text).textStyle(style).foregroundStyle(Palette.inkTertiary)
|
||||
}
|
||||
}
|
||||
|
||||
/// Brushed aluminium: a vertical gradient with a horizontal grain.
|
||||
public struct BrushedBackground: View {
|
||||
@Environment(\.colorScheme) private var scheme
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
LinearGradient(colors: [Palette.surfaceMetalHigh, Palette.surfaceMetalLow], startPoint: .top, endPoint: .bottom)
|
||||
Image(nsImage: scheme == .dark ? BrushedTexture.dark : BrushedTexture.light)
|
||||
.resizable(resizingMode: .tile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum BrushedTexture {
|
||||
private static let side = 256
|
||||
private static let gain = 2.2 // makes the token grain value visible as streak opacity
|
||||
|
||||
static let light = make(grain: Texture.brushedGrainLight)
|
||||
static let dark = make(grain: Texture.brushedGrainDark)
|
||||
|
||||
private struct SplitMix64: RandomNumberGenerator {
|
||||
var state: UInt64
|
||||
mutating func next() -> UInt64 {
|
||||
state &+= 0x9E37_79B9_7F4A_7C15
|
||||
var z = state
|
||||
z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
|
||||
z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
|
||||
return z ^ (z >> 31)
|
||||
}
|
||||
}
|
||||
|
||||
/// A seeded grain: every row has its own brightness and pixels vary only slightly, which gives fine horizontal streaks.
|
||||
private static func make(grain: Double) -> NSImage {
|
||||
var rng = SplitMix64(state: 7)
|
||||
let rows = (0..<side).map { _ in Double.random(in: 0...1, using: &rng) }
|
||||
var pixels = [UInt8](repeating: 0, count: side * side * 4)
|
||||
for y in 0..<side {
|
||||
let row = rows[y]
|
||||
for x in 0..<side {
|
||||
let value = row * 0.9 + Double.random(in: 0...1, using: &rng) * 0.1
|
||||
let alpha = min(1, abs(value - 0.5) * 2 * grain * gain)
|
||||
let shade = value > 0.5 ? 1.0 : 0.0
|
||||
let index = (y * side + x) * 4
|
||||
let premultiplied = UInt8(shade * alpha * 255)
|
||||
pixels[index] = premultiplied
|
||||
pixels[index + 1] = premultiplied
|
||||
pixels[index + 2] = premultiplied
|
||||
pixels[index + 3] = UInt8(alpha * 255)
|
||||
}
|
||||
}
|
||||
let provider = CGDataProvider(data: Data(pixels) as CFData)!
|
||||
let image = CGImage(
|
||||
width: side, height: side, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: side * 4,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
||||
provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)!
|
||||
return NSImage(cgImage: image, size: NSSize(width: side, height: side))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user