multiple scenes, drag and drop, blob snapshots from video

main
km0 2 years ago
parent 64e14c715d
commit 43e73e3dc9

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

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

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

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

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

@ -1,79 +1,101 @@
<template> <template>
<div class="scene">
<button class="remove" @click="emit('remove')">x</button> <section class="scene">
<img :src="img">
<div class="meta">
<span class="order">{{order}}</span> <h2>{{title}}</h2>
<h3 class="title">{{title}}</h3>
<p class="description">{{description}}</p>
</div>
</div> <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> </template>
<script setup> <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']) import {ref, computed} from 'vue'
const emit = defineEmits(['remove'])
</script> const props = defineProps(['title'])
<style>
.scene { const shots = ref([])
position: relative; const drag = ref(false)
display: inline-block;
border: 1px solid currentColor; const dragOptions = computed(()=> {
} return {
animation: 200,
group: "description",
disabled: false,
ghostClass: "ghost"
}})
.scene img {
width: 320px;
height: 180px;
object-fit: cover;
}
.meta { const add = (e) => {
position: relative; shots.value.push(e)
max-width: 320px;
} }
.title, .description{ const remove = (index) => {
margin-block: 0; shots.value.splice(index, 1)
margin-inline: 8px;
} }
.description {
font-size: 0.75rem;
margin-bottom: 8px;
</script>
<style>
.shots {
margin-block: 32px;
display: flex;
flex-wrap: wrap;
gap: 16px;
} }
.order { .ghost {
position: absolute; opacity: 0.5;
top: 0;
right: 4px ;
display: inline-block;
background-color: white;
z-index: 100;
padding: 4px;
font-weight: bold;
} }
.remove { .scene {
opacity: 0; margin-block: 32px;
dislay: inline-block;
position: absolute;
background: none;
border: none;
color: white;
right: 4px;
top: 0px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
} }
.scene:hover .remove { .scene h2 {
opacity: 1; margin-block: 32px;
} }
</style> </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: optionalDependencies:
fsevents "~2.3.2" 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: source-map-js@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 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/runtime-dom" "3.3.4"
"@vue/server-renderer" "3.3.4" "@vue/server-renderer" "3.3.4"
"@vue/shared" "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