Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions Sources/KeyPathAppKit/Models/LogicalKeymap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ struct LogicalKeymap: Identifiable {
let iconFilename: String // SVG filename in Resources/Keymaps (without extension)
let coreLabels: [UInt16: String] // 30-key letter block (always applied)
let extraLabels: [UInt16: String] // Number row + outer punctuation (toggle)
let shiftLabels: [UInt16: String] // Shifted legends when the keymap defines them

init(
id: String,
name: String,
description: String,
learnMoreURL: URL,
iconFilename: String,
coreLabels: [UInt16: String],
extraLabels: [UInt16: String],
shiftLabels: [UInt16: String] = [:]
) {
self.id = id
self.name = name
self.description = description
self.learnMoreURL = learnMoreURL
self.iconFilename = iconFilename
self.coreLabels = coreLabels
self.extraLabels = extraLabels
self.shiftLabels = shiftLabels
}

func label(for keyCode: UInt16, includeExtraKeys: Bool) -> String? {
if let label = coreLabels[keyCode] {
Expand Down Expand Up @@ -93,7 +114,8 @@ struct LogicalKeymap: Identifiable {
learnMoreURL: URL(string: "https://support.apple.com/guide/mac-help/change-input-sources-mchlp1406/mac")!,
iconFilename: "QWERTY", // Fallback icon; UI uses globe SF Symbol instead
coreLabels: labels,
extraLabels: [:]
extraLabels: [:],
shiftLabels: provider.currentShiftLabels
)
}

Expand All @@ -108,7 +130,22 @@ struct LogicalKeymap: Identifiable {
home: ["a", "s", "d", "f", "g", "h", "j", "k", "l", ";"],
bottom: ["z", "x", "c", "v", "b", "n", "m", ",", ".", "/"]
),
extraLabels: [:]
extraLabels: [:],
shiftLabels: [
KeyCode.grave: "~",
KeyCode.one: "!",
KeyCode.two: "@",
KeyCode.three: "#",
KeyCode.four: "$",
KeyCode.five: "%",
KeyCode.six: "^",
KeyCode.seven: "&",
KeyCode.eight: "*",
KeyCode.nine: "(",
KeyCode.zero: ")",
KeyCode.minus: "_",
KeyCode.equal: "+",
]
)

/// AZERTY - French keyboard layout
Expand Down Expand Up @@ -278,6 +315,16 @@ struct LogicalKeymap: Identifiable {

private enum KeyCode {
static let grave: UInt16 = 50
static let one: UInt16 = 18
static let two: UInt16 = 19
static let three: UInt16 = 20
static let four: UInt16 = 21
static let five: UInt16 = 23
static let six: UInt16 = 22
static let seven: UInt16 = 26
static let eight: UInt16 = 28
static let nine: UInt16 = 25
static let zero: UInt16 = 29
static let minus: UInt16 = 27
static let equal: UInt16 = 24
static let leftBracket: UInt16 = 33
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,15 @@ struct KeyboardStageCinematicLighting: Equatable, Sendable {
)
}

/// The front's travel is eased, not linear: the keyboard occupies the
/// trailing ~58% of the window, so a linear sweep crosses every keycap in
/// the first ~0.28 s of the reveal and spends the rest on flat copy. The
/// ease-in keeps roughly 60% of the duration on the keyboard span.
static let frontTravelExponent: Float = 1.9

static func frontX(for entrance: KeyboardStageEntranceFrame) -> Float {
frontStart + (frontEnd - frontStart) * entrance.progress
let eased = pow(entrance.progress, frontTravelExponent)
return frontStart + (frontEnd - frontStart) * eased
}

static func exposure(
Expand Down Expand Up @@ -248,7 +255,7 @@ struct KeyboardStageEntrancePresentation: Equatable, Sendable {
}

static let holdDuration: TimeInterval = 1.50
static let transitionDuration: TimeInterval = 0.55
static let transitionDuration: TimeInterval = 0.75
static let reducedMotionTransitionDuration: TimeInterval = 0.25

private(set) var phase = Phase.pending
Expand Down
50 changes: 25 additions & 25 deletions Sources/KeyPathAppKit/UI/KeyboardStage/KeyboardStagePalette.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,43 +194,43 @@ struct KeyboardStagePalette: Equatable, Sendable {
)
case .recommended:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.875, 0.91, 0.929),
fill: KeyboardStageRGBA(0.910, 0.918, 0.922),
accent: KeyboardStageRGBA(0.439, 0.561, 0.631),
glow: KeyboardStageRGBA(0.263, 0.655, 0.902),
legend: KeyboardStageRGBA(0.337, 0.416, 0.467),
borderStrength: 0.52
legend: KeyboardStageRGBA(0.285, 0.337, 0.369),
borderStrength: 0.62
)
case .escape:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.835, 0.882, 0.918),
fill: KeyboardStageRGBA(0.900, 0.914, 0.922),
accent: KeyboardStageRGBA(0.38, 0.50, 0.57),
glow: KeyboardStageRGBA(0.263, 0.655, 0.902),
legend: KeyboardStageRGBA(0.337, 0.416, 0.467),
borderStrength: 0.68
legend: KeyboardStageRGBA(0.268, 0.326, 0.360),
borderStrength: 0.78
)
case .hyper:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.855, 0.89, 0.929),
fill: KeyboardStageRGBA(0.906, 0.914, 0.925),
accent: KeyboardStageRGBA(0.455, 0.549, 0.663),
glow: KeyboardStageRGBA(0.302, 0.518, 0.847),
legend: KeyboardStageRGBA(0.306, 0.376, 0.463),
borderStrength: 0.56
legend: KeyboardStageRGBA(0.278, 0.326, 0.386),
borderStrength: 0.68
)
case .launcher:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.855, 0.914, 0.925),
fill: KeyboardStageRGBA(0.905, 0.921, 0.923),
accent: KeyboardStageRGBA(0.404, 0.60, 0.647),
glow: KeyboardStageRGBA(0.263, 0.655, 0.902),
legend: KeyboardStageRGBA(0.251, 0.373, 0.396),
borderStrength: 0.52
legend: KeyboardStageRGBA(0.251, 0.345, 0.365),
borderStrength: 0.64
)
case .installed:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.863, 0.91, 0.925),
fill: KeyboardStageRGBA(0.906, 0.918, 0.923),
accent: KeyboardStageRGBA(0.404, 0.576, 0.647),
glow: KeyboardStageRGBA(0.263, 0.655, 0.902),
legend: KeyboardStageRGBA(0.278, 0.376, 0.416),
borderStrength: 0.48
legend: KeyboardStageRGBA(0.265, 0.345, 0.377),
borderStrength: 0.60
)
case .dimmed:
KeyboardStageSurfaceStyle(
Expand Down Expand Up @@ -267,38 +267,38 @@ struct KeyboardStagePalette: Equatable, Sendable {
)
case .recommended:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.15, 0.31, 0.54),
fill: KeyboardStageRGBA(0.115, 0.145, 0.190),
accent: KeyboardStageRGBA(0.34, 0.66, 1),
legend: KeyboardStageRGBA(0.94, 0.98, 1),
borderStrength: 0.52
borderStrength: 0.68
)
case .escape:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.12, 0.37, 0.66),
fill: KeyboardStageRGBA(0.095, 0.145, 0.205),
accent: KeyboardStageRGBA(0.39, 0.76, 1),
legend: KeyboardStageRGBA(0.96, 0.99, 1),
borderStrength: 0.58
borderStrength: 0.82
)
case .hyper:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.35, 0.23, 0.61),
fill: KeyboardStageRGBA(0.135, 0.125, 0.195),
accent: KeyboardStageRGBA(0.70, 0.55, 1),
legend: KeyboardStageRGBA(0.98, 0.97, 1),
borderStrength: 0.62
borderStrength: 0.76
)
case .launcher:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.12, 0.43, 0.45),
fill: KeyboardStageRGBA(0.095, 0.165, 0.175),
accent: KeyboardStageRGBA(0.32, 0.83, 0.79),
legend: KeyboardStageRGBA(0.94, 1, 0.99),
borderStrength: 0.58
borderStrength: 0.72
)
case .installed:
KeyboardStageSurfaceStyle(
fill: KeyboardStageRGBA(0.15, 0.39, 0.30),
fill: KeyboardStageRGBA(0.105, 0.165, 0.145),
accent: KeyboardStageRGBA(0.38, 0.81, 0.59),
legend: KeyboardStageRGBA(0.95, 1, 0.97),
borderStrength: 0.5
borderStrength: 0.66
)
case .dimmed:
KeyboardStageSurfaceStyle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@ struct KeyboardStageProjection: Equatable, Sendable {
let destinationHeight = Float(max(1, size.height))
let aspectRatio = destinationWidth / destinationHeight
let bounds = scene.layoutBounds
let zoom = max(0.1, scene.viewport.zoom)
let horizontalZoom = max(0.1, scene.viewport.zoom)
// The keyboard hero is intentionally wider than it is tall. Above 1x,
// only the horizontal camera moves closer; the vertical stays at 1 so
// the whole board — top edge through bottom apron — remains in frame.
// A vertical crop makes the keys read as sliding off the deck.
let verticalZoom = min(1, horizontalZoom)

var visibleHeight = min(bounds.size.height, bounds.size.height / zoom)
var visibleHeight = min(bounds.size.height, bounds.size.height / verticalZoom)
var visibleWidth = visibleHeight * aspectRatio
if visibleWidth > bounds.size.width {
visibleWidth = min(bounds.size.width, bounds.size.width / zoom)
visibleWidth = min(bounds.size.width, bounds.size.width / verticalZoom)
visibleHeight = visibleWidth / aspectRatio
}
if horizontalZoom > verticalZoom {
visibleWidth *= verticalZoom / horizontalZoom
}

visibleWidth = min(bounds.size.width, max(0.1, visibleWidth))
visibleHeight = min(bounds.size.height, max(0.1, visibleHeight))
Expand Down
21 changes: 18 additions & 3 deletions Sources/KeyPathAppKit/UI/KeyboardStage/KeyboardStageScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,22 +198,35 @@ enum KeyboardStageRevealTarget: Equatable, Sendable {

// MARK: - Scene elements

/// Horizontal placement of a legend within its keycap. MacBook hardware sets
/// word legends on wide edge keys toward the outer edge (`tab`, `caps lock`,
/// and left `shift` leading; `delete`, `return`, and right `shift` trailing)
/// while letters, symbols, and stacked legends stay centered.
enum KeyboardStageLegendAlignment: Equatable, Sendable {
case leading
case center
case trailing
}

struct KeyboardStageLegend: Equatable, Sendable {
var primary: String
var previous: String?
var secondary: String?
var transitionProgress: Float
var alignment: KeyboardStageLegendAlignment

init(
primary: String,
previous: String? = nil,
secondary: String? = nil,
transitionProgress: Float = 1
transitionProgress: Float = 1,
alignment: KeyboardStageLegendAlignment = .center
) {
self.primary = primary
self.previous = previous
self.secondary = secondary
self.transitionProgress = transitionProgress
self.alignment = alignment
}

static func interpolated(
Expand All @@ -226,7 +239,8 @@ struct KeyboardStageLegend: Equatable, Sendable {
primary: end.primary,
previous: start.primary,
secondary: progress < 0.5 ? start.secondary : end.secondary,
transitionProgress: progress
transitionProgress: progress,
alignment: progress < 0.5 ? start.alignment : end.alignment
)
}

Expand All @@ -238,7 +252,8 @@ struct KeyboardStageLegend: Equatable, Sendable {
start.transitionProgress,
end.transitionProgress,
progress
)
),
alignment: end.alignment
)
}
}
Expand Down
Loading
Loading