Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vector shapes example and add helper methods #521

Merged
merged 3 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 77 additions & 16 deletions core/shared/src/main/scala/eu/joaocosta/minart/geometry/Shape.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,84 @@ trait Shape {
}

object Shape {

/** Coordinates of a point in the shape.
*
* For performance reasons, only integer coordinates are supported,
* although shapes are free to use floating point in intermediate states
* and transformations.
*/
final case class Point(x: Int, y: Int)

/** The shape of a circle.
*
* If the radius is positive, the circle's front face is facing the viewer.
* If the radius is negative, the circle's back face is facing the viewer.
*/
def circle(center: Point, radius: Int): Circle = Circle(center, radius)

/** The shape of an axis aligned rectangle.
*
* If p1 is the top left point or bottom right point, the rectangle's front face is facing the viewer.
* Otherwise, the rectangle's back face is facing the viewer.
*/
def rectangle(p1: Point, p2: Point): ConvexPolygon = ConvexPolygon(
Vector(
p1,
Point(p2.x, p1.y),
p2,
Point(p1.x, p2.y)
)
)

/** The shape of a triangle.
*
* If the points are ordered clockwise, the triangle's front face is facing the viewer.
* Otherwise, the triangle's back face is facing the viewer.
*/
def triangle(p1: Point, p2: Point, p3: Point): ConvexPolygon = ConvexPolygon(
Vector(
p1,
p2,
p3
)
)

/** The shape of an arbitrary convex polygon.
*
* If the points are ordered clockwise, the polygon's front face is facing the viewer.
* Otherwise, the polygon's back face is facing the viewer.
*
* If the points do not form a convex polygon, the behavior is undefined.
*/
def convexPolygon(p1: Point, p2: Point, p3: Point, ps: Point*): ConvexPolygon = ConvexPolygon(
Vector(
p1,
p2,
p3
) ++ ps
)

/** Face of a convex polygon.
*
* If the points are defined in clockwise order, the front face faces the viewer.
*/
enum Face {
case Front
case Back
}

// Preallocated values to avoid repeated allocations
private[geometry] val someFront = Some(Face.Front)
private[geometry] val someBack = Some(Face.Back)

private[Shape] final case class MatrixShape(matrix: Matrix, shape: Shape) extends Shape {
def knownFace: Option[Shape.Face] = shape.knownFace
def knownFace: Option[Shape.Face] = if (matrix.a * matrix.e < 0)
shape.knownFace.map {
case Face.Front => Face.Back
case Face.Back => Face.Front
}
else shape.knownFace
lazy val aabb: AxisAlignedBoundingBox = {
val xs = Vector(
matrix.applyX(shape.aabb.x1, shape.aabb.y1),
Expand All @@ -159,19 +235,4 @@ object Shape {
override def mapMatrix(matrix: Matrix) =
MatrixShape(matrix.multiply(this.matrix), shape)
}

/** Face if a convex polygon.
*
* If the points are defined in clockwise order, the front face faces the viewer.
*/
enum Face {
case Front
case Back
}

// Preallocated values to avoid repeated allocations
private[geometry] val someFront = Some(Face.Front)
private[geometry] val someBack = Some(Face.Back)

final case class Point(x: Int, y: Int)
}
92 changes: 92 additions & 0 deletions examples/snapshot/10-vector-shapes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 10. Vector shapes

Besides surfaces, Minart also allows you to render some basic vector shapes.

## Drawing shapes

### Dependencies and imports

The relevant methods are in the `eu.joaocosta.minart.geometry` package

```scala
//> using scala "3.3.3"
//> using dep "eu.joaocosta::minart::0.6.2-SNAPSHOT"

import eu.joaocosta.minart.backend.defaults.given
import eu.joaocosta.minart.graphics.*
import eu.joaocosta.minart.geometry.*
import eu.joaocosta.minart.runtime.*
```

### Shapes

Vectorial shapes are represented by the `Shape` abstraction.

Minart already comes with some basic shapes, such as circle and convex polygons, with helper methods in the `Shape` companion object.

First, let's create a few shapes with those methods.

```scala
import eu.joaocosta.minart.geometry.Shape.Point

val triangle = Shape.triangle(Point(-16, 16), Point(0, -16), Point(16, 16))
val square = Shape.rectangle(Point(-16, -16), Point(16, 16))
val octagon = Shape.convexPolygon(
Point(-8, -16),
Point(8, -16),
Point(16, -8),
Point(16, 8),
Point(8, 16),
Point(-8, 16),
Point(-16, 8),
Point(-16, -8)
)
val circle = Shape.circle(Point(0, 0), 16)
```

### Faces

Notice that all shapes are defined with points in clockwise fashion.

All shapes have two faces: A front face and a back face.

Depending on the way they are defined (or transformed), different faces might be shown.

Minart allows you to set different colors for each face, and even no color at all!
This is helpful if, for some reason, you know you don't want to draw back faces.

### Rasterizing

Now we just need to use the `rasterize` operation, just like we did with `blit`.

In this example we will also scale our images with time, to show how the color changes when the face flips.

```scala
val frontfaceColor = Color(255, 0, 0)
val backfaceColor = Color(0, 255, 0)

def application(t: Double, canvas: Canvas): Unit = {
val scale = math.sin(t)
canvas.rasterize(triangle.scale(scale, 1.0), Some(frontfaceColor), Some(backfaceColor))(32, 32)
canvas.rasterize(square.scale(scale, 1.0), Some(frontfaceColor), Some(backfaceColor))(64, 32)
canvas.rasterize(octagon.scale(scale, 1.0), Some(frontfaceColor), Some(backfaceColor))(32, 64)
canvas.rasterize(circle.scale(scale, 1.0), Some(frontfaceColor), Some(backfaceColor))(64, 64)
}
```

### Putting it all together

```scala
val canvasSettings = Canvas.Settings(width = 128, height = 128, scale = Some(4), clearColor = Color(0, 0, 0))

AppLoop
.statefulRenderLoop((t: Double) => (canvas: Canvas) => {
canvas.clear()
application(t, canvas)
canvas.redraw()
t + 0.01
}
)
.configure(canvasSettings, LoopFrequency.hz60, 0)
.run()
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 10. Audio playback
# 11. Audio playback

Besides graphics and input, Minart also supports loading and playing back audio.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 11. Loading sounds
# 12. Loading sounds

Just like we did with images, we can also load audio clips from files.

Expand Down
Loading