forked from perelman/contra-renderer
339 lines
15 KiB
TypeScript
339 lines
15 KiB
TypeScript
import * as animation from "./animation.js";
|
|
import { CoupleRole, DanceRole, DancerIdentity } from "./danceCommon.js";
|
|
import * as common from "./danceCommon.js";
|
|
import { Hand, setDistance } from "./rendererConstants.js";
|
|
import { nameLibFigureParameters, Move, chooser_pairz } from "./libfigureMapper.js";
|
|
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js";
|
|
import { BalanceWeight, CirclePosition, CircleSide, CircleSideOrCenter, DancerDistance, Facing, HandConnection, HandTo, LongLines, PositionKind, SemanticPosition, ShortLinesPosition, StarGrip, handToDancerToSideInCircleFacingAcross, handsFourImproper, handsInCircle, handsInLine, handsInShortLine, oppositeFacing } from "./interpreterCommon.js";
|
|
import { ContraDBDance } from "./danceLibrary.js";
|
|
import { AllVariantsForMoveArgs, ErrorMoveInterpreter, LowLevelMovesForAllDancers, MoveAsLowLevelMovesArgs, SemanticPositionsForAllDancers, Variant, VariantCollection, errorMoveInterpreterCtor, moveInterpreters } from "./moves/_moveInterpreter.js";
|
|
|
|
// Import all individual move files.
|
|
// ls [a-z]*.js | sed 's@.*@import "./moves/\0";@'
|
|
import "./moves/allemande.js";
|
|
import "./moves/balance.js";
|
|
import "./moves/balanceTheRing.js";
|
|
import "./moves/boxCirculate.js";
|
|
import "./moves/boxTheGnat.js";
|
|
import "./moves/butterflyWhirl.js";
|
|
import "./moves/californiaTwirl.js";
|
|
import "./moves/chain.js";
|
|
import "./moves/circle.js";
|
|
import "./moves/custom.js";
|
|
import "./moves/doSiDo.js";
|
|
import "./moves/downTheHall.js";
|
|
import "./moves/formAnOceanWave.js";
|
|
import "./moves/formLongWaves.js";
|
|
import "./moves/giveTake.js";
|
|
import "./moves/hey.js";
|
|
import "./moves/longLines.js";
|
|
import "./moves/madRobin.js";
|
|
import "./moves/passBy.js";
|
|
import "./moves/passThrough.js";
|
|
import "./moves/petronella.js";
|
|
import "./moves/promenade.js";
|
|
import "./moves/pullByDancers.js";
|
|
import "./moves/revolvingDoor.js";
|
|
import "./moves/rightLeftThrough.js";
|
|
import "./moves/rollAway.js";
|
|
import "./moves/roryOMore.js";
|
|
import "./moves/slice.js";
|
|
import "./moves/slideAlongSet.js";
|
|
import "./moves/star.js";
|
|
import "./moves/starPromenade.js";
|
|
import "./moves/swing.js";
|
|
import "./moves/turnAlone.js";
|
|
import "./moves/upTheHall.js";
|
|
|
|
|
|
// TODO Get rid of nextMove here once variants are actually supported by Swing.
|
|
function allVariantsForMove({ move, nextMove, startingVariants, numProgessions }: { move: Move; nextMove: Move; startingVariants: AllVariantsForMoveArgs; numProgessions: number; }): VariantCollection {
|
|
const moveInterpreter = moveInterpreters.get(move.move) ?? errorMoveInterpreterCtor;
|
|
return new moveInterpreter({ move, nextMove, numProgessions }).allVariantsForMove(startingVariants);
|
|
}
|
|
|
|
|
|
function danceAsLowLevelMoves(moves: Move[], startingVariants: AllVariantsForMoveArgs): Map<DancerIdentity, LowLevelMove[]> {
|
|
let currentVariants = new Map<string, MoveAsLowLevelMovesArgs>(startingVariants);
|
|
let currentVariantMoves: Map<string, LowLevelMovesForAllDancers> = new Map<string, LowLevelMovesForAllDancers>(
|
|
[...startingVariants.keys()].map(name => [name, new Map<DancerIdentity, LowLevelMove[]>(DancerIdentity.all().map(id => [id, []]))]));
|
|
let numProgessions = 0;
|
|
for (let i = 0; i < moves.length; i++) {
|
|
const move = moves[i];
|
|
const nextMove = i === moves.length - 1 ? moves[0] : moves[i + 1];
|
|
|
|
function updateVariants(newVariants: VariantCollection) {
|
|
let previousVariants = currentVariants;
|
|
let previousVariantMoves = currentVariantMoves;
|
|
try {
|
|
currentVariants = new Map<string, MoveAsLowLevelMovesArgs>();
|
|
currentVariantMoves = new Map<string, LowLevelMovesForAllDancers>();
|
|
|
|
variant: for (const [name, variant] of newVariants) {
|
|
const oldMoves = previousVariantMoves.get(variant.previousMoveVariant);
|
|
const oldPos = previousVariants.get(variant.previousMoveVariant);
|
|
if (!oldMoves || !oldPos) {
|
|
throw new Error("Referenced unknown previous variant: " + variant.previousMoveVariant);
|
|
}
|
|
|
|
const newMoves: LowLevelMovesForAllDancers = new Map<DancerIdentity, LowLevelMove[]>();
|
|
const newPositions = new Map<DancerIdentity, SemanticPosition>();
|
|
for (const [id, newMoveList] of variant.lowLevelMoves.entries()) {
|
|
const prevEnd = oldPos.startingPos.get(id)!;
|
|
const newStart = newMoveList[0].startPosition;
|
|
|
|
if ((prevEnd.setOffset ?? 0) != (newStart.setOffset ?? 0)
|
|
|| (prevEnd.lineOffset ?? 0) != (newStart.lineOffset ?? 0)
|
|
|| prevEnd.kind != newStart.kind
|
|
|| prevEnd.which != newStart.which
|
|
|| prevEnd.balance != newStart.balance
|
|
|| prevEnd.dancerDistance != newStart.dancerDistance) {
|
|
// TODO Should this be considered a bug?
|
|
// TODO Require more properties to match? facing? probably not hands.
|
|
//throw new Error("Moves start/end did not line up.");
|
|
continue variant;
|
|
}
|
|
|
|
newMoves.set(id, [...oldMoves.get(id)!, ...newMoveList]);
|
|
newPositions.set(id, newMoveList.at(-1)!.endPosition);
|
|
}
|
|
|
|
currentVariants.set(name, { startingPos: newPositions });
|
|
currentVariantMoves.set(name, newMoves);
|
|
}
|
|
} catch (ex) {
|
|
currentVariants = previousVariants;
|
|
currentVariantMoves = previousVariantMoves;
|
|
throw ex;
|
|
}
|
|
|
|
if (currentVariantMoves.size === 0) {
|
|
currentVariants = previousVariants;
|
|
currentVariantMoves = previousVariantMoves;
|
|
throw new Error("All variants were eliminated as possibilities.");
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (i > 0 && move.beats === 0 && move.move === "slide along set") {
|
|
const slideLeft: boolean = move.parameters.slide;
|
|
for (const [name, currentPos] of currentVariants.entries()) {
|
|
for (const [id, currPos] of currentPos.startingPos.entries()) {
|
|
const slideAmount = (currPos.which.leftRightSide() === CircleSide.Left) === slideLeft ? +0.5 : -0.5;
|
|
const setOffset = (currPos.setOffset ?? 0) + slideAmount;
|
|
currentPos.startingPos.set(id, { ...currPos, setOffset });
|
|
|
|
const prevMove = currentVariantMoves.get(name)!.get(id)!.at(-1)!;
|
|
prevMove.movementPattern.setSlideAmount = slideAmount;
|
|
prevMove.endPosition.setOffset = setOffset;
|
|
}
|
|
}
|
|
} else {
|
|
const newMoves = allVariantsForMove({ move, nextMove, startingVariants: currentVariants, numProgessions });
|
|
if (newMoves.size === 0) {
|
|
updateVariants(new ErrorMoveInterpreter({ move, nextMove, numProgessions },
|
|
"ERROR: " + move.move + " has no valid starting variants.").allVariantsForMove(currentVariants));
|
|
} else {
|
|
updateVariants(newMoves);
|
|
}
|
|
|
|
if (currentVariantMoves.size === 0) {
|
|
throw new Error("Variants selection somehow went down to zero?");
|
|
}
|
|
}
|
|
}
|
|
catch (ex) {
|
|
if (currentVariantMoves.size === 0) throw ex;
|
|
|
|
// catch exception so something can be displayed
|
|
const errorMessage: string = ex instanceof Error ? ex.message : ex;
|
|
const errorVariants = new ErrorMoveInterpreter({ move, nextMove, numProgessions }, errorMessage).allVariantsForMove(currentVariants);
|
|
updateVariants(errorVariants);
|
|
}
|
|
if (move.progression) numProgessions++;
|
|
}
|
|
|
|
// If there's multiple variants, check if there's fewer that flow into the first move.
|
|
if (currentVariantMoves.size !== 1) {
|
|
let newMoves: VariantCollection | undefined;
|
|
try {
|
|
newMoves = allVariantsForMove({ move: moves[0], nextMove: moves[1], startingVariants: currentVariants, numProgessions });
|
|
} catch {
|
|
newMoves = undefined;
|
|
}
|
|
if (newMoves) {
|
|
const firstMoveVariants = [...newMoves.values()].map(v => v.previousMoveVariant);
|
|
if (firstMoveVariants.length > 0) {
|
|
for (const variant of [...currentVariantMoves.keys()]) {
|
|
if (!firstMoveVariants.includes(variant)) {
|
|
currentVariantMoves.delete(variant);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const res = [...currentVariantMoves.values()].at(-1)!;
|
|
if (currentVariantMoves.size !== 1) {
|
|
res.get(DancerIdentity.OnesRobin)![0].interpreterError = "Expected exactly one variant. Found "
|
|
+ currentVariantMoves.size + ": " + [...currentVariantMoves.keys()].join(", ");
|
|
}
|
|
|
|
try {
|
|
const progression = animateFromLowLevelMoves(res).progression;
|
|
const progressionInSets = progression.y / setDistance;
|
|
|
|
// fixup end positions to match start of next move
|
|
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";
|
|
// TODO Exactly what is the StandStill fixup needed for? Can it be handled in other ways? Should it be rotation only?
|
|
/*
|
|
if (lowLevelMoves[i].movementPattern.kind === SemanticAnimationKind.StandStill) {
|
|
lowLevelMoves[i].startPosition = lowLevelMoves[i].endPosition;
|
|
if (i > 0) {
|
|
lowLevelMoves[i - 1].endPosition = lowLevelMoves[i].startPosition;
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
// If progression isn't detected properly, do nothing.
|
|
if (progressionInSets === 0) {
|
|
lowLevelMoves[lowLevelMoves.length - 1].interpreterError = "No progression detected. Not lining up end with start of dance.";
|
|
} else {
|
|
const startPos = lowLevelMoves[0].startPosition;
|
|
lowLevelMoves[lowLevelMoves.length - 1].endPosition = {
|
|
...lowLevelMoves[0].startPosition,
|
|
// progressed
|
|
setOffset: (startPos.setOffset ?? 0) + (id.coupleRole == CoupleRole.Ones ? 1 : -1) * progressionInSets,
|
|
};
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
catch (ex) {
|
|
res.get(DancerIdentity.OnesLark)![0].interpreterError = "Error detecting progression: " + (ex instanceof Error ? ex.message : ex);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
function improperShortWaves(slideTo: Hand) {
|
|
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
|
([id, pos]) => ([id, {
|
|
kind: PositionKind.ShortLines,
|
|
which: pos.which.toShortLines(slideTo),
|
|
facing: pos.which.facingUpOrDown(),
|
|
}])
|
|
));
|
|
}
|
|
|
|
function improperUnfoldToShortLines(center: CircleSide.Bottom | CircleSide.Top) {
|
|
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
|
([id, pos]) => ([id, {
|
|
kind: PositionKind.ShortLines,
|
|
which: pos.which.unfoldToShortLines(center),
|
|
facing: center === CircleSide.Top ? Facing.Down : Facing.Up,
|
|
}])
|
|
));
|
|
}
|
|
|
|
function improperLongWaves(facingOut: DanceRole) {
|
|
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
|
([id, pos]) => ([id, {
|
|
...pos,
|
|
facing: id.danceRole === facingOut ? pos.which.facingOut() : pos.which.facingAcross(),
|
|
hands: handsInLine({ wavy: true, which: pos.which, facing: undefined })
|
|
}])
|
|
));
|
|
}
|
|
|
|
function StartingPosForFormation(formation: common.StartFormation, dance?: ContraDBDance): Map<DancerIdentity, SemanticPosition> {
|
|
const preamble = dance?.preamble ?? "";
|
|
if (preamble.includes("Starts in short waves, right hands to neighbors")) {
|
|
return improperShortWaves(Hand.Left);
|
|
} else if (preamble.includes("face down the hall in lines of 4, 1s splitting the 2s")) {
|
|
return improperUnfoldToShortLines(CircleSide.Top);
|
|
} else if (preamble.includes("Long waves, larks face out")) {
|
|
return improperLongWaves(DanceRole.Lark);
|
|
}
|
|
|
|
switch (formation) {
|
|
case "improper":
|
|
return handsFourImproper;
|
|
case "Becket":
|
|
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
|
([id, pos]) => ([id, {...pos, which: pos.which.circleLeft(1)}])
|
|
));
|
|
case "Becket ccw":
|
|
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
|
([id, pos]) => ([id, {...pos, which: pos.which.circleRight(1)}])
|
|
));
|
|
case "Sawtooth Becket":
|
|
// Dancers start becket, then slide one person to the right. https://contradb.com/dances/848
|
|
// TODO Not sure this is right.
|
|
return new Map<common.DancerIdentity, SemanticPosition>([
|
|
[DancerIdentity.OnesLark, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.BottomLeft,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
}],
|
|
[DancerIdentity.OnesRobin, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.TopLeft,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
}],
|
|
[DancerIdentity.TwosLark, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.BottomRight,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
}],
|
|
[DancerIdentity.TwosRobin, {
|
|
kind: PositionKind.Circle,
|
|
which: CirclePosition.TopRight,
|
|
facing: Facing.CenterOfCircle,
|
|
hands: handsInCircle,
|
|
setOffset: 1,
|
|
}],
|
|
]);
|
|
}
|
|
}
|
|
|
|
function startingVariantsForFormation(formation: common.StartFormation, dance?: ContraDBDance) : AllVariantsForMoveArgs {
|
|
const defaultFormation = StartingPosForFormation(formation, dance);
|
|
if (formation !== "improper" || defaultFormation !== handsFourImproper) {
|
|
return new Map<string, MoveAsLowLevelMovesArgs>([
|
|
["Initial", { startingPos: defaultFormation }]
|
|
]);
|
|
} else {
|
|
// Handle various different starting position that all count as "improper" like short/long waves, etc.
|
|
return new Map<string, MoveAsLowLevelMovesArgs>([
|
|
["InitialCircle", { startingPos: handsFourImproper }],
|
|
["InitialShortWavesLeft", { startingPos: improperShortWaves(Hand.Left) }],
|
|
["InitialShortWavesRight", { startingPos: improperShortWaves(Hand.Right) }],
|
|
["InitialShortLinesDown", { startingPos: improperUnfoldToShortLines(CircleSide.Top) }],
|
|
["InitialShortLinesUp", { startingPos: improperUnfoldToShortLines(CircleSide.Bottom) }],
|
|
["InitialLongWavesLarksOut", { startingPos: improperLongWaves(DanceRole.Lark) }],
|
|
["InitialLongWavesRobinsOut", { startingPos: improperLongWaves(DanceRole.Robin) }],
|
|
]);
|
|
|
|
}
|
|
}
|
|
|
|
export let mappedDance: Move[];
|
|
|
|
export let interpretedDance: Map<DancerIdentity, LowLevelMove[]>;
|
|
export let interpretedAnimation: animation.Animation;
|
|
|
|
export function loadDance(dance: ContraDBDance): animation.Animation {
|
|
mappedDance = dance.figures.map(nameLibFigureParameters);
|
|
interpretedDance = danceAsLowLevelMoves(mappedDance, startingVariantsForFormation(dance.start_type, dance));
|
|
interpretedAnimation = animateFromLowLevelMoves(interpretedDance);
|
|
|
|
return interpretedAnimation;
|
|
}
|