<div id="app"></div>

<!-- <div class="mdl-card__supporting-text meta meta--fill mdl-color-text--grey-600">
  <ul class="mdl-menu mdl-js-menu mdl-menu--bottom-right mdl-js-ripple-effect" for="menubtn">
    <li class="mdl-menu__item">About</li>
    <li class="mdl-menu__item">Message</li>
    <li class="mdl-menu__item">Favorite</li>
    <li class="mdl-menu__item">Search</li>
  </ul>
  <button id="menubtn" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon">
    <i class="material-icons" role="presentation">more_vert</i>
    <span class="visuallyhidden">show menu</span>
  </button>
</div> -->
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,700');

$spot-color: #417dc4;
$text-color: #545454;
$title-bg-color: #255690;

*, *::after, *::before {
    box-sizing: border-box;
}
html, body {
    height: 100%;
    min-height: 100%;
}
body {
    background: #434A54;
    margin: 0;  
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'Roboto', sans-serif;
    -webkit-font-smoothing: antialiased;
}
[v-cloak] { display:none; }

.icon {
  display: inline-block;
  width: 1.4em;
  height: 1.4em;
  stroke-width: 0;
  stroke: currentColor;
  fill: currentColor;
} 

.app {
    width: 95vw;

    @media (min-width: 768px) {
      max-width: 1600px;
    }
}

.image-hotspot {
    position: relative;
    padding-top: 66.6%;
    overflow: hidden;

    // Blur background image on hotspot selected   
    // &.is-selected {
    //     > img {
    //         filter: blur(4px);
    //         transition: all .4s .2s;
    //     }
    // }

    > img {
        position: absolute;
        top: 0;
        left: 0;
        display: block;
        width: 100%;
        height: auto;
        transition: all .5s;
    }
}

.hotspot-point {
    z-index: 2;
    position: absolute;
    display: block;
    
    span {
        position: relative;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 2.6em;
        height: 2.6em;
        background: $spot-color;
        border-radius: 50%;
        animation: pulse 3s ease infinite;
        transition: background .3s;
        box-shadow: 0 2px 10px rgba(#000, .2);
        left:-1.3em;
        top: -1.3em;
    }
  
    &.selected {
      z-index: 100;
      
      svg {
       transform: rotate(45deg);
      }
    }

    svg {
        opacity: 1;
        color: white;
        font-size: 1.4em;
        transition: transform .2s, opacity .2s;
    }

    &:before,
    &:after  {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        border-radius: 50%;
        pointer-events: none;
    }

    &:before {
        z-index: -1;
        border: .15em solid rgba(#fff, .9);
        opacity: 0;
        transform: scale(2);
        transition: transform .25s, opacity .2s;
        left: -1.3em;
        top: -1.3em;
    }

    &:after {
        z-index: -2;
        border: 4px solid #fff;
        animation: wave 2s infinite;
        left: -1.3em;
        top: -1.3em;
    }
    
    &:hover {
      z-index: 200;

        span {
            animation: none;
            background: #fff;

            &:after {
                opacity: 1;
                transform: translate(-50%, 0) scale(1);
            }
        }

        svg {
            color: $spot-color;
            opacity: 1;
        }

        &:before {
            opacity: 1;
            transform: scale(1.3);
            animation: borderColor 2s linear infinite;
        }
        &:after {
            animation: none;
            opacity: 0;
        }
    }
}

.hotspots-enter-active {
  transition: all .5s 1s;
} 
.hotspots-leave-active {
  transition: all .5s;
}
.hotspots-enter, .hotspots-leave-to {
  opacity: 0;
  transform: scale(.3);
}

@keyframes pulse{
    0%, 100% {
        transform: scale(1);
    }
}
@keyframes borderColor{
    0% {
        border-color: #fff;
    }
    50% {
        border-color: $spot-color;
    }
    100% {
        border-color: #fff;
    }
}

@keyframes wave{
    0% {
        opacity: 1;
        transform: scale(.8);
    }
    100% {
        opacity: 0;
        transform: scale(2);
    }
}

.hotspot-details {
  --top: 0;
  --left: 0;

  background: #fff;
  z-index: 10;
  position: absolute;
  bottom: 0;
  right: 0;
  width: 25%;
  height: 20%;
  will-change: clip-path;
  transform: translateZ(0);

  &.is-loaded {
    .hotspot-details__content {
      opacity: 1;
      transform: scale(1);
      transition: opacity .3s, transform .3s;
      backface-visibility: hidden;
    }
  }

  &__content {
    opacity: 0;
    width: 100%;
    transition: opacity .3s .3s, transform .3s .3s;

    img {
      width: 80%;
    }
  }

  &__title {
    background-color: $title-bg-color;
    color: white;
    margin-top: 0;
    margin-bottom: 0;
    padding: 1em;
    font-weight: 300;
    font-size: 16px;
    font-weight: 700;
    text-transform: uppercase;
  }
  
  &__description {
    color: $text-color;
    font-size: 14px;
    padding: 1em;

    @media (min-width: 768px) {
      font-size: 16px;
    }
  }

  &__nav {
    position: absolute;
    bottom: 1em;
    left: 0;
    width: 47%;
    display: flex;
  }

  &__nav-item {
    width: 33%;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 1em;
    transform: translate(0, 120%);
    transition: transform .3s;

    @for $i from 2 through 3 {
      &:nth-child(#{$i}) {
          transition-delay: 75ms * $i;
      }
    }

    &:hover {
      img {
        transform: scale(1.2);
        opacity: 1;
      }
    }

    img {
      width: 35px;
      opacity: .8;
      transform: scale(1);
      transition: transform .2s, opacity .2s;
      backface-visibility: hidden;
    }
  }

  &__close {
    display: block;
    z-index: 2;
    position: absolute;
    top: 10px;
    right: 10px;
    color: black;
    font-size: 2em;
    line-height: 1;
    text-decoration: none;

    &:hover,
    &:active {
      color: #000;
      transition: transform .3s;
    }
    &:active {
      transform: scale(1.1);
    }
  }
}

.detail-enter-active {
  animation: detailsReveal .4s cubic-bezier(0.645, 0.045, 0.355, 1);
}

.detail-leave-active {
  animation: detailsReveal .4s .4s cubic-bezier(0.645, 0.045, 0.355, 1) reverse;
}

@keyframes detailsReveal {
  0% {
    clip-path: circle(0 at 100% 100%);
  }
  100% {
    clip-path: circle(130% at 50% 50%);
  }
}
View Compiled
const hotspots = [
  {
    title: 'Equipped with on board cranes',
    description: 'First passenger ship in the world to be equipped with on board cranes that allow 40ft LNG containers to be lifted into a fixed position.',
    position: { top: 55, left: 31}
  },
  {
    title: 'Will operate three daily return sailings',
    description: 'Alongside Brittany Ferries’ Mont St Michel, she will operate on three daily return sailings.',
    position: { top: 65, left: 42}
  },
  {
    title: 'Powered by LNG (Liquefied Natural Gas)',
    description: 'Compared with diesel fuel, LNG emits less carbon dioxide during combustion and burns with no smoke.',
    position: { top: 42, left: 50}
  },
  {
    title: 'Digital innovations to passenger spaces',
    description: 'WiFi in all public spaces, cabins, exterior decks and car decks, a first for Brittany Ferries.',
    position: { top: 55, left: 58}
  },
  {
    title: 'Will carry up to 1,680 passengers',
    description: 'And will come with 257 cabins, two cinemas, restaurants, boutique shopping and expanisve passenger lounges.',
    position: { top: 47, left: 77}
  },
  {
    title: 'Investments in ‘scrubber’ technology',
    description: 'A €90 million investment in sulphur and pariculate-reducing ‘scrubber’ technology follows the move to LNG.',
    position: { top: 65, left: 70}
  }
]

const HotspotDetails = {
  name: 'HotspotDetails',
  template: `
    <transition
      name="detail"
      @before-enter="beforeEnter"
      @after-enter="afterEnter"
      @before-leave="beforeLeave"
      @after-leave="afterLeave"
    >
      <div class="hotspot-details">
        <div class="hotspot-details__content">
            <transition name="product-fade" mode="out-in">
              <h3 class="hotspot-details__title animated" :key="selectedItem.title">{{ selectedItem.title }}</h3>
            </transition>
            <transition name="product-fade" mode="out-in">
              <div class="hotspot-details__description animated" :key="selectedItem.title">{{ selectedItem.description }}</div>
            </transition>
          </div>
      </div>
    </transition>
  `,
  props: {
    item: { type: Object },
    selectedIndex: { type: Number },
    allItems: { type: Array }
  },
  data() {
    return {
      selectedItem: this.item
    }
  },
  methods: {
    close() {
      this.$emit('close');
    },
    onProductSelected() {
      this.selectedItem = this.allItems[this.carousel.selectedIndex];
    },
    beforeEnter(el) {
      el.style.setProperty(`--top`, this.item.position.top+'%');
      el.style.setProperty(`--left`, this.item.position.left+'%');
      el.style.setProperty(`top`, (this.item.position.top-20)+'%');
      el.style.setProperty(`left`, (this.item.position.left-25)+'%');
    },
    afterEnter(el) {
      el.classList.add('is-loaded');
    },
    beforeLeave(el) {
      el.classList.remove('is-loaded');
      this.$emit('after');
    },
    afterLeave(el) {
      this.$emit('after');
    }
  }
}

const App = {
  name: 'app',
  components: {
    HotspotDetails
  },
  template: `
    <div class="app">
      <div class="image-hotspot" :class="{'is-selected': open }">
        <hotspot-details 
          :item="selectedHotspot"
          :selected-index="selectedIndex"
          :all-items="hotspots"
          @close="closeDetails"
          v-on:after="closeAfter"
          v-if="open"
        ></hotspot-details>
        <transition-group name="hotspots">
          <a 
            href="#" 
            class="hotspot-point"
            v-for="(hotspot, index) in hotspotItems"
            :style="{ top: hotspot.position.top+'%', left: hotspot.position.left+'%' }"
            @click.prevent="hotspotClicked(hotspot, index)"
            :key="index"
            :class="{ selected: hotspot.clicked }"
          >
              <span>
                <svg class="icon icon-close" viewBox="0 0 24 24">
                  <path d="M18.984 12.984h-6v6h-1.969v-6h-6v-1.969h6v-6h1.969v6h6v1.969z"></path>
                </svg>
              </span>
          </a>
        </transition-group>
        <img src="https://bf-honfleur.s3.amazonaws.com/img/ship-explore.jpg" alt="" class="img-fluid">
      </div>
    </div>
  `,
  data () {
    return {
      hotspots,
      open: false,
      hotspotPosition: null,
      selectedHotspot: null
    }
  },
  computed: {
    hotspotItems() {
      // return this.open ? [] : this.hotspots;
      return this.hotspots; // Displays all hotspots when displaying details
    }
  },
  methods: {
    closeDetails() {
      this.open = false;
      // this.selectedHotspot.clicked = false;
    },
    closeAfter() {
      console.log(this.selectedHotspot);
      this.selectedHotspot.clicked = false;
      this.selectedIndex = false;
    },
    hotspotClicked(hotspot, index) { 
      if (this.selectedIndex && this.selectedIndex !== index){
        return;
      }
      
      if (this.selectedIndex === index){
        this.open = false;
        this.selectedHotspot.clicked = false;
      }
      else {
        this.selectedHotspot = hotspot;
        this.selectedIndex = index;
        this.open = true;
        this.selectedHotspot.clicked = true;
      }
    }
  }
}

new Vue({
  el: '#app',
  components: {
    App
  },
  render: h => h(App)
})
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/flickity@2/dist/flickity.pkgd.min.js
  2. https://unpkg.com/vue