From be15d1690a98abc0f178dfd930d522b7926a1b30 Mon Sep 17 00:00:00 2001 From: trussosw Date: Tue, 10 Nov 2026 23:34:15 -0500 Subject: [PATCH] Add pen size, ink tone and a 1 second fade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small drawing options in Settings → Drawing: - Pen size: Thin, Medium (today's width) or Thick. - Ink tone: Vivid (today's colours), Pastel, Muted or Deep. Shifts every ink colour together, including Match the pointer; white and black stay as they are. - Lines fade: adds After 1 second. Both new settings are cleared by Reset like the rest. Co-Authored-By: Claude Opus 5.5 --- Sources/TinyHand/ArmView.swift | 2 +- Sources/TinyHand/Preferences.swift | 71 ++++++++++++++++++++++++++++- Sources/TinyHand/SettingsView.swift | 23 ++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/Sources/TinyHand/ArmView.swift b/Sources/TinyHand/ArmView.swift index 7de9cee..3c4fefc 100644 --- a/Sources/TinyHand/ArmView.swift +++ b/Sources/TinyHand/ArmView.swift @@ -20,7 +20,7 @@ final class ArmView: NSView { let ink = Ink() var held = false - var inkWidth: CGFloat { max(3, bounds.height * 0.0055) } + var inkWidth: CGFloat { max(2, max(3, bounds.height * 0.0055) * Preferences.penSize.scale) } private var pressAmount: CGFloat = 0 private var gripX: CGFloat? diff --git a/Sources/TinyHand/Preferences.swift b/Sources/TinyHand/Preferences.swift index 6eff8f7..2d3e5de 100644 --- a/Sources/TinyHand/Preferences.swift +++ b/Sources/TinyHand/Preferences.swift @@ -59,7 +59,12 @@ enum InkChoice: String, CaseIterable { } } + /// The colour with the Ink tone setting applied. var color: CGColor? { + baseColor.map(Preferences.inkTone.apply) + } + + private var baseColor: CGColor? { switch self { case .match: return nil case .red: return Ink.red @@ -74,7 +79,59 @@ enum InkChoice: String, CaseIterable { } func resolved(for style: PointerStyle) -> CGColor { - color ?? style.inkColor + color ?? Preferences.inkTone.apply(style.inkColor) + } +} + +/// Shifts every ink colour together: softer, greyer or darker. +enum InkTone: String, CaseIterable { + case vivid, pastel, muted, deep + + var title: String { + switch self { + case .vivid: return "Vivid" + case .pastel: return "Pastel" + case .muted: return "Muted" + case .deep: return "Deep" + } + } + + func apply(_ color: CGColor) -> CGColor { + guard self != .vivid, let ns = NSColor(cgColor: color)?.usingColorSpace(.sRGB) else { + return color + } + var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + ns.getHue(&h, saturation: &s, brightness: &b, alpha: &a) + if s < 0.08 { + return color // leave white and black alone + } + switch self { + case .vivid: break + case .pastel: s *= 0.45; b = min(1, b * 0.25 + 0.78) + case .muted: s *= 0.55; b *= 0.82 + case .deep: s = min(1, s * 1.05); b *= 0.62 + } + return NSColor(hue: h, saturation: s, brightness: b, alpha: a).cgColor + } +} + +enum PenSize: String, CaseIterable { + case thin, medium, thick + + var title: String { + switch self { + case .thin: return "Thin" + case .medium: return "Medium" + case .thick: return "Thick" + } + } + + var scale: CGFloat { + switch self { + case .thin: return 0.55 + case .medium: return 1 + case .thick: return 1.9 + } } } @@ -109,6 +166,16 @@ enum Preferences { set { set(newValue, "fadeSeconds") } } + static var inkTone: InkTone { + get { defaults.string(forKey: "inkTone").flatMap(InkTone.init(rawValue:)) ?? .vivid } + set { set(newValue.rawValue, "inkTone") } + } + + static var penSize: PenSize { + get { defaults.string(forKey: "penSize").flatMap(PenSize.init(rawValue:)) ?? .medium } + set { set(newValue.rawValue, "penSize") } + } + static var handSound: Bool { get { defaults.object(forKey: "sound.hands") as? Bool ?? true } set { set(newValue, "sound.hands") } @@ -187,7 +254,7 @@ enum Preferences { static func resetAll() { for key in ["shortcut.toggle", "shortcut.clear", "pointerSize", "showShadow", "fadeSeconds", "sound.hands", "sound.laser", "interactKey", "interactKeepsHand", "doubleShiftClickThrough", "pinnedStyles", "pointerStyle", - "inkChoice", "showInkButton", "showToolbar", "tapSound"] { + "inkChoice", "showInkButton", "showToolbar", "tapSound", "inkTone", "penSize"] { defaults.removeObject(forKey: key) } NotificationCenter.default.post(name: changed, object: nil) diff --git a/Sources/TinyHand/SettingsView.swift b/Sources/TinyHand/SettingsView.swift index 4b62fc9..a8eb08f 100644 --- a/Sources/TinyHand/SettingsView.swift +++ b/Sources/TinyHand/SettingsView.swift @@ -973,6 +973,7 @@ private struct AppearancePage: View { private struct DrawingPage: View { @ObservedObject var model: SettingsModel private static let choices: [(String, Double)] = [ + ("After 1 second", 1), ("After 3 seconds", 3), ("After 6 seconds", 6), ("After 10 seconds", 10), @@ -991,6 +992,28 @@ private struct DrawingPage: View { } } } + Row("Ink tone", detail: "Shifts every colour together: Pastel is softer, Muted is greyer, Deep is darker.") { + Picker("", selection: model.binding({ Preferences.inkTone }, { Preferences.inkTone = $0 })) { + ForEach(InkTone.allCases, id: \.self) { tone in + Text(tone.title).tag(tone) + } + } + .labelsHidden() + .pickerStyle(.segmented) + .fixedSize() + .focusable(false) + } + Row("Pen size") { + Picker("", selection: model.binding({ Preferences.penSize }, { Preferences.penSize = $0 })) { + ForEach(PenSize.allCases, id: \.self) { size in + Text(size.title).tag(size) + } + } + .labelsHidden() + .pickerStyle(.segmented) + .fixedSize() + .focusable(false) + } Row("Lines fade", detail: "Clear them any time with \(Preferences.clearShortcut.display).") { Picker("", selection: model.binding({ Preferences.fadeSeconds }, { Preferences.fadeSeconds = $0 })) { ForEach(DrawingPage.choices, id: \.1) { choice in