You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
37 KiB
682 lines
37 KiB
(function (global) {
|
|
'use strict';
|
|
|
|
/**
|
|
* shell(按真机形态做 CSS 边框):
|
|
* - classic Home 键(iPhone 8 / SE)
|
|
* - notch 刘海(iPhone X~14 标准)
|
|
* - island 灵动岛(iPhone 14 Pro 起)
|
|
* - punch 居中挖孔(多数安卓)
|
|
* - punchL 左上挖孔(部分三星/小米)
|
|
* - teardrop 水滴(中端安卓)
|
|
* - tablet 平板
|
|
* - tabletCam 平板+前摄
|
|
* - flat 无装饰框
|
|
*/
|
|
var SHELL = {
|
|
classic: { padX: 0.055, padY: 0.12, minPadX: 14, minPadY: 32, radius: 0.1 },
|
|
notch: { padX: 0.032, padY: 0.032, minPadX: 10, minPadY: 10, radius: 0.13 },
|
|
island: { padX: 0.028, padY: 0.028, minPadX: 10, minPadY: 10, radius: 0.14 },
|
|
punch: { padX: 0.022, padY: 0.022, minPadX: 8, minPadY: 8, radius: 0.11 },
|
|
punchL: { padX: 0.022, padY: 0.022, minPadX: 8, minPadY: 8, radius: 0.11 },
|
|
teardrop: { padX: 0.024, padY: 0.024, minPadX: 8, minPadY: 8, radius: 0.11 },
|
|
android: { padX: 0.022, padY: 0.022, minPadX: 8, minPadY: 8, radius: 0.11 },
|
|
tablet: { padX: 0.036, padY: 0.036, minPadX: 16, minPadY: 16, radius: 0.05 },
|
|
tabletCam: { padX: 0.036, padY: 0.036, minPadX: 16, minPadY: 16, radius: 0.05 },
|
|
flat: { padX: 0.018, padY: 0.018, minPadX: 6, minPadY: 6, radius: 0.08 },
|
|
};
|
|
|
|
/** 主流厂商代表性 / 较新机型(逻辑分辨率) */
|
|
var DEVICES = [
|
|
{ id: 'fit', name: '适应窗口', w: 0, h: 0, frame: false, shell: 'flat', brand: '通用' },
|
|
// —— Apple iPhone ——
|
|
{ id: 'iphone-se', name: 'Apple iPhone SE 3', w: 375, h: 667, frame: true, shell: 'classic', brand: 'Apple' },
|
|
{ id: 'iphone-8', name: 'Apple iPhone 8', w: 375, h: 667, frame: true, shell: 'classic', brand: 'Apple' },
|
|
{ id: 'iphone-x', name: 'Apple iPhone X / XS', w: 375, h: 812, frame: true, shell: 'notch', brand: 'Apple' },
|
|
{ id: 'iphone-xr', name: 'Apple iPhone XR / 11', w: 414, h: 896, frame: true, shell: 'notch', brand: 'Apple' },
|
|
{ id: 'iphone-12', name: 'Apple iPhone 12 / 13', w: 390, h: 844, frame: true, shell: 'notch', brand: 'Apple' },
|
|
{ id: 'iphone-12p', name: 'Apple iPhone 12 / 13 Pro', w: 390, h: 844, frame: true, shell: 'notch', brand: 'Apple' },
|
|
{ id: 'iphone-12pm', name: 'Apple iPhone 12 / 13 Pro Max', w: 428, h: 926, frame: true, shell: 'notch', brand: 'Apple' },
|
|
{ id: 'iphone-14', name: 'Apple iPhone 14', w: 390, h: 844, frame: true, shell: 'notch', brand: 'Apple' },
|
|
{ id: 'iphone-14-plus', name: 'Apple iPhone 14 Plus', w: 428, h: 926, frame: true, shell: 'notch', brand: 'Apple' },
|
|
{ id: 'iphone-14-pro', name: 'Apple iPhone 14 Pro', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-14-promax', name: 'Apple iPhone 14 Pro Max', w: 430, h: 932, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-15', name: 'Apple iPhone 15 / 15 Plus', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-15-pro', name: 'Apple iPhone 15 Pro', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-15-promax', name: 'Apple iPhone 15 Pro Max', w: 430, h: 932, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-16', name: 'Apple iPhone 16', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-16-plus', name: 'Apple iPhone 16 Plus', w: 430, h: 932, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-16-pro', name: 'Apple iPhone 16 Pro', w: 402, h: 874, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-16-promax', name: 'Apple iPhone 16 Pro Max', w: 440, h: 956, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-17', name: 'Apple iPhone 17', w: 393, h: 852, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-17-pro', name: 'Apple iPhone 17 Pro', w: 402, h: 874, frame: true, shell: 'island', brand: 'Apple' },
|
|
{ id: 'iphone-air', name: 'Apple iPhone Air', w: 430, h: 932, frame: true, shell: 'island', brand: 'Apple' },
|
|
// —— Apple iPad ——
|
|
{ id: 'ipad', name: 'Apple iPad 10', w: 820, h: 1180, frame: true, shell: 'tabletCam', brand: 'Apple' },
|
|
{ id: 'ipad-9', name: 'Apple iPad 9', w: 768, h: 1024, frame: true, shell: 'classic', brand: 'Apple' },
|
|
{ id: 'ipad-air', name: 'Apple iPad Air', w: 820, h: 1180, frame: true, shell: 'tabletCam', brand: 'Apple' },
|
|
{ id: 'ipad-pro', name: 'Apple iPad Pro 11', w: 834, h: 1194, frame: true, shell: 'tabletCam', brand: 'Apple' },
|
|
{ id: 'ipad-pro13', name: 'Apple iPad Pro 13', w: 1024, h: 1366, frame: true, shell: 'tabletCam', brand: 'Apple' },
|
|
// —— Samsung 直板 ——
|
|
{ id: 'galaxy-s20', name: 'Samsung Galaxy S20', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
{ id: 'galaxy-s21', name: 'Samsung Galaxy S21 / S22', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
{ id: 'galaxy-s23', name: 'Samsung Galaxy S23 / S24', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
{ id: 'galaxy-s25', name: 'Samsung Galaxy S25', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
{ id: 'galaxy-s25u', name: 'Samsung Galaxy S25 Ultra', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
{ id: 'galaxy-s24u', name: 'Samsung Galaxy S24 Ultra', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
{ id: 'galaxy-note20', name: 'Samsung Galaxy Note 20', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
{ id: 'galaxy-a54', name: 'Samsung Galaxy A54', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Samsung' },
|
|
// —— Google ——
|
|
{ id: 'pixel-5', name: 'Google Pixel 5', w: 393, h: 851, frame: true, shell: 'punch', brand: 'Google' },
|
|
{ id: 'pixel-6', name: 'Google Pixel 6 / 7', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Google' },
|
|
{ id: 'pixel-8', name: 'Google Pixel 8 / 8 Pro', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Google' },
|
|
{ id: 'pixel-9', name: 'Google Pixel 9 / 9 Pro', w: 412, h: 915, frame: true, shell: 'punch', brand: 'Google' },
|
|
// —— Xiaomi / Redmi ——
|
|
{ id: 'mi-13', name: 'Xiaomi 小米 13 / 14', w: 393, h: 873, frame: true, shell: 'punch', brand: 'Xiaomi' },
|
|
{ id: 'mi-14-pro', name: 'Xiaomi 小米 14 Pro', w: 400, h: 882, frame: true, shell: 'punch', brand: 'Xiaomi' },
|
|
{ id: 'mi-15', name: 'Xiaomi 小米 15 / 15 Pro', w: 393, h: 873, frame: true, shell: 'punch', brand: 'Xiaomi' },
|
|
{ id: 'redmi-note12', name: 'Xiaomi Redmi Note 12 / 13', w: 360, h: 800, frame: true, shell: 'teardrop', brand: 'Xiaomi' },
|
|
{ id: 'redmi-k60', name: 'Xiaomi Redmi K60 / K70', w: 393, h: 873, frame: true, shell: 'punch', brand: 'Xiaomi' },
|
|
{ id: 'redmi-k80', name: 'Xiaomi Redmi K80', w: 393, h: 873, frame: true, shell: 'punch', brand: 'Xiaomi' },
|
|
// —— Huawei / Honor ——
|
|
{ id: 'huawei-mate60', name: 'Huawei Mate 60 Pro', w: 400, h: 880, frame: true, shell: 'punch', brand: 'Huawei' },
|
|
{ id: 'huawei-p60', name: 'Huawei P60 / nova', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Huawei' },
|
|
{ id: 'huawei-pura70', name: 'Huawei Pura 70', w: 400, h: 880, frame: true, shell: 'punch', brand: 'Huawei' },
|
|
{ id: 'honor-magic6', name: 'Honor 荣耀 Magic6', w: 360, h: 800, frame: true, shell: 'punch', brand: 'Honor' },
|
|
{ id: 'honor-magic7', name: 'Honor 荣耀 Magic7', w: 393, h: 873, frame: true, shell: 'punch', brand: 'Honor' },
|
|
// —— OPPO / OnePlus / vivo / iQOO ——
|
|
{ id: 'oppo-find', name: 'OPPO Find X6 / X7', w: 360, h: 800, frame: true, shell: 'punch', brand: 'OPPO' },
|
|
{ id: 'oppo-find-x8', name: 'OPPO Find X8', w: 412, h: 915, frame: true, shell: 'punch', brand: 'OPPO' },
|
|
{ id: 'oppo-reno', name: 'OPPO Reno 11 / 12', w: 360, h: 800, frame: true, shell: 'punch', brand: 'OPPO' },
|
|
{ id: 'oneplus-12', name: 'OnePlus 一加 12', w: 450, h: 1000, frame: true, shell: 'punch', brand: 'OnePlus' },
|
|
{ id: 'oneplus-13', name: 'OnePlus 一加 13', w: 450, h: 1000, frame: true, shell: 'punch', brand: 'OnePlus' },
|
|
{ id: 'vivo-x100', name: 'vivo X100 / X200', w: 412, h: 915, frame: true, shell: 'punch', brand: 'vivo' },
|
|
{ id: 'iqoo-12', name: 'iQOO 12 / 13', w: 393, h: 873, frame: true, shell: 'punch', brand: 'iQOO' },
|
|
// —— 折叠屏 · Samsung ——
|
|
{ id: 'zfold2-in', name: 'Samsung Galaxy Z Fold 2 内屏', w: 2208, h: 1768, frame: true, shell: 'tabletCam', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold3-in', name: 'Samsung Galaxy Z Fold 3 内屏', w: 2208, h: 1768, frame: true, shell: 'tabletCam', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold4-in', name: 'Samsung Galaxy Z Fold 4 内屏', w: 2176, h: 1812, frame: true, shell: 'tabletCam', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold5-in', name: 'Samsung Galaxy Z Fold 5 内屏', w: 2176, h: 1812, frame: true, shell: 'tabletCam', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold6-in', name: 'Samsung Galaxy Z Fold 6 内屏', w: 2160, h: 1856, frame: true, shell: 'tabletCam', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold7-in', name: 'Samsung Galaxy Z Fold 7 内屏', w: 2184, h: 1968, frame: true, shell: 'tabletCam', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold8-in', name: 'Samsung Galaxy Z Fold 8 内屏', w: 2184, h: 1968, frame: true, shell: 'tabletCam', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold3-out', name: 'Samsung Galaxy Z Fold 3 外屏', w: 2260, h: 832, frame: true, shell: 'punch', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold5-out', name: 'Samsung Galaxy Z Fold 5 外屏', w: 2316, h: 904, frame: true, shell: 'punch', brand: 'Samsung 折叠' },
|
|
{ id: 'zfold7-out', name: 'Samsung Galaxy Z Fold 7 外屏', w: 2376, h: 968, frame: true, shell: 'punch', brand: 'Samsung 折叠' },
|
|
{ id: 'zflip3-in', name: 'Samsung Galaxy Z Flip 3 内屏', w: 2640, h: 1080, frame: true, shell: 'punch', brand: 'Samsung 折叠' },
|
|
{ id: 'zflip5-in', name: 'Samsung Galaxy Z Flip 5 内屏', w: 2640, h: 1080, frame: true, shell: 'punch', brand: 'Samsung 折叠' },
|
|
{ id: 'zflip6-in', name: 'Samsung Galaxy Z Flip 6 内屏', w: 2640, h: 1080, frame: true, shell: 'punch', brand: 'Samsung 折叠' },
|
|
{ id: 'zflip7-in', name: 'Samsung Galaxy Z Flip 7 内屏', w: 2640, h: 1080, frame: true, shell: 'punch', brand: 'Samsung 折叠' },
|
|
// —— 折叠屏 · Huawei ——
|
|
{ id: 'mate-x2', name: 'Huawei Mate X2 内屏', w: 2480, h: 2200, frame: true, shell: 'tabletCam', brand: 'Huawei 折叠' },
|
|
{ id: 'mate-x3', name: 'Huawei Mate X3 / X5 内屏', w: 2496, h: 2224, frame: true, shell: 'tabletCam', brand: 'Huawei 折叠' },
|
|
{ id: 'mate-x6', name: 'Huawei Mate X6 内屏', w: 2440, h: 2240, frame: true, shell: 'tabletCam', brand: 'Huawei 折叠' },
|
|
{ id: 'mate-x7', name: 'Huawei Mate X7 / XT 内屏', w: 2440, h: 2240, frame: true, shell: 'tabletCam', brand: 'Huawei 折叠' },
|
|
{ id: 'mate-x3-out', name: 'Huawei Mate X3 外屏', w: 2504, h: 1080, frame: true, shell: 'punch', brand: 'Huawei 折叠' },
|
|
{ id: 'mate-x6-out', name: 'Huawei Mate X6 外屏', w: 2520, h: 1080, frame: true, shell: 'punch', brand: 'Huawei 折叠' },
|
|
{ id: 'pocket2', name: 'Huawei Pocket 2 内屏', w: 2700, h: 1188, frame: true, shell: 'teardrop', brand: 'Huawei 折叠' },
|
|
{ id: 'pocket3', name: 'Huawei Pocket 3 内屏', w: 2700, h: 1188, frame: true, shell: 'teardrop', brand: 'Huawei 折叠' },
|
|
// —— 折叠屏 · Xiaomi ——
|
|
{ id: 'mix-fold2', name: 'Xiaomi MIX Fold 2 内屏', w: 2160, h: 1914, frame: true, shell: 'tabletCam', brand: 'Xiaomi 折叠' },
|
|
{ id: 'mix-fold3', name: 'Xiaomi MIX Fold 3 内屏', w: 2160, h: 1916, frame: true, shell: 'tabletCam', brand: 'Xiaomi 折叠' },
|
|
{ id: 'mix-fold4', name: 'Xiaomi MIX Fold 4 内屏', w: 2488, h: 2224, frame: true, shell: 'tabletCam', brand: 'Xiaomi 折叠' },
|
|
{ id: 'mix-fold5', name: 'Xiaomi MIX Fold 5 内屏', w: 2488, h: 2224, frame: true, shell: 'tabletCam', brand: 'Xiaomi 折叠' },
|
|
{ id: 'mix-flip', name: 'Xiaomi MIX Flip / Flip 2 内屏', w: 2912, h: 1388, frame: true, shell: 'punch', brand: 'Xiaomi 折叠' },
|
|
// —— 折叠屏 · OPPO / OnePlus ——
|
|
{ id: 'find-n', name: 'OPPO Find N / N2 内屏', w: 1920, h: 1792, frame: true, shell: 'tabletCam', brand: 'OPPO 折叠' },
|
|
{ id: 'find-n3', name: 'OPPO Find N3 内屏', w: 2344, h: 2156, frame: true, shell: 'tabletCam', brand: 'OPPO 折叠' },
|
|
{ id: 'find-n5', name: 'OPPO Find N5 内屏', w: 2248, h: 2480, frame: true, shell: 'tabletCam', brand: 'OPPO 折叠' },
|
|
{ id: 'find-n6', name: 'OPPO Find N6 内屏', w: 2248, h: 2480, frame: true, shell: 'tabletCam', brand: 'OPPO 折叠' },
|
|
{ id: 'oneplus-open', name: 'OnePlus Open / Open 2 内屏', w: 2344, h: 2156, frame: true, shell: 'tabletCam', brand: 'OnePlus 折叠' },
|
|
// —— 折叠屏 · vivo ——
|
|
{ id: 'xfold', name: 'vivo X Fold 内屏', w: 2160, h: 1916, frame: true, shell: 'tabletCam', brand: 'vivo 折叠' },
|
|
{ id: 'xfold3', name: 'vivo X Fold3 内屏', w: 2480, h: 2200, frame: true, shell: 'tabletCam', brand: 'vivo 折叠' },
|
|
{ id: 'xfold5', name: 'vivo X Fold5 内屏', w: 2480, h: 2200, frame: true, shell: 'tabletCam', brand: 'vivo 折叠' },
|
|
{ id: 'xfold-n5', name: 'vivo X Fold N5 内屏', w: 2248, h: 2480, frame: true, shell: 'tabletCam', brand: 'vivo 折叠' },
|
|
{ id: 'xflip', name: 'vivo X Flip 内屏', w: 2520, h: 1080, frame: true, shell: 'punch', brand: 'vivo 折叠' },
|
|
// —— 折叠屏 · Honor / Google ——
|
|
{ id: 'magic-v', name: 'Honor Magic V 内屏', w: 2272, h: 1984, frame: true, shell: 'tabletCam', brand: 'Honor 折叠' },
|
|
{ id: 'magic-v3', name: 'Honor Magic V3 内屏', w: 2344, h: 2156, frame: true, shell: 'tabletCam', brand: 'Honor 折叠' },
|
|
{ id: 'magic-v4', name: 'Honor Magic V4 内屏', w: 2344, h: 2156, frame: true, shell: 'tabletCam', brand: 'Honor 折叠' },
|
|
{ id: 'magic-v-flip', name: 'Honor Magic V Flip 内屏', w: 2320, h: 1080, frame: true, shell: 'punch', brand: 'Honor 折叠' },
|
|
{ id: 'pixel-fold', name: 'Google Pixel Fold 内屏', w: 2208, h: 1840, frame: true, shell: 'tabletCam', brand: 'Google 折叠' },
|
|
{ id: 'pixel-9-fold', name: 'Google Pixel 9 Pro Fold 内屏', w: 2076, h: 2152, frame: true, shell: 'tabletCam', brand: 'Google 折叠' },
|
|
{ id: 'pixel-10-fold', name: 'Google Pixel 10 Pro Fold 内屏', w: 2076, h: 2152, frame: true, shell: 'tabletCam', brand: 'Google 折叠' },
|
|
// —— 其它常见分辨率 ——
|
|
{ id: 'android-360', name: '通用 Android 360×800', w: 360, h: 800, frame: true, shell: 'punch', brand: '通用' },
|
|
{ id: 'android-412', name: '通用 Android 412×915', w: 412, h: 915, frame: true, shell: 'punch', brand: '通用' },
|
|
{ id: 'android-720', name: '通用 720×1280', w: 720, h: 1280, frame: true, shell: 'flat', brand: '通用' },
|
|
{ id: 'android-1080', name: '通用 1080×1920', w: 1080, h: 1920, frame: true, shell: 'flat', brand: '通用' },
|
|
{ id: 'design', name: '通用 设计分辨率', w: 750, h: 1600, frame: true, shell: 'island', brand: '通用' },
|
|
];
|
|
|
|
/**
|
|
* 设备预览:真实机型边框 + 画布铺满屏幕容器(无 CSS transform 缩放)。
|
|
* @constructor
|
|
*/
|
|
function DevicePreview() {
|
|
this.deviceId = 'iphone-x';
|
|
this.rotated = false;
|
|
this._capsuleOn = true;
|
|
this._capsuleStyle = 'douyin';
|
|
this._stage = null;
|
|
this._bezel = null;
|
|
this._screen = null;
|
|
this._cutout = null;
|
|
this._homeBar = null;
|
|
this._homeBtn = null;
|
|
this._scaleLabel = null;
|
|
this._center = null;
|
|
this._applying = false;
|
|
this._notifyTimer = 0;
|
|
this._lastKey = '';
|
|
this._frameW = 0;
|
|
this._frameH = 0;
|
|
this._ro = null;
|
|
this._onWinResize = this._onWinResize.bind(this);
|
|
}
|
|
|
|
DevicePreview.prototype.mount = function (centerEl, contentEl) {
|
|
var stage = document.createElement('div');
|
|
stage.className = 'pw-device-stage';
|
|
stage.id = 'pw-device-stage';
|
|
|
|
var bezel = document.createElement('div');
|
|
bezel.className = 'pw-device-bezel';
|
|
bezel.id = 'pw-device-bezel';
|
|
bezel.innerHTML =
|
|
'<div class="pw-device-btn pw-device-btn-silent" aria-hidden="true"></div>' +
|
|
'<div class="pw-device-btn pw-device-btn-volup" aria-hidden="true"></div>' +
|
|
'<div class="pw-device-btn pw-device-btn-voldown" aria-hidden="true"></div>' +
|
|
'<div class="pw-device-btn pw-device-btn-power" aria-hidden="true"></div>' +
|
|
'<div class="pw-device-ear" aria-hidden="true"></div>' +
|
|
'<div class="pw-device-screen" id="pw-device-screen">' +
|
|
'<div class="pw-device-cutout" id="pw-device-cutout" aria-hidden="true"></div>' +
|
|
'<div class="pw-device-capsule" id="pw-device-capsule" data-style="douyin" hidden>' +
|
|
'<span class="pw-capsule-menu"></span><span class="pw-capsule-line"></span>' +
|
|
'<span class="pw-capsule-home"></span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="pw-device-home-bar" id="pw-device-home-bar" aria-hidden="true"></div>' +
|
|
'<div class="pw-device-home-btn" id="pw-device-home-btn" aria-hidden="true"></div>';
|
|
|
|
var scaleLabel = document.createElement('div');
|
|
scaleLabel.className = 'pw-device-scale';
|
|
scaleLabel.id = 'pw-device-scale';
|
|
|
|
stage.appendChild(bezel);
|
|
stage.appendChild(scaleLabel);
|
|
|
|
var screen = bezel.querySelector('#pw-device-screen');
|
|
// 内容与挖孔/胶囊同在 screen 内,保证位置相对屏幕
|
|
if (contentEl.parentNode) contentEl.parentNode.insertBefore(stage, contentEl);
|
|
screen.insertBefore(contentEl, screen.firstChild);
|
|
|
|
this._stage = stage;
|
|
this._bezel = bezel;
|
|
this._screen = screen;
|
|
this._cutout = bezel.querySelector('#pw-device-cutout');
|
|
this._homeBar = bezel.querySelector('#pw-device-home-bar');
|
|
this._homeBtn = bezel.querySelector('#pw-device-home-btn');
|
|
this._capsule = bezel.querySelector('#pw-device-capsule');
|
|
this._scaleLabel = scaleLabel;
|
|
this._center = centerEl;
|
|
|
|
window.addEventListener('resize', this._onWinResize);
|
|
if (typeof ResizeObserver !== 'undefined' && centerEl) {
|
|
var self = this;
|
|
this._ro = new ResizeObserver(function () { self.apply(true); });
|
|
this._ro.observe(centerEl);
|
|
}
|
|
return stage;
|
|
};
|
|
|
|
DevicePreview.prototype._onWinResize = function () {
|
|
this.apply(true);
|
|
};
|
|
|
|
/** @returns {Array} */
|
|
DevicePreview.prototype.list = function () {
|
|
return DEVICES.slice();
|
|
};
|
|
|
|
/**
|
|
* @param {string} id
|
|
*/
|
|
DevicePreview.prototype.setDevice = function (id) {
|
|
this.deviceId = id || 'iphone-x';
|
|
this.apply(false);
|
|
try { localStorage.setItem('preview-workbench-device', this.deviceId); } catch (e) { /* */ }
|
|
};
|
|
|
|
/** 微信/抖音等小程序右上角胶囊按钮开关 */
|
|
DevicePreview.prototype.setCapsule = function (on) {
|
|
this._capsuleOn = !!on;
|
|
if (this._capsule) {
|
|
this._capsule.hidden = !this._capsuleOn;
|
|
this._capsule.className = 'pw-device-capsule' +
|
|
(this._capsuleOn ? ' show' : '') +
|
|
' cap-' + (this._capsuleStyle || 'wechat');
|
|
this._capsule.setAttribute('data-style', this._capsuleStyle || 'wechat');
|
|
}
|
|
try { localStorage.setItem('preview-workbench-capsule', this._capsuleOn ? '1' : '0'); } catch (e) { /* */ }
|
|
return this._capsuleOn;
|
|
};
|
|
|
|
/** 胶囊样式:none=不显示;douyin 默认。wechat / wechat-dark 对齐微信官方菜单 */
|
|
DevicePreview.prototype.setCapsuleStyle = function (style) {
|
|
var allow = {
|
|
none: 1, douyin: 1, wechat: 1, 'wechat-dark': 1,
|
|
alipay: 1, qq: 1, kuaishou: 1, bilibili: 1,
|
|
};
|
|
this._capsuleStyle = allow[style] ? style : 'douyin';
|
|
// 「无」= 不显示;其它样式默认显示
|
|
if (this._capsuleStyle === 'none') {
|
|
this._capsuleOn = false;
|
|
} else {
|
|
this._capsuleOn = true;
|
|
}
|
|
this.setCapsule(this._capsuleOn);
|
|
if (this._capsule) {
|
|
this._capsule.setAttribute('data-style', this._capsuleStyle);
|
|
this._capsule.className = 'pw-device-capsule' +
|
|
((this._capsuleOn && this._capsuleStyle !== 'none') ? ' show' : '') +
|
|
' cap-' + this._capsuleStyle;
|
|
}
|
|
try { localStorage.setItem('preview-workbench-capsule-style', this._capsuleStyle); } catch (e) { /* */ }
|
|
return this._capsuleStyle;
|
|
};
|
|
|
|
DevicePreview.prototype.getCapsuleStyle = function () {
|
|
return this._capsuleStyle || 'douyin';
|
|
};
|
|
|
|
DevicePreview.prototype.isCapsule = function () {
|
|
return !!this._capsuleOn && this._capsuleStyle !== 'none';
|
|
};
|
|
|
|
DevicePreview.capsuleStyles = function () {
|
|
return [
|
|
{ id: 'none', name: '无' },
|
|
{ id: 'douyin', name: '抖音' },
|
|
{ id: 'wechat', name: '微信·浅' },
|
|
{ id: 'wechat-dark', name: '微信·深' },
|
|
{ id: 'alipay', name: '支付宝' },
|
|
{ id: 'qq', name: 'QQ' },
|
|
{ id: 'kuaishou', name: '快手' },
|
|
{ id: 'bilibili', name: 'B站' },
|
|
];
|
|
};
|
|
|
|
DevicePreview.prototype.toggleRotate = function () {
|
|
this.rotated = !this.rotated;
|
|
this.apply(false);
|
|
return this.rotated;
|
|
};
|
|
|
|
DevicePreview.prototype._find = function () {
|
|
for (var i = 0; i < DEVICES.length; i++) {
|
|
if (DEVICES[i].id === this.deviceId) return DEVICES[i];
|
|
}
|
|
return DEVICES[3];
|
|
};
|
|
|
|
DevicePreview.prototype._shellOf = function (d) {
|
|
var key = (d && d.shell) || 'island';
|
|
return SHELL[key] || SHELL.island;
|
|
};
|
|
|
|
/**
|
|
* 安全区(逻辑分辨率单位,与 w/h 同一坐标系)。
|
|
* 供 Cocos SafeArea / getSafeAreaEdge 使用。
|
|
*/
|
|
DevicePreview.prototype._safeOf = function (d) {
|
|
if (d && d.safe) {
|
|
return {
|
|
top: d.safe.top || 0,
|
|
bottom: d.safe.bottom || 0,
|
|
left: d.safe.left || 0,
|
|
right: d.safe.right || 0,
|
|
};
|
|
}
|
|
var shell = (d && d.shell) || 'island';
|
|
var h = (d && d.h) || 812;
|
|
var w = (d && d.w) || 375;
|
|
// 以 iPhone 点值为基准按高度比例换算
|
|
var s = h / 852;
|
|
if (shell === 'island') {
|
|
return { top: Math.round(59 * s), bottom: Math.round(34 * s), left: 0, right: 0 };
|
|
}
|
|
if (shell === 'notch') {
|
|
return { top: Math.round(47 * s), bottom: Math.round(34 * s), left: 0, right: 0 };
|
|
}
|
|
if (shell === 'classic') {
|
|
return { top: 20, bottom: 0, left: 0, right: 0 };
|
|
}
|
|
if (shell === 'punch' || shell === 'punchL' || shell === 'android') {
|
|
return { top: Math.round(28 * s), bottom: Math.round(16 * s), left: 0, right: 0 };
|
|
}
|
|
if (shell === 'teardrop') {
|
|
return { top: Math.round(24 * s), bottom: Math.round(12 * s), left: 0, right: 0 };
|
|
}
|
|
if (shell === 'tablet' || shell === 'tabletCam') {
|
|
return { top: 24, bottom: 20, left: 0, right: 0 };
|
|
}
|
|
return { top: 0, bottom: 0, left: 0, right: 0 };
|
|
};
|
|
|
|
/** 把安全区注入引擎,供 cc.SafeArea / view.getSafeAreaEdge 使用 */
|
|
DevicePreview.prototype._applySafeArea = function () {
|
|
var cc = global.cc;
|
|
if (!cc) return;
|
|
var d = this._find();
|
|
var safe = this._safeOf(d);
|
|
if (this.rotated) {
|
|
// 横屏:上下安全区对应侧边
|
|
safe = {
|
|
top: 0,
|
|
bottom: 0,
|
|
left: this._safeOf(d).bottom,
|
|
right: 0,
|
|
};
|
|
// 保持 notch/island 在横屏左侧时的上/左边距
|
|
var base = this._safeOf(d);
|
|
safe = { top: 0, bottom: 0, left: base.bottom || 0, right: base.top || 0 };
|
|
}
|
|
this._safeEdge = safe;
|
|
|
|
var applyEdge = function () {
|
|
return {
|
|
top: safe.top,
|
|
bottom: safe.bottom,
|
|
left: safe.left,
|
|
right: safe.right,
|
|
};
|
|
};
|
|
|
|
try {
|
|
if (cc.view) {
|
|
cc.view.getSafeAreaEdge = applyEdge;
|
|
if (typeof cc.view.getSafeAreaRect === 'function') {
|
|
var origRect = cc.view.getSafeAreaRect;
|
|
cc.view.getSafeAreaRect = function () {
|
|
var vs = cc.view.getVisibleSize ? cc.view.getVisibleSize() : { width: 0, height: 0 };
|
|
var origin = cc.view.getVisibleOrigin ? cc.view.getVisibleOrigin() : { x: 0, y: 0 };
|
|
var e = applyEdge();
|
|
// 逻辑点:与设计分辨率同一尺度(view 一般用设计/可见单位)
|
|
return {
|
|
x: origin.x + e.left,
|
|
y: origin.y + e.bottom,
|
|
width: Math.max(1, vs.width - e.left - e.right),
|
|
height: Math.max(1, vs.height - e.top - e.bottom),
|
|
contains: origRect ? undefined : undefined,
|
|
};
|
|
};
|
|
}
|
|
}
|
|
// 部分机型走 screen.safeAreaEdge
|
|
if (global.screen && global.screen.safeAreaEdge !== undefined) {
|
|
global.screen.safeAreaEdge = applyEdge();
|
|
}
|
|
} catch (e) { /* */ }
|
|
|
|
// 胶囊:贴齐安全区右上
|
|
if (this._capsule) {
|
|
var capTop = Math.max(6, safe.top + 6);
|
|
this._capsule.style.top = capTop + 'px';
|
|
this._capsule.style.right = '10px';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} shell
|
|
* @param {boolean} useFrame
|
|
* @param {boolean} landscape
|
|
*/
|
|
DevicePreview.prototype._applyShellClass = function (shell, useFrame, landscape) {
|
|
var bezel = this._bezel;
|
|
bezel.className = 'pw-device-bezel';
|
|
if (!useFrame) {
|
|
bezel.classList.add('no-frame');
|
|
return;
|
|
}
|
|
bezel.classList.add('pw-shell-' + shell);
|
|
if (landscape) bezel.classList.add('is-landscape');
|
|
if (shell === 'notch' && !landscape) bezel.classList.add('has-cutout', 'has-notch');
|
|
if (shell === 'island' && !landscape) bezel.classList.add('has-cutout', 'has-island');
|
|
if ((shell === 'punch' || shell === 'android') && !landscape) {
|
|
bezel.classList.add('has-cutout', 'has-punch');
|
|
}
|
|
if (shell === 'punchL' && !landscape) bezel.classList.add('has-cutout', 'has-punch', 'has-punch-l');
|
|
if (shell === 'teardrop' && !landscape) bezel.classList.add('has-cutout', 'has-teardrop');
|
|
if (shell === 'classic') bezel.classList.add('has-home-btn');
|
|
if (shell === 'notch' || shell === 'island' || shell === 'tablet' || shell === 'tabletCam') {
|
|
bezel.classList.add('has-home-bar');
|
|
}
|
|
if (shell === 'tabletCam' && !landscape) bezel.classList.add('has-tablet-cam');
|
|
};
|
|
|
|
/**
|
|
* @param {number} w
|
|
* @param {number} h
|
|
*/
|
|
DevicePreview.prototype._fillScreen = function (w, h) {
|
|
if (this._screen) {
|
|
this._screen.style.width = w + 'px';
|
|
this._screen.style.height = h + 'px';
|
|
}
|
|
var gameDiv = document.getElementById('GameDiv');
|
|
var content = document.getElementById('content');
|
|
var wrap = content ? content.querySelector('.contentWrap') : null;
|
|
var container = document.getElementById('Cocos3dGameContainer');
|
|
var canvas = document.getElementById('GameCanvas');
|
|
var list = [content, wrap, gameDiv, container, canvas];
|
|
for (var i = 0; i < list.length; i++) {
|
|
var el = list[i];
|
|
if (!el) continue;
|
|
el.style.setProperty('width', '100%', 'important');
|
|
el.style.setProperty('height', '100%', 'important');
|
|
el.style.setProperty('max-width', 'none', 'important');
|
|
el.style.setProperty('max-height', 'none', 'important');
|
|
el.style.margin = '0';
|
|
}
|
|
if (gameDiv) {
|
|
gameDiv.style.position = 'relative';
|
|
gameDiv.style.overflow = 'hidden';
|
|
}
|
|
this._frameW = w;
|
|
this._frameH = h;
|
|
};
|
|
|
|
/**
|
|
* @param {boolean} [fromWindowResize]
|
|
*/
|
|
DevicePreview.prototype.apply = function (fromWindowResize) {
|
|
if (this._applying) return;
|
|
var d = this._find();
|
|
if (!this._bezel || !this._stage || !this._screen) return;
|
|
|
|
this._applying = true;
|
|
try {
|
|
var logicW = d.w;
|
|
var logicH = d.h;
|
|
if (this.rotated && logicW && logicH) {
|
|
var swap = logicW; logicW = logicH; logicH = swap;
|
|
}
|
|
|
|
var useFrame = !!d.frame && logicW > 0 && logicH > 0;
|
|
var shellKey = d.shell || 'island';
|
|
var shell = this._shellOf(d);
|
|
this._applyShellClass(shellKey, useFrame, !!this.rotated);
|
|
|
|
var stageW = this._center ? this._center.clientWidth : window.innerWidth;
|
|
var stageH = this._center ? this._center.clientHeight : window.innerHeight;
|
|
var pad = useFrame ? 48 : 12;
|
|
var availW = Math.max(120, stageW - pad);
|
|
var availH = Math.max(120, stageH - pad);
|
|
|
|
var screenW;
|
|
var screenH;
|
|
|
|
if (!logicW || !logicH) {
|
|
screenW = Math.floor(availW);
|
|
screenH = Math.floor(availH);
|
|
this._bezel.style.padding = '0';
|
|
this._bezel.style.width = screenW + 'px';
|
|
this._bezel.style.height = screenH + 'px';
|
|
this._bezel.style.borderRadius = '0';
|
|
if (this._scaleLabel) this._scaleLabel.textContent = '适应窗口 ' + screenW + '×' + screenH;
|
|
} else {
|
|
var padX0 = Math.max(shell.minPadX, logicW * shell.padX);
|
|
var padY0 = Math.max(shell.minPadY, logicH * shell.padY);
|
|
if (this.rotated && shellKey === 'classic') {
|
|
// 横屏经典机:左右加宽留给 Home 键区域感
|
|
padX0 = Math.max(shell.minPadY, logicW * shell.padY * 0.55);
|
|
padY0 = Math.max(shell.minPadX, logicH * shell.padX);
|
|
}
|
|
var totalW0 = logicW + padX0 * 2;
|
|
var totalH0 = logicH + padY0 * 2;
|
|
var fit = Math.min(availW / totalW0, availH / totalH0, 1);
|
|
screenW = Math.max(80, Math.floor(logicW * fit));
|
|
screenH = Math.max(80, Math.floor(logicH * fit));
|
|
var bezelPadX = Math.max(8, Math.floor(padX0 * fit));
|
|
var bezelPadY = Math.max(8, Math.floor(padY0 * fit));
|
|
var radius = Math.max(12, Math.floor(Math.min(screenW, screenH) * shell.radius));
|
|
var screenRadius = Math.max(6, Math.floor(radius * 0.72));
|
|
|
|
this._bezel.style.padding = bezelPadY + 'px ' + bezelPadX + 'px';
|
|
this._bezel.style.width = (screenW + bezelPadX * 2) + 'px';
|
|
this._bezel.style.height = (screenH + bezelPadY * 2) + 'px';
|
|
this._bezel.style.borderRadius = radius + 'px';
|
|
this._screen.style.borderRadius = screenRadius + 'px';
|
|
this._bezel.style.setProperty('--pw-shell-r', radius + 'px');
|
|
this._bezel.style.setProperty('--pw-screen-r', screenRadius + 'px');
|
|
this._bezel.style.setProperty('--pw-bezel-x', bezelPadX + 'px');
|
|
this._bezel.style.setProperty('--pw-bezel-y', bezelPadY + 'px');
|
|
|
|
if (this._scaleLabel) {
|
|
this._scaleLabel.textContent = d.name + (this.rotated ? '(横屏)' : '(竖屏)') +
|
|
' ' + screenW + '×' + screenH +
|
|
(fit < 1 ? ' · ' + Math.round(fit * 100) + '%' : '');
|
|
}
|
|
}
|
|
|
|
this._bezel.style.transform = 'none';
|
|
this._fillScreen(screenW, screenH);
|
|
this._applySafeArea();
|
|
|
|
var key = this.deviceId + '|' + (this.rotated ? 1 : 0) + '|' + screenW + 'x' + screenH + '|' + stageW + 'x' + stageH;
|
|
var changed = key !== this._lastKey;
|
|
this._lastKey = key;
|
|
if (changed || !fromWindowResize) this._scheduleEngineNotify();
|
|
} finally {
|
|
this._applying = false;
|
|
}
|
|
};
|
|
|
|
DevicePreview.prototype._scheduleEngineNotify = function () {
|
|
var self = this;
|
|
if (this._notifyTimer) clearTimeout(this._notifyTimer);
|
|
this._notifyTimer = setTimeout(function () {
|
|
self._notifyTimer = 0;
|
|
self._notifyEngine();
|
|
}, 50);
|
|
};
|
|
|
|
DevicePreview.prototype._notifyEngine = function () {
|
|
var screen = this._screen;
|
|
var w = screen ? screen.clientWidth : this._frameW;
|
|
var h = screen ? screen.clientHeight : this._frameH;
|
|
if (!w || !h) {
|
|
w = this._frameW;
|
|
h = this._frameH;
|
|
}
|
|
if (!w || !h) return;
|
|
this._frameW = w;
|
|
this._frameH = h;
|
|
this._applySafeArea();
|
|
|
|
var cc = global.cc;
|
|
try {
|
|
if (cc && cc.view) {
|
|
if (typeof cc.view.resizeWithBrowserSize === 'function') {
|
|
cc.view.resizeWithBrowserSize(false);
|
|
}
|
|
// 铺满设备屏:只改 frame/window 尺寸,不改项目的分辨率策略
|
|
if (typeof cc.view.setFrameSize === 'function') {
|
|
try { cc.view.setFrameSize(w, h); } catch (eFrame) { /* */ }
|
|
}
|
|
var scr = global.screen || (cc.screen);
|
|
if (scr) {
|
|
try {
|
|
if (cc.Size) scr.windowSize = new cc.Size(w, h);
|
|
else if (typeof scr.setWindowSize === 'function') scr.setWindowSize(w, h);
|
|
} catch (eScr) { /* */ }
|
|
}
|
|
// 保持项目原有设计分辨率与适配策略,避免强推 SHOW_ALL 造成黑边
|
|
var dr = null;
|
|
if (typeof cc.view.getDesignResolutionSize === 'function') {
|
|
dr = cc.view.getDesignResolutionSize();
|
|
}
|
|
if (dr && dr.width > 0 && dr.height > 0 && typeof cc.view.setDesignResolutionSize === 'function') {
|
|
var policy = null;
|
|
if (typeof cc.view.getResolutionPolicy === 'function') {
|
|
try { policy = cc.view.getResolutionPolicy(); } catch (ePol) { policy = null; }
|
|
}
|
|
if (policy != null) {
|
|
cc.view.setDesignResolutionSize(dr.width, dr.height, policy);
|
|
} else if (cc.ResolutionPolicy && cc.ResolutionPolicy.EXACT_FIT != null) {
|
|
// 默认铺满容器(预览设备)
|
|
cc.view.setDesignResolutionSize(dr.width, dr.height, cc.ResolutionPolicy.EXACT_FIT);
|
|
} else {
|
|
cc.view.setDesignResolutionSize(dr.width, dr.height);
|
|
}
|
|
}
|
|
if (typeof cc.view.emit === 'function') {
|
|
try { cc.view.emit('canvas-resize'); } catch (e1) { /* */ }
|
|
}
|
|
}
|
|
window.dispatchEvent(new CustomEvent('pw-device-resized', { detail: { width: w, height: h } }));
|
|
} catch (e) {
|
|
console.warn('[调试工作台] 通知引擎尺寸失败', e);
|
|
}
|
|
};
|
|
|
|
DevicePreview.prototype.restore = function () {
|
|
try {
|
|
var id = localStorage.getItem('preview-workbench-device');
|
|
if (id) this.deviceId = id;
|
|
var capStyle = localStorage.getItem('preview-workbench-capsule-style');
|
|
this._capsuleStyle = capStyle || 'douyin';
|
|
// 默认显示抖音;仅样式为 none 时隐藏
|
|
this._capsuleOn = this._capsuleStyle !== 'none';
|
|
if (this._capsule) {
|
|
this._capsule.hidden = !this._capsuleOn;
|
|
this._capsule.setAttribute('data-style', this._capsuleStyle);
|
|
this._capsule.className = 'pw-device-capsule' +
|
|
(this._capsuleOn ? ' show' : '') +
|
|
' cap-' + this._capsuleStyle;
|
|
}
|
|
} catch (e) { /* */ }
|
|
};
|
|
|
|
/**
|
|
* 隐藏官方预览工具栏。
|
|
* @returns {HTMLSelectElement|null}
|
|
*/
|
|
DevicePreview.hideOfficialToolbar = function () {
|
|
var root = document.getElementById('pw-root');
|
|
var el = document.body.firstElementChild;
|
|
while (el && el !== root) {
|
|
var next = el.nextElementSibling;
|
|
if (el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && el.tagName !== 'LINK') {
|
|
el.classList.add('pw-official-toolbar-hidden');
|
|
el.setAttribute('aria-hidden', 'true');
|
|
}
|
|
el = next;
|
|
}
|
|
return document.querySelector('.pw-official-toolbar-hidden select, #toolbar select, .toolbar select');
|
|
};
|
|
|
|
global.PWDevicePreview = DevicePreview;
|
|
})(window);
|
|
|