Skip to content

Commit 224c784

Browse files
Add frontend and backend code
1 parent d4d6be4 commit 224c784

11 files changed

Lines changed: 2077 additions & 1 deletion

File tree

.github/workflows/deploy.yml

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
name: Build and deploy Pixel2Polygon to GitHub Pages
2+
3+
on:
4+
pull_request:
5+
branches: ["main"]
6+
push:
7+
branches: ["main"]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
pages: write
13+
id-token: write
14+
15+
concurrency:
16+
group: "pages"
17+
cancel-in-progress: false
18+
19+
jobs:
20+
build:
21+
name: Compile Multilingual sources to WebAssembly
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- name: Check out repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Python 3.12
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.12"
32+
33+
- name: Set up Node.js
34+
uses: actions/setup-node@v4
35+
with:
36+
node-version: "20"
37+
38+
- name: Install build dependencies
39+
run: |
40+
python -m pip install --upgrade pip
41+
python -m pip install -r requirements-build.txt
42+
43+
- name: Compile Multilingual sources to WebAssembly
44+
run: python -m multilingualprogramming scripts/compile_wasm.ml
45+
46+
- name: Verify build artifacts
47+
run: |
48+
ls -la public
49+
test -f public/index.html
50+
test -f public/style.css
51+
test -f public/app.js
52+
test -f public/hexagonify.wasm
53+
54+
- name: Check JavaScript syntax
55+
run: node --check public/app.js
56+
57+
- name: Validate WASM or fall back to JavaScript-only publish
58+
run: |
59+
node -e "
60+
const fs = require('fs');
61+
const wasmPath = 'public/hexagonify.wasm';
62+
const watPath = 'public/hexagonify.wat';
63+
const fallback = (message, detail) => {
64+
console.warn(message);
65+
if (detail) {
66+
console.warn(detail);
67+
}
68+
if (fs.existsSync(wasmPath)) fs.unlinkSync(wasmPath);
69+
if (fs.existsSync(watPath)) fs.unlinkSync(watPath);
70+
console.log('Publishing JavaScript-only fallback.');
71+
process.exit(0);
72+
};
73+
if (!fs.existsSync(wasmPath)) {
74+
fallback('WASM artifact missing after compilation.');
75+
}
76+
const bytes = fs.readFileSync(wasmPath);
77+
const module = new WebAssembly.Module(bytes);
78+
const importObject = {};
79+
for (const entry of WebAssembly.Module.imports(module)) {
80+
if (!importObject[entry.module]) {
81+
importObject[entry.module] = {};
82+
}
83+
if (entry.kind === 'function') {
84+
importObject[entry.module][entry.name] = () => 0;
85+
} else if (entry.kind === 'memory') {
86+
importObject[entry.module][entry.name] = new WebAssembly.Memory({ initial: 16 });
87+
} else if (entry.kind === 'table') {
88+
importObject[entry.module][entry.name] = new WebAssembly.Table({ initial: 0, element: 'anyfunc' });
89+
} else if (entry.kind === 'global') {
90+
importObject[entry.module][entry.name] = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
91+
}
92+
}
93+
if (!importObject.env) {
94+
importObject.env = {};
95+
}
96+
WebAssembly.instantiate(module, importObject).then((instance) => {
97+
const exports = instance.exports;
98+
const required = [
99+
'hex_vertex_x', 'hex_vertex_y',
100+
'horiz_spacing', 'vert_spacing',
101+
'tri_h', 'tri_vertex_x', 'tri_vertex_y',
102+
'color_mean',
103+
'method_hex', 'method_square', 'method_triangle',
104+
];
105+
const missing = required.filter((name) => typeof exports[name] !== 'function');
106+
if (missing.length) {
107+
fallback('WASM exports missing.', missing.join(', '));
108+
}
109+
const checks = [
110+
['hex_vertex_x', Number(exports.hex_vertex_x(0, 0, 10, 0)), 0, 0.001],
111+
['hex_vertex_y', Number(exports.hex_vertex_y(0, 0, 10, 0)), -10, 0.001],
112+
['horiz_spacing', Number(exports.horiz_spacing(10)), 17.320508, 0.01],
113+
['vert_spacing', Number(exports.vert_spacing(10)), 15, 0.001],
114+
['tri_h', Number(exports.tri_h(10)), 8.660254, 0.01],
115+
['tri_vertex_x', Number(exports.tri_vertex_x(5, 10, 1, 1)), 10, 0.001],
116+
['tri_vertex_y', Number(exports.tri_vertex_y(5, 10, 2, 0)), 13.660254, 0.01],
117+
['color_mean', Number(exports.color_mean(11, 2)), 6, 0.001],
118+
['method_hex', Number(exports.method_hex()), 0, 0.001],
119+
['method_square', Number(exports.method_square()), 1, 0.001],
120+
['method_triangle', Number(exports.method_triangle()), 2, 0.001],
121+
];
122+
for (const [name, actual, expected, tolerance] of checks) {
123+
if (Math.abs(actual - expected) > tolerance) {
124+
fallback('WASM smoke test failed.', JSON.stringify({ name, expected, actual, tolerance }));
125+
}
126+
}
127+
console.log('WASM smoke test OK');
128+
}).catch((err) => {
129+
fallback('Could not instantiate WASM.', err && err.stack ? err.stack : String(err));
130+
});
131+
"
132+
133+
- name: Configure GitHub Pages
134+
uses: actions/configure-pages@v5
135+
136+
- name: Upload Pages artifact
137+
uses: actions/upload-pages-artifact@v3
138+
with:
139+
path: "public"
140+
141+
deploy:
142+
name: Deploy to GitHub Pages
143+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
144+
runs-on: ubuntu-latest
145+
needs: build
146+
environment:
147+
name: github-pages
148+
url: ${{ steps.deployment.outputs.page_url }}
149+
150+
steps:
151+
- name: Deploy to GitHub Pages
152+
id: deployment
153+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,9 @@ cython_debug/
205205
marimo/_static/
206206
marimo/_lsp/
207207
__marimo__/
208+
209+
# Local IDE/editor settings
210+
.idea/
211+
.vscode/
212+
.claude/
213+
*.code-workspace

README.md

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,190 @@
11
# pixel2polygon
2-
Pixel2Polygon converts images into geometric mosaics by replacing pixels with regular polygon tiles (triangles, squares, hexagons, and more).
2+
3+
A web application that turns raster images into geometric mosaics by tiling them with regular polygons — hexagons, squares, or equilateral triangles — and filling each tile with a representative colour.
4+
5+
All geometry computation runs in **WebAssembly** compiled from **French [Multilingual](https://multilingualprogramming.github.io/docs) sources**. The browser relies entirely on the WASM binary.
6+
7+
---
8+
9+
## Features
10+
11+
- Three tiling modes: **hexagon** (pointy-top, staggered rows), **square**, **equilateral triangle**
12+
- Tile size configurable from 5 px to 120 px
13+
- Optional tile outlines with adjustable colour and opacity
14+
- Drag-and-drop / click / paste image upload
15+
- Before-and-after canvas view with one-click PNG download
16+
- Geometry (`sommet_hex_x`, `espacement_horiz`, `hauteur_tri`, `couleur_moyenne`, …) exported from WASM with French identifiers
17+
- Full canonical implementation — including **KMeans dominant-colour** mode via `MiniBatchKMeans` — available in `src/hexagonify_canonique.ml`
18+
19+
---
20+
21+
## Project structure
22+
23+
```
24+
pixel2polygon/
25+
├── src/
26+
│ ├── hexagonify_wasm.ml # WASM module — geometry + mean colour (French multilingual)
27+
│ ├── hexagonify_canonique.ml # Canonical source — full algorithm + KMeans (French multilingual)
28+
│ └── main.ml # Entry point: importer hexagonify_wasm
29+
├── scripts/
30+
│ └── compile_wasm.ml # Build script (French multilingual)
31+
├── public/
32+
│ ├── index.html # Web UI
33+
│ ├── style.css # Dark-theme stylesheet
34+
│ ├── app.js # JavaScript frontend (WASM-only, no fallback)
35+
│ ├── hexagonify.wasm # Compiled binary ← generated by build
36+
│ └── hexagonify.wat # WAT text format ← generated by build
37+
└── requirements-build.txt # Python build dependencies
38+
```
39+
40+
---
41+
42+
## Architecture
43+
44+
```
45+
src/hexagonify_wasm.ml (French multilingual)
46+
47+
48+
Lexer / Parser
49+
50+
51+
WATCodeGenerator ──► hexagonify.wat
52+
53+
54+
wasmtime.wat2wasm ──► hexagonify.wasm
55+
56+
57+
Browser loads WASM, calls French exports:
58+
sommet_hex_x / sommet_hex_y
59+
espacement_horiz / espacement_vert
60+
hauteur_tri / sommet_tri_x / sommet_tri_y
61+
couleur_moyenne
62+
```
63+
64+
The JavaScript frontend (`public/app.js`) calls these exports directly. If the WASM binary is absent or invalid, the application is disabled and the build command is shown.
65+
66+
---
67+
68+
## Building the WASM
69+
70+
### Prerequisites
71+
72+
```bash
73+
pip install -r requirements-build.txt
74+
# installs: multilingualprogramming, wasmtime
75+
```
76+
77+
### Compile
78+
79+
```bash
80+
multilingual run scripts/compile_wasm.ml
81+
```
82+
83+
Outputs:
84+
85+
| File | Description |
86+
|------|-------------|
87+
| `public/hexagonify.wasm` | Binary WebAssembly module loaded by the browser |
88+
| `public/hexagonify.wat` | Human-readable WAT (WebAssembly Text) for inspection |
89+
90+
To point the build script at a local development checkout of the multilingual runtime:
91+
92+
```bash
93+
MULTILINGUAL_DEV_PATH=/path/to/multilingual multilingual run scripts/compile_wasm.ml
94+
```
95+
96+
---
97+
98+
## Running the web application
99+
100+
Serve the `public/` directory with any static file server that sets the correct MIME type for `.wasm` files (`application/wasm`):
101+
102+
```bash
103+
# Python (built-in)
104+
python -m http.server 8080 --directory public
105+
106+
# Node.js (npx serve)
107+
npx serve public
108+
```
109+
110+
Then open `http://localhost:8080` in a browser.
111+
112+
> **Note:** opening `index.html` directly as a `file://` URL will block the WASM fetch due to browser CORS restrictions. Always use a local server.
113+
114+
---
115+
116+
## Source files
117+
118+
### `src/hexagonify_wasm.ml` — WASM module
119+
120+
Written in **French multilingual**. Exports pure numeric functions that the JavaScript frontend calls per tile:
121+
122+
| Export (French) | Description |
123+
|-----------------|-------------|
124+
| `sommet_hex_x(cx, cy, a, idx)` | x-coordinate of vertex *idx* (0–5) of a pointy-top hexagon |
125+
| `sommet_hex_y(cx, cy, a, idx)` | y-coordinate of the same vertex |
126+
| `espacement_horiz(a)` | Horizontal stride: √3 · *a* |
127+
| `espacement_vert(a)` | Vertical stride: 1.5 · *a* |
128+
| `hauteur_tri(a)` | Triangle height: √3/2 · *a* |
129+
| `sommet_tri_x(x, a, idx, vers_haut)` | x-coordinate of triangle vertex *idx* |
130+
| `sommet_tri_y(y, a, idx, vers_haut)` | y-coordinate of triangle vertex *idx* |
131+
| `couleur_moyenne(total, compte)` | Per-channel colour mean: `round(total / compte)` |
132+
| `methode_hexagone()` / `methode_carre()` / `methode_triangle()` | Method codes (0 / 1 / 2) |
133+
134+
### `src/hexagonify_canonique.ml` — canonical source
135+
136+
Written in **French multilingual**. Implements the complete algorithm as a Python-compiled program, including:
137+
138+
- PIL/NumPy polygon masks and bounding-box sampling
139+
- **`mode_couleur="kmeans"`** — dominant colour via `MiniBatchKMeans` (scikit-learn)
140+
- `mode_couleur="moyenne"` — fast per-channel mean
141+
- `carreler_image()` — main entry point equivalent to the original `hexagonify.py`
142+
- CLI demo block (`si __name__ == "__main__":`)
143+
144+
Run canonically with:
145+
146+
```bash
147+
multilingual run src/hexagonify_canonique.ml
148+
```
149+
150+
Requires `Pillow`, `numpy`, and optionally `scikit-learn`:
151+
152+
```bash
153+
pip install pillow numpy scikit-learn
154+
```
155+
156+
### `scripts/compile_wasm.ml` — build script
157+
158+
Written in **French multilingual**. Mirrors the pattern established by [Cellcosmos](https://github.com/multilingualprogramming/cellcosmos):
159+
160+
1. Locates the project root
161+
2. Reads and bundles `src/main.ml` + `src/hexagonify_wasm.ml`
162+
3. Lexes and parses the bundle with `language="fr"`
163+
4. Generates WAT via `WATCodeGenerator`
164+
5. Compiles to binary WASM via `wasmtime.wat2wasm`
165+
6. Copies source files into `public/` alongside the binary
166+
167+
---
168+
169+
## Colour modes
170+
171+
| Mode | Where implemented | How |
172+
|------|-------------------|-----|
173+
| **Moyenne** (mean) | `hexagonify_wasm.ml` → WASM → browser | Per-channel arithmetic mean of all pixels inside the tile |
174+
| **KMeans** | `hexagonify_canonique.ml` → Python | `MiniBatchKMeans` finds the dominant cluster centroid; falls back to mean if scikit-learn is unavailable |
175+
176+
The web interface uses **moyenne** (computed in WASM). The canonical `.ml` source exposes both modes for offline / batch use.
177+
178+
---
179+
180+
## Tiling geometry
181+
182+
- **Hexagons** — pointy-top orientation, staggered rows (offset every other row by half the horizontal pitch). Partial tiles at all four borders are preserved.
183+
- **Squares** — axis-aligned grid, top-left anchored. Border tiles clipped to image dimensions.
184+
- **Triangles** — alternating upward/downward equilateral triangles on a `(row + col) % 2` parity grid. Border tiles preserved.
185+
186+
---
187+
188+
## Relation to `hexagonify.py`
189+
190+
`hexagonify_canonique.ml` is a faithful French-multilingual translation of the original [`hexagonify.py`](../Images/hexagonify.py) algorithm, preserving the same geometry helpers, mask generation, and KMeans colour logic. `hexagonify_wasm.ml` is the minimal numeric subset of that algorithm that maps cleanly to WebAssembly.

0 commit comments

Comments
 (0)