contra-renderer/www/js/interpreter.ts

585 lines
21 KiB
TypeScript

import * as animation from "./animation.js";
import { CoupleRole, DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
import * as common from "./danceCommon.js";
import { Hand, setDistance, setHeight } from "./rendererConstants.js";
import { nameLibFigureParameters, Move, LibFigureDance, chooser_pairz } from "./libfigureMapper.js";
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js";
import { BalanceWeight, CirclePosition, CircleSide, DancerDistance, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "./interpreterCommon.js";
const handsInCircle = new Map<Hand, HandConnection>([
[Hand.Left, {
to: HandTo.LeftInCircle,
hand: Hand.Right,
}],
[Hand.Right, {
to: HandTo.RightInCircle,
hand: Hand.Left,
}],
]);
const handsFourImproper: Map<common.DancerIdentity, SemanticPosition> = new Map<common.DancerIdentity, SemanticPosition>([
[DancerIdentity.OnesLark, {
kind: PositionKind.Circle,
which: CirclePosition.TopLeft,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
[DancerIdentity.OnesRobin, {
kind: PositionKind.Circle,
which: CirclePosition.TopRight,
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.BottomLeft,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
]);
// Two Hearts in Time by Isaac Banner. Selected arbitrarily.
const exampleDance: LibFigureDance = [{ "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": ["neighbors", "balance", 16], "move": "swing" }, { "parameter_values": ["ladles", true, 540, 8], "move": "allemande" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }, { "parameter_values": ["gentlespoons", 360, 6], "move": "mad robin" }, { "parameter_values": [true, 270, 6], "move": "circle" }, { "parameter_values": ["partners", 4], "move": "California twirl", "progression": 1 }];
function balanceCircleInAndOut(move: Move, startPos: SemanticPosition, balanceBeats?: number): [LowLevelMove, LowLevelMove] {
if (startPos.kind !== PositionKind.Circle) {
throw "Balance circle must start in a circle, but starting at " + startPos;
}
balanceBeats ??= 4;
const balancePartBeats = balanceBeats/2;
const holdingHandsInCircle: SemanticPosition = {...startPos,
facing: Facing.CenterOfCircle,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Right, to: HandTo.LeftInCircle }],
[Hand.Right, { hand: Hand.Left, to: HandTo.RightInCircle }],
]),
};
const circleBalancedIn: SemanticPosition = {...holdingHandsInCircle,
balance: BalanceWeight.Forward,
};
const balanceIn: LowLevelMove = {
move,
startBeat: 0,
beats: balancePartBeats,
startPosition: holdingHandsInCircle,
endPosition: circleBalancedIn,
movementPattern: { kind: SemanticAnimationKind.Linear },
};
const balanceOut: LowLevelMove = {...balanceIn,
startBeat: balancePartBeats,
startPosition: circleBalancedIn,
endPosition: holdingHandsInCircle,
};
return [balanceIn, balanceOut];
}
function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, SemanticPosition>): Map<DancerIdentity, LowLevelMove[]> {
type PartialLowLevelMove = {
remarks?: string,
beats: number,
startPosition?: SemanticPosition,
endPosition: SemanticPosition,
movementPattern: SemanticAnimation
};
function append(moves: LowLevelMove[],
newMove: LowLevelMove | PartialLowLevelMove
| ((prevEnd: SemanticPosition) => PartialLowLevelMove)): LowLevelMove[] {
const lastMove = moves.at(-1)!;
const prevEnd = lastMove.endPosition;
if (typeof newMove === 'function') {
newMove = newMove(prevEnd);
}
if (!newMove.startPosition) {
newMove.startPosition = prevEnd;
}
moves.push({
...newMove,
startPosition: newMove.startPosition ?? prevEnd,
move: lastMove.move,
startBeat: lastMove.startBeat + lastMove.beats,
});
return moves;
}
function combine(moves: ((LowLevelMove | PartialLowLevelMove
| ((prevEnd: SemanticPosition) => PartialLowLevelMove))[]),
startPos?: SemanticPosition): LowLevelMove[] {
const res: LowLevelMove[] = [];
if (moves.length === 0) return res;
let firstMove = moves[0];
if ('move' in firstMove) {
res.push(firstMove);
} else {
if (typeof firstMove === 'function') {
firstMove = firstMove(startPos!);
}
res.push({...firstMove,
move: move,
startBeat: 0,
startPosition: firstMove.startPosition ?? startPos!,
});
}
for (const move of moves.slice(1)) {
append(res, move);
}
return res;
}
function findPairOpposite(who: chooser_pairz, id: DancerIdentity): common.ExtendedDancerIdentity | null {
switch (who) {
case "partners":
return id.partner().asExtendedDancerIdentity();
case "neighbors":
return id.neighbor().asExtendedDancerIdentity();
// These three might get used when not with neighbors?
case "gentlespoons":
if (id.danceRole === DanceRole.Robin) return null;
return id.oppositeSameRole().asExtendedDancerIdentity();
case "ladles":
if (id.danceRole === DanceRole.Lark) return null;
return id.oppositeSameRole().asExtendedDancerIdentity();
case "same roles":
return id.oppositeSameRole().asExtendedDancerIdentity();
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 "Unsupported who: " + who;
}
}
// TOOD Type for this? Probably SemanticPosition?
function findCenterBetween(id: DancerIdentity, other: common.ExtendedDancerIdentity) {
const selfPos = startingPos.get(id)!;
selfPos.setOffset ??= 0;
selfPos.lineOffset ??= 0;
const otherBasePos = startingPos.get(other.setIdentity)!;
const otherPos : SemanticPosition = {...otherBasePos,
setOffset: (otherBasePos.setOffset ?? 0) + other.relativeSet,
lineOffset: (otherBasePos.lineOffset ?? 0) + other.relativeLine,
};
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.";
}
}
const res = new Map<DancerIdentity, LowLevelMove[]>();
switch (move.move) {
case "petronella":
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
// TODO This doesn't handle petronella to a new circle. How is that notated?
const finalPosition = {
...startPos,
facing: Facing.CenterOfCircle,
which: startPos.which.circleRight(1),
hands: undefined,
};
if (move.parameters.bal) {
const balance: LowLevelMove[] = balanceCircleInAndOut(move, startPos);
res.set(id, append([...balance], prevEnd => ({
beats: move.beats - 4,
startPosition: {
...prevEnd,
hands: undefined,
},
endPosition: finalPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: 180,
handsDuring: "None",
},
})));
} else {
res.set(id, combine([{
beats: move.beats,
startPosition: {
...startPos,
facing: Facing.CenterOfCircle,
hands: undefined,
},
endPosition: finalPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: 180,
handsDuring: "None",
},
}]));
}
}
return res;
case "mad robin":
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
// TODO Read who of mad robin to decide direction?
const startAndEndPos = {
...startPos,
facing: startPos.which.isLeft() ? Facing.Right : Facing.Left,
hands: undefined,
};
res.set(id, combine([{
beats: move.beats,
startPosition: startAndEndPos,
endPosition: startAndEndPos,
movementPattern: {
kind: SemanticAnimationKind.DoSiDo,
amount: move.parameters.circling,
},
}]));
}
return res;
case "swing":
for (const [id, startPos] of startingPos.entries()) {
// TODO swing can start from other positions.
// TODO swing end is only in notes / looking at next move.
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
switch (move.parameters.who) {
case "ladles":
case "gentlespoons":
case "ones":
case "twos":
throw "Swing in middle is currently unsupported.";
case "first corners":
case "second corners":
throw "contra corners currently unsupported.";
case "same roles":
throw "same role swing currently unsupported."
case "neighbors":
case "partners":
case "shadows":
// TODO Make sure referenced dancer is using same center.
break;
}
const startPosition: SemanticPosition = {
...startPos,
facing: startPos.which.topBottomSide() === CircleSide.Bottom ? Facing.Up : Facing.Down,
};
const endPosition: SemanticPosition = {
...startPos,
which: startPos.which.isOnLeftLookingAcross() === (id.danceRole === DanceRole.Lark)
? startPos.which
: startPos.which.swapUpAndDown(),
facing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
balance: undefined,
dancerDistance: undefined,
hands: new Map<Hand, HandConnection>([id.danceRole === DanceRole.Lark
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
};
// TODO same role swing currently unsupported.
const swingRole = id.danceRole === DanceRole.Lark ? DancerDistance.SwingLark : DancerDistance.SwingRobin;
switch (move.parameters.prefix) {
case "none":
res.set(id, combine([
{
beats: move.beats,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
], startPos));
break;
case "balance":
// TODO Right length for balance?
const balancePartBeats = move.beats > 8 ? (move.beats - 8) / 2 : 2;
const startWithBalHands = {
...startPosition,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { to: HandTo.DancerForward, hand: Hand.Right }],
[Hand.Right, { to: HandTo.DancerForward, hand: Hand.Left }],
]),
};
res.set(id, combine([
{
beats: balancePartBeats,
startPosition: startWithBalHands,
endPosition: {
...startWithBalHands,
balance: BalanceWeight.Forward,
dancerDistance: DancerDistance.Compact,
},
movementPattern: { kind: SemanticAnimationKind.Linear, },
},
prevEnd => ({
beats: balancePartBeats,
endPosition: {
...prevEnd,
balance: BalanceWeight.Backward,
},
movementPattern: { kind: SemanticAnimationKind.Linear, },
}),
{
beats: move.beats - 2 * balancePartBeats,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
], startPos));
break;
case "meltdown":
const meltdownBeats = 4; // TODO right number here?
res.set(id, combine([
prevEnd => ({
beats: meltdownBeats,
endPosition: prevEnd,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: 360,
around: startPos.which.leftRightSide(),
byHand: undefined,
},
}),
{
beats: move.beats - meltdownBeats,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
], startPos));
break;
}
}
return res;
case "allemande":
for (const [id, startPos] of startingPos.entries()) {
// TODO allemande can start from other positions.
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
const withId = findPairOpposite(move.parameters.who, id);
// findPairOpposite of null means this dancer doesn't participate in this move.
if (!withId) {
res.set(id, combine([prevEnd => ({
beats: move.beats,
startPosition: { ...prevEnd, hands: undefined },
endPosition: { ...prevEnd, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
})], startPos));
continue;
};
const around = findCenterBetween(id, withId);
// TODO Not sure if this is right.
const byHand = move.parameters.hand ? Hand.Right : Hand.Left;
const swap = move.parameters.circling % 360 === 180;
if (!swap && move.parameters.circling % 360 !== 0) {
throw "Unsupported allemande circle amount: " + move.parameters.circling;
}
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;
}
res.set(id, combine([
{
beats: move.beats,
endPosition,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? move.parameters.circling : -move.parameters.circling,
around,
byHand,
},
},
], startPos));
}
return res;
case "circle":
const places = move.parameters.places/90 * (move.parameters.turn ? 1 : -1);
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
res.set(id, combine([
prevEnd => ({
beats: move.beats,
endPosition: {
...prevEnd,
hands: handsInCircle,
which: startPos.which.circleLeft(places),
},
movementPattern: {
kind: SemanticAnimationKind.Circle,
places,
}
}),
], { ...startPos, facing: Facing.CenterOfCircle }));
}
return res;
case "California twirl":
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
const onLeft : boolean = startPos.which.isOnLeftLookingUpAndDown();
res.set(id, combine([
{
beats: 1,
endPosition: {
...startPos,
hands: new Map<Hand, HandConnection>([onLeft
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
facing: startPos.which.topBottomSide() === CircleSide.Top ? Facing.Down : Facing.Up,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: move.beats - 1,
endPosition: {
...startPos,
which: startPos.which.swapAcross(),
facing: startPos.which.topBottomSide() === CircleSide.Top ? Facing.Up : Facing.Down,
},
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around: startPos.which.topBottomSide(),
hand: onLeft ? Hand.Right : Hand.Left,
}
}], startPos));
}
return res;
}
// XXX DEBUG Just leave out unsupported moves for now to allow viewing the known moves.
//throw "Unknown move: " + move.move + ": " + JSON.stringify(move);
for (const [id, startPos] of startingPos.entries()) {
res.set(id, [{
remarks: "UNKNOWN MOVE: standing still",
move,
startBeat: 0,
beats: move.beats,
startPosition: startPos,
endPosition: startPos,
movementPattern: {
kind: SemanticAnimationKind.StandStill,
},
}]);
}
return res;
}
function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, SemanticPosition>): Map<DancerIdentity, LowLevelMove[]> {
const res = new Map<DancerIdentity, LowLevelMove[]>([...startingPos.keys()].map(id => [id, []]));
let currentPos = startingPos;
for (const move of moves) {
const newMoves = moveAsLowLevelMoves(move, currentPos);
for (const [id, newMoveList] of newMoves.entries()) {
res.get(id)!.push(...newMoveList);
currentPos.set(id, newMoveList.at(-1)!.endPosition);
}
}
const progression = animateFromLowLevelMoves(res).progression;
if (progression.x !== 0) throw "Progressing to different line is unsupported.";
if (progression.y % setHeight / 2 !== 0) throw "Progression is not an integer number of places.";
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";
}
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;
}
export let mappedDance: Move[];
export let interpretedDance: Map<DancerIdentity, LowLevelMove[]>;
export let interpretedAnimation: animation.Animation;
export function loadDance(dance: LibFigureDance): animation.Animation {
mappedDance = dance.map(nameLibFigureParameters);
interpretedDance = danceAsLowLevelMoves(mappedDance, handsFourImproper);
interpretedAnimation = animateFromLowLevelMoves(interpretedDance);
return interpretedAnimation;
}
loadDance(exampleDance);