Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

17 changed files with 4634 additions and 7063 deletions

View file

@ -1,22 +1,12 @@
{ {
"presets": [ "presets": [
[ ["env", {
"env", "modules": false,
{ "targets": {
"modules": false, "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
"targets": {
"browsers": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
} }
], }],
"stage-2" "stage-2"
], ],
"plugins": [ "plugins": ["transform-vue-jsx", "transform-runtime"]
"transform-vue-jsx",
"transform-runtime"
]
} }

View file

@ -1,35 +1,25 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = { module.exports = {
"env": { root: true,
"browser": true, parser: 'babel-eslint',
"es6": true, parserOptions: {
sourceType: 'module'
}, },
"extends": [ env: {
"eslint:recommended", browser: true,
"plugin:vue/essential", },
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
], ],
"globals": { // add your custom rules here
"Atomics": "readonly", rules: {
"SharedArrayBuffer": "readonly", // allow async-await
}, 'generator-star-spacing': 'off',
"parserOptions": { // allow debugger during development
"ecmaVersion": 2018, 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
"sourceType": "module", }
}, }
"plugins": [
"vue",
],
"rules": {
"linebreak-style": [
"error",
"unix",
],
"quotes": [
"error",
"double",
],
"semi": [
"error",
"always",
],
},
};

View file

@ -5,6 +5,6 @@ module.exports = {
"postcss-import": {}, "postcss-import": {},
"postcss-url": {}, "postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json // to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}, "autoprefixer": {}
}, }
}; }

View file

@ -1,37 +0,0 @@
# 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
View file

@ -1,21 +0,0 @@
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,7 +1,7 @@
"use strict"; 'use strict'
const merge = require("webpack-merge"); const merge = require('webpack-merge')
const prodEnv = require("./prod.env"); const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, { module.exports = merge(prodEnv, {
NODE_ENV: "\"development\"", NODE_ENV: '"development"'
}); })

View file

@ -1,19 +1,19 @@
"use strict"; 'use strict'
// Template version: 1.2.8 // Template version: 1.2.8
// see http://vuejs-templates.github.io/webpack for documentation. // see http://vuejs-templates.github.io/webpack for documentation.
const path = require("path"); const path = require('path')
module.exports = { module.exports = {
dev: { dev: {
// Paths // Paths
assetsSubDirectory: "static", assetsSubDirectory: 'static',
assetsPublicPath: "/", assetsPublicPath: '/',
proxyTable: {}, proxyTable: {},
// Various Dev Server settings // Various Dev Server settings
host: "localhost", // can be overwritten by process.env.HOST host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false, autoOpenBrowser: false,
errorOverlay: true, errorOverlay: true,
@ -33,7 +33,7 @@ module.exports = {
*/ */
// https://webpack.js.org/configuration/devtool/#development // https://webpack.js.org/configuration/devtool/#development
devtool: "cheap-module-eval-source-map", devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools, // If you have problems debugging vue-files in devtools,
// set this to false - it *may* help // set this to false - it *may* help
@ -45,12 +45,12 @@ module.exports = {
build: { build: {
// Template for index.html // Template for index.html
index: path.resolve(__dirname, "../dist/index.html"), index: path.resolve(__dirname, '../dist/index.html'),
// Paths // Paths
assetsRoot: path.resolve(__dirname, "../dist"), assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: "static", assetsSubDirectory: 'static',
assetsPublicPath: "/tools/slitscan", assetsPublicPath: '/tools/slitscan',
/** /**
* Source Maps * Source Maps
@ -58,19 +58,19 @@ module.exports = {
productionSourceMap: true, productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production // https://webpack.js.org/configuration/devtool/#production
devtool: "#source-map", devtool: '#source-map',
// Gzip off by default as many popular static hosts such as // Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you. // Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to: // Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin // npm install --save-dev compression-webpack-plugin
productionGzip: false, productionGzip: false,
productionGzipExtensions: ["js", "css"], productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to // Run the build command with an extra argument to
// View the bundle analyzer report after build finishes: // View the bundle analyzer report after build finishes:
// `npm run build --report` // `npm run build --report`
// Set to `true` or `false` to always turn it on or off // Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report, bundleAnalyzerReport: process.env.npm_config_report
}, }
}; }

View file

@ -1,8 +1,4 @@
"use strict"; 'use strict'
const package_json = require("../package.json")
module.exports = { module.exports = {
NODE_ENV: "\"production\"", NODE_ENV: '"production"'
VERSION: JSON.stringify(package_json.version), }
HOMEPAGE_URL: JSON.stringify(package_json.homepage)
};

10355
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,8 @@
{ {
"name": "slitscan_vue", "name": "slitscan_vue",
"version": "1.1.1", "version": "1.0.0",
"description": "A video experiment for converting static images to strobe sequences.", "description": "A Vue.js project",
"homepage": "https://gitlab.com/tmladek/slitscan", "author": "Tomáš Mládek <tmladek@inventati.org>",
"author": "Tomáš Mládek <t@mldk.cz>",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
@ -12,9 +11,12 @@
"build": "node build/build.js" "build": "node build/build.js"
}, },
"dependencies": { "dependencies": {
"bowser": "^1.9.2",
"ccapture.js": "^1.0.7",
"q": "^1.5.1", "q": "^1.5.1",
"sprintf-js": "^1.1.1", "sprintf-js": "^1.1.1",
"vue": "^2.5.2" "vue": "^2.5.2",
"whammy": "0.0.1"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^7.1.2", "autoprefixer": "^7.1.2",
@ -39,7 +41,6 @@
"eslint-plugin-node": "^5.2.0", "eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0", "eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^5.2.3",
"extract-text-webpack-plugin": "^3.0.0", "extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4", "file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1", "friendly-errors-webpack-plugin": "^1.6.1",

View file

@ -1,92 +1,62 @@
<template> <template>
<div id="app"> <div id="app">
<div id="menu-wrap"> <div id="menu-wrap">
<Sidebar ref="sidebar" @params="setParams" :slice="slice" :offset="offset" @loadImage="loadImage"/> <Sidebar ref="sidebar" @loadImage="loadImage" @guideSize="guideSize"/>
</div> </div>
<div id="canvas-wrap"> <div id="canvas-wrap">
<Canvas ref="canvas" @params="setParams" :slice="slice" :offset="offset"/> <Canvas ref="canvas"/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Canvas from "./components/Canvas"; import Canvas from './components/Canvas'
import Sidebar from "./components/Sidebar"; import Sidebar from './components/Sidebar'
export default { export default {
name: "app", name: 'app',
components: { components: {
Canvas, Sidebar, Canvas, Sidebar
},
data () {
return {
slice: [64, 64],
offset: [0, 0],
};
},
methods: {
loadImage: function (url) {
this.$refs.canvas.imageUrl = url;
}, },
setParams: function (coords) { methods: {
switch (coords.type) { loadImage: function (url) {
case "slice": this.$refs.canvas.imageUrl = url
this.slice = [Math.max(2, coords.x), Math.max(2, coords.y)]; },
break; guideSize: function (size) {
case "offset": this.$refs.canvas.slice = size
this.offset = [coords.x, coords.y];
break;
} }
}, }
}, }
};
</script> </script>
<style> <style>
html, body, #app { html, body, #app {
height: 100%; height: 100%;
width: 100%; width: 100%;
border: 0; border: 0;
margin: 0; margin: 0;
font-family: Consolas, Inconsolata, monospace, serif; font-family: Consolas, Inconsolata, monospace, serif;
line-height: .97em; line-height: .97em;
font-size: .95em; font-size: .95em;
} }
input, button { input {
font-family: Consolas, Inconsolata, monospace, serif; font-size: .95em;
} }
input { #menu-wrap, #canvas-wrap {
font-size: .95em; display: inline-block;
padding: 4px 0; }
background: white;
border: 1px solid black;
}
input[type="file"] { #menu-wrap {
border: none; width: 20%;
} height: 100%;
float: left;
}
button { #canvas-wrap {
background: white; width: 80%;
border: 1px solid black; height: 100%;
box-shadow: 2px 2px #272727; }
}
#menu-wrap, #canvas-wrap {
display: inline-block;
}
#menu-wrap {
width: 20%;
height: 100%;
float: left;
}
#canvas-wrap {
width: 80%;
height: 100%;
}
</style> </style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -1,129 +1,98 @@
<template> <template>
<div id="canvas-container"> <div id="canvas-container">
<canvas ref="canvas" id="canvas" @mousedown="onMouse" @mousemove="onMouse" oncontextmenu="return false;"></canvas> <canvas ref="canvas" id="canvas"></canvas>
</div> </div>
</template> </template>
<script> <script>
import {getDimsFit} from "../helpers/image.js"; import {getDimsFit} from '../helpers/image.js'
export default { export default {
name: "Canvas", name: 'Canvas',
data: function () { data: function () {
return { return {
imageUrl: "", imageUrl: '',
image: null, image: null,
imageLoaded: false, imageLoaded: false,
ratio: 1, slice: [0, 0]
};
},
props: ["slice", "offset"],
computed: {},
watch: {
imageUrl: function (url) {
this.loadImage(url);
},
slice: function () {
this.refresh();
},
offset: function () {
this.refresh();
},
},
mounted: function () {
window.addEventListener("resize", this.handleResize);
this.handleResize();
},
methods: {
loadImage: function (imageUrl) {
this.image = new Image();
this.image.onload = () => {
this.imageLoaded = true;
this.$bus.$emit("imageLoaded", this.image);
this.refresh();
};
this.image.src = imageUrl;
},
refresh: function () {
if (!this.imageLoaded) return;
let image = this.image;
let canvas = this.$refs.canvas;
let ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
let dims = getDimsFit(image.width, image.height, canvas.width, canvas.height);
ctx.drawImage(image,
0, 0, image.width, image.height,
0, 0, dims.width, dims.height);
this.ratio = dims.width / image.width;
let sliceX = parseInt(this.slice[0]) * this.ratio;
let sliceY = parseInt(this.slice[1]) * this.ratio;
if (sliceX > 0 && sliceY > 0) {
for (let x = this.offset[0] * this.ratio; x < dims.width; x += sliceX) {
ctx.beginPath();
ctx.moveTo(x, this.offset[1] * this.ratio);
ctx.lineTo(x, dims.height);
ctx.strokeStyle = "red";
ctx.stroke();
}
for (let y = this.offset[1] * this.ratio; y < dims.height; y += sliceY) {
ctx.beginPath();
ctx.moveTo(this.offset[0] * this.ratio, y);
ctx.lineTo(dims.width, y);
ctx.strokeStyle = "red";
ctx.stroke();
}
} }
}, },
handleResize: function () { computed: {},
let canvasContainer = document.getElementById("canvas-container"); watch: {
let canvas = document.getElementById("canvas"); imageUrl: function (url) {
this.loadImage(url)
canvas.width = canvasContainer.offsetWidth - 10; },
canvas.height = canvasContainer.offsetHeight - 10; slice: function () {
this.refresh(); this.refresh()
},
onMouse: function (ev) {
if (ev.buttons === 0) {
return;
} }
ev.preventDefault(); },
mounted: function () {
const type = { window.addEventListener('resize', this.handleResize)
1: "slice", this.handleResize()
2: "offset", },
}[ev.buttons]; methods: {
loadImage: function (imageUrl) {
if (type !== undefined) { this.image = new Image()
let x, y; this.image.onload = () => {
switch (type) { this.imageLoaded = true
case "slice": this.$bus.$emit('imageLoaded', this.image)
x = Math.round((ev.x - ev.target.offsetLeft) / this.ratio - this.offset[0]); this.refresh()
y = Math.round((ev.y - ev.target.offsetTop) / this.ratio - this.offset[1]);
break;
case "offset":
x = Math.round((ev.x - ev.target.offsetLeft) / this.ratio);
y = Math.round((ev.y - ev.target.offsetTop) / this.ratio);
break;
} }
this.$emit("params", {type, x, y}); this.image.src = imageUrl
},
refresh: function () {
if (!this.imageLoaded) return
let image = this.image
let canvas = this.$refs.canvas
let ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
let dims = getDimsFit(image.width, image.height, canvas.width, canvas.height)
ctx.drawImage(image,
0, 0, image.width, image.height,
0, 0, dims.width, dims.height)
let ratio = dims.width / image.width
let sliceX = parseInt(this.slice[0]) * ratio
let sliceY = parseInt(this.slice[1]) * ratio
if (sliceX > 0 && sliceY > 0) {
for (let x = 0; x < dims.width; x += sliceX) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, dims.height)
ctx.strokeStyle = 'red'
ctx.stroke()
}
for (let y = 0; y < dims.height; y += sliceY) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(dims.width, y)
ctx.strokeStyle = 'red'
ctx.stroke()
}
}
},
handleResize: function () {
let canvasContainer = document.getElementById('canvas-container')
let canvas = document.getElementById('canvas')
canvas.width = canvasContainer.offsetWidth - 10
canvas.height = canvasContainer.offsetHeight - 10
this.refresh()
} }
return false; }
}, }
},
};
</script> </script>
<style scoped> <style scoped>
#canvas-container { #canvas-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
border: 0; border: 0;
padding: 0; padding: 0;
} }
</style> </style>

View file

@ -1,30 +1,26 @@
<template> <template>
<div id="player"> <div id="player" :class="{fullscreen: fullscreen}">
<div> <div>
<label for="zoom">Zoom:</label> <label for="zoom">Zoom:</label>
<input type="number" min="0.1" value="1" step="0.1" class="slider" v-model.number="zoom" id="zoom"> <input type="number" min="1" :max="50" value="1" class="slider" v-model.number="zoom" id="zoom">
</div> </div>
<div class="player-container"> <div id="player-canvas-container">
<canvas ref="canvas" id="player-canvas" <canvas ref="canvas" id="player-canvas"
:height="canvas_height" :width="canvas_width"> :height="canvas_height" :width="canvas_width">
</canvas> </canvas>
</div>
<div id="controls" class="{'fullscreen-controls': fullscreen}">
<div>
<label for="recordMode">Record output</label>
<input type="checkbox" v-model="record" id="recordMode">
</div>
<div> <div>
<input type="range" min="0" :max="frames - 1" value="0" class="slider" <input type="range" min="0" :max="frames - 1" value="0" class="slider"
v-model.number="position" :disabled=recording> v-model.number="position" :disabled=record>
</div>
</div>
<div class="controls">
<div>
<button @click="playOnceOrPause">{{!this.playing ? "PLAY ONCE" : "STOP"}}</button>
</div> </div>
<div> <div>
<button @click="recordSequence">RECORD</button> <button @click="playPause">{{buttonLabel}}</button>
</div> </div>
<div>
<button @click="loopFullscreen">LOOP & FULLSCREEN</button>
</div>
</div>
<div class="options">
<div> <div>
<label for="sort">Sort by size</label> <label for="sort">Sort by size</label>
<input type="checkbox" v-model="sortBySize" id="sort"> <input type="checkbox" v-model="sortBySize" id="sort">
@ -36,270 +32,202 @@
</label> </label>
<input type="checkbox" v-model="sortDirection" id="sortDirection"> <input type="checkbox" v-model="sortDirection" id="sortDirection">
</div> </div>
<div>
<label for="discard">Discard partial frames</label>
<input type="checkbox" v-model="discardPartial" id="discard">
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import CCapture from 'ccapture.js'
import bowser from 'bowser'
export default { export default {
name: "player", name: 'player',
props: { props: {
width: Number, width: Number,
height: Number, height: Number
offset: Array,
},
data: function () {
return {
image: null,
playing: false,
zoom: 1,
recording: false,
looping: false,
sortBySize: false,
sortDirection: false,
discardPartial: false,
position: 0,
tmp_ctx: null,
animation_id: null,
recorder: null,
};
},
computed: {
frames: function () {
if (this.image === null) return 0;
const roundFn = this.discardPartial ? Math.floor : Math.ceil;
return roundFn((this.image.width - this.offset[0]) / this.width) * roundFn((this.image.height - this.offset[1]) / this.height);
}, },
canvas_height: function () { data: function () {
return this.height * this.zoom; return {
}, image: null,
canvas_width: function () { playing: false,
return this.width * this.zoom; zoom: 1,
}, fullscreen: false,
frame_sequence: function () { sortBySize: false,
if (this.image === null) return []; sortDirection: false,
position: 0,
let sequence = []; tmp_ctx: null,
for (let pos = 0; pos < this.frames; pos++) { animation_id: null,
let wb = (this.discardPartial ? Math.floor : Math.ceil)((this.image.width - this.offset[0]) / this.width); // width_blocks record: false,
let x = this.offset[0] + (pos % wb) * this.width; // x offset capturer: new CCapture({
let w = x + this.width < this.image.width ? this.width : this.image.width - x; // frame width format: 'webm',
let y = this.offset[1] + Math.floor(pos / wb) * this.height; // y offset verbose: true
let h = y + this.height < this.image.height ? this.height : this.image.height - y; // frame height })
sequence.push([x, y, w, h]);
} }
if (this.sortBySize) { },
let tmpCanvas = document.createElement("canvas"); computed: {
let ctx = tmpCanvas.getContext("2d"); frames: function () {
let sizes = sequence.map((el, i) => { if (this.image === null) return 0
if (tmpCanvas.width !== el[2] || return Math.ceil(this.image.width / this.width) *
tmpCanvas.height !== el[3]) { Math.ceil(this.image.height / this.height)
tmpCanvas.width = el[2]; },
tmpCanvas.height = el[3]; canvas_height: function () {
return this.height * this.zoom
},
canvas_width: function () {
return this.width * this.zoom
},
frame_sequence: function () {
if (this.image === null) return []
let sequence = []
for (let pos = 0; pos < this.frames; pos++) {
let wb = Math.ceil(this.image.width / this.width) // width_blocks
let x = (pos % wb) * this.width // x offset
let w = x + this.width < this.image.width ? this.width : this.image.width - x // frame width
let y = Math.floor(pos / wb) * this.height // y offset
let h = y + this.height < this.image.height ? this.height : this.image.height - y // frame height
sequence.push([x, y, w, h])
}
if (this.sortBySize) {
let tmpCanvas = document.createElement('canvas')
let ctx = tmpCanvas.getContext('2d')
let sizes = sequence.map((el, i) => {
if (tmpCanvas.width !== el[2] ||
tmpCanvas.height !== el[3]) {
tmpCanvas.width = el[2]
tmpCanvas.height = el[3]
}
ctx.drawImage(this.image, el[0], el[1], el[2], el[3],
0, 0, el[2], el[3])
return {index: i, length: tmpCanvas.toDataURL('image/png').length}
})
sizes.sort(function (a, b) {
return a.length - b.length
})
if (!this.sortDirection) {
sizes.reverse()
} }
ctx.drawImage(this.image, el[0], el[1], el[2], el[3],
0, 0, el[2], el[3]);
return {index: i, length: tmpCanvas.toDataURL("image/png").length};
});
sizes.sort(function (a, b) { return sizes.map(function (el) {
return a.length - b.length; return sequence[el.index]
}); })
if (!this.sortDirection) {
sizes.reverse();
}
return sizes.map(function (el) {
return sequence[el.index];
});
} else {
return sequence;
}
},
},
mounted: function () {
this.$bus.$on("imageLoaded", (image) => {
this.image = image;
});
this.clear("red");
},
watch: {
canvas_width: function () {
setTimeout(() => {
this.clear("red");
}, 0);
},
canvas_height: function () {
setTimeout(() => {
this.clear("red");
}, 0);
},
frames: function () {
this.$emit("frames", this.frames);
},
position: function () {
if (this.playing || this.image === null) return;
this.tmp_ctx = this.$refs.canvas.getContext("2d");
this.$render();
},
looping: function () {
this.recording = false;
},
recording: function () {
if (this.recording) {
this.looping = false;
this.position = 0;
}
},
},
methods: {
play: function () {
if (this.frames === 0) {
return;
}
if (!this.playing) {
// eslint-disable-next-line no-console
console.log("STARTED");
this.tmp_ctx = this.$refs.canvas.getContext("2d");
this.$render_advance();
this.playing = true;
}
},
stop: function () {
if (this.playing) {
// eslint-disable-next-line no-console
console.log("STOPPED");
cancelAnimationFrame(this.animation_id);
this.playing = false;
if (this.recording && this.recorder) {
this.recorder.stop();
this.recording = false;
}
}
},
playOnceOrPause: function () {
if (!this.playing) {
this.looping = false;
this.recording = false;
this.position = 0;
this.play();
} else {
this.stop();
}
},
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.recorder.start();
this.play();
},
loopFullscreen: function () {
this.play();
this.looping = true;
this.recording = false;
document.getElementById("player-canvas").requestFullscreen();
},
clear: function (style) {
let ctx = this.$refs.canvas.getContext("2d");
ctx.fillStyle = style;
ctx.fillRect(0, 0, parseInt(this.canvas_width), parseInt(this.canvas_height));
},
$render_advance: function () {
this.$render();
if (this.position < this.frames) {
this.position += 1;
this.animation_id = requestAnimationFrame(this.$render_advance);
} else {
if (!this.looping) {
this.stop();
} else { } else {
this.animation_id = requestAnimationFrame(this.$render_advance); return sequence
}
},
buttonLabel: function () {
if (!this.playing) {
if (this.record) {
return 'RECORD'
} else {
return 'PLAY'
}
} else {
return 'STOP'
} }
this.position = 0;
} }
}, },
$render: function () { mounted: function () {
let frame = this.frame_sequence[this.position]; this.$bus.$on('imageLoaded', (image) => {
if (frame === undefined) return; this.image = image
let [x, y, w, h] = frame; })
if (w !== this.width || h !== this.height) this.clear("black"); this.clear('red')
this.tmp_ctx.drawImage(this.image,
x, y, w, h,
0, 0, w * this.zoom, h * this.zoom);
}, },
createRecorder: function () { watch: {
const stream = this.$refs.canvas.captureStream(); canvas_width: function () {
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(() => { setTimeout(() => {
document.body.removeChild(a); this.clear('red')
window.URL.revokeObjectURL(url); }, 0)
}, 100); },
}; canvas_height: function () {
setTimeout(() => {
this.clear('red')
}, 0)
},
position: function () {
if (this.playing || this.image === null) return
this.tmp_ctx = this.$refs.canvas.getContext('2d')
this.$render()
},
record: function () {
if (this.record) {
this.position = 0
if (!bowser.chrome) {
alert('Recording only supported in Chrome :( \n' +
'https://github.com/spite/ccapture.js/#limitations')
}
}
}
}, },
}, methods: {
}; playPause: function () {
if (this.frames === 0) {
return
}
this.playing = !this.playing
if (this.playing) {
this.tmp_ctx = this.$refs.canvas.getContext('2d')
if (this.record) this.capturer.start()
this.$render_advance()
} else {
cancelAnimationFrame(this.animation_id)
}
},
clear: function (style) {
let ctx = this.$refs.canvas.getContext('2d')
ctx.fillStyle = style
ctx.fillRect(0, 0, parseInt(this.canvas_width), parseInt(this.canvas_height))
},
$render: function () {
let frame = this.frame_sequence[this.position]
if (frame === undefined) return
let [x, y, w, h] = frame
if (w !== this.width || h !== this.height) this.clear('black')
this.tmp_ctx.drawImage(this.image,
x, y, w, h,
0, 0, w * this.zoom, h * this.zoom)
},
$render_advance: function () {
this.$render()
if (this.position < this.frames) {
this.position += 1
this.animation_id = requestAnimationFrame(this.$render_advance)
if (this.record) this.capturer.capture(this.$refs.canvas)
} else {
this.playPause()
this.position = 0
if (this.record) {
this.capturer.stop()
this.capturer.save()
}
}
}
}
}
</script> </script>
<style scoped> <style scoped>
#player { #player {
text-align: center; text-align: center;
} }
#zoom { .fullscreen {
width: 3em; position: absolute;
text-align: center; top: 0;
} left: 0;
background-color: rgba(1, 1, 1, .9);
width: 100%;
height: 100%;
}
.player-container { .fullscreen-controls {
padding: 2em 0 1em 0; position: absolute;
} bottom: 0;
}
.controls, .options { #zoom {
display: flex; width: 3em;
flex-direction: column; }
}
.controls button {
width: 12em;
margin: .25em;
}
.options {
padding-top: 1em;
}
.options div {
display: flex;
justify-content: space-between;
}
</style> </style>

View file

@ -1,247 +1,185 @@
<template> <template>
<div id="sidebar-container"> <div id="sidebar-container">
<div class="sidebar-section"> <div>
<div> <input type="file" id="file" @change="loadImage($event)">
<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>
<div class="sidebar-section"> <div>
<div class="section-formlike"> <label>Image size: {{imageSize[0]}} x {{imageSize[1]}}</label>
<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> </div>
<div class="sidebar-section"> <hr>
<div class="section-formlike"> <div>
<label>Offset: x={{offset[0]}}, y={{offset[1]}}</label> <label for="slice_x">Slice X: </label>
<button @click="$emit('params', {type: 'offset', x: 0, y: 0})">RESET</button> <input class="spinBox" id="slice_x" v-model.number="guideSizeX" type="number" min="2" max="1024" step="1"
</div> value="64">
</div> </div>
<div class="sidebar-section"> <div>
<div class="section-formlike"> <label for="slice_x">No X remainder: {{noXremainder}}</label>
<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>
<div class="sidebar-section player-section"> <div>
<player class="player" :width="guideSizeX" :height="guideSizeY" :offset="offset" @frames="(n)=>{slices=n}"/> <label for="slice_y">Slice Y: </label>
<input class="spinBox" id="slice_y" v-model.number="guideSizeY" type="number" min="2" max="1024" step="1"
value="64">
</div> </div>
<div class="footer"><a :href="env.HOMEPAGE_URL">Version: {{env.VERSION}}</a></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>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"/>
</div> </div>
</template> </template>
<!--suppress JSSuspiciousNameCombination --> <!--suppress JSSuspiciousNameCombination -->
<script> <script>
import Player from "@/components/Player"; import Player from '@/components/Player'
export default { export default {
name: "Sidebar", name: 'Sidebar',
components: { components: {
player: Player, player: Player
}, },
props: ["slice", "offset"], data: function () {
data: function () { return {
return { imageSize: [-1, -1],
imageSize: [-1, -1], guideSizeX: 64,
guideSizeX: 64, guideSizeY: 64,
guideSizeY: 64, lastGuideSizeX: 64,
lastGuideSizeX: 64, lastGuideSizeY: 64,
lastGuideSizeY: 64, lockSize: true,
lockSize: true, cleanSize: false,
cleanSize: false, fps: 60
slices: 0, }
fps: 60, },
}; watch: {
}, guideSizeX (size) {
watch: { if (this.lockSize) this.guideSizeY = size
guideSizeX (size) { if (this.cleanSize) {
if (this.lockSize) this.guideSizeY = size; let tmpSize = parseInt(size)
if (this.cleanSize) { while (this.imageSize[0] % tmpSize !== 0 &&
let tmpSize = parseInt(size); tmpSize < this.imageSize[0] &&
while (this.imageSize[0] % tmpSize !== 0 && tmpSize > 2) {
tmpSize < this.imageSize[0] && if (this.lastGuideSizeX < size) {
tmpSize > 2) { tmpSize += 1
if (this.lastGuideSizeX < size) { } else {
tmpSize += 1; tmpSize -= 1
} else { }
tmpSize -= 1; }
if (tmpSize !== 2 && tmpSize !== this.imageSize[0]) {
this.guideSizeX = tmpSize
} }
} }
if (tmpSize !== 2 && tmpSize !== this.imageSize[0]) { this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
this.guideSizeX = tmpSize; this.lastGuideSizeX = this.guideSizeX
} this.lastGuideSizeY = this.guideSizeY
} },
this.$emit("params", { guideSizeY (size) {
type: "slice", if (this.lockSize) this.guideSizeX = size
x: this.guideSizeX, if (this.cleanSize) {
y: this.guideSizeY, let tmpSize = parseInt(size)
}); while (this.imageSize[1] % tmpSize !== 0 &&
this.lastGuideSizeX = this.guideSizeX; tmpSize < this.imageSize[1] &&
this.lastGuideSizeY = this.guideSizeY; tmpSize > 2) {
}, if (this.lastGuideSizeY < size) {
guideSizeY (size) { tmpSize += 1
if (this.lockSize) this.guideSizeX = size; } else {
if (this.cleanSize) { tmpSize -= 1
let tmpSize = parseInt(size); }
while (this.imageSize[1] % tmpSize !== 0 && }
tmpSize < this.imageSize[1] && if (tmpSize !== 2 && tmpSize !== this.imageSize[1]) {
tmpSize > 2) { this.guideSizeY = tmpSize
if (this.lastGuideSizeY < size) {
tmpSize += 1;
} else {
tmpSize -= 1;
} }
} }
if (tmpSize !== 2 && tmpSize !== this.imageSize[1]) { this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
this.guideSizeY = tmpSize; this.lastGuideSizeX = this.guideSizeX
this.lastGuideSizeY = this.guideSizeY
},
lockSize: function (locked) {
if (locked) {
if (this.guideSizeX < this.guideSizeY) this.guideSizeY = this.guideSizeX
else this.guideSizeX = this.guideSizeY
this.cleanSize = false
}
},
cleanSize: function (clean) {
if (clean) {
this.lockSize = false
} }
} }
this.$emit("params", {
type: "slice",
x: this.guideSizeX,
y: this.guideSizeY,
});
this.lastGuideSizeX = this.guideSizeX;
this.lastGuideSizeY = this.guideSizeY;
}, },
slice: function (size) { computed: {
this.lockSize = size[0] === size[1]; slices: function () {
this.guideSizeX = size[0]; if (this.imageSize[0] === -1 || this.imageSize[1] === -1) {
this.guideSizeY = size[1]; return '-'
},
lockSize: function (locked) {
if (locked) {
if (this.guideSizeX < this.guideSizeY) {
this.guideSizeY = this.guideSizeX;
} else { } else {
this.guideSizeX = this.guideSizeY; return Math.ceil(this.imageSize[0] / this.guideSizeX) *
Math.ceil(this.imageSize[1] / this.guideSizeY)
} }
this.cleanSize = false; },
} length: function () {
}, if (this.imageSize[0] === -1 || this.imageSize[1] === -1) {
cleanSize: function (clean) { return '-'
if (clean) { } else {
this.lockSize = false; let seconds = Math.round(this.slices / this.fps * 100) / 100
}
},
},
computed: {
length: function () {
if (this.imageSize[0] === -1 || this.imageSize[1] === -1) {
return "-";
} else {
let seconds = Math.round(this.slices / this.fps * 100) / 100;
let mins = Math.floor(seconds / 60); let mins = Math.floor(seconds / 60)
let secs = Math.round(seconds % 60); let secs = Math.round(seconds % 60)
return "~" + seconds + " seconds" + (mins > 0 ? (" (" + mins + "m " + secs + "s)") : ""); 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 () { methods: {
// eslint-disable-next-line no-undef loadImage (e) {
return process.env; let url = URL.createObjectURL(e.target.files[0])
}, this.$emit('loadImage', url)
},
methods: {
loadImage (e) {
let url = URL.createObjectURL(e.target.files[0]);
this.$emit("loadImage", url);
},
setParams (coords) {
switch (coords.type) {
case "size":
this.lockSize = false;
this.guideSizeX = coords.x;
this.guideSizeY = coords.y;
break;
} }
}, },
}, mounted: function () {
mounted: function () { this.$bus.$on('imageLoaded', (image) => {
this.$bus.$on("imageLoaded", (image) => { this.imageSize = [image.width, image.height]
this.imageSize = [image.width, image.height]; })
}); this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
this.$emit("guideSize", [this.guideSizeX, this.guideSizeY]); }
}, }
};
</script> </script>
<style scoped> <style scoped>
#sidebar-container { #sidebar-container {
display: flex; width: 100%;
flex-direction: column; height: 100%;
width: 100%;
height: 100%;
border-right: 1px solid black; border-right: 1px solid black;
} }
.sidebar-section { .spinBox {
padding: .5em; height: 1em;
border-bottom: 1px solid grey; }
}
.sidebar-section > div { .green {
padding: .25em; background-color: green;
} }
.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> </style>

View file

@ -1,16 +1,16 @@
export function getDimsFit (wOriginal, hOriginal, wContain, hContain) { export function getDimsFit (wOriginal, hOriginal, wContain, hContain) {
let origRatio = wOriginal / hOriginal; let origRatio = wOriginal / hOriginal
let containRatio = wContain / hContain; let containRatio = wContain / hContain
if (origRatio > containRatio) { if (origRatio > containRatio) {
return { return {
width: wContain, width: wContain,
height: wContain / origRatio, height: wContain / origRatio
}; }
} else { } else {
return { return {
width: origRatio * hContain, width: origRatio * hContain,
height: hContain, height: hContain
}; }
} }
} }

View file

@ -1,15 +1,15 @@
// The Vue build version to load with the `import` command // The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from "vue"; import Vue from 'vue'
import App from "./App"; import App from './App'
Vue.config.productionTip = false; Vue.config.productionTip = false
Vue.prototype.$bus = new Vue({}); Vue.prototype.$bus = new Vue({})
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
el: "#app", el: '#app',
template: "<App/>", template: '<App/>',
components: {App}, components: { App }
}); })