国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

使用SVG創(chuàng)建平行投影和等角投影的方法以及創(chuàng)建等軸測圖

解如何使用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é)

等角投影無需深度調節(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)先搜索來找到元素的正確順序。

作為第一步,我們需要填充初始圖 - 對于每個對象,我們需要找到其背后的所有對象。我們可以通過比較它們底邊的位置來做到這一點。讓我們用圖像來說明此步驟 - 例如,我們采用三個元素,它們的位置如下:

圖形

圖2

我們在第二張圖像中標記了每個對象的底側。使用這些數據,我們將創(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 索引計算

以上等軸測圖的 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 演示

在本文中,我們使用開源 JointJS 庫進行說明


文章來源地址http://www.zghlxwxcb.cn/article/421.html

到此這篇關于使用SVG創(chuàng)建平行投影和等角投影的方法以及創(chuàng)建等軸測圖的文章就介紹到這了,更多相關內容可以在右上角搜索或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!

原文地址:http://www.zghlxwxcb.cn/article/421.html

如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請聯系站長進行投訴反饋,一經查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 用純C語言實現3D空間中的點坐標轉化為屏幕二維點坐標,包含主視圖、側視圖、俯視圖、正等軸投影

    要實現3D空間中的點坐標轉換為屏幕二維點坐標,需要進行透視變換和投影變換。以下是一些基本的思路和示例代碼,可以用于實現主視圖、側視圖、俯視圖、正等軸投影。 1. 主視圖投影 主視圖投影是指以一個點作為視點,從一個方向觀察物體,投影到一個平面上。通常情況

    2024年02月04日
    瀏覽(23)
  • 圖片轉Svg格式以及壓縮優(yōu)化方法

    圖片轉Svg格式以及壓縮優(yōu)化方法

    除了文中方法,還可以使用 svgo 工具對svg圖片進行壓縮:可參考svgo 前端有些時候需要把像png、jpg等格式的圖片轉為svg格式的矢量圖使用,但是直接去一些網站里轉換,得到的svg代碼往往因為冗余信息實在太多,大得離譜 看著都頭疼 以至于cv到QQ里都發(fā)不出去 所以查了一波資

    2024年02月05日
    瀏覽(24)
  • 微信小程序里使用SVG矢量圖標方法

    微信小程序里使用SVG矢量圖標有2種引入方法: 一、SVG圖標轉換為BASE64編碼 可以在百度搜索’svg在線轉BASE64’(可能會有問題,如下) 如下方式 同時還需要添加 background-size: contain 屬性使圖標能根據元素大小自動縮放 二、使用運程地址引入SVG圖標 把SVG圖標上傳到網站服務器

    2024年02月05日
    瀏覽(27)
  • .net core 創(chuàng)建WebAPI以及使用EF DBFirst框架使用方法與疑問解答(.net 6)

    EF語法包: 生成實體模型: 修改實體模型: 把生成的實體和上下文都輸出到某個文件夾命令 增加JSON格式腳手架: 若想增加某個版本json腳手架,需要加入后綴如: 問題與解決方案: 1、問題: Your startup project \\\'XXX\\\' doesn\\\'t reference Microsoft.EntityFrameworkCore.Design. This package is requi

    2024年02月16日
    瀏覽(27)
  • 使用html2canvas將整個元素導出為圖片,其中包含svg和img,解決img跟svg導出時img或svg(canvg處理)不顯示的問題,以及相關優(yōu)化

    使用html2canvas將整個元素導出為圖片,其中包含svg和img,解決img跟svg導出時img或svg(canvg處理)不顯示的問題,以及相關優(yōu)化

    目錄 前言 一、準備 二、解決問題 1.將svg跟img轉為canvas的方法 2.將base64轉換成file文件的方法 3.點擊下載使用方法 1).對dom沒有處理,需求只是將圖片導出即可 ?2).涉及對dom的拖拽,流程圖之類的(需復制dom,在復制的dom上進行處理) ?三、效果圖如下 查閱很多相關的文章和

    2024年01月20日
    瀏覽(25)
  • 初學前端-記使用阿里圖庫SVG圖標不顯示的解決方法

    初學前端-記使用阿里圖庫SVG圖標不顯示的解決方法

    使用VUE3+Element-Plus做來制作前端界面,做到左側菜單欄時遇到了一個困難,添加的SVG圖標始終不顯示,位置存在,圖標的信息也沒有問題,但是就是一直顯示不出來。 ?后經多方搜索, 經vue前端項目引入iconfont阿里圖標的四種方式_飛歌Fly的博客-CSDN博客的提示在Main.js中導入了

    2024年01月25日
    瀏覽(28)
  • SVG 在前端的7種使用方法,你還知道哪幾種?

    SVG 在前端的7種使用方法,你還知道哪幾種?

    點贊 + 關注 + 收藏 = 學會了 技術一直在演變,在網頁中使用 SVG 的方法也層出不窮。每個時期都有對應的最優(yōu)解。 所以我打算把我知道的 7種 SVG 的使用方法列舉出來,有備無患~ 如果你還知道其他方法,可以在評論區(qū)補充~ ```svg ``` xml 是瀏覽器能讀取的格式,但如果希望 sv

    2024年02月06日
    瀏覽(31)
  • 視覺相機模型以及投影原理推導——(單目)

    視覺相機模型以及投影原理推導——(單目)

    參考文獻:視覺SLAM十四講、視覺慣性SLAM理論與源碼分析、該博客、文中的公式直接引用上面的文章,如有侵權請聯系本人刪除 投影過程 三維世界中的物體(目標點)P反射光線,通過相機光心,投影到相機的感光平面(物理成像平面/像素成像平面),一個個的光線投影點匯

    2024年02月09日
    瀏覽(20)
  • 對數螺旋線(等角螺旋線)數學公式推導

    對數螺旋線(等角螺旋線)數學公式推導

    在某些論文或文章里看到對數(等角)螺旋線的公式: 。但是搜索了一下,通常都是一些講解對數等角螺旋線的性質的結果以及公式結果,并沒有詳細的公式推導。當然對于大多數人只要知道上述公式就好了,當作一個結論公式使用。 當然也有像我這樣的想知道公式怎么推出了

    2023年04月23日
    瀏覽(14)
  • k8s service的概念以及創(chuàng)建方法

    k8s service的概念以及創(chuàng)建方法

    Service 的功能: Service主要用于提供網絡服務,通過Service的定義,能夠為客戶端應用提供穩(wěn)定的訪問地址(域名或IP地址)和負載均衡功能,以及屏蔽后端Endpoint的變化,是K8s實現微服務的核心資源。 Service 主要解決了什么問題: 引入Service主要是解決Pod的動態(tài)變化,通過創(chuàng)建

    2024年03月08日
    瀏覽(24)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包