@dan-uni/dan-any 0.1.0 → 0.2.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.
Files changed (54) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +5956 -1
  3. package/dist/index.umd.min.js +19951 -1
  4. package/dist/index.umd.min.js.LICENSE.txt +17 -0
  5. package/dist/src/ass-gen/__tests__/generate.test.d.ts +1 -0
  6. package/dist/src/ass-gen/ass/create.d.ts +4 -0
  7. package/dist/src/ass-gen/ass/dialogue.d.ts +17 -0
  8. package/dist/src/ass-gen/ass/event.d.ts +3 -0
  9. package/dist/src/ass-gen/ass/info.d.ts +8 -0
  10. package/dist/src/ass-gen/ass/raw.d.ts +10 -0
  11. package/dist/src/ass-gen/ass/style.d.ts +3 -0
  12. package/dist/src/ass-gen/config.d.ts +3 -0
  13. package/dist/src/ass-gen/index.d.ts +26 -0
  14. package/dist/src/ass-gen/types.d.ts +47 -0
  15. package/dist/src/ass-gen/util/color.d.ts +18 -0
  16. package/dist/src/ass-gen/util/danconvert.d.ts +4 -0
  17. package/dist/src/ass-gen/util/index.d.ts +4 -0
  18. package/dist/src/ass-gen/util/lang.d.ts +3 -0
  19. package/dist/src/ass-gen/util/layout.d.ts +4 -0
  20. package/dist/{index.d.ts → src/index.d.ts} +15 -1
  21. package/dist/src/index.test.d.ts +1 -0
  22. package/dist/{utils → src/utils}/dm-gen.d.ts +13 -5
  23. package/dist/src/utils/dm-gen.test.d.ts +1 -0
  24. package/package.json +9 -7
  25. package/rslib.config.ts +81 -0
  26. package/src/ass-gen/__tests__/898651903.xml +1619 -0
  27. package/src/ass-gen/__tests__/898651903.xml.ass +1392 -0
  28. package/src/ass-gen/__tests__/canvas.test.ts +11 -0
  29. package/src/ass-gen/__tests__/generate.test.ts +20 -0
  30. package/src/ass-gen/ass/create.ts +26 -0
  31. package/src/ass-gen/ass/dialogue.ts +91 -0
  32. package/src/ass-gen/ass/event.ts +58 -0
  33. package/src/ass-gen/ass/info.ts +28 -0
  34. package/src/ass-gen/ass/raw.ts +69 -0
  35. package/src/ass-gen/ass/style.ts +67 -0
  36. package/src/ass-gen/config.ts +45 -0
  37. package/src/ass-gen/index.ts +52 -0
  38. package/src/ass-gen/types.ts +52 -0
  39. package/src/ass-gen/util/color.ts +55 -0
  40. package/src/ass-gen/util/danconvert.ts +36 -0
  41. package/src/ass-gen/util/index.ts +10 -0
  42. package/src/ass-gen/util/lang.ts +35 -0
  43. package/src/ass-gen/util/layout.ts +238 -0
  44. package/src/index.test.ts +9 -0
  45. package/src/index.ts +111 -0
  46. package/src/utils/dm-gen.test.ts +66 -0
  47. package/src/utils/dm-gen.ts +91 -11
  48. package/tsconfig.json +7 -7
  49. package/dist/index.min.js +0 -1
  50. package/rollup.config.mjs +0 -51
  51. /package/dist/{index.test.d.ts → src/ass-gen/__tests__/canvas.test.d.ts} +0 -0
  52. /package/dist/{proto → src/proto}/gen/bili/dm_pb.d.ts +0 -0
  53. /package/dist/{proto → src/proto}/gen/danuni_pb.d.ts +0 -0
  54. /package/dist/{utils → src/utils}/id-gen.d.ts +0 -0
@@ -0,0 +1,11 @@
1
+ import { assertType, it } from 'vitest'
2
+
3
+ import { measureTextWidth } from '../util/layout'
4
+
5
+ it('canvas measureTextWidth', () => {
6
+ const text = '一段测试文字'
7
+ const width = measureTextWidth('SimHei', 25, false, text)
8
+ assertType<number>(width)
9
+ console.info(width, text.length)
10
+ // assert(width >= 25 * text.length)
11
+ })
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs'
2
+ import path, { dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { it } from 'vitest'
5
+
6
+ import { generateASS } from '../'
7
+ import { UniPool } from '../..'
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url))
10
+
11
+ it('generate ass from xml', () => {
12
+ const filename = '898651903.xml'
13
+ const xmlPath = path.join(__dirname, filename)
14
+ const xmlText = fs.readFileSync(xmlPath, 'utf-8')
15
+ const assText = generateASS(UniPool.fromBiliXML(xmlText), {
16
+ // filename,
17
+ // title: '我的忏悔',
18
+ })
19
+ fs.writeFileSync(path.join(__dirname, `${filename}.ass`), assText, 'utf-8')
20
+ })
@@ -0,0 +1,26 @@
1
+ import type { UniPool } from '../..'
2
+ import type { Context, SubtitleStyle } from '../types'
3
+
4
+ import { UniPool2DanmakuLists } from '../util'
5
+ import event from './event'
6
+ import info from './info'
7
+ import raw from './raw'
8
+ import style from './style'
9
+
10
+ // eslint-disable-next-line import/no-default-export
11
+ export default (
12
+ list: UniPool,
13
+ rawList: UniPool,
14
+ config: SubtitleStyle,
15
+ context: Context = { filename: 'unknown', title: 'unknown' },
16
+ ) => {
17
+ const Elist = UniPool2DanmakuLists(list),
18
+ ErawList = UniPool2DanmakuLists(rawList)
19
+ const content = [info(config, context), style(config), event(Elist, config)]
20
+
21
+ if (config.includeRaw) {
22
+ content.push(raw(ErawList, config, context))
23
+ }
24
+
25
+ return `${content.join('\n\n')}\n`
26
+ }
@@ -0,0 +1,91 @@
1
+ import type { RGB } from '../types'
2
+
3
+ import { DanmakuType } from '../types'
4
+ import { formatColor, getDecoratingColor, isWhite } from '../util'
5
+
6
+ // Dialogue: 0,0:00:00.00,0:00:08.00,R2L,,20,20,2,,{\move(622.5,25,-62.5,25)}标准小裤裤
7
+ // Dialogue: 0,0:00:08.35,0:00:12.35,Fix,,20,20,2,,{\pos(280,50)\c&HEAA000}没男主吗
8
+
9
+ const formatTime = (seconds: number) => {
10
+ const div = (i: number, j: number) => Math.floor(i / j)
11
+ const pad = (n: number) => (n < 10 ? `0${n}` : String(n))
12
+
13
+ const integer = Math.floor(seconds)
14
+ const hour = div(integer, 60 * 60)
15
+ const minute = div(integer, 60) % 60
16
+ const second = integer % 60
17
+ const minorSecond = Math.floor((seconds - integer) * 100) // 取小数部分2位
18
+
19
+ return `${hour}:${pad(minute)}:${pad(second)}.${minorSecond}`
20
+ }
21
+
22
+ const encode = (text: string) =>
23
+ text
24
+ .toString()
25
+ .replaceAll('{', '{')
26
+ .replaceAll('}', '}')
27
+ .replaceAll(/\r|\n/g, '')
28
+
29
+ const scrollCommand = ({
30
+ start,
31
+ end,
32
+ top,
33
+ }: {
34
+ start: number
35
+ end: number
36
+ top: number
37
+ }) => `\\move(${start},${top},${end},${top})`
38
+ const fixCommand = ({ top, left }: { top: number; left: number }) =>
39
+ `\\an8\\pos(${left},${top})`
40
+ const colorCommand = (color: RGB) => `\\c${formatColor(color)}`
41
+ const borderColorCommand = (color: RGB) => `\\3c${formatColor(color)}`
42
+
43
+ // eslint-disable-next-line import/no-default-export
44
+ export default (
45
+ danmaku: {
46
+ type: (typeof DanmakuType)[keyof typeof DanmakuType]
47
+ color: RGB
48
+ fontSizeType: number
49
+ content: string
50
+ time: number
51
+ start: number
52
+ end: number
53
+ top: number
54
+ left: number
55
+ },
56
+ config: { scrollTime: number; fixTime: number },
57
+ ) => {
58
+ const { fontSizeType, content, time } = danmaku
59
+ const { scrollTime, fixTime } = config
60
+
61
+ const commands = [
62
+ danmaku.type === DanmakuType.SCROLL
63
+ ? scrollCommand({
64
+ start: danmaku.start,
65
+ end: danmaku.end,
66
+ top: danmaku.top,
67
+ })
68
+ : fixCommand({ top: danmaku.top, left: danmaku.left }),
69
+ // 所有网站的原始默认色是白色,所以白色的时候不用额外加和颜色相关的指令
70
+ isWhite(danmaku.color) ? '' : colorCommand(danmaku.color),
71
+ isWhite(danmaku.color)
72
+ ? ''
73
+ : borderColorCommand(getDecoratingColor(danmaku.color)),
74
+ ]
75
+ const fields = [
76
+ 0, // Layer,
77
+ formatTime(time), // Start
78
+ formatTime(
79
+ time + (danmaku.type === DanmakuType.SCROLL ? scrollTime : fixTime),
80
+ ), // End
81
+ `F${fontSizeType}`, // Style
82
+ '', // Name
83
+ '0000', // MarginL
84
+ '0000', // MarginR
85
+ '0000', // MarginV
86
+ '', // Effect
87
+ `{${commands.join('')}}${encode(content)}`, // Text
88
+ ]
89
+
90
+ return `Dialogue: ${fields.join(',')}`
91
+ }
@@ -0,0 +1,58 @@
1
+ import type { Danmaku, SubtitleStyle } from '../types'
2
+
3
+ import { DanmakuType } from '../types'
4
+ import dialogue from './dialogue'
5
+
6
+ const calculateDanmakuPosition = (danmaku: Danmaku, config: SubtitleStyle) => {
7
+ const { playResX, playResY, scrollTime, fixTime } = config
8
+
9
+ switch (danmaku.type) {
10
+ case DanmakuType.SCROLL: {
11
+ const start = playResX
12
+ const end = -playResX / 10 // Some extra space for complete exit
13
+ const top = (danmaku.fontSizeType * playResY) / 20
14
+ return {
15
+ ...danmaku,
16
+ start,
17
+ end,
18
+ top,
19
+ left: 0,
20
+ duration: scrollTime,
21
+ }
22
+ }
23
+ case DanmakuType.TOP:
24
+ case DanmakuType.BOTTOM: {
25
+ const left = playResX / 2
26
+ const top =
27
+ danmaku.type === DanmakuType.TOP
28
+ ? (danmaku.fontSizeType * playResY) / 20
29
+ : playResY -
30
+ config.bottomSpace -
31
+ (danmaku.fontSizeType * playResY) / 20
32
+ return {
33
+ ...danmaku,
34
+ start: 0,
35
+ end: 0,
36
+ top,
37
+ left,
38
+ duration: fixTime,
39
+ }
40
+ }
41
+ default:
42
+ throw new Error(`Unknown danmaku type: ${danmaku.type}`)
43
+ }
44
+ }
45
+
46
+ // eslint-disable-next-line import/no-default-export
47
+ export default (list: Danmaku[], config: SubtitleStyle) => {
48
+ const content = [
49
+ '[Events]',
50
+ 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
51
+ ...list.map((danmaku) => {
52
+ const positionedDanmaku = calculateDanmakuPosition(danmaku, config)
53
+ return dialogue(positionedDanmaku, config)
54
+ }),
55
+ ]
56
+
57
+ return content.join('\n')
58
+ }
@@ -0,0 +1,28 @@
1
+ import type { Context } from '../types'
2
+
3
+ import pkg from '../../../package.json' with { type: 'json' }
4
+
5
+ type ExtraInfo = Context
6
+
7
+ type Resolution = {
8
+ playResX: number
9
+ playResY: number
10
+ }
11
+
12
+ // eslint-disable-next-line import/no-default-export
13
+ export default (
14
+ { playResX, playResY }: Resolution,
15
+ { filename, title }: ExtraInfo,
16
+ ) => {
17
+ const content = [
18
+ '[Script Info]',
19
+ `Title: ${title}`,
20
+ `Original Script: 根据 ${filename} 的弹幕信息,由 ${pkg.homepage} 生成`,
21
+ 'ScriptType: v4.00+',
22
+ 'Collisions: Reverse',
23
+ `PlayResX: ${playResX}`,
24
+ `PlayResY: ${playResY}`,
25
+ 'Timer: 100.0000',
26
+ ]
27
+ return content.join('\n')
28
+ }
@@ -0,0 +1,69 @@
1
+ import { brotliCompressSync, brotliDecompressSync, gzipSync } from 'node:zlib'
2
+ import * as base16384 from 'base16384'
3
+ import type { Context, Danmaku, SubtitleStyle } from '../types'
4
+
5
+ type compressType = 'brotli' | 'gzip'
6
+ type baseType = 'base64' | 'base18384'
7
+ const compressTypes = ['brotli', 'gzip'],
8
+ baseTypes = ['base64', 'base18384']
9
+
10
+ function fromUint16Array(array: Uint16Array): string {
11
+ let result = ''
12
+ for (const element of array) {
13
+ result += String.fromCharCode(element)
14
+ }
15
+ return result
16
+ }
17
+
18
+ // eslint-disable-next-line import/no-default-export
19
+ export default (
20
+ list: Danmaku[],
21
+ config: SubtitleStyle,
22
+ context: Context,
23
+ compressType: compressType = 'brotli',
24
+ baseType: baseType = 'base18384',
25
+ ) => {
26
+ const raw = { list, config, context },
27
+ rawText = JSON.stringify(raw)
28
+ let compress = Buffer.from('')
29
+ if (compressType === 'brotli') compress = brotliCompressSync(rawText)
30
+ else compress = gzipSync(rawText)
31
+ return `;RawCompressType: ${compressType}\n;RawBaseType: ${baseType}\n;Raw: ${baseType === 'base64' ? compress.toString('base64') : fromUint16Array(base16384.encode(compress))}`
32
+ }
33
+
34
+ export function deRaw(ass: string):
35
+ | {
36
+ list: Danmaku[]
37
+ config: SubtitleStyle
38
+ context: Context
39
+ }
40
+ | undefined {
41
+ const arr = ass.split('\n'),
42
+ lineCompressType = arr.find((line) => line.startsWith(';RawCompressType:')),
43
+ lineBaseType = arr.find((line) => line.startsWith(';RawBaseType:')),
44
+ lineRaw = arr.find((line) => line.startsWith(';Raw:'))
45
+ if (!lineCompressType || !lineBaseType || !lineRaw) return undefined
46
+ else {
47
+ let compressType = lineCompressType
48
+ .replace(';RawCompressType: ', '')
49
+ .trim(),
50
+ baseType = lineBaseType.replace(';RawBaseType: ', '').trim()
51
+ if (!compressTypes.includes(compressType)) compressType = 'gzip'
52
+ if (!baseTypes.includes(baseType)) baseType = 'base64'
53
+ const text = lineRaw.replace(';Raw: ', '').trim(),
54
+ buffer =
55
+ baseType === 'base64'
56
+ ? Buffer.from(text, 'base64')
57
+ : Buffer.from(
58
+ base16384.decode(Buffer.from(text, 'utf-8').toString('utf-8')),
59
+ )
60
+ let decompress = Buffer.from('')
61
+ if (compressType === 'brotli') decompress = brotliDecompressSync(buffer)
62
+ else decompress = gzipSync(buffer)
63
+ try {
64
+ return JSON.parse(decompress.toString('utf-8'))
65
+ } catch {
66
+ return undefined
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,67 @@
1
+ import type { SubtitleStyle } from '../types'
2
+
3
+ import { formatColor, getDecoratingColor, hexColorToRGB } from '../util'
4
+
5
+ // eslint-disable-next-line import/no-default-export
6
+ export default ({
7
+ fontName,
8
+ fontSize,
9
+ color: configColor,
10
+ outlineColor,
11
+ backColor,
12
+ bold,
13
+ outline,
14
+ shadow,
15
+ opacity,
16
+ }: SubtitleStyle) => {
17
+ const fields = [
18
+ 'Name',
19
+ 'Fontname',
20
+ 'Fontsize',
21
+ 'PrimaryColour',
22
+ 'SecondaryColour',
23
+ 'OutlineColour',
24
+ 'BackColour',
25
+ 'Bold',
26
+ 'Italic',
27
+ 'Underline',
28
+ 'StrikeOut',
29
+ 'ScaleX',
30
+ 'ScaleY',
31
+ 'Spacing',
32
+ 'Angle',
33
+ 'BorderStyle',
34
+ 'Outline',
35
+ 'Shadow',
36
+ 'Alignment',
37
+ 'MarginL',
38
+ 'MarginR',
39
+ 'MarginV',
40
+ 'Encoding',
41
+ ]
42
+ // 默认白底黑框
43
+ const primaryColorValue = formatColor(hexColorToRGB(configColor), opacity)
44
+ // 边框和阴影颜色没给的话算一个出来,不是黑就是白
45
+ const secondaryColor = getDecoratingColor(hexColorToRGB(configColor))
46
+ const outlineColorValue = formatColor(
47
+ outlineColor ? hexColorToRGB(outlineColor) : secondaryColor,
48
+ opacity,
49
+ )
50
+ const backColorValue = formatColor(
51
+ backColor ? hexColorToRGB(backColor) : secondaryColor,
52
+ opacity,
53
+ )
54
+ const colorStyle = `${primaryColorValue},${primaryColorValue},${outlineColorValue},${backColorValue}`
55
+
56
+ const boldValue = bold ? '1' : '0'
57
+ const fontStyle = `${boldValue},0,0,0,100,100,0,0,1,${outline},${shadow},7,0,0,0,0`
58
+
59
+ const fontDeclaration = (size: number, i: number) =>
60
+ `Style: F${i},${fontName},${size},${colorStyle},${fontStyle}`
61
+ const content = [
62
+ '[V4+ Styles]',
63
+ `Format: ${fields.join(',')}`,
64
+ ...fontSize.map(fontDeclaration),
65
+ ]
66
+ return content.join('\n')
67
+ }
@@ -0,0 +1,45 @@
1
+ import type { SubtitleStyle } from './types'
2
+
3
+ import { assign, formatColor, hexColorToRGB } from './util'
4
+
5
+ // const builtinRules = {
6
+ // COLOR: true,
7
+ // TOP: true,
8
+ // BOTTOM: true
9
+ // }
10
+
11
+ // const convertBlockRule = (rule: string) =>
12
+ // builtinRules[rule] ? rule : new RegExp(rule)
13
+
14
+ export default (overrides = {}): SubtitleStyle => {
15
+ const defaults = {
16
+ fontSize: [25, 25, 36],
17
+ fontName: 'SimHei',
18
+ color: '#ffffff',
19
+ outlineColor: undefined,
20
+ backColor: undefined,
21
+ outline: 2,
22
+ shadow: 0,
23
+ bold: false,
24
+ padding: [2, 2, 2, 2],
25
+ playResX: 1280,
26
+ playResY: 720,
27
+ scrollTime: 8,
28
+ fixTime: 4,
29
+ opacity: 0.6,
30
+ bottomSpace: 60,
31
+ includeRaw: true,
32
+ mergeIn: -1,
33
+ // block: [],
34
+ }
35
+
36
+ const config = assign(defaults, overrides)
37
+ config.color = formatColor(hexColorToRGB(config.color))
38
+ config.outlineColor =
39
+ config.outlineColor && formatColor(hexColorToRGB(config.outlineColor))
40
+ config.backColor =
41
+ config.backColor && formatColor(hexColorToRGB(config.backColor))
42
+ // config.block = uniqueArray(config.block).map(convertBlockRule)
43
+
44
+ return config
45
+ }
@@ -0,0 +1,52 @@
1
+ // import parse from './parse/bilibili'
2
+ import type { SubtitleStyle } from './types'
3
+
4
+ import { UniPool } from '..'
5
+ import ass from './ass/create'
6
+ import { deRaw } from './ass/raw'
7
+ import getConfig from './config'
8
+ import { DanmakuList2UniPool, layoutDanmaku } from './util'
9
+
10
+ export type Options = {
11
+ filename?: string
12
+ title?: string
13
+ substyle?: Partial<SubtitleStyle>
14
+ }
15
+
16
+ /**
17
+ * 使用bilibili弹幕(XMl)生成ASS字幕文件
18
+ * @param {string} danmaku XML弹幕文件内容
19
+ * @param {Options} options 杂项
20
+ * @returns {string} 返回ASS字幕文件内容
21
+ * @description 杂项相关
22
+ `filename`: 还原文件为XML时使用的默认文件名
23
+ `title`: ASS [Script Info] Title 项的值,显示于播放器字幕选择
24
+ `substyle`: ASS字幕样式
25
+ * @example ```ts
26
+ import fs from 'fs'
27
+ const filename = 'example.xml'
28
+ const xmlText = fs.readFileSync(filename, 'utf-8')
29
+ const assText = generateASS(xmlText, { filename, title: 'Quick Example' })
30
+ fs.writeFileSync(`${filename}.ass`, assText, 'utf-8')
31
+ ```
32
+ */
33
+ export function generateASS(danmaku: UniPool, options: Options): string {
34
+ // const result = parse(text)
35
+ const config = getConfig(options.substyle)
36
+ // const filteredList = filterDanmaku(result.list, config.block)
37
+ // const mergedList = mergeDanmaku(result.list, config.mergeIn)
38
+ const mergedList = danmaku.merge(config.mergeIn)
39
+ const layoutList = layoutDanmaku(mergedList, config)
40
+ const content = ass(layoutList, danmaku, config, {
41
+ filename: options?.filename || 'unknown',
42
+ title: options?.title || 'unknown',
43
+ })
44
+
45
+ return content
46
+ }
47
+
48
+ export function parseAssRawField(ass: string): UniPool {
49
+ const raw = deRaw(ass)
50
+ if (!raw) return UniPool.create()
51
+ else return DanmakuList2UniPool(raw.list)
52
+ }
@@ -0,0 +1,52 @@
1
+ // export type BlockRule = string | RegExp
2
+
3
+ import type { UniDM } from '../utils/dm-gen'
4
+
5
+ export interface Context {
6
+ filename: string
7
+ title: string
8
+ }
9
+
10
+ export interface SubtitleStyle {
11
+ fontSize: number[]
12
+ fontName: string
13
+ color: string
14
+ outlineColor?: string
15
+ backColor?: string
16
+ outline: number
17
+ shadow: number
18
+ bold: boolean
19
+ padding: number[]
20
+ playResX: number
21
+ playResY: number
22
+ scrollTime: number
23
+ fixTime: number
24
+ opacity: number
25
+ bottomSpace: number
26
+ // block: BlockRule[]
27
+ includeRaw: boolean
28
+ mergeIn: number
29
+ }
30
+
31
+ export type RGB = { r: number; g: number; b: number }
32
+
33
+ export const DanmakuType = {
34
+ SCROLL: 1,
35
+ BOTTOM: 2,
36
+ TOP: 3,
37
+ }
38
+
39
+ export const FontSize = {
40
+ SMALL: 0,
41
+ NORMAL: 1,
42
+ LARGE: 2,
43
+ }
44
+
45
+ export type Danmaku = {
46
+ time: number
47
+ type: number
48
+ fontSizeType: number
49
+ color: RGB
50
+ content: string
51
+ extra: UniDM
52
+ }
@@ -0,0 +1,55 @@
1
+ import type { RGB } from '../types'
2
+
3
+ const pad = (s: string) => (s.length < 2 ? `0${s}` : s)
4
+ const decimalToHex = (n: number) => pad(n.toString(16))
5
+
6
+ /**
7
+ * 本函数实现复制自 [us-danmaku](https://github.com/tiansh/us-danmaku) 项目
8
+ */
9
+ const isDarkColor = ({ r, g, b }: RGB) =>
10
+ r * 0.299 + g * 0.587 + b * 0.114 < 0x30
11
+
12
+ const WHITE = { r: 255, g: 255, b: 255 }
13
+ const BLACK = { r: 0, g: 0, b: 0 }
14
+
15
+ export const hexColorToRGB = (hex: string) => {
16
+ if (hex.indexOf('#') === 0) {
17
+ hex = hex.slice(1)
18
+ }
19
+
20
+ const [r, g, b] =
21
+ hex.length === 3 ? hex.split('').map((c) => c + c) : hex.match(/../g) || []
22
+
23
+ return {
24
+ r: Number.parseInt(r, 16),
25
+ g: Number.parseInt(g, 16),
26
+ b: Number.parseInt(b, 16),
27
+ }
28
+ }
29
+
30
+ export const decimalColorToRGB = (decimal: number) => {
31
+ const div = (i: number, j: number) => Math.floor(i / j)
32
+
33
+ return {
34
+ r: div(decimal, 256 * 256),
35
+ g: div(decimal, 256) % 256,
36
+ b: decimal % 256,
37
+ }
38
+ }
39
+
40
+ export const formatColor = ({ r, g, b }: RGB, opacity?: number) => {
41
+ const color = [b, g, r]
42
+
43
+ if (opacity !== undefined) {
44
+ const alpha = Math.round((1 - opacity) * 255)
45
+ color.unshift(alpha)
46
+ }
47
+
48
+ return `&H${color.map(decimalToHex).join('').toUpperCase()}`
49
+ }
50
+
51
+ export const getDecoratingColor = (color: RGB) =>
52
+ isDarkColor(color) ? WHITE : BLACK
53
+
54
+ export const isWhite = (color: RGB) =>
55
+ color.r === 255 && color.g === 255 && color.b === 255
@@ -0,0 +1,36 @@
1
+ import type { Danmaku, RGB } from '../types'
2
+
3
+ import { UniPool } from '../..'
4
+ import { Modes } from '../../utils/dm-gen'
5
+ import { DanmakuType } from '../types'
6
+
7
+ function decimalToRGB888(decimal: number): RGB {
8
+ const r = (decimal >> 16) & 0xff
9
+ const g = (decimal >> 8) & 0xff
10
+ const b = decimal & 0xff
11
+ return {
12
+ r,
13
+ g,
14
+ b,
15
+ } satisfies RGB
16
+ }
17
+
18
+ export function UniPool2DanmakuLists(UP: UniPool): Danmaku[] {
19
+ const dans = UP.dans
20
+ let type = DanmakuType.SCROLL
21
+ return dans.map((d) => {
22
+ if (d.mode === Modes.Bottom) type = DanmakuType.BOTTOM
23
+ else if (d.mode === Modes.Top) type = DanmakuType.TOP
24
+ return {
25
+ time: d.progress,
26
+ type,
27
+ fontSizeType: d.fontsize,
28
+ content: d.content,
29
+ color: decimalToRGB888(d.color),
30
+ extra: d,
31
+ } satisfies Danmaku
32
+ })
33
+ }
34
+ export function DanmakuList2UniPool(d: Danmaku[]): UniPool {
35
+ return new UniPool(d.map((d) => d.extra))
36
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ decimalColorToRGB,
3
+ formatColor,
4
+ getDecoratingColor,
5
+ hexColorToRGB,
6
+ isWhite,
7
+ } from './color'
8
+ export { layoutDanmaku } from './layout'
9
+ export { arrayOfLength, assign, uniqueArray } from './lang'
10
+ export { DanmakuList2UniPool, UniPool2DanmakuLists } from './danconvert'
@@ -0,0 +1,35 @@
1
+ export const assign = <T extends object>(
2
+ source: T,
3
+ ...targets: Partial<T>[]
4
+ ): T => {
5
+ for (const target of targets) {
6
+ for (const key of Object.keys(target)) {
7
+ ;(source as any)[key] = target[key as keyof typeof target]
8
+ }
9
+ }
10
+
11
+ return source
12
+ }
13
+
14
+ export const arrayOfLength = <T>(length: number, defaultValue: T): T[] => {
15
+ // eslint-disable-next-line unicorn/no-new-array
16
+ const array = new Array(length)
17
+ for (let i = 0; i < length; i++) {
18
+ array[i] = defaultValue
19
+ }
20
+ return array
21
+ }
22
+
23
+ export const uniqueArray = <T>(array: T[]) => {
24
+ const duplicates: T[] = []
25
+ const result: T[] = []
26
+
27
+ for (const item of array) {
28
+ if (!duplicates.includes(item)) {
29
+ duplicates.push(item)
30
+ result.push(item)
31
+ }
32
+ }
33
+
34
+ return result
35
+ }