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.
- package/apps/mac-voice/Sources/AgentXVoice/Character.swift +4 -1
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +50 -4
- package/apps/mac-voice/Sources/AgentXVoice/PlayActs.swift +164 -0
- package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +94 -46
- package/apps/mac-voice/Sources/AgentXVoice/PlayMotion.swift +110 -0
- package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +56 -3
- package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +6 -0
- package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +4 -1
- package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +18 -1
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +24 -10
- package/dist/{App-UHSIO3UE.js → App-XLQDP26J.js} +2 -2
- package/dist/{ChatApp-LLTFBLJY.js → ChatApp-DRDUXZ66.js} +2 -2
- package/dist/{agent-6RJ633MQ.js → agent-QHM3J2XS.js} +2 -2
- package/dist/{board-dashboard-A3ZEI4F6.js → board-dashboard-AIT26JTK.js} +4 -4
- package/dist/{chunk-ZZSNM2RI.js → chunk-4BCAH7XI.js} +2 -2
- package/dist/{chunk-ALWMYA2B.js → chunk-B32A6OOY.js} +2 -2
- package/dist/{chunk-VO2GNAKB.js → chunk-BERHVF3T.js} +2 -2
- package/dist/{chunk-6DCY2JCB.js → chunk-BHKWXFJD.js} +5 -5
- package/dist/{chunk-4XHKJZLJ.js → chunk-BTLW2RML.js} +2 -2
- package/dist/{chunk-DU6536CC.js → chunk-DRUEPGLP.js} +2 -2
- package/dist/{chunk-KIGFWUKP.js → chunk-DXJRMPGH.js} +2 -2
- package/dist/{chunk-IPDEWVVT.js → chunk-GJ5EJVMZ.js} +2 -2
- package/dist/{chunk-EBVB727L.js → chunk-IQJ7UZVV.js} +2 -2
- package/dist/{chunk-ZVW645NU.js → chunk-J4JVKVEK.js} +2 -2
- package/dist/{chunk-TE44E733.js → chunk-J64VIHL5.js} +2 -2
- package/dist/{chunk-XEKZKH3W.js → chunk-JREA25GJ.js} +4 -4
- package/dist/chunk-JREA25GJ.js.map +1 -0
- package/dist/{chunk-KEKO4S45.js → chunk-JZJ7AZJD.js} +2 -2
- package/dist/{chunk-MKF6HSBA.js → chunk-LXJGLNVG.js} +2 -2
- package/dist/{chunk-QZIEYDIY.js → chunk-OFVMUBIP.js} +2 -2
- package/dist/{chunk-GHXSBJZS.js → chunk-QUU5RJ7D.js} +2 -2
- package/dist/{chunk-ZYJDAT3E.js → chunk-S2GPUSBU.js} +2 -2
- package/dist/{chunk-J2MHITWR.js → chunk-SJMOZY7Y.js} +2 -2
- package/dist/{chunk-MNS4SBRR.js → chunk-WWCVQHV3.js} +149 -149
- package/dist/chunk-WWCVQHV3.js.map +1 -0
- package/dist/{chunk-44UGVUN7.js → chunk-YYCFI4VP.js} +2 -2
- package/dist/{chunk-6NECC6YS.js → chunk-Z4FY64BS.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/{client-ARS6ZFT2.js → client-BLUCLNIS.js} +2 -2
- package/dist/{config-XO5Z3BO5.js → config-ZUMI4IY4.js} +2 -2
- package/dist/config-mutator-S3CD2IWQ.js +2 -0
- package/dist/{doctor-VUK3JE6Z.js → doctor-XGQW5SMD.js} +2 -2
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/dist/{program-HUQB4IIO.js → program-FK7REU2M.js} +117 -117
- package/dist/program-FK7REU2M.js.map +1 -0
- package/dist/{promote-UGQXA4KV.js → promote-K5LP5Q6Z.js} +2 -2
- package/dist/{query-DL7RHPMA.js → query-LHXSINM2.js} +2 -2
- package/dist/{registry-YIPKBRBH.js → registry-OQUO6NCW.js} +2 -2
- package/dist/{request-planner-L2FAVNKP.js → request-planner-TW5OCKTF.js} +2 -2
- package/dist/{review-candidates-RPUP4XBU.js → review-candidates-EFELFOSY.js} +2 -2
- package/dist/{routing-G66SVTRO.js → routing-TSRZLD7B.js} +2 -2
- package/dist/schedule-tool-JUHRVU5D.js +2 -0
- package/dist/scheduler-EHJPTULN.js +2 -0
- package/dist/{seat-ZPH7DA5L.js → seat-NIPWHLKC.js} +2 -2
- package/dist/{select-2WTV2BI2.js → select-UAKDLN76.js} +2 -2
- package/dist/{set-enabled-KI2FINQD.js → set-enabled-74DLXMSM.js} +2 -2
- package/dist/{tool-ZFQ46KU3.js → tool-CEBL57UW.js} +2 -2
- package/dist/{tool-2P4LRWN4.js → tool-TEGHFORD.js} +2 -2
- package/dist/wiki-WOK4SCB7.js +2 -0
- package/package.json +1 -1
- package/dist/chunk-MNS4SBRR.js.map +0 -1
- package/dist/chunk-XEKZKH3W.js.map +0 -1
- package/dist/config-mutator-MJX4EIYV.js +0 -2
- package/dist/program-HUQB4IIO.js.map +0 -1
- package/dist/schedule-tool-FOWPIWKP.js +0 -2
- package/dist/scheduler-GFVUPAD6.js +0 -2
- package/dist/wiki-XATGT3DI.js +0 -2
- /package/dist/{App-UHSIO3UE.js.map → App-XLQDP26J.js.map} +0 -0
- /package/dist/{ChatApp-LLTFBLJY.js.map → ChatApp-DRDUXZ66.js.map} +0 -0
- /package/dist/{agent-6RJ633MQ.js.map → agent-QHM3J2XS.js.map} +0 -0
- /package/dist/{board-dashboard-A3ZEI4F6.js.map → board-dashboard-AIT26JTK.js.map} +0 -0
- /package/dist/{chunk-ZZSNM2RI.js.map → chunk-4BCAH7XI.js.map} +0 -0
- /package/dist/{chunk-ALWMYA2B.js.map → chunk-B32A6OOY.js.map} +0 -0
- /package/dist/{chunk-VO2GNAKB.js.map → chunk-BERHVF3T.js.map} +0 -0
- /package/dist/{chunk-6DCY2JCB.js.map → chunk-BHKWXFJD.js.map} +0 -0
- /package/dist/{chunk-4XHKJZLJ.js.map → chunk-BTLW2RML.js.map} +0 -0
- /package/dist/{chunk-DU6536CC.js.map → chunk-DRUEPGLP.js.map} +0 -0
- /package/dist/{chunk-KIGFWUKP.js.map → chunk-DXJRMPGH.js.map} +0 -0
- /package/dist/{chunk-IPDEWVVT.js.map → chunk-GJ5EJVMZ.js.map} +0 -0
- /package/dist/{chunk-EBVB727L.js.map → chunk-IQJ7UZVV.js.map} +0 -0
- /package/dist/{chunk-ZVW645NU.js.map → chunk-J4JVKVEK.js.map} +0 -0
- /package/dist/{chunk-TE44E733.js.map → chunk-J64VIHL5.js.map} +0 -0
- /package/dist/{chunk-KEKO4S45.js.map → chunk-JZJ7AZJD.js.map} +0 -0
- /package/dist/{chunk-MKF6HSBA.js.map → chunk-LXJGLNVG.js.map} +0 -0
- /package/dist/{chunk-QZIEYDIY.js.map → chunk-OFVMUBIP.js.map} +0 -0
- /package/dist/{chunk-GHXSBJZS.js.map → chunk-QUU5RJ7D.js.map} +0 -0
- /package/dist/{chunk-ZYJDAT3E.js.map → chunk-S2GPUSBU.js.map} +0 -0
- /package/dist/{chunk-J2MHITWR.js.map → chunk-SJMOZY7Y.js.map} +0 -0
- /package/dist/{chunk-44UGVUN7.js.map → chunk-YYCFI4VP.js.map} +0 -0
- /package/dist/{chunk-6NECC6YS.js.map → chunk-Z4FY64BS.js.map} +0 -0
- /package/dist/{client-ARS6ZFT2.js.map → client-BLUCLNIS.js.map} +0 -0
- /package/dist/{config-XO5Z3BO5.js.map → config-ZUMI4IY4.js.map} +0 -0
- /package/dist/{config-mutator-MJX4EIYV.js.map → config-mutator-S3CD2IWQ.js.map} +0 -0
- /package/dist/{doctor-VUK3JE6Z.js.map → doctor-XGQW5SMD.js.map} +0 -0
- /package/dist/{promote-UGQXA4KV.js.map → promote-K5LP5Q6Z.js.map} +0 -0
- /package/dist/{query-DL7RHPMA.js.map → query-LHXSINM2.js.map} +0 -0
- /package/dist/{registry-YIPKBRBH.js.map → registry-OQUO6NCW.js.map} +0 -0
- /package/dist/{request-planner-L2FAVNKP.js.map → request-planner-TW5OCKTF.js.map} +0 -0
- /package/dist/{review-candidates-RPUP4XBU.js.map → review-candidates-EFELFOSY.js.map} +0 -0
- /package/dist/{routing-G66SVTRO.js.map → routing-TSRZLD7B.js.map} +0 -0
- /package/dist/{schedule-tool-FOWPIWKP.js.map → schedule-tool-JUHRVU5D.js.map} +0 -0
- /package/dist/{scheduler-GFVUPAD6.js.map → scheduler-EHJPTULN.js.map} +0 -0
- /package/dist/{seat-ZPH7DA5L.js.map → seat-NIPWHLKC.js.map} +0 -0
- /package/dist/{select-2WTV2BI2.js.map → select-UAKDLN76.js.map} +0 -0
- /package/dist/{set-enabled-KI2FINQD.js.map → set-enabled-74DLXMSM.js.map} +0 -0
- /package/dist/{tool-ZFQ46KU3.js.map → tool-CEBL57UW.js.map} +0 -0
- /package/dist/{tool-2P4LRWN4.js.map → tool-TEGHFORD.js.map} +0 -0
- /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
|
-
|
|
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
|
-
///
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
191
|
-
|
|
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)) *
|
|
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
|
-
|
|
206
|
-
case .
|
|
207
|
-
|
|
208
|
-
f.pose.sy *=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|