解如何使用SVG創(chuàng)建和操作平行投影和等角投影,以及如何利用JointJS庫定義這些對象,以實現三維物體在二維空間中的可視化。掌握平行投影和等角投影的概念和實現方法,為數字藝術家、工程師和建筑師提供了更多樣性的創(chuàng)作和展示方式。
在二維空間中可視化三維物體有多種方法。例如,大多數3D圖形引擎都使用透視投影作為主要的投影形式。這是因為透視投影是現實世界的絕佳表示,其中物體隨著距離的增加而變小。但當物體的相對位置并不重要,并且為了更好地了解物體的大小時,可以使用平行投影。它們在工程和建筑中更常見,保持平行線很重要。自從計算機圖形學誕生以來,當 3D 渲染硬件加速無法實現時,這些投影就被用來渲染 3D 場景。最近,各種形式的平行投影已成為數字藝術家的風格選擇,它們通常用于顯示信息圖表和數字藝術中的對象。
本文的目的是展示如何在 SVG 中創(chuàng)建和操作等距視圖,以及如何使用(特別是)JointJS 庫來定義這些對象。為了說明 SVG 創(chuàng)建平行投影的功能,我們將使用等角投影作為示例。此投影是主要的投影類型之一,因為它允許您保持對象沿所有軸的相對比例。
等角投影
讓我們定義什么是等角投影。首先,它是一種平行類型的投影,其中來自“相機”的所有線都是平行的。這意味著物體的比例不取決于“相機”和物體之間的距離。具體來說,在等距(希臘語中的意思是“等量”)投影中,沿每個軸的縮放比例是相同的。這是通過在所有軸之間定義相等的角度來實現的。
在下圖中,您可以看到軸在等角投影中的定位方式。請記住,在本文中,我們將使用左手坐標系。
等角投影的特點之一是它可以解構為三種不同的二維投影:頂投影、側投影和正投影。例如,長方體可以由每個 2D 投影上的三個矩形表示,然后組合成一個等角視圖。下一個圖像表示使用左手坐標系的對象的單獨投影。
正交投影的單獨視圖
然后,我們可以將它們組合成一個等軸測視圖:
示例對象的等距視圖
SVG 面臨的挑戰(zhàn)是它包含位于一個 XY 平面上的 2D 對象。但我們可以通過將所有投影組合在一個平面上,然后分別對每個對象應用變換來克服這個問題。
SVG 等軸測視圖轉換
在 3D 中,要創(chuàng)建等距視圖,我們可以將相機移動到某個位置,但 SVG 純粹是 2D 格式,因此我們必須創(chuàng)建一個解決方法來構建這樣的視圖。根據本文,我們需要分別為對象的每個 2D 投影創(chuàng)建變換。
首先,我們需要將平面旋轉 30 度。然后,我們將 2D 圖像傾斜 -30 度。此變換將使我們的軸與等角投影的軸對齊。
然后,我們需要使用縮放運算符將 2D 投影垂直縮小 0.8602。由于等角投影失真的事實,我們需要這樣做。
讓我們介紹一些 SVG 功能,這些功能將幫助我們實現等角投影。SVG 規(guī)范允許用戶在SVG 元素的transform 屬性中指定特定的轉換。此屬性幫助我們對 SVG 元素應用線性變換。要將 2D 投影轉換為等軸測視圖,我們需要應用縮放、旋轉和傾斜運算符。
為了在代碼中表示變換,我們可以使用DOMMatrixReadOnly對象(瀏覽器 API)來表示變換矩陣。使用這個接口,我們可以創(chuàng)建一個矩陣,如下所示:
const isoMatrix = new DOMMatrixReadOnly() .rotate(30) .skewX(-30) .scale(1, 0.8602);
該接口允許使用我們的值構建變換矩陣,然后我們可以使用 matrix 函數將結果值應用到 transform 屬性。
在 SVG 中,我們一次只能呈現一個 2D 空間,因此對于我們的轉換,我們將使用頂部投影作為基礎投影。這主要是因為該投影中的軸與普通 SVG 視口中的軸相對應。
為了演示 SVG 的可能性,我們將使用 JointJS 庫。我們在 XY 平面中定義了一個單元寬度為 20 的矩形網格。讓我們?yōu)槭纠许敳客队吧系脑囟x SVG。為了正確渲染該對象,我們需要為對象的兩個級別指定兩個多邊形。此外,我們可以使用 DOMMatrix 在 2D 空間中對元素應用平移轉換:
// Top1 元素的平移轉換 const matrix2D = new DOMMatrixReadOnly() .translate(200, 200);
<!--Top1 element--> <polygon joint-selector="body" id="v-4" stroke-width="2" stroke="#333333" fill="#ff0000" fill-opacity="0.7" points="0,0 60,0 60,20 40,20 40,60 0,60" transform="matrix(1,0,0,1,200,200)"> </polygon> <!--Top2 element--> <polygon joint-selector="body" id="v-6" stroke-width="2" stroke="#333333" fill="#ff0000" fill-opacity="0.7" points="0,0 20,0 20,40 0,40" transform="matrix(1,0,0,1,240,220)"> </polygon>
然后,我們可以將等距矩陣應用于我們的元素。此外,我們將添加一個翻譯轉換以將元素放置在正確的位置:
const isoMatrix = new DOMMatrixReadOnly() .rotate(30) .skewX(-30) .scale(1, 0.8602); const top1Matrix = isoMatrix.translate(200, 200); const top2Matrix = isoMatrix.translate(240, 220);
等距視圖,無需高度調整
為簡單起見,我們假設元素的基平面位于 XY 平面上。因此,我們需要平移頂視圖,以便將其視為位于對象的頂部。為此,我們只需按縮放后的 SVG 空間上的 Z 坐標平移投影,如下所示。Top1 元素的標高為 80,因此我們應該將其平移 (-80, -80)。同樣,Top2 元素的標高為 40。我們可以將這些轉換應用到現有矩陣:
const top1MatrixWithHeight = top1Matrix.translate(-80, -80); const top2MatrixWithHeight = top1Matrix.translate(-40, -40);
頂部投影的最終等角視圖
最后,我們將擁有和元素transform的以下屬性。請注意,它們僅在最后兩個值上有所不同,這兩個值表示平移轉換:Top1Top2
// Top1 element transform="matrix(0.8660254037844387,0.49999999999999994,-0.8165000081062317,0.47140649947346464,5.9,116.6)" // Top2 element transform="matrix(0.8660254037844387,0.49999999999999994,-0.8165000081062317,0.47140649947346464,26.2,184.9)"
要創(chuàng)建側面和正面投影的等距視圖,我們需要制作一個網,以便可以將所有投影放置在 2D SVG 空間上。讓我們通過附加類似于經典立方體網絡的側視圖和前視圖來創(chuàng)建一個網絡:
然后,我們需要將skewX側面和正面投影成45度。它將允許我們對齊所有投影的 Z 軸。經過這個變換,我們將得到如下圖像:
準備好的二維投影
然后,我們可以將 isoMatrix 應用于該對象:
等角投影無需深度調節(jié)
在每個投影中,都有具有不同第三坐標值的部分。因此,我們需要為每個投影調整深度坐標,就像我們對頂部投影及其 Z 坐標所做的那樣。最終我們會得到如下的等軸測圖:
物體的最終等距視圖
使用 JointJS 繪制等距圖
由于其元素框架和廣泛的工具集,JointJS 使我們能夠輕松創(chuàng)建和操作此類對象。使用JointJS,我們可以定義和控制等距對象來構建強大的等距圖。
還記得文章開頭的基本等距變換嗎?
const isoMatrix = new DOMMatrixReadOnly() .rotate(30) .skewX(-30) .scale(1, 0.8602);
在 JointJS 庫中,我們可以將此轉換應用于存儲所有 SVG 元素的整個對象,然后簡單地在此基礎上應用特定于對象的轉換。
等距網格渲染
JointJS 在渲染自定義 SVG 標記方面具有強大的功能。利用 JointJS,我們可以生成一條與未轉換的網格對齊的路徑,并使其隨網格自動轉換,這要歸功于我們之前提到的全局紙張轉換。您可以在下面的演示中看到網格以及我們如何解釋坐標系。請注意,我們可以動態(tài)更改紙張轉換,這使我們能夠動態(tài)更改視圖:
以上等距網格圖片顯示代碼如下:
<!--css--> <style> #paper-container {position:absolute;right:0;top:0;left:0;bottom:0;} #logo {position:absolute;bottom:20px;right:0;} label {position:absolute;top:30px;right:30px;font-family:sans-serif;} label input {vertical-align:text-top;} </style> <!--html--> <div id="paper-container"></div> <label> <span>Isometric Transformation:</span> <input type="checkbox" id="isometric-switch" checked /> </label> <a target="_blank" href="http://www.zghlxwxcb.cn"> <img id="logo" src="http://www.zghlxwxcb.cn/style/defalut/img/logo.png" width="200" height="50"></img> </a> <!--JS--> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jointjs@3.7.2/dist/joint.min.js"></script> <script> const { dia, shapes, util } = joint; const GRID_SIZE = 20; const GRID_COUNT = 12; // Matrix of the isometric transformation and its parameters const SCALE = 1; const ISOMETRIC_SCALE = 0.8602; const ROTATION_DEGREES = 30; const transformationMatrix = () => { return V.createSVGMatrix() .translate(GRID_SIZE * GRID_COUNT, GRID_SIZE) .rotate(ROTATION_DEGREES) .skewX(-ROTATION_DEGREES) .scaleNonUniform(SCALE, SCALE * ISOMETRIC_SCALE); }; // Paper const cellNamespace = { ...shapes }; const graph = new dia.Graph({}, { cellNamespace }); const paper = new dia.Paper({ el: document.getElementById("paper-container"), model: graph, restrictTranslate: { x: 0, y: 0, width: GRID_SIZE * GRID_COUNT, height: GRID_SIZE * GRID_COUNT }, width: "100%", height: "100%", gridSize: GRID_SIZE, async: true, autoFreeze: true, sorting: dia.Paper.sorting.APPROX, cellViewNamespace: cellNamespace }); // Make the paper isometric by applying the isometric matrix to all // SVG content it contains. paper.matrix(transformationMatrix()); const gVEl = V("g", { fill: "#ed2637" }); const rectVEl = V("rect", { width: GRID_SIZE, height: GRID_SIZE, stroke: "#ed2637", "stroke-width": 1 }); const textVEl = V("text").attr({ "text-anchor": "start", x: 2 * GRID_SIZE, "font-size": GRID_SIZE, "font-family": "sans-serif", stroke: "white", "stroke-width": 3, "paint-order": "stroke" }); gVEl.append([rectVEl, textVEl]); paper.el.addEventListener( "mousemove", (evt) => { const { x, y } = paper.clientToLocalPoint(evt.clientX, evt.clientY); const i = Math.floor(x / GRID_SIZE); const j = Math.floor(y / GRID_SIZE); drawCoordinates(paper, i, j); }, false ); drawGrid(paper); drawCoordinates(paper, 0, 0); // Add switch to toggle the isometric view with 2d for demonstration purposes document .getElementById("isometric-switch") .addEventListener("change", (evt) => { if (evt.target.checked) { paper.matrix(transformationMatrix()); } else { paper.matrix( V.createSVGMatrix().translate(GRID_SIZE * GRID_COUNT, GRID_SIZE) ); } }); // A function to draw the grid. function drawGrid(paper) { const gridData = []; const j = GRID_COUNT; for (let i = 0; i <= j; i++) { gridData.push(`M 0,${i * GRID_SIZE} ${j * GRID_SIZE},${i * GRID_SIZE}`); gridData.push(`M ${i * GRID_SIZE},0 ${i * GRID_SIZE},${j * GRID_SIZE}`); } const gridEl = V("path").attr({ d: gridData.join(" "), fill: "none", stroke: "lightgray" }).node; // When the grid is appended to one of the paper's layer, it gets automatically transformed // by the isometric matrix paper.getLayerNode(dia.Paper.Layers.BACK).append(gridEl); } // A function to highlight a point in the grid function drawCoordinates(paper, i, j) { textVEl.text(`x: ${i} y: ${j}`, { verticalTextAnchor: "middle" }); gVEl.attr("transform", `translate(${i * GRID_SIZE},${j * GRID_SIZE})`); if (i >= 0 && j >= 0 && i < GRID_COUNT && j < GRID_COUNT) { if (!gVEl.node.isConnected) { gVEl.appendTo(paper.getLayerNode(dia.Paper.Layers.BACK)); } } else { gVEl.remove(); } } </script>
創(chuàng)建自定義等距 SVG 元素
在這里,我們在 JointJS 中展示了自定義 SVG 等距形狀。在我們的示例中,我們使用該isometricHeight屬性來存儲有關第三維的信息,然后使用它來渲染我們的等距對象。以下代碼片段顯示了如何調用自定義createIsometricElement函數來更改對象屬性:
const element = createIsometricElement({ isometricHeight: GRID_SIZE * 3, size: { width: GRID_SIZE * 3, height: GRID_SIZE * 6 }, position: { x: GRID_SIZE * 6, y: GRID_SIZE * 6 } });
在下面的演示中,您可以看到我們的自定義等距元素可以像等距網格上的普通元素一樣移動。createIsometricElement您可以通過更改源代碼中函數的參數來更改尺寸(當您單擊“在 CodePen 上編輯”時):
以上等距網格上的自定義等距元素效果圖的代碼如下:
<!--css--> <style> #paper-container {position:absolute;right:0;top:0;left:0;bottom:0;} #logo {position:absolute;bottom:20px;right:0;} label {position:absolute;top:30px;right:30px;font-family:sans-serif;} label input {vertical-align:text-top;} </style> <!--html--> <div id="paper-container"></div> <label> <span>Isometric Transformation:</span> <input type="checkbox" id="isometric-switch" checked /> </label> <a target="_blank" href="http://www.zghlxwxcb.cn"> <img id="logo" src="http://www.zghlxwxcb.cn/style/defalut/img/logo.png" width="200" height="50"></img> </a> <!--JS--> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jointjs@3.7.2/dist/joint.min.js"></script> <script> const { dia, shapes, util } = joint; const GRID_SIZE = 20; const GRID_COUNT = 12; // 確保 Z 方向元素可見性的值 const PAPER_Z_OFFSET = GRID_SIZE * 4; // 等距變換矩陣及其參數 const SCALE = 1; const ISOMETRIC_SCALE = 0.8602; const ROTATION_DEGREES = 30; const transformationMatrix = () => { return V.createSVGMatrix() .translate(GRID_COUNT * GRID_SIZE * SCALE * ISOMETRIC_SCALE + GRID_SIZE, PAPER_Z_OFFSET + GRID_SIZE) .rotate(ROTATION_DEGREES) .skewX(-ROTATION_DEGREES) .scaleNonUniform(SCALE, SCALE * ISOMETRIC_SCALE); }; // 這里我們指定元素標記和 // 標記部分的常量屬性 const IsometricElement = dia.Element.define( 'IsometricElement', { attrs: { top1: { strokeWidth: 2, stroke: '#333333', fill: '#ff0000', fillOpacity: 0.7, }, top2: { strokeWidth: 2, stroke: '#333333', fill: '#ff0000', fillOpacity: 0.7, }, side1: { strokeWidth: 2, stroke: '#333333', fill: '#ffff00', fillOpacity: 0.7, }, side2: { strokeWidth: 2, stroke: '#333333', fill: '#ffff00', fillOpacity: 0.7 }, front1: { strokeWidth: 2, stroke: '#333333', fill: '#0000ff', fillOpacity: 0.7, }, front2: { strokeWidth: 2, stroke: '#333333', fill: '#0000ff', fillOpacity: 0.7, } } }, { markup: util.svg/* xml */ ` <polygon @selector="top1"></polygon> <polygon @selector="top2"></polygon> <polygon @selector="side1"></polygon> <polygon @selector="side2"></polygon> <polygon @selector="front1"></polygon> <polygon @selector="front2"></polygon> ` } ); // 將尺寸參數轉換為標記零件的路徑屬性 // 這樣可以以更靈活的方式創(chuàng)建元素 const createIsometricElement = (properties) => { const d = { x: properties.size.width, y: properties.size.height, z: properties.isometricHeight }; properties.attrs = properties.attrs || {}; properties.attrs.top1 = { points: `0,0 ${d.x},0 ${d.x},${GRID_SIZE} ${d.x - GRID_SIZE},${GRID_SIZE} ${d.x - GRID_SIZE},${d.y} 0,${d.y}`, transform: `translate(${-d.z}, ${-d.z})` }; properties.attrs.top2 = { points: `${d.x - GRID_SIZE},${GRID_SIZE} ${d.x},${GRID_SIZE} ${d.x},${d.y} ${d.x - GRID_SIZE},${d.y}`, transform: `translate(${-d.z + GRID_SIZE * 2}, ${-d.z + GRID_SIZE * 2})` }; properties.attrs.side1 = { points: `0,0 ${d.x - GRID_SIZE},0 ${d.x - GRID_SIZE},${GRID_SIZE * 2} ${d.x},${GRID_SIZE * 2} ${d.x},${d.z} 0,${d.z}`, transform: V.matrixToTransformString( new DOMMatrixReadOnly() .translate(-d.z, -d.z + d.y) .skewX(45) ) }; properties.attrs.side2 = { points: `0,0 ${GRID_SIZE},0 ${GRID_SIZE},${GRID_SIZE * 2} 0,${GRID_SIZE * 2}`, transform: V.matrixToTransformString( new DOMMatrixReadOnly() .translate(-d.z + d.x - GRID_SIZE, -d.z + GRID_SIZE) .skewX(45) ) }; properties.attrs.front1 = { points: `0,0 ${d.z},0 ${d.z},${d.y} ${GRID_SIZE * 2},${d.y}, ${GRID_SIZE * 2},${GRID_SIZE} 0,${GRID_SIZE}`, transform: V.matrixToTransformString( new DOMMatrixReadOnly() .translate(-d.z + d.x, -d.z) .skewY(45) ) }, properties.attrs.front2 = { points: `0,0 ${GRID_SIZE * 2},0 ${GRID_SIZE * 2},${d.y - GRID_SIZE} 0,${d.y - GRID_SIZE}`, transform: V.matrixToTransformString( new DOMMatrixReadOnly() .translate(-d.z + d.x - GRID_SIZE, -d.z + GRID_SIZE) .skewY(45) ) }; return new IsometricElement(properties); }; // Paper const cellNamespace = { ...shapes, IsometricElement }; const graph = new dia.Graph({}, { cellNamespace }); const paper = new dia.Paper({ el: document.getElementById('paper-container'), model: graph, restrictTranslate: { x: 0, y: 0, width: GRID_SIZE * GRID_COUNT, height: GRID_SIZE * GRID_COUNT }, width: '100%', height: '100%', gridSize: GRID_SIZE, async: true, autoFreeze: true, sorting: dia.Paper.sorting.APPROX, cellViewNamespace: cellNamespace }); //通過將等距矩陣應用于所有紙張,使紙張等距 // 它包含的 SVG 內容。 paper.matrix(transformationMatrix()); // 將等距元素添加到圖形中。 // 您可以使用元素的大小和附加 z 參數指定元素的尺寸 const element = createIsometricElement({ isometricHeight: GRID_SIZE * 4, size: { width: GRID_SIZE * 3, height: GRID_SIZE * 6 }, position: { x: GRID_SIZE * 6, y: GRID_SIZE * 6 } }); element.addTo(graph); // 繪制網格的函數。 drawGrid(paper); function drawGrid(paper) { const gridData = []; const j = GRID_COUNT; for (let i = 0; i <= j; i++) { gridData.push(`M 0,${i * GRID_SIZE} ${j * GRID_SIZE},${i * GRID_SIZE}`); gridData.push(`M ${i * GRID_SIZE},0 ${i * GRID_SIZE},${j * GRID_SIZE}`); } const gridEl = V('path').attr({ d: gridData.join(' '), fill: 'none', stroke: 'lightgray' }).node; //當網格附加到紙張的某一層時,它會自動轉換 // 通過等距矩陣 paper.getLayerNode(dia.Paper.Layers.BACK).append(gridEl); } // 添加開關以切換 2d 等距視圖以用于演示目的 document .getElementById('isometric-switch') .addEventListener('change', (evt) => { if (evt.target.checked) { paper.matrix(transformationMatrix()); } else { paper.matrix( V.createSVGMatrix().translate( GRID_SIZE * GRID_COUNT, PAPER_Z_OFFSET + GRID_SIZE ) ); } }); </script>
等軸測圖中的 Z 索引計算
等距視圖的問題之一是將元素分別放置到它們的相對位置。與 2D 平面不同,在等軸測視圖中,物體具有感知高度,并且可以一個一個地放置在另一個物體后面。我們可以通過將它們以正確的順序放入 DOM 中來在 SVG 中實現此行為。為了在我們的例子中定義順序,我們可以使用 JointJSz屬性,它允許將正確的元素發(fā)送到后臺,以便它可以按預期被其他元素重疊/隱藏。您可以在Andreas Hager 撰寫的一篇精彩文章中找到有關此問題的更多信息。
我們決定使用拓撲排序算法對元素進行排序。該算法由兩個步驟組成。首先,我們需要創(chuàng)建一個特殊的圖,然后我們需要對該圖使用深度優(yōu)先搜索來找到元素的正確順序。
作為第一步,我們需要填充初始圖 - 對于每個對象,我們需要找到其背后的所有對象。我們可以通過比較它們底邊的位置來做到這一點。讓我們用圖像來說明此步驟 - 例如,我們采用三個元素,它們的位置如下:
我們在第二張圖像中標記了每個對象的底側。使用這些數據,我們將創(chuàng)建一個圖形結構,該結構將建模元素之間的拓撲關系。在圖像中,您可以看到我們如何定義底部的點 - 我們可以通過比較aMax和bMin點來找到所有元素的相對位置。我們定義,如果pointx和y的坐標bMin小于 point 的坐標aMax,則 objectb位于 object 的后面a。
二維空間中的算法數據
比較前面示例中的三個元素,我們可以生成下圖:
拓撲圖
之后,我們需要使用深度優(yōu)先搜索算法的變體來找到正確的渲染順序。深度優(yōu)先搜索允許我們根據可見性順序訪問圖節(jié)點,從最遠的節(jié)點開始。這是該算法的一個與庫無關的示例:
const sortElements = (elements: Rect[]) => { const nodes = elements.map((el) => { return { el: el, behind: [], visited: false, depth: null, }; }); for (let i = 0; i < nodes.length; ++i) { const a = nodes[i].el; const aMax = aBBox.bottomRight(); for (let j = 0; j < nodes.length; ++j) { if (i != j) { const b = nodes[j].el; const bMin = bBBox.topLeft(); if (bMin.x < aMax.x && bMin.y < aMax.y) { nodes[i].behind.push(nodes[j]); } } } } const sortedElements = depthFirstSearch(nodes); return sortedElements; }; const depthFirstSearch = (nodes) => { let depth = 0; let sortedElements = []; const visitNode = (node) => { if (!node.visited) { node.visited = true; for (let i = 0; i < node.behind.length; ++i) { if (node.behind[i] == null) { break; } else { visitNode(node.behind[i]); delete node.behind[i]; } } node.depth = depth++; sortedElements.push(node.el); } }; for (let i = 0; i < nodes.length; ++i) { visitNode(nodes[i]); } return sortedElements; };
使用 JointJS 庫可以輕松實現此方法 - 在下面的 CodePen 中,每當元素的位置發(fā)生更改時,我們都會使用特殊的 JointJS 事件來重新計算元素的 z 索引。如上所述,我們使用z元素模型的特殊屬性來指定渲染順序并在深度優(yōu)先遍歷期間分配它。(請注意,由于等距對象實現的性質,在元素相交的情況下,算法的行為是未定義的。)
以上等軸測圖的 Z 索引計算效果顯示圖代碼如下:
<!--css--> <style> #paper-container {position:absolute;right:0;top:0;left:0;bottom:0;} #logo {position:absolute;bottom:20px;right:0;} label {position:absolute;top:30px;right:30px;font-family:sans-serif;} label input {vertical-align:text-top;} </style> <!--html--> <div id="paper-container"></div> <label> <span>Isometric Transformation:</span> <input type="checkbox" id="isometric-switch" checked /> </label> <a target="_blank" href="http://www.zghlxwxcb.cn"> <img id="logo" src="http://www.zghlxwxcb.cn/style/defalut/img/logo.png" width="200" height="50"></img> </a> <!--JS--> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jointjs@3.7.2/dist/joint.min.js"></script> <script> const { dia, shapes, util } = joint; const GRID_SIZE = 20; const GRID_COUNT = 12; // 確保 Z 方向元素可見性的值 const PAPER_Z_OFFSET = GRID_SIZE * 4; // 等距變換矩陣及其參數 const SCALE = 1; const ISOMETRIC_SCALE = 0.8602; const ROTATION_DEGREES = 30; const transformationMatrix = () => { return V.createSVGMatrix() .translate(GRID_COUNT * GRID_SIZE * SCALE * ISOMETRIC_SCALE + GRID_SIZE, PAPER_Z_OFFSET + GRID_SIZE) .rotate(ROTATION_DEGREES) .skewX(-ROTATION_DEGREES) .scaleNonUniform(SCALE, SCALE * ISOMETRIC_SCALE); }; // 這里我們指定元素的標記和 // 標記部分的常量屬性 const IsometricPyramid = dia.Element.define( 'IsometricPyramid', { attrs: { front: { strokeWidth: 1, stroke: '#333333', fillOpacity: '0.8' }, side: { strokeWidth: 1, stroke: '#333333', fillOpacity: '0.8' } } }, { markup: util.svg/* xml */ ` <polygon @selector="front"></polygon> <polygon @selector="side"></polygon> ` } ); const IsometricRectangularPrism = dia.Element.define( 'IsometricPyramid', { attrs: { top: { strokeWidth: 1, stroke: '#333333', fillOpacity: '0.8' }, front: { strokeWidth: 1, stroke: '#333333', fillOpacity: '0.8' }, side: { strokeWidth: 1, stroke: '#333333', fillOpacity: '0.8' } } }, { markup: util.svg/* xml */ ` <polygon @selector="top"></polygon> <polygon @selector="front"></polygon> <polygon @selector="side"></polygon> ` } ); // 將尺寸參數轉換為標記零件的路徑屬性 // 這樣可以以更靈活的方式創(chuàng)建元素 const createIsometricPyramid = (properties) => { const d = { x: properties.size.width, y: properties.size.height, z: properties.isometricHeight }; properties.attrs = properties.attrs || {}; properties.attrs.front = { fill: properties.color, points: `${(d.x / 2) - d.z},${(d.y / 2) - d.z} ${d.x},0 ${d.x},${d.y}`, }; properties.attrs.side = { fill: properties.color, points: `${(d.x / 2) - d.z},${(d.y / 2) - d.z} ${d.x},${d.y} 0,${d.y}`, }; return new IsometricPyramid(properties); }; const createIsometricRectangularPrism = (properties) => { const d = { x: properties.size.width, y: properties.size.height, z: properties.isometricHeight }; properties.attrs = properties.attrs || {}; properties.attrs.top = { fill: properties.color, points: `0,0 ${d.x},0 ${d.x},${d.y} 0,${d.y}`, transform: `translate(${-d.z},${-d.z})`, }; properties.attrs.side = { fill: properties.color, points: `0,0 ${d.x},0 ${d.x},${d.z} 0,${d.z}`, transform: V.matrixToTransformString( new DOMMatrixReadOnly() .translate(-d.z, -d.z + d.y) .skewX(45) ) }; properties.attrs.front = { fill: properties.color, points: `0,0 ${d.z},0 ${d.z},${d.y} 0,${d.y}`, transform: V.matrixToTransformString( new DOMMatrixReadOnly() .translate(-d.z + d.x, -d.z) .skewY(45) ) }; return new IsometricRectangularPrism(properties); }; // Z-index 計算 const topologicalSort = (nodes) => { let depth = 0; const visitNode = (node) => { if (!node.visited) { node.visited = true; for (let i = 0; i < node.behind.length; ++i) { if (node.behind[i] == null) { break; } else { visitNode(node.behind[i]); delete node.behind[i]; } } node.depth = depth++; node.el.set('z', node.depth); } } for (let i = 0; i < nodes.length; ++i) { visitNode(nodes[i]); } } const sortElements = (graph) => { const elements = graph.getElements(); const nodes = elements.map(el => { return { el: el, behind: [], visited: false } }); for (let i = 0; i < nodes.length; ++i) { const a = nodes[i].el; const aBBox = a.getBBox(); cellBBoxes[a.id].setAttribute('width', aBBox.width); cellBBoxes[a.id].setAttribute('height', aBBox.height); cellBBoxes[a.id].setAttribute('x', aBBox.x) cellBBoxes[a.id].setAttribute('y', aBBox.y) const aMax = aBBox.bottomRight(); for (let j = 0; j < nodes.length; ++j) { if (i != j) { const b = nodes[j].el; const bBBox = b.getBBox(); const bMin = bBBox.topLeft(); if (bMin.x < aMax.x && bMin.y < aMax.y) { nodes[i].behind.push(nodes[j]); } } } } topologicalSort(nodes); return nodes; } // Paper const cellNamespace = { ...shapes, IsometricPyramid, IsometricRectangularPrism }; const graph = new dia.Graph({}, { cellNamespace }); const paper = new dia.Paper({ el: document.getElementById('paper-container'), model: graph, restrictTranslate: { x: 0, y: 0, width: GRID_SIZE * GRID_COUNT, height: GRID_SIZE * GRID_COUNT }, width: '100%', height: '100%', gridSize: GRID_SIZE, async: true, autoFreeze: true, sorting: dia.Paper.sorting.APPROX, cellViewNamespace: cellNamespace }); // 通過將等距矩陣應用于所有紙張,使紙張等距 // 它包含的 SVG 內容。 paper.matrix(transformationMatrix()); // 將等距元素添加到圖表中。 const pyramid = createIsometricPyramid({ isometricHeight: GRID_SIZE * 4, color: '#ff0000', size: { width: GRID_SIZE * 2, height: GRID_SIZE * 3 }, position: { x: GRID_SIZE * 6, y: GRID_SIZE * 6 } }); const prism = createIsometricRectangularPrism({ isometricHeight: GRID_SIZE * 2, color: '#00ff00', size: { width: GRID_SIZE * 2, height: GRID_SIZE * 3 }, position: { x: GRID_SIZE * 2, y: GRID_SIZE * 2 } }); const prism2 = createIsometricRectangularPrism({ isometricHeight: GRID_SIZE * 1, color: '#0000ff', size: { width: GRID_SIZE * 1, height: GRID_SIZE * 2 }, position: { x: GRID_SIZE * 8, y: GRID_SIZE * 8 } }); graph.addCells([pyramid, prism, prism2]); //繪制網格的函數。 drawGrid(paper); function drawGrid(paper) { const gridData = []; const j = GRID_COUNT; for (let i = 0; i <= j; i++) { gridData.push(`M 0,${i * GRID_SIZE} ${j * GRID_SIZE},${i * GRID_SIZE}`); gridData.push(`M ${i * GRID_SIZE},0 ${i * GRID_SIZE},${j * GRID_SIZE}`); } const gridEl = V('path').attr({ d: gridData.join(' '), fill: 'none', stroke: 'lightgray' }).node; // 當網格附加到紙張的某一層時,它會自動轉換 //通過等距矩陣 paper.getLayerNode(dia.Paper.Layers.BACK).append(gridEl); } const cellBBoxes = {} graph.getCells().forEach(cell => { cellBBoxes[cell.id] = V('rect', { fill: '#888888', stroke: '#000000', 'stroke-width': 2 }); cellBBoxes[cell.id].appendTo(paper.getLayerNode(dia.Paper.Layers.BACK)); }); graph.on('change:position', () => { sortElements(graph); }); sortElements(graph); // 添加開關以切換 2d 等距視圖以用于演示目的 document .getElementById('isometric-switch') .addEventListener('change', (evt) => { if (evt.target.checked) { paper.matrix(transformationMatrix()); } else { paper.matrix( V.createSVGMatrix().translate( GRID_SIZE * GRID_COUNT, PAPER_Z_OFFSET + GRID_SIZE ) ); } }); </script>
JointJS 演示
我們創(chuàng)建了一個 JointJS 演示,它結合了所有這些方法和技術,還允許您在 2D 和等距 SVG 標記之間輕松切換。至關重要的是,正如您所看到的,JointJS 的強大功能(它允許我們移動元素、將它們與鏈接連接以及創(chuàng)建編輯它們的工具等)在等距視圖中的工作效果與在 2D 視圖中的工作效果一樣好。
您可以在此處查看演示。
在本文中,我們使用開源 JointJS 庫進行說明文章來源:http://www.zghlxwxcb.cn/article/421.html
文章來源地址http://www.zghlxwxcb.cn/article/421.html
到此這篇關于使用SVG創(chuàng)建平行投影和等角投影的方法以及創(chuàng)建等軸測圖的文章就介紹到這了,更多相關內容可以在右上角搜索或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!