kipphi 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/chart.ts +481 -0
- package/chartTypes.ts +512 -0
- package/easing.ts +543 -0
- package/env.ts +7 -0
- package/evaluator.ts +173 -0
- package/event.ts +853 -0
- package/index.ts +11 -0
- package/judgeline.ts +605 -0
- package/jumparray.ts +234 -0
- package/note.ts +731 -0
- package/package.json +20 -0
- package/rpeChartCompiler.ts +425 -0
- package/time.ts +308 -0
- package/tsconfig.json +30 -0
- package/util.ts +26 -0
- package/version.ts +8 -0
package/time.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { EventType, type BPMSegmentData, type TimeT } from "./chartTypes";
|
|
2
|
+
import { EventEndNode, EventNodeLike, EventNodeSequence, EventStartNode, type AnyEN, type ENOrHead, type ENOrTail } from "./event";
|
|
3
|
+
import { JumpArray } from "./jumparray";
|
|
4
|
+
import { NodeType } from "./util";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/// #declaration:global
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
class BPMStartNode extends EventStartNode {
|
|
13
|
+
spb: number;
|
|
14
|
+
cachedStartIntegral?: number;
|
|
15
|
+
override cachedIntegral?: number;
|
|
16
|
+
override next: BPMEndNode | BPMNodeLike<NodeType.TAIL>;
|
|
17
|
+
override previous: BPMEndNode | BPMNodeLike<NodeType.HEAD>;
|
|
18
|
+
constructor(startTime: TimeT, bpm: number) {
|
|
19
|
+
super(startTime, bpm);
|
|
20
|
+
this.spb = 60 / bpm;
|
|
21
|
+
}
|
|
22
|
+
override getIntegral(beats: number): number {
|
|
23
|
+
return (beats - TimeCalculator.toBeats(this.time)) * 60 / this.value;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* may only used with a startnode whose next is not tail
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
override getFullIntegral(): number {
|
|
30
|
+
return (TimeCalculator.toBeats(this.next.time) - TimeCalculator.toBeats(this.time)) * 60 / this.value;
|
|
31
|
+
}
|
|
32
|
+
/*
|
|
33
|
+
static connect(node1: BPMStartNode, node2: BPMEndNode | Tailer<EventStartNode>): void;
|
|
34
|
+
static connect(node1: BPMEndNode | Header<BPMStartNode>, node2: BPMStartNode): void;
|
|
35
|
+
static connect(node1: EventNode | Header<EventNode>, node2: EventNode | Tailer<EventNode>): void {
|
|
36
|
+
super.connect(node1, node2);
|
|
37
|
+
}
|
|
38
|
+
*/
|
|
39
|
+
}
|
|
40
|
+
class BPMEndNode extends EventEndNode {
|
|
41
|
+
spb: number;
|
|
42
|
+
override previous: BPMStartNode;
|
|
43
|
+
override next: BPMStartNode;
|
|
44
|
+
constructor(endTime: TimeT) {
|
|
45
|
+
super(endTime, null);
|
|
46
|
+
}
|
|
47
|
+
// @ts-expect-error
|
|
48
|
+
get value(): number {
|
|
49
|
+
return this.previous.value
|
|
50
|
+
}
|
|
51
|
+
set value(val) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface BPMNodeLike<T extends NodeType> extends EventNodeLike<T> {
|
|
55
|
+
next: [BPMStartNode, null, BNOrTail][T] | null;
|
|
56
|
+
previous: [null, BPMStartNode, BNOrHead][T] | null;
|
|
57
|
+
}
|
|
58
|
+
type BPMNode = BPMStartNode | BPMEndNode;
|
|
59
|
+
type AnyBN = (BPMNode | BPMNodeLike<NodeType.TAIL> | BPMNodeLike<NodeType.HEAD>);
|
|
60
|
+
type BNOrTail = BPMNode | BPMNodeLike<NodeType.TAIL>;
|
|
61
|
+
type BNOrHead = BPMNode | BPMNodeLike<NodeType.HEAD>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 拥有与事件类似的逻辑
|
|
65
|
+
* 每对节点之间代表一个BPM相同的片段
|
|
66
|
+
* 片段之间BPM可以发生改变
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
export class BPMSequence extends EventNodeSequence {
|
|
70
|
+
override head: BPMNodeLike<NodeType.HEAD>;
|
|
71
|
+
override tail: BPMNodeLike<NodeType.TAIL>;
|
|
72
|
+
/** 从拍数访问节点 */
|
|
73
|
+
override jump: JumpArray<AnyEN>;
|
|
74
|
+
/** 以秒计时的跳数组,处理从秒访问节点 */
|
|
75
|
+
secondJump: JumpArray<AnyBN>;
|
|
76
|
+
constructor(bpmList: BPMSegmentData[], public duration: number) {
|
|
77
|
+
super(EventType.bpm, null);
|
|
78
|
+
let curPos: BPMNodeLike<NodeType.HEAD> | BPMEndNode = this.head;
|
|
79
|
+
let next = bpmList[0];
|
|
80
|
+
this.listLength = bpmList.length;
|
|
81
|
+
for (let i = 1; i < bpmList.length; i++) {
|
|
82
|
+
const each = next;
|
|
83
|
+
next = bpmList[i];
|
|
84
|
+
const startNode = new BPMStartNode(each.startTime, each.bpm);
|
|
85
|
+
const endNode = new BPMEndNode(next.startTime);
|
|
86
|
+
BPMStartNode.connect(startNode, endNode);
|
|
87
|
+
BPMStartNode.connect(curPos, startNode);
|
|
88
|
+
curPos = endNode;
|
|
89
|
+
}
|
|
90
|
+
const last = new BPMStartNode(next.startTime, next.bpm)
|
|
91
|
+
BPMStartNode.connect(curPos, last);
|
|
92
|
+
BPMStartNode.connect(last, this.tail);
|
|
93
|
+
this.initJump();
|
|
94
|
+
}
|
|
95
|
+
override initJump(): void {
|
|
96
|
+
console.log(this)
|
|
97
|
+
this.effectiveBeats = TimeCalculator.toBeats(this.tail.previous.time)
|
|
98
|
+
if (this.effectiveBeats !== 0) {
|
|
99
|
+
super.initJump(); // 为0可以跳过jumpArray,用不到
|
|
100
|
+
// 只有一个BPM片段就会这样
|
|
101
|
+
}
|
|
102
|
+
this.updateSecondJump();
|
|
103
|
+
}
|
|
104
|
+
updateSecondJump(): void {
|
|
105
|
+
let integral = 0;
|
|
106
|
+
// 计算积分并缓存到BPMNode
|
|
107
|
+
let node: BPMStartNode = this.head.next;
|
|
108
|
+
while (true) {
|
|
109
|
+
node.cachedStartIntegral = integral;
|
|
110
|
+
if (node.next.type === NodeType.TAIL) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
const endNode = <BPMEndNode>(<BPMStartNode>node).next;
|
|
114
|
+
integral += node.getFullIntegral();
|
|
115
|
+
node.cachedIntegral = integral;
|
|
116
|
+
|
|
117
|
+
node = endNode.next;
|
|
118
|
+
}
|
|
119
|
+
node.cachedStartIntegral = integral;
|
|
120
|
+
if (this.effectiveBeats === 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const originalListLength = this.listLength;
|
|
124
|
+
this.secondJump = new JumpArray<AnyBN>(
|
|
125
|
+
this.head,
|
|
126
|
+
this.tail,
|
|
127
|
+
originalListLength,
|
|
128
|
+
this.duration,
|
|
129
|
+
(node: BPMStartNode) => {
|
|
130
|
+
if (node.type === NodeType.TAIL) {
|
|
131
|
+
return [null, null];
|
|
132
|
+
}
|
|
133
|
+
if (node.type === NodeType.HEAD) {
|
|
134
|
+
return [0, node.next];
|
|
135
|
+
}
|
|
136
|
+
const endNode = <BPMEndNode>(<BPMStartNode>node).next;
|
|
137
|
+
const time = node.cachedIntegral;
|
|
138
|
+
const nextNode = endNode.next;
|
|
139
|
+
if (nextNode.next.type === NodeType.TAIL) {
|
|
140
|
+
return [time, nextNode.next]; // Tailer代替最后一个StartNode去占位
|
|
141
|
+
} else {
|
|
142
|
+
return [time, nextNode];
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
(node: BPMStartNode, seconds: number) => {
|
|
146
|
+
return node.cachedIntegral > seconds ? false : (<BPMEndNode>node.next).next;
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
override updateJump(from: ENOrHead, to: ENOrTail): void {
|
|
151
|
+
super.updateJump(from, to);
|
|
152
|
+
this.updateSecondJump();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getNodeBySeconds(seconds: number): BPMStartNode {
|
|
156
|
+
if (this.effectiveBeats === 0) {
|
|
157
|
+
return this.tail.previous
|
|
158
|
+
}
|
|
159
|
+
const node = this.secondJump.getNodeAt(seconds);
|
|
160
|
+
if (node.type === NodeType.TAIL) {
|
|
161
|
+
return node.previous;
|
|
162
|
+
}
|
|
163
|
+
return node as BPMStartNode;
|
|
164
|
+
}
|
|
165
|
+
dumpBPM(): BPMSegmentData[] {
|
|
166
|
+
let cur = this.head.next;
|
|
167
|
+
const ret: BPMSegmentData[] = [];
|
|
168
|
+
while (true) {
|
|
169
|
+
ret.push({
|
|
170
|
+
bpm: cur.value,
|
|
171
|
+
startTime: cur.time
|
|
172
|
+
})
|
|
173
|
+
const end = cur.next;
|
|
174
|
+
if (end.type === NodeType.TAIL) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
cur = end.next;
|
|
178
|
+
}
|
|
179
|
+
return ret;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @alias TC
|
|
185
|
+
*/
|
|
186
|
+
export class TimeCalculator {
|
|
187
|
+
bpmList: BPMSegmentData[];
|
|
188
|
+
bpmSequence: BPMSequence;
|
|
189
|
+
duration: number;
|
|
190
|
+
|
|
191
|
+
constructor() {
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
update() {
|
|
195
|
+
let bpmList = this.bpmList;
|
|
196
|
+
this.bpmSequence = new BPMSequence(bpmList, this.duration);
|
|
197
|
+
}
|
|
198
|
+
toSeconds(beats: number) {
|
|
199
|
+
const node: BPMStartNode = this.bpmSequence.getNodeAt(beats);
|
|
200
|
+
return node.cachedStartIntegral + node.getIntegral(beats)
|
|
201
|
+
}
|
|
202
|
+
segmentToSeconds(beats1: number, beats2: number): number {
|
|
203
|
+
let ret = this.toSeconds(beats2) - this.toSeconds(beats1)
|
|
204
|
+
if (ret < 0) {
|
|
205
|
+
console.warn("segmentToSeconds的第二个参数需大于第一个!", "得到的参数:", arguments)
|
|
206
|
+
}
|
|
207
|
+
return ret
|
|
208
|
+
}
|
|
209
|
+
secondsToBeats(seconds: number) {
|
|
210
|
+
const node = this.bpmSequence.getNodeBySeconds(seconds);
|
|
211
|
+
// console.log("node:", node)
|
|
212
|
+
const beats = (seconds - node.cachedStartIntegral) / node.spb;
|
|
213
|
+
return TimeCalculator.toBeats(node.time) + beats
|
|
214
|
+
}
|
|
215
|
+
static toBeats(beaT: TimeT): number {
|
|
216
|
+
if (!beaT) debugger
|
|
217
|
+
return beaT[0] + beaT[1] / beaT[2]
|
|
218
|
+
}
|
|
219
|
+
static getDelta(beaT1: TimeT, beaT2: TimeT): number {
|
|
220
|
+
return this.toBeats(beaT1) - this.toBeats(beaT2)
|
|
221
|
+
}
|
|
222
|
+
static eq(beaT1: TimeT, beaT2: TimeT): boolean {
|
|
223
|
+
return beaT1[0] === beaT2 [0] && beaT1[1] * beaT2[2] === beaT1[2] * beaT2[1] // 这里曾经把两个都写成beaT1,特此留念(
|
|
224
|
+
}
|
|
225
|
+
static gt(beaT1:TimeT, beaT2: TimeT): boolean {
|
|
226
|
+
return beaT1[0] > beaT2[0] || beaT1[0] === beaT2[0] && beaT1[1] * beaT2[2] > beaT1[2] * beaT2[1]
|
|
227
|
+
}
|
|
228
|
+
static lt(beaT1:TimeT, beaT2: TimeT): boolean {
|
|
229
|
+
return beaT1[0] < beaT2[0] || beaT1[0] === beaT2[0] && beaT1[1] * beaT2[2] < beaT1[2] * beaT2[1]
|
|
230
|
+
}
|
|
231
|
+
static ne(beaT1:TimeT, beaT2: TimeT): boolean {
|
|
232
|
+
return beaT1[0] !== beaT2[0] || beaT1[1] * beaT2[2] !== beaT1[2] * beaT2[1]
|
|
233
|
+
}
|
|
234
|
+
static add(beaT1: TimeT, beaT2: TimeT): TimeT {
|
|
235
|
+
return [beaT1[0] + beaT2[0], beaT1[1] * beaT2[2] + beaT1[2] * beaT2[1], beaT1[2] * beaT2[2]]
|
|
236
|
+
}
|
|
237
|
+
static sub(beaT1: TimeT, beaT2: TimeT): TimeT {
|
|
238
|
+
return [beaT1[0] - beaT2[0], beaT1[1] * beaT2[2] - beaT1[2] * beaT2[1], beaT1[2] * beaT2[2]]
|
|
239
|
+
}
|
|
240
|
+
static div(beaT1: TimeT, beaT2: TimeT): [number, number] {
|
|
241
|
+
return [(beaT1[0] * beaT1[2] + beaT1[1]) * beaT2[2], (beaT2[0] * beaT2[2] + beaT2[1]) * beaT1[2]]
|
|
242
|
+
}
|
|
243
|
+
static mul(beaT: TimeT, ratio: [number, number]): TimeT {
|
|
244
|
+
// 将带分数beaT: TimeT乘一个分数[number, number]得到一个新的带分数returnval: TimeT,不要求这个带分数分子不超过分母,但所有的数都是整数
|
|
245
|
+
// (输入的两个元组都是整数元组)
|
|
246
|
+
const [numerator, denominator] = ratio
|
|
247
|
+
const b0nume = beaT[0] * numerator;
|
|
248
|
+
const remainder = b0nume % denominator;
|
|
249
|
+
if (remainder === 0) {
|
|
250
|
+
return [b0nume / denominator, beaT[1] * numerator, beaT[2] * denominator]
|
|
251
|
+
} else {
|
|
252
|
+
return [Math.floor(b0nume / denominator), beaT[1] * numerator + remainder * beaT[2], beaT[2] * denominator]
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 原地规范化时间元组,但仍然返回这个元组,方便使用
|
|
257
|
+
* validate TimeT in place
|
|
258
|
+
* @param beaT
|
|
259
|
+
*/
|
|
260
|
+
static validateIp(beaT: TimeT): TimeT {
|
|
261
|
+
if (beaT === undefined || beaT[2] === 0) {
|
|
262
|
+
throw new Error("Invalid time" + beaT.valueOf());
|
|
263
|
+
}
|
|
264
|
+
if (beaT[1] >= beaT[2]) {
|
|
265
|
+
const quotient = Math.floor(beaT[1] / beaT[2]);
|
|
266
|
+
const remainder = beaT[1] % beaT[2];
|
|
267
|
+
beaT[0] += quotient;
|
|
268
|
+
beaT[1] = remainder;
|
|
269
|
+
} else if (beaT[1] < 0) {
|
|
270
|
+
const quotient = Math.floor(beaT[1] / beaT[2]);
|
|
271
|
+
const remainder = beaT[2] + beaT[1] % beaT[2];
|
|
272
|
+
beaT[0] += quotient;
|
|
273
|
+
beaT[1] = remainder;
|
|
274
|
+
}
|
|
275
|
+
if (beaT[1] === 0) {
|
|
276
|
+
beaT[2] = 1;
|
|
277
|
+
return beaT;
|
|
278
|
+
}
|
|
279
|
+
const gcd = this.gcd(beaT[2], beaT[1]);
|
|
280
|
+
if (gcd > 1) {
|
|
281
|
+
beaT[1] /= gcd;
|
|
282
|
+
beaT[2] /= gcd;
|
|
283
|
+
}
|
|
284
|
+
return beaT;
|
|
285
|
+
}
|
|
286
|
+
static vadd(beaT1: TimeT, beaT2: TimeT) { return this.validateIp(this.add(beaT1, beaT2)); }
|
|
287
|
+
static vsub(beaT1: TimeT, beaT2: TimeT) { return this.validateIp(this.sub(beaT1, beaT2)); }
|
|
288
|
+
static vmul(beaT: TimeT, ratio: [number, number]): TimeT { return this.validateIp(this.mul(beaT, ratio)); }
|
|
289
|
+
static gcd(a: number, b: number): number {
|
|
290
|
+
if (a === 0 || b === 0) {
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
while (b !== 0) {
|
|
294
|
+
const r = a % b;
|
|
295
|
+
a = b;
|
|
296
|
+
b = r;
|
|
297
|
+
}
|
|
298
|
+
return a;
|
|
299
|
+
}
|
|
300
|
+
dump(): BPMSegmentData[] {
|
|
301
|
+
return this.bpmSequence.dumpBPM();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export const TC = TimeCalculator;
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
/// #enddeclaration
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ES2017"],
|
|
5
|
+
"target": "ES2016",
|
|
6
|
+
"module": "es2020",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"outFile": "../dist/index.d.ts",
|
|
10
|
+
|
|
11
|
+
"emitDeclarationOnly": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Best practices
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUncheckedIndexedAccess": true,
|
|
19
|
+
"noImplicitOverride": true,
|
|
20
|
+
|
|
21
|
+
// Some stricter flags (disabled by default)
|
|
22
|
+
"noUnusedLocals": false,
|
|
23
|
+
"noUnusedParameters": false,
|
|
24
|
+
"noPropertyAccessFromIndexSignature": false
|
|
25
|
+
},
|
|
26
|
+
"include": [
|
|
27
|
+
"../index.ts"
|
|
28
|
+
],
|
|
29
|
+
"exclude": ["../dist"]
|
|
30
|
+
}
|
package/util.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RGB } from "./chartTypes";
|
|
2
|
+
|
|
3
|
+
/// #declaration:global
|
|
4
|
+
|
|
5
|
+
export enum NodeType {
|
|
6
|
+
HEAD=0,
|
|
7
|
+
TAIL=1,
|
|
8
|
+
MIDDLE=2
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TupleCoord = [x: number, y: number]
|
|
12
|
+
|
|
13
|
+
export const rgb2hex = (rgb: RGB) => {
|
|
14
|
+
return rgb[0] << 16 | rgb[1] << 8 | rgb[2];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const hex2rgb = (hex: number): RGB => {
|
|
18
|
+
return [hex >> 16, hex >> 8 & 0xFF, hex & 0xFF]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 四位精度小数变分数
|
|
22
|
+
export const numberToRatio = (num: number): [number, number] => {
|
|
23
|
+
return [Math.round(num * 10000), 10000]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// #enddeclaration
|