polish: skin buttons
This commit is contained in:
committed by
Alejandro González
parent
6a18f7cf04
commit
560b674cb7
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user