Compare commits

..

42 Commits

Author SHA1 Message Date
6bf56a8c05 Add development instructions and scripts to README. 2024-07-01 14:43:39 -07:00
2e4485809f Add user settings for adjusting trail display. 2024-06-16 20:48:00 -07:00
f971e52736 Use background pattern in debug information to remind lark vs. robins by shape. 2023-11-24 16:51:33 -08:00
b16267b9dc Fix debug information table to match new color scheme. 2023-11-24 16:30:52 -08:00
b683803ab7 Merge branch 'exp/dancer-colors' to rework colors and dancer shapes. 2023-11-24 16:17:19 -08:00
15937c55f7 Cleanup/remove dead code. 2023-11-24 16:15:38 -08:00
6f2259faf2 Swap lark and robin shapes so lark is dome and robin is triangle. 2023-11-24 16:09:21 -08:00
82bc463859 Fix dancer label colors to not be almost white on white. 2023-11-12 20:10:36 -08:00
6ace619ec1 [WIP] Trying out different ways to make dancers visually distinctive.
* Different shapes for larks/robins: robins are now domes, larks still
   triangles.
 * Related: partners the same color (might try very close colors?)
 * Attempt to have a color scheme so Ones are warm colors and Twos are
   cool colors to make neighbor vs. shadow interactions more clear at a
   glance.
2023-11-12 19:57:22 -08:00
9fbf7d18ac [WIP] Refactor to split interpreter into one file per move.
Currently just copied over the existing code and applied the quick
fixes to get it to compile. Each move should be refactored to be handle
its parameters earlier where applicable. But variants support should
probably be added first so both refactors can happen together.
2023-10-15 05:25:06 -07:00
5b88361239 [WIP] Starting to split interpreter.ts into separate files. 2023-10-15 03:51:11 -07:00
d2b3e3a826 [WIP] Add comment on limitation of 'pull by dancers'. 2023-10-14 16:23:50 -07:00
d8e7fbe12d Support allemande from long lines to short lines. 2023-10-14 16:11:29 -07:00
8761e409b2 For pull/pass by, spin in direction shoulder used. 2023-10-14 16:10:47 -07:00
4f64f045d3 Add 'Pass By'. 2023-10-14 15:43:43 -07:00
e1ca99ba51 Fix mad robin not in initial set. 2023-10-14 15:43:21 -07:00
ef137dc998 Special-case notes to correctly interpret Van Is My Middle Name. 2023-10-14 15:25:56 -07:00
1caab5d112 Initial support for ricochet in hey. 2023-10-14 15:08:41 -07:00
0bb4e2c051 Fix CirclePosition.fromSides() 2023-10-14 15:08:15 -07:00
987270e073 Support pass through across set. 2023-10-14 12:57:37 -07:00
951073dbe1 Make turn as a couple in short lines render better. 2023-10-14 12:57:13 -07:00
58149ab195 Fix rotation on bend the line after up the hall. 2023-10-14 12:56:17 -07:00
ad14e4e51f Make swing after take look less weird. 2023-10-14 01:50:22 -07:00
10d67df9cc Basic debug display for transition animation. 2023-10-14 01:50:03 -07:00
69c211c858 Fix some weird rotations. 2023-10-14 01:05:50 -07:00
9a7c1d5ea8 Handle allemande 3/4 in One Fish, Two Fish, Right and Left Through Fish. 2023-10-14 00:53:05 -07:00
61badb6404 Support allemande from short to line lines. 2023-10-14 00:24:30 -07:00
e02b8b2e76 Initial support for pass through to ocean wave. 2023-10-13 04:28:17 -07:00
950e828e44 Adjustments to star and progression logic. 2023-10-13 04:06:14 -07:00
afd54e23d1 Animation for star. 2023-10-13 03:39:19 -07:00
b487c1f17f Hardcode parsing preamble for start formation for Binary Stars. 2023-10-12 04:09:30 -07:00
17a7947a41 Animation for promenade across. 2023-10-12 03:55:54 -07:00
08dbddb131 Fix allemande starting hands. 2023-10-11 21:03:27 -07:00
35e42079f8 Simplify swing facing logic. Make swing after take not look as weird... transition is still weird. 2023-10-11 20:41:55 -07:00
a6e1f69730 Add debug display of start/end of rotations. 2023-10-11 20:26:18 -07:00
eb98288b4b Adjust do nothing for paired move. 2023-10-11 20:16:35 -07:00
5bfdd56c3b Fix do si do 360 degrees. 2023-10-10 05:14:36 -07:00
ad1ba9c659 Adjust 'idle' logic to not have hands and allow rotations. 2023-10-10 04:42:54 -07:00
2704d3bf4d Fix rotations starting on side from short lines. 2023-10-10 04:26:44 -07:00
5d24ffa950 Support Rory O'More in short lines. 2023-10-10 04:21:04 -07:00
d94e599c91 Initial support for give and take. 2023-10-10 03:08:28 -07:00
3b765838d4 Fix current move in moves list for last beat. 2023-10-10 03:05:25 -07:00
47 changed files with 3639 additions and 2175 deletions

4
.gitignore vendored
View File

@ -1,2 +1,2 @@
www/js/*.js
www/js/*.js.map
www/js/**/*.js
www/js/**/*.js.map

View File

@ -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
View 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
View File

@ -0,0 +1,7 @@
#!/bin/sh
# Change directory to repo root.
cd "$(dirname "$0")" || exit
echo "Watching $(pwd)"
# From /
tsc --watch

View File

@ -58,29 +58,58 @@
position: sticky;
top: 0;
}
th.Ones.Lark {
background-color: hsl(0, 80%, 50%);
th.Ones {
background-color: hsl(27, 99%, 59%);
}
td.Ones.Lark {
background-color: hsl(0, 90%, 70%);
td.Ones {
background-color: hsl(27, 99%, 85%);
}
th.Ones.Robin {
background-color: hsl(39, 80%, 50%);
th.Twos {
background-color: hsl(249, 42%, 57%);
}
td.Ones.Robin {
background-color: hsl(39, 90%, 70%);
td.Twos {
background-color: hsl(249, 52%, 85%);
}
th.Twos.Lark {
background-color: hsl(240, 70%, 65%);
th.Lark::before {
content: "◠";
text-decoration: line-through;
padding-right: 1ex;
}
td.Twos.Lark {
background-color: hsl(240, 90%, 80%);
th.Robin::before {
content: "△";
padding-right: 1ex;
}
th.Twos.Robin {
background-color: hsl(180, 80%, 50%);
td.Lark.Ones {
background:
radial-gradient(circle at bottom left, hsl(27, 99%, 75%) 15%, transparent 16%),
radial-gradient(circle at bottom right, hsl(27, 99%, 75%) 15%, transparent 16%),
hsl(27, 99%, 85%);
background-size: 6em 3em;
}
td.Twos.Robin {
background-color: hsl(180, 90%, 70%);
td.Lark.Twos {
background:
radial-gradient(circle at bottom left, hsl(249, 42%, 75%) 15%, transparent 16%),
radial-gradient(circle at bottom right,hsl(249, 42%, 75%) 15%, transparent 16%),
hsl(249, 52%, 85%);
background-size: 6em 3em;
}
td.Robin.Ones {
background:
linear-gradient(45deg,hsl(27, 99%, 75%) 10%, transparent 10%),
linear-gradient(135deg, transparent 90%,hsl(27, 99%, 75%) 90%),
hsl(27, 99%, 85%);
background-size: 6em 3em;
}
td.Robin.Twos {
background:
linear-gradient(45deg,hsl(249, 42%, 75%) 10%, transparent 10%),
linear-gradient(135deg, transparent 90%,hsl(249, 42%, 75%) 90%),
hsl(249, 52%, 85%);
background-size: 6em 3em;
}
.move {

View File

@ -98,6 +98,8 @@ export class LinearAnimationSegment extends AnimationSegment {
export interface AnimationTransitionFlags {
rotation?: boolean;
rotationDuring?: "Actual" | "Start" | "End";
rotationDirection?: Hand;
hands?: boolean;
handsDuring?: "Actual" | "None" | "Start" | "End" | Map<Hand, Offset>;
}
@ -125,20 +127,30 @@ export class TransitionAnimationSegment extends AnimationSegment {
this.startRotation = this.startPosition.rotation;
this.endRotation = this.endPosition.rotation;
if (this.flags.rotation) {
let rotationDirection = flags.rotationDirection;
if (!flags.rotationDirection) {
const actualStart = this.actualAnimation.interpolateRotation(0);
const actualEnd = this.actualAnimation.interpolateRotation(1);
if (actualEnd > actualStart) {
rotationDirection = Hand.Right;
} else if (actualEnd < actualStart) {
rotationDirection = Hand.Left;
}
}
const transitionStart = this.actualAnimation.interpolateRotation(this.startTransitionProgress);
const transitionEnd = this.actualAnimation.interpolateRotation(1 - this.endTransitionProgress);
if (actualEnd > actualStart) {
if (rotationDirection === Hand.Right) {
while (transitionStart <= this.startRotation - 180) {
this.startRotation -= 360;
}
while (transitionEnd >= this.endRotation + 180) {
this.endRotation += 360;
}
} else if (actualEnd < actualStart) {
} else if (rotationDirection === Hand.Left) {
while (transitionStart >= this.startRotation + 180) {
this.startRotation += 360;
}
@ -147,7 +159,9 @@ export class TransitionAnimationSegment extends AnimationSegment {
}
}
// Transitions should be short adjustments, not spins.
if (!this.flags.rotationDirection) {
// Transitions should be short adjustments, not spins...
// ... unless a direction is explicitly specified.
while (transitionStart - this.startRotation < -180) {
this.startRotation -= 360;
}
@ -162,12 +176,17 @@ export class TransitionAnimationSegment extends AnimationSegment {
}
}
}
}
override interpolateOffset(progress: number): Offset {
return this.actualAnimation.interpolateOffset(progress);
}
override interpolateRotation(progress: number): number {
switch (this.flags.rotationDuring) {
case undefined:
case "Actual":
const actualRotation = this.actualAnimation.interpolateRotation(progress);
if (this.flags.rotation) {
@ -179,6 +198,21 @@ export class TransitionAnimationSegment extends AnimationSegment {
}
return actualRotation;
case "Start":
if ((1 - progress) < this.endTransitionProgress) {
return interpolateLinear((1 - progress) / this.endTransitionProgress, this.endRotation, this.startRotation);
} else {
return this.startRotation;
}
case "End":
if (progress < this.startTransitionProgress) {
return interpolateLinear(progress / this.startTransitionProgress, this.startRotation, this.endRotation);
} else {
return this.endRotation;
}
}
}
override interpolateHandOffset(progress: number, hand: Hand): Offset | undefined {
@ -218,7 +252,21 @@ export class TransitionAnimationSegment extends AnimationSegment {
}
override drawDebug(ctx: CanvasRenderingContext2D, progress: number) {
// TODO display transition somehow?
// TODO better way to display transition?
if (progress < this.startTransitionProgress) {
ctx.beginPath();
ctx.moveTo(this.startPosition.position.x, this.startPosition.position.y);
const transitionStart = this.actualAnimation.interpolateOffset(this.startTransitionProgress);
ctx.lineTo(transitionStart.x, transitionStart.y);
ctx.stroke();
} else if (progress > 1 - this.endTransitionProgress) {
ctx.beginPath();
const transitionEnd = this.actualAnimation.interpolateOffset(this.endTransitionProgress);
ctx.moveTo(transitionEnd.x, transitionEnd.y);
ctx.lineTo(this.endPosition.position.x, this.endPosition.position.y);
ctx.stroke();
}
this.actualAnimation.drawDebug(ctx, progress);
}
}
@ -227,7 +275,8 @@ export class TransitionAnimationSegment extends AnimationSegment {
export enum RotationAnimationFacing {
Linear = "Linear", // Default, linearly interpolate.
Center = "Center", // Always face the center.
CenterRelative = "CenterRelative", // Always face the center.
CenterRelative = "CenterRelative",
CenterRelativeOffset = "CenterRelativeOffset",
Forward = "Forward", // Always face the direction of the rotation.
Backward = "Backward", // Opposite of forward.
Start = "Start", // Stay facing the same direction as at the beginning.
@ -337,6 +386,8 @@ export class RotationAnimationSegment extends AnimationSegment {
return this.startFacing;
case RotationAnimationFacing.Center:
return degrees - 90;
case RotationAnimationFacing.CenterRelativeOffset:
return degrees - 90 + this.centerRelativeTo;
case RotationAnimationFacing.CenterRelative:
return degrees - this.startRotation + this.centerRelativeTo;
case RotationAnimationFacing.Forward:
@ -392,6 +443,16 @@ export class RotationAnimationSegment extends AnimationSegment {
start, end,
this.endRotation < this.startRotation);
ctx.stroke();
const startPos = this.interpolateOffset(0);
const endPos = this.interpolateOffset(1);
ctx.beginPath();
ctx.ellipse(startPos.x, startPos.y, 0.1, 0.1, 0, 0, 2*Math.PI);
ctx.stroke();
const endSize = 0.05;
ctx.fillRect(endPos.x - endSize, endPos.y - endSize, endSize*2, endSize*2);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,16 @@ export class CirclePosition {
this.enumValue = enumValue;
}
public static fromSides(leftRightSide: CircleSide.Left | CircleSide.Right, topBottomSide: CircleSide.Bottom | CircleSide.Top) {
return leftRightSide === CircleSide.Left
? topBottomSide === CircleSide.Top
? CirclePosition.TopLeft
: CirclePosition.BottomLeft
: topBottomSide === CircleSide.Top
? CirclePosition.TopRight
: CirclePosition.BottomRight;
}
private static enumValueToNumber(enumValue: CirclePositionEnum) : number {
switch (enumValue) {
case CirclePositionEnum.TopLeft:
@ -95,6 +105,38 @@ export class CirclePosition {
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public toShortLines(slideTo: Hand) : ShortLinesPosition {
return slideTo === Hand.Left
? [
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarRight,
ShortLinesPosition.MiddleRight,
][CirclePosition.enumValueToNumber(this.enumValue)]
: [
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleRight,
ShortLinesPosition.FarRight,
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public unfoldToShortLines(center: CircleSide.Bottom | CircleSide.Top) : ShortLinesPosition {
return center === CircleSide.Bottom
? [
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.MiddleRight,
ShortLinesPosition.FarRight,
][CirclePosition.enumValueToNumber(this.enumValue)]
: [
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarLeft,
ShortLinesPosition.FarRight,
ShortLinesPosition.MiddleRight,
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public swapDiagonal() : CirclePosition {
return this.swapAcross().swapUpAndDown();
}
@ -220,6 +262,16 @@ export class ShortLinesPosition {
][ShortLinesPosition.enumValueToNumber(this.enumValue)];
}
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) {
throw new Error("Invalid shift: " + this + " facing " + facing + " to " + dir + ".");
}
return ShortLinesPosition.get(ShortLinesPosition.numberToEnumValue(newNum));
}
public isMiddle() : boolean {
return this.enumValue === ShortLinesPositionEnum.MiddleRight || this.enumValue === ShortLinesPositionEnum.MiddleLeft;
}
@ -234,6 +286,11 @@ export class ShortLinesPosition {
return this.leftRightSide() === CircleSide.Left;
}
// Of the two positions on the same leftRightSide() is this the one further to the left?
public isLeftOfSide() : boolean {
return this.enumValue === ShortLinesPositionEnum.FarLeft || this.enumValue === ShortLinesPositionEnum.MiddleRight;
}
public facingSide() : Facing.Left | Facing.Right {
return this.isLeft() === this.isMiddle() ? Facing.Left : Facing.Right;
}
@ -304,7 +361,14 @@ export enum DancerDistance {
SwingRobin = "SwingRobin",
}
export enum LongLines {
// Walked forward into center.
Forward = "Forward",
// Only a little offset (has walked almost all the way from the other side after a give and take).
Near = "Near",
// Actually in center. May be slightly offset for wavy lines.
Center = "Center",
}
export type SemanticPosition = {
kind: PositionKind.Circle,
@ -325,6 +389,7 @@ export type SemanticPosition = {
hands?: Map<Hand, HandConnection>,
balance?: BalanceWeight,
dancerDistance?: DancerDistance,
longLines?: undefined,
};
export const handsInCircle = new Map<Hand, HandConnection>([
@ -337,7 +402,7 @@ export const handsInCircle = new Map<Hand, HandConnection>([
hand: Hand.Left,
}],
]);
export const handsFourImproper: Map<DancerIdentity, SemanticPosition> = new Map<DancerIdentity, SemanticPosition>([
export const handsFourImproper: Map<DancerIdentity, SemanticPosition & { kind: PositionKind.Circle }> = new Map<DancerIdentity, SemanticPosition & { kind: PositionKind.Circle }>([
[DancerIdentity.OnesLark, {
kind: PositionKind.Circle,
which: CirclePosition.TopLeft,
@ -363,3 +428,38 @@ export const handsFourImproper: Map<DancerIdentity, SemanticPosition> = new Map<
hands: handsInCircle,
}],
]);
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>([
[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>([
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 }]
]);
}
export function handsInLine(args: { wavy: boolean, which: ShortLinesPosition | CirclePosition, facing?: Facing }) {
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 }],
]);
}
}
export function handToDancerToSideInCircleFacingAcross(which: CirclePosition): Map<Hand, HandConnection> {
return new Map<Hand, HandConnection>([
which.isOnLeftLookingAcross()
? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }]
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
]);
}
export function handToDancerToSideInCircleFacingUpOrDown(which: CirclePosition): Map<Hand, HandConnection> {
return new Map<Hand, HandConnection>([
which.isOnLeftLookingUpAndDown()
? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }]
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
]);
}

View File

@ -40,7 +40,7 @@ type chooser_dancers = "everyone" | "gentlespoon" | "gentlespoons" | "ladle" | "
type chooser_pair = "gentlespoons" | "ladles" | "ones" | "twos" | "first corners" | "second corners";
type chooser_pair_or_everyone = "everyone" | "gentlespoons" | "ladles" | "ones" | "twos" | "first corners" | "second corners";
type chooser_pairc_or_everyone = "everyone" | "gentlespoons" | "ladles" | "centers" | "ones" | "twos";
export type chooser_pairz = "gentlespoons" | "ladles" | "partners" | "neighbors" | "next neighbors" | "ones" | "twos" | "same roles" | "first corners" | "second corners" | "shadows";
export type chooser_pairz = "gentlespoons" | "ladles" | "partners" | "neighbors" | "next neighbors" | "3rd neighbors" | "ones" | "twos" | "same roles" | "first corners" | "second corners" | "shadows";
type chooser_pairz_or_unspecified = "" | "gentlespoons" | "ladles" | "partners" | "neighbors" | "ones" | "twos" | "same roles" | "first corners" | "second corners" | "shadows";
type chooser_pairs = "partners" | "neighbors" | "same roles" | "shadows";
type chooser_pairs_or_ones_or_twos = "partners" | "neighbors" | "same roles" | "ones" | "twos" | "shadows";

View File

@ -3,7 +3,7 @@ import * as common from "./danceCommon.js";
import { DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
import { BalanceWeight, CirclePosition, CircleSide, CircleSideOrCenter, DancerDistance, Facing, HandConnection, HandTo, LongLines, PositionKind, SemanticPosition, ShortLinesPosition, StarGrip, handsInCircle } from "./interpreterCommon.js";
import { Move } from "./libfigureMapper.js";
import { DancerSetPosition, Hand, Offset, OffsetPlus, OffsetRotate, OffsetTimes, OffsetTranspose, dancerHeightOffset, dancerWidth, lineDistance, offsetZero, setDistance, setHeight, setSpacing, setWidth } from "./rendererConstants.js";
import { DancerSetPosition, Hand, Offset, OffsetPlus, OffsetRotate, OffsetTimes, OffsetTranspose, dancerHeight, dancerHeightOffset, dancerWidth, lineDistance, offsetZero, setDistance, setHeight, setSpacing, setWidth } from "./rendererConstants.js";
export enum SemanticAnimationKind {
@ -88,6 +88,9 @@ export type SemanticAnimation = {
// Swings are asymmetric. This is usually but not always the dancer's role.
swingRole: common.DanceRole,
// After a take need to fixup the position.
afterTake: boolean,
} | {
kind: SemanticAnimationKind.TwirlSwap,
@ -270,8 +273,12 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
throw "Invalid circle position: " + semantic.which;
}
switch(semantic.longLines) {
case LongLines.Center:
position.x *= 0.1;
case LongLines.Forward:
position.x *= 0.25;
position.x *= 0.3;
case LongLines.Near:
position.x *= 0.6;
}
let balanceOffset: Offset = offsetZero;
if (semantic.balance) {
@ -359,6 +366,10 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
case HandTo.DiagonalAcrossCircle:
// TODO Is "diagonal" even enough information?
return { x: -0.5, y: +0.5 }
case HandTo.LeftDiagonalAcrossCircle:
return { x: -0.5 - setDistance, y: +0.5 }
case HandTo.RightDiagonalAcrossCircle:
return { x: -0.5 + setDistance, y: +0.5 }
default:
throw "Unkown connection: " + connection.to;
}
@ -372,12 +383,15 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
};
case PositionKind.ShortLines:
let yOffset = 0;
switch (semantic.facing) {
case Facing.Up:
rotation = Rotation.Up;
yOffset = +dancerHeight;
break;
case Facing.Down:
rotation = Rotation.Down;
yOffset = -dancerHeight;
break;
case Facing.Left:
rotation = Rotation.Left;
@ -391,16 +405,16 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
switch (semantic.which) {
case ShortLinesPosition.FarLeft:
position = { x: -1.5, y: 0 };
position = { x: -1.5, y: yOffset };
break;
case ShortLinesPosition.MiddleLeft:
position = { x: -0.5, y: 0 };
position = { x: -0.5, y: yOffset };
break;
case ShortLinesPosition.MiddleRight:
position = { x: +0.5, y: 0 };
position = { x: +0.5, y: yOffset };
break;
case ShortLinesPosition.FarRight:
position = { x: +1.5, y: 0 };
position = { x: +1.5, y: yOffset };
break;
default:
throw "Invalid circle position: " + semantic.which;
@ -443,12 +457,14 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
// TODO Hands. Might need more info? Or need context of nearby dancer SemanticPositions?
if (!connection) return undefined;
const balanceYOffset = semantic.balance === BalanceWeight.Forward ? -shortWavesBalanceAmount : semantic.balance === BalanceWeight.Backward ? shortWavesBalanceAmount : 0;
const balanceYOffset = (semantic.facing === Facing.Up ? yOffset : -yOffset)
+ (semantic.balance === BalanceWeight.Forward ? -shortWavesBalanceAmount : semantic.balance === BalanceWeight.Backward ? shortWavesBalanceAmount : 0);
const balanceXOffset = semantic.balance === BalanceWeight.Left ? -balanceAmount : semantic.balance === BalanceWeight.Right ? balanceAmount : 0;
switch (connection.to) {
case HandTo.DancerLeft:
return { x: -0.5, y: balanceYOffset };
return { x: -0.5 + balanceXOffset, y: balanceYOffset };
case HandTo.DancerRight:
return { x: +0.5, y: balanceYOffset };
return { x: +0.5 + balanceXOffset, y: balanceYOffset };
case HandTo.DancerForward:
if (hand === connection.hand) {
return { x: 0, y: +0.5 };
@ -509,12 +525,13 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
];
case SemanticAnimationKind.Linear:
let rotation = endSetPosition.rotation - startSetPosition.rotation;
let rotationDuring: boolean = true;
const minRotation = move.movementPattern.minRotation;
try {
rotation = common.normalizeRotation(rotation, minRotation);
} catch {
throw new Error("Expected zero rotation, but start and end positions at different orientations: start="
+ JSON.stringify(move.startPosition) + ", end=" + JSON.stringify(move.endPosition));
rotation = common.normalizeRotation(rotation, undefined);
rotationDuring = false;
}
return [
new animation.TransitionAnimationSegment({
@ -529,19 +546,28 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
flags: {
hands: (move.movementPattern.handsDuring ?? "Linear") !== "Linear",
handsDuring: move.movementPattern.handsDuring === "Linear" ? undefined : move.movementPattern.handsDuring,
rotation: !rotationDuring,
rotationDuring: rotationDuring ? undefined : "Start",
},
startTransitionBeats: 1,
}),
];
case SemanticAnimationKind.Circle:
case SemanticAnimationKind.Star:
if (move.startPosition.kind !== PositionKind.Circle) {
throw "Circle must start and end in a circle.";
throw new Error(move.movementPattern.kind + " must start and end in a circle.");
}
const posWithHands = SemanticToSetPosition({...move.endPosition, hands: handsInCircle})
const circleHands = new Map<Hand, Offset>([
const isCircle = move.movementPattern.kind === SemanticAnimationKind.Circle;
const posWithHands = SemanticToSetPosition({...move.endPosition, hands: isCircle ? handsInCircle : undefined})
const circleHands = move.movementPattern.kind === SemanticAnimationKind.Circle ? new Map<Hand, Offset>([
[Hand.Left, posWithHands.leftArmEnd!],
[Hand.Right, posWithHands.rightArmEnd!],
]) : new Map<Hand, Offset>([
[move.movementPattern.hand, {
x: (move.movementPattern.hand === Hand.Right ? +1 : -1) * (1 + (move.movementPattern.grip === StarGrip.HandsAcross ? 0 : 0.15)) * Math.sqrt(2),
y: move.movementPattern.grip === StarGrip.HandsAcross ? 0 : 0.25
}]
]);
return [
@ -556,7 +582,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
width: setWidth,
height: setHeight,
},
facing: animation.RotationAnimationFacing.Center,
facing: isCircle ? animation.RotationAnimationFacing.Center : animation.RotationAnimationFacing.Forward,
closer: undefined,
hands: new Map<Hand, animation.HandAnimation>([
[Hand.Left, { kind: "End" }],
@ -564,6 +590,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
])
}),
flags: {
rotation: true,
hands: true,
handsDuring: circleHands,
},
@ -646,18 +673,35 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
const rotateSwingCenter = CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
const dancerDistance = move.movementPattern.swingRole === DanceRole.Lark ? DancerDistance.SwingLark : DancerDistance.SwingRobin;
const swingStart = SemanticToSetPosition({
const baseSwingStart = {
...move.startPosition,
dancerDistance,
dancerDistance: move.movementPattern.afterTake ? undefined : dancerDistance,
balance: undefined,
});
longLines: undefined,
}
const swingStartUnadjusted = SemanticToSetPosition(move.startPosition.longLines === LongLines.Near ? {
...baseSwingStart,
kind: PositionKind.Circle,
which: move.startPosition.which.swapUpAndDown(),
} : baseSwingStart);
const swingStart = move.movementPattern.afterTake
? {
...swingStartUnadjusted, position: {
x: swingStartUnadjusted.position.x
+ ((move.startPosition.longLines === LongLines.Near) === (move.startPosition.which.isLeft())
? +0.5
: -0.5),
y: rotateSwingCenter.y,
}
}
: swingStartUnadjusted;
const beforeUnfold = SemanticToSetPosition({
...move.endPosition,
dancerDistance,
});
const unfolded = SemanticToSetPosition({
...move.endPosition,
dancerDistance: DancerDistance.Normal,
dancerDistance: move.endPosition.dancerDistance === DancerDistance.Compact ? DancerDistance.Compact : DancerDistance.Normal,
hands: move.movementPattern.swingRole === DanceRole.Lark
? new Map<Hand, HandConnection>([[Hand.Right, {hand: Hand.Left, to: HandTo.DancerRight}]])
: new Map<Hand, HandConnection>([[Hand.Left, {hand: Hand.Right, to: HandTo.DancerLeft}]]),
@ -669,15 +713,16 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
const turns = Math.ceil(Math.abs(move.movementPattern.minAmount / 360));
const swingMinRotation = (minTurns - turns) * (move.movementPattern.minAmount < 0 ? -360 : 360) + move.movementPattern.minAmount;
return [
new animation.TransitionAnimationSegment({
const slideAmount = move.movementPattern.afterTake ? { x: 0, y: rotateSwingCenter.y - startSetPosition.position.y } : undefined;
const swingAnimation = new animation.TransitionAnimationSegment({
actualAnimation: new animation.RotationAnimationSegment({
beats: swingBeats,
startPosition: startSetPosition,
endPosition: beforeUnfold,
startPosition: slideAmount ? { ...swingStart, position: OffsetPlus(swingStart.position, OffsetTimes(slideAmount, -1)) } : swingStart,
endPosition: slideAmount ? { ...beforeUnfold, position: OffsetPlus(beforeUnfold.position, OffsetTimes(slideAmount, -1)) } : beforeUnfold,
rotation: swingMinRotation,
around: {
center: rotateSwingCenter,
center: slideAmount ? OffsetPlus(rotateSwingCenter, OffsetTimes(slideAmount, -1)) : rotateSwingCenter,
width: 1,
height: 1,
},
@ -685,8 +730,8 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
minDistance: 1,
transitionBeats: 1,
},
facing: animation.RotationAnimationFacing.CenterRelative,
centerRelativeTo: startSetPosition.rotation + ((move.startPosition.facing === Facing.Up || move.startPosition.facing === Facing.Down) === (dancerDistance === DancerDistance.SwingLark) ? -45 : +45)
facing: animation.RotationAnimationFacing.CenterRelativeOffset,
centerRelativeTo: (dancerDistance === DancerDistance.SwingLark ? -45 : +45),
}),
flags: {
rotation: true,
@ -695,7 +740,14 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
},
startTransitionBeats: 1,
endTransitionBeats: 0,
}),
});
return [
slideAmount ? new animation.SlideAnimationSegment(
swingAnimation.beats,
[swingAnimation],
slideAmount,
) : swingAnimation,
new animation.LinearAnimationSegment({
beats: 1,
startPosition: beforeUnfold,
@ -711,6 +763,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
const twirlCenter =
CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
const aroundTopOrBottom = move.movementPattern.around === CircleSide.Top || move.movementPattern.around === CircleSide.Bottom;
const inShortLines = move.startPosition.kind === PositionKind.ShortLines;
return [
new animation.TransitionAnimationSegment({
actualAnimation: new animation.RotationAnimationSegment({
@ -727,7 +780,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
around: {
center: twirlCenter,
width: aroundTopOrBottom ? setWidth : setWidth / 4,
height: aroundTopOrBottom ? setHeight / 4 : setHeight,
height: aroundTopOrBottom || inShortLines ? setHeight / 4 : setHeight,
},
facing: animation.RotationAnimationFacing.Linear,
hands: new Map<Hand, animation.HandAnimation>([
@ -779,6 +832,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
flags: {
hands: true,
rotation: true,
rotationDirection: move.movementPattern.side,
},
startTransitionBeats: 0.5,
})
@ -838,10 +892,17 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
startTransitionBeats: 1,
})
];
case SemanticAnimationKind.Promenade:
return [
new animation.StepWideLinearAnimationSegment({
beats: move.beats,
startPosition: startSetPosition,
endPosition: endSetPosition,
distanceAtMidpoint: setHeight / 2,
})
]
// TODO Unsupported moves, just doing linear for now.
case SemanticAnimationKind.RollAway:
case SemanticAnimationKind.Promenade:
case SemanticAnimationKind.Star:
return [
new animation.LinearAnimationSegment({
beats: move.beats,

View File

@ -146,14 +146,14 @@ wrapperDiv.appendChild(beatDisplay);
function drawAtCurrentBeat() {
r.drawSetsWithTrails(beatSlider.valueAsNumber, progressionSelector.valueAsNumber);
const moveForCurrent = movesByBeat[Math.floor(beatSlider.valueAsNumber)];
if (!moveForCurrent.classList.contains('currentMove')) {
const moveForCurrent = movesByBeat.at(Math.floor(beatSlider.valueAsNumber));
if (!moveForCurrent?.classList.contains('currentMove') ?? false) {
for (let i = 0; i < movesByBeat.length; i++) {
if (movesByBeat[i].classList.contains('currentMove')) {
movesByBeat[i].classList.remove('currentMove');
}
}
moveForCurrent.classList.add('currentMove');
moveForCurrent?.classList.add('currentMove');
}
}
@ -289,6 +289,7 @@ function buildMovesList() {
}
if (lastItem) {
lastItem.classList.add('moveForBeat_' + currentBeat);
movesByBeat[currentBeat] = lastItem;
}
if (r.animation.progressionError) {
@ -420,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);
@ -462,7 +503,7 @@ verifyButton.addEventListener('click', () => {
libfigureError = 'libfigure ex: ' + e;
}
try {
progressionError = interpreter.loadDance(dance.figures, dance.start_type).progressionError;
progressionError = interpreter.loadDance(dance).progressionError;
const moveError = [...interpreter.interpretedDance.values()].flatMap(moves => moves.filter(m => m.interpreterError !== undefined).map(m => m.interpreterError)).at(0);
if (moveError) {
interpreterError = "interpreter move error: " + moveError;
@ -537,7 +578,7 @@ function loadDance() {
danceTitle.appendChild(title);
danceTitle.appendChild(author);
}
r.animation = interpreter.loadDance(dance.figures, dance.start_type);
r.animation = interpreter.loadDance(dance);
if (cancelAnim !== undefined) {
cancelAnimationFrame(cancelAnim);
playButton.innerText = 'Play';

View File

@ -0,0 +1,341 @@
import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "../danceCommon.js";
import { CirclePosition, CircleSideOrCenter, PositionKind, SemanticPosition } from "../interpreterCommon.js";
import { Move, chooser_pairz } from "../libfigureMapper.js";
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind } from "../lowLevelMove.js";
type MoveName = string & Move["move"];
export type MoveInterpreterCtor<N extends MoveName> = new (args: MoveInterpreterCtorArgs<N>) => MoveInterpreter<N>;
export const moveInterpreters: Map<MoveName, MoveInterpreterCtor<MoveName>> = new Map<MoveName, MoveInterpreterCtor<MoveName>>();
export interface MoveInterpreterCtorArgs<N extends MoveName> {
move: Move & { move: N };
nextMove: Move;
numProgessions: number;
}
export type SemanticPositionsForAllDancers = Map<DancerIdentity, SemanticPosition>;
export interface MoveAsLowLevelMovesArgs {
startingPos: SemanticPositionsForAllDancers;
}
export type LowLevelMovesForAllDancers = Map<DancerIdentity, LowLevelMove[]>;
export interface Variant {
previousMoveVariant?: string,
lowLevelMoves: LowLevelMovesForAllDancers,
};
export type VariantCollection = Map<string, Variant>;
export type PartialLowLevelMove = {
remarks?: string,
beats: number,
startPosition?: SemanticPosition,
endPosition: SemanticPosition,
movementPattern: SemanticAnimation,
};
export interface ISingleVariantMoveInterpreter {
moveAsLowLevelMoves: () => LowLevelMovesForAllDancers;
moveAsVariants: () => VariantCollection;
};
export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>, N extends MoveName> implements ISingleVariantMoveInterpreter {
protected readonly moveInterpreter: T;
protected readonly startingPos: SemanticPositionsForAllDancers;
constructor(moveInterpreter: T, startingPos: SemanticPositionsForAllDancers) {
this.moveInterpreter = moveInterpreter;
this.startingPos = startingPos;
}
get move() : Move & { move: N } {
return this.moveInterpreter.move;
}
abstract moveAsLowLevelMoves(): LowLevelMovesForAllDancers;
moveAsVariants(): VariantCollection {
return new Map<string, Variant>([
["default", { lowLevelMoves: this.moveAsLowLevelMoves() }]
]);
}
static append(moves: LowLevelMove[],
newMove: LowLevelMove | PartialLowLevelMove
| ((prevEnd: SemanticPosition) => PartialLowLevelMove)): LowLevelMove[] {
const lastMove = moves.at(-1)!;
const prevEnd = lastMove.endPosition;
if (typeof newMove === 'function') {
newMove = newMove(prevEnd);
}
if (!newMove.startPosition) {
newMove.startPosition = prevEnd;
}
moves.push({
...newMove,
startPosition: newMove.startPosition ?? prevEnd,
move: lastMove.move,
startBeat: lastMove.startBeat + lastMove.beats,
});
return moves;
}
combine(moves: ((LowLevelMove | PartialLowLevelMove
| ((prevEnd: SemanticPosition) => PartialLowLevelMove))[]),
startPos?: SemanticPosition): LowLevelMove[] {
const res: LowLevelMove[] = [];
if (moves.length === 0) return res;
let firstMove = moves[0];
if ('move' in firstMove) {
res.push(firstMove);
} else {
if (typeof firstMove === 'function') {
firstMove = firstMove(startPos!);
}
res.push({...firstMove,
move: this.move,
startBeat: 0,
startPosition: firstMove.startPosition ?? startPos!,
});
}
for (const move of moves.slice(1)) {
SingleVariantMoveInterpreter.append(res, move);
}
if (res[0].startPosition === undefined) {
throw new Error("combine() called without a startPosition.");
}
return res;
}
findPairOpposite(who: chooser_pairz, id: DancerIdentity): ExtendedDancerIdentity | null {
const pos = this.getPosFor(id.asExtendedDancerIdentity());
const inSameSet = (proposedId: ExtendedDancerIdentity) => {
const proposedPos = this.getPosFor(proposedId);
return {
...proposedId,
// Get the same role dancer in the set the dancer is currently in.
relativeSet: proposedId.relativeSet + (pos.setOffset - proposedPos.setOffset)
}
}
switch (who) {
case "partners":
return id.partner().asExtendedDancerIdentity();
case "neighbors":
case "next neighbors":
case "3rd neighbors":
// TODO This isn't quite right... especially if it's "next" to intentionally progress...
return inSameSet(id.neighbor().asExtendedDancerIdentity());
// These three might get used when not with neighbors?
case "gentlespoons":
case "ladles":
case "same roles":
if (who === "gentlespoons" && id.danceRole === DanceRole.Robin
|| who === "ladles" && id.danceRole === DanceRole.Lark) {
return null;
}
return inSameSet(id.oppositeSameRole().asExtendedDancerIdentity());
case "ones":
if (id.coupleRole === CoupleRole.Twos) return null;
return id.partner().asExtendedDancerIdentity();
case "twos":
if (id.coupleRole === CoupleRole.Ones) return null;
return id.partner().asExtendedDancerIdentity();
case "shadows":
throw new Error("Not sure shadow is consistently the same.");
case "first corners":
case "second corners":
throw new Error("Contra corners are unsupported.");
default:
throw new Error("Unsupported who: " + who);
}
}
getPosFor(id: ExtendedDancerIdentity): SemanticPosition & { setOffset: number, lineOffset: number } {
const basePos = this.startingPos.get(id.setIdentity)!;
return {...basePos,
setOffset: (basePos.setOffset ?? 0) + id.relativeSet,
lineOffset: (basePos.lineOffset ?? 0) + id.relativeLine,
};
}
handleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
const res = new Map<DancerIdentity, LowLevelMove[]>();
let anyProgressed = false;
for (const [id, startPos] of this.startingPos.entries()) {
const lowLevelMoves = dancerFunc({ id, startPos });
if (this.move.progression) {
const startingPos: SemanticPosition = lowLevelMoves.at(0)?.startPosition!;
const endPos: SemanticPosition = lowLevelMoves.at(-1)?.endPosition!;
if (startingPos.setOffset !== endPos.setOffset) {
anyProgressed = true;
}
}
res.set(id, lowLevelMoves);
}
if (this.move.progression && !anyProgressed) {
for (const [id, lowLevelMoves] of res.entries()) {
const startingPos: SemanticPosition = lowLevelMoves.at(0)?.startPosition!;
const endPos: SemanticPosition = lowLevelMoves.at(-1)?.endPosition!;
if (startingPos.setOffset === endPos.setOffset && endPos.kind === PositionKind.Circle) {
const endSetOffset = (endPos.setOffset ?? 0) + (endPos.which.isTop() ? -0.5 : +0.5);
const endWhich = endPos.which.swapUpAndDown();
lowLevelMoves[lowLevelMoves.length - 1] = {
...lowLevelMoves[lowLevelMoves.length - 1],
endPosition: {
...endPos,
setOffset: endSetOffset,
which: endWhich,
}
};
}
}
}
return res;
}
handleCircleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition & { kind: PositionKind.Circle } }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
return this.handleMove(({ id, startPos }) => {
if (startPos.kind !== PositionKind.Circle) {
throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos);
}
return dancerFunc({ id, startPos });
});
}
handlePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
id: DancerIdentity,
startPos: SemanticPosition,
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
withId: ExtendedDancerIdentity,
around: CircleSideOrCenter,
}) => LowLevelMove[]), meanwhileFunc?: ((arg: {
id: DancerIdentity,
startPos: SemanticPosition,
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
return this.handleMove(({ id, startPos }) => {
const withId = this.findPairOpposite(who, id);
if (!withId) {
if (meanwhileFunc) {
return meanwhileFunc({ id, startPos });
} else {
return this.combine([{
beats: this.move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
// TODO Not sure this is actually a good default...
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]);
}
}
const withPos = this.getPosFor(withId);
const setDifference = withPos.setOffset - (startPos.setOffset ?? 0);
let startPosAdjusted = startPos;
if (setDifference !== 0) {
// TODO Can move be with a different short line or just a different circle?
// PassBy can probably be with the next short line...
if (startPos.kind === PositionKind.Circle && (setDifference === 1 || setDifference === -1)) {
startPosAdjusted = {
...startPos,
setOffset: (startPos.setOffset ?? 0) + setDifference / 2,
which: startPos.which.swapUpAndDown(),
}
} else {
throw new Error("Not near dancer to " + this.move.move + " with.");
}
}
const startWhich = startPosAdjusted.which;
// TODO Can swing be across the set (top or bottom)?
const around = withPos.which.leftRightSide() === startWhich.leftRightSide()
? startWhich.leftRightSide()
: withPos.kind === PositionKind.Circle
? (startWhich instanceof CirclePosition && withPos.which.topBottomSide() === startWhich.topBottomSide()
? startWhich.topBottomSide()
: "Center")
: "Center";
return dancerFunc({ id, startPos: startPosAdjusted, withId, withPos, around });
});
}
handleCirclePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
id: DancerIdentity,
startPos: SemanticPosition & { kind: PositionKind.Circle },
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
withId: ExtendedDancerIdentity,
around: CircleSideOrCenter,
}) => LowLevelMove[]), meanwhileFunc?: ((arg: {
id: DancerIdentity,
startPos: SemanticPosition & { kind: PositionKind.Circle },
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
return this.handlePairedMove(who, ({ id, startPos, withId, withPos, around }) => {
if (startPos.kind !== PositionKind.Circle) {
throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos);
}
return dancerFunc({ id, startPos, withId, withPos, around });
}, meanwhileFunc ? ({id, startPos}) => {
if (startPos.kind !== PositionKind.Circle) {
throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos);
}
return meanwhileFunc({id, startPos});
} : undefined);
}
errorStandStill() {
return this.handleMove(({ startPos }) => {
return [{
interpreterError: "UNKNOWN MOVE '" + this.move.move + "': standing still",
move: this.move,
startBeat: 0,
beats: this.move.beats,
startPosition: startPos,
endPosition: startPos,
movementPattern: {
kind: SemanticAnimationKind.StandStill,
},
}];
});
}
}
export abstract class MoveInterpreter<N extends MoveName> {
public readonly move: Move & { move: N };
public readonly nextMove: Move;
public readonly numProgressions: number;
constructor({ move, nextMove, numProgessions }: MoveInterpreterCtorArgs<N>) {
this.move = move;
this.nextMove = nextMove; // TODO Should be able to get rid of this using variants.
this.numProgressions = numProgessions;
}
abstract buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter;
moveAsLowLevelMoves({ startingPos }: MoveAsLowLevelMovesArgs): LowLevelMovesForAllDancers {
return this.buildSingleVariantMoveInterpreter(startingPos).moveAsLowLevelMoves();
}
}
class DefaultSingleVariantMoveInterpreter extends SingleVariantMoveInterpreter<DefaultMoveInterpreter, MoveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.errorStandStill();
}
}
class DefaultMoveInterpreter extends MoveInterpreter<MoveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
throw new Error("Method not implemented.");
}
}
export const errorMoveInterpreterCtor: MoveInterpreterCtor<MoveName> = DefaultMoveInterpreter;

170
www/js/moves/allemande.ts Normal file
View File

@ -0,0 +1,170 @@
import { SemanticPosition, PositionKind, CircleSide, Facing, CirclePosition, LongLines, HandConnection } from "../interpreterCommon.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { Hand } from "../rendererConstants.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
type allemandeMoves = "allemande" | "allemande orbit" | "gyre";
class AllemandeSingleVariant extends SingleVariantMoveInterpreter<Allemande, allemandeMoves> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
// 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;
}
return this.handlePairedMove(move.parameters.who, ({ startPos, around, withId, withPos }) => {
let endPosition: SemanticPosition = startPos;
let startingPos = startPos;
if (swap) {
// TODO This was more complicated. Is this wrong?
endPosition = withPos;
} else if (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)
!== (startPos.facing === Facing.Up)
!== startPos.which.isLeftOfSide())) {
startWhich = startPos.which.swapOnSide()
startingPos = {
...startPos,
which: startWhich,
};
}
const endWhich = CirclePosition.fromSides(startingPos.which.leftRightSide(),
startWhich.isLeftOfSide()
!== (byHandOrShoulder === Hand.Right)
!== (intoWavePositions === 1)
? CircleSide.Top
: CircleSide.Bottom);
endPosition = {
kind: PositionKind.Circle,
which: endWhich,
facing: (startingPos.facing === Facing.Up) === (intoWavePositions === 1)
? endWhich.facingAcross()
: endWhich.facingOut(),
setOffset: startingPos.setOffset,
lineOffset: startingPos.lineOffset,
}
} else {
throw new Error("Allemande from short lines to line line in middle unsupported.");
}
} else {
if (around === "Center") {
const startCenter = startPos.longLines === LongLines.Center;
const endWhich = startPos.which.circleRight(intoWavePositions);
endPosition = {
kind: PositionKind.Circle,
which: endWhich,
facing: startPos.which.facingOut(),
longLines: startCenter ? undefined : LongLines.Center,
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
}
} else {
const endWhich = startPos.which.toShortLines(intoWavePositions === 1 ? Hand.Right : Hand.Left);
endPosition = {
kind: PositionKind.ShortLines,
which: endWhich,
facing: endWhich.isLeftOfSide() === (byHandOrShoulder === Hand.Left) ? Facing.Up : Facing.Down,
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
}
}
}
}
return this.combine([
{
beats: move.beats,
endPosition,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHandOrShoulder === Hand.Right ? allemandeCircling : -allemandeCircling,
around,
byHand: move.move === "allemande" || move.move === "allemande orbit" ? byHandOrShoulder : undefined,
close: true,
},
},
], {
...startingPos,
hands: startPos.hands && move.move !== "gyre"
? new Map<Hand, HandConnection>([...startPos.hands.entries()].filter(([h, c]) => h === 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,
balance: undefined,
dancerDistance: undefined,
}
let endPosition: SemanticPosition;
if (swap) {
if (startingPos.kind === PositionKind.Circle) {
endPosition =
{
...startingPos,
which: startingPos.which.swapDiagonal(),
facing: startingPos.which.isLeft() ? Facing.Left : Facing.Right,
}
} else {
endPosition =
{
...startingPos,
which: startingPos.which.swapSides(),
facing: startingPos.which.isLeft() ? Facing.Left : Facing.Right,
}
}
} else {
endPosition = startingPos;
}
return this.combine([
{
beats: move.beats,
endPosition,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
// Orbit is opposite direction of allemande.
minAmount: byHandOrShoulder === Hand.Right ? -orbitAmount : +orbitAmount,
around: "Center",
byHand: undefined,
close: false,
},
},
], startingPos);
});
}
}
class Allemande extends MoveInterpreter<allemandeMoves> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new AllemandeSingleVariant(this, startingPos);
}
}
moveInterpreters.set("allemande", Allemande);
moveInterpreters.set("allemande orbit", Allemande);
moveInterpreters.set("gyre", Allemande);

44
www/js/moves/balance.ts Normal file
View File

@ -0,0 +1,44 @@
import { BalanceWeight } from "../interpreterCommon.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
class BalanceSingleVariant extends SingleVariantMoveInterpreter<Balance, "balance"> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
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?
return this.combine([
{
beats: this.moveInterpreter.forwardBeats,
endPosition: { ...startPos, balance: BalanceWeight.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: this.moveInterpreter.backwardBeats,
endPosition: { ...startPos, balance: BalanceWeight.Backward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
], startPos);
});
}
}
class Balance extends MoveInterpreter<"balance"> {
public readonly forwardBeats: number;
public readonly backwardBeats: number;
constructor(args: MoveInterpreterCtorArgs<"balance">) {
super(args);
this.forwardBeats = this.move.beats / 2;
this.backwardBeats = this.move.beats - this.forwardBeats;
}
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new BalanceSingleVariant(this, startingPos);
}
}
moveInterpreters.set("balance", Balance);

View File

@ -0,0 +1,56 @@
import { BalanceWeight, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { LowLevelMove, SemanticAnimationKind } from "../lowLevelMove.js";
import { Hand } from "../rendererConstants.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
export function balanceCircleInAndOut(move: Move, startPos: SemanticPosition, balanceBeats?: number): [LowLevelMove, LowLevelMove] {
if (startPos.kind !== PositionKind.Circle) {
throw "Balance circle must start in a circle, but starting at " + startPos;
}
balanceBeats ??= 4;
const balancePartBeats = balanceBeats/2;
const holdingHandsInCircle: SemanticPosition = {...startPos,
facing: Facing.CenterOfCircle,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Right, to: HandTo.LeftInCircle }],
[Hand.Right, { hand: Hand.Left, to: HandTo.RightInCircle }],
]),
};
const circleBalancedIn: SemanticPosition = {...holdingHandsInCircle,
balance: BalanceWeight.Forward,
};
const balanceIn: LowLevelMove = {
move,
startBeat: 0,
beats: balancePartBeats,
startPosition: holdingHandsInCircle,
endPosition: circleBalancedIn,
movementPattern: { kind: SemanticAnimationKind.Linear },
};
const balanceOut: LowLevelMove = {...balanceIn,
startBeat: balancePartBeats,
startPosition: circleBalancedIn,
endPosition: holdingHandsInCircle,
};
return [balanceIn, balanceOut];
}
class BalanceTheRingSingleVariant extends SingleVariantMoveInterpreter<BalanceTheRing, "balance the ring"> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.handleCircleMove(({ startPos }) => balanceCircleInAndOut(this.move, startPos));
}
}
class BalanceTheRing extends MoveInterpreter<"balance the ring"> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new BalanceTheRingSingleVariant(this, startingPos);
}
}
moveInterpreters.set("balance the ring", BalanceTheRing);

View File

@ -0,0 +1,83 @@
import { DanceRole, CoupleRole } from "../danceCommon.js";
import { SemanticPosition, Facing, HandConnection, HandTo, BalanceWeight } 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";
const moveName: Move["move"] = "box circulate";
class BoxCirculateSingleVariant extends SingleVariantMoveInterpreter<BoxCirculate, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
const circulateRight: boolean = this.move.parameters.hand;
const whoCrosses = this.move.parameters.who;
return this.handleCircleMove(({ id, startPos }) => {
let isCrossing: boolean;
switch (whoCrosses) {
case "gentlespoons":
isCrossing = id.danceRole === DanceRole.Lark;
break;
case "ladles":
isCrossing = id.danceRole === DanceRole.Robin;
break;
case "ones":
isCrossing = id.coupleRole === CoupleRole.Ones;
break;
case "twos":
isCrossing = id.coupleRole === CoupleRole.Twos;
break;
case "first corners":
case "second corners":
throw "first/second corner leading box circulate doesn't make sense?";
}
// Starts in long wavy lines.
const startingPos: SemanticPosition = {
...startPos,
facing: isCrossing === startPos.which.isLeft() ? Facing.Right : Facing.Left,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }],
[Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }],
]),
balance: undefined,
longLines: undefined,
dancerDistance: undefined,
};
const balance: PartialLowLevelMove[] = this.move.parameters.bal ? [
{
beats: 2,
endPosition: { ...startingPos, balance: circulateRight ? BalanceWeight.Right : BalanceWeight.Left },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: 2,
endPosition: { ...startingPos, balance: BalanceWeight.Backward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
] : [];
const circulate: PartialLowLevelMove = {
beats: this.move.beats - (this.move.parameters.bal ? 4 : 0),
endPosition: {
...startingPos,
which: isCrossing ? startingPos.which.swapAcross() : startingPos.which.swapUpAndDown(),
facing: isCrossing ? startingPos.facing : startingPos.facing === Facing.Right ? Facing.Left : Facing.Right,
},
movementPattern: {
// TODO Not sure loop should really be linear...
kind: SemanticAnimationKind.Linear,
minRotation: isCrossing ? undefined : circulateRight ? 180 : -180,
handsDuring: "None",
}
};
return this.combine([...balance, circulate], startingPos);
});
}
}
class BoxCirculate extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new BoxCirculateSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, BoxCirculate);

View File

@ -0,0 +1,74 @@
import { HandConnection, HandTo, BalanceWeight } 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";
const moveName: Move["move"] = "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;
// TODO Adjust facing?
const startPosition = { ...startPos, hands: new Map<Hand, HandConnection>([[hand, { hand, to: HandTo.DancerForward }]]) };
if (around === "Center") {
throw "TwirlSwap around center is unsupported.";
}
const twirl: PartialLowLevelMove = {
beats: twirlBeats,
endPosition: withPos,
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around,
hand,
}
};
if (this.move.parameters.bal) {
return this.combine([
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Forward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Backward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
twirl], startPosition);
} else {
return this.combine([twirl], startPosition);
}
});
}
}
class BoxTheGnat extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new BoxTheGnatSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, BoxTheGnat);

View File

@ -0,0 +1,33 @@
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";
const moveName: Move["move"] = "butterfly whirl";
class ButterflyWhirlSingleVariant extends SingleVariantMoveInterpreter<ButterflyWhirl, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.handleCircleMove(({ startPos }) => {
return this.combine([{
beats: this.move.beats,
endPosition: startPos,
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,
close: true,
minAmount: 360,
}
}], startPos);
});
}
}
class ButterflyWhirl extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new ButterflyWhirlSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, ButterflyWhirl);

View File

@ -0,0 +1,53 @@
import { HandConnection, HandTo, CircleSide, Facing } 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";
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.
const onLeft: boolean = startPos.which.isOnLeftLookingUpAndDown();
// TODO get rid of this 1 beat set up and make it part of TwirlSwap?
return this.combine([
{
beats: 1,
endPosition: {
...startPos,
hands: new Map<Hand, HandConnection>([onLeft
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
facing: startPos.which.topBottomSide() === CircleSide.Top ? Facing.Down : Facing.Up,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: this.move.beats - 1,
endPosition: {
...startPos,
which: startPos.which.swapAcross(),
facing: startPos.which.topBottomSide() === CircleSide.Top ? Facing.Up : Facing.Down,
},
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around: startPos.which.topBottomSide(),
hand: onLeft ? Hand.Right : Hand.Left,
}
}], startPos);
});
}
}
class CaliforniaTwirl extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new CaliforniaTwirlSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, CaliforniaTwirl);

154
www/js/moves/chain.ts Normal file
View File

@ -0,0 +1,154 @@
import { DanceRole } from "../danceCommon.js";
import { HandTo, HandConnection, PositionKind } 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";
const moveName: Move["move"] = "chain";
class ChainSingleVariant extends SingleVariantMoveInterpreter<Chain, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
const mainRole = this.move.parameters.who === "gentlespoons" ? DanceRole.Lark : DanceRole.Robin;
const pullToTurnBeats = 2;
const pullBeats = this.move.beats / 2 - pullToTurnBeats;
const turnBeats = this.move.beats - pullBeats - pullToTurnBeats;
const chainHand: Hand = this.move.parameters.hand ? Hand.Right : Hand.Left;
const cwCourtesyTurn = chainHand === Hand.Left;
return this.handleCircleMove(({ id, startPos }) => {
if (id.danceRole === mainRole) {
const endWhich = startPos.which.swapDiagonal();
let endSet = startPos.setOffset ?? 0;
let to: HandTo;
switch (this.move.parameters.dir) {
case "along":
throw "Don't know what chaining along the set means.";
case "across":
to = HandTo.DiagonalAcrossCircle;
break;
case "right diagonal":
to = HandTo.RightDiagonalAcrossCircle;
endSet += startPos.which.isLeft() ? -1 : +1;
break;
case "left diagonal":
to = HandTo.LeftDiagonalAcrossCircle;
endSet += startPos.which.isLeft() ? +1 : -1;
break;
}
const startPosition = {
...startPos,
hands: new Map<Hand, HandConnection>([[chainHand, { hand: chainHand, to }]]),
facing: startPos.which.facingAcross(),
};
const turnTo = chainHand === Hand.Right ? HandTo.DancerRight : HandTo.DancerLeft;
return this.combine([
{
beats: pullBeats,
endPosition: {
...startPos,
which: endWhich,
facing: endWhich.facingUpOrDown(),
setOffset: endSet,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Left, to: turnTo }],
[Hand.Right, { hand: Hand.Right, to: turnTo }],
]),
},
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around: "Center",
side: chainHand,
withHands: true,
facing: "Forward",
otherPath: "Swap",
}
},
{
beats: pullToTurnBeats,
endPosition: {
...this.startingPos,
kind: PositionKind.Circle,
which: endWhich.swapUpAndDown(),
facing: endWhich.facingOut(),
setOffset: endSet,
},
movementPattern: {
kind: SemanticAnimationKind.PassBy,
side: chainHand.opposite(),
withHands: true,
around: endWhich.leftRightSide(),
facing: "Forward", // TODO Is this right?
otherPath: "Swap",
}
},
prevEnd => ({
beats: turnBeats,
endPosition: {
kind: PositionKind.Circle,
which: endWhich,
facing: endWhich.facingAcross(),
hands: prevEnd.hands,
setOffset: prevEnd.setOffset,
lineOffset: prevEnd.lineOffset,
},
movementPattern: {
kind: SemanticAnimationKind.CourtesyTurn,
clockwise: cwCourtesyTurn,
}
})
], startPosition);
} else {
const startingPos = { ...startPos, hands: undefined };
return this.combine([
{
beats: pullBeats,
endPosition: { ...startingPos, facing: startingPos.which.facingUpOrDown() },
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: pullToTurnBeats,
endPosition: {
...startingPos,
which: startingPos.which.swapUpAndDown(),
facing: startingPos.which.facingOut()
},
movementPattern: {
kind: SemanticAnimationKind.PassBy,
side: chainHand.opposite(),
withHands: true,
around: startingPos.which.leftRightSide(),
facing: "Forward", // TODO Is this right?
otherPath: "Swap",
}
},
{
beats: turnBeats,
endPosition: {
...startingPos,
// TODO Does CourtesyTurn always end in same position?
which: startPos.which,
facing: startPos.which.facingAcross(),
},
movementPattern: {
kind: SemanticAnimationKind.CourtesyTurn,
clockwise: cwCourtesyTurn,
}
}
], startingPos);
}
});
}
}
class Chain extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new ChainSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, Chain);

38
www/js/moves/circle.ts Normal file
View File

@ -0,0 +1,38 @@
import { 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";
const moveName: Move["move"] = "circle";
class CircleSingleVariant extends SingleVariantMoveInterpreter<Circle, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.handleCircleMove(({ startPos }) => {
const places = this.move.parameters.places / 90 * (this.move.parameters.turn ? 1 : -1);
return this.combine([
{
beats: this.move.beats,
endPosition: {
...startPos,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
which: startPos.which.circleLeft(places),
},
movementPattern: {
kind: SemanticAnimationKind.Circle,
places,
}
},
], { ...startPos, facing: Facing.CenterOfCircle });
});
}
}
class Circle extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new CircleSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, Circle);

38
www/js/moves/custom.ts Normal file
View File

@ -0,0 +1,38 @@
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
class CustomSingleVariant extends SingleVariantMoveInterpreter<Custom, "custom"> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
// TODO refactor so this is in separate classes?
if (this.move.parameters.custom.includes("mirrored mad robin")) {
return this.handleCircleMove(({ id, startPos }) => {
// TODO Read custom to decide direction?
const startAndEndPos = {
...startPos,
facing: startPos.which.facingAcross(),
hands: undefined,
};
return this.combine([{
beats: this.move.beats,
startPosition: startAndEndPos,
endPosition: startAndEndPos,
movementPattern: {
kind: SemanticAnimationKind.DoSiDo,
amount: startPos.which.isLeft() ? -360 : 360,
around: startPos.which.leftRightSide(),
},
}]);
});
} else {
return this.errorStandStill();
}
}
}
class Custom extends MoveInterpreter<"custom"> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new CustomSingleVariant(this, startingPos);
}
}
moveInterpreters.set("custom", Custom);

80
www/js/moves/doSiDo.ts Normal file
View File

@ -0,0 +1,80 @@
import { SemanticPosition, Facing, CircleSide, PositionKind } 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";
const moveName: Move["move"] = "do si do";
class DoSiDoSingleVariant extends SingleVariantMoveInterpreter<DoSiDo, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (!this.move.parameters.shoulder) {
throw new Error("do si do by left shoulder is unsupported.");
}
let doSiDoEndKind: "Start" | "Swap" | "ShortLinesLeft" | "ShortLinesRight";
const doSiDoCircling = this.move.parameters.circling % 360;
if (doSiDoCircling === 0) {
doSiDoEndKind = "Start";
} else if (doSiDoCircling === 180) {
doSiDoEndKind = "Swap";
} else if (doSiDoCircling === 90) {
doSiDoEndKind = this.move.parameters.shoulder ? "ShortLinesLeft" : "ShortLinesRight";
} else if (doSiDoCircling === 270) {
doSiDoEndKind = this.move.parameters.shoulder ? "ShortLinesRight" : "ShortLinesLeft";
} else {
throw new Error("do si do by " + this.move.parameters.circling + " degrees is unsupported.");
}
return this.handleCirclePairedMove(this.move.parameters.who, ({ startPos, around, withPos }) => {
// TODO Use other parameters?
const startingPos: SemanticPosition = {
...startPos,
hands: undefined,
facing: around === "Center"
? Facing.CenterOfCircle
: around === CircleSide.Left || around === CircleSide.Right
? startPos.which.facingUpOrDown()
: startPos.which.facingAcross(),
};
let endPos: SemanticPosition;
switch (doSiDoEndKind) {
case "Start":
endPos = startingPos;
break;
case "Swap":
endPos = { ...withPos, facing: startingPos.facing };
break;
case "ShortLinesLeft":
case "ShortLinesRight":
endPos = {
kind: PositionKind.ShortLines,
which: startPos.which.toShortLines(doSiDoEndKind === "ShortLinesLeft" ? Hand.Left : Hand.Right),
facing: startingPos.facing,
setOffset: startingPos.setOffset,
lineOffset: startingPos.lineOffset,
}
}
return this.combine([{
beats: this.move.beats,
startPosition: startingPos,
endPosition: endPos,
movementPattern: {
kind: SemanticAnimationKind.DoSiDo,
amount: this.move.parameters.circling,
around,
},
}]);
});
}
}
class DoSiDo extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new DoSiDoSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, DoSiDo);

View File

@ -0,0 +1,93 @@
import { Facing, ShortLinesPosition, PositionKind, CirclePosition, SemanticPosition, HandConnection, HandTo, oppositeFacing } 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";
const moveName: Move["move"] = "down the hall";
class DownTheHallSingleVariant extends SingleVariantMoveInterpreter<DownTheHall, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.who !== "everyone") {
throw new Error("Don't know what it means for not everyone to go down the hall.");
}
if (this.move.parameters.moving !== "all") {
throw new Error("Not sure what it means for not all to be moving in down the hall.");
}
if (this.move.parameters.ender !== "turn-alone" && this.move.parameters.ender !== "turn-couple") {
throw new Error("Unsupported down the hall ender: " + this.move.parameters.ender);
}
if (this.move.parameters.facing === "forward then backward") {
throw new Error("Not sure what " + this.move.parameters.facing + " means for down the hall.");
}
return this.handleMove(({ startPos }) => {
const startFacing = this.move.parameters.facing === "backward" ? Facing.Up : Facing.Down;
const startWhich: ShortLinesPosition = startPos.kind === PositionKind.ShortLines
? startPos.which
// TODO Is this always the right way to convert circle to short lines?
// (Does it even matter except for dance starting formations?)
: new Map<CirclePosition, ShortLinesPosition>([
[CirclePosition.TopLeft, ShortLinesPosition.FarLeft],
[CirclePosition.BottomLeft, ShortLinesPosition.MiddleLeft],
[CirclePosition.BottomRight, ShortLinesPosition.MiddleRight],
[CirclePosition.TopRight, ShortLinesPosition.FarRight],
]).get(startPos.which)!;
const startingPos: SemanticPosition & { kind: PositionKind.ShortLines, setOffset: number } = {
kind: PositionKind.ShortLines,
facing: startFacing,
which: startWhich,
hands: startWhich.isMiddle() ? new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }],
[Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }],
]) : new Map<Hand, HandConnection>([
startWhich.isLeft() === (this.move.parameters.facing === "backward")
? [Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }]
: [Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }]
]),
setOffset: startPos.setOffset ?? 0,
lineOffset: startPos.lineOffset,
};
return this.combine([
{
beats: 4,
endPosition: {
...startingPos,
setOffset: startingPos.setOffset + 1
},
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: this.move.beats - 4,
endPosition: {
...startingPos,
setOffset: startingPos.setOffset + 1,
facing: oppositeFacing(startFacing),
which: this.move.parameters.ender === "turn-alone" ? startWhich : startWhich.swapOnSide(),
},
movementPattern: this.move.parameters.ender === "turn-couple"
? {
kind: SemanticAnimationKind.TwirlSwap,
around: startWhich.leftRightSide(),
// !== is NXOR, each of these booleans being flipped flips which hand to use.
hand: startWhich.isMiddle() !== (startWhich.isLeft() !== (this.move.parameters.facing === "forward"))
? Hand.Left
: Hand.Right
}
: {
kind: SemanticAnimationKind.Linear,
minRotation: startWhich.isMiddle() === startWhich.isLeft() ? -180 : +180,
},
},
], startingPos);
});
}
}
class DownTheHall extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new DownTheHallSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, DownTheHall);

View File

@ -0,0 +1,131 @@
import { SemanticPosition, BalanceWeight, ShortLinesPosition, Facing, PositionKind, handsInShortLine, handsInLine } 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";
const moveName: Move["move"] = "form an ocean wave";
class FormAnOceanWaveSingleVariant extends SingleVariantMoveInterpreter<FormAnOceanWave, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.dir !== "across") {
throw new Error("Diagonal ocean waves are unsupported.");
}
const centerHand = this.move.parameters["c.hand"] ? Hand.Right : Hand.Left;
if (this.move.parameters["pass thru"]) {
return this.handleCircleMove(({ id, startPos }) => {
const balBeats = this.move.parameters.bal ? this.move.beats / 2 : 0;
const balPartBeats = balBeats / 2;
// TODO balance direction?
const balance: ((prevEnd: SemanticPosition) => PartialLowLevelMove)[] = this.move.parameters.bal ? [
prevEnd => ({
beats: balPartBeats,
endPosition: { ...prevEnd, balance: BalanceWeight.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
}),
prevEnd => ({
beats: balPartBeats,
endPosition: { ...prevEnd, balance: BalanceWeight.Backward },
movementPattern: { kind: SemanticAnimationKind.Linear },
}),
] : [];
const isCenter = this.findPairOpposite(this.move.parameters.center, id) !== null;
const which = startPos.which.isLeft()
? isCenter
? ShortLinesPosition.MiddleRight
: ShortLinesPosition.FarRight
: isCenter
? ShortLinesPosition.MiddleLeft
: ShortLinesPosition.FarLeft;
// TODO Not sure this facing computation is right.
const facing = (centerHand === Hand.Left) !== isCenter !== which.isLeft() ? Facing.Up : Facing.Down;
return this.combine([{
beats: this.move.beats - balBeats,
endPosition: {
kind: PositionKind.ShortLines,
which,
facing,
hands: handsInShortLine({ which, facing, wavy: true }),
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
},
movementPattern: which.isMiddle()
? { kind: SemanticAnimationKind.Linear }
: {
kind: SemanticAnimationKind.RotateAround,
around: "Center",
minAmount: centerHand === Hand.Left ? 1 : -1,
close: false,
byHand: undefined,
},
}, ...balance], startPos);
});
} else {
return this.handleMove(({ id, startPos }) => {
const isCenter = this.findPairOpposite(this.move.parameters.center, id) !== null;
const which = startPos.which.isLeft()
? (isCenter ? ShortLinesPosition.MiddleLeft : ShortLinesPosition.FarLeft)
: (isCenter ? ShortLinesPosition.MiddleRight : ShortLinesPosition.FarRight);
const facing = (centerHand === Hand.Right) === (which === ShortLinesPosition.MiddleLeft || which === ShortLinesPosition.FarRight) ? Facing.Down : Facing.Up;
const linePos: SemanticPosition = {
kind: PositionKind.ShortLines,
which,
facing,
hands: handsInLine({ wavy: true, which, facing }),
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
};
if (this.move.parameters.bal) {
// TODO Is balance weight always forward/backward here?
const balanceBeats = Math.min(this.move.beats, 4);
const transitionBeats = this.move.beats - balanceBeats;
const balanceForwardBeats = balanceBeats / 2;
const balanceBackwardBeats = balanceBeats - balanceForwardBeats;
const balance: [PartialLowLevelMove, PartialLowLevelMove] = [
{
beats: balanceForwardBeats,
endPosition: { ...linePos, balance: BalanceWeight.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: balanceBackwardBeats,
endPosition: { ...linePos, balance: BalanceWeight.Backward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
];
if (transitionBeats === 0) {
// No transition, just balance.
return this.combine(balance, linePos);
} else {
return this.combine([{
beats: transitionBeats,
endPosition: linePos,
movementPattern: { kind: SemanticAnimationKind.Linear },
},
...balance], startPos);
}
} else {
return this.combine([{
beats: this.move.beats,
endPosition: linePos,
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos);
}
});
}
}
}
class FormAnOceanWave extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new FormAnOceanWaveSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, FormAnOceanWave);

View File

@ -0,0 +1,42 @@
import { SemanticPosition, PositionKind, handsInLine } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
const moveName: Move["move"] = "form long waves";
class FormLongWavesSingleVariant extends SingleVariantMoveInterpreter<FormLongWaves, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
// TODO A zero beat move should just be selecting a variant? Or maybe not because this just changes facing/hands.
if (this.move.beats !== 0) {
throw new Error(this.move.move + " unsupported except for zero beats marking end of previous move.");
}
return this.handleCircleMove(({ id, startPos }) => {
const facingIn = this.findPairOpposite(this.move.parameters.who, id) !== null;
const startAndEndPos: SemanticPosition = {
kind: PositionKind.Circle,
which: startPos.which,
facing: facingIn ? startPos.which.facingAcross() : startPos.which.facingOut(),
hands: handsInLine({ wavy: true, which: startPos.which }),
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
}
return this.combine([{
beats: this.move.beats,
endPosition: startAndEndPos,
movementPattern: { kind: SemanticAnimationKind.StandStill },
}
], startAndEndPos);
});
}
}
class FormLongWaves extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new FormLongWavesSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, FormLongWaves);

56
www/js/moves/giveTake.ts Normal file
View File

@ -0,0 +1,56 @@
import { LongLines, HandConnection, HandTo } 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";
const moveName: Move["move"] = "give & take";
class GiveTakeSingleVariant extends SingleVariantMoveInterpreter<GiveTake, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
const give: boolean = this.move.parameters.give;
const takeToSide = this.move.parameters.who;
const giveBeats = give ? this.move.beats / 2 : 0;
const takeBeats = this.move.beats - giveBeats;
return this.handleCircleMove(({ id, startPos }) => {
const isTaker = this.findPairOpposite(takeToSide, id) !== null;
const maybeGive: [] | [PartialLowLevelMove] = give ? [{
beats: giveBeats,
endPosition: { ...startPos, longLines: LongLines.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear, minRotation: 0 },
}] : [];
const takeHands = new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Right, to: HandTo.DancerForward }],
[Hand.Right, { hand: Hand.Left, to: HandTo.DancerForward }],
]);
return this.combine([...maybeGive, (prevEnd) => ({
beats: takeBeats,
startPosition: { ...prevEnd, hands: undefined },
endPosition: isTaker ? {
...startPos,
which: startPos.which,
longLines: undefined,
hands: takeHands,
} : {
...startPos,
which: startPos.which.swapAcross(),
longLines: LongLines.Near,
hands: takeHands,
},
movementPattern: { kind: SemanticAnimationKind.Linear, minRotation: 0 },
})], startPos);
});
}
}
class GiveTake extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new GiveTakeSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, GiveTake);

339
www/js/moves/hey.ts Normal file
View File

@ -0,0 +1,339 @@
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 { 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;
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++) {
for (const id of withoutOtherPath.keys()) {
const lowLevelMove = withoutOtherPath.get(id)![i];
if (lowLevelMove.movementPattern.kind !== SemanticAnimationKind.PassBy
|| !lowLevelMove.heyStep
|| lowLevelMove.movementPattern.otherPath) {
continue;
}
const heyStepKind = lowLevelMove.heyStep.kind;
let foundPair = false;
for (const otherId of withoutOtherPath.keys()) {
const otherLowLevelMove = withoutOtherPath.get(otherId)![i];
if (id === otherId
|| otherLowLevelMove.movementPattern.kind !== SemanticAnimationKind.PassBy
|| !otherLowLevelMove.heyStep
|| otherLowLevelMove.movementPattern.otherPath) {
continue;
}
const otherHeyStepKind = otherLowLevelMove.heyStep.kind;
if (heyStepKind === "CenterPass" && otherHeyStepKind === "CenterPass"
|| (lowLevelMove.startPosition.which.leftRightSide() === otherLowLevelMove.startPosition.which.leftRightSide()
&& (heyStepKind === "EndsPassIn" && otherHeyStepKind === "EndsPassOut"
|| heyStepKind === "EndsPassOut" && otherHeyStepKind === "EndsPassIn"))) {
lowLevelMove.movementPattern.otherPath = {
start: { ...otherLowLevelMove.startPosition, setOffset: lowLevelMove.startPosition.setOffset, lineOffset: lowLevelMove.startPosition.lineOffset },
end: { ...otherLowLevelMove.endPosition, setOffset: lowLevelMove.endPosition.setOffset, lineOffset: lowLevelMove.endPosition.lineOffset },
}
otherLowLevelMove.movementPattern.otherPath = {
start: { ...lowLevelMove.startPosition, setOffset: otherLowLevelMove.startPosition.setOffset, lineOffset: otherLowLevelMove.startPosition.lineOffset },
end: { ...lowLevelMove.endPosition, setOffset: otherLowLevelMove.endPosition.setOffset, lineOffset: otherLowLevelMove.endPosition.lineOffset },
}
foundPair = true;
break;
}
}
if (!foundPair && heyStepKind === "EndsPassOut") {
// Then other is standing still.
const pos = {
...([...withoutOtherPath.values()]
.map(otherMoves => otherMoves[i])
.filter(m => m.movementPattern.kind === SemanticAnimationKind.StandStill
&& m.endPosition.which.leftRightSide() === lowLevelMove.endPosition.which.leftRightSide())
[0].endPosition),
setOffset: lowLevelMove.startPosition.setOffset, lineOffset: lowLevelMove.startPosition.lineOffset
}
lowLevelMove.movementPattern.otherPath = { start: pos, end: pos };
}
}
for (const id of withoutOtherPath.keys()) {
const lowLevelMove = withoutOtherPath.get(id)![i];
if (lowLevelMove.movementPattern.kind === SemanticAnimationKind.PassBy
&& !lowLevelMove.movementPattern.otherPath) {
throw new Error("Failed to fill in otherPath for " + id + " on hey step " + i);
}
}
}
// Object was mutated.
return withoutOtherPath;
}
return fixupHeyOtherPath(this.handleMove(({ id, startPos }) => {
const endsInCircle = startPos.kind === PositionKind.Circle;
function heyStepToPartialLowLevelMove(heyStep: HeyStep): PartialLowLevelMove & { heyStep: HeyStep } {
return {
beats: heyPartBeats,
// TODO use circle positions on ends? ... unless hey ends in a box the gnat or similar...
endPosition: heyStep.endPosition,
movementPattern: heyStep.kind === "StandStill" ? {
kind: SemanticAnimationKind.StandStill,
} : heyStep.kind === "Loop" ? {
// TODO Loop should probably be its own kind? Or RotateAround?
kind: SemanticAnimationKind.Linear,
minRotation: endsShoulder === Hand.Right ? +180 : -180,
} : heyStep.kind === "Ricochet" ? {
// TODO This is a hack.
kind: SemanticAnimationKind.PassBy,
around: heyStep.endPosition.which.leftRightSide(),
withHands: false,
otherPath: "Swap",
facing: "Start",
side: endsShoulder,
} : {
kind: SemanticAnimationKind.PassBy,
around: heyStep.kind === "CenterPass" ? "Center" : heyStep.endPosition.which.leftRightSide(),
withHands: false,
side: heyStep.kind === "CenterPass" ? centerShoulder : endsShoulder,
facing: "Start",
otherPath: undefined!, // Placeholder, fixup later.
},
heyStep,
};
}
function continueHey(prevStep: HeyStep, stepsLeft: number, beenInCenter: boolean): HeyStep {
// TODO Not sure why type checker requires rechecking this here.
if (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) {
if (prevStep.endPosition.facing === prevStep.endPosition.which.facingAcross()) {
if (stepsLeft === 0) {
return {
kind: "StandStill",
endPosition: prevStep.endPosition,
}
}
return {
kind: "EndsPassIn",
endPosition: {
kind: PositionKind.ShortLines,
which: prevStep.endPosition.which.isLeft() ? ShortLinesPosition.MiddleLeft : ShortLinesPosition.MiddleRight,
facing: prevStep.endPosition.which.facingAcross(),
setOffset: prevStep.endPosition.setOffset,
lineOffset: prevStep.endPosition.lineOffset,
},
}
}
else {
if (stepsLeft === 1 && !endsInCircle) {
return {
kind: "Loop",
endPosition: {
kind: PositionKind.ShortLines,
which: prevStep.endPosition.which.isLeft() ? ShortLinesPosition.FarLeft : ShortLinesPosition.FarRight,
facing: prevStep.endPosition.which.facingAcross(),
setOffset: prevStep.endPosition.setOffset,
lineOffset: prevStep.endPosition.lineOffset,
},
}
}
return {
kind: "Loop",
endPosition: {
...prevStep.endPosition,
which: prevStep.endPosition.which.swapUpAndDown(),
facing: prevStep.endPosition.which.facingAcross()
},
}
}
}
else if (prevStep.endPosition.kind === PositionKind.ShortLines) {
const isFacingSide = prevStep.endPosition.facing === prevStep.endPosition.which.facingSide();
const inMiddle = prevStep.endPosition.which.isMiddle();
if (!inMiddle && !isFacingSide) {
return {
kind: "Loop",
endPosition: { ...prevStep.endPosition, facing: prevStep.endPosition.which.facingSide() },
}
} else if (inMiddle && isFacingSide) {
return {
kind: "EndsPassOut",
endPosition: {
...prevStep.endPosition,
kind: PositionKind.Circle,
which: prevStep.endPosition.which.isLeft()
? (endsShoulder === Hand.Right ? CirclePosition.TopLeft : CirclePosition.BottomLeft)
: (endsShoulder === Hand.Right ? CirclePosition.BottomRight : CirclePosition.TopRight),
},
}
}
else if (!isFacingSide) {
const rico = inCenterFirst
? beenInCenter
? move.parameters.rico3
: move.parameters.rico1
: beenInCenter
? move.parameters.rico4
: move.parameters.rico2;
if (rico) {
const onLeftSide = prevStep.endPosition.which.isLeft();
return {
kind: "Ricochet",
endPosition: {
...prevStep.endPosition,
kind: PositionKind.Circle,
which: CirclePosition.fromSides(prevStep.endPosition.which.leftRightSide(),
// TODO might be swapped
(endsShoulder === Hand.Left) === onLeftSide ? CircleSide.Top : CircleSide.Bottom),
facing: onLeftSide ? Facing.Left : Facing.Right,
}
}
} else {
return {
kind: "CenterPass",
endPosition: {
...prevStep.endPosition,
which: prevStep.endPosition.which.swapSides()
},
}
}
}
else {
return {
kind: inMiddle ? "EndsPassOut" : "EndsPassIn",
endPosition: {
...prevStep.endPosition,
which: prevStep.endPosition.which.swapOnSide()
},
}
}
} else {
throw new Error("Unexpected PositionKind: " + (<any>prevStep.endPosition).kind);
}
}
const inCenterFirst = 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 (startPos.kind !== PositionKind.Circle) {
throw new Error("Hey starting in center not from circle is unsupported.");
}
startingPos = {
kind: startPos.kind,
which: startPos.which,
facing: startPos.which.isLeft() ? Facing.Right : Facing.Left,
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
};
if (inCenterFirst) {
if (this.move.parameters.rico1) {
firstHeyStep = {
kind: "Ricochet",
endPosition: {
kind: PositionKind.Circle,
which: startPos.which.swapUpAndDown(),
facing: startPos.which.facingOut(),
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
}
};
} else {
firstHeyStep = {
kind: "CenterPass",
endPosition: {
kind: PositionKind.ShortLines,
which: startPos.which.isLeft() ? ShortLinesPosition.MiddleRight : ShortLinesPosition.MiddleLeft,
facing: startingPos.facing,
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
}
};
}
} else {
firstHeyStep = {
kind: "StandStill",
endPosition: startingPos,
}
}
} else {
if (startPos.kind !== PositionKind.ShortLines) {
throw new Error("Hey with first pass on ends must start approximately in short lines.");
}
const startFacing = startPos.which.facingSide();
startingPos = {
kind: startPos.kind,
which: startPos.which,
facing: startFacing,
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
};
firstHeyStep = {
kind: startingPos.which.isMiddle() ? "EndsPassOut" : "EndsPassIn",
endPosition: { ...startingPos, which: startPos.which.swapOnSide() },
}
}
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);
beenInCenter ||= nextHeyStep.kind === "CenterPass" || nextHeyStep.kind === "Ricochet";
heySteps.push(nextHeyStep);
}
return this.combine(heySteps.map(heyStepToPartialLowLevelMove), { ...startingPos, hands: undefined });
}));
}
}
class Hey extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new HeySingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, Hey);

55
www/js/moves/longLines.ts Normal file
View File

@ -0,0 +1,55 @@
import { SemanticPosition, Facing, HandConnection, HandTo, LongLines } 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";
const moveName: Move["move"] = "long lines";
class LongLinesSingleVariant extends SingleVariantMoveInterpreter<LongLinesInterpreter, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.handleCircleMove(({ startPos }) => {
const startPosition: SemanticPosition = {
...startPos,
longLines: undefined,
// TODO Occassionally dances have long lines facing out. This will get that wrong.
facing: startPos.which.isLeft() ? Facing.Right : Facing.Left,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }],
[Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }],
]),
};
if (this.move.parameters.go) {
const forwardBeats = this.move.beats / 2;
const backwardBeats = this.move.beats - forwardBeats;
return this.combine([
{
beats: forwardBeats,
endPosition: { ...startPosition, longLines: LongLines.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: backwardBeats,
endPosition: startPosition,
movementPattern: { kind: SemanticAnimationKind.Linear, minRotation: 0 },
},
], startPosition);
} else {
return this.combine([{
beats: this.move.beats,
endPosition: { ...startPosition, longLines: LongLines.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPosition);
}
});
}
}
class LongLinesInterpreter extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new LongLinesSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, LongLinesInterpreter);

46
www/js/moves/madRobin.ts Normal file
View File

@ -0,0 +1,46 @@
import { SemanticPosition, PositionKind } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
const moveName: Move["move"] = "mad robin";
class MadRobinSingleVariant extends SingleVariantMoveInterpreter<MadRobin, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.circling !== 360) {
throw new Error("mad robin circling not exactly once is unsupported.");
}
return this.handleCircleMove(({ id, startPos }) => {
// Read who of mad robin to decide direction.
const madRobinClockwise: boolean = (this.findPairOpposite(this.move.parameters.who, id) !== null) === startPos.which.isOnLeftLookingAcross();
const startAndEndPos: SemanticPosition = {
kind: PositionKind.Circle,
which: startPos.which,
facing: startPos.which.facingAcross(),
setOffset: startPos.setOffset,
lineOffset: startPos.lineOffset,
}
return this.combine([{
beats: this.move.beats,
startPosition: startAndEndPos,
endPosition: startAndEndPos,
movementPattern: {
kind: SemanticAnimationKind.DoSiDo,
amount: madRobinClockwise ? this.move.parameters.circling : -this.move.parameters.circling,
around: startPos.which.leftRightSide(),
},
}]);
});
}
}
class MadRobin extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new MadRobinSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, MadRobin);

35
www/js/moves/passBy.ts Normal file
View File

@ -0,0 +1,35 @@
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";
const moveName: Move["move"] = "pass by";
class PassBySingleVariant extends SingleVariantMoveInterpreter<PassBy, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
const passByShoulder = this.move.parameters.shoulder ? Hand.Left : Hand.Right;
return this.handlePairedMove(this.move.parameters.who, ({ startPos, around, withPos }) => {
return this.combine([{
beats: this.move.beats,
// TODO Is pass by always a swap?
endPosition: withPos,
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around,
facing: "Forward",
otherPath: "Swap",
side: passByShoulder,
withHands: false,
}
}], startPos);
});
}
}
class PassBy extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new PassBySingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, PassBy);

122
www/js/moves/passThrough.ts Normal file
View File

@ -0,0 +1,122 @@
import { CoupleRole } from "../danceCommon.js";
import { Facing, SemanticPosition, PositionKind, CircleSide } 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";
const moveName: Move["move"] = "pass through";
class PassThroughSingleVariant extends SingleVariantMoveInterpreter<PassThrough, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.dir === "left diagonal" || this.move.parameters.dir === "right diagonal") {
// TODO There's logic for this below, but unsure it's right.
throw new Error(this.move.move + " with dir of " + this.move.parameters.dir + " is unsupported.");
}
const alongSet = this.move.parameters.dir === "along";
const passShoulder = this.move.parameters.shoulder ? Hand.Right : Hand.Left;
// Special-case this.
if (this.move.note?.includes("2s shooting the 1s down the center") ?? false) {
return this.handleCircleMove(({ id, startPos }) => {
const facing = id.coupleRole === CoupleRole.Ones ? Facing.Down : Facing.Up;
const endPos: SemanticPosition = {
kind: PositionKind.ShortLines,
which: startPos.which.unfoldToShortLines(CircleSide.Top),
facing,
setOffset: (startPos.setOffset ?? 0) + (facing === Facing.Up ? -0.5 : +0.5),
lineOffset: startPos.lineOffset,
};
return this.combine([{
beats: this.move.beats,
endPosition: endPos,
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around: startPos.which.leftRightSide(),
side: endPos.which.isLeft() ? Hand.Left : Hand.Right,
withHands: false,
facing: "Forward",
otherPath: "Swap",
},
}], startPos);
});
}
return this.handleMove(({ startPos }) => {
if (alongSet && startPos.kind === PositionKind.Circle) {
const facing = startPos.which.facingUpOrDown();
const endPos: SemanticPosition = {
kind: PositionKind.Circle,
which: startPos.which,
facing,
setOffset: (startPos.setOffset ?? 0) + (facing === Facing.Up ? -0.5 : +0.5),
lineOffset: startPos.lineOffset,
};
return this.combine([{
beats: this.move.beats,
endPosition: endPos,
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around: startPos.which.leftRightSide(),
side: passShoulder,
withHands: false,
facing: "Start",
otherPath: "Swap",
},
}], startPos);
} else if (!alongSet && startPos.kind === PositionKind.Circle) {
const facing = startPos.which.facingAcross();
const endPos: SemanticPosition = {
kind: PositionKind.Circle,
which: startPos.which.swapAcross(),
facing,
setOffset: (startPos.setOffset ?? 0) + (this.move.parameters.dir === "across"
? 0
: (this.move.parameters.dir === "left diagonal") === startPos.which.isLeft() ? -1 : +1),
lineOffset: startPos.lineOffset,
};
return this.combine([{
beats: this.move.beats,
endPosition: endPos,
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around: startPos.which.topBottomSide(),
side: passShoulder,
withHands: false,
facing: "Forward",
otherPath: "Swap",
},
}], startPos);
} else if (alongSet && startPos.kind === PositionKind.ShortLines) {
// TODO This assumes short *wavy* lines.
const endPos: SemanticPosition = {
...startPos,
balance: undefined,
// TODO only swap sometimes...
which: startPos.which.swapOnSide(),
setOffset: (startPos.setOffset ?? 0) + (startPos.facing === Facing.Up ? -0.5 : +0.5),
};
return this.combine([{
beats: this.move.beats,
endPosition: endPos,
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos);
} else {
throw new Error(this.move.move + " with dir of " + this.move.parameters.dir + " starting from " + startPos.kind + " is unsupported.");
}
});
}
}
class PassThrough extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new PassThroughSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, PassThrough);

View File

@ -0,0 +1,59 @@
import { Facing, SemanticPosition } from "../interpreterCommon.js";
import { SemanticAnimationKind, LowLevelMove } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
import { balanceCircleInAndOut } from "./balanceTheRing.js";
class PetronellaSingleVariant extends SingleVariantMoveInterpreter<Petronella, "petronella"> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
// TODO These should be actual parameters, not parsing the notes...
const rightShoulder: boolean = !(this.move.note?.includes('left') ?? false);
const newCircle: boolean = this.move.note?.includes('end facing') ?? this.move.progression;
return this.handleCircleMove(({ startPos }) => {
let finalPosition = {
...startPos,
facing: Facing.CenterOfCircle,
which: startPos.which.circleRight(rightShoulder ? 1 : -1),
hands: undefined,
};
if (newCircle) {
finalPosition = {
...finalPosition,
which: finalPosition.which.swapUpAndDown(),
setOffset: (finalPosition.setOffset ?? 0) + (finalPosition.which.isTop() ? -0.5 : +0.5),
}
}
const spin: ((prevEnd: SemanticPosition) => PartialLowLevelMove) = prevEnd => ({
beats: this.move.beats - (this.move.parameters.bal ? 4 : 0),
startPosition: {
...prevEnd,
facing: Facing.CenterOfCircle,
hands: undefined,
},
endPosition: finalPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: rightShoulder ? 180 : -180,
handsDuring: "None",
},
});
if (this.move.parameters.bal) {
const balance: LowLevelMove[] = balanceCircleInAndOut(this.move, startPos);
return PetronellaSingleVariant.append([...balance], spin);
} else {
return this.combine([spin]);
}
});
}
}
class Petronella extends MoveInterpreter<"petronella"> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new PetronellaSingleVariant(this, startingPos);
}
}
moveInterpreters.set("petronella", Petronella);

79
www/js/moves/promenade.ts Normal file
View File

@ -0,0 +1,79 @@
import { DanceRole } from "../danceCommon.js";
import { HandTo, SemanticPosition, DancerDistance, HandConnection, PositionKind } 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";
const moveName: Move["move"] = "promenade";
class PromenadeSingleVariant extends SingleVariantMoveInterpreter<Promenade, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.dir !== "across") {
// TODO "along" would be the bicycle chain? Not sure what left/right diagonal means here.
throw "Promenade not across the set is unsupported."
}
// TODO How to know?
const twirlAfterPromenade = true;
const twirlBeats = twirlAfterPromenade ? this.move.beats / 2 : 0;
const promenadeBeats = this.move.beats - twirlBeats;
return this.handleCirclePairedMove(this.move.parameters.who, ({ startPos }) => {
const handTo = startPos.which.isOnLeftLookingAcross() ? HandTo.DancerRight : HandTo.DancerLeft;
const startingPos: SemanticPosition = {
...startPos,
facing: startPos.which.facingAcross(),
dancerDistance: DancerDistance.Compact,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Left, to: handTo }],
[Hand.Right, { hand: Hand.Right, to: handTo }],
]),
};
const endSetOffset = this.move.progression
? (startPos.setOffset ?? 0) + (startPos.which.isLeft() ? +0.5 : -0.5)
: startPos.setOffset;
const beforeTwirlPos: SemanticPosition & { kind: PositionKind.Circle } = {
...startingPos,
which: startPos.which.swapAcross(),
facing: startPos.which.facingAcross(),
setOffset: endSetOffset,
};
const endPos: SemanticPosition = {
...beforeTwirlPos,
which: beforeTwirlPos.which.swapUpAndDown(),
facing: beforeTwirlPos.which.facingAcross(),
dancerDistance: undefined,
};
const maybeTwirl: [] | [PartialLowLevelMove] = (twirlAfterPromenade ? [{
beats: twirlBeats,
endPosition: endPos,
movementPattern: {
kind: SemanticAnimationKind.CourtesyTurn,
clockwise: this.move.parameters.turn, // TODO or ! this?
}
}] : [])
return this.combine([{
beats: promenadeBeats,
endPosition: beforeTwirlPos,
movementPattern: {
kind: SemanticAnimationKind.Promenade,
swingRole: startPos.which.isOnLeftLookingAcross() ? DanceRole.Lark : DanceRole.Robin,
twirl: true,
passBy: this.move.parameters.turn ? Hand.Left : Hand.Right,
}
}, ...maybeTwirl], startingPos);
});
}
}
class Promenade extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new PromenadeSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, Promenade);

View File

@ -0,0 +1,83 @@
import { HandConnection, HandTo, BalanceWeight } 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";
const moveName: Move["move"] = "pull by dancers";
class PullByDancersSingleVariant extends SingleVariantMoveInterpreter<PullByDancers, typeof moveName> {
moveAsLowLevelMoves(): 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;
// TODO Adjust facing?
const startPosition = {
...startPos,
hands: new Map<Hand, HandConnection>([
[
hand,
{ hand, to: around === "Center" ? HandTo.DiagonalAcrossCircle : HandTo.DancerForward }
]])
};
const passBy: PartialLowLevelMove = {
beats: pullBeats,
endPosition: { ...withPos, facing: startPos.facing },
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around,
side: hand,
withHands: true,
facing: "Start",
otherPath: "Swap",
}
};
if (this.move.parameters.bal) {
return this.combine([
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Forward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Backward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
passBy], startPosition);
} else {
return this.combine([passBy], startPosition);
}
});
}
}
class PullByDancers extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new PullByDancersSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, PullByDancers);

View File

@ -0,0 +1,74 @@
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";
const moveName: Move["move"] = "revolving door";
class RevolvingDoorSingleVariant extends SingleVariantMoveInterpreter<RevolvingDoor, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
const byHand = this.move.parameters.hand ? Hand.Right : Hand.Left;
// TODO More parts? Or define an animation kind?
const waitBeats = 2;
const carryBeats = this.move.beats / 2;
const returnBeats = this.move.beats - carryBeats - waitBeats;
return this.handleCirclePairedMove(this.move.parameters.whom, ({ id, startPos }) => {
const isCarried = this.findPairOpposite(this.move.parameters.who, id) === null;
const startingPos = { ...startPos, hands: undefined };
// TODO animation here needs work.
if (isCarried) {
const endWhich = startPos.which.swapDiagonal();
return this.combine([
prevEnd => ({
beats: waitBeats,
endPosition: prevEnd,
movementPattern: { kind: SemanticAnimationKind.StandStill },
}),
{
beats: carryBeats,
endPosition: {
...startPos,
which: endWhich,
facing: endWhich.facingAcross(),
},
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? 180 : -180,
around: "Center",
byHand,
close: false,
},
},
prevEnd => ({
beats: returnBeats,
endPosition: prevEnd,
movementPattern: { kind: SemanticAnimationKind.StandStill },
}),
], startingPos);
} else {
return this.combine([
{
beats: this.move.beats,
endPosition: startPos,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? 180 : -180,
around: "Center",
byHand,
close: true,
},
},
], startingPos);
}
});
}
}
class RevolvingDoor extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new RevolvingDoorSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, RevolvingDoor);

View File

@ -0,0 +1,52 @@
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";
const moveName: Move["move"] = "right left through";
class RightLeftThroughSingleVariant extends SingleVariantMoveInterpreter<RightLeftThrough, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.dir !== "across") {
throw new Error(this.move.move + " with dir " + this.move.parameters.dir + " is unsupported.");
}
return this.handleCircleMove(({ startPos }) => {
const startingPos = { ...startPos, facing: startPos.which.facingAcross() };
const swappedPos = { ...startingPos, which: startingPos.which.swapAcross() };
return this.combine([
{
beats: this.move.beats / 2,
endPosition: swappedPos,
movementPattern: {
kind: SemanticAnimationKind.PassBy,
side: Hand.Right,
withHands: true,
facing: "Start",
around: startingPos.which.topBottomSide(),
otherPath: "Swap",
},
},
{
beats: this.move.beats / 2,
endPosition: {
...startingPos,
which: startingPos.which.swapDiagonal(),
facing: startingPos.which.facingOut()
},
movementPattern: {
kind: SemanticAnimationKind.CourtesyTurn,
},
}
], startingPos);
});
}
}
class RightLeftThrough extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new RightLeftThroughSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, RightLeftThrough);

74
www/js/moves/rollAway.ts Normal file
View File

@ -0,0 +1,74 @@
import { DanceRole, CoupleRole } from "../danceCommon.js";
import { PositionKind } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
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 }) => {
let isRoller: boolean;
switch (this.move.parameters.who) {
case "gentlespoons":
isRoller = id.danceRole === DanceRole.Lark;
break;
case "ladles":
isRoller = id.danceRole === DanceRole.Robin;
break;
case "ones":
isRoller = id.coupleRole === CoupleRole.Ones;
break;
case "twos":
isRoller = id.coupleRole === CoupleRole.Twos;
break;
case "first corners":
case "second corners":
throw "Roll away in contra corners is unsupported.";
}
// TODO This isn't quite right if there's no 1/2 sash?
let swapPos = withPos;
if (swapPos.kind === PositionKind.Circle && swapPos.longLines) {
swapPos = { ...swapPos, longLines: undefined };
}
// TODO animate hands?
if (isRoller) {
if (this.move.parameters["½sash"]) {
// swap positions by sliding
return this.combine([{
beats: this.move.beats,
endPosition: swapPos,
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos);
} else {
// just stand still
return this.combine([{
beats: this.move.beats,
endPosition: { ...startPos, longLines: undefined },
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos);
}
} else {
// being rolled away, so do a spin
return this.combine([{
beats: this.move.beats,
// TODO Is this the right end position logic?
endPosition: this.move.parameters["½sash"] ? swapPos : { ...startPos, which: startPos.which.swapDiagonal() },
movementPattern: { kind: SemanticAnimationKind.RollAway },
}], startPos);
}
});
}
}
class RollAway extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new RollAwaySingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, RollAway);

76
www/js/moves/roryOMore.ts Normal file
View File

@ -0,0 +1,76 @@
import { PositionKind, SemanticPosition, Facing, BalanceWeight, handsInLine } 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";
const moveName: Move["move"] = "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 })
};
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);
}
const endWhich = startPos.which.shift(roryDir, startPos.facing);
endPos = {
...startPos,
which: endWhich,
hands: handsInLine({ wavy: true, which: endWhich, facing: startPos.facing })
};
} else {
throw new Error(this.move.move + " is currently only supported in short lines.");
}
const maybeBalance: PartialLowLevelMove[] = (this.move.parameters.bal ? [
{
beats: balPartBeats,
endPosition: { ...startingPos, balance: roryDir === Hand.Left ? BalanceWeight.Left : BalanceWeight.Right },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: balPartBeats,
endPosition: { ...startingPos, balance: roryDir === Hand.Left ? BalanceWeight.Right : BalanceWeight.Left },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
] : []);
return this.combine([...maybeBalance,
{
beats: roryBeats,
endPosition: endPos,
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: roryDir === Hand.Right ? +360 : -360,
handsDuring: "None",
},
}
], startingPos);
});
}
}
class RoryOMore extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new RoryOMoreSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, RoryOMore);

56
www/js/moves/slice.ts Normal file
View File

@ -0,0 +1,56 @@
import { SemanticPosition, PositionKind, handToDancerToSideInCircleFacingAcross, LongLines } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
const moveName: Move["move"] = "slice";
class SliceSingleVariant extends SingleVariantMoveInterpreter<Slice, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters["slice increment"] === "dancer") {
// TODO Maybe this only actually gets used to move an entire couple by going diagonal back?
throw new Error("Slicing by a single dancer is unsupported.");
}
const sliceLeft = this.move.parameters.slide;
const sliceReturns = this.move.parameters["slice return"] !== "none";
const sliceForwardBeats = sliceReturns ? this.move.beats / 2 : this.move.beats;
const sliceBackwardBeats = this.move.beats - sliceForwardBeats;
return this.handleCircleMove(({ startPos }) => {
const startingPos: SemanticPosition & { setOffset: number } = {
kind: PositionKind.Circle,
which: startPos.which,
facing: startPos.which.facingAcross(),
hands: handToDancerToSideInCircleFacingAcross(startPos.which),
setOffset: startPos.setOffset ?? 0,
lineOffset: startPos.lineOffset,
};
const sliceAmount = startingPos.which.isLeft() === sliceLeft ? +0.5 : -0.5;
const forwardOffset = startingPos.setOffset + sliceAmount;
const endOffset = this.move.parameters["slice return"] === "diagonal" ? forwardOffset + sliceAmount : forwardOffset;
const sliceForward: PartialLowLevelMove = {
beats: sliceForwardBeats,
endPosition: { ...startingPos, setOffset: forwardOffset, longLines: LongLines.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
};
const maybeSliceBackward: PartialLowLevelMove[] = sliceReturns ? [{
beats: sliceBackwardBeats,
endPosition: { ...startingPos, setOffset: endOffset },
movementPattern: { kind: SemanticAnimationKind.Linear },
}] : [];
return this.combine([sliceForward, ...maybeSliceBackward], startingPos);
});
}
}
class Slice extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new SliceSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, Slice);

View File

@ -0,0 +1,35 @@
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
const moveName : Move["move"] = "slide along set";
class SlideAlongSetSingleVariant extends SingleVariantMoveInterpreter<SlideAlongSet, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
const slideLeft = this.move.parameters.slide;
return this.handleCircleMove(({ startPos }) => {
const startingPos = {
...startPos,
facing: startPos.which.facingAcross(),
};
return this.combine([{
beats: this.move.beats,
endPosition: {
...startingPos,
setOffset: (startingPos.setOffset ?? 0) + (startingPos.which.isLeft() === slideLeft ? +0.5 : -0.5),
},
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startingPos);
});
}
}
class SlideAlongSet extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new SlideAlongSetSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, SlideAlongSet);

49
www/js/moves/star.ts Normal file
View File

@ -0,0 +1,49 @@
import { StarGrip, Facing } 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";
const moveName: Move["move"] = "star";
class StarSingleVariant extends SingleVariantMoveInterpreter<Star, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.handleCircleMove(({ startPos }) => {
const hand = this.move.parameters.hand ? Hand.Right : Hand.Left;
const grip = this.move.parameters.grip === "hands across"
? StarGrip.HandsAcross
: this.move.parameters.grip === "wrist grip"
? StarGrip.WristGrip
: undefined;
const facing = hand === Hand.Left ? Facing.LeftInCircle : Facing.RightInCircle;
const places = this.move.parameters.places / 90 * (hand === Hand.Right ? 1 : -1);
return this.combine([
{
beats: this.move.beats,
endPosition: {
...startPos,
facing,
hands: undefined,
which: startPos.which.circleLeft(places),
},
movementPattern: {
kind: SemanticAnimationKind.Star,
hand,
grip,
places,
}
},
], { ...startPos, facing });
});
}
}
class Star extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new StarSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, Star);

View File

@ -0,0 +1,51 @@
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";
const moveName: Move["move"] = "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 endFacing = endWhich.facingAcross();
return this.combine([{
beats: this.move.beats,
endPosition: {
...startPos,
which: endWhich,
facing: endFacing,
dancerDistance: DancerDistance.Compact,
// TODO Perhaps different hands indication for "scooped"?
hands: handToDancerToSideInCircleFacingAcross(endWhich),
},
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
around: "Center",
byHand: inCenter ? starPromenadeHand : undefined,
close: inCenter,
minAmount: this.move.parameters.hand ? this.move.parameters.circling : -this.move.parameters.circling,
}
}], startPos);
});
}
}
class StarPromenade extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new StarPromenadeSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, StarPromenade);

187
www/js/moves/swing.ts Normal file
View File

@ -0,0 +1,187 @@
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 { SemanticAnimationKind } from "../lowLevelMove.js";
import { Hand } from "../rendererConstants.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
const moveName: Move["move"] = "swing";
class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
// TODO Use variants instead of nextMove
const nextMove = this.moveInterpreter.nextMove;
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.
// 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;
const startWhich = startPos.which;
const startPosition: SemanticPosition = {
...startPos,
facing: around === CircleSide.Left || CircleSide.Right
? (startWhich instanceof CirclePosition
? afterTake
? startPos.longLines === LongLines.Near ? startWhich.facingOut() : startWhich.facingAcross()
: (startWhich.topBottomSide() === CircleSide.Bottom ? Facing.Up : Facing.Down)
: startWhich.facingSide())
: (startWhich.isLeft() ? Facing.Right : Facing.Left),
};
const swingRole = id.danceRole != withId.setIdentity.danceRole
? id.danceRole
// Make some arbitrary choice for same-role swings
: id.coupleRole !== withId.setIdentity.coupleRole
? (id.coupleRole === CoupleRole.Ones ? DanceRole.Lark : DanceRole.Robin)
: withId.relativeSet !== 0
? (withId.relativeSet > 0 ? DanceRole.Lark : DanceRole.Robin)
: withId.relativeLine !== 0
? (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
? [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 = {
...startPos,
kind: PositionKind.ShortLines,
which: endWhich,
facing: endFacing,
balance: undefined,
dancerDistance: undefined,
longLines: undefined,
hands: handsInLine({ wavy: false, which: endWhich, facing: endFacing }),
};
}
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,
};
}
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,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around,
endFacing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole,
afterTake,
},
};
switch (this.move.parameters.prefix) {
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>([
[Hand.Left, { to: HandTo.DancerForward, hand: Hand.Right }],
[Hand.Right, { to: HandTo.DancerForward, hand: Hand.Left }],
]),
};
const balForwardPos = startWithBalHands.kind === PositionKind.Circle
? {
...startWithBalHands,
balance: BalanceWeight.Forward,
dancerDistance: DancerDistance.Compact,
}
: {
...startWithBalHands,
balance: BalanceWeight.Forward,
};
return this.combine([
{
beats: balancePartBeats,
startPosition: startWithBalHands,
endPosition: balForwardPos,
movementPattern: { kind: SemanticAnimationKind.Linear, },
},
prevEnd => ({
beats: balancePartBeats,
endPosition: {
...prevEnd,
balance: BalanceWeight.Backward,
},
movementPattern: { kind: SemanticAnimationKind.Linear, },
}),
swing,
], startPosition);
case "meltdown":
const meltdownBeats = 4; // TODO right number here?
return this.combine([
prevEnd => ({
beats: meltdownBeats,
endPosition: { ...prevEnd, dancerDistance: DancerDistance.Compact },
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: 360,
around,
byHand: undefined,
close: true,
},
}),
swing,
], startPosition);
}
});
}
}
class Swing extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new SwingSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, Swing);

37
www/js/moves/turnAlone.ts Normal file
View File

@ -0,0 +1,37 @@
import { SemanticPosition } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
const moveName: Move["move"] = "turn alone";
class TurnAloneSingleVariant extends SingleVariantMoveInterpreter<TurnAlone, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.who !== "everyone" || this.move.beats !== 0) {
throw new Error("turn alone unsupported except for changing to new circle.");
}
return this.handleCircleMove(({ startPos }) => {
const which = startPos.which.swapUpAndDown();
const startAndEndPos: SemanticPosition = {
...startPos,
which,
facing: which.facingUpOrDown(),
setOffset: (startPos.setOffset ?? 0) + (startPos.which.isTop() ? +0.5 : -0.5),
}
return this.combine([{
beats: this.move.beats,
endPosition: startAndEndPos,
movementPattern: { kind: SemanticAnimationKind.StandStill },
}], startAndEndPos);
});
}
}
class TurnAlone extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new TurnAloneSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, TurnAlone);

95
www/js/moves/upTheHall.ts Normal file
View File

@ -0,0 +1,95 @@
import { Facing, ShortLinesPosition, PositionKind, CirclePosition, SemanticPosition, HandConnection, HandTo, handsInCircle } 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";
const moveName: Move["move"] = "up the hall";
// TODO Share implementation between up/down the hall?
class UpTheHallSingleVariant extends SingleVariantMoveInterpreter<UpTheHall, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
if (this.move.parameters.who !== "everyone") {
throw new Error("Don't know what it means for not everyone to go up the hall.");
}
if (this.move.parameters.moving !== "all") {
throw new Error("Not sure what it means for not all to be moving in up the hall.");
}
if (this.move.parameters.ender !== "circle") {
throw new Error("Unsupported up the hall ender: " + this.move.parameters.ender);
}
if (this.move.parameters.facing !== "forward") {
throw new Error("Unsupported up the hall facing: " + this.move.parameters.facing);
}
return this.handleMove(({ startPos }) => {
const startFacing = this.move.parameters.facing === "backward" ? Facing.Down : Facing.Up;
const startWhich: ShortLinesPosition = startPos.kind === PositionKind.ShortLines
? startPos.which
// TODO Is this always the right way to convert circle to short lines?
// (Does it even matter except for dance starting formations?)
: new Map<CirclePosition, ShortLinesPosition>([
[CirclePosition.TopLeft, ShortLinesPosition.MiddleLeft],
[CirclePosition.BottomLeft, ShortLinesPosition.FarLeft],
[CirclePosition.BottomRight, ShortLinesPosition.FarRight],
[CirclePosition.TopRight, ShortLinesPosition.MiddleRight],
]).get(startPos.which)!;
const startingPos: SemanticPosition & { kind: PositionKind.ShortLines, setOffset: number } = {
kind: PositionKind.ShortLines,
facing: startFacing,
which: startWhich,
hands: startWhich.isMiddle() ? new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }],
[Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }],
]) : new Map<Hand, HandConnection>([
startWhich.isLeft() === (this.move.parameters.facing === "backward")
? [Hand.Right, { hand: Hand.Right, to: HandTo.DancerRight }]
: [Hand.Left, { hand: Hand.Left, to: HandTo.DancerLeft }]
]),
setOffset: startPos.setOffset ?? 0,
lineOffset: startPos.lineOffset,
};
const endWhich = new Map<ShortLinesPosition, CirclePosition>([
[ShortLinesPosition.FarLeft, CirclePosition.TopLeft],
[ShortLinesPosition.MiddleLeft, CirclePosition.BottomLeft],
[ShortLinesPosition.MiddleRight, CirclePosition.BottomRight],
[ShortLinesPosition.FarRight, CirclePosition.TopRight],
]).get(startWhich)!;
const endingPos: SemanticPosition & { kind: PositionKind.Circle } = {
kind: PositionKind.Circle,
which: endWhich,
facing: Facing.CenterOfCircle,
setOffset: startingPos.setOffset - 1,
lineOffset: startingPos.lineOffset,
hands: handsInCircle,
}
return this.combine([
{
beats: 4,
endPosition: {
...startingPos,
setOffset: startingPos.setOffset - 1
},
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: this.move.beats - 4,
endPosition: endingPos,
// TODO Is bend the line just linear?
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: startingPos.which.isLeft() ? -1 : +1
},
}], startingPos);
});
}
}
class UpTheHall extends MoveInterpreter<typeof moveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
return new UpTheHallSingleVariant(this, startingPos);
}
}
moveInterpreters.set(moveName, UpTheHall);

View File

@ -1,99 +1,59 @@
import { Animation } from "./animation.js";
import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "./danceCommon.js";
import * as exampleAnimations from "./exampleAnimations.js";
import { DancerSetPosition, DancersSetPositions, dancerHeight, dancerHeightOffset, leftShoulder, lineDistance, rightShoulder, setDistance, degreesToRadians } from "./rendererConstants.js";
function hueForDancer(identity: DancerIdentity): number {
if (identity.coupleRole == CoupleRole.Ones) {
if (identity.danceRole == DanceRole.Lark) {
return 0; //red
function baseColorForDancer(identity: ExtendedDancerIdentity): {hue: number, sat: number, lum: number} {
if (identity.setIdentity.coupleRole == CoupleRole.Ones) {
if (identity.relativeSet < 0)
return { hue: 340, sat: 67, lum: 56 };
if (identity.relativeSet === 0)
return { hue: 27, sat: 99, lum: 59 };
if (identity.relativeSet > 0)
return { hue: 54, sat: 97, lum: 49 };
} else {
return 39; //orange
}
} else {
if (identity.danceRole == DanceRole.Lark) {
return 240; //blue
} else {
return 180; //teal
}
}
if (identity.relativeSet < 0)
return { hue: 183, sat: 88, lum: 23 };
if (identity.relativeSet === 0)
return { hue: 249, sat: 42, lum: 37 };
if (identity.relativeSet > 0)
return { hue: 13, sat: 33, lum: 29 };
}
const colorForDancerCache = new Map<DancerIdentity, string[]>(
DancerIdentity.all().map(id => [id,
[...Array(49).keys()]
.map(reverseColorCacheKey)
.map(({relativeSet, relativeLine}) => colorForDancer(id, relativeSet, relativeLine))]));
const colorForDancerLabelCache = new Map<DancerIdentity, string[]>(
DancerIdentity.all().map(id => [id,
[...Array(49).keys()]
.map(i => reverseColorCacheKey(i))
.map(({relativeSet, relativeLine}) => colorForDancerLabel(id, relativeSet, relativeLine))]));
function colorCacheKey(relativeSet: number, relativeLine: number) {
relativeSet = Math.max(-3, Math.min(3, relativeSet)) + 3;
relativeLine = Math.max(-3, Math.min(3, relativeLine)) + 3;
return relativeLine * 7 + relativeSet;
}
function reverseColorCacheKey(key: number): { relativeSet: number, relativeLine: number } {
return {
relativeSet: (key % 7) - 3,
relativeLine: Math.floor(key / 7) - 3,
}
throw new Error("Unreachable: relativeSet must be one of <, ===, or > 0.");
}
function colorForDancer(setIdentity: DancerIdentity, relativeSet: number, relativeLine: number) : string {
const hue = hueForDancer(setIdentity);
const sat = 100 - Math.abs(relativeLine * 40);
const unclampedLum = 50 + relativeSet * 20;
function colorForDancer(identity: ExtendedDancerIdentity) : string {
const baseColor = baseColorForDancer(identity);
const hue = baseColor.hue;
const sat = baseColor.sat - Math.abs(identity.relativeLine * 40);
const unclampedLum = baseColor.lum + (Math.abs(identity.relativeSet) <= 1 ? 0 : identity.relativeSet * 20);
const lum = unclampedLum < 10 ? 10 : unclampedLum > 90 ? 90 : unclampedLum;
return `hsl(${hue}, ${sat}%, ${lum}%)`;
}
function colorForDancerCached(setIdentity: DancerIdentity, relativeSet: number, relativeLine: number) : string {
return colorForDancerCache.get(setIdentity)![colorCacheKey(relativeSet, relativeLine)];
}
function colorForDancerById(identity: ExtendedDancerIdentity) : string {
return colorForDancer(identity.setIdentity, identity.relativeSet, identity.relativeLine);
}
function colorForDancerLabel(setIdentity: DancerIdentity, relativeSet: number, relativeLine: number) : string {
const dancerHue = hueForDancer(setIdentity);
const hue = (dancerHue + 180) % 360;
function colorForDancerLabel(identity: ExtendedDancerIdentity) : string {
const dancerColor = baseColorForDancer(identity);
const hue = (dancerColor.hue + 180) % 360;
const sat = 100;
const lum = dancerHue === 240 && relativeSet < 2 || relativeSet < 0
const unclampedLum = ((dancerColor.hue >= 215 && dancerColor.hue <= 285) || (identity.relativeSet < 0)
|| dancerColor.lum < 40) && identity.relativeSet < 2
? 100
: 20 - relativeSet * 40;
: 20 - identity.relativeSet * 40;
const lum = unclampedLum < 0 ? 0 : unclampedLum > 100 ? 100 : unclampedLum;
return `hsl(${hue}, ${sat}%, ${lum}%)`;
}
function colorForDancerLabelCached(setIdentity: DancerIdentity, relativeSet: number, relativeLine: number) : string {
return colorForDancerLabelCache.get(setIdentity)![colorCacheKey(relativeSet, relativeLine)];
}
function colorForDancerLabelById(identity: ExtendedDancerIdentity) : string {
return colorForDancerLabel(identity.setIdentity, identity.relativeSet, identity.relativeLine);
}
const positiveNumberStrings: string[] = [...Array(10).keys()].map(i => i.toString());
const negativeNumberStrings: string[] = [...Array(10).keys()].map(i => (-i).toString());
function cachedToString(smallNumber: number) {
if (Math.abs(smallNumber) >= 10) return smallNumber.toString();
if (smallNumber < 0) return negativeNumberStrings[-smallNumber];
else return positiveNumberStrings[smallNumber];
}
export class Renderer {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
animation?: Animation;
extraSets?: number;
extraLines?: number;
trailIncrements: number = 6;
trailLengthInBeats: number = 1;
drawDebug: boolean = false;
constructor(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
@ -101,33 +61,41 @@ export class Renderer {
this.ctx = ctx;
}
drawDancerBody(setIdentity: DancerIdentity, relativeSet: number, relativeLine: number, drawText: boolean) {
drawDancerBody(identity: ExtendedDancerIdentity, drawText: boolean) {
this.ctx.beginPath();
this.ctx.moveTo(leftShoulder.x, leftShoulder.y);
if (identity.setIdentity.danceRole === DanceRole.Robin) {
// Draw triangle for robin.
this.ctx.lineTo(rightShoulder.x, rightShoulder.y);
this.ctx.lineTo(0, dancerHeight-dancerHeightOffset);
} else {
// Draw dome for lark.
this.ctx.arcTo(0, dancerHeight*2-dancerHeightOffset, rightShoulder.x, rightShoulder.y, dancerHeight * 1.5);
this.ctx.lineTo(rightShoulder.x, rightShoulder.y);
}
this.ctx.fill();
// Draw dot at origin to identify "center" point of dancer.
const backupFillStyle = this.ctx.fillStyle;
this.ctx.fillStyle = 'black';
const pointSize = 0.05;
this.ctx.fillRect(-pointSize/2, -pointSize/2, pointSize, pointSize);
if (drawText) {
if (drawText && identity) {
this.ctx.save();
this.ctx.scale(-1, 1);
this.ctx.rotate(Math.PI);
this.ctx.fillStyle = colorForDancerLabelCached(setIdentity, relativeSet, relativeLine);
this.ctx.fillStyle = colorForDancerLabel(identity);
this.ctx.font = '0.15px sans'
this.ctx.fillText(setIdentity.danceRole === DanceRole.Lark ? 'L' : 'R', -0.14, +0.04);
this.ctx.fillText(setIdentity.coupleRole === CoupleRole.Ones ? '1' : '2', +0.04, +0.04);
this.ctx.fillText(identity.setIdentity.danceRole === DanceRole.Lark ? 'L' : 'R', -0.14, +0.04);
this.ctx.fillText(identity.setIdentity.coupleRole === CoupleRole.Ones ? '1' : '2', +0.04, +0.04);
this.ctx.font = '0.1px sans'
this.ctx.fillText(cachedToString(relativeLine), +0.14, +0.04);
this.ctx.fillText(cachedToString(relativeSet), -0.22, +0.04);
this.ctx.fillText(identity.relativeLine.toString(), +0.14, +0.04);
this.ctx.fillText(identity.relativeSet.toString(), -0.22, +0.04);
this.ctx.restore();
}
@ -140,8 +108,11 @@ export class Renderer {
this.ctx.translate(identity.relativeLine * lineDistance,
identity.relativeSet * setDistance);
const relativeSet = identity.relativeSet + (offsetSets * (identity.setIdentity.coupleRole === CoupleRole.Ones ? 1 : -1));
this.ctx.fillStyle = this.ctx.strokeStyle = colorForDancerCached(identity.setIdentity, relativeSet, identity.relativeLine);
const realIdentity = {
...identity,
relativeSet: identity.relativeSet + (offsetSets * (identity.setIdentity.coupleRole === CoupleRole.Ones ? 1 : -1))
};
this.ctx.fillStyle = this.ctx.strokeStyle = colorForDancer(realIdentity);
if (drawDebug) {
if (this.drawDebug && position.drawDebug) {
@ -154,7 +125,7 @@ export class Renderer {
this.ctx.translate(position.position.x, position.position.y);
this.ctx.rotate(-degreesToRadians(position.rotation));
this.drawDancerBody(identity.setIdentity, relativeSet, identity.relativeLine, drawText);
this.drawDancerBody(realIdentity, drawText);
// Draw arms.
this.ctx.lineWidth = 0.03;
@ -201,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);
@ -246,9 +216,9 @@ export class Renderer {
for (var relativeLine = -extraLines; relativeLine <= extraLines; relativeLine++) {
for (var relativeSet = -extraSets; relativeSet <= extraSets; relativeSet++) {
this.ctx.save();
const hue = (relativeLine + relativeSet) % 2 === 0 ? 60 : 170;
const sat = 100 - Math.abs(relativeLine * 40);
const lum = Math.min(98, 90 + Math.abs(relativeSet) * 5);
const hue = 0;
const sat = 0;
const lum = Math.min(98, 90 + Math.abs(Math.abs(relativeSet) + Math.abs(relativeLine)) * 5);
this.ctx.fillStyle = `hsl(${hue}, ${sat}%, ${lum}%)`;
this.ctx.translate(relativeLine * lineDistance,
relativeSet * setDistance);