Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
c637ebd144 | |||
9ee3bff615 | |||
a4f6194ea2 | |||
f9547511d8 | |||
3e5b2ca502 | |||
062fbf84df | |||
0c1656b87e | |||
4a208f6078 | |||
907f323583 | |||
1707379e78 | |||
7ef1778cad | |||
089f7d56c0 | |||
676c8f7fe0 | |||
4c3b984b21 | |||
01f3e54f58 |
17 changed files with 7050 additions and 4621 deletions
22
.babelrc
22
.babelrc
|
@ -1,12 +1,22 @@
|
||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
["env", {
|
[
|
||||||
"modules": false,
|
"env",
|
||||||
"targets": {
|
{
|
||||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
"modules": false,
|
||||||
|
"targets": {
|
||||||
|
"browsers": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie <= 8"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}],
|
],
|
||||||
"stage-2"
|
"stage-2"
|
||||||
],
|
],
|
||||||
"plugins": ["transform-vue-jsx", "transform-runtime"]
|
"plugins": [
|
||||||
|
"transform-vue-jsx",
|
||||||
|
"transform-runtime"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
54
.eslintrc.js
54
.eslintrc.js
|
@ -1,25 +1,35 @@
|
||||||
// https://eslint.org/docs/user-guide/configuring
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
"env": {
|
||||||
parser: 'babel-eslint',
|
"browser": true,
|
||||||
parserOptions: {
|
"es6": true,
|
||||||
sourceType: 'module'
|
|
||||||
},
|
},
|
||||||
env: {
|
"extends": [
|
||||||
browser: true,
|
"eslint:recommended",
|
||||||
},
|
"plugin:vue/essential",
|
||||||
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
|
|
||||||
extends: 'standard',
|
|
||||||
// required to lint *.vue files
|
|
||||||
plugins: [
|
|
||||||
'html'
|
|
||||||
],
|
],
|
||||||
// add your custom rules here
|
"globals": {
|
||||||
rules: {
|
"Atomics": "readonly",
|
||||||
// allow async-await
|
"SharedArrayBuffer": "readonly",
|
||||||
'generator-star-spacing': 'off',
|
},
|
||||||
// allow debugger during development
|
"parserOptions": {
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
"ecmaVersion": 2018,
|
||||||
}
|
"sourceType": "module",
|
||||||
}
|
},
|
||||||
|
"plugins": [
|
||||||
|
"vue",
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix",
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"double",
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -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": {},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
37
CHANGELOG.md
Normal file
37
CHANGELOG.md
Normal 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
21
LICENSE
Normal 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.
|
|
@ -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\"",
|
||||||
})
|
});
|
||||||
|
|
|
@ -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,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
'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)
|
||||||
|
};
|
||||||
|
|
10319
package-lock.json
generated
10319
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "slitscan_vue",
|
"name": "slitscan_vue",
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"description": "A Vue.js project",
|
"description": "A video experiment for converting static images to strobe sequences.",
|
||||||
"author": "Tomáš Mládek <tmladek@inventati.org>",
|
"homepage": "https://gitlab.com/tmladek/slitscan",
|
||||||
|
"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",
|
||||||
|
@ -11,12 +12,9 @@
|
||||||
"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",
|
||||||
|
@ -41,6 +39,7 @@
|
||||||
"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",
|
||||||
|
|
110
src/App.vue
110
src/App.vue
|
@ -1,62 +1,92 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="menu-wrap">
|
<div id="menu-wrap">
|
||||||
<Sidebar ref="sidebar" @loadImage="loadImage" @guideSize="guideSize"/>
|
<Sidebar ref="sidebar" @params="setParams" :slice="slice" :offset="offset" @loadImage="loadImage"/>
|
||||||
</div>
|
</div>
|
||||||
<div id="canvas-wrap">
|
<div id="canvas-wrap">
|
||||||
<Canvas ref="canvas"/>
|
<Canvas ref="canvas" @params="setParams" :slice="slice" :offset="offset"/>
|
||||||
</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;
|
||||||
},
|
},
|
||||||
methods: {
|
setParams: function (coords) {
|
||||||
loadImage: function (url) {
|
switch (coords.type) {
|
||||||
this.$refs.canvas.imageUrl = url
|
case "slice":
|
||||||
},
|
this.slice = [Math.max(2, coords.x), Math.max(2, coords.y)];
|
||||||
guideSize: function (size) {
|
break;
|
||||||
this.$refs.canvas.slice = size
|
case "offset":
|
||||||
|
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 {
|
input, button {
|
||||||
font-size: .95em;
|
font-family: Consolas, Inconsolata, monospace, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu-wrap, #canvas-wrap {
|
input {
|
||||||
display: inline-block;
|
font-size: .95em;
|
||||||
}
|
padding: 4px 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
#menu-wrap {
|
input[type="file"] {
|
||||||
width: 20%;
|
border: none;
|
||||||
height: 100%;
|
}
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas-wrap {
|
button {
|
||||||
width: 80%;
|
background: white;
|
||||||
height: 100%;
|
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>
|
</style>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,98 +1,129 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="canvas-container">
|
<div id="canvas-container">
|
||||||
<canvas ref="canvas" id="canvas"></canvas>
|
<canvas ref="canvas" id="canvas" @mousedown="onMouse" @mousemove="onMouse" oncontextmenu="return false;"></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,
|
||||||
slice: [0, 0]
|
ratio: 1,
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
props: ["slice", "offset"],
|
||||||
|
computed: {},
|
||||||
|
watch: {
|
||||||
|
imageUrl: function (url) {
|
||||||
|
this.loadImage(url);
|
||||||
},
|
},
|
||||||
computed: {},
|
slice: function () {
|
||||||
watch: {
|
this.refresh();
|
||||||
imageUrl: function (url) {
|
|
||||||
this.loadImage(url)
|
|
||||||
},
|
|
||||||
slice: function () {
|
|
||||||
this.refresh()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted: function () {
|
offset: function () {
|
||||||
window.addEventListener('resize', this.handleResize)
|
this.refresh();
|
||||||
this.handleResize()
|
|
||||||
},
|
},
|
||||||
methods: {
|
},
|
||||||
loadImage: function (imageUrl) {
|
mounted: function () {
|
||||||
this.image = new Image()
|
window.addEventListener("resize", this.handleResize);
|
||||||
this.image.onload = () => {
|
this.handleResize();
|
||||||
this.imageLoaded = true
|
},
|
||||||
this.$bus.$emit('imageLoaded', this.image)
|
methods: {
|
||||||
this.refresh()
|
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
|
for (let y = this.offset[1] * this.ratio; y < dims.height; y += sliceY) {
|
||||||
},
|
ctx.beginPath();
|
||||||
refresh: function () {
|
ctx.moveTo(this.offset[0] * this.ratio, y);
|
||||||
if (!this.imageLoaded) return
|
ctx.lineTo(dims.width, y);
|
||||||
let image = this.image
|
ctx.strokeStyle = "red";
|
||||||
let canvas = this.$refs.canvas
|
ctx.stroke();
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
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>
|
</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>
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="player" :class="{fullscreen: fullscreen}">
|
<div id="player">
|
||||||
<div>
|
<div>
|
||||||
<label for="zoom">Zoom:</label>
|
<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>
|
||||||
<div id="player-canvas-container">
|
<div class="player-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=record>
|
v-model.number="position" :disabled=recording>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div>
|
||||||
|
<button @click="playOnceOrPause">{{!this.playing ? "PLAY ONCE" : "STOP"}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button @click="playPause">{{buttonLabel}}</button>
|
<button @click="recordSequence">RECORD</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">
|
||||||
|
@ -32,202 +36,270 @@
|
||||||
</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);
|
||||||
},
|
},
|
||||||
data: function () {
|
canvas_height: function () {
|
||||||
return {
|
return this.height * this.zoom;
|
||||||
image: null,
|
},
|
||||||
playing: false,
|
canvas_width: function () {
|
||||||
zoom: 1,
|
return this.width * this.zoom;
|
||||||
fullscreen: false,
|
},
|
||||||
sortBySize: false,
|
frame_sequence: function () {
|
||||||
sortDirection: false,
|
if (this.image === null) return [];
|
||||||
position: 0,
|
|
||||||
tmp_ctx: null,
|
let sequence = [];
|
||||||
animation_id: null,
|
for (let pos = 0; pos < this.frames; pos++) {
|
||||||
record: false,
|
let wb = (this.discardPartial ? Math.floor : Math.ceil)((this.image.width - this.offset[0]) / this.width); // width_blocks
|
||||||
capturer: new CCapture({
|
let x = this.offset[0] + (pos % wb) * this.width; // x offset
|
||||||
format: 'webm',
|
let w = x + this.width < this.image.width ? this.width : this.image.width - x; // frame width
|
||||||
verbose: true
|
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 () {
|
mounted: function () {
|
||||||
if (this.image === null) return 0
|
this.$bus.$on("imageLoaded", (image) => {
|
||||||
return Math.ceil(this.image.width / this.width) *
|
this.image = image;
|
||||||
Math.ceil(this.image.height / this.height)
|
});
|
||||||
},
|
this.clear("red");
|
||||||
canvas_height: function () {
|
},
|
||||||
return this.height * this.zoom
|
watch: {
|
||||||
},
|
canvas_width: function () {
|
||||||
canvas_width: function () {
|
setTimeout(() => {
|
||||||
return this.width * this.zoom
|
this.clear("red");
|
||||||
},
|
}, 0);
|
||||||
frame_sequence: function () {
|
},
|
||||||
if (this.image === null) return []
|
canvas_height: function () {
|
||||||
|
setTimeout(() => {
|
||||||
let sequence = []
|
this.clear("red");
|
||||||
for (let pos = 0; pos < this.frames; pos++) {
|
}, 0);
|
||||||
let wb = Math.ceil(this.image.width / this.width) // width_blocks
|
},
|
||||||
let x = (pos % wb) * this.width // x offset
|
frames: function () {
|
||||||
let w = x + this.width < this.image.width ? this.width : this.image.width - x // frame width
|
this.$emit("frames", this.frames);
|
||||||
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
|
position: function () {
|
||||||
sequence.push([x, y, w, h])
|
if (this.playing || this.image === null) return;
|
||||||
}
|
this.tmp_ctx = this.$refs.canvas.getContext("2d");
|
||||||
if (this.sortBySize) {
|
this.$render();
|
||||||
let tmpCanvas = document.createElement('canvas')
|
},
|
||||||
let ctx = tmpCanvas.getContext('2d')
|
looping: function () {
|
||||||
let sizes = sequence.map((el, i) => {
|
this.recording = false;
|
||||||
if (tmpCanvas.width !== el[2] ||
|
},
|
||||||
tmpCanvas.height !== el[3]) {
|
recording: function () {
|
||||||
tmpCanvas.width = el[2]
|
if (this.recording) {
|
||||||
tmpCanvas.height = el[3]
|
this.looping = false;
|
||||||
}
|
this.position = 0;
|
||||||
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}
|
},
|
||||||
})
|
methods: {
|
||||||
|
play: function () {
|
||||||
sizes.sort(function (a, b) {
|
if (this.frames === 0) {
|
||||||
return a.length - b.length
|
return;
|
||||||
})
|
}
|
||||||
|
if (!this.playing) {
|
||||||
if (!this.sortDirection) {
|
// eslint-disable-next-line no-console
|
||||||
sizes.reverse()
|
console.log("STARTED");
|
||||||
}
|
this.tmp_ctx = this.$refs.canvas.getContext("2d");
|
||||||
|
this.$render_advance();
|
||||||
return sizes.map(function (el) {
|
this.playing = true;
|
||||||
return sequence[el.index]
|
}
|
||||||
})
|
},
|
||||||
} else {
|
stop: function () {
|
||||||
return sequence
|
if (this.playing) {
|
||||||
}
|
// eslint-disable-next-line no-console
|
||||||
},
|
console.log("STOPPED");
|
||||||
buttonLabel: function () {
|
cancelAnimationFrame(this.animation_id);
|
||||||
if (!this.playing) {
|
this.playing = false;
|
||||||
if (this.record) {
|
if (this.recording && this.recorder) {
|
||||||
return 'RECORD'
|
this.recorder.stop();
|
||||||
} else {
|
this.recording = false;
|
||||||
return 'PLAY'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return 'STOP'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
playOnceOrPause: function () {
|
||||||
this.$bus.$on('imageLoaded', (image) => {
|
if (!this.playing) {
|
||||||
this.image = image
|
this.looping = false;
|
||||||
})
|
this.recording = false;
|
||||||
this.clear('red')
|
this.position = 0;
|
||||||
|
this.play();
|
||||||
|
} else {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
recordSequence: function () {
|
||||||
canvas_width: 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(() => {
|
setTimeout(() => {
|
||||||
this.clear('red')
|
document.body.removeChild(a);
|
||||||
}, 0)
|
window.URL.revokeObjectURL(url);
|
||||||
},
|
}, 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen {
|
#zoom {
|
||||||
position: absolute;
|
width: 3em;
|
||||||
top: 0;
|
text-align: center;
|
||||||
left: 0;
|
}
|
||||||
background-color: rgba(1, 1, 1, .9);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-controls {
|
.player-container {
|
||||||
position: absolute;
|
padding: 2em 0 1em 0;
|
||||||
bottom: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#zoom {
|
.controls, .options {
|
||||||
width: 3em;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
width: 12em;
|
||||||
|
margin: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,185 +1,247 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="sidebar-container">
|
<div id="sidebar-container">
|
||||||
<div>
|
<div class="sidebar-section">
|
||||||
<input type="file" id="file" @change="loadImage($event)">
|
<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>
|
||||||
<div>
|
<div class="sidebar-section">
|
||||||
<label>Image size: {{imageSize[0]}} x {{imageSize[1]}}</label>
|
<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>
|
</div>
|
||||||
<hr>
|
<div class="sidebar-section">
|
||||||
<div>
|
<div class="section-formlike">
|
||||||
<label for="slice_x">Slice X: </label>
|
<label>Offset: x={{offset[0]}}, y={{offset[1]}}</label>
|
||||||
<input class="spinBox" id="slice_x" v-model.number="guideSizeX" type="number" min="2" max="1024" step="1"
|
<button @click="$emit('params', {type: 'offset', x: 0, y: 0})">RESET</button>
|
||||||
value="64">
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="sidebar-section">
|
||||||
<label for="slice_x">No X remainder: {{noXremainder}}</label>
|
<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>
|
||||||
<div>
|
<div class="sidebar-section player-section">
|
||||||
<label for="slice_y">Slice Y: </label>
|
<player class="player" :width="guideSizeX" :height="guideSizeY" :offset="offset" @frames="(n)=>{slices=n}"/>
|
||||||
<input class="spinBox" id="slice_y" v-model.number="guideSizeY" type="number" min="2" max="1024" step="1"
|
|
||||||
value="64">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="footer"><a :href="env.HOMEPAGE_URL">Version: {{env.VERSION}}</a></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,
|
||||||
},
|
},
|
||||||
data: function () {
|
props: ["slice", "offset"],
|
||||||
return {
|
data: function () {
|
||||||
imageSize: [-1, -1],
|
return {
|
||||||
guideSizeX: 64,
|
imageSize: [-1, -1],
|
||||||
guideSizeY: 64,
|
guideSizeX: 64,
|
||||||
lastGuideSizeX: 64,
|
guideSizeY: 64,
|
||||||
lastGuideSizeY: 64,
|
lastGuideSizeX: 64,
|
||||||
lockSize: true,
|
lastGuideSizeY: 64,
|
||||||
cleanSize: false,
|
lockSize: true,
|
||||||
fps: 60
|
cleanSize: false,
|
||||||
}
|
slices: 0,
|
||||||
},
|
fps: 60,
|
||||||
watch: {
|
};
|
||||||
guideSizeX (size) {
|
},
|
||||||
if (this.lockSize) this.guideSizeY = size
|
watch: {
|
||||||
if (this.cleanSize) {
|
guideSizeX (size) {
|
||||||
let tmpSize = parseInt(size)
|
if (this.lockSize) this.guideSizeY = size;
|
||||||
while (this.imageSize[0] % tmpSize !== 0 &&
|
if (this.cleanSize) {
|
||||||
tmpSize < this.imageSize[0] &&
|
let tmpSize = parseInt(size);
|
||||||
tmpSize > 2) {
|
while (this.imageSize[0] % tmpSize !== 0 &&
|
||||||
if (this.lastGuideSizeX < size) {
|
tmpSize < this.imageSize[0] &&
|
||||||
tmpSize += 1
|
tmpSize > 2) {
|
||||||
} else {
|
if (this.lastGuideSizeX < size) {
|
||||||
tmpSize -= 1
|
tmpSize += 1;
|
||||||
}
|
} else {
|
||||||
}
|
tmpSize -= 1;
|
||||||
if (tmpSize !== 2 && tmpSize !== this.imageSize[0]) {
|
|
||||||
this.guideSizeX = tmpSize
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
|
if (tmpSize !== 2 && tmpSize !== this.imageSize[0]) {
|
||||||
this.lastGuideSizeX = this.guideSizeX
|
this.guideSizeX = tmpSize;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.$emit("params", {
|
||||||
|
type: "slice",
|
||||||
|
x: this.guideSizeX,
|
||||||
|
y: this.guideSizeY,
|
||||||
|
});
|
||||||
|
this.lastGuideSizeX = this.guideSizeX;
|
||||||
|
this.lastGuideSizeY = this.guideSizeY;
|
||||||
},
|
},
|
||||||
computed: {
|
guideSizeY (size) {
|
||||||
slices: function () {
|
if (this.lockSize) this.guideSizeX = size;
|
||||||
if (this.imageSize[0] === -1 || this.imageSize[1] === -1) {
|
if (this.cleanSize) {
|
||||||
return '-'
|
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 {
|
} else {
|
||||||
return Math.ceil(this.imageSize[0] / this.guideSizeX) *
|
this.guideSizeX = this.guideSizeY;
|
||||||
Math.ceil(this.imageSize[1] / this.guideSizeY)
|
|
||||||
}
|
}
|
||||||
},
|
this.cleanSize = false;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
cleanSize: function (clean) {
|
||||||
loadImage (e) {
|
if (clean) {
|
||||||
let url = URL.createObjectURL(e.target.files[0])
|
this.lockSize = false;
|
||||||
this.$emit('loadImage', url)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
},
|
||||||
this.$bus.$on('imageLoaded', (image) => {
|
computed: {
|
||||||
this.imageSize = [image.width, image.height]
|
length: function () {
|
||||||
})
|
if (this.imageSize[0] === -1 || this.imageSize[1] === -1) {
|
||||||
this.$emit('guideSize', [this.guideSizeX, this.guideSizeY])
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#sidebar-container {
|
#sidebar-container {
|
||||||
width: 100%;
|
display: flex;
|
||||||
height: 100%;
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
border-right: 1px solid black;
|
border-right: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinBox {
|
.sidebar-section {
|
||||||
height: 1em;
|
padding: .5em;
|
||||||
}
|
border-bottom: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
.green {
|
.sidebar-section > div {
|
||||||
background-color: green;
|
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>
|
</style>
|
||||||
|
|
|
@ -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,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
src/main.js
16
src/main.js
|
@ -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},
|
||||||
})
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue