agentix-cli 0.97.0 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/apps/mac-voice/Sources/AgentXVoice/Character.swift +41 -63
  2. package/apps/mac-voice/Sources/AgentXVoice/CharacterMath.swift +9 -0
  3. package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +46 -44
  4. package/apps/mac-voice/Sources/AgentXVoice/CharacterSimFrame.swift +59 -0
  5. package/apps/mac-voice/Sources/AgentXVoice/CharacterView.swift +57 -0
  6. package/apps/mac-voice/Sources/AgentXVoice/Config.swift +2 -1
  7. package/apps/mac-voice/Sources/AgentXVoice/Guide.swift +185 -0
  8. package/apps/mac-voice/Sources/AgentXVoice/GuideMath.swift +49 -0
  9. package/apps/mac-voice/Sources/AgentXVoice/Meets.swift +39 -0
  10. package/apps/mac-voice/Sources/AgentXVoice/Panel.swift +9 -0
  11. package/apps/mac-voice/Sources/AgentXVoice/PlayActs.swift +35 -8
  12. package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +17 -9
  13. package/apps/mac-voice/Sources/AgentXVoice/PlayMotion.swift +110 -0
  14. package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +43 -5
  15. package/apps/mac-voice/Sources/AgentXVoice/PointerPlay.swift +137 -0
  16. package/apps/mac-voice/Sources/AgentXVoice/main.swift +7 -0
  17. package/dist/{App-6R4I6QPE.js → App-XLQDP26J.js} +2 -2
  18. package/dist/{ChatApp-R3O4BJ24.js → ChatApp-DRDUXZ66.js} +2 -2
  19. package/dist/{agent-P6GPE7HD.js → agent-QHM3J2XS.js} +2 -2
  20. package/dist/{board-dashboard-AKQHIY4J.js → board-dashboard-F7ZNW6YA.js} +9 -9
  21. package/dist/board-dashboard-F7ZNW6YA.js.map +1 -0
  22. package/dist/{chunk-QPIMPKZ2.js → chunk-4BCAH7XI.js} +2 -2
  23. package/dist/{chunk-3S4IYAOC.js → chunk-B32A6OOY.js} +2 -2
  24. package/dist/{chunk-QHAO6U7U.js → chunk-BERHVF3T.js} +2 -2
  25. package/dist/{chunk-TWC4CYTU.js → chunk-BHKWXFJD.js} +5 -5
  26. package/dist/{chunk-Q53XKCRL.js → chunk-BTLW2RML.js} +2 -2
  27. package/dist/{chunk-55HM3SUN.js → chunk-DRUEPGLP.js} +2 -2
  28. package/dist/{chunk-SZ5TGL25.js → chunk-DXJRMPGH.js} +2 -2
  29. package/dist/{chunk-ZCQCUA4J.js → chunk-GJ5EJVMZ.js} +2 -2
  30. package/dist/{chunk-2GFSHO24.js → chunk-IQJ7UZVV.js} +2 -2
  31. package/dist/{chunk-6W3SCPDK.js → chunk-J4JVKVEK.js} +2 -2
  32. package/dist/{chunk-MDFUSQ6S.js → chunk-J64VIHL5.js} +2 -2
  33. package/dist/{chunk-ERPEU2ZR.js → chunk-JREA25GJ.js} +3 -3
  34. package/dist/chunk-JREA25GJ.js.map +1 -0
  35. package/dist/{chunk-WLKO6FOL.js → chunk-JZJ7AZJD.js} +2 -2
  36. package/dist/{chunk-4BG3WJSP.js → chunk-LXJGLNVG.js} +2 -2
  37. package/dist/{chunk-LXHTVVJV.js → chunk-OFVMUBIP.js} +2 -2
  38. package/dist/{chunk-TOGRRWBX.js → chunk-QUU5RJ7D.js} +2 -2
  39. package/dist/{chunk-F3DW5ZJN.js → chunk-S2GPUSBU.js} +2 -2
  40. package/dist/{chunk-C2POHIWJ.js → chunk-SJMOZY7Y.js} +2 -2
  41. package/dist/{chunk-3SF7PLHF.js → chunk-WEYMSJGZ.js} +191 -190
  42. package/dist/chunk-WEYMSJGZ.js.map +1 -0
  43. package/dist/{chunk-X4S2I233.js → chunk-YYCFI4VP.js} +2 -2
  44. package/dist/{chunk-DGVTRG5Q.js → chunk-Z4FY64BS.js} +2 -2
  45. package/dist/cli.js +1 -1
  46. package/dist/{client-UQKCIEHA.js → client-BLUCLNIS.js} +2 -2
  47. package/dist/{config-OE23UC5C.js → config-ZUMI4IY4.js} +2 -2
  48. package/dist/config-mutator-S3CD2IWQ.js +2 -0
  49. package/dist/{doctor-QQ7YLJK4.js → doctor-XGQW5SMD.js} +2 -2
  50. package/dist/index.js +1 -1
  51. package/dist/{program-O5RQ6NRG.js → program-3FNVZD26.js} +161 -161
  52. package/dist/program-3FNVZD26.js.map +1 -0
  53. package/dist/{promote-AW4TJXXT.js → promote-K5LP5Q6Z.js} +2 -2
  54. package/dist/{query-24BGIJCZ.js → query-LHXSINM2.js} +2 -2
  55. package/dist/{registry-GZT2OSBK.js → registry-OQUO6NCW.js} +2 -2
  56. package/dist/{request-planner-U6HHKWLX.js → request-planner-TW5OCKTF.js} +2 -2
  57. package/dist/{review-candidates-LJUJCVFA.js → review-candidates-EFELFOSY.js} +2 -2
  58. package/dist/{routing-WCYPBKFH.js → routing-TSRZLD7B.js} +2 -2
  59. package/dist/schedule-tool-JUHRVU5D.js +2 -0
  60. package/dist/scheduler-EHJPTULN.js +2 -0
  61. package/dist/{seat-5PBTFTGS.js → seat-NIPWHLKC.js} +2 -2
  62. package/dist/{select-JD7MSY25.js → select-UAKDLN76.js} +2 -2
  63. package/dist/{set-enabled-MUSO2KJL.js → set-enabled-74DLXMSM.js} +2 -2
  64. package/dist/{tool-Y7SOGJ2S.js → tool-CEBL57UW.js} +2 -2
  65. package/dist/{tool-FMKGK474.js → tool-TEGHFORD.js} +2 -2
  66. package/dist/wiki-WOK4SCB7.js +2 -0
  67. package/package.json +1 -1
  68. package/dist/board-dashboard-AKQHIY4J.js.map +0 -1
  69. package/dist/chunk-3SF7PLHF.js.map +0 -1
  70. package/dist/chunk-ERPEU2ZR.js.map +0 -1
  71. package/dist/config-mutator-2UK5GX4B.js +0 -2
  72. package/dist/program-O5RQ6NRG.js.map +0 -1
  73. package/dist/schedule-tool-E7ER2XOI.js +0 -2
  74. package/dist/scheduler-FK3LOWU2.js +0 -2
  75. package/dist/wiki-SJRTAYMT.js +0 -2
  76. /package/dist/{App-6R4I6QPE.js.map → App-XLQDP26J.js.map} +0 -0
  77. /package/dist/{ChatApp-R3O4BJ24.js.map → ChatApp-DRDUXZ66.js.map} +0 -0
  78. /package/dist/{agent-P6GPE7HD.js.map → agent-QHM3J2XS.js.map} +0 -0
  79. /package/dist/{chunk-QPIMPKZ2.js.map → chunk-4BCAH7XI.js.map} +0 -0
  80. /package/dist/{chunk-3S4IYAOC.js.map → chunk-B32A6OOY.js.map} +0 -0
  81. /package/dist/{chunk-QHAO6U7U.js.map → chunk-BERHVF3T.js.map} +0 -0
  82. /package/dist/{chunk-TWC4CYTU.js.map → chunk-BHKWXFJD.js.map} +0 -0
  83. /package/dist/{chunk-Q53XKCRL.js.map → chunk-BTLW2RML.js.map} +0 -0
  84. /package/dist/{chunk-55HM3SUN.js.map → chunk-DRUEPGLP.js.map} +0 -0
  85. /package/dist/{chunk-SZ5TGL25.js.map → chunk-DXJRMPGH.js.map} +0 -0
  86. /package/dist/{chunk-ZCQCUA4J.js.map → chunk-GJ5EJVMZ.js.map} +0 -0
  87. /package/dist/{chunk-2GFSHO24.js.map → chunk-IQJ7UZVV.js.map} +0 -0
  88. /package/dist/{chunk-6W3SCPDK.js.map → chunk-J4JVKVEK.js.map} +0 -0
  89. /package/dist/{chunk-MDFUSQ6S.js.map → chunk-J64VIHL5.js.map} +0 -0
  90. /package/dist/{chunk-WLKO6FOL.js.map → chunk-JZJ7AZJD.js.map} +0 -0
  91. /package/dist/{chunk-4BG3WJSP.js.map → chunk-LXJGLNVG.js.map} +0 -0
  92. /package/dist/{chunk-LXHTVVJV.js.map → chunk-OFVMUBIP.js.map} +0 -0
  93. /package/dist/{chunk-TOGRRWBX.js.map → chunk-QUU5RJ7D.js.map} +0 -0
  94. /package/dist/{chunk-F3DW5ZJN.js.map → chunk-S2GPUSBU.js.map} +0 -0
  95. /package/dist/{chunk-C2POHIWJ.js.map → chunk-SJMOZY7Y.js.map} +0 -0
  96. /package/dist/{chunk-X4S2I233.js.map → chunk-YYCFI4VP.js.map} +0 -0
  97. /package/dist/{chunk-DGVTRG5Q.js.map → chunk-Z4FY64BS.js.map} +0 -0
  98. /package/dist/{client-UQKCIEHA.js.map → client-BLUCLNIS.js.map} +0 -0
  99. /package/dist/{config-OE23UC5C.js.map → config-ZUMI4IY4.js.map} +0 -0
  100. /package/dist/{config-mutator-2UK5GX4B.js.map → config-mutator-S3CD2IWQ.js.map} +0 -0
  101. /package/dist/{doctor-QQ7YLJK4.js.map → doctor-XGQW5SMD.js.map} +0 -0
  102. /package/dist/{promote-AW4TJXXT.js.map → promote-K5LP5Q6Z.js.map} +0 -0
  103. /package/dist/{query-24BGIJCZ.js.map → query-LHXSINM2.js.map} +0 -0
  104. /package/dist/{registry-GZT2OSBK.js.map → registry-OQUO6NCW.js.map} +0 -0
  105. /package/dist/{request-planner-U6HHKWLX.js.map → request-planner-TW5OCKTF.js.map} +0 -0
  106. /package/dist/{review-candidates-LJUJCVFA.js.map → review-candidates-EFELFOSY.js.map} +0 -0
  107. /package/dist/{routing-WCYPBKFH.js.map → routing-TSRZLD7B.js.map} +0 -0
  108. /package/dist/{schedule-tool-E7ER2XOI.js.map → schedule-tool-JUHRVU5D.js.map} +0 -0
  109. /package/dist/{scheduler-FK3LOWU2.js.map → scheduler-EHJPTULN.js.map} +0 -0
  110. /package/dist/{seat-5PBTFTGS.js.map → seat-NIPWHLKC.js.map} +0 -0
  111. /package/dist/{select-JD7MSY25.js.map → select-UAKDLN76.js.map} +0 -0
  112. /package/dist/{set-enabled-MUSO2KJL.js.map → set-enabled-74DLXMSM.js.map} +0 -0
  113. /package/dist/{tool-Y7SOGJ2S.js.map → tool-CEBL57UW.js.map} +0 -0
  114. /package/dist/{tool-FMKGK474.js.map → tool-TEGHFORD.js.map} +0 -0
  115. /package/dist/{wiki-SJRTAYMT.js.map → wiki-WOK4SCB7.js.map} +0 -0
@@ -0,0 +1,49 @@
1
+ import CoreGraphics
2
+ import Foundation
3
+
4
+ /// Guiding (#482): the answering agent sends the character to something
5
+ /// on screen. Where it stands to show it, and the mark it leaves there.
6
+ /// Kept free of AppKit so the tests can check it without a window.
7
+ /// Rectangles are in screen coordinates, origin bottom-left, as AppKit
8
+ /// uses them, once toAppKit has turned them.
9
+ enum GuideMath {
10
+ enum Mark: String {
11
+ case box, circle, underline, none
12
+ }
13
+
14
+ /// Accessibility → AppKit: the daemon's rectangles are measured from
15
+ /// the top of the primary display, whichever screen they are on.
16
+ static func toAppKit(_ r: CGRect, primaryHeight: CGFloat) -> CGRect {
17
+ CGRect(x: r.minX, y: primaryHeight - r.maxY, width: r.width, height: r.height)
18
+ }
19
+
20
+ /// The point under the character while it shows `rect`: beside it,
21
+ /// its body level with the middle. On the left, away from which its
22
+ /// bubble reaches, so the bubble does not cover what it shows; on the
23
+ /// right when the left has no room in `visible`.
24
+ static func stand(beside rect: CGRect, in visible: CGRect, body: CGFloat, tall: CGFloat,
25
+ inset: CGFloat, gap: CGFloat = 14) -> CGPoint {
26
+ let left = rect.minX - gap - body / 2
27
+ let x = left >= visible.minX + inset ? left : rect.maxX + gap + body / 2
28
+ return CGPoint(x: x, y: rect.midY - tall / 2)
29
+ }
30
+
31
+ /// What the mark is drawn along, around `rect`: a box a little wider
32
+ /// than it, an oval that clears its corners, or a line under it.
33
+ static func outline(_ mark: Mark, around rect: CGRect) -> CGRect {
34
+ switch mark {
35
+ case .box: return rect.insetBy(dx: -5, dy: -5)
36
+ // An oval through a rectangle's corners is √2 times its size.
37
+ case .circle: return rect.insetBy(dx: -(rect.width * 0.21 + 6), dy: -(rect.height * 0.21 + 6))
38
+ case .underline: return CGRect(x: rect.minX - 2, y: rect.minY - 5, width: rect.width + 4, height: 0)
39
+ case .none: return .null
40
+ }
41
+ }
42
+
43
+ /// One frame of its way up or down to where it is sent: most of the
44
+ /// way in a third of a second, and there once it is within a point.
45
+ static func glide(_ y: CGFloat, toward goal: CGFloat, dt: Double) -> CGFloat {
46
+ let next = y + (goal - y) * CGFloat(1 - exp(-7 * min(max(dt, 0), 0.1)))
47
+ return abs(goal - next) < 1 ? goal : next
48
+ }
49
+ }
@@ -0,0 +1,39 @@
1
+ import CoreGraphics
2
+ import Foundation
3
+
4
+ /// What the character meets on the live screen (#539): the sides of the
5
+ /// windows that stand on its line. Read from the window list, which gives
6
+ /// each window's rectangle and no picture, title or text, so it needs no
7
+ /// permission.
8
+ enum Meets {
9
+ /// A window smaller than this is not something to stop at.
10
+ static let least = CGSize(width: 80, height: 40)
11
+
12
+ /// Where the sides of `windows`, front to back, cross `band`: each
13
+ /// side only where no window in front covers it. One set of
14
+ /// coordinates for both.
15
+ static func edges(of windows: [CGRect], band: ClosedRange<CGFloat>) -> [Double] {
16
+ var found: [Double] = []
17
+ for (i, window) in windows.enumerated() {
18
+ let low = max(window.minY, band.lowerBound), high = min(window.maxY, band.upperBound)
19
+ guard window.width >= least.width, window.height >= least.height, low < high else { continue }
20
+ let y = (low + high) / 2
21
+ for x in [window.minX, window.maxX] {
22
+ let covered = windows[..<i].contains { $0.minX < x && x < $0.maxX && $0.minY <= y && y < $0.maxY }
23
+ if !covered { found.append(Double(x)) }
24
+ }
25
+ }
26
+ return found
27
+ }
28
+
29
+ /// The ordinary windows on screen, front to back, measured from the
30
+ /// top left of the main screen.
31
+ static func windows() -> [CGRect] {
32
+ let list = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
33
+ return list.compactMap { window in
34
+ guard window[kCGWindowLayer as String] as? Int == 0, window[kCGWindowAlpha as String] as? Double != 0,
35
+ let bounds = window[kCGWindowBounds as String] else { return nil }
36
+ return CGRect(dictionaryRepresentation: bounds as! CFDictionary)
37
+ }
38
+ }
39
+ }
@@ -570,6 +570,15 @@ final class Panel: NSPanel {
570
570
  onRender?(state)
571
571
  }
572
572
 
573
+ /// On screen with something to use: an answer, an error or the call
574
+ /// buttons. The character does not play with the pointer then (#537).
575
+ @MainActor
576
+ var holdsSomething: Bool {
577
+ guard isVisible else { return false }
578
+ if case .error = current { return true }
579
+ return expanded || callBar.mode != .hidden
580
+ }
581
+
573
582
  /// The call buttons for this moment of a call; `.hidden` outside one.
574
583
  @MainActor
575
584
  func showCall(_ mode: CallBar.Mode) {
@@ -5,12 +5,23 @@ import Foundation
5
5
  /// different play each time. Numbers only, like `PlayMath`.
6
6
  extension PlayMath {
7
7
  /// A piece cut out of the picture and drawn somewhere else: which
8
- /// part of the picture, where its top-left corner is now, and how far
9
- /// it has turned (radians).
8
+ /// part of the picture, where its top-left corner is now, how far it
9
+ /// has turned (radians), and the page's colour around it, which is
10
+ /// left out so only its ink moves.
10
11
  struct Piece: Equatable {
11
12
  var from: Rect
12
13
  var at: Point
13
14
  var turn = 0.0
15
+ var paper: UInt32 = 0xFFFFFF
16
+ }
17
+
18
+ /// How much of a pixel of a moved piece is drawn, 0…1: none of the
19
+ /// page's colour, all of the ink, and the soft edge of a letter in
20
+ /// between. Without it a flying letter carries a box of page with it
21
+ /// and covers the text it passes.
22
+ static func ink(r: UInt8, g: UInt8, b: UInt8, paper: UInt32) -> Double {
23
+ let far = max(abs(Int(r) - Int(paper >> 16 & 0xFF)), abs(Int(g) - Int(paper >> 8 & 0xFF)), abs(Int(b) - Int(paper & 0xFF)))
24
+ return min(max(Double(far - 10) / 40, 0), 1)
14
25
  }
15
26
 
16
27
  /// A piece from the moment it leaves its place. Its place is covered
@@ -36,18 +47,22 @@ extension PlayMath {
36
47
  static let gravity = 1400.0
37
48
  static let lift = 0.35
38
49
 
39
- /// Where a thrown piece is `seconds` after it left. It stops on the
40
- /// bottom of the picture and at its sides.
50
+ /// Where a thrown piece is `seconds` after it left. It bounces once
51
+ /// on the bottom of the picture and lies still, and stops at its sides.
41
52
  static func thrown(_ source: Rect, v: Point, spin: Double, rise: Double, after seconds: Double,
42
53
  width: Double, height: Double) -> Piece {
43
54
  let floor = max(height - source.h - rise, source.y)
44
55
  let landing = (-v.y + (v.y * v.y + 2 * gravity * (floor - source.y)).squareRoot()) / gravity
45
56
  let s = min(max(seconds, 0), landing)
46
- let x = min(max(source.x + v.x * s, 0), max(width - source.w, 0))
57
+ // How fast it comes down, and the bounce that makes: a third as
58
+ // fast, sideways too.
59
+ let hit = v.y + gravity * landing
60
+ let more = min(max(seconds - landing, 0), PlayMotion.bounceTime(speed: hit, gravity: gravity))
61
+ let x = min(max(source.x + v.x * (s + more / 3), 0), max(width - source.w, 0))
47
62
  // It turns about as fast as `spin`, and lands flat: the right way
48
63
  // up or upside down.
49
64
  let flat = (spin * landing / .pi).rounded() * .pi
50
- return Piece(from: source, at: Point(x: x, y: source.y + v.y * s + gravity * s * s / 2), turn: landing > 0 ? flat * s / landing : 0)
65
+ return Piece(from: source, at: Point(x: x, y: source.y + v.y * s + gravity * s * s / 2 - PlayMotion.bounce(more, speed: hit, gravity: gravity)), turn: landing > 0 ? flat * s / landing : 0)
51
66
  }
52
67
 
53
68
  /// What it can do to a line.
@@ -107,17 +122,29 @@ extension Play {
107
122
  f.gone.append((cover(cut.source), cut.paper))
108
123
  switch cut.path {
109
124
  case .thrown(let v, let spin, let rise):
110
- f.pieces.append(P.thrown(cut.source, v: v, spin: spin, rise: rise, after: f.t - cut.start, width: size.x, height: size.y))
125
+ var piece = P.thrown(cut.source, v: v, spin: spin, rise: rise, after: f.t - cut.start, width: size.x, height: size.y)
126
+ piece.paper = cut.paper
127
+ f.pieces.append(piece)
111
128
  case .held(let until, let to):
112
129
  // From its place to above the head, and from there down.
113
130
  let head = P.Point(x: f.at.x - cut.source.w / 2, y: f.at.y - P.top - cut.source.h)
114
131
  let a = f.t < until ? P.Point(x: cut.source.x, y: cut.source.y) : head, b = f.t < until ? head : to
115
132
  let q = M.ease(f.t < until ? M.span(f.t, cut.start, cut.start + P.lift) : M.span(f.t, until, until + P.lift))
116
- f.pieces.append(P.Piece(from: cut.source, at: P.Point(x: a.x + (b.x - a.x) * q, y: a.y + (b.y - a.y) * q)))
133
+ f.pieces.append(P.Piece(from: cut.source, at: P.Point(x: a.x + (b.x - a.x) * q, y: a.y + (b.y - a.y) * q), paper: cut.paper))
117
134
  }
118
135
  }
119
136
  }
120
137
 
138
+ /// How far along a move it is: a jump crouches first and lands at the
139
+ /// end, a walk and a wipe start and stop, the rest keep one pace.
140
+ func body(_ m: Move, at t: Double) -> PlayMotion.Body {
141
+ switch m.kind {
142
+ case .jump, .carry: return PlayMotion.jump(t - m.start, of: m.duration)
143
+ case .walk, .wipe: return PlayMotion.glide(t - m.start, of: m.duration)
144
+ default: return PlayMotion.Body(travel: M.span(t, m.start, m.end))
145
+ }
146
+ }
147
+
121
148
  /// Which way it faces during a move, and its pose without the motion.
122
149
  func look(_ m: Move) -> (face: Double, pose: M.Pose) {
123
150
  let way: Double = m.to.x >= m.from.x ? 1 : -1
@@ -90,6 +90,7 @@ enum PlayMath {
90
90
  struct Play {
91
91
  typealias P = PlayMath
92
92
  typealias M = CharacterMath
93
+ typealias Mo = PlayMotion
93
94
 
94
95
  enum Kind { case jump, walk, eat, wipe, kick, stomp, fall, lift, carry, drop, rest }
95
96
 
@@ -131,13 +132,16 @@ struct Play {
131
132
  self.lines = lines
132
133
  self.size = size
133
134
  var at = start, now = 0.0
135
+ /// The seconds of a jump over `points`: its crouch, its time in
136
+ /// the air and its landing.
137
+ func leap(_ points: Double) -> Double { Mo.crouch + 0.34 + points / 900 + Mo.landing }
134
138
  func add(_ kind: Kind, to: P.Point, _ seconds: Double, line: Int = 0, words: Range<Int> = 0..<0) -> Move {
135
139
  Move(kind: kind, from: at, to: to, start: now, duration: seconds, line: line, words: words)
136
140
  }
137
141
  /// Get to `to` first: a jump, unless it is already there.
138
142
  func reach(_ to: P.Point, into list: inout [Move]) {
139
143
  guard at != to else { return }
140
- let m = add(.jump, to: to, 0.5 + hypot(to.x - at.x, to.y - at.y) / 900)
144
+ let m = add(.jump, to: to, leap(hypot(to.x - at.x, to.y - at.y)))
141
145
  list.append(m); at = to; now = m.end
142
146
  }
143
147
  var list: [Move] = [], cuts: [P.Cut] = []
@@ -183,8 +187,10 @@ struct Play {
183
187
  let word = lines[i].words[w]
184
188
  reach(P.Point(x: word.rect.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
185
189
  let m = stay(.kick, 0.6, line: i)
186
- for (k, letter) in P.letters(of: word).enumerated() {
187
- throwing(letter, line: i, at: m.start + 0.25, v: P.Point(x: 150 + 40 * Double(k), y: -320 - 30 * Double(k % 3)),
190
+ // The letters leave one after the other, the nearest first.
191
+ let letters = P.letters(of: word)
192
+ for (k, letter) in letters.enumerated() {
193
+ throwing(letter, line: i, at: m.start + 0.25 + Mo.stagger(k, of: letters.count), v: P.Point(x: 150 + 40 * Double(k), y: -320 - 30 * Double(k % 3)),
188
194
  spin: 4 + Double(k % 3))
189
195
  }
190
196
  case .stomp(let i):
@@ -207,7 +213,7 @@ struct Play {
207
213
  let word = lines[i].words[w].rect, home = lines[j].rect
208
214
  reach(P.Point(x: word.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
209
215
  let up = stay(.lift, P.lift, line: i)
210
- let m = add(.carry, to: P.Point(x: home.maxX - 4, y: home.y), 0.5 + hypot(home.maxX - 4 - at.x, home.y - at.y) / 900, line: j)
216
+ let m = add(.carry, to: P.Point(x: home.maxX - 4, y: home.y), leap(hypot(home.maxX - 4 - at.x, home.y - at.y)), line: j)
211
217
  list.append(m); at = m.to; now = m.end
212
218
  let down = stay(.drop, P.lift, line: j)
213
219
  // After the line, or on top of its end when the page stops there.
@@ -228,12 +234,13 @@ struct Play {
228
234
  let m = moves[i], p = M.span(t, m.start, m.end)
229
235
  var f = Frame(t: t, done: t >= duration)
230
236
 
231
- f.at = P.Point(x: m.from.x + (m.to.x - m.from.x) * p, y: m.from.y + (m.to.y - m.from.y) * p)
237
+ let body = body(m, at: t)
238
+ f.at = P.Point(x: m.from.x + (m.to.x - m.from.x) * body.travel, y: m.from.y + (m.to.y - m.from.y) * body.travel)
232
239
  switch m.kind {
233
240
  case .jump, .carry:
234
241
  // An arc over both ends, kept inside the picture.
235
242
  let rise = 30 + hypot(m.to.x - m.from.x, m.to.y - m.from.y) * 0.12
236
- f.at.y -= min(rise, max(min(m.from.y, m.to.y) - P.top, 0)) * 4 * p * (1 - p)
243
+ f.at.y -= min(rise, max(min(m.from.y, m.to.y) - P.top, 0)) * body.air
237
244
  // Three hops on the spot.
238
245
  case .stomp: f.at.y -= min(22, max(m.from.y - P.top, 0)) * abs(sin(3 * .pi * p))
239
246
  // Faster and faster.
@@ -249,10 +256,11 @@ struct Play {
249
256
  f.face = before.face + (now.face - before.face) * into
250
257
  f.pose = M.blend(before.pose, now.pose, eyes: into, body: into, marks: into)
251
258
  switch m.kind {
252
- case .walk: f.pose.lift += 3 * into * abs(sin(2 * .pi * t * 2.2))
259
+ // A step every 50 points, smaller while it starts and stops.
260
+ case .walk: f.pose.lift += 3 * body.speed * abs(sin(.pi * abs(f.at.x - m.from.x) / 50))
253
261
  case .jump, .carry:
254
- let air = sin(.pi * p)
255
- f.pose.sy *= 1 + 0.12 * air; f.pose.sx *= 1 - 0.08 * air
262
+ f.pose.sy *= body.sy; f.pose.sx *= body.sx
263
+ f.pose.lift *= 1 - 0.5 * body.dip
256
264
  case .eat:
257
265
  let bite = into * (0.5 + 0.5 * sin(2 * .pi * t / P.perLetter))
258
266
  f.pose.sy *= 1 - 0.08 * bite; f.pose.sx *= 1 + 0.05 * bite
@@ -0,0 +1,110 @@
1
+ import Foundation
2
+
3
+ /// How things move in play mode (#505), after the rules the videos are
4
+ /// made with: nothing starts or stops at full speed, a jump is announced
5
+ /// by a crouch and ends in a soft landing, letters leave one after the
6
+ /// other, and what falls bounces once before it lies still. Numbers only.
7
+ enum PlayMotion {
8
+ typealias M = CharacterMath
9
+
10
+ /// Fast start, slow stop; and slow start, fast end.
11
+ static func out(_ p: Double) -> Double { 1 - into(1 - p) }
12
+ static func into(_ p: Double) -> Double {
13
+ let p = min(max(p, 0), 1)
14
+ return p * p
15
+ }
16
+
17
+ /// The body at one moment of a move: how far along its way (0…1),
18
+ /// how high in its arc (0…1), its stretch, how deep it sits on its
19
+ /// feet (0…1) and how fast it goes (0…1).
20
+ struct Body: Equatable {
21
+ var travel = 0.0, air = 0.0
22
+ var sx = 1.0, sy = 1.0
23
+ var dip = 0.0
24
+ var speed = 0.0
25
+ }
26
+
27
+ /// Seconds: the crouch before a jump, the landing after it, and the
28
+ /// start and the stop of a walk.
29
+ static let crouch = 0.16
30
+ static let landing = 0.26
31
+ static let walkUp = 0.2
32
+ static let walkDown = 0.26
33
+ /// How much taller the body is at full stretch, and how much shorter
34
+ /// at the deepest of a crouch and of a landing.
35
+ static let stretch = 0.14
36
+ static let squat = 0.16
37
+ static let squash = 0.2
38
+
39
+ /// A jump of `duration` seconds, `t` seconds in. It crouches first
40
+ /// and goes nowhere, leaves stretched, is round at the top, comes
41
+ /// down stretched, and lands: squashed, a little too tall once, then
42
+ /// still. It has arrived when the landing begins.
43
+ static func jump(_ t: Double, of duration: Double) -> Body {
44
+ guard duration > 0 else { return Body(travel: 1) }
45
+ let t = min(max(t, 0), duration)
46
+ let before = min(crouch, duration * 0.25), after = min(landing, duration * 0.3)
47
+ // A jump too short for a whole landing changes shape less, not faster.
48
+ let long = stretch * after / landing, flat = squash * after / landing
49
+ if t < before {
50
+ // Down slowly, up fast: the push.
51
+ let q = t / before
52
+ let deep = q < 0.65 ? out(q / 0.65) : 1 - into((q - 0.65) / 0.35)
53
+ return Body(sx: 1 + squat * 0.7 * deep, sy: 1 - squat * deep, dip: deep)
54
+ }
55
+ if t <= duration - after {
56
+ let u = M.span(t, before, duration - after)
57
+ // Stretched while it moves fast up or down, round at the top.
58
+ let more = long * abs(1 - 2 * u) * min(u / 0.12, 1)
59
+ return Body(travel: u, air: 4 * u * (1 - u), sx: 1 - more * 0.6, sy: 1 + more, speed: 1)
60
+ }
61
+ let more = keys([(0, long), (0.4, -flat), (0.72, 0.04), (1, 0)], at: (t - (duration - after)) / after)
62
+ return Body(travel: 1, sx: 1 - more * 0.6, sy: 1 + more, dip: max(-more / flat, 0))
63
+ }
64
+
65
+ /// A walk, or a wipe, of `duration` seconds, `t` seconds in: it gets
66
+ /// up to speed, keeps it, and slows to a stop.
67
+ static func glide(_ t: Double, of duration: Double) -> Body {
68
+ guard duration > 0 else { return Body(travel: 1) }
69
+ let t = min(max(t, 0), duration)
70
+ let up = min(walkUp, duration / 3), down = min(walkDown, duration / 3)
71
+ // Full speed, in ways a second.
72
+ let top = 1 / (duration - up / 2 - down / 2)
73
+ if t < up { return Body(travel: top * t * t / (2 * up), speed: t / up) }
74
+ if t <= duration - down { return Body(travel: top * (t - up / 2), speed: 1) }
75
+ let left = duration - t
76
+ return Body(travel: 1 - top * left * left / (2 * down), speed: left / down)
77
+ }
78
+
79
+ /// When the `k`th of `count` things starts after the first: one after
80
+ /// the other, and all gone within 0.4 seconds, so it reads as one go.
81
+ static func stagger(_ k: Int, of count: Int) -> Double {
82
+ guard count > 1 else { return 0 }
83
+ return Double(k) * min(0.035, 0.4 / Double(count - 1))
84
+ }
85
+
86
+ /// How high something is above where it landed, `seconds` after it
87
+ /// hit at `speed` (points a second) under `gravity`: one bounce, a
88
+ /// third as fast as it came down, then it lies still.
89
+ static func bounce(_ seconds: Double, speed: Double, gravity: Double) -> Double {
90
+ let up = abs(speed) / 3
91
+ guard seconds > 0, gravity > 0 else { return 0 }
92
+ return max(up * seconds - gravity * seconds * seconds / 2, 0)
93
+ }
94
+
95
+ /// The seconds that bounce lasts.
96
+ static func bounceTime(speed: Double, gravity: Double) -> Double {
97
+ gravity > 0 ? 2 * abs(speed) / 3 / gravity : 0
98
+ }
99
+
100
+ /// The value at `p` between (place, value) keys in order, eased from
101
+ /// each key to the next.
102
+ static func keys(_ keys: [(at: Double, value: Double)], at p: Double) -> Double {
103
+ guard let first = keys.first, let last = keys.last else { return 0 }
104
+ if p <= first.at { return first.value }
105
+ for (a, b) in zip(keys, keys.dropFirst()) where p <= b.at {
106
+ return a.value + (b.value - a.value) * M.ease(M.span(p, a.at, b.at))
107
+ }
108
+ return last.value
109
+ }
110
+ }
@@ -4,7 +4,7 @@ import AppKit
4
4
  /// words are gone, the pieces that moved, the cloth, and the character. The context is flipped,
5
5
  /// in the picture's points.
6
6
  enum PlayDraw {
7
- static func draw(_ frame: Play.Frame, in ctx: CGContext, stops: [NSColor], picture: CGImage?, scale: CGFloat) {
7
+ static func draw(_ frame: Play.Frame, in ctx: CGContext, stops: [NSColor], cutouts: PlayCutouts?) {
8
8
  for gone in frame.gone {
9
9
  let c = gone.paper
10
10
  ctx.setFillColor(CGColor(srgbRed: CGFloat(c >> 16 & 0xFF) / 255, green: CGFloat(c >> 8 & 0xFF) / 255,
@@ -13,7 +13,7 @@ enum PlayDraw {
13
13
  }
14
14
  for piece in frame.pieces {
15
15
  let from = cg(piece.from)
16
- guard let part = picture?.cropping(to: from.applying(CGAffineTransform(scaleX: scale, y: scale))) else { continue }
16
+ guard let part = cutouts?.image(from, paper: piece.paper) else { continue }
17
17
  ctx.saveGState()
18
18
  ctx.translateBy(x: piece.at.x + from.width / 2, y: piece.at.y + from.height / 2)
19
19
  ctx.rotate(by: piece.turn)
@@ -40,6 +40,41 @@ enum PlayDraw {
40
40
  private static func cg(_ r: PlayMath.Rect) -> CGRect { CGRect(x: r.x, y: r.y, width: r.w, height: r.h) }
41
41
  }
42
42
 
43
+ /// The pieces of the picture that move, each cut out once: the ink alone,
44
+ /// the page's colour around it left out (`PlayMath.ink`).
45
+ final class PlayCutouts {
46
+ private let picture: CGImage
47
+ /// Pixels of the picture per point.
48
+ private let scale: CGFloat
49
+ private var made: [[CGFloat]: CGImage] = [:]
50
+
51
+ init(picture: CGImage, scale: CGFloat) {
52
+ self.picture = picture
53
+ self.scale = scale
54
+ }
55
+
56
+ /// The part of the picture at `from` (points), without its paper.
57
+ func image(_ from: CGRect, paper: UInt32) -> CGImage? {
58
+ let key = [from.minX, from.minY, from.width, from.height]
59
+ if let done = made[key] { return done }
60
+ guard let part = picture.cropping(to: from.applying(CGAffineTransform(scaleX: scale, y: scale))),
61
+ let ctx = CGContext(data: nil, width: part.width, height: part.height, bitsPerComponent: 8, bytesPerRow: part.width * 4,
62
+ space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue),
63
+ let data = ctx.data else { return nil }
64
+ ctx.draw(part, in: CGRect(x: 0, y: 0, width: part.width, height: part.height))
65
+ let px = data.bindMemory(to: UInt8.self, capacity: part.width * part.height * 4)
66
+ for i in stride(from: 0, to: part.width * part.height * 4, by: 4) {
67
+ let a = PlayMath.ink(r: px[i], g: px[i + 1], b: px[i + 2], paper: paper)
68
+ // Premultiplied: the colour fades with it.
69
+ for k in 0..<3 { px[i + k] = UInt8(Double(px[i + k]) * a) }
70
+ px[i + 3] = UInt8(255 * a)
71
+ }
72
+ let image = ctx.makeImage()
73
+ made[key] = image
74
+ return image
75
+ }
76
+ }
77
+
43
78
  /// The stage: a frozen picture of the screen laid over everything, with
44
79
  /// the character playing on it. The real page is never touched: nothing
45
80
  /// is clicked, typed or changed, and what looks eaten or wiped is only
@@ -157,7 +192,8 @@ private final class PlayView: NSView {
157
192
  var shown = Play.Frame()
158
193
  var stops = CharacterDraw.stops(tint: Brand.accent, colors: nil)
159
194
  /// The picture under it: moved pieces are cut from it.
160
- var picture: CGImage?
195
+ var picture: CGImage? { didSet { cutouts = nil } }
196
+ private var cutouts: PlayCutouts?
161
197
  var onInput: (() -> Void)?
162
198
 
163
199
  override var isFlipped: Bool { true }
@@ -166,8 +202,10 @@ private final class PlayView: NSView {
166
202
 
167
203
  override func draw(_ dirtyRect: NSRect) {
168
204
  guard let ctx = NSGraphicsContext.current?.cgContext else { return }
169
- PlayDraw.draw(shown, in: ctx, stops: stops, picture: picture,
170
- scale: CGFloat(picture?.width ?? 0) / max(bounds.width, 1))
205
+ if cutouts == nil, let picture {
206
+ cutouts = PlayCutouts(picture: picture, scale: CGFloat(picture.width) / max(bounds.width, 1))
207
+ }
208
+ PlayDraw.draw(shown, in: ctx, stops: stops, cutouts: cutouts)
171
209
  }
172
210
 
173
211
  override func keyDown(with event: NSEvent) { onInput?() }
@@ -0,0 +1,137 @@
1
+ import Foundation
2
+
3
+ /// Play with the pointer (#505): with "Play mode" ticked, an idle
4
+ /// character does more than step aside. When the pointer moves within
5
+ /// its sight it starts a game: it follows it along the edge, crouches
6
+ /// and jumps at it like a cat, or runs away from it. Between games it
7
+ /// rests, and the usual rules hold. A click in its sight makes it jump.
8
+ ///
9
+ /// It only decides where the character wants to be and how it crouches
10
+ /// and hops; `CharacterSim` moves it. Foundation only, and the same
11
+ /// games in the same order on every run, so the tests can count on them.
12
+ struct PointerPlay {
13
+ typealias M = CharacterMath
14
+
15
+ enum Game: Equatable { case follow, pounce, flee }
16
+
17
+ struct Out: Equatable {
18
+ /// Where it wants to be along the edge. Nil: no game, the usual rules.
19
+ var target: Double?
20
+ /// 0…1 crouched, and the height of a hop in drawing units.
21
+ var crouch = 0.0
22
+ var hop = 0.0
23
+ /// It came down on the pointer just now.
24
+ var caught = false
25
+ }
26
+
27
+ /// How far along the edge and above it the pointer is seen, in points.
28
+ static let sight = (x: 520.0, y: 360.0)
29
+ /// A pointer slower than this, in points a second, starts no game.
30
+ static let stir = 80.0
31
+ /// Following, it stops this short of the pointer.
32
+ static let gap = 70.0
33
+ /// It jumps at a pointer no further than this, and no higher.
34
+ static let leap = (x: 220.0, y: 150.0)
35
+ /// It has caught a pointer this close to where it comes down.
36
+ static let grip = 45.0
37
+ /// Running away, it puts this much between the pointer and itself.
38
+ static let far = 320.0
39
+ /// Seconds: the crouch before a jump, the jump, running away, and
40
+ /// the jump a click gives it.
41
+ static let wind = 0.5, air = 0.6, flight = 2.5, start = 0.35
42
+ /// Drawing units: how high it jumps at the pointer, and at a click.
43
+ static let high = 70.0, jolt = 34.0
44
+
45
+ private(set) var game: Game?
46
+ private(set) var games = 0
47
+ private var began = 0.0, until = 0.0, next = 0.0
48
+ private var jumpTo: Double?
49
+ private var seen: (x: Double, y: Double)?
50
+ private var down = false
51
+ private var jolted: Double?
52
+
53
+ /// One frame. `x` is where the character is; `pointer` along the edge
54
+ /// and above it, nil on another screen; `down` the mouse button.
55
+ mutating func step(_ now: Double, _ dt: Double, x: Double, pointer: (x: Double, y: Double)?,
56
+ down: Bool, range: ClosedRange<Double>) -> Out {
57
+ let clicked = down && !self.down
58
+ self.down = down
59
+ let before = seen
60
+ seen = pointer
61
+ guard let p = pointer, abs(p.x - x) < Self.sight.x, abs(p.y) < Self.sight.y else {
62
+ if game != nil { end(now) }
63
+ return Out(hop: hop(now))
64
+ }
65
+ func inside(_ v: Double) -> Double { min(max(v, range.lowerBound), range.upperBound) }
66
+
67
+ // A click startles it: a small jump, and the game is over.
68
+ if clicked && jumpTo == nil {
69
+ jolted = now
70
+ if game != nil { end(now) }
71
+ }
72
+
73
+ if game == nil, now >= next, jolted == nil, let was = before, dt > 0,
74
+ hypot(p.x - was.x, p.y - was.y) / dt > Self.stir {
75
+ games += 1
76
+ let pick = M.rnd(games, 7)
77
+ let reach = abs(p.x - x) < Self.leap.x && abs(p.y) < Self.leap.y
78
+ game = pick < 0.5 ? .follow : pick < 0.8 ? (reach ? .pounce : .follow) : .flee
79
+ began = now
80
+ jumpTo = nil
81
+ until = now + (game == .follow ? 4 + 3 * M.rnd(games, 2) : game == .flee ? Self.flight : Self.wind + Self.air)
82
+ }
83
+
84
+ var out = Out(hop: hop(now))
85
+ switch game {
86
+ case nil:
87
+ break
88
+ case .follow:
89
+ // On the side it is on, a little short of the pointer.
90
+ out.target = inside(p.x + (x >= p.x ? Self.gap : -Self.gap))
91
+ case .pounce:
92
+ let t = now - began
93
+ if t < Self.wind {
94
+ out.target = x
95
+ out.crouch = M.ease(t / (Self.wind * 0.8))
96
+ } else {
97
+ // It jumps at where the pointer was when it let go.
98
+ if jumpTo == nil { jumpTo = inside(p.x) }
99
+ let a = M.span(t, Self.wind, Self.wind + Self.air)
100
+ out.target = jumpTo
101
+ out.crouch = 1 - M.ease(a / 0.25)
102
+ out.hop = Self.high * sin(.pi * a)
103
+ }
104
+ case .flee:
105
+ out.target = abs(p.x - x) < Self.far ? M.aside(x: x, pointer: p.x, clear: Self.far, range: range) : x
106
+ }
107
+ if game != nil && now >= until {
108
+ out.caught = game == .pounce && abs(p.x - x) < Self.grip && abs(p.y) < Self.leap.y
109
+ end(now)
110
+ }
111
+ return out
112
+ }
113
+
114
+ /// The assistant has work, or play is off: no game, and none at once
115
+ /// after. The button is still followed, so one held through it is no click.
116
+ mutating func stop(_ now: Double, down: Bool) {
117
+ if game != nil { end(now) }
118
+ self.down = down
119
+ jolted = nil
120
+ seen = nil
121
+ }
122
+
123
+ /// A game is over: a rest of five to thirteen seconds before the next.
124
+ private mutating func end(_ now: Double) {
125
+ game = nil
126
+ jumpTo = nil
127
+ next = now + 5 + 8 * M.rnd(games, 3)
128
+ }
129
+
130
+ /// The jump a click gives it.
131
+ private mutating func hop(_ now: Double) -> Double {
132
+ guard let at = jolted else { return 0 }
133
+ let a = (now - at) / Self.start
134
+ if a >= 1 { jolted = nil; return 0 }
135
+ return Self.jolt * sin(.pi * a)
136
+ }
137
+ }
@@ -85,6 +85,7 @@ final class App: NSObject, NSApplicationDelegate {
85
85
 
86
86
  // An agent ringing the owner (#321).
87
87
  private let callWatcher = CallWatcher()
88
+ private let guideWatcher = GuideWatcher()
88
89
  private let ringer = Ringer()
89
90
  /// The call ringing on the pill now.
90
91
  private var ringingCall: IncomingCall?
@@ -392,6 +393,7 @@ final class App: NSObject, NSApplicationDelegate {
392
393
  panel.attach(head: head, visible: visible)
393
394
  return panel.isVisible ? panel.frame : nil
394
395
  }
396
+ character.bubbleShows = { [weak self] in self?.panel.holdsSomething ?? false }
395
397
  panel.onLook = { [weak self] state, tint, colors in
396
398
  self?.character.show(state.activity, tint: tint, colors: colors)
397
399
  // The assistant has something to show: play gives way.
@@ -507,6 +509,11 @@ final class App: NSObject, NSApplicationDelegate {
507
509
  return CallModel.pollQuery(ringing: self.ringingCall?.id ?? self.answeringCall, canRing: self.canRing)
508
510
  }
509
511
  callWatcher.start()
512
+ // The answering agent sends the character to something (#482).
513
+ guideWatcher.wanted = { [weak self] in self?.character.onScreen == true }
514
+ guideWatcher.onCommand = { [weak self] rect, mark in self?.character.guide(to: rect, mark: mark) }
515
+ guideWatcher.start()
516
+ character.onScreenChanged = { [weak self] in self?.guideWatcher.start() }
510
517
 
511
518
  // The target does not wait for the microphone: the menu shows it
512
519
  // either way.