Skip to content

Commit

Permalink
Merge pull request #105 from mjvotaw/nps-graph
Browse files Browse the repository at this point in the history
Add NPS Graph Widget
  • Loading branch information
tillvit authored Jul 13, 2024
2 parents 7453415 + d052197 commit e4efe41
Show file tree
Hide file tree
Showing 8 changed files with 587 additions and 189 deletions.
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

0 comments on commit e4efe41

Please sign in to comment.