Files

176 lines
6.6 KiB
Swift

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))
}
}