200 lines
7.0 KiB
Vue
200 lines
7.0 KiB
Vue
<template>
|
|
<view
|
|
class="sugar-tree-graphic"
|
|
:class="[`lv-${clampedLevel}`, `tier-${visualTier}`, `size-${size}`, { watering: watering, 'is-max': clampedLevel >= MAX_LEVEL }]"
|
|
>
|
|
<view class="stg-glow" />
|
|
<view v-if="clampedLevel >= 7" class="stg-aura" />
|
|
<image v-if="!treeUseFallback" class="stg-image" :src="treeImageSrc" mode="aspectFit" @error="treeUseFallback = true" />
|
|
<text v-else class="stg-emoji">{{ treeEmoji }}</text>
|
|
<view v-if="clampedLevel >= 7" class="stg-sparkle stg-sparkle-a" />
|
|
<view v-if="clampedLevel >= 8" class="stg-sparkle stg-sparkle-b" />
|
|
<view v-if="clampedLevel >= MAX_LEVEL" class="stg-sparkle stg-sparkle-c" />
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import { svgToDataUrl } from '../utils/svgDataUrl.js'
|
|
import { TREE_MAX_LEVEL, TREE_LEVELS } from '../utils/treeLevels.js'
|
|
|
|
const MAX_LEVEL = TREE_MAX_LEVEL
|
|
|
|
// #ifdef MP-WEIXIN
|
|
const treeUseFallback = ref(true)
|
|
// #endif
|
|
// #ifndef MP-WEIXIN
|
|
const treeUseFallback = ref(false)
|
|
// #endif
|
|
|
|
const props = defineProps({
|
|
level: { type: Number, default: 0 },
|
|
size: { type: String, default: 'md' },
|
|
watering: { type: Boolean, default: false }
|
|
})
|
|
|
|
const clampedLevel = computed(() => Math.min(MAX_LEVEL, Math.max(0, Number(props.level) || 0)))
|
|
const visualTier = computed(() => {
|
|
const lv = clampedLevel.value
|
|
if (lv <= 0) return 0
|
|
if (lv <= 2) return 1
|
|
if (lv <= 4) return 2
|
|
if (lv <= 6) return 3
|
|
if (lv <= 8) return 4
|
|
return 5
|
|
})
|
|
const treeEmoji = computed(() => (TREE_LEVELS[clampedLevel.value] || TREE_LEVELS[0]).emoji)
|
|
|
|
function buildTreeSvg(level) {
|
|
const pot = `
|
|
<ellipse cx="24" cy="50" rx="15" ry="3" fill="#204e2b" opacity="0.12"/>
|
|
<path d="M11 46h26c1.2 0 2 1 2 2.2v3.8c0 1-.8 1.8-1.8 1.8H10.8c-1 0-1.8-.8-1.8-1.8v-3.8c0-1.2.8-2.2 2-2.2z" fill="#E7E5E4"/>
|
|
<path d="M12.5 46h23c.8 0 1.5.7 1.5 1.5v2.2c0 .6-.5 1.1-1.1 1.1H12.1c-.6 0-1.1-.5-1.1-1.1v-2.2c0-.8.7-1.5 1.5-1.5z" fill="#D6D3D1"/>
|
|
<ellipse cx="24" cy="46.5" rx="9" ry="1.6" fill="#A8A29E" opacity="0.35"/>
|
|
`
|
|
const soil = `<ellipse cx="24" cy="44.5" rx="8" ry="2.2" fill="#386641" opacity="0.18"/>`
|
|
|
|
if (level <= 0) {
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">
|
|
${pot}${soil}
|
|
<circle cx="24" cy="41.5" r="2.2" fill="#a8a29e" opacity="0.7"/>
|
|
<path d="M24 41.5v3.5" stroke="#78716c" stroke-width="1" stroke-linecap="round"/>
|
|
</svg>`
|
|
}
|
|
|
|
const trunkH = level >= 6 ? 16 : level >= 3 ? 14 : 10
|
|
const trunkY = 46 - trunkH
|
|
const trunk = `<rect x="22.2" y="${trunkY}" width="3.6" height="${trunkH}" rx="1.8" fill="#78716C"/>
|
|
<rect x="22.6" y="${trunkY + 1}" width="2.8" height="${trunkH - 1}" rx="1.4" fill="#A8A29E" opacity="0.35"/>`
|
|
|
|
const leaf = (cx, cy, r, fill, opacity = 1) =>
|
|
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="${fill}" opacity="${opacity}"/>
|
|
<circle cx="${cx - r * 0.25}" cy="${cy - r * 0.2}" r="${r * 0.35}" fill="#eef6ef" opacity="0.55"/>`
|
|
|
|
const bloom =
|
|
level >= 7
|
|
? leaf(17, 18, 3, '#fda4af', 0.95) +
|
|
leaf(31, 17, 2.8, '#f9a8d4', 0.9) +
|
|
leaf(24, 13, 3.2, '#fb7185', 0.95) +
|
|
`<circle cx="24" cy="12" r="1.3" fill="#fef3c7"/>`
|
|
: ''
|
|
const crown =
|
|
level >= 9
|
|
? leaf(12, 22, 6, '#34d399', 0.9) +
|
|
leaf(36, 22, 6, '#34d399', 0.9) +
|
|
leaf(24, 10, 7, '#386641', 0.95) +
|
|
bloom
|
|
: bloom
|
|
|
|
let canopy = ''
|
|
if (level === 1) {
|
|
canopy = leaf(24, 38, 4, '#afe2b3') + `<path d="M24 38v-5" stroke="#386641" stroke-width="1.2" stroke-linecap="round"/>`
|
|
} else if (level === 2) {
|
|
canopy = leaf(24, 32, 5.5, '#34d399') + leaf(20, 34, 3.5, '#6ee7b7', 0.9)
|
|
} else if (level <= 4) {
|
|
canopy =
|
|
leaf(24, 28, 7, '#34d399') +
|
|
leaf(17, 30, 5, '#6ee7b7', 0.9) +
|
|
leaf(31, 30, 5, '#6ee7b7', 0.9)
|
|
} else if (level <= 6) {
|
|
canopy =
|
|
leaf(24, 24, 9, '#22c55e') +
|
|
leaf(14, 26, 7, '#4ade80', 0.92) +
|
|
leaf(34, 26, 7, '#4ade80', 0.92) +
|
|
leaf(24, 16, 6, '#386641', 0.88)
|
|
} else {
|
|
canopy =
|
|
leaf(24, 20, 10, '#204e2b') +
|
|
leaf(13, 24, 8, '#34d399', 0.95) +
|
|
leaf(35, 24, 8, '#34d399', 0.95) +
|
|
crown
|
|
}
|
|
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">${pot}${soil}${trunk}${canopy}</svg>`
|
|
}
|
|
|
|
const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value)))
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.sugar-tree-graphic {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
}
|
|
.size-sm { width: 64rpx; height: 72rpx; }
|
|
.size-md { width: 80rpx; height: 88rpx; }
|
|
.size-lg { width: 112rpx; height: 124rpx; }
|
|
|
|
.stg-glow {
|
|
position: absolute;
|
|
inset: 6%;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, rgba(32, 78, 43, 0.22) 0%, transparent 68%);
|
|
pointer-events: none;
|
|
}
|
|
.lv-0 .stg-glow { background: radial-gradient(circle, rgba(148, 163, 184, 0.25) 0%, transparent 70%); }
|
|
.tier-4 .stg-glow,
|
|
.tier-5 .stg-glow,
|
|
.is-max .stg-glow {
|
|
background: radial-gradient(circle, rgba(251, 191, 36, 0.3) 0%, rgba(32, 78, 43, 0.15) 55%, transparent 72%);
|
|
}
|
|
.stg-aura {
|
|
position: absolute;
|
|
inset: -8%;
|
|
border-radius: 50%;
|
|
border: 2rpx solid rgba(253, 224, 71, 0.35);
|
|
animation: stg-aura-pulse 2.4s ease-in-out infinite;
|
|
pointer-events: none;
|
|
}
|
|
@keyframes stg-aura-pulse {
|
|
0%, 100% { transform: scale(0.92); opacity: 0.5; }
|
|
50% { transform: scale(1.05); opacity: 1; }
|
|
}
|
|
.stg-image {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
transition: transform 0.35s ease;
|
|
}
|
|
.sugar-tree-graphic.watering .stg-image,
|
|
.sugar-tree-graphic.watering .stg-emoji {
|
|
animation: stg-water-bounce 0.65s ease;
|
|
}
|
|
.stg-emoji {
|
|
position: relative;
|
|
z-index: 1;
|
|
line-height: 1;
|
|
font-size: 72rpx;
|
|
}
|
|
.size-sm .stg-emoji { font-size: 48rpx; }
|
|
.size-md .stg-emoji { font-size: 60rpx; }
|
|
.size-lg .stg-emoji { font-size: 88rpx; }
|
|
@keyframes stg-water-bounce {
|
|
0%, 100% { transform: scale(1); }
|
|
35% { transform: scale(1.1) translateY(-6rpx); }
|
|
60% { transform: scale(0.96) translateY(2rpx); }
|
|
}
|
|
.stg-sparkle {
|
|
position: absolute;
|
|
z-index: 2;
|
|
width: 8rpx;
|
|
height: 8rpx;
|
|
border-radius: 50%;
|
|
background: #fde68a;
|
|
box-shadow: 0 0 6rpx rgba(253, 224, 71, 0.8);
|
|
pointer-events: none;
|
|
animation: stg-sparkle-twinkle 1.8s ease-in-out infinite;
|
|
}
|
|
.stg-sparkle-a { top: 4%; right: 16%; }
|
|
.stg-sparkle-b { top: 12%; left: 10%; width: 6rpx; height: 6rpx; animation-delay: 0.4s; }
|
|
.stg-sparkle-c { top: 22%; right: 28%; width: 10rpx; height: 10rpx; animation-delay: 0.8s; }
|
|
@keyframes stg-sparkle-twinkle {
|
|
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
|
50% { opacity: 1; transform: scale(1.2); }
|
|
}
|
|
</style>
|