Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
Tomáš Mládek | c637ebd144 | |
Tomáš Mládek | 9ee3bff615 | |
Tomáš Mládek | a4f6194ea2 | |
Tomáš Mládek | f9547511d8 | |
Tomáš Mládek | 3e5b2ca502 | |
Tomáš Mládek | 062fbf84df |
|
@ -0,0 +1,37 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.1.1] - 2019-11-02
|
||||
|
||||
### Added
|
||||
|
||||
- Version label and link to gitlab repo to sidebar
|
||||
|
||||
### Changed
|
||||
|
||||
- Cleaned up UI
|
||||
- Recording is now done through `MediaRecorder` API instead of `ccapture.js` - less dependencies, works on firefox
|
||||
|
||||
## [1.1.0] - 2019-11-01
|
||||
|
||||
### Added
|
||||
|
||||
- Loop & fullscreen button
|
||||
- Discarding / filtering out partial frames
|
||||
- Slices can have an offset from origin
|
||||
- Slice size and offset can be set by clicking on canvas
|
||||
|
||||
## [1.0.0] - 2018-02-26
|
||||
|
||||
Initial version. Supports recording, basic slitscan functionality.
|
||||
|
||||
[unreleased]: https://gitlab.com/tmladek/slitscan/compare/v1.1.1...master
|
||||
[1.1.1]: https://gitlab.com/tmladek/slitscan/compare/v1.1.0...v1.1.1
|
||||
[1.1.0]: https://gitlab.com/tmladek/slitscan/compare/v1.0.0...v1.1.0
|
||||
[1.0.0]: https://gitlab.com/tmladek/slitscan/-/tags/v1.0.0
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Tomáš Mládek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,4 +1,8 @@
|
|||
"use strict";
|
||||
const package_json = require("../package.json")
|
||||
|
||||
module.exports = {
|
||||
NODE_ENV: "\"production\"",
|
||||
VERSION: JSON.stringify(package_json.version),
|
||||
HOMEPAGE_URL: JSON.stringify(package_json.homepage)
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"name": "slitscan_vue",
|
||||
"version": "1.0.0",
|
||||
"description": "A Vue.js project",
|
||||
"author": "Tomáš Mládek <tmladek@inventati.org>",
|
||||
"version": "1.1.1",
|
||||
"description": "A video experiment for converting static images to strobe sequences.",
|
||||
"homepage": "https://gitlab.com/tmladek/slitscan",
|
||||
"author": "Tomáš Mládek <t@mldk.cz>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
|
||||
|
@ -11,12 +12,9 @@
|
|||
"build": "node build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bowser": "^1.9.2",
|
||||
"ccapture.js": "^1.0.7",
|
||||
"q": "^1.5.1",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"vue": "^2.5.2",
|
||||
"whammy": "0.0.1"
|
||||
"vue": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.2",
|
||||
|
|
17
src/App.vue
17
src/App.vue
|
@ -54,8 +54,25 @@ html, body, #app {
|
|||
font-size: .95em;
|
||||
}
|
||||
|
||||
input, button {
|
||||
font-family: Consolas, Inconsolata, monospace, serif;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: .95em;
|
||||
padding: 4px 0;
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
box-shadow: 2px 2px #272727;
|
||||
}
|
||||
|
||||
#menu-wrap, #canvas-wrap {
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
<div id="player">
|
||||
<div>
|
||||
<label for="zoom">Zoom:</label>
|
||||
<input type="number" min="0.01" value="1" step="0.1" class="slider" v-model.number="zoom" id="zoom">
|
||||
<input type="number" min="0.1" value="1" step="0.1" class="slider" v-model.number="zoom" id="zoom">
|
||||
</div>
|
||||
<div id="player-canvas-container">
|
||||
<div class="player-container">
|
||||
<canvas ref="canvas" id="player-canvas"
|
||||
:height="canvas_height" :width="canvas_width">
|
||||
</canvas>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<div>
|
||||
<input type="range" min="0" :max="frames - 1" value="0" class="slider"
|
||||
v-model.number="position" :disabled=recording>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div>
|
||||
<button @click="playOnceOrPause">{{!this.playing ? "PLAY ONCE" : "STOP"}}</button>
|
||||
</div>
|
||||
|
@ -23,6 +23,8 @@
|
|||
<div>
|
||||
<button @click="loopFullscreen">LOOP & FULLSCREEN</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options">
|
||||
<div>
|
||||
<label for="sort">Sort by size</label>
|
||||
<input type="checkbox" v-model="sortBySize" id="sort">
|
||||
|
@ -43,8 +45,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import CCapture from "ccapture.js";
|
||||
import bowser from "bowser";
|
||||
|
||||
export default {
|
||||
name: "player",
|
||||
|
@ -66,10 +66,7 @@ export default {
|
|||
position: 0,
|
||||
tmp_ctx: null,
|
||||
animation_id: null,
|
||||
capturer: new CCapture({
|
||||
format: "webm",
|
||||
verbose: true,
|
||||
}),
|
||||
recorder: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -156,13 +153,9 @@ export default {
|
|||
},
|
||||
recording: function () {
|
||||
if (this.recording) {
|
||||
this.looping = false;
|
||||
this.position = 0;
|
||||
if (!bowser.chrome) {
|
||||
alert("Recording only supported in Chrome :( \n" +
|
||||
"https://github.com/spite/ccapture.js/#limitations");
|
||||
}
|
||||
}
|
||||
this.looping = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -184,6 +177,10 @@ export default {
|
|||
console.log("STOPPED");
|
||||
cancelAnimationFrame(this.animation_id);
|
||||
this.playing = false;
|
||||
if (this.recording && this.recorder) {
|
||||
this.recorder.stop();
|
||||
this.recording = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
playOnceOrPause: function () {
|
||||
|
@ -197,11 +194,17 @@ export default {
|
|||
}
|
||||
},
|
||||
recordSequence: function () {
|
||||
try {
|
||||
this.createRecorder();
|
||||
} catch (err) {
|
||||
alert("Error initializing recording!\n" + err);
|
||||
return;
|
||||
}
|
||||
this.looping = false;
|
||||
this.recording = true;
|
||||
this.stop();
|
||||
this.position = 0;
|
||||
this.capturer.start();
|
||||
this.recorder.start();
|
||||
this.play();
|
||||
},
|
||||
loopFullscreen: function () {
|
||||
|
@ -220,14 +223,9 @@ export default {
|
|||
if (this.position < this.frames) {
|
||||
this.position += 1;
|
||||
this.animation_id = requestAnimationFrame(this.$render_advance);
|
||||
if (this.recording) this.capturer.capture(this.$refs.canvas);
|
||||
} else {
|
||||
if (!this.looping) {
|
||||
this.stop();
|
||||
if (this.recording) {
|
||||
this.capturer.stop();
|
||||
this.capturer.save();
|
||||
}
|
||||
} else {
|
||||
this.animation_id = requestAnimationFrame(this.$render_advance);
|
||||
}
|
||||
|
@ -243,6 +241,31 @@ export default {
|
|||
x, y, w, h,
|
||||
0, 0, w * this.zoom, h * this.zoom);
|
||||
},
|
||||
createRecorder: function () {
|
||||
const stream = this.$refs.canvas.captureStream();
|
||||
this.recorder = new MediaRecorder(stream, {mimeType: "video/webm"});
|
||||
if (!this.recorder) {
|
||||
throw new Error("Unknown error, couldn't initialize recorder.");
|
||||
}
|
||||
this.recorder.ondataavailable = function (event) {
|
||||
if (!event.data) {
|
||||
alert.error("Error during recording: No data available!");
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([event.data], {type: "video/webm"});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = "slitscan.webm";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 100);
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -252,21 +275,31 @@ export default {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(1, 1, 1, .9);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fullscreen-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#zoom {
|
||||
width: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player-container {
|
||||
padding: 2em 0 1em 0;
|
||||
}
|
||||
|
||||
.controls, .options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
width: 12em;
|
||||
margin: .25em;
|
||||
}
|
||||
|
||||
.options {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.options div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,54 +1,64 @@
|
|||
<template>
|
||||
<div id="sidebar-container">
|
||||
<div>
|
||||
<input type="file" id="file" @change="loadImage($event)">
|
||||
<div class="sidebar-section">
|
||||
<div>
|
||||
<input type="file" id="file" @change="loadImage($event)">
|
||||
</div>
|
||||
<div class="section-formlike">
|
||||
<label>Image size:</label>
|
||||
<div class="readout">{{imageSize[0]}} x {{imageSize[1]}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Image size: {{imageSize[0]}} x {{imageSize[1]}}</label>
|
||||
<div class="sidebar-section">
|
||||
<div class="section-formlike">
|
||||
<label for="slice_x">Slice X: </label>
|
||||
<input class="spinBox" id="slice_x" v-model.number="guideSizeX" type="number"
|
||||
min="2" :max="imageSize[0]/2" step="1" value="64">
|
||||
</div>
|
||||
<div>
|
||||
<label for="slice_x">X remainder: {{this.imageSize[0] % this.guideSizeX}}</label>
|
||||
</div>
|
||||
<div class="section-formlike">
|
||||
<label for="slice_y">Slice Y: </label>
|
||||
<input class="spinBox" id="slice_y" v-model.number="guideSizeY" type="number"
|
||||
min="2" :max="imageSize[1]/2" step="1" value="64">
|
||||
</div>
|
||||
<div>
|
||||
<label for="slice_x">Y remainder: {{this.imageSize[1] % this.guideSizeY}}</label>
|
||||
</div>
|
||||
<div class="section-formlike">
|
||||
<label for="lockSize">Maintain square ratio: </label>
|
||||
<input type="checkbox" id="lockSize" v-model="lockSize">
|
||||
</div>
|
||||
<div class="section-formlike">
|
||||
<label for="cleanSize">Allow only clean sizes: </label>
|
||||
<input type="checkbox" id="cleanSize" v-model="cleanSize">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<label for="slice_x">Slice X: </label>
|
||||
<input class="spinBox" id="slice_x" v-model.number="guideSizeX" type="number" min="2" :max="imageSize[0]/2"
|
||||
step="1" value="64">
|
||||
<div class="sidebar-section">
|
||||
<div class="section-formlike">
|
||||
<label>Offset: x={{offset[0]}}, y={{offset[1]}}</label>
|
||||
<button @click="$emit('params', {type: 'offset', x: 0, y: 0})">RESET</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="slice_x">No X remainder: {{noXremainder}}</label>
|
||||
<div class="sidebar-section">
|
||||
<div class="section-formlike">
|
||||
<label>Total amount of slices:</label>
|
||||
<div class="readout">{{slices}}</div>
|
||||
</div>
|
||||
<div class="section-formlike">
|
||||
<label for="fps">FPS: </label>
|
||||
<input class="spinBox" id="fps" v-model="fps" type="number" min="1" max="60" step="1" value="60" disabled>
|
||||
</div>
|
||||
<div class="section-formlike">
|
||||
<label>Est. length:</label>
|
||||
<div class="readout">{{length}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="slice_y">Slice Y: </label>
|
||||
<input class="spinBox" id="slice_y" v-model.number="guideSizeY" type="number" min="2" :max="imageSize[1]/2"
|
||||
step="1" value="64">
|
||||
<div class="sidebar-section player-section">
|
||||
<player class="player" :width="guideSizeX" :height="guideSizeY" :offset="offset" @frames="(n)=>{slices=n}"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="slice_x">No Y remainder: {{noYremainder}}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lockSize">Maintain square ratio: </label>
|
||||
<input type="checkbox" id="lockSize" v-model="lockSize">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cleanSize">Allow only clean sizes: </label>
|
||||
<input type="checkbox" id="cleanSize" v-model="cleanSize">
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<label>Offset: x={{offset[0]}}, y={{offset[1]}}</label>
|
||||
<button @click="$emit('params', {type: 'offset', x: 0, y: 0})">RESET</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<label>Total amount of slices: {{slices}}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="fps">FPS: </label>
|
||||
<input class="spinBox" id="fps" v-model="fps" type="number" min="1" max="60" step="1" value="60" disabled>
|
||||
</div>
|
||||
<div>
|
||||
<label>Est. length: {{length}}</label>
|
||||
</div>
|
||||
<hr>
|
||||
<player :width="guideSizeX" :height="guideSizeY" :offset="offset" @frames="(n)=>{slices=n}"/>
|
||||
<div class="footer"><a :href="env.HOMEPAGE_URL">Version: {{env.VERSION}}</a></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -160,11 +170,9 @@ export default {
|
|||
return "~" + seconds + " seconds" + (mins > 0 ? (" (" + mins + "m " + secs + "s)") : "");
|
||||
}
|
||||
},
|
||||
noXremainder: function () {
|
||||
return this.imageSize[0] % this.guideSizeX === 0;
|
||||
},
|
||||
noYremainder: function () {
|
||||
return this.imageSize[1] % this.guideSizeY === 0;
|
||||
env: function () {
|
||||
// eslint-disable-next-line no-undef
|
||||
return process.env;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -193,17 +201,47 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
#sidebar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.spinBox {
|
||||
height: 1em;
|
||||
.sidebar-section {
|
||||
padding: .5em;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: green;
|
||||
.sidebar-section > div {
|
||||
padding: .25em;
|
||||
}
|
||||
|
||||
.section-formlike {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-formlike input[type="number"] {
|
||||
width: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-formlike .readout {
|
||||
min-width: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player-section {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: .5em;
|
||||
left: .5em;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue