176 lines
6.6 KiB
Swift
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))
|
|
}
|
|
}
|