diff --git a/nise-replay-viewer/public/hit100.png b/nise-replay-viewer/public/hit100.png new file mode 100644 index 0000000..26f7bbf Binary files /dev/null and b/nise-replay-viewer/public/hit100.png differ diff --git a/nise-replay-viewer/public/hit300.png b/nise-replay-viewer/public/hit300.png new file mode 100644 index 0000000..26f7bbf Binary files /dev/null and b/nise-replay-viewer/public/hit300.png differ diff --git a/nise-replay-viewer/public/hit50.png b/nise-replay-viewer/public/hit50.png new file mode 100644 index 0000000..d45dacc Binary files /dev/null and b/nise-replay-viewer/public/hit50.png differ diff --git a/nise-replay-viewer/public/hitcircleoverlay.png b/nise-replay-viewer/public/hitcircleoverlay.png index a79aad2..3134106 100644 Binary files a/nise-replay-viewer/public/hitcircleoverlay.png and b/nise-replay-viewer/public/hitcircleoverlay.png differ diff --git a/nise-replay-viewer/public/hitmiss.png b/nise-replay-viewer/public/hitmiss.png new file mode 100644 index 0000000..0af3170 Binary files /dev/null and b/nise-replay-viewer/public/hitmiss.png differ diff --git a/nise-replay-viewer/src/osu/Drawer.ts b/nise-replay-viewer/src/osu/Drawer.ts index c4277fd..ca45bbf 100644 --- a/nise-replay-viewer/src/osu/Drawer.ts +++ b/nise-replay-viewer/src/osu/Drawer.ts @@ -2,7 +2,7 @@ import { Vector2 } from "osu-classes"; import p5 from "p5"; import { loadImageAsync } from "@/utils"; import { Md5 } from "ts-md5"; -import {OsuRenderer} from "@/osu/OsuRenderer"; +import {Judgements, OsuRenderer} from "@/osu/OsuRenderer"; export class Drawer { @@ -27,6 +27,9 @@ export class Drawer { default7: undefined as any as p5.Image, default8: undefined as any as p5.Image, default9: undefined as any as p5.Image, + hitmiss: undefined as any as p5.Image, + hit50: undefined as any as p5.Image, + hit100: undefined as any as p5.Image }; private static p: p5; @@ -50,27 +53,127 @@ export class Drawer { this.p.drawingContext.globalAlpha = opacity; } - static drawCircleJudgement( + /** + * Draws the error bar for the given judgements. + * @param judgements + */ + static drawErrorBar(judgements: Judgements[]) { + let width = 512; + let height = 384; + + // TODO: Calculate real values + // @https://osu.ppy.sh/wiki/en/Beatmap/Overall_difficulty + let hitWindow300 = 50; + let hitWindow100 = 100; + let hitWindow50 = 150; + + let barWidth = 512 * 0.8; // Width of the error bar, 80% of canvas width + let barHeight = 20; // The height of the error bar + let barX = (width - barWidth) / 2; // Center the bar on the canvas + let barY = height + 52; + + // Draw the background for the 50's hit window (biggest) + Drawer.p.fill(217, 174, 71); + Drawer.p.rect(barX + barWidth / 2 - (hitWindow50), barY, hitWindow50 * 2, barHeight); + + // Draw the background for the 100's hit window (middle) + Drawer.p.fill(87, 225, 19); + Drawer.p.rect(barX + barWidth / 2 - (hitWindow100), barY, hitWindow100 * 2, barHeight); + + // Draw the background for the 300's hit window (smallest/closer to the center) + Drawer.p.fill(50, 188, 231); + Drawer.p.rect(barX + barWidth / 2 - (hitWindow300), barY, hitWindow300 * 2, barHeight); + + let tickDensity = new Array(Math.ceil(barWidth / 5)).fill(0); // Example: Each segment is 5 pixels wide + judgements.forEach(judgement => { + let judgementX = barX + barWidth / 2 + judgement.error / 2; + this.drawJudgementTick(judgementX, barY, barHeight, tickDensity); + }); + + // Draw a vertical bar in the exact center of the hit error bar + // Drawer.p.push(); + // Drawer.p.noFill(); + // Drawer.p.stroke(255); + // Drawer.p.strokeWeight(2); + // Drawer.p.line(barX + barWidth / 2, barY - 5, barX + barWidth / 2, barY + barHeight + 5); + + Drawer.p.pop(); + } + + //@ts-ignore + static drawJudgementTick(x, y, height, tickDensity) { + let barWidth = 512 * 0.8; + let barX = (512 - barWidth) / 2; // Center the bar on the canvas + + const segmentIndex = Math.floor((x - barX) / 5); // Assuming 5-pixel segments + tickDensity[segmentIndex]++; + + const opacity = this.calculateOpacity(tickDensity[segmentIndex]); + + Drawer.p.stroke(255, 255, 255, 255); + Drawer.p.strokeWeight(2); + Drawer.p.strokeCap(Drawer.p.SQUARE); // Slightly thicker ends for clarity + Drawer.p.colorMode(Drawer.p.RGB); + Drawer.p.stroke(Drawer.p.color(255, 255, 255, 255 * opacity)); // Apply opacity + Drawer.p.line(x, y, x, y + height); + } + + /** + * Calculate the opacity of the judgement tick. Lower opacity means less amount. + * + * @param count The amount of ticks + * @returns The opacity of the tick (0-1) + */ + static calculateOpacity(count: number): number { + const MIN_OPACITY = 0.55; // Minimum opacity enforced + const MAX_OPACITY = 1; // Maximum opacity + const MAX_DENSITY = 50; // Opacity reaches maximum at this tick count + + // Calculate linear opacity based on count + let opacity = (count / MAX_DENSITY) * (MAX_OPACITY - MIN_OPACITY) + MIN_OPACITY; + + // Ensure opacity is at least MIN_OPACITY and at most MAX_OPACITY + opacity = Math.max(opacity, MIN_OPACITY); + opacity = Math.min(opacity, MAX_OPACITY); + + return opacity; + } + + + static drawJudgement( position: Vector2, - radius: number, - judgement: string + judgement: string, + opacity: number ) { Drawer.p.push(); - Drawer.p.strokeWeight(2); - if (judgement === "OK") { - Drawer.p.stroke(`rgb(106, 176, 76)`); - Drawer.p.fill(`rgb(106, 176, 76)`); + //@ts-ignore + Drawer.p.drawingContext.globalAlpha = opacity; + if (judgement === "ONE_HUNDRED") { + Drawer.p.image( + this.images.hit100, + position.x, + position.y, + this.images.hit100.width, + this.images.hit100.height + ); } - if (judgement === "MEH") { - Drawer.p.stroke(`rgb(241, 196, 15)`); - Drawer.p.fill(`rgb(241, 196, 15)`); + if (judgement === "FIFTY") { + Drawer.p.image( + this.images.hit50, + position.x, + position.y, + this.images.hit50.width, + this.images.hit50.height + ); } if (judgement === "MISS") { - Drawer.p.stroke(`rgb(231, 76, 60)`); - Drawer.p.fill(`rgb(231, 76, 60)`); - } - if (judgement !== "GREAT") { - Drawer.p.circle(position.x, position.y, radius * 2); + Drawer.p.image( + this.images.hitmiss, + position.x, + position.y, + this.images.hitmiss.width, + this.images.hitmiss.height + ); } Drawer.p.pop(); } @@ -259,14 +362,12 @@ export class Drawer { let size = 4; if (lastFrame.button.mouseLeft1 || lastFrame.button.mouseLeft2) { Drawer.p.stroke("#BB6BD9"); - // TODO: Draw an X instead Drawer.p.line(lastFrame.position.x - size, lastFrame.position.y - size, lastFrame.position.x + size, lastFrame.position.y + size); Drawer.p.line(lastFrame.position.x + size, lastFrame.position.y - size, lastFrame.position.x - size, lastFrame.position.y + size); } if (lastFrame.button.mouseRight1 || lastFrame.button.mouseRight2) { Drawer.p.stroke("#F2994A"); - // TODO: Draw an X instead Drawer.p.line(lastFrame.position.x - size, lastFrame.position.y - size, lastFrame.position.x + size, lastFrame.position.y + size); Drawer.p.line(lastFrame.position.x + size, lastFrame.position.y - size, lastFrame.position.x - size, lastFrame.position.y + size); } diff --git a/nise-replay-viewer/src/osu/OsuRenderer.ts b/nise-replay-viewer/src/osu/OsuRenderer.ts index f4552c3..d2cc835 100644 --- a/nise-replay-viewer/src/osu/OsuRenderer.ts +++ b/nise-replay-viewer/src/osu/OsuRenderer.ts @@ -26,7 +26,7 @@ export enum OsuRendererEvents { SETTINGS = "SETTINGS", } -interface Judgements { +export interface Judgements { x: number; y: number; time: number; @@ -141,6 +141,8 @@ export class OsuRenderer { this.updateStatistics(); } + Drawer.drawErrorBar(this.judgements.filter(j => j.time < this.time && j.time - this.time > -10000)); + this.lastRender = Date.now(); this.renderPath(this.replay.replay!, Drawer.images.cursor); @@ -475,15 +477,16 @@ export class OsuRenderer { hitObject.currentComboIndex ); - // if (GameplayAnalyzer.renderJudgements[hitObject.startTime]) { - // Drawer.setDrawingOpacity(opacity / 2); - // - // Drawer.drawCircleJudgement( - // hitObject.stackedStartPosition, - // hitObject.radius, - // GameplayAnalyzer.renderJudgements[hitObject.startTime] - // ); - // } + let judgementsToShow = this.judgements.filter(j => j.time < this.time && j.time > hitObject.startTime); + if(judgementsToShow.length > 0) { + let judgement = judgementsToShow[0]; + Drawer.drawJudgement( + hitObject.stackedStartPosition, + judgement.type, + opacity + ); + } + Drawer.endDrawing(); return arScale; } @@ -531,6 +534,16 @@ export class OsuRenderer { Drawer.drawSliderFollowPoint(sliderPos, hitObject.radius); } + let judgementsToShow = this.judgements.filter(j => j.time < this.time && j.time > hitObject.startTime); + if(judgementsToShow.length > 0) { + let judgement = judgementsToShow[0]; + Drawer.drawJudgement( + hitObject.stackedStartPosition, + judgement.type, + opacity + ); + } + Drawer.endDrawing(); return arScale; }