diff --git a/Readme.md b/Readme.md index 2b0e222..fb5a4d2 100644 --- a/Readme.md +++ b/Readme.md @@ -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"); @@ -24,4 +50,6 @@ ImageIO.write(image, "png", f) Resulting image -![test.png](./images/test.png) \ No newline at end of file +![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`. diff --git a/images/test-color.png b/images/test-color.png new file mode 100644 index 0000000..4807f38 Binary files /dev/null and b/images/test-color.png differ diff --git a/images/test.png b/images/test.png index d90af5a..88c36f3 100644 Binary files a/images/test.png and b/images/test.png differ diff --git a/src/main/scala/net/michalp/identicon4s/Demo.scala b/src/main/scala/net/michalp/identicon4s/Demo.scala index 7e4861d..5e6c56a 100644 --- a/src/main/scala/net/michalp/identicon4s/Demo.scala +++ b/src/main/scala/net/michalp/identicon4s/Demo.scala @@ -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) diff --git a/src/main/scala/net/michalp/identicon4s/Identicon.scala b/src/main/scala/net/michalp/identicon4s/Identicon.scala index ea468c4..d1fa143 100644 --- a/src/main/scala/net/michalp/identicon4s/Identicon.scala +++ b/src/main/scala/net/michalp/identicon4s/Identicon.scala @@ -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 + ) + + } + } diff --git a/src/main/scala/net/michalp/identicon4s/Layouts.scala b/src/main/scala/net/michalp/identicon4s/Layouts.scala index ecff474..68d5f8a 100644 --- a/src/main/scala/net/michalp/identicon4s/Layouts.scala +++ b/src/main/scala/net/michalp/identicon4s/Layouts.scala @@ -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 { @@ -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) ) } @@ -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) ) } @@ -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) ) } @@ -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) ) @@ -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) ) } diff --git a/src/main/scala/net/michalp/identicon4s/Renderer.scala b/src/main/scala/net/michalp/identicon4s/Renderer.scala index a282980..b01e76f 100644 --- a/src/main/scala/net/michalp/identicon4s/Renderer.scala +++ b/src/main/scala/net/michalp/identicon4s/Renderer.scala @@ -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 @@ -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) @@ -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)