Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NPS Graph Widget #105

Merged
merged 13 commits into from
Jul 13, 2024
Merged
29 changes: 29 additions & 0 deletions app/src/chart/sm/Chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export class Chart {
private _notedataStats!: Record<string, number>
private _npsGraph!: number[]

private _lastBeat = 0
private _lastSecond = 0

constructor(sm: Simfile, data?: string | { [key: string]: string }) {
this.timingData = sm.timingData.createChartTimingData(this)
this.timingData.reloadCache()
Expand Down Expand Up @@ -134,6 +137,14 @@ export class Chart {
return max
}

getLastBeat() {
return this._lastBeat
}

getLastSecond() {
return this._lastSecond
}

getSecondsFromBeat(
beat: number,
option?: "noclamp" | "before" | "after" | ""
Expand All @@ -157,6 +168,23 @@ export class Chart {
return this.timingData.isBeatFaked(beat)
}

private recalculateLastNote() {
let lastBeat = 0
let lastSecond = 0
this.notedata.forEach(note => {
const endBeat = note.beat + (isHoldNote(note) ? note.hold : 0)
const endSecond = this.timingData.getSecondsFromBeat(endBeat)
if (endBeat > lastBeat) {
lastBeat = endBeat
}
if (endSecond > lastSecond) {
lastSecond = endSecond
}
})
this._lastBeat = lastBeat
this._lastSecond = lastSecond
}

private getNoteIndex(note: PartialNotedataEntry): number {
if (this.notedata.includes(note as NotedataEntry)) {
return this.notedata.indexOf(note as NotedataEntry)
Expand Down Expand Up @@ -268,6 +296,7 @@ export class Chart {
this.notedata,
this.timingData
)
this.recalculateLastNote()
}

getMusicPath(): string {
Expand Down
49 changes: 49 additions & 0 deletions app/src/data/UserOptionsWindowData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,55 @@ export const USER_OPTIONS_WINDOW_DATA: UserOption[] = [
},
],
},
{
type: "subgroup",
label: "Note Layout",
children: [
{
type: "item",
label: "Show Note Layout",
id: "chart.noteLayout.enabled",
input: {
type: "checkbox",
},
},
],
},
{
type: "subgroup",
label: "NPS Graph",
children: [
{
type: "item",
label: "Show NPS Graph",
id: "chart.npsGraph.enabled",
input: {
type: "checkbox",
},
},
{
type: "subgroup",
children: [
{
type: "item",
label: "Start Color",
id: "chart.npsGraph.color1",
input: {
type: "color",
},
},
{
type: "item",
label: "End Color",
id: "chart.npsGraph.color2",
input: {
type: "color",
},
},
],
},
],
},
],
},
{
Expand Down
225 changes: 225 additions & 0 deletions app/src/gui/widget/BaseTimelineWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { Container, FederatedPointerEvent, Sprite, Texture } from "pixi.js"
import { EditMode } from "../../chart/ChartManager"
import { Chart } from "../../chart/sm/Chart"
import { BetterRoundedRect } from "../../util/BetterRoundedRect"
import { EventHandler } from "../../util/EventHandler"
import { Flags } from "../../util/Flags"
import { clamp, lerp, maxArr, minArr, unlerp } from "../../util/Math"
import { Options } from "../../util/Options"
import { getNoteEnd } from "../../util/Util"
import { Widget } from "./Widget"
import { WidgetManager } from "./WidgetManager"

export class BaseTimelineWidget extends Widget {
backing: BetterRoundedRect = new BetterRoundedRect()
overlay: Sprite = new Sprite(Texture.WHITE)
selectionOverlay: Sprite = new Sprite(Texture.WHITE)
container: Container = new Container()

protected lastHeight = 0
protected lastCMod
protected mouseDown = false
protected queued = false

protected verticalMargin = 40
protected backingVerticalPadding = 10
protected backingWidth = 32
xOffset = 20

constructor(
manager: WidgetManager,
xOffset: number = 20,
backingWidth: number = 32
) {
super(manager)
this.backingWidth = backingWidth
this.xOffset = xOffset

this.addChild(this.backing)
this.addChild(this.container)
this.visible = false

this.backing.tint = 0
this.backing.alpha = 0.3

this.overlay.anchor.x = 0.5
this.overlay.anchor.y = 0
this.overlay.alpha = 0.3

this.lastCMod = Options.chart.CMod
this.addChild(this.overlay)

this.x = this.manager.app.renderer.screen.width / 2 - this.xOffset

EventHandler.on("chartLoaded", () => {
this.queued = false
this.populate()
})
EventHandler.on("chartModifiedAfter", () => {
if (!this.queued) this.populate()
this.queued = true
})
const interval = setInterval(() => {
if (this.queued) {
this.queued = false
this.populate()
}
}, 3000)

this.on("destroyed", () => clearInterval(interval))

this.eventMode = "static"
this.on("mousedown", event => {
this.mouseDown = true
this.handleMouse(event)
})
this.on("mousemove", event => {
if (this.mouseDown) this.handleMouse(event)
})

this.on("mouseup", () => {
this.mouseDown = false
})

this.on("mouseleave", () => {
this.mouseDown = false
})
}

protected handleMouse(event: FederatedPointerEvent) {
if (this.manager.chartManager.getMode() == EditMode.Play) return
if (!this.getChart()) return
let t =
(this.container.toLocal(event.global).y + this.container.height / 2) /
this.container.height
t = clamp(t, 0, 1)
const lastNote = this.getChart().getNotedata().at(-1)
if (!lastNote) return
if (Options.chart.CMod) {
this.manager.chartManager.setTime(
lerp(
-this.getChart().timingData.getOffset(),
this.getChart().getLastSecond(),
t
)
)
} else {
this.manager.chartManager.setBeat(this.getChart().getLastBeat() * t)
}
}

update() {
this.scale.y = Options.chart.reverse ? -1 : 1
const width = this.manager.app.renderer.screen.height - this.verticalMargin
this.backing.height = width + this.backingVerticalPadding
this.backing.position.y = -this.backing.height / 2
this.backing.position.x = -this.backing.width / 2
this.x = this.manager.app.renderer.screen.width / 2 - this.xOffset
const chart = this.getChart()
const chartView = this.manager.chartManager.chartView!
if (!chart || !chartView || !Flags.layout) {
this.visible = false
return
}
this.visible = true
const lastNote = chart.getNotedata().at(-1)
if (!lastNote) {
this.overlay.height = 0
return
}

const overlayStart = Options.chart.CMod
? chartView.getSecondFromYPos(
-this.manager.app.renderer.screen.height / 2
)
: chartView.getBeatFromYPos(
-this.manager.app.renderer.screen.height / 2,
true
)
const overlayEnd = Options.chart.CMod
? chartView.getSecondFromYPos(this.manager.app.renderer.screen.height / 2)
: chartView.getBeatFromYPos(
this.manager.app.renderer.screen.height / 2,
true
)
const overlayRange = this.getYFromRange(chart, overlayStart, overlayEnd)
this.overlay.y = overlayRange.startY
this.overlay.height = overlayRange.endY - overlayRange.startY
this.overlay.height = Math.max(2, this.overlay.height)

const selection = this.manager.chartManager.selection.notes
if (selection.length < 1) {
this.selectionOverlay.visible = false
} else {
this.selectionOverlay.visible = true
let selectionStart, selectionEnd
if (Options.chart.CMod) {
selectionStart = minArr(selection.map(note => note.second))
selectionEnd = maxArr(
selection.map(note => chart.getSecondsFromBeat(getNoteEnd(note)))
)
} else {
selectionStart = minArr(selection.map(note => note.beat))
selectionEnd = maxArr(selection.map(note => getNoteEnd(note)))
}
const selectionRange = this.getYFromRange(
chart,
selectionStart,
selectionEnd
)
this.selectionOverlay.y = selectionRange.startY
this.selectionOverlay.height = selectionRange.endY - selectionRange.startY
this.selectionOverlay.height = Math.max(2, this.selectionOverlay.height)
}

if (
this.manager.app.renderer.screen.height != this.lastHeight ||
this.lastCMod != Options.chart.CMod
) {
this.lastCMod = Options.chart.CMod
this.lastHeight = this.manager.app.renderer.screen.height
this.updateDimensions()
this.populate()
}
}

private getYFromRange(chart: Chart, start: number, end: number) {
const lastBeat = chart.getLastBeat()
const lastSecond = chart.getLastSecond()
let t_startY = unlerp(0, lastBeat, start)
let t_endY = unlerp(0, lastBeat, end)
if (Options.chart.CMod) {
t_startY = unlerp(-chart.timingData.getOffset(), lastSecond, start)
t_endY = unlerp(-chart.timingData.getOffset(), lastSecond, end)
}
t_startY = clamp(t_startY, 0, 1)
t_endY = clamp(t_endY, 0, 1)
if (t_startY > t_endY) [t_startY, t_endY] = [t_endY, t_startY]
const startY = (t_startY - 0.5) * this.container.height
const endY = (t_endY - 0.5) * this.container.height
return {
startY,
endY,
}
}

updateDimensions() {
const chart = this.getChart()
if (!chart) {
return
}

const height = this.manager.app.renderer.screen.height - this.verticalMargin
this.backing.height = height + this.backingVerticalPadding
this.backing.width = this.backingWidth
this.overlay.width = this.backingWidth
this.selectionOverlay.width = this.backingWidth
this.pivot.x = this.backing.width / 2
}

populate() {}

protected getChart(): Chart {
return this.manager.chartManager.loadedChart!
}
}
Loading