Compare commits

...

4 Commits

7 changed files with 192 additions and 66 deletions

View File

@ -1,5 +1,5 @@
import { DancerIdentity, Rotation, normalizeRotation } from "./danceCommon.js";
import { DancerSetPosition, DancersSetPositions, Hand } from "./rendererConstants.js";
import { CoupleRole, DancerIdentity, Rotation, normalizeRotation } from "./danceCommon.js";
import { DancerSetPosition, DancersSetPositions, Hand, OffsetEquals, OffsetMinus, OffsetPlus, OffsetTimes, offsetZero } from "./rendererConstants.js";
import { Offset, leftShoulder, rightShoulder, degreesToRadians, radiansToDegrees } from "./rendererConstants.js";
export enum AnimationKind {
@ -355,34 +355,87 @@ export class RotationAnimationSegment extends AnimationSegment {
}
}
export type Animation = Map<DancerIdentity, AnimationSegment[]>;
export function newAnimation() : Animation { return new Map<DancerIdentity, AnimationSegment[]>() };
export class Animation {
private readonly segments : Map<DancerIdentity, AnimationSegment[]>;
public readonly progression : Offset;
public readonly numBeats : number;
function positionAtBeat(animation: AnimationSegment[], beat: number): DancerSetPosition {
let lastFrame : AnimationSegment = animation[0];
let lastFrameEndBeat = 0;
for (const frame of animation) {
let currentFrameEndBeat = lastFrameEndBeat + frame.beats;
if (currentFrameEndBeat < beat) {
lastFrame = frame;
lastFrameEndBeat = currentFrameEndBeat;
continue;
} else if (currentFrameEndBeat === beat) {
return frame.endPosition;
} else {
const progress = (beat - lastFrameEndBeat) / (currentFrameEndBeat - lastFrameEndBeat);
return frame.positionAtFraction(progress);
constructor(segments: Map<DancerIdentity, AnimationSegment[]>) {
this.segments = segments;
this.numBeats = Math.max(...[...this.segments.values()].map(el => el.reduce((a, s) => a + s.beats, 0)));
const progressions = new Map<DancerIdentity, Offset>(
[...this.segments.entries()]
.map(([id, a]) => [
id,
OffsetMinus(
a.at(-1)!.endPosition.position,
a.at(0)!.startPosition.position)]));
this.progression = progressions.get(DancerIdentity.OnesLark)!;
if (this.progression.x !== 0) {
throw "Progressing to different line is unsupported.";
}
if (this.progression.y === 0) {
throw "Dance does not progress.";
}
if (!OffsetEquals(progressions.get(DancerIdentity.OnesLark)!, progressions.get(DancerIdentity.OnesRobin)!)) {
throw "Ones are not progressing the same as each other.";
}
if (!OffsetEquals(progressions.get(DancerIdentity.TwosLark)!, progressions.get(DancerIdentity.TwosRobin)!)) {
throw "Twos are not progressing the same as each other.";
}
if (!OffsetEquals(progressions.get(DancerIdentity.OnesLark)!, OffsetMinus(offsetZero, progressions.get(DancerIdentity.TwosLark)!))) {
throw "Ones and Twos are not progressing opposite each other.";
}
}
// fallthrough
return lastFrame.endPosition;
}
positionsAtBeat(beat: number, progression?: number): DancersSetPositions {
function positionAtBeat(animation: AnimationSegment[], beat: number): DancerSetPosition {
let lastFrame: AnimationSegment = animation[0];
let lastFrameEndBeat = 0;
for (const frame of animation) {
let currentFrameEndBeat = lastFrameEndBeat + frame.beats;
if (currentFrameEndBeat < beat) {
lastFrame = frame;
lastFrameEndBeat = currentFrameEndBeat;
continue;
} else if (currentFrameEndBeat === beat) {
return frame.endPosition;
} else {
const progress = (beat - lastFrameEndBeat) / (currentFrameEndBeat - lastFrameEndBeat);
return frame.positionAtFraction(progress);
}
}
export function positionsAtBeat(animation: Animation, beat: number): DancersSetPositions {
// fallthrough
return lastFrame.endPosition;
}
progression ??= 0;
while (beat < 0) {
beat += this.numBeats;
progression -= 1;
}
while (beat > this.numBeats) {
beat -= this.numBeats;
progression += 1;
}
const res = new Map<DancerIdentity, DancerSetPosition>();
for (const [id, animationSegments] of animation.entries()) {
res.set(id, positionAtBeat(animationSegments, beat));
for (const [id, animationSegments] of this.segments.entries()) {
const basePosition = positionAtBeat(animationSegments, beat);
if (progression !== 0) {
res.set(id, {
...basePosition,
position: OffsetPlus(
basePosition.position,
OffsetTimes(
this.progression,
progression * (id.coupleRole === CoupleRole.Ones ? 1 : -1)))
});
} else {
res.set(id, basePosition);
}
}
return res;
}
}

View File

@ -125,6 +125,15 @@ export class DancerIdentity {
public toString() : string {
return this.danceRole.toString() + "!" + this.coupleRole.toString();
}
public static all() : DancerIdentity[] {
return [
DancerIdentity.OnesLark,
DancerIdentity.OnesRobin,
DancerIdentity.TwosLark,
DancerIdentity.TwosRobin,
]
}
}
export interface ExtendedDancerIdentity {

View File

@ -1,7 +1,7 @@
import * as animation from "./animation.js";
import { CoupleRole, DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
import * as common from "./danceCommon.js";
import { Hand } from "./rendererConstants.js";
import { Hand, setDistance, setHeight } from "./rendererConstants.js";
import { nameLibFigureParameters, Move, LibFigureDance, chooser_pairz } from "./libfigureMapper.js";
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js";
import { BalanceWeight, CirclePosition, CircleSide, DancerDistance, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "./interpreterCommon.js";
@ -544,6 +544,12 @@ function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, Se
currentPos.set(id, newMoveList.at(-1)!.endPosition);
}
}
const progression = animateFromLowLevelMoves(res).progression;
if (progression.x !== 0) throw "Progressing to different line is unsupported.";
if (progression.y % setHeight / 2 !== 0) throw "Progression is not an integer number of places.";
const progressionInSets = progression.y / setDistance;
// fixup end positions to match start of next move
// TODO Handle progression.
for (const [id, lowLevelMoves] of res.entries()) {
@ -556,8 +562,7 @@ function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, Se
lowLevelMoves[lowLevelMoves.length - 1].endPosition = {
...lowLevelMoves[0].startPosition,
// progressed
// TODO progression kind? This assumes single progression. Maybe read from endPosition?
setOffset: (startPos.setOffset ?? 0) + (id.coupleRole == CoupleRole.Ones ? 0.5 : -0.5),
setOffset: (startPos.setOffset ?? 0) + (id.coupleRole == CoupleRole.Ones ? 1 : -1) * progressionInSets,
};
}
return res;

View File

@ -512,9 +512,9 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
}
export function animateFromLowLevelMoves(moves: Map<DancerIdentity, LowLevelMove[]>): animation.Animation {
const res = animation.newAnimation();
const res = new Map<DancerIdentity, animation.AnimationSegment[]>();
for (const [id, moveList] of moves.entries()) {
res.set(id, moveList.flatMap(move => animateLowLevelMove(move)))
}
return res;
return new animation.Animation(res);
}

View File

@ -35,7 +35,7 @@ beatSliderLabel.appendChild(beatSlider);
const beatDisplay = document.createElement('span');
beatDisplay.className = 'beatDisplay';
beatDisplay.innerText = '0';
beatDisplay.innerText = '0.0';
const ctx = canvas.getContext('2d')!;
@ -55,6 +55,7 @@ r.extraLines = 3;
r.extraSets = 3;
r.clear();
r.animation = interpreter.interpretedAnimation;
beatSlider.max = r.animation.numBeats.toString();
r.drawSetsWithTrails(0);
const bpmSelector = document.createElement('input');
@ -65,20 +66,37 @@ bpmSelector.style.width = '4em';
const bpmLabel = document.createElement('label');
bpmLabel.innerText = 'BPM';
bpmLabel.htmlFor = 'bpm';
const progressionSelector = document.createElement('input');
progressionSelector.type = 'number';
progressionSelector.value = '0';
progressionSelector.id = 'progression';
progressionSelector.style.width = '4em';
const progressionLabel = document.createElement('label');
progressionLabel.innerText = 'Progression';
progressionLabel.htmlFor = 'progression';
const playButton = document.createElement('button');
playButton.innerText = "Play";
const numBeats = Math.max(...[...r.animation.values()].map(el => el.reduce((a, s) => a + s.beats, 0)));
beatSlider.max = numBeats.toString();
wrapperDiv.appendChild(bpmSelector);
wrapperDiv.appendChild(bpmLabel);
wrapperDiv.appendChild(playButton);
wrapperDiv.appendChild(document.createElement('br'));
wrapperDiv.appendChild(progressionSelector);
wrapperDiv.appendChild(progressionLabel);
wrapperDiv.appendChild(document.createElement('br'));
wrapperDiv.appendChild(beatSliderLabel);
wrapperDiv.appendChild(beatDisplay);
beatSlider.addEventListener('input', (ev) => {
r.drawSetsWithTrails(parseFloat(beatSlider.value));
beatDisplay.innerText = beatSlider.value;
r.drawSetsWithTrails(beatSlider.valueAsNumber, progressionSelector.valueAsNumber);
beatDisplay.innerText = beatSlider.valueAsNumber.toFixed(1);
restartAnimation(false);
});
progressionSelector.addEventListener('input', (ev) => {
r.drawSetsWithTrails(beatSlider.valueAsNumber, progressionSelector.valueAsNumber);
restartAnimation(false);
});
@ -92,14 +110,14 @@ function restartAnimation(startIfPaused: boolean) {
const bpm = parseFloat(bpmSelector.value);
if (bpm < 0) {
playAnimation(
parseFloat(bpmSelector.value),
beatSlider.value === '0' ? numBeats : parseFloat(beatSlider.value),
bpmSelector.valueAsNumber,
beatSlider.value === '0' ? r.animation.numBeats : beatSlider.valueAsNumber,
0);
} else if (bpm > 0) {
playAnimation(
parseFloat(bpmSelector.value),
beatSlider.value === beatSlider.max ? 0 : parseFloat(beatSlider.value),
numBeats);
bpmSelector.valueAsNumber,
beatSlider.value === beatSlider.max ? 0 : beatSlider.valueAsNumber,
r.animation.numBeats);
}
}
playButton.addEventListener('click', (ev) => {
@ -121,20 +139,30 @@ function playAnimation(bpm: number, start: number, end: number) {
function anim() {
const now = Date.now();
const msElapsed = now - startTime;
const beat = start + msElapsed / msPerBeat;
if (bpm > 0 && beat > end || bpm < 0 && beat < end) {
beatSlider.value = end.toString();
beatDisplay.innerText = end.toString();
r.drawSetsWithTrails(end);
cancelAnim = undefined;
playButton.innerText = 'Play';
} else {
beatSlider.value = beat.toString();
beatDisplay.innerText = beat.toString();
r.drawSetsWithTrails(beat);
cancelAnim = requestAnimationFrame(anim);
playButton.innerText = 'Pause';
let beat = start + msElapsed / msPerBeat;
let changedProgression = false;
if (bpm > 0 && beat > end) {
beat -= r.animation.numBeats;
progressionSelector.valueAsNumber++;
changedProgression = true;
}
if (bpm < 0 && beat < end) {
beat += r.animation.numBeats;
progressionSelector.valueAsNumber--;
changedProgression = true;
}
beatSlider.value = beat.toString();
beatDisplay.innerText = beat.toFixed(1);
r.drawSetsWithTrails(beat, progressionSelector.valueAsNumber);
if (changedProgression) {
restartAnimation(true);
return;
}
cancelAnim = requestAnimationFrame(anim);
playButton.innerText = 'Pause';
}
anim();
}
@ -160,7 +188,8 @@ loadDanceButton.addEventListener('click', (ev) => {
playButton.innerText = 'Play';
}
beatSlider.value = '0';
beatDisplay.innerText = '0';
beatSlider.max = r.animation.numBeats.toString();
beatDisplay.innerText = '0.0';
r.drawSetsWithTrails(0);
buildDebugTable();
});

View File

@ -1,4 +1,4 @@
import { Animation, positionsAtBeat } from "./animation.js";
import { Animation } from "./animation.js";
import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "./danceCommon.js";
import * as exampleAnimations from "./exampleAnimations.js";
import { DancerSetPosition, DancersSetPositions, dancerHeight, dancerHeightOffset, leftShoulder, lineDistance, rightShoulder, setDistance, degreesToRadians } from "./rendererConstants.js";
@ -31,7 +31,7 @@ function colorForDancer(identity: ExtendedDancerIdentity) : string {
function colorForDancerLabel(identity: ExtendedDancerIdentity) : string {
const hue = (hueForDancer(identity.setIdentity) + 180) % 360;
const sat = 100;
const lum = hueForDancer(identity.setIdentity) === 240 || identity.relativeSet < 0
const lum = hueForDancer(identity.setIdentity) === 240 && identity.relativeSet < 2 || identity.relativeSet < 0
? 100
: 20 - identity.relativeSet * 40;
return `hsl(${hue}, ${sat}%, ${lum}%)`;
@ -83,17 +83,21 @@ export class Renderer {
this.ctx.fillStyle = backupFillStyle;
}
drawDancer(position: DancerSetPosition, identity: ExtendedDancerIdentity) {
drawDancer(position: DancerSetPosition, identity: ExtendedDancerIdentity, offsetSets: number) {
this.ctx.save();
this.ctx.translate(identity.relativeLine * lineDistance,
identity.relativeSet * setDistance);
this.ctx.fillStyle = this.ctx.strokeStyle = colorForDancer(identity);
const realIdentity = {
...identity,
relativeSet: identity.relativeSet + (offsetSets * (identity.setIdentity.coupleRole === CoupleRole.Ones ? 1 : -1))
};
this.ctx.fillStyle = this.ctx.strokeStyle = colorForDancer(realIdentity);
this.ctx.translate(position.position.x, position.position.y);
this.ctx.rotate(-degreesToRadians(position.rotation));
this.drawDancerBody(identity);
this.drawDancerBody(realIdentity);
// Draw arms.
this.ctx.lineWidth = 0.03;
@ -113,36 +117,37 @@ export class Renderer {
this.ctx.restore();
}
drawSet(positions: DancersSetPositions, relativeSet: number, relativeLine: number) {
drawSet(positions: DancersSetPositions, relativeSet: number, relativeLine: number, offsetSets: number) {
for (const entry of positions.entries()) {
this.drawDancer(entry[1], { setIdentity: entry[0], relativeLine, relativeSet });
this.drawDancer(entry[1], { setIdentity: entry[0], relativeLine, relativeSet }, offsetSets);
}
}
drawSets(positions: DancersSetPositions) {
drawSets(positions: DancersSetPositions, offsetSets?: number) {
const extraSets = this.extraSets ?? 0;
const extraLines = this.extraLines ?? 0;
for (var relativeLine = -extraLines; relativeLine <= extraLines; relativeLine++) {
for (var relativeSet = -extraSets; relativeSet <= extraSets; relativeSet++) {
this.drawSet(positions, relativeSet, relativeLine);
this.drawSet(positions, relativeSet, relativeLine, offsetSets ?? 0);
}
}
}
drawSetsAtBeat(beat: number) {
this.drawSets(positionsAtBeat(this.animation, beat));
this.drawSets(this.animation.positionsAtBeat(beat));
}
drawSetsWithTrails(beat: number) {
drawSetsWithTrails(beat: number, progression?: number) {
this.clear();
const increments = 10;
const trailLengthInBeats = 1;
const incrementLength = trailLengthInBeats / increments;
progression ??= 0;
const offsetSets = -((progression - (progression % 2)) / 2) / ((this.animation.progression.y * 2) / setDistance);
for (var i = increments; i >= 0; i--) {
const beatToDraw = beat - i*incrementLength;
if (beatToDraw < 0) continue;
this.ctx.globalAlpha = i == 0 ? 1 : (1 - i/increments)*0.3;
this.drawSets(positionsAtBeat(this.animation, beatToDraw));
this.drawSets(this.animation.positionsAtBeat(beatToDraw, progression % 2), offsetSets);
}
}

View File

@ -5,6 +5,31 @@ export interface Offset {
y: number;
}
export function OffsetPlus(a: Offset, b: Offset) {
return {
x: a.x + b.x,
y: a.y + b.y,
};
}
export function OffsetTimes(a: Offset, b: number) {
return {
x: a.x * b,
y: a.y * b,
};
}
export function OffsetMinus(a: Offset, b: Offset) {
return {
x: a.x - b.x,
y: a.y - b.y,
};
}
export function OffsetEquals(a: Offset, b: Offset) {
return a.x === b.x && a.y === b.y;
}
export interface DancerSetPosition {
// Position of the dancer relative to the center of their set.
position: Offset;