feat: Add boomerang feature to UI

This commit is contained in:
wryk 2019-03-29 17:17:42 +01:00 committed by Tixie
parent 0ab14263ac
commit 615dd875ce
11 changed files with 230 additions and 37 deletions

View file

@ -7,10 +7,14 @@
-------------------------------------------------------------- */ -------------------------------------------------------------- */
.options { .options {
position: relative;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 2.2rem 0; overflow: hidden;
margin-right: -.2rem;
margin-left: -.2rem;
padding: 2.2rem .2rem;
} }
.options__select, .options__select,
@ -38,6 +42,80 @@
} }
.options__btn svg { .options__btn svg {
margin-right: 1.5rem;
vertical-align: middle; vertical-align: middle;
line-height: 1;
}
.options__btn--check {
position: relative;
}
.options__btn--check::after {
position: absolute;
right: 1.8rem;
bottom: 1.5rem;
width: 1.8rem;
height: 1.5rem;
background: url("/assets/img/boomerang-check.svg");
content: "";
animation: btnCheckArrive .5s cubic-bezier(.18,.89,.32,1.28);
animation-delay: 100ms;
}
@keyframes btnCheckArrive {
0% {
transform: scale(1);
}
50% {
transform: scale(3);
}
100% {
transform: scale(1);
}
}
/* Panel
-------------------------------------------------------------- */
.options__panel {
position: absolute;
top: 2.2rem;
bottom: 2.2rem;
left: 0;
display: flex;
overflow: hidden;
width: 100%;
border-radius: .3rem;
background-color: #4c4981;
transition: 300ms transform;
transform: translateY(-9rem);
}
.options__panel.active {
transform: translateY(0);
}
.option__panelOption,
.option__panelOption:visited {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;
width: 50%;
border: none;
border-radius: 0;
background-color: transparent;
color: #fff;
font-size: 1.2rem;
cursor: pointer;
}
.option__panelOption.current {
background-color: var(--color-primary);
}
.option__panelOption svg {
margin-bottom: .6rem;
max-height: 1.6rem;
} }

View file

@ -18,15 +18,23 @@
height: 11rem; height: 11rem;
} }
/* Capture actions
-------------------------------------------------------------- */
.capture-actions {
position: relative;
display: flex;
justify-content: center;
margin-top: auto;
margin-bottom: 4rem;
}
/* Capture button /* Capture button
-------------------------------------------------------------- */ -------------------------------------------------------------- */
.capture-btn, .capture-btn,
.capture-btn:visited { .capture-btn:visited {
position: relative; position: relative;
align-self: center;
margin-top: auto;
margin-bottom: 4rem;
padding: 0; padding: 0;
width: 8rem; width: 8rem;
height: 8rem; height: 8rem;
@ -83,3 +91,24 @@
transform: rotate(359deg); transform: rotate(359deg);
} }
} }
/* Switch button
-------------------------------------------------------------- */
.capture-switch,
.capture-switch:visited {
position: absolute;
top: calc(50% - 3rem);
right: 0;
padding: 1rem;
border: none;
border-radius: 0;
background-color: transparent;
color: #fff;
line-height: 1;
}
.capture-switch svg {
width: 3rem;
height: 3rem;
}

View file

@ -54,11 +54,19 @@
align-self: center; align-self: center;
} }
.capture-btn { .capture-actions {
flex-direction: column;
flex-shrink: .0; flex-shrink: .0;
margin: 0 auto; margin: 0 auto;
} }
.capture-switch,
.capture-switch:visited {
position: absolute;
top: 0;
right: calc(50% - 3rem);
}
/* Options /* Options
-------------------------------------------------------------- */ -------------------------------------------------------------- */

View file

@ -0,0 +1,4 @@
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.31802 4.31802C3.0143 2.62174 5.72789 2.56278 7.49488 4.14116L10.318 1.31802C12.0754 -0.43934 14.9246 -0.43934 16.682 1.31802C18.4393 3.07538 18.4393 5.92462 16.682 7.68198L10.682 13.682C8.92462 15.4393 6.07538 15.4393 4.31802 13.682L1.31802 10.682C-0.43934 8.92462 -0.43934 6.07538 1.31802 4.31802Z" fill="#212044"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5607 3.43934C15.1464 4.02513 15.1464 4.97487 14.5607 5.56066L8.56066 11.5607C7.97487 12.1464 7.02513 12.1464 6.43934 11.5607L3.43934 8.56066C2.85355 7.97487 2.85355 7.02513 3.43934 6.43934C4.02513 5.85355 4.97487 5.85355 5.56066 6.43934L7.5 8.37868L12.4393 3.43934C13.0251 2.85355 13.9749 2.85355 14.5607 3.43934Z" fill="#28EE94"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B

View file

@ -9,10 +9,11 @@ export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
state: { state: {
cameraShouldFaceUser: true, cameraShouldFaceUser: true,
timer: { duration: {
selected: 2, selected: 2,
list: [2, 3, 5] list: [2, 3, 5]
}, },
boomerang: false,
camera: null, camera: null,
capture: null, capture: null,
gif: null, gif: null,
@ -22,14 +23,16 @@ export default new Vuex.Store({
updateCameraShouldFaceUser (state, cameraShouldFaceUser) { updateCameraShouldFaceUser (state, cameraShouldFaceUser) {
state.cameraShouldFaceUser = cameraShouldFaceUser state.cameraShouldFaceUser = cameraShouldFaceUser
}, },
updateTimer (state, time) { updateDuration (state, time) {
state.timer.selected = time state.duration.selected = time
},
updateBoomerang (state, value) {
state.boomerang = value
}, },
updateCamera (state, camera) { updateCamera (state, camera) {
if (state.camera) { if (state.camera) {
state.camera.mediaStream.getTracks().forEach(track => track.stop()) state.camera.mediaStream.getTracks().forEach(track => track.stop())
} }
state.camera = camera state.camera = camera
}, },
updateCapture (state, capture) { updateCapture (state, capture) {

View file

@ -1,38 +1,78 @@
<template lang="html"> <template lang="html">
<div class="options"> <div class="options">
<select v-model="timer.selected" class="options__select" @change="updateTimer(timer.selected)"> <button v-if="backBtn" class="options__btn" @click="backBtn"> back</button>
<option v-for="time in timer.list" :key="time" :value="time"> <select v-if="!disabledTime" v-model="selectedTime" class="options__select" @change="updateDuration(duration.selected)">
<option v-for="time in duration.list" :key="time" :value="time">
{{ timeLabel(time) }} {{ timeLabel(time) }}
</option> </option>
<button class="options__btn" :class="{ 'options__btn--check': boomerang }" title="Boomerang mode" @click="openBoomerang">
<icon-boomerang></icon-boomerang>
</button>
</select> </select>
<button class="options__btn" @click="switchCamera"><icon-switch></icon-switch>switch</button> <div v-if="!disabledBoomerang" class="options__panel" :class="{ 'active': boomerangOpen }">
<button class="option__panelOption" :class="{ 'current': !boomerang }" @click="updateBoomerang(false)"><icon-disabled></icon-disabled>Linear</button>
<button class="option__panelOption" :class="{ 'current': boomerang }" @click="updateBoomerang(true)"><icon-boomerang></icon-boomerang>Boomerang</button>
</div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import iconSwitch from '/views/icons/ico-switch' import iconBoomerang from '/views/icons/ico-boomerang'
import iconDisabled from '/views/icons/ico-disabled'
export default { export default {
name: 'captureOptions', name: 'captureOptions',
components: { components: {
iconSwitch iconBoomerang,
iconDisabled
},
data () {
return {
boomerangOpen: false
}
},
props: {
backBtn: {
type: Function,
default: null
},
disabledTime: {
type: Boolean,
default: false
},
disabledBoomerang: {
type: Boolean,
default: false
}
}, },
computed: { computed: {
...mapState([ ...mapState([
'timer' 'duration',
]) 'boomerang'
]),
selectedTime: {
get: function () { return this.duration.selected },
set: function (value) { this.$store.commit('updateDuration', value) }
}
}, },
methods: { methods: {
switchCamera () {
this.$store.dispatch('requestCamera', true)
},
timeLabel (time) { timeLabel (time) {
return time + 's' return time + 's'
}, },
updateTimer (time) { updateDuration (time) {
this.$store.commit('updateTimer', time) this.$store.commit('updateDuration', time)
},
openBoomerang () {
this.boomerangOpen = true
},
closeBoomerang () {
this.boomerangOpen = false
},
updateBoomerang (value) {
this.$store.commit('updateBoomerang', value)
this.closeBoomerang()
} }
} }
} }

View file

@ -0,0 +1,5 @@
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.56 6C15.6 6 13.16 8.77941 12 10.3676C10.84 8.77941 8.4 6 5.44 6C2.44 6 0 8.69118 0 12C0 15.3088 2.44 18 5.44 18C8.4 18 10.84 15.2206 12 13.6324C13.16 15.2206 15.6 18 18.56 18C21.56 18 24 15.3088 24 12C24 8.69118 21.56 6 18.56 6ZM5.44 16.1029C3.4 16.1029 1.72 14.25 1.72 12C1.72 9.75 3.4 7.89706 5.44 7.89706C7.92 7.89706 10.12 10.8088 10.92 12C9.96 13.5 7.72 16.1029 5.44 16.1029ZM18.56 16.1029C16.08 16.1029 13.88 13.2353 13.08 12C13.88 10.8088 16.08 7.89706 18.56 7.89706C20.6 7.89706 22.28 9.75 22.28 12C22.28 14.25 20.6 16.1029 18.56 16.1029Z" fill="currentColor"/>
</svg>
</template>

View file

@ -0,0 +1,5 @@
<template>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4765 12.8907C10.4957 13.5892 9.29583 14 8 14C4.68629 14 2 11.3137 2 8C2 6.70291 2.40981 5.50453 3.1091 4.52331L11.4765 12.8907ZM12.8907 11.4765L4.52327 3.10906C5.50471 2.40967 6.70365 2 8 2C11.3137 2 14 4.68629 14 8C14 9.29583 13.5892 10.4957 12.8907 11.4765ZM13.9288 13.3712C15.2159 11.9514 16 10.0673 16 8C16 3.58172 12.4183 0 8 0C5.93272 0 4.04857 0.784126 2.62877 2.07118C2.50649 2.11999 2.39189 2.19389 2.29289 2.29289C2.19389 2.39189 2.11999 2.50649 2.07118 2.62877C0.784126 4.04858 0 5.93272 0 8C0 12.4183 3.58172 16 8 16C10.0673 16 11.9514 15.2159 13.3712 13.9288C13.4935 13.88 13.6081 13.8061 13.7071 13.7071C13.8061 13.6081 13.88 13.4935 13.9288 13.3712Z" fill="currentColor"/>
</svg>
</template>

View file

@ -1,4 +1,3 @@
<template lang="html"> <template lang="html">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-ccw"><polyline points="1 4 1 10 7 10"></polyline><polyline points="23 20 23 14 17 14"></polyline><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path> <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.25 4.99997V12.5H8.75" stroke="white" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/><path d="M28.75 25V17.5H21.25" stroke="white" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/><path d="M25.6125 11.25C24.9785 9.45845 23.9011 7.85673 22.4807 6.59425C21.0602 5.33176 19.3432 4.44968 17.4896 4.0303C15.6361 3.61091 13.7066 3.6679 11.881 4.19594C10.0555 4.72398 8.39343 5.70586 7.05 7.04997L1.25 12.5M28.75 17.5L22.95 22.95C21.6066 24.2941 19.9445 25.276 18.119 25.804C16.2934 26.3321 14.3639 26.389 12.5104 25.9697C10.6568 25.5503 8.93975 24.6682 7.51933 23.4057C6.09892 22.1432 5.02146 20.5415 4.3875 18.75" stroke="white" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>
</svg>
</template> </template>

View file

@ -9,7 +9,10 @@
<video ref="preview" class="preview-visual" :class="{ 'preview--flip': shouldFlip }" preload="yes" autoplay muted playsinline webkit-playsinline></video> <video ref="preview" class="preview-visual" :class="{ 'preview--flip': shouldFlip }" preload="yes" autoplay muted playsinline webkit-playsinline></video>
</div> </div>
<button class="capture-btn" :class="{ 'capture-btn--capturing': capturing }" :disabled="!camera" @click.prevent="startCapturing">Capture</button> <div class="capture-actions">
<button class="capture-btn" :class="{ 'capture-btn--capturing': capturing }" :disabled="!camera" @click.prevent="startCapturing">Capture</button>
<button class="capture-switch" title="Switch camera" @click="switchCamera"><icon-switch></icon-switch></button>
</div>
</div> </div>
</template> </template>
@ -22,11 +25,14 @@ import 'objectFitPolyfill'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import iconSwitch from '/views/icons/ico-switch'
export default { export default {
name: 'capture', name: 'capture',
components: { components: {
captureOptions, captureOptions,
captureProgress captureProgress,
iconSwitch
}, },
data: () => ({ data: () => ({
capturing: false, capturing: false,
@ -35,7 +41,7 @@ export default {
computed: { computed: {
...mapState([ ...mapState([
'camera', 'camera',
'timer', 'duration',
'capture' 'capture'
]), ]),
shouldFlip () { shouldFlip () {
@ -57,9 +63,12 @@ export default {
} }
}, },
methods: { methods: {
switchCamera () {
this.$store.dispatch('requestCamera', true)
},
startCapturing () { startCapturing () {
this.capturing = true this.capturing = true
const capturing = capture(this.camera, this.timer.selected * 1000) const capturing = capture(this.camera, this.duration.selected * 1000)
capturing.once('error', error => { capturing.once('error', error => {
console.error(error) console.error(error)

View file

@ -1,9 +1,6 @@
<template lang="html"> <template lang="html">
<div class="download"> <div class="download">
<div class="options"> <capture-options :disabled-time="true" :back-btn="back"></capture-options>
<span></span>
<button class="options__btn" @click="back"> back</button>
</div>
<div class="preview"> <div class="preview">
<canvas ref="previewCanvas" class="preview-visual"></canvas> <canvas ref="previewCanvas" class="preview-visual"></canvas>
@ -18,6 +15,7 @@
<script> <script>
import { encode } from '/services/encode.js' import { encode } from '/services/encode.js'
import encodingOverlay from '/views/components/encoding' import encodingOverlay from '/views/components/encoding'
import captureOptions from '/views/components/capture-options'
import { cycle } from '/services/util.js' import { cycle } from '/services/util.js'
import { mapState } from 'vuex' import { mapState } from 'vuex'
@ -25,7 +23,8 @@ import { mapState } from 'vuex'
export default { export default {
name: 'preview', name: 'preview',
components: { components: {
encodingOverlay encodingOverlay,
captureOptions
}, },
data: () => ({ data: () => ({
encoding: false, encoding: false,
@ -35,7 +34,8 @@ export default {
computed: { computed: {
...mapState([ ...mapState([
'camera', 'camera',
'capture' 'capture',
'boomerang'
]) ])
}, },
methods: { methods: {
@ -54,7 +54,11 @@ export default {
canvas.height = this.capture.imageHeight canvas.height = this.capture.imageHeight
const canvasContext = canvas.getContext('2d') const canvasContext = canvas.getContext('2d')
const imagesIterator = cycle(this.capture.imageDataList) const frames = this.boomerang
? [...this.capture.imageDataList, ...this.capture.imageDataList.slice(1, this.capture.imageDataList.length - 1).reverse()]
: this.capture.imageDataList
const imagesIterator = cycle(frames)
const delay = this.capture.delayTime const delay = this.capture.delayTime
this.previewInterval = setInterval(() => { this.previewInterval = setInterval(() => {
@ -74,9 +78,12 @@ export default {
}, delay) }, delay)
} }
}, },
stopPreview () {
window.clearInterval(this.previewInterval)
},
startEncoding () { startEncoding () {
this.encoding = true this.encoding = true
const encoding = encode(this.capture, { boomerangEffect: false }) const encoding = encode(this.capture, { boomerangEffect: this.boomerang })
encoding.once('error', error => { encoding.once('error', error => {
console.error(error) console.error(error)
@ -96,6 +103,12 @@ export default {
}) })
} }
}, },
watch: {
boomerang: function () {
this.stopPreview()
this.startPreview()
}
},
created () { created () {
if (!this.capture) { if (!this.capture) {
this.backHome() this.backHome()
@ -105,7 +118,7 @@ export default {
this.startPreview() this.startPreview()
}, },
destroyed () { destroyed () {
window.clearInterval(this.previewInterval) this.stopPreview()
} }
} }
</script> </script>