В данной статье я расскажу вам о том, как рисовать треугольный градиент. Наверняка некоторые языки программирования имеют встроенные средства для выполнения этой задачи. Мы же с вами попробуем реализовать такой градиент используя стандартные геометрический формулы. Вот что в итоге необходимо получить:
Что имеем:
Рассмотрим треугольник с вершинами в точках A,B и C. Точки пересечения перпендикуляров, опущенных из вершин ABC, с противоположными сторонами треугольника назовем A", B" и C":
Задача:
Необходимо в каждой точке внутри треугольника(назовем такую точку O), определить проценты близости этой точки к каждой вершине(A,B,C) треугольника. Проценты в сумме должны дать значение 100.
Решение:
Все что нам понадобится, это формулы пересечения прямых и уравнение пермендикуляра, опущенного из точки на прямую на плоскости. Определить точку пересечения прямых можно по формуле:
// прямые, заданные по двум точкам l1 = {p1,p2}; l2 = {p1,p2}; // d = (l2.p2.y-l2.p1.y) * (l1.p2.x-l1.p1._x) - (l2.p2.x-l2.p1.x) * (l1.p2.y-l1.p1.y); a = (l2.p2.x-l2.p1.x) * (l1.p1.y-l2.p1._y) - (l2.p2.y-l2.p1.y) * (l1.p1.x-l2.p1.x); // точка пересечения x0 = l1.p1.x + a * (l1.p2.x-l1.p1.x)/d; y0 = l1.p1.y + a * (l1.p2.y-l1.p1.y)/d;
Получить уравнение перпендикуляра опущенного из точки A на прямую line можно так:
// точка задана по двум координатам A = {x,y}; // линия задана по двум точкам line = {p1,p2}; // // определяем вектор направления линии line dir = {0,0}; dir.x = line.p2.x - line.p1.x; dir.y = line.p2.y - line.p1.y; // // очевидно, что первая точка искомой линии(перпендикуляра) будет точка A // найдем вторую точку point2 = {0,0}; if (dir.y != 0) { temp = dir.x*A.x + dir.y*A.y; point2.x = A.x != 1 ? 1 : 2; point2.y = (-dir.x * point2.x + temp) / dir.y; } else { point2.x = A.x; point2.y = line.p1.y; } // // искомая линия, перпендикуляр опущенный из точки A на прямую line, // будет прямая проходящая через точки A и point2 // Для решения поставленной в статье задачи, // нам будет необходимо найти длину высоты, // опущенной из вершины треугольника на противоположную сторону. // Эту длину можно определить следующим образом. // Находим точку пересечения прямой A-point2 и противоположной точке А прямой в треугольнике, // и находим расстояние между найденной точкой и заданной A.
Опустим перепендикуляр из заданной точки O на пермендикуляр опущенный из точки A на противоположную сторону треугольника. Точку пересечения полученной линии с перпендикуляром, опущенным из точки A, назовем Ah. Аналогично находим точки Bh и Ch:
Процент близости от заданной точки О к вершине A можно определить отношением AAh/AA", где AAh — длина отрезка A->Ah, а AA" — длина высоты опущенной из вершины A на противоположную сторону (т.е. отрезок A->A"):
p1 = AAh/AA"; p2 = BBh/BB"; p3 = CCh/CC";
Определив проценты близости точки, можем определить цвет этой точки по формуле:
red = color1.red*p1 + color2.red*p2 + color3.red*p3; green = color1.green*p1 + color2.green*p2 + color3.green*p3; blue = color1.blue*p1 + color2.blue*p2 + color3.blue*p3;
Где red, green и blue — это каналы в цветовой схеме RGB. Получив значения каналов RGB для заданной точки, мы можем получить значение цвета в шеснадцатеричном формате:
color = (red << 16) + (green << 8) + blue;
Обратное преобразование из шестандцатеричного кода в RGB можно сделать с помощью формул:
color = 0xffff00; // красный канал red = (color >>> 16) & 0xff // зеленый канал green = (color >>> 8) & 0xff // синий канал blue = color & 0xff
Более подробно про работу с каналами RGB можно почитать здесь.
Пример на языке ActionScript 3.0
Посмотреть результат вы можете во флешке.
package { import flash.display.Sprite; import flash.filters.GlowFilter; import ru.flashpress.callback.ICallback; import ru.flashpress.geom.line.FPGLine2d; import ru.flashpress.geom.line.math.FPGLineToPoint2dMath; import ru.flashpress.geom.point.FPGPoint2d; import ru.flashpress.geom.point.FPGPoint2dMath; import ru.flashpress.geom.triangles.FPGTriangle2d; import ru.flashpress.geom.view.core.FillData; import ru.flashpress.geom.view.line.LineView; import ru.flashpress.geom.view.point.PointView; import ru.flashpress.geom.view.triangle.TriangleView; public class testPointG3 extends Sprite implements ICallback { // геометрия треугольника private var triangle:FPGTriangle2d; // отображение треугольника private var triangleView:TriangleView; // геометория проверяемой точки private var targetPoint:FPGPoint2d; // отображение проверяемой точки private var targetPointView:PointView; // // проекции трех вершин на противповоложные стороны private var pHeight1:PointView; private var pHeight2:PointView; private var pHeight3:PointView; // // проекции текущей точки на высоты // опущенные из вершин треугольника private var crossHeight1:PointView; private var crossHeight2:PointView; private var crossHeight3:PointView; // // пукнтирные линии соединяющие текущую точку // с проекциями crossHeight(1/2/3) private var lineCross1:LineView; private var lineCross2:LineView; private var lineCross3:LineView; // public function testPointG3() { var glow:GlowFilter = new GlowFilter(0xffffff, 1, 2, 2, 10, 3); // var size:Number = Math.min(stage.stageWidth, stage.stageHeight)*0.8; var p1:FPGPoint2d = new FPGPoint2d(size/2, 0); var p2:FPGPoint2d = new FPGPoint2d(size, size*0.8); var p3:FPGPoint2d = new FPGPoint2d(0, size*0.8); triangle = new FPGTriangle2d(p1, p2, p3); triangle.translate((stage.stageWidth-size)*0.5, (stage.stageHeight-size*0.8)*0.35); // triangleView = new TriangleView(triangle, new FillData(0x666666, 3)); triangleView.visibleHeights = true; triangleView.heightLines.fill = new FillData(0x9900, 2); // triangleView.point1.filters = [glow]; triangleView.point2.filters = [glow]; triangleView.point3.filters = [glow]; triangleView.point1.fill = color1.fill(2); triangleView.point2.fill = color2.fill(2); triangleView.point3.fill = color3.fill(2); triangleView.point1.name = 'A'; triangleView.point2.name = 'B'; triangleView.point3.name = 'C'; triangleView.point1.scale = 1.5; triangleView.point2.scale = 1.5; triangleView.point3.scale = 1.5; // triangleView.heightLines.line1.fill = color1.fill(2); triangleView.heightLines.line2.fill = color2.fill(2); triangleView.heightLines.line3.fill = color3.fill(2); // // pHeight1 = new PointView(null, color1.fill()); pHeight1.enabled = false; pHeight1.filters = [glow]; pHeight1.name = 'A"'; // pHeight2 = new PointView(null, color2.fill()); pHeight2.enabled = false; pHeight2.filters = [glow]; pHeight2.name = 'B"'; // pHeight3 = new PointView(null, color3.fill()); pHeight3.enabled = false; pHeight3.filters = [glow]; pHeight3.name = 'C"'; // crossHeight1 = new PointView(null, color1.fill()); crossHeight1.enabled = false; crossHeight1.filters = [glow]; crossHeight1.name = 'Ah'; // crossHeight2 = new PointView(null, color2.fill()); crossHeight2.enabled = false; crossHeight2.filters = [glow]; crossHeight2.name = 'Bh'; // crossHeight3 = new PointView(null, color3.fill()); crossHeight3.enabled = false; crossHeight3.filters = [glow]; crossHeight3.name = 'Ch'; // lineCross1 = new LineView(new FPGLine2d(new FPGPoint2d(0, 0), new FPGPoint2d(100, 100)), 0, 0, color1.fill(1, true)); lineCross2 = new LineView(new FPGLine2d(new FPGPoint2d(0, 0), new FPGPoint2d(100, 100)), 0, 0, color2.fill(1, true)); lineCross3 = new LineView(new FPGLine2d(new FPGPoint2d(0, 0), new FPGPoint2d(100, 100)), 0, 0, color3.fill(1, true)); // var xp:Number = p3.x + (triangle.p2.x-p3.x)*0.40; var yp:Number = p1.y + (triangle.p2.y-p1.y)*0.92; targetPoint = new FPGPoint2d(xp, yp); targetPointView = new PointView(targetPoint, new FillData(0x0, 1)); targetPointView.filters = [glow] targetPointView.scale = 1.8; // this.addChild(triangleView); this.addChild(pHeight1); this.addChild(pHeight2); this.addChild(pHeight3); this.addChild(crossHeight1); this.addChild(crossHeight2); this.addChild(crossHeight3); this.addChild(lineCross1); this.addChild(lineCross2); this.addChild(lineCross3); this.addChild(targetPointView); // targetPoint.addCallback(this); triangle.addCallback(this); callbackEvent(null, null); } /** * Были изменены положения вершин треугольника, * или текущей точки targetPoint */ public function callbackEvent(target:Object, data:Object):void { pHeight1.data = triangle.heightA.p2; pHeight2.data = triangle.heightB.p2; pHeight3.data = triangle.heightC.p2; // var color:Number = getColor(targetPoint.x, targetPoint.y); targetPointView.fill = new FillData(color); } private var color1:RGB = new RGB(255, 0, 0); private var color2:RGB = new RGB(0, 200, 0); private var color3:RGB = new RGB(0, 0, 255); private function getColor(_x:Number, _y:Number):uint { var point:FPGPoint2d = new FPGPoint2d(_x, _y); // // находим уравнение высоты опущенной из текущей точки(point) // на высоту опущенную из вершины A(triangle.heightA) var h2mA:FPGLine2d = FPGLineToPoint2dMath.lineHeight(point, triangle.heightA); // аналогично находим высоты для других точек B и C var h2mB:FPGLine2d = FPGLineToPoint2dMath.lineHeight(point, triangle.heightB); var h2mC:FPGLine2d = FPGLineToPoint2dMath.lineHeight(point, triangle.heightC); if (!h2mA || !h2mB|| !h2mC) return 0xffffff; // // h2mA.p2 - это точка пересечения прямой h2mA с // высотой опущенной из вершины A crossHeight1.data = h2mA.p2; crossHeight2.data = h2mB.p2; crossHeight3.data = h2mC.p2; // // // находим расстояние от текущей вершины треугольника // до точки пересечения прямой h2mA c высотой, опущенной из вершины A var h2Alen:Number = FPGPoint2dMath.length(triangle.p1, h2mA.p2); // аналогично находим расстояния для других точек var h2Blen:Number = FPGPoint2dMath.length(triangle.p2, h2mB.p2); var h2Clen:Number = FPGPoint2dMath.length(triangle.p3, h2mC.p2); // // рисуем пунктриную линию lineCross1.data = h2mA; lineCross2.data = h2mB; lineCross3.data = h2mC; // // находим проценты близости var p1:Number = 1-h2Alen/(triangle.heightA.length); var p2:Number = 1-h2Blen/(triangle.heightB.length); var p3:Number = 1-h2Clen/(triangle.heightC.length); // // задаем нижнюю границу if (p1 < 0) p1 = 0; if (p2 < 0) p2 = 0; if (p3 < 0) p3 = 0; // // // вычисляем значения цветовых каналов red, green и blue var r:Number = color1.r*p1 + color2.r*p2 + color3.r*p3; var g:Number = color1.g*p1 + color2.g*p2 + color3.g*p3; var b:Number = color1.b*p1 + color2.b*p2 + color3.b*p3; // задаем верхнюю границу if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; // вычисляем значение цвета var color:int = (r << 16) + (g << 8) + b; return color; } } } import ru.flashpress.geom.view.core.FillData; class RGB { public var r:uint; public var g:uint; public var b:uint; public var code:uint; public function RGB(r:uint, g:uint, b:uint) { this.r = r; this.g = g; this.b = b; this.code = (r << 16) + (g << 8) + b; } public function fill(stroke:uint=1, dotline:Boolean=false):FillData { return new FillData(this.code, stroke, dotline); } }
Для геометрических вычислений используется библиотека FPGeometry2d.swc.
Пример ActionScript3.0 с использованием шейдера
Результат закраски треугольника с использованием шейдера во флешке.
package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.geom.Point; import ru.flashpress.filters.G3Shader; public class testShaderG3 extends Sprite { // шейдер, рисующий треугольный градиент private var shader:G3Shader; public function testShaderG3() { start(); } private var p1:Sprite; private var p2:Sprite; private var p3:Sprite; private function start():void { shader = new G3Shader(); // // создаем три вершины треугольника p1 = createPoint(200, 50); p2 = createPoint(350, 350); p3 = createPoint(50, 350); // shader.initPoints( new Point(p1.x, p1.y), new Point(p2.x, p2.y), new Point(p3.x, p3.y)); // redraw(); } private function createPoint(_x:Number, _y:Number):Sprite { var point:Sprite = new Sprite(); point.graphics.beginFill(0x0, 0.5); point.graphics.drawCircle(0, 0, 10); this.addChild(point); point.x = _x; point.y = _y; point.buttonMode = true; point.addEventListener(MouseEvent.MOUSE_DOWN, downHandler); return point; } private var currentPoint:Sprite; private function downHandler(event:MouseEvent):void { currentPoint = event.target as Sprite; currentPoint.startDrag(); this.stage.addEventListener(MouseEvent.MOUSE_MOVE, moveHandler); this.stage.addEventListener(MouseEvent.MOUSE_UP, upHandler); } private function moveHandler(event:MouseEvent):void { event.updateAfterEvent(); switch (currentPoint) { // задаем шейдеру значения вершин треугольника case p1: shader.point1 = new Point(p1.x, p1.y); break; case p2: shader.point2 = new Point(p2.x, p2.y); break; case p3: shader.point3 = new Point(p3.x, p3.y); break; } redraw(); } private function upHandler(event:MouseEvent):void { currentPoint.stopDrag(); this.stage.removeEventListener(MouseEvent.MOUSE_MOVE, moveHandler); this.stage.removeEventListener(MouseEvent.MOUSE_UP, upHandler); } private function redraw():void { this.graphics.clear(); this.graphics.beginFill(0xff0000, 1); this.graphics.beginShaderFill(shader); this.graphics.moveTo(p1.x, p1.y); this.graphics.lineTo(p2.x, p2.y); this.graphics.lineTo(p3.x, p3.y); } } }
package ru.flashpress.filters { import flash.display.Shader; import flash.geom.Point; /** * Шейдер, рисующий треугольный градиент, по заданным точкам и цветам к ним * @author Serious Sam */ public class G3Shader extends Shader { private static var dir:Point = new Point(); private static var point2:Point = new Point(); private static var temp:Number; private static var k1:Number; private static var k2:Number; private static function getHeight(o:Point, p1:Point, p2:Point):void { dir.x = p2.x-p1.x; dir.y = p2.y-p1.y; point2.x = 0; point2.y = 0; if (dir.y != 0) { temp = dir.x*o.x + dir.y*o.y; point2.x = o.x != 1 ? 1 : 2; point2.y = (-dir.x * point2.x + temp) / dir.y; } else { point2.x = o.x; point2.y = p1.y; } // k1 = (point2.y-o.y)*(p2.x-p1.x) - (point2.x-o.x)*(p2.y-p1.y); k2 = (point2.x-o.x)*(p1.y-o.y) - (point2.y-o.y)*(p1.x-o.x); pointH.x = p1.x + k2*(p2.x-p1.x)/k1; pointH.y = p1.y + k2*(p2.y-p1.y)/k1; } // // // [Embed(source="g3.pbj", mimeType="application/octet-stream")] private var BytecodesClass:Class; // /** * @private */ public function G3Shader() { super(new BytecodesClass()); } private static var pointH:Point = new Point(); /** * В этом методе необходимо сообщить шейдеру значения координат вершин (a, b и c) * а так же координаты проекций этих вершин на противоположные стороны (ah, bh, ch) */ private function reinit():void { // вершина A треугольника this.data.a.value = [_point1.x, _point1.y]; getHeight(_point1, _point2, _point3); // проекция вершины A на сторону BC this.data.ah.value = [pointH.x, pointH.y]; // this.data.b.value = [_point2.x, _point2.y]; getHeight(_point2, _point3, _point1); this.data.bh.value = [pointH.x, pointH.y]; // this.data.c.value = [_point3.x, _point3.y]; getHeight(_point3, _point1, _point2); this.data.ch.value = [pointH.x, pointH.y]; } /** * Инициализировать координаты вершин трегольника */ public function initPoints(p1:Point, p2:Point, p3:Point):void { this._point1.x = p1.x; this._point1.y = p1.y; // this._point2.x = p2.x; this._point2.y = p2.y; // this._point3.x = p3.x; this._point3.y = p3.y; // reinit(); } private var _point1:Point = new Point(100, 0); /** * Координаты первой вершины треугольника */ public function get point1():Point {return this._point1.clone();} public function set point1(value:Point):void { this._point1.x = value.x; this._point1.y = value.y; // this.reinit(); } private var _point2:Point = new Point(200, 200); /** * Координаты второй вершины треугольника */ public function get point2():Point {return this._point2.clone();} public function set point2(value:Point):void { this._point2.x = value.x; this._point2.y = value.y; // this.reinit(); } private var _point3:Point = new Point(0, 200); /** * Координаты третьей вершины треугольника */ public function get point3():Point {return this._point3.clone();} public function set point3(value:Point):void { this._point3.x = value.x; this._point3.y = value.y; // this.reinit(); } /** * Инициализировать цвета вершин треугольника */ public function initColors(color1:uint, color2:uint, color3:uint):void { this.color1 = color1; this.color2 = color2; this.color3 = color3; } private var _color1:Number = 0; /** * Цвет первой вершины треугольника */ public function get color1():uint {return this._color1;} public function set color1(value:uint):void { this._color1 = value; this.data.color1.value = [ ((value >>> 16) & 0xff)/255, ((value >>> 8) & 0xff)/255, (value & 0xff)/255]; } private var _color2:Number = 0; /** * Цвет второй вершины треугольника */ public function get color2():uint {return this._color2;} public function set color2(value:uint):void { this._color2 = value; this.data.color2.value = [ ((value >>> 16) & 0xff)/255, ((value >>> 8) & 0xff)/255, (value & 0xff)/255]; } private var _color3:Number = 0; /** * Цвет третьей вершины треугольника */ public function get color3():uint {return this._color3;} public function set color3(value:uint):void { this._color3 = value; this.data.color3.value = [ ((value >>> 16) & 0xff)/255, ((value >>> 8) & 0xff)/255, (value & 0xff)/255]; } } }
<languageVersion: 1.0;> kernel TriangleGradient < namespace : "flashpress.ru"; vendor : "FlashPress.ru"; version : 1; description : "Triangle Gradient"; > { parameter float2 a < defaultValue:float2(50.0, 50.0); >; parameter float2 ah < defaultValue:float2(50.0, 200.0); >; parameter float2 b < defaultValue:float2(50.0, 200.0); >; parameter float2 bh < defaultValue:float2(125.0, 125.0); >; parameter float2 c < defaultValue:float2(200.0, 200.0); >; parameter float2 ch < defaultValue:float2(50.0, 200.0); >; parameter float3 color1 < minValue:float3(0.0, 0.0, 0.0); maxValue:float3(1.0, 1.0, 1.0); defaultValue:float3(1.0, 0.0, 0.0); >; parameter float3 color2 < minValue:float3(0.0, 0.0, 0.0); maxValue:float3(1.0, 1.0, 1.0); defaultValue:float3(0.0, 1.0, 0.0); >; parameter float3 color3 < minValue:float3(0.0, 0.0, 0.0); maxValue:float3(1.0, 1.0, 1.0); defaultValue:float3(0.0, 0.0, 1.0); >; input image4 src; output float4 dst; void evaluatePixel() { // // // // float2 direction; float lineA; float lineB; float lineC; float checkd; float checka; float2 point; float2 o = outCoord(); // // уравнение перпендикуляра опущенного из точки O на прямую a-ah direction = ah-a; lineA = -direction.y; lineB = -direction.x; lineC = direction.x*o.x + direction.y*o.y; if (lineA != 0.0) { point = float2(o.x + 1.0, 0); point.y = -(lineB*point.x+lineC)/lineA; } else { point = float2(o.x, a.y); } // пересечение p1-point и b-c checkd = (point.y-o.y)*(ah.x-a.x) - (point.x-o.x)*(ah.y-a.y); checka = (point.x-o.x)*(a.y-o.y) - (point.y-o.y)*(a.x-o.x); // точка пересечения перпендикуляра опущенного из A и прямой BC float2 aho = float2(0.0, 0.0); aho.x = a.x + checka*(ah.x-a.x)/checkd; aho.y = a.y + checka*(ah.y-a.y)/checkd; // // // // уравнение перпендикуляра опущенного из точки O на прямую b-bh direction = bh-b; lineA = -direction.y; lineB = -direction.x; lineC = direction.x*o.x + direction.y*o.y; if (lineA != 0.0) { point = float2(o.x+1.0, 0.0); point.y = -(lineB*point.x+lineC)/lineA; } else { point = float2(o.x, b.y); } // пересечение p1-point и b-c checkd = (point.y-o.y)*(bh.x-b.x) - (point.x-o.x)*(bh.y-b.y); checka = (point.x-o.x)*(b.y-o.y) - (point.y-o.y)*(b.x-o.x); // точка пересечения перпендикуляра опущенного из A и прямой BC float2 bho = float2(0.0, 0.0); bho.x = b.x + checka*(bh.x-b.x)/checkd; bho.y = b.y + checka*(bh.y-b.y)/checkd; // // // // уравнение перпендикуляра опущенного из точки O на прямую c-ch direction = ch-c; lineA = -direction.y; lineB = -direction.x; lineC = direction.x*o.x + direction.y*o.y; if (lineA != 0.0) { point = float2(o.x+1.0, 0.0); point.y = -(lineB*point.x+lineC)/lineA; } else { point = float2(o.x, c.y); } // пересечение p1-point и b-c checkd = (point.y-o.y)*(ch.x-c.x) - (point.x-o.x)*(ch.y-c.y); checka = (point.x-o.x)*(c.y-o.y) - (point.y-o.y)*(c.x-o.x); // точка пересечения перпендикуляра опущенного из A и прямой BC float2 cho = float2(0.0, 0.0); cho.x = c.x + checka*(ch.x-c.x)/checkd; cho.y = c.y + checka*(ch.y-c.y)/checkd; // // // float pp1 = 1.0-((a.x-aho.x)*(a.x-aho.x)+(a.y-aho.y)*(a.y-aho.y))/((a.x-ah.x)*(a.x-ah.x)+(a.y-ah.y)*(a.y-ah.y)); float pp2 = 1.0-((b.x-bho.x)*(b.x-bho.x)+(b.y-bho.y)*(b.y-bho.y))/((b.x-bh.x)*(b.x-bh.x)+(b.y-bh.y)*(b.y-bh.y)); float pp3 = 1.0-((c.x-cho.x)*(c.x-cho.x)+(c.y-cho.y)*(c.y-cho.y))/((c.x-ch.x)*(c.x-ch.x)+(c.y-ch.y)*(c.y-ch.y)); // dst = float4(0.0, 0.0, 0.0, 1.0); dst.r = color1.r*pp1 + color2.r*pp2 + color3.r*pp3; dst.g = color1.g*pp1 + color2.g*pp2 + color3.g*pp3; dst.b = color1.b*pp1 + color2.b*pp2 + color3.b*pp3; } }
ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/196104/
Добавить комментарий