Compare commits

...

2 Commits

4 changed files with 578 additions and 23 deletions

View File

@ -4,7 +4,7 @@ import * as common from "./danceCommon.js";
import { Hand, setDistance, setHeight } from "./rendererConstants.js";
import { nameLibFigureParameters, Move, LibFigureDance, chooser_pairz } from "./libfigureMapper.js";
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js";
import { BalanceWeight, CirclePosition, CircleSide, DancerDistance, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "./interpreterCommon.js";
import { BalanceWeight, CirclePosition, CircleSide, CircleSideOrCenter, DancerDistance, Facing, HandConnection, HandTo, LongLines, PositionKind, SemanticPosition } from "./interpreterCommon.js";
const handsInCircle = new Map<Hand, HandConnection>([
@ -139,6 +139,29 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
return res;
}
// If true, dancer is not selected by who so they stand still for the move (or possibly do a meanwhile figure).
function excludedByWho(who: chooser_pairz, id: DancerIdentity): boolean {
switch (who) {
case "partners":
case "neighbors":
case "same roles":
case "shadows":
case "first corners":
case "second corners":
return false;
case "gentlespoons":
return id.danceRole === DanceRole.Robin;
case "ladles":
return id.danceRole === DanceRole.Lark;
case "ones":
return id.coupleRole === CoupleRole.Twos;
case "twos":
return id.coupleRole === CoupleRole.Ones;
default:
throw "Unsupported who: " + who;
}
}
function findPairOpposite(who: chooser_pairz, id: DancerIdentity): common.ExtendedDancerIdentity | null {
switch (who) {
case "partners":
@ -170,16 +193,20 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
}
}
// TOOD Type for this? Probably SemanticPosition?
function findCenterBetween(id: DancerIdentity, other: common.ExtendedDancerIdentity) {
function getPosFor(id: common.ExtendedDancerIdentity) {
const basePos = startingPos.get(id.setIdentity)!;
return {...basePos,
setOffset: (basePos.setOffset ?? 0) + id.relativeSet,
lineOffset: (basePos.lineOffset ?? 0) + id.relativeLine,
};
}
function findCenterBetween(id: DancerIdentity, other: common.ExtendedDancerIdentity): CircleSideOrCenter {
const selfPos = startingPos.get(id)!;
selfPos.setOffset ??= 0;
selfPos.lineOffset ??= 0;
const otherBasePos = startingPos.get(other.setIdentity)!;
const otherPos : SemanticPosition = {...otherBasePos,
setOffset: (otherBasePos.setOffset ?? 0) + other.relativeSet,
lineOffset: (otherBasePos.lineOffset ?? 0) + other.relativeLine,
};
const otherPos = getPosFor(other);
if (selfPos.kind === PositionKind.Circle && otherPos.kind === PositionKind.Circle) {
if (!(selfPos.lineOffset === otherPos.lineOffset && selfPos.setOffset === otherPos.setOffset)) {
@ -198,6 +225,11 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
const res = new Map<DancerIdentity, LowLevelMove[]>();
switch (move.move) {
case "balance the ring":
for (const [id, startPos] of startingPos.entries()) {
res.set(id, balanceCircleInAndOut(move, startPos));
}
return res;
case "petronella":
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
@ -268,6 +300,16 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
return res;
case "swing":
for (const [id, startPos] of startingPos.entries()) {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
// TODO swing can start from other positions.
// TODO swing end is only in notes / looking at next move.
if (startPos.kind !== PositionKind.Circle) {
@ -400,7 +442,18 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
return res;
case "allemande":
case "gyre":
for (const [id, startPos] of startingPos.entries()) {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
// TODO allemande can start from other positions.
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
@ -409,18 +462,12 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
const withId = findPairOpposite(move.parameters.who, id);
// findPairOpposite of null means this dancer doesn't participate in this move.
if (!withId) {
res.set(id, combine([prevEnd => ({
beats: move.beats,
startPosition: { ...prevEnd, hands: undefined },
endPosition: { ...prevEnd, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
})], startPos));
continue;
};
throw "fairPairOpposite and excludedByWho disagree on who does not act: who=" + move.parameters.who + ", id=" + id;
}
const around = findCenterBetween(id, withId);
// TODO Not sure if this is right.
const byHand = move.parameters.hand ? Hand.Right : Hand.Left;
const byHandOrShoulder = (move.move === "allemande" ? move.parameters.hand : move.parameters.shoulder) ? Hand.Right : Hand.Left;
const swap = move.parameters.circling % 360 === 180;
if (!swap && move.parameters.circling % 360 !== 0) {
throw "Unsupported allemande circle amount: " + move.parameters.circling;
@ -438,9 +485,9 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
endPosition,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? move.parameters.circling : -move.parameters.circling,
minAmount: byHandOrShoulder === Hand.Right ? move.parameters.circling : -move.parameters.circling,
around,
byHand,
byHand: move.move === "allemande" ? byHandOrShoulder : undefined,
},
},
], startPos));
@ -457,10 +504,11 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
}
res.set(id, combine([
prevEnd => ({
{
beats: move.beats,
endPosition: {
...prevEnd,
...startPos,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
which: startPos.which.circleLeft(places),
},
@ -468,7 +516,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
kind: SemanticAnimationKind.Circle,
places,
}
}),
},
], { ...startPos, facing: Facing.CenterOfCircle }));
}
@ -476,11 +524,24 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
case "California twirl":
for (const [id, startPos] of startingPos.entries()) {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + 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?
res.set(id, combine([
{
beats: 1,
@ -510,6 +571,380 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
}], startPos));
}
return res;
case "box the gnat":
for (const [id, startPos] of startingPos.entries()) {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
const hand = move.parameters.hand ? Hand.Right : Hand.Left;
const balanceBeats = move.parameters.bal
? move.beats > 4
? move.beats - 4
: 2
: 0;
const balancePartBeats = balanceBeats / 2;
const twirlBeats = move.beats - balanceBeats;
// TODO Adjust facing?
const startPosition = { ...startPos, hands: new Map<Hand, HandConnection>([[hand, { hand, to: HandTo.DancerForward }]]) };
const withId = findPairOpposite(move.parameters.who, id);
// findPairOpposite of null means this dancer doesn't participate in this move.
if (!withId) {
throw "fairPairOpposite and excludedByWho disagree on who does not act: who=" + move.parameters.who + ", id=" + id;
}
const around = findCenterBetween(id, withId);
if (around === "Center") {
throw "TwirlSwap around center is unsupported.";
}
if (move.parameters.bal) {
res.set(id, combine([
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Forward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Backward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: twirlBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around,
hand,
}
}], startPosition));
} else {
res.set(id, combine([
{
beats: twirlBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around,
hand,
}
}], startPosition));
}
}
return res;
case "pull by dancers":
for (const [id, startPos] of startingPos.entries()) {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
const hand = move.parameters.hand ? Hand.Right : Hand.Left;
const balanceBeats = move.parameters.bal
? move.beats > 4
? move.beats - 4
: 2
: 0;
const balancePartBeats = balanceBeats / 2;
const pullBeats = move.beats - balanceBeats;
const withId = findPairOpposite(move.parameters.who, id);
// findPairOpposite of null means this dancer doesn't participate in this move.
if (!withId) {
throw "fairPairOpposite and excludedByWho disagree on who does not act: who=" + move.parameters.who + ", id=" + id;
}
const around = findCenterBetween(id, withId);
// TODO Adjust facing?
const startPosition = {
...startPos,
hands: new Map<Hand, HandConnection>([
[
hand,
{ hand, to: around === "Center" ? HandTo.DiagonalAcrossCircle : HandTo.DancerForward }
]])
};
if (move.parameters.bal) {
res.set(id, combine([
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Forward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: balancePartBeats,
endPosition: {
...startPosition,
balance: BalanceWeight.Backward,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
{
beats: pullBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around,
side: hand,
withHands: true,
}
}], startPosition));
} else {
res.set(id, combine([
{
beats: pullBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around,
side: hand,
withHands: true,
}
}], startPosition));
}
}
return res;
case "chain":
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
const mainRole = move.parameters.who === "gentlespoons" ? DanceRole.Lark : DanceRole.Robin;
const pullBeats = move.beats / 2;
const turnBeats = move.beats - pullBeats;
if (id.danceRole === mainRole) {
const hand = move.parameters.hand ? Hand.Right : Hand.Left;
const endWhich = startPos.which.swapDiagonal();
let endSet = startPos.setOffset ?? 0;
let to : HandTo;
switch (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>([[hand, { hand, to }]]) };
const turnTo = hand === Hand.Right ? HandTo.DancerRight : HandTo.DancerLeft;
res.set(id, combine([
{
beats: pullBeats,
endPosition: {
...startPos,
which: endWhich,
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: hand,
withHands: true,
}
},
prevEnd => ({
beats: turnBeats,
endPosition: {
kind: PositionKind.Circle,
which: endWhich.swapUpAndDown(),
facing: endWhich.isLeft() ? Facing.Right : Facing.Left,
hands: prevEnd.hands,
setOffset: prevEnd.setOffset,
lineOffset: prevEnd.lineOffset,
},
movementPattern: {
kind: SemanticAnimationKind.CourtesyTurn,
}
})
], startPosition));
} else {
res.set(id, combine([
{
beats: pullBeats,
endPosition: startPos,
movementPattern: {
kind: SemanticAnimationKind.StandStill,
}
},
{
beats: turnBeats,
endPosition: {
...startPos,
// TODO Does CourtesyTurn always swap?
which: startPos.which.swapUpAndDown(),
},
movementPattern: {
kind: SemanticAnimationKind.CourtesyTurn,
}
}
], startPos));
}
}
return res;
case "long lines":
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
const 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 (move.parameters.go) {
const forwardBeats = move.beats / 2;
const backwardBeats = move.beats - forwardBeats;
res.set(id, combine([
{
beats: forwardBeats,
endPosition: { ...startPosition, longLines: LongLines.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: backwardBeats,
endPosition: startPosition,
movementPattern: { kind: SemanticAnimationKind.Linear },
},
], startPosition));
} else {
res.set(id, combine([{
beats: move.beats,
endPosition: { ...startPosition, longLines: LongLines.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPosition));
}
}
return res;
case "roll away":
for (const [id, startPos] of startingPos.entries()) {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
let isRoller: boolean;
switch (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.";
}
let oppositeId = findPairOpposite(move.parameters.whom, id);
if (!oppositeId) {
// TODO
throw "Failed to identify dancer to roll away with.";
}
// TODO This isn't quite right if there's no 1/2 sash?
const oppositePos = getPosFor(oppositeId);
let swapPos = oppositePos;
if (swapPos.kind === PositionKind.Circle && swapPos.longLines) {
swapPos = { ...swapPos, longLines: undefined };
}
// TODO animate hands?
if (isRoller) {
if (move.parameters["½sash"]) {
// swap positions by sliding
res.set(id, combine([{
beats: move.beats,
endPosition: swapPos,
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos));
} else {
// just stand still
res.set(id, combine([{
beats: move.beats,
endPosition: { ...startPos, longLines: undefined },
movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos));
}
} else {
// being rolled away, so do a spin
res.set(id, combine([{
beats: move.beats,
// TODO Is this the right end position logic?
endPosition: move.parameters["½sash"] ? swapPos : { ...startPos, which: startPos.which.swapDiagonal() },
movementPattern: { kind: SemanticAnimationKind.RollAway },
}], startPos));
}
}
return res;
}

View File

@ -95,6 +95,10 @@ export class CirclePosition {
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public swapDiagonal() : CirclePosition {
return this.swapAcross().swapUpAndDown();
}
public facingCenterRotation() : Rotation {
return (45 + 90 * CirclePosition.enumValueToNumber(this.enumValue)) % 360;
}
@ -132,6 +136,79 @@ export class CirclePosition {
}
}
enum ShortLinesPositionEnum {
FarLeft = "FarLeft",
MiddleLeft = "MiddleLeft",
MiddleRight = "MiddleRight",
FarRight = "FarRight",
}
export class ShortLinesPosition {
public static readonly FarLeft = new ShortLinesPosition(ShortLinesPositionEnum.FarLeft);
public static readonly MiddleLeft = new ShortLinesPosition(ShortLinesPositionEnum.MiddleLeft);
public static readonly MiddleRight = new ShortLinesPosition(ShortLinesPositionEnum.MiddleRight);
public static readonly FarRight = new ShortLinesPosition(ShortLinesPositionEnum.FarRight);
private readonly enumValue: ShortLinesPositionEnum;
private constructor(enumValue: ShortLinesPositionEnum) {
this.enumValue = enumValue;
}
private static enumValueToNumber(enumValue: ShortLinesPositionEnum) : number {
switch (enumValue) {
case ShortLinesPositionEnum.FarLeft:
return 0;
case ShortLinesPositionEnum.MiddleLeft:
return 1;
case ShortLinesPositionEnum.MiddleRight:
return 2;
case ShortLinesPositionEnum.FarRight:
return 3;
}
}
private static numberToEnumValue(num: number) : ShortLinesPositionEnum {
if (num < 0 || num > 3) {
num %= 4;
if (num < 0) num += 4;
}
return [
ShortLinesPositionEnum.FarLeft,
ShortLinesPositionEnum.MiddleLeft,
ShortLinesPositionEnum.MiddleRight,
ShortLinesPositionEnum.FarRight,
][num];
}
private static get(enumValue: ShortLinesPositionEnum) : ShortLinesPosition {
return [
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.MiddleRight,
ShortLinesPosition.FarRight,
][this.enumValueToNumber(enumValue)];
}
public isMiddle() : boolean {
return this.enumValue === ShortLinesPositionEnum.MiddleRight || this.enumValue === ShortLinesPositionEnum.MiddleLeft;
}
public leftRightSide() : CircleSide.Left | CircleSide.Right {
return this.enumValue === ShortLinesPositionEnum.FarLeft || this.enumValue === ShortLinesPositionEnum.MiddleLeft
? CircleSide.Left
: CircleSide.Right;
}
public isLeft() : boolean {
return this.leftRightSide() === CircleSide.Left;
}
public toString() : string {
return this.enumValue.toString();
}
}
export enum Facing {
CenterOfCircle = "CenterOfCircle",
Up = "Up",
@ -143,6 +220,9 @@ export enum HandTo {
LeftInCircle = "LeftInCircle",
RightInCircle = "RightInCircle",
AcrossCircle = "AcrossCircle",
DiagonalAcrossCircle = "DiagonalAcrossCircle",
LeftDiagonalAcrossCircle = "LeftDiagonalAcrossCircle",
RightDiagonalAcrossCircle = "RightDiagonalAcrossCircle",
DancerForward = "DancerForward",
DancerLeft = "DancerLeft",
DancerRight = "DancerRight",
@ -165,6 +245,9 @@ export enum DancerDistance {
SwingLark = "SwingLark",
SwingRobin = "SwingRobin",
}
export enum LongLines {
Forward = "Forward",
}
export type SemanticPosition = {
kind: PositionKind.Circle,
setOffset?: number,
@ -174,10 +257,13 @@ export type SemanticPosition = {
hands?: Map<Hand, HandConnection>,
balance?: BalanceWeight,
dancerDistance?: DancerDistance,
longLines?: LongLines,
} | {
kind: PositionKind.ShortLines,
setOffset?: number,
lineOffset?: number,
which: ShortLinesPosition,
facing: Facing,
hands?: Map<Hand, HandConnection>,
balance?: BalanceWeight,
};

View File

@ -28,6 +28,11 @@ export enum SemanticAnimationKind {
RotateAround = "RotateAround",
Swing = "Swing",
CourtesyTurn = "CourtesyTurn",
// Dancer being rolled away / spinning. Other dancer is just Linear.
RollAway = "RollAway",
}
export type SemanticAnimation = {
kind: SemanticAnimationKind.StandStill,
@ -77,6 +82,19 @@ export type SemanticAnimation = {
around: CircleSide,
hand: Hand,
} | {
kind: SemanticAnimationKind.PassBy,
around: CircleSideOrCenter,
side: Hand,
// If true, pull by the specified hand, if false, just pass by that side without hands.
withHands: boolean,
} | {
kind: SemanticAnimationKind.CourtesyTurn,
} | {
kind: SemanticAnimationKind.RollAway,
}
@ -253,6 +271,9 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
const xSign = connection.to === HandTo.LeftInCircle ? -1 : +1;
const balanceFactor = semantic.balance === BalanceWeight.Forward ? 1-balanceAmount : 1;
return { x: balanceFactor*xSign/Math.sqrt(2), y: balanceFactor/Math.sqrt(2) };
case HandTo.DiagonalAcrossCircle:
// TODO Is "diagonal" even enough information?
return { x: -0.5, y: +0.5 }
default:
throw "Unkown connection: " + connection.to;
}
@ -494,6 +515,17 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
endTransitionBeats: 1,
})
];
// TODO Unsupported moves, just doing linear for now.
case SemanticAnimationKind.PassBy:
case SemanticAnimationKind.CourtesyTurn:
case SemanticAnimationKind.RollAway:
return [
new animation.LinearAnimationSegment({
beats: move.beats,
startPosition: startSetPosition,
endPosition: endSetPosition,
})
]
}
throw "Unsupported move in animateLowLevelMove: " + JSON.stringify(move);

View File

@ -143,7 +143,9 @@ export class Renderer {
const trailLengthInBeats = 1;
const incrementLength = trailLengthInBeats / increments;
progression ??= 0;
const offsetSets = -((progression - (progression % 2)) / 2) / ((this.animation.progression.y * 2) / setDistance);
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;