从 Axolotl 1.9.7 移植皮肤编辑器的盔甲/盔甲纹饰预览功能, 让玩家在皮肤选择页可试穿盔甲并调整纹饰。 新增: - 前端组件 ArmorTrimTab / SkinListSkeleton 及 skin-armor-preview helper - packages/ui 组件 ArmorPreviewControls / ArmorTrimPanel - packages/ui armor 预览 composable(armor-preview-assets/types、use-armor-preview) - packages/ui webgl 盔甲渲染(armor-geometry / armor-preview-object / armor-rendering) - packages/ui/src/assets/minecraft-armor 盔甲素材 改动: - SkinPreviewRenderer 增加 armorPreview/armorControls props、 armorConfig model 与 useArmorPreview 集成 - Skins.vue 增加盔甲 tab、状态逻辑与模板接入 - 补齐 skin/composables/utils 的导出 适配说明:ArmorPreviewControls 原版依赖 reka-ui 的 popover, Starlight 无该依赖且只用 external 模式,故改写为 external-only, 去除 reka-ui 与 overlays.css 依赖。
This commit is contained in:
1 parent
20d5d2baab
commit
ae2992d7f8
151 files changed
+1936
-32
No files matched your search
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { RightArrowIcon, ShieldIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import type { ArmorPreviewConfig } from '#ui/composables/skin-rendering'
|
||||
|
||||
const messages = defineMessages({
|
||||
armorPreview: { id: 'skin.preview.armor.open', defaultMessage: 'Armor and trims' },
|
||||
})
|
||||
|
||||
const { open = false, panelId } = defineProps<{
|
||||
/** Whether the panel this button toggles is currently open. */
|
||||
open?: boolean
|
||||
/** id of the panel this toggle controls, wired to `aria-controls`. */
|
||||
panelId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open' | 'close'): void
|
||||
}>()
|
||||
|
||||
// Kept for API compatibility with the upstream component; the model is owned by
|
||||
// the panel (ArmorTrimTab) rather than this toggle.
|
||||
defineModel<ArmorPreviewConfig>({ required: false })
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
/**
|
||||
* The shield flips about its centre axis into a right arrow while the panel is
|
||||
* open, so the same button reads as "back to the list" and closes it.
|
||||
*/
|
||||
const isFlipped = computed(() => open)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pointer-events-auto">
|
||||
<button
|
||||
class="flex h-10 min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] border-0 bg-surface-4 px-4 py-2.5 text-base font-semibold leading-5 shadow-md transition-[filter,transform] duration-200 hover:brightness-[--hover-brightness] focus-visible:brightness-[--hover-brightness] active:scale-95 [&>svg]:size-5 [&>svg]:shrink-0"
|
||||
:aria-label="formatMessage(messages.armorPreview)"
|
||||
:aria-expanded="open"
|
||||
:aria-controls="panelId"
|
||||
@click="open ? emit('close') : emit('open')"
|
||||
>
|
||||
<span class="relative size-5 shrink-0">
|
||||
<ShieldIcon
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 size-5 transition-[transform,opacity] duration-200 ease-out"
|
||||
:class="isFlipped ? 'scale-x-0 opacity-0' : 'scale-x-100'"
|
||||
/>
|
||||
<RightArrowIcon
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 size-5 transition-[transform,opacity] duration-200 ease-out"
|
||||
:class="isFlipped ? 'scale-x-100' : 'scale-x-0 opacity-0'"
|
||||
/>
|
||||
</span>
|
||||
<span>{{ formatMessage(messages.armorPreview) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,334 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
ARMOR_SLOTS,
|
||||
ARMOR_TRIM_MATERIALS,
|
||||
ARMOR_TRIM_PATTERNS,
|
||||
type ArmorMaterial,
|
||||
armorMaterialsForSlot,
|
||||
type ArmorPreviewConfig,
|
||||
type ArmorSlot,
|
||||
type ArmorTrimMaterial,
|
||||
type ArmorTrimPattern,
|
||||
} from '#ui/composables/skin-rendering'
|
||||
import {
|
||||
getArmorItemIcon,
|
||||
getArmorSlotIcon,
|
||||
getTrimMaterialIcon,
|
||||
getTrimPatternIcon,
|
||||
} from '#ui/composables/skin-rendering/armor-preview-assets'
|
||||
|
||||
const messages = defineMessages({
|
||||
armorPiece: { id: 'skin.preview.armor.piece', defaultMessage: 'Armor piece' },
|
||||
armorMaterial: { id: 'skin.preview.armor.material', defaultMessage: 'Armor material' },
|
||||
trimPattern: { id: 'skin.preview.armor.trim-pattern', defaultMessage: 'Trim pattern' },
|
||||
trimMaterial: { id: 'skin.preview.armor.trim-material', defaultMessage: 'Trim material' },
|
||||
unequipped: { id: 'skin.preview.armor.unequipped', defaultMessage: 'Unequipped' },
|
||||
removePiece: {
|
||||
id: 'skin.preview.armor.remove-piece',
|
||||
defaultMessage: 'Remove {slot}',
|
||||
},
|
||||
removeTrim: { id: 'skin.preview.armor.remove-trim', defaultMessage: 'No armor trim' },
|
||||
slotState: {
|
||||
id: 'skin.preview.armor.slot-state',
|
||||
defaultMessage: '{slot}: {state}',
|
||||
},
|
||||
equipMaterial: {
|
||||
id: 'skin.preview.armor.equip-material',
|
||||
defaultMessage: '{material} {slot}',
|
||||
},
|
||||
helmet: { id: 'skin.preview.armor.slot.helmet', defaultMessage: 'Helmet' },
|
||||
chestplate: { id: 'skin.preview.armor.slot.chestplate', defaultMessage: 'Chestplate' },
|
||||
leggings: { id: 'skin.preview.armor.slot.leggings', defaultMessage: 'Leggings' },
|
||||
boots: { id: 'skin.preview.armor.slot.boots', defaultMessage: 'Boots' },
|
||||
leather: { id: 'skin.preview.armor.material.leather', defaultMessage: 'Leather' },
|
||||
chainmail: { id: 'skin.preview.armor.material.chainmail', defaultMessage: 'Chainmail' },
|
||||
copper: { id: 'skin.preview.armor.material.copper', defaultMessage: 'Copper' },
|
||||
gold: { id: 'skin.preview.armor.material.gold', defaultMessage: 'Gold' },
|
||||
iron: { id: 'skin.preview.armor.material.iron', defaultMessage: 'Iron' },
|
||||
diamond: { id: 'skin.preview.armor.material.diamond', defaultMessage: 'Diamond' },
|
||||
netherite: { id: 'skin.preview.armor.material.netherite', defaultMessage: 'Netherite' },
|
||||
turtle: { id: 'skin.preview.armor.material.turtle', defaultMessage: 'Turtle Shell' },
|
||||
bolt: { id: 'skin.preview.armor.trim.bolt', defaultMessage: 'Bolt Armor Trim' },
|
||||
coast: { id: 'skin.preview.armor.trim.coast', defaultMessage: 'Coast Armor Trim' },
|
||||
dune: { id: 'skin.preview.armor.trim.dune', defaultMessage: 'Dune Armor Trim' },
|
||||
eye: { id: 'skin.preview.armor.trim.eye', defaultMessage: 'Eye Armor Trim' },
|
||||
flow: { id: 'skin.preview.armor.trim.flow', defaultMessage: 'Flow Armor Trim' },
|
||||
host: { id: 'skin.preview.armor.trim.host', defaultMessage: 'Host Armor Trim' },
|
||||
raiser: { id: 'skin.preview.armor.trim.raiser', defaultMessage: 'Raiser Armor Trim' },
|
||||
rib: { id: 'skin.preview.armor.trim.rib', defaultMessage: 'Rib Armor Trim' },
|
||||
sentry: { id: 'skin.preview.armor.trim.sentry', defaultMessage: 'Sentry Armor Trim' },
|
||||
shaper: { id: 'skin.preview.armor.trim.shaper', defaultMessage: 'Shaper Armor Trim' },
|
||||
silence: { id: 'skin.preview.armor.trim.silence', defaultMessage: 'Silence Armor Trim' },
|
||||
snout: { id: 'skin.preview.armor.trim.snout', defaultMessage: 'Snout Armor Trim' },
|
||||
spire: { id: 'skin.preview.armor.trim.spire', defaultMessage: 'Spire Armor Trim' },
|
||||
tide: { id: 'skin.preview.armor.trim.tide', defaultMessage: 'Tide Armor Trim' },
|
||||
vex: { id: 'skin.preview.armor.trim.vex', defaultMessage: 'Vex Armor Trim' },
|
||||
ward: { id: 'skin.preview.armor.trim.ward', defaultMessage: 'Ward Armor Trim' },
|
||||
wayfinder: { id: 'skin.preview.armor.trim.wayfinder', defaultMessage: 'Wayfinder Armor Trim' },
|
||||
wild: { id: 'skin.preview.armor.trim.wild', defaultMessage: 'Wild Armor Trim' },
|
||||
amethyst: { id: 'skin.preview.armor.trim-material.amethyst', defaultMessage: 'Amethyst' },
|
||||
emerald: { id: 'skin.preview.armor.trim-material.emerald', defaultMessage: 'Emerald' },
|
||||
lapis: { id: 'skin.preview.armor.trim-material.lapis', defaultMessage: 'Lapis Lazuli' },
|
||||
quartz: { id: 'skin.preview.armor.trim-material.quartz', defaultMessage: 'Quartz' },
|
||||
redstone: { id: 'skin.preview.armor.trim-material.redstone', defaultMessage: 'Redstone' },
|
||||
resin: { id: 'skin.preview.armor.trim-material.resin', defaultMessage: 'Resin Brick' },
|
||||
})
|
||||
|
||||
const model = defineModel<ArmorPreviewConfig>({ required: true })
|
||||
const { formatMessage } = useVIntl()
|
||||
const selectedSlot = ref<ArmorSlot>('helmet')
|
||||
const selectedPiece = computed(() => model.value[selectedSlot.value])
|
||||
|
||||
function slotLabel(slot: ArmorSlot): string {
|
||||
return formatMessage(messages[slot])
|
||||
}
|
||||
|
||||
function materialLabel(material: ArmorMaterial | ArmorTrimMaterial): string {
|
||||
return formatMessage(messages[material])
|
||||
}
|
||||
|
||||
function patternLabel(pattern: ArmorTrimPattern): string {
|
||||
return formatMessage(messages[pattern])
|
||||
}
|
||||
|
||||
function slotTooltip(slot: ArmorSlot): string {
|
||||
const material = model.value[slot].material
|
||||
return formatMessage(messages.slotState, {
|
||||
slot: slotLabel(slot),
|
||||
state: material ? materialLabel(material) : formatMessage(messages.unequipped),
|
||||
})
|
||||
}
|
||||
|
||||
function materialTooltip(material: ArmorMaterial): string {
|
||||
return formatMessage(messages.equipMaterial, {
|
||||
material: materialLabel(material),
|
||||
slot: slotLabel(selectedSlot.value),
|
||||
})
|
||||
}
|
||||
|
||||
function updateSelectedPiece(patch: Partial<ArmorPreviewConfig[ArmorSlot]>): void {
|
||||
model.value = {
|
||||
...model.value,
|
||||
[selectedSlot.value]: {
|
||||
...model.value[selectedSlot.value],
|
||||
...patch,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function setMaterial(material: ArmorMaterial | null): void {
|
||||
updateSelectedPiece({ material })
|
||||
}
|
||||
|
||||
function setTrimPattern(trimPattern: ArmorTrimPattern | null): void {
|
||||
updateSelectedPiece({ trimPattern })
|
||||
}
|
||||
|
||||
function setTrimMaterial(trimMaterial: ArmorTrimMaterial): void {
|
||||
updateSelectedPiece({ trimMaterial })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="armor-preview-panel">
|
||||
<section class="armor-preview-section">
|
||||
<h3>{{ formatMessage(messages.armorPiece) }}</h3>
|
||||
<div class="armor-preview-options armor-preview-options--four">
|
||||
<button
|
||||
v-for="slot in ARMOR_SLOTS"
|
||||
:key="slot"
|
||||
v-tooltip="slotTooltip(slot)"
|
||||
class="armor-preview-option"
|
||||
:class="{ 'armor-preview-option--selected': selectedSlot === slot }"
|
||||
:aria-label="slotTooltip(slot)"
|
||||
:aria-pressed="selectedSlot === slot"
|
||||
@click="selectedSlot = slot"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
:src="
|
||||
model[slot].material
|
||||
? getArmorItemIcon(model[slot].material, slot)
|
||||
: getArmorSlotIcon(slot)
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="armor-preview-section">
|
||||
<h3>{{ formatMessage(messages.armorMaterial) }}</h3>
|
||||
<div class="armor-preview-options">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.removePiece, { slot: slotLabel(selectedSlot) })"
|
||||
class="armor-preview-option"
|
||||
:class="{ 'armor-preview-option--selected': !selectedPiece.material }"
|
||||
:aria-label="formatMessage(messages.removePiece, { slot: slotLabel(selectedSlot) })"
|
||||
:aria-pressed="!selectedPiece.material"
|
||||
@click="setMaterial(null)"
|
||||
>
|
||||
<img alt="" :src="getArmorSlotIcon(selectedSlot)" />
|
||||
</button>
|
||||
<button
|
||||
v-for="material in armorMaterialsForSlot(selectedSlot)"
|
||||
:key="material"
|
||||
v-tooltip="materialTooltip(material)"
|
||||
class="armor-preview-option"
|
||||
:class="{ 'armor-preview-option--selected': selectedPiece.material === material }"
|
||||
:aria-label="materialTooltip(material)"
|
||||
:aria-pressed="selectedPiece.material === material"
|
||||
@click="setMaterial(material)"
|
||||
>
|
||||
<img alt="" :src="getArmorItemIcon(material, selectedSlot)" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="selectedPiece.material" class="armor-preview-section">
|
||||
<h3>{{ formatMessage(messages.trimPattern) }}</h3>
|
||||
<div class="armor-preview-options">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.removeTrim)"
|
||||
class="armor-preview-option"
|
||||
:class="{ 'armor-preview-option--selected': !selectedPiece.trimPattern }"
|
||||
:aria-label="formatMessage(messages.removeTrim)"
|
||||
:aria-pressed="!selectedPiece.trimPattern"
|
||||
@click="setTrimPattern(null)"
|
||||
>
|
||||
<img alt="" :src="getArmorSlotIcon(selectedSlot)" />
|
||||
</button>
|
||||
<button
|
||||
v-for="pattern in ARMOR_TRIM_PATTERNS"
|
||||
:key="pattern"
|
||||
v-tooltip="patternLabel(pattern)"
|
||||
class="armor-preview-option"
|
||||
:class="{
|
||||
'armor-preview-option--selected': selectedPiece.trimPattern === pattern,
|
||||
}"
|
||||
:aria-label="patternLabel(pattern)"
|
||||
:aria-pressed="selectedPiece.trimPattern === pattern"
|
||||
@click="setTrimPattern(pattern)"
|
||||
>
|
||||
<img alt="" :src="getTrimPatternIcon(pattern)" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="selectedPiece.material && selectedPiece.trimPattern"
|
||||
class="armor-preview-section"
|
||||
>
|
||||
<h3>{{ formatMessage(messages.trimMaterial) }}</h3>
|
||||
<div class="armor-preview-options">
|
||||
<button
|
||||
v-for="material in ARMOR_TRIM_MATERIALS"
|
||||
:key="material"
|
||||
v-tooltip="materialLabel(material)"
|
||||
class="armor-preview-option"
|
||||
:class="{
|
||||
'armor-preview-option--selected': selectedPiece.trimMaterial === material,
|
||||
}"
|
||||
:aria-label="materialLabel(material)"
|
||||
:aria-pressed="selectedPiece.trimMaterial === material"
|
||||
@click="setTrimMaterial(material)"
|
||||
>
|
||||
<img alt="" :src="getTrimMaterialIcon(material)" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.armor-preview-option {
|
||||
box-sizing: border-box;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0.375rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-default);
|
||||
background: var(--surface-3);
|
||||
border: 1px solid var(--surface-5);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
||||
transition:
|
||||
background-color 120ms ease,
|
||||
border-color 120ms ease,
|
||||
transform 120ms ease;
|
||||
}
|
||||
|
||||
.armor-preview-option:hover {
|
||||
background: var(--surface-4);
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
|
||||
.armor-preview-option:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.armor-preview-option:focus-visible {
|
||||
outline: 2px solid var(--color-brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.armor-preview-option--selected {
|
||||
background: var(--color-brand-highlight);
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
|
||||
.armor-preview-option > svg {
|
||||
flex-shrink: 0;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
}
|
||||
|
||||
.armor-preview-option > img {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
filter: drop-shadow(0 2px 2px rgb(0 0 0 / 35%));
|
||||
}
|
||||
|
||||
.armor-preview-panel {
|
||||
box-sizing: border-box;
|
||||
width: min(19rem, calc(100vw - 2rem));
|
||||
max-height: min(39rem, calc(100vh - 8rem));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.armor-preview-section + .armor-preview-section {
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px solid var(--surface-4);
|
||||
}
|
||||
|
||||
.armor-preview-section h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1rem;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.armor-preview-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 2.5rem);
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.armor-preview-options--four {
|
||||
grid-template-columns: repeat(4, 2.5rem);
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.armor-preview-panel {
|
||||
max-height: calc(100vh - 6rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,12 +17,17 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.subtitle"
|
||||
class="absolute left-0 right-0 z-10 flex items-center justify-center pointer-events-none"
|
||||
v-if="$slots.subtitle || showsArmorControls"
|
||||
class="absolute left-0 right-0 z-10 flex max-h-[calc(100%_-_1rem)] min-h-0 items-center justify-center overflow-y-auto pointer-events-none"
|
||||
:style="subtitlePositionStyle"
|
||||
>
|
||||
<div ref="subtitleElement" class="pointer-events-auto" @click="ignoreControlClick">
|
||||
<div
|
||||
ref="subtitleElement"
|
||||
class="flex max-w-[calc(100vw-2rem)] flex-wrap items-center justify-center gap-2 pointer-events-auto"
|
||||
@click="ignoreControlClick"
|
||||
>
|
||||
<slot name="subtitle" />
|
||||
<ArmorPreviewControls v-if="showsArmorControls" v-model="armorConfig" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -116,12 +121,15 @@ import {
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import type {
|
||||
ArmorPreviewConfig,
|
||||
SkinPreviewAnimationConfig,
|
||||
SkinPreviewFitPadding,
|
||||
SkinPreviewFraming,
|
||||
SkinPreviewTuple,
|
||||
} from '#ui/composables/skin-rendering'
|
||||
import {
|
||||
createDefaultArmorPreviewConfig,
|
||||
useArmorPreview,
|
||||
useSkinPreviewAnimation,
|
||||
useSkinPreviewControls,
|
||||
useSkinPreviewFit,
|
||||
@@ -130,6 +138,7 @@ import {
|
||||
} from '#ui/composables/skin-rendering'
|
||||
|
||||
import { useDynamicFontSize } from '../../composables'
|
||||
import ArmorPreviewControls from './ArmorPreviewControls.vue'
|
||||
import { createRadialSpotlightShader, syncDamageFlashShader } from './skin-preview-shader'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -157,6 +166,12 @@ const props = withDefaults(
|
||||
fov?: number
|
||||
initialRotation?: number
|
||||
animationConfig?: SkinPreviewAnimationConfig
|
||||
armorPreview?: boolean
|
||||
/**
|
||||
* Whether the preview draws the armour controls itself. A page that lays
|
||||
* them out alongside its own controls turns this off and hosts them.
|
||||
*/
|
||||
armorControls?: boolean
|
||||
}>(),
|
||||
{
|
||||
variant: 'CLASSIC',
|
||||
@@ -173,6 +188,8 @@ const props = withDefaults(
|
||||
randomAnimationInterval: 8000,
|
||||
transitionDuration: 0.2,
|
||||
}),
|
||||
armorPreview: false,
|
||||
armorControls: true,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -197,6 +214,7 @@ const isDarkPreviewTheme = computed(() => {
|
||||
const classList = document.documentElement.classList
|
||||
return classList.contains('dark-mode') || classList.contains('oled-mode')
|
||||
})
|
||||
const showsArmorControls = computed(() => Boolean(props.armorPreview) && props.armorControls)
|
||||
|
||||
function getSubtitleLayoutRoot(element: HTMLElement) {
|
||||
const elementChildren = Array.from(element.children).filter(
|
||||
@@ -286,6 +304,18 @@ const { isModelLoaded, isTextureLoaded, modelCenter, modelSize, scene } = useSki
|
||||
cleanupAnimationState,
|
||||
})
|
||||
|
||||
const armorConfig = defineModel<ArmorPreviewConfig>('armorConfig', {
|
||||
default: createDefaultArmorPreviewConfig,
|
||||
})
|
||||
|
||||
// Auto-fit is anchored to the load-time pose. Measuring again after an async
|
||||
// armor update could capture an arbitrary animation frame and jump on resize.
|
||||
useArmorPreview({
|
||||
scene,
|
||||
config: armorConfig,
|
||||
enabled: toRef(props, 'armorPreview'),
|
||||
})
|
||||
|
||||
function syncDamageFlashShaderMaterials() {
|
||||
syncDamageFlashShader(scene.value, damageFlashIntensity.value)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export { default as ArmorPreviewControls } from './ArmorPreviewControls.vue'
|
||||
export { default as ArmorTrimPanel } from './ArmorTrimPanel.vue'
|
||||
export { default as CapeButton } from './CapeButton.vue'
|
||||
export { default as CapeLikeTextButton } from './CapeLikeTextButton.vue'
|
||||
export { default as SkinButton } from './SkinButton.vue'
|
||||
|
||||
Reference in new issue
Block a user