From aa40714334d7824d05b4ed107cfacff3e300eb52 Mon Sep 17 00:00:00 2001 From: maxikobi Date: Fri, 21 Nov 2025 01:43:30 +0200 Subject: [PATCH 1/4] Add animation to icons in sidebar --- apps/app-frontend/src/App.vue | 99 ++++++++++++++++++++++++++- packages/assets/icons/change-skin.svg | 39 +++++++++-- packages/assets/icons/library.svg | 17 ++++- 3 files changed, 147 insertions(+), 8 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 6f2a17a8af..64d7538304 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -804,10 +804,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload) :is-primary="() => route.path.startsWith('/browse') && !route.query.i" :is-subpage="(route) => route.path.startsWith('/project') && !route.query.i" > - + - + - +
@@ -1351,6 +1351,99 @@ provideAppUpdateDownloadProgress(appUpdateDownload) opacity: 0; } +.compass-icon:hover { + animation: compass-spin 0.75s cubic-bezier(0.68, -0.46, 0.34, 1.37); + + @keyframes compass-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +} + +#change-skin-icon { + width: 3rem; +} +#change-skin-icon:hover :deep(.hook) { + transform-origin: 24.704px 7.7px; + animation: swing 0.75s linear; + --swing-angle: 30deg; +} + +#change-skin-icon:hover :deep(.shirt) { + transform-origin: 24px 19.102px; + animation: swing 0.75s ease-out 0.15s; + --swing-angle: 40deg; +} + +@keyframes swing { + 0%, + 100% { + transform: rotate(0deg); + } + 25%, + 32% { + transform: rotate(var(--swing-angle)); + } + 65% { + transform: rotate(calc(var(--swing-angle) * -0.75)); + } + 85% { + transform: rotate(7deg); + } +} + +#library-icon { + height: 3rem; + --jump-duration: 0.5s; + --jump-timing: cubic-bezier(1, 0, 0.75, 1); + --jump-delay: 0.1s; +} + +#library-icon:hover :deep(.item1) { + animation: jump var(--jump-duration) var(--jump-timing); +} + +#library-icon:hover :deep(.item2) { + animation: jump var(--jump-duration) var(--jump-timing) var(--jump-delay); +} + +#library-icon:hover :deep(.item3) { + animation: jump var(--jump-duration) var(--jump-timing) calc(var(--jump-delay) * 2); +} + +#library-icon:hover :deep(.item4) { + transform-origin: 20px 20px; + animation: lean 0.65s cubic-bezier(0.2, 0.5, 0.7, 0.3) calc(var(--jump-delay) * 2.7); +} + +@keyframes jump { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-30%); + } +} + +@keyframes lean { + 0%, + 100% { + transform: rotate(0deg); + } + 25%, + 70% { + transform: rotate(14deg); + } + 50% { + transform: rotate(16deg); + } +} + @media (prefers-reduced-motion: no-preference) { .toast-enter-active, .nav-button-animated-enter-active { diff --git a/packages/assets/icons/change-skin.svg b/packages/assets/icons/change-skin.svg index 7626051500..43e169313f 100644 --- a/packages/assets/icons/change-skin.svg +++ b/packages/assets/icons/change-skin.svg @@ -1,5 +1,36 @@ - - - - + + + + + + + + + diff --git a/packages/assets/icons/library.svg b/packages/assets/icons/library.svg index 828635efcc..898cc30bad 100644 --- a/packages/assets/icons/library.svg +++ b/packages/assets/icons/library.svg @@ -1 +1,16 @@ - + + + + + + From 969175285cc01a7c1118cedf22fd9a4a6b8ce8c6 Mon Sep 17 00:00:00 2001 From: maxikobi Date: Mon, 24 Nov 2025 03:08:54 +0200 Subject: [PATCH 2/4] Added new animatedIcon component and moved animations inside the icon svg files --- apps/app-frontend/src/App.vue | 94 +------------------ .../src/components/ui/AnimatedIcon.vue | 69 ++++++++++++++ apps/app-frontend/vite.config.ts | 3 + packages/assets/icons/change-skin.svg | 31 ++++++ packages/assets/icons/compass.svg | 33 ++++++- packages/assets/icons/library.svg | 46 ++++++++- 6 files changed, 182 insertions(+), 94 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/AnimatedIcon.vue diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 64d7538304..ef2cd4aba1 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -97,6 +97,7 @@ import { import { useError } from '@/store/error.js' import { useInstall } from '@/store/install.js' import { useLoading, useTheming } from '@/store/state' +import AnimatedIcon from './components/ui/AnimatedIcon.vue' import { create_profile_and_install_from_file } from './helpers/pack' import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' @@ -804,10 +805,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload) :is-primary="() => route.path.startsWith('/browse') && !route.query.i" :is-subpage="(route) => route.path.startsWith('/project') && !route.query.i" > - + - + - +
@@ -1351,97 +1352,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload) opacity: 0; } -.compass-icon:hover { - animation: compass-spin 0.75s cubic-bezier(0.68, -0.46, 0.34, 1.37); - - @keyframes compass-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } -} - #change-skin-icon { width: 3rem; } -#change-skin-icon:hover :deep(.hook) { - transform-origin: 24.704px 7.7px; - animation: swing 0.75s linear; - --swing-angle: 30deg; -} - -#change-skin-icon:hover :deep(.shirt) { - transform-origin: 24px 19.102px; - animation: swing 0.75s ease-out 0.15s; - --swing-angle: 40deg; -} - -@keyframes swing { - 0%, - 100% { - transform: rotate(0deg); - } - 25%, - 32% { - transform: rotate(var(--swing-angle)); - } - 65% { - transform: rotate(calc(var(--swing-angle) * -0.75)); - } - 85% { - transform: rotate(7deg); - } -} #library-icon { - height: 3rem; - --jump-duration: 0.5s; - --jump-timing: cubic-bezier(1, 0, 0.75, 1); - --jump-delay: 0.1s; -} - -#library-icon:hover :deep(.item1) { - animation: jump var(--jump-duration) var(--jump-timing); -} - -#library-icon:hover :deep(.item2) { - animation: jump var(--jump-duration) var(--jump-timing) var(--jump-delay); -} - -#library-icon:hover :deep(.item3) { - animation: jump var(--jump-duration) var(--jump-timing) calc(var(--jump-delay) * 2); -} - -#library-icon:hover :deep(.item4) { - transform-origin: 20px 20px; - animation: lean 0.65s cubic-bezier(0.2, 0.5, 0.7, 0.3) calc(var(--jump-delay) * 2.7); -} - -@keyframes jump { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-30%); - } -} - -@keyframes lean { - 0%, - 100% { - transform: rotate(0deg); - } - 25%, - 70% { - transform: rotate(14deg); - } - 50% { - transform: rotate(16deg); - } + height: 2rem; } @media (prefers-reduced-motion: no-preference) { diff --git a/apps/app-frontend/src/components/ui/AnimatedIcon.vue b/apps/app-frontend/src/components/ui/AnimatedIcon.vue new file mode 100644 index 0000000000..27a6f070cb --- /dev/null +++ b/apps/app-frontend/src/components/ui/AnimatedIcon.vue @@ -0,0 +1,69 @@ + + + diff --git a/apps/app-frontend/vite.config.ts b/apps/app-frontend/vite.config.ts index ac1c29a042..c0303f6290 100644 --- a/apps/app-frontend/vite.config.ts +++ b/apps/app-frontend/vite.config.ts @@ -26,6 +26,9 @@ export default defineConfig({ name: 'preset-default', params: { overrides: { + cleanupIds: false, + inlineStyles: false, + minifyStyles: false, removeViewBox: false, }, }, diff --git a/packages/assets/icons/change-skin.svg b/packages/assets/icons/change-skin.svg index 43e169313f..f3e38dcd9d 100644 --- a/packages/assets/icons/change-skin.svg +++ b/packages/assets/icons/change-skin.svg @@ -3,7 +3,38 @@ xml:space="preserve" transform="scale(-1 1)" viewBox="-15.425 0 80 55" + class="shirt-icon" > + \ No newline at end of file + + + + + diff --git a/packages/assets/icons/library.svg b/packages/assets/icons/library.svg index 898cc30bad..256c833ee3 100644 --- a/packages/assets/icons/library.svg +++ b/packages/assets/icons/library.svg @@ -8,9 +8,47 @@ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" + class="library-icon" > - - - - + + + + + + From 2d22c00d45a46710cb62f9efff326cbc2f4886cc Mon Sep 17 00:00:00 2001 From: maxikobi Date: Mon, 24 Nov 2025 14:39:31 +0200 Subject: [PATCH 3/4] added support for infinite animation and added new animations --- apps/app-frontend/src/App.vue | 2 +- .../src/components/ui/AnimatedIcon.vue | 43 +++++++++++++++---- apps/app-frontend/src/pages/Skins.vue | 3 +- packages/assets/icons/settings.svg | 32 ++++++++++++-- packages/assets/icons/updated.svg | 32 +++++++++++++- 5 files changed, 98 insertions(+), 14 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index ef2cd4aba1..c62f81695f 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -878,7 +878,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) v-tooltip.right="formatMessage(commonMessages.settingsLabel)" :to="() => $refs.settingsModal.show()" > - + { await nextTick() @@ -25,26 +27,37 @@ onMounted(async () => { // calculate animation duration el.classList.add('animating') el.offsetHeight - longestAnimation = calculateLongestAnimation(el) + AnimationLength = calculateLongestAnimation(el) el.classList.remove('animating') // use parent if it exists, otherwise fallback to component itself target = el.parentElement || el target.addEventListener('mouseenter', playAnimation) + target.addEventListener('mouseleave', stopHover) }) onBeforeUnmount(() => { - if (target) target.removeEventListener('mouseenter', playAnimation) + if (!target) return + target.removeEventListener('mouseenter', playAnimation) + target.removeEventListener('mouseleave', stopHover) }) const playAnimation = () => { + hover = true if (isAnimating.value) return isAnimating.value = true setTimeout(() => { isAnimating.value = false - }, longestAnimation) + if (isInfinite && hover) { + playAnimation() + } + }, AnimationLength) +} + +function stopHover() { + hover = false } function calculateLongestAnimation(el) { @@ -54,16 +67,30 @@ function calculateLongestAnimation(el) { const iconElements = [el, ...el.querySelectorAll('*')] iconElements.forEach((child) => { const style = getComputedStyle(child) - const durations = style.animationDuration.split(',').map((s) => parseFloat(s) || 0) - const delays = style.animationDelay.split(',').map((s) => parseFloat(s) || 0) + const durations = style.animationDuration.split(',').map((s) => parseTimeMs(s)) + const delays = style.animationDelay.split(',').map((s) => parseTimeMs(s)) + const iterations = style.animationIterationCount.split(',').map((s) => { + if (s === 'infinite') { + isInfinite = true + return 1 + } + return parseFloat(s) || 1 + }) - durations.forEach((d, i) => { + durations.forEach((duration, i) => { const delay = delays[i] || 0 - const total = (d + delay) * 1000 + const iter = iterations[i] || 1 + const total = duration * iter + delay if (total > maxDuration) maxDuration = total }) }) return maxDuration } + +function parseTimeMs(s) { + const num = parseFloat(s) + if (!num) return 0 + return num * (s.endsWith('ms') ? 1 : 1000) +} diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue index 49461d2b67..b12026a685 100644 --- a/apps/app-frontend/src/pages/Skins.vue +++ b/apps/app-frontend/src/pages/Skins.vue @@ -23,6 +23,7 @@ import type { Ref } from 'vue' import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue' import type AccountsCard from '@/components/ui/AccountsCard.vue' +import AnimatedIcon from '@/components/ui/AnimatedIcon.vue' import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue' import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue' import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue' @@ -353,7 +354,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()]) ) " > - + Change cape diff --git a/packages/assets/icons/settings.svg b/packages/assets/icons/settings.svg index 5accf92ee3..0feff8b9bd 100644 --- a/packages/assets/icons/settings.svg +++ b/packages/assets/icons/settings.svg @@ -1,6 +1,32 @@ - + + + d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" + /> diff --git a/packages/assets/icons/updated.svg b/packages/assets/icons/updated.svg index b4a29e8ac0..da0adb6a62 100644 --- a/packages/assets/icons/updated.svg +++ b/packages/assets/icons/updated.svg @@ -1 +1,31 @@ - + + + + + + From dad42364d13dbc71b12c04342019bfa5853197a8 Mon Sep 17 00:00:00 2001 From: maxikobi Date: Mon, 24 Nov 2025 14:41:23 +0200 Subject: [PATCH 4/4] Added blur to overflowing searchCard tags --- apps/app-frontend/src/components/ui/SearchCard.vue | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/app-frontend/src/components/ui/SearchCard.vue b/apps/app-frontend/src/components/ui/SearchCard.vue index 7f7b6c444b..c0779fc613 100644 --- a/apps/app-frontend/src/components/ui/SearchCard.vue +++ b/apps/app-frontend/src/components/ui/SearchCard.vue @@ -14,7 +14,7 @@
-
+
{{ project.title }} @@ -24,7 +24,10 @@
{{ project.description }}
-
+
{{ formatCategory(tag.name) }}
-
+
@@ -87,7 +90,7 @@
-
+