├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .prettierrc.js ├── .stylelintrc.js ├── LICENSE ├── README-EN.md ├── README.md ├── babel.config.js ├── images └── social-preview-1.png ├── index.html ├── jest.config.js ├── landing-page ├── avatar-1.png ├── avatar-2.png ├── avatar-3.png ├── landing-page.html ├── preview.png ├── ts.svg ├── vite.svg └── vue.svg ├── package.json ├── public └── favicon.svg ├── src ├── App.vue ├── __tests__ │ └── utils.test.ts ├── assets │ ├── icons │ │ ├── icon-back.svg │ │ ├── icon-close.svg │ │ ├── icon-code.svg │ │ ├── icon-flip.svg │ │ ├── icon-github.svg │ │ ├── icon-next.svg │ │ └── icon-right.svg │ ├── logo.svg │ ├── preview │ │ ├── beard │ │ │ └── scruff.svg │ │ ├── clothes │ │ │ ├── collared.svg │ │ │ ├── crew.svg │ │ │ └── open.svg │ │ ├── ear │ │ │ ├── attached.svg │ │ │ └── detached.svg │ │ ├── earrings │ │ │ ├── hoop.svg │ │ │ └── stud.svg │ │ ├── eyebrows │ │ │ ├── down.svg │ │ │ ├── eyelashesdown.svg │ │ │ ├── eyelashesup.svg │ │ │ └── up.svg │ │ ├── eyes │ │ │ ├── ellipse.svg │ │ │ ├── eyeshadow.svg │ │ │ ├── round.svg │ │ │ └── smiling.svg │ │ ├── face │ │ │ └── base.svg │ │ ├── glasses │ │ │ ├── round.svg │ │ │ └── square.svg │ │ ├── mouth │ │ │ ├── frown.svg │ │ │ ├── laughing.svg │ │ │ ├── nervous.svg │ │ │ ├── pucker.svg │ │ │ ├── sad.svg │ │ │ ├── smile.svg │ │ │ ├── smirk.svg │ │ │ └── surprised.svg │ │ ├── nose │ │ │ ├── curve.svg │ │ │ ├── pointed.svg │ │ │ └── round.svg │ │ └── tops │ │ │ ├── beanie.svg │ │ │ ├── clean.svg │ │ │ ├── danny.svg │ │ │ ├── fonze.svg │ │ │ ├── funny.svg │ │ │ ├── pixie.svg │ │ │ ├── punk.svg │ │ │ ├── turban.svg │ │ │ └── wave.svg │ └── widgets │ │ ├── beard │ │ └── scruff.svg │ │ ├── clothes │ │ ├── collared.svg │ │ ├── crew.svg │ │ └── open.svg │ │ ├── ear │ │ ├── attached.svg │ │ └── detached.svg │ │ ├── earrings │ │ ├── hoop.svg │ │ └── stud.svg │ │ ├── eyebrows │ │ ├── down.svg │ │ ├── eyelashesdown.svg │ │ ├── eyelashesup.svg │ │ └── up.svg │ │ ├── eyes │ │ ├── ellipse.svg │ │ ├── eyeshadow.svg │ │ ├── round.svg │ │ └── smiling.svg │ │ ├── face │ │ └── base.svg │ │ ├── glasses │ │ ├── round.svg │ │ └── square.svg │ │ ├── mouth │ │ ├── frown.svg │ │ ├── laughing.svg │ │ ├── nervous.svg │ │ ├── pucker.svg │ │ ├── sad.svg │ │ ├── smile.svg │ │ ├── smirk.svg │ │ └── surprised.svg │ │ ├── nose │ │ ├── curve.svg │ │ ├── pointed.svg │ │ └── round.svg │ │ └── tops │ │ ├── beanie.svg │ │ ├── clean.svg │ │ ├── danny.svg │ │ ├── fonze.svg │ │ ├── funny.svg │ │ ├── pixie.svg │ │ ├── punk.svg │ │ ├── turban.svg │ │ └── wave.svg ├── components │ ├── ActionBar.vue │ ├── CodeModal.vue │ ├── ConfettiCanvas.vue │ ├── Configurator.vue │ ├── DownloadModal.vue │ ├── Logo.vue │ ├── PerfectScrollbar.vue │ ├── SectionWrapper.vue │ ├── VueColorAvatar.vue │ └── widgets │ │ └── Background.vue ├── enums │ └── index.ts ├── env.d.ts ├── hooks │ ├── index.ts │ ├── useAvatarOption.ts │ └── useSider.ts ├── i18n │ ├── index.ts │ └── locales │ │ ├── en │ │ └── index.ts │ │ └── zh │ │ └── index.ts ├── layouts │ ├── Container.vue │ ├── Footer.vue │ ├── Header.vue │ └── Sider.vue ├── main.ts ├── store │ ├── index.ts │ └── mutation-type.ts ├── styles │ ├── global.scss │ ├── reset.css │ └── var.scss ├── types │ └── index.ts └── utils │ ├── constant.ts │ ├── dynamic-data.ts │ ├── ga.ts │ └── index.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | jest: true, 6 | }, 7 | globals: { 8 | defineProps: 'readonly', 9 | defineEmits: 'readonly', 10 | defineExpose: 'readonly', 11 | withDefaults: 'readonly', 12 | }, 13 | parser: 'vue-eslint-parser', 14 | parserOptions: { 15 | parser: '@typescript-eslint/parser', 16 | ecmaVersion: 2020, 17 | sourceType: 'module', 18 | ecmaFeatures: { 19 | tsx: true, 20 | }, 21 | }, 22 | extends: [ 23 | 'eslint:recommended', 24 | 'plugin:import/recommended', 25 | 'plugin:import/typescript', 26 | 'plugin:@typescript-eslint/recommended', 27 | 'plugin:vue/vue3-recommended', 28 | 'plugin:prettier/recommended', 29 | ], 30 | plugins: ['simple-import-sort'], 31 | rules: { 32 | 'vue/no-v-html': 0, 33 | 'simple-import-sort/imports': 1, 34 | 'simple-import-sort/exports': 1, 35 | 'sort-imports': 0, 36 | 'import/order': 0, 37 | 'import/no-unresolved': [ 38 | 2, 39 | { 40 | ignore: ['^@/', '^@@/'], 41 | }, 42 | ], 43 | 'vue/no-unused-vars': 1, 44 | '@typescript-eslint/explicit-module-boundary-types': 0, 45 | '@typescript-eslint/consistent-type-imports': 1, 46 | '@typescript-eslint/no-non-null-assertion': 0, 47 | }, 48 | ignorePatterns: [ 49 | 'dist', 50 | 'public', 51 | '!.eslintrc.js', 52 | '!.prettierrc.js', 53 | '!.stylelintrc.js', 54 | '!.lintstagedrc.js', 55 | ], 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | yarn-error.log 7 | stats.html 8 | coverage -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{vue,js,jsx,ts,tsx}': 'eslint --fix', 3 | '*.{vue,css,less,scss}': 'stylelint --fix', 4 | '*.{md,json,html}': 'prettier --write', 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: true, 4 | bracketSameLine: false, 5 | jsxSingleQuote: false, 6 | printWidth: 80, 7 | quoteProps: 'as-needed', 8 | rangeStart: 0, 9 | rangeEnd: Infinity, 10 | semi: false, 11 | singleQuote: true, 12 | tabWidth: 2, 13 | trailingComma: 'es5', 14 | useTabs: false, 15 | endOfLine: 'auto', 16 | } 17 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-recommended', 4 | 'stylelint-prettier/recommended', 5 | 'stylelint-config-rational-order', 6 | ], 7 | 8 | plugins: ['stylelint-scss', 'stylelint-order'], 9 | 10 | rules: { 11 | 'at-rule-no-unknown': null, 12 | 'no-irregular-whitespace': null, 13 | 'scss/at-rule-no-unknown': [ 14 | true, 15 | { 16 | ignoreAtRules: ['tailwind'], 17 | }, 18 | ], 19 | 'font-family-no-missing-generic-family-keyword': [ 20 | true, 21 | { ignoreFontFamilies: ['Fallback'] }, 22 | ], 23 | 'selector-pseudo-class-no-unknown': [ 24 | true, 25 | { ignorePseudoClasses: ['deep'] }, 26 | ], 27 | }, 28 | 29 | ignoreFiles: ['dist/**/*.css'], 30 | 31 | overrides: [ 32 | { 33 | files: ['**/*.vue'], 34 | customSyntax: 'postcss-html', 35 | }, 36 | { 37 | files: ['**/*.scss'], 38 | customSyntax: 'postcss-scss', 39 | }, 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LeoKu 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-EN.md: -------------------------------------------------------------------------------- 1 |
2 |

Vue Color Avatar

3 | 4 |

🧑‍🦱 A front-end only avatar generation website 🧑‍🦳

5 | 6 | [简体中文](./README.md) 7 | 8 |
9 | 10 | 11 | website-preview 12 | 13 | 14 | ## Preview 15 | 16 | [`https://vue-color-avatar.vercel.app`](https://vue-color-avatar.vercel.app) 17 | 18 | ## Introduction 19 | 20 | **By swapping components around, you can build your own avatar.** 21 | 22 | Features you might be interested in: 23 | 24 | - Visual component configuration bar 25 | - Randomly generate an avatar 26 | - Redo/Undo 27 | - i18n 28 | 29 | ## Assets 30 | 31 | Implementation of [Avatar Illustration System](https://www.figma.com/community/file/829741575478342595) by Micah Lanier. Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). 32 | 33 | ## Develop 34 | 35 | This project is implemented using `Vue3`. 36 | 37 | ```sh 38 | # 1. Clone project 39 | git clone https://github.com/Codennnn/vue-color-avatar.git 40 | 41 | # 2. Install dependencies 42 | yarn install 43 | 44 | # 3. Run 45 | yarn dev 46 | ``` 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Vue Color Avatar

3 | 4 |

🧑‍🦱 一个纯前端实现的头像生成网站 🧑‍🦳

5 | 6 | [Read In English](./README-EN.md) 7 | 8 |
9 | 10 | 11 | website-preview 12 | 13 | 14 | ## 在线预览 15 | 16 | [`https://vue-color-avatar.vercel.app`](https://vue-color-avatar.vercel.app) 17 | 18 | ## 介绍 19 | 20 | **这是一款矢量风格头像的生成器,你可以搭配不同的素材组件,生成自己的个性化头像。** 21 | 22 | 你可能感兴趣的功能: 23 | 24 | - 可视化组件配置栏 25 | - 随机生成头像,有一定概率触发彩蛋 26 | - 撤销/还原*更改* 27 | - 国际化多语言 28 | 29 | ## 设计资源 30 | 31 | - 设计师:[@Micah](https://www.figma.com/@Micah) on Figma 32 | - 素材来源:[Avatar Illustration System](https://www.figma.com/community/file/829741575478342595) 33 | 34 | > 请注意,虽然该项目是 MIT 协议,但是素材资源基于 [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) 协议。如果你有好的创意素材,欢迎补充! 35 | 36 | ## 项目开发 37 | 38 | 该项目使用 `Vue3` + `Vite` 进行开发。 39 | 40 | ```sh 41 | # 1. 克隆项目至本地 42 | git clone https://github.com/Codennnn/vue-color-avatar.git 43 | 44 | # 2. 安装项目依赖 45 | yarn install 46 | 47 | # 3. 运行项目 48 | yarn dev 49 | ``` 50 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /images/social-preview-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DsTansice/vue-color-avatar/3cd7053864725d30477d8d6417e365c117402a67/images/social-preview-1.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 29 | 30 | 31 | Vue Color Avatar 32 | 33 | 34 | 38 | 47 | 48 | 119 | 120 | 121 | 122 |
123 |
124 | 125 |
Coming soon...
126 |
127 |
128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | moduleNameMapper: { 8 | '^@/(.*)$': '/src/$1', 9 | }, 10 | 11 | clearMocks: true, 12 | 13 | collectCoverage: false, 14 | 15 | coverageDirectory: 'coverage', 16 | 17 | testEnvironment: 'jsdom', 18 | } 19 | -------------------------------------------------------------------------------- /landing-page/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DsTansice/vue-color-avatar/3cd7053864725d30477d8d6417e365c117402a67/landing-page/avatar-1.png -------------------------------------------------------------------------------- /landing-page/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DsTansice/vue-color-avatar/3cd7053864725d30477d8d6417e365c117402a67/landing-page/avatar-2.png -------------------------------------------------------------------------------- /landing-page/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DsTansice/vue-color-avatar/3cd7053864725d30477d8d6417e365c117402a67/landing-page/avatar-3.png -------------------------------------------------------------------------------- /landing-page/landing-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 47 | 48 | Vue Color Avatar 49 | 50 | 51 | 52 |
53 |
54 |
64 |
65 |
66 | 67 |
68 |
Color Avatar
69 |
70 | 71 |
80 | 81 | 82 | 83 |
84 |
85 | 86 |
87 |

97 | Front-End Only 98 | Avatar Generator 107 |

108 |
116 | 117 | 118 | 119 |
120 | 121 |
122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 |
130 | 131 | 132 | -------------------------------------------------------------------------------- /landing-page/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DsTansice/vue-color-avatar/3cd7053864725d30477d8d6417e365c117402a67/landing-page/preview.png -------------------------------------------------------------------------------- /landing-page/ts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /landing-page/vite.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 16 | 17 | 25 | 26 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /landing-page/vue.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OSKYUavatar", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "author": "PinYoung (https://0skyu.cn)", 7 | "scripts": { 8 | "build": "npm run test && vite build", 9 | "build:prerelease": "vite build --mode prerelease", 10 | "deps": "yarn upgrade-interactive --latest", 11 | "dev": "vite", 12 | "lint": "yarn lint:es && yarn lint:style && yarn lint:ts", 13 | "lint:es": "eslint \"src/**/*.{js,jsx,ts,tsx,vue}\"", 14 | "lint:prettier": "prettier --write \"src/**/*.{md,json,html}\"", 15 | "lint:style": "stylelint \"src/**/*.{css,scss,vue}\"", 16 | "lint:ts": "tsc --noEmit --skipLibCheck", 17 | "prepare": "husky install", 18 | "preview": "vite preview", 19 | "test": "jest" 20 | }, 21 | "dependencies": { 22 | "canvas-confetti": "^1.4.0", 23 | "clipboard": "^2.0.8", 24 | "html2canvas": "^1.3.2", 25 | "perfect-scrollbar": "^1.5.2", 26 | "vue": "^3.2.6", 27 | "vue-i18n": "^9.2.0-beta.9", 28 | "vuex": "^4.0.2" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.15.8", 32 | "@babel/preset-env": "^7.15.8", 33 | "@babel/preset-typescript": "^7.15.0", 34 | "@types/canvas-confetti": "^1.4.2", 35 | "@types/jest": "^27.0.2", 36 | "@typescript-eslint/eslint-plugin": "^5.2.0", 37 | "@typescript-eslint/parser": "^5.2.0", 38 | "@vitejs/plugin-vue": "^1.9.2", 39 | "@vue/compiler-sfc": "^3.2.19", 40 | "babel-jest": "^27.2.5", 41 | "eslint": "^8.1.0", 42 | "eslint-config-prettier": "^8.3.0", 43 | "eslint-plugin-import": "^2.25.2", 44 | "eslint-plugin-prettier": "^4.0.0", 45 | "eslint-plugin-simple-import-sort": "^7.0.0", 46 | "eslint-plugin-vue": "^8.0.3", 47 | "husky": "^7.0.2", 48 | "jest": "^27.2.5", 49 | "lint-staged": "^11.1.2", 50 | "prettier": "^2.4.1", 51 | "rollup-plugin-visualizer": "^5.5.2", 52 | "sass": "^1.42.1", 53 | "stylelint": "^14.0.1", 54 | "stylelint-config-prettier": "^9.0.3", 55 | "stylelint-config-rational-order": "^0.1.2", 56 | "stylelint-config-recommended": "^6.0.0", 57 | "stylelint-order": "^5.0.0", 58 | "stylelint-prettier": "^1.2.0", 59 | "stylelint-scss": "^4.0.0", 60 | "typescript": "^4.4.3", 61 | "vite": "^2.6.2", 62 | "vue-tsc": "^0.3.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 208 | 209 | 327 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import localeEN from '../i18n/locales/en' 2 | import localeZH from '../i18n/locales/zh' 3 | import { highlightJSON } from '../utils' 4 | 5 | test('highlightJSON', () => { 6 | const str = JSON.stringify({ a: 1, b: '2' }) 7 | expect(highlightJSON(str)).toMatch('key') 8 | expect(highlightJSON(str)).toMatch('number') 9 | expect(highlightJSON(str)).toMatch('string') 10 | }) 11 | 12 | test('check locales completeness', () => { 13 | const zh = Reflect.ownKeys(localeZH).sort() 14 | const en = Reflect.ownKeys(localeEN).sort() 15 | expect(zh).toEqual(en) 16 | }) 17 | -------------------------------------------------------------------------------- /src/assets/icons/icon-back.svg: -------------------------------------------------------------------------------- 1 | 8 | 14 | 23 | -------------------------------------------------------------------------------- /src/assets/icons/icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 15 | 22 | 29 | -------------------------------------------------------------------------------- /src/assets/icons/icon-code.svg: -------------------------------------------------------------------------------- 1 | 8 | 15 | 22 | 28 | -------------------------------------------------------------------------------- /src/assets/icons/icon-flip.svg: -------------------------------------------------------------------------------- 1 | 8 | 14 | 21 | 28 | 35 | -------------------------------------------------------------------------------- /src/assets/icons/icon-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 15 | 21 | 27 | -------------------------------------------------------------------------------- /src/assets/icons/icon-next.svg: -------------------------------------------------------------------------------- 1 | 8 | 14 | 23 | -------------------------------------------------------------------------------- /src/assets/icons/icon-right.svg: -------------------------------------------------------------------------------- 1 | 8 | 14 | 21 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 18 | -------------------------------------------------------------------------------- /src/assets/preview/clothes/collared.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - collared 10 | 16 | 22 | 28 | 35 | 42 | 49 | 56 | 57 | -------------------------------------------------------------------------------- /src/assets/preview/clothes/crew.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - crew 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/preview/clothes/open.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - open 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/preview/ear/attached.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | ear - attached 10 | 15 | 19 | 24 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/preview/ear/detached.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | ear - detached 10 | 15 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /src/assets/preview/earrings/hoop.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | earrings - hoop 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/earrings/stud.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | earrings - stud 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/preview/eyebrows/down.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - down 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/preview/eyebrows/eyelashesdown.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - eyelashesdown 10 | 16 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/preview/eyebrows/eyelashesup.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - eyelashesup 10 | 16 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/preview/eyebrows/up.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - up 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/preview/eyes/ellipse.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - ellipse 10 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/preview/eyes/eyeshadow.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - eyeshadow 10 | 17 | 25 | 32 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets/preview/eyes/round.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - round 10 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/preview/eyes/smiling.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - smiling 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/preview/face/base.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | face - base 10 | 16 | 25 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 45 | 50 | 51 | 57 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/assets/preview/glasses/round.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | glasses - round 10 | 17 | 24 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/assets/preview/glasses/square.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | glasses - square 10 | 16 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/frown.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - frown 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/laughing.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - laughing 10 | 16 | 25 | 29 | 30 | 31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/nervous.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - nervous 10 | 19 | 25 | 30 | 31 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/pucker.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - pucker 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/sad.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - sad 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/smile.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - smile 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/smirk.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - smirk 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/mouth/surprised.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - surprised 10 | 16 | 25 | 33 | 34 | 35 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/assets/preview/nose/curve.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | nose - curve 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/nose/pointed.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | nose - pointed 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/nose/round.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | nose - round 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/preview/tops/beanie.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/preview/tops/clean.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - clean 10 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/preview/tops/danny.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - danny 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/preview/tops/fonze.svg: -------------------------------------------------------------------------------- 1 | 8 | tops - fonze 9 | 13 | 17 | 21 | 25 | 29 | -------------------------------------------------------------------------------- /src/assets/preview/tops/funny.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - funny 10 | 15 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /src/assets/preview/tops/pixie.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - pixie 10 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/preview/tops/punk.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - punk 10 | 15 | 19 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/preview/tops/turban.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - turban 10 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/preview/tops/wave.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - wave 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/widgets/clothes/collared.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - collared 10 | 16 | 22 | 28 | 35 | 42 | 49 | 56 | 57 | -------------------------------------------------------------------------------- /src/assets/widgets/clothes/crew.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - crew 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/widgets/clothes/open.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - open 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/widgets/ear/attached.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | ear - attached 10 | 15 | 19 | 24 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/widgets/ear/detached.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | ear - detached 10 | 15 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /src/assets/widgets/earrings/hoop.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | earrings - hoop 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/earrings/stud.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | earrings - stud 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/widgets/eyebrows/down.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - down 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/widgets/eyebrows/eyelashesdown.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - eyelashesdown 10 | 16 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/widgets/eyebrows/eyelashesup.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - eyelashesup 10 | 16 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/widgets/eyebrows/up.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyebrows - up 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/widgets/eyes/ellipse.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - ellipse 10 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/widgets/eyes/eyeshadow.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - eyeshadow 10 | 17 | 25 | 32 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets/widgets/eyes/round.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - round 10 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/widgets/eyes/smiling.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | eyes - smiling 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/widgets/face/base.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | face - base 10 | 16 | 25 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 45 | 50 | 51 | 57 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/assets/widgets/glasses/round.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | glasses - round 10 | 17 | 24 | 29 | 34 | 35 | -------------------------------------------------------------------------------- /src/assets/widgets/glasses/square.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | glasses - square 10 | 16 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/frown.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - frown 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/laughing.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - laughing 10 | 16 | 25 | 29 | 30 | 31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/nervous.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - nervous 10 | 19 | 25 | 30 | 31 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/pucker.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - pucker 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/sad.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - sad 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/smile.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - smile 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/smirk.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - smirk 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/mouth/surprised.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | mouth - surprised 10 | 16 | 25 | 33 | 34 | 35 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/assets/widgets/nose/curve.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | nose - curve 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/nose/pointed.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | nose - pointed 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/nose/round.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | nose - round 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/beanie.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/clean.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - clean 10 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/danny.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - danny 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/fonze.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - fonze 10 | 14 | 18 | 22 | 26 | 30 | 31 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/funny.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - funny 10 | 15 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/pixie.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - pixie 10 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/punk.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - punk 10 | 15 | 19 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/turban.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - turban 10 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/wave.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - wave 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/ActionBar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | 64 | 94 | -------------------------------------------------------------------------------- /src/components/CodeModal.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 87 | 88 | 223 | 224 | 253 | -------------------------------------------------------------------------------- /src/components/ConfettiCanvas.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/components/Configurator.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 166 | 167 | 325 | -------------------------------------------------------------------------------- /src/components/DownloadModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 38 | 39 | 134 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/components/PerfectScrollbar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | -------------------------------------------------------------------------------- /src/components/SectionWrapper.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /src/components/VueColorAvatar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 117 | 118 | 140 | -------------------------------------------------------------------------------- /src/components/widgets/Background.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export const enum Locale { 2 | ZH = 'zh', 3 | EN = 'en', 4 | } 5 | 6 | export const enum ActionType { 7 | Undo = 'undo', 8 | Redo = 'redo', 9 | Flip = 'flip', 10 | Code = 'code', 11 | } 12 | 13 | export enum Gender { 14 | Male = 'male', 15 | Female = 'female', 16 | NotSet = 'notSet', 17 | } 18 | 19 | export enum WidgetType { 20 | Face = 'face', 21 | Tops = 'tops', 22 | Ear = 'ear', 23 | Earrings = 'earrings', 24 | Eyebrows = 'eyebrows', 25 | Eyes = 'eyes', 26 | Nose = 'nose', 27 | Glasses = 'glasses', 28 | Mouth = 'mouth', 29 | Beard = 'beard', 30 | Clothes = 'clothes', 31 | } 32 | 33 | export enum WrapperShape { 34 | Circle = 'circle', 35 | Square = 'square', 36 | Squircle = 'squircle', 37 | } 38 | 39 | /** 40 | * WidgetShape 41 | * 42 | * All enumeration values of `WidgetShape` correspond to the file name. 43 | */ 44 | 45 | export enum FaceShape { 46 | Base = 'base', 47 | } 48 | 49 | export enum TopsShape { 50 | Fonze = 'fonze', 51 | Funny = 'funny', 52 | Clean = 'clean', 53 | Punk = 'punk', 54 | Danny = 'danny', 55 | Wave = 'wave', 56 | Turban = 'turban', 57 | Pixie = 'pixie', 58 | Beanie = 'beanie', 59 | } 60 | 61 | export enum EarShape { 62 | Attached = 'attached', 63 | Detached = 'detached', 64 | } 65 | 66 | export enum EarringsShape { 67 | Hoop = 'hoop', 68 | Stud = 'stud', 69 | None = 'none', 70 | } 71 | 72 | export enum EyebrowsShape { 73 | Up = 'up', 74 | Down = 'down', 75 | Eyelashesup = 'eyelashesup', 76 | Eyelashesdown = 'eyelashesdown', 77 | } 78 | 79 | export enum EyesShape { 80 | Ellipse = 'ellipse', 81 | Smiling = 'smiling', 82 | Eyeshadow = 'eyeshadow', 83 | Round = 'round', 84 | } 85 | 86 | export enum NoseShape { 87 | Curve = 'curve', 88 | Round = 'round', 89 | Pointed = 'pointed', 90 | } 91 | 92 | export enum MouthShape { 93 | Frown = 'frown', 94 | Laughing = 'laughing', 95 | Nervous = 'nervous', 96 | Pucker = 'pucker', 97 | Sad = 'sad', 98 | Smile = 'smile', 99 | Smirk = 'smirk', 100 | Surprised = 'surprised', 101 | } 102 | 103 | export enum BeardShape { 104 | Scruff = 'scruff', 105 | None = 'none', 106 | } 107 | 108 | export enum GlassesShape { 109 | Round = 'round', 110 | Square = 'square', 111 | None = 'none', 112 | } 113 | 114 | export enum ClothesShape { 115 | Crew = 'crew', 116 | Collared = 'collared', 117 | Open = 'open', 118 | } 119 | 120 | export type WidgetShape = 121 | | FaceShape 122 | | TopsShape 123 | | EarShape 124 | | EarringsShape 125 | | EyebrowsShape 126 | | EyesShape 127 | | NoseShape 128 | | MouthShape 129 | | BeardShape 130 | | GlassesShape 131 | | ClothesShape 132 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | interface Window { 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | gtag: (...params: any[]) => void 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useAvatarOption } from './useAvatarOption' 2 | export { default as useSider } from './useSider' 3 | -------------------------------------------------------------------------------- /src/hooks/useAvatarOption.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | 3 | import { useStore } from '@/store' 4 | import { SET_AVATAR_OPTION } from '@/store/mutation-type' 5 | import type { AvatarOption } from '@/types' 6 | 7 | export default function useAvatarOption() { 8 | const store = useStore() 9 | 10 | const avatarOption = computed(() => store.state.history.present) 11 | 12 | const setAvatarOption = (newOption: AvatarOption) => { 13 | store.commit(SET_AVATAR_OPTION, newOption) 14 | } 15 | 16 | return [avatarOption, setAvatarOption] as const 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/useSider.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | 3 | import { useStore } from '@/store' 4 | import { SET_SIDER_STATUS } from '@/store/mutation-type' 5 | 6 | export default function useSider() { 7 | const store = useStore() 8 | 9 | const isCollapsed = computed(() => store.state.isSiderCollapsed) 10 | 11 | const openSider = () => { 12 | store.commit(SET_SIDER_STATUS, false) 13 | } 14 | 15 | const closeSider = () => { 16 | store.commit(SET_SIDER_STATUS, true) 17 | } 18 | 19 | return { isCollapsed, openSider, closeSider } 20 | } 21 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | 3 | import { Locale } from '@/enums' 4 | 5 | import en from './locales/en' 6 | import zh from './locales/zh' 7 | 8 | const messages = { en, zh } 9 | 10 | const [locale, fallbackLocale] = /^zh\b/.test(window.navigator.language) 11 | ? [Locale.ZH, Locale.EN] 12 | : [Locale.EN, Locale.ZH] 13 | 14 | export default createI18n({ 15 | locale, 16 | fallbackLocale, 17 | messages, 18 | }) 19 | -------------------------------------------------------------------------------- /src/i18n/locales/en/index.ts: -------------------------------------------------------------------------------- 1 | import { WidgetType } from '@/enums' 2 | 3 | export default { 4 | action: { 5 | undo: 'undo', 6 | redo: 'redo', 7 | flip: 'flip', 8 | code: 'code', 9 | randomize: 'Randomize', 10 | download: 'Download', 11 | copyCode: 'Copy', 12 | copied: 'Copied', 13 | downloading: 'Downloading', 14 | close: 'Close', 15 | }, 16 | label: { 17 | wrapperShape: 'Avatar Shape', 18 | backgroundColor: 'Background Color', 19 | }, 20 | widgetType: { 21 | [WidgetType.Face]: 'Face', 22 | [WidgetType.Tops]: 'Tops', 23 | [WidgetType.Ear]: 'Ear', 24 | [WidgetType.Earrings]: 'Earrings', 25 | [WidgetType.Eyebrows]: 'Eyebrows', 26 | [WidgetType.Eyes]: 'Eyes', 27 | [WidgetType.Nose]: 'Nose', 28 | [WidgetType.Glasses]: 'Glasses', 29 | [WidgetType.Mouth]: 'Mouth', 30 | [WidgetType.Beard]: 'Beard', 31 | [WidgetType.Clothes]: 'Clothes', 32 | }, 33 | wrapperShape: { 34 | circle: 'Circle', 35 | square: 'Square', 36 | squircle: 'Squircle', 37 | }, 38 | text: { 39 | codeModalTitle: 'Code', 40 | downloadTip: 'LONG PRESS or RIGHT CLICK to save', 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /src/i18n/locales/zh/index.ts: -------------------------------------------------------------------------------- 1 | import { WidgetType } from '@/enums' 2 | 3 | export default { 4 | action: { 5 | undo: '撤销', 6 | redo: '还原', 7 | flip: '水平翻转', 8 | code: '配置代码', 9 | randomize: '随机生成', 10 | download: '下载头像', 11 | copyCode: '复制代码', 12 | copied: '已复制', 13 | downloading: '准备下载', 14 | close: '关闭', 15 | }, 16 | label: { 17 | wrapperShape: '头像形状', 18 | backgroundColor: '背景颜色', 19 | }, 20 | widgetType: { 21 | [WidgetType.Face]: '脸蛋', 22 | [WidgetType.Tops]: '头发 / 头饰', 23 | [WidgetType.Ear]: '耳朵', 24 | [WidgetType.Earrings]: '耳环', 25 | [WidgetType.Eyebrows]: '眉毛', 26 | [WidgetType.Eyes]: '眼睛', 27 | [WidgetType.Nose]: '鼻子', 28 | [WidgetType.Glasses]: '眼镜', 29 | [WidgetType.Mouth]: '嘴巴', 30 | [WidgetType.Beard]: '胡子', 31 | [WidgetType.Clothes]: '衣着', 32 | }, 33 | wrapperShape: { 34 | circle: '圆形', 35 | square: '方形', 36 | squircle: '方圆形', 37 | }, 38 | text: { 39 | codeModalTitle: '配置代码', 40 | downloadTip: '长按图片或右键点击下载至本地相册', 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /src/layouts/Container.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 54 | 55 | 72 | -------------------------------------------------------------------------------- /src/layouts/Footer.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 66 | -------------------------------------------------------------------------------- /src/layouts/Header.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 34 | 35 | 85 | -------------------------------------------------------------------------------- /src/layouts/Sider.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 63 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'perfect-scrollbar/css/perfect-scrollbar.css' 2 | import './styles/reset.css' 3 | import './styles/global.scss' 4 | 5 | import { createApp } from 'vue' 6 | 7 | import store, { storeKey } from '@/store' 8 | 9 | import App from './App.vue' 10 | import i18n from './i18n' 11 | 12 | const app = createApp(App) 13 | 14 | app.use(store, storeKey) 15 | 16 | app.use(i18n) 17 | 18 | app.mount('#app') 19 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | import type { Store } from 'vuex' 3 | import { createStore, useStore as baseUseStore } from 'vuex' 4 | 5 | import { WrapperShape } from '@/enums' 6 | import type { AvatarOption } from '@/types' 7 | import { getRandomAvatarOption } from '@/utils' 8 | import { SCREEN } from '@/utils/constant' 9 | 10 | import { 11 | REDO, 12 | SET_AVATAR_OPTION, 13 | SET_SIDER_STATUS, 14 | UNDO, 15 | } from './mutation-type' 16 | 17 | export interface State { 18 | history: { 19 | past: AvatarOption[] 20 | present: AvatarOption 21 | future: AvatarOption[] 22 | } 23 | isSiderCollapsed: boolean 24 | } 25 | 26 | export default createStore({ 27 | strict: true, 28 | 29 | state: { 30 | history: { 31 | past: [], 32 | present: getRandomAvatarOption({ wrapperShape: WrapperShape.Squircle }), 33 | future: [], 34 | }, 35 | isSiderCollapsed: window.innerWidth <= SCREEN.lg, 36 | }, 37 | 38 | mutations: { 39 | [SET_AVATAR_OPTION](state, data: AvatarOption) { 40 | state.history = { 41 | past: [...state.history.past, state.history.present], 42 | present: data, 43 | future: [], 44 | } 45 | }, 46 | 47 | [UNDO](state) { 48 | if (state.history.past.length > 0) { 49 | const previous = state.history.past[state.history.past.length - 1] 50 | const newPast = state.history.past.slice( 51 | 0, 52 | state.history.past.length - 1 53 | ) 54 | state.history = { 55 | past: newPast, 56 | present: previous, 57 | future: [state.history.present, ...state.history.future], 58 | } 59 | } 60 | }, 61 | 62 | [REDO](state) { 63 | if (state.history.future.length > 0) { 64 | const next = state.history.future[0] 65 | const newFuture = state.history.future.slice(1) 66 | state.history = { 67 | past: [...state.history.past, state.history.present], 68 | present: next, 69 | future: newFuture, 70 | } 71 | } 72 | }, 73 | 74 | [SET_SIDER_STATUS](state, collapsed) { 75 | if (collapsed !== state.isSiderCollapsed) { 76 | state.isSiderCollapsed = collapsed 77 | } 78 | }, 79 | }, 80 | }) 81 | 82 | export const storeKey: InjectionKey> = Symbol() 83 | 84 | export function useStore() { 85 | return baseUseStore(storeKey) 86 | } 87 | -------------------------------------------------------------------------------- /src/store/mutation-type.ts: -------------------------------------------------------------------------------- 1 | export const SET_AVATAR_OPTION = 'SET_AVATAR_OPTION' 2 | export const UNDO = 'UNDO' 3 | export const REDO = 'REDO' 4 | export const SET_SIDER_STATUS = 'SET_SIDER_STATUS' 5 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @use 'src/styles/var'; 2 | /* stylelint-disable-next-line no-invalid-position-at-import-rule */ 3 | @import url('https://fonts.googleapis.com/css2?family=Rubik&display=swap'); 4 | /* stylelint-disable-next-line no-invalid-position-at-import-rule */ 5 | @import url('https://fonts.googleapis.com/css2?family=Ubuntu+Mono&display=swap'); 6 | 7 | @font-face { 8 | font-family: Fallback; 9 | src: local(system-ui), local(-apple-system), local(BlinkMacSystemFont), 10 | local(Segoe UI), local(Roboto), local(Ubuntu), local(Helvetica), 11 | local(Arial), local(sans-serif); 12 | } 13 | 14 | html, 15 | body { 16 | height: 100%; 17 | margin: 0; 18 | font-size: 16px; 19 | font-family: Rubik, Fallback; 20 | scroll-behavior: smooth; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | img { 26 | user-select: none; 27 | -webkit-user-drag: none; 28 | } 29 | 30 | ::selection { 31 | background: rgba(var.$color-text, 0.15); 32 | } 33 | 34 | #app { 35 | width: 100%; 36 | height: 100%; 37 | } 38 | -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */ 2 | html, 3 | body, 4 | p, 5 | ol, 6 | ul, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | blockquote, 12 | figure, 13 | fieldset, 14 | legend, 15 | textarea, 16 | pre, 17 | iframe, 18 | hr, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | h1, 29 | h2, 30 | h3, 31 | h4, 32 | h5, 33 | h6 { 34 | font-weight: normal; 35 | font-size: 100%; 36 | } 37 | ul { 38 | list-style: none; 39 | } 40 | button, 41 | input, 42 | select, 43 | textarea { 44 | margin: 0; 45 | } 46 | html { 47 | box-sizing: border-box; 48 | } 49 | *, 50 | *::before, 51 | *::after { 52 | box-sizing: inherit; 53 | } 54 | img, 55 | video { 56 | max-width: 100%; 57 | height: auto; 58 | } 59 | iframe { 60 | border: 0; 61 | } 62 | table { 63 | border-collapse: collapse; 64 | border-spacing: 0; 65 | } 66 | td, 67 | th { 68 | padding: 0; 69 | } 70 | td:not([align]), 71 | th:not([align]) { 72 | text-align: left; 73 | } 74 | a { 75 | color: inherit; 76 | text-decoration: none; 77 | } 78 | button { 79 | padding: 0; 80 | font-family: inherit; 81 | border: none; 82 | } 83 | -------------------------------------------------------------------------------- /src/styles/var.scss: -------------------------------------------------------------------------------- 1 | $color-accent: hsl(241, 99%, 70%); 2 | $color-primary: $color-accent; 3 | $color-secondary: hsl(186, 84%, 74%); 4 | $color-text: hsl(211, 19%, 70%); 5 | $color-dark: hsl(216, 14%, 14%); 6 | $color-gray: lighten($color-dark, 5); 7 | $color-page-bg: darken($color-dark, 5); 8 | $color-configurator: $color-dark; 9 | $color-stroke: $color-text; 10 | 11 | $layout-header-height: 6rem; 12 | $layout-sider-width: 20rem; 13 | $layout-footer-height: 4rem; 14 | 15 | $screen-sm: 480px; 16 | $screen-md: 768px; 17 | $screen-lg: 976px; 18 | $screen-xl: 1440px; 19 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { NONE } from '@/utils/constant' 2 | 3 | export type None = typeof NONE 4 | 5 | import type { 6 | BeardShape, 7 | ClothesShape, 8 | EarringsShape, 9 | EarShape, 10 | EyebrowsShape, 11 | EyesShape, 12 | FaceShape, 13 | Gender, 14 | GlassesShape, 15 | MouthShape, 16 | NoseShape, 17 | TopsShape, 18 | WrapperShape, 19 | } from '../enums' 20 | 21 | interface Widget { 22 | shape: Shape | None 23 | zIndex?: number 24 | fillColor?: string 25 | strokeColor?: string 26 | } 27 | 28 | type AvatarWidgets = { 29 | face: Widget 30 | tops: Widget 31 | ear: Widget 32 | earrings: Widget 33 | eyebrows: Widget 34 | glasses: Widget 35 | eyes: Widget 36 | nose: Widget 37 | mouth: Widget 38 | beard: Widget 39 | clothes: Widget 40 | } 41 | 42 | export interface AvatarOption { 43 | gender?: Gender 44 | 45 | wrapperShape?: `${WrapperShape}` 46 | 47 | background: { 48 | color: string 49 | } 50 | 51 | widgets: Partial 52 | } 53 | 54 | export interface AvatarSettings { 55 | gender: [Gender, Gender] 56 | 57 | wrapperShape: WrapperShape[] 58 | faceShape: FaceShape[] 59 | topsShape: TopsShape[] 60 | earShape: EarShape[] 61 | earringsShape: EarringsShape[] 62 | eyebrowsShape: EyebrowsShape[] 63 | eyesShape: EyesShape[] 64 | noseShape: NoseShape[] 65 | mouthShape: MouthShape[] 66 | beardShape: BeardShape[] 67 | glassesShape: GlassesShape[] 68 | clothesShape: ClothesShape[] 69 | 70 | backgroundColor: string[] 71 | skinColor: string[] 72 | clothesColor: string[] 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | import type { AvatarOption, AvatarSettings } from '@/types' 2 | 3 | import { 4 | BeardShape, 5 | ClothesShape, 6 | EarringsShape, 7 | EarShape, 8 | EyebrowsShape, 9 | EyesShape, 10 | FaceShape, 11 | Gender, 12 | GlassesShape, 13 | MouthShape, 14 | NoseShape, 15 | TopsShape, 16 | WidgetType, 17 | WrapperShape, 18 | } from '../enums' 19 | 20 | export const AVATAR_LAYER: Readonly<{ 21 | [key in `${WidgetType}`]: { zIndex: number } 22 | }> = { 23 | [WidgetType.Face]: { 24 | zIndex: 10, 25 | }, 26 | [WidgetType.Ear]: { 27 | zIndex: 102, 28 | }, 29 | [WidgetType.Earrings]: { 30 | zIndex: 103, 31 | }, 32 | [WidgetType.Eyebrows]: { 33 | zIndex: 70, 34 | }, 35 | [WidgetType.Eyes]: { 36 | zIndex: 50, 37 | }, 38 | [WidgetType.Nose]: { 39 | zIndex: 60, 40 | }, 41 | [WidgetType.Glasses]: { 42 | zIndex: 90, 43 | }, 44 | [WidgetType.Mouth]: { 45 | zIndex: 100, 46 | }, 47 | [WidgetType.Beard]: { 48 | zIndex: 105, 49 | }, 50 | [WidgetType.Tops]: { 51 | zIndex: 80, 52 | }, 53 | [WidgetType.Clothes]: { 54 | zIndex: 110, 55 | }, 56 | } 57 | 58 | export const SETTINGS: Readonly = { 59 | gender: [Gender.Male, Gender.Female], 60 | 61 | wrapperShape: Object.values(WrapperShape), 62 | faceShape: Object.values(FaceShape), 63 | topsShape: Object.values(TopsShape), 64 | earShape: Object.values(EarShape), 65 | earringsShape: Object.values(EarringsShape), 66 | eyebrowsShape: Object.values(EyebrowsShape), 67 | eyesShape: Object.values(EyesShape), 68 | noseShape: Object.values(NoseShape), 69 | glassesShape: Object.values(GlassesShape), 70 | mouthShape: Object.values(MouthShape), 71 | beardShape: Object.values(BeardShape), 72 | clothesShape: Object.values(ClothesShape), 73 | 74 | backgroundColor: [ 75 | '#6BD9E9', 76 | '#FC909F', 77 | '#F4D150', 78 | '#E0DDFF', 79 | '#D2EFF3', 80 | '#FFEDEF', 81 | '#FFEBA4', 82 | '#506AF4', 83 | '#F48150', 84 | '#48A99A', 85 | '#C09FFF', 86 | '#FD6F5D', 87 | 'linear-gradient(45deg, #E3648C, #D97567)', 88 | 'linear-gradient(62deg, #8EC5FC, #E0C3FC)', 89 | 'linear-gradient(90deg, #ffecd2, #fcb69f)', 90 | 'linear-gradient(120deg, #a1c4fd, #c2e9fb)', 91 | 'linear-gradient(-135deg, #fccb90, #d57eeb)', 92 | 'transparent', 93 | ], 94 | skinColor: ['#F9C9B6', '#AC6651'], 95 | clothesColor: ['#9287FF', '#6BD9E9', '#FC909F', '#F4D150', '#77311D'], 96 | } 97 | 98 | export const SCREEN = { 99 | lg: 976, 100 | } as const 101 | 102 | export const NONE = 'none' 103 | 104 | export const TRIGGER_PROBABILITY = 0.1 105 | 106 | export const SPECIAL_AVATARS: Readonly = [ 107 | { 108 | wrapperShape: 'squircle', 109 | background: { 110 | color: '#E0DDFF', 111 | }, 112 | widgets: { 113 | face: { 114 | shape: FaceShape.Base, 115 | }, 116 | tops: { 117 | shape: TopsShape.Pixie, 118 | }, 119 | ear: { 120 | shape: EarShape.Attached, 121 | }, 122 | earrings: { 123 | shape: EarringsShape.Stud, 124 | }, 125 | eyebrows: { 126 | shape: EyebrowsShape.Up, 127 | }, 128 | eyes: { 129 | shape: EyesShape.Eyeshadow, 130 | }, 131 | nose: { 132 | shape: NoseShape.Pointed, 133 | }, 134 | glasses: { 135 | shape: NONE, 136 | }, 137 | mouth: { 138 | shape: MouthShape.Laughing, 139 | }, 140 | beard: { 141 | shape: NONE, 142 | }, 143 | clothes: { 144 | shape: ClothesShape.Crew, 145 | }, 146 | }, 147 | }, 148 | { 149 | wrapperShape: 'squircle', 150 | background: { 151 | color: '#F4D150', 152 | }, 153 | widgets: { 154 | face: { 155 | shape: FaceShape.Base, 156 | }, 157 | tops: { 158 | shape: TopsShape.Clean, 159 | }, 160 | ear: { 161 | shape: EarShape.Attached, 162 | }, 163 | earrings: { 164 | shape: NONE, 165 | }, 166 | eyebrows: { 167 | shape: EyebrowsShape.Eyelashesdown, 168 | }, 169 | eyes: { 170 | shape: EyesShape.Round, 171 | }, 172 | nose: { 173 | shape: NoseShape.Round, 174 | }, 175 | glasses: { 176 | shape: NONE, 177 | }, 178 | mouth: { 179 | shape: MouthShape.Surprised, 180 | }, 181 | beard: { 182 | shape: NONE, 183 | }, 184 | clothes: { 185 | shape: ClothesShape.Crew, 186 | }, 187 | }, 188 | }, 189 | ] 190 | 191 | export const NOT_COMPATIBLE_AGENTS = [ 192 | 'quark', 193 | 'micromessenger', 194 | 'weibo', 195 | 'douban', 196 | ] as const 197 | 198 | export const DOWNLOAD_DELAY = 800 199 | -------------------------------------------------------------------------------- /src/utils/dynamic-data.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BeardShape, 3 | ClothesShape, 4 | EarringsShape, 5 | EarShape, 6 | EyebrowsShape, 7 | EyesShape, 8 | FaceShape, 9 | GlassesShape, 10 | MouthShape, 11 | NoseShape, 12 | TopsShape, 13 | WidgetType, 14 | } from '../enums' 15 | 16 | /** @internal */ 17 | type Data = Readonly<{ 18 | [key in `${WidgetType}`]: { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | [key in string]: () => Promise 21 | } 22 | }> 23 | 24 | const widgetData: Data = { 25 | [WidgetType.Face]: { 26 | [FaceShape.Base]: () => import(`../assets/widgets/face/base.svg?raw`), 27 | }, 28 | 29 | [WidgetType.Ear]: { 30 | [EarShape.Attached]: () => import(`../assets/widgets/ear/attached.svg?raw`), 31 | [EarShape.Detached]: () => import(`../assets/widgets/ear/detached.svg?raw`), 32 | }, 33 | 34 | [WidgetType.Eyes]: { 35 | [EyesShape.Ellipse]: () => import(`../assets/widgets/eyes/ellipse.svg?raw`), 36 | [EyesShape.Eyeshadow]: () => 37 | import(`../assets/widgets/eyes/eyeshadow.svg?raw`), 38 | [EyesShape.Round]: () => import(`../assets/widgets/eyes/round.svg?raw`), 39 | [EyesShape.Smiling]: () => import(`../assets/widgets/eyes/smiling.svg?raw`), 40 | }, 41 | 42 | [WidgetType.Beard]: { 43 | [BeardShape.Scruff]: () => import(`../assets/widgets/beard/scruff.svg?raw`), 44 | }, 45 | 46 | [WidgetType.Clothes]: { 47 | [ClothesShape.Collared]: () => 48 | import(`../assets/widgets/clothes/collared.svg?raw`), 49 | [ClothesShape.Crew]: () => import(`../assets/widgets/clothes/crew.svg?raw`), 50 | [ClothesShape.Open]: () => import(`../assets/widgets/clothes/open.svg?raw`), 51 | }, 52 | 53 | [WidgetType.Earrings]: { 54 | [EarringsShape.Hoop]: () => 55 | import(`../assets/widgets/earrings/hoop.svg?raw`), 56 | [EarringsShape.Stud]: () => 57 | import(`../assets/widgets/earrings/stud.svg?raw`), 58 | }, 59 | 60 | [WidgetType.Eyebrows]: { 61 | [EyebrowsShape.Down]: () => 62 | import(`../assets/widgets/eyebrows/down.svg?raw`), 63 | [EyebrowsShape.Eyelashesdown]: () => 64 | import(`../assets/widgets/eyebrows/eyelashesdown.svg?raw`), 65 | [EyebrowsShape.Eyelashesup]: () => 66 | import(`../assets/widgets/eyebrows/eyelashesup.svg?raw`), 67 | [EyebrowsShape.Up]: () => import(`../assets/widgets/eyebrows/up.svg?raw`), 68 | }, 69 | 70 | [WidgetType.Glasses]: { 71 | [GlassesShape.Round]: () => 72 | import(`../assets/widgets/glasses/round.svg?raw`), 73 | [GlassesShape.Square]: () => 74 | import(`../assets/widgets/glasses/square.svg?raw`), 75 | }, 76 | 77 | [WidgetType.Mouth]: { 78 | [MouthShape.Frown]: () => import(`../assets/widgets/mouth/frown.svg?raw`), 79 | [MouthShape.Laughing]: () => 80 | import(`../assets/widgets/mouth/laughing.svg?raw`), 81 | [MouthShape.Nervous]: () => 82 | import(`../assets/widgets/mouth/nervous.svg?raw`), 83 | [MouthShape.Pucker]: () => import(`../assets/widgets/mouth/pucker.svg?raw`), 84 | [MouthShape.Sad]: () => import(`../assets/widgets/mouth/sad.svg?raw`), 85 | [MouthShape.Smile]: () => import(`../assets/widgets/mouth/smile.svg?raw`), 86 | [MouthShape.Smirk]: () => import(`../assets/widgets/mouth/smirk.svg?raw`), 87 | [MouthShape.Surprised]: () => 88 | import(`../assets/widgets/mouth/surprised.svg?raw`), 89 | }, 90 | 91 | [WidgetType.Nose]: { 92 | [NoseShape.Curve]: () => import(`../assets/widgets/nose/curve.svg?raw`), 93 | [NoseShape.Pointed]: () => import(`../assets/widgets/nose/pointed.svg?raw`), 94 | [NoseShape.Round]: () => import(`../assets/widgets/nose/round.svg?raw`), 95 | }, 96 | 97 | [WidgetType.Tops]: { 98 | [TopsShape.Beanie]: () => import(`../assets/widgets/tops/beanie.svg?raw`), 99 | [TopsShape.Clean]: () => import(`../assets/widgets/tops/clean.svg?raw`), 100 | [TopsShape.Danny]: () => import(`../assets/widgets/tops/danny.svg?raw`), 101 | [TopsShape.Fonze]: () => import(`../assets/widgets/tops/fonze.svg?raw`), 102 | [TopsShape.Funny]: () => import(`../assets/widgets/tops/funny.svg?raw`), 103 | [TopsShape.Pixie]: () => import(`../assets/widgets/tops/pixie.svg?raw`), 104 | [TopsShape.Punk]: () => import(`../assets/widgets/tops/punk.svg?raw`), 105 | [TopsShape.Turban]: () => import(`../assets/widgets/tops/turban.svg?raw`), 106 | [TopsShape.Wave]: () => import(`../assets/widgets/tops/wave.svg?raw`), 107 | }, 108 | } 109 | 110 | const previewData: Data = { 111 | [WidgetType.Face]: { 112 | [FaceShape.Base]: () => import(`../assets/preview/face/base.svg?raw`), 113 | }, 114 | 115 | [WidgetType.Ear]: { 116 | [EarShape.Attached]: () => import(`../assets/preview/ear/attached.svg?raw`), 117 | [EarShape.Detached]: () => import(`../assets/preview/ear/detached.svg?raw`), 118 | }, 119 | 120 | [WidgetType.Eyes]: { 121 | [EyesShape.Ellipse]: () => import(`../assets/preview/eyes/ellipse.svg?raw`), 122 | [EyesShape.Eyeshadow]: () => 123 | import(`../assets/preview/eyes/eyeshadow.svg?raw`), 124 | [EyesShape.Round]: () => import(`../assets/preview/eyes/round.svg?raw`), 125 | [EyesShape.Smiling]: () => import(`../assets/preview/eyes/smiling.svg?raw`), 126 | }, 127 | 128 | [WidgetType.Beard]: { 129 | [BeardShape.Scruff]: () => import(`../assets/preview/beard/scruff.svg?raw`), 130 | }, 131 | 132 | [WidgetType.Clothes]: { 133 | [ClothesShape.Collared]: () => 134 | import(`../assets/preview/clothes/collared.svg?raw`), 135 | [ClothesShape.Crew]: () => import(`../assets/preview/clothes/crew.svg?raw`), 136 | [ClothesShape.Open]: () => import(`../assets/preview/clothes/open.svg?raw`), 137 | }, 138 | 139 | [WidgetType.Earrings]: { 140 | [EarringsShape.Hoop]: () => 141 | import(`../assets/preview/earrings/hoop.svg?raw`), 142 | [EarringsShape.Stud]: () => 143 | import(`../assets/preview/earrings/stud.svg?raw`), 144 | }, 145 | 146 | [WidgetType.Eyebrows]: { 147 | [EyebrowsShape.Down]: () => 148 | import(`../assets/preview/eyebrows/down.svg?raw`), 149 | [EyebrowsShape.Eyelashesdown]: () => 150 | import(`../assets/preview/eyebrows/eyelashesdown.svg?raw`), 151 | [EyebrowsShape.Eyelashesup]: () => 152 | import(`../assets/preview/eyebrows/eyelashesup.svg?raw`), 153 | [EyebrowsShape.Up]: () => import(`../assets/preview/eyebrows/up.svg?raw`), 154 | }, 155 | 156 | [WidgetType.Glasses]: { 157 | [GlassesShape.Round]: () => 158 | import(`../assets/preview/glasses/round.svg?raw`), 159 | [GlassesShape.Square]: () => 160 | import(`../assets/preview/glasses/square.svg?raw`), 161 | }, 162 | 163 | [WidgetType.Mouth]: { 164 | [MouthShape.Frown]: () => import(`../assets/preview/mouth/frown.svg?raw`), 165 | [MouthShape.Laughing]: () => 166 | import(`../assets/preview/mouth/laughing.svg?raw`), 167 | [MouthShape.Nervous]: () => 168 | import(`../assets/preview/mouth/nervous.svg?raw`), 169 | [MouthShape.Pucker]: () => import(`../assets/preview/mouth/pucker.svg?raw`), 170 | [MouthShape.Sad]: () => import(`../assets/preview/mouth/sad.svg?raw`), 171 | [MouthShape.Smile]: () => import(`../assets/preview/mouth/smile.svg?raw`), 172 | [MouthShape.Smirk]: () => import(`../assets/preview/mouth/smirk.svg?raw`), 173 | [MouthShape.Surprised]: () => 174 | import(`../assets/preview/mouth/surprised.svg?raw`), 175 | }, 176 | 177 | [WidgetType.Nose]: { 178 | [NoseShape.Curve]: () => import(`../assets/preview/nose/curve.svg?raw`), 179 | [NoseShape.Pointed]: () => import(`../assets/preview/nose/pointed.svg?raw`), 180 | [NoseShape.Round]: () => import(`../assets/preview/nose/round.svg?raw`), 181 | }, 182 | 183 | [WidgetType.Tops]: { 184 | [TopsShape.Beanie]: () => import(`../assets/preview/tops/beanie.svg?raw`), 185 | [TopsShape.Clean]: () => import(`../assets/preview/tops/clean.svg?raw`), 186 | [TopsShape.Danny]: () => import(`../assets/preview/tops/danny.svg?raw`), 187 | [TopsShape.Fonze]: () => import(`../assets/preview/tops/fonze.svg?raw`), 188 | [TopsShape.Funny]: () => import(`../assets/preview/tops/funny.svg?raw`), 189 | [TopsShape.Pixie]: () => import(`../assets/preview/tops/pixie.svg?raw`), 190 | [TopsShape.Punk]: () => import(`../assets/preview/tops/punk.svg?raw`), 191 | [TopsShape.Turban]: () => import(`../assets/preview/tops/turban.svg?raw`), 192 | [TopsShape.Wave]: () => import(`../assets/preview/tops/wave.svg?raw`), 193 | }, 194 | } 195 | 196 | export { previewData, widgetData } 197 | -------------------------------------------------------------------------------- /src/utils/ga.ts: -------------------------------------------------------------------------------- 1 | export function recordEvent( 2 | action: string, 3 | params: { 4 | event_category: string 5 | event_label?: string 6 | value?: number 7 | } 8 | ) { 9 | window?.gtag('event', action, params) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { EarringsShape, GlassesShape } from '@/enums' 2 | import { BeardShape, Gender, TopsShape } from '@/enums' 3 | import type { AvatarOption, None } from '@/types' 4 | 5 | import { NONE, SETTINGS, SPECIAL_AVATARS } from './constant' 6 | 7 | /** 8 | * get a random value from an array 9 | */ 10 | function getRandomValue( 11 | arr: Item[], 12 | { 13 | avoid = [], 14 | usually = [], 15 | }: { avoid?: unknown[]; usually?: (Item | 'none')[] } = {} 16 | ): Item { 17 | const avoidValues = avoid.filter(Boolean) 18 | const filteredArr = arr.filter((it) => !avoidValues.includes(it)) 19 | 20 | const usuallyValues = usually 21 | .filter(Boolean) 22 | .reduce((acc, cur) => acc.concat(new Array(15).fill(cur)), []) 23 | 24 | const finalArr = filteredArr.concat(usuallyValues) 25 | 26 | const randomIdx = Math.floor(Math.random() * finalArr.length) 27 | const randomValue = finalArr[randomIdx] 28 | 29 | return randomValue 30 | } 31 | 32 | export function getRandomAvatarOption( 33 | presetOption: Partial = {}, 34 | useOption: Partial = {} 35 | ): AvatarOption { 36 | const gender = getRandomValue(SETTINGS.gender) 37 | 38 | const beardList: BeardShape[] = [] 39 | let topList: TopsShape[] = [TopsShape.Danny, TopsShape.Wave, TopsShape.Pixie] 40 | 41 | if (gender === Gender.Male) { 42 | beardList.push(BeardShape.Scruff) 43 | topList = SETTINGS.topsShape.filter((shape) => !topList.includes(shape)) 44 | } 45 | 46 | const avatarOption: AvatarOption = { 47 | gender, 48 | 49 | wrapperShape: 50 | presetOption?.wrapperShape || getRandomValue(SETTINGS.wrapperShape), 51 | 52 | background: { 53 | color: getRandomValue(SETTINGS.backgroundColor, { 54 | avoid: [useOption.background?.color], 55 | }), 56 | }, 57 | 58 | widgets: { 59 | face: { 60 | shape: getRandomValue(SETTINGS.faceShape), 61 | }, 62 | tops: { 63 | shape: getRandomValue(topList, { 64 | avoid: [useOption.widgets?.tops?.shape], 65 | }), 66 | }, 67 | ear: { 68 | shape: getRandomValue(SETTINGS.earShape, { 69 | avoid: [useOption.widgets?.ear?.shape], 70 | }), 71 | }, 72 | earrings: { 73 | shape: getRandomValue(SETTINGS.earringsShape, { 74 | usually: [NONE], 75 | }), 76 | }, 77 | eyebrows: { 78 | shape: getRandomValue(SETTINGS.eyebrowsShape, { 79 | avoid: [useOption.widgets?.eyebrows?.shape], 80 | }), 81 | }, 82 | eyes: { 83 | shape: getRandomValue(SETTINGS.eyesShape, { 84 | avoid: [useOption.widgets?.eyes?.shape], 85 | }), 86 | }, 87 | nose: { 88 | shape: getRandomValue(SETTINGS.noseShape, { 89 | avoid: [useOption.widgets?.nose?.shape], 90 | }), 91 | }, 92 | glasses: { 93 | shape: getRandomValue(SETTINGS.glassesShape, { 94 | usually: [NONE], 95 | }), 96 | }, 97 | mouth: { 98 | shape: getRandomValue(SETTINGS.mouthShape, { 99 | avoid: [useOption.widgets?.mouth?.shape], 100 | }), 101 | }, 102 | beard: { 103 | shape: getRandomValue(beardList, { 104 | usually: [NONE], 105 | }), 106 | }, 107 | clothes: { 108 | shape: getRandomValue(SETTINGS.clothesShape, { 109 | avoid: [useOption.widgets?.clothes?.shape], 110 | }), 111 | }, 112 | }, 113 | } 114 | 115 | return avatarOption 116 | } 117 | 118 | export function getSpecialAvatarOption(): AvatarOption { 119 | return SPECIAL_AVATARS[Math.floor(Math.random() * SPECIAL_AVATARS.length)] 120 | } 121 | 122 | export function showConfetti() { 123 | import('canvas-confetti').then((confetti) => { 124 | const canvasEle: HTMLCanvasElement | null = 125 | document.querySelector('#confetti') 126 | 127 | if (!canvasEle) { 128 | return 129 | } 130 | 131 | const myConfetti = confetti.create(canvasEle, { 132 | resize: true, 133 | useWorker: true, 134 | disableForReducedMotion: true, 135 | }) 136 | 137 | const duration = performance.now() + 1 * 1000 138 | 139 | const colors = ['#6967fe', '#85e9f4', '#e16984'] 140 | 141 | void (function frame() { 142 | myConfetti({ 143 | particleCount: colors.length, 144 | angle: 60, 145 | spread: 55, 146 | origin: { x: 0 }, 147 | colors: colors, 148 | }) 149 | myConfetti({ 150 | particleCount: colors.length, 151 | angle: 120, 152 | spread: 55, 153 | origin: { x: 1 }, 154 | colors: colors, 155 | }) 156 | 157 | if (performance.now() < duration) { 158 | requestAnimationFrame(frame) 159 | } 160 | })() 161 | }) 162 | } 163 | 164 | export function highlightJSON(json: string): string { 165 | if (!json) { 166 | return '' 167 | } 168 | 169 | if (typeof json != 'string') { 170 | json = JSON.stringify(json, undefined, 2) 171 | } 172 | 173 | json = json.replace(/&/g, '&').replace(//g, '>') 174 | 175 | return json.replace( 176 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, 177 | (match) => { 178 | let cls = '' 179 | if (/^"/.test(match)) { 180 | if (/:$/.test(match)) { 181 | cls = 'key' 182 | } else { 183 | cls = 'string' 184 | } 185 | } else if (/true|false/.test(match)) { 186 | cls = 'boolean' 187 | } else if (/null/.test(match)) { 188 | cls = 'null' 189 | } else { 190 | cls = 'number' 191 | } 192 | return `${match}` 193 | } 194 | ) 195 | } 196 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "useDefineForClassFields": true, 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "noImplicitAny": false, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "lib": ["esnext", "dom"], 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | } 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import path from 'path' 3 | import { visualizer } from 'rollup-plugin-visualizer' 4 | import { defineConfig } from 'vite' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig(({ mode }) => ({ 8 | plugins: [ 9 | vue(), 10 | ...(mode === 'prerelease' 11 | ? [ 12 | visualizer({ 13 | open: true, 14 | gzipSize: true, 15 | brotliSize: true, 16 | template: 'sunburst', 17 | }), 18 | ] 19 | : []), 20 | ], 21 | 22 | resolve: { 23 | alias: { 24 | '@': path.resolve(__dirname, './src'), 25 | }, 26 | }, 27 | 28 | define: { 29 | __VUE_I18N_FULL_INSTALL__: false, 30 | __VUE_I18N_LEGACY_API__: false, 31 | __INTLIFY_PROD_DEVTOOLS__: false, 32 | }, 33 | })) 34 | --------------------------------------------------------------------------------