polish: skin buttons

This commit is contained in:
Calum H.
2025-05-22 00:24:07 +01:00
committed by Alejandro González
parent 6a18f7cf04
commit 560b674cb7
4 changed files with 90 additions and 126 deletions

View File

@@ -21,7 +21,7 @@ class BatchSkinRenderer {
private bodyNode: THREE.Object3D | null = null
private capeAttached: boolean = false
constructor(width: number = 215, height: number = 215 * 2) {
constructor(width: number = 360, height: number = 504) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
@@ -155,7 +155,7 @@ class BatchSkinRenderer {
if (headPart) {
const headPosition = new THREE.Vector3()
headPart.getWorldPosition(headPosition)
lookAtTarget = [headPosition.x, headPosition.y, headPosition.z]
lookAtTarget = [headPosition.x, headPosition.y - 0.3, headPosition.z]
} else {
throw new Error("Failed to find 'Head' object in model.")
}
@@ -219,7 +219,7 @@ class BatchSkinRenderer {
const group = new THREE.Group()
group.add(model)
group.position.set(0, -0.5, 1.95)
group.position.set(0, 0.3, 1.95)
group.scale.set(0.8, 0.8, 0.8)
this.scene.add(group)

View File

@@ -7,7 +7,8 @@ import {
SkinLikeTextButton,
Button
} from '@modrinth/ui'
import {ref, computed, useTemplateRef, watch, onMounted, onUnmounted, inject, Ref} from 'vue'
import type { Ref} from 'vue';
import {ref, computed, useTemplateRef, watch, onMounted, onUnmounted, inject} from 'vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import { handleError } from '@/store/notifications'
@@ -26,7 +27,7 @@ import type { RenderResult } from '@/helpers/rendering/batchSkinRenderer.ts'
import { generateSkinPreviews, map } from '@/helpers/rendering/batchSkinRenderer.ts'
import {handleSevereError} from "@/store/error";
import {trackEvent} from "@/helpers/analytics";
import AccountsCard from "@/components/ui/AccountsCard.vue";
import type AccountsCard from "@/components/ui/AccountsCard.vue";
const editSkinModal = useTemplateRef('editSkinModal')
const selectCapeModal = useTemplateRef('selectCapeModal')
@@ -178,8 +179,8 @@ watch(
/>
<SelectCapeModal ref="selectCapeModal" :capes="capes" @select="handleCapeSelected" />
<div v-if="currentUser" class="p-6 grid grid-cols-[300px_1fr] xl:grid-cols-[3fr_5fr] gap-6">
<div class="sticky top-6 self-start">
<div v-if="currentUser" class="p-4 grid grid-cols-[300px_1fr] xl:grid-cols-[3fr_5fr] gap-6">
<div class="sticky top-6 self-start p-2">
<div class="flex justify-between gap-4">
<h1 class="m-0 text-2xl font-bold">Skins</h1>
<ButtonStyled :disabled="!!selectedSkin?.cape_id">
@@ -219,12 +220,12 @@ watch(
</div>
</div>
<div class="flex flex-col gap-6 add-perspective">
<section class="flex flex-col gap-3">
<div class="flex flex-col gap-6 add-perspective pt-2">
<section class="flex flex-col gap-2 mt-1">
<h2 class="text-lg font-bold m-0 text-primary">Saved skins</h2>
<div class="flex flex-row flex-wrap gap-2">
<div class="flex flex-row flex-wrap gap-2 w-full">
<SkinLikeTextButton
class="flex-none w-[8vw] h-[15vw] max-w-[8rem] max-h-[15rem]"
class="flex-none skin-card"
tooltip="Add a skin"
@click="editSkinModal?.show"
>
@@ -237,7 +238,7 @@ watch(
<SkinButton
v-for="skin in savedSkins"
:key="`saved-skin-${skin.texture_key}`"
class="flex-none w-[8vw] h-[15vw] max-w-[8rem] max-h-[15rem]"
class="flex-none skin-card"
editable
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
@@ -248,13 +249,13 @@ watch(
</div>
</section>
<section class="flex flex-col gap-3">
<section class="flex flex-col gap-2 mt-1 mr-0">
<h2 class="text-lg font-bold m-0 text-primary">Default skins</h2>
<div class="flex flex-row flex-wrap gap-2">
<div class="flex flex-row flex-wrap gap-2 w-full">
<SkinButton
v-for="skin in defaultSkins"
:key="`default-skin-${skin.texture_key}`"
class="flex-none w-[8vw] h-[15vw] max-w-[8rem] max-h-[15rem]"
class="flex-none skin-card"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="selectedSkin === skin"
@@ -277,10 +278,10 @@ watch(
<p class="text-lg m-0">
Please sign into your Minecraft account to use the skin management features of the Modrinth app.
</p>
<ButtonStyled color="brand" :disabled="accountsCard.loginDisabled" v-show="accountsCard">
<Button @click="login" :disabled="accountsCard.loginDisabled">
<ButtonStyled v-show="accountsCard" color="brand" :disabled="accountsCard.loginDisabled">
<Button :disabled="accountsCard.loginDisabled" @click="login">
<LogInIcon v-if="!accountsCard.loginDisabled" />
<SpinnerIcon class="animate-spin" v-else />
<SpinnerIcon v-else class="animate-spin" />
Sign In
</Button>
</ButtonStyled>
@@ -288,3 +289,17 @@ watch(
</div>
</div>
</template>
<style lang="scss" scoped>
$skin-card-width: 10.5vw;
$skin-card-aspect: 1.1;
.skin-card {
width: $skin-card-width;
min-width: $skin-card-width;
max-width: $skin-card-width;
height: calc(#{$skin-card-width} * #{$skin-card-aspect});
min-height: calc(#{$skin-card-width} * #{$skin-card-aspect});
max-height: calc(#{$skin-card-width} * #{$skin-card-aspect});
}
</style>

View File

@@ -21,7 +21,6 @@ const props = withDefaults(defineProps<{
editable: false,
})
const pressed = ref(false)
const imagesLoaded = ref({
forward: Boolean(props.forwardImageSrc),
backward: Boolean(props.backwardImageSrc)
@@ -35,45 +34,44 @@ function onImageLoad(type: 'forward' | 'backward') {
<template>
<div
v-tooltip="tooltip ?? undefined"
class="skin-button__parent group flex relative border-2 border-solid transform-3d rotate-y-90 transition-all p-0 bg-transparent rounded-xl overflow-hidden"
class="group flex relative overflow-hidden rounded-xl border-solid border-2 transition-colors duration-200"
:class="[
selected ? `border-brand` : 'border-transparent',
{
'scale-95': pressed,
},
selected ? 'border-brand' : 'border-transparent hover:border-white/50'
]"
>
<button
class="absolute inset-0 rounded-xl cursor-pointer p-0 border-none group-hover:brightness-125"
:class="selected ? `bg-brand-highlight` : 'bg-button-bg'"
@mousedown="pressed = true"
@mouseup="pressed = false"
@mouseleave="pressed = false"
class="skin-btn-bg absolute inset-0 rounded-xl cursor-pointer p-0 border-none group-hover:brightness-125"
:class="selected ? 'selected' : ''"
@click="emit('select')"
></button>
<div v-if="!forwardImageSrc || !backwardImageSrc" class="skeleton-loader w-full h-full rounded-xl">
<div class="absolute inset-0 rounded-xl skeleton"></div>
<div v-if="!(imagesLoaded.forward && imagesLoaded.backward)" class="skeleton-loader w-full h-full rounded-xl">
<div class="skeleton absolute inset-0 rounded-xl aspect-[5/7]"></div>
</div>
<span v-else class="skin-button__image-parent pointer-events-none w-full h-full flex flex-col justify-end">
<span
v-show="imagesLoaded.forward && imagesLoaded.backward"
:class="['skin-button__image-parent pointer-events-none w-full h-full grid [transform-style:preserve-3d] transition-transform duration-500 group-hover:[transform:rotateY(180deg)] place-items-stretch', selected ? 'with-shadow' : '']"
>
<img
alt=""
:src="forwardImageSrc"
class="skin-button__image-facing rounded-xl object-contain w-full h-auto mx-auto mb-0"
class="skin-button__image-facing rounded-xl object-contain w-full h-full [backface-visibility:hidden] col-start-1 row-start-1"
height="504"
@load="onImageLoad('forward')"
/>
<img
alt=""
:src="backwardImageSrc"
class="skin-button__image-away rounded-xl object-contain w-full h-auto mx-auto mb-0"
class="skin-button__image-away rounded-xl object-contain w-full h-full [backface-visibility:hidden] [transform:rotateY(180deg)] col-start-1 row-start-1"
height="504"
@load="onImageLoad('backward')"
/>
</span>
<span
v-if="editable"
class="absolute pointer-events-none inset-0 flex items-end justify-center p-2 translate-y-4 scale-75 opacity-0 transition-all group-hover:opacity-100 group-hover:scale-100 group-hover:translate-y-0 group-hover:translate-x-0"
class="absolute pointer-events-none inset-0 flex items-end justify-start p-2 translate-y-4 scale-75 opacity-0 transition-all group-hover:opacity-100 group-hover:scale-100 group-hover:translate-y-0 group-hover:translate-x-0"
>
<ButtonStyled color="brand">
<button
@@ -91,71 +89,19 @@ function onImageLoad(type: 'forward' | 'backward') {
</template>
<style scoped lang="scss">
.skin-button__parent {
perspective: 1000px;
.skeleton-loader {
aspect-ratio: 5 / 7;
}
.skeleton-loader {
aspect-ratio: 1 / 1;
}
.skeleton {
background: linear-gradient(
90deg,
var(--color-bg, #f0f0f0) 25%,
var(--color-raised-bg, #e0e0e0) 50%,
var(--color-bg, #f0f0f0) 75%
);
background-size: 200% 100%;
animation: wave 1500ms infinite linear;
}
.skin-button__image-parent {
transform-style: preserve-3d;
display: grid;
/*
Set the images to be in the same position
*/
.skin-button__image-facing,
.skin-button__image-away {
grid-area: 1 / 1;
}
}
.skin-button__image-parent {
transition: transform 0.3s ease-in-out;
.skin-button__image-away {
opacity: 0;
transform: rotateY(180deg);
}
}
:not(&:hover) {
.skin-button__image-parent {
.skin-button__image-facing {
animation: appear-halfway 0.3s ease-in-out forwards;
}
.skin-button__image-away {
animation: vanish-halfway 0.3s ease-in-out forwards;
}
}
}
&:hover {
.skin-button__image-parent {
transform: rotateY(180deg);
.skin-button__image-facing {
animation: vanish-halfway 0.3s ease-in-out forwards;
}
.skin-button__image-away {
animation: appear-halfway 0.3s ease-in-out forwards;
}
}
}
.skeleton {
background: linear-gradient(
90deg,
var(--color-bg, #f0f0f0) 25%,
var(--color-raised-bg, #e0e0e0) 50%,
var(--color-bg, #f0f0f0) 75%
);
background-size: 200% 100%;
animation: wave 1500ms infinite linear;
}
@keyframes wave {
@@ -167,33 +113,25 @@ function onImageLoad(type: 'forward' | 'backward') {
}
}
@keyframes vanish-halfway {
0% {
opacity: 1;
}
50% {
opacity: 1;
}
51% {
opacity: 0;
}
100% {
opacity: 0;
}
.skin-btn-bg {
background: linear-gradient(180deg, #3a3d47 0%, #33363d 100%);
}
.skin-btn-bg.selected {
background: linear-gradient(180deg, #5a5d6b 0%, #424956 100%);
}
.with-shadow img {
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
}
@keyframes appear-halfway {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
51% {
opacity: 1;
}
100% {
opacity: 1;
}
.skin-button__image-parent img {
transition: filter 200ms ease-in-out;
}
.group:hover .skin-button__image-parent img {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.with-shadow img {
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
}
</style>

View File

@@ -9,7 +9,7 @@ const pressed = ref(false)
:class="{ 'scale-95': pressed }"
>
<button
class="absolute inset-0 bg-button-bg rounded-xl cursor-pointer p-0 border-none hover:brightness-125"
class="absolute inset-0 skin-like-text-bg rounded-xl cursor-pointer p-0 border-none"
@mousedown="pressed = true"
@mouseup="pressed = false"
@mouseleave="pressed = false"
@@ -24,3 +24,14 @@ const pressed = ref(false)
</div>
</div>
</template>
<style scoped lang="scss">
.skin-like-text-bg {
background: linear-gradient(180deg, #3a3d47 0%, #33363d 100%);
transition: filter 200ms ease-in-out, box-shadow 200ms ease-in-out;
}
.skin-like-text-bg:hover {
filter: brightness(var(--hover-brightness));
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
</style>