Алгоритмы 2D графики
- Основы математики
- Угол между двумя прямыми
- Пересечение отрезков
- Расстояние от точки до отрезка
- Выпуклость и самопересечение полигона
- Точка в полигоне
- Библиотека g2d.js
Основы математики
Положение точки $\mathbf{p}=\{p_x,\,p_y\}$ на плоскости
описываем структурой {x: число, y: число }.
Вектор $\mathbf{a}=\mathbf{p}_2-\mathbf{p}_1$ направлен из точки $\mathbf{p}_1$ в точку $\mathbf{p}_2$ и соединяет их.
Длина вектора равна:
$$
|\mathbf{a}| = \sqrt{a^2_x+a^2_y}.
$$
Два вектора перпендикулярны, если их скалярное произведение равно нулю:
$$
\mathbf{a}\mathbf{b} = a_x b_x+a_x b_x = 0.
$$
В частности, к вектору $\mathbf{a}=\{a_x,a_y\}$ будет перпендикулярен вектор с компонентами $\{a_y, -a_x\}$.
Косинус угла между двумя векторами равен:
$$
\cos\alpha = \frac{\mathbf{a}\mathbf{b}}{|\mathbf{a}|\, |\mathbf{b}|}.
$$
Отрезок характерезуется двумя точками -- положением начала $\mathbf{p}_1$
и конца $\mathbf{p}_2$. Произвольную точку $\mathbf{p}$ отрезка удобно задавать в параметрическом
виде:
$$
\mathbf{p} = \mathbf{p}_1 + (\mathbf{p}_2-\mathbf{p}_1)\,t \equiv \mathbf{p}_1\,(1-t) + \mathbf{p}_2 \,t,
$$
где параметр $t$ пробегает значения от 0 до 1. Если $t=0$, то $\mathbf{p}=\mathbf{p}_1$, а при $t=1$ - имеем $\mathbf{p}=\mathbf{p}_2$.
Описанные ниже алгоритмы реализованы в библиотеке класса G2D (модуль g2d.js), функции которого являются статическими. Примеры использования этих алгоритмов можно изменять, хватаясь мышкой за точки. Реализации примеров см. в исходниках этого документа.
Угол между двумя прямыми
Пусть необходимо найти угол между двумя отрезками, начинающимися из одной точки. Один из способов получения угла между вектором p={x,y} и осью x -- это функция ang=Math.atan2(y,x). Возвращаемый ею угол будет в интервале $[-\pi,\pi]$. При этом ang > 0, если компонента y > 0 (так как ось y на канвасе направлена вниз, это соответствует направлению вектора вниз относительно горизонтали). Справа соответствующие углы (в градусах !) приведены рядом с концами отрезков.
Для вычисления угла между двумя векторами
удобнее воспользоваться стандартной формулой $\cos\alpha = \mathbf{a}\mathbf{b}/|\mathbf{a}|\,|\mathbf{b}|$.
Этот угол (в радианах) реализован функциями
G2D.angle(a, b)
и
G2D.angleSign(a, b). Во втором случае угол имеет знак
+, если b находится на канвасе по часовой стрелке от a и -, если против.
По модулю, естественно, всегда получается положительны минимальный угол, даваемый функцией G2D.angle(a, b).
На канвасе справа угол переведен из радиан в градусы.
Функция G2D.getMedianVec(p1, p0, p2) даёт единичный вектор вдоль медианы треугольника p1, p0, p2 для угла из точки p0 (на канвасе он используется для получения положения текста отображения угла между отрезками).
Пересечение отрезков
Пусть один отрезок задан точками b1 и e1, а второй - точками b2 и e2. Отрезки пересекаются, если существует решение уравнения $$\mathbf{b}_1+(\mathbf{e}_1-\mathbf{b}_1)\,t_1 = \mathbf{b}_2+(\mathbf{e}_2-\mathbf{b}_2)\,t_2,$$ расписанного в компонентах (система двух уравнений). При этом параметры $t_1$ и $t_2$ должны находиться в интервале $[0,1]$. Функция G2D.intersectSegments(b1, e1, b2, e2, point) возвращает true, если отрезки пересекаются и в структуре point будет находится точка пересечения.
Отдельно стоит обрабатывать систуацию, когда отрезки не пересекаются, а имеют общие краевые точки. Учитывая возможные проблемы с округлением, для сравнения равенства точек стоит использовать функцию G2D.equiv1(a, b), которая сравнивает координаты точек с точностью до одной десятичной цифры после запятой, в отличие от функции G2D.equiv(a, b), которая требует полного совпадения.
Расстояние от точки до отрезка
Расстояние от точки $\mathbf{p}_0$ до отрезка $\mathbf{p}_1$ - $\mathbf{p}_2$ находится из следующих соображений. Пусть $\mathbf{p}$ - точка пересечения отрезка нормалью, опущенной из $\mathbf{p}_0$ на отрезок (слева - зелёная точка, а нормаль - пунктир). Тогда $\mathbf{p} = \mathbf{p}_1+(\mathbf{p}_2-\mathbf{p}_1)\,t$, где $0 < t < 1$. Так как $\mathbf{p}-\mathbf{p}_0$ (вектор вдоль нормали) перпендикулярен $\mathbf{p}_2-\mathbf{p}_1$ (вектор вдоль прямой), то $(\mathbf{p}-\mathbf{p}_0)(\mathbf{p}_2-\mathbf{p}_1)=0$, откуда находим: $$ t = \frac{(\mathbf{p}_0-\mathbf{p}_1)(\mathbf{p}_2-\mathbf{p}_1)}{(\mathbf{p}_2-\mathbf{p}_1)^2}. $$ Если $t \le 0$ или $t \ge 1$, точкой "нормали" будет один из концов отрезка. Эта формула реализована в функции G2D.fromPointToSegment(p0, p1, p2).
Выпуклость и самопересечение полигона
Полигон будет выпуклым, если все его углы одного знака. Проведем через ребро $(\mathbf{p}_i,\mathbf{p}_{i-1})$ прямую. Следующая точка $\mathbf{p}_{i+1}$ будет лежать слева или справа от этой прямой относительно направляющего вектора $\mathbf{k}=\mathbf{p}_i-\mathbf{p}_{i-1}$. Нормаль к вектору $\mathbf{k}=\{k_x,k_y\}$ равна $\mathbf{n}=\{k_y,\,-k_x\}$. С одной стороны от прямой означает, что скалярное произведение $\mathbf{n}\,(\mathbf{p}_{i+1}-\mathbf{p}_i)$ для всех вершин полигона одного знака.
Самопересечение полигона происходит, когда у него пересекаются хотя бы два отрезка. Отрезки $(\mathbf{p}_{i+1},\mathbf{p}_{i})$ и $(\mathbf{p}_{j+1},\mathbf{p}_{j})$ пересекаются, если концы одного находятся по разные стороны от прямой, проходящей через второй отрезок. Индексы $i,j$ выбираются так, чтобы протестировать все пары не смежных рёбер полигона.
Проверка выпуклости полигона проводится функцией G2D.fromPointToSegment(pnts), где pnts -- массив точек, задающих полигон (последняя точка замыкается ребром с первой). На канвасе полигон станет желтым, если окажется не выпуклым.
Проверка самопересечения полигона проводится функцией G2D.polygonGood(pnts), которая возвращает true, если полигон не имеет самопересечений. На канвасе полигон станет красным, если окажется самопересекающимся.
Точка в полигоне
Проведём из точки $\mathbf{p}$ векторы к каждой вершине полигона $\mathbf{p}_i$. Проведём из точки $\mathbf{p}$ векторы к каждой вершине полигона $\mathbf{p}_i$. Если точка находится внутри полигона, то сумма углов между этими векторами (с учётом их знаков) будет равна $2\pi$, иначе - нулю.
Функция G2D.pointInPolygon(p,pnts) возвращает true, если точка p находится внутри полигона, задаваемого массивом pnts. В противном случае возвращается false.
Функция G2D.pointToPolygon(p,pnts) возвращает ближайшую на полигоне точку к точке p. Эта функция, вместе с предыдущей, позволяет выяснить, помещается ли окружность внутрь полигона. Справа, если это происходит, окружность меняет свой цвет на синий.
Функция G2D.polygonRect(pnts) возвращает прямоугольник {x,y,w,h}, ограничивающий полигон. Для ускорения проверки попадает ли точка в полигон, стоит сначала проверять, попадает ли она в охватывающий прямоугольник.
Библиотека g2d.js
Графические функции
- setCanvas(canvas_id) - получить контекст рисования ctx по canvas id и его ширину w, высоту h
- plotLine(p1, p2) - нарисовать линию от точки p1:{x,y} к точке p2:{x,y}
- plotCircle(p, r) - нарисовать окружность с радиусом r и центром в точке p:{x,y}
- plotPoint(p, r) - нарисовать "толстую" точку радиуса r с центром в точке p:{x,y} (fill circle)
- plotArc(p, r, a1, a2) - нарисовать дугу из центра p радиуса r от угла a1 до угла a2 против часовой стрелке
- plotText(txt, p) - нарисовать текст txt в точке p
- plotCircles(crcls, stroke, fill) - нарисовать окружности из массива crcls, сначала контуром текущего цвета strokeStyle, а затем залить цветом fillStyle (получится кластер с обведенным контуром)
- plotPolygon(pnts) - нарисовать замкнутый полигон по точкам в массиве pnts
Векторные функции
- setXY(a, b) - a=b (присвоить координаты точки b в точку a)
- equiv(a, b) - a==b совпадают точки (true) или нет (false)
- equiv1(a, b) - a==b совпадают точки (true) или нет (false) с точностью до одной десятичной цифры после запятой
- add(a, b) - a+b (сложить 2 вектора и вернуть результат)
- sub(a, b) - a-b (вычесть 2 вектора и вернуть результат)
- mult(c, v) - c*v (умножить вектор v на число c и вернуть результат)
- scalar(a, b) - a*b (скалярное произведение векторов)
- dist(p1, p2) - расстояние между двумя точками p1:{x,y} - p2:{x,y}
- dist2(p1, p2) - квадрат расстояния между двумя точками p1:{x,y} - p2:{x,y}
- len(v) - длина вектора v:{x,y}
- norm(v) - квадрат длины вектора v:{x,y}
- unit(v) - возвращает единичный вектор в направлении v, не меняя v
- normalize(v) - нормирует вектор на единицу и возвращет его
- angle(a, b) - угол между векторами
- angleSign(a, b) - угол между векторами со знаком +, если b находится против часовой стрелке от a
- printXY(v) - вернуть в виде строки координаты вектора
- print1XY(v) - вернуть в виде строки координаты вектора, окуруглив до 1 цифры после запятой
- print1XYR(c) - вернуть в виде строки параметры окуржности, окуруглив до 1 цифры после запятой
- setXYR(c1, c2) - c1=c2 (присвоить координаты и радиус окружности c2 в окружность c1)
Базовые алгоритмы
- nearestPoint(p, points) - индекс в массиве points = [{x,y}, ...], соответствующий ближайшей точки к точке p:{x,y}
- farthestPoint(p, points) - индекс в массиве points = [{x,y}, ...], соответствующий самой удалёной точки к точке p:{x,y}
- centerPoints(points) - возвращает центр масс массива точек points = [{x,y}, ...]
- medianVec(p1, p0, p2) - возвращает единичный вектор вдоль медианы треугольника p1, p0, p2 для угла из точки p0
- intersectSegments(b1, e1, b2, e2, point) - возвращает true, если сегмент между точками b1 to e1 и сегмент между точками b2 to e2 пересекаются; если это происходит, point равна точке пересечения.
- sideFromLine(p1, p2, p) - возвращает знак +1, если точка p лежит справа от прямой, проходящей через p1, p2 и -1, слева и 0, если лежит на прямой с точностью до 1/100
- fromPointToSegment(p0, p1, p2) - возвращает положение нормали к отрезку p1:{x,y}-p2:{x,y} из точки p0 (или краевые точки, если нормаль лежит вне отрезка)
- circleInCircle(c1, c2) - Возвращает true, если окружность c1 = {x, y, r} находится внутри другой окружности c2
- circleInCircles(c, crcls) - возвращает true, если окружность c = {x, y, r} находится внутри одной из окружностей из массива crcls
- squareIntersectTwoCircles(c1, c2) - площадь пересечения двух кругов с1 = {x, y, r} и с2 = {x, y, r}
- normAng(a) - нормализация угла к интервалу [-PI..PI]
- angCmp(a1, a2) - Сравнивает углы a1,a2 при отсчете от оси x; углы нормированы на [-PI..PI] -1, если a1
a2 и 0 если равны \todo
Работа с кластерами
- centerCircles(crcls, p) - возвращает центр масс массива окружностей circles = [{x,y,r}, ...]
- radiusCircles(crcls) - радиус окружности, с центром в точке p, охватывающей все окружности из массива circles
- squareCircles(crcls) - площадь поверхности, покрываемой кругами из массива crcls (с учётом их пересечений)
- scaleCircles(crcls, scale) - измененить масштаб в scale раз набора окружностей crcls в их системе координат
- translateCircles(crcls, p) - сдвинуть все координаты окружностей на вектор p
- rotateCircles(crcls, a, p) - повернуть все координаты окружностей на угол a относительно точки p
- setCircles(crcls1, crcls2) - crcls1 = crcls2: присвоить координаты и радиусы окружностей crcls2 в crcls1
- clasterInСlaster(crcls1, с1, crcls2, с2) - возвращает true, если сферы массива crcls1, которые окруражет сфера c1={x,y,r} пересекаются с сферами массива crcls2, которые окружает сфера c2={x,y,r}
- calcCrossCircles(crcls) - вычислить дуги невидимых частей огружностей при их пересечении в массиве окружностей. В каждую окружность добавляется массив angs: [ [a1,a2],...] из интервалов (массивы двух чисел) углов невидимых из-за пересечения дуг окружностей
- compressCrossCircles(crcls) - убираем пересекающиеся сегменты невидимых частей окружностей (см. calcCrossCircles)\todo
Работа с полигонами
- polygonConvex(pnts) - возвращает true, если полигон, заданный точками pnts - выпуклый
- polygonGood(pnts) - возвращает true, если полигон, заданный точками pnts является не самопересекающимся
- polygonRect(pnts) - возвращает прямоугольник {x, y, w, h}, окужающий полигон
- polygonSquare(pnts) - площадь полигона
- polygonPerimeter(pnts) - периметр полигона
- pointToPolygon(p, pnts) - возвращает положение ближайшей на полигоне pnts точки к точке p
- pointInPolygon(p, pnts) - возвращает true, если точка p находится внутри полигона pnts
- circleInPolygon(c, pnts) - возвращает true, если сфера c = {x,y,r} находится внутри полигона pnts
- clasterInPolygon(crcls, c, pnts) - возвращает true, если сферы массива crcls, которые окруражет сфера c={x,y,r} находится внутри полигона pnts
Математические функции ms.js
- MS.rand(n) - целое случайное число от 0 до n-1
- MS.sqr(n) - n*n (квадрат числа)