|
13 | 13 | background: black; |
14 | 14 | } |
15 | 15 |
|
| 16 | + html { |
| 17 | + --ios-safe-area-top: env(safe-area-inset-top, 0px); |
| 18 | + --ios-safe-area-right: env(safe-area-inset-right, 0px); |
| 19 | + --ios-safe-area-bottom: env(safe-area-inset-bottom, 0px); |
| 20 | + --ios-safe-area-left: env(safe-area-inset-left, 0px); |
| 21 | + } |
| 22 | + |
16 | 23 | .html-body__effect-background { |
17 | 24 | height: 100vh; |
18 | 25 | width: 100vw; |
|
134 | 141 |
|
135 | 142 | html.ios-phone-viewport, |
136 | 143 | html.ios-phone-viewport body { |
| 144 | + width: 100%; |
| 145 | + height: 100%; |
137 | 146 | overflow: hidden; |
138 | 147 | overscroll-behavior: none; |
139 | 148 | background: black; |
140 | 149 | } |
141 | 150 |
|
142 | 151 | html.ios-phone-viewport body { |
143 | | - width: 2560px; |
144 | | - height: 1440px; |
145 | | - position: relative; |
| 152 | + position: fixed; |
| 153 | + inset: 0; |
146 | 154 | } |
147 | 155 |
|
148 | 156 | html.ios-phone-viewport #ebg { |
|
156 | 164 | position: absolute; |
157 | 165 | top: 0; |
158 | 166 | left: 0; |
159 | | - transform: none !important; |
160 | 167 | transform-origin: top left; |
161 | 168 | } |
162 | 169 | </style> |
|
201 | 208 | const titleEnter = document.querySelector('.html-body__title-enter'); |
202 | 209 | const effectBackground = document.querySelector('.html-body__effect-background'); |
203 | 210 | let viewportMeta = document.querySelector('meta[name="viewport"]'); |
| 211 | + let iosPhoneLayoutFrame = 0; |
| 212 | + let touchStartX = 0; |
| 213 | + let touchStartY = 0; |
204 | 214 |
|
205 | 215 | window.__WEBGAL_DEVICE_INFO__ = { isIOS, isIOSPhone }; |
206 | 216 |
|
|
213 | 223 | return viewportMeta; |
214 | 224 | }; |
215 | 225 |
|
| 226 | + const parseInset = (value) => { |
| 227 | + const parsed = Number.parseFloat(value); |
| 228 | + return Number.isFinite(parsed) ? parsed : 0; |
| 229 | + }; |
| 230 | + |
| 231 | + const getSafeAreaInsets = () => { |
| 232 | + const computedStyle = window.getComputedStyle(html); |
| 233 | + return { |
| 234 | + top: parseInset(computedStyle.getPropertyValue('--ios-safe-area-top')), |
| 235 | + right: parseInset(computedStyle.getPropertyValue('--ios-safe-area-right')), |
| 236 | + bottom: parseInset(computedStyle.getPropertyValue('--ios-safe-area-bottom')), |
| 237 | + left: parseInset(computedStyle.getPropertyValue('--ios-safe-area-left')), |
| 238 | + }; |
| 239 | + }; |
| 240 | + |
| 241 | + const applyIOSPhoneLayout = () => { |
| 242 | + const safeArea = getSafeAreaInsets(); |
| 243 | + const viewportWidth = window.innerWidth; |
| 244 | + const viewportHeight = window.innerHeight; |
| 245 | + const usableWidth = Math.max(viewportWidth - safeArea.left - safeArea.right, 1); |
| 246 | + const usableHeight = Math.max(viewportHeight - safeArea.top - safeArea.bottom, 1); |
| 247 | + const scale = Math.max(Math.min(usableWidth / targetWidth, usableHeight / targetHeight), 0.01); |
| 248 | + const left = safeArea.left + Math.max((usableWidth - targetWidth * scale) / 2, 0); |
| 249 | + const top = safeArea.top + Math.max((usableHeight - targetHeight * scale) / 2, 0); |
| 250 | + const transform = `translate(${left}px, ${top}px) scale(${scale})`; |
| 251 | + |
| 252 | + if (effectBackground) { |
| 253 | + effectBackground.style.transform = ''; |
| 254 | + } |
| 255 | + for (const element of [root, titleEnter]) { |
| 256 | + if (element) { |
| 257 | + element.style.transform = transform; |
| 258 | + } |
| 259 | + } |
| 260 | + }; |
| 261 | + |
| 262 | + const scheduleIOSPhoneLayout = () => { |
| 263 | + if (iosPhoneLayoutFrame) { |
| 264 | + window.cancelAnimationFrame(iosPhoneLayoutFrame); |
| 265 | + } |
| 266 | + iosPhoneLayoutFrame = window.requestAnimationFrame(() => { |
| 267 | + iosPhoneLayoutFrame = 0; |
| 268 | + applyIOSPhoneLayout(); |
| 269 | + }); |
| 270 | + }; |
| 271 | + |
| 272 | + const canScrollInDirection = (element, deltaX, deltaY) => { |
| 273 | + const style = window.getComputedStyle(element); |
| 274 | + const overflowY = style.overflowY; |
| 275 | + const overflowX = style.overflowX; |
| 276 | + const canScrollY = /(auto|scroll|overlay)/.test(overflowY) && element.scrollHeight > element.clientHeight + 1; |
| 277 | + const canScrollX = /(auto|scroll|overlay)/.test(overflowX) && element.scrollWidth > element.clientWidth + 1; |
| 278 | + |
| 279 | + if (Math.abs(deltaY) >= Math.abs(deltaX) && canScrollY) { |
| 280 | + const maxScrollTop = element.scrollHeight - element.clientHeight; |
| 281 | + if (deltaY > 0 && element.scrollTop > 0) return true; |
| 282 | + if (deltaY < 0 && element.scrollTop < maxScrollTop) return true; |
| 283 | + } |
| 284 | + |
| 285 | + if (Math.abs(deltaX) > Math.abs(deltaY) && canScrollX) { |
| 286 | + const maxScrollLeft = element.scrollWidth - element.clientWidth; |
| 287 | + if (deltaX > 0 && element.scrollLeft > 0) return true; |
| 288 | + if (deltaX < 0 && element.scrollLeft < maxScrollLeft) return true; |
| 289 | + } |
| 290 | + |
| 291 | + return false; |
| 292 | + }; |
| 293 | + |
| 294 | + const findScrollableAncestor = (startElement, deltaX, deltaY) => { |
| 295 | + let current = startElement; |
| 296 | + while (current && current !== body) { |
| 297 | + if (current instanceof HTMLElement && canScrollInDirection(current, deltaX, deltaY)) { |
| 298 | + return current; |
| 299 | + } |
| 300 | + current = current.parentElement; |
| 301 | + } |
| 302 | + return null; |
| 303 | + }; |
| 304 | + |
| 305 | + const installIOSPhoneGestureGuards = () => { |
| 306 | + const blockEvent = (event) => { |
| 307 | + event.preventDefault(); |
| 308 | + }; |
| 309 | + |
| 310 | + document.addEventListener( |
| 311 | + 'gesturestart', |
| 312 | + (event) => { |
| 313 | + blockEvent(event); |
| 314 | + }, |
| 315 | + { passive: false }, |
| 316 | + ); |
| 317 | + document.addEventListener( |
| 318 | + 'gesturechange', |
| 319 | + (event) => { |
| 320 | + blockEvent(event); |
| 321 | + }, |
| 322 | + { passive: false }, |
| 323 | + ); |
| 324 | + document.addEventListener( |
| 325 | + 'gestureend', |
| 326 | + (event) => { |
| 327 | + blockEvent(event); |
| 328 | + }, |
| 329 | + { passive: false }, |
| 330 | + ); |
| 331 | + |
| 332 | + document.addEventListener( |
| 333 | + 'touchstart', |
| 334 | + (event) => { |
| 335 | + if (event.touches.length > 1) { |
| 336 | + blockEvent(event); |
| 337 | + return; |
| 338 | + } |
| 339 | + const touch = event.touches[0]; |
| 340 | + if (touch) { |
| 341 | + touchStartX = touch.clientX; |
| 342 | + touchStartY = touch.clientY; |
| 343 | + } |
| 344 | + }, |
| 345 | + { passive: false }, |
| 346 | + ); |
| 347 | + |
| 348 | + document.addEventListener( |
| 349 | + 'touchmove', |
| 350 | + (event) => { |
| 351 | + if (event.touches.length > 1) { |
| 352 | + blockEvent(event); |
| 353 | + return; |
| 354 | + } |
| 355 | + const touch = event.touches[0]; |
| 356 | + if (!touch) { |
| 357 | + return; |
| 358 | + } |
| 359 | + const deltaX = touch.clientX - touchStartX; |
| 360 | + const deltaY = touch.clientY - touchStartY; |
| 361 | + const target = event.target instanceof Element ? event.target : null; |
| 362 | + const scrollableAncestor = target ? findScrollableAncestor(target, deltaX, deltaY) : null; |
| 363 | + |
| 364 | + if (!scrollableAncestor) { |
| 365 | + blockEvent(event); |
| 366 | + } |
| 367 | + }, |
| 368 | + { passive: false }, |
| 369 | + ); |
| 370 | + }; |
| 371 | + |
216 | 372 | const resize = () => { |
217 | 373 | const h = window.innerHeight; // 窗口高度 |
218 | 374 | const w = window.innerWidth; // 窗口宽度 |
|
293 | 449 | if (isIOSPhone) { |
294 | 450 | html.classList.add('ios-phone-viewport'); |
295 | 451 | body.classList.add('ios-phone-viewport'); |
296 | | - ensureViewportMeta().content = `width=${targetWidth}, height=${targetHeight}, user-scalable=no, viewport-fit=cover`; |
297 | | - if (effectBackground) { |
298 | | - effectBackground.style.transform = ''; |
299 | | - } |
300 | | - for (const element of [root, titleEnter]) { |
301 | | - if (element) { |
302 | | - element.style.left = '0'; |
303 | | - element.style.top = '0'; |
304 | | - element.style.transform = 'none'; |
305 | | - } |
306 | | - } |
| 452 | + ensureViewportMeta().content = 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover'; |
| 453 | + installIOSPhoneGestureGuards(); |
| 454 | + scheduleIOSPhoneLayout(); |
| 455 | + window.addEventListener('load', scheduleIOSPhoneLayout); |
| 456 | + window.addEventListener('resize', scheduleIOSPhoneLayout); |
| 457 | + window.addEventListener('orientationchange', () => { |
| 458 | + scheduleIOSPhoneLayout(); |
| 459 | + setTimeout(scheduleIOSPhoneLayout, 300); |
| 460 | + }); |
307 | 461 | } else { |
308 | 462 | ensureViewportMeta().content = 'width=device-width, initial-scale=0.22, minimum-scale=0.01, maximum-scale=1'; |
309 | 463 | } |
|
0 commit comments