fix: nametag cleanup + scroll fix
This commit is contained in:
committed by
Alejandro González
parent
006611bd23
commit
d37e8fd155
@@ -455,6 +455,7 @@ $skin-card-gap: 4px;
|
||||
|
||||
.preview-panel {
|
||||
top: 1.5rem;
|
||||
position: sticky;
|
||||
align-self: start;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/how-ago": "^3.0.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"apexcharts": "^3.44.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative w-full h-full cursor-grab">
|
||||
<div ref="skinPreviewContainer" class="relative w-full h-full cursor-grab">
|
||||
<div
|
||||
class="absolute bottom-[18%] left-0 right-0 flex flex-col justify-center items-center mb-2 pointer-events-none z-10 gap-2"
|
||||
>
|
||||
@@ -14,9 +14,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="nametag"
|
||||
ref="nametagElement"
|
||||
class="absolute top-[13%] left-1/2 transform -translate-x-1/2 px-3 py-1 rounded-md pointer-events-none z-10 font-minecraft text-inverted nametag-bg"
|
||||
:style="{ fontSize: dynamicNametagFontSize }"
|
||||
class="absolute top-[20%] left-1/2 transform -translate-x-1/2 px-3 py-1 rounded-md pointer-events-none z-10 font-minecraft text-inverted nametag-bg transition-all duration-200"
|
||||
:style="{ fontSize: nametagFontSize }"
|
||||
>
|
||||
{{ nametag }}
|
||||
</div>
|
||||
@@ -105,8 +104,6 @@ import {
|
||||
onUnmounted,
|
||||
toRefs,
|
||||
useTemplateRef,
|
||||
onMounted,
|
||||
nextTick,
|
||||
} from 'vue'
|
||||
import {
|
||||
applyTexture,
|
||||
@@ -116,6 +113,7 @@ import {
|
||||
createTransparentTexture,
|
||||
loadTexture as loadSkinTexture,
|
||||
} from '@modrinth/utils'
|
||||
import { useDynamicFontSize } from '../../composables'
|
||||
|
||||
interface AnimationConfig {
|
||||
baseAnimation: string
|
||||
@@ -157,6 +155,19 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const skinPreviewContainer = useTemplateRef<HTMLElement>('skinPreviewContainer')
|
||||
const nametagText = computed(() => props.nametag)
|
||||
|
||||
const { fontSize: nametagFontSize } = useDynamicFontSize({
|
||||
containerElement: skinPreviewContainer,
|
||||
text: nametagText,
|
||||
baseFontSize: 2,
|
||||
minFontSize: 1.25,
|
||||
maxFontSize: 4,
|
||||
padding: 24,
|
||||
fontFamily: 'inherit',
|
||||
})
|
||||
|
||||
const selectedModelSrc = computed(() =>
|
||||
props.variant === 'SLIM' ? props.slimModelSrc : props.wideModelSrc,
|
||||
)
|
||||
@@ -180,30 +191,6 @@ const clock = new THREE.Clock()
|
||||
const currentAnimation = ref<string>('')
|
||||
const randomAnimationTimer = ref<number | null>(null)
|
||||
|
||||
const nametagElement = useTemplateRef<HTMLElement>('nametagElement')
|
||||
const maxContainerWidth = ref(300)
|
||||
|
||||
const dynamicNametagFontSize = computed(() => {
|
||||
if (!props.nametag) return '2rem'
|
||||
const textLength = props.nametag.length
|
||||
const baseSize = 32
|
||||
const minSize = 12
|
||||
const maxSize = 32
|
||||
const availableWidth = maxContainerWidth.value - 64
|
||||
const estimatedCharWidth = baseSize * 0.6
|
||||
const estimatedTextWidth = textLength * estimatedCharWidth
|
||||
const scaleFactor = availableWidth / estimatedTextWidth
|
||||
const calculatedSize = Math.max(minSize, Math.min(maxSize, baseSize * scaleFactor))
|
||||
|
||||
return `${calculatedSize}px`
|
||||
})
|
||||
|
||||
const updateContainerWidth = () => {
|
||||
if (nametagElement.value?.parentElement) {
|
||||
maxContainerWidth.value = nametagElement.value.parentElement.clientWidth
|
||||
}
|
||||
}
|
||||
|
||||
const { baseAnimation, randomAnimations } = toRefs(props.animationConfig)
|
||||
|
||||
function initializeAnimations(loadedScene: THREE.Object3D, clips: THREE.AnimationClip[]) {
|
||||
@@ -250,7 +237,6 @@ function playAnimation(name: string) {
|
||||
}
|
||||
})
|
||||
|
||||
// Reset and play the new animation
|
||||
action.reset()
|
||||
action.setLoop(THREE.LoopRepeat, Infinity)
|
||||
action.fadeIn(transitionDuration)
|
||||
@@ -526,22 +512,6 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.nametag,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
updateContainerWidth()
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
updateContainerWidth()
|
||||
window.addEventListener('resize', updateContainerWidth)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
isTextureLoaded.value = false
|
||||
@@ -571,8 +541,6 @@ onUnmounted(() => {
|
||||
mixer.value.stopAllAction()
|
||||
mixer.value = null
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', updateContainerWidth)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
117
packages/ui/src/composables/dynamic-font-size.ts
Normal file
117
packages/ui/src/composables/dynamic-font-size.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { computed, onMounted, onUnmounted, type Ref } from 'vue'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
export interface DynamicFontSizeOptions {
|
||||
containerElement: Ref<HTMLElement | null>
|
||||
text: Ref<string | undefined>
|
||||
baseFontSize?: number
|
||||
minFontSize?: number
|
||||
maxFontSize?: number
|
||||
availableWidthRatio?: number
|
||||
maxContainerWidth?: number
|
||||
padding?: number
|
||||
fontFamily?: string
|
||||
fontWeight?: string | number
|
||||
}
|
||||
|
||||
export function useDynamicFontSize(options: DynamicFontSizeOptions) {
|
||||
const {
|
||||
containerElement,
|
||||
text,
|
||||
baseFontSize = 1.25,
|
||||
minFontSize = 0.75,
|
||||
maxFontSize = 2,
|
||||
availableWidthRatio = 0.9,
|
||||
maxContainerWidth = 400,
|
||||
padding = 24,
|
||||
fontFamily = 'inherit',
|
||||
fontWeight = 'inherit',
|
||||
} = options
|
||||
|
||||
const { width: containerWidth } = useElementSize(containerElement)
|
||||
let measurementElement: HTMLElement | null = null
|
||||
|
||||
const createMeasurementElement = () => {
|
||||
if (measurementElement) return measurementElement
|
||||
|
||||
measurementElement = document.createElement('div')
|
||||
measurementElement.style.cssText = `
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
font-family: ${fontFamily};
|
||||
font-weight: ${fontWeight};
|
||||
`
|
||||
measurementElement.setAttribute('aria-hidden', 'true')
|
||||
document.body.appendChild(measurementElement)
|
||||
|
||||
return measurementElement
|
||||
}
|
||||
|
||||
const cleanupMeasurementElement = () => {
|
||||
if (measurementElement?.parentNode) {
|
||||
measurementElement.parentNode.removeChild(measurementElement)
|
||||
measurementElement = null
|
||||
}
|
||||
}
|
||||
|
||||
const measureTextWidth = (textContent: string, fontSize: number): number => {
|
||||
if (!textContent) return 0
|
||||
|
||||
const element = createMeasurementElement()
|
||||
element.style.fontSize = `${fontSize}rem`
|
||||
element.textContent = textContent
|
||||
|
||||
return element.getBoundingClientRect().width
|
||||
}
|
||||
|
||||
const findOptimalFontSize = (textContent: string, availableWidth: number): number => {
|
||||
let low = minFontSize
|
||||
let high = maxFontSize
|
||||
let bestSize = minFontSize
|
||||
|
||||
const maxWidth = measureTextWidth(textContent, maxFontSize)
|
||||
if (maxWidth <= availableWidth) return maxFontSize
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const mid = (low + high) / 2
|
||||
const width = measureTextWidth(textContent, mid)
|
||||
|
||||
if (width <= availableWidth) {
|
||||
bestSize = mid
|
||||
low = mid
|
||||
} else {
|
||||
high = mid
|
||||
}
|
||||
|
||||
if (high - low < 0.01) break
|
||||
}
|
||||
|
||||
return Math.max(bestSize, minFontSize)
|
||||
}
|
||||
|
||||
const fontSize = computed(() => {
|
||||
if (!text.value || !containerWidth.value) return `${baseFontSize}rem`
|
||||
|
||||
const availableWidth =
|
||||
Math.min(containerWidth.value * availableWidthRatio, maxContainerWidth) - padding
|
||||
|
||||
const baseWidth = measureTextWidth(text.value, baseFontSize)
|
||||
if (baseWidth <= availableWidth) return `${baseFontSize}rem`
|
||||
|
||||
const optimalSize = findOptimalFontSize(text.value, availableWidth)
|
||||
return `${optimalSize}rem`
|
||||
})
|
||||
|
||||
onMounted(createMeasurementElement)
|
||||
onUnmounted(cleanupMeasurementElement)
|
||||
|
||||
return {
|
||||
fontSize,
|
||||
containerWidth,
|
||||
cleanup: cleanupMeasurementElement,
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './how-ago'
|
||||
export * from './dynamic-font-size'
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -444,6 +444,9 @@ importers:
|
||||
'@vintl/how-ago':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(@formatjs/intl@2.10.4(typescript@5.5.4))
|
||||
'@vueuse/core':
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0(vue@3.5.13(typescript@5.5.4))
|
||||
apexcharts:
|
||||
specifier: ^3.44.0
|
||||
version: 3.49.2
|
||||
|
||||
Reference in New Issue
Block a user