Compare commits
13 Commits
main
...
feature/va
Author | SHA1 | Date | |
---|---|---|---|
a0b974a7e3 | |||
98d9a27235 | |||
fe481f37b5 | |||
83dd2953e2 | |||
f182962365 | |||
b98547cd3d | |||
e240b690f4 | |||
2631bad801 | |||
5de6db868b | |||
c4462b0f50 | |||
d2f07b203b | |||
192edd4356 | |||
c0abd76a0c |
31
README.md
31
README.md
|
@ -6,3 +6,34 @@ early in development and only works on a single dance, Isaac Banner's
|
|||
["Two Hearts in Time"](https://contradb.com/dances/2014), chosen as a
|
||||
simple dance (i.e. single progression improper, no shadow or
|
||||
next/previous neighbor interactions).
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
To develop locally, you need
|
||||
[TypeScript](https://www.typescriptlang.org/download/) installed. You
|
||||
can check if you have the **`tsc`** (TypeScript Compiler) command available.
|
||||
|
||||
While developing, leave the [watch-tsc.sh](./watch-tsc.sh) script running
|
||||
or, equivalently, run the following the command from the root of the
|
||||
repository:
|
||||
```sh
|
||||
tsc --watch
|
||||
```
|
||||
|
||||
Also, serve the `www/` directory from a local webserver.
|
||||
The [serve.sh](./serve.sh) script will do this or you can run the
|
||||
following command from the `www/` directory of the repository:
|
||||
```sh
|
||||
python -m http.server --bind localhost 8085
|
||||
```
|
||||
(Nothing special about [Python](https://www.python.org/downloads/) here,
|
||||
just the easiest web server to set up.)
|
||||
|
||||
Then open http://localhost:8085/ in a web browser. The site should work
|
||||
in any modern browser (tested in Firefox and Chromium).
|
||||
|
||||
Any text editor/IDE works, but I find
|
||||
[VS Code](https://code.visualstudio.com/Download)'s
|
||||
[TypeScript support](https://code.visualstudio.com/Docs/languages/typescript)
|
||||
works well.
|
||||
|
|
7
serve.sh
Executable file
7
serve.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Change directory to www/ directory.
|
||||
cd "$(dirname "$0")/www/" || exit
|
||||
echo "Serving $(pwd)"
|
||||
# From www/
|
||||
python -m http.server --bind localhost 8085
|
7
watch-tsc.sh
Executable file
7
watch-tsc.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Change directory to repo root.
|
||||
cd "$(dirname "$0")" || exit
|
||||
echo "Watching $(pwd)"
|
||||
# From /
|
||||
tsc --watch
|
|
@ -6,7 +6,7 @@ import { nameLibFigureParameters, Move, chooser_pairz } from "./libfigureMapper.
|
|||
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 { errorMoveInterpreterCtor, moveInterpreters } from "./moves/_moveInterpreter.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";@'
|
||||
|
@ -46,73 +46,158 @@ import "./moves/turnAlone.js";
|
|||
import "./moves/upTheHall.js";
|
||||
|
||||
|
||||
function moveAsLowLevelMoves({ move, nextMove, startingPos, numProgessions }: { move: Move; nextMove: Move; startingPos: Map<DancerIdentity, SemanticPosition>; numProgessions: number; }): Map<DancerIdentity, LowLevelMove[]> {
|
||||
// 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 }).moveAsLowLevelMoves({ startingPos });
|
||||
return new moveInterpreter({ move, nextMove, numProgessions }).allVariantsForMove(startingVariants);
|
||||
}
|
||||
|
||||
function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, SemanticPosition>): Map<DancerIdentity, LowLevelMove[]> {
|
||||
const res = new Map<DancerIdentity, LowLevelMove[]>([...startingPos.keys()].map(id => [id, []]));
|
||||
let currentPos = new Map<DancerIdentity, SemanticPosition>(startingPos);
|
||||
|
||||
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 [id, currPos] of currentPos.entries()) {
|
||||
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.set(id, { ...currPos, setOffset });
|
||||
currentPos.startingPos.set(id, { ...currPos, setOffset });
|
||||
|
||||
const prevMove = res.get(id)!.at(-1)!;
|
||||
const prevMove = currentVariantMoves.get(name)!.get(id)!.at(-1)!;
|
||||
prevMove.movementPattern.setSlideAmount = slideAmount;
|
||||
prevMove.endPosition.setOffset = setOffset;
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
const newMoves = moveAsLowLevelMoves({ move, nextMove, startingPos: currentPos, numProgessions });
|
||||
for (const [id, newMoveList] of newMoves.entries()) {
|
||||
res.get(id)!.push(...newMoveList);
|
||||
currentPos.set(id, newMoveList.at(-1)!.endPosition);
|
||||
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
|
||||
for (const [id, pos] of currentPos.entries()) {
|
||||
res.get(id)!.push({
|
||||
beats: move.beats,
|
||||
startPosition: pos,
|
||||
endPosition: pos,
|
||||
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
||||
move,
|
||||
startBeat: 0,
|
||||
interpreterError: ex instanceof Error ? ex.message : ex,
|
||||
});
|
||||
}
|
||||
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
|
||||
// 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";
|
||||
// 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) {
|
||||
|
@ -134,24 +219,44 @@ function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, Se
|
|||
}
|
||||
}
|
||||
|
||||
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 new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
||||
([id, pos]) => ([id, {
|
||||
kind: PositionKind.ShortLines,
|
||||
which: pos.which.toShortLines(Hand.Left),
|
||||
facing: pos.which.isTop() ? Facing.Down : Facing.Up,
|
||||
}])
|
||||
));
|
||||
return improperShortWaves(Hand.Left);
|
||||
} else if (preamble.includes("face down the hall in lines of 4, 1s splitting the 2s")) {
|
||||
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
|
||||
([id, pos]) => ([id, {
|
||||
kind: PositionKind.ShortLines,
|
||||
which: pos.which.unfoldToShortLines(CircleSide.Top),
|
||||
facing: Facing.Down,
|
||||
}])
|
||||
));
|
||||
return improperUnfoldToShortLines(CircleSide.Top);
|
||||
} else if (preamble.includes("Long waves, larks face out")) {
|
||||
return improperLongWaves(DanceRole.Lark);
|
||||
}
|
||||
|
||||
switch (formation) {
|
||||
|
@ -198,6 +303,27 @@ function StartingPosForFormation(formation: common.StartFormation, dance?: Contr
|
|||
}
|
||||
}
|
||||
|
||||
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[]>;
|
||||
|
@ -205,7 +331,7 @@ export let interpretedAnimation: animation.Animation;
|
|||
|
||||
export function loadDance(dance: ContraDBDance): animation.Animation {
|
||||
mappedDance = dance.figures.map(nameLibFigureParameters);
|
||||
interpretedDance = danceAsLowLevelMoves(mappedDance, StartingPosForFormation(dance.start_type, dance));
|
||||
interpretedDance = danceAsLowLevelMoves(mappedDance, startingVariantsForFormation(dance.start_type, dance));
|
||||
interpretedAnimation = animateFromLowLevelMoves(interpretedDance);
|
||||
|
||||
return interpretedAnimation;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DancerIdentity, Rotation } from "./danceCommon.js";
|
||||
import { DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
|
||||
import { Hand } from "./rendererConstants.js";
|
||||
|
||||
export enum PositionKind {
|
||||
|
@ -21,6 +21,14 @@ export enum CircleSide {
|
|||
Right = "Right",
|
||||
}
|
||||
export type CircleSideOrCenter = CircleSide | "Center";
|
||||
|
||||
export function isLeftRightCircleSide(side: CircleSideOrCenter): side is CircleSide.Left | CircleSide.Right {
|
||||
return side === CircleSide.Left || side === CircleSide.Right;
|
||||
}
|
||||
export function isTopBottomCircleSide(side: CircleSideOrCenter): side is CircleSide.Top | CircleSide.Bottom {
|
||||
return side === CircleSide.Top || side === CircleSide.Bottom;
|
||||
}
|
||||
|
||||
export class CirclePosition {
|
||||
public static readonly TopLeft = new CirclePosition(CirclePositionEnum.TopLeft);
|
||||
public static readonly BottomLeft = new CirclePosition(CirclePositionEnum.BottomLeft);
|
||||
|
@ -43,6 +51,29 @@ export class CirclePosition {
|
|||
: CirclePosition.BottomRight;
|
||||
}
|
||||
|
||||
public static fromSwing(side: CircleSide, swingRole: DanceRole, facing: "In" | "Out" | "Across" | Facing.Up | Facing.Down) {
|
||||
if (isLeftRightCircleSide(side)) {
|
||||
if (facing === Facing.Up || facing === Facing.Down) {
|
||||
throw new Error("Ending a swing in a circle on the left/right side facing up/down doesn't make sense.");
|
||||
} else if (facing === "Across") {
|
||||
facing = "In";
|
||||
}
|
||||
|
||||
return this.fromSides(side,
|
||||
(side === CircleSide.Left) !== (swingRole === DanceRole.Lark) !== (facing === "In") ? CircleSide.Bottom : CircleSide.Top);
|
||||
} else {
|
||||
if (facing === Facing.Up || facing === Facing.Down) {
|
||||
facing = (facing === Facing.Up) === (side === CircleSide.Top) ? "Out" : "In";
|
||||
} else if (facing === "Across") {
|
||||
throw new Error("Ending a swing in a circle on the top/bottom side facing across doesn't make sense.");
|
||||
}
|
||||
|
||||
return this.fromSides(
|
||||
(side === CircleSide.Top) !== (swingRole === DanceRole.Lark) !== (facing === "In") ? CircleSide.Left : CircleSide.Right,
|
||||
side);
|
||||
}
|
||||
}
|
||||
|
||||
private static enumValueToNumber(enumValue: CirclePositionEnum) : number {
|
||||
switch (enumValue) {
|
||||
case CirclePositionEnum.TopLeft:
|
||||
|
@ -105,6 +136,14 @@ export class CirclePosition {
|
|||
][CirclePosition.enumValueToNumber(this.enumValue)];
|
||||
}
|
||||
|
||||
public swapOnSide(side: CircleSide) : CirclePosition {
|
||||
if (side === CircleSide.Bottom || side === CircleSide.Top) {
|
||||
return this.swapAcross();
|
||||
} else {
|
||||
return this.swapUpAndDown();
|
||||
}
|
||||
}
|
||||
|
||||
public toShortLines(slideTo: Hand) : ShortLinesPosition {
|
||||
return slideTo === Hand.Left
|
||||
? [
|
||||
|
@ -173,6 +212,28 @@ export class CirclePosition {
|
|||
return this.enumValue === CirclePositionEnum.TopLeft || this.enumValue === CirclePositionEnum.BottomRight;
|
||||
}
|
||||
|
||||
public isOnLeft(around: CircleSide, facing: Facing.Down | Facing.Up | Facing.Left | Facing.Right | Facing.CenterOfCircle | "Out") {
|
||||
if (facing === Facing.CenterOfCircle) {
|
||||
facing = facingInAround(around);
|
||||
} else if (facing === "Out") {
|
||||
facing = oppositeFacing(facingInAround(around));
|
||||
}
|
||||
|
||||
if (around === CircleSide.Bottom || around === CircleSide.Top) {
|
||||
if (facing === Facing.Left || facing === Facing.Right) {
|
||||
throw new Error("Cannot face " + facing + " and have a left/right side around " + around);
|
||||
}
|
||||
|
||||
return (around === CircleSide.Bottom) !== (facing === Facing.Down) !== (this.isLeft());
|
||||
} else {
|
||||
if (facing === Facing.Up || facing === Facing.Down) {
|
||||
throw new Error("Cannot face " + facing + " and have a left/right side around " + around);
|
||||
}
|
||||
|
||||
return (around === CircleSide.Left) !== (facing === Facing.Right) !== (this.isTop());
|
||||
}
|
||||
}
|
||||
|
||||
public facingAcross() : Facing.Left | Facing.Right {
|
||||
return this.isLeft() ? Facing.Right : Facing.Left;
|
||||
}
|
||||
|
@ -208,6 +269,25 @@ export class ShortLinesPosition {
|
|||
this.enumValue = enumValue;
|
||||
}
|
||||
|
||||
public static fromSide(side: CircleSide.Left | CircleSide.Right, which: "Far" | "Middle") {
|
||||
if (side === CircleSide.Left) {
|
||||
return which === "Far" ? ShortLinesPosition.FarLeft : ShortLinesPosition.MiddleLeft;
|
||||
} else {
|
||||
return which === "Far" ? ShortLinesPosition.FarRight : ShortLinesPosition.MiddleRight;
|
||||
}
|
||||
}
|
||||
|
||||
public static fromSwing(side: "Center" | CircleSide.Left | CircleSide.Right, swingRole: DanceRole, facing: Facing.Up | Facing.Down) {
|
||||
if (side === "Center") {
|
||||
return this.fromSide(
|
||||
(swingRole === DanceRole.Lark) !== (facing === Facing.Down) ? CircleSide.Right : CircleSide.Left,
|
||||
"Middle");
|
||||
} else {
|
||||
return this.fromSide(side,
|
||||
(side === CircleSide.Left) !== (swingRole === DanceRole.Lark) !== (facing === Facing.Down) ? "Far" : "Middle");
|
||||
}
|
||||
}
|
||||
|
||||
private static enumValueToNumber(enumValue: ShortLinesPositionEnum) : number {
|
||||
switch (enumValue) {
|
||||
case ShortLinesPositionEnum.FarLeft:
|
||||
|
@ -263,13 +343,25 @@ export class ShortLinesPosition {
|
|||
}
|
||||
|
||||
public shift(dir: Hand, facing: Facing.Up | Facing.Down): ShortLinesPosition {
|
||||
const shift = (dir === Hand.Left) === (facing === Facing.Down) ? -1 : +1;
|
||||
const newNum = ShortLinesPosition.enumValueToNumber(this.enumValue) + shift;
|
||||
if (newNum < 0 || newNum > 3) {
|
||||
const { newPos, wrap } = this.shiftWithWrap(dir, facing);
|
||||
if (wrap) {
|
||||
throw new Error("Invalid shift: " + this + " facing " + facing + " to " + dir + ".");
|
||||
}
|
||||
|
||||
return ShortLinesPosition.get(ShortLinesPosition.numberToEnumValue(newNum));
|
||||
return newPos;
|
||||
}
|
||||
|
||||
public shiftWithWrap(dir: Hand, facing: Facing.Up | Facing.Down): { newPos: ShortLinesPosition, wrap?: -1 | 1 } {
|
||||
const shift = (dir === Hand.Left) === (facing === Facing.Down) ? -1 : +1;
|
||||
const newNum = ShortLinesPosition.enumValueToNumber(this.enumValue) + shift;
|
||||
const newPos = ShortLinesPosition.get(ShortLinesPosition.numberToEnumValue(newNum))
|
||||
if (newNum < 0) {
|
||||
return { newPos, wrap: -1 };
|
||||
} else if (newNum > 3) {
|
||||
return { newPos, wrap: +1 };
|
||||
} else {
|
||||
return { newPos };
|
||||
}
|
||||
}
|
||||
|
||||
public isMiddle() : boolean {
|
||||
|
@ -295,6 +387,14 @@ export class ShortLinesPosition {
|
|||
return this.isLeft() === this.isMiddle() ? Facing.Left : Facing.Right;
|
||||
}
|
||||
|
||||
public facingAcross() : Facing.Left | Facing.Right {
|
||||
return this.isLeft() ? Facing.Right : Facing.Left;
|
||||
}
|
||||
|
||||
public isToLeftOf(otherPos: ShortLinesPosition): boolean {
|
||||
return this.enumValue < otherPos.enumValue;
|
||||
}
|
||||
|
||||
public toString() : string {
|
||||
return this.enumValue.toString();
|
||||
}
|
||||
|
@ -310,7 +410,11 @@ export enum Facing {
|
|||
Left = "Left",
|
||||
Right = "Right",
|
||||
}
|
||||
export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | Facing.Right | Facing.LeftInCircle | Facing.RightInCircle) {
|
||||
export function oppositeFacing(facing: Facing.Up | Facing.Down) : Facing.Up | Facing.Down;
|
||||
export function oppositeFacing(facing: Facing.Left | Facing.Right) : Facing.Left | Facing.Right;
|
||||
export function oppositeFacing(facing: Facing.LeftInCircle | Facing.RightInCircle) : Facing.LeftInCircle | Facing.RightInCircle;
|
||||
export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | Facing.Right) : Facing.Up | Facing.Down | Facing.Left | Facing.Right;
|
||||
export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | Facing.Right | Facing.LeftInCircle | Facing.RightInCircle) : Facing {
|
||||
switch (facing) {
|
||||
case Facing.LeftInCircle:
|
||||
return Facing.RightInCircle;
|
||||
|
@ -326,6 +430,25 @@ export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | F
|
|||
return Facing.Left;
|
||||
}
|
||||
}
|
||||
export function isFacingUpOrDown(facing: Facing | "Across"): facing is Facing.Up | Facing.Down {
|
||||
return facing === Facing.Up || facing === Facing.Down;
|
||||
}
|
||||
export function facingInAround(side: CircleSide) : Facing.Down | Facing.Up | Facing.Left | Facing.Right {
|
||||
switch (side) {
|
||||
case CircleSide.Bottom:
|
||||
return Facing.Up;
|
||||
break;
|
||||
case CircleSide.Top:
|
||||
return Facing.Down;
|
||||
break;
|
||||
case CircleSide.Left:
|
||||
return Facing.Right;
|
||||
break;
|
||||
case CircleSide.Right:
|
||||
return Facing.Left;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export enum StarGrip {
|
||||
HandsAcross = "HandsAcross",
|
||||
|
@ -429,11 +552,17 @@ export const handsFourImproper: Map<DancerIdentity, SemanticPosition & { kind: P
|
|||
}],
|
||||
]);
|
||||
|
||||
export function handsInShortLine({ which, facing, wavy }: { which: ShortLinesPosition; facing: Facing.Up | Facing.Down; wavy: boolean; }): Map<Hand, HandConnection> {
|
||||
return which.isMiddle() ? new Map<Hand, HandConnection>([
|
||||
export function handsInLongLines(wavy: boolean) {
|
||||
return new Map<Hand, HandConnection>([
|
||||
[Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }],
|
||||
[Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }],
|
||||
]) : new Map<Hand, HandConnection>([
|
||||
]);
|
||||
}
|
||||
|
||||
export function handsInShortLine({ which, facing, wavy }: { which: ShortLinesPosition; facing: Facing.Up | Facing.Down; wavy: boolean; }): Map<Hand, HandConnection> {
|
||||
return which.isMiddle()
|
||||
? handsInLongLines(wavy)
|
||||
: new Map<Hand, HandConnection>([
|
||||
which.isLeft() === (facing === Facing.Up)
|
||||
? [Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }]
|
||||
: [Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }]
|
||||
|
@ -443,10 +572,7 @@ export function handsInLine(args: { wavy: boolean, which: ShortLinesPosition | C
|
|||
if (args.which instanceof ShortLinesPosition && (args.facing === Facing.Up || args.facing === Facing.Down)) {
|
||||
return handsInShortLine({ wavy: args.wavy, which: args.which, facing: args.facing });
|
||||
} else {
|
||||
return new Map<Hand, HandConnection>([
|
||||
[Hand.Left, { hand: args.wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }],
|
||||
[Hand.Right, { hand: args.wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }],
|
||||
]);
|
||||
return handsInLongLines(args.wavy);
|
||||
}
|
||||
}
|
||||
export function handToDancerToSideInCircleFacingAcross(which: CirclePosition): Map<Hand, HandConnection> {
|
||||
|
@ -463,3 +589,93 @@ export function handToDancerToSideInCircleFacingUpOrDown(which: CirclePosition):
|
|||
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
|
||||
]);
|
||||
}
|
||||
|
||||
export function facingAdjacent(pos: SemanticPosition, otherPos: SemanticPosition): Facing | undefined {
|
||||
if (pos.kind !== otherPos.kind) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (pos.kind === PositionKind.ShortLines) {
|
||||
if (otherPos.kind !== PositionKind.ShortLines) {
|
||||
return undefined;
|
||||
}
|
||||
if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) {
|
||||
return undefined;
|
||||
}
|
||||
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
|
||||
if (pos.which === ShortLinesPosition.FarLeft && otherPos.which === ShortLinesPosition.FarRight
|
||||
&& ((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0)) {
|
||||
return Facing.Left;
|
||||
} else if (pos.which === ShortLinesPosition.FarRight && otherPos.which === ShortLinesPosition.FarLeft
|
||||
&& ((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0)) {
|
||||
return Facing.Right;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (otherPos.which.isToLeftOf(pos.which)) {
|
||||
return Facing.Left;
|
||||
} else {
|
||||
return Facing.Right;
|
||||
}
|
||||
} else if (pos.kind === PositionKind.Circle) {
|
||||
if (otherPos.kind !== PositionKind.Circle) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
|
||||
// TODO
|
||||
return undefined;
|
||||
}
|
||||
if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) {
|
||||
// TODO
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (pos.which.leftRightSide() === otherPos.which.leftRightSide()) {
|
||||
if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) {
|
||||
return pos.which.facingUpOrDown();
|
||||
} else if (((pos.setOffset ?? 0) + 1) === (otherPos.setOffset ?? 0) && pos.which.isTop()) {
|
||||
return Facing.Up;
|
||||
} else if (((pos.setOffset ?? 0) - 1) === (otherPos.setOffset ?? 0) && !pos.which.isTop()) {
|
||||
return Facing.Down;
|
||||
}
|
||||
} else if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) {
|
||||
if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
|
||||
return pos.which.facingAcross();
|
||||
} else if (((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0) && pos.which.isLeft()) {
|
||||
return Facing.Left;
|
||||
} else if (((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0) && !pos.which.isLeft()) {
|
||||
return Facing.Right;
|
||||
}
|
||||
} else {
|
||||
// Opposite corners of circle.
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unexpected PositionKind: " + otherPos.kind);
|
||||
}
|
||||
}
|
||||
|
||||
export function facingRequireAdjacent(pos: SemanticPosition, otherPos: SemanticPosition, moveName: string): Facing {
|
||||
const res = facingAdjacent(pos, otherPos);
|
||||
|
||||
if (!res) {
|
||||
throw new Error("Cannot " + moveName + " when not adjacent to paired dancer.");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
|
@ -34,7 +34,7 @@ type chooser_slice_return = "straight" | "diagonal" | "none";
|
|||
type chooser_all_or_center_or_outsides = "all" | "center" | "outsides";
|
||||
type chooser_down_the_hall_ender = "turn-alone" | "turn-couple" | "circle" | "cozy" | "cloverleaf" | "thread-needle" | "right-high" | "sliding-doors" | "";
|
||||
type chooser_zig_zag_ender = "" | "ring" | "allemande";
|
||||
type chooser_hey_length = "full" | "half" | "less than half" | "between half and full";
|
||||
type chooser_hey_length = "full" | "half" | "less than half" | "between half and full" | { dancer: chooser_pairz, time: 1 | 2};
|
||||
type chooser_swing_prefix = "none" | "balance" | "meltdown";
|
||||
type chooser_dancers = "everyone" | "gentlespoon" | "gentlespoons" | "ladle" | "ladles" | "partners" | "neighbors" | "ones" | "twos" | "same roles" | "first corners" | "second corners" | "first gentlespoon" | "first ladle" | "second gentlespoon" | "second ladle" | "shadows";
|
||||
type chooser_pair = "gentlespoons" | "ladles" | "ones" | "twos" | "first corners" | "second corners";
|
||||
|
@ -410,8 +410,21 @@ export function nameLibFigureParameters(move: LibFigureMove): Move {
|
|||
.map((v, i, a) => a.indexOf(v) === i && a.lastIndexOf(v) === i ? v : v + (a.slice(0, i).filter(el => el === v).length + 1));
|
||||
const parameters = {};
|
||||
for (let i = 0; i < parameterNames.length; i++) {
|
||||
if (parameterNames[i] === "until") {
|
||||
// parse chooser_hey_length
|
||||
const hey_length: string = move.parameter_values[i];
|
||||
if (hey_length.includes('%%')) {
|
||||
parameters[parameterNames[i]] = {
|
||||
dancer: hey_length.slice(0, -3),
|
||||
time: parseInt(hey_length.at(-1)!),
|
||||
};
|
||||
} else {
|
||||
parameters[parameterNames[i]] = hey_length;
|
||||
}
|
||||
} else {
|
||||
parameters[parameterNames[i]] = move.parameter_values[i];
|
||||
}
|
||||
}
|
||||
const beats: number = parameters["beats"];
|
||||
parameters["beats"] = undefined;
|
||||
return {
|
||||
|
|
|
@ -77,6 +77,8 @@ export type SemanticAnimation = {
|
|||
|
||||
// If true, move in close while rotating.
|
||||
close: boolean,
|
||||
|
||||
facing?: animation.RotationAnimationFacing,
|
||||
} | {
|
||||
kind: SemanticAnimationKind.Swing,
|
||||
|
||||
|
@ -292,7 +294,7 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
|
|||
&& (semantic.balance === BalanceWeight.Backward || semantic.balance === BalanceWeight.Forward)) {
|
||||
balanceOffset = {
|
||||
x: 0,
|
||||
y: position.y * (
|
||||
y: Math.sign(position.y) * (
|
||||
semantic.balance === BalanceWeight.Forward
|
||||
? -balanceAmount
|
||||
: balanceAmount)
|
||||
|
@ -352,9 +354,9 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
|
|||
case HandTo.DancerRight:
|
||||
return OffsetPlus({ x: +1, y: 0 }, balanceHandAdjustment);
|
||||
case HandTo.DancerForward:
|
||||
const armLength = yAmount + (semantic.balance === BalanceWeight.Backward ? balanceAmount : 0);
|
||||
const armLength = yAmount + (semantic.balance === BalanceWeight.Backward ? balanceAmount : semantic.balance === BalanceWeight.Forward ? -balanceAmount : 0);
|
||||
if (hand === connection.hand) {
|
||||
return { x: 0, y: +armLength/2 };
|
||||
return { x: 0, y: +armLength };
|
||||
} else {
|
||||
return { x: dancerWidth / 2 * (hand === Hand.Left ? -1 : +1), y: +armLength };
|
||||
}
|
||||
|
@ -655,7 +657,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
|
|||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
facing: animation.RotationAnimationFacing.Forward,
|
||||
facing: move.movementPattern.facing ?? animation.RotationAnimationFacing.Forward,
|
||||
closer: move.movementPattern.close ? {
|
||||
transitionBeats: 1,
|
||||
minDistance: 1,
|
||||
|
|
|
@ -421,6 +421,46 @@ displaySettingsDiv.appendChild(document.createElement('br'));
|
|||
displaySettingsDiv.appendChild(extraLinesLabel);
|
||||
displaySettingsDiv.appendChild(extraLinesSelector);
|
||||
|
||||
const trailIncrementsSelector = document.createElement('input');
|
||||
trailIncrementsSelector.type = 'number';
|
||||
trailIncrementsSelector.min = '0';
|
||||
trailIncrementsSelector.step = '1';
|
||||
trailIncrementsSelector.value = r.trailIncrements!.toString();
|
||||
trailIncrementsSelector.id = 'trailIncrements';
|
||||
trailIncrementsSelector.style.width = '3em';
|
||||
trailIncrementsSelector.addEventListener('input', (ev) => {
|
||||
r.trailIncrements = trailIncrementsSelector.valueAsNumber;
|
||||
drawAtCurrentBeat();
|
||||
restartAnimation(false);
|
||||
})
|
||||
const trailIncrementsLabel = document.createElement('label');
|
||||
trailIncrementsLabel.innerText = '# trails (faded previous positions): ';
|
||||
trailIncrementsLabel.htmlFor = 'trailIncrements';
|
||||
|
||||
displaySettingsDiv.appendChild(document.createElement('br'));
|
||||
displaySettingsDiv.appendChild(trailIncrementsLabel);
|
||||
displaySettingsDiv.appendChild(trailIncrementsSelector);
|
||||
|
||||
const trailBeatsSelector = document.createElement('input');
|
||||
trailBeatsSelector.type = 'number';
|
||||
trailBeatsSelector.min = '0';
|
||||
trailBeatsSelector.step = '0.1';
|
||||
trailBeatsSelector.value = r.trailLengthInBeats!.toPrecision(1);
|
||||
trailBeatsSelector.id = 'trailBeats';
|
||||
trailBeatsSelector.style.width = '3em';
|
||||
trailBeatsSelector.addEventListener('input', (ev) => {
|
||||
r.trailLengthInBeats = trailBeatsSelector.valueAsNumber;
|
||||
drawAtCurrentBeat();
|
||||
restartAnimation(false);
|
||||
})
|
||||
const trailBeatsLabel = document.createElement('label');
|
||||
trailBeatsLabel.innerText = '# max age of trails (faded previous positions) in beats: ';
|
||||
trailBeatsLabel.htmlFor = 'trailBeats';
|
||||
|
||||
displaySettingsDiv.appendChild(document.createElement('br'));
|
||||
displaySettingsDiv.appendChild(trailBeatsLabel);
|
||||
displaySettingsDiv.appendChild(trailBeatsSelector);
|
||||
|
||||
displaySettingsDiv.appendChild(document.createElement('br'));
|
||||
displaySettingsDiv.appendChild(debugRender);
|
||||
displaySettingsDiv.appendChild(debugRenderLabel);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "../danceCommon.js";
|
||||
import { CirclePosition, CircleSideOrCenter, PositionKind, SemanticPosition } from "../interpreterCommon.js";
|
||||
import { BalanceWeight, CirclePosition, CircleSideOrCenter, DancerDistance, LongLines, PositionKind, SemanticPosition } from "../interpreterCommon.js";
|
||||
import { Move, chooser_pairz } from "../libfigureMapper.js";
|
||||
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
|
||||
|
@ -9,7 +9,7 @@ export const moveInterpreters: Map<MoveName, MoveInterpreterCtor<MoveName>> = ne
|
|||
|
||||
export interface MoveInterpreterCtorArgs<N extends MoveName> {
|
||||
move: Move & { move: N };
|
||||
nextMove: Move;
|
||||
nextMove?: Move;
|
||||
numProgessions: number;
|
||||
}
|
||||
export type SemanticPositionsForAllDancers = Map<DancerIdentity, SemanticPosition>;
|
||||
|
@ -18,11 +18,13 @@ export interface MoveAsLowLevelMovesArgs {
|
|||
}
|
||||
export type LowLevelMovesForAllDancers = Map<DancerIdentity, LowLevelMove[]>;
|
||||
export interface Variant {
|
||||
previousMoveVariant?: string,
|
||||
previousMoveVariant: string,
|
||||
lowLevelMoves: LowLevelMovesForAllDancers,
|
||||
};
|
||||
export type VariantCollection = Map<string, Variant>;
|
||||
|
||||
export type AllVariantsForMoveArgs = Map<string, MoveAsLowLevelMovesArgs>;
|
||||
|
||||
export type PartialLowLevelMove = {
|
||||
remarks?: string,
|
||||
beats: number,
|
||||
|
@ -32,8 +34,7 @@ export type PartialLowLevelMove = {
|
|||
};
|
||||
|
||||
export interface ISingleVariantMoveInterpreter {
|
||||
moveAsLowLevelMoves: () => LowLevelMovesForAllDancers;
|
||||
moveAsVariants: () => VariantCollection;
|
||||
moveAsVariants: (previousMoveVariant: string | undefined) => VariantCollection;
|
||||
};
|
||||
|
||||
export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>, N extends MoveName> implements ISingleVariantMoveInterpreter {
|
||||
|
@ -43,17 +44,38 @@ export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>,
|
|||
constructor(moveInterpreter: T, startingPos: SemanticPositionsForAllDancers) {
|
||||
this.moveInterpreter = moveInterpreter;
|
||||
this.startingPos = startingPos;
|
||||
|
||||
const checkDistance = !this.moveInterpreter.allowStartingClose();
|
||||
const checkBalance = !this.moveInterpreter.allowStartingBalance();
|
||||
const checkLongLines = !this.moveInterpreter.allowStartingLongLines();
|
||||
if (checkBalance || checkDistance || checkLongLines) {
|
||||
for (const [id, startPos] of this.startingPos) {
|
||||
if (checkBalance && startPos.balance) {
|
||||
throw new Error("Can not start " + this.move.move + " with balance weight " + startPos.balance);
|
||||
}
|
||||
if (checkDistance && startPos.dancerDistance && startPos.dancerDistance !== DancerDistance.Normal) {
|
||||
throw new Error("Can not start " + this.move.move + " at dancerDistance " + startPos.dancerDistance);
|
||||
}
|
||||
if (checkLongLines && startPos.longLines) {
|
||||
throw new Error("Can not start " + this.move.move + " at long lines " + startPos.longLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get move() : Move & { move: N } {
|
||||
return this.moveInterpreter.move;
|
||||
}
|
||||
|
||||
abstract moveAsLowLevelMoves(): LowLevelMovesForAllDancers;
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
throw new Error("You must implement either moveAsLowLevelMoves() or moveAsVariants().");
|
||||
}
|
||||
|
||||
moveAsVariants(): VariantCollection {
|
||||
// TODO It would make more sense for previousMoveVariant to get passed into the constructor...
|
||||
// ... but that requires touching every subclass, so it's an annoying refactor.
|
||||
moveAsVariants(previousMoveVariant: string): VariantCollection {
|
||||
return new Map<string, Variant>([
|
||||
["default", { lowLevelMoves: this.moveAsLowLevelMoves() }]
|
||||
["default", { lowLevelMoves: this.moveAsLowLevelMoves(), previousMoveVariant }]
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -293,10 +315,10 @@ export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>,
|
|||
} : undefined);
|
||||
}
|
||||
|
||||
errorStandStill() {
|
||||
errorStandStill(error?: string) {
|
||||
return this.handleMove(({ startPos }) => {
|
||||
return [{
|
||||
interpreterError: "UNKNOWN MOVE '" + this.move.move + "': standing still",
|
||||
interpreterError: error ?? ("UNKNOWN MOVE '" + this.move.move + "': standing still"),
|
||||
move: this.move,
|
||||
startBeat: 0,
|
||||
beats: this.move.beats,
|
||||
|
@ -312,7 +334,7 @@ export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>,
|
|||
|
||||
export abstract class MoveInterpreter<N extends MoveName> {
|
||||
public readonly move: Move & { move: N };
|
||||
public readonly nextMove: Move;
|
||||
public readonly nextMove?: Move;
|
||||
public readonly numProgressions: number;
|
||||
|
||||
constructor({ move, nextMove, numProgessions }: MoveInterpreterCtorArgs<N>) {
|
||||
|
@ -321,21 +343,97 @@ export abstract class MoveInterpreter<N extends MoveName> {
|
|||
this.numProgressions = numProgessions;
|
||||
}
|
||||
|
||||
allowStartingClose(): boolean {
|
||||
// Swings can end close, but most moves can't start close, so do this check by default for all moves.
|
||||
return false;
|
||||
}
|
||||
|
||||
allowStartingBalance(): boolean {
|
||||
// Make moves that support balance beforehand explicitly opt-in.
|
||||
return false;
|
||||
}
|
||||
|
||||
allowStartingLongLines(): boolean {
|
||||
// Long lines is used for tweaking positioning; most moves don't know about it.
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter;
|
||||
|
||||
moveAsLowLevelMoves({ startingPos }: MoveAsLowLevelMovesArgs): LowLevelMovesForAllDancers {
|
||||
return this.buildSingleVariantMoveInterpreter(startingPos).moveAsLowLevelMoves();
|
||||
// TODO Better name?
|
||||
moveAsVariants({ startingPos }: MoveAsLowLevelMovesArgs, previousMoveVariant: string): VariantCollection {
|
||||
return this.buildSingleVariantMoveInterpreter(startingPos).moveAsVariants(previousMoveVariant);
|
||||
}
|
||||
|
||||
allVariantsForMove(args: AllVariantsForMoveArgs): VariantCollection {
|
||||
const res = new Map<string, Variant>();
|
||||
let error;
|
||||
|
||||
for (const [variantName, variantArgs] of args.entries()) {
|
||||
let newVariants: VariantCollection;
|
||||
try {
|
||||
newVariants = this.moveAsVariants(variantArgs, variantName);
|
||||
} catch (ex) {
|
||||
// TODO Maybe have a way to distinguish invalid start from error processing move?
|
||||
error = ex;
|
||||
// If this variant can't be processed, just continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [newVariantName, variant] of newVariants) {
|
||||
let combinedVariantName: string;
|
||||
if (args.size === 1) {
|
||||
combinedVariantName = newVariantName;
|
||||
} else if (newVariants.size === 1) {
|
||||
combinedVariantName = variantName;
|
||||
} else {
|
||||
combinedVariantName = newVariantName + "_from_" + variantName;
|
||||
}
|
||||
|
||||
res.set(combinedVariantName, variant);
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultSingleVariantMoveInterpreter extends SingleVariantMoveInterpreter<DefaultMoveInterpreter, MoveName> {
|
||||
if (res.size === 0) throw error;
|
||||
else if (res.size > 1) {
|
||||
// TODO Try to reduce variants if possible.
|
||||
// XXX TODO Simple hack: perfer starting improper in a circle.
|
||||
if ([...res.values()].find(v => v.previousMoveVariant === "InitialCircle")) {
|
||||
for (const key of [...res.entries()].filter(([k, v]) => v.previousMoveVariant !== "InitialCircle").map(([k, v]) => k)) {
|
||||
res.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorSingleVariantMoveInterpreter extends SingleVariantMoveInterpreter<ErrorMoveInterpreter, MoveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
return this.errorStandStill();
|
||||
return this.errorStandStill(this.moveInterpreter.error);
|
||||
}
|
||||
}
|
||||
class DefaultMoveInterpreter extends MoveInterpreter<MoveName> {
|
||||
export class ErrorMoveInterpreter extends MoveInterpreter<MoveName> {
|
||||
public readonly error?: string;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<MoveName>, error?: string) {
|
||||
super(args);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
override allowStartingBalance(): boolean {
|
||||
return true;
|
||||
}
|
||||
override allowStartingClose(): boolean {
|
||||
return true;
|
||||
}
|
||||
override allowStartingLongLines(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
throw new Error("Method not implemented.");
|
||||
//throw new Error("Method not implemented.");
|
||||
return new ErrorSingleVariantMoveInterpreter(this, startingPos);
|
||||
}
|
||||
}
|
||||
export const errorMoveInterpreterCtor: MoveInterpreterCtor<MoveName> = DefaultMoveInterpreter;
|
||||
export const errorMoveInterpreterCtor: MoveInterpreterCtor<MoveName> = ErrorMoveInterpreter;Error
|
|
@ -1,7 +1,7 @@
|
|||
import { SemanticPosition, PositionKind, CircleSide, Facing, CirclePosition, LongLines, HandConnection } from "../interpreterCommon.js";
|
||||
import { SemanticPosition, PositionKind, CircleSide, Facing, CirclePosition, LongLines, HandConnection, DancerDistance } from "../interpreterCommon.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
type allemandeMoves = "allemande" | "allemande orbit" | "gyre";
|
||||
|
||||
|
@ -10,32 +10,36 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
// Need to store this locally so checking move.move restricts move.parameters.
|
||||
const move = this.move;
|
||||
|
||||
const allemandeCircling = move.move === "allemande orbit" ? move.parameters.circling1 : move.parameters.circling;
|
||||
const byHandOrShoulder = (move.move === "gyre" ? move.parameters.shoulder : move.parameters.hand) ? Hand.Right : Hand.Left;
|
||||
|
||||
// TODO Not sure if this is right.
|
||||
const swap = allemandeCircling % 360 === 180;
|
||||
const returnToStart = allemandeCircling % 360 === 0;
|
||||
const intoWave = !swap && !returnToStart && allemandeCircling % 90 == 0;
|
||||
const intoWavePositions = !intoWave ? 0 : (allemandeCircling % 360 === 90) === (byHandOrShoulder === Hand.Left) ? 1 : -1;
|
||||
if (!swap && !returnToStart && !intoWave) {
|
||||
// TODO Support allemande that's not a swap or no-op.
|
||||
throw "Unsupported allemande circle amount: " + allemandeCircling;
|
||||
for (const [id, pos] of this.startingPos.entries()) {
|
||||
if (this.findPairOpposite(this.move.parameters.who, id) === null) {
|
||||
if (pos.dancerDistance && pos.dancerDistance !== DancerDistance.Normal) {
|
||||
throw new Error("Dancers not involved in allemande must start at normal dancer distance.");
|
||||
}
|
||||
if (pos.longLines) {
|
||||
throw new Error("Dancers not involved in allemande must start not in long lines.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.handlePairedMove(move.parameters.who, ({ startPos, around, withId, withPos }) => {
|
||||
let endPosition: SemanticPosition = startPos;
|
||||
let startingPos = startPos;
|
||||
if (swap) {
|
||||
return this.handlePairedMove(this.move.parameters.who, ({ startPos, around, withId, withPos }) => {
|
||||
if (startPos.kind === PositionKind.ShortLines && withPos.kind === PositionKind.ShortLines
|
||||
&& startPos.which.leftRightSide() != withPos.which.leftRightSide()
|
||||
&& (!startPos.which.isMiddle() || !withPos.which.isMiddle())) {
|
||||
throw new Error("Allemande in short lines must either be on same side of set or in the middle.");
|
||||
}
|
||||
|
||||
let endPosition: SemanticPosition = {...startPos, dancerDistance: undefined, longLines: undefined, balance: undefined };
|
||||
let startingPos = {...startPos };
|
||||
if (this.moveInterpreter.swap) {
|
||||
// TODO This was more complicated. Is this wrong?
|
||||
endPosition = withPos;
|
||||
} else if (intoWave) {
|
||||
endPosition = {...withPos, dancerDistance: undefined, longLines: undefined, balance: undefined };
|
||||
} else if (this.moveInterpreter.intoWave) {
|
||||
if (startPos.kind === PositionKind.ShortLines) {
|
||||
if (around === CircleSide.Left || around === CircleSide.Right) {
|
||||
// Fix startPos if necessary. Needed because pass through always swaps but sometimes shouldn't.
|
||||
let startWhich = startPos.which;
|
||||
if ((startPos.facing === Facing.Up || startPos.facing === Facing.Down) &&
|
||||
((byHandOrShoulder === Hand.Right)
|
||||
((this.moveInterpreter.byHandOrShoulder === Hand.Right)
|
||||
!== (startPos.facing === Facing.Up)
|
||||
!== startPos.which.isLeftOfSide())) {
|
||||
startWhich = startPos.which.swapOnSide()
|
||||
|
@ -47,14 +51,14 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
|
||||
const endWhich = CirclePosition.fromSides(startingPos.which.leftRightSide(),
|
||||
startWhich.isLeftOfSide()
|
||||
!== (byHandOrShoulder === Hand.Right)
|
||||
!== (intoWavePositions === 1)
|
||||
!== (this.moveInterpreter.byHandOrShoulder === Hand.Right)
|
||||
!== (this.moveInterpreter.intoWavePositions === 1)
|
||||
? CircleSide.Top
|
||||
: CircleSide.Bottom);
|
||||
endPosition = {
|
||||
kind: PositionKind.Circle,
|
||||
which: endWhich,
|
||||
facing: (startingPos.facing === Facing.Up) === (intoWavePositions === 1)
|
||||
facing: (startingPos.facing === Facing.Up) === (this.moveInterpreter.intoWavePositions === 1)
|
||||
? endWhich.facingAcross()
|
||||
: endWhich.facingOut(),
|
||||
setOffset: startingPos.setOffset,
|
||||
|
@ -66,7 +70,7 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
} else {
|
||||
if (around === "Center") {
|
||||
const startCenter = startPos.longLines === LongLines.Center;
|
||||
const endWhich = startPos.which.circleRight(intoWavePositions);
|
||||
const endWhich = startPos.which.circleRight(this.moveInterpreter.intoWavePositions);
|
||||
endPosition = {
|
||||
kind: PositionKind.Circle,
|
||||
which: endWhich,
|
||||
|
@ -76,11 +80,11 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
lineOffset: startPos.lineOffset,
|
||||
}
|
||||
} else {
|
||||
const endWhich = startPos.which.toShortLines(intoWavePositions === 1 ? Hand.Right : Hand.Left);
|
||||
const endWhich = startPos.which.toShortLines(this.moveInterpreter.intoWavePositions === 1 ? Hand.Right : Hand.Left);
|
||||
endPosition = {
|
||||
kind: PositionKind.ShortLines,
|
||||
which: endWhich,
|
||||
facing: endWhich.isLeftOfSide() === (byHandOrShoulder === Hand.Left) ? Facing.Up : Facing.Down,
|
||||
facing: endWhich.isLeftOfSide() === (this.moveInterpreter.byHandOrShoulder === Hand.Left) ? Facing.Up : Facing.Down,
|
||||
setOffset: startPos.setOffset,
|
||||
lineOffset: startPos.lineOffset,
|
||||
}
|
||||
|
@ -90,30 +94,23 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
|
||||
return this.combine([
|
||||
{
|
||||
beats: move.beats,
|
||||
beats: this.move.beats,
|
||||
endPosition,
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.RotateAround,
|
||||
minAmount: byHandOrShoulder === Hand.Right ? allemandeCircling : -allemandeCircling,
|
||||
minAmount: this.moveInterpreter.byHandOrShoulder === Hand.Right ? this.moveInterpreter.allemandeCircling : -this.moveInterpreter.allemandeCircling,
|
||||
around,
|
||||
byHand: move.move === "allemande" || move.move === "allemande orbit" ? byHandOrShoulder : undefined,
|
||||
byHand: move.move === "allemande" || move.move === "allemande orbit" ? this.moveInterpreter.byHandOrShoulder : undefined,
|
||||
close: true,
|
||||
},
|
||||
},
|
||||
], {
|
||||
...startingPos,
|
||||
hands: startPos.hands && move.move !== "gyre"
|
||||
? new Map<Hand, HandConnection>([...startPos.hands.entries()].filter(([h, c]) => h === byHandOrShoulder))
|
||||
? new Map<Hand, HandConnection>([...startPos.hands.entries()].filter(([h, c]) => h === this.moveInterpreter.byHandOrShoulder))
|
||||
: undefined
|
||||
});
|
||||
}, move.move !== "allemande orbit" ? undefined : ({ id, startPos }) => {
|
||||
const orbitAmount = move.parameters.circling2;
|
||||
const swap = orbitAmount % 360 === 180;
|
||||
if (!swap && orbitAmount % 360 !== 0) {
|
||||
// TODO Support allemande that's not a swap or no-op.
|
||||
throw "Unsupported allemande orbit amount: " + orbitAmount;
|
||||
}
|
||||
|
||||
const startingPos: SemanticPosition = {
|
||||
...startPos,
|
||||
hands: undefined,
|
||||
|
@ -121,7 +118,7 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
dancerDistance: undefined,
|
||||
}
|
||||
let endPosition: SemanticPosition;
|
||||
if (swap) {
|
||||
if (this.moveInterpreter.orbitSwap) {
|
||||
if (startingPos.kind === PositionKind.Circle) {
|
||||
endPosition =
|
||||
{
|
||||
|
@ -148,7 +145,7 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
movementPattern: {
|
||||
kind: SemanticAnimationKind.RotateAround,
|
||||
// Orbit is opposite direction of allemande.
|
||||
minAmount: byHandOrShoulder === Hand.Right ? -orbitAmount : +orbitAmount,
|
||||
minAmount: this.moveInterpreter.byHandOrShoulder === Hand.Right ? -this.moveInterpreter.orbitCircling : +this.moveInterpreter.orbitCircling,
|
||||
around: "Center",
|
||||
byHand: undefined,
|
||||
close: false,
|
||||
|
@ -160,6 +157,55 @@ class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, all
|
|||
}
|
||||
|
||||
class Allemande extends MoveInterpreter<allemandeMoves> {
|
||||
public readonly allemandeCircling: number;
|
||||
public readonly orbitCircling: number;
|
||||
public readonly byHandOrShoulder: Hand;
|
||||
public readonly swap: boolean;
|
||||
public readonly orbitSwap: boolean;
|
||||
public readonly returnToStart: boolean;
|
||||
public readonly intoWave: boolean;
|
||||
public readonly intoWavePositions: number;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<allemandeMoves>) {
|
||||
super(args);
|
||||
|
||||
this.allemandeCircling = this.move.move === "allemande orbit" ? this.move.parameters.circling1 : this.move.parameters.circling;
|
||||
this.byHandOrShoulder = (this.move.move === "gyre" ? this.move.parameters.shoulder : this.move.parameters.hand) ? Hand.Right : Hand.Left;
|
||||
|
||||
// TODO Not sure if this is right.
|
||||
this.swap = this.allemandeCircling % 360 === 180;
|
||||
this.returnToStart = this.allemandeCircling % 360 === 0;
|
||||
this.intoWave = !this.swap && !this.returnToStart && this.allemandeCircling % 90 == 0;
|
||||
this.intoWavePositions = !this.intoWave
|
||||
? 0
|
||||
: (this.allemandeCircling % 360 === 90) === (this.byHandOrShoulder === Hand.Left)
|
||||
? +1
|
||||
: -1;
|
||||
if (!this.swap && !this.returnToStart && !this.intoWave) {
|
||||
// TODO Support allemande that's not a swap or no-op.
|
||||
throw "Unsupported allemande circle amount: " + this.allemandeCircling;
|
||||
}
|
||||
|
||||
if (this.move.move === "allemande orbit") {
|
||||
this.orbitCircling = this.move.parameters.circling2;
|
||||
this.orbitSwap = this.orbitCircling % 360 === 180;
|
||||
if (!this.orbitSwap && this.orbitCircling % 360 !== 0) {
|
||||
// TODO Support allemande that's not a swap or no-op.
|
||||
throw "Unsupported allemande orbit amount: " + this.orbitCircling;
|
||||
}
|
||||
} else {
|
||||
this.orbitCircling = 0;
|
||||
this.orbitSwap = false;
|
||||
}
|
||||
}
|
||||
|
||||
override allowStartingClose(): boolean {
|
||||
return true;
|
||||
}
|
||||
override allowStartingLongLines(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new AllemandeSingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -1,39 +1,100 @@
|
|||
import { BalanceWeight } from "../interpreterCommon.js";
|
||||
import { BalanceWeight, Facing, HandConnection, HandTo, PositionKind, facingAdjacent, facingRequireAdjacent } from "../interpreterCommon.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, Variant, VariantCollection, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
class BalanceSingleVariant extends SingleVariantMoveInterpreter<Balance, "balance"> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
const moveName = "balance";
|
||||
|
||||
class BalanceSingleVariant extends SingleVariantMoveInterpreter<Balance, typeof moveName> {
|
||||
private static readonly balanceOptions : [BalanceWeight, BalanceWeight | undefined, Hand | undefined][]= [
|
||||
// TODO Any others?
|
||||
[BalanceWeight.Forward, undefined, undefined],
|
||||
[BalanceWeight.Forward, BalanceWeight.Backward, undefined],
|
||||
// TODO Should be left/right/inside/outside hands?
|
||||
[BalanceWeight.Forward, BalanceWeight.Backward, Hand.Left],
|
||||
[BalanceWeight.Forward, BalanceWeight.Backward, Hand.Right],
|
||||
[BalanceWeight.Right, BalanceWeight.Backward, undefined],
|
||||
[BalanceWeight.Right, BalanceWeight.Left, undefined],
|
||||
[BalanceWeight.Left, BalanceWeight.Right, undefined],
|
||||
];
|
||||
|
||||
moveAsVariants(previousMoveVariant: string): VariantCollection {
|
||||
const res = new Map<string, Variant>();
|
||||
|
||||
for (const [firstWeight, secondWeight, hand] of BalanceSingleVariant.balanceOptions) {
|
||||
// If balancing someone, need to know by which hand.
|
||||
if ((hand === undefined) !== (this.move.parameters.who === "everyone")) continue;
|
||||
try {
|
||||
res.set((firstWeight?.toString() ?? "") + (secondWeight?.toString() ?? "") + (hand === undefined ? "" : hand.toString() + "Hand"), {
|
||||
lowLevelMoves: this.moveAsLowLevelMovesWeights(firstWeight, secondWeight, hand),
|
||||
previousMoveVariant
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
moveAsLowLevelMovesWeights(firstWeight?: BalanceWeight, secondWeight?: BalanceWeight, hand?: Hand): LowLevelMovesForAllDancers {
|
||||
if (this.move.parameters.who !== "everyone") {
|
||||
return this.handlePairedMove(this.move.parameters.who, ({ startPos, withPos }) => {
|
||||
// TODO Does this need to support balancing inside/outside hands? If so how to identify them?
|
||||
const hands = hand === undefined ? startPos.hands : new Map<Hand, HandConnection>([[hand, {hand, to: HandTo.DancerForward}]]);
|
||||
|
||||
const facing = facingRequireAdjacent(startPos, withPos, this.move.move);
|
||||
|
||||
const startingPos = {...startPos, facing, hands};
|
||||
|
||||
return this.combine([
|
||||
{
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: { ...startingPos, balance: firstWeight, hands },
|
||||
movementPattern: { kind: SemanticAnimationKind.Linear },
|
||||
},
|
||||
{
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: { ...startingPos, balance: secondWeight, hands },
|
||||
movementPattern: { kind: SemanticAnimationKind.Linear },
|
||||
},
|
||||
], startingPos);
|
||||
});
|
||||
} else {
|
||||
return this.handleMove(({ startPos }) => {
|
||||
// TODO Use who to determine facing?
|
||||
// TODO Could be left to right, not back and forth?
|
||||
// TODO How to determine hand... by next move, I guess?
|
||||
if (startPos.kind === PositionKind.Circle && startPos.facing === Facing.CenterOfCircle) {
|
||||
if (firstWeight === BalanceWeight.Left || firstWeight === BalanceWeight.Right || firstWeight === BalanceWeight.Backward
|
||||
|| secondWeight === BalanceWeight.Left || secondWeight === BalanceWeight.Right || secondWeight === BalanceWeight.Backward) {
|
||||
throw new Error("Balancing left or right in a circle is unsupported.");
|
||||
}
|
||||
}
|
||||
|
||||
return this.combine([
|
||||
{
|
||||
beats: this.moveInterpreter.forwardBeats,
|
||||
endPosition: { ...startPos, balance: BalanceWeight.Forward },
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: { ...startPos, balance: firstWeight },
|
||||
movementPattern: { kind: SemanticAnimationKind.Linear },
|
||||
},
|
||||
{
|
||||
beats: this.moveInterpreter.backwardBeats,
|
||||
endPosition: { ...startPos, balance: BalanceWeight.Backward },
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: { ...startPos, balance: secondWeight },
|
||||
movementPattern: { kind: SemanticAnimationKind.Linear },
|
||||
},
|
||||
], startPos);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Balance extends MoveInterpreter<"balance"> {
|
||||
public readonly forwardBeats: number;
|
||||
public readonly backwardBeats: number;
|
||||
class Balance extends MoveInterpreter<typeof moveName> {
|
||||
public readonly balancePartBeats: number;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<"balance">) {
|
||||
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
||||
super(args);
|
||||
|
||||
this.forwardBeats = this.move.beats / 2;
|
||||
this.backwardBeats = this.move.beats - this.forwardBeats;
|
||||
this.balancePartBeats = this.move.beats / 2;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
|
@ -41,4 +102,4 @@ class Balance extends MoveInterpreter<"balance"> {
|
|||
}
|
||||
}
|
||||
|
||||
moveInterpreters.set("balance", Balance);
|
||||
moveInterpreters.set(moveName, Balance);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BalanceWeight, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "../interpreterCommon.js";
|
||||
import { BalanceWeight, Facing, HandConnection, HandTo, PositionKind, SemanticPosition, handsInCircle } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { LowLevelMove, SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
|
@ -15,10 +15,7 @@ export function balanceCircleInAndOut(move: Move, startPos: SemanticPosition, ba
|
|||
|
||||
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 }],
|
||||
]),
|
||||
hands: handsInCircle,
|
||||
};
|
||||
const circleBalancedIn: SemanticPosition = {...holdingHandsInCircle,
|
||||
balance: BalanceWeight.Forward,
|
||||
|
|
|
@ -1,14 +1,30 @@
|
|||
import { DanceRole, CoupleRole } from "../danceCommon.js";
|
||||
import { SemanticPosition, Facing, HandConnection, HandTo, BalanceWeight } from "../interpreterCommon.js";
|
||||
import { SemanticPosition, Facing, HandConnection, HandTo, BalanceWeight, handsInLine, LongLines } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, Variant, VariantCollection, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
const moveName: Move["move"] = "box circulate";
|
||||
|
||||
class BoxCirculateSingleVariant extends SingleVariantMoveInterpreter<BoxCirculate, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
moveAsVariants(previousMoveVariant: string): VariantCollection {
|
||||
const res = new Map<string, Variant>();
|
||||
|
||||
for (const crossPartway of (this.move.note?.includes("catch") ? [true] : [true, false])) {
|
||||
try {
|
||||
res.set(crossPartway ? "CrossPartway" : "Normal", {
|
||||
lowLevelMoves: this.moveAsLowLevelMovesCross(crossPartway),
|
||||
previousMoveVariant
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
moveAsLowLevelMovesCross(crossPartway: boolean): LowLevelMovesForAllDancers {
|
||||
const circulateRight: boolean = this.move.parameters.hand;
|
||||
const whoCrosses = this.move.parameters.who;
|
||||
return this.handleCircleMove(({ id, startPos }) => {
|
||||
|
@ -34,7 +50,7 @@ class BoxCirculateSingleVariant extends SingleVariantMoveInterpreter<BoxCirculat
|
|||
// Starts in long wavy lines.
|
||||
const startingPos: SemanticPosition = {
|
||||
...startPos,
|
||||
facing: isCrossing === startPos.which.isLeft() ? Facing.Right : Facing.Left,
|
||||
facing: isCrossing ? startPos.which.facingAcross() : startPos.which.facingOut(),
|
||||
hands: new Map<Hand, HandConnection>([
|
||||
[Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }],
|
||||
[Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }],
|
||||
|
@ -61,6 +77,7 @@ class BoxCirculateSingleVariant extends SingleVariantMoveInterpreter<BoxCirculat
|
|||
...startingPos,
|
||||
which: isCrossing ? startingPos.which.swapAcross() : startingPos.which.swapUpAndDown(),
|
||||
facing: isCrossing ? startingPos.facing : startingPos.facing === Facing.Right ? Facing.Left : Facing.Right,
|
||||
longLines: isCrossing && crossPartway ? LongLines.Center : undefined,
|
||||
},
|
||||
movementPattern: {
|
||||
// TODO Not sure loop should really be linear...
|
||||
|
|
|
@ -1,44 +1,41 @@
|
|||
import { HandConnection, HandTo, BalanceWeight } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { HandConnection, HandTo, BalanceWeight, facingRequireAdjacent } from "../interpreterCommon.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
const moveName: Move["move"] = "box the gnat";
|
||||
const moveName = "box the gnat";
|
||||
|
||||
class BoxTheGnatSingleVariant extends SingleVariantMoveInterpreter<BoxTheGnat, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
return this.handlePairedMove(this.move.parameters.who, ({ startPos, around, withPos }) => {
|
||||
const hand = this.move.parameters.hand ? Hand.Right : Hand.Left;
|
||||
const balanceBeats = this.move.parameters.bal
|
||||
? this.move.beats > 4
|
||||
? this.move.beats - 4
|
||||
: 2
|
||||
: 0;
|
||||
const balancePartBeats = balanceBeats / 2;
|
||||
const twirlBeats = this.move.beats - balanceBeats;
|
||||
if (startPos.hands && startPos.hands.get(Hand.Right) === undefined && startPos.hands.get(Hand.Left)?.hand === Hand.Left) {
|
||||
throw new Error(this.move.move + " shouldn't start with holding left hands. Something went wrong.");
|
||||
}
|
||||
if (startPos.balance && startPos.balance !== BalanceWeight.Backward) {
|
||||
throw new Error(this.move.move + " shouldn't start with balance weight " + startPos.balance);
|
||||
}
|
||||
|
||||
// TODO Adjust facing?
|
||||
const startPosition = { ...startPos, hands: new Map<Hand, HandConnection>([[hand, { hand, to: HandTo.DancerForward }]]) };
|
||||
const facing = facingRequireAdjacent(startPos, withPos, this.move.move);
|
||||
const startPosition = { ...startPos, hands: this.moveInterpreter.hands, facing };
|
||||
|
||||
if (around === "Center") {
|
||||
throw "TwirlSwap around center is unsupported.";
|
||||
}
|
||||
|
||||
const twirl: PartialLowLevelMove = {
|
||||
beats: twirlBeats,
|
||||
endPosition: withPos,
|
||||
beats: this.moveInterpreter.twirlBeats,
|
||||
endPosition: { ...withPos, balance: undefined },
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.TwirlSwap,
|
||||
around,
|
||||
hand,
|
||||
hand: this.moveInterpreter.hand,
|
||||
}
|
||||
};
|
||||
|
||||
if (this.move.parameters.bal) {
|
||||
return this.combine([
|
||||
{
|
||||
beats: balancePartBeats,
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: {
|
||||
...startPosition,
|
||||
balance: BalanceWeight.Forward,
|
||||
|
@ -48,7 +45,7 @@ class BoxTheGnatSingleVariant extends SingleVariantMoveInterpreter<BoxTheGnat, t
|
|||
}
|
||||
},
|
||||
{
|
||||
beats: balancePartBeats,
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: {
|
||||
...startPosition,
|
||||
balance: BalanceWeight.Backward,
|
||||
|
@ -66,6 +63,29 @@ class BoxTheGnatSingleVariant extends SingleVariantMoveInterpreter<BoxTheGnat, t
|
|||
}
|
||||
|
||||
class BoxTheGnat extends MoveInterpreter<typeof moveName> {
|
||||
public readonly hand: Hand;
|
||||
public readonly hands: Map<Hand, HandConnection>;
|
||||
public readonly balancePartBeats: number;
|
||||
public readonly twirlBeats: number;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
||||
super(args);
|
||||
|
||||
this.hand = this.move.parameters.hand ? Hand.Right : Hand.Left;
|
||||
this.hands = new Map<Hand, HandConnection>([[this.hand, { hand: this.hand, to: HandTo.DancerForward }]]);
|
||||
const balanceBeats = this.move.parameters.bal
|
||||
? this.move.beats > 4
|
||||
? this.move.beats - 4
|
||||
: this.move.beats / 2
|
||||
: 0;
|
||||
this.balancePartBeats = balanceBeats / 2;
|
||||
this.twirlBeats = this.move.beats - balanceBeats;
|
||||
}
|
||||
|
||||
override allowStartingBalance(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new BoxTheGnatSingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { RotationAnimationFacing } from "../animation.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
|
@ -14,8 +15,10 @@ class ButterflyWhirlSingleVariant extends SingleVariantMoveInterpreter<Butterfly
|
|||
movementPattern: {
|
||||
kind: SemanticAnimationKind.RotateAround,
|
||||
around: startPos.which.leftRightSide(),
|
||||
// TODO hand around isn't the same as allemande...
|
||||
byHand: startPos.which.isOnLeftLookingAcross() ? Hand.Right : Hand.Left,
|
||||
facing: startPos.which.isOnLeftLookingAcross()
|
||||
? RotationAnimationFacing.Forward
|
||||
: RotationAnimationFacing.Backward,
|
||||
close: true,
|
||||
minAmount: 360,
|
||||
}
|
||||
|
@ -25,6 +28,10 @@ class ButterflyWhirlSingleVariant extends SingleVariantMoveInterpreter<Butterfly
|
|||
}
|
||||
|
||||
class ButterflyWhirl extends MoveInterpreter<typeof moveName> {
|
||||
override allowStartingClose(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new ButterflyWhirlSingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { HandConnection, HandTo, CircleSide, Facing } from "../interpreterCommon.js";
|
||||
import { HandConnection, HandTo, CircleSide, Facing, facingInAround, oppositeFacing } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
|
@ -8,10 +8,17 @@ const moveName: Move["move"] = "California twirl";
|
|||
|
||||
class CaliforniaTwirlSingleVariant extends SingleVariantMoveInterpreter<CaliforniaTwirl, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
return this.handleCirclePairedMove(this.move.parameters.who, ({ startPos }) => {
|
||||
// TODO does "who" matter here or is it entirely positional? At least need to know who to omit.
|
||||
return this.handleCirclePairedMove(this.move.parameters.who, ({ startPos, around }) => {
|
||||
if (around === "Center") {
|
||||
throw new Error("Don't know how to " + this.move.move + " around the center.");
|
||||
}
|
||||
|
||||
const onLeft: boolean = startPos.which.isOnLeftLookingUpAndDown();
|
||||
if (startPos.facing === Facing.LeftInCircle || startPos.facing === Facing.RightInCircle) {
|
||||
throw new Error("Don't know how to " + this.move.move + " starting facing " + startPos.facing);
|
||||
}
|
||||
|
||||
const facing = startPos.facing === Facing.CenterOfCircle ? facingInAround(around) : startPos.facing;
|
||||
const onLeft: boolean = startPos.which.isOnLeft(around, startPos.facing);
|
||||
// TODO get rid of this 1 beat set up and make it part of TwirlSwap?
|
||||
return this.combine([
|
||||
{
|
||||
|
@ -21,7 +28,7 @@ class CaliforniaTwirlSingleVariant extends SingleVariantMoveInterpreter<Californ
|
|||
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,
|
||||
facing,
|
||||
},
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.Linear,
|
||||
|
@ -31,12 +38,12 @@ class CaliforniaTwirlSingleVariant extends SingleVariantMoveInterpreter<Californ
|
|||
beats: this.move.beats - 1,
|
||||
endPosition: {
|
||||
...startPos,
|
||||
which: startPos.which.swapAcross(),
|
||||
facing: startPos.which.topBottomSide() === CircleSide.Top ? Facing.Up : Facing.Down,
|
||||
which: startPos.which.swapOnSide(around),
|
||||
facing: oppositeFacing(facing),
|
||||
},
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.TwirlSwap,
|
||||
around: startPos.which.topBottomSide(),
|
||||
around,
|
||||
hand: onLeft ? Hand.Right : Hand.Left,
|
||||
}
|
||||
}], startPos);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Facing, handsInCircle } from "../interpreterCommon.js";
|
||||
import { DancerDistance, Facing, handsInCircle } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
|
|
@ -22,6 +22,9 @@ class DownTheHallSingleVariant extends SingleVariantMoveInterpreter<DownTheHall,
|
|||
}
|
||||
return this.handleMove(({ startPos }) => {
|
||||
const startFacing = this.move.parameters.facing === "backward" ? Facing.Up : Facing.Down;
|
||||
if (startPos.facing !== startFacing && (startPos.facing === Facing.Up || startPos.facing === Facing.Down)) {
|
||||
throw new Error("Started facing the wrong direction.");
|
||||
}
|
||||
const startWhich: ShortLinesPosition = startPos.kind === PositionKind.ShortLines
|
||||
? startPos.which
|
||||
// TODO Is this always the right way to convert circle to short lines?
|
||||
|
|
|
@ -1,44 +1,17 @@
|
|||
import { DancerIdentity } from "../danceCommon.js";
|
||||
import { SemanticPosition, PositionKind, ShortLinesPosition, CirclePosition, CircleSide, Facing } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { LowLevelMove, SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { dancerIsPair } from "../libfigure/util.js";
|
||||
|
||||
const moveName: Move["move"] = "hey";
|
||||
|
||||
class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
// Needed for inner functions... that probably should be methods.
|
||||
const move = this.move;
|
||||
const moveName = "hey";
|
||||
|
||||
type HeyStep = {
|
||||
kind: "StandStill" | "Loop" | "CenterPass" | "EndsPassIn" | "EndsPassOut" | "Ricochet",
|
||||
endPosition: SemanticPosition,
|
||||
}
|
||||
|
||||
if (this.move.parameters.dir !== "across") {
|
||||
throw new Error("Unsupported hey direction: " + this.move.parameters.dir);
|
||||
}
|
||||
|
||||
let heyParts: number;
|
||||
switch (this.move.parameters.until) {
|
||||
case "half":
|
||||
heyParts = 4;
|
||||
break;
|
||||
case "full":
|
||||
heyParts = 8;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported hey 'until': " + this.move.parameters.until);
|
||||
}
|
||||
const heyPartBeats: number = this.move.beats / heyParts;
|
||||
// TODO is this right?
|
||||
const firstPassInCenter: boolean = dancerIsPair(this.move.parameters.who);
|
||||
const centerShoulder = firstPassInCenter === this.move.parameters.shoulder ? Hand.Right : Hand.Left;
|
||||
const endsShoulder = centerShoulder.opposite();
|
||||
|
||||
function fixupHeyOtherPath(withoutOtherPath: Map<DancerIdentity, (LowLevelMove & { heyStep?: HeyStep })[]>): Map<DancerIdentity, LowLevelMove[]> {
|
||||
const numSteps = withoutOtherPath.get(DancerIdentity.OnesLark)!.length;
|
||||
for (let i = 0; i < numSteps; i++) {
|
||||
|
@ -107,11 +80,10 @@ class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName
|
|||
return withoutOtherPath;
|
||||
}
|
||||
|
||||
return fixupHeyOtherPath(this.handleMove(({ id, startPos }) => {
|
||||
const endsInCircle = startPos.kind === PositionKind.Circle;
|
||||
function heyStepToPartialLowLevelMove(heyStep: HeyStep): PartialLowLevelMove & { heyStep: HeyStep } {
|
||||
class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName> {
|
||||
heyStepToPartialLowLevelMove(heyStep: HeyStep): PartialLowLevelMove & { heyStep: HeyStep } {
|
||||
return {
|
||||
beats: heyPartBeats,
|
||||
beats: this.moveInterpreter.heyPartBeats,
|
||||
// TODO use circle positions on ends? ... unless hey ends in a box the gnat or similar...
|
||||
endPosition: heyStep.endPosition,
|
||||
movementPattern: heyStep.kind === "StandStill" ? {
|
||||
|
@ -119,7 +91,7 @@ class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName
|
|||
} : heyStep.kind === "Loop" ? {
|
||||
// TODO Loop should probably be its own kind? Or RotateAround?
|
||||
kind: SemanticAnimationKind.Linear,
|
||||
minRotation: endsShoulder === Hand.Right ? +180 : -180,
|
||||
minRotation: this.moveInterpreter.endsShoulder === Hand.Right ? +180 : -180,
|
||||
} : heyStep.kind === "Ricochet" ? {
|
||||
// TODO This is a hack.
|
||||
kind: SemanticAnimationKind.PassBy,
|
||||
|
@ -127,21 +99,24 @@ class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName
|
|||
withHands: false,
|
||||
otherPath: "Swap",
|
||||
facing: "Start",
|
||||
side: endsShoulder,
|
||||
side: this.moveInterpreter.endsShoulder,
|
||||
} : {
|
||||
kind: SemanticAnimationKind.PassBy,
|
||||
around: heyStep.kind === "CenterPass" ? "Center" : heyStep.endPosition.which.leftRightSide(),
|
||||
withHands: false,
|
||||
side: heyStep.kind === "CenterPass" ? centerShoulder : endsShoulder,
|
||||
side: heyStep.kind === "CenterPass" ? this.moveInterpreter.centerShoulder : this.moveInterpreter.endsShoulder,
|
||||
facing: "Start",
|
||||
otherPath: undefined!, // Placeholder, fixup later.
|
||||
},
|
||||
heyStep,
|
||||
};
|
||||
}
|
||||
function continueHey(prevStep: HeyStep, stepsLeft: number, beenInCenter: boolean): HeyStep {
|
||||
|
||||
continueHey(prevStep: HeyStep, stepsLeft: number,
|
||||
{ beenInCenter, endsInCircle, inCenterFirst }:
|
||||
{ beenInCenter: boolean, endsInCircle: boolean, inCenterFirst: boolean }): HeyStep {
|
||||
// TODO Not sure why type checker requires rechecking this here.
|
||||
if (move.move !== "hey") throw new Error("Unreachable.");
|
||||
if (this.move.move !== "hey") throw new Error("Unreachable.");
|
||||
|
||||
// Continuing hey so everyone is either passing (in center or on ends) or looping on ends.
|
||||
if (prevStep.endPosition.kind === PositionKind.Circle) {
|
||||
|
@ -201,19 +176,19 @@ class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName
|
|||
...prevStep.endPosition,
|
||||
kind: PositionKind.Circle,
|
||||
which: prevStep.endPosition.which.isLeft()
|
||||
? (endsShoulder === Hand.Right ? CirclePosition.TopLeft : CirclePosition.BottomLeft)
|
||||
: (endsShoulder === Hand.Right ? CirclePosition.BottomRight : CirclePosition.TopRight),
|
||||
? (this.moveInterpreter.endsShoulder === Hand.Right ? CirclePosition.TopLeft : CirclePosition.BottomLeft)
|
||||
: (this.moveInterpreter.endsShoulder === Hand.Right ? CirclePosition.BottomRight : CirclePosition.TopRight),
|
||||
},
|
||||
}
|
||||
}
|
||||
else if (!isFacingSide) {
|
||||
const rico = inCenterFirst
|
||||
? beenInCenter
|
||||
? move.parameters.rico3
|
||||
: move.parameters.rico1
|
||||
? this.move.parameters.rico3
|
||||
: this.move.parameters.rico1
|
||||
: beenInCenter
|
||||
? move.parameters.rico4
|
||||
: move.parameters.rico2;
|
||||
? this.move.parameters.rico4
|
||||
: this.move.parameters.rico2;
|
||||
|
||||
if (rico) {
|
||||
const onLeftSide = prevStep.endPosition.which.isLeft();
|
||||
|
@ -224,7 +199,7 @@ class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName
|
|||
kind: PositionKind.Circle,
|
||||
which: CirclePosition.fromSides(prevStep.endPosition.which.leftRightSide(),
|
||||
// TODO might be swapped
|
||||
(endsShoulder === Hand.Left) === onLeftSide ? CircleSide.Top : CircleSide.Bottom),
|
||||
(this.moveInterpreter.endsShoulder === Hand.Left) === onLeftSide ? CircleSide.Top : CircleSide.Bottom),
|
||||
facing: onLeftSide ? Facing.Left : Facing.Right,
|
||||
}
|
||||
}
|
||||
|
@ -252,12 +227,16 @@ class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName
|
|||
}
|
||||
}
|
||||
|
||||
const inCenterFirst = firstPassInCenter && this.findPairOpposite(this.move.parameters.who, id) !== null
|
||||
|| this.move.parameters.who2 && this.findPairOpposite(this.move.parameters.who2, id) !== null;
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
return fixupHeyOtherPath(this.handleMove(({ id, startPos }) => {
|
||||
const endsInCircle = startPos.kind === PositionKind.Circle;
|
||||
|
||||
const inCenterFirst = this.moveInterpreter.firstPassInCenter && (this.findPairOpposite(this.move.parameters.who, id) !== null)
|
||||
|| !!this.move.parameters.who2 && (this.findPairOpposite(this.move.parameters.who2, id) !== null);
|
||||
|
||||
let firstHeyStep: HeyStep;
|
||||
let startingPos: SemanticPosition;
|
||||
if (firstPassInCenter) {
|
||||
if (this.moveInterpreter.firstPassInCenter) {
|
||||
if (startPos.kind !== PositionKind.Circle) {
|
||||
throw new Error("Hey starting in center not from circle is unsupported.");
|
||||
}
|
||||
|
@ -319,18 +298,60 @@ class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName
|
|||
|
||||
const heySteps: HeyStep[] = [firstHeyStep];
|
||||
let beenInCenter = firstHeyStep.kind === "CenterPass" || firstHeyStep.kind === "Ricochet";
|
||||
for (let i = 1; i < heyParts; i++) {
|
||||
const isLast = i === heyParts - 1;
|
||||
const nextHeyStep = continueHey(heySteps[i - 1], heyParts - i - 1, beenInCenter);
|
||||
for (let i = 1; i < this.moveInterpreter.heyParts; i++) {
|
||||
const nextHeyStep = this.continueHey(heySteps[i - 1], this.moveInterpreter.heyParts - i - 1, {beenInCenter, endsInCircle, inCenterFirst});
|
||||
beenInCenter ||= nextHeyStep.kind === "CenterPass" || nextHeyStep.kind === "Ricochet";
|
||||
heySteps.push(nextHeyStep);
|
||||
}
|
||||
return this.combine(heySteps.map(heyStepToPartialLowLevelMove), { ...startingPos, hands: undefined });
|
||||
return this.combine(heySteps.map(s => this.heyStepToPartialLowLevelMove(s)), { ...startingPos, hands: undefined });
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class Hey extends MoveInterpreter<typeof moveName> {
|
||||
public readonly heyParts: number;
|
||||
public readonly heyPartBeats: number;
|
||||
public readonly firstPassInCenter: boolean;
|
||||
public readonly centerShoulder: Hand;
|
||||
public readonly endsShoulder: Hand;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
||||
super(args);
|
||||
|
||||
if (this.move.parameters.dir !== "across") {
|
||||
throw new Error("Unsupported hey direction: " + this.move.parameters.dir);
|
||||
}
|
||||
|
||||
if (typeof this.move.parameters.until === "string") {
|
||||
switch (this.move.parameters.until) {
|
||||
case "half":
|
||||
this.heyParts = 4;
|
||||
break;
|
||||
case "full":
|
||||
this.heyParts = 8;
|
||||
break;
|
||||
// TODO Are these right? Can it sometimes be 1 or 3 instead of 2?
|
||||
case "less than half":
|
||||
this.heyParts = 2;
|
||||
break;
|
||||
case "between half and full":
|
||||
this.heyParts = 6;
|
||||
default:
|
||||
throw new Error("Unsupported hey 'until': " + this.move.parameters.until);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TODO Is this actually this simple?
|
||||
this.heyParts = this.move.parameters.until.time === 1 ? 2 : 6;
|
||||
//throw new Error("Unsupported hey 'until': " + this.move.parameters.until.dancer + " time " + this.move.parameters.until.time);
|
||||
}
|
||||
this.heyPartBeats = this.move.beats / this.heyParts;
|
||||
// TODO is this right?
|
||||
this.firstPassInCenter = dancerIsPair(this.move.parameters.who);
|
||||
this.centerShoulder = this.firstPassInCenter === this.move.parameters.shoulder ? Hand.Right : Hand.Left;
|
||||
this.endsShoulder = this.centerShoulder.opposite();
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new HeySingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SemanticPosition, PositionKind } from "../interpreterCommon.js";
|
||||
import { SemanticPosition, PositionKind, DancerDistance } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
@ -12,6 +12,10 @@ class MadRobinSingleVariant extends SingleVariantMoveInterpreter<MadRobin, typeo
|
|||
}
|
||||
|
||||
return this.handleCircleMove(({ id, startPos }) => {
|
||||
if (startPos.dancerDistance && startPos.dancerDistance !== DancerDistance.Normal) {
|
||||
throw new Error(this.move.move + " can only start in normal DancerDistance.");
|
||||
}
|
||||
|
||||
// Read who of mad robin to decide direction.
|
||||
const madRobinClockwise: boolean = (this.findPairOpposite(this.move.parameters.who, id) !== null) === startPos.which.isOnLeftLookingAcross();
|
||||
|
||||
|
|
|
@ -1,43 +1,71 @@
|
|||
import { HandConnection, HandTo, BalanceWeight } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { HandConnection, HandTo, BalanceWeight, PositionKind, ShortLinesPosition, SemanticPosition } from "../interpreterCommon.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, Variant, VariantCollection, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
const moveName: Move["move"] = "pull by dancers";
|
||||
const moveName = "pull by dancers";
|
||||
|
||||
class PullByDancersSingleVariant extends SingleVariantMoveInterpreter<PullByDancers, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
moveAsVariants(previousMoveVariant: string): VariantCollection {
|
||||
const res = new Map<string, Variant>();
|
||||
|
||||
for (const toShortLines of [true, false]) {
|
||||
try {
|
||||
res.set(toShortLines ? "ToShortLines" : "FullPullBy", {
|
||||
lowLevelMoves: this.moveAsLowLevelMovesTo(toShortLines),
|
||||
previousMoveVariant
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
moveAsLowLevelMovesTo(toShortLines: boolean): LowLevelMovesForAllDancers {
|
||||
// TODO Might make sense to think of pull by as not a full swap?
|
||||
// e.g., in Blue and Green Candles, it's treated as only getting to
|
||||
// ShortLinesPosition.Middle* before doing an allemande.
|
||||
return this.handlePairedMove(this.move.parameters.who, ({ startPos, around, withPos }) => {
|
||||
const hand = this.move.parameters.hand ? Hand.Right : Hand.Left;
|
||||
const balanceBeats = this.move.parameters.bal
|
||||
? this.move.beats > 4
|
||||
? this.move.beats - 4
|
||||
: 2
|
||||
: 0;
|
||||
const balancePartBeats = balanceBeats / 2;
|
||||
const pullBeats = this.move.beats - balanceBeats;
|
||||
if (toShortLines) {
|
||||
if (startPos.kind !== PositionKind.Circle) {
|
||||
throw new Error("ToShortLines variant of " + this.move.move + " only makes sense starting from a circle.");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Adjust facing?
|
||||
const startPosition = {
|
||||
...startPos,
|
||||
hands: new Map<Hand, HandConnection>([
|
||||
[
|
||||
hand,
|
||||
{ hand, to: around === "Center" ? HandTo.DiagonalAcrossCircle : HandTo.DancerForward }
|
||||
]])
|
||||
this.moveInterpreter.hand,
|
||||
{ hand: this.moveInterpreter.hand, to: around === "Center" ? HandTo.DiagonalAcrossCircle : HandTo.DancerForward }
|
||||
]]),
|
||||
facing: startPos.which.facingAcross(),
|
||||
};
|
||||
|
||||
const endPos: SemanticPosition = toShortLines
|
||||
? {
|
||||
...withPos,
|
||||
hands: undefined,
|
||||
facing: startPosition.facing,
|
||||
kind: PositionKind.ShortLines,
|
||||
which: ShortLinesPosition.fromSide(withPos.which.leftRightSide(), "Middle"),
|
||||
longLines: undefined, // Needed to satisfy type-checker.
|
||||
}
|
||||
: {
|
||||
...withPos,
|
||||
hands: undefined,
|
||||
facing: startPosition.facing,
|
||||
};
|
||||
const passBy: PartialLowLevelMove = {
|
||||
beats: pullBeats,
|
||||
endPosition: { ...withPos, facing: startPos.facing },
|
||||
beats: this.moveInterpreter.pullBeats,
|
||||
endPosition: endPos,
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.PassBy,
|
||||
around,
|
||||
side: hand,
|
||||
side: this.moveInterpreter.hand,
|
||||
withHands: true,
|
||||
facing: "Start",
|
||||
otherPath: "Swap",
|
||||
|
@ -47,7 +75,7 @@ class PullByDancersSingleVariant extends SingleVariantMoveInterpreter<PullByDanc
|
|||
if (this.move.parameters.bal) {
|
||||
return this.combine([
|
||||
{
|
||||
beats: balancePartBeats,
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: {
|
||||
...startPosition,
|
||||
balance: BalanceWeight.Forward,
|
||||
|
@ -57,7 +85,7 @@ class PullByDancersSingleVariant extends SingleVariantMoveInterpreter<PullByDanc
|
|||
}
|
||||
},
|
||||
{
|
||||
beats: balancePartBeats,
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: {
|
||||
...startPosition,
|
||||
balance: BalanceWeight.Backward,
|
||||
|
@ -70,11 +98,45 @@ class PullByDancersSingleVariant extends SingleVariantMoveInterpreter<PullByDanc
|
|||
} else {
|
||||
return this.combine([passBy], startPosition);
|
||||
}
|
||||
}, !toShortLines ? undefined : ({startPos}) => {
|
||||
return this.combine([{
|
||||
beats: this.move.beats,
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.Linear,
|
||||
},
|
||||
endPosition: {
|
||||
...startPos,
|
||||
kind: PositionKind.ShortLines,
|
||||
which: ShortLinesPosition.fromSide(startPos.which.leftRightSide(), "Far"),
|
||||
facing: startPos.which.facingAcross(),
|
||||
hands: undefined,
|
||||
longLines: undefined,
|
||||
}
|
||||
}], startPos);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PullByDancers extends MoveInterpreter<typeof moveName> {
|
||||
public readonly hand: Hand;
|
||||
public readonly balancePartBeats: number;
|
||||
public readonly pullBeats: number;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
||||
super(args);
|
||||
|
||||
this.hand = this.move.parameters.hand ? Hand.Right : Hand.Left;
|
||||
const balanceBeats = this.move.parameters.bal
|
||||
? this.move.beats > 4
|
||||
? this.move.beats - 4
|
||||
: this.move.beats > 2
|
||||
? 2
|
||||
: this.move.beats / 2
|
||||
: 0;
|
||||
this.balancePartBeats = balanceBeats / 2;
|
||||
this.pullBeats = this.move.beats - balanceBeats;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new PullByDancersSingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ const moveName: Move["move"] = "roll away";
|
|||
class RollAwaySingleVariant extends SingleVariantMoveInterpreter<RollAway, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
// TODO maybe can roll away in short lines?
|
||||
return this.handleCirclePairedMove(this.move.parameters.who, ({ id, startPos, withPos }) => {
|
||||
return this.handleCirclePairedMove(this.move.parameters.whom, ({ id, startPos, withPos }) => {
|
||||
let isRoller: boolean;
|
||||
switch (this.move.parameters.who) {
|
||||
case "gentlespoons":
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
import { PositionKind, SemanticPosition, Facing, BalanceWeight, handsInLine } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { PositionKind, SemanticPosition, Facing, BalanceWeight, handsInLine, CircleSide, handsInLongLines } from "../interpreterCommon.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
const moveName: Move["move"] = "Rory O'More";
|
||||
const moveName = "Rory O'More";
|
||||
class RoryOMoreSingleVariant extends SingleVariantMoveInterpreter<RoryOMore, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
if (this.move.parameters.who !== "everyone") {
|
||||
throw new Error(this.move.move + " that doesn't include everyone is unsupported.");
|
||||
}
|
||||
|
||||
// TODO Could be in long or short lines.
|
||||
const roryDir = this.move.parameters.slide ? Hand.Left : Hand.Right;
|
||||
const balBeats = this.move.parameters.bal ? this.move.beats / 2 : 0;
|
||||
const balPartBeats = balBeats / 2;
|
||||
const roryBeats = this.move.beats - balBeats;
|
||||
return this.handleMove(({ startPos }) => {
|
||||
const isShortLines: boolean = startPos.kind === PositionKind.ShortLines;
|
||||
|
||||
const startingPos: SemanticPosition = {
|
||||
...startPos,
|
||||
hands: handsInLine({ wavy: true, which: startPos.which, facing: startPos.facing })
|
||||
|
@ -27,38 +17,61 @@ class RoryOMoreSingleVariant extends SingleVariantMoveInterpreter<RoryOMore, typ
|
|||
let endPos: SemanticPosition;
|
||||
if (startPos.kind === PositionKind.ShortLines) {
|
||||
if (startPos.facing !== Facing.Up && startPos.facing !== Facing.Down) {
|
||||
throw new Error("To be in wavy lines, must be facing up or down, not " + startPos.facing);
|
||||
throw new Error("To be in short wavy lines, must be facing up or down, not " + startPos.facing);
|
||||
}
|
||||
const { newPos: endWhich, wrap } = startPos.which.shiftWithWrap(this.moveInterpreter.dir, startPos.facing);
|
||||
const endLineOffset = !wrap
|
||||
? startPos.lineOffset
|
||||
: (startPos.lineOffset ?? 0) + wrap;
|
||||
if (wrap) {
|
||||
startingPos.hands = handsInLongLines(true)
|
||||
}
|
||||
const endWhich = startPos.which.shift(roryDir, startPos.facing);
|
||||
endPos = {
|
||||
...startPos,
|
||||
which: endWhich,
|
||||
hands: handsInLine({ wavy: true, which: endWhich, facing: startPos.facing })
|
||||
hands: handsInLine({ wavy: true, which: endWhich, facing: startPos.facing }),
|
||||
lineOffset: endLineOffset,
|
||||
};
|
||||
} else {
|
||||
throw new Error(this.move.move + " is currently only supported in short lines.");
|
||||
if (startPos.facing !== Facing.Left && startPos.facing !== Facing.Right) {
|
||||
throw new Error("To be in long wavy lines, must be facing left or right, not " + startPos.facing);
|
||||
}
|
||||
const endWhich = startPos.which.swapUpAndDown();
|
||||
const sideTowards = (startPos.facing === Facing.Right)
|
||||
=== (this.moveInterpreter.dir === Hand.Left)
|
||||
? CircleSide.Bottom
|
||||
: CircleSide.Top;
|
||||
const endSetOffset = sideTowards === endWhich.topBottomSide()
|
||||
? startPos.setOffset
|
||||
: (startPos.setOffset ?? 0) + (sideTowards === CircleSide.Bottom ? +1 : -1);
|
||||
endPos = {
|
||||
...startPos,
|
||||
which: endWhich,
|
||||
hands: handsInLine({ wavy: true, which: endWhich, facing: startPos.facing }),
|
||||
setOffset: endSetOffset,
|
||||
};
|
||||
}
|
||||
|
||||
const maybeBalance: PartialLowLevelMove[] = (this.move.parameters.bal ? [
|
||||
{
|
||||
beats: balPartBeats,
|
||||
endPosition: { ...startingPos, balance: roryDir === Hand.Left ? BalanceWeight.Left : BalanceWeight.Right },
|
||||
beats: this.moveInterpreter.balPartBeats,
|
||||
endPosition: { ...startingPos, balance: this.moveInterpreter.dir === Hand.Left ? BalanceWeight.Left : BalanceWeight.Right },
|
||||
movementPattern: { kind: SemanticAnimationKind.Linear },
|
||||
},
|
||||
{
|
||||
beats: balPartBeats,
|
||||
endPosition: { ...startingPos, balance: roryDir === Hand.Left ? BalanceWeight.Right : BalanceWeight.Left },
|
||||
beats: this.moveInterpreter.balPartBeats,
|
||||
endPosition: { ...startingPos, balance: this.moveInterpreter.dir === Hand.Left ? BalanceWeight.Right : BalanceWeight.Left },
|
||||
movementPattern: { kind: SemanticAnimationKind.Linear },
|
||||
},
|
||||
] : []);
|
||||
|
||||
return this.combine([...maybeBalance,
|
||||
{
|
||||
beats: roryBeats,
|
||||
beats: this.moveInterpreter.roryBeats,
|
||||
endPosition: endPos,
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.Linear,
|
||||
minRotation: roryDir === Hand.Right ? +360 : -360,
|
||||
minRotation: this.moveInterpreter.dir === Hand.Right ? +360 : -360,
|
||||
handsDuring: "None",
|
||||
},
|
||||
}
|
||||
|
@ -68,6 +81,23 @@ class RoryOMoreSingleVariant extends SingleVariantMoveInterpreter<RoryOMore, typ
|
|||
}
|
||||
|
||||
class RoryOMore extends MoveInterpreter<typeof moveName> {
|
||||
public readonly dir: Hand;
|
||||
public readonly balPartBeats: number;
|
||||
public readonly roryBeats: number;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
||||
super(args);
|
||||
|
||||
if (this.move.parameters.who !== "everyone") {
|
||||
throw new Error(this.move.move + " that doesn't include everyone is unsupported.");
|
||||
}
|
||||
|
||||
this.dir = this.move.parameters.slide ? Hand.Left : Hand.Right;
|
||||
const balBeats = this.move.parameters.bal ? this.move.beats / 2 : 0;
|
||||
this.balPartBeats = balBeats / 2;
|
||||
this.roryBeats = this.move.beats - balBeats;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new RoryOMoreSingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import { DancerDistance, handToDancerToSideInCircleFacingAcross } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
const moveName: Move["move"] = "star promenade";
|
||||
const moveName = "star promenade";
|
||||
|
||||
class StarPromenadeSingleVariant extends SingleVariantMoveInterpreter<StarPromenade, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
const starPromenadeHand = this.move.parameters.hand ? Hand.Right : Hand.Left;
|
||||
const starPromenadeSwap = (this.move.parameters.circling % 360) === 180;
|
||||
if (!starPromenadeSwap && (this.move.parameters.circling % 360 !== 0)) {
|
||||
throw new Error(this.move.move + " circling by not a multiple of 180 degrees is unsupported.");
|
||||
}
|
||||
|
||||
// TODO start promenade hands/show dancers close
|
||||
return this.handleCircleMove(({ id, startPos }) => {
|
||||
const inCenter = this.findPairOpposite(this.move.parameters.who, id) !== null;
|
||||
// TODO Actually, does star promenade end facing out and butterfly whirl swaps?
|
||||
const endWhich = starPromenadeSwap ? startPos.which.swapDiagonal() : startPos.which;
|
||||
const endWhich = this.moveInterpreter.swap ? startPos.which.swapDiagonal() : startPos.which;
|
||||
const endFacing = endWhich.facingAcross();
|
||||
return this.combine([{
|
||||
beats: this.move.beats,
|
||||
|
@ -29,11 +22,13 @@ class StarPromenadeSingleVariant extends SingleVariantMoveInterpreter<StarPromen
|
|||
dancerDistance: DancerDistance.Compact,
|
||||
// TODO Perhaps different hands indication for "scooped"?
|
||||
hands: handToDancerToSideInCircleFacingAcross(endWhich),
|
||||
longLines: undefined,
|
||||
balance: undefined,
|
||||
},
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.RotateAround,
|
||||
around: "Center",
|
||||
byHand: inCenter ? starPromenadeHand : undefined,
|
||||
byHand: inCenter ? this.moveInterpreter.hand : undefined,
|
||||
close: inCenter,
|
||||
minAmount: this.move.parameters.hand ? this.move.parameters.circling : -this.move.parameters.circling,
|
||||
}
|
||||
|
@ -43,6 +38,23 @@ class StarPromenadeSingleVariant extends SingleVariantMoveInterpreter<StarPromen
|
|||
}
|
||||
|
||||
class StarPromenade extends MoveInterpreter<typeof moveName> {
|
||||
public readonly hand: Hand;
|
||||
public readonly swap: boolean;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
||||
super(args);
|
||||
|
||||
this.hand = this.move.parameters.hand ? Hand.Right : Hand.Left;
|
||||
this.swap = (this.move.parameters.circling % 360) === 180;
|
||||
if (!this.swap && (this.move.parameters.circling % 360 !== 0)) {
|
||||
throw new Error(this.move.move + " circling by not a multiple of 180 degrees is unsupported.");
|
||||
}
|
||||
}
|
||||
|
||||
override allowStartingLongLines(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new StarPromenadeSingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -1,39 +1,77 @@
|
|||
import { CoupleRole, DanceRole } from "../danceCommon.js";
|
||||
import { LongLines, CircleSide, SemanticPosition, CirclePosition, Facing, PositionKind, HandConnection, HandTo, ShortLinesPosition, handsInLine, BalanceWeight, DancerDistance } from "../interpreterCommon.js";
|
||||
import { Move } from "../libfigureMapper.js";
|
||||
import { LongLines, CircleSide, SemanticPosition, CirclePosition, Facing, PositionKind, HandConnection, HandTo, ShortLinesPosition, handsInLine, BalanceWeight, DancerDistance, isFacingUpOrDown, isLeftRightCircleSide } from "../interpreterCommon.js";
|
||||
import { SemanticAnimationKind } from "../lowLevelMove.js";
|
||||
import { Hand } from "../rendererConstants.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
||||
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, Variant, VariantCollection, moveInterpreters } from "./_moveInterpreter.js";
|
||||
|
||||
const moveName: Move["move"] = "swing";
|
||||
const moveName = "swing";
|
||||
|
||||
/* Possible swings:
|
||||
* - default: start on left/right side, end facing across
|
||||
* - give and take: start in one corner, end facing across
|
||||
* - contra corners: just one pair, start on top/bottom side, end facing in to new circle
|
||||
* - to short lines: start on left/right side, end facing up/down in short lines
|
||||
* - same role: larks/robins go into center, end facing out
|
||||
* - join short lines: just one pair in center, joins hands in short lines after
|
||||
* - ... ?
|
||||
*/
|
||||
interface SwingEnd {
|
||||
facing: "Across" | Facing.Up | Facing.Down,
|
||||
close: boolean,
|
||||
}
|
||||
function swingEndString(swingEnd: SwingEnd): string {
|
||||
return swingEnd.facing + (swingEnd.close ? "Close" : "");
|
||||
}
|
||||
const swingEndValues: SwingEnd[] = [
|
||||
{ facing: "Across", close: false},
|
||||
{ facing: "Across", close: true},
|
||||
{ facing: Facing.Up, close: false},
|
||||
{ facing: Facing.Up, close: true},
|
||||
{ facing: Facing.Down, close: false},
|
||||
{ facing: Facing.Down, close: true},
|
||||
];
|
||||
|
||||
class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof moveName> {
|
||||
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
||||
// TODO Use variants instead of nextMove
|
||||
const nextMove = this.moveInterpreter.nextMove;
|
||||
moveAsVariants(previousMoveVariant: string): VariantCollection {
|
||||
const res = new Map<string, Variant>();
|
||||
|
||||
for (const swingEnd of swingEndValues) {
|
||||
try {
|
||||
res.set(swingEndString(swingEnd), {
|
||||
lowLevelMoves: this.moveAsLowLevelMovesFacing(swingEnd),
|
||||
previousMoveVariant
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
moveAsLowLevelMovesFacing(swingEnd: SwingEnd): LowLevelMovesForAllDancers {
|
||||
return this.handlePairedMove(this.move.parameters.who, ({ id, startPos, around, withId, withPos }) => {
|
||||
// TODO swing can start from non-circle positions.
|
||||
// TODO swing end is only in notes / looking at next move.
|
||||
if (startPos.longLines && startPos.longLines !== LongLines.Near) {
|
||||
throw new Error(this.move.move + " shouldn't start with long lines " + startPos.longLines);
|
||||
}
|
||||
|
||||
// TODO better way to detect swing end?
|
||||
// TODO more structured way to do this than enumerating next moves here?
|
||||
// maybe instead of nextMove an optional endPosition for fixing up positions?
|
||||
// ... but then every move would have to handle that...
|
||||
const afterTake = startPos.longLines === LongLines.Near || withPos.longLines === LongLines.Near;
|
||||
const toShortLines = nextMove.move === "down the hall" || nextMove.move === "up the hall";
|
||||
const endFacingAcross = (around === CircleSide.Left || around === CircleSide.Right) && !toShortLines;
|
||||
|
||||
if (around === "Center" && swingEnd.facing === "Across") {
|
||||
throw new Error("Cannot end a swing around the center facing across.");
|
||||
}
|
||||
|
||||
const toShortLines = around === "Center" || (isLeftRightCircleSide(around) && isFacingUpOrDown(swingEnd.facing));
|
||||
|
||||
const startWhich = startPos.which;
|
||||
const startPosition: SemanticPosition = {
|
||||
...startPos,
|
||||
facing: around === CircleSide.Left || CircleSide.Right
|
||||
facing: isLeftRightCircleSide(around)
|
||||
? (startWhich instanceof CirclePosition
|
||||
? afterTake
|
||||
? startPos.longLines === LongLines.Near ? startWhich.facingOut() : startWhich.facingAcross()
|
||||
: (startWhich.topBottomSide() === CircleSide.Bottom ? Facing.Up : Facing.Down)
|
||||
: startWhich.facingUpOrDown()
|
||||
: startWhich.facingSide())
|
||||
: (startWhich.isLeft() ? Facing.Right : Facing.Left),
|
||||
: startWhich.facingAcross(),
|
||||
};
|
||||
|
||||
const swingRole = id.danceRole != withId.setIdentity.danceRole
|
||||
|
@ -47,71 +85,43 @@ class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof move
|
|||
? (withId.relativeLine > 0 ? DanceRole.Lark : DanceRole.Robin)
|
||||
: /* should be unreachable as this means withId is equal to id */ DanceRole.Lark;
|
||||
|
||||
// TODO This assumes swing around right/left, not center or top/bottom.
|
||||
let endPosition: SemanticPosition;
|
||||
if (endFacingAcross) {
|
||||
endPosition = {
|
||||
...startPos,
|
||||
kind: PositionKind.Circle,
|
||||
which: startWhich instanceof CirclePosition
|
||||
? (startWhich.isOnLeftLookingAcross() === (swingRole === DanceRole.Lark)
|
||||
? startWhich
|
||||
: startWhich.swapUpAndDown())
|
||||
: (startWhich.isLeft()
|
||||
? (swingRole === DanceRole.Lark ? CirclePosition.BottomLeft : CirclePosition.TopLeft)
|
||||
: (swingRole === DanceRole.Lark ? CirclePosition.TopRight : CirclePosition.BottomRight)),
|
||||
facing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
|
||||
balance: undefined,
|
||||
dancerDistance: undefined,
|
||||
longLines: undefined,
|
||||
hands: new Map<Hand, HandConnection>([swingRole === DanceRole.Lark
|
||||
const endFacing = swingEnd.facing === "Across" ? startWhich.facingAcross() : swingEnd.facing;
|
||||
const endWhich = toShortLines && /*always true, just for type-checker*/isFacingUpOrDown(swingEnd.facing)
|
||||
? ShortLinesPosition.fromSwing(around, swingRole, swingEnd.facing)
|
||||
: CirclePosition.fromSwing(/*always true, just for type-checker*/<CircleSide>around, swingRole, swingEnd.facing)
|
||||
|
||||
const endHands = toShortLines
|
||||
? handsInLine({ wavy: false, which: endWhich, facing: endFacing })
|
||||
: new Map<Hand, HandConnection>([swingRole === DanceRole.Lark
|
||||
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
|
||||
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
|
||||
};
|
||||
} else if (toShortLines) {
|
||||
const endFacing = nextMove.move === "down the hall" !== (nextMove.parameters.facing === "backward")
|
||||
? Facing.Down
|
||||
: Facing.Up;
|
||||
const endWhich = startWhich.isLeft()
|
||||
? ((endFacing === Facing.Down) === (swingRole === DanceRole.Lark) ? ShortLinesPosition.FarLeft : ShortLinesPosition.MiddleLeft)
|
||||
: ((endFacing === Facing.Down) === (swingRole === DanceRole.Lark) ? ShortLinesPosition.MiddleRight : ShortLinesPosition.FarRight)
|
||||
endPosition = {
|
||||
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]);
|
||||
|
||||
const partialEndPosition = {
|
||||
...startPos,
|
||||
kind: PositionKind.ShortLines,
|
||||
which: endWhich,
|
||||
facing: endFacing,
|
||||
balance: undefined,
|
||||
dancerDistance: undefined,
|
||||
dancerDistance: swingEnd.close ? DancerDistance.Compact : undefined,
|
||||
longLines: undefined,
|
||||
hands: handsInLine({ wavy: false, which: endWhich, facing: endFacing }),
|
||||
};
|
||||
hands: endHands,
|
||||
}
|
||||
else {
|
||||
// TODO Need to figure out the logic of knowing if this should be facing up or down.
|
||||
// Probably based on knowing Ones vs. Twos? Also then the not-participating-dancers need their
|
||||
// "standing still" to update that they are in a new set...
|
||||
//throw new Error("Swing to new circle currently unsupported.");
|
||||
endPosition = {
|
||||
// end not facing across or in short lines, so transitioning to new circle like in many contra corners dances.
|
||||
...startPos,
|
||||
};
|
||||
let endPosition: SemanticPosition = toShortLines ? {
|
||||
...partialEndPosition,
|
||||
kind: PositionKind.ShortLines,
|
||||
which: <ShortLinesPosition>endWhich,
|
||||
} : {
|
||||
...partialEndPosition,
|
||||
kind: PositionKind.Circle,
|
||||
which: <CirclePosition>endWhich,
|
||||
}
|
||||
|
||||
const swingBeats = this.move.parameters.prefix === "none" ? this.move.beats
|
||||
: this.move.parameters.prefix === "balance"
|
||||
? this.move.beats > 8 ? 8 : this.move.beats - 4
|
||||
: this.move.parameters.prefix === "meltdown"
|
||||
? this.move.beats - 4
|
||||
: (() => { throw "Unknown swing prefix: " + this.move.parameters.prefix })();
|
||||
|
||||
const swing: PartialLowLevelMove = {
|
||||
beats: swingBeats,
|
||||
beats: this.moveInterpreter.swingBeats,
|
||||
endPosition: endPosition,
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.Swing,
|
||||
minAmount: 360,
|
||||
around,
|
||||
endFacing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
|
||||
endFacing,
|
||||
swingRole,
|
||||
afterTake,
|
||||
},
|
||||
|
@ -121,8 +131,6 @@ class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof move
|
|||
case "none":
|
||||
return this.combine([swing,], startPosition);
|
||||
case "balance":
|
||||
// TODO Right length for balance?
|
||||
const balancePartBeats = this.move.beats > 8 ? (this.move.beats - 8) / 2 : 2;
|
||||
const startWithBalHands = {
|
||||
...startPosition,
|
||||
hands: new Map<Hand, HandConnection>([
|
||||
|
@ -142,13 +150,13 @@ class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof move
|
|||
};
|
||||
return this.combine([
|
||||
{
|
||||
beats: balancePartBeats,
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
startPosition: startWithBalHands,
|
||||
endPosition: balForwardPos,
|
||||
movementPattern: { kind: SemanticAnimationKind.Linear, },
|
||||
},
|
||||
prevEnd => ({
|
||||
beats: balancePartBeats,
|
||||
beats: this.moveInterpreter.balancePartBeats,
|
||||
endPosition: {
|
||||
...prevEnd,
|
||||
balance: BalanceWeight.Backward,
|
||||
|
@ -158,10 +166,9 @@ class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof move
|
|||
swing,
|
||||
], startPosition);
|
||||
case "meltdown":
|
||||
const meltdownBeats = 4; // TODO right number here?
|
||||
return this.combine([
|
||||
prevEnd => ({
|
||||
beats: meltdownBeats,
|
||||
beats: this.moveInterpreter.meltdownBeats,
|
||||
endPosition: { ...prevEnd, dancerDistance: DancerDistance.Compact },
|
||||
movementPattern: {
|
||||
kind: SemanticAnimationKind.RotateAround,
|
||||
|
@ -179,6 +186,53 @@ class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof move
|
|||
}
|
||||
|
||||
class Swing extends MoveInterpreter<typeof moveName> {
|
||||
public readonly swingBeats: number;
|
||||
public readonly balancePartBeats: number;
|
||||
public readonly meltdownBeats: number;
|
||||
|
||||
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
||||
super(args);
|
||||
|
||||
switch(this.move.parameters.prefix) {
|
||||
case "none":
|
||||
this.balancePartBeats = 0;
|
||||
this.meltdownBeats = 0;
|
||||
this.swingBeats = this.move.beats;
|
||||
break;
|
||||
case "balance":
|
||||
this.meltdownBeats = 0;
|
||||
this.balancePartBeats = this.move.beats > 8
|
||||
? (this.move.beats - 8) / 2
|
||||
: this.move.beats > 4
|
||||
? 2
|
||||
: this.move.beats / 4;
|
||||
this.swingBeats = this.move.beats - this.balancePartBeats * 2;
|
||||
break;
|
||||
case "meltdown":
|
||||
this.balancePartBeats = 0;
|
||||
this.meltdownBeats = this.move.beats >= 8 ? 4 : this.move.beats / 2;
|
||||
this.swingBeats = this.move.beats - this.meltdownBeats;
|
||||
break;
|
||||
default:
|
||||
throw new Error ("Unknown swing prefix: " + this.move.parameters.prefix);
|
||||
}
|
||||
this.swingBeats = this.move.parameters.prefix === "none" ? this.move.beats
|
||||
: this.move.parameters.prefix === "balance"
|
||||
? this.move.beats > 8 ? 8 : this.move.beats - 4
|
||||
: this.move.parameters.prefix === "meltdown"
|
||||
? this.move.beats - 4
|
||||
: (() => { throw "Unknown swing prefix: " + this.move.parameters.prefix })();
|
||||
}
|
||||
|
||||
override allowStartingClose(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override allowStartingLongLines(): boolean {
|
||||
// Long lines is used for LongLines.Near after give and take.
|
||||
return true;
|
||||
}
|
||||
|
||||
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
||||
return new SwingSingleVariant(this, startingPos);
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ export class Renderer {
|
|||
animation?: Animation;
|
||||
extraSets?: number;
|
||||
extraLines?: number;
|
||||
trailIncrements: number = 6;
|
||||
trailLengthInBeats: number = 1;
|
||||
drawDebug: boolean = false;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
|
||||
|
@ -170,16 +172,15 @@ export class Renderer {
|
|||
if (!this.animation) throw new Error("Attempted to render before setting animation.");
|
||||
|
||||
this.clear();
|
||||
const increments = 7;
|
||||
const trailLengthInBeats = 1;
|
||||
const incrementLength = trailLengthInBeats / increments;
|
||||
const increments = this.trailLengthInBeats > 0 && this.trailIncrements > 0 ? this.trailIncrements : 0;
|
||||
const incrementLength = this.trailLengthInBeats / (increments + 1);
|
||||
progression ??= 0;
|
||||
const offsetSets = this.animation.progression.y === 0
|
||||
? 0
|
||||
: -((progression - (progression % 2)) / 2) / ((this.animation.progression.y * 2) / setDistance);
|
||||
for (var i = increments; i >= 0; i--) {
|
||||
const beatToDraw = beat - i*incrementLength;
|
||||
this.ctx.globalAlpha = i == 0 ? 1 : (1 - i/increments)*0.3;
|
||||
const beatToDraw = i == 0 ? beat : beat - i*incrementLength;
|
||||
this.ctx.globalAlpha = i == 0 ? 1 : (1 - i / (increments + 1)) * 0.3;
|
||||
const positions = this.animation.positionsAtBeat(beatToDraw, progression % 2);
|
||||
if (this.drawDebug) this.drawSets(positions, offsetSets, true, true);
|
||||
this.drawSets(positions, offsetSets, i === 0, false);
|
||||
|
|
Loading…
Reference in New Issue
Block a user