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 { return which.isMiddle() ? new Map([ [Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }], [Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }], ]) : new Map([ 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.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 { return new Map([ which.isOnLeftLookingAcross() ? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }] : [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }] ]); } function handToDancerToSideInCircleFacingUpOrDown(which: CirclePosition): Map { return new Map([ 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.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; numProgessions: number; }): Map { 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 { const res = new Map(); 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 { 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 { 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 { 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.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([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.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([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, { 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, { 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([[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.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.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.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.Left, { hand: Hand.Left, to: HandTo.DancerLeft }], [Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }], ]) : new Map([ 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.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.Left, { hand: Hand.Left, to: HandTo.DancerLeft }], [Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }], ]) : new Map([ 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.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): Map { 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: " + (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): Map { const res = new Map([...startingPos.keys()].map(id => [id, []])); let currentPos = new Map(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 { switch (formation) { case "improper": return handsFourImproper; case "Becket": return new Map([...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([...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([ [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; 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; }