Compare commits

...

2 Commits

2 changed files with 231 additions and 155 deletions

View File

@ -25,10 +25,10 @@ export abstract class AnimationSegment {
this.endPosition = endPosition; this.endPosition = endPosition;
} }
protected abstract interpolateOffset(progress: number); abstract interpolateOffset(progress: number);
// TODO Not sure this interpolation is actually right for arms... // TODO Not sure this interpolation is actually right for arms...
protected interpolateHandOffset(progress: number, hand: Hand) : Offset | undefined { interpolateHandOffset(progress: number, hand: Hand) : Offset | undefined {
let prev = hand === Hand.Left ? this.startPosition.leftArmEnd : this.startPosition.rightArmEnd; let prev = hand === Hand.Left ? this.startPosition.leftArmEnd : this.startPosition.rightArmEnd;
let next = hand === Hand.Left ? this.endPosition.leftArmEnd : this.endPosition.rightArmEnd; let next = hand === Hand.Left ? this.endPosition.leftArmEnd : this.endPosition.rightArmEnd;
@ -39,7 +39,7 @@ export abstract class AnimationSegment {
return interpolateLinearOffset(progress, prev, next); return interpolateLinearOffset(progress, prev, next);
} }
protected interpolateRotation(progress: number) { interpolateRotation(progress: number) {
return interpolateLinear(progress, this.startPosition.rotation, this.endPosition.rotation); return interpolateLinear(progress, this.startPosition.rotation, this.endPosition.rotation);
} }
@ -65,19 +65,120 @@ function interpolateLinearOffset(progress: number, prev: Offset, next: Offset):
} }
export class LinearAnimationSegment extends AnimationSegment { export class LinearAnimationSegment extends AnimationSegment {
constructor(beats: number, startPosition: DancerSetPosition, endPosition: DancerSetPosition) { constructor({ beats, startPosition, endPosition }: {
beats: number;
startPosition: DancerSetPosition;
endPosition: DancerSetPosition;
}) {
super(beats, startPosition, endPosition); super(beats, startPosition, endPosition);
} }
public static standStill(startAndEndPosition: DancerSetPosition, beats?: number) { public static standStill(startAndEndPosition: DancerSetPosition, beats?: number) {
return new LinearAnimationSegment(beats ?? 0, startAndEndPosition, startAndEndPosition); return new LinearAnimationSegment({
beats: beats ?? 0,
startPosition: startAndEndPosition,
endPosition: startAndEndPosition
});
} }
protected interpolateOffset(progress: number) { override interpolateOffset(progress: number) {
return interpolateLinearOffset(progress, this.startPosition.position, this.endPosition.position); return interpolateLinearOffset(progress, this.startPosition.position, this.endPosition.position);
} }
} }
export interface AnimationTransitionFlags {
rotation?: boolean;
hands?: boolean;
}
export class TransitionAnimationSegment extends AnimationSegment {
private readonly actualAnimation: AnimationSegment;
private readonly flags: AnimationTransitionFlags;
private readonly startTransitionProgress: number;
private readonly endTransitionProgress: number;
private readonly startRotation: number;
private readonly endRotation: number;
constructor({ actualAnimation, flags, startTransitionBeats, endTransitionBeats }: {
actualAnimation: AnimationSegment;
flags: AnimationTransitionFlags;
startTransitionBeats: number;
endTransitionBeats?: number;
}) {
super(actualAnimation.beats, actualAnimation.startPosition, actualAnimation.endPosition);
this.actualAnimation = actualAnimation;
this.flags = flags;
this.startTransitionProgress = startTransitionBeats / actualAnimation.beats;
this.endTransitionProgress = endTransitionBeats === undefined ? this.startTransitionProgress : endTransitionBeats / actualAnimation.beats;
this.startRotation = this.startPosition.rotation;
this.endRotation = this.endPosition.rotation;
if (this.flags.rotation) {
const actualStart = this.actualAnimation.interpolateRotation(0);
const actualEnd = this.actualAnimation.interpolateRotation(1);
const rotationPositive = actualEnd > actualStart;
const transitionStart = this.actualAnimation.interpolateRotation(this.startTransitionProgress);
const transitionEnd = this.actualAnimation.interpolateRotation(1 - this.endTransitionProgress);
if (rotationPositive) {
while (transitionStart < this.startRotation) {
this.startRotation -= 360;
}
while (transitionEnd > this.endRotation) {
this.endRotation += 360;
}
} else {
while (transitionStart > this.startRotation) {
this.startRotation += 360;
}
while (transitionEnd < this.endRotation) {
this.endRotation -= 360;
}
}
}
}
override interpolateOffset(progress: number) {
return this.actualAnimation.interpolateOffset(progress);
}
override interpolateRotation(progress: number): number {
const actualRotation = this.actualAnimation.interpolateRotation(progress);
if (this.flags.rotation) {
if (progress < this.startTransitionProgress) {
return interpolateLinear(progress / this.startTransitionProgress, this.startRotation, actualRotation);
} else if ((1 - progress) < this.endTransitionProgress) {
return interpolateLinear((1 - progress) / this.endTransitionProgress, this.endRotation, actualRotation);
}
}
return actualRotation;
}
override interpolateHandOffset(progress: number, hand: Hand): Offset | undefined {
const actualHandOffset = this.actualAnimation.interpolateHandOffset(progress, hand);
if (this.flags.hands) {
if (progress < this.startTransitionProgress) {
const startHand = hand === Hand.Left
? this.startPosition.leftArmEnd ?? leftShoulder
: this.startPosition.rightArmEnd ?? rightShoulder;
return interpolateLinearOffset(progress / this.startTransitionProgress, startHand, actualHandOffset ?? hand.shoulderPosition());
} else if (1 - progress < this.endTransitionProgress) {
const endHand = hand === Hand.Left
? this.endPosition.leftArmEnd ?? leftShoulder
: this.endPosition.rightArmEnd ?? rightShoulder;
return interpolateLinearOffset((1 - progress) / this.endTransitionProgress, endHand, actualHandOffset ?? hand.shoulderPosition());
}
}
return actualHandOffset;
}
}
// TODO Not sure this really belongs here instead of on some Semantic* type. // TODO Not sure this really belongs here instead of on some Semantic* type.
export enum RotationAnimationFacing { export enum RotationAnimationFacing {
Linear = "Linear", // Default, linearly interpolate. Linear = "Linear", // Default, linearly interpolate.
@ -95,12 +196,7 @@ interface Closer {
transitionProgress: number, transitionProgress: number,
} }
export interface HandAnimation { export interface HandAnimation {
kind: "Linear" | "Center" | "None", kind: "Linear" | "Center" | "None" | "Start" | "End"
transitionBeats?: number,
}
interface HandAnimationInternal {
kind: "Linear" | "Center" | "None",
transitionProgress: number,
} }
export class RotationAnimationSegment extends AnimationSegment { export class RotationAnimationSegment extends AnimationSegment {
@ -114,27 +210,21 @@ 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, HandAnimationInternal>; private readonly hands: Map<Hand, HandAnimation>;
private readonly rotationTransitionProgress: number;
constructor(beats: number, startPosition: DancerSetPosition, endPosition: DancerSetPosition, constructor({ beats, startPosition, endPosition, rotation, around, facing, closer, hands }: {
rotation: number, around: RotationAround, facing: RotationAnimationFacing, beats: number;
closer?: CloserDuringRotation, hands?: Map<Hand, HandAnimation>, startPosition: DancerSetPosition;
rotationTransitionBeats?: number) { endPosition: DancerSetPosition;
rotation: number; around: RotationAround;
facing: RotationAnimationFacing;
closer?: CloserDuringRotation;
hands?: Map<Hand, HandAnimation>;
}) {
super(beats, startPosition, endPosition); super(beats, startPosition, endPosition);
function convertHandAnimation(anim: HandAnimation | undefined): HandAnimationInternal { this.hands = new Map<Hand, HandAnimation>(
if (!anim) { [Hand.Left, Hand.Right].map(h => [h, hands?.get(h) ?? { 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;
@ -148,7 +238,6 @@ export class RotationAnimationSegment extends AnimationSegment {
// Canvas rotation is backwards of what we expect, so take the negative here. // Canvas rotation is backwards of what we expect, so take the negative here.
return -radiansToDegrees(radians); return -radiansToDegrees(radians);
} }
this.rotationTransitionProgress = (rotationTransitionBeats ?? 0) / beats;
this.startRotation = positionToDegrees(startPosition.position); this.startRotation = positionToDegrees(startPosition.position);
this.startFacing = startPosition.rotation; this.startFacing = startPosition.rotation;
const actualRotation = normalizeRotation(positionToDegrees(endPosition.position) - this.startRotation, const actualRotation = normalizeRotation(positionToDegrees(endPosition.position) - this.startRotation,
@ -169,7 +258,7 @@ export class RotationAnimationSegment extends AnimationSegment {
} }
} }
protected interpolateOffset(progress: number) { override interpolateOffset(progress: number) {
const radians = -degreesToRadians(interpolateLinear(progress, this.startRotation, this.endRotation)); const radians = -degreesToRadians(interpolateLinear(progress, this.startRotation, this.endRotation));
let distance: number; let distance: number;
if (!this.closer) { if (!this.closer) {
@ -189,7 +278,7 @@ export class RotationAnimationSegment extends AnimationSegment {
} }
} }
protected interpolateRotation(progress: number): number { override interpolateRotation(progress: number): number {
if (this.facing === RotationAnimationFacing.Linear) { if (this.facing === RotationAnimationFacing.Linear) {
return super.interpolateRotation(progress); return super.interpolateRotation(progress);
} else { } else {
@ -197,41 +286,27 @@ export class RotationAnimationSegment extends AnimationSegment {
let rotation : number; let rotation : number;
switch (this.facing) { switch (this.facing) {
case RotationAnimationFacing.Center: case RotationAnimationFacing.Center:
rotation = degrees - 90; return degrees - 90;
break;
case RotationAnimationFacing.CenterRelative: case RotationAnimationFacing.CenterRelative:
rotation = degrees - this.startRotation + this.startFacing; return degrees - this.startRotation + this.startFacing;
break;
case RotationAnimationFacing.Forward: case RotationAnimationFacing.Forward:
rotation = degrees + 180; return degrees + 180;
break;
case RotationAnimationFacing.Backward: case RotationAnimationFacing.Backward:
rotation = degrees; return degrees;
break;
}
if (progress < this.rotationTransitionProgress) {
return interpolateLinear(progress / this.rotationTransitionProgress, this.startRotation, rotation);
} else if ((1 - progress) < this.rotationTransitionProgress) {
return interpolateLinear((1 - progress) / this.rotationTransitionProgress, this.endRotation, rotation);
} else {
return rotation;
} }
} }
} }
protected interpolateHandOffset(progress: number, hand: Hand): Offset | undefined { override interpolateHandOffset(progress: number, hand: Hand): Offset | undefined {
const handAnim : HandAnimationInternal = this.hands.get(hand)!; const handAnim : HandAnimation = this.hands.get(hand)!;
let middlePos : Offset; let middlePos : Offset;
switch (handAnim.kind) { switch (handAnim.kind) {
case "Start":
return hand === Hand.Left ? this.startPosition.leftArmEnd : this.startPosition.rightArmEnd;
case "End":
return hand === Hand.Left ? this.endPosition.leftArmEnd : this.endPosition.rightArmEnd;
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 = {
@ -244,28 +319,10 @@ export class RotationAnimationSegment extends AnimationSegment {
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; return centerPos;
break;
case "None": case "None":
middlePos = hand === Hand.Left ? leftShoulder : rightShoulder; return 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

@ -287,11 +287,13 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
+ JSON.stringify(move.startPosition) + ", end=" + JSON.stringify(move.endPosition); + JSON.stringify(move.startPosition) + ", end=" + JSON.stringify(move.endPosition);
} }
return [ return [
new animation.LinearAnimationSegment(move.beats, new animation.LinearAnimationSegment({
startSetPosition, beats: move.beats,
{ startPosition: startSetPosition,
endPosition: {
...endSetPosition, ...endSetPosition,
rotation: startSetPosition.rotation + rotation, rotation: startSetPosition.rotation + rotation,
}
}), }),
]; ];
case SemanticAnimationKind.Circle: case SemanticAnimationKind.Circle:
@ -300,22 +302,27 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
} }
return [ return [
new animation.RotationAnimationSegment(move.beats, new animation.TransitionAnimationSegment({
startSetPosition, actualAnimation: new animation.RotationAnimationSegment({
endSetPosition, beats: move.beats,
move.movementPattern.places * 90, startPosition: startSetPosition,
{ endPosition: endSetPosition,
rotation: move.movementPattern.places * 90,
around: {
center: CenterOfSet(move.startPosition.setOffset, move.startPosition.lineOffset), center: CenterOfSet(move.startPosition.setOffset, move.startPosition.lineOffset),
width: setWidth, width: setWidth,
height: setHeight, height: setHeight,
}, },
animation.RotationAnimationFacing.Center, facing: animation.RotationAnimationFacing.Center, closer: undefined, hands: new Map<Hand, animation.HandAnimation>([
undefined, [Hand.Left, { kind: "End" }],
new Map<Hand, animation.HandAnimation>([ [Hand.Right, { kind: "End" }],
[Hand.Left, { kind: "Linear", transitionBeats: 1 }],
[Hand.Right, { kind: "Linear", transitionBeats: 1 }],
]) ])
), }),
flags: {
hands: true
},
startTransitionBeats: 1
}),
]; ];
case SemanticAnimationKind.DoSiDo: case SemanticAnimationKind.DoSiDo:
// TODO Rotation // TODO Rotation
@ -326,17 +333,18 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
} }
const center = CenterOf(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({
startSetPosition, beats: move.beats,
endSetPosition, startPosition: startSetPosition,
move.movementPattern.amount, endPosition: endSetPosition,
{ rotation: move.movementPattern.amount,
around: {
center, center,
width: setWidth / 3, width: setWidth / 3,
height: setHeight, height: setHeight,
}, },
animation.RotationAnimationFacing.Linear, facing: animation.RotationAnimationFacing.Linear
), }),
]; ];
case SemanticAnimationKind.RotateAround: case SemanticAnimationKind.RotateAround:
// TODO Rotation // TODO Rotation
@ -349,31 +357,37 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
const rotateCenter = CenterOf(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 const hands = move.movementPattern.byHand
? new Map<Hand, animation.HandAnimation>([ ? new Map<Hand, animation.HandAnimation>([
[move.movementPattern.byHand, { kind: "Center", transitionBeats: 1 }], [move.movementPattern.byHand, { kind: "Center" }],
[move.movementPattern.byHand.opposite(), { kind: "None", transitionBeats: 1 }], [move.movementPattern.byHand.opposite(), { kind: "None" }],
]) ])
: new Map<Hand, animation.HandAnimation>([ : new Map<Hand, animation.HandAnimation>([
[Hand.Left, { kind: "None", transitionBeats: 1 }], [Hand.Left, { kind: "None" }],
[Hand.Right, { kind: "None", transitionBeats: 1 }], [Hand.Right, { kind: "None" }],
]); ]);
return [ return [
new animation.RotationAnimationSegment(move.beats, new animation.TransitionAnimationSegment({
startSetPosition, actualAnimation: new animation.RotationAnimationSegment({
endSetPosition, beats: move.beats,
move.movementPattern.minAmount, startPosition: startSetPosition,
{ endPosition: endSetPosition,
rotation: move.movementPattern.minAmount,
around: {
center: rotateCenter, center: rotateCenter,
width: setWidth / 5, width: setWidth / 5,
height: setHeight / 5, height: setHeight / 5,
}, },
animation.RotationAnimationFacing.Center, facing: animation.RotationAnimationFacing.Center, closer: {
{
transitionBeats: 1, transitionBeats: 1,
minDistance: setWidth, minDistance: setWidth,
}, },
hands, hands
1, }),
), flags: {
hands: true,
rotation: true
},
startTransitionBeats: 1
}),
]; ];
case SemanticAnimationKind.Swing: case SemanticAnimationKind.Swing:
if (move.startPosition.kind !== PositionKind.Circle || move.endPosition.kind !== PositionKind.Circle) { if (move.startPosition.kind !== PositionKind.Circle || move.endPosition.kind !== PositionKind.Circle) {
@ -400,29 +414,33 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
}); });
return [ return [
new animation.LinearAnimationSegment(1, new animation.LinearAnimationSegment({
startSetPosition, beats: 1,
swingStart, startPosition: startSetPosition,
), endPosition: swingStart,
new animation.RotationAnimationSegment(move.beats - 3, }),
swingStart, new animation.RotationAnimationSegment({
beforeUnfold, beats: move.beats - 3,
move.movementPattern.minAmount, // TODO base minAmount on number of beats? startPosition: swingStart,
{ endPosition: beforeUnfold,
rotation: move.movementPattern.minAmount,
around: {
center: rotateSwingCenter, center: rotateSwingCenter,
width: setWidth / 5, width: setWidth / 5,
height: setHeight / 5, height: setHeight / 5,
}, },
animation.RotationAnimationFacing.CenterRelative, facing: animation.RotationAnimationFacing.CenterRelative
), }),
new animation.LinearAnimationSegment(1, new animation.LinearAnimationSegment({
beforeUnfold, beats: 1,
unfolded, startPosition: beforeUnfold,
), endPosition: unfolded,
new animation.LinearAnimationSegment(1, }),
unfolded, new animation.LinearAnimationSegment({
endSetPosition, beats: 1,
), startPosition: unfolded,
endPosition: endSetPosition,
}),
]; ];
case SemanticAnimationKind.TwirlSwap: case SemanticAnimationKind.TwirlSwap:
const twirlCenter = const twirlCenter =
@ -435,25 +453,26 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
rightArmEnd: startSetPosition.rightArmEnd === undefined ? undefined : endSetPosition.rightArmEnd, rightArmEnd: startSetPosition.rightArmEnd === undefined ? undefined : endSetPosition.rightArmEnd,
}; };
return [ return [
new animation.RotationAnimationSegment(move.beats - 1, new animation.RotationAnimationSegment({
startSetPosition, beats: move.beats - 1,
{ startPosition: startSetPosition,
endPosition: {
...postTwirlPos, ...postTwirlPos,
rotation: endSetPosition.rotation - (move.startPosition.hands?.has(Hand.Left) ? 360 : 0) rotation: endSetPosition.rotation - (move.startPosition.hands?.has(Hand.Left) ? 360 : 0)
}, },
180, rotation: 180,
{ around: {
center: twirlCenter, center: twirlCenter,
width: aroundTopOrBottom ? setWidth : setWidth / 4, width: aroundTopOrBottom ? setWidth : setWidth / 4,
height: aroundTopOrBottom ? setHeight / 4 : setHeight, height: aroundTopOrBottom ? setHeight / 4 : setHeight,
}, },
animation.RotationAnimationFacing.Linear, facing: animation.RotationAnimationFacing.Linear, closer: undefined, hands: new Map<Hand, animation.HandAnimation>([[[...move.startPosition.hands!.keys()][0], { kind: "Center" }]])
undefined, }),
new Map<Hand, animation.HandAnimation>([[[...move.startPosition.hands!.keys()][0], {kind: "Center"}]]), new animation.LinearAnimationSegment({
), beats: 1,
new animation.LinearAnimationSegment(1, startPosition: postTwirlPos,
postTwirlPos, endPosition: endSetPosition
endSetPosition) })
]; ];
} }