midgard-hex-grid 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -1
- package/demo/demo-minimal.ts +531 -0
- package/demo/demo-theory.ts +1679 -0
- package/demo/index.html +11 -0
- package/demo/minimal.html +11 -0
- package/demo/package-lock.json +1162 -0
- package/demo/package.json +11 -0
- package/demo/vite.config.ts +9 -0
- package/dist/coordinate-layer.d.ts +1 -11
- package/dist/coordinate-layer.d.ts.map +1 -1
- package/dist/coordinate-layer.js +5 -5
- package/dist/coordinate-layer.js.map +1 -1
- package/dist/coordinate-positioner.d.ts +1 -17
- package/dist/coordinate-positioner.d.ts.map +1 -1
- package/dist/coordinate-positioner.js +7 -7
- package/dist/coordinate-positioner.js.map +1 -1
- package/dist/coordinate-range-fill.d.ts +1 -20
- package/dist/coordinate-range-fill.d.ts.map +1 -1
- package/dist/coordinate-range-fill.js +12 -10
- package/dist/coordinate-range-fill.js.map +1 -1
- package/dist/coordinate-range.d.ts +16 -7
- package/dist/coordinate-range.d.ts.map +1 -1
- package/dist/coordinate-range.js +30 -13
- package/dist/coordinate-range.js.map +1 -1
- package/dist/geometry.d.ts +1 -25
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +7 -7
- package/dist/geometry.js.map +1 -1
- package/dist/h-g-bounds.d.ts +8 -5
- package/dist/h-g-bounds.d.ts.map +1 -1
- package/dist/h-g-bounds.js +12 -10
- package/dist/h-g-bounds.js.map +1 -1
- package/dist/h-g-hexagon.d.ts +21 -7
- package/dist/h-g-hexagon.d.ts.map +1 -1
- package/dist/h-g-hexagon.js +34 -12
- package/dist/h-g-hexagon.js.map +1 -1
- package/dist/hex-grid.d.ts +5 -1
- package/dist/hex-grid.d.ts.map +1 -1
- package/dist/hex-grid.js +32 -15
- package/dist/hex-grid.js.map +1 -1
- package/dist/neighbours.d.ts +1 -23
- package/dist/neighbours.d.ts.map +1 -1
- package/dist/neighbours.js +7 -7
- package/dist/neighbours.js.map +1 -1
- package/docs/images/minimal-x.png +0 -0
- package/docs/images/minimal-y.png +0 -0
- package/docs/images/theory-x.png +0 -0
- package/docs/images/theory-y.png +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,4 +205,105 @@ import { MIDGARD_HEX_GRID_VERSION } from './dist/index.js'
|
|
|
205
205
|
|
|
206
206
|
## License
|
|
207
207
|
|
|
208
|
-
ISC
|
|
208
|
+
ISC
|
|
209
|
+
|
|
210
|
+
## Visual Demonstrations
|
|
211
|
+
|
|
212
|
+
Midgard Hex Grid includes two complementary demonstrations.
|
|
213
|
+
|
|
214
|
+
The Minimal Demonstration shows how little TypeScript is needed to create a grid. The Theory Demonstration explains the coordinate system: parity, the skeleton, and the two orientations.
|
|
215
|
+
|
|
216
|
+
### Minimal Demonstration
|
|
217
|
+
|
|
218
|
+
A grid is created with `new HexGrid()` and `createGrid()`. Each example below sets `hexDiameter` to 120 and uses `skeletonWidth: 2` and `skeletonHeight: 2`. For an X-dominated grid, `hexDiameter` is the hexagon's width. For a Y-dominated grid, it is the hexagon's height.
|
|
219
|
+
|
|
220
|
+
`createGrid()` builds the complete grid around the skeleton and fills in the surrounding hexagons. These examples produce 17 hexagons. The four light coastal-blue hexagons are the skeleton, at (2,2), (4,2), (2,4), and (4,4). The cream hexagons are the surrounding fill. Both orientations use the same logical coordinates. Only the geometry changes.
|
|
221
|
+
|
|
222
|
+
**X-dominated Hex Grid**
|
|
223
|
+
|
|
224
|
+

|
|
225
|
+
|
|
226
|
+
**Y-dominated Hex Grid**
|
|
227
|
+
|
|
228
|
+

|
|
229
|
+
|
|
230
|
+
The functions below are the examples used by the Minimal Demonstration. Import `HexGrid` as shown in [Basic usage](#basic-usage).
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { HexGrid } from './dist/index.js'
|
|
234
|
+
|
|
235
|
+
function createXDominatedGrid() {
|
|
236
|
+
const hexDiameter = 120
|
|
237
|
+
const hexGrid = new HexGrid('x-dominated')
|
|
238
|
+
|
|
239
|
+
const hexagons = hexGrid.createGrid({
|
|
240
|
+
hexDiameter,
|
|
241
|
+
skeletonWidth: 2,
|
|
242
|
+
skeletonHeight: 2
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
return hexagons
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { HexGrid } from './dist/index.js'
|
|
251
|
+
|
|
252
|
+
function createYDominatedGrid() {
|
|
253
|
+
const hexDiameter = 120
|
|
254
|
+
const hexGrid = new HexGrid('y-dominated')
|
|
255
|
+
|
|
256
|
+
const hexagons = hexGrid.createGrid({
|
|
257
|
+
hexDiameter,
|
|
258
|
+
skeletonWidth: 2,
|
|
259
|
+
skeletonHeight: 2
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
return hexagons
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Theory Demonstration
|
|
267
|
+
|
|
268
|
+
The diagrams below show how a rectangular coordinate field becomes a hexagonal grid, and which movements are valid.
|
|
269
|
+
|
|
270
|
+
#### Coordinate parity
|
|
271
|
+
|
|
272
|
+
A valid Midgard coordinate is two even numbers, such as (2,2) or (4,4), or two odd numbers, such as (3,3) or (1,5). A mixed coordinate, such as (3,4), is invalid because its numbers have different parity.
|
|
273
|
+
|
|
274
|
+
#### The skeleton foundation
|
|
275
|
+
|
|
276
|
+
(2,2) is always the top-left skeleton hexagon. Every skeleton begins there, whatever the grid's size or orientation. A 1 × 1 skeleton contains only (2,2) as its skeleton hexagon. Increasing `skeletonWidth` or `skeletonHeight` extends the skeleton from that origin through further even coordinates, such as (4,2), (2,4), and (4,4).
|
|
277
|
+
|
|
278
|
+
#### Grid orientation
|
|
279
|
+
|
|
280
|
+
An X-dominated grid uses hexagons with pointed tops and bottoms. A Y-dominated grid uses hexagons with flat tops and bottoms. Both orientations follow the same parity rules, so a coordinate such as (2,2) keeps its logical identity when the geometry changes.
|
|
281
|
+
|
|
282
|
+
**X-dominated — Coordinate System and Neighbour Movement**
|
|
283
|
+
|
|
284
|
+

|
|
285
|
+
|
|
286
|
+
The left figure maps the rectangular coordinates onto the hexagons. The right figure shows the six valid neighbours of the center hexagon and marks movement in the Y direction as forbidden.
|
|
287
|
+
|
|
288
|
+
**Y-dominated — Coordinate System and Neighbour Movement**
|
|
289
|
+
|
|
290
|
+

|
|
291
|
+
|
|
292
|
+
The same relationship holds for the Y-dominated orientation. The forbidden movements are in the X direction.
|
|
293
|
+
|
|
294
|
+
### Explore the Demonstrations
|
|
295
|
+
|
|
296
|
+
The rendered pages are not published at a separate public address. The source of each page is in this repository:
|
|
297
|
+
|
|
298
|
+
- [Minimal Demonstration](demo/minimal.html)
|
|
299
|
+
- [Theory Demonstration](demo/index.html)
|
|
300
|
+
|
|
301
|
+
To run them locally, install the demo dependencies and start Vite from the `demo` directory. The `dev` script in `demo/package.json` runs `vite`.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
cd demo
|
|
305
|
+
npm install
|
|
306
|
+
npm run dev
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Vite prints a local address. The Theory Demonstration is `demo/index.html`, served at `/`. The Minimal Demonstration is `demo/minimal.html`.
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { HexGrid, type LayeredHexagon } from '../src/index.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ⬡ X-dominated Hex-grid
|
|
5
|
+
*
|
|
6
|
+
* The following TypeScript code generates an X-dominated hexagonal
|
|
7
|
+
* grid using the Midgard Hex Grid library.
|
|
8
|
+
*
|
|
9
|
+
* An X-dominated hexagon has a pointed top and a pointed bottom,
|
|
10
|
+
* with flat sides on the left and right.
|
|
11
|
+
*
|
|
12
|
+
* The `HexGrid` constructor selects the orientation. Passing
|
|
13
|
+
* `'x-dominated'` creates this pointy-top grid. The same class
|
|
14
|
+
* creates a flat-top grid when given `'y-dominated'`.
|
|
15
|
+
*
|
|
16
|
+
* - `hexDiameter` is the horizontal width of each hexagon, from the
|
|
17
|
+
* left flat side to the right flat side. This example uses 120.
|
|
18
|
+
* - `skeletonWidth` and `skeletonHeight` are the skeleton dimensions,
|
|
19
|
+
* counted in skeleton coordinates. This example uses 2 and 2.
|
|
20
|
+
*
|
|
21
|
+
* The skeleton is the rectangular core. It starts at coordinate
|
|
22
|
+
* (2, 2) and steps by two, so these dimensions are the four
|
|
23
|
+
* coordinates (2, 2), (4, 2), (2, 4), and (4, 4). `createGrid()`
|
|
24
|
+
* fills the surrounding hexagons automatically. A 2 × 2 skeleton
|
|
25
|
+
* generates 17 hexagons.
|
|
26
|
+
*
|
|
27
|
+
* Each hexagon in the returned array has its Midgard `coordinate`,
|
|
28
|
+
* its `center`, its six corner `points`, and a `zIndex`. The library
|
|
29
|
+
* does not generate SVG or HTML. Draw `points` with the graphics
|
|
30
|
+
* system the application already uses.
|
|
31
|
+
*
|
|
32
|
+
* Change `hexDiameter` to resize the hexagons. Change
|
|
33
|
+
* `skeletonWidth`, `skeletonHeight`, or both to create another grid
|
|
34
|
+
* size. The library recalculates the geometry.
|
|
35
|
+
*
|
|
36
|
+
* @returns The 17 hexagons of the complete grid, including the
|
|
37
|
+
* surrounding fill, with coordinates and geometry ready to render.
|
|
38
|
+
*/
|
|
39
|
+
function createXDominatedGrid() {
|
|
40
|
+
const hexDiameter = 120
|
|
41
|
+
const hexGrid = new HexGrid('x-dominated')
|
|
42
|
+
|
|
43
|
+
const hexagons = hexGrid.createGrid({
|
|
44
|
+
hexDiameter,
|
|
45
|
+
skeletonWidth: 2,
|
|
46
|
+
skeletonHeight: 2
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return hexagons
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ⬡ Y-dominated Hex-grid
|
|
54
|
+
*
|
|
55
|
+
* The following TypeScript code generates a Y-dominated hexagonal
|
|
56
|
+
* grid using the Midgard Hex Grid library.
|
|
57
|
+
*
|
|
58
|
+
* A Y-dominated hexagon has a flat top and a flat bottom, with
|
|
59
|
+
* points on the left and right.
|
|
60
|
+
*
|
|
61
|
+
* Both orientations use the same `HexGrid` class. The constructor
|
|
62
|
+
* argument selects the orientation: `'y-dominated'` here, and
|
|
63
|
+
* `'x-dominated'` in `createXDominatedGrid`.
|
|
64
|
+
*
|
|
65
|
+
* - For a Y-dominated grid, `hexDiameter` is the vertical hexagon
|
|
66
|
+
* height, from the flat top to the flat bottom. The library
|
|
67
|
+
* derives the horizontal width. This example uses 120.
|
|
68
|
+
* - `skeletonWidth` and `skeletonHeight` again describe a 2 × 2
|
|
69
|
+
* skeleton.
|
|
70
|
+
*
|
|
71
|
+
* `createGrid()` fills the surrounding hexagons automatically and
|
|
72
|
+
* calculates their geometry. The returned array contains 17 hexagons.
|
|
73
|
+
*
|
|
74
|
+
* Each hexagon carries its `coordinate`, `center`, `points`, and
|
|
75
|
+
* `zIndex`. The application decides how to render that geometry.
|
|
76
|
+
* The library does not generate SVG or HTML.
|
|
77
|
+
*
|
|
78
|
+
* Change `hexDiameter` to change the hexagon height, or change the
|
|
79
|
+
* skeleton dimensions to change the grid. Keep the constructor
|
|
80
|
+
* argument `'y-dominated'` to keep the flat-top orientation.
|
|
81
|
+
*
|
|
82
|
+
* @returns The 17 hexagons of the complete grid, including the
|
|
83
|
+
* surrounding fill, with coordinates and geometry ready to render.
|
|
84
|
+
*/
|
|
85
|
+
function createYDominatedGrid() {
|
|
86
|
+
const hexDiameter = 120
|
|
87
|
+
const hexGrid = new HexGrid('y-dominated')
|
|
88
|
+
|
|
89
|
+
const hexagons = hexGrid.createGrid({
|
|
90
|
+
hexDiameter,
|
|
91
|
+
skeletonWidth: 2,
|
|
92
|
+
skeletonHeight: 2
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return hexagons
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* =====================================================================
|
|
99
|
+
* PRESENTATION — SVG, HTML AND CSS
|
|
100
|
+
*
|
|
101
|
+
* The functions above generate the hexagons. Everything below only
|
|
102
|
+
* displays those results.
|
|
103
|
+
* ===================================================================== */
|
|
104
|
+
|
|
105
|
+
const xDominatedHexagons = createXDominatedGrid()
|
|
106
|
+
const yDominatedHexagons = createYDominatedGrid()
|
|
107
|
+
|
|
108
|
+
const xDominatedSource = `function createXDominatedGrid() {
|
|
109
|
+
const hexDiameter = 120
|
|
110
|
+
const hexGrid = new HexGrid('x-dominated')
|
|
111
|
+
|
|
112
|
+
const hexagons = hexGrid.createGrid({
|
|
113
|
+
hexDiameter,
|
|
114
|
+
skeletonWidth: 2,
|
|
115
|
+
skeletonHeight: 2
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return hexagons
|
|
119
|
+
}`
|
|
120
|
+
|
|
121
|
+
const yDominatedSource = `function createYDominatedGrid() {
|
|
122
|
+
const hexDiameter = 120
|
|
123
|
+
const hexGrid = new HexGrid('y-dominated')
|
|
124
|
+
|
|
125
|
+
const hexagons = hexGrid.createGrid({
|
|
126
|
+
hexDiameter,
|
|
127
|
+
skeletonWidth: 2,
|
|
128
|
+
skeletonHeight: 2
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return hexagons
|
|
132
|
+
}`
|
|
133
|
+
|
|
134
|
+
const styles = `
|
|
135
|
+
:root {
|
|
136
|
+
--page-navy: #0e1c2e;
|
|
137
|
+
--panel-parchment: #c7a47f;
|
|
138
|
+
--panel-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
|
139
|
+
--ink-strong: #2a1a0e;
|
|
140
|
+
--ink-body: #3d2918;
|
|
141
|
+
--ink-soft: #4a3120;
|
|
142
|
+
--rule-brown: #7a4e2d;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
html {
|
|
146
|
+
font-size: 17px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
body {
|
|
150
|
+
margin: 0;
|
|
151
|
+
min-height: 100vh;
|
|
152
|
+
background: var(--page-navy);
|
|
153
|
+
color: var(--ink-body);
|
|
154
|
+
font-family: Palatino, 'Palatino Linotype', 'Iowan Old Style', Georgia, serif;
|
|
155
|
+
line-height: 1.45;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.page {
|
|
159
|
+
padding: 32px 20px 48px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.sheet {
|
|
163
|
+
width: 100%;
|
|
164
|
+
max-width: 1200px;
|
|
165
|
+
margin: 0 auto;
|
|
166
|
+
padding: 48px 40px 56px;
|
|
167
|
+
background: var(--panel-parchment);
|
|
168
|
+
box-shadow: var(--panel-shadow);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
h1 {
|
|
172
|
+
margin: 0;
|
|
173
|
+
color: var(--ink-strong);
|
|
174
|
+
font-size: 40px;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
line-height: 1.15;
|
|
177
|
+
text-align: center;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.sheet__minimal {
|
|
181
|
+
display: block;
|
|
182
|
+
margin: 8px 0 0;
|
|
183
|
+
color: var(--ink-soft);
|
|
184
|
+
font-size: 18px;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
line-height: 1.25;
|
|
187
|
+
text-align: center;
|
|
188
|
+
text-decoration-line: underline;
|
|
189
|
+
text-decoration-thickness: 1px;
|
|
190
|
+
text-underline-offset: 0.2em;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.principle {
|
|
194
|
+
max-width: 820px;
|
|
195
|
+
margin: 36px auto;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.principle h2 {
|
|
199
|
+
margin: 0 0 16px;
|
|
200
|
+
color: var(--ink-strong);
|
|
201
|
+
font-size: 26px;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
line-height: 1.25;
|
|
204
|
+
text-align: center;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.principle p {
|
|
208
|
+
margin: 0 0 16px;
|
|
209
|
+
color: var(--ink-body);
|
|
210
|
+
line-height: 1.6;
|
|
211
|
+
text-align: left;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.principle p:last-child {
|
|
215
|
+
margin-bottom: 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.principle strong {
|
|
219
|
+
color: var(--ink-strong);
|
|
220
|
+
font-weight: 700;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.grid {
|
|
224
|
+
display: grid;
|
|
225
|
+
grid-template-columns: 1fr 1fr;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 40px;
|
|
228
|
+
margin-top: 36px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.grid__hex {
|
|
232
|
+
display: flex;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.grid polygon {
|
|
237
|
+
fill: #f4ead8;
|
|
238
|
+
stroke: #2a1a0e;
|
|
239
|
+
stroke-width: 2;
|
|
240
|
+
stroke-linejoin: round;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.grid polygon.skeleton {
|
|
244
|
+
fill: #d6f0f5;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.skeleton-label {
|
|
248
|
+
fill: #10283f;
|
|
249
|
+
font-family: Palatino, 'Palatino Linotype', 'Iowan Old Style', Georgia, serif;
|
|
250
|
+
text-anchor: middle;
|
|
251
|
+
dominant-baseline: central;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.skeleton-label__name {
|
|
255
|
+
font-size: 13px;
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.skeleton-label__coordinate {
|
|
260
|
+
font-size: 23px;
|
|
261
|
+
font-weight: 700;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.grid__doc h2 {
|
|
265
|
+
margin: 0 0 12px;
|
|
266
|
+
padding-bottom: 8px;
|
|
267
|
+
border-bottom: 1px solid var(--rule-brown);
|
|
268
|
+
color: var(--ink-strong);
|
|
269
|
+
font-size: 26px;
|
|
270
|
+
font-weight: 600;
|
|
271
|
+
line-height: 1.25;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.grid__doc p {
|
|
275
|
+
margin: 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.grid pre {
|
|
279
|
+
margin: 18px 0 0;
|
|
280
|
+
padding: 22px 24px;
|
|
281
|
+
overflow-x: auto;
|
|
282
|
+
border-radius: 10px;
|
|
283
|
+
background: var(--page-navy);
|
|
284
|
+
box-shadow: 0 10px 24px rgba(14, 28, 46, 0.18);
|
|
285
|
+
color: #f4ead8;
|
|
286
|
+
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
|
287
|
+
font-size: 15px;
|
|
288
|
+
line-height: 1.6;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.grid code {
|
|
292
|
+
font-family: inherit;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.tok-keyword {
|
|
296
|
+
color: #e7c98a;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.tok-string {
|
|
300
|
+
color: #c9e2a6;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.tok-number {
|
|
304
|
+
color: #9fd4f0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@media (max-width: 1100px) {
|
|
308
|
+
.grid {
|
|
309
|
+
grid-template-columns: 1fr;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@media (max-width: 980px) {
|
|
314
|
+
.sheet {
|
|
315
|
+
padding: 36px 22px 40px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
h1 {
|
|
319
|
+
font-size: 32px;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
`
|
|
323
|
+
|
|
324
|
+
const style = document.createElement('style')
|
|
325
|
+
style.textContent = styles
|
|
326
|
+
document.head.append(style)
|
|
327
|
+
|
|
328
|
+
const page = document.createElement('main')
|
|
329
|
+
page.className = 'page'
|
|
330
|
+
|
|
331
|
+
const sheet = document.createElement('article')
|
|
332
|
+
sheet.className = 'sheet'
|
|
333
|
+
|
|
334
|
+
const heading = document.createElement('h1')
|
|
335
|
+
heading.textContent = 'Midgard Hex Grid Minimal Demonstration'
|
|
336
|
+
|
|
337
|
+
const theory = document.createElement('a')
|
|
338
|
+
theory.className = 'sheet__minimal'
|
|
339
|
+
theory.href = '/index.html'
|
|
340
|
+
theory.textContent = 'Theory Demonstration'
|
|
341
|
+
|
|
342
|
+
const headingSection = document.createElement('div')
|
|
343
|
+
headingSection.append(heading, theory)
|
|
344
|
+
|
|
345
|
+
sheet.append(
|
|
346
|
+
headingSection,
|
|
347
|
+
createPrincipleSection(),
|
|
348
|
+
createDemonstration(
|
|
349
|
+
xDominatedHexagons,
|
|
350
|
+
'X-dominated Hex-grid',
|
|
351
|
+
'The following TypeScript code generates an X-dominated hexagonal grid using the Midgard Hex Grid library.',
|
|
352
|
+
xDominatedSource
|
|
353
|
+
),
|
|
354
|
+
createDemonstration(
|
|
355
|
+
yDominatedHexagons,
|
|
356
|
+
'Y-dominated Hex-grid',
|
|
357
|
+
'The following TypeScript code generates a Y-dominated hexagonal grid using the Midgard Hex Grid library.',
|
|
358
|
+
yDominatedSource
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
page.append(sheet)
|
|
362
|
+
document.body.prepend(page)
|
|
363
|
+
|
|
364
|
+
function createPrincipleSection() {
|
|
365
|
+
const section = document.createElement('section')
|
|
366
|
+
section.className = 'principle'
|
|
367
|
+
|
|
368
|
+
const heading = document.createElement('h2')
|
|
369
|
+
heading.textContent = '⬡ The Parity Principle & Grid Orientation'
|
|
370
|
+
|
|
371
|
+
const parity = document.createElement('p')
|
|
372
|
+
parity.append(
|
|
373
|
+
'Midgard Hex Grid is built on a simple mathematical principle called ',
|
|
374
|
+
strong('coordinate parity'),
|
|
375
|
+
'. Every valid hexagon coordinate consists of two numbers that are either both even or both odd. The even coordinates, such as (2,2), (4,2), (2,4), and (4,4), form the underlying ',
|
|
376
|
+
strong('skeleton grid'),
|
|
377
|
+
', while the odd coordinates, such as (3,3), (1,3), and (3,5), identify the surrounding hexagons that complete the grid. Mixed coordinates, such as (3,4) or (2,5), are never valid because their numbers have different parity.'
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
const foundation = document.createElement('p')
|
|
381
|
+
foundation.append(
|
|
382
|
+
strong('The foundation of every grid is the skeleton hexagon at coordinate (2,2).'),
|
|
383
|
+
' This is always the top-left skeleton hexagon and the fixed starting point from which the skeleton grid is constructed. Regardless of the grid\'s dimensions or orientation, the skeleton begins at (2,2). For example, when skeletonWidth: 1 and skeletonHeight: 1, the skeleton consists of exactly one hexagon: (2,2). Increasing the skeleton width or height extends the skeleton from this fixed origin, using even coordinates such as (4,2), (2,4), and (4,4). This makes (2,2) the permanent anchor of the Midgard coordinate system.'
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
const orientation = document.createElement('p')
|
|
387
|
+
orientation.append(
|
|
388
|
+
'The library supports two orientations: ',
|
|
389
|
+
strong('X-dominated'),
|
|
390
|
+
' and ',
|
|
391
|
+
strong('Y-dominated'),
|
|
392
|
+
'. X-dominated grids use hexagons with pointed tops and bottoms, while Y-dominated grids use hexagons with flat tops and bottoms. Both orientations follow exactly the same parity rules and coordinate system; only the geometric arrangement changes. This means that a hexagon at coordinate (2,2) retains its logical identity regardless of orientation.'
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
const separation = document.createElement('p')
|
|
396
|
+
separation.append(
|
|
397
|
+
'By separating ',
|
|
398
|
+
strong('logical coordinates from visual geometry'),
|
|
399
|
+
', Midgard Hex Grid provides a consistent foundation for creating, positioning, and navigating hexagonal grids in either orientation.'
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
section.append(heading, parity, foundation, orientation, separation)
|
|
403
|
+
return section
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function strong(text: string) {
|
|
407
|
+
const element = document.createElement('strong')
|
|
408
|
+
element.textContent = text
|
|
409
|
+
return element
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function createDemonstration(
|
|
413
|
+
hexagons: LayeredHexagon[],
|
|
414
|
+
title: string,
|
|
415
|
+
explanation: string,
|
|
416
|
+
source: string
|
|
417
|
+
) {
|
|
418
|
+
const section = document.createElement('section')
|
|
419
|
+
section.className = 'grid'
|
|
420
|
+
|
|
421
|
+
const figure = document.createElement('div')
|
|
422
|
+
figure.className = 'grid__hex'
|
|
423
|
+
figure.append(renderHexagons(hexagons))
|
|
424
|
+
|
|
425
|
+
const heading = document.createElement('h2')
|
|
426
|
+
heading.textContent = title
|
|
427
|
+
|
|
428
|
+
const text = document.createElement('p')
|
|
429
|
+
text.textContent = explanation
|
|
430
|
+
|
|
431
|
+
const code = document.createElement('code')
|
|
432
|
+
fillCode(code, source)
|
|
433
|
+
|
|
434
|
+
const listing = document.createElement('pre')
|
|
435
|
+
listing.append(code)
|
|
436
|
+
|
|
437
|
+
const doc = document.createElement('div')
|
|
438
|
+
doc.className = 'grid__doc'
|
|
439
|
+
doc.append(heading, text, listing)
|
|
440
|
+
|
|
441
|
+
section.append(figure, doc)
|
|
442
|
+
return section
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function fillCode(code: HTMLElement, source: string) {
|
|
446
|
+
const pattern = /'(?:\\.|[^'])*'|\b(?:const|function|new|return)\b|\b\d+\b/g
|
|
447
|
+
let cursor = 0
|
|
448
|
+
|
|
449
|
+
for (const match of source.matchAll(pattern)) {
|
|
450
|
+
const token = match[0]
|
|
451
|
+
const index = match.index
|
|
452
|
+
if (token === undefined || index === undefined) continue
|
|
453
|
+
if (index > cursor) code.append(source.slice(cursor, index))
|
|
454
|
+
|
|
455
|
+
const span = document.createElement('span')
|
|
456
|
+
span.className = token.startsWith("'")
|
|
457
|
+
? 'tok-string'
|
|
458
|
+
: /^\d+$/.test(token)
|
|
459
|
+
? 'tok-number'
|
|
460
|
+
: 'tok-keyword'
|
|
461
|
+
span.textContent = token
|
|
462
|
+
code.append(span)
|
|
463
|
+
cursor = index + token.length
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (cursor < source.length) code.append(source.slice(cursor))
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isSkeletonHexagon(hexagon: LayeredHexagon) {
|
|
470
|
+
const { x, y } = hexagon.coordinate
|
|
471
|
+
return (x === 2 && y === 2)
|
|
472
|
+
|| (x === 4 && y === 2)
|
|
473
|
+
|| (x === 2 && y === 4)
|
|
474
|
+
|| (x === 4 && y === 4)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function renderHexagons(hexagons: LayeredHexagon[]) {
|
|
478
|
+
const firstPoint = hexagons[0]?.points[0]
|
|
479
|
+
|
|
480
|
+
if (firstPoint === undefined) {
|
|
481
|
+
throw new Error('The generated grid did not contain a hexagon.')
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let minX = firstPoint.x
|
|
485
|
+
let minY = firstPoint.y
|
|
486
|
+
let maxX = firstPoint.x
|
|
487
|
+
let maxY = firstPoint.y
|
|
488
|
+
|
|
489
|
+
for (const hexagon of hexagons) {
|
|
490
|
+
for (const point of hexagon.points) {
|
|
491
|
+
if (point.x < minX) minX = point.x
|
|
492
|
+
if (point.y < minY) minY = point.y
|
|
493
|
+
if (point.x > maxX) maxX = point.x
|
|
494
|
+
if (point.y > maxY) maxY = point.y
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const pad = 2
|
|
499
|
+
const width = maxX - minX + pad * 2
|
|
500
|
+
const height = maxY - minY + pad * 2
|
|
501
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
502
|
+
svg.setAttribute('viewBox', `${minX - pad} ${minY - pad} ${width} ${height}`)
|
|
503
|
+
svg.setAttribute('width', String(width))
|
|
504
|
+
svg.setAttribute('height', String(height))
|
|
505
|
+
|
|
506
|
+
for (const hexagon of hexagons) {
|
|
507
|
+
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
|
|
508
|
+
polygon.setAttribute('points', hexagon.points.map((point) => `${point.x},${point.y}`).join(' '))
|
|
509
|
+
if (isSkeletonHexagon(hexagon)) polygon.classList.add('skeleton')
|
|
510
|
+
svg.append(polygon)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (const hexagon of hexagons) {
|
|
514
|
+
if (!isSkeletonHexagon(hexagon)) continue
|
|
515
|
+
svg.append(
|
|
516
|
+
skeletonText(hexagon.center.x, hexagon.center.y - 12, 'skeleton-label skeleton-label__name', 'skeleton hex'),
|
|
517
|
+
skeletonText(hexagon.center.x, hexagon.center.y + 12, 'skeleton-label skeleton-label__coordinate', `${hexagon.coordinate.x},${hexagon.coordinate.y}`)
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return svg
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function skeletonText(x: number, y: number, className: string, value: string) {
|
|
525
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
526
|
+
text.setAttribute('x', String(x))
|
|
527
|
+
text.setAttribute('y', String(y))
|
|
528
|
+
text.setAttribute('class', className)
|
|
529
|
+
text.textContent = value
|
|
530
|
+
return text
|
|
531
|
+
}
|