contra-renderer/www/js/renderer.ts

231 lines
8.6 KiB
TypeScript

import { Animation } from "./animation.js";
import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "./danceCommon.js";
import { DancerSetPosition, DancersSetPositions, dancerHeight, dancerHeightOffset, leftShoulder, lineDistance, rightShoulder, setDistance, degreesToRadians } from "./rendererConstants.js";
function baseColorForDancer(identity: ExtendedDancerIdentity): {hue: number, sat: number, lum: number} {
if (identity.setIdentity.coupleRole == CoupleRole.Ones) {
if (identity.relativeSet < 0)
return { hue: 340, sat: 67, lum: 56 };
if (identity.relativeSet === 0)
return { hue: 27, sat: 99, lum: 59 };
if (identity.relativeSet > 0)
return { hue: 54, sat: 97, lum: 49 };
} else {
if (identity.relativeSet < 0)
return { hue: 183, sat: 88, lum: 23 };
if (identity.relativeSet === 0)
return { hue: 249, sat: 42, lum: 37 };
if (identity.relativeSet > 0)
return { hue: 13, sat: 33, lum: 29 };
}
throw new Error("Unreachable: relativeSet must be one of <, ===, or > 0.");
}
function colorForDancer(identity: ExtendedDancerIdentity) : string {
const baseColor = baseColorForDancer(identity);
const hue = baseColor.hue;
const sat = baseColor.sat - Math.abs(identity.relativeLine * 40);
const unclampedLum = baseColor.lum + (Math.abs(identity.relativeSet) <= 1 ? 0 : identity.relativeSet * 20);
const lum = unclampedLum < 10 ? 10 : unclampedLum > 90 ? 90 : unclampedLum;
return `hsl(${hue}, ${sat}%, ${lum}%)`;
}
function colorForDancerLabel(identity: ExtendedDancerIdentity) : string {
const dancerColor = baseColorForDancer(identity);
const hue = (dancerColor.hue + 180) % 360;
const sat = 100;
const unclampedLum = ((dancerColor.hue >= 215 && dancerColor.hue <= 285) || (identity.relativeSet < 0)
|| dancerColor.lum < 40) && identity.relativeSet < 2
? 100
: 20 - identity.relativeSet * 40;
const lum = unclampedLum < 0 ? 0 : unclampedLum > 100 ? 100 : unclampedLum;
return `hsl(${hue}, ${sat}%, ${lum}%)`;
}
export class Renderer {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
animation?: Animation;
extraSets?: number;
extraLines?: number;
trailIncrements: number = 6;
trailLengthInBeats: number = 1;
drawDebug: boolean = false;
constructor(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
this.canvas = canvas;
this.ctx = ctx;
}
drawDancerBody(identity: ExtendedDancerIdentity, drawText: boolean) {
this.ctx.beginPath();
this.ctx.moveTo(leftShoulder.x, leftShoulder.y);
if (identity.setIdentity.danceRole === DanceRole.Robin) {
// Draw triangle for robin.
this.ctx.lineTo(rightShoulder.x, rightShoulder.y);
this.ctx.lineTo(0, dancerHeight-dancerHeightOffset);
} else {
// Draw dome for lark.
this.ctx.arcTo(0, dancerHeight*2-dancerHeightOffset, rightShoulder.x, rightShoulder.y, dancerHeight * 1.5);
this.ctx.lineTo(rightShoulder.x, rightShoulder.y);
}
this.ctx.fill();
// Draw dot at origin to identify "center" point of dancer.
const backupFillStyle = this.ctx.fillStyle;
this.ctx.fillStyle = 'black';
const pointSize = 0.05;
this.ctx.fillRect(-pointSize/2, -pointSize/2, pointSize, pointSize);
if (drawText && identity) {
this.ctx.save();
this.ctx.scale(-1, 1);
this.ctx.rotate(Math.PI);
this.ctx.fillStyle = colorForDancerLabel(identity);
this.ctx.font = '0.15px sans'
this.ctx.fillText(identity.setIdentity.danceRole === DanceRole.Lark ? 'L' : 'R', -0.14, +0.04);
this.ctx.fillText(identity.setIdentity.coupleRole === CoupleRole.Ones ? '1' : '2', +0.04, +0.04);
this.ctx.font = '0.1px sans'
this.ctx.fillText(identity.relativeLine.toString(), +0.14, +0.04);
this.ctx.fillText(identity.relativeSet.toString(), -0.22, +0.04);
this.ctx.restore();
}
this.ctx.fillStyle = backupFillStyle;
}
drawDancer(position: DancerSetPosition, identity: ExtendedDancerIdentity, offsetSets: number, drawText: boolean, drawDebug: boolean) {
this.ctx.save();
this.ctx.translate(identity.relativeLine * lineDistance,
identity.relativeSet * setDistance);
const realIdentity = {
...identity,
relativeSet: identity.relativeSet + (offsetSets * (identity.setIdentity.coupleRole === CoupleRole.Ones ? 1 : -1))
};
this.ctx.fillStyle = this.ctx.strokeStyle = colorForDancer(realIdentity);
if (drawDebug) {
if (this.drawDebug && position.drawDebug) {
this.ctx.save();
this.ctx.lineWidth = 0.05;
position.drawDebug(this.ctx);
this.ctx.restore();
}
} else {
this.ctx.translate(position.position.x, position.position.y);
this.ctx.rotate(-degreesToRadians(position.rotation));
this.drawDancerBody(realIdentity, drawText);
// Draw arms.
this.ctx.lineWidth = 0.03;
if (position.leftArmEnd) {
this.ctx.beginPath();
this.ctx.moveTo(leftShoulder.x, leftShoulder.y);
this.ctx.lineTo(position.leftArmEnd.x, position.leftArmEnd.y);
this.ctx.stroke();
}
if (position.rightArmEnd) {
this.ctx.beginPath();
this.ctx.moveTo(rightShoulder.x, rightShoulder.y);
this.ctx.lineTo(position.rightArmEnd.x, position.rightArmEnd.y);
this.ctx.stroke();
}
}
this.ctx.restore();
}
drawSet(positions: DancersSetPositions, relativeSet: number, relativeLine: number, offsetSets: number, drawText: boolean, drawDebug: boolean) {
for (const entry of positions.entries()) {
this.drawDancer(entry[1], { setIdentity: entry[0], relativeLine, relativeSet }, offsetSets, drawText, drawDebug);
}
}
drawSets(positions: DancersSetPositions, offsetSets?: number, drawText?: boolean, drawDebug?: boolean) {
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, offsetSets ?? 0, drawText ?? true, !!drawDebug);
}
}
}
drawSetsAtBeat(beat: number) {
if (!this.animation) throw new Error("Attempted to render before setting animation.");
this.drawSets(this.animation.positionsAtBeat(beat));
}
drawSetsWithTrails(beat: number, progression?: number) {
if (!this.animation) throw new Error("Attempted to render before setting animation.");
this.clear();
const increments = this.trailLengthInBeats > 0 && this.trailIncrements > 0 ? this.trailIncrements : 0;
const incrementLength = this.trailLengthInBeats / (increments + 1);
progression ??= 0;
const offsetSets = this.animation.progression.y === 0
? 0
: -((progression - (progression % 2)) / 2) / ((this.animation.progression.y * 2) / setDistance);
for (var i = increments; i >= 0; i--) {
const beatToDraw = i == 0 ? beat : beat - i*incrementLength;
this.ctx.globalAlpha = i == 0 ? 1 : (1 - i / (increments + 1)) * 0.3;
const positions = this.animation.positionsAtBeat(beatToDraw, progression % 2);
if (this.drawDebug) this.drawSets(positions, offsetSets, true, true);
this.drawSets(positions, offsetSets, i === 0, false);
}
}
playAnimation(bpm: number, start: number, end: number) {
const startTime = Date.now();
const msPerBeat = (60*1000)/bpm;
const drawSetsWithTrails = (b: number) => this.drawSetsWithTrails(b);
function anim() {
const now = Date.now();
const msElapsed = now - startTime;
const beat = start + msElapsed/msPerBeat;
if (beat > end) {
drawSetsWithTrails(end);
} else {
drawSetsWithTrails(beat);
requestAnimationFrame(anim);
}
}
anim();
}
clear() {
// TODO Right way to clear?
this.ctx.fillStyle = 'white';
this.ctx.fillRect(-this.canvas.width, -this.canvas.height, 2*this.canvas.width, 2*this.canvas.height);
const extraLines = this.extraLines ?? 0;
const extraSets = this.extraSets ?? 0;
for (var relativeLine = -extraLines; relativeLine <= extraLines; relativeLine++) {
for (var relativeSet = -extraSets; relativeSet <= extraSets; relativeSet++) {
this.ctx.save();
const hue = 0;
const sat = 0;
const lum = Math.min(98, 90 + Math.abs(Math.abs(relativeSet) + Math.abs(relativeLine)) * 5);
this.ctx.fillStyle = `hsl(${hue}, ${sat}%, ${lum}%)`;
this.ctx.translate(relativeLine * lineDistance,
relativeSet * setDistance);
this.ctx.fillRect(-1.975, -1.975, 3.95, 3.95);
this.ctx.restore();
}
}
}
}