Work on replay viewer
This commit is contained in:
parent
7d0d3b676f
commit
e9f7d29b46
@ -1,5 +1,6 @@
|
|||||||
package com.nisemoe.nise
|
package com.nisemoe.nise
|
||||||
|
|
||||||
|
import com.nisemoe.nise.integrations.CircleguardService
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@ -96,7 +97,8 @@ data class ReplayDataSimilarScore(
|
|||||||
|
|
||||||
data class ReplayViewerData(
|
data class ReplayViewerData(
|
||||||
val replay: String,
|
val replay: String,
|
||||||
val beatmap: String,
|
val beatmap: CircleguardService.BeatmapResponse,
|
||||||
|
val mods: List<String>
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ReplayData(
|
data class ReplayData(
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.nisemoe.generated.tables.records.ScoresJudgementsRecord
|
|||||||
import com.nisemoe.generated.tables.records.ScoresRecord
|
import com.nisemoe.generated.tables.records.ScoresRecord
|
||||||
import com.nisemoe.generated.tables.references.*
|
import com.nisemoe.generated.tables.references.*
|
||||||
import com.nisemoe.nise.*
|
import com.nisemoe.nise.*
|
||||||
|
import com.nisemoe.nise.integrations.CircleguardService
|
||||||
import com.nisemoe.nise.osu.Mod
|
import com.nisemoe.nise.osu.Mod
|
||||||
import com.nisemoe.nise.service.AuthService
|
import com.nisemoe.nise.service.AuthService
|
||||||
import com.nisemoe.nise.service.CompressJudgements
|
import com.nisemoe.nise.service.CompressJudgements
|
||||||
@ -21,6 +22,7 @@ import kotlin.math.roundToInt
|
|||||||
@Service
|
@Service
|
||||||
class ScoreService(
|
class ScoreService(
|
||||||
private val dslContext: DSLContext,
|
private val dslContext: DSLContext,
|
||||||
|
private val circleguardService: CircleguardService,
|
||||||
private val beatmapService: BeatmapService,
|
private val beatmapService: BeatmapService,
|
||||||
private val authService: AuthService,
|
private val authService: AuthService,
|
||||||
private val compressJudgements: CompressJudgements
|
private val compressJudgements: CompressJudgements
|
||||||
@ -45,6 +47,7 @@ class ScoreService(
|
|||||||
fun getReplayViewerData(replayId: Long): ReplayViewerData? {
|
fun getReplayViewerData(replayId: Long): ReplayViewerData? {
|
||||||
val result = dslContext.select(
|
val result = dslContext.select(
|
||||||
SCORES.REPLAY,
|
SCORES.REPLAY,
|
||||||
|
SCORES.MODS,
|
||||||
BEATMAPS.BEATMAP_FILE
|
BEATMAPS.BEATMAP_FILE
|
||||||
)
|
)
|
||||||
.from(SCORES)
|
.from(SCORES)
|
||||||
@ -61,9 +64,15 @@ class ScoreService(
|
|||||||
|
|
||||||
if(result.get(BEATMAPS.BEATMAP_FILE, String::class.java) == null) return null
|
if(result.get(BEATMAPS.BEATMAP_FILE, String::class.java) == null) return null
|
||||||
|
|
||||||
|
val mods = result.get(SCORES.MODS, Int::class.java)
|
||||||
|
|
||||||
|
val beatmapFile = result.get(BEATMAPS.BEATMAP_FILE, String::class.java)
|
||||||
|
val beatmapData = this.circleguardService.processBeatmap(beatmapFile, mods = mods).get()
|
||||||
|
|
||||||
return ReplayViewerData(
|
return ReplayViewerData(
|
||||||
replay = replayData,
|
replay = replayData,
|
||||||
beatmap = result.get(BEATMAPS.BEATMAP_FILE, String::class.java)
|
beatmap = beatmapData,
|
||||||
|
mods = Mod.parseModCombination(mods)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -106,6 +106,105 @@ class CircleguardService {
|
|||||||
replayResponse.error_skewness = replayResponse.error_skewness?.times(conversionFactor)
|
replayResponse.error_skewness = replayResponse.error_skewness?.times(conversionFactor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BeatmapRequest(
|
||||||
|
val beatmap_file: String,
|
||||||
|
val mods: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Circle(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val time: Double,
|
||||||
|
val new_combo: Boolean,
|
||||||
|
val current_combo: Int,
|
||||||
|
val combo_color: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SliderCurvePoint(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SliderCurve(
|
||||||
|
val type: String,
|
||||||
|
val points: List<SliderCurvePoint>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Slider(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val time: Double,
|
||||||
|
val end_time: Double,
|
||||||
|
val curve: SliderCurve,
|
||||||
|
val length: Double,
|
||||||
|
val new_combo: Boolean,
|
||||||
|
val current_combo: Int,
|
||||||
|
val combo_color: String,
|
||||||
|
val repeat: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Spinner(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val time: Double,
|
||||||
|
val end_time: Double,
|
||||||
|
val new_combo: Boolean,
|
||||||
|
val current_combo: Int,
|
||||||
|
val combo_color: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Difficulty(
|
||||||
|
val hp_drain_rate: Double,
|
||||||
|
val circle_size: Double,
|
||||||
|
val overral_difficulty: Double,
|
||||||
|
val approach_rate: Double,
|
||||||
|
val slider_multiplier: Double,
|
||||||
|
val slider_tick_rate: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BeatmapResponse(
|
||||||
|
val circles: List<Circle>,
|
||||||
|
val sliders: List<Slider>,
|
||||||
|
val spinners: List<Spinner>,
|
||||||
|
val difficulty: Difficulty,
|
||||||
|
val audio_lead_in: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun processBeatmap(beatmapFile: String, mods: Int): CompletableFuture<BeatmapResponse> {
|
||||||
|
val requestUri = "$apiUrl/beatmap"
|
||||||
|
|
||||||
|
val request = BeatmapRequest(
|
||||||
|
beatmap_file = beatmapFile,
|
||||||
|
mods = mods
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serialize the request object to JSON
|
||||||
|
val requestBody = serializer.encodeToString(BeatmapRequest.serializer(), request)
|
||||||
|
|
||||||
|
val httpRequest = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(requestUri))
|
||||||
|
.header("Content-Type", "application/json") // Set content type to application/json
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
|
||||||
|
.thenApply { response: HttpResponse<String> ->
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
serializer.decodeFromString(BeatmapResponse.serializer(), response.body())
|
||||||
|
} else {
|
||||||
|
throw RuntimeException("Failed to process beatmap: ${response.body()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun processReplay(replayData: String, beatmapData: String, mods: Int = 0): CompletableFuture<ReplayResponse> {
|
fun processReplay(replayData: String, beatmapData: String, mods: Int = 0): CompletableFuture<ReplayResponse> {
|
||||||
val requestUri = "$apiUrl/replay"
|
val requestUri = "$apiUrl/replay"
|
||||||
|
|
||||||
|
|||||||
@ -2,17 +2,16 @@ import base64
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from itertools import combinations
|
|
||||||
from math import isnan
|
|
||||||
from typing import List, Iterable
|
from typing import List, Iterable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy
|
import scipy
|
||||||
from brparser import Replay, BeatmapOsu, Mod
|
from brparser import Replay, BeatmapOsu, Mod
|
||||||
from circleguard import Circleguard, ReplayString, Hit
|
from circleguard import Circleguard, ReplayString, Hit
|
||||||
from circleguard.utils import filter_outliers
|
|
||||||
from flask import Flask, request, jsonify, abort
|
from flask import Flask, request, jsonify, abort
|
||||||
from slider import Beatmap
|
from itertools import combinations
|
||||||
|
from math import isnan
|
||||||
|
from slider import Beatmap, Circle, Slider, Spinner
|
||||||
|
|
||||||
from src.WriteStreamWrapper import WriteStreamWrapper
|
from src.WriteStreamWrapper import WriteStreamWrapper
|
||||||
from src.keypresses import get_kp_sliders
|
from src.keypresses import get_kp_sliders
|
||||||
@ -43,6 +42,139 @@ def my_filter_outliers(arr, bias=1.5):
|
|||||||
arr_without_outliers = [x for x in arr if lower_limit < x < upper_limit]; return arr if not arr_without_outliers else arr_without_outliers
|
arr_without_outliers = [x for x in arr if lower_limit < x < upper_limit]; return arr if not arr_without_outliers else arr_without_outliers
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BeatmapRequest:
|
||||||
|
beatmap_file: str
|
||||||
|
mods: int
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data):
|
||||||
|
try:
|
||||||
|
return BeatmapRequest(
|
||||||
|
beatmap_file=data['beatmap_file'],
|
||||||
|
mods=data['mods']
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, TypeError) as e:
|
||||||
|
raise ValueError(f"Invalid data format: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/beatmap")
|
||||||
|
def process_beatmap():
|
||||||
|
try:
|
||||||
|
request_data = request.get_json()
|
||||||
|
if not request_data:
|
||||||
|
abort(400, description="Bad Request: No JSON data provided.")
|
||||||
|
|
||||||
|
beatmap_request = BeatmapRequest.from_dict(request_data)
|
||||||
|
|
||||||
|
cg_beatmap = Beatmap.parse(beatmap_request.beatmap_file)
|
||||||
|
|
||||||
|
circles = []
|
||||||
|
sliders = []
|
||||||
|
spinners = []
|
||||||
|
|
||||||
|
def map_slider_curve_type(slider: Slider):
|
||||||
|
if str(type(slider.curve)) == "<class 'slider.curve.Linear'>":
|
||||||
|
return 'Linear'
|
||||||
|
if str(type(slider.curve)) == "<class 'slider.curve.Perfect'>":
|
||||||
|
return 'Perfect'
|
||||||
|
elif str(type(slider.curve)) == "<class 'slider.curve.MultiBezier'>":
|
||||||
|
return 'MultiBezier'
|
||||||
|
elif str(type(slider.curve)) == "<class 'slider.curve.CatMull'>":
|
||||||
|
return 'CatMull'
|
||||||
|
|
||||||
|
combo_colors = {
|
||||||
|
1: "255,81,81", # Combo1
|
||||||
|
2: "255,128,64", # Combo2
|
||||||
|
3: "128,64,0", # Combo3
|
||||||
|
4: "212,212,212" # Combo4
|
||||||
|
}
|
||||||
|
|
||||||
|
current_combo = 1 # Initialize current combo counter
|
||||||
|
combo_counter = 0 # Keep track of how many combos have been counted
|
||||||
|
|
||||||
|
hit_objects = cg_beatmap.hit_objects(
|
||||||
|
easy=bool(beatmap_request.mods & Mod.Easy.value),
|
||||||
|
hard_rock=bool(beatmap_request.mods & Mod.HardRock.value),
|
||||||
|
half_time=bool(beatmap_request.mods & Mod.HalfTime.value),
|
||||||
|
double_time=bool(beatmap_request.mods & Mod.DoubleTime.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
for obj in hit_objects:
|
||||||
|
if obj.new_combo:
|
||||||
|
combo_counter += 1 # Increment combo counter
|
||||||
|
if combo_counter > 4:
|
||||||
|
combo_counter = 1 # Reset combo counter after 4
|
||||||
|
current_combo = 1 # Reset current combo to 1 on new combo
|
||||||
|
else:
|
||||||
|
current_combo += 1 # Increment current combo for each hit object
|
||||||
|
|
||||||
|
# Assign combo color based on the combo_counter
|
||||||
|
combo_color = combo_colors[combo_counter]
|
||||||
|
|
||||||
|
if obj.type_code & Circle.type_code:
|
||||||
|
circles.append({
|
||||||
|
"x": obj.position.x,
|
||||||
|
"y": obj.position.y,
|
||||||
|
"time": obj.time.total_seconds() * 1000,
|
||||||
|
"new_combo": obj.new_combo,
|
||||||
|
"combo_color": combo_color,
|
||||||
|
"current_combo": current_combo
|
||||||
|
})
|
||||||
|
elif obj.type_code & Slider.type_code:
|
||||||
|
slider: Slider = obj
|
||||||
|
sliders.append({
|
||||||
|
"x": slider.position.x,
|
||||||
|
"y": slider.position.y,
|
||||||
|
"time": slider.time.total_seconds() * 1000,
|
||||||
|
"end_time": slider.end_time.total_seconds() * 1000,
|
||||||
|
"curve": {
|
||||||
|
'type': map_slider_curve_type(slider),
|
||||||
|
'points': [{'x': p.x, 'y': p.y} for p in slider.curve.points]
|
||||||
|
},
|
||||||
|
"length": slider.length,
|
||||||
|
"new_combo": slider.new_combo,
|
||||||
|
"combo_color": combo_color,
|
||||||
|
"current_combo": current_combo,
|
||||||
|
"repeat": slider.repeat,
|
||||||
|
})
|
||||||
|
elif obj.type_code & Spinner.type_code:
|
||||||
|
spinner: Spinner = obj
|
||||||
|
spinners.append({
|
||||||
|
"x": spinner.position.x,
|
||||||
|
"y": spinner.position.y,
|
||||||
|
"time": spinner.time.total_seconds() * 1000,
|
||||||
|
"end_time": spinner.end_time.total_seconds() * 1000,
|
||||||
|
"new_combo": spinner.new_combo,
|
||||||
|
"combo_color": combo_color,
|
||||||
|
"current_combo": current_combo
|
||||||
|
})
|
||||||
|
|
||||||
|
# Reset current_combo if new_combo is True
|
||||||
|
if obj.new_combo:
|
||||||
|
current_combo = 1
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"circles": circles,
|
||||||
|
"sliders": sliders,
|
||||||
|
"spinners": spinners,
|
||||||
|
"difficulty": {
|
||||||
|
"hp_drain_rate": cg_beatmap.hp(),
|
||||||
|
"circle_size": cg_beatmap.cs(),
|
||||||
|
"overral_difficulty": cg_beatmap.od(),
|
||||||
|
"approach_rate": cg_beatmap.ar(),
|
||||||
|
"slider_multiplier": cg_beatmap.slider_multiplier,
|
||||||
|
"slider_tick_rate": cg_beatmap.slider_tick_rate
|
||||||
|
},
|
||||||
|
"audio_lead_in": cg_beatmap.audio_lead_in.total_seconds() * 1000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ReplayRequest:
|
class ReplayRequest:
|
||||||
replay_data: str
|
replay_data: str
|
||||||
|
|||||||
@ -13,9 +13,69 @@ export interface ReplayDataSimilarScore {
|
|||||||
correlation: number;
|
correlation: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Circle {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
time: number;
|
||||||
|
new_combo: boolean;
|
||||||
|
current_combo: number;
|
||||||
|
combo_color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SliderCurvePoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SliderCurve {
|
||||||
|
type: string;
|
||||||
|
points: SliderCurvePoint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Slider {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
time: number;
|
||||||
|
end_time: number;
|
||||||
|
curve: SliderCurve;
|
||||||
|
length: number;
|
||||||
|
new_combo: boolean;
|
||||||
|
current_combo: number;
|
||||||
|
combo_color: string;
|
||||||
|
repeat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Spinner {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
time: number;
|
||||||
|
end_time: number;
|
||||||
|
new_combo: boolean;
|
||||||
|
current_combo: number;
|
||||||
|
combo_color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Difficulty {
|
||||||
|
hp_drain_rate: number;
|
||||||
|
circle_size: number;
|
||||||
|
overral_difficulty: number;
|
||||||
|
approach_rate: number;
|
||||||
|
slider_multiplier: number;
|
||||||
|
slider_tick_rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeatmapResponse {
|
||||||
|
circles: Circle[];
|
||||||
|
sliders: Slider[];
|
||||||
|
spinners: Spinner[];
|
||||||
|
difficulty: Difficulty;
|
||||||
|
audio_lead_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReplayViewerData {
|
export interface ReplayViewerData {
|
||||||
replay: string;
|
replay: string;
|
||||||
beatmap: string;
|
beatmap: BeatmapResponse;
|
||||||
|
mods: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReplayData {
|
export interface ReplayData {
|
||||||
|
|||||||
BIN
nise-frontend/src/assets/replay-viewer/sliderball.png
Normal file
BIN
nise-frontend/src/assets/replay-viewer/sliderball.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@ -1,125 +0,0 @@
|
|||||||
export enum HitObjectType {
|
|
||||||
HitCircle = 1,
|
|
||||||
Slider = 2,
|
|
||||||
Spinner = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HitObject = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
time: number;
|
|
||||||
type: number;
|
|
||||||
hitSound: number;
|
|
||||||
objectParams: string;
|
|
||||||
hitSample: string;
|
|
||||||
currentCombo: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface BeatmapDifficulty {
|
|
||||||
hpDrainRate: number;
|
|
||||||
circleSize: number;
|
|
||||||
overralDifficulty: number;
|
|
||||||
approachRate: number;
|
|
||||||
sliderMultiplier: number;
|
|
||||||
sliderTickRate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseBeatmapDifficulty(beatmap: string): BeatmapDifficulty {
|
|
||||||
const lines = beatmap.split('\n');
|
|
||||||
let recording = false;
|
|
||||||
const difficulty: Partial<BeatmapDifficulty> = {};
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim() === '[Difficulty]') {
|
|
||||||
recording = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recording) continue;
|
|
||||||
|
|
||||||
if (line.startsWith('[')) break;
|
|
||||||
|
|
||||||
const parts = line.split(':');
|
|
||||||
if (parts.length < 2) continue;
|
|
||||||
|
|
||||||
switch (parts[0]) {
|
|
||||||
case 'HPDrainRate':
|
|
||||||
difficulty.hpDrainRate = parseFloat(parts[1]);
|
|
||||||
break;
|
|
||||||
case 'CircleSize':
|
|
||||||
difficulty.circleSize = parseFloat(parts[1]);
|
|
||||||
break;
|
|
||||||
case 'OverallDifficulty':
|
|
||||||
difficulty.overralDifficulty = parseFloat(parts[1]);
|
|
||||||
break;
|
|
||||||
case 'ApproachRate':
|
|
||||||
difficulty.approachRate = parseFloat(parts[1]);
|
|
||||||
break;
|
|
||||||
case 'SliderMultiplier':
|
|
||||||
difficulty.sliderMultiplier = parseFloat(parts[1]);
|
|
||||||
break;
|
|
||||||
case 'SliderTickRate':
|
|
||||||
difficulty.sliderTickRate = parseFloat(parts[1]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty as BeatmapDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseHitObjects(beatmap: string): any[] {
|
|
||||||
const lines = beatmap.split('\n');
|
|
||||||
let recording = false;
|
|
||||||
const hitObjects = [];
|
|
||||||
let currentCombo = 1;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim() === '[HitObjects]') {
|
|
||||||
recording = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recording) continue;
|
|
||||||
|
|
||||||
if (line.startsWith('[')) break;
|
|
||||||
|
|
||||||
const parts = line.split(',');
|
|
||||||
if (parts.length < 5) continue;
|
|
||||||
|
|
||||||
const type = parseInt(parts[3], 10);
|
|
||||||
const isNewCombo = type & (1 << 2); // Bit at index 2 for new combo
|
|
||||||
if (isNewCombo) {
|
|
||||||
currentCombo = 1; // Reset combo
|
|
||||||
} else {
|
|
||||||
// If not the start of a new combo, increment the current combo
|
|
||||||
currentCombo++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hitObject = {
|
|
||||||
x: parseInt(parts[0], 10),
|
|
||||||
y: parseInt(parts[1], 10),
|
|
||||||
time: parseInt(parts[2], 10),
|
|
||||||
type: getTypeFromFlag(type),
|
|
||||||
hitSound: parseInt(parts[4], 10),
|
|
||||||
objectParams: parts[5],
|
|
||||||
hitSample: parts.length > 6 ? parts[6] : '0:0:0:0:',
|
|
||||||
currentCombo: currentCombo
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isNewCombo) {
|
|
||||||
// Reset currentCombo after assigning to hitObject if it's the start of a new combo
|
|
||||||
currentCombo = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
hitObjects.push(hitObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hitObjects;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypeFromFlag(flag: number): HitObjectType | null {
|
|
||||||
if (flag & HitObjectType.HitCircle) return HitObjectType.HitCircle;
|
|
||||||
if (flag & HitObjectType.Slider) return HitObjectType.Slider;
|
|
||||||
if (flag & HitObjectType.Spinner) return HitObjectType.Spinner;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@ -14,21 +14,30 @@ export class ReplayEventProcessed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processReplay(events: ReplayEvent[]): ReplayEventProcessed[] {
|
export function processReplay(events: ReplayEvent[], mods: string[], audioLeadIn: number): ReplayEventProcessed[] {
|
||||||
|
console.log(events);
|
||||||
|
|
||||||
if (events.length === 0) throw new Error("This replay's replay data was empty. It indicates a misbehaved replay.");
|
if (events.length === 0) throw new Error("This replay's replay data was empty. It indicates a misbehaved replay.");
|
||||||
|
|
||||||
if (events[0].timeDelta === 0 && events.length > 1) events.shift();
|
if (events[0].timeDelta === 0 && events.length > 1) events.shift();
|
||||||
|
|
||||||
const pEvents: ReplayEventProcessed[] = [];
|
const pEvents: ReplayEventProcessed[] = [];
|
||||||
|
|
||||||
let cumulativeTimeDelta = events[0].timeDelta;
|
let cumulativeTimeDelta = events[0].timeDelta + audioLeadIn;
|
||||||
let highestTimeDelta = Number.NEGATIVE_INFINITY;
|
let highestTimeDelta = Number.NEGATIVE_INFINITY;
|
||||||
let lastPositiveFrame: ReplayEvent | null = null;
|
let lastPositiveFrame: ReplayEvent | null = null;
|
||||||
|
|
||||||
let wasInNegativeSection = false;
|
let wasInNegativeSection = false;
|
||||||
const lastPositiveFrameData: [number, [number, number]][] = [];
|
const lastPositiveFrameData: [number, [number, number]][] = [];
|
||||||
|
|
||||||
|
const timeModifier = mods.includes("DT") ? 2 / 3 : mods.includes("HT") ? 4 / 3 : 1;
|
||||||
|
|
||||||
events.slice(1).forEach((currentFrame, index) => {
|
events.slice(1).forEach((currentFrame, index) => {
|
||||||
|
currentFrame.timeDelta *= timeModifier;
|
||||||
|
if(mods.includes("HR")) {
|
||||||
|
currentFrame.y = 384 - currentFrame.y;
|
||||||
|
}
|
||||||
|
|
||||||
const previousCumulativeTime = cumulativeTimeDelta;
|
const previousCumulativeTime = cumulativeTimeDelta;
|
||||||
cumulativeTimeDelta += currentFrame.timeDelta;
|
cumulativeTimeDelta += currentFrame.timeDelta;
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
import { ElementRef, Injectable } from '@angular/core';
|
import { ElementRef } from '@angular/core';
|
||||||
import {BeatmapDifficulty, HitObject, HitObjectType, parseBeatmapDifficulty, parseHitObjects} from "./decode-beatmap";
|
|
||||||
import {processReplay, ReplayEventProcessed} from "./process-replay";
|
import {processReplay, ReplayEventProcessed} from "./process-replay";
|
||||||
import {ReplayViewerData} from "../../../app/replays";
|
import {ReplayViewerData, Slider} from "../../../app/replays";
|
||||||
import {getEvents} from "./decode-replay";
|
import {getEvents} from "./decode-replay";
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class ReplayService {
|
export class ReplayService {
|
||||||
|
|
||||||
private hitObjects: HitObject[] = [];
|
private replayViewerData: ReplayViewerData;
|
||||||
private replayEvents: ReplayEventProcessed[] = [];
|
private readonly replayEvents: ReplayEventProcessed[] = [];
|
||||||
private difficulty: BeatmapDifficulty | null = null;
|
|
||||||
|
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
speedFactor: number = 1;
|
speedFactor: number = 1;
|
||||||
@ -29,12 +24,20 @@ export class ReplayService {
|
|||||||
private hitCircleOverlay = new Image();
|
private hitCircleOverlay = new Image();
|
||||||
private approachCircleImage = new Image();
|
private approachCircleImage = new Image();
|
||||||
private cursorImage = new Image();
|
private cursorImage = new Image();
|
||||||
|
private sliderBall = new Image();
|
||||||
|
private reverseArrow = new Image();
|
||||||
|
|
||||||
constructor() {
|
constructor(replayViewerData: ReplayViewerData) {
|
||||||
this.hitCircleImage.src = 'assets/replay-viewer/hitcircle.png';
|
this.hitCircleImage.src = 'assets/replay-viewer/hitcircle.png';
|
||||||
this.hitCircleOverlay.src = 'assets/replay-viewer/hitcircleoverlay.png';
|
this.hitCircleOverlay.src = 'assets/replay-viewer/hitcircleoverlay.png';
|
||||||
this.approachCircleImage.src = 'assets/replay-viewer/approachcircle.png';
|
this.approachCircleImage.src = 'assets/replay-viewer/approachcircle.png';
|
||||||
this.cursorImage.src = 'assets/replay-viewer/cursor.png';
|
this.cursorImage.src = 'assets/replay-viewer/cursor.png';
|
||||||
|
this.sliderBall.src = 'assets/replay-viewer/sliderball.png';
|
||||||
|
this.reverseArrow.src = 'assets/replay-viewer/reversearrow.png';
|
||||||
|
|
||||||
|
this.replayViewerData = replayViewerData;
|
||||||
|
this.replayEvents = processReplay(getEvents(replayViewerData.replay), replayViewerData.mods, replayViewerData.beatmap.audio_lead_in);
|
||||||
|
this.calculateTotalDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanvasElement(canvas: ElementRef<HTMLCanvasElement>) {
|
setCanvasElement(canvas: ElementRef<HTMLCanvasElement>) {
|
||||||
@ -45,15 +48,6 @@ export class ReplayService {
|
|||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadReplay(replayViewerData: ReplayViewerData): void {
|
|
||||||
this.replayEvents = processReplay(getEvents(replayViewerData.replay))
|
|
||||||
|
|
||||||
this.hitObjects = parseHitObjects(replayViewerData.beatmap)
|
|
||||||
this.calculateTotalDuration();
|
|
||||||
|
|
||||||
this.difficulty = parseBeatmapDifficulty(replayViewerData.beatmap)
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateTotalDuration() {
|
private calculateTotalDuration() {
|
||||||
if (this.replayEvents.length === 0) {
|
if (this.replayEvents.length === 0) {
|
||||||
this.totalDuration = 0;
|
this.totalDuration = 0;
|
||||||
@ -158,22 +152,21 @@ export class ReplayService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleHitCircles = this.hitObjects.filter(obj =>
|
const visibleHitCircles = this.replayViewerData!.beatmap.circles.filter(obj =>
|
||||||
obj.type === HitObjectType.HitCircle &&
|
this.currentTime >= obj.time - 200 &&
|
||||||
this.currentTime >= obj.time - 200 && // Start showing 200ms before
|
this.currentTime <= obj.time
|
||||||
this.currentTime <= obj.time // Hide after its time
|
|
||||||
);
|
);
|
||||||
|
|
||||||
visibleHitCircles.forEach(hitCircle => {
|
visibleHitCircles.forEach(hitCircle => {
|
||||||
const opacity = this.calculateOpacity(hitCircle.time);
|
const opacity = this.calculateOpacity(hitCircle.time);
|
||||||
this.drawHitCircle(hitCircle.x, hitCircle.y, opacity, hitCircle.currentCombo);
|
this.drawHitCircle(hitCircle.x, hitCircle.y, opacity, hitCircle.current_combo, hitCircle.combo_color);
|
||||||
this.drawApproachCircle(hitCircle.x, hitCircle.y, 1, hitCircle.time)
|
this.drawApproachCircle(hitCircle.x, hitCircle.y, 1, hitCircle.time)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawApproachCircle(x: number, y: number, opacity: number, hitTime: number) {
|
private drawApproachCircle(x: number, y: number, opacity: number, hitTime: number) {
|
||||||
let {fadeIn, totalDisplayTime} = this.calculatePreempt();
|
let {fadeIn, totalDisplayTime} = this.calculatePreempt();
|
||||||
let baseSize = 54.4 - 4.48 * this.difficulty!.circleSize;
|
let baseSize = 54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size;
|
||||||
|
|
||||||
// Calculate scale using the provided formula
|
// Calculate scale using the provided formula
|
||||||
let scale = Math.max(1, ((hitTime - this.currentTime) / totalDisplayTime) * 3 + 1);
|
let scale = Math.max(1, ((hitTime - this.currentTime) / totalDisplayTime) * 3 + 1);
|
||||||
@ -203,7 +196,7 @@ export class ReplayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private calculatePreempt() {
|
private calculatePreempt() {
|
||||||
const AR = this.difficulty!.approachRate;
|
const AR = this.replayViewerData.beatmap.difficulty.approach_rate;
|
||||||
let fadeIn = 0;
|
let fadeIn = 0;
|
||||||
|
|
||||||
if (AR === 5) {
|
if (AR === 5) {
|
||||||
@ -227,9 +220,9 @@ export class ReplayService {
|
|||||||
return {fadeIn, totalDisplayTime};
|
return {fadeIn, totalDisplayTime};
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawHitCircle(x: number, y: number, opacity: number, combo: number) {
|
private drawHitCircle(x: number, y: number, opacity: number, combo: number, color: string) {
|
||||||
this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect
|
this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect
|
||||||
let radius = 54.4 - 4.48 * this.difficulty!.circleSize;
|
let radius = 54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size;
|
||||||
|
|
||||||
this.ctx!.drawImage(this.hitCircleImage, x - radius, y - radius, radius * 2, radius * 2);
|
this.ctx!.drawImage(this.hitCircleImage, x - radius, y - radius, radius * 2, radius * 2);
|
||||||
this.ctx!.drawImage(this.hitCircleOverlay, x - radius, y - radius, radius * 2, radius * 2);
|
this.ctx!.drawImage(this.hitCircleOverlay, x - radius, y - radius, radius * 2, radius * 2);
|
||||||
@ -240,9 +233,9 @@ export class ReplayService {
|
|||||||
this.ctx!.fillText(combo.toString(), x - measure.width / 2, y + 10);
|
this.ctx!.fillText(combo.toString(), x - measure.width / 2, y + 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawSlider(x: number, y: number, opacity: number, combo: number) {
|
private drawSliderHitCircle(x: number, y: number, opacity: number, combo: number) {
|
||||||
this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect
|
this.ctx!.fillStyle = `rgba(255, 255, 255, ${opacity})`; // Set opacity for fade-in effect
|
||||||
let radius = 54.4 - 4.48 * this.difficulty!.circleSize;
|
let radius = 54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size;
|
||||||
|
|
||||||
this.ctx!.drawImage(this.hitCircleImage, x - radius, y - radius, radius * 2, radius * 2);
|
this.ctx!.drawImage(this.hitCircleImage, x - radius, y - radius, radius * 2, radius * 2);
|
||||||
this.ctx!.drawImage(this.hitCircleOverlay, x - radius, y - radius, radius * 2, radius * 2);
|
this.ctx!.drawImage(this.hitCircleOverlay, x - radius, y - radius, radius * 2, radius * 2);
|
||||||
@ -259,19 +252,240 @@ export class ReplayService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleSliders = this.hitObjects.filter(obj =>
|
const visibleSliders = this.replayViewerData.beatmap.sliders.filter(obj => {
|
||||||
obj.type === HitObjectType.Slider &&
|
return this.currentTime >= obj.time - 200 && // Start showing 200ms before
|
||||||
this.currentTime >= obj.time - 200 && // Start showing 200ms before
|
this.currentTime <= obj.end_time; // Hide after its time
|
||||||
this.currentTime <= obj.time // Hide after its time
|
});
|
||||||
);
|
|
||||||
|
|
||||||
visibleSliders.forEach(slider => {
|
visibleSliders.forEach(slider => {
|
||||||
const opacity = this.calculateOpacity(slider.time);
|
const opacity = this.calculateOpacity(slider.time);
|
||||||
this.drawSlider(slider.x, slider.y, opacity, slider.currentCombo);
|
this.drawSliderBody(slider);
|
||||||
|
this.drawSliderHitCircle(slider.x, slider.y, opacity, slider.current_combo);
|
||||||
this.drawApproachCircle(slider.x, slider.y, 1, slider.time)
|
this.drawApproachCircle(slider.x, slider.y, 1, slider.time)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawSliderBody(slider: Slider): void {
|
||||||
|
let playfieldScale = 1;
|
||||||
|
let mapCsr = (54.4 - 4.48 * this.replayViewerData.beatmap.difficulty.circle_size) * 2;
|
||||||
|
let SLIDERSZ = -7 + mapCsr;
|
||||||
|
let SLIDERBORDERSIZE = 5;
|
||||||
|
|
||||||
|
let duration = slider.end_time - slider.time;
|
||||||
|
|
||||||
|
let sliderBallsizeX = mapCsr * (this.sliderBall.width / this.sliderBall.width);
|
||||||
|
let sliderBallsizeY = mapCsr * (this.sliderBall.height / this.sliderBall.width);
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
var slidesDone;
|
||||||
|
if (this.currentTime - slider.time < 0){
|
||||||
|
slidesDone = 0;
|
||||||
|
} else {
|
||||||
|
slidesDone = Math.floor((this.currentTime - slider.time) / duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(slider.curve.type == "Linear") {
|
||||||
|
this.ctx!.beginPath();
|
||||||
|
var start = [slider.x, slider.y];
|
||||||
|
var end = [slider.curve.points.slice(- 1)[0].x, slider.curve.points.slice(- 1)[0].y];
|
||||||
|
var clength = Math.pow(Math.pow((end[0] - start[0]), 2) + Math.pow((end[1] - start[1]), 2), 0.5);
|
||||||
|
if (clength < length * playfieldScale){
|
||||||
|
this.ctx!.lineTo(((end[0] - start[0]) / clength) * length * playfieldScale + start[0], ((end[1] - start[1]) / clength) * length * playfieldScale + start[1]);
|
||||||
|
clength = length * playfieldScale;
|
||||||
|
}
|
||||||
|
this.ctx!.moveTo(start[0], start[1]);
|
||||||
|
this.ctx!.lineTo(end[0], end[1]);
|
||||||
|
this.ctx!.lineCap = 'round';
|
||||||
|
this.ctx!.lineWidth = SLIDERSZ - SLIDERBORDERSIZE;
|
||||||
|
this.ctx!.strokeStyle = 'rgb(3,3,12,0.5)';//}
|
||||||
|
this.ctx!.stroke();
|
||||||
|
this.ctx!.closePath();
|
||||||
|
|
||||||
|
|
||||||
|
this.ctx!.save();
|
||||||
|
this.ctx!.translate(start[0], start[1]);
|
||||||
|
if (end[0] > start[0]) { //I don't have enough braincells to figure out why this is a thing
|
||||||
|
this.ctx!.rotate(Math.atan((end[1] - start[1]) / (end[0] - start[0])) - Math.PI / 2);
|
||||||
|
} else {
|
||||||
|
this.ctx!.rotate(Math.atan((end[1] - start[1]) / (end[0] - start[0])) + Math.PI / 2);
|
||||||
|
}
|
||||||
|
this.ctx!.beginPath();
|
||||||
|
this.ctx!.moveTo(- SLIDERSZ / 2, 0);
|
||||||
|
this.ctx!.lineTo(- SLIDERSZ / 2, clength);
|
||||||
|
this.ctx!.arc (0, clength, SLIDERSZ / 2, Math.PI, 0, true);
|
||||||
|
//this.ctx!.moveTo( SLIDERSZ / 2, length*playfieldScale);
|
||||||
|
this.ctx!.lineTo(SLIDERSZ / 2, 0);
|
||||||
|
this.ctx!.arc (0, 0, SLIDERSZ / 2, 0, Math.PI, true);
|
||||||
|
//this.ctx!.moveTo(-SLIDERSZ / 2, 0);
|
||||||
|
this.ctx!.lineCap = 'butt';
|
||||||
|
this.ctx!.lineWidth = SLIDERBORDERSIZE;
|
||||||
|
this.ctx!.strokeStyle = 'rgb(190,190,190)';
|
||||||
|
//this.ctx!.strokeRect(-SLIDERSZ / 2,0,SLIDERSZ,length*playfieldScale);
|
||||||
|
this.ctx!.stroke();
|
||||||
|
this.ctx!.closePath();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//console.log(slidesDone);
|
||||||
|
if (slidesDone % 2 === 0){
|
||||||
|
this.ctx!.translate(0, clength);
|
||||||
|
this.ctx!.rotate(- Math.PI / 2);
|
||||||
|
} else {
|
||||||
|
this.ctx!.rotate(Math.PI / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Draw sliderend (if any)
|
||||||
|
//1 slide is duration long. current-timing is elapsed time. (current-timing)/duration is amount of slides gone thru.
|
||||||
|
//therefore, amount of slides left is slides-(current-timing)/duration. But current-timing must be gt 0
|
||||||
|
if (this.currentTime < slider.time){
|
||||||
|
if (slider.repeat > 1) {
|
||||||
|
this.ctx!.drawImage(this.reverseArrow, - mapCsr / 2, - mapCsr / 2, mapCsr, mapCsr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (slider.repeat - slidesDone > 1) {
|
||||||
|
this.ctx!.drawImage(this.reverseArrow, - mapCsr / 2, - mapCsr / 2, mapCsr, mapCsr);
|
||||||
|
}
|
||||||
|
if ((slider.repeat - slidesDone > 2)) { //TODO, I can't test this right now, but im pretty sure this will render it when it isnt supposed to be
|
||||||
|
this.ctx!.drawImage(this.reverseArrow, - mapCsr / 2, - mapCsr / 2, mapCsr, mapCsr); //REMEMEBER TO EDIT THIS FOR ALL SLIDERS !!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx!.restore();
|
||||||
|
|
||||||
|
//Draw sliderball along the path
|
||||||
|
if (this.currentTime >= slider.time && this.currentTime < slider.time + duration * 1) {
|
||||||
|
var sliderBall = (this.currentTime - (slider.time + duration * slidesDone)) * (clength / duration);
|
||||||
|
|
||||||
|
var sliderBallX; var sliderBallY;
|
||||||
|
if (slidesDone % 2 === 0){
|
||||||
|
sliderBallX = ((end[0] - start[0]) / clength) * sliderBall + start[0];
|
||||||
|
sliderBallY = ((end[1] - start[1]) / clength) * sliderBall + start[1];
|
||||||
|
} else {
|
||||||
|
sliderBallX = ((start[0] - end[0]) / clength) * sliderBall + end[0];
|
||||||
|
sliderBallY = ((start[1] - end[1]) / clength) * sliderBall + end[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx!.drawImage(this.sliderBall, sliderBallX - sliderBallsizeX / 2, sliderBallY - sliderBallsizeX / 2, sliderBallsizeX, sliderBallsizeX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slider.curve.type == "Perfect"){
|
||||||
|
var start = [slider.x, slider.y];
|
||||||
|
var mid = [slider.curve.points[1].x, slider.curve.points[1].y];
|
||||||
|
var end = [slider.curve.points[2].x, slider.curve.points[2].y];
|
||||||
|
|
||||||
|
var arcMidpoint = [];
|
||||||
|
start[0] -= mid[0]; //Translate points so mid is at origin
|
||||||
|
start[1] -= mid[1];
|
||||||
|
end[0] -= mid[0];
|
||||||
|
end[1] -= mid[1];
|
||||||
|
var D = 2*(start[0]*end[1]-end[0]*start[1]);
|
||||||
|
var z1 = start[0]*start[0] + start[1]*start[1];
|
||||||
|
var z2 = end[0]*end[0]+end[1]*end[1];
|
||||||
|
arcMidpoint[0] = (z1 * end[1] - z2 * start[1]) / D + mid[0];
|
||||||
|
arcMidpoint[1] = (start[0] * z2 - end[0] * z1) / D + mid[1];
|
||||||
|
|
||||||
|
start[0] += mid[0];
|
||||||
|
start[1] += mid[1];
|
||||||
|
end[0] += mid[0];
|
||||||
|
end[1] += mid[1];
|
||||||
|
|
||||||
|
|
||||||
|
var arcRadius = Math.pow(Math.pow(mid[0]-arcMidpoint[0],2)+Math.pow(mid[1]-arcMidpoint[1],2),0.5);
|
||||||
|
//var arcStartAngle = -arctan((arcMidpoint[1]-start[1])/(arcMidpoint[0]-start[0]));
|
||||||
|
//var arcEndAngle = -arctan((end[1]-arcMidpoint[1])/(end[0]-arcMidpoint[0]));
|
||||||
|
var arcStartAngle = Math.atan2(start[1] - arcMidpoint[1], start[0] - arcMidpoint[0]);
|
||||||
|
var arcEndAngle = Math.atan2(end[1] - arcMidpoint[1], end[0] - arcMidpoint[0]);
|
||||||
|
var isClockwise = ((end[0] - start[0]) * (mid[1] - start[1]) - (end[1] - start[1]) * (mid[0] - start[0])) >= 0
|
||||||
|
var clength = Math.abs(arcEndAngle - arcStartAngle) * arcRadius;
|
||||||
|
if (length > clength) {
|
||||||
|
arcEndAngle += (length * playfieldScale - clength) / arcRadius;
|
||||||
|
end = [arcMidpoint[0] + arcRadius * Math.cos(arcEndAngle),arcMidpoint[1] + arcRadius * Math.sin(arcEndAngle)];
|
||||||
|
}
|
||||||
|
//console.log(arcMidpoint[0], arcMidpoint[1], arcRadius, arcStartAngle, arcEndAngle)
|
||||||
|
|
||||||
|
this.ctx!.beginPath();
|
||||||
|
this.ctx!.arc(arcMidpoint[0], arcMidpoint[1], arcRadius, arcStartAngle, arcEndAngle, isClockwise);
|
||||||
|
this.ctx!.lineCap = 'round';
|
||||||
|
//this.ctx!.lineWidth = 10;
|
||||||
|
this.ctx!.lineWidth = SLIDERSZ - SLIDERBORDERSIZE;
|
||||||
|
// this.ctx!.strokeStyle = 'rgb(200,100,100)';
|
||||||
|
this.ctx!.strokeStyle = 'rgb(3,3,12,0.5)';//}
|
||||||
|
this.ctx!.stroke();
|
||||||
|
this.ctx!.closePath();
|
||||||
|
|
||||||
|
//Now draw the slider border //This might have just been the most satisfying thing I've made it a while
|
||||||
|
this.ctx!.beginPath(); //its so nice when the math just works out how you calculated it to
|
||||||
|
|
||||||
|
let effectiveRadius = arcRadius - SLIDERSZ/2;
|
||||||
|
if (effectiveRadius < 0) {
|
||||||
|
// Option 1: Adjust SLIDERSZ to ensure effectiveRadius is positive
|
||||||
|
SLIDERSZ = 2 * arcRadius; // This is just an example adjustment
|
||||||
|
effectiveRadius = arcRadius - SLIDERSZ/2; // Recalculate effectiveRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx!.arc(arcMidpoint[0], arcMidpoint[1], arcRadius+SLIDERSZ/2, arcStartAngle, arcEndAngle, isClockwise);
|
||||||
|
this.ctx!.arc(end[0], end[1], SLIDERSZ/2, arcEndAngle, arcEndAngle+Math.PI,isClockwise);
|
||||||
|
this.ctx!.arc(arcMidpoint[0], arcMidpoint[1], effectiveRadius, arcEndAngle, arcStartAngle, !isClockwise);
|
||||||
|
this.ctx!.arc(start[0], start[1], SLIDERSZ/2, arcStartAngle+Math.PI, arcStartAngle,isClockwise);
|
||||||
|
this.ctx!.lineCap = 'butt';
|
||||||
|
this.ctx!.lineWidth = SLIDERBORDERSIZE;
|
||||||
|
this.ctx!.strokeStyle = 'rgb(190,190,190)';
|
||||||
|
// this.ctx!.strokeStyle = 'rgb(200,100,100)';
|
||||||
|
this.ctx!.stroke();
|
||||||
|
this.ctx!.closePath();
|
||||||
|
|
||||||
|
//TODO PRIORITY 1 !!!! !!! !!! !!! !!! fix the sliderend, it looks slanted I swear
|
||||||
|
this.ctx!.save();
|
||||||
|
// if (slidesDone % 2 === 0){
|
||||||
|
// this.ctx!.translate(end[0],end[1]);
|
||||||
|
// //this.ctx!.rotate(arcEndAngle + pi / 2 + (isClockwise ? 0 : pi)); //This was my first theory on how to rotate the slider
|
||||||
|
// this.ctx!.rotate(arcEndAngle + Math.PI / 2 + (isClockwise ? 0.1 : Math.PI-0.1));
|
||||||
|
// } else {
|
||||||
|
// this.ctx!.translate(start[0],start[1]); //TODO cannot test this
|
||||||
|
// this.ctx!.rotate(arcStartAngle - Math.PI / 2 + (isClockwise ? Math.PI-0.1 : 0.1));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (current < timing){
|
||||||
|
// if (slides > 1) {
|
||||||
|
// this.ctx!.drawImage(assets['reverseArrow'], - mapCSr / 2, - mapCSr / 2, mapCSr, mapCSr);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// if (slides - slidesDone > 1) {
|
||||||
|
// this.ctx!.drawImage(assets['reverseArrow'], - mapCSr / 2, - mapCSr / 2, mapCSr, mapCSr);
|
||||||
|
// }
|
||||||
|
// //if ((slides - slidesDone > 2)) { //TODO, I can't test this right now, but im pretty sure this will render it when it isnt supposed to be
|
||||||
|
// // this.ctx!.drawImage(assets['reverseArrow'], end[0] - mapCSr / 2, end[1] - mapCSr / 2, mapCSr, mapCSr);
|
||||||
|
// //}//TODO THIS WONT WORK AAAAA
|
||||||
|
// }
|
||||||
|
this.ctx!.restore();
|
||||||
|
|
||||||
|
// //Draw sliderball along the path
|
||||||
|
let timing = slider.time
|
||||||
|
|
||||||
|
if (this.currentTime >= timing && this.currentTime < timing + duration * 1) {
|
||||||
|
var angleDt = Math.abs(arcStartAngle-arcEndAngle);
|
||||||
|
//This took way too long and I don't even know why it works or if it there is possible bugged behavior
|
||||||
|
//If anyone can explain what is happening here or the best practice I would be super thankful
|
||||||
|
if ((isClockwise && (arcStartAngle<arcEndAngle)) || (!isClockwise && (arcStartAngle>arcEndAngle))) {angleDt = 2*Math.PI - angleDt;}
|
||||||
|
var sliderBall = (isClockwise?-1:1)*(this.currentTime - (timing + duration * slidesDone)) * (angleDt / duration);
|
||||||
|
|
||||||
|
|
||||||
|
var sliderBallX; var sliderBallY;
|
||||||
|
if (slidesDone % 2 === 0){
|
||||||
|
sliderBallX = arcMidpoint[0] + arcRadius * Math.cos(arcStartAngle + sliderBall);
|
||||||
|
sliderBallY = arcMidpoint[1] + arcRadius * Math.sin(arcStartAngle + sliderBall);
|
||||||
|
} else {
|
||||||
|
sliderBallX = arcMidpoint[0] + arcRadius * Math.cos(arcEndAngle - sliderBall);
|
||||||
|
sliderBallY = arcMidpoint[1] + arcRadius * Math.sin(arcEndAngle - sliderBall);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx!.drawImage(this.sliderBall, sliderBallX - sliderBallsizeX / 2, sliderBallY - sliderBallsizeX / 2, sliderBallsizeX, sliderBallsizeX);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getTotalDuration() {
|
getTotalDuration() {
|
||||||
return this.totalDuration;
|
return this.totalDuration;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
<canvas #replayCanvas width="600" height="400"></canvas>
|
<canvas #replayCanvas width="600" height="400"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="replayService">
|
||||||
<div class="text-center mb-2">
|
<div class="text-center mb-2">
|
||||||
<button style="font-size: 20px" (click)="togglePlayPause()">{{ replayService.getIsPlaying() ? 'Pause' : 'Play' }}</button>
|
<button style="font-size: 20px" (click)="togglePlayPause()">{{ replayService.getIsPlaying() ? 'Pause' : 'Play' }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="some-page-wrapper text-center">
|
<div class="some-page-wrapper text-center">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
@ -35,6 +35,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
|
import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
|
||||||
import {getEvents} from "./decode-replay";
|
import {DecimalPipe, JsonPipe, NgForOf, NgIf} from "@angular/common";
|
||||||
import {DecimalPipe, JsonPipe, NgForOf} from "@angular/common";
|
|
||||||
import {ReplayService} from "./replay-service";
|
import {ReplayService} from "./replay-service";
|
||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import {parseHitObjects} from "./decode-beatmap";
|
|
||||||
import {processReplay} from "./process-replay";
|
|
||||||
import {ReplayViewerData} from "../../../app/replays";
|
import {ReplayViewerData} from "../../../app/replays";
|
||||||
|
|
||||||
export enum KeyPress {
|
export enum KeyPress {
|
||||||
@ -45,18 +42,21 @@ export interface ReplayEvent {
|
|||||||
JsonPipe,
|
JsonPipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
DecimalPipe,
|
DecimalPipe,
|
||||||
NgForOf
|
NgForOf,
|
||||||
|
NgIf
|
||||||
],
|
],
|
||||||
templateUrl: './replay-viewer.component.html',
|
templateUrl: './replay-viewer.component.html',
|
||||||
styleUrl: './replay-viewer.component.css'
|
styleUrl: './replay-viewer.component.css'
|
||||||
})
|
})
|
||||||
export class ReplayViewerComponent implements OnInit, AfterViewInit {
|
export class ReplayViewerComponent implements AfterViewInit {
|
||||||
|
|
||||||
@ViewChild('replayCanvas') replayCanvas!: ElementRef<HTMLCanvasElement>;
|
@ViewChild('replayCanvas') replayCanvas!: ElementRef<HTMLCanvasElement>;
|
||||||
private ctx!: CanvasRenderingContext2D;
|
private ctx!: CanvasRenderingContext2D;
|
||||||
|
|
||||||
@Input() replayViewerData!: ReplayViewerData;
|
@Input() replayViewerData!: ReplayViewerData;
|
||||||
|
|
||||||
|
public replayService!: ReplayService | null;
|
||||||
|
|
||||||
// TODO: Calculate AudioLeadIn
|
// TODO: Calculate AudioLeadIn
|
||||||
// TODO: Hard-Rock, DT, Easy
|
// TODO: Hard-Rock, DT, Easy
|
||||||
|
|
||||||
@ -72,13 +72,12 @@ export class ReplayViewerComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
// TODO: Compare two replays for similarity (different cursor color)
|
// TODO: Compare two replays for similarity (different cursor color)
|
||||||
|
|
||||||
constructor(public replayService: ReplayService) { }
|
constructor() {
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.replayService.loadReplay(this.replayViewerData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
|
this.replayService = new ReplayService(this.replayViewerData);
|
||||||
this.ctx = this.replayCanvas.nativeElement.getContext('2d')!;
|
this.ctx = this.replayCanvas.nativeElement.getContext('2d')!;
|
||||||
|
|
||||||
this.replayService.setCanvasElement(this.replayCanvas);
|
this.replayService.setCanvasElement(this.replayCanvas);
|
||||||
@ -88,6 +87,8 @@ export class ReplayViewerComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
togglePlayPause() {
|
togglePlayPause() {
|
||||||
|
if(!this.replayService) return;
|
||||||
|
|
||||||
if (this.replayService.getIsPlaying()) {
|
if (this.replayService.getIsPlaying()) {
|
||||||
this.replayService.pause();
|
this.replayService.pause();
|
||||||
} else {
|
} else {
|
||||||
@ -96,6 +97,8 @@ export class ReplayViewerComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
seek(time: number) {
|
seek(time: number) {
|
||||||
|
if(!this.replayService) return;
|
||||||
|
|
||||||
this.replayService.seek(time);
|
this.replayService.seek(time);
|
||||||
if (!this.replayService.getIsPlaying()) {
|
if (!this.replayService.getIsPlaying()) {
|
||||||
// Redraw the canvas for the new current time without resuming playback
|
// Redraw the canvas for the new current time without resuming playback
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user