├── .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 |
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 |
--------------------------------------------------------------------------------