diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..e1a08a3 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,41 @@ +name: Pages + +on: + push: + branches: [main] + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + pages: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install pip --upgrade + python -m pip install -r requirements.txt + - name: Build + run: python -m tools.build + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./www + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..22e0cd8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install pip --upgrade + python -m pip install -r requirements.txt + - name: Build + run: python -m tools.build + - name: Release + uses: softprops/action-gh-release@v2 + with: + files: ./build/releases/*.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be4413e --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ + +# www +www/fonts/ diff --git a/README.md b/README.md index e1902c3..a2b126c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Specifications

- Bitroot Typeface Preview + Bitroot Typeface Preview

The characters are packed into a **tiny 5×10-pixel area**, **baseline 7 pixels down from the top**! **Capital letters** use up the full **7-pixel height**, meanwhile regular **lowercases** occupy a **5-pixel x-height** measured from the baseline upward. diff --git a/bitroot.ttf b/bitroot.ttf deleted file mode 100644 index bf5bc4b..0000000 Binary files a/bitroot.ttf and /dev/null differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c1b2317 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pixel-font-builder==0.0.36 +pixel-font-knife==0.0.13 diff --git a/preview.png b/src/preview.png similarity index 100% rename from preview.png rename to src/preview.png diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/build.py b/tools/build.py new file mode 100644 index 0000000..9e0afbe --- /dev/null +++ b/tools/build.py @@ -0,0 +1,206 @@ +import shutil +import zipfile +from datetime import datetime +from pathlib import Path + +import png +from pixel_font_builder import Glyph, FontBuilder, WeightName, SerifStyle, SlantStyle, WidthStyle, opentype +from pixel_font_knife.mono_bitmap import MonoBitmap + +from tools import path_define + + +def _load_src_png(file_path: Path) -> MonoBitmap: + """ + The source image uses white as pixel, + so we need to customize the loading to convert white to 1 and others to 0 + :param file_path: '.png' file path + :return: MonoBitmap + """ + width, height, pixels, _ = png.Reader(filename=file_path).read() + bitmap = MonoBitmap() + bitmap.width = width + bitmap.height = height + for pixels_row in pixels: + bitmap_row = [] + for i in range(0, width * 4, 4): + red = pixels_row[i] + green = pixels_row[i + 1] + blue = pixels_row[i + 2] + alpha = pixels_row[i + 3] + bitmap_row.append(1 if (red, green, blue, alpha) == (255, 255, 255, 255) else 0) + bitmap.append(bitmap_row) + return bitmap + + +def main(): + # ------------- + # source glyphs + # ------------- + + src_bitmap = _load_src_png(path_define.src_dir.joinpath('font-sheet.png')) + src_alphabet = [ + 'ABCDEFGHIJKLMNOPQR', + 'STUVWXYZ', + 'abcdefghijklmnopqr', + 'stuvwxyz', + '0123456789', + '$₵€£¥¤+-*/÷=%"\'#@&', + '_(),.;:¿?¡!\\{}<>[]', + '§¶µ´^~©®™', + ] + + # ------------------ + # Crop Mono bitmaps + # ------------------ + + bitmap_mapping = {} # code_point -> bitmap + for row, line in enumerate(src_alphabet): + for col, c in enumerate(line): + code_point = ord(c) + bitmap = src_bitmap.crop(7 * col + 2, 12 * row + 2, 5, 10) + bitmap_mapping[code_point] = bitmap + bitmap_mapping[-1] = src_bitmap.crop(7 * 8 + 2, 12 * 0 + 2, 5, 10) # notdef + bitmap_mapping[32] = src_bitmap.crop(7 * 8 + 2, 12 * 1 + 2, 5, 10) # space + + # ----------------------------------------------------- + # Create glyph mapping, this is what the font file need + # Keep glyphs array is sorted as Unicode + # ------------------------------------------------------ + + character_mapping = {} # code_point -> glyph_name + glyphs = [] + for code_point, bitmap in sorted(bitmap_mapping.items()): + if code_point == -1: + glyph_name = '.notdef' + c = '.notdef' + else: + glyph_name = f'u{code_point:04X}' + c = chr(code_point) + character_mapping[code_point] = glyph_name + + glyphs.append(Glyph( + name=glyph_name, + horizontal_offset=(0, -3), + advance_width=6, # Right 1px as char-gap + vertical_offset=(-3, 1), + advance_height=11, # Top 1px as line-gap + bitmap=bitmap.data, + )) + + # Print and check if the bitmap is correct + print('--------------------\n') + print(f'glyph: {glyph_name}, char: {c}\n') + print(bitmap.draw(end='*')) + + # ------------ + # Fill in font + # ------------ + + builder = FontBuilder() + builder.font_metric.font_size = 11 + builder.font_metric.horizontal_layout.ascent = 8 # Top 1px as line-gap + builder.font_metric.horizontal_layout.descent = -3 + builder.font_metric.vertical_layout.ascent = 3 + builder.font_metric.vertical_layout.descent = -3 + builder.font_metric.x_height = 5 + builder.font_metric.cap_height = 7 + builder.font_metric.underline_position = -3 + builder.font_metric.underline_thickness = 1 + builder.font_metric.strikeout_position = 3 + builder.font_metric.strikeout_thickness = 1 + + builder.meta_info.version = '0.0.0' # TODO <-- modify this each release + builder.meta_info.created_time = datetime.fromisoformat('2025-06-12T00:00:00Z') # TODO <-- modify this each release + builder.meta_info.modified_time = builder.meta_info.created_time + builder.meta_info.family_name = 'Bitroot' + builder.meta_info.weight_name = WeightName.REGULAR + builder.meta_info.serif_style = SerifStyle.SERIF + builder.meta_info.slant_style = SlantStyle.NORMAL + builder.meta_info.width_style = WidthStyle.MONOSPACED + builder.meta_info.manufacturer = 'Rio' + builder.meta_info.designer = 'Rio' + builder.meta_info.description = 'Sweet fonts to leave your worries behind you' + builder.meta_info.copyright_info = 'Copyright (c) 2025, Rio (engineer@disroot.org)' + builder.meta_info.license_info = 'SIL Open Font License 1.1' + builder.meta_info.vendor_url = 'https://github.com/serialexperimentsrio/bitroot' + builder.meta_info.designer_url = 'https://github.com/serialexperimentsrio' + builder.meta_info.license_url = 'https://github.com/serialexperimentsrio/bitroot/blob/main/LICENSE.txt' + builder.meta_info.sample_text = 'Jackfruit groves with zinc-fed topsoil maximize bounty' + + builder.character_mapping.update(character_mapping) + builder.glyphs.extend(glyphs) + + # ------------------ + # Clean 'build' dir + # ------------------ + + if path_define.build_dir.exists(): + shutil.rmtree(path_define.build_dir) + path_define.outputs_dir.mkdir(parents=True) + path_define.releases_dir.mkdir(parents=True) + + # ------------------ + # Build normal fonts + # ------------------ + + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot.otf')) + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot.otf.woff'), flavor=opentype.Flavor.WOFF) + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot.otf.woff2'), flavor=opentype.Flavor.WOFF2) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot.ttf')) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot.ttf.woff'), flavor=opentype.Flavor.WOFF) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot.ttf.woff2'), flavor=opentype.Flavor.WOFF2) + builder.save_bdf(path_define.outputs_dir.joinpath('Bitroot.bdf')) + builder.save_pcf(path_define.outputs_dir.joinpath('Bitroot.pcf')) + + # ---------------------- + # Build square dot fonts + # ---------------------- + + builder.meta_info.family_name = 'Bitroot SquareDot' + builder.opentype_config.outlines_painter = opentype.SquareDotOutlinesPainter() + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot-SquareDot.otf')) + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot-SquareDot.otf.woff'), flavor=opentype.Flavor.WOFF) + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot-SquareDot.otf.woff2'), flavor=opentype.Flavor.WOFF2) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot-SquareDot.ttf')) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot-SquareDot.ttf.woff'), flavor=opentype.Flavor.WOFF) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot-SquareDot.ttf.woff2'), flavor=opentype.Flavor.WOFF2) + + # ---------------------- + # Build circle dot fonts + # ---------------------- + + builder.meta_info.family_name = 'Bitroot CircleDot' + builder.opentype_config.outlines_painter = opentype.CircleDotOutlinesPainter() + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot-CircleDot.otf')) + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot-CircleDot.otf.woff'), flavor=opentype.Flavor.WOFF) + builder.save_otf(path_define.outputs_dir.joinpath('Bitroot-CircleDot.otf.woff2'), flavor=opentype.Flavor.WOFF2) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot-CircleDot.ttf')) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot-CircleDot.ttf.woff'), flavor=opentype.Flavor.WOFF) + builder.save_ttf(path_define.outputs_dir.joinpath('Bitroot-CircleDot.ttf.woff2'), flavor=opentype.Flavor.WOFF2) + + # ------------------- + # Pack release '.zip' + # ------------------- + + with zipfile.ZipFile(path_define.releases_dir.joinpath(f'bitroot-v{builder.meta_info.version}.zip'), 'w') as file: + file.write(path_define.project_root_dir.joinpath('LICENSE.txt'), 'LICENSE.txt') + for font_file_path in path_define.outputs_dir.iterdir(): + if font_file_path.suffix in ('.otf', '.ttf', '.woff', '.woff2', '.bdf', '.pcf'): + file.write(font_file_path, font_file_path.name) + + # ------------------------------------- + # Copy '.otf.woff2' to 'www/fonts' dir + # ------------------------------------- + + if path_define.www_fonts_dir.exists(): + shutil.rmtree(path_define.www_fonts_dir) + path_define.www_fonts_dir.mkdir(parents=True) + + for font_file_path in path_define.outputs_dir.iterdir(): + if font_file_path.name.endswith('.otf.woff2'): + shutil.copyfile(font_file_path, path_define.www_fonts_dir.joinpath(font_file_path.name)) + + +if __name__ == '__main__': + main() diff --git a/tools/path_define.py b/tools/path_define.py new file mode 100644 index 0000000..a6773a4 --- /dev/null +++ b/tools/path_define.py @@ -0,0 +1,12 @@ +from pathlib import Path + +project_root_dir = Path(__file__).parent.joinpath('..').resolve() + +src_dir = project_root_dir.joinpath('src') + +build_dir = project_root_dir.joinpath('build') +outputs_dir = build_dir.joinpath('outputs') +releases_dir = build_dir.joinpath('releases') + +www_dir = project_root_dir.joinpath('www') +www_fonts_dir = www_dir.joinpath('fonts') diff --git a/www/css/index.css b/www/css/index.css new file mode 100644 index 0000000..e7b158e --- /dev/null +++ b/www/css/index.css @@ -0,0 +1,65 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +@font-face { + font-family: Bitroot; + src: url("../fonts/Bitroot.otf.woff2"); +} + +@font-face { + font-family: Bitroot-SquareDot; + src: url("../fonts/Bitroot-SquareDot.otf.woff2"); +} + +@font-face { + font-family: Bitroot-CircleDot; + src: url("../fonts/Bitroot-CircleDot.otf.woff2"); +} + +body { + font-family: sans-serif; +} + +.header { + .title { + margin-top: 16px; + margin-bottom: 16px; + text-align: center; + font-family: Bitroot, sans-serif; + font-size: 66px; + font-weight: normal; + } +} + +.main { + padding: 16px; + + .options { + margin-bottom: 16px; + text-align: center; + + .option { + margin-left: 12px; + margin-right: 12px; + font-size: 16px; + font-weight: bold; + } + } + + .input-box { + width: 100%; + height: 400px; + padding: 16px; + resize: vertical; + font-family: Bitroot, sans-serif; + font-size: 44px; + } +} + +.footer { + text-align: center; + font-size: 16px; +} diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..f51cb13 --- /dev/null +++ b/www/index.html @@ -0,0 +1,56 @@ + + + + + + Bitroot + + + + +
+

Bitroot

+
+ +
+
+ + + +
+ + +
+ + + + + + + diff --git a/www/js/index.js b/www/js/index.js new file mode 100644 index 0000000..a09abf7 --- /dev/null +++ b/www/js/index.js @@ -0,0 +1,13 @@ + +window.onFontStyleChange = fontStyle => { + let fontFamily + if (fontStyle === 'SquareDot') { + fontFamily = 'Bitroot-SquareDot, sans-serif' + } else if (fontStyle === 'CircleDot') { + fontFamily = 'Bitroot-CircleDot, sans-serif' + } else { + fontFamily = 'Bitroot, sans-serif' + } + document.getElementById('title').style.fontFamily = fontFamily + document.getElementById('input-box').style.fontFamily = fontFamily +}