Compare commits

...

3 Commits

4 changed files with 131 additions and 105 deletions

View File

@ -95,7 +95,12 @@ interface Closer {
transitionProgress: number, transitionProgress: number,
} }
export interface HandAnimation { 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 { export class RotationAnimationSegment extends AnimationSegment {
@ -109,16 +114,25 @@ export class RotationAnimationSegment extends AnimationSegment {
private readonly facing: RotationAnimationFacing; private readonly facing: RotationAnimationFacing;
private readonly startFacing: Rotation; private readonly startFacing: Rotation;
private readonly closer?: Closer; private readonly closer?: Closer;
private readonly hands: Map<Hand, HandAnimation>; private readonly hands: Map<Hand, HandAnimationInternal>;
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,
closer?: CloserDuringRotation, hands?: Map<Hand, HandAnimation>) { closer?: CloserDuringRotation, hands?: Map<Hand, HandAnimation>) {
super(beats, startPosition, endPosition); super(beats, startPosition, endPosition);
this.hands = hands ?? new Map<Hand, HandAnimation>(); function convertHandAnimation(anim: HandAnimation | undefined): HandAnimationInternal {
if (!this.hands.has(Hand.Left)) this.hands.set(Hand.Left, {kind: "Linear"}); if (!anim) {
if (!this.hands.has(Hand.Right)) this.hands.set(Hand.Right, {kind: "Linear"}); 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.facing = facing;
this.xRadius = around.width / 2; this.xRadius = around.width / 2;
@ -191,9 +205,17 @@ export class RotationAnimationSegment extends AnimationSegment {
} }
protected interpolateHandOffset(progress: number, hand: Hand): Offset | undefined { 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": case "Linear":
if (handAnim.transitionProgress === 0) {
return super.interpolateHandOffset(progress, hand); 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": case "Center":
const position = this.interpolateOffset(progress); const position = this.interpolateOffset(progress);
const offset = { const offset = {
@ -202,11 +224,32 @@ export class RotationAnimationSegment extends AnimationSegment {
}; };
const rotation = degreesToRadians(this.interpolateRotation(progress)); const rotation = degreesToRadians(this.interpolateRotation(progress));
return { const centerPos = {
x: -Math.cos(rotation) * offset.x + Math.sin(rotation) * offset.y, x: -Math.cos(rotation) * offset.x + Math.sin(rotation) * offset.y,
y: -Math.sin(rotation) * offset.x - Math.cos(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": case "none":
res.set(id, combine([ res.set(id, combine([
{ {
beats: 1, beats: move.beats,
endPosition: { ...startPosition, dancerDistance: swingRole }, endPosition: endPosition,
movementPattern: { kind: SemanticAnimationKind.Linear, },
},
{
beats: move.beats - 2,
endPosition: { ...endPosition, dancerDistance: swingRole },
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.Swing, 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, endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
}, },
}, },
{
beats: 1,
endPosition: endPosition,
movementPattern: { kind: SemanticAnimationKind.Linear, },
},
], startPos)); ], startPos));
break; break;
case "balance": case "balance":
@ -353,37 +344,22 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
movementPattern: { kind: SemanticAnimationKind.Linear, }, movementPattern: { kind: SemanticAnimationKind.Linear, },
}, },
prevEnd => ({ prevEnd => ({
beats: balancePartBeats - 1, beats: balancePartBeats,
endPosition: { endPosition: {
...prevEnd, ...prevEnd,
balance: BalanceWeight.Backward, balance: BalanceWeight.Backward,
}, },
movementPattern: { kind: SemanticAnimationKind.Linear, }, movementPattern: { kind: SemanticAnimationKind.Linear, },
}), }),
prevEnd => ({
beats: 1,
endPosition: {
...prevEnd,
balance: undefined,
dancerDistance: swingRole,
},
movementPattern: { kind: SemanticAnimationKind.Linear, },
}),
{ {
beats: move.beats - 2 * balancePartBeats - 1, beats: move.beats - 2 * balancePartBeats,
endPosition: { ...endPosition, dancerDistance: DancerDistance.Compact }, endPosition: endPosition,
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.Swing, 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, endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
}, swingRole: id.danceRole,
},
{
beats: 1,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
}, },
}, },
], startPos)); ], startPos));
@ -393,14 +369,12 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
res.set(id, combine([ res.set(id, combine([
prevEnd => ({ prevEnd => ({
beats: meltdownBeats, beats: meltdownBeats,
endPosition: { endPosition: prevEnd,
...prevEnd,
dancerDistance: swingRole,
},
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.RotateAround, kind: SemanticAnimationKind.RotateAround,
minAmount: 360, minAmount: 360,
around: startPos.which.leftRightSide(), around: startPos.which.leftRightSide(),
byHand: undefined,
}, },
}), }),
{ {
@ -411,6 +385,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
minAmount: 360, minAmount: 360,
around: startPos.which.leftRightSide(), around: startPos.which.leftRightSide(),
endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left, endFacing: startPos.which.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
}, },
}, },
], startPos)); ], startPos));
@ -452,39 +427,15 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
endPosition.lineOffset = (endPosition.lineOffset ?? 0) + withId.relativeLine; endPosition.lineOffset = (endPosition.lineOffset ?? 0) + withId.relativeLine;
} }
// TODO Take more than zero time to change hands?
res.set(id, combine([ 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, beats: move.beats,
endPosition: { 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,
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.RotateAround, kind: SemanticAnimationKind.RotateAround,
minAmount: byHand === Hand.Right ? move.parameters.circling : -move.parameters.circling, minAmount: byHand === Hand.Right ? move.parameters.circling : -move.parameters.circling,
around, around,
byHand,
}, },
}, },
], startPos)); ], startPos));
@ -501,21 +452,11 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
} }
res.set(id, combine([ 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 => ({ prevEnd => ({
beats: move.beats - 1, beats: move.beats,
endPosition: { endPosition: {
...prevEnd, ...prevEnd,
hands: handsInCircle,
which: startPos.which.circleLeft(places), which: startPos.which.circleLeft(places),
}, },
movementPattern: { movementPattern: {

View File

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

View File

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