Skip to content

Commit

Permalink
Merge pull request #530 from JD557/rasterize-lines
Browse files Browse the repository at this point in the history
Support stroke rasterization
  • Loading branch information
JD557 authored Oct 5, 2024
2 parents aff735b + 4baf726 commit 19c5ef7
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import sdl2.all.*
import sdl2.enumerations.SDL_BlendMode.*
import sdl2.enumerations.SDL_EventType.*
import sdl2.enumerations.SDL_InitFlag.*
import sdl2.enumerations.SDL_KeyCode.*
import sdl2.enumerations.SDL_WindowFlags.*

import eu.joaocosta.minart.graphics.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ object AxisAlignedBoundingBox {
this
}

def add(point: Shape.Point): this.type = add(point.x, point.y)
def add(point: Point): this.type = add(point.x, point.y)

def result(): AxisAlignedBoundingBox =
if (x1 > x2 || y1 > y2) AxisAlignedBoundingBox(0, 0, 0, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package eu.joaocosta.minart.geometry
* @param center center of the circle.
* @param radius circle radius
*/
final case class Circle(center: Shape.Point, radius: Double) extends Shape {
final case class Circle(center: Point, radius: Double) extends Shape.ShapeWithContour {

/** The absolute radius */
val absRadius = math.abs(radius)
Expand All @@ -29,6 +29,8 @@ final case class Circle(center: Shape.Point, radius: Double) extends Shape {
else if (radius < 0) Shape.someBack
else None

def contour: Vector[Stroke] = Vector(Stroke.Circle(center, radius))

def faceAt(x: Int, y: Int): Option[Shape.Face] = {
if ((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y) <= squareRadius)
knownFace
Expand Down Expand Up @@ -57,26 +59,26 @@ final case class Circle(center: Shape.Point, radius: Double) extends Shape {
(this.center.x - that.center.x) * (this.center.x - that.center.x) +
(this.center.y - that.center.y) * (this.center.y - that.center.y) <= sumRadius * sumRadius

override def translate(dx: Double, dy: Double): Shape =
override def translate(dx: Double, dy: Double): Circle =
if (dx == 0 && dy == 0) this
else Circle(Shape.Point(center.x + dx, center.y + dy), radius)
else Circle(Point(center.x + dx, center.y + dy), radius)

override def flipH: Circle =
Circle(Shape.Point(-center.x, center.y), -radius)
Circle(Point(-center.x, center.y), -radius)

override def flipV: Circle =
Circle(Shape.Point(center.x, -center.y), -radius)
Circle(Point(center.x, -center.y), -radius)

override def scale(s: Double): Shape =
override def scale(s: Double): Circle =
if (s == 1.0) this
else Circle(Shape.Point(center.x * s, center.y * s), radius * s)
else Circle(Point(center.x * s, center.y * s), radius * s)

override def rotate(theta: Double): Shape = {
val matrix = Matrix.rotation(theta)
if (matrix == Matrix.identity) this
else {
Circle(
Shape.Point(
Point(
matrix.applyX(center.x.toDouble, center.y.toDouble),
matrix.applyY(center.x.toDouble, center.y.toDouble)
),
Expand All @@ -86,5 +88,5 @@ final case class Circle(center: Shape.Point, radius: Double) extends Shape {
}

override def transpose: Circle =
Circle(center = Shape.Point(center.y, center.x), -radius)
Circle(center = Point(center.y, center.x), -radius)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ package eu.joaocosta.minart.geometry
*
* @param vertices ordered sequence of vertices.
*/
final case class ConvexPolygon(vertices: Vector[Shape.Point]) extends Shape {
final case class ConvexPolygon(vertices: Vector[Point]) extends Shape.ShapeWithContour {
val size = vertices.size
require(size >= 3, "A polygon needs at least 3 vertices")

Expand All @@ -24,10 +24,18 @@ final case class ConvexPolygon(vertices: Vector[Shape.Point]) extends Shape {
lazy val knownFace: Option[Shape.Face] =
faceAt(vertices.head)

lazy val contour: Vector[Stroke] =
(vertices :+ vertices.head)
.sliding(2)
.collect { case Vector(a, b) =>
Stroke.Line(a, b)
}
.toVector

private def edgeFunction(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double): Double =
(x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)

private def edgeFunction(p1: Shape.Point, p2: Shape.Point, p3: Shape.Point): Double =
private def edgeFunction(p1: Point, p2: Point, p3: Point): Double =
edgeFunction(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)

private def rawWeights(x: Double, y: Double): Iterator[Double] = {
Expand Down Expand Up @@ -129,20 +137,37 @@ final case class ConvexPolygon(vertices: Vector[Shape.Point]) extends Shape {
* @param point point to test
* @return weight
*/
def edgeWeights(point: Shape.Point): Vector[Double] = {
def edgeWeights(point: Point): Vector[Double] = {
rawWeights(point.x, point.y).map(_ / maxWeight.toDouble).toVector
}

override def mapMatrix(matrix: Matrix) =
override def mapMatrix(matrix: Matrix): Shape.ShapeWithContour =
if (matrix == Matrix.identity) this
else ConvexPolygon.MatrixPolygon(matrix, this)

// Overrides to refine the type signature
override def contramapMatrix(matrix: Matrix): Shape.ShapeWithContour =
mapMatrix(matrix.inverse)
override def translate(dx: Double, dy: Double): Shape.ShapeWithContour =
mapMatrix(Matrix.translation(dx, dy))
override def flipH: Shape.ShapeWithContour = mapMatrix(Matrix.flipH)
override def flipV: Shape.ShapeWithContour = mapMatrix(Matrix.flipV)
override def scale(sx: Double, sy: Double): Shape.ShapeWithContour =
mapMatrix(Matrix.scaling(sx, sy))
override def scale(s: Double): Shape.ShapeWithContour = scale(s, s)
override def rotate(theta: Double): Shape.ShapeWithContour =
mapMatrix(Matrix.rotation(theta))
override def shear(sx: Double, sy: Double): Shape.ShapeWithContour =
mapMatrix(Matrix.shear(sx, sy))
override def transpose: Shape.ShapeWithContour = mapMatrix(Matrix.transpose)
}

object ConvexPolygon {
private[ConvexPolygon] final case class MatrixPolygon(matrix: Matrix, polygon: ConvexPolygon) extends Shape {
private[ConvexPolygon] final case class MatrixPolygon(matrix: Matrix, polygon: ConvexPolygon)
extends Shape.ShapeWithContour {
lazy val toConvexPolygon = ConvexPolygon(
vertices = polygon.vertices.map { point =>
Shape.Point(
Point(
matrix.applyX(point.x, point.y),
matrix.applyY(point.x, point.y)
)
Expand All @@ -151,12 +176,15 @@ object ConvexPolygon {

def knownFace: Option[Shape.Face] = toConvexPolygon.knownFace
def aabb: AxisAlignedBoundingBox = toConvexPolygon.aabb
def contour = toConvexPolygon.contour
def faceAt(x: Int, y: Int): Option[Shape.Face] =
toConvexPolygon.faceAt(x, y)
override def contains(x: Int, y: Int): Boolean =
toConvexPolygon.contains(x, y)
override def mapMatrix(matrix: Matrix) =
override def mapMatrix(matrix: Matrix): MatrixPolygon =
if (matrix == Matrix.identity) this
else MatrixPolygon(matrix.multiply(this.matrix), polygon)
override def translate(dx: Double, dy: Double): MatrixPolygon =
mapMatrix(Matrix.translation(dx, dy))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package eu.joaocosta.minart.geometry

/** Coordinates of a point in the shape or stroke.
*/
final case class Point(x: Double, y: Double)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package eu.joaocosta.minart.geometry

/** Abstract shape.
*
* Can be combined with other shapes and can check if a point is inside of it.
* Can be combined with other shapes and can check if a point is inside of it.
*
* This API is *experimental* and might change in the near future.
*/
Expand Down Expand Up @@ -40,7 +40,7 @@ trait Shape {
* @param y y coordinates of the point
* @return None if the point is not contained, Some(face) if the point is contained.
*/
final def faceAt(point: Shape.Point): Option[Shape.Face] = faceAt(point.x.toInt, point.y.toInt)
final def faceAt(point: Point): Option[Shape.Face] = faceAt(point.x.toInt, point.y.toInt)

/** Checks if this shape contains a point.
*
Expand All @@ -60,7 +60,7 @@ trait Shape {
* @param y y coordinates of the point
* @return None if the point is not contained, Some(face) if the point is contained.
*/
final def contains(point: Shape.Point): Boolean = contains(point.x.toInt, point.y.toInt)
final def contains(point: Point): Boolean = contains(point.x.toInt, point.y.toInt)

/** Contramaps the points in this shape using a matrix.
*
Expand Down Expand Up @@ -89,14 +89,14 @@ trait Shape {
* Internally, this method will invert the matrix, so for performance sensitive operations it is recommended to use
* mapMatrix with the direct matrix instead.
*/
def contramapMatrix(matrix: Matrix) =
def contramapMatrix(matrix: Matrix): Shape =
mapMatrix(matrix.inverse)

/** Maps this the points in this shape using a matrix.
*
* This method can be chained multiple times efficiently.
*/
def mapMatrix(matrix: Matrix) =
def mapMatrix(matrix: Matrix): Shape =
if (matrix == Matrix.identity) this
else Shape.MatrixShape(matrix, this)

Expand Down Expand Up @@ -132,10 +132,6 @@ trait Shape {

object Shape {

/** Coordinates of a point in the shape.
*/
final case class Point(x: Double, y: Double)

/** The shape of a circle.
*
* If the radius is positive, the circle's front face is facing the viewer.
Expand Down Expand Up @@ -194,6 +190,17 @@ object Shape {
case Back
}

/** Shape with a contour that can be rendered.
*
* It's OK for some shapes to not provide a contour.
*/
trait ShapeWithContour extends Shape {

/** Contour of this shape as a list of strokes.
*/
def contour: Vector[Stroke]
}

// Preallocated values to avoid repeated allocations
private[geometry] val someFront = Some(Face.Front)
private[geometry] val someBack = Some(Face.Back)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package eu.joaocosta.minart.geometry

/** Represents lines, curves or countours that can be stroked.
*/
enum Stroke {
case Line(p1: Point, p2: Point)
case Circle(center: Point, radius: Double)
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ trait MutableSurface extends Surface {
blit(surface, blendMode)(0, 0)
}

/** Draws a stroke on top of this surface.
*
* This API is *experimental* and might change in the near future.
*
* @param stroke shape to draw
* @param color color of the line
* @param x position of the shape origin on the destination surface
* @param y position of the shape origin on the destination surface
*/
def rasterizeStroke(
stroke: Stroke,
color: Color
)(x: Int, y: Int): Unit = {
Rasterizer.rasterizeStroke(this, stroke, color, x, y)
}

/** Draws a shape on top of this surface.
*
* This API is *experimental* and might change in the near future.
Expand All @@ -120,7 +136,7 @@ trait MutableSurface extends Surface {
* @param x position of the shape origin on the destination surface
* @param y position of the shape origin on the destination surface
*/
def rasterize(
def rasterizeShape(
shape: Shape,
frontfaceColor: Option[Color],
backfaceColor: Option[Color] = None,
Expand All @@ -129,6 +145,22 @@ trait MutableSurface extends Surface {
Rasterizer.rasterizeShape(this, shape.translate(x, y), frontfaceColor, backfaceColor, blendMode)
}

/** Draws the contour of a shape on top of this surface.
*
* This API is *experimental* and might change in the near future.
*
* @param shape shape whose countour to draw
* @param color color of the line
* @param x position of the shape origin on the destination surface
* @param y position of the shape origin on the destination surface
*/
def rasterizeContour(
shape: Shape.ShapeWithContour,
color: Color
)(x: Int, y: Int): Unit = {
shape.contour.foreach(stroke => rasterizeStroke(stroke, color)(x, y))
}

/** Modifies this surface using surface view transformations
*
* @param f operations to apply
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ private[graphics] object Rasterizer {
}
}

private def rasterizeShapeBothFaces(
def rasterizeShapeBothFaces(
dest: MutableSurface,
area: AxisAlignedBoundingBox,
shape: Shape,
Expand Down Expand Up @@ -92,4 +92,72 @@ private[graphics] object Rasterizer {
}
}

// From https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
def rasterizeLine(dest: MutableSurface, stroke: Stroke.Line, color: Color): Unit = {
val x1 = math.round(stroke.p1.x).toInt
val y1 = math.round(stroke.p1.y).toInt
val x2 = math.round(stroke.p2.x).toInt
val y2 = math.round(stroke.p2.y).toInt

val dx = math.abs(x2 - x1)
val sx = if (x1 < x2) 1 else -1

val dy = -math.abs(y2 - y1)
val sy = if (y1 < y2) 1 else -1

var x = x1
var y = y1
var error = dx + dy

while (!(x == x2 && y == y2)) {
dest.putPixel(x, y, color)
val doubleError = 2 * error
if (doubleError >= dy && x != x2) {
error = error + dy
x = x + sx
}
if (doubleError <= dx && y != y2) {
error = error + dx
y = y + sy
}
}
dest.putPixel(x, y, color)
}

// From https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
def rasterizeCircle(dest: MutableSurface, stroke: Stroke.Circle, color: Color): Unit = {
val cx = math.round(stroke.center.x).toInt
val cy = math.round(stroke.center.y).toInt

var t = math.round(stroke.radius / 16).toInt
var dx = math.round(stroke.radius).toInt
var dy = 0

while (dx >= dy) {
dest.putPixel(cx + dx, cy + dy, color)
dest.putPixel(cx + dx, cy - dy, color)
dest.putPixel(cx - dx, cy + dy, color)
dest.putPixel(cx - dx, cy - dy, color)

dest.putPixel(cx + dy, cy + dx, color)
dest.putPixel(cx + dy, cy - dx, color)
dest.putPixel(cx - dy, cy + dx, color)
dest.putPixel(cx - dy, cy - dx, color)

dy = dy + 1
t = t + dy
if (t - dx >= 0) {
t = t - dx
dx = dx - 1
}
}
}

def rasterizeStroke(dest: MutableSurface, stroke: Stroke, color: Color, dx: Int, dy: Int): Unit = stroke match {
case Stroke.Line(Point(x1, y1), Point(x2, y2)) =>
rasterizeLine(dest, Stroke.Line(Point(x1 + dx, y1 + dy), Point(x2 + dx, y2 + dy)), color)
case Stroke.Circle(Point(cx, cy), radius) =>
rasterizeCircle(dest, Stroke.Circle(Point(cx + dx, cy + dy), radius), color)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import eu.joaocosta.minart.audio.*
import eu.joaocosta.minart.backend.defaults.*
import eu.joaocosta.minart.backend.subsystem.*
import eu.joaocosta.minart.graphics.*
import eu.joaocosta.minart.runtime.*

/** App loop that keeps an internal state that is passed to every iteration.
*/
Expand Down
Loading

0 comments on commit 19c5ef7

Please sign in to comment.