├── .github └── workflows │ ├── pull-request.yml │ └── storybook.yml ├── .gitignore ├── .npmignore ├── .storybook ├── main.js └── preview.js ├── LICENSE ├── README.md ├── package.json ├── src ├── Jazzicon.tsx ├── Paper.tsx ├── colorUtils.ts ├── colors.ts ├── index.tsx ├── jsNumberForAddress.ts └── stories │ └── Jazzicon.stories.tsx ├── tsconfig.json └── yarn.lock /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | pull-request: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: "16.x" 13 | registry-url: "https://npm.pkg.github.com" 14 | scope: "@marcusmolchany" 15 | - run: yarn 16 | - run: yarn test:types 17 | -------------------------------------------------------------------------------- /.github/workflows/storybook.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | paths: ["src/**"] # Trigger the action only when files change in the folders defined here 5 | jobs: 6 | build-and-deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 🛎️ 10 | uses: actions/checkout@v2.3.1 11 | with: 12 | persist-credentials: false 13 | - name: Install and Build 🔧 14 | run: | # Install yarn packages and build the Storybook files 15 | yarn 16 | yarn build-storybook 17 | - name: Deploy 🚀 18 | uses: JamesIves/github-pages-deploy-action@3.6.2 19 | with: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | BRANCH: gh-pages # The branch the action should deploy to. 22 | FOLDER: docs-build # The folder that the build-storybook script generates files. 23 | CLEAN: true # Automatically remove deleted files from the deploy branch 24 | TARGET_FOLDER: docs # The folder that we serve our Storybook files from 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore compiled dist files 2 | dist 3 | 4 | # ignore typescript tsbuildinfo 5 | tsconfig.tsbuildinfo 6 | 7 | # ignore mac system file 8 | .DS_Store 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Typescript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | tsconfig.json 4 | tsconfig.tsbuildinfo 5 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: [ 3 | "../src/stories/**/*.stories.mdx", 4 | "../src/stories/**/*.stories.@(js|jsx|ts|tsx)", 5 | ], 6 | addons: ["@storybook/addon-links", "@storybook/addon-essentials"], 7 | framework: "@storybook/react", 8 | }; 9 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Marcus Molchany 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 | # react-jazzicon 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![License][license-image]][license-url] 5 | [![Known Vulnerabilities][snyk-image]][snyk-url] 6 | [![Downloads][downloads-image]][downloads-url] 7 | 8 | This is a react component for [Dan Finlay's](https://github.com/danfinlay) 9 | [jazzicon](https://github.com/danfinlay/jazzicon). 10 | 11 | # usage 12 | 13 | ```js 14 | import Jazzicon from 'react-jazzicon' 15 | 16 | export default function App(){ 17 | 18 | … 19 | 20 | return ( 21 | 22 | ) 23 | } 24 | ``` 25 | 26 | for Ethereum addresses 27 | 28 | ```js 29 | import Jazzicon, { jsNumberForAddress } from 'react-jazzicon' 30 | 31 | export default function App () { 32 | 33 | … 34 | 35 | return ( 36 | 37 | ) 38 | } 39 | ``` 40 | 41 | # setup 42 | 43 | ```sh 44 | $ git clone https://github.com/marcusmolchany/react-jazzicon 45 | $ cd react-jazzicon 46 | $ yarn # or npm i 47 | ``` 48 | 49 | ## storybooks 50 | 51 | the storybooks github pages are hosted from the `/docs` directory on the `gh-pages` branch. 52 | 53 | run the storybooks locally by running the following commands: 54 | 55 | ```sh 56 | $ yarn # or npm i 57 | $ yarn storybooks # or npm run storybooks 58 | ``` 59 | 60 | [npm-image]: https://img.shields.io/npm/v/react-jazzicon.svg?style=for-the-badge&labelColor=161c22 61 | [npm-url]: https://www.npmjs.com/package/react-jazzicon 62 | [license-image]: https://img.shields.io/npm/l/react-jazzicon.svg?style=for-the-badge&labelColor=161c22 63 | [license-url]: /LICENSE 64 | [snyk-image]: https://snyk.io/test/github/marcusmolchany/react-jazzicon/badge.svg?targetFile=package.json 65 | [snyk-url]: https://snyk.io/test/github/marcusmolchany/react-jazzicon?targetFile=package.json 66 | [downloads-image]: https://img.shields.io/npm/dm/react-jazzicon.svg?style=for-the-badge&labelColor=161c22 67 | [downloads-url]: https://www.npmjs.com/package/react-jazzicon 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-jazzicon", 3 | "version": "1.0.4", 4 | "description": "React component for danfinlay/jazzicon", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "npm run clean && ./node_modules/.bin/tsc", 12 | "clean": "rimraf dist && rimraf tsconfig.tsbuildinfo", 13 | "prepublish": "npm run build", 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "test:types": "./node_modules/.bin/tsc --noEmit", 16 | "storybook": "start-storybook -p 6006", 17 | "build-storybook": "build-storybook -o docs-build" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/marcusmolchany/react-jazzicon.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/marcusmolchany/react-jazzicon/issues" 25 | }, 26 | "keywords": [ 27 | "jazzicon", 28 | "react", 29 | "ethereum", 30 | "metamask" 31 | ], 32 | "author": "marcusmolchany", 33 | "license": "MIT", 34 | "dependencies": { 35 | "mersenne-twister": "^1.1.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.17.0", 39 | "@storybook/addon-actions": "^6.4.18", 40 | "@storybook/addon-essentials": "^6.4.18", 41 | "@storybook/addon-links": "^6.4.18", 42 | "@storybook/react": "^6.4.18", 43 | "@types/mersenne-twister": "^1.1.2", 44 | "babel-loader": "^8.2.3", 45 | "react": "^17.0.2", 46 | "react-dom": "^17.0.2", 47 | "rimraf": "^3.0.2", 48 | "typescript": "^4.5.5" 49 | }, 50 | "peerDependencies": { 51 | "react": ">=17.0.0", 52 | "react-dom": ">=17.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Jazzicon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import MersenneTwister from "mersenne-twister"; 3 | 4 | import { colorRotate } from "./colorUtils"; 5 | import colors from "./colors"; 6 | import Paper from "./Paper"; 7 | 8 | // constants 9 | const shapeCount = 4; 10 | const svgns = "http://www.w3.org/2000/svg"; 11 | const wobble = 30; 12 | const defaultDiameter = 24; 13 | 14 | type JazziconProps = { 15 | diameter?: number; 16 | paperStyles?: object; 17 | seed?: number; 18 | svgStyles?: object; 19 | }; 20 | 21 | type Colors = Array; 22 | 23 | export default class Jazzicon extends React.PureComponent { 24 | generator: MersenneTwister; 25 | props: JazziconProps; 26 | 27 | genColor = (colors: Colors): string => { 28 | // @ts-ignore 29 | const rand = this.generator.random(); // purposefully call the generator once, before using it again on the next line 30 | const idx = Math.floor(colors.length * this.generator.random()); 31 | const color = colors.splice(idx, 1)[0]; 32 | return color; 33 | }; 34 | 35 | hueShift = (colors: Colors, generator: MersenneTwister): Array => { 36 | const amount = generator.random() * 30 - wobble / 2; 37 | const rotate = (hex: string) => colorRotate(hex, amount); 38 | return colors.map(rotate); 39 | }; 40 | 41 | genShape = ( 42 | remainingColors: Colors, 43 | diameter: number, 44 | i: number, 45 | total: number 46 | ) => { 47 | const center = diameter / 2; 48 | const firstRot = this.generator.random(); 49 | const angle = Math.PI * 2 * firstRot; 50 | const velocity = 51 | (diameter / total) * this.generator.random() + (i * diameter) / total; 52 | const tx = Math.cos(angle) * velocity; 53 | const ty = Math.sin(angle) * velocity; 54 | const translate = "translate(" + tx + " " + ty + ")"; 55 | 56 | // Third random is a shape rotation on top of all of that. 57 | const secondRot = this.generator.random(); 58 | const rot = firstRot * 360 + secondRot * 180; 59 | const rotate = 60 | "rotate(" + rot.toFixed(1) + " " + center + " " + center + ")"; 61 | const transform = translate + " " + rotate; 62 | const fill = this.genColor(remainingColors); 63 | 64 | return ( 65 | 76 | ); 77 | }; 78 | 79 | render() { 80 | const { 81 | diameter = defaultDiameter, 82 | paperStyles = {}, 83 | seed, 84 | svgStyles = {}, 85 | } = this.props; 86 | 87 | this.generator = new MersenneTwister(seed); 88 | 89 | const remainingColors = this.hueShift(colors.slice(), this.generator); 90 | const shapesArr = Array(shapeCount).fill(undefined); 91 | 92 | return ( 93 | 98 | 106 | {shapesArr.map((s, i) => 107 | this.genShape(remainingColors, diameter, i, shapeCount - 1) 108 | )} 109 | 110 | 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Paper.tsx: -------------------------------------------------------------------------------- 1 | const styles = { 2 | borderRadius: "50px", 3 | display: "inline-block", 4 | margin: 0, 5 | overflow: "hidden", 6 | padding: 0, 7 | }; 8 | 9 | type PaperProps = { 10 | children: any; 11 | color: string; 12 | diameter: number; 13 | style: object; 14 | }; 15 | 16 | const Paper = ({ 17 | children, 18 | color, 19 | diameter, 20 | style: styleOverrides, 21 | }: PaperProps) => ( 22 |
32 | {children} 33 |
34 | ); 35 | 36 | export default Paper; 37 | -------------------------------------------------------------------------------- /src/colorUtils.ts: -------------------------------------------------------------------------------- 1 | // from: https://github.com/MetaMask/jazzicon/blob/master/index.js 2 | 3 | type HSL = { h: number; s: number; l: number }; 4 | 5 | export const colorRotate = (hex: string, degrees: number) => { 6 | let hsl = hexToHSL(hex); 7 | let hue = hsl.h; 8 | hue = (hue + degrees) % 360; 9 | hue = hue < 0 ? 360 + hue : hue; 10 | hsl.h = hue; 11 | return HSLToHex(hsl); 12 | }; 13 | 14 | export const hexToHSL = (hex: string): HSL => { 15 | // Convert hex to RGB first 16 | const rStr = "0x" + hex[1] + hex[2]; 17 | const gStr = "0x" + hex[3] + hex[4]; 18 | const bStr = "0x" + hex[5] + hex[6]; 19 | // Then to HSL 20 | const r = parseInt(rStr) / 255; 21 | const g = parseInt(gStr) / 255; 22 | const b = parseInt(bStr) / 255; 23 | let cmin = Math.min(r, g, b), 24 | cmax = Math.max(r, g, b), 25 | delta = cmax - cmin, 26 | h = 0, 27 | s = 0, 28 | l = 0; 29 | 30 | if (delta == 0) h = 0; 31 | else if (cmax == r) h = ((g - b) / delta) % 6; 32 | else if (cmax == g) h = (b - r) / delta + 2; 33 | else h = (r - g) / delta + 4; 34 | 35 | h = Math.round(h * 60); 36 | 37 | if (h < 0) h += 360; 38 | 39 | l = (cmax + cmin) / 2; 40 | s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); 41 | s = +(s * 100).toFixed(1); 42 | l = +(l * 100).toFixed(1); 43 | 44 | return { h, s, l }; 45 | }; 46 | 47 | export const HSLToHex = (hsl: HSL): string => { 48 | let { h, s, l } = hsl; 49 | s /= 100; 50 | l /= 100; 51 | 52 | let c = (1 - Math.abs(2 * l - 1)) * s, 53 | x = c * (1 - Math.abs(((h / 60) % 2) - 1)), 54 | m = l - c / 2, 55 | r = 0, 56 | g = 0, 57 | b = 0; 58 | 59 | if (0 <= h && h < 60) { 60 | r = c; 61 | g = x; 62 | b = 0; 63 | } else if (60 <= h && h < 120) { 64 | r = x; 65 | g = c; 66 | b = 0; 67 | } else if (120 <= h && h < 180) { 68 | r = 0; 69 | g = c; 70 | b = x; 71 | } else if (180 <= h && h < 240) { 72 | r = 0; 73 | g = x; 74 | b = c; 75 | } else if (240 <= h && h < 300) { 76 | r = x; 77 | g = 0; 78 | b = c; 79 | } else if (300 <= h && h < 360) { 80 | r = c; 81 | g = 0; 82 | b = x; 83 | } 84 | // Having obtained RGB, convert channels to hex 85 | let rStr = Math.round((r + m) * 255).toString(16); 86 | let gStr = Math.round((g + m) * 255).toString(16); 87 | let bStr = Math.round((b + m) * 255).toString(16); 88 | 89 | // Prepend 0s, if necessary 90 | if (rStr.length == 1) rStr = "0" + rStr; 91 | if (gStr.length == 1) gStr = "0" + gStr; 92 | if (bStr.length == 1) bStr = "0" + bStr; 93 | 94 | return "#" + rStr + gStr + bStr; 95 | }; 96 | -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | export default Object.freeze([ 2 | "#01888c", // teal 3 | "#fc7500", // bright orange 4 | "#034f5d", // dark teal 5 | "#f73f01", // orangered 6 | "#fc1960", // magenta 7 | "#c7144c", // raspberry 8 | "#f3c100", // goldenrod 9 | "#1598f2", // lightning blue 10 | "#2465e1", // sail blue 11 | "#f19e02", // gold 12 | ]); 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "./Jazzicon"; 2 | export { default as jsNumberForAddress } from "./jsNumberForAddress"; 3 | -------------------------------------------------------------------------------- /src/jsNumberForAddress.ts: -------------------------------------------------------------------------------- 1 | export default function jsNumberForAddress(address: string): number { 2 | const addr = address.slice(2, 10); 3 | const seed = parseInt(addr, 16); 4 | 5 | return seed; 6 | } 7 | -------------------------------------------------------------------------------- /src/stories/Jazzicon.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from "@storybook/react"; 2 | 3 | import Jazzicon, { jsNumberForAddress } from ".."; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | export default { 7 | title: "Jazzicon", 8 | component: Jazzicon, 9 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 10 | argTypes: { 11 | diameter: { 12 | control: "number", 13 | }, 14 | seed: { 15 | control: "text", 16 | }, 17 | }, 18 | } as ComponentMeta; 19 | 20 | // // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 21 | const Template: ComponentStory = (args) => ( 22 | 23 | ); 24 | 25 | export const Primary = Template.bind({}); 26 | // More on args: https://storybook.js.org/docs/react/writing-stories/args 27 | Primary.args = { 28 | diameter: 100, 29 | seed: "0x1111111111111111111111111111111111111111", 30 | }; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "jsx": "react-jsx", 10 | "lib": ["es2015", "es5", "dom"], 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "noEmitOnError": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noUnusedLocals": true, 18 | "outDir": "dist", 19 | "preserveSymlinks": true, 20 | "preserveWatchOutput": true, 21 | "pretty": false, 22 | "rootDir": "src", 23 | "sourceMap": true, 24 | "strictNullChecks": true, 25 | "target": "es5", 26 | "types": ["node"] 27 | }, 28 | "exclude": ["node_modules", "dist", "src/stories"], 29 | "include": ["src"] 30 | } 31 | --------------------------------------------------------------------------------