웹 어셈블리 드로잉 4: CSS

지금까지 우리는svg,canvas,webgl 세 가지 방식으로 도표를 완성했다.이번에 우리는 CSS를 사용하여 물건을 그리려고 시도할 것이다.이것은 CSS가 더욱 구체적이어서 일반 그림에 적합하지 않기 때문에 더 많은 생각을 필요로 한다.

본보기


예전과 같이 우리는 이 견본에서부터 시작한다.
function windowValue(v, vmin, vmax, flipped = false) {
    v = flipped ? -v : v;
    return (v - vmin) / (vmax - vmin);
}
function hyphenCaseToCamelCase(text) {
    return text.replace(/-([a-z])/g, g => g[1].toUpperCase());
}

class WcGraphSvg extends HTMLElement {
    #points = [];
    #width = 320;
    #height = 240;
    #xmax = 100;
    #xmin = -100;
    #ymax = 100;
    #ymin = -100;
    #func;
    #step = 1;
    #thickness = 1;
    #continuous = false;

    #defaultShape = "circle";
    #defaultSize = 2;
    #defaultColor = "#F00"

    static observedAttributes = ["points", "func", "step", "width", "height", "xmin", "xmax", "ymin", "ymax", "default-shape", "default-size", "default-color", "continuous", "thickness"];
    constructor() {
        super();
        this.bind(this);
    }
    bind(element) {
        element.attachEvents.bind(element);
    }
    render() {
        if (!this.shadowRoot) {
            this.attachShadow({ mode: "open" });
        }
        this.shadowRoot.innerHTML = "";

        let points;
        if (this.#func) {
            points = [];
            for (let x = this.#xmin; x < this.#xmax; x += this.#step) {
                const y = this.#func(x);
                points.push({ x, y, color: this.#defaultColor, size: this.#defaultSize, shape: this.#defaultShape });
            }
        } else {
            points = this.#points;
        }
    }
    attachEvents() {

    }
    connectedCallback() {
        this.render();
        this.attachEvents();
    }
    attributeChangedCallback(name, oldValue, newValue) {
        this[hyphenCaseToCamelCase(name)] = newValue;
    }
    set points(value) {
        if (typeof (value) === "string") {
            value = JSON.parse(value);
        }

        value = value.map(p => ({
            x: p[0],
            y: p[1],
            color: p[2] ?? this.#defaultColor,
            size: p[3] ?? this.#defaultSize,
            shape: p[4] ?? this.#defaultShape
        }));

        this.#points = value;

        this.render();
    }
    get points() {
        return this.#points;
    }
    set width(value) {
        this.#width = parseFloat(value);
    }
    get width() {
        return this.#width;
    }
    set height(value) {
        this.#height = parseFloat(value);
    }
    get height() {
        return this.#height;
    }
    set xmax(value) {
        this.#xmax = parseFloat(value);
    }
    get xmax() {
        return this.#xmax;
    }
    set xmin(value) {
        this.#xmin = parseFloat(value);
    }
    get xmin() {
        return this.#xmin;
    }
    set ymax(value) {
        this.#ymax = parseFloat(value);
    }
    get ymax() {
        return this.#ymax;
    }
    set ymin(value) {
        this.#ymin = parseFloat(value);
    }
    get ymin() {
        return this.#ymin;
    }
    set func(value) {
        this.#func = new Function(["x"], value);
        this.render();
    }
    set step(value) {
        this.#step = parseFloat(value);
    }
    set defaultSize(value) {
        this.#defaultSize = parseFloat(value);
    }
    set defaultShape(value) {
        this.#defaultShape = value;
    }
    set defaultColor(value) {
        this.#defaultColor = value;
    }
    set continuous(value) {
        this.#continuous = value !== undefined;
    }
    set thickness(value) {
        this.#thickness = parseFloat(value);
    }
}

customElements.define("wc-graph-css", WcGraphCss);

SVG와 비슷하지만 드로잉 코드는 삭제했습니다.
특히 CSS 방법은 이렇게 클라이언트를 이용하지 않을 수도 있고 사용자 정의 요소도 필요하지 않을 수도 있다.제가 이렇게 한 것은 봉인하기 쉽고 다른 버전과 비교하기 쉽기 때문입니다. 그러나 서버에 일반적인 태그 형식으로 미리 보여줌으로써 이익을 얻기를 원합니다.

DOM에서 차트 표시


아마도 DOM에서 도표를 나타내는 가장 좋은 방법은 표일 것이다.그것은 이미 접근할 수 있기 때문에, 우리가 그것을 어떻게 설계할지 생각해 낼 수만 있다면, 우리는 순수한 텍스트 브라우저를 사용해도 일을 할 수 있는 좋은 예비 방안을 가지고 있다. 이것은 다른 브라우저들이 쉽게 할 수 없는 것이다.
매핑 확대/축소 후에 값을 인쇄하지만 먼저 테이블에서 사용하기 때문에 원래 Y 값을 전달해야 합니다.
points = points.map(p => ({
    x: windowValue(p.x, this.#xmin, this.#xmax) * this.#width,
    y: windowValue(p.y, this.#ymin, this.#ymax, true) * this.#height,
    value: p.y,
    color: p.color,
    size: p.size,
    shape: p.shape
}));
그런 다음 테이블을 인쇄합니다.
const table = document.createElement("table");
const tbody = document.createElement("tbody");
table.append(tbody);
for (const point of points) {
    const tr = document.createElement("tr");
    const td = document.createElement("td");
    td.textContent = point.value;
    tr.append(td);
    tbody.append(tr);
}
table.append(tbody);
this.shadowRoot.append(table);
이제 우리는 줄마다 칸이 있는 간단한 시계가 하나 생겼다.

테이블을 차트로 표시


우선 책상의 높이와 너비를 사용하자.
table.style.width = this.#width + "px";
table.style.height = this.#height + "px";
그런 다음 CSS 파일을 삽입합니다.
const style = document.createElement("link");
style.rel = "stylesheet";
style.href = "./wc-graph-css.css";
this.shadowRoot.append(style);
여기서 우리는 CSS 수정에 주의를 기울일 수 있다.
다음에 우리가 해야 할 일은 줄에서 열로 바꾸는 것이다.행은 읽기 쉽지만 대부분의 차트는 변수에 의해 Y가 생성된 X/Y 축의 데이터를 나타냅니다.따라서 요소의 테이블 표시를 삭제하고 수평으로 배치해야 합니다.
table {
    display: inline-block;
}
tbody {
    display: grid;
    height: 100%;
}
tr {
    display: block;
    height: 100%;
}
td {
    display: block;
    height: 100%;
    width: 100%;
}
이렇게 하면 격자선이 사용되고 모든 테이블 스타일이 재설정되지만 열은 얼마나 커야 합니까?
우리가 창을 열 때, 우리는 확실히 모든 점을 정확한 수량으로 축소했지만, 우리는 천의 절대점이 아닌 열폭을 만들어야 한다.우리는 마지막 열 사이의 거리인 새로운 속성을 계산해서 이 점을 실현할 수 있다.첫 번째 열에 앞의 열이 없는 내용에 대해 우리는 거리를 0로 정하기만 하면 된다.
그 전에 각 점을 정렬하여 장애가 발생하지 않도록 해야 합니다.
points.sort((a,b) => b.x - a.x).reverse();
그런 다음 각 세그먼트의 너비를 계산합니다.
points = points.map((p, i, arr) => ({
    ...p,
    ...{
        width: p.x - (arr[i - 1]?.x ?? 0)
    }
}));
우리는 단지 두 번째 교체를 하고 있을 뿐이다. 왜냐하면 우리가 이미 축소된 x 좌표가 있으면 더욱 쉽기 때문이다.
이제 tr가 수평으로 정렬되어 수직으로 배치할 수 있습니다td.
tr {
    display: block;
    height: 100%;
    position: relative;
}
td {
    display: block;
    position: absolute;
    width: 100%;
}
td.style.backgroundColor = point.color;
td.style.top = (point.y - point.size) + "px";
td.style.right = point.size * -1 + "px";
td.style.height = point.size + "px";
td.style.width = point.size + "px";
td.textContent = point.value;
점은 열의 오른쪽에 정렬되지만, right 속성은 점이 수평으로 가운데에 있기 때문에, 우리는 점 반경 (크기) 을 추가해야 한다.수직으로 정렬하려면 y 좌표에서 점 반지름을 빼야 합니다.
다음은 우리가 무엇을 했는지 멈추는 좋은 관점이다.

보기에는 괜찮은데, 게다가 우리는 무료 라벨도 있다.우리는 어리석은 안내서를 추가할 수도 있다. 내가 그것을 실현할 때마다 나는 내가 그것을 어디로 가야 할지 모른다는 것을 깨닫게 된다. (정적 인터페이스 부분은 내가 진정으로 사용한 적이 없다.)기왕 우리가 CSS를 꾸준히 사용한다면, 우리는 CSS를 중심으로 그것들을 그릴 것이다. 선형의 점차적인 변화!
table {
    display: block;
    background: linear-gradient(90deg, transparent, transparent calc(50% - 1px), black 50%, black 50%, transparent calc(50% + 1px), transparent), linear-gradient(0deg, transparent, transparent calc(50% - 1px), black 50%, black 50%, transparent calc(50% + 1px), transparent);
       font-size: 0;
}
다른 버전처럼 보일 수 있도록 font size=0을 삽입할 수도 있습니다.나는 또한 내가 이전의 API에서 잘못을 저질렀다는 것을 깨달았다. 크기는 직사각형의 너비와 원의 반경으로 해석된다. 이것이 바로 직사각형이 항상 더 작은 이유이다.아이고.품홍점의 크기는 2.5이어야 한다. 만약에 우리가 그것을 반경으로 해석한다면 5px의 정사각형을 형성할 것이다.우리는 예전처럼 형상도를 추출할 수 있다.
function createShape(shape, [x, y], size, color, value){
    const td = document.createElement("td");
    td.style.backgroundColor = color;
    td.style.top = (y - size) + "px";
    td.style.height = size * 2 + "px";
    td.style.width = size * 2 + "px";
    td.style.right = point.size * -1 + "px";;
    td.textContent = value;

    switch(shape){
        case "circle": {
            td.style.borderRadius = size + "px";
        }
    }

    return td;
}
원의 경우 경계 반경의 절반 너비/높이만 제공합니다(경계 반경: 50%는 부모 너비에 기초하여 원하는 것이 거의 아닙니다).비교해보겠습니다.

그것은 시각적으로 CSS와 완전히 같다.

사용자 정의 속성


이것은 매우 전통적인 것이지만, 우리는 더 많은 코드를 CSS로 옮기기를 희망한다.이것은 CSS를 사용하여 내용을 변경할 수 있을 뿐만 아니라, JS에서 직접 설정할 수 없기 때문에 다음에 필요한 다른 내용, 즉 위조 요소에 접근할 수 있습니다.CSS 사용자 정의 속성을 사용하여 다음을 수행할 수 있습니다.
function createShape(shape, [x, y], size, color, value){
    const td = document.createElement("td");
    td.style.setProperty("--y", y + "px");
    td.style.setProperty("--size", size + "px");
    td.style.setProperty("--color", color);
    td.textContent = value;

    switch(shape){
        case "circle": {
            td.classList.add("circle");
        }
    }

    return td;
}
현재 우리는 CSS를 직접 설정하지 않고 일부 값만 제시합니다. 그러면 CSS가 임무를 완성할 수 있습니다.
td {
    display: block;
    position: relative;
    padding: 0;
    height: 100%;
    width: 100%;
}
td::after {
    content: "";
    display: block;
    position: absolute;
    top: calc(var(--y) - var(--size));
    height: calc(var(--size) * 2);
    width: calc(var(--size) * 2);
    background-color: var(--color);
        right: calc(var(--size) * -1);
}
td.circle::before {
    border-radius: var(--size);
}
현재tdtr에서 같은 공간을 차지하고 ::before위조원소의 정의점이 있다.CSS에 계산을calcs로 가져올 수 있으며, 클래스를 사용하여 모양을 정할 수 있습니다.이것은 CSS를 중심으로 하는 것으로 보기에 완전히 같아야 한다.

연속 경로


지금은 일이 매우 재미있게 변했다.우리는 선으로 점을 연결하기를 원하지만, HTML 요소는 왕왕 정사각형이다. 그러면 우리는 어떻게 해야 합니까?한 가지 방법은 편집 경로chart.css를 사용하여div를 만드는 것이다.우리는 ::before 위조 요소를 만들 수 있다. 왜냐하면 배경에 남아 있기 때문이고::after'점'은 맨 위에 있기 때문이다.우리는 그것을 편집하면 점도 편집되기 때문에 td 사용할 수 없습니다.
올바른 클립 경로를 얻으려면 이전 Y 값과 현재 Y 값이 필요합니다.폭을 얻은 곳에서 우리는 다음과 같은 것을 추가할 수 있다.
points = points.map((p, i, arr) => ({
    ...p,
    ...{
        width: p.x - (arr[i - 1]?.x ?? 0),
        previousY: (arr[i - 1]?.y ?? 0)
    }
}));
또한 사용자 정의 속성createShape을 (매개변수로 추가)해야 합니다.
td.style.setProperty("--prev-y", previousY + "px");
다음은 td 아래의 모든 컨텐트에 대한 CSS입니다.
td {
    display: block;
    position: relative;
    padding: 0;
    height: 100%;
    width: 100%;
}
td::before {
    content: "";
    display: block;
    height: 100%;
    width: 100%;
    background-color: magenta;
    clip-path: polygon(0 var(--prev-y), 100% var(--y), 100% 100%, 0 100%);
}
tr:first-child td::before {
    content: none;
}
td::after {
    content: "";
    display: block;
    position: absolute;
    top: calc(var(--y) - var(--size));
    height: calc(var(--size) * 2);
    width: calc(var(--size) * 2);
    background-color: var(--color);
    right: right: calc(var(--size) * -1);
}
td.circle::after {
    border-radius: var(--size);
}
tr:first-child td::before의 선택기를 주의하세요.우리는 첫 단락을 그리고 싶지 않다.다음과 같은 상황이 발생합니다.

우리는 그것을 어떤 부피도로 바꿀 수 있다.하지만 라인을 얻기 위해서는 밑부분도 만들어야 한다.
td::before {
    content: "";
    display: block;
    height: 100%;
    width: 100%;
    background-color: magenta;
    clip-path: polygon(
        0 calc(var(--prev-y) - var(--size)), 
        100% calc(var(--y) - var(--size)), 
        100% calc(var(--y) - var(--size) + 1px), 
        0 calc(var(--prev-y) - var(--size) + 1px)
    );
}
다른 방법과 달리 우리는 단일 점을 사용하여 선을 착색하거나 기본 색을 사용할 수 있다.여기서 SVG 버전과 일치하도록 기본 색상을 사용하도록 선택했습니다.두께도 늘렸어요.또한, 우리는 테이블에 continuous 클래스를 만들 것입니다. (우리는 이 속성을 사용할 수 있지만, 사용자 정의 요소는 봉인에 더욱 적합합니다.)
table.style.setProperty("--default-color", this.#defaultColor);
table.style.setProperty("--thickness", this.#thickness + "px");
if(this.#continuous){
    table.classList.add("continuous");
}
이제 CSS를 업데이트할 수 있습니다.
table.continuous td::before {
    content: "";
    display: block;
    height: 100%;
    width: 100%;
    background-color: var(--default-color);
    clip-path: polygon(
        0 calc(var(--prev-y) - var(--size) - var(--thickness) / 2), 
        100% calc(var(--y) - var(--size) - var(--thickness) / 2), 
        100% calc(var(--y) - var(--size) + var(--thickness) / 2), 
        0 calc(var(--prev-y) - var(--size) + var(--thickness) / 2)
    );
}
불행히도 두께가 맞지 않았다.

문제는 우리가 Y축에서만 확장하는 것이다.실제로 CSS만 사용하면 진정으로 향상시킬 수 없다...근데적어도 지금까지CSS trigonometric functions는 사용할 수 없기 때문에 오늘 발표할 수 없습니다.
또 다른 결함은 연속적인 함수가 필요하다는 것이다.실제로 우리는 같은 x에 Y 2개를 표시할 수 없습니다. 때때로 CSS는 SVG보다 정확하지 않습니다.만약 네가 일부 인공 제품을 보았다면, 이것이 바로 왜냐는 것이다.그럼에도 불구하고, 대부분의 물건은 작동할 수 있으며, 접근할 수 있고, CSS에서 직접 제어할 수 있다.

좋은 웹페이지 즐겨찾기