agentix-cli 0.102.2 → 0.103.1
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.
- package/apps/mac-voice/Sources/AgentXVoice/Character.swift +33 -8
- package/apps/mac-voice/Sources/AgentXVoice/CharacterMath.swift +13 -0
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +14 -16
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSimFrame.swift +15 -0
- package/apps/mac-voice/Sources/AgentXVoice/Guide.swift +14 -6
- package/apps/mac-voice/Sources/AgentXVoice/GuideMath.swift +14 -2
- package/apps/mac-voice/Sources/AgentXVoice/IdlePlay.swift +113 -0
- package/apps/mac-voice/Sources/AgentXVoice/PlayActs.swift +67 -14
- package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +13 -17
- package/apps/mac-voice/Sources/AgentXVoice/PlayRead.swift +10 -3
- package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +6 -3
- package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +11 -0
- package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +4 -1
- package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +8 -2
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +23 -11
- package/dist/{App-EYUDDF6K.js → App-A7WZMOGA.js} +2 -2
- package/dist/{ChatApp-F5LZLBI3.js → ChatApp-EWNT36ZE.js} +2 -2
- package/dist/{agent-XF3BWMOB.js → agent-MUGC4RZP.js} +2 -2
- package/dist/{board-dashboard-TVSOWW32.js → board-dashboard-IRMCGCU5.js} +4 -4
- package/dist/{chunk-ZJXDBVBT.js → chunk-25RQQ55U.js} +2 -2
- package/dist/{chunk-YOCQ2MWL.js → chunk-32BPHJ4I.js} +2 -2
- package/dist/{chunk-AYNZAOJN.js → chunk-76KJXOEV.js} +2 -2
- package/dist/{chunk-V6W3FFW7.js → chunk-BEFKOHRC.js} +2 -2
- package/dist/{chunk-QRD3ZG27.js → chunk-BUAVQUHR.js} +2 -2
- package/dist/{chunk-PYCTWI2I.js → chunk-CUS7WGZT.js} +2 -2
- package/dist/{chunk-CDM4G3CW.js → chunk-CW667M47.js} +185 -185
- package/dist/{chunk-CDM4G3CW.js.map → chunk-CW667M47.js.map} +1 -1
- package/dist/{chunk-EXPIKLUN.js → chunk-FZ7VHFL5.js} +2 -2
- package/dist/{chunk-RWSCQMLD.js → chunk-HE4PDESB.js} +2 -2
- package/dist/{chunk-HMDM5MCR.js → chunk-HVPRCHRG.js} +2 -2
- package/dist/{chunk-ECM2QEXF.js → chunk-IA3BC7M3.js} +10 -10
- package/dist/chunk-IA3BC7M3.js.map +1 -0
- package/dist/{chunk-S337S2M7.js → chunk-IOPAWHKW.js} +2 -2
- package/dist/{chunk-Y6POV2X5.js → chunk-MVLNVMWI.js} +2 -2
- package/dist/{chunk-HQJW3BC5.js → chunk-N4IWNCH3.js} +2 -2
- package/dist/{chunk-MWLX3QCD.js → chunk-NWNVHL57.js} +2 -2
- package/dist/{chunk-5UO6BBDG.js → chunk-OLUAEBHJ.js} +2 -2
- package/dist/{chunk-XO55WKCX.js → chunk-Q57GZPDA.js} +2 -2
- package/dist/{chunk-TXFIMVVF.js → chunk-UXTAK5WD.js} +5 -5
- package/dist/{chunk-GSFB46YC.js → chunk-WTFNEPJ7.js} +2 -2
- package/dist/{chunk-EEZI2K7S.js → chunk-XHUOWYNY.js} +2 -2
- package/dist/{chunk-N57ES35J.js → chunk-ZBKKOUNJ.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/{client-526OCGWD.js → client-ZX7AY7H3.js} +2 -2
- package/dist/config-BXOS2PLC.js +2 -0
- package/dist/config-mutator-KRAUD3CA.js +2 -0
- package/dist/{doctor-AC5ONEO2.js → doctor-P3QQP33C.js} +2 -2
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/dist/{program-M5LDV6UQ.js → program-DELG6KQQ.js} +169 -169
- package/dist/program-DELG6KQQ.js.map +1 -0
- package/dist/{promote-CNKIINYK.js → promote-FHX4Q62Z.js} +2 -2
- package/dist/{query-A2N2LIR7.js → query-WBK3YEPV.js} +2 -2
- package/dist/{registry-REKWHOHS.js → registry-NQHC7ZAV.js} +2 -2
- package/dist/{request-planner-MS5BS6HY.js → request-planner-3LA5RAKG.js} +2 -2
- package/dist/{review-candidates-2F4QK6QQ.js → review-candidates-QV66TOCL.js} +2 -2
- package/dist/{routing-SU5Q4IPM.js → routing-HF4XVD4Y.js} +2 -2
- package/dist/schedule-tool-V66FX5XL.js +2 -0
- package/dist/scheduler-XVMWASQR.js +2 -0
- package/dist/{seat-PZBFMYQL.js → seat-NQTZUIKH.js} +2 -2
- package/dist/{select-D3XOFXA5.js → select-3GVC5RDA.js} +2 -2
- package/dist/{set-enabled-DNDRQ3CZ.js → set-enabled-SXCE2MZX.js} +2 -2
- package/dist/{tool-FB6SPVIS.js → tool-23GOQNUW.js} +2 -2
- package/dist/{tool-RRGOHJW6.js → tool-E6JBENAN.js} +2 -2
- package/dist/wiki-VM2RTHVF.js +2 -0
- package/package.json +1 -1
- package/dist/chunk-ECM2QEXF.js.map +0 -1
- package/dist/config-HCJ5NZW5.js +0 -2
- package/dist/config-mutator-PGR2OGWC.js +0 -2
- package/dist/program-M5LDV6UQ.js.map +0 -1
- package/dist/schedule-tool-24KHZY2Z.js +0 -2
- package/dist/scheduler-ALDZOUKJ.js +0 -2
- package/dist/wiki-3DH2QYTX.js +0 -2
- /package/dist/{App-EYUDDF6K.js.map → App-A7WZMOGA.js.map} +0 -0
- /package/dist/{ChatApp-F5LZLBI3.js.map → ChatApp-EWNT36ZE.js.map} +0 -0
- /package/dist/{agent-XF3BWMOB.js.map → agent-MUGC4RZP.js.map} +0 -0
- /package/dist/{board-dashboard-TVSOWW32.js.map → board-dashboard-IRMCGCU5.js.map} +0 -0
- /package/dist/{chunk-ZJXDBVBT.js.map → chunk-25RQQ55U.js.map} +0 -0
- /package/dist/{chunk-YOCQ2MWL.js.map → chunk-32BPHJ4I.js.map} +0 -0
- /package/dist/{chunk-AYNZAOJN.js.map → chunk-76KJXOEV.js.map} +0 -0
- /package/dist/{chunk-V6W3FFW7.js.map → chunk-BEFKOHRC.js.map} +0 -0
- /package/dist/{chunk-QRD3ZG27.js.map → chunk-BUAVQUHR.js.map} +0 -0
- /package/dist/{chunk-PYCTWI2I.js.map → chunk-CUS7WGZT.js.map} +0 -0
- /package/dist/{chunk-EXPIKLUN.js.map → chunk-FZ7VHFL5.js.map} +0 -0
- /package/dist/{chunk-RWSCQMLD.js.map → chunk-HE4PDESB.js.map} +0 -0
- /package/dist/{chunk-HMDM5MCR.js.map → chunk-HVPRCHRG.js.map} +0 -0
- /package/dist/{chunk-S337S2M7.js.map → chunk-IOPAWHKW.js.map} +0 -0
- /package/dist/{chunk-Y6POV2X5.js.map → chunk-MVLNVMWI.js.map} +0 -0
- /package/dist/{chunk-HQJW3BC5.js.map → chunk-N4IWNCH3.js.map} +0 -0
- /package/dist/{chunk-MWLX3QCD.js.map → chunk-NWNVHL57.js.map} +0 -0
- /package/dist/{chunk-5UO6BBDG.js.map → chunk-OLUAEBHJ.js.map} +0 -0
- /package/dist/{chunk-XO55WKCX.js.map → chunk-Q57GZPDA.js.map} +0 -0
- /package/dist/{chunk-TXFIMVVF.js.map → chunk-UXTAK5WD.js.map} +0 -0
- /package/dist/{chunk-GSFB46YC.js.map → chunk-WTFNEPJ7.js.map} +0 -0
- /package/dist/{chunk-EEZI2K7S.js.map → chunk-XHUOWYNY.js.map} +0 -0
- /package/dist/{chunk-N57ES35J.js.map → chunk-ZBKKOUNJ.js.map} +0 -0
- /package/dist/{client-526OCGWD.js.map → client-ZX7AY7H3.js.map} +0 -0
- /package/dist/{config-HCJ5NZW5.js.map → config-BXOS2PLC.js.map} +0 -0
- /package/dist/{config-mutator-PGR2OGWC.js.map → config-mutator-KRAUD3CA.js.map} +0 -0
- /package/dist/{doctor-AC5ONEO2.js.map → doctor-P3QQP33C.js.map} +0 -0
- /package/dist/{promote-CNKIINYK.js.map → promote-FHX4Q62Z.js.map} +0 -0
- /package/dist/{query-A2N2LIR7.js.map → query-WBK3YEPV.js.map} +0 -0
- /package/dist/{registry-REKWHOHS.js.map → registry-NQHC7ZAV.js.map} +0 -0
- /package/dist/{request-planner-MS5BS6HY.js.map → request-planner-3LA5RAKG.js.map} +0 -0
- /package/dist/{review-candidates-2F4QK6QQ.js.map → review-candidates-QV66TOCL.js.map} +0 -0
- /package/dist/{routing-SU5Q4IPM.js.map → routing-HF4XVD4Y.js.map} +0 -0
- /package/dist/{schedule-tool-24KHZY2Z.js.map → schedule-tool-V66FX5XL.js.map} +0 -0
- /package/dist/{scheduler-ALDZOUKJ.js.map → scheduler-XVMWASQR.js.map} +0 -0
- /package/dist/{seat-PZBFMYQL.js.map → seat-NQTZUIKH.js.map} +0 -0
- /package/dist/{select-D3XOFXA5.js.map → select-3GVC5RDA.js.map} +0 -0
- /package/dist/{set-enabled-DNDRQ3CZ.js.map → set-enabled-SXCE2MZX.js.map} +0 -0
- /package/dist/{tool-FB6SPVIS.js.map → tool-23GOQNUW.js.map} +0 -0
- /package/dist/{tool-RRGOHJW6.js.map → tool-E6JBENAN.js.map} +0 -0
- /package/dist/{wiki-3DH2QYTX.js.map → wiki-VM2RTHVF.js.map} +0 -0
|
@@ -32,6 +32,8 @@ final class CharacterHost {
|
|
|
32
32
|
private var shown = false
|
|
33
33
|
/// Hidden with its bubble, until the talk key or the menu (#502).
|
|
34
34
|
private var hidden = false
|
|
35
|
+
/// On the page, playing (#580): its window is out until it is back.
|
|
36
|
+
private var away = false
|
|
35
37
|
private var animated = true
|
|
36
38
|
private var reduceMotion = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
|
|
37
39
|
private var activity = CharacterMath.Activity.idle
|
|
@@ -64,6 +66,9 @@ final class CharacterHost {
|
|
|
64
66
|
/// (#505) takes it from there.
|
|
65
67
|
var stops: [NSColor] { view.stops }
|
|
66
68
|
var foot: NSPoint { NSPoint(x: CGFloat(sim.x), y: rest.y) }
|
|
69
|
+
/// Its windows, by number: its own and its mark's. The picture of a
|
|
70
|
+
/// play leaves them out (#583).
|
|
71
|
+
var windows: [Int] { [window.windowNumber, guide.window] }
|
|
67
72
|
/// Carried by the pointer: where the pointer took hold, and where it
|
|
68
73
|
/// stood then, which is not where it rests once it has stepped aside.
|
|
69
74
|
private var carried: (from: NSPoint, rest: CGPoint)?
|
|
@@ -72,12 +77,14 @@ final class CharacterHost {
|
|
|
72
77
|
/// Sent somewhere, and what its bubble says there (#562).
|
|
73
78
|
var sent: Bool { guide.showing != nil }
|
|
74
79
|
var caption: String? { guide.caption }
|
|
80
|
+
/// The state it was asked to show by name (#570), there or where it rests.
|
|
81
|
+
private var asked: CharacterMath.Mood?
|
|
75
82
|
/// Told when it is sent somewhere, to its next stop or back.
|
|
76
83
|
var onGuide: (() -> Void)?
|
|
77
84
|
/// A still picture: Reduce Motion, or "Animated orb" off.
|
|
78
85
|
private var still: Bool { !animated || reduceMotion }
|
|
79
86
|
/// The look is the character: whether it is on screen. Nil with the orb.
|
|
80
|
-
var onScreen: Bool? { shown ? !hidden : nil }
|
|
87
|
+
var onScreen: Bool? { shown ? !hidden && !away : nil }
|
|
81
88
|
/// Told when it comes on screen or leaves it: shown or hidden, or the look changed.
|
|
82
89
|
var onScreenChanged: (() -> Void)?
|
|
83
90
|
|
|
@@ -117,6 +124,14 @@ final class CharacterHost {
|
|
|
117
124
|
onScreenChanged?()
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
/// Out for a play on the page, and back as it left, where it left.
|
|
128
|
+
func setAway(_ on: Bool) {
|
|
129
|
+
guard away != on else { return }
|
|
130
|
+
away = on
|
|
131
|
+
run()
|
|
132
|
+
onScreenChanged?()
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
/// Hide it, as its bubble is hidden: gone until it is asked back.
|
|
121
136
|
func setHidden(_ on: Bool) {
|
|
122
137
|
guard hidden != on else { return }
|
|
@@ -136,6 +151,10 @@ final class CharacterHost {
|
|
|
136
151
|
|
|
137
152
|
/// `voice.stroll`: a slow walk now and then while it has nothing to do.
|
|
138
153
|
var strolls = false
|
|
154
|
+
/// `voice.animations` as the wait between two small animations by
|
|
155
|
+
/// itself (`IdlePlay.gap`), and AgentX's own hold, which stops them.
|
|
156
|
+
var animates = 0.0
|
|
157
|
+
private var quiet = false
|
|
139
158
|
|
|
140
159
|
/// "Animated orb" in the menu.
|
|
141
160
|
func setAnimated(_ on: Bool) {
|
|
@@ -151,9 +170,11 @@ final class CharacterHost {
|
|
|
151
170
|
redraw()
|
|
152
171
|
}
|
|
153
172
|
|
|
154
|
-
/// Go beside `rect` and mark it, or with nil go back to where it
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
/// Go beside `rect` and mark it, or with nil go back to where it
|
|
174
|
+
/// rests; in the state `asked`, or with nil its real one.
|
|
175
|
+
func guide(to rect: NSRect?, mark kind: GuideMath.Mark = .none, caption: String? = nil, asked mood: CharacterMath.Mood? = nil) {
|
|
176
|
+
asked = shown && !hidden && !away ? mood : nil
|
|
177
|
+
if shown, !hidden, !away, carried == nil, let rect {
|
|
157
178
|
guide.show(rect, kind, caption: caption, color: view.stops[2], animated: !still)
|
|
158
179
|
} else {
|
|
159
180
|
guide.end()
|
|
@@ -172,7 +193,7 @@ final class CharacterHost {
|
|
|
172
193
|
private func run() {
|
|
173
194
|
timer?.invalidate()
|
|
174
195
|
timer = nil
|
|
175
|
-
guard shown, !hidden else { carried = nil; guide(to: nil); window.orderOut(nil); return }
|
|
196
|
+
guard shown, !hidden, !away else { carried = nil; guide(to: nil); window.orderOut(nil); return }
|
|
176
197
|
tick()
|
|
177
198
|
window.orderFrontRegardless()
|
|
178
199
|
// Still, it only watches for the hand that moves it.
|
|
@@ -239,10 +260,13 @@ final class CharacterHost {
|
|
|
239
260
|
|
|
240
261
|
let frame: CharacterSim.Frame
|
|
241
262
|
if still {
|
|
242
|
-
frame = CharacterSim.still(activity, home: home)
|
|
263
|
+
frame = CharacterSim.still(activity, asked: asked, sent: guide.showing != nil, home: home)
|
|
243
264
|
} else {
|
|
244
265
|
if activity == .listening, let read = levelSource {
|
|
245
266
|
level = OrbMath.smooth(level, toward: OrbMath.level(fromRMS: read()))
|
|
267
|
+
} else if asked == .listening {
|
|
268
|
+
// Asked to listen, there is no microphone to follow: a slow swell.
|
|
269
|
+
level = 0.45 + 0.3 * sin(now * 5)
|
|
246
270
|
} else {
|
|
247
271
|
level = 0
|
|
248
272
|
}
|
|
@@ -259,11 +283,12 @@ final class CharacterHost {
|
|
|
259
283
|
edgesRead = now
|
|
260
284
|
// The window list counts down from the top of the main screen.
|
|
261
285
|
let top = first.frame.maxY - rest.y
|
|
286
|
+
quiet = animates > 0 && Hold.isOn
|
|
262
287
|
edges = strolls && Config.playMode ? Meets.edges(of: Meets.windows(), band: (top - Self.head)...top) : []
|
|
263
288
|
}
|
|
264
289
|
frame = sim.step(to: now,
|
|
265
|
-
CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held, sent: guide.showing != nil, strolls: strolls,
|
|
266
|
-
edges: edges, shows: bubbleShows?() ?? false, plays: Config.playMode, down: NSEvent.pressedMouseButtons & 1 != 0,
|
|
290
|
+
CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held, sent: guide.showing != nil, asked: asked, strolls: strolls,
|
|
291
|
+
edges: edges, shows: bubbleShows?() ?? false, plays: Config.playMode, animates: quiet ? 0 : animates, down: NSEvent.pressedMouseButtons & 1 != 0,
|
|
267
292
|
home: home, range: Double(spot.ends.lowerBound)...Double(spot.ends.upperBound)))
|
|
268
293
|
}
|
|
269
294
|
let origin = NSPoint(x: (CGFloat(frame.x) - Self.size.width / 2).rounded(), y: (rest.y - Self.ground).rounded())
|
|
@@ -31,6 +31,19 @@ enum CharacterMath {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/// The state shown when one was asked for by name, or it was sent to
|
|
35
|
+
/// show something (#570). Someone talking, a ringing call and a call
|
|
36
|
+
/// between turns always show: the real turn comes first. Otherwise
|
|
37
|
+
/// the state asked for; and sent somewhere with none asked for, it
|
|
38
|
+
/// does not take the busy look, which would hide the move.
|
|
39
|
+
static func mood(for activity: Activity, asked: Mood?, sent: Bool) -> Mood {
|
|
40
|
+
switch activity {
|
|
41
|
+
case .listening, .ringing, .waiting: return mood(for: activity)
|
|
42
|
+
case .thinking: return asked ?? (sent ? .idle : .working)
|
|
43
|
+
case .idle, .speaking: return asked ?? mood(for: activity)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
/// The state it goes through on the way from `from` to `to`, if any:
|
|
35
48
|
/// it notices you before it listens and when it wakes, and nods once
|
|
36
49
|
/// it has heard you.
|
|
@@ -44,6 +44,8 @@ struct CharacterSim {
|
|
|
44
44
|
private var met: Double?
|
|
45
45
|
/// Its games with the pointer, and the crouch and hop they give it.
|
|
46
46
|
private(set) var play = PointerPlay()
|
|
47
|
+
/// Its small animations by itself (#571).
|
|
48
|
+
private(set) var idle = IdlePlay()
|
|
47
49
|
private var crouch = 0.0, hop = 0.0
|
|
48
50
|
private var face = 0.0, lean = 0.0
|
|
49
51
|
private var glance = (x: 0.0, y: 0.0)
|
|
@@ -117,8 +119,8 @@ struct CharacterSim {
|
|
|
117
119
|
|
|
118
120
|
// Which state. A change starts from the pose it has now, so a
|
|
119
121
|
// change in the middle of another is still one smooth move.
|
|
120
|
-
if input.activity != .idle { restSince = now }
|
|
121
|
-
var want = M.mood(for: input.activity)
|
|
122
|
+
if input.activity != .idle || input.asked != nil { restSince = now }
|
|
123
|
+
var want = M.mood(for: input.activity, asked: input.asked, sent: input.sent)
|
|
122
124
|
if want == .idle && now - restSince >= Self.dozeAfter { want = .dozing }
|
|
123
125
|
if want != goal {
|
|
124
126
|
goal = want
|
|
@@ -130,6 +132,10 @@ struct CharacterSim {
|
|
|
130
132
|
}
|
|
131
133
|
if let at = pending, now >= at { pending = nil; go(goal, now) }
|
|
132
134
|
var pose = change.pose(at: now)
|
|
135
|
+
// Idle, left alone and standing: a small animation now and then.
|
|
136
|
+
let alone = mood == .idle && input.activity == .idle && input.asked == nil && !input.sent && !input.held && !input.shows
|
|
137
|
+
&& !near && game == PointerPlay.Out() && play.game == nil && abs(speed) < 40 && strolled == 0 && strollTo == 0
|
|
138
|
+
let own = idle.step(now, dt, free: alone, gap: input.animates, dozeIn: restSince + Self.dozeAfter - now)
|
|
133
139
|
|
|
134
140
|
// Along the edge: a spring a little short of critical, so it
|
|
135
141
|
// arrives with a small overshoot.
|
|
@@ -144,11 +150,11 @@ struct CharacterSim {
|
|
|
144
150
|
let stopped = strollTo != 0 && abs(strollTo - strolled) < 0.5 ? met : nil
|
|
145
151
|
let turned = stopped.map { $0 > x ? 0.7 : -0.7 } ?? 0
|
|
146
152
|
toward(&face, min(max(speed / 200 + turned, -1), 1), 10)
|
|
147
|
-
toward(&lean, min(max(speed / 480, -1), 1) * 13, 8)
|
|
153
|
+
toward(&lean, min(max(speed / 480, -1), 1) * 13 + own.tilt, 8)
|
|
148
154
|
// A game's crouch and hop are followed, not taken at once, so a
|
|
149
155
|
// game cut short by work does not snap.
|
|
150
|
-
toward(&crouch, game.crouch, 20)
|
|
151
|
-
toward(&hop, game.hop, 20)
|
|
156
|
+
toward(&crouch, game.crouch + own.crouch, 20)
|
|
157
|
+
toward(&hop, game.hop + own.hop, 20)
|
|
152
158
|
pose.sx *= 1 + 0.14 * crouch
|
|
153
159
|
pose.sy *= 1 - 0.22 * crouch
|
|
154
160
|
pose.lift += hop - 8 * crouch
|
|
@@ -180,8 +186,8 @@ struct CharacterSim {
|
|
|
180
186
|
look = (dx / far * pull, dy / far * pull)
|
|
181
187
|
}
|
|
182
188
|
if stopped != nil { look = (turned * 5, -2) }
|
|
183
|
-
toward(&glance.x, look.x + face * 2, 9)
|
|
184
|
-
toward(&glance.y, look.y, 9)
|
|
189
|
+
toward(&glance.x, look.x + face * 2 + own.gx, 9)
|
|
190
|
+
toward(&glance.y, look.y + own.gy, 9)
|
|
185
191
|
|
|
186
192
|
// What keeps it alive: breath, bob, blinks, and the voice.
|
|
187
193
|
let t = now - (began ?? now)
|
|
@@ -201,6 +207,7 @@ struct CharacterSim {
|
|
|
201
207
|
nextBlink = now + 2.2 + 4.5 * M.rnd(blinks) // uneven, never the same loop twice
|
|
202
208
|
}
|
|
203
209
|
if now >= nextBlink { pose.open *= 1 - sin(.pi * (now - nextBlink) / Self.blink) }
|
|
210
|
+
pose.open *= 1 - own.shut
|
|
204
211
|
|
|
205
212
|
return Frame(pose: pose, x: x, face: face, level: input.level, voice: voice, t: t,
|
|
206
213
|
dots: dots.compactMap { dot(at: now, $0) }, stars: bursts.flatMap { stars(at: now, $0) })
|
|
@@ -248,15 +255,6 @@ struct CharacterSim {
|
|
|
248
255
|
strolled = 0; strollTo = 0; strollNext = nil
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
/// The state alone, where it rests: for Reduce Motion, which shows a
|
|
252
|
-
/// still picture that changes between states and never a loop.
|
|
253
|
-
static func still(_ activity: M.Activity, home: Double) -> Frame {
|
|
254
|
-
// Its marks at full strength, so each state reads without motion:
|
|
255
|
-
// the arcs of its voice only while it speaks.
|
|
256
|
-
let pose = M.pose(M.mood(for: activity))
|
|
257
|
-
return Frame(pose: pose, x: home, level: 1, voice: pose.speak)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
258
|
private mutating func go(_ to: M.Mood, _ now: Double) {
|
|
261
259
|
change = M.Transition(from: change.pose(at: now), to: M.pose(to), start: now, duration: M.duration(into: to))
|
|
262
260
|
mood = to
|
|
@@ -15,6 +15,9 @@ extension CharacterSim {
|
|
|
15
15
|
/// The answering agent sent it to show something (#482): it goes
|
|
16
16
|
/// to `home` at once, awake, whatever the pointer did before.
|
|
17
17
|
var sent = false
|
|
18
|
+
/// The state it was asked to show by name (#570), in place of
|
|
19
|
+
/// what the assistant is doing.
|
|
20
|
+
var asked: M.Mood?
|
|
18
21
|
/// `voice.stroll`: with nothing to do, it takes a slow stroll
|
|
19
22
|
/// beside where it rests now and then (#482).
|
|
20
23
|
var strolls = false
|
|
@@ -26,6 +29,9 @@ extension CharacterSim {
|
|
|
26
29
|
var shows = false
|
|
27
30
|
/// "Play mode" is ticked: idle, it plays with the pointer (#505).
|
|
28
31
|
var plays = false
|
|
32
|
+
/// `voice.animations`: the shortest wait between two small
|
|
33
|
+
/// animations by itself, in seconds (#571). 0: none.
|
|
34
|
+
var animates = 0.0
|
|
29
35
|
/// The mouse button is down.
|
|
30
36
|
var down = false
|
|
31
37
|
/// Where it rests, and how far it may go.
|
|
@@ -56,4 +62,13 @@ extension CharacterSim {
|
|
|
56
62
|
var dots: [Mark] = []
|
|
57
63
|
var stars: [Mark] = []
|
|
58
64
|
}
|
|
65
|
+
|
|
66
|
+
/// The state alone, where it rests: for Reduce Motion, which shows a
|
|
67
|
+
/// still picture that changes between states and never a loop.
|
|
68
|
+
static func still(_ activity: M.Activity, asked: M.Mood? = nil, sent: Bool = false, home: Double) -> Frame {
|
|
69
|
+
// Its marks at full strength, so each state reads without motion:
|
|
70
|
+
// the arcs of its voice only while it speaks.
|
|
71
|
+
let pose = M.pose(M.mood(for: activity, asked: asked, sent: sent))
|
|
72
|
+
return Frame(pose: pose, x: home, level: 1, voice: pose.speak)
|
|
73
|
+
}
|
|
59
74
|
}
|
|
@@ -13,6 +13,9 @@ enum GuideClient {
|
|
|
13
13
|
/// What its bubble says at the stop (#562). Nil: no bubble there,
|
|
14
14
|
/// and a daemon too old to send one.
|
|
15
15
|
let text: String?
|
|
16
|
+
/// The state it shows meanwhile (#570). Nil: its real state, and
|
|
17
|
+
/// a daemon too old to send one.
|
|
18
|
+
let expression: String?
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/// GET /voice/guide: the command after `seq`, as soon as there is
|
|
@@ -37,8 +40,9 @@ final class GuideWatcher {
|
|
|
37
40
|
private var task: Task<Void, Never>?
|
|
38
41
|
/// The character is on screen.
|
|
39
42
|
var wanted: (() -> Bool)?
|
|
40
|
-
/// Show this, in AppKit coordinates, with this caption
|
|
41
|
-
|
|
43
|
+
/// Show this, in AppKit coordinates, with this caption and in this
|
|
44
|
+
/// state, or go home.
|
|
45
|
+
var onCommand: ((NSRect?, GuideMath.Mark, String?, CharacterMath.Mood?) -> Void)?
|
|
42
46
|
|
|
43
47
|
/// (Re)start the wait. Called again when the character comes on screen
|
|
44
48
|
/// or leaves it, so a wait already open is ended: cancelling it closes
|
|
@@ -56,7 +60,7 @@ final class GuideWatcher {
|
|
|
56
60
|
private func turn() async {
|
|
57
61
|
guard wanted?() == true, let command = await GuideClient.next(after: seq) else {
|
|
58
62
|
// Hidden, or the daemon is gone: nothing is left on screen.
|
|
59
|
-
if away { away = false; onCommand?(nil, .none, nil) }
|
|
63
|
+
if away { away = false; onCommand?(nil, .none, nil, nil) }
|
|
60
64
|
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
|
61
65
|
return
|
|
62
66
|
}
|
|
@@ -66,9 +70,10 @@ final class GuideWatcher {
|
|
|
66
70
|
GuideMath.toAppKit(CGRect(x: $0.x, y: $0.y, width: $0.width, height: $0.height),
|
|
67
71
|
primaryHeight: NSScreen.screens.first?.frame.maxY ?? 0)
|
|
68
72
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
let asked = command.expression.flatMap { CharacterMath.Mood(rawValue: $0) }
|
|
74
|
+
away = rect != nil || asked != nil
|
|
75
|
+
Log.info("guide: \(rect.map { "to \(Int($0.minX)),\(Int($0.minY)) \(Int($0.width))x\(Int($0.height)) \(command.mark)" } ?? "home")\(asked.map { ", \($0.rawValue)" } ?? "")")
|
|
76
|
+
onCommand?(rect, GuideMath.Mark(rawValue: command.mark) ?? .box, rect == nil ? nil : command.text, asked)
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -80,6 +85,8 @@ final class CharacterGuide {
|
|
|
80
85
|
/// What its bubble says there (#562).
|
|
81
86
|
private(set) var caption: String?
|
|
82
87
|
private let mark = GuideMark()
|
|
88
|
+
/// The number of its mark's window.
|
|
89
|
+
var window: Int { mark.number }
|
|
83
90
|
/// On its way up or down, to what it shows or back; and the last frame.
|
|
84
91
|
private var gliding = false
|
|
85
92
|
private var ticked = 0.0
|
|
@@ -125,6 +132,7 @@ final class GuideMark {
|
|
|
125
132
|
private static let margin: CGFloat = 6
|
|
126
133
|
private let window: NSPanel
|
|
127
134
|
private let shape = CAShapeLayer()
|
|
135
|
+
var number: Int { window.windowNumber }
|
|
128
136
|
|
|
129
137
|
init() {
|
|
130
138
|
window = NSPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
|
|
@@ -46,9 +46,21 @@ enum GuideMath {
|
|
|
46
46
|
/// At a stop the bubble says the command's caption, and with none
|
|
47
47
|
/// there is no bubble; the idle hint is for where it rests, not for
|
|
48
48
|
/// a stop, the way there or a play. `away`: sent somewhere, or playing.
|
|
49
|
-
|
|
49
|
+
/// `hint`: the hint may show (hintShows).
|
|
50
|
+
static func idle(caption: String?, away: Bool, hint: Bool = true) -> Idle {
|
|
50
51
|
if let caption, !caption.isEmpty { return .caption(caption) }
|
|
51
|
-
return away ? .nothing : .hint
|
|
52
|
+
return away || !hint ? .nothing : .hint
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Seconds after the app starts that the character's bubble says the hint.
|
|
56
|
+
static let hintSeconds = 6.0
|
|
57
|
+
|
|
58
|
+
/// Whether the idle hint may show (#576). A pill of its own always
|
|
59
|
+
/// says it. The character's bubble says it only for the first seconds
|
|
60
|
+
/// after the app starts, then there is no bubble at idle; "notifications
|
|
61
|
+
/// held" stays, since nothing else on screen says it.
|
|
62
|
+
static func hintShows(character: Bool, sinceStart: Double, held: Bool) -> Bool {
|
|
63
|
+
!character || held || sinceStart < hintSeconds
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/// What a label needs beyond the width of its words (#569): a text
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Small animations by itself (#571): idle and left alone, the character
|
|
4
|
+
/// now and then looks around, hops or sways, just for the fun of it, and
|
|
5
|
+
/// stretches with a yawn before it dozes. `voice.animations` says how
|
|
6
|
+
/// often, or not at all.
|
|
7
|
+
///
|
|
8
|
+
/// It only decides what an animation adds to the pose; `CharacterSim`
|
|
9
|
+
/// follows it, so one cut short by work does not snap. Foundation only,
|
|
10
|
+
/// and the same animations in the same order on every run, so the tests
|
|
11
|
+
/// can count on them.
|
|
12
|
+
struct IdlePlay {
|
|
13
|
+
typealias M = CharacterMath
|
|
14
|
+
|
|
15
|
+
/// The animations, by name. The yawn comes before it dozes; the
|
|
16
|
+
/// others are picked now and then.
|
|
17
|
+
enum Act: String, CaseIterable { case look, hop, sway, yawn }
|
|
18
|
+
|
|
19
|
+
/// `voice.animations` as the shortest wait between two, in seconds.
|
|
20
|
+
/// The wait is up to twice that. 0: none.
|
|
21
|
+
static func gap(_ often: String?) -> Double {
|
|
22
|
+
switch often {
|
|
23
|
+
case "rarely": return 60
|
|
24
|
+
case "sometimes": return 25
|
|
25
|
+
case "often": return 10
|
|
26
|
+
default: return 0
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Seconds an animation takes.
|
|
31
|
+
static func length(_ act: Act) -> Double {
|
|
32
|
+
switch act {
|
|
33
|
+
case .look, .yawn: return 2.6
|
|
34
|
+
case .hop: return 0.9
|
|
35
|
+
case .sway: return 1.8
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// This close to dozing, in seconds, no animation starts but the yawn.
|
|
40
|
+
static let calm = 6.0
|
|
41
|
+
|
|
42
|
+
/// What an animation adds to the pose: gaze and height in drawing
|
|
43
|
+
/// units, lean in degrees, 0…1 crouched (below 0: stretched tall) and
|
|
44
|
+
/// 0…1 how far the eyes are shut.
|
|
45
|
+
struct Out: Equatable {
|
|
46
|
+
var gx = 0.0, gy = 0.0, tilt = 0.0, hop = 0.0, crouch = 0.0, shut = 0.0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private(set) var act: Act?
|
|
50
|
+
private(set) var played = 0
|
|
51
|
+
private var began = 0.0
|
|
52
|
+
private var next: Double?
|
|
53
|
+
private var yawned = false
|
|
54
|
+
private var turn = 0
|
|
55
|
+
private var shut = 0.0
|
|
56
|
+
|
|
57
|
+
/// One frame. `free`: idle, left alone and standing where it is.
|
|
58
|
+
/// `dozeIn`: seconds until it dozes.
|
|
59
|
+
mutating func step(_ now: Double, _ dt: Double, free: Bool, gap: Double, dozeIn: Double) -> Out {
|
|
60
|
+
var out = Out()
|
|
61
|
+
if !free || gap <= 0 {
|
|
62
|
+
act = nil; next = nil
|
|
63
|
+
} else {
|
|
64
|
+
if dozeIn > Self.calm { yawned = false }
|
|
65
|
+
if let on = act, now - began >= Self.length(on) { act = nil; next = nil }
|
|
66
|
+
if act == nil {
|
|
67
|
+
if !yawned && dozeIn <= Self.length(.yawn) {
|
|
68
|
+
yawned = true
|
|
69
|
+
start(.yawn, now)
|
|
70
|
+
} else if next == nil {
|
|
71
|
+
next = now + gap * (1 + M.rnd(played, 5))
|
|
72
|
+
} else if now >= next!, dozeIn > Self.calm {
|
|
73
|
+
// One of the other two, so the same never comes twice.
|
|
74
|
+
turn = (turn + 1 + min(Int(M.rnd(played, 6) * 2), 1)) % 3
|
|
75
|
+
start([Act.look, .hop, .sway][turn], now)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if let on = act { out = Self.shape(on, (now - began) / Self.length(on)) }
|
|
79
|
+
}
|
|
80
|
+
// The eyes are followed here; the rest by the sim.
|
|
81
|
+
shut += (out.shut - shut) * (1 - exp(-dt * 20))
|
|
82
|
+
out.shut = shut
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Play `act` now, whatever the wait.
|
|
87
|
+
mutating func start(_ act: Act, _ now: Double) {
|
|
88
|
+
self.act = act
|
|
89
|
+
began = now
|
|
90
|
+
played += 1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// An animation `p` of the way through, 0…1. Each starts and ends at
|
|
94
|
+
/// nothing, so it joins the resting pose at both ends.
|
|
95
|
+
static func shape(_ act: Act, _ p: Double) -> Out {
|
|
96
|
+
switch act {
|
|
97
|
+
case .look:
|
|
98
|
+
// To one side, to the other, and back.
|
|
99
|
+
return Out(gx: 8 * sin(2 * .pi * p), gy: -1.5 * sin(.pi * p))
|
|
100
|
+
case .hop:
|
|
101
|
+
// A short crouch, then up and down again.
|
|
102
|
+
return Out(hop: 26 * sin(.pi * M.span(p, 0.3, 1)),
|
|
103
|
+
crouch: 0.7 * M.ease(p / 0.3) * (1 - M.ease(M.span(p, 0.3, 0.5))))
|
|
104
|
+
case .sway:
|
|
105
|
+
// Two swings from side to side, the widest in the middle.
|
|
106
|
+
return Out(tilt: 10 * sin(4 * .pi * p) * sin(.pi * p))
|
|
107
|
+
case .yawn:
|
|
108
|
+
// It stretches tall, looks up, and shuts its eyes a moment.
|
|
109
|
+
return Out(gy: -2 * sin(.pi * p), crouch: -0.8 * sin(.pi * M.span(p, 0, 0.8)),
|
|
110
|
+
shut: M.ease(M.span(p, 0.15, 0.4)) * (1 - M.ease(M.span(p, 0.75, 1))))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -65,6 +65,22 @@ extension PlayMath {
|
|
|
65
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
66
|
}
|
|
67
67
|
|
|
68
|
+
/// A word's letters: the word's rectangle cut into equal parts, one a
|
|
69
|
+
/// character. An estimate, close enough to take them one at a time.
|
|
70
|
+
static func letters(of word: Word) -> [Rect] {
|
|
71
|
+
let n = max(word.text.count, 1), w = word.rect.w / Double(n)
|
|
72
|
+
return (0..<n).map { Rect(x: word.rect.x + Double($0) * w, y: word.rect.y, w: w, h: word.rect.h) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// The colour most of `samples` share: the page around a line. Close
|
|
76
|
+
/// shades count as one, so the soft edge of a letter does not win.
|
|
77
|
+
static func common(_ samples: [UInt32]) -> UInt32? {
|
|
78
|
+
var count: [UInt32: Int] = [:]
|
|
79
|
+
for s in samples { count[s & 0xF8F8F8, default: 0] += 1 }
|
|
80
|
+
guard let best = count.max(by: { ($0.value, $0.key) < ($1.value, $1.key) })?.key else { return nil }
|
|
81
|
+
return samples.first { $0 & 0xF8F8F8 == best }
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
/// What it can do to a line.
|
|
69
85
|
enum Act: CaseIterable { case eat, wipe, kick, stomp, carry }
|
|
70
86
|
|
|
@@ -82,24 +98,31 @@ extension PlayMath {
|
|
|
82
98
|
}
|
|
83
99
|
}
|
|
84
100
|
|
|
85
|
-
/// A play, different for each seed
|
|
86
|
-
///
|
|
87
|
-
///
|
|
88
|
-
///
|
|
89
|
-
///
|
|
101
|
+
/// A play, different for each page and each seed. It jumps onto the
|
|
102
|
+
/// page's heading, the line clearly taller than the others, or with
|
|
103
|
+
/// no heading onto one of the lines near the middle, and walks it.
|
|
104
|
+
/// Then it does one thing to each of up to four others, never the same
|
|
105
|
+
/// thing twice, and each thing to the line that suits it (`suits`).
|
|
106
|
+
/// Then it goes home. Lines too close to the top to stand on, too
|
|
107
|
+
/// short or of one word are left alone.
|
|
90
108
|
static func script(_ lines: [Line], width: Double, height: Double, seed: UInt64) -> [Step] {
|
|
91
109
|
var rng = Seeded(seed)
|
|
92
110
|
let fit = lines.indices.filter { lines[$0].words.count >= 2 && lines[$0].rect.w >= 120 && lines[$0].rect.y >= top + 40 }
|
|
93
111
|
func far(_ i: Int) -> Double { hypot(lines[i].rect.midX - width / 2, lines[i].rect.midY - height / 2) }
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let
|
|
112
|
+
var near = Array(fit.sorted { far($0) < far($1) }.prefix(8))
|
|
113
|
+
let usual = fit.map { lines[$0].rect.h }.sorted()[safe: fit.count / 2] ?? 0
|
|
114
|
+
let heading = fit.filter { lines[$0].rect.h >= usual * 1.4 }.max { lines[$0].rect.h < lines[$1].rect.h }
|
|
115
|
+
guard let first = heading ?? near.randomElement(using: &rng) else { return [] }
|
|
116
|
+
near.removeAll { $0 == first }
|
|
97
117
|
var steps: [Step] = [.jump(line: first), .walk(line: first)]
|
|
98
118
|
// With one line only, it plays on the line it walked.
|
|
99
|
-
|
|
100
|
-
for
|
|
101
|
-
let
|
|
102
|
-
|
|
119
|
+
if near.isEmpty { near = [first] }
|
|
120
|
+
for act in Act.allCases.shuffled(using: &rng).prefix(4) {
|
|
121
|
+
let luck = near.map { _ in Double.random(in: 0..<1, using: &rng) }
|
|
122
|
+
guard let k = near.indices.max(by: { suits(act, lines[near[$0]], luck[$0]) < suits(act, lines[near[$1]], luck[$1]) }) else { break }
|
|
123
|
+
let i = near.remove(at: k), words = lines[i].words
|
|
124
|
+
let n = words.count, word = Int.random(in: 0..<n, using: &rng)
|
|
125
|
+
switch act {
|
|
103
126
|
case .eat:
|
|
104
127
|
let from = Int.random(in: 0..<n - 1, using: &rng)
|
|
105
128
|
steps.append(.eat(line: i, words: from..<min(from + 3, n)))
|
|
@@ -107,14 +130,44 @@ extension PlayMath {
|
|
|
107
130
|
case .stomp: steps.append(.stomp(line: i))
|
|
108
131
|
// The walked line is whole: the word goes to its end.
|
|
109
132
|
case .carry where i != first: steps.append(.carry(line: i, word: word, to: first))
|
|
110
|
-
|
|
133
|
+
// Its longest word: the most letters to fly.
|
|
134
|
+
case .kick, .carry: steps.append(.kick(line: i, word: words.indices.max { words[$0].text.count < words[$1].text.count } ?? word))
|
|
111
135
|
}
|
|
112
136
|
}
|
|
113
|
-
return steps + [.rest(
|
|
137
|
+
return steps + [.rest(0.6), .home, .rest(0.3)]
|
|
114
138
|
}
|
|
139
|
+
|
|
140
|
+
/// How well a line suits an act, the page deciding what happens
|
|
141
|
+
/// where (#580): the widest line is wiped, the one with the most words
|
|
142
|
+
/// stomped, the one with the longest word kicked, the shortest gives
|
|
143
|
+
/// the word that is carried. Any line can be eaten: `luck` decides.
|
|
144
|
+
static func suits(_ act: Act, _ line: Line, _ luck: Double) -> Double {
|
|
145
|
+
switch act {
|
|
146
|
+
case .wipe: return line.rect.w
|
|
147
|
+
case .stomp: return Double(line.words.count)
|
|
148
|
+
case .kick: return Double(line.words.map(\.text.count).max() ?? 0)
|
|
149
|
+
case .carry: return -line.rect.w
|
|
150
|
+
case .eat: return luck
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private extension Array {
|
|
156
|
+
subscript(safe i: Int) -> Element? { indices.contains(i) ? self[i] : nil }
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
extension Play {
|
|
160
|
+
/// Its size at `t`, in play sizes (#580): as big as on its edge when
|
|
161
|
+
/// it leaves it and when it is back, shrinking in the air of its
|
|
162
|
+
/// first jump and growing in the air of its last. It never changes
|
|
163
|
+
/// size at once.
|
|
164
|
+
func scale(at t: Double) -> Double {
|
|
165
|
+
func air(_ m: Move) -> Double { M.ease(M.span(t, m.start + Mo.crouch, m.end - Mo.landing)) }
|
|
166
|
+
if let back, t >= moves[back].start { return 1 + (P.edge - 1) * air(moves[back]) }
|
|
167
|
+
guard let first = moves.first, first.kind == .jump, back != 0 else { return 1 }
|
|
168
|
+
return P.edge - (P.edge - 1) * air(first)
|
|
169
|
+
}
|
|
170
|
+
|
|
118
171
|
/// The pieces that have left their place by this frame: each place is
|
|
119
172
|
/// covered, and the piece is drawn where it is now.
|
|
120
173
|
func move(_ f: inout Frame) {
|
|
@@ -52,11 +52,15 @@ enum PlayMath {
|
|
|
52
52
|
case carry(line: Int, word: Int, to: Int)
|
|
53
53
|
/// Stand still, facing you, for so many seconds.
|
|
54
54
|
case rest(Double)
|
|
55
|
+
/// Jump back to where it left its edge, growing to its size there.
|
|
56
|
+
case home
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/// Points per drawing unit on the page: a body 40 points across,
|
|
58
60
|
/// smaller than on the bottom edge so it fits between lines.
|
|
59
61
|
static let unit = 0.4
|
|
62
|
+
/// How many times bigger it is on its edge: 56 points across there.
|
|
63
|
+
static let edge = 1.4
|
|
60
64
|
/// The middle of its body above its feet, and its top, in points.
|
|
61
65
|
static let middle = (M.Pose().lift + 50) * unit
|
|
62
66
|
static let top = (M.Pose().lift + 100) * unit + 8
|
|
@@ -66,22 +70,6 @@ enum PlayMath {
|
|
|
66
70
|
static let perLetter = 0.09
|
|
67
71
|
/// Its mouth, ahead of its middle.
|
|
68
72
|
static let mouth = 10.0
|
|
69
|
-
|
|
70
|
-
/// A word's letters: the word's rectangle cut into equal parts, one a
|
|
71
|
-
/// character. An estimate, close enough to take them one at a time.
|
|
72
|
-
static func letters(of word: Word) -> [Rect] {
|
|
73
|
-
let n = max(word.text.count, 1), w = word.rect.w / Double(n)
|
|
74
|
-
return (0..<n).map { Rect(x: word.rect.x + Double($0) * w, y: word.rect.y, w: w, h: word.rect.h) }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/// The colour most of `samples` share: the page around a line. Close
|
|
78
|
-
/// shades count as one, so the soft edge of a letter does not win.
|
|
79
|
-
static func common(_ samples: [UInt32]) -> UInt32? {
|
|
80
|
-
var count: [UInt32: Int] = [:]
|
|
81
|
-
for s in samples { count[s & 0xF8F8F8, default: 0] += 1 }
|
|
82
|
-
guard let best = count.max(by: { ($0.value, $0.key) < ($1.value, $1.key) })?.key else { return nil }
|
|
83
|
-
return samples.first { $0 & 0xF8F8F8 == best }
|
|
84
|
-
}
|
|
85
73
|
}
|
|
86
74
|
|
|
87
75
|
/// A script laid out in time: every move has its start and its end, so
|
|
@@ -110,6 +98,8 @@ struct Play {
|
|
|
110
98
|
var face = 0.0
|
|
111
99
|
var pose = M.Pose()
|
|
112
100
|
var t = 0.0
|
|
101
|
+
/// Its size, in play sizes: bigger as it leaves its edge and comes back.
|
|
102
|
+
var scale = 1.0
|
|
113
103
|
/// What is covered with the page's colour, and which colour.
|
|
114
104
|
var gone: [(rect: P.Rect, paper: UInt32)] = []
|
|
115
105
|
/// The cloth, while it wipes.
|
|
@@ -124,6 +114,8 @@ struct Play {
|
|
|
124
114
|
let size: P.Point
|
|
125
115
|
private(set) var moves: [Move] = []
|
|
126
116
|
private(set) var cuts: [P.Cut] = []
|
|
117
|
+
/// The jump that takes it back to its place, if the script has one.
|
|
118
|
+
private(set) var back: Int?
|
|
127
119
|
var duration: Double { moves.last?.end ?? 0 }
|
|
128
120
|
|
|
129
121
|
/// `start` is where its feet are when play begins. Steps that name a
|
|
@@ -223,6 +215,10 @@ struct Play {
|
|
|
223
215
|
case .rest(let seconds):
|
|
224
216
|
let m = add(.rest, to: at, max(seconds, 0))
|
|
225
217
|
list.append(m); now = m.end
|
|
218
|
+
case .home:
|
|
219
|
+
guard at != start else { continue }
|
|
220
|
+
back = list.count
|
|
221
|
+
reach(start, into: &list)
|
|
226
222
|
}
|
|
227
223
|
}
|
|
228
224
|
moves = list
|
|
@@ -232,7 +228,7 @@ struct Play {
|
|
|
232
228
|
func frame(at t: Double) -> Frame {
|
|
233
229
|
guard let i = moves.lastIndex(where: { $0.start <= t }) else { return Frame(done: true) }
|
|
234
230
|
let m = moves[i], p = M.span(t, m.start, m.end)
|
|
235
|
-
var f = Frame(t: t, done: t >= duration)
|
|
231
|
+
var f = Frame(t: t, scale: scale(at: t), done: t >= duration)
|
|
236
232
|
|
|
237
233
|
let body = body(m, at: t)
|
|
238
234
|
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)
|