937 lines
35 KiB
TypeScript
937 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,
|
|
} | {
|
|
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: 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 : 0);
|
|
if (hand === connection.hand) {
|
|
return { x: 0, y: +armLength/2 };
|
|
} 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: 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);
|
|
} |