├── .babelrc ├── .npmignore ├── .codeclimate.yml ├── src └── app │ ├── script │ ├── component │ │ ├── ViewPanel.jsx │ │ ├── MainView.jsx │ │ ├── KeyboardShortcut.jsx │ │ ├── tool │ │ │ ├── Quantization.jsx │ │ │ ├── SampleRate.jsx │ │ │ ├── BitPlane.jsx │ │ │ ├── Grayscale.jsx │ │ │ ├── HistogramEqualization.jsx │ │ │ ├── Binarization.jsx │ │ │ ├── Histogram.jsx │ │ │ ├── ChannelAdjust.jsx │ │ │ ├── Statistics.jsx │ │ │ └── Convolve.jsx │ │ ├── Preview.jsx │ │ ├── HistoryItem.jsx │ │ ├── StatusPanel.jsx │ │ ├── HistoryPanel.jsx │ │ ├── ToolPanel.jsx │ │ └── TitleBar.jsx │ ├── dispatcher │ │ └── mangekyouDispatcher.jsx │ ├── keyMap.js │ ├── constant │ │ └── mangekyouConstant.jsx │ ├── mangekyou.js │ ├── worker │ │ ├── Quantization.js │ │ ├── BitPlane.js │ │ ├── SampleRate.js │ │ ├── Grayscale.js │ │ ├── Binarization.js │ │ ├── util.js │ │ ├── ChannelAdjust.js │ │ ├── worker.js │ │ ├── Statistics.js │ │ ├── Convolve.js │ │ ├── HistogramEqualization.js │ │ └── ColorConversion.js │ ├── action │ │ └── mangekyouAction.jsx │ ├── app.jsx │ └── store │ │ └── mangekyouStore.jsx │ ├── index.html │ ├── style │ ├── general.scss │ └── font.scss │ ├── bin │ └── mangekyou │ └── main.js ├── .gitignore ├── .imdone └── config.json ├── .travis.yml ├── package.json ├── README.md ├── gulpfile.babel.js ├── .eslintrc └── LICENSE /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore all 2 | /* 3 | /*/ 4 | 5 | # just keep builded stuff 6 | !/build/ 7 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | ratings: 5 | paths: 6 | - src/app/*.js 7 | - src/app/*.jsx 8 | - src/app/**/*.js 9 | - src/app/**/*.jsx 10 | - gulpfile.babel.js 11 | -------------------------------------------------------------------------------- /src/app/script/component/ViewPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ViewPanel = React.createClass({ 4 | render() { 5 | return ( // eslint-disable-line no-extra-parens 6 |

View Panel Body

7 | ); 8 | }, 9 | }); 10 | 11 | export default ViewPanel; 12 | -------------------------------------------------------------------------------- /src/app/script/dispatcher/mangekyouDispatcher.jsx: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | 3 | const mangekyouDispatcher = new Dispatcher(); 4 | 5 | Object.assign( 6 | mangekyouDispatcher, 7 | { 8 | handleAction(action) { 9 | this.dispatch({ 10 | source: 'VIEW_ACTION', 11 | action, 12 | }); 13 | }, 14 | } 15 | ); 16 | 17 | export default mangekyouDispatcher; 18 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mangekyou 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/script/keyMap.js: -------------------------------------------------------------------------------- 1 | import mangekyouAction from './action/mangekyouAction'; 2 | 3 | const keyMap = [ 4 | { 5 | char: 'h', 6 | action: () => { mangekyouAction.triggerShowing('historyPanel'); }, 7 | }, { 8 | char: 't', 9 | action: () => { mangekyouAction.triggerShowing('toolPanel'); }, 10 | }, { 11 | char: 'a', 12 | action: () => { mangekyouAction.triggerShowing('statusPanel'); }, 13 | }, 14 | ]; 15 | 16 | export default keyMap; 17 | -------------------------------------------------------------------------------- /src/app/script/constant/mangekyouConstant.jsx: -------------------------------------------------------------------------------- 1 | const mangekyouConstant = { 2 | NEW_IMAGE: 'NEW_IMAGE', 3 | ADD_HISTORY: 'ADD_HISTORY', 4 | LOAD_HISTORY: 'LOAD_HISTORY', 5 | UPDATE_PREVIEW: 'UPDATE_PREVIEW', 6 | TRIGGER_SHOWING: 'TRIGGER_SHOWING', 7 | SET_PROCESSING_STATE: 'SET_PROCESSING_STATE', 8 | TRIGGER_COMPUTE: 'TRIGGER_COMPUTE', 9 | LOADING_FILE: 'LOADING_FILE', 10 | COMPUTING: 'COMPUTING', 11 | IDLE: 'IDLE', 12 | }; 13 | 14 | export default mangekyouConstant; 15 | -------------------------------------------------------------------------------- /src/app/style/general.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | ::-webkit-scrollbar { 7 | width: 12px; 8 | height: 5px; 9 | background: transparent; 10 | } 11 | 12 | ::-webkit-scrollbar-thumb { 13 | background: rgba(0, 0, 0, 0.2); 14 | } 15 | 16 | ::-webkit-scrollbar-thumb:hover, 17 | ::-webkit-scrollbar-thumb:active { 18 | background: rgba(0, 0, 0, 0.4); 19 | } 20 | 21 | @keyframes logoRotate { 22 | from { transform: scale(1.2) rotate(0deg); } 23 | to { transform: scale(1.2) rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/script/mangekyou.js: -------------------------------------------------------------------------------- 1 | // emulate a full ES6 environment 2 | // http://babeljs.io/docs/usage/polyfill/ 3 | import 'babel-polyfill'; 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import app from './app'; 8 | 9 | // Needed for onTouchTap 10 | // Can go away when react 1.0 release 11 | // Check this repo: 12 | // https://github.com/zilverline/react-tap-event-plugin 13 | import injectTapEventPlugin from 'react-tap-event-plugin'; 14 | injectTapEventPlugin(); 15 | 16 | // render on dov#app 17 | ReactDOM.render(React.createElement(app), document.getElementById('app')); 18 | -------------------------------------------------------------------------------- /src/app/bin/mangekyou: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // script from http://blog.soulserv.net/building-a-package-featuring-electron-as-a-stand-alone-application/ 3 | 4 | // It just returns a path 5 | var electronPath = require('electron-prebuilt'); 6 | 7 | var childProcess = require('child_process'); 8 | 9 | // Adjust the command line arguments: remove the "node " part 10 | var args = process.argv.slice(2); 11 | // ... and insert the root path of our application 12 | // as the first argument 13 | args.unshift(__dirname + '/../../../'); 14 | 15 | // Run electron 16 | childProcess.spawn(electronPath, args, { stdio: 'inherit' }); 17 | -------------------------------------------------------------------------------- /src/app/script/worker/Quantization.js: -------------------------------------------------------------------------------- 1 | import {getAllPositions} from './util'; 2 | 3 | function levelize(level, value) { 4 | // quantilize value [0, 255] into [1, 256] levels result 5 | return Math.floor(value - value % (256 / level)); 6 | } 7 | 8 | function Quantization({width, height, data}, {level}) { 9 | const allPos = getAllPositions(width, height); 10 | const lv = levelize.bind(this, level); 11 | for (const [,, index] of allPos()) { 12 | data[index] = lv(data[index]); // red 13 | data[index + 1] = lv(data[index + 1]); // green 14 | data[index + 2] = lv(data[index + 2]); // blue 15 | } 16 | return {width, height, data}; 17 | } 18 | 19 | export default Quantization; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | bower_components 29 | 30 | # Dolphin 31 | .directory 32 | 33 | # builded app 34 | build/ 35 | 36 | # travis releasing temp folder 37 | ghpage 38 | 39 | npm-debug.log 40 | -------------------------------------------------------------------------------- /src/app/script/component/MainView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ToolPanel from './ToolPanel'; 3 | import HistoryPanel from './HistoryPanel'; 4 | import StatusPanel from './StatusPanel'; 5 | import Preview from './Preview'; 6 | 7 | const MainView = React.createClass({ 8 | render() { 9 | return ( // eslint-disable-line no-extra-parens 10 |
20 | 21 | 22 | 23 | 24 |
25 | ); 26 | }, 27 | }); 28 | 29 | export default MainView; 30 | -------------------------------------------------------------------------------- /src/app/script/worker/BitPlane.js: -------------------------------------------------------------------------------- 1 | import {getAllPositions} from './util'; 2 | 3 | function getRange(depth, planeIndex) { 4 | const unit = 256 / depth; 5 | return [Math.floor(unit * planeIndex), Math.floor(unit * (planeIndex + 1))]; 6 | } 7 | 8 | function isBetween(lowerBound, upperBound, value) { 9 | return value >= lowerBound && value < upperBound; 10 | } 11 | 12 | function BitPlane({width, height, data}, {depth, planeIndex}) { 13 | const bi = isBetween.bind(this, ...getRange(depth, planeIndex)); 14 | const allPos = getAllPositions(width, height); 15 | for (const [,, index] of allPos()) { 16 | data[index] = bi(data[index]) ? 255 : 0; 17 | data[index + 1] = bi(data[index + 2]) ? 255 : 0; 18 | data[index + 2] = bi(data[index + 2]) ? 255 : 0; 19 | } 20 | return {width, height, data}; 21 | } 22 | 23 | export default BitPlane; 24 | -------------------------------------------------------------------------------- /src/app/script/worker/SampleRate.js: -------------------------------------------------------------------------------- 1 | import {getCoordinate, getAllPositions} from './util'; 2 | 3 | // resample input image by given sample distance 4 | function SampleRate({width, height, data}, {distance}) { 5 | const cord = getCoordinate(width); 6 | const oWidth = Math.floor(width / distance); 7 | const oHeight = Math.floor(height / distance); 8 | const oAllPos = getAllPositions(oWidth, oHeight); 9 | const oData = new Uint8ClampedArray(4 * oWidth * oHeight); 10 | 11 | for (const [oX, oY, oIndex] of oAllPos()) { 12 | const index = cord(oX * distance, oY * distance); 13 | oData[oIndex] = data[index]; // red 14 | oData[oIndex + 1] = data[index + 1]; // green 15 | oData[oIndex + 2] = data[index + 2]; // blud 16 | oData[oIndex + 3] = data[index + 3]; // alpha 17 | } 18 | 19 | return { 20 | width: oWidth, 21 | height: oHeight, 22 | data: oData, 23 | }; 24 | } 25 | 26 | export default SampleRate; 27 | -------------------------------------------------------------------------------- /.imdone/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "^(node_modules|bower_components|\\.imdone|target|build|dist|logs)[\\/\\\\]?|\\.(git|svn|hg|npmignore)|\\~$|\\.(jpg|png|gif|swp|ttf|otf)$" 4 | ], 5 | "watcher": true, 6 | "code": { 7 | "include_lists": [ 8 | "TODO", 9 | "DOING", 10 | "DONE", 11 | "PLANNING", 12 | "FIXME", 13 | "ARCHIVE", 14 | "HACK", 15 | "CHANGED", 16 | "XXX", 17 | "IDEA", 18 | "NOTE", 19 | "REVIEW" 20 | ] 21 | }, 22 | "lists": [ 23 | { 24 | "name": "TODO", 25 | "hidden": false 26 | }, 27 | { 28 | "name": "FIXME", 29 | "hidden": false 30 | }, 31 | { 32 | "name": "HACK", 33 | "hidden": false 34 | } 35 | ], 36 | "marked": { 37 | "gfm": true, 38 | "tables": true, 39 | "breaks": false, 40 | "pedantic": false, 41 | "smartLists": true, 42 | "langPrefix": "language-" 43 | } 44 | } -------------------------------------------------------------------------------- /src/app/script/worker/Grayscale.js: -------------------------------------------------------------------------------- 1 | // disable new-cap linting for color space name usage in function name 2 | /* eslint-disable new-cap */ 3 | 4 | import {getAllPositions, maxOf, minOf} from './util'; 5 | import {luma709, luma601, RGBToHSL} from './ColorConversion'; 6 | 7 | /* eslint-disable key-spacing */ 8 | // avalible RGB grayscale algorithms 9 | const methods = { 10 | rec709 : (r, g, b) => luma709(r, g, b), 11 | rec601 : (r, g, b) => luma601(r, g, b), 12 | hsl : (r, g, b) => Math.round(RGBToHSL(r / 255, g / 255, b / 255)[2] * 255), 13 | average: (r, g, b) => 1 / 3 * (r + g + b), 14 | max : (r, g, b) => maxOf(r, g, b), 15 | min : (r, g, b) => minOf(r, g, b), 16 | }; 17 | /* eslint-enable key-spacing */ 18 | 19 | function Grayscale({width, height, data}, {method}) { 20 | const allPos = getAllPositions(width, height); 21 | const getLuma = methods[method]; 22 | for (const [,, index] of allPos()) { 23 | const [r, g, b] = [data[index], data[index + 1], data[index + 2]]; 24 | const lum = getLuma(r, g, b); 25 | data[index] = lum; 26 | data[index + 1] = lum; 27 | data[index + 2] = lum; 28 | } 29 | return {width, height, data}; 30 | } 31 | 32 | export default Grayscale; 33 | -------------------------------------------------------------------------------- /src/app/main.js: -------------------------------------------------------------------------------- 1 | import {app, ipcMain as ipc} from 'electron'; 2 | import BrowserWindow from 'browser-window'; 3 | 4 | require('crash-reporter').start(); 5 | 6 | let mainWindow = null; 7 | 8 | app.on('window-all-closed', () => { 9 | app.quit(); 10 | }); 11 | 12 | app.on('ready', () => { 13 | const mangekyouWindow = { 14 | width: 800, 15 | height: 600, 16 | 'min-width': 800, 17 | 'min-height': 600, 18 | frame: false, 19 | show: false, 20 | }; 21 | 22 | mainWindow = new BrowserWindow(mangekyouWindow); 23 | 24 | // for product 25 | // disable default electron menu 26 | // mainWindow.setMenu(null); 27 | 28 | mainWindow.loadURL(`file://${__dirname}/index.html`); 29 | mainWindow.show(); 30 | 31 | mainWindow.on('closed', () => { 32 | mainWindow = null; 33 | }); 34 | 35 | ipc.on('Wminimize', () => { 36 | mainWindow.minimize(); 37 | }); 38 | ipc.on('Wmaximize', () => { 39 | if (mainWindow.isMaximized()) { 40 | mainWindow.unmaximize(); 41 | } else { 42 | mainWindow.maximize(); 43 | } 44 | }); 45 | ipc.on('Wclose', () => { 46 | mainWindow.close(); 47 | }); 48 | ipc.on('Wreload', () => { 49 | mainWindow.reload(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/style/font.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(../font/MaterialIcons-Regular.eot); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url(../font/MaterialIcons-Regular.woff2) format('woff2'), 9 | url(../font/MaterialIcons-Regular.woff) format('woff'), 10 | url(../font/MaterialIcons-Regular.ttf) format('truetype'); 11 | } 12 | 13 | .material-icons { 14 | font-family: 'Material Icons'; 15 | font-weight: normal; 16 | font-style: normal; 17 | font-size: 24px; /* Preferred icon size */ 18 | display: inline-block; 19 | width: 1em; 20 | height: 1em; 21 | line-height: 1; 22 | color:initial; 23 | vertical-align: middle; 24 | text-transform: none; 25 | letter-spacing: normal; 26 | word-wrap: normal; 27 | white-space: nowrap; 28 | direction: ltr; 29 | 30 | /* Support for all WebKit browsers. */ 31 | -webkit-font-smoothing: antialiased; 32 | /* Support for Safari and Chrome. */ 33 | text-rendering: optimizeLegibility; 34 | 35 | /* Support for Firefox. */ 36 | -moz-osx-font-smoothing: grayscale; 37 | 38 | -webkit-font-feature-settings: 'liga'; 39 | /* Support for IE. */ 40 | font-feature-settings: 'liga'; 41 | } 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - secure: "Kva3mzmx2cSlo3C3bxghBZQxOxJGGU9qjNheRfqd/wv9Qhgo07aMXktq971W3q0dWEdXkszkc3QVK0jl2IJIbc0NDxRpeMn1nAiwHm5JMptqGNKhMu2N78F6ZjziUUpj0Nna44maMD7DXVlUl0yUqSvxfhHRUbh+yiqqw59mL472mSInsDWg06Fy5v5Cr64sD6p/jQGc8b6iDJTmvcczVaMPoD5duK+u7WS3lnsVG95dOSp9idDxrbDZhOyz8sz857xTuANbzpsFfx0NyHKGYlYs9ktTXG2Q10+bUAuzQ0mMXk2TTCiuyjCoTiUn4Mn8mvTUBn5149T+ohBu7fMeW2MOHjJSCCIBWKEKF1gRAjscdW7Sgon3unurLmmHg+8cGTglZ376raGkNMqqaxEOW6Qu5qIRYEad8bJNK52x/pDjYe39Vy9wzXemPiLVlVqoKEm2nnG9cavlSQ4wDN4IQCO3RhB/XiJ1G6mrDKeQQlVSD3NAD2k51g16HyKIff6OvOHmvQbLEgc+ZOs7qUEmQNEWvekaB5bnVXLzr9e5tZxPd0ZW744ikDTddVRvFzaBUXibY7P/Hf8MwH1hrSVajryxodWaJp64Z0u9CS2ozsKjkLW/YF7OrqT3AgzgDbTwLRbKX7CzobbU23B3Q+UqF5iz7EVuVbYJpRTHYdu9l3I=" 3 | language: node_js 4 | node_js: 5 | - "node" 6 | branches: 7 | only: 8 | - release 9 | install: true 10 | script: 11 | - git config --global user.email "$GIT_EMAIL" 12 | - git config --global user.name "$GIT_NAME" 13 | - git fetch 14 | - git checkout release 15 | - npm install 16 | - npm run build 17 | after_success: 18 | - git clone https://$GH_TOKEN@github.com/frantic1048/mangekyou.git ghpage 19 | - cd ghpage 20 | - git checkout gh-pages 21 | - cp -rf ../build/app/* ./ 22 | - git add -A . 23 | - git commit --amend -m '⚡୧| ” •̀ ل͜ •́ ” |୨⚡ Trace...ON !' 24 | - git push --quiet --force origin gh-pages 25 | -------------------------------------------------------------------------------- /src/app/script/worker/Binarization.js: -------------------------------------------------------------------------------- 1 | // disable new-cap linting for color space name usage in function name 2 | /* eslint-disable new-cap */ 3 | 4 | import {RGBToHSL, 5 | RGBToHSV, 6 | RGBToHSY709, 7 | RGBToHSY601} from './ColorConversion'; 8 | import {getAllPositions} from './util'; 9 | 10 | function Binarization({width, height, data}, {space, channelIndex, threshold}) { 11 | const allPos = getAllPositions(width, height); 12 | 13 | // convert RGB -> Specified color space 14 | let RGBToSpec; 15 | 16 | switch (space) { 17 | case 'hsl': 18 | RGBToSpec = RGBToHSL; 19 | break; 20 | case 'hsv': 21 | RGBToSpec = RGBToHSV; 22 | break; 23 | case 'hsy709': 24 | RGBToSpec = RGBToHSY709; 25 | break; 26 | case 'hsy601': 27 | RGBToSpec = RGBToHSY601; 28 | break; 29 | default: 30 | break; 31 | } 32 | 33 | for (const [,, index] of allPos()) { 34 | const sPixel = RGBToSpec(data[index] / 255, 35 | data[index + 1] / 255, 36 | data[index + 2] / 255); 37 | 38 | // apply white/black by threshold 39 | if (sPixel[channelIndex] * 255 > threshold) { 40 | data[index] = 255; 41 | data[index + 1] = 255; 42 | data[index + 2] = 255; 43 | } else { 44 | data[index] = 0; 45 | data[index + 1] = 0; 46 | data[index + 2] = 0; 47 | } 48 | } 49 | 50 | return {width, height, data}; 51 | } 52 | 53 | export default Binarization; 54 | -------------------------------------------------------------------------------- /src/app/script/component/KeyboardShortcut.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const KeyboardShortcut = React.createClass({ 4 | propTypes: { 5 | descriptors: React.PropTypes.arrayOf( 6 | React.PropTypes.shape({ 7 | action: React.PropTypes.func.isRequired, 8 | char: React.PropTypes.string, 9 | ctrl: React.PropTypes.bool, 10 | alt: React.PropTypes.bool, 11 | shift: React.PropTypes.bool, 12 | }) 13 | ).isRequired, 14 | }, 15 | getInitialState() { 16 | return { 17 | listeners: this.props.descriptors.map(this._KeyListenerGen), 18 | }; 19 | }, 20 | componentDidMount() { 21 | for (const listener of this.state.listeners) { 22 | window.addEventListener('keydown', listener); 23 | } 24 | }, 25 | componentWillUnmount() { 26 | for (const listener of this.state.listeners) { 27 | window.removeEventListener('keydown', listener); 28 | } 29 | }, 30 | render() { 31 | return
; 32 | }, 33 | _KeyListenerGen({ 34 | action = () => {}, 35 | ctrl = false, 36 | alt = false, 37 | shift = false, 38 | char = '', 39 | }) { 40 | return (ev) => { 41 | if (ev.ctrlKey === ctrl 42 | && ev.altKey === alt 43 | && ev.shiftKey === shift 44 | && String.fromCharCode(ev.keyCode).toLowerCase() === char 45 | ) { 46 | action(); 47 | } 48 | }; 49 | }, 50 | }); 51 | 52 | export default KeyboardShortcut; 53 | -------------------------------------------------------------------------------- /src/app/script/action/mangekyouAction.jsx: -------------------------------------------------------------------------------- 1 | import mangekyouDispatcher from './../dispatcher/mangekyouDispatcher'; 2 | import mangekyouConstant from './../constant/mangekyouConstant'; 3 | 4 | const mangekyouAction = { 5 | newImage(image) { 6 | mangekyouDispatcher.handleAction({ 7 | actionType: mangekyouConstant.NEW_IMAGE, 8 | data: image, 9 | }); 10 | }, 11 | addHistory({operation, operationDisplayName, image}) { 12 | mangekyouDispatcher.handleAction({ 13 | actionType: mangekyouConstant.ADD_HISTORY, 14 | data: {operation, operationDisplayName, image}, 15 | }); 16 | }, 17 | loadHistory(index) { 18 | mangekyouDispatcher.handleAction({ 19 | actionType: mangekyouConstant.LOAD_HISTORY, 20 | data: index, 21 | }); 22 | }, 23 | setProcessingState(processingState) { 24 | mangekyouDispatcher.handleAction({ 25 | actionType: mangekyouConstant.SET_PROCESSING_STATE, 26 | data: processingState, 27 | }); 28 | }, 29 | updatePreviewImage(image) { 30 | mangekyouDispatcher.handleAction({ 31 | actionType: mangekyouConstant.UPDATE_PREVIEW, 32 | data: image, 33 | }); 34 | }, 35 | triggerShowing(componentName) { 36 | mangekyouDispatcher.handleAction({ 37 | actionType: mangekyouConstant.TRIGGER_SHOWING, 38 | data: componentName, 39 | }); 40 | }, 41 | triggerCompute() { 42 | mangekyouDispatcher.handleAction({ 43 | actionType: mangekyouConstant.TRIGGER_COMPUTE, 44 | }); 45 | }, 46 | }; 47 | 48 | export default mangekyouAction; 49 | -------------------------------------------------------------------------------- /src/app/script/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ThemeManager from 'material-ui/lib/styles/theme-manager'; 3 | import LightRawTheme from 'material-ui/lib/styles/raw-themes/light-raw-theme'; 4 | import Colors from 'material-ui/lib/styles/colors'; 5 | import TitleBar from './component/TitleBar'; 6 | import MainView from './component/MainView'; 7 | import KeyboardShortcut from './component/KeyboardShortcut'; 8 | import keyMap from './keyMap'; 9 | 10 | const main = React.createClass({ 11 | childContextTypes: { 12 | muiTheme: React.PropTypes.object, 13 | }, 14 | getInitialState() { 15 | let mangekyouTheme; 16 | mangekyouTheme = ThemeManager.getMuiTheme(LightRawTheme); 17 | mangekyouTheme = ThemeManager.modifyRawThemePalette(mangekyouTheme, { 18 | primary1Color: Colors.lightBlue500, 19 | primary2Color: Colors.lightBlue500, 20 | primary3Color: Colors.lightBlack, 21 | accent1Color: Colors.pinkA200, 22 | accent2Color: Colors.grey100, 23 | accent3Color: Colors.grey500, 24 | }); 25 | return { 26 | muiTheme: mangekyouTheme, 27 | }; 28 | }, 29 | getChildContext() { 30 | return { 31 | muiTheme: this.state.muiTheme, 32 | }; 33 | }, 34 | render() { 35 | return ( // eslint-disable-line no-extra-parens 36 |
37 | 38 | 39 | 40 |
41 | ); 42 | }, 43 | }); 44 | 45 | export default main; 46 | -------------------------------------------------------------------------------- /src/app/script/component/tool/Quantization.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Slider from 'material-ui/lib/slider'; 3 | import mangekyouStore from '../../store/mangekyouStore'; 4 | 5 | const Quantization = React.createClass({ 6 | propTypes: { 7 | willProcess: React.PropTypes.func.isRequired, 8 | currentImage: React.PropTypes.shape({ 9 | width: React.PropTypes.number.isRequired, 10 | height: React.PropTypes.number.isRequired, 11 | }), 12 | }, 13 | getInitialState() { 14 | return { 15 | param: { 16 | level: 256, 17 | }, 18 | }; 19 | }, 20 | componentDidMount() { 21 | mangekyouStore.addComputeListener(this._compute); 22 | }, 23 | componentWillUnmount() { 24 | mangekyouStore.removeComputeListener(this._compute); 25 | }, 26 | render() { 27 | return ( // eslint-disable-line no-extra-parens 28 |
29 | 40 |
41 | ); 42 | }, 43 | _handleChange(event, value) { 44 | this.setState({ 45 | param: { 46 | level: value, 47 | }, 48 | }); 49 | }, 50 | _compute() { 51 | this.props.willProcess({ 52 | operationName: 'Quantization', 53 | operationParam: this.state.param, 54 | }); 55 | }, 56 | }); 57 | 58 | export default Quantization; 59 | -------------------------------------------------------------------------------- /src/app/script/worker/util.js: -------------------------------------------------------------------------------- 1 | // General helpers 2 | 3 | // max one of input values 4 | function maxOf(...values) { 5 | return values.reduce((pre, cur) => Math.max(pre, cur), -Infinity); 6 | } 7 | 8 | // min one of input values 9 | function minOf(...values) { 10 | return values.reduce((pre, cur) => Math.min(pre, cur), Infinity); 11 | } 12 | 13 | // clamp input value between given range 14 | function clampBetween(value, lowerBound, upperBound) { 15 | let result; 16 | if ( value >= lowerBound && value <= upperBound) { 17 | result = value; 18 | } else if (value < lowerBound) { 19 | result = lowerBound; 20 | } else if ( value > upperBound) { 21 | result = upperBound; 22 | } 23 | return result; 24 | } 25 | 26 | // a Python like rangex() helper 27 | function* range(start = 0, end = 10, step = 1) { 28 | let cur = start; 29 | while (step > 0 ? cur < end : cur > end) { 30 | yield cur; 31 | cur += step; 32 | } 33 | } 34 | 35 | // Coordinate helper for pixel accesing of ImageData 36 | function getCoordinate(width) { 37 | return (x, y) => y * width * 4 + x * 4; 38 | } 39 | 40 | // ImageData helper 41 | // return a genetor function iterates all position of image 42 | function getAllPositions(width, height) { 43 | const positions = function* pos() { 44 | for (let y = 0; y < height; ++y) { 45 | for (let x = 0; x < width; ++x) { 46 | yield [x, y, y * width * 4 + x * 4]; 47 | } 48 | } 49 | }; 50 | positions[Symbol.iterator] = positions; 51 | return positions; 52 | } 53 | 54 | export { 55 | maxOf, 56 | minOf, 57 | clampBetween, 58 | getCoordinate, 59 | getAllPositions, 60 | range, 61 | }; 62 | -------------------------------------------------------------------------------- /src/app/script/component/tool/SampleRate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Slider from 'material-ui/lib/slider'; 3 | import mangekyouStore from '../../store/mangekyouStore'; 4 | 5 | const SampleRate = React.createClass({ 6 | propTypes: { 7 | willProcess: React.PropTypes.func.isRequired, 8 | currentImage: React.PropTypes.shape({ 9 | width: React.PropTypes.number.isRequired, 10 | height: React.PropTypes.number.isRequired, 11 | }), 12 | }, 13 | getInitialState() { 14 | return { 15 | param: { 16 | distance: 1, 17 | }, 18 | }; 19 | }, 20 | componentDidMount() { 21 | mangekyouStore.addComputeListener(this._compute); 22 | }, 23 | componentWillUnmount() { 24 | mangekyouStore.removeComputeListener(this._compute); 25 | }, 26 | render() { 27 | return ( // eslint-disable-line no-extra-parens 28 |
29 | 39 |
40 | ); 41 | }, 42 | _handleChange(event, value) { 43 | this.setState({ 44 | param: { 45 | distance: value, 46 | }, 47 | }); 48 | }, 49 | _compute() { 50 | this.props.willProcess({ 51 | operationName: 'SampleRate', 52 | operationParam: this.state.param, 53 | }); 54 | }, 55 | }); 56 | 57 | export default SampleRate; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mangekyou", 3 | "version": "0.7.32", 4 | "description": "简单的图像处理程序", 5 | "main": "build/app/main.js", 6 | "bin": { 7 | "mangekyou": "build/app/bin/mangekyou" 8 | }, 9 | "scripts": { 10 | "run": "electron ./", 11 | "dev": "gulp dev", 12 | "lint": "gulp lint", 13 | "build": "gulp ci" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/frantic1048/mangekyou.git" 18 | }, 19 | "author": "frantic1048", 20 | "license": "GPL-2.0+", 21 | "bugs": { 22 | "url": "https://github.com/frantic1048/mangekyou/issues" 23 | }, 24 | "homepage": "https://github.com/frantic1048/mangekyou#readme", 25 | "dependencies": { 26 | "electron-prebuilt": "^0.36.2" 27 | }, 28 | "devDependencies": { 29 | "events": "^1.1.0", 30 | "flux": "^2.1.1", 31 | "material-ui": "^0.14.1", 32 | "react": "^0.14.6", 33 | "react-dom": "^0.14.6", 34 | "react-tap-event-plugin": "^0.2.1", 35 | "babel-core": "^6.4.0", 36 | "babel-eslint": "^5.0.0-beta6", 37 | "babel-loader": "^6.2.1", 38 | "babel-polyfill": "^6.3.14", 39 | "babel-preset-es2015": "^6.3.13", 40 | "babel-preset-react": "^6.3.13", 41 | "babel-preset-stage-2": "^6.3.13", 42 | "electron-connect": "^0.3.4", 43 | "eslint-plugin-react": "^3.14.0", 44 | "gulp": "gulpjs/gulp#4.0", 45 | "gulp-babel": "^6.1.1", 46 | "gulp-eslint": "^1.1.1", 47 | "gulp-newer": "^1.1.0", 48 | "gulp-sass": "^2.1.1", 49 | "gulp-sourcemaps": "^1.6.0", 50 | "gulp-uglify": "^1.5.1", 51 | "gulp-webpack-sourcemaps": "^0.3.0", 52 | "json-loader": "^0.5.4", 53 | "newer": "^0.1.5", 54 | "vinyl-named": "^1.1.0", 55 | "webpack-stream": "^3.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/script/worker/ChannelAdjust.js: -------------------------------------------------------------------------------- 1 | // disable new-cap linting for color space name usage in function name 2 | /* eslint-disable new-cap */ 3 | 4 | import {RGBToHSL, HSLToRGB, 5 | RGBToHSV, HSVToRGB, 6 | RGBToHSY709, HSY709ToRGB, 7 | RGBToHSY601, HSY601ToRGB} from './ColorConversion'; 8 | import {getAllPositions} from './util'; 9 | 10 | function ChannelAdjust({width, height, data}, {space, delta}) { 11 | const allPos = getAllPositions(width, height); 12 | 13 | // function convert RGB <-> Specified 3-channel Color Space 14 | let RGBToSpec; 15 | let SpecToRGB; 16 | 17 | switch (space) { 18 | case 'rgb': 19 | RGBToSpec = (r, g, b) => [r, g, b]; 20 | SpecToRGB = (r, g, b) => [r, g, b]; 21 | break; 22 | case 'hsl': 23 | RGBToSpec = RGBToHSL; 24 | SpecToRGB = HSLToRGB; 25 | break; 26 | case 'hsv': 27 | RGBToSpec = RGBToHSV; 28 | SpecToRGB = HSVToRGB; 29 | break; 30 | case 'hsy709': 31 | RGBToSpec = RGBToHSY709; 32 | SpecToRGB = HSY709ToRGB; 33 | break; 34 | case 'hsy601': 35 | RGBToSpec = RGBToHSY601; 36 | SpecToRGB = HSY601ToRGB; 37 | break; 38 | default: 39 | break; 40 | } 41 | 42 | for (const [,, index] of allPos()) { 43 | // convert piexl to target color space 44 | const sPixel = RGBToSpec(data[index] / 255, 45 | data[index + 1] / 255, 46 | data[index + 2] / 255); 47 | 48 | // adjust channels by delta 49 | sPixel[0] += delta[0]; 50 | sPixel[1] += delta[1]; 51 | sPixel[2] += delta[2]; 52 | 53 | // convert back to RGB 54 | const xPixel = SpecToRGB(...sPixel); 55 | data[index] = xPixel[0] * 255; 56 | data[index + 1] = xPixel[1] * 255; 57 | data[index + 2] = xPixel[2] * 255; 58 | } 59 | 60 | return {width, height, data}; 61 | } 62 | 63 | export default ChannelAdjust; 64 | -------------------------------------------------------------------------------- /src/app/script/component/tool/BitPlane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RadioButtonGroup from 'material-ui/lib/radio-button-group'; 3 | import RadioButton from 'material-ui/lib/radio-button'; 4 | import mangekyouStore from '../../store/mangekyouStore'; 5 | import {range} from '../../worker/util'; 6 | 7 | const BitPlane = React.createClass({ 8 | propTypes: { 9 | willProcess: React.PropTypes.func.isRequired, 10 | currentImage: React.PropTypes.shape({ 11 | width: React.PropTypes.number.isRequired, 12 | height: React.PropTypes.number.isRequired, 13 | }), 14 | }, 15 | getInitialState() { 16 | return { 17 | param: { 18 | depth: 8, 19 | planeIndex: 0, 20 | }, 21 | indexOptions: [...range(0, 8)].map(v => { 22 | return {label: `第 ${v} 平面`, value: `${v}`, key: `${v}`}; 23 | }), 24 | }; 25 | }, 26 | componentDidMount() { 27 | mangekyouStore.addComputeListener(this._compute); 28 | this._compute(); 29 | }, 30 | componentWillUnmount() { 31 | mangekyouStore.removeComputeListener(this._compute); 32 | }, 33 | render() { 34 | return ( // eslint-disable-line no-extra-parens 35 |
36 |

8 个位平面

37 | 42 | {this.state.indexOptions.map(({key, value, label}) => ( // eslint-disable-line no-extra-parens 43 | 48 | ))} 49 | 50 |
51 | ); 52 | }, 53 | _handleChange(event, selected) { 54 | this.setState({ 55 | param: { 56 | ...this.state.param, 57 | planeIndex: selected, 58 | }, 59 | }, this._compute); 60 | }, 61 | _compute() { 62 | this.props.willProcess({ 63 | operationName: 'BitPlane', 64 | operationParam: this.state.param, 65 | }); 66 | }, 67 | }); 68 | 69 | export default BitPlane; 70 | -------------------------------------------------------------------------------- /src/app/script/worker/worker.js: -------------------------------------------------------------------------------- 1 | // emulate a full ES6 environment 2 | // http://babeljs.io/docs/usage/polyfill/ 3 | import 'babel-polyfill'; 4 | 5 | /** image processing functions */ 6 | import SampleRate from './SampleRate'; 7 | import Quantization from './Quantization'; 8 | import Grayscale from './Grayscale'; 9 | import BitPlane from './BitPlane'; 10 | import Statistics from './Statistics'; 11 | import HistogramEqualization from './HistogramEqualization'; 12 | import Binarization from './Binarization'; 13 | import ChannelAdjust from './ChannelAdjust'; 14 | import Convolve from './Convolve'; 15 | 16 | const OUT_TYPE = { 17 | IMAGE: 'IMAGE', 18 | FREE: 'FREE', 19 | }; 20 | 21 | const op = { 22 | SampleRate: { 23 | func: SampleRate, 24 | outType: OUT_TYPE.IMAGE, 25 | }, 26 | Quantization: { 27 | func: Quantization, 28 | outType: OUT_TYPE.IMAGE, 29 | }, 30 | Grayscale: { 31 | func: Grayscale, 32 | outType: OUT_TYPE.IMAGE, 33 | }, 34 | BitPlane: { 35 | func: BitPlane, 36 | outType: OUT_TYPE.IMAGE, 37 | }, 38 | Statistics: { 39 | func: Statistics, 40 | outType: OUT_TYPE.FREE, 41 | }, 42 | HistogramEqualization: { 43 | func: HistogramEqualization, 44 | outType: OUT_TYPE.IMAGE, 45 | }, 46 | Binarization: { 47 | func: Binarization, 48 | outType: OUT_TYPE.IMAGE, 49 | }, 50 | ChannelAdjust: { 51 | func: ChannelAdjust, 52 | outType: OUT_TYPE.IMAGE, 53 | }, 54 | Convolve: { 55 | func: Convolve, 56 | outType: OUT_TYPE.IMAGE, 57 | }, 58 | }; 59 | 60 | self.onmessage = ({data: {operationName, image, operationParam}}) => { 61 | const workerResult = op[operationName].func(image, operationParam); 62 | switch (op[operationName].outType) { 63 | case OUT_TYPE.IMAGE: 64 | self.postMessage( 65 | { 66 | proceed: true, 67 | image: { 68 | width: workerResult.width, 69 | height: workerResult.height, 70 | buffer: workerResult.data.buffer, 71 | }, 72 | }, 73 | [workerResult.data.buffer] 74 | ); 75 | break; 76 | case OUT_TYPE.FREE: 77 | self.postMessage(...workerResult); 78 | break; 79 | default: 80 | // do nothing~ 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/app/script/component/tool/Grayscale.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RadioButtonGroup from 'material-ui/lib/radio-button-group'; 3 | import RadioButton from 'material-ui/lib/radio-button'; 4 | import mangekyouStore from '../../store/mangekyouStore'; 5 | 6 | const Grayscale = React.createClass({ 7 | propTypes: { 8 | willProcess: React.PropTypes.func.isRequired, 9 | currentImage: React.PropTypes.shape({ 10 | width: React.PropTypes.number.isRequired, 11 | height: React.PropTypes.number.isRequired, 12 | }), 13 | }, 14 | getInitialState() { 15 | return { 16 | param: { 17 | method: 'rec709', 18 | }, 19 | options: [ 20 | /* eslint-disable key-spacing */ 21 | { 22 | label: '亮度(ITU-R BT.709)', 23 | value: 'rec709', 24 | key : 'rec709', 25 | }, { 26 | label: '亮度(ITU-R BT.601)', 27 | value: 'rec601', 28 | key : 'rec601', 29 | }, { 30 | label: '亮度(HSL)', 31 | value: 'hsl', 32 | key : 'hsl', 33 | }, { 34 | label: '平均值', 35 | value: 'average', 36 | key : 'average', 37 | }, { 38 | label: '最大值', 39 | value: 'max', 40 | key : 'max', 41 | }, { 42 | label: '最小值', 43 | value: 'min', 44 | key : 'min', 45 | }, 46 | /* eslint-enable key-spacing */ 47 | ], 48 | }; 49 | }, 50 | componentDidMount() { 51 | mangekyouStore.addComputeListener(this._compute); 52 | this._compute(); 53 | }, 54 | componentWillUnmount() { 55 | mangekyouStore.removeComputeListener(this._compute); 56 | }, 57 | render() { 58 | return ( // eslint-disable-line no-extra-parens 59 |
60 | 65 | {this.state.options.map(({key, value, label}) => ( // eslint-disable-line no-extra-parens 66 | 71 | ))} 72 | 73 |
74 | ); 75 | }, 76 | _handleChange(event, selected) { 77 | this.setState({ 78 | param: { 79 | method: selected, 80 | }, 81 | }, this._compute); 82 | }, 83 | _compute() { 84 | this.props.willProcess({ 85 | operationName: 'Grayscale', 86 | operationParam: this.state.param, 87 | }); 88 | }, 89 | }); 90 | 91 | export default Grayscale; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mangekyou 2 | ![license](https://img.shields.io/github/license/frantic1048/mangekyou.svg?style=flat-square) 3 | [![npmjs version](https://img.shields.io/npm/v/mangekyou.svg?style=flat-square)](https://www.npmjs.com/package/mangekyou) 4 | 5 | [![codeclimate analysis](https://img.shields.io/codeclimate/github/frantic1048/mangekyou.svg?style=flat-square)](https://codeclimate.com/github/frantic1048/mangekyou) 6 | [![travis-ci](https://img.shields.io/travis/frantic1048/mangekyou.svg?style=flat-square)](https://travis-ci.org/frantic1048/mangekyou) 7 | [![david-dm](https://img.shields.io/david/frantic1048/mangekyou.svg?style=flat-square)](https://david-dm.org/frantic1048/mangekyou) 8 | 9 | :stars:简易图像处理程序。 10 | 11 | # 功能 12 | 13 | - 采样:自定义距离重采样 14 | - 量化:RGB 通道 1~256 级量化 15 | - 灰度化(HSL,HSY,平均值,最大值,最小值) 16 | - 位平面:RGB 通道分别 8 级量化后的位平面 17 | - 直方图和统计信息:R/G/B/Rec. 709 4 个通道的直方图及详细信息 18 | - 直方图均衡化:HSL、HSV、HSY 三种空间亮度通道均衡 19 | - 二值图:HSL、HSV、HSY 三种空间亮度通道为阈值的二值图。 20 | 21 | # 使用 22 | 23 | ## 桌面程序 24 | 25 | ```bash 26 | # 从 npm 安装 27 | npm i -g mangekyou 28 | 29 | # 运行 30 | mangekyou 31 | ``` 32 | 33 | ## 现代浏览器 34 | 35 | 访问:[http://frantic1048.github.io/mangekyou](http://frantic1048.github.io/mangekyou) 36 | 37 | # 目录结构 38 | 39 | ``` 40 | ./ 41 | ├── .babelrc // 编译配置 42 | ├── .codeclimate.yml // Code Climate 平台配置 43 | ├── .travis.yml // Travis CI 平台配置 44 | ├── .eslintrc // 静态分析配置 45 | ├── .gitignore // git 忽略文件设定 46 | ├── .npmignore // npm 忽略文件设定 47 | ├── gulpfile.babel.js // 构建配置 48 | ├── LICENSE // 发布协议 49 | ├── package.json // 依赖信息 50 | ├── README.md // 自述文档 51 | └── src // 源代码目录 52 |    └── app 53 |    ├── index.html // 界面基底 54 |    ├── main.js // 主程序入口 55 |    ├── script 56 |    │   ├── entry.js // 界面程序入口 57 |    │   ├── app.jsx // 主窗口 58 |    │   ├── keyMap.js // 键盘映射配置 59 |    │   ├── component // 界面组件目录 60 |    │   │  └── tool // 各种算法对应的控件目录 61 |    │   ├── constant // 程序常量 62 |    │   ├── action // Flux 架构的 Action 63 |    │   ├── dispatcher // Flux 架构的 Dispatcher 64 |    │   ├── store // Flux 架构的 Store 65 |    │   └── worker // 处理计算的程序目录 66 |    │   ├── worker.js // 计算程序入口 67 |    │   ├── util.js // 通用算法模块 68 |    │   └── ....js // 文件名对应相关算法 69 |    └── style // 界面样式表目录 70 | ``` 71 | 72 | # 构建 73 | 74 | 环境需求: 75 | 76 | - Node.js,最新稳定版本 77 | - npm,最新稳定版本 78 | - git 79 | 80 | ```bash 81 | #克隆代码到本地 82 | git clone --depth=1 https://github.com/frantic1048/mangekyou.git 83 | cd mangekyou 84 | 85 | # 安装依赖 86 | npm install 87 | 88 | # 构建 89 | npm run build 90 | 91 | # 运行 92 | npm run run 93 | ``` 94 | -------------------------------------------------------------------------------- /src/app/script/component/Preview.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import mangekyouStore from '../store/mangekyouStore'; 4 | 5 | const Preview = React.createClass({ 6 | getInitialState() { 7 | return { 8 | image: mangekyouStore.getPreviewImage(), 9 | }; 10 | }, 11 | componentDidMount() { 12 | this._initCanvas(); 13 | mangekyouStore.addPreviewImageChangeListener(this._onChange); 14 | window.addEventListener('resize', this._onResize); 15 | }, 16 | componentDidUpdate() { 17 | this._draw(); 18 | }, 19 | componentWillUnmount() { 20 | mangekyouStore.removePreviewImageChangeListener(this._onChange); 21 | window.removeEventListener('resize', this._onResize); 22 | }, 23 | render() { 24 | return ( // eslint-disable-line no-extra-parens 25 |
34 | 35 |
36 | ); 37 | }, 38 | _initCanvas() { 39 | this._onResize(); 40 | }, 41 | _onChange() { 42 | this.setState({ 43 | image: mangekyouStore.getPreviewImage(), 44 | }); 45 | }, 46 | _onResize() { 47 | const container = ReactDOM.findDOMNode(this); 48 | const canvas = container.children[0]; 49 | const {width, height} = container.getBoundingClientRect(); 50 | canvas.setAttribute('width', Math.floor(width)); 51 | canvas.setAttribute('height', Math.floor(height)); 52 | this._draw(); 53 | }, 54 | _draw() { 55 | if (this.state.image) { 56 | const container = ReactDOM.findDOMNode(this); 57 | const canvas = container.children[0]; 58 | const ctx = canvas.getContext('2d'); 59 | const {width, height} = canvas; 60 | const iwidth = this.state.image.width; 61 | const iheight = this.state.image.height; 62 | 63 | const wRatio = width / iwidth; 64 | const hRatio = height / iheight; 65 | 66 | // ratio to size image contained in canvas 67 | // image is bigger: shrink to fit 68 | // image is smaller: no scaling 69 | const ratio = Math.min(Math.min(wRatio, hRatio), 1); 70 | 71 | // shift to center image in canvas 72 | const shiftX = (width - iwidth * ratio) / 2; 73 | const shiftY = (height - iheight * ratio) / 2; 74 | 75 | ctx.clearRect(0, 0, width, height); 76 | ctx.drawImage( 77 | this.state.image, 78 | 0, 0, iwidth, iheight, 79 | shiftX, shiftY, iwidth * ratio, iheight * ratio); 80 | } 81 | }, 82 | }); 83 | 84 | export default Preview; 85 | -------------------------------------------------------------------------------- /src/app/script/component/tool/HistogramEqualization.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RadioButtonGroup from 'material-ui/lib/radio-button-group'; 3 | import RadioButton from 'material-ui/lib/radio-button'; 4 | import mangekyouStore from '../../store/mangekyouStore'; 5 | 6 | const HistogramEqualization = React.createClass({ 7 | propTypes: { 8 | willProcess: React.PropTypes.func.isRequired, 9 | currentImage: React.PropTypes.shape({ 10 | width: React.PropTypes.number.isRequired, 11 | height: React.PropTypes.number.isRequired, 12 | }), 13 | }, 14 | getInitialState() { 15 | return { 16 | param: { 17 | space: 'hsl', 18 | channelIndex: 2, 19 | }, 20 | options: { 21 | /* eslint-disable key-spacing*/ 22 | hsl: { 23 | label: '亮度(HSL)', 24 | value: 'hsl', 25 | key : 'hsl', 26 | param: {space: 'hsl', channelIndex: 2}, 27 | }, 28 | hsv: { 29 | label: '亮度(HSV)', 30 | value: 'hsv', 31 | key : 'hsv', 32 | param: {space: 'hsv', channelIndex: 2}, 33 | }, 34 | hsy709: { 35 | label: '亮度(HSY, Rec. 709)', 36 | value: 'hsy709', 37 | key : 'hsy709', 38 | param: {space: 'hsy709', channelIndex: 2}, 39 | }, 40 | hsy601: { 41 | label: '亮度(HSY, Rec. 610)', 42 | value: 'hsy601', 43 | key : 'hsy601', 44 | param: {space: 'hsy601', channelIndex: 2}, 45 | }, 46 | /* eslint-enable key-spacing*/ 47 | }, 48 | }; 49 | }, 50 | componentDidMount() { 51 | mangekyouStore.addComputeListener(this._compute); 52 | this._compute(); 53 | }, 54 | componentWillUnmount() { 55 | mangekyouStore.removeComputeListener(this._compute); 56 | }, 57 | render() { 58 | return ( // eslint-disable-line no-extra-parens 59 |
60 | 65 | {Object.keys(this.state.options).map(key => ( // eslint-disable-line no-extra-parens 66 | 71 | ))} 72 | 73 |
74 | ); 75 | }, 76 | _handleChange(event, selected) { 77 | this.setState({ 78 | param: this.state.options[selected].param, 79 | }, this._compute); 80 | }, 81 | _compute() { 82 | this.props.willProcess({ 83 | operationName: 'HistogramEqualization', 84 | operationParam: this.state.param, 85 | }); 86 | }, 87 | }); 88 | 89 | export default HistogramEqualization; 90 | -------------------------------------------------------------------------------- /src/app/script/component/HistoryItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListItem from 'material-ui/lib/lists/list-item'; 3 | import IconButton from 'material-ui/lib/icon-button'; 4 | import ContentForward from 'material-ui/lib/svg-icons/content/forward'; 5 | import mangekyouAction from './../action/mangekyouAction'; 6 | 7 | const HistoryItem = React.createClass({ 8 | propTypes: { 9 | history: React.PropTypes.shape({ 10 | operationDisplayName: React.PropTypes.string.isRequired, 11 | image: React.PropTypes.shape({ 12 | width: React.PropTypes.number.isRequired, 13 | height: React.PropTypes.number.isRequired, 14 | toDataURL: React.PropTypes.func.isRequired, 15 | }).isRequired, 16 | }).isRequired, 17 | index: React.PropTypes.number.isRequired, 18 | }, 19 | componentDidMount() { 20 | this._draw(); 21 | }, 22 | componentDidUpdate() {}, 23 | render() { 24 | return ( // eslint-disable-line no-extra-parens 25 | 39 | } 40 | primaryText={this.props.history.operationDisplayName} 41 | rightIconButton={ 42 | 47 | 48 | 49 | } 50 | /> 51 | ); 52 | }, 53 | handleLoadHistory() { 54 | mangekyouAction.loadHistory(this.props.index); 55 | }, 56 | _draw() { 57 | const canvas = this.refs.canvas; 58 | const ctx = canvas.getContext('2d'); 59 | const {width, height} = canvas; 60 | const iwidth = this.props.history.image.width; 61 | const iheight = this.props.history.image.height; 62 | 63 | const wRatio = width / iwidth; 64 | const hRatio = height / iheight; 65 | 66 | // ratio to size image contained in canvas 67 | // image is bigger: shrink to fit 68 | // image is smaller: no scaling 69 | const ratio = Math.min(Math.min(wRatio, hRatio), 1); 70 | 71 | // shift to center image in canvas 72 | const shiftX = (width - iwidth * ratio) / 2; 73 | const shiftY = (height - iheight * ratio) / 2; 74 | 75 | // enable smoothing 76 | ctx.imageSmoothingEnabled = true; 77 | ctx.mozImageSmoothingEnabled = true; 78 | ctx.webkitImageSmoothingEnabled = true; 79 | ctx.msImageSmoothingEnabled = true; 80 | 81 | ctx.clearRect(0, 0, width, height); 82 | ctx.drawImage( 83 | this.props.history.image, 84 | 0, 0, iwidth, iheight, 85 | shiftX, shiftY, iwidth * ratio, iheight * ratio); 86 | }, 87 | }); 88 | 89 | export default HistoryItem; 90 | -------------------------------------------------------------------------------- /src/app/script/worker/Statistics.js: -------------------------------------------------------------------------------- 1 | import {getAllPositions, range, minOf} from './util'; 2 | import {luma709} from './ColorConversion'; 3 | 4 | function levelMedian(levelCountArray, itemCount) { 5 | return levelCountArray.reduce((pre, cur, idx) => { 6 | const result = {...pre}; 7 | if (!pre.ok) { 8 | result.sum = pre.sum + cur; 9 | result.ok = result.sum > 0.5 * itemCount; 10 | if (result.ok) { 11 | result.mid = idx; 12 | } 13 | } 14 | return result; 15 | }, {ok: false, sum: 0, mid: 0}).mid; 16 | } 17 | 18 | function levelAverange(levelCountArray, itemCount) { 19 | return levelCountArray.reduce((pre, cur, idx) => pre + idx * cur, 0) / itemCount; 20 | } 21 | 22 | // https://en.wikipedia.org/wiki/Standard_deviation#Uncorrected_sample_standard_deviation 23 | function levelStandardDeviation(levelCountArray, averangeValue, itemCount) { 24 | return Math.sqrt(levelCountArray.reduce((pre, cur, idx) => { 25 | return pre + Math.pow(cur - averangeValue, 2) * idx; 26 | }, 0) / itemCount); 27 | } 28 | 29 | function Statistics({width, height, data}) { 30 | const allPos = getAllPositions(width, height); 31 | const pixelCount = width * height; 32 | const lbCount = new Array(256).fill(0); // lowerbound count of least color levels 33 | const rCount = new Array(256).fill(0); // count of red levels 34 | const gCount = new Array(256).fill(0); // count of green levels 35 | const bCount = new Array(256).fill(0); // count of blue levels 36 | const lCount = new Array(256).fill(0); // count of Rec. 709 luma levels 37 | 38 | for (const [,, index] of allPos()) { 39 | const [r, g, b] = [data[index], data[index + 1], data[index + 2]]; 40 | ++rCount[r]; 41 | ++gCount[g]; 42 | ++bCount[b]; 43 | ++lCount[Math.round(luma709(r, g, b))]; 44 | } 45 | 46 | for (const i of range(0, 256)) { 47 | lbCount[i] = minOf(rCount[i], gCount[i], bCount[i]); 48 | } 49 | 50 | // color levels frequency 51 | const frequency = { 52 | red: rCount.map(v => v / pixelCount), // frequency of red levels 53 | green: gCount.map(v => v / pixelCount), // frequency of green levels 54 | blue: bCount.map(v => v / pixelCount), // frequency of blue levels 55 | lowerbound: lbCount.map(v => v / pixelCount), // lowerbound frequency of rgb levels 56 | }; 57 | 58 | // averange levels 59 | const averange = { 60 | red: levelAverange(rCount, pixelCount), 61 | green: levelAverange(gCount, pixelCount), 62 | blue: levelAverange(bCount, pixelCount), 63 | luma: levelAverange(lCount, pixelCount), 64 | }; 65 | 66 | // median levels 67 | const median = { 68 | red: levelMedian(rCount, pixelCount), 69 | green: levelMedian(gCount, pixelCount), 70 | blue: levelMedian(bCount, pixelCount), 71 | luma: levelMedian(lCount, pixelCount), 72 | }; 73 | 74 | // standard deviation 75 | const standardDeviation = { 76 | red: levelStandardDeviation(rCount, averange.red, pixelCount), 77 | green: levelStandardDeviation(rCount, averange.green, pixelCount), 78 | blue: levelStandardDeviation(rCount, averange.blue, pixelCount), 79 | luma: levelStandardDeviation(lCount, averange.luma, pixelCount), 80 | }; 81 | 82 | return [ 83 | { 84 | proceed: true, 85 | width, 86 | height, 87 | pixelCount, 88 | averange, 89 | median, 90 | standardDeviation, 91 | frequency, 92 | }, 93 | ]; 94 | } 95 | 96 | export default Statistics; 97 | -------------------------------------------------------------------------------- /src/app/script/component/StatusPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Paper from 'material-ui/lib/paper'; 3 | import Histogram from './tool/Histogram'; 4 | import Statistics from './tool/Statistics'; 5 | import mangekyouStore from './../store/mangekyouStore'; 6 | 7 | const StatusPanel = React.createClass({ 8 | getInitialState() { 9 | return { 10 | showing: mangekyouStore.getShowing().statusPanel, 11 | worker: null, 12 | statistics: { 13 | width: 0, 14 | height: 0, 15 | pixelCount: 0, 16 | averange: {red: 0, green: 0, blue: 0, luma: 0}, 17 | median: {red: 0, green: 0, blue: 0, luma: 0}, 18 | standardDeviation: {red: 0, green: 0, blue: 0, luma: 0}, 19 | frequency: null, 20 | }, 21 | }; 22 | }, 23 | componentDidMount() { 24 | mangekyouStore.addShowingChangeListener(this._handleShowingChange); 25 | mangekyouStore.addPreviewImageChangeListener(this._handlePreviewImageChange); 26 | }, 27 | componentWillUnmount() { 28 | mangekyouStore.removeShowingChangeListener(this._handleShowingChange); 29 | mangekyouStore.removePreviewImageChangeListener(this._handlePreviewImageChange); 30 | }, 31 | render() { 32 | return ( // eslint-disable-line no-extra-parens 33 | 55 | 56 | 57 | 58 | ); 59 | }, 60 | _handleShowingChange() { 61 | this.setState({ 62 | showing: mangekyouStore.getShowing().statusPanel, 63 | }); 64 | }, 65 | _handlePreviewImageChange() { 66 | const previewImage = mangekyouStore.getPreviewImage(); 67 | if (this.state.worker) { 68 | this.state.worker.terminate(); 69 | } 70 | if (previewImage) { 71 | this._compute(previewImage); 72 | } 73 | }, 74 | _compute(previewImage) { 75 | const {width, height} = previewImage; 76 | const imgData = previewImage 77 | .getContext('2d') 78 | .getImageData(0, 0, width, height); 79 | const aworker = new Worker('script/worker.js'); 80 | this.setState({ 81 | worker: aworker, 82 | }); 83 | aworker.onmessage = this._didCompute; 84 | aworker.postMessage({ 85 | operationName: 'Statistics', 86 | operationParam: {}, 87 | image: { 88 | width: imgData.width, 89 | height: imgData.height, 90 | data: imgData.data, 91 | }, 92 | }); 93 | }, 94 | _didCompute({data}) { 95 | if (data.proceed) { 96 | this.setState({ 97 | statistics: data, 98 | }); 99 | } 100 | this.state.worker.terminate(); 101 | this.setState({ 102 | worker: null, 103 | }); 104 | }, 105 | }); 106 | 107 | export default StatusPanel; 108 | -------------------------------------------------------------------------------- /src/app/script/worker/Convolve.js: -------------------------------------------------------------------------------- 1 | // disable new-cap linting for color space name usage in function name 2 | /* eslint-disable new-cap */ 3 | 4 | import {RGBToHSL, HSLToRGB, 5 | RGBToHSV, HSVToRGB, 6 | RGBToHSY709, HSY709ToRGB, 7 | RGBToHSY601, HSY601ToRGB} from './ColorConversion'; 8 | import {getAllPositions, clampBetween} from './util'; 9 | 10 | // Coordinate helper for pixel accesing of ImageData 11 | // add clampping for convolve processing 12 | function getConvolveCoordinate(width, height) { 13 | return (x, y) => clampBetween(y, 0, height) * width * 4 + clampBetween(x, 0, width) * 4; 14 | } 15 | 16 | function Convolve({width, height, data}, {core, space}) { 17 | const allPos = getAllPositions(width, height); 18 | const convolveCord = getConvolveCoordinate(width, height); 19 | 20 | // core is an 2-dimension array like 21 | // [[1, 1, 1], 22 | // [1, 1, 1], 23 | // [1, 1, 1]] 24 | // each dimension's length is (2n + 1); 25 | // core[y][x] is (x, y) position of convolve core 26 | const coreHeight = core.length; 27 | const coreWidth = core[0].length; 28 | 29 | // data in specified color space 30 | // use 3 channels + 1 alpha channel for each pixel 31 | const sData = new Uint8ClampedArray(4 * width * height); 32 | 33 | // function convert RGB <-> Specified 3-channel Color Space 34 | let RGBToSpec; 35 | let SpecToRGB; 36 | 37 | switch (space) { 38 | case 'rgb': 39 | RGBToSpec = (r, g, b) => [r, g, b]; 40 | SpecToRGB = (r, g, b) => [r, g, b]; 41 | break; 42 | case 'hsl': 43 | RGBToSpec = RGBToHSL; 44 | SpecToRGB = HSLToRGB; 45 | break; 46 | case 'hsv': 47 | RGBToSpec = RGBToHSV; 48 | SpecToRGB = HSVToRGB; 49 | break; 50 | case 'hsy709': 51 | RGBToSpec = RGBToHSY709; 52 | SpecToRGB = HSY709ToRGB; 53 | break; 54 | case 'hsy601': 55 | RGBToSpec = RGBToHSY601; 56 | SpecToRGB = HSY601ToRGB; 57 | break; 58 | default: 59 | break; 60 | } 61 | 62 | // convert RGB data to Specified color space 63 | for (const [,, index] of allPos()) { 64 | const sPixel = RGBToSpec(data[index] * (1 / 255), 65 | data[index + 1] * (1 / 255), 66 | data[index + 2] * (1 / 255)); 67 | sData[index] = Math.round(sPixel[0] * 255); 68 | sData[index + 1] = Math.round(sPixel[1] * 255); 69 | sData[index + 2] = Math.round(sPixel[2] * 255); 70 | 71 | // copy alpha channel as it is. 72 | sData[index + 3] = data[index + 3]; 73 | } 74 | 75 | for (const [x, y, index] of allPos()) { 76 | // get three channels' value 77 | const sPixel = [sData[index], sData[index + 1], sData[index + 2]]; 78 | 79 | // convolved pixel 80 | const cPixel = [0, 0, 0]; 81 | 82 | // convolve on sData 83 | sPixel.forEach((value, channelIndex) => { 84 | let cValue = 0.0; 85 | core.forEach((line, cy) => { 86 | line.forEach((weight, cx) => { 87 | cValue += weight * sData[convolveCord(x + cx - (coreWidth - 1) * ( 1 / 2), y + cy - (coreHeight - 1) * (1 / 2)) + channelIndex]; 88 | }); 89 | }); 90 | cPixel[channelIndex] = cValue; 91 | }); 92 | 93 | // convert convolved pixel back to RGB 94 | const xPixel = SpecToRGB(cPixel[0] * ( 1 / 255), 95 | cPixel[1] * ( 1 / 255), 96 | cPixel[2] * ( 1 / 255)); 97 | data[index] = Math.round(xPixel[0] * 255); 98 | data[index + 1] = Math.round(xPixel[1] * 255); 99 | data[index + 2] = Math.round(xPixel[2] * 255); 100 | } 101 | 102 | return {width, height, data}; 103 | } 104 | 105 | export default Convolve; 106 | -------------------------------------------------------------------------------- /src/app/script/component/tool/Binarization.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RadioButtonGroup from 'material-ui/lib/radio-button-group'; 3 | import RadioButton from 'material-ui/lib/radio-button'; 4 | import Slider from 'material-ui/lib/slider'; 5 | import mangekyouStore from '../../store/mangekyouStore'; 6 | 7 | const Binarization = React.createClass({ 8 | propTypes: { 9 | willProcess: React.PropTypes.func.isRequired, 10 | currentImage: React.PropTypes.shape({ 11 | width: React.PropTypes.number.isRequired, 12 | height: React.PropTypes.number.isRequired, 13 | }), 14 | }, 15 | getInitialState() { 16 | return { 17 | param: { 18 | space: 'hsl', 19 | channelIndex: 2, 20 | threshold: 125, 21 | }, 22 | options: { 23 | /* eslint-disable key-spacing */ 24 | hsl: { 25 | label: 'HSL', 26 | value: 'hsl', 27 | key : 'hsl', 28 | param: {space: 'hsl', channelIndex: 2}, 29 | }, 30 | hsv: { 31 | label: 'HSV', 32 | value: 'hsv', 33 | key : 'hsv', 34 | param: {space: 'hsv', channelIndex: 2}, 35 | }, 36 | hsy709: { 37 | label: 'HSY, Rec. 709', 38 | value: 'hsy709', 39 | key : 'hsy709', 40 | param: {space: 'hsy709', channelIndex: 2}, 41 | }, 42 | hsy601: { 43 | label: 'HSY, Rec. 601', 44 | value: 'hsy601', 45 | key : 'hsy601', 46 | param: {space: 'hsy601', channelIndex: 2}, 47 | }, 48 | /* eslint-enable key-spacing */ 49 | }, 50 | }; 51 | }, 52 | componentDidMount() { 53 | mangekyouStore.addComputeListener(this._compute); 54 | this._compute(); 55 | }, 56 | componentWillUnmount() { 57 | mangekyouStore.removeComputeListener(this._compute); 58 | }, 59 | render() { 60 | return ( // eslint-disable-line no-extra-parens 61 |
62 |

色彩空间

63 | 68 | {Object.keys(this.state.options).map(keyName => ( // eslint-disable-line no-extra-parens 69 | 74 | ))} 75 | 76 | 88 |
89 | ); 90 | }, 91 | _handleSpaceChange(event, selected) { 92 | this.setState({ 93 | param: { 94 | ...this.state.param, 95 | ...this.state.options[selected].param, 96 | }, 97 | }, this._compute); 98 | }, 99 | _handleThresholdChange(event, value) { 100 | this.setState({ 101 | param: { 102 | ...this.state.param, 103 | threshold: value, 104 | }, 105 | }); 106 | }, 107 | _compute() { 108 | this.props.willProcess({ 109 | operationName: 'Binarization', 110 | operationParam: this.state.param, 111 | }); 112 | }, 113 | }); 114 | 115 | export default Binarization; 116 | -------------------------------------------------------------------------------- /src/app/script/worker/HistogramEqualization.js: -------------------------------------------------------------------------------- 1 | // disable new-cap linting for color space name usage in function name 2 | /* eslint-disable new-cap */ 3 | 4 | import {RGBToHSL, HSLToRGB, 5 | RGBToHSV, HSVToRGB, 6 | RGBToHSY709, HSY709ToRGB, 7 | RGBToHSY601, HSY601ToRGB} from './ColorConversion'; 8 | import {getAllPositions} from './util'; 9 | 10 | // space is one of: hsl, hsv, hsy709, hsy610 11 | // channel is index (0 based) of selected color space: 0, 1, 2 12 | // algorithm from wiki. 13 | // https://en.wikipedia.org/wiki/Histogram_equalization 14 | function HistogramEqualization({width, height, data}, {space, channelIndex}) { 15 | const allPos = getAllPositions(width, height); 16 | const pixelCount = width * height; 17 | 18 | // data in specified color space 19 | // use 3 channels + 1 alpha channel for each pixel 20 | const sData = new Uint8ClampedArray(4 * width * height); 21 | 22 | // function convert RGB <-> Specified 3-channel Color Space 23 | let RGBToSpec; 24 | let SpecToRGB; 25 | 26 | // target channel's levels count 27 | const xCount = new Array(256).fill(0); 28 | 29 | // discrete Cumulative Distribution Function values 30 | // https://en.wikipedia.org/wiki/Cumulative_distribution_function 31 | const xCDF = new Array(256).fill(0); 32 | let xCDFMin; // min value of xCDF 33 | 34 | // map original level(index) to equalized level(value) 35 | let xEqualized; 36 | 37 | switch (space) { 38 | case 'hsl': 39 | RGBToSpec = RGBToHSL; 40 | SpecToRGB = HSLToRGB; 41 | break; 42 | case 'hsv': 43 | RGBToSpec = RGBToHSV; 44 | SpecToRGB = HSVToRGB; 45 | break; 46 | case 'hsy709': 47 | RGBToSpec = RGBToHSY709; 48 | SpecToRGB = HSY709ToRGB; 49 | break; 50 | case 'hsy601': 51 | RGBToSpec = RGBToHSY601; 52 | SpecToRGB = HSY601ToRGB; 53 | break; 54 | default: 55 | break; 56 | } 57 | 58 | // convert RGB data to Specified color space 59 | for (const [,, index] of allPos()) { 60 | const sPixel = RGBToSpec(data[index] / 255, 61 | data[index + 1] / 255, 62 | data[index + 2] / 255); 63 | sData[index] = Math.round(sPixel[0] * 255); 64 | sData[index + 1] = Math.round(sPixel[1] * 255); 65 | sData[index + 2] = Math.round(sPixel[2] * 255); 66 | 67 | // copy alpha channel as it is. 68 | sData[index + 3] = data[index + 3]; 69 | } 70 | 71 | // analyze frequency of selected channel leves. 72 | for (const [,, index] of allPos()) { 73 | ++xCount[sData[index + channelIndex]]; 74 | } 75 | 76 | // convert conuts to CDF 77 | xCount.reduce((pre, cur, idx) => { 78 | const result = pre + cur; 79 | xCDF[idx] = result; 80 | return result; 81 | }, 0); 82 | 83 | // compute equalized levels 84 | xCDFMin = xCDF.find(val => val > 0); 85 | xEqualized = xCDF.map((val) => { 86 | return Math.round((val - xCDFMin) / (pixelCount - xCDFMin) * 255); 87 | }); 88 | 89 | for (const [,, index] of allPos()) { 90 | // map original channel values to the equalized 91 | const orginalValue = sData[index + channelIndex]; 92 | sData[index + channelIndex] = xEqualized[orginalValue]; 93 | 94 | // convert equalized data back to RGB 95 | const xPixel = SpecToRGB(sData[index] * ( 1 / 255), 96 | sData[index + 1] * ( 1 / 255), 97 | sData[index + 2] * ( 1 / 255)); 98 | data[index] = Math.round(xPixel[0] * 255); 99 | data[index + 1] = Math.round(xPixel[1] * 255); 100 | data[index + 2] = Math.round(xPixel[2] * 255); 101 | } 102 | 103 | return {width, height, data}; 104 | } 105 | 106 | export default HistogramEqualization; 107 | -------------------------------------------------------------------------------- /src/app/script/component/tool/Histogram.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import mangekyouStore from './../../store/mangekyouStore'; 4 | 5 | const Histogram = React.createClass({ 6 | propTypes: { 7 | frequency: React.PropTypes.shape({ 8 | luma: React.PropTypes.arrayOf(React.PropTypes.number), 9 | red: React.PropTypes.arrayOf(React.PropTypes.number), 10 | green: React.PropTypes.arrayOf(React.PropTypes.number), 11 | blue: React.PropTypes.arrayOf(React.PropTypes.number), 12 | }), 13 | }, 14 | getInitialState() { 15 | const histogram = document.createElement('canvas'); 16 | histogram.setAttribute('width', 256); 17 | histogram.setAttribute('height', 150); 18 | return { 19 | showing: mangekyouStore.getShowing().toolPanel, 20 | histogram, 21 | }; 22 | }, 23 | componentDidMount() { 24 | // disable smoothing for histogram. 25 | const container = ReactDOM.findDOMNode(this); 26 | const canvas = container.children[0]; 27 | canvas.setAttribute('width', container.getBoundingClientRect().width); 28 | canvas.setAttribute('height', 150); 29 | const ctx = canvas.getContext('2d'); 30 | 31 | ctx.imageSmoothingEnabled = false; 32 | ctx.mozImageSmoothingEnabled = false; 33 | ctx.webkitImageSmoothingEnabled = false; 34 | ctx.msImageSmoothingEnabled = false; 35 | }, 36 | componentDidUpdate() { 37 | if (this.props.frequency) { 38 | this._updateHistogram(); 39 | } 40 | }, 41 | render() { 42 | return ( // eslint-disable-line no-extra-parens 43 |
50 | 51 |
52 | ); 53 | }, 54 | _drawHistogram() { 55 | const container = ReactDOM.findDOMNode(this); 56 | const canvas = container.children[0]; 57 | const ctx = canvas.getContext('2d'); 58 | const {width, height} = canvas; 59 | 60 | ctx.clearRect(0, 0, width, height); 61 | ctx.drawImage(this.state.histogram, 0, 0, width, height); 62 | }, 63 | _updateHistogram() { 64 | const freq = this.props.frequency; 65 | const {width, height} = this.state.histogram; 66 | const ctx = this.state.histogram.getContext('2d'); 67 | 68 | // vertical scale histogram so that details show clearly 69 | const scaleFactor = 30; 70 | 71 | // clear old histogram 72 | ctx.clearRect(0, 0, width, height); 73 | 74 | // draw red histogram 75 | ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; 76 | freq.red.forEach((value, index) => { 77 | ctx.fillRect( 78 | index, height - Math.round(value * height * scaleFactor), 79 | 1, Math.round(value * height * scaleFactor) 80 | ); 81 | }); 82 | 83 | // draw green histogram 84 | ctx.fillStyle = 'rgba(0, 255, 0, 0.3)'; 85 | freq.green.forEach((value, index) => { 86 | ctx.fillRect( 87 | index, height - Math.round(value * height * scaleFactor), 88 | 1, Math.round(value * height * scaleFactor) 89 | ); 90 | }); 91 | 92 | // draw blue histogram 93 | ctx.fillStyle = 'rgba(0, 0, 255, 0.3)'; 94 | freq.blue.forEach((value, index) => { 95 | ctx.fillRect( 96 | index, height - Math.round(value * height * scaleFactor), 97 | 1, Math.round(value * height * scaleFactor)); 98 | }); 99 | 100 | // draw luma histogram 101 | ctx.fillStyle = 'rgba(100, 100, 100, 1)'; 102 | freq.lowerbound.forEach((value, index) => { 103 | ctx.fillRect( 104 | index, height - Math.round(value * height * scaleFactor), 105 | 1, Math.round(value * height * scaleFactor) 106 | ); 107 | }); 108 | 109 | // display on histogram component. 110 | this._drawHistogram(); 111 | }, 112 | }); 113 | 114 | export default Histogram; 115 | -------------------------------------------------------------------------------- /src/app/script/store/mangekyouStore.jsx: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import mangekyouConstant from './../constant/mangekyouConstant'; 3 | import mangekyouDispatcher from './../dispatcher/mangekyouDispatcher'; 4 | 5 | const CHANGE_EVENT = { 6 | HISTORY: 'HISTORY', 7 | PREVIEW: 'PREVIEW', 8 | SHOWING: 'SHOWING', 9 | PROCESSING: 'PROCESSING', 10 | COMPUTE: 'COMPUTE', 11 | }; 12 | 13 | const _store = { 14 | history: [], 15 | previewImage: null, 16 | processingState: mangekyouConstant.IDLE, 17 | showing: { 18 | historyPanel: true, 19 | toolPanel: true, 20 | statusPanel: true, 21 | }, 22 | }; 23 | 24 | function addHistory({operation, operationDisplayName, image}) { 25 | _store.history.push({operation, operationDisplayName, image, key: performance.now()}); 26 | _store.previewImage = image; 27 | } 28 | 29 | function loadHistory(index) { 30 | const image = _store.history[index].image; 31 | 32 | if ( _store.history.slice(-1)[0].operation === 'historyJump') { 33 | _store.history.pop(); 34 | } 35 | 36 | addHistory({ 37 | operation: 'historyJump', 38 | operationDisplayName: '历史跳转', 39 | image, 40 | }); 41 | } 42 | 43 | function setProcessingState(processingState) { 44 | _store.processingState = processingState; 45 | } 46 | 47 | function updatePreviewImage(image) { 48 | _store.previewImage = image; 49 | } 50 | 51 | function newImage(image) { 52 | addHistory({ 53 | operation: 'addFile', 54 | operationDisplayName: '添加文件', 55 | image, 56 | }); 57 | } 58 | 59 | function triggerShowing(componentName) { 60 | _store.showing[componentName] = !_store.showing[componentName]; 61 | } 62 | 63 | const mangekyouStore = { 64 | ...EventEmitter.prototype, 65 | addHistoryChangeListener(cb) { 66 | this.on(CHANGE_EVENT.HISTORY, cb); 67 | }, 68 | removeHistoryChangeListener(cb) { 69 | this.removeListener(CHANGE_EVENT.HISTORY, cb); 70 | }, 71 | getHistory() { 72 | return _store.history; 73 | }, 74 | getLastHistory() { 75 | return _store.history.length > 0 ? _store.history.slice(-1)[0] : null; 76 | }, 77 | getProcessingState() { 78 | return _store.processingState; 79 | }, 80 | addProcessingChangeListener(cb) { 81 | this.on(CHANGE_EVENT.PROCESSING, cb); 82 | }, 83 | removeProcessingChangeListener(cb) { 84 | this.removeListener(CHANGE_EVENT.PROCESSING, cb); 85 | }, 86 | addPreviewImageChangeListener(cb) { 87 | this.on(CHANGE_EVENT.PREVIEW, cb); 88 | }, 89 | removePreviewImageChangeListener(cb) { 90 | this.removeListener(CHANGE_EVENT.PREVIEW, cb); 91 | }, 92 | getPreviewImage() { 93 | return _store.previewImage; 94 | }, 95 | addShowingChangeListener(cb) { 96 | this.on(CHANGE_EVENT.SHOWING, cb); 97 | }, 98 | removeShowingChangeListener(cb) { 99 | this.removeListener(CHANGE_EVENT.SHOWING, cb); 100 | }, 101 | getShowing() { 102 | return _store.showing; 103 | }, 104 | addComputeListener(cb) { 105 | this.on(CHANGE_EVENT.COMPUTE, cb); 106 | }, 107 | removeComputeListener(cb) { 108 | this.removeListener(CHANGE_EVENT.COMPUTE, cb); 109 | }, 110 | }; 111 | 112 | mangekyouDispatcher.register(payload => { 113 | const {data, actionType} = payload.action; 114 | switch (actionType) { 115 | case mangekyouConstant.ADD_HISTORY: 116 | addHistory(data); 117 | mangekyouStore.emit(CHANGE_EVENT.PREVIEW); 118 | mangekyouStore.emit(CHANGE_EVENT.HISTORY); 119 | break; 120 | case mangekyouConstant.LOAD_HISTORY: 121 | loadHistory(data); 122 | mangekyouStore.emit(CHANGE_EVENT.PREVIEW); 123 | mangekyouStore.emit(CHANGE_EVENT.HISTORY); 124 | break; 125 | case mangekyouConstant.SET_PROCESSING_STATE: 126 | setProcessingState(data); 127 | mangekyouStore.emit(CHANGE_EVENT.PROCESSING); 128 | break; 129 | case mangekyouConstant.NEW_IMAGE: 130 | newImage(data); 131 | mangekyouStore.emit(CHANGE_EVENT.PREVIEW); 132 | mangekyouStore.emit(CHANGE_EVENT.HISTORY); 133 | break; 134 | case mangekyouConstant.UPDATE_PREVIEW: 135 | updatePreviewImage(data); 136 | mangekyouStore.emit(CHANGE_EVENT.PREVIEW); 137 | break; 138 | case mangekyouConstant.TRIGGER_SHOWING: 139 | triggerShowing(data); 140 | mangekyouStore.emit(CHANGE_EVENT.SHOWING); 141 | break; 142 | case mangekyouConstant.TRIGGER_COMPUTE: 143 | mangekyouStore.emit(CHANGE_EVENT.COMPUTE); 144 | break; 145 | default: 146 | return true; 147 | } 148 | }); 149 | 150 | export default mangekyouStore; 151 | -------------------------------------------------------------------------------- /src/app/script/component/HistoryPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Paper from 'material-ui/lib/paper'; 4 | import List from 'material-ui/lib/lists/list'; 5 | import HistoryItem from './HistoryItem'; 6 | import mangekyouStore from './../store/mangekyouStore'; 7 | 8 | const HistoryPanel = React.createClass({ 9 | getInitialState() { 10 | return { 11 | historyList: mangekyouStore.getHistory(), 12 | showing: mangekyouStore.getShowing().historyPanel, 13 | scrolling: false, 14 | scrollingStartPos: {y: 0}, 15 | scrollingStartScrollTop: 0, 16 | }; 17 | }, 18 | componentDidMount() { 19 | mangekyouStore.addHistoryChangeListener(this._onHistoryChange); 20 | mangekyouStore.addShowingChangeListener(this._onShowingChange); 21 | 22 | const container = ReactDOM.findDOMNode(this); 23 | container.addEventListener('mousedown', this._onScrollStart, false); 24 | container.addEventListener('mousemove', this._onScrollMove); 25 | container.addEventListener('mouseup', this._onScrollEnd); 26 | container.addEventListener('mouseleave', this._onScrollEnd); 27 | }, 28 | shouldComponentUpdate(nextProps, nextState) { 29 | return this.state.showing !== nextState.showing || 30 | this.state.historyList !== nextState.historyList; 31 | }, 32 | componentDidUpdate() { 33 | if (this.state.historyList.length > 0) { 34 | if (this.state.historyList.slice(-1)[0].operation !== '历史跳转') { 35 | // scroll to last history record. 36 | const list = ReactDOM.findDOMNode(this.refs.historyList); 37 | list.scrollTop = list.scrollHeight; 38 | } 39 | } 40 | }, 41 | componentWillUnmount() { 42 | mangekyouStore.removeHistoryChangeListener(this._onHistoryChange); 43 | mangekyouStore.removeShowingChangeListener(this._onShowingChange); 44 | 45 | const container = ReactDOM.findDOMNode(this); 46 | container.removeEventListener('mousedown', this._onScrollStart); 47 | container.removeEventListener('mousemove', this._onScrollMove); 48 | container.removeEventListener('mouseup', this._onScrollEnd); 49 | container.removeEventListener('mouseleave', this._onScrollEnd); 50 | }, 51 | render() { 52 | const listItems = []; 53 | this.state.historyList.forEach((history, index) => { 54 | listItems.push( 55 | 60 | ); 61 | }); 62 | return ( // eslint-disable-line no-extra-parens 63 | 78 | 88 | {listItems} 89 | 90 | 91 | ); 92 | }, 93 | _onScrollStart(ev) { 94 | const {clientY} = ev; 95 | const list = ReactDOM.findDOMNode(this.refs.historyList); 96 | this.setState({ 97 | scrolling: true, 98 | scrollingStartPos: { 99 | y: clientY, 100 | }, 101 | scrollingStartScrollTop: list.scrollTop, 102 | }); 103 | }, 104 | _onScrollMove(ev) { 105 | if (this.state.scrolling) { 106 | const list = ReactDOM.findDOMNode(this.refs.historyList); 107 | const deltaY = ev.clientY - this.state.scrollingStartPos.y; 108 | const {scrollHeight} = list; 109 | 110 | // keep scrollTop value in [0, scrollHeight] 111 | list.scrollTop = Math.max(0, Math.min(scrollHeight, this.state.scrollingStartScrollTop - deltaY)); 112 | } 113 | }, 114 | _onScrollEnd() { 115 | this.setState({ 116 | scrolling: false, 117 | }); 118 | }, 119 | _onHistoryChange() { 120 | this.setState({ 121 | historyList: mangekyouStore.getHistory().slice(), 122 | }); 123 | }, 124 | _onShowingChange() { 125 | this.setState({ 126 | showing: mangekyouStore.getShowing().historyPanel, 127 | }); 128 | }, 129 | }); 130 | 131 | export default HistoryPanel; 132 | -------------------------------------------------------------------------------- /src/app/script/component/tool/ChannelAdjust.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DropDownMenu from 'material-ui/lib/drop-down-menu'; 3 | import MenuItem from 'material-ui/lib/menus/menu-item'; 4 | import Slider from 'material-ui/lib/slider'; 5 | import mangekyouStore from '../../store/mangekyouStore'; 6 | 7 | const ChannelAdjust = React.createClass({ 8 | propTypes: { 9 | willProcess: React.PropTypes.func.isRequired, 10 | currentImage: React.PropTypes.shape({ 11 | width: React.PropTypes.number.isRequired, 12 | height: React.PropTypes.number.isRequired, 13 | }), 14 | }, 15 | getInitialState() { 16 | return { 17 | param: { 18 | space: 'rgb', 19 | delta: [0, 0, 0], 20 | }, 21 | options: { 22 | /* eslint-disable key-spacing */ 23 | rgb: { 24 | displayName : 'RGB', 25 | channels: ['Red', 'Green', 'Blue'], 26 | param : {space: 'rgb'}, 27 | }, 28 | hsl: { 29 | displayName : 'HSL', 30 | channels: ['Hue', 'Saturation', 'Lightness'], 31 | param : {space: 'hsl'}, 32 | }, 33 | hsv: { 34 | displayName : 'HSV', 35 | channels: ['Hue', 'Saturation', 'Value'], 36 | param : {space: 'hsv'}, 37 | }, 38 | hsy709: { 39 | displayName : 'HSY, Rec. 709', 40 | channels: ['Hue', 'Saturation', 'Luma'], 41 | param : {space: 'hsy709'}, 42 | }, 43 | hsy601: { 44 | displayName : 'HSY, Rec. 601', 45 | channels: ['Hue', 'Saturation', 'Luma'], 46 | param : {space: 'hsy601'}, 47 | }, 48 | /* eslint-enable key-spacing */ 49 | }, 50 | }; 51 | }, 52 | componentDidMount() { 53 | mangekyouStore.addComputeListener(this._compute); 54 | }, 55 | componentWillUnmount() { 56 | mangekyouStore.removeComputeListener(this._compute); 57 | }, 58 | render() { 59 | const menuItems = []; 60 | 61 | for (const space of Object.keys(this.state.options)) { 62 | menuItems.push( 63 | 68 | ); 69 | } 70 | 71 | const channelNames = this.state.options[this.state.param.space].channels; 72 | const delta = this.state.param.delta; 73 | 74 | return ( // eslint-disable-line no-extra-parens 75 |
76 | 色彩空间: 77 | 81 | {menuItems} 82 | 83 | = 0 ? '+' : ''}${delta[0].toFixed(2)}`} 93 | style={{marginTop: '1rem'}} 94 | /> 95 | = 0 ? '+' : ''}${delta[1].toFixed(2)}`} 105 | style={{marginTop: '1rem'}} 106 | /> 107 | = 0 ? '+' : ''}${delta[2].toFixed(2)}`} 117 | style={{marginTop: '1rem'}} 118 | /> 119 |
120 | ); 121 | }, 122 | _handleSpaceChange(event, selectedIndex, value) { 123 | this.setState({ 124 | param: { 125 | ...this.state.param, 126 | ...this.state.options[value].param, 127 | delta: [0, 0, 0], 128 | }, 129 | }, this._compute()); 130 | }, 131 | _handleDeltaChange(channelIndex, event, value) { 132 | const newDelta = this.state.param.delta; 133 | newDelta[channelIndex] = value * 0.01; // scale value to [-1, 1] 134 | this.setState({ 135 | param: { 136 | ...this.state.param, 137 | delta: newDelta, 138 | }, 139 | }); 140 | }, 141 | _compute() { 142 | this.props.willProcess({ 143 | operationName: 'ChannelAdjust', 144 | operationParam: this.state.param, 145 | }); 146 | }, 147 | }); 148 | 149 | export default ChannelAdjust; 150 | -------------------------------------------------------------------------------- /src/app/script/component/tool/Statistics.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from 'material-ui/lib/table/table'; 3 | import TableBody from 'material-ui/lib/table/table-body'; 4 | import TableRow from 'material-ui/lib/table/table-row'; 5 | import TableRowColumn from 'material-ui/lib/table/table-row-column'; 6 | import TableHeader from 'material-ui/lib/table/table-header'; 7 | import TableHeaderColumn from 'material-ui/lib/table/table-header-column'; 8 | 9 | const Statistics = React.createClass({ 10 | propTypes: { 11 | statistics: React.PropTypes.shape({ 12 | width: React.PropTypes.number, 13 | height: React.PropTypes.number, 14 | pixelCount: React.PropTypes.number, 15 | averange: React.PropTypes.object, 16 | median: React.PropTypes.object, 17 | standardDeviation: React.PropTypes.object, 18 | }), 19 | }, 20 | render() { 21 | const stat = this.props.statistics; 22 | const columnPadding = { 23 | paddingLeft: '8px', 24 | paddingRight: '8px', 25 | }; 26 | const lumaRowColor = {color: 'rgba(53, 53, 53, 0.8)'}; 27 | const redRowColor = {color: 'rgba(227, 45, 70, 0.8)'}; 28 | const greenRowColor = {color: 'rgba(65, 164, 22, 0.8)'}; 29 | const blueRowColor = {color: 'rgba(30, 123, 217, 0.8)'}; 30 | const headerColumnStyle = { 31 | ...columnPadding, 32 | color: 'rgba(0, 0, 0, 0.8)', 33 | }; 34 | const rowColumnStyle = {...columnPadding}; 35 | return ( // eslint-disable-line no-extra-parens 36 |
42 | 46 | 50 | 51 | 52 | 55 | 56 | 宽×高:{`${stat.width}×${stat.height}`} 57 | 像素量:{stat.pixelCount} 58 | 59 | 60 |
61 | 65 | 69 | 70 | 通道 71 | Rec. 709 72 | Red 73 | Green 74 | Blue 75 | 76 | 77 | 80 | 81 | 平均值 82 | {Math.round(stat.averange.luma)} 83 | {Math.round(stat.averange.red)} 84 | {Math.round(stat.averange.green)} 85 | {Math.round(stat.averange.blue)} 86 | 87 | 88 | 中值 89 | {stat.median.luma} 90 | {stat.median.red} 91 | {stat.median.green} 92 | {stat.median.blue} 93 | 94 | 95 | 标准差 96 | {Math.round(stat.standardDeviation.luma)} 97 | {Math.round(stat.standardDeviation.red)} 98 | {Math.round(stat.standardDeviation.green)} 99 | {Math.round(stat.standardDeviation.blue)} 100 | 101 | 102 |
103 |
104 | ); 105 | }, 106 | }); 107 | 108 | export default Statistics; 109 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import newer from 'gulp-newer'; 3 | import uglify from 'gulp-uglify'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import sourcemaps from 'gulp-sourcemaps'; 7 | import sass from 'gulp-sass'; 8 | import electronConnect from 'electron-connect'; 9 | import webpack from 'webpack-stream'; 10 | import named from 'vinyl-named'; 11 | 12 | const electron = electronConnect.server.create(); 13 | 14 | const webpackConf = { 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.jsx?$/, 22 | exclude: /(node_modules|bower_components)/, 23 | loader: 'babel', 24 | query: { 25 | presets: ['react', 'es2015', 'stage-2'], 26 | }, 27 | }, 28 | { 29 | test: /\.json$/, 30 | exclude: /(node_modules|bower_components|src)/, 31 | loader: 'json', 32 | }, 33 | ], 34 | }, 35 | }; 36 | 37 | const webpackDevConf = { 38 | ...webpackConf, 39 | watch: true, 40 | devtool: '#inline-source-map', 41 | }; 42 | 43 | const webpackProductConf = { 44 | ...webpackConf, 45 | plugins: [ 46 | new webpack.webpack.optimize.UglifyJsPlugin({ 47 | sourceMap: false, 48 | compress: { 49 | warnings: false, 50 | }, 51 | }), 52 | ], 53 | }; 54 | 55 | const app = { 56 | js: {}, 57 | bin: {}, 58 | css: {}, 59 | html: {}, 60 | bundle: {}, 61 | }; 62 | 63 | app.bundle.entry = ['src/app/script/mangekyou.js', 'src/app/script/worker/worker.js']; 64 | app.bundle.dest = ['build/app/script/mangekyou.js', 'build/app/script/worker/worker.js']; 65 | app.bundle.destPath = 'build/app/script'; 66 | 67 | app.bin.src = ['src/app/bin/mangekyou']; 68 | app.bin.destPath = 'build/app/bin'; 69 | 70 | app.js.src = ['src/app/main.js']; 71 | app.js.dest = ['build/app/main.js']; 72 | app.js.destPath = 'build/app'; 73 | app.js.lintSrc = Array.prototype.concat( 74 | app.js.src, 75 | [ 76 | 'gulpfile.babel.js', 77 | 'src/app/*.+(js|jsx)', 78 | 'src/app/**/*.+(js|jsx)', 79 | ] 80 | ); 81 | 82 | app.css.src = 'src/app/style/*.+(css|scss)'; 83 | app.css.dest = 'build/app/style/*.css'; 84 | app.css.destPath = 'build/app/style'; 85 | 86 | app.html.src = 'src/app/index.html'; 87 | app.html.dest = 'build/app/index.html'; 88 | app.html.destPath = 'build/app'; 89 | 90 | gulp.task('html', () => { 91 | return gulp.src(app.html.src) 92 | .pipe(gulp.dest(app.html.destPath)); 93 | }); 94 | 95 | gulp.task('bin', () => { 96 | return gulp.src(app.bin.src) 97 | .pipe(gulp.dest(app.bin.destPath)); 98 | }); 99 | 100 | gulp.task('lint', () => { 101 | return gulp.src(app.js.lintSrc) 102 | .pipe(eslint({ rulePaths: ['./']} )) 103 | .pipe(eslint.format()); 104 | }); 105 | 106 | gulp.task('css-dev', () => { 107 | return gulp.src(app.css.src) 108 | .pipe(newer(app.css.destPath)) 109 | .pipe(sourcemaps.init()) 110 | .pipe(sass()) 111 | .pipe(sourcemaps.write()) 112 | .pipe(gulp.dest(app.css.destPath)); 113 | }); 114 | 115 | gulp.task('css-product', () => { 116 | return gulp.src(app.css.src) 117 | .pipe(sass({outputStyle: 'compressed'})) 118 | .pipe(gulp.dest(app.css.destPath)); 119 | }); 120 | 121 | gulp.task('js-dev', () => { 122 | return gulp.src(app.js.src) 123 | .pipe(newer(app.js.destPath)) 124 | .pipe(sourcemaps.init()) 125 | .pipe(babel({ 126 | presets: ['es2015'], 127 | })) 128 | .pipe(sourcemaps.write()) 129 | .pipe(gulp.dest(app.js.destPath)); 130 | }); 131 | 132 | gulp.task('js-product', () => { 133 | return gulp.src(app.js.src) 134 | .pipe(babel({ 135 | presets: ['es2015', 'stage-2'], 136 | })) 137 | .pipe(uglify({compress: { warnings: false }})) 138 | .pipe(gulp.dest(app.js.destPath)); 139 | }); 140 | 141 | gulp.task('webpack-dev', ()=>{ 142 | return gulp.src(app.bundle.entry) 143 | .pipe(named()) 144 | .pipe(webpack(webpackDevConf)) 145 | .pipe(gulp.dest(app.bundle.destPath)); 146 | }); 147 | 148 | gulp.task('webpack-product', ()=>{ 149 | return gulp.src(app.bundle.entry) 150 | .pipe(named()) 151 | .pipe(webpack(webpackProductConf)) 152 | .pipe(gulp.dest(app.bundle.destPath)); 153 | }); 154 | 155 | gulp.task('serve', (callback) => { 156 | electron.start(); 157 | gulp.watch( 158 | Array.prototype.concat(app.js.dest), 159 | electron.restart 160 | ); 161 | gulp.watch( 162 | Array.prototype.concat(app.html.dest, app.css.dest, app.bundle.dest), 163 | electron.reload 164 | ); 165 | callback(); 166 | }); 167 | 168 | gulp.task('dev', (callback) => { 169 | gulp.watch(app.html.src, gulp.parallel('html')); 170 | gulp.watch(app.css.src, gulp.parallel('css-dev')); 171 | gulp.watch(app.js.src, gulp.parallel('js-dev')); 172 | gulp.series( 173 | gulp.parallel( 174 | 'html', 175 | 'bin', 176 | 'css-dev', 177 | 'js-dev' 178 | ), 179 | gulp.parallel( 180 | 'webpack-dev', 181 | 'serve' 182 | ) 183 | )(); 184 | callback(); 185 | }); 186 | 187 | gulp.task('product', (callback) => { 188 | gulp.parallel( 189 | 'html', 190 | 'bin', 191 | 'css-product', 192 | 'js-product', 193 | 'webpack-product' 194 | )(); 195 | callback(); 196 | }); 197 | 198 | gulp.task('ci', (callback) => { 199 | gulp.parallel( 200 | 'lint', 201 | 'product' 202 | )(); 203 | callback(); 204 | }); 205 | 206 | gulp.task('default', gulp.series('ci'), (callback) => { 207 | callback(); 208 | }); 209 | -------------------------------------------------------------------------------- /src/app/script/component/tool/Convolve.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DropDownMenu from 'material-ui/lib/drop-down-menu'; 3 | import MenuItem from 'material-ui/lib/menus/menu-item'; 4 | import RadioButtonGroup from 'material-ui/lib/radio-button-group'; 5 | import RadioButton from 'material-ui/lib/radio-button'; 6 | import mangekyouStore from '../../store/mangekyouStore'; 7 | 8 | const Convolve = React.createClass({ 9 | propTypes: { 10 | willProcess: React.PropTypes.func.isRequired, 11 | currentImage: React.PropTypes.shape({ 12 | width: React.PropTypes.number.isRequired, 13 | height: React.PropTypes.number.isRequired, 14 | }), 15 | }, 16 | getInitialState() { 17 | return { 18 | param: { 19 | space: 'rgb', 20 | core: [[0, -1, 0], 21 | [-1, 5, -1], 22 | [0, -1, 0]], 23 | }, 24 | spaces: { 25 | /* eslint-disable key-spacing */ 26 | rgb: { 27 | displayName : 'RGB', 28 | param : {space: 'rgb'}, 29 | }, 30 | hsl: { 31 | displayName : 'HSL', 32 | param : {space: 'hsl'}, 33 | }, 34 | hsv: { 35 | displayName : 'HSV', 36 | param : {space: 'hsv'}, 37 | }, 38 | hsy709: { 39 | displayName : 'HSY, Rec. 709', 40 | param : {space: 'hsy709'}, 41 | }, 42 | hsy601: { 43 | displayName : 'HSY, Rec. 601', 44 | param : {space: 'hsy601'}, 45 | }, 46 | /* eslint-enable key-spacing */ 47 | }, 48 | cores: { 49 | highpass: { 50 | label: '高通滤波', 51 | key: 'highpass', 52 | value: 'highpass', 53 | param: { 54 | core: [[0, -1, 0], 55 | [-1, 5, -1], 56 | [0, -1, 0]], 57 | }, 58 | }, 59 | highpass2: { 60 | label: '高通滤波2', 61 | key: 'highpass2', 62 | value: 'highpass2', 63 | param: { 64 | core: [[1, -2, 1], 65 | [-2, 5, -2], 66 | [1, -2, 1]], 67 | }, 68 | }, 69 | highpass3: { 70 | label: '高通滤波3', 71 | key: 'highpass3', 72 | value: 'highpass3', 73 | param: { 74 | core: [[1, -1, 1], 75 | [-1, 9, -1], 76 | [1, -1, 1]], 77 | }, 78 | }, 79 | lowpass: { 80 | label: '低通滤波', 81 | key: 'lowpass', 82 | value: 'lowpass', 83 | param: { 84 | core: [[0.1, 0.1, 0.1], 85 | [0.1, 0.2, 0.1], 86 | [0.1, 0.1, 0.1]], 87 | }, 88 | }, 89 | lowpass2: { 90 | label: '低通滤波2', 91 | key: 'lowpass2', 92 | value: 'lowpass2', 93 | param: { 94 | core: [[1 / 9, 1 / 9, 1 / 9], 95 | [1 / 9, 1 / 9, 1 / 9], 96 | [1 / 9, 1 / 9, 1 / 9]], 97 | }, 98 | }, 99 | lowpass3: { 100 | label: '低通滤波3', 101 | key: 'lowpass3', 102 | value: 'lowpass3', 103 | param: { 104 | core: [[1 / 16, 2 / 16, 1 / 16], 105 | [2 / 16, 4 / 16, 2 / 16], 106 | [1 / 16, 2 / 16, 1 / 16]], 107 | }, 108 | }, 109 | move: { 110 | label: '平移和差分边缘检测', 111 | key: 'move', 112 | value: 'move', 113 | param: { 114 | core: [[0, 0, 0], 115 | [-1, 1, 0], 116 | [0, 0, 0]], 117 | }, 118 | }, 119 | move2: { 120 | label: '平移和差分边缘检测2', 121 | key: 'move2', 122 | value: 'move2', 123 | param: { 124 | core: [[0, -1, 0], 125 | [0, 1, 0], 126 | [0, 0, 0]], 127 | }, 128 | }, 129 | move3: { 130 | label: '平移和差分边缘检测3', 131 | key: 'move3', 132 | value: 'move3', 133 | param: { 134 | core: [[-1, 0, 0], 135 | [0, 1, 0], 136 | [0, 0, 0]], 137 | }, 138 | }, 139 | edge: { 140 | label: '边缘检测', 141 | key: 'edge', 142 | value: 'edge', 143 | param: { 144 | core: [[-1, 0, -1], 145 | [0, 4, 0], 146 | [-1, 0, -1]], 147 | }, 148 | }, 149 | edge2: { 150 | label: '边缘检测2', 151 | key: 'edge2', 152 | value: 'edge2', 153 | param: { 154 | core: [[1, -2, 1], 155 | [-2, 4, -2], 156 | [1, -2, 1]], 157 | }, 158 | }, 159 | edge3: { 160 | label: '边缘检测3', 161 | key: 'edge3', 162 | value: 'edge3', 163 | param: { 164 | core: [[-1, -1, -1], 165 | [-1, 8, -1], 166 | [-1, -1, -1]], 167 | }, 168 | }, 169 | laplace: { 170 | label: '拉普拉斯边缘检测', 171 | key: 'laplace', 172 | value: 'laplace', 173 | param: { 174 | core: [[0, 0, 1, 0, 0], 175 | [0, 1, 2, 1, 0], 176 | [1, 2, -16, 2, 1], 177 | [0, 1, 2, 1, 0], 178 | [0, 0, 1, 0, 0]], 179 | }, 180 | }, 181 | }, 182 | }; 183 | }, 184 | componentDidMount() { 185 | mangekyouStore.addComputeListener(this._compute); 186 | this._compute(); 187 | }, 188 | componentWillUnmount() { 189 | mangekyouStore.removeComputeListener(this._compute); 190 | }, 191 | render() { 192 | const spaceMenuItems = []; 193 | 194 | for (const space of Object.keys(this.state.spaces)) { 195 | spaceMenuItems.push( 196 | 201 | ); 202 | } 203 | 204 | return ( // eslint-disable-line no-extra-parens 205 |
206 | 色彩空间: 207 | 211 | {spaceMenuItems} 212 | 213 | 219 | {Object.keys(this.state.cores).map(keyName => ( // eslint-disable-line no-extra-parens 220 | 225 | ))} 226 | 227 |
228 | ); 229 | }, 230 | _handleSpaceChange(event, selectedIndex, value) { 231 | this.setState({ 232 | param: { 233 | ...this.state.param, 234 | ...this.state.spaces[value].param, 235 | }, 236 | }, this._compute()); 237 | }, 238 | _handleCoreChange(event, selected) { 239 | this.setState({ 240 | param: { 241 | ...this.state.param, 242 | ...this.state.cores[selected].param, 243 | }, 244 | }, this._compute); 245 | }, 246 | _compute() { 247 | this.props.willProcess({ 248 | operationName: 'Convolve', 249 | operationParam: this.state.param, 250 | }); 251 | }, 252 | }); 253 | 254 | export default Convolve; 255 | -------------------------------------------------------------------------------- /src/app/script/component/ToolPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Paper from 'material-ui/lib/paper'; 3 | import RaisedButton from 'material-ui/lib/raised-button'; 4 | import DropDownMenu from 'material-ui/lib/drop-down-menu'; 5 | import MenuItem from 'material-ui/lib/menus/menu-item'; 6 | import mangekyouAction from '../action/mangekyouAction'; 7 | import mangekyouStore from '../store/mangekyouStore'; 8 | import mangekyouConstant from '../constant/mangekyouConstant'; 9 | import SampleRate from './tool/SampleRate'; 10 | import Quantization from './tool/Quantization'; 11 | import Grayscale from './tool/Grayscale'; 12 | import BitPlane from './tool/BitPlane'; 13 | import HistogramEqualization from './tool/HistogramEqualization'; 14 | import Binarization from './tool/Binarization'; 15 | import ChannelAdjust from './tool/ChannelAdjust'; 16 | import Convolve from './tool/Convolve'; 17 | 18 | const ToolPanel = React.createClass({ 19 | getInitialState() { 20 | return { 21 | showing: mangekyouStore.getShowing().toolPanel, 22 | currentRecord: mangekyouStore.getLastHistory(), 23 | processingState: mangekyouStore.getProcessingState(), 24 | proceedImage: null, 25 | proceedOperation: '', 26 | proceedOperationDisplayName: '', 27 | worker: null, 28 | selectedOperation: 'Nothing', 29 | operations: { 30 | Nothing: { 31 | displayName: '无', 32 | }, 33 | SampleRate: { 34 | displayName: '采样率', 35 | component: SampleRate, 36 | }, 37 | Quantization: { 38 | displayName: '量化', 39 | component: Quantization, 40 | }, 41 | Grayscale: { 42 | displayName: '灰度化', 43 | component: Grayscale, 44 | }, 45 | BitPlane: { 46 | displayName: '位平面', 47 | component: BitPlane, 48 | }, 49 | HistogramEqualization: { 50 | displayName: '直方图均衡', 51 | component: HistogramEqualization, 52 | }, 53 | Binarization: { 54 | displayName: '二值图', 55 | component: Binarization, 56 | }, 57 | ChannelAdjust: { 58 | displayName: '通道调节', 59 | component: ChannelAdjust, 60 | }, 61 | Convolve: { 62 | displayName: '卷积处理', 63 | component: Convolve, 64 | }, 65 | }, 66 | }; 67 | }, 68 | componentDidMount() { 69 | mangekyouStore.addHistoryChangeListener(this._onHistoryChange); 70 | mangekyouStore.addShowingChangeListener(this._onShowingChange); 71 | mangekyouStore.addProcessingChangeListener(this._onProcessingChange); 72 | }, 73 | componentWillUnmount() { 74 | mangekyouStore.removeHistoryChangeListener(this._onHistoryChange); 75 | mangekyouStore.removeShowingChangeListener(this._onShowingChange); 76 | mangekyouStore.removeProcessingChangeListener(this._onProcessingChange); 77 | }, 78 | render() { 79 | const menuItems = []; 80 | 81 | // add avilable operations to menu 82 | for (const op of Object.keys(this.state.operations)) { 83 | menuItems.push( 84 | 89 | ); 90 | } 91 | 92 | // create component of selected operation. 93 | const Tool = this.state.selectedOperation !== 'Nothing' ? this.state.operations[this.state.selectedOperation].component : 'span'; 94 | 95 | return ( // eslint-disable-line no-extra-parens 96 | 112 |
120 |
操作:
125 | 129 | {menuItems} 130 | 131 | 136 |
137 |
142 | 146 |
147 |
148 | ); 149 | }, 150 | _handleChange(event, selectedIndex, value) { 151 | this.setState({ 152 | selectedOperation: value, 153 | selectedOperationDisplayName: this.state.operations[value].displayName, 154 | }); 155 | }, 156 | _handleApply() { 157 | if (this.state.proceedImage) { 158 | mangekyouAction.addHistory({ 159 | operation: this.state.proceedOperation, 160 | operationDisplayName: this.state.proceedOperationDisplayName, 161 | image: this.state.proceedImage, 162 | }); 163 | } 164 | }, 165 | _onHistoryChange() { 166 | this.setState({ 167 | currentRecord: mangekyouStore.getLastHistory(), 168 | }); 169 | }, 170 | _onShowingChange() { 171 | this.setState({ 172 | showing: mangekyouStore.getShowing().toolPanel, 173 | }); 174 | }, 175 | _onProcessingChange() { 176 | this.setState({ 177 | processingState: mangekyouStore.getProcessingState(), 178 | }); 179 | }, 180 | _WillProcess({operationName, operationParam}) { 181 | if (this.state.worker) { 182 | this.state.worker.terminate(); 183 | } 184 | const currentRecord = mangekyouStore.getLastHistory(); 185 | if (currentRecord) { 186 | // if there's an image, process it. 187 | const {width, height} = currentRecord.image; 188 | const imgData = currentRecord.image 189 | .getContext('2d') 190 | .getImageData(0, 0, width, height); 191 | const aworker = new Worker('script/worker.js'); 192 | this.setState({ 193 | worker: aworker, 194 | proceedOperation: this.state.selectedOperation, 195 | proceedOperationDisplayName: this.state.selectedOperationDisplayName, 196 | }); 197 | aworker.onmessage = this._DidProcess; 198 | aworker.postMessage({ 199 | operationName, 200 | operationParam, 201 | image: { 202 | width: imgData.width, 203 | height: imgData.height, 204 | data: imgData.data, 205 | }, 206 | }); 207 | mangekyouAction.setProcessingState(mangekyouConstant.COMPUTING); 208 | } 209 | }, 210 | _DidProcess({data}) { 211 | if (data.proceed) { 212 | const imgd = new ImageData(new Uint8ClampedArray(data.image.buffer), data.image.width, data.image.height); 213 | const canvas = document.createElement('canvas'); 214 | const ctx = canvas.getContext('2d'); 215 | canvas.setAttribute('width', data.image.width); 216 | canvas.setAttribute('height', data.image.height); 217 | ctx.putImageData(imgd, 0, 0); 218 | mangekyouAction.updatePreviewImage(canvas); 219 | this.state.worker.terminate(); 220 | this.setState({ 221 | worker: null, 222 | processing: false, 223 | proceedImage: canvas, 224 | }); 225 | } 226 | mangekyouAction.setProcessingState(mangekyouConstant.IDLE); 227 | }, 228 | }); 229 | 230 | export default ToolPanel; 231 | -------------------------------------------------------------------------------- /src/app/script/component/TitleBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppBar from 'material-ui/lib/app-bar'; 3 | import IconMenu from 'material-ui/lib/menus/icon-menu'; 4 | import MenuItem from 'material-ui/lib/menus/menu-item'; 5 | import Divider from 'material-ui/lib/divider'; 6 | import IconButton from 'material-ui/lib/icon-button'; 7 | import MoreVertIcon from 'material-ui/lib/svg-icons/navigation/more-vert'; 8 | import ImageAddToPhotosIcon from 'material-ui/lib/svg-icons/image/add-to-photos'; 9 | import ContentSaveIcon from 'material-ui/lib/svg-icons/content/save'; 10 | import ActionHistoryIcon from 'material-ui/lib/svg-icons/action/history'; 11 | import ImageStyleIcon from 'material-ui/lib/svg-icons/image/style'; 12 | import ImageFilterVintageIcon from 'material-ui/lib/svg-icons/image/filter-vintage'; 13 | import SocialCakeIcon from 'material-ui/lib/svg-icons/social/cake'; 14 | import AVEqualizerIcon from 'material-ui/lib/svg-icons/av/equalizer'; 15 | import KeyboardShortcut from './KeyboardShortcut'; 16 | import mangekyouAction from '../action/mangekyouAction'; 17 | import mangekyouStore from '../store/mangekyouStore'; 18 | import mangekyouConstant from '../constant/mangekyouConstant'; 19 | import {version as mangekyouVersion} from '../../../../package.json'; 20 | 21 | const TitleBar = React.createClass({ 22 | getInitialState() { 23 | return { 24 | showing: mangekyouStore.getShowing(), 25 | currentImage: null, 26 | processingState: mangekyouStore.getProcessingState(), 27 | animating: false, 28 | intervalId: Infinity, 29 | keyMap: [ 30 | { 31 | char: 'o', 32 | action: () => { this._handleAddImageClick(); }, 33 | }, 34 | { 35 | char: 'e', 36 | action: () => { this._handleExportImageClick(); }, 37 | }, 38 | ], 39 | }; 40 | }, 41 | componentDidMount() { 42 | mangekyouStore.addShowingChangeListener(this._onShowingChange); 43 | mangekyouStore.addHistoryChangeListener(this._onHistoryChange); 44 | mangekyouStore.addProcessingChangeListener(this._onProcessingChange); 45 | }, 46 | componentWillUnmount() { 47 | mangekyouStore.removeShowingChangeListener(this._onShowingChange); 48 | mangekyouStore.removeHistoryChangeListener(this._onHistoryChange); 49 | mangekyouStore.removeProcessingChangeListener(this._onProcessingChange); 50 | }, 51 | render() { 52 | return ( // eslint-disable-line no-extra-parens 53 |