Compare commits

...

6 Commits

Author SHA1 Message Date
Tomáš Mládek c637ebd144 Add LICENSE 2019-11-02 11:37:15 +00:00
Tomáš Mládek 9ee3bff615 add changelog 2019-11-02 12:24:54 +01:00
Tomáš Mládek a4f6194ea2 add link @ version read-out to sidebar 2019-11-02 12:24:54 +01:00
Tomáš Mládek f9547511d8 update package.json 2019-11-02 12:24:54 +01:00
Tomáš Mládek 3e5b2ca502 get rid of ccapture.js, use MediaRecorder api instead
recording now works in Firefox
less dependencies
2019-11-02 12:24:54 +01:00
Tomáš Mládek 062fbf84df styling & improvements 2019-11-02 12:24:54 +01:00
8 changed files with 2530 additions and 2229 deletions

37
CHANGELOG.md Normal file
View File

@ -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

21
LICENSE Normal file
View File

@ -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.

View File

@ -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)
};

4421
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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 {

View File

@ -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>

View File

@ -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>