@astronox/core 0.5.5 → 0.5.7
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/package.json +1 -1
- package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.css +0 -79
- package/src/patterns/MagneticFieldPuzzle/magneticFieldPuzzleController.ts +14 -148
- package/src/patterns/PuzzleGrid/movement.css +80 -0
- package/src/patterns/PuzzleGrid/movement.ts +149 -0
- package/src/patterns/PuzzleGrid/selectionDismissal.ts +4 -2
- package/src/patterns/SlidingPuzzle/SlidingPuzzle.astro +185 -0
- package/src/patterns/SlidingPuzzle/SlidingPuzzle.css +186 -0
- package/src/patterns/SlidingPuzzle/contracts.ts +62 -0
- package/src/patterns/SlidingPuzzle/simulation.ts +106 -0
- package/src/patterns/SlidingPuzzle/slidingPuzzleController.ts +802 -0
- package/src/patterns/index.ts +4 -0
- package/src/styles/style.css +2 -0
package/package.json
CHANGED
|
@@ -148,57 +148,6 @@
|
|
|
148
148
|
overflow: visible;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
.magnetic-field-puzzle__path-stream {
|
|
152
|
-
--field-path-rotation: 0deg;
|
|
153
|
-
|
|
154
|
-
position: relative;
|
|
155
|
-
z-index: 3;
|
|
156
|
-
grid-row: var(--field-path-row);
|
|
157
|
-
grid-column: var(--field-path-column);
|
|
158
|
-
min-inline-size: 0;
|
|
159
|
-
min-block-size: 0;
|
|
160
|
-
overflow: visible;
|
|
161
|
-
pointer-events: none;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.magnetic-field-puzzle__path-chevron {
|
|
165
|
-
--field-path-chevron-opacity: 0.38;
|
|
166
|
-
|
|
167
|
-
position: absolute;
|
|
168
|
-
display: grid;
|
|
169
|
-
inset-block-start: var(--field-path-static-block);
|
|
170
|
-
inset-inline-start: var(--field-path-static-inline);
|
|
171
|
-
inline-size: 1em;
|
|
172
|
-
block-size: 1em;
|
|
173
|
-
place-items: center;
|
|
174
|
-
color: var(--nox-source-tone-success);
|
|
175
|
-
font-size: var(--nox-font-size-xl);
|
|
176
|
-
opacity: var(--field-path-chevron-opacity);
|
|
177
|
-
pointer-events: none;
|
|
178
|
-
transform: translate(-50%, -50%) rotate(var(--field-path-rotation));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.magnetic-field-puzzle__path-chevron::before {
|
|
182
|
-
content: '';
|
|
183
|
-
inline-size: 0.46em;
|
|
184
|
-
block-size: 0.46em;
|
|
185
|
-
border-block-start: calc(var(--nox-border-width) * 2) solid currentColor;
|
|
186
|
-
border-inline-end: calc(var(--nox-border-width) * 2) solid currentColor;
|
|
187
|
-
transform: rotate(45deg);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.magnetic-field-puzzle__path-stream[data-field-path-direction='north'] {
|
|
191
|
-
--field-path-rotation: -90deg;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.magnetic-field-puzzle__path-stream[data-field-path-direction='south'] {
|
|
195
|
-
--field-path-rotation: 90deg;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.magnetic-field-puzzle__path-stream[data-field-path-direction='west'] {
|
|
199
|
-
--field-path-rotation: 180deg;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
151
|
.magnetic-field-puzzle__piece {
|
|
203
152
|
--piece-pupil-block-position: 35%;
|
|
204
153
|
--piece-pupil-inline-position: 68%;
|
|
@@ -342,12 +291,6 @@
|
|
|
342
291
|
.magnetic-field-puzzle__piece[data-piece-moved] {
|
|
343
292
|
animation: magnetic-field-slide 260ms var(--nox-motion-easing-standard);
|
|
344
293
|
}
|
|
345
|
-
|
|
346
|
-
.magnetic-field-puzzle__path-chevron {
|
|
347
|
-
animation: magnetic-field-path var(--field-path-duration) linear infinite
|
|
348
|
-
both;
|
|
349
|
-
animation-delay: calc(var(--field-path-chevron-index) * -700ms);
|
|
350
|
-
}
|
|
351
294
|
}
|
|
352
295
|
|
|
353
296
|
@keyframes magnetic-field-slide {
|
|
@@ -359,25 +302,3 @@
|
|
|
359
302
|
);
|
|
360
303
|
}
|
|
361
304
|
}
|
|
362
|
-
|
|
363
|
-
@keyframes magnetic-field-path {
|
|
364
|
-
0% {
|
|
365
|
-
inset-block-start: 50%;
|
|
366
|
-
inset-inline-start: 50%;
|
|
367
|
-
opacity: 0;
|
|
368
|
-
transform: translate(-50%, -50%) rotate(var(--field-path-rotation))
|
|
369
|
-
scale(0.9);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
12%,
|
|
373
|
-
82% {
|
|
374
|
-
opacity: var(--field-path-chevron-opacity);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
100% {
|
|
378
|
-
inset-block-start: var(--field-path-target-block);
|
|
379
|
-
inset-inline-start: var(--field-path-target-inline);
|
|
380
|
-
opacity: 0;
|
|
381
|
-
transform: translate(-50%, -50%) rotate(var(--field-path-rotation)) scale(1);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
animatePieceMovement,
|
|
3
|
+
cancelDragFrame,
|
|
4
|
+
findDirectionalDropDestination,
|
|
5
|
+
hasLeftOriginCell,
|
|
6
|
+
renderPathStream,
|
|
7
|
+
scheduleDragFrame,
|
|
8
|
+
type PuzzleDrag,
|
|
9
|
+
} from '../PuzzleGrid/movement';
|
|
1
10
|
import {
|
|
2
11
|
setPuzzleGridCellState,
|
|
3
12
|
setPuzzleGridState,
|
|
@@ -5,7 +14,6 @@ import {
|
|
|
5
14
|
import { initPuzzleSelectionDismissal } from '../PuzzleGrid/selectionDismissal';
|
|
6
15
|
import type {
|
|
7
16
|
MagneticFieldDirection,
|
|
8
|
-
MagneticFieldMovement,
|
|
9
17
|
MagneticFieldPolarity,
|
|
10
18
|
MagneticFieldPupilPosition,
|
|
11
19
|
MagneticFieldPuzzleChangeDetail,
|
|
@@ -20,32 +28,14 @@ import {
|
|
|
20
28
|
simulateMagneticFieldMove,
|
|
21
29
|
} from './simulation';
|
|
22
30
|
|
|
23
|
-
type MagneticFieldDrag = {
|
|
24
|
-
pointerId: number;
|
|
25
|
-
pieceId: string;
|
|
26
|
-
startX: number;
|
|
27
|
-
startY: number;
|
|
28
|
-
currentX: number;
|
|
29
|
-
currentY: number;
|
|
30
|
-
frameId: number | null;
|
|
31
|
-
element: HTMLButtonElement;
|
|
32
|
-
originCellBounds: DOMRect;
|
|
33
|
-
originCenterX: number;
|
|
34
|
-
originCenterY: number;
|
|
35
|
-
destinations: readonly HTMLButtonElement[] | null;
|
|
36
|
-
moved: boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
31
|
type MagneticFieldInstance = {
|
|
40
32
|
activeIndex: number;
|
|
41
33
|
selectedPieceId: string | null;
|
|
42
|
-
drag:
|
|
34
|
+
drag: PuzzleDrag | null;
|
|
43
35
|
ignoreNextClick: boolean;
|
|
44
36
|
};
|
|
45
37
|
|
|
46
38
|
const dragThreshold = 12;
|
|
47
|
-
const pathChevronsPerCell = 3;
|
|
48
|
-
const pathDurationPerCellMs = 2100;
|
|
49
39
|
const instances = new WeakMap<HTMLElement, MagneticFieldInstance>();
|
|
50
40
|
|
|
51
41
|
export function initMagneticFieldPuzzle(root: HTMLElement): void {
|
|
@@ -155,7 +145,7 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
|
|
|
155
145
|
drag.destinations ??= getDropDestinations(root);
|
|
156
146
|
drag.currentX = event.clientX;
|
|
157
147
|
drag.currentY = event.clientY;
|
|
158
|
-
scheduleDragFrame(
|
|
148
|
+
scheduleDragFrame(drag, () => instances.get(root)?.drag === drag);
|
|
159
149
|
});
|
|
160
150
|
|
|
161
151
|
root.addEventListener('pointerup', (event) => {
|
|
@@ -295,7 +285,8 @@ function updateDestinations(root: HTMLElement): void {
|
|
|
295
285
|
|
|
296
286
|
for (const movement of getMagneticFieldMoves(puzzle, state, pieceId)) {
|
|
297
287
|
const directionLabel = readDirectionLabel(labels, movement.direction);
|
|
298
|
-
|
|
288
|
+
const board = panel.querySelector<HTMLElement>('[data-field-board]');
|
|
289
|
+
if (board) renderPathStream(board, movement);
|
|
299
290
|
|
|
300
291
|
const destination = findCell(panel, movement.to);
|
|
301
292
|
if (!destination) continue;
|
|
@@ -452,22 +443,6 @@ function renderState(
|
|
|
452
443
|
}
|
|
453
444
|
}
|
|
454
445
|
|
|
455
|
-
function animatePieceMovement(
|
|
456
|
-
element: HTMLButtonElement,
|
|
457
|
-
initialBounds: DOMRect,
|
|
458
|
-
): void {
|
|
459
|
-
const finalBounds = element.getBoundingClientRect();
|
|
460
|
-
element.style.setProperty(
|
|
461
|
-
'--piece-move-inline-distance',
|
|
462
|
-
`${initialBounds.left - finalBounds.left}px`,
|
|
463
|
-
);
|
|
464
|
-
element.style.setProperty(
|
|
465
|
-
'--piece-move-block-distance',
|
|
466
|
-
`${initialBounds.top - finalBounds.top}px`,
|
|
467
|
-
);
|
|
468
|
-
element.setAttribute('data-piece-moved', '');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
446
|
function finishDrag(
|
|
472
447
|
root: HTMLElement,
|
|
473
448
|
event: PointerEvent,
|
|
@@ -508,41 +483,6 @@ function finishDrag(
|
|
|
508
483
|
if (drag.moved) selectPiece(root, null);
|
|
509
484
|
}
|
|
510
485
|
|
|
511
|
-
function hasLeftOriginCell(
|
|
512
|
-
drag: MagneticFieldDrag,
|
|
513
|
-
deltaX: number,
|
|
514
|
-
deltaY: number,
|
|
515
|
-
): boolean {
|
|
516
|
-
const centerX = drag.originCenterX + deltaX;
|
|
517
|
-
const centerY = drag.originCenterY + deltaY;
|
|
518
|
-
const { bottom, left, right, top } = drag.originCellBounds;
|
|
519
|
-
|
|
520
|
-
return centerX < left || centerX > right || centerY < top || centerY > bottom;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function scheduleDragFrame(root: HTMLElement, drag: MagneticFieldDrag): void {
|
|
524
|
-
if (drag.frameId !== null) return;
|
|
525
|
-
drag.frameId = window.requestAnimationFrame(() => {
|
|
526
|
-
drag.frameId = null;
|
|
527
|
-
if (instances.get(root)?.drag !== drag) return;
|
|
528
|
-
|
|
529
|
-
drag.element.style.setProperty(
|
|
530
|
-
'--piece-drag-x',
|
|
531
|
-
`${drag.currentX - drag.startX}px`,
|
|
532
|
-
);
|
|
533
|
-
drag.element.style.setProperty(
|
|
534
|
-
'--piece-drag-y',
|
|
535
|
-
`${drag.currentY - drag.startY}px`,
|
|
536
|
-
);
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function cancelDragFrame(drag: MagneticFieldDrag): void {
|
|
541
|
-
if (drag.frameId === null) return;
|
|
542
|
-
window.cancelAnimationFrame(drag.frameId);
|
|
543
|
-
drag.frameId = null;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
486
|
function getDropDestinations(root: HTMLElement): HTMLButtonElement[] {
|
|
547
487
|
const panel = getActivePanel(root);
|
|
548
488
|
if (!panel) return [];
|
|
@@ -552,30 +492,9 @@ function getDropDestinations(root: HTMLElement): HTMLButtonElement[] {
|
|
|
552
492
|
];
|
|
553
493
|
}
|
|
554
494
|
|
|
555
|
-
function findDirectionalDropDestination(
|
|
556
|
-
drag: MagneticFieldDrag,
|
|
557
|
-
deltaX: number,
|
|
558
|
-
deltaY: number,
|
|
559
|
-
): HTMLButtonElement | null {
|
|
560
|
-
let direction: MagneticFieldDirection;
|
|
561
|
-
if (Math.abs(deltaX) >= Math.abs(deltaY)) {
|
|
562
|
-
direction = deltaX >= 0 ? 'east' : 'west';
|
|
563
|
-
} else {
|
|
564
|
-
direction = deltaY >= 0 ? 'south' : 'north';
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return (
|
|
568
|
-
(drag.destinations ?? []).find(
|
|
569
|
-
(destination) =>
|
|
570
|
-
destination.dataset.destinationDirection === direction &&
|
|
571
|
-
destination.dataset.destinationPiece === drag.pieceId,
|
|
572
|
-
) ?? null
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
495
|
function hideMovePreviews(panel: HTMLElement): void {
|
|
577
496
|
for (const stream of panel.querySelectorAll<HTMLElement>(
|
|
578
|
-
'[data-
|
|
497
|
+
'[data-puzzle-path-stream]',
|
|
579
498
|
)) {
|
|
580
499
|
stream.remove();
|
|
581
500
|
}
|
|
@@ -597,59 +516,6 @@ function hideMovePreviews(panel: HTMLElement): void {
|
|
|
597
516
|
}
|
|
598
517
|
}
|
|
599
518
|
|
|
600
|
-
function renderPathStream(
|
|
601
|
-
panel: HTMLElement,
|
|
602
|
-
movement: MagneticFieldMovement,
|
|
603
|
-
): void {
|
|
604
|
-
const board = panel.querySelector<HTMLElement>('[data-field-board]');
|
|
605
|
-
if (!board) return;
|
|
606
|
-
|
|
607
|
-
const rowDistance = movement.to.row - movement.from.row;
|
|
608
|
-
const columnDistance = movement.to.column - movement.from.column;
|
|
609
|
-
const cellDistance = Math.abs(rowDistance) + Math.abs(columnDistance);
|
|
610
|
-
const chevronCount = pathChevronsPerCell * cellDistance;
|
|
611
|
-
const stream = document.createElement('span');
|
|
612
|
-
stream.className = 'magnetic-field-puzzle__path-stream';
|
|
613
|
-
stream.dataset.fieldPathStream = '';
|
|
614
|
-
stream.dataset.fieldPathDirection = movement.direction;
|
|
615
|
-
stream.setAttribute('aria-hidden', 'true');
|
|
616
|
-
stream.style.setProperty('--field-path-row', String(movement.from.row + 1));
|
|
617
|
-
stream.style.setProperty(
|
|
618
|
-
'--field-path-column',
|
|
619
|
-
String(movement.from.column + 1),
|
|
620
|
-
);
|
|
621
|
-
stream.style.setProperty(
|
|
622
|
-
'--field-path-target-inline',
|
|
623
|
-
`${50 + columnDistance * 100}%`,
|
|
624
|
-
);
|
|
625
|
-
stream.style.setProperty(
|
|
626
|
-
'--field-path-target-block',
|
|
627
|
-
`${50 + rowDistance * 100}%`,
|
|
628
|
-
);
|
|
629
|
-
stream.style.setProperty(
|
|
630
|
-
'--field-path-duration',
|
|
631
|
-
`${pathDurationPerCellMs * cellDistance}ms`,
|
|
632
|
-
);
|
|
633
|
-
|
|
634
|
-
for (let index = 0; index < chevronCount; index += 1) {
|
|
635
|
-
const progress = (index + 0.5) / chevronCount;
|
|
636
|
-
const chevron = document.createElement('span');
|
|
637
|
-
chevron.className = 'magnetic-field-puzzle__path-chevron';
|
|
638
|
-
chevron.style.setProperty('--field-path-chevron-index', String(index));
|
|
639
|
-
chevron.style.setProperty(
|
|
640
|
-
'--field-path-static-inline',
|
|
641
|
-
`${50 + columnDistance * progress * 100}%`,
|
|
642
|
-
);
|
|
643
|
-
chevron.style.setProperty(
|
|
644
|
-
'--field-path-static-block',
|
|
645
|
-
`${50 + rowDistance * progress * 100}%`,
|
|
646
|
-
);
|
|
647
|
-
stream.append(chevron);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
board.append(stream);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
519
|
function findCell(
|
|
654
520
|
panel: HTMLElement,
|
|
655
521
|
position: { row: number; column: number },
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.puzzle-grid__path-stream {
|
|
2
|
+
--puzzle-path-rotation: 0deg;
|
|
3
|
+
|
|
4
|
+
position: relative;
|
|
5
|
+
z-index: 3;
|
|
6
|
+
grid-row: var(--puzzle-path-row);
|
|
7
|
+
grid-column: var(--puzzle-path-column);
|
|
8
|
+
min-inline-size: 0;
|
|
9
|
+
min-block-size: 0;
|
|
10
|
+
overflow: visible;
|
|
11
|
+
pointer-events: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.puzzle-grid__path-chevron {
|
|
15
|
+
--puzzle-path-chevron-opacity: 0.38;
|
|
16
|
+
|
|
17
|
+
position: absolute;
|
|
18
|
+
display: grid;
|
|
19
|
+
inset-block-start: var(--puzzle-path-static-block);
|
|
20
|
+
inset-inline-start: var(--puzzle-path-static-inline);
|
|
21
|
+
inline-size: 1em;
|
|
22
|
+
block-size: 1em;
|
|
23
|
+
place-items: center;
|
|
24
|
+
color: var(--nox-source-tone-success);
|
|
25
|
+
font-size: var(--nox-font-size-xl);
|
|
26
|
+
opacity: var(--puzzle-path-chevron-opacity);
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
transform: translate(-50%, -50%) rotate(var(--puzzle-path-rotation));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.puzzle-grid__path-chevron::before {
|
|
32
|
+
content: '';
|
|
33
|
+
inline-size: 0.46em;
|
|
34
|
+
block-size: 0.46em;
|
|
35
|
+
border-block-start: calc(var(--nox-border-width) * 2) solid currentColor;
|
|
36
|
+
border-inline-end: calc(var(--nox-border-width) * 2) solid currentColor;
|
|
37
|
+
transform: rotate(45deg);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.puzzle-grid__path-stream[data-puzzle-path-direction='north'] {
|
|
41
|
+
--puzzle-path-rotation: -90deg;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.puzzle-grid__path-stream[data-puzzle-path-direction='south'] {
|
|
45
|
+
--puzzle-path-rotation: 90deg;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.puzzle-grid__path-stream[data-puzzle-path-direction='west'] {
|
|
49
|
+
--puzzle-path-rotation: 180deg;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
53
|
+
.puzzle-grid__path-chevron {
|
|
54
|
+
animation: puzzle-grid-path var(--puzzle-path-duration) linear infinite both;
|
|
55
|
+
animation-delay: calc(var(--puzzle-path-chevron-index) * -700ms);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@keyframes puzzle-grid-path {
|
|
60
|
+
0% {
|
|
61
|
+
inset-block-start: 50%;
|
|
62
|
+
inset-inline-start: 50%;
|
|
63
|
+
opacity: 0;
|
|
64
|
+
transform: translate(-50%, -50%) rotate(var(--puzzle-path-rotation))
|
|
65
|
+
scale(0.9);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
12%,
|
|
69
|
+
82% {
|
|
70
|
+
opacity: var(--puzzle-path-chevron-opacity);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
100% {
|
|
74
|
+
inset-block-start: var(--puzzle-path-target-block);
|
|
75
|
+
inset-inline-start: var(--puzzle-path-target-inline);
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transform: translate(-50%, -50%) rotate(var(--puzzle-path-rotation))
|
|
78
|
+
scale(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export type PuzzleDrag = {
|
|
2
|
+
pointerId: number;
|
|
3
|
+
pieceId: string;
|
|
4
|
+
startX: number;
|
|
5
|
+
startY: number;
|
|
6
|
+
currentX: number;
|
|
7
|
+
currentY: number;
|
|
8
|
+
frameId: number | null;
|
|
9
|
+
element: HTMLButtonElement;
|
|
10
|
+
originCellBounds: DOMRect;
|
|
11
|
+
originCenterX: number;
|
|
12
|
+
originCenterY: number;
|
|
13
|
+
destinations: readonly HTMLButtonElement[] | null;
|
|
14
|
+
moved: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Direction = 'north' | 'east' | 'south' | 'west';
|
|
18
|
+
|
|
19
|
+
const pathChevronsPerCell = 3;
|
|
20
|
+
const pathDurationPerCellMs = 2100;
|
|
21
|
+
|
|
22
|
+
export function animatePieceMovement(
|
|
23
|
+
element: HTMLButtonElement,
|
|
24
|
+
initialBounds: DOMRect,
|
|
25
|
+
): void {
|
|
26
|
+
const finalBounds = element.getBoundingClientRect();
|
|
27
|
+
element.style.setProperty(
|
|
28
|
+
'--piece-move-inline-distance',
|
|
29
|
+
`${initialBounds.left - finalBounds.left}px`,
|
|
30
|
+
);
|
|
31
|
+
element.style.setProperty(
|
|
32
|
+
'--piece-move-block-distance',
|
|
33
|
+
`${initialBounds.top - finalBounds.top}px`,
|
|
34
|
+
);
|
|
35
|
+
element.setAttribute('data-piece-moved', '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hasLeftOriginCell(
|
|
39
|
+
drag: PuzzleDrag,
|
|
40
|
+
deltaX: number,
|
|
41
|
+
deltaY: number,
|
|
42
|
+
): boolean {
|
|
43
|
+
const centerX = drag.originCenterX + deltaX;
|
|
44
|
+
const centerY = drag.originCenterY + deltaY;
|
|
45
|
+
const { bottom, left, right, top } = drag.originCellBounds;
|
|
46
|
+
|
|
47
|
+
return centerX < left || centerX > right || centerY < top || centerY > bottom;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function scheduleDragFrame(
|
|
51
|
+
drag: PuzzleDrag,
|
|
52
|
+
isCurrent: () => boolean,
|
|
53
|
+
): void {
|
|
54
|
+
if (drag.frameId !== null) return;
|
|
55
|
+
drag.frameId = window.requestAnimationFrame(() => {
|
|
56
|
+
drag.frameId = null;
|
|
57
|
+
if (!isCurrent()) return;
|
|
58
|
+
|
|
59
|
+
drag.element.style.setProperty(
|
|
60
|
+
'--piece-drag-x',
|
|
61
|
+
`${drag.currentX - drag.startX}px`,
|
|
62
|
+
);
|
|
63
|
+
drag.element.style.setProperty(
|
|
64
|
+
'--piece-drag-y',
|
|
65
|
+
`${drag.currentY - drag.startY}px`,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function cancelDragFrame(drag: PuzzleDrag): void {
|
|
71
|
+
if (drag.frameId === null) return;
|
|
72
|
+
window.cancelAnimationFrame(drag.frameId);
|
|
73
|
+
drag.frameId = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function findDirectionalDropDestination(
|
|
77
|
+
drag: PuzzleDrag,
|
|
78
|
+
deltaX: number,
|
|
79
|
+
deltaY: number,
|
|
80
|
+
): HTMLButtonElement | null {
|
|
81
|
+
let direction: Direction;
|
|
82
|
+
if (Math.abs(deltaX) >= Math.abs(deltaY)) {
|
|
83
|
+
direction = deltaX >= 0 ? 'east' : 'west';
|
|
84
|
+
} else {
|
|
85
|
+
direction = deltaY >= 0 ? 'south' : 'north';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
(drag.destinations ?? []).find(
|
|
90
|
+
(destination) =>
|
|
91
|
+
destination.dataset.destinationDirection === direction &&
|
|
92
|
+
destination.dataset.destinationPiece === drag.pieceId,
|
|
93
|
+
) ?? null
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderPathStream(
|
|
98
|
+
board: HTMLElement,
|
|
99
|
+
movement: {
|
|
100
|
+
from: { row: number; column: number };
|
|
101
|
+
to: { row: number; column: number };
|
|
102
|
+
direction: Direction;
|
|
103
|
+
},
|
|
104
|
+
): void {
|
|
105
|
+
const rowDistance = movement.to.row - movement.from.row;
|
|
106
|
+
const columnDistance = movement.to.column - movement.from.column;
|
|
107
|
+
const cellDistance = Math.abs(rowDistance) + Math.abs(columnDistance);
|
|
108
|
+
const chevronCount = pathChevronsPerCell * cellDistance;
|
|
109
|
+
const stream = document.createElement('span');
|
|
110
|
+
stream.className = 'puzzle-grid__path-stream';
|
|
111
|
+
stream.dataset.puzzlePathStream = '';
|
|
112
|
+
stream.dataset.puzzlePathDirection = movement.direction;
|
|
113
|
+
stream.setAttribute('aria-hidden', 'true');
|
|
114
|
+
stream.style.setProperty('--puzzle-path-row', String(movement.from.row + 1));
|
|
115
|
+
stream.style.setProperty(
|
|
116
|
+
'--puzzle-path-column',
|
|
117
|
+
String(movement.from.column + 1),
|
|
118
|
+
);
|
|
119
|
+
stream.style.setProperty(
|
|
120
|
+
'--puzzle-path-target-inline',
|
|
121
|
+
`${50 + columnDistance * 100}%`,
|
|
122
|
+
);
|
|
123
|
+
stream.style.setProperty(
|
|
124
|
+
'--puzzle-path-target-block',
|
|
125
|
+
`${50 + rowDistance * 100}%`,
|
|
126
|
+
);
|
|
127
|
+
stream.style.setProperty(
|
|
128
|
+
'--puzzle-path-duration',
|
|
129
|
+
`${pathDurationPerCellMs * cellDistance}ms`,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
for (let index = 0; index < chevronCount; index += 1) {
|
|
133
|
+
const progress = (index + 0.5) / chevronCount;
|
|
134
|
+
const chevron = document.createElement('span');
|
|
135
|
+
chevron.className = 'puzzle-grid__path-chevron';
|
|
136
|
+
chevron.style.setProperty('--puzzle-path-chevron-index', String(index));
|
|
137
|
+
chevron.style.setProperty(
|
|
138
|
+
'--puzzle-path-static-inline',
|
|
139
|
+
`${50 + columnDistance * progress * 100}%`,
|
|
140
|
+
);
|
|
141
|
+
chevron.style.setProperty(
|
|
142
|
+
'--puzzle-path-static-block',
|
|
143
|
+
`${50 + rowDistance * progress * 100}%`,
|
|
144
|
+
);
|
|
145
|
+
stream.append(chevron);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
board.append(stream);
|
|
149
|
+
}
|
|
@@ -6,7 +6,8 @@ type PuzzleSelectionDismissalOptions = {
|
|
|
6
6
|
export function initPuzzleSelectionDismissal(
|
|
7
7
|
root: HTMLElement,
|
|
8
8
|
{ dismiss, isSelectionTarget }: PuzzleSelectionDismissalOptions,
|
|
9
|
-
): void {
|
|
9
|
+
): () => void {
|
|
10
|
+
const controller = new AbortController();
|
|
10
11
|
document.addEventListener(
|
|
11
12
|
'pointerdown',
|
|
12
13
|
(event) => {
|
|
@@ -22,6 +23,7 @@ export function initPuzzleSelectionDismissal(
|
|
|
22
23
|
focused.blur();
|
|
23
24
|
}
|
|
24
25
|
},
|
|
25
|
-
true,
|
|
26
|
+
{ capture: true, signal: controller.signal },
|
|
26
27
|
);
|
|
28
|
+
return () => controller.abort();
|
|
27
29
|
}
|