Skip to content

Instantly share code, notes, and snippets.

@mashurex
Last active May 28, 2021 23:14
Show Gist options
  • Save mashurex/ee8e8190442daf659d8a0bb69c408376 to your computer and use it in GitHub Desktop.
Save mashurex/ee8e8190442daf659d8a0bb69c408376 to your computer and use it in GitHub Desktop.
SVG avatar generator from sha256 hash values
package com.ashurex
import java.io.FileWriter
import java.math.BigDecimal
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.round
import kotlin.math.sin
import kotlin.math.sqrt
// Kotlin version of hashvatar described at https://francoisbest.com/posts/2021/hashvatars
class AvatarGenerator
fun main(vararg args: String) {
// Replace this with args[0] for input... this is the value to be hashed.
val id = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
// [0.0 - 1.0] The bigger the number the larger the innermost circle is.
val radiusFactor = 0.75
val primaryRadius = 1.0
val radii = (0 until 4).map {
when (it) {
1 -> mix((primaryRadius * sqrt(3.0)) / 2, primaryRadius * 0.75, radiusFactor)
2 -> mix((primaryRadius * sqrt(2.0)) / 2, primaryRadius * 0.5, radiusFactor)
3 -> mix(primaryRadius * 0.5, primaryRadius * 0.25, radiusFactor)
else -> primaryRadius
}
}
val innerRadii = arrayOf(*radii.slice(1..3).toTypedArray(), 0.0)
val outerRadii = arrayOf(*radii.toTypedArray())
// Peel off 2 string chars at a time
val bytes = (0..id.length).mapNotNull { i ->
val next = i + 1
if (Math.floorMod(i, 2) == 0 && next < id.length) {
id.slice(i..next)
} else {
null
}
}.toTypedArray()
val horcruxes = computeCrux(bytes)
val sections = bytes.mapIndexed { index, s ->
val circleIndex = floor(index / 8.0).toInt()
val innerRadius = innerRadii[circleIndex]
val outerRadius = outerRadii[circleIndex]
val crux = horcruxes.ringCruxes[circleIndex]
mapOf(
"section" to sectionPath(index, outerRadius, innerRadius, crux),
"color" to mapValueToColor(s.toInt(16), horcruxes.hashCrux, crux)
)
}
val svgStringBuilder =
StringBuilder("<svg xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" viewBox=\"-1 -1 2 2\">\n<g>\n")
svgStringBuilder.append(sections.joinToString("\n") { s ->
val section = s["section"] as Section
val d = section.path
val fill = s["color"] as String
val strokeColor = "white"
val style = when (section.transform.isBlank()) {
true -> "transition: .15s ease-out;"
else -> "transition: .15s ease-out; transform: ${section.transform};"
}
"""<path d="$d" stroke="$strokeColor" stroke-width="0.02" stroke-linejoin="round" fill="$fill" style="$style"/>"""
})
svgStringBuilder.append("</g>\n</svg>\n")
FileWriter("./output.svg").use {
it.write(svgStringBuilder.toString())
}
}
fun sectionPath(index: Int, outerRadius: Double, innerRadius: Double, crux: Double, stagger: Boolean = true): Section {
val angleA = index / 8.0
val angleB = (index + 1) / 8.0
val angleOffset = when(stagger) {
true -> crux / 8.0
else -> 0.0
}
val path = arrayOf(
Point(0.0, 0.0).moveTo(),
Point.polarPoint(outerRadius, angleA).lineTo(),
Point.polarPoint(outerRadius, angleB).arcTo(outerRadius),
"Z"
).joinToString(separator = " ")
return Section(
path = path,
transform = when (angleOffset.compareTo(0.0) != 0) {
true -> "rotate(${BigDecimal.valueOf(angleOffset).toPlainString()}turn)"
else -> ""
}
)
}
fun reduceCrux(value: Int, byte: String): Int {
return (value xor byte.toInt(16))
}
fun computeCrux(bytes: Array<String>): Cruxes {
val len = round(bytes.size.toDouble() / 4).toInt()
val rings = arrayOf(
bytes.slice(0 until len),
bytes.slice(len until (2 * len)),
bytes.slice((2 * len) until (3 * len)),
bytes.slice((3 * len) until (4 * len))
)
val hashCrux = (bytes.fold(0) { acc, s ->
reduceCrux(acc, s)
}.toDouble() / 0xFF) * 2 - 1
val ringCruxes = rings.map {
(it.fold(0) { acc, s ->
reduceCrux(acc, s)
}.toDouble() / 0xFF) * 2 - 1
}
return Cruxes(hashCrux, ringCruxes)
}
fun mapValueToColor(value: Int, hashCrux: Double, ringCrux: Double): String {
// 4bits
val hue = value shr 4
// 2bits
val saturation = (value shr 2) and 0x03
// 2bits
val lightness = value and 0x03
val h = 360 * hashCrux + 120 * ringCrux + (30 * hue) / 16
// Between 50 - 100%
val s = 50 + (50 * saturation) / 4
// Between 50 - 90%
val l = 50 + (40 * lightness) / 8
return "hsl($h, $s%, $l%)"
}
fun mix(a: Double, b: Double, radiusFactor: Double = 0.42): Double {
return a * radiusFactor + b * (1 - radiusFactor)
}
data class Cruxes(val hashCrux: Double, val ringCruxes: List<Double>)
data class Section(
val path: String,
val transform: String,
)
data class Point(val x: Double, val y: Double) {
fun moveTo(): String {
return "M $this"
}
fun lineTo(): String {
return "L $this"
}
fun arcTo(radius: Number): String {
return "A $radius $radius 0 0 1 $this"
}
override fun toString(): String {
return "$x $y"
}
companion object {
@JvmStatic
fun polarPoint(radius: Double, angle: Double): Point {
// Angle is expressed as [0, 1]
// Subtract pi / 2 to start at 12 o'clock and go clock wise
// Trig rotation + inverted Y = clockwise rotation
val rotation = (2 * PI * angle - PI / 2)
return Point(
x = radius * cos(rotation),
y = radius * sin(rotation)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment