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 { CoupleRole, DancerIdentity, Rotation, normalizeRotation } from "./danceCommon.js";
import { DancerSetPosition, DancersSetPositions, Hand } from "./rendererConstants.js"; import { DancerSetPosition, DancersSetPositions, Hand, OffsetEquals, OffsetMinus, OffsetPlus, OffsetTimes, offsetZero } from "./rendererConstants.js";
import { Offset, leftShoulder, rightShoulder, degreesToRadians, radiansToDegrees } from "./rendererConstants.js"; import { Offset, leftShoulder, rightShoulder, degreesToRadians, radiansToDegrees } from "./rendererConstants.js";
export enum AnimationKind { export enum AnimationKind {
@ -355,34 +355,87 @@ export class RotationAnimationSegment extends AnimationSegment {
} }
} }
export type Animation = Map<DancerIdentity, AnimationSegment[]>; export class Animation {
export function newAnimation() : Animation { return new Map<DancerIdentity, AnimationSegment[]>() }; private readonly segments : Map<DancerIdentity, AnimationSegment[]>;
public readonly progression : Offset;
public readonly numBeats : number;
function positionAtBeat(animation: AnimationSegment[], beat: number): DancerSetPosition { constructor(segments: Map<DancerIdentity, AnimationSegment[]>) {
let lastFrame : AnimationSegment = animation[0]; this.segments = segments;
let lastFrameEndBeat = 0; this.numBeats = Math.max(...[...this.segments.values()].map(el => el.reduce((a, s) => a + s.beats, 0)));
for (const frame of animation) {
let currentFrameEndBeat = lastFrameEndBeat + frame.beats; const progressions = new Map<DancerIdentity, Offset>(
if (currentFrameEndBeat < beat) { [...this.segments.entries()]
lastFrame = frame; .map(([id, a]) => [
lastFrameEndBeat = currentFrameEndBeat; id,
continue; OffsetMinus(
} else if (currentFrameEndBeat === beat) { a.at(-1)!.endPosition.position,
return frame.endPosition; a.at(0)!.startPosition.position)]));
} else { this.progression = progressions.get(DancerIdentity.OnesLark)!;
const progress = (beat - lastFrameEndBeat) / (currentFrameEndBeat - lastFrameEndBeat); if (this.progression.x !== 0) {
return frame.positionAtFraction(progress); 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 positionsAtBeat(beat: number, progression?: number): DancersSetPositions {
return lastFrame.endPosition; 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>(); const res = new Map<DancerIdentity, DancerSetPosition>();
for (const [id, animationSegments] of animation.entries()) { for (const [id, animationSegments] of this.segments.entries()) {
res.set(id, positionAtBeat(animationSegments, beat)); 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; return res;
}
} }

View File

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

View File

@ -1,7 +1,7 @@
import * as animation from "./animation.js"; import * as animation from "./animation.js";
import { CoupleRole, DanceRole, DancerIdentity, Rotation } from "./danceCommon.js"; import { CoupleRole, DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
import * as common 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 { nameLibFigureParameters, Move, LibFigureDance, chooser_pairz } from "./libfigureMapper.js";
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js"; import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js";
import { BalanceWeight, CirclePosition, CircleSide, DancerDistance, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "./interpreterCommon.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); 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 // fixup end positions to match start of next move
// TODO Handle progression. // TODO Handle progression.
for (const [id, lowLevelMoves] of res.entries()) { 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[lowLevelMoves.length - 1].endPosition = {
...lowLevelMoves[0].startPosition, ...lowLevelMoves[0].startPosition,
// progressed // progressed
// TODO progression kind? This assumes single progression. Maybe read from endPosition? setOffset: (startPos.setOffset ?? 0) + (id.coupleRole == CoupleRole.Ones ? 1 : -1) * progressionInSets,
setOffset: (startPos.setOffset ?? 0) + (id.coupleRole == CoupleRole.Ones ? 0.5 : -0.5),
}; };
} }
return res; return res;

View File

@ -512,9 +512,9 @@ export function animateLowLevelMove(move: LowLevelMove): animation.AnimationSegm
} }
export function animateFromLowLevelMoves(moves: Map<DancerIdentity, LowLevelMove[]>): animation.Animation { 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()) { for (const [id, moveList] of moves.entries()) {
res.set(id, moveList.flatMap(move => animateLowLevelMove(move))) 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'); const beatDisplay = document.createElement('span');
beatDisplay.className = 'beatDisplay'; beatDisplay.className = 'beatDisplay';
beatDisplay.innerText = '0'; beatDisplay.innerText = '0.0';
const ctx = canvas.getContext('2d')!; const ctx = canvas.getContext('2d')!;
@ -55,6 +55,7 @@ r.extraLines = 3;
r.extraSets = 3; r.extraSets = 3;
r.clear(); r.clear();
r.animation = interpreter.interpretedAnimation; r.animation = interpreter.interpretedAnimation;
beatSlider.max = r.animation.numBeats.toString();
r.drawSetsWithTrails(0); r.drawSetsWithTrails(0);
const bpmSelector = document.createElement('input'); const bpmSelector = document.createElement('input');
@ -65,20 +66,37 @@ bpmSelector.style.width = '4em';
const bpmLabel = document.createElement('label'); const bpmLabel = document.createElement('label');
bpmLabel.innerText = 'BPM'; bpmLabel.innerText = 'BPM';
bpmLabel.htmlFor = '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'); const playButton = document.createElement('button');
playButton.innerText = "Play"; 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(bpmSelector);
wrapperDiv.appendChild(bpmLabel); wrapperDiv.appendChild(bpmLabel);
wrapperDiv.appendChild(playButton); wrapperDiv.appendChild(playButton);
wrapperDiv.appendChild(document.createElement('br')); wrapperDiv.appendChild(document.createElement('br'));
wrapperDiv.appendChild(progressionSelector);
wrapperDiv.appendChild(progressionLabel);
wrapperDiv.appendChild(document.createElement('br'));
wrapperDiv.appendChild(beatSliderLabel); wrapperDiv.appendChild(beatSliderLabel);
wrapperDiv.appendChild(beatDisplay); wrapperDiv.appendChild(beatDisplay);
beatSlider.addEventListener('input', (ev) => { beatSlider.addEventListener('input', (ev) => {
r.drawSetsWithTrails(parseFloat(beatSlider.value)); r.drawSetsWithTrails(beatSlider.valueAsNumber, progressionSelector.valueAsNumber);
beatDisplay.innerText = beatSlider.value; beatDisplay.innerText = beatSlider.valueAsNumber.toFixed(1);
restartAnimation(false);
});
progressionSelector.addEventListener('input', (ev) => {
r.drawSetsWithTrails(beatSlider.valueAsNumber, progressionSelector.valueAsNumber);
restartAnimation(false); restartAnimation(false);
}); });
@ -92,14 +110,14 @@ function restartAnimation(startIfPaused: boolean) {
const bpm = parseFloat(bpmSelector.value); const bpm = parseFloat(bpmSelector.value);
if (bpm < 0) { if (bpm < 0) {
playAnimation( playAnimation(
parseFloat(bpmSelector.value), bpmSelector.valueAsNumber,
beatSlider.value === '0' ? numBeats : parseFloat(beatSlider.value), beatSlider.value === '0' ? r.animation.numBeats : beatSlider.valueAsNumber,
0); 0);
} else if (bpm > 0) { } else if (bpm > 0) {
playAnimation( playAnimation(
parseFloat(bpmSelector.value), bpmSelector.valueAsNumber,
beatSlider.value === beatSlider.max ? 0 : parseFloat(beatSlider.value), beatSlider.value === beatSlider.max ? 0 : beatSlider.valueAsNumber,
numBeats); r.animation.numBeats);
} }
} }
playButton.addEventListener('click', (ev) => { playButton.addEventListener('click', (ev) => {
@ -121,20 +139,30 @@ function playAnimation(bpm: number, start: number, end: number) {
function anim() { function anim() {
const now = Date.now(); const now = Date.now();
const msElapsed = now - startTime; const msElapsed = now - startTime;
const beat = start + msElapsed / msPerBeat;
if (bpm > 0 && beat > end || bpm < 0 && beat < end) { let beat = start + msElapsed / msPerBeat;
beatSlider.value = end.toString(); let changedProgression = false;
beatDisplay.innerText = end.toString(); if (bpm > 0 && beat > end) {
r.drawSetsWithTrails(end); beat -= r.animation.numBeats;
cancelAnim = undefined; progressionSelector.valueAsNumber++;
playButton.innerText = 'Play'; changedProgression = true;
} else {
beatSlider.value = beat.toString();
beatDisplay.innerText = beat.toString();
r.drawSetsWithTrails(beat);
cancelAnim = requestAnimationFrame(anim);
playButton.innerText = 'Pause';
} }
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(); anim();
} }
@ -160,7 +188,8 @@ loadDanceButton.addEventListener('click', (ev) => {
playButton.innerText = 'Play'; playButton.innerText = 'Play';
} }
beatSlider.value = '0'; beatSlider.value = '0';
beatDisplay.innerText = '0'; beatSlider.max = r.animation.numBeats.toString();
beatDisplay.innerText = '0.0';
r.drawSetsWithTrails(0); r.drawSetsWithTrails(0);
buildDebugTable(); 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 { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "./danceCommon.js";
import * as exampleAnimations from "./exampleAnimations.js"; import * as exampleAnimations from "./exampleAnimations.js";
import { DancerSetPosition, DancersSetPositions, dancerHeight, dancerHeightOffset, leftShoulder, lineDistance, rightShoulder, setDistance, degreesToRadians } from "./rendererConstants.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 { function colorForDancerLabel(identity: ExtendedDancerIdentity) : string {
const hue = (hueForDancer(identity.setIdentity) + 180) % 360; const hue = (hueForDancer(identity.setIdentity) + 180) % 360;
const sat = 100; 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 ? 100
: 20 - identity.relativeSet * 40; : 20 - identity.relativeSet * 40;
return `hsl(${hue}, ${sat}%, ${lum}%)`; return `hsl(${hue}, ${sat}%, ${lum}%)`;
@ -83,17 +83,21 @@ export class Renderer {
this.ctx.fillStyle = backupFillStyle; this.ctx.fillStyle = backupFillStyle;
} }
drawDancer(position: DancerSetPosition, identity: ExtendedDancerIdentity) { drawDancer(position: DancerSetPosition, identity: ExtendedDancerIdentity, offsetSets: number) {
this.ctx.save(); this.ctx.save();
this.ctx.translate(identity.relativeLine * lineDistance, this.ctx.translate(identity.relativeLine * lineDistance,
identity.relativeSet * setDistance); 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.translate(position.position.x, position.position.y);
this.ctx.rotate(-degreesToRadians(position.rotation)); this.ctx.rotate(-degreesToRadians(position.rotation));
this.drawDancerBody(identity); this.drawDancerBody(realIdentity);
// Draw arms. // Draw arms.
this.ctx.lineWidth = 0.03; this.ctx.lineWidth = 0.03;
@ -113,36 +117,37 @@ export class Renderer {
this.ctx.restore(); 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()) { 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 extraSets = this.extraSets ?? 0;
const extraLines = this.extraLines ?? 0; const extraLines = this.extraLines ?? 0;
for (var relativeLine = -extraLines; relativeLine <= extraLines; relativeLine++) { for (var relativeLine = -extraLines; relativeLine <= extraLines; relativeLine++) {
for (var relativeSet = -extraSets; relativeSet <= extraSets; relativeSet++) { for (var relativeSet = -extraSets; relativeSet <= extraSets; relativeSet++) {
this.drawSet(positions, relativeSet, relativeLine); this.drawSet(positions, relativeSet, relativeLine, offsetSets ?? 0);
} }
} }
} }
drawSetsAtBeat(beat: number) { 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(); this.clear();
const increments = 10; const increments = 10;
const trailLengthInBeats = 1; const trailLengthInBeats = 1;
const incrementLength = trailLengthInBeats / increments; 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--) { for (var i = increments; i >= 0; i--) {
const beatToDraw = beat - i*incrementLength; const beatToDraw = beat - i*incrementLength;
if (beatToDraw < 0) continue;
this.ctx.globalAlpha = i == 0 ? 1 : (1 - i/increments)*0.3; 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; 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 { export interface DancerSetPosition {
// Position of the dancer relative to the center of their set. // Position of the dancer relative to the center of their set.
position: Offset; position: Offset;