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.
Files changed (49) hide show
  1. package/README.md +102 -1
  2. package/demo/demo-minimal.ts +531 -0
  3. package/demo/demo-theory.ts +1679 -0
  4. package/demo/index.html +11 -0
  5. package/demo/minimal.html +11 -0
  6. package/demo/package-lock.json +1162 -0
  7. package/demo/package.json +11 -0
  8. package/demo/vite.config.ts +9 -0
  9. package/dist/coordinate-layer.d.ts +1 -11
  10. package/dist/coordinate-layer.d.ts.map +1 -1
  11. package/dist/coordinate-layer.js +5 -5
  12. package/dist/coordinate-layer.js.map +1 -1
  13. package/dist/coordinate-positioner.d.ts +1 -17
  14. package/dist/coordinate-positioner.d.ts.map +1 -1
  15. package/dist/coordinate-positioner.js +7 -7
  16. package/dist/coordinate-positioner.js.map +1 -1
  17. package/dist/coordinate-range-fill.d.ts +1 -20
  18. package/dist/coordinate-range-fill.d.ts.map +1 -1
  19. package/dist/coordinate-range-fill.js +12 -10
  20. package/dist/coordinate-range-fill.js.map +1 -1
  21. package/dist/coordinate-range.d.ts +16 -7
  22. package/dist/coordinate-range.d.ts.map +1 -1
  23. package/dist/coordinate-range.js +30 -13
  24. package/dist/coordinate-range.js.map +1 -1
  25. package/dist/geometry.d.ts +1 -25
  26. package/dist/geometry.d.ts.map +1 -1
  27. package/dist/geometry.js +7 -7
  28. package/dist/geometry.js.map +1 -1
  29. package/dist/h-g-bounds.d.ts +8 -5
  30. package/dist/h-g-bounds.d.ts.map +1 -1
  31. package/dist/h-g-bounds.js +12 -10
  32. package/dist/h-g-bounds.js.map +1 -1
  33. package/dist/h-g-hexagon.d.ts +21 -7
  34. package/dist/h-g-hexagon.d.ts.map +1 -1
  35. package/dist/h-g-hexagon.js +34 -12
  36. package/dist/h-g-hexagon.js.map +1 -1
  37. package/dist/hex-grid.d.ts +5 -1
  38. package/dist/hex-grid.d.ts.map +1 -1
  39. package/dist/hex-grid.js +32 -15
  40. package/dist/hex-grid.js.map +1 -1
  41. package/dist/neighbours.d.ts +1 -23
  42. package/dist/neighbours.d.ts.map +1 -1
  43. package/dist/neighbours.js +7 -7
  44. package/dist/neighbours.js.map +1 -1
  45. package/docs/images/minimal-x.png +0 -0
  46. package/docs/images/minimal-y.png +0 -0
  47. package/docs/images/theory-x.png +0 -0
  48. package/docs/images/theory-y.png +0 -0
  49. 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
+ ![X-dominated grid of 17 pointy-top hexagons. The skeleton hexagons (2,2), (4,2), (2,4), and (4,4) are light coastal blue and labeled. The createXDominatedGrid example is shown beside the grid.](docs/images/minimal-x.png)
225
+
226
+ **Y-dominated Hex Grid**
227
+
228
+ ![Y-dominated grid of 17 flat-top hexagons. The same skeleton coordinates (2,2), (4,2), (2,4), and (4,4) are light coastal blue and labeled. The createYDominatedGrid example is shown beside the grid.](docs/images/minimal-y.png)
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
+ ![X-dominated theory diagram. A rectangular coordinate field stands beside a pointy-top hex grid, showing the six valid neighbours of the center and the forbidden vertical movements.](docs/images/theory-x.png)
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
+ ![Y-dominated theory diagram. A rectangular coordinate field stands beside a flat-top hex grid, showing the six valid neighbours of the center and the forbidden horizontal movements.](docs/images/theory-y.png)
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
+ }