<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;
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
This Pen doesn't use any external CSS resources.