├── .eslintignore ├── src ├── ui.ts ├── ui │ ├── service │ │ ├── event-hub.ts │ │ ├── cache-hub.ts │ │ ├── schema-parser.ts │ │ └── worker.ts │ ├── language │ │ ├── index.ts │ │ ├── zh-cn.ts │ │ └── en-us.ts │ ├── component │ │ ├── progress │ │ │ ├── index.less │ │ │ └── index.ts │ │ ├── action-sheet │ │ │ ├── index.less │ │ │ └── index.ts │ │ ├── mask │ │ │ ├── index.less │ │ │ └── index.ts │ │ └── loading │ │ │ ├── index.less │ │ │ └── index.ts │ ├── module │ │ ├── sketch │ │ │ ├── index.less │ │ │ └── index.ts │ │ ├── header │ │ │ ├── index.ts │ │ │ └── index.less │ │ ├── panel │ │ │ ├── index.ts │ │ │ └── index.less │ │ └── dashboard │ │ │ └── index.less │ ├── config │ │ ├── effect.ts │ │ ├── process.ts │ │ └── adjust.ts │ └── index.ts ├── index.ts ├── core │ ├── digit │ │ ├── filter │ │ │ ├── filter.ts │ │ │ ├── enum.ts │ │ │ └── index.ts │ │ ├── hsl │ │ │ ├── index.ts │ │ │ └── hsl.ts │ │ ├── rgba │ │ │ ├── rgb.ts │ │ │ ├── rgba.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── transform │ │ │ ├── static.ts │ │ │ ├── index.ts │ │ │ ├── hsl2rgb.ts │ │ │ └── rgb2hsl.ts │ │ ├── process │ │ │ ├── index.ts │ │ │ ├── grayscale.ts │ │ │ ├── invert.ts │ │ │ ├── hue.ts │ │ │ ├── lightness.ts │ │ │ ├── saturation.ts │ │ │ ├── sepia.ts │ │ │ ├── gamma.ts │ │ │ ├── posterize.ts │ │ │ ├── alpha.ts │ │ │ └── sobel.ts │ │ ├── effect │ │ │ └── index.ts │ │ └── digit-image-data.ts │ ├── canvas │ │ └── render.ts │ ├── layer │ │ ├── draw-action.ts │ │ └── index.ts │ ├── sketch │ │ └── index.ts │ └── sketchpad │ │ └── index.ts ├── digit.ts ├── worker.ts ├── browser.ts └── util │ ├── style.ts │ ├── event-emitter.ts │ ├── image-data.ts │ ├── istype.ts │ ├── image-file.ts │ ├── compress.ts │ └── sanbox.ts ├── example ├── css │ └── reset.css ├── image │ ├── lena.jpg │ ├── lena.png │ ├── lena-256.png │ ├── github-404.png │ ├── pexels-photo-001.jpg │ ├── pexels-photo-002.jpg │ ├── pexels-photo-003.jpg │ ├── pexels-photo-004.jpg │ └── pexels-photo-005.jpg ├── module │ ├── browser-sandbox-invert.html │ ├── browser-sandbox-sepia.html │ ├── browser-sandbox-sobel.html │ ├── browser-sandbox-hue.html │ ├── browser-sandbox-gamma.html │ ├── browser-sandbox-grayscale.html │ ├── browser-sandbox-alpha.html │ ├── browser-sandbox-posterize.html │ ├── browser-sandbox-lightness.html │ ├── browser-sandbox-saturation.html │ ├── browser-sandbox.html │ ├── digit-process-sepia.html │ ├── digit-transform.html │ ├── digit-process-invert.html │ ├── digit-process-grayscale.html │ ├── digit-process-gamma.html │ ├── digit-process-posterize.html │ ├── digit-process-sobel.html │ ├── digit-process-alpha.html │ ├── digit-process-hue.html │ ├── digit-process-lightness.html │ ├── digit-process-saturation.html │ ├── digit-effect.html │ ├── digit-debug.html │ └── pictool-ui.html └── index.html ├── .travis.yml ├── __tests__ ├── screenshot │ ├── digit-debug.jpg │ ├── digit-effect.jpg │ ├── browser-sandbox.jpg │ ├── digit-transform.jpg │ ├── browser-sandbox-hue.jpg │ ├── digit-process-alpha.jpg │ ├── digit-process-gamma.jpg │ ├── digit-process-hue.jpg │ ├── digit-process-sepia.jpg │ ├── digit-process-sobel.jpg │ ├── browser-sandbox-alpha.jpg │ ├── browser-sandbox-gamma.jpg │ ├── browser-sandbox-sepia.jpg │ ├── browser-sandbox-sobel.jpg │ ├── digit-process-invert.jpg │ ├── browser-sandbox-invert.jpg │ ├── digit-process-grayscale.jpg │ ├── digit-process-lightness.jpg │ ├── digit-process-posterize.jpg │ ├── digit-process-saturation.jpg │ ├── browser-sandbox-grayscale.jpg │ ├── browser-sandbox-lightness.jpg │ ├── browser-sandbox-posterize.jpg │ └── browser-sandbox-saturation.jpg ├── digit-transform.test.js ├── e2e.config.js ├── digit-effect.test.js ├── screenshot.js ├── digit-digitimagedata.test.js ├── digit-process.test.js ├── index.e2e.js └── digit-process.adjust.test.js ├── script ├── rollup.config.mini.js ├── rollup.config.prod.js ├── rollup.config.dev.js ├── config.js ├── rollup.config.js └── lib │ └── rollup-plugin-ascii.js ├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── LICENSE ├── .gitignore ├── package.json ├── README.md └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # **/*.js 2 | 3 | example/**/*.js 4 | build/ 5 | dist/ -------------------------------------------------------------------------------- /src/ui.ts: -------------------------------------------------------------------------------- 1 | import UI from './ui/index'; 2 | 3 | export default UI; -------------------------------------------------------------------------------- /example/css/reset.css: -------------------------------------------------------------------------------- 1 | html,body { 2 | padding: 0; 3 | margin: 0; 4 | } -------------------------------------------------------------------------------- /example/image/lena.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/lena.jpg -------------------------------------------------------------------------------- /example/image/lena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/lena.png -------------------------------------------------------------------------------- /example/image/lena-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/lena-256.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12.3.1' 4 | script: 5 | - npm install 6 | - npm run test -------------------------------------------------------------------------------- /example/image/github-404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/github-404.png -------------------------------------------------------------------------------- /example/image/pexels-photo-001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/pexels-photo-001.jpg -------------------------------------------------------------------------------- /example/image/pexels-photo-002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/pexels-photo-002.jpg -------------------------------------------------------------------------------- /example/image/pexels-photo-003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/pexels-photo-003.jpg -------------------------------------------------------------------------------- /example/image/pexels-photo-004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/pexels-photo-004.jpg -------------------------------------------------------------------------------- /example/image/pexels-photo-005.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/example/image/pexels-photo-005.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-debug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-debug.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-effect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-effect.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-transform.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-transform.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-hue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-hue.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-alpha.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-alpha.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-gamma.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-gamma.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-hue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-hue.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-sepia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-sepia.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-sobel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-sobel.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-alpha.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-alpha.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-gamma.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-gamma.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-sepia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-sepia.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-sobel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-sobel.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-invert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-invert.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-invert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-invert.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-grayscale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-grayscale.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-lightness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-lightness.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-posterize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-posterize.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/digit-process-saturation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/digit-process-saturation.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-grayscale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-grayscale.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-lightness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-lightness.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-posterize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-posterize.jpg -------------------------------------------------------------------------------- /__tests__/screenshot/browser-sandbox-saturation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenshenhai/pictool/HEAD/__tests__/screenshot/browser-sandbox-saturation.jpg -------------------------------------------------------------------------------- /src/ui/service/event-hub.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from './../../util/event-emitter'; 2 | 3 | const eventHub = new EventEmitter(); 4 | 5 | export default eventHub; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import browser from './browser'; 2 | import UI from './ui'; 3 | import digit from './digit'; 4 | 5 | export default { 6 | browser, 7 | digit, 8 | UI, 9 | } -------------------------------------------------------------------------------- /src/core/digit/filter/filter.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData } from './../digit-image-data'; 2 | 3 | export interface FilterOpts { 4 | imageData: DigitImageData; 5 | options?: any; 6 | } -------------------------------------------------------------------------------- /src/core/digit/hsl/index.ts: -------------------------------------------------------------------------------- 1 | import { HSLObject, HSLCell } from './hsl'; 2 | 3 | export class HSL { 4 | 5 | private _hslData: HSLObject = null; 6 | 7 | constructor(hslData: HSLObject, opts) { 8 | this._hslData = hslData; 9 | } 10 | } -------------------------------------------------------------------------------- /src/core/digit/hsl/hsl.ts: -------------------------------------------------------------------------------- 1 | export interface HSLCell { 2 | h: number; // [0, 360] 3 | s: number; // [0, 100] 4 | l: number; // [0, 100] 5 | } 6 | 7 | export interface HSLObject { 8 | data: HSLCell[]; 9 | width: number; 10 | height: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/digit.ts: -------------------------------------------------------------------------------- 1 | import digit from './core/digit/index'; 2 | import { Effect } from './core/digit/effect'; 3 | 4 | const { transform, process, DigitImageData, } = digit; 5 | export default { 6 | transform, 7 | process, 8 | DigitImageData, 9 | Effect, 10 | }; -------------------------------------------------------------------------------- /src/core/digit/rgba/rgb.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface RGBCell { 3 | r: number; // [0, 255] 4 | g: number; // [0, 255] 5 | b: number; // [0, 255] 6 | } 7 | 8 | 9 | export interface RGBObject { 10 | data: RGBCell[]; 11 | width: number; 12 | height: number; 13 | } -------------------------------------------------------------------------------- /src/ui/service/cache-hub.ts: -------------------------------------------------------------------------------- 1 | const cacheStorage: Map = new Map(); 2 | 3 | const cacheHub = { 4 | set(key, val) { 5 | cacheStorage.set(key, val); 6 | }, 7 | 8 | get(key) { 9 | return cacheStorage.get(key); 10 | } 11 | }; 12 | 13 | export default cacheHub; -------------------------------------------------------------------------------- /src/core/digit/index.ts: -------------------------------------------------------------------------------- 1 | import transform from './transform/index'; 2 | import process from './process/index'; 3 | import { DigitImageData } from './digit-image-data'; 4 | 5 | const digit = { 6 | transform, 7 | process, 8 | DigitImageData, 9 | } 10 | 11 | export default digit; 12 | -------------------------------------------------------------------------------- /src/core/digit/rgba/rgba.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface RGBACell { 3 | r: number; // [0, 255] 4 | g: number; // [0, 255] 5 | b: number; // [0, 255] 6 | a: number; // [0, 255] 7 | } 8 | 9 | 10 | export interface RGBAObject { 11 | data: RGBACell[]; 12 | width: number; 13 | height: number; 14 | } -------------------------------------------------------------------------------- /src/core/digit/filter/enum.ts: -------------------------------------------------------------------------------- 1 | export enum FilterEnum { 2 | lightness = 'lightness', 3 | hue = 'hue', 4 | saturation = 'saturation', 5 | alpha = 'alpha', 6 | origin = 'origin', 7 | invert = 'invert', 8 | grayscale = 'grayscale', 9 | natural = 'natural', 10 | lineDrawing = 'lineDrawing', 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | import * as filterMap from './core/digit/filter/index'; 2 | 3 | onmessage = function (event) { 4 | 5 | const filerAction = filterMap[event.data.key]; 6 | const result = filerAction(event.data.param); 7 | 8 | postMessage({ 9 | 'key': event.data.key, 10 | 'result': result 11 | }, ''); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/core/digit/transform/static.ts: -------------------------------------------------------------------------------- 1 | export const RGBA_MID = 255 / 2; 2 | export const RGBA_MAX = 255; 3 | export const RGBA_MIN = 0; 4 | 5 | export const H_MAX = 360; 6 | export const H_MIN = 0; 7 | 8 | export const S_MAX = 100; 9 | export const S_MIN = 0; 10 | export const S_MID = 50; 11 | 12 | export const L_MAX = 100; 13 | export const L_MIN = 0; 14 | export const L_MID = 50; 15 | -------------------------------------------------------------------------------- /src/core/canvas/render.ts: -------------------------------------------------------------------------------- 1 | const canvasRender = { 2 | 3 | renderImageData(canvas, imageData) { 4 | const ctx = canvas.getContext('2d'); 5 | canvas.width = imageData.width; 6 | canvas.height = imageData.height; 7 | ctx.clearRect(0, 0, canvas.width, canvas.height); 8 | ctx.putImageData(imageData, 0, 0); 9 | } 10 | 11 | } 12 | 13 | export default canvasRender; 14 | -------------------------------------------------------------------------------- /src/ui/language/index.ts: -------------------------------------------------------------------------------- 1 | import enUS from './en-us'; 2 | import zhCN from './zh-cn'; 3 | 4 | export interface LanguageType { 5 | [key: string]: string; 6 | } 7 | 8 | const baseLang: LanguageType = enUS; 9 | 10 | export function getLanguage(lang = 'en-us') { 11 | let result: LanguageType = baseLang; 12 | if (lang === 'zh-cn') { 13 | result = { ...{}, ...baseLang, ...zhCN }; 14 | } 15 | return result; 16 | } -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | import { getImageBySrc, getImageDataBySrc, } from './util/image-file'; 2 | import { compressImage } from './util/compress'; 3 | import { digitImageData2ImageData, imageData2DigitImageData, imageData2Base64 } from './util/image-data'; 4 | 5 | import { Sandbox } from './util/sanbox'; 6 | 7 | const util = { 8 | getImageBySrc, 9 | getImageDataBySrc, 10 | compressImage, 11 | imageData2Base64, 12 | digitImageData2ImageData, 13 | imageData2DigitImageData, 14 | }; 15 | 16 | export default { 17 | util, 18 | Sandbox, 19 | }; -------------------------------------------------------------------------------- /src/ui/language/zh-cn.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | PROCESS: '简单处理', 3 | EFFECT: '效果', 4 | ADJUST: '图像调节', 5 | 6 | PROCESS_ORIGIN: '原图', 7 | PROCESS_GRAYSCALE: '灰度', 8 | PROCESS_SOBEL: '边缘检测', 9 | PROCESS_INVERT: '反色', 10 | PROCESS_SEPIA: '褐色化', 11 | 12 | ADJUST_LIGHTNESS: '亮度', 13 | ADJUST_HUE: '色相', 14 | ADJUST_SATURATION: '饱和度', 15 | ADJUST_ALPHA: '透明度', 16 | ADJUST_GAMMA: '伽马', 17 | ADJUST_POSTERIZE: '色阶', 18 | 19 | EFFECT_ORIGIN: '原图', 20 | EFFECT_OLD: '怀旧', 21 | EFFECT_LINEDRAWING: '素描', 22 | EFFECT_OILDRAWING: '油画', 23 | EFFECT_NATURAL: '自然', 24 | } -------------------------------------------------------------------------------- /src/core/digit/process/index.ts: -------------------------------------------------------------------------------- 1 | import { grayscale } from './grayscale'; 2 | import { sobel } from './sobel'; 3 | import { invert } from './invert'; 4 | import { hue } from './hue'; 5 | import { lightness } from './lightness'; 6 | import { saturation } from './saturation'; 7 | import { alpha } from './alpha'; 8 | import { sepia } from './sepia'; 9 | import { posterize } from './posterize'; 10 | import { gamma } from './gamma'; 11 | 12 | 13 | export default { 14 | grayscale, 15 | sobel, 16 | invert, 17 | hue, 18 | saturation, 19 | lightness, 20 | alpha, 21 | sepia, 22 | posterize, 23 | gamma, 24 | } -------------------------------------------------------------------------------- /script/rollup.config.mini.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | 3 | const { uglify } = require('rollup-plugin-uglify'); 4 | const configList = require('./rollup.config'); 5 | 6 | const resolveFile = function(filePath) { 7 | return path.join(__dirname, '..', filePath) 8 | } 9 | 10 | configList.map((config, index) => { 11 | 12 | config.output.sourcemap = false; 13 | config.output.file = config.output.file.replace(/\.js/, '.min.js'); 14 | config.plugins = [ 15 | ...config.plugins, 16 | ...[ 17 | uglify(), 18 | ] 19 | ] 20 | 21 | return config; 22 | }) 23 | 24 | module.exports = configList; -------------------------------------------------------------------------------- /src/util/style.ts: -------------------------------------------------------------------------------- 1 | import istype from './istype'; 2 | 3 | export const mergeCSS2StyleAttr = function(cssMap = {}): string { 4 | const cssList = []; 5 | if (istype.json(cssMap) === true) { 6 | for (const key in cssMap) { 7 | let cssKey: string = `${key}`; 8 | let cssVal: string = `${cssMap[key]}`; 9 | cssKey = cssKey.trim(); 10 | cssVal = cssVal.trim(); 11 | cssList.push(`${cssKey}:${cssVal}`); 12 | } 13 | } 14 | const styleAttr = cssList.join('; '); 15 | return styleAttr; 16 | } 17 | 18 | export const parseStyleAttr2CSSMap = function() { 19 | const cssMap = {}; 20 | // TODO 21 | } -------------------------------------------------------------------------------- /script/rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | 3 | const { uglify } = require('rollup-plugin-uglify'); 4 | const configList = require('./rollup.config'); 5 | 6 | const resolveFile = function(filePath) { 7 | return path.join(__dirname, '..', filePath) 8 | } 9 | 10 | configList.map((config, index) => { 11 | 12 | config.output.sourcemap = false; 13 | // config.output.file = config.output.file.replace(/\.js/, '.min.js'); 14 | config.plugins = [ 15 | ...config.plugins, 16 | // ...[ 17 | // uglify(), 18 | // ] 19 | ] 20 | 21 | return config; 22 | }) 23 | 24 | module.exports = configList; -------------------------------------------------------------------------------- /src/ui/language/en-us.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | PROCESS: 'Process', 3 | EFFECT: 'Effect', 4 | ADJUST: 'Adjustment', 5 | 6 | PROCESS_ORIGIN: 'Origin', 7 | PROCESS_GRAYSCALE: 'Grayscale', 8 | PROCESS_SOBEL: 'Sobel', 9 | PROCESS_INVERT: 'Invert', 10 | PROCESS_SEPIA: 'Sepia', 11 | 12 | ADJUST_LIGHTNESS: 'Lightness', 13 | ADJUST_HUE: 'Hue', 14 | ADJUST_SATURATION: 'Saturation', 15 | ADJUST_ALPHA: 'Alpha', 16 | ADJUST_GAMMA: 'Gamma', 17 | ADJUST_POSTERIZE: 'Posterize', 18 | 19 | EFFECT_ORIGIN: 'Origin', 20 | EFFECT_OLD: 'Old', 21 | EFFECT_LINEDRAWING: 'LineDrawing', 22 | EFFECT_OILDRAWING: 'OilDrawing', 23 | EFFECT_NATURAL: 'Natural', 24 | } -------------------------------------------------------------------------------- /src/util/event-emitter.ts: -------------------------------------------------------------------------------- 1 | import istype from './istype'; 2 | 3 | class EventEmitter { 4 | 5 | private _listeners: Map; 6 | 7 | constructor() { 8 | this._listeners = new Map(); 9 | } 10 | 11 | on(eventKey, callback) { 12 | this._listeners.set(eventKey, callback); 13 | } 14 | 15 | remove(eventKey) { 16 | this._listeners.delete(eventKey); 17 | } 18 | 19 | trigger(eventKey, ...args) { 20 | let listener = this._listeners.get(eventKey); 21 | if (istype.function(listener)) { 22 | listener(...args); 23 | return true; 24 | } else { 25 | return false; 26 | } 27 | } 28 | 29 | } 30 | 31 | export default EventEmitter; 32 | -------------------------------------------------------------------------------- /src/ui/component/progress/index.less: -------------------------------------------------------------------------------- 1 | @process-height: 28px; 2 | @process-radius: 14px; 3 | 4 | .pictool-component-progress { 5 | width: 100%; 6 | height: @process-height; 7 | display: block; 8 | 9 | &.progress-hidden { 10 | display: none; 11 | } 12 | 13 | .pictool-progress-outer { 14 | position: relative; 15 | height: @process-height; 16 | width: 100%; 17 | background: #666666; 18 | border-radius: @process-radius; 19 | overflow: hidden; 20 | } 21 | 22 | .pictool-progress-inner { 23 | position: absolute; 24 | height: @process-height; 25 | width: 100%; 26 | background: #00d4ff; 27 | border-radius: @process-radius; 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /__tests__/digit-transform.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const digit = require('../dist/digit'); 3 | const expect = chai.expect 4 | 5 | const transform = digit.transform; 6 | 7 | describe( 'test: Pictool.digit.transform', ( ) => { 8 | it('transform.RGB2HSL', ( done ) => { 9 | const rgb = {r: 12, g: 15, b: 10}; 10 | const hsl = transform.RGB2HSL(rgb); 11 | const expectResult = {h:96, s:20, l:5}; 12 | expect(hsl).to.deep.equal(expectResult) 13 | done() 14 | }); 15 | 16 | it('transform.HSL2RGB', ( done ) => { 17 | const hsl = {h:96, s:20, l:5} ; 18 | const rgb = transform.HSL2RGB(hsl); 19 | const expectResult = {r: 12, g: 15, b: 10}; 20 | expect(rgb).to.deep.equal(expectResult) 21 | done() 22 | }) 23 | }) -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | "rules": { 8 | "@typescript-eslint/rule-name": "off", 9 | "indent": ["error", 2, { 10 | "SwitchCase": 1, 11 | "VariableDeclarator": 1, 12 | "outerIIFEBody": 1, 13 | "MemberExpression": 1, 14 | "FunctionDeclaration": { "parameters": 1, "body": 1 }, 15 | "FunctionExpression": { "parameters": 1, "body": 1 }, 16 | "CallExpression": { "arguments": 1 }, 17 | "ArrayExpression": 1, 18 | "ObjectExpression": 1, 19 | "ImportDeclaration": 1, 20 | "flatTernaryExpressions": false, 21 | "ignoreComments": false 22 | }], 23 | } 24 | } -------------------------------------------------------------------------------- /src/ui/component/action-sheet/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .pictool-component-actionsheet { 4 | display: none; 5 | background: #ffffff00; 6 | position: fixed; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | font-size: 14px; 11 | color: #333333; 12 | 13 | .pictool-actionsheet-container { 14 | position: absolute; 15 | left: 0; 16 | right: 0; 17 | bottom: 0; 18 | height: 100%; 19 | width: 100%; 20 | background: #ffffff; 21 | transition: 0.6s; 22 | } 23 | 24 | &.actionsheet-open { 25 | display: block; 26 | // .pictool-actionsheet-container { 27 | // bottom: 0; 28 | // } 29 | } 30 | 31 | 32 | .pictool-actionsheet-content { 33 | position: absolute; 34 | top: 0; 35 | bottom: 0; 36 | width: 100%; 37 | background: #ffffff; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/core/digit/rgba/index.ts: -------------------------------------------------------------------------------- 1 | import { HSL } from './../hsl/index'; 2 | import { RGBACell, } from './rgba'; 3 | 4 | export interface RGBAOpts { 5 | // TODO; 6 | } 7 | 8 | export class RGBA { 9 | 10 | private _imgData: ImageData = null; 11 | private _opts: RGBAOpts = null; 12 | private _hsl: HSL = null; 13 | constructor(imgData: ImageData, opts: RGBAOpts) { 14 | this._imgData = imgData; 15 | this._opts = opts; 16 | } 17 | 18 | private _toHSL(): HSL { 19 | if (this._hsl) { 20 | return this._hsl; 21 | } 22 | const imgData: ImageData = this._imgData; 23 | const { data, } = imgData; 24 | for(let i = 0; i < data.length; i+=4) { 25 | const r = data[i]; 26 | const g = data[i + 1]; 27 | const b = data[i + 2]; 28 | const a = data[i + 3]; 29 | const cell: RGBACell = {r, g, b, a}; 30 | } 31 | } 32 | 33 | public destroy() { 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /src/core/digit/process/grayscale.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData, DigitImageDataRGBA } from './../digit-image-data'; 2 | 3 | export const grayscale = function(imgData: DigitImageData): DigitImageData { 4 | 5 | const width: number = imgData.getWidth(); 6 | const height: number = imgData.getHeight(); 7 | const data: Uint8ClampedArray = imgData.getData(); 8 | const digitImg = new DigitImageData({width, height, data}); 9 | 10 | for (let x = 0; x < width; x ++) { 11 | for (let y = 0; y < height; y ++) { 12 | const idx = (width * y + x) * 4; 13 | const px: DigitImageDataRGBA = digitImg.pixelAt(x, y); 14 | const gray: number = Math.round((px.r + px.g + px.b) / 3); 15 | digitImg.setDataUnit(idx, gray); 16 | digitImg.setDataUnit(idx + 1, gray); 17 | digitImg.setDataUnit(idx + 2, gray); 18 | digitImg.setDataUnit(idx + 3, 255); 19 | } 20 | } 21 | 22 | return digitImg; 23 | } -------------------------------------------------------------------------------- /src/core/digit/process/invert.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData, DigitImageDataRGBA } from './../digit-image-data'; 2 | import { RGBA_MAX } from './../transform/static' 3 | 4 | export const invert = function(imgData: DigitImageData): DigitImageData { 5 | const width: number = imgData.getWidth(); 6 | const height: number = imgData.getHeight(); 7 | const data: Uint8ClampedArray = imgData.getData(); 8 | const digitImg = new DigitImageData({width, height, data}); 9 | 10 | for (let x = 0; x < width; x ++) { 11 | for (let y = 0; y < height; y ++) { 12 | const idx = (width * y + x) * 4; 13 | const px: DigitImageDataRGBA = digitImg.pixelAt(x, y); 14 | digitImg.setDataUnit(idx, RGBA_MAX - px.r); 15 | digitImg.setDataUnit(idx + 1, RGBA_MAX - px.g); 16 | digitImg.setDataUnit(idx + 2, RGBA_MAX - px.b); 17 | digitImg.setDataUnit(idx + 3, px.a); 18 | } 19 | } 20 | 21 | return digitImg; 22 | } -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 15.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm install 29 | - run: npm run build 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /script/rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development'; 2 | 3 | const path = require('path'); 4 | const serve = require('rollup-plugin-serve'); 5 | const configList = require('./rollup.config'); 6 | 7 | const resolveFile = function(filePath) { 8 | return path.join(__dirname, '..', filePath) 9 | } 10 | const PORT = 3000; 11 | 12 | const devSite = `http://127.0.0.1:${PORT}`; 13 | const devPath = path.join('example', 'index.html'); 14 | const devUrl = `${devSite}/${devPath}`; 15 | 16 | setTimeout(()=>{ 17 | console.log(`[dev]: ${devUrl}`) 18 | }, 1000); 19 | 20 | configList.map((config, index) => { 21 | 22 | config.output.sourcemap = true; 23 | 24 | if( index === 0 ) { 25 | config.plugins = [ 26 | ...config.plugins, 27 | ...[ 28 | serve({ 29 | port: PORT, 30 | contentBase: [resolveFile('')] 31 | }) 32 | ] 33 | ] 34 | } 35 | 36 | return config; 37 | }); 38 | 39 | module.exports = configList; -------------------------------------------------------------------------------- /src/ui/module/sketch/index.less: -------------------------------------------------------------------------------- 1 | .pictool-module-sketch { 2 | width: 100%; 3 | height: 100%; 4 | display: table; 5 | 6 | .pictool-sketch-container { 7 | width: 100%; 8 | height: 100%; 9 | display: table-row; 10 | } 11 | 12 | .pictool-sketch-main { 13 | width: 100%; 14 | height: 100%; 15 | display: table-cell; 16 | vertical-align: middle; 17 | text-align: center; 18 | // display: inline-block; 19 | 20 | user-select: none; 21 | background-position: 0px 0px, 10px 10px; 22 | background-size: 20px 20px; 23 | background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%),linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%); 24 | 25 | .pictool-sketch-canvas { 26 | max-height: 100%; 27 | max-width: 100%; 28 | } 29 | } 30 | 31 | .pictool-sketch-hidden-area { 32 | width: 0; 33 | height: 0; 34 | display: none; 35 | } 36 | } -------------------------------------------------------------------------------- /src/ui/config/effect.ts: -------------------------------------------------------------------------------- 1 | import { LanguageType } from './../language/index'; 2 | 3 | export interface EffectMenuItemType { 4 | name: string; 5 | filter: string; 6 | } 7 | 8 | export interface EffectMenuConfigType { 9 | title: string; 10 | menu: EffectMenuItemType[], 11 | } 12 | 13 | export function getEffectMenuConfig(lang: LanguageType): EffectMenuConfigType { 14 | const effectMenuConfig = { 15 | title: lang.EFFECT, 16 | menu: [ 17 | { 18 | name: lang.EFFECT_ORIGIN, 19 | filter: 'origin', 20 | }, 21 | { 22 | name: lang.EFFECT_OLD, 23 | filter: 'old', 24 | }, 25 | { 26 | name: lang.EFFECT_LINEDRAWING, 27 | filter: 'lineDrawing', 28 | }, 29 | { 30 | name: lang.EFFECT_OILDRAWING, 31 | filter: 'oilDrawing', 32 | }, 33 | { 34 | name: lang.EFFECT_NATURAL, 35 | filter: 'natural', 36 | } 37 | ] 38 | } 39 | return effectMenuConfig; 40 | } -------------------------------------------------------------------------------- /src/ui/config/process.ts: -------------------------------------------------------------------------------- 1 | import { LanguageType } from './../language/index'; 2 | 3 | export interface ProcessMenuItemType { 4 | name: string; 5 | filter: string; 6 | } 7 | 8 | export interface ProcessMenuConfigType { 9 | title: string; 10 | menu: ProcessMenuItemType[], 11 | } 12 | 13 | export function getProcessMenuConfig(lang: LanguageType): ProcessMenuConfigType { 14 | const processMenuConfig: ProcessMenuConfigType = { 15 | title: lang.PROCESS, 16 | menu: [ 17 | { 18 | name: lang.PROCESS_ORIGIN, 19 | filter: 'origin', 20 | }, 21 | { 22 | name: lang.PROCESS_GRAYSCALE, 23 | filter: 'grayscale', 24 | }, 25 | { 26 | name: lang.PROCESS_SOBEL, 27 | filter: 'sobel', 28 | }, 29 | { 30 | name: lang.PROCESS_INVERT, 31 | filter: 'invert', 32 | }, 33 | { 34 | name: lang.PROCESS_SEPIA, 35 | filter: 'sepia', 36 | } 37 | ] 38 | }; 39 | return processMenuConfig; 40 | } -------------------------------------------------------------------------------- /src/core/digit/process/hue.ts: -------------------------------------------------------------------------------- 1 | import { transformDigitImageData } from '../transform/index'; 2 | import { DigitImageData } from '../digit-image-data'; 3 | import { 4 | HSLTransformPercent, 5 | HSLTransformValue 6 | } from '../transform/rgb2hsl'; 7 | 8 | export interface LightnessOpts { 9 | percent?: number; // [-100, 100] 10 | value?: number; // [0, 360] 11 | } 12 | 13 | export const hue = function( 14 | imgData: DigitImageData, 15 | opts: LightnessOpts 16 | ): DigitImageData { 17 | const width: number = imgData.getWidth(); 18 | const height: number = imgData.getHeight(); 19 | const data: Uint8ClampedArray = imgData.getData(); 20 | let digitImg: DigitImageData = new DigitImageData({width, height, data}); 21 | 22 | let percent: HSLTransformPercent|undefined = undefined; 23 | let value: HSLTransformValue|undefined = undefined; 24 | if (opts.value) { 25 | value = { h: opts.value } 26 | } else if (opts.percent) { 27 | percent = { h: opts.percent } 28 | } 29 | digitImg = transformDigitImageData(digitImg, {percent, value}); 30 | 31 | return digitImg; 32 | } -------------------------------------------------------------------------------- /src/core/digit/process/lightness.ts: -------------------------------------------------------------------------------- 1 | import { transformDigitImageData } from '../transform/index'; 2 | import { DigitImageData } from '../digit-image-data'; 3 | import { 4 | HSLTransformPercent, 5 | HSLTransformValue 6 | } from '../transform/rgb2hsl'; 7 | 8 | export interface LightnessOpts { 9 | percent?: number; // [-100, 100] 10 | value?: number; // [0, 100] 11 | } 12 | 13 | export const lightness = function( 14 | imgData: DigitImageData, 15 | opts: LightnessOpts 16 | ): DigitImageData { 17 | const width: number = imgData.getWidth(); 18 | const height: number = imgData.getHeight(); 19 | const data: Uint8ClampedArray = imgData.getData(); 20 | let digitImg: DigitImageData = new DigitImageData({width, height, data}); 21 | 22 | let percent: HSLTransformPercent|undefined = undefined; 23 | let value: HSLTransformValue|undefined = undefined; 24 | if (opts.value) { 25 | value = { l: opts.value } 26 | } else if (opts.percent) { 27 | percent = { l: opts.percent } 28 | } 29 | digitImg = transformDigitImageData(digitImg, {percent, value}); 30 | 31 | return digitImg; 32 | } -------------------------------------------------------------------------------- /src/core/digit/process/saturation.ts: -------------------------------------------------------------------------------- 1 | import { transformDigitImageData } from '../transform/index'; 2 | import { DigitImageData } from '../digit-image-data'; 3 | import { 4 | HSLTransformPercent, 5 | HSLTransformValue 6 | } from '../transform/rgb2hsl'; 7 | 8 | export interface LightnessOpts { 9 | percent?: number; // [-100, 100] 10 | value?: number; // [0, 100] 11 | } 12 | 13 | export const saturation = function( 14 | imgData: DigitImageData, 15 | opts: LightnessOpts 16 | ): DigitImageData { 17 | const width: number = imgData.getWidth(); 18 | const height: number = imgData.getHeight(); 19 | const data: Uint8ClampedArray = imgData.getData(); 20 | let digitImg: DigitImageData = new DigitImageData({width, height, data}); 21 | 22 | let percent: HSLTransformPercent|undefined = undefined; 23 | let value: HSLTransformValue|undefined = undefined; 24 | if (opts.value) { 25 | value = { s: opts.value } 26 | } else if (opts.percent) { 27 | percent = { s: opts.percent } 28 | } 29 | digitImg = transformDigitImageData(digitImg, {percent, value}); 30 | 31 | return digitImg; 32 | } -------------------------------------------------------------------------------- /__tests__/e2e.config.js: -------------------------------------------------------------------------------- 1 | const exampleModuleList = [ 2 | 'browser-sandbox-alpha.html', 3 | 'browser-sandbox-gamma.html', 4 | 'browser-sandbox-grayscale.html', 5 | 'browser-sandbox-hue.html', 6 | 'browser-sandbox-invert.html', 7 | 'browser-sandbox-lightness.html', 8 | 'browser-sandbox-posterize.html', 9 | 'browser-sandbox-saturation.html', 10 | 'browser-sandbox-sepia.html', 11 | 'browser-sandbox-sobel.html', 12 | 'browser-sandbox.html', 13 | 'digit-debug.html', 14 | 'digit-effect.html', 15 | 'digit-process-alpha.html', 16 | 'digit-process-gamma.html', 17 | 'digit-process-grayscale.html', 18 | 'digit-process-hue.html', 19 | 'digit-process-invert.html', 20 | 'digit-process-lightness.html', 21 | 'digit-process-posterize.html', 22 | 'digit-process-saturation.html', 23 | 'digit-process-sepia.html', 24 | 'digit-process-sobel.html', 25 | 'digit-transform.html', 26 | // 'pictool-ui.html', 27 | ]; 28 | 29 | const port = 3001; 30 | const width = 400; 31 | const height = 600; 32 | 33 | module.exports = { 34 | exampleModuleList, 35 | port, 36 | width, 37 | height, 38 | } -------------------------------------------------------------------------------- /src/core/digit/process/sepia.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData, DigitImageDataRGBA } from './../digit-image-data'; 2 | 3 | export const sepia = function(imgData: DigitImageData): DigitImageData { 4 | const width: number = imgData.getWidth(); 5 | const height: number = imgData.getHeight(); 6 | const data: Uint8ClampedArray = imgData.getData(); 7 | const digitImg = new DigitImageData({width, height, data}); 8 | 9 | for (let x = 0; x < width; x ++) { 10 | for (let y = 0; y < height; y ++) { 11 | const idx = (width * y + x) * 4; 12 | const px: DigitImageDataRGBA = digitImg.pixelAt(x, y); 13 | 14 | const r = Math.floor((px.r * 0.393) + (px.g * 0.769) + (px.b * 0.189)); 15 | const g = Math.floor((px.r * 0.349) + (px.g * 0.686) + (px.b * 0.168)); 16 | const b = Math.floor((px.r * 0.272) + (px.g * 0.534) + (px.b * 0.131)); 17 | const a = px.a; 18 | 19 | digitImg.setDataUnit(idx, r); 20 | digitImg.setDataUnit(idx + 1, g); 21 | digitImg.setDataUnit(idx + 2, b); 22 | digitImg.setDataUnit(idx + 3, a); 23 | } 24 | } 25 | 26 | return digitImg; 27 | } -------------------------------------------------------------------------------- /script/config.js: -------------------------------------------------------------------------------- 1 | const config = [ 2 | { 3 | input: 'src/index.ts', 4 | output: { 5 | file: 'dist/index.js', 6 | format: 'umd', 7 | name: 'Pictool', 8 | amd: { 9 | id: 'Pictool' 10 | } 11 | } 12 | }, 13 | { 14 | input: 'src/ui.ts', 15 | output: { 16 | file: 'dist/ui.js', 17 | format: 'umd', 18 | name: 'Pictool.UI', 19 | amd: { 20 | id: 'Pictool.UI' 21 | } 22 | } 23 | }, 24 | { 25 | input: 'src/worker.ts', 26 | output: { 27 | file: 'dist/worker.js', 28 | format: 'iife', 29 | }, 30 | }, 31 | { 32 | input: 'src/digit.ts', 33 | output: { 34 | file: 'dist/digit.js', 35 | format: 'umd', 36 | name: 'Pictool.digit', 37 | amd: { 38 | id: 'Pictool.digit' 39 | } 40 | } 41 | }, 42 | { 43 | input: 'src/browser.ts', 44 | output: { 45 | file: 'dist/browser.js', 46 | format: 'umd', 47 | name: 'Pictool.browser', 48 | amd: { 49 | id: 'Pictool.browser' 50 | } 51 | } 52 | } 53 | ]; 54 | 55 | module.exports = config; 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 大深海 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ui/component/mask/index.less: -------------------------------------------------------------------------------- 1 | @header-height: 40px; 2 | @footer-height: 120px; 3 | 4 | 5 | .pictool-component-mask { 6 | display: none; 7 | background: #0000007a; 8 | position: fixed; 9 | left: 0; 10 | right: 0; 11 | top: 0; 12 | bottom: 0; 13 | 14 | .pictool-mask-container { 15 | position: absolute; 16 | left: 0; 17 | right: 0; 18 | bottom: -100%; 19 | height: 100%; 20 | width: 100%; 21 | background: #ffffff; 22 | transition: 0.6s; 23 | } 24 | 25 | &.mask-open { 26 | display: block; 27 | .pictool-mask-container { 28 | bottom: 0; 29 | } 30 | } 31 | 32 | .pictool-mask-header { 33 | position: absolute; 34 | top: 0; 35 | width: 100%; 36 | height: @header-height; 37 | background: #222222; 38 | } 39 | 40 | .pictool-mask-content { 41 | position: absolute; 42 | top: @header-height; 43 | bottom: @footer-height; 44 | width: 100%; 45 | background: #333333; 46 | } 47 | 48 | .pictool-mask-footer { 49 | position: absolute; 50 | bottom: 0; 51 | width: 100%; 52 | height: @footer-height; 53 | background: #222222; 54 | } 55 | } -------------------------------------------------------------------------------- /src/ui/service/schema-parser.ts: -------------------------------------------------------------------------------- 1 | import { SketchSchema } from '../../core/sketch/index'; 2 | 3 | const schemaParser = { 4 | parseImageData(schema: SketchSchema): ImageData { 5 | const layerList = schema.layerList; 6 | const layer = layerList[0]; 7 | const drawActionList = layer.drawActionList; 8 | const action = drawActionList[0]; 9 | const actionArgs = action.args; 10 | const imageData = actionArgs[0]; 11 | return imageData; 12 | }, 13 | 14 | parseImageDataToSchema(imageData: ImageData): SketchSchema { 15 | const schema = { 16 | layerList: [ 17 | { 18 | key: 'image', 19 | drawActionList: [{ 20 | method: 'putImageData', 21 | args: [imageData, 0, 0], 22 | }], 23 | }, 24 | ] 25 | } 26 | return schema; 27 | }, 28 | 29 | updateSchemaImageData(schema: SketchSchema, imageData: ImageData) { 30 | const layerList = schema.layerList; 31 | const layer = layerList[0]; 32 | const drawActionList = layer.drawActionList; 33 | const action = drawActionList[0]; 34 | const actionArgs = action.args; 35 | actionArgs[0] = imageData; 36 | return schema; 37 | } 38 | } 39 | 40 | export default schemaParser; 41 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-invert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 45 | 46 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-sepia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-sobel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-hue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-gamma.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-grayscale.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-alpha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-posterize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 45 | 46 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-lightness.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | 64 | test.html 65 | dist/*.map 66 | dist/**/*.map 67 | screenshot-diff/ 68 | 69 | -------------------------------------------------------------------------------- /example/module/browser-sandbox-saturation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/browser-sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 50 | 51 | -------------------------------------------------------------------------------- /src/core/digit/transform/index.ts: -------------------------------------------------------------------------------- 1 | import { HSL2RGB } from './hsl2rgb'; 2 | import { RGB2HSL, HSLTransformOpts } from './rgb2hsl'; 3 | import { HSLCell, } from './../hsl/hsl'; 4 | import { RGBCell, } from './../rgba/rgb'; 5 | import { DigitImageData } from './../digit-image-data'; 6 | 7 | export const transformDigitImageData = function(digitImageData: DigitImageData, opts: HSLTransformOpts): DigitImageData { 8 | const width: number = digitImageData.getWidth(); 9 | const height: number = digitImageData.getHeight(); 10 | const data: Uint8ClampedArray = digitImageData.getData(); 11 | const rsImageData = new DigitImageData({width, height, data}); 12 | for(let i = 0; i < data.length; i += 4) { 13 | const r = data[i]; 14 | const g = data[i + 1]; 15 | const b = data[i + 2]; 16 | const a = data[i + 3]; 17 | const cell: RGBCell = {r, g, b}; 18 | const hslCell: HSLCell = RGB2HSL(cell, opts); 19 | const rsHsl: HSLCell = { ...{}, ...hslCell, } 20 | 21 | const rgbCell = HSL2RGB(rsHsl); 22 | rsImageData.setDataUnit(i, rgbCell.r); 23 | rsImageData.setDataUnit(i + 1, rgbCell.g); 24 | rsImageData.setDataUnit(i + 2, rgbCell.b); 25 | rsImageData.setDataUnit(i + 3, a); 26 | } 27 | digitImageData.destory(); 28 | // digitImageData = null; 29 | return rsImageData 30 | } 31 | 32 | const transform = { 33 | HSL2RGB, 34 | RGB2HSL, 35 | } 36 | 37 | export default transform; 38 | -------------------------------------------------------------------------------- /src/core/digit/effect/index.ts: -------------------------------------------------------------------------------- 1 | import process from './../process/index'; 2 | import { DigitImageData } from './../digit-image-data'; 3 | import { digitImageData2ImageData, imageData2DigitImageData } from '../../../util/image-data'; 4 | 5 | export class Effect { 6 | private _digitImageData: DigitImageData|null = null; 7 | 8 | constructor(imageData: DigitImageData) { 9 | this._digitImageData = imageData; 10 | // if (imageData instanceof DigitImageData) { 11 | // this._digitImageData = imageData; 12 | // } else { 13 | // this._digitImageData = imageData2DigitImageData(imageData); 14 | // } 15 | } 16 | 17 | public process(method: string, opts?: any): Effect { 18 | if (process && typeof process[method] !== 'function') { 19 | throw new Error(`Pictool.digit.process.${method} is not a function `); 20 | } 21 | this._digitImageData = process[method](this._digitImageData, opts); 22 | return this; 23 | } 24 | 25 | public getImageData(): ImageData|null { 26 | if (this._digitImageData) { 27 | const imageData = digitImageData2ImageData(this._digitImageData); 28 | return imageData; 29 | } else { 30 | return null; 31 | } 32 | } 33 | 34 | public getDigitImageData(): DigitImageData|null { 35 | return this._digitImageData; 36 | } 37 | 38 | public destory () { 39 | if (this._digitImageData) { 40 | this._digitImageData.destory(); 41 | this._digitImageData = null; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/util/image-data.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData } from './../core/digit/digit-image-data'; 2 | 3 | export const digitImageData2ImageData = function(digitImgData: DigitImageData): ImageData { 4 | const data: Uint8ClampedArray = digitImgData.getData(); 5 | const width: number = digitImgData.getWidth(); 6 | const height: number = digitImgData.getHeight(); 7 | const imgData = new ImageData(width, height); 8 | data.forEach(function(num, i) { 9 | imgData.data[i] = num; 10 | }) 11 | return imgData; 12 | } 13 | 14 | export const imageData2DigitImageData = function(imgData: ImageData): DigitImageData { 15 | const { data, width, height } = imgData; 16 | const digitImgData = new DigitImageData({width, height, data}); 17 | return digitImgData; 18 | } 19 | 20 | /** 21 | * 22 | * @param {ImageData} imageData 23 | * @param {object} opts 24 | * opts.type 'image/png' 'image/jpg' 25 | * opts.encoderOptions [0, 1] 26 | */ 27 | export const imageData2Base64 = function(imageData: ImageData, opts = {type: 'image/png', encoderOptions: 1 }): string|null { 28 | const canvas = document.createElement('canvas'); 29 | const ctx = canvas.getContext('2d'); 30 | canvas.width = imageData.width; 31 | canvas.height = imageData.height; 32 | let base64:string|null = null; 33 | if (ctx) { 34 | ctx.clearRect(0, 0, canvas.width, canvas.height); 35 | ctx.putImageData(imageData, 0, 0); 36 | base64 = canvas.toDataURL(opts.type, opts.encoderOptions); 37 | } 38 | return base64; 39 | } -------------------------------------------------------------------------------- /src/util/istype.ts: -------------------------------------------------------------------------------- 1 | function parsePrototype (data: any) { 2 | const typeStr = Object.prototype.toString.call(data) || ''; 3 | const result = typeStr.replace(/(\[object|\])/ig, '').trim(); 4 | return result; 5 | }; 6 | const istype = { 7 | 8 | type(data: any, lowerCase?: boolean) { 9 | let result = parsePrototype(data); 10 | return lowerCase === true ? result.toLocaleLowerCase() : result; 11 | }, 12 | 13 | array (data: any) { 14 | return parsePrototype(data) === 'Array'; 15 | }, 16 | 17 | json (data: any) { 18 | return parsePrototype(data) === 'Object'; 19 | }, 20 | 21 | function (data: any) { 22 | return parsePrototype(data) === 'Function'; 23 | }, 24 | 25 | asyncFunction (data: any) { 26 | return parsePrototype(data) === 'AsyncFunction'; 27 | }, 28 | 29 | string (data: any) { 30 | return parsePrototype(data) === 'String'; 31 | }, 32 | 33 | number (data: any) { 34 | return parsePrototype(data) === 'Number'; 35 | }, 36 | 37 | undefined (data: any) { 38 | return parsePrototype(data) === 'Undefined'; 39 | }, 40 | 41 | null (data: any) { 42 | return parsePrototype(data) === 'Null'; 43 | }, 44 | 45 | promise (data: any) { 46 | return parsePrototype(data) === 'Promise'; 47 | }, 48 | 49 | nodeList (data: any) { 50 | return parsePrototype(data) === 'NodeList'; 51 | }, 52 | 53 | imageData(data: any) { 54 | return parsePrototype(data) === 'ImageData'; 55 | } 56 | 57 | }; 58 | 59 | export default istype; -------------------------------------------------------------------------------- /src/util/image-file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} imageSrc 3 | * @return {promise} 4 | */ 5 | export const getImageBySrc = function(imageSrc: string): Promise { 6 | const img: HTMLImageElement = document.createElement('img'); 7 | return new Promise(function(resolve, reject) { 8 | img.onload = function(){ 9 | resolve(img); 10 | } 11 | img.onerror = function() { 12 | reject(new Error('GET_IMAGE_SRC_ERROR')); 13 | } 14 | img.src = imageSrc; 15 | }); 16 | }; 17 | 18 | 19 | /** 20 | * @param {string} imageSrc 21 | * @return {promise} 22 | */ 23 | export const getImageDataBySrc = function(imageSrc: string): Promise { 24 | return new Promise(function(resolve, reject) { 25 | getImageBySrc(imageSrc).then(function(img: HTMLImageElement){ 26 | const canvas: HTMLCanvasElement = document.createElement('canvas'); 27 | const drawWidth: number = img.width; 28 | const drawHeight: number = img.height; 29 | canvas.width = drawWidth; 30 | canvas.height = drawHeight; 31 | const ctx: CanvasRenderingContext2D|null = canvas.getContext('2d'); 32 | if (ctx) { 33 | ctx.clearRect(0, 0, canvas.width, canvas.height); 34 | ctx.drawImage(img, 0, 0, drawWidth, drawHeight); 35 | const imgData = ctx.getImageData(0, 0, drawWidth, drawHeight); 36 | resolve(imgData); 37 | } else { 38 | reject(new Error(`canvas.getContext('2d') is null`)) 39 | } 40 | }).catch(function(err) { 41 | reject(err); 42 | }); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/ui/module/header/index.ts: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | import istype from '../../../util/istype'; 3 | 4 | export interface HeaderOpts { 5 | closeFeedback: Function; 6 | saveFeedback: Function; 7 | } 8 | 9 | export class Header { 10 | private _mount: HTMLElement = null; 11 | private _opts: HeaderOpts = null; 12 | private _hasRendered: boolean = false; 13 | 14 | constructor(mount: HTMLElement, opts: HeaderOpts) { 15 | this._mount = mount; 16 | this._opts = opts; 17 | this._render(); 18 | } 19 | 20 | private _render() { 21 | if (this._hasRendered === true) { 22 | return; 23 | } 24 | const html = ` 25 |
26 |
27 |
28 |
29 | `; 30 | this._mount.innerHTML = html; 31 | this._registerEvent(); 32 | this._hasRendered = true; 33 | } 34 | 35 | private _registerEvent() { 36 | const btnClose = this._mount.querySelector('div.pictool-header-btn-close'); 37 | const btnSave = this._mount.querySelector('div.pictool-header-btn-save'); 38 | const options = this._opts; 39 | btnClose.addEventListener('click', function() { 40 | if (istype.function(options.closeFeedback)) { 41 | options.closeFeedback(); 42 | } 43 | }); 44 | 45 | btnSave.addEventListener('click', function() { 46 | if (istype.function(options.saveFeedback)) { 47 | options.saveFeedback(); 48 | } 49 | }); 50 | } 51 | 52 | } 53 | 54 | export default Header; -------------------------------------------------------------------------------- /src/core/layer/draw-action.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Authors chenshenhai. All rights reserved. MIT license. 2 | // https://github.com/chenshenhai/logox/blob/master/src/layer/draw_action.ts 3 | 4 | import istype from './../../util/istype'; 5 | 6 | const context2dRenderActionMap = { 7 | fillStyle: { 8 | type: 'attribute', 9 | argumentsType: 'string', 10 | executeAction(ctx: CanvasRenderingContext2D, args: string) { 11 | ctx.fillStyle = args; 12 | } 13 | }, 14 | 15 | fillRect: { 16 | type: 'function', 17 | argumentsType: 'array', 18 | executeAction(ctx: CanvasRenderingContext2D, args: number[]) { 19 | ctx.fillRect(args[0], args[1], args[2], args[3]); 20 | } 21 | }, 22 | 23 | clearRect: { 24 | type: 'function', 25 | argumentsType: 'array', 26 | executeAction(ctx: CanvasRenderingContext2D, args: number[]) { 27 | ctx.clearRect(args[0], args[1], args[2], args[3]); 28 | } 29 | }, 30 | 31 | putImageData: { 32 | type: 'function', 33 | argumentsType: 'array', 34 | executeAction(ctx: CanvasRenderingContext2D, args: any[]) { 35 | ctx.putImageData(args[0], args[1], args[2]); 36 | } 37 | }, 38 | } 39 | 40 | 41 | const drawAction = function(ctx: CanvasRenderingContext2D, method: string, args: any) { 42 | const action = context2dRenderActionMap[method]; 43 | if (action && istype.type(args, true) === action.argumentsType) { 44 | action.executeAction(ctx, args); 45 | } else { 46 | console.warn(`Layer can't support execute context.${method}`); 47 | } 48 | } 49 | 50 | export default drawAction; -------------------------------------------------------------------------------- /src/ui/component/loading/index.less: -------------------------------------------------------------------------------- 1 | @loading-result: 90%; 2 | 3 | @keyframes loading-animate { 4 | 0% { 5 | width: 0%; 6 | } 7 | 100% { 8 | width: @loading-result; 9 | } 10 | } 11 | .pictool-component-loading { 12 | position: fixed; 13 | left: 0; 14 | right: 0; 15 | top: 0; 16 | bottom: 0; 17 | z-index: 1000; 18 | background: #000000aa; 19 | display: none; 20 | 21 | &.loading-show { 22 | display: block; 23 | 24 | .pictool-loading-inner { 25 | height: 100%; 26 | width: @loading-result; 27 | background: -webkit-linear-gradient(left,#4bf0f882,#3d7bd9); 28 | background: -moz-linear-gradient(left,#4bf0f882,hsl(216, 67%, 55%)); 29 | background: -o-linear-gradient(left,#4bf0f882,#3d7bd9); 30 | background: -ms-linear-gradient(left,#4bf0f882,#3d7bd9); 31 | background: linear-gradient(left,#4bf0f882,#3d7bd9); 32 | border-radius:10px; 33 | box-shadow: inset 0 -2px 2px rgba(0, 0, 0, 0.2); 34 | animation: loading-animate 2s linear; 35 | } 36 | 37 | .pictool-loading-outer { 38 | position: absolute; 39 | top: 50%; 40 | left: 10%; 41 | right: 10%; 42 | height: 10px; 43 | border-radius:10px; 44 | background: -webkit-linear-gradient(left,#e4e3e4,#e4e5e4); 45 | background: -moz-linear-gradient(left,#e4e3e4,#e4e5e4); 46 | background: -o-linear-gradient(left,#e4e3e4,#e4e5e4); 47 | background: -ms-linear-gradient(left,#e4e3e4,#e4e5e4); 48 | background: linear-gradient(left,#e4e3e4,#e4e5e4); 49 | } 50 | 51 | } 52 | 53 | 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/core/digit/process/gamma.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData, DigitImageDataRGBA } from '../digit-image-data'; 2 | import { RGBA_MAX } from '../transform/static'; 3 | 4 | export interface GammaOpts { 5 | value?: number; // [0, 100] 6 | } 7 | 8 | 9 | function isGammaValue(num: number) { 10 | if(num >= 0 && num <= 100) { 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | } 16 | 17 | 18 | export const gamma = function(imgData: DigitImageData, opts: GammaOpts = {}): DigitImageData { 19 | const width: number = imgData.getWidth(); 20 | const height: number = imgData.getHeight(); 21 | const data: Uint8ClampedArray = imgData.getData(); 22 | const digitImg = new DigitImageData({width, height, data}); 23 | 24 | let value: number|undefined = opts.value || 16; 25 | 26 | if (value && isGammaValue(value)) { 27 | value = Math.min(100, value); 28 | value = Math.max(0, value); 29 | } 30 | 31 | const gammaVal = ((value + 100) / 200) * 2; 32 | 33 | for (let x = 0; x < width; x ++) { 34 | for (let y = 0; y < height; y ++) { 35 | const idx = (width * y + x) * 4; 36 | const px: DigitImageDataRGBA = digitImg.pixelAt(x, y); 37 | 38 | const r = Math.floor(Math.pow(px.r, gammaVal)); 39 | const g = Math.floor(Math.pow(px.g, gammaVal)); 40 | const b = Math.floor(Math.pow(px.b, gammaVal)); 41 | const a = px.a; 42 | 43 | digitImg.setDataUnit(idx, r); 44 | digitImg.setDataUnit(idx + 1, g); 45 | digitImg.setDataUnit(idx + 2, b); 46 | digitImg.setDataUnit(idx + 3, a); 47 | } 48 | } 49 | 50 | return digitImg; 51 | } -------------------------------------------------------------------------------- /src/core/digit/process/posterize.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData, DigitImageDataRGBA } from '../digit-image-data'; 2 | import { RGBA_MAX } from './../transform/static'; 3 | 4 | export interface PosterizeOpts { 5 | value?: number; // [0, 100] 6 | } 7 | 8 | 9 | function isPosterizeValue(num: number) { 10 | if(num >= 0 && num <= 100) { 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | } 16 | 17 | export const posterize = function(imgData: DigitImageData, opts: PosterizeOpts = {}): DigitImageData { 18 | const width: number = imgData.getWidth(); 19 | const height: number = imgData.getHeight(); 20 | const data: Uint8ClampedArray = imgData.getData(); 21 | const digitImg = new DigitImageData({width, height, data}); 22 | 23 | let value: number|undefined = opts.value || 100; 24 | 25 | if (value && isPosterizeValue(value)) { 26 | value = Math.min(100, value); 27 | value = Math.max(0, value); 28 | } 29 | 30 | const step1 = RGBA_MAX / value; 31 | const step2 = step1; 32 | 33 | for (let x = 0; x < width; x ++) { 34 | for (let y = 0; y < height; y ++) { 35 | const idx = (width * y + x) * 4; 36 | const px: DigitImageDataRGBA = digitImg.pixelAt(x, y); 37 | 38 | const r = Math.floor(Math.floor(px.r / step1) * step2); 39 | const g = Math.floor(Math.floor(px.g / step1) * step2); 40 | const b = Math.floor(Math.floor(px.b / step1) * step2); 41 | const a = px.a; 42 | 43 | digitImg.setDataUnit(idx, r); 44 | digitImg.setDataUnit(idx + 1, g); 45 | digitImg.setDataUnit(idx + 2, b); 46 | digitImg.setDataUnit(idx + 3, a); 47 | } 48 | } 49 | 50 | return digitImg; 51 | } -------------------------------------------------------------------------------- /__tests__/digit-effect.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const digit = require('../dist/digit'); 3 | const expect = chai.expect 4 | const Effect = digit.Effect; 5 | const DigitImageData = digit.DigitImageData; 6 | 7 | const img = require('./data/image-data-origin.json'); 8 | const imgEffect = require('./data/image-data-effect.json'); 9 | 10 | 11 | describe( 'test: Pictool.digit.Effect', ( ) => { 12 | 13 | it('Effect.process(..).getDigitImageData()', ( done ) => { 14 | 15 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 16 | const expectImg = imgEffect; 17 | 18 | const effect = new Effect(digitImg) 19 | const digitImgRs = effect.process('sobel', {}).process('invert', {}).getDigitImageData(); 20 | 21 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 22 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 23 | const rsData = digitImgRs.getData(); 24 | expectImg.data.forEach(function(num, i) { 25 | // console.log(`expect index is: ${i}`) 26 | expect(rsData[i]).to.deep.equal(num); 27 | }); 28 | 29 | done() 30 | }); 31 | 32 | it('Effect.destory()', ( done ) => { 33 | 34 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 35 | 36 | const effect = new Effect(digitImg) 37 | const digitImgRs = effect.process('invert', {}).getDigitImageData(); 38 | effect.destory(); 39 | 40 | expect(digitImgRs.getWidth()).to.deep.equal(0); 41 | expect(digitImgRs.getHeight()).to.deep.equal(0); 42 | expect(digitImgRs.getData()).to.deep.equal(new Uint8ClampedArray(0)); 43 | 44 | done() 45 | }); 46 | 47 | }) -------------------------------------------------------------------------------- /example/module/digit-process-sepia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 51 | 52 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Welcome to Pictool

9 | 14 | 15 | 48 | 49 | -------------------------------------------------------------------------------- /example/module/digit-transform.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 46 | 47 | -------------------------------------------------------------------------------- /example/module/digit-process-invert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 52 | 53 | -------------------------------------------------------------------------------- /example/module/digit-process-grayscale.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 52 | 53 | -------------------------------------------------------------------------------- /src/core/layer/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Authors chenshenhai. All rights reserved. MIT license. 2 | // https://github.com/chenshenhai/logox/blob/master/src/layer/mod.ts 3 | 4 | import drawAction from './draw-action'; 5 | 6 | export interface LayerDrawAction { 7 | method: string; 8 | args: any; 9 | } 10 | 11 | export interface LayerOptions { 12 | width: number; 13 | height: number; 14 | } 15 | 16 | export interface LayerSchema { 17 | key?: string; 18 | drawActionList: LayerDrawAction[]; 19 | } 20 | 21 | export class Layer { 22 | private _context: CanvasRenderingContext2D; 23 | private _layerSchema: LayerSchema; 24 | private _options: LayerOptions; 25 | 26 | constructor(context: CanvasRenderingContext2D, opts: LayerOptions) { 27 | this._context = context; 28 | this._options = opts; 29 | } 30 | 31 | getLayerContext() { 32 | return this._context; 33 | } 34 | 35 | clearDrawAction() { 36 | const { width, height } = this._options; 37 | this._context.clearRect(0, 0, width, height); 38 | this._layerSchema = { 39 | key: '', 40 | drawActionList: [] 41 | }; 42 | } 43 | 44 | 45 | private _executeDrawAction() { 46 | const schema: LayerSchema = this._layerSchema; 47 | const list: LayerDrawAction[] = schema.drawActionList; 48 | const context = this._context; 49 | for (let i = 0; i < list.length; i++) { 50 | const action = list[i]; 51 | drawAction(context, action.method, action.args); 52 | } 53 | } 54 | 55 | draw(layerSchema: LayerSchema) { 56 | this._layerSchema = layerSchema; 57 | this._executeDrawAction(); 58 | } 59 | 60 | getSchema(): LayerSchema { 61 | return this._layerSchema; 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /example/module/digit-process-gamma.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 52 | 53 | -------------------------------------------------------------------------------- /example/module/digit-process-posterize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 52 | 53 | -------------------------------------------------------------------------------- /src/ui/component/loading/index.ts: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | // import { mergeCSS2StyleAttr } from '../../util/style'; 4 | // import istype from '../../util/istype'; 5 | 6 | 7 | export interface LoadingOpts { 8 | mount?: HTMLElement; 9 | zIndex: number; // ms 10 | } 11 | 12 | export class Loading { 13 | private _options: LoadingOpts = null; 14 | private _hasRendered: boolean = false; 15 | private _component: HTMLElement = null; 16 | 17 | constructor(opts: LoadingOpts) { 18 | this._options = opts; 19 | this._render(); 20 | } 21 | 22 | private _render() { 23 | if (this._hasRendered === true) { 24 | return; 25 | } 26 | const options = this._options; 27 | const { mount, zIndex, } = options; 28 | 29 | const html = ` 30 |
31 |
32 |
33 |
34 |
35 | `; 36 | 37 | const tempDom = document.createElement('div');; 38 | tempDom.innerHTML = html; 39 | const component: HTMLDivElement = tempDom.querySelector('div.pictool-component-loading'); 40 | if (mount instanceof HTMLElement) { 41 | mount.appendChild(component); 42 | } else { 43 | const body = document.querySelector('body'); 44 | body.appendChild(component); 45 | } 46 | this._component = component; 47 | } 48 | 49 | show(timeout?: number) { 50 | const that = this; 51 | this._component.classList.add('loading-show'); 52 | if (timeout > 0) { 53 | setTimeout(function() { 54 | that.hide(); 55 | }, timeout) 56 | } 57 | } 58 | 59 | hide() { 60 | this._component.classList.remove('loading-show'); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /example/module/digit-process-sobel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 58 | 59 | -------------------------------------------------------------------------------- /example/module/digit-process-alpha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 58 | 59 | -------------------------------------------------------------------------------- /example/module/digit-process-hue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 59 | 60 | -------------------------------------------------------------------------------- /example/module/digit-process-lightness.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 59 | 60 | -------------------------------------------------------------------------------- /example/module/digit-process-saturation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 59 | 60 | -------------------------------------------------------------------------------- /example/module/digit-effect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 58 | 59 | -------------------------------------------------------------------------------- /__tests__/screenshot.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const http = require('http'); 3 | const fs = require('fs'); 4 | const puppeteer = require('puppeteer'); 5 | const serveHandler = require('serve-handler'); 6 | const jimp = require('jimp'); 7 | const compose = require('koa-compose'); 8 | const { exampleModuleList, port, width, height, } = require('./e2e.config'); 9 | const screenDir = path.join(__dirname, 'screenshot'); 10 | 11 | main(); 12 | 13 | async function main() { 14 | 15 | const server = http.createServer((req, res) => serveHandler(req, res, { 16 | public: path.join(__dirname, '..'), 17 | })); 18 | server.listen(port, async () => { 19 | try { 20 | const browser = await puppeteer.launch(); 21 | const page = await browser.newPage(); 22 | await page.setViewport( { width: width, height: height } ); 23 | 24 | const tasks = []; 25 | exampleModuleList.forEach((mod) => { 26 | const name = mod.replace(/.html$/, ''); 27 | const pagePath = `/example/module/${mod}`; 28 | tasks.push(async (ctx, next) => { 29 | await page.goto(`http://127.0.0.1:${port}${pagePath}`); 30 | await sleep(1000); 31 | const buf = await page.screenshot(); 32 | const screenPicPath = path.join(screenDir, `${name}.jpg`); 33 | (await jimp.read(buf)).scale(1).quality(100).write(screenPicPath); 34 | console.log(`create screenshot [${name}] scuccess.`) ; 35 | await next(); 36 | }) 37 | }); 38 | await compose(tasks)(); 39 | 40 | await browser.close(); 41 | server.close(); 42 | } catch (err) { 43 | server.close(); 44 | console.error(err); 45 | process.exit(-1); 46 | } 47 | }); 48 | server.on('SIGINT', () => process.exit(1) ); 49 | } 50 | 51 | 52 | function sleep(time = 100) { 53 | return new Promise((resolve) => { 54 | setTimeout(() => { 55 | resolve(); 56 | }, time); 57 | }); 58 | } -------------------------------------------------------------------------------- /src/ui/component/mask/index.ts: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | export interface MaskAfterRenderArgs { 4 | contentMount: HTMLElement; 5 | headerMount: HTMLElement; 6 | footerMount: HTMLElement; 7 | } 8 | 9 | export interface MaskOpts { 10 | zIndex: number; 11 | afterRender: Function; 12 | } 13 | 14 | export class Mask { 15 | 16 | private _options: MaskOpts; 17 | private _hasRendered: boolean = false; 18 | private _component: HTMLDivElement = null; 19 | 20 | constructor(opts: MaskOpts) { 21 | this._options = opts; 22 | this._render(); 23 | } 24 | 25 | show() { 26 | this._component.classList.add('mask-open'); 27 | } 28 | 29 | hide() { 30 | this._component.classList.remove('mask-open'); 31 | } 32 | 33 | private _render() { 34 | if (this._hasRendered === true) { 35 | return; 36 | } 37 | const options = this._options; 38 | const { zIndex, afterRender } = options; 39 | const html = ` 40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 | `; 48 | const body = document.querySelector('body'); 49 | const mountDom = document.createElement('div');; 50 | mountDom.innerHTML = html; 51 | const component : HTMLDivElement = mountDom.querySelector('div.pictool-component-mask') 52 | body.appendChild(component); 53 | 54 | const contentMount: HTMLDivElement = component.querySelector('div.pictool-mask-content'); 55 | const headerMount: HTMLDivElement = component.querySelector('div.pictool-mask-header'); 56 | const footerMount: HTMLDivElement = component.querySelector('div.pictool-mask-footer'); 57 | 58 | if (typeof afterRender === 'function') { 59 | const args: MaskAfterRenderArgs = { contentMount, headerMount, footerMount}; 60 | afterRender(args) 61 | } 62 | 63 | this._hasRendered = true; 64 | this._component = component; 65 | } 66 | 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/core/digit/digit-image-data.ts: -------------------------------------------------------------------------------- 1 | export interface DigitImageDataOpts { 2 | width: number; 3 | height: number; 4 | data?: Uint8ClampedArray; 5 | } 6 | 7 | export interface DigitImageDataRGBA { 8 | r: number; 9 | g: number; 10 | b: number; 11 | a: number; 12 | } 13 | 14 | 15 | export class DigitImageData { 16 | 17 | private _data: Uint8ClampedArray|null; 18 | private _width: number|null; 19 | private _height: number|null; 20 | private _nullData: Uint8ClampedArray = new Uint8ClampedArray(0); 21 | 22 | constructor(opts: DigitImageDataOpts) { 23 | const { width, height, data } = opts; 24 | const size: number = width * height * 4; 25 | 26 | if (data instanceof Uint8ClampedArray && data.length === size) { 27 | this._data = new Uint8ClampedArray(data); 28 | } else if (data instanceof Array && data.length === size) { 29 | this._data = new Uint8ClampedArray(data); 30 | } else { 31 | this._data = new Uint8ClampedArray(size); 32 | } 33 | 34 | this._width = width; 35 | this._height = height; 36 | } 37 | 38 | public getWidth(): number { 39 | return this._width !== null ? this._width : 0; 40 | } 41 | 42 | public getHeight(): number { 43 | return this._height !== null ? this._height : 0; 44 | } 45 | 46 | public getData(): Uint8ClampedArray { 47 | return this._data !== null ? this._data : this._nullData; 48 | } 49 | 50 | public setDataUnit(index: number, unit: number) { 51 | if (this._data instanceof Uint8ClampedArray) { 52 | this._data[index] = unit; 53 | } 54 | } 55 | 56 | public pixelAt(x: number, y: number): DigitImageDataRGBA { 57 | const width: number = this.getWidth(); 58 | const data: Uint8ClampedArray = this.getData(); 59 | const idx = (width * y + x) * 4; 60 | const r = data[idx]; 61 | const g = data[idx + 1]; 62 | const b = data[idx + 2]; 63 | const a = data[idx + 3]; 64 | const rgba: DigitImageDataRGBA = {r, g, b, a}; 65 | return rgba; 66 | } 67 | 68 | public destory(): void { 69 | this._data = null; 70 | this._width = null; 71 | this._height = null; 72 | } 73 | } -------------------------------------------------------------------------------- /src/ui/service/worker.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as filterMap from './../../core/digit/filter/index'; 3 | import { FilterEnum } from './../../core/digit/filter/enum'; 4 | 5 | export interface WorkerConfig { 6 | use: boolean; 7 | path: string; 8 | } 9 | 10 | export interface WorkerAction { 11 | key: string; 12 | param?: any; 13 | feedback?: Function; 14 | } 15 | 16 | export const syncWorker = function (action: WorkerAction, config: WorkerConfig) { 17 | const { key, param, feedback } = action; 18 | 19 | if (config && config.use === true) { 20 | const { path } = config; 21 | const worker: Worker = new Worker(path); 22 | worker.onmessage = function (event) { 23 | if (typeof feedback === 'function') { 24 | feedback(event.data.result, event.data.error); 25 | worker.terminate(); 26 | } 27 | }; 28 | worker.onerror = function (err) { 29 | if (typeof feedback === 'function') { 30 | feedback(null, err.message); 31 | worker.terminate(); 32 | } 33 | }; 34 | worker.postMessage({ 35 | key, 36 | param, 37 | }); 38 | } else { 39 | setTimeout(() => { 40 | let error: Error|null = null; 41 | let result = null; 42 | try { 43 | const filerAction = filterMap[key]; 44 | result = filerAction(param); 45 | } catch (err) { 46 | error = err; 47 | } 48 | if (typeof feedback === 'function') { 49 | feedback(result, error); 50 | } 51 | }, 1); 52 | } 53 | }; 54 | 55 | export const asyncWorker = function(action: WorkerAction, config: WorkerConfig): Promise { 56 | 57 | return new Promise(function (resolve, reject) { 58 | try { 59 | const asyncAction: WorkerAction = { 60 | key: action.key, 61 | param: action.param, 62 | feedback: function(result: any, err: Error|null) { 63 | if (!err) { 64 | resolve(result); 65 | } else { 66 | reject(err); 67 | } 68 | } 69 | } 70 | syncWorker(asyncAction, config); 71 | } catch (err) { 72 | reject(err); 73 | } 74 | }) 75 | } -------------------------------------------------------------------------------- /example/module/digit-debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 68 | 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pictool", 3 | "version": "0.4.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "npm run build:prod && npm run build:mini", 8 | "build:mini": "node_modules/.bin/rollup -c ./script/rollup.config.mini.js", 9 | "build:prod": "node_modules/.bin/rollup -c ./script/rollup.config.prod.js", 10 | "dev": "node_modules/.bin/rollup -w -c ./script/rollup.config.dev.js", 11 | "lint": "./node_modules/.bin/eslint --fix --ext .ts ./src", 12 | "precommit": "npm run lint", 13 | "prepush": "npm run lint", 14 | "test": "npm run test:unit && npm run test:e2e", 15 | "test:unit": "./node_modules/.bin/mocha --exit ./__tests__/*.test.js", 16 | "test:e2e": "./node_modules/.bin/mocha --exit ./__tests__/*.e2e.js", 17 | "screenshot": "node ./__tests__/screenshot.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/chenshenhai/pictool.git" 22 | }, 23 | "author": "chenshenhai", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/chenshenhai/pictool/issues" 27 | }, 28 | "homepage": "https://github.com/chenshenhai/pictool#readme", 29 | "devDependencies": { 30 | "@babel/core": "^7.4.5", 31 | "@babel/plugin-transform-classes": "^7.4.4", 32 | "@babel/preset-env": "^7.4.5", 33 | "@typescript-eslint/eslint-plugin": "^1.11.0", 34 | "@typescript-eslint/parser": "^1.11.0", 35 | "chai": "^4.2.0", 36 | "chalk": "^4.1.0", 37 | "eslint": "^5.16.0", 38 | "husky": "^2.7.0", 39 | "jimp": "^0.16.1", 40 | "koa-compose": "^4.1.0", 41 | "less": "^3.9.0", 42 | "magic-string": "^0.25.3", 43 | "mocha": "^6.2.3", 44 | "pixelmatch": "^5.2.1", 45 | "pngjs": "^6.0.0", 46 | "puppeteer": "^7.1.0", 47 | "rollup": "^1.15.6", 48 | "rollup-plugin-babel": "^4.3.2", 49 | "rollup-plugin-buble": "^0.19.6", 50 | "rollup-plugin-postcss": "^2.0.3", 51 | "rollup-plugin-serve": "^1.0.1", 52 | "rollup-plugin-typescript": "^1.0.1", 53 | "rollup-plugin-uglify": "^6.0.2", 54 | "serve-handler": "^6.1.3", 55 | "tslib": "^1.10.0", 56 | "typescript": "^3.5.2" 57 | }, 58 | "files": [ 59 | "dist" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/ui/module/header/index.less: -------------------------------------------------------------------------------- 1 | .pictool-module-header { 2 | position: relative; 3 | font-size: 14px; 4 | color: #ffffff; 5 | 6 | .pictool-header-btn-close { 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | width: 60px; 11 | height: 40px; 12 | &::before { 13 | content: ''; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | right: 0; 18 | bottom: 0; 19 | background: url('data:image/svg+xml;charset=utf-8,'); 20 | background-repeat: no-repeat; 21 | background-position: center; 22 | background-size: 30px; 23 | } 24 | } 25 | 26 | 27 | .pictool-header-btn-save { 28 | position: absolute; 29 | right: 0; 30 | top: 0; 31 | width: 60px; 32 | height: 40px; 33 | &::before { 34 | content: ''; 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | right: 0; 39 | bottom: 0; 40 | background: url('data:image/svg+xml;charset=utf-8,'); 41 | background-repeat: no-repeat; 42 | background-position: center; 43 | background-size: 30px; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /script/rollup.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const typescript = require('rollup-plugin-typescript'); 3 | const postcss = require('rollup-plugin-postcss'); 4 | // const buble = require('rollup-plugin-buble'); 5 | const babel = require('rollup-plugin-babel'); 6 | const ascii = require('./lib/rollup-plugin-ascii'); 7 | 8 | const less = require('less'); 9 | const config = require('./config'); 10 | 11 | const resolveFile = function(filePath) { 12 | return path.join(__dirname, '..', filePath) 13 | } 14 | 15 | const processLess = function(context, payload) { 16 | return new Promise(( resolve, reject ) => { 17 | less.render({ 18 | file: context 19 | }, function(err, result) { 20 | if( !err ) { 21 | resolve(result); 22 | } else { 23 | reject(err); 24 | } 25 | }); 26 | 27 | less.render(context, {}).then(function(output) { 28 | // output.css = string of css 29 | // output.map = string of sourcemap 30 | // output.imports = array of string filenames of the imports referenced 31 | if( output && output.css ) { 32 | resolve(output.css); 33 | } else { 34 | reject({}) 35 | } 36 | }, 37 | function(err) { 38 | reject(err) 39 | }); 40 | 41 | }) 42 | } 43 | 44 | 45 | function getPlugins() { 46 | return [ 47 | postcss({ 48 | extract: false, 49 | minimize: process.env.NODE_ENV === 'production', 50 | process: processLess, 51 | }), 52 | typescript(), 53 | // buble(), 54 | babel({ 55 | babelrc: false, 56 | presets: [ 57 | ['@babel/preset-env', { modules: false }] 58 | ], 59 | plugins: [ 60 | ["@babel/plugin-transform-classes", { 61 | "loose": true 62 | }] 63 | ] 64 | }), 65 | ascii({ 66 | sourcemap: process.env.NODE_ENV === 'development', 67 | }), 68 | ] 69 | } 70 | 71 | 72 | 73 | function parseConfig (config = []) { 74 | const rsConfig = []; 75 | if (Array.isArray(config) && config.length > 0) { 76 | config.forEach(function(item = {}) { 77 | item.output.file = resolveFile(item.output.file); 78 | const output = item.output; 79 | rsConfig.push({ 80 | input: resolveFile(item.input), 81 | output, 82 | plugins: getPlugins(), 83 | }) 84 | }); 85 | } 86 | return rsConfig; 87 | } 88 | 89 | module.exports = parseConfig(config); -------------------------------------------------------------------------------- /example/module/pictool-ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 21 | 22 | 23 |

Welcome to Pictool UI

24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 | 77 | 78 | -------------------------------------------------------------------------------- /script/lib/rollup-plugin-ascii.js: -------------------------------------------------------------------------------- 1 | // const extname = require("path").extname; 2 | // const {createFilter, dataToEsm} = require('rollup-pluginutils'); 3 | const MagicString = require('magic-string'); 4 | 5 | // /** 6 | // * thanks to https://github.com/yinhaibo01/unicode-loader/ 7 | // * @param {*} str 8 | // * @param {*} identifier 9 | // */ 10 | // function toAscii(str, identifier) { 11 | // return str.replace(/[\u0080-\uffff]/g, function (ch) { 12 | // // console.log('ch =', ch); 13 | // var code = ch.charCodeAt(0).toString(16); 14 | // if (code.length <= 2 && !identifier) { 15 | // while (code.length < 2) code = "0" + code; 16 | // return "\\x" + code; 17 | // } else { 18 | // while (code.length < 4) code = "0" + code; 19 | // return "\\u" + code; 20 | // } 21 | // }); 22 | // }; 23 | 24 | module.exports = function ascii (options = {}) { 25 | // const filter = createFilter(options.include, options.exclude); 26 | const sourcemap = options.sourcemap === true || options.sourceMap === true; 27 | 28 | return { 29 | name: 'rollup-plugin-ascii', 30 | 31 | // 插件处理代码编译 32 | transform (code, id) { 33 | // if (!filter(id) || extname(id) !== ".js" || extname(id) !== ".ts") return; 34 | 35 | let codeStr = `${code}`; 36 | const magic = new MagicString(codeStr); 37 | 38 | // codeStr = toAscii(codeStr); 39 | 40 | // codeStr = toAscii(codeStr); 41 | 42 | codeStr = codeStr.replace(/[\u0080-\uffff]/g, function(match, offset) { 43 | 44 | let newStr = match; 45 | // console.log('match =', match); 46 | var code = match.charCodeAt(0).toString(16); 47 | if (code.length <= 2) { 48 | if (code.length < 2) { 49 | code = "0" + code 50 | }; 51 | newStr = "\\x" + code; 52 | } else { 53 | if (code.length < 4) { 54 | code = "0" + code 55 | }; 56 | newStr = "\\u" + code; 57 | } 58 | 59 | const start = offset; 60 | const end = offset + match.length; 61 | magic.overwrite(start, end, newStr); 62 | return newStr; 63 | }); 64 | 65 | 66 | const resultCode = magic.toString(); 67 | let resultMap = false; 68 | if (sourcemap === true) { 69 | resultMap = magic.generateMap({ 70 | hires: true, 71 | }); 72 | } 73 | return { 74 | code: resultCode, 75 | map: resultMap, 76 | }; 77 | } 78 | }; 79 | 80 | } -------------------------------------------------------------------------------- /src/core/digit/transform/hsl2rgb.ts: -------------------------------------------------------------------------------- 1 | import { RGBCell } from '../rgba/rgb'; 2 | import { HSLObject, HSLCell } from '../hsl/hsl'; 3 | import { 4 | RGBA_MID, RGBA_MAX, RGBA_MIN, 5 | H_MAX, H_MIN, 6 | S_MAX, S_MIN, S_MID, 7 | L_MAX, L_MIN, L_MID 8 | } from './static'; 9 | 10 | // const H2RGBNum = function(l: number): number { 11 | // let num = l / H_MAX * RGBA_MAX; 12 | // num = Math.round(num); 13 | // return num; 14 | // } 15 | 16 | // const S2RGBNum = function(l: number): number { 17 | // let num = l / S_MAX * RGBA_MAX; 18 | // num = Math.round(num); 19 | // return num; 20 | // } 21 | 22 | const L2RGBNum = function(l: number): number { 23 | let num = l * RGBA_MAX; 24 | num = Math.round(num); 25 | num = Math.max(0, num); 26 | num = Math.min(255, num); 27 | return num; 28 | } 29 | 30 | export const HSL2RGB = function(cell: HSLCell): RGBCell { 31 | 32 | const originH = cell.h; 33 | const originS = cell.s; 34 | const originL = cell.l; 35 | 36 | const h = originH / H_MAX; // [0, 1]; 37 | const s = originS / S_MAX; // [0, 1]; 38 | const l = originL / L_MAX; // [0, 1]; 39 | // const max = 1; 40 | // const min = 0; 41 | // const mid = 0.5 42 | 43 | let r: number = 0; 44 | let g: number = 0; 45 | let b: number = 0; 46 | 47 | if (s === 0) { 48 | r = L2RGBNum(l); 49 | g = r; 50 | b = g; 51 | // g = L2RGBNum(l * RGBA_MAX); 52 | // b = L2RGBNum(l * RGBA_MAX); 53 | } else { 54 | const tempRGB: number[] = []; 55 | let q: number = l >= 0.5 ? ( l + s - l * s ) : ( l * (1 + s) ); 56 | let p: number = 2 * l - q; 57 | tempRGB[0] = h + 1 / 3; 58 | tempRGB[1] = h; 59 | tempRGB[2] = h - 1 / 3; 60 | for (let i = 0; i < tempRGB.length; i++){ 61 | let tempColor: number = tempRGB[i]; 62 | if (tempColor < 0){ 63 | tempColor = tempColor + 1; 64 | } else if(tempColor > 1) { 65 | tempColor = tempColor - 1; 66 | } 67 | switch(true){ 68 | case (tempColor < (1/6)): 69 | tempColor = p + (q - p) * 6 * tempColor; 70 | break; 71 | case ((1 / 6) <= tempColor && tempColor<0.5): 72 | tempColor = q; 73 | break; 74 | case (0.5 <= tempColor && tempColor < (2 / 3)): 75 | tempColor = p + (q - p) * (4 - 6 * tempColor); 76 | break; 77 | default: 78 | tempColor = p; 79 | break; 80 | } 81 | tempRGB[i] = Math.round(tempColor * RGBA_MAX); 82 | } 83 | 84 | r = tempRGB[0]; 85 | g = tempRGB[1]; 86 | b = tempRGB[2]; 87 | } 88 | 89 | return { r, g, b }; 90 | } -------------------------------------------------------------------------------- /__tests__/digit-digitimagedata.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const digit = require('../dist/digit'); 3 | const expect = chai.expect 4 | 5 | const DigitImageData = digit.DigitImageData; 6 | 7 | const img = require('./data/image-data-origin.json'); 8 | 9 | 10 | describe( 'test: Pictool.digit.DigitImageData', ( ) => { 11 | 12 | it('DigitImageData init', ( done ) => { 13 | const digitImgRs = new DigitImageData({width: img.width, height: img.height}); 14 | const expectImg = img; 15 | 16 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 17 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 18 | done() 19 | }); 20 | 21 | it('DigitImageData.getData(..)', ( done ) => { 22 | const digitImgRs = new DigitImageData({ 23 | width: img.width, 24 | height: img.height, 25 | data: img.data 26 | }); 27 | const expectImg = img; 28 | 29 | const rsData = digitImgRs.getData(); 30 | 31 | expectImg.data.forEach(function(num, i) { 32 | expect(rsData[i]).to.deep.equal(num); 33 | }); 34 | 35 | done() 36 | }); 37 | 38 | it('DigitImageData.setDataUnit(..)', ( done ) => { 39 | const digitImgRs = new DigitImageData({width: img.width, height: img.height, data: img.data}); 40 | const expectImg = img; 41 | 42 | expectImg.data.forEach(function(num, i) { 43 | digitImgRs.setDataUnit(i, num); 44 | }); 45 | 46 | const rsData = digitImgRs.getData(); 47 | expectImg.data.forEach(function(num, i) { 48 | // console.log(`expect index is: ${i}`) 49 | expect(rsData[i]).to.deep.equal(num); 50 | }); 51 | 52 | done() 53 | }); 54 | 55 | it('DigitImageData.pixelAt(..)', ( done ) => { 56 | const digitImgRs = new DigitImageData({width: img.width, height: img.height, data: img.data}); 57 | const x = 3; 58 | const y = 4; 59 | const i = (y * img.width + x) * 4; 60 | const pixel = digitImgRs.pixelAt(x, y); 61 | const expectImg = img; 62 | 63 | expect(pixel.r).to.deep.equal(expectImg.data[i]); 64 | expect(pixel.g).to.deep.equal(expectImg.data[i + 1]); 65 | expect(pixel.b).to.deep.equal(expectImg.data[i + 2]); 66 | expect(pixel.a).to.deep.equal(expectImg.data[i + 3]); 67 | 68 | done() 69 | }); 70 | 71 | it('DigitImageData.destory()', ( done ) => { 72 | const digitImgRs = new DigitImageData({width: img.width, height: img.height, data: img.data}); 73 | digitImgRs.destory(); 74 | 75 | expect(digitImgRs.getWidth()).to.deep.equal(0); 76 | expect(digitImgRs.getHeight()).to.deep.equal(0); 77 | expect(digitImgRs.getData()).to.deep.equal(new Uint8ClampedArray(0)); 78 | 79 | done() 80 | }); 81 | 82 | }) -------------------------------------------------------------------------------- /src/util/compress.ts: -------------------------------------------------------------------------------- 1 | 2 | const IMG_LIMIT_SIZE = 2000 * 2000; 3 | const PIECE_SIZE = 1000 * 1000; 4 | 5 | export enum CompressImageTypeEnum { 6 | png = 'image/png', 7 | jpg = 'image/webp', 8 | jpeg = 'image/jpeg', 9 | } 10 | 11 | export interface CompressImageOpts { 12 | type: CompressImageTypeEnum, 13 | encoderOptions: number, // [0, 1] 14 | } 15 | 16 | export const compressImage = function( 17 | img: HTMLImageElement, 18 | opts: CompressImageOpts = {type: CompressImageTypeEnum.png, encoderOptions: 1 } 19 | ): string|null { 20 | const {type, encoderOptions } = opts; 21 | const w = img.width; 22 | const h = img.height; 23 | 24 | let outputW = w; 25 | let outputH = h; 26 | 27 | let imageSize = w * h; 28 | let ratio = Math.ceil(Math.sqrt(Math.ceil(imageSize / IMG_LIMIT_SIZE))); 29 | 30 | if ( ratio > 1) { 31 | outputW = w / ratio; 32 | outputH = h / ratio; 33 | } else { 34 | ratio = 1; 35 | } 36 | 37 | let canvas: HTMLCanvasElement|null = document.createElement('canvas'); 38 | let tempCanvas: HTMLCanvasElement|null = document.createElement('canvas'); 39 | let context: CanvasRenderingContext2D|null = canvas.getContext('2d'); 40 | if (!context) { 41 | return null; 42 | } 43 | 44 | canvas.width = outputW; 45 | canvas.height = outputH; 46 | context.fillStyle = '#FFFFFF'; 47 | context.fillRect(0, 0, canvas.width, canvas.height); 48 | 49 | const pieceCount = Math.ceil(imageSize / PIECE_SIZE); 50 | 51 | if (pieceCount > 1) { 52 | 53 | const pieceW = Math.ceil(canvas.width / pieceCount); 54 | const pieceH = Math.ceil(canvas.height / pieceCount); 55 | 56 | tempCanvas.width = pieceW; 57 | tempCanvas.height = pieceH; 58 | let tempContext: CanvasRenderingContext2D | null = tempCanvas.getContext('2d'); 59 | if (!tempContext) { 60 | return null; 61 | } 62 | 63 | const sw = pieceW * ratio; 64 | const sh = pieceH * ratio; 65 | const dw = pieceW; 66 | const dh = pieceH; 67 | for(let i = 0; i < pieceCount; i++) { 68 | for(let j = 0; j < pieceCount; j++) { 69 | const sx = i * pieceW * ratio; 70 | const sy = j * pieceH * ratio; 71 | tempContext.drawImage(img, sx, sy, sw, sh, 0, 0, dw, dh); 72 | context.drawImage(tempCanvas, i * pieceW, j * pieceH, dw, dh); 73 | } 74 | } 75 | 76 | tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height); 77 | tempCanvas.width = 0; 78 | tempCanvas.height = 0; 79 | tempCanvas = null; 80 | } else { 81 | context.drawImage(img, 0, 0, outputW, outputH); 82 | } 83 | const base64 = canvas.toDataURL(type, encoderOptions); 84 | context.clearRect(0, 0, canvas.width, canvas.height); 85 | canvas.width = 0; 86 | canvas.height = 0; 87 | canvas = null; 88 | 89 | return base64; 90 | } -------------------------------------------------------------------------------- /src/ui/index.ts: -------------------------------------------------------------------------------- 1 | import { Mask, MaskAfterRenderArgs } from './component/mask/index'; 2 | import { Sketch as ModSketch } from './module/sketch/index'; 3 | import { Dashboard } from './module/dashboard/index'; 4 | import { SketchSchema } from '../core/sketch/index'; 5 | import { Header } from './module/header/index'; 6 | // import schemaParser from './service/schema-parser'; 7 | import { WorkerConfig } from './service/worker'; 8 | import eventHub from './service/event-hub'; 9 | import cacheHub from './service/cache-hub'; 10 | 11 | const ZINDEX = 1000; 12 | 13 | interface PictoolOptsUIConfig { 14 | zIndex?: number; 15 | language?: string; 16 | } 17 | 18 | interface PictoolUIOpts{ 19 | uiConfig?: PictoolOptsUIConfig; 20 | workerConfig: WorkerConfig; 21 | } 22 | 23 | class PictoolUI { 24 | private _options: any; 25 | private _mask: Mask; 26 | private _imageData: ImageData|null = null; 27 | private _sketch: ModSketch|null = null; 28 | private _dashboard: Dashboard|null = null; 29 | private _header: Header|null = null; 30 | 31 | constructor(imageData: ImageData, options: PictoolUIOpts= { uiConfig: {}, workerConfig: { use: false, path: '' }}) { 32 | this._imageData = imageData; 33 | this._options = options; 34 | const { uiConfig = {}, workerConfig } = options; 35 | let zIndex: number|undefined = uiConfig.zIndex; 36 | if (!(zIndex && zIndex * 1 > 0)) { 37 | zIndex = ZINDEX; 38 | } 39 | 40 | // const that = this; 41 | const mask = new Mask({ 42 | zIndex, 43 | afterRender: (opts: MaskAfterRenderArgs) => { 44 | const {contentMount, headerMount, footerMount } = opts; 45 | const header = new Header(headerMount, { 46 | closeFeedback() { 47 | mask.hide(); 48 | }, 49 | saveFeedback() { 50 | eventHub.trigger('GlobalEvent.moduleSketch.downloadImage'); 51 | } 52 | }); 53 | const sketch = new ModSketch(contentMount, { imageData, }); 54 | if (!(zIndex && zIndex * 1 > 0)) { 55 | zIndex = ZINDEX; 56 | } 57 | const dashboard = new Dashboard(footerMount, { 58 | zIndex: zIndex + 1, 59 | language: uiConfig.language || 'en-us', 60 | workerConfig, 61 | }); 62 | this._sketch = sketch; 63 | this._dashboard = dashboard; 64 | this._header = header; 65 | } 66 | }); 67 | this._mask = mask; 68 | } 69 | 70 | show() { 71 | const sketchSchema: SketchSchema = cacheHub.get('Sketch.originSketchSchema'); 72 | this._sketch && this._sketch.renderImage(sketchSchema); 73 | this._mask.show(); 74 | eventHub.trigger('GlobalEvent.moduleSketch.resizeCanvas'); 75 | } 76 | 77 | hide() { 78 | this._mask.hide(); 79 | } 80 | } 81 | 82 | export default PictoolUI; -------------------------------------------------------------------------------- /src/core/digit/process/alpha.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData } from '../digit-image-data'; 2 | import { RGBA_MAX, RGBA_MIN } from './../transform/static'; 3 | 4 | export interface AlphaOpts { 5 | percent?: number; // [-100, 100] 6 | value?: number; // [0, 100] 7 | } 8 | 9 | function isPercent(num: number) { 10 | if(num >= -100 && num <= 100) { 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | } 16 | 17 | function isAlphaValue(num: number) { 18 | if(num >= 0 && num <= 100) { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | } 24 | 25 | export const alpha = function( 26 | digitImg: DigitImageData, 27 | opts: AlphaOpts 28 | ): DigitImageData { 29 | const width: number = digitImg.getWidth(); 30 | const height: number = digitImg.getHeight(); 31 | const data: Uint8ClampedArray = digitImg.getData(); 32 | let rsDigitImg: DigitImageData = new DigitImageData({width, height, data}); 33 | 34 | let percent: number|undefined = opts.percent; 35 | let value: number|undefined = opts.value; 36 | 37 | if (percent && isPercent(percent)) { 38 | percent = Math.min(100, percent); 39 | percent = Math.max(-100, percent); 40 | } else if (value && isAlphaValue(value)) { 41 | value = Math.min(100, value); 42 | value = Math.max(0, value); 43 | } 44 | 45 | if (value || value === 0) { 46 | for(let i = 0; i < data.length; i += 4) { 47 | const r: number = data[i]; 48 | const g: number = data[i + 1]; 49 | const b: number = data[i + 2]; 50 | let a: number = data[i + 3]; 51 | 52 | a = Math.floor(value * RGBA_MAX / 100); 53 | a = Math.min(RGBA_MAX, a); 54 | a = Math.max(RGBA_MIN, a); 55 | 56 | rsDigitImg.setDataUnit(i, r); 57 | rsDigitImg.setDataUnit(i + 1, g); 58 | rsDigitImg.setDataUnit(i + 2, b); 59 | rsDigitImg.setDataUnit(i + 3, a); 60 | } 61 | } else if (percent || percent === 0) { 62 | for(let i = 0; i < data.length; i += 4) { 63 | const r: number = data[i]; 64 | const g: number = data[i + 1]; 65 | const b: number = data[i + 2]; 66 | let a: number = data[i + 3]; 67 | 68 | a = Math.floor(a * (100 + percent) / 100); 69 | a = Math.min(RGBA_MAX, a); 70 | a = Math.max(RGBA_MIN, a); 71 | 72 | rsDigitImg.setDataUnit(i, r); 73 | rsDigitImg.setDataUnit(i + 1, g); 74 | rsDigitImg.setDataUnit(i + 2, b); 75 | rsDigitImg.setDataUnit(i + 3, a); 76 | } 77 | } else { 78 | for(let i = 0; i < data.length; i += 4) { 79 | const r: number = data[i]; 80 | const g: number = data[i + 1]; 81 | const b: number = data[i + 2]; 82 | const a: number = data[i + 3]; 83 | 84 | rsDigitImg.setDataUnit(i, r); 85 | rsDigitImg.setDataUnit(i + 1, g); 86 | rsDigitImg.setDataUnit(i + 2, b); 87 | rsDigitImg.setDataUnit(i + 3, a); 88 | } 89 | } 90 | 91 | return rsDigitImg; 92 | } -------------------------------------------------------------------------------- /src/core/digit/process/sobel.ts: -------------------------------------------------------------------------------- 1 | // Thanks to https://github.com/miguelmota/sobel/ 2 | 3 | import { DigitImageData } from './../digit-image-data'; 4 | import { grayscale } from './grayscale'; 5 | 6 | function imgDataAt(digitData: DigitImageData, x: number, y: number): number { 7 | const width: number = digitData.getWidth(); 8 | // const height: number = digitData.getHeight(); 9 | const data: Uint8ClampedArray = digitData.getData(); 10 | const idx = (width * y + x) * 4; 11 | let num = data[idx]; 12 | if (!(num >= 0 && num < 255)) { 13 | num = 0; 14 | } 15 | return num; 16 | } 17 | 18 | export const sobel = function(imgData: DigitImageData): DigitImageData { 19 | const width: number = imgData.getWidth(); 20 | const height: number = imgData.getHeight(); 21 | const data: Uint8ClampedArray = new Uint8ClampedArray(width * height * 4) 22 | const digitImg = new DigitImageData({width, height, data}); 23 | 24 | const kernelX = [ 25 | [-1, 0, 1], 26 | [-2, 0, 2], 27 | [-1, 0, 1] 28 | ]; 29 | 30 | const kernelY = [ 31 | [-1, -2, -1], 32 | [0, 0, 0], 33 | [1, 2, 1] 34 | ]; 35 | 36 | let grayImg: DigitImageData|null = grayscale(imgData); 37 | 38 | for (let x = 0; x < width; x ++) { 39 | for (let y = 0; y < height; y ++) { 40 | const pixelX = ( 41 | (kernelX[0][0] * imgDataAt(grayImg, x - 1, y - 1)) + 42 | (kernelX[0][1] * imgDataAt(grayImg, x, y - 1)) + 43 | (kernelX[0][2] * imgDataAt(grayImg, x + 1, y - 1)) + 44 | (kernelX[1][0] * imgDataAt(grayImg, x - 1, y)) + 45 | (kernelX[1][1] * imgDataAt(grayImg, x, y)) + 46 | (kernelX[1][2] * imgDataAt(grayImg, x + 1, y)) + 47 | (kernelX[2][0] * imgDataAt(grayImg, x - 1, y + 1)) + 48 | (kernelX[2][1] * imgDataAt(grayImg, x, y + 1)) + 49 | (kernelX[2][2] * imgDataAt(grayImg, x + 1, y + 1)) 50 | ); 51 | 52 | const pixelY = ( 53 | (kernelY[0][0] * imgDataAt(grayImg, x - 1, y - 1)) + 54 | (kernelY[0][1] * imgDataAt(grayImg, x, y - 1)) + 55 | (kernelY[0][2] * imgDataAt(grayImg, x + 1, y - 1)) + 56 | (kernelY[1][0] * imgDataAt(grayImg, x - 1, y)) + 57 | (kernelY[1][1] * imgDataAt(grayImg, x, y)) + 58 | (kernelY[1][2] * imgDataAt(grayImg, x + 1, y)) + 59 | (kernelY[2][0] * imgDataAt(grayImg, x - 1, y + 1)) + 60 | (kernelY[2][1] * imgDataAt(grayImg, x, y + 1)) + 61 | (kernelY[2][2] * imgDataAt(grayImg, x + 1, y + 1)) 62 | ); 63 | const magnitude = Math.round(Math.sqrt((pixelX * pixelX) + (pixelY * pixelY))); 64 | const idx = (width * y + x) * 4; 65 | digitImg.setDataUnit(idx, magnitude); 66 | digitImg.setDataUnit(idx + 1, magnitude); 67 | digitImg.setDataUnit(idx + 2, magnitude); 68 | digitImg.setDataUnit(idx + 3, 255); 69 | } 70 | 71 | } 72 | 73 | grayImg.destory(); 74 | grayImg = null; 75 | 76 | return digitImg; 77 | } -------------------------------------------------------------------------------- /__tests__/digit-process.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const digit = require('../dist/digit'); 3 | const expect = chai.expect 4 | const process = digit.process; 5 | const DigitImageData = digit.DigitImageData; 6 | 7 | const img = require('./data/image-data-origin.json'); 8 | const imgGrayscale = require('./data/image-data-grayscrale.json'); 9 | const imgInvert = require('./data/image-data-invert.json'); 10 | const imgSobel = require('./data/image-data-sobel.json'); 11 | const imgSepia = require('./data/image-data-sepia.json') 12 | 13 | describe( 'test: Pictool.digit.process', ( ) => { 14 | 15 | it('process.grayscale', ( done ) => { 16 | 17 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 18 | const digitImgRs = process.grayscale(digitImg); 19 | const expectImg = imgGrayscale; 20 | 21 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 22 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 23 | 24 | const rsData = digitImgRs.getData(); 25 | expectImg.data.forEach(function(num, i) { 26 | // console.log(`expect index is: ${i}`) 27 | expect(rsData[i]).to.deep.equal(num); 28 | }); 29 | 30 | done() 31 | }); 32 | 33 | 34 | it('process.invert', ( done ) => { 35 | 36 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 37 | const digitImgRs = process.invert(digitImg); 38 | const expectImg = imgInvert; 39 | 40 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 41 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 42 | 43 | const rsData = digitImgRs.getData(); 44 | expectImg.data.forEach(function(num, i) { 45 | // console.log(`expect index is: ${i}`) 46 | expect(rsData[i]).to.deep.equal(num); 47 | }); 48 | 49 | done() 50 | }); 51 | 52 | it('process.sobel', ( done ) => { 53 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 54 | const digitImgRs = process.sobel(digitImg); 55 | const expectImg = imgSobel; 56 | 57 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 58 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 59 | 60 | const rsData = digitImgRs.getData(); 61 | expectImg.data.forEach(function(num, i) { 62 | // console.log(`expect index is: ${i}`) 63 | expect(rsData[i]).to.deep.equal(num); 64 | }); 65 | 66 | done() 67 | }); 68 | 69 | 70 | it('process.sepia', ( done ) => { 71 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 72 | const digitImgRs = process.sepia(digitImg); 73 | const expectImg = imgSepia; 74 | 75 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 76 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 77 | 78 | const rsData = digitImgRs.getData(); 79 | expectImg.data.forEach(function(num, i) { 80 | expect(rsData[i]).to.deep.equal(num); 81 | }); 82 | 83 | done() 84 | }); 85 | 86 | 87 | }) -------------------------------------------------------------------------------- /__tests__/index.e2e.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const assert = require('assert'); 3 | const http = require('http'); 4 | const fs = require('fs'); 5 | const puppeteer = require('puppeteer'); 6 | const serveHandler = require('serve-handler'); 7 | const jimp = require('jimp'); 8 | const compose = require('koa-compose'); 9 | const pixelmatch = require('pixelmatch'); 10 | const pngjs = require('pngjs'); 11 | const chalk = require('chalk'); 12 | const { exampleModuleList, port, width, height, } = require('./e2e.config'); 13 | 14 | 15 | const screenDir = path.join(__dirname, 'screenshot'); 16 | const diffDir = path.join(__dirname, 'screenshot-diff'); 17 | if (!fs.existsSync(diffDir)) { 18 | fs.mkdirSync(diffDir); 19 | } 20 | 21 | 22 | const { PNG } = pngjs; 23 | 24 | 25 | describe('E2E Testing', function() { 26 | it('testing...', function(done){ 27 | 28 | this.timeout(1000 * 60 * 5); 29 | 30 | const server = http.createServer((req, res) => serveHandler(req, res, { 31 | public: path.join(__dirname, '..'), 32 | })); 33 | server.listen(port, async () => { 34 | try { 35 | const browser = await puppeteer.launch(); 36 | const page = await browser.newPage(); 37 | await page.setViewport( { width: width, height: height } ); 38 | 39 | const tasks = []; 40 | exampleModuleList.forEach((mod) => { 41 | const name = mod.replace(/.html$/, ''); 42 | const pagePath = `/example/module/${mod}`; 43 | tasks.push(async (ctx, next) => { 44 | await page.goto(`http://127.0.0.1:${port}${pagePath}`); 45 | await sleep(); 46 | const buf = await page.screenshot(); 47 | const screenPicPath = path.join(screenDir, `${name}.jpg`); 48 | 49 | const actual = (await jimp.read(buf)).scale(1).quality(100).bitmap; 50 | const expected = (await jimp.read(fs.readFileSync(screenPicPath))).bitmap; 51 | const diff = new PNG({width, height}); 52 | 53 | const failedPixel = pixelmatch(expected.data, actual.data, diff.data, actual.width, actual.height); 54 | const failRate = failedPixel / (width * height); 55 | const info = `E2E: test [${name}] diff pixel rate: ${failRate * 100}%`; 56 | if (failRate > 0) { 57 | fs.writeFileSync(path.join(diffDir, `${name}.jpg`), PNG.sync.write(diff)); 58 | console.log(chalk.red(info)) 59 | } else { 60 | console.log(chalk.green(info)); 61 | } 62 | assert.ok(failRate < 0.005); 63 | await next(); 64 | }) 65 | }); 66 | await compose(tasks)(); 67 | 68 | await browser.close(); 69 | server.close(); 70 | done(); 71 | } catch (err) { 72 | server.close(); 73 | done(err); 74 | console.error(err); 75 | process.exit(-1); 76 | } 77 | }); 78 | server.on('SIGINT', () => process.exit(1) ); 79 | 80 | }); 81 | }); 82 | 83 | 84 | function sleep(time = 100) { 85 | return new Promise((resolve) => { 86 | setTimeout(() => { 87 | resolve(); 88 | }, time); 89 | }); 90 | } -------------------------------------------------------------------------------- /src/ui/component/action-sheet/index.ts: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | export interface ActionSheetLifeCycleArgs { 4 | contentMount: HTMLElement; 5 | } 6 | 7 | export interface ActionSheetOpts { 8 | height: number; 9 | zIndex: number; 10 | beforeRender?: Function; 11 | afterRender?: Function; 12 | beforeShow?: Function; 13 | afterShow?: Function; 14 | beforeHide?: Function; 15 | afterHide?: Function; 16 | mount?: HTMLElement; 17 | } 18 | 19 | export class ActionSheet { 20 | 21 | private _options: any; 22 | private _hasRendered: boolean = false; 23 | private _component: HTMLDivElement = null; 24 | private _contentMount: HTMLDivElement = null; 25 | 26 | constructor(opts: ActionSheetOpts) { 27 | this._options = opts; 28 | this._render(); 29 | } 30 | 31 | show() { 32 | const { beforeShow, afterShow, } = this._options; 33 | const contentMount = this._contentMount; 34 | if (typeof beforeShow === 'function') { 35 | beforeShow({ contentMount }); 36 | } 37 | this._component.classList.add('actionsheet-open'); 38 | if (typeof afterShow === 'function') { 39 | afterShow({ contentMount }); 40 | } 41 | } 42 | 43 | hide() { 44 | const { beforeHide, afterHide, } = this._options; 45 | const contentMount = this._contentMount; 46 | if (typeof beforeHide === 'function') { 47 | beforeHide({ contentMount }); 48 | } 49 | this._component.classList.remove('actionsheet-open'); 50 | if (typeof afterHide === 'function') { 51 | afterHide({ contentMount }); 52 | } 53 | } 54 | 55 | private _render() { 56 | if (this._hasRendered === true) { 57 | return; 58 | } 59 | const { afterRender, beforeRender, mount, } = this._options; 60 | if (typeof beforeRender === 'function') { 61 | beforeRender(); 62 | } 63 | const opts: ActionSheetOpts = this._options; 64 | const { height, zIndex, } = opts; 65 | const html = ` 66 |
67 |
68 |
69 |
70 | 71 |
72 |
73 | `; 74 | const body = document.querySelector('body'); 75 | const mountDom = document.createElement('div');; 76 | mountDom.innerHTML = html; 77 | const component : HTMLDivElement = mountDom.querySelector('div.pictool-component-actionsheet') 78 | 79 | if (mount) { 80 | mount.appendChild(component); 81 | } else { 82 | body.appendChild(component); 83 | } 84 | 85 | 86 | const contentMount: HTMLDivElement = component.querySelector('div.pictool-actionsheet-content'); 87 | 88 | if (typeof afterRender === 'function') { 89 | const args: ActionSheetLifeCycleArgs = { contentMount, }; 90 | afterRender(args) 91 | } 92 | 93 | this._hasRendered = true; 94 | this._component = component; 95 | this._contentMount = contentMount; 96 | } 97 | 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/ui/config/adjust.ts: -------------------------------------------------------------------------------- 1 | import { LanguageType } from './../language/index'; 2 | 3 | export interface AdjustMenuItemType { 4 | name: string; 5 | percent: number; 6 | filter: string; 7 | range: any; 8 | parseOptions(data: any): any; 9 | } 10 | 11 | export interface AdjustMenuConfigType { 12 | title: string; 13 | menu: AdjustMenuItemType[], 14 | } 15 | 16 | export function getAdjustMenuConfig(lang: LanguageType = {}): AdjustMenuConfigType { 17 | const adjustMenuConfig: AdjustMenuConfigType = { 18 | title: lang.ADJUST, 19 | menu: [ 20 | { 21 | name: lang.ADJUST_LIGHTNESS, 22 | percent: 50, 23 | range: { 24 | min: -100, 25 | max: 100, 26 | }, 27 | filter: 'lightness', 28 | parseOptions(data: any) { 29 | const percent = Math.round(data.value); 30 | console.log('lightness.percent = ', percent); 31 | return { 32 | percent, 33 | } 34 | } 35 | }, 36 | { 37 | name: lang.ADJUST_HUE, 38 | percent: 50, 39 | range: { 40 | min: 0, 41 | max: 360, 42 | }, 43 | filter: 'hue', 44 | parseOptions(data: any) { 45 | const value = Math.round(data.value); 46 | console.log('hue.value = ', value); 47 | return { 48 | value, 49 | } 50 | } 51 | }, 52 | { 53 | name: lang.ADJUST_SATURATION, 54 | percent: 50, 55 | range: { 56 | min: -100, 57 | max: 100, 58 | }, 59 | filter: 'saturation', 60 | parseOptions(data: any) { 61 | const percent = Math.round(data.value); 62 | console.log('saturation.percent = ', percent); 63 | return { 64 | percent, 65 | } 66 | } 67 | }, 68 | { 69 | name: lang.ADJUST_ALPHA, 70 | percent: 50, 71 | range: { 72 | min: 0, 73 | max: 100, 74 | }, 75 | filter: 'alpha', 76 | parseOptions(data: any) { 77 | const value = Math.round(data.value); 78 | console.log('alpha.value = ', value); 79 | return { 80 | value, 81 | } 82 | } 83 | }, 84 | { 85 | name: lang.ADJUST_GAMMA, 86 | percent: 50, 87 | range: { 88 | min: 0, 89 | max: 100, 90 | }, 91 | filter: 'gamma', 92 | parseOptions(data: any) { 93 | const value = Math.round(data.value); 94 | console.log('gamma.value = ', value); 95 | return { 96 | value, 97 | } 98 | } 99 | }, 100 | { 101 | name: lang.ADJUST_POSTERIZE, 102 | percent: 50, 103 | range: { 104 | min: 0, 105 | max: 100, 106 | }, 107 | filter: 'posterize', 108 | parseOptions(data: any) { 109 | const value = Math.round(data.value); 110 | console.log('posterize.value = ', value); 111 | return { 112 | value, 113 | } 114 | } 115 | }, 116 | ] 117 | } 118 | 119 | return adjustMenuConfig; 120 | } 121 | -------------------------------------------------------------------------------- /src/core/sketch/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Authors chenshenhai. All rights reserved. MIT license. 2 | // https://github.com/chenshenhai/logox/blob/master/src/sketch/mod.ts 3 | 4 | 5 | import {Layer, LayerDrawAction, LayerSchema } from './../layer/index'; 6 | 7 | export interface SketchOptions { 8 | width: number; 9 | height: number; 10 | layerCount: number; 11 | } 12 | 13 | export interface SketchSchema { 14 | key?: string; 15 | layerList: LayerSchema[]; 16 | } 17 | 18 | export class Sketch { 19 | 20 | private _width: number; 21 | private _height: number; 22 | private _layerCount: number; 23 | private _canvasStack: HTMLCanvasElement[] = []; 24 | private _layerStack: Layer[] = []; 25 | private _tempCanvas: HTMLCanvasElement = null; 26 | // private _sketchSchema: SketchSchema; 27 | 28 | constructor(opts: SketchOptions) { 29 | this._width = opts.width; 30 | this._height = opts.height; 31 | this._layerCount = opts.layerCount; 32 | this._initSketch(); 33 | } 34 | 35 | private _initSketch() { 36 | const width = this._width; 37 | const height = this._height; 38 | this._tempCanvas = document.createElement('canvas'); 39 | this._tempCanvas.width = width; 40 | this._tempCanvas.height = height; 41 | 42 | const count = this._layerCount; 43 | for (let i = 0; i < count; i ++) { 44 | const canvas = document.createElement('canvas'); 45 | canvas.width = width; 46 | canvas.height = height; 47 | const ctx = canvas.getContext('2d'); 48 | const layer = new Layer(ctx, { width, height }); 49 | this._layerStack.push(layer); 50 | this._canvasStack.push(canvas); 51 | } 52 | } 53 | 54 | getCanvasStack() { 55 | return this._canvasStack; 56 | } 57 | 58 | getLayerStack() { 59 | return this._layerStack; 60 | } 61 | 62 | drawLayer(index: number, layerSchema: LayerSchema) { 63 | const layer: Layer = this._layerStack[index]; 64 | layer.clearDrawAction(); 65 | layer.draw(layerSchema); 66 | } 67 | 68 | drawAllLayer(sketchSchema: SketchSchema) { 69 | const layerSchemaList: LayerSchema[] = sketchSchema.layerList; 70 | layerSchemaList.forEach((layerSchema: LayerSchema, index: number) => { 71 | this.drawLayer(index, layerSchema); 72 | }) 73 | } 74 | 75 | getSchema() { 76 | const layerList = []; 77 | this._layerStack.forEach((layer) => { 78 | const schema = layer.getSchema(); 79 | layerList.push(schema); 80 | }); 81 | return { 82 | layerList, 83 | } 84 | } 85 | 86 | mergeLayer() { 87 | const { _tempCanvas, _width, _height } = this; 88 | let tempContext = _tempCanvas.getContext('2d'); 89 | tempContext.clearRect(0, 0, _width, _height); 90 | const canvasStack: HTMLCanvasElement[] = this._canvasStack; 91 | canvasStack.forEach((canvas: HTMLCanvasElement) => { 92 | tempContext.drawImage(canvas, 0, 0); 93 | }) 94 | const mergeImageData = tempContext.getImageData(0, 0, _width, _height); 95 | tempContext.clearRect(0, 0, _width, _height); 96 | return mergeImageData; 97 | } 98 | 99 | moveUpLayer(index: number) { 100 | // TODO 101 | } 102 | 103 | moveDownLayer(index: number) { 104 | // TODO 105 | } 106 | 107 | 108 | } -------------------------------------------------------------------------------- /src/core/sketchpad/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Authors chenshenhai. All rights reserved. MIT license. 2 | // https://github.com/chenshenhai/logox/blob/master/src/sketchpad/mod.ts 3 | 4 | 5 | import { Sketch, SketchSchema } from './../sketch/index'; 6 | import { LayerSchema } from './../layer/index'; 7 | 8 | export interface SketchpadOptions { 9 | width: number, 10 | height: number, 11 | layerCount: number, 12 | container: HTMLElement, 13 | } 14 | 15 | function mergeCSS2Style(css: object) { 16 | let result = ''; 17 | let resultList = []; 18 | const keys = Object.keys(css); 19 | keys.forEach((name: string) => { 20 | if (typeof name === 'string') { 21 | const value: string = css[name] || ''; 22 | if (typeof value === 'string') { 23 | resultList.push(`${name}:${value}`); 24 | } 25 | } 26 | }); 27 | result = resultList.join(';'); 28 | return result; 29 | } 30 | 31 | export class Sketchpad extends Sketch { 32 | 33 | private _options: SketchpadOptions; 34 | private _container: HTMLElement; 35 | private _sketchSchema: SketchSchema; 36 | 37 | constructor(opts: SketchpadOptions) { 38 | super({ 39 | width: opts.width, 40 | height: opts.height, 41 | layerCount: opts.layerCount 42 | }); 43 | this._options = opts; 44 | this._container = opts.container 45 | } 46 | 47 | render(sketchSchema: SketchSchema) { 48 | this._sketchSchema = sketchSchema; 49 | const container: HTMLElement = this._container; 50 | while (container.firstChild) { 51 | let tempNode = container.removeChild(container.firstChild); 52 | tempNode = null; 53 | } 54 | const { width, height, layerCount } = this._options; 55 | const style = mergeCSS2Style({ 56 | width: `${width}px`, 57 | height: `${height}px`, 58 | position: 'relative', 59 | display: 'inline-block', 60 | }); 61 | const canvasStack = this.getCanvasStack(); 62 | container.setAttribute('style', style); 63 | const count = layerCount; 64 | for (let i = 0; i < count; i ++) { 65 | const canvas = canvasStack[i]; 66 | canvas.width = width; 67 | canvas.height = height; 68 | canvas.setAttribute('style', mergeCSS2Style({ 69 | width: `${width}px`, 70 | height: `${height}px`, 71 | position: 'absolute', 72 | left: '0', 73 | top: '0', 74 | })); 75 | container.appendChild(canvas); 76 | } 77 | this.drawAllLayer(sketchSchema); 78 | } 79 | 80 | renderLayer(index: number, layerSchema: LayerSchema) { 81 | this.drawLayer(index, layerSchema); 82 | } 83 | 84 | downloadImage(filename = 'download-image') { 85 | const { width, height } = this._options; 86 | let tempCanvas = document.createElement('canvas'); 87 | tempCanvas.width = width; 88 | tempCanvas.height = height; 89 | const tempContext = tempCanvas.getContext('2d'); 90 | const mergeImageData = this.mergeLayer(); 91 | tempContext.putImageData(mergeImageData, 0, 0); 92 | const stream = tempCanvas.toDataURL("image/png"); 93 | const downloadLink = document.createElement('a'); 94 | downloadLink.href = stream; 95 | downloadLink.download = filename; 96 | const downloadClickEvent = document.createEvent('MouseEvents'); 97 | downloadClickEvent.initEvent('click', true, false); 98 | downloadLink.dispatchEvent(downloadClickEvent); 99 | 100 | // clear 101 | tempContext.clearRect(0, 0, width, height); 102 | tempCanvas = null; 103 | } 104 | 105 | } 106 | 107 | -------------------------------------------------------------------------------- /src/ui/module/sketch/index.ts: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | import canvasRender from './../../../core/canvas/render'; 4 | import { SketchSchema } from './../../../core/sketch/index'; 5 | import { Sketchpad, SketchpadOptions } from './../../../core/sketchpad/index'; 6 | import eventHub from './../../service/event-hub'; 7 | import cacheHub from './../../service/cache-hub'; 8 | import schemaParser from './../../service/schema-parser'; 9 | 10 | export interface SketchOpts { 11 | imageData: ImageData; 12 | } 13 | 14 | export class Sketch { 15 | private _mount: HTMLElement = null; 16 | private _opts: SketchOpts = null; 17 | private _hasRendered: boolean = false; 18 | private _sketchpad: Sketchpad = null; 19 | 20 | constructor(mount: HTMLElement, opts: SketchOpts) { 21 | const that = this; 22 | this._mount = mount; 23 | this._opts = opts; 24 | this._render(); 25 | 26 | const { imageData, } = this._opts; 27 | const hiddenSketchpad: HTMLElement = this._mount.querySelector('div.pictool-sketch-hidden-area-sketchpad'); 28 | const height: number = imageData.height; 29 | const width: number = imageData.width; 30 | const layerCount: number = 2; 31 | const padOpts : SketchpadOptions = { 32 | height, 33 | width, 34 | layerCount, 35 | container: hiddenSketchpad, 36 | } 37 | const sketchpad = new Sketchpad(padOpts); 38 | this._sketchpad = sketchpad; 39 | 40 | const originSketchSchema = schemaParser.parseImageDataToSchema(imageData); 41 | cacheHub.set('Sketch.originSketchSchema', originSketchSchema); 42 | 43 | eventHub.on('GlobalEvent.moduleSketch.renderImage', function(schema) { 44 | that.renderImage(schema); 45 | }); 46 | eventHub.on('GlobalEvent.moduleSketch.downloadImage', function() { 47 | sketchpad.downloadImage('download-pictool.png'); 48 | }); 49 | } 50 | 51 | renderImage(sketchSchema: SketchSchema) { 52 | const sketchpad = this._sketchpad; 53 | sketchpad.render(sketchSchema); 54 | const mergeImageData = sketchpad.mergeLayer(); 55 | const canvas = this._mount.querySelector('canvas.pictool-sketch-canvas'); 56 | canvasRender.renderImageData(canvas, mergeImageData); 57 | 58 | cacheHub.set('Sketch.sketchSchema', sketchSchema); 59 | } 60 | 61 | private _render() { 62 | if (this._hasRendered === true) { 63 | return; 64 | } 65 | const html = ` 66 |
67 |
68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | `; 79 | this._mount.innerHTML = html; 80 | this._registerEvent(); 81 | this._hasRendered = true; 82 | } 83 | 84 | private _registerEvent() { 85 | if (this._hasRendered === true) { 86 | return; 87 | } 88 | const that = this; 89 | eventHub.on('GlobalEvent.moduleSketch.resizeCanvas', function() { 90 | that._resizeSketch(); 91 | }); 92 | } 93 | 94 | private _resizeSketch() { 95 | const container = this._mount.querySelector('.pictool-sketch-container'); 96 | const canvas = container.querySelector('.pictool-sketch-canvas'); 97 | const height = container.clientHeight; 98 | const width = container.clientWidth; 99 | const size = Math.min(height, width); 100 | canvas.setAttribute('style', `max-height: ${size}px; max-width: ${size}px; `); 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pictool 2 | 3 | ## A front-end image processing gadget 4 | [![Node.js CI](https://github.com/chenshenhai/pictool/actions/workflows/node.js.yml/badge.svg?branch=master)](https://github.com/chenshenhai/pictool/actions/workflows/node.js.yml) 5 | [![Build Status](https://travis-ci.com/chenshenhai/pictool.svg?branch=master)](https://travis-ci.com/chenshenhai/pictool) 6 | [![npm-version](https://img.shields.io/npm/l/pictool.svg)](./LICENSE) 7 | [![](https://img.shields.io/npm/v/pictool.svg)](https://www.npmjs.com/package/pictool) 8 | 9 | ![pictool-logo](https://user-images.githubusercontent.com/8216630/61581603-28ffd180-ab53-11e9-9461-a24d31643ec7.png) 10 | 11 | 12 | > Examples of online use 13 | 14 | [https://chenshenhai.github.io/pictool/example/module/pictool-ui.html](https://chenshenhai.github.io/pictool/example/module/pictool-ui.html) 15 | 16 | ## Installation 17 | 18 | ### Prerequisites 19 | 20 | - Operating System: Windows,macOS,Linux 21 | - Node.js Runtime: `12.3+` 22 | 23 | 24 | ### NPM Usage 25 | 26 | ```sh 27 | npm i --save pictool 28 | ``` 29 | 30 | 31 | ```js 32 | import Pictool from 'pictool'; 33 | ``` 34 | 35 | or 36 | 37 | ```js 38 | import PictoolBrowser from 'pictool/dist/browser'; 39 | import PictoolUI from 'pictool/dist/ui'; 40 | import PictoolDigit from 'pictool/dist/digit'; 41 | ``` 42 | 43 | ### CDN Usage 44 | 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | or 51 | 52 | ```html 53 | 54 | 55 | 56 | ``` 57 | 58 | 59 | ## Getting started 60 | 61 | ### JavaScript Code 62 | 63 | ```js 64 | import Pictool from 'pictool'; 65 | 66 | const src = './image/test.jpg'; 67 | const Sandbox = Pictool.browser.Sandbox; 68 | const sandbox = new Sandbox(src); 69 | const dom = document.querySelector('#J_Example_01'); 70 | 71 | sandbox.queueProcess([ 72 | { 73 | process: 'sobel', 74 | options: {}, 75 | }, 76 | { 77 | process: 'invert', 78 | options: {}, 79 | } 80 | ]).then(function(base64) { 81 | dom.innerHTML = ``; 82 | }).catch(function(err) { 83 | console.log(err); 84 | }); 85 | ``` 86 | 87 | ### HTML Code 88 | 89 | ```html 90 | 91 | 92 | 93 | 97 | 98 | 99 |
100 | 101 |
102 | 103 |
104 | 105 |
106 | 107 | 108 | 109 | ``` 110 | 111 | ### Browser Result 112 | 113 | 114 | ![001](https://user-images.githubusercontent.com/8216630/61582779-bb0ed680-ab61-11e9-8830-01fbf59edb94.jpg) 115 | 116 | 117 | 118 | ## Features 119 | 120 | - ✔︎ Brightness 121 | - ✔︎ Hue 122 | - ✔︎ Saturation 123 | - ✔︎ Alpha 124 | - ✔︎ Invert 125 | - ✔︎ Grayscale 126 | - ✔︎ Sobel 127 | - ✔︎ Sepia 128 | - ✔︎ Posterize 129 | - ✔︎ Gamma 130 | 131 | 132 | ## Documentation 133 | 134 | - [中文文档](https://chenshenhai.github.io/pictool-doc/) 135 | - [English Documents](https://chenshenhai.github.io/pictool-doc/page/en-US/) //TODO 136 | 137 | ## Example 138 | 139 | > Please use the latest version of Chrome Browser 140 | 141 | > 请在最新版本 chrome 浏览器下浏览 142 | 143 | [https://chenshenhai.github.io/pictool/example/index.html](https://chenshenhai.github.io/pictool/example/index.html) 144 | 145 | 146 | 147 | 148 | ## Testing 149 | 150 | ```sh 151 | npm run test 152 | ``` 153 | 154 | ## License 155 | 156 | [MIT](./LICENSE) -------------------------------------------------------------------------------- /src/core/digit/transform/rgb2hsl.ts: -------------------------------------------------------------------------------- 1 | import { RGBCell } from '../rgba/rgb'; 2 | import { HSLObject, HSLCell } from '../hsl/hsl'; 3 | import { RGBA_MID, RGBA_MAX, RGBA_MIN } from './static'; 4 | 5 | const parseRGBNum = function(origin: number): number { 6 | return origin * 100 / RGBA_MAX; // [1, 100] 7 | } 8 | 9 | function isPercent(num: number) { 10 | if(num >= -100 && num <= 100) { 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | } 16 | 17 | function isHueValue(num: number) { 18 | if(num >= 0 && num <= 360) { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | } 24 | 25 | function isLightnessValue(num: number) { 26 | if(num >= 0 && num <= 100) { 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | } 32 | 33 | function isStaurationValue(num: number) { 34 | if(num >= 0 && num <= 100) { 35 | return true; 36 | } else { 37 | return false; 38 | } 39 | } 40 | 41 | 42 | export interface HSLTransformPercent { 43 | h?: number; // [-100, 100] 44 | s?: number; // [-100, 100] 45 | l?: number; // [-100, 100] 46 | } 47 | 48 | export interface HSLTransformValue { 49 | h?: number; // [0, 360] 50 | s?: number; // [0, 100] 51 | l?: number; // [0, 100] 52 | } 53 | 54 | export interface HSLTransformOpts { 55 | percent?: HSLTransformPercent; 56 | value?: HSLTransformValue; 57 | } 58 | 59 | export const RGB2HSL = function(cell: RGBCell, opts?: HSLTransformOpts): HSLCell { 60 | 61 | const orginR = cell.r; 62 | const orginG = cell.g; 63 | const orginB = cell.b; 64 | 65 | const r = parseRGBNum(orginR); 66 | const g = parseRGBNum(orginG); 67 | const b = parseRGBNum(orginB); 68 | 69 | const min: number = Math.min(r, g, b); 70 | const max: number = Math.max(r, g, b); 71 | const range: number = max - min; 72 | 73 | let h: number = 0; // [0, 360] 74 | let s: number = 0; // [0, 100] 75 | let l: number = (max + min) / 2; // [0, 100] 76 | 77 | if (max === min) { 78 | h = 0; 79 | s = 0; 80 | } else { 81 | // transform Hua 82 | if (max === r && g >= b) { 83 | h = 60 * ((g - b) / range) + 0; 84 | } else if (max === r && g < b) { 85 | h = 60 * ((g - b) / range) + 360; 86 | } else if (max === g) { 87 | h = 60 * ((b - r) / range) + 120; 88 | } else if (max === b) { 89 | h = 60 * ((r - g) / range) + 240; 90 | } 91 | 92 | // tranform Statution 93 | if (l === 0 || max === min) { 94 | s = 0 95 | } else if (l > RGBA_MIN && l <= RGBA_MID) { 96 | s = range / (max + min); 97 | } else if (l > RGBA_MID) { 98 | s = range / (2 * RGBA_MAX - (max + min)); 99 | } 100 | } 101 | 102 | 103 | 104 | h = Math.round(h); 105 | s = Math.round(s * 100); 106 | l = Math.round(l); 107 | 108 | if (opts && opts.value) { 109 | const { value } = opts; 110 | if (value.h && isHueValue(value.h)) { 111 | h = value.h; 112 | h = Math.min(360, h); 113 | h = Math.max(0, h); 114 | } 115 | 116 | if (value.s && isStaurationValue(value.s)) { 117 | s = value.s; 118 | s = Math.min(100, s); 119 | s = Math.max(0, s); 120 | } 121 | 122 | if (value.l && isLightnessValue(value.l)) { 123 | l = value.l; 124 | l = Math.min(100, l); 125 | l = Math.max(0, l); 126 | } 127 | } else if (opts && opts.percent) { 128 | const { percent } = opts; 129 | if (percent.h && isPercent(percent.h)) { 130 | h = Math.floor(h * (100 + percent.h) / 100); 131 | h = Math.min(360, h); 132 | h = Math.max(0, h); 133 | } 134 | 135 | if (percent.s && isPercent(percent.s)) { 136 | s = Math.floor(s * (100 + percent.s) / 100); 137 | s = Math.min(100, s); 138 | s = Math.max(0, s); 139 | } 140 | 141 | if (percent.l && isPercent(percent.l)) { 142 | l = Math.floor(l * (100 + percent.l) / 100) 143 | l = Math.min(100, l); 144 | l = Math.max(0, l); 145 | } 146 | } 147 | 148 | return { h, s, l }; 149 | } 150 | -------------------------------------------------------------------------------- /src/util/sanbox.ts: -------------------------------------------------------------------------------- 1 | import { DigitImageData } from '../core/digit/digit-image-data'; 2 | import { imageData2DigitImageData, imageData2Base64, } from './image-data'; 3 | import { getImageBySrc, getImageDataBySrc } from './image-file'; 4 | import { Effect } from '../core/digit/effect/index'; 5 | import { 6 | compressImage, 7 | CompressImageOpts, 8 | CompressImageTypeEnum 9 | } from './compress'; 10 | 11 | export interface SandboxOpts { 12 | compressRatio: number; // [1, 0] 13 | } 14 | 15 | export interface ProcessOpts { 16 | process: string; 17 | options?: any; 18 | } 19 | 20 | export class Sandbox { 21 | 22 | private _imgSrc: string; 23 | private _options: SandboxOpts|undefined; 24 | private _digitImg: DigitImageData|null = null; 25 | private _effect: Effect|null = null; 26 | 27 | constructor(imgSrc: string, opts?: SandboxOpts) { 28 | this._imgSrc = imgSrc; 29 | this._options = opts; 30 | } 31 | 32 | public queueProcess(opts: ProcessOpts|ProcessOpts[]): Promise { 33 | const queue: ProcessOpts[] = []; 34 | if (Array.isArray(opts)) { 35 | opts.forEach(function(opt) { 36 | queue.push(opt); 37 | }) 38 | } else { 39 | queue.push(opts) 40 | } 41 | return new Promise((resolve, reject) => { 42 | this._getEffectAsync().then((effect) => { 43 | queue.forEach((opt: ProcessOpts) => { 44 | const process: string = opt.process; 45 | const options: any = opt.options; 46 | this._digitImg = effect.process(process, options).getDigitImageData(); 47 | }); 48 | const imageData = effect.getImageData(); 49 | const base64: string|null = imageData2Base64(imageData); 50 | resolve(base64); 51 | }).catch((err: Error) => { 52 | reject(err); 53 | }) 54 | }) 55 | } 56 | 57 | public _getEffectAsync(): Promise { 58 | if (this._effect instanceof Effect) { 59 | return Promise.resolve(this._effect); 60 | } 61 | 62 | return new Promise((resolve, reject) => { 63 | this._parseDigitAsync().then((result) => { 64 | if (result === true) { 65 | const digitData: DigitImageData|null = this._digitImg; 66 | if (digitData instanceof DigitImageData) { 67 | const effect = new Effect(digitData); 68 | this._effect = effect; 69 | resolve(this._effect); 70 | } else { 71 | reject(new Error('_digitImg is null')) 72 | } 73 | } else { 74 | reject(new Error('image src parse fail')) 75 | } 76 | }).catch((err: Error) => { 77 | reject(err); 78 | }); 79 | }); 80 | } 81 | 82 | private _parseDigitAsync(): Promise { 83 | if (this._digitImg) { 84 | return Promise.resolve(true); 85 | } 86 | const options: SandboxOpts|undefined = this._options; 87 | let compressRatio: number = 1; 88 | if (options) { 89 | compressRatio = options.compressRatio; 90 | } 91 | const imgSrc = this._imgSrc; 92 | let ratio: number = Math.max(0.1, compressRatio); 93 | ratio = Math.min(1, compressRatio); 94 | const imgType = CompressImageTypeEnum.jpg; 95 | const compressOpts: CompressImageOpts = {type: imgType, encoderOptions: ratio } 96 | return new Promise((resolve, reject) => { 97 | getImageBySrc(imgSrc).then((img: HTMLImageElement) => { 98 | const compressedImgSrc: string|null = compressImage(img, compressOpts); 99 | if (typeof compressedImgSrc === 'string') { 100 | getImageDataBySrc(compressedImgSrc).then((imgData: ImageData) => { 101 | const digitImg: DigitImageData = imageData2DigitImageData(imgData); 102 | this._digitImg = digitImg; 103 | resolve(true); 104 | }).catch((err: Error) => { 105 | reject(err); 106 | }); 107 | } else { 108 | reject(new Error('compressImage result is null')); 109 | } 110 | 111 | }).catch((err: Error) => { 112 | reject(err); 113 | }); 114 | }); 115 | } 116 | 117 | 118 | } -------------------------------------------------------------------------------- /src/ui/module/panel/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionSheet, ActionSheetOpts, ActionSheetLifeCycleArgs, } from '../../component/action-sheet/index'; 2 | import istype from '../../../util/istype'; 3 | import eventHub from '../../service/event-hub'; 4 | import { SketchSchema } from '../../../core/sketch'; 5 | 6 | import './index.less'; 7 | 8 | interface NavBtn { 9 | name: string; 10 | feedback(): Promise|null; 11 | // useProgressBar?: boolean | false; 12 | receiveEventKey?: string | null; 13 | } 14 | 15 | export interface PanelOpts { 16 | title: string; 17 | navList: NavBtn[]; 18 | zIndex: number; 19 | mount: HTMLElement; 20 | } 21 | 22 | export class Panel { 23 | private _actionSheet: ActionSheet = null; 24 | private _opts: PanelOpts = null; 25 | private _hasRendered: boolean = false; 26 | 27 | constructor(opts: PanelOpts) { 28 | this._opts = opts; 29 | const that = this; 30 | const { zIndex, mount, } = opts; 31 | const actionSheetOpts: ActionSheetOpts = { 32 | height: 120, 33 | mount, 34 | zIndex, 35 | afterRender(args: ActionSheetLifeCycleArgs) { 36 | const { contentMount, } = args; 37 | that._render(contentMount); 38 | }, 39 | beforeHide: function() { 40 | eventHub.trigger('GlobalEvent.moduleDashboard.progress.hide'); 41 | } 42 | } 43 | const actionSheet = new ActionSheet(actionSheetOpts); 44 | this._actionSheet = actionSheet; 45 | } 46 | 47 | show() { 48 | this._actionSheet.show(); 49 | } 50 | 51 | hide() { 52 | this._actionSheet.hide(); 53 | } 54 | 55 | private _render(mount: HTMLElement) { 56 | if (this._hasRendered === true) { 57 | return; 58 | } 59 | const opts: PanelOpts = this._opts; 60 | const { navList, title, } = opts; 61 | const isBeyond = navList.length > 4; 62 | const html = ` 63 |
64 |
65 |
66 |
${title || ''}
67 |
68 |
69 |
72 | ${istype.array(navList) && navList.map(function(nav: NavBtn, idx) { 73 | return ` 74 |
77 | ${nav.name} 78 |
79 | `; 80 | }).join('')} 81 |
82 |
83 |
84 | `; 85 | mount.innerHTML = html; 86 | this._registerEvent(mount); 87 | this._hasRendered = true; 88 | } 89 | 90 | private _registerEvent(mount: HTMLElement) { 91 | if (this._hasRendered === true) { 92 | return; 93 | } 94 | const that = this; 95 | const opts: PanelOpts = this._opts; 96 | const { navList, } = opts; 97 | const navElemList = mount.querySelectorAll('[data-panel-nav-idx]'); 98 | const btnClose = mount.querySelector('div.pictool-panel-btn-close'); 99 | 100 | btnClose.addEventListener('click', function() { 101 | that.hide(); 102 | }); 103 | 104 | if (istype.nodeList(navElemList) === true) { 105 | navElemList.forEach(function(navElem) { 106 | navElem.addEventListener('click', function(event) { 107 | 108 | const elem = this; 109 | const idx = elem.getAttribute('data-panel-nav-idx') * 1; 110 | const navConf = navList[idx]; 111 | const primise = navConf.feedback(); 112 | 113 | if (istype.promise(primise)) { 114 | primise.then(function(rs) { 115 | if (rs) { 116 | eventHub.trigger('GlobalEvent.moduleSketch.renderImage', rs) 117 | } 118 | }).catch((err) => { 119 | console.log(err); 120 | }) 121 | } else if(istype.null(primise) !== true) { 122 | console.warn('feedback is not a promise or null') 123 | } 124 | navElemList.forEach(function(nav){ 125 | nav.classList.remove('panelnav-active'); 126 | }) 127 | elem.classList.add('panelnav-active'); 128 | }); 129 | }); 130 | } 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/core/digit/filter/index.ts: -------------------------------------------------------------------------------- 1 | import { FilterOpts } from './filter'; 2 | import { Effect } from './../effect/index'; 3 | import browser from './../../../browser'; 4 | 5 | export const origin = function(opts: FilterOpts ) { 6 | const { imageData } = opts; 7 | const rsImageData = browser.util.digitImageData2ImageData(imageData); 8 | return rsImageData; 9 | } 10 | 11 | // base image process filter 12 | 13 | export const grayscale = function(opts: FilterOpts ) { 14 | const { imageData } = opts; 15 | let effect:Effect|null = new Effect(imageData); 16 | const rsImageData = effect.process('grayscale').getImageData(); 17 | effect.destory(); 18 | effect = null; 19 | return rsImageData; 20 | } 21 | 22 | export const hue = function(opts: FilterOpts ) { 23 | const { imageData, options } = opts; 24 | let effect:Effect|null = new Effect(imageData); 25 | const rsImageData = effect.process('hue', options).getImageData(); 26 | effect.destory(); 27 | effect = null; 28 | return rsImageData; 29 | } 30 | 31 | export const lightness = function(opts: FilterOpts ) { 32 | const { imageData, options } = opts; 33 | let effect:Effect|null = new Effect(imageData); 34 | const rsImageData = effect.process('lightness', options).getImageData(); 35 | effect.destory(); 36 | effect = null; 37 | return rsImageData; 38 | } 39 | 40 | export const saturation = function(opts: FilterOpts ) { 41 | const { imageData, options } = opts; 42 | let effect:Effect|null = new Effect(imageData); 43 | const rsImageData = effect.process('saturation', options).getImageData(); 44 | effect.destory(); 45 | effect = null; 46 | return rsImageData; 47 | } 48 | 49 | export const invert = function(opts: FilterOpts ) { 50 | const { imageData, options } = opts; 51 | let effect:Effect|null = new Effect(imageData); 52 | const rsImageData = effect.process('invert', options).getImageData(); 53 | effect.destory(); 54 | effect = null; 55 | return rsImageData; 56 | } 57 | 58 | export const sobel = function(opts: FilterOpts ) { 59 | const { imageData, options } = opts; 60 | let effect:Effect|null = new Effect(imageData); 61 | const rsImageData = effect.process('sobel', options).getImageData(); 62 | effect.destory(); 63 | effect = null; 64 | return rsImageData; 65 | } 66 | 67 | // multiple image process filter 68 | 69 | export const lineDrawing = function(opts: FilterOpts ) { 70 | const { imageData, options } = opts; 71 | let effect:Effect|null = new Effect(imageData); 72 | const rsImageData = effect.process('sobel', options).process('invert', options).getImageData(); 73 | effect.destory(); 74 | effect = null; 75 | return rsImageData; 76 | } 77 | 78 | 79 | export const oilDrawing = function(opts: FilterOpts ) { 80 | const { imageData, options } = opts; 81 | let effect:Effect|null = new Effect(imageData); 82 | const rsImageData = effect.process('gamma', {value: 8}).process('posterize', {value: 8}).getImageData(); 83 | effect.destory(); 84 | effect = null; 85 | return rsImageData; 86 | } 87 | 88 | export const old = function(opts: FilterOpts ) { 89 | const { imageData, options } = opts; 90 | let effect:Effect|null = new Effect(imageData); 91 | const rsImageData = effect.process('sepia').getImageData(); 92 | effect.destory(); 93 | effect = null; 94 | return rsImageData; 95 | } 96 | 97 | export const natural = function(opts: FilterOpts ) { 98 | const { imageData, options } = opts; 99 | let effect:Effect|null = new Effect(imageData); 100 | const rsImageData = effect.process('saturation', {percent: 76 }).getImageData(); 101 | effect.destory(); 102 | effect = null; 103 | return rsImageData; 104 | } 105 | 106 | export const alpha = function(opts: FilterOpts ) { 107 | const { imageData, options } = opts; 108 | let effect:Effect|null = new Effect(imageData); 109 | const rsImageData = effect.process('alpha', options).getImageData(); 110 | effect.destory(); 111 | effect = null; 112 | return rsImageData; 113 | } 114 | 115 | 116 | 117 | export const sepia = function(opts: FilterOpts ) { 118 | const { imageData, options } = opts; 119 | let effect:Effect|null = new Effect(imageData); 120 | const rsImageData = effect.process('sepia', options).getImageData(); 121 | effect.destory(); 122 | effect = null; 123 | return rsImageData; 124 | } 125 | 126 | 127 | export const posterize = function(opts: FilterOpts ) { 128 | const { imageData, options } = opts; 129 | let effect:Effect|null = new Effect(imageData); 130 | const rsImageData = effect.process('posterize', options).getImageData(); 131 | effect.destory(); 132 | effect = null; 133 | return rsImageData; 134 | } 135 | 136 | 137 | export const gamma = function(opts: FilterOpts ) { 138 | const { imageData, options } = opts; 139 | let effect:Effect|null = new Effect(imageData); 140 | const rsImageData = effect.process('gamma', options).getImageData(); 141 | effect.destory(); 142 | effect = null; 143 | return rsImageData; 144 | } -------------------------------------------------------------------------------- /src/ui/module/panel/index.less: -------------------------------------------------------------------------------- 1 | .pictool-module-panel { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | font-size: 14px; 6 | color: #333333; 7 | 8 | .pictool-panel-header { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | right: 0; 13 | height: 40px; 14 | box-shadow: 0 0 6px 0 rgba(0,0,0,0.08); 15 | 16 | .pictool-panel-title { 17 | line-height: 40px; 18 | text-align: center; 19 | font-size: 16px; 20 | } 21 | 22 | .pictool-panel-btn-close { 23 | position: absolute; 24 | right: 0; 25 | top: 0; 26 | width: 60px; 27 | height: 40px; 28 | &::before { 29 | content: ''; 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | right: 0; 34 | bottom: 0; 35 | background: url('data:image/svg+xml;charset=utf-8,'); 36 | background-repeat: no-repeat; 37 | background-position: center; 38 | background-size: 30px; 39 | } 40 | } 41 | 42 | } 43 | 44 | .pictool-panel-navigation { 45 | position: absolute; 46 | height: 80px; 47 | width: 100%; 48 | bottom: 0; 49 | background: #2196f30d; 50 | overflow: auto; 51 | -webkit-overflow-scrolling: touch; 52 | } 53 | 54 | .pictool-panel-navlist { 55 | width: 100%; 56 | height: 100%; 57 | display: flex; 58 | -webkit-overflow-scrolling: touch; 59 | 60 | &.panel-beyond-width { 61 | display: block; 62 | .pictool-panel-nav-btn { 63 | width: 100px; 64 | display: block; 65 | float: left; 66 | } 67 | } 68 | } 69 | .pictool-panel-nav-btn { 70 | flex: 1; 71 | text-align: center; 72 | font-size: 14px; 73 | position: relative; 74 | padding-top: 40px; 75 | 76 | &.panelnav-icon { 77 | &::before { 78 | content: ''; 79 | position: absolute; 80 | top: 0; 81 | left: 0; 82 | right: 0; 83 | height: 40px; 84 | background: url('data:image/svg+xml;charset=utf-8,'); 85 | background-repeat: no-repeat; 86 | background-position: center bottom; 87 | background-size: 30px; 88 | } 89 | } 90 | 91 | &.panelnav-active { 92 | color: #17abe3; 93 | &.panelnav-icon { 94 | &::before { 95 | content: ''; 96 | position: absolute; 97 | top: 0; 98 | left: 0; 99 | right: 0; 100 | height: 40px; 101 | background: url('data:image/svg+xml;charset=utf-8,'); 102 | background-repeat: no-repeat; 103 | background-position: center bottom; 104 | background-size: 30px; 105 | } 106 | } 107 | } 108 | 109 | 110 | } 111 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ui/component/progress/index.ts: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | import { mergeCSS2StyleAttr } from './../../../util/style'; 4 | import istype from './../../../util/istype'; 5 | 6 | 7 | export interface ProgressOpts { 8 | mount: HTMLElement; 9 | customStyle: {}; 10 | percent: number; // [0, 100]; 11 | onChange: Function|null; 12 | max?: number, // default 100 13 | min?: number, // default 0 14 | } 15 | 16 | 17 | export interface ProcessOnChangeData { 18 | value: number; // [0, 100] 19 | } 20 | 21 | export class Progress { 22 | private _options: ProgressOpts = null; 23 | private _hasRendered: boolean = false; 24 | private _component: HTMLElement = null; 25 | private _rangeList: number[]; 26 | 27 | constructor(opts: ProgressOpts) { 28 | this._options = opts; 29 | this._render(); 30 | this._rangeList = []; 31 | 32 | const options = this._options; 33 | const { max = 100, min = 0 } = options; 34 | this.resetRange(max, min); 35 | } 36 | 37 | private _render() { 38 | if (this._hasRendered === true) { 39 | return; 40 | } 41 | const options = this._options; 42 | const { mount, customStyle, percent, } = options; 43 | const styleAttr = mergeCSS2StyleAttr(customStyle); 44 | const html = ` 45 |
46 |
47 |
48 |
49 |
50 | `; 51 | 52 | const tempDom = document.createElement('div');; 53 | tempDom.innerHTML = html; 54 | const component: HTMLDivElement = tempDom.querySelector('div.pictool-component-progress'); 55 | mount.appendChild(component); 56 | this._component = component; 57 | this._setInnerMovePercent(percent); 58 | this._triggerEvent(); 59 | } 60 | 61 | show() { 62 | this._component.classList.remove('progress-hidden'); 63 | } 64 | 65 | hide() { 66 | this._component.classList.add('progress-hidden'); 67 | } 68 | 69 | resetPercent(percent: number) { 70 | this._setInnerMovePercent(percent); 71 | } 72 | 73 | resetOnChange(onChange: Function|null) { 74 | this._options.onChange = onChange; 75 | } 76 | 77 | resetRange(min: number, max: number) { 78 | this._rangeList = []; 79 | const item = (max - min) / 100; 80 | for (let i = min; i < max; i += item) { 81 | this._rangeList.push(i); 82 | } 83 | this._rangeList.push(max); 84 | } 85 | 86 | private _triggerEvent() { 87 | const that = this; 88 | const options = this._options; 89 | const component: HTMLElement = this._component; 90 | const outer = component.querySelector('.pictool-progress-outer'); 91 | const inner = component.querySelector('.pictool-progress-inner'); 92 | 93 | outer.addEventListener('touchstart', function(event: TouchEvent) { 94 | const touchClientX = event.touches[0].clientX; 95 | let movePercent = that._calculateMovePercent(touchClientX); 96 | that._setInnerMovePercent(movePercent); 97 | }) 98 | outer.addEventListener('touchmove', function(event: TouchEvent) { 99 | const touchClientX = event.touches[0].clientX; 100 | let movePercent = that._calculateMovePercent(touchClientX); 101 | that._setInnerMovePercent(movePercent); 102 | }); 103 | outer.addEventListener('touchend', function() { 104 | const value = that._getInnerValue(); 105 | const data: ProcessOnChangeData = { 106 | value, 107 | } 108 | const options = that._options; 109 | const { onChange, } = options; 110 | if (istype.function(onChange)) { 111 | onChange(data); 112 | } 113 | }); 114 | 115 | 116 | outer.addEventListener('mousedown', function(event: MouseEvent) { 117 | const touchClientX = event.clientX; 118 | let movePercent = that._calculateMovePercent(touchClientX); 119 | that._setInnerMovePercent(movePercent); 120 | }) 121 | // outer.addEventListener('mousemove', function(event: MouseEvent) { 122 | // const touchClientX = event.clientX; 123 | // let movePercent = that._calculateMovePercent(touchClientX); 124 | // that._setInnerMovePercent(movePercent); 125 | // }); 126 | outer.addEventListener('mouseup', function() { 127 | const value = that._getInnerValue(); 128 | const data: ProcessOnChangeData = { 129 | value, 130 | } 131 | const options = that._options; 132 | const { onChange, } = options; 133 | if (istype.function(onChange)) { 134 | onChange(data); 135 | } 136 | }); 137 | } 138 | 139 | private _calculateMovePercent(touchClientX: number) { 140 | const component: HTMLElement = this._component; 141 | const outer = component.querySelector('.pictool-progress-outer'); 142 | const outerLeft = this._getViewAbsoluteLeft(outer); 143 | const outerWidth = outer.clientWidth; 144 | const moveLelf = touchClientX - outerLeft; 145 | let movePercent = Math.ceil(moveLelf * 100 / outerWidth); 146 | return movePercent; 147 | } 148 | 149 | private _setInnerMovePercent(percent: number) { 150 | const component = this._component; 151 | const inner = component.querySelector('.pictool-progress-inner'); 152 | let displayPercent = percent > 0 ? percent : 0; 153 | displayPercent = Math.min(displayPercent, 100); 154 | displayPercent = Math.max(displayPercent, 0); 155 | const innerStyleAttr = mergeCSS2StyleAttr({ 156 | left: `-${100 - displayPercent}%` 157 | }); 158 | inner.setAttribute('style', innerStyleAttr); 159 | inner.setAttribute('data-component-inner-percent', `${displayPercent}`); 160 | } 161 | 162 | private _getInnerValue() { 163 | const component = this._component; 164 | const inner = component.querySelector('.pictool-progress-inner'); 165 | const percentAttr: string = inner.getAttribute('data-component-inner-percent'); 166 | let percent = parseInt(percentAttr, 10); 167 | percent = Math.min(100, percent); 168 | percent = Math.max(0, percent); 169 | const value = this._rangeList[percent] 170 | return value; 171 | } 172 | 173 | private _getViewAbsoluteLeft(elem){ 174 | let actualLeft = elem.offsetLeft; 175 | let current = elem.offsetParent; 176 | 177 | while (current !== null){ 178 | actualLeft += current.offsetLeft; 179 | current = current.offsetParent; 180 | } 181 | return actualLeft; 182 | } 183 | 184 | } -------------------------------------------------------------------------------- /__tests__/digit-process.adjust.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const digit = require('../dist/digit'); 3 | const expect = chai.expect 4 | const process = digit.process; 5 | const DigitImageData = digit.DigitImageData; 6 | 7 | const img = require('./data/image-data-origin.json'); 8 | const imgHueVal180 = require('./data/image-data-hue.val.180.json'); 9 | const imgHuePer75 = require('./data/image-data-hue.per.75.json'); 10 | const imgSaturationVal50 = require('./data/image-data-saturation.val.50.json'); 11 | const imgSaturationPer60 = require('./data/image-data-saturation.per.60.json'); 12 | const imgLightnessVal70 = require('./data/image-data-lightness.val.70.json') 13 | const imgLightnessPer60 = require('./data/image-data-lightness.per.60.json') 14 | const imgAlphaPer80 = require('./data/image-data-alpha.per.80.json') 15 | const imgPosterizeVal10 = require('./data/image-data-posterize.val.10.json'); 16 | const imgGammaVal16 = require('./data/image-data-gamma.val.16.json'); 17 | 18 | 19 | describe( 'test: Pictool.digit.process', ( ) => { 20 | 21 | it('process.hue({value: 180})', ( done ) => { 22 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 23 | const digitImgRs = process.hue(digitImg, {value: 180}); 24 | const expectImg = imgHueVal180; 25 | 26 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 27 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 28 | 29 | const rsData = digitImgRs.getData(); 30 | expectImg.data.forEach(function(num, i) { 31 | // console.log(`expect index is: ${i}`) 32 | expect(rsData[i]).to.deep.equal(num); 33 | }); 34 | 35 | done() 36 | }); 37 | 38 | it('process.hue({percent: 75})', ( done ) => { 39 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 40 | const digitImgRs = process.hue(digitImg, {percent: 75}); 41 | const expectImg = imgHuePer75; 42 | 43 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 44 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 45 | 46 | const rsData = digitImgRs.getData(); 47 | 48 | expectImg.data.forEach(function(num, i) { 49 | // console.log(`expect index is: ${i}`) 50 | expect(rsData[i]).to.deep.equal(num); 51 | }); 52 | 53 | done() 54 | }); 55 | 56 | it('process.saturation({value: 50})', ( done ) => { 57 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 58 | const digitImgRs = process.saturation(digitImg, {value: 50}); 59 | const expectImg = imgSaturationVal50; 60 | 61 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 62 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 63 | 64 | const rsData = digitImgRs.getData(); 65 | expectImg.data.forEach(function(num, i) { 66 | // console.log(`expect index is: ${i}`) 67 | expect(rsData[i]).to.deep.equal(num); 68 | }); 69 | 70 | done() 71 | }); 72 | 73 | it('process.saturation({percent: -60})', ( done ) => { 74 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 75 | const digitImgRs = process.saturation(digitImg, {percent: -60}); 76 | const expectImg = imgSaturationPer60; 77 | 78 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 79 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 80 | 81 | const rsData = digitImgRs.getData(); 82 | expectImg.data.forEach(function(num, i) { 83 | // console.log(`expect index is: ${i}`) 84 | expect(rsData[i]).to.deep.equal(num); 85 | }); 86 | 87 | done() 88 | }); 89 | 90 | it('process.lightness({value: 70})', ( done ) => { 91 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 92 | const digitImgRs = process.lightness(digitImg, {value: 70}); 93 | const expectImg = imgLightnessVal70; 94 | 95 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 96 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 97 | 98 | const rsData = digitImgRs.getData(); 99 | expectImg.data.forEach(function(num, i) { 100 | // console.log(`expect index is: ${i}`) 101 | expect(rsData[i]).to.deep.equal(num); 102 | }); 103 | 104 | done() 105 | }); 106 | 107 | 108 | it('process.lightness({percent: -60})', ( done ) => { 109 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 110 | const digitImgRs = process.lightness(digitImg, {percent: -60}); 111 | const expectImg = imgLightnessPer60; 112 | 113 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 114 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 115 | 116 | 117 | const rsData = digitImgRs.getData(); 118 | expectImg.data.forEach(function(num, i) { 119 | // console.log(`expect index is: ${i}`) 120 | expect(rsData[i]).to.deep.equal(num); 121 | }); 122 | 123 | done() 124 | }); 125 | 126 | 127 | it('process.alpha({percent: -80})', ( done ) => { 128 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 129 | const digitImgRs = process.alpha(digitImg, {percent: -80}); 130 | const expectImg = imgAlphaPer80; 131 | 132 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 133 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 134 | 135 | 136 | const rsData = digitImgRs.getData(); 137 | expectImg.data.forEach(function(num, i) { 138 | // console.log(`expect index is: ${i}`) 139 | expect(rsData[i]).to.deep.equal(num); 140 | }); 141 | 142 | done() 143 | }); 144 | 145 | it('process.posterize({value: 10})', ( done ) => { 146 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 147 | const digitImgRs = process.posterize(digitImg, {value: 10}); 148 | const expectImg = imgPosterizeVal10; 149 | 150 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 151 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 152 | 153 | 154 | const rsData = digitImgRs.getData(); 155 | expectImg.data.forEach(function(num, i) { 156 | // console.log(`expect index is: ${i}`) 157 | expect(rsData[i]).to.deep.equal(num); 158 | }); 159 | 160 | done() 161 | }); 162 | 163 | it('process.gamma({value: 16})', ( done ) => { 164 | const digitImg = new DigitImageData({width: img.width, height: img.height, data: img.data}); 165 | const digitImgRs = process.gamma(digitImg, {value: 16}); 166 | const expectImg = imgGammaVal16; 167 | 168 | expect(digitImgRs.getWidth()).to.deep.equal(expectImg.width); 169 | expect(digitImgRs.getHeight()).to.deep.equal(expectImg.height); 170 | 171 | 172 | const rsData = digitImgRs.getData(); 173 | expectImg.data.forEach(function(num, i) { 174 | // console.log(`expect index is: ${i}`) 175 | expect(rsData[i]).to.deep.equal(num); 176 | }); 177 | 178 | done() 179 | }); 180 | 181 | }) -------------------------------------------------------------------------------- /src/ui/module/dashboard/index.less: -------------------------------------------------------------------------------- 1 | .pictool-module-dashboard { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | font-size: 14px; 6 | color: #333333; 7 | 8 | .pictool-dashboard-navlist { 9 | position: absolute; 10 | height: 80px; 11 | display: flex; 12 | background: #ffffff; 13 | width: 100%; 14 | bottom: 0; 15 | } 16 | .pictool-dashboard-nav-btn { 17 | flex: 1; 18 | text-align: center; 19 | font-size: 14px; 20 | padding-top: 40px; 21 | position: relative; 22 | 23 | 24 | &.dashboard-process { 25 | &::before { 26 | content: ''; 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | right: 0; 31 | height: 40px; 32 | background: url('data:image/svg+xml;charset=utf-8,'); 33 | background-repeat: no-repeat; 34 | background-position: center bottom; 35 | background-size: 30px; 36 | } 37 | } 38 | 39 | &.dashboard-effect { 40 | &::before { 41 | content: ''; 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | right: 0; 46 | height: 40px; 47 | background: url('data:image/svg+xml;charset=utf-8,'); 48 | background-repeat: no-repeat; 49 | background-position: center bottom; 50 | background-size: 30px; 51 | } 52 | } 53 | 54 | &.dashboard-adjust { 55 | &::before { 56 | content: ''; 57 | position: absolute; 58 | top: 0; 59 | left: 0; 60 | right: 0; 61 | height: 40px; 62 | background: url('data:image/svg+xml;charset=utf-8,'); 63 | background-repeat: no-repeat; 64 | background-position: center bottom; 65 | background-size: 30px; 66 | } 67 | } 68 | 69 | &.dashboard-edit { 70 | &::before { 71 | content: ''; 72 | position: absolute; 73 | top: 0; 74 | left: 0; 75 | right: 0; 76 | height: 40px; 77 | background: url('data:image/svg+xml;charset=utf-8,'); 78 | background-repeat: no-repeat; 79 | background-position: center bottom; 80 | background-size: 30px; 81 | } 82 | } 83 | 84 | &.dashboard-text { 85 | &::before { 86 | content: ''; 87 | position: absolute; 88 | top: 0; 89 | left: 0; 90 | right: 0; 91 | height: 40px; 92 | background: url('data:image/svg+xml;charset=utf-8,'); 93 | background-repeat: no-repeat; 94 | background-position: center bottom; 95 | background-size: 30px; 96 | } 97 | } 98 | 99 | 100 | } 101 | 102 | 103 | 104 | } --------------------------------------------------------------------------------