Draw a Filled Polygon using Scanline Loop

送分小仙女□ 提交于 2021-01-29 05:01:21

问题


I'm trying to draw a filled polygon using individual pixels in a scanline loop (so no lineTo or fill Canvas methods).

I was able to achieve a triangle in this method (example below), but I'm not sure where to begin with a more complex polygon (such as a star shape ★). Can anyone give any advice on how to approach this or existing shape algorithm examples for Javascript & Canvas?

I have researched Bresenham’s Algorithm but was unsuccessful implementing it for polygons because of my limited understanding of it. Please let me know if anything I've explained is unclear.

Thank you!

var canvas = document.querySelector('#canvas')
var ctx = canvas.getContext('2d');
var widthRange = document.querySelector('#widthRange')
var heightRange = document.querySelector('#heightRange')
ctx.fillStyle = 'blue';

var DrawPixel = function (x, y) {
  ctx.fillRect(x, y, 1, 1);
}

var x = 100;
var y = 100;
var width = widthRange.value;
var height = heightRange.value;

const draw = () =>
{
  ctx.clearRect(0,0,canvas.width,canvas.height); 
  wHRatio = width/height;
  for (var j=0; j<height; j++)
  {
    w = width-j*wHRatio;
    for (var i=0; i<w; i++)
    {
      DrawPixel(Math.floor(i+(j*(wHRatio/2))),height-j);
    }
  }
}

draw();

widthRange.addEventListener("input", function(e){
  width = e.currentTarget.value;
  draw();
})

heightRange.addEventListener("input", function(e){
  height = e.currentTarget.value;
  draw();
})
#canvas {
  outline: 1px solid grey;
}

.slidecontainer
{
  display: inline-block;
}
<div class="slidecontainer">
  <label for="widthRange">Width</label>
  <input type="range" min="1" max="300" value="100" class="slider" id="widthRange">
</div>
<div class="slidecontainer">
  <label for="heightRange">Height</label>
  <input type="range" min="1" max="150" value="100" class="slider" id="heightRange">
</div>
<canvas width=300 height=150 id="canvas"></canvas>

回答1:


Scanline for non self intersecting polygons.

Draw any concave or convex polygon with 3 or more sides using scanline method.

  • Is the easiest, also slowest.
  • Polygon is a set of lines with start and end points.
  • Polygon lines can be unordered
  • Polygon must be closed
  • None of polygon lines can cross any of the other polygon lines.

Steps

Find bounding box of lines. top, left, right, bottom.
Set x, y to  top left of bounding box.
while y is less than bottom.
    Find all lines that will cross the line from left, y to right, y
    Sort lines in distance from x to point where above line crossed
    while there are sorted lines
        shift two lines from sorted lines and scan the pixels between
    add 1 to y      
    

An implementation.

Function scanlinePoly(lines, col) draw the pixels. lines is create with createLines which is an array of lines with helper functions to add lines, find lines, sort lines, and get bounds.

Helper functions

  • createStar(x, y, r1, r2, points) will create a lines array for a star, x, y center of star, r1 radius, r2 second radius, points number of points
  • P2(x, y) returns 2D point
  • L2(p1, p2) returns 2D line that includes the slope of the line. p1, p2 are points as created by P2
  • atLineLevelY(y) returns true if line crosses scan line at y

const scanlinePoly = (lines, col) => {
    const b = lines.getBounds();
    var x, y, xx;
    ctx.fillStyle = col;
    b.left = Math.floor(b.left);
    b.top = Math.floor(b.top);
    for (y = b.top; y <= b.bottom; y ++) {
        // update 
        // old line was const ly = lines.getLinesAtY(y).sortLeftToRightAtY(y);
        // changed to
        const ly = lines.getLinesAtY(y + 0.5).sortLeftToRightAtY(y + 0.5);
        x = b.left - 1; 
        while(x <= b.right) {
            const nx1 = ly.nextLineFromX(x);
            if (nx1 !== undefined) {
                const nx2 = ly.nextLineFromX(nx1);
                if (nx2 !== undefined) {
                    const xS = Math.floor(nx1); 
                    const xE = Math.floor(nx2); 
                    for (xx = xS; xx < xE; xx++) {
                        ctx.fillRect(xx, y, 1, 1);
                    }
                    x = nx2;
                } else { break }
            } else { break }
        }
    }
}

function createLines(linesArray = []) {
     return   Object.assign(linesArray, {
        addLine(l) { this.push(l) },
        getLinesAtY(y) { return createLines(this.filter(l => atLineLevelY(y, l))) },
        sortLeftToRightAtY(y) {
            for (const l of this) { l.dist = l.p1.x + l.slope * (y - l.p1.y) }
            this.sort((a,b) => a.dist - b.dist);
            return this;
        },
        nextLineFromX(x) { // only when sorted
            const line = this.find(l => l.dist > x);
            return line ? line.dist : undefined;
        },
        getBounds() {
            var top = Infinity, left = Infinity;
            var right = -Infinity,  bottom = -Infinity;
            for (const l of this) {
                top = Math.min(top, l.p1.y, l.p2.y);
                left = Math.min(left, l.p1.x, l.p2.x);
                right = Math.max(right, l.p1.x, l.p2.x);
                bottom = Math.max(bottom, l.p1.y, l.p2.y);
            }
            return {top, left, right, bottom};
        },
    });
}

const createStar = (x, y, r1, r2, points) => {
    var i = 0, pFirst, p1, p2;
    const lines = createLines()
    while (i < points * 2) {
        const r = i % 2 ? r1 : r2;
        const ang = (i / (points * 2)) * Math.PI * 2;
        p2 = P2(Math.cos(ang) * r + x, Math.sin(ang) * r + y);
        if (pFirst === undefined) { pFirst = p2 };
        if (p1 !== undefined) { lines.addLine(L2(p1, p2)) }
        p1 = p2;
        i++;
    }
    lines.addLine(L2(p2, pFirst));    
    return lines;
}
const ctx = canvas.getContext("2d");

const P2 = (x = 0,y = 0) => ({x, y});
const L2 = (p1 = P2(), p2 = P2()) => ({p1, p2, slope: (p2.x - p1.x) / (p2.y - p1.y)});
const atLineLevelY = (y, l) => l.p1.y < l.p2.y && (y >= l.p1.y && y <= l.p2.y) || (y >= l.p2.y && y <= l.p1.y);

canvas.addEventListener("click", () => {
    ctx.clearRect(0,0,200,200);
    const star = createStar(
        100, 90, 
        Math.random() * 80 + 10,
        Math.random() * 80 + 10,
        Math.random() * 20 + 2 | 0
    );
    scanlinePoly(star, "#F00")


})


const star = createStar(100, 90, 90, 40, 10);
scanlinePoly(star, "#F00")
canvas {border: 1px solid black;}
<canvas id="canvas" width="200" height="180"></canvas>Click for rand star

Note that inner loop

for (xx = xS; xx < xE; xx++) {
    ctx.fillRect(xx, y, 1, 1);
}

Can be replaced with ctx.fillRect(xS, y, xE - xS, 1) to greatly improve performance.

UPDATE

Looking at my answer again to see if it could be improved I noticed a problem that resulted in lines incorrectly rendered.

To fix the the first line inside the outer loop of function scanlinePoly needs to be changed from.

const ly = lines.getLinesAtY(y).sortLeftToRightAtY(y);

To

const ly = lines.getLinesAtY(y + 0.5).sortLeftToRightAtY(y + 0.5);


来源:https://stackoverflow.com/questions/65573101/draw-a-filled-polygon-using-scanline-loop

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!