cozyclay 3.0.0 → 3.0.2
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/CHANGELOG.md +57 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/bin/agent/agent-routes.mjs +3 -3
- package/bin/agent/agent-runner.mjs +5 -5
- package/bin/agent/pi-tools.mjs +12 -1
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +99 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +1 -1
- package/dist/app/index.html +4 -4
- package/dist/assets/analytics-DbKG0NgD.js +6 -0
- package/dist/assets/app-34i-iUy1.js +5088 -0
- package/dist/assets/compile-worker-DYpa2nvO.js +5 -0
- package/dist/assets/{first-shot-handoff-CC4xe4HM.js → first-shot-handoff-CrGZ3n_O.js} +1 -1
- package/dist/assets/helvetiker-bold.typeface-BUlyMz0-.js +13 -0
- package/dist/assets/{landing-DjHD8u7k.js → landing-mq5bnqFE.js} +1 -1
- package/dist/assets/scene-asset-sync-7J-w5oNh.js +20 -0
- package/dist/assets/workflow-dUhurYUc.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/workflow/index.html +3 -3
- package/package.json +1 -1
- package/src/App.jsx +1 -1
- package/src/agent-progress.js +3 -2
- package/src/agent-turn-mark.js +33 -0
- package/src/app-context.js +7 -2
- package/src/command-bus.js +10 -11
- package/src/commands/elements/character.js +2 -1
- package/src/commands/elements/motion.js +7 -1
- package/src/commands/elements/scene.js +3 -0
- package/src/commands/elements/stage.js +2 -1
- package/src/commands/elements/take-read.js +31 -0
- package/src/commands/elements.js +16 -2
- package/src/commands/motion3.js +15 -6
- package/src/commands/objects.js +2 -2
- package/src/commands/shot.js +6 -2
- package/src/domains/motion/take-source.js +4 -2
- package/src/draft-ghost.js +3 -2
- package/src/motion/beat-board-model.js +1 -1
- package/src/motion/beat-board.jsx +2 -1
- package/src/motion3/compile-job.js +4 -0
- package/src/motion3/compiler/anchors.js +68 -4
- package/src/motion3/compiler/compile.js +5 -1
- package/src/motion3/compiler/grip.js +8 -2
- package/src/motion3/compiler/limbs.js +35 -8
- package/src/motion3/compiler/pose.js +17 -2
- package/src/motion3/compiler/socket-grip.js +107 -0
- package/src/motion3/compiler/validate-objects.js +3 -2
- package/src/motion3/compiler/validate-parts.js +9 -2
- package/src/motion3/compiler/validate.js +3 -3
- package/src/motion3/compiler/vocab.js +23 -16
- package/src/motion3/describe.js +5 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/free-motion.js +54 -15
- package/src/motion3/object-keys.js +36 -5
- package/src/motion3/object-pose.js +39 -6
- package/src/motion3/people.js +82 -0
- package/src/motion3/qa/checks.js +6 -2
- package/src/motion3/qa/hints-contact.js +17 -1
- package/src/motion3/qa/index.js +6 -4
- package/src/motion3/qa/people-checks.js +37 -0
- package/src/motion3/qa/style.js +14 -4
- package/src/motion3/refs.js +10 -1
- package/src/motion3/scene.js +4 -3
- package/src/motion3/schema/fields.js +4 -4
- package/src/motion3/schema/topics.js +9 -6
- package/src/motion3/schema/types.js +11 -9
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +10 -3
- package/src/motion3/skills/flip.js +200 -0
- package/src/motion3/skills/impact.js +109 -0
- package/src/motion3/skills/index.js +3 -1
- package/src/motion3/skills/info.js +19 -4
- package/src/motion3/sockets.js +104 -0
- package/src/motion3/take-edit.js +1 -1
- package/src/motion3/take.js +6 -4
- package/src/object-catalog.jsx +3 -1
- package/src/object-grasp.js +38 -0
- package/src/object-look.js +2 -0
- package/src/object-travel.js +6 -3
- package/src/panels/ObjectLookPanel.jsx +24 -3
- package/src/props.jsx +5 -5
- package/src/qa-label.js +25 -0
- package/src/scene-objects.js +19 -11
- package/src/shell/details-selection.js +2 -1
- package/src/studio-actions.js +4 -0
- package/src/studio-agent-protocol.js +11 -12
- package/src/studio-app-binding.js +3 -3
- package/src/studio-elements.js +3 -1
- package/src/studio-error-codes.js +8 -1
- package/src/text/fonts/helvetiker-LICENSE.txt +13 -0
- package/src/text/fonts/helvetiker-bold.typeface.json +1 -0
- package/src/text/text-object.jsx +106 -0
- package/src/text/text-record.js +122 -0
- package/src/workflow/AgentProgressCard.jsx +6 -4
- package/tools/run-tests.mjs +2 -2
- package/dist/assets/analytics-C9sUnndd.js +0 -1
- package/dist/assets/app-CCh--1Hc.js +0 -5087
- package/dist/assets/compile-worker-CkxiD6lH.js +0 -5
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +0 -20
- package/dist/assets/workflow-C4yKH1p4.js +0 -179
|
@@ -11,14 +11,14 @@ export const KEY_FIELDS = ["f", "set", "step", "src"];
|
|
|
11
11
|
export const USE_FIELDS = ["skill", "args", "f0"];
|
|
12
12
|
/** Anchor fields by anchor kind (besides `kind`). */
|
|
13
13
|
export const ANCHOR_FIELDS = { world: ["p"], body: ["p"], bone: ["bone", "offset"], segment: ["from", "to", "t", "tMin", "offset", "reach"],
|
|
14
|
-
object: ["object", "local"], landmark: ["name"], prop: ["prop", "point", "offset"] };
|
|
14
|
+
object: ["object", "local", "socket", "grasp"], landmark: ["name"], prop: ["prop", "point", "offset"], person: ["person", "bone", "offset"] };
|
|
15
15
|
export const PROP_FIELDS = ["hand", "origin", "points"];
|
|
16
16
|
export const FIT_FIELDS = ["side", "anchor", "effector", "offset", "orient"];
|
|
17
17
|
/** spec.objects[id]: a hinge or a carry (a body or a group id), see C3; carry keys: C14; a switch (any record, C15) moves nothing; a follow (any record, C16) rides a point of the body; a free entry (C17) moves by keys and falls. */
|
|
18
18
|
export const OBJECT_FIELDS = { hinge: ["pivot", "axis", "channel"], carry: ["prop", "at", "along", "visible", "level", "grab", "release", "keys", "fall"], switch: ["visible", "level"],
|
|
19
19
|
follow: ["follow", "offset", "aim", "visible", "level"], free: ["keys", "fall", "visible", "level"] };
|
|
20
|
-
/** One spec.objects[id].fall entry (C17): the frame it starts to fall, what it lands on, how much it bounces. */
|
|
21
|
-
export const FALL_FIELDS = ["at", "onto", "bounce"];
|
|
20
|
+
/** One spec.objects[id].fall entry (C17): the frame it starts to fall, what it lands on, how much it bounces, whether it rolls to rest. */
|
|
21
|
+
export const FALL_FIELDS = ["at", "onto", "bounce", "tumble"];
|
|
22
22
|
/** One spec.objects[id].keys entry (C14): the held object's world pose at frame f; aim (+ roll) in place of rot (C16). */
|
|
23
23
|
export const OBJECT_KEY_FIELDS = ["f", "pos", "rot", "aim", "roll", "lean"];
|
|
24
24
|
export const WALK_FIELDS = ["type", "f0", "steps", "step", "period", "swing", "halfWidth", "lift", "heading", "turnDistance", "pelvisHigh", "pelvisLow", "firstStep", "armSwing", "vary"];
|
|
@@ -39,6 +39,6 @@ export const GOAL_FIELDS = ["name", "near", "above", "moved", "still", "speed",
|
|
|
39
39
|
/** One motion.compile probes entry (C5): exactly one of distance | position. */
|
|
40
40
|
export const PROBE_FIELDS = ["name", "distance", "position", "frames", "every"];
|
|
41
41
|
/** Point reference kinds (C2, refs.js). */
|
|
42
|
-
export const REF_KINDS = ["joint", "bone", "prop", "anchor", "landmark", "object"];
|
|
42
|
+
export const REF_KINDS = ["joint", "bone", "prop", "anchor", "landmark", "object", "person"];
|
|
43
43
|
/** motion.describe topics (C9); examples: whole specs that compile (schema/spec-examples.js). */
|
|
44
44
|
export const DESCRIBE_TOPICS = ["essentials", "spec", "skills", "anchors", "props", "objects", "checks", "goals", "probes", "channels", "refs", "fix", "examples"];
|
|
@@ -48,7 +48,7 @@ export const EXAMPLES = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
const UNITS = `World metres, +Y up; ${CONVENTIONS.yaw}; the character's left is +X at yaw 0. Angles in degrees.`;
|
|
51
|
-
const NO_JUMP = "Without at/along it keeps its grab-frame offset
|
|
51
|
+
const NO_JUMP = "Without at/along it keeps its grab-frame palm offset: no jump.";
|
|
52
52
|
/** The objects example's carry: a grip prop and the object it holds. */
|
|
53
53
|
const CARRY = { props: EXAMPLES.objects.props, objects: { cup: EXAMPLES.objects.objects.cup } };
|
|
54
54
|
|
|
@@ -64,13 +64,15 @@ export const NOTES = {
|
|
|
64
64
|
"More: topics anchors, props, objects, checks, goals, probes, channels, refs, fix, skills."],
|
|
65
65
|
anchors: ["An anchor is a named point a hand can go to: key w_<anchor>_l / w_<anchor>_r from 0 to 1 with armw_<side> 1 (the IK arm).",
|
|
66
66
|
"An anchor name may not end in _l or _r.", "object anchors (a body or a group; local in its own axes) follow the object where it is (hinges, carries): put the other hand on a carried object with one; the carrying hand's own anchor stays where it took it. prop anchors are a point of a prop the other hand holds.",
|
|
67
|
-
"segment with reach slides from t toward tMin until the arm reaches it."
|
|
67
|
+
"segment with reach slides from t toward tMin until the arm reaches it.",
|
|
68
|
+
"An object anchor with grasp (power, pinch, hook, palm) takes the hand by a socket of its object and the compiler turns the hand: power and hook put the knuckle line along the socket's axis with the palm on it, pinch holds at the fingertips, palm presses flat on the nearest face. socket names one (object.grasp sockets the user or you stored, or a thin part by its id: a bar, a grip, a magazine); without it the socket nearest local, or the shoulder. Two hands on one carried object: each hand an object anchor with grasp on its part (a rifle's grip and magazine). reachGrip on such an anchor closes the fingers to the grasp. Never key the wrist for it; a wrist you key (wtwist/wflex) wins.",
|
|
69
|
+
"person { person: <character id>, bone, offset? } is another character's joint where its installed take has it that frame (a hand on a shoulder, a push on the chest): install that character's take first; reachGrip on it allows the hand to touch that person."],
|
|
68
70
|
props: ["A prop is a set of points fixed to one hand, [d, l, n] along the palm axes; prop:<prop>.origin is the mean of its origin bones.",
|
|
69
71
|
"fits: the hand target moves so the effector (a prop point or the mean of joints), not the wrist, lands on the anchor.",
|
|
70
72
|
"A fit on a scene body also turns the hand about the forearm into a grip: a thin body (a can, a bar, a handle, a neck) across the palm, else thumb up; never past a comfortable forearm turn. A wrist you turn (wtwist/wflex) wins; fit.orient: false (reachGrip args.orient: false) turns it off.",
|
|
71
73
|
"reachGrip adds props.<anchor>Grip and its fit. Carry a scene object with objects.<id>.prop (topic objects)."],
|
|
72
74
|
objects: ["A hinge turns the object (a body or a group) about pivot/axis by the angle channel your keys set (degrees).",
|
|
73
|
-
"A carry follows the prop's hand: before grab the scene pose, grab..release in the hand, after release
|
|
75
|
+
"A carry follows the prop's hand: before grab the scene pose, grab..release in the hand, after release where it was let go, riding a body under it that keys or a hinge move.",
|
|
74
76
|
`No grab: in hand from frame 0; no release: to the end. ${NO_JUMP}`,
|
|
75
77
|
"A group (Empty) id moves all its parts; at/along pin a group's root to the palm too (an object.createKit kit). A carried object is an obstacle for the body and the scene.",
|
|
76
78
|
"A switch { visible?, level? } (no prop, no hinge field) moves nothing: it shows or hides any record (a part, a light, an effect) and sets its level, 0..4 light, glow and effect strength "
|
|
@@ -81,14 +83,15 @@ export const NOTES = {
|
|
|
81
83
|
+ "a left hand on a prop anchor of it turns with it; "
|
|
82
84
|
+ `the hand eases in and out over ${KEY_BLEND} frames. A REACH group "<id>: n cm off its keys" is what the hand could not reach; `
|
|
83
85
|
+ "output.observations.held measures every key as drawn (deg off the asked +Y, cm off pos, the drawn +Y, the rot an aim key used).",
|
|
84
|
-
"free { keys?, fall? } moves what nobody holds: keys at any frame; fall drops it at frame at
|
|
85
|
-
+ "one small bounce (0: none). A carry falls before grab or after release.",
|
|
86
|
+
"free { keys?, fall? } moves what nobody holds: keys at any frame; fall drops it at frame at onto the still top below (onto: that one; else the floor) "
|
|
87
|
+
+ "to rest on a stable face, one small bounce (0: none). A carry falls before grab or after release.",
|
|
86
88
|
"A follow { follow: <ref>, offset?, aim?, visible?, level? } puts any record (an effect, a light, a part) on a body point every frame: "
|
|
87
89
|
+ "aim sets its +Y (an effect emits along it): up (default), face (forward), a direction or a ref (toward it); offset is metres along its axes after aim. It is no obstacle.",
|
|
88
90
|
"The editor's motion.keyObject writes one key from a drag (a hinge: its channel key)."],
|
|
89
91
|
checks: ["Each entry adds QA groups: contacts CONTACT, orient ORIENT, below BELOW, clearance CLEARANCE, appear APPEAR; supports mark where a part rests on an object (no FLOAT, no balance there); a foot on an object's top is on the ground without one.",
|
|
90
92
|
"when: a weight channel (active above 0.99) or [f0, f1]; clearance uses frames.", "Check points take joint:, prop: and anchor: only; goals and probes take every ref (topic refs).",
|
|
91
93
|
"allow excuses a body part inside an object up to depth; limits override the QA limits. Unnamed entries are named by place, contacts[0].",
|
|
94
|
+
"Other characters with an installed take are bodies too: PERSON \"<part> x person:<id>\" is a part inside that person; a touch you mean (a push, a hug) is allow { part, object: \"person:<id>\", depth }.",
|
|
92
95
|
"Speed, acceleration, knee bend and balance are style: measured in output.observations, not judged. Naming speedCore/speedLimb, accCore/accLimb, knee or balance in limits makes it a check (SPEED, ACC, KNEE, BALANCE). observations.near: each hand's and foot's nearest object and gap (< 0 inside), measured, not judged.",
|
|
93
96
|
"motion.qa { checks } replaces every check for a re-measure."],
|
|
94
97
|
goals: ["Exactly one of near (distance <= tol), above (a.y - b.y >= by), moved (an object's base moved at least by from the scene pose; any other point ref, e.g. prop:bat.tip, at least by from where it is at at[0], at: [f0, f1]), still (stays within tol, default 0.01 m, of where moved measures from) or speed (the point reaches by m/s).",
|
|
@@ -102,7 +105,7 @@ export const NOTES = {
|
|
|
102
105
|
"Flow (spec.flow, on by default): the spine, head, arm swing/bend and wrist carry their speed through a key (a small overshoot) and trail the body (spine 1, head 2, arms 3 frames; lag: { spine, head, arms } or a number); feet, root, hand targets, weights, poles, fingers and objects land on their keys on time. flow: false turns it off; { smooth: false } or { lag: false } one part.",
|
|
103
106
|
"w_<anchor>_<side> (0..1) weights a hand onto an anchor; the weights of one hand add up to at most 1.",
|
|
104
107
|
"Any other numeric channel must come from spec.channels, a hinge (objects.<id>.channel), a check's when or a visible switch; names ending _vis are visibility switches (shown above 0.5)."],
|
|
105
|
-
refs: ["A ref names a point: joint:/bone:<Bone>, prop:<prop>.<point>, anchor:<name>, landmark:mouth, object:<id>[.center|.top|.base].",
|
|
108
|
+
refs: ["A ref names a point: joint:/bone:<Bone>, prop:<prop>.<point>, anchor:<name>, landmark:mouth, object:<id>[.center|.top|.base], person:<character id>.<Bone> (another character's joint in its installed take).",
|
|
106
109
|
"object:<id> takes a body or a group (the box around its parts); in a distance a bare object:<id> means its surface.",
|
|
107
110
|
"Goals and probes take every kind; spec.checks keep joint:, prop: and anchor:."],
|
|
108
111
|
fix: [`spec.fix keys: ${FIX_KEYS.join(", ")}; false turns all off. output.fixed reports what each one changed.`,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// looks each field up here, and throws when one is missing, so a new field needs a type here.
|
|
4
4
|
// Shared shapes are $refs into DEFS (describe.js adds the ones an answer uses).
|
|
5
5
|
import { REF_KINDS, OBJECT_KEY_FIELDS } from "./fields.js";
|
|
6
|
-
import { CONVENTIONS } from "../compiler/vocab.js";
|
|
6
|
+
import { CONVENTIONS, GRASPS } from "../compiler/vocab.js";
|
|
7
7
|
import { QA_LIMITS } from "../qa/checks.js";
|
|
8
8
|
import { CAPSULES } from "../qa/geometry.js";
|
|
9
9
|
|
|
@@ -64,11 +64,12 @@ export const ANCHOR_TYPES = {
|
|
|
64
64
|
from: str("bone"), to: str("bone"), t: num("0 = from .. 1 = to", { default: 0.5 }), tMin: num("with reach: the lowest t to slide back to", { default: 0.15 }),
|
|
65
65
|
reach: side("slide toward `from` until this arm reaches"), object: str("scene object id"), local: ref("vec3", "point in the object's frame, m"),
|
|
66
66
|
name: { const: "mouth" }, prop: str("a prop held by the other hand"), point: str("a point of that prop, or origin"),
|
|
67
|
+
person: str("a character id"), socket: str("a socket of the object"), grasp: { enum: GRASPS, description: "the hand takes it by a socket" },
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const palm = d => ref("vec3", `${d} [d, l, n] on the palm axes (d to the middle finger, l to the thumb side, n = d x l), m`);
|
|
70
71
|
/** Fields an anchor of each kind must have. */
|
|
71
|
-
export const ANCHOR_REQUIRED = { world: ["p"], body: ["p"], bone: ["bone"], segment: ["from", "to"], object: ["object"], landmark: ["name"], prop: ["prop", "point"] };
|
|
72
|
+
export const ANCHOR_REQUIRED = { world: ["p"], body: ["p"], bone: ["bone"], segment: ["from", "to"], object: ["object"], landmark: ["name"], prop: ["prop", "point"], person: ["person", "bone"] };
|
|
72
73
|
|
|
73
74
|
export const PROP_TYPES = {
|
|
74
75
|
hand: side(), origin: { type: "array", items: { type: "string" }, minItems: 1, description: "bones whose mean is the prop origin" },
|
|
@@ -85,17 +86,17 @@ export const FIT_TYPES = {
|
|
|
85
86
|
const OBJECT_KEY_TYPES = { f: int(), pos: ref("vec3"), rot: { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4, description: "quat x, y, z, w" },
|
|
86
87
|
aim: ref("vec3", "or rot: +Y dir"), roll: num("deg about aim"), lean: num("aim tilt", { minimum: 0, maximum: 45 }) };
|
|
87
88
|
|
|
88
|
-
const FALL = { type: "object", properties: { at: int(), onto: str(), bounce: num(null, { default: 0.25, minimum: 0, maximum: 0.6 }) }, required: ["at"], additionalProperties: false };
|
|
89
|
+
const FALL = { type: "object", properties: { at: int(), onto: str(), bounce: num(null, { default: 0.25, minimum: 0, maximum: 0.6 }), tumble: { type: "boolean" } }, required: ["at"], additionalProperties: false };
|
|
89
90
|
/** A free entry's keys and fall (C17) are a carry's, at any frame: said once, not twice. */
|
|
90
91
|
const AS_CARRY = { description: "as carry; keys at any frame" };
|
|
91
92
|
export const FREE_TYPES = { keys: AS_CARRY, fall: AS_CARRY };
|
|
92
93
|
|
|
93
94
|
export const OBJECT_TYPES = {
|
|
94
|
-
pivot: ref("vec3", "world hinge point
|
|
95
|
-
prop: str("spec.props name: it rides that hand"), at: ref("vec3", "palm offset [d, l, n]
|
|
96
|
-
visible: str("channel: shown > 0.5"), level: str("channel: strength 0..4"), grab: int("
|
|
95
|
+
pivot: ref("vec3", "world hinge point"), axis: ref("vec3", "default +Y"), channel: str("angle channel, deg"),
|
|
96
|
+
prop: str("spec.props name: it rides that hand"), at: ref("vec3", "palm offset [d, l, n]"), along: ref("vec3", "+Y on the palm, fixed"),
|
|
97
|
+
visible: str("channel: shown > 0.5"), level: str("channel: strength 0..4"), grab: int("hand takes it", { default: 0 }), release: int("let go (else end)"),
|
|
97
98
|
keys: { type: "array", items: { type: "object", properties: Object.fromEntries(OBJECT_KEY_FIELDS.map(f => [f, OBJECT_KEY_TYPES[f]])), required: ["f", "pos"], additionalProperties: false },
|
|
98
|
-
description: "world pose at f
|
|
99
|
+
description: "world pose at f; hand follows" },
|
|
99
100
|
fall: { ...FALL, description: "or a list; topic objects" },
|
|
100
101
|
follow: ref("ref"), offset: ref("vec3"), aim: { oneOf: [{ enum: ["face", "up"] }, { $ref: "#/$defs/vec3" }, { $ref: "#/$defs/ref" }], default: "up" },
|
|
101
102
|
};
|
|
@@ -142,6 +143,7 @@ export const FIX_TYPES = {
|
|
|
142
143
|
};
|
|
143
144
|
|
|
144
145
|
export const REF_DOCS = {
|
|
145
|
-
joint: "joint:<Bone
|
|
146
|
-
anchor: "anchor:<name
|
|
146
|
+
joint: "joint:<Bone>", bone: "bone:<Bone> (= joint:)", prop: "prop:<prop>.<point> (origin exists)",
|
|
147
|
+
anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base], a body or group; bare = its surface (distance)",
|
|
148
|
+
person: "person:<id>.<Bone>",
|
|
147
149
|
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// How a falling object (C17, free-motion.js) comes to rest. It falls with its centre on a parabola, turning at the spin it
|
|
2
|
+
// was let go with, and lands on its lowest point. Then it rolls about what touches the support. Landing on an edge or a
|
|
3
|
+
// corner it rocks back onto the face nearest down, or falls over onto the face it leans to when it leans past that edge.
|
|
4
|
+
// Then it goes over an edge onto a lower face while the landing has the energy (a share TIP of the drop; only one that hit
|
|
5
|
+
// tilted or spinning, since a flat landing has nothing to tip it), or at once from a precarious face (one it can tilt less
|
|
6
|
+
// than PRECARIOUS before its centre passes the edge). So a phone on its edge falls flat, a tilted box comes down on a face,
|
|
7
|
+
// a can let go at a slant lies down, and a can or cup dropped upright stays upright. It only rolls onto a face that lowers
|
|
8
|
+
// its centre by at least LOWER of its height, so a near cube stays the way it lands. The shape is the box around the
|
|
9
|
+
// object's parts in its own frame, and its centre is the parts' volume-weighted centre. Every step is a rotation about a
|
|
10
|
+
// pivot on the support, paced by gravity: closed form and deterministic.
|
|
11
|
+
import { add, sub, scale, dot, cross, len, norm, mul, apply, axisRot, clamp } from "./compiler/math.js";
|
|
12
|
+
import { G } from "./free-motion.js";
|
|
13
|
+
|
|
14
|
+
/** The share of the drop height a landing spends tipping the object over an edge. */
|
|
15
|
+
export const TIP = 0.3;
|
|
16
|
+
const LOWER = 0.2, FLAT = 0.5, PRECARIOUS = 15, MIN_SPIN = 1, MAX_SPIN = 6 * Math.PI, MAX_STEPS = 3;
|
|
17
|
+
const DOWN = [0, -1, 0];
|
|
18
|
+
const AXES = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
|
|
19
|
+
const deg = r => (r * 180) / Math.PI;
|
|
20
|
+
|
|
21
|
+
/** The shape of part boxes { pos, R, half } given in the root frame: { com, lo, hi } (box around the corners); null without volume. */
|
|
22
|
+
export function shapeOf(boxes) {
|
|
23
|
+
const solid = boxes.filter(b => b.half[0] * b.half[1] * b.half[2] > 1e-12);
|
|
24
|
+
if (!solid.length) return null;
|
|
25
|
+
let vol = 0, c = [0, 0, 0];
|
|
26
|
+
const corners = [];
|
|
27
|
+
for (const b of solid) {
|
|
28
|
+
const v = b.half[0] * b.half[1] * b.half[2];
|
|
29
|
+
vol += v;
|
|
30
|
+
c = add(c, scale(add(b.pos, apply(b.R, [0, b.half[1], 0])), v));
|
|
31
|
+
for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) corners.push(add(b.pos, apply(b.R, [x * b.half[0], y * b.half[1], z * b.half[2]])));
|
|
32
|
+
}
|
|
33
|
+
return { com: scale(c, 1 / vol), lo: [0, 1, 2].map(i => Math.min(...corners.map(p => p[i]))), hi: [0, 1, 2].map(i => Math.max(...corners.map(p => p[i]))) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const boxCorners = s => [0, 1, 2, 3, 4, 5, 6, 7].map(i => [i & 1 ? s.hi[0] : s.lo[0], i & 2 ? s.hi[1] : s.lo[1], i & 4 ? s.hi[2] : s.lo[2]]);
|
|
37
|
+
const world = (pose, p) => add(pose.pos, apply(pose.R, p));
|
|
38
|
+
const lowest = (s, pose) => Math.min(...boxCorners(s).map(p => world(pose, p)[1]));
|
|
39
|
+
/** The pose moved up or down so its lowest corner sits on the support top. */
|
|
40
|
+
const snap = (s, pose, top) => ({ pos: [pose.pos[0], pose.pos[1] + top - lowest(s, pose), pose.pos[2]], R: pose.R });
|
|
41
|
+
const about = (pose, pivot, Q) => ({ pos: add(pivot, apply(Q, sub(pose.pos, pivot))), R: mul(Q, pose.R) });
|
|
42
|
+
/** The face whose outward normal points most nearly down. */
|
|
43
|
+
const downFace = R => AXES.reduce((best, n, k) => (dot(apply(R, n), DOWN) > dot(apply(R, AXES[best]), DOWN) ? k : best), 0);
|
|
44
|
+
/** Where the plane of face k lies along its axis, and the centre's distance to it (its height when it rests on k). */
|
|
45
|
+
const facePlane = (s, k) => { const a = k >> 1; return AXES[k][a] > 0 ? s.hi[a] : s.lo[a]; };
|
|
46
|
+
const heightOn = (s, k) => Math.abs(facePlane(s, k) - s.com[k >> 1]);
|
|
47
|
+
|
|
48
|
+
/** The spin (rad/s, world axis times rate) that takes rotation Ra to Rb in one frame at fps; capped at MAX_SPIN, none under MIN_SPIN (a hand's jitter). */
|
|
49
|
+
export function spinOf(Ra, Rb, fps) {
|
|
50
|
+
const D = mul(Rb, [[Ra[0][0], Ra[1][0], Ra[2][0]], [Ra[0][1], Ra[1][1], Ra[2][1]], [Ra[0][2], Ra[1][2], Ra[2][2]]]);
|
|
51
|
+
const a = Math.acos(clamp((D[0][0] + D[1][1] + D[2][2] - 1) / 2, -1, 1));
|
|
52
|
+
if (a * fps < MIN_SPIN) return [0, 0, 0];
|
|
53
|
+
const axis = norm([D[2][1] - D[1][2], D[0][2] - D[2][0], D[1][0] - D[0][1]]), rate = Math.min(MAX_SPIN, a * fps);
|
|
54
|
+
return len(axis) > 0 ? scale(axis, rate) : [0, 0, 0];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The fall through the air from `start` onto the support top: { tl (s), at(t) } with the centre on a parabola, turning at `spin`. */
|
|
58
|
+
export function airPath(s, start, spin, top, fps) {
|
|
59
|
+
const rate = len(spin), com0 = world(start, s.com);
|
|
60
|
+
const at = t => {
|
|
61
|
+
const R = rate > 0 ? mul(axisRot(spin, deg(rate * t)), start.R) : start.R;
|
|
62
|
+
return { pos: sub(add(com0, [0, -0.5 * G * t * t, 0]), apply(R, s.com)), R };
|
|
63
|
+
};
|
|
64
|
+
const h = Math.max(0, lowest(s, start) - top);
|
|
65
|
+
if (rate === 0) return { tl: Math.sqrt((2 * h) / G), at };
|
|
66
|
+
// spinning: the first sub-frame step whose lowest corner reaches the top, then halved down to the touch
|
|
67
|
+
const dt = 1 / (fps * 8), cap = Math.sqrt((2 * (h + len(sub(s.hi, s.lo)))) / G) + dt;
|
|
68
|
+
let a = 0, b = dt;
|
|
69
|
+
while (b < cap && lowest(s, at(b)) > top) { a = b; b += dt; }
|
|
70
|
+
for (let i = 0; i < 24 && b - a > 1e-7; i++) { const m = (a + b) / 2; if (lowest(s, at(m)) > top) a = m; else b = m; }
|
|
71
|
+
return { tl: b, at };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A step's duration (s): gravity-paced by the centre's height and the angle it rolls through. */
|
|
75
|
+
const stepTime = (height, angle) => 0.12 + 0.9 * Math.sqrt(Math.max(height, 1e-4) / G) * Math.sqrt(angle / 90);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* From the pose it lands in, how it rolls to rest on the support top with `energy` (m of drop) to tip over edges:
|
|
79
|
+
* { steps: [{ from, pivot, axis, angle, time }], rest, time }. `lean` (horizontal) is the way a spin tips it: it gives a flat
|
|
80
|
+
* landing the energy and breaks a tie between edges.
|
|
81
|
+
*/
|
|
82
|
+
export function settlePath(s, impact, top, energy, lean = [0, 0, 0]) {
|
|
83
|
+
const steps = [];
|
|
84
|
+
let pose = snap(s, impact, top), time = 0;
|
|
85
|
+
const push = (pivot, axis, angle) => {
|
|
86
|
+
const height = world(pose, s.com)[1] - top, t = stepTime(height, angle);
|
|
87
|
+
steps.push({ from: pose, pivot, axis, angle, time: t });
|
|
88
|
+
pose = snap(s, about(pose, pivot, axisRot(axis, angle)), top);
|
|
89
|
+
time += t;
|
|
90
|
+
};
|
|
91
|
+
// landed on an edge or a corner: back onto the face nearest down, or over onto the face it leans to when it leans past
|
|
92
|
+
// that edge (more than the angle its centre needs to pass it), rolling about the corners on the support
|
|
93
|
+
let k = downFace(pose.R);
|
|
94
|
+
const tiltOf = j => deg(Math.acos(clamp(dot(apply(pose.R, AXES[j]), DOWN), -1, 1))), tilt = tiltOf(k);
|
|
95
|
+
if (tilt > FLAT) {
|
|
96
|
+
const side = [0, 1, 2, 3, 4, 5].filter(j => j >> 1 !== k >> 1).reduce((a, j) => (tiltOf(j) < tiltOf(a) ? j : a), k === 0 || k === 1 ? 2 : 0);
|
|
97
|
+
if (tilt > deg(Math.atan2(heightOn(s, side), heightOn(s, k)))) k = side;
|
|
98
|
+
const touching = boxCorners(s).map(p => world(pose, p)).filter(p => p[1] <= top + 1e-4);
|
|
99
|
+
const n = apply(pose.R, AXES[k]);
|
|
100
|
+
push(scale(touching.reduce(add, [0, 0, 0]), 1 / touching.length), norm(cross(n, DOWN)), tiltOf(k));
|
|
101
|
+
}
|
|
102
|
+
// over an edge onto a lower face while the landing has the energy, at once from a precarious face (or a centre past the
|
|
103
|
+
// edge); a flat landing with no spin has none
|
|
104
|
+
let left = tilt > FLAT || len(lean) > 0 ? energy : 0;
|
|
105
|
+
for (let i = 0; i < MAX_STEPS; i++) {
|
|
106
|
+
const com = world(pose, s.com), cK = com[1] - top;
|
|
107
|
+
let best = null;
|
|
108
|
+
for (let j = 0; j < 6; j++) {
|
|
109
|
+
if (j >> 1 === k >> 1) continue;
|
|
110
|
+
const out = apply(pose.R, AXES[j]), edge = world(pose, AXES[j].map((v, a) => (v ? facePlane(s, j) : a === k >> 1 ? facePlane(s, k) : s.com[a])));
|
|
111
|
+
const w = -dot(sub(com, edge), out), cJ = heightOn(s, j);
|
|
112
|
+
if (!(w < 0 || cJ < cK * (1 - LOWER))) continue;
|
|
113
|
+
const cost = w < 0 || deg(Math.atan2(w, cK)) < PRECARIOUS ? 0 : Math.hypot(cK, w) - cK, side = dot(out, lean);
|
|
114
|
+
if (!best || cost < best.cost - 1e-9 || (Math.abs(cost - best.cost) <= 1e-9 && side > best.side)) best = { j, out, edge, cost, side };
|
|
115
|
+
}
|
|
116
|
+
if (!best || best.cost > left) break;
|
|
117
|
+
left -= best.cost;
|
|
118
|
+
const touching = boxCorners(s).map(p => world(pose, p)).filter(p => p[1] <= top + 1e-4 && dot(sub(p, best.edge), best.out) >= -1e-4);
|
|
119
|
+
push(touching.length ? scale(touching.reduce(add, [0, 0, 0]), 1 / touching.length) : [best.edge[0], top, best.edge[2]], norm(cross(best.out, DOWN)), 90);
|
|
120
|
+
k = best.j;
|
|
121
|
+
}
|
|
122
|
+
return { steps, rest: pose, time };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The pose `u` seconds after it landed, along the settle path: each step rolls about its pivot, accelerating like a fall. */
|
|
126
|
+
export function settledAt(path, u) {
|
|
127
|
+
let t = u;
|
|
128
|
+
for (const st of path.steps) {
|
|
129
|
+
if (t < st.time) { const s = t / st.time; return about(st.from, st.pivot, axisRot(st.axis, st.angle * s * s)); }
|
|
130
|
+
t -= st.time;
|
|
131
|
+
}
|
|
132
|
+
return path.rest;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The energy (m of drop) a spin adds: its kinetic energy over m g, for the box's radius of gyration about its centre. */
|
|
136
|
+
export const spinEnergy = (s, spin) => (len(spin) ** 2 * ((s.hi[0] - s.lo[0]) ** 2 + (s.hi[1] - s.lo[1]) ** 2 + (s.hi[2] - s.lo[2]) ** 2)) / (24 * G);
|
|
@@ -4,6 +4,10 @@ import { add, apply, ry } from "../compiler/math.js";
|
|
|
4
4
|
import { walk } from "../compiler/walk.js";
|
|
5
5
|
import { keyBook, skillError } from "./book.js";
|
|
6
6
|
import { walkEnd } from "./info.js";
|
|
7
|
+
import { GRASP_FINGERS, GRASP_POINTS } from "../compiler/vocab.js";
|
|
8
|
+
|
|
9
|
+
/** The fingers' point a reachGrip fits onto its anchor: the finger middles, or where a grasp holds the thing (the palm is -n on the right hand). */
|
|
10
|
+
const graspPoint = (grasp, k) => (GRASP_POINTS[grasp] ? GRASP_POINTS[grasp].map((v, i) => (i === 2 && k === "r" ? -v : v)) : [0, 0, 0]);
|
|
7
11
|
|
|
8
12
|
/** standIdle({ at: [x, z], heading = 0, until }): a neutral standing pose, arms down (FK). */
|
|
9
13
|
export function standIdle(ctx) {
|
|
@@ -59,7 +63,9 @@ export function reachGrip(ctx) {
|
|
|
59
63
|
const { args, state } = ctx, k = args.side, name = args.anchor;
|
|
60
64
|
if (k !== "l" && k !== "r") skillError(ctx.tag, `args.side must be "l" or "r", got ${JSON.stringify(k)}`);
|
|
61
65
|
if (typeof name !== "string" || /_[lr]$/.test(name)) skillError(ctx.tag, "args.anchor must name an anchor (not ending in _l/_r)");
|
|
62
|
-
const reach = args.reach ?? 20, hold = args.hold ?? 22, release = args.release ?? 12
|
|
66
|
+
const reach = args.reach ?? 20, hold = args.hold ?? 22, release = args.release ?? 12;
|
|
67
|
+
// an anchor that takes a grasp closes the hand to that grasp's fingers (../sockets.js)
|
|
68
|
+
const grasp = (args.define ?? ctx.anchors?.[name])?.grasp, preset = args.fingers ?? GRASP_FINGERS[grasp] ?? "grip";
|
|
63
69
|
const w = `w_${name}_${k}`, fg = `fingers_${k}`, armw = `armw_${k}`, el = `elbow_${k}`, wasFk = !(state[armw] > 0.5);
|
|
64
70
|
// a take that ends after the hold but before the hand lets go cuts the release: the grip holds to the last frame
|
|
65
71
|
const last = ctx.frames - 1, cut = ctx.f0 + reach + hold <= last && ctx.f0 + reach + hold + release > last;
|
|
@@ -71,10 +77,11 @@ export function reachGrip(ctx) {
|
|
|
71
77
|
if (!cut) b.at(reach + hold + release, { [w]: 0, [fg]: { ...state[fg], [preset]: 0 }, ...(wasFk ? { [armw]: 0 } : {}) });
|
|
72
78
|
const S = k === "l" ? "Left" : "Right", prop = `${name}Grip`;
|
|
73
79
|
const def = args.define ? { [name]: args.define } : {};
|
|
74
|
-
|
|
80
|
+
// the hand may touch what it grips: an object, or the other person (people.js) whose joint the anchor is on
|
|
81
|
+
const a = args.define ?? ctx.anchors?.[name], obj = a?.kind === "object" ? a.object : a?.kind === "person" ? `person:${a.person}` : null;
|
|
75
82
|
return {
|
|
76
83
|
keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut: `release cut at f${last} (hold to the end)` } : {}), anchors: def,
|
|
77
|
-
props: { [prop]: { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers:
|
|
84
|
+
props: { [prop]: { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: graspPoint(grasp, k) } } },
|
|
78
85
|
fits: [{ side: k, anchor: name, effector: { prop, point: "fingers" }, ...(args.orient === false ? { orient: false } : {}) }],
|
|
79
86
|
checks: { contacts: [{ name: `fingers on ${name}`, point: `prop:${prop}.fingers`, target: `anchor:${name}`, tol: 0.04, when: w }],
|
|
80
87
|
allow: obj ? [{ part: "hand", object: obj, depth: 0.04 }] : [] },
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// flip: a standing back or front tuck, or a cartwheel, from the standing pose keyed before f0. The root and the feet (and
|
|
2
|
+
// the cartwheel's hands) are keyed every frame from closed-form curves whose speeds join without a kink (no POP); the
|
|
3
|
+
// arms, spine and head are keyed at the phase edges and the channels' own interpolation carries them between.
|
|
4
|
+
// - back/front: crouch, push-off (the heels roll up over the balls), a ballistic flight (the hips on a parabola under
|
|
5
|
+
// gravity, so QA reads a jump, not a float) with the knees tucked to the chest while the whole body turns 360 deg a turn
|
|
6
|
+
// (pitch, - backward), a landing that absorbs the fall, and the stand.
|
|
7
|
+
// - cartwheel: the lead foot steps out, then the body rolls 360 deg toward args.side like a wheel whose spokes are the
|
|
8
|
+
// straddled legs and the arms overhead: each frame the hips sit as high as the lowest spoke touching the floor allows,
|
|
9
|
+
// and a hand or foot whose spoke is down stays planted where it touched.
|
|
10
|
+
// Both end with the turn's full angle and then 0 on the last two frames (the same pose), so a later key at 0 does not
|
|
11
|
+
// unwind it. Poles are given so the knees and elbows bend toward the turning body's front (pose.js tiltFrame).
|
|
12
|
+
import { add, sub, scale, apply, mul, rx, ry, rz, dist, T } from "../compiler/math.js";
|
|
13
|
+
import { pivot } from "../compiler/walk-pelvis.js";
|
|
14
|
+
import { tiltFrame } from "../compiler/pose.js";
|
|
15
|
+
import { skillError } from "./book.js";
|
|
16
|
+
|
|
17
|
+
const G = 9.81 / (24 * 24); // m per frame^2 at the timeline's 24 fps
|
|
18
|
+
const CROUCH = 10, PUSH = 5, LAND = 6, RISE = 14, TOE_OFF = 50;
|
|
19
|
+
const clamp01 = u => Math.min(1, Math.max(0, u));
|
|
20
|
+
const ease = u => { const t = clamp01(u); return t * t * (3 - 2 * t); };
|
|
21
|
+
const mix = (a, b, t) => a + (b - a) * t;
|
|
22
|
+
const mix3 = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t);
|
|
23
|
+
const window = (u, a, b, c, d) => ease((u - a) / (b - a)) - ease((u - c) / (d - c));
|
|
24
|
+
const SIDE = { l: 1, r: -1 };
|
|
25
|
+
const bodyFrame = (h, pitch, roll) => mul(ry(h), mul(rx(pitch), rz(roll)));
|
|
26
|
+
/** A pole the turning body (B) holds as `local`, written as the channel pose.js turns by its own frame. */
|
|
27
|
+
const poleFor = (h, pitch, roll, B, local) => apply(T(tiltFrame(h, pitch, roll)?.R ?? ry(h)), apply(B, local));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* fyaw/fpitch that make pose.js aim the foot (orientFoot, in its tilt frame) where the turning body B holds it at fpitch
|
|
31
|
+
* `fp` in its own frame: the toe direction solved back through the frame pose.js reads the channels in (clamped to fpitch's
|
|
32
|
+
* range). Nearer upright pose.js reads them on the level, as for any foot, and they pass as they are.
|
|
33
|
+
*/
|
|
34
|
+
function footAim(b, k, pitch, roll, B, fp) {
|
|
35
|
+
const tilt = tiltFrame(b.h, pitch, roll);
|
|
36
|
+
if (!tilt) return { fyaw: b.h, fpitch: fp };
|
|
37
|
+
const rest = b.toe[k], want = apply(B, apply(rx(fp), rest)), d = apply(T(tilt.R), want);
|
|
38
|
+
const phi = Math.atan2(-rest[1], rest[2]), deg = 180 / Math.PI;
|
|
39
|
+
const p = Math.asin(Math.max(-1, Math.min(1, -d[1] / Math.hypot(...d)))) - phi;
|
|
40
|
+
return { fyaw: b.h + Math.atan2(d[0], d[2]) * deg, fpitch: Math.max(-60, Math.min(90, p * deg)) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function base(ctx) {
|
|
44
|
+
const { sk, args, state } = ctx, I = sk.index, P = sk.restWorldPos;
|
|
45
|
+
if (!state.foot_l || !state.foot_r || !state.root) skillError(ctx.tag, "needs a standing pose keyed before f0 (use standIdle or another skill first)");
|
|
46
|
+
const h = args.heading ?? state.yaw ?? 0, R = ry(h), hips = P[I.Hips];
|
|
47
|
+
const leg = dist(P[I.LeftUpLeg], P[I.LeftLeg]) + dist(P[I.LeftLeg], P[I.LeftFoot]), arm = dist(P[I.LeftArm], P[I.LeftForeArm]) + dist(P[I.LeftForeArm], P[I.LeftHand]);
|
|
48
|
+
return { h, R, F: apply(R, [0, 0, 1]), L: apply(R, [1, 0, 0]), ank: P[I.LeftFoot][1], y0: hips[1] - 0.01, leg, arm,
|
|
49
|
+
feet: { l: state.foot_l.slice(), r: state.foot_r.slice() }, root: state.root.slice(),
|
|
50
|
+
hipOff: { l: sub(P[I.LeftUpLeg], hips), r: sub(P[I.RightUpLeg], hips) }, shoulder: { l: sub(P[I.LeftArm], hips), r: sub(P[I.RightArm], hips) },
|
|
51
|
+
toe: { l: sub(P[I.LeftToeBase], P[I.LeftFoot]), r: sub(P[I.RightToeBase], P[I.RightFoot]) } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tuck(ctx, b, kind) {
|
|
55
|
+
// one turn from a standing jump whose hips rise H at the top: the curves below are measured clean for that flip (a
|
|
56
|
+
// higher jump or more turns would need its own tuck timing, so the skill takes neither)
|
|
57
|
+
const { args } = ctx, s = kind === "back" ? -1 : 1, turn = s * 360, H = 0.5;
|
|
58
|
+
const travel = args.travel ?? (kind === "back" ? -0.25 : 0.4);
|
|
59
|
+
const yT = b.y0 + 0.05, yL = b.y0 - 0.02; // the hips at take-off (toes pointed) and at touch-down
|
|
60
|
+
const Tf = Math.max(10, Math.round(Math.sqrt(2 * (H - 0.05) / G) + Math.sqrt(2 * (yT + H - 0.05 - yL) / G)));
|
|
61
|
+
const v0 = (yL - yT + (G * Tf * Tf) / 2) / Tf, vL = v0 - G * Tf, yc = yT - (v0 * PUSH) / 2, bottom = yL + (vL * LAND) / 2;
|
|
62
|
+
const vx = travel / (PUSH / 2 + Tf + LAND / 2);
|
|
63
|
+
const t0 = ctx.f0, tP = t0 + CROUCH, tA = tP + PUSH, tD = tA + Tf, tL = tD + LAND, tE = tL + RISE;
|
|
64
|
+
const land = k => add(b.feet[k], scale(b.F, travel)), ball = k => add(b.feet[k], apply(b.R, b.toe[k]));
|
|
65
|
+
const hipsAt = f => {
|
|
66
|
+
let y, x;
|
|
67
|
+
if (f <= tP) { y = mix(b.root[1], yc, ease((f - t0) / CROUCH)); x = 0; }
|
|
68
|
+
else if (f <= tA) { const t = f - tP; y = yc + (v0 / PUSH) * t * t / 2; x = (vx / PUSH) * t * t / 2; }
|
|
69
|
+
else if (f <= tD) { const t = f - tA; y = yT + v0 * t - (G * t * t) / 2; x = (vx * PUSH) / 2 + vx * t; }
|
|
70
|
+
else if (f <= tL) { const t = f - tD; y = yL + vL * t - (vL / LAND) * t * t / 2; x = (vx * PUSH) / 2 + vx * Tf + vx * t - (vx / LAND) * t * t / 2; }
|
|
71
|
+
else { y = mix(bottom, b.y0, ease((f - tL) / RISE)); x = travel; }
|
|
72
|
+
return add([b.root[0], y, b.root[2]], scale(b.F, x));
|
|
73
|
+
};
|
|
74
|
+
// the turn starts 2 frames before take-off and ends 3 after touch-down
|
|
75
|
+
const pitchAt = f => turn * ease((f - tA + 2) / (Tf + 5)), frameAt = f => bodyFrame(b.h, pitchAt(f), 0);
|
|
76
|
+
// in the air the feet ride the body: from where they left the floor, through the tuck, to where they land, all in its frame
|
|
77
|
+
const local = (f, p) => apply(T(frameAt(f)), sub(p, hipsAt(f)));
|
|
78
|
+
const offL = { l: local(tA, pivot(ball("l"), b.h, TOE_OFF, b.feet.l)), r: local(tA, pivot(ball("r"), b.h, TOE_OFF, b.feet.r)) };
|
|
79
|
+
const onL = { l: local(tD, land("l")), r: local(tD, land("r")) };
|
|
80
|
+
const keys = [];
|
|
81
|
+
for (let f = t0; f <= tE + 1; f++) {
|
|
82
|
+
const pitch = f > tE ? 0 : pitchAt(f), B = bodyFrame(b.h, pitch, 0), hips = hipsAt(f), u = (f - tA) / Tf;
|
|
83
|
+
const tk = f > tA && f < tD ? window(u, 0.12, 0.34, 0.6, 0.82) : 0;
|
|
84
|
+
const set = { root: hips, yaw: b.h, pitch, roll: 0, armw_l: 0, armw_r: 0, fyaw_l: b.h, fyaw_r: b.h };
|
|
85
|
+
for (const k of ["l", "r"]) {
|
|
86
|
+
let foot, fp;
|
|
87
|
+
if (f <= tP) { foot = b.feet[k]; fp = 0; }
|
|
88
|
+
else if (f <= tA) { fp = TOE_OFF * ease((f - tP) / PUSH); foot = pivot(ball(k), b.h, fp, b.feet[k]); }
|
|
89
|
+
else if (f < tD) {
|
|
90
|
+
const a = 1 - ease(u / 0.2), c = ease((u - 0.72) / 0.28);
|
|
91
|
+
const mid = add(b.hipOff[k], mix3([0, -b.leg * 0.97, 0], [SIDE[k] * 0.02, -0.24, 0.16], tk));
|
|
92
|
+
foot = add(hips, apply(B, add(add(scale(offL[k], a), scale(onL[k], c)), scale(mid, 1 - a - c))));
|
|
93
|
+
fp = mix(mix(TOE_OFF, 30, ease(u / 0.2)), 0, ease((u - 0.7) / 0.3));
|
|
94
|
+
} else { foot = land(k); fp = 0; }
|
|
95
|
+
const aim = f > tA && f < tD ? footAim(b, k, pitch, 0, B, fp) : { fyaw: b.h, fpitch: fp };
|
|
96
|
+
Object.assign(set, { [`foot_${k}`]: foot, [`fpitch_${k}`]: aim.fpitch, [`fyaw_${k}`]: aim.fyaw, [`knee_${k}`]: poleFor(b.h, pitch, 0, B, [SIDE[k] * 0.05, 0.5 * tk, 1]) });
|
|
97
|
+
}
|
|
98
|
+
keys.push({ f, set });
|
|
99
|
+
}
|
|
100
|
+
// the arms, spine and head at the phase edges: back in the crouch, overhead at take-off, to the shins in the tuck,
|
|
101
|
+
// forward for the landing, then down
|
|
102
|
+
const head = kind === "back" ? [-12, 18] : [10, 25];
|
|
103
|
+
for (const [f, sw, bd, sp, hd] of [[t0, 0, 0, 0, 0], [tP, -45, 15, 20, 0], [tA, 160, 5, -8, head[0]], [Math.round(tA + 0.3 * Tf), 55, 85, 32, head[1]],
|
|
104
|
+
[Math.round(tA + 0.62 * Tf), 55, 85, 32, head[1]], [tD, 75, 25, 22, 0], [tL, 70, 25, 28, 0], [tE, 0, 0, 0, 0]]) {
|
|
105
|
+
keys.push({ f, set: { aswing_l: sw, aswing_r: sw, abend_l: bd, abend_r: bd, spine_pitch: sp, head_pitch: hd } });
|
|
106
|
+
}
|
|
107
|
+
return { keys, end: tE + 1 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cartwheel(ctx, b) {
|
|
111
|
+
const { args } = ctx, dir = args.side === "r" ? -1 : 1, rs = -dir; // toward the character's left the head goes to +x: roll -
|
|
112
|
+
const W = 12, ROT = 34, REC = 14, t0 = ctx.f0, tR = t0 + W, tC = tR + ROT, tE = tC + REC;
|
|
113
|
+
const lead = dir > 0 ? "l" : "r", trail = lead === "l" ? "r" : "l";
|
|
114
|
+
// spokes from the hips in the body's frame (x the body's left): the legs straddled, the arms overhead and apart
|
|
115
|
+
const spoke = { foot: k => add(b.hipOff[k], [SIDE[k] * 0.3, -b.leg * 0.93, 0]), hand: k => add(b.shoulder[k], [SIDE[k] * 0.32, b.arm * 0.88, 0]) };
|
|
116
|
+
const floor = { foot: b.ank, hand: 0.1 };
|
|
117
|
+
// the order the ends touch down rolling toward the lead side; the body pivots on each in turn like a wheel on its spokes
|
|
118
|
+
const order = [["foot", lead], ["hand", lead], ["hand", trail], ["foot", trail], ["foot", lead]];
|
|
119
|
+
const rollAt = f => f <= tR ? 0 : f <= tC ? rs * 360 * ease((f - tR) / ROT) : rs * 360;
|
|
120
|
+
const end = (f, hips, part, k) => add(hips, apply(bodyFrame(b.h, 0, rollAt(f)), spoke[part](k)));
|
|
121
|
+
// start: the trail foot stays, the lead foot steps out so both stand on their spokes; the hips over them
|
|
122
|
+
const B0 = bodyFrame(b.h, 0, 0), hips0 = sub(b.feet[trail], apply(B0, spoke.foot(trail))), stepTo = end(tR, hips0, "foot", lead);
|
|
123
|
+
stepTo[1] = b.ank;
|
|
124
|
+
const raw = [], plants = { foot: { l: [], r: [] }, hand: { l: [], r: [] } };
|
|
125
|
+
let at = 0, pivot = b.feet[lead].slice(), from = tR;
|
|
126
|
+
plants.foot[trail].push({ a: t0, b: tR, p: b.feet[trail] });
|
|
127
|
+
for (let f = t0; f <= tE + 1; f++) {
|
|
128
|
+
if (f <= tR) { raw.push(mix3(b.root, hips0, ease((f - t0) / W))); continue; }
|
|
129
|
+
if (f === tR + 1) pivot = stepTo.slice();
|
|
130
|
+
let hips = sub(pivot, apply(bodyFrame(b.h, 0, rollAt(f)), spoke[order[at][0]](order[at][1])));
|
|
131
|
+
const next = order[at + 1];
|
|
132
|
+
if (next && f <= tC) {
|
|
133
|
+
const e = end(f, hips, ...next);
|
|
134
|
+
if (e[1] <= floor[next[0]]) {
|
|
135
|
+
// the end touched between the frames: pivot where it crossed the floor, so the hips do not jump
|
|
136
|
+
const ep = end(f - 1, raw.at(-1), ...next), h1 = ep[1] - floor[next[0]], h2 = e[1] - floor[next[0]];
|
|
137
|
+
const cross = mix3(ep, e, h1 > 0 ? h1 / (h1 - h2) : 1);
|
|
138
|
+
plants[order[at][0]][order[at][1]].push({ a: from, b: f, p: pivot });
|
|
139
|
+
at++; from = f; pivot = [cross[0], floor[next[0]], cross[2]];
|
|
140
|
+
hips = sub(pivot, apply(bodyFrame(b.h, 0, rollAt(f)), spoke[next[0]](next[1])));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
raw.push(hips);
|
|
144
|
+
}
|
|
145
|
+
plants[order[at][0]][order[at][1]].push({ a: from, b: tE + 1, p: pivot });
|
|
146
|
+
// the trail foot that landed stays down to the end too
|
|
147
|
+
const last = order[order.length - 2];
|
|
148
|
+
if (at >= order.length - 1) plants[last[0]][last[1]].at(-1).b = tE + 1;
|
|
149
|
+
// the pivot path turns at each touch-down: blurred over a few frames (the planted ends absorb it), then the stand
|
|
150
|
+
const blur = raw.map((p, i) => { let s = [0, 0, 0], w = 0; for (let d = -3; d <= 3; d++) { const j = Math.min(raw.length - 1, Math.max(0, i + d)), g = Math.exp(-(d * d) / 4); s = add(s, scale(raw[j], g)); w += g; } return scale(s, 1 / w); });
|
|
151
|
+
const hipsAt = f => f <= tC ? blur[f - t0] : add(blur[tC - t0], [0, (b.y0 - blur[tC - t0][1]) * ease((f - tC) / REC), 0]);
|
|
152
|
+
// a planted end the turning body has rolled away from is drawn toward its hip or shoulder until the limb reaches it
|
|
153
|
+
const reach = (p, from, n) => { const v = sub(p, from), d = Math.hypot(...v); return d > n ? add(from, scale(v, n / d)) : p; };
|
|
154
|
+
// a hand or foot near its plant: easing onto it over the 6 frames before, held, then let go by following the body from
|
|
155
|
+
// where it left the floor (its place in the body's frame then) toward its spoke, so it lifts without a jolt
|
|
156
|
+
const touch = (part, k, f, free) => {
|
|
157
|
+
for (const r of plants[part][k]) {
|
|
158
|
+
if (f >= r.a - 6 && f <= r.b) return mix3(free, r.p, ease((f - r.a + 6) / 6));
|
|
159
|
+
if (f > r.b && f <= r.b + 10) {
|
|
160
|
+
const at = bodyFrame(b.h, 0, rollAt(r.b)), left = apply(T(at), sub(r.p, hipsAt(r.b)));
|
|
161
|
+
const follow = add(hipsAt(f), apply(bodyFrame(b.h, 0, rollAt(f)), mix3(left, spoke[part](k), ease((f - r.b - 1) / 9))));
|
|
162
|
+
return mix3(r.p, follow, ease((f - r.b) / 8));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return free;
|
|
166
|
+
};
|
|
167
|
+
const planted = (part, k, f) => plants[part][k].some(r => f >= r.a && f <= r.b);
|
|
168
|
+
// where the feet stand after the roll: a planted one where it is, else under its spoke at the roll's end
|
|
169
|
+
const landed = Object.fromEntries(["l", "r"].map(k => { const p = planted("foot", k, tC) ? plants.foot[k].at(-1).p.slice() : end(tC, hipsAt(tC), "foot", k); p[1] = b.ank; return [k, p]; }));
|
|
170
|
+
const keys = [];
|
|
171
|
+
for (let f = t0; f <= tE + 1; f++) {
|
|
172
|
+
const roll = f === tE + 1 ? 0 : rollAt(f), B = bodyFrame(b.h, 0, rollAt(f)), hips = hipsAt(f);
|
|
173
|
+
const set = { root: hips, yaw: b.h, pitch: 0, roll };
|
|
174
|
+
for (const k of ["l", "r"]) {
|
|
175
|
+
let foot;
|
|
176
|
+
if (f <= tR && k === lead) { const w = ease((f - t0 - 1) / (W - 4)); foot = add(mix3(b.feet[k], stepTo, w), [0, 0.07 * Math.sin(Math.PI * w), 0]); }
|
|
177
|
+
else if (f > tC) foot = landed[k];
|
|
178
|
+
else {
|
|
179
|
+
const down = ease((f - tC + 4) / 4);
|
|
180
|
+
foot = reach(mix3(touch("foot", k, f, end(f, hips, "foot", k)), landed[k], down), add(hips, apply(B, b.hipOff[k])), b.leg * 0.995);
|
|
181
|
+
}
|
|
182
|
+
const aim = f > tR && f <= tC ? footAim(b, k, 0, rollAt(f), B, 0) : { fyaw: b.h, fpitch: 0 };
|
|
183
|
+
Object.assign(set, { [`foot_${k}`]: foot, [`fyaw_${k}`]: aim.fyaw, [`fpitch_${k}`]: aim.fpitch, [`knee_${k}`]: poleFor(b.h, 0, roll, B, [SIDE[k] * 0.05, 0, 1]),
|
|
184
|
+
[`hand_${k}`]: reach(f > tR && f <= tC ? touch("hand", k, f, end(f, hips, "hand", k)) : end(f, hips, "hand", k), add(hips, apply(B, b.shoulder[k])), b.arm * 0.995), [`elbow_${k}`]: poleFor(b.h, 0, roll, B, [SIDE[k] * 0.4, 0, -1]),
|
|
185
|
+
[`armw_${k}`]: f <= tR ? 0 : f <= tC ? ease((f - tR) / 6) : 1 - ease((f - tC) / 8) });
|
|
186
|
+
}
|
|
187
|
+
// the FK arms overhead along the hand spokes, so the hand eases onto its IK target without a jump
|
|
188
|
+
set.arm_rest = f <= tR ? mix(72, -56, ease((f - t0) / W)) : f <= tC ? -56 : mix(-56, 72, ease((f - tC) / REC));
|
|
189
|
+
keys.push({ f, set });
|
|
190
|
+
}
|
|
191
|
+
return { keys, end: tE + 1 };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** flip({ kind: "back" | "front" | "cartwheel", travel, side, heading }) */
|
|
195
|
+
export function flip(ctx) {
|
|
196
|
+
const kind = ctx.args.kind ?? "back";
|
|
197
|
+
if (!["back", "front", "cartwheel"].includes(kind)) skillError(ctx.tag, `kind must be "back", "front" or "cartwheel", got ${JSON.stringify(kind)}`);
|
|
198
|
+
const b = base(ctx);
|
|
199
|
+
return kind === "cartwheel" ? cartwheel(ctx, b) : tuck(ctx, b, kind);
|
|
200
|
+
}
|