Compare commits
5 Commits
abf26a0a59
...
5625e0c83a
Author | SHA1 | Date | |
---|---|---|---|
5625e0c83a | |||
7e63c07cda | |||
eefea94149 | |||
a1ecc7ec1a | |||
6433b30ed1 |
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ export class CoupleRole {
|
|||
}
|
||||
}
|
||||
|
||||
export type StartFormation = "improper" | "Becket" | "Becket ccw" | "Sawtooth Becket";
|
||||
|
||||
export enum Rotation {
|
||||
Up = 180,
|
||||
Down = 0,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
|
@ -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
|
||||
|
|
133
www/js/main.ts
133
www/js/main.ts
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue
Block a user