Научу вас, как добиться рисованной графики 🔵

JavaScript
Научу вас, как добиться рисованной графики 🔵

Rough.jsЭто графическая библиотека в стиле рисованной графики, которая предоставляет некоторые базовые возможности рисования графики, такие как:

m2.png

m14.png

Хоть автор и грубый человек, но у меня нет сопротивления таким милым вещам.Пользование этой библиотекой само по себе очень простое, и сказать нечего, но она имеет только возможность рисовать, а не взаимодействовать, так что использование сценарии ограничены, поэтому давайте сначала воспользуемся этим. Нарисуйте пример графика:

import rough from 'roughjs/bundled/rough.esm.js'

this.rc = rough.canvas(this.$refs.canvas)
this.rc.rectangle(100, 150, 300, 200, {
    fillweight: 0,
    roughness: 3
})
this.rc.circle(195, 220, 40, {
    fill: 'red'
})
this.rc.circle(325, 220, 40, {
    fill: 'red'
})
this.rc.rectangle(225, 270, 80, 30, {
    fill: 'red',
    fillweight: 5
})
this.rc.line(200, 150, 150, 80, { roughness: 5 })
this.rc.line(300, 150, 350, 80, { roughness: 2 })

Эффект следующий:

image-20210204153435392.png

Это немного глупо?Основное содержание этой статьи состоит в том, чтобы помочь вам вручную реализовать приведенную выше графику и просмотреть окончательный эффект:Г-жа Лю Сяоцин также.com/#/demo/Bean…. Без лишних слов смотрите код.

отрезок

Все основано на линейных сегментах, поэтому давайте сначала посмотрим, как рисовать линейные сегменты.Если вы внимательно посмотрите на рисунок выше, вы обнаружите, что нарисованная от руки версия линейного сегмента на самом деле состоит из двух изогнутых линейных сегментов. Кривая может быть построена с использованием кривой Безье.Здесь используется кубическая кривая Безье.Тогда остается проблема найти координаты начальной точки, конечной точки и двух контрольных точек.

Кривые Безье можно попробовать на этом сайте:cubic-bezier.com/.

Во-первых, к началу и концу отрезка линии мы добавляем небольшое случайное значение.Например, между [-2, 2]случайное значение также может быть связано с длиной этого отрезка и отрезка линии, чем длиннее линия сегмент, случайное значение Чем он больше.

// 直线变曲线
_line (x1, y1, x2, y2) {
    let result = []
    // 起始点
    result[0] = x1 + this.random(-this.offset, this.offset)
    result[1] = y1 + this.random(-this.offset, this.offset)
    // 终点
    result[2] = x2 + this.random(-this.offset, this.offset)
    result[3] = y2 + this.random(-this.offset, this.offset)
}

Далее идут две контрольные точки, которые мы ограничиваем прямоугольником, где находится отрезок:

image-20210204165810055.png

_line (x1, y1, x2, y2) {
    let result = []
    // 起始点
    // ...
    // 终点
    // ...
    // 两个控制点
    let xo = x2 - x1
    let yo = y2 - y1
    let randomFn = (x) => {
        return x > 0 ? this.random(0, x) : this.random(x, 0)
    }
    result[4] = x1 + randomFn(xo)
    result[5] = y1 + randomFn(yo)
    result[6] = x1 + randomFn(xo)
    result[7] = y1 + randomFn(yo)
    return result
}

Затем нарисуйте кривую, созданную выше:

// 绘制手绘线段
line (x1, y1, x2, y2) {
	this.drawDoubleLine(x1, y1, x2, y2)
}

// 绘制两条曲线
drawDoubleLine (x1, y1, x2, y2) {
    // 绘制生成的两条曲线
    let line1 = this._line(x1, y1, x2, y2)
    let line2 = this._line(x1, y1, x2, y2)
    this.drawLine(line1)
    this.drawLine(line2)
}

// 绘制单条曲线
drawLine (line) {
    this.ctx.beginPath()
    this.ctx.moveTo(line[0], line[1])
    // bezierCurveTo方法前两个点为控制点,第三个点为结束点
    this.ctx.bezierCurveTo(line[4], line[5], line[6], line[7], line[2], line[3])
    this.ctx.strokeStyle = '#000'
    this.ctx.stroke()
}

Эффект следующий:

image-20210204171243093.png

Но после нескольких попыток вы обнаружите, что отклонение слишком велико, а степень изгиба слишком велика:

image-20210204180036030.png

Это не похоже на то, что может нарисовать человек с нормальными руками. Перейдите на веб-сайт кривой Безье выше и попробуйте несколько раз. Вы обнаружите, что чем ближе две контрольные точки к сегменту линии, тем менее изогнута кривая:

image-20210313175539327.png

Итак, нам нужно найти точку рядом с сегментом линии в качестве контрольной точки, сначала случайную точку абсцисс, а затем мы можем вычислить точку ординаты, соответствующую абсциссе на сегменте линии, и добавить или вычесть случайное значение к точке ординаты.

_line (x1, y1, x2, y2) {
    let result = []
    // ...
    // 两个控制点
    let c1 = this.getNearRandomPoint(x1, y1, x2, y2)
    let c2 = this.getNearRandomPoint(x1, y1, x2, y2)
    result[4] = c1[0]
    result[5] = c1[1]
    result[6] = c2[0]
    result[7] = c2[1]
    return result
}

// 计算两个点连成的线段上附近的一个随机点
getNearRandomPoint (x1, y1, x2, y2) {
    let xo, yo, rx, ry
    // 垂直x轴的线段特殊处理
    if (x1 === x2) {
        yo = y2 - y1
        rx = x1 + this.random(-2, 2)// 在横坐标附近找一个随机点
        ry = y1 + yo * this.random(0, 1)// 在线段上找一个随机点
        return [rx, ry]
    }
    xo = x2 - x1
    rx = x1 + xo * this.random(0, 1)// 找一个随机的横坐标
    ry = ((rx - x1) * (y2 - y1)) / (x2 - x1) + y1// 通过两点式求出直线方程
    ry += this.random(-2, 2)// 纵坐标加一点随机值
    return [rx, ry]
}

Взгляните на эффект:

2021-03-17-10-16-45.gif

конечно иRough.jsЭто все равно не очень хорошо в сравнении, если интересно, можете посмотреть исходники сами, все равно автору не понять, слишком много управляющих переменных, а комментариев пока нет.

Многоугольник и прямоугольник

Многоугольник состоит в том, чтобы соединить несколько точек встык и пройти по вершинам, чтобы вызвать метод рисования отрезков линии:

// 绘制手绘多边形
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    let len = points.length
    for (let i = 0; i < len - 1; i++) {
        this.line(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1])
    }
    // 首尾相连
    this.line(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1])
}

image-20210207161425915.png

Прямоугольник — это частный случай многоугольника. Четыре угла прямые. Как правило, параметрами являются координата x, координата y вершины левого верхнего угла, ширина прямоугольника и высота прямоугольника. :

// 绘制手绘矩形
rectangle (x, y, width, height, opt = {}) {
    let points = [
        [x, y],
        [x + width, y],
        [x + width, y + height],
        [x, y + height]
    ]
    this.polygon(points, opt)
}

image-20210207161756507.png

круглый

Как обращаться с кругом?Во-первых,все знают,что круг можно аппроксимировать многоугольником.Пока у многоугольника достаточно сторон,он выглядит достаточно круглым.Так как вы не хотите, чтобы он был слишком круглым,то восстановить его в многоугольник Полигоны обсуждались выше. Восстановить в многоугольники очень просто.Например, мы хотим превратить круг в десятиугольник (вы также можете связать его с периметром круга, когда он восстанавливается до нескольких многоугольников), затем радианы, соответствующие каждой стороне является2*Math.PI/10, затем используйтеMath.cosа такжеMath.sinЧтобы вычислить положение вершины и, наконец, вызвать метод рисования многоугольника для рисования:

// 绘制手绘圆
circle (x, y, r) {
    let stepCount = 10
    let step = (2 * Math.PI) / stepCount
    let points = []
    for (let angle = 0; angle < 2 * Math.PI; angle += step) {
        let p = [
            x + r * Math.cos(angle),
            y + r * Math.sin(angle)
        ]
        points.push(p)
    }
    this.polygon(points)
}

Эффект следующий:

image-20210317134337592.png

Видно, что эффект очень общий, даже если количество ребер немного больше, он не выглядит так:

image-20210317134538803.png

Если напрямую соединить нормальными отрезками, то это будет обычный многоугольник, и точно не получится, поэтому суть в том, чтобы превратить отрезки в случайные дуги. случайности к радиусу круга и каждой вершине.Приращение:

circle (x, y, r) {
    let stepCount = 10
    let step = (2 * Math.PI) / stepCount
    let points = []
    let rx = r + this.random(-r * 0.05, r * 0.05)
    let ry = r + this.random(-r * 0.05, r * 0.05)
    for (let angle = 0; angle < 2 * Math.PI; angle += step) {
        let p = [
            x + rx * Math.cos(angle) + this.random(-2, 2),
            y + ry * Math.sin(angle) + this.random(-2, 2)
        ]
        points.push(p)
    }
}

Следующая проблема заключается в том, чтобы рассчитать два контрольных точка кривой Безье. Прежде всего, потому что дуга должна быть выпуклости полигон, в соответствии с природой кривой Безье, два контрольных точка должны быть вне сегмента линии., Я попробовал, если я напрямую использовал две конечные точки самого сегмента линии, чтобы рассчитать, сложнее обрабатывать, разные углы могут потребовать специальной обработки, поэтому мы ссылаемся наRough.jsОдна точка друг от друга:

image-20210318152243835.png

Например, в многоугольнике на рисунке выше мы можем случайным образом найти отрезок прямой.bc, для точкиbПоследняя точкаa, следующий моментc,bдобавить очкиcуменьшатьaПо разнице между абсциссой и ординатой получается контрольная точкаc1, то же самое и с другими точками.Конечная расчетная контрольная точка будет снаружи.Осталась еще одна контрольная точка.Не давайте точкуcЕсли вы бездействуете, добавьте разницу между двумя точками до и после:

image-20210318152754865.png

можно увидеть сутьcконтрольная точкаc2а такжеc1все находятся на одной стороне, поэтому кривая, нарисованная таким образом, очевидно, имеет одно направление:

image-20210318163849897.png

Сделаем симметрично, пусть точкаcПредыдущая точка минус следующая точка:

image-20210318154453700.png

Нарисованная таким образом кривая по-прежнему не работает:

image-20210318163925954.png

Причина очень проста, контрольные точки слишком далеко, поэтому мы добавляем чуть меньше разницы, и окончательный код выглядит следующим образом:

circle (x, y, r) {
    // ...
    let len = points.length
    this.ctx.beginPath()
    // 路径的起点移到第一个点
    this.ctx.moveTo(points[0][0], points[0][1])
    this.ctx.strokeStyle = '#000'
    for (let i = 1; i + 2 < len; i++) {
        let c1, c2, c3
        let point = points[i]
        // 控制点1
        c1 = [
            point[0] + (points[i + 1][0] - points[i - 1][0]) / 5,
            point[1] + (points[i + 1][1] - points[i - 1][1]) / 5
        ]
        // 控制点2
        c2 = [
            points[i + 1][0] + (point[0] - points[i + 2][0]) / 5,
            points[i + 1][1] + (point[1] - points[i + 2][1]) / 5
        ]
        c3 = [points[i + 1][0], points[i + 1][1]]
        this.ctx.bezierCurveTo(
            c1[0],
            c1[1],
            c2[0],
            c2[1],
            c3[0],
            c3[1]
        )
    }
    this.ctx.stroke()
}

Добавляем только пятую часть разницы, я пробовал,5-7между самыми естественными,Rough.jsПлюс одна шестая.

2021-03-18-16-40-06.gif

На этом дело не заканчивается.Во-первых, в этом круге еще есть разрыв.Причина очень проста.i + 2 < lenУсловие петли ,последняя точка не соединена и конец не соединен.Кроме того,первый участок очень неестественный и слишком прямой.Причина в том,что начальная точка нашего пути начинается с первой точки,а наша первая кривая Конечная точка уже является третьей точкой, поэтому сначала переместите начальную точку пути во вторую точку:

this.ctx.moveTo(points[1][0], points[1][1])

Это делает разрыв еще больше:

image-20210318164148681.png

Красные представляют первые две точки, а синие - последнюю точку.Чтобы подключиться ко второй точке, нам нужно добавить первые три точки в списке вершин в конец списка:

// 把前三个点追加到列表最后
points.push([points[0][0], points[0][1]], [points[1][0], points[1][1]], [points[2][0], points[2][1]])
let len = points.length
this.ctx.beginPath()
// ...

Эффект следующий:

image-20210318165518383.png

Проблема снова, никто не сможет передать голову и хвост идеальный круг даже, поэтому добавьте вторую точку, что мы не можем получить его точно так же, как исходная точка, вам нужно добавить смещение:

let end = [] // 处理最后一个连线点,让它和原本的点来点随机偏移
let radRandom = step * this.random(0.1, 0.5)// 让该点超前一点,代表画过头了,也可以来点负数,代表差一点才连上,但是比较丑
end[0] = x + rx * Math.cos(step + radRandom)// 要连的最后一个点实际上是列表里的第二个点,所以角度是step而不是0
end[1] = y + ry * Math.sin(step + radRandom)
points.push(
    [points[0][0], points[0][1]],
    [end[0], end[1]],
    [points[2][0], points[2][1]]
)
let len = points.length
this.ctx.beginPath()
//...

Последняя точка, которую нужно оптимизировать, — это начальная точка или конечная позиция.Вообще говоря, мы рисуем круги от руки сверху, потому что 0 градусов находится в направлении положительной оси X, поэтому мы вычитаемMath.PI/2Вы можете переместить начальную точку вверху слева и справа, и окончательный полный код выглядит следующим образом:

drawCircle (x, y, r) {
    // 圆变多边形
    let stepCount = 10
    let step = (2 * Math.PI) / stepCount// 多边形的一条边对应的角度
    let startOffset = -Math.PI / 2 + this.random(-Math.PI / 4, Math.PI / 4)// 起点偏移角度
    let points = []
    let rx = r + this.random(-r * 0.05, r * 0.05)
    let ry = r + this.random(-r * 0.05, r * 0.05)
    for (let angle = startOffset; angle < (2 * Math.PI + startOffset); angle += step) {
        let p = [
            x + rx * Math.cos(angle) + this.random(-2, 2),
            y + ry * Math.sin(angle) + this.random(-2, 2)
        ]
        points.push(p)
    }
    // 线段变曲线
    let end = [] // 处理最后一个连线点,让它和原本的点来点随机偏移
    let radRandom = step * this.random(0.1, 0.5)
    end[0] = x + rx * Math.cos(startOffset + step + radRandom)
    end[1] = y + ry * Math.sin(startOffset + step + radRandom)
    points.push(
        [points[0][0], points[0][1]],
        [end[0], end[1]],
        [points[2][0], points[2][1]]
    )
    let len = points.length
    this.ctx.beginPath()
    this.ctx.moveTo(points[1][0], points[1][1])
    this.ctx.strokeStyle = '#000'
    for (let i = 1; i + 2 < len; i++) {
        let c1, c2, c3
        let point = points[i]
        let num = 6
        c1 = [
            point[0] + (points[i + 1][0] - points[i - 1][0]) / num,
            point[1] + (points[i + 1][1] - points[i - 1][1]) / num
        ]
        c2 = [
            points[i + 1][0] + (point[0] - points[i + 2][0]) / num,
            points[i + 1][1] + (point[1] - points[i + 2][1]) / num
        ]
        c3 = [points[i + 1][0], points[i + 1][1]]
        this.ctx.bezierCurveTo(c1[0], c1[1], c2[0], c2[1], c3[0], c3[1])
    }
    this.ctx.stroke()
}

В конце концов, вы также можете нарисовать один и тот же отрезок дважды, как показано выше, общий эффект будет следующим:

2021-03-18-20-32-57.gif

Окружность сделана, а эллипс аналогичен.Ведь окружность это частный случай эллипса.Кстати примерная формула периметра эллипса такова:

image-20210318204417614.png

заполнение

стиль 1

Давайте сначала рассмотрим относительно простую начинку:

image-20210319134159471.png

Четыре стороны прямоугольников, которые мы нарисовали выше, разъединены, и путь не закрыт.canvasизfillметодом, поэтому вам нужно соединить эти четыре кривые встык:

// 绘制手绘多边形
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    // 加上填充方法
    let lines = this.closeLines(points)
    this.fillLines(lines, opt)
    
    // 描边
    let len = points.length
    // ...
}

closeLinesМетод используется для замыкания вершин в кривую:

// 把多边形的顶点转换成首尾相连的闭合线段
closeLines (points) {
    let len = points.length
    let lines = []
    let lastPoint = null
    for (let i = 0; i < len - 1; i++) {
        // _line方法上文已经实现了,把直线段转换成曲线
        let arr = this._line(
            points[i][0],
            points[i][1],
            points[i + 1][0],
            points[i + 1][1]
        )
        lines.push([
            lastPoint ? lastPoint[2] : arr[0], // 上一个点存在则使用上一个点的终点来作为该点的起点
            lastPoint ? lastPoint[3] : arr[1],
            arr[2],
            arr[3],
            arr[4],
            arr[5],
            arr[6],
            arr[7]
        ])
        lastPoint = arr
    }
    // 首尾闭合
    let arr = this._line(
        points[len - 1][0],
        points[len - 1][1],
        points[0][0],
        points[0][1]
    )
    lines.push([
        lastPoint ? lastPoint[2] : arr[0],
        lastPoint ? lastPoint[3] : arr[1],
        lines[0][0], // 终点是第一条线段的起点
        lines[0][1],
        arr[4],
        arr[5],
        arr[6],
        arr[7]
    ])
    return lines
}

Сегмент линии есть, просто пройдите сегмент линии и нарисуйте его и, наконец, вызовитеfillМетод может быть:

// 填充多边形
fillLines (lines, opt) {
    this.ctx.beginPath()
    this.ctx.fillStyle = opt.fillStyle
    for (let i = 0; i + 1 < lines.length; i++) {
        let line = lines[i]
        if (i === 0) {
            this.ctx.moveTo(line[0], line[1])
        }
        this.ctx.bezierCurveTo(
            line[4],
            line[5],
            line[6],
            line[7],
            line[2],
            line[3]
        )
    }
    this.ctx.fill()
}

Эффект следующий:

2021-03-19-14-36-12.gif

Круг еще проще, и он сам почти замкнулся, если убрать специальную логику обработки последней точки:

// 下面几行代码都给去掉,使用原本的点即可
let end = []
let radRandom = step * this.random(0.1, 0.5)
end[0] = x + rx * Math.cos(startOffset + step + radRandom)
end[1] = y + ry * Math.sin(startOffset + step + radRandom)

2021-03-19-14-54-42.gif

стиль 2

Второй вид заливки будет немного сложнее.Например,самая простая заливка ниже это на самом деле какие-то наклонные отрезки,но проблема в том как определить концы этих отрезков.Конечно прямоугольники можно вычислить насильно,но как насчет неправильных многоугольников?, поэтому нам нужно найти общий метод.

image-20210205112436404.png

Самый жестокий метод заполнения — определить, находится ли каждая точка внутри многоугольника, но это слишком ресурсоемко.Я проверил идею заполнения полигона, и, вероятно, существует два алгоритма: заполнение строки сканирования и заполнение начальным числом. популярнее. ,Rough.jsЭтот метод также используется, поэтому далее вводится алгоритм.

Заполнение линии развертки очень простое, то есть линия развертки (горизонтальная линия) сканируется снизу вверх от полигона, тогда каждая строка развертки будет иметь пересечение с полигоном, а область между пересечением той же линии развертки и многоугольник - это то, что мы хотим заполнить Да, тогда возникает вопрос, как определить пересечение и как судить, принадлежат ли два пересечения внутренней части многоугольника.

image-20210319182645014.png

Что касается расчета пересечения, то прежде всего у нас есть пересечениеyКоординаты известны, то есть линия развёрткиyкоординаты, то толькоx, знать координаты двух концов отрезка, то можно найти уравнение прямой, а потом вычислить его, но есть более простой способ, то есть использовать соотношение ребра, то есть узнать точка на отрезке, соседние с ней точки могут быть легко получены из этой точки, и следующий процесс вывода:

// 设直线方程
y = kx + b
// 设两点:c(x3, y3),d点的y坐标为c点y坐标+1,d(x4, y3 + 1),那么要求出x4
y3 = kx3 + b// 1
y3 + 1 = kX4 + b// 2
// 1式代入2式
kx3 + b + 1 = kX4 + b
kx3 + 1 = kX4// 约去b
X4 = x3 + 1 / k// 两边同时除k
// 所以y坐标+1,x坐标为上一个点的x坐标加上直线斜率的倒数
// 多边形的线段是已知两个点的,假设为a(x1, y1)、b(x2, y2),那么斜率k如下:
k = (y2 - y1) / 
// 斜率的倒数也就是
1/k = (x2 - x1) / (y2 - y1)

Таким образом, мы начинаем с одной конечной точки отрезка прямой и можем вычислить все точки отрезка прямой одну за другой.

Для подробного ознакомления с алгоритмом и процесса вывода вы можете посмотреть этот PPT:Library.Baidu.com/view/4 О, о, 141…, а затем непосредственно смотреть на процесс реализации алгоритма.

Кратко познакомимся с некоторыми существительными:

1. Боковой дозор ET

Таблица ребер ET, массив, в котором хранится информация обо всех ребрах многоугольника Информация, хранящаяся в каждом ребре: максимальное значение ребра yymaxи минимумymin, значение x нижней точки стороныxi, обратная величина наклона стороныdx. боковой прессyminсортировка по возрастанию,yminТо же самое, нажмитеxiприращение,xiТо же самое можно увидеть толькоymax,еслиymaxЭто все еще то же самое, что указывает на то, что два края совпадают, если нет, нажмитеyamxСортировка по возрастанию.

2. Активный столик AET

Это также массив, который содержит информацию о границах, которые пересекаются с текущей линией развертки.Он будет меняться по мере сканирования строки развертки, удалять непересекающиеся и добавлять новые пересекающиеся. Нажмите на край столаxiСортировка по возрастанию.

Например, следующий многоугольникETПорядок стола такой:

// ET
[p1p5, p1p2, p5p4, p2p3, p4p3]

image-20210319164037805.png

Ниже приведены конкретные шаги алгоритма:

1. Создан из вершинных данных полигонаETповерхностьedgeTable, в вышеуказанном порядке;

2. Создайте пустойAETповерхностьactiveEdgeTable;

3. Начните сканирование, отсканируйте строкиy= самой нижней точки многоугольникаyценность, то естьactiveEdgeTable[0].ymin;

4. Повторяйте следующие шаги, покаETстол иAETВсе столы пусты:

(1) ОтETВозьмите ребро, пересекающее текущую линию развертки со стола, и добавьте его вAETВ таблице, также отсортированной в указанном выше порядке

(2) Вынимайте парамиAETдополнительная информация в таблицеxiзначение, заполнение между каждой парой

(3) ОтAETУдалить ребро, которое было просканировано до конца в таблице, т.е.y >= ymax

(4) ОбновитьAETостальная дополнительная информация в таблицеxi,Прямо сейчасxi = xi + dx

(5) Обновить строку сканированияy,Прямо сейчасy = y + 1

На это не сложно посмотреть.Далее преобразуйте его в код и сначала создайте боковую таблицу.ET:

// 创建排序边表ET
createEdgeTable (points) {
    // 边表ET
    let edgeTable = []
    // 将第一个点复制一份到队尾,用来闭合多边形
    let _points = points.concat([[points[0][0], points[0][1]]])
    let len = _points.length
    for (let i = 0; i < len - 1; i++) {
        let p1 = _points[i]
        let p2 = _points[i + 1]
        // 过滤掉平行于x轴的线段,详见上述PPT链接
        if (p1[1] !== p2[1]) {
            let ymin = Math.min(p1[1], p2[1])
            edgeTable.push({
                ymin,
                ymax: Math.max(p1[1], p2[1]),
                xi: ymin === p1[1] ? p1[0] : p2[0], // 最低顶点的x值
                dx: (p2[0] - p1[0]) / (p2[1] - p1[1]) // 线段的斜率的倒数
            })
        }
    }
    // 对边表进行排序
    edgeTable.sort((e1, e2) => {
        // 按ymin递增排序
        if (e1.ymin < e2.ymin) {
            return -1
        }
        if (e1.ymin > e2.ymin) {
            return 1
        }
        // ymin相同则按xi递增
        if (e1.xi < e2.xi) {
            return -1
        }
        if (e1.xi > e2.xi) {
            return 1
        }
        // xi也相同则只能看ymax
        // ymax还相同,说明两条边重合
        if (e1.ymax === e2.ymax) {
            return 0
        }
        // 如果不重合,则按yamx递增排序
        if (e1.ymax < e2.ymax) {
            return -1
        }
        if (e1.ymax > e2.ymax) {
            return 1
        }
    })
    return edgeTable
}

Следующим шагом будет сканирование:

scanLines (points) {
    if (points.length < 3) {
        return []
    }
    let lines = []
    // 创建排序边表ET
    let edgeTable = this.createEdgeTable(points)
    // 活动边表AET
    let activeEdgeTable = []
    // 开始扫描,从多边形的最低点开始
    let y = edgeTable[0].ymin
    // 循环的终点是两个表都为空
    while (edgeTable.length > 0 || activeEdgeTable.length > 0) {
        // 从ET表里把当前扫描线的边添加到AET表里
        if (edgeTable.length > 0) {
            // 将当前ET表里和扫描线相交的边添加到AET表里
            for (let i = 0; i < edgeTable.length; i++) {
                // 如果扫描线的间隔加大,可能高低差比较小的线段会被整个直接跳过,导致死循环,需要考虑到这种情况
                if (edgeTable[i].ymin <= y && edgeTable[i].ymax >= y || edgeTable[i].ymax < y) {
                    let removed = edgeTable.splice(i, 1)
                    activeEdgeTable.push(...removed)
                    i--
                }
            }
        }
        // 从AET表里删除y=ymax的记录
        activeEdgeTable = activeEdgeTable.filter((item) => {
            return y < item.ymax
        })
        // 按xi从小到大排序
        activeEdgeTable.sort((e1, e2) => {
            if (e1.xi < e2.xi) {
                return -1
            } else if (e1.xi > e2.xi) {
                return 1
            } else {
                return 0
            }
        })
        // 如果存在活动边,则填充活动边之间的区域
        if (activeEdgeTable.length > 1) {
            // 每次取两个边出来进行填充
            for (let i = 0; i + 1 < activeEdgeTable.length; i += 2) {
                lines.push([
                    [Math.round(activeEdgeTable[i].xi), y],
                    [Math.round(activeEdgeTable[i + 1].xi), y]
                ])
            }
        }
        // 更新活动边的xi
        activeEdgeTable.forEach((item) => {
            item.xi += item.dx
        })
        // 更新扫描线y
        y += 1
    }
    return lines
}

Код на самом деле является переводом вышеописанного процесса алгоритма.Понять код алгоритма несложно.Вызовите этот метод в методе многоугольника:

// 绘制手绘多边形
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    // 加上填充方法
    let lines = this.scanLines(points)
    lines.forEach((line) => {
        this.drawDoubleLine(line[0][0], line[0][1], line[1][0], line[1][1], {
            color: opt.fillStyle
        })
    })
    
    // 描边
    let len = points.length
    // ...
}

Взгляните на окончательный эффект заливки:

image-20210319191716772.png

Эффект вышел, но он слишком плотный, потому что наша строка развёртки каждый раз прибавляется на 1, попробуем ещё:

scanLines (points) {
    // ...
    
    // 我们让扫描线每次加10
    let gap = 10
    // 更新活动边的xi
    activeEdgeTable.forEach((item) => {
        item.xi += item.dx * gap// 斜率的倒数为什么也要乘10可以去看上面的推导过程
    })
    // 更新扫描线y
    y += gap
    
    // ...
}

Кстати, ширина отрезка тоже утолщена, и эффект такой:

2021-03-19-19-58-37.gif

Вы также можете поочередно соединить начало и конец отрезка линии в эффект одного мазка:

2021-03-19-21-11-53.gif

Конкретную реализацию можно увидеть в исходном коде.Далее, давайте рассмотрим последний вопрос, который состоит в том, чтобы сделать линию заливки немного наклонной, а сейчас она горизонтальна. Если заштрихованная линия должна быть наклонной, сначала мы можем повернуть график на определенный угол, чтобы отсканированная линия оставалась горизонтальной, а затем повернуть график и заштрихованную линию вместе, чтобы получить наклонную линию.

image-20210319213337900.png

Верхнее изображение показывает, что график повернут против часовой стрелки, а затем отсканировано, а нижнее изображение показывает, что график и линия заполнения повернуты назад по часовой стрелке.

image-20210319213401043.png

Поворот графа — это поворот каждой вершины, поэтому задача состоит в том, чтобы найти положение точки после поворота на заданный угол, выведем его ниже.

image-20210320101658780.png

Нажмите на картинку выше(x,y)Исходный угол равенa, длина отрезка равнаr, найти угол поворотаbкоординаты после(x1,y1):

x = Math.cos(a) * r// 1
y = Math.sin(a) * r// 2

x1 = Math.cos(a + b) * r
y1 = Math.sin(a + b) * r

// 把cos(a+b)、sin(a+b)展开
x1 = (Math.cos(a) * Math.cos(b) - Math.sin(a) * Math.sin(b)) * r// 3
y1 = (Math.sin(a) * Math.cos(b) + Math.cos(a) * Math.sin(b)) * r// 4

// 把1式和2式代入3式和4式
Math.cos(a) = x / r
Math.sin(a) = y / r
x1 = ((x / r) * Math.cos(b) - (y / r) * Math.sin(b)) * r
y1 = ((y / r) * Math.cos(b) + (x / r) * Math.sin(b)) * r
// 约去r
x1 = x * Math.cos(b) - y * Math.sin(b)
y1 = y * Math.cos(b) + x * Math.sin(b)

Отсюда можно получить функцию для нахождения координат точки, повернутой на заданный угол:

getRotatedPos (x, y, rad) {
    return [
        x: x * Math.cos(rad) - y * Math.sin(rad),
        y: y * Math.cos(rad) + x * Math.sin(rad)
    ]
}

С помощью этой функции мы можем вращать многоугольник:

// 绘制手绘多边形
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    // 扫描前先旋转多边形
    let _points = this.rotatePoints(points, opt.rotate)
    let lines = this.scanLines(_points)
    // 扫描完得到的线段我们再旋转相反的角度
    lines = this.rotateLines(lines, -opt.rotate)
    lines.forEach((line) => {
        this.drawDoubleLine(line[0][0], line[0][1], line[1][0], line[1][1], {
            color: opt.fillStyle
        })
    })
    
    // 描边
    let len = points.length
    // ...
}

// 旋转顶点列表
rotatePoints (points, rotate) {
    return points.map((item) => {
        return this.getRotatedPos(item[0], item[1], rotate)
    })
}

// 旋转线段列表
rotateLines (lines, rotate) {
    return lines.map((line) => {
        return [
            this.getRotatedPos(line[0][0], line[0][1], rotate),
            this.getRotatedPos(line[1][0], line[1][1], rotate)
        ]
    })
}

Эффект следующий:

2021-03-20-11-14-00.gif

То же самое верно и для кругов, которые сначала преобразуются в многоугольники и поворачиваются, а затем сканируются и поворачиваются обратно:

image-20210320133836887.png

Суммировать

В этой статье представлены несколько методов реализации рисованного стиля для простой графики, которые включают простые математические знания и алгоритмы заполнения областей. Если существует необоснованный или лучший метод реализации, обсудите его в области сообщений. Полный пример кода находится по адресу:GitHub.com/Ван Линь2/Ха…. Спасибо за прочтение, увидимся в следующий раз~

Справочная статья: