Compare commits
4 Commits
abecb02112
...
17a49d8c45
Author | SHA1 | Date | |
---|---|---|---|
17a49d8c45 | |||
3d073efce8 | |||
d3e8cedef2 | |||
d7e1af26bf |
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user