├── .gitignore ├── .vscode └── extensions.json ├── License ├── README.md ├── index.html ├── lib ├── LICENSE.txt └── opencv.js ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── fontPlayer-logo.icns ├── fontPlayer-logo.ico ├── fontPlayer-logo.png ├── fontplayer.ico ├── glyphs │ ├── comp_glyphs_data_v7_v4.json │ ├── radical_glyphs_data_v7_v5.json │ └── stroke_glyphs_data_v7_v4.json ├── templates │ ├── playground.json │ ├── template1.json │ └── templates2 │ │ ├── 二横折.js │ │ ├── 平捺.js │ │ ├── 弯钩.js │ │ ├── 挑.js │ │ ├── 挑捺.js │ │ ├── 捺.js │ │ ├── 撇.js │ │ ├── 撇挑.js │ │ ├── 撇点.js │ │ ├── 斜钩.js │ │ ├── 横.js │ │ ├── 横弯钩.js │ │ ├── 横折.js │ │ ├── 横折2.js │ │ ├── 横折弯.js │ │ ├── 横折弯钩.js │ │ ├── 横折折弯钩.js │ │ ├── 横折折撇.js │ │ ├── 横折挑.js │ │ ├── 横折钩.js │ │ ├── 横撇.js │ │ ├── 横撇弯钩.js │ │ ├── 横钩.js │ │ ├── 点.js │ │ ├── 竖.js │ │ ├── 竖弯.js │ │ ├── 竖弯钩.js │ │ ├── 竖折.js │ │ ├── 竖折折钩.js │ │ ├── 竖挑.js │ │ ├── 竖撇.js │ │ └── 竖钩.js └── vite.svg ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── lib.rs │ └── main.rs └── tauri.conf.json ├── src ├── App.vue ├── assets │ ├── base.css │ ├── icons │ │ ├── add-icon.svg │ │ ├── copy-icon.svg │ │ ├── delete-icon.svg │ │ ├── edit-icon.svg │ │ ├── fill-drip-solid.svg │ │ ├── pen-cursor.cur │ │ ├── pen-nib-solid.png │ │ ├── pen-nib-solid.svg │ │ ├── rotate-left-solid.svg │ │ ├── rotate-right-solid.svg │ │ └── square-solid.svg │ ├── main.css │ └── svg │ │ └── loading.svg ├── features │ ├── bezierCurve.ts │ ├── fitCurve.ts │ ├── font.ts │ ├── image.ts │ ├── layout.ts │ └── svg.ts ├── fontEditor │ ├── Event │ │ └── bus.ts │ ├── background │ │ ├── layoutGrid.ts │ │ ├── mesh.ts │ │ └── transparent.ts │ ├── canvas │ │ └── canvas.ts │ ├── components │ │ ├── BottomBar │ │ │ ├── BottomBar.vue │ │ │ └── GlyphBottomBar.vue │ │ ├── CharacterList │ │ │ ├── CharacterList.vue │ │ │ ├── CompGlyphList.vue │ │ │ ├── GlyphList.vue │ │ │ ├── RadicalGlyphList.vue │ │ │ └── StrokeGlyphList.vue │ │ ├── Dialogs │ │ │ ├── AddGlyphDialog.vue │ │ │ ├── AddIconDialog.vue │ │ │ ├── AddTextDialog.vue │ │ │ ├── CloseFileTipDialog.vue │ │ │ ├── CreateFileDialog.vue │ │ │ ├── ExportFileDialog.vue │ │ │ ├── ExportFontDialog.vue │ │ │ ├── ExportFontDialog_tauri.vue │ │ │ ├── FontSettingsDialog.vue │ │ │ ├── GlyphComponentsDialog.vue │ │ │ ├── ImportTemplatesDialog.vue │ │ │ ├── LanguageSettingsDialog.vue │ │ │ ├── PreferenceSettingsDialog.vue │ │ │ ├── SaveAsDialog.vue │ │ │ ├── SaveDialog.vue │ │ │ ├── SaveFileTipDialog.vue │ │ │ ├── SelectGlobalParamDialog.vue │ │ │ ├── SetAsGlobalParamDialog.vue │ │ │ ├── TipsDialog.vue │ │ │ ├── copyCharacterDialog.vue │ │ │ ├── copyGlyphDialog.vue │ │ │ ├── editCharacterDialog.vue │ │ │ ├── editGlyphDialog.vue │ │ │ └── fontSettings │ │ │ │ ├── FontSettingsDialog.vue │ │ │ │ ├── head.vue │ │ │ │ ├── hhea.vue │ │ │ │ ├── name.vue │ │ │ │ ├── os_2.vue │ │ │ │ └── post.vue │ │ ├── FilesBar │ │ │ └── FilesBar.vue │ │ ├── FontEditorPanels │ │ │ ├── EditPanel.vue │ │ │ ├── GlyphEditPanel.vue │ │ │ └── ThumbnailEditPanel.vue │ │ ├── FontMainPanel │ │ │ └── FontMainPanel.vue │ │ ├── LeftPanel │ │ │ └── LeftPanel.vue │ │ ├── List │ │ │ ├── CharacterComponentList.vue │ │ │ ├── CharacterSubComponentList.vue │ │ │ ├── GlyphComponentList.vue │ │ │ └── GlyphSubComponentList.vue │ │ ├── RightPanel │ │ │ └── RightPanel.vue │ │ ├── SettingsPanel │ │ │ └── SettingsPanel.vue │ │ ├── SideBar │ │ │ └── SideBar.vue │ │ ├── ToolBar │ │ │ └── ToolBar.vue │ │ ├── TopBar │ │ │ └── TopBar.vue │ │ ├── ViewList │ │ │ └── ViewList.vue │ │ ├── Widgets │ │ │ ├── GridController.vue │ │ │ ├── GridController_playground.vue │ │ │ ├── MetricsController.vue │ │ │ └── RingController.vue │ │ └── paramsEditPanels │ │ │ ├── EllipseEditPanel.vue │ │ │ ├── FontPicEditPanel.vue │ │ │ ├── GlyphEditPanel.vue │ │ │ ├── GlyphEditPanel_Glyph.vue │ │ │ ├── GlyphParamsPanel.vue │ │ │ ├── LayoutEditPanel.vue │ │ │ ├── MetricsEditPanel.vue │ │ │ ├── PenEditPanel.vue │ │ │ ├── PictureEditPanel.vue │ │ │ ├── PolygonEditPanel.vue │ │ │ └── RectangleEditPanel.vue │ ├── menus │ │ ├── handlers.ts │ │ └── web_menus.ts │ ├── programming │ │ ├── Character.ts │ │ ├── ConstantsMap.ts │ │ ├── CustomGlyph.ts │ │ ├── EllipseComponent.ts │ │ ├── FPUtils.ts │ │ ├── Joint.ts │ │ ├── ParametersMap.ts │ │ ├── PenComponent.ts │ │ ├── PolygonComponent.ts │ │ ├── RectangleComponent.ts │ │ ├── Skeleton.ts │ │ └── global_contants.ts │ ├── renderer │ │ └── index.ts │ ├── stores │ │ ├── dialogs.ts │ │ ├── edit.ts │ │ ├── ellipse.ts │ │ ├── files.ts │ │ ├── font.ts │ │ ├── global.ts │ │ ├── glyph.ts │ │ ├── glyphDragger.ts │ │ ├── glyphDragger_glyph.ts │ │ ├── glyphLayoutResizer.ts │ │ ├── glyphLayoutResizer_glyph.ts │ │ ├── pen.ts │ │ ├── playground.ts │ │ ├── polygon.ts │ │ ├── rectangle.ts │ │ ├── select.ts │ │ ├── settings.ts │ │ └── system.ts │ ├── templates │ │ └── strokes_1.ts │ ├── tools │ │ ├── coordsViewer.ts │ │ ├── ellipse.ts │ │ ├── glyphDragger.ts │ │ ├── glyphDragger_glyph.ts │ │ ├── glyphLayoutResizer.ts │ │ ├── glyphLayoutResizer_glyph.ts │ │ ├── pen.ts │ │ ├── picture.ts │ │ ├── polygon.ts │ │ ├── rectangle.ts │ │ ├── select │ │ │ ├── penSelect.ts │ │ │ └── select.ts │ │ └── translateMover.ts │ ├── views │ │ ├── CharacterProgrammingEditor.vue │ │ ├── Editor.vue │ │ ├── GlyphProgrammingEditor.vue │ │ ├── Playground.vue │ │ └── Welcome.vue │ └── worker │ │ ├── index.ts │ │ ├── shim.ts │ │ └── worker.ts ├── fontManager │ ├── character.ts │ ├── decode.ts │ ├── encode.ts │ ├── font.ts │ ├── index.ts │ ├── table.ts │ ├── tables │ │ ├── cff.ts │ │ ├── cmap.ts │ │ ├── glyf.ts │ │ ├── head.ts │ │ ├── hhea.ts │ │ ├── hmtx.ts │ │ ├── loca.ts │ │ ├── maxp.ts │ │ ├── name.ts │ │ ├── os_2.ts │ │ ├── post.ts │ │ └── sfnt.ts │ ├── utils │ │ └── index.ts │ └── validators.ts ├── i18n │ ├── dialogs.ts │ ├── electron │ │ └── index.ts │ ├── index.ts │ ├── menus.ts │ ├── panels.ts │ ├── programming.ts │ └── welcome.ts ├── main.ts ├── router │ └── index.ts ├── test │ ├── fontManager │ │ ├── font.spec.ts │ │ ├── tables │ │ │ ├── cff.spec.ts │ │ │ ├── cmap.spec.ts │ │ │ ├── head.spec.ts │ │ │ ├── hhea.spec.ts │ │ │ ├── hmtx.spec.ts │ │ │ ├── maxp.spec.ts │ │ │ ├── name.spec.ts │ │ │ ├── os_2.spec.ts │ │ │ └── post.spec.ts │ │ └── utils.ts │ ├── stores │ │ ├── addComponentForCurrentCharacterFile.spec.ts │ │ ├── addFile.spec.ts │ │ ├── insertComponentForCurrentCharacterFile.spec.ts │ │ ├── modifyComponentForCurrentCharacterFile.spec.ts │ │ ├── removeComponentForCurrentCharacterFile.spec.ts │ │ └── removeFile.spec.ts │ ├── tools │ │ ├── ellipse.spec.ts │ │ ├── pen.spec.ts │ │ ├── polygon.spec.ts │ │ └── rectangle.spec.ts │ └── utils │ │ ├── canvas.spec.ts │ │ ├── data.spec.ts │ │ ├── math.spec.ts │ │ └── string.spec.ts ├── utils │ ├── canvas.ts │ ├── data.ts │ ├── math.ts │ └── string.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-electron 13 | 14 | dist-ssr 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | stats.html -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 玩具工匠 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 字玩FontPlayer 2 | 一款开源的字体设计工具。 3 | 4 | 使用Vue3 + ElementUI + Tauri2开发,支持Web端、MacOS和Windows平台。 5 | 6 | 官网:https://www.font-player.com 7 | 8 | 在线体验:https://toysmaker.github.io/fontplayer_demo/ 9 | 10 | 桌面版下载: 11 | 12 | gitee release: https://gitee.com/toysmaker/fontplayer/releases 13 | 14 | github release: https://github.com/HiToysMaker/fontplayer/releases 15 | 16 | ### 运行程序 17 | 首先安装依赖: 18 | ``` 19 | npm run install 20 | ``` 21 | 运行程序: 22 | ``` 23 | npm run dev 24 | ``` 25 | 26 | ### 运行Tauri应用 27 | 开发环境下测试: 28 | ``` 29 | npx tauri dev 30 | ``` 31 | 32 | Tauri应用打包: 33 | ``` 34 | npx tauri build 35 | ``` 36 | 37 | ### 致谢 38 | 1. opentypes.js: https://github.com/opentypejs/opentype.js 39 | 字玩中字体文件解析生成模块参考了opentype.js的设计,并使用了部分代码 40 | 41 | 2. fitCurves: https://github.com/volkerp/fitCurves 42 | 字玩中拟合贝塞尔曲线模块参照了这个开源项目,改写为ts版 43 | 44 | 3. 字玩中图像处理部分使用了opencv.js,源码放在lib文件夹下,未做修改,拷贝自opencv官网:https://docs.opencv.org/4.5.0/opencv.js 45 | 注:opencv项目使用Apache-2.0 license协议,协议副本包含在lib文件夹中。感谢opencv的开源:https://github.com/opencv/opencv 46 | 47 | 4. 思源黑体:字玩中默认黑体模板在结构上参考思源黑体。思源黑体开源地址:https://github.com/adobe-fonts/source-han-sans -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontplayer", 3 | "displayName": "字玩", 4 | "version": "0.2.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview --port 4173", 9 | "test:unit": "vitest --environment jsdom", 10 | "test:e2e": "start-server-and-test preview http://localhost:4173/ 'cypress open --e2e'", 11 | "test:e2e:ci": "start-server-and-test preview http://localhost:4173/ 'cypress run --e2e'", 12 | "build-only": "vite build", 13 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 14 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 15 | }, 16 | "devDependencies": { 17 | "@tauri-apps/cli": "^2.1.0", 18 | "cross-env": "^7.0.3", 19 | "rollup-plugin-visualizer": "^5.12.0" 20 | }, 21 | "dependencies": { 22 | "@codemirror/lang-javascript": "^6.2.2", 23 | "@codemirror/theme-one-dark": "^6.1.2", 24 | "@element-plus/icons-vue": "^2.0.9", 25 | "@fortawesome/fontawesome-svg-core": "^6.2.0", 26 | "@fortawesome/free-brands-svg-icons": "^6.2.0", 27 | "@fortawesome/free-regular-svg-icons": "^6.2.0", 28 | "@fortawesome/free-solid-svg-icons": "^6.2.0", 29 | "@fortawesome/vue-fontawesome": "^3.0.1", 30 | "@rushstack/eslint-patch": "^1.1.4", 31 | "@tauri-apps/api": "^2.1.1", 32 | "@tauri-apps/plugin-clipboard-manager": "^2.2.0", 33 | "@tauri-apps/plugin-dialog": "~2", 34 | "@tauri-apps/plugin-fs": "~2.2.0", 35 | "@techstark/opencv-js": "4.6.0-release.1", 36 | "@types/file-saver": "^2.0.5", 37 | "@types/jsdom": "^20.0.0", 38 | "@types/node": "^16.11.56", 39 | "@types/opentype.js": "^1.3.4", 40 | "@types/ramda": "^0.28.15", 41 | "@vitejs/plugin-vue": "^3.0.3", 42 | "@vitejs/plugin-vue-jsx": "^2.0.1", 43 | "@vue/eslint-config-prettier": "^7.0.0", 44 | "@vue/eslint-config-typescript": "^11.0.0", 45 | "@vue/test-utils": "^2.0.2", 46 | "@vue/tsconfig": "^0.1.3", 47 | "codemirror": "^6.0.1", 48 | "crypto-js": "^4.1.1", 49 | "cypress": "^10.7.0", 50 | "element-plus": "^2.3.12", 51 | "eslint": "^8.22.0", 52 | "eslint-plugin-cypress": "^2.12.1", 53 | "eslint-plugin-vue": "^9.3.0", 54 | "file-saver": "^2.0.5", 55 | "fs-extra": "^11.2.0", 56 | "i18next": "^23.6.0", 57 | "iconv-lite": "^0.6.3", 58 | "jsdom": "^20.0.0", 59 | "jszip": "^3.10.1", 60 | "localforage": "^1.10.0", 61 | "mitt": "^3.0.0", 62 | "nanoid": "^5.0.1", 63 | "npm-run-all": "^4.1.5", 64 | "opentype.js": "^1.3.4", 65 | "paper": "^0.12.18", 66 | "prettier": "^2.7.1", 67 | "ramda": "^0.28.0", 68 | "rollup-plugin-copy": "^3.5.0", 69 | "start-server-and-test": "^1.14.0", 70 | "tiny-pinyin": "^1.3.2", 71 | "typescript": "~4.7.4", 72 | "vite": "^5.2.9", 73 | "vite-plugin-static-copy": "^1.0.6", 74 | "vitest": "^0.23.0", 75 | "vue": "^3.2.38", 76 | "vue-i18n": "^9.4.1", 77 | "vue-router": "^4.1.5", 78 | "vue-tsc": "^0.40.7" 79 | } 80 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/public/favicon.ico -------------------------------------------------------------------------------- /public/fontPlayer-logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/public/fontPlayer-logo.icns -------------------------------------------------------------------------------- /public/fontPlayer-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/public/fontPlayer-logo.ico -------------------------------------------------------------------------------- /public/fontPlayer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/public/fontPlayer-logo.png -------------------------------------------------------------------------------- /public/fontplayer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/public/fontplayer.ico -------------------------------------------------------------------------------- /public/templates/template1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.0, 3 | "template": [ 4 | { 5 | "char": "挨", 6 | "layout": "左右", 7 | "data": "左<扌-部首,25,100,300,800>右<矣-组件,275,100,700,800>" 8 | }, 9 | { 10 | "char": "按", 11 | "layout": "左右", 12 | "data": "左<扌-部首,25,100,300,800>右<安-组件,275,100,700,800>" 13 | }, 14 | { 15 | "char": "把", 16 | "layout": "左右", 17 | "data": "左<扌-部首,25,100,300,800>右<巴-组件,300,100,675,800>" 18 | }, 19 | { 20 | "char": "摆", 21 | "layout": "左右", 22 | "data": "左<扌-部首,25,100,300,800>右<罢-组件,275,100,700,800>" 23 | }, 24 | { 25 | "char": "抱", 26 | "layout": "左右", 27 | "data": "左<扌-部首,25,100,300,800>右<包-组件,270,100,700,800>" 28 | }, 29 | { 30 | "char": "拨", 31 | "layout": "左右", 32 | "data": "左<扌-部首,25,100,300,800>右<发-组件,200,100,800,800>" 33 | }, 34 | { 35 | "char": "播", 36 | "layout": "左右", 37 | "data": "左<扌-部首,25,100,300,800>右<番-组件,250,100,700,800>" 38 | }, 39 | { 40 | "char": "捕", 41 | "layout": "左右", 42 | "data": "左<扌-部首,25,100,300,800>右<甫-组件,275,100,700,800>" 43 | }, 44 | { 45 | "char": "拆", 46 | "layout": "左右", 47 | "data": "左<扌-部首,25,100,300,800>右<斥-组件,275,100,700,800>" 48 | }, 49 | { 50 | "char": "抄", 51 | "layout": "左右", 52 | "data": "左<扌-部首,25,100,300,800>右<少-组件,275,100,700,800>" 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /gen/schemas 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.2.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | rust-version = "1.77.2" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [lib] 14 | name = "app_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2.0.2", features = [] } 19 | 20 | [dependencies] 21 | serde_json = "1.0" 22 | serde = { version = "1.0", features = ["derive"] } 23 | log = "0.4" 24 | tauri = { version = "2.1.0", features = [] } 25 | tauri-plugin-log = "2.0.0-rc" 26 | lazy_static = "1.4" 27 | native-dialog = "0.5.4" 28 | regex = "1" 29 | base64 = "0.21" 30 | tauri-plugin-fs = "2" 31 | tauri-plugin-dialog = "2" 32 | tauri-plugin-clipboard-manager = "2.2.0" 33 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "enables the default permissions", 5 | "windows": [ 6 | "main", 7 | "glyph-script", 8 | "character-script" 9 | ], 10 | "remote": { 11 | "urls": [ 12 | "https://*.tauri.app" 13 | ] 14 | }, 15 | "permissions": [ 16 | { 17 | "identifier": "fs:scope", 18 | "allow": [ 19 | { 20 | "path": "$APPDATA" 21 | }, 22 | { 23 | "path": "$APPDATA/**" 24 | } 25 | ] 26 | }, 27 | "core:default", 28 | "fs:read-files", 29 | "fs:write-files", 30 | "fs:allow-appdata-read-recursive", 31 | "fs:allow-appdata-write-recursive", 32 | "fs:default", 33 | "clipboard-manager:allow-read-text", 34 | "clipboard-manager:allow-write-text", 35 | "dialog:default", 36 | "core:event:allow-emit", 37 | "core:event:allow-listen", 38 | "core:webview:allow-clear-all-browsing-data", 39 | "core:webview:allow-create-webview", 40 | "core:webview:allow-create-webview-window", 41 | "core:webview:allow-get-all-webviews", 42 | "core:webview:allow-internal-toggle-devtools", 43 | "core:webview:allow-print", 44 | "core:webview:allow-reparent", 45 | "core:webview:allow-set-webview-focus", 46 | "core:webview:allow-set-webview-position", 47 | "core:webview:allow-set-webview-size", 48 | "core:webview:allow-set-webview-zoom", 49 | "core:webview:allow-webview-close", 50 | "core:webview:allow-webview-hide", 51 | "core:webview:allow-webview-position", 52 | "core:webview:allow-webview-show", 53 | "core:webview:allow-webview-size", 54 | "core:window:allow-available-monitors", 55 | "core:window:allow-center", 56 | "core:window:allow-close", 57 | "core:window:allow-create", 58 | "core:window:allow-current-monitor", 59 | "core:window:allow-cursor-position", 60 | "core:window:allow-destroy", 61 | "core:window:allow-get-all-windows", 62 | "core:window:allow-hide", 63 | "core:window:allow-inner-position", 64 | "core:window:allow-inner-size", 65 | "core:window:allow-internal-toggle-maximize", 66 | "core:window:allow-is-closable", 67 | "core:window:allow-is-decorated", 68 | "core:window:allow-is-enabled", 69 | "core:window:allow-is-focused", 70 | "core:window:allow-is-fullscreen", 71 | "core:window:allow-is-maximizable", 72 | "core:window:allow-is-maximized", 73 | "core:window:allow-is-minimizable", 74 | "core:window:allow-is-minimized", 75 | "core:window:allow-is-resizable", 76 | "core:window:allow-is-visible", 77 | "core:window:allow-maximize", 78 | "core:window:allow-minimize", 79 | "core:window:allow-monitor-from-point", 80 | "core:window:allow-outer-position", 81 | "core:window:allow-outer-size", 82 | "core:window:allow-primary-monitor", 83 | "core:window:allow-request-user-attention", 84 | "core:window:allow-scale-factor", 85 | "core:window:allow-set-always-on-bottom", 86 | "core:window:allow-set-always-on-top", 87 | "core:window:allow-set-closable", 88 | "core:window:allow-set-content-protected", 89 | "core:window:allow-set-cursor-grab", 90 | "core:window:allow-set-cursor-icon", 91 | "core:window:allow-set-cursor-position", 92 | "core:window:allow-set-decorations", 93 | "core:window:allow-set-effects", 94 | "core:window:allow-set-enabled", 95 | "core:window:allow-set-focus", 96 | "core:window:allow-set-fullscreen", 97 | "core:window:allow-set-icon", 98 | "core:window:allow-set-ignore-cursor-events", 99 | "core:window:allow-set-max-size", 100 | "core:window:allow-set-maximizable", 101 | "core:window:allow-set-min-size", 102 | "core:window:allow-set-minimizable", 103 | "core:window:allow-set-position", 104 | "core:window:allow-set-progress-bar", 105 | "core:window:allow-set-resizable", 106 | "core:window:allow-set-shadow", 107 | "core:window:allow-set-size", 108 | "core:window:allow-set-size-constraints", 109 | "core:window:allow-set-skip-taskbar", 110 | "core:window:allow-set-theme", 111 | "core:window:allow-set-title", 112 | "core:window:allow-set-title-bar-style", 113 | "core:window:allow-set-visible-on-all-workspaces", 114 | "core:window:allow-show", 115 | "core:window:allow-start-dragging", 116 | "core:window:allow-start-resize-dragging", 117 | "core:window:allow-theme", 118 | "core:window:allow-title", 119 | "core:window:allow-toggle-maximize", 120 | "core:window:allow-unmaximize", 121 | "core:window:allow-unminimize" 122 | ] 123 | } -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | use std::panic; 5 | 6 | fn main() { 7 | // 设置 panic 钩子 8 | panic::set_hook(Box::new(|panic_info| { 9 | println!("捕获到 panic: {:?}", panic_info); 10 | })); 11 | 12 | app_lib::run(); 13 | } -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", 3 | "productName": "字玩", 4 | "version": "0.2.0", 5 | "identifier": "com.fontplayer.app", 6 | "build": { 7 | "frontendDist": "../dist", 8 | "devUrl": "http://localhost:5173", 9 | "beforeDevCommand": "npm run dev", 10 | "beforeBuildCommand": "npm run build" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "字玩", 16 | "width": 1280, 17 | "height": 800, 18 | "resizable": true, 19 | "fullscreen": false 20 | } 21 | ], 22 | "security": { 23 | "csp": null 24 | } 25 | }, 26 | "bundle": { 27 | "active": true, 28 | "targets": "all", 29 | "icon": [ 30 | "icons/icon.png", 31 | "icons/icon.icns", 32 | "icons/icon.ico" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | 76 | body, html { 77 | width: 100%; 78 | height: 100%; 79 | position: relative; 80 | padding: 0; 81 | margin: 0; 82 | overflow: hidden; 83 | } 84 | -------------------------------------------------------------------------------- /src/assets/icons/add-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/copy-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/delete-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/edit-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/fill-drip-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/pen-cursor.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/src/assets/icons/pen-cursor.cur -------------------------------------------------------------------------------- /src/assets/icons/pen-nib-solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiToysMaker/fontplayer/bd73ea34c885e7f62b82979fffc7b4ea81fcb53b/src/assets/icons/pen-nib-solid.png -------------------------------------------------------------------------------- /src/assets/icons/pen-nib-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/rotate-left-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/rotate-right-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/square-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/bezierCurve.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 该文件包含了贝塞尔曲线相关的实用方法 3 | */ 4 | /** 5 | * this file contains related methods for bezier curves 6 | */ 7 | 8 | export interface IPoint { 9 | x: number; 10 | y: number; 11 | } 12 | 13 | interface IVector { 14 | x: number; 15 | y: number; 16 | } 17 | 18 | const bezierCurve = { 19 | q: (bezier: Array, t: number) => { 20 | const p0 = bezier[0] 21 | const p1 = bezier[1] 22 | const p2 = bezier[2] 23 | const p3 = bezier[3] 24 | const x = 25 | p0.x * Math.pow((1 - t), 3) + 26 | 3 * p1.x * t * Math.pow((1 - t), 2) + 27 | 3 * p2.x * t * t * (1 - t) + 28 | p3.x * Math.pow(t, 3) 29 | const y = 30 | p0.y * Math.pow((1 - t), 3) + 31 | 3 * p1.y * t * Math.pow((1 - t), 2) + 32 | 3 * p2.y * t * t * (1 - t) + 33 | p3.y * Math.pow(t, 3) 34 | return { x, y } 35 | }, 36 | qprime: (bezier: Array, t: number) => { 37 | const p0 = bezier[0] 38 | const p1 = bezier[1] 39 | const p2 = bezier[2] 40 | const p3 = bezier[3] 41 | const x = 42 | 3 * Math.pow((1.0 - t), 2) * (p1.x - p0.x) + 43 | 6 * (1.0 - t) * t * (p2.x - p1.x) + 44 | 3 * Math.pow(t, 2) * (p3.x - p2.x) 45 | const y = 46 | 3 * Math.pow((1.0 - t), 2) * (p1.y - p0.y) + 47 | 6 * (1.0 - t) * t * (p2.y - p1.y) + 48 | 3 * Math.pow(t, 2) * (p3.y - p2.y) 49 | return { x, y } 50 | }, 51 | qprimeprime: (bezier: Array, t: number) => { 52 | const p0 = bezier[0] 53 | const p1 = bezier[1] 54 | const p2 = bezier[2] 55 | const p3 = bezier[3] 56 | const x = 57 | 6 * (1.0 - t) * (p2.x - 2 * p1.x + p0.x) + 58 | 6 * t * (p3.x - 2 * p2.x + p1.x) 59 | const y = 60 | 6 * (1.0 - t) * (p2.y - 2 * p1.y + p0.y) + 61 | 6 * t * (p3.y - 2 * p2.y + p1.y) 62 | return { x, y } 63 | }, 64 | } 65 | 66 | export { 67 | bezierCurve, 68 | } -------------------------------------------------------------------------------- /src/features/image.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 图像处理相关的一些基础方法 3 | */ 4 | /** 5 | * some methods for image processing 6 | */ 7 | 8 | import * as R from 'ramda' 9 | 10 | const toPixels = (image: HTMLImageElement) => { 11 | const canvas = document.createElement('canvas') 12 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D 13 | canvas.width = image.width 14 | canvas.height = image.height 15 | ctx?.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height) 16 | const pixels: Uint8ClampedArray = ctx?.getImageData(0, 0, canvas.width, canvas.height).data as Uint8ClampedArray 17 | return pixels 18 | } 19 | 20 | const reversePixels = (pixels: Uint8ClampedArray | Array, width: number, height: number) => { 21 | const canvas = document.createElement('canvas') 22 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D 23 | canvas.width = width 24 | canvas.height = height 25 | for (let i = 0; i < width; i++) { 26 | for (let j = 0; j < height; j++) { 27 | const index = (j * width + i) * 4 28 | ctx.fillStyle = `rgba(${255 - pixels[index]}, ${255 - pixels[index + 1]}, ${255 - pixels[index + 2]}, ${255 - pixels[index + 3]})` 29 | ctx.fillRect(i, j, 1, 1) 30 | } 31 | } 32 | const _pixels: Uint8ClampedArray = ctx?.getImageData(0, 0, canvas.width, canvas.height).data as Uint8ClampedArray 33 | return { 34 | pixels: _pixels, 35 | canvas, 36 | } 37 | } 38 | 39 | const toBlackWhiteBitMap = (data: Uint8ClampedArray | Array, thresholds: { 40 | r: number, 41 | g: number, 42 | b: number, 43 | }, options: { 44 | x: number, 45 | y: number, 46 | size: number, 47 | width: number, 48 | height: number, 49 | }) => { 50 | const pixels = R.clone(data) 51 | const { x, y, width, height, size } = options 52 | let w = size 53 | let h = size 54 | if (size < 0) { 55 | w = width 56 | h = height 57 | } 58 | for (let i = x; i < x + w; i++) { 59 | for (let j = y; j < y + h; j++) { 60 | if (i > width || i < 0) continue 61 | if (j > height || j < 0) continue 62 | const { r, g, b, a }: { 63 | r: number, g: number, b: number, a: number 64 | } = { 65 | r: data[(j * width + i) * 4], 66 | g: data[(j * width + i) * 4 + 1], 67 | b: data[(j * width + i) * 4 + 2], 68 | a: data[(j * width + i) * 4 + 3], 69 | } 70 | if (r > thresholds.r || g > thresholds.g || b > thresholds.b) { 71 | pixels[(j * width + i) * 4] = 255 72 | pixels[(j * width + i) * 4 + 1] = 255 73 | pixels[(j * width + i) * 4 + 2] = 255 74 | pixels[(j * width + i) * 4 + 3] = 1 75 | } else { 76 | pixels[(j * width + i) * 4] = 0 77 | pixels[(j * width + i) * 4 + 1] = 0 78 | pixels[(j * width + i) * 4 + 2] = 0 79 | pixels[(j * width + i) * 4 + 3] = 1 80 | } 81 | } 82 | } 83 | return pixels 84 | } 85 | 86 | const pixelsToCanvas = (pixels: Uint8ClampedArray | Array, width: number, height: number) => { 87 | const canvas: HTMLCanvasElement = document.createElement('canvas') 88 | canvas.width = width 89 | canvas.height = height 90 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D 91 | for (let i = 0; i < width; i++) { 92 | for (let j = 0; j < height; j++) { 93 | const index = (j * width + i) * 4 94 | ctx.fillStyle = `rgba(${pixels[index]}, ${pixels[index + 1]}, ${pixels[index + 2]}, ${pixels[index + 3]})` 95 | ctx.fillRect(i, j, 1, 1) 96 | } 97 | } 98 | return canvas 99 | } 100 | 101 | const pixelsToImage = async (pixels: Uint8ClampedArray | Array, width: number, height: number) => { 102 | return new Promise((resolve, reject) => { 103 | const canvas: HTMLCanvasElement = document.createElement('canvas') 104 | canvas.width = width 105 | canvas.height = height 106 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D 107 | for (let i = 0; i < width; i++) { 108 | for (let j = 0; j < height; j++) { 109 | const index = (j * width + i) * 4 110 | ctx.fillStyle = `rgba(${pixels[index]}, ${pixels[index + 1]}, ${pixels[index + 2]}, ${pixels[index + 3]})` 111 | ctx.fillRect(i, j, 1, 1) 112 | } 113 | } 114 | const data = canvas.toDataURL() 115 | const img = document.createElement('img') 116 | img.onload = () => { 117 | resolve({ 118 | image: img, data, 119 | }) 120 | } 121 | img.src = data 122 | }) 123 | } 124 | 125 | export { 126 | toPixels, 127 | toBlackWhiteBitMap, 128 | pixelsToImage, 129 | pixelsToCanvas, 130 | reversePixels, 131 | } -------------------------------------------------------------------------------- /src/fontEditor/Event/bus.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | // 定义使用到的事件 4 | // events definition 5 | type Events = { 6 | toggleLocalBrushEdit: boolean; 7 | resetEditFontPic: boolean; 8 | seperateText: boolean; 9 | addSectionText: string; 10 | renderPreviewCanvas: boolean; 11 | renderPreviewCanvasByUUID: string; 12 | renderGlyphPreviewCanvas: boolean; 13 | renderGlyphPreviewCanvasByUUID: string; 14 | renderStrokeGlyphPreviewCanvas: boolean; 15 | renderStrokeGlyphPreviewCanvasByUUID: string; 16 | renderRadicalGlyphPreviewCanvas: boolean; 17 | renderRadicalGlyphPreviewCanvasByUUID: string; 18 | renderCompGlyphPreviewCanvas: boolean; 19 | renderCompGlyphPreviewCanvasByUUID: string; 20 | renderGlyphSelection: boolean; 21 | renderGlyphSelectionByUUID: string; 22 | renderStrokeGlyphSelection: boolean; 23 | renderStrokeGlyphSelectionByUUID: string; 24 | renderRadicalGlyphSelection: boolean; 25 | renderRadicalGlyphSelectionByUUID: string; 26 | renderCompGlyphSelection: boolean; 27 | renderCompGlyphSelectionByUUID: string; 28 | renderGlyph: boolean; 29 | renderCharacter: boolean; 30 | updateGlyphView: boolean; 31 | updateCharacterView: boolean; 32 | updateGlyphInfoPreviewCanvasByUUID: string; 33 | updateCharacterInfoPreviewCanvasByUUID: string; 34 | renderCharacter_forceUpdate: boolean; 35 | renderGlyph_forceUpdate: boolean; 36 | refreshPlaygroundGridController: boolean; 37 | } 38 | 39 | const emitter = mitt() 40 | 41 | export { 42 | emitter 43 | } -------------------------------------------------------------------------------- /src/fontEditor/background/layoutGrid.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mapCanvasWidth, 3 | mapCanvasHeight, 4 | } from '../../utils/canvas' 5 | 6 | import { editCharacterFile } from '../stores/files' 7 | import { renderLayout } from '../../features/layout' 8 | 9 | /** 10 | * 绘制网格背景 11 | * @param canvas 画布 12 | * @param precision 精度 13 | */ 14 | /** 15 | * paint mesh background 16 | * @param canvas canvas 17 | * @param precision precision 18 | */ 19 | const layoutGrid = ( 20 | canvas: HTMLCanvasElement, 21 | ) => { 22 | const { dx, dy, centerSquareSize, size } = editCharacterFile.value.info.gridSettings 23 | const layoutTree = editCharacterFile.value.info.layoutTree 24 | const x1 = Math.round((size - centerSquareSize) / 2) + dx 25 | const x2 = Math.round((size - centerSquareSize) / 2 + centerSquareSize) + dx 26 | const y1 = Math.round((size - centerSquareSize) / 2) + dy 27 | const y2 = Math.round((size - centerSquareSize) / 2 + centerSquareSize) + dy 28 | const barycenter = [mapCanvasWidth(size / 2) + mapCanvasWidth(dx), mapCanvasHeight(size / 2) + mapCanvasHeight(dy)] 29 | const ctx = (canvas as unknown as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D 30 | 31 | ctx.strokeStyle = '#811616' 32 | ctx.lineWidth = 2 33 | 34 | // grid 35 | ctx.beginPath() 36 | ctx.moveTo(0, mapCanvasHeight(y1)) 37 | ctx.lineTo(mapCanvasWidth(size), mapCanvasHeight(y1)) 38 | ctx.stroke() 39 | ctx.closePath() 40 | 41 | ctx.beginPath() 42 | ctx.moveTo(0, mapCanvasHeight(y2)) 43 | ctx.lineTo(mapCanvasWidth(size), mapCanvasHeight(y2)) 44 | ctx.stroke() 45 | ctx.closePath() 46 | 47 | ctx.beginPath() 48 | ctx.moveTo(mapCanvasWidth(x1), 0) 49 | ctx.lineTo(mapCanvasWidth(x1),mapCanvasHeight(size)) 50 | ctx.stroke() 51 | ctx.closePath() 52 | 53 | ctx.beginPath() 54 | ctx.moveTo(mapCanvasWidth(x2), 0) 55 | ctx.lineTo(mapCanvasWidth(x2), mapCanvasHeight(size)) 56 | ctx.stroke() 57 | ctx.closePath() 58 | 59 | // center 60 | ctx.beginPath() 61 | ctx.strokeStyle = '#153063' 62 | ctx.rect( 63 | mapCanvasWidth(size / 2 - centerSquareSize / 4), 64 | mapCanvasHeight(size / 2 - centerSquareSize / 4), 65 | mapCanvasWidth(centerSquareSize / 2), 66 | mapCanvasHeight(centerSquareSize / 2), 67 | ) 68 | ctx.stroke() 69 | ctx.closePath() 70 | 71 | // barycenter 72 | ctx.fillStyle = '#153063' 73 | ctx.lineWidth = 0 74 | ctx.beginPath() 75 | ctx.arc( 76 | barycenter[0], 77 | barycenter[1], 78 | 20, 0, 2 * Math.PI 79 | ) 80 | ctx.closePath() 81 | ctx.fill() 82 | 83 | if (layoutTree.length) { 84 | // layout 85 | renderLayout( 86 | layoutTree, 87 | { x: 0, y: 0, w: size, h: size }, 88 | 1, 89 | { x1: x1, x2: x2, y1: y1, y2: y2, l: size }, 90 | canvas, 91 | ) 92 | } 93 | } 94 | 95 | export { 96 | layoutGrid, 97 | } -------------------------------------------------------------------------------- /src/fontEditor/background/mesh.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mapCanvasWidth, 3 | mapCanvasHeight, 4 | } from '../../utils/canvas' 5 | import { selectedFile } from '../stores/files' 6 | 7 | /** 8 | * 绘制网格背景 9 | * @param canvas 画布 10 | * @param precision 精度 11 | */ 12 | /** 13 | * paint mesh background 14 | * @param canvas canvas 15 | * @param precision precision 16 | */ 17 | const mesh = ( 18 | canvas: HTMLCanvasElement, 19 | precision: number, 20 | ) => { 21 | const { 22 | unitsPerEm, 23 | descender, 24 | } = selectedFile.value.fontSettings 25 | 26 | const fontWidth = 0.8 * unitsPerEm 27 | const fontHeight = 0.6 * unitsPerEm 28 | const gaps = 16 29 | 30 | const width = canvas.width 31 | const height = canvas.height 32 | const ctx: CanvasRenderingContext2D = canvas.getContext('2d') as CanvasRenderingContext2D 33 | ctx.fillStyle = '#dcdfe6' 34 | const deltaX = mapCanvasWidth(precision) 35 | const deltaY = mapCanvasHeight(precision) 36 | ctx.fillRect(0, 0, width, height) 37 | // for (let i = 0; i <= width; i += deltaX) { 38 | // ctx.beginPath() 39 | // ctx.moveTo(i, 0) 40 | // ctx.lineTo(i, height) 41 | // ctx.closePath() 42 | // ctx.stroke() 43 | // } 44 | // for (let i = 0; i <= height; i += deltaY) { 45 | // ctx.beginPath() 46 | // ctx.moveTo(0, i) 47 | // ctx.lineTo(width, i) 48 | // ctx.closePath() 49 | // ctx.stroke() 50 | // } 51 | 52 | const left = (width - mapCanvasWidth(fontWidth)) / 2 53 | const right = left + mapCanvasWidth(fontWidth) 54 | const top = height + mapCanvasHeight(descender) - mapCanvasHeight(fontHeight) 55 | const bold = [0, 8, 16] 56 | const bottom = top + mapCanvasHeight(fontHeight) 57 | for (let i = 0; i <= gaps; i++) { 58 | const x = left + mapCanvasWidth(fontWidth) / gaps * i 59 | ctx.strokeStyle = bold.indexOf(i) !== -1 ? '#811616' : '#cda2a2' 60 | ctx.beginPath() 61 | ctx.moveTo(x, top) 62 | ctx.lineTo(x, bottom) 63 | ctx.closePath() 64 | ctx.stroke() 65 | } 66 | for (let i = 0; i <= gaps; i++) { 67 | const y = top + mapCanvasHeight(fontHeight) / gaps * i 68 | ctx.strokeStyle = bold.indexOf(i) !== -1 ? '#811616' : '#cda2a2' 69 | ctx.beginPath() 70 | ctx.moveTo(left, y) 71 | ctx.lineTo(right, y) 72 | ctx.closePath() 73 | ctx.stroke() 74 | } 75 | } 76 | 77 | export { 78 | mesh 79 | } -------------------------------------------------------------------------------- /src/fontEditor/background/transparent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 绘制透明背景 3 | * @param canvas 画布 4 | */ 5 | 6 | import { mapCanvasWidth } from "../../utils/canvas" 7 | import { grid } from "../stores/global" 8 | 9 | /** 10 | * paint transparent background 11 | * @param canvas canvas 12 | */ 13 | const transparent = ( 14 | canvas: HTMLCanvasElement, 15 | ) => { 16 | const width = canvas.width 17 | const height = canvas.height 18 | const ctx: CanvasRenderingContext2D = canvas.getContext('2d') as CanvasRenderingContext2D 19 | const d = mapCanvasWidth(grid.precision) 20 | for (let i = 0; i < width; i+=2*d) { 21 | for (let j = 0; j < height; j+=2*d) { 22 | ctx.fillStyle = '#FFFFFF' 23 | ctx.fillRect(i, j, d, d) 24 | ctx.fillRect(i + d, j + d, d, d) 25 | ctx.fillStyle = '#EFEFEF' 26 | ctx.fillRect(i + d, j, d, d) 27 | ctx.fillRect(i, j + d, d, d) 28 | } 29 | } 30 | } 31 | 32 | export { 33 | transparent 34 | } -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/AddGlyphDialog.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {{ t('dialogs.addGlyphDialog.cancel') }} 80 | 81 | {{ t('dialogs.addGlyphDialog.confirm') }} 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/AddIconDialog.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {{ t('dialogs.addIconDialog.cancel') }} 99 | 100 | {{ t('dialogs.addIconDialog.confirm') }} 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/AddTextDialog.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 93 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {{ t('dialogs.addTextDialog.cancel') }} 106 | 107 | {{ t('dialogs.addTextDialog.confirm') }} 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/CloseFileTipDialog.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | 45 | 46 | {{ t('dialogs.tipsDialog.tip1') }} 47 | 48 | 49 | 50 | {{ t('dialogs.tipsDialog.cancel') }} 51 | 52 | {{ t('dialogs.tipsDialog.confirm') }} 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/ExportFileDialog.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 78 | 83 | 84 | {{ t('dialogs.exportDialog.exportMsg') }} 85 | 90 | {{ t('programming.character') }} 91 | 92 | 96 | {{ t('programming.stroke') }} 97 | 98 | 102 | {{ t('programming.radical') }} 103 | 104 | 108 | {{ t('programming.comp') }} 109 | 110 | 114 | {{ t('programming.glyph_comp') }} 115 | 116 | 117 | 118 | 119 | {{ t('dialogs.exportDialog.cancel') }} 120 | 121 | {{ t('dialogs.exportDialog.confirm') }} 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/ExportFontDialog.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 44 | 49 | 50 | 54 | {{ t('menus.tools.remove_overlap') }} 55 | 56 | 57 | 58 | 59 | {{ t('dialogs.exportDialog.cancel') }} 60 | 61 | {{ t('dialogs.exportDialog.confirm') }} 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/ExportFontDialog_tauri.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 43 | 44 | 48 | {{ t('menus.tools.remove_overlap') }} 49 | 50 | 51 | 52 | 53 | {{ t('dialogs.exportDialog.cancel') }} 54 | 55 | {{ t('dialogs.exportDialog.confirm') }} 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/LanguageSettingsDialog.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {{ t('dialogs.languageSettingsDialog.cancel') }} 65 | 66 | {{ t('dialogs.languageSettingsDialog.confirm') }} 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/SaveDialog.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 92 | 97 | 98 | {{ t('dialogs.exportDialog.exportMsg') }} 99 | 104 | {{ t('programming.character') }} 105 | 106 | 110 | {{ t('programming.stroke') }} 111 | 112 | 116 | {{ t('programming.radical') }} 117 | 118 | 122 | {{ t('programming.comp') }} 123 | 124 | 128 | {{ t('programming.glyph_comp') }} 129 | 130 | 131 | 132 | 133 | {{ t('dialogs.exportDialog.cancel') }} 134 | 135 | {{ t('dialogs.exportDialog.confirm') }} 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/SaveFileTipDialog.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 31 | 32 | {{ t('dialogs.tipsDialog.tip2') }} 33 | 34 | 35 | 36 | {{ t('dialogs.tipsDialog.cancel') }} 37 | 38 | {{ t('dialogs.tipsDialog.confirm') }} 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/TipsDialog.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 31 | 32 | {{ tips }} 33 | 34 | 35 | 36 | {{ t('dialogs.tipsDialog.cancel') }} 37 | 38 | {{ t('dialogs.tipsDialog.confirm') }} 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/copyCharacterDialog.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 80 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {{ t('dialogs.copyCharacterDialog.cancel') }} 93 | 94 | {{ t('dialogs.copyCharacterDialog.confirm') }} 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/copyGlyphDialog.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {{ t('dialogs.copyGlyphDialog.cancel') }} 72 | 73 | {{ t('dialogs.copyGlyphDialog.confirm') }} 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/editCharacterDialog.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {{ t('dialogs.editCharacterDialog.cancel') }} 79 | 80 | {{ t('dialogs.editCharacterDialog.confirm') }} 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/editGlyphDialog.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {{ t('dialogs.editGlyphDialog.cancel') }} 62 | 63 | {{ t('dialogs.copyGlyphDialog.confirm') }} 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/fontEditor/components/Dialogs/fontSettings/FontSettingsDialog.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {{ t('dialogs.fontSettingsDialog.cancel') }} 63 | updateFont()" 66 | > 67 | {{ t('dialogs.fontSettingsDialog.confirm') }} 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/fontEditor/components/LeftPanel/LeftPanel.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 44 | 49 | 56 | 62 | 63 | 70 | 76 | 77 | 78 | {{ t('panels.view') }} 79 | 80 | 81 | 82 | 88 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 120 | -------------------------------------------------------------------------------- /src/fontEditor/components/RightPanel/RightPanel.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 50 | 53 | 56 | 59 | 60 | 61 | 64 | 67 | 70 | 73 | 76 | 79 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/fontEditor/components/SideBar/SideBar.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 100 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 119 | {{ subMenu.label }} 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/fontEditor/components/TopBar/TopBar.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 97 | 98 | 99 | 104 | 105 | 106 | 107 | 108 | 109 | {{ menu.label }} 110 | 111 | 116 | {{ subMenu.label }} 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/fontEditor/components/Widgets/MetricsController.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 57 | 58 | 59 | 63 | advanceWidth 64 | 68 | 69 | 73 | lsb 74 | 78 | 79 | 83 | rsb 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/fontEditor/programming/Character.ts: -------------------------------------------------------------------------------- 1 | import { ICharacterFile, getCharacterRatioLayout } from '../stores/files' 2 | import { ICustomGlyph } from '../stores/glyph' 3 | import { getRatioCoords } from '../../features/layout' 4 | 5 | class Character { 6 | private _character: ICharacterFile 7 | 8 | constructor (character) { 9 | this._character = character 10 | character._o = this 11 | } 12 | 13 | public getComponent (name) { 14 | for (let i = 0; i < this._character.components.length; i++) { 15 | if (this._character.components[i].name === name) { 16 | return this._character.components[i] 17 | } 18 | } 19 | return null 20 | } 21 | 22 | public getGlyph (name) { 23 | for (let i = 0; i < this._character.components.length; i++) { 24 | if (this._character.components[i].name === name) { 25 | return (this._character.components[i].value as ICustomGlyph)._o 26 | } 27 | } 28 | return null 29 | } 30 | 31 | public getLayoutByID (id: string) { 32 | if (this._character.info && this._character.info.layoutTree) { 33 | const tree = this._character.info.layoutTree 34 | return getNodeByID(id, tree) 35 | } 36 | return null 37 | } 38 | 39 | public getCoords (layout, col, row, n) { 40 | const rect = layout.rect 41 | const { x, y, w, h } = rect 42 | const _x = x + w / n * col 43 | const _y = y + h / n * row 44 | return { 45 | x: _x, 46 | y: _y, 47 | } 48 | } 49 | 50 | public getRatioCoords (layout, col, row, n) { 51 | const { dx, dy, size, centerSquareSize } = (this._character as ICharacterFile).info.gridSettings 52 | const x1 = Math.round((size - centerSquareSize) / 2) + dx 53 | const x2 = Math.round((size - centerSquareSize) / 2 + centerSquareSize) + dx 54 | const y1 = Math.round((size - centerSquareSize) / 2) + dy 55 | const y2 = Math.round((size - centerSquareSize) / 2 + centerSquareSize) + dy 56 | return getRatioCoords(layout, col, row, n, { 57 | x1, x2, y1, y2, l: size, 58 | }) 59 | } 60 | 61 | public getRatioLayout (value) { 62 | return getCharacterRatioLayout(this._character, value) 63 | } 64 | } 65 | 66 | const getNodeByID = (id: string, tree: any) => { 67 | for (let i = 0; i < tree.length; i++) { 68 | const node = tree[i] 69 | if (node.id === id) { 70 | return node 71 | } else if (node.children) { 72 | const _node = getNodeByID(id, node.children) 73 | if (_node) { 74 | return _node 75 | } 76 | } 77 | } 78 | return null 79 | } 80 | 81 | export { 82 | Character, 83 | } -------------------------------------------------------------------------------- /src/fontEditor/programming/ConstantsMap.ts: -------------------------------------------------------------------------------- 1 | import { IConstant } from '../stores/glyph' 2 | 3 | class ConstantsMap { 4 | private constants: Array 5 | 6 | constructor (constants: Array) { 7 | this.constants = constants 8 | } 9 | 10 | public update (constants: Array) { 11 | this.constants = constants 12 | } 13 | 14 | public get (name: string) { 15 | for (let i = 0; i < this.constants.length; i++) { 16 | if (this.constants[i].name === name) { 17 | return this.constants[i].value 18 | } 19 | } 20 | } 21 | 22 | public getByUUID (uuid: string) { 23 | for (let i = 0; i < this.constants.length; i++) { 24 | if (this.constants[i].uuid === uuid) { 25 | return this.constants[i].value 26 | } 27 | } 28 | } 29 | } 30 | 31 | export { 32 | ConstantsMap, 33 | } -------------------------------------------------------------------------------- /src/fontEditor/programming/ParametersMap.ts: -------------------------------------------------------------------------------- 1 | import { IParameter, constantsMap, ParameterType } from '../stores/glyph' 2 | import { constantsMap as constantsMap_playground } from '../stores/playground' 3 | 4 | class ParametersMap { 5 | public parameters: Array 6 | 7 | constructor (parameters: Array) { 8 | this.parameters = parameters 9 | } 10 | 11 | public get (name: string) { 12 | for (let i = 0; i < this.parameters.length; i++) { 13 | if (this.parameters[i].name === name) { 14 | return this.getValue(this.parameters[i]) 15 | } 16 | } 17 | } 18 | 19 | public getRange (name: string) { 20 | for (let i = 0; i < this.parameters.length; i++) { 21 | if (this.parameters[i].name === name) { 22 | return { 23 | min: this.parameters[i].min || 0, 24 | max: this.parameters[i].max || 1000, 25 | } 26 | } 27 | } 28 | } 29 | 30 | public set (name: string, value: number) { 31 | for (let i = 0; i < this.parameters.length; i++) { 32 | const param = this.parameters[i] 33 | if (param.name === name) { 34 | if (value < param.min) { 35 | param.value = param.min 36 | } else if (value > param.max) { 37 | param.value = param.max 38 | } else { 39 | param.value = value 40 | } 41 | if (param.type === ParameterType.Constant) { 42 | param.type = ParameterType.Number 43 | } 44 | } 45 | } 46 | } 47 | 48 | public getByUUID (uuid: string) { 49 | for (let i = 0; i < this.parameters.length; i++) { 50 | if (this.parameters[i].uuid === uuid) { 51 | return this.getValue(this.parameters[i]) 52 | } 53 | } 54 | } 55 | 56 | public getValue (parameter: IParameter) { 57 | if (parameter.type === ParameterType.Number || parameter.type === ParameterType.RingController) return parameter.value 58 | else if (parameter.type === ParameterType.Constant) { 59 | return constantsMap.getByUUID(parameter.value as string) 60 | } 61 | else if (parameter.type === ParameterType.PlaygroundConstant) { 62 | // playground中需要调用playground store中存储的constantsMap 63 | return constantsMap_playground.getByUUID(parameter.value as string) 64 | } 65 | else if (parameter.type === ParameterType.Enum) { 66 | // 如果选择类型,返回相应的类型value标识 67 | return parameter.value 68 | } 69 | } 70 | } 71 | 72 | export { 73 | ParametersMap, 74 | } -------------------------------------------------------------------------------- /src/fontEditor/programming/global_contants.ts: -------------------------------------------------------------------------------- 1 | const default_unitsPerEm = 1000 2 | 3 | const globalConstants = { 4 | thick: 40, 5 | gap_0: 20, 6 | gap_1: 40, 7 | gap_2: 60, 8 | } 9 | 10 | const mapGlobalConstants = (global_constants, unitsPerEm) => { 11 | const map = {} 12 | Object.keys(global_constants).map((key) => { 13 | map[key] = global_constants[key] * unitsPerEm / default_unitsPerEm 14 | }) 15 | return map 16 | } 17 | 18 | export { 19 | mapGlobalConstants, 20 | globalConstants, 21 | } -------------------------------------------------------------------------------- /src/fontEditor/renderer/index.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from '../stores/system' 2 | import { tauri_handlers } from '../menus/handlers' 3 | import { Status, editStatus } from '../stores/font' 4 | import { watch } from 'vue' 5 | import { listen } from '@tauri-apps/api/event' 6 | import { invoke } from '@tauri-apps/api/core' 7 | 8 | const editStatusToString = (status: Status) => { 9 | if (editStatus.value === Status.Edit) { 10 | return 'edit' 11 | } else if (editStatus.value === Status.Glyph) { 12 | return 'glyph' 13 | } else if (editStatus.value === Status.Pic) { 14 | return 'pic' 15 | } 16 | return 'list' 17 | } 18 | 19 | watch(editStatus, () => { 20 | if (ENV.value === 'tauri') { 21 | invoke('toggle_menu_disabled', { editStatus: editStatusToString(editStatus.value) }) 22 | } 23 | }) 24 | 25 | const initTauri = () => { 26 | //@ts-ignore 27 | if (!!window.__TAURI_INTERNALS__) { 28 | ENV.value = 'tauri' 29 | const keys = Object.keys(tauri_handlers) 30 | for (let i = 0; i < keys.length; i++) { 31 | const key = keys[i] 32 | listen(key, (event) => { 33 | tauri_handlers[key]() 34 | }) 35 | } 36 | } 37 | } 38 | 39 | export { 40 | initTauri, 41 | } -------------------------------------------------------------------------------- /src/fontEditor/stores/ellipse.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了ellipse编辑时需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when ellipse editing 8 | */ 9 | 10 | // 是否正在编辑 11 | // whether on editing 12 | const editing: Ref = ref(false) 13 | const setEditing = (status: boolean) => { 14 | editing.value = status 15 | } 16 | 17 | // 椭圆起始位置x坐标 18 | // ellipse x coord 19 | const ellipseX: Ref = ref(-1) 20 | const setEllipseX = (value: number) => { 21 | ellipseX.value = value 22 | } 23 | 24 | // 椭圆起始位置y坐标 25 | // ellipse y coord 26 | const ellipseY: Ref = ref(-1) 27 | const setEllipseY = (value: number) => { 28 | ellipseY.value = value 29 | } 30 | 31 | // 椭圆x半径 32 | // ellipse x radius 33 | const radiusX: Ref = ref(0) 34 | const setRadiusX = (value: number) => { 35 | radiusX.value = value 36 | } 37 | 38 | // 椭圆y半径 39 | // ellipse y radius 40 | const radiusY: Ref = ref(0) 41 | const setRadiusY = (value: number) => { 42 | radiusY.value = value 43 | } 44 | 45 | export { 46 | editing, 47 | ellipseX, 48 | ellipseY, 49 | radiusX, 50 | radiusY, 51 | setEditing, 52 | setEllipseX, 53 | setEllipseY, 54 | setRadiusX, 55 | setRadiusY, 56 | } 57 | -------------------------------------------------------------------------------- /src/fontEditor/stores/global.ts: -------------------------------------------------------------------------------- 1 | import { ref, reactive, type Ref } from 'vue' 2 | 3 | export enum GridType { 4 | None, 5 | Mesh, 6 | Mi, 7 | LayoutGrid, 8 | } 9 | 10 | export enum BackgroundType { 11 | Transparent, 12 | Color, 13 | } 14 | 15 | export interface IGrid { 16 | precision: number; 17 | type: GridType, 18 | } 19 | 20 | export interface IBackground { 21 | type: BackgroundType, 22 | color: string, 23 | } 24 | 25 | const base = ''//'/fontplayer_demo' 26 | 27 | const useSkeletonGrid = ref(false) 28 | 29 | const jointsCheckedMap = ref({}) 30 | 31 | const draggable = ref(true) 32 | const dragOption = ref('default') 33 | const checkJoints = ref(false) 34 | const checkRefLines = ref(false) 35 | 36 | const fontRenderStyle: Ref = ref('contour') 37 | 38 | const tips = ref('') 39 | 40 | const tool: Ref = ref('') 41 | const width: Ref = ref(500) 42 | const height: Ref = ref(500) 43 | const canvas: Ref = ref(null) 44 | 45 | let grid: IGrid = reactive({ 46 | type: GridType.None, 47 | precision: 20, 48 | }) 49 | 50 | let background: IBackground = reactive({ 51 | type: BackgroundType.Transparent, 52 | color: '', 53 | }) 54 | 55 | const gridSettings = ref({ 56 | dx: 0, 57 | dy: 0, 58 | centerSquareSize: 1000 / 3, 59 | size: 1000, 60 | default: true, 61 | }) 62 | 63 | const layoutOptions = ref([ 64 | { 65 | value: '左右', 66 | label: '左右', 67 | layout: '左<0,x1+0.5*(x2-x1)>右', 68 | subLayout: '左右', 69 | }, 70 | { 71 | value: '左中右', 72 | label: '左中右', 73 | layout: '左<0,l/3)>中右<2*l/3,l>', 74 | subLayout: '左中右', 75 | }, 76 | { 77 | value: '上下', 78 | label: '上下', 79 | layout: '上<0,x1+0.5*(y2-y1)>下', 80 | subLayout: '上右', 81 | }, 82 | { 83 | value: '上中下', 84 | label: '上中下', 85 | layout: '上<0,l/3)>中下<2*l/3,l>', 86 | subLayout: '上中下', 87 | }, 88 | { 89 | value: '独体字', 90 | label: '独体字', 91 | layout: '独体字', 92 | subLayout: '独体字', 93 | }, 94 | ]) 95 | 96 | const templates = [ 97 | { 98 | name: '春晓', 99 | path: '/templates/chun_xiao.json' 100 | } 101 | ] 102 | 103 | const setTool = (item: string) => { 104 | tool.value = item 105 | } 106 | const setWidth = (value: number) => { 107 | width.value = value 108 | } 109 | const setHeight = (value: number) => { 110 | height.value = value 111 | } 112 | const setBackgroundType = (type: BackgroundType) => { 113 | background.type = type 114 | } 115 | const setBackgroundColor = (color: string) => { 116 | background.color = color 117 | } 118 | const setGridType = (type: GridType) => { 119 | grid.type = type 120 | } 121 | const setGridPrecision = (precision: number) => { 122 | grid.precision = precision 123 | } 124 | const setCanvas = (value: HTMLCanvasElement) => { 125 | canvas.value = value 126 | } 127 | 128 | const loading = ref(false) 129 | const loaded = ref(0) 130 | const total = ref(100) 131 | 132 | const gridChanged = ref(false) 133 | 134 | export { 135 | tool, 136 | grid, 137 | canvas, 138 | width, 139 | height, 140 | background, 141 | setTool, 142 | setWidth, 143 | setHeight, 144 | setBackgroundType, 145 | setBackgroundColor, 146 | setGridType, 147 | setGridPrecision, 148 | setCanvas, 149 | fontRenderStyle, 150 | loading, 151 | gridSettings, 152 | layoutOptions, 153 | templates, 154 | draggable, 155 | dragOption, 156 | checkJoints, 157 | checkRefLines, 158 | loaded, 159 | total, 160 | tips, 161 | jointsCheckedMap, 162 | gridChanged, 163 | useSkeletonGrid, 164 | base, 165 | } -------------------------------------------------------------------------------- /src/fontEditor/stores/glyphDragger.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, type Ref } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了字形拖拽时需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when glyph dragging 8 | */ 9 | 10 | // 是否正在编辑 11 | // whether on editing 12 | const editing: Ref = ref(false) 13 | const setEditing = (status: boolean) => { 14 | editing.value = status 15 | } 16 | 17 | const draggingJoint = ref(null) 18 | 19 | const putAtCoord = ref(null) 20 | 21 | const movingJoint = ref(null) 22 | 23 | export { 24 | editing, 25 | setEditing, 26 | draggingJoint, 27 | putAtCoord, 28 | movingJoint, 29 | } -------------------------------------------------------------------------------- /src/fontEditor/stores/glyphDragger_glyph.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, type Ref } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了字形拖拽时需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when glyph dragging 8 | */ 9 | 10 | // 是否正在编辑 11 | // whether on editing 12 | const editing: Ref = ref(false) 13 | const setEditing = (status: boolean) => { 14 | editing.value = status 15 | } 16 | 17 | const draggingJoint = ref(null) 18 | 19 | const putAtCoord = ref(null) 20 | 21 | const movingJoint = ref(null) 22 | 23 | export { 24 | editing, 25 | setEditing, 26 | draggingJoint, 27 | putAtCoord, 28 | movingJoint, 29 | } -------------------------------------------------------------------------------- /src/fontEditor/stores/glyphLayoutResizer.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, type Ref } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了字形结构变换时需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when glyph layout resizing 8 | */ 9 | 10 | // 是否正在编辑 11 | // whether on editing 12 | const editing: Ref = ref(false) 13 | const setEditing = (status: boolean) => { 14 | editing.value = status 15 | } 16 | 17 | const selectControl = ref('') 18 | 19 | export { 20 | editing, 21 | setEditing, 22 | selectControl, 23 | } 24 | -------------------------------------------------------------------------------- /src/fontEditor/stores/glyphLayoutResizer_glyph.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, type Ref } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了字形结构变换时需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when glyph layout resizing 8 | */ 9 | 10 | // 是否正在编辑 11 | // whether on editing 12 | const editing: Ref = ref(false) 13 | const setEditing = (status: boolean) => { 14 | editing.value = status 15 | } 16 | 17 | const selectControl = ref('') 18 | 19 | export { 20 | editing, 21 | setEditing, 22 | selectControl, 23 | } 24 | -------------------------------------------------------------------------------- /src/fontEditor/stores/pen.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref, reactive } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了钢笔工具需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when pen editing 8 | */ 9 | 10 | // 点的数据结构 11 | // point data struct 12 | export interface IPoint { 13 | uuid: string; 14 | x: number; 15 | y: number; 16 | type: string; 17 | origin: string | null; 18 | isShow?: boolean; 19 | } 20 | 21 | export interface IPoints { 22 | value: Array; 23 | } 24 | 25 | // 是否正在编辑钢笔路径 26 | // whether on editing 27 | const editing: Ref = ref(false) 28 | const setEditing = (status: boolean) => { 29 | editing.value = status 30 | } 31 | 32 | // 钢笔路径包含的点 33 | // points for pen path 34 | const points: IPoints = reactive({ 35 | value: [] 36 | }) 37 | const setPoints = (value: Array) => { 38 | points.value = value 39 | } 40 | 41 | const mousedown = ref(false) 42 | const mousemove = ref(false) 43 | 44 | export { editing, points, setEditing, setPoints, mousedown, mousemove } -------------------------------------------------------------------------------- /src/fontEditor/stores/polygon.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref, reactive } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了多边形工具需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when polygon editing 8 | */ 9 | 10 | // 点的数据结构 11 | // point data struct 12 | export interface IPoint { 13 | uuid: string; 14 | x: number; 15 | y: number; 16 | } 17 | 18 | export interface IPoints { 19 | value: Array; 20 | } 21 | 22 | // 是否正在编辑多边形 23 | // whether on editing 24 | const editing: Ref = ref(false) 25 | const setEditing = (status: boolean) => { 26 | editing.value = status 27 | } 28 | 29 | // 多边形路径包含的点 30 | // points for polygon path 31 | const points: IPoints = reactive({ 32 | value: [] 33 | }) 34 | const setPoints = (value: Array) => { 35 | points.value = value 36 | } 37 | 38 | const mousedown = ref(false) 39 | const mousemove = ref(false) 40 | 41 | export { editing, points, setEditing, setPoints, mousedown, mousemove } 42 | -------------------------------------------------------------------------------- /src/fontEditor/stores/rectangle.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue' 2 | 3 | /** 4 | * 此store文件包含了长方形工具需要的一些信息 5 | */ 6 | /** 7 | * this store file contains basic info used when rectangle editing 8 | */ 9 | 10 | // 是否正在编辑长方形 11 | // whether on editing 12 | const editing: Ref = ref(false) 13 | const setEditing = (status: boolean) => { 14 | editing.value = status 15 | } 16 | 17 | // 长方形起始位置x坐标 18 | // rectangle x coord 19 | const rectX: Ref = ref(-1) 20 | const setRectX = (value: number) => { 21 | rectX.value = value 22 | } 23 | 24 | // 长方形起始位置y坐标 25 | // rectangle y coord 26 | const rectY: Ref = ref(-1) 27 | const setRectY = (value: number) => { 28 | rectY.value = value 29 | } 30 | 31 | // 长方形宽度 32 | // rectangle width 33 | const rectWidth: Ref = ref(0) 34 | const setRectWidth = (value: number) => { 35 | rectWidth.value = value 36 | } 37 | 38 | // 长方形高度 39 | // rectangle height 40 | const rectHeight: Ref = ref(0) 41 | const setRectHeight= (value: number) => { 42 | rectHeight.value = value 43 | } 44 | 45 | export { 46 | editing, 47 | rectX, 48 | rectY, 49 | rectWidth, 50 | rectHeight, 51 | setEditing, 52 | setRectX, 53 | setRectY, 54 | setRectWidth, 55 | setRectHeight, 56 | } 57 | -------------------------------------------------------------------------------- /src/fontEditor/stores/select.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue' 2 | 3 | /** 4 | * 该store文件包含一些编辑字符图形时,尤其是选择操作(包括选择控制点、区域等)时所用的信息 5 | */ 6 | /** 7 | * this store file contain some basic info for select operation on editing 8 | */ 9 | 10 | const selectControl: Ref = ref('null') 11 | const setSelectControl = (value: string) => { 12 | selectControl.value = value 13 | } 14 | 15 | const selectAnchor: Ref = ref('') 16 | const setSelectAnchor = (uuid: string) => { 17 | selectAnchor.value = uuid 18 | } 19 | 20 | const selectPenPoint: Ref = ref('') 21 | const setSelectPenPoint = (uuid: string) => { 22 | selectPenPoint.value = uuid 23 | } 24 | 25 | const hoverPenPoint: Ref = ref('') 26 | const setHoverPenPoint = (uuid: string) => { 27 | hoverPenPoint.value = uuid 28 | } 29 | 30 | const onPenEditMode: Ref = ref(false) 31 | const setOnPenEditMode = (onEdit: boolean) => { 32 | onPenEditMode.value = onEdit 33 | } 34 | 35 | export { 36 | selectControl, 37 | selectAnchor, 38 | selectPenPoint, 39 | hoverPenPoint, 40 | onPenEditMode, 41 | setSelectControl, 42 | setSelectPenPoint, 43 | setSelectAnchor, 44 | setHoverPenPoint, 45 | setOnPenEditMode, 46 | } 47 | -------------------------------------------------------------------------------- /src/fontEditor/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { selectedFile } from "./files"; 3 | import { hasChineseChar } from "@/fontManager/utils"; 4 | import { convertToPinyin } from "tiny-pinyin"; 5 | 6 | const head_data = ref({ 7 | majorVersion: 0x0001, 8 | minorVersion: 0x0000, 9 | fontRevision: 0x00010000, 10 | flags: [ 11 | true, 12 | true, 13 | false, 14 | false, 15 | false, 16 | false, 17 | false, 18 | false, 19 | false, 20 | false, 21 | false, 22 | false, 23 | false, 24 | false, 25 | false, 26 | false, 27 | ], 28 | created: { 29 | timestamp: Math.floor(Date.now() / 1000) + 2082844800, 30 | value: Date.now(), 31 | }, 32 | modified: { 33 | timestamp: Math.floor(Date.now() / 1000) + 2082844800, 34 | value: Date.now() 35 | }, 36 | macStyle: [ 37 | false, 38 | false, 39 | false, 40 | false, 41 | false, 42 | false, 43 | false, 44 | false, 45 | false, 46 | false, 47 | false, 48 | false, 49 | false, 50 | false, 51 | false, 52 | false, 53 | ], 54 | lowestRecPPEM: 7, 55 | fontDirectionHint: 2, 56 | }) 57 | 58 | const hhea_data = ref({ 59 | majorVersion: 0x0001, 60 | minorVersion: 0x0000, 61 | lineGap: 0, 62 | caretSlopeRise: 1, 63 | caretSlopeRun: 0, 64 | caretOffset: 0, 65 | }) 66 | 67 | const getEnName = (name: string) => { 68 | let enName = name 69 | if (hasChineseChar(name)) { 70 | enName = convertToPinyin(name) 71 | } 72 | return enName 73 | } 74 | 75 | const name_data = ref([]) 76 | 77 | const os2_data = ref({ 78 | version: 0x0005, 79 | usWeightClass: 400, 80 | usWidthClass: 5, 81 | fsType: 0, 82 | ySubscriptXSize: 650, 83 | ySubscriptYSize: 699, 84 | ySubscriptXOffset: 0, 85 | ySubscriptYOffset: 140, 86 | ySuperscriptXSize: 650, 87 | ySuperscriptYSize: 699, 88 | ySuperscriptXOffset: 0, 89 | ySuperscriptYOffset: 479, 90 | yStrikeoutSize: 49, 91 | yStrikeoutPosition: 258, 92 | sFamilyClass: 0, 93 | panose: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 94 | achVendID: 'UKWN', 95 | fsSelection: [ 96 | false, 97 | false, 98 | false, 99 | false, 100 | false, 101 | false, 102 | true, 103 | false, 104 | false, 105 | false, 106 | ], 107 | // usDefaultChar: hasChar(font, ' ') ? 32 : 0, 108 | // usBreakChar: hasChar(font, ' ') ? 32 : 0, 109 | usMaxContext: 0, 110 | usLowerOpticalPointSize: 8, 111 | usUpperOpticalPointSize: 72, 112 | }) 113 | 114 | const post_data = ref({ 115 | version: 0x00030000, 116 | italicAngle: 0, 117 | underlinePosition: 0, 118 | underlineThickness: 0, 119 | isFixedPitch: 1, 120 | minMemType42: 0, 121 | maxMemType42: 0, 122 | minMemType1: 0, 123 | maxMemType1: 0, 124 | }) 125 | 126 | const metrics_data = ref({ 127 | advanceWidth: 1000, 128 | lsb: 0, 129 | rsb: 0, 130 | xMin: 0, 131 | yMin: 0, 132 | xMax: 1000, 133 | yMax: 1000, 134 | }) 135 | 136 | export { 137 | head_data, 138 | hhea_data, 139 | name_data, 140 | os2_data, 141 | post_data, 142 | metrics_data, 143 | getEnName, 144 | } -------------------------------------------------------------------------------- /src/fontEditor/stores/system.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | // 当前环境,'web'为网页,'tauri'为Tauri桌面应用 4 | // current environment, 'web' for web page, 'tauri' for Tauri 5 | const ENV = ref('web') 6 | 7 | const LOADING = ref(true) 8 | 9 | export { 10 | ENV, 11 | LOADING, 12 | } -------------------------------------------------------------------------------- /src/fontEditor/tools/coordsViewer.ts: -------------------------------------------------------------------------------- 1 | import { selectedFile } from '../stores/files' 2 | import { width, height } from '../stores/global' 3 | import { coordsText } from '../stores/font' 4 | 5 | // 坐标查看工具初始化方法 6 | // initializer for coords viewer tool 7 | const initCoordsViewer = (canvas: HTMLCanvasElement, glyph: boolean = false) => { 8 | const onMouseMove = (e: MouseEvent) => { 9 | if (!glyph) { 10 | const unitsPerEm = selectedFile.value.fontSettings.unitsPerEm 11 | const coordX = e.offsetX / width.value * unitsPerEm 12 | const coordY = e.offsetY / height.value * unitsPerEm 13 | coordsText.value = `${coordX}, ${coordY}` 14 | } else { 15 | const unitsPerEm = 1000 16 | const coordX = e.offsetX / width.value * unitsPerEm 17 | const coordY = e.offsetY / height.value * unitsPerEm 18 | coordsText.value = `${coordX}, ${coordY}` 19 | } 20 | } 21 | canvas?.addEventListener('mousemove', onMouseMove) 22 | const closeMover = () => { 23 | canvas?.removeEventListener('mousemove', onMouseMove) 24 | } 25 | return closeMover 26 | } 27 | 28 | export { 29 | initCoordsViewer, 30 | } -------------------------------------------------------------------------------- /src/fontEditor/tools/picture.ts: -------------------------------------------------------------------------------- 1 | import { toPixels } from '../../features/image' 2 | import type { IComponent } from '../../fontEditor/stores/files' 3 | import { genUUID } from "../../utils/string" 4 | 5 | 6 | // 生成图片组件 7 | // generate picture component 8 | const genPictureComponent = async (data: string, maxWidth: number, maxHeight: number): Promise => { 9 | return new Promise((resolve, reject) => { 10 | const originImg = document.createElement('img') 11 | originImg.onload = () => { 12 | let w = originImg.width 13 | let h = originImg.height 14 | if (w > maxWidth || h > maxHeight) { 15 | if (h / maxHeight > w / maxWidth) { 16 | h = maxHeight 17 | w = originImg.width / originImg.height * h 18 | } else { 19 | w = maxWidth 20 | h= originImg.height / originImg.width * w 21 | } 22 | } 23 | const canvas = document.createElement('canvas') 24 | canvas.width = w 25 | canvas.height = h 26 | const ctx =canvas.getContext('2d') as CanvasRenderingContext2D 27 | ctx.drawImage(originImg, 0, 0, originImg.width, originImg.height, 0, 0, w, h) 28 | const img = document.createElement('img') 29 | const imgData = canvas.toDataURL() 30 | img.onload = () => { 31 | const pixels = toPixels(img) 32 | const component = { 33 | uuid: genUUID(), 34 | type: 'picture', 35 | name: 'picture', 36 | lock: false, 37 | visible: true, 38 | value: { 39 | data: imgData, 40 | img, 41 | pixels, 42 | originImg, 43 | pixelMode: false, 44 | }, 45 | x: 0, 46 | y: 0, 47 | w, 48 | h, 49 | rotation: 0, 50 | flipX: false, 51 | flipY: false, 52 | opacity: 0.5, 53 | usedInCharacter: false, 54 | } 55 | //@ts-ignore 56 | resolve(component) 57 | } 58 | img.src = imgData 59 | } 60 | originImg.src = data 61 | }) 62 | } 63 | 64 | export { 65 | genPictureComponent, 66 | } -------------------------------------------------------------------------------- /src/fontEditor/tools/translateMover.ts: -------------------------------------------------------------------------------- 1 | import { editCharacterFile, modifyCharacterFile } from '../../fontEditor/stores/files' 2 | import { editGlyph, modifyGlyph } from '../../fontEditor/stores/glyph' 3 | import { getCoord } from '../../utils/canvas' 4 | 5 | // 画布移动工具初始化方法 6 | // initializer for canvas moving tool 7 | const initTranslateMover = (canvas: HTMLCanvasElement, glyph: boolean = false) => { 8 | const { translateX, translateY } = glyph ? editGlyph.value.view : editCharacterFile.value.view 9 | let mouseDownX = -1 10 | let mouseDownY = -1 11 | const onMouseDown = (e: MouseEvent) => { 12 | mouseDownX = e.clientX 13 | mouseDownY = e.clientY 14 | document.addEventListener('mousemove', onMouseMove) 15 | document.addEventListener('mouseup', onMouseUp) 16 | } 17 | const onMouseMove = (e: MouseEvent) => { 18 | if (!glyph) { 19 | modifyCharacterFile(editCharacterFile.value.uuid, { 20 | view: { 21 | translateX: translateX + getCoord(e.clientX - mouseDownX), 22 | translateY: translateY + getCoord(e.clientY - mouseDownY), 23 | } 24 | }) 25 | } else { 26 | modifyGlyph(editGlyph.value.uuid, { 27 | view: { 28 | translateX: translateX + getCoord(e.clientX - mouseDownX), 29 | translateY: translateY + getCoord(e.clientY - mouseDownY), 30 | } 31 | }) 32 | } 33 | } 34 | const onMouseUp = (e: MouseEvent) => { 35 | document.removeEventListener('mousemove', onMouseMove) 36 | document.removeEventListener('mouseup', onMouseUp) 37 | } 38 | canvas?.addEventListener('mousedown', onMouseDown) 39 | const closeMover = () => { 40 | canvas?.removeEventListener('mousedown', onMouseDown) 41 | } 42 | return closeMover 43 | } 44 | 45 | export { 46 | initTranslateMover, 47 | } -------------------------------------------------------------------------------- /src/fontEditor/worker/index.ts: -------------------------------------------------------------------------------- 1 | const initWorker = () => { 2 | let worker = null 3 | if (window.Worker) { 4 | worker = new Worker(new URL('./worker.ts', import.meta.url)) 5 | } 6 | return worker 7 | } 8 | 9 | enum WorkerEventType { 10 | ParseFont, 11 | } 12 | 13 | export { 14 | initWorker, 15 | WorkerEventType, 16 | } -------------------------------------------------------------------------------- /src/fontEditor/worker/shim.ts: -------------------------------------------------------------------------------- 1 | self.document = { 2 | //@ts-ignore 3 | querySelectorAll(){return []}, 4 | //@ts-ignore 5 | createElement(){ 6 | return { 7 | setAttribute: () => {}, 8 | addEventListener: () => {}, 9 | } 10 | }, 11 | addEventListener: () => {}, 12 | head: { 13 | //@ts-ignore 14 | appendChild: () => {}, 15 | }, 16 | //@ts-ignore 17 | queryCommandSupported: () => {}, 18 | } 19 | 20 | //@ts-ignore 21 | self.UIEvent = class {} 22 | 23 | self.window = { 24 | //@ts-ignore 25 | navigator: { 26 | userAgent: '', 27 | }, 28 | //@ts-ignore 29 | location: { 30 | href: '', 31 | } 32 | } -------------------------------------------------------------------------------- /src/fontManager/decode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * decode: 用于对字体数据进行解码 3 | */ 4 | /** 5 | * decode: used for decode font data 6 | */ 7 | 8 | let data: DataView 9 | let offset: number 10 | 11 | /** 12 | * 启动新的解码 13 | * @param _data 字体文件DataView数据 14 | * @param _offset 当前需要解码的偏移位置 15 | */ 16 | /** 17 | * start decode 18 | * @param _data font data, type of DataView 19 | * @param _offset decoded offset 20 | */ 21 | const start = (_data: DataView, _offset: number) => { 22 | data = _data 23 | offset = _offset 24 | } 25 | 26 | /** 27 | * 结束解码 28 | */ 29 | /** 30 | * stop decode 31 | */ 32 | const end = () => { 33 | data = new DataView(new ArrayBuffer(0)) 34 | offset = 0 35 | } 36 | 37 | /** 38 | * 获取当前位置 39 | * @returns offset 40 | */ 41 | /** 42 | * get current offset 43 | * @returns offset 44 | */ 45 | const getOffset = () => { 46 | return offset 47 | } 48 | 49 | /** 50 | * 设置位置 51 | */ 52 | /** 53 | * set current offset 54 | */ 55 | const setOffset = (_offset: number) => { 56 | offset = _offset 57 | } 58 | 59 | /** 60 | * 解码器,包含不同数据类型的解码方法 61 | */ 62 | /** 63 | * decoder, contains decode methods for each different data type 64 | */ 65 | const decoder = { 66 | uint8: () => { 67 | const value = data.getUint8(offset) 68 | offset += 1 69 | return value 70 | }, 71 | int8: () => { 72 | const value = data.getInt8(offset) 73 | offset += 1 74 | return value 75 | }, 76 | uint16: () => { 77 | const value = data.getUint16(offset) 78 | offset += 2 79 | return value 80 | }, 81 | int16: () => { 82 | const value = data.getInt16(offset) 83 | offset += 2 84 | return value 85 | }, 86 | uint32: () => { 87 | const value = data.getUint32(offset) 88 | offset += 4 89 | return value 90 | }, 91 | int32: () => { 92 | const value = data.getInt32(offset) 93 | offset += 4 94 | return value 95 | }, 96 | bigInt: () => { 97 | const value = data.getBigInt64(offset) 98 | offset += 8 99 | return value 100 | }, 101 | Fixed: () => { 102 | return decoder.int32() 103 | }, 104 | FWORD: () => { 105 | return decoder.int16() 106 | }, 107 | UFWORD: () => { 108 | return decoder.uint16() 109 | }, 110 | F2DOT14: () => { 111 | const v = data.getInt16(offset) / 16384 112 | offset += 2 113 | return v 114 | }, 115 | LONGDATETIME: () => { 116 | const value = Number(data.getBigInt64(offset)) 117 | offset += 8 118 | return value 119 | }, 120 | Tag: () => { 121 | const tagArr: Array = [] 122 | let tagStr: string = '' 123 | for (let i = 0; i < 4; i++) { 124 | tagArr.push(data.getUint8(offset + i)) 125 | tagStr += String.fromCharCode(tagArr[i]) 126 | } 127 | offset += 4 128 | return { 129 | tagArr, tagStr, 130 | } 131 | }, 132 | Offset16: () => { 133 | return decoder.uint16() 134 | }, 135 | Offset32: () => { 136 | return decoder.uint32() 137 | }, 138 | Version16Dot16: () => { 139 | const major = data.getUint16(offset) 140 | const minor = data.getUint16(offset + 2) 141 | offset += 4 142 | return major + minor / 0x1000 / 10 143 | }, 144 | Card8: () => { 145 | return decoder.uint8() 146 | }, 147 | Card16: () => { 148 | return decoder.uint16() 149 | }, 150 | OffSize: () => { 151 | return decoder.uint8() 152 | }, 153 | SID: () => { 154 | return decoder.uint16() 155 | }, 156 | } 157 | 158 | export { 159 | start, 160 | end, 161 | decoder, 162 | getOffset, 163 | setOffset, 164 | } -------------------------------------------------------------------------------- /src/fontManager/index.ts: -------------------------------------------------------------------------------- 1 | import type { IOption } from './font' 2 | import { parseFont, createFont, toArrayBuffer, hasChar } from './font' 3 | import type { ITable } from './table' 4 | import { PathType, drawByOption } from './character' 5 | import type { 6 | ICharacter, 7 | ILine, 8 | ICubicBezierCurve, 9 | IQuadraticBezierCurve, 10 | IPoint, 11 | } from './character' 12 | 13 | /** 14 | * 通过指定url解析字体 15 | * @param url 字体文件对应的url 16 | * @returns font对象 17 | */ 18 | /** 19 | * parse font data by url 20 | * @param url font url 21 | * @returns font object 22 | */ 23 | const parseUrl = async (url: string) => { 24 | const res = await fetch(url) 25 | const buffer = await res.arrayBuffer() 26 | return parse(buffer) 27 | } 28 | 29 | /** 30 | * 通过ArrayBuffer解析字体数据 31 | * @param buffer ArrayBuffer 32 | * @returns font对象 33 | */ 34 | /** 35 | * parse font data by ArrayBuffer 36 | * @param buffer ArrayBuffer 37 | * @returns font object 38 | */ 39 | const parse = (buffer: ArrayBuffer) => { 40 | return parseFont(buffer) 41 | } 42 | 43 | const getBytes = (data: ArrayBuffer | Array, tables: Array) => { 44 | return tables.map((table) => { 45 | return { 46 | name: table.name, 47 | bytes: data.slice(table.config.offset, table.config.offset + table.config.length) 48 | } 49 | }) 50 | } 51 | 52 | /** 53 | * 创建字体 54 | * @param characters 字符数组 55 | * @param options 配置选项 56 | * @returns font对象 57 | */ 58 | const create = (characters: Array, options: IOption) => { 59 | return createFont(characters, options) 60 | } 61 | 62 | export { 63 | parse, 64 | parseUrl, 65 | create, 66 | toArrayBuffer, 67 | hasChar, 68 | getBytes, 69 | PathType, 70 | drawByOption 71 | } 72 | 73 | export type { 74 | ITable, 75 | ICharacter, 76 | ILine, 77 | ICubicBezierCurve, 78 | IQuadraticBezierCurve, 79 | IPoint, 80 | } -------------------------------------------------------------------------------- /src/fontManager/table.ts: -------------------------------------------------------------------------------- 1 | import type { IHeadTable } from './tables/head' 2 | import type { IHheaTable } from './tables/hhea' 3 | import type { IOS2Table } from './tables/os_2' 4 | import type { IMaxpTable } from './tables/maxp' 5 | import type { INameTable } from './tables/name' 6 | import type { IPostTable } from './tables/post' 7 | import type { ICmapTable } from './tables/cmap' 8 | import type { IHmtxTable } from './tables/hmtx' 9 | import type { IGlyfTable } from './tables/glyf' 10 | import type { ILocaTable } from './tables/loca' 11 | import type { ICffTable } from './tables/cff' 12 | import * as headTable from './tables/head' 13 | import * as hheaTable from './tables/hhea' 14 | import * as os2Table from './tables/os_2' 15 | import * as maxpTable from './tables/maxp' 16 | import * as nameTable from './tables/name' 17 | import * as postTable from './tables/post' 18 | import * as cmapTable from './tables/cmap' 19 | import * as hmtxTable from './tables/hmtx' 20 | import * as glyfTable from './tables/glyf' 21 | import * as locaTable from './tables/loca' 22 | import * as cffTable from './tables/cff' 23 | 24 | /** 25 | * 所有表的工具(通常包含parse和create方法)集合 26 | */ 27 | /** 28 | * set for all table tool 29 | */ 30 | const tableTool: any = { 31 | 'head': headTable, 32 | 'hhea': hheaTable, 33 | 'OS/2': os2Table, 34 | 'maxp': maxpTable, 35 | 'name': nameTable, 36 | 'post': postTable, 37 | 'cmap': cmapTable, 38 | 'hmtx': hmtxTable, 39 | 'glyf': glyfTable, 40 | 'loca': locaTable, 41 | 'CFF ': cffTable, 42 | } 43 | 44 | type ITableType = 'IHeadTable | IHheaTable | IOS2Table | IMaxpTable | INameTable | IPostTable | ICmapTable | IHmtxTable | IGlyfTable | ILocaTable | ICffTable' | null 45 | 46 | // table 数据类型 47 | // table data type 48 | interface ITable { 49 | name: string; 50 | table: ITableType, 51 | config: ITableConfig; 52 | } 53 | 54 | interface ITableConfig { 55 | tableTag: { 56 | tagArr?: Array, 57 | tagStr: string, 58 | }; 59 | checkSum: number; 60 | offset: number; 61 | length: number; 62 | } 63 | 64 | export { 65 | tableTool, 66 | } 67 | 68 | export type { 69 | ITable, 70 | } -------------------------------------------------------------------------------- /src/fontManager/tables/head.ts: -------------------------------------------------------------------------------- 1 | import type { IFont } from '../font' 2 | import { encoder } from '../encode' 3 | import * as decode from '../decode' 4 | 5 | // head表格式 6 | // head table format 7 | interface IHeadTable { 8 | majorVersion?: number; 9 | minorVersion?: number; 10 | fontRevision?: number; 11 | checkSumAdjustment?: number; 12 | magicNumber?: number; 13 | flags?: number; 14 | unitsPerEm?: number; 15 | created?: number; 16 | modified?: number; 17 | xMin?: number; 18 | yMin?: number; 19 | xMax?: number; 20 | yMax?: number; 21 | macStyle?: number; 22 | lowestRecPPEM?: number; 23 | fontDirectionHint?: number; 24 | indexToLocFormat?: number; 25 | glyphDataFormat?: number; 26 | } 27 | 28 | // head表数据类型 29 | // head table data type 30 | const types = { 31 | majorVersion: 'uint16', 32 | minorVersion: 'uint16', 33 | fontRevision: 'Fixed', 34 | checkSumAdjustment: 'uint32', 35 | magicNumber: 'uint32', 36 | flags: 'uint16', 37 | unitsPerEm: 'uint16', 38 | created: 'LONGDATETIME', 39 | modified: 'LONGDATETIME', 40 | xMin: 'int16', 41 | yMin: 'int16', 42 | xMax: 'int16', 43 | yMax: 'int16', 44 | macStyle: 'uint16', 45 | lowestRecPPEM: 'uint16', 46 | fontDirectionHint: 'int16', 47 | indexToLocFormat: 'int16', 48 | glyphDataFormat: 'int16', 49 | } 50 | 51 | const getMacStyle = (macStyle: number) => { 52 | 53 | } 54 | 55 | /** 56 | * 解析head表 57 | * @param data 字体文件DataView数据 58 | * @param offset 当前表的位置 59 | * @param font 字体对象 60 | * @returns IHeadTable对象 61 | */ 62 | /** 63 | * parse head table 64 | * @param data font data, type of DataView 65 | * @param offset offset of current table 66 | * @param font font object 67 | * @returns IHeadTable object 68 | */ 69 | const parse = (data: DataView, offset: number, font: IFont) => { 70 | // 获取head表中的键值 71 | // get keys in head table 72 | const keys = Object.keys(types) 73 | const table: IHeadTable = {} 74 | 75 | // 启动一个新的decoder 76 | // start a new decoder 77 | decode.start(data, offset) 78 | for (let i = 0; i < keys.length; i++) { 79 | const key = keys[i] 80 | 81 | // 根据每个键值对应的数据类型,进行解析 82 | // parse each key according to its data type 83 | table[key as keyof typeof table] = decode.decoder[types[key as keyof typeof types] as keyof typeof decode.decoder]() as number 84 | } 85 | decode.end() 86 | 87 | font.settings.indexToLocFormat = table.indexToLocFormat 88 | font.settings.unitsPerEm = table.unitsPerEm 89 | 90 | return table 91 | } 92 | 93 | /** 94 | * 根据IHeadTable对象创建该表的原始数据 95 | * @param table IHeadTable table 96 | * @returns 原始数据数组,每项类型是8-bit数字 97 | */ 98 | /** 99 | * generate raw data from IHeadTable table 100 | * @param table IHeadTable table 101 | * @returns raw data array, each entry is type of 8-bit number 102 | */ 103 | const create = (table: IHeadTable) => { 104 | let data: Array = [] 105 | 106 | // 遍历table的每个键值,生成对应数据 107 | // traverse table, generate data for each key 108 | Object.keys(table).forEach((key: string) => { 109 | const type = types[key as keyof typeof types] 110 | const value = table[key as keyof typeof table] 111 | // 使用encoder中的方法,根据不同键值对应的数据类型生成数据 112 | // generate data use encoder according to each key's data type 113 | const bytes = encoder[type as keyof typeof encoder](value as number) 114 | if (bytes) { 115 | data = data.concat(bytes) 116 | } 117 | }) 118 | 119 | return data 120 | } 121 | 122 | export { 123 | parse, 124 | create, 125 | } 126 | 127 | export type { 128 | IHeadTable, 129 | } -------------------------------------------------------------------------------- /src/fontManager/tables/hhea.ts: -------------------------------------------------------------------------------- 1 | import type { IFont } from '../font' 2 | import { encoder } from '../encode' 3 | import * as decode from '../decode' 4 | 5 | // hhea表格式 6 | // hhea table format 7 | interface IHheaTable { 8 | majorVersion?: number; 9 | minorVersion?: number; 10 | ascender?: number; 11 | descender?: number; 12 | lineGap?: number; 13 | advanceWidthMax?: number; 14 | minLeftSideBearing?: number; 15 | minRightSideBearing?: number; 16 | xMaxExtent?: number; 17 | caretSlopeRise?: number; 18 | caretSlopeRun?: number; 19 | caretOffset?: number; 20 | reserved0?: number; 21 | reserved1?: number; 22 | reserved2?: number; 23 | reserved3?: number; 24 | metricDataFormat?: number; 25 | numberOfHMetrics?: number; 26 | } 27 | 28 | // hhea表数据类型 29 | // hhea table data type 30 | const types = { 31 | majorVersion: 'uint16', 32 | minorVersion: 'uint16', 33 | ascender: 'FWORD', 34 | descender: 'FWORD', 35 | lineGap: 'FWORD', 36 | advanceWidthMax: 'UFWORD', 37 | minLeftSideBearing: 'FWORD', 38 | minRightSideBearing: 'FWORD', 39 | xMaxExtent: 'FWORD', 40 | caretSlopeRise: 'int16', 41 | caretSlopeRun: 'int16', 42 | caretOffset: 'int16', 43 | reserved0: 'int16', 44 | reserved1: 'int16', 45 | reserved2: 'int16', 46 | reserved3: 'int16', 47 | metricDataFormat: 'int16', 48 | numberOfHMetrics: 'uint16', 49 | } 50 | 51 | /** 52 | * 解析hhea表 53 | * @param data 字体文件DataView数据 54 | * @param offset 当前表的位置 55 | * @param font 字体对象 56 | * @returns IHheaTable对象 57 | */ 58 | /** 59 | * parse hhea table 60 | * @param data font data, type of DataView 61 | * @param offset offset of current table 62 | * @param font font object 63 | * @returns IHheaTable object 64 | */ 65 | const parse = (data: DataView, offset: number, font: IFont) => { 66 | // 获取head表中的键值 67 | // get keys in hhea table 68 | const keys = Object.keys(types) 69 | const table: IHheaTable = {} 70 | 71 | // 启动一个新的decoder 72 | // start a new decoder 73 | decode.start(data, offset) 74 | for (let i = 0; i < keys.length; i++) { 75 | const key = keys[i] 76 | 77 | // 根据每个键值对应的数据类型,进行解析 78 | // parse each key according to its data type 79 | table[key as keyof typeof table] = decode.decoder[types[key as keyof typeof types] as keyof typeof decode.decoder]() as number 80 | } 81 | decode.end() 82 | 83 | font.settings.numberOfHMetrics = table.numberOfHMetrics 84 | font.settings.ascender = table.ascender 85 | font.settings.descender = table.descender 86 | 87 | return table 88 | } 89 | 90 | /** 91 | * 根据IHheaTable对象创建该表的原始数据 92 | * @param table IHheaTable table 93 | * @returns 原始数据数组,每项类型是8-bit数字 94 | */ 95 | /** 96 | * generate raw data from IHheaTable table 97 | * @param table IHheaTable table 98 | * @returns raw data array, each entry is type of 8-bit number 99 | */ 100 | const create = (table: IHheaTable) => { 101 | let data: Array = [] 102 | 103 | // 遍历table的每个键值,生成对应数据 104 | // traverse table, generate data for each key 105 | Object.keys(table).forEach((key: string) => { 106 | const type = types[key as keyof typeof types] 107 | const value = table[key as keyof typeof table] 108 | // 使用encoder中的方法,根据不同键值对应的数据类型生成数据 109 | // generate data use encoder according to each key's data type 110 | const bytes = encoder[type as keyof typeof encoder](value as number) 111 | if (bytes) { 112 | data = data.concat(bytes) 113 | } 114 | }) 115 | 116 | return data 117 | } 118 | 119 | export { 120 | parse, 121 | create, 122 | } 123 | 124 | export type { 125 | IHheaTable, 126 | } -------------------------------------------------------------------------------- /src/fontManager/tables/hmtx.ts: -------------------------------------------------------------------------------- 1 | import type { IFont } from '../font' 2 | import { encoder } from '../encode' 3 | import * as decode from '../decode' 4 | 5 | // hmtx表格式 6 | // hmtx table format 7 | interface IHmtxTable { 8 | hMetrics: Array; 9 | leftSideBearings?: Array; 10 | } 11 | 12 | // hMetrics数据类型 13 | // hMetrics data type 14 | interface ILongHorMetric { 15 | advanceWidth: number; 16 | lsb: number; 17 | } 18 | 19 | // hmtx表数据类型 20 | // hmtx table data type 21 | const types = { 22 | advanceWidth: 'uint16', 23 | lsb: 'int16', 24 | leftSideBearings: 'int16', 25 | } 26 | 27 | /** 28 | * 解析hmtx表 29 | * @param data 字体文件DataView数据 30 | * @param offset 当前表的位置 31 | * @param font 字体对象 32 | * @returns IHmtxTable对象 33 | */ 34 | /** 35 | * parse hmtx table 36 | * @param data font data, type of DataView 37 | * @param offset offset of current table 38 | * @param font font object 39 | * @returns IHmtxTable object 40 | */ 41 | const parse = (data: DataView, offset: number, font: IFont) => { 42 | const numberOfHMetrics = font.settings.numberOfHMetrics as number 43 | const numGlyphs = font.settings.numGlyphs as number 44 | 45 | const hMetrics = [] 46 | const leftSideBearings = [] 47 | 48 | // 启动一个新的decoder 49 | // start a new decoder 50 | decode.start(data, offset) 51 | 52 | for (let i = 0; i < numberOfHMetrics; i++) { 53 | const advanceWidth = decode.decoder[types['advanceWidth'] as keyof typeof decode.decoder]() as number 54 | const lsb = decode.decoder[types['lsb'] as keyof typeof decode.decoder]() as number 55 | hMetrics.push({ 56 | advanceWidth, 57 | lsb, 58 | }) 59 | } 60 | 61 | for (let i = 0; i < (numGlyphs - numberOfHMetrics); i++) { 62 | leftSideBearings.push(decode.decoder[types['leftSideBearings'] as keyof typeof decode.decoder]() as number) 63 | } 64 | 65 | const table: IHmtxTable = { 66 | hMetrics, 67 | leftSideBearings, 68 | } 69 | 70 | decode.end() 71 | 72 | return table 73 | } 74 | 75 | /** 76 | * 根据IHtmxTable对象创建该表的原始数据 77 | * @param table IHtmxTable table 78 | * @returns 原始数据数组,每项类型是8-bit数字 79 | */ 80 | /** 81 | * generate raw data from IHheaTable table 82 | * @param table IHtmxTable table 83 | * @returns raw data array, each entry is type of 8-bit number 84 | */ 85 | const create = (table: IHmtxTable) => { 86 | let bytes: Array = [] 87 | const hMetrics = table.hMetrics as Array 88 | for (let i = 0; i < hMetrics.length; i++) { 89 | const hMetric = hMetrics[i] 90 | Object.keys(hMetric).forEach((key) => { 91 | const type = types[key as keyof typeof types] 92 | const value = hMetric[key as keyof typeof hMetric] 93 | bytes = bytes.concat(encoder[type as keyof typeof encoder](value) as Array) 94 | }) 95 | } 96 | const leftSideBearings = table.leftSideBearings as Array 97 | if (leftSideBearings) { 98 | for (let i = 0; i < leftSideBearings.length; i++) { 99 | bytes = bytes.concat(encoder[types['leftSideBearings'] as keyof typeof encoder](leftSideBearings[i]) as Array) 100 | } 101 | } 102 | return bytes 103 | } 104 | 105 | export { 106 | parse, 107 | create, 108 | } 109 | 110 | export type { 111 | IHmtxTable, 112 | } -------------------------------------------------------------------------------- /src/fontManager/tables/loca.ts: -------------------------------------------------------------------------------- 1 | import type { IFont } from '../font' 2 | import { encoder } from '../encode' 3 | import * as decode from '../decode' 4 | 5 | // loca表格式 6 | // loca table format 7 | interface ILocaTable { 8 | offsets: Array; 9 | } 10 | 11 | /** 12 | * 解析loca表 13 | * @param data 字体文件DataView数据 14 | * @param offset 当前表的位置 15 | * @param font 字体对象 16 | * @returns ILocaTable对象 17 | */ 18 | /** 19 | * parse loca table 20 | * @param data font data, type of DataView 21 | * @param offset offset of current table 22 | * @param font font object 23 | * @returns ILocaTable object 24 | */ 25 | const parse = (data: DataView, offset: number, font: IFont) => { 26 | const version = font.settings.indexToLocFormat as number 27 | const numGlyphs = font.settings.numGlyphs as number 28 | const table: ILocaTable = { 29 | offsets: [] 30 | } 31 | // 启动一个新的decoder 32 | // start a new decoder 33 | decode.start(data, offset) 34 | if (version === 0) { 35 | for (let i = 0; i < numGlyphs; i++) { 36 | table.offsets.push(decode.decoder['Offset16']() * 2) 37 | } 38 | } else if (version === 1) { 39 | for (let i = 0; i < numGlyphs; i++) { 40 | table.offsets.push(decode.decoder['Offset32']()) 41 | } 42 | } 43 | decode.end() 44 | return table 45 | } 46 | 47 | /** 48 | * 根据ILocaTable对象创建该表的原始数据 49 | * @param table ILocaTable table 50 | * @param options 配置项 51 | * @returns 原始数据数组,每项类型是8-bit数字 52 | */ 53 | /** 54 | * generate raw data from ILocaTable table 55 | * @param table ILocaTable table 56 | * @param options options 57 | * @returns raw data array, each entry is type of 8-bit number 58 | */ 59 | const create = (table: ILocaTable, options: { version: number }) => { 60 | let data: Array = [] 61 | for (let i = 0; i < table.offsets.length; i++) { 62 | if (options.version === 0) { 63 | const bytes = encoder['Offset16'](table.offsets[i] as number) as Array 64 | if (bytes) { 65 | data = data.concat(bytes) 66 | } 67 | } else if (options.version === 1) { 68 | const bytes = encoder['Offset32'](table.offsets[i] as number) as Array 69 | if (bytes) { 70 | data = data.concat(bytes) 71 | } 72 | } 73 | } 74 | return data 75 | } 76 | 77 | export { 78 | parse, 79 | create, 80 | } 81 | 82 | export type { 83 | ILocaTable, 84 | } -------------------------------------------------------------------------------- /src/fontManager/tables/maxp.ts: -------------------------------------------------------------------------------- 1 | import { getVersion } from '../utils' 2 | import type { IFont } from '../font' 3 | import { encoder } from '../encode' 4 | import type { IValue } from '../encode' 5 | import * as decode from '../decode' 6 | 7 | // maxp表格式 8 | // maxp table format 9 | interface IMaxpTable { 10 | version?: number; 11 | numGlyphs?: number; 12 | maxPoints?: number; 13 | maxContours?: number; 14 | maxCompositePoints?: number; 15 | maxCompositeContours?: number; 16 | maxZones?: number; 17 | maxTwilightPoints?: number; 18 | maxStorage?: number; 19 | maxFunctionDefs?: number; 20 | maxInstructionDefs?: number; 21 | maxStackElements?: number; 22 | maxSizeOfInstructions?: number; 23 | maxComponentElements?: number; 24 | maxComponentDepth?: number; 25 | } 26 | 27 | // maxp表数据类型 28 | // maxp table data type 29 | const types = { 30 | version: 'Version16Dot16', 31 | numGlyphs: 'uint16', 32 | maxPoints: 'uint16', 33 | maxContours: 'uint16', 34 | maxCompositePoints: 'uint16', 35 | maxCompositeContours: 'uint16', 36 | maxZones: 'uint16', 37 | maxTwilightPoints: 'uint16', 38 | maxStorage: 'uint16', 39 | maxFunctionDefs: 'uint16', 40 | maxInstructionDefs: 'uint16', 41 | maxStackElements: 'uint16', 42 | maxSizeOfInstructions: 'uint16', 43 | maxComponentElements: 'uint16', 44 | maxComponentDepth: 'uint16', 45 | } 46 | 47 | /** 48 | * 解析maxp表 49 | * @param data 字体文件DataView数据 50 | * @param offset 当前表的位置 51 | * @param font 字体对象 52 | * @returns IMaxpTable对象 53 | */ 54 | /** 55 | * parse head table 56 | * @param data font data, type of DataView 57 | * @param offset offset of current table 58 | * @param font font object 59 | * @returns IMaxpTable object 60 | */ 61 | const parse = (data: DataView, offset: number, font: IFont) => { 62 | // 获取maxp table version 63 | // get maxp table version 64 | const version = getVersion(data, offset) 65 | 66 | // 获取maxp表中的键值 67 | // get keys in maxp table 68 | const keys = Object.keys(types) 69 | const table: IMaxpTable = {} 70 | 71 | // 启动一个新的decoder 72 | // start a new decoder 73 | decode.start(data, offset) 74 | for (let i = 0; i < keys.length; i++) { 75 | const key = keys[i] 76 | 77 | if (version >= 1 || key === 'numGlyphs' || key === 'version') { 78 | // 根据每个键值对应的数据类型,进行解析 79 | // parse each key according to its data type 80 | table[key as keyof typeof table] = decode.decoder[types[key as keyof typeof types] as keyof typeof decode.decoder]() as number 81 | } 82 | } 83 | decode.end() 84 | 85 | font.settings.numGlyphs = table.numGlyphs 86 | 87 | return table 88 | } 89 | 90 | /** 91 | * 根据IMaxpTable对象创建该表的原始数据 92 | * @param table IMaxpTable table 93 | * @returns 原始数据数组,每项类型是8-bit数字 94 | */ 95 | /** 96 | * generate raw data from IHeadTable table 97 | * @param table IMaxpTable table 98 | * @returns raw data array, each entry is type of 8-bit number 99 | */ 100 | const create = (table: IMaxpTable) => { 101 | let data: Array = [] 102 | 103 | // 遍历table的每个键值,生成对应数据 104 | // traverse table, generate data for each key 105 | Object.keys(table).forEach((key: string) => { 106 | const type = types[key as keyof typeof types] 107 | const value = table[key as keyof typeof table] as IValue 108 | // 使用encoder中的方法,根据不同键值对应的数据类型生成数据 109 | // generate data use encoder according to each key's data type 110 | const bytes = encoder[type as keyof typeof encoder](value) 111 | if (bytes) { 112 | data = data.concat(bytes) 113 | } 114 | }) 115 | return data 116 | } 117 | 118 | export { 119 | parse, 120 | create, 121 | } 122 | 123 | export type { 124 | IMaxpTable, 125 | } -------------------------------------------------------------------------------- /src/fontManager/utils/index.ts: -------------------------------------------------------------------------------- 1 | const getTag = (data: DataView, offset: number) => { 2 | const tagArr: Array = [] 3 | let tagStr: string = '' 4 | for (let i = 0; i < 4; i++) { 5 | tagArr.push(data.getUint8(offset + i)) 6 | tagStr += String.fromCharCode(tagArr[i]) 7 | } 8 | return { 9 | tagArr, tagStr, 10 | } 11 | } 12 | 13 | const getVersion = (data: DataView, offset: number) => { 14 | const major = data.getUint16(offset) 15 | const minor = data.getUint16(offset + 2) 16 | return major + minor / 0x1000 / 10 17 | } 18 | 19 | const getStorageString = (data: DataView, offset: number, bytes: number) => { 20 | const chars = [] 21 | for (let i = 0; i < bytes / 2; i++) { 22 | chars[i] = data.getUint16(offset + i * 2) 23 | } 24 | return String.fromCharCode.apply(null, chars) 25 | } 26 | 27 | const computeCheckSum = (_bytes: Array, write: boolean = false) => { 28 | let bytes = _bytes 29 | if (!write) { 30 | bytes = Object.assign([], _bytes) 31 | } 32 | while (bytes.length % 4 !== 0) { 33 | bytes.push(0) 34 | } 35 | 36 | let sum = 0 37 | for (let i = 0; i < bytes.length; i += 4) { 38 | // sum += (bytes[i] << 24) + 39 | // (bytes[i + 1] << 16) + 40 | // (bytes[i + 2] << 8) + 41 | // (bytes[i + 3]) 42 | 43 | const value = ((bytes[i] << 24) >>> 0) + 44 | ((bytes[i + 1] << 16) >>> 0) + 45 | ((bytes[i + 2] << 8) >>> 0) + 46 | (bytes[i + 3] >>> 0) 47 | 48 | sum = (sum + value) >>> 0 49 | } 50 | 51 | sum %= 0x100000000 52 | return sum 53 | } 54 | 55 | const hasChineseChar = (text) => { 56 | let rs = false 57 | for (let i = 0; i < text.length; i++) { 58 | if (isChineseChar(text[i])) { 59 | rs = true 60 | } 61 | } 62 | return rs 63 | } 64 | 65 | const isChineseChar = (char) => { 66 | return /^[\u4E00-\u9FFF]$/.test(char); 67 | } 68 | 69 | export { 70 | getTag, 71 | getVersion, 72 | getStorageString, 73 | computeCheckSum, 74 | hasChineseChar, 75 | isChineseChar, 76 | } -------------------------------------------------------------------------------- /src/fontManager/validators.ts: -------------------------------------------------------------------------------- 1 | type IValue = number | string | Array | Array | Array 2 | const LIMIT16 = 32768 3 | const LIMIT32 = 2147483648 4 | const validators = { 5 | uint8: (v: IValue) => { 6 | if (typeof v === 'number' && v >= 0 && v <= 255) 7 | return true 8 | return false 9 | }, 10 | int8: (v: IValue) => { 11 | if (typeof v === 'number' && v >= -128 && v <= 127) 12 | return true 13 | return false 14 | }, 15 | uint16: (v: IValue) => { 16 | if (typeof v !== 'number') return false 17 | return true 18 | }, 19 | int16: (v: IValue) => { 20 | if (typeof v !== 'number') return false 21 | return true 22 | }, 23 | uint24: (v: IValue) => { 24 | if (typeof v !== 'number') return false 25 | return true 26 | }, 27 | uint32: (v: IValue) => { 28 | if (typeof v !== 'number') return false 29 | return true 30 | }, 31 | int32: (v: IValue) => { 32 | if (typeof v !== 'number') return false 33 | return true 34 | }, 35 | Fixed: (v: IValue) => { 36 | if (typeof v !== 'number') return false 37 | return true 38 | }, 39 | FWORD: (v: IValue) => { 40 | if (typeof v !== 'number') return false 41 | return true 42 | }, 43 | UFWORD: (v: IValue) => { 44 | if (typeof v !== 'number') return false 45 | return true 46 | }, 47 | LONGDATETIME: (v: IValue) => { 48 | if (typeof v !== 'number') return false 49 | const timestamp = (v - 2082844800) * 1000 50 | if (new Date(timestamp).getTime() > 0) return true 51 | return false 52 | }, 53 | Tag: (v: IValue) => { 54 | if ((v as string).length === 4) return true 55 | return false 56 | } 57 | } 58 | 59 | export { 60 | validators, 61 | } -------------------------------------------------------------------------------- /src/i18n/electron/index.ts: -------------------------------------------------------------------------------- 1 | const i18next = require('i18next') 2 | import { dialogs } from '../dialogs' 3 | import { panels } from '../panels' 4 | import { menus } from '../menus' 5 | import { welcome } from '../welcome' 6 | import { programming } from '../programming' 7 | 8 | i18next.init({ 9 | lng: 'zh', 10 | resources: { 11 | zh: { 12 | translation: { 13 | menus: menus.zh, 14 | dialogs: dialogs.zh, 15 | panels: panels.zh, 16 | welcome: welcome.zh, 17 | programming: programming.zh, 18 | } 19 | }, 20 | en: { 21 | translation: { 22 | menus: menus.en, 23 | dialogs: dialogs.en, 24 | panels: panels.en, 25 | welcome: welcome.en, 26 | programming: programming.en, 27 | } 28 | } 29 | } 30 | }) 31 | 32 | export { 33 | i18next, 34 | } -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { dialogs } from './dialogs' 2 | import { panels } from './panels' 3 | import { menus } from './menus' 4 | import { welcome } from './welcome' 5 | import { programming } from './programming' 6 | import { createI18n } from 'vue-i18n' 7 | 8 | const messages = { 9 | zh: { 10 | menus: menus.zh, 11 | dialogs: dialogs.zh, 12 | panels: panels.zh, 13 | welcome: welcome.zh, 14 | programming: programming.zh, 15 | }, 16 | en: { 17 | menus: menus.en, 18 | dialogs: dialogs.en, 19 | panels: panels.en, 20 | welcome: welcome.en, 21 | programming: programming.en, 22 | }, 23 | } 24 | 25 | const i18nOptions = { 26 | locale: 'zh', 27 | fallbackLocale: 'en', 28 | allowComposition: true, 29 | messages, 30 | } 31 | 32 | const i18n = createI18n(i18nOptions) 33 | 34 | export { 35 | i18n, 36 | i18nOptions, 37 | menus, 38 | dialogs, 39 | panels, 40 | welcome, 41 | } -------------------------------------------------------------------------------- /src/i18n/menus.ts: -------------------------------------------------------------------------------- 1 | const menus = { 2 | zh: { 3 | app: '字玩', 4 | about: '关于', 5 | file: { 6 | file: '文件', 7 | new: '新建工程', 8 | open: '打开工程', 9 | save: '保存工程', 10 | saveas: '另存为', 11 | clear: '清空缓存', 12 | export: '导出工程', 13 | sync_data: '同步缓存', 14 | }, 15 | edit: { 16 | edit: '编辑', 17 | undo: '撤销', 18 | redo: '重做', 19 | cut: '剪切', 20 | copy: '复制', 21 | paste: '粘贴', 22 | delete: '删除', 23 | }, 24 | import: { 25 | import: '导入', 26 | font: '导入字体库', 27 | glyph: '导入字形', 28 | picture: '识别图片', 29 | svg: '导入SVG', 30 | }, 31 | export: { 32 | export: '导出', 33 | font: '导出字体库', 34 | glyph: '导出字形', 35 | jpeg: '导出JPEG', 36 | png: '导出PNG', 37 | svg: '导出SVG', 38 | }, 39 | char: { 40 | char: '字符与图标', 41 | character: '添加字符', 42 | icon: '添加图标', 43 | }, 44 | settings: { 45 | settings: '设置', 46 | font: '字体设置', 47 | preference: '偏好设置', 48 | language: '语言设置', 49 | }, 50 | templates: { 51 | templates: '模板', 52 | test1: '测试模板1', 53 | test2: '测试模板2', 54 | }, 55 | tools: { 56 | remove_overlap: '去除重叠', 57 | }, 58 | }, 59 | en: { 60 | app: 'FontPlayer', 61 | about: 'about', 62 | file: { 63 | file: 'File', 64 | new: 'New File', 65 | open: 'Open File', 66 | save: 'Save', 67 | saveas: 'Save As', 68 | clear: 'Clear Cache', 69 | export: 'Export Project', 70 | sync_data: 'Sync Data', 71 | }, 72 | edit: { 73 | edit: 'Edit', 74 | undo: 'Undo', 75 | redo: 'Redo', 76 | cut: 'Cut', 77 | copy: 'Copy', 78 | paste: 'Paste', 79 | delete: 'Delete', 80 | }, 81 | import: { 82 | import: 'Import', 83 | font: 'Import Font', 84 | glyph: 'Import Glyph', 85 | picture: 'Import From Picture', 86 | svg: 'Import SVG', 87 | }, 88 | export: { 89 | export: 'Export', 90 | font: 'Export Font', 91 | glyph: 'Export Glyph', 92 | jpeg: 'Export JPEG', 93 | png: 'Export PNG', 94 | svg: 'Export SVG', 95 | }, 96 | char: { 97 | char: 'Char And Icon', 98 | character: 'Add Character', 99 | icon: 'Add Icon', 100 | }, 101 | settings: { 102 | settings: 'Settings', 103 | font: 'Font Settings', 104 | preference: 'Preference', 105 | language: 'Language', 106 | }, 107 | templates: { 108 | templates: 'Templates', 109 | test1: 'TestTemplate1', 110 | }, 111 | tools: { 112 | remove_overlap: 'remove_overlap', 113 | }, 114 | }, 115 | } 116 | 117 | export { 118 | menus, 119 | } -------------------------------------------------------------------------------- /src/i18n/programming.ts: -------------------------------------------------------------------------------- 1 | const programming = { 2 | zh: { 3 | 'global-constants': '全局常量', 4 | 'glyph-parameters': '字形参数', 5 | 'glyph-layout': '字形布局', 6 | 'new-constant': '新建常量', 7 | 'new-parameter': '新建参数', 8 | joint: '关键点', 9 | refline: '辅助线', 10 | constant: '常量', 11 | number: '变量', 12 | ring: '环形组件', 13 | widget: '控件', 14 | controlType: '控件类型', 15 | type: '类型', 16 | value: '值', 17 | character: '字符', 18 | glyph: '字形', 19 | radical: '部首', 20 | comp: '字形', 21 | glyph_comp: '组件', 22 | stroke: '笔画', 23 | reset: '重置', 24 | execute: '运行', 25 | script: '脚本', 26 | charCounts: '字符数量', 27 | strokeCounts: '笔画数量', 28 | radicalCounts: '部首数量', 29 | compCounts: '字形数量', 30 | glyphCompCounts: '组件数量', 31 | }, 32 | en: { 33 | 'global-constants': 'Global Constants', 34 | 'glyph-parameters': 'Glyph Parameters', 35 | 'glyph-layout': 'Glyph Layout', 36 | 'new-constant': 'New Constant', 37 | 'new-parameter': 'New Parameter', 38 | joint: 'Joint', 39 | refline: 'Ref Line', 40 | constants: 'const', 41 | number: 'number', 42 | ring: 'RingController', 43 | widget: 'Widget', 44 | controlType: 'Control Type', 45 | type: 'Type', 46 | value: 'value', 47 | character: 'Character', 48 | glyph: 'Glyph', 49 | radical: 'Radical', 50 | comp: 'Glyph', 51 | glyph_comp: 'Component', 52 | stroke: 'Stroke', 53 | reset: 'Reset', 54 | execute: 'Execute', 55 | script: 'Script', 56 | charCounts: 'Character', 57 | strokeCounts: 'Stroke', 58 | radicalCounts: 'Radical', 59 | compCounts: 'Glyph', 60 | glyphCompCounts: 'Components', 61 | } 62 | } 63 | 64 | export { 65 | programming, 66 | } -------------------------------------------------------------------------------- /src/i18n/welcome.ts: -------------------------------------------------------------------------------- 1 | const welcome = { 2 | zh: { 3 | new: { 4 | name: '新建工程', 5 | description: '新建字体(或web图标字体)工程', 6 | }, 7 | open: { 8 | name: '打开工程', 9 | description: '打开已有字体(或web图标字体)工程', 10 | }, 11 | import: { 12 | name: '导入字体库', 13 | description: '将字体库导入成新的字体(或web图标字体)工程', 14 | }, 15 | sync: { 16 | name: '同步缓存', 17 | description: '同步缓存的字体(或web图标字体)工程', 18 | }, 19 | template: { 20 | name: '导入模板', 21 | description: '导入字玩自带的测试模板', 22 | }, 23 | playground: { 24 | title: '玩一玩字玩', 25 | description: '通过参数化快速创建一个迷你字库', 26 | }, 27 | }, 28 | en: { 29 | new: { 30 | name: 'New Project', 31 | description: 'Create new font (or web font) project', 32 | }, 33 | open: { 34 | name: 'Open Project', 35 | description: 'Open existed font (or web font) project', 36 | }, 37 | import: { 38 | name: 'Import Font', 39 | description: 'Import font as project', 40 | }, 41 | sync: { 42 | name: 'Sync Cache', 43 | description: 'Sync cache', 44 | }, 45 | template: { 46 | name: 'Import Template', 47 | description: 'Import template that FontPlayer provides for testing', 48 | }, 49 | playground: { 50 | title: 'Playground', 51 | description: 'Quickly create a mini font library through parameterization', 52 | }, 53 | }, 54 | } 55 | 56 | export { 57 | welcome, 58 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import App from './App.vue' 4 | import router from './router' 5 | 6 | import ElementPlus from 'element-plus' 7 | import 'element-plus/dist/index.css' 8 | // import * as ElementPlusIconsVue from '@element-plus/icons-vue' 9 | import { Files, Edit, Upload, Download, Tickets, Setting, List, Tools, QuestionFilled } from '@element-plus/icons-vue' 10 | 11 | import { i18n } from './i18n' 12 | 13 | import './assets/main.css' 14 | 15 | import { library } from '@fortawesome/fontawesome-svg-core' 16 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 17 | 18 | // import { fas } from '@fortawesome/free-solid-svg-icons' 19 | // import { far } from '@fortawesome/free-regular-svg-icons' 20 | // import { fab } from '@fortawesome/free-brands-svg-icons' 21 | 22 | import { 23 | faArrowPointer, 24 | faCircle, 25 | faPercent, 26 | faArrowDownWideShort, 27 | faPenNib, 28 | faSquare, 29 | faDrawPolygon, 30 | faImage, 31 | faFont, 32 | faTerminal, 33 | faSliders, 34 | faTableCells, 35 | faHand as faHandSolid, 36 | faTextWidth, 37 | faGamepad, 38 | } from '@fortawesome/free-solid-svg-icons' 39 | import { 40 | faHand, 41 | faSquare as faSquare_regular, 42 | faCircle as faCircle_regular, 43 | } from '@fortawesome/free-regular-svg-icons' 44 | 45 | import { initWorker } from './fontEditor/worker' 46 | 47 | import localForage from 'localforage' 48 | 49 | 50 | localForage.config({ 51 | driver : localForage.INDEXEDDB, // Force WebSQL; same as using setDriver() 52 | name : 'myDatabase', 53 | version : 1.0, 54 | size : 4980736, // Size of database, in bytes. WebSQL-only for now. 55 | storeName : 'keyvaluepairs', // Should be alphanumeric, with underscores. 56 | description : 'my database' 57 | }) 58 | 59 | declare global { 60 | interface Window { 61 | FP: any; 62 | constantsMap: any; 63 | glyph: any; 64 | comp_glyph: any; 65 | character: any; 66 | __constants: any; 67 | __parameters: any; 68 | __script: any; 69 | __is_web: boolean; 70 | } 71 | } 72 | 73 | // await initGlyphEnvironment() 74 | 75 | // library.add(fas, far, fab) 76 | 77 | library.add( 78 | faArrowPointer, 79 | faCircle, 80 | faPercent, 81 | faArrowDownWideShort, 82 | faPenNib, 83 | faSquare, 84 | faDrawPolygon, 85 | faImage, 86 | faFont, 87 | faTerminal, 88 | faSliders, 89 | faTableCells, 90 | faHand, 91 | faSquare_regular, 92 | faCircle_regular, 93 | faHandSolid, 94 | faTextWidth, 95 | faGamepad, 96 | ) 97 | 98 | const app = createApp(App) 99 | 100 | app.config.errorHandler = (err, vm, info) => { 101 | console.error('全局错误:', err) 102 | } 103 | 104 | app.component('font-awesome-icon', FontAwesomeIcon) 105 | app.use(ElementPlus) 106 | // for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 107 | // app.component(key, component) 108 | // } 109 | app.component('Files', Files) 110 | app.component('Edit', Edit) 111 | app.component('Upload', Upload) 112 | app.component('Download', Download) 113 | app.component('Tickets', Tickets) 114 | app.component('Setting', Setting) 115 | app.component('List', List) 116 | app.component('Tools', Tools) 117 | app.component('QuestionFilled', QuestionFilled) 118 | app.use(router) 119 | app.use(i18n) 120 | 121 | app.mount('#app') 122 | 123 | const worker = initWorker() 124 | //const worker = new MyWorker() 125 | 126 | export { 127 | app, 128 | worker, 129 | } -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' 2 | import Welcome from '../fontEditor/views/Welcome.vue' 3 | import Editor from '../fontEditor/views/Editor.vue' 4 | import CharacterProgrammingEditor from '../fontEditor/views/CharacterProgrammingEditor.vue' 5 | import GlyphProgrammingEditor from '../fontEditor/views/GlyphProgrammingEditor.vue' 6 | import Playground from '../fontEditor/views/Playground.vue' 7 | 8 | const router = createRouter({ 9 | // history: createWebHistory(import.meta.env.BASE_URL), 10 | history: createWebHashHistory(), 11 | routes: [ 12 | { 13 | path: '/', 14 | name: 'welcome', 15 | component: Welcome, 16 | }, 17 | { 18 | path: '/editor', 19 | name: 'editor', 20 | component: Editor, 21 | }, 22 | { 23 | path: '/character-programming-editor', 24 | name: 'character-programming-editor', 25 | component: CharacterProgrammingEditor, 26 | }, 27 | { 28 | path: '/glyph-programming-editor', 29 | name: 'glyph-programming-editor', 30 | component: GlyphProgrammingEditor, 31 | }, 32 | { 33 | path: '/playground', 34 | name: 'playground', 35 | component: Playground, 36 | } 37 | ] 38 | }) 39 | 40 | router.beforeEach((to, from, next) => { 41 | // Ensure the logic is minimal and efficient 42 | next(); 43 | }); 44 | 45 | export default router -------------------------------------------------------------------------------- /src/test/fontManager/font.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFont, hasChar } from '@/fontManager/font' 2 | import { describe, it, expect, test } from 'vitest' 3 | 4 | describe('font', () => { 5 | const characters = [{ 6 | unicode: 0, 7 | name: '.notdef', 8 | contours: [[]], 9 | contourNum: 0, 10 | advanceWidth: 500, 11 | }, { 12 | advanceWidth: 500, 13 | contourNum: 1, 14 | contours:[[ 15 | { 16 | control1: {x: 417, y: 545}, 17 | control2: {x: 498, y: 599}, 18 | end: {x: 540, y: 584}, 19 | start: {x: 386, y: 512}, 20 | type: 2, 21 | }, 22 | { 23 | control1: {x: 604, y: 560}, 24 | control2: {x: 716, y: 469}, 25 | end: {x: 640, y: 356}, 26 | start: {x: 540, y: 584}, 27 | type: 2, 28 | },{ 29 | control1: {x: 640, y: 356}, 30 | control2: {x: 386, y: 512}, 31 | end: {x: 386, y: 512}, 32 | start: {x: 640, y: 356}, 33 | type: 2, 34 | }, 35 | ]], 36 | name: 'a', 37 | unicode: 97, 38 | }] 39 | 40 | const font = createFont(characters, { 41 | familyName: 'Test', 42 | styleName: 'Medium', 43 | unitsPerEm: 1000, 44 | ascender: 800, 45 | descender: -200, 46 | }) 47 | 48 | it('create correct number of tables', () => { 49 | expect(font.tables.length).toBe(9) 50 | }) 51 | 52 | test('hasChar method', () => { 53 | expect(hasChar(font, 'a')).toBe(true) 54 | }) 55 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/cff.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { create, createTable } from '@/fontManager/tables/cff' 3 | import { hex } from '../utils' 4 | 5 | describe('cff table', () => { 6 | const data = '01 00 04 01 00 01 01 01 0B 54 65 73 74 4D 65 64 69 75 6D 00 01 01 01 27 F8 1C 00 F8 1D 02 F8 1E 03 F8 1F 04 8B 8B F9 B4 F8 88 05 1D 00 00 00 6A 0F 1D 00 00 00 6D 11 8B 1D 00 00 00 97 12 00 05 01 01 02 0D 18 1C 22 61 56 65 72 73 69 6F 6E 20 30 2E 31 54 65 73 74 20 4D 65 64 69 75 6D 54 65 73 74 4D 65 64 69 75 6D 00 00 00 01 87 00 02 01 01 04 25 F8 88 0E F8 88 F8 16 F8 94 15 AB AD DC C1 B5 7B 08 CB 73 F7 05 30 3E FB 05 08 8B 8B FB 92 F7 30 8B 8B 08 0E' 7 | const characters = [{ 8 | unicode: 0, 9 | name: '.notdef', 10 | contours: [[]], 11 | contourNum: 0, 12 | advanceWidth: 500, 13 | }, { 14 | advanceWidth: 500, 15 | contourNum: 1, 16 | contours:[[ 17 | { 18 | control1: {x: 417.5, y: 545.6}, 19 | control2: {x: 498.4, y: 599.6}, 20 | end: {x: 540, y: 584}, 21 | start: {x: 386, y: 512}, 22 | type: 2, 23 | }, 24 | { 25 | control1: {x: 604, y: 560}, 26 | control2: {x: 716.7, y: 469.1}, 27 | end: {x: 640, y: 356}, 28 | start: {x: 540, y: 584}, 29 | type: 2, 30 | },{ 31 | control1: {x: 640, y: 356}, 32 | control2: {x: 386, y: 512}, 33 | end: {x: 386, y: 512}, 34 | start: {x: 640, y: 356}, 35 | type: 2, 36 | }, 37 | ]], 38 | name: 'a', 39 | unicode: 97, 40 | }] 41 | 42 | const cffTable = createTable(characters, { 43 | version: 'Version 0.1', 44 | fullName: 'Test Medium', 45 | familyName: 'Test', 46 | weightName: 'Medium', 47 | postScriptName: 'TestMedium', 48 | unitsPerEm: 1000, 49 | fontBBox: [0, 0, 800, 500] 50 | }) 51 | 52 | const table = { 53 | header: { major: 1, minor: 0, hdrSize: 4, offSize: 1 }, 54 | nameIndex: { data: [ 'TestMedium' ] }, 55 | globalSubrIndex: { data: [] }, 56 | topDict: { 57 | version: 'Version 0.1', 58 | fullName: 'Test Medium', 59 | familyName: 'Test', 60 | weight: 'Medium', 61 | fontBBox: [ 0, 0, 800, 500 ], 62 | fontMatrix: [ 0.001, 0, 0, 0.001, 0, 0 ], 63 | charset: 999, 64 | encoding: 0, 65 | charStrings: 999, 66 | private: [ 0, 999 ] 67 | }, 68 | charsets: { format: 0, data: [ 'a' ] }, 69 | glyphTables: [ 70 | { numberOfContours: 0, contours: [[]], advanceWidth: 500 }, 71 | { numberOfContours: 1, contours: [[ 72 | { 73 | control1: {x: 417.5, y: 545.6}, 74 | control2: {x: 498.4, y: 599.6}, 75 | end: {x: 540, y: 584}, 76 | start: {x: 386, y: 512}, 77 | type: 2, 78 | }, 79 | { 80 | control1: {x: 604, y: 560}, 81 | control2: {x: 716.7, y: 469.1}, 82 | end: {x: 640, y: 356}, 83 | start: {x: 540, y: 584}, 84 | type: 2, 85 | },{ 86 | control1: {x: 640, y: 356}, 87 | control2: {x: 386, y: 512}, 88 | end: {x: 386, y: 512}, 89 | start: {x: 640, y: 356}, 90 | type: 2, 91 | }, 92 | ]], advanceWidth: 500 } 93 | ], 94 | stringIndex: { data: [] } 95 | } 96 | 97 | it('create table correctly', () => { 98 | assert.deepEqual(cffTable, table) 99 | }) 100 | 101 | it('create data correctly', () => { 102 | expect(hex(create(cffTable))).toBe(data) 103 | }) 104 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/cmap.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { create, createTable } from '@/fontManager/tables/cmap' 3 | import { hex } from '../utils' 4 | 5 | describe('cmap table', () => { 6 | const data = '00 00 00 01 00 03 00 01 00 00 00 0C 00 04 00 28 00 00 00 06 00 04 00 01 00 02 00 00 00 61 FF FF 00 00 00 00 00 61 FF FF 00 00 FF A0 00 01 00 00 00 00 00 00' 7 | const characters = [{ 8 | unicode: 0, 9 | name: '.notdef', 10 | contours: [[]], 11 | contourNum: 0, 12 | advanceWidth: 500, 13 | }, { 14 | advanceWidth: 500, 15 | contourNum: 1, 16 | contours:[[ 17 | { 18 | control1: {x: 417.5, y: 545.6}, 19 | control2: {x: 498.4, y: 599.6}, 20 | end: {x: 540, y: 584}, 21 | start: {x: 386, y: 512}, 22 | type: 2, 23 | }, 24 | { 25 | control1: {x: 604, y: 560}, 26 | control2: {x: 716.7, y: 469.1}, 27 | end: {x: 640, y: 356}, 28 | start: {x: 540, y: 584}, 29 | type: 2, 30 | },{ 31 | control1: {x: 640, y: 356}, 32 | control2: {x: 386, y: 512}, 33 | end: {x: 386, y: 512}, 34 | start: {x: 640, y: 356}, 35 | type: 2, 36 | }, 37 | ]], 38 | name: 'a', 39 | unicode: 97, 40 | }] 41 | 42 | const table = { 43 | version: 0, 44 | numTables: 1, 45 | encodingRecords: [ 46 | { 47 | platformID: 3, 48 | encodingID: 1, 49 | subtableOffset: 12, 50 | subTable: { 51 | format: 4, 52 | length: 40, 53 | language: 0, 54 | segCount: 3, 55 | searchRange: 4, 56 | entrySelector: 1, 57 | rangeShift: 2, 58 | segments: [ 59 | { endCode: 0, startCode: 0, idDelta: -0, idRangeOffset: 0 }, 60 | { endCode: 97, startCode: 97, idDelta: -96, idRangeOffset: 0 }, 61 | { endCode: 65535, startCode: 65535, idDelta: 1, idRangeOffset: 0 } 62 | ], 63 | glyphIndexMap: { '0': 0, '97': 1 } 64 | } 65 | } 66 | ] 67 | } 68 | 69 | const cmapTable = createTable(characters) 70 | 71 | it('create table correctly', () => { 72 | assert.deepEqual(cmapTable, table) 73 | }) 74 | 75 | it('create data correctly', () => { 76 | expect(hex(create(cmapTable))).toBe(data) 77 | }) 78 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/head.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { parse, create } from '@/fontManager/tables/head' 3 | import { hex, unhex } from '../utils' 4 | import { IFont } from '@/fontManager/font' 5 | 6 | describe('head table', () => { 7 | const data = `00 01 00 00 00 01 00 00 00 00 00 00 5F 0F 3C F5 00 03 03 E8 00 00 00 00 65 1C F6 55 00 00 00 00 65 1C F6 55 00 00 00 00 03 E8 03 E8 00 00 00 03 00 02 00 00 00 00` 8 | const timestimp = 1696396885 9 | 10 | const headTable = { 11 | majorVersion: 0x0001, 12 | minorVersion: 0x0000, 13 | fontRevision: 0x00010000, 14 | checkSumAdjustment: 0, 15 | magicNumber: 0x5F0F3CF5, 16 | flags: 3, 17 | unitsPerEm: 1000, 18 | created: timestimp, 19 | modified: timestimp, 20 | xMin: 0, 21 | yMin: 0, 22 | xMax: 1000, 23 | yMax: 1000, 24 | macStyle: 0, 25 | lowestRecPPEM: 3, 26 | fontDirectionHint: 2, 27 | indexToLocFormat: 0, 28 | glyphDataFormat: 0, 29 | } 30 | 31 | const font: IFont = { 32 | characters: [], 33 | settings: { 34 | unitsPerEm: 1000, 35 | ascender: 800, 36 | descender: -200, 37 | } 38 | } 39 | 40 | it('parse data correctly', () => { 41 | assert.deepEqual(parse(unhex(data), 0, font), headTable) 42 | }) 43 | 44 | it('create data correctly', () => { 45 | expect(hex(create(headTable))).toBe(data) 46 | }) 47 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/hhea.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { parse, create } from '@/fontManager/tables/hhea' 3 | import { hex, unhex } from '../utils' 4 | import { IFont } from '@/fontManager/font' 5 | 6 | describe('hhea table', () => { 7 | const data = '00 01 00 00 03 20 FF 38 00 00 03 E8 00 00 00 00 03 E8 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02' 8 | 9 | const hheaTable = { 10 | majorVersion: 0x0001, 11 | minorVersion: 0x0000, 12 | ascender: 800, 13 | descender: -200, 14 | lineGap: 0, 15 | advanceWidthMax: 1000, 16 | minLeftSideBearing: 0, 17 | minRightSideBearing: 0, 18 | xMaxExtent: 1000, 19 | caretSlopeRise: 1, 20 | caretSlopeRun: 0, 21 | caretOffset: 0, 22 | reserved0: 0, 23 | reserved1: 0, 24 | reserved2: 0, 25 | reserved3: 0, 26 | metricDataFormat: 0, 27 | numberOfHMetrics: 2, 28 | } 29 | 30 | const font: IFont = { 31 | characters: [], 32 | settings: { 33 | unitsPerEm: 1000, 34 | ascender: 800, 35 | descender: -200, 36 | } 37 | } 38 | 39 | it('parse data correctly', () => { 40 | assert.deepEqual(parse(unhex(data), 0, font), hheaTable) 41 | }) 42 | 43 | it('create data correctly', () => { 44 | expect(hex(create(hheaTable))).toBe(data) 45 | }) 46 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/hmtx.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { parse, create } from '@/fontManager/tables/hmtx' 3 | import { hex, unhex } from '../utils' 4 | import { IFont } from '@/fontManager/font' 5 | 6 | describe('hmtx table', () => { 7 | const data = '01 F4 00 00 01 F4 00 00' 8 | 9 | const hmtxTable = { 10 | hMetrics: [ 11 | { 12 | advanceWidth: 500, 13 | lsb: 0, 14 | }, 15 | { 16 | advanceWidth: 500, 17 | lsb: 0, 18 | } 19 | ], 20 | leftSideBearings: [], 21 | } 22 | 23 | const font: IFont = { 24 | characters: [], 25 | settings: { 26 | unitsPerEm: 1000, 27 | ascender: 800, 28 | descender: -200, 29 | numberOfHMetrics: 2, 30 | numGlyphs: 2, 31 | } 32 | } 33 | 34 | it('parse data correctly', () => { 35 | assert.deepEqual(parse(unhex(data), 0, font), hmtxTable) 36 | }) 37 | 38 | it('create data correctly', () => { 39 | expect(hex(create(hmtxTable))).toBe(data) 40 | }) 41 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/maxp.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { parse, create } from '@/fontManager/tables/maxp' 3 | import { hex, unhex, decodeVersion } from '../utils' 4 | import { IFont } from '@/fontManager/font' 5 | 6 | describe('maxp table', () => { 7 | const data = '00 00 50 00 00 02' 8 | 9 | const maxpTable = { 10 | version: 0x00005000, 11 | numGlyphs: 2, 12 | } 13 | 14 | const font: IFont = { 15 | characters: [], 16 | settings: { 17 | unitsPerEm: 1000, 18 | ascender: 800, 19 | descender: -200, 20 | } 21 | } 22 | 23 | it('parse data correctly', () => { 24 | assert.deepEqual(parse(unhex(data), 0, font), Object.assign({}, { 25 | ...maxpTable, 26 | version: decodeVersion(maxpTable.version), 27 | })) 28 | }) 29 | 30 | it('create data correctly', () => { 31 | expect(hex(create(maxpTable))).toBe(data) 32 | }) 33 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/os_2.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { parse, create } from '@/fontManager/tables/os_2' 3 | import { hex, unhex } from '../utils' 4 | import { IFont } from '@/fontManager/font' 5 | 6 | describe('os/2 table', () => { 7 | interface ITag { 8 | tagArr: Array, 9 | tagStr: string, 10 | } 11 | 12 | const data = '00 03 03 E8 00 00 00 00 00 00 02 8A 02 BB 00 00 00 8C 02 8A 02 BB 00 00 01 DF 00 31 01 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 58 58 58 58 00 00 00 00 00 00 03 20 FF 38 00 00 03 E8 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00' 13 | 14 | const os2Table = { 15 | version: 0x0003, 16 | xAvgCharWidth: Math.round(1000), 17 | usWeightClass: 0, 18 | usWidthClass: 0, 19 | fsType: 0, 20 | ySubscriptXSize: 650, 21 | ySubscriptYSize: 699, 22 | ySubscriptXOffset: 0, 23 | ySubscriptYOffset: 140, 24 | ySuperscriptXSize: 650, 25 | ySuperscriptYSize: 699, 26 | ySuperscriptXOffset: 0, 27 | ySuperscriptYOffset: 479, 28 | yStrikeoutSize: 49, 29 | yStrikeoutPosition: 258, 30 | sFamilyClass: 0, 31 | panose: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 32 | ulUnicodeRange1: 0, 33 | ulUnicodeRange2: 0, 34 | ulUnicodeRange3: 0, 35 | ulUnicodeRange4: 0, 36 | achVendID: 'XXXX', 37 | fsSelection: 0, 38 | usFirstCharIndex: 0, 39 | usLastCharIndex: 0, 40 | sTypoAscender: 800, 41 | sTypoDescender: -200, 42 | sTypoLineGap: 0, 43 | usWinAscent: 1000, 44 | usWinDescent: 0, 45 | ulCodePageRange1: 1, 46 | ulCodePageRange2: 0, 47 | sxHeight: 0, 48 | sCapHeight: 0, 49 | usDefaultChar: 0, 50 | usBreakChar: 0, 51 | usMaxContext: 0, 52 | } 53 | 54 | const font: IFont = { 55 | characters: [], 56 | settings: { 57 | unitsPerEm: 1000, 58 | ascender: 800, 59 | descender: -200, 60 | } 61 | } 62 | 63 | it('parse data correctly', () => { 64 | const table = parse(unhex(data), 0, font) 65 | table.achVendID = (table.achVendID as ITag).tagStr 66 | assert.deepEqual(table, os2Table) 67 | }) 68 | 69 | it('create data correctly', () => { 70 | expect(hex(create(os2Table))).toBe(data) 71 | }) 72 | }) -------------------------------------------------------------------------------- /src/test/fontManager/tables/post.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { parse, create } from '@/fontManager/tables/post' 3 | import { hex, unhex, decodeVersion } from '../utils' 4 | import { IFont } from '@/fontManager/font' 5 | 6 | describe('post table', () => { 7 | const data = '00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' 8 | 9 | const postTable = { 10 | version: 0x00030000, 11 | italicAngle: 0, 12 | underlinePosition: 0, 13 | underlineThickness: 0, 14 | isFixedPitch: 0, 15 | minMemType42: 0, 16 | maxMemType42: 0, 17 | minMemType1: 0, 18 | maxMemType1: 0, 19 | } 20 | 21 | const font: IFont = { 22 | characters: [], 23 | settings: { 24 | unitsPerEm: 1000, 25 | ascender: 800, 26 | descender: -200, 27 | } 28 | } 29 | 30 | it('parse data correctly', () => { 31 | assert.deepEqual(parse(unhex(data), 0, font), Object.assign({}, { 32 | ...postTable, 33 | version: decodeVersion(postTable.version) 34 | })) 35 | }) 36 | 37 | it('create data correctly', () => { 38 | expect(hex(create(postTable))).toBe(data) 39 | }) 40 | }) -------------------------------------------------------------------------------- /src/test/fontManager/utils.ts: -------------------------------------------------------------------------------- 1 | const hex = (bytes) => { 2 | const values = [] 3 | for (let i = 0; i < bytes.length; i++) { 4 | const b = bytes[i] 5 | if (b < 16) { 6 | values.push('0' + b.toString(16)) 7 | } else { 8 | values.push(b.toString(16)) 9 | } 10 | } 11 | 12 | return values.join(' ').toUpperCase() 13 | } 14 | 15 | const unhex = (str) => { 16 | str = str.split(' ').join('') 17 | const len = str.length / 2 18 | const data = new DataView(new ArrayBuffer(len), 0) 19 | for (let i = 0; i < len; i++) { 20 | data.setUint8(i, parseInt(str.slice(i * 2, i * 2 + 2), 16)) 21 | } 22 | 23 | return data 24 | } 25 | 26 | const decodeVersion = (v) => { 27 | const data = new DataView(new Uint8Array([(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]).buffer) 28 | const major = data.getUint16(0) 29 | const minor = data.getUint16(2) 30 | return major + minor / 0x1000 / 10 31 | } 32 | 33 | export { hex, unhex, decodeVersion } -------------------------------------------------------------------------------- /src/test/stores/addComponentForCurrentCharacterFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, expect } from 'vitest' 2 | import { addComponentForCurrentCharacterFile, files, editCharacterFileUUID, IComponent, selectedFileUUID } from '@/fontEditor/stores/files' 3 | import { genUUID, toUnicode } from '@/utils/string' 4 | import { genRectComponent } from '@/fontEditor/tools/rectangle' 5 | 6 | describe('add component for current character', () => { 7 | files.value = [{ 8 | uuid: 'file-1', 9 | characterList: [ 10 | { 11 | uuid: 'char-1', 12 | type: 'text', 13 | character: { 14 | uuid: genUUID(), 15 | text: 'a', 16 | unicode: toUnicode('a'), 17 | //@ts-ignore 18 | components: [], 19 | }, 20 | components: [], 21 | groups: [], 22 | orderedList: [], 23 | selectedComponentsUUIDs: [], 24 | view: { 25 | zoom: 100, 26 | translateX: 0, 27 | translateY: 0, 28 | } 29 | } 30 | ], 31 | name: 'file1', 32 | width: 500, 33 | height: 500, 34 | saved: false, 35 | iconsCount: 0, 36 | }] 37 | selectedFileUUID.value = 'file-1' 38 | editCharacterFileUUID.value = 'char-1' 39 | const rectX = 10 40 | const rectY = 20 41 | const rectWidth = 50 42 | const rectHeight = 100 43 | const rect = genRectComponent( 44 | rectX, 45 | rectY, 46 | rectWidth, 47 | rectHeight, 48 | ) as unknown as IComponent 49 | addComponentForCurrentCharacterFile(rect) 50 | 51 | it('components\' has correct length after adding', () => { 52 | expect(files.value[0].characterList[0].components.length).toBe(1) 53 | }) 54 | 55 | it('add componnent for current editing character correctly', () => { 56 | assert.deepEqual(files.value[0].characterList[0].components[0], rect) 57 | }) 58 | }) -------------------------------------------------------------------------------- /src/test/stores/addFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, vi, beforeEach, afterEach, expect } from 'vitest' 2 | import { addFile, files } from '@/fontEditor/stores/files' 3 | import { genUUID } from '@/utils/string' 4 | 5 | describe('add file', () => { 6 | beforeEach(() => { 7 | vi.mock('nanoid', () => { 8 | return { nanoid: () => 'xxxxxx' } 9 | }) 10 | }) 11 | 12 | afterEach(() => { 13 | vi.restoreAllMocks() 14 | }) 15 | 16 | it('has correct files\' length after adding files', () => { 17 | files.value = [] 18 | const file1 = { 19 | uuid: genUUID(), 20 | characterList: [], 21 | name: 'file1', 22 | width: 500, 23 | height: 500, 24 | saved: false, 25 | iconsCount: 0, 26 | } 27 | const file2 = { 28 | uuid: genUUID(), 29 | characterList: [], 30 | name: 'file2', 31 | width: 100, 32 | height: 100, 33 | saved: false, 34 | iconsCount: 0, 35 | } 36 | addFile(file1) 37 | addFile(file2) 38 | expect(files.value.length).toBe(2) 39 | }) 40 | 41 | it('add file correctly into files array', () => { 42 | files.value = [] 43 | const file1 = { 44 | uuid: genUUID(), 45 | characterList: [], 46 | name: 'file1', 47 | width: 500, 48 | height: 500, 49 | saved: false, 50 | iconsCount: 0, 51 | } 52 | addFile(file1) 53 | assert.deepEqual(files.value[0], file1) 54 | }) 55 | }) -------------------------------------------------------------------------------- /src/test/stores/insertComponentForCurrentCharacterFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, expect } from 'vitest' 2 | import { insertComponentForCurrentCharacterFile, files, editCharacterFileUUID, IComponent, selectedFileUUID, IComponentValue } from '@/fontEditor/stores/files' 3 | import { genUUID, toUnicode } from '@/utils/string' 4 | import { genRectComponent } from '@/fontEditor/tools/rectangle' 5 | 6 | describe('add component for current character', () => { 7 | files.value = [{ 8 | uuid: 'file-1', 9 | characterList: [ 10 | { 11 | uuid: 'char-1', 12 | type: 'text', 13 | character: { 14 | uuid: genUUID(), 15 | text: 'a', 16 | unicode: toUnicode('a'), 17 | //@ts-ignore 18 | components: [], 19 | }, 20 | components: [ 21 | { 22 | uuid: 'comp-1', 23 | type: 'rectangle', 24 | name: 'rectangle', 25 | lock: false, 26 | visible: true, 27 | value: { 28 | width: 10, 29 | height: 10, 30 | fillColor: '', 31 | strokeColor: '#000', 32 | } as unknown as IComponentValue, 33 | x: 0, 34 | y: 0, 35 | w: 10, 36 | h: 10, 37 | rotation: 0, 38 | flipX: false, 39 | flipY: false, 40 | usedInCharacter: true, 41 | } 42 | ], 43 | groups: [], 44 | orderedList: [{ uuid: 'comp-1', type: 'rectangle' }], 45 | selectedComponentsUUIDs: [], 46 | view: { 47 | zoom: 100, 48 | translateX: 0, 49 | translateY: 0, 50 | } 51 | } 52 | ], 53 | name: 'file1', 54 | width: 500, 55 | height: 500, 56 | saved: false, 57 | iconsCount: 0, 58 | }] 59 | selectedFileUUID.value = 'file-1' 60 | editCharacterFileUUID.value = 'char-1' 61 | const insertedRect = genRectComponent( 62 | 50, 63 | 50, 64 | 100, 65 | 100, 66 | ) as unknown as IComponent 67 | insertedRect.uuid = 'comp-2' 68 | insertComponentForCurrentCharacterFile(insertedRect, { 69 | uuid: 'comp-1', 70 | pos: 'prev', 71 | }) 72 | 73 | it('components\' has correct length after inserting', () => { 74 | expect(files.value[0].characterList[0].components.length).toBe(2) 75 | }) 76 | 77 | it('insert componnent correctly (just push into component list)', () => { 78 | assert.deepEqual(files.value[0].characterList[0].components[1], insertedRect) 79 | }) 80 | 81 | it('insert componnent into ordered list correctly (update order list for ordering)', () => { 82 | expect(files.value[0].characterList[0].orderedList[0].uuid).toBe('comp-2') 83 | }) 84 | }) -------------------------------------------------------------------------------- /src/test/stores/modifyComponentForCurrentCharacterFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { modifyComponentForCurrentCharacterFile, files, editCharacterFileUUID, selectedFileUUID, IComponentValue } from '@/fontEditor/stores/files' 3 | import { genUUID, toUnicode } from '@/utils/string' 4 | 5 | describe('add component for current character', () => { 6 | files.value = [{ 7 | uuid: 'file-1', 8 | characterList: [ 9 | { 10 | uuid: 'char-1', 11 | type: 'text', 12 | character: { 13 | uuid: genUUID(), 14 | text: 'a', 15 | unicode: toUnicode('a'), 16 | //@ts-ignore 17 | components: [], 18 | }, 19 | components: [ 20 | { 21 | uuid: 'comp-1', 22 | type: 'rectangle', 23 | name: 'rectangle', 24 | lock: false, 25 | visible: true, 26 | value: { 27 | width: 10, 28 | height: 10, 29 | fillColor: '', 30 | strokeColor: '#000', 31 | } as unknown as IComponentValue, 32 | x: 0, 33 | y: 0, 34 | w: 10, 35 | h: 10, 36 | rotation: 0, 37 | flipX: false, 38 | flipY: false, 39 | usedInCharacter: true, 40 | } 41 | ], 42 | groups: [], 43 | orderedList: [{ uuid: 'comp-1', type: 'rectangle' }], 44 | selectedComponentsUUIDs: [], 45 | view: { 46 | zoom: 100, 47 | translateX: 0, 48 | translateY: 0, 49 | } 50 | } 51 | ], 52 | name: 'file1', 53 | width: 500, 54 | height: 500, 55 | saved: false, 56 | iconsCount: 0, 57 | }] 58 | selectedFileUUID.value = 'file-1' 59 | editCharacterFileUUID.value = 'char-1' 60 | modifyComponentForCurrentCharacterFile('comp-1', { 61 | x: 10, 62 | y: 10, 63 | w: 100, 64 | h: 100, 65 | rotation: 10, 66 | flipX: true, 67 | flipY: true, 68 | usedInCharacter: false, 69 | value: { 70 | width: 100, 71 | height: 100, 72 | fillColor: 'white', 73 | strokeColor: '#fff', 74 | } 75 | }) 76 | 77 | it('modify x correctly', () => { 78 | expect(files.value[0].characterList[0].components[0].x).toBe(10) 79 | }) 80 | 81 | it('modify y correctly', () => { 82 | expect(files.value[0].characterList[0].components[0].y).toBe(10) 83 | }) 84 | 85 | it('modify w correctly', () => { 86 | expect(files.value[0].characterList[0].components[0].w).toBe(100) 87 | }) 88 | 89 | it('modify h correctly', () => { 90 | expect(files.value[0].characterList[0].components[0].h).toBe(100) 91 | }) 92 | 93 | it('modify rotation correctly', () => { 94 | expect(files.value[0].characterList[0].components[0].rotation).toBe(10) 95 | }) 96 | 97 | it('modify flipX correctly', () => { 98 | expect(files.value[0].characterList[0].components[0].flipX).toBe(true) 99 | }) 100 | 101 | it('modify flipY correctly', () => { 102 | expect(files.value[0].characterList[0].components[0].flipY).toBe(true) 103 | }) 104 | 105 | it('modify usedInCharacter correctly', () => { 106 | expect(files.value[0].characterList[0].components[0].usedInCharacter).toBe(false) 107 | }) 108 | 109 | it('modify value correctly', () => { 110 | assert.deepEqual(files.value[0].characterList[0].components[0].value, { 111 | width: 100, 112 | height: 100, 113 | fillColor: 'white', 114 | strokeColor: '#fff', 115 | } as unknown as IComponentValue) 116 | }) 117 | }) -------------------------------------------------------------------------------- /src/test/stores/removeComponentForCurrentCharacterFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { removeComponentForCurrentCharacterFile, files, editCharacterFileUUID, selectedFileUUID, IComponentValue } from '@/fontEditor/stores/files' 3 | import { genUUID, toUnicode } from '@/utils/string' 4 | 5 | describe('add component for current character', () => { 6 | files.value = [{ 7 | uuid: 'file-1', 8 | characterList: [ 9 | { 10 | uuid: 'char-1', 11 | type: 'text', 12 | character: { 13 | uuid: genUUID(), 14 | text: 'a', 15 | unicode: toUnicode('a'), 16 | //@ts-ignore 17 | components: [], 18 | }, 19 | components: [ 20 | { 21 | uuid: 'comp-1', 22 | type: 'rectangle', 23 | name: 'rectangle', 24 | lock: false, 25 | visible: true, 26 | value: { 27 | width: 10, 28 | height: 10, 29 | fillColor: '', 30 | strokeColor: '#000', 31 | } as unknown as IComponentValue, 32 | x: 0, 33 | y: 0, 34 | w: 10, 35 | h: 10, 36 | rotation: 0, 37 | flipX: false, 38 | flipY: false, 39 | usedInCharacter: true, 40 | } 41 | ], 42 | groups: [], 43 | orderedList: [{ uuid: 'comp-1', type: 'rectangle' }], 44 | selectedComponentsUUIDs: [], 45 | view: { 46 | zoom: 100, 47 | translateX: 0, 48 | translateY: 0, 49 | } 50 | } 51 | ], 52 | name: 'file1', 53 | width: 500, 54 | height: 500, 55 | saved: false, 56 | iconsCount: 0, 57 | }] 58 | selectedFileUUID.value = 'file-1' 59 | editCharacterFileUUID.value = 'char-1' 60 | removeComponentForCurrentCharacterFile('comp-1') 61 | 62 | it('components\' has correct length after removing', () => { 63 | expect(files.value[0].characterList[0].components.length).toBe(0) 64 | }) 65 | }) -------------------------------------------------------------------------------- /src/test/stores/removeFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, vi, beforeEach, afterEach, expect } from 'vitest' 2 | import { removeFile, files } from '@/fontEditor/stores/files' 3 | 4 | describe('add file', () => { 5 | beforeEach(() => { 6 | vi.mock('nanoid', () => { 7 | return { nanoid: () => 'xxxxxx' } 8 | }) 9 | }) 10 | 11 | afterEach(() => { 12 | vi.restoreAllMocks() 13 | }) 14 | 15 | it('remove file correctly', () => { 16 | const file1 = { 17 | uuid: '1', 18 | characterList: [], 19 | name: 'file1', 20 | width: 500, 21 | height: 500, 22 | saved: false, 23 | iconsCount: 0, 24 | } 25 | const file2 = { 26 | uuid: '2', 27 | characterList: [], 28 | name: 'file2', 29 | width: 100, 30 | height: 100, 31 | saved: false, 32 | iconsCount: 0, 33 | } 34 | files.value = [file1, file2] 35 | removeFile('1') 36 | expect(files.value.length).toBe(1) 37 | assert.deepEqual(files.value[0], file2) 38 | }) 39 | }) -------------------------------------------------------------------------------- /src/test/tools/ellipse.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, vi, beforeEach, afterEach } from 'vitest' 2 | import { genEllipseComponent } from '@/fontEditor/tools/ellipse' 3 | import { genUUID } from '@/utils/string' 4 | 5 | describe('ellipse tool', () => { 6 | beforeEach(() => { 7 | vi.mock('nanoid', () => { 8 | return { nanoid: () => 'xxxxxx' } 9 | }) 10 | }) 11 | 12 | afterEach(() => { 13 | vi.restoreAllMocks() 14 | }) 15 | 16 | it('generate correct ellipse component', () => { 17 | const ellipseX = 50 18 | const ellipseY = 100 19 | const radiusX = 40 20 | const radiusY = 20 21 | const ellipse1 = { 22 | uuid: genUUID(), 23 | type: 'ellipse', 24 | name: 'ellipse', 25 | lock: false, 26 | visible: true, 27 | value: { 28 | radiusX: radiusX, 29 | radiusY: radiusY, 30 | fillColor: '', 31 | strokeColor: '#000', 32 | }, 33 | x: 50, 34 | y: 100, 35 | w: 80, 36 | h: 40, 37 | rotation: 0, 38 | flipX: false, 39 | flipY: false, 40 | usedInCharacter: true, 41 | } 42 | const ellipse2 = genEllipseComponent(ellipseX, ellipseY, radiusX, radiusY) 43 | //@ts-ignore 44 | delete ellipse2.value.preview 45 | //@ts-ignore 46 | delete ellipse2.value.contour 47 | //@ts-ignore 48 | assert.deepEqual(ellipse1, ellipse2) 49 | }) 50 | }) -------------------------------------------------------------------------------- /src/test/tools/pen.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, vi, beforeEach, afterEach } from 'vitest' 2 | import { genPenComponent } from '@/fontEditor/tools/pen' 3 | import { genUUID } from '@/utils/string' 4 | 5 | describe('pen tool', () => { 6 | beforeEach(() => { 7 | vi.mock('nanoid', () => { 8 | return { nanoid: () => 'xxxxxx' } 9 | }) 10 | }) 11 | 12 | afterEach(() => { 13 | vi.restoreAllMocks() 14 | }) 15 | 16 | it('generate correct pen component', () => { 17 | const points = JSON.parse('[{"uuid":"H6KVuacOKld2sod4qRrYu","type":"anchor","x":183,"y":116,"origin":null,"isShow":true},{"uuid":"FjPN70Y5cjPduRVzEHr8G","type":"control","x":202.25,"y":87.65,"origin":"H6KVuacOKld2sod4qRrYu","isShow":true},{"uuid":"FfFRWBkOWAJbHZ5yhenRX","type":"control","x":253.05,"y":58.449999999999996,"origin":"7qOC_If1exPFlrkOwJVeW","isShow":true},{"uuid":"7qOC_If1exPFlrkOwJVeW","type":"anchor","x":281,"y":102,"origin":null,"isShow":true},{"uuid":"D9H4jF22zNSZXzTeIvG8k","type":"control","x":324,"y":169,"origin":"7qOC_If1exPFlrkOwJVeW","isShow":true},{"uuid":"d0wNWDOQqejnpGLetGvGa","type":"control","x":354.05,"y":214.15,"origin":"TL9E_zrb2eIjeSc1ofdWP","isShow":true},{"uuid":"TL9E_zrb2eIjeSc1ofdWP","type":"anchor","x":304,"y":233,"origin":null,"isShow":true},{"uuid":"2Iy6zIRsFyTLQ5sWz5Cen","type":"control","x":227,"y":262,"origin":"TL9E_zrb2eIjeSc1ofdWP","isShow":true},{"uuid":"gOoAWiYqAaH7wPI3z15qG","type":"control","x":225.35,"y":260.15,"origin":"HXQUQ1H36drbLzSZxWzV-","isShow":true},{"uuid":"HXQUQ1H36drbLzSZxWzV-","type":"anchor","x":200,"y":240,"origin":null,"isShow":true},{"uuid":"Fh57yjYmxnWlEEE-7Pimh","type":"control","x":161,"y":209,"origin":"HXQUQ1H36drbLzSZxWzV-","isShow":true},{"uuid":"MNCZEuXOUpeEQvMzZ-omH","type":"control","x":187.55,"y":135.5,"origin":"ZSY2soQHtlbYn5BePiDwe","isShow":true},{"uuid":"ZSY2soQHtlbYn5BePiDwe","type":"anchor","x":183,"y":116,"origin":null,"isShow":true},{"uuid":"p9ImBNEDbsGOZEFo39wDB","type":"control","x":176,"y":86,"origin":"ZSY2soQHtlbYn5BePiDwe","isShow":true}]') 18 | const w = 193.05 19 | const h = 203.55 20 | const x = 161 21 | const y = 58.449999999999996 22 | const closePath = true 23 | const pen1 = { 24 | uuid: genUUID(), 25 | type: 'pen', 26 | name: 'pen', 27 | lock: false, 28 | visible: true, 29 | value: { 30 | points: points, 31 | fillColor: '', 32 | strokeColor: '#000', 33 | closePath, 34 | editMode: false, 35 | }, 36 | x, 37 | y, 38 | w, 39 | h, 40 | rotation: 0, 41 | flipX: false, 42 | flipY: false, 43 | usedInCharacter: true, 44 | } 45 | const pen2 = genPenComponent(points, closePath) 46 | //@ts-ignore 47 | delete pen2.value.preview 48 | //@ts-ignore 49 | delete pen2.value.contour 50 | //@ts-ignore 51 | assert.deepEqual(pen1, pen2) 52 | }) 53 | }) -------------------------------------------------------------------------------- /src/test/tools/polygon.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, vi, beforeEach, afterEach } from 'vitest' 2 | import { genPolygonComponent } from '@/fontEditor/tools/polygon' 3 | import { genUUID } from '@/utils/string' 4 | 5 | describe('polygon tool', () => { 6 | beforeEach(() => { 7 | vi.mock('nanoid', () => { 8 | return { nanoid: () => 'xxxxxx' } 9 | }) 10 | }) 11 | 12 | afterEach(() => { 13 | vi.restoreAllMocks() 14 | }) 15 | 16 | it('generate correct polygon component', () => { 17 | const points = [ 18 | { uuid: genUUID(), x: 50, y: 50 }, 19 | { uuid: genUUID(), x: 100, y: 50 }, 20 | { uuid: genUUID(), x: 100, y: 100 }, 21 | { uuid: genUUID(), x: 50, y: 100 }, 22 | { uuid: genUUID(), x: 50, y: 50 }, 23 | ] 24 | const closePath = true 25 | const polygon1 = { 26 | uuid: genUUID(), 27 | type: 'polygon', 28 | name: 'polygon', 29 | lock: false, 30 | visible: true, 31 | value: { 32 | points: points, 33 | fillColor: '', 34 | strokeColor: '#000', 35 | closePath, 36 | }, 37 | x: 50, 38 | y: 50, 39 | w: 50, 40 | h: 50, 41 | rotation: 0, 42 | flipX: false, 43 | flipY: false, 44 | usedInCharacter: true, 45 | } 46 | const polygon2 = genPolygonComponent(points, closePath) 47 | //@ts-ignore 48 | delete polygon2.value.preview 49 | //@ts-ignore 50 | delete polygon2.value.contour 51 | //@ts-ignore 52 | assert.deepEqual(polygon1, polygon2) 53 | }) 54 | }) -------------------------------------------------------------------------------- /src/test/tools/rectangle.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert, vi, beforeEach, afterEach } from 'vitest' 2 | import { genRectComponent } from '@/fontEditor/tools/rectangle' 3 | import { genUUID } from '@/utils/string' 4 | 5 | describe('rectange tool', () => { 6 | beforeEach(() => { 7 | vi.mock('nanoid', () => { 8 | return { nanoid: () => 'xxxxxx' } 9 | }) 10 | }) 11 | 12 | afterEach(() => { 13 | vi.restoreAllMocks() 14 | }) 15 | 16 | it('generate correct rectangle component', () => { 17 | const rectX = 10 18 | const rectY = 20 19 | const rectWidth = 50 20 | const rectHeight = 100 21 | const rect1 = { 22 | uuid: genUUID(), 23 | type: 'rectangle', 24 | name: 'rectangle', 25 | lock: false, 26 | visible: true, 27 | value: { 28 | width: rectWidth, 29 | height: rectHeight, 30 | fillColor: '', 31 | strokeColor: '#000', 32 | }, 33 | x: 10, 34 | y: 20, 35 | w: 50, 36 | h: 100, 37 | rotation: 0, 38 | flipX: false, 39 | flipY: false, 40 | usedInCharacter: true, 41 | } 42 | const rect2 = genRectComponent( 43 | rectX, 44 | rectY, 45 | rectWidth, 46 | rectHeight, 47 | ) 48 | //@ts-ignore 49 | delete rect2.value.preview 50 | //@ts-ignore 51 | delete rect2.value.contour 52 | //@ts-ignore 53 | assert.deepEqual(rect1, rect2) 54 | }) 55 | }) -------------------------------------------------------------------------------- /src/test/utils/canvas.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { 3 | mapCanvasCoords, 4 | mapCanvasX, 5 | mapCanvasY, 6 | mapCanvasWidth, 7 | mapCanvasHeight, 8 | unMapCanvasWidth, 9 | unMapCanvasHeight, 10 | } from '@/utils/canvas' 11 | 12 | describe('canvas util methods', () => { 13 | it('mapCanvasCoords correctly', () => { 14 | assert.deepEqual(mapCanvasCoords({ x: 10, y: 20 }), { x: 20, y: 40 }) 15 | }) 16 | 17 | it('mapCanvasX correctly', () => { 18 | expect(mapCanvasX(10)).toBe(20) 19 | }) 20 | 21 | it('mapCanvasY correctly', () => { 22 | expect(mapCanvasY(20)).toBe(40) 23 | }) 24 | 25 | it('mapCanvasWidth correctly', () => { 26 | expect(mapCanvasWidth(100)).toBe(200) 27 | }) 28 | 29 | it('mapCanvasHeight correctly', () => { 30 | expect(mapCanvasHeight(100)).toBe(200) 31 | }) 32 | 33 | it('unMapCanvasWidth correctly', () => { 34 | expect(unMapCanvasWidth(200)).toBe(100) 35 | }) 36 | 37 | it('unMapCanvasHeight correctly', () => { 38 | expect(unMapCanvasHeight(200)).toBe(100) 39 | }) 40 | }) -------------------------------------------------------------------------------- /src/test/utils/data.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, assert } from 'vitest' 2 | import { listToMap } from '@/utils/data' 3 | 4 | describe('data util methods', () => { 5 | it('convert list to map correctly', () => { 6 | assert.deepEqual(listToMap([ 7 | { 8 | uuid: '1', 9 | x: 1, 10 | y: 2, 11 | }, 12 | { 13 | uuid: '2', 14 | x: 3, 15 | y: 4, 16 | }, 17 | { 18 | uuid: '3', 19 | x: 5, 20 | y: 6, 21 | } 22 | ], 'uuid'), { 23 | '1': { 24 | uuid: '1', 25 | x: 1, 26 | y: 2, 27 | }, 28 | '2': { 29 | uuid: '2', 30 | x: 3, 31 | y: 4, 32 | }, 33 | '3': { 34 | uuid: '3', 35 | x: 5, 36 | y: 6, 37 | } 38 | }) 39 | }) 40 | }) -------------------------------------------------------------------------------- /src/test/utils/math.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, assert } from 'vitest' 2 | import { 3 | isNearPoint, 4 | distance, 5 | getBound, 6 | rotatePoint, 7 | inComponentBound, 8 | angleBetween, 9 | transformPoints, 10 | getEllipsePoints, 11 | getRectanglePoints, 12 | } from '@/utils/math' 13 | import { IComponentValue } from '@/fontEditor/stores/files' 14 | 15 | describe('math util methods', () => { 16 | it('calculate isNearPoint correctly', () => { 17 | expect(isNearPoint(10, 10, 12, 12, 5)).toBe(true) 18 | expect(isNearPoint(10, 10, 12, 12, 1)).toBe(false) 19 | }) 20 | 21 | it('calculate distance correctly', () => { 22 | expect(distance(10, 10, 12, 12)).toBe(Math.sqrt(8)) 23 | }) 24 | 25 | it('getBound correctly', () => { 26 | assert.deepEqual(getBound([ 27 | { x: 10, y: 10 }, 28 | { x: 5, y: 20 }, 29 | { x: 15, y: 20 }, 30 | { x: 10, y: 10 }, 31 | ]), { 32 | x: 5, 33 | y: 10, 34 | w: 10, 35 | h: 10, 36 | }) 37 | }) 38 | 39 | it('rotatePoint correctly', () => { 40 | assert.deepEqual( 41 | rotatePoint({x: 10, y: 20}, {x: 0, y: 0}, 30), 42 | {x: -1.339745962155611, y: 22.320508075688775} 43 | ) 44 | }) 45 | 46 | it('calculate inComponentBound correctly', () => { 47 | const rect = { 48 | uuid: 'comp-1', 49 | type: 'rectangle', 50 | name: 'rectangle', 51 | lock: false, 52 | visible: true, 53 | value: { 54 | width: 10, 55 | height: 10, 56 | fillColor: '', 57 | strokeColor: '#000', 58 | } as unknown as IComponentValue, 59 | x: 0, 60 | y: 0, 61 | w: 10, 62 | h: 10, 63 | rotation: 0, 64 | flipX: false, 65 | flipY: false, 66 | usedInCharacter: true, 67 | } 68 | expect(inComponentBound({ x: 4, y: 6 }, rect)).toBe(true) 69 | }) 70 | 71 | it('calculate angleBetween correctly', () => { 72 | expect(angleBetween({x: 10, y: 20}, {x: 50, y: 50})).toBe(18) 73 | }) 74 | 75 | it('transformPoints correctly', () => { 76 | assert.deepEqual(transformPoints( 77 | [ 78 | { x: 10, y: 20 }, 79 | { x: 20, y: 40 }, 80 | { x: 10, y: 40 }, 81 | { x: 10, y: 20 } 82 | ], 83 | { x: 50, y: 50, w: 5, h: 5, rotation: 30, flipX: false, flipY: false } 84 | ), 85 | [ 86 | {x: 52.8349364905389, y: 49.7099364905389}, 87 | {x: 52.1650635094611, y: 55.2900635094611}, 88 | {x: 47.83493649053891, y: 54.0400635094611}, 89 | {x: 52.8349364905389, y: 49.7099364905389}, 90 | ]) 91 | }) 92 | 93 | it('getEllipsePoints correctly', () => { 94 | assert.deepEqual(getEllipsePoints(20, 40, 10, 0, 0), [ 95 | {x: -20, y: 0}, 96 | {x: -12, y: 32}, 97 | {x: -4, y: 39.191835884530846}, 98 | {x: 4, y: 39.191835884530846}, 99 | {x: 12, y: 32}, 100 | {x: 12, y: -32}, 101 | {x: 4, y: -39.191835884530846}, 102 | {x: -4, y: -39.191835884530846}, 103 | {x: -12, y: -32}, 104 | {x: -20, y: 0}, 105 | ]) 106 | }) 107 | 108 | it('getRectanglePoints correctly', () => { 109 | assert.deepEqual(getRectanglePoints(20, 40, 0, 0), [ 110 | {x: 0, y: 0}, 111 | {x: 20, y: 0}, 112 | {x: 20, y: 40}, 113 | {x: 0, y: 40}, 114 | ]) 115 | }) 116 | }) -------------------------------------------------------------------------------- /src/test/utils/string.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { toUnicode } from '@/utils/string' 3 | 4 | describe('string util methods', () => { 5 | it('convert char to unicode correctly', () => { 6 | expect(toUnicode('a')).toBe('61') 7 | }) 8 | }) -------------------------------------------------------------------------------- /src/utils/canvas.ts: -------------------------------------------------------------------------------- 1 | import { selectedFile } from '../fontEditor/stores/files' 2 | import { editStatus, Status } from '../fontEditor/stores/font' 3 | import { width } from '../fontEditor/stores/global' 4 | 5 | const ratio = 2 6 | const default_unitsPerEm = 1000 7 | 8 | export const mapCanvasCoords = (point: { x: number, y: number }) => { 9 | return { 10 | x: ratio * point.x, 11 | y: ratio * point.y, 12 | } 13 | } 14 | 15 | export const mapCanvasX = (x: number) => { 16 | return ratio * x 17 | } 18 | 19 | export const mapCanvasY = (y: number) => { 20 | return ratio * y 21 | } 22 | 23 | export const mapCanvasWidth = (width: number) => { 24 | return ratio * width 25 | } 26 | 27 | export const mapCanvasHeight = (height: number) => { 28 | return ratio * height 29 | } 30 | 31 | export const unMapCanvasWidth = (width: number) => { 32 | return width / ratio 33 | } 34 | 35 | export const unMapCanvasHeight = (height: number) => { 36 | return height / ratio 37 | } 38 | 39 | export const getCoord = (coord: number) => { 40 | if (editStatus.value === Status.Edit) { 41 | return coord / width.value * selectedFile.value.width 42 | } else if (editStatus.value === Status.Glyph) { 43 | return coord / width.value * default_unitsPerEm 44 | } 45 | return coord 46 | } -------------------------------------------------------------------------------- /src/utils/data.ts: -------------------------------------------------------------------------------- 1 | const listToMap = (list: Array, key: string) => { 2 | const map: Object = {} 3 | list.map((item: Object) => { 4 | // @ts-ignore 5 | map[item[key]] = item 6 | }) 7 | return map 8 | } 9 | 10 | export { 11 | listToMap, 12 | } -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid' 2 | 3 | const genUUID = () => { 4 | // function S4() { 5 | // return (((1 + Math.random())*0x10000)|0).toString(16).substring(1) 6 | // } 7 | // return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()) 8 | return nanoid() 9 | } 10 | 11 | const toUnicode = (character: string) => { 12 | return character.charCodeAt(0).toString(16) 13 | } 14 | 15 | const toIconUnicode = (count: number) => { 16 | const start = 41000 17 | return (start + count).toString(16) 18 | } 19 | 20 | export { 21 | genUUID, 22 | toUnicode, 23 | toIconUnicode, 24 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "allowSyntheticDefaultImports": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": false, 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | 18 | /* Linting */ 19 | "strict": false, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noFallthroughCasesInSwitch": true, 23 | 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | } 29 | }, 30 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 31 | "references": [{ "path": "./tsconfig.node.json" }] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | }, 9 | "include": ["**/*.ts", "**/*.d.ts", "**/*.vue"] 10 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | import copyPlugin from 'rollup-plugin-copy' 7 | 8 | import { visualizer } from 'rollup-plugin-visualizer' 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig({ 12 | //base: '/fontplayer_demo/', 13 | plugins: [ 14 | vue(), 15 | visualizer({ 16 | open: true, 17 | }) 18 | ].filter(Boolean), 19 | resolve: { 20 | alias: { 21 | '@': fileURLToPath(new URL('./src', import.meta.url)) 22 | }, 23 | }, 24 | build: { 25 | target: 'es2022', 26 | rollupOptions: { 27 | plugins: [ 28 | copyPlugin({ 29 | targets: [ 30 | { src: 'lib/**/*', dest: 'dist/lib' } 31 | ], 32 | hook: 'writeBundle', 33 | }), 34 | ] 35 | }, 36 | outDir: 'dist', 37 | assetsDir: 'assets', 38 | emptyOutDir: false, 39 | terserOptions: { 40 | compress: { 41 | drop_console: false // 确保不去掉 console 42 | } 43 | } 44 | }, 45 | define: { 46 | Module: {} 47 | }, 48 | }) 49 | --------------------------------------------------------------------------------