Compare commits

...

3 Commits

4 changed files with 131 additions and 105 deletions

View File

@ -95,7 +95,12 @@ interface Closer {
transitionProgress: number,
}
export interface HandAnimation {
kind: "Linear" | "Center"
kind: "Linear" | "Center" | "None",
transitionBeats?: number,
}
interface HandAnimationInternal {
kind: "Linear" | "Center" | "None",
transitionProgress: number,
}
export class RotationAnimationSegment extends AnimationSegment {
@ -109,16 +114,25 @@ export class RotationAnimationSegment extends AnimationSegment {
private readonly facing: RotationAnimationFacing;
private readonly startFacing: Rotation;
private readonly closer?: Closer;
private readonly hands: Map<Hand, HandAnimation>;
private readonly hands: Map<Hand, HandAnimationInternal>;
constructor(beats: number, startPosition: DancerSetPosition, endPosition: DancerSetPosition,
rotation: number, around: RotationAround, facing: RotationAnimationFacing,
closer?: CloserDuringRotation, hands?: Map<Hand, HandAnimation>) {
super(beats, startPosition, endPosition);
this.hands = hands ?? new Map<Hand, HandAnimation>();
if (!this.hands.has(Hand.Left)) this.hands.set(Hand.Left, {kind: "Linear"});
if (!this.hands.has(Hand.Right)) this.hands.set(Hand.Right, {kind: "Linear"});
function convertHandAnimation(anim: HandAnimation | undefined): HandAnimationInternal {
if (!anim) {
return { kind: "Linear", transitionProgress: 0 };
} else {
return {
...anim,
transitionProgress: (anim.transitionBeats ?? 0) / beats
}
}
}
this.hands = new Map<Hand, HandAnimationInternal>(
[Hand.Left, Hand.Right].map(h => [h, convertHandAnimation(hands?.get(h))]));
this.facing = facing;
this.xRadius = around.width / 2;
@ -191,9 +205,17 @@ export class RotationAnimationSegment extends AnimationSegment {
}
protected interpolateHandOffset(progress: number, hand: Hand): Offset | undefined {
switch (this.hands.get(hand)!.kind) {
const handAnim : HandAnimationInternal = this.hands.get(hand)!;
let middlePos : Offset;
switch (handAnim.kind) {
case "Linear":
return super.interpolateHandOffset(progress, hand);
if (handAnim.transitionProgress === 0) {
return super.interpolateHandOffset(progress, hand);
} else if (progress < handAnim.transitionProgress) {
return super.interpolateHandOffset(progress / handAnim.transitionProgress, hand);
} else {
return super.interpolateHandOffset(1, hand);
}
case "Center":
const position = this.interpolateOffset(progress);
const offset = {
@ -202,11 +224,32 @@ export class RotationAnimationSegment extends AnimationSegment {
};
const rotation = degreesToRadians(this.interpolateRotation(progress));
return {
x: -Math.cos(rotation)*offset.x + Math.sin(rotation)*offset.y,
y: -Math.sin(rotation)*offset.x - Math.cos(rotation)*offset.y,
const centerPos = {
x: -Math.cos(rotation) * offset.x + Math.sin(rotation) * offset.y,
y: -Math.sin(rotation) * offset.x - Math.cos(rotation) * offset.y,
}
middlePos = centerPos;
break;
case "None":
middlePos = hand === Hand.Left ? leftShoulder : rightShoulder;
break;
}
if (handAnim.transitionProgress) {
if (progress < handAnim.transitionProgress) {
const startHand = hand === Hand.Left
? this.startPosition.leftArmEnd ?? leftShoulder
: this.startPosition.rightArmEnd ?? rightShoulder;
return interpolateLinearOffset(progress / handAnim.transitionProgress, startHand, middlePos);
} else if (1 - progress < handAnim.transitionProgress) {
const endHand = hand === Hand.Left
? this.endPosition.leftArmEnd ?? leftShoulder
: this.endPosition.rightArmEnd ?? rightShoulder;
return interpolateLinearOffset((1 - progress) / handAnim.transitionProgress, endHand, middlePos);
}
}
return middlePos;
}
}

View File

@ -310,25 +310,16 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
case "none":
res.set(id, combine([
{
beats: 1,
endPosition: { ...startPosition, dancerDistance: swingRole },
movementPattern: { kind: SemanticAnimationKind.Linear, },
},
{
beats: move.beats - 2,
endPosition: { ...endPosition, dancerDistance: swingRole },
beats: move.beats,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
{
beats: 1,
endPosition: endPosition,
movementPattern: { kind: SemanticAnimationKind.Linear, },
},
], startPos));
break;
case "balance":
@ -353,37 +344,22 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
movementPattern: { kind: SemanticAnimationKind.Linear, },
},
prevEnd => ({
beats: balancePartBeats - 1,
beats: balancePartBeats,
endPosition: {
...prevEnd,
balance: BalanceWeight.Backward,
},
movementPattern: { kind: SemanticAnimationKind.Linear, },
}),
prevEnd => ({
beats: 1,
endPosition: {
...prevEnd,
balance: undefined,
dancerDistance: swingRole,
},
movementPattern: { kind: SemanticAnimationKind.Linear, },
}),
{
beats: move.beats - 2 * balancePartBeats - 1,
endPosition: { ...endPosition, dancerDistance: DancerDistance.Compact },
beats: move.beats - 2 * balancePartBeats,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
},
},
{
beats: 1,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
swingRole: id.danceRole,
},
},
], startPos));
@ -393,14 +369,12 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
res.set(id, combine([
prevEnd => ({
beats: meltdownBeats,
endPosition: {
...prevEnd,
dancerDistance: swingRole,
},
endPosition: prevEnd,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: 360,
around: startPos.which.leftRightSide(),
byHand: undefined,
},
}),
{
@ -411,6 +385,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
minAmount: 360,
around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
], startPos));
@ -452,39 +427,15 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
endPosition.lineOffset = (endPosition.lineOffset ?? 0) + withId.relativeLine;
}
// TODO Take more than zero time to change hands?
res.set(id, combine([
prevEnd => ({
beats: 0,
endPosition: {
...prevEnd,
hands: new Map<Hand, HandConnection>([[byHand, { hand: byHand, to: HandTo.DancerForward }]]),
},
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? move.parameters.circling : -move.parameters.circling,
around,
},
}),
{
beats: move.beats,
endPosition: {
...endPosition,
hands: new Map<Hand, HandConnection>([[byHand, { hand: byHand, to: HandTo.DancerForward }]]),
},
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? move.parameters.circling : -move.parameters.circling,
around,
},
},
{
beats: 0,
endPosition: endPosition,
endPosition,
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? move.parameters.circling : -move.parameters.circling,
around,
byHand,
},
},
], startPos));
@ -501,21 +452,11 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
}
res.set(id, combine([
// TODO Is this right? Or should the circling and hands/positioning start at the same time?
{
beats: 1,
endPosition: {...startPos,
hands: handsInCircle,
facing: Facing.CenterOfCircle,
},
movementPattern: {
kind: SemanticAnimationKind.Linear,
}
},
prevEnd => ({
beats: move.beats - 1,
beats: move.beats,
endPosition: {
...prevEnd,
hands: handsInCircle,
which: startPos.which.circleLeft(places),
},
movementPattern: {

View File

@ -20,6 +20,7 @@ export enum CircleSide {
Left = "Left",
Right = "Right",
}
export type CircleSideOrCenter = CircleSide | "Center";
export class CirclePosition {
public static readonly TopLeft = new CirclePosition(CirclePositionEnum.TopLeft);
public static readonly BottomLeft = new CirclePosition(CirclePositionEnum.BottomLeft);

View File

@ -1,9 +1,9 @@
import * as animation from "./animation.js";
import * as common from "./danceCommon.js";
import { DancerIdentity, Rotation } from "./danceCommon.js";
import { BalanceWeight, CirclePosition, CircleSide, DancerDistance, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "./interpreterCommon.js";
import { DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
import { BalanceWeight, CirclePosition, CircleSide, CircleSideOrCenter, DancerDistance, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "./interpreterCommon.js";
import { Move } from "./libfigureMapper.js";
import { DancerSetPosition, Hand, Offset, dancerWidth, lineDistance, setDistance, setHeight, setWidth } from "./rendererConstants.js";
import { DancerSetPosition, Hand, Offset, dancerWidth, lineDistance, setDistance, setHeight, setSpacing, setWidth } from "./rendererConstants.js";
export enum SemanticAnimationKind {
@ -56,6 +56,8 @@ export type SemanticAnimation = {
minAmount: number,
around: CircleSide | "Center",
byHand: Hand | undefined,
} | {
kind: SemanticAnimationKind.Swing,
@ -64,6 +66,9 @@ export type SemanticAnimation = {
around: CircleSide | "Center",
endFacing: Facing,
// Swings are asymmetric. This is usually but not always the dancer's role.
swingRole: common.DanceRole,
} | {
kind: SemanticAnimationKind.TwirlSwap,
@ -103,8 +108,12 @@ function CenterOfSet(setOffset?: number, lineOffset?: number): Offset {
return position;
}
function CenterOfSideOfSet(side: CircleSide, setOffset?: number, lineOffset?: number): Offset {
function CenterOf(sideOrCenter: CircleSideOrCenter, setOffset?: number, lineOffset?: number): Offset {
const setCenter = CenterOfSet(setOffset, lineOffset);
if (sideOrCenter === "Center") return setCenter;
const side : CircleSide = sideOrCenter;
const xOffset = side === CircleSide.Left ? -1 : side === CircleSide.Right ? +1 : 0;
const yOffset = side === CircleSide.Top ? -1 : side === CircleSide.Bottom ? +1 : 0;
@ -181,8 +190,8 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
x: position.x,
y: position.y + position.y * (
semantic.balance === BalanceWeight.Forward
? balanceAmount
: -balanceAmount)
? -balanceAmount
: balanceAmount)
};
}
}
@ -301,6 +310,11 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
height: setHeight,
},
animation.RotationAnimationFacing.Center,
undefined,
new Map<Hand, animation.HandAnimation>([
[Hand.Left, { kind: "Linear", transitionBeats: 1 }],
[Hand.Right, { kind: "Linear", transitionBeats: 1 }],
])
),
];
case SemanticAnimationKind.DoSiDo:
@ -310,7 +324,7 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
if (move.startPosition.kind !== PositionKind.Circle) {
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 = CenterOf(move.startPosition.which.leftRightSide(), move.startPosition.setOffset, move.startPosition.lineOffset);
return [
new animation.RotationAnimationSegment(move.beats,
startSetPosition,
@ -332,9 +346,16 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
throw "Rotate must start in a circle: " + JSON.stringify(move);
}
// TODO Could rotate around other things.
const rotateCenter = move.movementPattern.around === "Center"
? CenterOfSet(move.startPosition.setOffset, move.startPosition.lineOffset)
: CenterOfSideOfSet(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
const rotateCenter = CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
const hands = move.movementPattern.byHand
? new Map<Hand, animation.HandAnimation>([
[move.movementPattern.byHand, { kind: "Center", transitionBeats: 1 }],
[move.movementPattern.byHand.opposite(), { kind: "None", transitionBeats: 1 }],
])
: new Map<Hand, animation.HandAnimation>([
[Hand.Left, { kind: "None", transitionBeats: 1 }],
[Hand.Right, { kind: "None", transitionBeats: 1 }],
]);
return [
new animation.RotationAnimationSegment(move.beats,
startSetPosition,
@ -350,26 +371,42 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
transitionBeats: 1,
minDistance: setWidth,
},
hands,
),
];
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 });
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({
...move.startPosition,
dancerDistance,
balance: undefined,
});
const beforeUnfold = SemanticToSetPosition({
...move.endPosition,
dancerDistance,
});
const unfolded = SemanticToSetPosition({
...move.endPosition,
dancerDistance: 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}]]),
});
return [
new animation.RotationAnimationSegment(move.beats - 1,
new animation.LinearAnimationSegment(1,
startSetPosition,
swingStart,
),
new animation.RotationAnimationSegment(move.beats - 3,
swingStart,
beforeUnfold,
move.movementPattern.minAmount,
move.movementPattern.minAmount, // TODO base minAmount on number of beats?
{
center: rotateSwingCenter,
width: setWidth / 5,
@ -379,12 +416,16 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
),
new animation.LinearAnimationSegment(1,
beforeUnfold,
unfolded,
),
new animation.LinearAnimationSegment(1,
unfolded,
endSetPosition,
),
];
case SemanticAnimationKind.TwirlSwap:
const twirlCenter =
CenterOfSideOfSet(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
CenterOf(move.movementPattern.around, move.startPosition.setOffset, move.startPosition.lineOffset);
const aroundTopOrBottom = move.movementPattern.around === CircleSide.Top || move.movementPattern.around === CircleSide.Bottom;
const postTwirlPos =
{