-
Notifications
You must be signed in to change notification settings - Fork 168
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: ios: setup PolkadotIdenticon package * feat: ios: migrating part of Circle code * feat: ios: public key formats conversion * feat: ios: public key decoding logic * feat: ios: clean up, all necessary models, layout generation * feat: ios: Blake2 based colors generator * Update ios/Packages/PolkadotIdenticon/README.md Co-authored-by: Pavel Rybalko <[email protected]> * feat: ios: generate scalable dot identicons that reflect by-pixel rendering of Rust library * feat: ios: Final icon rendering as UIImage * feat: ios: SwiftUI wrapper and test previews * feat: ios: update SPM --------- Co-authored-by: Pavel Rybalko <[email protected]>
- Loading branch information
Showing
17 changed files
with
922 additions
and
4 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/config/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// swift-tools-version: 5.7 | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "PolkadotIdenticon", | ||
platforms: [ | ||
.iOS(.v15) | ||
], | ||
products: [ | ||
.library( | ||
name: "PolkadotIdenticon", | ||
targets: ["PolkadotIdenticon"] | ||
) | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/tesseract-one/Blake2.swift.git", from: "0.1.0"), | ||
.package(url: "https://github.com/attaswift/BigInt.git", from: "5.3.0") | ||
], | ||
targets: [ | ||
.target( | ||
name: "PolkadotIdenticon", | ||
dependencies: [ | ||
.product(name: "Blake2", package: "Blake2.swift"), | ||
.product(name: "BigInt", package: "BigInt") | ||
] | ||
), | ||
.testTarget( | ||
name: "PolkadotIdenticonTests", | ||
dependencies: ["PolkadotIdenticon"] | ||
) | ||
] | ||
) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Polkadot Identicon | ||
|
||
A description of this package. |
81 changes: 81 additions & 0 deletions
81
ios/Packages/PolkadotIdenticon/Sources/PolkadotIdenticon/Helpers/CircleLayoutGenerator.swift
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// | ||
// CircleLayoutGenerator.swift | ||
// | ||
// | ||
// Created by Krzysztof Rodak on 24/07/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
public final class CircleLayoutGenerator { | ||
public init() {} | ||
/// Returns an array of circles with assigned positions, radii and colors. | ||
/// The circles are positioned in a radial layout and each is assigned a color from the provided array. | ||
/// - Parameters: | ||
/// - distanceBetweenCenters: The distance between the centers of adjacent circles. | ||
/// - circleRadius: The radius of each circle. | ||
/// - colors: An array of colors to be assigned to the circles. The colors are assigned in the order they appear | ||
/// in the array. | ||
/// - Returns: An array of circles with assigned positions, radii and colors. | ||
func generateCircles(distanceBetweenCenters: Float, circleRadius: Float, colors: [Color]) -> [Circle] { | ||
calculateCirclePositions(distanceBetweenCenters: distanceBetweenCenters) | ||
.enumerated() | ||
.map { index, position in Circle( | ||
position: .init(centerX: position.centerX, centerY: position.centerY), | ||
radius: circleRadius, | ||
rgba_color: colors[index] | ||
) } | ||
} | ||
} | ||
|
||
private extension CircleLayoutGenerator { | ||
/// Calculates the positions for the centers of the circles in a radial layout. | ||
/// | ||
/// The layout is as follows: | ||
/// | ||
/// 0 | ||
/// | ||
/// 2 17 | ||
/// | ||
/// 3 1 15 | ||
/// | ||
/// 4 16 | ||
/// | ||
/// 5 18 14 | ||
/// | ||
/// 7 13 | ||
/// | ||
/// 6 10 12 | ||
/// | ||
/// 8 11 | ||
/// | ||
/// 9 | ||
/// | ||
/// - Parameter distanceBetweenCenters: The distance between the centers of adjacent circles. | ||
/// - Returns: An array of positions for the circle centers. | ||
func calculateCirclePositions(distanceBetweenCenters: Float) -> [CirclePosition] { | ||
let a = distanceBetweenCenters | ||
let b = distanceBetweenCenters * sqrt(3) / 2 | ||
return [ | ||
CirclePosition(centerX: 0, centerY: -2 * a), | ||
CirclePosition(centerX: 0, centerY: -a), | ||
CirclePosition(centerX: -b, centerY: -3 * a / 2), | ||
CirclePosition(centerX: -2 * b, centerY: -a), | ||
CirclePosition(centerX: -b, centerY: -a / 2), | ||
CirclePosition(centerX: -2 * b, centerY: 0), | ||
CirclePosition(centerX: -2 * b, centerY: a), | ||
CirclePosition(centerX: -b, centerY: a / 2), | ||
CirclePosition(centerX: -b, centerY: 3 * a / 2), | ||
CirclePosition(centerX: 0, centerY: 2 * a), | ||
CirclePosition(centerX: 0, centerY: a), | ||
CirclePosition(centerX: b, centerY: 3 * a / 2), | ||
CirclePosition(centerX: 2 * b, centerY: a), | ||
CirclePosition(centerX: b, centerY: a / 2), | ||
CirclePosition(centerX: 2 * b, centerY: 0), | ||
CirclePosition(centerX: 2 * b, centerY: -a), | ||
CirclePosition(centerX: b, centerY: -a / 2), | ||
CirclePosition(centerX: b, centerY: -3 * a / 2), | ||
CirclePosition(centerX: 0, centerY: 0) | ||
] | ||
} | ||
} |
194 changes: 194 additions & 0 deletions
194
...ckages/PolkadotIdenticon/Sources/PolkadotIdenticon/Helpers/IdenticonColorsGenerator.swift
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
// | ||
// IdenticonColorsGenerator.swift | ||
// | ||
// | ||
// Created by Krzysztof Rodak on 31/07/2023. | ||
// | ||
|
||
import Blake2 | ||
import Foundation | ||
|
||
public final class IdenticonColorsGenerator { | ||
private enum Constants { | ||
static let byteHashLength = 64 | ||
static let arrayZeroBytesLength = 32 | ||
static let derivedIDRotationFactorMultiplier: UInt8 = 6 | ||
static let derivedIDRotationFactorModulo: UInt8 = 3 | ||
static let hueDegrees = 360 | ||
static let colorArrayLength = 19 | ||
static let lightnessPercentages = [53, 15, 35, 75] | ||
} | ||
|
||
public init() {} | ||
|
||
/// Returns an array of 19 colors based on the input data. | ||
/// | ||
/// The function first hashes the input data and a zero-filled array with the Blake2b hashing algorithm. | ||
/// It then derives a unique ID by subtracting the hashed zero-filled array from the hashed input data. | ||
/// The ID is used to calculate a saturation component and derive a palette of 19 colors. | ||
/// Finally, it selects a color scheme based on the derived ID and rotates the colors in the scheme. | ||
/// | ||
/// - Parameter inputData: A byte array to derive the colors from. | ||
/// - Returns: An array of 19 derived colors. | ||
func deriveColors(from inputData: [UInt8]) -> [Color] { | ||
let zeroBytes = Array(repeating: UInt8(0), count: Constants.arrayZeroBytesLength) | ||
|
||
guard let hashedInput = try? Blake2.hash(.b2b, size: Constants.byteHashLength, bytes: inputData), | ||
let hashedZeroBytes = try? Blake2.hash(.b2b, size: Constants.byteHashLength, bytes: zeroBytes) else { | ||
return [] | ||
} | ||
|
||
// Create an ID array by subtracting elements of hashedInput and zeroBytes. | ||
var derivedID: [UInt8] = [] | ||
for (index, byte) in hashedInput.enumerated() { | ||
let newValue = byte &- hashedZeroBytes[index] | ||
derivedID.append(newValue) | ||
} | ||
|
||
let colorPalette = deriveColors(derivedID: derivedID) | ||
|
||
// Choose the color scheme based on the 30th and 31st byte of the derived ID. | ||
let colorSchemes = ColorScheme.defaultColorSchemes | ||
let totalFrequency: Int = colorSchemes.reduce(0) { $0 + $1.frequency } | ||
let selectionFactor = (UInt(derivedID[30]) + UInt(derivedID[31]) * 256) % UInt(totalFrequency) | ||
let selectedScheme = chooseScheme(schemes: colorSchemes, selectionFactor: selectionFactor) | ||
|
||
// Calculate rotation factor for the color scheme. | ||
let rotationFactor = (derivedID[28] % Constants.derivedIDRotationFactorMultiplier) * Constants | ||
.derivedIDRotationFactorModulo | ||
|
||
// Generate the final array of colors using selected color scheme and rotation factor. | ||
return (0 ..< Constants.colorArrayLength).map { index in | ||
let colorIndex = index < Constants | ||
.colorArrayLength - 1 ? (index + Int(rotationFactor)) % (Constants.colorArrayLength - 1) : Constants | ||
.colorArrayLength - 1 | ||
let paletteIndex = selectedScheme.colorPaletteIndices[colorIndex] | ||
return colorPalette[paletteIndex] | ||
} | ||
} | ||
|
||
private func deriveColors(derivedID: [UInt8]) -> [Color] { | ||
// Calculate saturation component using 29th byte of the derived ID. | ||
let sat = UInt8((((UInt(derivedID[29]) * 70) / 256 + 26) % 80) + 30) | ||
let saturationComponent: Double = Double(sat) / 100.0 | ||
|
||
// Generate palette of colors using derived ID and saturation component. | ||
return derivedID | ||
.enumerated() | ||
.map { index, byte in | ||
let byteColor = byte &+ (UInt8(index % 28) &* 58) | ||
switch byteColor { | ||
case 0: | ||
return Color(red: 4, green: 4, blue: 4, alpha: 255) | ||
case 255: | ||
return Color.foregroundColor | ||
default: | ||
return derive(fromByte: byteColor, saturationComponent: saturationComponent) | ||
} | ||
} | ||
} | ||
|
||
/// Derives a color from a byte and a saturation component. | ||
/// | ||
/// - Parameters: | ||
/// - byte: The byte to derive the color from. | ||
/// - saturationComponent: The saturation component to use. | ||
/// - Returns: The derived color. | ||
private func derive(fromByte byte: UInt8, saturationComponent: Double) -> Color { | ||
// HSL color hue in degrees | ||
let hueModulus = 64 | ||
let hue = Int(byte % UInt8(hueModulus)) * Constants.hueDegrees / hueModulus | ||
// HSL lightness in percents | ||
let lightnessIndexFactor: UInt8 = 64 | ||
let lightnessIndex = byte / lightnessIndexFactor | ||
let lightnessPercentage = lightnessIndex < Constants.lightnessPercentages.count ? Constants | ||
.lightnessPercentages[Int(lightnessIndex)] : 0 | ||
let lightnessComponent: Double = Double(lightnessPercentage) / 100.0 | ||
let (red, green, blue) = hslToRgb( | ||
hue: Double(hue), | ||
saturation: saturationComponent, | ||
lightness: lightnessComponent | ||
) | ||
|
||
return Color(red: red, green: green, blue: blue, alpha: 255) | ||
} | ||
|
||
/// Choose a color scheme based on a selection factor. | ||
/// | ||
/// - Parameters: | ||
/// - schemes: An array of color schemes. | ||
/// - selectionFactor: A selection factor to determine which color scheme to use. | ||
/// - Returns: The selected color scheme. | ||
private func chooseScheme(schemes: [ColorScheme], selectionFactor: UInt) -> ColorScheme { | ||
var sum: UInt = 0 | ||
var foundScheme: ColorScheme? | ||
for scheme in schemes { | ||
sum += UInt(scheme.frequency) | ||
if selectionFactor < sum { | ||
foundScheme = scheme | ||
break | ||
} | ||
} | ||
return foundScheme! | ||
} | ||
|
||
/// Converts HSL color space values to RGB color space values. | ||
/// | ||
/// - Parameters: | ||
/// - hue: The hue value of the HSL color, specified as a degree between 0 and 360. | ||
/// - saturation: The saturation value of the HSL color, specified as a double between 0 and 1. | ||
/// - lightness: The lightness value of the HSL color, specified as a double between 0 and 1. | ||
/// - Returns: A tuple representing the RGB color values, each a UInt8 between 0 and 255. | ||
private func hslToRgb( | ||
hue: Double, | ||
saturation: Double, | ||
lightness: Double | ||
) -> (red: UInt8, green: UInt8, blue: UInt8) { | ||
var redComponent: Double = 0.0 | ||
var greenComponent: Double = 0.0 | ||
var blueComponent: Double = 0.0 | ||
|
||
let normalizedHue = hue / 360.0 | ||
|
||
if saturation == 0.0 { | ||
// Achromatic color (gray scale) | ||
redComponent = lightness | ||
greenComponent = lightness | ||
blueComponent = lightness | ||
} else { | ||
let qValue = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * | ||
saturation | ||
let pValue = 2 * lightness - qValue | ||
|
||
redComponent = convertHueToRgbComponent(p: pValue, q: qValue, hueShift: normalizedHue + 1 / 3) | ||
greenComponent = convertHueToRgbComponent(p: pValue, q: qValue, hueShift: normalizedHue) | ||
blueComponent = convertHueToRgbComponent(p: pValue, q: qValue, hueShift: normalizedHue - 1 / 3) | ||
} | ||
|
||
return ( | ||
red: UInt8(max(min(floor(redComponent * 256), 255), 0)), | ||
green: UInt8(max(min(floor(greenComponent * 256), 255), 0)), | ||
blue: UInt8(max(min(floor(blueComponent * 256), 255), 0)) | ||
) | ||
} | ||
|
||
/// Calculates a single RGB color component from HSL values. | ||
/// | ||
/// - Parameters: | ||
/// - p: The first helper value derived from the lightness value of the HSL color. | ||
/// - q: The second helper value derived from the lightness and saturation values of the HSL color. | ||
/// - hueShift: The hue value of the HSL color, shifted by a certain amount. | ||
/// - Returns: A double representing the calculated RGB color component. | ||
private func convertHueToRgbComponent(p: Double, q: Double, hueShift: Double) -> Double { | ||
var shiftedHue = hueShift | ||
|
||
if shiftedHue < 0 { shiftedHue += 1 } | ||
if shiftedHue > 1 { shiftedHue -= 1 } | ||
|
||
if shiftedHue < 1 / 6 { return p + (q - p) * 6 * shiftedHue } | ||
if shiftedHue < 1 / 2 { return q } | ||
if shiftedHue < 2 / 3 { return p + (q - p) * (2 / 3 - shiftedHue) * 6 } | ||
|
||
return p | ||
} | ||
} |
Oops, something went wrong.