agentix-cli 0.96.1 → 0.98.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 (109) hide show
  1. package/apps/mac-voice/Sources/AgentXVoice/Character.swift +4 -1
  2. package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +50 -4
  3. package/apps/mac-voice/Sources/AgentXVoice/PlayActs.swift +164 -0
  4. package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +94 -46
  5. package/apps/mac-voice/Sources/AgentXVoice/PlayMotion.swift +110 -0
  6. package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +56 -3
  7. package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +6 -0
  8. package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +4 -1
  9. package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +18 -1
  10. package/apps/mac-voice/Sources/AgentXVoice/main.swift +24 -10
  11. package/dist/{App-UHSIO3UE.js → App-XLQDP26J.js} +2 -2
  12. package/dist/{ChatApp-LLTFBLJY.js → ChatApp-DRDUXZ66.js} +2 -2
  13. package/dist/{agent-6RJ633MQ.js → agent-QHM3J2XS.js} +2 -2
  14. package/dist/{board-dashboard-A3ZEI4F6.js → board-dashboard-AIT26JTK.js} +4 -4
  15. package/dist/{chunk-ZZSNM2RI.js → chunk-4BCAH7XI.js} +2 -2
  16. package/dist/{chunk-ALWMYA2B.js → chunk-B32A6OOY.js} +2 -2
  17. package/dist/{chunk-VO2GNAKB.js → chunk-BERHVF3T.js} +2 -2
  18. package/dist/{chunk-6DCY2JCB.js → chunk-BHKWXFJD.js} +5 -5
  19. package/dist/{chunk-4XHKJZLJ.js → chunk-BTLW2RML.js} +2 -2
  20. package/dist/{chunk-DU6536CC.js → chunk-DRUEPGLP.js} +2 -2
  21. package/dist/{chunk-KIGFWUKP.js → chunk-DXJRMPGH.js} +2 -2
  22. package/dist/{chunk-IPDEWVVT.js → chunk-GJ5EJVMZ.js} +2 -2
  23. package/dist/{chunk-EBVB727L.js → chunk-IQJ7UZVV.js} +2 -2
  24. package/dist/{chunk-ZVW645NU.js → chunk-J4JVKVEK.js} +2 -2
  25. package/dist/{chunk-TE44E733.js → chunk-J64VIHL5.js} +2 -2
  26. package/dist/{chunk-XEKZKH3W.js → chunk-JREA25GJ.js} +4 -4
  27. package/dist/chunk-JREA25GJ.js.map +1 -0
  28. package/dist/{chunk-KEKO4S45.js → chunk-JZJ7AZJD.js} +2 -2
  29. package/dist/{chunk-MKF6HSBA.js → chunk-LXJGLNVG.js} +2 -2
  30. package/dist/{chunk-QZIEYDIY.js → chunk-OFVMUBIP.js} +2 -2
  31. package/dist/{chunk-GHXSBJZS.js → chunk-QUU5RJ7D.js} +2 -2
  32. package/dist/{chunk-ZYJDAT3E.js → chunk-S2GPUSBU.js} +2 -2
  33. package/dist/{chunk-J2MHITWR.js → chunk-SJMOZY7Y.js} +2 -2
  34. package/dist/{chunk-MNS4SBRR.js → chunk-WWCVQHV3.js} +149 -149
  35. package/dist/chunk-WWCVQHV3.js.map +1 -0
  36. package/dist/{chunk-44UGVUN7.js → chunk-YYCFI4VP.js} +2 -2
  37. package/dist/{chunk-6NECC6YS.js → chunk-Z4FY64BS.js} +2 -2
  38. package/dist/cli.js +1 -1
  39. package/dist/{client-ARS6ZFT2.js → client-BLUCLNIS.js} +2 -2
  40. package/dist/{config-XO5Z3BO5.js → config-ZUMI4IY4.js} +2 -2
  41. package/dist/config-mutator-S3CD2IWQ.js +2 -0
  42. package/dist/{doctor-VUK3JE6Z.js → doctor-XGQW5SMD.js} +2 -2
  43. package/dist/index.d.ts +7 -1
  44. package/dist/index.js +1 -1
  45. package/dist/{program-HUQB4IIO.js → program-FK7REU2M.js} +117 -117
  46. package/dist/program-FK7REU2M.js.map +1 -0
  47. package/dist/{promote-UGQXA4KV.js → promote-K5LP5Q6Z.js} +2 -2
  48. package/dist/{query-DL7RHPMA.js → query-LHXSINM2.js} +2 -2
  49. package/dist/{registry-YIPKBRBH.js → registry-OQUO6NCW.js} +2 -2
  50. package/dist/{request-planner-L2FAVNKP.js → request-planner-TW5OCKTF.js} +2 -2
  51. package/dist/{review-candidates-RPUP4XBU.js → review-candidates-EFELFOSY.js} +2 -2
  52. package/dist/{routing-G66SVTRO.js → routing-TSRZLD7B.js} +2 -2
  53. package/dist/schedule-tool-JUHRVU5D.js +2 -0
  54. package/dist/scheduler-EHJPTULN.js +2 -0
  55. package/dist/{seat-ZPH7DA5L.js → seat-NIPWHLKC.js} +2 -2
  56. package/dist/{select-2WTV2BI2.js → select-UAKDLN76.js} +2 -2
  57. package/dist/{set-enabled-KI2FINQD.js → set-enabled-74DLXMSM.js} +2 -2
  58. package/dist/{tool-ZFQ46KU3.js → tool-CEBL57UW.js} +2 -2
  59. package/dist/{tool-2P4LRWN4.js → tool-TEGHFORD.js} +2 -2
  60. package/dist/wiki-WOK4SCB7.js +2 -0
  61. package/package.json +1 -1
  62. package/dist/chunk-MNS4SBRR.js.map +0 -1
  63. package/dist/chunk-XEKZKH3W.js.map +0 -1
  64. package/dist/config-mutator-MJX4EIYV.js +0 -2
  65. package/dist/program-HUQB4IIO.js.map +0 -1
  66. package/dist/schedule-tool-FOWPIWKP.js +0 -2
  67. package/dist/scheduler-GFVUPAD6.js +0 -2
  68. package/dist/wiki-XATGT3DI.js +0 -2
  69. /package/dist/{App-UHSIO3UE.js.map → App-XLQDP26J.js.map} +0 -0
  70. /package/dist/{ChatApp-LLTFBLJY.js.map → ChatApp-DRDUXZ66.js.map} +0 -0
  71. /package/dist/{agent-6RJ633MQ.js.map → agent-QHM3J2XS.js.map} +0 -0
  72. /package/dist/{board-dashboard-A3ZEI4F6.js.map → board-dashboard-AIT26JTK.js.map} +0 -0
  73. /package/dist/{chunk-ZZSNM2RI.js.map → chunk-4BCAH7XI.js.map} +0 -0
  74. /package/dist/{chunk-ALWMYA2B.js.map → chunk-B32A6OOY.js.map} +0 -0
  75. /package/dist/{chunk-VO2GNAKB.js.map → chunk-BERHVF3T.js.map} +0 -0
  76. /package/dist/{chunk-6DCY2JCB.js.map → chunk-BHKWXFJD.js.map} +0 -0
  77. /package/dist/{chunk-4XHKJZLJ.js.map → chunk-BTLW2RML.js.map} +0 -0
  78. /package/dist/{chunk-DU6536CC.js.map → chunk-DRUEPGLP.js.map} +0 -0
  79. /package/dist/{chunk-KIGFWUKP.js.map → chunk-DXJRMPGH.js.map} +0 -0
  80. /package/dist/{chunk-IPDEWVVT.js.map → chunk-GJ5EJVMZ.js.map} +0 -0
  81. /package/dist/{chunk-EBVB727L.js.map → chunk-IQJ7UZVV.js.map} +0 -0
  82. /package/dist/{chunk-ZVW645NU.js.map → chunk-J4JVKVEK.js.map} +0 -0
  83. /package/dist/{chunk-TE44E733.js.map → chunk-J64VIHL5.js.map} +0 -0
  84. /package/dist/{chunk-KEKO4S45.js.map → chunk-JZJ7AZJD.js.map} +0 -0
  85. /package/dist/{chunk-MKF6HSBA.js.map → chunk-LXJGLNVG.js.map} +0 -0
  86. /package/dist/{chunk-QZIEYDIY.js.map → chunk-OFVMUBIP.js.map} +0 -0
  87. /package/dist/{chunk-GHXSBJZS.js.map → chunk-QUU5RJ7D.js.map} +0 -0
  88. /package/dist/{chunk-ZYJDAT3E.js.map → chunk-S2GPUSBU.js.map} +0 -0
  89. /package/dist/{chunk-J2MHITWR.js.map → chunk-SJMOZY7Y.js.map} +0 -0
  90. /package/dist/{chunk-44UGVUN7.js.map → chunk-YYCFI4VP.js.map} +0 -0
  91. /package/dist/{chunk-6NECC6YS.js.map → chunk-Z4FY64BS.js.map} +0 -0
  92. /package/dist/{client-ARS6ZFT2.js.map → client-BLUCLNIS.js.map} +0 -0
  93. /package/dist/{config-XO5Z3BO5.js.map → config-ZUMI4IY4.js.map} +0 -0
  94. /package/dist/{config-mutator-MJX4EIYV.js.map → config-mutator-S3CD2IWQ.js.map} +0 -0
  95. /package/dist/{doctor-VUK3JE6Z.js.map → doctor-XGQW5SMD.js.map} +0 -0
  96. /package/dist/{promote-UGQXA4KV.js.map → promote-K5LP5Q6Z.js.map} +0 -0
  97. /package/dist/{query-DL7RHPMA.js.map → query-LHXSINM2.js.map} +0 -0
  98. /package/dist/{registry-YIPKBRBH.js.map → registry-OQUO6NCW.js.map} +0 -0
  99. /package/dist/{request-planner-L2FAVNKP.js.map → request-planner-TW5OCKTF.js.map} +0 -0
  100. /package/dist/{review-candidates-RPUP4XBU.js.map → review-candidates-EFELFOSY.js.map} +0 -0
  101. /package/dist/{routing-G66SVTRO.js.map → routing-TSRZLD7B.js.map} +0 -0
  102. /package/dist/{schedule-tool-FOWPIWKP.js.map → schedule-tool-JUHRVU5D.js.map} +0 -0
  103. /package/dist/{scheduler-GFVUPAD6.js.map → scheduler-EHJPTULN.js.map} +0 -0
  104. /package/dist/{seat-ZPH7DA5L.js.map → seat-NIPWHLKC.js.map} +0 -0
  105. /package/dist/{select-2WTV2BI2.js.map → select-UAKDLN76.js.map} +0 -0
  106. /package/dist/{set-enabled-KI2FINQD.js.map → set-enabled-74DLXMSM.js.map} +0 -0
  107. /package/dist/{tool-ZFQ46KU3.js.map → tool-CEBL57UW.js.map} +0 -0
  108. /package/dist/{tool-2P4LRWN4.js.map → tool-TEGHFORD.js.map} +0 -0
  109. /package/dist/{wiki-XATGT3DI.js.map → wiki-WOK4SCB7.js.map} +0 -0
@@ -113,6 +113,9 @@ final class CharacterHost {
113
113
  redraw()
114
114
  }
115
115
 
116
+ /// `voice.stroll`: a slow walk now and then while it has nothing to do.
117
+ var strolls = false
118
+
116
119
  /// "Animated orb" in the menu.
117
120
  func setAnimated(_ on: Bool) {
118
121
  guard animated != on else { return }
@@ -217,7 +220,7 @@ final class CharacterHost {
217
220
  || (bubbleFrame?.insetBy(dx: 0, dy: -PillPlacement.tail).contains(mouse) ?? false)
218
221
  || (NSEvent.modifierFlags.contains(.command) && window.frame.contains(mouse))
219
222
  frame = sim.step(to: ProcessInfo.processInfo.systemUptime,
220
- CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held,
223
+ CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held, strolls: strolls,
221
224
  home: home, range: Double(spot.ends.lowerBound)...Double(spot.ends.upperBound)))
222
225
  }
223
226
  let origin = NSPoint(x: (CGFloat(frame.x) - Self.size.width / 2).rounded(), y: (rest.y - Self.ground).rounded())
@@ -20,6 +20,9 @@ struct CharacterSim {
20
20
  /// The pointer is on its speech bubble: it stays where it is, so
21
21
  /// the bubble is not pulled from under the pointer.
22
22
  var held = false
23
+ /// `voice.stroll`: with nothing to do, it takes a slow stroll
24
+ /// beside where it rests now and then (#482).
25
+ var strolls = false
23
26
  /// Where it rests, and how far it may go.
24
27
  var home = 0.0
25
28
  var range: ClosedRange<Double> = 0...0
@@ -56,6 +59,9 @@ struct CharacterSim {
56
59
  /// How far it keeps from the pointer, and how long before it comes back.
57
60
  static let clear = 150.0
58
61
  static let awayFor = 4.0
62
+ /// A stroll: how far from where it rests, at most, and how fast.
63
+ static let strollReach = 110.0
64
+ static let strollSpeed = 28.0
59
65
 
60
66
  private(set) var mood = M.Mood.idle
61
67
  private var goal = M.Mood.idle
@@ -71,6 +77,11 @@ struct CharacterSim {
71
77
  private var target = 0.0
72
78
  private var awayUntil = 0.0
73
79
  private var steppedAside = false
80
+ /// A stroll: how far from home it is going and has got, when it turns
81
+ /// round or sets off again, and how many it has taken.
82
+ private var strollTo = 0.0, strolled = 0.0
83
+ private var strollNext: Double?
84
+ private var strolls = 0
74
85
  private var face = 0.0, lean = 0.0
75
86
  private var glance = (x: 0.0, y: 0.0)
76
87
 
@@ -110,11 +121,15 @@ struct CharacterSim {
110
121
  awayUntil = now + Self.awayFor
111
122
  restSince = now
112
123
  steppedAside = true
124
+ // The stroll is over: left alone it goes home, not back to
125
+ // where the pointer met it.
126
+ strolled = 0; strollTo = 0; strollNext = nil
113
127
  } else if now >= awayUntil {
114
128
  let taken = input.pointer.map { abs($0.x - input.home) < Self.clear && abs($0.y) < tall } ?? false
115
129
  if !taken {
116
130
  if target != input.home { steppedAside = false }
117
- target = min(max(input.home, input.range.lowerBound), input.range.upperBound)
131
+ stroll(now, dt, input)
132
+ target = min(max(input.home + strolled, input.range.lowerBound), input.range.upperBound)
118
133
  }
119
134
  }
120
135
 
@@ -197,16 +212,47 @@ struct CharacterSim {
197
212
  dots: dots.compactMap { dot(at: now, $0) }, stars: bursts.flatMap { stars(at: now, $0) })
198
213
  }
199
214
 
200
- /// Carried by the pointer (#502): it is where it is put, at once.
215
+ /// With nothing to do, a slow walk a little way from home, a wait
216
+ /// there, and a slow walk back: slow enough to leave no trail. Any
217
+ /// work, or dozing off, takes it home the same way.
218
+ private mutating func stroll(_ now: Double, _ dt: Double, _ input: Input) {
219
+ if !input.strolls || mood != .idle {
220
+ strollTo = 0; strollNext = nil
221
+ } else if strollNext == nil {
222
+ strollNext = now + 25 + 30 * M.rnd(strolls, 3)
223
+ } else if now >= strollNext! {
224
+ if strollTo == 0 {
225
+ strolls += 1
226
+ let far = 40 + (Self.strollReach - 40) * M.rnd(strolls, 1)
227
+ let room = (left: input.home - input.range.lowerBound, right: input.range.upperBound - input.home)
228
+ // To the side picked, unless only the other has the room.
229
+ var way: Double = M.rnd(strolls, 2) < 0.5 ? -1 : 1
230
+ if (way > 0 ? room.right : room.left) < far { way = -way }
231
+ strollTo = way * min(far, way > 0 ? room.right : room.left)
232
+ strollNext = now + abs(strollTo) / Self.strollSpeed + 5 + 6 * M.rnd(strolls, 4)
233
+ } else {
234
+ strollTo = 0
235
+ strollNext = nil
236
+ }
237
+ }
238
+ let pace = Self.strollSpeed * dt
239
+ strolled += min(max(strollTo - strolled, -pace), pace)
240
+ }
241
+
242
+ /// Carried by the pointer (#502): it is where it is put, at once, and
243
+ /// a stroll it was on is over.
201
244
  mutating func carry(to place: Double) {
202
245
  x = place; target = place; speed = 0
246
+ strolled = 0; strollTo = 0; strollNext = nil
203
247
  }
204
248
 
205
249
  /// The state alone, where it rests: for Reduce Motion, which shows a
206
250
  /// still picture that changes between states and never a loop.
207
251
  static func still(_ activity: M.Activity, home: Double) -> Frame {
208
- // Its marks at full strength, so each state reads without motion.
209
- Frame(pose: M.pose(M.mood(for: activity)), x: home, level: 1, voice: 1)
252
+ // Its marks at full strength, so each state reads without motion:
253
+ // the arcs of its voice only while it speaks.
254
+ let pose = M.pose(M.mood(for: activity))
255
+ return Frame(pose: pose, x: home, level: 1, voice: pose.speak)
210
256
  }
211
257
 
212
258
  private mutating func go(_ to: M.Mood, _ now: Double) {
@@ -0,0 +1,164 @@
1
+ import Foundation
2
+
3
+ /// Play mode past the first script (#505): pieces of the picture that
4
+ /// move (a kicked letter, a word that falls, a word carried away), and a
5
+ /// different play each time. Numbers only, like `PlayMath`.
6
+ extension PlayMath {
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, how far it
9
+ /// has turned (radians), and the page's colour around it, which is
10
+ /// left out so only its ink moves.
11
+ struct Piece: Equatable {
12
+ var from: Rect
13
+ var at: Point
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)
25
+ }
26
+
27
+ /// A piece from the moment it leaves its place. Its place is covered
28
+ /// with the page's colour from then on.
29
+ struct Cut {
30
+ enum Path {
31
+ /// Thrown at this speed (points a second) and turning (about
32
+ /// so many radians a second); it falls and stays on the bottom
33
+ /// of the picture, `rise` points up so a pile is not one row.
34
+ case thrown(v: Point, spin: Double, rise: Double)
35
+ /// Lifted above the character's head, kept there until
36
+ /// `until`, then put down with its corner at `to`.
37
+ case held(until: Double, to: Point)
38
+ }
39
+ var source: Rect
40
+ var paper: UInt32
41
+ var start: Double
42
+ var path: Path
43
+ }
44
+
45
+ /// Points a second each second, and the seconds to lift a word or put
46
+ /// it down.
47
+ static let gravity = 1400.0
48
+ static let lift = 0.35
49
+
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.
52
+ static func thrown(_ source: Rect, v: Point, spin: Double, rise: Double, after seconds: Double,
53
+ width: Double, height: Double) -> Piece {
54
+ let floor = max(height - source.h - rise, source.y)
55
+ let landing = (-v.y + (v.y * v.y + 2 * gravity * (floor - source.y)).squareRoot()) / gravity
56
+ let s = min(max(seconds, 0), landing)
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))
62
+ // It turns about as fast as `spin`, and lands flat: the right way
63
+ // up or upside down.
64
+ let flat = (spin * landing / .pi).rounded() * .pi
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)
66
+ }
67
+
68
+ /// What it can do to a line.
69
+ enum Act: CaseIterable { case eat, wipe, kick, stomp, carry }
70
+
71
+ /// The same numbers for the same seed, so a play can be told again
72
+ /// and tested (SplitMix64).
73
+ struct Seeded: RandomNumberGenerator {
74
+ var state: UInt64
75
+ init(_ seed: UInt64) { state = seed }
76
+ mutating func next() -> UInt64 {
77
+ state &+= 0x9E37_79B9_7F4A_7C15
78
+ var z = state
79
+ z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
80
+ z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
81
+ return z ^ (z >> 31)
82
+ }
83
+ }
84
+
85
+ /// A play, different for each seed: it jumps onto one of the lines
86
+ /// near the middle of the page and walks it, then does one thing to
87
+ /// each of up to four others, never the same thing twice. Lines too
88
+ /// close to the top to stand on, too short or of one word are left
89
+ /// alone.
90
+ static func script(_ lines: [Line], width: Double, height: Double, seed: UInt64) -> [Step] {
91
+ var rng = Seeded(seed)
92
+ let fit = lines.indices.filter { lines[$0].words.count >= 2 && lines[$0].rect.w >= 120 && lines[$0].rect.y >= top + 40 }
93
+ func far(_ i: Int) -> Double { hypot(lines[i].rect.midX - width / 2, lines[i].rect.midY - height / 2) }
94
+ let near = fit.sorted { far($0) < far($1) }.prefix(8).shuffled(using: &rng)
95
+ guard let first = near.first else { return [] }
96
+ let acts = Act.allCases.shuffled(using: &rng)
97
+ var steps: [Step] = [.jump(line: first), .walk(line: first)]
98
+ // With one line only, it plays on the line it walked.
99
+ let stage = near.count > 1 ? Array(near.dropFirst().prefix(4)) : [first]
100
+ for (k, i) in stage.enumerated() {
101
+ let n = lines[i].words.count, word = Int.random(in: 0..<n, using: &rng)
102
+ switch acts[k] {
103
+ case .eat:
104
+ let from = Int.random(in: 0..<n - 1, using: &rng)
105
+ steps.append(.eat(line: i, words: from..<min(from + 3, n)))
106
+ case .wipe: steps.append(.wipe(line: i))
107
+ case .stomp: steps.append(.stomp(line: i))
108
+ // The walked line is whole: the word goes to its end.
109
+ case .carry where i != first: steps.append(.carry(line: i, word: word, to: first))
110
+ case .kick, .carry: steps.append(.kick(line: i, word: word))
111
+ }
112
+ }
113
+ return steps + [.rest(1.2)]
114
+ }
115
+ }
116
+
117
+ extension Play {
118
+ /// The pieces that have left their place by this frame: each place is
119
+ /// covered, and the piece is drawn where it is now.
120
+ func move(_ f: inout Frame) {
121
+ for cut in cuts where cut.start <= f.t {
122
+ f.gone.append((cover(cut.source), cut.paper))
123
+ switch cut.path {
124
+ case .thrown(let v, let spin, let rise):
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)
128
+ case .held(let until, let to):
129
+ // From its place to above the head, and from there down.
130
+ let head = P.Point(x: f.at.x - cut.source.w / 2, y: f.at.y - P.top - cut.source.h)
131
+ let a = f.t < until ? P.Point(x: cut.source.x, y: cut.source.y) : head, b = f.t < until ? head : to
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))
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))
134
+ }
135
+ }
136
+ }
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
+
148
+ /// Which way it faces during a move, and its pose without the motion.
149
+ func look(_ m: Move) -> (face: Double, pose: M.Pose) {
150
+ let way: Double = m.to.x >= m.from.x ? 1 : -1
151
+ switch m.kind {
152
+ case .jump: return (way, M.Pose(tilt: 6 * way, gy: -2))
153
+ case .walk: return (way, M.Pose(tilt: 7 * way))
154
+ case .eat: return (way, M.Pose(size: 1.12))
155
+ case .wipe: return (way, M.Pose(tilt: 12 * way, gy: 4, lid: 0.3))
156
+ case .kick: return (1, M.Pose(gy: 3, size: 1.08))
157
+ case .stomp: return (0, M.Pose(gy: 5))
158
+ case .fall: return (0, M.Pose(gy: -4, size: 1.22))
159
+ case .lift, .drop: return (1, M.Pose(gy: -5))
160
+ case .carry: return (way, M.Pose(tilt: 6 * way, gy: -5))
161
+ case .rest: return (0, M.Pose())
162
+ }
163
+ }
164
+ }
@@ -2,8 +2,8 @@ import Foundation
2
2
 
3
3
  /// Play mode (#505): the character leaves the bottom edge and plays on a
4
4
  /// frozen picture of the screen. It walks along a line of text as on a
5
- /// floor, jumps to another line, eats words and wipes a line. These are
6
- /// the numbers only: where the words are, what it does and when, where it
5
+ /// floor, jumps to another line, eats words, wipes a line, kicks a word,
6
+ /// stomps a line down and carries a word away. These are the numbers only: where the words are, what it does and when, where it
7
7
  /// is at a given moment and which letters are gone. Foundation only, so
8
8
  /// the tests can run it without a window.
9
9
  ///
@@ -44,6 +44,12 @@ enum PlayMath {
44
44
  case eat(line: Int, words: Range<Int>)
45
45
  /// Wipe a whole line with a cloth.
46
46
  case wipe(line: Int)
47
+ /// Kick a word: its letters fly and pile up at the bottom.
48
+ case kick(line: Int, word: Int)
49
+ /// Hop on a line until its words drop, and fall after them.
50
+ case stomp(line: Int)
51
+ /// Lift a word, carry it to the end of another line, put it down.
52
+ case carry(line: Int, word: Int, to: Int)
47
53
  /// Stand still, facing you, for so many seconds.
48
54
  case rest(Double)
49
55
  }
@@ -76,25 +82,6 @@ enum PlayMath {
76
82
  guard let best = count.max(by: { ($0.value, $0.key) < ($1.value, $1.key) })?.key else { return nil }
77
83
  return samples.first { $0 & 0xF8F8F8 == best }
78
84
  }
79
-
80
- /// The built-in script: the line nearest the middle of the page and
81
- /// the two under it in the same column. It walks the first, eats the
82
- /// start of the second and wipes the third. Lines too close to the top
83
- /// to stand on, too short or of one word are left alone.
84
- static func demo(_ lines: [Line], width: Double, height: Double) -> [Step] {
85
- let fit = lines.indices.filter { lines[$0].words.count >= 2 && lines[$0].rect.w >= 120 && lines[$0].rect.y >= top + 40 }
86
- func far(_ i: Int) -> Double { hypot(lines[i].rect.midX - width / 2, lines[i].rect.midY - height / 2) }
87
- guard let first = fit.min(by: { far($0) < far($1) }) else { return [] }
88
- let a = lines[first].rect
89
- let under = fit.filter { lines[$0].rect.y > a.y + a.h / 2 && lines[$0].rect.x < a.maxX && lines[$0].rect.maxX > a.x }
90
- .sorted { lines[$0].rect.y < lines[$1].rect.y }
91
- let picked = [first] + under.prefix(2)
92
- var steps: [Step] = [.jump(line: first), .walk(line: first)]
93
- if picked.count > 1 { steps += [.eat(line: picked[1], words: 0..<min(3, lines[picked[1]].words.count))] }
94
- else { steps += [.eat(line: first, words: 0..<min(2, lines[first].words.count))] }
95
- if picked.count > 2 { steps += [.wipe(line: picked[2])] }
96
- return steps + [.rest(1.2)]
97
- }
98
85
  }
99
86
 
100
87
  /// A script laid out in time: every move has its start and its end, so
@@ -103,8 +90,9 @@ enum PlayMath {
103
90
  struct Play {
104
91
  typealias P = PlayMath
105
92
  typealias M = CharacterMath
93
+ typealias Mo = PlayMotion
106
94
 
107
- enum Kind { case jump, walk, eat, wipe, rest }
95
+ enum Kind { case jump, walk, eat, wipe, kick, stomp, fall, lift, carry, drop, rest }
108
96
 
109
97
  struct Move {
110
98
  var kind: Kind
@@ -126,28 +114,48 @@ struct Play {
126
114
  var gone: [(rect: P.Rect, paper: UInt32)] = []
127
115
  /// The cloth, while it wipes.
128
116
  var cloth: P.Rect?
117
+ /// What was kicked, dropped or carried, where it is now.
118
+ var pieces: [P.Piece] = []
129
119
  var done = false
130
120
  }
131
121
 
132
122
  let lines: [P.Line]
123
+ /// The picture's width and height: where a thrown piece stops.
124
+ let size: P.Point
133
125
  private(set) var moves: [Move] = []
126
+ private(set) var cuts: [P.Cut] = []
134
127
  var duration: Double { moves.last?.end ?? 0 }
135
128
 
136
129
  /// `start` is where its feet are when play begins. Steps that name a
137
130
  /// line or a word that is not there are skipped.
138
- init(lines: [P.Line], steps: [P.Step], start: P.Point) {
131
+ init(lines: [P.Line], steps: [P.Step], start: P.Point, size: P.Point) {
139
132
  self.lines = lines
133
+ self.size = size
140
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 }
141
138
  func add(_ kind: Kind, to: P.Point, _ seconds: Double, line: Int = 0, words: Range<Int> = 0..<0) -> Move {
142
139
  Move(kind: kind, from: at, to: to, start: now, duration: seconds, line: line, words: words)
143
140
  }
144
141
  /// Get to `to` first: a jump, unless it is already there.
145
142
  func reach(_ to: P.Point, into list: inout [Move]) {
146
143
  guard at != to else { return }
147
- 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)))
148
145
  list.append(m); at = to; now = m.end
149
146
  }
150
- var list: [Move] = []
147
+ var list: [Move] = [], cuts: [P.Cut] = []
148
+ /// A move on the spot.
149
+ func stay(_ kind: Kind, _ seconds: Double, line: Int) -> Move {
150
+ let m = add(kind, to: at, seconds, line: line)
151
+ list.append(m); now = m.end
152
+ return m
153
+ }
154
+ /// A piece falls: each one a little higher on the pile.
155
+ func throwing(_ source: P.Rect, line: Int, at start: Double, v: P.Point, spin: Double) {
156
+ cuts.append(P.Cut(source: source, paper: lines[line].paper, start: start,
157
+ path: .thrown(v: v, spin: spin, rise: Double(cuts.count % 4) * 4)))
158
+ }
151
159
  for step in steps {
152
160
  switch step {
153
161
  case .jump(let i):
@@ -174,12 +182,51 @@ struct Play {
174
182
  reach(P.Point(x: r.x, y: r.y), into: &list)
175
183
  let m = add(.wipe, to: P.Point(x: r.maxX, y: r.y), r.w / P.wipeSpeed, line: i)
176
184
  list.append(m); at = m.to; now = m.end
185
+ case .kick(let i, let w):
186
+ guard lines.indices.contains(i), lines[i].words.indices.contains(w) else { continue }
187
+ let word = lines[i].words[w]
188
+ reach(P.Point(x: word.rect.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
189
+ let m = stay(.kick, 0.6, line: i)
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)),
194
+ spin: 4 + Double(k % 3))
195
+ }
196
+ case .stomp(let i):
197
+ guard lines.indices.contains(i) else { continue }
198
+ let r = lines[i].rect
199
+ reach(P.Point(x: r.midX, y: r.y), into: &list)
200
+ let m = stay(.stomp, 1.2, line: i)
201
+ for (k, word) in lines[i].words.enumerated() {
202
+ let side: Double = k % 2 == 0 ? -1 : 1
203
+ throwing(word.rect, line: i, at: m.start + 0.4 + 0.8 * Double(k) / Double(lines[i].words.count),
204
+ v: P.Point(x: 50 * side, y: -90), spin: 5 * side)
205
+ }
206
+ // Its floor is gone: it falls after the words, slowly
207
+ // enough to follow with the eye.
208
+ let floor = P.Point(x: at.x, y: max(size.y - 6, at.y)), drop = floor.y - at.y
209
+ let fall = add(.fall, to: floor, max((2 * drop / P.gravity).squareRoot(), drop / 600), line: i)
210
+ list.append(fall); at = floor; now = fall.end
211
+ case .carry(let i, let w, let j):
212
+ guard lines.indices.contains(i), lines[i].words.indices.contains(w), lines.indices.contains(j), j != i else { continue }
213
+ let word = lines[i].words[w].rect, home = lines[j].rect
214
+ reach(P.Point(x: word.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
215
+ let up = stay(.lift, P.lift, line: i)
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)
217
+ list.append(m); at = m.to; now = m.end
218
+ let down = stay(.drop, P.lift, line: j)
219
+ // After the line, or on top of its end when the page stops there.
220
+ let after = home.maxX + 8 + word.w <= size.x
221
+ let to = after ? P.Point(x: home.maxX + 8, y: home.midY - word.h / 2) : P.Point(x: home.maxX - word.w, y: home.y - word.h - 2)
222
+ cuts.append(P.Cut(source: word, paper: lines[i].paper, start: up.start, path: .held(until: down.start, to: to)))
177
223
  case .rest(let seconds):
178
224
  let m = add(.rest, to: at, max(seconds, 0))
179
225
  list.append(m); now = m.end
180
226
  }
181
227
  }
182
228
  moves = list
229
+ self.cuts = cuts
183
230
  }
184
231
 
185
232
  func frame(at t: Double) -> Frame {
@@ -187,11 +234,18 @@ struct Play {
187
234
  let m = moves[i], p = M.span(t, m.start, m.end)
188
235
  var f = Frame(t: t, done: t >= duration)
189
236
 
190
- 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)
191
- if m.kind == .jump {
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)
239
+ switch m.kind {
240
+ case .jump, .carry:
192
241
  // An arc over both ends, kept inside the picture.
193
242
  let rise = 30 + hypot(m.to.x - m.from.x, m.to.y - m.from.y) * 0.12
194
- 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
244
+ // Three hops on the spot.
245
+ case .stomp: f.at.y -= min(22, max(m.from.y - P.top, 0)) * abs(sin(3 * .pi * p))
246
+ // Faster and faster.
247
+ case .fall: f.at.y = m.from.y + (m.to.y - m.from.y) * p * p
248
+ default: break
195
249
  }
196
250
 
197
251
  // The look of this move, reached from the look of the one before
@@ -202,15 +256,20 @@ struct Play {
202
256
  f.face = before.face + (now.face - before.face) * into
203
257
  f.pose = M.blend(before.pose, now.pose, eyes: into, body: into, marks: into)
204
258
  switch m.kind {
205
- case .walk: f.pose.lift += 3 * into * abs(sin(2 * .pi * t * 2.2))
206
- case .jump:
207
- let air = sin(.pi * p)
208
- f.pose.sy *= 1 + 0.12 * air; f.pose.sx *= 1 - 0.08 * air
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))
261
+ case .jump, .carry:
262
+ f.pose.sy *= body.sy; f.pose.sx *= body.sx
263
+ f.pose.lift *= 1 - 0.5 * body.dip
209
264
  case .eat:
210
265
  let bite = into * (0.5 + 0.5 * sin(2 * .pi * t / P.perLetter))
211
266
  f.pose.sy *= 1 - 0.08 * bite; f.pose.sx *= 1 + 0.05 * bite
212
267
  case .wipe: f.pose.tilt += 5 * into * sin(2 * .pi * t * 5)
213
- case .rest: break
268
+ // Leans back, then into the kick.
269
+ case .kick: f.pose.tilt -= 14 * sin(2 * .pi * p)
270
+ case .stomp: f.pose.sy *= 1 + into * (0.1 * abs(sin(3 * .pi * p)) - 0.05)
271
+ case .fall: f.pose.sy *= 1 + 0.12 * p; f.pose.sx *= 1 - 0.08 * p
272
+ case .lift, .drop, .rest: break
214
273
  }
215
274
 
216
275
  for done in moves[...i] {
@@ -230,21 +289,10 @@ struct Play {
230
289
  default: break
231
290
  }
232
291
  }
292
+ move(&f)
233
293
  return f
234
294
  }
235
295
 
236
296
  /// A little more than the letters, so nothing of them shows at the edges.
237
- private func cover(_ r: P.Rect) -> P.Rect { P.Rect(x: r.x - 1, y: r.y - 2, w: r.w + 2, h: r.h + 4) }
238
-
239
- /// Which way it faces during a move, and its pose without the motion.
240
- private func look(_ m: Move) -> (face: Double, pose: M.Pose) {
241
- let way: Double = m.to.x >= m.from.x ? 1 : -1
242
- switch m.kind {
243
- case .jump: return (way, M.Pose(tilt: 6 * way, gy: -2))
244
- case .walk: return (way, M.Pose(tilt: 7 * way))
245
- case .eat: return (way, M.Pose(size: 1.12))
246
- case .wipe: return (way, M.Pose(tilt: 12 * way, gy: 4, lid: 0.3))
247
- case .rest: return (0, M.Pose())
248
- }
249
- }
297
+ func cover(_ r: P.Rect) -> P.Rect { P.Rect(x: r.x - 1, y: r.y - 2, w: r.w + 2, h: r.h + 4) }
250
298
  }
@@ -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
+ }