import * as animation from "./animation.js"; import * as common from "./danceCommon.js"; import { DanceRole, DancerIdentity, Rotation } from "./danceCommon.js"; import { BalanceWeight, CirclePosition, CircleSide, CircleSideOrCenter, DancerDistance, Facing, HandConnection, HandTo, LongLines, PositionKind, SemanticPosition, ShortLinesPosition, StarGrip, handsInCircle } from "./interpreterCommon.js"; import { Move } from "./libfigureMapper.js"; import { DancerSetPosition, Hand, Offset, OffsetPlus, OffsetRotate, OffsetTimes, OffsetTranspose, dancerHeight, dancerHeightOffset, dancerWidth, lineDistance, offsetZero, setDistance, setHeight, setSpacing, setWidth } from "./rendererConstants.js"; export enum SemanticAnimationKind { StandStill = "StandStill", // Long lines, balances?, slide, walk forward to new wave Linear = "Linear", // Rotate around a set: circle left/right, star Circle = "Circle", Star = "Star", // Walk in rectangle pattern passing by someone. Also Mad Robin and See Saw. Pousset might fit here? DoSiDo = "DoSiDo", // Also pull by. PassBy = "PassBy", // California twirl, box the gnat, etc. TwirlSwap = "TwirlSwap", // Rotate as a pair around a point. Allemande, right/left shoulder round RotateAround = "RotateAround", Swing = "Swing", // Walk side-by-side with both hands to same hand, optionally TwirlSwap at end. Promenade = "Promenade", CourtesyTurn = "CourtesyTurn", // Dancer being rolled away / spinning. Other dancer is just Linear. RollAway = "RollAway", } export type SemanticAnimation = { setSlideAmount?: number, } & ({ kind: SemanticAnimationKind.StandStill, } | { kind: SemanticAnimationKind.Linear, // Approximate rotation. endPosition determines final orientation, generally a multiple of +/-360. minRotation?: number, handsDuring?: "Linear" | "None" | "Start" | "End"; } | { kind: SemanticAnimationKind.Circle, minRotation?: number, // positive is to the left. places: number, } | { kind: SemanticAnimationKind.DoSiDo, minRotation?: number, // amount to do si do, measured in degrees clockwise (negative for counterclockwise/passing by left) amount: number, around: CircleSideOrCenter, } | { kind: SemanticAnimationKind.RotateAround, minAmount: number, around: CircleSideOrCenter, byHand: Hand | undefined, // If true, move in close while rotating. close: boolean, facing?: animation.RotationAnimationFacing, } | { kind: SemanticAnimationKind.Swing, minAmount: number, around: CircleSideOrCenter, endFacing: Facing, // Swings are asymmetric. This is usually but not always the dancer's role. swingRole: common.DanceRole, // After a take need to fixup the position. afterTake: boolean, } | { kind: SemanticAnimationKind.TwirlSwap, around: CircleSide, hand: Hand, } | { kind: SemanticAnimationKind.PassBy, around: CircleSideOrCenter, otherPath: "Swap" | { start: SemanticPosition, end: SemanticPosition, } side: Hand, // If true, pull by the specified hand, if false, just pass by that side without hands. withHands: boolean, // Face the same direction the whole time or in the direction of the movement. facing: "Start" | "Forward", } | { kind: SemanticAnimationKind.CourtesyTurn, clockwise?: boolean, } | { kind: SemanticAnimationKind.RollAway, } | { kind: SemanticAnimationKind.Promenade, swingRole: DanceRole, twirl: boolean, passBy: Hand, } | { kind: SemanticAnimationKind.Star, hand: Hand, grip?: StarGrip, // Positive is to the left. Note that means this is always negative for a left hand star. places: number, }); // TODO export interface LowLevelMove { // for debugging messages remarks?: string, interpreterError?: string, // move is here for reference/debugging, shouldn't actually get used. move: Move, // for debugging, if move has been broken into multiple low-level moves, where does this one start startBeat: number, // How many beats does this take up? beats: number, // TODO decide if startPosition, endPosition, or both need to adjust for adjacent moves. // TODO when are hands optional? // Position this should start in. Previous move might not end here? Or should adjust to end here? startPosition: SemanticPosition, // Position this should end in. Next move night not star there? Or should adjust to start here? endPosition: SemanticPosition, movementPattern: SemanticAnimation, // TODO assertion of spin direction? } function CenterOfSet(setOffset?: number, lineOffset?: number): Offset { let position = {x: 0, y: 0}; if (setOffset) position.y += setOffset * setDistance; if (lineOffset) position.x += lineOffset * lineDistance; return position; } function CenterOf(sideOrCenter: CircleSideOrCenter, setOffset?: number, lineOffset?: number): Offset { const setCenter = CenterOfSet(setOffset, lineOffset); if (sideOrCenter === "Center") return setCenter; const side : CircleSide = sideOrCenter; const xOffset = side === CircleSide.Left ? -1 : side === CircleSide.Right ? +1 : 0; const yOffset = side === CircleSide.Top ? -1 : side === CircleSide.Bottom ? +1 : 0; return { x: setCenter.x + xOffset, y: setCenter.y + yOffset }; } function swingHandPosition(role: DancerDistance.SwingLark | DancerDistance.SwingRobin, hand: Hand): Offset { if (role === DancerDistance.SwingLark) { if (hand === Hand.Left) { return { x: 0, y: Math.sqrt(1 / 2) }; } else { return { x: 0.8, y: 0.5 }; } } else /* role === DancerDistance.SwingRobin */ { if (hand === Hand.Left) { return { x: -0.8, y: 0.5 }; } else { return { x: 0, y: Math.sqrt(1 / 2) }; }; } } function swingHandPositions(role: DancerDistance.SwingLark | DancerDistance.SwingRobin): Map { return new Map([ [Hand.Left, swingHandPosition(role, Hand.Left)], [Hand.Right, swingHandPosition(role, Hand.Right)], ]); } function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition { let rotation: number; let position: Offset; const balanceAmount = 0.25; const setOffset = CenterOfSet(semantic.setOffset, semantic.lineOffset); switch (semantic.kind) { case PositionKind.Circle: switch (semantic.facing) { case Facing.CenterOfCircle: rotation = semantic.which.facingCenterRotation(); break; case Facing.LeftInCircle: rotation = +90 + semantic.which.facingCenterRotation(); break; case Facing.RightInCircle: rotation = -90 + semantic.which.facingCenterRotation(); break; case Facing.Up: rotation = Rotation.Up; break; case Facing.Down: rotation = Rotation.Down; break; case Facing.Left: rotation = Rotation.Left; break; case Facing.Right: rotation = Rotation.Right; break; default: throw "Unexpected facing: " + semantic.facing; } let yAmount : number; switch (semantic.dancerDistance) { case DancerDistance.Normal: case undefined: yAmount = 1; break; case DancerDistance.Compact: case DancerDistance.SwingLark: case DancerDistance.SwingRobin: yAmount = 0.5; break; default: throw "Unknown DancerDistance: " + (semantic).dancerDistance; } switch (semantic.which) { case CirclePosition.TopLeft: position = { x: -1, y: -yAmount }; break; case CirclePosition.TopRight: position = { x: +1, y: -yAmount }; break; case CirclePosition.BottomLeft: position = { x: -1, y: +yAmount }; break; case CirclePosition.BottomRight: position = { x: +1, y: +yAmount }; break; default: throw "Invalid circle position: " + semantic.which; } switch(semantic.longLines) { case LongLines.Center: position.x *= 0.1; case LongLines.Forward: position.x *= 0.3; case LongLines.Near: position.x *= 0.6; } let balanceOffset: Offset = offsetZero; if (semantic.balance) { if (semantic.facing === Facing.CenterOfCircle) { if (semantic.balance === BalanceWeight.Forward) { position = { x: position.x * (1-balanceAmount), y: position.y * (1-balanceAmount) }; } else { throw "Don't know what balancing in a circle not forward means. BalanceWeight=" + semantic.balance; } } else if ((semantic.facing === Facing.Up || semantic.facing === Facing.Down) && (semantic.balance === BalanceWeight.Backward || semantic.balance === BalanceWeight.Forward)) { balanceOffset = { x: 0, y: Math.sign(position.y) * ( semantic.balance === BalanceWeight.Forward ? -balanceAmount : balanceAmount) }; } else if ((semantic.facing === Facing.Left || semantic.facing === Facing.Right) && (semantic.balance === BalanceWeight.Backward || semantic.balance === BalanceWeight.Forward)) { balanceOffset = { x: (semantic.balance === BalanceWeight.Forward) === (semantic.facing === Facing.Left) ? -balanceAmount : balanceAmount, y: 0 }; } else if ((semantic.facing === Facing.Left || semantic.facing === Facing.Right) && (semantic.balance === BalanceWeight.Left || semantic.balance === BalanceWeight.Right)) { balanceOffset = { x: 0, y: (semantic.balance === BalanceWeight.Left) === (semantic.facing === Facing.Left) ? -balanceAmount : balanceAmount }; } else if ((semantic.facing === Facing.Up || semantic.facing === Facing.Down) && (semantic.balance === BalanceWeight.Left || semantic.balance === BalanceWeight.Right)) { balanceOffset = { x: (semantic.balance === BalanceWeight.Left) === (semantic.facing === Facing.Up) ? -balanceAmount : balanceAmount, y: 0 }; } } position = OffsetPlus(position, balanceOffset, setOffset); const isFacingUpOrDown = semantic.facing === Facing.Up || semantic.facing === Facing.Down; if (semantic.dancerDistance === DancerDistance.SwingLark || semantic.dancerDistance == DancerDistance.SwingRobin) { if (isFacingUpOrDown == (semantic.dancerDistance === DancerDistance.SwingLark)) { rotation -= 45; } else { rotation += 45; } return { position, rotation, leftArmEnd: swingHandPosition(semantic.dancerDistance, Hand.Left), rightArmEnd: swingHandPosition(semantic.dancerDistance, Hand.Right), }; } function processHandInCircle(hand: Hand, connection: HandConnection | undefined): Offset | undefined { // TODO Hands. Might need more info? Or need context of nearby dancer SemanticPositions? if (!connection) return undefined; const balanceHandAdjustment = OffsetRotate(balanceOffset, -rotation); switch (connection.to) { case HandTo.DancerLeft: return OffsetPlus({ x: -1, y: 0 }, balanceHandAdjustment); case HandTo.DancerRight: return OffsetPlus({ x: +1, y: 0 }, balanceHandAdjustment); case HandTo.DancerForward: const armLength = yAmount + (semantic.balance === BalanceWeight.Backward ? balanceAmount : semantic.balance === BalanceWeight.Forward ? -balanceAmount : 0); if (hand === connection.hand) { return { x: 0, y: +armLength }; } else { return { x: dancerWidth / 2 * (hand === Hand.Left ? -1 : +1), y: +armLength }; } case HandTo.LeftInCircle: case HandTo.RightInCircle: const xSign = connection.to === HandTo.LeftInCircle ? -1 : +1; const balanceFactor = semantic.balance === BalanceWeight.Forward ? 1 - balanceAmount : 1; return { x: balanceFactor * xSign / Math.sqrt(2), y: balanceFactor / Math.sqrt(2) }; case HandTo.DiagonalAcrossCircle: // TODO Is "diagonal" even enough information? return { x: -0.5, y: +0.5 } case HandTo.LeftDiagonalAcrossCircle: return { x: -0.5 - setDistance, y: +0.5 } case HandTo.RightDiagonalAcrossCircle: return { x: -0.5 + setDistance, y: +0.5 } default: throw "Unkown connection: " + connection.to; } } return { position, rotation, leftArmEnd: processHandInCircle(Hand.Left, semantic.hands?.get(Hand.Left)), rightArmEnd: processHandInCircle(Hand.Right, semantic.hands?.get(Hand.Right)), }; case PositionKind.ShortLines: let yOffset = 0; switch (semantic.facing) { case Facing.Up: rotation = Rotation.Up; yOffset = +dancerHeight; break; case Facing.Down: rotation = Rotation.Down; yOffset = -dancerHeight; break; case Facing.Left: rotation = Rotation.Left; break; case Facing.Right: rotation = Rotation.Right; break; default: throw "Unexpected facing: " + semantic.facing; } switch (semantic.which) { case ShortLinesPosition.FarLeft: position = { x: -1.5, y: yOffset }; break; case ShortLinesPosition.MiddleLeft: position = { x: -0.5, y: yOffset }; break; case ShortLinesPosition.MiddleRight: position = { x: +0.5, y: yOffset }; break; case ShortLinesPosition.FarRight: position = { x: +1.5, y: yOffset }; break; default: throw "Invalid circle position: " + semantic.which; } const shortWavesBalanceAmount = 0.1; if (semantic.balance) { if ((semantic.facing === Facing.Up || semantic.facing === Facing.Down) && (semantic.balance === BalanceWeight.Backward || semantic.balance === BalanceWeight.Forward)) { position = { x: position.x, y: position.y + ( (semantic.balance === BalanceWeight.Forward) === (semantic.facing === Facing.Up) ? -shortWavesBalanceAmount : shortWavesBalanceAmount) }; } else if ((semantic.facing === Facing.Left || semantic.facing === Facing.Right) && (semantic.balance === BalanceWeight.Backward || semantic.balance === BalanceWeight.Forward)) { position = { x: position.x + ( (semantic.balance === BalanceWeight.Forward) === (semantic.facing === Facing.Left) ? -balanceAmount : balanceAmount), y: position.y }; } else if ((semantic.facing === Facing.Up || semantic.facing === Facing.Down) && (semantic.balance === BalanceWeight.Left || semantic.balance === BalanceWeight.Right)) { position = { x: position.x + ( (semantic.balance === BalanceWeight.Left) === (semantic.facing === Facing.Up) ? -balanceAmount : balanceAmount), y: position.y }; } } position.x += setOffset.x; position.y += setOffset.y; function processHandInShortLines(hand: Hand, connection: HandConnection | undefined): Offset | undefined { // TODO Hands. Might need more info? Or need context of nearby dancer SemanticPositions? if (!connection) return undefined; const balanceYOffset = (semantic.facing === Facing.Up ? yOffset : -yOffset) + (semantic.balance === BalanceWeight.Forward ? -shortWavesBalanceAmount : semantic.balance === BalanceWeight.Backward ? shortWavesBalanceAmount : 0); const balanceXOffset = semantic.balance === BalanceWeight.Left ? -balanceAmount : semantic.balance === BalanceWeight.Right ? balanceAmount : 0; switch (connection.to) { case HandTo.DancerLeft: return { x: -0.5 + balanceXOffset, y: balanceYOffset }; case HandTo.DancerRight: return { x: +0.5 + balanceXOffset, y: balanceYOffset }; case HandTo.DancerForward: if (hand === connection.hand) { return { x: 0, y: +0.5 }; } else { return { x: dancerWidth / 2 * (hand === Hand.Left ? -1 : +1), y: +1 }; } default: throw "Unkown connection: " + connection.to; } } return { position, rotation, leftArmEnd: processHandInShortLines(Hand.Left, semantic.hands?.get(Hand.Left)), rightArmEnd: processHandInShortLines(Hand.Right, semantic.hands?.get(Hand.Right)), }; default: throw "Unsupported PositionKind: " + (semantic).kind; } } export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegment[] { if (!move.movementPattern.setSlideAmount) { return animateLowLevelMoveWithoutSlide(move); } const withoutSlide = animateLowLevelMoveWithoutSlide({ ...move, movementPattern: { ...move.movementPattern, setSlideAmount: undefined }, endPosition: { ...move.endPosition, setOffset: (move.endPosition.setOffset ?? 0) - (move.movementPattern.setSlideAmount ?? 0) }, }); return [new animation.SlideAnimationSegment( move.beats, withoutSlide, { x: 0, y: move.movementPattern.setSlideAmount * setDistance })]; } function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.AnimationSegment[] { const startSetPosition = SemanticToSetPosition(move.startPosition); const endSetPosition = SemanticToSetPosition(move.endPosition); function handleMove(): animation.AnimationSegment[] { switch (move.movementPattern.kind) { case SemanticAnimationKind.StandStill: // TODO Where is the check that the current position is already move.endPosition? // And that end and start position are the same... modulo hands and rotation modulus? return [ animation.LinearAnimationSegment.standStill(startSetPosition, move.beats), ]; case SemanticAnimationKind.Linear: let rotation = endSetPosition.rotation - startSetPosition.rotation; let rotationDuring: boolean = true; const minRotation = move.movementPattern.minRotation; try { rotation = common.normalizeRotation(rotation, minRotation); } catch { rotation = common.normalizeRotation(rotation, undefined); rotationDuring = false; } return [ new animation.TransitionAnimationSegment({ actualAnimation: new animation.LinearAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: { ...endSetPosition, rotation: startSetPosition.rotation + rotation, } }), flags: { hands: (move.movementPattern.handsDuring ?? "Linear") !== "Linear", handsDuring: move.movementPattern.handsDuring === "Linear" ? undefined : move.movementPattern.handsDuring, rotation: !rotationDuring, rotationDuring: rotationDuring ? undefined : "Start", }, startTransitionBeats: 1, }), ]; case SemanticAnimationKind.Circle: case SemanticAnimationKind.Star: if (move.startPosition.kind !== PositionKind.Circle) { throw new Error(move.movementPattern.kind + " must start and end in a circle."); } const isCircle = move.movementPattern.kind === SemanticAnimationKind.Circle; const posWithHands = SemanticToSetPosition({...move.endPosition, hands: isCircle ? handsInCircle : undefined}) const circleHands = move.movementPattern.kind === SemanticAnimationKind.Circle ? new Map([ [Hand.Left, posWithHands.leftArmEnd!], [Hand.Right, posWithHands.rightArmEnd!], ]) : new Map([ [move.movementPattern.hand, { x: (move.movementPattern.hand === Hand.Right ? +1 : -1) * (1 + (move.movementPattern.grip === StarGrip.HandsAcross ? 0 : 0.15)) * Math.sqrt(2), y: move.movementPattern.grip === StarGrip.HandsAcross ? 0 : 0.25 }] ]); return [ new animation.TransitionAnimationSegment({ actualAnimation: new animation.RotationAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: endSetPosition, rotation: move.movementPattern.places * 90, around: { center: CenterOfSet(move.startPosition.setOffset, move.startPosition.lineOffset), width: setWidth, height: setHeight, }, facing: isCircle ? animation.RotationAnimationFacing.Center : animation.RotationAnimationFacing.Forward, closer: undefined, hands: new Map([ [Hand.Left, { kind: "End" }], [Hand.Right, { kind: "End" }], ]) }), flags: { rotation: true, hands: true, handsDuring: circleHands, }, startTransitionBeats: 1 }), ]; case SemanticAnimationKind.DoSiDo: // TODO Rotation // TODO Parameters: rotation, amount, direction, diagonal? if (move.startPosition.kind !== PositionKind.Circle) { throw "DoSiDo must start in a circle: " + JSON.stringify(move); } const center = CenterOf(move.startPosition.which.leftRightSide(), move.startPosition.setOffset, move.startPosition.lineOffset); return [ new animation.TransitionAnimationSegment({ actualAnimation: new animation.RotationAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: endSetPosition, rotation: move.movementPattern.amount, around: { center, width: setWidth / 3, height: setHeight, }, facing: animation.RotationAnimationFacing.Start, }), flags: { rotation: true, hands: true, handsDuring: "None", }, startTransitionBeats: 0, endTransitionBeats: 1, }), ]; case SemanticAnimationKind.RotateAround: // TODO Rotation // TODO Parameters: rotation, amount, direction, diagonal? // TODO Could rotate around other things. const rotateCenter = CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset); const hands = move.movementPattern.byHand ? new Map([ [move.movementPattern.byHand, { kind: "Center" }], [move.movementPattern.byHand.opposite(), { kind: "None" }], ]) : new Map([ [Hand.Left, { kind: "None" }], [Hand.Right, { kind: "None" }], ]); return [ new animation.TransitionAnimationSegment({ actualAnimation: new animation.RotationAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: endSetPosition, rotation: move.movementPattern.minAmount, around: { center: rotateCenter, width: 1, height: 1, }, facing: move.movementPattern.facing ?? animation.RotationAnimationFacing.Forward, closer: move.movementPattern.close ? { transitionBeats: 1, minDistance: 1, } : undefined, hands }), flags: { hands: true, rotation: true }, startTransitionBeats: 1 }), ]; case SemanticAnimationKind.Swing: const rotateSwingCenter = CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset); const dancerDistance = move.movementPattern.swingRole === DanceRole.Lark ? DancerDistance.SwingLark : DancerDistance.SwingRobin; const baseSwingStart = { ...move.startPosition, dancerDistance: move.movementPattern.afterTake ? undefined : dancerDistance, balance: undefined, longLines: undefined, } const swingStartUnadjusted = SemanticToSetPosition(move.startPosition.longLines === LongLines.Near ? { ...baseSwingStart, kind: PositionKind.Circle, which: move.startPosition.which.swapUpAndDown(), } : baseSwingStart); const swingStart = move.movementPattern.afterTake ? { ...swingStartUnadjusted, position: { x: swingStartUnadjusted.position.x + ((move.startPosition.longLines === LongLines.Near) === (move.startPosition.which.isLeft()) ? +0.5 : -0.5), y: rotateSwingCenter.y, } } : swingStartUnadjusted; const beforeUnfold = SemanticToSetPosition({ ...move.endPosition, dancerDistance, }); const unfolded = SemanticToSetPosition({ ...move.endPosition, dancerDistance: move.endPosition.dancerDistance === DancerDistance.Compact ? DancerDistance.Compact : DancerDistance.Normal, hands: move.movementPattern.swingRole === DanceRole.Lark ? new Map([[Hand.Right, {hand: Hand.Left, to: HandTo.DancerRight}]]) : new Map([[Hand.Left, {hand: Hand.Right, to: HandTo.DancerLeft}]]), }); const swingBeats = move.beats - 2; const beatsPerRotation = 3; const minTurns = Math.floor(swingBeats / beatsPerRotation); const turns = Math.ceil(Math.abs(move.movementPattern.minAmount / 360)); const swingMinRotation = (minTurns - turns) * (move.movementPattern.minAmount < 0 ? -360 : 360) + move.movementPattern.minAmount; const slideAmount = move.movementPattern.afterTake ? { x: 0, y: rotateSwingCenter.y - startSetPosition.position.y } : undefined; const swingAnimation = new animation.TransitionAnimationSegment({ actualAnimation: new animation.RotationAnimationSegment({ beats: swingBeats, startPosition: slideAmount ? { ...swingStart, position: OffsetPlus(swingStart.position, OffsetTimes(slideAmount, -1)) } : swingStart, endPosition: slideAmount ? { ...beforeUnfold, position: OffsetPlus(beforeUnfold.position, OffsetTimes(slideAmount, -1)) } : beforeUnfold, rotation: swingMinRotation, around: { center: slideAmount ? OffsetPlus(rotateSwingCenter, OffsetTimes(slideAmount, -1)) : rotateSwingCenter, width: 1, height: 1, }, closer: { minDistance: 1, transitionBeats: 1, }, facing: animation.RotationAnimationFacing.CenterRelativeOffset, centerRelativeTo: (dancerDistance === DancerDistance.SwingLark ? -45 : +45), }), flags: { rotation: true, hands: true, handsDuring: swingHandPositions(dancerDistance), }, startTransitionBeats: 1, endTransitionBeats: 0, }); return [ slideAmount ? new animation.SlideAnimationSegment( swingAnimation.beats, [swingAnimation], slideAmount, ) : swingAnimation, new animation.LinearAnimationSegment({ beats: 1, startPosition: beforeUnfold, endPosition: unfolded, }), new animation.LinearAnimationSegment({ beats: 1, startPosition: unfolded, endPosition: endSetPosition, }), ]; case SemanticAnimationKind.TwirlSwap: const twirlCenter = CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset); const aroundTopOrBottom = move.movementPattern.around === CircleSide.Top || move.movementPattern.around === CircleSide.Bottom; const inShortLines = move.startPosition.kind === PositionKind.ShortLines; return [ new animation.TransitionAnimationSegment({ actualAnimation: new animation.RotationAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: { ...endSetPosition, // Make sure rotation is towards the arm being rotated around. rotation: move.movementPattern.hand === Hand.Left ? endSetPosition.rotation < startSetPosition.rotation ? endSetPosition.rotation : endSetPosition.rotation - 360 : endSetPosition.rotation > startSetPosition.rotation ? endSetPosition.rotation : endSetPosition.rotation + 360 }, rotation: 180, around: { center: twirlCenter, width: aroundTopOrBottom ? setWidth : setWidth / 4, height: aroundTopOrBottom || inShortLines ? setHeight / 4 : setHeight, }, facing: animation.RotationAnimationFacing.Linear, hands: new Map([ [move.movementPattern.hand, { kind: "Center" }], [move.movementPattern.hand.opposite(), { kind: "None" }], ]) }), flags: { hands: true }, startTransitionBeats: 0, endTransitionBeats: 1, }) ]; case SemanticAnimationKind.PassBy: const width = dancerWidth/2; const distanceAtMidpoint = move.movementPattern.side == Hand.Left ? +width : -width; // "Pull By" is just "Pass By" with hands. const passByHands = move.movementPattern.withHands ? new Map([ [move.movementPattern.side, { kind: "CenterUntilPassed" }], [move.movementPattern.side.opposite(), { kind: "None" }], ]) : new Map([ [Hand.Left, { kind: "None" }], [Hand.Right, { kind: "None" }], ]); let endRotation = endSetPosition.rotation; const rotationAmount = endSetPosition.rotation - startSetPosition.rotation; if (rotationAmount !== 0) { if (rotationAmount >= 180 && move.movementPattern.side === Hand.Left) { endRotation -= 360; } else if (rotationAmount <= -180 && move.movementPattern.side === Hand.Right) { endRotation += 360; } } return [ new animation.TransitionAnimationSegment({ actualAnimation: new animation.StepWideLinearAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: { ...endSetPosition, rotation: endRotation }, distanceAtMidpoint, otherPath: move.movementPattern.otherPath === "Swap" ? undefined : { start: SemanticToSetPosition(move.movementPattern.otherPath.start).position, end: SemanticToSetPosition(move.movementPattern.otherPath.end).position, }, hands: passByHands, facing: move.movementPattern.facing, }), flags: { hands: true, rotation: true, rotationDirection: move.movementPattern.side, }, startTransitionBeats: 0.5, }) ]; case SemanticAnimationKind.CourtesyTurn: if (move.startPosition.kind !== PositionKind.Circle) { throw new Error("CourtesyTurn only supported on side of set."); } const courtesyTurnCenter = CenterOf(move.startPosition.which.leftRightSide(), move.startPosition.setOffset, move.startPosition.lineOffset); const cwCourtesyTurn: boolean = !!move.movementPattern.clockwise; const beingTurned = cwCourtesyTurn !== move.startPosition.which.isOnLeftLookingAcross(); return [ new animation.TransitionAnimationSegment({ actualAnimation: new animation.RotationAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: endSetPosition, around: { center: courtesyTurnCenter, width: 1, height: 1, }, rotation: cwCourtesyTurn ? +180 : -180, facing: beingTurned ? animation.RotationAnimationFacing.Forward : animation.RotationAnimationFacing.Backward, closer: { minDistance: dancerWidth, transitionBeats: 1, }, }), flags: { rotation: true, hands: true, handsDuring: new Map(beingTurned ? (cwCourtesyTurn ? [ [Hand.Left, { x: 0, y: -dancerHeightOffset * 2 }], [Hand.Right, { x: dancerWidth / 2, y: 0 }], ] : [ [Hand.Right, { x: 0, y: -dancerHeightOffset * 2 }], [Hand.Left, { x: -dancerWidth / 2, y: 0 }], ]) : (cwCourtesyTurn ? [ [Hand.Left, { x: -dancerWidth, y: -dancerHeightOffset * 2 }], [Hand.Right, { x: -dancerWidth / 2, y: 0 }], ] : [ [Hand.Right, { x: dancerWidth, y: -dancerHeightOffset * 2 }], [Hand.Left, { x: dancerWidth / 2, y: 0 }], ] )), }, startTransitionBeats: 1, }) ]; case SemanticAnimationKind.Promenade: return [ new animation.StepWideLinearAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: endSetPosition, distanceAtMidpoint: setHeight / 2, }) ] // TODO Unsupported moves, just doing linear for now. case SemanticAnimationKind.RollAway: return [ new animation.LinearAnimationSegment({ beats: move.beats, startPosition: startSetPosition, endPosition: endSetPosition, }) ] } throw "Unsupported move in animateLowLevelMove: " + JSON.stringify(move); } const anim: animation.AnimationSegment[] = handleMove(); // Normalize start/end positions if necessary. if (JSON.stringify(anim[0].startPosition) != JSON.stringify(startSetPosition)) { anim.unshift(animation.LinearAnimationSegment.standStill(startSetPosition)) } if (JSON.stringify(anim[anim.length - 1].endPosition) != JSON.stringify(endSetPosition)) { anim.push(animation.LinearAnimationSegment.standStill(endSetPosition)) } return anim; } export function animateFromLowLevelMoves(moves: Map): animation.Animation { const res = new Map(); for (const [id, moveList] of moves.entries()) { res.set(id, moveList.flatMap(move => animateLowLevelMove(move))) } return new animation.Animation(res); }