以移動一個SVG圖形為例,完整代碼如下:
<!doctype html>
<html>
<head>
<style>
body {
margin: 0;
}
/* 最外層容器 */
#content {
/* 阻止頁面大小隨着SVG移動而變化 */
overflow: hidden;
}
/* 固定Header */
#header {
left: 0;
right: 0;
position: fixed;
height: 50px!important;
background-color: rgb(124, 252, 45);
z-index: 10;
}
/* 容納需要跟隨手勢移動的SVG的容器 */
#panel_container {
height: 100vh;
z-index: 5;
background-color: rgb(248, 213, 98);
}
/* 固定Footer */
#footer {
left: 0;
right: 0;
bottom: 0;
position: fixed;
height: 50px!important;
background-color: rgb(124, 252, 45);
z-index: 10;
}
/* 需要跟隨手勢移動的SVG */
svg {
background-color: rgb(250, 250, 153);
width: 100%;
height: 100%;
}
</style>
<script>
// 需要跟隨手勢移動的SVG。頁面加載後賦值
var svg = null;
// 用户操作後期望的SVG縮放比例
var scale = 1;
// 用户操作後期望的SVG中心點的橫向偏移
var posX = 0;
// 用户操作後期望的SVG中心點的縱向偏移
var posY = 0;
// 我們不希望SVG比例過小或偏出屏幕,所以在SVG被移動或縮放到符合用户期望的比例和位置後,還需要讓SVG回彈到以下符合我們期望的比例和位置
// 經過修正後的SVG縮放比例
var finalScale = 1;
// 經過修正後的SVG中心點的橫向偏移
var finalPosX = 0;
// 經過修正後的SVG中心點的縱向偏移
var finalPosY = 0;
// 上次根據用户操作進行渲染的時間戳。每次執行render()時被設置。用户的操作優先於系統的回彈修正,所以只有當前時間距離該時間戳已經過去了一段時間(200毫秒)後,我們才可以開始對SVG進行回彈修正。詳見bounce()
var lastRenderingTime;
// 監聽頁面加載完成事件。為SVG加點元素以便位置參考
window.addEventListener("load", function() {
svg = document.querySelector("svg");
// 簡單加一個小方塊
let newRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
newRect.setAttribute("x", "150");
newRect.setAttribute("y", "150");
newRect.setAttribute("width", "100");
newRect.setAttribute("height", "100");
newRect.setAttribute("fill", "#5cceee");
svg.appendChild(newRect);
});
// 根據用户的操作進行渲染
var render = () => {
// 更新渲染的時間戳
lastRenderingTime = new Date().getTime();
window.requestAnimationFrame(() => {
// 根據新的位置和縮放比例渲染SVG
svg.style.transform = `translate3D(${posX}px, ${posY}px, 0px) scale(${scale})`;
// 如果不想要回彈效果,以下代碼可以去掉
// 讓系統稍後嘗試回彈修正(有必要的話)
setTimeout(bounce, 500);
});
};
// 如果不想要回彈效果,以下bounce()代碼可以去掉
// 根據修正值回彈修正SVG
var bounce = () => {
// 用户操作後的縮放和位置沒有問題,用户期望與系統期望相同,直接返回
if (scale == finalScale && posX == finalPosX && posY == finalPosY) return;
// 用户可能還在操作中,按優先級規則不進行系統操作,而是等待片刻再嘗試
if (new Date().getTime() - lastRenderingTime < 200) {
// 等待500毫秒
setTimeout(bounce, 500);
// 返回
return;
}
// 容納SVG的容器。結合SVG和容納SVG的容器的相對位置和大小來計算回彈量
var ref = document.getElementById("panel_container").getBoundingClientRect();
// SVG當前的縮放比例
var lastScale = svg.getBoundingClientRect().width / ref.width;
// SVG當前的中心點橫向偏移
var lastPosX = svg.getBoundingClientRect().x + svg.getBoundingClientRect().width / 2 - ref.width / 2;
// SVG當前的中心點縱向偏移
var lastPosY = svg.getBoundingClientRect().y + svg.getBoundingClientRect().height / 2 - ref.height / 2;
// 該次回彈的目標比例。與最終修正值差距過小則直接置為最終修正值
var stepScale = Math.abs(finalScale - lastScale) > 0.1 ? (lastScale + Math.sign(finalScale - lastScale) * 0.1) : finalScale;
// 該次回彈的目標中心點橫向偏移。與最終修正值差距過小則直接置為最終修正值
var stepPosX = Math.abs(finalPosX - lastPosX) > 10 ? (lastPosX + (finalPosX - lastPosX) / 10 + Math.sign(finalPosX - lastPosX) * 10) : finalPosX;
// 該次回彈的目標中心點縱向偏移。與最終修正值差距過小則直接置為最終修正值
var stepPosY = Math.abs(finalPosY - lastPosY) > 10 ? (lastPosY + (finalPosY - lastPosY) / 10 + Math.sign(finalPosY - lastPosY) * 10) : finalPosY;
window.requestAnimationFrame(() => {
// 根據目標位置和縮放比例渲染SVG
svg.style.transform = `translate3D(${stepPosX}px, ${stepPosY}px, 0px) scale(${stepScale})`;
});
// 如果已經修正完成則返回
if (stepScale == finalScale && stepPosX == finalPosX && stepPosY == finalPosY) return;
// 繼續嘗試進行下一次修正
setTimeout(bounce, 500);
};
// 監聽觸控板手勢事件。實際為鼠標滾輪事件
window.addEventListener('wheel', (e) => {
// 容納SVG的容器。結合SVG和容納SVG的容器的相對位置和大小來計算修正值
var ref = document.getElementById("panel_container").getBoundingClientRect();
// SVG當前的縮放比例
var lastScale = svg.getBoundingClientRect().width / ref.width;
// SVG當前的中心點橫向偏移
var lastPosX = svg.getBoundingClientRect().x + svg.getBoundingClientRect().width / 2 - ref.width / 2;
// SVG當前的中心點縱向偏移
var lastPosY = svg.getBoundingClientRect().y + svg.getBoundingClientRect().height / 2 - ref.height / 2;
if (e.ctrlKey) {
// 縮放事件
// 根據操作確定縮放值。位置不變
finalScale = scale = lastScale - e.deltaY * 0.01;
finalPosX = posX = lastPosX;
finalPosY = posY = lastPosY;
} else {
// 移動事件
// 根據操作確定位置。縮放值不變
finalScale = scale = lastScale;
finalPosX = posX = lastPosX - e.deltaX * 2;
finalPosY = posY = lastPosY - e.deltaY * 2;
}
// 如果不想要回彈效果,以下對finalXXX的賦值可以直接修改為對XXX的賦值
// 不允許SVG過小(縮放值小於1)
if (scale < 1) finalScale = 1;
// 不允許SVG右邊界進入屏幕內
var minPosX = ref.width / 2 - ref.width * finalScale / 2;
// 不允許SVG左邊界進入屏幕內
var maxPosX = ref.width * finalScale / 2 - ref.width / 2;
// 不允許SVG下邊界進入屏幕內
var minPosY = ref.height / 2 - ref.height * finalScale / 2;
// 不允許SVG上邊界進入屏幕內
var maxPosY = ref.height * finalScale / 2 - ref.height / 2;
// 根據以上限制值確定系統最終修正值
if (posX < minPosX) finalPosX = minPosX;
if (posX > maxPosX) finalPosX = maxPosX;
if (posY < minPosY) finalPosY = minPosY;
if (posY > maxPosY) finalPosY = maxPosY;
// 根據用户操作進行渲染
render();
});
</script>
</head>
<body>
<!-- 最外層容器 -->
<div id='content'>
<!-- 放置一個固定Header為肉眼觀察位移作參考 -->
<div id='header'>
Header
</div>
<!-- 容納需要跟隨手勢移動的SVG的容器 -->
<div id='panel_container'>
<!-- 需要跟隨手勢移動的SVG -->
<svg></svg>
</div>
<!-- 放置一個固定Footer為肉眼觀察位移作參考 -->
<div id='footer'>
Footer
</div>
</div>
</body>
</html>