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(); } } } }