diff --git a/2D/js/main.js b/2D/js/main.js index 0371aaa..275a96c 100644 --- a/2D/js/main.js +++ b/2D/js/main.js @@ -24,12 +24,12 @@ class MainApp { duplicate: new Image(), trash: new Image(), }; - const as = '../assets/'; - this.icons.flip.src = as + 'ic_flip.png'; - this.icons.rotateLeft.src = as + 'ic_rotate_left.png'; - this.icons.rotateRight.src = as + 'ic_rotate_right.png'; - this.icons.duplicate.src = as + 'ic_duplicate.png'; - this.icons.trash.src = as + 'ic_trash.png'; + const as = '../assets/icons/'; + this.icons.flip.src = as + 'flip.png'; + this.icons.rotateLeft.src = as + 'rotate_left.png'; + this.icons.rotateRight.src = as + 'rotate_right.png'; + this.icons.duplicate.src = as + 'duplicate.png'; + this.icons.trash.src = as + 'trash.png'; this.gridBoard = new GridBoard(this.canvas, this.gridSize, this.rows, this.cols); this.guiController = new GUIController(this); this.toolbar = new Toolbar(this); diff --git a/2D/js/popup/grid.js b/2D/js/popup/grid.js index 0f23a75..b0d5935 100644 --- a/2D/js/popup/grid.js +++ b/2D/js/popup/grid.js @@ -36,7 +36,7 @@ export function showGridPopup(toolbar) { const assetsReady = Promise.all( iconRows.map((row) => - loadImage(`../assets/ic_${row.icon}.png`).then((image) => { + loadImage(`../assets/icons/${row.icon}.png`).then((image) => { if (image) iconMap.set(row.icon, image); }) ) diff --git a/2D/js/popup/setting.js b/2D/js/popup/setting.js index 34f750e..059f306 100644 --- a/2D/js/popup/setting.js +++ b/2D/js/popup/setting.js @@ -36,7 +36,7 @@ export function showSettingsPopup(toolbar) { const assetsReady = Promise.all( iconRows.map((row) => - loadImage(`../assets/ic_${row.icon}.png`).then((image) => { + loadImage(`../assets/icons/${row.icon}.png`).then((image) => { if (image) iconMap.set(row.icon, image); }) ) diff --git a/2D/js/popup/solve.js b/2D/js/popup/solve.js index e73df5f..0d446f7 100644 --- a/2D/js/popup/solve.js +++ b/2D/js/popup/solve.js @@ -75,7 +75,7 @@ export function showSolvePopup(toolbar) { const assetsReady = Promise.all( iconRows.map((row) => - loadImage(`../assets/ic_${row.icon}.png`).then((image) => { + loadImage(`../assets/icons/${row.icon}.png`).then((image) => { if (image) iconMap.set(row.icon, image); }) ) diff --git a/2D/js/popup/tutorial.js b/2D/js/popup/tutorial.js index 6a45ee7..2686b3e 100644 --- a/2D/js/popup/tutorial.js +++ b/2D/js/popup/tutorial.js @@ -85,8 +85,8 @@ export function showTutorialPopup(toolbar) { const iconMap = new Map(); const allIconPaths = [ - ...rows.filter((row) => row.icon).map((row) => `ic_${row.icon}.png`), - ...subIcons.map((icon) => `ic_${icon.path}.png`), + ...rows.filter((row) => row.icon).map((row) => `icons/${row.icon}.png`), + ...subIcons.map((icon) => `icons/${icon.path}.png`), ]; const uniqueIconPaths = [...new Set(allIconPaths)]; @@ -160,7 +160,7 @@ export function showTutorialPopup(toolbar) { } if (row.icon) { - const icon = iconMap.get(`ic_${row.icon}.png`); + const icon = iconMap.get(`icons/${row.icon}.png`); if (icon) { ctx.drawImage(icon, popup.width - 64, y - 14, 50, 50); } @@ -193,7 +193,7 @@ export function showTutorialPopup(toolbar) { }); subIcons.forEach((subIcon) => { - const subIconImage = iconMap.get(`ic_${subIcon.path}.png`); + const subIconImage = iconMap.get(`icons/${subIcon.path}.png`); if (subIconImage) { ctx.drawImage(subIconImage, subIcon.x, subIcon.y, 25, 25); } diff --git a/2D/js/toolbar.js b/2D/js/toolbar.js index 95e4b7f..6bca0bc 100644 --- a/2D/js/toolbar.js +++ b/2D/js/toolbar.js @@ -50,7 +50,7 @@ export class Toolbar { } preloadIcons() { - this.homeImage = this.preloadImage('../assets/ic_home.png'); + this.homeImage = this.preloadImage('../assets/icons/home.png'); this.buttons.forEach((button) => { button.image = this.preloadImage(button.icon); }); @@ -60,31 +60,31 @@ export class Toolbar { return [ { name: 'Create Polyomino', - icon: '../assets/ic_plus.png', + icon: '../assets/icons/plus.png', action: () => this.togglePopup('polyomino'), description: 'To select available Polyomino blocks.\nAdd directly to canvas.', }, { name: 'Grid Settings', - icon: '../assets/ic_table.png', + icon: '../assets/icons/table.png', action: () => this.togglePopup('grid'), description: 'To change the grid settings.\nAdjust rows, columns, and size.', }, { name: 'Solving Polyomino', - icon: '../assets/ic_solving.png', + icon: '../assets/icons/solving.png', action: () => this.togglePopup('solve'), description: 'To solve the polyomino puzzle.\nUse different algorithms to solve.', }, { name: 'Tutorial', - icon: '../assets/ic_question.png', + icon: '../assets/icons/question.png', action: () => this.togglePopup('tutorial'), description: 'To view the tutorial.\nLearn how to use the application.', }, { name: 'Settings', - icon: '../assets/ic_setting.png', + icon: '../assets/icons/setting.png', action: () => this.togglePopup('settings'), description: 'To adjust application settings.\nChange colors, tooltips, and more.', }, @@ -408,7 +408,7 @@ export class Toolbar { } const closeIcon = new Image(); - closeIcon.src = '../assets/ic_close.png'; + closeIcon.src = '../assets/icons/close.png'; closeIcon.style.position = 'fixed'; Object.assign(closeIcon.style, { cursor: 'pointer', diff --git a/2D/style.css b/2D/style.css index 91d13f6..0b084a9 100644 --- a/2D/style.css +++ b/2D/style.css @@ -1,7 +1,7 @@ @charset "UTF-8"; @font-face { font-family: 'Pixellari'; - src: url('../assets/Pixellari.ttf') format('truetype'); + src: url('../assets/fonts/Pixellari.ttf') format('truetype'); } * { font-family: 'Pixellari', sans-serif; diff --git a/3D/js/constants.js b/3D/js/constants.js index 9a5155f..ad6da7b 100644 --- a/3D/js/constants.js +++ b/3D/js/constants.js @@ -382,43 +382,43 @@ export const TUTORIAL_POPUP_ROWS = [ underline: true, icon: 'plus', description: - "Click this icon to open a menu.\n\nSteps to create a Polycube:\n\n1. Enter the desired number of cubes in the 'N° cubes' input field.\n2. Enter the position coordinates (x, y, z) for the Polycube.\n3. Use the 'Previous' [ic_arrow_left] and 'Next' [ic_arrow_right] buttons to navigate through highlighted cubes.\n4. Click 'Select' [ic_select] to add the highlighted cube to your Polycube.\n5. Click 'Undo' [ic_reset] to remove the last added cube.\n6. Click 'Create' to finalize and create the Polycube.\n7. Click 'Clear' to reset the Polycube creation and start over.\n8. Use the 'Info' button to display current Polycube information.", + "Click this icon to open a menu.\n\nSteps to create a Polycube:\n\n1. Enter the desired number of cubes in the 'N° cubes' input field.\n2. Enter the position coordinates (x, y, z) for the Polycube.\n3. Use the 'Previous' [icons/arrow_left] and 'Next' [icons/arrow_right] buttons to navigate through highlighted cubes.\n4. Click 'Select' [icons/select] to add the highlighted cube to your Polycube.\n5. Click 'Undo' [icons/reset] to remove the last added cube.\n6. Click 'Create' to finalize and create the Polycube.\n7. Click 'Clear' to reset the Polycube creation and start over.\n8. Use the 'Info' button to display current Polycube information.", }, { label: '2) Manipulate the Polycubes :', underline: true, description: - "Click to any Polycube on the scene to select it.\n\nAfter selecting a Polycube, you can:\n\n1. Use the [ic_left_click] 'Left Mouse Button' to rotate the Polycube.\n2. Use the [ic_right_click] 'Right Mouse Button' to move the Polycube.\n\nCustom visible views of Polycubes with DAT.GUI :\n\n1. Click 'polycubeVisible' to show or hide the selected Polycube on the scene.\n2. Click 'allCubesVisible' to show or hide all Polycubes on the scene.", + "Click to any Polycube on the scene to select it.\n\nAfter selecting a Polycube, you can:\n\n1. Use the [icons/left_click] 'Left Mouse Button' to rotate the Polycube.\n2. Use the [icons/right_click] 'Right Mouse Button' to move the Polycube.\n\nCustom visible views of Polycubes with DAT.GUI :\n\n1. Click 'polycubeVisible' to show or hide the selected Polycube on the scene.\n2. Click 'allCubesVisible' to show or hide all Polycubes on the scene.", }, { label: '3) Grid Board Settings', underline: true, icon: 'table', description: - "This menu lets you delete and or create the new grid.\n\n1. Click [ic_trash] 'Delete current grid' to delete the grid.\n2. Enter (x, y, z) sizes for the grid. Click [ic_draw] to create a new grid.", + "This menu lets you delete and or create the new grid.\n\n1. Click [icons/trash] 'Delete current grid' to delete the grid.\n2. Enter (x, y, z) sizes for the grid. Click [icons/draw] to create a new grid.", }, { label: '4) Manipulate the Grid Board :', underline: true, description: - "You can only control the grid through camera perspective with Orbit Controls :\n\n1. Use the [ic_left_click] 'Left Mouse Button' to rotate the grid.\n2. Use the [ic_right_click] 'Right Mouse Button' to move the grid.\n3. Use the 'Center Mouse Button' to zoom the grid.\n\nCustom visible views of grid with DAT.GUI :\n\n1. Click 'showInnerGrid' to show the grid's inner grid.\n2. Click 'showOuterGrid' to show the grid's outer grid. But this is already shown by default.", + "You can only control the grid through camera perspective with Orbit Controls :\n\n1. Use the [icons/left_click] 'Left Mouse Button' to rotate the grid.\n2. Use the [icons/right_click] 'Right Mouse Button' to move the grid.\n3. Use the 'Center Mouse Button' to zoom the grid.\n\nCustom visible views of grid with DAT.GUI :\n\n1. Click 'showInnerGrid' to show the grid's inner grid.\n2. Click 'showOuterGrid' to show the grid's outer grid. But this is already shown by default.", }, { label: '5) Use Settings :', underline: true, icon: 'setting', description: - "In this menu, you can:\n\n1. Click [ic_trash] 'Delete Selected Polycube' to delete the Polycube that you are currently selected.", + "In this menu, you can:\n\n1. Click [icons/trash] 'Delete Selected Polycube' to delete the Polycube that you are currently selected.", }, ]; export const TUTORIAL_ICON_MAP = { - '[ic_arrow_left]': '../assets/ic_arrow_left.png', - '[ic_arrow_right]': '../assets/ic_arrow_right.png', - '[ic_select]': '../assets/ic_select.png', - '[ic_reset]': '../assets/ic_reset.png', - '[ic_left_click]': '../assets/ic_left_click.png', - '[ic_right_click]': '../assets/ic_right_click.png', - '[ic_trash]': '../assets/ic_trash.png', - '[ic_draw]': '../assets/ic_draw.png', + '[icons/arrow_left]': '../assets/icons/arrow_left.png', + '[icons/arrow_right]': '../assets/icons/arrow_right.png', + '[icons/select]': '../assets/icons/select.png', + '[icons/reset]': '../assets/icons/reset.png', + '[icons/left_click]': '../assets/icons/left_click.png', + '[icons/right_click]': '../assets/icons/right_click.png', + '[icons/trash]': '../assets/icons/trash.png', + '[icons/draw]': '../assets/icons/draw.png', }; diff --git a/3D/js/popup/cube/view.js b/3D/js/popup/cube/view.js index e814ea5..6406a70 100644 --- a/3D/js/popup/cube/view.js +++ b/3D/js/popup/cube/view.js @@ -10,7 +10,10 @@ export class CubePopupView { } render({ onNChange, onNavigate, onInfo, onClear, onCreate }) { - this.popupContainer = this.toolbar.createPopupContainer('cubePopup', this.toolbar.buttons[0].name); + this.popupContainer = this.toolbar.createPopupContainer( + 'cubePopup', + this.toolbar.buttons[0].name + ); const popup = this.popupContainer.querySelector('canvas'); const ctx = popup.getContext('2d'); @@ -19,7 +22,10 @@ export class CubePopupView { this.renderFormFields(onNChange); const previewCanvas = this.createPreviewCanvas(); - this.createTextZone(popup.width - CUBE_POPUP_CONSTANTS.TEXT_ZONE.WIDTH_OFFSET, 'Polycube Info...'); + this.createTextZone( + popup.width - CUBE_POPUP_CONSTANTS.TEXT_ZONE.WIDTH_OFFSET, + 'Polycube Info...' + ); this.createNavigationButtons(onNavigate); this.createActionButtons({ onInfo, onClear, onCreate }); @@ -38,7 +44,9 @@ export class CubePopupView { FORM_FIELD_LAYOUT.forEach((field, index) => { const rowY = - field.row === 1 ? startY - inputOffset + rowOffset : startY + rowSpacing - inputOffset + rowOffset; + field.row === 1 + ? startY - inputOffset + rowOffset + : startY + rowSpacing - inputOffset + rowOffset; this.createLabel(field.label, field.labelX, rowY); const input = this.createInputField(field.inputX, rowY, field.defaultValue); @@ -147,7 +155,7 @@ export class CubePopupView { buttonContainer.appendChild(label); const icon = document.createElement('img'); - icon.src = `../assets/ic_${item.icon}.png`; + icon.src = `../assets/icons/${item.icon}.png`; icon.width = CUBE_POPUP_CONSTANTS.NAV.ICON_SIZE; icon.height = CUBE_POPUP_CONSTANTS.NAV.ICON_SIZE; icon.style.cursor = 'pointer'; diff --git a/3D/js/popup/grid.js b/3D/js/popup/grid.js index d3390c6..5343a85 100644 --- a/3D/js/popup/grid.js +++ b/3D/js/popup/grid.js @@ -13,7 +13,10 @@ export class GridPopup { } render() { - this.popupContainer = this.toolbar.createPopupContainer('gridPopup', this.toolbar.buttons[1].name); + this.popupContainer = this.toolbar.createPopupContainer( + 'gridPopup', + this.toolbar.buttons[1].name + ); this.popup = this.popupContainer.querySelector('canvas'); const ctx = this.popup.getContext('2d'); ctx.fillStyle = GRID_POPUP_CONSTANTS.BACKGROUND_COLOR; @@ -94,7 +97,7 @@ export class GridPopup { createIconButton(y, iconName, onClick) { const { LAYOUT } = GRID_POPUP_CONSTANTS; const iconButton = document.createElement('img'); - iconButton.src = `../assets/ic_${iconName}.png`; + iconButton.src = `../assets/icons/${iconName}.png`; applyStyles(iconButton, { position: 'absolute', left: `${this.popup.width - LAYOUT.ICON_LEFT_OFFSET}px`, diff --git a/3D/js/popup/setting.js b/3D/js/popup/setting.js index 3e677b5..cb3e11d 100644 --- a/3D/js/popup/setting.js +++ b/3D/js/popup/setting.js @@ -63,7 +63,7 @@ export class SettingsPopup { this.popupContainer.appendChild(rowLabel); const iconButton = document.createElement('img'); - iconButton.src = `../assets/ic_${SETTINGS_POPUP_CONTENT.ACTION_ICON}.png`; + iconButton.src = `../assets/icons/${SETTINGS_POPUP_CONTENT.ACTION_ICON}.png`; applyStyles(iconButton, { position: 'absolute', left: `${this.popup.width - LAYOUT.ICON_LEFT_OFFSET}px`, diff --git a/3D/js/popup/solve.js b/3D/js/popup/solve.js index 5bcb295..aba11fd 100644 --- a/3D/js/popup/solve.js +++ b/3D/js/popup/solve.js @@ -12,7 +12,10 @@ export class SolvePopup { } render() { - this.popupContainer = this.toolbar.createPopupContainer('solvePopup', this.toolbar.buttons[2].name); + this.popupContainer = this.toolbar.createPopupContainer( + 'solvePopup', + this.toolbar.buttons[2].name + ); this.popup = this.popupContainer.querySelector('canvas'); const ctx = this.popup.getContext('2d'); const { LAYOUT, FONT, COLORS } = SOLVE_POPUP_CONSTANTS; @@ -69,7 +72,7 @@ export class SolvePopup { if (row.icon) { const iconButton = document.createElement('img'); - iconButton.src = `../assets/ic_${row.icon}.png`; + iconButton.src = `../assets/icons/${row.icon}.png`; applyStyles(iconButton, { position: 'absolute', right: '0', diff --git a/3D/js/popup/tutorial.js b/3D/js/popup/tutorial.js index 549f151..da19dd5 100644 --- a/3D/js/popup/tutorial.js +++ b/3D/js/popup/tutorial.js @@ -1,8 +1,4 @@ -import { - TUTORIAL_POPUP_CONSTANTS, - TUTORIAL_POPUP_ROWS, - TUTORIAL_ICON_MAP, -} from '../constants.js'; +import { TUTORIAL_POPUP_CONSTANTS, TUTORIAL_POPUP_ROWS, TUTORIAL_ICON_MAP } from '../constants.js'; function applyStyles(element, styles) { Object.assign(element.style, styles); @@ -60,7 +56,7 @@ export class TutorialPopup { if (row.icon) { const icon = document.createElement('img'); - icon.src = `../assets/ic_${row.icon}.png`; + icon.src = `../assets/icons/${row.icon}.png`; applyStyles(icon, { position: 'absolute', right: '0', diff --git a/3D/js/toolbar.js b/3D/js/toolbar.js index 02e19ee..fd3b5ec 100644 --- a/3D/js/toolbar.js +++ b/3D/js/toolbar.js @@ -81,31 +81,31 @@ export class Toolbar { return [ { name: 'Create Polycube', - icon: '../assets/ic_plus.png', + icon: '../assets/icons/plus.png', action: () => this.togglePopup('cube'), description: 'To create a new cube and add it to the scene.', }, { name: 'Grid Settings', - icon: '../assets/ic_table.png', + icon: '../assets/icons/table.png', action: () => this.togglePopup('grid'), description: 'To change the grid settings.', }, { name: 'Solving Polycube', - icon: '../assets/ic_solving.png', + icon: '../assets/icons/solving.png', action: () => this.togglePopup('solve'), description: 'To solve the polycube puzzle.\nUse different algorithms to solve.', }, { name: 'Tutorial', - icon: '../assets/ic_question.png', + icon: '../assets/icons/question.png', action: () => this.togglePopup('tutorial'), description: 'To view the tutorial.\nLearn how to use the application.', }, { name: 'Settings', - icon: '../assets/ic_setting.png', + icon: '../assets/icons/setting.png', action: () => this.togglePopup('settings'), description: 'To adjust application settings.\nChange colors, tooltips, and more.', }, @@ -155,14 +155,14 @@ export class Toolbar { TOOLBAR_CONSTANTS.BUTTON_ICON_SIZE, TOOLBAR_CONSTANTS.BUTTON_ICON_SIZE, () => { - this.ctx.strokeStyle = '#fff'; - this.ctx.strokeRect( - x - TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, - TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, - TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE, - TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE - ); - } + this.ctx.strokeStyle = '#fff'; + this.ctx.strokeRect( + x - TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, + TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, + TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE, + TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE + ); + } ); }); } @@ -184,14 +184,14 @@ export class Toolbar { TOOLBAR_CONSTANTS.BUTTON_ICON_SIZE, TOOLBAR_CONSTANTS.BUTTON_ICON_SIZE, () => { - this.ctx.strokeStyle = '#fff'; - this.ctx.strokeRect( - TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, - y - TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, - TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE, - TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE - ); - } + this.ctx.strokeStyle = '#fff'; + this.ctx.strokeRect( + TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, + y - TOOLBAR_CONSTANTS.BUTTON_STROKE_OFFSET, + TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE, + TOOLBAR_CONSTANTS.BUTTON_OUTER_SIZE + ); + } ); }); } @@ -320,7 +320,7 @@ export class Toolbar { } addHomeButton() { - this.drawIcon('../assets/ic_home.png', 10, 10, 30, 30, () => { + this.drawIcon('../assets/icons/home.png', 10, 10, 30, 30, () => { this.ctx.strokeStyle = '#fff'; this.ctx.strokeRect(5, 5, 40, 40); }); @@ -397,7 +397,7 @@ export class Toolbar { document.body.removeChild(this.currentCloseIcon); } const closeIcon = new Image(); - closeIcon.src = '../assets/ic_close.png'; + closeIcon.src = '../assets/icons/close.png'; closeIcon.style.position = 'fixed'; closeIcon.style.top = this.isMobile ? '56px' : '10px'; closeIcon.style.left = this.isMobile ? 'calc(50% + 162px)' : '400px'; diff --git a/3D/style.css b/3D/style.css index efacef6..2471d09 100644 --- a/3D/style.css +++ b/3D/style.css @@ -1,7 +1,7 @@ @charset "UTF-8"; @font-face { font-family: 'Pixellari'; - src: url('../assets/Pixellari.ttf') format('truetype'); + src: url('../assets/fonts/Pixellari.ttf') format('truetype'); } * { font-family: 'Pixellari', sans-serif; diff --git a/assets/Pixellari.ttf b/assets/Pixellari.ttf deleted file mode 100644 index 5a3a3c2..0000000 Binary files a/assets/Pixellari.ttf and /dev/null differ diff --git a/assets/fonts/Pixellari.ttf b/assets/fonts/Pixellari.ttf new file mode 100644 index 0000000..76ebfdc Binary files /dev/null and b/assets/fonts/Pixellari.ttf differ diff --git a/assets/fonts/README.md b/assets/fonts/README.md new file mode 100644 index 0000000..aee31a1 --- /dev/null +++ b/assets/fonts/README.md @@ -0,0 +1,87 @@ +# Fonts Workflow (Vietnamese Glyphs) + +This folder contains the source font and scripts to check/generate full Vietnamese glyph coverage for the project. + +## Folder Structure + +- `Pixellari.ttf`: the base font currently used in the game. +- `generate_vietnamese_full.py`: main script to create/overwrite missing Vietnamese glyphs. +- `check_vietnamese_missing.py`: quick check for missing Vietnamese glyphs in the font. +- `check_glyphs.py`: prints all glyph names available in the font. +- `requirements.txt`: minimal dependencies for Python scripts related to font processing. + +## Environment Requirements + +The scripts use **FontForge Python bindings** (`fontforge`, `psMat`). + +1. Install system dependencies (recommended on Ubuntu/Debian): + +```bash +sudo apt update +sudo apt install fontforge python3-fontforge +``` + +2. (Optional) create a virtual environment if you want isolated Python package management. + +## How to Run + +Run commands from the `assets/fonts` folder. + +### 1) Check missing glyphs before generating + +```bash +python3 check_vietnamese_missing.py +``` + +### 2) Generate full Vietnamese glyph coverage + +```bash +python3 generate_vietnamese_full.py +``` + +Default output: `Pixellari-Vietnamese.ttf`. + +Useful options: + +```bash +python3 generate_vietnamese_full.py --input Pixellari.ttf --output Pixellari-Vietnamese.ttf +python3 generate_vietnamese_full.py --force +fontforge -script generate_vietnamese_full.py --input Pixellari.ttf --output Pixellari-Vietnamese.ttf +``` + +- `--force`: overwrite Vietnamese glyphs even if they already exist (useful when regenerating from a previously generated font). + +### 3) Check again after generating + +```bash +python3 check_vietnamese_missing.py +``` + +If generation is fully successful, the final number of missing glyphs should be `0`. + +## Glyph Composition Logic (Summary) + +`generate_vietnamese_full.py` builds glyphs by **composing** from existing glyph parts in the font: + +- Decomposes Vietnamese characters with NFD to identify: + - base letter (`a`, `e`, `o`, `u`, `i`, ...) + - shape marks (`breve`, `circumflex`, `horn`) + - tone marks (`grave`, `acute`, `hook`, `tilde`, `dot below`) +- Builds the shaped base first (for example `ă`, `â`, `ê`, `ô`, `ơ`, `ư`), then applies tone marks. +- For `ơ/ư`, the script constructs the horn automatically if the base font does not provide those glyphs. +- For lowercase `i` with top marks, the script uses `dotlessi` to avoid dot overlap. +- Marks are aligned by bounding boxes and Pixellari's pixel grid, then rounded and overlap-cleaned. + +## Adding New Glyph Sets Beyond Vietnamese + +1. Open `generate_vietnamese_full.py`. +2. Update the target character set (`VIETNAMESE_REQUIRED`) or add a new constant for another language. +3. If new marks or placement rules are needed: + - add/update rules in `parse_decomposition`, `shape_base_char`, or `ensure_composed_vietnamese_glyph`. + - reuse existing helpers (`add_layer_with_alignment`, `add_hook_above`, `add_dot_below`, ...). +4. Regenerate the font and re-run the missing glyph check. + +## Notes + +- `requirements.txt` only documents minimal Python-level dependencies. In practice, `fontforge` is usually installed via the OS package manager. +- Avoid committing generated font output files for local experiments; commit only after validating rendering in the game. diff --git a/assets/fonts/__pycache__/generate_vietnamese_full.cpython-312.pyc b/assets/fonts/__pycache__/generate_vietnamese_full.cpython-312.pyc new file mode 100644 index 0000000..84bf1ce Binary files /dev/null and b/assets/fonts/__pycache__/generate_vietnamese_full.cpython-312.pyc differ diff --git a/assets/fonts/check_glyphs.py b/assets/fonts/check_glyphs.py new file mode 100644 index 0000000..c5d02f2 --- /dev/null +++ b/assets/fonts/check_glyphs.py @@ -0,0 +1,6 @@ +import fontforge + +font = fontforge.open("Pixellari.ttf") + +for name in font: + print(name) diff --git a/assets/fonts/check_vietnamese_missing.py b/assets/fonts/check_vietnamese_missing.py new file mode 100644 index 0000000..3b97a3a --- /dev/null +++ b/assets/fonts/check_vietnamese_missing.py @@ -0,0 +1,24 @@ +import fontforge +import unicodedata + +font = fontforge.open("Pixellari.ttf") +vi = "aàáảãạăằắẳẵặâầấẩẫậbcdđeèéẻẽẹêềếểễệghiìíỉĩịklmnoòóỏõọôồốổỗộơờớởỡợpqrstuùúủũụưừứửữựvxyỳýỷỹỵAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬBCDĐEÈÉẺẼẸÊỀẾỂỄỆGHIÌÍỈĨỊKLMNOÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢPQRSTUÙÚỦŨỤƯỪỨỬỮỰVXYỲÝỶỸỴ" + +seen = [] +for ch in vi: + if ch not in seen: + seen.append(ch) + +missing = [] +for ch in seen: + cp = ord(ch) + try: + _ = font[cp].glyphname + except Exception: + missing.append(ch) + +print("required", len(seen)) +print("missing", len(missing)) +for ch in missing: + cp = ord(ch) + print("U+%04X\t%s\t%s" % (cp, ch, unicodedata.name(ch, "UNKNOWN"))) diff --git a/assets/fonts/generate_vietnamese_full.py b/assets/fonts/generate_vietnamese_full.py new file mode 100644 index 0000000..11b5857 --- /dev/null +++ b/assets/fonts/generate_vietnamese_full.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +""" +Generate full Vietnamese glyph coverage for Pixellari.ttf. + +Usage examples: + python3 generate_vietnamese_full.py + python3 generate_vietnamese_full.py --input Pixellari.ttf --output Pixellari-VI.ttf + python3 generate_vietnamese_full.py --force + fontforge -script generate_vietnamese_full.py --input Pixellari.ttf --output Pixellari-VI.ttf +""" + +from __future__ import annotations + +import argparse +import os +import sys +import unicodedata +from typing import Dict, List, Sequence, Tuple + +try: + import fontforge + import psMat +except ImportError as exc: # pragma: no cover + raise SystemExit( + "Missing dependency: fontforge Python module.\n" + "Run with FontForge Python (e.g. `fontforge -script ...`) or install python-fontforge." + ) from exc + + +TONE_GRAVE = "\u0300" +TONE_ACUTE = "\u0301" +TONE_HOOK = "\u0309" +TONE_TILDE = "\u0303" +TONE_DOT = "\u0323" +TONE_MARKS = {TONE_GRAVE, TONE_ACUTE, TONE_HOOK, TONE_TILDE, TONE_DOT} + +SHAPE_CIRC = "\u0302" +SHAPE_BREVE = "\u0306" +SHAPE_HORN = "\u031B" + +VIETNAMESE_REQUIRED = ( + "aàáảãạăằắẳẵặâầấẩẫậbcdđeèéẻẽẹêềếểễệghiìíỉĩị" + "klmnoòóỏõọôồốổỗộơờớởỡợpqrstuùúủũụưừứửữựvxyỳýỷỹỵ" + "AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬBCDĐEÈÉẺẼẸÊỀẾỂỄỆGHIÌÍỈĨỊ" + "KLMNOÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢPQRSTUÙÚỦŨỤƯỪỨỬỮỰVXYỲÝỶỸỴ" +) + +# These glyphs are rebuilt even without --force so i/I accent behavior stays correct. +ALWAYS_REBUILD = set("ìíĩỉịÌÍĨỈỊ") + + +def _nfc(s: str) -> str: + return unicodedata.normalize("NFC", s) + + +def build_vietnamese_charset() -> List[str]: + seen = set() + ordered_unique: List[str] = [] + for ch in VIETNAMESE_REQUIRED: + if ch not in seen: + seen.add(ch) + ordered_unique.append(ch) + return ordered_unique + + +def glyph_exists(font, codepoint: int) -> bool: + try: + _ = font[codepoint].glyphname + return True + except Exception: + return False + + +def glyph_has_data(glyph) -> bool: + try: + if len(glyph.references) > 0: + return True + except Exception: + pass + try: + return not glyph.foreground.isEmpty() + except Exception: + return False + + +def copy_glyph_data(dst, src) -> None: + dst.clear() + dst.foreground = src.foreground.dup() + dst.width = src.width + + +def normalize_generated_glyph(glyph) -> None: + try: + glyph.round() + except Exception: + pass + try: + glyph.removeOverlap() + except Exception: + pass + try: + glyph.round() + except Exception: + pass + + +def contour_bbox(contour) -> Tuple[float, float, float, float]: + xs = [p.x for p in contour] + ys = [p.y for p in contour] + return (min(xs), min(ys), max(xs), max(ys)) + + +def filter_layer_contours(layer, keep_fn): + filtered = layer.dup() + to_delete: List[int] = [] + for i, contour in enumerate(filtered): + if not keep_fn(contour_bbox(contour)): + to_delete.append(i) + for i in reversed(to_delete): + del filtered[i] + return filtered + + +def extract_top_mark_layer(font, donor_name: str, donor_base_name: str, right_only: bool = False): + donor = font[donor_name] + donor_base = font[donor_base_name] + _, _, base_x2, base_y2 = donor_base.boundingBox() + base_x1, _, _, _ = donor_base.boundingBox() + base_center_x = (base_x1 + base_x2) / 2.0 + + donor_layer = donor.foreground.dup() + top_layer = filter_layer_contours( + donor_layer, + lambda bb: bb[3] > base_y2 + 1.0, + ) + if right_only: + top_layer = filter_layer_contours( + top_layer, + lambda bb: ((bb[0] + bb[2]) / 2.0) > base_center_x, + ) + return top_layer + + +def layer_bbox_center_x(layer) -> float: + x1, _, x2, _ = layer.boundingBox() + return (x1 + x2) / 2.0 + + +def add_layer_with_alignment( + target_layer, + mark_layer, + donor_base_glyph, + target_base_glyph, + y_anchor: str = "top", + scale_x: float = 1.0, + y_offset: float = 0.0, +): + moved = mark_layer.dup() + if scale_x != 1.0: + mx1, my1, mx2, my2 = moved.boundingBox() + mcx = (mx1 + mx2) / 2.0 + mcy = (my1 + my2) / 2.0 + moved.transform(psMat.translate(-mcx, -mcy)) + moved.transform(psMat.scale(scale_x, 1.0)) + moved.transform(psMat.translate(mcx, mcy)) + + donor_x1, donor_y1, donor_x2, donor_y2 = donor_base_glyph.boundingBox() + target_x1, target_y1, target_x2, target_y2 = target_base_glyph.boundingBox() + + dx = ((target_x1 + target_x2) / 2.0) - ((donor_x1 + donor_x2) / 2.0) + if y_anchor == "top": + dy = target_y2 - donor_y2 + y_offset + elif y_anchor == "bottom": + dy = target_y1 - donor_y1 + y_offset + else: + dy = y_offset + + moved.transform(psMat.translate(dx, dy)) + target_layer += moved + return target_layer + + +def add_dot_below(target_layer, base_glyph, period_glyph): + dot = period_glyph.foreground.dup() + dot_x1, dot_y1, dot_x2, _ = dot.boundingBox() + base_x1, base_y1, base_x2, _ = base_glyph.boundingBox() + dx = ((base_x1 + base_x2) / 2.0) - ((dot_x1 + dot_x2) / 2.0) + # Keep one 64-unit gap below baseline for Pixellari's pixel grid. + dy = (base_y1 - 192.0) - dot_y1 + dot.transform(psMat.translate(dx, dy)) + target_layer += dot + return target_layer + + +def add_hook_above(target_layer, base_glyph, comma_glyph, y_lift: float = 0.0): + hook = comma_glyph.foreground.dup() + # Mirror comma upward to get a compact hook shape that matches stroke style. + hook.transform(psMat.scale(1.0, -1.0)) + hook_x1, hook_y1, hook_x2, _ = hook.boundingBox() + base_x1, _, base_x2, base_y2 = base_glyph.boundingBox() + dx = ((base_x1 + base_x2) / 2.0 + 32.0) - ((hook_x1 + hook_x2) / 2.0) + # Keep hook close to the main top accent zone; avoids over-tall stacks. + dy = (base_y2 - 64.0 + y_lift) - hook_y1 + hook.transform(psMat.translate(dx, dy)) + target_layer += hook + return target_layer + + +def add_horn_right(target_layer, base_glyph, comma_glyph): + horn = comma_glyph.foreground.dup() + horn_x1, horn_y1, horn_x2, _ = horn.boundingBox() + base_x1, _, base_x2, base_y2 = base_glyph.boundingBox() + + # Place horn at top-right shoulder while keeping inside advance width. + dx = (base_x2 - 32.0) - ((horn_x1 + horn_x2) / 2.0) + dy = (base_y2 - 64.0) - horn_y1 + horn.transform(psMat.translate(dx, dy)) + target_layer += horn + return target_layer + + +def ensure_horn_base(font, ch: str, comma_glyph) -> bool: + cp = ord(ch) + if glyph_exists(font, cp) and glyph_has_data(font[cp]): + return True + + mapping = { + "ơ": "o", + "ư": "u", + "Ơ": "O", + "Ư": "U", + } + if ch not in mapping: + return False + + base_name = mapping[ch] + base = font[base_name] + + dst = font.createChar(cp) + layer = base.foreground.dup() + layer = add_horn_right(layer, base, comma_glyph) + + dst.clear() + dst.foreground = layer + dst.width = base.width + normalize_generated_glyph(dst) + return glyph_has_data(dst) + + +def parse_decomposition(ch: str) -> Tuple[str, List[str], List[str]]: + nfd = unicodedata.normalize("NFD", ch) + base = nfd[0] + shape_marks: List[str] = [] + tone_marks: List[str] = [] + for mark in nfd[1:]: + if mark in TONE_MARKS: + tone_marks.append(mark) + elif mark in {SHAPE_BREVE, SHAPE_CIRC, SHAPE_HORN}: + shape_marks.append(mark) + return base, shape_marks, tone_marks + + +def shape_base_char(base: str, shape_marks: Sequence[str]) -> str: + has_circ = SHAPE_CIRC in shape_marks + has_breve = SHAPE_BREVE in shape_marks + has_horn = SHAPE_HORN in shape_marks + if has_horn: + if base == "o": + return "ơ" + if base == "u": + return "ư" + if base == "O": + return "Ơ" + if base == "U": + return "Ư" + if has_breve: + return _nfc(base + SHAPE_BREVE) + if has_circ: + return _nfc(base + SHAPE_CIRC) + return base + + +def ensure_composed_vietnamese_glyph( + font, + ch: str, + mark_layers: Dict[str, object], + force_overwrite: bool, +) -> bool: + cp = ord(ch) + if (not force_overwrite) and (ch not in ALWAYS_REBUILD) and glyph_exists(font, cp) and glyph_has_data(font[cp]): + return True + + base, shape_marks, tone_marks = parse_decomposition(ch) + shaped = shape_base_char(base, shape_marks) + + # Ensure horn bases first because Pixellari does not ship o-horn/u-horn. + if SHAPE_HORN in shape_marks: + if not ensure_horn_base(font, shaped, mark_layers["comma"]): + return False + + shaped_cp = ord(shaped) + if not glyph_exists(font, shaped_cp) or not glyph_has_data(font[shaped_cp]): + return False + + base_glyph = font[shaped_cp] + # Vietnamese lowercase i drops the top dot only for top marks (grave/acute/hook/tilde). + if base == "i" and tone_marks and (tone_marks[0] != TONE_DOT): + base_glyph = font["dotlessi"] + base_width = base_glyph.width + dst = font.createChar(cp) + layer = base_glyph.foreground.dup() + + if tone_marks: + tone = tone_marks[0] + is_upper = base.isupper() + if tone == TONE_GRAVE: + mark = mark_layers["grave_upper"] if is_upper else mark_layers["grave_lower"] + donor = font["A"] if is_upper else font["a"] + y_offset = 64.0 if base == "I" else 0.0 + layer = add_layer_with_alignment(layer, mark, donor, base_glyph, y_anchor="top", y_offset=y_offset) + elif tone == TONE_ACUTE: + mark = mark_layers["acute_upper"] if is_upper else mark_layers["acute_lower"] + donor = font["A"] if is_upper else font["a"] + y_offset = 64.0 if base == "I" else 0.0 + layer = add_layer_with_alignment(layer, mark, donor, base_glyph, y_anchor="top", y_offset=y_offset) + elif tone == TONE_TILDE: + if base == "I": + mark = mark_layers["tilde_i_upper"] + donor = font["I"] + elif base == "i": + mark = mark_layers["tilde_lower"] + donor = font["a"] + else: + mark = mark_layers["tilde_upper"] if is_upper else mark_layers["tilde_lower"] + donor = font["A"] if is_upper else font["a"] + scale_x = 0.75 if base == "i" else 1.0 + y_offset = 64.0 if base == "I" else 0.0 + layer = add_layer_with_alignment( + layer, mark, donor, base_glyph, y_anchor="top", scale_x=scale_x, y_offset=y_offset + ) + elif tone == TONE_HOOK: + y_lift = 64.0 if base == "I" else 0.0 + layer = add_hook_above(layer, base_glyph, font["comma"], y_lift=y_lift) + elif tone == TONE_DOT: + layer = add_dot_below(layer, base_glyph, font["period"]) + + dst.clear() + dst.foreground = layer + dst.width = base_width + normalize_generated_glyph(dst) + return glyph_has_data(dst) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate Vietnamese glyphs for Pixellari with manual pixel-grid composition." + ) + parser.add_argument( + "--input", + default="Pixellari.ttf", + help="Input TTF path (default: Pixellari.ttf)", + ) + parser.add_argument( + "--output", + default="Pixellari-Vietnamese.ttf", + help="Output TTF path (default: Pixellari-Vietnamese.ttf)", + ) + parser.add_argument( + "--force", + action="store_true", + help="Overwrite Vietnamese glyphs even if they already exist (useful when regenerating from a previously generated font).", + ) + args = parser.parse_args() + + if not os.path.exists(args.input): + print(f"Input font not found: {args.input}", file=sys.stderr) + return 1 + + font = fontforge.open(args.input) + + mark_layers: Dict[str, object] = { + "grave_lower": extract_top_mark_layer(font, "agrave", "a"), + "acute_lower": extract_top_mark_layer(font, "aacute", "a"), + "tilde_lower": extract_top_mark_layer(font, "atilde", "a"), + "grave_upper": extract_top_mark_layer(font, "Agrave", "A"), + "acute_upper": extract_top_mark_layer(font, "Aacute", "A"), + "tilde_upper": extract_top_mark_layer(font, "Atilde", "A"), + "tilde_i_upper": extract_top_mark_layer(font, "Itilde", "I"), + "comma": font["comma"], + } + + required = build_vietnamese_charset() + missing_before = [] + for ch in required: + cp = ord(ch) + if (not glyph_exists(font, cp)) or (not glyph_has_data(font[cp])): + missing_before.append(ch) + + created: List[str] = [] + failed: List[str] = [] + for ch in required: + if ensure_composed_vietnamese_glyph(font, ch, mark_layers, force_overwrite=args.force): + created.append(ch) + else: + failed.append(ch) + + still_missing = [] + for ch in required: + cp = ord(ch) + if (not glyph_exists(font, cp)) or (not glyph_has_data(font[cp])): + still_missing.append(ch) + + font.generate(args.output) + font.close() + + print(f"Input: {args.input}") + print(f"Output: {args.output}") + print(f"Vietnamese required: {len(required)}") + print(f"Missing before: {len(missing_before)}") + print(f"Created now: {len(created)}") + print(f"Failed to build: {len(failed)}") + print(f"Still missing after generate: {len(still_missing)}") + + if still_missing: + print("Missing codepoints:") + for ch in still_missing: + cp = ord(ch) + print(f" U+{cp:04X} {ch} {unicodedata.name(ch, 'UNKNOWN')}") + return 2 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/assets/fonts/requirements.txt b/assets/fonts/requirements.txt new file mode 100644 index 0000000..036a614 --- /dev/null +++ b/assets/fonts/requirements.txt @@ -0,0 +1,6 @@ +# Python dependencies for scripts in this folder. +# NOTE: `fontforge` is usually installed via OS package manager (not reliable via pip). +# Ubuntu/Debian example: +# sudo apt update && sudo apt install fontforge python3-fontforge + +fontforge diff --git a/assets/ic_arrow_left.png b/assets/icons/arrow_left.png similarity index 100% rename from assets/ic_arrow_left.png rename to assets/icons/arrow_left.png diff --git a/assets/ic_arrow_right.png b/assets/icons/arrow_right.png similarity index 100% rename from assets/ic_arrow_right.png rename to assets/icons/arrow_right.png diff --git a/assets/ic_blacken.png b/assets/icons/blacken.png similarity index 100% rename from assets/ic_blacken.png rename to assets/icons/blacken.png diff --git a/assets/ic_blacken_cell.png b/assets/icons/blacken_cell.png similarity index 100% rename from assets/ic_blacken_cell.png rename to assets/icons/blacken_cell.png diff --git a/assets/ic_close.png b/assets/icons/close.png similarity index 100% rename from assets/ic_close.png rename to assets/icons/close.png diff --git a/assets/ic_draw.png b/assets/icons/draw.png similarity index 100% rename from assets/ic_draw.png rename to assets/icons/draw.png diff --git a/assets/ic_duplicate.png b/assets/icons/duplicate.png similarity index 100% rename from assets/ic_duplicate.png rename to assets/icons/duplicate.png diff --git a/assets/ic_flip.png b/assets/icons/flip.png similarity index 100% rename from assets/ic_flip.png rename to assets/icons/flip.png diff --git a/assets/ic_github.png b/assets/icons/github.png similarity index 100% rename from assets/ic_github.png rename to assets/icons/github.png diff --git a/assets/ic_home.png b/assets/icons/home.png similarity index 100% rename from assets/ic_home.png rename to assets/icons/home.png diff --git a/assets/info.png b/assets/icons/info.png similarity index 100% rename from assets/info.png rename to assets/icons/info.png diff --git a/assets/ic_invert_blacken.png b/assets/icons/invert_blacken.png similarity index 100% rename from assets/ic_invert_blacken.png rename to assets/icons/invert_blacken.png diff --git a/assets/ic_left_click.png b/assets/icons/left_click.png similarity index 100% rename from assets/ic_left_click.png rename to assets/icons/left_click.png diff --git a/assets/ic_plus.png b/assets/icons/plus.png similarity index 100% rename from assets/ic_plus.png rename to assets/icons/plus.png diff --git a/assets/ic_question.png b/assets/icons/question.png similarity index 100% rename from assets/ic_question.png rename to assets/icons/question.png diff --git a/assets/ic_random_blacken_cell.png b/assets/icons/random_blacken_cell.png similarity index 100% rename from assets/ic_random_blacken_cell.png rename to assets/icons/random_blacken_cell.png diff --git a/assets/ic_reset.png b/assets/icons/reset.png similarity index 100% rename from assets/ic_reset.png rename to assets/icons/reset.png diff --git a/assets/ic_right_click.png b/assets/icons/right_click.png similarity index 100% rename from assets/ic_right_click.png rename to assets/icons/right_click.png diff --git a/assets/ic_rotate_left.png b/assets/icons/rotate_left.png similarity index 100% rename from assets/ic_rotate_left.png rename to assets/icons/rotate_left.png diff --git a/assets/ic_rotate_right.png b/assets/icons/rotate_right.png similarity index 100% rename from assets/ic_rotate_right.png rename to assets/icons/rotate_right.png diff --git a/assets/ic_select.png b/assets/icons/select.png similarity index 100% rename from assets/ic_select.png rename to assets/icons/select.png diff --git a/assets/ic_setting.png b/assets/icons/setting.png similarity index 100% rename from assets/ic_setting.png rename to assets/icons/setting.png diff --git a/assets/ic_shuffle.png b/assets/icons/shuffle.png similarity index 100% rename from assets/ic_shuffle.png rename to assets/icons/shuffle.png diff --git a/assets/ic_solution.png b/assets/icons/solution.png similarity index 100% rename from assets/ic_solution.png rename to assets/icons/solution.png diff --git a/assets/ic_solving.png b/assets/icons/solving.png similarity index 100% rename from assets/ic_solving.png rename to assets/icons/solving.png diff --git a/assets/ic_split.png b/assets/icons/split.png similarity index 100% rename from assets/ic_split.png rename to assets/icons/split.png diff --git a/assets/ic_table.png b/assets/icons/table.png similarity index 100% rename from assets/ic_table.png rename to assets/icons/table.png diff --git a/assets/ic_trash.png b/assets/icons/trash.png similarity index 100% rename from assets/ic_trash.png rename to assets/icons/trash.png diff --git a/assets/ic_whiten.png b/assets/icons/whiten.png similarity index 100% rename from assets/ic_whiten.png rename to assets/icons/whiten.png diff --git a/js/main.js b/js/main.js index fd0d83a..5f01471 100644 --- a/js/main.js +++ b/js/main.js @@ -40,7 +40,7 @@ class MainApp { ]; this.usedColors = []; this.iconImage = new Image(); - this.iconImage.src = './assets/ic_arrow_right.png'; + this.iconImage.src = './assets/icons/arrow_right.png'; this.targetFps = 60; this.frameDurationMs = 1000 / this.targetFps; this.maxDeltaMs = 100; @@ -122,7 +122,9 @@ class MainApp { this.ctx.textAlign = 'center'; this.ctx.fillText('Research polyominoes &', this.canvas.width / 2, 200); this.ctx.fillText('Propose solving solutions.', this.canvas.width / 2, 230); - this.ctx.fillText('Mini Polyominoes Games :', this.canvas.width / 2, this.miniGamesLabelY); + if (this.canvas.width >= 700) { + this.ctx.fillText('Mini Polyominoes Games :', this.canvas.width / 2, this.miniGamesLabelY); + } this.buttons.forEach((button) => this.drawButton(button)); } diff --git a/js/tetris.js b/js/tetris.js index 40c63b8..90b3fad 100644 --- a/js/tetris.js +++ b/js/tetris.js @@ -12,11 +12,11 @@ class Tetris { this.tetriminos = TETRIMINOS; this.controlItems = [ - { name: 'ic_arrow_left.png', action: () => this.move(-1), holdable: true }, - { name: 'ic_arrow_right.png', action: () => this.move(1), holdable: true }, - { name: 'ic_rotate_left.png', action: () => this.rotateLeft(), holdable: false }, - { name: 'ic_rotate_right.png', action: () => this.rotateRight(), holdable: false }, - { name: 'ic_home.png', action: () => this.goHome(), holdable: false }, + { name: 'icons/arrow_left.png', action: () => this.move(-1), holdable: true }, + { name: 'icons/arrow_right.png', action: () => this.move(1), holdable: true }, + { name: 'icons/rotate_left.png', action: () => this.rotateLeft(), holdable: false }, + { name: 'icons/rotate_right.png', action: () => this.rotateRight(), holdable: false }, + { name: 'icons/home.png', action: () => this.goHome(), holdable: false }, ]; this.holdDelayMs = 180; this.holdRepeatMs = 70; @@ -172,15 +172,15 @@ class Tetris { getFallbackLabel(iconName) { switch (iconName) { - case 'ic_arrow_left.png': + case 'icons/arrow_left.png': return 'L'; - case 'ic_arrow_right.png': + case 'icons/arrow_right.png': return 'R'; - case 'ic_rotate_left.png': + case 'icons/rotate_left.png': return 'RL'; - case 'ic_rotate_right.png': + case 'icons/rotate_right.png': return 'RR'; - case 'ic_home.png': + case 'icons/home.png': return 'H'; default: return '?'; diff --git a/style.css b/style.css index 627fde8..24f298f 100644 --- a/style.css +++ b/style.css @@ -1,7 +1,7 @@ @charset "UTF-8"; @font-face { font-family: 'Pixellari'; - src: url('./assets/Pixellari.ttf') format('truetype'); + src: url('./assets/fonts/Pixellari.ttf') format('truetype'); } html,