diff --git a/src/flambe/display/Graphics.hx b/src/flambe/display/Graphics.hx index 3ec63287..abb27214 100644 --- a/src/flambe/display/Graphics.hx +++ b/src/flambe/display/Graphics.hx @@ -57,4 +57,7 @@ interface Graphics /** Draws a colored rectangle at the given region. */ function fillRect (color :Int, x :Float, y :Float, width :Float, height :Float) :Void; + + /** Draws a line using the given specifications. */ + function drawLine (color :Int, xStart :Float, yStart :Float, xEnd :Float, yEnd :Float, width :Float, roundedCap :Bool) :Void; } diff --git a/src/flambe/display/Shape.hx b/src/flambe/display/Shape.hx new file mode 100644 index 00000000..9a40d2e5 --- /dev/null +++ b/src/flambe/display/Shape.hx @@ -0,0 +1,156 @@ +// +// Flambe - Rapid game development +// https://github.com/aduros/flambe/blob/master/LICENSE.txt + +package flambe.display; + +import flambe.display.Shape.LineCaps; +import flambe.display.Sprite; +import flambe.math.Point; +import flambe.math.FMath; +import flambe.util.Assert; + +enum LineCaps +{ + None; + Rounded; +} + +/** + * A user defined shape (line, rectangle, polygon) that is assembled by adding + * various primitives together. Can be transformed like any Sprite object. + */ +class Shape extends Sprite +{ + public var lineWidth(default, default) :Float; + public var lineCap(default, default) :LineCaps; + public var strokeColor(default, default) :Int; + public var fillColor(default, default) :Int; + public var penCoordinate(default, null) :Point; + + private var _segments :Array; + + + public function new() + { + super(); + + lineWidth = 1.0; + lineCap = None; + strokeColor = 0x000000; + fillColor = 0xFFFFFF; + penCoordinate = new Point(); + + _segments = new Array(); + } + + public function lineStyle(width :Float, color :Int, cap :LineCaps) : Void + { + lineWidth = width; + strokeColor = color; + lineCap = cap; + } + + public function fillStyle(color :Int) : Void + { + fillColor = color; + } + + public function moveTo(x :Float, y :Float) : Void + { + penCoordinate.set(x, y); + } + + public function lineTo(x :Float, y :Float) : Void + { + var startPoint :Point = new Point(penCoordinate.x, penCoordinate.y); + penCoordinate.set(x, y); + + trace(startPoint); + + var index = _segments.length; + _segments[index] = new Segment(startPoint, penCoordinate, lineWidth, lineCap, strokeColor); + } + + public function curveTo(anchorX :Float, anchorY :Float, x :Float, y :Float) : Void + { + // Determine how much percision + var iPercInterval = 0.1; // 0.1 == 10 vertices + + var i :Float = 0.0; + var xa, ya, xb, yb; + while (i < 1.0) { + // Compute anchor/path + xa = FMath.lerp(penCoordinate.x, anchorX, i); + ya = FMath.lerp(penCoordinate.y, anchorY, i); + xb = FMath.lerp(anchorX, x, i ); + yb = FMath.lerp(anchorY, y, i ); + + // Find position along the anchor/path + lineTo(FMath.lerp(xa, xb, i), FMath.lerp(ya, yb, i)); + + i += iPercInterval; + } + } + + public function drawCircle(x :Float, y :Float, radius :Float) : Void + { + var numWedges :Int = Std.int(radius / 2); + if (numWedges < 12) numWedges = 12; + + var wedgeAngle :Float = (2.0 * Math.PI) / numWedges; + + moveTo(x + radius, y); + var theta :Float = 0.0; + for (i in 0...numWedges) + { + theta = i * wedgeAngle; + lineTo(x + radius * Math.cos(theta), y + radius * Math.sin(theta)); + } + } + + public function drawEllipse(x :Float, y :Float, width :Float, height :Float, ?rotation :Float) : Void + { + } + + public function drawRect(x :Float, y :Float, width :Float, height :Float) : Void + { + moveTo(x, y); + lineTo(x + width, y); + lineTo(x + width, y + height); + lineTo(x, y + height); + lineTo(x, y); + } + + public function clear() : Void + { + _segments = new Array(); + } + + override public function draw (g :Graphics) + { + for (seg in _segments) { + g.drawLine(seg.color, seg.startPt.x, seg.startPt.y, seg.endPt.x, seg.endPt.y, seg.width, seg.cap == Rounded); + } + } +} + +private class Segment +{ + public var startPt(default, null) :Point; + public var endPt(default, null) :Point; + public var width(default, null) :Float; + public var cap(default, null) :LineCaps; + public var color(default, null) :Int; + + public function new(startPoint :Point, endPoint :Point, lineWidth :Float, lineCap :LineCaps, clr :Int) + { + startPt = new Point(startPoint.x, startPoint.y); + endPt = new Point(endPoint.x, endPoint.y); + + width = lineWidth; + cap = lineCap; + + color = clr; + } +} \ No newline at end of file diff --git a/src/flambe/math/FMath.hx b/src/flambe/math/FMath.hx index b61bd913..86759df3 100644 --- a/src/flambe/math/FMath.hx +++ b/src/flambe/math/FMath.hx @@ -66,4 +66,9 @@ class FMath else if (value > 0) 1 else 0; } + + public static function lerp (n1 :Float, n2 :Float, percent :Float) :Float + { + return n1 + ((n2 - n1) * percent); + } } diff --git a/src/flambe/platform/OverdrawGraphics.hx b/src/flambe/platform/OverdrawGraphics.hx index d32c487e..3085d9ad 100644 --- a/src/flambe/platform/OverdrawGraphics.hx +++ b/src/flambe/platform/OverdrawGraphics.hx @@ -86,6 +86,11 @@ class OverdrawGraphics drawRegion(x, y, width, height); } + public function drawLine (color :Int, xStart :Float, yStart :Float, xEnd :Float, yEnd :Float, width :Float, roundedCap :Bool) + { + _impl.drawLine(color, xStart, yStart, xEnd, yEnd, width, roundedCap); + } + public function willRender () { _impl.willRender(); diff --git a/src/flambe/platform/flash/Stage3DGraphics.hx b/src/flambe/platform/flash/Stage3DGraphics.hx index f00f4947..f4f2615a 100644 --- a/src/flambe/platform/flash/Stage3DGraphics.hx +++ b/src/flambe/platform/flash/Stage3DGraphics.hx @@ -233,6 +233,57 @@ class Stage3DGraphics data[++offset] = a; } + public function drawLine (color :Int, xStart :Float, yStart :Float, xEnd :Float, yEnd :Float, width :Float, roundedCap :Bool) :Void + { + var state = getTopState(); + if (state.emptyScissor()) { + return; + } + + var pos = transformQuadForLine(xStart, yStart, xEnd, yEnd, width); + var r = (color & 0xff0000) / 0xff0000; + var g = (color & 0x00ff00) / 0x00ff00; + var b = (color & 0x0000ff) / 0x0000ff; + var a = state.alpha; + + var offset = _batcher.prepareFillRect(_renderTarget, state.blendMode, state.getScissor()); + var data = _batcher.data; + + data[ offset] = pos[0]; + data[++offset] = pos[1]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + data[++offset] = pos[3]; + data[++offset] = pos[4]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + data[++offset] = pos[6]; + data[++offset] = pos[7]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + data[++offset] = pos[9]; + data[++offset] = pos[10]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + if (roundedCap) + { + drawLineCap(true, xStart, yStart, width, r, g, b, a); + drawLineCap(false, xEnd, yEnd, width, r, g, b, a); + } + } + public function multiplyAlpha (factor :Float) { getTopState().alpha *= factor; @@ -339,6 +390,134 @@ class Stage3DGraphics return pos; } + private function transformQuadForLine(xStart :Float, yStart :Float, xEnd :Float, yEnd :Float, width :Float) :Vector + { + var halfWidth = width * 0.5; + var pos = _scratchQuadVector; + + pos[2] = 0; + pos[5] = 0; + pos[8] = 0; + pos[11] = 0; + + // Case for vertical line + if(xStart == xEnd) { + pos[0] = xStart - halfWidth; + pos[1] = yStart; + pos[3] = xStart + halfWidth; + pos[4] = yStart; + + pos[6] = xEnd + halfWidth; + pos[7] = yEnd; + pos[9] = xEnd - halfWidth; + pos[10] = yEnd; + + _startTheta = (yStart > yEnd) ? Math.PI : 0.0; + } + // Case for horizontal line + else if(yStart == yEnd) { + pos[0] = xStart; + pos[1] = yStart - halfWidth; + pos[3] = xStart; + pos[4] = yStart + halfWidth; + + pos[6] = xEnd; + pos[7] = yEnd + halfWidth; + pos[9] = xEnd; + pos[10] = yEnd - halfWidth; + + _startTheta = (xStart > xEnd) ? 0.5*Math.PI : -0.5*Math.PI; + } + // Final case for any line with slope + else { + var slopePerp = (xStart - xEnd) / (yEnd - yStart); + var xOffset = Math.sqrt((halfWidth * halfWidth) / (1.0 + (slopePerp * slopePerp))); + + pos[0] = xStart - xOffset; + pos[1] = slopePerp * (pos[0] - xStart) + yStart; + pos[3] = xStart + xOffset; + pos[4] = slopePerp * (pos[3] - xStart) + yStart; + + pos[6] = xEnd + xOffset; + pos[7] = slopePerp * (pos[6] - xEnd) + yEnd; + pos[9] = xEnd - xOffset; + pos[10] = slopePerp * (pos[9] - xEnd) + yEnd; + + _startTheta = Math.atan(slopePerp); + if(yStart > yEnd) { + _startTheta += Math.PI; + } + } + + getTopState().matrix.transformVectors(pos, pos); + return pos; + } + + private function drawLineCap(startCap :Bool, xCtr :Float, yCtr :Float, width :Float, red :Float, green :Float, blue :Float, alpha :Float) + { + var halfWidth = width * 0.5; + var numWedgeForCap :Int = Std.int(width / 4); + if (numWedgeForCap < 6) numWedgeForCap = 6; + + var wedgeAngle = (2.0 * Math.PI) / (numWedgeForCap * 2); + wedgeAngle *= (startCap ? -1.0 : 1.0); + + for (i in 0...numWedgeForCap) + { + var pos = _scratchQuadVector; + pos[0] = xCtr; + pos[1] = yCtr; + + var theta = _startTheta; + theta += i * wedgeAngle; + + pos[3] = xCtr + halfWidth*Math.cos(theta); + pos[4] = yCtr + halfWidth*Math.sin(theta); + + theta += wedgeAngle; + + pos[6] = xCtr + halfWidth*Math.cos(theta); + pos[7] = yCtr + halfWidth*Math.sin(theta); + + pos[9] = xCtr; + pos[10] = yCtr; + + var state = getTopState(); + state.matrix.transformVectors(pos, pos); + + var offset = _batcher.prepareFillRect(_renderTarget, state.blendMode, state.getScissor()); + var data = _batcher.data; + + data[ offset] = pos[0]; + data[++offset] = pos[1]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + + data[++offset] = pos[3]; + data[++offset] = pos[4]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + + data[++offset] = pos[6]; + data[++offset] = pos[7]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + + data[++offset] = pos[9]; + data[++offset] = pos[10]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + } + } + private static var _scratchMatrix3D = new Matrix3D(); private static var _scratchClipVector = new Vector(2*3, true); private static var _scratchQuadVector = new Vector(4*3, true); @@ -348,6 +527,8 @@ class Stage3DGraphics return v; })(); + private var _startTheta :Float; // Used for drawing rounded line caps + private var _batcher :Stage3DBatcher; private var _renderTarget :Stage3DTextureRoot; diff --git a/src/flambe/platform/html/CanvasGraphics.hx b/src/flambe/platform/html/CanvasGraphics.hx index 62a56094..9f0ae1ad 100644 --- a/src/flambe/platform/html/CanvasGraphics.hx +++ b/src/flambe/platform/html/CanvasGraphics.hx @@ -114,6 +114,23 @@ class CanvasGraphics _canvasCtx.fillRect(Std.int(x), Std.int(y), Std.int(width), Std.int(height)); } + public function drawLine (color :Int, xStart :Float, yStart :Float, xEnd :Float, yEnd :Float, width :Float, roundedCap :Bool) + { + _canvasCtx.beginPath(); + _canvasCtx.moveTo(xStart, yStart); + _canvasCtx.lineTo(xEnd, yEnd); + _canvasCtx.lineWidth = width; + _canvasCtx.lineCap = roundedCap ? "round" : "butt"; + + // Convert color into a hex string in the form of #RRGGBB + var hex = untyped (0xffffff & color).toString(16); + while (hex.length < 6) { + hex = "0"+hex; + } + _canvasCtx.strokeStyle = hex; + _canvasCtx.stroke(); + } + public function multiplyAlpha (factor :Float) { _canvasCtx.globalAlpha *= factor; diff --git a/src/flambe/platform/html/WebGLGraphics.hx b/src/flambe/platform/html/WebGLGraphics.hx index b0dd9c3e..627dde89 100644 --- a/src/flambe/platform/html/WebGLGraphics.hx +++ b/src/flambe/platform/html/WebGLGraphics.hx @@ -227,6 +227,54 @@ class WebGLGraphics data[++offset] = a; } + public function drawLine (color :Int, xStart :Float, yStart :Float, xEnd :Float, yEnd :Float, width :Float, roundedCap :Bool) + { + var state = getTopState(); + + var pos = transformQuadForLine(xStart, yStart, xEnd, yEnd, width); + var r = (color & 0xff0000) / 0xff0000; + var g = (color & 0x00ff00) / 0x00ff00; + var b = (color & 0x0000ff) / 0x0000ff; + var a = state.alpha; + + var offset = _batcher.prepareFillRect(_renderTarget, state.blendMode, state.scissor); + var data = _batcher.data; + + data[ offset] = pos[0]; + data[++offset] = pos[1]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + data[++offset] = pos[2]; + data[++offset] = pos[3]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + data[++offset] = pos[4]; + data[++offset] = pos[5]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + data[++offset] = pos[6]; + data[++offset] = pos[7]; + data[++offset] = r; + data[++offset] = g; + data[++offset] = b; + data[++offset] = a; + + if (roundedCap) + { + drawLineCap(true, xStart, yStart, width, r, g, b, a); + drawLineCap(false, xEnd, yEnd, width, r, g, b, a); + } + } + public function multiplyAlpha (factor :Float) { getTopState().alpha *= factor; @@ -323,9 +371,134 @@ class WebGLGraphics return pos; } + private function transformQuadForLine(xStart :Float, yStart :Float, xEnd :Float, yEnd :Float, width :Float) :Float32Array + { + var halfWidth = width * 0.5; + var pos = _scratchQuadArray; + + // Edge case for vertical line + if(xStart == xEnd) { + pos[0] = xStart - halfWidth; + pos[1] = yStart; + pos[2] = xStart + halfWidth; + pos[3] = yStart; + + pos[4] = xEnd + halfWidth; + pos[5] = yEnd; + pos[6] = xEnd - halfWidth; + pos[7] = yEnd; + + _startTheta = (yStart > yEnd) ? Math.PI : 0.0; + } + // Edge case for horizontal line + else if(yStart == yEnd) { + pos[0] = xStart; + pos[1] = yStart - halfWidth; + pos[2] = xStart; + pos[3] = yStart + halfWidth; + + pos[4] = xEnd; + pos[5] = yEnd + halfWidth; + pos[6] = xEnd; + pos[7] = yEnd - halfWidth; + + _startTheta = (xStart > xEnd) ? 0.5*Math.PI : -0.5*Math.PI; + } + // Final Edge case for any line with slope + else { + var slopePerp = (xStart - xEnd) / (yEnd - yStart); + var xOffset = Math.sqrt((halfWidth * halfWidth) / (1.0 + (slopePerp * slopePerp))); + + pos[0] = xStart - xOffset; + pos[1] = slopePerp * (pos[0] - xStart) + yStart; + pos[2] = xStart + xOffset; + pos[3] = slopePerp * (pos[2] - xStart) + yStart; + + pos[4] = xEnd + xOffset; + pos[5] = slopePerp * (pos[4] - xEnd) + yEnd; + pos[6] = xEnd - xOffset; + pos[7] = slopePerp * (pos[6] - xEnd) + yEnd; + + _startTheta = Math.atan(slopePerp); + if(yStart > yEnd) { + _startTheta += Math.PI; + } + } + + getTopState().matrix.transformArray(cast pos, 8, cast pos); + return pos; + } + + private function drawLineCap(startCap :Bool, xCtr :Float, yCtr :Float, width :Float, red :Float, green :Float, blue :Float, alpha :Float) + { + var halfWidth = width * 0.5; + var numWedgeForCap :Int = Std.int(width / 4); + if (numWedgeForCap < 6) numWedgeForCap = 6; + + var wedgeAngle = (2.0 * Math.PI) / (numWedgeForCap * 2); + wedgeAngle *= (startCap ? -1.0 : 1.0); + + for (i in 0...numWedgeForCap) + { + var pos = _scratchQuadArray; + pos[0] = xCtr; + pos[1] = yCtr; + + var theta = _startTheta; + theta += i * wedgeAngle; + + pos[2] = xCtr + halfWidth*Math.cos(theta); + pos[3] = yCtr + halfWidth*Math.sin(theta); + + theta += wedgeAngle; + + pos[4] = xCtr + halfWidth*Math.cos(theta); + pos[5] = yCtr + halfWidth*Math.sin(theta); + + pos[6] = xCtr; + pos[7] = yCtr; + + var state = getTopState(); + state.matrix.transformArray(cast pos, 8, cast pos); + + var offset = _batcher.prepareFillRect(_renderTarget, state.blendMode, state.scissor); + var data = _batcher.data; + + data[ offset] = pos[0]; + data[++offset] = pos[1]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + + data[++offset] = pos[2]; + data[++offset] = pos[3]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + + data[++offset] = pos[4]; + data[++offset] = pos[5]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + + data[++offset] = pos[6]; + data[++offset] = pos[7]; + data[++offset] = red; + data[++offset] = green; + data[++offset] = blue; + data[++offset] = alpha; + } + } + private static var _scratchMatrix = new Matrix(); private static var _scratchQuadArray :Float32Array = null; + private var _startTheta :Float; // Used for drawing rounded line caps + private var _batcher :WebGLBatcher; private var _renderTarget :WebGLTextureRoot;