Skip to content

Instantly share code, notes, and snippets.

@unixzii
Created June 29, 2022 09:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save unixzii/df8b454a40a000b29dcdc917ef45341b to your computer and use it in GitHub Desktop.
Save unixzii/df8b454a40a000b29dcdc917ef45341b to your computer and use it in GitHub Desktop.
Matrix live in your app!
//
// HackingTextTransition.swift
// Copyright (c) 2022 Cyandev<unixzii@gmail.com>.
//
import Foundation
import DequeModule
class HackingTextTransition {
private struct _IndexState {
@usableFromInline
var index: Array.Index
@usableFromInline
var step: Int = 0
@inlinable
@inline(__always)
init(_ index: Array.Index) {
self.index = index
}
@inlinable
@inline(__always)
mutating func advance() -> (Array.Index, Int) {
step += 1
return (index, step)
}
}
let fromString: String
let toString: String
let transitionStepsPerCharacter: Int = 10
var currentString: String {
return String(_currentString)
}
private let _toStringPadded: [Character]
private var _currentString: [Character]
private var _indexWindow: Deque<_IndexState>
init(from fromString: String, to toString: String) {
self.fromString = fromString
self.toString = toString
// Workaround:
// There is a bug that when the string contains Emoji characters,
// `padding(toLength:withPad:startingAt:)` will behave incorrectly,
// losing the trailing characters. We use the count from `UTF16View`
// to ensure that no characters will be lost.
let maxLength = max(fromString.utf16.count, toString.utf16.count)
_toStringPadded = Array(toString.padding(toLength: maxLength, withPad: " ", startingAt: 0))
// The initial transition string is the same as the `fromString` but
// is padded to the length of the longest string between `fromString`
// and `toString` with whitespaces.
_currentString = Array(fromString.padding(toLength: maxLength, withPad: " ", startingAt: 0))
_indexWindow = .init(minimumCapacity: transitionStepsPerCharacter)
}
func advance() -> Bool {
if _indexWindow.count >= transitionStepsPerCharacter {
let _ = _indexWindow.popFirst()
}
let index: (Array.Index) = {
if let lastIndex = _indexWindow.last?.index {
return lastIndex + 1
}
return 0
}()
if index < _currentString.count {
_indexWindow.append(.init(index))
}
var canAdvance = false
for i in 0..<_indexWindow.count {
let (index, step) = _indexWindow[i].advance()
if step > transitionStepsPerCharacter {
continue
}
canAdvance = true
_currentString[index] = interpolatedCharacter(
for: _currentString[index],
to: index < _toStringPadded.count ? _toStringPadded[index] : " ",
step: step,
totalSteps: transitionStepsPerCharacter
)
}
return canAdvance
}
}
fileprivate let magicCharacterPool: [Character] = ["!", "@", "#", "$", "%", "&", "*", "+", ".", "-", " "]
fileprivate func interpolatedCharacter(
for character: Character,
to toCharacter: Character,
step: Int,
totalSteps: Int
) -> Character {
if step == totalSteps {
return toCharacter
}
// We don't support Unicode grapheme currently.
guard character.unicodeScalars.count == 1 &&
toCharacter.unicodeScalars.count == 1 else {
return magicCharacterPool.randomElement()!
}
// Assuming that there are 5 transition steps.
// A -> F:
// B (forward offsetted based on Unicode scalar)
// * (magic character)
// # (magic character)
// E (backward offsetted based on Unicode scalar)
// F (the final state)
let phaseBoundary1 = totalSteps / 4
let phaseBoundary2 = totalSteps - phaseBoundary1
if step > phaseBoundary1 && step < phaseBoundary2 {
return magicCharacterPool.randomElement()!
}
let (offset, unicodeScalar): (UInt32, UnicodeScalar) = {
if step <= phaseBoundary1 {
return (UInt32(step), character.unicodeScalars.first!)
} else {
return (UInt32(totalSteps - step), toCharacter.unicodeScalars.first!)
}
}()
guard let newUnicodeScalar = UnicodeScalar(unicodeScalar.value + offset) else {
return magicCharacterPool.randomElement()!
}
return Character(newUnicodeScalar)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment