Last active
November 6, 2023 17:41
-
-
Save clragon/9c9f99eca93d3c0c07e7d7e6c50ab49d to your computer and use it in GitHub Desktop.
Character Swapping Animation
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
| import 'dart:math'; | |
| import 'package:flutter/material.dart'; | |
| void main() => runApp(const App()); | |
| class App extends StatelessWidget { | |
| const App({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| theme: ThemeData( | |
| colorScheme: ColorScheme.fromSeed( | |
| seedColor: Colors.deepPurple, | |
| brightness: Brightness.dark, | |
| ), | |
| useMaterial3: true, | |
| ), | |
| home: const Home(), | |
| ); | |
| } | |
| } | |
| class Home extends StatefulWidget { | |
| const Home({super.key}); | |
| @override | |
| State<Home> createState() => _HomeState(); | |
| } | |
| class _HomeState extends State<Home> { | |
| final List<String> fruitsInDifferentLanguages = [ | |
| 'apple', | |
| 'manzana', | |
| 'pomme', | |
| 'apfel', | |
| 'mela', | |
| 'яблоко', | |
| '蘋果', | |
| 'りんご', | |
| '사과', | |
| 'maçã', | |
| 'elma', | |
| 'تفاح', | |
| 'सेब', | |
| 'แอปเปิ้ล', | |
| 'táo', | |
| 'apel', | |
| 'frukt', | |
| 'fruktas', | |
| 'frukto', | |
| 'fruta', | |
| 'ผลไม้', | |
| ]; | |
| int currentFruitIndex = 0; | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| body: Center( | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| SwappingText( | |
| text: fruitsInDifferentLanguages[currentFruitIndex], | |
| style: Theme.of(context).textTheme.displayLarge, | |
| ), | |
| const SizedBox(height: 32), | |
| ElevatedButton( | |
| onPressed: () { | |
| setState(() { | |
| while (true) { | |
| final next = | |
| Random().nextInt(fruitsInDifferentLanguages.length); | |
| if (next != currentFruitIndex) { | |
| currentFruitIndex = next; | |
| break; | |
| } | |
| } | |
| }); | |
| }, | |
| child: const Text('Swap'), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class CharacterAnimator { | |
| CharacterAnimator(this.source, this.target) | |
| : random = Random(generateSeed(target)); | |
| final String source; | |
| final String target; | |
| final Random random; | |
| static int generateSeed(String text) => | |
| text.codeUnits.fold(0, (prev, elem) => prev + elem); | |
| List<int> determineSwaps() { | |
| List<int> swaps = []; | |
| int maxLength = max(source.length, target.length); | |
| for (int i = 0; i < maxLength; i++) { | |
| if (i >= source.length || i >= target.length || source[i] != target[i]) { | |
| swaps.add(i); | |
| } | |
| } | |
| return swaps; | |
| } | |
| final List<MapEntry<int, int>> printableRanges = [ | |
| const MapEntry(0x0020, 0x007E), // Basic Latin | |
| const MapEntry(0x00A0, 0x00FF), // Latin-1 Supplement | |
| const MapEntry(0x0100, 0x017F), // Latin Extended-A | |
| const MapEntry(0x0180, 0x024F), // Latin Extended-B | |
| const MapEntry(0x0250, 0x02AF), // IPA Extensions | |
| const MapEntry(0x02B0, 0x02FF), // Spacing Modifier Letters | |
| const MapEntry(0x0300, 0x036F), // Combining Diacritical Marks | |
| const MapEntry(0x0370, 0x03FF), // Greek and Coptic | |
| const MapEntry(0x0400, 0x04FF), // Cyrillic | |
| const MapEntry(0x0500, 0x052F), // Cyrillic Supplement | |
| const MapEntry(0x0530, 0x058F), // Armenian | |
| const MapEntry(0x0590, 0x05FF), // Hebrew | |
| const MapEntry(0x0600, 0x06FF), // Arabic | |
| const MapEntry(0x0700, 0x074F), // Syriac | |
| const MapEntry(0x0750, 0x077F), // Arabic Supplement | |
| const MapEntry(0x0780, 0x07BF), // Thaana | |
| const MapEntry(0x3040, 0x309F), // Hiragana | |
| const MapEntry(0x30A0, 0x30FF), // Katakana | |
| const MapEntry(0x4E00, 0x9FFF), // CJK Unified Ideographs | |
| const MapEntry(0xAC00, 0xD7AF), // Hangul Syllables | |
| ]; | |
| String interpolateCharacters(int index, double progress) { | |
| int startVal; | |
| int endVal; | |
| if (index >= source.length) { | |
| if (progress == 0.0) { | |
| startVal = getDefaultCharacterCloseToSource(); | |
| } else { | |
| startVal = getDefaultCharacterCloseToSource(index); | |
| } | |
| endVal = target.codeUnitAt(index); | |
| } else if (index >= target.length) { | |
| return " "; | |
| } else { | |
| startVal = source.codeUnitAt(index); | |
| endVal = target.codeUnitAt(index); | |
| } | |
| int interpolatedVal = startVal + ((endVal - startVal) * progress).round(); | |
| interpolatedVal = getClosestPrintableCharacter(interpolatedVal); | |
| return String.fromCharCode(interpolatedVal); | |
| } | |
| int getDefaultCharacterCloseToSource([int? index]) { | |
| int avgCharCode = source.isNotEmpty | |
| ? source.codeUnits.reduce((a, b) => a + b) ~/ source.length | |
| : getRandomPrintableCharacter(); | |
| return getClosestPrintableCharacter(avgCharCode + | |
| (index != null ? (random.nextInt(3) - 1) * (index % 10) : 0)); | |
| } | |
| int getRandomPrintableCharacter() { | |
| int randomRangeIndex = random.nextInt(printableRanges.length); | |
| final range = printableRanges[randomRangeIndex]; | |
| return random.nextInt(range.value - range.key + 1) + range.key; | |
| } | |
| int getClosestPrintableCharacter(int codeUnit) { | |
| for (final range in printableRanges) { | |
| if (codeUnit >= range.key && codeUnit <= range.value) { | |
| return codeUnit; | |
| } | |
| } | |
| List<int> distances = []; | |
| for (final range in printableRanges) { | |
| if (codeUnit < range.key) { | |
| distances.add(range.key - codeUnit); | |
| } else if (codeUnit > range.value) { | |
| distances.add(codeUnit - range.value); | |
| } | |
| } | |
| distances.sort(); | |
| int closestDistance = distances.first; | |
| return codeUnit + closestDistance; | |
| } | |
| String animateText(double progress) { | |
| List<int> swaps = determineSwaps(); | |
| StringBuffer animatedText = StringBuffer(); | |
| for (int i = 0; i < swaps.length; i++) { | |
| animatedText.write(interpolateCharacters(swaps[i], progress)); | |
| } | |
| return animatedText.toString(); | |
| } | |
| } | |
| class SwapStringTween extends Tween<String> { | |
| SwapStringTween({ | |
| String? begin, | |
| required String end, | |
| }) : super(begin: begin, end: end); | |
| @override | |
| String lerp(double t) { | |
| String beginText = begin ?? ''; | |
| String endText = end ?? ''; | |
| CharacterAnimator animator = CharacterAnimator( | |
| beginText, | |
| endText, | |
| ); | |
| return animator.animateText(t); | |
| } | |
| } | |
| class SwappingText extends StatelessWidget { | |
| const SwappingText({ | |
| super.key, | |
| required this.text, | |
| this.duration = const Duration(milliseconds: 200), | |
| this.style, | |
| }); | |
| final String text; | |
| final Duration duration; | |
| final TextStyle? style; | |
| @override | |
| Widget build(BuildContext context) { | |
| return TweenAnimationBuilder( | |
| duration: duration, | |
| tween: SwapStringTween( | |
| begin: text, | |
| end: text, | |
| ), | |
| builder: (context, text, child) => Text( | |
| text, | |
| style: style, | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment