From 9283791ab34d152012cb27a3e0ae3c5386a45a42 Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Sun, 10 Apr 2022 17:01:10 +0200 Subject: [PATCH] adjust layouts, introduce shape coloring, make identicon configurable, extend the documentation --- Readme.md | 34 +++++++++++-- images/test-color.png | Bin 0 -> 3967 bytes images/test.png | Bin 2360 -> 2629 bytes .../scala/net/michalp/identicon4s/Demo.scala | 2 +- .../net/michalp/identicon4s/Identicon.scala | 30 +++++++++-- .../net/michalp/identicon4s/Layouts.scala | 47 ++++++++++-------- .../net/michalp/identicon4s/Renderer.scala | 36 +++++++++++--- 7 files changed, 113 insertions(+), 36 deletions(-) create mode 100644 images/test-color.png 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 0000000000000000000000000000000000000000..4807f38a32f2b3511d1d4b48514507308585d74f GIT binary patch literal 3967 zcma)#Tsb6wxRKF@Ps@6Y>wJ@@DNJlFjsowl>&Y>@y=&)k zl^zaf<)qDjs^0l3NP8JMYAQ&xn`aYjhRfb=(b)zNss}mbP-!NBpnwgQME5d*6J*I- z0NU{XFY(<1bnJ2qb$3EEhk@LKuzg-lf;{&b^SASmMk+V8%-%n5$SGb`$q;PgryIXz zbLF&nKaqPirac>yZZ1koEl-3bvH@v#zeGZ!vs4NEb*~UhSO-pP#4+&}E+8$f(*}YR zQ6os5xsMpaIwzW*WX0dLtr5gEJdRm)J1JkJ4UfUQosxG$cRC#}7qrz!l;=2-3v&b^ z_5(*9wzjt^Moz`?UT#i8zs2nhQk`UDte9iAZjbqMi`!aoYxR5tRvl#F;pgjr*}^zA zxY7JczyR;@>L+*PQ4OX*V~G+TRWhVQJ``IjPeQZAOZn;&h)&uU!zE`9W%n6WbIX#g zN{m-SeXftqY$(;+-7XacE0R%R?wY#D;0<9{l@n*U4-2Do+pF^mp!(rSRU##-@jP73 zQj=$2+ZKRcz=pX_i{g=rqa0Ud=diX!4~5U{s>u}Sy`{Kj0W?YZ6F4)3Rn+2|l+`!< zctY#tEOc(40ZXz%OPb;ih2BkWc-3lsu@GP(M26qHb?vK{fBr1_2L?^+yhYKiFW8SEasSa)0>9s<&mnzvezOPMSD*0jlc_`3w$2<((c*rm=N55bd8X_jNl-Iq%Fct_ zvR4V62Z7aV>&LD1#Un-6{&24lDE{G#NSy`Amn)pC!^P+jtnx({Ph(b1WwJVHF9EH5+zKb>#4=volW8{wv3 z-TCP1lsh-OxfbGOM(lA{+~vRX@c7W)q)yu6`}SRM$MP|vdB^UEaMWb%nb{M0;~8U9 zDs%4~a&wE9raWM+aeN`bCVvTe5h8#T(15dk{qPZ3!*p~ih4Z?W4ga{9>?o?O$I1)) zLiydf?A+qH6(IX|zI^eUH0k;YbemS!2^Vo&AB)MzeB3(6cCEhcOV3FR9b^80AKpsz zA5&jj6WNtEd6$spcX(;`#Ry8TzZH>P$){8f(l8yHdesf$sad(&9Fr_u3QpdX^^1fS zrOz^!9ZYUNMG6F+pd4S_JMy~!C8_Rui^*zcr#p>dnU}NIJ1;;9Smztb>N!eGdZ92G zU2uP4e~LPDo?Jk?^JRz1&<%?dpm1dX|Dz|r5Y-ZYQ(EAyXtkcgN-%R=$Py7<%2Tn4 zLPM*w5G(dGS+^FTS4akI-_~Fzx4;wFC*O!*9zU6R$JLa zPYJyVsTuM8Bs?N-q5ZMdyP(h-ksvyF{!=nC6dWV%07Dn5H;w*J!c6rhd@FaT-A_k9 z8A+n=FDGT{r{qc;i(z@uGj*rt(8wx`)uru{R4EZMuTtdAQIPRwKOR}$Wc&Mz9pj>u4AGv1uI%ElWa&VY z+GKHL!Nu^+2&2QYN0T$+g!bn(LuY*&quesjNr}1Zw_E}x{*mWOFK`vy_3hIBc+=!{Bm$^827R@$ycvcvcrOy@ zpoK`UG?w)}K(8ykpbvdYh4k{(96BphhRm{AiJBTo8sAM!3zR!`tt=qMnczRJNNStX z4W&V?Yw>(6t9(K7zY^}ev?KeMS+8Fkj3`0i&dcgakaf_CI0Q{f5Z1}1NM5R*J04qi8>CoPEAjZU+E(@ zwgAbP`iiAkW0>ah>Kgu6`7_s8^1`zDFHPT1MP9I(4SV&s+585J-dw%DZD-T(-^Pq<1WAN#2*9}lV=Mn`O*TQFfHcalK>BCd4PEpT=; zZb(JN9H}g!g_YSrX}zlU?8~*(1Yqi)T~^H5;p>IZQcaJ6O>v_WoWjc_b0b%%}k=m%6pXF)<8M_HJ2#0cU{ z<&#L{VPBM*VdtaM4+i>OPRolz&kd=mT{se&=9tfyyVfCBG=I9Iqe6tEQlq*!|3>1( zBmvu%*8Z(4PV|+7v%3lAL+-Hs8$-e}(%X>=kILzC)FZ2l|5AI55wP{mLNfT{SAN4! zd!A5PKAh;k)?u4@~OO}ubm9>1ww=k7*OgsLOn0vJ2SnAX-*nMiw)wo(fl?`N#vc;DfseDVr@ z3e}lH&&{_OFN4hpUNl<5c&kV`)HEfJ@Of_(V)H2~!wUs2gX!X`|mFbm`r z&DU=(ZBQceM;OPvg`e*Dbwdgdq$#$rp@r-Rieu5ZUWjh)VBE^(I1i$M@+$iKl2`>A z#{toHdCv@bZ)Po{J3}cnj zzo1kGn}wuo52W)T@6|@P6^d#Y`~+0I@;Fd4=_BKrS00fe(@nlUxEr-|3Dfvi>@%}Y z_=>EV?6+o>|2TXxSZ~Bd?=p0id*XV2KwMi?)SpGayOK3hb=#P2V`Wp^)34*(W(IEm zWvxB^*`>cUGran=sj3j=I#-s6-{z-QG}{L*(pS9=bV5B7wacsPGadKuJ?57YLW}Y`{6O~);tj>A3Ee*neMa|m zN__>5P|=!89g(Z2m|H3ofAf>W%h|4UHJ=Tzq_jU% zB_i@18V7>t3pKv&V$_3<24fwCAJM67x)Ea zn8y!S(U0Tj&(0x?D$Q$2Su>jwtOV&uONQm)VwCSXGzh{+AI-DJ=@CHni_Z})_M&VE zkYNGb`*P4x@qU!=J$H}ud|0gyk>MMHE^$_T&4(EVNtO5^%wMPFA+JPr5TW)ZEfz8? sEB$6mgi}-S-|wvdb|4?P`R=jJ_WS|N?D8QqFV+An3p-fNNkr^_0Uy-B82|tP literal 0 HcmV?d00001 diff --git a/images/test.png b/images/test.png index d90af5a93f58a9277b98fdf2c0c6d6c7f2e96f08..88c36f3511f4c97084db27ba5b7beae286336498 100644 GIT binary patch literal 2629 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 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)