diff --git a/app/js/fragmentShader.glsl b/app/js/fragmentShader.glsl index a75f712..b58682d 100644 --- a/app/js/fragmentShader.glsl +++ b/app/js/fragmentShader.glsl @@ -9,6 +9,8 @@ varying vec2 v_texCoord; ${uniforms} +${functions} + void main() { vec2 pixel = vec2(1.0, 1.0) / u_textureSize; // Look up a color from the texture. diff --git a/app/package.json b/app/package.json index 4c02080..246306e 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "FilterJS", "description": "Generate textures with JS power!", - "version": "1.0.0", + "version": "1.1.0", "main": "index.html", "dependencies": { "fs-extra": "^6.0.0", diff --git a/app/sampleFilters/BilateralFilter.fjs b/app/sampleFilters/BilateralFilter.fjs new file mode 100644 index 0000000..2f857d7 --- /dev/null +++ b/app/sampleFilters/BilateralFilter.fjs @@ -0,0 +1,77 @@ +{ + "name": "New filter", + "graph": [ + { + "id": 0, + "inputLinks": [ + { + "inputKey": "image", + "outBlock": 2, + "outKey": "output" + } + ], + "x": 200, + "y": -40, + "template": "outputImage", + "tagValues": {} + }, + { + "id": 1, + "inputLinks": [], + "x": -540, + "y": -40, + "template": "inputImage", + "tagValues": {} + }, + { + "id": 2, + "inputLinks": [ + { + "inputKey": "input", + "outBlock": 1, + "outKey": "image" + }, + { + "inputKey": "threshold", + "outBlock": 4, + "outKey": "number" + }, + { + "inputKey": "size", + "outBlock": 3, + "outKey": "number" + } + ], + "x": -240, + "y": -40, + "template": "bilateralFilter", + "tagValues": {} + }, + { + "id": 3, + "inputLinks": [], + "x": -540, + "y": 100, + "template": "numberInput", + "tagValues": { + "number": 0.01 + } + }, + { + "id": 4, + "inputLinks": [], + "x": -540, + "y": 260, + "template": "numberInput", + "tagValues": { + "number": 0.1 + } + } + ], + "view": { + "x": -23, + "y": -60 + }, + "lastId": 5, + "seed": 0 +} \ No newline at end of file diff --git a/app/sampleFilters/Median.fjs b/app/sampleFilters/Median.fjs new file mode 100644 index 0000000..80110ee --- /dev/null +++ b/app/sampleFilters/Median.fjs @@ -0,0 +1,108 @@ +{ + "name": "New filter", + "graph": [ + { + "id": 0, + "inputLinks": [ + { + "inputKey": "image", + "outBlock": 4, + "outKey": "canvas" + } + ], + "x": 340, + "y": -40, + "template": "outputImage", + "tagValues": {} + }, + { + "id": 1, + "inputLinks": [], + "x": -600, + "y": -40, + "template": "inputImage", + "tagValues": {} + }, + { + "id": 2, + "inputLinks": [ + { + "inputKey": "input", + "outBlock": 3, + "outKey": "pixels" + }, + { + "inputKey": "size", + "outBlock": 5, + "outKey": "number" + }, + { + "inputKey": "percentile", + "outBlock": 6, + "outKey": "number" + } + ], + "x": -180, + "y": -40, + "template": "median", + "tagValues": { + "mode": "circular", + "quality": true + } + }, + { + "id": 3, + "inputLinks": [ + { + "inputKey": "canvas", + "outBlock": 1, + "outKey": "image" + } + ], + "x": -380, + "y": -40, + "template": "canvasToPixels", + "tagValues": {} + }, + { + "id": 4, + "inputLinks": [ + { + "inputKey": "pixels", + "outBlock": 2, + "outKey": "output" + } + ], + "x": 120, + "y": -40, + "template": "pixelsToCanvas", + "tagValues": {} + }, + { + "id": 5, + "inputLinks": [], + "x": -540, + "y": 100, + "template": "numberInput", + "tagValues": { + "number": 0.01 + } + }, + { + "id": 6, + "inputLinks": [], + "x": -540, + "y": 280, + "template": "numberInput", + "tagValues": { + "number": 0.5 + } + } + ], + "view": { + "x": -41, + "y": -109 + }, + "lastId": 7, + "seed": 0 +} \ No newline at end of file diff --git a/app/sampleFilters/SkinSoftener.fjs b/app/sampleFilters/SkinSoftener.fjs new file mode 100644 index 0000000..bc15ce1 --- /dev/null +++ b/app/sampleFilters/SkinSoftener.fjs @@ -0,0 +1,214 @@ +{ + "name": "New filter", + "graph": [ + { + "id": 0, + "inputLinks": [ + { + "inputKey": "image", + "outBlock": 10, + "outKey": "result" + } + ], + "x": 960, + "y": -40, + "template": "outputImage", + "tagValues": {} + }, + { + "id": 1, + "inputLinks": [], + "x": -780, + "y": 200, + "template": "inputImage", + "tagValues": {} + }, + { + "id": 3, + "inputLinks": [ + { + "inputKey": "input", + "outBlock": 4, + "outKey": "pixels" + } + ], + "x": -220, + "y": 320, + "template": "getSkinColors", + "tagValues": { + "method": "J. Kovac (uniform daylight)" + } + }, + { + "id": 4, + "inputLinks": [ + { + "inputKey": "canvas", + "outBlock": 1, + "outKey": "image" + } + ], + "x": -580, + "y": 320, + "template": "canvasToPixels", + "tagValues": {} + }, + { + "id": 8, + "inputLinks": [], + "x": -800, + "y": -160, + "template": "numberInput", + "tagValues": { + "number": 0.01 + } + }, + { + "id": 9, + "inputLinks": [ + { + "inputKey": "a", + "outBlock": 3, + "outKey": "output" + }, + { + "inputKey": "r", + "outBlock": 12, + "outKey": "channel" + }, + { + "inputKey": "g", + "outBlock": 12, + "outKey": "g" + }, + { + "inputKey": "b", + "outBlock": 12, + "outKey": "b" + } + ], + "x": 160, + "y": 0, + "template": "combineChannels", + "tagValues": {} + }, + { + "id": 10, + "inputLinks": [ + { + "inputKey": "opacity", + "outBlock": 11, + "outKey": "number" + }, + { + "inputKey": "bottom", + "outBlock": 1, + "outKey": "image" + }, + { + "inputKey": "top", + "outBlock": 13, + "outKey": "canvas" + } + ], + "x": 640, + "y": -40, + "template": "blend", + "tagValues": { + "mode": "source-over" + } + }, + { + "id": 11, + "inputLinks": [], + "x": 380, + "y": 220, + "template": "numberInput", + "tagValues": { + "number": 0.6 + } + }, + { + "id": 12, + "inputLinks": [ + { + "inputKey": "pixels", + "outBlock": 16, + "outKey": "pixels" + } + ], + "x": -60, + "y": 0, + "template": "splitChannels", + "tagValues": {} + }, + { + "id": 13, + "inputLinks": [ + { + "inputKey": "pixels", + "outBlock": 9, + "outKey": "output" + } + ], + "x": 420, + "y": 0, + "template": "pixelsToCanvas", + "tagValues": {} + }, + { + "id": 14, + "inputLinks": [ + { + "inputKey": "size", + "outBlock": 8, + "outKey": "number" + }, + { + "inputKey": "threshold", + "outBlock": 15, + "outKey": "number" + }, + { + "inputKey": "input", + "outBlock": 1, + "outKey": "image" + } + ], + "x": -520, + "y": -140, + "template": "bilateralFilter", + "tagValues": {} + }, + { + "id": 15, + "inputLinks": [], + "x": -800, + "y": 20, + "template": "numberInput", + "tagValues": { + "number": 0.1 + } + }, + { + "id": 16, + "inputLinks": [ + { + "inputKey": "canvas", + "outBlock": 14, + "outKey": "output" + } + ], + "x": -240, + "y": -140, + "template": "canvasToPixels", + "tagValues": {} + } + ], + "view": { + "x": -5, + "y": -104 + }, + "lastId": 17, + "seed": 0 +} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 552b5c0..0fce2bc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -22,7 +22,7 @@ const notifier = require('node-notifier'), stylint = require('gulp-stylint'); var tsProject = typescript.createProject('./tsconfig.json'); -const nwVersion = '0.35.5'; +const nwVersion = '0.37.3'; const makeErrorObj = (title, err) => ({ title, @@ -191,6 +191,7 @@ const release = gulp.series([build, lint, done => { files: './app/**', platforms: ['osx64', 'win32', 'win64', 'linux32', 'linux64'], version: nwVersion, + zip: false, flavor: 'normal', buildType: 'versioned' }); @@ -225,7 +226,7 @@ const spawnise = (app, attrs) => new Promise((resolve, reject) => { }); const deploy = done => { var pack = require('./app/package.json'); - spawnise('./butler', ['push', `./build/ctjs - v${pack.version}/linux32`, 'comigo/filterjs:linux32', '--userversion', pack.version]) + spawnise('./butler', ['push', `./build/FilterJS - v${pack.version}/linux32`, 'comigo/filterjs:linux32', '--userversion', pack.version]) .then(() => spawnise('./butler', ['push', `./build/FilterJS - v${pack.version}/linux64`, 'comigo/filterjs:linux64', '--userversion', pack.version])) .then(() => spawnise('./butler', ['push', `./build/FilterJS - v${pack.version}/osx64`, 'comigo/filterjs:osx64', '--userversion', pack.version])) .then(() => spawnise('./butler', ['push', `./build/FilterJS - v${pack.version}/win32`, 'comigo/filterjs:win32', '--userversion', pack.version])) diff --git a/package-lock.json b/package-lock.json index 55942fc..1910f2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6396,7 +6396,6 @@ "semver": "^5.5.0", "simple-glob": "~0.2.0", "tar-fs": "^1.13.0", - "temp": "github:adam-lynch/node-temp#279c1350cb7e4f02515d91da9e35d39a40774016", "thenify": "^3.3.0", "update-notifier": "^2.4.0", "winresourcer": "^0.9.0" @@ -6406,6 +6405,20 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "temp": { + "version": "github:adam-lynch/node-temp#279c1350cb7e4f02515d91da9e35d39a40774016", + "from": "github:adam-lynch/node-temp#279c1350cb7e4f02515d91da9e35d39a40774016", + "requires": { + "rimraf": "~2.2.6" + }, + "dependencies": { + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" + } + } } } }, @@ -8355,20 +8368,6 @@ } } }, - "temp": { - "version": "github:adam-lynch/node-temp#279c1350cb7e4f02515d91da9e35d39a40774016", - "from": "github:adam-lynch/node-temp#remove_tmpdir_dep", - "requires": { - "rimraf": "~2.2.6" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" - } - } - }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", diff --git a/screenshot.png b/screenshot.png index dde0600..cfa299a 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/js/blocks/channelMath.ts b/src/js/blocks/channelMath.ts index 58a24d8..36d52c9 100644 --- a/src/js/blocks/channelMath.ts +++ b/src/js/blocks/channelMath.ts @@ -29,7 +29,8 @@ const channelSum = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(inputs.a.data[x + y*w] + inputs.b.data[x + y*w]); + const ind = x + y*w; + result.data[ind] = inputs.a.data[ind] + inputs.b.data[ind]; } } resolve({ @@ -67,7 +68,8 @@ const channelAddNumber = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(inputs.a.data[x + y*w] + inputs.b); + const ind = x + y*w; + result.data[ind] = inputs.a.data[ind] + inputs.b; } } resolve({ @@ -104,7 +106,8 @@ const channelSubtract = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(inputs.a.data[x + y*w] - inputs.b.data[x + y*w]); + const ind = x + y*w; + result.data[ind] = inputs.a.data[ind] - inputs.b.data[ind]; } } resolve({ @@ -141,7 +144,8 @@ const channelMultiply = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(inputs.a.data[x + y*w] * inputs.b.data[x + y*w] / 256); + const ind = x + y*w; + result.data[ind] = inputs.a.data[ind] * inputs.b.data[ind] / 256; } } resolve({ @@ -178,7 +182,8 @@ const channelMultiplyNumber = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(inputs.a.data[x + y*w] * inputs.b); + const ind = x + y*w; + result.data[ind] = inputs.a.data[ind] * inputs.b; } } resolve({ @@ -221,8 +226,9 @@ const channelDivide = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - const pixel = inputs.a.data[x + y*w] / inputs.b.data[x + y*w] * 256; - result.data.push(isFinite(pixel)? pixel : (inputs.error || Infinity)); + const ind = x + y*w; + const pixel = inputs.a.data[ind] / inputs.b.data[ind] * 256; + result.data[ind] = isFinite(pixel)? pixel : (inputs.error || Infinity); } } resolve({ @@ -254,7 +260,8 @@ const channelAbs = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(Math.abs(inputs.a.data[x + y*w])); + const ind = x + y*w; + result.data[ind] = Math.abs(inputs.a.data[ind]); } } resolve({ @@ -291,7 +298,8 @@ const channelPower = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(Math.pow(inputs.a.data[x + y*w], inputs.power)); + const ind = x + y*w; + result.data[ind] = Math.pow(inputs.a.data[ind], inputs.power); } } resolve({ @@ -329,7 +337,8 @@ const channelModulo = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(inputs.a.data[x + y*w] % inputs.modulo); + const ind = x + y*w; + result.data[ind] = inputs.a.data[ind] % inputs.modulo; } } resolve({ @@ -367,7 +376,8 @@ const channelMax = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(Math.max(inputs.a.data[x + y*w], inputs.b.data[x + y*w])); + const ind = x + y*w; + result.data[ind] = Math.max(inputs.a.data[ind], inputs.b.data[ind]); } } resolve({ @@ -404,7 +414,8 @@ const channelMin = { const result = new Channel(w, h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { - result.data.push(Math.min(inputs.a.data[x + y*w], inputs.b.data[x + y*w])); + const ind = x + y*w; + result.data[ind] = Math.min(inputs.a.data[ind], inputs.b.data[ind]); } } resolve({ diff --git a/src/js/blocks/channels.ts b/src/js/blocks/channels.ts index 4ae897b..3958d1e 100644 --- a/src/js/blocks/channels.ts +++ b/src/js/blocks/channels.ts @@ -45,7 +45,8 @@ const splitChannels = { for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { for (let i = 0; i < 4; i++) { - channels[i].data.push(inputs.pixels.data[(x + y*w)*4 + i]); + const ind = x + y*w; + channels[i].data[ind] = inputs.pixels.data[(ind)*4 + i]; } } } diff --git a/src/js/blocks/noise.ts b/src/js/blocks/noise.ts index 39f9296..e661556 100644 --- a/src/js/blocks/noise.ts +++ b/src/js/blocks/noise.ts @@ -52,6 +52,7 @@ const perlinNoise = { exec(inputs: any, block: Block) { return new Promise((resolve, reject) => { const seedSet = Noise.seed(block.tagValues.seed || 0), + // tslint:disable-next-line: no-implicit-dependencies plainRandom = new (require('mersenne-twister'))(0); const sizeX = inputs.sizeX || 5, sizeY = inputs.sizeY || 5; diff --git a/src/js/blocks/processing.ts b/src/js/blocks/processing.ts index 73a5a68..3728957 100644 --- a/src/js/blocks/processing.ts +++ b/src/js/blocks/processing.ts @@ -261,19 +261,31 @@ const gammaCorrection = { } }; +const {fastSharpen} = require('./processing/fastSharpen.js'); +const {adaptiveSharpen} = require('./processing/adaptiveSharpen.js'); const {computeNormals, computeNormalsPixels} = require('./processing/normals.js'); const {gaussianBlur} = require('./processing/blur.js'); const {simpleShader} = require('./processing/shaders.js'); +const {median} = require('./processing/medianBlock.js'); +const {nearestNeighbor} = require('./processing/nearestNeighbor.js'); +const {getSkinColors} = require('./processing/skinColors.js'); +const {bilateralFilter} = require('./processing/bilateralFilter.js'); module.exports = { name: 'Processing', blocks: { + invert, grayscale, grayscaleChannel, + getSkinColors, brightnessContrast, gammaCorrection, - invert, + fastSharpen, + adaptiveSharpen, gaussianBlur, + bilateralFilter, + median, + nearestNeighbor, computeNormals, simpleShader } diff --git a/src/js/blocks/processing/adaptiveSharpen.ts b/src/js/blocks/processing/adaptiveSharpen.ts new file mode 100644 index 0000000..d590989 --- /dev/null +++ b/src/js/blocks/processing/adaptiveSharpen.ts @@ -0,0 +1,390 @@ +import OSWebGL = require('./../../oneShotWebGL.js'); +import BlockError = require('./../../types/BlockError.js'); +const glsl = e => e; + +/* +Copyright (c) 2015-2018, bacondither +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer + in this position and unchanged. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const firstPassOGL = new OSWebGL(glsl` + // Get points and clip out of range values (BTB & WTW) + // [ c9 ] + // [ c1, c2, c3 ] + // [ c10, c4, c0, c5, c11 ] + // [ c6, c7, c8 ] + // [ c12 ] + vec3 c[13]; + c[0] = get( 0, 0); + c[1] = get(-1,-1); + c[2] = get( 0,-1); + c[3] = get( 1,-1); + c[4] = get(-1, 0); + c[5] = get( 1, 0); + c[6] = get(-1, 1); + c[7] = get( 0, 1); + c[8] = get( 1, 1); + c[9] = get( 0,-2); + c[10] = get(-2, 0); + c[11] = get( 2, 0); + c[12] = get( 0, 2); + + // Blur, gauss 3x3 + vec3 blur = (2.0*(c[2]+c[4]+c[5]+c[7]) + (c[1]+c[3]+c[6]+c[8]) + 4.0*c[0])*0.0625; + + // Contrast compression, center = 0.5, scaled to 1/3 + float c_comp = clamp(0.266666 + 0.9*exp2(dot(blur, vec3(-2.46666))), 0.0, 1.0); + + // Edge detection + // Relative matrix weights + // [ 1 ] + // [ 4, 5, 4 ] + // [ 1, 5, 6, 5, 1 ] + // [ 4, 5, 4 ] + // [ 1 ] + float edge = length(1.38*(b_diff(0)) + + 1.15*(b_diff(2) + b_diff(4) + b_diff(5) + b_diff(7)) + + 0.92*(b_diff(1) + b_diff(3) + b_diff(6) + b_diff(8)) + + 0.23*(b_diff(9) + b_diff(10) + b_diff(11) + b_diff(12))); + + gl_FragColor = vec4((texture2D(u_image, v_texCoord).rgb), (edge*c_comp + a_offset)); +`, {}, glsl` + //--------------------------------------------------------------------------------- + #define a_offset 0.0 // Edge channel offset, MUST BE THE SAME IN ALL PASSES + //--------------------------------------------------------------------------------- + + // Get destination pixel values + #define get(x,y) (clamp(texture2D(u_image, vec2(1.0, 1.0) / u_textureSize*vec2(x, y) + v_texCoord).rgb, 0.0, 1.0) ) + + // Component-wise distance + #define b_diff(pix) ( abs(blur - c[pix]) ) +`); + +const secondPassOGL = new OSWebGL(glsl` + // Look up a color from the texture. + vec4 orig = get(0, 0); + float c_edge = orig.a - a_offset; + + if (bounds_check == true) { + if (c_edge > 24.0 || c_edge < -0.5) { + gl_FragColor = vec4( 0, 1.0, 0, alpha_out ); + return; + } + } + + // Get points, clip out of range colour data in c[0] + // [ c22 ] + // [ c24, c9, c23 ] + // [ c21, c1, c2, c3, c18 ] + // [ c19, c10, c4, c0, c5, c11, c16 ] + // [ c20, c6, c7, c8, c17 ] + // [ c15, c12, c14 ] + // [ c13 ] + vec4 c[25]; + c[0] = sat(orig); + c[1] = get(-1,-1); + c[2] = get( 0,-1); + c[3] = get( 1,-1); + c[4] = get(-1, 0); + c[5] = get( 1, 0); + c[6] = get(-1, 1); + c[7] = get( 0, 1); + c[8] = get( 1, 1); + c[9] = get( 0,-2); + c[10] = get(-2, 0); + c[11] = get( 2, 0); + c[12] = get( 0, 2); + c[13] = get( 0, 3); + c[14] = get( 1, 2); + c[15] = get(-1, 2); + c[16] = get( 3, 0); + c[17] = get( 2, 1); + c[18] = get( 2,-1); + c[19] = get(-3, 0); + c[20] = get(-2, 1); + c[21] = get(-2,-1); + c[22] = get( 0,-3); + c[23] = get( 1,-2); + c[24] = get(-1,-2); + + // Allow for higher overshoot if the current edge pixel is surrounded by similar edge pixels + float maxedge = max4( max4(c[1].a,c[2].a,c[3].a,c[4].a), max4(c[5].a,c[6].a,c[7].a,c[8].a), + max4(c[9].a,c[10].a,c[11].a,c[12].a), c[0].a ) - a_offset; + + // [ x ] + // [ z, x, w ] + // [ z, z, x, w, w ] + // [ y, y, y, 0, y, y, y ] + // [ w, w, x, z, z ] + // [ w, x, z ] + // [ x ] + float sbe = soft_if(c[2].a,c[9].a, c[22].a)*soft_if(c[7].a,c[12].a,c[13].a) // x dir + + soft_if(c[4].a,c[10].a,c[19].a)*soft_if(c[5].a,c[11].a,c[16].a) // y dir + + soft_if(c[1].a,c[24].a,c[21].a)*soft_if(c[8].a,c[14].a,c[17].a) // z dir + + soft_if(c[3].a,c[23].a,c[18].a)*soft_if(c[6].a,c[20].a,c[15].a); // w dir + + vec2 cs = mix( vec2(L_compr_low, D_compr_low), + vec2(L_compr_high, D_compr_high), smoothstep(2.0, 3.1, sbe) ); + + // RGB to luma + float c0_Y = CtL(c[0]); + + float luma[25]; + luma[0] = c0_Y; + for (int i = 1; i < 25; i++) { + luma[i] = CtL(c[i]); + } + + // Pre-calculated default squared kernel weights + const vec3 W1 = vec3(0.5, 1.0, 1.41421356237); // 0.25, 1.0, 2.0 + const vec3 W2 = vec3(0.86602540378, 1.0, 0.54772255751); // 0.75, 1.0, 0.3 + + // Transition to a concave kernel if the center edge val is above thr + vec3 dW = pow(mix(W1, W2, smoothstep(dW_lothr, dW_hithr, c_edge)), vec3(2.0)); + + float mdiff_c0 = 0.02 + 3.0*( abs(luma[0]-luma[2]) + abs(luma[0]-luma[4]) + + abs(luma[0]-luma[5]) + abs(luma[0]-luma[7]) + + 0.25*(abs(luma[0]-luma[1]) + abs(luma[0]-luma[3]) + +abs(luma[0]-luma[6]) + abs(luma[0]-luma[8])) ); + + // Use lower weights for pixels in a more active area relative to center pixel area + // This results in narrower and less visible overshoots around sharp edges + float weights[12]; + weights[0] = min(mdiff_c0/mdiff(24, 21, 2, 4, 9, 10, 1), dW.y), // c1 + weights[1] = dW.x; // c2 + weights[2] = min(mdiff_c0/mdiff(23, 18, 5, 2, 9, 11, 3), dW.y); // c3 + weights[3] = dW.x; // c4 + weights[4] = dW.x; // c5 + weights[5] = min(mdiff_c0/mdiff(4, 20, 15, 7, 10, 12, 6), dW.y); // c6 + weights[6] = dW.x; // c7 + weights[7] = min(mdiff_c0/mdiff(5, 7, 17, 14, 12, 11, 8), dW.y); // c8 + weights[8] = min(mdiff_c0/mdiff(2, 24, 23, 22, 1, 3, 9), dW.z); // c9 + weights[9] = min(mdiff_c0/mdiff(20, 19, 21, 4, 1, 6, 10), dW.z); // c10 + weights[10] = min(mdiff_c0/mdiff(17, 5, 18, 16, 3, 8, 11), dW.z); // c11 + weights[11] = min(mdiff_c0/mdiff(13, 15, 7, 14, 6, 8, 12), dW.z); // c12 + + weights[0] = (max(max((weights[8] + weights[9]) / 4.0, weights[0]), 0.25) + weights[0]) / 2.0; + weights[2] = (max(max((weights[8] + weights[10]) / 4.0, weights[2]), 0.25) + weights[2]) / 2.0; + weights[5] = (max(max((weights[9] + weights[11]) / 4.0, weights[5]), 0.25) + weights[5]) / 2.0; + weights[7] = (max(max((weights[10] + weights[11]) / 4.0, weights[7]), 0.25) + weights[7]) / 2.0; + + // Calculate the negative part of the laplace kernel and the low threshold weight + float lowthrsum = 0.0; + float weightsum = 0.0; + float neg_laplace = 0.0; + + for (int pix = 0; pix < 12; ++pix) { + float t = clamp((c[pix + 1].a - a_offset - 0.01)/(lowthr_mxw - 0.01), 0.0, 1.0); + float lowthr = t*t*(2.97 - 1.98*t) + 0.01; // t*t*(3.0 - a*3.0 - (2.0 - a*2.0)*t) + a; + + neg_laplace += pow(luma[pix + 1] + 0.06, 2.4)*(weights[pix]*lowthr); + weightsum += weights[pix]*lowthr; + lowthrsum += lowthr / 12.0; + } + + neg_laplace = pow(abs(neg_laplace/weightsum), (1.0/2.4)) - 0.06; + + // Compute sharpening magnitude function + float sharpen_val = curveHeight/(curveHeight*curveslope*pow(abs(c_edge), 3.5) + 0.625); + + // Calculate sharpening diff and scale + float sharpdiff = (c0_Y - neg_laplace)*(lowthrsum*sharpen_val + 0.01); + + // Calculate local near min & max, partial sort + float temp; + for (int j = 0; j < 24; j += 2) { + temp = luma[j]; + luma[j] = min(luma[j], luma[j+1]); + luma[j+1] = max(temp, luma[j+1]); + } + for (int jj = 24; jj > 0; jj -= 2) { + temp = luma[0]; + luma[0] = min(luma[0], luma[jj]); + luma[jj] = max(temp, luma[jj]); + temp = luma[24]; + luma[24] = max(luma[24], luma[jj-1]); + luma[jj-1] = min(temp, luma[jj-1]); + } + + for (int j = 1; j < 23; j += 2) { + temp = luma[j]; + luma[j] = min(luma[j], luma[j+1]); + luma[j+1] = max(temp, luma[j+1]); + } + for (int jj = 23; jj > 1; jj -= 2) { + temp = luma[1]; + luma[1] = min(luma[1], luma[jj]); + luma[jj] = max(temp, luma[jj]); + temp = luma[23]; + luma[23] = max(luma[23], luma[jj-1]); + luma[jj-1] = min(temp, luma[jj-1]); + } + + for (int j = 3; j < 22; j += 2) { + temp = luma[j]; + luma[j] = min(luma[j], luma[j+1]); + luma[j+1] = max(temp, luma[j+1]); + } + for (int jj = 22; jj > 2; jj -= 2) { + temp = luma[2]; + luma[2] = min(luma[2], luma[jj]); + luma[jj] = max(temp, luma[jj]); + temp = luma[22]; + luma[22] = max(luma[22], luma[jj-1]); + luma[jj-1] = min(temp, luma[jj-1]); + } + + float nmax = (max(luma[22] + luma[23]*2.0, c0_Y*3.0) + luma[24]) / 4.0; + float nmin = (min(luma[2] + luma[1]*2.0, c0_Y*3.0) + luma[0]) / 4.0; + + // Calculate tanh scale factors + float min_dist = min(abs(nmax - c0_Y), abs(c0_Y - nmin)); + float pos_scale = min_dist + min(L_overshoot, 1.0001 - min_dist - c0_Y); + float neg_scale = min_dist + min(D_overshoot, 0.0001 + c0_Y - min_dist); + + pos_scale = min(pos_scale, scale_lim*(1.0 - scale_cs) + pos_scale*scale_cs); + neg_scale = min(neg_scale, scale_lim*(1.0 - scale_cs) + neg_scale*scale_cs); + + // Soft limited anti-ringing with tanh, wpmean to control compression slope + sharpdiff = wpmean( max(sharpdiff, 0.0), soft_lim( max(sharpdiff, 0.0), pos_scale ), cs.x ) + - wpmean( min(sharpdiff, 0.0), soft_lim( min(sharpdiff, 0.0), neg_scale ), cs.y ); + + // Compensate for saturation loss/gain while making pixels brighter/darker + float sharpdiff_lim = clamp(c0_Y + sharpdiff, 0.0, 1.0) - c0_Y; + float satmul = (c0_Y + max(sharpdiff_lim*0.9, sharpdiff_lim)*1.03 + 0.03)/(c0_Y + 0.03); + vec3 res = c0_Y + (sharpdiff_lim*3.0 + sharpdiff)/4.0 + (c[0].rgb - c0_Y)*satmul; + + gl_FragColor = vec4( (video_level_out == true ? res + orig.rgb - c[0].rgb : res), alpha_out ); +`, { + curveHeight: 'float' +}, glsl` + + //--------------------------------------- Settings ------------------------------------------------ + + // define curveHeight 1.0 // Main control of sharpening strength [>0] + // 0.3 <-> 2.0 is a reasonable range of values + + #define video_level_out false // True to preserve BTB & WTW (minor summation error) + // Normally it should be set to false + + //------------------------------------------------------------------------------------------------- + // Defined values under this row are "optimal" DO NOT CHANGE IF YOU DO NOT KNOW WHAT YOU ARE DOING! + + #define curveslope 0.5 // Sharpening curve slope, high edge values + + #define L_overshoot 0.003 // Max light overshoot before compression [>0.001] + #define L_compr_low 0.167 // Light compression, default (0.167=~6x) + #define L_compr_high 0.334 // Light compression, surrounded by edges (0.334=~3x) + + #define D_overshoot 0.009 // Max dark overshoot before compression [>0.001] + #define D_compr_low 0.250 // Dark compression, default (0.250=4x) + #define D_compr_high 0.500 // Dark compression, surrounded by edges (0.500=2x) + + #define scale_lim 0.1 // Abs max change before compression [>0.01] + #define scale_cs 0.056 // Compression slope above scale_lim + + #define dW_lothr 0.3 // Start interpolating between W1 and W2 + #define dW_hithr 0.8 // When dW is equal to W2 + + #define lowthr_mxw 0.1 // Edge value for max lowthr weight [>0.01] + + #define pm_p 0.7 // Power mean p-value [>0-1.0] + + #define alpha_out 1.0 // MPDN requires the alpha channel output to be 1.0 + + //------------------------------------------------------------------------------------------------- + #define a_offset 0.0 // Edge channel offset, MUST BE THE SAME IN ALL PASSES + #define bounds_check false // If edge data is outside bounds, make pixels green + //------------------------------------------------------------------------------------------------- + + // Soft if, fast linear approx + #define soft_if(a,b,c) (clamp((a + b + c - 3.0*a_offset + 0.056)/(abs(maxedge) + 0.03) - 0.85, 0.0, 1.0)) + + // Soft limit, modified tanh + #define soft_lim(v,s) ((exp(2.0*min(abs(v), s*24.0)/s) - 1.0)/(exp(2.0*min(abs(v), s*24.0)/s) + 1.0)*s) + + // Weighted power mean + #define wpmean(a,b,w) (pow(w*pow(abs(a), pm_p) + abs(1.0-w)*pow(abs(b), pm_p), (1.0/pm_p))) + + // Get destination pixel values + #define get(x,y) (texture2D(u_image, vec2(1.0, 1.0) / u_textureSize*vec2(x, y) + v_texCoord)) + #define sat(var) (vec4(clamp((var).rgb, 0.0, 1.0), (var).a) ) + + // Maximum of four values + #define max4(a,b,c,d) ( max(max(a, b), max(c, d)) ) + + // Colour to luma, fast approx gamma, avg of rec. 709 & 601 luma coeffs + #define CtL(RGB) ( sqrt(dot(vec3(0.2558, 0.6511, 0.0931), clamp((RGB)*abs(RGB), 0.0, 1.0).rgb)) ) + + // Center pixel diff + #define mdiff(a,b,c,d,e,f,g) ( abs(luma[g] - luma[a]) + abs(luma[g] - luma[b]) \ + + abs(luma[g] - luma[c]) + abs(luma[g] - luma[d]) \ + + 0.5*(abs(luma[g] - luma[e]) + abs(luma[g] - luma[f])) ) +`); + +const adaptiveSharpen = { + nameLoc: 'blocks.processing.adaptiveSharpen.name', + name: 'Adaptive sharpen', + inputs: [{ + key: 'input', + type: 'canvas', + name: 'Input', + nameLoc: 'blocks.processing.adaptiveSharpen.input' + }, { + key: 'strength', + type: 'number', + name: 'Strength', + nameLoc: 'blocks.processing.adaptiveSharpen.strength', + hint: 'A number between 0 and 1; 1 produces the most sharp results', + optional: true + }], + outputs: [{ + key: 'output', + type: 'canvas', + name: 'Output', + nameLoc: 'blocks.composing.adaptiveSharpen.output' + }], + exec(inputs, block) { + const inp = inputs.input, + curveHeight = (Math.max(0, Math.min(1, inputs.strength === void 0? 0.5 : inputs.strength)) + 0.3) * 1.7; + return new Promise((resolve, reject) => { + firstPassOGL.render(inp) + .then(intermediate => secondPassOGL.render(inp, { + curveHeight + })) + .then(result => { + resolve({ + output: result + }); + }) + .catch(err => { + throw new BlockError(err, block); + }); + }); + } +}; + +module.exports = { + adaptiveSharpen +}; diff --git a/src/js/blocks/processing/bilateralFilter.ts b/src/js/blocks/processing/bilateralFilter.ts new file mode 100644 index 0000000..82ef54c --- /dev/null +++ b/src/js/blocks/processing/bilateralFilter.ts @@ -0,0 +1,112 @@ +import OSWebGL = require('./../../oneShotWebGL.js'); +import BlockError = require('../../types/BlockError.js'); +const glsl = e => e; + +/* +MIT License + +Copyright (c) 2017 Tran Van Sang + +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. + */ +const OGLCode = '' + glsl` + float facS = -1.0 / (sigS*sigS); + float facL = -1.0 / (sigL*sigL); + + float sumW = 0.0; + vec4 sumC = vec4(0.0); + + float l = length(texture2D(u_image, v_texCoord).xyz); + + for (int x = -$halfSize; x <= $halfSize; x++) { + for (int y = -$halfSize; y <= $halfSize; y++) { + vec2 pos = vec2(float(x), float(y)); + vec4 offsetColor = texture2D(u_image, v_texCoord + pos * pixel); + + float distS = length(pos); + float distL = length(offsetColor.xyz)-l; + + float wS = exp(facS * float(distS*distS)); + float wL = exp(facL * float(distL*distL)); + float w = wS*wL; + + sumW += w; + sumC += offsetColor * w; + } + } + + gl_FragColor = sumC/sumW; +`; + +const bilateralFilter = { + nameLoc: 'blocks.processing.bilateralFilter.name', + name: 'Smart blur', + inputs: [{ + key: 'input', + type: 'canvas', + name: 'Input', + nameLoc: 'blocks.processing.bilateralFilter.input' + }, { + key: 'size', + type: 'number', + name: 'Radius', + hint: 'The size of the kernel relative to the image. Should be less than 1, usually something around 0,01.', + nameLoc: 'blocks.processing.bilateralFilter.size' + }, { + key: 'threshold', + type: 'number', + name: 'Threshold', + hint: '', + nameLoc: 'blocks.processing.bilateralFilter.threshold' + }], + outputs: [{ + key: 'output', + type: 'canvas', + name: 'Output', + nameLoc: 'blocks.processing.bilateralFilter.output', + }], + exec(inputs, block) { + return new Promise((resolve, reject) => { + const inp = inputs.input, + {size} = inputs; + const pixels = Math.floor(size / 2 * inp.width); + const shot = OGLCode.replace(/\$halfSize/g, pixels.toString()); + (new OSWebGL(shot, { + sigS: 'float', + sigL: 'float' + })).render(inp, { + sigS: pixels, + sigL: inputs.threshold + }) + .then(image => { + resolve({ + output: image + }); + }) + .catch(err => { + console.error(err); + reject(new BlockError(err, block)); + }); + }); + } +}; + +module.exports = { + bilateralFilter +}; diff --git a/src/js/blocks/processing/fastSharpen.ts b/src/js/blocks/processing/fastSharpen.ts new file mode 100644 index 0000000..9ad0a96 --- /dev/null +++ b/src/js/blocks/processing/fastSharpen.ts @@ -0,0 +1,143 @@ +import OSWebGL = require('./../../oneShotWebGL.js'); +import BlockError = require('./../../types/BlockError.js'); +const glsl = e => e; + +/** + * By a matrix + * 0 -1 0 + * -1 5 -1 + * 0 -1 0 + */ +const diamondSharpen = new OSWebGL(glsl` + gl_FragColor = vec4(0.0); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel); + gl_FragColor += 5.0 * texture2D(u_image, v_texCoord + vec2(0.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel); +`); +const diamondSharpenNoOvershoot = new OSWebGL(glsl` + vec4 minimum = min(texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel), + texture2D(u_image, v_texCoord))))); + vec4 maximum = max(texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel), + texture2D(u_image, v_texCoord))))); + gl_FragColor = vec4(0.0); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel); + gl_FragColor += 5.0 * texture2D(u_image, v_texCoord + vec2(0.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel); + + gl_FragColor = max(minimum, min(gl_FragColor, maximum)); +`); +/** + * By a matrix + * -1 -1 -1 + * -1 9 -1 + * -1 -1 -1 + */ +const boxSharpen = new OSWebGL(glsl` + gl_FragColor = vec4(0.0); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, -1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, -1.0) * pixel); + gl_FragColor += 9.0 * texture2D(u_image, v_texCoord + vec2(0.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, 1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, 1.0) * pixel); +`); +const boxSharpenNoOvershoot = new OSWebGL(glsl` + vec4 minimum = min(texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(-1.0, -1.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(1.0, 1.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(-1.0, 1.0) * pixel), + min(texture2D(u_image, v_texCoord + vec2(1.0, -1.0) * pixel), + texture2D(u_image, v_texCoord))))))))); + vec4 maximum = max(texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(-1.0, -1.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(1.0, 1.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(-1.0, 1.0) * pixel), + max(texture2D(u_image, v_texCoord + vec2(1.0, -1.0) * pixel), + texture2D(u_image, v_texCoord))))))))); + + gl_FragColor = vec4(0.0); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, -1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, -1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, -1.0) * pixel); + gl_FragColor += 9.0 * texture2D(u_image, v_texCoord + vec2(0.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(0.0, 1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, 0.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(1.0, 1.0) * pixel); + gl_FragColor += -1.0 * texture2D(u_image, v_texCoord + vec2(-1.0, 1.0) * pixel); + + gl_FragColor = max(minimum, min(gl_FragColor, maximum)); +`); + +const fastSharpen = { + nameLoc: 'blocks.processing.fastSharpen.name', + name: 'Fast sharpen', + inputs: [{ + key: 'input', + type: 'canvas', + name: 'Input', + nameLoc: 'blocks.processing.fastSharpen.input' + }], + outputs: [{ + key: 'output', + type: 'canvas', + name: 'Output', + nameLoc: 'blocks.composing.fastSharpen.output' + }], + tags: [{ + tag: 'bool-input', + key: 'box', + label: 'Sharpen+', + defaultValue: false + }, { + tag: 'bool-input', + key: 'overshootSuppression', + label: 'Suppress overshooting', + defaultValue: false + }], + exec(inputs, block) { + return new Promise((resolve, reject) => { + const inp = inputs.input, + box = block.tagValues.box, + suppress = block.tagValues.overshootSuppression; + var method; + // tslint:disable-next-line: prefer-conditional-expression + if (box) { + method = suppress? boxSharpenNoOvershoot : boxSharpen; + } else { + method = suppress? diamondSharpenNoOvershoot : diamondSharpen; + } + method.render(inp) + .then(output => { + resolve({ + output + }); + }) + .catch(err => { + console.error(err); + reject(new BlockError(err, block)); + }); + }); + } +}; + +module.exports = {fastSharpen}; diff --git a/src/js/blocks/processing/medianBlock.ts b/src/js/blocks/processing/medianBlock.ts new file mode 100644 index 0000000..d10fd47 --- /dev/null +++ b/src/js/blocks/processing/medianBlock.ts @@ -0,0 +1,65 @@ +var {MedianFilter} = require('./medianFilter.js'); + +const median = { + nameLoc: 'blocks.processing.median.name', + name: 'Median & Percentile', + inputs: [{ + key: 'input', + type: 'pixels', + name: 'Input', + nameLoc: 'blocks.processing.median.input' + }, { + key: 'size', + type: 'number', + name: 'Size', + hint: 'The size of the filtering kernel, relative to the image. Should be less than 0.5 (mostly MUCH less than 0.5, e.g. 0.01). Bigger values result into slower processing', + nameLoc: 'blocks.processing.median.size' + }, { + key: 'percentile', + type: 'number', + name: 'Percentile', + optional: true, + hint: 'A number between 0 and 1 that defaults to 0.5. Smaller values make image darker, while higher make it brighter.' + }], + outputs: [{ + key: 'output', + type: 'pixels', + name: 'Output', + nameLoc: 'blocks.processing.median.output', + }], + tags: [{ + tag: 'select-input', + key: 'mode', + label: 'Mask shape:', + defaultValue: 'circular', + options: [ + 'circular', + 'rectangular', + 'diamond' + ] + }, { + tag: 'bool-input', + key: 'quality', + label: 'High quality', + defaultValue: false + }], + exec(inputs, block) { + const inp = inputs.input; + const filter = new MedianFilter({ + size: Math.round(inp.width * (inputs.size || 0.01)), + shape: block.tagValues.mode || 'circular', + percentile: Math.max(0, Math.min(0.5, inputs.percentile || 0.5)), + highQuality: block.tagValues.quality + }); + return new Promise((resolve, reject) => { + const output = filter.convertImage(inp); + resolve({ + output + }); + }); + } +}; + +module.exports = { + median +}; diff --git a/src/js/blocks/processing/medianFilter.js b/src/js/blocks/processing/medianFilter.js new file mode 100644 index 0000000..a0543a0 --- /dev/null +++ b/src/js/blocks/processing/medianFilter.js @@ -0,0 +1,862 @@ +/* eslint-disable func-style */ +/* eslint-disable no-empty */ +/* eslint-disable no-nested-ternary */ +/* eslint-disable max-params */ +/* eslint-disable prefer-destructuring */ +/* eslint-disable no-use-before-define */ +/* eslint-disable no-bitwise */ +/** + ## Median filter + Espoo, Finland, November 2014 + Petri Leskinen, + petri.leskinen@icloud.com + http://pixelero.wordpress.com/ + MIT license + + Simple example of usage in javascript: + + ```js + // grab the pixels: + var imageData = context.getImageData(0, 0, w, h); + // clear area: + context.clearRect (0, 0, canvas.width, canvas.height); + // replace with filtered image: + context.putImageData(convertImage(imageData), 0, 0); +``` + */ + +var MedianFilter = function(options) { + options = options || {}; + console.log(options); + // Mask Dimensions: + this.maskHeight = options.size || 9; + this.maskWidth = options.size || 9; + + this.CIRCULAR = 'circular'; + this.RECTANGULAR = 'rectangular'; + this.DIAMOND = 'diamond'; + + this.shape = options.shape || this.RECTANGULAR; + + // controls which item to pick from the sorted order: + // default 0.5 = median + // 0.0 = min + // 1.0 = max + + this.kth = options.percentile !== void 0? Math.max(0, Math.min(options.percentile, 1)) : 0.5; + + this.FAST = 'fast'; + this.QUALITY = 'quality'; + + this.mode = options.highQuality? this.QUALITY : this.FAST; + + this.NUMCLUSTERS = 64; + + this.convertImage = function (imageData) { + + var {data} = imageData; + var w = imageData.width, + h = imageData.height; + + // first analyze the data + var hs= this.calculateHistograms(data), + bins=this.NUMCLUSTERS; + + hs[0]=this.blurHistogram(hs[0]); + hs[1]=this.blurHistogram(hs[1]); + hs[2]=this.blurHistogram(hs[2]); + + // histograms of how values of each channel are distributed: + // note: this is a modified version: it doesn't equalize the channels, + // but tries to equalize their sum: + var tst= this.equalizeHistogramSum(hs[0],hs[1],hs[2], bins), + [rarr, garr, barr] = tst; + + /* + // equalize by channels: + var rarr=this.equalizeHistogram(hs[0], bins), + garr=this.equalizeHistogram(hs[1], 2*bins), + barr=this.equalizeHistogram(hs[2], bins); + */ + + + /* element: filter mask consisting of 0:s and 1:s: + 001100 + 011110 + 111111 + 111111 + 011110 + 001100 + */ + var element; + switch (this.shape) { + case this.DIAMOND: + element=this.getDiamondMask(this.maskWidth,this.maskHeight); + break; + + case this.CIRCULAR: + element=this.getCircularMask(this.maskWidth,this.maskHeight); + break; + + default: // RECTANGULAR + element=this.getRectangleMask(this.maskWidth,this.maskHeight); + } + + /* mask: an array of arrays consisting of information of row elements + where does it end, its y-position, and length + 001100 -> [end=4,y=0,length=2] + 011110 -> [end=5,y=1,length=4] + 111111 -> [end=6,y=2, length=6] + */ + var mask=this.maskToArray(element); + + // empty filtering, let's nothing ! + if (mask.length === 0) { + return imageData; + } + + /* unnecessery try to smoothen the output by extra addition of nearby values */ + if (this.mode === this.FAST) { + if (this.maskWidth>3 && this.maskHeight>>3) { + const i=this.maskWidth>>1, + j=this.maskHeight>>1; + // mask.push([i+1,j-1,3]); + mask.push([i+1,j,3]); + // mask.push([i+1,j+1,3]); + } + } + + // each pixel = 4 elements in image data [r,g,b,a] + // scale start positions by 4 + for (let i=0; i>1, + // ysize is the largest y-coordinate of the mask: + ysize=mask[0][1]; + for (let i=1; iysize) {ysize=mask[i][1];} + } + ysize++; + + + // evalPixel: a function giving the actual value of how to sort the pixels + // by lightness: + // var evalPixel = function (r,g,b) { return (5*g+3*r+b)>>2; }; + // by average; + // var evalPixel = function (r,g,b) { return (g+r+b)>>3; }; + // by equalized average: + var evalPixel = function (r,g,b) { return rarr[r]+garr[g]+barr[b]; }; + + var rows=new Array(ysize), + // yNext: the number of the next line to read: + yNext=-(ysize>>1); + + // read first lines: rows[0]≈[] ! + for (let i=1; i=h ? h-1 : y); + + // grab pixel from data: + var arr=data.subarray(4*w*y,4*w*(y+1)-1), + row= new Uint8ClampedArray((w+2*padding)<<2); + row.set(arr, padding<<2); + + // pad to start of array: + for (let j=(padding+0)<<2, k=j-4; k>-1;) { + const tmp=row.subarray(j,j+3); + row.set(tmp,k); + k-=4; + } + + // pad to end of array: + for (let j=(w+padding)*4, k=j-4; j=h ? 2*(h-1)-y+1 : y); + + // grab pixel from data: + var arr=data.subarray(4*w*y,4*w*(y+1)-1), + row= new Uint8ClampedArray((w+2*padding)<<2); + row.set(arr, padding<<2); + + // pad to start of array: + for (let j=(padding+0)<<2, k=j-4; k>-1;) { + const tmp=row.subarray(j,j+3); + row.set(tmp,k); + k-=4; j+=4; + } + + // pad to end of array: + for (let j=(w+padding)*4, k=j-4; jm) { + sum -=m; + j++; + } + hst2[i]=j; + } + return hst2; + }; + + this.equalizeHistogramSum = function(rh,gh,bh, bins) { + var totalSum=0, + N=rh.length; + for (var i=0; imax) {max=b;} + + if (max>=0.9*(r+g+b)) { + rh2[ir++]= + gh2[ig++]= + bh2[ib++]=j; + sum += r+g+b; + } else { + + if (r===min) { + rh2[ir++]=j; + } else if (g===min) { + gh2[ig++]=j; + } else { + bh2[ib++]=j; + } + if (r===max) { + rh2[ir++]=j; + } else if (g===max) { + gh2[ig++]=j; + } else { + bh2[ib++]=j; + } + sum += min+max; + } + while (sum>m) { sum -=m; j++; } + + } + return [rh2,gh2,bh2]; + }; + + + // applies a running average of three [previous,this,next] to an array: + this.blurHistogram = function(hst) { + var hst2=new Uint32Array(hst.length); + hst2[0]=hst2[1]=1.5*hst[0]; + var i=hst.length-1; + hst2[i]=hst[i-1]=1.5*hst[i]; + for (i=1; i [ 2, 6, 7, 9 ] + // returns an array of positions where sequence of 0 turn to 1 or vise versa + this.countBinaries = function (arr) { + var last = 0, + i = 0, + res = []; + while (i < arr.length) { + while (arr[i] === last) {i++;} + last = arr[i]; + res.push(i); + } + return res.length<2 ? [] : res; + }; + + /* For testing only + this.testMedian = function (size) { + var hst=new MedianHistogram(size); + var arr=[],sz=230; + for (var i=0; i>1]; + } + */ + return this; +}; + + +function MedianHistogram(size) { + + this.init = function(size) { + this.medianbin=0; + this.medianindex=1; + + this.histogram = new Uint16Array(size); + + this.colors = []; + for (var i=size; i>0;) { + this.colors[--i]= new Uint32Array(3); + } + }; + + this.init(size); + + this.addItem = function(index, r,g,b) { + // if added value is below the median, + // decrease median's position + if (index>= 1; + c[1] >>= 1; + c[2] >>= 1; + break; + + default: + // decrease value by one, + // subtract the average from color counts: + var i=this.histogram[index]--, + f=this.histogram[index]/i; + + c[0] *= f; + c[1] *= f; + c[2] *= f; + } + + }; + + // remove an array of images: + this.removeItems = function(arr) { + for (var i=0; i>0; + return this.getMedian(); + }; + + + this.getMedian = function () { + // if needed, update index/bin to current median position: + while (this.medianindex> this.histogram[this.medianbin]) { + this.medianindex -= this.histogram[this.medianbin++]; + } + while (this.medianindex<1) { + this.medianindex += this.histogram[--this.medianbin]; + } + + + // return the value ... + // as an average of bin entries: + + var c=this.colors[this.medianbin]; + switch (this.histogram[this.medianbin]) { + case 1: + // only 1 entry, return colors values as they are: + return c; + + case 2: + return [c[0]>>1,c[1]>>1,c[2]>>1]; + + case 4: + return [c[0]>>2,c[1]>>2,c[2]>>2]; + + default: + var f=1.0/this.histogram[this.medianbin]; + return [f*c[0],f*c[1],f*c[2]]; + } + + }; + + // same algorithms as in getMedian, but + // writes output directly to imageData array: + this.setMedian = function (data,id) { + // if needed, update index/bin to current median position: + while (this.medianindex> this.histogram[this.medianbin]) { + this.medianindex -= this.histogram[this.medianbin++]; + } + while (this.medianindex<1) { + this.medianindex += this.histogram[--this.medianbin]; + } + + // set the value ... + // as an average of bin entries: + + var c=this.colors[this.medianbin]; + switch (this.histogram[this.medianbin]) { + case 1: + // only 1 entry, set color values as they are: + data[id]=c[0]; + data[id+1]=c[1]; + data[id+2]=c[2]; + break; + + case 2: + data[id]=c[0]>>1; + data[id+1]=c[1]>>1; + data[id+2]=c[2]>>1; + break; + + case 4: + data[id]=c[0]>>2; + data[id+1]=c[1]>>2; + data[id+2]=c[2]>>2; + break; + + default: + var f=1.0/this.histogram[this.medianbin]; + data[id] = f*c[0]; + data[id+1] = f*c[1]; + data[id+2] = f*c[2]; + } + + }; + + + this.clear = function() { + this.init(this.histogram.length); + }; + + // no used: + this.getEntries = function () { + var arr=[]; + this.entries = 0; + for (var i=0; i0) { + this.entries++; + for (var j=0; j>0; + return this.getMedian(); + }; + + + this.getMedian = function () { + // if needed, update index/bin to current median position: + while (this.medianindex> this.histogram[this.medianbin]) { + this.medianindex -= this.histogram[this.medianbin++]; + } + while (this.medianindex<1) { + this.medianindex += this.histogram[--this.medianbin]; + } + + // return the value ... + // as an average of bin entries: + return this.colors[this.medianbin]; + }; + + + this.setMedian = function (data, i) { + // if needed, update index/bin to current median position: + while (this.medianindex> this.histogram[this.medianbin]) { + this.medianindex -= this.histogram[this.medianbin++]; + } + while (this.medianindex<1) { + this.medianindex += this.histogram[--this.medianbin]; + } + + // return the value ... + // as an average of bin entries: + var c=this.colors[this.medianbin]; + data[i]=c[0]; + data[i+1]=c[1]; + data[i+2]=c[2]; + }; + + this.clear = function() { + this.init(this.histogram.length); + }; + + // no used: + this.getEntries = function () { + var arr=[]; + this.entries = 0; + for (var i=0; i0) { + this.entries++; + for (var j=0; j e; + +const OGLCode = '' + glsl` + vec4 v = texture2D(u_image, v_texCoord); + int kernelSize = ($halfSize*2 + 1)*($halfSize*2 + 1); + const int startInd = -$halfSize; + + vec4 meanColor = vec4(0.0, 0.0, 0.0, 0.0); + + for (int y = startInd; y <= $halfSize; y++) { + for (int x = startInd; x <= $halfSize; x++) { + vec4 v1 = texture2D(u_image, v_texCoord + vec2(x, y) * pixel); + vec4 v2 = texture2D(u_image, v_texCoord + vec2(-x, -y) * pixel); + vec4 d1 = abs(v - v1); + vec4 d2 = abs(v - v2); + vec4 rv = vec4(((d1[0] < d2[0]) ? v1[0] : v2[0]), + ((d1[1] < d2[1]) ? v1[1] : v2[1]), + ((d1[2] < d2[2]) ? v1[2] : v2[2]),1); + meanColor += rv; + } + } + gl_FragColor = meanColor / float(kernelSize); +`; + +const nearestNeighbor = { + nameLoc: 'blocks.processing.nearestNeighbor.name', + name: 'Nearest neighbor filter', + inputs: [{ + key: 'input', + type: 'canvas', + name: 'Input', + nameLoc: 'blocks.processing.nearestNeighbor.input' + }, { + key: 'size', + type: 'number', + name: 'Size', + hint: 'A number of kernel in pixels. Should be 2 or more.', + nameLoc: 'blocks.processing.nearestNeighbor.size' + }], + outputs: [{ + key: 'output', + type: 'canvas', + name: 'Output', + nameLoc: 'blocks.processing.nearestNeighbor.output', + }], + exec(inputs, block) { + return new Promise((resolve, reject) => { + const inp = inputs.input, + {size} = inputs; + const shot = OGLCode.replace(/\$halfSize/g, Math.floor(size / 2).toString()); + (new OSWebGL(shot, { + iterations: 'int' + })).render(inp) + .then(image => { + resolve({ + output: image + }); + }) + .catch(err => { + console.error(err); + reject(new BlockError(err, block)); + }); + }); + } +}; + +module.exports = { + nearestNeighbor +}; diff --git a/src/js/blocks/processing/skinColors.ts b/src/js/blocks/processing/skinColors.ts new file mode 100644 index 0000000..57acb92 --- /dev/null +++ b/src/js/blocks/processing/skinColors.ts @@ -0,0 +1,66 @@ +import Channel = require('./../../types/Channel.js'); + +const getSkinColors = { + nameLoc: 'blocks.composing.getSkinColors.name', + name: 'Skin colors', + noPreview: true, + inputs: [{ + key: 'input', + type: 'pixels', + name: 'Input', + nameLoc: 'blocks.composing.getSkinColors.input' + }], + outputs: [{ + key: 'output', + type: 'channel', + name: 'Skin mask', + nameLoc: 'blocks.composing.getSkinColors.output' + }], + tags: [{ + tag: 'select-input', + key: 'method', + defaultValue: 'J. Kovac (uniform daylight)', + options: [ + 'J. Kovac (uniform daylight)', + 'J. Kovac (flash light)' + ] + }], + exec(inputs, block) { + return new Promise((resolve, reject) => { + const inp = inputs.input; + const w = inp.width, + h = inp.height; + const out = new Channel(w, h); + for (let x = 0; x < w; x++) { + for (let y = 0; y < h; y++) { + const ind = x + y*w; + const r = inp.data[ind*4 + 0], + g = inp.data[ind*4 + 1], + b = inp.data[ind*4 + 2]; + out.data[ind] = 0; + if (block.tagValues.method === 'J. Kovac (uniform daylight)') { + if (r > 95 && g > 40 && b > 20 && + (Math.max(r, g, b) - Math.min(r, g, b) > 15) && + (Math.abs(r - g) > 15) && + r > b && g > b) { + out.data[ind] = 255; + } + } else if (block.tagValues.method === 'J. Kovac (flash light)') { + if (r > 220 && g > 210 && b > 170 && + (Math.abs(r - g) <= 15) && + r > b && g > b) { + out.data[ind] = 255; + } + } + } + } + resolve({ + output: out + }); + }); + } +}; + +module.exports = { + getSkinColors +}; diff --git a/src/js/oneShotWebGL.ts b/src/js/oneShotWebGL.ts index e9cd146..d624153 100644 --- a/src/js/oneShotWebGL.ts +++ b/src/js/oneShotWebGL.ts @@ -1,14 +1,15 @@ // tslint:disable:member-ordering +// tslint:disable-next-line: no-implicit-dependencies const fs = require('fs-extra'); const vectorShader = fs.readFileSync('./js/vertexShader.glsl', { encoding: 'utf-8' }), -fragmentTemplate = fs.readFileSync('./js/fragmentShader.glsl', { - encoding: 'utf-8' -}); + fragmentTemplate = fs.readFileSync('./js/fragmentShader.glsl', { + encoding: 'utf-8' + }); const VERT = WebGLRenderingContext.VERTEX_SHADER, - FRAG = WebGLRenderingContext.FRAGMENT_SHADER; + FRAG = WebGLRenderingContext.FRAGMENT_SHADER; const getRGB = (hex: string) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); @@ -26,7 +27,11 @@ class Renderer { private fragShader: WebGLShader; private program: WebGLProgram; private paramNotation: object; - constructor(fragCode: string, params?: object) { + constructor(fragCode: string, params?: object, functions?: string) { + if (!functions && typeof params === 'string') { + functions = params; + params = {}; + } this.paramNotation = params || {}; const gl = this.gl; this.canvas.addEventListener('webglcontextlost', e => { @@ -34,17 +39,18 @@ class Renderer { }, false); this.canvas.addEventListener('webglcontextrestored', this.setup.bind(this, [fragCode, this.paramNotation]), false); this.canvas.width = this.canvas.height = 1; - this.setup(fragCode, this.paramNotation); + this.setup(fragCode, this.paramNotation, functions); return this; } - setup(fragCode: string, params?: object) { + setup(fragCode: string, params?: object, functions?: string) { this.gl = this.canvas.getContext('webgl2'); const gl = this.gl; this.vecShader = gl.createShader(VERT); this.fragShader = gl.createShader(FRAG); this.program = gl.createProgram(); var fragSource = fragmentTemplate; - fragSource = fragSource.replace('${source}', fragCode); + // tslint:disable-next-line: no-invalid-template-strings + fragSource = fragSource.replace('${source}', fragCode).replace('${functions}', functions || ''); var uniformsCode = ''; params = params || {}; for (const key in params) { @@ -58,8 +64,11 @@ class Renderer { uniformsCode += `uniform vec2 ${key};\n`; } else if (params[key] === 'vec4') { uniformsCode += `uniform vec4 ${key};\n`; + } else if (params[key].indexOf('int') !== -1) { + uniformsCode += `uniform ${params[key]} ${key};\n`; } } + // tslint:disable-next-line: no-invalid-template-strings fragSource = fragSource.replace('${uniforms}', uniformsCode); gl.shaderSource(this.fragShader, fragSource); gl.shaderSource(this.vecShader, vectorShader); @@ -67,10 +76,12 @@ class Renderer { gl.compileShader(this.vecShader); if (!gl.getShaderParameter(this.fragShader, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(this.fragShader)); + console.error(fragSource); return null; } if (!gl.getShaderParameter(this.vecShader, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(this.vecShader)); + console.error(fragSource); return null; } gl.attachShader(this.program, this.vecShader); @@ -82,7 +93,7 @@ class Renderer { return null; } } - render(image: HTMLImageElement|HTMLCanvasElement, params?: object, positions?: Array) { + render(image: HTMLImageElement | HTMLCanvasElement, params?: object, positions?: Array) { return new Promise((resolve, reject) => { this.canvas.width = image.width; this.canvas.height = image.height; @@ -109,12 +120,12 @@ class Renderer { const texCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ - 0.0, 0.0, - 1.0, 0.0, - 0.0, 1.0, - 0.0, 1.0, - 1.0, 0.0, - 1.0, 1.0 + 0.0, 0.0, + 1.0, 0.0, + 0.0, 1.0, + 0.0, 1.0, + 1.0, 0.0, + 1.0, 1.0 ]), gl.STATIC_DRAW); // Create a texture. @@ -162,7 +173,7 @@ class Renderer { const [r, g, b] = getRGB(param); gl.uniform3f(loc, r / 256, g / 256, b / 256); } else if (typeof param === 'boolean') { - gl.uniform1f(loc, param? 1 : 0); + gl.uniform1f(loc, param ? 1 : 0); } else if (param && typeof param[Symbol.iterator] === 'function') { // an array or such if (param.length === 1) { gl.uniform1f(loc, param[0]); diff --git a/src/js/types/BlockError.ts b/src/js/types/BlockError.ts index 2028fac..b8f4973 100644 --- a/src/js/types/BlockError.ts +++ b/src/js/types/BlockError.ts @@ -3,6 +3,9 @@ class BlockError extends Error { constructor(message: string, block: Block) { super(message); this.block = block; + console.error(this); + // tslint:disable-next-line: no-console + console.trace(); } } diff --git a/src/js/types/Channel.ts b/src/js/types/Channel.ts index ecb387e..fd9c051 100644 --- a/src/js/types/Channel.ts +++ b/src/js/types/Channel.ts @@ -1,10 +1,11 @@ class Channel { width: number; height: number; - data: Array = []; + data: Uint8Array; constructor(w: number, h: number) { this.width = w; this.height = h; + this.data = new Uint8Array(w * h); } } diff --git a/src/js/types/Filter.ts b/src/js/types/Filter.ts index 851379a..c39e8cb 100644 --- a/src/js/types/Filter.ts +++ b/src/js/types/Filter.ts @@ -55,7 +55,12 @@ class Filter { input = this.addBlock('inputImage', -200, 40); output.addLink('image', input, 'image'); - this.exec = () => output.exec(); + this.exec = () => output.exec() + .catch(err => { + console.error(err); + // tslint:disable-next-line: no-console + console.trace(); + }); } addBlock(templateKey, x = 0, y = 0) { const block = new Block(templateKey, x, y); diff --git a/src/riotTags/editor-screen.tag b/src/riotTags/editor-screen.tag index 5775323..db4fdd6 100644 --- a/src/riotTags/editor-screen.tag +++ b/src/riotTags/editor-screen.tag @@ -145,7 +145,7 @@ editor-screen }; this.copyToClipboard = e => { var clip = nw.Clipboard.get(); - clip.set(this.currentResult.toDataURL(), 'png'); + clip.set(this.currentResult.toDataURL? this.currentResult.toDataURL() : this.currentResult.src, 'png'); }; this.exportPNG = e => { this.imageSaveFormat = 'png'; @@ -160,9 +160,9 @@ editor-screen this.finishExportImage = e => { var data; if (this.imageSaveFormat === 'jpg') { - data = this.currentResult.toDataURL('image/jpeg'); + data = this.currentResult.toDataURL? this.currentResult.toDataURL('image/jpeg') : this.currentResult.src; } else { - data = this.currentResult.toDataURL(); + data = this.currentResult.toDataURL? this.currentResult.toDataURL() : this.currentResult.src; } data = data.replace(/^data:image\/\w+;base64,/, ''); var buff = new Buffer(data, 'base64'); diff --git a/src/riotTags/graph-editor.tag b/src/riotTags/graph-editor.tag index 563609e..4508ffd 100644 --- a/src/riotTags/graph-editor.tag +++ b/src/riotTags/graph-editor.tag @@ -79,6 +79,7 @@ graph-editor(style="background-position: {this.view.x + this.width/2}px {this.vi }); this.keyListener = e => { + console.log(e); if ((e.key === 'Backspace' || e.key === 'Delete') && glob.selectedBlocks.length) { var blockTagMap = glob.selectedBlocks.map(tag => tag.block); // firstly, delete all the blocks from the graph @@ -105,7 +106,10 @@ graph-editor(style="background-position: {this.view.x + this.width/2}px {this.vi this.update(); } }; - window.addEventListener('keypress', this.keyListener); + window.addEventListener('keydown', this.keyListener); + this.on('unmount', () => { + window.removeEventListener('keydown', this.keyListener); + }); this.updateLinks = () => { cx.clearRect(0, 0, this.width, this.height); diff --git a/src/riotTags/inputs/select-input.tag b/src/riotTags/inputs/select-input.tag index d28f09c..8c159cb 100644 --- a/src/riotTags/inputs/select-input.tag +++ b/src/riotTags/inputs/select-input.tag @@ -1,4 +1,5 @@ select-input + span(if="{opts.label}") {opts.label} select(ref="input" onchange="{onChange}" value="{option}") option(each="{option in opts.options}" selected="{option === parent.parent.block.tagValues[editedKey]}" value="{option}") {option} script. diff --git a/src/styl/tags/editor-screen.styl b/src/styl/tags/editor-screen.styl index 2a2d576..ecab69a 100644 --- a/src/styl/tags/editor-screen.styl +++ b/src/styl/tags/editor-screen.styl @@ -47,7 +47,6 @@ editor-screen max-width 100% max-height 100% min-width 100% - min-height 100% text-align center padding 1rem box-sizing border-box @@ -62,7 +61,7 @@ editor-screen #theEditingPanel display grid grid-gap 1px - height 100% + height calc(100% - 4rem) background borders flex 1 1 auto position relative