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.

369 lines
16 KiB

(function (global) {
'use strict';
/**
* shell:
* - classic 圆角矩形 + Home 键(iPhone SE/8)
* - notch 刘海(iPhone X / 14)
* - island 灵动岛(iPhone 14 Pro+)
* - android 窄边框 + 打孔/小刘海感
* - tablet 平板宽边框
*/
var SHELL = {
classic: { padX: 0.048, padY: 0.11, minPadX: 12, minPadY: 28, radius: 0.12 },
notch: { padX: 0.03, padY: 0.03, minPadX: 10, minPadY: 10, radius: 0.14 },
island: { padX: 0.028, padY: 0.028, minPadX: 10, minPadY: 10, radius: 0.145 },
android: { padX: 0.02, padY: 0.02, minPadX: 8, minPadY: 8, radius: 0.1 },
tablet: { padX: 0.032, padY: 0.032, minPadX: 14, minPadY: 14, radius: 0.05 },
};
/** 常见预览设备 */
var DEVICES = [
{ id: 'fit', name: '适应窗口', w: 0, h: 0, frame: false, shell: 'island' },
{ id: 'iphone-se', name: 'iPhone SE', w: 375, h: 667, frame: true, shell: 'classic' },
{ id: 'iphone-8', name: 'iPhone 8', w: 375, h: 667, frame: true, shell: 'classic' },
{ id: 'iphone-x', name: 'iPhone X / XS', w: 375, h: 812, frame: true, shell: 'notch' },
{ id: 'iphone-14', name: 'iPhone 14', w: 390, h: 844, frame: true, shell: 'notch' },
{ id: 'iphone-14-pro', name: 'iPhone 14 Pro', w: 393, h: 852, frame: true, shell: 'island' },
{ id: 'iphone-14-promax', name: 'iPhone 14 Pro Max', w: 430, h: 932, frame: true, shell: 'island' },
{ id: 'pixel-5', name: 'Pixel 5', w: 393, h: 851, frame: true, shell: 'android' },
{ id: 'galaxy-s20', name: 'Galaxy S20', w: 360, h: 800, frame: true, shell: 'android' },
{ id: 'ipad', name: 'iPad', w: 768, h: 1024, frame: true, shell: 'tablet' },
{ id: 'ipad-pro', name: 'iPad Pro 11', w: 834, h: 1194, frame: true, shell: 'tablet' },
{ id: 'custom-720', name: '720×1280', w: 720, h: 1280, frame: true, shell: 'android' },
{ id: 'custom-1080', name: '1080×1920', w: 1080, h: 1920, frame: true, shell: 'android' },
{ id: 'design', name: '设计分辨率', w: 750, h: 1600, frame: true, shell: 'island' },
];
/**
* 设备预览:真实机型边框 + 画布铺满屏幕容器(无 CSS transform 缩放)。
* @constructor
*/
function DevicePreview() {
this.deviceId = 'iphone-x';
this.rotated = false;
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-cutout" id="pw-device-cutout" aria-hidden="true"></div>' +
'<div class="pw-device-screen" id="pw-device-screen"></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');
if (contentEl.parentNode) contentEl.parentNode.insertBefore(stage, contentEl);
screen.appendChild(contentEl);
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._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.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;
};
/**
* @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 === 'android' && !landscape) bezel.classList.add('has-cutout', 'has-punch');
if (shell === 'classic') bezel.classList.add('has-home-btn');
if (shell === 'notch' || shell === 'island') bezel.classList.add('has-home-bar');
if (shell === 'tablet') bezel.classList.add('has-home-bar');
};
/**
* @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);
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;
var cc = global.cc;
try {
if (cc && cc.view) {
if (typeof cc.view.resizeWithBrowserSize === 'function') {
cc.view.resizeWithBrowserSize(false);
}
if (typeof cc.view.setFrameSize === 'function') {
cc.view.setFrameSize(w, h);
}
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') {
policy = cc.view.getResolutionPolicy();
}
if (policy != null) {
cc.view.setDesignResolutionSize(dr.width, dr.height, policy);
} else if (cc.ResolutionPolicy && cc.ResolutionPolicy.SHOW_ALL != null) {
cc.view.setDesignResolutionSize(dr.width, dr.height, cc.ResolutionPolicy.SHOW_ALL);
} 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;
} 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);