Skip to content

Instantly share code, notes, and snippets.

@CodeMyUI
Created August 9, 2017 01:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CodeMyUI/93c48398f7083f7000d57a785a8a96c7 to your computer and use it in GitHub Desktop.
Save CodeMyUI/93c48398f7083f7000d57a785a8a96c7 to your computer and use it in GitHub Desktop.
Study: Book UI Component
transition-group name="fade" tag="div" class="flex" :css="false" appear="" v-on:before-enter="beforeItemEnters" v-on:enter="whenItemEnters"
book v-for="(book, index) in books.artbooks" :key="book.id" :title="book.title" :subtitle="book.subtitle" :description="book.description" :image="book.image" :groove="book.groove" :book-styles="book.bookStyles" :obi-styles="book.obiStyles" :data-index="index"
/*
|----------------------------------------------------
| A little BEM helper, not required though
|----------------------------------------------------
*/
let BEM = {
computed: {
BEMBlock () {
return `v-${this.$options.name}`
},
BEMElement () {
return `v-${this.$parent.$options.name}__${this.$options.name}`
}
}
}
/*
|----------------------------------------------------
| Some Mock Data
|----------------------------------------------------
*/
const ARTBOOKS = [
{
id: '001',
title: 'Artworks',
subtitle: 'Tactics Ogre',
description: 'Full page and full color llustrations Sketches, for the game. 8.25" x 11.75", 232 pgs, all in color.',
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/16584/tactics_ogre_cover.jpg',
groove: true,
obiStyles: {
color: 'hsl(0, 0, 22%)',
backgroundColor: 'hsla(48, 89%, 50%, .22)',
textAlign: 'right'
}
},
{
id: '002',
title: 'The Art of Eorzea',
subtitle: 'Final Fantasy XIV',
description: 'One book that was recorded illustrations were drawn in the production process of summarizing the whole world released. "Shinsei Eoruzea" Genesis of record "Final Fantasy XIV" the first official art book of "Shinsei FFXIV"',
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/16584/ffxiv_taoe_cover.jpg',
groove: true,
bookStyles: {
color: 'hsl(0, 100%, 100%)',
backgroundColor: 'hsl(150, 8%, 5%)'
},
obiStyles: {
color: 'hsl(0, 100%, 100%)',
backgroundColor: 'hsla(0, 0%, 0%, .22)',
textAlign: 'right'
}
}
]
/*
|----------------------------------------------------
| Book Components
|----------------------------------------------------
*/
Vue.component('book', {
mixins: [BEM],
props: ['title', 'subtitle', 'description', 'image', 'groove', 'book-styles', 'obi-styles'],
data () {
return {
isFlipped: false,
classObject: {
'has-groove': this.groove
}
}
},
template: `
<article @click="flip" :class="[BEMBlock, classObject, { 'is-flipped' : isFlipped }]">
<obi
:title="title"
:subtitle="subtitle"
:book-styles="bookStyles"
:obi-styles="obiStyles"
:front="true">
</obi>
<cover :image="image" :book-styles="bookStyles" front>
<section slot="front"></section>
</cover>
<spine :book-styles="bookStyles"></spine>
<cover :book-styles="bookStyles" back>
<p slot="back" class="v-book__description" v-text="description"></p>
</cover>
<obi :obi-styles="obiStyles"></obi>
</article>
`,
methods: {
flip: function () {
this.isFlipped = !this.isFlipped
}
}
})
/*
|----------------------------------------------------
| The Obi, or Belly Band, around the book
|----------------------------------------------------
*/
Vue.component('obi', {
props: ['title', 'subtitle', 'obi-styles', 'front'],
template: `
<div class="v-book__obi" :style="obiStyles" :class="{ 'is-back': !front }">
<div v-if="front">
<div v-if="subtitle" class="v-book__subtitle" v-text="subtitle"></div>
<div v-if="title" class="v-book__title" v-text="title"></div>
</div>
</div>
`
})
Vue.component('cover', {
mixins: [BEM],
props: ['front','back', 'image', 'book-styles'],
data () {
return {
loading: false,
classObject: {
'is-back': (this.back !== undefined),
'is-front': (this.front !== undefined)
}
}
},
computed: {
/*A makeshift lazy-load that fixes the jittering
when the cover image is loaded. In a "real"
situation you would probably do something
more proper. */
coverImage: function() {
let vm = this
let img = new Image()
let url = this.image
vm.loading = true
img.onload = function() {
img.src = url
}
if (img.complete) {
vm.loading = false
}
return {
backgroundImage: `url(${this.image})`
}
}
},
template: `
<section :class="[BEMElement, classObject]" :style="[bookStyles, coverImage]">
<loader v-if="loading"></loader>
<slot name="front"></slot>
<slot name="back"></slot>
</section>
`
})
/*
Admitted, a separate component for a spine is a bit overkill,
but what the heck. We're just having fun here. */
Vue.component('spine', {
mixins: [BEM],
props: ['book-styles'],
template: `
<section :class="[BEMElement]" :style="[bookStyles]"></section>
`
})
Vue.component('loader', {
props: ['is-loading'],
template: `
<div class="v-loader">✵</div>
`
})
new Vue({
el: document.getElementsByTagName('body')[0],
data: {
books: {
artbooks: ARTBOOKS
}
},
methods: {
/* A note on staggering, or that neat, delayed
fade in effect on the books. In Vue 2.0, you
have to combine the special <transition-group>
tag along with a few methods like these below
to get a result like the one you see.
Basically, we first decide what "state" the
book should be in BEFORE it enters the stage
and then WHEN it enters the stage, we change
that state to how it should end up.
*/
beforeItemEnters: function (el) {
el.style.opacity = 0
el.style.top = '-10px'
},
whenItemEnters: function (el) {
const delay = el.dataset.index * 500
setTimeout(function(){
el.style.opacity = 1
el.style.top = '0px'
}, delay)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.0-beta.5/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.9.3/vue-resource.min.js"></script>

Study: Book UI Component

I've always loved art books from games such as Final Fantasy etc. Recently, I stumbled upon a huge collection of these in a Swedish geek shop and something struck me: "I want to create a digital version of these!"

It was a perfect opportunity for me to mix in a little VueJS as well.

For the curious of you, the base for the books and their 3D rotations are based on Mary Lou's awesome-as-usual "3D Book Showcase" demo from 2013.

A Pen by Anders Schmidt Hansen on CodePen.

License.

/*
|----------------------------------------------------
| Book UI Component: A Study
|----------------------------------------------------
*/
// A little preparation first
body {
font-family: 'Open Sans', Arial, sans-serif;
padding: 3.2rem;
background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/16584/retina_wood.png');
perspective: 1800px;
perspective-origin: left center;
}
[slot] { height: 100%; }
// To ease up layouting a little, I use flexbox
.flex {
display: flex;
justify-content: center;
align-items: center;
align-content: center;
flex-wrap: wrap;
height: 100%;
}
/*
|----------------------------------------------------
| The book itself
|----------------------------------------------------
| One special thing to notice is the different use
| of shadows. Instead of just one, generic shadow
| I wanted to simulate different shadows when
| lifting, tilting and flipping a book.
| Try hovering, pressing down and clicking to tilt,
| lift and flip one of the books (or both).
*/
.v-book {
$v-default-transition: 480ms ease-in-out;
$v-shadow-when-not-lifted: 3px 3px 15px hsla(0, 0%, 0%, .25);
$v-shadow-when-lifted: 30px 16px 40px hsla(0, 0%, 0%, .15);
$v-shadow-when-lifted-and-flipped: -30px 16px 40px hsla(0, 0%, 0%, .15);
$v-shadow-when-tilted: 15px 8px 20px hsla(0, 0%, 0%, .25);
flex-shrink: 0;
margin: 3.618rem;
width: 22.1rem;
height: 29.97rem;
transform-style: preserve-3d;
transition: all $v-default-transition;
cursor: pointer;
&.is-flipped {
transform: translate3d(0, 0, 140px) rotate3d(0, 1, 0, 180deg);
.v-book__cover.is-back {
box-shadow: $v-shadow-when-lifted-and-flipped;
}
}
&:not(.is-flipped) {
&:hover {
transform: translate3d(0, 0, 0) rotate3d(0, 1, 0, 25deg);
.v-book__cover.is-back {
box-shadow: $v-shadow-when-tilted;
}
}
&:active {
transform: translate3d(0, 0, 140px) rotate3d(0, 1, 0, 25deg);
z-index: 100;
user-select: none;
.v-book__cover.is-back {
box-shadow: $v-shadow-when-lifted;
}
}
}
&.has-groove .v-book__cover::after {
$groove-shadow: 0 2px 0 hsla(0, 0, 0, .05);
position: absolute;
top: 0;
left: 3px;
height: 100%;
width: 10px;
box-shadow:
3px $groove-shadow inset,
-3px $groove-shadow inset;
content: '';
}
&.has-groove .v-book__cover.is-back::after {
left: auto;
right: 3px;
}
&__cover {
position: absolute;
width: 100%;
height: 100%;
padding: 3.236rem;
background-color: white;
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
transition: box-shadow $v-default-transition;
&.is-front {
display: flex;
justify-content: center;
align-items: center;
padding: 0;
transform-origin: 0% 50%;
transform: translate3d(0, 0, 5px);
transition: transform $v-default-transition;
z-index: 10;
border-radius: 1px 3px 2px 1px;
}
&.is-back {
transform: rotate3d(0, 1, 0, -180deg) translate3d(0, 0, 6px);
box-shadow: $v-shadow-when-not-lifted;
background-color: white;
border-radius: 3px 1px 2px 2px;
}
}
@mixin spineBase {
position: absolute;
top: 0;
left: -7px;
width: 14px;
height: 100%;
transform: rotate3d(0, 1, 0, -90deg);
}
&__spine {
@include spineBase;
background-color: white;
}
&__obi {
position: absolute;
left: -1%;
bottom: 15px;
padding: 1.618rem;
width: 102.5%;
height: 80px;
background-color: hsla(0,0,100%,.75);
box-shadow: 0 2px 3px 0 hsla(0,0,0,.1);
transform: translate3d(0, 0, 6px);
transform-style: inherit;
z-index: 1;
border-radius: 1px;
&.is-back {
transform: rotate3d(0, 1, 0, -180deg) translate3d(0, 0, 6px);
}
// Obi Spine
&::after {
@include spineBase;
background-color: inherit;
box-shadow: inherit;
content: '';
}
}
&__title {
font-size: 2.25rem;
font-weight: 700;
}
&__subtitle {
font-size: 1.2rem;
font-weight: 600;
}
&__description {
font-size: 1.412rem;
margin: 0;
}
}
/*
|----------------------------------------------------
| A little loader. Hopefully you won't even see it
|----------------------------------------------------
*/
.v-loader {
position: absolute;
font-size: 72px;
opacity: 0;
animation: pulse 800ms alternate infinite;
@keyframes pulse {
from { opacity: 0; }
to { opacity: 1; }
}
}
<link href="https://codepen.io/andersschmidt/pen/9c3abb9209fdb855db392559017300dd" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment