├── .env.sample ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── GENERATING_FONTS.md ├── LICENSE ├── README.md ├── assets ├── 555-day-204.webp ├── 8-bit-rocky.pdf └── rocky.wav ├── foundry.toml ├── metadata ├── fonts │ ├── base64-encode-font.py │ ├── condense-font.sh │ ├── generate-glyphs.py │ ├── input │ │ ├── FiraCode-Medium.ttf │ │ ├── FiraCode-Regular.ttf │ │ └── Inter-Medium.ttf │ └── output │ │ ├── FiraCode-Medium-Subset.ttf │ │ ├── FiraCode-Medium-Subset.txt │ │ ├── FiraCode-Medium-Subset.woff2 │ │ ├── FiraCode-Regular-Subset.ttf │ │ ├── FiraCode-Regular-Subset.txt │ │ ├── FiraCode-Regular-Subset.woff2 │ │ ├── Inter-Medium-Subset.ttf │ │ ├── Inter-Medium-Subset.txt │ │ ├── Inter-Medium-Subset.woff2 │ │ ├── inter-medium-glyphs.txt │ │ ├── medium-glyphs.txt │ │ └── regular-glyphs.txt ├── generate-location-data.py └── generate-running-data.py ├── script ├── GenerateAudioOutput.s.sol ├── GenerateJSONOutput.s.sol └── PrintData.s.sol ├── src ├── FiveFiveFive.sol ├── interfaces │ └── IFiveFiveFive.sol └── utils │ ├── FiveFiveFiveArt.sol │ ├── FiveFiveFiveAudio.sol │ ├── FiveFiveFiveConstants.sol │ └── FiveFiveFiveData.sol └── test └── FiveFiveFive.t.sol /.env.sample: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # RPC URLs 3 | ################################################################################ 4 | 5 | RPC_URL_BASE_MAINNET="" 6 | RPC_URL_BASE_SEPOLIA="" 7 | 8 | ################################################################################ 9 | # API Keys 10 | ################################################################################ 11 | 12 | BASESCAN_API_KEY="" 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | # Preview 17 | script/preview/ 18 | 19 | # Script output files 20 | /output/svg/** 21 | /output/txt/** 22 | /output/wav/** 23 | 24 | # Mac 25 | .DS_STORE 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/transmissions11/solmate 7 | [submodule "lib/solady"] 8 | path = lib/solady 9 | url = https://github.com/vectorized/solady 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.inlineSuggest.enabled": true, 4 | "editor.rulers": [80, 100], 5 | "editor.tabSize": 4, 6 | "typescript.updateImportsOnFileMove.enabled": "always" 7 | } 8 | -------------------------------------------------------------------------------- /GENERATING_FONTS.md: -------------------------------------------------------------------------------- 1 | # Generating Fonts 2 | 3 | > [!NOTE] 4 | > This was adapted from [**this Gist**](https://gist.github.com/fiveoutofnine/48d7f344433bba862c4151a7f3cf318f/). 5 | 6 | This file is a step-by-step guide to generating [**optimized font files**](https://github.com/fiveoutofnine/555/blob/c4ef5ab0e110895105a061fd6bc0b174b776dabb/src/utils/FiveFiveFiveConstants.sol#L16). 7 | 8 | ### 1. Add font files 9 | 10 | Add the starting font files to `./metadata/fonts/input/*`. They must be static (i.e. non-variable). 11 | 12 | ### 2. Generating glyphs 13 | 14 | Most of the size reduction comes from removing unused glyphs from the font file. To generate and write a set of unicode characters to `./metadata/fonts/output/*.txt` that we'll read from later when generating the subset font file, run the following command: 15 | 16 | ```py 17 | python3 ./metadata/fonts/generate-glyphs.py 18 | ``` 19 | 20 | > [!NOTE] 21 | > This command must be run from the root of the project (or you can update the base path accordingly). 22 | 23 | > [!TIP] 24 | > Change the font file names/formats and characters to meet your need. 25 | 26 | ### 3. Generating the subset font files 27 | 28 | To generate and write the subset font files to `./metadata/fonts/output/*.-Subset.ttf`, run the following command: 29 | 30 | ```sh 31 | source ./metadata/fonts/condense-font.sh 32 | ``` 33 | 34 | > [!NOTE] 35 | > This command must be run from the root of the project (or you can update the base path accordingly). 36 | 37 | ### 4. `ttf` to `woff2` conversion 38 | 39 | Next, we can (usually) get a bit more savings by convering the font files to `woff2` format. I like to use [**this**](https://cloudconvert.com/ttf-to-woff2) online converter. 40 | 41 | > [!TIP] 42 | > In addition to generally being smaller in size, the `woff2` format has [**pretty good browser support**](https://caniuse.com/?search=woff2) as of 2024. 43 | 44 | ### 5. Base64 encoding 45 | 46 | To base64 encode the font files and write the result to `./metadata/fonts/output/*-Subset.txt`, you can run the following command: 47 | 48 | ```sh 49 | python3 ./metadata/fonts/base64-encode-font.py 50 | ``` 51 | 52 | > [!NOTE] 53 | > This command must be run from the root of the project (or you can update the base path accordingly). 54 | 55 | You can then use it inside an SVG/HTML as follows: 56 | 57 | ```svg 58 | 69 | ``` 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 5/9 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/555-day-204.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/assets/555-day-204.webp -------------------------------------------------------------------------------- /assets/8-bit-rocky.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/assets/8-bit-rocky.pdf -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.21" 6 | via-ir = true 7 | optimizer_runs = 55_555_555 8 | fs_permissions = [ 9 | { access = "read-write", path = "./deployments/31337/data" }, 10 | { access = "read", path = "./out" }, 11 | { access = "read-write", path = "./output" }, 12 | ] 13 | remappings = [ 14 | "transmissions11/solmate/=lib/solmate/src/", 15 | "forge-std/=lib/forge-std/src/", 16 | "solady/=lib/solady/src/", 17 | ] 18 | 19 | [fmt] 20 | line_length = 100 21 | tab_width = 4 22 | bracket_spacing = true 23 | int_types = "long" 24 | func_attrs_with_params_multiline = false 25 | quote_style = "double" 26 | number_underscore = "thousands" 27 | 28 | [etherscan] 29 | base = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api" } 30 | base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" } 31 | 32 | [rpc_endpoints] 33 | base = "${RPC_URL_BASE_MAINNET}" 34 | base_sepolia = "${RPC_URL_BASE_SEPOLIA}" 35 | -------------------------------------------------------------------------------- /metadata/fonts/base64-encode-font.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | BASE_PATH = "./metadata/fonts/" 4 | 5 | with open(f"{BASE_PATH}output/FiraCode-Regular-Subset.txt", "w") as output_file: 6 | with open(f"{BASE_PATH}output/FiraCode-Regular-Subset.woff2", "rb") as input_file: 7 | output_file.write( 8 | f"data:font/woff2;utf-8;base64,{base64.b64encode(input_file.read()).decode('utf-8')}" 9 | ) 10 | 11 | with open(f"{BASE_PATH}output/FiraCode-Medium-Subset.txt", "w") as output_file: 12 | with open(f"{BASE_PATH}output/FiraCode-Medium-Subset.woff2", "rb") as input_file: 13 | output_file.write( 14 | f"data:font/woff2;utf-8;base64,{base64.b64encode(input_file.read()).decode('utf-8')}" 15 | ) 16 | 17 | with open(f"{BASE_PATH}output/Inter-Medium-Subset.txt", "w") as output_file: 18 | with open(f"{BASE_PATH}output/Inter-Medium-Subset.woff2", "rb") as input_file: 19 | output_file.write( 20 | f"data:font/woff2;utf-8;base64,{base64.b64encode(input_file.read()).decode('utf-8')}" 21 | ) 22 | 23 | """ 24 | You can now use it as follows: 25 | 36 | """ 37 | -------------------------------------------------------------------------------- /metadata/fonts/condense-font.sh: -------------------------------------------------------------------------------- 1 | # pip3 install fonttools 2 | 3 | pyftsubset ./metadata/fonts/input/FiraCode-Regular.ttf --output-file=./metadata/fonts/output/FiraCode-Regular-Subset.ttf --unicodes-file=./metadata/fonts/output/regular-glyphs.txt 4 | pyftsubset ./metadata/fonts/input/FiraCode-Medium.ttf --output-file=./metadata/fonts/output/FiraCode-Medium-Subset.ttf --unicodes-file=./metadata/fonts/output/medium-glyphs.txt 5 | pyftsubset ./metadata/fonts/input/Inter-Medium.ttf --output-file=./metadata/fonts/output/Inter-Medium-Subset.ttf --unicodes-file=./metadata/fonts/output/inter-medium-glyphs.txt 6 | -------------------------------------------------------------------------------- /metadata/fonts/generate-glyphs.py: -------------------------------------------------------------------------------- 1 | BASE_PATH = "./metadata/fonts/" 2 | 3 | REGULAR_CHARACTERS = "┌─╥┐│ 0.km║└╨┘080▮.192►fromkHzkbps[:/]━PLAY123456789" 4 | MEDIUM_CHARACTERS = "daymileagelocation0123456789new york city san francisco" +\ 5 | " seoul huntington beach westminister milan luštica bay shanghai paris r" +\ 6 | "eykjavík selfoss scotts valley redwood city jeju kagoshima denver7dwork" +\ 7 | "loadbytebeatgonnaflynowrocky" 8 | INTER_MEDIUM_CHARACTERS = "1000 × ⁵⁄₉ = 555 — 36×11" 9 | 10 | with open(f"{BASE_PATH}output/regular-glyphs.txt", "w") as f: 11 | f.write('\n'.join([f"U+{str(hex(ord(char))[2:]).zfill(4).upper()}" for char in sorted(list(set(REGULAR_CHARACTERS)))])) 12 | 13 | with open(f"{BASE_PATH}output/medium-glyphs.txt", "w") as f: 14 | f.write('\n'.join([f"U+{str(hex(ord(char))[2:]).zfill(4).upper()}" for char in sorted(list(set(MEDIUM_CHARACTERS)))])) 15 | 16 | with open(f"{BASE_PATH}output/inter-medium-glyphs.txt", "w") as f: 17 | f.write('\n'.join([f"U+{str(hex(ord(char))[2:]).zfill(4).upper()}" for char in sorted(list(set(INTER_MEDIUM_CHARACTERS)))])) 18 | -------------------------------------------------------------------------------- /metadata/fonts/input/FiraCode-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/input/FiraCode-Medium.ttf -------------------------------------------------------------------------------- /metadata/fonts/input/FiraCode-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/input/FiraCode-Regular.ttf -------------------------------------------------------------------------------- /metadata/fonts/input/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/input/Inter-Medium.ttf -------------------------------------------------------------------------------- /metadata/fonts/output/FiraCode-Medium-Subset.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/output/FiraCode-Medium-Subset.ttf -------------------------------------------------------------------------------- /metadata/fonts/output/FiraCode-Medium-Subset.txt: -------------------------------------------------------------------------------- 1 | data:font/woff2;utf-8;base64,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 -------------------------------------------------------------------------------- /metadata/fonts/output/FiraCode-Medium-Subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/output/FiraCode-Medium-Subset.woff2 -------------------------------------------------------------------------------- /metadata/fonts/output/FiraCode-Regular-Subset.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/output/FiraCode-Regular-Subset.ttf -------------------------------------------------------------------------------- /metadata/fonts/output/FiraCode-Regular-Subset.txt: -------------------------------------------------------------------------------- 1 | data:font/woff2;utf-8;base64,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 -------------------------------------------------------------------------------- /metadata/fonts/output/FiraCode-Regular-Subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/output/FiraCode-Regular-Subset.woff2 -------------------------------------------------------------------------------- /metadata/fonts/output/Inter-Medium-Subset.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/output/Inter-Medium-Subset.ttf -------------------------------------------------------------------------------- /metadata/fonts/output/Inter-Medium-Subset.txt: -------------------------------------------------------------------------------- 1 | data:font/woff2;utf-8;base64,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 -------------------------------------------------------------------------------- /metadata/fonts/output/Inter-Medium-Subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiveoutofnine/555/c9d9af91c2cc4494e35addec2dfe11c5d3ae1e3b/metadata/fonts/output/Inter-Medium-Subset.woff2 -------------------------------------------------------------------------------- /metadata/fonts/output/inter-medium-glyphs.txt: -------------------------------------------------------------------------------- 1 | U+0020 2 | U+0030 3 | U+0031 4 | U+0033 5 | U+0035 6 | U+0036 7 | U+003D 8 | U+00D7 9 | U+2014 10 | U+2044 11 | U+2075 12 | U+2089 -------------------------------------------------------------------------------- /metadata/fonts/output/medium-glyphs.txt: -------------------------------------------------------------------------------- 1 | U+0020 2 | U+0030 3 | U+0031 4 | U+0032 5 | U+0033 6 | U+0034 7 | U+0035 8 | U+0036 9 | U+0037 10 | U+0038 11 | U+0039 12 | U+0061 13 | U+0062 14 | U+0063 15 | U+0064 16 | U+0065 17 | U+0066 18 | U+0067 19 | U+0068 20 | U+0069 21 | U+006A 22 | U+006B 23 | U+006C 24 | U+006D 25 | U+006E 26 | U+006F 27 | U+0070 28 | U+0072 29 | U+0073 30 | U+0074 31 | U+0075 32 | U+0076 33 | U+0077 34 | U+0079 35 | U+00ED 36 | U+0161 -------------------------------------------------------------------------------- /metadata/fonts/output/regular-glyphs.txt: -------------------------------------------------------------------------------- 1 | U+0020 2 | U+002E 3 | U+002F 4 | U+0030 5 | U+0031 6 | U+0032 7 | U+0033 8 | U+0034 9 | U+0035 10 | U+0036 11 | U+0037 12 | U+0038 13 | U+0039 14 | U+003A 15 | U+0041 16 | U+0048 17 | U+004C 18 | U+0050 19 | U+0059 20 | U+005B 21 | U+005D 22 | U+0062 23 | U+0066 24 | U+006B 25 | U+006D 26 | U+006F 27 | U+0070 28 | U+0072 29 | U+0073 30 | U+007A 31 | U+2500 32 | U+2501 33 | U+2502 34 | U+250C 35 | U+2510 36 | U+2514 37 | U+2518 38 | U+2551 39 | U+2565 40 | U+2568 41 | U+25AE 42 | U+25BA -------------------------------------------------------------------------------- /metadata/generate-location-data.py: -------------------------------------------------------------------------------- 1 | # We initialize `data` to bitpack the location data together where the day # 2 | # corresponds to the bit position. Since there are 16 unique cities, we can 3 | # safely reserve 4 bits to store each day's location (2**4 = 16). See 4 | # {FiveFiveFiveData.getDayLocation} to see the mapping of location values to 5 | # cities. Since 1 character in hex is 4 bits, we can just construct a hex string 6 | # by concatenating the hex values of each day's location. 7 | data = "0x" 8 | 9 | locations = [ 10 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 13 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 14 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 16 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18 | 0, 0, 0, 0, 0, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 19 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 20 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 21 | 2, 7, 7, 7, 7, 7, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 23 | 8, 9, 10, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24 | 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 12, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 26 | 2, 2, 2, 2, 13, 13, 13, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29 | 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 14, 14, 14, 14, 14, 2, 2, 2, 2, 2, 2, 2, 2, 30 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 31 | 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 32 | 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 33 | ] 34 | 35 | for location in locations: 36 | data += hex(location & 0xf)[2] 37 | 38 | # Test 39 | for i in range(555): 40 | value = locations[i] 41 | data_value = int(f"0x{data[i + 2]}", 16) 42 | assert value == data_value, f"Error at day w/ index {i}: expected {value}, got {data_value}" 43 | 44 | print(data) 45 | -------------------------------------------------------------------------------- /metadata/generate-running-data.py: -------------------------------------------------------------------------------- 1 | # We initialize `data` to bitpack the mileage data together where the day # 2 | # corresponds to the bit position. Since we only have granularity to the 0.01th 3 | # of a kilometer, we record 100 times the mileage in kilometers. Since there are 4 | # many (178) runs greater than 20.47km ((2**11 - 1) / 100), but only 1 run is 5 | # greater than 40.95km ((2**12 - 1) / 100), it is efficient to reserve 12 bits 6 | # for each run. The only day that exceeds 40.95km is day 375, which recorded 7 | # 50.02km. We can handle this as an edge case when retrieving. 8 | data = 0 9 | 10 | mileage_string = """15.96 11 | 24.03 12 | 15.80 13 | 10.61 14 | 19.56 15 | 18.33 16 | 16.19 17 | 25.00 18 | 13.31 19 | 15.84 20 | 16.24 21 | 17.21 22 | 18.73 23 | 20.36 24 | 21.13 25 | 20.63 26 | 17.29 27 | 17.77 28 | 16.43 29 | 18.38 30 | 14.01 31 | 22.91 32 | 15.01 33 | 18.31 34 | 13.67 35 | 19.55 36 | 35.16 37 | 17.20 38 | 18.02 39 | 16.02 40 | 20.87 41 | 15.40 42 | 32.17 43 | 17.01 44 | 15.67 45 | 19.01 46 | 13.42 47 | 14.69 48 | 12.19 49 | 10.37 50 | 15.78 51 | 15.47 52 | 10.04 53 | 16.01 54 | 29.07 55 | 22.07 56 | 21.50 57 | 20.74 58 | 10.01 59 | 22.91 60 | 12.87 61 | 18.78 62 | 23.45 63 | 27.38 64 | 21.49 65 | 18.93 66 | 10.82 67 | 26.66 68 | 11.05 69 | 21.21 70 | 7.57 71 | 12.45 72 | 12.73 73 | 17.46 74 | 13.03 75 | 14.05 76 | 14.41 77 | 20.41 78 | 13.07 79 | 12.00 80 | 29.32 81 | 25.30 82 | 16.57 83 | 12.64 84 | 5.96 85 | 21.92 86 | 19.53 87 | 19.39 88 | 5.96 89 | 30.69 90 | 10.39 91 | 18.53 92 | 22.55 93 | 21.63 94 | 8.74 95 | 13.02 96 | 24.27 97 | 24.30 98 | 14.53 99 | 5.01 100 | 22.88 101 | 10.38 102 | 21.42 103 | 8.83 104 | 21.88 105 | 10.03 106 | 19.48 107 | 14.04 108 | 18.76 109 | 8.07 110 | 22.87 111 | 26.18 112 | 16.01 113 | 7.17 114 | 10.23 115 | 36.70 116 | 9.34 117 | 25.68 118 | 13.48 119 | 9.00 120 | 10.00 121 | 14.74 122 | 26.04 123 | 8.63 124 | 38.22 125 | 10.55 126 | 16.07 127 | 10.54 128 | 8.79 129 | 16.33 130 | 12.65 131 | 30.70 132 | 12.32 133 | 20.07 134 | 15.51 135 | 11.74 136 | 2.74 137 | 20.65 138 | 13.00 139 | 18.02 140 | 19.16 141 | 16.26 142 | 15.73 143 | 12.52 144 | 24.24 145 | 7.31 146 | 18.29 147 | 9.30 148 | 21.19 149 | 8.60 150 | 12.49 151 | 6.63 152 | 9.73 153 | 30.87 154 | 10.55 155 | 13.20 156 | 20.78 157 | 11.40 158 | 8.00 159 | 10.81 160 | 32.23 161 | 23.51 162 | 25.58 163 | 18.97 164 | 16.88 165 | 26.91 166 | 18.01 167 | 16.83 168 | 13.31 169 | 13.80 170 | 6.47 171 | 25.14 172 | 17.72 173 | 19.41 174 | 16.43 175 | 9.10 176 | 14.36 177 | 6.88 178 | 14.71 179 | 13.46 180 | 14.50 181 | 14.52 182 | 18.56 183 | 18.64 184 | 20.03 185 | 9.64 186 | 15.36 187 | 16.02 188 | 19.25 189 | 15.68 190 | 21.59 191 | 18.69 192 | 9.88 193 | 8.88 194 | 10.67 195 | 11.07 196 | 16.52 197 | 15.57 198 | 16.60 199 | 13.01 200 | 21.93 201 | 5.84 202 | 24.50 203 | 12.19 204 | 8.06 205 | 20.16 206 | 12.68 207 | 21.76 208 | 17.07 209 | 15.52 210 | 14.90 211 | 18.50 212 | 19.18 213 | 22.02 214 | 19.32 215 | 12.13 216 | 11.69 217 | 11.34 218 | 18.58 219 | 19.21 220 | 20.00 221 | 18.62 222 | 17.45 223 | 18.25 224 | 10.03 225 | 6.68 226 | 9.28 227 | 20.28 228 | 19.33 229 | 16.73 230 | 21.11 231 | 15.45 232 | 10.49 233 | 10.38 234 | 18.80 235 | 10.76 236 | 10.47 237 | 7.16 238 | 14.12 239 | 21.11 240 | 19.64 241 | 21.23 242 | 18.46 243 | 30.13 244 | 18.68 245 | 18.92 246 | 14.74 247 | 18.29 248 | 11.50 249 | 12.80 250 | 18.59 251 | 10.02 252 | 7.43 253 | 13.66 254 | 15.01 255 | 18.62 256 | 14.75 257 | 7.49 258 | 22.26 259 | 10.46 260 | 21.66 261 | 32.34 262 | 21.24 263 | 30.97 264 | 22.58 265 | 25.59 266 | 22.71 267 | 6.57 268 | 13.01 269 | 19.91 270 | 21.11 271 | 13.93 272 | 17.26 273 | 7.17 274 | 15.12 275 | 10.49 276 | 15.83 277 | 10.06 278 | 19.53 279 | 20.04 280 | 21.68 281 | 21.10 282 | 18.58 283 | 10.63 284 | 19.66 285 | 9.97 286 | 11.50 287 | 16.25 288 | 13.48 289 | 11.29 290 | 4.83 291 | 16.43 292 | 11.37 293 | 20.96 294 | 13.00 295 | 19.07 296 | 9.87 297 | 12.01 298 | 22.56 299 | 11.45 300 | 13.31 301 | 19.39 302 | 10.04 303 | 15.81 304 | 24.04 305 | 12.24 306 | 22.33 307 | 19.41 308 | 19.90 309 | 22.10 310 | 16.23 311 | 19.13 312 | 15.82 313 | 15.38 314 | 10.45 315 | 20.79 316 | 18.35 317 | 28.58 318 | 9.51 319 | 18.04 320 | 14.00 321 | 20.20 322 | 23.18 323 | 10.86 324 | 18.84 325 | 24.60 326 | 11.03 327 | 19.88 328 | 17.64 329 | 21.10 330 | 15.42 331 | 18.49 332 | 12.22 333 | 19.80 334 | 14.53 335 | 12.66 336 | 19.52 337 | 17.99 338 | 15.51 339 | 27.14 340 | 17.18 341 | 18.29 342 | 11.24 343 | 11.14 344 | 9.39 345 | 30.02 346 | 13.32 347 | 18.34 348 | 30.65 349 | 14.74 350 | 6.69 351 | 21.10 352 | 11.55 353 | 16.02 354 | 16.39 355 | 15.35 356 | 13.01 357 | 13.27 358 | 27.37 359 | 22.67 360 | 20.13 361 | 10.27 362 | 19.01 363 | 17.15 364 | 12.23 365 | 12.08 366 | 18.32 367 | 12.02 368 | 26.03 369 | 16.17 370 | 19.97 371 | 16.62 372 | 20.68 373 | 13.90 374 | 15.74 375 | 20.18 376 | 15.03 377 | 19.55 378 | 16.31 379 | 10.04 380 | 30.38 381 | 22.78 382 | 31.01 383 | 19.04 384 | 50.02 385 | 20.88 386 | 10.43 387 | 12.43 388 | 25.01 389 | 13.79 390 | 19.37 391 | 13.53 392 | 19.02 393 | 17.01 394 | 28.21 395 | 20.01 396 | 16.50 397 | 13.64 398 | 32.40 399 | 13.03 400 | 13.39 401 | 19.34 402 | 10.78 403 | 18.29 404 | 18.34 405 | 16.81 406 | 20.45 407 | 14.66 408 | 20.08 409 | 15.02 410 | 19.43 411 | 20.57 412 | 19.51 413 | 15.21 414 | 22.73 415 | 20.80 416 | 15.44 417 | 17.48 418 | 21.91 419 | 12.10 420 | 27.50 421 | 22.61 422 | 29.22 423 | 25.11 424 | 16.72 425 | 23.22 426 | 28.40 427 | 19.71 428 | 23.00 429 | 22.11 430 | 21.06 431 | 30.22 432 | 17.40 433 | 21.34 434 | 17.01 435 | 22.98 436 | 20.41 437 | 20.96 438 | 20.72 439 | 23.24 440 | 16.24 441 | 17.86 442 | 17.78 443 | 32.23 444 | 18.95 445 | 19.12 446 | 19.02 447 | 16.27 448 | 22.06 449 | 19.30 450 | 26.42 451 | 18.65 452 | 22.77 453 | 19.07 454 | 21.17 455 | 21.10 456 | 20.82 457 | 30.63 458 | 20.63 459 | 19.32 460 | 17.35 461 | 20.88 462 | 22.39 463 | 26.14 464 | 24.26 465 | 23.58 466 | 16.68 467 | 10.06 468 | 28.19 469 | 13.22 470 | 23.78 471 | 21.52 472 | 21.85 473 | 22.27 474 | 18.16 475 | 16.27 476 | 21.20 477 | 33.40 478 | 21.66 479 | 22.02 480 | 17.77 481 | 15.23 482 | 19.31 483 | 18.93 484 | 19.50 485 | 22.01 486 | 20.76 487 | 19.75 488 | 24.37 489 | 17.77 490 | 18.14 491 | 19.40 492 | 21.06 493 | 22.58 494 | 14.59 495 | 13.35 496 | 18.56 497 | 14.64 498 | 17.82 499 | 21.42 500 | 21.95 501 | 17.00 502 | 26.45 503 | 23.33 504 | 20.24 505 | 18.21 506 | 21.12 507 | 19.61 508 | 21.62 509 | 23.03 510 | 21.51 511 | 22.19 512 | 20.14 513 | 21.64 514 | 21.32 515 | 20.33 516 | 20.05 517 | 10.53 518 | 32.30 519 | 19.24 520 | 20.73 521 | 16.29 522 | 18.02 523 | 20.83 524 | 18.21 525 | 20.42 526 | 18.28 527 | 21.15 528 | 19.36 529 | 19.03 530 | 23.66 531 | 14.51 532 | 21.62 533 | 26.36 534 | 22.31 535 | 26.82 536 | 22.68 537 | 23.25 538 | 21.41 539 | 15.20 540 | 25.80 541 | 20.17 542 | 17.48 543 | 30.59 544 | 18.11 545 | 23.46 546 | 25.82 547 | 23.66 548 | 23.41 549 | 27.46 550 | 25.15 551 | 24.49 552 | 23.61 553 | 23.36 554 | 22.98 555 | 20.23 556 | 18.22 557 | 18.78 558 | 23.74 559 | 17.02 560 | 16.24 561 | 16.16 562 | 25.33 563 | 22.94 564 | 21.31""".split('\n') 565 | 566 | mileage = [int(m.replace(".", "")) for m in mileage_string] 567 | 568 | for m in mileage: 569 | data <<= 12 # We can just shift left by 12 because we know the first element is not 0. 570 | data |= m & 0xfff 571 | 572 | # Test 573 | for i in range(555): 574 | value = mileage[i] 575 | data_i = 554 - i 576 | data_value = (data >> (12 * data_i)) & 0xfff if i != 374 else 5002 577 | assert value == data_value, f"Error at day w/ index {i}: expected {value}, got {data_value}`" 578 | 579 | print(hex(data)) 580 | -------------------------------------------------------------------------------- /script/GenerateAudioOutput.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {console} from "forge-std/Test.sol"; 6 | import {Base64} from "solady/utils/Base64.sol"; 7 | import {DynamicBufferLib} from "solady/utils/DynamicBufferLib.sol"; 8 | import {LibString} from "solady/utils/LibString.sol"; 9 | import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; 10 | 11 | import {FiveFiveFiveAudio} from "src/utils/FiveFiveFiveAudio.sol"; 12 | 13 | /// @notice A script to create and write the WAV audio file of the arrangement 14 | /// of “Gonna Fly Now” by Bill Conti completely with smart contracts. 15 | /// @dev You must run this script with a high `--memory-limit` option (e.g. 16 | /// `50_000_000_000` works) and `--via-ir`. 17 | contract GenerateAudioOutputScript is Script { 18 | using DynamicBufferLib for DynamicBufferLib.DynamicBuffer; 19 | using LibString for uint256; 20 | using Math for int256; 21 | using Math for uint256; 22 | 23 | // ------------------------------------------------------------------------- 24 | // Constants 25 | // ------------------------------------------------------------------------- 26 | 27 | /// @notice The total number of ticks for 1 cycle of the audio. 28 | /// @dev Note that the length of the WAV file is written into the header 29 | /// returned by {FiveFiveFiveAudio.getAudioWavFileHeader} to take in a 30 | /// maximum of `776*2**10` samples. To change this, update the header 31 | /// returned accordingly. 32 | uint256 internal constant TICKS_PER_CYCLE = 776 << 10; 33 | 34 | // ------------------------------------------------------------------------- 35 | // Script `run()` 36 | // ------------------------------------------------------------------------- 37 | 38 | /// @notice Calls the {FiveFiveFiveAudio} library to generate the audio data 39 | /// and writes the WAV file to `./output/wav/rocky.wav`. 40 | function run() public { 41 | DynamicBufferLib.DynamicBuffer memory buffer; 42 | 43 | // First, write the WAV file header. 44 | buffer.p(FiveFiveFiveAudio.getAudioWavFileHeader()); 45 | 46 | // Next, we form the audio data by calling `getSoundValueAtSample` for 47 | // each sample in the range `[0, 776*2**10)` and write each result to 48 | // `data`. 49 | bytes memory data = new bytes(TICKS_PER_CYCLE); 50 | for (uint256 tick; tick < TICKS_PER_CYCLE; ) { 51 | data[tick] = bytes1(FiveFiveFiveAudio.getSoundValueAtSample(tick)); 52 | 53 | unchecked { 54 | ++tick; 55 | } 56 | } 57 | 58 | buffer.p(data); 59 | vm.writeFile("./output/wav/rocky.wav", string(buffer.data)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /script/GenerateJSONOutput.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {console} from "forge-std/Test.sol"; 6 | import {LibString} from "solady/utils/LibString.sol"; 7 | 8 | import {IFiveFiveFive} from "src/interfaces/IFiveFiveFive.sol"; 9 | import {FiveFiveFiveArt} from "src/utils/FiveFiveFiveArt.sol"; 10 | 11 | /// @notice A script to create and write the base64-encoded JSON output of a 12 | /// given token's metadata, directly from the utility library. 13 | /// @dev You must run this script with `--via-ir`. 14 | contract GenerateJSONOutputScript is Script { 15 | using LibString for uint256; 16 | 17 | // ------------------------------------------------------------------------- 18 | // Script `run()` 19 | // ------------------------------------------------------------------------- 20 | 21 | /// @notice Calls the {FiveFiveFiveArt} library to generate the JSON output 22 | /// for a given token ID `i` and writes it `./output/json/{i}.json`. 23 | function run() public { 24 | for (uint256 i; i < 555; ) { 25 | vm.writeFile( 26 | string.concat("./output/txt/", (i + 1).toString(), ".txt"), 27 | FiveFiveFiveArt.render({ 28 | _day: i, 29 | _theme: IFiveFiveFive.Theme({ 30 | background: 0x000000, 31 | terminalBackground: 0x161616, 32 | primary: 0x0090ff, 33 | text: 0xededed, 34 | subtext: 0xa0a0a0, 35 | label: 0xff8b3e, 36 | intent1: 0x4cc38a, 37 | intent2: 0xf0c000, 38 | intent3: 0xff8b3e, 39 | intent4: 0xff6369, 40 | modified: false 41 | }) 42 | }) 43 | ); 44 | 45 | unchecked { 46 | ++i; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /script/PrintData.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {console} from "forge-std/Test.sol"; 6 | 7 | import {FiveFiveFiveData} from "src/utils/FiveFiveFiveData.sol"; 8 | 9 | /// @notice A testing script to use during development to print out the mileage 10 | /// and location data for each run from {FiveFiveFiveData}. 11 | contract PrintDataScript is Script { 12 | // ------------------------------------------------------------------------- 13 | // Script `run()` 14 | // ------------------------------------------------------------------------- 15 | 16 | /// @notice Calls {FiveFiveFiveArt} and retrieves data for a range of days. 17 | function run() public view { 18 | for (uint256 i = 325; i < 336; ) { 19 | uint256 mileage = FiveFiveFiveData.getDayMileage(i); 20 | (string memory location, ) = FiveFiveFiveData.getDayLocation(i); 21 | console.log(i, mileage, location); 22 | 23 | unchecked { 24 | ++i; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/FiveFiveFive.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {Base64} from "solady/utils/Base64.sol"; 5 | import {LibString} from "solady/utils/LibString.sol"; 6 | import {Owned} from "solmate/auth/Owned.sol"; 7 | import {ERC721} from "solmate/tokens/ERC721.sol"; 8 | 9 | import {IFiveFiveFive} from "./interfaces/IFiveFiveFive.sol"; 10 | import {FiveFiveFiveArt} from "./utils/FiveFiveFiveArt.sol"; 11 | import {FiveFiveFiveAudio} from "./utils/FiveFiveFiveAudio.sol"; 12 | 13 | /// @title 555 (1000 × ⁵⁄₉) NFTs 14 | /// @author fiveoutofnine 15 | /// @notice A ⁵⁄₉-themed NFT to commemorate me running 10000km in 555 days of 16 | /// running everyday. For each token, in addition to displaying information 17 | /// about the day's run with a themeable color palette, the metadata of each 18 | /// token contains a 100% onchain-generated 24.832 second long audio of a 5-part 19 | /// arrangement of “Gonna Fly Now” by Bill Conti, popularly known as the theme 20 | /// song from the movie Rocky (1976), at 117.1875 BPM. 21 | contract FiveFiveFive is IFiveFiveFive, ERC721, Owned { 22 | using LibString for uint256; 23 | 24 | // ------------------------------------------------------------------------- 25 | // Constants 26 | // ------------------------------------------------------------------------- 27 | 28 | /// @notice Address of fiveoutofnine.eth, the contract owner. 29 | address constant FIVEOUTOFNINE = 0xA85572Cd96f1643458f17340b6f0D6549Af482F5; 30 | 31 | /// @notice Price to mint a token. 32 | /// @dev Equivalent to (⁵⁄₉) / 10 ETH. 33 | uint256 constant PRICE = 0.055_555_555_555_555_555 ether; 34 | 35 | /// @notice Description of the collection. 36 | string constant COLLECTION_DESCRIPTION = 37 | unicode"A ⁵⁄₉-themed NFT to commemorate me running 10000km in 555 days " 38 | unicode"of running everyday. For each token, in addition to displaying " 39 | unicode"information about the day's run with a themeable color palette," 40 | unicode" the metadata of each token contains a 100% onchain-generated 2" 41 | unicode"4.832 second long audio of a 5-part arrangement of “Gonna Fly N" 42 | unicode"ow” by Bill Conti, popularly known as the theme song from the m" 43 | unicode"ovie Rocky (1976), at 117.1875 BPM."; 44 | 45 | // ------------------------------------------------------------------------- 46 | // Storage 47 | // ------------------------------------------------------------------------- 48 | 49 | /// @inheritdoc IFiveFiveFive 50 | string public override baseURI; 51 | 52 | /// @notice The next token ID to be minted. 53 | /// @dev The valid tokens to be minted are `[1, 555]`. Since `[1, 52]` are 54 | /// minted in the constructor, the next token ID to be minted is 53. Note 55 | /// that, in addition to the first 52, tokens 375, 459, and 555 are also 56 | /// minted in the constructor. Thus, when `mint()` is called when 57 | /// `_nextTokenId` is 375, 459, or 555, it will increment by 2 instead of 1. 58 | uint256 internal _nextTokenId = 53; 59 | 60 | /// @notice A mapping of token IDs to their themes. 61 | mapping(uint256 => Theme) internal _themes; 62 | 63 | // ------------------------------------------------------------------------- 64 | // Constructor + functions 65 | // ------------------------------------------------------------------------- 66 | 67 | constructor() 68 | ERC721(unicode"555 (1000 × ⁵⁄₉)", "555") 69 | Owned(FIVEOUTOFNINE) 70 | { 71 | // shanefan.eth gets #1. 72 | _mint(0xaFDc1A3EF3992f53C10fC798d242E15E2F0DF51A, 1); 73 | _mint(FIVEOUTOFNINE, 2); 74 | _mint(FIVEOUTOFNINE, 3); 75 | _mint(FIVEOUTOFNINE, 4); 76 | _mint(FIVEOUTOFNINE, 5); 77 | _mint(FIVEOUTOFNINE, 6); 78 | _mint(FIVEOUTOFNINE, 7); 79 | _mint(FIVEOUTOFNINE, 8); 80 | _mint(0x00cDa37BfC3Dd20349Aa901Fe8646688218d8772, 9); 81 | _mint(0x0734d56DA60852A03e2Aafae8a36FFd8c12B32f1, 10); 82 | _mint(0x16cCd2a1346978e27FDCbda43569E251C4227341, 11); 83 | _mint(0x1a8906a0EBB799ED4C0e385d7493D11701700d3a, 12); 84 | _mint(0x1B7688538170E98856ea86D0a68C7e407D49C5C3, 13); 85 | _mint(0x230d31EEC85F4063a405B0F95bdE509C0d0A8b5D, 14); 86 | _mint(0x2Abc80332a8DFa064bD2f361E8B72d76ef8637C5, 15); 87 | _mint(0x2BdA41589fc6b86D17F9ADb4Cc90313799D5F6e5, 16); 88 | _mint(0x317Bc38b66566566529C41462bA774F489b4a63f, 17); 89 | _mint(0x34aA3F359A9D614239015126635CE7732c18fDF3, 18); 90 | _mint(0x4a69B81A2cBEb3581C61d5087484fBda2Ed39605, 19); 91 | _mint(0x5C227217875D0Bc94AeeE8798aF9de3935CFf0f2, 20); 92 | _mint(0x5DFfD5527551888c2AC47f799c4Dc8e830dECeE7, 21); 93 | _mint(0x61E2029e46A9d2584bad8093b19bc53572E95d5D, 22); 94 | _mint(0x655aF72e1500EB8A8d1c90856Ae3B8f148A78471, 23); 95 | _mint(0x66810420d110919a0E8b550fDE3fE24D50ef0e26, 24); 96 | _mint(0x66BF25e328156eF5d08404094Ae2532D8592F87D, 25); 97 | _mint(0x6dacb7352B4eC1e2B979a05E3cF1F126AD641110, 26); 98 | _mint(0x75d4bdBf6593ed463e9625694272a0FF9a6D346F, 27); 99 | _mint(0x79d31bFcA5Fda7A4F15b36763d2e44C99D811a6C, 28); 100 | _mint(0x7eD52863829AB99354F3a0503A622e82AcD5F7d3, 29); 101 | _mint(0x849151d7D0bF1F34b70d5caD5149D28CC2308bf1, 30); 102 | _mint(0x85C153AAe1f101Af08151863306d9e0b823eA1B5, 31); 103 | _mint(0x88F09Bdc8e99272588242a808052eb32702f88D0, 32); 104 | _mint(0x8E10FF7D1195484a3FDd5B19D48E2F394a88FaD3, 33); 105 | _mint(0x8FC68A56f9682312953a1730Ae62AFD1a99FdC4F, 34); 106 | _mint(0x91031DCFdEa024b4d51e775486111d2b2A715871, 35); 107 | _mint(0xa35156ca4294c13fe256B10091F3D6595E3Bf7d7, 36); 108 | _mint(0xB0623C91c65621df716aB8aFE5f66656B21A9108, 37); 109 | _mint(0xB95777719Ae59Ea47A99e744AfA59CdcF1c410a1, 38); 110 | _mint(0xBad58e133138549936D2576ebC33251bE841d3e9, 39); 111 | _mint(0xbC955ed49f1Cd4595953873Bbf9B90AfAF45E996, 40); 112 | _mint(0xc95C558dAA63b1A79331B2AB4a2a7af375384d3B, 41); 113 | _mint(0xD7029BDEa1c17493893AAfE29AAD69EF892B8ff2, 42); 114 | _mint(0xd7B8865970528b0cC372cE380D64a864039c2B9e, 43); 115 | _mint(0xDbAacdcadD7c51a325B771ff75B261a1e7baE11c, 44); 116 | _mint(0xDC40CbF86727093c52582405703e5b97D5C64B66, 45); 117 | _mint(0xE340b00B6B622C136fFA5CFf130eC8edCdDCb39D, 46); 118 | _mint(0xEee718c1e522ecB4b609265db7A83Ab48ea0B06f, 47); 119 | _mint(0xF196b0B975b0fD11dF1936C3A35Cf10ea218f283, 48); 120 | _mint(0xF1b0E893ea21e95763577CeC398ea0af5dB79037, 49); 121 | _mint(0xF2829E74C8a8709e170E21979A482f88C607B632, 50); 122 | _mint(0xf32dd1Bd55bD14d929218499a2E7D106F72f79c7, 51); 123 | _mint(0xFaaDaaB725709f9Ac6d5C03d9C6A6F5E3511FD70, 52); 124 | // kaki.eth gets #375, the only marathon+ day (50.02km). 125 | _mint(0x4fd9D0eE6D6564E80A9Ee00c0163fC952d0A45Ed, 375); 126 | // fiveoutofnine.eth gets #459, 1:19:16 HM PB. 127 | _mint(FIVEOUTOFNINE, 459); 128 | // fiveoutofnine.eth gets #555. 129 | _mint(FIVEOUTOFNINE, 555); 130 | } 131 | 132 | /// @inheritdoc IFiveFiveFive 133 | function mint() external payable override { 134 | uint256 tokenId = _nextTokenId; 135 | 136 | // Minting is complete. 137 | if (tokenId > 554) revert MintingEnded(); 138 | // Revert if the sender didn't supply enough funds. 139 | if (msg.value < PRICE) revert InsufficientFunds(); 140 | 141 | // Increment the next token ID; 142 | unchecked { 143 | _nextTokenId = tokenId + (tokenId == 374 || tokenId == 458 ? 2 : 1); 144 | } 145 | 146 | // Mint token. 147 | _mint(msg.sender, tokenId); 148 | } 149 | 150 | /// @inheritdoc IFiveFiveFive 151 | function setBaseURI(string calldata _baseURI) external override onlyOwner { 152 | // Set new base URI. 153 | baseURI = _baseURI; 154 | 155 | // Emit event. 156 | emit SetBaseURI(_baseURI); 157 | } 158 | 159 | /// @inheritdoc IFiveFiveFive 160 | function withdraw(address _to) external override onlyOwner { 161 | (bool success, ) = payable(_to).call{value: address(this).balance}(""); 162 | require(success); 163 | } 164 | 165 | // ------------------------------------------------------------------------- 166 | // Color theming 167 | // ------------------------------------------------------------------------- 168 | 169 | /// @inheritdoc IFiveFiveFive 170 | function getTokenTheme(uint256 _id) external view override returns (Theme memory) { 171 | // Revert if the token hasn't been minted. 172 | if (_ownerOf[_id] == address(0)) revert TokenUnminted(); 173 | 174 | Theme memory theme = _themes[_id]; 175 | // Return the default theme if the theme was never set. 176 | if (!theme.modified) { 177 | return 178 | Theme({ 179 | background: 0x000000, 180 | terminalBackground: 0x161616, 181 | primary: 0x0090ff, 182 | text: 0xededed, 183 | subtext: 0xa0a0a0, 184 | label: 0xff8b3e, 185 | intent1: 0x4cc38a, 186 | intent2: 0xf0c000, 187 | intent3: 0xff8b3e, 188 | intent4: 0xff6369, 189 | modified: false 190 | }); 191 | } 192 | 193 | return theme; 194 | } 195 | 196 | /// @inheritdoc IFiveFiveFive 197 | function setTokenTheme( 198 | uint256 _id, 199 | uint24 _background, 200 | uint24 _terminalBackground, 201 | uint24 _primary, 202 | uint24 _text, 203 | uint24 _subtext, 204 | uint24 _label, 205 | uint24 _intent1, 206 | uint24 _intent2, 207 | uint24 _intent3, 208 | uint24 _intent4 209 | ) external override { 210 | // Revert if the sender is not the owner of the token. 211 | if (_ownerOf[_id] != msg.sender) revert Unauthorized(); 212 | 213 | // Set new token theme. 214 | Theme memory theme = Theme({ 215 | background: _background, 216 | terminalBackground: _terminalBackground, 217 | primary: _primary, 218 | text: _text, 219 | subtext: _subtext, 220 | label: _label, 221 | intent1: _intent1, 222 | intent2: _intent2, 223 | intent3: _intent3, 224 | intent4: _intent4, 225 | modified: true 226 | }); 227 | _themes[_id] = theme; 228 | 229 | // Emit event. 230 | emit SetTokenTheme(_id, theme); 231 | } 232 | 233 | // ------------------------------------------------------------------------- 234 | // Onchain audio generation 235 | // ------------------------------------------------------------------------- 236 | 237 | /// @inheritdoc IFiveFiveFive 238 | function getAudioWavFileHeader() external pure returns (bytes memory) { 239 | return FiveFiveFiveAudio.getAudioWavFileHeader(); 240 | } 241 | 242 | /// @inheritdoc IFiveFiveFive 243 | function getSoundValueAtSample(uint256 _tick) external pure returns (uint8) { 244 | return FiveFiveFiveAudio.getSoundValueAtSample(_tick); 245 | } 246 | 247 | // ------------------------------------------------------------------------- 248 | // ERC721Metadata 249 | // ------------------------------------------------------------------------- 250 | 251 | /// @notice Returns the adjusted URI for a given token ID. 252 | /// @dev Reverts if the token ID does not exist. Additionally, if `baseURI` 253 | /// is unset, `tokenURI` will directly return the URI generated onchain via 254 | /// {_tokenURI(uint256)}. Otherwise, it will return `baseURI + tokenId`. 255 | /// @param _id The token ID. 256 | /// @return The adjusted URI for the given token ID. 257 | function tokenURI(uint256 _id) public view override returns (string memory) { 258 | // Revert if the token hasn't been minted. 259 | if (_ownerOf[_id] == address(0)) revert TokenUnminted(); 260 | 261 | return 262 | bytes(baseURI).length == 0 263 | ? _tokenURI(_id) 264 | : string.concat(baseURI, _id.toString()); 265 | } 266 | 267 | /// @notice Returns the URI for a given token ID, as generated onchain. 268 | /// @param _id The token ID. 269 | /// @return The URI for the given token ID. 270 | function _tokenURI(uint256 _id) internal view returns (string memory) { 271 | Theme memory theme = _themes[_id]; 272 | 273 | // Use the default theme if the theme was never set. 274 | if (!theme.modified) { 275 | theme = Theme({ 276 | background: 0x000000, 277 | terminalBackground: 0x161616, 278 | primary: 0x0090ff, 279 | text: 0xededed, 280 | subtext: 0xa0a0a0, 281 | label: 0xff8b3e, 282 | intent1: 0x4cc38a, 283 | intent2: 0xf0c000, 284 | intent3: 0xff8b3e, 285 | intent4: 0xff6369, 286 | modified: false 287 | }); 288 | } 289 | 290 | return FiveFiveFiveArt.render({ _day: _id - 1, _theme: theme }); 291 | } 292 | 293 | // ------------------------------------------------------------------------- 294 | // Contract metadata 295 | // ------------------------------------------------------------------------- 296 | 297 | /// @inheritdoc IFiveFiveFive 298 | function contractURI() external pure override returns (string memory) { 299 | return 300 | string.concat( 301 | "data:application/json;charset=utf-8;base64,", 302 | Base64.encode( 303 | abi.encodePacked( 304 | unicode'{"name":"555 (1000 × ⁵⁄₉)","description":"', 305 | COLLECTION_DESCRIPTION, 306 | '"}' 307 | ) 308 | ) 309 | ); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/interfaces/IFiveFiveFive.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | /// @title The interface for {FiveFiveFive} 5 | /// @author fiveoutofnine 6 | interface IFiveFiveFive { 7 | // ------------------------------------------------------------------------- 8 | // Errors 9 | // ------------------------------------------------------------------------- 10 | 11 | /// @notice Emitted when the sender didn't supply enough funds. 12 | error InsufficientFunds(); 13 | 14 | /// @notice Emitted when the minting period has ended. 15 | error MintingEnded(); 16 | 17 | /// @notice Emitted when a token hasn't been minted. 18 | error TokenUnminted(); 19 | 20 | /// @notice Reverts if the sender isn't the owner of the token. 21 | error Unauthorized(); 22 | 23 | // ------------------------------------------------------------------------- 24 | // Structs 25 | // ------------------------------------------------------------------------- 26 | 27 | /// @notice Struct containing 24-bit RGB color values for the theme to 28 | /// render the token URI with. 29 | /// @param background The background color behind the terminal window. 30 | /// @param terminalBackground The background color of the terminal window. 31 | /// @param primary The color inner frames' borders and highlights. 32 | /// @param text The color of the primary text. 33 | /// @param subtext The color of the secondary text. 34 | /// @param label The color of the inner frames' labels. 35 | /// @param intent1 The color of the most positive intent. 36 | /// @param intent2 The color of the second most positive intent. 37 | /// @param intent3 The color of the second most negative intent. 38 | /// @param intent4 The color of the most negative intent. 39 | /// @param modified Whether the theme has been modified by the owner. 40 | struct Theme { 41 | uint24 background; 42 | uint24 terminalBackground; 43 | uint24 primary; 44 | uint24 text; 45 | uint24 subtext; 46 | uint24 label; 47 | uint24 intent1; 48 | uint24 intent2; 49 | uint24 intent3; 50 | uint24 intent4; 51 | bool modified; 52 | } 53 | 54 | // ------------------------------------------------------------------------- 55 | // Events 56 | // ------------------------------------------------------------------------- 57 | 58 | /// @notice Emitted when the base URI is set. 59 | /// @param baseURI The new base URI. 60 | event SetBaseURI(string baseURI); 61 | 62 | /// @notice Emitted when some token's theme is set. 63 | /// @param id The ID of the token. 64 | /// @param theme The theme of the token. 65 | event SetTokenTheme(uint256 indexed id, Theme indexed theme); 66 | 67 | // ------------------------------------------------------------------------- 68 | // Storage 69 | // ------------------------------------------------------------------------- 70 | 71 | /// @dev If `baseURI` is unset, `tokenURI` will directly return the URI 72 | /// generated onchain via {FiveFiveFiveArt._tokenURI(uint256)}. Otherwise, 73 | /// it will return `baseURI + tokenId`. 74 | /// @return The base URI for the token collection. 75 | function baseURI() external view returns (string memory); 76 | 77 | // ------------------------------------------------------------------------- 78 | // Functions 79 | // ------------------------------------------------------------------------- 80 | 81 | /// @notice Mints the next mintable token to the sender. 82 | /// @dev The ID of the token MUST automatically increment by 1 every time, 83 | /// unless the token ID is 374, 458, or 554, in which case it MUST increment 84 | /// by 2. This is because they are special days minted in the constructor. 85 | function mint() external payable; 86 | 87 | /// @notice Sets the base URI for the token collection. 88 | /// @dev This function can only be called by the contract owner. 89 | /// @param _baseURI The new base URI for the token collection. 90 | function setBaseURI(string calldata _baseURI) external; 91 | 92 | /// @notice Withdraws the contract's balance to some address. 93 | /// @param _to The address to withdraw the contract's balance to. 94 | function withdraw(address _to) external; 95 | 96 | // ------------------------------------------------------------------------- 97 | // Color theming 98 | // ------------------------------------------------------------------------- 99 | 100 | /// @notice Returns the token theme for a token. 101 | /// @param _id The ID of the token to get the theme for. 102 | /// @return The token theme for the token. 103 | function getTokenTheme(uint256 _id) external view returns (Theme memory); 104 | 105 | /// @notice Sets the token theme for a token given 24-bit RGB color values. 106 | /// @dev This function can only be called by the token's owner. `modified` 107 | /// MUST be set to `true` the first time this function is called for a 108 | /// token. 109 | /// @param _background The background color behind the terminal window. e.g. 110 | /// `0x000000`. 111 | /// @param _terminalBackground The background color of the terminal window. 112 | /// e.g. `0x161616`. 113 | /// @param _primary The color inner frames' borders and highlights. e.g. 114 | /// `0x0090ff`. 115 | /// @param _text The color of the primary text. e.g. `0xededed`. 116 | /// @param _subtext The color of the secondary text. e.g. `0xa0a0a0`. 117 | /// @param _label The color of the inner frames' labels. e.g. `0xff8b3e`. 118 | /// @param _intent1 The color of the most positive intent. e.g. `0x4cc38a`. 119 | /// @param _intent2 The color of the second most positive intent. e.g. 120 | /// `0xf0c000`. 121 | /// @param _intent3 The color of the second most negative intent. e.g. 122 | /// `0xff8b3e`. 123 | /// @param _intent4 The color of the most negative intent. e.g. `0xff6369`. 124 | function setTokenTheme( 125 | uint256 _id, 126 | uint24 _background, 127 | uint24 _terminalBackground, 128 | uint24 _primary, 129 | uint24 _text, 130 | uint24 _subtext, 131 | uint24 _label, 132 | uint24 _intent1, 133 | uint24 _intent2, 134 | uint24 _intent3, 135 | uint24 _intent4 136 | ) external; 137 | 138 | // ------------------------------------------------------------------------- 139 | // Onchain audio generation 140 | // ------------------------------------------------------------------------- 141 | 142 | /// @notice Returns the WAV file header for the audio file for 1 full cycle 143 | /// of the token's sound with the parameters the token's sound was generated 144 | /// with: 145 | /// * Size: 776.044921875kB (794670 bytes) 146 | /// * Number of channels: 1 147 | /// * Sample rate: 32000Hz 148 | /// * Bits/sample: 8 bits/sample 149 | function getAudioWavFileHeader() external pure returns (bytes memory); 150 | 151 | /// @notice Returns the sound value at a given time tick in the audio. 152 | /// @dev {FiveFiveFive}'s audio was generated at a sample rate of 32000Hz, 153 | /// which means that `_tick` increments by 1 every 1/32000 seconds. 154 | /// @param _tick The number of samples since the beginning of the audio at 155 | /// a frequency of 32000Hz to get the sound value at. 156 | /// @return The sound value at the given time tick, a value in the range 157 | /// `[0, 255]` (higher means louder). 158 | function getSoundValueAtSample(uint256 _tick) external pure returns (uint8); 159 | 160 | // ------------------------------------------------------------------------- 161 | // Metadata 162 | // ------------------------------------------------------------------------- 163 | 164 | /// @notice Returns the contract URI for this contract. 165 | /// @return The contract URI for this contract. 166 | function contractURI() external view returns (string memory); 167 | } 168 | -------------------------------------------------------------------------------- /src/utils/FiveFiveFiveArt.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {Base64} from "solady/utils/Base64.sol"; 5 | import {DynamicBufferLib} from "solady/utils/DynamicBufferLib.sol"; 6 | import {LibString} from "solady/utils/LibString.sol"; 7 | 8 | import {FiveFiveFiveConstants} from "./FiveFiveFiveConstants.sol"; 9 | import {FiveFiveFiveData} from "./FiveFiveFiveData.sol"; 10 | import {IFiveFiveFive} from "src/interfaces/IFiveFiveFive.sol"; 11 | 12 | /// @title {FiveFiveFive} NFT visual art 13 | /// @author fiveoutofnine 14 | /// @notice A library for generating art (SVG and HTML) and metadata for 15 | /// {FiveFiveFive}. 16 | /// @dev The source code for this library is intended to be viewed with 17 | /// `Menlo, Monaco, 'Courier New', monospace`. 18 | library FiveFiveFiveArt { 19 | using DynamicBufferLib for DynamicBufferLib.DynamicBuffer; 20 | using LibString for uint256; 21 | 22 | // ------------------------------------------------------------------------- 23 | // `render` function 24 | // ------------------------------------------------------------------------- 25 | 26 | /// @notice Renders the JSON output representing the 555 token with the 27 | /// token's corresponding image (SVG) data, animation (HTML) data, and 28 | /// metadata. 29 | /// @param _day 0-indexed day. 30 | /// @param _theme The theme to use for the token. 31 | /// @return JSON output representing the 555 token. 32 | function render(uint256 _day, IFiveFiveFive.Theme memory _theme) 33 | public 34 | pure 35 | returns (string memory) 36 | { 37 | uint256 id = _day + 1; 38 | 39 | // Retrieve data for the day. 40 | uint256 distance = FiveFiveFiveData.getDayMileage(_day); 41 | (string memory locationName, uint256 locationLength) = FiveFiveFiveData.getDayLocation(_day); 42 | uint256 workload = distance 43 | + (_day > 0 ? FiveFiveFiveData.getDayMileage(_day - 1) : 0) 44 | + (_day > 1 ? FiveFiveFiveData.getDayMileage(_day - 2) : 0) 45 | + (_day > 2 ? FiveFiveFiveData.getDayMileage(_day - 3) : 0) 46 | + (_day > 3 ? FiveFiveFiveData.getDayMileage(_day - 4) : 0) 47 | + (_day > 4 ? FiveFiveFiveData.getDayMileage(_day - 5) : 0) 48 | + (_day > 5 ? FiveFiveFiveData.getDayMileage(_day - 6) : 0); 49 | 50 | // Split distance and workload into km and m. 51 | (uint256 km, uint256 m) = (distance / 100, distance % 100); 52 | (uint256 workloadKm, uint256 workloadM) = (workload / 100, workload % 100); 53 | uint256 workloadBars = workload > 80_00 ? (workload - 80_00) / 467 : 0; 54 | 55 | // Generate styles. 56 | DynamicBufferLib.DynamicBuffer memory stylesBuffer; 57 | stylesBuffer.p( 58 | abi.encodePacked( 59 | FiveFiveFiveConstants.getStyleHeader(), 60 | uint256(_theme.terminalBackground).toHexStringNoPrefix(3), 61 | ";width:280px;padding:0 8px;margin:0;height:188px;display:flex;" 62 | "}code{font-size:12px;margin:auto}span{font-variant-ligatures:n" 63 | "one}.f{color:#", 64 | uint256(_theme.text).toHexStringNoPrefix(3), 65 | "}.i{color:#", 66 | uint256(_theme.subtext).toHexStringNoPrefix(3), 67 | "}.v{color:#", 68 | uint256(_theme.primary).toHexStringNoPrefix(3), 69 | "}.e{color:#", 70 | uint256(_theme.label).toHexStringNoPrefix(3), 71 | "}.o{color:#", 72 | uint256(_theme.intent1).toHexStringNoPrefix(3), 73 | "}.u{color:#", 74 | uint256(_theme.intent2).toHexStringNoPrefix(3), 75 | "}.t{color:#", 76 | uint256(_theme.intent3).toHexStringNoPrefix(3), 77 | "}.x{color:#", 78 | uint256(_theme.intent4).toHexStringNoPrefix(3), 79 | "}.y{font-family:A}.n{font-family:B}.z{position:absolute;margin" 80 | ":auto 0;border-radius:100%;top:6px;width:12px;height:12px}" 82 | ) 83 | ); 84 | 85 | // Generate inner HTML. 86 | DynamicBufferLib.DynamicBuffer memory innerHTMLBuffer; 87 | innerHTMLBuffer.p( 88 | abi.encodePacked( 89 | '
1000 × ⁵⁄₉ = '
96 | unicode'555 — 36×11
'
100 | unicode'┌─day─╥─mile'
101 | unicode'age─╥─location─────────'
102 | unicode"┐\n│ ",
103 | // 0-pad day number to 3 digits.
104 | id < 10
105 | ? '00'
106 | : id < 100 ? '0' : "",
107 | '',
108 | id.toString(),
109 | unicode' ║ ',
110 | // 0-pad km portion of distance to 2 digits. Since the largest
111 | // mileage day is 50.02, we never need to pad to more.
112 | km < 10 ? " " : "",
113 | km.toString(),
114 | '.',
115 | // 0-pad m portion of distance to 2 digits. Since the data only
116 | // has granularity to the 0.01th of a kilometer, we never need
117 | // to pad to more.
118 | m < 10 ? "0" : "",
119 | m.toString(),
120 | unicode'km ║ ',
121 | locationName,
122 | "",
123 | // Add spaces to align the remaining part of the frame.
124 | LibString.repeat(" ", 17 - locationLength),
125 | unicode"│\n└─────╨─────────╨──────────────────┘\n┌─7d─workload──────'
127 | unicode"───────",
128 | // Pad km portion of workload to 3 digits. Since no sliding
129 | // window of 7 day mileage exceeds 1000 km, we never need to pad
130 | // to more.
131 | workloadKm < 100 ? unicode"─" : "",
132 | '',
133 | workloadKm.toString(),
134 | '.',
135 | // 0-pad m portion of distance to 2 digits. Since the data only
136 | // has granularity to the 0.01th of a kilometer, we never need
137 | // to pad to more.
138 | workloadM < 10 ? "0" : "",
139 | workloadM.toString(),
140 | unicode'km─┐\n│ 080 ',
142 | // Construct workload gradient bars.
143 | getWorkloadSegment(workloadBars < 6 ? workloadBars : 6),
144 | '',
145 | workloadBars > 11 ? unicode"▮▮▮▮▮▮" : "",
146 | workloadBars < 7 ? '......' : "",
147 | workloadBars > 6 && workloadBars < 12 ? getWorkloadSegment(workloadBars - 6) : "",
148 | '',
149 | workloadBars > 17 ? unicode"▮▮▮▮▮▮" : "",
150 | workloadBars < 13 ? '......' : "",
151 | workloadBars > 12 && workloadBars < 18 ? getWorkloadSegment(workloadBars - 12) : "",
152 | '',
153 | workloadBars > 23 ? unicode"▮▮▮▮▮▮" : "",
154 | workloadBars < 19 ? '......' : "",
155 | workloadBars > 18 && workloadBars < 24 ? getWorkloadSegment(workloadBars - 18) : "",
156 | unicode" 192 │\n└────────────────────────────────"
157 | unicode'──┘\n┌─bytebeat───────────────'
158 | unicode'──────────┐\n│ ► gonna fly now from rocky │\n│ 32kHz 31.25kbps [0 :00 /'
163 | unicode' 0:25] │\n│ ━────────────────────────<'
166 | unicode'/span> [PLAY] │\n└──────────────────────────────────┘'
170 | unicode"