Compare commits

...

15 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
Tomáš Mládek 0c1656b87e Revert "npm audit fix --force"
This reverts commit bd4f110936fcf2bbbf6f476ba3ac1e48b1aa1921.
2019-11-02 12:24:54 +01:00
Tomáš Mládek 4a208f6078 zoom has steps and allows < 1 2019-11-02 12:24:54 +01:00
Tomáš Mládek 907f323583 loop & fullscreen
refactor playing/recoridng/pausing functions
2019-11-02 12:24:54 +01:00
Tomáš Mládek 1707379e78 add option for discarding partial frames 2019-11-02 12:24:54 +01:00
Tomáš Mládek 7ef1778cad refactor, add slice offset
(centralize slice size and offset in App.vue)
2019-11-02 12:24:54 +01:00
Tomáš Mládek 089f7d56c0 allow selection of size by mouse 2019-11-02 12:24:54 +01:00
Tomáš Mládek 676c8f7fe0 autoformat && eslint change 2019-11-02 12:24:54 +01:00
Tomáš Mládek 4c3b984b21 npm audit fix --force 2019-11-02 12:24:54 +01:00
Tomáš Mládek 01f3e54f58 remove logo 2019-11-02 12:24:54 +01:00
17 changed files with 7050 additions and 4621 deletions

View File

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

View File

@ -1,25 +1,35 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
"env": {
"browser": true,
"es6": true,
},
env: {
browser: true,
},
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
"extends": [
"eslint:recommended",
"plugin:vue/essential",
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly",
},
"parserOptions": {
"ecmaVersion": 2018,
"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-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
"autoprefixer": {},
},
};

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,7 +1,7 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
"use strict";
const merge = require("webpack-merge");
const prodEnv = require("./prod.env");
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
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
const path = require("path");
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
assetsSubDirectory: "static",
assetsPublicPath: "/",
proxyTable: {},
// 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
autoOpenBrowser: false,
errorOverlay: true,
@ -33,7 +33,7 @@ module.exports = {
*/
// 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,
// set this to false - it *may* help
@ -45,12 +45,12 @@ module.exports = {
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
index: path.resolve(__dirname, "../dist/index.html"),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/tools/slitscan',
assetsRoot: path.resolve(__dirname, "../dist"),
assetsSubDirectory: "static",
assetsPublicPath: "/tools/slitscan",
/**
* Source Maps
@ -58,19 +58,19 @@ module.exports = {
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
devtool: "#source-map",
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
productionGzipExtensions: ["js", "css"],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// 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,4 +1,8 @@
'use strict'
"use strict";
const package_json = require("../package.json")
module.exports = {
NODE_ENV: '"production"'
}
NODE_ENV: "\"production\"",
VERSION: JSON.stringify(package_json.version),
HOMEPAGE_URL: JSON.stringify(package_json.homepage)
};

10319
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",
@ -41,6 +39,7 @@
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^5.2.3",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,98 +1,129 @@
<template>
<div id="canvas-container">
<canvas ref="canvas" id="canvas"></canvas>
<canvas ref="canvas" id="canvas" @mousedown="onMouse" @mousemove="onMouse" oncontextmenu="return false;"></canvas>
</div>
</template>
<script>
import {getDimsFit} from '../helpers/image.js'
import {getDimsFit} from "../helpers/image.js";
export default {
name: 'Canvas',
data: function () {
return {
imageUrl: '',
image: null,
imageLoaded: false,
slice: [0, 0]
}
export default {
name: "Canvas",
data: function () {
return {
imageUrl: "",
image: null,
imageLoaded: false,
ratio: 1,
};
},
props: ["slice", "offset"],
computed: {},
watch: {
imageUrl: function (url) {
this.loadImage(url);
},
computed: {},
watch: {
imageUrl: function (url) {
this.loadImage(url)
},
slice: function () {
this.refresh()
}
slice: function () {
this.refresh();
},
mounted: function () {
window.addEventListener('resize', this.handleResize)
this.handleResize()
offset: function () {
this.refresh();
},
methods: {
loadImage: function (imageUrl) {
this.image = new Image()
this.image.onload = () => {
this.imageLoaded = true
this.$bus.$emit('imageLoaded', this.image)
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();
}
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()
}
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 () {
let canvasContainer = document.getElementById('canvas-container')
let canvas = document.getElementById('canvas')
canvas.width = canvasContainer.offsetWidth - 10
canvas.height = canvasContainer.offsetHeight - 10
this.refresh()
}
}
}
},
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();
},
onMouse: function (ev) {
if (ev.buttons === 0) {
return;
}
ev.preventDefault();
const type = {
1: "slice",
2: "offset",
}[ev.buttons];
if (type !== undefined) {
let x, y;
switch (type) {
case "slice":
x = Math.round((ev.x - ev.target.offsetLeft) / this.ratio - this.offset[0]);
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});
}
return false;
},
},
};
</script>
<style scoped>
#canvas-container {
width: 100%;
height: 100%;
margin: 0;
border: 0;
padding: 0;
}
#canvas-container {
width: 100%;
height: 100%;
margin: 0;
border: 0;
padding: 0;
}
</style>

View File

@ -1,26 +1,30 @@
<template>
<div id="player" :class="{fullscreen: fullscreen}">
<div id="player">
<div>
<label for="zoom">Zoom:</label>
<input type="number" min="1" :max="50" value="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" class="{'fullscreen-controls': fullscreen}">
<div>
<label for="recordMode">Record output</label>
<input type="checkbox" v-model="record" id="recordMode">
</div>
<div>
<input type="range" min="0" :max="frames - 1" value="0" class="slider"
v-model.number="position" :disabled=record>
v-model.number="position" :disabled=recording>
</div>
</div>
<div class="controls">
<div>
<button @click="playOnceOrPause">{{!this.playing ? "PLAY ONCE" : "STOP"}}</button>
</div>
<div>
<button @click="playPause">{{buttonLabel}}</button>
<button @click="recordSequence">RECORD</button>
</div>
<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">
@ -32,202 +36,270 @@
</label>
<input type="checkbox" v-model="sortDirection" id="sortDirection">
</div>
<div>
<label for="discard">Discard partial frames</label>
<input type="checkbox" v-model="discardPartial" id="discard">
</div>
</div>
</div>
</template>
<script>
import CCapture from 'ccapture.js'
import bowser from 'bowser'
export default {
name: 'player',
props: {
width: Number,
height: Number
export default {
name: "player",
props: {
width: 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);
},
data: function () {
return {
image: null,
playing: false,
zoom: 1,
fullscreen: false,
sortBySize: false,
sortDirection: false,
position: 0,
tmp_ctx: null,
animation_id: null,
record: false,
capturer: new CCapture({
format: 'webm',
verbose: true
})
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 = (this.discardPartial ? Math.floor : Math.ceil)((this.image.width - this.offset[0]) / this.width); // width_blocks
let x = this.offset[0] + (pos % wb) * this.width; // x offset
let w = x + this.width < this.image.width ? this.width : this.image.width - x; // frame width
let y = this.offset[1] + 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();
}
return sizes.map(function (el) {
return sequence[el.index];
});
} else {
return sequence;
}
},
computed: {
frames: function () {
if (this.image === null) return 0
return Math.ceil(this.image.width / this.width) *
Math.ceil(this.image.height / this.height)
},
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()
}
return sizes.map(function (el) {
return sequence[el.index]
})
} else {
return sequence
}
},
buttonLabel: function () {
if (!this.playing) {
if (this.record) {
return 'RECORD'
} else {
return 'PLAY'
}
} else {
return 'STOP'
},
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;
}
}
},
mounted: function () {
this.$bus.$on('imageLoaded', (image) => {
this.image = image
})
this.clear('red')
playOnceOrPause: function () {
if (!this.playing) {
this.looping = false;
this.recording = false;
this.position = 0;
this.play();
} else {
this.stop();
}
},
watch: {
canvas_width: function () {
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 {
this.animation_id = requestAnimationFrame(this.$render_advance);
}
this.position = 0;
}
},
$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);
},
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(() => {
this.clear('red')
}, 0)
},
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')
}
}
}
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
};
},
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>
<style scoped>
#player {
text-align: center;
}
#player {
text-align: center;
}
.fullscreen {
position: absolute;
top: 0;
left: 0;
background-color: rgba(1, 1, 1, .9);
width: 100%;
height: 100%;
}
#zoom {
width: 3em;
text-align: center;
}
.fullscreen-controls {
position: absolute;
bottom: 0;
}
.player-container {
padding: 2em 0 1em 0;
}
#zoom {
width: 3em;
}
.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,185 +1,247 @@
<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="1024" 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="1024" 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>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 class="footer"><a :href="env.HOMEPAGE_URL">Version: {{env.VERSION}}</a></div>
</div>
</template>
<!--suppress JSSuspiciousNameCombination -->
<script>
import Player from '@/components/Player'
import Player from "@/components/Player";
export default {
name: 'Sidebar',
components: {
player: Player
},
data: function () {
return {
imageSize: [-1, -1],
guideSizeX: 64,
guideSizeY: 64,
lastGuideSizeX: 64,
lastGuideSizeY: 64,
lockSize: true,
cleanSize: false,
fps: 60
}
},
watch: {
guideSizeX (size) {
if (this.lockSize) this.guideSizeY = size
if (this.cleanSize) {
let tmpSize = parseInt(size)
while (this.imageSize[0] % tmpSize !== 0 &&
tmpSize < this.imageSize[0] &&
tmpSize > 2) {
if (this.lastGuideSizeX < size) {
tmpSize += 1
} else {
tmpSize -= 1
}
}
if (tmpSize !== 2 && tmpSize !== this.imageSize[0]) {
this.guideSizeX = tmpSize
export default {
name: "Sidebar",
components: {
player: Player,
},
props: ["slice", "offset"],
data: function () {
return {
imageSize: [-1, -1],
guideSizeX: 64,
guideSizeY: 64,
lastGuideSizeX: 64,
lastGuideSizeY: 64,
lockSize: true,
cleanSize: false,
slices: 0,
fps: 60,
};
},
watch: {
guideSizeX (size) {
if (this.lockSize) this.guideSizeY = size;
if (this.cleanSize) {
let tmpSize = parseInt(size);
while (this.imageSize[0] % tmpSize !== 0 &&
tmpSize < this.imageSize[0] &&
tmpSize > 2) {
if (this.lastGuideSizeX < size) {
tmpSize += 1;
} else {
tmpSize -= 1;
}
}
this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
this.lastGuideSizeX = this.guideSizeX
this.lastGuideSizeY = this.guideSizeY
},
guideSizeY (size) {
if (this.lockSize) this.guideSizeX = size
if (this.cleanSize) {
let tmpSize = parseInt(size)
while (this.imageSize[1] % tmpSize !== 0 &&
tmpSize < this.imageSize[1] &&
tmpSize > 2) {
if (this.lastGuideSizeY < size) {
tmpSize += 1
} else {
tmpSize -= 1
}
}
if (tmpSize !== 2 && tmpSize !== this.imageSize[1]) {
this.guideSizeY = tmpSize
}
}
this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
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
if (tmpSize !== 2 && tmpSize !== this.imageSize[0]) {
this.guideSizeX = tmpSize;
}
}
this.$emit("params", {
type: "slice",
x: this.guideSizeX,
y: this.guideSizeY,
});
this.lastGuideSizeX = this.guideSizeX;
this.lastGuideSizeY = this.guideSizeY;
},
computed: {
slices: function () {
if (this.imageSize[0] === -1 || this.imageSize[1] === -1) {
return '-'
guideSizeY (size) {
if (this.lockSize) this.guideSizeX = size;
if (this.cleanSize) {
let tmpSize = parseInt(size);
while (this.imageSize[1] % tmpSize !== 0 &&
tmpSize < this.imageSize[1] &&
tmpSize > 2) {
if (this.lastGuideSizeY < size) {
tmpSize += 1;
} else {
tmpSize -= 1;
}
}
if (tmpSize !== 2 && tmpSize !== this.imageSize[1]) {
this.guideSizeY = tmpSize;
}
}
this.$emit("params", {
type: "slice",
x: this.guideSizeX,
y: this.guideSizeY,
});
this.lastGuideSizeX = this.guideSizeX;
this.lastGuideSizeY = this.guideSizeY;
},
slice: function (size) {
this.lockSize = size[0] === size[1];
this.guideSizeX = size[0];
this.guideSizeY = size[1];
},
lockSize: function (locked) {
if (locked) {
if (this.guideSizeX < this.guideSizeY) {
this.guideSizeY = this.guideSizeX;
} else {
return Math.ceil(this.imageSize[0] / this.guideSizeX) *
Math.ceil(this.imageSize[1] / this.guideSizeY)
this.guideSizeX = this.guideSizeY;
}
},
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 secs = Math.round(seconds % 60)
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
this.cleanSize = false;
}
},
methods: {
loadImage (e) {
let url = URL.createObjectURL(e.target.files[0])
this.$emit('loadImage', url)
cleanSize: function (clean) {
if (clean) {
this.lockSize = false;
}
},
mounted: function () {
this.$bus.$on('imageLoaded', (image) => {
this.imageSize = [image.width, image.height]
})
this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
}
}
},
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 secs = Math.round(seconds % 60);
return "~" + seconds + " seconds" + (mins > 0 ? (" (" + mins + "m " + secs + "s)") : "");
}
},
env: function () {
// eslint-disable-next-line no-undef
return process.env;
},
},
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 () {
this.$bus.$on("imageLoaded", (image) => {
this.imageSize = [image.width, image.height];
});
this.$emit("guideSize", [this.guideSizeX, this.guideSizeY]);
},
};
</script>
<style scoped>
#sidebar-container {
width: 100%;
height: 100%;
#sidebar-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
border-right: 1px solid black;
}
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>

View File

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

View File

@ -1,15 +1,15 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import Vue from "vue";
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 */
new Vue({
el: '#app',
template: '<App/>',
components: { App }
})
el: "#app",
template: "<App/>",
components: {App},
});