├── .env ├── .env.development ├── .gitignore ├── .prettierrc ├── .yarnrc ├── LICENSE ├── README.md ├── README_ZH.md ├── package.json ├── public ├── CNAME ├── favicon.ico ├── google46c5aeb0e05a4a13.html ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── scripts ├── build.js ├── createProtucol.js ├── getGitRelease.js └── sentry.js ├── src ├── App.test.tsx ├── app │ ├── App.tsx │ ├── components │ │ ├── AnglePicker │ │ │ ├── AnglePicker.module.css │ │ │ ├── AnglePicker.tsx │ │ │ └── index.ts │ │ ├── ColorInput │ │ │ ├── ColorInput.module.css │ │ │ ├── ColorInput.tsx │ │ │ └── index.ts │ │ ├── GradientPicker │ │ │ ├── ColorStop.module.scss │ │ │ ├── ColorStop.tsx │ │ │ ├── ColorStopsHolder.module.css │ │ │ ├── ColorStopsHolder.tsx │ │ │ ├── GradientBuilder.tsx │ │ │ └── index.ts │ │ ├── GridInput │ │ │ ├── GridInput.tsx │ │ │ └── index.ts │ │ ├── Palette │ │ │ ├── Palette.module.css │ │ │ ├── Palette.tsx │ │ │ └── index.ts │ │ └── WrappedSketchPicker │ │ │ ├── WrappedSketchPicker.tsx │ │ │ └── index.ts │ ├── hooks │ │ ├── useSpaceDrag.ts │ │ └── useWheel.ts │ ├── layout │ │ ├── LeftBar │ │ │ ├── LeftBar.tsx │ │ │ ├── index.ts │ │ │ └── modules │ │ │ │ ├── Font │ │ │ │ ├── Font.tsx │ │ │ │ ├── FontFamily.tsx │ │ │ │ ├── FontSize.tsx │ │ │ │ ├── LineHeight.tsx │ │ │ │ ├── Sharp.tsx │ │ │ │ └── index.ts │ │ │ │ ├── GlobalMetric │ │ │ │ ├── GlobalMetric.tsx │ │ │ │ └── index.ts │ │ │ │ ├── Glyphs │ │ │ │ ├── Glyphs.tsx │ │ │ │ └── index.ts │ │ │ │ └── PackConfig │ │ │ │ ├── AutoPack.tsx │ │ │ │ ├── FixedSize.tsx │ │ │ │ ├── PackConfig.tsx │ │ │ │ ├── PackHeight.tsx │ │ │ │ ├── PackWidth.tsx │ │ │ │ ├── Padding.tsx │ │ │ │ ├── Spacing.tsx │ │ │ │ └── index.ts │ │ ├── RightBar │ │ │ ├── RightBar.tsx │ │ │ ├── index.ts │ │ │ └── modules │ │ │ │ ├── BackgroundColor │ │ │ │ ├── BackgroundColor.tsx │ │ │ │ └── index.ts │ │ │ │ ├── Fill │ │ │ │ ├── Fill.tsx │ │ │ │ └── index.ts │ │ │ │ ├── Shadow │ │ │ │ ├── Shadow.tsx │ │ │ │ └── index.ts │ │ │ │ └── Stroke │ │ │ │ ├── Stroke.tsx │ │ │ │ └── index.ts │ │ ├── TitleBar │ │ │ ├── ButtonExport.tsx │ │ │ ├── ButtonNew.tsx │ │ │ ├── ButtonOpen.tsx │ │ │ ├── ButtonSave.tsx │ │ │ ├── TitleBar.module.css │ │ │ ├── TitleBar.tsx │ │ │ └── index.ts │ │ ├── WorkSpace │ │ │ ├── WorkSpace.module.css │ │ │ ├── WorkSpace.tsx │ │ │ ├── index.ts │ │ │ └── modules │ │ │ │ ├── ControlerBar │ │ │ │ ├── ControlerBar.module.css │ │ │ │ ├── ControlerBar.tsx │ │ │ │ └── index.ts │ │ │ │ ├── ImageGlyphList │ │ │ │ ├── ImageGlyph.module.scss │ │ │ │ ├── ImageGlyph.tsx │ │ │ │ ├── ImageGlyphList.module.scss │ │ │ │ ├── ImageGlyphList.tsx │ │ │ │ ├── LayerBox.module.scss │ │ │ │ ├── LayerBox.tsx │ │ │ │ └── index.ts │ │ │ │ ├── MainView │ │ │ │ ├── MainView.module.css │ │ │ │ ├── MainView.tsx │ │ │ │ └── index.ts │ │ │ │ ├── PackView │ │ │ │ ├── PackCanvas.module.scss │ │ │ │ ├── PackCanvas.tsx │ │ │ │ ├── PackSizeBar.module.css │ │ │ │ ├── PackSizeBar.tsx │ │ │ │ ├── PackView.tsx │ │ │ │ └── index.ts │ │ │ │ ├── Preview │ │ │ │ ├── LetterList.module.scss │ │ │ │ ├── LetterList.tsx │ │ │ │ ├── Preview.tsx │ │ │ │ ├── PreviewCanvas.module.scss │ │ │ │ ├── PreviewCanvas.tsx │ │ │ │ ├── PreviewKerning.tsx │ │ │ │ ├── PreviewMertic.tsx │ │ │ │ ├── PreviewText.tsx │ │ │ │ ├── getPreviewCanvas.ts │ │ │ │ └── index.ts │ │ │ │ └── ProjectTabs │ │ │ │ ├── ProjectTab.module.scss │ │ │ │ ├── ProjectTab.tsx │ │ │ │ ├── ProjectTabs.tsx │ │ │ │ └── index.ts │ │ ├── Wrap │ │ │ ├── UpdateToast.tsx │ │ │ ├── Wrap.module.css │ │ │ ├── Wrap.tsx │ │ │ └── index.ts │ │ └── common │ │ │ ├── FormAdjustMetric │ │ │ ├── FormAdjustMetric.tsx │ │ │ └── index.ts │ │ │ ├── FormAngle │ │ │ ├── FormAngle.tsx │ │ │ └── index.ts │ │ │ ├── FormColor │ │ │ ├── FormColor.tsx │ │ │ └── index.ts │ │ │ ├── FormFill │ │ │ ├── FormFill.tsx │ │ │ └── index.ts │ │ │ ├── FormGradient │ │ │ ├── FormGradient.tsx │ │ │ └── index.ts │ │ │ └── FormImage │ │ │ ├── FileSelector.tsx │ │ │ ├── FormImage.tsx │ │ │ └── index.ts │ └── theme │ │ ├── components.ts │ │ └── index.ts ├── file │ ├── conversion │ │ ├── index.ts │ │ └── types │ │ │ ├── index.ts │ │ │ ├── littera │ │ │ ├── check.ts │ │ │ ├── decode.ts │ │ │ ├── index.ts │ │ │ └── schema │ │ │ │ ├── background.ts │ │ │ │ ├── bevel.ts │ │ │ │ ├── fill.ts │ │ │ │ ├── font.ts │ │ │ │ ├── glow.ts │ │ │ │ ├── glyphs.ts │ │ │ │ ├── index.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── shadow.ts │ │ │ │ └── stroke.ts │ │ │ ├── sbf │ │ │ ├── check.ts │ │ │ ├── decode.ts │ │ │ ├── encode.ts │ │ │ ├── getVersion.ts │ │ │ ├── index.ts │ │ │ ├── prefix.ts │ │ │ ├── proto │ │ │ │ ├── 1.0.0 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── project.d.ts │ │ │ │ │ ├── project.js │ │ │ │ │ ├── project.proto │ │ │ │ │ └── updateToNext.ts │ │ │ │ ├── 1.0.1 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── project.d.ts │ │ │ │ │ ├── project.js │ │ │ │ │ ├── project.proto │ │ │ │ │ └── updateToNext.ts │ │ │ │ ├── 1.0.2 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── project.d.ts │ │ │ │ │ ├── project.js │ │ │ │ │ ├── project.proto │ │ │ │ │ └── updateToNext.ts │ │ │ │ ├── 1.1.0 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── project.d.ts │ │ │ │ │ ├── project.js │ │ │ │ │ ├── project.proto │ │ │ │ │ └── updateToNext.ts │ │ │ │ ├── 1.1.1 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── project.d.ts │ │ │ │ │ ├── project.js │ │ │ │ │ ├── project.proto │ │ │ │ │ └── updateToNext.ts │ │ │ │ ├── encodeProject.ts │ │ │ │ ├── index.ts │ │ │ │ ├── project.d.ts │ │ │ │ ├── project.js │ │ │ │ ├── project.proto │ │ │ │ └── toOriginBuffer.ts │ │ │ └── updateOldProject.ts │ │ │ └── type.ts │ ├── export │ │ ├── exportFile.ts │ │ ├── index.ts │ │ ├── toBmfInfo.ts │ │ ├── type.ts │ │ └── types │ │ │ ├── text.ts │ │ │ ├── type.ts.template │ │ │ └── xml.ts │ └── prefix.ts ├── index.tsx ├── react-app-env.d.ts ├── service-worker.ts ├── serviceWorkerRegistration.ts ├── setupTests.ts ├── store │ ├── base │ │ ├── fill.ts │ │ ├── font.ts │ │ ├── glyphBase.ts │ │ ├── glyphFont.ts │ │ ├── glyphImage.ts │ │ ├── gradient.ts │ │ ├── index.ts │ │ ├── layout.ts │ │ ├── metric.ts │ │ ├── patternTexture.ts │ │ ├── shadow.ts │ │ ├── stroke.ts │ │ ├── style.ts │ │ └── ui.ts │ ├── hooks.ts │ ├── index.ts │ ├── project.ts │ ├── ui.ts │ └── workspace.ts ├── utils │ ├── base64ToArrayBuffer.ts │ ├── ctxDoPath.ts │ ├── deepMapToObject.ts │ ├── drawPackCanvas.ts │ ├── fontStyleStringify.ts │ ├── getBaselinesFromCssText.ts │ ├── getBaselinesFromOpentypeFont.ts │ ├── getCanvasStyle.ts │ ├── getFontGlyphs.ts │ ├── getLetterSizeFromCssText.ts │ ├── getPointOnCircle.ts │ ├── getTrimImageInfo.ts │ ├── getVersionNumber.ts │ ├── is.ts │ ├── pathDoSharp.ts │ ├── readFile.ts │ ├── replaceVariables.ts │ ├── trimImageData.ts │ ├── updateFontFace.ts │ └── use.ts └── workers │ ├── AutoPacker.worker.ts │ └── RectanglePacker.worker.ts ├── tsconfig.json ├── types ├── fonteditor-core │ └── index.d.ts ├── global.d.ts ├── mobx-utils │ └── index.d.ts ├── opentype.js │ └── index.d.ts ├── requestidlecallback │ └── index.d.ts ├── theme │ └── index.d.ts └── workers │ └── index.d.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=./ 2 | REACT_APP_SENTRY_DSN=https://007c463bad354a5baf9a11d8e9d7c8a6@o501223.ingest.sentry.io/5981296 -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | EXTEND_ESLINT = true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /node_modules1 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | package-lock.json 27 | 28 | .eslintcache 29 | 30 | /test 31 | .sentryclirc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 3 | "printWidth": 80, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "semi": false, 7 | "singleQuote": true, 8 | "jsxSingleQuote": true, 9 | "endOfLine": "lf", 10 | 11 | "arrowParens": "always", 12 | "quoteProps": "as-needed", 13 | "bracketSpacing": true, 14 | "jsxBracketSameLine": false, 15 | 16 | "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], 17 | "importOrderSeparation": true, 18 | "importOrderSortSpecifiers": true 19 | } 20 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | network-timeout 1200000 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Leo 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

SnowBamboo Bitmap Font Generator Online

5 | 6 | [简体中文](README_ZH.md) 7 | 8 | [https://snowb.org/](https://snowb.org/) 9 | 10 | Recently, [Google Chrome officially says goodbye to Flash](https://www.blog.google/products/chrome/saying-goodbye-flash-chrome/), [Adobe will not support Flash Player any longer as well](https://www.adobe.com/products/flashplayer/end-of-life.html). That also means the online tool Littera which we frequently applied before is no longer available in Chrome. SnowBamboo applied Canvas API and follows modern browsers' specification, making it simple to edit Bitmap Font online. It is compatible with Littera files (.ltr) and can be easily converted to SnowBamboo files (.sbf). 11 | 12 | [![SnowBamboo Bitmap Font Generator Preview](https://github.com/SilenceLeo/snowb-bmf/assets/4632034/182efea8-6254-4bb7-80a1-1d4c3be1e928)](https://snowb.org/) 13 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

SnowBamboo Bitmap Font 在线生成工具

5 | 6 | [English](README.md) 7 | 8 | [https://snowb.org/](https://snowb.org/) 9 | 10 | 现今,[Google Chrome 正式与 Flash 说再见](https://www.blog.google/products/chrome/saying-goodbye-flash-chrome/),[Adobe 也不再对 Flash Player 进行支持](https://www.adobe.com/products/flashplayer/end-of-life.html),这意味着我们经常使用的在线工具 Littera 将无法在 Chrome 中使用。 SnowBamboo 使用 CanvasAPI,遵循现代浏览器规范,可以很方便的进行在线制作 BMF。工具兼容 Littera 文件(.ltr),可以方便的转换为 SnowBamboo 文件(.sbf)。 11 | 12 | [![SnowBamboo Bitmap Font 生成工具预览](https://github.com/SilenceLeo/snowb-bmf/assets/4632034/182efea8-6254-4bb7-80a1-1d4c3be1e928)](https://snowb.org/) 13 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | snowb.org 2 | www.snowb.org 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilenceLeo/snowb-bmf/4cddc56f1da7dadb0d9a8df924d727949578c7f6/public/favicon.ico -------------------------------------------------------------------------------- /public/google46c5aeb0e05a4a13.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google46c5aeb0e05a4a13.html -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilenceLeo/snowb-bmf/4cddc56f1da7dadb0d9a8df924d727949578c7f6/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilenceLeo/snowb-bmf/4cddc56f1da7dadb0d9a8df924d727949578c7f6/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "SnowB Bitmap Font", 3 | "name": "Snow Bamboo BitmapFont", 4 | "description": "Snow Bamboo BitmapFont Editor Online.", 5 | "icons": [ 6 | { 7 | "src": "favicon.ico", 8 | "sizes": "128x128 64x64 32x32 24x24 16x16", 9 | "type": "image/x-icon" 10 | }, 11 | { 12 | "src": "logo192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "logo512.png", 18 | "type": "image/png", 19 | "sizes": "512x512" 20 | } 21 | ], 22 | "start_url": ".", 23 | "display": "fullscreen", 24 | "theme_color": "#1e1e1e", 25 | "background_color": "#1e1e1e" 26 | } 27 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path') 3 | const corssEnv = require('cross-env') 4 | const getGitRelease = require('./getGitRelease') 5 | 6 | const bin = path.join(process.cwd(), 'node_modules', '.bin') 7 | const reactScripts = path.join(bin, 'react-scripts') 8 | 9 | async function setSentryRelease() { 10 | const release = await getGitRelease() 11 | corssEnv([`REACT_APP_SENTRY_RELEASE=${release}`, reactScripts, `build`]) 12 | } 13 | 14 | setSentryRelease() 15 | -------------------------------------------------------------------------------- /scripts/createProtucol.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const { exec } = require('child_process') 5 | 6 | const pbjs = require('protobufjs-cli/pbjs') 7 | const pbts = require('protobufjs-cli/pbts') 8 | 9 | const inputFile = path.join( 10 | process.cwd(), 11 | 'src/file/conversion/types/sbf/proto/project.proto', 12 | ) 13 | const outputDir = path.dirname(inputFile) 14 | 15 | pbjs.main( 16 | [ 17 | '--target', 18 | 'static-module', 19 | 'src/file/conversion/types/sbf/proto/project.proto', 20 | '-w', 21 | 'es6', 22 | '--es6', 23 | ], 24 | function (err, output) { 25 | if (err) throw err 26 | 27 | const js = path.join(outputDir, 'project.js') 28 | const dts = path.join(outputDir, 'project.d.ts') 29 | fs.writeFile( 30 | js, 31 | '/* eslint-disable */' + output.replace(/\/\*[\s\S\w\W].*?\*\//, ''), 32 | () => { 33 | pbts.main([js], (err, outputDts) => { 34 | if (err) throw err 35 | fs.writeFile(dts, '/* eslint-disable */\n' + outputDts, (err) => { 36 | if (err) throw err 37 | exec( 38 | `${path.join( 39 | process.cwd(), 40 | 'node_modules/.bin/prettier', 41 | )} --write ${outputDir}*.{ts,js}`, 42 | ) 43 | }) 44 | }) 45 | }, 46 | ) 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /scripts/getGitRelease.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec 2 | 3 | function getGitRelease() { 4 | return new Promise((resolve, reject) => { 5 | exec('git rev-parse --short HEAD', function (err, stdout, stderr) { 6 | if (err != null || typeof stderr != 'string') { 7 | reject(err || stderr) 8 | } 9 | resolve(stdout.trim()) 10 | }) 11 | }) 12 | } 13 | 14 | module.exports = getGitRelease 15 | -------------------------------------------------------------------------------- /scripts/sentry.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const SentryCli = require('@sentry/cli') 3 | const getGitRelease = require('./getGitRelease') 4 | 5 | async function createReleaseAndUpload() { 6 | const release = await getGitRelease() 7 | if (!release) { 8 | console.warn('REACT_APP_SENTRY_RELEASE is not set') 9 | return 10 | } 11 | 12 | const cli = new SentryCli() 13 | 14 | try { 15 | console.log('Creating sentry release ' + release) 16 | await cli.releases.new(release) 17 | 18 | console.log('Uploading source maps') 19 | await cli.releases.uploadSourceMaps(release, { 20 | include: ['build/static/js'], 21 | urlPrefix: '~/static/js', 22 | rewrite: false, 23 | }) 24 | 25 | console.log('Finalizing release') 26 | await cli.releases.finalize(release) 27 | } catch (e) { 28 | console.error('Source maps uploading failed:', e) 29 | } 30 | } 31 | 32 | createReleaseAndUpload() 33 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import App from './app/App' 4 | 5 | test('renders learn react link', () => { 6 | render() 7 | const linkElement = screen.getByText(/learn react/i) 8 | expect(linkElement).toBeInTheDocument() 9 | }) 10 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import CssBaseline from '@mui/material/CssBaseline' 2 | import { ThemeProvider } from '@mui/material/styles' 3 | import { StyledEngineProvider } from '@mui/material/styles' 4 | import { SnackbarProvider } from 'notistack' 5 | import createStore, { StoreContext } from 'src/store' 6 | 7 | import Wrap from './layout/Wrap' 8 | import theme from './theme' 9 | 10 | function App(): JSX.Element { 11 | return ( 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default App 31 | -------------------------------------------------------------------------------- /src/app/components/AnglePicker/AnglePicker.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 36px; 3 | height: 36px; 4 | position: relative; 5 | cursor: crosshair; 6 | overflow: hidden; 7 | border-radius: 100%; 8 | background: #fff; 9 | } 10 | .point { 11 | width: 6px; 12 | height: 6px; 13 | border-radius: 100%; 14 | position: relative; 15 | left: 50%; 16 | top: 50%; 17 | margin-top: -2px; 18 | background: #000; 19 | pointer-events: none; 20 | transform-origin: 0 50%; 21 | transform: rotate(0deg) translate(10px, 0); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/AnglePicker/AnglePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useRef, 4 | useEffect, 5 | FunctionComponent, 6 | useCallback, 7 | } from 'react' 8 | 9 | import styles from './AnglePicker.module.css' 10 | 11 | export interface AnglePickerProps { 12 | width?: number 13 | angle: number 14 | onChange(angle: number): void 15 | } 16 | 17 | const AnglePicker: FunctionComponent = ( 18 | props: AnglePickerProps, 19 | ) => { 20 | const { onChange } = props 21 | const rootRef = useRef(null) 22 | const [isDragging, setIsDragging] = useState(false) 23 | 24 | const handleMouseMove = useCallback( 25 | (e: React.MouseEvent | MouseEvent) => { 26 | if (!rootRef.current) return 27 | 28 | const { clientX, clientY } = e 29 | const bounds = rootRef.current.getBoundingClientRect() 30 | const radians = Math.atan2( 31 | clientY - (bounds.y + bounds.height / 2), 32 | clientX - (bounds.x + bounds.width / 2), 33 | ) 34 | onChange(Math.round(radians * (180 / Math.PI))) 35 | }, 36 | [onChange], 37 | ) 38 | 39 | const handleMouseUp = useCallback((e: MouseEvent) => { 40 | e.stopPropagation() 41 | e.preventDefault() 42 | setIsDragging(false) 43 | }, []) 44 | 45 | const handleMouseDown = (e: React.MouseEvent) => { 46 | if (!rootRef.current) return 47 | setIsDragging(true) 48 | handleMouseMove(e) 49 | } 50 | 51 | useEffect(() => { 52 | if (isDragging) { 53 | window.addEventListener('mousemove', handleMouseMove) 54 | window.addEventListener('mouseup', handleMouseUp) 55 | } else { 56 | window.removeEventListener('mousemove', handleMouseMove) 57 | window.removeEventListener('mouseup', handleMouseUp) 58 | } 59 | 60 | return () => { 61 | window.removeEventListener('mousemove', handleMouseMove) 62 | window.removeEventListener('mouseup', handleMouseUp) 63 | } 64 | }, [handleMouseMove, handleMouseUp, isDragging]) 65 | 66 | return ( 67 |
77 |
85 |
86 | ) 87 | } 88 | 89 | export default AnglePicker 90 | -------------------------------------------------------------------------------- /src/app/components/AnglePicker/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './AnglePicker' 2 | export * from './AnglePicker' 3 | -------------------------------------------------------------------------------- /src/app/components/ColorInput/ColorInput.module.css: -------------------------------------------------------------------------------- 1 | .swatch { 2 | display: inline-block; 3 | cursor: pointer; 4 | } 5 | .color { 6 | width: 46px; 7 | height: 24px; 8 | border: 5px solid; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/components/ColorInput/ColorInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useRef, useState } from 'react' 2 | import ClickAwayListener from '@mui/material/ClickAwayListener' 3 | import { useTheme } from '@mui/material/styles' 4 | 5 | import WrappedSketchPicker from '../WrappedSketchPicker' 6 | 7 | import styles from './ColorInput.module.css' 8 | 9 | export interface ColorInputProps { 10 | color?: string 11 | onChange?: (color: string) => void 12 | } 13 | 14 | const ColorInput: FunctionComponent = ( 15 | props: ColorInputProps, 16 | ) => { 17 | const { color, onChange } = props 18 | const { palette, bgPixel } = useTheme() 19 | const anchorEl = useRef(null) 20 | const [open, setOpen] = useState(false) 21 | 22 | return ( 23 | setOpen(false)} 26 | > 27 |
35 |
setOpen(!open)} 43 | /> 44 | 50 |
51 | 52 | ) 53 | } 54 | 55 | export default ColorInput 56 | -------------------------------------------------------------------------------- /src/app/components/ColorInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ColorInput' 2 | -------------------------------------------------------------------------------- /src/app/components/GradientPicker/ColorStop.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 12px; 3 | height: 12px; 4 | border-style: solid; 5 | border-width: 0 1px 1px; 6 | position: absolute; 7 | cursor: pointer; 8 | margin-left: -6px; 9 | z-index: 1; 10 | &:before, 11 | &:after { 12 | position: absolute; 13 | content: ''; 14 | width: 0; 15 | height: 0; 16 | border-style: solid; 17 | left: 0; 18 | } 19 | &:before { 20 | top: -6px; 21 | left: -1px; 22 | border-width: 0 6px 6px 6px; 23 | } 24 | &:after { 25 | top: -5px; 26 | border-width: 0 5px 5px 5px; 27 | } 28 | } 29 | .color { 30 | width: 100%; 31 | height: 100%; 32 | pointer-events: none; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/components/GradientPicker/ColorStop.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import { useTheme } from '@mui/material/styles' 3 | import clsx from 'clsx' 4 | import React, { FunctionComponent } from 'react' 5 | 6 | import styles from './ColorStop.module.scss' 7 | 8 | interface ColorStopPorps { 9 | className?: string 10 | left?: string | number 11 | top?: string | number 12 | color: string 13 | isActive: boolean 14 | onMouseDown: (e: React.MouseEvent) => void 15 | } 16 | 17 | const ColorStop: FunctionComponent = ( 18 | props: ColorStopPorps, 19 | ) => { 20 | const { left, top, color, isActive, className, ...divProps } = props 21 | const { bgPixel, palette } = useTheme() 22 | 23 | return ( 24 | 46 | 52 | 53 | ) 54 | } 55 | 56 | export default ColorStop 57 | -------------------------------------------------------------------------------- /src/app/components/GradientPicker/ColorStopsHolder.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | height: 17px; 4 | position: relative; 5 | cursor: crosshair; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/GradientPicker/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './GradientBuilder' 2 | export * from './GradientBuilder' 3 | export * from './ColorStopsHolder' 4 | -------------------------------------------------------------------------------- /src/app/components/GridInput/GridInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactNode, 3 | FunctionComponent, 4 | PropsWithChildren, 5 | ElementType, 6 | CSSProperties, 7 | } from 'react' 8 | import Typography from '@mui/material/Typography' 9 | import Grid from '@mui/material/Grid' 10 | 11 | interface GridInputProps { 12 | before?: ReactNode | string 13 | after?: ReactNode 14 | component?: ElementType 15 | childrenWidth?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 16 | style?: CSSProperties 17 | children?: ReactNode 18 | } 19 | 20 | const GridInput: FunctionComponent = ( 21 | props: PropsWithChildren, 22 | ): JSX.Element => { 23 | const { before, children, component, after, childrenWidth, ...other } = props 24 | return ( 25 | 34 | 35 | {typeof before === 'object' ? ( 36 | before 37 | ) : ( 38 | 39 | {before} 40 | 41 | )} 42 | 43 | 44 | {children} 45 | 46 | 47 | {typeof after === 'object' ? ( 48 | after 49 | ) : ( 50 | 51 | {after} 52 | 53 | )} 54 | 55 | 56 | ) 57 | } 58 | 59 | export default GridInput 60 | -------------------------------------------------------------------------------- /src/app/components/GridInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './GridInput' 2 | -------------------------------------------------------------------------------- /src/app/components/Palette/Palette.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | border: 1px solid #ccc; 3 | } 4 | .svg { 5 | width: 100%; 6 | height: 100%; 7 | vertical-align: top; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/components/Palette/Palette.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from 'react' 2 | import { useTheme } from '@mui/material/styles' 3 | import Box from '@mui/material/Box' 4 | 5 | import styles from './Palette.module.css' 6 | export interface PaletteItem { 7 | id: number | string 8 | offset: number 9 | color: string 10 | } 11 | 12 | interface PaletteProps { 13 | width?: number | string 14 | height?: number | string 15 | palette: PaletteItem[] 16 | } 17 | 18 | const Palette: FunctionComponent = ( 19 | props: PaletteProps, 20 | ): JSX.Element => { 21 | const { palette, width, height } = props 22 | const { bgPixel } = useTheme() 23 | const [id] = useState(`palette_${Math.random().toString().substr(2, 9)}`) 24 | const sortedPalette = [...palette].sort( 25 | ({ offset: offset1 }, { offset: offset2 }) => offset1 - offset2, 26 | ) 27 | 28 | return ( 29 | 33 | 34 | 35 | 36 | {sortedPalette.map((item) => ( 37 | 42 | ))} 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | export default Palette 52 | -------------------------------------------------------------------------------- /src/app/components/Palette/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Palette' 2 | -------------------------------------------------------------------------------- /src/app/components/WrappedSketchPicker/WrappedSketchPicker.tsx: -------------------------------------------------------------------------------- 1 | import Popper, { PopperPlacementType } from '@mui/material/Popper' 2 | import { useTheme } from '@mui/material/styles' 3 | import type { Theme } from '@mui/material/styles' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { FunctionComponent } from 'react' 6 | import { ColorResult, SketchPicker } from 'react-color' 7 | 8 | export interface ChildrenProps { 9 | open: boolean 10 | color: string 11 | placement: PopperPlacementType 12 | anchorEl: HTMLDivElement | null 13 | onChange(color: string): void 14 | } 15 | 16 | const usePickerStyle = (theme: Theme) => { 17 | const { palette } = theme 18 | 19 | if (palette.mode === 'light') return {} 20 | 21 | return { 22 | default: { 23 | picker: { 24 | background: palette.background.titleBar, 25 | shadow: theme.shadows[24], 26 | }, 27 | alpha: { 28 | background: '#fff', 29 | }, 30 | color: { 31 | background: '#fff', 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | const WrappedSketchPicker: FunctionComponent> = ( 38 | props: Partial, 39 | ) => { 40 | const { open, anchorEl, color, onChange, placement } = props 41 | const theme = useTheme() 42 | const pickerStyle = usePickerStyle(theme) 43 | const { palette } = theme 44 | 45 | return ( 46 | 64 | {/* @ts-ignore */} 65 | { 69 | if (onChange) 70 | onChange( 71 | `rgba(${rgb.r},${rgb.g},${rgb.b},${ 72 | typeof rgb.a === 'undefined' ? 1 : rgb.a 73 | })`, 74 | ) 75 | }} 76 | /> 77 | 78 | ) 79 | } 80 | 81 | export default observer(WrappedSketchPicker) 82 | -------------------------------------------------------------------------------- /src/app/components/WrappedSketchPicker/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './WrappedSketchPicker' 2 | export * from './WrappedSketchPicker' 3 | -------------------------------------------------------------------------------- /src/app/hooks/useWheel.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback, RefObject, DependencyList } from 'react' 2 | 3 | interface DeltaInfo { 4 | deltaScale: number 5 | deltaX: number 6 | deltaY: number 7 | } 8 | 9 | interface WheelCallback { 10 | (deltaInfo: DeltaInfo): void 11 | } 12 | 13 | function useWheel( 14 | ref: RefObject, 15 | onWheel: WheelCallback, 16 | deps: DependencyList = [], 17 | ): void { 18 | const callback = useCallback(onWheel, [onWheel, deps]) 19 | const handleWheel = useCallback( 20 | (e: WheelEvent) => { 21 | e.preventDefault() 22 | e.stopPropagation() 23 | const { ctrlKey, altKey, deltaX, deltaY } = e 24 | if (ctrlKey) { 25 | let d = -0.01 26 | if (Math.abs(deltaY) > 50) d *= 0.1 27 | callback({ deltaScale: deltaY * d, deltaX: 0, deltaY: 0 }) 28 | } else { 29 | let x = -deltaX 30 | let y = -deltaY 31 | if (deltaX === 0 && altKey && Math.abs(deltaY) > 50) { 32 | x = -deltaY 33 | y = 0 34 | } 35 | callback({ 36 | deltaX: x, 37 | deltaY: y, 38 | deltaScale: 0, 39 | }) 40 | } 41 | }, 42 | [callback], 43 | ) 44 | 45 | useEffect(() => { 46 | if (!ref.current) return undefined 47 | 48 | const dom = ref.current 49 | 50 | dom.addEventListener('wheel', handleWheel, { 51 | passive: false, 52 | }) 53 | 54 | return () => dom.removeEventListener('wheel', handleWheel) 55 | }, [ref, handleWheel]) 56 | } 57 | export default useWheel 58 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/LeftBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Box from '@mui/material/Box' 3 | import Divider from '@mui/material/Divider' 4 | import Typography from '@mui/material/Typography' 5 | 6 | import Font from './modules/Font' 7 | import Glyphs from './modules/Glyphs' 8 | import PackConfig from './modules/PackConfig' 9 | import GlobalMetric from './modules/GlobalMetric' 10 | 11 | const LeftBar: FunctionComponent = () => { 12 | return ( 13 | 22 | 23 | Font Config 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | export default LeftBar 39 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LeftBar' 2 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/Font/Font.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Typography from '@mui/material/Typography' 3 | import Box from '@mui/material/Box' 4 | 5 | import FontFamily from './FontFamily' 6 | import FontSize from './FontSize' 7 | import Sharp from './Sharp' 8 | import LineHeight from './LineHeight' 9 | 10 | const Font: FunctionComponent = () => { 11 | return ( 12 | <> 13 | 14 | Font 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default Font 33 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/Font/FontSize.tsx: -------------------------------------------------------------------------------- 1 | import Input from '@mui/material/Input' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput/GridInput' 5 | import { useFont } from 'src/store/hooks' 6 | 7 | const FontSize: FunctionComponent = () => { 8 | const { size, setSize } = useFont() 9 | 10 | const handleInput = ( 11 | event: React.ChangeEvent, 12 | ): void => { 13 | setSize(Number(event.target.value)) 14 | } 15 | 16 | return ( 17 | 18 | 25 | 26 | ) 27 | } 28 | 29 | export default observer(FontSize) 30 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/Font/LineHeight.tsx: -------------------------------------------------------------------------------- 1 | import Input from '@mui/material/Input' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput/GridInput' 5 | import { useFont } from 'src/store/hooks' 6 | 7 | const LineHeight: FunctionComponent = () => { 8 | const { size, lineHeight, setLineHeight } = useFont() 9 | 10 | const handleInput = ( 11 | event: React.ChangeEvent, 12 | ): void => { 13 | setLineHeight(Number(event.target.value) / size) 14 | } 15 | 16 | return ( 17 | 18 | 25 | 26 | ) 27 | } 28 | 29 | export default observer(LineHeight) 30 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/Font/Sharp.tsx: -------------------------------------------------------------------------------- 1 | import Slider from '@mui/material/Slider' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput/GridInput' 5 | import { useFont } from 'src/store/hooks' 6 | 7 | const Sharp: FunctionComponent = () => { 8 | const { sharp, setSharp, mainFont } = useFont() 9 | 10 | const handleInput = (event: Event, value: number | number[]): void => { 11 | setSharp(value as unknown as number) 12 | } 13 | 14 | return ( 15 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default observer(Sharp) 26 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/Font/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Font' 2 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/GlobalMetric/GlobalMetric.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Typography from '@mui/material/Typography' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { FunctionComponent } from 'react' 5 | import FormAdjustMetric from 'src/app/layout/common/FormAdjustMetric' 6 | import { useProject } from 'src/store/hooks' 7 | 8 | const GlobalMetric: FunctionComponent = () => { 9 | const { globalAdjustMetric } = useProject() 10 | const { xAdvance, xOffset, yOffset, setXAdvance, setXOffset, setYOffset } = 11 | globalAdjustMetric 12 | 13 | return ( 14 | <> 15 | 16 | Global Metric Adjustments 17 | 18 | 26 | 27 | ) 28 | } 29 | 30 | export default observer(GlobalMetric) 31 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/GlobalMetric/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './GlobalMetric' 2 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/Glyphs/Glyphs.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import TextField from '@mui/material/TextField' 3 | import Typography from '@mui/material/Typography' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { 6 | FunctionComponent, 7 | useCallback, 8 | useEffect, 9 | useState, 10 | } from 'react' 11 | import { useProject } from 'src/store/hooks' 12 | 13 | const Glyphs: FunctionComponent = () => { 14 | const { text, setText } = useProject() 15 | const [isIME, setIsIME] = useState(false) 16 | const [inputText, setInputText] = useState(text) 17 | 18 | const handleInput = (event: React.ChangeEvent): void => { 19 | const { value } = event.target 20 | const str = Array.from(new Set(Array.from(value))).join('') 21 | if (isIME) { 22 | setInputText(value) 23 | } else { 24 | setInputText(str) 25 | if (str !== text) setText(str) 26 | } 27 | } 28 | 29 | const handleCompositionStart = useCallback((): void => { 30 | setInputText(text) 31 | setIsIME(true) 32 | }, [text]) 33 | 34 | const handleCompositionEnd = (): void => { 35 | setIsIME(false) 36 | const str = Array.from(new Set(Array.from(inputText))).join('') 37 | setInputText(str) 38 | if (str !== text) setText(str) 39 | } 40 | 41 | useEffect(() => { 42 | setInputText(text) 43 | }, [text]) 44 | 45 | return ( 46 | <> 47 | 48 | Glyphs 49 | 50 | 51 | 64 | 65 | 66 | ) 67 | } 68 | 69 | export default observer(Glyphs) 70 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/Glyphs/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Glyphs' 2 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/AutoPack.tsx: -------------------------------------------------------------------------------- 1 | import Checkbox from '@mui/material/Checkbox' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput' 5 | import { useLayout } from 'src/store/hooks' 6 | 7 | const AutoPack: FunctionComponent = () => { 8 | const { auto, setAuto } = useLayout() 9 | 10 | return ( 11 | 12 | setAuto(e.target.checked)} 17 | /> 18 | 19 | ) 20 | } 21 | 22 | export default observer(AutoPack) 23 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/FixedSize.tsx: -------------------------------------------------------------------------------- 1 | import Checkbox from '@mui/material/Checkbox' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput' 5 | import { useLayout } from 'src/store/hooks' 6 | 7 | const FixedSize: FunctionComponent = () => { 8 | const { auto, fixedSize, setFixedSize } = useLayout() 9 | 10 | return ( 11 | 12 | setFixedSize(e.target.checked)} 17 | disabled={auto} 18 | /> 19 | 20 | ) 21 | } 22 | 23 | export default observer(FixedSize) 24 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/PackConfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | 3 | import Typography from '@mui/material/Typography' 4 | import Box from '@mui/material/Box' 5 | 6 | import Padding from './Padding' 7 | import Spacing from './Spacing' 8 | import AutoPack from './AutoPack' 9 | import FixedSize from './FixedSize' 10 | import PackWidth from './PackWidth' 11 | import PackHeight from './PackHeight' 12 | 13 | const PackConfig: FunctionComponent = () => { 14 | return ( 15 | <> 16 | 17 | Layout 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | export default PackConfig 42 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/PackHeight.tsx: -------------------------------------------------------------------------------- 1 | import Input from '@mui/material/Input' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput' 5 | import { useLayout } from 'src/store/hooks' 6 | 7 | const PackHeight: FunctionComponent = () => { 8 | const { height, auto, fixedSize, setHeight } = useLayout() 9 | 10 | const handleInput = (event: React.ChangeEvent): void => { 11 | setHeight(Number(event.target.value)) 12 | } 13 | 14 | return ( 15 | 16 | 24 | 25 | ) 26 | } 27 | 28 | export default observer(PackHeight) 29 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/PackWidth.tsx: -------------------------------------------------------------------------------- 1 | import Input from '@mui/material/Input' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput' 5 | import { useLayout } from 'src/store/hooks' 6 | 7 | const PackWidth: FunctionComponent = () => { 8 | const { width, auto, fixedSize, setWidth } = useLayout() 9 | 10 | const handleInput = (event: React.ChangeEvent): void => { 11 | setWidth(Number(event.target.value)) 12 | } 13 | 14 | return ( 15 | 16 | 24 | 25 | ) 26 | } 27 | 28 | export default observer(PackWidth) 29 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/Padding.tsx: -------------------------------------------------------------------------------- 1 | import Input from '@mui/material/Input' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput' 5 | import { useLayout } from 'src/store/hooks' 6 | 7 | const Padding: FunctionComponent = () => { 8 | const { padding, setPadding } = useLayout() 9 | 10 | const handleInput = (event: React.ChangeEvent): void => { 11 | setPadding(Number(event.target.value)) 12 | } 13 | 14 | return ( 15 | 16 | 23 | 24 | ) 25 | } 26 | 27 | export default observer(Padding) 28 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/Spacing.tsx: -------------------------------------------------------------------------------- 1 | import Input from '@mui/material/Input' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import GridInput from 'src/app/components/GridInput' 5 | import { useLayout } from 'src/store/hooks' 6 | 7 | const Spacing: FunctionComponent = () => { 8 | const { spacing, setSpacing } = useLayout() 9 | 10 | return ( 11 | 12 | setSpacing(Number(e.target.value))} 18 | /> 19 | 20 | ) 21 | } 22 | 23 | export default observer(Spacing) 24 | -------------------------------------------------------------------------------- /src/app/layout/LeftBar/modules/PackConfig/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PackConfig' 2 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/RightBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Box from '@mui/material/Box' 3 | import Divider from '@mui/material/Divider' 4 | import Typography from '@mui/material/Typography' 5 | 6 | import Fill from './modules/Fill' 7 | import Stroke from './modules/Stroke' 8 | import Shadow from './modules/Shadow' 9 | import BackgroundColor from './modules/BackgroundColor' 10 | 11 | const RightBar: FunctionComponent = () => { 12 | return ( 13 | 22 | 23 | Style Config 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | export default RightBar 39 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './RightBar' 2 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/modules/BackgroundColor/BackgroundColor.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Typography from '@mui/material/Typography' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { FunctionComponent } from 'react' 5 | import { useStyle } from 'src/store/hooks' 6 | 7 | import FormColor from '../../../common/FormColor' 8 | 9 | const BackgroundColor: FunctionComponent = () => { 10 | const { bgColor, setBgColor } = useStyle() 11 | 12 | return ( 13 | <> 14 | 23 | Background Color 24 | 25 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export default observer(BackgroundColor) 38 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/modules/BackgroundColor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './BackgroundColor' 2 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/modules/Fill/Fill.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Typography from '@mui/material/Typography' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { FunctionComponent } from 'react' 5 | import { useFill } from 'src/store/hooks' 6 | 7 | import FormFill from '../../../common/FormFill' 8 | 9 | const Fill: FunctionComponent = () => { 10 | const fill = useFill() 11 | return ( 12 | <> 13 | 19 | Fill 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | export default observer(Fill) 27 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/modules/Fill/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Fill' 2 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/modules/Shadow/Shadow.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Input from '@mui/material/Input' 3 | import Switch from '@mui/material/Switch' 4 | import Typography from '@mui/material/Typography' 5 | import { observer } from 'mobx-react-lite' 6 | import React, { FunctionComponent } from 'react' 7 | import GridInput from 'src/app/components/GridInput' 8 | import { useStyle } from 'src/store/hooks' 9 | 10 | import FormColor from '../../../common/FormColor' 11 | 12 | const Shadow: FunctionComponent = () => { 13 | const { shadow, useShadow, setUseShadow } = useStyle() 14 | const { setOffsetX, setOffsetY, setBlur, setColor } = shadow 15 | 16 | return ( 17 | <> 18 | 27 | 28 | Shadow 29 | 30 | Off 31 | setUseShadow(e.target.checked)} 35 | /> 36 | On 37 | 38 | 48 | 49 | 50 | setOffsetX(Number(e.target.value))} 56 | /> 57 | 58 | 59 | 60 | 61 | setOffsetY(Number(e.target.value))} 67 | /> 68 | 69 | 70 | 71 | 72 | setBlur(Number(e.target.value))} 79 | /> 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | export default observer(Shadow) 90 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/modules/Shadow/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Shadow' 2 | -------------------------------------------------------------------------------- /src/app/layout/RightBar/modules/Stroke/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Stroke' 2 | -------------------------------------------------------------------------------- /src/app/layout/TitleBar/ButtonNew.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button' 2 | import hotkeys from 'hotkeys-js' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { FunctionComponent, useCallback, useEffect } from 'react' 5 | import { useWorkspace } from 'src/store/hooks' 6 | 7 | interface ButtonNewProps { 8 | className?: string 9 | } 10 | 11 | const ButtonNew: FunctionComponent = ( 12 | props: ButtonNewProps, 13 | ) => { 14 | const { className } = props 15 | 16 | const worckSpace = useWorkspace() 17 | const { addProject } = worckSpace 18 | 19 | const handleNewProject = useCallback( 20 | (e: { preventDefault(): void }) => { 21 | e.preventDefault() 22 | addProject() 23 | return false 24 | }, 25 | [addProject], 26 | ) 27 | 28 | useEffect(() => { 29 | hotkeys.unbind('alt+n,control+n') 30 | hotkeys('alt+n,control+n', handleNewProject) 31 | return () => { 32 | hotkeys.unbind('alt+n,control+n') 33 | } 34 | }, [handleNewProject]) 35 | 36 | return ( 37 | 44 | ) 45 | } 46 | 47 | export default observer(ButtonNew) 48 | -------------------------------------------------------------------------------- /src/app/layout/TitleBar/ButtonOpen.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button' 2 | import * as Sentry from '@sentry/react' 3 | import { observer } from 'mobx-react-lite' 4 | import { useSnackbar } from 'notistack' 5 | import React, { FunctionComponent, useRef, useState } from 'react' 6 | import conversion from 'src/file/conversion' 7 | import { useWorkspace } from 'src/store/hooks' 8 | import readFile from 'src/utils/readFile' 9 | 10 | interface ButtonOpenProps { 11 | className?: string 12 | } 13 | 14 | const ButtonOpen: FunctionComponent = ( 15 | props: ButtonOpenProps, 16 | ) => { 17 | const { className } = props 18 | const { enqueueSnackbar } = useSnackbar() 19 | 20 | const worckSpace = useWorkspace() 21 | const labelRef = useRef(null) 22 | const [inputKey, changeInputKey] = useState(Date.now()) 23 | const { addProject } = worckSpace 24 | 25 | const handleLoad = (e: React.ChangeEvent): void => { 26 | if (!e.target?.files || !e.target.files[0]) return 27 | const file = e.target.files[0] 28 | const isText = /\.ltr$/.test(file.name) 29 | 30 | readFile(file, isText).then((buffer) => { 31 | try { 32 | const project = conversion(buffer) 33 | if (!project.name) project.name = file.name 34 | if (addProject(project)) { 35 | enqueueSnackbar( 36 | 'The project already exists and has been switched to the current tab.', 37 | { variant: 'success' }, 38 | ) 39 | } 40 | } catch (e) { 41 | console.log(e) 42 | Sentry.captureException(e) 43 | enqueueSnackbar((e as Error).toString(), { variant: 'error' }) 44 | } 45 | changeInputKey(Date.now()) 46 | }) 47 | } 48 | 49 | return ( 50 | 65 | ) 66 | } 67 | 68 | export default observer(ButtonOpen) 69 | -------------------------------------------------------------------------------- /src/app/layout/TitleBar/ButtonSave.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button' 2 | import * as Sentry from '@sentry/react' 3 | import { saveAs } from 'file-saver' 4 | import hotkeys from 'hotkeys-js' 5 | import { toJS } from 'mobx' 6 | import { observer } from 'mobx-react-lite' 7 | import { useSnackbar } from 'notistack' 8 | import React, { FunctionComponent, useCallback, useEffect } from 'react' 9 | import { encode } from 'src/file/conversion' 10 | import { useWorkspace } from 'src/store/hooks' 11 | 12 | interface ButtonSaveProps { 13 | className?: string 14 | } 15 | 16 | const ButtonSave: FunctionComponent = ( 17 | props: ButtonSaveProps, 18 | ) => { 19 | const { className } = props 20 | 21 | const { enqueueSnackbar } = useSnackbar() 22 | const worckSpace = useWorkspace() 23 | const { currentProject: project } = worckSpace 24 | 25 | const handleSaveProject = useCallback( 26 | (e: { preventDefault(): void }) => { 27 | e.preventDefault() 28 | try { 29 | const buffer = encode(toJS(project)) 30 | saveAs(new Blob([buffer]), `${project.name}.sbf`) 31 | } catch (e) { 32 | Sentry.captureException(e) 33 | enqueueSnackbar((e as Error).message) 34 | } 35 | }, 36 | [enqueueSnackbar, project], 37 | ) 38 | 39 | useEffect(() => { 40 | hotkeys.unbind('ctrl+s') 41 | hotkeys('ctrl+s', handleSaveProject) 42 | return () => { 43 | hotkeys.unbind('ctrl+s') 44 | } 45 | }, [handleSaveProject]) 46 | 47 | return ( 48 | 55 | ) 56 | } 57 | 58 | export default observer(ButtonSave) 59 | -------------------------------------------------------------------------------- /src/app/layout/TitleBar/TitleBar.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | } 6 | .appName { 7 | font-size: 1.25rem; 8 | font-weight: bolder; 9 | margin-right: 1rem; 10 | } 11 | .appNameSup { 12 | font-weight: lighter; 13 | font-size: 0.5em; 14 | margin-left: 0.5rem; 15 | } 16 | .btn { 17 | text-transform: none; 18 | color: #fff; 19 | } 20 | .btn:hover { 21 | background-color: #252525; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/layout/TitleBar/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Box from '@mui/material/Box' 3 | import { useTheme } from '@mui/material/styles' 4 | import Link from '@mui/material/Link' 5 | import IconButton from '@mui/material/IconButton' 6 | import Typography from '@mui/material/Typography' 7 | import GitHubIcon from '@mui/icons-material/GitHub' 8 | import ArrowForwardIcon from '@mui/icons-material/ArrowForward' 9 | 10 | import ButtonNew from './ButtonNew' 11 | import ButtonOpen from './ButtonOpen' 12 | import ButtonSave from './ButtonSave' 13 | import ButtonExport from './ButtonExport' 14 | 15 | import styles from './TitleBar.module.css' 16 | 17 | const TitleBar: FunctionComponent = () => { 18 | const { zIndex } = useTheme() 19 | 20 | return ( 21 | 29 | 30 | SnowB Bitmap Font 31 | {/* BETA */} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 47 | Give it a star to encourage the author! 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | export default TitleBar 57 | -------------------------------------------------------------------------------- /src/app/layout/TitleBar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './TitleBar' 2 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/WorkSpace.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | flex: 1; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | width: 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/WorkSpace.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react' 2 | import Box from '@mui/material/Box' 3 | 4 | import MainView from './modules/MainView' 5 | import ProjectTabs from './modules/ProjectTabs' 6 | import ControlerBar from './modules/ControlerBar' 7 | import ImageGlyphList from './modules/ImageGlyphList' 8 | 9 | import styles from './WorkSpace.module.css' 10 | 11 | const WorkSpace: FunctionComponent = () => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default WorkSpace 23 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './WorkSpace' 2 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ControlerBar/ControlerBar.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | .preview { 6 | display: flex; 7 | align-items: center; 8 | } 9 | .slider { 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ControlerBar/ControlerBar.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Button from '@mui/material/Button' 3 | import ClickAwayListener from '@mui/material/ClickAwayListener' 4 | import MenuItem from '@mui/material/MenuItem' 5 | import MenuList from '@mui/material/MenuList' 6 | import Paper from '@mui/material/Paper' 7 | import Popper from '@mui/material/Popper' 8 | import Slider from '@mui/material/Slider' 9 | import Switch from '@mui/material/Switch' 10 | import { observer } from 'mobx-react-lite' 11 | import React, { FunctionComponent, useRef, useState } from 'react' 12 | import { useProjectUi } from 'src/store/hooks' 13 | 14 | import styles from './ControlerBar.module.css' 15 | 16 | const ControlerBar: FunctionComponent = () => { 17 | const { 18 | scale, 19 | setTransform, 20 | previewScale, 21 | setPreviewTransform, 22 | showPreview, 23 | setShowPreview, 24 | } = useProjectUi() 25 | const [open, setOpen] = useState(false) 26 | const anchorRef = useRef(null) 27 | const [list] = useState([0.25, 0.5, 0.75, 1, 1.25, 1.5, 5, 10]) 28 | const handleToggle = () => { 29 | setOpen((prevOpen) => !prevOpen) 30 | } 31 | 32 | const handleClose = (event: MouseEvent | TouchEvent) => { 33 | if ( 34 | anchorRef.current && 35 | anchorRef.current.contains(event.target as HTMLElement) 36 | ) { 37 | return 38 | } 39 | 40 | setOpen(false) 41 | } 42 | 43 | const handleChange = (event: unknown, val: number | number[]) => { 44 | if (showPreview) { 45 | setPreviewTransform({ previewScale: val as number }) 46 | } else { 47 | setTransform({ scale: val as number }) 48 | } 49 | } 50 | 51 | const handleSelect = (val: number) => { 52 | handleChange(null, val) 53 | setOpen(false) 54 | } 55 | 56 | return ( 57 | 58 | 59 | Preview 60 | setShowPreview(e.target.checked)} 65 | /> 66 | 67 | 75 | 78 | 79 | 80 | 81 | 82 | {list.map((n) => ( 83 | handleSelect(n)}> 84 | {`${n * 100}%`} 85 | 86 | ))} 87 | 88 | 89 | 90 | 91 | 92 | ) 93 | } 94 | 95 | export default observer(ControlerBar) 96 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ControlerBar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ControlerBar' 2 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ImageGlyphList/ImageGlyph.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | position: relative; 6 | width: 80px; 7 | height: 80px; 8 | } 9 | .image { 10 | max-width: 100%; 11 | max-height: 100%; 12 | pointer-events: none; 13 | } 14 | .actions { 15 | width: 100%; 16 | height: 100%; 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | flex-direction: column; 21 | } 22 | .inputLabel { 23 | width: 100%; 24 | height: 100%; 25 | align-items: flex-end; 26 | background: linear-gradient( 27 | to bottom, 28 | rgba(0, 0, 0, 0), 29 | rgba(0, 0, 0, 0.3) 50%, 30 | rgba(0, 0, 0, 0.8) 31 | ); 32 | 33 | & input { 34 | text-align: center; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ImageGlyphList/ImageGlyph.tsx: -------------------------------------------------------------------------------- 1 | import DeleteIcon from '@mui/icons-material/Delete' 2 | import Checkbox from '@mui/material/Checkbox' 3 | import Grid from '@mui/material/Grid' 4 | import IconButton from '@mui/material/IconButton' 5 | import InputBase from '@mui/material/InputBase' 6 | import Paper from '@mui/material/Paper' 7 | import { observer } from 'mobx-react-lite' 8 | import React, { FunctionComponent, useState } from 'react' 9 | import { GlyphImage } from 'src/store' 10 | import { useProject } from 'src/store/hooks' 11 | 12 | import styles from './ImageGlyph.module.scss' 13 | 14 | interface ImageGlyphProps { 15 | glyph: GlyphImage 16 | selected?: boolean 17 | } 18 | 19 | const ImageGlyph: FunctionComponent = ( 20 | props: ImageGlyphProps, 21 | ) => { 22 | const { removeImage } = useProject() 23 | const [isIME, setIsIME] = useState(false) 24 | const { glyph } = props 25 | const [inputValue, setInputValue] = useState(glyph.letter) 26 | const { changeSelect, selected, setGlyph } = glyph 27 | 28 | const handleChangeGlyph = (e: React.ChangeEvent): void => { 29 | const { value } = e.target 30 | if (!isIME) { 31 | setGlyph(value) 32 | } else { 33 | setInputValue(value.slice(0, 1)) 34 | setGlyph(value.slice(0, 1)) 35 | } 36 | } 37 | 38 | const handleCompositionEnd = (): void => { 39 | setIsIME(false) 40 | setInputValue((iv) => iv.slice(0, 1)) 41 | setGlyph(inputValue.slice(0, 1)) 42 | } 43 | 44 | return ( 45 | 46 | {glyph.fileName} 47 | 48 | 53 | changeSelect(e.target.checked)} 58 | /> 59 | removeImage(glyph)} 63 | > 64 | 65 | 66 | 67 | 68 | e.target.select()} 72 | onInput={handleChangeGlyph} 73 | onCompositionEnd={handleCompositionEnd} 74 | onCompositionStart={() => setIsIME(true)} 75 | /> 76 | 77 | 78 | 79 | ) 80 | } 81 | 82 | export default observer(ImageGlyph) 83 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ImageGlyphList/ImageGlyphList.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | display: flex; 4 | flex-wrap: wrap; 5 | gap: 8px; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ImageGlyphList/ImageGlyphList.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import { useProject } from 'src/store/hooks' 5 | 6 | import ImageGlyph from './ImageGlyph' 7 | import styles from './ImageGlyphList.module.scss' 8 | 9 | const ImageGlyphList: FunctionComponent = () => { 10 | const { glyphImages } = useProject() 11 | 12 | return ( 13 | 14 | {glyphImages.map((glyph) => { 15 | return 16 | })} 17 | 18 | ) 19 | } 20 | 21 | export default observer(ImageGlyphList) 22 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ImageGlyphList/LayerBox.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | position: relative; 4 | } 5 | .fixed { 6 | position: fixed; 7 | left: 0; 8 | top: 0; 9 | z-index: 999999; 10 | width: 100%; 11 | height: 100%; 12 | & .panel { 13 | max-height: none; 14 | } 15 | } 16 | .panel { 17 | width: 100%; 18 | display: flex; 19 | flex-direction: column; 20 | max-height: 305px; 21 | } 22 | .continer { 23 | flex: 1; 24 | overflow: hidden; 25 | overflow-y: auto; 26 | } 27 | .listWrap { 28 | min-height: 224px; 29 | height: 100%; 30 | width: 100%; 31 | overflow: hidden; 32 | overflow-y: auto; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ImageGlyphList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LayerBox' 2 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/MainView/MainView.module.css: -------------------------------------------------------------------------------- 1 | @keyframes slideDown { 2 | from { 3 | opacity: 0; 4 | transform: translate(0, -100%); 5 | } 6 | to { 7 | opacity: 1; 8 | transform: translate(0, 0); 9 | } 10 | } 11 | .root { 12 | position: relative; 13 | display: flex; 14 | flex: 1; 15 | flex-direction: column; 16 | } 17 | .toast { 18 | position: absolute; 19 | left: 0; 20 | top: 0; 21 | width: 100%; 22 | z-index: 10; 23 | text-align: center; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | font-size: 12px; 28 | padding: 2px; 29 | animation-name: slideDown; 30 | animation-duration: 300ms; 31 | pointer-events: none; 32 | } 33 | .icon { 34 | margin-right: 5px; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/MainView/MainView.tsx: -------------------------------------------------------------------------------- 1 | import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' 2 | import Box from '@mui/material/Box' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { FunctionComponent } from 'react' 5 | import { useProjectUi } from 'src/store/hooks' 6 | 7 | import PackView from '../PackView' 8 | import Preview from '../Preview' 9 | import styles from './MainView.module.css' 10 | 11 | const MainView: FunctionComponent = () => { 12 | const { showPreview, packFailed } = useProjectUi() 13 | 14 | return ( 15 | 16 | {packFailed ? ( 17 | 18 | 19 | Packaging failed, try to increase the size of the package please. 20 | 21 | ) : null} 22 | {showPreview ? : } 23 | 24 | ) 25 | } 26 | 27 | export default observer(MainView) 28 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/MainView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './MainView' 2 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/PackView/PackCanvas.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | overflow: hidden; 6 | flex: 1; 7 | } 8 | .canvas { 9 | transform-origin: 50% 50%; 10 | position: absolute; 11 | left: 50%; 12 | top: 50%; 13 | image-rendering: pixelated; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/PackView/PackSizeBar.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | text-align: center; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | font-size: 12px; 8 | padding: 2px; 9 | animation-duration: 300ms; 10 | pointer-events: none; 11 | position: relative; 12 | } 13 | .loading { 14 | position: absolute; 15 | left: 0; 16 | top: 100%; 17 | width: 100%; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/PackView/PackSizeBar.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import LinearProgress from '@mui/material/LinearProgress' 3 | import { useTheme } from '@mui/material/styles' 4 | import { observer } from 'mobx-react-lite' 5 | import { FunctionComponent } from 'react' 6 | import { useProject } from 'src/store/hooks' 7 | 8 | import styles from './PackSizeBar.module.css' 9 | 10 | const PackSizeBar: FunctionComponent = () => { 11 | const { palette } = useTheme() 12 | const { isPacking, ui } = useProject() 13 | const { width, height } = ui 14 | 15 | return ( 16 | 20 | Packed texture size: {width} x {height} 21 | {isPacking ? : null} 22 | 23 | ) 24 | } 25 | 26 | export default observer(PackSizeBar) 27 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/PackView/PackView.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | 3 | import PackCanvas from './PackCanvas' 4 | import PackSizeBar from './PackSizeBar' 5 | 6 | const PackView: FunctionComponent = () => { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default PackView 16 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/PackView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PackView' 2 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/LetterList.module.scss: -------------------------------------------------------------------------------- 1 | .letter { 2 | position: absolute; 3 | &:hover, 4 | &.select { 5 | background: rgba(0, 0, 0, 0.2); 6 | outline: 1px solid #000; 7 | } 8 | } 9 | .select { 10 | & + .next { 11 | background: rgba(0, 0, 0, 0.1); 12 | outline: 1px dashed #666; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/LetterList.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { FunctionComponent } from 'react' 4 | import { useProjectUi } from 'src/store/hooks' 5 | 6 | import styles from './LetterList.module.scss' 7 | import { PreviewObject } from './getPreviewCanvas' 8 | 9 | interface LetterListProps { 10 | data: PreviewObject 11 | drawYOffset: number 12 | } 13 | 14 | const LetterList: FunctionComponent = ( 15 | props: LetterListProps, 16 | ) => { 17 | const { 18 | data: { xOffset, yOffset, list }, 19 | drawYOffset, 20 | } = props 21 | const ui = useProjectUi() 22 | 23 | const handleSelect = ( 24 | e: React.MouseEvent, 25 | letter: string, 26 | next: string, 27 | ) => { 28 | e.stopPropagation() 29 | ui.setSelectLetter(letter, next) 30 | } 31 | return ( 32 | <> 33 | {list.map((item, idx) => { 34 | const key = `${item.letter}${idx}` 35 | return ( 36 |
handleSelect(e, item.letter, item.next)} 50 | /> 51 | ) 52 | })} 53 | 54 | ) 55 | } 56 | 57 | export default observer(LetterList) 58 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/Preview.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Grid from '@mui/material/Grid' 3 | 4 | import PreviewCanvas from './PreviewCanvas' 5 | import PreviewText from './PreviewText' 6 | import PreviewMertic from './PreviewMertic' 7 | import PreviewKerning from './PreviewKerning' 8 | 9 | const Preview: FunctionComponent = () => { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default Preview 29 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/PreviewCanvas.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | flex: 1; 4 | width: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | .wrap { 9 | transform-origin: 50% 50%; 10 | position: absolute; 11 | left: 50%; 12 | top: 50%; 13 | } 14 | .canvas { 15 | width: 100%; 16 | height: 100%; 17 | image-rendering: pixelated; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/PreviewKerning.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Input from '@mui/material/Input' 3 | import Typography from '@mui/material/Typography' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { FunctionComponent, useEffect, useState } from 'react' 6 | import GridInput from 'src/app/components/GridInput' 7 | import { GlyphFont, GlyphImage } from 'src/store' 8 | import { useProject } from 'src/store/hooks' 9 | 10 | const GlobalMetric: FunctionComponent = () => { 11 | const { 12 | glyphList, 13 | ui, 14 | style: { 15 | font: { opentype, size }, 16 | }, 17 | } = useProject() 18 | const [offset, setOffset] = useState(0) 19 | const [glyph, setGlyph] = useState() 20 | 21 | useEffect(() => { 22 | setGlyph(glyphList.find((gl) => gl.letter === ui.selectLetter)) 23 | }, [glyphList, ui.selectLetter]) 24 | 25 | useEffect(() => { 26 | if (glyph && ui.selectNextLetter && opentype) { 27 | const fontScale = (1 / opentype.unitsPerEm) * size 28 | setOffset( 29 | Math.round( 30 | opentype.getKerningValue( 31 | opentype.charToGlyphIndex(glyph.letter), 32 | opentype.charToGlyphIndex(ui.selectNextLetter), 33 | ) * fontScale, 34 | ), 35 | ) 36 | } 37 | }, [glyph, opentype, size, ui.selectNextLetter]) 38 | 39 | const handleChange = ( 40 | e: React.ChangeEvent, 41 | ) => { 42 | if (glyph) 43 | glyph.steKerning(ui.selectNextLetter, Number(e.target.value) - offset) 44 | } 45 | 46 | if (!glyph || !ui.selectNextLetter) return null 47 | 48 | return ( 49 | <> 50 | 51 | {`"${glyph.letter}" - "${ui.selectNextLetter}" Kerning`} 52 | 53 | 54 | 55 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export default observer(GlobalMetric) 68 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/PreviewMertic.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Typography from '@mui/material/Typography' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { FunctionComponent } from 'react' 5 | import FormAdjustMetric from 'src/app/layout/common/FormAdjustMetric' 6 | import { useProject } from 'src/store/hooks' 7 | 8 | const GlobalMetric: FunctionComponent = () => { 9 | const project = useProject() 10 | const { glyphList, ui } = project 11 | const glyph = glyphList.find((gl) => gl.letter === ui.selectLetter) 12 | if (!glyph) return null 13 | const { adjustMetric, letter } = glyph 14 | const { xAdvance, xOffset, yOffset, setXAdvance, setXOffset, setYOffset } = 15 | adjustMetric 16 | 17 | return ( 18 | <> 19 | 20 | {`"${letter}" Adjustment`} 21 | 22 | 30 | 31 | ) 32 | } 33 | 34 | export default observer(GlobalMetric) 35 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/PreviewText.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import TextField from '@mui/material/TextField' 3 | import Typography from '@mui/material/Typography' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { FunctionComponent, useState } from 'react' 6 | import { useProjectUi } from 'src/store/hooks' 7 | 8 | const Preview: FunctionComponent = () => { 9 | const { previewText, setPreviewText } = useProjectUi() 10 | const [isIME, setIsIME] = useState(false) 11 | const [inputText, setInputText] = useState(previewText) 12 | 13 | const handleInput = (event: React.ChangeEvent): void => { 14 | const { value } = event.target 15 | if (isIME) { 16 | setInputText(value) 17 | } else { 18 | setInputText(value) 19 | if (value !== previewText) setPreviewText(value) 20 | } 21 | } 22 | 23 | const handleCompositionEnd = (): void => { 24 | setIsIME(false) 25 | setInputText(inputText) 26 | if (inputText !== previewText) setPreviewText(inputText) 27 | } 28 | 29 | return ( 30 | 31 | 32 | Glyphs 33 | 34 | 35 | setIsIME(true)} 45 | onCompositionEnd={handleCompositionEnd} 46 | /> 47 | 48 | 49 | ) 50 | } 51 | 52 | export default observer(Preview) 53 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/getPreviewCanvas.ts: -------------------------------------------------------------------------------- 1 | import { BMFontChar } from 'src/file/export' 2 | 3 | interface PreviewItem { 4 | x: number 5 | y: number 6 | width: number 7 | height: number 8 | letter: string 9 | next: string 10 | } 11 | 12 | export interface PreviewObject { 13 | xOffset: number 14 | yOffset: number 15 | width: number 16 | height: number 17 | list: PreviewItem[] 18 | lines: number 19 | } 20 | 21 | export default function getPreviewCanvas( 22 | text: string, 23 | chars: Map, 24 | kernings: Map>, 25 | lineHeight: number, 26 | fontHeight: number, 27 | padding: number = 0, 28 | ): PreviewObject { 29 | const list: PreviewItem[] = [] 30 | const lines = text.split(/\r\n|\r|\n/) 31 | let minX = 0 32 | let minY = 0 33 | let maxX = 0 34 | let maxY = 0 35 | let y = 0 36 | let x = 0 37 | 38 | lines.forEach((str, index) => { 39 | y = lineHeight * index 40 | x = 0 41 | const arr = Array.from(str) 42 | arr.forEach((letter, idx) => { 43 | const char = chars.get(letter) 44 | if (!char) return 45 | const next = arr[idx + 1] 46 | const lk = kernings.get(letter.charCodeAt(0)) 47 | let kering = 0 48 | if (next && lk && lk.has(next.charCodeAt(0))) { 49 | kering = lk.get(next.charCodeAt(0)) || 0 50 | } 51 | const obj = { 52 | x: x + char.xoffset + (char.width === 0 ? 0 : padding), 53 | y: y + char.yoffset + (char.width === 0 ? 0 : padding), 54 | width: 55 | (char.width || char.xadvance) - (char.width === 0 ? 0 : padding * 2), 56 | height: 57 | (char.height || fontHeight) - (char.width === 0 ? 0 : padding * 2), 58 | letter: char.letter, 59 | next, 60 | } 61 | x += char.xadvance + kering 62 | minX = Math.min(obj.x, minX) 63 | minY = Math.min(obj.y, minY) 64 | maxX = Math.max(obj.x + obj.width, maxX) 65 | maxY = Math.max(obj.y + obj.height, maxY) 66 | list.push(obj) 67 | }) 68 | }) 69 | 70 | return { 71 | lines: lines.length, 72 | list, 73 | xOffset: minX, 74 | yOffset: minY, 75 | width: maxX - minX, 76 | height: Math.max(maxY - minY, lines.length * lineHeight - minY) + 2, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/Preview/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Preview' 2 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ProjectTabs/ProjectTab.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | min-height: auto; 3 | min-width: 80px; 4 | max-width: none; 5 | height: 34px; 6 | line-height: 16px; 7 | padding: 10px; 8 | color: rgba(255, 255, 255, 0.5); 9 | background-color: rgb(45, 45, 45); 10 | text-transform: none; 11 | display: inline-flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | cursor: pointer; 15 | &:hover { 16 | & .icon { 17 | opacity: 1; 18 | } 19 | } 20 | &:last-child { 21 | border-right: 0 none; 22 | } 23 | } 24 | .selected { 25 | color: #fff; 26 | & .icon { 27 | opacity: 1; 28 | } 29 | } 30 | .name { 31 | white-space: nowrap; 32 | position: relative; 33 | background: inherit; 34 | } 35 | .editor { 36 | color: rgba(0, 0, 0, 0); 37 | } 38 | .input { 39 | position: absolute; 40 | width: 100%; 41 | height: 100%; 42 | left: 0; 43 | font-size: inherit; 44 | padding: 0; 45 | border: 0 none; 46 | appearance: none; 47 | color: inherit; 48 | background: inherit; 49 | } 50 | .icon { 51 | width: 16px; 52 | height: 16px; 53 | margin-left: 10px; 54 | opacity: 0; 55 | } 56 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ProjectTabs/ProjectTabs.tsx: -------------------------------------------------------------------------------- 1 | import Tabs from '@mui/material/Tabs' 2 | import { useTheme } from '@mui/material/styles' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { FunctionComponent } from 'react' 5 | import { useWorkspace } from 'src/store/hooks' 6 | 7 | import ProjectTab from './ProjectTab' 8 | 9 | const ProjectTabs: FunctionComponent = () => { 10 | const { palette, shadows } = useTheme() 11 | const workSpace = useWorkspace() 12 | const { 13 | addProject, 14 | selectProject, 15 | removeProject, 16 | setProjectName, 17 | namedList, 18 | activeId, 19 | } = workSpace 20 | 21 | const handleChange = (e: unknown, value: number): void => { 22 | selectProject(value) 23 | } 24 | 25 | const handleRemove = ( 26 | e: React.MouseEvent, 27 | value?: number, 28 | ): void => { 29 | if (typeof value !== 'undefined') removeProject(value) 30 | } 31 | 32 | const handleDoubleClick = (): void => { 33 | addProject() 34 | } 35 | 36 | return ( 37 | 56 | {namedList.map((item) => { 57 | return ( 58 | 1} 60 | name={item.name} 61 | value={item.id} 62 | key={item.id} 63 | onRename={setProjectName} 64 | onRemove={handleRemove} 65 | /> 66 | ) 67 | })} 68 | 69 | ) 70 | } 71 | 72 | export default observer(ProjectTabs) 73 | -------------------------------------------------------------------------------- /src/app/layout/WorkSpace/modules/ProjectTabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ProjectTabs' 2 | -------------------------------------------------------------------------------- /src/app/layout/Wrap/UpdateToast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from '@mui/material/Button' 3 | import Snackbar from '@mui/material/Snackbar' 4 | import IconButton from '@mui/material/IconButton' 5 | import CloseIcon from '@mui/icons-material/Close' 6 | 7 | export interface SnackbarMessage { 8 | message: string 9 | key: number 10 | } 11 | 12 | export interface State { 13 | open: boolean 14 | snackPack: SnackbarMessage[] 15 | messageInfo?: SnackbarMessage 16 | } 17 | 18 | export default function ConsecutiveSnackbars() { 19 | const [open, setOpen] = React.useState(false) 20 | 21 | const handleClose = ( 22 | event: React.SyntheticEvent | Event, 23 | reason?: string, 24 | ) => { 25 | if (reason === 'clickaway') { 26 | return 27 | } 28 | setOpen(false) 29 | } 30 | 31 | const updateVersion = React.useCallback( 32 | (event: Event & { detail?: boolean }) => { 33 | const { detail } = event 34 | setOpen(!!detail) 35 | }, 36 | [], 37 | ) 38 | 39 | const handleReload = () => { 40 | window.location.reload() 41 | } 42 | 43 | React.useEffect(() => { 44 | window.addEventListener('updateVerion', updateVersion, false) 45 | return () => 46 | window.removeEventListener('updateVerion', updateVersion, false) 47 | }, [updateVersion]) 48 | 49 | return ( 50 | 60 | 63 | 69 | 70 | 71 | 72 | } 73 | /> 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/app/layout/Wrap/Wrap.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | .content { 8 | display: flex; 9 | flex: 1; 10 | position: relative; 11 | height: 0; 12 | overflow: hidden; 13 | } 14 | .loadingBackdrop { 15 | color: #fff; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/layout/Wrap/Wrap.tsx: -------------------------------------------------------------------------------- 1 | import Backdrop from '@mui/material/Backdrop' 2 | import Box from '@mui/material/Box' 3 | import CircularProgress from '@mui/material/CircularProgress' 4 | import { observer } from 'mobx-react-lite' 5 | import { FunctionComponent } from 'react' 6 | import useStores from 'src/store/hooks' 7 | 8 | import LeftBar from '../LeftBar' 9 | import RightBar from '../RightBar' 10 | import TitleBar from '../TitleBar' 11 | import WorkSpace from '../WorkSpace' 12 | import UpdateToast from './UpdateToast' 13 | import styles from './Wrap.module.css' 14 | 15 | const Wrap: FunctionComponent = () => { 16 | const { ui } = useStores() 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default observer(Wrap) 35 | -------------------------------------------------------------------------------- /src/app/layout/Wrap/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Wrap' 2 | -------------------------------------------------------------------------------- /src/app/layout/common/FormAdjustMetric/FormAdjustMetric.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Box from '@mui/material/Box' 3 | import Input from '@mui/material/Input' 4 | import GridInput from 'src/app/components/GridInput' 5 | 6 | interface SetHandle { 7 | (value: number): void 8 | } 9 | 10 | interface FormAdjustMetricProps { 11 | xAdvance: number 12 | xOffset: number 13 | yOffset: number 14 | setXAdvance: SetHandle 15 | setXOffset: SetHandle 16 | setYOffset: SetHandle 17 | } 18 | 19 | const FormAdjustMetric: FunctionComponent = ( 20 | props: FormAdjustMetricProps, 21 | ) => { 22 | const { xAdvance, xOffset, yOffset, setXAdvance, setXOffset, setYOffset } = 23 | props 24 | 25 | const getHandle = 26 | (handleSet: SetHandle) => (e: React.ChangeEvent) => 27 | handleSet(Number(e.target.value)) 28 | 29 | return ( 30 | <> 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | ) 63 | } 64 | 65 | export default FormAdjustMetric 66 | -------------------------------------------------------------------------------- /src/app/layout/common/FormAdjustMetric/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FormAdjustMetric' 2 | -------------------------------------------------------------------------------- /src/app/layout/common/FormAngle/FormAngle.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Input from '@mui/material/Input' 3 | 4 | import GridInput from 'src/app/components/GridInput' 5 | import AnglePicker, { AnglePickerProps } from 'src/app/components/AnglePicker' 6 | 7 | const FormAngle: FunctionComponent = ( 8 | props: AnglePickerProps, 9 | ) => { 10 | const { angle, onChange } = props 11 | 12 | return ( 13 | } 16 | > 17 | onChange(Number(e.target.value))} 22 | /> 23 | 24 | ) 25 | } 26 | 27 | export default FormAngle 28 | -------------------------------------------------------------------------------- /src/app/layout/common/FormAngle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FormAngle' 2 | -------------------------------------------------------------------------------- /src/app/layout/common/FormColor/FormColor.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | 3 | import GridInput from 'src/app/components/GridInput' 4 | import ColorInput from 'src/app/components/ColorInput' 5 | 6 | interface FormColorProps { 7 | color: string 8 | onChange(color: string): void 9 | } 10 | 11 | const FormColor: FunctionComponent = ( 12 | props: FormColorProps, 13 | ) => { 14 | const { color, onChange } = props 15 | 16 | return ( 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | export default FormColor 24 | -------------------------------------------------------------------------------- /src/app/layout/common/FormColor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FormColor' 2 | -------------------------------------------------------------------------------- /src/app/layout/common/FormFill/FormFill.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import FormControlLabel from '@mui/material/FormControlLabel' 3 | import Radio from '@mui/material/Radio' 4 | import RadioGroup from '@mui/material/RadioGroup' 5 | import { observer } from 'mobx-react-lite' 6 | import React, { FunctionComponent } from 'react' 7 | import { FillType, FontStyleConfig } from 'src/store' 8 | 9 | import FormColor from '../FormColor' 10 | import FormGradient from '../FormGradient' 11 | import FormImage from '../FormImage' 12 | 13 | interface FormFillProps { 14 | config: FontStyleConfig 15 | } 16 | 17 | const FormFill: FunctionComponent = (props: FormFillProps) => { 18 | const { 19 | config: { type, color, gradient, patternTexture, setType, setColor }, 20 | } = props 21 | 22 | return ( 23 | <> 24 | 25 | setType(Number(e.target.value))} 30 | > 31 | } 34 | label='Solid' 35 | /> 36 | } 39 | label='Gradient' 40 | /> 41 | } 44 | label='Image' 45 | /> 46 | 47 | 48 | {type === 0 ? ( 49 | 50 | 51 | 52 | ) : null} 53 | {type === 1 ? : null} 54 | {type === 2 ? ( 55 | 60 | ) : null} 61 | 62 | ) 63 | } 64 | 65 | export default observer(FormFill) 66 | -------------------------------------------------------------------------------- /src/app/layout/common/FormFill/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FormFill' 2 | -------------------------------------------------------------------------------- /src/app/layout/common/FormGradient/FormGradient.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import FormControlLabel from '@mui/material/FormControlLabel' 3 | import Radio from '@mui/material/Radio' 4 | import RadioGroup from '@mui/material/RadioGroup' 5 | import { observer } from 'mobx-react-lite' 6 | import React, { FunctionComponent } from 'react' 7 | import GradientPicker from 'src/app/components/GradientPicker' 8 | import GridInput from 'src/app/components/GridInput' 9 | import WrappedSketchPicker from 'src/app/components/WrappedSketchPicker' 10 | import { Gradient, GradientType } from 'src/store' 11 | 12 | import FormAngle from '../FormAngle' 13 | 14 | interface FormGradientProps { 15 | gradient: Gradient 16 | } 17 | 18 | const FormGradient: FunctionComponent = ( 19 | props: FormGradientProps, 20 | ) => { 21 | const { 22 | gradient: { 23 | type, 24 | angle, 25 | palette, 26 | addColor, 27 | updatePalette, 28 | setAngle, 29 | setType, 30 | }, 31 | } = props 32 | 33 | return ( 34 | <> 35 | 36 | 37 | 38 | 39 | 40 | 41 | setType(Number(e.target.value))} 46 | style={{ flexWrap: 'nowrap' }} 47 | > 48 | } 51 | label='Linear' 52 | /> 53 | } 56 | label='Radial' 57 | /> 58 | 59 | 60 | 61 | 62 | addColor(e.offset, e.color)} 65 | onUpdate={updatePalette} 66 | > 67 | 68 | 69 | 70 | 71 | ) 72 | } 73 | 74 | export default observer(FormGradient) 75 | -------------------------------------------------------------------------------- /src/app/layout/common/FormGradient/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FormGradient' 2 | -------------------------------------------------------------------------------- /src/app/layout/common/FormImage/FileSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import Box from '@mui/material/Box' 3 | import { useTheme } from '@mui/material/styles' 4 | 5 | import readFile from 'src/utils/readFile' 6 | 7 | interface FileSelectorProps { 8 | src: string 9 | onChange(image: ArrayBuffer): void 10 | } 11 | 12 | const FileSelector: FunctionComponent = ( 13 | props: FileSelectorProps, 14 | ) => { 15 | const { src, onChange } = props 16 | const theme = useTheme() 17 | 18 | const handleChange = (e: React.ChangeEvent): void => { 19 | if (!e.target.files) return 20 | if (e.target.files.length > 0) { 21 | readFile(e.target.files[0]).then((buffer) => { 22 | if (buffer instanceof ArrayBuffer) onChange(buffer) 23 | }) 24 | } 25 | } 26 | 27 | return ( 28 | 42 | 49 | 58 | 59 | ) 60 | } 61 | 62 | export default FileSelector 63 | -------------------------------------------------------------------------------- /src/app/layout/common/FormImage/FormImage.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box' 2 | import Input from '@mui/material/Input' 3 | import MenuItem from '@mui/material/MenuItem' 4 | import Select from '@mui/material/Select' 5 | import { observer } from 'mobx-react-lite' 6 | import React, { FunctionComponent } from 'react' 7 | import GridInput from 'src/app/components/GridInput' 8 | import { PatternTexture, Repetition } from 'src/store' 9 | 10 | import FileSelector from './FileSelector' 11 | 12 | interface FormImageProps { 13 | patternTexture: PatternTexture 14 | scale: number 15 | src: string 16 | } 17 | 18 | const FormImage: FunctionComponent = ( 19 | props: FormImageProps, 20 | ) => { 21 | const { patternTexture } = props 22 | const { src, scale, repetition, setRepetition, setScale, setImage } = 23 | patternTexture 24 | 25 | return ( 26 | <> 27 | 28 | } 32 | > 33 | setScale(Number(e.target.value))} 39 | /> 40 | 41 | 42 | 43 | 44 | 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | export default observer(FormImage) 63 | -------------------------------------------------------------------------------- /src/app/layout/common/FormImage/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FormImage' 2 | -------------------------------------------------------------------------------- /src/app/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { createTheme, responsiveFontSizes } from '@mui/material/styles' 2 | 3 | import components from './components' 4 | 5 | const theme = createTheme({ 6 | palette: { 7 | mode: 'dark', 8 | primary: { main: '#444' }, 9 | secondary: { main: '#424242' }, 10 | background: { 11 | paper: 'rgb(37, 37, 37)', 12 | default: 'rgb(30, 30, 30)', 13 | activityBar: 'rgb(51, 51, 51)', 14 | titleBar: 'rgb(50, 50, 50)', 15 | sidebar: 'rgb(37, 37, 37)', 16 | }, 17 | common: { 18 | black: 'rgb(30,30,30)', 19 | white: 'rgb(204,204,204)', 20 | }, 21 | action: { 22 | hover: 'rgba(255, 255, 255, 0.1)', 23 | }, 24 | }, 25 | bgPixel: { 26 | backgroundColor: '#fff', 27 | backgroundImage: ` 28 | linear-gradient(45deg, #ccc 25%, transparent 0, transparent 75%, #ccc 0), 29 | linear-gradient(45deg, #ccc 25%, transparent 0, transparent 75%, #ccc 0)`, 30 | backgroundSize: '8px 8px', 31 | backgroundPosition: '0 0, 4px 4px', 32 | backgroundRepeat: 'repeat', 33 | }, 34 | spacing: 4, 35 | typography: { fontSize: 13 }, 36 | transitions: { 37 | create: () => 'none', 38 | }, 39 | shape: { borderRadius: 0 }, 40 | components, 41 | }) 42 | 43 | export default responsiveFontSizes(theme) 44 | -------------------------------------------------------------------------------- /src/file/conversion/index.ts: -------------------------------------------------------------------------------- 1 | import conversionList from './types' 2 | export { encode } from './types/sbf' 3 | 4 | function conversion(inputFile: unknown) { 5 | const conversion = conversionList.find((item) => item.check(inputFile)) 6 | if (!conversion) throw new Error('unknow file') 7 | return conversion.decode(inputFile) 8 | } 9 | 10 | export default conversion 11 | -------------------------------------------------------------------------------- /src/file/conversion/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ConversionFileItem } from './type' 2 | import sbf from './sbf' 3 | import littera from './littera' 4 | 5 | const conversionList: ConversionFileItem[] = [sbf, littera] 6 | 7 | export default conversionList 8 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/check.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import validate from './schema' 3 | import { CheckFunction } from '../type' 4 | 5 | const check: CheckFunction = (litteraStr) => { 6 | let litteraData 7 | 8 | if (typeof litteraStr === 'string') { 9 | try { 10 | litteraData = JSON.parse(litteraStr) 11 | } catch (e) { 12 | return false 13 | } 14 | } 15 | 16 | if (typeof litteraData !== 'object') return false 17 | 18 | const isLittera = validate(litteraData) 19 | 20 | if (!isLittera) { 21 | if (process.env.NODE_ENV === 'development') 22 | console.log(isLittera, validate.errors) 23 | 24 | validate.errors?.forEach((item) => { 25 | Sentry.addBreadcrumb({ 26 | category: 'littera', 27 | message: 'Littera validate error', 28 | level: 'info', 29 | data: item, 30 | }) 31 | }) 32 | Sentry.captureMessage('Littera validate error') 33 | } 34 | 35 | return isLittera 36 | } 37 | 38 | export default check 39 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/index.ts: -------------------------------------------------------------------------------- 1 | import { ConversionFileItem } from '../type' 2 | import check from './check' 3 | import decode from './decode' 4 | 5 | const litteraFile: ConversionFileItem = { 6 | ext: '.ltr', 7 | check, 8 | decode, 9 | } 10 | 11 | export default litteraFile 12 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/background.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface BackgroundData { 4 | color: number 5 | alpha: number 6 | } 7 | 8 | const background: JTDSchemaType = { 9 | properties: { 10 | color: { type: 'float32' }, 11 | alpha: { type: 'float32' }, 12 | }, 13 | } 14 | 15 | export default background 16 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/bevel.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface BevelData { 4 | bevelEnabled: boolean 5 | highlightColor: number 6 | highlightAlpha: number 7 | quality: number 8 | angle: number 9 | shadowColor: number 10 | shadowAlpha: number 11 | blurX: number 12 | blurY: number 13 | type: 'inner' | 'outer' | 'full' 14 | strength: number 15 | distance: number 16 | } 17 | 18 | const bevel: JTDSchemaType = { 19 | properties: { 20 | bevelEnabled: { type: 'boolean' }, 21 | highlightColor: { type: 'float32' }, 22 | highlightAlpha: { type: 'float32' }, 23 | quality: { type: 'float32' }, 24 | angle: { type: 'float32' }, 25 | shadowColor: { type: 'float32' }, 26 | shadowAlpha: { type: 'float32' }, 27 | blurX: { type: 'float32' }, 28 | blurY: { type: 'float32' }, 29 | type: { enum: ['inner', 'outer', 'full'] }, 30 | strength: { type: 'float32' }, 31 | distance: { type: 'float32' }, 32 | }, 33 | } 34 | 35 | export default bevel 36 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/fill.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface FillData { 4 | gradientAlphas: number[] 5 | yOffset: number 6 | gradientType: 'linear' | 'radial' 7 | gradientRotation: number 8 | fillType: 'gradientFill' | 'textureFill' 9 | textureScale: number 10 | distanceFieldEnabled: boolean 11 | distanceFieldColor: number 12 | gradientColors: number[] 13 | distanceFieldDownscale: number 14 | distanceFieldSpread: number 15 | distanceFieldType: 'Type 1' | 'Type 2' 16 | gradientRatios: number[] 17 | xOffset: number 18 | texture?: string 19 | } 20 | 21 | const fill: JTDSchemaType = { 22 | properties: { 23 | gradientAlphas: { elements: { type: 'float32' } }, 24 | yOffset: { type: 'float32' }, 25 | gradientType: { enum: ['linear', 'radial'] }, 26 | gradientRotation: { type: 'float32' }, 27 | fillType: { enum: ['gradientFill', 'textureFill'] }, 28 | textureScale: { type: 'float32' }, 29 | distanceFieldEnabled: { type: 'boolean' }, 30 | distanceFieldColor: { type: 'float32' }, 31 | gradientColors: { elements: { type: 'float32' } }, 32 | distanceFieldDownscale: { type: 'float32' }, 33 | distanceFieldSpread: { type: 'float32' }, 34 | distanceFieldType: { enum: ['Type 1', 'Type 2'] }, 35 | gradientRatios: { elements: { type: 'float32' } }, 36 | xOffset: { type: 'float32' }, 37 | }, 38 | optionalProperties: { 39 | texture: { type: 'string' }, 40 | }, 41 | } 42 | 43 | export default fill 44 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/font.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface FontData { 4 | size: number 5 | data?: string 6 | spacing: number 7 | } 8 | 9 | const font: JTDSchemaType = { 10 | properties: { 11 | size: { type: 'float32' }, 12 | spacing: { type: 'float32' }, 13 | }, 14 | optionalProperties: { 15 | data: { type: 'string' }, 16 | }, 17 | } 18 | 19 | export default font 20 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/glow.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface GlowData { 4 | quality: number 5 | colors: number[] 6 | glowEnabled: boolean 7 | alphas: number[] 8 | ratios: number[] 9 | blurX: number 10 | angle: number 11 | blurY: number 12 | strength: number 13 | distance: number 14 | } 15 | 16 | const glow: JTDSchemaType = { 17 | properties: { 18 | quality: { type: 'float32' }, 19 | colors: { elements: { type: 'float32' } }, 20 | glowEnabled: { type: 'boolean' }, 21 | alphas: { elements: { type: 'float32' } }, 22 | ratios: { elements: { type: 'float32' } }, 23 | blurX: { type: 'float32' }, 24 | angle: { type: 'float32' }, 25 | blurY: { type: 'float32' }, 26 | strength: { type: 'float32' }, 27 | distance: { type: 'float32' }, 28 | }, 29 | } 30 | 31 | export default glow 32 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/glyphs.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface GlyphsData { 4 | glyphs: string 5 | powerOfTwo: boolean 6 | canvasHeight: string 7 | padding: number 8 | packMethod: number 9 | canvasWidth: string 10 | roundValues: boolean 11 | descriptionFormat: number 12 | } 13 | 14 | const glyphs: JTDSchemaType = { 15 | properties: { 16 | glyphs: { type: 'string' }, 17 | powerOfTwo: { type: 'boolean' }, 18 | canvasHeight: { type: 'string' }, 19 | padding: { type: 'float32' }, 20 | packMethod: { type: 'float32' }, 21 | canvasWidth: { type: 'string' }, 22 | roundValues: { type: 'boolean' }, 23 | descriptionFormat: { type: 'float32' }, 24 | }, 25 | } 26 | 27 | export default glyphs 28 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/index.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { JTDSchemaType } from 'ajv/dist/jtd' 2 | import glow, { GlowData } from './glow' 3 | import fill, { FillData } from './fill' 4 | import settings, { SettingsData } from './settings' 5 | import shadow, { ShadowData } from './shadow' 6 | import stroke, { StrokeData } from './stroke' 7 | import background, { BackgroundData } from './background' 8 | import bevel, { BevelData } from './bevel' 9 | import glyphs, { GlyphsData } from './glyphs' 10 | import font, { FontData } from './font' 11 | 12 | const ajv = new Ajv() 13 | 14 | export interface LitteraData { 15 | glow: GlowData 16 | fill: FillData 17 | settings: SettingsData 18 | shadow: ShadowData 19 | stroke: StrokeData 20 | background: BackgroundData 21 | bevel: BevelData 22 | glyphs: GlyphsData 23 | font: FontData 24 | fallbackfont?: string 25 | } 26 | 27 | const schema: JTDSchemaType = { 28 | properties: { 29 | glow, 30 | fill, 31 | settings, 32 | shadow, 33 | stroke, 34 | background, 35 | bevel, 36 | glyphs, 37 | font, 38 | }, 39 | optionalProperties: { 40 | fallbackfont: { type: 'string' }, 41 | }, 42 | } 43 | 44 | export * from './glow' 45 | export * from './fill' 46 | export * from './settings' 47 | export * from './shadow' 48 | export * from './stroke' 49 | export * from './background' 50 | export * from './bevel' 51 | export * from './glyphs' 52 | export * from './font' 53 | 54 | export const validate = ajv.compile(schema) 55 | 56 | export default validate 57 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/settings.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface SettingsData { 4 | postfixes: string 5 | filename: string 6 | scalings: string 7 | } 8 | 9 | const settings: JTDSchemaType = { 10 | properties: { 11 | postfixes: { type: 'string' }, 12 | filename: { type: 'string' }, 13 | scalings: { type: 'string' }, 14 | }, 15 | } 16 | 17 | export default settings 18 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/shadow.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface ShadowData { 4 | quality: number 5 | color: number 6 | strength: number 7 | blurX: number 8 | angle: number 9 | blurY: number 10 | shadowEnabled: boolean 11 | alpha: number 12 | distance: number 13 | } 14 | 15 | const shadow: JTDSchemaType = { 16 | properties: { 17 | quality: { type: 'float32' }, 18 | color: { type: 'float32' }, 19 | strength: { type: 'float32' }, 20 | blurX: { type: 'float32' }, 21 | angle: { type: 'float32' }, 22 | blurY: { type: 'float32' }, 23 | shadowEnabled: { type: 'boolean' }, 24 | alpha: { type: 'float32' }, 25 | distance: { type: 'float32' }, 26 | }, 27 | } 28 | 29 | export default shadow 30 | -------------------------------------------------------------------------------- /src/file/conversion/types/littera/schema/stroke.ts: -------------------------------------------------------------------------------- 1 | import { JTDSchemaType } from 'ajv/dist/jtd' 2 | 3 | export interface StrokeData { 4 | gradientAlphas: number[] 5 | yOffset: number 6 | gradientType: 'linear' | 'radial' 7 | gradientRotation: number 8 | fillType: 'gradientFill' | 'textureFill' 9 | pixelHinting: boolean 10 | textureScale: number 11 | gradientColors: number[] 12 | strokeEnabled: boolean 13 | miterLimit: number 14 | jointStyle: 'miter' | 'bevel' | 'round' 15 | size: number 16 | gradientRatios: number[] 17 | xOffset: number 18 | texture?: string 19 | } 20 | 21 | const stroke: JTDSchemaType = { 22 | properties: { 23 | gradientAlphas: { elements: { type: 'float32' } }, 24 | yOffset: { type: 'float32' }, 25 | gradientType: { enum: ['linear', 'radial'] }, 26 | gradientRotation: { type: 'float32' }, 27 | fillType: { enum: ['gradientFill', 'textureFill'] }, 28 | pixelHinting: { type: 'boolean' }, 29 | textureScale: { type: 'float32' }, 30 | gradientColors: { elements: { type: 'float32' } }, 31 | strokeEnabled: { type: 'boolean' }, 32 | miterLimit: { type: 'float32' }, 33 | jointStyle: { enum: ['miter', 'bevel', 'round'] }, 34 | size: { type: 'float32' }, 35 | gradientRatios: { elements: { type: 'float32' } }, 36 | xOffset: { type: 'float32' }, 37 | }, 38 | optionalProperties: { 39 | texture: { type: 'string' }, 40 | }, 41 | } 42 | 43 | export default stroke 44 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/check.ts: -------------------------------------------------------------------------------- 1 | import { CheckFunction } from '../type' 2 | import getVersion from './getVersion' 3 | 4 | const check: CheckFunction = (buffer) => getVersion(buffer) > 0 5 | 6 | export default check 7 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/decode.ts: -------------------------------------------------------------------------------- 1 | import { DecodeProjectFunction } from '../type' 2 | import { 3 | Project as ProjectProto, 4 | oldProto, 5 | OldProto, 6 | toOriginBuffer, 7 | } from './proto/index' 8 | import prefix from './prefix' 9 | import getVersion from './getVersion' 10 | import updateOldProject from './updateOldProject' 11 | 12 | const decode: DecodeProjectFunction = (buffer) => { 13 | if (!(buffer instanceof ArrayBuffer)) throw new Error('unknow file') 14 | 15 | const version = getVersion(buffer) 16 | 17 | if (version === 0) throw new Error('unknow file') 18 | 19 | const perfixBuffer = prefix() 20 | const u8 = new Uint8Array(buffer) 21 | const filePrefix = u8.slice(0, perfixBuffer.byteLength) 22 | 23 | const decodeProto = 24 | oldProto[version as keyof OldProto]?.Project || ProjectProto 25 | 26 | const project = decodeProto.decode(u8.slice(filePrefix.byteLength)) 27 | 28 | return toOriginBuffer(updateOldProject(project, version)) 29 | } 30 | 31 | export default decode 32 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/encode.ts: -------------------------------------------------------------------------------- 1 | import { Project } from 'src/store' 2 | import { encodeProject } from './proto' 3 | 4 | import prefix from './prefix' 5 | 6 | export default function encode(project: Project): Uint8Array { 7 | const perfixBuffer = prefix() 8 | const projectBuffer = encodeProject(project) 9 | 10 | const buffer = new Uint8Array( 11 | perfixBuffer.byteLength + projectBuffer.byteLength, 12 | ) 13 | 14 | buffer.set(perfixBuffer, 0) 15 | buffer.set(projectBuffer, perfixBuffer.byteLength) 16 | 17 | return buffer 18 | } 19 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/getVersion.ts: -------------------------------------------------------------------------------- 1 | import getVersionNumber from 'src/utils/getVersionNumber' 2 | import prefix from './prefix' 3 | 4 | export default function decode(buffer: unknown): number { 5 | if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 17) return 0 6 | const perfixBuffer = prefix() 7 | const perfixName = perfixBuffer.slice(0, perfixBuffer.byteLength - 3) 8 | const u8 = new Uint8Array(buffer) 9 | const filePrefix = u8.slice(0, perfixBuffer.byteLength) 10 | const versionBuffer = filePrefix.slice(filePrefix.byteLength - 3) 11 | let isSbf = true 12 | 13 | perfixName.forEach((e, i) => { 14 | if (filePrefix[i] !== e) isSbf = false 15 | }) 16 | 17 | if (!isSbf) return 0 18 | 19 | return getVersionNumber(Array.from(versionBuffer)) 20 | } 21 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/index.ts: -------------------------------------------------------------------------------- 1 | import { ConversionFileItem } from '../type' 2 | import check from './check' 3 | import decode from './decode' 4 | 5 | const sbfFile: ConversionFileItem = { 6 | ext: '.sbf', 7 | check, 8 | decode, 9 | } 10 | 11 | export { default as encode } from './encode' 12 | export default sbfFile 13 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/prefix.ts: -------------------------------------------------------------------------------- 1 | export const PREFIX_STR = 'SnowBambooBMF' 2 | const prefix = (): Uint8Array => 3 | new Uint8Array([...PREFIX_STR.split('').map((s) => s.charCodeAt(0)), 1, 1, 2]) 4 | 5 | export default prefix 6 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.0/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project' 2 | export { default } from './project' 3 | export { default as updateToNext } from './updateToNext' 4 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.0/project.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Metric { 4 | sint32 xAdvance = 1; 5 | 6 | sint32 xOffset = 2; 7 | 8 | sint32 yOffset = 3; 9 | } 10 | 11 | message GradientColor { 12 | int32 id = 1; 13 | 14 | sint32 offset = 2; 15 | 16 | string color = 3; 17 | } 18 | 19 | message Gradient { 20 | int32 type = 1; 21 | 22 | float angle = 2; 23 | 24 | repeated GradientColor palette = 3; 25 | } 26 | 27 | message PatternTexture { 28 | bytes buffer = 1; 29 | 30 | double scale = 2; 31 | 32 | string repetition = 3; 33 | } 34 | 35 | message Fill { 36 | int32 type = 1; 37 | 38 | string color = 2; 39 | 40 | Gradient gradient = 3; 41 | 42 | PatternTexture patternTexture = 4; 43 | 44 | int32 width = 5; 45 | 46 | string lineCap = 6; 47 | 48 | string lineJoin = 7; 49 | } 50 | 51 | message Font { 52 | optional bytes font = 1; 53 | 54 | string family = 2; 55 | 56 | int32 size = 3; 57 | 58 | int32 lineHeight = 4; 59 | } 60 | 61 | message GlyphFont { 62 | string letter = 1; 63 | 64 | Metric adjustMetric = 2; 65 | 66 | map kerning = 3; 67 | } 68 | 69 | message GlyphImage { 70 | string letter = 1; 71 | 72 | Metric adjustMetric = 2; 73 | 74 | bytes buffer = 3; 75 | 76 | string fileName = 4; 77 | 78 | string fileType = 5; 79 | 80 | bool selected = 6; 81 | 82 | map kerning = 7; 83 | } 84 | 85 | message Layout { 86 | int32 padding = 1; 87 | 88 | int32 spacing = 2; 89 | 90 | bool power = 3; 91 | } 92 | 93 | message Shadow { 94 | string color = 1; 95 | 96 | int32 blur = 2; 97 | 98 | sint32 offsetX = 3; 99 | 100 | sint32 offsetY = 4; 101 | } 102 | 103 | message Style { 104 | Font font = 1; 105 | 106 | Fill fill = 2; 107 | 108 | bool useStroke = 3; 109 | 110 | Fill stroke = 4; 111 | 112 | bool useShadow = 5; 113 | 114 | Shadow shadow = 6; 115 | 116 | string bgColor = 7; 117 | } 118 | 119 | message Ui { 120 | string previewText = 1; 121 | } 122 | 123 | message Project { 124 | int64 id = 1; 125 | 126 | string name = 2; 127 | 128 | string text = 3; 129 | 130 | map glyphs = 4; 131 | 132 | repeated GlyphImage glyphImages = 5; 133 | 134 | Style style = 6; 135 | 136 | Layout layout = 7; 137 | 138 | Metric globalAdjustMetric = 8; 139 | 140 | Ui ui = 9; 141 | } -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.0/updateToNext.ts: -------------------------------------------------------------------------------- 1 | import { IProject } from './project' 2 | import { IProject as IProjectNext } from '../1.0.1' 3 | 4 | export default function updateToNext(project: IProject): IProjectNext { 5 | const next = project as IProjectNext 6 | next.layout = { ...project.layout } 7 | next.layout.width = 1024 8 | next.layout.height = 1024 9 | next.layout.auto = true 10 | next.layout.fixedSize = false 11 | return next 12 | } 13 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.1/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project' 2 | export { default } from './project' 3 | export { default as updateToNext } from './updateToNext' 4 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.1/project.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Metric { 4 | sint32 xAdvance = 1; 5 | 6 | sint32 xOffset = 2; 7 | 8 | sint32 yOffset = 3; 9 | } 10 | 11 | message GradientColor { 12 | int32 id = 1; 13 | 14 | sint32 offset = 2; 15 | 16 | string color = 3; 17 | } 18 | 19 | message Gradient { 20 | int32 type = 1; 21 | 22 | float angle = 2; 23 | 24 | repeated GradientColor palette = 3; 25 | } 26 | 27 | message PatternTexture { 28 | bytes buffer = 1; 29 | 30 | double scale = 2; 31 | 32 | string repetition = 3; 33 | } 34 | 35 | message Fill { 36 | int32 type = 1; 37 | 38 | string color = 2; 39 | 40 | Gradient gradient = 3; 41 | 42 | PatternTexture patternTexture = 4; 43 | 44 | int32 width = 5; 45 | 46 | string lineCap = 6; 47 | 48 | string lineJoin = 7; 49 | } 50 | 51 | message Font { 52 | optional bytes font = 1; 53 | 54 | string family = 2; 55 | 56 | int32 size = 3; 57 | 58 | int32 lineHeight = 4; 59 | } 60 | 61 | message GlyphFont { 62 | string letter = 1; 63 | 64 | Metric adjustMetric = 2; 65 | 66 | map kerning = 3; 67 | } 68 | 69 | message GlyphImage { 70 | string letter = 1; 71 | 72 | Metric adjustMetric = 2; 73 | 74 | bytes buffer = 3; 75 | 76 | string fileName = 4; 77 | 78 | string fileType = 5; 79 | 80 | bool selected = 6; 81 | 82 | map kerning = 7; 83 | } 84 | 85 | message Layout { 86 | int32 padding = 1; 87 | 88 | int32 spacing = 2; 89 | 90 | int32 width = 3; 91 | 92 | int32 height = 4; 93 | 94 | bool auto = 5; 95 | 96 | bool fixedSize = 6; 97 | } 98 | 99 | message Shadow { 100 | string color = 1; 101 | 102 | int32 blur = 2; 103 | 104 | sint32 offsetX = 3; 105 | 106 | sint32 offsetY = 4; 107 | } 108 | 109 | message Style { 110 | Font font = 1; 111 | 112 | Fill fill = 2; 113 | 114 | bool useStroke = 3; 115 | 116 | Fill stroke = 4; 117 | 118 | bool useShadow = 5; 119 | 120 | Shadow shadow = 6; 121 | 122 | string bgColor = 7; 123 | } 124 | 125 | message Ui { 126 | string previewText = 1; 127 | } 128 | 129 | message Project { 130 | int64 id = 1; 131 | 132 | string name = 2; 133 | 134 | string text = 3; 135 | 136 | map glyphs = 4; 137 | 138 | repeated GlyphImage glyphImages = 5; 139 | 140 | Style style = 6; 141 | 142 | Layout layout = 7; 143 | 144 | Metric globalAdjustMetric = 8; 145 | 146 | Ui ui = 9; 147 | } -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.1/updateToNext.ts: -------------------------------------------------------------------------------- 1 | import { IProject } from './project' 2 | import { IProject as IProjectNext, IGradientColor } from '../1.0.2' 3 | 4 | export default function updateToNext(project: IProject): IProjectNext { 5 | function fixOffset(list: IGradientColor[]) { 6 | const len = list.length - 1 7 | list.forEach((item, idx) => { 8 | item.offset = (1 / len) * idx 9 | }) 10 | } 11 | if ( 12 | project?.style?.fill?.gradient?.palette && 13 | project.style.fill.gradient.palette.length > 0 14 | ) { 15 | fixOffset(project.style.fill.gradient.palette) 16 | } 17 | 18 | if ( 19 | project?.style?.stroke?.gradient?.palette && 20 | project.style.stroke.gradient.palette.length > 0 21 | ) { 22 | fixOffset(project.style.stroke.gradient.palette) 23 | } 24 | 25 | return project 26 | } 27 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.2/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project' 2 | export { default } from './project' 3 | export { default as updateToNext } from './updateToNext' 4 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.2/project.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Metric { 4 | sint32 xAdvance = 1; 5 | 6 | sint32 xOffset = 2; 7 | 8 | sint32 yOffset = 3; 9 | } 10 | 11 | message GradientColor { 12 | int32 id = 1; 13 | 14 | float offset = 2; 15 | 16 | string color = 3; 17 | } 18 | 19 | message Gradient { 20 | int32 type = 1; 21 | 22 | float angle = 2; 23 | 24 | repeated GradientColor palette = 3; 25 | } 26 | 27 | message PatternTexture { 28 | bytes buffer = 1; 29 | 30 | double scale = 2; 31 | 32 | string repetition = 3; 33 | } 34 | 35 | message Fill { 36 | int32 type = 1; 37 | 38 | string color = 2; 39 | 40 | Gradient gradient = 3; 41 | 42 | PatternTexture patternTexture = 4; 43 | 44 | int32 width = 5; 45 | 46 | string lineCap = 6; 47 | 48 | string lineJoin = 7; 49 | } 50 | 51 | message Font { 52 | optional bytes font = 1; 53 | 54 | string family = 2; 55 | 56 | int32 size = 3; 57 | 58 | int32 lineHeight = 4; 59 | } 60 | 61 | message GlyphFont { 62 | string letter = 1; 63 | 64 | Metric adjustMetric = 2; 65 | 66 | map kerning = 3; 67 | } 68 | 69 | message GlyphImage { 70 | string letter = 1; 71 | 72 | Metric adjustMetric = 2; 73 | 74 | bytes buffer = 3; 75 | 76 | string fileName = 4; 77 | 78 | string fileType = 5; 79 | 80 | bool selected = 6; 81 | 82 | map kerning = 7; 83 | } 84 | 85 | message Layout { 86 | int32 padding = 1; 87 | 88 | int32 spacing = 2; 89 | 90 | int32 width = 3; 91 | 92 | int32 height = 4; 93 | 94 | bool auto = 5; 95 | 96 | bool fixedSize = 6; 97 | } 98 | 99 | message Shadow { 100 | string color = 1; 101 | 102 | int32 blur = 2; 103 | 104 | sint32 offsetX = 3; 105 | 106 | sint32 offsetY = 4; 107 | } 108 | 109 | message Style { 110 | Font font = 1; 111 | 112 | Fill fill = 2; 113 | 114 | bool useStroke = 3; 115 | 116 | Fill stroke = 4; 117 | 118 | bool useShadow = 5; 119 | 120 | Shadow shadow = 6; 121 | 122 | string bgColor = 7; 123 | } 124 | 125 | message Ui { 126 | string previewText = 1; 127 | } 128 | 129 | message Project { 130 | int64 id = 1; 131 | 132 | string name = 2; 133 | 134 | string text = 3; 135 | 136 | map glyphs = 4; 137 | 138 | repeated GlyphImage glyphImages = 5; 139 | 140 | Style style = 6; 141 | 142 | Layout layout = 7; 143 | 144 | Metric globalAdjustMetric = 8; 145 | 146 | Ui ui = 9; 147 | } -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.0.2/updateToNext.ts: -------------------------------------------------------------------------------- 1 | import { IProject } from './project' 2 | import { IProject as IProjectNext, IFont } from '../1.1.0' 3 | 4 | export default function updateToNext(project: IProject): IProjectNext { 5 | if (project.style?.font?.font) { 6 | ;(project.style.font as IFont).fonts = [{ font: project.style.font.font }] 7 | } 8 | return project 9 | } 10 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.1.0/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project' 2 | export { default } from './project' 3 | export { default as updateToNext } from './updateToNext' 4 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.1.0/project.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Metric { 4 | sint32 xAdvance = 1; 5 | 6 | sint32 xOffset = 2; 7 | 8 | sint32 yOffset = 3; 9 | } 10 | 11 | message GradientColor { 12 | int32 id = 1; 13 | 14 | float offset = 2; 15 | 16 | string color = 3; 17 | } 18 | 19 | message Gradient { 20 | int32 type = 1; 21 | 22 | float angle = 2; 23 | 24 | repeated GradientColor palette = 3; 25 | } 26 | 27 | message PatternTexture { 28 | bytes buffer = 1; 29 | 30 | double scale = 2; 31 | 32 | string repetition = 3; 33 | } 34 | 35 | message Fill { 36 | int32 type = 1; 37 | 38 | string color = 2; 39 | 40 | Gradient gradient = 3; 41 | 42 | PatternTexture patternTexture = 4; 43 | 44 | int32 width = 5; 45 | 46 | string lineCap = 6; 47 | 48 | string lineJoin = 7; 49 | } 50 | 51 | message FontResource { 52 | bytes font = 1; 53 | } 54 | 55 | message Font { 56 | repeated FontResource fonts = 1; 57 | 58 | int32 size = 2; 59 | 60 | int32 lineHeight = 3; 61 | } 62 | 63 | message GlyphFont { 64 | string letter = 1; 65 | 66 | Metric adjustMetric = 2; 67 | 68 | map kerning = 3; 69 | } 70 | 71 | message GlyphImage { 72 | string letter = 1; 73 | 74 | Metric adjustMetric = 2; 75 | 76 | bytes buffer = 3; 77 | 78 | string fileName = 4; 79 | 80 | string fileType = 5; 81 | 82 | bool selected = 6; 83 | 84 | map kerning = 7; 85 | } 86 | 87 | message Layout { 88 | int32 padding = 1; 89 | 90 | int32 spacing = 2; 91 | 92 | int32 width = 3; 93 | 94 | int32 height = 4; 95 | 96 | bool auto = 5; 97 | 98 | bool fixedSize = 6; 99 | } 100 | 101 | message Shadow { 102 | string color = 1; 103 | 104 | int32 blur = 2; 105 | 106 | sint32 offsetX = 3; 107 | 108 | sint32 offsetY = 4; 109 | } 110 | 111 | message Style { 112 | Font font = 1; 113 | 114 | Fill fill = 2; 115 | 116 | bool useStroke = 3; 117 | 118 | Fill stroke = 4; 119 | 120 | bool useShadow = 5; 121 | 122 | Shadow shadow = 6; 123 | 124 | string bgColor = 7; 125 | } 126 | 127 | message Ui { 128 | string previewText = 1; 129 | } 130 | 131 | message Project { 132 | int64 id = 1; 133 | 134 | string name = 2; 135 | 136 | string text = 3; 137 | 138 | map glyphs = 4; 139 | 140 | repeated GlyphImage glyphImages = 5; 141 | 142 | Style style = 6; 143 | 144 | Layout layout = 7; 145 | 146 | Metric globalAdjustMetric = 8; 147 | 148 | Ui ui = 9; 149 | } -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.1.0/updateToNext.ts: -------------------------------------------------------------------------------- 1 | import { IProject } from './project' 2 | import { IProject as IProjectNext, IFill } from '../project' 3 | 4 | export default function updateToNext(project: IProject): IProjectNext { 5 | if (project.style?.stroke) { 6 | ;(project.style?.stroke as IFill).strokeType = 7 | (project.style?.stroke as IFill)?.strokeType || 0 8 | } 9 | return project 10 | } 11 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.1.1/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project' 2 | export { default } from './project' 3 | export { default as updateToNext } from './updateToNext' 4 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.1.1/project.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Metric { 4 | sint32 xAdvance = 1; 5 | 6 | sint32 xOffset = 2; 7 | 8 | sint32 yOffset = 3; 9 | } 10 | 11 | message GradientColor { 12 | int32 id = 1; 13 | 14 | float offset = 2; 15 | 16 | string color = 3; 17 | } 18 | 19 | message Gradient { 20 | int32 type = 1; 21 | 22 | float angle = 2; 23 | 24 | repeated GradientColor palette = 3; 25 | } 26 | 27 | message PatternTexture { 28 | bytes buffer = 1; 29 | 30 | double scale = 2; 31 | 32 | string repetition = 3; 33 | } 34 | 35 | message Fill { 36 | int32 type = 1; 37 | 38 | string color = 2; 39 | 40 | Gradient gradient = 3; 41 | 42 | PatternTexture patternTexture = 4; 43 | 44 | int32 width = 5; 45 | 46 | string lineCap = 6; 47 | 48 | string lineJoin = 7; 49 | 50 | int32 strokeType = 8; 51 | } 52 | 53 | message FontResource { 54 | bytes font = 1; 55 | } 56 | 57 | message Font { 58 | repeated FontResource fonts = 1; 59 | 60 | int32 size = 2; 61 | 62 | int32 lineHeight = 3; 63 | } 64 | 65 | message GlyphFont { 66 | string letter = 1; 67 | 68 | Metric adjustMetric = 2; 69 | 70 | map kerning = 3; 71 | } 72 | 73 | message GlyphImage { 74 | string letter = 1; 75 | 76 | Metric adjustMetric = 2; 77 | 78 | bytes buffer = 3; 79 | 80 | string fileName = 4; 81 | 82 | string fileType = 5; 83 | 84 | bool selected = 6; 85 | 86 | map kerning = 7; 87 | } 88 | 89 | message Layout { 90 | int32 padding = 1; 91 | 92 | int32 spacing = 2; 93 | 94 | int32 width = 3; 95 | 96 | int32 height = 4; 97 | 98 | bool auto = 5; 99 | 100 | bool fixedSize = 6; 101 | } 102 | 103 | message Shadow { 104 | string color = 1; 105 | 106 | int32 blur = 2; 107 | 108 | sint32 offsetX = 3; 109 | 110 | sint32 offsetY = 4; 111 | } 112 | 113 | message Style { 114 | Font font = 1; 115 | 116 | Fill fill = 2; 117 | 118 | bool useStroke = 3; 119 | 120 | Fill stroke = 4; 121 | 122 | bool useShadow = 5; 123 | 124 | Shadow shadow = 6; 125 | 126 | string bgColor = 7; 127 | } 128 | 129 | message Ui { 130 | string previewText = 1; 131 | } 132 | 133 | message Project { 134 | int64 id = 1; 135 | 136 | string name = 2; 137 | 138 | string text = 3; 139 | 140 | map glyphs = 4; 141 | 142 | repeated GlyphImage glyphImages = 5; 143 | 144 | Style style = 6; 145 | 146 | Layout layout = 7; 147 | 148 | Metric globalAdjustMetric = 8; 149 | 150 | Ui ui = 9; 151 | } -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/1.1.1/updateToNext.ts: -------------------------------------------------------------------------------- 1 | import { IProject as IProjectNext } from '../project' 2 | import { IProject } from './project' 3 | 4 | export default function updateToNext(project: IProject): IProjectNext { 5 | return project 6 | } 7 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/encodeProject.ts: -------------------------------------------------------------------------------- 1 | import { Project } from 'src/store' 2 | import { Project as ProjectProto, IProject } from './project' 3 | import deepMapToObject from 'src/utils/deepMapToObject' 4 | 5 | export default function saveProject(project: Project): Uint8Array { 6 | // font 7 | if (project.style.font.fonts && project.style.font.fonts.length) { 8 | project.style.font.fonts.forEach( 9 | (fontResource) => (fontResource.font = new Uint8Array(fontResource.font)), 10 | ) 11 | } 12 | 13 | // images 14 | project.glyphImages.forEach((glyphImage) => { 15 | if (glyphImage.buffer) glyphImage.buffer = new Uint8Array(glyphImage.buffer) 16 | }) 17 | 18 | // fill 19 | if (project.style.fill.patternTexture.buffer) { 20 | project.style.fill.patternTexture.buffer = new Uint8Array( 21 | project.style.fill.patternTexture.buffer, 22 | ) 23 | } 24 | 25 | // stroke 26 | if (project.style.stroke.patternTexture.buffer) { 27 | project.style.stroke.patternTexture.buffer = new Uint8Array( 28 | project.style.stroke.patternTexture.buffer, 29 | ) 30 | } 31 | 32 | return ProjectProto.encode( 33 | ProjectProto.create(deepMapToObject(project) as unknown as IProject), 34 | ).finish() 35 | } 36 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/index.ts: -------------------------------------------------------------------------------- 1 | import * as proto1000000 from './1.0.0' 2 | import * as proto1000001 from './1.0.1' 3 | import * as proto1000002 from './1.0.2' 4 | import * as proto1001000 from './1.1.0' 5 | import * as proto1001001 from './1.1.1' 6 | 7 | export interface OldProto { 8 | 1000000: typeof proto1000000 9 | 1000001: typeof proto1000001 10 | 1000002: typeof proto1000002 11 | 1001000: typeof proto1001000 12 | 1001001: typeof proto1001001 13 | } 14 | 15 | export const oldProto: OldProto = { 16 | 1000000: proto1000000, 17 | 1000001: proto1000001, 18 | 1000002: proto1000002, 19 | 1001000: proto1001000, 20 | 1001001: proto1001001, 21 | } 22 | 23 | export { default as encodeProject } from './encodeProject' 24 | export { default as toOriginBuffer } from './toOriginBuffer' 25 | export * from './project' 26 | export { default } from './project' 27 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/project.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Metric { 4 | sint32 xAdvance = 1; 5 | 6 | sint32 xOffset = 2; 7 | 8 | sint32 yOffset = 3; 9 | } 10 | 11 | message GradientColor { 12 | int32 id = 1; 13 | 14 | float offset = 2; 15 | 16 | string color = 3; 17 | } 18 | 19 | message Gradient { 20 | int32 type = 1; 21 | 22 | float angle = 2; 23 | 24 | repeated GradientColor palette = 3; 25 | } 26 | 27 | message PatternTexture { 28 | bytes buffer = 1; 29 | 30 | double scale = 2; 31 | 32 | string repetition = 3; 33 | } 34 | 35 | message Fill { 36 | int32 type = 1; 37 | 38 | string color = 2; 39 | 40 | Gradient gradient = 3; 41 | 42 | PatternTexture patternTexture = 4; 43 | 44 | int32 width = 5; 45 | 46 | string lineCap = 6; 47 | 48 | string lineJoin = 7; 49 | 50 | int32 strokeType = 8; 51 | } 52 | 53 | message FontResource { 54 | bytes font = 1; 55 | } 56 | 57 | message Font { 58 | repeated FontResource fonts = 1; 59 | 60 | int32 size = 2; 61 | 62 | float lineHeight = 3; 63 | } 64 | 65 | message GlyphFont { 66 | string letter = 1; 67 | 68 | Metric adjustMetric = 2; 69 | 70 | map kerning = 3; 71 | } 72 | 73 | message GlyphImage { 74 | string letter = 1; 75 | 76 | Metric adjustMetric = 2; 77 | 78 | bytes buffer = 3; 79 | 80 | string fileName = 4; 81 | 82 | string fileType = 5; 83 | 84 | bool selected = 6; 85 | 86 | map kerning = 7; 87 | } 88 | 89 | message Layout { 90 | int32 padding = 1; 91 | 92 | int32 spacing = 2; 93 | 94 | int32 width = 3; 95 | 96 | int32 height = 4; 97 | 98 | bool auto = 5; 99 | 100 | bool fixedSize = 6; 101 | } 102 | 103 | message Shadow { 104 | string color = 1; 105 | 106 | int32 blur = 2; 107 | 108 | sint32 offsetX = 3; 109 | 110 | sint32 offsetY = 4; 111 | } 112 | 113 | message Style { 114 | Font font = 1; 115 | 116 | Fill fill = 2; 117 | 118 | bool useStroke = 3; 119 | 120 | Fill stroke = 4; 121 | 122 | bool useShadow = 5; 123 | 124 | Shadow shadow = 6; 125 | 126 | string bgColor = 7; 127 | } 128 | 129 | message Ui { 130 | string previewText = 1; 131 | } 132 | 133 | message Project { 134 | int64 id = 1; 135 | 136 | string name = 2; 137 | 138 | string text = 3; 139 | 140 | map glyphs = 4; 141 | 142 | repeated GlyphImage glyphImages = 5; 143 | 144 | Style style = 6; 145 | 146 | Layout layout = 7; 147 | 148 | Metric globalAdjustMetric = 8; 149 | 150 | Ui ui = 9; 151 | } -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/proto/toOriginBuffer.ts: -------------------------------------------------------------------------------- 1 | import { Project } from 'src/store' 2 | import { IProject } from './project' 3 | 4 | export default function toOriginBuffer(protoProject: IProject): Project { 5 | const project = protoProject as unknown as Project 6 | const map = new Map() 7 | 8 | // font 9 | if (protoProject?.style?.font?.fonts) { 10 | protoProject.style.font.fonts.forEach((fontResource, idx) => { 11 | if (fontResource.font) 12 | project.style.font.fonts[idx].font = fontResource.font.slice().buffer 13 | }) 14 | } 15 | 16 | // images 17 | if (protoProject?.glyphImages) { 18 | protoProject.glyphImages.forEach((glyphImage, idx) => { 19 | if (glyphImage.buffer) { 20 | project.glyphImages[idx].buffer = glyphImage.buffer.slice().buffer 21 | } 22 | if (glyphImage.kerning) { 23 | const imgKerning = new Map() 24 | Object.keys(glyphImage.kerning).forEach((key) => { 25 | if (glyphImage && glyphImage.kerning && glyphImage.kerning[key]) 26 | imgKerning.set(key, glyphImage.kerning[key] || 0) 27 | }) 28 | glyphImage.kerning = imgKerning as {} 29 | } 30 | }) 31 | } 32 | 33 | if (protoProject?.glyphs) { 34 | Object.keys(protoProject.glyphs).forEach((k) => { 35 | if (protoProject && protoProject.glyphs && protoProject.glyphs[k]) { 36 | const gl = protoProject.glyphs[k] 37 | const glyphKerning = new Map() 38 | if (gl && gl.kerning) { 39 | Object.keys(gl.kerning).forEach((key) => { 40 | if (gl.kerning) glyphKerning.set(key, gl.kerning[key] || 0) 41 | }) 42 | } 43 | map.set(k, { ...gl, kerning: glyphKerning }) 44 | } 45 | }) 46 | project.glyphs = map 47 | } 48 | 49 | // fill 50 | if (protoProject?.style?.fill?.patternTexture?.buffer) { 51 | project.style.fill.patternTexture.buffer = 52 | protoProject.style.fill.patternTexture.buffer.slice().buffer 53 | } 54 | 55 | // stroke 56 | if (protoProject?.style?.stroke?.patternTexture?.buffer) { 57 | project.style.stroke.patternTexture.buffer = 58 | protoProject.style.stroke.patternTexture.buffer.slice().buffer 59 | } 60 | 61 | return project 62 | } 63 | -------------------------------------------------------------------------------- /src/file/conversion/types/sbf/updateOldProject.ts: -------------------------------------------------------------------------------- 1 | import { IProject, oldProto, OldProto } from './proto/index' 2 | 3 | type OldKey = keyof OldProto 4 | 5 | const verions: OldKey[] = Object.keys(oldProto) 6 | .map((verion) => `${Number(verion)}` as unknown as OldKey) 7 | .sort() 8 | 9 | function updateOldProject(project: IProject, version: number): IProject { 10 | verions.forEach((v) => { 11 | if (version <= v && oldProto[v]) oldProto[v].updateToNext(project) 12 | }) 13 | return project 14 | } 15 | 16 | export default updateOldProject 17 | -------------------------------------------------------------------------------- /src/file/conversion/types/type.ts: -------------------------------------------------------------------------------- 1 | import { Project } from 'src/store' 2 | 3 | export type CheckFunction = (fileSource: unknown) => boolean 4 | 5 | export type DecodeProjectFunction = (buffer: unknown) => Partial 6 | 7 | export interface ConversionFileItem { 8 | ext: string 9 | check: CheckFunction 10 | decode: DecodeProjectFunction 11 | } 12 | -------------------------------------------------------------------------------- /src/file/export/exportFile.ts: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver' 2 | import JSZip from 'jszip' 3 | import { Project } from 'src/store' 4 | import drawPackCanvas from 'src/utils/drawPackCanvas' 5 | 6 | import toBmfInfo from './toBmfInfo' 7 | import { ConfigItem } from './type' 8 | 9 | export default function exportFile( 10 | project: Project, 11 | config: ConfigItem, 12 | fontName: string, 13 | fileName: string, 14 | ): void { 15 | const zip = new JSZip() 16 | const { packCanvas, glyphList, name, layout, ui } = project 17 | const bmfont = toBmfInfo(project, fontName) 18 | let text = config.getString(bmfont) 19 | const saveFileName = fileName || name 20 | 21 | if (name !== saveFileName) { 22 | text = text.replace(`file="${name}.png"`, `file="${saveFileName}.png"`) 23 | } 24 | 25 | zip.file(`${saveFileName}.${config.ext}`, text) 26 | 27 | const canvas = document.createElement('canvas') 28 | canvas.width = ui.width 29 | canvas.height = ui.height 30 | drawPackCanvas(canvas, packCanvas, glyphList, layout.padding) 31 | 32 | canvas.toBlob((blob) => { 33 | if (blob) zip.file(`${saveFileName}.png`, blob) 34 | zip 35 | .generateAsync({ type: 'blob' }) 36 | .then((content) => saveAs(content, `${saveFileName}.zip`)) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/file/export/index.ts: -------------------------------------------------------------------------------- 1 | import { ConfigItem } from './type' 2 | import text from './types/text' 3 | import xml from './types/xml' 4 | 5 | const list = [text, xml] 6 | 7 | export const configList: ConfigItem[] = [] 8 | 9 | list.forEach(({ type, exts, getString }) => { 10 | exts.forEach((ext) => { 11 | configList.push({ 12 | id: type + ext, 13 | ext, 14 | type, 15 | getString, 16 | }) 17 | }) 18 | }) 19 | 20 | export * from './type' 21 | export * from './toBmfInfo' 22 | export { default as toBmfInfo } from './toBmfInfo' 23 | export { default as exportFile } from './exportFile' 24 | export default configList 25 | -------------------------------------------------------------------------------- /src/file/export/type.ts: -------------------------------------------------------------------------------- 1 | export interface BMFontInfo extends Record { 2 | face: string 3 | size: number 4 | bold: number 5 | italic: number 6 | charset: string 7 | unicode: number 8 | stretchH: number 9 | smooth: number 10 | aa: number 11 | padding: number[] 12 | spacing: number[] 13 | } 14 | 15 | export interface BMFontCommon extends Record { 16 | lineHeight: number 17 | base: number 18 | scaleW: number 19 | scaleH: number 20 | pages: number 21 | packed: number 22 | } 23 | 24 | export interface BMFontPage extends Record { 25 | id: number 26 | file: string 27 | } 28 | 29 | export interface BMFontChar extends Record { 30 | letter: string 31 | id: number 32 | x: number 33 | y: number 34 | width: number 35 | height: number 36 | xoffset: number 37 | yoffset: number 38 | xadvance: number 39 | page: number 40 | chnl: number 41 | } 42 | 43 | export interface BMFontChars extends Record { 44 | count: number 45 | list: BMFontChar[] 46 | } 47 | 48 | export interface BMFontKerning extends Record { 49 | first: number 50 | second: number 51 | amount: number 52 | } 53 | 54 | export interface BMFontKernings extends Record { 55 | count: number 56 | list: BMFontKerning[] 57 | } 58 | 59 | export interface BMFont { 60 | info: BMFontInfo 61 | common: BMFontCommon 62 | pages: BMFontPage[] 63 | chars: BMFontChars 64 | kernings: BMFontKernings 65 | } 66 | 67 | export type OutputType = string 68 | 69 | export type OutputExt = string 70 | 71 | export type OutputExts = OutputExt[] 72 | 73 | export type FontToString = (fontInfo: BMFont) => string 74 | 75 | export interface Output { 76 | type: OutputType 77 | exts: OutputExts 78 | getString: FontToString 79 | } 80 | 81 | export interface ConfigItem extends Omit { 82 | id: string 83 | ext: string 84 | } 85 | -------------------------------------------------------------------------------- /src/file/export/types/text.ts: -------------------------------------------------------------------------------- 1 | import formatStr from 'src/utils/replaceVariables' 2 | 3 | import { FontToString, Output } from '../type' 4 | 5 | const TEMP_INFO = `info face="$face$" size=$size$ bold=$bold$ italic=$italic$ charset=$charset$ unicode=$unicode$ stretchH=$stretchH$ smooth=$smooth$ aa=$aa$ padding=$padding$ spacing=$spacing$\n` 6 | const TEMP_COMMON = `common lineHeight=$lineHeight$ base=$base$ scaleW=$scaleW$ scaleH=$scaleH$ pages=$pages$ packed=$packed$\n` 7 | const TEMP_PAGE = `page id=$id$ file="$file$"\n` 8 | const TEMP_CHARS = `chars count=$count$\n` 9 | const TEMP_CHAR = `char id=$id$ x=$x$ y=$y$ width=$width$ height=$height$ xoffset=$xoffset$ yoffset=$yoffset$ xadvance=$xadvance$ page=$page$ chnl=$chnl$\n` 10 | const TEMP_KERNINGS = `kernings count=$count$\n` 11 | const TEMP_KERNING = `kerning first=$first$ second=$second$ amount=$amount$\n` 12 | 13 | const type = 'TEXT' 14 | 15 | const exts = ['fnt', 'txt'] 16 | 17 | const getString: FontToString = (bmfont) => { 18 | const { info, common, pages, chars, kernings } = bmfont 19 | 20 | let str = '' 21 | 22 | str += formatStr(TEMP_INFO, { ...info, charset: info.charset || '""' }) 23 | 24 | str += formatStr(TEMP_COMMON, common) 25 | 26 | pages.forEach((p) => { 27 | str += formatStr(TEMP_PAGE, p) 28 | }) 29 | 30 | str += formatStr(TEMP_CHARS, chars) 31 | 32 | chars.list.forEach((char) => { 33 | str += formatStr(TEMP_CHAR, char) 34 | }) 35 | 36 | if (kernings.count) { 37 | str += formatStr(TEMP_KERNINGS, kernings) 38 | 39 | kernings.list.forEach((kerning) => { 40 | str += formatStr(TEMP_KERNING, kerning) 41 | }) 42 | } 43 | 44 | return str 45 | } 46 | 47 | const outputConfig: Output = { type, exts, getString } 48 | 49 | export default outputConfig 50 | -------------------------------------------------------------------------------- /src/file/export/types/type.ts.template: -------------------------------------------------------------------------------- 1 | import formatStr from 'src/utils/formatStr' 2 | import { Output, FontToString } from '../type' 3 | 4 | const type = 'TEXT' 5 | 6 | const exts = ['fnt', 'txt'] 7 | 8 | const getString: FontToString = (bmfont) => { 9 | const { info, common, pages, chars, kernings } = bmfont 10 | 11 | let str = '' 12 | 13 | return str 14 | } 15 | 16 | const outputConfig: Output = { type, exts, getString } 17 | 18 | export default outputConfig 19 | -------------------------------------------------------------------------------- /src/file/export/types/xml.ts: -------------------------------------------------------------------------------- 1 | import formatStr from 'src/utils/replaceVariables' 2 | 3 | import { FontToString, Output } from '../type' 4 | 5 | const TEMP_INFO = `` 6 | const TEMP_COMMON = `` 7 | const TEMP_PAGE = `` 8 | const TEMP_CHARS = `` 9 | const TEMP_CHAR = `` 10 | const TEMP_KERNINGS = `` 11 | const TEMP_KERNING = `` 12 | 13 | const type = 'XML' 14 | 15 | const exts = ['xml', 'fnt'] 16 | 17 | // http://www.angelcode.com/products/bmfont/doc/file_format.html 18 | const getString: FontToString = (bmfont) => { 19 | const { info, common, pages, chars, kernings } = bmfont 20 | 21 | const parser = new DOMParser() 22 | const xmlDOM = document.implementation.createDocument('', 'font', null) 23 | 24 | const infoDoc = parser.parseFromString(formatStr(TEMP_INFO, info), 'text/xml') 25 | xmlDOM.documentElement.appendChild(infoDoc.childNodes[0]) 26 | 27 | const commonDoc = parser.parseFromString( 28 | formatStr(TEMP_COMMON, common), 29 | 'text/xml', 30 | ) 31 | xmlDOM.documentElement.appendChild(commonDoc.childNodes[0]) 32 | 33 | const pagesDoc = parser.parseFromString( 34 | `${pages.map((p) => formatStr(TEMP_PAGE, p))}`, 35 | 'text/xml', 36 | ) 37 | xmlDOM.documentElement.appendChild(pagesDoc.childNodes[0]) 38 | 39 | const charsDoc = parser.parseFromString( 40 | formatStr(TEMP_CHARS, chars), 41 | 'text/xml', 42 | ) 43 | 44 | chars.list.forEach((char) => { 45 | const charDoc = parser.parseFromString( 46 | formatStr(TEMP_CHAR, char), 47 | 'text/xml', 48 | ) 49 | charsDoc.childNodes[0].appendChild(charDoc.childNodes[0]) 50 | charsDoc.childNodes[0].appendChild(new Comment(` ${char.letter} `)) 51 | }) 52 | 53 | xmlDOM.documentElement.appendChild(charsDoc.childNodes[0]) 54 | 55 | if (kernings.count) { 56 | const kerningsDoc = parser.parseFromString( 57 | formatStr(TEMP_KERNINGS, kernings), 58 | 'text/xml', 59 | ) 60 | 61 | kernings.list.forEach((kerning) => { 62 | const kerningDoc = parser.parseFromString( 63 | formatStr(TEMP_KERNING, kerning), 64 | 'text/xml', 65 | ) 66 | kerningsDoc.childNodes[0].appendChild(kerningDoc.childNodes[0]) 67 | }) 68 | 69 | xmlDOM.documentElement.appendChild(kerningsDoc.childNodes[0]) 70 | } 71 | 72 | return `${new XMLSerializer().serializeToString( 73 | xmlDOM, 74 | )}` 75 | } 76 | 77 | const outputConfig: Output = { type, exts, getString } 78 | 79 | export default outputConfig 80 | -------------------------------------------------------------------------------- /src/file/prefix.ts: -------------------------------------------------------------------------------- 1 | export const PREFIX_STR = 'SnowBambooBMF' 2 | const prefix = (): Uint8Array => 3 | new Uint8Array([...PREFIX_STR.split('').map((s) => s.charCodeAt(0)), 1, 1, 0]) 4 | 5 | export default prefix 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import * as Sentry from '@sentry/react' 4 | 5 | import App from './app/App' 6 | 7 | import * as serviceWorkerRegistration from './serviceWorkerRegistration' 8 | 9 | if (process.env.NODE_ENV === 'production' && process.env.REACT_APP_SENTRY_DSN) { 10 | Sentry.init({ 11 | dsn: process.env.REACT_APP_SENTRY_DSN, 12 | release: process.env.REACT_APP_SENTRY_RELEASE || 'test', 13 | integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()], 14 | tracesSampleRate: 1.0, 15 | environment: process.env.NODE_ENV, 16 | replaysSessionSampleRate: 0, 17 | replaysOnErrorSampleRate: 1.0, 18 | }) 19 | } 20 | 21 | createRoot(document.getElementById('root') as HTMLElement).render() 22 | 23 | // If you want your app to work offline and load faster, you can change 24 | // unregister() to register() below. Note this comes with some pitfalls. 25 | // Learn more about service workers: https://cra.link/PWA 26 | serviceWorkerRegistration.register({ 27 | onUpdate(registration) { 28 | const worker = registration.waiting 29 | if (!worker) return 30 | 31 | const channel = new MessageChannel() 32 | 33 | channel.port1.onmessage = () => { 34 | window.dispatchEvent(new CustomEvent('updateVerion', { detail: worker })) 35 | } 36 | 37 | worker.postMessage({ type: 'SKIP_WAITING' }, [channel.port2]) 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /src/store/base/fill.ts: -------------------------------------------------------------------------------- 1 | import { action, observable, makeObservable } from 'mobx' 2 | 3 | import Gradient from './gradient' 4 | import PatternTexture from './patternTexture' 5 | 6 | export enum FillType { 7 | SOLID, 8 | GRADIENT, 9 | IMAGE, 10 | } 11 | 12 | class Fill { 13 | type: FillType 14 | 15 | color: string 16 | 17 | gradient: Gradient 18 | 19 | patternTexture: PatternTexture 20 | 21 | constructor(fill: Partial = {}) { 22 | makeObservable(this, { 23 | type: observable, 24 | color: observable, 25 | gradient: observable, 26 | patternTexture: observable, 27 | setType: action.bound, 28 | setColor: action.bound, 29 | }) 30 | this.color = fill.color || '#000000' 31 | this.type = fill.type && FillType[fill.type] ? fill.type : 0 32 | this.gradient = new Gradient(fill.gradient) 33 | this.patternTexture = new PatternTexture(fill.patternTexture) 34 | } 35 | 36 | setType(type: FillType = 0): void { 37 | this.type = type 38 | } 39 | 40 | setColor(color = '#000000'): void { 41 | this.color = color 42 | } 43 | } 44 | 45 | export default Fill 46 | -------------------------------------------------------------------------------- /src/store/base/glyphBase.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable } from 'mobx' 2 | 3 | import Metric from './metric' 4 | 5 | export type GlyphType = 'text' | 'image' 6 | 7 | class GlyphBase { 8 | readonly type: GlyphType = 'text' 9 | 10 | letter = '' 11 | 12 | width = 0 13 | 14 | height = 0 15 | 16 | x = 0 17 | 18 | y = 0 19 | 20 | fontWidth = 0 21 | 22 | fontHeight = 0 23 | 24 | trimOffsetTop = 0 25 | 26 | trimOffsetLeft = 0 27 | 28 | adjustMetric: Metric 29 | 30 | kerning: Map = new Map() 31 | 32 | constructor(glyph: Partial = {}) { 33 | makeObservable(this, { 34 | letter: observable, 35 | width: observable, 36 | height: observable, 37 | x: observable, 38 | y: observable, 39 | fontWidth: observable, 40 | fontHeight: observable, 41 | trimOffsetTop: observable, 42 | trimOffsetLeft: observable, 43 | kerning: observable, 44 | adjustMetric: observable.ref, 45 | steKerning: action.bound, 46 | }) 47 | 48 | this.letter = glyph.letter || '' 49 | this.adjustMetric = new Metric(glyph.adjustMetric) 50 | 51 | if (glyph.kerning) { 52 | this.kerning = glyph.kerning 53 | } 54 | } 55 | 56 | steKerning(text: string, kerning: number) { 57 | this.kerning.set(text, kerning) 58 | } 59 | } 60 | 61 | export default GlyphBase 62 | -------------------------------------------------------------------------------- /src/store/base/glyphFont.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable } from 'mobx' 2 | import { GlyphItem } from 'src/utils/getFontGlyphs' 3 | 4 | import GlyphBase from './glyphBase' 5 | 6 | class GlyphFont extends GlyphBase { 7 | canvasX = 0 8 | 9 | canvasY = 0 10 | 11 | constructor(galyphFont: Partial = {}, glyphInfo?: GlyphItem) { 12 | super(galyphFont) 13 | makeObservable(this, { 14 | setGlyphInfo: action, 15 | }) 16 | if (glyphInfo) this.setGlyphInfo(glyphInfo) 17 | } 18 | 19 | setGlyphInfo(glyphInfo: GlyphItem): void { 20 | this.width = glyphInfo.width 21 | this.height = glyphInfo.height 22 | this.fontWidth = glyphInfo.fontWidth 23 | this.fontHeight = glyphInfo.fontHeight 24 | this.trimOffsetTop = glyphInfo.trimOffsetTop 25 | this.trimOffsetLeft = glyphInfo.trimOffsetLeft 26 | this.canvasX = glyphInfo.canvasX 27 | this.canvasY = glyphInfo.canvasY 28 | } 29 | } 30 | 31 | export default GlyphFont 32 | -------------------------------------------------------------------------------- /src/store/base/glyphImage.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable, runInAction } from 'mobx' 2 | import getTrimImageInfo from 'src/utils/getTrimImageInfo' 3 | 4 | import GlyphBase, { GlyphType } from './glyphBase' 5 | 6 | export interface FileInfo { 7 | letter?: string 8 | fileName: string 9 | fileType: string 10 | buffer: ArrayBuffer 11 | } 12 | 13 | class GlyphImage extends GlyphBase { 14 | readonly type: GlyphType = 'image' 15 | 16 | src = '' 17 | 18 | source: HTMLImageElement | HTMLCanvasElement | null = null 19 | 20 | buffer: ArrayBuffer | null = null 21 | 22 | fileName = '' 23 | 24 | fileType = '' 25 | 26 | selected = true 27 | 28 | constructor(glyphImage: Partial) { 29 | super(glyphImage) 30 | makeObservable(this, { 31 | src: observable, 32 | fileName: observable, 33 | fileType: observable, 34 | selected: observable, 35 | buffer: observable.ref, 36 | initImage: action.bound, 37 | setGlyph: action.bound, 38 | changeSelect: action.bound, 39 | }) 40 | this.letter = glyphImage.letter || '' 41 | this.fileName = glyphImage.fileName || '' 42 | this.fileType = glyphImage.fileType || '' 43 | this.buffer = glyphImage.buffer || null 44 | if (glyphImage.buffer) { 45 | this.src = URL.createObjectURL(new Blob([glyphImage.buffer])) 46 | this.initImage() 47 | } 48 | } 49 | 50 | initImage(): Promise { 51 | return new Promise((resolve) => { 52 | const image = new Image() 53 | image.onload = () => { 54 | runInAction(() => { 55 | const { naturalWidth, naturalHeight } = image 56 | this.fontWidth = naturalWidth 57 | this.fontHeight = naturalHeight 58 | 59 | const trimInfo = getTrimImageInfo(image) 60 | this.width = trimInfo.width 61 | this.height = trimInfo.height 62 | this.trimOffsetLeft = trimInfo.trimOffsetLeft 63 | this.trimOffsetTop = trimInfo.trimOffsetTop 64 | 65 | this.source = trimInfo.canvas 66 | resolve() 67 | }) 68 | } 69 | image.src = this.src 70 | }) 71 | } 72 | 73 | setGlyph(text: string): void { 74 | this.letter = text[0] || '' 75 | } 76 | 77 | changeSelect(isSelect: boolean): void { 78 | this.selected = isSelect 79 | } 80 | } 81 | 82 | export default GlyphImage 83 | -------------------------------------------------------------------------------- /src/store/base/gradient.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx' 2 | 3 | export enum GradientType { 4 | LINEAR, 5 | RADIAL, 6 | } 7 | 8 | export interface GradientColor { 9 | offset: number 10 | color: string 11 | } 12 | 13 | export interface GradientPaletteItem extends GradientColor { 14 | id: number 15 | } 16 | 17 | export interface GradientColorOption extends GradientColor { 18 | id?: number 19 | } 20 | 21 | class Gradient { 22 | type: GradientType = 0 23 | 24 | angle: number 25 | 26 | palette: GradientPaletteItem[] = [] 27 | 28 | constructor(gradient: Partial = {}) { 29 | makeObservable(this, { 30 | type: observable, 31 | angle: observable, 32 | palette: observable.shallow, 33 | ids: computed, 34 | nextId: computed, 35 | setType: action.bound, 36 | setAngle: action.bound, 37 | addColor: action.bound, 38 | updatePalette: action.bound, 39 | }) 40 | 41 | this.type = gradient.type && GradientType[gradient.type] ? gradient.type : 0 42 | this.angle = gradient.angle || 0 43 | if (gradient.palette) { 44 | gradient.palette.forEach((item) => { 45 | this.palette.push({ 46 | ...item, 47 | id: item.id || this.nextId, 48 | }) 49 | }) 50 | } else { 51 | this.addColor(0, 'rgba(255,255,255,1)') 52 | this.addColor(1) 53 | } 54 | } 55 | 56 | get ids(): number[] { 57 | return this.palette.map((color) => color.id) 58 | } 59 | 60 | get nextId(): number { 61 | if (this.ids.length === 0) return 1 62 | return Math.max(...this.ids) + 1 63 | } 64 | 65 | setType(type: GradientType): void { 66 | this.type = type 67 | } 68 | 69 | setAngle(angle: number): void { 70 | this.angle = angle 71 | } 72 | 73 | addColor(offset = 0, color = 'rgba(0,0,0,1)'): void { 74 | this.palette.push({ offset, color, id: this.nextId }) 75 | } 76 | 77 | updatePalette(palette: GradientPaletteItem[]): void { 78 | this.palette = palette 79 | } 80 | } 81 | 82 | export default Gradient 83 | -------------------------------------------------------------------------------- /src/store/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fill' 2 | export { default as FontStyleConfig } from './fill' 3 | 4 | export * from './font' 5 | export { default as Font } from './font' 6 | 7 | export * from './glyphBase' 8 | export { default as glyphBase } from './glyphBase' 9 | 10 | export { default as GlyphFont } from './glyphFont' 11 | 12 | export * from './glyphImage' 13 | export { default as GlyphImage } from './glyphImage' 14 | 15 | export * from './gradient' 16 | export { default as Gradient } from './gradient' 17 | 18 | export { default as Layout } from './layout' 19 | 20 | export { default as Metric } from './metric' 21 | 22 | export * from './patternTexture' 23 | export { default as PatternTexture } from './patternTexture' 24 | 25 | export { default as ShadowStyleConfig } from './shadow' 26 | 27 | export { default as StrokeStyleConfig } from './stroke' 28 | 29 | export { default as Style } from './style' 30 | 31 | export { default as ProjectUi } from './ui' 32 | -------------------------------------------------------------------------------- /src/store/base/layout.ts: -------------------------------------------------------------------------------- 1 | import { action, observable, makeObservable } from 'mobx' 2 | import use from 'src/utils/use' 3 | 4 | class Layout { 5 | padding = 1 6 | 7 | spacing = 1 8 | 9 | width = 512 10 | 11 | height = 512 12 | 13 | auto = true 14 | 15 | fixedSize = false 16 | 17 | constructor(layout: Partial = {}) { 18 | makeObservable(this, { 19 | padding: observable, 20 | spacing: observable, 21 | width: observable, 22 | height: observable, 23 | auto: observable, 24 | fixedSize: observable, 25 | setPadding: action.bound, 26 | setSpacing: action.bound, 27 | setWidth: action.bound, 28 | setHeight: action.bound, 29 | setAuto: action.bound, 30 | setFixedSize: action.bound, 31 | }) 32 | this.padding = use.num(layout.padding, 1) 33 | 34 | this.spacing = use.num(layout.spacing, 1) 35 | 36 | this.width = use.num(layout.width, 512) 37 | 38 | this.height = use.num(layout.height, 512) 39 | 40 | // Compatible with old files, default true. 41 | this.auto = layout.auto === false ? false : true 42 | 43 | this.fixedSize = !!layout.fixedSize 44 | } 45 | 46 | setPadding(padding: number): void { 47 | this.padding = padding 48 | } 49 | 50 | setSpacing(spacing: number): void { 51 | this.spacing = spacing 52 | } 53 | 54 | setWidth(width: number): void { 55 | this.width = width 56 | } 57 | 58 | setHeight(height: number): void { 59 | this.height = height 60 | } 61 | 62 | setAuto(auto: boolean): void { 63 | this.auto = auto 64 | } 65 | 66 | setFixedSize(fixedSize: boolean): void { 67 | this.fixedSize = fixedSize 68 | } 69 | } 70 | 71 | export default Layout 72 | -------------------------------------------------------------------------------- /src/store/base/metric.ts: -------------------------------------------------------------------------------- 1 | import { action, observable, makeObservable } from 'mobx' 2 | 3 | class Metric { 4 | xAdvance = 0 5 | 6 | xOffset = 0 7 | 8 | yOffset = 0 9 | 10 | constructor(metric: Partial = {}) { 11 | makeObservable(this, { 12 | xAdvance: observable, 13 | xOffset: observable, 14 | yOffset: observable, 15 | setXAdvance: action.bound, 16 | setXOffset: action.bound, 17 | setYOffset: action.bound, 18 | }) 19 | this.xAdvance = metric.xAdvance || 0 20 | this.xOffset = metric.xOffset || 0 21 | this.yOffset = metric.yOffset || 0 22 | } 23 | 24 | setXAdvance(xAdvance: number): void { 25 | this.xAdvance = xAdvance 26 | } 27 | 28 | setXOffset(xOffset: number): void { 29 | this.xOffset = xOffset 30 | } 31 | 32 | setYOffset(yOffset: number): void { 33 | this.yOffset = yOffset 34 | } 35 | } 36 | 37 | export default Metric 38 | -------------------------------------------------------------------------------- /src/store/base/patternTexture.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable, runInAction } from 'mobx' 2 | import base64ToArrayBuffer from 'src/utils/base64ToArrayBuffer' 3 | import use from 'src/utils/use' 4 | 5 | export type Repetition = 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' 6 | 7 | const DEFAULT_IMAGE = 8 | 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX////MzMw46qqDAAAADklEQVQI12Pgh8IPEAgAEeAD/Xk4HBcAAAAASUVORK5CYII=' 9 | 10 | class PatternTexture { 11 | buffer: ArrayBuffer = base64ToArrayBuffer(DEFAULT_IMAGE) 12 | 13 | image: HTMLImageElement | null = null 14 | 15 | src = '' 16 | 17 | repetition: Repetition = 'repeat' 18 | 19 | scale: number 20 | 21 | constructor(pt: Partial = {}) { 22 | makeObservable(this, { 23 | buffer: observable.ref, 24 | image: observable.ref, 25 | src: observable, 26 | repetition: observable, 27 | scale: observable, 28 | setImage: action.bound, 29 | setRepetition: action.bound, 30 | setScale: action.bound, 31 | }) 32 | this.scale = use.num(pt.scale, 1) 33 | this.repetition = pt.repetition || 'repeat' 34 | this.setImage(pt.buffer || this.buffer) 35 | } 36 | 37 | setImage(buffer: ArrayBuffer): void { 38 | const src = URL.createObjectURL(new Blob([buffer])) 39 | const img = new Image() 40 | img.onload = () => { 41 | runInAction(() => { 42 | this.buffer = buffer 43 | this.image = img 44 | this.src = src 45 | img.onload = null 46 | }) 47 | } 48 | img.src = src 49 | } 50 | 51 | setRepetition(repetition: Repetition): void { 52 | this.repetition = repetition 53 | } 54 | 55 | setScale(scale: number): void { 56 | this.scale = scale 57 | } 58 | } 59 | 60 | export default PatternTexture 61 | -------------------------------------------------------------------------------- /src/store/base/shadow.ts: -------------------------------------------------------------------------------- 1 | import { action, observable, makeObservable } from 'mobx' 2 | import use from 'src/utils/use' 3 | 4 | class Shadow { 5 | color: string 6 | 7 | blur = 1 8 | 9 | offsetX = 1 10 | 11 | offsetY = 1 12 | 13 | constructor(shadow: Partial = {}) { 14 | makeObservable(this, { 15 | color: observable, 16 | blur: observable, 17 | offsetX: observable, 18 | offsetY: observable, 19 | setColor: action.bound, 20 | setBlur: action.bound, 21 | setOffsetX: action.bound, 22 | setOffsetY: action.bound, 23 | setOffset: action.bound, 24 | }) 25 | this.color = shadow.color || '#000000' 26 | this.blur = use.num(shadow.blur, 1) 27 | this.offsetX = use.num(shadow.offsetX, 1) 28 | this.offsetY = use.num(shadow.offsetY, 1) 29 | } 30 | 31 | setColor(color: string): void { 32 | this.color = color 33 | } 34 | 35 | setBlur(blur: number): void { 36 | this.blur = blur 37 | } 38 | 39 | setOffsetX(offsetX: number): void { 40 | this.offsetX = offsetX 41 | } 42 | 43 | setOffsetY(offsetY: number): void { 44 | this.offsetY = offsetY 45 | } 46 | 47 | setOffset(offsetX: number, offsetY: number): void { 48 | this.offsetX = offsetX 49 | this.offsetY = offsetY 50 | } 51 | } 52 | 53 | export default Shadow 54 | -------------------------------------------------------------------------------- /src/store/base/stroke.ts: -------------------------------------------------------------------------------- 1 | import { action, makeObservable, observable } from 'mobx' 2 | import use from 'src/utils/use' 3 | 4 | import Fill from './fill' 5 | 6 | class Stroke extends Fill { 7 | width = 1 8 | 9 | /** 10 | * butt 默认。向线条的每个末端添加平直的边缘。 11 | * round 向线条的每个末端添加圆形线帽。 12 | * square 向线条的每个末端添加正方形线帽。 13 | */ 14 | lineCap: CanvasLineCap 15 | 16 | /** 17 | * bevel 创建斜角。 18 | * round 创建圆角。 19 | * miter 默认。创建尖角。 20 | */ 21 | lineJoin: CanvasLineJoin 22 | 23 | /** 24 | * Stroke Type 25 | * 26 | * 0 outer stroke 27 | * 1 middle stroke 28 | * 2 inner stroke 29 | */ 30 | strokeType: 0 | 1 | 2 31 | 32 | constructor(stroke: Partial = {}) { 33 | super(stroke) 34 | makeObservable(this, { 35 | width: observable, 36 | lineCap: observable, 37 | lineJoin: observable, 38 | strokeType: observable, 39 | setWidth: action.bound, 40 | setLineCap: action.bound, 41 | setLineJoin: action.bound, 42 | setStrokeType: action.bound, 43 | }) 44 | this.width = use.num(stroke.width, 1) 45 | this.lineCap = stroke.lineCap || 'round' 46 | this.lineJoin = stroke.lineJoin || 'round' 47 | this.strokeType = stroke.strokeType || 0 48 | } 49 | 50 | setWidth(width: number): void { 51 | this.width = width 52 | } 53 | 54 | setLineCap(lineCap: CanvasLineCap): void { 55 | this.lineCap = lineCap 56 | } 57 | 58 | setLineJoin(lineJoin: CanvasLineJoin): void { 59 | this.lineJoin = lineJoin 60 | } 61 | 62 | setStrokeType(strokeType: 0 | 1 | 2): void { 63 | this.strokeType = strokeType 64 | } 65 | } 66 | 67 | export default Stroke 68 | -------------------------------------------------------------------------------- /src/store/base/style.ts: -------------------------------------------------------------------------------- 1 | import { action, observable, makeObservable } from 'mobx' 2 | 3 | import Font from './font' 4 | import Fill from './fill' 5 | import Stroke from './stroke' 6 | import Shadow from './shadow' 7 | 8 | class Style { 9 | readonly font: Font 10 | 11 | readonly fill: Fill 12 | 13 | useStroke: boolean 14 | 15 | readonly stroke: Stroke 16 | 17 | useShadow: boolean 18 | 19 | readonly shadow: Shadow 20 | 21 | bgColor = 'rgba(0,0,0,0)' 22 | 23 | constructor(style: Partial