├── .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 |
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 | [](https://snowb.org/)
13 |
--------------------------------------------------------------------------------
/README_ZH.md:
--------------------------------------------------------------------------------
1 |
2 |
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 | [](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 |
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 |
42 | New
43 |
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 |
56 | Open
57 |
64 |
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 |
53 | Save
54 |
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 |
76 | {`${Math.round((showPreview ? previewScale : scale) * 1000) / 10}%`}
77 |
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 |
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 |
61 | Reload
62 |
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 | setRepetition(e.target.value as Repetition)}
47 | displayEmpty
48 | fullWidth
49 | variant='standard'
50 | >
51 | Repeat
52 | Repeat-X
53 | Repeat-Y
54 | No Repeat
55 |
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