@hkdigital/lib-core 0.5.10 → 0.5.11
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/dist/config/generators/imagetools.js +63 -0
- package/dist/config/imagetools.d.ts +4 -46
- package/dist/ui/components/game-box/GameBox.svelte +24 -70
- package/dist/ui/components/game-box/GameBox.svelte.d.ts +0 -5
- package/dist/ui/components/game-box/README.md +16 -0
- package/package.json +1 -1
- package/dist/ui/components/game-box/GameBox.svelte.with-scaling__ +0 -577
|
@@ -2,6 +2,25 @@ const DEFAULT_WIDTHS = [1920, 1536, 1024, 640];
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_THUMBNAIL_WIDTH = 150;
|
|
4
4
|
|
|
5
|
+
const FAVICON_SIZES = [
|
|
6
|
+
16, // classic browser tab icon
|
|
7
|
+
32, // high-resolution browser support
|
|
8
|
+
48, // Windows desktop shortcuts
|
|
9
|
+
120, // iPhone older retina
|
|
10
|
+
152, // iPad retina, iOS Safari bookmarks
|
|
11
|
+
167, // iPad Pro
|
|
12
|
+
180, // iPhone retina, iOS home screen
|
|
13
|
+
192, // Android home screen, Chrome PWA
|
|
14
|
+
512 // PWA application icon, Android splash
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const APPLE_TOUCH_SIZES = [
|
|
18
|
+
120, // iPhone older retina
|
|
19
|
+
152, // iPad retina, iOS Safari bookmarks
|
|
20
|
+
167, // iPad Pro
|
|
21
|
+
180 // iPhone retina, iOS home screen
|
|
22
|
+
];
|
|
23
|
+
|
|
5
24
|
const DEFAULT_PRESETS = {
|
|
6
25
|
default: {
|
|
7
26
|
format: 'avif',
|
|
@@ -74,9 +93,15 @@ export function generateResponseConfigs(options) {
|
|
|
74
93
|
|
|
75
94
|
// @ts-ignore
|
|
76
95
|
const responsiveConfig = entries.find(([key]) => key === 'responsive');
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
const faviconsConfig = entries.find(([key]) => key === 'favicons');
|
|
98
|
+
// @ts-ignore
|
|
99
|
+
const appleTouchIconsConfig = entries.find(([key]) => key === 'apple-touch-icons');
|
|
77
100
|
// console.log('responsiveConfig found:', !!responsiveConfig);
|
|
78
101
|
|
|
79
102
|
const widths = options?.widths ?? DEFAULT_WIDTHS;
|
|
103
|
+
const faviconSizes = options?.faviconSizes ?? FAVICON_SIZES;
|
|
104
|
+
const appleTouchSizes = options?.appleTouchSizes ?? APPLE_TOUCH_SIZES;
|
|
80
105
|
|
|
81
106
|
// Always include the main image(s) and a thumbnail version
|
|
82
107
|
const thumbnailConfig = {
|
|
@@ -84,6 +109,32 @@ export function generateResponseConfigs(options) {
|
|
|
84
109
|
w: String(options?.thumbnailWidth ?? DEFAULT_THUMBNAIL_WIDTH)
|
|
85
110
|
};
|
|
86
111
|
|
|
112
|
+
// Handle favicons directive - generate all favicon sizes as PNG
|
|
113
|
+
if (faviconsConfig) {
|
|
114
|
+
const faviconConfigs = faviconSizes.map((w) => {
|
|
115
|
+
return {
|
|
116
|
+
...configPairs,
|
|
117
|
+
w: String(w),
|
|
118
|
+
format: 'png'
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
// console.log('Returning favicon configs:', faviconConfigs);
|
|
122
|
+
return faviconConfigs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle apple-touch-icons directive - generate Apple touch icon sizes as PNG
|
|
126
|
+
if (appleTouchIconsConfig) {
|
|
127
|
+
const appleTouchConfigs = appleTouchSizes.map((w) => {
|
|
128
|
+
return {
|
|
129
|
+
...configPairs,
|
|
130
|
+
w: String(w),
|
|
131
|
+
format: 'png'
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
// console.log('Returning apple-touch-icon configs:', appleTouchConfigs);
|
|
135
|
+
return appleTouchConfigs;
|
|
136
|
+
}
|
|
137
|
+
|
|
87
138
|
if (!responsiveConfig) {
|
|
88
139
|
// Directive 'responsive' was not set => return original + thumbnail
|
|
89
140
|
const originalConfig = configPairs; // No 'w' means original dimensions
|
|
@@ -129,6 +180,18 @@ export function generateDefaultDirectives(options) {
|
|
|
129
180
|
params.set('as', 'metadata');
|
|
130
181
|
}
|
|
131
182
|
|
|
183
|
+
// > Return metadata if directive 'favicons' is set
|
|
184
|
+
|
|
185
|
+
if (params.has('favicons')) {
|
|
186
|
+
params.set('as', 'metadata');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// > Return metadata if directive 'apple-touch-icons' is set
|
|
190
|
+
|
|
191
|
+
if (params.has('apple-touch-icons')) {
|
|
192
|
+
params.set('as', 'metadata');
|
|
193
|
+
}
|
|
194
|
+
|
|
132
195
|
// > Process presets
|
|
133
196
|
|
|
134
197
|
if (presetName) {
|
|
@@ -73,56 +73,14 @@ declare module '*&preset=blur' {
|
|
|
73
73
|
|
|
74
74
|
/* For favicons */
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
declare module '*?
|
|
76
|
+
// Generate all favicon sizes (16, 32, 48, 120, 152, 167, 180, 192, 512)
|
|
77
|
+
declare module '*?favicons' {
|
|
78
78
|
const out: ImageSource;
|
|
79
79
|
export default out;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
declare module '*?
|
|
84
|
-
const out: ImageSource;
|
|
85
|
-
export default out;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Windows desktop shortcuts
|
|
89
|
-
declare module '*?w=48' {
|
|
90
|
-
const out: ImageSource;
|
|
91
|
-
export default out;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// iPhone older retina
|
|
95
|
-
declare module '*?w=120' {
|
|
96
|
-
const out: ImageSource;
|
|
97
|
-
export default out;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// iPad retina, iOS Safari bookmarks
|
|
101
|
-
declare module '*?w=152' {
|
|
102
|
-
const out: ImageSource;
|
|
103
|
-
export default out;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// iPad Pro
|
|
107
|
-
declare module '*?w=167' {
|
|
108
|
-
const out: ImageSource;
|
|
109
|
-
export default out;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// iPhone retina, iOS home screen
|
|
113
|
-
declare module '*?w=180' {
|
|
114
|
-
const out: ImageSource;
|
|
115
|
-
export default out;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Android home screen, Chrome PWA
|
|
119
|
-
declare module '*?w=192' {
|
|
120
|
-
const out: ImageSource;
|
|
121
|
-
export default out;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// PWA application icon, Android splash
|
|
125
|
-
declare module '*?w=512' {
|
|
82
|
+
// Generate Apple touch icon sizes (120, 152, 167, 180)
|
|
83
|
+
declare module '*?apple-touch-icons' {
|
|
126
84
|
const out: ImageSource;
|
|
127
85
|
export default out;
|
|
128
86
|
}
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
* requestDevmode:function,
|
|
22
22
|
* requestFullscreen:function,
|
|
23
23
|
* gameWidth: number,
|
|
24
|
-
* gameHeight: number
|
|
25
|
-
* iosLandscapeHeightQuirk: boolean
|
|
24
|
+
* gameHeight: number
|
|
26
25
|
* }} SnippetParams
|
|
27
26
|
*/
|
|
28
27
|
|
|
@@ -134,34 +133,6 @@
|
|
|
134
133
|
let isIos = $derived(os === 'iOS');
|
|
135
134
|
let isAndroid = $derived(os === 'Android');
|
|
136
135
|
|
|
137
|
-
/**
|
|
138
|
-
* Detect iOS landscape height quirk (status bar appears/disappears)
|
|
139
|
-
*
|
|
140
|
-
* Detection: in landscape, innerHeight is ~20px less than screen.width
|
|
141
|
-
*
|
|
142
|
-
* NOTE: This detection is reactive and will update when dimensions change.
|
|
143
|
-
* iOS PWA only fires viewport resize events when there's scrollable
|
|
144
|
-
* overflow content, which we add via CSS ::after when quirk is detected.
|
|
145
|
-
*/
|
|
146
|
-
let iosLandscapeHeightQuirk = $derived.by(() => {
|
|
147
|
-
// Force reactivity by accessing these variables
|
|
148
|
-
const currentLandscape = isLandscape;
|
|
149
|
-
const currentIos = isIos;
|
|
150
|
-
const width = iosWindowWidth ?? windowWidth;
|
|
151
|
-
const height = iosWindowHeight ?? windowHeight;
|
|
152
|
-
|
|
153
|
-
if (!currentLandscape || !currentIos) return false;
|
|
154
|
-
|
|
155
|
-
if (!width || !height || !window.screen) return false;
|
|
156
|
-
|
|
157
|
-
// In landscape: window.innerHeight should equal screen.width
|
|
158
|
-
// If it's 20px less, the quirk is active
|
|
159
|
-
const screenWidth = window.screen.width;
|
|
160
|
-
const heightDifference = screenWidth - height;
|
|
161
|
-
|
|
162
|
-
return heightDifference >= 18 && heightDifference <= 22;
|
|
163
|
-
});
|
|
164
|
-
|
|
165
136
|
// Debounce window dimensions on iOS to skip intermediate resize events
|
|
166
137
|
let skipNextResize = false;
|
|
167
138
|
let resetTimer;
|
|
@@ -343,29 +314,23 @@
|
|
|
343
314
|
const html = document.documentElement;
|
|
344
315
|
html.classList.add(gameBoxNoScroll);
|
|
345
316
|
|
|
346
|
-
// Prevent page scroll while allowing child elements to scroll
|
|
347
|
-
const preventPageScroll = () => {
|
|
348
|
-
|
|
349
|
-
};
|
|
317
|
+
// // Prevent page scroll while allowing child elements to scroll
|
|
318
|
+
// const preventPageScroll = () => {
|
|
319
|
+
// window.scrollTo(0, 0);
|
|
320
|
+
// };
|
|
350
321
|
|
|
351
|
-
window.addEventListener('scroll', preventPageScroll, { passive: true });
|
|
322
|
+
// window.addEventListener('scroll', preventPageScroll, { passive: true });
|
|
352
323
|
|
|
324
|
+
// return () => {
|
|
325
|
+
// html.classList.remove(gameBoxNoScroll);
|
|
326
|
+
// window.removeEventListener('scroll', preventPageScroll);
|
|
327
|
+
// };
|
|
353
328
|
return () => {
|
|
354
329
|
html.classList.remove(gameBoxNoScroll);
|
|
355
|
-
window.removeEventListener('scroll', preventPageScroll);
|
|
356
|
-
};
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// Toggle overflow content based on quirk detection
|
|
360
|
-
$effect(() => {
|
|
361
|
-
const html = document.documentElement;
|
|
362
|
-
if (iosLandscapeHeightQuirk) {
|
|
363
|
-
html.classList.add('ios-height-quirck');
|
|
364
|
-
} else {
|
|
365
|
-
html.classList.remove('ios-height-quirck');
|
|
366
330
|
}
|
|
367
331
|
});
|
|
368
332
|
|
|
333
|
+
|
|
369
334
|
function getOS() {
|
|
370
335
|
if (isAppleMobile) {
|
|
371
336
|
return 'iOS';
|
|
@@ -520,8 +485,7 @@
|
|
|
520
485
|
requestDevmode,
|
|
521
486
|
requestFullscreen,
|
|
522
487
|
gameWidth,
|
|
523
|
-
gameHeight
|
|
524
|
-
iosLandscapeHeightQuirk
|
|
488
|
+
gameHeight
|
|
525
489
|
})}
|
|
526
490
|
</ScaledContainer>
|
|
527
491
|
<!-- Portrait content -->
|
|
@@ -545,8 +509,7 @@
|
|
|
545
509
|
requestDevmode,
|
|
546
510
|
requestFullscreen,
|
|
547
511
|
gameWidth,
|
|
548
|
-
gameHeight
|
|
549
|
-
iosLandscapeHeightQuirk
|
|
512
|
+
gameHeight
|
|
550
513
|
})}
|
|
551
514
|
</ScaledContainer>
|
|
552
515
|
{:else if supportsFullscreen && !isDevMode}
|
|
@@ -570,8 +533,7 @@
|
|
|
570
533
|
requestDevmode,
|
|
571
534
|
requestFullscreen,
|
|
572
535
|
gameWidth,
|
|
573
|
-
gameHeight
|
|
574
|
-
iosLandscapeHeightQuirk
|
|
536
|
+
gameHeight
|
|
575
537
|
})}
|
|
576
538
|
</ScaledContainer>
|
|
577
539
|
{:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
|
|
@@ -595,8 +557,7 @@
|
|
|
595
557
|
requestDevmode,
|
|
596
558
|
requestFullscreen,
|
|
597
559
|
gameWidth,
|
|
598
|
-
gameHeight
|
|
599
|
-
iosLandscapeHeightQuirk
|
|
560
|
+
gameHeight
|
|
600
561
|
})}
|
|
601
562
|
</ScaledContainer>
|
|
602
563
|
{:else}
|
|
@@ -621,8 +582,7 @@
|
|
|
621
582
|
requestDevmode,
|
|
622
583
|
requestFullscreen,
|
|
623
584
|
gameWidth,
|
|
624
|
-
gameHeight
|
|
625
|
-
iosLandscapeHeightQuirk
|
|
585
|
+
gameHeight
|
|
626
586
|
})}
|
|
627
587
|
</ScaledContainer>
|
|
628
588
|
<!-- Portrait content -->
|
|
@@ -646,8 +606,7 @@
|
|
|
646
606
|
requestDevmode,
|
|
647
607
|
requestFullscreen,
|
|
648
608
|
gameWidth,
|
|
649
|
-
gameHeight
|
|
650
|
-
iosLandscapeHeightQuirk
|
|
609
|
+
gameHeight
|
|
651
610
|
})}
|
|
652
611
|
</ScaledContainer>
|
|
653
612
|
{/if}
|
|
@@ -674,8 +633,7 @@
|
|
|
674
633
|
requestDevmode,
|
|
675
634
|
requestFullscreen,
|
|
676
635
|
gameWidth,
|
|
677
|
-
gameHeight
|
|
678
|
-
iosLandscapeHeightQuirk
|
|
636
|
+
gameHeight
|
|
679
637
|
})}
|
|
680
638
|
</ScaledContainer>
|
|
681
639
|
<!-- Portrait content -->
|
|
@@ -699,8 +657,7 @@
|
|
|
699
657
|
requestDevmode,
|
|
700
658
|
requestFullscreen,
|
|
701
659
|
gameWidth,
|
|
702
|
-
gameHeight
|
|
703
|
-
iosLandscapeHeightQuirk
|
|
660
|
+
gameHeight
|
|
704
661
|
})}
|
|
705
662
|
</ScaledContainer>
|
|
706
663
|
{/if}
|
|
@@ -720,16 +677,13 @@
|
|
|
720
677
|
}
|
|
721
678
|
|
|
722
679
|
:global(html.game-box-no-scroll) {
|
|
723
|
-
|
|
680
|
+
/* Prevent all scrolling - clip is stricter than hidden */
|
|
681
|
+
overflow: clip;
|
|
724
682
|
scrollbar-width: none; /* Firefox */
|
|
725
683
|
-ms-overflow-style: none; /* IE and Edge */
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
content: '\A'; /* newline character */
|
|
730
|
-
white-space: pre; /* preserve newline */
|
|
731
|
-
display: block;
|
|
732
|
-
height: 20px; /* create overflow to trigger iOS resize events */
|
|
684
|
+
/* Prevent bounce/overscroll on iOS */
|
|
685
|
+
overscroll-behavior: none;
|
|
686
|
+
-webkit-overflow-scrolling: auto;
|
|
733
687
|
}
|
|
734
688
|
:global(html.game-box-no-scroll::-webkit-scrollbar) {
|
|
735
689
|
display: none;
|
|
@@ -100,7 +100,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
100
100
|
requestFullscreen: Function;
|
|
101
101
|
gameWidth: number;
|
|
102
102
|
gameHeight: number;
|
|
103
|
-
iosLandscapeHeightQuirk: boolean;
|
|
104
103
|
}]>;
|
|
105
104
|
snippetPortrait?: import("svelte").Snippet<[{
|
|
106
105
|
isLandscape: boolean;
|
|
@@ -115,7 +114,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
115
114
|
requestFullscreen: Function;
|
|
116
115
|
gameWidth: number;
|
|
117
116
|
gameHeight: number;
|
|
118
|
-
iosLandscapeHeightQuirk: boolean;
|
|
119
117
|
}]>;
|
|
120
118
|
snippetRequireFullscreen?: import("svelte").Snippet<[{
|
|
121
119
|
isLandscape: boolean;
|
|
@@ -130,7 +128,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
130
128
|
requestFullscreen: Function;
|
|
131
129
|
gameWidth: number;
|
|
132
130
|
gameHeight: number;
|
|
133
|
-
iosLandscapeHeightQuirk: boolean;
|
|
134
131
|
}]>;
|
|
135
132
|
snippetInstallOnHomeScreen?: import("svelte").Snippet<[{
|
|
136
133
|
isLandscape: boolean;
|
|
@@ -145,7 +142,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
145
142
|
requestFullscreen: Function;
|
|
146
143
|
gameWidth: number;
|
|
147
144
|
gameHeight: number;
|
|
148
|
-
iosLandscapeHeightQuirk: boolean;
|
|
149
145
|
}]>;
|
|
150
146
|
}, {}, "">;
|
|
151
147
|
type SnippetParams = {
|
|
@@ -161,5 +157,4 @@ type SnippetParams = {
|
|
|
161
157
|
requestFullscreen: Function;
|
|
162
158
|
gameWidth: number;
|
|
163
159
|
gameHeight: number;
|
|
164
|
-
iosLandscapeHeightQuirk: boolean;
|
|
165
160
|
};
|
|
@@ -248,6 +248,22 @@ The component includes special handling for mobile PWAs:
|
|
|
248
248
|
- **Screen Orientation**: Listens for orientation changes and updates layout
|
|
249
249
|
- **No Scroll**: Automatically prevents scrolling when active
|
|
250
250
|
|
|
251
|
+
### iOS Landscape Height Bug
|
|
252
|
+
|
|
253
|
+
For games and fullscreen applications, use `viewport-fit=cover` in your
|
|
254
|
+
viewport meta tag to prevent iOS landscape height bugs:
|
|
255
|
+
|
|
256
|
+
```html
|
|
257
|
+
<meta name="viewport"
|
|
258
|
+
content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no,
|
|
259
|
+
width=device-width, height=device-height, viewport-fit=cover" />
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
This is handled automatically by the `PWA.svelte` component in the `(meta)`
|
|
263
|
+
folder when `disablePageZoom` is enabled. The `viewport-fit=cover` setting
|
|
264
|
+
ensures the viewport extends into safe areas on iOS devices, preventing a
|
|
265
|
+
common bug where landscape mode shows incorrect viewport heights.
|
|
266
|
+
|
|
251
267
|
## Development Mode
|
|
252
268
|
|
|
253
269
|
The component automatically enables development mode when:
|
package/package.json
CHANGED
|
@@ -1,577 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
getGameWidthOnLandscape,
|
|
6
|
-
getGameWidthOnPortrait
|
|
7
|
-
} from './gamebox.util.js';
|
|
8
|
-
|
|
9
|
-
import { enableContainerScaling } from '$lib/design/index.js';
|
|
10
|
-
// import { enableContainerScaling } from '@hkdigital/lib-core/design/index.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {{
|
|
14
|
-
* isMobile:boolean,
|
|
15
|
-
* os:'Android'|'iOS',
|
|
16
|
-
* isFullscreen:boolean,
|
|
17
|
-
* isDevMode:boolean,
|
|
18
|
-
* requestDevmode:function,
|
|
19
|
-
* requestFullscreen:function,
|
|
20
|
-
* gameWidth: number,
|
|
21
|
-
* gameHeight: number
|
|
22
|
-
* }} SnippetParams
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {import('svelte').Snippet<[SnippetParams]>} GameBoxSnippet
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @type {{
|
|
31
|
-
* base?: string,
|
|
32
|
-
* bg?: string,
|
|
33
|
-
* classes?: string,
|
|
34
|
-
* style?: string,
|
|
35
|
-
* aspectOnLandscape?: number,
|
|
36
|
-
* aspectOnPortrait?: number,
|
|
37
|
-
* marginLeft?: number,
|
|
38
|
-
* marginRight?: number,
|
|
39
|
-
* marginTop?: number,
|
|
40
|
-
* marginBottom?: number,
|
|
41
|
-
* center?: boolean,
|
|
42
|
-
* enableScaling?: boolean,
|
|
43
|
-
* designLandscape?: {width: number, height: number},
|
|
44
|
-
* designPortrait?: {width: number, height: number},
|
|
45
|
-
* clamping?: {
|
|
46
|
-
* ui: {min: number, max: number},
|
|
47
|
-
* textBase: {min: number, max: number},
|
|
48
|
-
* textHeading: {min: number, max: number},
|
|
49
|
-
* textUi: {min: number, max: number}
|
|
50
|
-
* },
|
|
51
|
-
* snippetLandscape?:GameBoxSnippet,
|
|
52
|
-
* snippetPortrait?: GameBoxSnippet,
|
|
53
|
-
* snippetRequireFullscreen?: GameBoxSnippet,
|
|
54
|
-
* snippetInstallOnHomeScreen?: GameBoxSnippet,
|
|
55
|
-
* [attr: string]: any
|
|
56
|
-
* }}
|
|
57
|
-
*/
|
|
58
|
-
const {
|
|
59
|
-
// > Style
|
|
60
|
-
base = '',
|
|
61
|
-
bg = '',
|
|
62
|
-
classes = '',
|
|
63
|
-
style = '',
|
|
64
|
-
|
|
65
|
-
// > Functional properties
|
|
66
|
-
aspectOnLandscape,
|
|
67
|
-
aspectOnPortrait,
|
|
68
|
-
|
|
69
|
-
marginLeft = 0,
|
|
70
|
-
marginRight = 0,
|
|
71
|
-
|
|
72
|
-
marginTop = 0,
|
|
73
|
-
marginBottom = 0,
|
|
74
|
-
|
|
75
|
-
center,
|
|
76
|
-
|
|
77
|
-
// > Scaling options
|
|
78
|
-
enableScaling = false,
|
|
79
|
-
designLandscape = { width: 1920, height: 1080 },
|
|
80
|
-
designPortrait = { width: 1920, height: 1080 },
|
|
81
|
-
clamping = {
|
|
82
|
-
ui: { min: 0.3, max: 2 },
|
|
83
|
-
textBase: { min: 0.75, max: 1.5 },
|
|
84
|
-
textHeading: { min: 0.75, max: 2.25 },
|
|
85
|
-
textUi: { min: 0.5, max: 1.25 }
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
// > Snippets
|
|
89
|
-
snippetLandscape,
|
|
90
|
-
snippetPortrait,
|
|
91
|
-
snippetRequireFullscreen,
|
|
92
|
-
snippetInstallOnHomeScreen
|
|
93
|
-
} = $props();
|
|
94
|
-
|
|
95
|
-
// > Game dimensions and state
|
|
96
|
-
let windowWidth = $state();
|
|
97
|
-
let windowHeight = $state();
|
|
98
|
-
|
|
99
|
-
let gameWidth = $state();
|
|
100
|
-
let gameHeight = $state();
|
|
101
|
-
|
|
102
|
-
let iosWindowWidth = $state();
|
|
103
|
-
let iosWindowHeight = $state();
|
|
104
|
-
|
|
105
|
-
function getIsLandscape() {
|
|
106
|
-
if (isPwa && isAppleMobile) {
|
|
107
|
-
return iosWindowWidth > iosWindowHeight;
|
|
108
|
-
} else {
|
|
109
|
-
return windowWidth > windowHeight;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let isLandscape = $state();
|
|
114
|
-
|
|
115
|
-
// $derived.by(getIsLandscape);
|
|
116
|
-
|
|
117
|
-
$effect(() => {
|
|
118
|
-
isLandscape = getIsLandscape();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Game container reference
|
|
122
|
-
let gameContainer = $state();
|
|
123
|
-
|
|
124
|
-
// Update game dimensions based on window size and orientation
|
|
125
|
-
$effect(() => {
|
|
126
|
-
const width = iosWindowWidth ?? windowWidth;
|
|
127
|
-
const height = iosWindowHeight ?? windowHeight;
|
|
128
|
-
|
|
129
|
-
if (!width || !height) return;
|
|
130
|
-
|
|
131
|
-
const availWidth = width - marginLeft - marginRight;
|
|
132
|
-
const availHeight = height - marginTop - marginBottom;
|
|
133
|
-
|
|
134
|
-
// console.debug('GameBox margins:', {
|
|
135
|
-
// marginLeft,
|
|
136
|
-
// marginRight,
|
|
137
|
-
// marginTop,
|
|
138
|
-
// marginBottom
|
|
139
|
-
// });
|
|
140
|
-
|
|
141
|
-
let gameAspect;
|
|
142
|
-
|
|
143
|
-
if (availWidth > availHeight) {
|
|
144
|
-
gameWidth = getGameWidthOnLandscape({
|
|
145
|
-
windowWidth: availWidth,
|
|
146
|
-
windowHeight: availHeight,
|
|
147
|
-
aspectOnLandscape
|
|
148
|
-
});
|
|
149
|
-
gameAspect = aspectOnLandscape;
|
|
150
|
-
} else {
|
|
151
|
-
gameWidth = getGameWidthOnPortrait({
|
|
152
|
-
windowWidth: availWidth,
|
|
153
|
-
windowHeight: availHeight,
|
|
154
|
-
aspectOnPortrait
|
|
155
|
-
});
|
|
156
|
-
gameAspect = aspectOnPortrait;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (gameAspect) {
|
|
160
|
-
gameHeight = gameWidth / gameAspect;
|
|
161
|
-
} else {
|
|
162
|
-
gameHeight = availHeight;
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Set up scaling if enabled, with orientation awareness
|
|
167
|
-
$effect(() => {
|
|
168
|
-
if (!enableScaling || !gameContainer || !gameWidth || !gameHeight) {
|
|
169
|
-
return () => {}; // No-op cleanup if scaling not enabled or required elements missing
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Select the appropriate design based on orientation
|
|
173
|
-
const activeDesign = isLandscape ? designLandscape : designPortrait;
|
|
174
|
-
|
|
175
|
-
// console.debug(
|
|
176
|
-
// `GameBox scaling [${isLandscape ? 'landscape' : 'portrait'}]:`,
|
|
177
|
-
// `game: ${gameWidth}x${gameHeight}`,
|
|
178
|
-
// `design: ${activeDesign.width}x${activeDesign.height}`
|
|
179
|
-
// );
|
|
180
|
-
|
|
181
|
-
// Apply scaling with the current design based on orientation
|
|
182
|
-
return enableContainerScaling({
|
|
183
|
-
container: gameContainer,
|
|
184
|
-
design: activeDesign,
|
|
185
|
-
clamping,
|
|
186
|
-
getDimensions: () => ({
|
|
187
|
-
width: gameWidth,
|
|
188
|
-
height: gameHeight
|
|
189
|
-
})
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
let show = $state(false);
|
|
194
|
-
|
|
195
|
-
const isAppleMobile = /iPhone|iPod/.test(navigator.userAgent);
|
|
196
|
-
|
|
197
|
-
let isPwa = $state(false);
|
|
198
|
-
|
|
199
|
-
let os = $state();
|
|
200
|
-
|
|
201
|
-
let isMobile = $state(false);
|
|
202
|
-
|
|
203
|
-
let isDevMode = $state(false);
|
|
204
|
-
|
|
205
|
-
// Check: always true for home app?
|
|
206
|
-
let isFullscreen = $state(false);
|
|
207
|
-
|
|
208
|
-
let supportsFullscreen = $state(false);
|
|
209
|
-
|
|
210
|
-
onMount(() => {
|
|
211
|
-
supportsFullscreen = document.fullscreenEnabled;
|
|
212
|
-
|
|
213
|
-
isMobile = getIsMobile();
|
|
214
|
-
|
|
215
|
-
os = getOS();
|
|
216
|
-
|
|
217
|
-
// Run before show
|
|
218
|
-
isFullscreen = !!document.fullscreenElement;
|
|
219
|
-
|
|
220
|
-
isPwa = window.matchMedia(
|
|
221
|
-
'(display-mode: fullscreen) or (display-mode: standalone)'
|
|
222
|
-
).matches;
|
|
223
|
-
|
|
224
|
-
isLandscape = getIsLandscape();
|
|
225
|
-
|
|
226
|
-
show = true;
|
|
227
|
-
|
|
228
|
-
function updateIosWidthHeight() {
|
|
229
|
-
// const isPwa = window.matchMedia(
|
|
230
|
-
// '(display-mode: fullscreen) or (display-mode: standalone)'
|
|
231
|
-
// ).matches;
|
|
232
|
-
|
|
233
|
-
if (isPwa && isAppleMobile) {
|
|
234
|
-
const angle = screen.orientation.angle;
|
|
235
|
-
|
|
236
|
-
if (angle === 90 || angle === 270) {
|
|
237
|
-
iosWindowWidth = screen.height;
|
|
238
|
-
iosWindowHeight = screen.width;
|
|
239
|
-
} else {
|
|
240
|
-
iosWindowWidth = screen.width;
|
|
241
|
-
iosWindowHeight = screen.height;
|
|
242
|
-
}
|
|
243
|
-
// console.debug( { iosWindowWidth, iosWindowHeight } );
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
updateIosWidthHeight();
|
|
248
|
-
|
|
249
|
-
function updateOrientation(event) {
|
|
250
|
-
// console.debug('updateOrientation');
|
|
251
|
-
const type = event.target.type;
|
|
252
|
-
const angle = event.target.angle;
|
|
253
|
-
|
|
254
|
-
// isPwa = window.matchMedia(
|
|
255
|
-
// '(display-mode: fullscreen) or (display-mode: standalone)'
|
|
256
|
-
// ).matches;
|
|
257
|
-
|
|
258
|
-
updateIosWidthHeight();
|
|
259
|
-
|
|
260
|
-
console.debug(
|
|
261
|
-
`ScreenOrientation change: ${type}, ${angle} degrees.`,
|
|
262
|
-
isPwa,
|
|
263
|
-
windowWidth,
|
|
264
|
-
windowHeight,
|
|
265
|
-
screen.width,
|
|
266
|
-
screen.height,
|
|
267
|
-
iosWindowWidth,
|
|
268
|
-
iosWindowHeight
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
// if( angle
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
$effect(() => {
|
|
275
|
-
screen.orientation.addEventListener('change', updateOrientation);
|
|
276
|
-
|
|
277
|
-
return () => {
|
|
278
|
-
screen.orientation.removeEventListener('change', updateOrientation);
|
|
279
|
-
};
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
onMount(() => {
|
|
286
|
-
const gameBoxNoScroll = 'game-box-no-scroll';
|
|
287
|
-
const html = document.documentElement;
|
|
288
|
-
html.classList.add(gameBoxNoScroll);
|
|
289
|
-
|
|
290
|
-
return () => {
|
|
291
|
-
html.classList.remove(gameBoxNoScroll);
|
|
292
|
-
};
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
function getOS() {
|
|
296
|
-
if (isAppleMobile) {
|
|
297
|
-
return 'iOS';
|
|
298
|
-
} else if (/Android/.test(navigator.userAgent)) {
|
|
299
|
-
return 'Android';
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Returns true if a device is a mobile phone (or similar)
|
|
305
|
-
*/
|
|
306
|
-
function getIsMobile() {
|
|
307
|
-
// @ts-ignore
|
|
308
|
-
if (navigator?.userAgentData?.mobile !== undefined) {
|
|
309
|
-
// Supports for mobile flag
|
|
310
|
-
// @ts-ignore
|
|
311
|
-
return navigator.userAgentData.mobile;
|
|
312
|
-
} else if (isAppleMobile) {
|
|
313
|
-
return true;
|
|
314
|
-
} else if (/Android/.test(navigator.userAgent)) {
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Returns true if the window is in full screen
|
|
323
|
-
* - Checks if CSS thinks we're in fullscreen mode
|
|
324
|
-
* - Checks if there is a fullscreen element (for safari)
|
|
325
|
-
*/
|
|
326
|
-
function getIsFullscreen() {
|
|
327
|
-
if (
|
|
328
|
-
window.matchMedia(
|
|
329
|
-
'(display-mode: fullscreen) or (display-mode: standalone)'
|
|
330
|
-
).matches
|
|
331
|
-
) {
|
|
332
|
-
return true;
|
|
333
|
-
} else if (document.fullscreenElement) {
|
|
334
|
-
// Safari
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async function requestFullscreen() {
|
|
342
|
-
console.debug('Request full screen');
|
|
343
|
-
show = false;
|
|
344
|
-
|
|
345
|
-
await document.documentElement.requestFullscreen();
|
|
346
|
-
isFullscreen = true;
|
|
347
|
-
|
|
348
|
-
setTimeout(() => {
|
|
349
|
-
show = true;
|
|
350
|
-
}, 1000);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// async function exitFullscreen() {
|
|
354
|
-
// console.debug('Exit full screen');
|
|
355
|
-
// show = false;
|
|
356
|
-
|
|
357
|
-
// await document.exitFullscreen();
|
|
358
|
-
// isFullscreen = false;
|
|
359
|
-
|
|
360
|
-
// setTimeout( () => { show = true; }, 1000 );
|
|
361
|
-
// }
|
|
362
|
-
|
|
363
|
-
$effect(() => {
|
|
364
|
-
// Update isFullscreen if window width or height changes
|
|
365
|
-
|
|
366
|
-
windowWidth;
|
|
367
|
-
windowHeight;
|
|
368
|
-
|
|
369
|
-
isFullscreen = getIsFullscreen();
|
|
370
|
-
|
|
371
|
-
// if( !isFullscreen )
|
|
372
|
-
// {
|
|
373
|
-
// show = false;
|
|
374
|
-
// setTimeout( () => { show = true; }, 1000 );
|
|
375
|
-
// }
|
|
376
|
-
|
|
377
|
-
// console.debug('isFullscreen', isFullscreen);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
isDevMode = false;
|
|
381
|
-
|
|
382
|
-
function requestDevmode() {
|
|
383
|
-
isDevMode = true;
|
|
384
|
-
console.debug(isDevMode);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
$effect(() => {
|
|
388
|
-
if (location.hostname === 'localhost') {
|
|
389
|
-
isDevMode = true;
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
$effect(() => {
|
|
394
|
-
if (isFullscreen) {
|
|
395
|
-
const url = new URL(window.location.href);
|
|
396
|
-
url.searchParams.set('preset', 'cinema');
|
|
397
|
-
window.history.pushState({}, '', url);
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
</script>
|
|
401
|
-
|
|
402
|
-
<svelte:window bind:innerWidth={windowWidth} bind:innerHeight={windowHeight} />
|
|
403
|
-
|
|
404
|
-
{#if gameHeight}
|
|
405
|
-
<div class:center>
|
|
406
|
-
<div
|
|
407
|
-
data-component="game-box"
|
|
408
|
-
data-orientation={isLandscape ? 'landscape' : 'portrait'}
|
|
409
|
-
bind:this={gameContainer}
|
|
410
|
-
class="{base} {bg} {classes}"
|
|
411
|
-
class:isMobile
|
|
412
|
-
style:width="{gameWidth}px"
|
|
413
|
-
style:height="{gameHeight}px"
|
|
414
|
-
style:--game-width={gameWidth}
|
|
415
|
-
style:--game-height={gameHeight}
|
|
416
|
-
style:margin-left="{marginLeft}px"
|
|
417
|
-
style:margin-right="{marginRight}px"
|
|
418
|
-
style:margin-top="{marginTop}px"
|
|
419
|
-
style:margin-bottom="{marginBottom}px"
|
|
420
|
-
{style}
|
|
421
|
-
>
|
|
422
|
-
{#if show}
|
|
423
|
-
{#if isLandscape}
|
|
424
|
-
<!-- Landscape -->
|
|
425
|
-
{#if snippetRequireFullscreen}
|
|
426
|
-
<!-- Require fullscreen -->
|
|
427
|
-
{#if isFullscreen && !isDevMode}
|
|
428
|
-
{@render snippetLandscape({
|
|
429
|
-
isMobile,
|
|
430
|
-
os,
|
|
431
|
-
isFullscreen,
|
|
432
|
-
isDevMode,
|
|
433
|
-
requestDevmode,
|
|
434
|
-
requestFullscreen,
|
|
435
|
-
gameWidth,
|
|
436
|
-
gameHeight
|
|
437
|
-
})}
|
|
438
|
-
{:else if supportsFullscreen && !isDevMode}
|
|
439
|
-
<!-- Require fullscreen (on landscape) -->
|
|
440
|
-
{@render snippetRequireFullscreen({
|
|
441
|
-
isMobile,
|
|
442
|
-
os,
|
|
443
|
-
isFullscreen,
|
|
444
|
-
isDevMode,
|
|
445
|
-
requestDevmode,
|
|
446
|
-
requestFullscreen,
|
|
447
|
-
gameWidth,
|
|
448
|
-
gameHeight
|
|
449
|
-
})}
|
|
450
|
-
{:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
|
|
451
|
-
<!-- Require install on home screen on mobile -->
|
|
452
|
-
{@render snippetInstallOnHomeScreen({
|
|
453
|
-
isMobile,
|
|
454
|
-
os,
|
|
455
|
-
isFullscreen,
|
|
456
|
-
isDevMode,
|
|
457
|
-
requestDevmode,
|
|
458
|
-
requestFullscreen,
|
|
459
|
-
gameWidth,
|
|
460
|
-
gameHeight
|
|
461
|
-
})}
|
|
462
|
-
{:else}
|
|
463
|
-
{@render snippetLandscape({
|
|
464
|
-
isMobile,
|
|
465
|
-
os,
|
|
466
|
-
isFullscreen,
|
|
467
|
-
isDevMode,
|
|
468
|
-
requestDevmode,
|
|
469
|
-
requestFullscreen,
|
|
470
|
-
gameWidth,
|
|
471
|
-
gameHeight
|
|
472
|
-
})}
|
|
473
|
-
{/if}
|
|
474
|
-
{:else}
|
|
475
|
-
<!-- Do not require fullscreen -->
|
|
476
|
-
<!-- *we do not try install home app -->
|
|
477
|
-
{@render snippetLandscape({
|
|
478
|
-
isMobile,
|
|
479
|
-
os,
|
|
480
|
-
isFullscreen,
|
|
481
|
-
isDevMode,
|
|
482
|
-
requestDevmode,
|
|
483
|
-
requestFullscreen,
|
|
484
|
-
gameWidth,
|
|
485
|
-
gameHeight
|
|
486
|
-
})}
|
|
487
|
-
{/if}
|
|
488
|
-
{:else}
|
|
489
|
-
<!-- Portrait -->
|
|
490
|
-
{#if snippetRequireFullscreen}
|
|
491
|
-
<!-- Require fullscreen -->
|
|
492
|
-
{#if isFullscreen && !isDevMode}
|
|
493
|
-
{@render snippetPortrait({
|
|
494
|
-
isMobile,
|
|
495
|
-
os,
|
|
496
|
-
isFullscreen,
|
|
497
|
-
isDevMode,
|
|
498
|
-
requestDevmode,
|
|
499
|
-
requestFullscreen,
|
|
500
|
-
gameWidth,
|
|
501
|
-
gameHeight
|
|
502
|
-
})}
|
|
503
|
-
{:else if supportsFullscreen && !isDevMode}
|
|
504
|
-
<!-- Require fullscreen (on landscape) -->
|
|
505
|
-
{@render snippetRequireFullscreen({
|
|
506
|
-
isMobile,
|
|
507
|
-
os,
|
|
508
|
-
isFullscreen,
|
|
509
|
-
isDevMode,
|
|
510
|
-
requestDevmode,
|
|
511
|
-
requestFullscreen,
|
|
512
|
-
gameWidth,
|
|
513
|
-
gameHeight
|
|
514
|
-
})}
|
|
515
|
-
{:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
|
|
516
|
-
<!-- Require install on home screen on mobile -->
|
|
517
|
-
{@render snippetInstallOnHomeScreen({
|
|
518
|
-
isMobile,
|
|
519
|
-
os,
|
|
520
|
-
isFullscreen,
|
|
521
|
-
isDevMode,
|
|
522
|
-
requestDevmode,
|
|
523
|
-
requestFullscreen,
|
|
524
|
-
gameWidth,
|
|
525
|
-
gameHeight
|
|
526
|
-
})}
|
|
527
|
-
{:else}
|
|
528
|
-
{@render snippetPortrait({
|
|
529
|
-
isMobile,
|
|
530
|
-
os,
|
|
531
|
-
isFullscreen,
|
|
532
|
-
isDevMode,
|
|
533
|
-
requestDevmode,
|
|
534
|
-
requestFullscreen,
|
|
535
|
-
gameWidth,
|
|
536
|
-
gameHeight
|
|
537
|
-
})}
|
|
538
|
-
{/if}
|
|
539
|
-
{:else}
|
|
540
|
-
<!-- Do not require fullscreen -->
|
|
541
|
-
<!-- *we do not try install home app -->
|
|
542
|
-
{@render snippetPortrait({
|
|
543
|
-
isMobile,
|
|
544
|
-
os,
|
|
545
|
-
isFullscreen,
|
|
546
|
-
isDevMode,
|
|
547
|
-
requestDevmode,
|
|
548
|
-
requestFullscreen,
|
|
549
|
-
gameWidth,
|
|
550
|
-
gameHeight
|
|
551
|
-
})}
|
|
552
|
-
{/if}
|
|
553
|
-
{/if}
|
|
554
|
-
{/if}
|
|
555
|
-
</div>
|
|
556
|
-
</div>
|
|
557
|
-
{/if}
|
|
558
|
-
|
|
559
|
-
<style>
|
|
560
|
-
.center {
|
|
561
|
-
display: grid;
|
|
562
|
-
height: 100lvh;
|
|
563
|
-
display: grid;
|
|
564
|
-
justify-items: center;
|
|
565
|
-
align-items: center;
|
|
566
|
-
/* border: solid 1px red;*/
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
:global(html.game-box-no-scroll) {
|
|
570
|
-
overflow: clip;
|
|
571
|
-
scrollbar-width: none; /* Firefox */
|
|
572
|
-
-ms-overflow-style: none; /* IE and Edge */
|
|
573
|
-
}
|
|
574
|
-
:global(html.game-box-no-scroll::-webkit-scrollbar) {
|
|
575
|
-
display: none;
|
|
576
|
-
}
|
|
577
|
-
</style>
|