add motion in chart

This commit is contained in:
2026-08-09 14:13:33 +03:30
parent e64d237182
commit bc33195e1c
2 changed files with 213 additions and 16 deletions
@@ -203,7 +203,9 @@
<div class="dlanding-chart">
<div
ref="heroChartPlotEl"
class="dlanding-chart__plot"
:class="{ 'is-drawn': heroChartDrawn }"
@pointermove="onHeroChartPointerMove"
@pointerleave="onHeroChartPointerLeave"
>
@@ -227,20 +229,23 @@
<path
class="dlanding-chart__path"
:d="landingChart.linePath"
pathLength="1"
fill="none"
stroke="#c98a5f"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle :cx="landingChart.lastX" :cy="landingChart.lastY" r="5" fill="#c98a5f" />
<circle
:cx="landingChart.lastX"
:cy="landingChart.lastY"
r="9"
fill="#c98a5f"
fill-opacity="0.2"
/>
<g class="dlanding-chart__endpoint">
<circle :cx="landingChart.lastX" :cy="landingChart.lastY" r="5" fill="#c98a5f" />
<circle
:cx="landingChart.lastX"
:cy="landingChart.lastY"
r="9"
fill="#c98a5f"
fill-opacity="0.2"
/>
</g>
</svg>
<div v-else class="dlanding-chart__empty">
{{ liveChartLoading ? 'در حال بارگذاری نمودار…' : 'نمودار در دسترس نیست' }}
@@ -559,7 +564,9 @@
</div>
</div>
<div
ref="liveChartPlotEl"
class="dlanding-live__chart"
:class="{ 'is-drawn': liveChartDrawn }"
@pointermove="onLiveChartPointerMove"
@pointerleave="onLiveChartPointerLeave"
>
@@ -576,25 +583,30 @@
</linearGradient>
</defs>
<path
class="dlanding-live__chart-area"
:d="liveSectionChart.areaPath"
fill="url(#liveChartFill)"
/>
<path
class="dlanding-live__chart-path"
:d="liveSectionChart.linePath"
pathLength="1"
fill="none"
stroke="#c98a5f"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle :cx="liveSectionChart.lastX" :cy="liveSectionChart.lastY" r="5" fill="#c98a5f" />
<circle
:cx="liveSectionChart.lastX"
:cy="liveSectionChart.lastY"
r="10"
fill="#c98a5f"
fill-opacity="0.2"
/>
<g class="dlanding-live__chart-endpoint">
<circle :cx="liveSectionChart.lastX" :cy="liveSectionChart.lastY" r="5" fill="#c98a5f" />
<circle
:cx="liveSectionChart.lastX"
:cy="liveSectionChart.lastY"
r="10"
fill="#c98a5f"
fill-opacity="0.2"
/>
</g>
</svg>
<div v-else class="dlanding-live__chart-empty">
{{
@@ -978,8 +990,12 @@ const canPromptInstall = ref(false)
/** اپ از قبل نصب شده یا در حالت standalone اجرا می‌شود */
const isAppInstalledOrStandalone = ref(false)
const statsMetricsEl = ref<HTMLElement | null>(null)
const heroChartPlotEl = ref<HTMLElement | null>(null)
const liveChartPlotEl = ref<HTMLElement | null>(null)
const metricAnimatedValues = ref<number[]>([0, 0, 0, 0])
const metricsCountStarted = ref(false)
const heroChartDrawn = ref(false)
const liveChartDrawn = ref(false)
const projectName = computed(() => config.config?.name_fa ?? 'بورس‌مس')
const patternMaskStyle = computed(() => ({
WebkitMaskImage: `url(${heroPattern})`,
@@ -1465,6 +1481,8 @@ onMounted(() => {
void nextTick(() => {
setupMetricsCountUp()
setupHeroChartDraw()
setupLiveChartDraw()
})
})
@@ -1475,6 +1493,10 @@ onBeforeUnmount(() => {
syncBodyScrollLock(false)
metricsObserver?.disconnect()
metricsObserver = undefined
heroChartObserver?.disconnect()
heroChartObserver = undefined
liveChartObserver?.disconnect()
liveChartObserver = undefined
if (metricsRaf) cancelAnimationFrame(metricsRaf)
if (pricePollTimer) {
clearInterval(pricePollTimer)
@@ -1513,8 +1535,87 @@ const displayMetrics = computed(() =>
)
let metricsObserver: IntersectionObserver | undefined
let heroChartObserver: IntersectionObserver | undefined
let liveChartObserver: IntersectionObserver | undefined
let metricsRaf = 0
function prefersReducedMotion() {
return (
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
)
}
function setupChartDrawOnce(
el: HTMLElement | null,
drawn: { value: boolean },
getObserver: () => IntersectionObserver | undefined,
setObserver: (observer: IntersectionObserver | undefined) => void,
) {
if (drawn.value) return
if (!el) return
if (prefersReducedMotion() || typeof IntersectionObserver === 'undefined') {
drawn.value = true
return
}
getObserver()?.disconnect()
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
drawn.value = true
observer.disconnect()
setObserver(undefined)
}
},
{ threshold: 0.35 },
)
setObserver(observer)
observer.observe(el)
}
function setupHeroChartDraw() {
setupChartDrawOnce(
heroChartPlotEl.value,
heroChartDrawn,
() => heroChartObserver,
(observer) => {
heroChartObserver = observer
},
)
}
function setupLiveChartDraw() {
setupChartDrawOnce(
liveChartPlotEl.value,
liveChartDrawn,
() => liveChartObserver,
(observer) => {
liveChartObserver = observer
},
)
}
watch(
() => landingChart.value.hasData,
async (hasData) => {
if (!hasData || heroChartDrawn.value) return
await nextTick()
setupHeroChartDraw()
},
)
watch(
() => liveSectionChart.value.hasData,
async (hasData) => {
if (!hasData || liveChartDrawn.value) return
await nextTick()
setupLiveChartDraw()
},
)
function isStandaloneDisplay() {
return (
window.matchMedia('(display-mode: standalone)').matches ||
@@ -597,6 +597,33 @@ body.dlanding-menu-open {
height: clamp(80px, 10vw, 96px);
}
&__path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
&__area {
opacity: 0;
}
&__endpoint {
opacity: 0;
}
&__plot.is-drawn {
.dlanding-chart__path {
animation: dlanding-chart-draw 1.45s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.dlanding-chart__area {
animation: dlanding-chart-fade 0.55s ease-out 0.75s forwards;
}
.dlanding-chart__endpoint {
animation: dlanding-chart-fade 0.35s ease-out 1.25s forwards;
}
}
&__empty {
display: flex;
align-items: center;
@@ -677,6 +704,48 @@ body.dlanding-menu-open {
}
}
@keyframes dlanding-chart-draw {
to {
stroke-dashoffset: 0;
}
}
@keyframes dlanding-chart-fade {
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.dlanding-chart {
&__path {
stroke-dasharray: none;
stroke-dashoffset: 0;
animation: none !important;
}
&__area,
&__endpoint {
opacity: 1;
animation: none !important;
}
}
.dlanding-live {
&__chart-path {
stroke-dasharray: none;
stroke-dashoffset: 0;
animation: none !important;
}
&__chart-area,
&__chart-endpoint {
opacity: 1;
animation: none !important;
}
}
}
.dlanding-order-card {
width: 556px;
max-width: 100%;
@@ -1517,6 +1586,33 @@ body.dlanding-menu-open {
height: 100%;
}
&__chart-path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
&__chart-area {
opacity: 0;
}
&__chart-endpoint {
opacity: 0;
}
&__chart.is-drawn {
.dlanding-live__chart-path {
animation: dlanding-chart-draw 1.55s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.dlanding-live__chart-area {
animation: dlanding-chart-fade 0.55s ease-out 0.85s forwards;
}
.dlanding-live__chart-endpoint {
animation: dlanding-chart-fade 0.35s ease-out 1.35s forwards;
}
}
&__chart-empty {
display: flex;
align-items: center;