├── .circleci └── config.yml ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── demo ├── activityBar.png ├── capture.gif ├── copy.gif ├── draw.gif ├── save.gif ├── screenify.gif ├── screenify1080.gif └── upload.gif ├── jsconfig.json ├── package-lock.json ├── package.json ├── resources ├── continue.png ├── github.png ├── heart.png ├── icon-big old1.png ├── icon-big old2.png ├── icon-big old4.png ├── icon-big.png ├── icon-big2.png ├── icon-bigold10.png ├── icon-bigold3.png ├── icon-bigold5.png ├── icon-bigold6.png ├── icon-bigold7.png ├── icon-bigold8.png ├── icon-bigold9.png ├── icon-heart.svg ├── icon.png ├── market-icon.png ├── new-icon.png ├── question.png └── twitter.svg ├── src ├── extension.js └── utils.js ├── test ├── runTest.js └── suite │ ├── extension.test.js │ └── index.js ├── vsc-extension-quickstart.md └── webview ├── assets ├── clear-symbol.png ├── copy.png ├── line.png ├── paint-brush.png ├── rounded-rectangle-stroked.png ├── undo.png └── upload-to-cloud.png ├── html2canvas.js ├── index.html ├── index.js ├── paint.js ├── pickr.js ├── pointer.js ├── styles.css └── vivus.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # version: 2.1 2 | # orbs: 3 | # node: circleci/node@1.1.6 4 | # jobs: 5 | # build-and-test: 6 | # executor: 7 | # name: node/default 8 | # steps: 9 | # - checkout 10 | # - node/with-cache: 11 | # steps: 12 | # - run: npm install 13 | # - test: 14 | version: 2 15 | jobs: 16 | build: 17 | docker: 18 | - image: circleci/node:10.15 19 | steps: 20 | - checkout 21 | - run: 22 | name: install-npm 23 | command: npm install 24 | 25 | - save_cache: 26 | key: dependency-cache-{{ checksum "package.json" }} 27 | paths: 28 | - ./node_modules 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | // "dom": true, 7 | "node": true, 8 | "mocha": true, 9 | "webextensions": true 10 | 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "sourceType": "module" 18 | }, 19 | "rules": { 20 | "no-const-assign": "off", 21 | "no-this-before-super": "off", 22 | "no-undef": "warn", 23 | "no-unreachable": "warn", 24 | "no-unused-vars": "warn", 25 | "constructor-super": "warn", 26 | "valid-typeof": "warn" 27 | } 28 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: adammomen 4 | open_collective: adam-momen 5 | ko_fi: # Replace with a single Ko-fi username 6 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 7 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 8 | liberapay: # Replace with a single Liberapay username 9 | issuehunt: # Replace with a single IssueHunt username 10 | otechie: # Replace with a single Otechie username 11 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | .vscode-test 3 | *.vsix 4 | .DS_Store 5 | npm-debug.log 6 | Thumbs.db 7 | */node_modules/ 8 | */out/ 9 | */.vs/ 10 | node_modules 11 | .vscode 12 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | vsc-extension-quickstart.md 6 | **/jsconfig.json 7 | **/*.map 8 | **/.eslintrc.json 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "screenify" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 | 5 | 6 |
7 | Screenify 8 |
9 |

10 | 11 |

Screenify your code

12 |

13 | 14 | CircleCI 15 | 16 | 17 | Version 18 | 19 | 20 | Installs 21 | 22 | 23 | Ratings 24 | 25 |

26 | 27 | ## Features 28 | 29 | - Quickly save screenshots of your code 30 | - Copy screenshots to your clipboard 31 | - Draw over your screenshot. 32 | - Upload your image online. 33 | 34 | ## Getting Started 35 | 36 | - Linux `Ctrl+Shift+S` 37 | - macOS `⌘Shift+S` 38 | - Windows `Ctrl+Shift+S` 39 | 40 | **Tips**: 41 | 42 | - You can also start secreenify by clicking on the camera icon 📸 on the statusbar. 43 | - Default key binding to start Screenify is `Ctrl+Shift+S` or `⌘Shift+S` If you'd like to bind screenify to another hotkey, open up your keyboard shortcut settings and bind `screenify.activate` to a custom keybinding. 44 | 45 | - If you'd like to copy to clipboard instead of saving, click the image and press the copy keyboard shortcut (defaults are Ctrl+C on Windows and Linux, Cmd+C on OS X). 46 | 47 | ## Examples 48 | 49 | [Nord](https://github.com/arcticicestudio/nord-visual-studio-code) + [Input Mono](http://input.fontbureau.com) 50 | 51 | ![demo1](https://raw.githubusercontent.com/octref/polacode/master/demo/1.png) 52 | 53 | [Monokai Pro](https://marketplace.visualstudio.com/items?itemName=monokai.theme-monokai-pro-vscode) + [Operator Mono](https://www.typography.com/blog/introducing-operator) 54 | 55 | ![demo2](https://raw.githubusercontent.com/octref/polacode/master/demo/2.png) 56 | 57 | [Material Theme Palenight](https://marketplace.visualstudio.com/items?itemName=Equinusocio.vsc-material-theme) + [Fira Code](https://github.com/tonsky/FiraCode) 58 | 59 | ![demo3](https://raw.githubusercontent.com/octref/polacode/master/demo/3.png) 60 | 61 | ## Demo 62 | 63 | ### *Share Your Code online* 64 | 65 | ![upload](./demo/upload.gif) 66 | 67 | Share your code snippets online, screenify uploads your images that you can share the image url with others. 68 | 69 | ### *Save your captured snippet on your local directory* 70 | 71 | ![!save](./demo/save.gif) 72 | 73 | Save your code as an image on your local machine directory. 74 | 75 | ## Known Issues 76 | 77 | >Note: drawing experiense is stil little bit laggy and not smooth and it's still in development. 78 | 79 | ## Tip 80 | 81 | - When running out of horizontal space, try the command `View: Toggle Editor Group Vertical/Horizontal Layout`. 82 | 83 | ## Credit 84 | 85 | Thanks to the great Polacode. 86 | 87 | Many color are taken from the elegant [Nord](https://github.com/arcticicestudio/nord) theme by [@arcticicestudio](https://github.com/arcticicestudio). 88 | 89 | Download button animation is made with [Vivus](https://github.com/maxwellito/vivus). 90 | 91 | Special Thanks to [SougCrypto](https://github.com/Soug-crypto) for helping out with desgin concepts of the MVP. 92 | 93 | ## Contributing 94 | 95 | Please, report issues/bugs and suggestions for improvements to the issue [here](https://github.com/screenify/screenify-vscode/issues). 96 | 97 | We're not users of Light versions so we need help to make light versions better. Please contribute if you have any suggestions. **PRs are welcomed!** :rocket: 98 | 99 | Copyright (C) 2020 by [AM](https://github.com/adammomen) 100 | ----------------------------------------------------------------------------------------------------------- 101 | ***Enjoy! Screenifying 📸*** 102 | -------------------------------------------------------------------------------- /demo/activityBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/activityBar.png -------------------------------------------------------------------------------- /demo/capture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/capture.gif -------------------------------------------------------------------------------- /demo/copy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/copy.gif -------------------------------------------------------------------------------- /demo/draw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/draw.gif -------------------------------------------------------------------------------- /demo/save.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/save.gif -------------------------------------------------------------------------------- /demo/screenify.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/screenify.gif -------------------------------------------------------------------------------- /demo/screenify1080.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/screenify1080.gif -------------------------------------------------------------------------------- /demo/upload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/demo/upload.gif -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ 5 | // "dom", "dom.iterable", 6 | "esnext", "es2016", "esnext.asynciterable" 7 | ], 8 | "target": "es6", 9 | "moduleResolution": "node", 10 | "checkJs": false, 11 | /* Typecheck .js files. */ 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "**/node_modules/*" 16 | ] 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screenify", 3 | "displayName": "Screenify", 4 | "description": "Capture your code snippets and turn them into canvas for drawing, then share them others by uploading the image to screenify CDN server.", 5 | "version": "0.0.4", 6 | "engines": { 7 | "vscode": "^1.42.0" 8 | }, 9 | "keywords": [ 10 | "screenshot", 11 | "snippet", 12 | "snap", 13 | "clipboard", 14 | "share", 15 | "polacode", 16 | "carbon", 17 | "codesnap", 18 | "upload" 19 | ], 20 | "categories": [ 21 | "Other" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/screenify/screenify-vscode.git" 26 | }, 27 | "publisher": "AdamMomen", 28 | "galleryBanner": { 29 | "color": "#fbfbfb", 30 | "theme": "light" 31 | }, 32 | "icon": "resources/icon-big.png", 33 | "activationEvents": [ 34 | "onCommand:screenify.activate", 35 | "onWebviewPanel:screenify", 36 | "onView:help" 37 | ], 38 | "main": "./src/extension", 39 | "contributes": { 40 | "viewsContainers": { 41 | "activitybar": [{ 42 | "id": "screenify", 43 | "title": "Screenify", 44 | "icon": "resources/icon-big.png" 45 | }] 46 | }, 47 | "views": { 48 | "screenify": [{ 49 | "id": "gettingStarted", 50 | "name": "Getting Started" 51 | }, 52 | { 53 | "id": "help", 54 | "name": "More" 55 | } 56 | ] 57 | }, 58 | "menu": { 59 | "view/item/context": [{ 60 | "command": "help.openUri", 61 | "when": "view == help && viewItem == openUrl", 62 | "group": "inline" 63 | }], 64 | "view/title": [{ 65 | "command": "screenify.supportScreenify", 66 | "when": "view == help", 67 | "group": "navigation" 68 | }] 69 | }, 70 | "viewsWelcome": [{ 71 | "view": "gettingStarted", 72 | "contents": "Click on 📸 to start using screenify:\n[📸](command:screenify.activate)" 73 | }], 74 | "commands": [{ 75 | "command": "screenify.activate", 76 | "title": "Screenify 📸" 77 | }, 78 | { 79 | "command": "help.openUri", 80 | "title": "Open Uri ❤" 81 | } 82 | ], 83 | "keybindings": [{ 84 | "command": "screenify.activate", 85 | "key": "ctrl+shift+s", 86 | "mac": "cmd+shift+s" 87 | }], 88 | "configuration": { 89 | "title": "Screenify", 90 | "properties": { 91 | "screenify.shadow": { 92 | "type": "string", 93 | "description": "Shadow of the snippet node. Use any value for CSS `box-shadow`", 94 | "default": "rgba(0, 0, 0, 0.55) 0px 20px 68px" 95 | }, 96 | "screenify.transparentBackground": { 97 | "type": "boolean", 98 | "description": "Transparent background for containers", 99 | "default": false 100 | }, 101 | "screenify.backgroundColor": { 102 | "type": "string", 103 | "description": "Background color of snippet container. Use any value for CSS `background-color`", 104 | "format": "color-hex", 105 | "default": "#f2f2f2" 106 | }, 107 | "screenify.target": { 108 | "type": "string", 109 | "description": "Shoot with or without container", 110 | "default": "container", 111 | "enum": [ 112 | "container", 113 | "snippet" 114 | ], 115 | "enumDescriptions": [ 116 | "Shoot with the container.", 117 | "Shoot with the snippet alone. If you want transparent padding, use `container` with `\"screenify.transparentBackground\": true`" 118 | ] 119 | }, 120 | "screenify.serverUrl": { 121 | "type": "string", 122 | "description": "`", 123 | "default": "screenify-njy7ok457q-ez.a.run.app" 124 | } 125 | } 126 | } 127 | }, 128 | "scripts": { 129 | "lint": "eslint .", 130 | "pretest": "npm run lint", 131 | "test": "node ./test/runTest.js" 132 | }, 133 | "devDependencies": { 134 | "@types/glob": "^7.1.1", 135 | "@types/mocha": "^7.0.1", 136 | "@types/node": "^12.11.7", 137 | "@types/vscode": "^1.42.0", 138 | "electron": "file:node_modules/electron", 139 | "eslint": "*", 140 | "glob": "^7.1.6", 141 | "mocha": "^7.0.1", 142 | "typescript": "^3.7.5", 143 | "vscode-test": "^1.3.0" 144 | }, 145 | "dependencies": { 146 | "bluebird": "^3.7.2", 147 | "fs": "0.0.1-security", 148 | "img-clipboard": "^1.0.4", 149 | "node-fetch": "^3.1.1", 150 | "node-powershell": "^4.0.0", 151 | "os": "^0.1.1", 152 | "path": "^0.12.7", 153 | "randomstring": "^1.1.5" 154 | } 155 | } -------------------------------------------------------------------------------- /resources/continue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/continue.png -------------------------------------------------------------------------------- /resources/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/github.png -------------------------------------------------------------------------------- /resources/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/heart.png -------------------------------------------------------------------------------- /resources/icon-big old1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-big old1.png -------------------------------------------------------------------------------- /resources/icon-big old2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-big old2.png -------------------------------------------------------------------------------- /resources/icon-big old4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-big old4.png -------------------------------------------------------------------------------- /resources/icon-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-big.png -------------------------------------------------------------------------------- /resources/icon-big2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-big2.png -------------------------------------------------------------------------------- /resources/icon-bigold10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-bigold10.png -------------------------------------------------------------------------------- /resources/icon-bigold3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-bigold3.png -------------------------------------------------------------------------------- /resources/icon-bigold5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-bigold5.png -------------------------------------------------------------------------------- /resources/icon-bigold6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-bigold6.png -------------------------------------------------------------------------------- /resources/icon-bigold7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-bigold7.png -------------------------------------------------------------------------------- /resources/icon-bigold8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-bigold8.png -------------------------------------------------------------------------------- /resources/icon-bigold9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon-bigold9.png -------------------------------------------------------------------------------- /resources/icon-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/icon.png -------------------------------------------------------------------------------- /resources/market-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/market-icon.png -------------------------------------------------------------------------------- /resources/new-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/new-icon.png -------------------------------------------------------------------------------- /resources/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/resources/question.png -------------------------------------------------------------------------------- /resources/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright(c) Screenify📸. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | const vscode = require("vscode"), 6 | fs = require("fs"), 7 | path = require("path"), 8 | os = require("os"), 9 | P_TITLE = "Screenify 📸", 10 | fetch = require("node-fetch"), 11 | Bluebird = require("bluebird"), 12 | { 13 | copyImg, 14 | } = require("img-clipboard"), 15 | { 16 | readHtml, 17 | } = require("./utils"); 18 | fetch.Promise = Bluebird; 19 | 20 | /** 21 | * @param {vscode.ExtensionContext} context 22 | * Extension Acativation 23 | **/ 24 | function activate(context) { 25 | const { 26 | subscriptions, 27 | } = context; 28 | 29 | /** Status Bar configuration **/ 30 | statusBarItem = vscode.window.createStatusBarItem( 31 | vscode.StatusBarAlignment.Right, 32 | 100, 33 | ); 34 | statusBarItem.command = "screenify.activate"; 35 | statusBarItem.text = `$(device-camera) Screenify`; 36 | statusBarItem.tooltip = "Capture Code Snippet"; 37 | statusBarItem.show(); 38 | subscriptions.push(statusBarItem); 39 | 40 | /** @class HelpDataProvider 41 | * Tree institation for view container tree items 42 | **/ 43 | class HelpDataProvider { 44 | constructor() { 45 | this.data = [ 46 | new TreeItem( 47 | "Give me your feedback", 48 | "twitter.svg", 49 | "https://twitter.com/adammuman81", 50 | ), 51 | new TreeItem( 52 | "Report an issue", 53 | "github.png", 54 | "https://github.com/AdamMomen/screenify-vscode/issues", 55 | ), 56 | new TreeItem( 57 | "Support", 58 | "icon-heart.svg", 59 | "https://www.patreon.com/adammomen", 60 | ), 61 | ]; 62 | } 63 | getTreeItem(element) { 64 | return element; 65 | } 66 | 67 | getChildren(element = undefined) { 68 | if (element === undefined) { 69 | return this.data; 70 | } 71 | return element.children; 72 | } 73 | } 74 | 75 | /** @class GettingStartedDataProvider 76 | * Tree institation for view container tree items 77 | **/ 78 | class GettingStartedDataProvider { 79 | constructor() { 80 | this.data = [ 81 | new TreeItem("Start Screenify 📸", "", "", { 82 | title: "Start Screenify", 83 | command: "screenify.activate", 84 | context: "start", 85 | }), 86 | ]; 87 | } 88 | getTreeItem(element) { 89 | return element; 90 | } 91 | 92 | getChildren(element = undefined) { 93 | if (element === undefined) { 94 | return this.data; 95 | } 96 | return element.children; 97 | } 98 | } 99 | class TreeItem extends vscode.TreeItem { 100 | constructor(label, icon, uri, cmd = { 101 | title: "Open Uri", 102 | command: "help.openUri", 103 | context: "openUrl", 104 | }) { 105 | super(label, vscode.TreeItemCollapsibleState.None); 106 | this.iconPath = path.join(context.extensionPath, "resources", icon); 107 | this.command = { 108 | title: cmd.title, 109 | command: cmd.command, 110 | arguments: [uri], 111 | }; 112 | this._uri = uri; 113 | this.contextValue = cmd.context; 114 | } 115 | } 116 | /** Register Tree Data provider **/ 117 | vscode.window.registerTreeDataProvider("help", new HelpDataProvider()); 118 | 119 | /** Register Tree Data provider **/ 120 | vscode.window.registerTreeDataProvider( 121 | "gettingStarted", 122 | new GettingStartedDataProvider(), 123 | ); 124 | 125 | /** Register command**/ 126 | vscode.commands.registerCommand("help.openUri", (node) => { 127 | vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(node)); 128 | }); 129 | 130 | /** Path to Html file **/ 131 | const htmlPath = path.resolve(context.extensionPath, "webview/index.html"); 132 | 133 | /** Path to the last saved image **/ 134 | let lastUsedImageUri = vscode.Uri.file( 135 | path.resolve(os.homedir(), "Desktop/code.png"), 136 | ); 137 | let panel; 138 | 139 | /** Regiseter Webview Pannl Serializer **/ 140 | vscode.window.registerWebviewPanelSerializer( 141 | "screenify", { 142 | async deserializeWebviewPanel(_panel, state) { 143 | panel = _panel; 144 | panel.webview.html = await readHtml(htmlPath, _panel); 145 | panel.webview.postMessage({ 146 | type: "restore", 147 | innerHTML: state.innerHTML, 148 | bgColor: context.globalState.get("screenify.bgColor", "#2e3440"), 149 | }); 150 | const selectionListener = setupSelectionSync(); 151 | panel.onDidDispose(() => { 152 | selectionListener.dispose(); 153 | }); 154 | setupMessageListeners(); 155 | }, 156 | }, 157 | ); 158 | 159 | /** Regiseter Screenify Acitivation Command **/ 160 | vscode.commands.registerCommand("screenify.activate", async() => { 161 | /** Show welcome inforamation message **/ 162 | vscode.window.showInformationMessage( 163 | "Screenify is enabled and running, happy shooting 📸 😊 ", 164 | ); 165 | 166 | /** Creates Webview Panel **/ 167 | panel = vscode.window.createWebviewPanel("screenify", P_TITLE, 2, { 168 | enableScripts: true, 169 | localResourceRoots: [ 170 | vscode.Uri.file(path.join(context.extensionPath, "webview")), 171 | ], 172 | }); 173 | 174 | /** Set webview Html content from the html path file **/ 175 | panel.webview.html = await readHtml(htmlPath, panel); 176 | 177 | /** Selcetion Listener **/ 178 | const selectionListener = setupSelectionSync(); 179 | panel.onDidDispose(() => { 180 | selectionListener.dispose(); 181 | }); 182 | 183 | setupMessageListeners(); 184 | 185 | /** Get fontFamily from the editor configuration **/ 186 | const fontFamily = vscode.workspace.getConfiguration("editor").fontFamily; 187 | 188 | /** Get bgColor and if NULL set it to #2e3440 **/ 189 | const bgColor = context.globalState.get("screenify.bgColor", "#2e3440"); 190 | 191 | /** Post resquest to webview **/ 192 | panel.webview.postMessage({ 193 | type: "init", 194 | fontFamily, 195 | bgColor, 196 | }); 197 | syncSettings(); 198 | }); 199 | 200 | /** Syncs Updates **/ 201 | vscode.workspace.onDidChangeConfiguration((e) => { 202 | if ( 203 | e.affectsConfiguration("screenify") || e.affectsConfiguration("editor") 204 | ) { 205 | syncSettings(); 206 | } 207 | }); 208 | 209 | /** 210 | * Copyies serial blob to the clipboard or uploads the blob to CDN uploaders 211 | * @param {Blob} serializedBlobHandler 212 | * @return {Promise} 213 | */ 214 | function serializedBlobHandler(serializeBlob, isUpload) { 215 | /** if blob is undefined */ 216 | if (!serializeBlob) return; 217 | 218 | /** Convert Serialize Blob to array of butes **/ 219 | const bytes = new Uint8Array(serializeBlob.split(",")); 220 | 221 | /** uploads state is true, then uploads the blob **/ 222 | if (isUpload) return upload(serializeBlob); 223 | 224 | /** else it will copy the blob to clipboard **/ 225 | return copyImg(Buffer.from(bytes)); 226 | } 227 | 228 | /** 229 | * Saves blob to into file 230 | * @param {Blob} serializeBlob 231 | * @return {Promise} 232 | */ 233 | function writeSerializedBlobToFile(serializeBlob, fileName) { 234 | /** Convert Serialize Blob to array of butes **/ 235 | const bytes = new Uint8Array(serializeBlob.split(",")); 236 | 237 | /** write buffer into file **/ 238 | fs.writeFileSync(fileName, Buffer.from(bytes)); 239 | } 240 | 241 | function setupMessageListeners() { 242 | panel.webview.onDidReceiveMessage(({ 243 | type, 244 | data, 245 | }) => { 246 | switch (type) { 247 | /** Save the image locally **/ 248 | case "shoot": 249 | vscode.window 250 | .showSaveDialog({ 251 | defaultUri: lastUsedImageUri, 252 | filters: { 253 | Images: ["png"], 254 | }, 255 | }) 256 | .then((uri) => { 257 | if (uri) { 258 | writeSerializedBlobToFile(data.serializedBlob, uri.fsPath); 259 | vscode.window.showInformationMessage("Snippet saved ✅"); 260 | lastUsedImageUri = uri; 261 | } 262 | }); 263 | break; 264 | 265 | /** Copy image to the clipboard **/ 266 | 267 | case "copy": 268 | serializedBlobHandler(data.serializedBlob, data.upload) 269 | .then(() => { 270 | vscode.window.showInformationMessage( 271 | "Snippet copied! 📋 ctrl + V to paste", 272 | "Close", 273 | ); 274 | }) 275 | .catch((err) => { 276 | vscode.window.showErrorMessage( 277 | `Ops! Something went wrong! ❌: ${err}`, 278 | "Close", 279 | ); 280 | }); 281 | break; 282 | 283 | /** Updates Cache Settings **/ 284 | 285 | case "getAndUpdateCacheAndSettings": 286 | panel.webview.postMessage({ 287 | type: "restoreBgColor", 288 | bgColor: context.globalState.get("screenify.bgColor", "#2e3440"), 289 | }); 290 | 291 | syncSettings(); 292 | break; 293 | case "updateBgColor": 294 | context.globalState.update("screenify.bgColor", data.bgColor); 295 | break; 296 | case "invalidPasteContent": 297 | vscode.window.showInformationMessage( 298 | "Pasted content is invalid. Only copy from VS Code and check if your shortcuts for copy/paste have conflicts.", 299 | ); 300 | break; 301 | } 302 | }); 303 | } 304 | 305 | function syncSettings() { 306 | const settings = vscode.workspace.getConfiguration("screenify"); 307 | const editorSettings = vscode.workspace.getConfiguration("editor", null); 308 | panel.webview.postMessage({ 309 | type: "updateSettings", 310 | shadow: settings.get("shadow"), 311 | transparentBackground: settings.get("transparentBackground"), 312 | backgroundColor: settings.get("backgroundColor"), 313 | target: settings.get("target"), 314 | ligature: editorSettings.get("fontLigatures"), 315 | }); 316 | } 317 | 318 | function setupSelectionSync() { 319 | return vscode.window.onDidChangeTextEditorSelection((e) => { 320 | if (e.selections[0] && !e.selections[0].isEmpty) { 321 | vscode.commands.executeCommand( 322 | "editor.action.clipboardCopyWithSyntaxHighlightingAction", 323 | ); 324 | panel.postMessage({ 325 | type: "update", 326 | }); 327 | } 328 | }); 329 | } 330 | 331 | /** 332 | * @function upload 333 | * @param {Buffer} image 334 | * @return {Promise} 335 | * Sends Http requset to screenify backend API to upload the image online. 336 | */ 337 | function upload(buffer) { 338 | /** Server Url **/ 339 | let serverUrl = `https://${settings.get("serverUrl")}/api/upload`; 340 | 341 | /** Show a progress loader... **/ 342 | vscode.window.withProgress({ 343 | location: 15, 344 | title: "Uploading Image...", 345 | }, (progress, token) => { 346 | token.onCancellationRequested(() => { 347 | return; 348 | }); 349 | 350 | /** Sending POST requset send with image buffer as the body **/ 351 | return fetch(serverUrl, { 352 | method: "POST", 353 | body: JSON.stringify({ 354 | buffer, 355 | }), 356 | headers: { 357 | "Content-Type": "application/json", 358 | }, 359 | }); 360 | }) 361 | /** convert the response into JSON **/ 362 | .then((res) => res.json()) 363 | .then((response) => { 364 | const { 365 | url, 366 | } = response; 367 | 368 | /** Copy url to the clipboard **/ 369 | vscode.env.clipboard.writeText(url) 370 | .then(() => { 371 | /** Sends post message with the uploaded image url to webview api **/ 372 | panel.webview.postMessage({ 373 | type: "successfulUplaod", 374 | url, 375 | }); 376 | 377 | /** Sucessful Upload info message **/ 378 | vscode.window.showInformationMessage( 379 | `Snippet uploaded! ✅ Url is copied to the clipboard 📋: `, 380 | url, 381 | "Copy", 382 | ); 383 | }); 384 | }) 385 | .catch((err) => { 386 | /** Error message **/ 387 | vscode.window.showErrorMessage( 388 | `Ops! Something went wrong! ❌: ${err}`, 389 | "Close", 390 | ); 391 | }); 392 | } 393 | } 394 | 395 | /** Get Screenify settings **/ 396 | const settings = vscode.workspace.getConfiguration("screenify"); 397 | 398 | /** Extension Decativation **/ 399 | function deactivate() { 400 | // TODO:complete implementing extension deactivation routine 401 | // #1 Clear cache 402 | // #2 Garbage collection 403 | } 404 | 405 | exports.activate = activate; 406 | exports.deactivate = deactivate; 407 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vscode = require('vscode'); 4 | const path = require('path'); 5 | const { 6 | readFile, 7 | writeFile 8 | } = require('fs').promises; 9 | 10 | const readHtml = async (htmlPath, panel) => 11 | (await readFile(htmlPath, 'utf-8')) 12 | .replace(/%CSP_SOURCE%/gu, panel.webview.cspSource) 13 | .replace( 14 | /(src|href)="([^"]*)"/gu, 15 | (_, type, src) => 16 | `${type}="${panel.webview.asWebviewUri( 17 | vscode.Uri.file(path.resolve(htmlPath, '..', src)) 18 | )}"` 19 | ); 20 | 21 | const getSettings = (group, keys) => { 22 | const settings = vscode.workspace.getConfiguration(group, null); 23 | const editor = vscode.window.activeTextEditor; 24 | const language = editor && editor.document && editor.document.languageId; 25 | const languageSettings = 26 | language && vscode.workspace.getConfiguration(null, null).get(`[${language}]`); 27 | return keys.reduce((acc, k) => { 28 | acc[k] = languageSettings && languageSettings[`${group}.${k}`]; 29 | if (acc[k] == null) acc[k] = settings.get(k); 30 | return acc; 31 | }, {}); 32 | }; 33 | 34 | module.exports = { 35 | readHtml, 36 | writeFile, 37 | getSettings 38 | }; -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { 4 | runTests 5 | } = require('vscode-test'); 6 | 7 | async function main() { 8 | try { 9 | // The folder containing the Extension Manifest package.json 10 | // Passed to `--extensionDevelopmentPath` 11 | const extensionDevelopmentPath = path.resolve(__dirname, '../'); 12 | 13 | // The path to the extension test script 14 | // Passed to --extensionTestsPath 15 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 16 | 17 | // Download VS Code, unzip it and run the integration test 18 | await runTests({ 19 | extensionDevelopmentPath, 20 | extensionTestsPath 21 | }); 22 | } catch (err) { 23 | console.error('Failed to run tests'); 24 | process.exit(1); 25 | } 26 | } 27 | 28 | main(); -------------------------------------------------------------------------------- /test/suite/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | const vscode = require('vscode'); 6 | // const screenify = require("../../src/extension") 7 | // const { 8 | // shootSnippet 9 | // } = require("../../webview/index") 10 | suite('Extension Test Suite', () => { 11 | vscode.window.showInformationMessage('Start all tests.'); 12 | 13 | test("should be present", () => { 14 | assert.ok(vscode.extensions.getExtension("adammomen.screenify")); 15 | }); 16 | test("should be able to register screenify commands", () => { 17 | return vscode.commands.getCommands(true).then((commands) => { 18 | const SCREENIFY_COMMANDS = [ 19 | "editor.action.clipboardCopyWithSyntaxHighlightingAction", 20 | "getAndUpdateCacheAndSettings.action.getAndUpdateCacheAndSettings" 21 | 22 | ] 23 | const foundScreenifyCommands = commands.filter((value) => { 24 | return SCREENIFY_COMMANDS.indexOf(value) >= 0 || value.startsWith("screenify."); 25 | }); 26 | const errorMsg = "Some screenify commands are not registered properly or a new command is not added to the test"; 27 | assert.equal(foundScreenifyCommands.length, SCREENIFY_COMMANDS.length, 28 | errorMsg 29 | ); 30 | }); 31 | }) 32 | }) -------------------------------------------------------------------------------- /test/suite/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Mocha = require('mocha'); 3 | const glob = require('glob'); 4 | 5 | function run() { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | // Use any mocha API 11 | mocha.useColors(true); 12 | 13 | const testsRoot = path.resolve(__dirname, '..'); 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { 17 | cwd: testsRoot 18 | }, (err, files) => { 19 | if (err) { 20 | return e(err); 21 | } 22 | 23 | // Add files to the test suite 24 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 25 | 26 | try { 27 | // Run the mocha test 28 | mocha.run(failures => { 29 | if (failures > 0) { 30 | e(new Error(`${failures} tests failed.`)); 31 | } else { 32 | c(); 33 | } 34 | }); 35 | } catch (err) { 36 | console.error(err); 37 | e(err); 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | module.exports = { 44 | run 45 | }; -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `extension.js` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `extension.js` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `extension.js`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 31 | * Press `F5` to run the tests in a new window with your extension loaded. 32 | * See the output of the test result in the debug console. 33 | * Make changes to `src/test/suite/extension.test.js` or create new test files inside the `test/suite` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | ## Go further 37 | 38 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 39 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 40 | -------------------------------------------------------------------------------- /webview/assets/clear-symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/webview/assets/clear-symbol.png -------------------------------------------------------------------------------- /webview/assets/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/webview/assets/copy.png -------------------------------------------------------------------------------- /webview/assets/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/webview/assets/line.png -------------------------------------------------------------------------------- /webview/assets/paint-brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/webview/assets/paint-brush.png -------------------------------------------------------------------------------- /webview/assets/rounded-rectangle-stroked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/webview/assets/rounded-rectangle-stroked.png -------------------------------------------------------------------------------- /webview/assets/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/webview/assets/undo.png -------------------------------------------------------------------------------- /webview/assets/upload-to-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/screenify/screenify-vscode/4d20a05636d504b445d090ef18c9c26cac023183/webview/assets/upload-to-cloud.png -------------------------------------------------------------------------------- /webview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | brush 21 | line 22 |  rectangle 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 |
34 | 35 |
36 | 37 |
38 |

...

x 39 |

...

40 |
41 |
42 | 43 | 44 |
45 | 52 | 53 |
55 |
56 | console.log('0. Run command `Screenify 📸 ` 57 | ') 58 |
59 |
60 | console.log('1. Copy some code 61 | ') 62 |
63 |
64 | console.log('2. Paste into Screenify view 65 | ') 66 |
67 |
68 | console.log('3. Click the button 📸 69 | ') 70 |
71 | 72 |
73 | 74 |
75 | 76 | 77 |
78 |
79 | 98 | SNAP ! 99 |
100 |
101 |
102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /webview/index.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Screenify. 3 | * Licensed under the MIT License 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | window.onload = function() { 7 | (function() { 8 | 9 | /** PointerJs initialization on page launch with init color of @Pickr color picker **/ 10 | init_pointer({ 11 | pointerColor: "#42445A" 12 | }) 13 | 14 | /** Snippet Container Background Color */ 15 | let backgroundColor = "#f2f2f2"; 16 | 17 | 18 | /** vscode-api **/ 19 | const vscode = acquireVsCodeApi(), 20 | oldState = vscode.getState(), 21 | 22 | /** Main Snippet Container **/ 23 | snippetContainerNode = document.getElementById("snippet-container"), 24 | 25 | /** Snippet **/ 26 | snippetNode = document.getElementById("snippet"), 27 | 28 | /** Snap Button **/ 29 | obturateurLogo = document.getElementById("save_logo"), 30 | 31 | /** Drawing Canvas **/ 32 | canvas = document.getElementById('my-canvas'), 33 | 34 | /** Canvas Context **/ 35 | ctx = canvas.getContext('2d'), 36 | 37 | /** Brush icon Tool **/ 38 | brush = document.getElementById("brush"), 39 | 40 | /** Line Tool **/ 41 | line = document.getElementById("line"), 42 | 43 | /** Rectangle Tool **/ 44 | rectangle = document.getElementById("rectangle"), 45 | 46 | /** Snippet Height Text **/ 47 | snippetHeight = document.getElementById("snippetHeight"), 48 | 49 | /** Snippet Width Text **/ 50 | snippetWidth = document.getElementById("snippetWidth"), 51 | 52 | /** Undo Tool **/ 53 | undo = document.getElementById("undo"), 54 | 55 | /** Copy Tool **/ 56 | copyBtn = document.getElementById("copy"), 57 | /** Upload Tool **/ 58 | upload = document.getElementById("upload"), 59 | 60 | /** Uploaded Url Container **/ 61 | uploadedUrlContainer = document.getElementById("upload-container"), 62 | 63 | /** clear tool **/ 64 | clear = document.getElementById("clear"), 65 | flashFx = document.getElementById("flash-fx"); 66 | 67 | const cameraFlashAnimation = async() => { 68 | flashFx.style.display = 'block'; 69 | redraw(flashFx); 70 | flashFx.style.opacity = '0'; 71 | await once(flashFx, 'transitionend'); 72 | flashFx.style.display = 'none'; 73 | flashFx.style.opacity = '1'; 74 | }; 75 | const redraw = node => node.clientHeight; 76 | const once = (elem, evt) => 77 | new Promise(done => elem.addEventListener(evt, done, { 78 | once: true 79 | })); 80 | 81 | /** Changing toolbar color to different color 82 | * @Note TODO: Update toolbar color to vscode color theme. **/ 83 | document.getElementsByClassName("toolbar")[0].style.backgroundColor = "#362b1b"; 84 | 85 | /** Post a message to vscode api, update cache and settings. **/ 86 | vscode.postMessage({ 87 | type: "getAndUpdateCacheAndSettings" 88 | }); 89 | 90 | /** Set @SnippetContainer node opacity to 1 **/ 91 | snippetContainerNode.style.opacity = "1"; 92 | if (oldState && oldState.innerHTML) { 93 | snippetNode.innerHTML = oldState.innerHTML; 94 | } 95 | 96 | /** 97 | * @function getInitialHtml 98 | * @param {String} fontFamily 99 | * Setup Custom Html with to vscode webview interface wtih font family. 100 | **/ 101 | const getInitialHtml = fontFamily => { 102 | const cameraWithFlashEmoji = String.fromCodePoint(128248); 103 | const monoFontStack = `${fontFamily},SFMono-Regular,Consolas,DejaVu Sans Mono,Ubuntu Mono,Liberation Mono,Menlo,Courier,monospace`; 104 | return `
console.log('0. Run command \`Screenify ${cameraWithFlashEmoji}\`')
console.log('1. Copy some code')
console.log('2. Paste into Screenify view')
console.log('3. Click the button ${cameraWithFlashEmoji}')
`; 105 | }; 106 | 107 | /** 108 | * @function serializeBlob 109 | * @param {Blob} blob 110 | * @param {CallBack} cb 111 | * Converts a blob into serialized blob. 112 | **/ 113 | const serializeBlob = (blob, cb) => { 114 | const fileReader = new FileReader(); 115 | 116 | fileReader.onload = () => { 117 | const bytes = new Uint8Array(fileReader.result); 118 | cb(Array.from(bytes).join(",")); 119 | }; 120 | fileReader.readAsArrayBuffer(blob); 121 | }; 122 | 123 | /** 124 | * @function shoot 125 | * @param {Blob} serializedBlob 126 | * Sends serializedBlob as post request to vscode api to save the blob locally. 127 | **/ 128 | function shoot(serializedBlob) { 129 | cameraFlashAnimation() 130 | vscode.postMessage({ 131 | type: "shoot", 132 | data: { 133 | serializedBlob 134 | } 135 | }); 136 | } 137 | 138 | /** 139 | * @function copy 140 | * @param {Blob} serializedBlob 141 | * @param {Boolean} upload 142 | * Sends serializedBlob as post request to vscode api to either copy the blob to clipboard or updload the blob to online CDN. 143 | **/ 144 | function copy(serializedBlob, upload = false) { 145 | cameraFlashAnimation() 146 | vscode.postMessage({ 147 | type: "copy", 148 | data: { 149 | "serializedBlob": serializedBlob, 150 | "upload": upload, 151 | } 152 | }); 153 | } 154 | 155 | /** 156 | * @function getBrightness 157 | * @param {String} hexColor 158 | * Converts hex color value into rgb value. 159 | **/ 160 | function getBrightness(hexColor) { 161 | const rgb = parseInt(hexColor.slice(1), 16); 162 | const r = (rgb >> 16) & 0xff; 163 | const g = (rgb >> 8) & 0xff; 164 | const b = (rgb >> 0) & 0xff; 165 | return (r * 299 + g * 587 + b * 114) / 1000; 166 | } 167 | 168 | /** 169 | * @function isDark 170 | * @param {String} hexColor 171 | * checks if the color is dark. 172 | **/ 173 | function isDark(hexColor) { 174 | return getBrightness(hexColor) < 128; 175 | } 176 | 177 | /** 178 | * @function getSnippetBgColor 179 | * @param {String} html 180 | * Gets snippet color from html string. 181 | **/ 182 | function getSnippetBgColor(html) { 183 | const match = html.match(/background-color: (#[a-fA-F0-9]+)/); 184 | return match ? match[1] : undefined; 185 | } 186 | 187 | /** 188 | * @function updateEnvironment 189 | * @param {String} snippetBgColor 190 | * Updates the snippet background color 191 | **/ 192 | function updateEnvironment(snippetBgColor) { 193 | 194 | /** update snippet bg color **/ 195 | document.getElementById("snippet").style.backgroundColor = snippetBgColor; 196 | 197 | /** update backdrop color **/ 198 | if (isDark(snippetBgColor)) { 199 | 200 | /** set background colorof snippet container to white #f2f2f2 **/ 201 | snippetContainerNode.style.backgroundColor = "#f2f2f2"; 202 | } else { 203 | 204 | /** set to none **/ 205 | snippetContainerNode.style.background = "none"; 206 | } 207 | } 208 | 209 | function getMinIndent(code) { 210 | const arr = code.split("\n"); 211 | 212 | let minIndentCount = Number.MAX_VALUE; 213 | for (let i = 0; i < arr.length; i++) { 214 | const wsCount = arr[i].search(/\S/); 215 | if (wsCount !== -1) { 216 | if (wsCount < minIndentCount) { 217 | minIndentCount = wsCount; 218 | } 219 | } 220 | } 221 | 222 | return minIndentCount; 223 | } 224 | 225 | function stripInitialIndent(html, indent) { 226 | const doc = new DOMParser().parseFromString(html, "text/html"); 227 | const initialSpans = doc.querySelectorAll("div > div span:first-child"); 228 | for (let i = 0; i < initialSpans.length; i++) { 229 | initialSpans[i].textContent = initialSpans[i].textContent.slice(indent); 230 | } 231 | return doc.body.innerHTML; 232 | } 233 | /** On paste event, of user code captured in the snippet container **/ 234 | document.addEventListener("paste", e => { 235 | 236 | /** clear the canvas on new incoming code snippet **/ 237 | ctx.clearRect(0, 0, canvas.width, canvas.height); 238 | const innerHTML = e.clipboardData.getData("text/html"); 239 | 240 | const code = e.clipboardData.getData("text/plain"); 241 | const minIndent = getMinIndent(code); 242 | 243 | const snippetBgColor = getSnippetBgColor(innerHTML); 244 | if (snippetBgColor) { 245 | vscode.postMessage({ 246 | type: "updateBgColor", 247 | data: { 248 | bgColor: snippetBgColor 249 | } 250 | }); 251 | updateEnvironment(snippetBgColor); 252 | 253 | } 254 | 255 | if (minIndent !== 0) { 256 | snippetNode.innerHTML = stripInitialIndent(innerHTML, minIndent); 257 | } else { 258 | snippetNode.innerHTML = innerHTML; 259 | } 260 | 261 | vscode.setState({ 262 | innerHTML 263 | }); 264 | }); 265 | 266 | /** Brush tool On Click Event Listener **/ 267 | brush.addEventListener("click", () => { 268 | changeTool("brush") 269 | }) 270 | 271 | /** Line tool On Click Event Listener **/ 272 | line.addEventListener("click", () => { 273 | changeTool("line") 274 | }) 275 | 276 | /** Rectangle tool On Click Event Listener **/ 277 | rectangle.addEventListener("click", () => { 278 | changeTool("rectangle") 279 | }) 280 | /** Undo tool On Click Event Listener **/ 281 | undo.addEventListener("click", () => { 282 | restoreState() 283 | }) 284 | /** CopyBtn tool On Click Event Listener **/ 285 | copyBtn.addEventListener("click", () => { 286 | copyImage() 287 | }) 288 | 289 | /** Upload tool On Click Event Listener **/ 290 | upload.addEventListener("click", () => { 291 | uploadImage() 292 | }) 293 | 294 | /** Clear tool On Click Event Listener **/ 295 | clear.addEventListener("click", () => { 296 | ctx.clearRect(0, 0, canvas.width, canvas.height); 297 | // clear the undo array 298 | undo_array = [] 299 | brushPoints = [] 300 | currentState = 0; 301 | saveCanvasImage() 302 | redrawCanvasImage() 303 | }) 304 | 305 | /** Snap button on click Event Listener **/ 306 | obturateurLogo.addEventListener("click", () => { 307 | snippetHandler(); 308 | }) 309 | 310 | /** Snippet on Resize Event Observer **/ 311 | const ro = new ResizeObserver((entries) => { 312 | for (let entry of entries) { 313 | 314 | /** Content Rectangular dimentions **/ 315 | const cr = entry.contentRect; 316 | reactToContainerResize(cr.width, cr.height) 317 | } 318 | }); 319 | 320 | /** Observe one or multiple elements **/ 321 | ro.observe(snippetNode); 322 | 323 | 324 | /** 325 | * @function reactToContainerResize 326 | * @param {Number} width 327 | * @param {Number} height 328 | * Updates the height and width of the snippet on the dom body and also updates the canvas height and width. 329 | **/ 330 | function reactToContainerResize(width, height) { 331 | 332 | /** HeightX Width conrdinates Update of the continer **/ 333 | snippetHeight.innerText = Math.floor(new Number(height)) 334 | snippetWidth.innerText = Math.floor(new Number(width)) 335 | 336 | /** @NOTE The following Saving and redrawing canvas appraoch is expensive on the memroy, a better design has to be implemented! **/ 337 | 338 | /** Save the canvas before update the size **/ 339 | saveCanvasImage() 340 | 341 | /** Update canvas height and width with continer with 20 as margin **/ 342 | canvasHeight = canvas.height = height + 20; 343 | canvasWidth = canvas.width = width + 20; 344 | 345 | /** redraw the image **/ 346 | redrawCanvasImage() 347 | 348 | /** Save the canvas again! **/ 349 | saveCanvasImage(); 350 | 351 | /** Redraw the canvas again! **/ 352 | redrawCanvasImage() 353 | } 354 | 355 | /** 356 | * @function html2blob 357 | * @returns {Promise} 358 | * An abstract function that calls @html2Canvas function as a promise, which convers the canvas to blob. 359 | **/ 360 | function html2blob() { 361 | 362 | /** Multiping the container height and width by 2 make room for scaling for the new canvas **/ 363 | const width = snippetContainerNode.offsetWidth * 2; 364 | const height = snippetContainerNode.offsetHeight * 2; 365 | 366 | /** Hiding the resizable handle on capture **/ 367 | snippetContainerNode.style.resize = "none"; 368 | 369 | /** Changing the snippet container background to transparenet temporary on capture **/ 370 | snippetContainerNode.style.backgroundColor = "transparent"; 371 | 372 | /** Scale snippetContainer by 2 temporary on capture **/ 373 | snippetContainerNode.style.transform = "scale(2)"; 374 | 375 | /** Canvas Options **/ 376 | const options = { 377 | removeContainer: true, 378 | width, 379 | height, 380 | } 381 | 382 | return new Promise((resolve, reject) => { 383 | html2canvas(snippetContainerNode, options).then((canvas) => { 384 | canvas.toBlob((blob) => { 385 | if (blob) { 386 | 387 | /** Reset color **/ 388 | snippetContainerNode.style.backgroundColor = "#f2f2f2" 389 | 390 | /** Reset scaling to previous **/ 391 | snippetContainerNode.style.transform = "none" 392 | 393 | /** show resize handle **/ 394 | snippetContainerNode.style.resize = ""; 395 | 396 | /** resolve passing blob as an argument **/ 397 | resolve(blob) 398 | } else reject(new Error("something bad happend")) 399 | }) 400 | }) 401 | }) 402 | } 403 | 404 | /** 405 | * @function snippetHandler 406 | * @param {Boolean} copyFlag 407 | * @param {Boolean} upload 408 | * Main function that handles canvas capturing and blob serializing and sending blob to vscode api. 409 | **/ 410 | function snippetHandler(copyFlag = false, upload) { 411 | html2blob() 412 | .then(blob => { 413 | serializeBlob(blob, serializedBlob => { 414 | 415 | if (copyFlag) copy(serializedBlob, upload); 416 | else shoot(serializedBlob); 417 | }); 418 | }) 419 | } 420 | 421 | /** Animation flag for the SNAP button watcing and keep track of the animation state **/ 422 | let isInAnimation = false; 423 | 424 | /** Snap button onhover Event Listener **/ 425 | obturateurLogo.addEventListener("mouseover", () => { 426 | if (!isInAnimation) { 427 | isInAnimation = true; 428 | 429 | new Vivus( 430 | "save_logo", { 431 | duration: 40, 432 | onReady: () => { 433 | obturateurLogo.className = "obturateur filling"; 434 | } 435 | }, 436 | () => { 437 | setTimeout(() => { 438 | isInAnimation = false; 439 | obturateurLogo.className = "obturateur"; 440 | }, 700); 441 | } 442 | ); 443 | } 444 | }); 445 | 446 | window.addEventListener("message", e => { 447 | if (e) { 448 | if (e.data.type === "init") { 449 | const { 450 | fontFamily, 451 | bgColor 452 | } = e.data; 453 | 454 | const initialHtml = getInitialHtml(fontFamily); 455 | snippetNode.innerHTML = initialHtml; 456 | vscode.setState({ 457 | innerHTML: initialHtml 458 | }); 459 | 460 | /** update backdrop color, using bgColor from last pasted snippet cannot deduce from initialHtml since it's always using Nord color **/ 461 | if (isDark(bgColor)) { 462 | snippetContainerNode.style.backgroundColor = "#f2f2f2"; 463 | } else { 464 | snippetContainerNode.style.background = "none"; 465 | } 466 | 467 | /** Event for successful Uplaod of the image from vscode api **/ 468 | } else if (e.data.type === "successfulUplaod") { 469 | 470 | /** Append the Upload url of the image to the body of the @UploadedUrlContainer as Html tags. **/ 471 | uploadedUrlContainer.innerHTML = 472 | ` 473 |
474 |
475 | 478 |
479 | 482 | Copied! 483 |
484 |
485 | ` 486 | /** Click Event Listener for clipboard button of the uploaded url **/ 487 | document.getElementById("clipboardBtn").addEventListener("click", () => { 488 | document.getElementById("copyNotif").className = "show" 489 | }) 490 | 491 | /** On update event from vscode api **/ 492 | } else if (e.data.type === "update") { 493 | document.execCommand("paste"); 494 | } else if (e.data.type === "restore") { 495 | snippetNode.innerHTML = e.data.innerHTML; 496 | updateEnvironment(e.data.bgColor); 497 | } else if (e.data.type === "restoreBgColor") { 498 | updateEnvironment(e.data.bgColor); 499 | } else if (e.data.type === "updateSettings") { 500 | snippetNode.style.boxShadow = e.data.shadow; 501 | target = e.data.target; 502 | transparentBackground = e.data.transparentBackground; 503 | snippetContainerNode.style.backgroundColor = e.data.backgroundColor; 504 | backgroundColor = e.data.backgroundColor; 505 | if (e.data.ligature) { 506 | snippetNode.style.fontVariantLigatures = "normal"; 507 | } else { 508 | snippetNode.style.fontVariantLigatures = "none"; 509 | } 510 | 511 | } 512 | } 513 | }); 514 | 515 | /** On key press event Listner **/ 516 | window.addEventListener("keypress", ReactToKeyup) 517 | 518 | /** On key up event Listner **/ 519 | window.addEventListener("keyup", ReactToKeyup) 520 | 521 | /** 522 | * @function ReactToKeyup 523 | * @param {Object} event 524 | * Reacts to key up keyboard key presses with toolbars functions such as saving the image on local directory or copying the image to clipboard. 525 | **/ 526 | function ReactToKeyup(event) { 527 | 528 | /** Ctrl + S or Cmd + S keyboard keypress for saving canvas as an image on the computer **/ 529 | if (event.which == 115 && (event.ctrlKey || event.metaKey) || (event.which == 19)) { 530 | event.preventDefault(); 531 | snippetHandler(); 532 | 533 | /** Ctrl + Z or Cmd + Z keyboard keypress for undo drawing **/ 534 | } else if (event.which == 90 && (event.ctrlKey || event.metaKey) || (event.which == 19)) { 535 | restoreState() 536 | 537 | /** Ctrl + C or Cmd + C keyboard keypress for copying image **/ 538 | } else if (event.which == 67 && (event.ctrlKey || event.metaKey) || (event.which == 19)) { 539 | copyImage() 540 | } 541 | } 542 | 543 | /** 544 | * PaintJS 545 | * Paint Canvas API 546 | **/ 547 | 548 | let savedImageData, 549 | 550 | /** Stores whether I'm currently dragging the mouse or not **/ 551 | dragging = false, 552 | 553 | /** Stroke Color of the brush **/ 554 | strokeColor = 'black', 555 | 556 | /** Stroke Color of the rectangle **/ 557 | fillColor = 'black', 558 | 559 | /** Line width for all tools **/ 560 | line_Width = 1, 561 | 562 | /** Tool currently used **/ 563 | currentTool = 'brush', 564 | 565 | /** Set canvas width to snippet width with 20px margin. **/ 566 | canvasWidth = snippetNode.clientWidth + 20, 567 | 568 | /** Set canvas height to snippet height with 20px margin. **/ 569 | canvasHeight = snippetNode.clientHeight + 20, 570 | 571 | /** Boolean for to check if Brush tool is being used. **/ 572 | usingBrush = false, 573 | 574 | /** Brush Points Storage **/ 575 | brushPoints = new Array(), 576 | 577 | /** History of canvas Drawing Storage **/ 578 | undo_array = new Array(), 579 | 580 | /** Pointer to track the currnet of the canvas drawing **/ 581 | currentState = 0; 582 | 583 | /** 584 | * @class ShapeBoundingBox 585 | * Stores size data used to create rubber band shapes that will redraw as the user moves the mouse. 586 | **/ 587 | class ShapeBoundingBox { 588 | constructor(left, top, width, height) { 589 | this.left = left; 590 | this.top = top; 591 | this.width = width; 592 | this.height = height; 593 | } 594 | } 595 | 596 | /** 597 | * @class MouseDownPos 598 | * Holds x & y position where clicked 599 | **/ 600 | class MouseDownPos { 601 | constructor(x, y) { 602 | this.x = x, 603 | this.y = y; 604 | } 605 | } 606 | 607 | /** 608 | * @class Location 609 | * Holds x & y location of the mouse 610 | **/ 611 | class Location { 612 | constructor(x, y) { 613 | this.x = x, 614 | this.y = y; 615 | } 616 | } 617 | 618 | /** Stores top left x & y and size of rubber band box **/ 619 | let shapeBoundingBox = new ShapeBoundingBox(0, 0, 0, 0); 620 | 621 | /** Holds x & y position where clicked **/ 622 | let mousedown = new MouseDownPos(0, 0); 623 | 624 | /** Holds x & y location of the mouse **/ 625 | let loc = new Location(0, 0); 626 | ctx.strokeStyle = strokeColor; 627 | ctx.lineWidth = line_Width; 628 | 629 | /** Execute ReactToMouseDown when the mouse is clicked **/ 630 | canvas.addEventListener("mousedown", ReactToMouseDown); 631 | 632 | /** Execute ReactToMouseMove when the mouse is clicked **/ 633 | canvas.addEventListener("mousemove", ReactToMouseMove); 634 | 635 | /** Execute ReactToMouseUp when the mouse is clicked **/ 636 | canvas.addEventListener("mouseup", ReactToMouseUp); 637 | 638 | /** 639 | * @function changeTool 640 | * @param {String} toolClicked 641 | * Changes the current tool to the tool selcted and applyies selected class on the currently used tool. 642 | **/ 643 | function changeTool(toolClicked) { 644 | 645 | /** remove class Selected from the unused tools **/ 646 | document.getElementById("brush").className = ""; 647 | document.getElementById("line").className = ""; 648 | document.getElementById("rectangle").className = ""; 649 | 650 | /** Highlight the last selected tool on toolbar **/ 651 | document.getElementById(toolClicked).className = "selected"; 652 | 653 | /** Change current tool used for drawing **/ 654 | currentTool = toolClicked; 655 | } 656 | 657 | /** 658 | * @function GetMousePosition 659 | * @param {Number} x 660 | * @param {Number} y 661 | * @returns {Object} 662 | * Returns mouse x & y position based on canvas position in page 663 | **/ 664 | function GetMousePosition(x, y) { 665 | 666 | /** Get canvas size and position in web page **/ 667 | let canvasSizeData = canvas.getBoundingClientRect(); 668 | return { 669 | x: (x - canvasSizeData.left) * (canvas.width / canvasSizeData.width), 670 | y: (y - canvasSizeData.top) * (canvas.height / canvasSizeData.height) 671 | }; 672 | } 673 | 674 | /** 675 | * @function saveCanvasImage 676 | * Saves the current canvas data. 677 | **/ 678 | function saveCanvasImage() { 679 | if (currentState != undo_array.length - 1) { 680 | undo_array.splice(currentState + 1, undo_array.length); 681 | } 682 | savedImageData = ctx.getImageData(0, 0, canvas.width, canvas.height) 683 | undo_array.push({ 684 | currentTool, 685 | savedImageData 686 | }) 687 | currentState++ 688 | } 689 | 690 | /** 691 | * @function redrawCanvasImage 692 | * Redraws the last saved canvas data. 693 | **/ 694 | function redrawCanvasImage() { 695 | if (savedImageData) ctx.putImageData(savedImageData, 0, 0); 696 | 697 | /** added this to cancel the bug of intial state **/ 698 | else { 699 | saveCanvasImage() 700 | redrawCanvasImage() 701 | } 702 | } 703 | 704 | /** 705 | * @function UpdateRubberbandBoxSizeData 706 | * @param {Object} loc 707 | * Updates Rubberband Box Size with mouse location 708 | **/ 709 | function UpdateRubberbandBoxSizeData(loc) { 710 | 711 | /** Height & width are the difference between were clicked and current mouse position **/ 712 | shapeBoundingBox.width = Math.abs(loc.x - mousedown.x); 713 | shapeBoundingBox.height = Math.abs(loc.y - mousedown.y); 714 | 715 | /** If mouse is below where mouse was clicked originally **/ 716 | if (loc.x > mousedown.x) { 717 | 718 | /** Store mousedown because it is farthest left **/ 719 | shapeBoundingBox.left = mousedown.x; 720 | } else { 721 | 722 | /** Store mouse location because it is most left **/ 723 | shapeBoundingBox.left = loc.x; 724 | } 725 | 726 | /** If mouse location is below where clicked originally **/ 727 | if (loc.y > mousedown.y) { 728 | 729 | /** Store mousedown because it is closer to the top of the canvas**/ 730 | shapeBoundingBox.top = mousedown.y; 731 | } else { 732 | 733 | /** Otherwise store mouse position **/ 734 | shapeBoundingBox.top = loc.y; 735 | } 736 | } 737 | 738 | 739 | /** Called to draw the line **/ 740 | function drawRubberbandShape(loc) { 741 | 742 | ctx.strokeStyle = strokeColor; 743 | ctx.fillStyle = fillColor; 744 | if (currentTool === "brush") { 745 | 746 | /** Create paint brush **/ 747 | DrawBrush(); 748 | } else if (currentTool === "line") { 749 | 750 | /** Draw Line **/ 751 | ctx.beginPath(); 752 | ctx.moveTo(mousedown.x, mousedown.y); 753 | ctx.lineTo(loc.x, loc.y); 754 | ctx.stroke(); 755 | } else if (currentTool === "rectangle") { 756 | 757 | /** Draw rectangle **/ 758 | ctx.strokeRect(shapeBoundingBox.left, shapeBoundingBox.top, shapeBoundingBox.width, shapeBoundingBox.height); 759 | } 760 | } 761 | 762 | /** 763 | * @function UpdateRubberbandBoxOnMove 764 | * @param {Object} loc 765 | * Updates Rubberband Box On the Move with x and y mouse locations. 766 | **/ 767 | function UpdateRubberbandBoxOnMove(loc) { 768 | 769 | /** Stores changing height, width, x & y position of most top left point being either the click or mouse location **/ 770 | UpdateRubberbandBoxSizeData(loc); 771 | 772 | /** Redraw the shape **/ 773 | drawRubberbandShape(loc); 774 | } 775 | 776 | /** 777 | * @function AddBrushPoint 778 | * @param {Number} x 779 | * @param {Number} y 780 | * @param {Boolean} mouseDown 781 | * @param {String} brushColor 782 | * @param {String} brushSize 783 | * @param {String} mode 784 | * @param {String} tool 785 | * Store each point as the mouse moves and whether the mouse button is currently being dragged or not and the color being used and also the tool was used back 786 | **/ 787 | function AddBrushPoint(x, y, mouseDown, brushColor, brushSize, mode = none, tool) { 788 | 789 | let point = { 790 | tool: tool, 791 | "x": x, 792 | "y": y, 793 | "isDrawing": mouseDown, 794 | size: brushSize, 795 | color: brushColor, 796 | mode: mode 797 | } 798 | brushPoints.push(point) 799 | } 800 | 801 | /** 802 | * @function DrawBrush 803 | * Cycle through all brush points and connect them with lines 804 | **/ 805 | function DrawBrush() { 806 | if (brushPoints.length == 0) return; 807 | 808 | for (var i = 0; i < brushPoints.length; i++) { 809 | let pt = brushPoints[i]; 810 | if (pt.tool !== "brush" || !pt.mode || !pt) return; 811 | let begin = false; 812 | ctx.strokeStyle = pt.color 813 | if (pt.mode == "begin" || begin) { 814 | ctx.beginPath(); 815 | ctx.moveTo(pt.x, pt.y); 816 | } 817 | ctx.lineTo(pt.x, pt.y); 818 | if (pt.mode == "end") { 819 | ctx.stroke(); 820 | } 821 | } 822 | ctx.stroke(); 823 | } 824 | 825 | /** 826 | * @function ReactToMouseDown 827 | * @param {Object} e Event 828 | * React on mouse down event. 829 | **/ 830 | function ReactToMouseDown(e) { 831 | 832 | /** Change the mouse pointer to a crosshair **/ 833 | canvas.style.cursor = "crosshair"; 834 | 835 | /** Store location **/ 836 | loc = GetMousePosition(e.clientX, e.clientY); 837 | 838 | /** Store mouse position when clicked **/ 839 | mousedown.x = loc.x; 840 | mousedown.y = loc.y; 841 | 842 | /** Store that yes the mouse is being held down **/ 843 | dragging = true; 844 | 845 | /** Brush will store points in an array **/ 846 | if (currentTool === 'brush') { 847 | usingBrush = true; 848 | AddBrushPoint(loc.x, loc.y, mouseDown = false, brushColor = strokeColor, brushSize = line_Width, mode = "begin", tool = currentTool); 849 | } 850 | }; 851 | 852 | /** 853 | * @function ReactToMouseMove 854 | * @param {Object} e Event 855 | * React on mounse event move. 856 | **/ 857 | function ReactToMouseMove(e) { 858 | 859 | /** Set the cursor to crosshair**/ 860 | canvas.style.cursor = "crosshair" 861 | 862 | /** get and location on the move **/ 863 | loc = GetMousePosition(e.clientX, e.clientY); 864 | 865 | /** If using brush tool and dragging store each point **/ 866 | if (currentTool === 'brush' && dragging && usingBrush) { 867 | if (loc.x > 0 && loc.x < canvasWidth - 5 && loc.y > 0 && loc.y < canvasHeight - 5) { 868 | ctx.lineTo(loc.x, loc.y); 869 | ctx.stroke(); 870 | AddBrushPoint(loc.x, loc.y, mouseDown = true, brushColor = strokeColor, brushSize = line_Width, mode = "draw", tool = currentTool); 871 | } 872 | 873 | /** Make the drawing stops on exceding canvas bounderies **/ 874 | else if (loc.x < 0 || loc.x > canvasWidth - 5 || loc.y < 0 || loc.y > canvasHeight - 5) { 875 | AddBrushPoint(loc.x, loc.y, mouseDown = false, brushColor = fillColor, brushSize = line_Width, mode = "end", tool = currentTool); 876 | ctx.stroke(); 877 | dragging = false 878 | } 879 | 880 | redrawCanvasImage(); 881 | DrawBrush(); 882 | } else { 883 | if (dragging) { 884 | redrawCanvasImage(); 885 | UpdateRubberbandBoxOnMove(loc); 886 | } 887 | } 888 | }; 889 | 890 | /** 891 | * @function ReactTomMouseUp 892 | * @param {Event} e Event 893 | * React to mouse Up event 894 | **/ 895 | function ReactToMouseUp(e) { 896 | 897 | /** if the mouse is beign dragged return **/ 898 | if (!dragging) return; 899 | 900 | /** Save canvas **/ 901 | saveCanvasImage() 902 | 903 | /** If current tool is "brush" tool then add brush point to draw **/ 904 | if (currentTool === "brush") AddBrushPoint(loc.x, loc.y, mouseDown = false, brushColor = fillColor, brushSize = line_Width, mode = "end", tool = currentTool); 905 | 906 | /** set cursor style to default **/ 907 | canvas.style.cursor = "defualt"; 908 | 909 | /** Update mouse location **/ 910 | loc = GetMousePosition(e.clientX, e.clientY); 911 | 912 | /** Set dragging flag to false **/ 913 | dragging = false; 914 | 915 | /** Redraw canvas image **/ 916 | redrawCanvasImage(); 917 | 918 | /** Update Rubber Band On move **/ 919 | UpdateRubberbandBoxOnMove(loc); 920 | 921 | /** Set usingbrush state to false **/ 922 | usingBrush = false; 923 | } 924 | 925 | 926 | /** 927 | * @function restoreState 928 | * Restores the previous state of canvas and set it the current state. 929 | **/ 930 | function restoreState() { 931 | 932 | /** if the array is empry or current status pointer is negtive return **/ 933 | if (!undo_array.length || currentState <= 0) return; 934 | 935 | /** take the previous canavas history **/ 936 | restore_state = undo_array[--currentState] 937 | 938 | /** if Brush tool was used deletes undo last brush points**/ 939 | if (restore_state.currentTool === "brush") { 940 | 941 | /** Delete last brush points **/ 942 | deleteLastBrushPoint() 943 | 944 | /** Draws brush points after delete **/ 945 | DrawBrush() 946 | } 947 | /** set current canvas image to previous canvas image **/ 948 | savedImageData = restore_state.savedImageData 949 | 950 | /** Redraw the canvas **/ 951 | redrawCanvasImage() 952 | } 953 | 954 | /** 955 | * @function deleteLastBrushPoint 956 | * Delete last brush points from the brushpoints array 957 | **/ 958 | function deleteLastBrushPoint() { 959 | 960 | /** Iterates through the all brush points and remove the last one **/ 961 | brushPoints.forEach((pt, i) => { 962 | if (pt.mode === "begin") { 963 | return brushPoints.splice(i, brushPoints.length) 964 | } 965 | }) 966 | } 967 | 968 | 969 | /** 970 | * @function copyImage 971 | * @param {Boolean} upload default False 972 | * Calls @snippetHandler that handles the snippet for uploading or copying functionality. 973 | */ 974 | function copyImage(upload = false) { 975 | /** calling @snippetHandler passing copyFlag set to True and upload paramater **/ 976 | snippetHandler(copyFlag = true, upload); 977 | } 978 | 979 | /** 980 | * @function uploadImage 981 | * Calls @snippetHandler that handles the snippet uploading functionality. 982 | */ 983 | function uploadImage() { 984 | /** call @copyImage funciton with upload flag set to True for uploading **/ 985 | copyImage(true) 986 | } 987 | 988 | /** 989 | * @ 990 | * Color Picker API 991 | **/ 992 | const pickr = Pickr.create({ 993 | el: '#pickr', 994 | theme: 'nano', 995 | 996 | /** Different Color options to pick from **/ 997 | swatches: [ 998 | 'rgba(244, 67, 54, 1)', 999 | 'rgba(233, 30, 99, 0.95)', 1000 | 'rgba(156, 39, 176, 0.9)', 1001 | 'rgba(103, 58, 183, 0.85)', 1002 | 'rgba(63, 81, 181, 0.8)', 1003 | 'rgba(33, 150, 243, 0.75)', 1004 | 'rgba(3, 169, 244, 0.7)', 1005 | 'rgba(0, 188, 212, 0.7)', 1006 | 'rgba(0, 150, 136, 0.75)', 1007 | 'rgba(76, 175, 80, 0.8)', 1008 | 'rgba(139, 195, 74, 0.85)', 1009 | 'rgba(205, 220, 57, 0.9)', 1010 | 'rgba(255, 235, 59, 0.95)', 1011 | 'rgba(255, 193, 7, 1)' 1012 | ], 1013 | 1014 | components: { 1015 | 1016 | /** Main components **/ 1017 | preview: true, 1018 | opacity: true, 1019 | hue: true, 1020 | } 1021 | }); 1022 | 1023 | /** Pickr Initialization **/ 1024 | pickr.on('init', (instance) => { 1025 | 1026 | /** Convert color to hex value **/ 1027 | const hexColor = color.toHEXA().toString(); 1028 | 1029 | /** Update fill and stroke color with picked color **/ 1030 | fillColor = strokeColor = hexColor 1031 | }); 1032 | 1033 | /** Pickr Color Change **/ 1034 | pickr.on('change', (color) => { 1035 | 1036 | /** Convert color to hex value **/ 1037 | const hexColor = color.toHEXA().toString(); 1038 | 1039 | /** Update fill and stroke color with picked color **/ 1040 | fillColor = strokeColor = hexColor 1041 | 1042 | /** Update PointerJs Ring Color **/ 1043 | init_pointer({ 1044 | pointerColor: hexColor, 1045 | }) 1046 | }) 1047 | 1048 | /** 1049 | * TODO: 1050 | * Redo feature 1051 | * to able to redo the last undo drawing implemented on the canvas. 1052 | **/ 1053 | }) 1054 | (); 1055 | } -------------------------------------------------------------------------------- /webview/paint.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | // Reference to the canvas element 3 | let canvas; 4 | // Context provides functions used for drawing and 5 | // working with Canvas 6 | let ctx; 7 | // Stores previously drawn image data to restore after 8 | // new drawings are added 9 | let savedImageData; 10 | // Stores whether I'm currently dragging the mouse 11 | let dragging = false; 12 | let strokeColor = 'black'; 13 | let fillColor = 'black'; 14 | let line_Width = 2; 15 | let polygonSides = 6; 16 | // Tool currently using 17 | let currentTool = 'brush'; 18 | let canvasWidth = 600; 19 | let canvasHeight = 600; 20 | 21 | // Stores size data used to create rubber band shapes 22 | // that will redraw as the user moves the mouse 23 | class ShapeBoundingBox { 24 | constructor(left, top, width, height) { 25 | this.left = left; 26 | this.top = top; 27 | this.width = width; 28 | this.height = height; 29 | } 30 | } 31 | 32 | // Holds x & y position where clicked 33 | class MouseDownPos { 34 | constructor(x, y) { 35 | this.x = x, 36 | this.y = y; 37 | } 38 | } 39 | 40 | // Holds x & y location of the mouse 41 | class Location { 42 | constructor(x, y) { 43 | this.x = x, 44 | this.y = y; 45 | } 46 | } 47 | 48 | // Holds x & y polygon point values 49 | class PolygonPoint { 50 | constructor(x, y) { 51 | this.x = x, 52 | this.y = y; 53 | } 54 | } 55 | // Stores top left x & y and size of rubber band box 56 | let shapeBoundingBox = new ShapeBoundingBox(0, 0, 0, 0); 57 | // Holds x & y position where clicked 58 | let mousedown = new MouseDownPos(0, 0); 59 | // Holds x & y location of the mouse 60 | let loc = new Location(0, 0); 61 | 62 | // Call for our function to execute when page is loaded 63 | document.addEventListener('DOMContentLoaded', setupCanvas); 64 | 65 | function setupCanvas() { 66 | // Get reference to canvas element 67 | canvas = document.getElementById('my-canvas'); 68 | // Get methods for manipulating the canvas 69 | ctx = canvas.getContext('2d'); 70 | ctx.strokeStyle = strokeColor; 71 | ctx.lineWidth = line_Width; 72 | // Execute ReactToMouseDown when the mouse is clicked 73 | canvas.addEventListener("mousedown", ReactToMouseDown); 74 | // Execute ReactToMouseMove when the mouse is clicked 75 | canvas.addEventListener("mousemove", ReactToMouseMove); 76 | // Execute ReactToMouseUp when the mouse is clicked 77 | canvas.addEventListener("mouseup", ReactToMouseUp); 78 | } 79 | 80 | function ChangeTool(toolClicked) { 81 | document.getElementById("open").className = ""; 82 | document.getElementById("save").className = ""; 83 | document.getElementById("brush").className = ""; 84 | document.getElementById("line").className = ""; 85 | document.getElementById("rectangle").className = ""; 86 | document.getElementById("circle").className = ""; 87 | document.getElementById("ellipse").className = ""; 88 | document.getElementById("polygon").className = ""; 89 | // Highlight the last selected tool on toolbar 90 | document.getElementById(toolClicked).className = "selected"; 91 | // Change current tool used for drawing 92 | currentTool = toolClicked; 93 | } 94 | 95 | // Returns mouse x & y position based on canvas position in page 96 | function GetMousePosition(x, y) { 97 | // Get canvas size and position in web page 98 | let canvasSizeData = canvas.getBoundingClientRect(); 99 | return { 100 | x: (x - canvasSizeData.left) * (canvas.width / canvasSizeData.width), 101 | y: (y - canvasSizeData.top) * (canvas.height / canvasSizeData.height) 102 | }; 103 | } 104 | 105 | function SaveCanvasImage() { 106 | // Save image 107 | savedImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 108 | } 109 | 110 | function RedrawCanvasImage() { 111 | // Restore image 112 | ctx.putImageData(savedImageData, 0, 0); 113 | } 114 | 115 | function UpdateRubberbandSizeData(loc) { 116 | // Height & width are the difference between were clicked 117 | // and current mouse position 118 | shapeBoundingBox.width = Math.abs(loc.x - mousedown.x); 119 | shapeBoundingBox.height = Math.abs(loc.y - mousedown.y); 120 | 121 | // If mouse is below where mouse was clicked originally 122 | if (loc.x > mousedown.x) { 123 | 124 | // Store mousedown because it is farthest left 125 | shapeBoundingBox.left = mousedown.x; 126 | } else { 127 | 128 | // Store mouse location because it is most left 129 | shapeBoundingBox.left = loc.x; 130 | } 131 | 132 | // If mouse location is below where clicked originally 133 | if (loc.y > mousedown.y) { 134 | 135 | // Store mousedown because it is closer to the top 136 | // of the canvas 137 | shapeBoundingBox.top = mousedown.y; 138 | } else { 139 | 140 | // Otherwise store mouse position 141 | shapeBoundingBox.top = loc.y; 142 | } 143 | } 144 | 145 | // Returns the angle using x and y 146 | // x = Adjacent Side 147 | // y = Opposite Side 148 | // Tan(Angle) = Opposite / Adjacent 149 | // Angle = ArcTan(Opposite / Adjacent) 150 | function getAngleUsingXAndY(mouselocX, mouselocY) { 151 | let adjacent = mousedown.x - mouselocX; 152 | let opposite = mousedown.y - mouselocY; 153 | 154 | return radiansToDegrees(Math.atan2(opposite, adjacent)); 155 | } 156 | 157 | function radiansToDegrees(rad) { 158 | if (rad < 0) { 159 | // Correct the bottom error by adding the negative 160 | // angle to 360 to get the correct result around 161 | // the whole circle 162 | return (360.0 + (rad * (180 / Math.PI))).toFixed(2); 163 | } else { 164 | return (rad * (180 / Math.PI)).toFixed(2); 165 | } 166 | } 167 | 168 | // Converts degrees to radians 169 | function degreesToRadians(degrees) { 170 | return degrees * (Math.PI / 180); 171 | } 172 | 173 | function ReactToMouseDown(e) { 174 | // Change the mouse pointer to a crosshair 175 | canvas.style.cursor = "crosshair"; 176 | // Store location 177 | loc = GetMousePosition(e.clientX, e.clientY); 178 | // Save the current canvas image 179 | SaveCanvasImage(); 180 | // Store mouse position when clicked 181 | mousedown.x = loc.x; 182 | mousedown.y = loc.y; 183 | // Store that yes the mouse is being held down 184 | dragging = true; 185 | } 186 | 187 | function ReactToMouseMove(e) { 188 | canvas.style.cursor = "crosshair"; 189 | loc = GetMousePosition(e.clientX, e.clientY); 190 | }; 191 | 192 | function ReactToMouseUp(e) { 193 | canvas.style.cursor = "default"; 194 | loc = GetMousePosition(e.clientX, e.clientY); 195 | RedrawCanvasImage(); 196 | UpdateRubberbandOnMove(loc); 197 | dragging = false; 198 | usingBrush = false; 199 | } 200 | 201 | // Saves the image in your default download directory 202 | function SaveImage() { 203 | // Get a reference to the link element 204 | var imageFile = document.getElementById("img-file"); 205 | // Set that you want to download the image when link is clicked 206 | imageFile.setAttribute('download', 'image.png'); 207 | // Reference the image in canvas for download 208 | imageFile.setAttribute('href', canvas.toDataURL()); 209 | } 210 | 211 | function OpenImage() { 212 | let img = new Image(); 213 | // Once the image is loaded clear the canvas and draw it 214 | img.onload = function () { 215 | ctx.clearRect(0, 0, canvas.width, canvas.height); 216 | ctx.drawImage(img, 0, 0); 217 | } 218 | img.src = 'image.png'; 219 | 220 | } 221 | } -------------------------------------------------------------------------------- /webview/pickr.js: -------------------------------------------------------------------------------- 1 | /*! Pickr 1.5.1 MIT | https://github.com/Simonwep/pickr */ ! function (t, e) { 2 | "object" == typeof exports && "object" == typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define([], e) : "object" == typeof exports ? exports.Pickr = e() : t.Pickr = e() 3 | }(window, (function () { 4 | return function (t) { 5 | var e = {}; 6 | 7 | function o(n) { 8 | if (e[n]) return e[n].exports; 9 | var i = e[n] = { 10 | i: n, 11 | l: !1, 12 | exports: {} 13 | }; 14 | return t[n].call(i.exports, i, i.exports, o), i.l = !0, i.exports 15 | } 16 | return o.m = t, o.c = e, o.d = function (t, e, n) { 17 | o.o(t, e) || Object.defineProperty(t, e, { 18 | enumerable: !0, 19 | get: n 20 | }) 21 | }, o.r = function (t) { 22 | "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(t, Symbol.toStringTag, { 23 | value: "Module" 24 | }), Object.defineProperty(t, "__esModule", { 25 | value: !0 26 | }) 27 | }, o.t = function (t, e) { 28 | if (1 & e && (t = o(t)), 8 & e) return t; 29 | if (4 & e && "object" == typeof t && t && t.__esModule) return t; 30 | var n = Object.create(null); 31 | if (o.r(n), Object.defineProperty(n, "default", { 32 | enumerable: !0, 33 | value: t 34 | }), 2 & e && "string" != typeof t) 35 | for (var i in t) o.d(n, i, function (e) { 36 | return t[e] 37 | }.bind(null, i)); 38 | return n 39 | }, o.n = function (t) { 40 | var e = t && t.__esModule ? function () { 41 | return t.default 42 | } : function () { 43 | return t 44 | }; 45 | return o.d(e, "a", e), e 46 | }, o.o = function (t, e) { 47 | return Object.prototype.hasOwnProperty.call(t, e) 48 | }, o.p = "", o(o.s = 1) 49 | }([function (t) { 50 | t.exports = JSON.parse('{"a":"1.5.1"}') 51 | }, function (t, e, o) { 52 | "use strict"; 53 | o.r(e); 54 | var n = {}; 55 | 56 | function i(t, e) { 57 | var o = Object.keys(t); 58 | if (Object.getOwnPropertySymbols) { 59 | var n = Object.getOwnPropertySymbols(t); 60 | e && (n = n.filter((function (e) { 61 | return Object.getOwnPropertyDescriptor(t, e).enumerable 62 | }))), o.push.apply(o, n) 63 | } 64 | return o 65 | } 66 | 67 | function r(t) { 68 | for (var e = 1; e < arguments.length; e++) { 69 | var o = null != arguments[e] ? arguments[e] : {}; 70 | e % 2 ? i(Object(o), !0).forEach((function (e) { 71 | s(t, e, o[e]) 72 | })) : Object.getOwnPropertyDescriptors ? Object.defineProperties(t, Object.getOwnPropertyDescriptors(o)) : i(Object(o)).forEach((function (e) { 73 | Object.defineProperty(t, e, Object.getOwnPropertyDescriptor(o, e)) 74 | })) 75 | } 76 | return t 77 | } 78 | 79 | function s(t, e, o) { 80 | return e in t ? Object.defineProperty(t, e, { 81 | value: o, 82 | enumerable: !0, 83 | configurable: !0, 84 | writable: !0 85 | }) : t[e] = o, t 86 | } 87 | 88 | function c(t, e, o, n, i = {}) { 89 | e instanceof HTMLCollection || e instanceof NodeList ? e = Array.from(e) : Array.isArray(e) || (e = [e]), Array.isArray(o) || (o = [o]); 90 | for (const s of e) 91 | for (const e of o) s[t](e, n, r({ 92 | capture: !1 93 | }, i)); 94 | return Array.prototype.slice.call(arguments, 1) 95 | } 96 | o.r(n), o.d(n, "on", (function () { 97 | return a 98 | })), o.d(n, "off", (function () { 99 | return l 100 | })), o.d(n, "createElementFromString", (function () { 101 | return p 102 | })), o.d(n, "removeAttribute", (function () { 103 | return u 104 | })), o.d(n, "createFromTemplate", (function () { 105 | return h 106 | })), o.d(n, "eventPath", (function () { 107 | return d 108 | })), o.d(n, "resolveElement", (function () { 109 | return f 110 | })), o.d(n, "adjustableInputNumbers", (function () { 111 | return m 112 | })); 113 | const a = c.bind(null, "addEventListener"), 114 | l = c.bind(null, "removeEventListener"); 115 | 116 | function p(t) { 117 | const e = document.createElement("div"); 118 | return e.innerHTML = t.trim(), e.firstElementChild 119 | } 120 | 121 | function u(t, e) { 122 | const o = t.getAttribute(e); 123 | return t.removeAttribute(e), o 124 | } 125 | 126 | function h(t) { 127 | return function t(e, o = {}) { 128 | const n = u(e, ":obj"), 129 | i = u(e, ":ref"), 130 | r = n ? o[n] = {} : o; 131 | i && (o[i] = e); 132 | for (const o of Array.from(e.children)) { 133 | const e = u(o, ":arr"), 134 | n = t(o, e ? {} : r); 135 | e && (r[e] || (r[e] = [])).push(Object.keys(n).length ? n : o) 136 | } 137 | return o 138 | }(p(t)) 139 | } 140 | 141 | function d(t) { 142 | let e = t.path || t.composedPath && t.composedPath(); 143 | if (e) return e; 144 | let o = t.target.parentElement; 145 | for (e = [t.target, o]; o = o.parentElement;) e.push(o); 146 | return e.push(document, window), e 147 | } 148 | 149 | function f(t) { 150 | return t instanceof Element ? t : "string" == typeof t ? t.split(/>>/g).reduce((t, e, o, n) => (t = t.querySelector(e), o < n.length - 1 ? t.shadowRoot : t), document) : null 151 | } 152 | 153 | function m(t, e = (t => t)) { 154 | function o(o) { 155 | const n = [.001, .01, .1][Number(o.shiftKey || 2 * o.ctrlKey)] * (o.deltaY < 0 ? 1 : -1); 156 | let i = 0, 157 | r = t.selectionStart; 158 | t.value = t.value.replace(/[\d.]+/g, (t, o) => o <= r && o + t.length >= r ? (r = o, e(Number(t), n, i)) : (i++, t)), t.focus(), t.setSelectionRange(r, r), o.preventDefault(), t.dispatchEvent(new Event("input")) 159 | } 160 | a(t, "focus", () => a(window, "wheel", o, { 161 | passive: !1 162 | })), a(t, "blur", () => l(window, "wheel", o)) 163 | } 164 | var v = o(0); 165 | const { 166 | min: b, 167 | max: y, 168 | floor: g, 169 | round: _ 170 | } = Math; 171 | 172 | function w(t, e, o) { 173 | e /= 100, o /= 100; 174 | const n = g(t = t / 360 * 6), 175 | i = t - n, 176 | r = o * (1 - e), 177 | s = o * (1 - i * e), 178 | c = o * (1 - (1 - i) * e), 179 | a = n % 6; 180 | return [255 * [o, s, r, r, c, o][a], 255 * [c, o, o, s, r, r][a], 255 * [r, r, c, o, o, s][a]] 181 | } 182 | 183 | function A(t, e, o) { 184 | const n = (2 - (e /= 100)) * (o /= 100) / 2; 185 | return 0 !== n && (e = 1 === n ? 0 : n < .5 ? e * o / (2 * n) : e * o / (2 - 2 * n)), [t, 100 * e, 100 * n] 186 | } 187 | 188 | function C(t, e, o) { 189 | const n = b(t /= 255, e /= 255, o /= 255), 190 | i = y(t, e, o), 191 | r = i - n; 192 | let s, c; 193 | if (0 === r) s = c = 0; 194 | else { 195 | c = r / i; 196 | const n = ((i - t) / 6 + r / 2) / r, 197 | a = ((i - e) / 6 + r / 2) / r, 198 | l = ((i - o) / 6 + r / 2) / r; 199 | t === i ? s = l - a : e === i ? s = 1 / 3 + n - l : o === i && (s = 2 / 3 + a - n), s < 0 ? s += 1 : s > 1 && (s -= 1) 200 | } 201 | return [360 * s, 100 * c, 100 * i] 202 | } 203 | 204 | function k(t, e, o, n) { 205 | return e /= 100, o /= 100, [...C(255 * (1 - b(1, (t /= 100) * (1 - (n /= 100)) + n)), 255 * (1 - b(1, e * (1 - n) + n)), 255 * (1 - b(1, o * (1 - n) + n)))] 206 | } 207 | 208 | function S(t, e, o) { 209 | return e /= 100, [t, 2 * (e *= (o /= 100) < .5 ? o : 1 - o) / (o + e) * 100, 100 * (o + e)] 210 | } 211 | 212 | function O(t) { 213 | return C(...t.match(/.{2}/g).map(t => parseInt(t, 16))) 214 | } 215 | 216 | function j(t) { 217 | t = t.match(/^[a-zA-Z]+$/) ? function (t) { 218 | if ("black" === t.toLowerCase()) return "#000"; 219 | const e = document.createElement("canvas").getContext("2d"); 220 | return e.fillStyle = t, "#000" === e.fillStyle ? null : e.fillStyle 221 | }(t) : t; 222 | const e = { 223 | cmyk: /^cmyk[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)/i, 224 | rgba: /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i, 225 | hsla: /^((hsla)|hsl)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i, 226 | hsva: /^((hsva)|hsv)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i, 227 | hexa: /^#?(([\dA-Fa-f]{3,4})|([\dA-Fa-f]{6})|([\dA-Fa-f]{8}))$/i 228 | }, 229 | o = t => t.map(t => /^(|\d+)\.\d+|\d+$/.test(t) ? Number(t) : void 0); 230 | let n; 231 | t: for (const i in e) { 232 | if (!(n = e[i].exec(t))) continue; 233 | const r = t => !!n[2] == ("number" == typeof t); 234 | switch (i) { 235 | case "cmyk": { 236 | const [, t, e, r, s] = o(n); 237 | if (t > 100 || e > 100 || r > 100 || s > 100) break t; 238 | return { 239 | values: k(t, e, r, s), 240 | type: i 241 | } 242 | } 243 | case "rgba": { 244 | const [, , , t, e, s, c] = o(n); 245 | if (t > 255 || e > 255 || s > 255 || c < 0 || c > 1 || !r(c)) break t; 246 | return { 247 | values: [...C(t, e, s), c], 248 | a: c, 249 | type: i 250 | } 251 | } 252 | case "hexa": { 253 | let [, t] = n; 254 | 4 !== t.length && 3 !== t.length || (t = t.split("").map(t => t + t).join("")); 255 | const e = t.substring(0, 6); 256 | let o = t.substring(6); 257 | return o = o ? parseInt(o, 16) / 255 : void 0, { 258 | values: [...O(e), o], 259 | a: o, 260 | type: i 261 | } 262 | } 263 | case "hsla": { 264 | const [, , , t, e, s, c] = o(n); 265 | if (t > 360 || e > 100 || s > 100 || c < 0 || c > 1 || !r(c)) break t; 266 | return { 267 | values: [...S(t, e, s), c], 268 | a: c, 269 | type: i 270 | } 271 | } 272 | case "hsva": { 273 | const [, , , t, e, s, c] = o(n); 274 | if (t > 360 || e > 100 || s > 100 || c < 0 || c > 1 || !r(c)) break t; 275 | return { 276 | values: [t, e, s, c], 277 | a: c, 278 | type: i 279 | } 280 | } 281 | } 282 | } 283 | return { 284 | values: null, 285 | type: null 286 | } 287 | } 288 | 289 | function x(t = 0, e = 0, o = 0, n = 1) { 290 | const i = (t, e) => (o = -1) => e(~o ? t.map(t => Number(t.toFixed(o))) : t), 291 | r = { 292 | h: t, 293 | s: e, 294 | v: o, 295 | a: n, 296 | toHSVA() { 297 | const t = [r.h, r.s, r.v, r.a]; 298 | return t.toString = i(t, t => "hsva(".concat(t[0], ", ").concat(t[1], "%, ").concat(t[2], "%, ").concat(r.a, ")")), t 299 | }, 300 | toHSLA() { 301 | const t = [...A(r.h, r.s, r.v), r.a]; 302 | return t.toString = i(t, t => "hsla(".concat(t[0], ", ").concat(t[1], "%, ").concat(t[2], "%, ").concat(r.a, ")")), t 303 | }, 304 | toRGBA() { 305 | const t = [...w(r.h, r.s, r.v), r.a]; 306 | return t.toString = i(t, t => "rgba(".concat(t[0], ", ").concat(t[1], ", ").concat(t[2], ", ").concat(r.a, ")")), t 307 | }, 308 | toCMYK() { 309 | const t = function (t, e, o) { 310 | const n = w(t, e, o), 311 | i = n[0] / 255, 312 | r = n[1] / 255, 313 | s = n[2] / 255, 314 | c = b(1 - i, 1 - r, 1 - s); 315 | return [100 * (1 === c ? 0 : (1 - i - c) / (1 - c)), 100 * (1 === c ? 0 : (1 - r - c) / (1 - c)), 100 * (1 === c ? 0 : (1 - s - c) / (1 - c)), 100 * c] 316 | }(r.h, r.s, r.v); 317 | return t.toString = i(t, t => "cmyk(".concat(t[0], "%, ").concat(t[1], "%, ").concat(t[2], "%, ").concat(t[3], "%)")), t 318 | }, 319 | toHEXA() { 320 | const t = function (t, e, o) { 321 | return w(t, e, o).map(t => _(t).toString(16).padStart(2, "0")) 322 | }(r.h, r.s, r.v), 323 | e = r.a >= 1 ? "" : Number((255 * r.a).toFixed(0)).toString(16).toUpperCase().padStart(2, "0"); 324 | return e && t.push(e), t.toString = () => "#".concat(t.join("").toUpperCase()), t 325 | }, 326 | clone: () => x(r.h, r.s, r.v, r.a) 327 | }; 328 | return r 329 | } 330 | const E = t => Math.max(Math.min(t, 1), 0); 331 | 332 | function L(t) { 333 | const e = { 334 | options: Object.assign({ 335 | lock: null, 336 | onchange: () => 0, 337 | onstop: () => 0 338 | }, t), 339 | _keyboard(t) { 340 | const { 341 | options: o 342 | } = e, { 343 | type: n, 344 | key: i 345 | } = t; 346 | if (document.activeElement === o.wrapper) { 347 | const { 348 | lock: o 349 | } = e.options, r = "ArrowUp" === i, s = "ArrowRight" === i, c = "ArrowDown" === i, a = "ArrowLeft" === i; 350 | if ("keydown" === n && (r || s || c || a)) { 351 | let n = 0, 352 | i = 0; 353 | "v" === o ? n = r || s ? 1 : -1 : "h" === o ? n = r || s ? -1 : 1 : (i = r ? -1 : c ? 1 : 0, n = a ? -1 : s ? 1 : 0), e.update(E(e.cache.x + .01 * n), E(e.cache.y + .01 * i)), t.preventDefault() 354 | } else i.startsWith("Arrow") && (e.options.onstop(), t.preventDefault()) 355 | } 356 | }, 357 | _tapstart(t) { 358 | a(document, ["mouseup", "touchend", "touchcancel"], e._tapstop), a(document, ["mousemove", "touchmove"], e._tapmove), t.preventDefault(), e._tapmove(t) 359 | }, 360 | _tapmove(t) { 361 | const { 362 | options: o, 363 | cache: n 364 | } = e, { 365 | lock: i, 366 | element: r, 367 | wrapper: s 368 | } = o, c = s.getBoundingClientRect(); 369 | let a = 0, 370 | l = 0; 371 | if (t) { 372 | const e = t && t.touches && t.touches[0]; 373 | a = t ? (e || t).clientX : 0, l = t ? (e || t).clientY : 0, a < c.left ? a = c.left : a > c.left + c.width && (a = c.left + c.width), l < c.top ? l = c.top : l > c.top + c.height && (l = c.top + c.height), a -= c.left, l -= c.top 374 | } else n && (a = n.x * c.width, l = n.y * c.height); 375 | "h" !== i && (r.style.left = "calc(".concat(a / c.width * 100, "% - ").concat(r.offsetWidth / 2, "px)")), "v" !== i && (r.style.top = "calc(".concat(l / c.height * 100, "% - ").concat(r.offsetHeight / 2, "px)")), e.cache = { 376 | x: a / c.width, 377 | y: l / c.height 378 | }; 379 | const p = E(a / c.width), 380 | u = E(l / c.height); 381 | switch (i) { 382 | case "v": 383 | return o.onchange(p); 384 | case "h": 385 | return o.onchange(u); 386 | default: 387 | return o.onchange(p, u) 388 | } 389 | }, 390 | _tapstop() { 391 | e.options.onstop(), l(document, ["mouseup", "touchend", "touchcancel"], e._tapstop), l(document, ["mousemove", "touchmove"], e._tapmove) 392 | }, 393 | trigger() { 394 | e._tapmove() 395 | }, 396 | update(t = 0, o = 0) { 397 | const { 398 | left: n, 399 | top: i, 400 | width: r, 401 | height: s 402 | } = e.options.wrapper.getBoundingClientRect(); 403 | "h" === e.options.lock && (o = t), e._tapmove({ 404 | clientX: n + r * t, 405 | clientY: i + s * o 406 | }) 407 | }, 408 | destroy() { 409 | const { 410 | options: t, 411 | _tapstart: o, 412 | _keyboard: n 413 | } = e; 414 | l(document, ["keydown", "keyup"], n), l([t.wrapper, t.element], "mousedown", o), l([t.wrapper, t.element], "touchstart", o, { 415 | passive: !1 416 | }) 417 | } 418 | }, 419 | { 420 | options: o, 421 | _tapstart: n, 422 | _keyboard: i 423 | } = e; 424 | return a([o.wrapper, o.element], "mousedown", n), a([o.wrapper, o.element], "touchstart", n, { 425 | passive: !1 426 | }), a(document, ["keydown", "keyup"], i), e 427 | } 428 | 429 | function P(t = {}) { 430 | t = Object.assign({ 431 | onchange: () => 0, 432 | className: "", 433 | elements: [] 434 | }, t); 435 | const e = a(t.elements, "click", e => { 436 | t.elements.forEach(o => o.classList[e.target === o ? "add" : "remove"](t.className)), t.onchange(e) 437 | }); 438 | return { 439 | destroy: () => l(...e) 440 | } 441 | } 442 | 443 | function B({ 444 | el: t, 445 | reference: e, 446 | padding: o = 8 447 | }) { 448 | const n = { 449 | start: "sme", 450 | middle: "mse", 451 | end: "ems" 452 | }, 453 | i = { 454 | top: "tbrl", 455 | right: "rltb", 456 | bottom: "btrl", 457 | left: "lrbt" 458 | }, 459 | r = ((t = {}) => (e, o = t[e]) => { 460 | if (o) return o; 461 | const [n, i = "middle"] = e.split("-"), r = "top" === n || "bottom" === n; 462 | return t[e] = { 463 | position: n, 464 | variant: i, 465 | isVertical: r 466 | } 467 | })(); 468 | return { 469 | update(s, c = !1) { 470 | const { 471 | position: a, 472 | variant: l, 473 | isVertical: p 474 | } = r(s), u = e.getBoundingClientRect(), h = t.getBoundingClientRect(), d = t => t ? { 475 | t: u.top - h.height - o, 476 | b: u.bottom + o 477 | } : { 478 | r: u.right + o, 479 | l: u.left - h.width - o 480 | }, f = t => t ? { 481 | s: u.left + u.width - h.width, 482 | m: -h.width / 2 + (u.left + u.width / 2), 483 | e: u.left 484 | } : { 485 | s: u.bottom - h.height, 486 | m: u.bottom - u.height / 2 - h.height / 2, 487 | e: u.bottom - u.height 488 | }, m = {}, v = (t, e, o) => { 489 | const n = "top" === o, 490 | i = n ? h.height : h.width, 491 | r = window[n ? "innerHeight" : "innerWidth"]; 492 | for (const n of t) { 493 | const t = e[n], 494 | s = m[o] = "".concat(t, "px"); 495 | if (t > 0 && t + i < r) return s 496 | } 497 | return null 498 | }; 499 | for (const e of [p, !p]) { 500 | const o = e ? "top" : "left", 501 | r = e ? "left" : "top", 502 | s = v(i[a], d(e), o), 503 | c = v(n[l], f(e), r); 504 | if (s && c) return t.style[r] = c, void(t.style[o] = s) 505 | } 506 | c ? (t.style.top = "".concat((window.innerHeight - h.height) / 2, "px"), t.style.left = "".concat((window.innerWidth - h.width) / 2, "px")) : (t.style.left = m.left, t.style.top = m.top) 507 | } 508 | } 509 | } 510 | 511 | function H(t, e, o) { 512 | return e in t ? Object.defineProperty(t, e, { 513 | value: o, 514 | enumerable: !0, 515 | configurable: !0, 516 | writable: !0 517 | }) : t[e] = o, t 518 | } 519 | class R { 520 | constructor(t) { 521 | H(this, "_initializingActive", !0), H(this, "_recalc", !0), H(this, "_nanopop", null), H(this, "_root", null), H(this, "_color", x()), H(this, "_lastColor", x()), H(this, "_swatchColors", []), H(this, "_eventListener", { 522 | init: [], 523 | save: [], 524 | hide: [], 525 | show: [], 526 | clear: [], 527 | change: [], 528 | changestop: [], 529 | cancel: [], 530 | swatchselect: [] 531 | }), this.options = t = Object.assign({ 532 | appClass: null, 533 | theme: "classic", 534 | useAsButton: !1, 535 | padding: 8, 536 | disabled: !1, 537 | comparison: !0, 538 | closeOnScroll: !1, 539 | outputPrecision: 0, 540 | lockOpacity: !1, 541 | autoReposition: !0, 542 | container: "body", 543 | components: { 544 | interaction: {} 545 | }, 546 | strings: {}, 547 | swatches: null, 548 | inline: !1, 549 | sliders: null, 550 | default: "#42445a", 551 | defaultRepresentation: null, 552 | position: "bottom-middle", 553 | adjustableNumbers: !0, 554 | showAlways: !1, 555 | closeWithKey: "Escape" 556 | }, t); 557 | const { 558 | swatches: e, 559 | components: o, 560 | theme: n, 561 | sliders: i, 562 | lockOpacity: r, 563 | padding: s 564 | } = t; 565 | ["nano", "monolith"].includes(n) && !i && (t.sliders = "h"), o.interaction || (o.interaction = {}); 566 | const { 567 | preview: c, 568 | opacity: a, 569 | hue: l, 570 | palette: p 571 | } = o; 572 | o.opacity = !r && a, o.palette = p || c || a || l, this._preBuild(), this._buildComponents(), this._bindEvents(), this._finalBuild(), e && e.length && e.forEach(t => this.addSwatch(t)); 573 | const { 574 | button: u, 575 | app: h 576 | } = this._root; 577 | this._nanopop = B({ 578 | reference: u, 579 | padding: s, 580 | el: h 581 | }), u.setAttribute("role", "button"), u.setAttribute("aria-label", "toggle color picker dialog"); 582 | const d = this; 583 | requestAnimationFrame((function e() { 584 | if (!h.offsetWidth && h.parentElement !== t.container) return requestAnimationFrame(e); 585 | d.setColor(t.default), d._rePositioningPicker(), t.defaultRepresentation && (d._representation = t.defaultRepresentation, d.setColorRepresentation(d._representation)), t.showAlways && d.show(), d._initializingActive = !1, d._emit("init") 586 | })) 587 | } 588 | _preBuild() { 589 | const t = this.options; 590 | for (const e of ["el", "container"]) t[e] = f(t[e]); 591 | this._root = (({ 592 | components: t, 593 | strings: e, 594 | useAsButton: o, 595 | inline: n, 596 | appClass: i, 597 | theme: r, 598 | lockOpacity: s 599 | }) => { 600 | const c = t => t ? "" : 'style="display:none" hidden', 601 | a = h('\n
\n\n '.concat(o ? "" : '', '\n\n
\n
\n
\n \n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n\n
\n\n
\n \n\n \n \n \n \n \n\n \n \n \n
\n
\n
\n ')), 602 | l = a.interaction; 603 | return l.options.find(t => !t.hidden && !t.classList.add("active")), l.type = () => l.options.find(t => t.classList.contains("active")), a 604 | })(t), t.useAsButton && (this._root.button = t.el), t.container.appendChild(this._root.root) 605 | } 606 | _finalBuild() { 607 | const t = this.options, 608 | e = this._root; 609 | if (t.container.removeChild(e.root), t.inline) { 610 | const o = t.el.parentElement; 611 | t.el.nextSibling ? o.insertBefore(e.app, t.el.nextSibling) : o.appendChild(e.app) 612 | } else t.container.appendChild(e.app); 613 | t.useAsButton ? t.inline && t.el.remove() : t.el.parentNode.replaceChild(e.root, t.el), t.disabled && this.disable(), t.comparison || (e.button.style.transition = "none", t.useAsButton || (e.preview.lastColor.style.transition = "none")), this.hide() 614 | } 615 | _buildComponents() { 616 | const t = this, 617 | e = this.options.components, 618 | o = (t.options.sliders || "v").repeat(2), 619 | [n, i] = o.match(/^[vh]+$/g) ? o : [], 620 | r = () => this._color || (this._color = this._lastColor.clone()), 621 | s = { 622 | palette: L({ 623 | element: t._root.palette.picker, 624 | wrapper: t._root.palette.palette, 625 | onstop: () => t._emit("changestop", t), 626 | onchange(o, n) { 627 | if (!e.palette) return; 628 | const i = r(), 629 | { 630 | _root: s, 631 | options: c 632 | } = t, 633 | { 634 | lastColor: a, 635 | currentColor: l 636 | } = s.preview; 637 | t._recalc && (i.s = 100 * o, i.v = 100 - 100 * n, i.v < 0 && (i.v = 0), t._updateOutput()); 638 | const p = i.toRGBA().toString(0); 639 | this.element.style.background = p, this.wrapper.style.background = "\n linear-gradient(to top, rgba(0, 0, 0, ".concat(i.a, "), transparent),\n linear-gradient(to left, hsla(").concat(i.h, ", 100%, 50%, ").concat(i.a, "), rgba(255, 255, 255, ").concat(i.a, "))\n "), c.comparison ? c.useAsButton || t._lastColor || (a.style.color = p) : (s.button.style.color = p, s.button.classList.remove("clear")); 640 | const u = i.toHEXA().toString(); 641 | for (const { 642 | el: e, 643 | color: o 644 | } of t._swatchColors) e.classList[u === o.toHEXA().toString() ? "add" : "remove"]("pcr-active"); 645 | l.style.color = p 646 | } 647 | }), 648 | hue: L({ 649 | lock: "v" === i ? "h" : "v", 650 | element: t._root.hue.picker, 651 | wrapper: t._root.hue.slider, 652 | onstop: () => t._emit("changestop", t), 653 | onchange(o) { 654 | if (!e.hue || !e.palette) return; 655 | const n = r(); 656 | t._recalc && (n.h = 360 * o), this.element.style.backgroundColor = "hsl(".concat(n.h, ", 100%, 50%)"), s.palette.trigger() 657 | } 658 | }), 659 | opacity: L({ 660 | lock: "v" === n ? "h" : "v", 661 | element: t._root.opacity.picker, 662 | wrapper: t._root.opacity.slider, 663 | onstop: () => t._emit("changestop", t), 664 | onchange(o) { 665 | if (!e.opacity || !e.palette) return; 666 | const n = r(); 667 | t._recalc && (n.a = Math.round(100 * o) / 100), this.element.style.background = "rgba(0, 0, 0, ".concat(n.a, ")"), s.palette.trigger() 668 | } 669 | }), 670 | selectable: P({ 671 | elements: t._root.interaction.options, 672 | className: "active", 673 | onchange(e) { 674 | t._representation = e.target.getAttribute("data-type").toUpperCase(), t._recalc && t._updateOutput() 675 | } 676 | }) 677 | }; 678 | this._components = s 679 | } 680 | _bindEvents() { 681 | const { 682 | _root: t, 683 | options: e 684 | } = this, o = [a(t.interaction.clear, "click", () => this._clearColor()), a([t.interaction.cancel, t.preview.lastColor], "click", () => { 685 | this._emit("cancel", this), this.setHSVA(...(this._lastColor || this._color).toHSVA(), !0) 686 | }), a(t.interaction.save, "click", () => { 687 | !this.applyColor() && !e.showAlways && this.hide() 688 | }), a(t.interaction.result, ["keyup", "input"], t => { 689 | this.setColor(t.target.value, !0) && !this._initializingActive && this._emit("change", this._color), t.stopImmediatePropagation() 690 | }), a(t.interaction.result, ["focus", "blur"], t => { 691 | this._recalc = "blur" === t.type, this._recalc && this._updateOutput() 692 | }), a([t.palette.palette, t.palette.picker, t.hue.slider, t.hue.picker, t.opacity.slider, t.opacity.picker], ["mousedown", "touchstart"], () => this._recalc = !0)]; 693 | if (!e.showAlways) { 694 | const n = e.closeWithKey; 695 | o.push(a(t.button, "click", () => this.isOpen() ? this.hide() : this.show()), a(document, "keyup", t => this.isOpen() && (t.key === n || t.code === n) && this.hide()), a(document, ["touchstart", "mousedown"], e => { 696 | this.isOpen() && !d(e).some(e => e === t.app || e === t.button) && this.hide() 697 | }, { 698 | capture: !0 699 | })) 700 | } 701 | if (e.adjustableNumbers) { 702 | const e = { 703 | rgba: [255, 255, 255, 1], 704 | hsva: [360, 100, 100, 1], 705 | hsla: [360, 100, 100, 1], 706 | cmyk: [100, 100, 100, 100] 707 | }; 708 | m(t.interaction.result, (t, o, n) => { 709 | const i = e[this.getColorRepresentation().toLowerCase()]; 710 | if (i) { 711 | const e = i[n], 712 | r = t + (e >= 100 ? 1e3 * o : o); 713 | return r <= 0 ? 0 : Number((r < e ? r : e).toPrecision(3)) 714 | } 715 | return t 716 | }) 717 | } 718 | if (e.autoReposition && !e.inline) { 719 | let t = null; 720 | const n = this; 721 | o.push(a(window, ["scroll", "resize"], () => { 722 | n.isOpen() && (e.closeOnScroll && n.hide(), null === t ? (t = setTimeout(() => t = null, 100), requestAnimationFrame((function e() { 723 | n._rePositioningPicker(), null !== t && requestAnimationFrame(e) 724 | }))) : (clearTimeout(t), t = setTimeout(() => t = null, 100))) 725 | }, { 726 | capture: !0 727 | })) 728 | } 729 | this._eventBindings = o 730 | } 731 | _rePositioningPicker() { 732 | const { 733 | options: t 734 | } = this; 735 | t.inline || this._nanopop.update(t.position, !this._recalc) 736 | } 737 | _updateOutput() { 738 | const { 739 | _root: t, 740 | _color: e, 741 | options: o 742 | } = this; 743 | if (t.interaction.type()) { 744 | const n = "to".concat(t.interaction.type().getAttribute("data-type")); 745 | t.interaction.result.value = "function" == typeof e[n] ? e[n]().toString(o.outputPrecision) : "" 746 | }!this._initializingActive && this._recalc && this._emit("change", e) 747 | } 748 | _clearColor(t = !1) { 749 | const { 750 | _root: e, 751 | options: o 752 | } = this; 753 | o.useAsButton || (e.button.style.color = "rgba(0, 0, 0, 0.15)"), e.button.classList.add("clear"), o.showAlways || this.hide(), this._lastColor = null, this._initializingActive || t || (this._emit("save", null), this._emit("clear", this)) 754 | } 755 | _parseLocalColor(t) { 756 | const { 757 | values: e, 758 | type: o, 759 | a: n 760 | } = j(t), { 761 | lockOpacity: i 762 | } = this.options, r = void 0 !== n && 1 !== n; 763 | return e && 3 === e.length && (e[3] = void 0), { 764 | values: !e || i && r ? null : e, 765 | type: o 766 | } 767 | } 768 | _emit(t, ...e) { 769 | this._eventListener[t].forEach(t => t(...e, this)) 770 | } 771 | on(t, e) { 772 | return "function" == typeof e && "string" == typeof t && t in this._eventListener && this._eventListener[t].push(e), this 773 | } 774 | off(t, e) { 775 | const o = this._eventListener[t]; 776 | if (o) { 777 | const t = o.indexOf(e); 778 | ~t && o.splice(t, 1) 779 | } 780 | return this 781 | } 782 | addSwatch(t) { 783 | const { 784 | values: e 785 | } = this._parseLocalColor(t); 786 | if (e) { 787 | const { 788 | _swatchColors: t, 789 | _root: o 790 | } = this, n = x(...e), i = p('