├── .gitignore ├── LICENSE ├── README.md ├── contracts ├── Bitmap.sol └── BitmapNFT.sol ├── hardhat.config.ts ├── package.json ├── scripts ├── deploy │ └── BitmapNFT.ts └── verify │ ├── Verify.ts │ └── verify.json ├── test └── BitmapNFT.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | node_modules/ 4 | typechain/ 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 double jump.tokyo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solidity-bitmap -------------------------------------------------------------------------------- /contracts/Bitmap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | struct XY { 5 | int256 x; 6 | int256 y; 7 | } 8 | 9 | struct RGB { 10 | bytes1 red; 11 | bytes1 green; 12 | bytes1 blue; 13 | } 14 | 15 | struct Bitmap { 16 | bytes data; 17 | uint256 lineSize; 18 | } 19 | 20 | library BitmapLib { 21 | uint256 constant headerSize = 54; 22 | uint256 constant pixelSize = 3; 23 | 24 | function init(Bitmap memory bitmap, XY memory size) internal pure { 25 | uint256 linePadding = (uint256(size.x) * pixelSize) % 4 == 0 26 | ? 0 27 | : 4 - ((uint256(size.x) * pixelSize) % 4); 28 | bitmap.lineSize = uint256(size.x) * pixelSize + linePadding; 29 | uint256 bodySize = bitmap.lineSize * uint256(size.y); 30 | uint256 fileSize = headerSize + bodySize; 31 | bitmap.data = new bytes(fileSize); 32 | 33 | bitmap.data[0] = 0x42; 34 | bitmap.data[1] = 0x4d; 35 | setUint32(bitmap, 2, uint32(fileSize)); 36 | setUint32(bitmap, 10, uint32(headerSize)); 37 | setUint32(bitmap, 14, 40); 38 | setUint32(bitmap, 18, uint32(int32(size.x))); 39 | setUint32(bitmap, 22, uint32(int32(size.y))); 40 | setUint16(bitmap, 26, 1); 41 | setUint16(bitmap, 28, uint16(pixelSize * 8)); 42 | setUint32(bitmap, 34, uint32(bodySize)); 43 | } 44 | 45 | function setPixel( 46 | Bitmap memory bitmap, 47 | XY memory position, 48 | RGB memory pixel 49 | ) internal pure { 50 | uint256 index = headerSize + 51 | uint256(position.y) * 52 | bitmap.lineSize + 53 | uint256(position.x) * 54 | pixelSize; 55 | bitmap.data[index] = pixel.blue; 56 | bitmap.data[index + 1] = pixel.green; 57 | bitmap.data[index + 2] = pixel.red; 58 | } 59 | 60 | function setBody(Bitmap memory bitmap, bytes memory body) internal pure { 61 | uint256 bodyLength = body.length; 62 | require( 63 | bitmap.data.length == headerSize + bodyLength, 64 | "invalid body size" 65 | ); 66 | for (uint256 i = 0; i < bodyLength; i++) { 67 | bitmap.data[headerSize + i] = body[i]; 68 | } 69 | } 70 | 71 | function setUint32( 72 | Bitmap memory bitmap, 73 | uint256 offset, 74 | uint32 value 75 | ) private pure { 76 | for (uint256 i = 0; i < 4; i++) { 77 | bitmap.data[offset + i] = bytes1(uint8(value / (2**(8 * i)))); 78 | } 79 | } 80 | 81 | function setUint16( 82 | Bitmap memory bitmap, 83 | uint256 offset, 84 | uint16 value 85 | ) private pure { 86 | for (uint256 i = 0; i < 2; i++) { 87 | bitmap.data[offset + i] = bytes1(uint8(value / (2**(8 * i)))); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/BitmapNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "@openzeppelin/contracts/access/AccessControl.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 7 | import "base64-sol/base64.sol"; 8 | import "./Bitmap.sol"; 9 | 10 | library RGBLib { 11 | function complementary(RGB memory pixel) 12 | internal 13 | pure 14 | returns (RGB memory) 15 | { 16 | uint256 red = uint256(uint8(pixel.red)); 17 | uint256 green = uint256(uint8(pixel.green)); 18 | uint256 blue = uint256(uint8(pixel.blue)); 19 | uint256 max = red; 20 | uint256 min = red; 21 | if (green > max) { 22 | max = green; 23 | } 24 | if (green < min) { 25 | min = green; 26 | } 27 | if (blue > max) { 28 | max = blue; 29 | } 30 | if (blue < min) { 31 | min = blue; 32 | } 33 | return 34 | RGB( 35 | bytes1(uint8(max + min - red)), 36 | bytes1(uint8(max + min - green)), 37 | bytes1(uint8(max + min - blue)) 38 | ); 39 | } 40 | } 41 | 42 | library PositionLib { 43 | function add( 44 | XY memory position, 45 | int256 x, 46 | int256 y 47 | ) internal pure returns (XY memory) { 48 | return XY(position.x + x, position.y + y); 49 | } 50 | } 51 | 52 | struct PixelFactory { 53 | bytes32 data; 54 | bool random; 55 | int256 step; 56 | RGB from; 57 | RGB to; 58 | } 59 | 60 | library PixelFactoryLib { 61 | using RGBLib for RGB; 62 | 63 | function init( 64 | PixelFactory memory factory, 65 | bytes32 data, 66 | int256 step 67 | ) internal pure { 68 | factory.data = data; 69 | factory.step = step; 70 | factory.random = uint256(data) % 3 == 0; 71 | if (factory.random) { 72 | return; 73 | } 74 | factory.from = RGB(data[0], data[1], data[2]); 75 | uint248 toPattern = uint248(bytes31(data)) % 4; 76 | if (toPattern == 0) { 77 | factory.to = RGB(0, 0, 0); 78 | } else if (toPattern == 1) { 79 | factory.to = RGB(0xff, 0xff, 0xff); 80 | } else if (toPattern == 2) { 81 | factory.to = factory.from.complementary(); 82 | } else { 83 | factory.to = RGB(data[3], data[4], data[5]); 84 | } 85 | if (uint240(bytes30(data)) % 2 == 0) { 86 | RGB memory tmp = factory.from; 87 | factory.from = factory.to; 88 | factory.to = tmp; 89 | } 90 | } 91 | 92 | function create(PixelFactory memory factory, int256 index) 93 | internal 94 | pure 95 | returns (RGB memory) 96 | { 97 | RGB memory pixel; 98 | if (factory.random) { 99 | pixel.blue = factory.data[(uint256(index) * 3) % 32]; 100 | pixel.green = factory.data[(uint256(index) * 3 + 1) % 32]; 101 | pixel.red = factory.data[(uint256(index) * 3 + 2) % 32]; 102 | return pixel; 103 | } 104 | int256 blueDiff = ((int256(uint256(uint8(factory.to.blue))) - 105 | int256(uint256(uint8(factory.from.blue)))) * index) / factory.step; 106 | pixel.blue = bytes1( 107 | uint8(int8(int256(uint256(uint8(factory.from.blue))) + blueDiff)) 108 | ); 109 | int256 greenDiff = ((int256(uint256(uint8(factory.to.green))) - 110 | int256(uint256(uint8(factory.from.green)))) * index) / factory.step; 111 | pixel.green = bytes1( 112 | uint8(int8(int256(uint256(uint8(factory.from.green))) + greenDiff)) 113 | ); 114 | int256 redDiff = ((int256(uint256(uint8(factory.to.red))) - 115 | int256(uint256(uint8(factory.from.red)))) * index) / factory.step; 116 | pixel.red = bytes1( 117 | uint8(int8(int256(uint256(uint8(factory.from.red))) + redDiff)) 118 | ); 119 | return pixel; 120 | } 121 | } 122 | 123 | contract BitmapNFT is ERC721, Ownable { 124 | using BitmapLib for Bitmap; 125 | using PositionLib for XY; 126 | using PixelFactoryLib for PixelFactory; 127 | 128 | constructor() ERC721("BitmapNFT", "BNFT") {} 129 | 130 | function mint(uint256 tokenId) public { 131 | _mint(_msgSender(), tokenId); 132 | } 133 | 134 | function supportsInterface(bytes4 interfaceId) 135 | public 136 | view 137 | override(ERC721) 138 | returns (bool) 139 | { 140 | return super.supportsInterface(interfaceId); 141 | } 142 | 143 | function getBitmapData(uint256 tokenId) public pure returns (bytes memory) { 144 | Bitmap memory bitmap; 145 | bitmap.init(XY(32, 32)); 146 | bytes32 data = keccak256(abi.encodePacked("BitmapNFT", tokenId)); 147 | uint256 bmpPattern = uint256(data) % 6; 148 | if (bmpPattern == 0) { 149 | vertical(bitmap, data); 150 | } else if (bmpPattern == 1) { 151 | horizontal(bitmap, data); 152 | } else if (bmpPattern == 2) { 153 | square(bitmap, data); 154 | } else if (bmpPattern == 3) { 155 | cross(bitmap, data); 156 | } else if (bmpPattern == 4) { 157 | diagonal(bitmap, data); 158 | } else { 159 | diagonal2(bitmap, data); 160 | } 161 | return bitmap.data; 162 | } 163 | 164 | function getBitmapBase64(uint256 tokenId) 165 | public 166 | pure 167 | returns (string memory) 168 | { 169 | return 170 | string( 171 | abi.encodePacked( 172 | "data:image/bmp;base64,", 173 | Base64.encode(getBitmapData(tokenId)) 174 | ) 175 | ); 176 | } 177 | 178 | function getSVG(uint256 tokenId) public pure returns (string memory) { 179 | string memory svg = string( 180 | abi.encodePacked( 181 | '', 182 | '' 185 | ) 186 | ); 187 | return 188 | string( 189 | abi.encodePacked( 190 | "data:image/svg+xml;base64,", 191 | Base64.encode(bytes(svg)) 192 | ) 193 | ); 194 | } 195 | 196 | function tokenURI(uint256 tokenId) 197 | public 198 | pure 199 | override 200 | returns (string memory) 201 | { 202 | string memory metadata = string( 203 | abi.encodePacked( 204 | '{"name": "BitmapNFT #', 205 | toString(tokenId), 206 | '",', 207 | '"description": "BitmapNFT",', 208 | '"image": "data:image/svg+xml;base64,', 209 | Base64.encode(bytes(getSVG(tokenId))), 210 | '"}' 211 | ) 212 | ); 213 | return 214 | string( 215 | abi.encodePacked( 216 | "data:application/json;base64,", 217 | Base64.encode(bytes(metadata)) 218 | ) 219 | ); 220 | } 221 | 222 | function vertical(Bitmap memory bitmap, bytes32 data) internal pure { 223 | PixelFactory memory factory; 224 | factory.init(data, 32); 225 | for (int256 i = 0; i < 32; i++) { 226 | RGB memory pixel = factory.create(i); 227 | for (int256 j = 0; j < 32; j++) { 228 | bitmap.setPixel(XY(i, j), pixel); 229 | } 230 | } 231 | } 232 | 233 | function horizontal(Bitmap memory bitmap, bytes32 data) internal pure { 234 | PixelFactory memory factory; 235 | factory.init(data, 32); 236 | for (int256 i = 0; i < 32; i++) { 237 | RGB memory pixel = factory.create(i); 238 | for (int256 j = 0; j < 32; j++) { 239 | bitmap.setPixel(XY(j, i), pixel); 240 | } 241 | } 242 | } 243 | 244 | function cross(Bitmap memory bitmap, bytes32 data) internal pure { 245 | PixelFactory memory factory; 246 | factory.init(data, 16); 247 | for (int256 i = 0; i < 16; i++) { 248 | RGB memory pixel = factory.create(i); 249 | for (int256 j = 0; j < 32; j++) { 250 | bitmap.setPixel(XY(i, j), pixel); 251 | bitmap.setPixel(XY(31 - i, j), pixel); 252 | bitmap.setPixel(XY(j, i), pixel); 253 | bitmap.setPixel(XY(j, 31 - i), pixel); 254 | } 255 | } 256 | } 257 | 258 | function square(Bitmap memory bitmap, bytes32 data) internal pure { 259 | PixelFactory memory factory; 260 | factory.init(data, 16); 261 | for (int256 i = 15; i >= 0; i--) { 262 | RGB memory pixel = factory.create(i); 263 | for (int256 j = 0; j < 32; j++) { 264 | bitmap.setPixel(XY(i, j), pixel); 265 | bitmap.setPixel(XY(31 - i, j), pixel); 266 | bitmap.setPixel(XY(j, i), pixel); 267 | bitmap.setPixel(XY(j, 31 - i), pixel); 268 | } 269 | } 270 | } 271 | 272 | function diagonal(Bitmap memory bitmap, bytes32 data) internal pure { 273 | PixelFactory memory factory; 274 | factory.init(data, 16); 275 | for (int256 i = 15; i >= 0; i--) { 276 | RGB memory pixel = factory.create(i); 277 | for (int256 j = 0; j < 32 - i; j++) { 278 | bitmap.setPixel(XY(i, 0).add(j, j), pixel); 279 | bitmap.setPixel(XY(0, i).add(j, j), pixel); 280 | bitmap.setPixel(XY(i, 31).add(j, -j), pixel); 281 | bitmap.setPixel(XY(0, 31 - i).add(j, -j), pixel); 282 | } 283 | } 284 | } 285 | 286 | function diagonal2(Bitmap memory bitmap, bytes32 data) internal pure { 287 | PixelFactory memory factory; 288 | factory.init(data, 16); 289 | for (int256 i = 0; i < 16; i++) { 290 | RGB memory pixel = factory.create(i); 291 | for (int256 j = 0; j < 32 - i; j++) { 292 | bitmap.setPixel(XY(i, 0).add(j, j), pixel); 293 | bitmap.setPixel(XY(0, i).add(j, j), pixel); 294 | bitmap.setPixel(XY(i, 31).add(j, -j), pixel); 295 | bitmap.setPixel(XY(0, 31 - i).add(j, -j), pixel); 296 | } 297 | } 298 | } 299 | 300 | function toString(uint256 value) internal pure returns (string memory) { 301 | // Inspired by OraclizeAPI's implementation - MIT license 302 | // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol 303 | 304 | if (value == 0) { 305 | return "0"; 306 | } 307 | uint256 temp = value; 308 | uint256 digits; 309 | while (temp != 0) { 310 | digits++; 311 | temp /= 10; 312 | } 313 | bytes memory buffer = new bytes(digits); 314 | while (value != 0) { 315 | digits -= 1; 316 | buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); 317 | value /= 10; 318 | } 319 | return string(buffer); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config' 2 | 3 | import '@nomiclabs/hardhat-waffle' 4 | import '@nomiclabs/hardhat-etherscan' 5 | import '@typechain/hardhat' 6 | import 'hardhat-gas-reporter' 7 | 8 | // const RINKEBY_PRIVATE_KEY = 'YOUR_KEY' 9 | // const POLYGON_PRIVATE_KEY = 'YOUR_KEY' 10 | // const ETHERSCAN_API_KEY = 'YOUR_KEY' 11 | 12 | const config: HardhatUserConfig = { 13 | defaultNetwork: 'hardhat', 14 | networks: { 15 | hardhat: {}, 16 | // rinkeby: { 17 | // url: 'YOUR_URL', 18 | // chainId: 4, 19 | // gas: 3000000, 20 | // gasPrice: 1000000001, 21 | // timeout: 20000, 22 | // accounts: [`0x${RINKEBY_PRIVATE_KEY}`], 23 | // }, 24 | // polygon: { 25 | // url: 'YOUR_URL', 26 | // chainId: 137, 27 | // accounts: [`0x${POLYGON_PRIVATE_KEY}`], 28 | // }, 29 | }, 30 | solidity: { 31 | version: '0.8.4', 32 | settings: { 33 | optimizer: { 34 | enabled: true, 35 | runs: 200, 36 | }, 37 | }, 38 | }, 39 | gasReporter: { 40 | enabled: true, 41 | currency: 'ETH', 42 | gasPrice: 100, 43 | }, 44 | // etherscan: { 45 | // apiKey: `${ETHERSCAN_API_KEY}`, 46 | // }, 47 | } 48 | 49 | export default config 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-bitmap", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@nomiclabs/hardhat-ethers": "^2.0.2", 14 | "@nomiclabs/hardhat-etherscan": "^2.1.6", 15 | "@nomiclabs/hardhat-waffle": "^2.0.1", 16 | "@typechain/ethers-v5": "^7.1.0", 17 | "@typechain/hardhat": "^2.3.0", 18 | "@types/chai": "^4.2.22", 19 | "@types/classnames": "^2.3.1", 20 | "@types/mocha": "^9.0.0", 21 | "@types/react": "^17.0.24", 22 | "chai": "^4.3.4", 23 | "ethereum-waffle": "^3.4.0", 24 | "ethers": "^5.4.7", 25 | "hardhat": "^2.7.0", 26 | "hardhat-gas-reporter": "^1.0.4", 27 | "ts-node": "^10.4.0", 28 | "typechain": "^5.1.2", 29 | "typescript": "^4.4.3" 30 | }, 31 | "dependencies": { 32 | "@openzeppelin/contracts": "^4.3.2", 33 | "base64-sol": "github:Brechtpd/base64" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/doublejumptokyo/solidity-bitmap.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/doublejumptokyo/solidity-bitmap/issues" 41 | }, 42 | "homepage": "https://github.com/doublejumptokyo/solidity-bitmap#readme" 43 | } 44 | -------------------------------------------------------------------------------- /scripts/deploy/BitmapNFT.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | 3 | const main = async () => { 4 | const [owner] = await ethers.getSigners() 5 | 6 | console.log("Deploying contracts with the account:", owner.address) 7 | console.log("Account balance:", (await owner.getBalance()).toString()) 8 | 9 | const BitmapNFT = await ethers.getContractFactory("BitmapNFT") 10 | const bitmapNFT = await BitmapNFT.connect(owner).deploy() 11 | await bitmapNFT.deployed() 12 | 13 | console.log("Token address:", bitmapNFT.address) 14 | } 15 | 16 | main() 17 | .then(() => process.exit(0)) 18 | .catch((error) => { 19 | console.error(error); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /scripts/verify/Verify.ts: -------------------------------------------------------------------------------- 1 | import hardhat from 'hardhat' 2 | import verify from './verify.json' 3 | 4 | const main = async () => { 5 | try { 6 | await hardhat.run("verify:verify", verify) 7 | } catch (e) { 8 | if (e.message == "Missing or invalid ApiKey") { 9 | console.log("Skip verifing with", e.message) 10 | return 11 | } 12 | if (e.message == "Contract source code already verified") { 13 | console.log("Skip verifing with", e.message) 14 | return 15 | } 16 | throw e 17 | } 18 | } 19 | 20 | main() 21 | .then(() => process.exit(0)) 22 | .catch((error) => { 23 | console.error(error); 24 | process.exit(1); 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/verify/verify.json: -------------------------------------------------------------------------------- 1 | { 2 | "contract": "contracts/BitmapNFT.sol:BitmapNFT", 3 | "address": "", 4 | "constructorArguments": [] 5 | } 6 | -------------------------------------------------------------------------------- /test/BitmapNFT.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' 3 | import { BitmapNFT } from 'typechain' 4 | 5 | describe('Token contract', function () { 6 | let bmpnft: BitmapNFT 7 | let owner: SignerWithAddress 8 | 9 | describe('Test', async () => { 10 | it('Test', async () => { 11 | [owner] = await ethers.getSigners() 12 | bmpnft = await (await ethers.getContractFactory('BitmapNFT')).connect(owner).deploy() as BitmapNFT 13 | await bmpnft.deployed() 14 | console.log(await bmpnft.getSVG(1)) 15 | console.log(await bmpnft.tokenURI(1)) 16 | // console.log(await bmpnft.getSVG(2)) 17 | // console.log(await bmpnft.getSVG(3)) 18 | // console.log(await bmpnft.getSVG(4)) 19 | // console.log(await bmpnft.getSVG(5)) 20 | // console.log(await bmpnft.getSVG(6)) 21 | // console.log(await bmpnft.getSVG(7)) 22 | // console.log(await bmpnft.getSVG(8)) 23 | // console.log(await bmpnft.getSVG(9)) 24 | // console.log(await bmpnft.getSVG(10)) 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "baseUrl": "./", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "paths": { 18 | "@/*": ["src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | --------------------------------------------------------------------------------