multiple scenes, drag and drop, blob snapshots from video

main
km0 1 year ago
parent 64e14c715d
commit 43e73e3dc9

@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/baobab.svg" />
<link rel="stylesheet" href="/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Story Baobab</title>
</head>

@ -9,7 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.47"
"vue": "^3.2.47",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",

@ -5,39 +5,27 @@
</header>
<main>
<AddScene @add="add"/>
<section class="scenes">
<SceneEntry
v-for="(scene, index) in scenes"
:order="index+1"
:title="scene.title"
:description="scene.description"
:img="scene.img"
@remove="remove(index)"
/>
</section>
<input v-model="title">
<button @click="add">New Scene</button>
<SceneEntry v-for="scene in scenes" :title="scene"/>
</main>
</template>
<script setup>
import AddScene from './components/AddScene.vue'
import SceneEntry from './components/SceneEntry.vue'
import SceneEntry from './components/SceneEntry.vue'
import {ref} from 'vue'
const scenes = ref([])
const add = (e) => {
scenes.value.push(e)
}
const remove = (index) => {
scenes.value.splice(index, 1)
}
const title = ref('')
const add = () => {
scenes.value.push(title.value)
title.value = ""
}
</script>
@ -47,11 +35,10 @@ header {
margin: 32px;
}
.scenes {
main {
margin: 32px;
display: flex;
flex-wrap: wrap;
gap: 16px;
}
</style>

@ -1,6 +1,6 @@
<template>
<div class="add-scene">
<FileLoader @upload="getImg" :key="key"></FileLoader>
<FileLoader class="cover" @upload="getImg" @filename="setTitle" :key="key"></FileLoader>
<div class="meta">
<input v-model="title" placeholder="Title">
<textarea v-model="description" placeholder="Description"></textarea>
@ -21,6 +21,8 @@
const getImg = (e) => img.value = e
const setTitle = e => title.value = e
const add = () => {
emits('add', {
@ -43,21 +45,32 @@
<style>
.add-scene {
margin: 32px;
display: flex;
position: relative;
display: inline-flex;
flex-direction: column;
gap: 8px;
}
.add-scene .cover {
width: 320px;
height: 180px;
object-fit: cover;
}
.meta {
width: 45ch;
position: relative;
max-width: 320px;
display: flex;
gap: 8px;
flex-direction: column;
gap: 8px;
}
input, textarea, button {
.meta input,
.meta textarea,
.meta button {
width: 100%;
box-sizing: border-box;
margin: 0;
padding: 8px;
font-family: sans-serif;
outline: none;

@ -15,7 +15,7 @@
<script setup>
import {ref, computed} from 'vue'
const emit = defineEmits(['upload'])
const emit = defineEmits(['upload', 'filename'])
const file = ref([])
@ -35,27 +35,32 @@
return 'div'
})
const createPreview = (file) => {
let fileExt;
const createPreview = async (file) => {
loading.value = true
const fileName = file.name
const filePath = (window.URL || window.webkitURL).createObjectURL(file)
// get file extension
fileExt = fileName.toLowerCase().substr(fileName.lastIndexOf('.')+1, fileName.length - fileName.lastIndexOf('.'))
emit('filename', fileName)
// get file type
fileType.value = file.type.toLowerCase().substr(0, file.type.indexOf("/"))
let filePath = (window.URL || window.webkitURL).createObjectURL(file)
const reader = new FileReader()
reader.readAsDataURL(file)
loading.value = true
// store the initial file
let fileToRead = file
// if file is video create a snapshot and use that instead of the orignal file
if (fileType.value == 'video') {
fileToRead = await videoSnapshot(filePath)
filePath = (window.URL || window.webkitURL).createObjectURL(fileToRead)
}
const reader = new FileReader()
reader.readAsDataURL(fileToRead)
reader.onloadend = (e) => {
if (e.target.readyState == FileReader.DONE) {
if (fileType.value == 'video') videoSnapshot(filePath)
if (fileType.value == 'image') imageSnapshot(filePath)
imageSnapshot(filePath)
}
}
}
@ -68,22 +73,35 @@
const videoSnapshot = (url) => {
let video = document.createElement('video')
video.src = url
const snapshot = () => {
let canvas = document.createElement('canvas')
canvas.width = 1280
canvas.height = 720
let ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
thumb.value.src = canvas.toDataURL('image/png')
emit('upload', thumb.value.src)
loading.value = false
video.removeEventListener('canplay', snapshot)
}
video.addEventListener('canplay', snapshot)
// wrap logic into a promise, in order to wait for the 'canplay' event
return new Promise((resolve, reject)=>{
let video = document.createElement('video')
video.src = url
const snapshot = async () => {
let canvas = document.createElement('canvas')
canvas.width = 1280
canvas.height = 720
let ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
thumb.value.src = canvas.toDataURL('image/png')
loading.value = false
video.removeEventListener('canplay', snapshot)
let file = await dataUrlToFile(thumb.value.src, 'snapshot')
resolve(file)
}
video.addEventListener('canplay', snapshot)
})
}
const dataUrlToFile = async (dataUrl, fileName) => {
// Transform Base64 img to blob file,
// in order not to have super long src urls for snapshot in the DOM
const res = await fetch(dataUrl);
const blob = await res.blob();
return new File([blob], fileName, { type: 'image/png' });
}
</script>
@ -92,10 +110,11 @@
.drop {
position: relative;
display: inline-block;
width: 160px;
height: 90px;
border: 1px solid currentColor;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
padding: 16px;
}

@ -1,79 +1,101 @@
<template>
<div class="scene">
<button class="remove" @click="emit('remove')">x</button>
<img :src="img">
<div class="meta">
<span class="order">{{order}}</span>
<h3 class="title">{{title}}</h3>
<p class="description">{{description}}</p>
</div>
</div>
<section class="scene">
<h2>{{title}}</h2>
<draggable
tag="transition-group"
:component-data="{
tag: 'div',
type: 'transition-group',
name: !drag ? 'flip-list' : null
}"
v-bind="dragOptions"
group="shots"
class="shots list-group"
:list="shots"
@start="drag=true"
@end="drag=false"
item-key="id">
<template #footer>
<AddShot @add="add"/>
</template>
<template #item="{element, index}">
<ShotEntry
:title="element.title"
:description="element.description"
:img="element.img"
:order="index + 1"
@remove="remove(index)"
/>
</template>
</draggable>
</section>
</template>
<script setup>
import draggable from 'vuedraggable'
import AddShot from '../components/AddShot.vue'
import ShotEntry from '../components/ShotEntry.vue'
const props = defineProps(['title', 'description', 'img', 'order'])
const emit = defineEmits(['remove'])
import {ref, computed} from 'vue'
</script>
<style>
const props = defineProps(['title'])
.scene {
position: relative;
display: inline-block;
border: 1px solid currentColor;
}
const shots = ref([])
const drag = ref(false)
const dragOptions = computed(()=> {
return {
animation: 200,
group: "description",
disabled: false,
ghostClass: "ghost"
}})
.scene img {
width: 320px;
height: 180px;
object-fit: cover;
}
.meta {
position: relative;
max-width: 320px;
const add = (e) => {
shots.value.push(e)
}
.title, .description{
margin-block: 0;
margin-inline: 8px;
const remove = (index) => {
shots.value.splice(index, 1)
}
.description {
font-size: 0.75rem;
margin-bottom: 8px;
</script>
<style>
.shots {
margin-block: 32px;
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.order {
position: absolute;
top: 0;
right: 4px ;
display: inline-block;
background-color: white;
z-index: 100;
padding: 4px;
font-weight: bold;
.ghost {
opacity: 0.5;
}
.remove {
opacity: 0;
dislay: inline-block;
position: absolute;
background: none;
border: none;
color: white;
right: 4px;
top: 0px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
.scene {
margin-block: 32px;
}
.scene:hover .remove {
opacity: 1;
.scene h2 {
margin-block: 32px;
}
</style>

@ -0,0 +1,85 @@
<template>
<div class="shot">
<button class="remove" @click="emit('remove')">x</button>
<img :src="img">
<div class="meta">
<span class="order">{{order}}</span>
<h3 class="title">{{title}}</h3>
<p class="description">{{description}}</p>
</div>
</div>
</template>
<script setup>
const props = defineProps(['title', 'description', 'img', 'order'])
const emit = defineEmits(['remove'])
</script>
<style>
.shot {
position: relative;
display: inline-block;
}
.shot img {
width: 320px;
height: 180px;
object-fit: cover;
}
.shot .meta {
position: relative;
max-width: 320px;
gap: 0;
}
.title, .description{
margin-block: 0;
margin-inline: 8px;
}
.title {
word-wrap: break-word;
padding-right: 1rem;
}
.description {
font-size: 0.85rem;
margin-bottom: 8px;
}
.order {
position: absolute;
top: 0;
right: 4px ;
display: inline-block;
background-color: white;
z-index: 100;
padding: 4px;
font-weight: bold;
opacity: 0.4;
}
.remove {
opacity: 0;
dislay: inline-block;
position: absolute;
background: none;
border: none;
color: white;
right: 4px;
top: 0px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
}
.shot:hover .remove {
opacity: 1;
}
</style>

@ -293,6 +293,11 @@ rollup@^3.21.0:
optionalDependencies:
fsevents "~2.3.2"
sortablejs@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
@ -319,3 +324,10 @@ vue@^3.2.47:
"@vue/runtime-dom" "3.3.4"
"@vue/server-renderer" "3.3.4"
"@vue/shared" "3.3.4"
vuedraggable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
dependencies:
sortablejs "1.14.0"

Loading…
Cancel
Save