vue-color-avatar/src/App.vue

400 lines
9.3 KiB
Vue
Raw Normal View History

2021-10-16 10:29:32 +00:00
<template>
<main class="main">
<Container>
<div class="content-warpper">
<div class="content-view">
<Header />
<div class="playground">
<div class="avatar-wrapper">
<VueColorAvatar
ref="colorAvatarRef"
:option="avatarOption"
:size="280"
:style="{
transform: `rotateY(${flipped ? -180 : 0}deg)`,
}"
/>
</div>
<ActionBar @action="handleAction" />
2021-10-16 10:29:32 +00:00
<div class="action-group">
2022-06-12 06:38:40 +00:00
<button
type="button"
class="action-btn action-randomize"
@click="handleGenerate"
>
2021-10-16 10:29:32 +00:00
{{ t('action.randomize') }}
</button>
2022-06-12 06:38:40 +00:00
2021-10-16 10:29:32 +00:00
<button
2022-06-12 06:38:40 +00:00
type="button"
class="action-btn action-download"
2021-10-16 10:29:32 +00:00
:disabled="downloading"
@click="handleDownload"
>
{{
downloading
? `${t('action.downloading')}...`
: t('action.download')
}}
</button>
2022-06-12 06:38:40 +00:00
<button
type="button"
class="action-btn action-multiple"
@click="() => generateMultiple()"
>
{{ t('action.downloadMultiple') }}
</button>
2021-10-16 10:29:32 +00:00
</div>
</div>
<Footer />
<CodeModal :visible="codeVisible" @close="codeVisible = false" />
<DownloadModal
:visible="downloadModalVisible"
:image-url="imageDataURL"
@close=";(downloadModalVisible = false), (imageDataURL = '')"
/>
</div>
<ConfettiCanvas />
2021-10-16 10:29:32 +00:00
<div class="gradient-bg">
<div class="gradient-top"></div>
<div class="gradient-bottom"></div>
</div>
</div>
</Container>
2022-06-12 06:38:40 +00:00
<BatchDownloadModal
:visible="avatarListVisible"
:avatar-list="avatarList"
2022-06-28 07:06:41 +00:00
@regenerate="generateMultiple"
2022-06-12 06:38:40 +00:00
@close=";(avatarListVisible = false), (avatarList = [])"
/>
2021-10-16 10:29:32 +00:00
<Sider>
<Configurator />
</Sider>
</main>
</template>
<script lang="ts" setup>
2022-06-12 06:38:40 +00:00
import { ref, watchEffect } from 'vue'
2021-10-16 10:29:32 +00:00
import { useI18n } from 'vue-i18n'
import ActionBar from '@/components/ActionBar.vue'
import Configurator from '@/components/Configurator.vue'
2022-06-12 06:38:40 +00:00
import BatchDownloadModal from '@/components/Modal/BatchDownloadModal.vue'
import CodeModal from '@/components/Modal/CodeModal.vue'
import DownloadModal from '@/components/Modal/DownloadModal.vue'
2022-02-26 03:30:06 +00:00
import VueColorAvatar, {
type VueColorAvatarRef,
} from '@/components/VueColorAvatar.vue'
2021-10-16 10:29:32 +00:00
import { ActionType } from '@/enums'
import { useAvatarOption } from '@/hooks'
import Container from '@/layouts/Container.vue'
import Footer from '@/layouts/Footer.vue'
import Header from '@/layouts/Header.vue'
import Sider from '@/layouts/Sider.vue'
import { useStore } from '@/store'
import { REDO, UNDO } from '@/store/mutation-type'
import {
getRandomAvatarOption,
getSpecialAvatarOption,
showConfetti,
} from '@/utils'
import {
DOWNLOAD_DELAY,
NOT_COMPATIBLE_AGENTS,
TRIGGER_PROBABILITY,
} from '@/utils/constant'
2021-10-19 13:12:37 +00:00
import { recordEvent } from '@/utils/ga'
2021-10-16 10:29:32 +00:00
2022-06-12 06:38:40 +00:00
import { name as appName } from '../package.json'
import ConfettiCanvas from './components/ConfettiCanvas.vue'
2022-06-12 06:38:40 +00:00
import type { AvatarOption } from './types'
2021-10-16 10:29:32 +00:00
const store = useStore()
const [avatarOption, setAvatarOption] = useAvatarOption()
const { t } = useI18n()
const colorAvatarRef = ref<VueColorAvatarRef>()
function handleGenerate() {
if (Math.random() <= TRIGGER_PROBABILITY) {
let colorfulOption = getSpecialAvatarOption()
while (
JSON.stringify(colorfulOption) === JSON.stringify(avatarOption.value)
) {
colorfulOption = getSpecialAvatarOption()
}
2021-10-19 15:45:30 +00:00
colorfulOption.wrapperShape = avatarOption.value.wrapperShape
2021-10-16 10:29:32 +00:00
setAvatarOption(colorfulOption)
showConfetti()
} else {
const randomOption = getRandomAvatarOption(avatarOption.value)
setAvatarOption(randomOption)
}
2021-10-19 13:12:37 +00:00
recordEvent('click_randomize', {
event_category: 'click',
})
2021-10-16 10:29:32 +00:00
}
const downloadModalVisible = ref(false)
const downloading = ref(false)
const imageDataURL = ref('')
async function handleDownload() {
try {
downloading.value = true
const avatarEle = colorAvatarRef.value?.avatarRef
const userAgent = window.navigator.userAgent.toLowerCase()
const notCompatible = NOT_COMPATIBLE_AGENTS.some(
(agent) => userAgent.indexOf(agent) !== -1
)
if (avatarEle) {
const html2canvas = (await import('html2canvas')).default
const canvas = await html2canvas(avatarEle, {
backgroundColor: null,
})
const dataURL = canvas.toDataURL()
if (notCompatible) {
imageDataURL.value = dataURL
downloadModalVisible.value = true
} else {
const trigger = document.createElement('a')
trigger.href = dataURL
2022-06-12 06:38:40 +00:00
trigger.download = `${appName}.png`
2021-10-16 10:29:32 +00:00
trigger.click()
}
}
2021-10-19 13:12:37 +00:00
recordEvent('click_download', {
event_category: 'click',
})
2021-10-16 10:29:32 +00:00
} finally {
setTimeout(() => {
downloading.value = false
}, DOWNLOAD_DELAY)
}
}
const flipped = ref(false)
const codeVisible = ref(false)
function handleAction(actionType: ActionType) {
switch (actionType) {
case ActionType.Undo:
store[UNDO]()
2021-10-19 13:12:37 +00:00
recordEvent('action_undo', {
event_category: 'action',
event_label: 'Undo',
})
2021-10-16 10:29:32 +00:00
break
case ActionType.Redo:
store[REDO]()
2021-10-19 13:12:37 +00:00
recordEvent('action_redo', {
event_category: 'action',
event_label: 'Redo',
})
2021-10-16 10:29:32 +00:00
break
case ActionType.Flip:
flipped.value = !flipped.value
2021-10-19 13:12:37 +00:00
recordEvent('action_flip_avatar', {
event_category: 'action',
event_label: 'Flip Avatar',
})
2021-10-16 10:29:32 +00:00
break
case ActionType.Code:
codeVisible.value = !codeVisible.value
2021-10-19 13:12:37 +00:00
recordEvent('action_view_code', {
event_category: 'action',
event_label: 'View Avatar Option Code',
2021-10-17 09:32:01 +00:00
})
2021-10-16 10:29:32 +00:00
break
}
}
2022-06-12 06:38:40 +00:00
const avatarListVisible = ref(false)
const avatarList = ref<AvatarOption[]>([])
watchEffect(() => {
avatarListVisible.value =
Array.isArray(avatarList.value) && avatarList.value.length > 0
})
async function generateMultiple(count = 5 * 6) {
const { default: hash } = await import('object-hash')
const avatarMap = [...Array(count)].reduce<Map<string, AvatarOption>>(
(res) => {
let randomAvatarOption: AvatarOption
let hashKey: string
do {
randomAvatarOption = getRandomAvatarOption(avatarOption.value)
hashKey = hash.sha1(randomAvatarOption)
} while (
randomAvatarOption.background.color === 'transparent' ||
res.has(hashKey)
)
res.set(hashKey, randomAvatarOption)
return res
},
new Map()
)
avatarList.value = Array.from(avatarMap.values())
recordEvent('click_generate_multiple', {
event_category: 'click',
})
}
2021-10-16 10:29:32 +00:00
</script>
<style lang="scss" scoped>
@use 'src/styles/var';
2021-10-16 10:29:32 +00:00
.main {
width: 100%;
height: 100%;
overflow: hidden;
color: var.$color-text;
background-color: var.$color-page-bg;
2021-10-16 10:29:32 +00:00
.content-warpper {
height: 100%;
transform: scale(1);
.content-view {
position: relative;
z-index: 110;
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
}
}
}
.playground {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 0;
.avatar-wrapper {
display: flex;
align-items: center;
justify-content: center;
@media screen and (max-width: var.$screen-sm) {
2021-10-16 10:29:32 +00:00
transform: scale(0.85);
}
}
.action-group {
display: flex;
align-items: center;
justify-content: center;
margin-top: 4rem;
2022-06-12 06:38:40 +00:00
column-gap: 1rem;
2021-10-16 10:29:32 +00:00
2022-06-12 06:38:40 +00:00
@supports not (column-gap: 1rem) {
.action-btn {
margin: 0 0.5rem;
}
}
.action-btn {
2021-10-16 10:29:32 +00:00
min-width: 6rem;
height: 2.5rem;
padding: 0 1rem;
color: var.$color-text;
2021-10-16 10:29:32 +00:00
font-weight: bold;
background: var.$color-gray;
2021-10-16 10:29:32 +00:00
border-radius: 0.6rem;
cursor: pointer;
transition: color 0.2s;
user-select: none;
&:hover {
color: lighten(var.$color-text, 10);
2021-10-16 10:29:32 +00:00
}
2021-10-17 03:13:46 +00:00
&:disabled,
&[disabled] {
color: rgba(var.$color-text, 0.5);
2021-10-16 10:29:32 +00:00
cursor: default;
}
}
2022-06-12 06:38:40 +00:00
@media screen and (max-width: var.$screen-sm) {
.action-multiple {
display: none;
}
}
2021-10-16 10:29:32 +00:00
}
}
2021-10-19 13:59:01 +00:00
@supports (filter: blur(4rem)) or (-webkit-filter: blur(4rem)) or
(-moz-filter: blur(4rem)) {
.gradient-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
@mixin gradient-style($color) {
position: absolute;
width: 100vh;
height: 100vh;
background-image: radial-gradient(
rgba($color, 0.8) 20%,
rgba($color, 0.6) 40%,
rgba($color, 0.4) 60%,
rgba($color, 0.2) 80%,
transparent 100%
);
border-radius: 50%;
opacity: 0.2;
filter: blur(4rem);
}
2021-10-16 10:29:32 +00:00
2021-10-19 13:59:01 +00:00
.gradient-top {
@include gradient-style(var.$color-secondary);
2021-10-16 10:29:32 +00:00
2021-10-19 13:59:01 +00:00
top: -50%;
right: -20%;
}
2021-10-16 10:29:32 +00:00
2021-10-19 13:59:01 +00:00
.gradient-bottom {
@include gradient-style(var.$color-accent);
2021-10-16 10:29:32 +00:00
2021-10-19 13:59:01 +00:00
bottom: -50%;
left: -20%;
}
2021-10-16 10:29:32 +00:00
}
}
</style>