mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 23:02:27 +08:00
feat: SDK 抽取为 webfont-sdk npm 包 + leafer-x-webfont 插件(浏览器验证通过)
- packages/webfont-sdk:TS 重写,IncrementalEngine 核心(去重/并发池/失败字符记忆/retryFailed) - CSS 模式(WebFont)API 与旧 public/webfont-sdk.js 完全兼容 - FontFace 模式(WebFontCanvas)供 Canvas 场景,unicodeRange 注册 + onChunk 回调 - tsdown 双产物:dist/(ESM+dts)+ dist-iife/(script 直引) - scripts/sync-public.mjs 构建后同步到 public/webfont-sdk.js - 修复 banner 内嵌套块注释导致的语法错误(/* 重绘 */ 提前闭合 banner) - packages/leafer-x-webfont:LeaferJS 插件,依赖 webfont-sdk - 监听 property.change + layout.end 防抖扫描 Text 节点聚合字符 - 新旧 API 兼容(on_/on__、off_/off__) - 注册后自动改写 fontFamily 为合法 CSS 名(去 .ttf 后缀) - demo/index.html:海报场景(标题/副标题/字体切换/导出 PNG)本地 vendor 验证通过 - backend/app.ts:fs 适配层改为 top-level await import(修复 dev 模式 mkdir 崩溃) - doc/:商业化思路一/二评估 + 主动拓客文档
This commit is contained in:
parent
fe61bace16
commit
0693521716
6
.gitignore
vendored
6
.gitignore
vendored
@ -35,4 +35,8 @@ dist_backend_node
|
||||
|
||||
verify_font_baseline
|
||||
benchmark_results
|
||||
.claude
|
||||
.claude
|
||||
# packages 构建产物与 demo vendor(可重建/可下载,不入库)
|
||||
packages/*/dist
|
||||
packages/*/dist-iife
|
||||
packages/leafer-x-webfont/demo/vendor
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
/** 首先加载 fs 适配层,必须在所有其他导入之前!仅在 LLRT 运行时引入 */
|
||||
(() => {
|
||||
if (typeof __RUNTIME__ !== "undefined" && __RUNTIME__ === "llrt") {
|
||||
require("./server/llrt");
|
||||
}
|
||||
})();
|
||||
/** 首先加载 fs 适配层,必须在 main() 之前完成!按运行时选择实现(top-level await 保证注册先于一切使用) */
|
||||
if (typeof __RUNTIME__ !== "undefined" && __RUNTIME__ === "llrt") {
|
||||
await import("./server/llrt");
|
||||
} else {
|
||||
/** Node.js(tsx 开发 / tsdown define "node" 构建)走 node 适配器 */
|
||||
await import("./server/node");
|
||||
}
|
||||
|
||||
import { mimeTypes } from "./server/mime_type";
|
||||
import type { cMiddleware } from "./server/req_res";
|
||||
|
||||
79
packages/leafer-x-webfont/README.md
Normal file
79
packages/leafer-x-webfont/README.md
Normal file
@ -0,0 +1,79 @@
|
||||
# leafer-x-webfont
|
||||
|
||||
> LeaferJS 中文字体插件 —— 画布里的 Text 用什么字,就只加载那几个字(6 字 ≈ 6KB,而非 16MB)。
|
||||
|
||||
## 为什么需要它
|
||||
|
||||
Leafer 的 `Text` 元素渲染时直接拼 `canvas.font = fontFamily`,依赖浏览器字体系统。
|
||||
中文字体动辄 10MB+,`FontFace` 注册又慢又耗流量,海报/设计器场景根本没法用。
|
||||
|
||||
本插件订阅画布内 `Text` 的 `text` / `fontFamily` 变化,只对**实际用到的字符**调用
|
||||
[webfont](https://github.com/2234839/web-font) 子集化 API,注册 KB 级子集字体后自动重渲染画布:
|
||||
|
||||
```
|
||||
new Text({ text: '静心茶舍', fontFamily: '令东齐伋复刻体.ttf' })
|
||||
→ 服务端裁剪 → 返回 ~6KB 子集 → FontFace 注册 → 画布自动重绘
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install leafer-x-webfont
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```ts
|
||||
import { Leafer, Text } from 'leafer-ui'
|
||||
import { WebFontPlugin } from 'leafer-x-webfont'
|
||||
|
||||
const leafer = new Leafer({ view: window })
|
||||
|
||||
// 一行接入
|
||||
const webfont = new WebFontPlugin(leafer)
|
||||
|
||||
leafer.add(new Text({ text: '静心茶舍', fontFamily: '令东齐伋复刻体.ttf', fontSize: 64 }))
|
||||
// 字体到位后画布自动重渲染
|
||||
```
|
||||
|
||||
### 导出图片前
|
||||
|
||||
```ts
|
||||
await webfont.ready() // 等待所有已用字符的子集注册完成
|
||||
const blob = await leafer.export('png', { pixelRatio: 2 })
|
||||
```
|
||||
|
||||
### 配置项
|
||||
|
||||
```ts
|
||||
new WebFontPlugin(leafer, {
|
||||
baseUrl: 'https://webfont.shenzilong.cn', // 自部署时改这里
|
||||
outType: 'woff2',
|
||||
debounceMs: 120,
|
||||
watch: true, // 持续监听画布变化;静态海报导出可关掉
|
||||
debug: false,
|
||||
resolveFont: null, // 自定义 fontFamily 解析规则,返回 null 跳过
|
||||
})
|
||||
```
|
||||
|
||||
## 特性
|
||||
|
||||
- **零配置**:任意 `fontFamily`(含 `xxx.ttf` 文件名写法)自动识别,字体不存在时静默回退
|
||||
- **增量去重**:同一字体下字符集只增不减,改一个字只多发一个字符的子集请求
|
||||
- **失败记忆**:字体不含的字符自动记入失败集,不反复 404
|
||||
- **导出友好**:`webfont.ready()` 保证 `leafer.export()` 时字体已注册
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
# 仓库根目录
|
||||
pnpm install
|
||||
pnpm dev # 起前后端
|
||||
|
||||
# 打开 demo(插件源码直引,无需构建)
|
||||
open packages/leafer-x-webfont/demo/index.html
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
144
packages/leafer-x-webfont/demo/index.html
Normal file
144
packages/leafer-x-webfont/demo/index.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>leafer-x-webfont —— LeaferJS 海报中文字体 Demo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f5f5f4; color: #1c1917; }
|
||||
.wrap { max-width: 960px; margin: 0 auto; padding: 32px 20px 80px; }
|
||||
h1 { font-size: 24px; margin-bottom: 8px; }
|
||||
.sub { color: #78716c; font-size: 14px; margin-bottom: 24px; line-height: 1.7; }
|
||||
.sub a { color: #0d9488; }
|
||||
.panel { background: #fff; border: 1px solid #e7e5e4; border-radius: 12px; padding: 16px; margin-bottom: 16px; display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
||||
label { font-size: 13px; color: #57534e; }
|
||||
input[type="text"], select { padding: 8px 10px; border: 1px solid #d6d3d1; border-radius: 8px; font-size: 14px; outline: none; }
|
||||
input[type="text"] { width: 280px; }
|
||||
input[type="text"]:focus, select:focus { border-color: #0d9488; }
|
||||
button { padding: 8px 16px; background: #0d9488; color: #fff; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; }
|
||||
button:hover { background: #0f766e; }
|
||||
#canvas-host { width: 100%; height: 480px; background: #fff; border: 1px solid #e7e5e4; border-radius: 12px; overflow: hidden; }
|
||||
.status { font-size: 12px; color: #78716c; }
|
||||
.ok { color: #0d9488; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>leafer-x-webfont</h1>
|
||||
<p class="sub">
|
||||
LeaferJS 中文字体插件 —— 画布里的 Text 用什么字体,就只加载那几个字的子集(6 字 ≈ 6KB,而非 16MB)。
|
||||
服务端:<a href="https://webfont.shenzilong.cn" target="_blank">webfont.shenzilong.cn</a>
|
||||
/ <a href="https://github.com/2234839/web-font" target="_blank">GitHub</a>
|
||||
</p>
|
||||
|
||||
<div class="panel">
|
||||
<label>标题文字</label>
|
||||
<input id="title-input" type="text" value="静心茶舍" maxlength="12">
|
||||
<label>字体</label>
|
||||
<select id="font-select"></select>
|
||||
<button id="export-btn">导出 PNG</button>
|
||||
<span class="status" id="status">初始化…</span>
|
||||
</div>
|
||||
|
||||
<div id="canvas-host"></div>
|
||||
</div>
|
||||
|
||||
<script src="vendor/leafer-ui.min.js"></script>
|
||||
<!-- leafer 导出 PNG 需要额外插件(画布转 blob) -->
|
||||
<script src="vendor/leafer-in-export.min.js"></script>
|
||||
<!-- demo 引插件构建产物(dist ESM),webfont-sdk 由相对路径 importmap 解析到同级包的 dist -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"webfont-sdk": "../../webfont-sdk/dist/index.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import { WebFontPlugin } from '../dist/index.js'
|
||||
|
||||
const { Leafer, Text, Rect, Ellipse, Image } = LeaferUI
|
||||
const status = document.getElementById('status')
|
||||
|
||||
const FONTS = [
|
||||
{ name: '令东齐伋复刻体.ttf', label: '令东齐伋复刻体(古籍宋)' },
|
||||
{ name: '霞鹜文楷.ttf', label: '霞鹜文楷(楷书)' },
|
||||
{ name: '得意黑.ttf', label: '得意黑(标题黑体)' },
|
||||
{ name: '演示佛系体.ttf', label: '演示佛系体' },
|
||||
{ name: '源界明朝.ttf', label: '源界明朝(日式明朝)' },
|
||||
{ name: '三极泼墨体.ttf', label: '三极泼墨体' },
|
||||
]
|
||||
|
||||
const leafer = new Leafer({ view: 'canvas-host', fill: '#fafaf9' })
|
||||
const webfont = new WebFontPlugin(leafer, { debug: true, baseUrl: 'http://localhost:8087' })
|
||||
|
||||
/* ---------- 海报场景 ---------- */
|
||||
const bg = new Rect({ x: 0, y: 0, width: 900, height: 480, fill: { type: 'linear', from: { x: 0, y: 0 }, to: { x: 1, y: 1 }, stops: [{ offset: 0, color: '#fef3c7' }, { offset: 1, color: '#fde68a' }] } })
|
||||
const deco = new Ellipse({ x: 660, y: -80, width: 360, height: 360, fill: 'rgba(255,255,255,0.35)' })
|
||||
const deco2 = new Ellipse({ x: -100, y: 320, width: 300, height: 300, fill: 'rgba(255,255,255,0.25)' })
|
||||
|
||||
const title = new Text({
|
||||
x: 80, y: 150,
|
||||
text: '静心茶舍',
|
||||
fontFamily: '令东齐伋复刻体.ttf',
|
||||
fontSize: 96,
|
||||
fill: '#1c1917',
|
||||
letterSpacing: 8,
|
||||
})
|
||||
const subtitle = new Text({
|
||||
x: 84, y: 290,
|
||||
text: '以茶为媒 · 观自在',
|
||||
fontFamily: '霞鹜文楷.ttf',
|
||||
fontSize: 28,
|
||||
fill: '#57534e',
|
||||
letterSpacing: 4,
|
||||
})
|
||||
leafer.add(bg)
|
||||
leafer.add(deco)
|
||||
leafer.add(deco2)
|
||||
leafer.add(title)
|
||||
leafer.add(subtitle)
|
||||
|
||||
/* ---------- 控件交互 ---------- */
|
||||
const fontSelect = document.getElementById('font-select')
|
||||
for (const f of FONTS) {
|
||||
const opt = document.createElement('option')
|
||||
opt.value = f.name
|
||||
opt.textContent = f.label
|
||||
fontSelect.appendChild(opt)
|
||||
}
|
||||
fontSelect.onchange = () => {
|
||||
title.fontFamily = fontSelect.value
|
||||
status.textContent = '字体切换中…'
|
||||
}
|
||||
|
||||
document.getElementById('title-input').oninput = (e) => {
|
||||
title.text = e.target.value || ' '
|
||||
}
|
||||
|
||||
document.getElementById('export-btn').onclick = async () => {
|
||||
status.textContent = '等待字体就绪…'
|
||||
await webfont.ready()
|
||||
/** leafer.export 返回 { data: dataURL, ... },转 Blob 下载 */
|
||||
const { data } = await leafer.export('png', { pixelRatio: 2 })
|
||||
const blob = await (await fetch(data)).blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `海报-${title.text}.png`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
status.textContent = '已导出 ✓'
|
||||
setTimeout(() => status.textContent = '', 2000)
|
||||
}
|
||||
|
||||
/* ---------- 首屏字体就绪提示 ---------- */
|
||||
setTimeout(async () => {
|
||||
await webfont.ready()
|
||||
const count = webfont.families.length
|
||||
status.innerHTML = `<span class="ok">就绪</span> · 已加载 ${count} 种字体的子集(打开 DevTools Network 查看 API 请求体积)`
|
||||
}, 600)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
47
packages/leafer-x-webfont/package.json
Normal file
47
packages/leafer-x-webfont/package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "leafer-x-webfont",
|
||||
"version": "0.1.0",
|
||||
"description": "LeaferJS 字体插件 —— Text 元素自动按需加载字体子集(6 字 ≈ 6KB),让画布与导出图用上任意中文字体",
|
||||
"type": "module",
|
||||
"main": "./dist/leafer-x-webfont.esm.js",
|
||||
"module": "./dist/leafer-x-webfont.esm.js",
|
||||
"types": "./types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./dist/leafer-x-webfont.esm.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"types",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"leafer",
|
||||
"leaferjs",
|
||||
"webfont",
|
||||
"font-subset",
|
||||
"canvas-font",
|
||||
"chinese-font",
|
||||
"poster"
|
||||
],
|
||||
"repository": "https://github.com/2234839/web-font",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"leafer-ui": "^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"leafer-ui": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"webfont-sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
303
packages/leafer-x-webfont/src/index.ts
Normal file
303
packages/leafer-x-webfont/src/index.ts
Normal file
@ -0,0 +1,303 @@
|
||||
/**
|
||||
* leafer-x-webfont —— LeaferJS 字体按需加载插件
|
||||
*
|
||||
* 原理:Leafer 的 Text 元素渲染时直接拼 `canvas.font = fontFamily`,
|
||||
* 依赖浏览器字体系统。大字体(尤其中文字体 10MB+)无法直接注册。
|
||||
* 本插件订阅画布内 Text 的 text / fontFamily 变化,把「实际用到的字符」
|
||||
* 交给 webfont-sdk(FontFace 模式)增量加载:fetch 子集 buffer →
|
||||
* FontFace(unicodeRange) 注册 → forceRender 重绘。
|
||||
*
|
||||
* 去重 / 并发池 / 失败字符记忆 / provider 抽象全部在 webfont-sdk 引擎层实现,
|
||||
* 本插件只做 Leafer 桥接:
|
||||
* - 树扫描(walk)聚合同 family 字符
|
||||
* - property.change + layout.end 事件订阅(防抖)
|
||||
* - fontFamily 规范化改写('xx.ttf' → 'xx',canvas font 串要求合法标识符)
|
||||
* - 片段就绪后 forceRender
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* import { Leafer, Text } from 'leafer-ui'
|
||||
* import { WebFontPlugin } from 'leafer-x-webfont'
|
||||
*
|
||||
* const leafer = new Leafer({ view: window })
|
||||
* const webfont = new WebFontPlugin(leafer)
|
||||
* leafer.add(new Text({ text: '静心茶舍', fontFamily: '令东齐伋复刻体.ttf', fontSize: 64 }))
|
||||
* // 字体到位后画布自动重渲染;导出前 await webfont.ready()
|
||||
* ```
|
||||
*/
|
||||
import { WebFontFontFaceMode, type IFontFaceLoader } from 'webfont-sdk'
|
||||
|
||||
/** 插件配置 */
|
||||
export interface IWebFontPluginConfig {
|
||||
/** 子集化服务基地址,默认官方在线服务 */
|
||||
baseUrl?: string
|
||||
/**
|
||||
* fontFamily 属性的解析规则:
|
||||
* - 不传(默认):任意 fontFamily 值(如 '令东齐伋复刻体.ttf'、'霞鹜文楷')都尝试向 API 请求;
|
||||
* 失败字符自动记忆不重试(webfont-sdk 引擎层),不阻塞渲染
|
||||
* - 传入函数则完全自定义:返回 null 表示不处理该字体
|
||||
*/
|
||||
resolveFont?: (fontFamily: string) => string | null
|
||||
/** 请求子集时的输出格式 */
|
||||
outType?: 'woff2' | 'ttf'
|
||||
/** 文本变化防抖(ms),打字场景避免每敲一键发一次请求 */
|
||||
debounceMs?: number
|
||||
/**
|
||||
* 初始全量扫描后,是否持续监听画布变化(默认 true)。
|
||||
* 关闭后仅处理创建时已存在的文本,适合静态海报导出
|
||||
*/
|
||||
watch?: boolean
|
||||
/** 是否在控制台输出调试日志 */
|
||||
debug?: boolean
|
||||
/**
|
||||
* 是否自动把节点 fontFamily 改写为合法 CSS family 名(去掉 .ttf 等后缀)。
|
||||
* 默认 true:canvas font 解析不认带扩展名的 family,不改写会静默回退系统字体
|
||||
*/
|
||||
rewriteFamily?: boolean
|
||||
}
|
||||
|
||||
/** fontFamily 里的文件后缀(注册 FontFace / CSS 都不认) */
|
||||
const FONT_EXT_RE = /\.(ttf|otf|woff2?|ttc)$/i
|
||||
/** 泛型族名没有对应字体文件,跳过 */
|
||||
const GENERIC_FAMILY_RE = /^(sans-serif|serif|monospace|caption|system-ui|cursive|fantasy)$/i
|
||||
|
||||
/** Leafer 节点最小结构(避免硬依赖 leafer-ui 类型,保持 peerDep 可选) */
|
||||
interface ILeaferNode {
|
||||
__tag?: string
|
||||
text?: unknown
|
||||
fontFamily?: unknown
|
||||
children?: unknown
|
||||
destroyed?: boolean
|
||||
forceRender?: () => void
|
||||
/** on_ 为公开事件订阅(返回 id);旧版 leafer 用 on__(下划线为内部 id 绑定) */
|
||||
on_?: (type: string, listener: (e: unknown) => void, bind?: unknown) => number
|
||||
off_?: (ids: number[]) => void
|
||||
/** 旧版 API 兼容(on__ 在新版本已更名 on_) */
|
||||
on__?: (type: string, listener: (e: unknown) => void, bind?: unknown) => number
|
||||
off__?: (ids: number[]) => void
|
||||
waitViewReady?: (cb: () => void) => void
|
||||
}
|
||||
|
||||
export class WebFontPlugin {
|
||||
/** 宿主 Leafer 实例 */
|
||||
private leafer: ILeaferNode | null
|
||||
private config: Required<Pick<IWebFontPluginConfig, 'debounceMs' | 'watch' | 'debug' | 'rewriteFamily'>> &
|
||||
IWebFontPluginConfig
|
||||
|
||||
/** SDK FontFace 模式(增量引擎在这层:去重/并发/失败记忆) */
|
||||
private mode: WebFontFontFaceMode
|
||||
/** family -> 增量加载器(由 SDK 管理) */
|
||||
private loaders = new Map<string, IFontFaceLoader>()
|
||||
/** 防抖定时器 */
|
||||
private debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
/** 解绑事件用的 id 列表 */
|
||||
private eventIds: number[] = []
|
||||
/** 统一的事件解绑函数(on_/off_ 新旧版别名解析后的句柄) */
|
||||
private offEvents: ((ids: number[]) => void) | null = null
|
||||
|
||||
constructor(leafer: ILeaferNode, config: IWebFontPluginConfig = {}) {
|
||||
this.leafer = leafer
|
||||
this.config = {
|
||||
debounceMs: config.debounceMs ?? 120,
|
||||
watch: config.watch ?? true,
|
||||
debug: config.debug ?? false,
|
||||
rewriteFamily: config.rewriteFamily ?? true,
|
||||
baseUrl: config.baseUrl,
|
||||
outType: config.outType,
|
||||
resolveFont: config.resolveFont,
|
||||
}
|
||||
|
||||
this.mode = new WebFontFontFaceMode({
|
||||
baseUrl: config.baseUrl,
|
||||
provider: null,
|
||||
})
|
||||
|
||||
this.bindEvents()
|
||||
/** 画布初始化完成后做一次全量扫描 */
|
||||
leafer.waitViewReady?.(() => this.scan())
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 事件绑定 —— 监听 Text 属性变化与新增节点
|
||||
* ============================================================ */
|
||||
|
||||
private bindEvents(): void {
|
||||
if (!this.config.watch) return
|
||||
|
||||
/**
|
||||
* 事件订阅:新版 leafer-ui 是 on_/off_,旧版为 on__/off__。
|
||||
* 统一收敛到 on_/off_ 两个别名上,构造时一次性解析。
|
||||
*/
|
||||
const leafer = this.leafer!
|
||||
const on = leafer.on_ ?? leafer.on__
|
||||
const off = leafer.off_ ?? leafer.off__
|
||||
this.offEvents = off ? (ids) => off.call(leafer, ids) : null
|
||||
|
||||
/**
|
||||
* Leafer 的属性变化事件(PropertyEvent.CHANGE = 'property.change')由每个 Leaf
|
||||
* 直接 emit 到 leafer 根节点(见 leafer 源码 LeafDataProxy.emitPropertyEvent:
|
||||
* `leafer.emitEvent(event)`),在根上监听即可捕获所有子元素的 text / fontFamily
|
||||
* 变化。用字符串而非导入常量,保持对 leafer-ui 的 peerDep 可选。
|
||||
*/
|
||||
this.eventIds.push(
|
||||
on!.call(leafer, 'property.change', (e) => {
|
||||
const ev = e as { attrName?: string }
|
||||
if (ev.attrName === 'text' || ev.attrName === 'fontFamily') {
|
||||
this.schedule()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
/** 布局结束(新增/删除节点都会触发布局)——覆盖新增 Text、海报模板切换等场景 */
|
||||
this.eventIds.push(
|
||||
on!.call(leafer, 'layout.end', () => this.schedule()),
|
||||
)
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 扫描与调度
|
||||
* ============================================================ */
|
||||
|
||||
/** 全量扫描画布中所有 Text 的 text + fontFamily,按 family 聚合新字符 */
|
||||
private scan(): void {
|
||||
const groups = new Map<string, { loader: IFontFaceLoader; fontName: string; family: string; chars: Set<string> }>()
|
||||
|
||||
const walk = (node: ILeaferNode | null | undefined): void => {
|
||||
if (!node || node.destroyed) return
|
||||
const isText = node.__tag === 'Text' || (typeof node.text === 'string' && typeof node.fontFamily === 'string')
|
||||
if (isText) {
|
||||
const fontFamily: string = node.fontFamily as string
|
||||
const text: string = String(node.text ?? '')
|
||||
if (fontFamily && text) {
|
||||
const fontName = this.resolveFontName(fontFamily)
|
||||
if (fontName) {
|
||||
const family = normalizeFamily(fontName)
|
||||
const entry = getOrCreate(groups, family, () => ({ loader: this.getLoader(fontName, family), fontName, family, chars: new Set() }))
|
||||
/** 自动改写节点 fontFamily 为合法 CSS 名(canvas font 串要求) */
|
||||
if (this.config.rewriteFamily && node.fontFamily !== family) {
|
||||
;(node as { fontFamily: string }).fontFamily = family
|
||||
}
|
||||
for (const ch of text) entry.chars.add(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
const children = node.children
|
||||
if (Array.isArray(children)) {
|
||||
for (const child of children as ILeaferNode[]) walk(child)
|
||||
}
|
||||
}
|
||||
|
||||
walk(this.leafer)
|
||||
|
||||
for (const [, { loader, chars }] of groups) {
|
||||
loader.update(charsToString(chars))
|
||||
}
|
||||
}
|
||||
|
||||
/** 防抖触发扫描 */
|
||||
private schedule(): void {
|
||||
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this.debounceTimer = null
|
||||
this.scan()
|
||||
}, this.config.debounceMs)
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 字体解析与加载器管理
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* 解析 fontFamily:决定是否需要走子集化。
|
||||
* 用户自定义 resolveFont 优先;默认策略是「非泛型族名都尝试」。
|
||||
*/
|
||||
private resolveFontName(fontFamily: string): string | null {
|
||||
if (this.config.resolveFont) return this.config.resolveFont(fontFamily)
|
||||
if (GENERIC_FAMILY_RE.test(fontFamily.trim())) return null
|
||||
return fontFamily
|
||||
}
|
||||
|
||||
/** 获取(或创建)family 对应的 SDK 增量加载器;注册成功后重绘画布 */
|
||||
private getLoader(fontName: string, family: string): IFontFaceLoader {
|
||||
let loader = this.loaders.get(family)
|
||||
if (!loader) {
|
||||
loader = this.mode.loadFontFace(
|
||||
{ fontName, family },
|
||||
() => {
|
||||
/** 字体注册成功后强制重绘整个画布(文本 metrics 需要重新计算) */
|
||||
this.leafer?.forceRender?.()
|
||||
},
|
||||
)
|
||||
this.loaders.set(family, loader)
|
||||
this.log('new font loader:', fontName, '->', family)
|
||||
}
|
||||
return loader
|
||||
}
|
||||
|
||||
private log(...args: unknown[]): void {
|
||||
if (this.config.debug) console.log('[leafer-x-webfont]', ...args)
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 公开 API
|
||||
* ============================================================ */
|
||||
|
||||
/** 立即做一次全量扫描(外部手动改完画布内容后调用) */
|
||||
public refresh(): void {
|
||||
this.scan()
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待当前所有待加载字体就绪(用于导出图片前):
|
||||
* ```ts
|
||||
* await webfont.ready()
|
||||
* const blob = await leafer.export('png')
|
||||
* ```
|
||||
* 引擎层在「请求 + FontFace 注册」都完成后才算就绪,导出不会丢字体
|
||||
*/
|
||||
public async ready(): Promise<void> {
|
||||
await this.mode.ready()
|
||||
}
|
||||
|
||||
/** 已加载(或加载中)的字体 family 列表(调试 / 状态展示用) */
|
||||
public get families(): string[] {
|
||||
return [...this.loaders.keys()]
|
||||
}
|
||||
|
||||
/** 销毁插件(解绑事件、丢弃加载器;FontFace 保留在 document.fonts 供继续渲染) */
|
||||
public destroy(): void {
|
||||
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
||||
this.offEvents?.(this.eventIds)
|
||||
this.eventIds.length = 0
|
||||
this.offEvents = null
|
||||
for (const loader of this.loaders.values()) loader.dispose()
|
||||
this.loaders.clear()
|
||||
this.leafer = null
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 辅助函数
|
||||
* ============================================================ */
|
||||
|
||||
/** fontFamily 原始值 -> 合法 CSS family 名(去除文件后缀与首尾空白) */
|
||||
function normalizeFamily(fontFamily: string): string {
|
||||
return fontFamily.replace(FONT_EXT_RE, '').trim()
|
||||
}
|
||||
|
||||
/** Map 的 get-or-create 惯用封装 */
|
||||
function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V {
|
||||
let v = map.get(key)
|
||||
if (!v) {
|
||||
v = create()
|
||||
map.set(key, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/** 字符集合 -> 字符串(保持插入序,便于日志与请求参数稳定) */
|
||||
function charsToString(set: Set<string>): string {
|
||||
let s = ''
|
||||
for (const c of set) s += c
|
||||
return s
|
||||
}
|
||||
17
packages/leafer-x-webfont/tsconfig.json
Normal file
17
packages/leafer-x-webfont/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"types": [],
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "tsdown.config.ts"]
|
||||
}
|
||||
10
packages/leafer-x-webfont/tsdown.config.ts
Normal file
10
packages/leafer-x-webfont/tsdown.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: 'esm',
|
||||
dts: true,
|
||||
clean: true,
|
||||
outDir: 'dist',
|
||||
platform: 'browser',
|
||||
})
|
||||
39
packages/webfont-sdk/package.json
Normal file
39
packages/webfont-sdk/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "webfont-sdk",
|
||||
"version": "0.1.0",
|
||||
"description": "Web 字体按需加载 SDK —— 只加载实际用到的字符,增量去重、无闪烁,支持 DOM(CSS) 与 Canvas(FontFace) 两种模式",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsdown && node scripts/sync-public.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"webfont",
|
||||
"font-subset",
|
||||
"font-face",
|
||||
"incremental-font",
|
||||
"canvas-font",
|
||||
"chinese-font"
|
||||
],
|
||||
"repository": "https://github.com/2234839/web-font",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.22.13",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
}
|
||||
43
packages/webfont-sdk/scripts/sync-public.mjs
Normal file
43
packages/webfont-sdk/scripts/sync-public.mjs
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 构建后同步脚本:把 IIFE 产物带用法 banner 写入主站 public/webfont-sdk.js
|
||||
* index.html 引用 /webfont-sdk.js?v=%BUILD_TIME%,产物路径不变、零改动。
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
const source = resolve(pkgRoot, 'dist-iife/iife.iife.js')
|
||||
const target = resolve(pkgRoot, '../../public/webfont-sdk.js')
|
||||
|
||||
const banner = `/**
|
||||
* WebFont SDK — 按需增量加载字体片段,无闪烁(本文件由 packages/webfont-sdk 构建,勿手改)
|
||||
*
|
||||
* 架构:核心增量引擎 + 两种注册模式
|
||||
* - 核心:IncrementalEngine 按 fontKey 管理字符集,只请求增量;失败字符自动记忆不重试
|
||||
* - CSS 模式(WebFont):loadFont(轮询)/ observeFont(DOM 事件)/ loadText(手动传文本)
|
||||
* - FontFace 模式(WebFontCanvas):Canvas/canvas 场景,FontFace + unicodeRange 注册
|
||||
*
|
||||
* 用法:
|
||||
* // 轮询模式
|
||||
* WebFont.loadFont({ fontName, selector, family, interval });
|
||||
*
|
||||
* // 事件驱动模式
|
||||
* var obs = WebFont.observeFont({ fontName, selector, family });
|
||||
* obs.dispose();
|
||||
*
|
||||
* // 直接传文本模式
|
||||
* var loader = WebFont.loadText({ fontName, text: "你好世界", family });
|
||||
* loader.update("追加文字");
|
||||
* loader.dispose();
|
||||
*
|
||||
* // Canvas 模式(leafer / 原生 canvas)
|
||||
* var face = WebFontCanvas.loadFontFace({ fontName }, function (chunk) { 在此重绘 });
|
||||
* face.update("画布上的文字");
|
||||
* await WebFontCanvas.ready();
|
||||
*/
|
||||
|
||||
`
|
||||
|
||||
writeFileSync(target, banner + readFileSync(source, 'utf8'))
|
||||
console.log(`synced: ${target} (from ${source})`)
|
||||
327
packages/webfont-sdk/src/css-mode.ts
Normal file
327
packages/webfont-sdk/src/css-mode.ts
Normal file
@ -0,0 +1,327 @@
|
||||
/**
|
||||
* CSS 模式 —— 注入 @font-face + unicode-range 样式(DOM 场景)
|
||||
*
|
||||
* 与原 public/webfont-sdk.js 行为一致:
|
||||
* - 片段就绪时注入 <style>,按 unicode-range 精确生效
|
||||
* - 用 document.fonts.load 追踪完成以释放并发槽
|
||||
* - loadFont(轮询)/ observeFont(MutationObserver)/ loadText(手动)三种触发器
|
||||
*/
|
||||
import { IncrementalEngine, createHttpProvider, type SubsetProvider, type LoadedChunk } from './engine'
|
||||
|
||||
/** 通用选项 */
|
||||
export interface IWebFontOptions {
|
||||
/** 字体文件名(如 '令东齐伋复刻体.ttf'),支持模糊匹配 */
|
||||
fontName: string
|
||||
/** 服务基地址,默认当前 origin */
|
||||
baseUrl?: string
|
||||
/** CSS font-family 名,默认去掉扩展名的字体名 */
|
||||
family?: string
|
||||
/** 输出格式,默认 woff2 */
|
||||
outType?: 'woff2' | 'ttf'
|
||||
}
|
||||
|
||||
export interface ILoadFontOptions extends IWebFontOptions {
|
||||
/** DOM 选择器 */
|
||||
selector: string
|
||||
/** 轮询间隔 ms,默认 1000 */
|
||||
interval?: number
|
||||
}
|
||||
|
||||
export interface IObserveFontOptions extends IWebFontOptions {
|
||||
/** DOM 选择器 */
|
||||
selector: string
|
||||
/** 防抖 ms,默认 50 */
|
||||
debounceMs?: number
|
||||
}
|
||||
|
||||
export interface ILoadTextOptions extends IWebFontOptions {
|
||||
/** 初始文本 */
|
||||
text: string
|
||||
}
|
||||
|
||||
/** loadText 返回的手动加载器 */
|
||||
export interface ITextLoader {
|
||||
/** 追加文本(自动去重) */
|
||||
update(text: string): void
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/** observeFont 返回的观察任务 */
|
||||
export interface IObserveTask {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/** 跨域 baseUrl 注入 preconnect,首个片段延迟从 ~90ms 降到 ~30ms */
|
||||
const preconnectedOrigins = new Set<string>()
|
||||
|
||||
function ensurePreconnect(baseUrl: string): void {
|
||||
let origin: string
|
||||
try {
|
||||
origin = new URL(baseUrl, location.href).origin
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (origin === location.origin) return
|
||||
if (preconnectedOrigins.has(origin)) return
|
||||
preconnectedOrigins.add(origin)
|
||||
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'preconnect'
|
||||
link.crossOrigin = 'anonymous'
|
||||
link.href = origin
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
|
||||
export class WebFontCSSMode {
|
||||
private engine: IncrementalEngine
|
||||
/** 每个加载器注入的 <style> 元素(销毁时移除) */
|
||||
private injectedStyles = new Map<string, HTMLStyleElement[]>()
|
||||
private pollTasks = new Map<string, { timer: ReturnType<typeof setInterval> }>()
|
||||
private observeTasks = new Map<string, IObserveTask>()
|
||||
|
||||
constructor(config: { baseUrl?: string; maxConcurrent?: number; provider?: SubsetProvider | null } = {}) {
|
||||
this.engine = new IncrementalEngine({
|
||||
maxConcurrent: config.maxConcurrent ?? 4,
|
||||
provider: config.provider ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
/** 底层引擎(FontFace 模式共用场景) */
|
||||
getEngine(): IncrementalEngine {
|
||||
return this.engine
|
||||
}
|
||||
|
||||
/** 注入自定义子集提供者(离线裁剪等),null 恢复 HTTP */
|
||||
setSubsetProvider(provider: SubsetProvider | null): void {
|
||||
this.engine.setProvider(provider)
|
||||
}
|
||||
|
||||
setMaxConcurrent(n: number): void {
|
||||
this.engine.setMaxConcurrent(n)
|
||||
}
|
||||
|
||||
/* ---------- 内部:片段就绪回调(注入 CSS) ---------- */
|
||||
|
||||
private makeOnLoadChunk(key: string, family: string) {
|
||||
return (chunk: LoadedChunk): void => {
|
||||
const unicodeRanges = chunk.chars
|
||||
.map((c) => 'U+' + c.codePointAt(0)!.toString(16).padStart(4, '0'))
|
||||
.join(', ')
|
||||
const style = document.createElement('style')
|
||||
style.textContent =
|
||||
'@font-face {\n' +
|
||||
` font-family: "${family}";\n` +
|
||||
` src: url("${chunk.url}") format("${chunk.format}");\n` +
|
||||
' unicode-range: ' + unicodeRanges + ';\n' +
|
||||
'}\n'
|
||||
document.head.appendChild(style)
|
||||
const list = this.injectedStyles.get(key) ?? []
|
||||
list.push(style)
|
||||
this.injectedStyles.set(key, list)
|
||||
/** 注入后等待字体真正可用于渲染,让 document.fonts 状态机推进(无 API 时定时器兑底) */
|
||||
this.waitFontLoaded(family)
|
||||
}
|
||||
}
|
||||
|
||||
/** 用 document.fonts.load 触发加载与就绪状态推进(无 API 时定时器兑底) */
|
||||
private waitFontLoaded(family: string): void {
|
||||
if (document.fonts && document.fonts.load) {
|
||||
void document.fonts.load(`16px "${family}"`)
|
||||
} else {
|
||||
setTimeout(() => {}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
private resolveDefaults(options: IWebFontOptions): { baseUrl: string; family: string; key: string } {
|
||||
const baseUrl = options.baseUrl ?? location.origin
|
||||
const family = options.family ?? options.fontName.replace(/\.[^.]+$/, '')
|
||||
return { baseUrl, family, key: IncrementalEngine.fontKey(options.fontName, family) }
|
||||
}
|
||||
|
||||
/* ---------- 1. loadFont:轮询模式 ---------- */
|
||||
|
||||
loadFont(options: ILoadFontOptions): void {
|
||||
const { baseUrl, family, key } = this.resolveDefaults(options)
|
||||
ensurePreconnect(baseUrl)
|
||||
this.engine.ensureState(key, options.fontName, {
|
||||
baseUrl,
|
||||
outType: options.outType ?? 'woff2',
|
||||
onLoadChunk: this.makeOnLoadChunk(key, family),
|
||||
})
|
||||
|
||||
if (this.pollTasks.has(options.selector)) {
|
||||
clearInterval(this.pollTasks.get(options.selector)!.timer)
|
||||
}
|
||||
|
||||
let applied = false
|
||||
const tick = (): void => {
|
||||
const charSet = collectChars(options.selector)
|
||||
const had = this.engine.getState(key)
|
||||
this.engine.submitText(key, charsToString(charSet))
|
||||
if (had && !applied) {
|
||||
applied = true
|
||||
applyFamily(options.selector, family)
|
||||
}
|
||||
}
|
||||
tick()
|
||||
const timer = setInterval(tick, options.interval ?? 1000)
|
||||
this.pollTasks.set(options.selector, { timer })
|
||||
}
|
||||
|
||||
/* ---------- 2. observeFont:MutationObserver 模式 ---------- */
|
||||
|
||||
observeFont(options: IObserveFontOptions): IObserveTask {
|
||||
const { baseUrl, family, key } = this.resolveDefaults(options)
|
||||
ensurePreconnect(baseUrl)
|
||||
this.engine.ensureState(key, options.fontName, {
|
||||
baseUrl,
|
||||
outType: options.outType ?? 'woff2',
|
||||
onLoadChunk: this.makeOnLoadChunk(key, family),
|
||||
})
|
||||
|
||||
if (this.observeTasks.has(options.selector)) {
|
||||
this.observeTasks.get(options.selector)!.dispose()
|
||||
}
|
||||
|
||||
let applied = false
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const doLoad = (): void => {
|
||||
const had = this.engine.getState(key)
|
||||
this.engine.submitText(key, charsToString(collectChars(options.selector)))
|
||||
if (had && !applied) {
|
||||
applied = true
|
||||
applyFamily(options.selector, family)
|
||||
}
|
||||
}
|
||||
const debouncedLoad = (): void => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(doLoad, options.debounceMs ?? 50)
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.type === 'childList' || m.type === 'characterData') {
|
||||
debouncedLoad()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
const inputHandler = (): void => debouncedLoad()
|
||||
|
||||
const elements = document.querySelectorAll(options.selector)
|
||||
observer.observe(document.body ?? document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
})
|
||||
for (const el of elements) {
|
||||
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
||||
el.addEventListener('input', inputHandler)
|
||||
}
|
||||
}
|
||||
doLoad()
|
||||
|
||||
let disposed = false
|
||||
const task: IObserveTask = {
|
||||
dispose: (): void => {
|
||||
if (disposed) return
|
||||
disposed = true
|
||||
observer.disconnect()
|
||||
for (const el of elements) {
|
||||
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
||||
el.removeEventListener('input', inputHandler)
|
||||
}
|
||||
}
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
this.observeTasks.delete(options.selector)
|
||||
},
|
||||
}
|
||||
this.observeTasks.set(options.selector, task)
|
||||
return task
|
||||
}
|
||||
|
||||
/* ---------- 3. loadText:手动文本模式 ---------- */
|
||||
|
||||
loadText(options: ILoadTextOptions): ITextLoader {
|
||||
const { baseUrl, family, key } = this.resolveDefaults(options)
|
||||
ensurePreconnect(baseUrl)
|
||||
this.engine.ensureState(key, options.fontName, {
|
||||
baseUrl,
|
||||
outType: options.outType ?? 'woff2',
|
||||
onLoadChunk: this.makeOnLoadChunk(key, family),
|
||||
})
|
||||
this.engine.submitText(key, options.text)
|
||||
|
||||
let disposed = false
|
||||
return {
|
||||
update: (text: string): void => {
|
||||
if (disposed) return
|
||||
this.engine.submitText(key, text)
|
||||
},
|
||||
dispose: (): void => {
|
||||
if (disposed) return
|
||||
disposed = true
|
||||
/** 移除该 loader 注入的所有 @font-face 样式,避免同名 family 的 CSS 优先级冲突 */
|
||||
const styles = this.injectedStyles.get(key)
|
||||
if (styles) {
|
||||
for (const s of styles) s.remove()
|
||||
this.injectedStyles.delete(key)
|
||||
}
|
||||
this.engine.removeState(key)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理所有任务与注入样式(页面卸载时调用) */
|
||||
disposeAll(): void {
|
||||
for (const { timer } of this.pollTasks.values()) clearInterval(timer)
|
||||
for (const task of this.observeTasks.values()) task.dispose()
|
||||
this.pollTasks.clear()
|
||||
this.observeTasks.clear()
|
||||
for (const styles of this.injectedStyles.values()) {
|
||||
for (const s of styles) s.remove()
|
||||
}
|
||||
this.injectedStyles.clear()
|
||||
}
|
||||
|
||||
/** 供 IIFE 全局导出对齐旧 API 名 */
|
||||
static createHttpProvider = createHttpProvider
|
||||
}
|
||||
|
||||
/* ---------- DOM 辅助 ---------- */
|
||||
|
||||
/** 收集选择器匹配元素中的所有字符 */
|
||||
function collectChars(selector: string): Set<string> {
|
||||
const charSet = new Set<string>()
|
||||
const elements = document.querySelectorAll(selector)
|
||||
for (const el of elements) {
|
||||
const text = getText(el)
|
||||
for (const ch of text) charSet.add(ch)
|
||||
}
|
||||
return charSet
|
||||
}
|
||||
|
||||
function charsToString(set: Set<string>): string {
|
||||
let s = ''
|
||||
for (const c of set) s += c
|
||||
return s
|
||||
}
|
||||
|
||||
function getText(el: Element): string {
|
||||
const tag = el.tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
||||
const input = el as HTMLInputElement
|
||||
/** 同时收集 value 和 placeholder,确保占位文本的字体也被加载 */
|
||||
return (input.value ?? '') + (input.placeholder ?? '')
|
||||
}
|
||||
return el.textContent ?? ''
|
||||
}
|
||||
|
||||
/** 应用字体到元素 */
|
||||
function applyFamily(selector: string, family: string): void {
|
||||
const elements = document.querySelectorAll(selector)
|
||||
for (const el of elements) {
|
||||
;(el as HTMLElement).style.fontFamily = `"${family}", sans-serif`
|
||||
}
|
||||
}
|
||||
211
packages/webfont-sdk/src/engine.ts
Normal file
211
packages/webfont-sdk/src/engine.ts
Normal file
@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 核心增量引擎 —— 按 fontKey 管理字符集与子集请求
|
||||
*
|
||||
* 职责(与「注册方式」无关,被 CSS 模式 / FontFace 模式共用):
|
||||
* 1. 字符去重:同一字体下只对未加载过的字符发起子集请求
|
||||
* 2. 并发控制:全局并发池,任务在「provider 返回 + 注册完成」后才释放槽位,
|
||||
* 与旧版一致地把浏览器字体挂载也纳入限流
|
||||
* 3. 失败记忆:加载/注册失败的字符记入 failedChars,不再反复请求(可手动重试)
|
||||
* 4. provider 抽象:默认走 HTTP API,可注入离线裁剪 provider
|
||||
*
|
||||
* 不负责:把字体片段注册到运行环境(CSS @font-face / FontFace 由外层模式完成)
|
||||
*/
|
||||
|
||||
/** 子集提供者:给定字体名+文本,返回字体片段 URL 与格式(HTTP 或 blob:) */
|
||||
export type SubsetProvider = (fontName: string, text: string, outType: string) => Promise<{ url: string; format: string }>
|
||||
|
||||
/** 单个字体的增量状态 */
|
||||
export interface IFontState {
|
||||
/** 字体名(API 查询用,如 '令东齐伋复刻体.ttf'),服务端支持模糊匹配 */
|
||||
fontName: string
|
||||
/** 该字体的服务基地址(HTTP provider 用) */
|
||||
baseUrl: string
|
||||
/** 输出格式 */
|
||||
outType: string
|
||||
/** 已成功加载的字符集 */
|
||||
loadedChars: Set<string>
|
||||
/** 加载失败过的字符集(字体不含此字 / 网络错误),避免反复请求 */
|
||||
failedChars: Set<string>
|
||||
/** 正在请求中的字符集(防重复并发) */
|
||||
pendingChars: Set<string>
|
||||
/**
|
||||
* 该字体的注册回调:新片段就绪时由模式层把字体挂到运行环境。
|
||||
* 返回 Promise 时引擎会等它完成才释放并发槽(保证 ready() 语义)。
|
||||
*/
|
||||
onLoadChunk: ((chunk: LoadedChunk) => void | Promise<void>) | null
|
||||
}
|
||||
|
||||
/** 一次成功加载的增量片段 */
|
||||
export interface LoadedChunk {
|
||||
/** 字体名 */
|
||||
fontName: string
|
||||
/** 本片段包含的字符 */
|
||||
chars: string[]
|
||||
/** 字体文件 URL */
|
||||
url: string
|
||||
/** 字体格式(woff2 / truetype) */
|
||||
format: string
|
||||
}
|
||||
|
||||
/** 引擎配置 */
|
||||
export interface IEngineConfig {
|
||||
/** 全局最大并发子集请求(含注册)数,默认 4 */
|
||||
maxConcurrent: number
|
||||
/** 自定义子集提供者(离线裁剪场景),null 表示走默认 HTTP */
|
||||
provider: SubsetProvider | null
|
||||
}
|
||||
|
||||
/** 创建字体状态的初始选项 */
|
||||
export interface IEnsureStateOptions {
|
||||
/** 服务基地址(HTTP provider 用) */
|
||||
baseUrl: string
|
||||
/** 输出格式 */
|
||||
outType: string
|
||||
/** 注册回调(可后补) */
|
||||
onLoadChunk?: (chunk: LoadedChunk) => void | Promise<void>
|
||||
}
|
||||
|
||||
export function createHttpProvider(baseUrl: string): SubsetProvider {
|
||||
return (fontName, text, outType) => {
|
||||
const url = `${baseUrl}/api?font=${encodeURIComponent(fontName)}&text=${encodeURIComponent(text)}&outType=${outType}`
|
||||
return Promise.resolve({ url, format: outType === 'woff2' ? 'woff2' : 'truetype' })
|
||||
}
|
||||
}
|
||||
|
||||
export class IncrementalEngine {
|
||||
/** fontKey -> 字体状态 */
|
||||
private states = new Map<string, IFontState>()
|
||||
private config: IEngineConfig
|
||||
|
||||
/** 并发池 */
|
||||
private active = 0
|
||||
private queue: Array<() => void> = []
|
||||
/** 在途任务数(provider 请求 + 注册回调),hasPending / ready 用 */
|
||||
private flying = 0
|
||||
|
||||
constructor(config: Partial<IEngineConfig> = {}) {
|
||||
this.config = {
|
||||
maxConcurrent: config.maxConcurrent ?? 4,
|
||||
provider: config.provider ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/** fontKey:fontName + family 唯一确定一个增量组 */
|
||||
static fontKey(fontName: string, family: string): string {
|
||||
return fontName + '|' + family
|
||||
}
|
||||
|
||||
setProvider(provider: SubsetProvider | null): void {
|
||||
this.config.provider = provider
|
||||
}
|
||||
|
||||
getState(key: string): IFontState | undefined {
|
||||
return this.states.get(key)
|
||||
}
|
||||
|
||||
/** 获取或创建字体状态;已存在时按传入项更新 baseUrl / outType / 回调 */
|
||||
ensureState(key: string, fontName: string, options: IEnsureStateOptions): IFontState {
|
||||
let state = this.states.get(key)
|
||||
if (!state) {
|
||||
state = {
|
||||
fontName,
|
||||
baseUrl: options.baseUrl,
|
||||
outType: options.outType,
|
||||
loadedChars: new Set(),
|
||||
failedChars: new Set(),
|
||||
pendingChars: new Set(),
|
||||
onLoadChunk: options.onLoadChunk ?? null,
|
||||
}
|
||||
this.states.set(key, state)
|
||||
return state
|
||||
}
|
||||
state.baseUrl = options.baseUrl
|
||||
state.outType = options.outType
|
||||
if (options.onLoadChunk) state.onLoadChunk = options.onLoadChunk
|
||||
return state
|
||||
}
|
||||
|
||||
/** 删除状态(销毁时) */
|
||||
removeState(key: string): void {
|
||||
this.states.delete(key)
|
||||
}
|
||||
|
||||
/** 是否还有在途任务(请求中或注册中,ready() 轮询用) */
|
||||
hasPending(): boolean {
|
||||
if (this.flying > 0) return true
|
||||
for (const s of this.states.values()) {
|
||||
if (s.pendingChars.size > 0) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 清除失败记录(下次遇到这些字符会重新请求) */
|
||||
retryFailed(key: string): void {
|
||||
this.states.get(key)?.failedChars.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交一批文本:过滤出新字符并异步请求子集。
|
||||
* 乐观标记 pending,成功移入 loaded、失败移入 failed。
|
||||
*/
|
||||
submitText(key: string, text: string): void {
|
||||
const state = this.states.get(key)
|
||||
if (!state) return
|
||||
const newChars: string[] = []
|
||||
for (const ch of text) {
|
||||
if (state.loadedChars.has(ch) || state.pendingChars.has(ch) || state.failedChars.has(ch)) continue
|
||||
/** 跳过控制字符 */
|
||||
if (ch.charCodeAt(0) < 0x20) continue
|
||||
newChars.push(ch)
|
||||
state.pendingChars.add(ch)
|
||||
}
|
||||
if (newChars.length === 0) return
|
||||
this.enqueue(() => this.loadChunk(state, newChars))
|
||||
}
|
||||
|
||||
/** 执行一次子集请求 + 注册(在并发槽内完成) */
|
||||
private async loadChunk(state: IFontState, chars: string[]): Promise<void> {
|
||||
this.flying++
|
||||
try {
|
||||
const text = chars.join('')
|
||||
const provider = this.config.provider ?? createHttpProvider(state.baseUrl)
|
||||
const result = await provider(state.fontName, text, state.outType)
|
||||
/** 注册完成后才把字符记为已加载:注册失败可走 failedChars 重试路径 */
|
||||
await state.onLoadChunk?.({
|
||||
fontName: state.fontName,
|
||||
chars,
|
||||
url: result.url,
|
||||
format: result.format,
|
||||
})
|
||||
for (const ch of chars) {
|
||||
state.loadedChars.add(ch)
|
||||
state.pendingChars.delete(ch)
|
||||
}
|
||||
} catch {
|
||||
for (const ch of chars) {
|
||||
state.pendingChars.delete(ch)
|
||||
state.failedChars.add(ch)
|
||||
}
|
||||
} finally {
|
||||
this.flying--
|
||||
}
|
||||
}
|
||||
|
||||
/** 并发池:超出 maxConcurrent 的任务排队等待 */
|
||||
private enqueue(fn: () => Promise<void>): void {
|
||||
const run = (): void => {
|
||||
this.active++
|
||||
fn().finally(() => {
|
||||
this.active--
|
||||
const next = this.queue.shift()
|
||||
if (next) run()
|
||||
})
|
||||
}
|
||||
if (this.active < this.config.maxConcurrent) run()
|
||||
else this.queue.push(() => undefined)
|
||||
}
|
||||
|
||||
setMaxConcurrent(n: number): void {
|
||||
this.config.maxConcurrent = Math.max(1, n | 0)
|
||||
}
|
||||
}
|
||||
122
packages/webfont-sdk/src/fontface-mode.ts
Normal file
122
packages/webfont-sdk/src/fontface-mode.ts
Normal file
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* FontFace 模式 —— Canvas 场景(leafer / fabric / konva / 原生 canvas)
|
||||
*
|
||||
* 与 CSS 模式的差异:不注入 <style>,直接 fetch 字体 buffer 用 FontFace API 注册,
|
||||
* 且带 unicodeRange(多个片段同名注册时按字符精确生效,避免覆盖)。
|
||||
* 注册完成后回调 onReady,由调用方触发画布重绘。
|
||||
*/
|
||||
import { IncrementalEngine, createHttpProvider, type SubsetProvider, type LoadedChunk, type IFontState } from './engine'
|
||||
|
||||
export interface IFontFaceOptions {
|
||||
/** 字体文件名(如 '令东齐伋复刻体.ttf'),支持模糊匹配 */
|
||||
fontName: string
|
||||
/** 服务基地址 */
|
||||
baseUrl?: string
|
||||
/** 注册用的 family 名,默认去掉扩展名 */
|
||||
family?: string
|
||||
/** 输出格式,默认 woff2 */
|
||||
outType?: 'woff2' | 'ttf'
|
||||
}
|
||||
|
||||
/** 单个 FontFace 字体的增量加载器(leafer 插件等持有) */
|
||||
export interface IFontFaceLoader {
|
||||
/** 提交文本(自动去重,只请求新字符) */
|
||||
update(text: string): void
|
||||
/** 该字体是否有片段在请求中 */
|
||||
isPending(): boolean
|
||||
/** 清除失败记录(重试场景) */
|
||||
retryFailed(): void
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
export class WebFontFontFaceMode {
|
||||
private engine: IncrementalEngine
|
||||
/** 未显式传 baseUrl 时的默认服务地址 */
|
||||
private defaultBaseUrl = 'https://webfont.shenzilong.cn'
|
||||
/** family -> 已注册的 FontFace(dispose 时从 document.fonts 删除) */
|
||||
private faces = new Map<string, FontFace[]>()
|
||||
|
||||
constructor(config: { baseUrl?: string; maxConcurrent?: number; provider?: SubsetProvider | null } = {}) {
|
||||
this.engine = new IncrementalEngine({
|
||||
maxConcurrent: config.maxConcurrent ?? 4,
|
||||
provider: config.provider ?? null,
|
||||
})
|
||||
if (config.baseUrl) this.defaultBaseUrl = config.baseUrl
|
||||
}
|
||||
|
||||
getEngine(): IncrementalEngine {
|
||||
return this.engine
|
||||
}
|
||||
|
||||
setSubsetProvider(provider: SubsetProvider | null): void {
|
||||
this.engine.setProvider(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建(或复用)一个字体的 FontFace 增量加载器
|
||||
*
|
||||
* @param options 字体选项
|
||||
* @param onChunk 单个片段注册完成后回调(调用方在此触发画布重绘)
|
||||
*/
|
||||
loadFontFace(options: IFontFaceOptions, onChunk?: (chunk: LoadedChunk) => void): IFontFaceLoader {
|
||||
const fontName = options.fontName
|
||||
const family = options.family ?? fontName.replace(/\.(ttf|otf|woff2?|ttc)$/i, '').trim()
|
||||
const key = IncrementalEngine.fontKey(fontName, family)
|
||||
const baseUrl = options.baseUrl ?? this.defaultBaseUrl
|
||||
|
||||
const handleChunk = async (chunk: LoadedChunk): Promise<void> => {
|
||||
const unicodeRanges = chunk.chars
|
||||
.map((c) => 'U+' + c.codePointAt(0)!.toString(16).padStart(4, '0'))
|
||||
.join(', ')
|
||||
const res = await fetch(chunk.url)
|
||||
const buffer = await res.arrayBuffer()
|
||||
const face = new FontFace(family, buffer, { unicodeRange: unicodeRanges })
|
||||
await face.load()
|
||||
document.fonts.add(face)
|
||||
const list = this.faces.get(family) ?? []
|
||||
list.push(face)
|
||||
this.faces.set(family, list)
|
||||
onChunk?.(chunk)
|
||||
}
|
||||
|
||||
this.engine.ensureState(key, fontName, {
|
||||
baseUrl,
|
||||
outType: options.outType ?? 'woff2',
|
||||
onLoadChunk: (chunk) => handleChunk(chunk),
|
||||
})
|
||||
|
||||
let disposed = false
|
||||
return {
|
||||
update: (text: string): void => {
|
||||
if (disposed) return
|
||||
this.engine.submitText(key, text)
|
||||
},
|
||||
isPending: (): boolean => {
|
||||
const s = this.engine.getState(key)
|
||||
return !!s && s.pendingChars.size > 0
|
||||
},
|
||||
retryFailed: (): void => this.engine.retryFailed(key),
|
||||
dispose: (): void => {
|
||||
if (disposed) return
|
||||
disposed = true
|
||||
this.engine.removeState(key)
|
||||
/** FontFace 不主动删除:其他画布可能还在用同 family(保守策略) */
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否有片段在请求/注册中(导出图片前轮询用) */
|
||||
hasPending(): boolean {
|
||||
return this.engine.hasPending()
|
||||
}
|
||||
|
||||
/** 等待所有 pending 片段就绪(导出图片前调用) */
|
||||
async ready(): Promise<void> {
|
||||
while (this.hasPending()) {
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { createHttpProvider, IncrementalEngine }
|
||||
export type { SubsetProvider, LoadedChunk, IFontState }
|
||||
19
packages/webfont-sdk/src/iife.ts
Normal file
19
packages/webfont-sdk/src/iife.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* IIFE 入口 —— 构建为 public/webfont-sdk.js(script 标签直接引入)
|
||||
*
|
||||
* 全局暴露 WebFont,API 与历史版本完全兼容:
|
||||
* WebFont.loadFont / observeFont / loadText / disposeAll /
|
||||
* setMaxConcurrent / setSubsetProvider
|
||||
* 另暴露 WebFont.canvas(FontFace 模式,高级场景可用)。
|
||||
*/
|
||||
import { WebFont, WebFontCanvas } from './index'
|
||||
|
||||
const g = globalThis as unknown as {
|
||||
WebFont: typeof WebFont
|
||||
WebFontCanvas: typeof WebFontCanvas
|
||||
/** WebFont 上也挂一份 canvas 引用(WebFont.canvas.loadFontFace),方便 Canvas 场景一处取用 */
|
||||
}
|
||||
g.WebFont = WebFont
|
||||
g.WebFontCanvas = WebFontCanvas
|
||||
/** 补充别名:与文档注释一致,WebFont.canvas 即 FontFace 模式实例 */
|
||||
;(WebFont as unknown as { canvas: typeof WebFontCanvas }).canvas = WebFontCanvas
|
||||
29
packages/webfont-sdk/src/index.ts
Normal file
29
packages/webfont-sdk/src/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* webfont-sdk —— Web 字体按需加载 SDK(不限中文:任何大字体都能按字符增量加载)
|
||||
*
|
||||
* 单包两种模式,共用同一增量引擎(去重 / 并发池 / 失败记忆 / provider 抽象):
|
||||
* - `WebFont`(CSS 模式):DOM 场景,注入 @font-face + unicode-range,API 与
|
||||
* 原线上 webfont-sdk.js 完全兼容(loadFont / observeFont / loadText / disposeAll /
|
||||
* setMaxConcurrent / setSubsetProvider)
|
||||
* - `WebFontCanvas`(FontFace 模式):Canvas 场景(leafer / fabric / 原生 canvas),
|
||||
* fetch buffer + FontFace(unicodeRange) 注册,onChunk 回调触发画布重绘
|
||||
*/
|
||||
import { WebFontCSSMode } from './css-mode'
|
||||
import { WebFontFontFaceMode } from './fontface-mode'
|
||||
|
||||
export { WebFontCSSMode, WebFontFontFaceMode }
|
||||
export { IncrementalEngine, createHttpProvider } from './engine'
|
||||
export type { SubsetProvider, LoadedChunk, IFontState, IEngineConfig } from './engine'
|
||||
export type {
|
||||
IWebFontOptions, ILoadFontOptions, IObserveFontOptions, ILoadTextOptions,
|
||||
ITextLoader, IObserveTask,
|
||||
} from './css-mode'
|
||||
export type { IFontFaceOptions, IFontFaceLoader } from './fontface-mode'
|
||||
|
||||
/**
|
||||
* CSS 模式默认实例 —— 与旧 webfont-sdk.js 的全局 WebFont 对象 API 兼容
|
||||
*/
|
||||
export const WebFont = new WebFontCSSMode()
|
||||
|
||||
/** FontFace 模式默认实例(Canvas 场景) */
|
||||
export const WebFontCanvas = new WebFontFontFaceMode()
|
||||
17
packages/webfont-sdk/tsconfig.json
Normal file
17
packages/webfont-sdk/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"types": [],
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "tsdown.config.ts"]
|
||||
}
|
||||
23
packages/webfont-sdk/tsdown.config.ts
Normal file
23
packages/webfont-sdk/tsdown.config.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default [
|
||||
/** ESM + d.ts —— npm 包主产物(leafer 插件 / bundler 用户) */
|
||||
defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: 'esm',
|
||||
dts: true,
|
||||
clean: true,
|
||||
outDir: 'dist',
|
||||
platform: 'browser',
|
||||
}),
|
||||
/** IIFE —— 构建为 public/webfont-sdk.js(script 标签直引,全局 WebFont) */
|
||||
defineConfig({
|
||||
entry: ['src/iife.ts'],
|
||||
format: 'iife',
|
||||
globalName: 'WebFontBundle',
|
||||
clean: false,
|
||||
outDir: 'dist-iife',
|
||||
platform: 'browser',
|
||||
minify: false,
|
||||
}),
|
||||
]
|
||||
468
pnpm-lock.yaml
generated
468
pnpm-lock.yaml
generated
@ -67,6 +67,24 @@ importers:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(@vue/compiler-sfc@3.5.40)(esbuild@0.27.7)(rolldown@1.2.0)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.7)(terser@5.49.0)(yaml@2.9.0))(vue@3.6.0-beta.17(typescript@7.0.2))
|
||||
|
||||
packages/leafer-x-webfont:
|
||||
dependencies:
|
||||
leafer-ui:
|
||||
specifier: ^2.0.0
|
||||
version: 2.2.9
|
||||
webfont-sdk:
|
||||
specifier: workspace:*
|
||||
version: link:../webfont-sdk
|
||||
|
||||
packages/webfont-sdk:
|
||||
devDependencies:
|
||||
tsdown:
|
||||
specifier: ^0.22.13
|
||||
version: 0.22.13(typescript@7.0.2)(unrun@0.2.37)
|
||||
typescript:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
|
||||
packages:
|
||||
|
||||
'@acemir/cssom@0.9.31':
|
||||
@ -376,6 +394,153 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@leafer-in/interface@2.2.9':
|
||||
resolution: {integrity: sha512-6T0Gf1EDKxS4a8sZCT8Q2PZ0a5JhHWswyWfsSXNHrvdJvapHcUE4YordpzXFQeki0KcCiAy2HIrOZ7mvFJ9spQ==}
|
||||
peerDependencies:
|
||||
'@leafer-ui/interface': ^2.2.9
|
||||
'@leafer/interface': ^2.2.9
|
||||
|
||||
'@leafer-ui/app@2.2.9':
|
||||
resolution: {integrity: sha512-SIxCEIx09LQBSSuj7qPGlpY7kIZfRvn6gGaa2dqAzeB4unn05eBFdU8rcM+R1pM8k0VEDGQZpDOyGqe02Kn9CQ==}
|
||||
|
||||
'@leafer-ui/bounds@2.2.9':
|
||||
resolution: {integrity: sha512-BjZxWPQcPf/SQ+L6Edk7PNyiy3LXbsbQZp6kuC1uN0fgOiQtyib2i0M+zKa1+WTrP7aNFQWkZwSk6+QS6m0vmQ==}
|
||||
|
||||
'@leafer-ui/color@2.2.9':
|
||||
resolution: {integrity: sha512-xBvAmKgRR6YRlkRUG6i5Qmav0WUy/EuydzLv6GM1VkATSkU9W42KDcPp9mkg3P7w2Bwc+sJGj4q6LzsuGr5QtQ==}
|
||||
|
||||
'@leafer-ui/core@2.2.9':
|
||||
resolution: {integrity: sha512-N1qwviYnISzWf9vjLClRFqOkGQBqDTdwiURbNpwmQJqMfP0LMfn4+9yQ6cixoVVg+jRVt0K6izsGbzzNAEiLnw==}
|
||||
|
||||
'@leafer-ui/data@2.2.9':
|
||||
resolution: {integrity: sha512-omNVf2e+Gt6baKqXsHlch0PVQwIyowjMSHlyPyNIsL74PkAceYfsrRDBVztsX/EzDVvF22ecdb2w/VmAhu1nyw==}
|
||||
|
||||
'@leafer-ui/decorator@2.2.9':
|
||||
resolution: {integrity: sha512-98v7ib76MbilkP+QLd6G8IKs7eHnn2LOAEd08kjOqkwTfnZ0Hj3s8AKqYWEAr1rfwp6IjBEM9Rmp2Gr1vR+AGA==}
|
||||
|
||||
'@leafer-ui/display-module@2.2.9':
|
||||
resolution: {integrity: sha512-ZpsuJ5L0QT/d2ZuTxsSj0dOGE4n1B0xvm9qJXQ5ThuWlQ0Opm8ySIPQrvIDas35pwyEjBcru6IF6a9KdwC8J7Q==}
|
||||
|
||||
'@leafer-ui/display@2.2.9':
|
||||
resolution: {integrity: sha512-3sM8N3j6E+EZcrGXuGhilTCpH2pYs5Med5hZs+xfxWstw3V4TOJdTHnqCI8weCJAQ9o7QLH6I7+akSs7urz35A==}
|
||||
|
||||
'@leafer-ui/draw@2.2.9':
|
||||
resolution: {integrity: sha512-76FF8teAGnTk8Y+irWOuHrfsR0hJSBMu87a6UlfNU3eda8ZnkKUg0c6lRzO2aUD9gatVgmOh9XaHY3VP8iRTFQ==}
|
||||
|
||||
'@leafer-ui/effect@2.2.9':
|
||||
resolution: {integrity: sha512-k/9tBYWy8VLkDufkvohKATTX0joSlh4v0zi/I/jZ+XP4AVYbIDViYkU+Nt5bpeQnXi2LBGT1t/ipDsqLLlUvPA==}
|
||||
|
||||
'@leafer-ui/event@2.2.9':
|
||||
resolution: {integrity: sha512-WbX3DMmo3JrGHLVWigNz7NPQwduSM5PFLakYlIpy8QbnGYMcRRml/1vlPQUvnYDXi4rRMOlO82EbMvnt736qsg==}
|
||||
|
||||
'@leafer-ui/external@2.2.9':
|
||||
resolution: {integrity: sha512-Vp6VGhKTo07y0M1BGlfOg3CxHj1Svd9ad5mGj0+J8h4eqQQarT5GhH7uaF46WtD4di5JVbwruKj62SFq6uzPeQ==}
|
||||
|
||||
'@leafer-ui/hit@2.2.9':
|
||||
resolution: {integrity: sha512-Qmmz1gCzWOGrGvkt4v6kfnddqp2o9FEiokuj4/zgUoo8Q+0C0D1Y44wzrSGbgC6aQBxa6B8XJ44HLIWf09FVSg==}
|
||||
|
||||
'@leafer-ui/interaction-web@2.2.9':
|
||||
resolution: {integrity: sha512-QLBk68qpXbdBrwtTERmc+Mb4z7ZOXKZf1KCj+OANlO2dsajnxRvc6Sf2RvwT+kzq5ic9Czk0N+UrGudPG1HR7Q==}
|
||||
|
||||
'@leafer-ui/interaction@2.2.9':
|
||||
resolution: {integrity: sha512-MPtScWDfD0rEiXaJ5qdIfJPNupOT+g3ieQywG1Lu6RVfmf+Vwn3RaxKvGk/R94rEJKSL9FQSWoQLuSXGBh/6Og==}
|
||||
|
||||
'@leafer-ui/interface@2.2.9':
|
||||
resolution: {integrity: sha512-AWnRhnQOaMk8kNOJwzymuZpNb4b28CsMUxvr4pVjGwmlXWG1qINJrlWYWrXbV3R44+zhK0VIJ8T7szsoO2qj7Q==}
|
||||
|
||||
'@leafer-ui/paint@2.2.9':
|
||||
resolution: {integrity: sha512-/scOodHPSs3hJo73Scvk556ntXn9dJI8/7GlaWRMbprruvB+ujc/vyWUc5DInOqikxxAQb9eEfnKX3GoFUGiSA==}
|
||||
|
||||
'@leafer-ui/partner@2.2.9':
|
||||
resolution: {integrity: sha512-Z88858CvaHmg0w0oyESHaOCeHyPUScjNSi6Z8sadKU6QV3gYrNxZ85eWfv2mtf+h1D6TyEe3SQCTQc6xdnV27A==}
|
||||
|
||||
'@leafer-ui/render@2.2.9':
|
||||
resolution: {integrity: sha512-hi15h0nGHQpmSVxBY1ReLUD9vbHOnZYtRJsvaBOlZZEHwvsvRWWTVlwGrwMpGpbDWdy5FcwHmPp4GezaPa18QQ==}
|
||||
|
||||
'@leafer-ui/text@2.2.9':
|
||||
resolution: {integrity: sha512-pKUUhxnkrdM+5ul6xI7nM9yr9jwj9AuIu18oVKIba3/gekrZRWBanpCeCwMR1IaQa5X97MI3bd/eVIZ892FX5g==}
|
||||
|
||||
'@leafer-ui/web@2.2.9':
|
||||
resolution: {integrity: sha512-0QgYEK3wuUFWHz0BM6HPMboh0uiRR5fRLqrPEopO+ofJLygWi0v6FIjU3OE4pOsHRdqrbBIuhOioRmIasBBgmg==}
|
||||
|
||||
'@leafer/canvas-web@2.2.9':
|
||||
resolution: {integrity: sha512-kj58yyHdqXq7VnZm18aYzXBTX8HrGooPOunBsQwbEnUn8bVe4JtkNaUgAWIjaDIJT0TuMwQ5kH/9E+6hpwHEPA==}
|
||||
|
||||
'@leafer/canvas@2.2.9':
|
||||
resolution: {integrity: sha512-bWSqq2ty/khi/qDVK7UCGIZOcmsEycAYPCZkXZFWOxXXCmRxI86CA5SXBJa5UFLeAXKT9I30E+UiDc3+jAONNg==}
|
||||
|
||||
'@leafer/core@2.2.9':
|
||||
resolution: {integrity: sha512-zXQv21ydpeELHBRj8fF770AQj3Ma0Dz81QfX5ERjZUQ2C1LcQCXSWHbc0fQwKeKy+PS+4FZDn8VRZSlM/FC2Ww==}
|
||||
|
||||
'@leafer/data@2.2.9':
|
||||
resolution: {integrity: sha512-BcCRDYH2+jYqdWv8saQXZZ4X80GlBI6H51bTR1FqdRHXTuGCaF3QMWFhyTyb7/+CTtvMU6Isk0qUaEXaSLEZLw==}
|
||||
|
||||
'@leafer/debug@2.2.9':
|
||||
resolution: {integrity: sha512-YQGZcCCDZ888JvidE/HpN9eqw4KxV5oVKQVTSkWAaWcARt9IX8RU4QTyF+JC4P8OXwstOZP0Y6a0IqMzAijUig==}
|
||||
|
||||
'@leafer/decorator@2.2.9':
|
||||
resolution: {integrity: sha512-reA3NafAm0PSYkSG44QuX8sYXOf/4hdlR3IfhpHwovNf+g3M/R1ibQ+2kROhRe28APUvpaN5N/0tzj5zeOFMqA==}
|
||||
|
||||
'@leafer/display-module@2.2.9':
|
||||
resolution: {integrity: sha512-aBL/ciSFgYWKRAd1jmPq3qkxW5vn8Njp61li4pO4jwy6pQvbznT0ENX145cp+nIzfp8lxbYfiY79E2SFB7sLTA==}
|
||||
|
||||
'@leafer/display@2.2.9':
|
||||
resolution: {integrity: sha512-FiZ0Vq9G3achrldVHDuiVa9Phr0AWhyWRJhfXuDN829CVbrS7liJYysJ+Y//SGdq3sI7igyKHJbsT1V6OAk2EQ==}
|
||||
|
||||
'@leafer/event@2.2.9':
|
||||
resolution: {integrity: sha512-jbjcaSxG6ifFyaQtoRFVfhvcWuChCCxGQJSRqVQvXLDEZfQHACdjrZNWkfgRWkMxsNRGTGo4r+A/0Cjgz6eiRw==}
|
||||
|
||||
'@leafer/file@2.2.9':
|
||||
resolution: {integrity: sha512-6kZLM4mmDa3qNMrG2A8MxiVVgHJo+5Yjd/yy6yNEweDoKg9QzNacf8mrILdj2CUjuZ3IPBoRIRGFASZpEmUycg==}
|
||||
|
||||
'@leafer/helper@2.2.9':
|
||||
resolution: {integrity: sha512-OVMB9k3IP5w2ARrKIzrDhrc2PTjFppIKtZpvLIFf24tH6Itq0/Hn6J8pa7gKEzdltpgOJD5lYT7IQNtVbJQVXg==}
|
||||
|
||||
'@leafer/image-web@2.2.9':
|
||||
resolution: {integrity: sha512-ctMDVYoDi16rniIOffrqXGfOR1qWJe+lm185l+mjsqfcQo//QZ9cml4m+FfChnzwUqB6yuJvhaP3idg59ARFVA==}
|
||||
|
||||
'@leafer/image@2.2.9':
|
||||
resolution: {integrity: sha512-hnl7pLM1kOVUo/qdR9Sonvt40ywt9796JDyinV3BQkVh1lfiWQXJY2wb/iQXCUPyDvhiD0cSC+sDR3x9VIFyCw==}
|
||||
|
||||
'@leafer/interface@2.2.9':
|
||||
resolution: {integrity: sha512-XmwjsgV2u1AeAFdjeKOSAlo4zj1ascUKnlEhi0SGL+01iBomDeORjcNm50AoFMZRxY0SqQSIjvDWe6CdxrDcNw==}
|
||||
|
||||
'@leafer/layout@2.2.9':
|
||||
resolution: {integrity: sha512-y/22wyyRVR8OQf9tQe+7Koqmmw5fUZkxUxJKzaeDUf1R6xauK6FtDaPDiDJlxBEgIYXFvVpUfQNEzFdyyMS2jw==}
|
||||
|
||||
'@leafer/layouter@2.2.9':
|
||||
resolution: {integrity: sha512-JFHCtoUI4ELBTwVow6Nd+cxJxvR2H0zg8lq/1lTqgdppeOD1PRU7bP7L1JN5QrmtJ/yxDUi3iGeYg7z9VnWS9w==}
|
||||
|
||||
'@leafer/list@2.2.9':
|
||||
resolution: {integrity: sha512-B9H7ydvE5I7jhdnnML8kox0q+r5/uwF/t0xNHZc3FzvynpHtvpAYTpuzE5zjK0E9C2UkfQi0T9DuUr9CsguMCA==}
|
||||
|
||||
'@leafer/math@2.2.9':
|
||||
resolution: {integrity: sha512-kkybyaR+76e0dsgAxk0Mpeb1coMNM3/mR/JvDBNzcF2iFCemYcOFCiI8+uhJyqUKLfYzLjD7CW5gFa0NmtbgDQ==}
|
||||
|
||||
'@leafer/partner@2.2.9':
|
||||
resolution: {integrity: sha512-W2CcbVPoGomvVQNve1CWdhq1B5zOmUKUJqKssO8eBZF6cIXtFW9Ab1CxZR1CcLCi+OelDUvcYFhjU3keLCnNdg==}
|
||||
|
||||
'@leafer/path@2.2.9':
|
||||
resolution: {integrity: sha512-GP3IXKNUkUGZuZI/V2IkcJc+uU6Rs2mAvu1xtgyh73iRbkvdlk3T/MbxgOkGFmQ5cO7KKCiwizWRYyW5Z7Fb0g==}
|
||||
|
||||
'@leafer/platform@2.2.9':
|
||||
resolution: {integrity: sha512-wSDawGKgam20Mrl10VWP4UcYrrrWU7wlKi4/T/0j0ljvu29ZSKcLA2o4//WYJxVcuDEqSwMl3EXdpkl7pYdHlg==}
|
||||
|
||||
'@leafer/renderer@2.2.9':
|
||||
resolution: {integrity: sha512-CxCT5p/VMJDlbX1rBpSoc9gKrFmOIRygqMT6u1vYZynJY4XM2aqWMCItBGEUzCutRaGRRg8fK3I0J4Hux2qhCA==}
|
||||
|
||||
'@leafer/selector@2.2.9':
|
||||
resolution: {integrity: sha512-YeMxiIlCfFNbCkNGwx7mIccwHlBQTROw9T/zXwhd34GZ52ESVsqPETY9Ot5YuVcExJAM9mEjA6MTQuTEHYjNWw==}
|
||||
|
||||
'@leafer/task@2.2.9':
|
||||
resolution: {integrity: sha512-NrbrofDssz8erxoOBZ3TQEQI9pl3tAPnPtqceOtOpCFC3cGDr8HJWb7F4ZZNRhN0qutPGj+BevOWC9BYoHgMlg==}
|
||||
|
||||
'@leafer/watcher@2.2.9':
|
||||
resolution: {integrity: sha512-v6nRfHh3+2DZs94MItG2i+WrLMHNOqnWD7cPQ8cxo1MVX/BghjWEE8LGy7hOcLIYIppDZ9ZHeVNEhtNTCRffug==}
|
||||
|
||||
'@leafer/web-core@2.2.9':
|
||||
resolution: {integrity: sha512-d/m46qw6zjAjf+X/MBx4q0ztd5SxSs/mYORdaNjiOZRg6qFXsBGtUUSUyN5dIprCjKFkdVge4ow3yLxv2RvdMw==}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6':
|
||||
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
|
||||
peerDependencies:
|
||||
@ -1401,6 +1566,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
leafer-ui@2.2.9:
|
||||
resolution: {integrity: sha512-z6GklKuOMV8gMCAxWvvRUDXOZdSLzOkBzJBFEiDMcjVuTMpceQD/HPsV+Sq7Dk8I1xqY6OcF+3yTLEWYMOauxA==}
|
||||
|
||||
lightningcss-android-arm64@1.33.0:
|
||||
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@ -2333,6 +2501,293 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@leafer-in/interface@2.2.9(@leafer-ui/interface@2.2.9)(@leafer/interface@2.2.9)':
|
||||
dependencies:
|
||||
'@leafer-ui/interface': 2.2.9
|
||||
'@leafer/interface': 2.2.9
|
||||
|
||||
'@leafer-ui/app@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/data': 2.2.9
|
||||
'@leafer-ui/display': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/bounds@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/color@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/core@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/app': 2.2.9
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer-ui/event': 2.2.9
|
||||
'@leafer-ui/hit': 2.2.9
|
||||
'@leafer-ui/interaction': 2.2.9
|
||||
|
||||
'@leafer-ui/data@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/external': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/decorator@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/display-module@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/bounds': 2.2.9
|
||||
'@leafer-ui/data': 2.2.9
|
||||
'@leafer-ui/render': 2.2.9
|
||||
|
||||
'@leafer-ui/display@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/data': 2.2.9
|
||||
'@leafer-ui/decorator': 2.2.9
|
||||
'@leafer-ui/display-module': 2.2.9
|
||||
'@leafer-ui/external': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/draw@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/decorator': 2.2.9
|
||||
'@leafer-ui/display': 2.2.9
|
||||
'@leafer-ui/display-module': 2.2.9
|
||||
'@leafer-ui/external': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/effect@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/event@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/external@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/hit@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/interaction-web@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/core': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/interaction@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer-ui/event': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/interface@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/interface': 2.2.9
|
||||
|
||||
'@leafer-ui/paint@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/partner@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/color': 2.2.9
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer-ui/effect': 2.2.9
|
||||
'@leafer-ui/paint': 2.2.9
|
||||
'@leafer-ui/text': 2.2.9
|
||||
|
||||
'@leafer-ui/render@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-ui/external': 2.2.9
|
||||
|
||||
'@leafer-ui/text@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer-ui/web@2.2.9':
|
||||
dependencies:
|
||||
'@leafer-in/interface': 2.2.9(@leafer-ui/interface@2.2.9)(@leafer/interface@2.2.9)
|
||||
'@leafer-ui/core': 2.2.9
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer-ui/interaction-web': 2.2.9
|
||||
'@leafer-ui/interface': 2.2.9
|
||||
'@leafer-ui/partner': 2.2.9
|
||||
'@leafer/interface': 2.2.9
|
||||
'@leafer/partner': 2.2.9
|
||||
'@leafer/web-core': 2.2.9
|
||||
|
||||
'@leafer/canvas-web@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer/canvas@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/file': 2.2.9
|
||||
'@leafer/list': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
'@leafer/path': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
|
||||
'@leafer/core@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/canvas': 2.2.9
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/decorator': 2.2.9
|
||||
'@leafer/display': 2.2.9
|
||||
'@leafer/display-module': 2.2.9
|
||||
'@leafer/event': 2.2.9
|
||||
'@leafer/file': 2.2.9
|
||||
'@leafer/helper': 2.2.9
|
||||
'@leafer/image': 2.2.9
|
||||
'@leafer/layout': 2.2.9
|
||||
'@leafer/list': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
'@leafer/path': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
'@leafer/task': 2.2.9
|
||||
|
||||
'@leafer/data@2.2.9': {}
|
||||
|
||||
'@leafer/debug@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
|
||||
'@leafer/decorator@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
|
||||
'@leafer/display-module@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/event': 2.2.9
|
||||
'@leafer/helper': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
|
||||
'@leafer/display@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/decorator': 2.2.9
|
||||
'@leafer/display-module': 2.2.9
|
||||
'@leafer/event': 2.2.9
|
||||
'@leafer/helper': 2.2.9
|
||||
'@leafer/image': 2.2.9
|
||||
'@leafer/layout': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
|
||||
'@leafer/event@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/decorator': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
|
||||
'@leafer/file@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
'@leafer/task': 2.2.9
|
||||
|
||||
'@leafer/helper@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
|
||||
'@leafer/image-web@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer/image@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/file': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
'@leafer/task': 2.2.9
|
||||
|
||||
'@leafer/interface@2.2.9': {}
|
||||
|
||||
'@leafer/layout@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/helper': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
'@leafer/platform': 2.2.9
|
||||
|
||||
'@leafer/layouter@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer/list@2.2.9': {}
|
||||
|
||||
'@leafer/math@2.2.9': {}
|
||||
|
||||
'@leafer/partner@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
'@leafer/layouter': 2.2.9
|
||||
'@leafer/renderer': 2.2.9
|
||||
'@leafer/selector': 2.2.9
|
||||
'@leafer/watcher': 2.2.9
|
||||
|
||||
'@leafer/path@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
|
||||
'@leafer/platform@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/debug': 2.2.9
|
||||
|
||||
'@leafer/renderer@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer/selector@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/core': 2.2.9
|
||||
|
||||
'@leafer/task@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/debug': 2.2.9
|
||||
'@leafer/math': 2.2.9
|
||||
|
||||
'@leafer/watcher@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/data': 2.2.9
|
||||
'@leafer/event': 2.2.9
|
||||
'@leafer/list': 2.2.9
|
||||
|
||||
'@leafer/web-core@2.2.9':
|
||||
dependencies:
|
||||
'@leafer/canvas-web': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
'@leafer/image-web': 2.2.9
|
||||
'@leafer/interface': 2.2.9
|
||||
'@leafer/partner': 2.2.9
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
@ -3181,6 +3636,19 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
leafer-ui@2.2.9:
|
||||
dependencies:
|
||||
'@leafer-in/interface': 2.2.9(@leafer-ui/interface@2.2.9)(@leafer/interface@2.2.9)
|
||||
'@leafer-ui/core': 2.2.9
|
||||
'@leafer-ui/draw': 2.2.9
|
||||
'@leafer-ui/interaction-web': 2.2.9
|
||||
'@leafer-ui/interface': 2.2.9
|
||||
'@leafer-ui/partner': 2.2.9
|
||||
'@leafer-ui/web': 2.2.9
|
||||
'@leafer/core': 2.2.9
|
||||
'@leafer/interface': 2.2.9
|
||||
'@leafer/partner': 2.2.9
|
||||
|
||||
lightningcss-android-arm64@1.33.0:
|
||||
optional: true
|
||||
|
||||
|
||||
@ -1 +1,3 @@
|
||||
packages:
|
||||
- packages/*
|
||||
approveBuilds: puppeteer
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user