diff --git a/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue b/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue index 7f727e1..f798259 100644 --- a/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue +++ b/src/views/base/aboutUs/desktop/DesktopAboutUsView.vue @@ -203,7 +203,9 @@
@@ -227,20 +229,23 @@ - - + + + +
{{ liveChartLoading ? 'در حال بارگذاری نمودار…' : 'نمودار در دسترس نیست' }} @@ -559,7 +564,9 @@
@@ -576,25 +583,30 @@ - - + + + +
{{ @@ -978,8 +990,12 @@ const canPromptInstall = ref(false) /** اپ از قبل نصب شده یا در حالت standalone اجرا می‌شود */ const isAppInstalledOrStandalone = ref(false) const statsMetricsEl = ref(null) +const heroChartPlotEl = ref(null) +const liveChartPlotEl = ref(null) const metricAnimatedValues = ref([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 || diff --git a/src/views/base/aboutUs/desktop/styles/desktop-about-us.less b/src/views/base/aboutUs/desktop/styles/desktop-about-us.less index 51ad128..0756edb 100644 --- a/src/views/base/aboutUs/desktop/styles/desktop-about-us.less +++ b/src/views/base/aboutUs/desktop/styles/desktop-about-us.less @@ -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;