Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,16 @@ The Z indexes of judgment lines whose `zIndex` is not present (see [Chart enhanc

Aside from adding support for RPE features, we've also designed some original properties for judgment lines & notes.

| Property | Value(s) | Example | Description |
| -------------------- | --------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `scaleOnNotes` | `0`: none; `1`: scale; `2`: clip | `"scaleOnNotes": 2` | Belongs to a judgment line. Decides how `scaleX` events affect notes. Defaults to `0`. |
| `appearanceOnAttach` | `0`: hidden; `1`: white colored; `2`: FC/AP colored | `"appearanceOnAttach": 2` | Belongs to a judgment line. Decides how the line will be displayed when a UI component or any video is attached to it. Color events will override the color defined by these options. Defaults to `0`. |
| `zIndex` | an integer or a float | `"zIndex": 3.5` | Belongs to a judgment line or note. Sets the Z index for the object. For a judgment line, this property, if set, overrides the `zOrder` property, allowing for more control over on which layer the line should be displayed. For default values, see [Z indexes](#z-indexes). |
| `zIndexHitEffects` | an integer or a float | `"zIndexHitEffects": 6.5` | Belongs to a note. Sets the Z index for the hit effects of the note. Defaults to `7`. |
| `tint` | [R, G, B], as seen in `colorEvents`; `null` | `"tint": [255, 0, 0]` | Belongs to a note. Sets the tint for the note. Defaults to `null`. |
| `tintHitEffects` | [R, G, B], as seen in `colorEvents`; `null` | `"tintHitEffects": [255, 0, 0]` | Belongs to a note. Sets the tint for the hit effects of the note. Defaults to `null`. |
| `judgeSize` | a positive number or 0 | `"judgeSize": 1.5` | Belongs to a note. Determines the width of the judgment area of the note. Defaults to `size`. |
| Property | Value(s) | Example | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `scaleOnNotes` | `0`: none; `1`: scale; `2`: clip | `"scaleOnNotes": 2` | Belongs to a judgment line. Decides how `scaleX` events affect notes. Defaults to `0`. |
| `appearanceOnAttach` | `0`: hidden; `1`: white colored; `2`: FC/AP colored | `"appearanceOnAttach": 2` | Belongs to a judgment line. Decides how the line will be displayed when a UI component or any video is attached to it. Color events will override the color defined by these options. Defaults to `0`. |
| `integrateSpeedEasings` | `false`: treat non-linear speed easings directly as height over time; `true`: integrate speed easings to obtain height functions | `"integrateSpeedEasings": true` | Belongs to a judgment line. Decides how the easing functions of all speed events of the line will be interpreted. Defaults to `false` for pre-1.7 charts and `true` for charts on 1.7 and later versions. |
| `zIndex` | an integer or a float | `"zIndex": 3.5` | Belongs to a judgment line or note. Sets the Z index for the object. For a judgment line, this property, if set, overrides the `zOrder` property, allowing for more control over on which layer the line should be displayed. For default values, see [Z indexes](#z-indexes). |
| `zIndexHitEffects` | an integer or a float | `"zIndexHitEffects": 6.5` | Belongs to a note. Sets the Z index for the hit effects of the note. Defaults to `7`. |
| `tint` | [R, G, B], as seen in `colorEvents`; `null` | `"tint": [255, 0, 0]` | Belongs to a note. Sets the tint for the note. Defaults to `null`. |
| `tintHitEffects` | [R, G, B], as seen in `colorEvents`; `null` | `"tintHitEffects": [255, 0, 0]` | Belongs to a note. Sets the tint for the hit effects of the note. Defaults to `null`. |
| `judgeSize` | a positive number or 0 | `"judgeSize": 1.5` | Belongs to a note. Determines the width of the judgment area of the note. Defaults to `size`. |

### Video enhancements

Expand Down
14 changes: 11 additions & 3 deletions src/lib/player/objects/Line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class Line {
private _rotationModifier: 1 | -1 = 1;
private _rotationOffset: 0 | 180 = 0;
private _rotateWithParent: boolean = false;
private _integrateSpeedEasings: boolean = false;

private _curX = [];
private _curY = [];
Expand Down Expand Up @@ -109,6 +110,8 @@ export class Line {

this._hasAttach = !!this._data.attachUI;
this._rotateWithParent = this._data.rotateWithFather ?? false;
this._integrateSpeedEasings =
this._data.integrateSpeedEasings ?? this._scene.chart.META.RPEVersion >= 170;
this._line.setScale(
this._scene.p(1) * (this._scaleX ?? 1),
(this._hasCustomTexture
Expand Down Expand Up @@ -430,18 +433,23 @@ export class Line {
}
while (cur[layerIndex] < events.length - 1 && beat > events[cur[layerIndex] + 1].startBeat) {
this._lastHeight +=
getIntegral(events[cur[layerIndex]], this._scene.bpmList) +
getIntegral(events[cur[layerIndex]], this._scene.bpmList, this._integrateSpeedEasings) +
events[cur[layerIndex]].end *
(getTimeSec(this._scene.bpmList, events[cur[layerIndex] + 1].startBeat) -
getTimeSec(this._scene.bpmList, events[cur[layerIndex]].endBeat));
cur[layerIndex]++;
}
let height = this._lastHeight;
if (beat <= events[cur[layerIndex]].endBeat) {
height += getIntegral(events[cur[layerIndex]], this._scene.bpmList, beat);
height += getIntegral(
events[cur[layerIndex]],
this._scene.bpmList,
this._integrateSpeedEasings,
beat,
);
} else {
height +=
getIntegral(events[cur[layerIndex]], this._scene.bpmList) +
getIntegral(events[cur[layerIndex]], this._scene.bpmList, this._integrateSpeedEasings) +
events[cur[layerIndex]].end *
(getTimeSec(this._scene.bpmList, beat) -
getTimeSec(this._scene.bpmList, events[cur[layerIndex]].endBeat));
Expand Down
212 changes: 208 additions & 4 deletions src/lib/player/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,172 @@ const EASINGS: ((x: number) => number)[] = [
(x) => (x < 0.5 ? (1 - EASINGS[25](1 - 2 * x)) / 2 : (1 + EASINGS[25](2 * x - 1)) / 2),
];

const EASING_INTEGRALS: ((x: number) => number)[] = [
(x) => (x * x) / 2,
(x) => (2 / Math.PI) * (1 - Math.cos((x * Math.PI) / 2)),
(x) => x - (2 / Math.PI) * Math.sin((x * Math.PI) / 2),
(x) => x * x - (x * x * x) / 3,
(x) => (x * x * x) / 3,
(x) => x / 2 - Math.sin(Math.PI * x) / (2 * Math.PI),
(x) => (x < 0.5 ? (2 / 3) * Math.pow(x, 3) : 2 * x * x - (2 / 3) * Math.pow(x, 3) - x + 1 / 6),
(x) => (3 / 2) * x * x - x * x * x + Math.pow(x, 4) / 4,
(x) => Math.pow(x, 4) / 4,
(x) => 2 * x * x - 2 * Math.pow(x, 3) + Math.pow(x, 4) - Math.pow(x, 5) / 5,
(x) => Math.pow(x, 5) / 5,
(x) =>
x < 0.5 ? Math.pow(x, 4) : -3 * x + 6 * x * x - 4 * Math.pow(x, 3) + Math.pow(x, 4) + 0.5,
(x) =>
x < 0.5
? (8 / 5) * Math.pow(x, 5)
: -7 * x +
16 * x * x -
16 * Math.pow(x, 3) +
8 * Math.pow(x, 4) -
(8 / 5) * Math.pow(x, 5) +
11 / 10,
(x) =>
(5 / 2) * x * x -
(10 / 3) * Math.pow(x, 3) +
(5 / 2) * Math.pow(x, 4) -
Math.pow(x, 5) +
Math.pow(x, 6) / 6,
(x) => Math.pow(x, 6) / 6,
(x) => x - (1 - Math.pow(2, -10 * x)) / (10 * Math.LN2),
(x) => (Math.pow(2, 10 * x - 10) - Math.pow(2, -10)) / (10 * Math.LN2),
(x) =>
0.5 * ((x - 1) * Math.sqrt(Math.max(0, 1 - Math.pow(x - 1, 2))) + Math.asin(x - 1)) +
Math.PI / 4,
(x) =>
x - 0.5 * (x * Math.sqrt(Math.max(0, 1 - x * x)) + Math.asin(Math.max(-1, Math.min(1, x)))),
(x) => {
const a = 2.70158;
const b = 1.70158;
return (
(1 - a + b) * x +
((3 * a - 2 * b) / 2) * x * x +
((-3 * a + b) / 3) * Math.pow(x, 3) +
(a / 4) * Math.pow(x, 4)
);
},
(x) => (2.70158 / 4) * Math.pow(x, 4) - (1.70158 / 3) * Math.pow(x, 3),
(x) =>
x < 0.5
? 0.5 * x -
0.25 * x * Math.sqrt(Math.max(0, 1 - 4 * x * x)) -
0.125 * Math.asin(Math.max(-1, Math.min(1, 2 * x)))
: 0.5 * x -
0.25 * (1 - x) * Math.sqrt(Math.max(0, 1 - 4 * (1 - x) * (1 - x))) -
0.125 * Math.asin(Math.max(-1, Math.min(1, 2 * (1 - x)))),
(x) => {
const s = 2.59491;
x = clamp(x, 0, 1);
if (x <= 0.5) return (s + 1) * Math.pow(x, 4) - (2 * s * Math.pow(x, 3)) / 3;
const Ihalf = (s + 1) * Math.pow(0.5, 4) - (2 * s * Math.pow(0.5, 3)) / 3;
const F = (t: number) =>
(s + 1) * Math.pow(t, 4) -
((10 * s + 12) / 3) * Math.pow(t, 3) +
((8 * s + 12) / 2) * t * t -
(2 * s + 3) * t;
return Ihalf + (F(x) - F(0.5));
},
// 23
(x) => {
x = clamp(x, 0, 1);
const K = 10 * Math.LN2;
const A = ((2 * Math.PI) / 3) * 10;
const B = -0.75 * ((2 * Math.PI) / 3);
const H = (t: number) =>
(Math.exp(-K * t) * (-K * Math.sin(A * t + B) - A * Math.cos(A * t + B))) / (A * A + K * K);
return x + (H(x) - H(0));
},
(x) => {
x = clamp(x, 0, 1);
const K = 10 * Math.LN2;
const A = ((2 * Math.PI) / 3) * 10;
const B = -10.75 * ((2 * Math.PI) / 3);
const C = Math.pow(2, -10);
const G = (t: number) =>
(-C * Math.exp(K * t) * (K * Math.sin(A * t + B) - A * Math.cos(A * t + B))) /
(A * A + K * K);
return G(x) - G(0);
},
(x) => {
x = clamp(x, 0, 1);
const A = 7.5625;
const b1 = 1 / 2.75;
const b2 = 2 / 2.75;
const b3 = 2.5 / 2.75;
const c1 = 1.5 / 2.75;
const c2 = 2.25 / 2.75;
const c3 = 2.625 / 2.75;
const I_b1 = (A * Math.pow(b1, 3)) / 3;
const I_b2 = I_b1 + (A / 3) * (Math.pow(b2 - c1, 3) - Math.pow(b1 - c1, 3)) + 0.75 * (b2 - b1);
const I_b3 =
I_b2 + (A / 3) * (Math.pow(b3 - c2, 3) - Math.pow(b2 - c2, 3)) + 0.9375 * (b3 - b2);
if (x < b1) return (A * Math.pow(x, 3)) / 3;
if (x < b2)
return I_b1 + (A / 3) * (Math.pow(x - c1, 3) - Math.pow(b1 - c1, 3)) + 0.75 * (x - b1);
if (x < b3)
return I_b2 + (A / 3) * (Math.pow(x - c2, 3) - Math.pow(b2 - c2, 3)) + 0.9375 * (x - b2);
return I_b3 + (A / 3) * (Math.pow(x - c3, 3) - Math.pow(b3 - c3, 3)) + 0.984375 * (x - b3);
},
(x) => {
x = clamp(x, 0, 1);
const A = 7.5625;
const b1 = 1 / 2.75;
const b2 = 2 / 2.75;
const b3 = 2.5 / 2.75;
const c1 = 1.5 / 2.75;
const c2 = 2.25 / 2.75;
const c3 = 2.625 / 2.75;
const outIntegral = (u: number) => {
u = clamp(u, 0, 1);
const I_b1 = (A * Math.pow(b1, 3)) / 3;
const I_b2 =
I_b1 + (A / 3) * (Math.pow(b2 - c1, 3) - Math.pow(b1 - c1, 3)) + 0.75 * (b2 - b1);
const I_b3 =
I_b2 + (A / 3) * (Math.pow(b3 - c2, 3) - Math.pow(b2 - c2, 3)) + 0.9375 * (b3 - b2);
if (u < b1) return (A * Math.pow(u, 3)) / 3;
if (u < b2)
return I_b1 + (A / 3) * (Math.pow(u - c1, 3) - Math.pow(b1 - c1, 3)) + 0.75 * (u - b1);
if (u < b3)
return I_b2 + (A / 3) * (Math.pow(u - c2, 3) - Math.pow(b2 - c2, 3)) + 0.9375 * (u - b2);
return I_b3 + (A / 3) * (Math.pow(u - c3, 3) - Math.pow(b3 - c3, 3)) + 0.984375 * (u - b3);
};
const I1 = outIntegral(1);
return x - (I1 - outIntegral(1 - x));
},
(x) => {
x = clamp(x, 0, 1);
const A = 7.5625;
const b1 = 1 / 2.75;
const b2 = 2 / 2.75;
const b3 = 2.5 / 2.75;
const c1 = 1.5 / 2.75;
const c2 = 2.25 / 2.75;
const c3 = 2.625 / 2.75;
const outIntegral = (u: number) => {
u = clamp(u, 0, 1);
const I_b1 = (A * Math.pow(b1, 3)) / 3;
const I_b2 =
I_b1 + (A / 3) * (Math.pow(b2 - c1, 3) - Math.pow(b1 - c1, 3)) + 0.75 * (b2 - b1);
const I_b3 =
I_b2 + (A / 3) * (Math.pow(b3 - c2, 3) - Math.pow(b2 - c2, 3)) + 0.9375 * (b3 - b2);
if (u < b1) return (A * Math.pow(u, 3)) / 3;
if (u < b2)
return I_b1 + (A / 3) * (Math.pow(u - c1, 3) - Math.pow(b1 - c1, 3)) + 0.75 * (u - b1);
if (u < b3)
return I_b2 + (A / 3) * (Math.pow(u - c2, 3) - Math.pow(b2 - c2, 3)) + 0.9375 * (u - b2);
return I_b3 + (A / 3) * (Math.pow(u - c3, 3) - Math.pow(b3 - c3, 3)) + 0.984375 * (u - b3);
};
const I1 = outIntegral(1);
if (x <= 0.5) {
return x / 2 + (I1 - outIntegral(1 - 2 * x)) / 4;
}
return x / 2 + (I1 + outIntegral(2 * x - 1)) / 4;
},
];

const calculateEasingValue = (
func: (x: number) => number,
x: number,
Expand Down Expand Up @@ -121,6 +287,22 @@ const EASING_DERIVATIVE_ENDS = EASINGS.map((func) => [
calculateDerivativeValue(func, 1),
]);

const calculateEasingIntegral = (
type: number,
x: number,
easingLeft = 0,
easingRight = 1,
): number => {
const scaledX = easingLeft + (easingRight - easingLeft) * x;

if (type < 1 || type > EASING_INTEGRALS.length) {
return (scaledX * scaledX) / 2;
}
const func = EASING_INTEGRALS[type - 1];
// ∫[left to scaledX] f(t) dt = F(scaledX) - F(left)
return func(scaledX) - func(easingLeft);
};

const sanitizeEasingParams = (type: number, x: number, easingLeft: number, easingRight: number) => {
return {
type: type > 0 && type <= EASINGS.length ? type : 1,
Expand Down Expand Up @@ -673,18 +855,34 @@ export const integrate = (
return k * calculateEasingValue(EASINGS[p.type - 1], p.x, p.easingLeft, p.easingRight) + b * p.x;
};

export const integrate2 = (
type: number,
x: number,
k: number,
c: number,
easingLeft = 0,
easingRight = 1,
): number => {
const integral = calculateEasingIntegral(type, x, easingLeft, easingRight);
return k * integral + c * x;
};

export const getIntegral = (
event: SpeedEvent | undefined,
bpmList: Bpm[],
integrateEasings: boolean,
beat: number | undefined = undefined,
): number => {
if (!event) return 0;
if (beat === undefined || beat >= event.endBeat) beat = event.endBeat;
const startSec = getTimeSec(bpmList, event.startBeat);
const progressedSec = getTimeSec(bpmList, beat) - startSec;
if ('easingType' in event && event.easingType > 1) {
const easingLeft = 'easingLeft' in event ? event.easingLeft : 0;
const easingRight = 'easingRight' in event ? event.easingRight : 1;
if (!('easingType' in event) || event.easingType <= 1) {
return ((event.start + (getEventValue(beat, event) as number)) * progressedSec) / 2;
}
const easingLeft = 'easingLeft' in event ? event.easingLeft : 0;
const easingRight = 'easingRight' in event ? event.easingRight : 1;
if (!integrateEasings) {
const df0 = derivative(event.easingType, 0, easingLeft, easingRight);
const df1 = derivative(event.easingType, 1, easingLeft, easingRight);
const k = (event.end - event.start) / (df1 - df0);
Expand All @@ -695,8 +893,14 @@ export const getIntegral = (
(integrate(event.easingType, x, k, b, easingLeft, easingRight) * lengthSec) /
(event.endBeat - event.startBeat)
);
} else {
const x = (beat - event.startBeat) / (event.endBeat - event.startBeat);
const easingIntegral = calculateEasingIntegral(event.easingType, x, easingLeft, easingRight);
const scaledIntegral =
event.start * x + ((event.end - event.start) * easingIntegral) / (easingRight - easingLeft);
const lengthSec = getTimeSec(bpmList, event.endBeat) - startSec;
return scaledIntegral * lengthSec;
}
return ((event.start + (getEventValue(beat, event) as number)) * progressedSec) / 2;
};

export const getJudgmentPosition = (input: PointerTap | PointerDrag, line: Line) => {
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export interface JudgeLine {
extended?: Extended;
father: number;
rotateWithFather?: boolean;
integrateSpeedEasings?: boolean;
isCover: number;
isGif?: boolean;
notes?: Note[];
Expand Down