720 lines
24 KiB
TypeScript
720 lines
24 KiB
TypeScript
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";
|
|
import { LibFigureDance, LibFigureMove, Move } from "./libfigureMapper.js";
|
|
import { animateLowLevelMove, LowLevelMove } from "./lowLevelMove.js";
|
|
import { figureBeats, figureToHtml } from "./libfigure/define-figure.js";
|
|
import { labelForBeats } from "./libfigure/dance.js";
|
|
import { setDistance } from "./rendererConstants.js";
|
|
|
|
const body = document.querySelector('body')!;
|
|
|
|
const wrapperDiv = document.createElement('div');
|
|
const canvasDiv = document.createElement('div');
|
|
canvasDiv.id = 'canvasDiv';
|
|
|
|
const canvas = document.createElement('canvas');
|
|
for (const str of ["left", "right", "top", "bottom"]) {
|
|
const hallLabel = document.createElement('span');
|
|
hallLabel.innerText = str;
|
|
hallLabel.classList.add(str);
|
|
hallLabel.classList.add('hallLabel');
|
|
canvasDiv.appendChild(hallLabel);
|
|
}
|
|
canvasDiv.appendChild(canvas);
|
|
wrapperDiv.appendChild(canvasDiv);
|
|
body.appendChild(wrapperDiv);
|
|
|
|
const beatSliderLabel = document.createElement('label');
|
|
beatSliderLabel.innerText = 'Beat: ';
|
|
const beatSlider = document.createElement('input');
|
|
beatSlider.type = 'range';
|
|
beatSlider.min = '0';
|
|
beatSlider.max = '64';
|
|
beatSlider.step = 'any';
|
|
beatSlider.value = '0';
|
|
beatSliderLabel.appendChild(beatSlider);
|
|
|
|
const beatDisplay = document.createElement('span');
|
|
beatDisplay.className = 'beatDisplay';
|
|
beatDisplay.innerText = '0.0';
|
|
|
|
const ctx = canvas.getContext('2d', { alpha: false })!;
|
|
const r = new renderer.Renderer(canvas, ctx);
|
|
|
|
const defaultSetSizeInPx = canvas.offsetHeight / 3;
|
|
type ResetCanvasSetting = { extraSets?: number; extraLines?: number; setSizeInPx?: number; zoom?: number; };
|
|
function resetCanvas({ extraSets, extraLines, setSizeInPx, zoom }: ResetCanvasSetting) {
|
|
ctx.resetTransform();
|
|
extraLines ??= 0;
|
|
extraSets ??= 0;
|
|
// TODO Redo this on resize?
|
|
const numSets = (1 + 2 * extraSets);
|
|
setSizeInPx = zoom ? zoom * defaultSetSizeInPx : (setSizeInPx ?? defaultSetSizeInPx);
|
|
const w = setSizeInPx * (1 + 2 * extraLines);
|
|
const h = setSizeInPx * numSets;
|
|
const s = window.devicePixelRatio;
|
|
|
|
canvasDiv.style.width = w + 'px';
|
|
canvasDiv.style.height = h + 'px';
|
|
|
|
canvas.width = w * s;
|
|
canvas.height = h * s;
|
|
|
|
ctx.translate(canvas.width / 2.0, canvas.height / 2.0);
|
|
const scale = numSets * setDistance;
|
|
ctx.scale(canvas.height / scale, -canvas.height / scale);
|
|
r.extraLines = Math.ceil(extraLines) + 1;
|
|
r.extraSets = Math.ceil(extraSets) + 1;
|
|
if (r.animation) {
|
|
drawAtCurrentBeat();
|
|
} else {
|
|
r.clear();
|
|
}
|
|
}
|
|
let canvasSetting: ResetCanvasSetting = {
|
|
extraLines: 0.6,
|
|
extraSets: 0.8,
|
|
zoom: 1,
|
|
};
|
|
function updateCanvasSettings(arg : ResetCanvasSetting) {
|
|
if (arg.extraLines !== undefined) canvasSetting.extraLines = arg.extraLines;
|
|
if (arg.extraSets !== undefined) canvasSetting.extraSets = arg.extraSets;
|
|
if (arg.zoom) canvasSetting.zoom = arg.zoom;
|
|
|
|
resetCanvas(canvasSetting);
|
|
}
|
|
|
|
updateCanvasSettings({});
|
|
|
|
const debugRender = document.createElement('input');
|
|
debugRender.type = 'checkbox';
|
|
debugRender.checked = r.drawDebug;
|
|
const debugRenderLabel = document.createElement('label');
|
|
debugRenderLabel.htmlFor = debugRender.id = 'debugRender';
|
|
debugRenderLabel.innerText = 'Debug display';
|
|
|
|
const bpmSelector = document.createElement('input');
|
|
bpmSelector.type = 'number';
|
|
bpmSelector.value = '180';
|
|
bpmSelector.step = '20';
|
|
bpmSelector.id = 'bpm';
|
|
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 autoProgress = document.createElement('input');
|
|
autoProgress.type = 'checkbox';
|
|
autoProgress.checked = true;
|
|
const autoProgressLabel = document.createElement('label');
|
|
autoProgressLabel.htmlFor = autoProgress.id = 'autoProgress';
|
|
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);
|
|
wrapperDiv.appendChild(bpmLabel);
|
|
wrapperDiv.appendChild(playButton);
|
|
wrapperDiv.appendChild(autoProgress);
|
|
wrapperDiv.appendChild(autoProgressLabel);
|
|
wrapperDiv.appendChild(document.createElement('br'));
|
|
wrapperDiv.appendChild(progressionSelector);
|
|
wrapperDiv.appendChild(progressionLabel);
|
|
wrapperDiv.appendChild(document.createElement('br'));
|
|
wrapperDiv.appendChild(beatSliderLabel);
|
|
wrapperDiv.appendChild(beatDisplay);
|
|
|
|
function drawAtCurrentBeat() {
|
|
r.drawSetsWithTrails(beatSlider.valueAsNumber, progressionSelector.valueAsNumber);
|
|
|
|
const moveForCurrent = movesByBeat.at(Math.floor(beatSlider.valueAsNumber));
|
|
if (!moveForCurrent?.classList.contains('currentMove') ?? false) {
|
|
for (let i = 0; i < movesByBeat.length; i++) {
|
|
if (movesByBeat[i].classList.contains('currentMove')) {
|
|
movesByBeat[i].classList.remove('currentMove');
|
|
}
|
|
}
|
|
moveForCurrent?.classList.add('currentMove');
|
|
}
|
|
}
|
|
|
|
debugRender.addEventListener('change', (ev) => {
|
|
r.drawDebug = debugRender.checked;
|
|
drawAtCurrentBeat();
|
|
restartAnimation(false);
|
|
})
|
|
|
|
beatSlider.addEventListener('input', (ev) => {
|
|
drawAtCurrentBeat();
|
|
beatDisplay.innerText = beatSlider.valueAsNumber.toFixed(1);
|
|
restartAnimation(false);
|
|
});
|
|
|
|
progressionSelector.addEventListener('input', (ev) => {
|
|
drawAtCurrentBeat();
|
|
restartAnimation(false);
|
|
});
|
|
|
|
let cancelAnim : number | undefined = undefined;
|
|
function restartAnimation(startIfPaused: boolean) {
|
|
if (!r.animation) return;
|
|
|
|
if (cancelAnim !== undefined) {
|
|
cancelAnimationFrame(cancelAnim);
|
|
} else if (!startIfPaused) {
|
|
return;
|
|
}
|
|
const bpm = parseFloat(bpmSelector.value);
|
|
if (bpm < 0) {
|
|
playAnimation(
|
|
bpmSelector.valueAsNumber,
|
|
beatSlider.value === '0' ? r.animation.numBeats : beatSlider.valueAsNumber,
|
|
0);
|
|
} else if (bpm > 0) {
|
|
playAnimation(
|
|
bpmSelector.valueAsNumber,
|
|
beatSlider.value === beatSlider.max ? 0 : beatSlider.valueAsNumber,
|
|
r.animation.numBeats);
|
|
}
|
|
}
|
|
playButton.addEventListener('click', (ev) => {
|
|
if (cancelAnim !== undefined) {
|
|
cancelAnimationFrame(cancelAnim);
|
|
cancelAnim = undefined;
|
|
playButton.innerText = 'Play';
|
|
} else {
|
|
restartAnimation(true);
|
|
}
|
|
});
|
|
bpmSelector.addEventListener('change', (ev) => {
|
|
restartAnimation(false);
|
|
});
|
|
|
|
// Copied from my (Daniel Perelman's) dialect settings on contradb.
|
|
const dialect = {
|
|
"moves": {
|
|
"gyre": "%S shoulder round"
|
|
},
|
|
"dancers": {
|
|
"ladle": "robin",
|
|
"ladles": "robins",
|
|
"first ladle": "first robin",
|
|
"second ladle": "second robin",
|
|
"gentlespoon": "lark",
|
|
"gentlespoons": "larks",
|
|
"first gentlespoon": "first lark",
|
|
"second gentlespoon": "second lark"
|
|
},
|
|
"text_in_dialect": true
|
|
};
|
|
|
|
let dance: danceLibrary.ContraDBDance;
|
|
|
|
let movesByBeat: HTMLLIElement[] = [];
|
|
function buildMovesList() {
|
|
if (!r.animation) return;
|
|
removeAllChildNodes(movesList);
|
|
movesByBeat = [];
|
|
|
|
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.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;
|
|
}
|
|
const beatsDescription = "[" + labelForBeats(Math.floor(startBeat / 16) * 16) + " "
|
|
+ "(" + startBeat + "-" + currentBeat + ")] ";
|
|
const figureDescription = document.createElement('span');
|
|
figureDescription.innerHTML = beatsDescription + figureHtml;
|
|
|
|
for (const { label, beat } of [
|
|
{ label: '<|', beat: startBeat },
|
|
{ label: '+1', beat: startBeat + 1 },
|
|
{ label: '½', beat: (startBeat + currentBeat) / 2 },
|
|
{ label: '-1', beat: currentBeat - 1 },
|
|
{ label: '|>', beat: currentBeat },
|
|
]) {
|
|
const jumpToButton = document.createElement('button');
|
|
jumpToButton.innerText = label;
|
|
jumpToButton.addEventListener('click', (ev) => {
|
|
setBeat(beat);
|
|
restartAnimation(false);
|
|
});
|
|
moveItem.appendChild(jumpToButton);
|
|
}
|
|
|
|
moveItem.appendChild(figureDescription);
|
|
moveItem.classList.add('move');
|
|
for (let beat = startBeat; beat < currentBeat; beat++) {
|
|
moveItem.classList.add('moveForBeat_' + beat);
|
|
movesByBeat[beat] = moveItem;
|
|
}
|
|
if (startBeat === 0) moveItem.classList.add('currentMove');
|
|
figureDescription.addEventListener('click', (ev) => {
|
|
setBeat(startBeat);
|
|
restartAnimation(false);
|
|
});
|
|
lastItem = moveItem;
|
|
movesList.appendChild(moveItem);
|
|
}
|
|
if (lastItem) {
|
|
lastItem.classList.add('moveForBeat_' + currentBeat);
|
|
movesByBeat[currentBeat] = lastItem;
|
|
}
|
|
|
|
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) {
|
|
beatSlider.value = beat.toString();
|
|
beatDisplay.innerText = beat.toFixed(1);
|
|
|
|
drawAtCurrentBeat();
|
|
}
|
|
function playAnimation(bpm: number, start: number, end: number) {
|
|
const startTime = Date.now();
|
|
const msPerBeat = (60 * 1000) / bpm;
|
|
|
|
function anim() {
|
|
if (!r.animation) return;
|
|
|
|
const now = Date.now();
|
|
const msElapsed = now - startTime;
|
|
|
|
let beat = start + msElapsed / msPerBeat;
|
|
let changedProgression = false;
|
|
function doEnd() {
|
|
setBeat(end);
|
|
cancelAnim = undefined;
|
|
playButton.innerText = 'Play';
|
|
}
|
|
if (bpm > 0 && beat > end) {
|
|
if (autoProgress.checked) {
|
|
beat -= r.animation.numBeats;
|
|
progressionSelector.valueAsNumber++;
|
|
changedProgression = true;
|
|
} else {
|
|
doEnd();
|
|
return;
|
|
}
|
|
}
|
|
if (bpm < 0 && beat < end) {
|
|
if (autoProgress.checked) {
|
|
beat += r.animation.numBeats;
|
|
progressionSelector.valueAsNumber--;
|
|
changedProgression = true;
|
|
} else {
|
|
doEnd();
|
|
return;
|
|
}
|
|
}
|
|
|
|
setBeat(beat);
|
|
if (changedProgression) {
|
|
restartAnimation(true);
|
|
return;
|
|
}
|
|
cancelAnim = requestAnimationFrame(anim);
|
|
playButton.innerText = 'Pause';
|
|
}
|
|
anim();
|
|
}
|
|
|
|
const displaySettingsDiv = document.createElement('div');
|
|
displaySettingsDiv.id = 'displaySettings';
|
|
displaySettingsDiv.style.margin = '1em';
|
|
|
|
const zoomSelector = document.createElement('input');
|
|
zoomSelector.type = 'number';
|
|
zoomSelector.min = '10';
|
|
zoomSelector.step = '10';
|
|
zoomSelector.value = '100';
|
|
zoomSelector.id = 'zoom';
|
|
zoomSelector.style.width = '4em';
|
|
zoomSelector.addEventListener('input', (ev) => {
|
|
updateCanvasSettings({ zoom: zoomSelector.valueAsNumber / 100 });
|
|
})
|
|
const zoomLabel = document.createElement('label');
|
|
zoomLabel.innerText = '% zoom';
|
|
zoomLabel.htmlFor = 'zoom';
|
|
|
|
displaySettingsDiv.appendChild(zoomSelector);
|
|
displaySettingsDiv.appendChild(zoomLabel);
|
|
|
|
const extraSetsSelector = document.createElement('input');
|
|
extraSetsSelector.type = 'number';
|
|
extraSetsSelector.min = '0';
|
|
extraSetsSelector.step = '0.2';
|
|
extraSetsSelector.value = canvasSetting.extraSets!.toPrecision(1);
|
|
extraSetsSelector.id = 'extraSets';
|
|
extraSetsSelector.style.width = '3em';
|
|
extraSetsSelector.addEventListener('input', (ev) => {
|
|
updateCanvasSettings({ extraSets: extraSetsSelector.valueAsNumber });
|
|
})
|
|
const extraSetsLabel = document.createElement('label');
|
|
extraSetsLabel.innerText = '# extra sets: ';
|
|
extraSetsLabel.htmlFor = 'extraSets';
|
|
|
|
displaySettingsDiv.appendChild(document.createElement('br'));
|
|
displaySettingsDiv.appendChild(extraSetsLabel);
|
|
displaySettingsDiv.appendChild(extraSetsSelector);
|
|
|
|
const extraLinesSelector = document.createElement('input');
|
|
extraLinesSelector.type = 'number';
|
|
extraLinesSelector.min = '0';
|
|
extraLinesSelector.step = '0.2';
|
|
extraLinesSelector.value = canvasSetting.extraLines!.toPrecision(1);
|
|
extraLinesSelector.id = 'extraLines';
|
|
extraLinesSelector.style.width = '3em';
|
|
extraLinesSelector.addEventListener('input', (ev) => {
|
|
updateCanvasSettings({ extraLines: extraLinesSelector.valueAsNumber });
|
|
})
|
|
const extraLinesLabel = document.createElement('label');
|
|
extraLinesLabel.innerText = '# extra lines: ';
|
|
extraLinesLabel.htmlFor = 'extraLines';
|
|
|
|
displaySettingsDiv.appendChild(document.createElement('br'));
|
|
displaySettingsDiv.appendChild(extraLinesLabel);
|
|
displaySettingsDiv.appendChild(extraLinesSelector);
|
|
|
|
const trailIncrementsSelector = document.createElement('input');
|
|
trailIncrementsSelector.type = 'number';
|
|
trailIncrementsSelector.min = '0';
|
|
trailIncrementsSelector.step = '1';
|
|
trailIncrementsSelector.value = r.trailIncrements!.toString();
|
|
trailIncrementsSelector.id = 'trailIncrements';
|
|
trailIncrementsSelector.style.width = '3em';
|
|
trailIncrementsSelector.addEventListener('input', (ev) => {
|
|
r.trailIncrements = trailIncrementsSelector.valueAsNumber;
|
|
drawAtCurrentBeat();
|
|
restartAnimation(false);
|
|
})
|
|
const trailIncrementsLabel = document.createElement('label');
|
|
trailIncrementsLabel.innerText = '# trails (faded previous positions): ';
|
|
trailIncrementsLabel.htmlFor = 'trailIncrements';
|
|
|
|
displaySettingsDiv.appendChild(document.createElement('br'));
|
|
displaySettingsDiv.appendChild(trailIncrementsLabel);
|
|
displaySettingsDiv.appendChild(trailIncrementsSelector);
|
|
|
|
const trailBeatsSelector = document.createElement('input');
|
|
trailBeatsSelector.type = 'number';
|
|
trailBeatsSelector.min = '0';
|
|
trailBeatsSelector.step = '0.1';
|
|
trailBeatsSelector.value = r.trailLengthInBeats!.toPrecision(1);
|
|
trailBeatsSelector.id = 'trailBeats';
|
|
trailBeatsSelector.style.width = '3em';
|
|
trailBeatsSelector.addEventListener('input', (ev) => {
|
|
r.trailLengthInBeats = trailBeatsSelector.valueAsNumber;
|
|
drawAtCurrentBeat();
|
|
restartAnimation(false);
|
|
})
|
|
const trailBeatsLabel = document.createElement('label');
|
|
trailBeatsLabel.innerText = '# max age of trails (faded previous positions) in beats: ';
|
|
trailBeatsLabel.htmlFor = 'trailBeats';
|
|
|
|
displaySettingsDiv.appendChild(document.createElement('br'));
|
|
displaySettingsDiv.appendChild(trailBeatsLabel);
|
|
displaySettingsDiv.appendChild(trailBeatsSelector);
|
|
|
|
displaySettingsDiv.appendChild(document.createElement('br'));
|
|
displaySettingsDiv.appendChild(debugRender);
|
|
displaySettingsDiv.appendChild(debugRenderLabel);
|
|
|
|
wrapperDiv.appendChild(displaySettingsDiv);
|
|
|
|
// Default dance is Two Hearts in Time by Isaac Banner. Selected arbitrarily.
|
|
const defaultDanceTitle = "Two Hearts in Time";
|
|
|
|
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).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(danceLibrary.dances.find(d => d.title === defaultDanceTitle), undefined, 2);
|
|
danceJsonArea.rows = 15;
|
|
danceJsonArea.cols = 30;
|
|
const loadDanceButton = document.createElement('button');
|
|
loadDanceButton.innerText = 'Load Dance';
|
|
wrapperDiv.appendChild(document.createElement('br'));
|
|
wrapperDiv.appendChild(danceJsonArea);
|
|
wrapperDiv.appendChild(loadDanceButton);
|
|
|
|
const copyDanceGuide = document.createElement('p');
|
|
copyDanceGuide.appendChild(document.createTextNode("You can get a dance's JSON from ContraDB by using the "))
|
|
const greasemonkeyLink = document.createElement('a');
|
|
greasemonkeyLink.href = 'https://en.wikipedia.org/wiki/Greasemonkey';
|
|
greasemonkeyLink.innerText = 'Greasemonkey';
|
|
copyDanceGuide.appendChild(greasemonkeyLink)
|
|
copyDanceGuide.appendChild(document.createTextNode(' userscript '))
|
|
const userScriptLink = document.createElement('a');
|
|
userScriptLink.href = 'https://git.aweirdimagination.net/perelman/contra-renderer/src/branch/main/ContraDB%20dance%20exporter.user.js';
|
|
userScriptLink.innerText = 'ContraDB dance exporter.user.js';
|
|
copyDanceGuide.appendChild(userScriptLink)
|
|
copyDanceGuide.appendChild(document.createTextNode('. While logged into ContraDB, the "🗐 Copy" button on a dance\'s page will copy the dance to the clipboard.'))
|
|
|
|
wrapperDiv.appendChild(document.createElement('br'));
|
|
wrapperDiv.appendChild(copyDanceGuide)
|
|
|
|
const table = document.createElement('table');
|
|
function loadDance() {
|
|
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);
|
|
if (cancelAnim !== undefined) {
|
|
cancelAnimationFrame(cancelAnim);
|
|
playButton.innerText = 'Play';
|
|
}
|
|
progressionSelector.value = '0';
|
|
beatSlider.value = '0';
|
|
beatSlider.max = r.animation.numBeats.toString();
|
|
beatDisplay.innerText = '0.0';
|
|
r.drawSetsWithTrails(0);
|
|
buildDebugTable();
|
|
buildMovesList();
|
|
}
|
|
loadDanceButton.addEventListener('click', loadDance);
|
|
loadDance();
|
|
|
|
function createJsonCell(content: any, rowSpan?: number, id?: DancerIdentity) {
|
|
const cell = document.createElement('td');
|
|
const pre = document.createElement('pre');
|
|
|
|
// from https://stackoverflow.com/a/56150320
|
|
function replacer(key, value) {
|
|
if (value instanceof Map) {
|
|
let res = {};
|
|
for (var [k, v] of value.entries()) {
|
|
res[k === undefined ? "undefined" : k.toString()] = v;
|
|
}
|
|
return res;
|
|
} else if (!value) {
|
|
return value;
|
|
} else if (value.enumValue) {
|
|
return value.enumValue;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
pre.innerText = content ? JSON.stringify(content, replacer, 2) : "";
|
|
cell.appendChild(pre);
|
|
if (rowSpan) {
|
|
cell.rowSpan = rowSpan;
|
|
}
|
|
if (id) {
|
|
cell.classList.add(id.coupleRole.toString());
|
|
cell.classList.add(id.danceRole.toString());
|
|
}
|
|
return cell;
|
|
}
|
|
function createHeaderCell(content: string, id?: DancerIdentity) {
|
|
const cell = document.createElement('th');
|
|
cell.innerText = content;
|
|
if (id) {
|
|
cell.classList.add(id.coupleRole.toString());
|
|
cell.classList.add(id.danceRole.toString());
|
|
cell.colSpan = 2;
|
|
}
|
|
return cell;
|
|
}
|
|
|
|
const showDebug = document.createElement('input');
|
|
showDebug.type = 'checkbox';
|
|
showDebug.id = 'showDebug';
|
|
const showDebugLabel = document.createElement('label');
|
|
showDebugLabel.innerText = 'Show Debug Information';
|
|
showDebugLabel.htmlFor = 'showDebug';
|
|
|
|
body.appendChild(showDebug);
|
|
body.appendChild(showDebugLabel);
|
|
|
|
table.id = 'debug'
|
|
|
|
function removeAllChildNodes(html: HTMLElement) {
|
|
while (html.childNodes.length > 0) {
|
|
html.removeChild(html.childNodes[html.childNodes.length - 1]);
|
|
}
|
|
}
|
|
function buildDebugTable() {
|
|
removeAllChildNodes(table);
|
|
|
|
const headerRow = document.createElement('tr');
|
|
const roles = [DancerIdentity.OnesLark, DancerIdentity.OnesRobin,
|
|
DancerIdentity.TwosLark, DancerIdentity.TwosRobin];
|
|
headerRow.appendChild(createHeaderCell("Move"));
|
|
for (const role of roles) {
|
|
headerRow.appendChild(createHeaderCell(role.toString(), role));
|
|
}
|
|
table.appendChild(headerRow);
|
|
|
|
const byMove: { move: Move, byRole: Map<DancerIdentity, { lowLevelMove?: LowLevelMove, animationSegment: animation.AnimationSegment, numSegments?: number }[]> }[] = [];
|
|
for (const [role, moveList] of interpreter.interpretedDance.entries()) {
|
|
for (const move of moveList) {
|
|
let entry = byMove.find(el => el.move === move.move);
|
|
if (!entry) {
|
|
entry = { move: move.move, byRole: new Map<DancerIdentity, { lowLevelMove?: LowLevelMove, animationSegment: animation.AnimationSegment, numSegments?: number }[]>() };
|
|
byMove.push(entry);
|
|
}
|
|
let forRole = entry.byRole.get(role);
|
|
if (!forRole) {
|
|
forRole = [];
|
|
entry.byRole.set(role, forRole);
|
|
}
|
|
const animation = animateLowLevelMove(move);
|
|
let first = true;
|
|
for (const animationSegment of animation) {
|
|
if (first) {
|
|
forRole.push({ lowLevelMove: move, animationSegment, numSegments: animation.length });
|
|
first = false;
|
|
} else {
|
|
forRole.push({ animationSegment });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const infoForMove of byMove) {
|
|
const moveRow = document.createElement('tr');
|
|
const numRows = Math.max(...[...infoForMove.byRole.values()].map(l => l.length));
|
|
moveRow.appendChild(createJsonCell(infoForMove.move, numRows));
|
|
|
|
for (let i = 0; i < numRows; i++) {
|
|
const row = i == 0 ? moveRow : document.createElement('tr');
|
|
|
|
for (const role of roles) {
|
|
const lowLevelMove = infoForMove.byRole.get(role)?.at(i);
|
|
if (!lowLevelMove) {
|
|
row.appendChild(createJsonCell(undefined, undefined, role));
|
|
} else if (lowLevelMove.lowLevelMove) {
|
|
row.appendChild(createJsonCell(lowLevelMove.lowLevelMove, lowLevelMove.numSegments, role));
|
|
}
|
|
row.appendChild(createJsonCell(lowLevelMove?.animationSegment, undefined, role));
|
|
}
|
|
|
|
table.appendChild(row);
|
|
}
|
|
}
|
|
}
|
|
buildDebugTable();
|
|
|
|
body.appendChild(table);
|