Skip to content
Snippets Groups Projects
VizTitle.vue 7.07 KiB
<template>
  <header id="grid-container">
    <div id="image-container">
      <picture>
        <!-- Serve WebP images  -->
        <source 
          type="image/webp"
          srcset="
            @/assets/images/responsive_images/hero_no-faces_5-320.webp 320w,
            @/assets/images/responsive_images/hero_no-faces_5-640.webp 640w,
            @/assets/images/responsive_images/hero_no-faces_5-1280.webp 1280w,
            @/assets/images/responsive_images/hero_no-faces_5-1920.webp 1920w"
          sizes="(max-width: 600px) 320px, 
                (max-width: 1200px) 640px, 
                (min-width: 1201px) 1280px, 
                1920px"
        />

        <!-- Fallback to JPG images for browsers that do not support WebP -->
        <img
          ref="heroImage"
          id="title-image"
          :class="{ mobile: mobileView }"
          srcset="
            @/assets/images/responsive_images/hero_no-faces_5-320.jpg 320w,
            @/assets/images/responsive_images/hero_no-faces_5-640.jpg 640w,
            @/assets/images/responsive_images/hero_no-faces_5-1280.jpg 1280w,
            @/assets/images/responsive_images/hero_no-faces_5-1920.jpg 1920w"
          sizes="(max-width: 600px) 320px, 
                (max-width: 1200px) 640px, 
                (min-width: 1201px) 1280px, 
                1920px"
          src="@/assets/images/responsive_images/hero_no-faces_5-1280.jpg"
          alt="Social vulnerability and water insecurity"
          @load="onImageLoad"
        />
      </picture>
      <img
        ref="bwHeroImage"
        id="bw-title-image"
        rel="preload"
        :class="{ mobile: mobileView }"
        srcset="
          @/assets/images/responsive_images/hero_no-faces_5-320.jpg 320w,
          @/assets/images/responsive_images/hero_no-faces_5-640.jpg 640w,
          @/assets/images/responsive_images/hero_no-faces_5-1280.jpg 1280w,
          @/assets/images/responsive_images/hero_no-faces_5-1920.jpg 1920w"
        sizes="(max-width: 600px) 320px, 
              (max-width: 1200px) 640px, 
              (min-width: 1201px) 1280px, 
              1920px"
        src="@/assets/images/responsive_images/hero_no-faces_5-1280.jpg"
        alt="Social vulnerability and water insecurity (grayscale)"
      />
      <!-- Using SVG masks with a circle shape to reveal color image where circles are on top of bw -->
      <svg ref="overlay" id="overlay" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <mask id="circleMask">
            <rect width="100%" height="100%" fill="white" />
            <!-- Circles will be added here via D3.js -->
          </mask>
        </defs>
        <rect width="100%" height="100%" fill="transparent" mask="url(#circleMask)" />
      </svg>
    </div>
    <div id="text-container">
      <h1>{{ t('text.pageTitle') }}</h1>
      <h3>{{ t('text.pageSubtitle') }}</h3>
    </div>
  </header>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue';
import { isMobile } from 'mobile-device-detect';
import * as d3 from 'd3';
import { useI18n } from 'vue-i18n';

const { t } = useI18n();
const mobileView = ref(isMobile);
const heroImage = ref(null);
const bwHeroImage = ref(null);
const overlay = ref(null);

const updateSvgDimensions = () => {
  if (heroImage.value && bwHeroImage.value && overlay.value) {
    const { width, height } = heroImage.value.getBoundingClientRect();
    overlay.value.setAttribute('width', width);
    overlay.value.setAttribute('height', height);
    bwHeroImage.value.style.width = `${width}px`;
    bwHeroImage.value.style.height = `${height}px`;
    bwHeroImage.value.style.top = '0';
    bwHeroImage.value.style.left = '0';
  }
};

const addRandomCircles = () => {
  nextTick(() => {
    const svg = d3.select(overlay.value);
    const mask = svg.select('#circleMask');
    const svgWidth = overlay.value.clientWidth;
    const svgHeight = overlay.value.clientHeight;

    if (svgWidth === 0 || svgHeight === 0) {
      console.error('SVG dimensions are not set correctly.');
      return;
    }

    mask.selectAll('circle').remove(); // Clear existing circles

    const numberOfCircles = 40; // Number of circles to add
    const circlesData = Array.from({ length: numberOfCircles }).map(() => ({
      cx: Math.random() * svgWidth,
      cy: Math.random() * svgHeight,
      r: Math.random() * (svgWidth / 20)
    }));

    const circles = mask.selectAll('circle')
      .data(circlesData)
      .enter()
      .append('circle')
      .attr('cx', d => d.cx)
      .attr('cy', d => d.cy)
      .attr('r', d => d.r)
      .attr('fill', 'black');

    function animateCircles() {
      circles.transition()
        .duration(1000 + Math.random() * 1000) 
        .ease(d3.easeSin)
        .attr('cy', d => d.cy + (Math.random() - 0.5) * 100) // Larger random range for more movement
        .attr('cx', d => d.cx + (Math.random() - 0.5) * 350)
        .on('end', function() {
          d3.select(this)
            .transition()
            .duration(2000 + Math.random() * 3000) 
            .ease(d3.easeCubic)
            .attr('cy', d => d.cy + (Math.random() - 0.5) * 200)
            .attr('cx', d => d.cx + (Math.random() - 0.5) * 300)
            .on('end', animateCircles); // Recursive call for continuous animation
        });
    }

    animateCircles();
  });
};

const onImageLoad = () => {
  nextTick(() => {
    updateSvgDimensions();
    addRandomCircles();
  });
};

onMounted(() => {
  const debounceResize = debounce(() => {
    updateSvgDimensions();
    addRandomCircles();
  }, 200);

  window.addEventListener('resize', debounceResize);

  if (heroImage.value.complete) {
    onImageLoad();
  } else {
    heroImage.value.addEventListener('load', onImageLoad);
  }
});

function debounce(func, wait) {
  let timeout;
  return function (...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}
</script>


<style lang="scss" scoped>
#grid-container {
  display: grid;
  grid-template-rows: auto auto;
  width: 100%;
  margin: auto;
}

#image-container {
  position: relative;
  width: 100%;
}

#title-image,
#bw-title-image {
  width: 100%;
}

#title-image {
  display: block;
}

#bw-title-image {
  filter: grayscale(100%);
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none; /* Ensure the grayscale image doesn't interfere with other interactions */
  mask: url(#circleMask); /* Apply the SVG mask */
}

#overlay {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none; /* Ensure the SVG overlay doesn't interfere with other interactions */
}

#text-container {
  width: 100%;
  background: var(--color-background-header-footer);
  color: var(--color-text);
  padding: 20px;
  box-sizing: border-box;
}

#image-container {
  color: var(--color-text);
  background: var(--color-background-header-footer);
}

h1 {
  margin: -20px 20px;
  color: white;
  text-align: left; /* Optional: Align text to the left */
}

h3 {
  margin: 20px 20px 0px 20px;
  color: white;
  text-align: left; /* Optional: Align text to the left */
}

svg {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
</style>