Last active
May 28, 2021 23:14
-
-
Save mashurex/ee8e8190442daf659d8a0bb69c408376 to your computer and use it in GitHub Desktop.
SVG avatar generator from sha256 hash values
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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