feat: add sheep idle scratching and minute-interval double hops
This commit is contained in:
@@ -25,5 +25,14 @@ export const CHARACTER = {
|
|||||||
torsoDepth: 0.014,
|
torsoDepth: 0.014,
|
||||||
headLift: 0.018,
|
headLift: 0.018,
|
||||||
shoulderLift: 0.008,
|
shoulderLift: 0.008,
|
||||||
|
fidget: {
|
||||||
|
minimumWaitSeconds: 8,
|
||||||
|
maximumWaitSeconds: 15,
|
||||||
|
scratchSeconds: 2.4,
|
||||||
|
hopIntervalSeconds: 60,
|
||||||
|
hopSeconds: 1,
|
||||||
|
hopHeight: 0.085,
|
||||||
|
hopCrouch: 0.025,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
|||||||
.filter((part): part is Object3D => Boolean(part))
|
.filter((part): part is Object3D => Boolean(part))
|
||||||
.map(part => ({ part, position: part.position.clone(), scale: part.scale.clone() }));
|
.map(part => ({ part, position: part.position.clone(), scale: part.scale.clone() }));
|
||||||
const animateIdle = typeof matchMedia === 'undefined' || !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
const animateIdle = typeof matchMedia === 'undefined' || !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
const modelRestPosition = model.position.clone();
|
||||||
const originals: Object3D[] = [];
|
const originals: Object3D[] = [];
|
||||||
group.traverse(object => originals.push(object));
|
group.traverse(object => originals.push(object));
|
||||||
function bindRig(group: Group, ownsAssets: boolean): Sheep {
|
function bindRig(group: Group, ownsAssets: boolean): Sheep {
|
||||||
@@ -123,7 +124,14 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
|||||||
const rigFlashlight = copy(gear?.flashlight);
|
const rigFlashlight = copy(gear?.flashlight);
|
||||||
const rigBreathingParts = breathingParts.map(rest => ({ ...rest, part: copy(rest.part)! }));
|
const rigBreathingParts = breathingParts.map(rest => ({ ...rest, part: copy(rest.part)! }));
|
||||||
const rigBody = copy(body), rigCoat = copy(gear?.raincoat);
|
const rigBody = copy(body), rigCoat = copy(gear?.raincoat);
|
||||||
|
const rigModel = copy(model)!;
|
||||||
const breathPhase = Math.random() * Math.PI * 2;
|
const breathPhase = Math.random() * Math.PI * 2;
|
||||||
|
const fidget = CHARACTER.idle.fidget;
|
||||||
|
const nextFidgetDelay = () => MathUtils.lerp(fidget.minimumWaitSeconds, fidget.maximumWaitSeconds, Math.random());
|
||||||
|
let fidgetWait = nextFidgetDelay();
|
||||||
|
let hopWait: number = fidget.hopIntervalSeconds;
|
||||||
|
let fidgetTime = 0;
|
||||||
|
let fidgetAction: 'scratch' | 'hop' | undefined;
|
||||||
let idleWeight = 0;
|
let idleWeight = 0;
|
||||||
let flashlightOn = false;
|
let flashlightOn = false;
|
||||||
let flashlightPose = 0;
|
let flashlightPose = 0;
|
||||||
@@ -163,6 +171,35 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
|||||||
pose(emote: Emote, elapsed: number, moving: boolean, time: number) {
|
pose(emote: Emote, elapsed: number, moving: boolean, time: number) {
|
||||||
const delta = Math.max(0, time - (previousPoseTime ?? time));
|
const delta = Math.max(0, time - (previousPoseTime ?? time));
|
||||||
const idle = animateIdle && !moving && emote === 'idle';
|
const idle = animateIdle && !moving && emote === 'idle';
|
||||||
|
rigModel.position.copy(modelRestPosition);
|
||||||
|
if (!idle) {
|
||||||
|
fidgetWait = nextFidgetDelay();
|
||||||
|
hopWait = fidget.hopIntervalSeconds;
|
||||||
|
fidgetTime = 0;
|
||||||
|
fidgetAction = undefined;
|
||||||
|
} else {
|
||||||
|
// Do not fast-forward gestures after a background tab resumes.
|
||||||
|
const idleDelta = Math.min(delta, .1);
|
||||||
|
hopWait -= idleDelta;
|
||||||
|
if (hopWait <= 0) {
|
||||||
|
fidgetAction = 'hop';
|
||||||
|
fidgetTime = 0;
|
||||||
|
hopWait += fidget.hopIntervalSeconds;
|
||||||
|
} else if (!fidgetAction) {
|
||||||
|
fidgetWait -= idleDelta;
|
||||||
|
if (fidgetWait <= 0) {
|
||||||
|
fidgetAction = 'scratch';
|
||||||
|
fidgetTime = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fidgetTime += idleDelta;
|
||||||
|
const duration = fidgetAction === 'scratch' ? fidget.scratchSeconds : fidget.hopSeconds;
|
||||||
|
if (fidgetTime >= duration) {
|
||||||
|
fidgetAction = undefined;
|
||||||
|
fidgetWait = nextFidgetDelay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const previousIdleWeight = idleWeight;
|
const previousIdleWeight = idleWeight;
|
||||||
const idleStep = delta / CHARACTER.idle.transitionSeconds;
|
const idleStep = delta / CHARACTER.idle.transitionSeconds;
|
||||||
idleWeight = idle ? Math.min(1, idleWeight + idleStep) : Math.max(0, idleWeight - idleStep);
|
idleWeight = idle ? Math.min(1, idleWeight + idleStep) : Math.max(0, idleWeight - idleStep);
|
||||||
@@ -209,6 +246,39 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
|||||||
joints.LegRight.rotateX(-stride);
|
joints.LegRight.rotateX(-stride);
|
||||||
joints.ArmLeft.rotateX(-stride * .6);
|
joints.ArmLeft.rotateX(-stride * .6);
|
||||||
joints.ArmRight.rotateX(stride * .6);
|
joints.ArmRight.rotateX(stride * .6);
|
||||||
|
} else if (fidgetAction === 'scratch') {
|
||||||
|
const blend = MathUtils.smoothstep(fidgetTime, 0, .4)
|
||||||
|
* (1 - MathUtils.smoothstep(fidgetTime, fidget.scratchSeconds - .4, fidget.scratchSeconds));
|
||||||
|
const left = carryingUmbrella;
|
||||||
|
const arm = joints[left ? 'ArmLeft' : 'ArmRight'];
|
||||||
|
// Only scratch with an empty hand, including while equipment blends out.
|
||||||
|
if (!left || (!flashlightOn && flashlightPose === 0)) {
|
||||||
|
pointArm(left ? 'ArmLeft' : 'ArmRight', aim.set(left ? -.3 : .3, .95, .12 + Math.sin(fidgetTime * 22) * .12));
|
||||||
|
arm.quaternion.slerp(rest[left ? 'ArmLeft' : 'ArmRight'], 1 - blend);
|
||||||
|
joints.Head.rotateZ((left ? -.1 : .1) * blend);
|
||||||
|
}
|
||||||
|
} else if (fidgetAction === 'hop') {
|
||||||
|
const progress = fidgetTime / fidget.hopSeconds * 2;
|
||||||
|
const phase = progress % 1;
|
||||||
|
const strength = progress < 1 ? 1 : .7;
|
||||||
|
// Each small hop has a planted preparation, a short flight and a soft landing.
|
||||||
|
const flight = MathUtils.clamp((phase - .24) / .48, 0, 1);
|
||||||
|
const bounce = 4 * flight * (1 - flight) * strength;
|
||||||
|
const crouch = (phase < .24 ? Math.sin(phase / .24 * Math.PI)
|
||||||
|
: phase > .72 ? Math.sin((phase - .72) / .28 * Math.PI) : 0) * strength;
|
||||||
|
rigModel.position.y += bounce * fidget.hopHeight;
|
||||||
|
for (const { part } of rigBreathingParts) {
|
||||||
|
if (part === rigBody || part === rigCoat) {
|
||||||
|
part.scale.y *= 1 - crouch * fidget.hopCrouch;
|
||||||
|
if (part === rigCoat && body) part.position.y += body.position.y * crouch * fidget.hopCrouch;
|
||||||
|
} else {
|
||||||
|
part.position.y -= crouch * fidget.hopCrouch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
joints.LegLeft.rotateX(-bounce * .1);
|
||||||
|
joints.LegRight.rotateX(-bounce * .1);
|
||||||
|
joints.ArmLeft.rotateZ(-bounce * .08);
|
||||||
|
joints.ArmRight.rotateZ(bounce * .08);
|
||||||
}
|
}
|
||||||
// Keep the grip stable while the free arm and legs perform their own animations.
|
// Keep the grip stable while the free arm and legs perform their own animations.
|
||||||
if (umbrellaPose > 0) {
|
if (umbrellaPose > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user