Dashed / Dotted lines, Line Thickness


#1

Hi,

Is it possible somehow (with a plugin, extension, library) to draw dashed and dotted lines and curves?
I can successfully import dashed lines from SWFs, but they’re seemingly being treated as fills and scaling actually scales line width, segment length etc, so it does not look really good (even with “hairline” selected as line thickness they still scale improperly. Hairline should always be 1 point thick. tested on Windows and HTML5 targets)

Anyone managed to draw these kind of lines and curves in code?

16

Thanks


#2

Totally just realized this is supported by Cairo and canvas:

https://www.cairographics.org/manual/cairo-cairo-t.html#cairo-set-dash

Sounds like this (and hairline) are TODO


#3

wondering what Cairo is for in OpenFL context.
Is it the default rendering engine for all OpenFL projects?


#4

Cairo is our software vector renderer. Think of it as canvas on native platforms :slight_smile:


#5

I converted a library (a single class) from AS3, if you want I can share.


#6

That would be awesome, thank you


#7

The original Class was made by Andy Woodruff, give it a try, I used it more than once and it made what I needed


/* 
DashedLine class
by Andy Woodruff (http://cartogrammar.com/blog || awoodruff@gmail.com)
May 2008
Still in progress; I'll get a more perfected version eventually. For now take it as is.

This is a Sprite with the capability to do basic lineTo drawing with dashed lines.
Example:
	var dashy:DashedLine = new DashedLine(2,0x333333,new Array(3,3,10,3,5,8,7,13));
	dashy.moveTo(120,120);
	dashy.beginFill(0xcccccc);
	dashy.lineTo(220,120);
	dashy.lineTo(220,220);
	dashy.lineTo(120,220);
	dashy.lineTo(120,120);
	dashy.endFill();
*/

package com.cartogrammar.drawing;

import openfl.display.Shape;
import openfl.display.Sprite;
import openfl.geom.Point;
import openfl.display.CapsStyle;

class DashedLine extends Sprite {
	
	private var lengthsArray	: Array<Int>	= new Array<Int>();	// array of dash and gap lengths (dash,gap,dash,gap....)
	private var lineColor		: UInt;							// line color
	private var lineWeight		: Float;						// line weight
	private var lineAlpha		: Float			= 1;			// line alpha
	private var curX			: Float			= 0;			// stores current x as it changes with lineTo and moveTo calls
	private var curY			: Float			= 0;			// same as above, but for y
	private var remainingDist	: Float			= 0;			// stores distance between the end of the last full dash or gap and the end coordinates specified in lineTo
	private var curIndex		: UInt;							// current index in the length array, so we know which dash or gap to draw
	private var arraySum		: Float			= 0;			// total length of the dashes and gaps... not currently being used for anything, but maybe useful?
	private var startIndex		: Int			= 0;			// array index (the particular dash or gap) to start with in a lineTo--based on the last dash or gap drawn in the previous lineTo (along with remainingDist, this is so our line can properly continue around corners!)
	
	private var fill			: Shape			= new Shape();	// shappe in the background to be used for fill (if any)
	private var stroke			: Shape			= new Shape();	// shape in the foreground to be used for the dashed line
	
	public function new(weight:Float = 0, color:Float = 0, lengthsArray:Array<Int> = null) {
		super();
		
		if (lengthsArray != null){	// if lengths array was specified, use it
			this.lengthsArray = lengthsArray;
		} else {	// if unspecified, use a default 5-5 line
			this.lengthsArray = [5,5];
		}
		if (this.lengthsArray.length % 2 != 0){	// if array has more dashes than gaps (i.e. an odd number of values), add a 5 gap to the end
			lengthsArray.push(5);
		}
		
		// sum the dash and gap lengths
		for (i in lengthsArray){
			arraySum += lengthsArray[i];
		}
		
		// set line weight and color properties from constructor arguments
		lineWeight = weight;
		lineColor = Math.round(color);
		
		// set the lineStyle according to specified properties - beyond weight and color, we use the defaults EXCEPT no line caps, as they interfere with the desired gaps
		stroke.graphics.lineStyle(lineWeight,lineColor,lineAlpha,false,"none",CapsStyle.NONE);
		
		// add fill and stroke shapes
		addChild(fill);
		addChild(stroke);
	}
	
	// basic moveTo method
	public function moveTo(x:Float,y:Float):Void{
		stroke.graphics.moveTo(x,y);	// move to specified x and y
		fill.graphics.moveTo(x,y);
		// keep track of x and y
		curX = x;	
		curY = y;
		// reset remainingDist and startIndex - if we are moving away from last line segment, the next one will start at the beginning of the dash-gap sequence
		remainingDist = 0;
		startIndex = 0;
	}
	
	// lineTo method
	public function lineTo(x:Float,y:Float):Void{
		var slope:Float = (y - curY)/(x - curX);	// get slope of segment to be drawn
		// record beginning x and y
		var startX:Float = curX;
		var startY:Float = curY;
		// positive or negative direction for each x and y?
		var xDir:Int = (x < startX) ? -1 : 1;
		var yDir:Int = (y < startY) ? -1 : 1;
		// keep drawing dashes and gaps as long as either the current x or y is not beyond the destination x or y
		
		var breakWhile: Bool = false;
		while (Math.abs(startX-curX) < Math.abs(startX-x) || Math.abs(startY-curY) < Math.abs(startY-y)){
			// loop through the array to draw the appropriate dash or gap, beginning with startIndex (either 0 or determined by the end of the last lineTo)
			for (i in startIndex...lengthsArray.length){
					var dist:Float = (remainingDist == 0) ? lengthsArray[i] : remainingDist;	// distance to draw is either the dash/gap length from the array or remainingDist left over from the last lineTo if there is any
					// get increments of x and y based on distance, slope, and direction - see getCoords()
					var xInc:Float = getCoords(dist,slope).x * xDir;
					var yInc:Float = getCoords(dist,slope).y * yDir;
					// if the length of the dash or gap will not go beyond the destination x or y of the lineTo, draw the dash or gap
					if (Math.abs(startX-curX) + Math.abs(xInc) < Math.abs(startX-x) || Math.abs(startY-curY) + Math.abs(yInc) < Math.abs(startY-y)){
						if (i % 2 == 0){	// if even index in the array, it is a dash, hence lineTo
							stroke.graphics.lineTo(curX + xInc,curY + yInc);
						} else {	// if odd, it's a gap, so moveTo
							stroke.graphics.moveTo(curX + xInc,curY + yInc);
						}
						// keep track of the new x and y
						curX += xInc;
						curY += yInc;
						curIndex = i;	// store the current dash or gap (array index)
						// reset startIndex and remainingDist, as these will only be non-zero for the first loop (through the array) of the lineTo
						startIndex = 0;
						remainingDist = 0;
					} else {	// if the dash or gap can't fit, break out of the loop
						remainingDist = getDistance(curX,curY,x,y);	// get the distance between the end of the last dash or gap and the destination x/y
						curIndex = i;	// store the current index
						breakWhile = true;	// ->
						break;				// -> break out of the while loop
					}
			}
			
			if (breakWhile) break;
		}
		
		startIndex = curIndex;	// for next time, the start index is the last index used in the loop

		if (remainingDist != 0){	// if there is a remaining distance, line or move from current x/y to the destination x/y
			if (curIndex % 2 == 0){	// even = dash
				stroke.graphics.lineTo(x,y);
			} else {	// odd = gap
				stroke.graphics.moveTo(x,y);
			}
			remainingDist = lengthsArray[curIndex] - remainingDist;	// remaining distance (which will be used at the beginning of the next lineTo) is now however much is left in the current dash or gap after that final lineTo/moveTo above
		} else {	// if there is no remaining distance (i.e. the final dash or gap fits perfectly), we're done with the current dash or gap, so increment the start index for next time
			if (startIndex == lengthsArray.length - 1){	// go to the beginning of the array if we're at the end
				startIndex = 0;
			} else {
				startIndex++;
			}
		}
		// at last, the current x and y are the destination x and y
		curX = x;
		curY = y;
		
		fill.graphics.lineTo(x,y);	// simple lineTo (invisible line) on the fill shape so that the fill (if one was started via beginFill below) follows along with the dashed line
	}
	
	// returns a point with the vertical and horizontal components of a diagonal given the distance and slope
	private function getCoords(distance:Float,slope:Float):Point {
		var angle:Float = Math.atan(slope);	// get the angle from the slope
		var vertical:Float = Math.abs(Math.sin(angle)*distance);	// vertical from sine of angle and length of hypotenuse - using absolute value here and applying negative as needed in lineTo, because this number doesn't always turn out to be negative or positive exactly when I want it to (haven't thought through the math enough yet to figure out why)
		var horizontal:Float = Math.abs(Math.cos(angle)*distance);	// horizontal from cosine
		return new Point(horizontal,vertical);	// return the point			
	}
	
	// basic Euclidean distance
	private function getDistance(startX:Float,startY:Float,endX:Float,endY:Float):Float{
		var distance:Float = Math.sqrt(Math.pow((endX-startX),2) + Math.pow((endY-startY),2));
		return distance;
	}
	
	// clear everything and reset the lineStyle
	public function clear():Void{
		stroke.graphics.clear();
		stroke.graphics.lineStyle(lineWeight,lineColor,lineAlpha,false,"none",CapsStyle.NONE);
		fill.graphics.clear();
		moveTo(0,0);
	}
	
	// set lineStyle with specified weight, color, and alpha
	public function lineStyle(w:Float=0,c:Float=0,a:Float=1):Void{
		lineWeight = w;
		lineColor = Math.round(c);
		lineAlpha = a;
		stroke.graphics.lineStyle(lineWeight,lineColor,lineAlpha,false,"none",CapsStyle.NONE);
	}
	
	// basic beginFill
	public function beginFill(c:UInt,a:Float=1):Void{
		fill.graphics.beginFill(c,a);
	}
	// basic endFill
	public function endFill():Void{
		fill.graphics.endFill();
	}
}

#8

Beautiful, thank you


#9

You’re welcome!

Let me know if it is ok for what you’re doing.