Skip to content

Commit

Permalink
adjust layouts, introduce shape coloring, make identicon configurable…
Browse files Browse the repository at this point in the history
…, extend the documentation
  • Loading branch information
majk-p committed Apr 10, 2022
1 parent 3701152 commit 9283791
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 36 deletions.
34 changes: 31 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,41 @@ Simple scala library for generating identicons - visual hashes for arbitrary str
- Take a string as an input
- Generate hash
- Use the hash as a random seed
- Randomly choose the layout
- Randomly choose the layout, by default combine from 1 to 3 random basic layout
- Fill the layout with randomly selected shapes
- Generate 2D image

## Usage

### Basic

The most basic use case is covered by the `defaultInstance`.

```scala
import net.michalp.identicon4s.Identicon
val identicon = Identicon.defaultInstance[Id]()

val image = identicon.generate("test")
val f = new File(s"test.png");
ImageIO.write(image, "png", f)
```

Resulting image

![test.png](./images/test.png)

### Advanced

Identicon is configurable. You can tune the amount of layout selection iterations and turn on the coloring.

```scala
val config = Identicon.Config(
minLayoutIterations = 5,
maxLayoutIterations = 10,
renderMonochromatic = false
)
import net.michalp.identicon4s.Identicon
val identicon = Identicon.defaultInstance[Id]
val identicon = Identicon.defaultInstance[Id](config)

val image = identicon.generate("test")
val f = new File(s"test.png");
Expand All @@ -24,4 +50,6 @@ ImageIO.write(image, "png", f)

Resulting image

![test.png](./images/test.png)
![test-color.png](./images/test-color.png)

There's also `def instance[F[_]: Hashing: Functor](config: Config)` method that allows you to instantiate `Identicon` with custom your own implementation of `Hashing`.
Binary file added images/test-color.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/main/scala/net/michalp/identicon4s/Demo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import java.io.File
import cats.Id

object Demo extends App {
val identicon = Identicon.defaultInstance[Id]
val identicon = Identicon.defaultInstance[Id]()

def renderImage(identicon: Identicon[Id])(text: String) = {
val image = identicon.generate(text)
Expand Down
30 changes: 25 additions & 5 deletions src/main/scala/net/michalp/identicon4s/Identicon.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,43 @@ trait Identicon[F[_]] {
object Identicon {
def apply[F[_]](implicit ev: Identicon[F]): Identicon[F] = ev

def defaultInstance[F[_]: Applicative] = {
def defaultInstance[F[_]: Applicative](config: Config = Config.default) = {
implicit val hashing: Hashing[F] = Hashing.instance
instance[F]
instance[F](config)
}

def instance[F[_]: Hashing: Functor] = new Identicon[F] {
def instance[F[_]: Hashing: Functor](config: Config) = new Identicon[F] {

override def generate(input: String): F[RenderedImage] =
Hashing[F].hash(input).map { seed =>
val random = new Random(seed)
val shapes: Shapes = Shapes.instance(random)
val layouts: Layouts = Layouts.instance(shapes, random)
val renderer: Renderer = Renderer.instance
val layouts: Layouts = Layouts.instance(shapes, random, config)
val renderer: Renderer = Renderer.instance(config, random)
val layout = layouts.randomLayout
renderer.render(layout)
}

}

final case class Config(
minLayoutIterations: Int,
maxLayoutIterations: Int,
renderMonochromatic: Boolean
) {
assert(minLayoutIterations >= 1, "At least one layout iteration required")
assert(maxLayoutIterations >= 1, "At least one layout iteration required")
assert(maxLayoutIterations > minLayoutIterations, "Minimal layout iterations has to be less than maximum")
}

object Config {

val default = Config(
minLayoutIterations = 1,
maxLayoutIterations = 3,
renderMonochromatic = true
)

}

}
47 changes: 27 additions & 20 deletions src/main/scala/net/michalp/identicon4s/Layouts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,17 @@ private[identicon4s] trait Layouts {

private[identicon4s] object Layouts {

def instance(shapes: Shapes, random: Random): Layouts = new Layouts {
def instance(shapes: Shapes, random: Random, config: Identicon.Config): Layouts = new Layouts {

override def randomLayout: Layout =
List.fill(random.between(1, 4))(singleRandomLayout).combineAll
List
.fill(
random.between(
config.minLayoutIterations,
config.maxLayoutIterations + 1
)
)(singleRandomLayout)
.combineAll

private def singleRandomLayout: Layout =
random.nextInt().abs.toInt % 5 match {
Expand Down Expand Up @@ -63,10 +70,10 @@ private[identicon4s] object Layouts {
final case class Diamond(a: Shape, b: Shape, c: Shape, d: Shape) extends Layout {

override val shapesOnLayout: Seq[ShapeOnLayout] = Seq(
ShapeOnLayout(a, 0.5, 0.1),
ShapeOnLayout(b, 0.1, 0.5),
ShapeOnLayout(c, 0.6, 0.5),
ShapeOnLayout(d, 0.5, 0.6)
ShapeOnLayout(a, 0.5, 0.2),
ShapeOnLayout(b, 0.2, 0.5),
ShapeOnLayout(c, 0.8, 0.5),
ShapeOnLayout(d, 0.5, 0.8)
)

}
Expand All @@ -79,10 +86,10 @@ private[identicon4s] object Layouts {
final case class Square(a: Shape, b: Shape, c: Shape, d: Shape) extends Layout {

override val shapesOnLayout: Seq[ShapeOnLayout] = Seq(
ShapeOnLayout(a, 0.1, 0.1),
ShapeOnLayout(b, 0.1, 0.6),
ShapeOnLayout(c, 0.6, 0.1),
ShapeOnLayout(d, 0.6, 0.6)
ShapeOnLayout(a, 0.2, 0.2),
ShapeOnLayout(b, 0.2, 0.8),
ShapeOnLayout(c, 0.8, 0.2),
ShapeOnLayout(d, 0.8, 0.8)
)

}
Expand All @@ -95,9 +102,9 @@ private[identicon4s] object Layouts {
final case class Triangle(a: Shape, b: Shape, c: Shape) extends Layout {

override val shapesOnLayout: Seq[ShapeOnLayout] = Seq(
ShapeOnLayout(a, 0.5, 0.1),
ShapeOnLayout(b, 0.1, 0.6),
ShapeOnLayout(c, 0.7, 0.6)
ShapeOnLayout(a, 0.5, 0.2),
ShapeOnLayout(b, 0.1, 0.8),
ShapeOnLayout(c, 0.9, 0.8)
)

}
Expand All @@ -112,10 +119,10 @@ private[identicon4s] object Layouts {
final case class ShapeX(a: Shape, b: Shape, c: Shape, d: Shape, e: Shape) extends Layout {

override val shapesOnLayout: Seq[ShapeOnLayout] = Seq(
ShapeOnLayout(a, 0.1, 0.1),
ShapeOnLayout(b, 0.1, 0.6),
ShapeOnLayout(c, 0.6, 0.1),
ShapeOnLayout(d, 0.6, 0.6),
ShapeOnLayout(a, 0.2, 0.2),
ShapeOnLayout(b, 0.2, 0.8),
ShapeOnLayout(c, 0.8, 0.2),
ShapeOnLayout(d, 0.8, 0.8),
ShapeOnLayout(e, 0.4, 0.4)
)

Expand All @@ -131,9 +138,9 @@ private[identicon4s] object Layouts {
final case class Diagonal(a: Shape, b: Shape, c: Shape) extends Layout {

override val shapesOnLayout: Seq[ShapeOnLayout] = Seq(
ShapeOnLayout(a, 0.1, 0.1),
ShapeOnLayout(b, 0.4, 0.4),
ShapeOnLayout(c, 0.6, 0.6)
ShapeOnLayout(a, 0.2, 0.2),
ShapeOnLayout(b, 0.5, 0.5),
ShapeOnLayout(c, 0.8, 0.8)
)

}
Expand Down
36 changes: 29 additions & 7 deletions src/main/scala/net/michalp/identicon4s/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import java.awt.Color
import java.awt.Graphics2D
import java.awt.Polygon
import java.awt.image.BufferedImage
import scala.util.Random

import Layouts.Layout
import Shapes.Shape
Expand All @@ -33,7 +34,7 @@ private[identicon4s] trait Renderer {

private[identicon4s] object Renderer {

def instance = new Renderer {
def instance(config: Identicon.Config, random: Random) = new Renderer {

override def render(layout: Layout): BufferedImage = {
val buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
Expand Down Expand Up @@ -61,17 +62,38 @@ private[identicon4s] object Renderer {
}

private def renderShape(shape: Shape, x: Int, y: Int): GraphicsOp[Unit] = liftOp { g2d =>
g2d.setColor(nextColor)
shape match {
case Shape.Square(edgeSize) =>
g2d.fillRect(x, y, (edgeSize * width).toInt, (edgeSize * height).toInt)
case Shape.Circle(radius) =>
g2d.fillOval(x, y, (radius * width * 2).toInt, (radius * height * 2).toInt)
case Shape.Triangle(edge) =>
val triangle = trianglePolygon((edge * width).toInt, x, y)
case Shape.Square(sizeRatio) =>
val shapeWidth = (sizeRatio * width).toInt
val shapeHeight = (sizeRatio * height).toInt
g2d.fillRect(x - (shapeWidth / 2), y - (shapeHeight / 2), shapeWidth, shapeHeight)
case Shape.Circle(radiusRatio) =>
val radius = (radiusRatio * width).toInt
g2d.fillOval(x - radius, y - radius, radius * 2, radius * 2)
case Shape.Triangle(sizeRatio) =>
val triangle = trianglePolygon((sizeRatio * width).toInt, x, y)
g2d.fillPolygon(triangle)
}

}

private def nextColor =
if (config.renderMonochromatic) Color.black
else
random
.shuffle(
Seq(
Color.red,
Color.green,
Color.blue,
Color.yellow,
Color.black,
Color.cyan
)
)
.head

private val width = 256
private val height = 256
private val squareRootOf3 = math.sqrt(3)
Expand Down

0 comments on commit 9283791

Please sign in to comment.