Compare commits

...

5 Commits

6 changed files with 191 additions and 31 deletions

View File

@ -1,5 +1,5 @@
import { CoupleRole, DancerIdentity, Rotation, normalizeRotation } from "./danceCommon.js";
import { DancerSetPosition, DancersSetPositions, Hand, OffsetEquals, OffsetMinus, OffsetPlus, OffsetTimes, offsetZero } from "./rendererConstants.js";
import { DancerSetPosition, DancersSetPositions, Hand, OffsetEquals, OffsetMinus, OffsetPlus, OffsetTimes, offsetZero, setHeight } from "./rendererConstants.js";
import { Offset, leftShoulder, rightShoulder, degreesToRadians, radiansToDegrees } from "./rendererConstants.js";
export enum AnimationKind {
@ -359,6 +359,7 @@ export class Animation {
private readonly segments : Map<DancerIdentity, AnimationSegment[]>;
public readonly progression : Offset;
public readonly numBeats : number;
public readonly progressionError : string | undefined;
constructor(segments: Map<DancerIdentity, AnimationSegment[]>) {
this.segments = segments;
@ -373,19 +374,22 @@ export class Animation {
a.at(0)!.startPosition.position)]));
this.progression = progressions.get(DancerIdentity.OnesLark)!;
if (this.progression.x !== 0) {
throw "Progressing to different line is unsupported.";
this.progressionError = "Progressing to different line is unsupported.";
}
if (this.progression.y === 0) {
throw "Dance does not progress.";
this.progressionError = "Dance does not progress.";
}
if (!OffsetEquals(progressions.get(DancerIdentity.OnesLark)!, progressions.get(DancerIdentity.OnesRobin)!)) {
throw "Ones are not progressing the same as each other.";
this.progressionError = "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.";
this.progressionError = "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.";
this.progressionError = "Ones and Twos are not progressing opposite each other.";
}
if (this.progression.y % setHeight / 2 !== 0) {
this.progressionError = "Progression is not an integer number of places.";
}
}

View File

@ -46,6 +46,8 @@ export class CoupleRole {
}
}
export type StartFormation = "improper" | "Becket" | "Becket ccw" | "Sawtooth Becket";
export enum Rotation {
Up = 180,
Down = 0,

View File

@ -1,10 +1,11 @@
import { StartFormation } from "./danceCommon.js";
import { LibFigureDance } from "./libfigureMapper.js";
export interface ContraDBDance {
id: number,
title: string,
choreographer_name: string,
start_type: "improper" | "Becket" | "Becket ccw" | "Sawtooth Becket",
start_type: StartFormation,
preamble: string,
figures: LibFigureDance,
notes: string,

View File

@ -44,9 +44,6 @@ const handsFourImproper: Map<common.DancerIdentity, SemanticPosition> = new Map<
}],
]);
// Two Hearts in Time by Isaac Banner. Selected arbitrarily.
const exampleDance: LibFigureDance = [{ "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": ["neighbors", "balance", 16], "move": "swing" }, { "parameter_values": ["ladles", true, 540, 8], "move": "allemande" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }, { "parameter_values": ["gentlespoons", 360, 6], "move": "mad robin" }, { "parameter_values": [true, 270, 6], "move": "circle" }, { "parameter_values": ["partners", 4], "move": "California twirl", "progression": 1 }];
function balanceCircleInAndOut(move: Move, startPos: SemanticPosition, balanceBeats?: number): [LowLevelMove, LowLevelMove] {
if (startPos.kind !== PositionKind.Circle) {
throw "Balance circle must start in a circle, but starting at " + startPos;
@ -520,7 +517,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
//throw "Unknown move: " + move.move + ": " + JSON.stringify(move);
for (const [id, startPos] of startingPos.entries()) {
res.set(id, [{
remarks: "UNKNOWN MOVE: standing still",
interpreterError: "UNKNOWN MOVE: standing still",
move,
startBeat: 0,
beats: move.beats,
@ -546,8 +543,6 @@ function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, Se
}
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
@ -568,18 +563,68 @@ function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, Se
return res;
}
function StartingPosForFormation(formation: common.StartFormation): Map<DancerIdentity, SemanticPosition> {
switch (formation) {
case "improper":
return handsFourImproper;
case "Becket":
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
el => {
const [id, pos] = el;
if (pos.kind !== PositionKind.Circle) throw "Unreachable; improper starts in a circle.";
return ([id, {...pos, which: pos.which.circleLeft(1)}])
}
));
case "Becket ccw":
return new Map<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
el => {
const [id, pos] = el;
if (pos.kind !== PositionKind.Circle) throw "Unreachable; improper starts in a circle.";
return ([id, {...pos, which: pos.which.circleRight(1)}])
}
));
case "Sawtooth Becket":
// Dancers start becket, then slide one person to the right. https://contradb.com/dances/848
// TODO Not sure this is right.
return new Map<common.DancerIdentity, SemanticPosition>([
[DancerIdentity.OnesLark, {
kind: PositionKind.Circle,
which: CirclePosition.BottomLeft,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
[DancerIdentity.OnesRobin, {
kind: PositionKind.Circle,
which: CirclePosition.TopLeft,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
[DancerIdentity.TwosLark, {
kind: PositionKind.Circle,
which: CirclePosition.BottomRight,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
[DancerIdentity.TwosRobin, {
kind: PositionKind.Circle,
which: CirclePosition.TopRight,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
setOffset: 1,
}],
]);
}
}
export let mappedDance: Move[];
export let interpretedDance: Map<DancerIdentity, LowLevelMove[]>;
export let interpretedAnimation: animation.Animation;
export function loadDance(dance: LibFigureDance): animation.Animation {
export function loadDance(dance: LibFigureDance, formation: common.StartFormation): animation.Animation {
mappedDance = dance.map(nameLibFigureParameters);
interpretedDance = danceAsLowLevelMoves(mappedDance, handsFourImproper);
interpretedDance = danceAsLowLevelMoves(mappedDance, StartingPosForFormation(formation));
interpretedAnimation = animateFromLowLevelMoves(interpretedDance);
return interpretedAnimation;
}
loadDance(exampleDance);
}

View File

@ -84,6 +84,7 @@ export type SemanticAnimation = {
export interface LowLevelMove {
// for debugging messages
remarks?: string,
interpreterError?: string,
// move is here for reference/debugging, shouldn't actually get used.
move: Move,
// for debugging, if move has been broken into multiple low-level moves, where does this one start

View File

@ -1,4 +1,5 @@
import * as animation from "./animation.js";
import * as danceLibrary from "./danceLibrary.js";
import * as interpreter from "./interpreter.js";
import * as renderer from "./renderer.js";
import { DancerIdentity } from "./danceCommon.js";
@ -56,9 +57,6 @@ const r = new renderer.Renderer(canvas, ctx);
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');
bpmSelector.type = 'number';
@ -88,8 +86,10 @@ autoProgressLabel.innerText = 'Loop playback';
const playButton = document.createElement('button');
playButton.innerText = "Play";
const danceTitle = document.createElement('h1');
const movesList = document.createElement('ul');
wrapperDiv.appendChild(danceTitle);
wrapperDiv.appendChild(movesList);
wrapperDiv.appendChild(document.createElement('br'));
wrapperDiv.appendChild(bpmSelector);
@ -159,10 +159,7 @@ bpmSelector.addEventListener('change', (ev) => {
restartAnimation(false);
});
// Two Hearts in Time by Isaac Banner. Selected arbitrarily.
const exampleDance: LibFigureDance = [{ "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": ["neighbors", "balance", 16], "move": "swing" }, { "parameter_values": ["ladles", true, 540, 8], "move": "allemande" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }, { "parameter_values": ["gentlespoons", 360, 6], "move": "mad robin" }, { "parameter_values": [true, 270, 6], "move": "circle" }, { "parameter_values": ["partners", 4], "move": "California twirl", "progression": 1 }];
// Copied from my dialect settings on contradb.
// Copied from my (Daniel Perelman's) dialect settings on contradb.
const dialect = {
"moves": {
"gyre": "%S shoulder round"
@ -180,19 +177,30 @@ const dialect = {
"text_in_dialect": true
};
let dance: LibFigureDance;
let dance: danceLibrary.ContraDBDance;
function buildMovesList() {
removeAllChildNodes(movesList);
const formation = document.createElement('li');
formation.innerText = 'Starting formation: ' + dance.start_type;
formation.className = 'formation';
movesList.appendChild(formation);
let currentBeat: number = 0;
let lastItem: HTMLLIElement | undefined;
for (const figure of dance) {
for (const figure of dance.figures) {
const startBeat = currentBeat;
currentBeat += figureBeats(figure);
const moveItem = document.createElement('li');
let figureHtml: string;
try {
figureHtml = figureToHtml(figure, dialect);
} catch (e) {
figureHtml = "libfigureError: " + e;
}
moveItem.innerHTML = "[" + labelForBeats(Math.floor(startBeat / 16) * 16) + " "
+ "(" + startBeat + "-" + currentBeat + ")] "
+ figureToHtml(figure, dialect);
+ figureHtml;
moveItem.classList.add('move');
for (let beat = startBeat; beat < currentBeat; beat++) {
moveItem.classList.add('moveForBeat_' + beat);
@ -208,6 +216,22 @@ function buildMovesList() {
if (lastItem) {
lastItem.classList.add('moveForBeat_' + currentBeat);
}
if (r.animation.progressionError) {
const error = document.createElement('li');
error.classList.add('error');
error.classList.add('progressionError');
error.innerText = 'Dance progression does not work; dance was probably interpreted wrong: ' + r.animation.progressionError;
movesList.appendChild(error);
}
const interpreterError = [...interpreter.interpretedDance.values()].flatMap(moves => moves.filter(m => m.interpreterError !== undefined).map(m => m.interpreterError)).at(0);
if (interpreterError) {
const error = document.createElement('li');
error.classList.add('error');
error.classList.add('interpreterError');
error.innerText = 'Interpreter failed on some move, see debug information below for more information: ' + interpreterError;
movesList.appendChild(error);
}
}
function setBeat(beat: number) {
@ -263,8 +287,66 @@ function playAnimation(bpm: number, start: number, end: number) {
anim();
}
// Default dance is Two Hearts in Time by Isaac Banner. Selected arbitrarily.
const defaultDanceTitle = "Two Hearts in Time";
wrapperDiv.appendChild(document.createElement('br'));
const danceList = document.createElement('select');
for (const [idx, dance] of danceLibrary.dances.entries()) {
const opt = document.createElement('option');
opt.value = idx.toString();
opt.innerText = dance.title + " by " + dance.choreographer_name;
if (dance.title === defaultDanceTitle) {
opt.selected = true;
}
danceList.appendChild(opt);
}
danceList.addEventListener('input', () => {
danceJsonArea.value = JSON.stringify(danceLibrary.dances[danceList.value], undefined, 2);
})
wrapperDiv.appendChild(danceList);
const verifyButton = document.createElement('button');
verifyButton.innerText = 'Test loading every dance';
verifyButton.addEventListener('click', () => {
danceList.childNodes.forEach((opt: HTMLOptionElement) => {
if (opt.innerText.startsWith('[')) return; // already processed
const dance = danceLibrary.dances[parseInt(opt.value)];
let interpreterError: string | undefined = undefined;
let progressionError: string | undefined = undefined;
let libfigureError: string | undefined = undefined;
try {
for (const figure of dance.figures) {
figureToHtml(figure, dialect);
}
} catch (e) {
libfigureError = 'libfigure ex: ' + e;
}
try {
progressionError = interpreter.loadDance(dance.figures, dance.start_type).progressionError;
const moveError = [...interpreter.interpretedDance.values()].flatMap(moves => moves.filter(m => m.interpreterError !== undefined).map(m => m.interpreterError)).at(0);
if (moveError) {
interpreterError = "interpreter move error: " + moveError;
}
} catch (e) {
interpreterError = "interpreter exception: " + e;
}
if (!interpreterError && !progressionError && !libfigureError) {
opt.innerText = "[OK] " + opt.innerText;
} else {
const errors = (libfigureError ? "[" + libfigureError + "] " : "")
+ (interpreterError ? "[" + interpreterError + "] " : "")
+ (progressionError ? "[" + progressionError + "] " : "");
opt.innerText = errors + opt.innerText;
}
});
});
wrapperDiv.appendChild(verifyButton);
const danceJsonArea = document.createElement('textarea');
danceJsonArea.value = JSON.stringify(exampleDance, undefined, 2);
danceJsonArea.value = JSON.stringify(danceLibrary.dances.find(d => d.title === defaultDanceTitle), undefined, 2);
danceJsonArea.rows = 15;
danceJsonArea.cols = 30;
const loadDanceButton = document.createElement('button');
@ -275,8 +357,33 @@ wrapperDiv.appendChild(loadDanceButton);
const table = document.createElement('table');
function loadDance() {
dance = JSON.parse(danceJsonArea.value);
r.animation = interpreter.loadDance(dance);
const loadedDance : LibFigureDance | danceLibrary.ContraDBDance = JSON.parse(danceJsonArea.value);
removeAllChildNodes(danceTitle);
if (loadedDance instanceof Array) {
dance = {
figures: loadedDance,
id: 0,
title: "",
choreographer_name: "",
start_type: "improper",
preamble: "",
notes: "",
hook: "",
};
} else {
dance = loadedDance;
const title = document.createElement('a');
title.href = 'https://contradb.com/dances/' + dance.id;
title.innerText = dance.title;
const author = document.createElement('span');
author.innerText = ' by ' + dance.choreographer_name;
danceTitle.appendChild(title);
danceTitle.appendChild(author);
}
r.animation = interpreter.loadDance(dance.figures, dance.start_type);
if (cancelAnim !== undefined) {
cancelAnimationFrame(cancelAnim);
playButton.innerText = 'Play';