├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierrc.js ├── .stylelintrc.js ├── Dockerfile ├── LICENSE ├── README-CN.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 ├── pnpm-lock.yaml ├── 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 │ ├── ConfettiCanvas.vue │ ├── Configurator.vue │ ├── Logo.vue │ ├── Modal │ │ ├── BatchDownloadModal.vue │ │ ├── CodeModal.vue │ │ ├── DownloadModal.vue │ │ └── ModalWrapper.vue │ ├── PerfectScrollbar.vue │ ├── SectionWrapper.vue │ ├── VueColorAvatar.vue │ └── widgets │ │ ├── Background.vue │ │ └── Border.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 /.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 | 'vue/multi-word-component-names': 0, 34 | 35 | 'simple-import-sort/imports': 1, 36 | 'simple-import-sort/exports': 1, 37 | 'sort-imports': 0, 38 | 'import/order': 0, 39 | 'import/no-unresolved': [ 40 | 2, 41 | { 42 | ignore: ['^@/', '^@@/'], 43 | }, 44 | ], 45 | 'vue/no-unused-vars': 1, 46 | '@typescript-eslint/explicit-module-boundary-types': 0, 47 | '@typescript-eslint/consistent-type-imports': 1, 48 | '@typescript-eslint/no-non-null-assertion': 0, 49 | '@typescript-eslint/no-explicit-any': 0, 50 | }, 51 | ignorePatterns: [ 52 | 'dist', 53 | 'public', 54 | '!.eslintrc.js', 55 | '!.prettierrc.js', 56 | '!.stylelintrc.js', 57 | '!.lintstagedrc.js', 58 | ], 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | yarn-error.log 7 | stats.html 8 | coverage -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Install dependencies first (for better caching) 8 | COPY package.json pnpm-lock.yaml ./ 9 | RUN corepack enable pnpm && pnpm install --frozen-lockfile 10 | 11 | # Copy source code 12 | COPY . . 13 | 14 | # Build the application 15 | RUN pnpm build 16 | 17 | # Production stage 18 | FROM nginx:alpine 19 | 20 | # Copy custom nginx config if needed 21 | # COPY nginx.conf /etc/nginx/conf.d/default.conf 22 | 23 | # Copy built assets from builder stage 24 | COPY --from=builder /app/dist /usr/share/nginx/html 25 | 26 | # Add healthcheck 27 | HEALTHCHECK --interval=30s --timeout=3s \ 28 | CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 29 | 30 | # Expose port 31 | EXPOSE 80 32 | 33 | # Start nginx 34 | CMD ["nginx", "-g", "daemon off;"] 35 | -------------------------------------------------------------------------------- /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-CN.md: -------------------------------------------------------------------------------- 1 |
2 |

Vue Color Avatar

3 | 4 |

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

5 | 6 | [Read in English](./README.md) 7 | 8 |
9 | 10 | 11 | website-cover 12 | 13 | 14 | ## 在线预览 15 | 16 | [`https://vue-color-avatar.leoku.dev`](https://vue-color-avatar.leoku.dev) 17 | 18 | ## 介绍 19 | 20 | **这是一款矢量风格头像的生成器,你可以搭配不同的素材组件,生成自己的个性化头像。** 21 | 22 | 你可能感兴趣的功能: 23 | 24 | - 可视化组件配置栏 25 | - 随机生成头像,有一定概率触发彩蛋 26 | - 撤销/还原*更改* 27 | - 国际化多语言 28 | - 批量生成多个头像 29 | 30 | ## 设计资源 31 | 32 | - 设计师:[@Micah](https://www.figma.com/@Micah) on Figma 33 | - 素材来源:[Avatar Illustration System](https://www.figma.com/community/file/829741575478342595) 34 | 35 | > **Note** 36 | > 虽然该项目是 MIT 协议,但是素材资源基于 [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) 协议。如果你有好的创意素材,欢迎补充! 37 | 38 | ## 项目开发 39 | 40 | 该项目使用 `Vue3` + `Vite` 进行开发。 41 | 42 | ```sh 43 | # 1. 克隆项目至本地 44 | git clone https://github.com/Codennnn/vue-color-avatar.git 45 | 46 | # 2. 安装项目依赖 47 | yarn install 48 | 49 | # 3. 运行项目 50 | yarn dev 51 | ``` 52 | 53 | ### Docker 快速部署 54 | 55 | ```sh 56 | #下载代码 57 | git clone https://github.com/Codennnn/vue-color-avatar.git 58 | 59 | #docker 编译 60 | cd vue-color-avatar/ 61 | docker build -t vue-color-avatar:latest . 62 | 63 | #启动服务 64 | docker run -d -p 3000:80 --name vue-color-avatar vue-color-avatar:latest 65 | ``` 66 | 67 | 最后,打开你的浏览器访问服务的地址 http://localhost:3000 即可。 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Vue Color Avatar

3 | 4 |

🧑‍🦱 A playful avatar generator 🧑‍🦳

5 | 6 | [简体中文](./README-CN.md) 7 | 8 |
9 | 10 | 11 | website-cover 12 | 13 | 14 | ## Preview 15 | 16 | [`https://vue-color-avatar.leoku.dev`](https://vue-color-avatar.leoku.dev) 17 | 18 | ## Introduction 19 | 20 | **This is a vector style avatar generator, you can match different material components to generate your own personalized 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 | - Generate multiple avatars in batch 29 | 30 | ## Assets 31 | 32 | > **Note** 33 | > The avatar assets implementation of [Avatar Illustration System](https://www.figma.com/community/file/829741575478342595) by Micah Lanier. And the licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). 34 | 35 | ## Develop 36 | 37 | This project is built with `Vue3` + `Vite`. 38 | 39 | ```sh 40 | # 1. Clone project 41 | git clone https://github.com/Codennnn/vue-color-avatar.git 42 | 43 | # 2. Install dependencies 44 | yarn install 45 | 46 | # 3. Run 47 | yarn dev 48 | ``` 49 | 50 | ## Docker deploy 51 | 52 | ```sh 53 | #clone the code 54 | git clone https://github.com/Codennnn/vue-color-avatar.git 55 | 56 | #docker build 57 | cd vue-color-avatar/ 58 | docker build -t vue-color-avatar:latest . 59 | 60 | #start server 61 | docker run -d -p 3000:80 --name vue-color-avatar vue-color-avatar:latest 62 | ``` 63 | 64 | Once the container is running, open your browser and visit: 65 | 66 | - http://localhost:3000 (if running locally) 67 | - http://your-server-ip:3000 (if running on a server) 68 | -------------------------------------------------------------------------------- /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/Codennnn/vue-color-avatar/01010444fc43392d0ecbc73cd34bf08bb339c153/images/social-preview-1.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 37 | 38 | 39 | Vue Color Avatar 40 | 41 | 42 | 46 | 55 | 56 | 127 | 128 | 129 | 130 |
131 |
132 | 133 |
Coming soon...
134 |
135 |
136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /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/Codennnn/vue-color-avatar/01010444fc43392d0ecbc73cd34bf08bb339c153/landing-page/avatar-1.png -------------------------------------------------------------------------------- /landing-page/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codennnn/vue-color-avatar/01010444fc43392d0ecbc73cd34bf08bb339c153/landing-page/avatar-2.png -------------------------------------------------------------------------------- /landing-page/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codennnn/vue-color-avatar/01010444fc43392d0ecbc73cd34bf08bb339c153/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/Codennnn/vue-color-avatar/01010444fc43392d0ecbc73cd34bf08bb339c153/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": "vue-color-avatar", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "author": "LeoKu (https://leoku.top)", 7 | "scripts": { 8 | "build": "npm run test && vite build", 9 | "build:prerelease": "vite build --mode prerelease", 10 | "deps": "pnpm upgrade-interactive --latest", 11 | "dev": "vite", 12 | "lint": "pnpm lint:es && pnpm lint:style && pnpm 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 | "preview": "vite preview", 18 | "test": "jest" 19 | }, 20 | "dependencies": { 21 | "canvas-confetti": "^1.4.0", 22 | "clipboard": "^2.0.8", 23 | "html2canvas": "^1.3.2", 24 | "jszip": "^3.10.0", 25 | "object-hash": "^3.0.0", 26 | "perfect-scrollbar": "^1.5.2", 27 | "pinia": "^2.0.30", 28 | "vue": "^3.2.30", 29 | "vue-i18n": "^9.2.0-beta.9" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.17.0", 33 | "@babel/preset-env": "^7.16.11", 34 | "@babel/preset-typescript": "^7.16.7", 35 | "@types/canvas-confetti": "^1.4.2", 36 | "@types/jest": "^27.0.2", 37 | "@types/object-hash": "^2.2.1", 38 | "@typescript-eslint/eslint-plugin": "^5.11.0", 39 | "@typescript-eslint/parser": "^5.11.0", 40 | "@vitejs/plugin-vue": "^2.1.0", 41 | "babel-jest": "^27.2.5", 42 | "eslint": "^8.8.0", 43 | "eslint-config-prettier": "^8.3.0", 44 | "eslint-plugin-import": "^2.25.4", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "eslint-plugin-simple-import-sort": "^7.0.0", 47 | "eslint-plugin-vue": "^8.4.1", 48 | "jest": "^27.2.5", 49 | "prettier": "^2.5.1", 50 | "rollup-plugin-visualizer": "^5.5.2", 51 | "sass": "^1.49.7", 52 | "stylelint": "^14.3.0", 53 | "stylelint-config-prettier": "^9.0.3", 54 | "stylelint-config-rational-order": "^0.1.2", 55 | "stylelint-config-recommended": "^6.0.0", 56 | "stylelint-order": "^5.0.0", 57 | "stylelint-prettier": "^2.0.0", 58 | "stylelint-scss": "^4.1.0", 59 | "typescript": "^4.5.5", 60 | "vite": "^3.0.3", 61 | "vue-tsc": "^0.31.2" 62 | }, 63 | "packageManager": "pnpm@9.15.3", 64 | "engines": { 65 | "node": ">=18" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 18 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { en } from '../i18n/locales/en' 2 | import { zh } from '../i18n/locales/zh' 3 | import { highlightJSON } from '../utils' 4 | 5 | test('highlightJSON', () => { 6 | const str = JSON.stringify({ a: 1, b: '2' }) 7 | 8 | expect(highlightJSON(str)).toMatch('key') 9 | expect(highlightJSON(str)).toMatch('number') 10 | expect(highlightJSON(str)).toMatch('string') 11 | }) 12 | 13 | const getKeys = (target: Record) => { 14 | const keys: string[] = [] 15 | 16 | for (const key in target) { 17 | if (typeof target[key] === 'object') { 18 | keys.push(...getKeys(target[key])) 19 | } else { 20 | keys.push(key) 21 | } 22 | } 23 | 24 | return keys 25 | } 26 | 27 | test('check locales completeness', () => { 28 | const localeZH = getKeys(zh).sort() 29 | const localeEN = getKeys(en).sort() 30 | 31 | expect(localeZH).toEqual(localeEN) 32 | }) 33 | -------------------------------------------------------------------------------- /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 | 58 | -------------------------------------------------------------------------------- /src/assets/widgets/clothes/crew.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - crew 10 | 16 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/assets/widgets/clothes/open.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | clothes - open 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/widgets/ear/attached.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | ear - attached 10 | 15 | 19 | 24 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/assets/widgets/ear/detached.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | ear - detached 10 | 15 | 21 | 26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /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 | 33 | 34 | 35 | 36 | 41 | 42 | 48 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /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 | 18 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/fonze.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - fonze 10 | 14 | 18 | 22 | 26 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/funny.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - funny 10 | 15 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/pixie.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - pixie 10 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/widgets/tops/punk.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | tops - punk 10 | 15 | 19 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /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 | 18 | -------------------------------------------------------------------------------- /src/components/ActionBar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | 64 | 94 | -------------------------------------------------------------------------------- /src/components/ConfettiCanvas.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/components/Modal/BatchDownloadModal.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 138 | 139 | 251 | -------------------------------------------------------------------------------- /src/components/Modal/CodeModal.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 88 | 89 | 204 | 205 | 234 | -------------------------------------------------------------------------------- /src/components/Modal/DownloadModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 38 | 39 | 134 | -------------------------------------------------------------------------------- /src/components/Modal/ModalWrapper.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 41 | -------------------------------------------------------------------------------- /src/components/PerfectScrollbar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /src/components/SectionWrapper.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /src/components/VueColorAvatar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | 141 | 142 | 155 | -------------------------------------------------------------------------------- /src/components/widgets/Background.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/components/widgets/Border.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /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 const 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 { useAvatarOption } from './useAvatarOption' 2 | export { 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 function useAvatarOption() { 8 | const store = useStore() 9 | 10 | const avatarOption = computed(() => store.history.present) 11 | 12 | const setAvatarOption = (newOption: AvatarOption) => { 13 | store[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 function useSider() { 7 | const store = useStore() 8 | 9 | const isCollapsed = computed(() => store.isSiderCollapsed) 10 | 11 | const openSider = () => { 12 | store[SET_SIDER_STATUS](false) 13 | } 14 | 15 | const closeSider = () => { 16 | store[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 const i18n = createI18n({ 15 | locale, 16 | fallbackLocale, 17 | messages, 18 | }) 19 | -------------------------------------------------------------------------------- /src/i18n/locales/en/index.ts: -------------------------------------------------------------------------------- 1 | import { WidgetType } from '@/enums' 2 | 3 | export const en = { 4 | action: { 5 | undo: 'undo', 6 | redo: 'redo', 7 | flip: 'flip', 8 | code: 'code', 9 | randomize: 'Randomize', 10 | download: 'Download', 11 | downloadMultiple: 'Generate multiple', 12 | copyCode: 'Copy', 13 | copied: 'Copied', 14 | downloading: 'Downloading', 15 | close: 'Close', 16 | }, 17 | label: { 18 | wrapperShape: 'Avatar Shape', 19 | borderColor: 'Border Color', 20 | backgroundColor: 'Background Color', 21 | colors: 'colors', 22 | }, 23 | widgetType: { 24 | [WidgetType.Face]: 'Face', 25 | [WidgetType.Tops]: 'Tops', 26 | [WidgetType.Ear]: 'Ear', 27 | [WidgetType.Earrings]: 'Earrings', 28 | [WidgetType.Eyebrows]: 'Eyebrows', 29 | [WidgetType.Eyes]: 'Eyes', 30 | [WidgetType.Nose]: 'Nose', 31 | [WidgetType.Glasses]: 'Glasses', 32 | [WidgetType.Mouth]: 'Mouth', 33 | [WidgetType.Beard]: 'Beard', 34 | [WidgetType.Clothes]: 'Clothes', 35 | }, 36 | wrapperShape: { 37 | circle: 'Circle', 38 | square: 'Square', 39 | squircle: 'Squircle', 40 | }, 41 | text: { 42 | codeModalTitle: 'Code', 43 | downloadTip: 'LONG PRESS or RIGHT CLICK to save', 44 | downloadMultiple: 'Download All', 45 | downloadingMultiple: 'Downloading', 46 | downloadMultipleTip: 'Automatically generated', 47 | regenerate: 'Regenerate', 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /src/i18n/locales/zh/index.ts: -------------------------------------------------------------------------------- 1 | import { WidgetType } from '@/enums' 2 | 3 | export const zh = { 4 | action: { 5 | undo: '撤销', 6 | redo: '还原', 7 | flip: '水平翻转', 8 | code: '配置代码', 9 | randomize: '随机生成', 10 | download: '下载头像', 11 | downloadMultiple: '批量生成', 12 | copyCode: '复制代码', 13 | copied: '已复制', 14 | downloading: '准备下载', 15 | close: '关闭', 16 | }, 17 | label: { 18 | wrapperShape: '头像形状', 19 | borderColor: '边框颜色', 20 | backgroundColor: '背景颜色', 21 | colors: '颜色', 22 | }, 23 | widgetType: { 24 | [WidgetType.Face]: '脸蛋', 25 | [WidgetType.Tops]: '头发 / 头饰', 26 | [WidgetType.Ear]: '耳朵', 27 | [WidgetType.Earrings]: '耳环', 28 | [WidgetType.Eyebrows]: '眉毛', 29 | [WidgetType.Eyes]: '眼睛', 30 | [WidgetType.Nose]: '鼻子', 31 | [WidgetType.Glasses]: '眼镜', 32 | [WidgetType.Mouth]: '嘴巴', 33 | [WidgetType.Beard]: '胡子', 34 | [WidgetType.Clothes]: '衣着', 35 | }, 36 | wrapperShape: { 37 | circle: '圆形', 38 | square: '方形', 39 | squircle: '方圆形', 40 | }, 41 | text: { 42 | codeModalTitle: '配置代码', 43 | downloadTip: '长按图片或右键点击下载至本地相册', 44 | downloadMultiple: '下载全部', 45 | downloadingMultiple: '正在下载', 46 | downloadMultipleTip: '已为你自动生成头像', 47 | regenerate: '换一批', 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /src/layouts/Container.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 54 | 55 | 75 | -------------------------------------------------------------------------------- /src/layouts/Footer.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | 37 | 68 | -------------------------------------------------------------------------------- /src/layouts/Header.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | 36 | 88 | -------------------------------------------------------------------------------- /src/layouts/Sider.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 67 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'perfect-scrollbar/css/perfect-scrollbar.css' 2 | import './styles/reset.css' 3 | import './styles/global.scss' 4 | 5 | import { createPinia } from 'pinia' 6 | import { createApp } from 'vue' 7 | 8 | import App from './App.vue' 9 | import { i18n } from './i18n' 10 | 11 | const app = createApp(App) 12 | 13 | app.use(createPinia()) 14 | 15 | app.use(i18n) 16 | 17 | app.mount('#app') 18 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | import { WrapperShape } from '@/enums' 4 | import type { AvatarOption } from '@/types' 5 | import { getRandomAvatarOption } from '@/utils' 6 | import { SCREEN } from '@/utils/constant' 7 | 8 | import { 9 | REDO, 10 | SET_AVATAR_OPTION, 11 | SET_SIDER_STATUS, 12 | UNDO, 13 | } from './mutation-type' 14 | 15 | export interface State { 16 | history: { 17 | past: AvatarOption[] 18 | present: AvatarOption 19 | future: AvatarOption[] 20 | } 21 | isSiderCollapsed: boolean 22 | } 23 | 24 | export const useStore = defineStore('store', { 25 | state: () => 26 | ({ 27 | history: { 28 | past: [], 29 | present: getRandomAvatarOption({ wrapperShape: WrapperShape.Squircle }), 30 | future: [], 31 | }, 32 | isSiderCollapsed: window.innerWidth <= SCREEN.lg, 33 | } as State), 34 | actions: { 35 | [SET_AVATAR_OPTION](data: AvatarOption) { 36 | this.history = { 37 | past: [...this.history.past, this.history.present], 38 | present: data, 39 | future: [], 40 | } 41 | }, 42 | 43 | [UNDO]() { 44 | if (this.history.past.length > 0) { 45 | const previous = this.history.past[this.history.past.length - 1] 46 | const newPast = this.history.past.slice(0, this.history.past.length - 1) 47 | this.history = { 48 | past: newPast, 49 | present: previous, 50 | future: [this.history.present, ...this.history.future], 51 | } 52 | } 53 | }, 54 | 55 | [REDO]() { 56 | if (this.history.future.length > 0) { 57 | const next = this.history.future[0] 58 | const newFuture = this.history.future.slice(1) 59 | this.history = { 60 | past: [...this.history.past, this.history.present], 61 | present: next, 62 | future: newFuture, 63 | } 64 | } 65 | }, 66 | 67 | [SET_SIDER_STATUS](collapsed: boolean) { 68 | if (collapsed !== this.isSiderCollapsed) { 69 | this.isSiderCollapsed = collapsed 70 | } 71 | }, 72 | }, 73 | }) 74 | -------------------------------------------------------------------------------- /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 | :root { 15 | color-scheme: dark; 16 | } 17 | 18 | html, 19 | body { 20 | height: 100%; 21 | margin: 0; 22 | font-size: 16px; 23 | font-family: Rubik, Fallback; 24 | scroll-behavior: smooth; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | 29 | img { 30 | user-select: none; 31 | -webkit-user-drag: none; 32 | } 33 | 34 | ::selection { 35 | background: rgba(var.$color-text, 0.15); 36 | } 37 | 38 | #app { 39 | width: 100%; 40 | height: 100%; 41 | } 42 | -------------------------------------------------------------------------------- /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 { 6 | type BeardShape, 7 | type ClothesShape, 8 | type EarringsShape, 9 | type EarShape, 10 | type EyebrowsShape, 11 | type EyesShape, 12 | type FaceShape, 13 | type Gender, 14 | type GlassesShape, 15 | type MouthShape, 16 | type NoseShape, 17 | type TopsShape, 18 | type 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 | borderColor: string 50 | } 51 | 52 | widgets: Partial 53 | } 54 | 55 | export interface AvatarSettings { 56 | gender: [Gender, Gender] 57 | 58 | wrapperShape: WrapperShape[] 59 | faceShape: FaceShape[] 60 | topsShape: TopsShape[] 61 | earShape: EarShape[] 62 | earringsShape: EarringsShape[] 63 | eyebrowsShape: EyebrowsShape[] 64 | eyesShape: EyesShape[] 65 | noseShape: NoseShape[] 66 | mouthShape: MouthShape[] 67 | beardShape: BeardShape[] 68 | glassesShape: GlassesShape[] 69 | clothesShape: ClothesShape[] 70 | 71 | commonColors: string[] 72 | skinColors: string[] 73 | backgroundColor: string[] 74 | borderColor: string[] 75 | } 76 | -------------------------------------------------------------------------------- /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 | commonColors: [ 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 | ], 88 | 89 | skinColors: ['#F8D9CE', '#F9C9B6', '#DEB3A3', '#C89583', '#9C6458'], 90 | 91 | get backgroundColor() { 92 | return [ 93 | ...this.commonColors, 94 | 'linear-gradient(45deg, #E3648C, #D97567)', 95 | 'linear-gradient(62deg, #8EC5FC, #E0C3FC)', 96 | 'linear-gradient(90deg, #ffecd2, #fcb69f)', 97 | 'linear-gradient(120deg, #a1c4fd, #c2e9fb)', 98 | 'linear-gradient(-135deg, #fccb90, #d57eeb)', 99 | 'transparent', 100 | ] 101 | }, 102 | 103 | get borderColor() { 104 | return [...this.commonColors, 'transparent'] 105 | }, 106 | } 107 | 108 | export const SCREEN = { 109 | lg: 976, 110 | } as const 111 | 112 | export const NONE = 'none' 113 | 114 | export const TRIGGER_PROBABILITY = 0.1 115 | 116 | export const SPECIAL_AVATARS: Readonly = [ 117 | { 118 | wrapperShape: 'squircle', 119 | background: { 120 | color: 'linear-gradient(62deg, #8EC5FC, #E0C3FC)', 121 | borderColor: 'transparent', 122 | }, 123 | widgets: { 124 | face: { 125 | shape: FaceShape.Base, 126 | fillColor: '#F9C9B6', 127 | }, 128 | tops: { 129 | shape: TopsShape.Pixie, 130 | fillColor: '#d2eff3', 131 | }, 132 | ear: { 133 | shape: EarShape.Attached, 134 | }, 135 | earrings: { 136 | shape: EarringsShape.Stud, 137 | }, 138 | eyebrows: { 139 | shape: EyebrowsShape.Up, 140 | }, 141 | eyes: { 142 | shape: EyesShape.Eyeshadow, 143 | }, 144 | nose: { 145 | shape: NoseShape.Pointed, 146 | }, 147 | glasses: { 148 | shape: NONE, 149 | }, 150 | mouth: { 151 | shape: MouthShape.Laughing, 152 | }, 153 | beard: { 154 | shape: NONE, 155 | }, 156 | clothes: { 157 | shape: ClothesShape.Crew, 158 | fillColor: '#e0ddff', 159 | }, 160 | }, 161 | }, 162 | { 163 | wrapperShape: 'squircle', 164 | background: { 165 | color: '#fd6f5d', 166 | borderColor: 'transparent', 167 | }, 168 | widgets: { 169 | face: { 170 | shape: FaceShape.Base, 171 | fillColor: '#F9C9B6', 172 | }, 173 | tops: { 174 | shape: TopsShape.Clean, 175 | }, 176 | ear: { 177 | shape: EarShape.Attached, 178 | }, 179 | earrings: { 180 | shape: NONE, 181 | }, 182 | eyebrows: { 183 | shape: EyebrowsShape.Eyelashesdown, 184 | }, 185 | eyes: { 186 | shape: EyesShape.Round, 187 | }, 188 | nose: { 189 | shape: NoseShape.Round, 190 | }, 191 | glasses: { 192 | shape: NONE, 193 | }, 194 | mouth: { 195 | shape: MouthShape.Surprised, 196 | }, 197 | beard: { 198 | shape: NONE, 199 | }, 200 | clothes: { 201 | shape: ClothesShape.Crew, 202 | fillColor: '#f4d150', 203 | }, 204 | }, 205 | }, 206 | ] 207 | 208 | export const NOT_COMPATIBLE_AGENTS = [ 209 | 'quark', 210 | 'micromessenger', 211 | 'weibo', 212 | 'douban', 213 | ] as const 214 | 215 | export const DOWNLOAD_DELAY = 800 216 | 217 | export const SHAPE_STYLE_SET = { 218 | [WrapperShape.Circle]: { 219 | borderRadius: '50%', 220 | }, 221 | [WrapperShape.Square]: { 222 | borderRadius: '0', 223 | }, 224 | [WrapperShape.Squircle]: { 225 | // TODO: Radius should adapt to the avatar size 226 | borderRadius: '25px', 227 | }, 228 | } 229 | -------------------------------------------------------------------------------- /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 { 2 | type EarringsShape, 3 | type GlassesShape, 4 | BeardShape, 5 | Gender, 6 | TopsShape, 7 | } from '@/enums' 8 | import type { AvatarOption, None } from '@/types' 9 | 10 | import { AVATAR_LAYER, NONE, SETTINGS, SPECIAL_AVATARS } from './constant' 11 | 12 | /** 13 | * Get a random value from an array. 14 | */ 15 | function getRandomValue( 16 | arr: Item[], 17 | { 18 | avoid = [], 19 | usually = [], 20 | }: { avoid?: unknown[]; usually?: (Item | 'none')[] } = {} 21 | ): Item { 22 | const avoidValues = avoid.filter(Boolean) 23 | const filteredArr = arr.filter((it) => !avoidValues.includes(it)) 24 | 25 | const usuallyValues = usually 26 | .filter(Boolean) 27 | .reduce((acc, cur) => acc.concat(new Array(15).fill(cur)), []) 28 | 29 | const finalArr = filteredArr.concat(usuallyValues) 30 | 31 | const randomIdx = Math.floor(Math.random() * finalArr.length) 32 | const randomValue = finalArr[randomIdx] 33 | 34 | return randomValue 35 | } 36 | 37 | export function getRandomFillColor(colors = SETTINGS.commonColors) { 38 | return colors[Math.floor(Math.random() * colors.length)] 39 | } 40 | 41 | export function getRandomAvatarOption( 42 | presetOption: Partial = {}, 43 | useOption: Partial = {} 44 | ): AvatarOption { 45 | const gender = getRandomValue(SETTINGS.gender) 46 | 47 | const beardList: BeardShape[] = [] 48 | let topList: TopsShape[] = [TopsShape.Danny, TopsShape.Wave, TopsShape.Pixie] 49 | 50 | if (gender === Gender.Male) { 51 | beardList.push(BeardShape.Scruff) 52 | topList = SETTINGS.topsShape.filter((shape) => !topList.includes(shape)) 53 | } 54 | 55 | const beardShape = getRandomValue(beardList, { 56 | usually: [NONE], 57 | }) 58 | 59 | const hairShape = getRandomValue(topList, { 60 | avoid: [useOption.widgets?.tops?.shape], 61 | }) 62 | const hairColor = getRandomFillColor() 63 | 64 | const avatarOption: AvatarOption = { 65 | gender, 66 | 67 | wrapperShape: 68 | presetOption?.wrapperShape || getRandomValue(SETTINGS.wrapperShape), 69 | 70 | background: { 71 | color: getRandomValue(SETTINGS.backgroundColor, { 72 | avoid: [ 73 | useOption.background?.color, 74 | (hairShape === TopsShape.Punk || hairShape === TopsShape.Fonze) && 75 | hairColor, // Handle special cases and prevent color conflicts. 76 | ], 77 | }), 78 | borderColor: getRandomValue(SETTINGS.borderColor, { 79 | avoid: [useOption.background?.color], 80 | usually: ['transparent'], 81 | }), 82 | }, 83 | 84 | widgets: { 85 | face: { 86 | shape: getRandomValue(SETTINGS.faceShape), 87 | fillColor: getRandomFillColor(SETTINGS.skinColors), 88 | }, 89 | tops: { 90 | shape: hairShape, 91 | fillColor: hairColor, 92 | }, 93 | ear: { 94 | shape: getRandomValue(SETTINGS.earShape, { 95 | avoid: [useOption.widgets?.ear?.shape], 96 | }), 97 | }, 98 | earrings: { 99 | shape: getRandomValue(SETTINGS.earringsShape, { 100 | usually: [NONE], 101 | }), 102 | }, 103 | eyebrows: { 104 | shape: getRandomValue(SETTINGS.eyebrowsShape, { 105 | avoid: [useOption.widgets?.eyebrows?.shape], 106 | }), 107 | }, 108 | eyes: { 109 | shape: getRandomValue(SETTINGS.eyesShape, { 110 | avoid: [useOption.widgets?.eyes?.shape], 111 | }), 112 | }, 113 | nose: { 114 | shape: getRandomValue(SETTINGS.noseShape, { 115 | avoid: [useOption.widgets?.nose?.shape], 116 | }), 117 | }, 118 | glasses: { 119 | shape: getRandomValue(SETTINGS.glassesShape, { 120 | usually: [NONE], 121 | }), 122 | }, 123 | mouth: { 124 | shape: getRandomValue(SETTINGS.mouthShape, { 125 | avoid: [useOption.widgets?.mouth?.shape], 126 | }), 127 | }, 128 | beard: { 129 | shape: beardShape, 130 | 131 | // HACK: 132 | ...(beardShape === BeardShape.Scruff 133 | ? { zIndex: AVATAR_LAYER['mouth'].zIndex - 1 } 134 | : undefined), 135 | }, 136 | clothes: { 137 | shape: getRandomValue(SETTINGS.clothesShape, { 138 | avoid: [useOption.widgets?.clothes?.shape], 139 | }), 140 | fillColor: getRandomFillColor(), 141 | }, 142 | }, 143 | } 144 | 145 | return avatarOption 146 | } 147 | 148 | export function getSpecialAvatarOption(): AvatarOption { 149 | return SPECIAL_AVATARS[Math.floor(Math.random() * SPECIAL_AVATARS.length)] 150 | } 151 | 152 | export function showConfetti() { 153 | import('canvas-confetti').then((confetti) => { 154 | const canvasEle: HTMLCanvasElement | null = 155 | document.querySelector('#confetti') 156 | 157 | if (!canvasEle) { 158 | return 159 | } 160 | 161 | const myConfetti = confetti.create(canvasEle, { 162 | resize: true, 163 | useWorker: true, 164 | disableForReducedMotion: true, 165 | }) 166 | 167 | const duration = performance.now() + 1 * 1000 168 | 169 | const confettiColors = ['#6967fe', '#85e9f4', '#e16984'] 170 | 171 | void (function frame() { 172 | myConfetti({ 173 | particleCount: confettiColors.length, 174 | angle: 60, 175 | spread: 55, 176 | origin: { x: 0 }, 177 | colors: confettiColors, 178 | }) 179 | myConfetti({ 180 | particleCount: confettiColors.length, 181 | angle: 120, 182 | spread: 55, 183 | origin: { x: 1 }, 184 | colors: confettiColors, 185 | }) 186 | 187 | if (performance.now() < duration) { 188 | requestAnimationFrame(frame) 189 | } 190 | })() 191 | }) 192 | } 193 | 194 | export function highlightJSON(json: string): string { 195 | if (!json) { 196 | return '' 197 | } 198 | 199 | if (typeof json != 'string') { 200 | json = JSON.stringify(json, undefined, 2) 201 | } 202 | 203 | json = json.replace(/&/g, '&').replace(//g, '>') 204 | 205 | return json.replace( 206 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, 207 | (match) => { 208 | let cls = '' 209 | if (/^"/.test(match)) { 210 | if (/:$/.test(match)) { 211 | cls = 'key' 212 | } else { 213 | cls = 'string' 214 | } 215 | } else if (/true|false/.test(match)) { 216 | cls = 'boolean' 217 | } else if (/null/.test(match)) { 218 | cls = 'null' 219 | } else { 220 | cls = 'number' 221 | } 222 | return `${match}` 223 | } 224 | ) 225 | } 226 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------