Compare commits

...

2 Commits

Author SHA1 Message Date
f533e3d202 Improved swing animation. 2023-08-23 17:43:25 -07:00
ea9871d545 Make debug output slightly easier to read. 2023-08-23 16:32:53 -07:00
4 changed files with 132 additions and 20 deletions

View File

@ -40,6 +40,11 @@
} }
table, tr, th, td { table, tr, th, td {
border: solid black 1px; border: solid black 1px;
vertical-align: top;
}
table pre {
position: sticky;
top: 0;
} }
th.Ones.Lark { th.Ones.Lark {
background-color: hsl(0, 80%, 50%); background-color: hsl(0, 80%, 50%);

View File

@ -1,4 +1,4 @@
import { DancerIdentity, normalizeRotation } from "./danceCommon.js"; import { DancerIdentity, Rotation, normalizeRotation } from "./danceCommon.js";
import { DancerSetPosition, DancersSetPositions, Hand } from "./rendererConstants.js"; import { DancerSetPosition, DancersSetPositions, Hand } from "./rendererConstants.js";
import { Offset, leftShoulder, rightShoulder, degreesToRadians, radiansToDegrees } from "./rendererConstants.js"; import { Offset, leftShoulder, rightShoulder, degreesToRadians, radiansToDegrees } from "./rendererConstants.js";
@ -82,6 +82,7 @@ export class LinearAnimationSegment extends AnimationSegment {
export enum RotationAnimationFacing { export enum RotationAnimationFacing {
Linear = "Linear", // Default, linearly interpolate. Linear = "Linear", // Default, linearly interpolate.
Center = "Center", // Always face the center. Center = "Center", // Always face the center.
CenterRelative = "CenterRelative", // Always face the center.
Forward = "Forward", // Always face the direction of the rotation. Forward = "Forward", // Always face the direction of the rotation.
Backward = "Backward", // Opposite of forward. Backward = "Backward", // Opposite of forward.
} }
@ -95,6 +96,7 @@ export class RotationAnimationSegment extends AnimationSegment {
private readonly endRotation: number; private readonly endRotation: number;
private readonly center: Offset; private readonly center: Offset;
private readonly facing: RotationAnimationFacing; private readonly facing: RotationAnimationFacing;
private readonly startFacing: Rotation;
constructor(beats: number, startPosition: DancerSetPosition, endPosition: DancerSetPosition, constructor(beats: number, startPosition: DancerSetPosition, endPosition: DancerSetPosition,
rotation: number, around: RotationAround, facing: RotationAnimationFacing) { rotation: number, around: RotationAround, facing: RotationAnimationFacing) {
@ -113,6 +115,7 @@ export class RotationAnimationSegment extends AnimationSegment {
return -radiansToDegrees(radians); return -radiansToDegrees(radians);
} }
this.startRotation = positionToDegrees(startPosition.position); this.startRotation = positionToDegrees(startPosition.position);
this.startFacing = startPosition.rotation;
const actualRotation = normalizeRotation(positionToDegrees(endPosition.position) - this.startRotation, const actualRotation = normalizeRotation(positionToDegrees(endPosition.position) - this.startRotation,
rotation); rotation);
this.endRotation = this.startRotation + actualRotation; this.endRotation = this.startRotation + actualRotation;
@ -142,6 +145,8 @@ export class RotationAnimationSegment extends AnimationSegment {
switch (this.facing) { switch (this.facing) {
case RotationAnimationFacing.Center: case RotationAnimationFacing.Center:
return degrees - 90; return degrees - 90;
case RotationAnimationFacing.CenterRelative:
return degrees - this.startRotation + this.startFacing;
case RotationAnimationFacing.Forward: case RotationAnimationFacing.Forward:
return degrees + 180; return degrees + 180;
case RotationAnimationFacing.Backward: case RotationAnimationFacing.Backward:

View File

@ -160,6 +160,14 @@ enum BalanceWeight {
Left = "Left", Left = "Left",
Right = "Right", Right = "Right",
} }
enum DancerDistance {
Normal = "Normal",
Compact = "Compact", // allemande, etc.
// Swings asymmetrical, but some dances may have the Lark do a swing as a Robin or vice versa,
// especially any dance with Larks Swing or Robins Swing.
SwingLark = "SwingLark",
SwingRobin = "SwingRobin",
}
type SemanticPosition = { type SemanticPosition = {
kind: PositionKind.Circle, kind: PositionKind.Circle,
setOffset?: number, setOffset?: number,
@ -168,7 +176,7 @@ type SemanticPosition = {
facing: Facing, facing: Facing,
hands?: Map<Hand, HandConnection>, hands?: Map<Hand, HandConnection>,
balance?: BalanceWeight, balance?: BalanceWeight,
sideCompact?: boolean, // if true, have moved in for swing/allemande/etc. dancerDistance?: DancerDistance,
} | { } | {
kind: PositionKind.ShortLines, kind: PositionKind.ShortLines,
setOffset?: number, setOffset?: number,
@ -205,8 +213,10 @@ enum SemanticAnimationKind {
// California twirl, box the gnat, etc. // California twirl, box the gnat, etc.
TwirlSwap = "TwirlSwap", TwirlSwap = "TwirlSwap",
// Rotate as a pair around a point. Allemande, right/left shoulder round, maybe swing? // Rotate as a pair around a point. Allemande, right/left shoulder round
RotateAround = "RotateAround", RotateAround = "RotateAround",
Swing = "Swing",
} }
type SemanticAnimation = { type SemanticAnimation = {
kind: SemanticAnimationKind.StandStill, kind: SemanticAnimationKind.StandStill,
@ -235,6 +245,14 @@ type SemanticAnimation = {
minAmount: number, minAmount: number,
around: CircleSide | "Center", around: CircleSide | "Center",
} | {
kind: SemanticAnimationKind.Swing,
minAmount: number,
around: CircleSide | "Center",
endFacing: Facing,
} | { } | {
kind: SemanticAnimationKind.TwirlSwap, kind: SemanticAnimationKind.TwirlSwap,
} }
@ -556,23 +574,25 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
: startPos.which.swapUpAndDown(), : startPos.which.swapUpAndDown(),
facing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left, facing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
balance: undefined, balance: undefined,
sideCompact: undefined, dancerDistance: undefined,
hands: new Map<Hand, HandConnection>([id.danceRole === DanceRole.Lark hands: new Map<Hand, HandConnection>([id.danceRole === DanceRole.Lark
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }] ? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]), : [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
}; };
// TODO same role swing currently unsupported.
const swingRole = id.danceRole === DanceRole.Lark ? DancerDistance.SwingLark : DancerDistance.SwingRobin;
switch (move.parameters.prefix) { switch (move.parameters.prefix) {
case "none": case "none":
res.set(id, combine([ res.set(id, combine([
{ {
beats: 1, beats: 1,
endPosition: { ...startPosition, sideCompact: true }, endPosition: { ...startPosition, dancerDistance: DancerDistance.Compact },
movementPattern: { kind: SemanticAnimationKind.Linear, }, movementPattern: { kind: SemanticAnimationKind.Linear, },
}, },
{ {
beats: move.beats - 2, beats: move.beats - 2,
endPosition: { ...endPosition, sideCompact: true }, endPosition: { ...endPosition, dancerDistance: DancerDistance.Compact },
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.RotateAround, kind: SemanticAnimationKind.RotateAround,
minAmount: 360, minAmount: 360,
@ -603,7 +623,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
endPosition: { endPosition: {
...startWithBalHands, ...startWithBalHands,
balance: BalanceWeight.Forward, balance: BalanceWeight.Forward,
sideCompact: true, dancerDistance: DancerDistance.Compact,
}, },
movementPattern: { kind: SemanticAnimationKind.Linear, }, movementPattern: { kind: SemanticAnimationKind.Linear, },
}, },
@ -620,16 +640,18 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
endPosition: { endPosition: {
...prevEnd, ...prevEnd,
balance: undefined, balance: undefined,
dancerDistance: swingRole,
}, },
movementPattern: { kind: SemanticAnimationKind.Linear, }, movementPattern: { kind: SemanticAnimationKind.Linear, },
}), }),
{ {
beats: move.beats - 2 * balancePartBeats - 1, beats: move.beats - 2 * balancePartBeats - 1,
endPosition: { ...endPosition, sideCompact: true }, endPosition: { ...endPosition, dancerDistance: DancerDistance.Compact },
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.RotateAround, kind: SemanticAnimationKind.Swing,
minAmount: 360, minAmount: 360,
around: startPos.which.leftRightSide(), around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
}, },
}, },
{ {
@ -648,7 +670,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
beats: meltdownBeats, beats: meltdownBeats,
endPosition: { endPosition: {
...prevEnd, ...prevEnd,
sideCompact: true, dancerDistance: swingRole,
}, },
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.RotateAround, kind: SemanticAnimationKind.RotateAround,
@ -660,9 +682,10 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
beats: move.beats - meltdownBeats, beats: move.beats - meltdownBeats,
endPosition: endPosition, endPosition: endPosition,
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.RotateAround, kind: SemanticAnimationKind.Swing,
minAmount: 360, minAmount: 360,
around: startPos.which.leftRightSide(), around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
}, },
}, },
], startPos)); ], startPos));
@ -1025,7 +1048,22 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
default: default:
throw "Unexpected facing: " + semantic.facing; throw "Unexpected facing: " + semantic.facing;
} }
const yAmount = semantic.sideCompact ? 0.75 : 1; let yAmount : number;
switch (semantic.dancerDistance) {
case DancerDistance.Normal:
case undefined:
yAmount = 1;
break;
case DancerDistance.Compact:
yAmount = 0.75;
break;
case DancerDistance.SwingLark:
case DancerDistance.SwingRobin:
yAmount = 0.5;
break;
default:
throw "Unknown DancerDistance: " + semantic.dancerDistance;
}
let position: Offset; let position: Offset;
switch (semantic.which) { switch (semantic.which) {
case CirclePosition.TopLeft: case CirclePosition.TopLeft:
@ -1066,6 +1104,35 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
position.x += setOffset.x; position.x += setOffset.x;
position.y += setOffset.y; position.y += setOffset.y;
const isFacingUpOrDown = semantic.facing === Facing.Up || semantic.facing === Facing.Down;
if (semantic.dancerDistance === DancerDistance.SwingLark) {
if (isFacingUpOrDown) {
rotation -= 45;
} else {
rotation += 45;
}
return {
position,
rotation,
leftArmEnd: { x: 0, y: Math.sqrt(1/2)},
rightArmEnd: { x: 0.8, y: 0.5 },
};
} else if (semantic.dancerDistance === DancerDistance.SwingRobin) {
if (isFacingUpOrDown) {
rotation += 45;
} else {
rotation -= 45;
}
return {
position,
rotation,
leftArmEnd: { x: -0.8, y: 0.5 },
rightArmEnd: { x: 0, y: Math.sqrt(1/2) },
};
}
function processHand(hand: Hand, connection: HandConnection | undefined): Offset | undefined { function processHand(hand: Hand, connection: HandConnection | undefined): Offset | undefined {
// TODO Hands. Might need more info? Or need context of nearby dancer SemanticPositions? // TODO Hands. Might need more info? Or need context of nearby dancer SemanticPositions?
if (!connection) return undefined; if (!connection) return undefined;
@ -1095,8 +1162,8 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
return { return {
position, position,
rotation, rotation,
leftArmEnd: processHand(Hand.Left, semantic.hands?.get(Hand.Left)) leftArmEnd: processHand(Hand.Left, semantic.hands?.get(Hand.Left)),
rightArmEnd: processHand(Hand.Right, semantic.hands?.get(Hand.Right)) rightArmEnd: processHand(Hand.Right, semantic.hands?.get(Hand.Right)),
}; };
default: default:
throw "Unsupported PositionKind: " + semantic.kind; throw "Unsupported PositionKind: " + semantic.kind;
@ -1174,7 +1241,7 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
// TODO Parameters: rotation, amount, direction, diagonal? // TODO Parameters: rotation, amount, direction, diagonal?
if (move.startPosition.kind !== PositionKind.Circle) { if (move.startPosition.kind !== PositionKind.Circle) {
throw "DoSoDo must start in a circle: " + JSON.stringify(move); throw "DoSiDo must start in a circle: " + JSON.stringify(move);
} }
const center = CenterOfSideOfSet(move.startPosition.which.leftRightSide(), move.startPosition.setOffset, move.startPosition.lineOffset); const center = CenterOfSideOfSet(move.startPosition.which.leftRightSide(), move.startPosition.setOffset, move.startPosition.lineOffset);
return [ return [
@ -1195,7 +1262,7 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
// TODO Parameters: rotation, amount, direction, diagonal? // TODO Parameters: rotation, amount, direction, diagonal?
if (move.startPosition.kind !== PositionKind.Circle) { if (move.startPosition.kind !== PositionKind.Circle) {
throw "DoSoDo must start in a circle: " + JSON.stringify(move); throw "Rotate must start in a circle: " + JSON.stringify(move);
} }
// TODO Could rotate around other things. // TODO Could rotate around other things.
const rotateCenter = move.movementPattern.around === "Center" const rotateCenter = move.movementPattern.around === "Center"
@ -1214,6 +1281,36 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
animation.RotationAnimationFacing.Center, animation.RotationAnimationFacing.Center,
), ),
]; ];
case SemanticAnimationKind.Swing:
// TODO Handle swing.
// TODO Rotation
// TODO Parameters: rotation, amount, direction, diagonal?
if (move.startPosition.kind !== PositionKind.Circle || move.endPosition.kind !== PositionKind.Circle) {
throw "Swing must start in a circle: " + JSON.stringify(move);
}
// TODO Could rotate around other things.
const rotateSwingCenter = move.movementPattern.around === "Center"
? CenterOfSet(move.startPosition.setOffset, move.startPosition.lineOffset)
: CenterOfSideOfSet(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
const beforeUnfold = SemanticToSetPosition({...move.endPosition, dancerDistance: move.startPosition.dancerDistance });
return [
new animation.RotationAnimationSegment(move.beats,
startSetPosition,
beforeUnfold,
move.movementPattern.minAmount,
{
center: rotateSwingCenter,
width: setWidth / 5,
height: setHeight / 5,
},
animation.RotationAnimationFacing.CenterRelative,
),
new animation.LinearAnimationSegment(move.beats,
beforeUnfold,
endSetPosition,
),
];
case SemanticAnimationKind.TwirlSwap: case SemanticAnimationKind.TwirlSwap:
// TODO // TODO
const twirlRotation : number = 180; const twirlRotation : number = 180;

View File

@ -79,10 +79,15 @@ function createJsonCell(content: any, rowSpan?: number, id?: DancerIdentity) {
// from https://stackoverflow.com/a/56150320 // from https://stackoverflow.com/a/56150320
function replacer(key, value) { function replacer(key, value) {
if (value instanceof Map) { if (value instanceof Map) {
return { let res = {};
dataType: 'Map', for (var entry of value.entries()) {
value: Array.from(value.entries()), // or with spread: value: [...value] res[entry[0].toString()] = entry[1];
}; }
return res;
} else if (!value) {
return value;
} else if (value.enumValue) {
return value.enumValue;
} else { } else {
return value; return value;
} }