├── .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 |
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 |
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 |
84 |
85 |
86 |
87 |
97 | Front-End Only
98 | Avatar Generator
107 |
108 |
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 |
9 |
--------------------------------------------------------------------------------
/landing-page/vite.svg:
--------------------------------------------------------------------------------
1 |
42 |
--------------------------------------------------------------------------------
/landing-page/vue.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-close.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-flip.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-github.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/clothes/collared.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/clothes/crew.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/clothes/open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/ear/attached.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/ear/detached.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/earrings/hoop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/earrings/stud.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyebrows/down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyebrows/eyelashesdown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyebrows/eyelashesup.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyebrows/up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyes/ellipse.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyes/eyeshadow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyes/round.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/eyes/smiling.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/face/base.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/glasses/round.svg:
--------------------------------------------------------------------------------
1 |
32 |
--------------------------------------------------------------------------------
/src/assets/preview/glasses/square.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/frown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/laughing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/nervous.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/pucker.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/sad.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/smile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/smirk.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/mouth/surprised.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/nose/curve.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/nose/pointed.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/nose/round.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/beanie.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/clean.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/danny.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/fonze.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/funny.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/pixie.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/punk.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/turban.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/preview/tops/wave.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/clothes/collared.svg:
--------------------------------------------------------------------------------
1 |
58 |
--------------------------------------------------------------------------------
/src/assets/widgets/clothes/crew.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/src/assets/widgets/clothes/open.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/assets/widgets/ear/attached.svg:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/src/assets/widgets/ear/detached.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/src/assets/widgets/earrings/hoop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/earrings/stud.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyebrows/down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyebrows/eyelashesdown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyebrows/eyelashesup.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyebrows/up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyes/ellipse.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyes/eyeshadow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyes/round.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/eyes/smiling.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/face/base.svg:
--------------------------------------------------------------------------------
1 |
60 |
--------------------------------------------------------------------------------
/src/assets/widgets/glasses/round.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/glasses/square.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/frown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/laughing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/nervous.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/pucker.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/sad.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/smile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/smirk.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/mouth/surprised.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/nose/curve.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/nose/pointed.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/nose/round.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/beanie.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/clean.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/danny.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/fonze.svg:
--------------------------------------------------------------------------------
1 |
32 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/funny.svg:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/pixie.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/punk.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/turban.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/widgets/tops/wave.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/components/ActionBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
63 |
64 |
94 |
--------------------------------------------------------------------------------
/src/components/ConfettiCanvas.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
17 |
--------------------------------------------------------------------------------
/src/components/Modal/BatchDownloadModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{ t('text.downloadMultipleTip') }}
6 |
7 |
15 |
16 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
138 |
139 |
251 |
--------------------------------------------------------------------------------
/src/components/Modal/CodeModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
28 |
29 |
30 |
31 |
32 |
33 |
88 |
89 |
204 |
205 |
234 |
--------------------------------------------------------------------------------
/src/components/Modal/DownloadModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
![vue-color-avatar]()
15 |
16 |
17 |
{{ t('text.downloadTip') }} 🥳
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
38 |
39 |
134 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
41 |
--------------------------------------------------------------------------------
/src/components/PerfectScrollbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
34 |
35 |
43 |
--------------------------------------------------------------------------------
/src/components/SectionWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ props.title }}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
27 |
--------------------------------------------------------------------------------
/src/components/VueColorAvatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
28 |
29 |
141 |
142 |
155 |
--------------------------------------------------------------------------------
/src/components/widgets/Background.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
25 |
--------------------------------------------------------------------------------
/src/components/widgets/Border.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
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 |
2 |
5 |
6 |
7 |
54 |
55 |
75 |
--------------------------------------------------------------------------------
/src/layouts/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
36 |
37 |
68 |
--------------------------------------------------------------------------------
/src/layouts/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
35 |
36 |
88 |
--------------------------------------------------------------------------------
/src/layouts/Sider.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
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 |
--------------------------------------------------------------------------------