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;
}
protected abstract interpolateOffset(progress: number);
abstract interpolateOffset(progress: number);
// 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 next = hand === Hand.Left ? this.endPosition.leftArmEnd : this.endPosition.rightArmEnd;
@ -39,7 +39,7 @@ export abstract class AnimationSegment {
return interpolateLinearOffset(progress, prev, next);
}
protected interpolateRotation(progress: number) {
interpolateRotation(progress: number) {
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 {
constructor(beats: number, startPosition: DancerSetPosition, endPosition: DancerSetPosition) {
constructor({ beats, startPosition, endPosition }: {
beats: number;
startPosition: DancerSetPosition;
endPosition: DancerSetPosition;
}) {
super(beats, startPosition, endPosition);
}
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);
}
}
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.
export enum RotationAnimationFacing {
Linear = "Linear", // Default, linearly interpolate.
@ -95,12 +196,7 @@ interface Closer {
transitionProgress: number,
}
export interface HandAnimation {
kind: "Linear" | "Center" | "None",
transitionBeats?: number,
}
interface HandAnimationInternal {
kind: "Linear" | "Center" | "None",
transitionProgress: number,
kind: "Linear" | "Center" | "None" | "Start" | "End"
}
export class RotationAnimationSegment extends AnimationSegment {
@ -114,27 +210,21 @@ export class RotationAnimationSegment extends AnimationSegment {
private readonly facing: RotationAnimationFacing;
private readonly startFacing: Rotation;
private readonly closer?: Closer;
private readonly hands: Map<Hand, HandAnimationInternal>;
private readonly rotationTransitionProgress: number;
private readonly hands: Map<Hand, HandAnimation>;
constructor(beats: number, startPosition: DancerSetPosition, endPosition: DancerSetPosition,
rotation: number, around: RotationAround, facing: RotationAnimationFacing,
closer?: CloserDuringRotation, hands?: Map<Hand, HandAnimation>,
rotationTransitionBeats?: number) {
constructor({ beats, startPosition, endPosition, rotation, around, facing, closer, hands }: {
beats: number;
startPosition: DancerSetPosition;
endPosition: DancerSetPosition;
rotation: number; around: RotationAround;
facing: RotationAnimationFacing;
closer?: CloserDuringRotation;
hands?: Map<Hand, HandAnimation>;
}) {
super(beats, startPosition, endPosition);
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.hands = new Map<Hand, HandAnimation>(
[Hand.Left, Hand.Right].map(h => [h, hands?.get(h) ?? { kind: "Linear" }]));
this.facing = facing;
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.
return -radiansToDegrees(radians);
}
this.rotationTransitionProgress = (rotationTransitionBeats ?? 0) / beats;
this.startRotation = positionToDegrees(startPosition.position);
this.startFacing = startPosition.rotation;
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));
let distance: number;
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) {
return super.interpolateRotation(progress);
} else {
@ -197,41 +286,27 @@ export class RotationAnimationSegment extends AnimationSegment {
let rotation : number;
switch (this.facing) {
case RotationAnimationFacing.Center:
rotation = degrees - 90;
break;
return degrees - 90;
case RotationAnimationFacing.CenterRelative:
rotation = degrees - this.startRotation + this.startFacing;
break;
return degrees - this.startRotation + this.startFacing;
case RotationAnimationFacing.Forward:
rotation = degrees + 180;
break;
return degrees + 180;
case RotationAnimationFacing.Backward:
rotation = 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;
return degrees;
}
}
}
protected interpolateHandOffset(progress: number, hand: Hand): Offset | undefined {
const handAnim : HandAnimationInternal = this.hands.get(hand)!;
override interpolateHandOffset(progress: number, hand: Hand): Offset | undefined {
const handAnim : HandAnimation = this.hands.get(hand)!;
let middlePos : Offset;
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":
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 = {
@ -244,28 +319,10 @@ export class RotationAnimationSegment extends AnimationSegment {
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;
return centerPos;
case "None":
middlePos = hand === Hand.Left ? leftShoulder : rightShoulder;
break;
return hand === Hand.Left ? leftShoulder : rightShoulder;
}
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);
}
return [
new animation.LinearAnimationSegment(move.beats,
startSetPosition,
{
new animation.LinearAnimationSegment({
beats: move.beats,
startPosition: startSetPosition,
endPosition: {
...endSetPosition,
rotation: startSetPosition.rotation + rotation,
}
}),
];
case SemanticAnimationKind.Circle:
@ -300,22 +302,27 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
}
return [
new animation.RotationAnimationSegment(move.beats,
startSetPosition,
endSetPosition,
move.movementPattern.places * 90,
{
new animation.TransitionAnimationSegment({
actualAnimation: new animation.RotationAnimationSegment({
beats: move.beats,
startPosition: startSetPosition,
endPosition: endSetPosition,
rotation: move.movementPattern.places * 90,
around: {
center: CenterOfSet(move.startPosition.setOffset, move.startPosition.lineOffset),
width: setWidth,
height: setHeight,
},
animation.RotationAnimationFacing.Center,
undefined,
new Map<Hand, animation.HandAnimation>([
[Hand.Left, { kind: "Linear", transitionBeats: 1 }],
[Hand.Right, { kind: "Linear", transitionBeats: 1 }],
facing: animation.RotationAnimationFacing.Center, closer: undefined, hands: new Map<Hand, animation.HandAnimation>([
[Hand.Left, { kind: "End" }],
[Hand.Right, { kind: "End" }],
])
),
}),
flags: {
hands: true
},
startTransitionBeats: 1
}),
];
case SemanticAnimationKind.DoSiDo:
// 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);
return [
new animation.RotationAnimationSegment(move.beats,
startSetPosition,
endSetPosition,
move.movementPattern.amount,
{
new animation.RotationAnimationSegment({
beats: move.beats,
startPosition: startSetPosition,
endPosition: endSetPosition,
rotation: move.movementPattern.amount,
around: {
center,
width: setWidth / 3,
height: setHeight,
},
animation.RotationAnimationFacing.Linear,
),
facing: animation.RotationAnimationFacing.Linear
}),
];
case SemanticAnimationKind.RotateAround:
// 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 hands = move.movementPattern.byHand
? new Map<Hand, animation.HandAnimation>([
[move.movementPattern.byHand, { kind: "Center", transitionBeats: 1 }],
[move.movementPattern.byHand.opposite(), { kind: "None", transitionBeats: 1 }],
[move.movementPattern.byHand, { kind: "Center" }],
[move.movementPattern.byHand.opposite(), { kind: "None" }],
])
: new Map<Hand, animation.HandAnimation>([
[Hand.Left, { kind: "None", transitionBeats: 1 }],
[Hand.Right, { kind: "None", transitionBeats: 1 }],
[Hand.Left, { kind: "None" }],
[Hand.Right, { kind: "None" }],
]);
return [
new animation.RotationAnimationSegment(move.beats,
startSetPosition,
endSetPosition,
move.movementPattern.minAmount,
{
new animation.TransitionAnimationSegment({
actualAnimation: new animation.RotationAnimationSegment({
beats: move.beats,
startPosition: startSetPosition,
endPosition: endSetPosition,
rotation: move.movementPattern.minAmount,
around: {
center: rotateCenter,
width: setWidth / 5,
height: setHeight / 5,
},
animation.RotationAnimationFacing.Center,
{
facing: animation.RotationAnimationFacing.Center, closer: {
transitionBeats: 1,
minDistance: setWidth,
},
hands,
1,
),
hands
}),
flags: {
hands: true,
rotation: true
},
startTransitionBeats: 1
}),
];
case SemanticAnimationKind.Swing:
if (move.startPosition.kind !== PositionKind.Circle || move.endPosition.kind !== PositionKind.Circle) {
@ -400,29 +414,33 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
});
return [
new animation.LinearAnimationSegment(1,
startSetPosition,
swingStart,
),
new animation.RotationAnimationSegment(move.beats - 3,
swingStart,
beforeUnfold,
move.movementPattern.minAmount, // TODO base minAmount on number of beats?
{
new animation.LinearAnimationSegment({
beats: 1,
startPosition: startSetPosition,
endPosition: swingStart,
}),
new animation.RotationAnimationSegment({
beats: move.beats - 3,
startPosition: swingStart,
endPosition: beforeUnfold,
rotation: move.movementPattern.minAmount,
around: {
center: rotateSwingCenter,
width: setWidth / 5,
height: setHeight / 5,
},
animation.RotationAnimationFacing.CenterRelative,
),
new animation.LinearAnimationSegment(1,
beforeUnfold,
unfolded,
),
new animation.LinearAnimationSegment(1,
unfolded,
endSetPosition,
),
facing: animation.RotationAnimationFacing.CenterRelative
}),
new animation.LinearAnimationSegment({
beats: 1,
startPosition: beforeUnfold,
endPosition: unfolded,
}),
new animation.LinearAnimationSegment({
beats: 1,
startPosition: unfolded,
endPosition: endSetPosition,
}),
];
case SemanticAnimationKind.TwirlSwap:
const twirlCenter =
@ -435,25 +453,26 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
rightArmEnd: startSetPosition.rightArmEnd === undefined ? undefined : endSetPosition.rightArmEnd,
};
return [
new animation.RotationAnimationSegment(move.beats - 1,
startSetPosition,
{
new animation.RotationAnimationSegment({
beats: move.beats - 1,
startPosition: startSetPosition,
endPosition: {
...postTwirlPos,
rotation: endSetPosition.rotation - (move.startPosition.hands?.has(Hand.Left) ? 360 : 0)
},
180,
{
rotation: 180,
around: {
center: twirlCenter,
width: aroundTopOrBottom ? setWidth : setWidth / 4,
height: aroundTopOrBottom ? setHeight / 4 : setHeight,
},
animation.RotationAnimationFacing.Linear,
undefined,
new Map<Hand, animation.HandAnimation>([[[...move.startPosition.hands!.keys()][0], {kind: "Center"}]]),
),
new animation.LinearAnimationSegment(1,
postTwirlPos,
endSetPosition)
facing: animation.RotationAnimationFacing.Linear, closer: undefined, hands: new Map<Hand, animation.HandAnimation>([[[...move.startPosition.hands!.keys()][0], { kind: "Center" }]])
}),
new animation.LinearAnimationSegment({
beats: 1,
startPosition: postTwirlPos,
endPosition: endSetPosition
})
];
}