contra-renderer/www/js/lowLevelMove.ts

939 lines
35 KiB
TypeScript

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<Hand, Offset> {
return new Map<Hand, Offset>([
[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: " + (<any>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: " + (<any>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, Offset>([
[Hand.Left, posWithHands.leftArmEnd!],
[Hand.Right, posWithHands.rightArmEnd!],
]) : new Map<Hand, Offset>([
[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, animation.HandAnimation>([
[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<Hand, animation.HandAnimation>([
[move.movementPattern.byHand, { kind: "Center" }],
[move.movementPattern.byHand.opposite(), { kind: "None" }],
])
: new Map<Hand, animation.HandAnimation>([
[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, HandConnection>([[Hand.Right, {hand: Hand.Left, to: HandTo.DancerRight}]])
: new Map<Hand, HandConnection>([[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<Hand, animation.HandAnimation>([
[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<Hand, animation.HandAnimation>([
[move.movementPattern.side, { kind: "CenterUntilPassed" }],
[move.movementPattern.side.opposite(), { kind: "None" }],
])
: new Map<Hand, animation.HandAnimation>([
[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<Hand, Offset>(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<DancerIdentity, LowLevelMove[]>): animation.Animation {
const res = new Map<DancerIdentity, animation.AnimationSegment[]>();
for (const [id, moveList] of moves.entries()) {
res.set(id, moveList.flatMap(move => animateLowLevelMove(move)))
}
return new animation.Animation(res);
}