forked from perelman/contra-renderer
2146 lines
84 KiB
TypeScript
2146 lines
84 KiB
TypeScript
import * as animation from "./animation.js";
|
|
import { CoupleRole, DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
|
|
import * as common from "./danceCommon.js";
|
|
import { Hand, setDistance, setHeight } from "./rendererConstants.js";
|
|
import { nameLibFigureParameters, Move, LibFigureDance, chooser_pairz } from "./libfigureMapper.js";
|
|
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js";
|
|
import { BalanceWeight, CirclePosition, CircleSide, CircleSideOrCenter, DancerDistance, Facing, HandConnection, HandTo, LongLines, PositionKind, SemanticPosition, ShortLinesPosition, StarGrip, handsFourImproper, handsInCircle, oppositeFacing } from "./interpreterCommon.js";
|
|
import { dancerIsPair } from "./libfigure/util.js";
|
|
|
|
|
|
function handsInShortLine({ which, facing, wavy }: { which: ShortLinesPosition; facing: Facing.Up | Facing.Down; wavy: boolean; }): Map<Hand, HandConnection> {
|
|
return which.isMiddle() ? new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }],
|
|
[Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }],
|
|
]) : new Map<Hand, HandConnection>([
|
|
which.isLeft() === (facing === Facing.Up)
|
|
? [Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }]
|
|
: [Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }]
|
|
]);
|
|
}
|
|
function handsInLine(args: { wavy: boolean } & ({ which: ShortLinesPosition, facing: Facing.Up | Facing.Down } | { which: CirclePosition })) {
|
|
if (args.which instanceof ShortLinesPosition && /*always true, type system limitation*/ 'facing' in args) {
|
|
return handsInShortLine(args);
|
|
} else {
|
|
return new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: args.wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }],
|
|
[Hand.Right, { hand: args.wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }],
|
|
]);
|
|
}
|
|
}
|
|
function handToDancerToSideInCircleFacingAcross(which: CirclePosition): Map<Hand, HandConnection> {
|
|
return new Map<Hand, HandConnection>([
|
|
which.isOnLeftLookingAcross()
|
|
? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }]
|
|
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
|
|
]);
|
|
}
|
|
function handToDancerToSideInCircleFacingUpOrDown(which: CirclePosition): Map<Hand, HandConnection> {
|
|
return new Map<Hand, HandConnection>([
|
|
which.isOnLeftLookingUpAndDown()
|
|
? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }]
|
|
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
|
|
]);
|
|
}
|
|
|
|
function balanceCircleInAndOut(move: Move, startPos: SemanticPosition, balanceBeats?: number): [LowLevelMove, LowLevelMove] {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw "Balance circle must start in a circle, but starting at " + startPos;
|
|
}
|
|
|
|
balanceBeats ??= 4;
|
|
const balancePartBeats = balanceBeats/2;
|
|
|
|
const holdingHandsInCircle: SemanticPosition = {...startPos,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: Hand.Right, to: HandTo.LeftInCircle }],
|
|
[Hand.Right, { hand: Hand.Left, to: HandTo.RightInCircle }],
|
|
]),
|
|
};
|
|
const circleBalancedIn: SemanticPosition = {...holdingHandsInCircle,
|
|
balance: BalanceWeight.Forward,
|
|
};
|
|
|
|
const balanceIn: LowLevelMove = {
|
|
move,
|
|
startBeat: 0,
|
|
beats: balancePartBeats,
|
|
startPosition: holdingHandsInCircle,
|
|
endPosition: circleBalancedIn,
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
};
|
|
const balanceOut: LowLevelMove = {...balanceIn,
|
|
startBeat: balancePartBeats,
|
|
startPosition: circleBalancedIn,
|
|
endPosition: holdingHandsInCircle,
|
|
};
|
|
|
|
return [balanceIn, balanceOut];
|
|
}
|
|
|
|
function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: { move: Move; nextMove: Move; startingPos: Map<DancerIdentity, SemanticPosition>; numProgessions: number; }): Map<DancerIdentity, LowLevelMove[]> {
|
|
type PartialLowLevelMove = {
|
|
remarks?: string,
|
|
beats: number,
|
|
startPosition?: SemanticPosition,
|
|
endPosition: SemanticPosition,
|
|
movementPattern: SemanticAnimation
|
|
};
|
|
|
|
function append(moves: LowLevelMove[],
|
|
newMove: LowLevelMove | PartialLowLevelMove
|
|
| ((prevEnd: SemanticPosition) => PartialLowLevelMove)): LowLevelMove[] {
|
|
const lastMove = moves.at(-1)!;
|
|
const prevEnd = lastMove.endPosition;
|
|
if (typeof newMove === 'function') {
|
|
newMove = newMove(prevEnd);
|
|
}
|
|
|
|
if (!newMove.startPosition) {
|
|
newMove.startPosition = prevEnd;
|
|
}
|
|
moves.push({
|
|
...newMove,
|
|
startPosition: newMove.startPosition ?? prevEnd,
|
|
move: lastMove.move,
|
|
startBeat: lastMove.startBeat + lastMove.beats,
|
|
});
|
|
|
|
return moves;
|
|
}
|
|
|
|
function combine(moves: ((LowLevelMove | PartialLowLevelMove
|
|
| ((prevEnd: SemanticPosition) => PartialLowLevelMove))[]),
|
|
startPos?: SemanticPosition): LowLevelMove[] {
|
|
const res: LowLevelMove[] = [];
|
|
if (moves.length === 0) return res;
|
|
|
|
let firstMove = moves[0];
|
|
if ('move' in firstMove) {
|
|
res.push(firstMove);
|
|
} else {
|
|
if (typeof firstMove === 'function') {
|
|
firstMove = firstMove(startPos!);
|
|
}
|
|
|
|
res.push({...firstMove,
|
|
move: move,
|
|
startBeat: 0,
|
|
startPosition: firstMove.startPosition ?? startPos!,
|
|
});
|
|
}
|
|
|
|
for (const move of moves.slice(1)) {
|
|
append(res, move);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
function findPairOpposite(who: chooser_pairz, id: DancerIdentity): common.ExtendedDancerIdentity | null {
|
|
const pos = getPosFor(id.asExtendedDancerIdentity());
|
|
|
|
switch (who) {
|
|
case "partners":
|
|
return id.partner().asExtendedDancerIdentity();
|
|
case "neighbors":
|
|
return id.neighbor().asExtendedDancerIdentity();
|
|
case "next neighbors":
|
|
// TODO right "next" neighbor?
|
|
return {
|
|
setIdentity: id.neighbor(),
|
|
relativeSet: id.coupleRole === CoupleRole.Ones ? 1 : -1,
|
|
relativeLine: 0,
|
|
}
|
|
// These three might get used when not with neighbors?
|
|
case "gentlespoons":
|
|
case "ladles":
|
|
case "same roles":
|
|
if (who === "gentlespoons" && id.danceRole === DanceRole.Robin
|
|
|| who === "ladles" && id.danceRole === DanceRole.Lark) {
|
|
return null;
|
|
}
|
|
const proposedId = id.oppositeSameRole().asExtendedDancerIdentity();
|
|
const proposedPos = getPosFor(proposedId);
|
|
return {
|
|
...proposedId,
|
|
// Get the same role dancer in the set the dancer is currently in.
|
|
relativeSet: proposedId.relativeSet + (pos.setOffset - proposedPos.setOffset)
|
|
}
|
|
case "ones":
|
|
if (id.coupleRole === CoupleRole.Twos) return null;
|
|
return id.partner().asExtendedDancerIdentity();
|
|
case "twos":
|
|
if (id.coupleRole === CoupleRole.Ones) return null;
|
|
return id.partner().asExtendedDancerIdentity();
|
|
case "shadows":
|
|
throw "Not sure shadow is consistently the same.";
|
|
case "first corners":
|
|
case "second corners":
|
|
throw "Contra corners are unsupported.";
|
|
default:
|
|
throw new Error("Unsupported who: " + who);
|
|
}
|
|
}
|
|
|
|
function getPosFor(id: common.ExtendedDancerIdentity): SemanticPosition & { setOffset: number, lineOffset: number } {
|
|
const basePos = startingPos.get(id.setIdentity)!;
|
|
return {...basePos,
|
|
setOffset: (basePos.setOffset ?? 0) + id.relativeSet,
|
|
lineOffset: (basePos.lineOffset ?? 0) + id.relativeLine,
|
|
};
|
|
}
|
|
|
|
function findCenterBetween(id: DancerIdentity, other: common.ExtendedDancerIdentity): CircleSideOrCenter {
|
|
const selfPos = startingPos.get(id)!;
|
|
selfPos.setOffset ??= 0;
|
|
selfPos.lineOffset ??= 0;
|
|
|
|
const otherPos = getPosFor(other);
|
|
|
|
if (selfPos.kind === PositionKind.Circle && otherPos.kind === PositionKind.Circle) {
|
|
if (!(selfPos.lineOffset === otherPos.lineOffset && selfPos.setOffset === otherPos.setOffset)) {
|
|
throw "Don't know how to find center between different circles.";
|
|
} else if (selfPos.which.leftRightSide() === otherPos.which.leftRightSide()) {
|
|
return selfPos.which.leftRightSide();
|
|
} else if (selfPos.which.topBottomSide() === otherPos.which.topBottomSide()) {
|
|
return selfPos.which.topBottomSide();
|
|
} else {
|
|
return "Center";
|
|
}
|
|
} else {
|
|
throw "Don't know how to find center between positions not in a circle.";
|
|
}
|
|
}
|
|
|
|
function handleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
const res = new Map<DancerIdentity, LowLevelMove[]>();
|
|
for (const [id, startPos] of startingPos.entries()) {
|
|
res.set(id, dancerFunc({ id, startPos }));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function handleCircleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition & { kind: PositionKind.Circle } }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
return handleMove(({ id, startPos }) => {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
|
|
}
|
|
|
|
return dancerFunc({ id, startPos });
|
|
});
|
|
}
|
|
|
|
function handlePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition,
|
|
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
|
|
withId: common.ExtendedDancerIdentity,
|
|
around: CircleSideOrCenter,
|
|
}) => LowLevelMove[]), meanwhileFunc?: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition,
|
|
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
return handleMove(({ id, startPos }) => {
|
|
const withId = findPairOpposite(who, id);
|
|
if (!withId) {
|
|
if (meanwhileFunc) {
|
|
return meanwhileFunc({ id, startPos });
|
|
} else {
|
|
return combine([{
|
|
beats: move.beats,
|
|
startPosition: { ...startPos, hands: undefined },
|
|
endPosition: { ...startPos, hands: undefined },
|
|
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
|
}]);
|
|
}
|
|
}
|
|
const withPos = getPosFor(withId);
|
|
const setDifference = withPos.setOffset - (startPos.setOffset ?? 0);
|
|
let startPosAdjusted = startPos;
|
|
if (setDifference !== 0) {
|
|
// TODO Can move be with a different short line or just a different circle?
|
|
// PassBy can probably be with the next short line...
|
|
if (startPos.kind === PositionKind.Circle && (setDifference === 1 || setDifference === -1)) {
|
|
startPosAdjusted = {
|
|
...startPos,
|
|
setOffset: (startPos.setOffset ?? 0) + setDifference / 2,
|
|
which: startPos.which.swapUpAndDown(),
|
|
}
|
|
} else {
|
|
throw "Not near dancer to " + move.move + " with.";
|
|
}
|
|
}
|
|
const startWhich = startPosAdjusted.which;
|
|
// TODO Can swing be across the set (top or bottom)?
|
|
const around = withPos.kind === PositionKind.Circle
|
|
? (withPos.which.leftRightSide() === startWhich.leftRightSide()
|
|
? startWhich.leftRightSide()
|
|
: startWhich instanceof CirclePosition && withPos.which.topBottomSide() === startWhich.topBottomSide()
|
|
? startWhich.topBottomSide()
|
|
: "Center")
|
|
: "Center";
|
|
|
|
return dancerFunc({ id, startPos: startPosAdjusted, withId, withPos, around });
|
|
});
|
|
}
|
|
|
|
function handleCirclePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition & { kind: PositionKind.Circle },
|
|
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
|
|
withId: common.ExtendedDancerIdentity,
|
|
around: CircleSideOrCenter,
|
|
}) => LowLevelMove[]), meanwhileFunc?: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition & { kind: PositionKind.Circle },
|
|
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
return handlePairedMove(who, ({ id, startPos, withId, withPos, around }) => {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
|
|
}
|
|
|
|
return dancerFunc({ id, startPos, withId, withPos, around });
|
|
}, meanwhileFunc ? ({id, startPos}) => {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
|
|
}
|
|
|
|
return meanwhileFunc({id, startPos});
|
|
} : undefined);
|
|
}
|
|
|
|
switch (move.move) {
|
|
case "balance the ring":
|
|
return handleCircleMove(({ startPos }) => balanceCircleInAndOut(move, startPos));
|
|
|
|
case "balance":
|
|
return handleMove(({ startPos }) => {
|
|
// TODO Use who to determine facing?
|
|
// TODO Could be left to right, not back and forth?
|
|
// TODO How to determine hand... by next move, I guess?
|
|
|
|
const forwardBeats = move.beats / 2;
|
|
const backwardBeats = move.beats - forwardBeats;
|
|
|
|
return combine([
|
|
{
|
|
beats: forwardBeats,
|
|
endPosition: { ...startPos, balance: BalanceWeight.Forward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
{
|
|
beats: backwardBeats,
|
|
endPosition: { ...startPos, balance: BalanceWeight.Backward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
], startPos);
|
|
});
|
|
|
|
case "petronella":
|
|
return handleCircleMove(({ startPos }) => {
|
|
// TODO These should be actual parameters, not parsing the notes...
|
|
const rightShoulder: boolean = !(move.note?.includes('left') ?? false);
|
|
const newCircle: boolean = move.note?.includes('end facing') ?? move.progression;
|
|
|
|
let finalPosition = {
|
|
...startPos,
|
|
facing: Facing.CenterOfCircle,
|
|
which: startPos.which.circleRight(rightShoulder ? 1 : -1),
|
|
hands: undefined,
|
|
};
|
|
|
|
if (newCircle) {
|
|
finalPosition = {...finalPosition,
|
|
which: finalPosition.which.swapUpAndDown(),
|
|
setOffset: (finalPosition.setOffset ?? 0) + (finalPosition.which.isTop() ? -0.5 : +0.5),
|
|
}
|
|
}
|
|
|
|
const spin: ((prevEnd: SemanticPosition) => PartialLowLevelMove) = prevEnd => ({
|
|
beats: move.beats - (move.parameters.bal ? 4 : 0),
|
|
startPosition: {
|
|
...prevEnd,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: undefined,
|
|
},
|
|
endPosition: finalPosition,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
minRotation: rightShoulder ? 180 : -180,
|
|
handsDuring: "None",
|
|
},
|
|
});
|
|
|
|
if (move.parameters.bal) {
|
|
const balance: LowLevelMove[] = balanceCircleInAndOut(move, startPos);
|
|
return append([...balance], spin);
|
|
} else {
|
|
return combine([spin]);
|
|
}
|
|
});
|
|
|
|
case "form long waves":
|
|
if (move.beats !== 0) {
|
|
throw new Error(move.move + " unsupported except for zero beats marking end of previous move.");
|
|
}
|
|
return handleCircleMove(({ id, startPos }) => {
|
|
const facingIn = findPairOpposite(move.parameters.who, id) !== null;
|
|
|
|
const startAndEndPos : SemanticPosition = {
|
|
kind: PositionKind.Circle,
|
|
which: startPos.which,
|
|
facing: facingIn ? startPos.which.facingAcross() : startPos.which.facingOut(),
|
|
hands: handsInLine({ wavy: true, which: startPos.which }),
|
|
setOffset: startPos.setOffset,
|
|
lineOffset: startPos.lineOffset,
|
|
}
|
|
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: startAndEndPos,
|
|
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
|
}
|
|
], startAndEndPos);
|
|
});
|
|
|
|
case "box circulate":
|
|
const circulateRight: boolean = move.parameters.hand;
|
|
const whoCrosses = move.parameters.who;
|
|
return handleCircleMove(({id, startPos}) => {
|
|
let isCrossing: boolean;
|
|
switch (whoCrosses) {
|
|
case "gentlespoons":
|
|
isCrossing = id.danceRole === DanceRole.Lark;
|
|
break;
|
|
case "ladles":
|
|
isCrossing = id.danceRole === DanceRole.Robin;
|
|
break;
|
|
case "ones":
|
|
isCrossing = id.coupleRole === CoupleRole.Ones;
|
|
break;
|
|
case "twos":
|
|
isCrossing = id.coupleRole === CoupleRole.Twos;
|
|
break;
|
|
case "first corners":
|
|
case "second corners":
|
|
throw "first/second corner leading box circulate doesn't make sense?";
|
|
}
|
|
|
|
// Starts in long wavy lines.
|
|
const startingPos: SemanticPosition = {
|
|
...startPos,
|
|
facing: isCrossing === startPos.which.isLeft() ? Facing.Right : Facing.Left,
|
|
hands: new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }],
|
|
[Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }],
|
|
]),
|
|
balance: undefined,
|
|
longLines: undefined,
|
|
dancerDistance: undefined,
|
|
};
|
|
const balance: PartialLowLevelMove[] = move.parameters.bal ? [
|
|
{
|
|
beats: 2,
|
|
endPosition: { ...startingPos, balance: circulateRight ? BalanceWeight.Right : BalanceWeight.Left },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
{
|
|
beats: 2,
|
|
endPosition: { ...startingPos, balance: BalanceWeight.Backward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
] : [];
|
|
const circulate: PartialLowLevelMove = {
|
|
beats: move.beats - (move.parameters.bal ? 4 : 0),
|
|
endPosition: {...startingPos,
|
|
which: isCrossing ? startingPos.which.swapAcross() : startingPos.which.swapUpAndDown(),
|
|
facing: isCrossing ? startingPos.facing : startingPos.facing === Facing.Right ? Facing.Left : Facing.Right,
|
|
},
|
|
movementPattern: {
|
|
// TODO Not sure loop should really be linear...
|
|
kind: SemanticAnimationKind.Linear,
|
|
minRotation: isCrossing ? undefined : circulateRight ? 180 : -180,
|
|
handsDuring: "None",
|
|
}
|
|
};
|
|
return combine([...balance, circulate], startingPos);
|
|
});
|
|
|
|
case "mad robin":
|
|
if (move.parameters.circling !== 360) {
|
|
throw new Error("mad robin circling not exactly once is unsupported.");
|
|
}
|
|
|
|
return handleCircleMove(({ id, startPos }) => {
|
|
// Read who of mad robin to decide direction.
|
|
const madRobinClockwise: boolean = (findPairOpposite(move.parameters.who, id) !== null) === startPos.which.isOnLeftLookingAcross();
|
|
|
|
const startAndEndPos: SemanticPosition = {
|
|
kind: PositionKind.Circle,
|
|
which: startPos.which,
|
|
facing: startPos.which.facingAcross(),
|
|
}
|
|
|
|
return combine([{
|
|
beats: move.beats,
|
|
startPosition: startAndEndPos,
|
|
endPosition: startAndEndPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.DoSiDo,
|
|
amount: madRobinClockwise ? move.parameters.circling : -move.parameters.circling,
|
|
around: startPos.which.leftRightSide(),
|
|
},
|
|
}]);
|
|
});
|
|
case "do si do":
|
|
return handleCirclePairedMove(move.parameters.who, ({ startPos, around }) => {
|
|
// TODO Use other parameters?
|
|
const startAndEndPos = {
|
|
...startPos,
|
|
hands: undefined,
|
|
facing: around === "Center"
|
|
? Facing.CenterOfCircle
|
|
: around === CircleSide.Left || around === CircleSide.Right
|
|
? (startPos.which.isTop() ? Facing.Down : Facing.Up)
|
|
: (startPos.which.isLeft() ? Facing.Right : Facing.Left),
|
|
};
|
|
return combine([{
|
|
beats: move.beats,
|
|
startPosition: startAndEndPos,
|
|
endPosition: startAndEndPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.DoSiDo,
|
|
amount: move.parameters.circling,
|
|
around,
|
|
},
|
|
}]);
|
|
});
|
|
case "swing":
|
|
return handlePairedMove(move.parameters.who, ({ id, startPos, around, withId }) => {
|
|
// TODO swing can start from non-circle positions.
|
|
// TODO swing end is only in notes / looking at next move.
|
|
|
|
// TODO better way to detect swing end?
|
|
// TODO more structured way to do this than enumerating next moves here?
|
|
// maybe instead of nextMove an optional endPosition for fixing up positions?
|
|
// ... but then every move would have to handle that...
|
|
const toShortLines = nextMove.move === "down the hall" || nextMove.move === "up the hall";
|
|
const endFacingAcross = (around === CircleSide.Left || around === CircleSide.Right) && !toShortLines;
|
|
|
|
const startWhich = startPos.which;
|
|
const startPosition: SemanticPosition = {
|
|
...startPos,
|
|
facing: around === CircleSide.Left || CircleSide.Right
|
|
? (startWhich instanceof CirclePosition ? (startWhich.topBottomSide() === CircleSide.Bottom ? Facing.Up : Facing.Down)
|
|
: startWhich.facingSide())
|
|
: (startWhich.isLeft() ? Facing.Right : Facing.Left),
|
|
};
|
|
|
|
const swingRole = id.danceRole != withId.setIdentity.danceRole
|
|
? id.danceRole
|
|
// Make some arbitrary choice for same-role swings
|
|
: id.coupleRole !== withId.setIdentity.coupleRole
|
|
? (id.coupleRole === CoupleRole.Ones ? DanceRole.Lark : DanceRole.Robin)
|
|
: withId.relativeSet !== 0
|
|
? (withId.relativeSet > 0 ? DanceRole.Lark : DanceRole.Robin)
|
|
: withId.relativeLine !== 0
|
|
? (withId.relativeLine > 0 ? DanceRole.Lark : DanceRole.Robin)
|
|
: /* should be unreachable as this means withId is equal to id */ DanceRole.Lark;
|
|
|
|
// TODO This assumes swing around right/left, not center or top/bottom.
|
|
let endPosition: SemanticPosition;
|
|
if (endFacingAcross) {
|
|
endPosition = {
|
|
...startPos,
|
|
kind: PositionKind.Circle,
|
|
which: startWhich instanceof CirclePosition
|
|
? (startWhich.isOnLeftLookingAcross() === (swingRole === DanceRole.Lark)
|
|
? startWhich
|
|
: startWhich.swapUpAndDown())
|
|
: (startWhich.isLeft()
|
|
? (swingRole === DanceRole.Lark ? CirclePosition.BottomLeft : CirclePosition.TopLeft)
|
|
: (swingRole === DanceRole.Lark ? CirclePosition.TopRight : CirclePosition.BottomRight)),
|
|
facing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
|
|
balance: undefined,
|
|
dancerDistance: undefined,
|
|
hands: new Map<Hand, HandConnection>([swingRole === DanceRole.Lark
|
|
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
|
|
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
|
|
};
|
|
} else if (toShortLines) {
|
|
const endFacing = nextMove.move === "down the hall" !== (nextMove.parameters.facing === "backward")
|
|
? Facing.Down
|
|
: Facing.Up;
|
|
const endWhich = startWhich.isLeft()
|
|
? ((endFacing === Facing.Down) === (swingRole === DanceRole.Lark) ? ShortLinesPosition.FarLeft : ShortLinesPosition.MiddleLeft)
|
|
: ((endFacing === Facing.Down) === (swingRole === DanceRole.Lark) ? ShortLinesPosition.MiddleRight : ShortLinesPosition.FarRight)
|
|
endPosition = {
|
|
...startPos,
|
|
kind: PositionKind.ShortLines,
|
|
which: endWhich,
|
|
facing: endFacing,
|
|
balance: undefined,
|
|
dancerDistance: undefined,
|
|
hands: handsInLine({ wavy: false, which: endWhich, facing: endFacing }),
|
|
};
|
|
}
|
|
else {
|
|
// TODO Need to figure out the logic of knowing if this should be facing up or down.
|
|
// Probably based on knowing Ones vs. Twos? Also then the not-participating-dancers need their
|
|
// "standing still" to update that they are in a new set...
|
|
//throw new Error("Swing to new circle currently unsupported.");
|
|
endPosition = {
|
|
// end not facing across or in short lines, so transitioning to new circle like in many contra corners dances.
|
|
...startPos,
|
|
};
|
|
}
|
|
|
|
const swingBeats = move.parameters.prefix === "none" ? move.beats
|
|
: move.parameters.prefix === "balance"
|
|
? move.beats > 8 ? 8 : move.beats - 4
|
|
: move.parameters.prefix === "meltdown"
|
|
? move.beats - 4
|
|
: (() => { throw "Unknown swing prefix: " + move.parameters.prefix })();
|
|
|
|
const swing: PartialLowLevelMove = {
|
|
beats: swingBeats,
|
|
endPosition: endPosition,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Swing,
|
|
minAmount: 360,
|
|
around,
|
|
endFacing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
|
|
swingRole,
|
|
},
|
|
};
|
|
|
|
switch (move.parameters.prefix) {
|
|
case "none":
|
|
return combine([swing,], startPosition);
|
|
case "balance":
|
|
// TODO Right length for balance?
|
|
const balancePartBeats = move.beats > 8 ? (move.beats - 8) / 2 : 2;
|
|
const startWithBalHands = {
|
|
...startPosition,
|
|
hands: new Map<Hand, HandConnection>([
|
|
[Hand.Left, { to: HandTo.DancerForward, hand: Hand.Right }],
|
|
[Hand.Right, { to: HandTo.DancerForward, hand: Hand.Left }],
|
|
]),
|
|
};
|
|
const balForwardPos = startWithBalHands.kind === PositionKind.Circle
|
|
? {
|
|
...startWithBalHands,
|
|
balance: BalanceWeight.Forward,
|
|
dancerDistance: DancerDistance.Compact,
|
|
}
|
|
: {
|
|
...startWithBalHands,
|
|
balance: BalanceWeight.Forward,
|
|
};
|
|
return combine([
|
|
{
|
|
beats: balancePartBeats,
|
|
startPosition: startWithBalHands,
|
|
endPosition: balForwardPos,
|
|
movementPattern: { kind: SemanticAnimationKind.Linear, },
|
|
},
|
|
prevEnd => ({
|
|
beats: balancePartBeats,
|
|
endPosition: {
|
|
...prevEnd,
|
|
balance: BalanceWeight.Backward,
|
|
},
|
|
movementPattern: { kind: SemanticAnimationKind.Linear, },
|
|
}),
|
|
swing,
|
|
], startPosition);
|
|
case "meltdown":
|
|
const meltdownBeats = 4; // TODO right number here?
|
|
return combine([
|
|
prevEnd => ({
|
|
beats: meltdownBeats,
|
|
endPosition: {...prevEnd, dancerDistance: DancerDistance.Compact },
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.RotateAround,
|
|
minAmount: 360,
|
|
around,
|
|
byHand: undefined,
|
|
close: true,
|
|
},
|
|
}),
|
|
swing,
|
|
], startPosition);
|
|
}
|
|
});
|
|
|
|
case "promenade":
|
|
if (move.parameters.dir !== "across") {
|
|
// TODO "along" would be the bicycle chain? Not sure what left/right diagonal means here.
|
|
throw "Promenade not across the set is unsupported."
|
|
}
|
|
return handleCirclePairedMove(move.parameters.who, ({ startPos }) => {
|
|
|
|
const endSetOffset = move.progression
|
|
? (startPos.setOffset ?? 0) + (startPos.which.isLeft() ? +0.5 : -0.5)
|
|
: startPos.setOffset;
|
|
const endPos: SemanticPosition = {
|
|
...startPos,
|
|
which: startPos.which.swapDiagonal(),
|
|
facing: startPos.which.facingOut(),
|
|
setOffset: endSetOffset,
|
|
};
|
|
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: endPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Promenade,
|
|
swingRole: startPos.which.isOnLeftLookingAcross() ? DanceRole.Lark : DanceRole.Robin,
|
|
twirl: true,
|
|
passBy: move.parameters.turn ? Hand.Left : Hand.Right,
|
|
}
|
|
}], startPos);
|
|
});
|
|
|
|
case "allemande":
|
|
case "allemande orbit":
|
|
case "gyre":
|
|
const allemandeCircling = move.move === "allemande orbit" ? move.parameters.circling1 : move.parameters.circling;
|
|
const byHandOrShoulder = (move.move === "gyre" ? move.parameters.shoulder : move.parameters.hand) ? Hand.Right : Hand.Left;
|
|
|
|
return handlePairedMove(move.parameters.who, ({ startPos, around, withId }) => {
|
|
// TODO Not sure if this is right.
|
|
const swap = allemandeCircling % 360 === 180;
|
|
if (!swap && allemandeCircling % 360 !== 0) {
|
|
// TODO Support allemande that's not a swap or no-op.
|
|
throw "Unsupported allemande circle amount: " + allemandeCircling;
|
|
}
|
|
let endPosition: SemanticPosition = startPos;
|
|
if (swap) {
|
|
endPosition = { ...startingPos.get(withId.setIdentity)! };
|
|
endPosition.setOffset = (endPosition.setOffset ?? 0) + withId.relativeSet;
|
|
endPosition.lineOffset = (endPosition.lineOffset ?? 0) + withId.relativeLine;
|
|
}
|
|
|
|
return combine([
|
|
{
|
|
beats: move.beats,
|
|
endPosition,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.RotateAround,
|
|
minAmount: byHandOrShoulder === Hand.Right ? allemandeCircling : -allemandeCircling,
|
|
around,
|
|
byHand: move.move === "allemande" || move.move === "allemande orbit" ? byHandOrShoulder : undefined,
|
|
close: true,
|
|
},
|
|
},
|
|
], startPos);
|
|
}, move.move !== "allemande orbit" ? undefined : ({ id, startPos}) => {
|
|
const orbitAmount = move.parameters.circling2;
|
|
const swap = orbitAmount % 360 === 180;
|
|
if (!swap && orbitAmount % 360 !== 0) {
|
|
// TODO Support allemande that's not a swap or no-op.
|
|
throw "Unsupported allemande orbit amount: " + orbitAmount;
|
|
}
|
|
|
|
const startingPos: SemanticPosition = {
|
|
...startPos,
|
|
hands: undefined,
|
|
balance: undefined,
|
|
dancerDistance: undefined,
|
|
}
|
|
let endPosition: SemanticPosition;
|
|
if (swap) {
|
|
if (startingPos.kind === PositionKind.Circle) {
|
|
endPosition =
|
|
{
|
|
...startingPos,
|
|
which: startingPos.which.swapDiagonal(),
|
|
facing: startingPos.which.isLeft() ? Facing.Left : Facing.Right,
|
|
}
|
|
} else {
|
|
endPosition =
|
|
{
|
|
...startingPos,
|
|
which: startingPos.which.swapSides(),
|
|
facing: startingPos.which.isLeft() ? Facing.Left : Facing.Right,
|
|
}
|
|
}
|
|
} else {
|
|
endPosition = startingPos;
|
|
}
|
|
|
|
return combine([
|
|
{
|
|
beats: move.beats,
|
|
endPosition,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.RotateAround,
|
|
// Orbit is opposite direction of allemande.
|
|
minAmount: byHandOrShoulder === Hand.Right ? -orbitAmount : +orbitAmount,
|
|
around: "Center",
|
|
byHand: undefined,
|
|
close: false,
|
|
},
|
|
},
|
|
], startingPos);
|
|
});
|
|
|
|
case "revolving door":
|
|
const byHand = move.parameters.hand ? Hand.Right : Hand.Left;
|
|
// TODO More parts? Or define an animation kind?
|
|
const waitBeats = 2;
|
|
const carryBeats = move.beats / 2;
|
|
const returnBeats = move.beats - carryBeats - waitBeats;
|
|
return handleCirclePairedMove(move.parameters.whom, ({ id, startPos }) => {
|
|
const isCarried = findPairOpposite(move.parameters.who, id) === null;
|
|
|
|
// TODO animation here needs work.
|
|
if (isCarried) {
|
|
const endWhich = startPos.which.swapDiagonal();
|
|
return combine([
|
|
prevEnd => ({
|
|
beats: waitBeats,
|
|
endPosition: prevEnd,
|
|
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
|
}),
|
|
{
|
|
beats: carryBeats,
|
|
endPosition: {
|
|
...startPos,
|
|
which: endWhich,
|
|
facing: endWhich.facingAcross(),
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.RotateAround,
|
|
minAmount: byHand === Hand.Right ? 180 : -180,
|
|
around: "Center",
|
|
byHand,
|
|
close: false,
|
|
},
|
|
},
|
|
prevEnd => ({
|
|
beats: returnBeats,
|
|
endPosition: prevEnd,
|
|
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
|
}),
|
|
], startPos);
|
|
} else {
|
|
return combine([
|
|
{
|
|
beats: move.beats,
|
|
endPosition: startPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.RotateAround,
|
|
minAmount: byHand === Hand.Right ? 180 : -180,
|
|
around: "Center",
|
|
byHand,
|
|
close: true,
|
|
},
|
|
},
|
|
], startPos);
|
|
}
|
|
});
|
|
|
|
case "star promenade":
|
|
const starPromenadeHand = move.parameters.hand ? Hand.Right : Hand.Left;
|
|
const starPromenadeSwap = (move.parameters.circling % 360) === 180;
|
|
if (!starPromenadeSwap && (move.parameters.circling % 360 !== 0)) {
|
|
throw new Error(move.move + " circling by not a multiple of 180 degrees is unsupported.");
|
|
}
|
|
|
|
// TODO start promenade hands/show dancers close
|
|
return handleCircleMove(({ id, startPos }) => {
|
|
const inCenter = findPairOpposite(move.parameters.who, id) !== null;
|
|
// TODO Actually, does star promenade end facing out and butterfly whirl swaps?
|
|
const endWhich = starPromenadeSwap ? startPos.which.swapDiagonal() : startPos.which;
|
|
const endFacing = endWhich.facingAcross();
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: {
|
|
...startPos,
|
|
which: endWhich,
|
|
facing: endFacing,
|
|
dancerDistance: DancerDistance.Compact,
|
|
// TODO Perhaps different hands indication for "scooped"?
|
|
hands: handToDancerToSideInCircleFacingAcross(endWhich),
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.RotateAround,
|
|
around: "Center",
|
|
byHand: inCenter ? starPromenadeHand : undefined,
|
|
close: inCenter,
|
|
minAmount: move.parameters.hand ? move.parameters.circling : -move.parameters.circling,
|
|
}
|
|
}], startPos);
|
|
});
|
|
|
|
case "butterfly whirl":
|
|
return handleCircleMove(({ startPos }) => {
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: startPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.RotateAround,
|
|
around: startPos.which.leftRightSide(),
|
|
// TODO hand around isn't the same as allemande...
|
|
byHand: startPos.which.isOnLeftLookingAcross() ? Hand.Right : Hand.Left,
|
|
close: true,
|
|
minAmount: 360,
|
|
}
|
|
}], startPos);
|
|
});
|
|
|
|
case "circle":
|
|
return handleCircleMove(({ startPos }) => {
|
|
const places = move.parameters.places / 90 * (move.parameters.turn ? 1 : -1);
|
|
|
|
return combine([
|
|
{
|
|
beats: move.beats,
|
|
endPosition: {
|
|
...startPos,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
which: startPos.which.circleLeft(places),
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Circle,
|
|
places,
|
|
}
|
|
},
|
|
], { ...startPos, facing: Facing.CenterOfCircle });
|
|
});
|
|
|
|
case "star":
|
|
return handleCircleMove(({ startPos }) => {
|
|
const hand = move.parameters.hand ? Hand.Right : Hand.Left;
|
|
const grip = move.parameters.grip === "hands across"
|
|
? StarGrip.HandsAcross
|
|
: move.parameters.grip === "wrist grip"
|
|
? StarGrip.WristGrip
|
|
: undefined;
|
|
const facing = hand === Hand.Left ? Facing.LeftInCircle : Facing.RightInCircle;
|
|
|
|
const places = move.parameters.places / 90 * (hand === Hand.Right ? 1 : -1);
|
|
|
|
return combine([
|
|
{
|
|
beats: move.beats,
|
|
endPosition: {
|
|
...startPos,
|
|
facing,
|
|
hands: undefined,
|
|
which: startPos.which.circleLeft(places),
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Star,
|
|
hand,
|
|
grip,
|
|
places,
|
|
}
|
|
},
|
|
], { ...startPos, facing });
|
|
});
|
|
|
|
case "California twirl":
|
|
return handleCirclePairedMove(move.parameters.who, ({ startPos }) => {
|
|
// TODO does "who" matter here or is it entirely positional? At least need to know who to omit.
|
|
|
|
const onLeft : boolean = startPos.which.isOnLeftLookingUpAndDown();
|
|
// TODO get rid of this 1 beat set up and make it part of TwirlSwap?
|
|
return combine([
|
|
{
|
|
beats: 1,
|
|
endPosition: {
|
|
...startPos,
|
|
hands: new Map<Hand, HandConnection>([onLeft
|
|
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
|
|
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
|
|
facing: startPos.which.topBottomSide() === CircleSide.Top ? Facing.Down : Facing.Up,
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
}
|
|
},
|
|
{
|
|
beats: move.beats - 1,
|
|
endPosition: {
|
|
...startPos,
|
|
which: startPos.which.swapAcross(),
|
|
facing: startPos.which.topBottomSide() === CircleSide.Top ? Facing.Up : Facing.Down,
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.TwirlSwap,
|
|
around: startPos.which.topBottomSide(),
|
|
hand: onLeft ? Hand.Right : Hand.Left,
|
|
}
|
|
}], startPos);
|
|
});
|
|
|
|
case "box the gnat":
|
|
return handlePairedMove(move.parameters.who, ({ startPos, around, withPos }) => {
|
|
const hand = move.parameters.hand ? Hand.Right : Hand.Left;
|
|
const balanceBeats = move.parameters.bal
|
|
? move.beats > 4
|
|
? move.beats - 4
|
|
: 2
|
|
: 0;
|
|
const balancePartBeats = balanceBeats / 2;
|
|
const twirlBeats = move.beats - balanceBeats;
|
|
|
|
// TODO Adjust facing?
|
|
const startPosition = { ...startPos, hands: new Map<Hand, HandConnection>([[hand, { hand, to: HandTo.DancerForward }]]) };
|
|
|
|
if (around === "Center") {
|
|
throw "TwirlSwap around center is unsupported.";
|
|
}
|
|
|
|
const twirl: PartialLowLevelMove = {
|
|
beats: twirlBeats,
|
|
endPosition: withPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.TwirlSwap,
|
|
around,
|
|
hand,
|
|
}
|
|
};
|
|
|
|
if (move.parameters.bal) {
|
|
return combine([
|
|
{
|
|
beats: balancePartBeats,
|
|
endPosition: {
|
|
...startPosition,
|
|
balance: BalanceWeight.Forward,
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
}
|
|
},
|
|
{
|
|
beats: balancePartBeats,
|
|
endPosition: {
|
|
...startPosition,
|
|
balance: BalanceWeight.Backward,
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
}
|
|
},
|
|
twirl], startPosition);
|
|
} else {
|
|
return combine([twirl], startPosition);
|
|
}
|
|
});
|
|
|
|
case "pull by dancers":
|
|
return handlePairedMove(move.parameters.who, ({ startPos, around, withPos }) => {
|
|
const hand = move.parameters.hand ? Hand.Right : Hand.Left;
|
|
const balanceBeats = move.parameters.bal
|
|
? move.beats > 4
|
|
? move.beats - 4
|
|
: 2
|
|
: 0;
|
|
const balancePartBeats = balanceBeats / 2;
|
|
const pullBeats = move.beats - balanceBeats;
|
|
|
|
// TODO Adjust facing?
|
|
const startPosition = {
|
|
...startPos,
|
|
hands: new Map<Hand, HandConnection>([
|
|
[
|
|
hand,
|
|
{ hand, to: around === "Center" ? HandTo.DiagonalAcrossCircle : HandTo.DancerForward }
|
|
]])
|
|
};
|
|
|
|
const passBy: PartialLowLevelMove = {
|
|
beats: pullBeats,
|
|
endPosition: { ...withPos, facing: startPos.facing },
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
around,
|
|
side: hand,
|
|
withHands: true,
|
|
facing: "Start",
|
|
otherPath: "Swap",
|
|
}
|
|
};
|
|
|
|
if (move.parameters.bal) {
|
|
return combine([
|
|
{
|
|
beats: balancePartBeats,
|
|
endPosition: {
|
|
...startPosition,
|
|
balance: BalanceWeight.Forward,
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
}
|
|
},
|
|
{
|
|
beats: balancePartBeats,
|
|
endPosition: {
|
|
...startPosition,
|
|
balance: BalanceWeight.Backward,
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
}
|
|
},
|
|
passBy], startPosition);
|
|
} else {
|
|
return combine([passBy], startPosition);
|
|
}
|
|
});
|
|
|
|
case "chain":
|
|
const mainRole = move.parameters.who === "gentlespoons" ? DanceRole.Lark : DanceRole.Robin;
|
|
const pullToTurnBeats = 2;
|
|
const pullBeats = move.beats / 2 - pullToTurnBeats;
|
|
const turnBeats = move.beats - pullBeats - pullToTurnBeats;
|
|
const chainHand: Hand = move.parameters.hand ? Hand.Right : Hand.Left;
|
|
const cwCourtesyTurn = chainHand === Hand.Left;
|
|
|
|
return handleCircleMove(({ id, startPos }) => {
|
|
if (id.danceRole === mainRole) {
|
|
const endWhich = startPos.which.swapDiagonal();
|
|
let endSet = startPos.setOffset ?? 0;
|
|
let to : HandTo;
|
|
switch (move.parameters.dir) {
|
|
case "along":
|
|
throw "Don't know what chaining along the set means.";
|
|
case "across":
|
|
to = HandTo.DiagonalAcrossCircle;
|
|
break;
|
|
case "right diagonal":
|
|
to = HandTo.RightDiagonalAcrossCircle;
|
|
endSet += startPos.which.isLeft() ? -1 : +1;
|
|
break;
|
|
case "left diagonal":
|
|
to = HandTo.LeftDiagonalAcrossCircle;
|
|
endSet += startPos.which.isLeft() ? +1 : -1;
|
|
break;
|
|
}
|
|
const startPosition = {
|
|
...startPos,
|
|
hands: new Map<Hand, HandConnection>([[chainHand, { hand: chainHand, to }]]),
|
|
facing: startPos.which.facingAcross(),
|
|
};
|
|
|
|
const turnTo = chainHand === Hand.Right ? HandTo.DancerRight : HandTo.DancerLeft;
|
|
|
|
return combine([
|
|
{
|
|
beats: pullBeats,
|
|
endPosition: {
|
|
...startPos,
|
|
which: endWhich,
|
|
facing: endWhich.facingUpOrDown(),
|
|
setOffset: endSet,
|
|
hands: new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: Hand.Left, to: turnTo }],
|
|
[Hand.Right, { hand: Hand.Right, to: turnTo }],
|
|
]),
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
around: "Center",
|
|
side: chainHand,
|
|
withHands: true,
|
|
facing: "Forward",
|
|
otherPath: "Swap",
|
|
}
|
|
},
|
|
{
|
|
beats: pullToTurnBeats,
|
|
endPosition: {
|
|
...startingPos,
|
|
kind: PositionKind.Circle,
|
|
which: endWhich.swapUpAndDown(),
|
|
facing: endWhich.facingOut()
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
side: chainHand.opposite(),
|
|
withHands: true,
|
|
around: endWhich.leftRightSide(),
|
|
facing: "Forward", // TODO Is this right?
|
|
otherPath: "Swap",
|
|
}
|
|
},
|
|
prevEnd => ({
|
|
beats: turnBeats,
|
|
endPosition: {
|
|
kind: PositionKind.Circle,
|
|
which: endWhich,
|
|
facing: endWhich.facingAcross(),
|
|
hands: prevEnd.hands,
|
|
setOffset: prevEnd.setOffset,
|
|
lineOffset: prevEnd.lineOffset,
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.CourtesyTurn,
|
|
clockwise: cwCourtesyTurn,
|
|
}
|
|
})
|
|
], startPosition);
|
|
} else {
|
|
const startingPos = { ...startPos, hands: undefined };
|
|
return combine([
|
|
{
|
|
beats: pullBeats,
|
|
endPosition: { ...startingPos, facing: startingPos.which.facingUpOrDown() },
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
}
|
|
},
|
|
{
|
|
beats: pullToTurnBeats,
|
|
endPosition: {
|
|
...startingPos,
|
|
which: startingPos.which.swapUpAndDown(),
|
|
facing: startingPos.which.facingOut()
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
side: chainHand.opposite(),
|
|
withHands: true,
|
|
around: startingPos.which.leftRightSide(),
|
|
facing: "Forward", // TODO Is this right?
|
|
otherPath: "Swap",
|
|
}
|
|
},
|
|
{
|
|
beats: turnBeats,
|
|
endPosition: {
|
|
...startingPos,
|
|
// TODO Does CourtesyTurn always end in same position?
|
|
which: startPos.which,
|
|
facing: startPos.which.facingAcross(),
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.CourtesyTurn,
|
|
clockwise: cwCourtesyTurn,
|
|
}
|
|
}
|
|
], startingPos);
|
|
}
|
|
});
|
|
|
|
case "long lines":
|
|
return handleCircleMove(({ startPos }) => {
|
|
const startPosition: SemanticPosition = {
|
|
...startPos,
|
|
longLines: undefined,
|
|
// TODO Occassionally dances have long lines facing out. This will get that wrong.
|
|
facing: startPos.which.isLeft() ? Facing.Right : Facing.Left,
|
|
hands: new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }],
|
|
[Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }],
|
|
]),
|
|
};
|
|
|
|
if (move.parameters.go) {
|
|
const forwardBeats = move.beats / 2;
|
|
const backwardBeats = move.beats - forwardBeats;
|
|
return combine([
|
|
{
|
|
beats: forwardBeats,
|
|
endPosition: { ...startPosition, longLines: LongLines.Forward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
{
|
|
beats: backwardBeats,
|
|
endPosition: startPosition,
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
], startPosition);
|
|
} else {
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: { ...startPosition, longLines: LongLines.Forward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}], startPosition);
|
|
}
|
|
});
|
|
|
|
case "roll away":
|
|
// TODO maybe can roll away in short lines?
|
|
return handleCirclePairedMove(move.parameters.who, ({ id, startPos, withPos }) => {
|
|
let isRoller: boolean;
|
|
switch (move.parameters.who) {
|
|
case "gentlespoons":
|
|
isRoller = id.danceRole === DanceRole.Lark;
|
|
break;
|
|
case "ladles":
|
|
isRoller = id.danceRole === DanceRole.Robin;
|
|
break;
|
|
case "ones":
|
|
isRoller = id.coupleRole === CoupleRole.Ones;
|
|
break;
|
|
case "twos":
|
|
isRoller = id.coupleRole === CoupleRole.Twos;
|
|
break;
|
|
case "first corners":
|
|
case "second corners":
|
|
throw "Roll away in contra corners is unsupported.";
|
|
}
|
|
|
|
// TODO This isn't quite right if there's no 1/2 sash?
|
|
let swapPos = withPos;
|
|
if (swapPos.kind === PositionKind.Circle && swapPos.longLines) {
|
|
swapPos = { ...swapPos, longLines: undefined };
|
|
}
|
|
|
|
// TODO animate hands?
|
|
if (isRoller) {
|
|
if (move.parameters["½sash"]) {
|
|
// swap positions by sliding
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: swapPos,
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}], startPos);
|
|
} else {
|
|
// just stand still
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: { ...startPos, longLines: undefined },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}], startPos);
|
|
}
|
|
} else {
|
|
// being rolled away, so do a spin
|
|
return combine([{
|
|
beats: move.beats,
|
|
// TODO Is this the right end position logic?
|
|
endPosition: move.parameters["½sash"] ? swapPos : { ...startPos, which: startPos.which.swapDiagonal() },
|
|
movementPattern: { kind: SemanticAnimationKind.RollAway },
|
|
}], startPos);
|
|
}
|
|
});
|
|
|
|
case "slide along set":
|
|
const slideLeft = move.parameters.slide;
|
|
|
|
return handleCircleMove(({ startPos }) => {
|
|
const startingPos = {
|
|
...startPos,
|
|
facing: startPos.which.facingAcross(),
|
|
};
|
|
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: {
|
|
...startingPos,
|
|
setOffset: (startingPos.setOffset ?? 0) + (startingPos.which.isLeft() === slideLeft ? +0.5 : -0.5),
|
|
},
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}], startingPos);
|
|
});
|
|
|
|
case "slice":
|
|
if (move.parameters["slice increment"] === "dancer") {
|
|
// TODO Maybe this only actually gets used to move an entire couple by going diagonal back?
|
|
throw new Error("Slicing by a single dancer is unsupported.");
|
|
}
|
|
|
|
const sliceLeft = move.parameters.slide;
|
|
const sliceReturns = move.parameters["slice return"] !== "none";
|
|
|
|
const sliceForwardBeats = sliceReturns ? move.beats / 2 : move.beats;
|
|
const sliceBackwardBeats = move.beats - sliceForwardBeats;
|
|
|
|
return handleCircleMove(({ startPos }) => {
|
|
const startingPos: SemanticPosition & { setOffset: number } = {
|
|
kind: PositionKind.Circle,
|
|
which: startPos.which,
|
|
facing: startPos.which.facingAcross(),
|
|
hands: handToDancerToSideInCircleFacingAcross(startPos.which),
|
|
setOffset: startPos.setOffset ?? 0,
|
|
lineOffset: startPos.lineOffset,
|
|
};
|
|
const sliceAmount = startingPos.which.isLeft() === sliceLeft ? +0.5 : -0.5;
|
|
const forwardOffset = startingPos.setOffset + sliceAmount;
|
|
const endOffset = move.parameters["slice return"] === "diagonal" ? forwardOffset + sliceAmount : forwardOffset;
|
|
|
|
const sliceForward: PartialLowLevelMove = {
|
|
beats: sliceForwardBeats,
|
|
endPosition: { ...startingPos, setOffset: forwardOffset, longLines: LongLines.Forward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
};
|
|
const maybeSliceBackward: PartialLowLevelMove[] = sliceReturns ? [{
|
|
beats: sliceBackwardBeats,
|
|
endPosition: { ...startingPos, setOffset: endOffset },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}] : [];
|
|
|
|
return combine([sliceForward, ...maybeSliceBackward], startingPos);
|
|
});
|
|
|
|
case "down the hall":
|
|
if (move.parameters.who !== "everyone") {
|
|
throw new Error("Don't know what it means for not everyone to go down the hall.");
|
|
}
|
|
if (move.parameters.moving !== "all") {
|
|
throw new Error("Not sure what it means for not all to be moving in down the hall.");
|
|
}
|
|
if (move.parameters.ender !== "turn-alone" && move.parameters.ender !== "turn-couple") {
|
|
throw new Error("Unsupported down the hall ender: " + move.parameters.ender);
|
|
}
|
|
if (move.parameters.facing === "forward then backward") {
|
|
throw new Error("Not sure what " + move.parameters.facing + " means for down the hall.");
|
|
}
|
|
return handleMove(({ startPos }) => {
|
|
const startFacing = move.parameters.facing === "backward" ? Facing.Up : Facing.Down;
|
|
const startWhich: ShortLinesPosition = startPos.kind === PositionKind.ShortLines
|
|
? startPos.which
|
|
// TODO Is this always the right way to convert circle to short lines?
|
|
// (Does it even matter except for dance starting formations?)
|
|
: new Map<CirclePosition, ShortLinesPosition>([
|
|
[CirclePosition.TopLeft, ShortLinesPosition.FarLeft],
|
|
[CirclePosition.BottomLeft, ShortLinesPosition.MiddleLeft],
|
|
[CirclePosition.BottomRight, ShortLinesPosition.MiddleRight],
|
|
[CirclePosition.TopRight, ShortLinesPosition.FarRight],
|
|
]).get(startPos.which)!;
|
|
const startingPos: SemanticPosition & { kind: PositionKind.ShortLines, setOffset: number } = {
|
|
kind: PositionKind.ShortLines,
|
|
facing: startFacing,
|
|
which: startWhich,
|
|
hands: startWhich.isMiddle() ? new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }],
|
|
[Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }],
|
|
]) : new Map<Hand, HandConnection>([
|
|
startWhich.isLeft() === (move.parameters.facing === "backward")
|
|
? [Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }]
|
|
: [Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }]
|
|
]),
|
|
setOffset: startPos.setOffset ?? 0,
|
|
lineOffset: startPos.lineOffset,
|
|
};
|
|
|
|
return combine([
|
|
{
|
|
beats: 4,
|
|
endPosition: {
|
|
...startingPos,
|
|
setOffset: startingPos.setOffset + 1
|
|
},
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
{
|
|
beats: move.beats - 4,
|
|
endPosition: {
|
|
...startingPos,
|
|
setOffset: startingPos.setOffset + 1,
|
|
facing: oppositeFacing(startFacing),
|
|
which: move.parameters.ender === "turn-alone" ? startWhich : startWhich.swapOnSide(),
|
|
},
|
|
movementPattern: move.parameters.ender === "turn-couple"
|
|
? {
|
|
kind: SemanticAnimationKind.TwirlSwap,
|
|
around: startWhich.leftRightSide(),
|
|
// !== is NXOR, each of these booleans being flipped flips which hand to use.
|
|
hand: startWhich.isMiddle() !== (startWhich.isLeft() !== (move.parameters.facing === "forward"))
|
|
? Hand.Left
|
|
: Hand.Right
|
|
}
|
|
: {
|
|
kind: SemanticAnimationKind.Linear,
|
|
minRotation: startWhich.isMiddle() === startWhich.isLeft() ? -180 : +180,
|
|
},
|
|
},
|
|
], startingPos);
|
|
});
|
|
|
|
case "up the hall":
|
|
// TODO Share implementation between up/down the hall?
|
|
if (move.parameters.who !== "everyone") {
|
|
throw new Error("Don't know what it means for not everyone to go up the hall.");
|
|
}
|
|
if (move.parameters.moving !== "all") {
|
|
throw new Error("Not sure what it means for not all to be moving in up the hall.");
|
|
}
|
|
if (move.parameters.ender !== "circle") {
|
|
throw new Error("Unsupported up the hall ender: " + move.parameters.ender);
|
|
}
|
|
if (move.parameters.facing !== "forward") {
|
|
throw new Error("Unsupported up the hall facing: " + move.parameters.facing);
|
|
}
|
|
|
|
return handleMove(({ startPos }) => {
|
|
const startFacing = move.parameters.facing === "backward" ? Facing.Down : Facing.Up;
|
|
const startWhich: ShortLinesPosition = startPos.kind === PositionKind.ShortLines
|
|
? startPos.which
|
|
// TODO Is this always the right way to convert circle to short lines?
|
|
// (Does it even matter except for dance starting formations?)
|
|
: new Map<CirclePosition, ShortLinesPosition>([
|
|
[CirclePosition.TopLeft, ShortLinesPosition.MiddleLeft],
|
|
[CirclePosition.BottomLeft, ShortLinesPosition.FarLeft],
|
|
[CirclePosition.BottomRight, ShortLinesPosition.FarRight],
|
|
[CirclePosition.TopRight, ShortLinesPosition.MiddleRight],
|
|
]).get(startPos.which)!;
|
|
const startingPos: SemanticPosition & { kind: PositionKind.ShortLines, setOffset: number } = {
|
|
kind: PositionKind.ShortLines,
|
|
facing: startFacing,
|
|
which: startWhich,
|
|
hands: startWhich.isMiddle() ? new Map<Hand, HandConnection>([
|
|
[Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }],
|
|
[Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }],
|
|
]) : new Map<Hand, HandConnection>([
|
|
startWhich.isLeft() === (move.parameters.facing === "backward")
|
|
? [Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }]
|
|
: [Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }]
|
|
]),
|
|
setOffset: startPos.setOffset ?? 0,
|
|
lineOffset: startPos.lineOffset,
|
|
};
|
|
const endWhich = new Map<ShortLinesPosition, CirclePosition>([
|
|
[ShortLinesPosition.FarLeft, CirclePosition.TopLeft],
|
|
[ShortLinesPosition.MiddleLeft, CirclePosition.BottomLeft],
|
|
[ShortLinesPosition.MiddleRight, CirclePosition.BottomRight],
|
|
[ShortLinesPosition.FarRight, CirclePosition.TopRight],
|
|
]).get(startWhich)!;
|
|
const endingPos: SemanticPosition & { kind: PositionKind.Circle } = {
|
|
kind: PositionKind.Circle,
|
|
which: endWhich,
|
|
facing: Facing.CenterOfCircle,
|
|
setOffset: startingPos.setOffset - 1,
|
|
lineOffset: startingPos.lineOffset,
|
|
hands: handsInCircle,
|
|
}
|
|
|
|
return combine([
|
|
{
|
|
beats: 4,
|
|
endPosition: {
|
|
...startingPos,
|
|
setOffset: startingPos.setOffset - 1
|
|
},
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
{
|
|
beats: move.beats - 4,
|
|
endPosition: endingPos,
|
|
// TODO Is bend the line just linear?
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}], startingPos);
|
|
});
|
|
|
|
case "form an ocean wave":
|
|
if (move.parameters["pass thru"]) {
|
|
throw new Error("Pass thru to ocean wave is unsupported.");
|
|
}
|
|
if (move.parameters.dir !== "across") {
|
|
throw new Error("Diagonal ocean waves are unsupported.");
|
|
}
|
|
const centerHand = move.parameters["c.hand"] ? Hand.Right : Hand.Left;
|
|
return handleMove(({ id, startPos }) => {
|
|
const isCenter = findPairOpposite(move.parameters.center, id) !== null;
|
|
|
|
const which = startPos.which.isLeft()
|
|
? (isCenter ? ShortLinesPosition.MiddleLeft : ShortLinesPosition.FarLeft)
|
|
: (isCenter ? ShortLinesPosition.MiddleRight : ShortLinesPosition.FarRight);
|
|
const facing = (centerHand === Hand.Right) === (which === ShortLinesPosition.MiddleLeft || which === ShortLinesPosition.FarRight) ? Facing.Down : Facing.Up;
|
|
|
|
const linePos: SemanticPosition = {
|
|
kind: PositionKind.ShortLines,
|
|
which,
|
|
facing,
|
|
hands: handsInLine({ wavy: true, which, facing }),
|
|
setOffset: startPos.setOffset,
|
|
lineOffset: startPos.lineOffset,
|
|
};
|
|
|
|
if (move.parameters.bal) {
|
|
// TODO Is balance weight always forward/backward here?
|
|
const balanceBeats = Math.min(move.beats, 4);
|
|
const transitionBeats = move.beats - balanceBeats;
|
|
const balanceForwardBeats = balanceBeats / 2;
|
|
const balanceBackwardBeats = balanceBeats - balanceForwardBeats;
|
|
|
|
const balance: [PartialLowLevelMove, PartialLowLevelMove] = [
|
|
{
|
|
beats: balanceForwardBeats,
|
|
endPosition: { ...linePos, balance: BalanceWeight.Forward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
{
|
|
beats: balanceBackwardBeats,
|
|
endPosition: { ...linePos, balance: BalanceWeight.Backward },
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
];
|
|
if (transitionBeats === 0) {
|
|
// No transition, just balance.
|
|
return combine(balance, linePos);
|
|
} else {
|
|
return combine([{
|
|
beats: transitionBeats,
|
|
endPosition: linePos,
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
},
|
|
...balance], startPos);
|
|
}
|
|
} else {
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: linePos,
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}], startPos);
|
|
}
|
|
});
|
|
|
|
case "pass through":
|
|
if (move.parameters.dir !== "along") {
|
|
throw new Error("Unsupported pass through direction: " + move.parameters.dir);
|
|
}
|
|
const passShoulder = move.parameters.shoulder ? Hand.Right : Hand.Left;
|
|
return handleMove(({ startPos }) => {
|
|
if (startPos.kind === PositionKind.Circle) {
|
|
const facing = startPos.which.facingUpOrDown();
|
|
const endPos: SemanticPosition = {
|
|
kind: PositionKind.Circle,
|
|
which: startPos.which,
|
|
facing,
|
|
setOffset: (startPos.setOffset ?? 0) + (facing === Facing.Up ? -0.5 : +0.5),
|
|
lineOffset: startPos.lineOffset,
|
|
};
|
|
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: endPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
around: startPos.which.leftRightSide(),
|
|
side: passShoulder,
|
|
withHands: false,
|
|
facing: "Start",
|
|
otherPath: "Swap",
|
|
},
|
|
}], startPos);
|
|
} else {
|
|
// TODO This assumes short *wavy* lines.
|
|
|
|
const endPos: SemanticPosition = {
|
|
...startPos,
|
|
balance: undefined,
|
|
which: startPos.which.swapOnSide(),
|
|
setOffset: (startPos.setOffset ?? 0) + (startPos.facing === Facing.Up ? -0.5 : +0.5),
|
|
};
|
|
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: endPos,
|
|
movementPattern: { kind: SemanticAnimationKind.Linear },
|
|
}], startPos);
|
|
}
|
|
});
|
|
|
|
case "right left through":
|
|
if (move.parameters.dir !== "across") {
|
|
throw new Error(move.move + " with dir " + move.parameters.dir + " is unsupported.");
|
|
}
|
|
|
|
return handleCircleMove(({startPos}) => {
|
|
const startingPos = { ...startPos, facing: startPos.which.facingAcross() };
|
|
const swappedPos = { ...startingPos, which: startingPos.which.swapAcross() };
|
|
return combine([
|
|
{
|
|
beats: move.beats / 2,
|
|
endPosition: swappedPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
side: Hand.Right,
|
|
withHands: true,
|
|
facing: "Start",
|
|
around: startingPos.which.topBottomSide(),
|
|
otherPath: "Swap",
|
|
},
|
|
},
|
|
{
|
|
beats: move.beats / 2,
|
|
endPosition: {
|
|
...startingPos,
|
|
which: startingPos.which.swapDiagonal(),
|
|
facing: startingPos.which.facingOut()
|
|
},
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.CourtesyTurn,
|
|
},
|
|
}
|
|
], startingPos);
|
|
});
|
|
|
|
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);
|
|
}
|
|
if (move.parameters.rico1 || move.parameters.rico2 || move.parameters.rico3 || move.parameters.rico4) {
|
|
throw new Error("Ricochet hey is unsupported.");
|
|
}
|
|
let heyParts: number;
|
|
switch (move.parameters.until) {
|
|
case "half":
|
|
heyParts = 4;
|
|
break;
|
|
case "full":
|
|
heyParts = 8;
|
|
break;
|
|
default:
|
|
throw new Error("Unsupported hey 'until': " + move.parameters.until);
|
|
}
|
|
const heyPartBeats: number = move.beats / heyParts;
|
|
// TODO is this right?
|
|
const firstPassInCenter: boolean = dancerIsPair(move.parameters.who);
|
|
const centerShoulder = firstPassInCenter === move.parameters.shoulder ? Hand.Right : Hand.Left;
|
|
const endsShoulder = centerShoulder.opposite();
|
|
|
|
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,
|
|
// TODO use circle positions on ends? ... unless hey ends in a box the gnat or similar...
|
|
endPosition: heyStep.endPosition,
|
|
movementPattern: heyStep.kind === "StandStill" ? {
|
|
kind: SemanticAnimationKind.StandStill,
|
|
} : heyStep.kind === "Loop" ? {
|
|
// TODO Loop should probably be its own kind? Or RotateAround?
|
|
kind: SemanticAnimationKind.Linear,
|
|
minRotation: endsShoulder === Hand.Right ? +180 : -180,
|
|
} : {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
around: heyStep.kind === "CenterPass" ? "Center" : heyStep.endPosition.which.leftRightSide(),
|
|
withHands: false,
|
|
side: heyStep.kind === "CenterPass" ? centerShoulder : endsShoulder,
|
|
facing: "Start",
|
|
otherPath: undefined!, // Placeholder, fixup later.
|
|
},
|
|
heyStep,
|
|
};
|
|
}
|
|
function continueHey(prevStep: HeyStep, stepsLeft: number): HeyStep {
|
|
// Continuing hey so everyone is either passing (in center or on ends) or looping on ends.
|
|
if (prevStep.endPosition.kind === PositionKind.Circle) {
|
|
if (prevStep.endPosition.facing === prevStep.endPosition.which.facingAcross()) {
|
|
if (stepsLeft === 0) {
|
|
return {
|
|
kind: "StandStill",
|
|
endPosition: prevStep.endPosition,
|
|
}
|
|
}
|
|
return {
|
|
kind: "EndsPassIn",
|
|
endPosition: {
|
|
kind: PositionKind.ShortLines,
|
|
which: prevStep.endPosition.which.isLeft() ? ShortLinesPosition.MiddleLeft : ShortLinesPosition.MiddleRight,
|
|
facing: prevStep.endPosition.which.facingAcross(),
|
|
setOffset: prevStep.endPosition.setOffset,
|
|
lineOffset: prevStep.endPosition.lineOffset,
|
|
},
|
|
}
|
|
}
|
|
else {
|
|
if (stepsLeft === 1 && !endsInCircle) {
|
|
return {
|
|
kind: "Loop",
|
|
endPosition: {
|
|
kind: PositionKind.ShortLines,
|
|
which: prevStep.endPosition.which.isLeft() ? ShortLinesPosition.FarLeft : ShortLinesPosition.FarRight,
|
|
facing: prevStep.endPosition.which.facingAcross(),
|
|
setOffset: prevStep.endPosition.setOffset,
|
|
lineOffset: prevStep.endPosition.lineOffset,
|
|
},
|
|
}
|
|
}
|
|
return {
|
|
kind: "Loop",
|
|
endPosition: {
|
|
...prevStep.endPosition,
|
|
which: prevStep.endPosition.which.swapUpAndDown(),
|
|
facing: prevStep.endPosition.which.facingAcross()
|
|
},
|
|
}
|
|
}
|
|
}
|
|
else if (prevStep.endPosition.kind === PositionKind.ShortLines) {
|
|
const isFacingSide = prevStep.endPosition.facing === prevStep.endPosition.which.facingSide();
|
|
const inMiddle = prevStep.endPosition.which.isMiddle();
|
|
if (!inMiddle && !isFacingSide) {
|
|
return {
|
|
kind: "Loop",
|
|
endPosition: { ...prevStep.endPosition, facing: prevStep.endPosition.which.facingSide() },
|
|
}
|
|
} else if (inMiddle && isFacingSide) {
|
|
return {
|
|
kind: "EndsPassOut",
|
|
endPosition: {
|
|
...prevStep.endPosition,
|
|
kind: PositionKind.Circle,
|
|
which: prevStep.endPosition.which.isLeft()
|
|
? (endsShoulder === Hand.Right ? CirclePosition.TopLeft : CirclePosition.BottomLeft)
|
|
: (endsShoulder === Hand.Right ? CirclePosition.BottomRight : CirclePosition.TopRight),
|
|
},
|
|
}
|
|
}
|
|
else {
|
|
return {
|
|
kind: isFacingSide ? (inMiddle ? "EndsPassOut" : "EndsPassIn") : "CenterPass",
|
|
endPosition: {
|
|
...prevStep.endPosition,
|
|
which: isFacingSide ? prevStep.endPosition.which.swapOnSide() : prevStep.endPosition.which.swapSides()
|
|
},
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error("Unexpected PositionKind: " + (<any>prevStep.endPosition).kind);
|
|
}
|
|
}
|
|
|
|
let firstHeyStep: HeyStep;
|
|
let startingPos: SemanticPosition;
|
|
if (firstPassInCenter) {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw new Error("Hey starting in center not from circle is unsupported.");
|
|
}
|
|
const inCenterFirst = findPairOpposite(move.parameters.who, id) !== null;
|
|
startingPos = {
|
|
kind: startPos.kind,
|
|
which: startPos.which,
|
|
facing: startPos.which.isLeft() ? Facing.Right : Facing.Left,
|
|
setOffset: startPos.setOffset,
|
|
lineOffset: startPos.lineOffset,
|
|
};
|
|
if (inCenterFirst) {
|
|
firstHeyStep = {
|
|
kind: "CenterPass",
|
|
endPosition: {
|
|
kind: PositionKind.ShortLines,
|
|
which: startPos.which.isLeft() ? ShortLinesPosition.MiddleRight : ShortLinesPosition.MiddleLeft,
|
|
facing: startingPos.facing,
|
|
setOffset: startPos.setOffset,
|
|
lineOffset: startPos.lineOffset,
|
|
}
|
|
};
|
|
} else {
|
|
firstHeyStep = {
|
|
kind: "StandStill",
|
|
endPosition: startingPos,
|
|
}
|
|
}
|
|
} else {
|
|
if (startPos.kind !== PositionKind.ShortLines) {
|
|
throw new Error("Hey with first pass on ends must start approximately in short lines.");
|
|
}
|
|
|
|
const startFacing = startPos.which.facingSide();
|
|
startingPos = {
|
|
kind: startPos.kind,
|
|
which: startPos.which,
|
|
facing: startFacing,
|
|
setOffset: startPos.setOffset,
|
|
lineOffset: startPos.lineOffset,
|
|
};
|
|
firstHeyStep = {
|
|
kind: startingPos.which.isMiddle() ? "EndsPassOut" : "EndsPassIn",
|
|
endPosition: { ...startingPos, which: startPos.which.swapOnSide() },
|
|
}
|
|
}
|
|
|
|
const heySteps: HeyStep[] = [firstHeyStep];
|
|
for(let i = 1; i < heyParts; i++) {
|
|
const isLast = i === heyParts - 1;
|
|
const nextHeyStep = continueHey(heySteps[i - 1], heyParts - i - 1);
|
|
heySteps.push(nextHeyStep);
|
|
}
|
|
return combine(heySteps.map(heyStepToPartialLowLevelMove), { ...startingPos, hands: undefined });
|
|
}));
|
|
|
|
case "turn alone":
|
|
if (move.parameters.who !== "everyone" || move.beats !== 0) {
|
|
throw new Error("turn alone unsupported except for changing to new circle.");
|
|
}
|
|
return handleCircleMove(({startPos}) => {
|
|
const which = startPos.which.swapUpAndDown();
|
|
const startAndEndPos: SemanticPosition = {
|
|
...startPos,
|
|
which,
|
|
facing: which.facingUpOrDown(),
|
|
setOffset: (startPos.setOffset ?? 0) + (startPos.which.isTop() ? +0.5 : -0.5),
|
|
}
|
|
|
|
return combine([{
|
|
beats: move.beats,
|
|
endPosition: startAndEndPos,
|
|
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
|
}], startAndEndPos);
|
|
})
|
|
|
|
case "custom":
|
|
if (move.parameters.custom.includes("mirrored mad robin")) {
|
|
return handleCircleMove(({ id, startPos }) => {
|
|
// TODO Read custom to decide direction?
|
|
const startAndEndPos = {
|
|
...startPos,
|
|
facing: startPos.which.facingAcross(),
|
|
hands: undefined,
|
|
};
|
|
return combine([{
|
|
beats: move.beats,
|
|
startPosition: startAndEndPos,
|
|
endPosition: startAndEndPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.DoSiDo,
|
|
amount: startPos.which.isLeft() ? -360 : 360,
|
|
around: startPos.which.leftRightSide(),
|
|
},
|
|
}]);
|
|
});
|
|
}
|
|
}
|
|
|
|
// XXX DEBUG Just leave out unsupported moves for now to allow viewing the known moves.
|
|
//throw "Unknown move: " + move.move + ": " + JSON.stringify(move);
|
|
return handleMove(({ startPos }) => {
|
|
return [{
|
|
interpreterError: "UNKNOWN MOVE '" + move.move + "': standing still",
|
|
move,
|
|
startBeat: 0,
|
|
beats: move.beats,
|
|
startPosition: startPos,
|
|
endPosition: startPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.StandStill,
|
|
},
|
|
}];
|
|
});
|
|
}
|
|
|
|
function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, SemanticPosition>): Map<DancerIdentity, LowLevelMove[]> {
|
|
const res = new Map<DancerIdentity, LowLevelMove[]>([...startingPos.keys()].map(id => [id, []]));
|
|
let currentPos = new Map<DancerIdentity, SemanticPosition>(startingPos);
|
|
let numProgessions = 0;
|
|
for (let i = 0; i < moves.length; i++) {
|
|
const move = moves[i];
|
|
const nextMove = i === moves.length - 1 ? moves[0] : moves[i + 1];
|
|
try {
|
|
if (i > 0 && move.beats === 0 && move.move === "slide along set") {
|
|
const slideLeft: boolean = move.parameters.slide;
|
|
for (const [id, currPos] of currentPos.entries()) {
|
|
const slideAmount = (currPos.which.leftRightSide() === CircleSide.Left) === slideLeft ? +0.5 : -0.5;
|
|
const setOffset = (currPos.setOffset ?? 0) + slideAmount;
|
|
currentPos.set(id, { ...currPos, setOffset });
|
|
|
|
const prevMove = res.get(id)!.at(-1)!;
|
|
prevMove.movementPattern.setSlideAmount = slideAmount;
|
|
prevMove.endPosition.setOffset = setOffset;
|
|
}
|
|
|
|
} else {
|
|
const newMoves = moveAsLowLevelMoves({ move, nextMove, startingPos: currentPos, numProgessions });
|
|
for (const [id, newMoveList] of newMoves.entries()) {
|
|
res.get(id)!.push(...newMoveList);
|
|
currentPos.set(id, newMoveList.at(-1)!.endPosition);
|
|
}
|
|
}
|
|
}
|
|
catch (ex) {
|
|
// catch exception so something can be displayed
|
|
for (const [id, pos] of currentPos.entries()) {
|
|
res.get(id)!.push({
|
|
beats: move.beats,
|
|
startPosition: pos,
|
|
endPosition: pos,
|
|
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
|
move,
|
|
startBeat: 0,
|
|
interpreterError: ex instanceof Error ? ex.message : ex,
|
|
});
|
|
}
|
|
}
|
|
if (move.progression) numProgessions++;
|
|
}
|
|
|
|
try {
|
|
const progression = animateFromLowLevelMoves(res).progression;
|
|
const progressionInSets = progression.y / setDistance;
|
|
|
|
// fixup end positions to match start of next move
|
|
// TODO Handle progression.
|
|
for (const [id, lowLevelMoves] of res.entries()) {
|
|
for (let i = 0; i < lowLevelMoves.length - 1; i++) {
|
|
if (!lowLevelMoves[i].endPosition) throw "endPosition is undefined";
|
|
lowLevelMoves[i].endPosition = lowLevelMoves[i + 1].startPosition;
|
|
if (!lowLevelMoves[i].endPosition) throw "endPosition is undefined now";
|
|
if (lowLevelMoves[i].movementPattern.kind === SemanticAnimationKind.StandStill) {
|
|
lowLevelMoves[i].startPosition = lowLevelMoves[i].endPosition;
|
|
if (i > 0) {
|
|
lowLevelMoves[i - 1].endPosition = lowLevelMoves[i].startPosition;
|
|
}
|
|
}
|
|
}
|
|
// If progression isn't detected properly, do nothing.
|
|
if (progressionInSets === 0) {
|
|
lowLevelMoves[lowLevelMoves.length - 1].interpreterError = "No progression detected. Not lining up end with start of dance.";
|
|
} else {
|
|
const startPos = lowLevelMoves[0].startPosition;
|
|
lowLevelMoves[lowLevelMoves.length - 1].endPosition = {
|
|
...lowLevelMoves[0].startPosition,
|
|
// progressed
|
|
setOffset: (startPos.setOffset ?? 0) + (id.coupleRole == CoupleRole.Ones ? 1 : -1) * progressionInSets,
|
|
};
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
catch (ex) {
|
|
res.get(DancerIdentity.OnesLark)![0].interpreterError = "Error detecting progression: " + (ex instanceof Error ? ex.message : ex);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
function StartingPosForFormation(formation: common.StartFormation): Map<DancerIdentity, SemanticPosition> {
|
|
switch (formation) {
|
|
case "improper":
|
|
return handsFourImproper;
|
|
case "Becket":
|
|
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
|
el => {
|
|
const [id, pos] = el;
|
|
if (pos.kind !== PositionKind.Circle) throw "Unreachable; improper starts in a circle.";
|
|
return ([id, {...pos, which: pos.which.circleLeft(1)}])
|
|
}
|
|
));
|
|
case "Becket ccw":
|
|
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
|
el => {
|
|
const [id, pos] = el;
|
|
if (pos.kind !== PositionKind.Circle) throw "Unreachable; improper starts in a circle.";
|
|
return ([id, {...pos, which: pos.which.circleRight(1)}])
|
|
}
|
|
));
|
|
case "Sawtooth Becket":
|
|
// Dancers start becket, then slide one person to the right. https://contradb.com/dances/848
|
|
// TODO Not sure this is right.
|
|
return new Map<common.DancerIdentity, SemanticPosition>([
|
|
[DancerIdentity.OnesLark, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.BottomLeft,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
}],
|
|
[DancerIdentity.OnesRobin, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.TopLeft,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
}],
|
|
[DancerIdentity.TwosLark, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.BottomRight,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
}],
|
|
[DancerIdentity.TwosRobin, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.TopRight,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
setOffset: 1,
|
|
}],
|
|
]);
|
|
}
|
|
}
|
|
|
|
export let mappedDance: Move[];
|
|
|
|
export let interpretedDance: Map<DancerIdentity, LowLevelMove[]>;
|
|
export let interpretedAnimation: animation.Animation;
|
|
|
|
export function loadDance(dance: LibFigureDance, formation: common.StartFormation): animation.Animation {
|
|
mappedDance = dance.map(nameLibFigureParameters);
|
|
interpretedDance = danceAsLowLevelMoves(mappedDance, StartingPosForFormation(formation));
|
|
interpretedAnimation = animateFromLowLevelMoves(interpretedDance);
|
|
|
|
return interpretedAnimation;
|
|
}
|