Compare commits

...

2 Commits

4 changed files with 148 additions and 23 deletions

View File

@ -1,5 +1,5 @@
import { CoupleRole, DancerIdentity, Rotation, normalizeRotation } from "./danceCommon.js";
import { DancerSetPosition, DancersSetPositions, Hand, OffsetEquals, OffsetMinus, OffsetPlus, OffsetRotate, OffsetTimes, OffsetTranspose, offsetZero, setHeight } from "./rendererConstants.js";
import { DancerSetPosition, DancersSetPositions, Hand, OffsetDistance, OffsetEquals, OffsetMinus, OffsetPlus, OffsetRotate, OffsetTimes, OffsetTranspose, offsetZero, setHeight } from "./rendererConstants.js";
import { Offset, leftShoulder, rightShoulder, degreesToRadians, radiansToDegrees } from "./rendererConstants.js";
export enum AnimationKind {
@ -369,17 +369,16 @@ export class StepWideLinearAnimationSegment extends AnimationSegment {
private readonly movementAngle: number;
private readonly handTransitionProgress: number;
private readonly progressCenter?: number;
private readonly center?: Offset;
private readonly actualCenter: Offset;
private readonly hands: Map<Hand, HandAnimation>;
private readonly facing: "Start" | "Forward";
constructor({ beats, startPosition, endPosition, distanceAtMidpoint, center, hands, facing }: {
constructor({ beats, startPosition, endPosition, distanceAtMidpoint, otherPath, hands, facing }: {
beats: number;
startPosition: DancerSetPosition;
endPosition: DancerSetPosition;
distanceAtMidpoint: number;
center?: Offset;
otherPath?: { start: Offset, end: Offset };
hands?: Map<Hand, HandAnimation>;
facing?: "Start" | "Forward";
}) {
@ -403,13 +402,51 @@ export class StepWideLinearAnimationSegment extends AnimationSegment {
// Center is the point where the dancer is closest to the other dancer.
// If omitted, it's assumed the movement is symmetrical, so that happens halfway through the move.
// Otherwise, find the point on the line between the start and end closest to the center.
if (center) {
this.actualCenter = this.center = center;
this.progressCenter = ((center.x - this.startPosition.position.x) * vector.x
+ (center.y - this.startPosition.position.y) * vector.y)
/ norm;
} else {
this.actualCenter = OffsetPlus(this.startPosition.position, OffsetTimes(vector, 0.5));
this.actualCenter = OffsetPlus(this.startPosition.position, OffsetTimes(vector, 0.5));
if (otherPath) {
/*
(xa(t), ya(t))
(xb(t), yb(t))
(xa_0 + xa_m*t, ya_0, ya_m*t)
(xb_0 + xb_m*t, yb_0, yb_m*t)
// can omit the sqrt
MIN[((xa_0 + xa_m*t) - (xb_0 + xb_m*t))**2 + ((ya_0, ya_m*t) - (yb_0, yb_m*t))**2]
MIN[(xa_0-xb_0 + (xa_m-xb_m)*t)**2 + (ya_0-yb_0 + (ya_m-yb_m)*t)**2]
MIN[(x_0 + x_m*t)**2 + (y_0 + y_m*t)**2]
MIN[(x_0**2 + 2x_0*x_m*t + x_m**2*t**2) + (y_0**2 + 2y_0*y_m*t + y_m**2*t**2)]
MIN[(x_0**2+y_0**2 + (2x_0*x_m+2y_0*y_m)*t + (x_m**2+y_m**2)*t**2)]
a = x_m**2+y_m**2 = (xa_m-xb_m)**2 + (ya_m-yb_m)**2
b = (2x_0*x_m+2y_0*y_m) = 2*((xa_0-xb_0)*(xa_m-xb_m) + (ya_0-yb_0)*(ya_m-yb_m))
t = -b/2a
*/
const otherVector = OffsetMinus(otherPath.end, otherPath.start);
const m = OffsetMinus(otherVector, vector);
const i = OffsetMinus(otherPath.start, this.startPosition.position);
const a = m.x*m.x + m.y*m.y;
const b = 2*(i.x*m.x + i.y*m.y);
const tMin = -b/(2*a);
const t = tMin >= 0 && tMin <= 1 ? tMin : undefined;
if (t !== undefined) {
this.progressCenter = t;
this.actualCenter = interpolateLinearOffset(t, this.startPosition.position, this.endPosition.position);
const otherCenter = interpolateLinearOffset(t, otherPath.start, otherPath.end);
const otherSideways = OffsetMinus(this.actualCenter, otherCenter);
const sidewaysLinedUp = Math.sign(otherSideways.x) === Math.sign(sideways.x) && Math.sign(otherSideways.y) === Math.sign(sideways.y);
const distanceAdjustment = OffsetDistance(this.actualCenter, otherCenter) * (sidewaysLinedUp ? +1 : -1);
if (this.distanceAtMidpoint > 0) {
this.distanceAtMidpoint = Math.max(0, this.distanceAtMidpoint - distanceAdjustment);
} else {
this.distanceAtMidpoint = Math.min(0, this.distanceAtMidpoint + distanceAdjustment);
}
}
}
this.handTransitionProgress = 0.5 / beats;

View File

@ -1066,6 +1066,7 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
side: hand,
withHands: true,
facing: "Start",
otherPath: "Swap",
}
};
@ -1149,6 +1150,7 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
side: hand,
withHands: true,
facing: "Forward",
otherPath: "Swap",
}
},
prevEnd => ({
@ -1581,6 +1583,7 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
side: passShoulder,
withHands: false,
facing: "Start",
otherPath: "Swap",
},
}], startPos);
} else {
@ -1608,16 +1611,18 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
return handleCircleMove(({startPos}) => {
const startingPos = { ...startPos, facing: startPos.which.facingAcross() };
const swappedPos = { ...startingPos, which: startingPos.which.swapAcross() };
return combine([
{
beats: move.beats / 2,
endPosition: {...startingPos, which: startingPos.which.swapAcross()},
endPosition: swappedPos,
movementPattern: {
kind: SemanticAnimationKind.PassBy,
side: Hand.Right,
withHands: true,
facing: "Start",
around: startingPos.which.topBottomSide(),
otherPath: "Swap",
},
},
{
@ -1635,6 +1640,11 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
});
case "hey":
type HeyStep = {
kind: "StandStill" | "Loop" | "CenterPass" | "EndsPassIn" | "EndsPassOut",
endPosition: SemanticPosition,
}
if (move.parameters.dir !== "across") {
throw new Error("Unsupported hey direction: " + move.parameters.dir);
}
@ -1657,12 +1667,77 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
const firstPassInCenter: boolean = dancerIsPair(move.parameters.who);
const centerShoulder = firstPassInCenter === move.parameters.shoulder ? Hand.Right : Hand.Left;
const endsShoulder = centerShoulder.opposite();
return handleMove(({ id, startPos }) => {
const endsInCircle = startPos.kind === PositionKind.Circle;
type HeyStep = {
kind: "StandStill" | "Loop" | "CenterPass" | "EndsPassIn" | "EndsPassOut",
endPosition: SemanticPosition,
function fixupHeyOtherPath(withoutOtherPath: Map<DancerIdentity, (LowLevelMove & { heyStep?: HeyStep })[]>): Map<DancerIdentity, LowLevelMove[]> {
const numSteps = withoutOtherPath.get(DancerIdentity.OnesLark)!.length;
for (let i = 0; i < numSteps; i++) {
for (const id of withoutOtherPath.keys()) {
const lowLevelMove = withoutOtherPath.get(id)![i];
if (lowLevelMove.movementPattern.kind !== SemanticAnimationKind.PassBy
|| !lowLevelMove.heyStep
|| lowLevelMove.movementPattern.otherPath) {
continue;
}
const heyStepKind = lowLevelMove.heyStep.kind;
let foundPair = false;
for (const otherId of withoutOtherPath.keys()) {
const otherLowLevelMove = withoutOtherPath.get(otherId)![i];
if (id === otherId
|| otherLowLevelMove.movementPattern.kind !== SemanticAnimationKind.PassBy
|| !otherLowLevelMove.heyStep
|| otherLowLevelMove.movementPattern.otherPath) {
continue;
}
const otherHeyStepKind = otherLowLevelMove.heyStep.kind;
if (heyStepKind === "CenterPass" && otherHeyStepKind === "CenterPass"
|| (lowLevelMove.startPosition.which.leftRightSide() === otherLowLevelMove.startPosition.which.leftRightSide()
&& (heyStepKind === "EndsPassIn" && otherHeyStepKind === "EndsPassOut"
|| heyStepKind === "EndsPassOut" && otherHeyStepKind === "EndsPassIn"))) {
lowLevelMove.movementPattern.otherPath = {
start: { ...otherLowLevelMove.startPosition, setOffset: lowLevelMove.startPosition.setOffset, lineOffset: lowLevelMove.startPosition.lineOffset },
end: { ...otherLowLevelMove.endPosition, setOffset: lowLevelMove.endPosition.setOffset, lineOffset: lowLevelMove.endPosition.lineOffset },
}
otherLowLevelMove.movementPattern.otherPath = {
start: { ...lowLevelMove.startPosition, setOffset: otherLowLevelMove.startPosition.setOffset, lineOffset: otherLowLevelMove.startPosition.lineOffset },
end: { ...lowLevelMove.endPosition, setOffset: otherLowLevelMove.endPosition.setOffset, lineOffset: otherLowLevelMove.endPosition.lineOffset },
}
foundPair = true;
break;
}
}
if (!foundPair && heyStepKind === "EndsPassOut") {
// Then other is standing still.
const pos = {
...([...withoutOtherPath.values()]
.map(otherMoves => otherMoves[i])
.filter(m => m.movementPattern.kind === SemanticAnimationKind.StandStill
&& m.endPosition.which.leftRightSide() === lowLevelMove.endPosition.which.leftRightSide())
[0].endPosition),
setOffset: lowLevelMove.startPosition.setOffset, lineOffset: lowLevelMove.startPosition.lineOffset
}
lowLevelMove.movementPattern.otherPath = { start: pos, end: pos };
}
}
for (const id of withoutOtherPath.keys()) {
const lowLevelMove = withoutOtherPath.get(id)![i];
if (lowLevelMove.movementPattern.kind === SemanticAnimationKind.PassBy
&& !lowLevelMove.movementPattern.otherPath) {
throw new Error("Failed to fill in otherPath for " + id + " on hey step " + i);
}
}
}
// Object was mutated.
return withoutOtherPath;
}
return fixupHeyOtherPath(handleMove(({ id, startPos }) => {
const endsInCircle = startPos.kind === PositionKind.Circle;
function heyStepToPartialLowLevelMove(heyStep: HeyStep): PartialLowLevelMove & { heyStep: HeyStep } {
return {
beats: heyPartBeats,
@ -1680,6 +1755,7 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
withHands: false,
side: heyStep.kind === "CenterPass" ? centerShoulder : endsShoulder,
facing: "Start",
otherPath: undefined!, // Placeholder, fixup later.
},
heyStep,
};
@ -1819,7 +1895,7 @@ function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: {
heySteps.push(nextHeyStep);
}
return combine(heySteps.map(heyStepToPartialLowLevelMove), { ...startingPos, hands: undefined });
});
}));
case "turn alone":
if (move.parameters.who !== "everyone" || move.beats !== 0) {
@ -2028,4 +2104,4 @@ export function loadDance(dance: LibFigureDance, formation: common.StartFormatio
interpretedAnimation = animateFromLowLevelMoves(interpretedDance);
return interpretedAnimation;
}
}

View File

@ -99,6 +99,11 @@ export type SemanticAnimation = {
around: CircleSideOrCenter,
otherPath: "Swap" | {
start: SemanticPosition,
end: SemanticPosition,
}
side: Hand,
// If true, pull by the specified hand, if false, just pass by that side without hands.
@ -729,9 +734,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
})
];
case SemanticAnimationKind.PassBy:
const passByCenter = CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
const wide = move.movementPattern.around === "Center";
const width = wide ? dancerWidth : dancerWidth/2;
const width = dancerWidth/2;
const distanceAtMidpoint = move.movementPattern.side == Hand.Left ? +width : -width;
// "Pull By" is just "Pass By" with hands.
const passByHands = move.movementPattern.withHands
@ -759,7 +762,10 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
startPosition: startSetPosition,
endPosition: { ...endSetPosition, rotation: endRotation },
distanceAtMidpoint,
center: move.movementPattern.around === "Center" ? passByCenter : undefined, // TODO Better center?
otherPath: move.movementPattern.otherPath === "Swap" ? undefined : {
start: SemanticToSetPosition(move.movementPattern.otherPath.start).position,
end: SemanticToSetPosition(move.movementPattern.otherPath.end).position,
},
hands: passByHands,
facing: move.movementPattern.facing,
}),

View File

@ -48,6 +48,12 @@ export function OffsetEquals(a: Offset, b: Offset): boolean {
return a.x === b.x && a.y === b.y;
}
export function OffsetDistance(a: Offset, b: Offset): number {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
export interface DancerSetPosition {
// Position of the dancer relative to the center of their set.
position: Offset;