diff --git a/apps/overlay/README.md b/apps/overlay/README.md index 31e5deb..843634a 100644 --- a/apps/overlay/README.md +++ b/apps/overlay/README.md @@ -56,6 +56,13 @@ build 把同一个 build ID 注入浏览器 bundle 与 worker,发现更新后 metadata 的唯一文案来源。Vite 在构建时解析并注入它,Rust 则嵌入同一文件来验证允许保存的 locale。增加文案键时必须为每个 locale 提供值;增加语言时同时增加完整 `[locales."".messages]` 段。业务命令如“点歌”是 Bilibili 输入协议,不属于 UI 翻译。 +## 字体资源 + +Noto Serif SC、ZCOOL XiaoWei 与 LXGW +WenKai 由锁定的 Fontsource 依赖提供;漓雨手书则以固定版本的 WOFF2 文件保存在 +`public/fonts/`。浏览器从 Rust 静态服务同域加载这些字体,不依赖 OBS 设备的系统字体。每套字体的 OFL-1.1 许可证均输出到 +`/fonts/licenses/`。 + ## 格式化与构建 ```bash diff --git a/apps/overlay/package-lock.json b/apps/overlay/package-lock.json index f362bec..a665589 100644 --- a/apps/overlay/package-lock.json +++ b/apps/overlay/package-lock.json @@ -8,6 +8,9 @@ "name": "lxc-stream-overlay", "version": "0.1.0", "dependencies": { + "@fontsource/lxgw-wenkai": "5.3.0", + "@fontsource/noto-serif-sc": "5.3.0", + "@fontsource/zcool-xiaowei": "5.3.0", "react": "19.1.1", "react-dom": "19.1.1" }, @@ -745,6 +748,33 @@ "node": ">=18" } }, + "node_modules/@fontsource/lxgw-wenkai": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/lxgw-wenkai/-/lxgw-wenkai-5.3.0.tgz", + "integrity": "sha512-+U5AAOXeaB7o+3G0Qm7I7Djz2Jd1hqNY+kClOZH8bTunyyGa2WxY12LZftt2AqUsxlfxwdQpd75Js1HQIt4H4g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/noto-serif-sc": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/noto-serif-sc/-/noto-serif-sc-5.3.0.tgz", + "integrity": "sha512-0/zaEFkidiWldE62rTeD74x8ygUsQvejiSNtO0LQxQk3qpaHnlMZ3w4C7yH80B4KTIg8VKeFP4oSgwWMchY9+g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/zcool-xiaowei": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/zcool-xiaowei/-/zcool-xiaowei-5.3.0.tgz", + "integrity": "sha512-iRuUPl9ONFbYzi6JcEshpE4VedINyzQ4hv0Z5XEt0cB0pMPiQeqc2Qvv1NwJCNzkPwG02VCdcO+cF2K8ylYn2A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/apps/overlay/package.json b/apps/overlay/package.json index f29370a..e8982b5 100644 --- a/apps/overlay/package.json +++ b/apps/overlay/package.json @@ -3,13 +3,16 @@ "private": true, "version": "0.1.0", "scripts": { - "build": "tsc -b && vite build", + "build": "tsc -b && vite build && node scripts/check-font-assets.mjs", "dev": "vite", "docs:check": "node scripts/check-doc-links.mjs ../..", "format": "prettier --ignore-path ../../.prettierignore --write . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml", "format:check": "prettier --ignore-path ../../.prettierignore --check . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml" }, "dependencies": { + "@fontsource/lxgw-wenkai": "5.3.0", + "@fontsource/noto-serif-sc": "5.3.0", + "@fontsource/zcool-xiaowei": "5.3.0", "react": "19.1.1", "react-dom": "19.1.1" }, diff --git a/apps/overlay/public/fonts/NOTICE.md b/apps/overlay/public/fonts/NOTICE.md new file mode 100644 index 0000000..558e1f4 --- /dev/null +++ b/apps/overlay/public/fonts/NOTICE.md @@ -0,0 +1,19 @@ +# Bundled font notice + +## Liyu Shoushu + +`liyu-shoushu-v0.107.woff2` is a WOFF2 conversion of `LiyuShoushu.ttf` from the official +[Liyu Shoushu v0.107 release](https://github.com/InkPantherType/LiyuShoushu/releases/tag/v0.107). + +- Release tag commit: `219a0f42cb7b3f0d00be6e5bf9f8b5298a1f3060` +- Original TTF SHA-256: `16554439b7e95da1fc354ed7b2c1551e83e942c3c5afb2a2e883761c6037dc22` +- Bundled WOFF2 SHA-256: `b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3` +- Conversion: FontTools 4.63.0 WOFF2 compression with Brotli 1.2.0; no glyph subsetting +- Coverage retained: 18,828 glyphs and 18,221 Unicode mappings +- License: SIL Open Font License 1.1; see + [`licenses/liyu-shoushu-OFL.txt`](licenses/liyu-shoushu-OFL.txt) + +The original font credits Yuji Kataoka and the Yuji Project Authors for the base brush glyphs, LXGW +and the LXGW ZhenKai Project Authors for punctuation and symbols, and Yuchen Tian's +[zi2zi-JiT](https://github.com/kaonashi-tyc/zi2zi-JiT) for generated Chinese glyphs. This notice +preserves the zi2zi-JiT attribution required by the upstream font documentation. diff --git a/apps/overlay/public/fonts/licenses/liyu-shoushu-OFL.txt b/apps/overlay/public/fonts/licenses/liyu-shoushu-OFL.txt new file mode 100644 index 0000000..7abebb4 --- /dev/null +++ b/apps/overlay/public/fonts/licenses/liyu-shoushu-OFL.txt @@ -0,0 +1,94 @@ +Copyright 2026 Alexander Tseng. +Portions Copyright 2021 The Yuji Project Authors (https://github.com/Kinutafontfactory/Yuji). +Portions Copyright LXGW and LXGWZhenKai Project Authors (https://github.com/lxgw/LxgwZhenKai). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/overlay/public/fonts/liyu-shoushu-v0.107.woff2 b/apps/overlay/public/fonts/liyu-shoushu-v0.107.woff2 new file mode 100644 index 0000000..31b4e8e Binary files /dev/null and b/apps/overlay/public/fonts/liyu-shoushu-v0.107.woff2 differ diff --git a/apps/overlay/pwa/control-sw.js b/apps/overlay/pwa/control-sw.js index ad6bb5e..ebe35f5 100644 --- a/apps/overlay/pwa/control-sw.js +++ b/apps/overlay/pwa/control-sw.js @@ -66,6 +66,7 @@ function isControlShell(pathname) { function isStaticAsset(pathname) { return ( pathname.startsWith('/assets/') || + pathname.startsWith('/fonts/') || pathname.startsWith('/pwa/') || pathname === '/control/manifest.webmanifest' ) diff --git a/apps/overlay/scripts/check-font-assets.mjs b/apps/overlay/scripts/check-font-assets.mjs new file mode 100644 index 0000000..6be8d38 --- /dev/null +++ b/apps/overlay/scripts/check-font-assets.mjs @@ -0,0 +1,67 @@ +import { createHash } from 'node:crypto' +import { readdir, readFile } from 'node:fs/promises' + +const dist = new URL('../dist/', import.meta.url) + +async function filesBelow(directory, prefix = '') { + const entries = await readdir(directory, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + const relative = `${prefix}${entry.name}` + if (entry.isDirectory()) { + files.push(...(await filesBelow(new URL(`${entry.name}/`, directory), `${relative}/`))) + } else { + files.push(relative) + } + } + return files +} + +const files = await filesBelow(dist) +const expectedFonts = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai'] +const liyuFont = 'fonts/liyu-shoushu-v0.107.woff2' + +for (const font of expectedFonts) { + if (!files.some(file => file.startsWith(`assets/${font}-`) && file.endsWith('.woff2'))) { + throw new Error(`Missing bundled WOFF2 assets for ${font}`) + } + if (!files.includes(`fonts/licenses/${font}-OFL.txt`)) { + throw new Error(`Missing bundled license for ${font}`) + } +} + +if (!files.includes(liyuFont)) throw new Error('Missing bundled WOFF2 asset for liyu-shoushu') +if (!files.includes('fonts/licenses/liyu-shoushu-OFL.txt')) { + throw new Error('Missing bundled license for liyu-shoushu') +} +const liyuDigest = createHash('sha256') + .update(await readFile(new URL(liyuFont, dist))) + .digest('hex') +if (liyuDigest !== 'b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3') { + throw new Error(`Unexpected liyu-shoushu WOFF2 digest: ${liyuDigest}`) +} + +const legacyWoff = files.find(file => file.endsWith('.woff')) +if (legacyWoff) throw new Error(`Unexpected legacy WOFF asset: ${legacyWoff}`) + +const css = ( + await Promise.all( + files.filter(file => file.endsWith('.css')).map(file => readFile(new URL(file, dist), 'utf8')), + ) +).join('\n') + +for (const family of ['Noto Serif SC', 'ZCOOL XiaoWei', 'LXGW WenKai', 'Liyu Shoushu']) { + const declarations = [ + `font-family:${family}`, + `font-family:${JSON.stringify(family)}`, + `font-family:'${family}'`, + ] + if (!declarations.some(declaration => css.includes(declaration))) { + throw new Error(`Missing @font-face declaration for ${family}`) + } +} +if (!css.includes('font-display:block') || css.includes('font-display:swap')) { + throw new Error('Bundled fonts must block local fallback while loading') +} + +console.log('Verified same-origin WOFF2 component fonts and licenses.') diff --git a/apps/overlay/src/fonts.css b/apps/overlay/src/fonts.css new file mode 100644 index 0000000..118f476 --- /dev/null +++ b/apps/overlay/src/fonts.css @@ -0,0 +1,7 @@ +@font-face { + font-family: 'Liyu Shoushu'; + font-style: normal; + font-display: block; + font-weight: 400; + src: url('/fonts/liyu-shoushu-v0.107.woff2') format('woff2'); +} diff --git a/apps/overlay/src/main.tsx b/apps/overlay/src/main.tsx index 3e4f423..f61865b 100644 --- a/apps/overlay/src/main.tsx +++ b/apps/overlay/src/main.tsx @@ -25,6 +25,10 @@ import { SongRequestOverlay } from './songOverlay' import { useComponentStream } from './stream' import { I18nProvider, translate, useI18n } from './i18n' import type { Session } from './types' +import '@fontsource/noto-serif-sc/400.css' +import '@fontsource/zcool-xiaowei/400.css' +import '@fontsource/lxgw-wenkai/500.css' +import './fonts.css' import './style.css' import './control.css' import './giftEffect.css' diff --git a/apps/overlay/src/themeEdges.css b/apps/overlay/src/themeEdges.css index 9f33a52..3d7d5ce 100644 --- a/apps/overlay/src/themeEdges.css +++ b/apps/overlay/src/themeEdges.css @@ -105,6 +105,8 @@ } .gift-menu-overlay.gift-menu-theme-moonlit-water { + height: 100%; + align-items: center; padding-block: calc(clamp(8px, 1.3vmin, 16px) + var(--moonlit-edge-space)); } diff --git a/apps/overlay/src/typography.ts b/apps/overlay/src/typography.ts index 2ff8d8e..8b420d7 100644 --- a/apps/overlay/src/typography.ts +++ b/apps/overlay/src/typography.ts @@ -1,7 +1,7 @@ import type { CSSProperties } from 'react' -/** Stable IDs persisted by the backend; each maps to a fixed, injection-safe stack. */ -export const fontFamilyIds = ['song', 'fang-song', 'kai'] as const +/** Stable IDs persisted by the backend; each maps to a bundled, same-origin webfont. */ +export const fontFamilyIds = ['song', 'fang-song', 'kai', 'liyu-shoushu'] as const export type FontFamilyId = (typeof fontFamilyIds)[number] @@ -9,12 +9,14 @@ export const fontFamilies: ReadonlyArray<{ id: FontFamilyId; nameKey: string }> { id: 'song', nameKey: 'settings.font_family.song' }, { id: 'fang-song', nameKey: 'settings.font_family.fang_song' }, { id: 'kai', nameKey: 'settings.font_family.kai' }, + { id: 'liyu-shoushu', nameKey: 'settings.font_family.liyu_shoushu' }, ] const fontStacks: Record = { - song: '"Source Han Serif SC", "Noto Serif CJK SC", "Noto Serif SC", "Songti SC", STSong, SimSun, serif', - 'fang-song': 'FangSong, STFangsong, "FZShuSong-Z01", "Noto Serif SC", "Songti SC", serif', - kai: '"Kaiti SC", STKaiti, KaiTi, "LXGW WenKai", "Noto Serif SC", serif', + song: '"Noto Serif SC", serif', + 'fang-song': '"ZCOOL XiaoWei", serif', + kai: '"LXGW WenKai", serif', + 'liyu-shoushu': '"Liyu Shoushu", serif', } export function normalizeFontFamilyId(value: unknown): FontFamilyId { diff --git a/apps/overlay/vite.config.ts b/apps/overlay/vite.config.ts index b3c8dcd..6eb4d78 100644 --- a/apps/overlay/vite.config.ts +++ b/apps/overlay/vite.config.ts @@ -14,6 +14,13 @@ import { parse } from 'smol-toml' // The waiting worker is still activated explicitly by the user in the console. const pwaBuildId = Date.now().toString(36) const serviceWorkerSource = readFileSync(new URL('./pwa/control-sw.js', import.meta.url), 'utf8') +const bundledFontLicenses = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai'].map(font => ({ + fileName: `fonts/licenses/${font}-OFL.txt`, + source: readFileSync( + new URL(`./node_modules/@fontsource/${font}/LICENSE`, import.meta.url), + 'utf8', + ), +})) const i18nResource = parse( readFileSync(new URL('../../resources/i18n.toml', import.meta.url), 'utf8'), ) as unknown as { @@ -51,6 +58,16 @@ const manifest = { export default defineConfig({ plugins: [ react(), + { + name: 'same-origin-woff2-fonts', + enforce: 'pre', + transform(source, id) { + if (!id.includes('/node_modules/@fontsource/') || !id.endsWith('.css')) return + return source + .replaceAll('font-display: swap', 'font-display: block') + .replace(/,\s*url\([^)]*\.woff\) format\(['"]woff['"]\)/g, '') + }, + }, { name: 'control-pwa-assets', transformIndexHtml(html) { @@ -61,6 +78,9 @@ export default defineConfig({ .replaceAll('__APP_DESCRIPTION__', defaultMessages['pwa.manifest.description']) }, generateBundle() { + for (const license of bundledFontLicenses) { + this.emitFile({ type: 'asset', ...license }) + } this.emitFile({ type: 'asset', fileName: 'control/sw.js', diff --git a/apps/server-rust/Cargo.lock b/apps/server-rust/Cargo.lock index fa02cd2..599da81 100644 --- a/apps/server-rust/Cargo.lock +++ b/apps/server-rust/Cargo.lock @@ -956,6 +956,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libilibili" version = "0.1.0" +source = "git+http://gitea.home.arpa/felis/libilibili.git#3cb2a902d4d9387c5581a1827cc19248b6000ae2" dependencies = [ "base64", "brotli", diff --git a/apps/server-rust/Cargo.toml b/apps/server-rust/Cargo.toml index 4fc3169..5d43865 100644 --- a/apps/server-rust/Cargo.toml +++ b/apps/server-rust/Cargo.toml @@ -7,20 +7,30 @@ edition = "2024" axum = { version = "0.8", features = ["ws", "json"] } async-trait = "0.1" base64 = "0.22" -chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } +chrono = { version = "0.4", default-features = false, features = [ + "clock", + "serde", +] } chacha20poly1305 = "0.10" deadpool-postgres = "0.14" # Kept as a sibling checkout during development. Docker Compose supplies the # same directory as a named BuildKit context at `/libilibili`. -libilibili = { path = "../../../libilibili" } +libilibili = { git = "http://gitea.home.arpa/felis/libilibili.git" } rand = "0.9" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", +] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" subtle = "2" tokio = { version = "1", features = ["full"] } -tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1", "with-chrono-0_4"] } +tokio-postgres = { version = "0.7", features = [ + "with-serde_json-1", + "with-uuid-1", + "with-chrono-0_4", +] } tokio-util = "0.7" toml = "0.8" totp-rs = { version = "5.7", features = ["gen_secret", "qr", "zeroize"] } diff --git a/apps/server-rust/src/typography.rs b/apps/server-rust/src/typography.rs index 0f7c690..030bf78 100644 --- a/apps/server-rust/src/typography.rs +++ b/apps/server-rust/src/typography.rs @@ -1,8 +1,8 @@ //! Shared, validated typography settings for all built-in OBS components. //! -//! Font IDs map to fixed frontend font stacks rather than accepting arbitrary -//! CSS. This keeps settings portable between OBS hosts and prevents untrusted -//! component settings from becoming a CSS injection boundary. +//! Font IDs map to fixed, same-origin WOFF2 assets in the frontend rather than +//! accepting arbitrary CSS. This keeps settings portable between OBS hosts and +//! prevents untrusted component settings from becoming a CSS injection boundary. use serde::{Deserialize, Serialize}; @@ -13,6 +13,7 @@ pub enum FontFamilyId { #[default] FangSong, Kai, + LiyuShoushu, } pub const fn default_font_brightness() -> u16 { @@ -35,5 +36,9 @@ mod tests { ); assert_eq!(sanitize_font_brightness(1), 70); assert_eq!(sanitize_font_brightness(u16::MAX), 180); + assert_eq!( + serde_json::to_string(&FontFamilyId::LiyuShoushu).unwrap(), + "\"liyu-shoushu\"" + ); } } diff --git a/docs/components/danmaku-overlay.md b/docs/components/danmaku-overlay.md index 329bafe..4ecf843 100644 --- a/docs/components/danmaku-overlay.md +++ b/docs/components/danmaku-overlay.md @@ -12,28 +12,30 @@ ## 设置 -| 字段 | 范围/单位 | 行为 | -| ------------------------ | ----------- | -------------------------- | -| `themeId` | 主题 ID | 选择已安装的完整视觉主题 | -| `fontFamily` | 字体 ID | `song`、`fang-song`、`kai` | -| `fontBrightness` | 70–180% | 只调整文字亮度 | -| `viewerColor` | `#RRGGBB` | 可选的用户昵称颜色覆盖 | -| `danmakuColor` | `#RRGGBB` | 可选的弹幕正文颜色覆盖 | -| `fontScale` | 50–300% | 展开与收缩字号的统一比例 | -| `decorationLineWeight` | 50–300% | 边框、分隔线与古风边缘粗细 | -| `maxVisible` | 1–12 | 同时保留的消息卡数量 | -| `collapseAfterSeconds` | 2–120 秒 | 最新卡从展开态切换到紧凑态 | -| `unfoldDurationMs` | 200–5000 ms | 横向卷轴展开动画时间 | -| `motionIntensity` | 0–100% | 卡片、流光与焦点动画强度 | -| `particleCount` | 0–12 | 每卡星花粒子数量 | -| `particleSpeed` | 25–300% | 粒子动画速度 | -| `lowPerformanceMode` | boolean | 关闭高成本动态效果 | -| `highValueThreshold` | 千分之一元 | 高价值礼物起点 | -| `featuredValueThreshold` | 千分之一元 | 焦点礼物起点 | -| `show*` | boolean | 控制各事件类别订阅 | +| 字段 | 范围/单位 | 行为 | +| ------------------------ | ----------- | ------------------------------------------ | +| `themeId` | 主题 ID | 选择已安装的完整视觉主题 | +| `fontFamily` | 字体 ID | `song`、`fang-song`、`kai`、`liyu-shoushu` | +| `fontBrightness` | 70–180% | 只调整文字亮度 | +| `viewerColor` | `#RRGGBB` | 可选的用户昵称颜色覆盖 | +| `danmakuColor` | `#RRGGBB` | 可选的弹幕正文颜色覆盖 | +| `fontScale` | 50–300% | 展开与收缩字号的统一比例 | +| `decorationLineWeight` | 50–300% | 边框、分隔线与古风边缘粗细 | +| `maxVisible` | 1–12 | 同时保留的消息卡数量 | +| `collapseAfterSeconds` | 2–120 秒 | 最新卡从展开态切换到紧凑态 | +| `unfoldDurationMs` | 200–5000 ms | 横向卷轴展开动画时间 | +| `motionIntensity` | 0–100% | 卡片、流光与焦点动画强度 | +| `particleCount` | 0–12 | 每卡星花粒子数量 | +| `particleSpeed` | 25–300% | 粒子动画速度 | +| `lowPerformanceMode` | boolean | 关闭高成本动态效果 | +| `highValueThreshold` | 千分之一元 | 高价值礼物起点 | +| `featuredValueThreshold` | 千分之一元 | 焦点礼物起点 | +| `show*` | boolean | 控制各事件类别订阅 | 后端的 `OverlaySettings::sanitize` 是最终边界。控制台 range -input 只改善交互,不能取代服务端校验。字体 ID 只映射到前端预先注册的安全字体栈,不接受任意 CSS;OBS 设备缺少首选字体时会依次回退到可用衬线字体。颜色覆盖同样只接受六位十六进制颜色;留空时使用主题色,切换主题后会自动采用新主题的配色。 +input 只改善交互,不能取代服务端校验。字体 ID 只映射到前端预先注册并由服务同域提供的 WOFF2 字体,不接受任意 CSS,也不依赖 OBS 设备安装的字体;`song`、`fang-song`、`kai` +与 `liyu-shoushu` 分别使用 Noto Serif SC、ZCOOL XiaoWei、LXGW +WenKai 与漓雨手书。颜色覆盖同样只接受六位十六进制颜色;留空时使用主题色,切换主题后会自动采用新主题的配色。 ## 主题 diff --git a/proposal.md b/proposal.md new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n.toml b/resources/i18n.toml index 42a015f..8cf412d 100644 --- a/resources/i18n.toml +++ b/resources/i18n.toml @@ -142,10 +142,11 @@ name = "简体中文" "nav.logout" = "退出" "settings.theme" = "主题" "settings.font_family" = "中文字体" -"settings.font_family_description" = "按 OBS 所在设备已安装字体依次匹配;缺失时自动回退到衬线字体。" -"settings.font_family.song" = "宋体 · 端正" -"settings.font_family.fang_song" = "仿宋 · 清雅" -"settings.font_family.kai" = "楷体 · 书卷" +"settings.font_family_description" = "字体随服务提供并由 OBS 同域加载,不依赖设备已安装字体。" +"settings.font_family.song" = "思源宋体 · 端正" +"settings.font_family.fang_song" = "小薇体 · 清雅" +"settings.font_family.kai" = "霞鹜文楷 · 书卷" +"settings.font_family.liyu_shoushu" = "漓雨手书 · 毛笔手写" "settings.font_brightness" = "文字亮度" "settings.font_brightness_description" = "只提亮文字,不改变透明背景、礼物图片或直播画面。" "settings.viewer_color" = "用户昵称颜色" @@ -642,10 +643,11 @@ name = "English" "nav.logout" = "Log out" "settings.theme" = "Theme" "settings.font_family" = "Chinese typeface" -"settings.font_family_description" = "Uses the first installed font on the OBS device and falls back safely to a serif face." -"settings.font_family.song" = "Song · Formal" -"settings.font_family.fang_song" = "FangSong · Refined" -"settings.font_family.kai" = "Kai · Calligraphic" +"settings.font_family_description" = "Fonts are served by this service and loaded from the same origin instead of relying on fonts installed on the OBS device." +"settings.font_family.song" = "Noto Serif SC · Formal" +"settings.font_family.fang_song" = "ZCOOL XiaoWei · Refined" +"settings.font_family.kai" = "LXGW WenKai · Calligraphic" +"settings.font_family.liyu_shoushu" = "Liyu Shoushu · Brush handwriting" "settings.font_brightness" = "Text brightness" "settings.font_brightness_description" = "Brightens text only; transparency, gift artwork, and the underlying scene are unchanged." "settings.viewer_color" = "Viewer name color"