734 lines
27 KiB
HTML
734 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>UJoin HiNC Demo</title>
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: Arial, sans-serif;
|
||
background-color: #f0f0f0;
|
||
height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.container {
|
||
width: 95%;
|
||
height: 95vh;
|
||
max-width: 1400px;
|
||
background-color: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.header {
|
||
background-color: #2196F3;
|
||
color: white;
|
||
padding: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.status {
|
||
background-color: #e8f5e9;
|
||
padding: 10px;
|
||
margin: 10px;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.status.connected {
|
||
background-color: #c8e6c9;
|
||
color: #2e7d32;
|
||
}
|
||
|
||
.status.error {
|
||
background-color: #ffcdd2;
|
||
color: #c62828;
|
||
}
|
||
|
||
.controls {
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.controls button {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
background-color: #2196F3;
|
||
color: white;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.controls button:hover {
|
||
background-color: #1976D2;
|
||
}
|
||
|
||
.controls button:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.view-controls {
|
||
padding: 10px 20px;
|
||
background-color: #e3f2fd;
|
||
display: flex;
|
||
gap: 5px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.view-controls button {
|
||
padding: 8px 16px;
|
||
border: 1px solid #2196F3;
|
||
border-radius: 4px;
|
||
background-color: white;
|
||
color: #2196F3;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.view-controls button:hover {
|
||
background-color: #2196F3;
|
||
color: white;
|
||
}
|
||
|
||
.canvas-container {
|
||
position: relative;
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 400px;
|
||
background-color: #f8f8f8;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 10px;
|
||
box-sizing: border-box;
|
||
overflow: hidden; /* 防止滾動條 */
|
||
}
|
||
|
||
#renderCanvas {
|
||
width: calc(100% - 20px); /* 減去 padding */
|
||
height: calc(100% - 20px); /* 減去 padding */
|
||
display: block;
|
||
cursor: grab;
|
||
border: 2px solid #666;
|
||
outline: none;
|
||
transition: border-color 0.3s, box-shadow 0.3s;
|
||
box-sizing: border-box;
|
||
background-color: white; /* 確保有背景色 */
|
||
}
|
||
|
||
#renderCanvas:focus {
|
||
border-color: #4CAF50;
|
||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.4);
|
||
}
|
||
|
||
#renderCanvas:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.log-container {
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
height: 200px;
|
||
overflow-y: auto;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.log-entry {
|
||
margin: 2px 0;
|
||
padding: 2px 5px;
|
||
}
|
||
|
||
.log-entry.info {
|
||
color: #1976D2;
|
||
}
|
||
|
||
.log-entry.error {
|
||
color: #c62828;
|
||
background-color: #ffebee;
|
||
}
|
||
|
||
.log-entry.warning {
|
||
color: #f57c00;
|
||
}
|
||
|
||
.log-entry.debug {
|
||
color: #666;
|
||
font-style: italic;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>HiNC WebAPI 渲染畫布示例</h1>
|
||
|
||
<div style="padding: 10px; background-color: #fff3cd; color: #856404; border: 1px solid #ffeaa7; border-radius: 4px; margin: 10px;">
|
||
<strong>提示:</strong>點擊畫布以啟用鍵盤控制。畫布獲得焦點時邊框會變成綠色。
|
||
<br>鍵盤控制:F1-F4切換視圖,方向鍵旋轉視圖,PageUp/PageDown縮放,Home重置視圖
|
||
</div>
|
||
|
||
<div class="view-controls">
|
||
<button onclick="setView('front')">前視圖</button>
|
||
<button onclick="setView('back')">後視圖</button>
|
||
<button onclick="setView('left')">左視圖</button>
|
||
<button onclick="setView('right')">右視圖</button>
|
||
<button onclick="setView('top')">頂視圖</button>
|
||
<button onclick="setView('bottom')">底視圖</button>
|
||
<button onclick="setView('isometric')">等角視圖</button>
|
||
<button onclick="setView('home')">主視圖</button>
|
||
</div>
|
||
|
||
<div class="canvas-container">
|
||
<canvas id="renderCanvas" tabindex="0"></canvas>
|
||
</div>
|
||
|
||
<div class="log-container" id="logContainer"></div>
|
||
</div>
|
||
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js"></script>
|
||
<script>
|
||
let connection = null;
|
||
let sessionId = null;
|
||
let canvas = null;
|
||
let canvasInitialized = false;
|
||
let compressionSupported = false;
|
||
let isFirefox = false;
|
||
|
||
// 頁面加載時自動連接
|
||
window.addEventListener('load', async () => {
|
||
canvas = document.getElementById('renderCanvas');
|
||
compressionSupported = typeof DecompressionStream !== 'undefined';
|
||
isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||
|
||
console.log('頁面加載完成,開始連接服務器...');
|
||
await connect();
|
||
});
|
||
|
||
// 頁面卸載時斷開連接
|
||
window.addEventListener('beforeunload', () => {
|
||
if (connection && connection.state === signalR.HubConnectionState.Connected) {
|
||
connection.stop();
|
||
}
|
||
});
|
||
|
||
// 處理頁面可見性變化
|
||
document.addEventListener('visibilitychange', async () => {
|
||
console.log(`頁面可見性變化: ${document.visibilityState}`);
|
||
|
||
if (connection && connection.state === signalR.HubConnectionState.Connected) {
|
||
try {
|
||
await connection.invoke("HandleVisibilityChange", document.visibilityState);
|
||
console.log(`已通知服務器可見性狀態: ${document.visibilityState}`);
|
||
} catch (err) {
|
||
console.error("處理可見性變化失敗:", err);
|
||
}
|
||
}
|
||
});
|
||
|
||
async function connect() {
|
||
try {
|
||
connection = new signalR.HubConnectionBuilder()
|
||
.withUrl("/renderingHub")
|
||
.configureLogging(signalR.LogLevel.Information)
|
||
.build();
|
||
|
||
// 設置消息處理器
|
||
connection.on("ImageUpdate", handleImageUpdate);
|
||
connection.on("CanvasInitialized", (id) => {
|
||
sessionId = id;
|
||
canvasInitialized = true;
|
||
console.log("畫布初始化完成,會話ID:", sessionId);
|
||
|
||
// 自動載入測試物件
|
||
loadTestObjects();
|
||
});
|
||
|
||
// 連接到服務器
|
||
await connection.start();
|
||
console.log("已連接到服務器");
|
||
|
||
// 初始化畫布
|
||
const rect = canvas.getBoundingClientRect();
|
||
const width = Math.floor(rect.width);
|
||
const height = Math.floor(rect.height);
|
||
console.log(`初始化畫布 - 寬度: ${width}, 高度: ${height}`);
|
||
|
||
// 設置畫布的實際像素大小
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
|
||
if (width <= 0 || height <= 0) {
|
||
console.error("畫布大小無效,延遲初始化...");
|
||
setTimeout(async () => {
|
||
const newRect = canvas.getBoundingClientRect();
|
||
const newWidth = Math.floor(newRect.width);
|
||
const newHeight = Math.floor(newRect.height);
|
||
console.log(`重新嘗試初始化畫布 - 寬度: ${newWidth}, 高度: ${newHeight}`);
|
||
canvas.width = newWidth;
|
||
canvas.height = newHeight;
|
||
await connection.invoke("InitializeCanvas", newWidth, newHeight);
|
||
}, 100);
|
||
return;
|
||
}
|
||
|
||
await connection.invoke("InitializeCanvas", width, height);
|
||
|
||
// 設置事件處理
|
||
setupMouseEvents();
|
||
setupKeyboardEvents();
|
||
setupTouchEvents();
|
||
|
||
// 監聽窗口大小變化
|
||
window.addEventListener('resize', handleWindowResize);
|
||
|
||
} catch (err) {
|
||
console.error("連接失敗:", err);
|
||
setTimeout(() => connect(), 5000); // 5秒後重試
|
||
}
|
||
}
|
||
|
||
// 斷開連接
|
||
async function disconnect() {
|
||
console.log('===== 開始斷開連接 =====');
|
||
if (connection) {
|
||
try {
|
||
console.log('正在停止 SignalR 連接...');
|
||
await connection.stop();
|
||
console.log('✓ SignalR 連接已停止');
|
||
updateStatus('未連接', false);
|
||
sessionId = null;
|
||
|
||
console.log('清除 Canvas...');
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
console.log('✓ Canvas 已清除');
|
||
|
||
window.frameCount = 0;
|
||
console.log('===== 斷開連接完成 =====');
|
||
} catch (err) {
|
||
console.error('斷開連接失敗: ' + err);
|
||
}
|
||
} else {
|
||
console.warn('沒有活動的連接');
|
||
}
|
||
}
|
||
|
||
// 載入測試對象
|
||
async function loadTestObjects() {
|
||
console.log('開始載入測試對象');
|
||
|
||
if (!sessionId) {
|
||
console.warn('尚未連接到服務器,無法載入測試對象');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const url = `/api/rendering/test-objects/${sessionId}`;
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
console.log('測試對象載入成功');
|
||
} else {
|
||
const errorText = await response.text();
|
||
console.error(`載入測試對象失敗: ${response.status} - ${errorText}`);
|
||
}
|
||
} catch (err) {
|
||
console.error('載入測試對象時出錯:', err);
|
||
}
|
||
}
|
||
|
||
// 設置視圖
|
||
async function setView(viewType) {
|
||
console.log(`===== 切換視圖: ${viewType} =====`);
|
||
|
||
if (connection && connection.state === 'Connected') {
|
||
try {
|
||
console.log(`正在調用 SetView('${viewType}')...`);
|
||
await connection.invoke('SetView', viewType);
|
||
console.log(`✓ 視圖已成功切換到: ${viewType}`);
|
||
} catch (err) {
|
||
console.error(`切換視圖失敗: ${err}`);
|
||
console.error('錯誤詳情: ' + JSON.stringify(err));
|
||
}
|
||
} else {
|
||
console.warn(`無法切換視圖: 連接狀態為 ${connection ? connection.state : 'null'}`);
|
||
}
|
||
}
|
||
|
||
// 渲染幀
|
||
function renderFrame(rgba, width, height) {
|
||
try {
|
||
// 檢查參數
|
||
if (!rgba || !width || !height) {
|
||
console.error(`無效的渲染參數 - rgba: ${rgba ? 'exists' : 'null'}, width: ${width}, height: ${height}`);
|
||
return;
|
||
}
|
||
|
||
// 檢查數據長度
|
||
const expectedLength = width * height * 4;
|
||
if (rgba.length !== expectedLength) {
|
||
console.error(`數據長度不匹配 - 預期: ${expectedLength}, 實際: ${rgba.length}`);
|
||
return;
|
||
}
|
||
|
||
// 確保 rgba 是正確的類型
|
||
let uint8Array;
|
||
if (rgba instanceof Uint8ClampedArray) {
|
||
uint8Array = rgba;
|
||
} else if (rgba instanceof Array || rgba instanceof Uint8Array) {
|
||
uint8Array = new Uint8ClampedArray(rgba);
|
||
} else {
|
||
console.error(`未知的數據類型: ${typeof rgba}`);
|
||
return;
|
||
}
|
||
|
||
const imageData = new ImageData(uint8Array, width, height);
|
||
ctx.putImageData(imageData, 0, 0);
|
||
// 不記錄每一幀,避免日誌過多
|
||
// log('info', '幀已渲染');
|
||
} catch (err) {
|
||
console.error('渲染錯誤: ' + err.message);
|
||
console.error('詳細錯誤:', err);
|
||
}
|
||
}
|
||
|
||
async function handleImageUpdate(compressedData, originalLength, width, height) {
|
||
try {
|
||
console.log(`收到圖像更新 - 寬度: ${width}, 高度: ${height}, 原始長度: ${originalLength}, 壓縮數據類型: ${typeof compressedData}`);
|
||
|
||
let imageData;
|
||
|
||
if (compressionSupported) {
|
||
// 解壓縮數據
|
||
const decompressed = await decompressData(compressedData, originalLength);
|
||
imageData = new Uint8ClampedArray(decompressed);
|
||
} else {
|
||
// 不支持壓縮,直接使用數據
|
||
imageData = new Uint8ClampedArray(compressedData);
|
||
}
|
||
|
||
console.log(`圖像數據準備就緒,長度: ${imageData.length}`);
|
||
|
||
// 創建 ImageData 對象並繪製到畫布
|
||
const ctx = canvas.getContext('2d');
|
||
const imgData = new ImageData(imageData, width, height);
|
||
ctx.putImageData(imgData, 0, 0);
|
||
|
||
console.log('圖像已繪製到畫布');
|
||
|
||
} catch (err) {
|
||
console.error("處理圖像更新時出錯:", err);
|
||
}
|
||
}
|
||
|
||
async function decompressData(compressedData, originalLength) {
|
||
try {
|
||
let bytes;
|
||
|
||
// 檢查輸入類型
|
||
if (compressedData instanceof Uint8Array) {
|
||
bytes = compressedData;
|
||
} else if (compressedData instanceof ArrayBuffer) {
|
||
bytes = new Uint8Array(compressedData);
|
||
} else if (typeof compressedData === 'string') {
|
||
// 如果是 Base64 字符串,轉換為 Uint8Array
|
||
const binaryString = atob(compressedData);
|
||
bytes = new Uint8Array(binaryString.length);
|
||
for (let i = 0; i < binaryString.length; i++) {
|
||
bytes[i] = binaryString.charCodeAt(i);
|
||
}
|
||
} else {
|
||
console.error("未知的壓縮數據類型:", typeof compressedData);
|
||
throw new Error("未知的壓縮數據類型");
|
||
}
|
||
|
||
// 使用 DecompressionStream 解壓縮
|
||
const stream = new ReadableStream({
|
||
start(controller) {
|
||
controller.enqueue(bytes);
|
||
controller.close();
|
||
}
|
||
});
|
||
|
||
const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip'));
|
||
const reader = decompressedStream.getReader();
|
||
const chunks = [];
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
chunks.push(value);
|
||
}
|
||
|
||
// 合併所有塊
|
||
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||
const result = new Uint8Array(totalLength);
|
||
let offset = 0;
|
||
for (const chunk of chunks) {
|
||
result.set(chunk, offset);
|
||
offset += chunk.length;
|
||
}
|
||
|
||
return result;
|
||
} catch (err) {
|
||
console.error("解壓縮失敗:", err);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
function setupMouseEvents() {
|
||
let isMouseDown = false;
|
||
let lastX = 0;
|
||
let lastY = 0;
|
||
let mouseButton = 0;
|
||
|
||
canvas.addEventListener('mousedown', async (e) => {
|
||
if (!canvasInitialized) return;
|
||
|
||
isMouseDown = true;
|
||
mouseButton = e.button;
|
||
const rect = canvas.getBoundingClientRect();
|
||
lastX = e.clientX - rect.left;
|
||
lastY = e.clientY - rect.top;
|
||
|
||
try {
|
||
await connection.invoke("HandleMouseDown", lastX, lastY, e.button);
|
||
} catch (err) {
|
||
console.error("鼠標按下處理失敗:", err);
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('mousemove', async (e) => {
|
||
if (!canvasInitialized) return;
|
||
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
const y = e.clientY - rect.top;
|
||
|
||
// 計算 buttonMask
|
||
const buttonMask = isMouseDown ? (1 << mouseButton) : 0;
|
||
|
||
try {
|
||
await connection.invoke("HandleMouseMove", x, y, buttonMask);
|
||
} catch (err) {
|
||
console.error("鼠標移動處理失敗:", err);
|
||
}
|
||
|
||
lastX = x;
|
||
lastY = y;
|
||
});
|
||
|
||
canvas.addEventListener('mouseup', async (e) => {
|
||
if (!canvasInitialized) return;
|
||
|
||
isMouseDown = false;
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
const y = e.clientY - rect.top;
|
||
|
||
try {
|
||
await connection.invoke("HandleMouseUp", x, y, e.button);
|
||
} catch (err) {
|
||
console.error("鼠標釋放處理失敗:", err);
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('wheel', async (e) => {
|
||
if (!canvasInitialized) return;
|
||
|
||
e.preventDefault();
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
const y = e.clientY - rect.top;
|
||
|
||
// 根據瀏覽器類型調整滾輪縮放因子
|
||
const browserBrand = isFirefox ? 'firefox' : 'chrome';
|
||
|
||
try {
|
||
await connection.invoke("HandleMouseWheel", x, y, e.deltaX, e.deltaY, browserBrand);
|
||
} catch (err) {
|
||
console.error("鼠標滾輪處理失敗:", err);
|
||
}
|
||
});
|
||
|
||
// 防止右鍵菜單
|
||
canvas.addEventListener('contextmenu', (e) => {
|
||
e.preventDefault();
|
||
});
|
||
}
|
||
|
||
function setupKeyboardEvents() {
|
||
// Canvas 焦點管理
|
||
canvas.addEventListener('click', () => {
|
||
console.log('Canvas clicked, focusing...');
|
||
canvas.focus();
|
||
});
|
||
|
||
canvas.addEventListener('focus', () => {
|
||
console.log('Canvas focused');
|
||
canvas.style.borderColor = '#28a745';
|
||
});
|
||
|
||
canvas.addEventListener('blur', () => {
|
||
console.log('Canvas lost focus');
|
||
canvas.style.borderColor = '#dee2e6';
|
||
});
|
||
|
||
// 鍵盤事件
|
||
canvas.addEventListener('keydown', async (e) => {
|
||
console.log(`鍵盤按下: key=${e.key}, code=${e.code}, ctrl=${e.ctrlKey}, shift=${e.shiftKey}, alt=${e.altKey}`);
|
||
|
||
if (!canvasInitialized) {
|
||
console.warn('Canvas 尚未初始化,忽略鍵盤事件');
|
||
return;
|
||
}
|
||
|
||
// 防止瀏覽器默認行為(如 F5 刷新頁面)
|
||
e.preventDefault();
|
||
|
||
try {
|
||
await connection.invoke("HandleKeyDown", e.key, e.code, e.ctrlKey, e.shiftKey, e.altKey);
|
||
console.log('鍵盤按下事件已發送到服務器');
|
||
} catch (err) {
|
||
console.error("鍵盤按下處理失敗:", err);
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('keyup', async (e) => {
|
||
console.log(`鍵盤釋放: key=${e.key}, code=${e.code}`);
|
||
|
||
if (!canvasInitialized) {
|
||
console.warn('Canvas 尚未初始化,忽略鍵盤事件');
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
|
||
try {
|
||
await connection.invoke("HandleKeyUp", e.key, e.code, e.ctrlKey, e.shiftKey, e.altKey);
|
||
console.log('鍵盤釋放事件已發送到服務器');
|
||
} catch (err) {
|
||
console.error("鍵盤釋放處理失敗:", err);
|
||
}
|
||
});
|
||
}
|
||
|
||
function setupTouchEvents() {
|
||
let touchStartX = 0;
|
||
let touchStartY = 0;
|
||
|
||
canvas.addEventListener('touchstart', async (e) => {
|
||
if (!canvasInitialized) return;
|
||
|
||
e.preventDefault();
|
||
const touch = e.touches[0];
|
||
const rect = canvas.getBoundingClientRect();
|
||
touchStartX = touch.clientX - rect.left;
|
||
touchStartY = touch.clientY - rect.top;
|
||
|
||
try {
|
||
await connection.invoke("HandleTouchDown", touch.identifier, touchStartX, touchStartY);
|
||
} catch (err) {
|
||
console.error("觸摸開始處理失敗:", err);
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('touchmove', async (e) => {
|
||
if (!canvasInitialized) return;
|
||
|
||
e.preventDefault();
|
||
const touch = e.touches[0];
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = touch.clientX - rect.left;
|
||
const y = touch.clientY - rect.top;
|
||
|
||
try {
|
||
await connection.invoke("HandleTouchMove", touch.identifier, x, y);
|
||
} catch (err) {
|
||
console.error("觸摸移動處理失敗:", err);
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('touchend', async (e) => {
|
||
if (!canvasInitialized) return;
|
||
|
||
e.preventDefault();
|
||
const touch = e.changedTouches[0];
|
||
|
||
try {
|
||
await connection.invoke("HandleTouchUp", touch.identifier);
|
||
} catch (err) {
|
||
console.error("觸摸結束處理失敗:", err);
|
||
}
|
||
});
|
||
}
|
||
|
||
let resizeTimeout = null;
|
||
|
||
async function handleWindowResize() {
|
||
// 使用防抖來避免頻繁調整
|
||
if (resizeTimeout) {
|
||
clearTimeout(resizeTimeout);
|
||
}
|
||
|
||
resizeTimeout = setTimeout(async () => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
const width = Math.floor(rect.width);
|
||
const height = Math.floor(rect.height);
|
||
|
||
console.log(`調整畫布大小: ${width}x${height}`);
|
||
|
||
// 保存當前畫布內容
|
||
const ctx = canvas.getContext('2d');
|
||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||
|
||
// 更新畫布大小
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
|
||
// 嘗試恢復畫布內容(如果大小相容)
|
||
try {
|
||
if (imageData.width > 0 && imageData.height > 0) {
|
||
ctx.putImageData(imageData, 0, 0);
|
||
}
|
||
} catch (e) {
|
||
console.warn('無法恢復畫布內容,等待服務器重繪');
|
||
}
|
||
|
||
if (connection && connection.state === signalR.HubConnectionState.Connected) {
|
||
try {
|
||
// 通知服務器調整大小並重新渲染
|
||
await connection.invoke("HandleResize", width, height);
|
||
console.log('已通知服務器調整大小');
|
||
} catch (err) {
|
||
console.error("調整大小失敗:", err);
|
||
}
|
||
}
|
||
}, 300); // 300ms 防抖延遲
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |