contra-renderer/www/js/interpreter.ts

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;
}