SOURCE / PINNED RELEASE
Made of little things.
Mini Samurai
- Release
- a7beb798adbc…
- Author-recorded commit
- 4f97be1515b8…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-3b28f0efc5b
Archive hash verified: 401ec9347ab8f8cd…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import * as THREE from 'three';
/**
* A full-body pose in the fighter's local space (+Z forward, +Y up, +X is the
* fighter's LEFT). Limbs are driven by IK targets, the trunk by rotations.
* Hand targets move with the hips automatically (see Rig.apply).
*/
export interface Pose {
/** Hips position relative to the feet origin. Rest is (0, 0.43, 0). */
hips: THREE.Vector3;
hipsQ: THREE.Quaternion;
spineQ: THREE.Quaternion;
chestQ: THREE.Quaternion;
headQ: THREE.Quaternion;
/** Ankle targets. */
footL: THREE.Vector3;
footR: THREE.Vector3;
/** Foot pitch in radians (toe up = negative). */
footLPitch: number;
footRPitch: number;
/** Wrist targets and their orientation (hand +Y = held item's blade, +Z = cutting edge). */
handR: THREE.Vector3;
handRQ: THREE.Quaternion;
handL: THREE.Vector3;
handLQ: THREE.Quaternion;
/** 1 = left hand clamps onto the held item's grip, 0 = left hand follows handL. */
twoHand: number;
/** Elbow hints (direction the elbow points, local space). */
elbowR: THREE.Vector3;
elbowL: THREE.Vector3;
}
export const REST_HIPS_Y = 0.43;
const RAD = Math.PI / 180;
export function emptyPose(): Pose {
return {
hips: new THREE.Vector3(0, REST_HIPS_Y, 0),
hipsQ: new THREE.Quaternion(),
spineQ: new THREE.Quaternion(),
chestQ: new THREE.Quaternion(),
headQ: new THREE.Quaternion(),
footL: new THREE.Vector3(0.08, 0.035, 0),
footR: new THREE.Vector3(-0.08, 0.035, 0),
footLPitch: 0,
footRPitch: 0,
handR: new THREE.Vector3(-0.2, 0.42, 0.05),
handRQ: new THREE.Quaternion(),
handL: new THREE.Vector3(0.2, 0.42, 0.05),
handLQ: new THREE.Quaternion(),
twoHand: 0,
elbowR: new THREE.Vector3(-1, -0.3, -0.6).normalize(),
elbowL: new THREE.Vector3(1, -0.3, -0.6).normalize(),
};
}
export function copyPose(out: Pose, p: Pose): Pose {
out.hips.copy(p.hips);
out.hipsQ.copy(p.hipsQ);
out.spineQ.copy(p.spineQ);
out.chestQ.copy(p.chestQ);
out.headQ.copy(p.headQ);
out.footL.copy(p.footL);
out.footR.copy(p.footR);
out.footLPitch = p.footLPitch;
out.footRPitch = p.footRPitch;
out.handR.copy(p.handR);
out.handRQ.copy(p.handRQ);
out.handL.copy(p.handL);
out.handLQ.copy(p.handLQ);
out.twoHand = p.twoHand;
out.elbowR.copy(p.elbowR);
out.elbowL.copy(p.elbowL);
return out;
}
/** out = a → b by t (out may alias a). */
export function blendPose(out: Pose, a: Pose, b: Pose, t: number): Pose {
out.hips.lerpVectors(a.hips, b.hips, t);
out.hipsQ.slerpQuaternions(a.hipsQ, b.hipsQ, t);
out.spineQ.slerpQuaternions(a.spineQ, b.spineQ, t);
out.chestQ.slerpQuaternions(a.chestQ, b.chestQ, t);
out.headQ.slerpQuaternions(a.headQ, b.headQ, t);
out.footL.lerpVectors(a.footL, b.footL, t);
out.footR.lerpVectors(a.footR, b.footR, t);
out.footLPitch = a.footLPitch + (b.footLPitch - a.footLPitch) * t;
out.footRPitch = a.footRPitch + (b.footRPitch - a.footRPitch) * t;
out.handR.lerpVectors(a.handR, b.handR, t);
out.handRQ.slerpQuaternions(a.handRQ, b.handRQ, t);
out.handL.lerpVectors(a.handL, b.handL, t);
out.handLQ.slerpQuaternions(a.handLQ, b.handLQ, t);
out.twoHand = a.twoHand + (b.twoHand - a.twoHand) * t;
out.elbowR.lerpVectors(a.elbowR, b.elbowR, t).normalize();
out.elbowL.lerpVectors(a.elbowL, b.elbowL, t).normalize();
return out;
}
// ---- Authoring helpers -------------------------------------------------------
/** Rotation from degrees: pitch (x, + leans forward), yaw (y, + turns left), roll (z). */
export function rot(pitch = 0, yaw = 0, roll = 0): THREE.Quaternion {
return new THREE.Quaternion().setFromEuler(new THREE.Euler(pitch * RAD, yaw * RAD, roll * RAD, 'YXZ'));
}
export function v(x: number, y: number, z: number): THREE.Vector3 {
return new THREE.Vector3(x, y, z);
}
const _x = new THREE.Vector3();
const _y = new THREE.Vector3();
const _z = new THREE.Vector3();
const _m = new THREE.Matrix4();
/**
* Orientation for a held item: its blade points along `blade`, and its cutting
* edge faces `edge` (re-orthogonalised against the blade).
*/
export function grip(blade: THREE.Vector3, edge: THREE.Vector3): THREE.Quaternion {
_y.copy(blade).normalize();
_z.copy(edge).addScaledVector(_y, -edge.dot(_y)).normalize();
_x.crossVectors(_y, _z);
_m.makeBasis(_x, _y, _z);
return new THREE.Quaternion().setFromRotationMatrix(_m);
}
/** Orientation for a blade swung around `axis`: the edge leads the motion. */
export function swing(blade: THREE.Vector3, axis: THREE.Vector3): THREE.Quaternion {
const edge = new THREE.Vector3().crossVectors(axis, blade);
return grip(blade, edge);
}
/** Rotation axis that carries direction a toward direction b. */
export function swingAxis(a: THREE.Vector3, b: THREE.Vector3): THREE.Vector3 {
return new THREE.Vector3().crossVectors(a.clone().normalize(), b.clone().normalize()).normalize();
}
/** Partial pose for authoring: unspecified fields come from a base pose. */
export type PoseDraft = Partial<Pose>;
export function makePose(base: Pose, draft: PoseDraft): Pose {
const p = copyPose(emptyPose(), base);
for (const key of Object.keys(draft) as (keyof Pose)[]) {
const value = draft[key];
if (value === undefined) continue;
const target = p[key];
if (typeof value === 'number') (p as unknown as Record<string, number>)[key] = value;
else if (target instanceof THREE.Vector3) target.copy(value as THREE.Vector3);
else if (target instanceof THREE.Quaternion) target.copy(value as THREE.Quaternion);
}
return p;
}
// ---- Clips -------------------------------------------------------------------
export type Ease = (t: number) => number;
export const ease = {
linear: (t: number) => t,
in: (t: number) => t * t * t,
out: (t: number) => 1 - (1 - t) ** 3,
inOut: (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2),
/** Snappy strike: slow start, very fast middle, soft end. */
strike: (t: number) => (t < 0.5 ? 16 * t ** 5 : 1 - (-2 * t + 2) ** 5 / 2),
outBack: (t: number) => {
const c = 1.9;
return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2;
},
};
export interface Key {
t: number;
pose: Pose;
/** Easing used while travelling INTO this key from the previous one. */
ease?: Ease;
}
/** Keyframed clip. Hand positions use Catmull-Rom so swings travel in arcs. */
export class Clip {
readonly duration: number;
constructor(readonly keys: Key[], readonly loop = false) {
this.duration = keys[keys.length - 1]!.t;
}
sample(time: number, out: Pose): Pose {
const keys = this.keys;
let t = this.loop ? ((time % this.duration) + this.duration) % this.duration : Math.min(Math.max(time, 0), this.duration);
if (t <= keys[0]!.t) return copyPose(out, keys[0]!.pose);
let i = 1;
while (i < keys.length - 1 && keys[i]!.t < t) i++;
const a = keys[i - 1]!;
const b = keys[i]!;
const span = b.t - a.t;
const raw = span > 0 ? Math.min(1, (t - a.t) / span) : 1;
const u = (b.ease ?? ease.inOut)(raw);
blendPose(out, a.pose, b.pose, u);
// Curve the hands through neighbouring keys so strikes sweep instead of sliding.
const prev = keys[Math.max(0, i - 2)]!.pose;
const next = keys[Math.min(keys.length - 1, i + 1)]!.pose;
catmull(out.handR, prev.handR, a.pose.handR, b.pose.handR, next.handR, u);
catmull(out.handL, prev.handL, a.pose.handL, b.pose.handL, next.handL, u);
return out;
}
}
function catmull(out: THREE.Vector3, p0: THREE.Vector3, p1: THREE.Vector3, p2: THREE.Vector3, p3: THREE.Vector3, t: number): void {
const t2 = t * t;
const t3 = t2 * t;
for (const k of ['x', 'y', 'z'] as const) {
out[k] =
0.5 *
(2 * p1[k] + (-p0[k] + p2[k]) * t + (2 * p0[k] - 5 * p1[k] + 4 * p2[k] - p3[k]) * t2 + (-p0[k] + 3 * p1[k] - 3 * p2[k] + p3[k]) * t3);
}
}
