├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc.yml ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── patches └── @simonwep+pickr+1.7.2.patch ├── src ├── _reset.scss ├── app │ ├── _variables.scss │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── canvas │ │ ├── canvas.component.html │ │ ├── canvas.component.scss │ │ └── canvas.component.ts │ ├── dashboard │ │ ├── dashboard.component.html │ │ ├── dashboard.component.scss │ │ ├── dashboard.component.spec.ts │ │ └── dashboard.component.ts │ ├── menu │ │ ├── menu.component.html │ │ ├── menu.component.scss │ │ ├── menu.component.spec.ts │ │ └── menu.component.ts │ ├── shared │ │ ├── directive │ │ │ ├── active-menu.directive.ts │ │ │ ├── click-stop-propagation.directive.ts │ │ │ ├── event.directive.ts │ │ │ ├── menu.directive.ts │ │ │ └── slide-brush-size.directive.ts │ │ ├── model │ │ │ ├── canvas-offset.model.ts │ │ │ ├── erase.model.ts │ │ │ ├── flgs.model.ts │ │ │ ├── history.model.ts │ │ │ ├── key.model.ts │ │ │ ├── offset.model.ts │ │ │ ├── point.model.ts │ │ │ ├── pointer-offset.model.ts │ │ │ ├── pointer.model.ts │ │ │ └── trail.model.ts │ │ └── service │ │ │ ├── core │ │ │ ├── canvas.service.ts │ │ │ ├── cpu.service.ts │ │ │ ├── cursor.service.ts │ │ │ ├── flg-event.service.ts │ │ │ ├── func.service.ts │ │ │ ├── gpu.service.ts │ │ │ ├── grid.service.ts │ │ │ ├── key-event.service.ts │ │ │ ├── key-map.service.ts │ │ │ ├── memory.service.ts │ │ │ ├── pointer-event.service.ts │ │ │ ├── register.service.ts │ │ │ ├── ruler.service.ts │ │ │ └── ui.service.ts │ │ │ ├── module │ │ │ ├── cleanup.service.ts │ │ │ ├── create-line.service.ts │ │ │ ├── create-square.service.ts │ │ │ ├── draw.service.ts │ │ │ ├── erase.service.ts │ │ │ ├── pen.service.ts │ │ │ ├── select.service.ts │ │ │ ├── select.ui.service.ts │ │ │ ├── slide-brush-size.service.ts │ │ │ └── zoom.service.ts │ │ │ └── util │ │ │ ├── coord.service.ts │ │ │ ├── debug.service.ts │ │ │ └── lib.service.ts │ ├── tool-bar │ │ ├── tool-bar.component.html │ │ ├── tool-bar.component.scss │ │ ├── tool-bar.component.spec.ts │ │ └── tool-bar.component.ts │ └── tool-menu │ │ ├── tool-menu.component.html │ │ ├── tool-menu.component.scss │ │ ├── tool-menu.component.spec.ts │ │ └── tool-menu.component.ts ├── assets │ ├── .gitkeep │ └── image.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.base.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major version 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-11 # For IE 9-11 support, remove 'not'. 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@angular-eslint/recommended'], 3 | rules: { 4 | '@angular-eslint/directive-selector': ['error', { type: 'attribute', prefix: 'app', style: 'camelCase' }], 5 | '@angular-eslint/component-selector': ['error', { type: 'element', prefix: 'app', style: 'kebab-case' }] 6 | }, 7 | overrides: [ 8 | { 9 | files: ['*.ts'], 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: 2020, 13 | sourceType: 'module', 14 | project: ['*/tsconfig.json', './tsconfig.**.json'] // 追加 15 | }, 16 | plugins: ['@angular-eslint/template'], 17 | processor: '@angular-eslint/template/extract-inline-html' 18 | } 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: none 2 | tabWidth: 2 3 | singleQuote: true 4 | useTabs: true 5 | printWidth: 120 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

InfiDraw

2 |

InfiDraw provides infinite canvas with drawing featrue. 3 |
Highly optimized and lightweight. 4 |
DEMO 5 |

6 |
7 | 8 | ![InfiDraw_preview](./src/assets/image.png) 9 | 10 | # Document 11 | Currently working on 12 |

13 | 14 | # Demo 15 | - Key bindings 16 | - 'p' : Pen (by default) 17 | - 'e' : Eraser 18 | - 'Ctrl + z' : Undo 19 | - 'Ctrl + Alt + z' : Redo 20 |

21 | 22 | # Licence 23 | MIT Licence 24 | 25 | Copyright (c) 2020 NkiHrk 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 28 | 29 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "infi-draw": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": ["src/favicon.ico", "src/assets"], 27 | "styles": ["src/styles.scss"], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true, 46 | "budgets": [ 47 | { 48 | "type": "initial", 49 | "maximumWarning": "2mb", 50 | "maximumError": "5mb" 51 | }, 52 | { 53 | "type": "anyComponentStyle", 54 | "maximumWarning": "6kb", 55 | "maximumError": "10kb" 56 | } 57 | ] 58 | } 59 | } 60 | }, 61 | "serve": { 62 | "builder": "@angular-devkit/build-angular:dev-server", 63 | "options": { 64 | "browserTarget": "infi-draw:build" 65 | }, 66 | "configurations": { 67 | "production": { 68 | "browserTarget": "infi-draw:build:production" 69 | } 70 | } 71 | }, 72 | "extract-i18n": { 73 | "builder": "@angular-devkit/build-angular:extract-i18n", 74 | "options": { 75 | "browserTarget": "infi-draw:build" 76 | } 77 | }, 78 | "test": { 79 | "builder": "@angular-devkit/build-angular:karma", 80 | "options": { 81 | "main": "src/test.ts", 82 | "polyfills": "src/polyfills.ts", 83 | "tsConfig": "tsconfig.spec.json", 84 | "karmaConfig": "karma.conf.js", 85 | "assets": ["src/favicon.ico", "src/assets"], 86 | "styles": ["src/styles.scss"], 87 | "scripts": [] 88 | } 89 | }, 90 | "lint": { 91 | "builder": "@angular-eslint/builder:lint", 92 | "options": { 93 | "eslintConfig": ".eslintrc.js", 94 | "tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json"], 95 | "exclude": ["**/node_modules/**"] 96 | } 97 | }, 98 | "e2e": { 99 | "builder": "@angular-devkit/build-angular:protractor", 100 | "options": { 101 | "protractorConfig": "e2e/protractor.conf.js", 102 | "devServerTarget": "infi-draw:serve" 103 | }, 104 | "configurations": { 105 | "production": { 106 | "devServerTarget": "infi-draw:serve:production" 107 | } 108 | } 109 | } 110 | } 111 | } 112 | }, 113 | "defaultProject": "infi-draw" 114 | } 115 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('infi-draw app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain( 20 | jasmine.objectContaining({ 21 | level: logging.Level.SEVERE 22 | } as logging.Entry) 23 | ); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: "", 7 | frameworks: ["jasmine", "@angular-devkit/build-angular"], 8 | plugins: [ 9 | require("karma-jasmine"), 10 | require("karma-chrome-launcher"), 11 | require("karma-jasmine-html-reporter"), 12 | require("karma-coverage-istanbul-reporter"), 13 | require("@angular-devkit/build-angular/plugins/karma") 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require("path").join(__dirname, "./coverage/infi-draw"), 20 | reports: ["html", "lcovonly", "text-summary"], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ["progress", "kjhtml"], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ["Chrome"], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infi-draw", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build --prod --base-href ./", 8 | "deploy": "gh-pages -d dist", 9 | "test": "ng test", 10 | "lint": "ng lint --fix", 11 | "e2e": "ng e2e", 12 | "postinstall": "npx patch-package" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~10.0.14", 17 | "@angular/common": "~10.0.14", 18 | "@angular/compiler": "~10.0.14", 19 | "@angular/core": "~10.0.14", 20 | "@angular/forms": "~10.0.14", 21 | "@angular/platform-browser": "~10.0.14", 22 | "@angular/platform-browser-dynamic": "~10.0.14", 23 | "@angular/router": "~10.0.14", 24 | "@fortawesome/angular-fontawesome": "^0.7.0", 25 | "@fortawesome/fontawesome-svg-core": "^1.2.29", 26 | "@fortawesome/free-regular-svg-icons": "^5.13.1", 27 | "@fortawesome/free-solid-svg-icons": "^5.13.1", 28 | "@simonwep/pickr": "^1.7.2", 29 | "@types/lodash": "^4.14.157", 30 | "lodash": "^4.17.19", 31 | "rxjs": "~6.6.2", 32 | "tslib": "^2.0.0", 33 | "zone.js": "~0.10.3" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "~0.1000.8", 37 | "@angular-eslint/builder": "0.0.1-alpha.32", 38 | "@angular-eslint/eslint-plugin": "0.0.1-alpha.32", 39 | "@angular-eslint/eslint-plugin-template": "0.0.1-alpha.32", 40 | "@angular-eslint/schematics": "0.0.1-alpha.32", 41 | "@angular-eslint/template-parser": "0.0.1-alpha.32", 42 | "@angular/cli": "~10.0.8", 43 | "@angular/compiler-cli": "~10.0.14", 44 | "@types/jasmine": "~3.5.0", 45 | "@types/jasminewd2": "~2.0.3", 46 | "@types/node": "^12.11.1", 47 | "@typescript-eslint/eslint-plugin": "2.31.0", 48 | "@typescript-eslint/parser": "2.31.0", 49 | "codelyzer": "^6.0.0-next.1", 50 | "eslint": "^6.8.0", 51 | "eslint-config-prettier": "^6.11.0", 52 | "eslint-plugin-prettier": "^3.1.4", 53 | "gh-pages": "^3.1.0", 54 | "jasmine-core": "~3.5.0", 55 | "jasmine-spec-reporter": "~5.0.0", 56 | "karma": "~5.0.0", 57 | "karma-chrome-launcher": "~3.1.0", 58 | "karma-coverage-istanbul-reporter": "~3.0.2", 59 | "karma-jasmine": "~3.3.0", 60 | "karma-jasmine-html-reporter": "^1.5.0", 61 | "prettier": "^2.0.5", 62 | "protractor": "~7.0.0", 63 | "ts-node": "~8.3.0", 64 | "typescript": "~3.9.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /patches/@simonwep+pickr+1.7.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@simonwep/pickr/dist/themes/monolith.min.css b/node_modules/@simonwep/pickr/dist/themes/monolith.min.css 2 | index 2afdd28..40f0e83 100644 3 | --- a/node_modules/@simonwep/pickr/dist/themes/monolith.min.css 4 | +++ b/node_modules/@simonwep/pickr/dist/themes/monolith.min.css 5 | @@ -1 +1,364 @@ 6 | -/*! Pickr 1.7.2 MIT | https://github.com/Simonwep/pickr */.pickr{position:relative;overflow:visible;transform:translateY(0)}.pickr *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr .pcr-button{position:relative;height:2em;width:2em;padding:.5em;cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;border-radius:.15em;background:url('data:image/svg+xml;utf8, ') no-repeat 50%;background-size:0;transition:all .3s}.pickr .pcr-button:before{background:url('data:image/svg+xml;utf8, ');background-size:.5em;z-index:-1;z-index:auto}.pickr .pcr-button:after,.pickr .pcr-button:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;border-radius:.15em}.pickr .pcr-button:after{transition:background .3s;background:currentColor}.pickr .pcr-button.clear{background-size:70%}.pickr .pcr-button.clear:before{opacity:0}.pickr .pcr-button.clear:focus{box-shadow:0 0 0 1px hsla(0,0%,100%,.85),0 0 0 3px currentColor}.pickr .pcr-button.disabled{cursor:not-allowed}.pcr-app *,.pickr *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pcr-app button.pcr-active,.pcr-app button:focus,.pcr-app input.pcr-active,.pcr-app input:focus,.pickr button.pcr-active,.pickr button:focus,.pickr input.pcr-active,.pickr input:focus{box-shadow:0 0 0 1px hsla(0,0%,100%,.85),0 0 0 3px currentColor}.pcr-app .pcr-palette,.pcr-app .pcr-slider,.pickr .pcr-palette,.pickr .pcr-slider{transition:box-shadow .3s}.pcr-app .pcr-palette:focus,.pcr-app .pcr-slider:focus,.pickr .pcr-palette:focus,.pickr .pcr-slider:focus{box-shadow:0 0 0 1px hsla(0,0%,100%,.85),0 0 0 3px rgba(0,0,0,.25)}.pcr-app{position:fixed;display:flex;flex-direction:column;z-index:10000;border-radius:.1em;background:#fff;opacity:0;visibility:hidden;transition:opacity .3s,visibility 0s .3s;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;box-shadow:0 .15em 1.5em 0 rgba(0,0,0,.1),0 0 1em 0 rgba(0,0,0,.03);left:0;top:0}.pcr-app.visible{transition:opacity .3s;visibility:visible;opacity:1}.pcr-app .pcr-swatches{display:flex;flex-wrap:wrap;margin-top:.75em}.pcr-app .pcr-swatches.pcr-last{margin:0}@supports (display:grid){.pcr-app .pcr-swatches{display:grid;align-items:center;grid-template-columns:repeat(auto-fit,1.75em)}}.pcr-app .pcr-swatches>button{font-size:1em;position:relative;width:calc(1.75em - 5px);height:calc(1.75em - 5px);border-radius:.15em;cursor:pointer;margin:2.5px;flex-shrink:0;justify-self:center;transition:all .15s;overflow:hidden;background:transparent;z-index:1}.pcr-app .pcr-swatches>button:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url('data:image/svg+xml;utf8, ');background-size:6px;border-radius:.15em;z-index:-1}.pcr-app .pcr-swatches>button:after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:currentColor;border:1px solid rgba(0,0,0,.05);border-radius:.15em;box-sizing:border-box}.pcr-app .pcr-swatches>button:hover{-webkit-filter:brightness(1.05);filter:brightness(1.05)}.pcr-app .pcr-swatches>button:not(.pcr-active){box-shadow:none}.pcr-app .pcr-interaction{display:flex;flex-wrap:wrap;align-items:center;margin:0 -.2em}.pcr-app .pcr-interaction>*{margin:0 .2em}.pcr-app .pcr-interaction input{letter-spacing:.07em;font-size:.75em;text-align:center;cursor:pointer;color:#75797e;background:#f1f3f4;border-radius:.15em;transition:all .15s;padding:.45em .5em;margin-top:.75em}.pcr-app .pcr-interaction input:hover{-webkit-filter:brightness(.975);filter:brightness(.975)}.pcr-app .pcr-interaction input:focus{box-shadow:0 0 0 1px hsla(0,0%,100%,.85),0 0 0 3px rgba(66,133,244,.75)}.pcr-app .pcr-interaction .pcr-result{color:#75797e;text-align:left;flex:1 1 8em;min-width:8em;transition:all .2s;border-radius:.15em;background:#f1f3f4;cursor:text}.pcr-app .pcr-interaction .pcr-result::-moz-selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-result::selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-type.active{color:#fff;background:#4285f4}.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear,.pcr-app .pcr-interaction .pcr-save{width:auto;color:#fff}.pcr-app .pcr-interaction .pcr-cancel:hover,.pcr-app .pcr-interaction .pcr-clear:hover,.pcr-app .pcr-interaction .pcr-save:hover{-webkit-filter:brightness(.925);filter:brightness(.925)}.pcr-app .pcr-interaction .pcr-save{background:#4285f4}.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{background:#f44250}.pcr-app .pcr-interaction .pcr-cancel:focus,.pcr-app .pcr-interaction .pcr-clear:focus{box-shadow:0 0 0 1px hsla(0,0%,100%,.85),0 0 0 3px rgba(244,66,80,.75)}.pcr-app .pcr-selection .pcr-picker{position:absolute;height:18px;width:18px;border:2px solid #fff;border-radius:100%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity,.pcr-app .pcr-selection .pcr-color-palette{position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:flex;flex-direction:column;cursor:grab;cursor:-webkit-grab}.pcr-app .pcr-selection .pcr-color-chooser:active,.pcr-app .pcr-selection .pcr-color-opacity:active,.pcr-app .pcr-selection .pcr-color-palette:active{cursor:grabbing;cursor:-webkit-grabbing}.pcr-app[data-theme=monolith]{width:14.25em;max-width:95vw;padding:.8em}.pcr-app[data-theme=monolith] .pcr-selection{display:flex;flex-direction:column;justify-content:space-between;flex-grow:1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview{position:relative;z-index:1;width:100%;height:1em;display:flex;flex-direction:row;justify-content:space-between;margin-bottom:.5em}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url('data:image/svg+xml;utf8, ');background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-last-color{cursor:pointer;transition:background-color .3s,box-shadow .3s;border-radius:.15em 0 0 .15em;z-index:2}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-current-color{border-radius:0 .15em .15em 0}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-current-color,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-last-color{background:currentColor;width:50%;height:100%}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-palette{width:100%;height:8em;z-index:1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-palette .pcr-palette{border-radius:.15em;width:100%;height:100%}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-palette .pcr-palette:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url('data:image/svg+xml;utf8, ');background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity{height:.5em;margin-top:.75em}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser .pcr-picker,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity .pcr-picker{top:50%;transform:translateY(-50%)}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser .pcr-slider,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity .pcr-slider{flex-grow:1;border-radius:50em}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser .pcr-slider{background:linear-gradient(90deg,red,#ff0,#0f0,#0ff,#00f,#f0f,red)}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity .pcr-slider{background:linear-gradient(90deg,transparent,#000),url('data:image/svg+xml;utf8, ');background-size:100%,.25em} 7 | \ No newline at end of file 8 | +/*! Pickr 1.7.2 MIT | https://github.com/Simonwep/pickr */ 9 | +.pickr { 10 | + position: relative; 11 | + overflow: visible; 12 | + transform: translateY(0); 13 | + display: flex; 14 | + align-content: center; 15 | +} 16 | +.pickr * { 17 | + box-sizing: border-box; 18 | + outline: none; 19 | + border: none; 20 | + -webkit-appearance: none; 21 | +} 22 | +.pickr .pcr-button { 23 | + position: relative; 24 | + height: 18px; 25 | + width: 18px; 26 | + padding: 0.5em; 27 | + cursor: pointer; 28 | + border-radius: 2px; 29 | + background: url('data:image/svg+xml;utf8, ') 30 | + no-repeat 50%; 31 | + background-size: 0; 32 | + transition: all 0.3s; 33 | +} 34 | +.pickr .pcr-button:before { 35 | + background: url('data:image/svg+xml;utf8, '); 36 | + background-size: 0.5em; 37 | + z-index: -1; 38 | + z-index: auto; 39 | +} 40 | +.pickr .pcr-button:after, 41 | +.pickr .pcr-button:before { 42 | + position: absolute; 43 | + content: ""; 44 | + top: 0; 45 | + left: 0; 46 | + width: 100%; 47 | + height: 100%; 48 | + border-radius: 0.15em; 49 | +} 50 | +.pickr .pcr-button:after { 51 | + transition: background 0.3s; 52 | + background: currentColor; 53 | +} 54 | +.pickr .pcr-button.clear { 55 | + background-size: 70%; 56 | +} 57 | +.pickr .pcr-button.clear:before { 58 | + opacity: 0; 59 | +} 60 | +.pickr .pcr-button.clear:focus { 61 | + box-shadow: 0 0 0 1px hsla(0, 0%, 100%, 0.85), 0 0 0 3px currentColor; 62 | +} 63 | +.pickr .pcr-button.disabled { 64 | + cursor: not-allowed; 65 | +} 66 | +.pcr-app *, 67 | +.pickr * { 68 | + box-sizing: border-box; 69 | + outline: none; 70 | + border: none; 71 | + -webkit-appearance: none; 72 | +} 73 | +.pcr-app button.pcr-active, 74 | +.pcr-app button:focus, 75 | +.pcr-app input.pcr-active, 76 | +.pcr-app input:focus, 77 | +.pickr button.pcr-active, 78 | +.pickr button:focus, 79 | +.pickr input.pcr-active, 80 | +.pickr input:focus { 81 | + box-shadow: 0 0 0 1px hsla(0, 0%, 100%, 0.85), 0 0 0 3px currentColor; 82 | +} 83 | +.pcr-app .pcr-palette, 84 | +.pcr-app .pcr-slider, 85 | +.pickr .pcr-palette, 86 | +.pickr .pcr-slider { 87 | + transition: box-shadow 0.3s; 88 | +} 89 | +.pcr-app .pcr-palette:focus, 90 | +.pcr-app .pcr-slider:focus, 91 | +.pickr .pcr-palette:focus, 92 | +.pickr .pcr-slider:focus { 93 | + box-shadow: 0 0 0 1px hsla(0, 0%, 100%, 0.85), 0 0 0 3px rgba(0, 0, 0, 0.25); 94 | +} 95 | +.pcr-app { 96 | + position: fixed; 97 | + display: flex; 98 | + flex-direction: column; 99 | + z-index: 10000; 100 | + border-radius: 0.1em; 101 | + background: #fff; 102 | + opacity: 0; 103 | + visibility: hidden; 104 | + transition: opacity 0.3s, visibility 0s 0.3s; 105 | + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; 106 | + box-shadow: 0 0.15em 1.5em 0 rgba(0, 0, 0, 0.1), 0 0 1em 0 rgba(0, 0, 0, 0.03); 107 | + left: 0; 108 | + top: 0; 109 | +} 110 | +.pcr-app.visible { 111 | + transition: opacity 0.3s; 112 | + visibility: visible; 113 | + opacity: 1; 114 | +} 115 | +.pcr-app .pcr-swatches { 116 | + display: flex; 117 | + flex-wrap: wrap; 118 | + margin-top: 0.75em; 119 | +} 120 | +.pcr-app .pcr-swatches.pcr-last { 121 | + margin: 0; 122 | +} 123 | +@supports (display: grid) { 124 | + .pcr-app .pcr-swatches { 125 | + display: grid; 126 | + align-items: center; 127 | + grid-template-columns: repeat(auto-fit, 1.75em); 128 | + } 129 | +} 130 | +.pcr-app .pcr-swatches > button { 131 | + font-size: 1em; 132 | + position: relative; 133 | + width: calc(1.75em - 5px); 134 | + height: calc(1.75em - 5px); 135 | + border-radius: 0.15em; 136 | + cursor: pointer; 137 | + margin: 2.5px; 138 | + flex-shrink: 0; 139 | + justify-self: center; 140 | + transition: all 0.15s; 141 | + overflow: hidden; 142 | + background: transparent; 143 | + z-index: 1; 144 | +} 145 | +.pcr-app .pcr-swatches > button:before { 146 | + position: absolute; 147 | + content: ""; 148 | + top: 0; 149 | + left: 0; 150 | + width: 100%; 151 | + height: 100%; 152 | + background: url('data:image/svg+xml;utf8, '); 153 | + background-size: 6px; 154 | + border-radius: 0.15em; 155 | + z-index: -1; 156 | +} 157 | +.pcr-app .pcr-swatches > button:after { 158 | + content: ""; 159 | + position: absolute; 160 | + top: 0; 161 | + left: 0; 162 | + width: 100%; 163 | + height: 100%; 164 | + background: currentColor; 165 | + border: 1px solid rgba(0, 0, 0, 0.05); 166 | + border-radius: 0.15em; 167 | + box-sizing: border-box; 168 | +} 169 | +.pcr-app .pcr-swatches > button:hover { 170 | + -webkit-filter: brightness(1.05); 171 | + filter: brightness(1.05); 172 | +} 173 | +.pcr-app .pcr-swatches > button:not(.pcr-active) { 174 | + box-shadow: none; 175 | +} 176 | +.pcr-app .pcr-interaction { 177 | + display: flex; 178 | + flex-wrap: wrap; 179 | + align-items: center; 180 | + margin: 0 -0.2em; 181 | +} 182 | +.pcr-app .pcr-interaction > * { 183 | + margin: 0 0.2em; 184 | +} 185 | +.pcr-app .pcr-interaction input { 186 | + letter-spacing: 0.07em; 187 | + font-size: 0.75em; 188 | + text-align: center; 189 | + cursor: pointer; 190 | + color: #75797e; 191 | + background: #f1f3f4; 192 | + border-radius: 0.15em; 193 | + transition: all 0.15s; 194 | + padding: 0.45em 0.5em; 195 | + margin-top: 0.75em; 196 | +} 197 | +.pcr-app .pcr-interaction input:hover { 198 | + -webkit-filter: brightness(0.975); 199 | + filter: brightness(0.975); 200 | +} 201 | +.pcr-app .pcr-interaction input:focus { 202 | + box-shadow: 0 0 0 1px hsla(0, 0%, 100%, 0.85), 0 0 0 3px rgba(66, 133, 244, 0.75); 203 | +} 204 | +.pcr-app .pcr-interaction .pcr-result { 205 | + color: #75797e; 206 | + text-align: left; 207 | + flex: 1 1 8em; 208 | + min-width: 8em; 209 | + transition: all 0.2s; 210 | + border-radius: 0.15em; 211 | + background: #f1f3f4; 212 | + cursor: text; 213 | +} 214 | +.pcr-app .pcr-interaction .pcr-result::-moz-selection { 215 | + background: #4285f4; 216 | + color: #fff; 217 | +} 218 | +.pcr-app .pcr-interaction .pcr-result::selection { 219 | + background: #4285f4; 220 | + color: #fff; 221 | +} 222 | +.pcr-app .pcr-interaction .pcr-type.active { 223 | + color: #fff; 224 | + background: #4285f4; 225 | +} 226 | +.pcr-app .pcr-interaction .pcr-cancel, 227 | +.pcr-app .pcr-interaction .pcr-clear, 228 | +.pcr-app .pcr-interaction .pcr-save { 229 | + width: auto; 230 | + color: #fff; 231 | +} 232 | +.pcr-app .pcr-interaction .pcr-cancel:hover, 233 | +.pcr-app .pcr-interaction .pcr-clear:hover, 234 | +.pcr-app .pcr-interaction .pcr-save:hover { 235 | + -webkit-filter: brightness(0.925); 236 | + filter: brightness(0.925); 237 | +} 238 | +.pcr-app .pcr-interaction .pcr-save { 239 | + background: #4285f4; 240 | +} 241 | +.pcr-app .pcr-interaction .pcr-cancel, 242 | +.pcr-app .pcr-interaction .pcr-clear { 243 | + background: #f44250; 244 | +} 245 | +.pcr-app .pcr-interaction .pcr-cancel:focus, 246 | +.pcr-app .pcr-interaction .pcr-clear:focus { 247 | + box-shadow: 0 0 0 1px hsla(0, 0%, 100%, 0.85), 0 0 0 3px rgba(244, 66, 80, 0.75); 248 | +} 249 | +.pcr-app .pcr-selection .pcr-picker { 250 | + position: absolute; 251 | + height: 18px; 252 | + width: 18px; 253 | + border: 2px solid #fff; 254 | + border-radius: 100%; 255 | + -webkit-user-select: none; 256 | + -moz-user-select: none; 257 | + -ms-user-select: none; 258 | + user-select: none; 259 | +} 260 | +.pcr-app .pcr-selection .pcr-color-chooser, 261 | +.pcr-app .pcr-selection .pcr-color-opacity, 262 | +.pcr-app .pcr-selection .pcr-color-palette { 263 | + position: relative; 264 | + -webkit-user-select: none; 265 | + -moz-user-select: none; 266 | + -ms-user-select: none; 267 | + user-select: none; 268 | + display: flex; 269 | + flex-direction: column; 270 | + cursor: grab; 271 | + cursor: -webkit-grab; 272 | +} 273 | +.pcr-app .pcr-selection .pcr-color-chooser:active, 274 | +.pcr-app .pcr-selection .pcr-color-opacity:active, 275 | +.pcr-app .pcr-selection .pcr-color-palette:active { 276 | + cursor: grabbing; 277 | + cursor: -webkit-grabbing; 278 | +} 279 | +.pcr-app[data-theme="monolith"] { 280 | + width: 14.25em; 281 | + max-width: 95vw; 282 | + padding: 0.8em; 283 | +} 284 | +.pcr-app[data-theme="monolith"] .pcr-selection { 285 | + display: flex; 286 | + flex-direction: column; 287 | + justify-content: space-between; 288 | + flex-grow: 1; 289 | +} 290 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-preview { 291 | + position: relative; 292 | + z-index: 1; 293 | + width: 100%; 294 | + height: 1em; 295 | + display: flex; 296 | + flex-direction: row; 297 | + justify-content: space-between; 298 | + margin-bottom: 0.5em; 299 | +} 300 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-preview:before { 301 | + position: absolute; 302 | + content: ""; 303 | + top: 0; 304 | + left: 0; 305 | + width: 100%; 306 | + height: 100%; 307 | + background: url('data:image/svg+xml;utf8, '); 308 | + background-size: 0.5em; 309 | + border-radius: 0.15em; 310 | + z-index: -1; 311 | +} 312 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-preview .pcr-last-color { 313 | + cursor: pointer; 314 | + transition: background-color 0.3s, box-shadow 0.3s; 315 | + border-radius: 0.15em 0 0 0.15em; 316 | + z-index: 2; 317 | +} 318 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-preview .pcr-current-color { 319 | + border-radius: 0 0.15em 0.15em 0; 320 | +} 321 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-preview .pcr-current-color, 322 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-preview .pcr-last-color { 323 | + background: currentColor; 324 | + width: 50%; 325 | + height: 100%; 326 | +} 327 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-palette { 328 | + width: 100%; 329 | + height: 8em; 330 | + z-index: 1; 331 | +} 332 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-palette .pcr-palette { 333 | + border-radius: 0.15em; 334 | + width: 100%; 335 | + height: 100%; 336 | +} 337 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-palette .pcr-palette:before { 338 | + position: absolute; 339 | + content: ""; 340 | + top: 0; 341 | + left: 0; 342 | + width: 100%; 343 | + height: 100%; 344 | + background: url('data:image/svg+xml;utf8, '); 345 | + background-size: 0.5em; 346 | + border-radius: 0.15em; 347 | + z-index: -1; 348 | +} 349 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-chooser, 350 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-opacity { 351 | + height: 0.5em; 352 | + margin-top: 0.75em; 353 | +} 354 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-chooser .pcr-picker, 355 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-opacity .pcr-picker { 356 | + top: 50%; 357 | + transform: translateY(-50%); 358 | +} 359 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-chooser .pcr-slider, 360 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-opacity .pcr-slider { 361 | + flex-grow: 1; 362 | + border-radius: 50em; 363 | +} 364 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-chooser .pcr-slider { 365 | + background: linear-gradient(90deg, red, #ff0, #0f0, #0ff, #00f, #f0f, red); 366 | +} 367 | +.pcr-app[data-theme="monolith"] .pcr-selection .pcr-color-opacity .pcr-slider { 368 | + background: linear-gradient(90deg, transparent, #000), 369 | + url('data:image/svg+xml;utf8, '); 370 | + background-size: 100%, 0.25em; 371 | +} 372 | -------------------------------------------------------------------------------- /src/_reset.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Forked from Bootstrap Reboot v4.3.1 (https://getbootstrap.com/), licensed MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 3 | */ 4 | 5 | *, 6 | *::before, 7 | *::after { 8 | -moz-box-sizing: border-box; 9 | -webkit-box-sizing: border-box; 10 | box-sizing: border-box; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | html { 16 | -webkit-text-size-adjust: 100%; 17 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 18 | } 19 | 20 | article, 21 | aside, 22 | figcaption, 23 | figure, 24 | footer, 25 | header, 26 | hgroup, 27 | main, 28 | nav, 29 | section { 30 | display: block; 31 | } 32 | 33 | body { 34 | margin: 0; 35 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Yu Gothic', YuGothic, Verdana, Meiryo, 'M+ 1p', 36 | sans-serif; 37 | font-size: 14px; 38 | line-height: 1.5; 39 | text-align: left; 40 | } 41 | 42 | [tabindex='-1']:focus { 43 | outline: 0 !important; 44 | } 45 | 46 | hr { 47 | box-sizing: content-box; 48 | height: 0; 49 | overflow: visible; 50 | } 51 | 52 | h1, 53 | h2, 54 | h3, 55 | h4, 56 | h5, 57 | h6 { 58 | margin: 0px; 59 | padding: 0px; 60 | } 61 | 62 | p { 63 | margin-top: 0; 64 | margin-bottom: 1rem; 65 | } 66 | 67 | abbr[title], 68 | abbr[data-original-title] { 69 | text-decoration: underline; 70 | -webkit-text-decoration: underline dotted; 71 | text-decoration: underline dotted; 72 | cursor: help; 73 | border-bottom: 0; 74 | -webkit-text-decoration-skip-ink: none; 75 | text-decoration-skip-ink: none; 76 | } 77 | 78 | address { 79 | margin-bottom: 1rem; 80 | font-style: normal; 81 | line-height: inherit; 82 | } 83 | 84 | ul { 85 | list-style: none; 86 | } 87 | 88 | ol, 89 | ul, 90 | dl { 91 | margin-top: 0; 92 | /* margin-bottom: 1rem; */ 93 | margin: 0px; 94 | padding: 0px; 95 | } 96 | 97 | ol ol, 98 | ul ul, 99 | ol ul, 100 | ul ol { 101 | margin-bottom: 0; 102 | } 103 | 104 | dt { 105 | font-weight: 700; 106 | } 107 | 108 | dd { 109 | margin-bottom: 0.5rem; 110 | margin-left: 0; 111 | } 112 | 113 | blockquote { 114 | margin: 0 0 1rem; 115 | } 116 | 117 | b, 118 | strong { 119 | font-weight: bolder; 120 | } 121 | 122 | small { 123 | font-size: 80%; 124 | } 125 | 126 | sub, 127 | sup { 128 | position: relative; 129 | font-size: 75%; 130 | line-height: 0; 131 | vertical-align: baseline; 132 | } 133 | 134 | sub { 135 | bottom: -0.25em; 136 | } 137 | 138 | sup { 139 | top: -0.5em; 140 | } 141 | 142 | a { 143 | text-decoration: none; 144 | background-color: transparent; 145 | } 146 | 147 | a:hover { 148 | text-decoration: none; 149 | } 150 | 151 | a:not([href]):not([tabindex]) { 152 | color: inherit; 153 | text-decoration: none; 154 | } 155 | 156 | a:not([href]):not([tabindex]):hover, 157 | a:not([href]):not([tabindex]):focus { 158 | color: inherit; 159 | text-decoration: none; 160 | } 161 | 162 | a:not([href]):not([tabindex]):focus { 163 | outline: 0; 164 | } 165 | 166 | pre, 167 | code, 168 | kbd, 169 | samp { 170 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; 171 | font-size: 1em; 172 | } 173 | 174 | pre { 175 | margin-top: 0; 176 | margin-bottom: 1rem; 177 | overflow: auto; 178 | } 179 | 180 | figure { 181 | margin: 0 0 1rem; 182 | } 183 | 184 | img { 185 | vertical-align: middle; 186 | border-style: none; 187 | } 188 | 189 | svg { 190 | overflow: hidden; 191 | vertical-align: middle; 192 | } 193 | 194 | canvas { 195 | image-rendering: optimizeSpeed; 196 | image-rendering: -moz-crisp-edges; 197 | image-rendering: -webkit-optimize-contrast; 198 | image-rendering: optimize-contrast; 199 | image-rendering: pixelated; 200 | image-rendering: crisp-edges; 201 | -ms-interpolation-mode: nearest-neighbor; 202 | -webkit-font-smoothing: none; 203 | 204 | vertical-align: bottom; 205 | } 206 | 207 | table { 208 | border-collapse: collapse; 209 | } 210 | 211 | caption { 212 | padding-top: 0.75rem; 213 | padding-bottom: 0.75rem; 214 | text-align: left; 215 | caption-side: bottom; 216 | } 217 | 218 | th { 219 | text-align: inherit; 220 | } 221 | 222 | label { 223 | display: inline-block; 224 | margin-bottom: 0.5rem; 225 | } 226 | 227 | button { 228 | border-radius: 0; 229 | } 230 | 231 | button:focus { 232 | outline: 1px dotted; 233 | outline: 5px auto -webkit-focus-ring-color; 234 | } 235 | 236 | input, 237 | button, 238 | select, 239 | optgroup, 240 | textarea { 241 | margin: 0; 242 | font-family: inherit; 243 | font-size: inherit; 244 | line-height: inherit; 245 | } 246 | 247 | button, 248 | input { 249 | overflow: visible; 250 | } 251 | 252 | button, 253 | select { 254 | text-transform: none; 255 | } 256 | 257 | select { 258 | word-wrap: normal; 259 | } 260 | 261 | button, 262 | [type='button'], 263 | [type='reset'], 264 | [type='submit'] { 265 | -webkit-appearance: button; 266 | } 267 | 268 | button:not(:disabled), 269 | [type='button']:not(:disabled), 270 | [type='reset']:not(:disabled), 271 | [type='submit']:not(:disabled) { 272 | cursor: pointer; 273 | } 274 | 275 | button::-moz-focus-inner, 276 | [type='button']::-moz-focus-inner, 277 | [type='reset']::-moz-focus-inner, 278 | [type='submit']::-moz-focus-inner { 279 | padding: 0; 280 | border-style: none; 281 | } 282 | 283 | input[type='radio'], 284 | input[type='checkbox'] { 285 | box-sizing: border-box; 286 | padding: 0; 287 | } 288 | 289 | input[type='date'], 290 | input[type='time'], 291 | input[type='datetime-local'], 292 | input[type='month'] { 293 | -webkit-appearance: listbox; 294 | } 295 | 296 | textarea { 297 | overflow: auto; 298 | resize: vertical; 299 | } 300 | 301 | fieldset { 302 | min-width: 0; 303 | padding: 0; 304 | margin: 0; 305 | border: 0; 306 | } 307 | 308 | legend { 309 | display: block; 310 | width: 100%; 311 | max-width: 100%; 312 | padding: 0; 313 | margin-bottom: 0.5rem; 314 | font-size: 1.5rem; 315 | line-height: inherit; 316 | color: inherit; 317 | white-space: normal; 318 | } 319 | 320 | progress { 321 | vertical-align: baseline; 322 | } 323 | 324 | [type='number']::-webkit-inner-spin-button, 325 | [type='number']::-webkit-outer-spin-button { 326 | height: auto; 327 | } 328 | 329 | [type='search'] { 330 | outline-offset: -2px; 331 | -webkit-appearance: none; 332 | } 333 | 334 | [type='search']::-webkit-search-decoration { 335 | -webkit-appearance: none; 336 | } 337 | 338 | ::-webkit-file-upload-button { 339 | font: inherit; 340 | -webkit-appearance: button; 341 | } 342 | 343 | output { 344 | display: inline-block; 345 | } 346 | 347 | summary { 348 | display: list-item; 349 | cursor: pointer; 350 | } 351 | 352 | template { 353 | display: none; 354 | } 355 | 356 | [hidden] { 357 | display: none !important; 358 | } 359 | -------------------------------------------------------------------------------- /src/app/_variables.scss: -------------------------------------------------------------------------------- 1 | /*——————————————————————————————————————————————————————— 2 | Color variables 3 | —————————————————————————————————————————————————————————*/ 4 | 5 | $red: #e56470; 6 | $white: #bcbcbe; 7 | $mid-white: #606060; 8 | $canvas-color: #32303f; 9 | $canvas-color-dark: #2e2d3b; 10 | 11 | /*——————————————————————————————————————————————————————— 12 | Number variables 13 | —————————————————————————————————————————————————————————*/ 14 | 15 | $ruler-thickness: 20px; 16 | $transition: 0.1s ease-in-out; 17 | 18 | /*——————————————————————————————————————————————————————— 19 | Mixin 20 | —————————————————————————————————————————————————————————*/ 21 | 22 | @mixin drop-shadow { 23 | // -webkit-filter: drop-shadow(-3px 3px 5px rgba(0, 0, 0, 0.3)); 24 | // filter: drop-shadow(-3px 3px 5px rgba(0, 0, 0, 0.3)); 25 | box-shadow: -3px 3px 5px rgba(0, 0, 0, 0.3); 26 | } 27 | 28 | @mixin disable-selection { 29 | -webkit-touch-callout: none; /* iOS Safari */ 30 | -webkit-user-select: none; /* Safari */ 31 | -khtml-user-select: none; /* Konqueror HTML */ 32 | -moz-user-select: none; /* Old versions of Firefox */ 33 | -ms-user-select: none; /* Internet Explorer/Edge */ 34 | user-select: none; /* Non-prefixed version, currently supported by Chrome, Opera and Firefox */ 35 | } 36 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { DashboardComponent } from './dashboard/dashboard.component'; 4 | 5 | const routes: Routes = [ 6 | { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, 7 | { path: 'dashboard', component: DashboardComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forRoot(routes, { useHash: true })], 12 | exports: [RouterModule] 13 | }) 14 | export class AppRoutingModule {} 15 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkihrk/infi-draw/a38c1df1a4a6950ab4b916a28fe96257df870e20/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'], 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { CanvasComponent } from './canvas/canvas.component'; 8 | import { EventDirective } from './shared/directive/event.directive'; 9 | import { ToolBarComponent } from './tool-bar/tool-bar.component'; 10 | import { DashboardComponent } from './dashboard/dashboard.component'; 11 | import { MenuComponent } from './menu/menu.component'; 12 | import { ToolMenuComponent } from './tool-menu/tool-menu.component'; 13 | import { ActiveMenuDirective } from './shared/directive/active-menu.directive'; 14 | import { SlideBrushSizeDirective } from './shared/directive/slide-brush-size.directive'; 15 | 16 | @NgModule({ 17 | declarations: [ 18 | AppComponent, 19 | CanvasComponent, 20 | EventDirective, 21 | ToolBarComponent, 22 | DashboardComponent, 23 | MenuComponent, 24 | ToolMenuComponent, 25 | ActiveMenuDirective, 26 | SlideBrushSizeDirective 27 | ], 28 | imports: [BrowserModule, AppRoutingModule, FontAwesomeModule], 29 | providers: [], 30 | bootstrap: [AppComponent] 31 | }) 32 | export class AppModule {} 33 | -------------------------------------------------------------------------------- /src/app/canvas/canvas.component.html: -------------------------------------------------------------------------------- 1 |
9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/canvas/canvas.component.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | .app-wrapper { 4 | position: relative; 5 | width: 100%; 6 | height: calc(100vh - 55px); 7 | padding-top: $ruler-thickness; 8 | padding-left: $ruler-thickness; 9 | overflow: hidden; 10 | 11 | &.initial-cursor { 12 | cursor: initial; 13 | } 14 | 15 | &.grab-cursor { 16 | cursor: grab; 17 | 18 | &:active { 19 | cursor: grabbing; 20 | } 21 | } 22 | 23 | &.zoom-in-cursor { 24 | cursor: zoom-in; 25 | } 26 | 27 | &.zoom-out-cursor { 28 | cursor: zoom-out; 29 | } 30 | } 31 | 32 | .canvas-wrapper { 33 | width: 100%; 34 | height: 100%; 35 | position: relative; 36 | overflow: hidden; 37 | border: solid 1px $mid-white; 38 | border-right: none; 39 | border-bottom: none; 40 | 41 | pointer-events: none; 42 | 43 | .canvas-render { 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | } 48 | } 49 | 50 | .ruler-wrapper { 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | margin: 0; 55 | padding: 0; 56 | width: 100%; 57 | height: 100%; 58 | pointer-events: none; 59 | 60 | canvas { 61 | position: absolute; 62 | 63 | &.ruler-line { 64 | z-index: 2; 65 | // background-color: red; 66 | } 67 | 68 | &.ruler-column { 69 | z-index: 1; 70 | // background-color: black; 71 | // display: none; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/canvas/canvas.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; 2 | import { RulerService } from '../shared/service/core/ruler.service'; 3 | import { KeyEventService } from '../shared/service/core/key-event.service'; 4 | import { FuncService } from '../shared/service/core/func.service'; 5 | import { Key } from '../shared/model/key.model'; 6 | import { GridService } from '../shared/service/core/grid.service'; 7 | import { GpuService } from '../shared/service/core/gpu.service'; 8 | import { MemoryService } from '../shared/service/core/memory.service'; 9 | import { DebugService } from '../shared/service/util/debug.service'; 10 | import { FlgEventService } from '../shared/service/core/flg-event.service'; 11 | import { CpuService } from '../shared/service/core/cpu.service'; 12 | import { Pointer } from '../shared/model/pointer.model'; 13 | import { DrawService } from '../shared/service/module/draw.service'; 14 | import { UiService } from '../shared/service/core/ui.service'; 15 | 16 | @Component({ 17 | selector: 'app-canvas', 18 | templateUrl: './canvas.component.html', 19 | styleUrls: ['./canvas.component.scss'] 20 | }) 21 | export class CanvasComponent implements OnInit { 22 | @ViewChild('appWrapper', { static: true }) appWrapperRef: ElementRef; 23 | @ViewChild('canvasWrapper', { static: true }) canvasWrapperRef: ElementRef; 24 | @ViewChild('rulerWrapper', { static: true }) rulerWrapperRef: ElementRef; 25 | @ViewChild('canvasMain', { static: true }) mainRef: ElementRef; 26 | @ViewChild('canvasUI', { static: true }) uiRef: ElementRef; 27 | @ViewChild('canvasLine', { static: true }) lineRef: ElementRef; 28 | @ViewChild('canvasColumn', { static: true }) columnRef: ElementRef; 29 | 30 | constructor( 31 | private ruler: RulerService, 32 | private keyevent: KeyEventService, 33 | private func: FuncService, 34 | private grid: GridService, 35 | private cpu: CpuService, 36 | private gpu: GpuService, 37 | private memory: MemoryService, 38 | private debug: DebugService, 39 | private flg: FlgEventService, 40 | private draw: DrawService, 41 | private ui: UiService 42 | ) {} 43 | 44 | ngOnInit(): void { 45 | this.memory.initRenderer( 46 | this.appWrapperRef, 47 | this.canvasWrapperRef, 48 | this.rulerWrapperRef, 49 | this.mainRef, 50 | this.uiRef, 51 | this.lineRef, 52 | this.columnRef 53 | ); 54 | this.render(); 55 | } 56 | 57 | onPointerEvents($event: Pointer): void { 58 | this.flg.updateFlgs($event); 59 | this.cpu.update($event); 60 | } 61 | 62 | onKeyEvents($event: Key): void { 63 | this.keyevent.onKeyEvents($event); 64 | } 65 | 66 | onUnload($event: any): void { 67 | this.func.unload($event); 68 | } 69 | 70 | private render(): void { 71 | const r: FrameRequestCallback = () => { 72 | this._render(); 73 | 74 | requestAnimationFrame(r); 75 | }; 76 | requestAnimationFrame(r); 77 | } 78 | 79 | private _render(): void { 80 | // Module renderer 81 | this.ruler.render(); 82 | this.grid.render(); 83 | this.ui.render(); 84 | this.draw.render(); 85 | 86 | // Main renderer 87 | this.debug.render(); 88 | this.gpu.render(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 |
8 | 9 | 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | 3 | .flex-wrapper { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: stretch; 7 | width: 100vw; 8 | height: 100vh; 9 | border: solid 1px $mid-white; 10 | } 11 | 12 | .flex-left-right { 13 | display: flex; 14 | flex-direction: row; 15 | align-items: stretch; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | .flex-top-bottom { 21 | display: flex; 22 | flex-direction: column; 23 | align-items: stretch; 24 | width:100%; 25 | height: 100%; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dashboard', 5 | templateUrl: './dashboard.component.html', 6 | styleUrls: ['./dashboard.component.scss'] 7 | }) 8 | export class DashboardComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.html: -------------------------------------------------------------------------------- 1 |
2 | 99 |
100 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | 3 | .flex-space-between { 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-between; 7 | align-items: center; 8 | width: 100%; 9 | 10 | user-select: none; 11 | 12 | .prevent-prefix { 13 | position: absolute; 14 | z-index: 0; 15 | top: 0; 16 | left: 0; 17 | width: 100vw; 18 | height: 100vh; 19 | //background-color: rgba(black, 0.5); 20 | } 21 | 22 | .user-name { 23 | border-left: solid 1px $mid-white; 24 | overflow: hidden; 25 | 26 | pointer-events: none; 27 | opacity: 0; 28 | } 29 | 30 | .menu-wrapper { 31 | display: flex; 32 | flex-direction: row; 33 | padding: 0 10px; 34 | height: 100%; 35 | 36 | .menu { 37 | position: relative; 38 | color: $white; 39 | font-size: 0.8rem; 40 | white-space: nowrap; 41 | 42 | .menu-title { 43 | padding: 2.5px 10px; // Since .menu height is 19px and .menu-title height is 14px 44 | 45 | &:hover, 46 | &.active { 47 | background-color: rgba($white, 0.2); 48 | } 49 | } 50 | 51 | .menu-list-title { 52 | display: flex; 53 | justify-content: space-between; 54 | 55 | span { 56 | margin: 4px 8px 4px 25px; 57 | 58 | &:nth-child(2) { 59 | font-size: 0.7rem; 60 | } 61 | } 62 | 63 | &:hover, 64 | &.active { 65 | border-radius: 2px; 66 | background-color: rgba($white, 0.1); 67 | } 68 | 69 | & > * { 70 | pointer-events: none; 71 | } 72 | } 73 | 74 | .separator-wrapper { 75 | pointer-events: none; 76 | 77 | .separator { 78 | width: 100%; 79 | height: 1px; 80 | background-color: rgba($mid-white, 0.5); 81 | margin: 2px 0; 82 | } 83 | } 84 | 85 | .main-menu-list { 86 | top: 100%; 87 | left: 0; 88 | 89 | &.active-stick.active { 90 | opacity: 1; 91 | pointer-events: initial; 92 | } 93 | } 94 | 95 | .sub-menu-list-wrapper { 96 | position: relative; 97 | 98 | .sub-menu-list { 99 | top: -3px; 100 | left: calc(100% + 2px); 101 | 102 | &.active { 103 | opacity: 1; 104 | pointer-events: initial; 105 | transition-delay: 500ms; 106 | } 107 | } 108 | } 109 | 110 | .menu-list { 111 | position: absolute; 112 | z-index: 200; 113 | background-color: $canvas-color; 114 | border: solid 1px $mid-white; 115 | border-radius: 0 0 2px 2px; 116 | min-width: 200px; 117 | font-size: 0.8rem; 118 | padding: 2px; 119 | opacity: 0; 120 | pointer-events: none; 121 | transition: opacity $transition; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MenusComponent } from './menus.component'; 4 | 5 | describe('MenusComponent', () => { 6 | let component: MenusComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MenusComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MenusComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChildren, QueryList, ElementRef } from '@angular/core'; 2 | import { MemoryService } from '../shared/service/core/memory.service'; 3 | import { FuncService } from '../shared/service/core/func.service'; 4 | 5 | // Fontawesome 6 | import { faPenNib } from '@fortawesome/free-solid-svg-icons'; 7 | import { faUser } from '@fortawesome/free-regular-svg-icons'; 8 | import { faCaretRight } from '@fortawesome/free-solid-svg-icons'; 9 | 10 | @Component({ 11 | selector: 'app-menu', 12 | templateUrl: './menu.component.html', 13 | styleUrls: ['./menu.component.scss'] 14 | }) 15 | export class MenuComponent implements OnInit { 16 | @ViewChildren('menuTitlesRef') menuTitlesRef: QueryList; 17 | @ViewChildren('menuListsRef') menuListsRef: QueryList; 18 | @ViewChildren('menuListTitlesRef') menuListTitlesRef: QueryList; 19 | @ViewChildren('subMenuListsRef') subMenuListsRef: QueryList; 20 | @ViewChildren('subMenuListTitlesRef') subMenuListTitlesRef: QueryList; 21 | @ViewChildren('subSubMenuListsRef') subSubMenuListsRef: QueryList; 22 | 23 | menuTitles: string[]; 24 | menuLists: MenuList[][] = []; 25 | subMenuLists: MenuList[][]; 26 | 27 | faPenNib = faPenNib; 28 | faUser = faUser; 29 | faCaretRight = faCaretRight; 30 | 31 | activeStickFlg = false; 32 | 33 | constructor(private memory: MemoryService, private func: FuncService) { 34 | this.menuTitles = ['ファイル', '編集', '変更', '表示', 'ヘルプ']; 35 | this.menuLists.push(this._file()); 36 | this.menuLists.push(this._edit()); 37 | this.menuLists.push(this._modify()); 38 | this.menuLists.push(this._show()); 39 | this.menuLists.push(this._help()); 40 | } 41 | 42 | ngOnInit(): void {} 43 | 44 | initializeActiveStates(): void { 45 | this.toggleActiveSticks(); 46 | this._removeAllActives(); 47 | } 48 | 49 | toggleActiveSticks(): void { 50 | // Sync with other modules 51 | this.memory.states.isCanvasLocked = !this.memory.states.isCanvasLocked; 52 | // Apply to the local state 53 | this.activeStickFlg = this.memory.states.isCanvasLocked; 54 | } 55 | 56 | removeActives($type: string, $event: any): void { 57 | if ($type === 'menu') { 58 | const classList: any = $event.target.classList; 59 | if (classList.contains('menu-title')) { 60 | this._removeAllActives(); 61 | } 62 | 63 | // To set actives to currentTarget after initializtions 64 | const children: HTMLCollection = $event.currentTarget.children; 65 | if ( 66 | children.length === 2 && 67 | !children[0].classList.contains('active') && 68 | !children[1].classList.contains('active') 69 | ) { 70 | children[0].classList.add('active'); 71 | children[1].classList.add('active'); 72 | } 73 | } else if ($type === 'menuList') { 74 | const classList: any = $event.target.classList; 75 | if (classList.contains('menu-list-title')) { 76 | this._removeActiveFromMenuLists(); 77 | this._removeActiveFromSubMenuLists(); 78 | } 79 | } else if ($type === 'subMenuList') { 80 | const classList: any = $event.target.classList; 81 | if (classList.contains('menu-list-title')) { 82 | this._removeActiveFromSubMenuLists(); 83 | } 84 | } 85 | } 86 | 87 | private _removeAllActives(): void { 88 | this._removeActiveFromMenus(); 89 | this._removeActiveFromMenuLists(); 90 | this._removeActiveFromSubMenuLists(); 91 | } 92 | 93 | private _removeActiveFromMenus(): void { 94 | const menuTitles: ElementRef[] = this.menuTitlesRef.toArray(); 95 | for (let i = 0; i < menuTitles.length; i++) { 96 | const menuTitle: HTMLElement = menuTitles[i].nativeElement; 97 | if (menuTitle.classList.contains('active')) menuTitle.classList.remove('active'); 98 | } 99 | 100 | const menuLists: ElementRef[] = this.menuListsRef.toArray(); 101 | for (let i = 0; i < menuLists.length; i++) { 102 | const menuList: HTMLElement = menuLists[i].nativeElement; 103 | if (menuList.classList.contains('active')) menuList.classList.remove('active'); 104 | } 105 | } 106 | 107 | private _removeActiveFromMenuLists(): void { 108 | const menuListTitles: ElementRef[] = this.menuListTitlesRef.toArray(); 109 | for (let i = 0; i < menuListTitles.length; i++) { 110 | const menuListTitle: HTMLElement = menuListTitles[i].nativeElement; 111 | if (menuListTitle.classList.contains('active')) menuListTitle.classList.remove('active'); 112 | } 113 | 114 | const subMenuLists: ElementRef[] = this.subMenuListsRef.toArray(); 115 | for (let i = 0; i < subMenuLists.length; i++) { 116 | const subMenuList: HTMLElement = subMenuLists[i].nativeElement; 117 | if (subMenuList.classList.contains('active')) subMenuList.classList.remove('active'); 118 | } 119 | } 120 | 121 | private _removeActiveFromSubMenuLists(): void { 122 | const subMenuListTitles: ElementRef[] = this.subMenuListTitlesRef.toArray(); 123 | for (let i = 0; i < subMenuListTitles.length; i++) { 124 | const subMenuListTitle: HTMLElement = subMenuListTitles[i].nativeElement; 125 | if (subMenuListTitle.classList.contains('active')) subMenuListTitle.classList.remove('active'); 126 | } 127 | 128 | const subSubMenuLists: ElementRef[] = this.subSubMenuListsRef.toArray(); 129 | for (let i = 0; i < subSubMenuLists.length; i++) { 130 | const subSubMenuList: HTMLElement = subSubMenuLists[i].nativeElement; 131 | if (subSubMenuList.classList.contains('active')) subSubMenuList.classList.remove('active'); 132 | } 133 | } 134 | 135 | private _file(): MenuList[] { 136 | const menuList: MenuList[] = [ 137 | { 138 | title: 'なし', 139 | key: '', 140 | type: 0, 141 | exec: () => { 142 | this.initializeActiveStates(); 143 | }, 144 | subMenuList: [] 145 | } 146 | ]; 147 | 148 | return menuList; 149 | } 150 | 151 | private _edit(): MenuList[] { 152 | const menuList: MenuList[] = [ 153 | { 154 | title: '元に戻す', 155 | key: 'Ctrl+Z', 156 | type: 0, 157 | exec: () => { 158 | this.func.undo(); 159 | this.initializeActiveStates(); 160 | }, 161 | subMenuList: [] 162 | }, 163 | { 164 | title: 'やり直す', 165 | key: 'Shift+Ctrl+Z', 166 | type: 0, 167 | exec: () => { 168 | this.func.redo(); 169 | this.initializeActiveStates(); 170 | }, 171 | subMenuList: [] 172 | }, 173 | { 174 | title: '', 175 | key: '', 176 | type: 2, 177 | exec: () => { 178 | this.initializeActiveStates(); 179 | }, 180 | subMenuList: [] 181 | }, 182 | { 183 | title: '切り取り', 184 | key: 'Ctrl+X', 185 | type: 0, 186 | exec: () => { 187 | this.initializeActiveStates(); 188 | }, 189 | subMenuList: [] 190 | }, 191 | { 192 | title: 'コピー', 193 | key: 'Ctrl+C', 194 | type: 0, 195 | exec: () => { 196 | this.initializeActiveStates(); 197 | }, 198 | subMenuList: [] 199 | }, 200 | { 201 | title: '貼り付け', 202 | key: 'Ctrl+V', 203 | type: 0, 204 | exec: () => { 205 | this.initializeActiveStates(); 206 | }, 207 | subMenuList: [] 208 | }, 209 | { 210 | title: '削除', 211 | key: 'Del', 212 | type: 0, 213 | exec: () => { 214 | this.initializeActiveStates(); 215 | }, 216 | subMenuList: [] 217 | }, 218 | { 219 | title: '複製', 220 | key: 'Ctrl+D', 221 | type: 0, 222 | exec: () => { 223 | this.initializeActiveStates(); 224 | }, 225 | subMenuList: [] 226 | }, 227 | { 228 | title: '', 229 | key: '', 230 | type: 2, 231 | exec: () => {}, 232 | subMenuList: [] 233 | }, 234 | { 235 | title: 'すべて選択', 236 | key: 'Ctrl+A', 237 | type: 0, 238 | exec: () => { 239 | this.initializeActiveStates(); 240 | }, 241 | subMenuList: [] 242 | }, 243 | { 244 | title: 'すべて選択解除', 245 | key: 'Shift+Ctrl+A', 246 | type: 0, 247 | exec: () => { 248 | this.initializeActiveStates(); 249 | }, 250 | subMenuList: [] 251 | }, 252 | { 253 | title: '選択範囲を反転', 254 | key: 'Shift+Ctrl+I', 255 | type: 0, 256 | exec: () => { 257 | this.initializeActiveStates(); 258 | }, 259 | subMenuList: [] 260 | } 261 | ]; 262 | 263 | return menuList; 264 | } 265 | 266 | private _modify(): MenuList[] { 267 | const subMenuListOrder: MenuList[] = [ 268 | { 269 | title: '最前面へ', 270 | key: 'Shift+Ctrl+上矢印', 271 | type: 0, 272 | exec: () => { 273 | this.initializeActiveStates(); 274 | }, 275 | subMenuList: [] 276 | }, 277 | { 278 | title: '一つ前面へ', 279 | key: 'Ctrl+上矢印', 280 | type: 0, 281 | exec: () => { 282 | this.initializeActiveStates(); 283 | }, 284 | subMenuList: [] 285 | }, 286 | { 287 | title: '一つ背面へ', 288 | key: 'Ctrl+下矢印', 289 | type: 0, 290 | exec: () => { 291 | this.initializeActiveStates(); 292 | }, 293 | subMenuList: [] 294 | }, 295 | { 296 | title: '最背面へ', 297 | key: 'Shift+Ctrl+下矢印', 298 | type: 0, 299 | exec: () => { 300 | this.initializeActiveStates(); 301 | }, 302 | subMenuList: [] 303 | } 304 | ]; 305 | 306 | const subMenuListAlign: MenuList[] = [ 307 | { 308 | title: '左揃え', 309 | key: '', 310 | type: 0, 311 | exec: () => { 312 | this.initializeActiveStates(); 313 | }, 314 | subMenuList: [] 315 | }, 316 | { 317 | title: '横の中央揃え', 318 | key: '', 319 | type: 0, 320 | exec: () => { 321 | this.initializeActiveStates(); 322 | }, 323 | subMenuList: [] 324 | }, 325 | { 326 | title: '右揃え', 327 | key: '', 328 | type: 0, 329 | exec: () => { 330 | this.initializeActiveStates(); 331 | }, 332 | subMenuList: [] 333 | }, 334 | { 335 | title: '', 336 | key: '', 337 | type: 2, 338 | exec: () => {}, 339 | subMenuList: [] 340 | }, 341 | { 342 | title: '上揃え', 343 | key: '', 344 | type: 0, 345 | exec: () => { 346 | this.initializeActiveStates(); 347 | }, 348 | subMenuList: [] 349 | }, 350 | { 351 | title: '縦の中央揃え', 352 | key: '', 353 | type: 0, 354 | exec: () => { 355 | this.initializeActiveStates(); 356 | }, 357 | subMenuList: [] 358 | }, 359 | { 360 | title: '下揃え', 361 | key: '', 362 | type: 0, 363 | exec: () => { 364 | this.initializeActiveStates(); 365 | }, 366 | subMenuList: [] 367 | } 368 | ]; 369 | 370 | const subMenuListTransform: MenuList[] = [ 371 | { 372 | title: '左に45°回転', 373 | key: '', 374 | type: 0, 375 | exec: () => { 376 | this.initializeActiveStates(); 377 | }, 378 | subMenuList: [] 379 | }, 380 | { 381 | title: '左に90°回転', 382 | key: '', 383 | type: 0, 384 | exec: () => { 385 | this.initializeActiveStates(); 386 | }, 387 | subMenuList: [] 388 | }, 389 | { 390 | title: '左に180°回転', 391 | key: '', 392 | type: 0, 393 | exec: () => { 394 | this.initializeActiveStates(); 395 | }, 396 | subMenuList: [] 397 | }, 398 | { 399 | title: '', 400 | key: '', 401 | type: 2, 402 | exec: () => {}, 403 | subMenuList: [] 404 | }, 405 | { 406 | title: '右に45°回転', 407 | key: '', 408 | type: 0, 409 | exec: () => { 410 | this.initializeActiveStates(); 411 | }, 412 | subMenuList: [] 413 | }, 414 | { 415 | title: '右に90°回転', 416 | key: '', 417 | type: 0, 418 | exec: () => { 419 | this.initializeActiveStates(); 420 | }, 421 | subMenuList: [] 422 | }, 423 | { 424 | title: '右に180°回転', 425 | key: '', 426 | type: 0, 427 | exec: () => { 428 | this.initializeActiveStates(); 429 | }, 430 | subMenuList: [] 431 | }, 432 | { 433 | title: '', 434 | key: '', 435 | type: 2, 436 | exec: () => {}, 437 | subMenuList: [] 438 | }, 439 | { 440 | title: '垂直方向にミラー化', 441 | key: '', 442 | type: 0, 443 | exec: () => { 444 | this.initializeActiveStates(); 445 | }, 446 | subMenuList: [] 447 | }, 448 | { 449 | title: '水平方向にミラー化', 450 | key: '', 451 | type: 0, 452 | exec: () => { 453 | this.initializeActiveStates(); 454 | }, 455 | subMenuList: [] 456 | } 457 | ]; 458 | 459 | const menuList: MenuList[] = [ 460 | { 461 | title: '重ね順', 462 | key: '', 463 | type: 1, 464 | exec: () => {}, 465 | subMenuList: subMenuListOrder 466 | }, 467 | { 468 | title: '配置', 469 | key: '', 470 | type: 1, 471 | exec: () => {}, 472 | subMenuList: subMenuListAlign 473 | }, 474 | { 475 | title: '移動', 476 | key: '', 477 | type: 1, 478 | exec: () => {}, 479 | subMenuList: subMenuListTransform 480 | } 481 | ]; 482 | 483 | return menuList; 484 | } 485 | 486 | private _show(): MenuList[] { 487 | const menuList: MenuList[] = [ 488 | { 489 | title: '元のビュー', 490 | key: 'Ctrl+O', 491 | type: 0, 492 | exec: () => { 493 | this.initializeActiveStates(); 494 | }, 495 | subMenuList: [] 496 | }, 497 | { 498 | title: '選択範囲を画面に合わせる', 499 | key: '', 500 | type: 0, 501 | exec: () => { 502 | this.initializeActiveStates(); 503 | }, 504 | subMenuList: [] 505 | }, 506 | { 507 | title: '全体を画面に合わせる', 508 | key: 'Alt+Ctrl+O', 509 | type: 0, 510 | exec: () => { 511 | this.initializeActiveStates(); 512 | }, 513 | subMenuList: [] 514 | }, 515 | { 516 | title: '', 517 | key: '', 518 | type: 2, 519 | exec: () => {}, 520 | subMenuList: [] 521 | }, 522 | { 523 | title: '拡大', 524 | key: 'Ctrl+Space+ドラッグ', 525 | type: 0, 526 | exec: () => { 527 | this.initializeActiveStates(); 528 | }, 529 | subMenuList: [] 530 | }, 531 | { 532 | title: '縮小', 533 | key: 'Ctrl+Alt+Space+ドラッグ', 534 | type: 0, 535 | exec: () => { 536 | this.initializeActiveStates(); 537 | }, 538 | subMenuList: [] 539 | } 540 | ]; 541 | 542 | return menuList; 543 | } 544 | 545 | private _help(): MenuList[] { 546 | const subMenuListSupport: MenuList[] = [ 547 | { 548 | title: 'お問い合わせ', 549 | key: '', 550 | type: 0, 551 | exec: () => { 552 | this.initializeActiveStates(); 553 | }, 554 | subMenuList: [] 555 | }, 556 | { 557 | title: '開発者に詳細を送信', 558 | key: '', 559 | type: 0, 560 | exec: () => { 561 | this.initializeActiveStates(); 562 | }, 563 | subMenuList: [] 564 | } 565 | ]; 566 | 567 | const subMenuListLang: MenuList[] = [ 568 | { 569 | title: '日本語', 570 | key: '', 571 | type: 0, 572 | exec: () => { 573 | this.initializeActiveStates(); 574 | }, 575 | subMenuList: [] 576 | }, 577 | { 578 | title: 'English', 579 | key: '', 580 | type: 0, 581 | exec: () => { 582 | this.initializeActiveStates(); 583 | }, 584 | subMenuList: [] 585 | } 586 | ]; 587 | 588 | const menuList: MenuList[] = [ 589 | { 590 | title: 'サポート', 591 | key: '', 592 | type: 1, 593 | exec: () => {}, 594 | subMenuList: subMenuListSupport 595 | }, 596 | { 597 | title: '言語', 598 | key: '', 599 | type: 1, 600 | exec: () => {}, 601 | subMenuList: subMenuListLang 602 | }, 603 | { 604 | title: '更新情報', 605 | key: '', 606 | type: 0, 607 | exec: () => { 608 | this.initializeActiveStates(); 609 | }, 610 | subMenuList: [] 611 | } 612 | ]; 613 | 614 | return menuList; 615 | } 616 | } 617 | 618 | interface MenuList { 619 | title: string; 620 | key: string; 621 | type: number; // 0: menu-list, 1: sub-menu-list, 2: separator 622 | exec: Function; 623 | subMenuList: MenuList[]; 624 | } 625 | -------------------------------------------------------------------------------- /src/app/shared/directive/active-menu.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener, ElementRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appActiveMenu]' 5 | }) 6 | export class ActiveMenuDirective { 7 | private target: HTMLElement; 8 | 9 | constructor(el: ElementRef) { 10 | this.target = el.nativeElement; 11 | } 12 | 13 | // Pointerenter listener 14 | @HostListener('pointerenter', ['$event']) onPointerEnter($e): void { 15 | const children: HTMLCollection = this.target.children; 16 | 17 | if ( 18 | children.length === 2 && 19 | !children[0].classList.contains('active') && 20 | !children[1].classList.contains('active') 21 | ) { 22 | children[0].classList.add('active'); 23 | children[1].classList.add('active'); 24 | } 25 | } 26 | 27 | // Pointerleave listener 28 | @HostListener('pointerleave', ['$event']) onPointerLeave($e): void { 29 | const isAllowedToRemoveActive = !!$e.relatedTarget 30 | ? !$e.relatedTarget.classList.contains('prevent-pointer-leave') 31 | : false; 32 | 33 | if (isAllowedToRemoveActive) { 34 | const children: HTMLCollection = this.target.children; 35 | 36 | if (children.length === 2) { 37 | if (children[0].classList.contains('active')) { 38 | children[0].classList.remove('active'); 39 | } 40 | if (children[1].classList.contains('active')) { 41 | children[1].classList.remove('active'); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/shared/directive/click-stop-propagation.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appClickStopPropagation]' 5 | }) 6 | export class ClickStopPropagationDirective { 7 | constructor() {} 8 | 9 | @HostListener('clickj', ['$event']) onClick($e: any): void { 10 | $e.stopPropagation(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/directive/event.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; 2 | import { Key } from '../model/key.model'; 3 | import { Pointer } from '../model/pointer.model'; 4 | 5 | @Directive({ 6 | selector: '[appEvent]' 7 | }) 8 | export class EventDirective { 9 | @Output() dataSet = new EventEmitter(); 10 | @Output() key = new EventEmitter(); 11 | @Output() isUnload = new EventEmitter(); 12 | 13 | // Mouse position 14 | private clientX = 0; 15 | private clientY = 0; 16 | 17 | // Wheel delta 18 | private delta = 0; 19 | // Mouse button number 20 | private btn = 0; 21 | 22 | // Flgs 23 | private wheelFlg = false; 24 | private downFlg = false; 25 | private moveFlg = false; 26 | private keyDownFlg = false; 27 | private dblClickFlg = false; 28 | 29 | constructor() {} 30 | 31 | _emitData($clientX: number, $clientY: number): void { 32 | this.dataSet.emit({ 33 | x: $clientX, 34 | y: $clientY, 35 | delta: this.delta, 36 | btn: this.btn, 37 | wheelFlg: this.wheelFlg, 38 | downFlg: this.downFlg, 39 | moveFlg: this.moveFlg, 40 | dblClickFlg: this.dblClickFlg 41 | }); 42 | } 43 | 44 | _resetAllFlgs(): void { 45 | this.downFlg = false; 46 | this.moveFlg = false; 47 | this.keyDownFlg = false; 48 | this.dblClickFlg = false; 49 | } 50 | 51 | // Window unload listener 52 | @HostListener('window:unload', ['$event']) onUnload($e): void { 53 | // console.log($e.returnValue); 54 | // $e.returnValue = true; 55 | 56 | this.isUnload.emit($e); 57 | } 58 | 59 | // Window unload listener 60 | @HostListener('window:beforeunload', ['$event']) onBeforeUnload($e): void { 61 | // console.log($e.returnValue); 62 | // $e.returnValue = true; 63 | 64 | this.isUnload.emit($e); 65 | } 66 | 67 | // Keydown listener 68 | @HostListener('document:keydown', ['$event']) onKeyDown($e): void { 69 | this.keyDownFlg = true; 70 | this.key.emit({ 71 | key: $e.key, 72 | type: $e.type, 73 | e: $e, 74 | x: this.clientX, 75 | y: this.clientY, 76 | keyDownFlg: this.keyDownFlg, 77 | downFlg: this.downFlg, 78 | moveFlg: this.moveFlg 79 | }); 80 | } 81 | 82 | // Keyup listener 83 | @HostListener('document:keyup', ['$event']) onKeyUp($e): void { 84 | this.keyDownFlg = false; 85 | this.key.emit({ 86 | key: $e.key, 87 | type: $e.type, 88 | e: $e, 89 | x: this.clientX, 90 | y: this.clientY, 91 | keyDownFlg: this.keyDownFlg, 92 | downFlg: this.downFlg, 93 | moveFlg: this.moveFlg 94 | }); 95 | } 96 | 97 | // Pointerdown listener 98 | @HostListener('pointerdown', ['$event']) onPointerDown($e): void { 99 | this._onDown($e); 100 | } 101 | 102 | // Pointerup listener 103 | @HostListener('document:pointerup', ['$event']) onPointerUp($e): void { 104 | this._onUp($e); 105 | } 106 | 107 | // Pointermove listener 108 | @HostListener('document:pointermove', ['$event']) onPointerMove($e): void { 109 | this._onMove($e); 110 | } 111 | 112 | // Touchstart listener 113 | @HostListener('touchstart', ['$event']) onTouchStart($e): void { 114 | this._onDown($e); 115 | } 116 | 117 | // Touchend listener 118 | @HostListener('document:touchend', ['$event']) onTouchEnd($e): void { 119 | this._onUp($e); 120 | } 121 | 122 | // Touchmove listener 123 | @HostListener('document:touchmove', ['$event']) onTouchMove($e): void { 124 | this._onMove($e); 125 | } 126 | 127 | // Mousemove listener 128 | @HostListener('dblclick', ['$event']) onDoubleClick($e): void { 129 | const clientX = $e.clientX; 130 | const clientY = $e.clientY; 131 | 132 | // Initialize flags 133 | this._resetAllFlgs(); 134 | this.dblClickFlg = true; 135 | this._emitData(clientX, clientY); 136 | 137 | // To prevent permanent zooming 138 | this.dblClickFlg = false; 139 | } 140 | 141 | // Wheel listener 142 | @HostListener('wheel', ['$event']) onMouseWheel($e): void { 143 | $e.preventDefault(); 144 | 145 | const clientX = $e.clientX; 146 | const clientY = $e.clientY; 147 | 148 | this.wheelFlg = true; 149 | this.delta = $e.deltaY; 150 | this._emitData(clientX, clientY); 151 | // To prevent permanent zooming 152 | this.wheelFlg = false; 153 | } 154 | 155 | // Contextmenu listener 156 | @HostListener('document:contextmenu', ['$event']) onContextMenu($e): void { 157 | $e.preventDefault(); 158 | } 159 | 160 | // Down event 161 | _onDown($e: any): void { 162 | let clientX: number; 163 | let clientY: number; 164 | 165 | if ($e.type === 'touchstart') { 166 | clientX = $e.touches[0].clientX; 167 | clientY = $e.touches[0].clientY; 168 | this.btn = 0; 169 | } else { 170 | clientX = $e.clientX; 171 | clientY = $e.clientY; 172 | this.btn = $e.button; 173 | } 174 | 175 | this.clientX = clientX; 176 | this.clientY = clientY; 177 | 178 | // Initialize flags 179 | this._resetAllFlgs(); 180 | this.downFlg = true; 181 | this._emitData(clientX, clientY); 182 | } 183 | 184 | // Up event 185 | _onUp($e: any): void { 186 | let clientX: number; 187 | let clientY: number; 188 | 189 | if ($e.type === 'touchend') { 190 | clientX = $e.changedTouches[0].clientX; 191 | clientY = $e.changedTouches[0].clientY; 192 | } else { 193 | clientX = $e.clientX; 194 | clientY = $e.clientY; 195 | } 196 | 197 | this.clientX = clientX; 198 | this.clientY = clientY; 199 | 200 | // Initialize flags 201 | this._resetAllFlgs(); 202 | this._emitData(clientX, clientY); 203 | } 204 | 205 | // Move event 206 | _onMove($e: any): void { 207 | let clientX: number; 208 | let clientY: number; 209 | 210 | if ($e.type === 'touchmove') { 211 | clientX = $e.touches[0].clientX; 212 | clientY = $e.touches[0].clientY; 213 | } else { 214 | clientX = $e.clientX; 215 | clientY = $e.clientY; 216 | } 217 | 218 | this.clientX = clientX; 219 | this.clientY = clientY; 220 | 221 | this.moveFlg = true; 222 | this._emitData(clientX, clientY); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/app/shared/directive/menu.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appMenu]' 5 | }) 6 | export class MenuDirective { 7 | @Output() isHover = new EventEmitter(); 8 | @Output() isPointerDown = new EventEmitter(); 9 | 10 | constructor() {} 11 | 12 | // Pointerenter listener 13 | @HostListener('pointerenter', ['$event']) onPointerEnter($e): void { 14 | $e.stopPropagation(); 15 | this.isHover.emit(true); 16 | } 17 | 18 | // Pointerenter listener 19 | @HostListener('pointerleave', ['$event']) onPointerLeave($e): void { 20 | $e.stopPropagation(); 21 | this.isHover.emit(false); 22 | } 23 | // Pointerdown listener 24 | @HostListener('document:pointerdown', ['$event']) onPointerdown($e): void { 25 | $e.stopPropagation(); 26 | this.isPointerDown.emit(true); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/directive/slide-brush-size.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener } from '@angular/core'; 2 | import { SlideBrushSizeService } from '../service/module/slide-brush-size.service'; 3 | 4 | @Directive({ 5 | selector: '[appSlideBrushSize]' 6 | }) 7 | export class SlideBrushSizeDirective { 8 | constructor(private slider: SlideBrushSizeService) {} 9 | 10 | // Pointerdown listener 11 | @HostListener('pointerdown', ['$event']) onPinterDown($e): void { 12 | this._onDown($e); 13 | } 14 | 15 | // Pointerup listener 16 | @HostListener('document:pointerup', ['$event']) onPointerUp($e): void { 17 | this._onUp($e); 18 | } 19 | 20 | // Pointermove listener 21 | @HostListener('document:pointermove', ['$event']) onPointerMove($e): void { 22 | this._onMove($e); 23 | } 24 | 25 | // Touchstart listener 26 | @HostListener('touchstart', ['$event']) onTouchStart($e): void { 27 | this._onDown($e); 28 | } 29 | 30 | // Touchend listener 31 | @HostListener('document:touchend', ['$event']) onTouchEnd($e): void { 32 | this._onUp($e); 33 | } 34 | 35 | // Touchmove listener 36 | @HostListener('document:touchmove', ['$event']) onTouchMove($e): void { 37 | this._onMove($e); 38 | } 39 | 40 | // Down event 41 | _onDown($e: any): void { 42 | let clientX: number; 43 | 44 | if ($e.type === 'touchmove') { 45 | clientX = $e.touches[0].clientX; 46 | } else { 47 | clientX = $e.clientX; 48 | } 49 | 50 | this.slider.activate(clientX); 51 | } 52 | 53 | // Up event 54 | _onUp($e: any): void { 55 | this.slider.disableSlider(); 56 | } 57 | 58 | // Move event 59 | _onMove($e: any): void { 60 | let clientX: number; 61 | 62 | if ($e.type === 'touchmove') { 63 | clientX = $e.touches[0].clientX; 64 | } else { 65 | clientX = $e.clientX; 66 | } 67 | 68 | this.slider.changeSlideAmount(clientX); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/shared/model/canvas-offset.model.ts: -------------------------------------------------------------------------------- 1 | import { Offset } from './offset.model'; 2 | 3 | export interface CanvasOffset extends Offset { 4 | zoomRatio: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/model/erase.model.ts: -------------------------------------------------------------------------------- 1 | export interface Erase { 2 | id: number; 3 | trailList: { trailId: number; pointIdList: number[] }[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/model/flgs.model.ts: -------------------------------------------------------------------------------- 1 | export interface Flgs { 2 | dblClickFlg: boolean; 3 | downFlg: boolean; 4 | // - Similarly to mousedown events 5 | leftDownFlg: boolean; 6 | middleDownFlg: boolean; 7 | rightDownFlg: boolean; 8 | // - Similarly to mouseup events 9 | leftUpFlg: boolean; 10 | middleUpFlg: boolean; 11 | rightUpFlg: boolean; 12 | // - Similarly to mousedown + mousemove events 13 | leftDownMoveFlg: boolean; 14 | middleDownMoveFlg: boolean; 15 | rightDownMoveFlg: boolean; 16 | // - Similarly to wheel event 17 | wheelFlg: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/model/history.model.ts: -------------------------------------------------------------------------------- 1 | import { Trail } from '../model/trail.model'; 2 | 3 | export interface History { 4 | trailList: Trail[]; 5 | isChangedStates: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/model/key.model.ts: -------------------------------------------------------------------------------- 1 | export interface Key { 2 | key: string; 3 | type: string; 4 | e: any; 5 | x: number; 6 | y: number; 7 | keyDownFlg: boolean; 8 | downFlg: boolean; 9 | moveFlg: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/model/offset.model.ts: -------------------------------------------------------------------------------- 1 | export interface Offset { 2 | prevOffsetX: number; 3 | prevOffsetY: number; 4 | newOffsetX: number; 5 | newOffsetY: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/model/point.model.ts: -------------------------------------------------------------------------------- 1 | import { Offset } from './offset.model'; 2 | 3 | export interface Point { 4 | id: number; 5 | color: string; 6 | visibility: boolean; 7 | relativeOffset: { 8 | x: number; 9 | y: number; 10 | }; 11 | pressure: number; 12 | lineWidth: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/model/pointer-offset.model.ts: -------------------------------------------------------------------------------- 1 | export interface PointerOffset { 2 | current: { 3 | x: number; 4 | y: number; 5 | }; 6 | prev: { 7 | x: number; 8 | y: number; 9 | }; 10 | raw: { 11 | x: number; 12 | y: number; 13 | }; 14 | tmp: { 15 | x: number; 16 | y: number; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/model/pointer.model.ts: -------------------------------------------------------------------------------- 1 | export interface Pointer { 2 | x: number; 3 | y: number; 4 | delta: number; 5 | btn: number; 6 | wheelFlg: boolean; 7 | downFlg: boolean; 8 | moveFlg: boolean; 9 | dblClickFlg: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/model/trail.model.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './point.model'; 2 | import { Offset } from './offset.model'; 3 | 4 | export interface Trail { 5 | id: number; 6 | colorId: string; 7 | name: string; 8 | visibility: boolean; 9 | min: { x: number; y: number }; 10 | max: { x: number; y: number }; 11 | origin: Offset; 12 | points: Point[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/service/core/canvas.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Pointer } from '../../model/pointer.model'; 3 | import { CoordService } from '../util/coord.service'; 4 | import { MemoryService } from '../../service/core/memory.service'; 5 | import { Offset } from '../../model/offset.model'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class CanvasService { 11 | constructor(private memory: MemoryService, private coord: CoordService) {} 12 | 13 | registerOnMouseDown(): void { 14 | this.memory.canvasOffset.prevOffsetX = this.memory.canvasOffset.newOffsetX; 15 | this.memory.canvasOffset.prevOffsetY = this.memory.canvasOffset.newOffsetY; 16 | } 17 | 18 | registerOnNoMouseDown(): void { 19 | this.registerOnMouseDown(); 20 | } 21 | 22 | registerOnWheel($event: Pointer): void { 23 | this._updateOffset(0, 0, $event); 24 | } 25 | 26 | registerOnMouseMiddleMove($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 27 | this._updateOffset($newOffsetX, $newOffsetY, $event); 28 | } 29 | 30 | private _updateOffset($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 31 | const offset: Offset = this.coord.updateOffset($newOffsetX, $newOffsetY, this.memory.canvasOffset, $event); 32 | let zoomRatio: number = this.memory.canvasOffset.zoomRatio; 33 | 34 | if (this.memory.flgs.wheelFlg) { 35 | zoomRatio = this.coord.updateZoomRatioByWheel(this.memory.canvasOffset.zoomRatio, $event); 36 | } 37 | 38 | this.memory.canvasOffset = { ...offset, zoomRatio }; 39 | } 40 | 41 | updateOffsetByZoom($x: number, $y: number, $deltaFlg: boolean): void { 42 | // Update prevOffsets 43 | this.registerOnNoMouseDown(); 44 | 45 | const offset: Offset = this.coord.updateOffsetWithGivenPoint($x, $y, this.memory.canvasOffset, $deltaFlg); 46 | const zoomRatio: number = this.coord.updateZoomRatioByPointer(this.memory.canvasOffset.zoomRatio, $deltaFlg); 47 | 48 | this.memory.canvasOffset = { ...offset, zoomRatio }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/shared/service/core/cpu.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Pointer } from '../../model/pointer.model'; 3 | import { MemoryService } from './memory.service'; 4 | import { RegisterService } from './register.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class CpuService { 10 | // Watch wheel events to detect an end 11 | private wInterval = 100; 12 | private wCounter1 = 0; 13 | private wCounter2 = 0; 14 | private wMaker = true; 15 | 16 | // Detect idling of pointer events 17 | private idleTimer: number; 18 | private idleInterval = 1; 19 | 20 | constructor(private memory: MemoryService, private register: RegisterService) {} 21 | 22 | //////////////////////////////////////////////////////////////////////////////////////////// 23 | // 24 | // CPU 25 | // 26 | //////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | update($event: Pointer): void { 29 | // Update ouseOffset 30 | this.memory.pointerOffset.current.x = $event.x - this.memory.renderer.canvasWrapper.getBoundingClientRect().left; 31 | this.memory.pointerOffset.current.y = $event.y - this.memory.renderer.canvasWrapper.getBoundingClientRect().top; 32 | this.memory.pointerOffset.raw.x = $event.x; 33 | this.memory.pointerOffset.raw.y = $event.y; 34 | 35 | // When pointer button is down 36 | if (this.memory.flgs.leftDownFlg || this.memory.flgs.middleDownFlg || this.memory.flgs.rightDownFlg) { 37 | this._onPointerDown(); 38 | } 39 | 40 | // When pointer button is up 41 | if (this.memory.flgs.leftUpFlg || this.memory.flgs.middleUpFlg || this.memory.flgs.rightUpFlg) { 42 | this._onPointerUp(); 43 | } 44 | 45 | // Update anytime when the event is not pointerdown 46 | if (!this.memory.flgs.downFlg) { 47 | this._onNoPointerDown($event); 48 | } 49 | 50 | // When double clicked 51 | if (this.memory.flgs.dblClickFlg) { 52 | } 53 | 54 | // Only allow left and middle buttons 55 | if (this.memory.flgs.leftDownMoveFlg || this.memory.flgs.middleDownMoveFlg) { 56 | this._onPointerMove($event); 57 | } 58 | } 59 | 60 | ////////////////////////////////////////////////////////// 61 | // 62 | // Pointerdown event 63 | // 64 | ////////////////////////////////////////////////////////// 65 | 66 | _onPointerDown(): void { 67 | // Pointerdown event with no pointermove 68 | this.memory.pointerOffset.prev.x = this.memory.pointerOffset.current.x; 69 | this.memory.pointerOffset.prev.y = this.memory.pointerOffset.current.y; 70 | this.memory.pointerOffset.tmp.x = this.memory.pointerOffset.current.x; 71 | this.memory.pointerOffset.tmp.y = this.memory.pointerOffset.current.y; 72 | 73 | this.register.onPointerDown(); 74 | 75 | this.memory.states.isNeededToUpdateHistory = true; 76 | } 77 | 78 | private _onShadowPointerDown(): void { 79 | // Pointerdown event with no pointermove 80 | this.memory.pointerOffset.tmp.x = this.memory.pointerOffset.current.x; 81 | this.memory.pointerOffset.tmp.y = this.memory.pointerOffset.current.y; 82 | } 83 | 84 | ////////////////////////////////////////////////////////// 85 | // 86 | // Pointerup event 87 | // 88 | ////////////////////////////////////////////////////////// 89 | 90 | _onPointerUp(): void { 91 | this.register.onPointerUp(); 92 | 93 | // Prevent infinite iteration on histroy updating 94 | this.memory.states.isNeededToUpdateHistory = false; 95 | } 96 | 97 | ////////////////////////////////////////////////////////// 98 | // 99 | // All events but pointerdown 100 | // 101 | ////////////////////////////////////////////////////////// 102 | 103 | _onNoPointerDown($event: Pointer): void { 104 | // Wheel event - zooming-in/out 105 | if (this.memory.flgs.wheelFlg) { 106 | // Watch wheel events to detect an end of the event 107 | this.detectWheelEnd(); 108 | } 109 | 110 | this.register.onNoPointerDown($event); 111 | } 112 | 113 | // https://jsfiddle.net/rafaylik/sLjyyfox/ 114 | private detectWheelEnd(): void { 115 | // if (this.wCounter1 === 0) this.memory.pileNewHistory(this.memory.history); 116 | this.wCounter1 += 1; 117 | if (this.wMaker) this._wheelStart(); 118 | } 119 | 120 | private _wheelStart(): void { 121 | this.wMaker = false; 122 | this._wheelAct(); 123 | } 124 | 125 | private _wheelAct(): void { 126 | this.wCounter2 = this.wCounter1; 127 | setTimeout(() => { 128 | if (this.wCounter2 === this.wCounter1) { 129 | this._wheelEnd(); 130 | } else { 131 | this._wheelAct(); 132 | } 133 | }, this.wInterval); 134 | } 135 | 136 | private _wheelEnd(): void { 137 | this.wCounter1 = 0; 138 | this.wCounter2 = 0; 139 | this.wMaker = true; 140 | } 141 | 142 | ////////////////////////////////////////////////////////// 143 | // 144 | // Pointermove event 145 | // 146 | ////////////////////////////////////////////////////////// 147 | 148 | _onPointerMove($event: Pointer): void { 149 | // Pointermove event with pointerdown (Wheel event is excluded) 150 | if (this.memory.flgs.wheelFlg) return; 151 | 152 | // Check if its idling 153 | this._onIdle($event); 154 | 155 | ////////////////////////////////////////////////////////// 156 | // 157 | // Pile new history 158 | // 159 | ////////////////////////////////////////////////////////// 160 | 161 | if ( 162 | this.memory.states.isNeededToUpdateHistory && 163 | this.memory.reservedByFunc.current.group === 'brush' && 164 | this.memory.flgs.leftDownMoveFlg 165 | ) { 166 | this.memory.pileNewHistory(); 167 | } 168 | 169 | const newOffsetX: number = this.memory.pointerOffset.current.x - this.memory.pointerOffset.prev.x; 170 | const newOffsetY: number = this.memory.pointerOffset.current.y - this.memory.pointerOffset.prev.y; 171 | 172 | this.register.onPointerMove(newOffsetX, newOffsetY, $event); 173 | 174 | // Prevent infinite iteration on histroy updating 175 | this.memory.states.isNeededToUpdateHistory = false; 176 | } 177 | 178 | private _onIdle($event: Pointer): void { 179 | if (!!this.idleTimer) clearInterval(this.idleTimer); 180 | this.idleTimer = setTimeout(() => { 181 | this._onShadowPointerDown(); 182 | }, this.idleInterval); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/app/shared/service/core/cursor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from './memory.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class CursorService { 8 | constructor(private memory: MemoryService) {} 9 | 10 | render($ctx: CanvasRenderingContext2D): void { 11 | const group: string = this.memory.reservedByFunc.current.group; 12 | 13 | if (group === 'brush') { 14 | this._brush($ctx); 15 | } else { 16 | const name: string = this.memory.reservedByFunc.current.name; 17 | 18 | if (name === 'hand') { 19 | this._hand(); 20 | } else if (name === 'zoom') { 21 | this._zoom(); 22 | } else { 23 | this._resetAppWrapperClass(); 24 | } 25 | } 26 | } 27 | 28 | private _resetAppWrapperClass(): void { 29 | const appWrapper: HTMLDivElement = this.memory.renderer.appWrapper; 30 | appWrapper.className = ''; 31 | appWrapper.classList.add('app-wrapper'); 32 | } 33 | 34 | private _hand(): void { 35 | const appWrapper: HTMLDivElement = this.memory.renderer.appWrapper; 36 | 37 | if (appWrapper.classList.contains('grab-cursor')) return; 38 | 39 | this._resetAppWrapperClass(); 40 | appWrapper.classList.add('grab-cursor'); 41 | } 42 | 43 | private _zoom(): void { 44 | const appWrapper: HTMLDivElement = this.memory.renderer.appWrapper; 45 | 46 | if (this.memory.states.isZoomCursorPositive) { 47 | if (appWrapper.classList.contains('zoom-in-cursor')) return; 48 | 49 | this._resetAppWrapperClass(); 50 | appWrapper.classList.add('zoom-in-cursor'); 51 | } else { 52 | if (appWrapper.classList.contains('zoom-out-cursor')) return; 53 | 54 | this._resetAppWrapperClass(); 55 | appWrapper.classList.add('zoom-out-cursor'); 56 | } 57 | } 58 | 59 | private _brush($ctx: CanvasRenderingContext2D): void { 60 | const rawX: number = this.memory.pointerOffset.raw.x; 61 | const rawY: number = this.memory.pointerOffset.raw.y; 62 | const canvasX: number = this.memory.renderer.main.getBoundingClientRect().x; 63 | const canvasY: number = this.memory.renderer.main.getBoundingClientRect().y; 64 | const isCanvas: boolean = canvasX < rawX && canvasY < rawY; 65 | 66 | const appWrapper: HTMLDivElement = this.memory.renderer.appWrapper; 67 | if (appWrapper.classList.length > 1) { 68 | this._resetAppWrapperClass(); 69 | } 70 | 71 | if (!isCanvas || this.memory.states.isCanvasLocked) return; 72 | 73 | const type: string = this.memory.reservedByFunc.current.type; 74 | const x: number = this.memory.pointerOffset.current.x; 75 | const y: number = this.memory.pointerOffset.current.y; 76 | 77 | let r = 0; 78 | if (type === 'draw') { 79 | r = (this.memory.brush.lineWidth.draw * this.memory.canvasOffset.zoomRatio) / 2; 80 | } else if (type === 'erase') { 81 | r = this.memory.brush.lineWidth.erase / 2; 82 | } 83 | 84 | if (r > 0) { 85 | $ctx.beginPath(); 86 | $ctx.strokeStyle = this.memory.constant.STROKE_STYLE; 87 | $ctx.lineWidth = 1; 88 | $ctx.arc(x, y, r, 0, 2 * Math.PI); 89 | $ctx.stroke(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/app/shared/service/core/flg-event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Flgs } from '../../model/flgs.model'; 3 | import { MemoryService } from './memory.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class FlgEventService { 9 | constructor(private memory: MemoryService) {} 10 | 11 | updateFlgs($event: any): void { 12 | const flgs: Flgs = { 13 | dblClickFlg: $event.dblClickFlg, 14 | downFlg: $event.downFlg, 15 | // - Similarly to mousedown events 16 | leftDownFlg: $event.downFlg && !$event.moveFlg && $event.btn === 0, 17 | middleDownFlg: $event.downFlg && !$event.moveFlg && $event.btn === 1, 18 | rightDownFlg: $event.downFlg && !$event.moveFlg && $event.btn === 2, 19 | // - Similarly to mouseup events 20 | leftUpFlg: !$event.downFlg && !$event.moveFlg && $event.btn === 0, 21 | middleUpFlg: !$event.downFlg && !$event.moveFlg && $event.btn === 1, 22 | rightUpFlg: !$event.downFlg && !$event.moveFlg && $event.btn === 2, 23 | // - Similarly to mousedown + mousemove events 24 | leftDownMoveFlg: $event.downFlg && $event.moveFlg && $event.btn === 0, 25 | middleDownMoveFlg: $event.downFlg && $event.moveFlg && $event.btn === 1, 26 | rightDownMoveFlg: $event.downFlg && $event.moveFlg && $event.btn === 2, 27 | // Similarly to wheel event 28 | wheelFlg: $event.wheelFlg 29 | }; 30 | 31 | this.memory.flgs = flgs; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/shared/service/core/func.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from './memory.service'; 3 | import { CleanupService } from '../module/cleanup.service'; 4 | import { SelectService } from '../module/select.service'; 5 | import { PenService } from '../module/pen.service'; 6 | import { EraseService } from '../module/erase.service'; 7 | import { CreateSquareService } from '../module/create-square.service'; 8 | import { CreateLineService } from '../module/create-line.service'; 9 | import { ZoomService } from '../module/zoom.service'; 10 | import { Erase } from '../../model/erase.model'; 11 | import { Trail } from '../../model/trail.model'; 12 | import * as _ from 'lodash'; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class FuncService { 18 | constructor( 19 | private memory: MemoryService, 20 | private cleanupFunc: CleanupService, 21 | private selectFunc: SelectService, 22 | private penFunc: PenService, 23 | private eraseFunc: EraseService, 24 | private createSquareFunc: CreateSquareService, 25 | private createLineFunc: CreateLineService, 26 | private zoomFunc: ZoomService 27 | ) {} 28 | 29 | ////////////////////////////////////////////////////////// 30 | // 31 | // Save 32 | // 33 | ////////////////////////////////////////////////////////// 34 | 35 | save(): void {} 36 | 37 | ////////////////////////////////////////////////////////// 38 | // 39 | // Undo / redo 40 | // 41 | ////////////////////////////////////////////////////////// 42 | 43 | undo(): void { 44 | this.memory.undo(); 45 | } 46 | 47 | redo(): void { 48 | this.memory.redo(); 49 | } 50 | 51 | ////////////////////////////////////////////////////////// 52 | // 53 | // Unload 54 | // 55 | ////////////////////////////////////////////////////////// 56 | 57 | unload($e: any): void { 58 | if (this.memory.states.isChangedStates) { 59 | $e.returnValue = true; 60 | } 61 | } 62 | 63 | ////////////////////////////////////////////////////////// 64 | // 65 | // Select 66 | // 67 | ////////////////////////////////////////////////////////// 68 | 69 | select(): void { 70 | this.selectFunc.activate(); 71 | } 72 | 73 | selectAll(): void { 74 | this.memory.selectedList = []; 75 | 76 | for (let i = 0; i < this.memory.trailList.length; i++) { 77 | if (!this.memory.trailList[i].visibility) continue; 78 | 79 | const trail: Trail = this.memory.trailList[i]; 80 | 81 | for (let j = 0; j < trail.points.length; j++) { 82 | if (!trail.points[j].visibility) continue; 83 | 84 | this.memory.selectedList.push(i); 85 | break; 86 | } 87 | } 88 | } 89 | 90 | ////////////////////////////////////////////////////////// 91 | // 92 | // Clean up 93 | // 94 | ////////////////////////////////////////////////////////// 95 | 96 | cleanUp(): void { 97 | this.cleanupFunc.activate(); 98 | } 99 | 100 | ////////////////////////////////////////////////////////// 101 | // 102 | // Hand 103 | // 104 | ////////////////////////////////////////////////////////// 105 | 106 | hand(): void { 107 | this.memory.reservedByFunc.current = { 108 | name: 'hand', 109 | type: '', 110 | group: '' 111 | }; 112 | } 113 | 114 | ////////////////////////////////////////////////////////// 115 | // 116 | // Pen 117 | // 118 | ////////////////////////////////////////////////////////// 119 | 120 | pen(): void { 121 | this.penFunc.activate(); 122 | } 123 | 124 | ////////////////////////////////////////////////////////// 125 | // 126 | // Eraser 127 | // 128 | ////////////////////////////////////////////////////////// 129 | 130 | eraser(): void { 131 | this.eraseFunc.activate(); 132 | } 133 | 134 | ////////////////////////////////////////////////////////// 135 | // 136 | // Create square 137 | // 138 | ////////////////////////////////////////////////////////// 139 | 140 | createSquare(): void { 141 | this.createSquareFunc.activate(); 142 | } 143 | 144 | ////////////////////////////////////////////////////////// 145 | // 146 | // Create line 147 | // 148 | ////////////////////////////////////////////////////////// 149 | 150 | createLine(): void { 151 | this.createLineFunc.activate(); 152 | } 153 | 154 | ////////////////////////////////////////////////////////// 155 | // 156 | // Zoom 157 | // 158 | ////////////////////////////////////////////////////////// 159 | 160 | zoom($toggleFlg?: boolean): void { 161 | this.zoomFunc.activate($toggleFlg); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/app/shared/service/core/gpu.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from './memory.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class GpuService { 8 | constructor(private memory: MemoryService) {} 9 | 10 | render(): void { 11 | ////////////////////////////////////////////////////////// 12 | // 13 | // Render result 14 | // 15 | ////////////////////////////////////////////////////////// 16 | 17 | const ctx: CanvasRenderingContext2D = this.memory.renderer.ctx.main; 18 | const c: HTMLCanvasElement = ctx.canvas; 19 | c.width = this.memory.renderer.canvasWrapper.clientWidth; 20 | c.height = this.memory.renderer.canvasWrapper.clientHeight; 21 | ctx.drawImage(this.memory.renderer.gridBuffer, 0, 0); 22 | ctx.drawImage(this.memory.renderer.oekakiBuffer, 0, 0); 23 | 24 | const ctxUi: CanvasRenderingContext2D = this.memory.renderer.ctx.ui; 25 | const d: HTMLCanvasElement = ctxUi.canvas; 26 | d.width = this.memory.renderer.canvasWrapper.clientWidth; 27 | d.height = this.memory.renderer.canvasWrapper.clientHeight; 28 | ctxUi.drawImage(this.memory.renderer.uiBuffer, 0, 0); 29 | ctxUi.drawImage(this.memory.renderer.debugger, 0, 0); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/service/core/grid.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from './memory.service'; 3 | import { CanvasOffset } from '../../model/canvas-offset.model'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class GridService { 9 | private gridScale = 50; // It is important to set the same value as rulerScale 10 | private gridColor = '#373543'; 11 | 12 | constructor(private memory: MemoryService) {} 13 | 14 | // Create grid on canvas 15 | render(): void { 16 | // Initialize grid buffer 17 | const ctxGridBuffer: CanvasRenderingContext2D = this.memory.renderer.ctx.gridBuffer; 18 | const c: HTMLCanvasElement = ctxGridBuffer.canvas; 19 | c.width = this.memory.renderer.canvasWrapper.clientWidth; 20 | c.height = this.memory.renderer.canvasWrapper.clientHeight; 21 | 22 | const canvasOffset: CanvasOffset = this.memory.canvasOffset; 23 | 24 | // X-axis 25 | const offsetX: number = canvasOffset.newOffsetX - 1; // -1 is prefix for a border width of main canvas 26 | const remainX: number = Math.floor(offsetX / (this.gridScale * canvasOffset.zoomRatio)); 27 | const cutoffX: number = offsetX - remainX * this.gridScale * canvasOffset.zoomRatio; 28 | 29 | // Y-axis 30 | const offsetY: number = canvasOffset.newOffsetY - 1; // -1 is prefix for a border width of main canvas 31 | const remainY: number = Math.floor(offsetY / (this.gridScale * canvasOffset.zoomRatio)); 32 | const cutoffY: number = offsetY - remainY * this.gridScale * canvasOffset.zoomRatio; 33 | 34 | // console.log(cutoffX, cutoffY); 35 | 36 | ctxGridBuffer.translate(0.5, 0.5); 37 | 38 | // Start rendering 39 | ctxGridBuffer.beginPath(); 40 | ctxGridBuffer.strokeStyle = this.gridColor; 41 | ctxGridBuffer.lineWidth = 1; 42 | 43 | // X-axis positive 44 | for (let i = cutoffX; i < c.width; i += this.gridScale * canvasOffset.zoomRatio) { 45 | ctxGridBuffer.moveTo(i, 0); 46 | ctxGridBuffer.lineTo(i, c.height); 47 | } 48 | // X-axis negative 49 | for (let i = cutoffX; i > 0; i -= this.gridScale * canvasOffset.zoomRatio) { 50 | ctxGridBuffer.moveTo(i, 0); 51 | ctxGridBuffer.lineTo(i, c.height); 52 | } 53 | 54 | // Y-axis positive 55 | for (let i = cutoffY; i < c.height; i += this.gridScale * canvasOffset.zoomRatio) { 56 | ctxGridBuffer.moveTo(0, i); 57 | ctxGridBuffer.lineTo(c.width, i); 58 | } 59 | // Y-axis negative 60 | for (let i = cutoffY; i > 0; i -= this.gridScale * canvasOffset.zoomRatio) { 61 | ctxGridBuffer.moveTo(0, i); 62 | ctxGridBuffer.lineTo(c.width, i); 63 | } 64 | 65 | // End rendering 66 | ctxGridBuffer.stroke(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/shared/service/core/key-event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Key } from '../../model/key.model'; 3 | import { FuncService } from './func.service'; 4 | import { KeyMapService } from './key-map.service'; 5 | import { MemoryService } from './memory.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class KeyEventService { 11 | private whichFunc = ''; 12 | private count = 0; 13 | 14 | constructor(private keymap: KeyMapService, private func: FuncService, private memory: MemoryService) {} 15 | 16 | onKeyEvents($e: Key): void { 17 | if ($e.type === 'keydown') { 18 | this.keymap.keyDownEvent($e); 19 | this._keyDownFuncs($e); 20 | } else if ($e.type === 'keyup') { 21 | this._keyUpFuncs($e); 22 | this.keymap.keyUpEvent($e); 23 | } 24 | } 25 | 26 | private _keyDownFuncs($e: Key): void { 27 | const keymap: any = this.keymap.keyMap; 28 | 29 | // Save a previous state once 30 | if (this.count === 0) { 31 | this.memory.reservedByFunc.prev = this.memory.reservedByFunc.current; 32 | } 33 | this.count++; 34 | 35 | if (keymap.Control) { 36 | if (keymap.Shift) { 37 | if (keymap.z || keymap.Z) { 38 | this._redo($e); 39 | } 40 | } else { 41 | if (keymap.a) { 42 | this._selectAll($e); 43 | } else if (keymap.z) { 44 | this._undo($e); 45 | } else if (keymap[' ']) { 46 | this._zoom($e); 47 | } 48 | } 49 | } else if (keymap.Shift) { 50 | if (keymap.Control) { 51 | if (keymap.z || keymap.Z) { 52 | this._redo($e); 53 | } 54 | } 55 | } else { 56 | if (keymap.e) { 57 | this._eraser($e); 58 | } else if (keymap.p) { 59 | this._pen($e); 60 | } else if (keymap.h) { 61 | this._hand($e); 62 | } else if (keymap.v) { 63 | this._select($e); 64 | } 65 | } 66 | } 67 | 68 | private _keyUpFuncs($e: Key): void { 69 | switch (this.whichFunc) { 70 | case 'select': 71 | this.func.select(); 72 | break; 73 | 74 | case 'pen': 75 | this.func.pen(); 76 | break; 77 | 78 | case 'eraser': 79 | this.func.eraser(); 80 | break; 81 | 82 | case 'hand': 83 | this.func.hand(); 84 | break; 85 | 86 | case 'zoom': 87 | this.func.zoom(false); 88 | break; 89 | 90 | default: 91 | break; 92 | } 93 | 94 | // Initialize 95 | this.whichFunc = ''; 96 | this.count = 0; 97 | } 98 | 99 | private _pen($e: Key): void { 100 | this.whichFunc = 'pen'; 101 | } 102 | 103 | private _eraser($e: Key): void { 104 | this.whichFunc = 'eraser'; 105 | } 106 | 107 | private _hand($e: Key): void { 108 | this.whichFunc = 'hand'; 109 | } 110 | 111 | private _select($e: Key): void { 112 | this.whichFunc = 'select'; 113 | } 114 | 115 | private _selectAll($e: Key): void { 116 | $e.e.preventDefault(); 117 | 118 | this.whichFunc = 'select-all'; 119 | this.func.selectAll(); 120 | } 121 | 122 | private _redo($e: Key): void { 123 | $e.e.preventDefault(); 124 | 125 | this.whichFunc = 'redo'; 126 | this.func.redo(); 127 | } 128 | 129 | private _undo($e: Key): void { 130 | $e.e.preventDefault(); 131 | 132 | this.whichFunc = 'undo'; 133 | this.func.undo(); 134 | } 135 | 136 | private _zoom($e: Key): void { 137 | this.whichFunc = 'zoom'; 138 | this.func.zoom(true); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/app/shared/service/core/key-map.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from './memory.service'; 3 | import { Key } from '../../model/key.model'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class KeyMapService { 9 | public keyMap: any = {}; 10 | 11 | constructor(private memory: MemoryService) {} 12 | 13 | keyDownEvent($e: Key): void { 14 | this.keyMap[$e.key] = true; 15 | 16 | const isCtrlKey: boolean = this.keyMap.Control; 17 | const isAltKey: boolean = this.keyMap.Alt; 18 | const isShiftKey: boolean = this.keyMap.Shift; 19 | const isSpaceKey: boolean = this.keyMap[' ']; 20 | 21 | const isAkey: boolean = this.keyMap.a; // Select all 22 | const isEkey: boolean = this.keyMap.e; // Erase 23 | const isHkey: boolean = this.keyMap.h; // Hand 24 | const isPkey: boolean = this.keyMap.p; // Draw 25 | const isVkey: boolean = this.keyMap.v; // Select 26 | const isZkey: boolean = this.keyMap.z; // Undo and redo 27 | 28 | const isPermitkey: boolean = 29 | isCtrlKey || isAltKey || isShiftKey || isSpaceKey || isAkey || isEkey || isHkey || isPkey || isVkey || isZkey; 30 | if (!isPermitkey) this.keyMap = {}; 31 | 32 | this.memory.keyMap = this.keyMap; 33 | } 34 | 35 | keyUpEvent($e: Key): void { 36 | if ($e.key === 'Control') this.keyMap.Control = false; 37 | if ($e.key === 'Alt') this.keyMap.Alt = false; 38 | if ($e.key === 'Shift') this.keyMap.Shift = false; 39 | this._initKeyMap(); 40 | } 41 | 42 | _initKeyMap(): void { 43 | if (this.keyMap.Control) { 44 | if (this.keyMap.Shift) { 45 | this.keyMap = {}; 46 | this.keyMap.Control = true; 47 | this.keyMap.Shift = true; 48 | } else { 49 | this.keyMap = {}; 50 | this.keyMap.Control = true; 51 | } 52 | } else if (this.keyMap.Alt) { 53 | this.keyMap = {}; 54 | this.keyMap.Alt = true; 55 | } else if (this.keyMap.Shift) { 56 | this.keyMap = {}; 57 | this.keyMap.Shift = true; 58 | } else { 59 | this.keyMap = {}; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/shared/service/core/memory.service.ts: -------------------------------------------------------------------------------- 1 | import { ElementRef, Injectable } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | import { LibService } from '../util/lib.service'; 4 | import { Flgs } from '../../model/flgs.model'; 5 | import { PointerOffset } from '../../model/pointer-offset.model'; 6 | import { History } from '../../model/history.model'; 7 | import { CanvasOffset } from '../../model/canvas-offset.model'; 8 | import { Trail } from '../../model/trail.model'; 9 | import { Erase } from '../../model/erase.model'; 10 | import { Point } from '../../model/point.model'; 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class MemoryService { 16 | // brush order 17 | private orderId = -1; 18 | // draw 19 | private drawId = -1; 20 | // erase 21 | private eraseId = -1; 22 | 23 | trailList: Trail[] = []; 24 | eraseList: Erase[] = []; 25 | oekakiOrder: number[] = []; 26 | colorIdList: { id: number; colorId: string }[] = []; 27 | selectedList: number[] = []; 28 | 29 | keyMap: any = {}; 30 | 31 | canvasOffset: CanvasOffset = { 32 | zoomRatio: 1, 33 | prevOffsetX: 0, 34 | prevOffsetY: 0, 35 | newOffsetX: 0, 36 | newOffsetY: 0 37 | }; 38 | 39 | brush = { 40 | color: 'rgba(233, 30, 99, 0.95)', 41 | lineWidth: { 42 | draw: 10, // px 43 | erase: 50 // px 44 | }, 45 | meterWidth: { 46 | draw: 1, // % 47 | erase: 1 // % 48 | } 49 | }; 50 | 51 | flgs: Flgs = { 52 | dblClickFlg: false, 53 | downFlg: false, 54 | // - Similarly to mousedown events 55 | leftDownFlg: false, 56 | middleDownFlg: false, 57 | rightDownFlg: false, 58 | // - Similarly to mouseup events 59 | leftUpFlg: false, 60 | middleUpFlg: false, 61 | rightUpFlg: false, 62 | // - Similarly to mousedown + mousemove events 63 | leftDownMoveFlg: false, 64 | middleDownMoveFlg: false, 65 | rightDownMoveFlg: false, 66 | // - Similarly to wheel event 67 | wheelFlg: false 68 | }; 69 | 70 | states = { 71 | isPreventSelect: false, 72 | isPreventWheel: false, 73 | isPreventTrans: false, 74 | isNeededToUpdateHistory: false, 75 | isChangedStates: false, 76 | isCanvasLocked: false, 77 | isZoomCursorPositive: true 78 | }; 79 | 80 | pointerOffset: PointerOffset = { 81 | current: { 82 | x: -Infinity, 83 | y: -Infinity 84 | }, 85 | prev: { 86 | x: -Infinity, 87 | y: -Infinity 88 | }, 89 | raw: { 90 | x: -Infinity, 91 | y: -Infinity 92 | }, 93 | tmp: { 94 | x: -Infinity, 95 | y: -Infinity 96 | } 97 | }; 98 | 99 | reservedByFunc = { 100 | current: { 101 | name: 'pen', 102 | type: 'draw', 103 | group: 'brush' 104 | }, 105 | prev: { 106 | name: '', 107 | type: '', 108 | group: '' 109 | } 110 | }; 111 | 112 | readonly constant = { 113 | WHEEL_ZOOM_SPEED: 0.2, 114 | POINTER_ZOOM_SPEED: 0.05, 115 | GRID_COLOR: '#373543', 116 | GRID_SCALE: 50, 117 | RULER_COLOR: '#606060', 118 | NUM_COLOR: '#9e9e9e', 119 | FONT_TYPE: 'bold sans-serif', 120 | STROKE_STYLE: '#ffffff', 121 | MAX_BRUSH_SIZE: 200 122 | }; 123 | 124 | brushSizeSlider: BrushSizeSlider = {} as BrushSizeSlider; 125 | renderer: Renderer = { ctx: {} as Ctx } as Renderer; 126 | 127 | constructor(private lib: LibService) {} 128 | 129 | initBrushSizeSlider( 130 | $brushSizeWrapper: ElementRef, 131 | $brushSizeMeter: ElementRef 132 | ): void { 133 | this.brushSizeSlider.wrapper = $brushSizeWrapper.nativeElement; 134 | this.brushSizeSlider.meter = $brushSizeMeter.nativeElement; 135 | 136 | // Initialize brushSizeMeter width 137 | this.brush.meterWidth.draw = (this.brush.lineWidth.draw / this.constant.MAX_BRUSH_SIZE) * 100; 138 | this.brush.meterWidth.erase = (this.brush.lineWidth.erase / this.constant.MAX_BRUSH_SIZE) * 100; 139 | } 140 | 141 | initRenderer( 142 | $appWrapperElem: ElementRef, 143 | $canvasWrapperElem: ElementRef, 144 | $rulerWrapperElem: ElementRef, 145 | $mainElem: ElementRef, 146 | $uiElem: ElementRef, 147 | $lElem: ElementRef, 148 | $cElem: ElementRef 149 | ): void { 150 | // Wrapper 151 | this.renderer.appWrapper = $appWrapperElem.nativeElement; 152 | this.renderer.canvasWrapper = $canvasWrapperElem.nativeElement; 153 | this.renderer.rulerWrapper = $rulerWrapperElem.nativeElement; 154 | 155 | // Renderer 156 | this.renderer.main = $mainElem.nativeElement; 157 | this.renderer.ui = $uiElem.nativeElement; 158 | this.renderer.rulerL = $lElem.nativeElement; 159 | this.renderer.rulerC = $cElem.nativeElement; 160 | 161 | // Buffer 162 | this.renderer.uiBuffer = document.createElement('canvas'); 163 | this.renderer.gridBuffer = document.createElement('canvas'); 164 | this.renderer.oekakiBuffer = document.createElement('canvas'); 165 | this.renderer.rulerLbuffer = document.createElement('canvas'); 166 | this.renderer.rulerCbuffer = document.createElement('canvas'); 167 | this.renderer.colorBuffer = document.createElement('canvas'); 168 | 169 | // ctx - Renderer 170 | this.renderer.ctx.main = this.renderer.main.getContext('2d'); 171 | this.renderer.ctx.ui = this.renderer.ui.getContext('2d'); 172 | this.renderer.ctx.rulerL = this.renderer.rulerL.getContext('2d'); 173 | this.renderer.ctx.rulerC = this.renderer.rulerC.getContext('2d'); 174 | 175 | // ctx - Buffer 176 | this.renderer.ctx.uiBuffer = this.renderer.uiBuffer.getContext('2d'); 177 | this.renderer.ctx.gridBuffer = this.renderer.gridBuffer.getContext('2d'); 178 | this.renderer.ctx.oekakiBuffer = this.renderer.oekakiBuffer.getContext('2d'); 179 | this.renderer.ctx.rulerLbuffer = this.renderer.rulerLbuffer.getContext('2d'); 180 | this.renderer.ctx.rulerCbuffer = this.renderer.rulerCbuffer.getContext('2d'); 181 | this.renderer.ctx.colorBuffer = this.renderer.colorBuffer.getContext('2d'); 182 | 183 | // Debugger 184 | this.renderer.debugger = document.createElement('canvas'); 185 | this.renderer.ctx.debugger = this.renderer.debugger.getContext('2d'); 186 | } 187 | 188 | undo(): void { 189 | const drawOrErase: number = this.oekakiOrder[this.orderId]; 190 | 191 | if (drawOrErase === undefined) return; 192 | 193 | if (drawOrErase === 1 && this.drawId > -1) { 194 | // draw 195 | this._updateDraw(this.drawId, false); 196 | 197 | let dId: number = this.drawId; 198 | dId -= dId > -1 ? 1 : 0; 199 | this.drawId = dId; 200 | } else if (drawOrErase === 0 && this.eraseId > -1) { 201 | // erase 202 | this._updateErase(this.eraseId); 203 | 204 | let eId: number = this.eraseId; 205 | eId -= eId > -1 ? 1 : 0; 206 | this.eraseId = eId; 207 | } 208 | 209 | let oId: number = this.orderId; 210 | oId -= oId > -1 ? 1 : 0; 211 | this.orderId = oId; 212 | } 213 | 214 | redo(): void { 215 | let oId: number = this.orderId; 216 | oId += oId < this.oekakiOrder.length - 1 ? 1 : 0; 217 | 218 | const drawOrErase: number = this.oekakiOrder[oId]; 219 | 220 | if (drawOrErase === undefined) return; 221 | 222 | if (drawOrErase === 1 && this.drawId < this.trailList.length - 1) { 223 | // draw 224 | let dId: number = this.drawId; 225 | dId += dId < this.trailList.length - 1 ? 1 : 0; 226 | 227 | this._updateDraw(dId, true); 228 | this.drawId = dId; 229 | } else if (drawOrErase === 0 && this.eraseId < this.eraseList.length - 1) { 230 | // erase 231 | let eId: number = this.eraseId; 232 | eId += eId < this.eraseList.length - 1 ? 1 : 0; 233 | 234 | this._updateErase(eId); 235 | this.eraseId = eId; 236 | } 237 | 238 | this.orderId = oId; 239 | } 240 | 241 | private _updateDraw($dId: number, $flg: boolean): void { 242 | const trail: Trail = this.trailList[$dId]; 243 | trail.visibility = $flg; 244 | } 245 | 246 | private _updateErase($eId: number): void { 247 | const erase: Erase = this.eraseList[$eId]; 248 | const trailList: Erase['trailList'] = erase.trailList; 249 | 250 | for (let i = 0; i < trailList.length; i++) { 251 | if (!trailList[i]) continue; 252 | 253 | const tId: number = trailList[i].trailId; 254 | const trail: Trail = this.trailList[tId]; 255 | const pointList: number[] = trailList[i].pointIdList; 256 | 257 | for (let j = 0; j < pointList.length; j++) { 258 | const pId: number = pointList[j]; 259 | const point: Point = trail.points[pId]; 260 | 261 | if (!trail || !point) continue; 262 | 263 | point.visibility = !point.visibility; 264 | } 265 | } 266 | } 267 | 268 | pileNewHistory(): void { 269 | // Remove unnecessary event ids 270 | this.oekakiOrder = _.take(this.oekakiOrder, this.orderId + 1); 271 | 272 | if (this.reservedByFunc.current.type === 'draw') { 273 | this._newDrawHistory(); 274 | } else if (this.reservedByFunc.current.type === 'erase') { 275 | this._newEraseHistory(); 276 | } 277 | 278 | this.orderId++; 279 | this.states.isChangedStates = true; 280 | } 281 | 282 | private _newDrawHistory(): void { 283 | this.trailList = _.take(this.trailList, this.drawId + 1); 284 | 285 | const trail: Trail = { 286 | id: this.trailList.length, 287 | colorId: this.lib.genUniqueColor(this.colorIdList), 288 | name: this.reservedByFunc.current.name, 289 | visibility: true, 290 | min: { 291 | x: Infinity, 292 | y: Infinity 293 | }, 294 | max: { 295 | x: -Infinity, 296 | y: -Infinity 297 | }, 298 | origin: { 299 | prevOffsetX: (this.pointerOffset.prev.x - this.canvasOffset.prevOffsetX) / this.canvasOffset.zoomRatio, 300 | prevOffsetY: (this.pointerOffset.prev.y - this.canvasOffset.prevOffsetY) / this.canvasOffset.zoomRatio, 301 | newOffsetX: (this.pointerOffset.prev.x - this.canvasOffset.prevOffsetX) / this.canvasOffset.zoomRatio, 302 | newOffsetY: (this.pointerOffset.prev.y - this.canvasOffset.prevOffsetY) / this.canvasOffset.zoomRatio 303 | }, 304 | points: [] as Point[] 305 | }; 306 | this.trailList.push(trail); 307 | 308 | // Push new colorId 309 | this.colorIdList.push({ id: trail.id, colorId: trail.colorId }); 310 | 311 | // To tell 'draw' 312 | this.oekakiOrder.push(1); 313 | this.drawId++; 314 | } 315 | 316 | private _newEraseHistory(): void { 317 | this.eraseList = _.take(this.eraseList, this.eraseId + 1); 318 | 319 | const erase: Erase = { 320 | id: this.eraseList.length, 321 | trailList: [] 322 | }; 323 | this.eraseList.push(erase); 324 | 325 | // To tell 'erase' 326 | this.oekakiOrder.push(0); 327 | this.eraseId++; 328 | } 329 | } 330 | 331 | interface BrushSizeSlider { 332 | // Wrapper 333 | wrapper: HTMLDivElement; 334 | meter: HTMLDivElement; 335 | } 336 | 337 | interface Renderer { 338 | // Wrapper 339 | appWrapper: HTMLDivElement; 340 | canvasWrapper: HTMLDivElement; 341 | rulerWrapper: HTMLDivElement; 342 | // Debugger 343 | debugger: HTMLCanvasElement; 344 | // Renderer 345 | main: HTMLCanvasElement; 346 | ui: HTMLCanvasElement; 347 | rulerL: HTMLCanvasElement; 348 | rulerC: HTMLCanvasElement; 349 | // Buffer 350 | uiBuffer: HTMLCanvasElement; 351 | gridBuffer: HTMLCanvasElement; 352 | oekakiBuffer: HTMLCanvasElement; 353 | rulerLbuffer: HTMLCanvasElement; 354 | rulerCbuffer: HTMLCanvasElement; 355 | colorBuffer: HTMLCanvasElement; 356 | ctx: Ctx; 357 | } 358 | 359 | interface Ctx { 360 | // Debugger 361 | debugger: CanvasRenderingContext2D; 362 | // Renderer 363 | main: CanvasRenderingContext2D; 364 | ui: CanvasRenderingContext2D; 365 | rulerL: CanvasRenderingContext2D; 366 | rulerC: CanvasRenderingContext2D; 367 | // Buffer 368 | uiBuffer: CanvasRenderingContext2D; 369 | gridBuffer: CanvasRenderingContext2D; 370 | oekakiBuffer: CanvasRenderingContext2D; 371 | rulerLbuffer: CanvasRenderingContext2D; 372 | rulerCbuffer: CanvasRenderingContext2D; 373 | colorBuffer: CanvasRenderingContext2D; 374 | } 375 | -------------------------------------------------------------------------------- /src/app/shared/service/core/pointer-event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Pointer } from '../../model/pointer.model'; 3 | import { Flgs } from '../../model/flgs.model'; 4 | import { CanvasService } from './canvas.service'; 5 | 6 | // Modules 7 | import { SelectService } from '../module/select.service'; 8 | import { DrawService } from '../module/draw.service'; 9 | import { EraseService } from '../module/erase.service'; 10 | import { ZoomService } from '../module/zoom.service'; 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class PointerEventService { 16 | constructor( 17 | private canvas: CanvasService, 18 | private selectFunc: SelectService, 19 | private drawFunc: DrawService, 20 | private eraseFunc: EraseService, 21 | private zoomFunc: ZoomService 22 | ) {} 23 | 24 | down(): void { 25 | this.canvas.registerOnMouseDown(); 26 | this.drawFunc.registerOnMouseDown(); 27 | } 28 | 29 | leftDown($name: string): void { 30 | switch ($name) { 31 | case 'select': 32 | this.selectFunc.getTargetTrailId(); 33 | break; 34 | 35 | default: 36 | break; 37 | } 38 | } 39 | 40 | rightDown(): void {} 41 | 42 | middleDown(): void {} 43 | 44 | noDown(): void { 45 | this.canvas.registerOnNoMouseDown(); 46 | this.drawFunc.registerOnNoMouseDown(); 47 | } 48 | 49 | wheel($event: Pointer): void { 50 | this.canvas.registerOnWheel($event); 51 | this.drawFunc.registerOnWheel($event); 52 | } 53 | 54 | leftUp(): void {} 55 | 56 | rightUp(): void {} 57 | 58 | middleUp(): void {} 59 | 60 | leftDownMove($type: string, $name: string, $newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 61 | switch ($type) { 62 | case 'draw': 63 | this.drawFunc.registerDrawFuncs($newOffsetX, $newOffsetY); 64 | break; 65 | 66 | case 'erase': 67 | this.eraseFunc.setVisibility(); 68 | break; 69 | 70 | default: 71 | if ($name === 'select') { 72 | this.selectFunc.updateTargetTrailOffset($newOffsetX, $newOffsetY, $event); 73 | } else if ($name === 'hand') { 74 | this._updateCanvases($newOffsetX, $newOffsetY, $event); 75 | } else if ($name === 'zoom') { 76 | this.zoomFunc.updateOffsets(); 77 | } 78 | break; 79 | } 80 | } 81 | 82 | rightDownMove($type: string, $newOffsetX: number, $newOffsetY: number, $event: Pointer): void {} 83 | 84 | middleDownMove($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 85 | this._updateCanvases($newOffsetX, $newOffsetY, $event); 86 | } 87 | 88 | ////////////////////////////////////////////////////////// 89 | // 90 | // Private methods 91 | // 92 | ////////////////////////////////////////////////////////// 93 | 94 | private _updateCanvases($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 95 | // Update canvas coordinates 96 | this.canvas.registerOnMouseMiddleMove($newOffsetX, $newOffsetY, $event); 97 | // Update trail point coordinates 98 | this.drawFunc.registerOnMouseMiddleMove($newOffsetX, $newOffsetY, $event); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/app/shared/service/core/register.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Pointer } from '../../model/pointer.model'; 3 | import { MemoryService } from './memory.service'; 4 | import { PointerEventService } from './pointer-event.service'; 5 | import { Flgs } from '../../model/flgs.model'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class RegisterService { 11 | constructor(private memory: MemoryService, private pointerEvent: PointerEventService) {} 12 | 13 | onPointerDown(): void { 14 | const flgs: Flgs = this.memory.flgs; 15 | const name: string = this.memory.reservedByFunc.current.name; 16 | 17 | this.pointerEvent.down(); 18 | 19 | if (flgs.leftDownFlg) { 20 | this.pointerEvent.leftDown(name); 21 | } else if (flgs.rightDownFlg) { 22 | this.pointerEvent.rightDown(); 23 | } else if (flgs.middleDownFlg) { 24 | this.pointerEvent.middleDown(); 25 | } 26 | } 27 | 28 | onNoPointerDown($event: Pointer): void { 29 | const flgs: Flgs = this.memory.flgs; 30 | 31 | this.pointerEvent.noDown(); 32 | 33 | // Wheel event - zooming-in/out 34 | if (flgs.wheelFlg) { 35 | this.pointerEvent.wheel($event); 36 | } 37 | } 38 | 39 | onPointerUp(): void { 40 | const flgs: Flgs = this.memory.flgs; 41 | 42 | if (flgs.leftUpFlg) { 43 | this.pointerEvent.leftUp(); 44 | } else if (flgs.rightUpFlg) { 45 | this.pointerEvent.rightUp(); 46 | } else if (flgs.middleUpFlg) { 47 | this.pointerEvent.middleUp(); 48 | } 49 | } 50 | 51 | onPointerMove($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 52 | const flgs: Flgs = this.memory.flgs; 53 | const type: string = this.memory.reservedByFunc.current.type; 54 | const name: string = this.memory.reservedByFunc.current.name; 55 | 56 | if (flgs.leftDownMoveFlg) { 57 | this.pointerEvent.leftDownMove(type, name, $newOffsetX, $newOffsetY, $event); 58 | } else if (flgs.rightDownMoveFlg) { 59 | this.pointerEvent.rightDownMove(type, $newOffsetX, $newOffsetY, $event); 60 | } else if (flgs.middleDownMoveFlg) { 61 | this.pointerEvent.middleDownMove($newOffsetX, $newOffsetY, $event); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/shared/service/core/ruler.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanvasOffset } from '../../model/canvas-offset.model'; 3 | import { LibService } from '../util/lib.service'; 4 | import { MemoryService } from './memory.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class RulerService { 10 | private rulerThickness = 20; // Thickness of the window ruler 11 | private parentScale = 100; // Parent scale of the window ruler 12 | private childScale = 10; // Child scale of the window ruler 13 | private middleLength = 0.5; // Length of the middleScale 14 | private childLength = 0.25; // Length of the childScale 15 | 16 | constructor(private lib: LibService, private memory: MemoryService) {} 17 | 18 | render(): void { 19 | const canvasOffset: CanvasOffset = this.memory.canvasOffset; 20 | if (this.parentScale * canvasOffset.zoomRatio * 2 < 100) { 21 | this.parentScale *= 2; 22 | } 23 | if ((this.parentScale * canvasOffset.zoomRatio) / 2 > 50) { 24 | const half = this.parentScale / 2; 25 | if (Number.isInteger(half)) this.parentScale = half; 26 | } 27 | 28 | const { renderer } = this.memory; 29 | 30 | this._createLine(); 31 | const l: HTMLCanvasElement = renderer.ctx.rulerL.canvas; 32 | l.width = renderer.rulerWrapper.clientWidth; 33 | l.height = this.rulerThickness; 34 | renderer.ctx.rulerL.drawImage(renderer.rulerLbuffer, 0, 0); 35 | 36 | this._createColumn(); 37 | const c: HTMLCanvasElement = renderer.ctx.rulerC.canvas; 38 | c.width = this.rulerThickness; 39 | c.height = renderer.rulerWrapper.clientHeight; 40 | renderer.ctx.rulerC.drawImage(renderer.rulerCbuffer, 0, 0); 41 | } 42 | 43 | _createLine(): void { 44 | const ctxLbuffer: CanvasRenderingContext2D = this.memory.renderer.ctx.rulerLbuffer; 45 | const l: HTMLCanvasElement = ctxLbuffer.canvas; 46 | l.width = this.memory.renderer.rulerWrapper.clientWidth; 47 | l.height = this.rulerThickness; 48 | 49 | const canvasOffset: CanvasOffset = this.memory.canvasOffset; 50 | 51 | const offsetX: number = l.height + canvasOffset.newOffsetX; 52 | const remain: number = Math.floor(offsetX / (this.parentScale * canvasOffset.zoomRatio)); 53 | const cutoff: number = offsetX - remain * this.parentScale * canvasOffset.zoomRatio; 54 | 55 | ctxLbuffer.translate(0.5, 0.5); 56 | 57 | // Frame 58 | ctxLbuffer.strokeStyle = this.memory.constant.RULER_COLOR; 59 | ctxLbuffer.lineWidth = 1; 60 | //ctxLbuffer.strokeRect(0, 0, l.width, l.height); 61 | 62 | ctxLbuffer.strokeStyle = this.memory.constant.RULER_COLOR; 63 | ctxLbuffer.font = this.memory.constant.FONT_TYPE; 64 | ctxLbuffer.fillStyle = this.memory.constant.NUM_COLOR; 65 | 66 | //////////////////////////////////////////////////////////////////// Children 67 | const childStep: number = (this.parentScale / this.childScale) * canvasOffset.zoomRatio; 68 | const childOffsetY: number = this.rulerThickness * (1 - this.childLength); 69 | 70 | ctxLbuffer.beginPath(); 71 | // Children - positive 72 | for (let i = cutoff; i < l.width; i += childStep) { 73 | ctxLbuffer.moveTo(i, childOffsetY); 74 | ctxLbuffer.lineTo(i, l.height); 75 | } 76 | // Children - negative 77 | for (let i = cutoff; i > 0; i -= childStep) { 78 | ctxLbuffer.moveTo(i, childOffsetY); 79 | ctxLbuffer.lineTo(i, l.height); 80 | } 81 | ctxLbuffer.stroke(); 82 | 83 | //////////////////////////////////////////////////////////////////// Middle 84 | const middleStep = (this.parentScale / 2) * canvasOffset.zoomRatio; 85 | const middleOffsetY: number = this.rulerThickness * (1 - this.middleLength); 86 | 87 | ctxLbuffer.beginPath(); 88 | // Middle - positive 89 | for (let i = cutoff; i < l.width; i += middleStep) { 90 | ctxLbuffer.clearRect(i - 1, childOffsetY, 2, l.height); 91 | ctxLbuffer.moveTo(i, middleOffsetY); 92 | ctxLbuffer.lineTo(i, l.height); 93 | } 94 | // Middle - negative 95 | for (let i = cutoff; i > 0; i -= middleStep) { 96 | ctxLbuffer.clearRect(i - 1, childOffsetY, 2, l.height); 97 | ctxLbuffer.moveTo(i, middleOffsetY); 98 | ctxLbuffer.lineTo(i, l.height); 99 | } 100 | ctxLbuffer.stroke(); 101 | 102 | //////////////////////////////////////////////////////////////////// Parents 103 | let scaleCount = 0; 104 | const parentStep: number = this.parentScale * canvasOffset.zoomRatio; 105 | 106 | ctxLbuffer.beginPath(); 107 | // Parents - positive 108 | for (let i = cutoff; i < l.width; i += parentStep) { 109 | ctxLbuffer.clearRect(i - 1, 1, 2, l.height); 110 | ctxLbuffer.moveTo(i, 0); 111 | ctxLbuffer.lineTo(i, l.height); 112 | ctxLbuffer.fillText(`${Math.abs((remain - scaleCount) * this.parentScale)}`, i + 5, 10); 113 | scaleCount++; 114 | } 115 | // Parents - nagative 116 | scaleCount = 0; 117 | for (let i = cutoff; i > parentStep; i -= parentStep) { 118 | ctxLbuffer.clearRect(i - 1, 1, 2, l.height); 119 | ctxLbuffer.moveTo(i, 0); 120 | ctxLbuffer.lineTo(i, l.height); 121 | ctxLbuffer.fillText(`${Math.abs((remain - scaleCount) * this.parentScale)}`, i + 5, 10); 122 | scaleCount++; 123 | } 124 | ctxLbuffer.stroke(); 125 | 126 | // Empty box 127 | ctxLbuffer.beginPath(); 128 | ctxLbuffer.setLineDash([]); 129 | ctxLbuffer.clearRect(-0.5, -0.5, this.rulerThickness, this.rulerThickness); 130 | ctxLbuffer.strokeStyle = this.memory.constant.RULER_COLOR; 131 | ctxLbuffer.moveTo(this.rulerThickness, 0); 132 | ctxLbuffer.lineTo(this.rulerThickness, this.rulerThickness); 133 | ctxLbuffer.stroke(); 134 | } 135 | 136 | _createColumn(): void { 137 | const ctxCbuffer: CanvasRenderingContext2D = this.memory.renderer.ctx.rulerCbuffer; 138 | const c: HTMLCanvasElement = ctxCbuffer.canvas; 139 | c.width = this.rulerThickness; 140 | c.height = this.memory.renderer.rulerWrapper.clientHeight; 141 | 142 | const canvasOffset: CanvasOffset = this.memory.canvasOffset; 143 | 144 | const offsetY: number = c.width + canvasOffset.newOffsetY; 145 | const remain: number = Math.floor(offsetY / (this.parentScale * canvasOffset.zoomRatio)); 146 | const cutoff: number = offsetY - remain * this.parentScale * canvasOffset.zoomRatio; 147 | 148 | ctxCbuffer.translate(0.5, 0.5); 149 | ctxCbuffer.clearRect(0, 0, c.width, c.height); 150 | 151 | // Frame 152 | ctxCbuffer.strokeStyle = this.memory.constant.RULER_COLOR; 153 | ctxCbuffer.lineWidth = 1; 154 | //ctxCbuffer.strokeRect(0, 0, c.width, c.height); 155 | 156 | ctxCbuffer.strokeStyle = this.memory.constant.RULER_COLOR; 157 | ctxCbuffer.font = this.memory.constant.FONT_TYPE; 158 | ctxCbuffer.fillStyle = this.memory.constant.NUM_COLOR; 159 | 160 | //////////////////////////////////////////////////////////////////// Children 161 | const childStep: number = (this.parentScale / this.childScale) * canvasOffset.zoomRatio; 162 | const childOffsetX: number = this.rulerThickness * (1 - this.childLength); 163 | 164 | ctxCbuffer.beginPath(); 165 | // Children - positive 166 | for (let i = cutoff; i < c.height; i += childStep) { 167 | ctxCbuffer.moveTo(childOffsetX, i); 168 | ctxCbuffer.lineTo(c.width, i); 169 | } 170 | // Children - nagative 171 | for (let i = cutoff; i > 0; i -= childStep) { 172 | ctxCbuffer.moveTo(childOffsetX, i); 173 | ctxCbuffer.lineTo(c.width, i); 174 | } 175 | ctxCbuffer.stroke(); 176 | 177 | //////////////////////////////////////////////////////////////////// Middle 178 | const middleStep: number = (this.parentScale / 2) * canvasOffset.zoomRatio; 179 | const middleOffsetX: number = this.rulerThickness * (1 - this.middleLength); 180 | 181 | ctxCbuffer.beginPath(); 182 | // Middle - positive 183 | for (let i = cutoff; i < c.height; i += middleStep) { 184 | ctxCbuffer.clearRect(childOffsetX, i - 1, c.width, 2); 185 | ctxCbuffer.moveTo(middleOffsetX, i); 186 | ctxCbuffer.lineTo(c.width, i); 187 | } 188 | // Middle - nagative 189 | for (let i = cutoff; i > 0; i -= middleStep) { 190 | ctxCbuffer.clearRect(childOffsetX, i - 1, c.width, 2); 191 | ctxCbuffer.moveTo(middleOffsetX, i); 192 | ctxCbuffer.lineTo(c.width, i); 193 | } 194 | ctxCbuffer.stroke(); 195 | 196 | //////////////////////////////////////////////////////////////////// Parents 197 | let scaleCount = 0; 198 | const parentStep: number = this.parentScale * canvasOffset.zoomRatio; 199 | 200 | ctxCbuffer.beginPath(); 201 | // Parents - positive 202 | for (let i = cutoff; i < c.height; i += parentStep) { 203 | ctxCbuffer.clearRect(1, i - 1, c.width, 2); 204 | ctxCbuffer.moveTo(0, i); 205 | ctxCbuffer.lineTo(c.width, i); 206 | this._fillTextLine(ctxCbuffer, `${Math.abs((remain - scaleCount) * this.parentScale)}`, 4, i + 10); 207 | scaleCount++; 208 | } 209 | // Parents - negative 210 | scaleCount = 0; 211 | for (let i = cutoff; i > parentStep; i -= parentStep) { 212 | ctxCbuffer.clearRect(1, i - 1, c.width, 2); 213 | ctxCbuffer.moveTo(0, i); 214 | ctxCbuffer.lineTo(c.width, i); 215 | this._fillTextLine(ctxCbuffer, `${Math.abs((remain - scaleCount) * this.parentScale)}`, 4, i + 10); 216 | scaleCount++; 217 | } 218 | ctxCbuffer.stroke(); 219 | 220 | // Empty box 221 | ctxCbuffer.beginPath(); 222 | ctxCbuffer.setLineDash([]); 223 | ctxCbuffer.clearRect(-0.5, -0.5, this.rulerThickness, this.rulerThickness); 224 | ctxCbuffer.strokeStyle = this.memory.constant.RULER_COLOR; 225 | ctxCbuffer.moveTo(0, this.rulerThickness); 226 | ctxCbuffer.lineTo(this.rulerThickness, this.rulerThickness); 227 | ctxCbuffer.stroke(); 228 | } 229 | 230 | _fillTextLine(ctx: CanvasRenderingContext2D, text: string, x: number, y: number): void { 231 | const textList: string[] = text.toString().split(''); 232 | const lineHeight: number = ctx.measureText('あ').width; 233 | textList.forEach(($txt, $i) => { 234 | const resY: number = y + lineHeight * $i - lineHeight * textList.length - 5; 235 | ctx.fillText($txt, x, this.lib.f2i(resY)); 236 | }); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/app/shared/service/core/ui.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from './memory.service'; 3 | import { CursorService } from '../core/cursor.service'; 4 | 5 | // Ui module 6 | import { SelectUiService } from '../module/select.ui.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class UiService { 12 | constructor(private memory: MemoryService, private cursor: CursorService, private selectUi: SelectUiService) {} 13 | 14 | render(): void { 15 | const ctxUiBuffer: CanvasRenderingContext2D = this.memory.renderer.ctx.uiBuffer; 16 | const c: HTMLCanvasElement = ctxUiBuffer.canvas; 17 | c.width = this.memory.renderer.canvasWrapper.clientWidth; 18 | c.height = this.memory.renderer.canvasWrapper.clientHeight; 19 | 20 | // To pixelize correctly 21 | ctxUiBuffer.translate(0.5, 0.5); 22 | 23 | // GUI for select 24 | this.selectUi.render(ctxUiBuffer); 25 | 26 | // Render cursor 27 | this.cursor.render(ctxUiBuffer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shared/service/module/cleanup.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { Trail } from '../../model/trail.model'; 4 | import { Point } from '../../model/point.model'; 5 | import { Erase } from '../../model/erase.model'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class CleanupService { 11 | constructor(private memory: MemoryService) {} 12 | 13 | activate(): void { 14 | // To store previous states 15 | const tmpReserved = this.memory.reservedByFunc.current; 16 | 17 | // To tell pipeline that this function is a part of the erase module 18 | this.memory.reservedByFunc.current = { 19 | name: 'cleanup', 20 | type: 'erase', 21 | group: 'brush' 22 | }; 23 | this.memory.pileNewHistory(); 24 | 25 | this.memory.selectedList = []; 26 | 27 | // Set visibility of all points to false 28 | this._setVisibilities(); 29 | 30 | // initialize with previous states 31 | this.memory.reservedByFunc.current = tmpReserved; 32 | } 33 | 34 | private _setVisibilities(): void { 35 | const trailLists: Trail[] = this.memory.trailList; 36 | 37 | for (let i = 0; i < trailLists.length; i++) { 38 | const trail: Trail = trailLists[i]; 39 | const points: Point[] = trailLists[i].points; 40 | 41 | for (let j = 0; j < points.length; j++) { 42 | const p: Point = points[j]; 43 | 44 | if (p.visibility) { 45 | const erase: Erase = this.memory.eraseList[this.memory.eraseList.length - 1]; 46 | 47 | if (!erase.trailList[i]) erase.trailList[i] = { trailId: -1, pointIdList: [] }; 48 | 49 | erase.trailList[i].trailId = i; 50 | erase.trailList[i].pointIdList.push(j); 51 | p.visibility = false; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/shared/service/module/create-line.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { Point } from '../../model/point.model'; 4 | import { Trail } from '../../model/trail.model'; 5 | import { Offset } from '../../model/offset.model'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class CreateLineService { 11 | private cutoff = 50; 12 | 13 | constructor(private memory: MemoryService) {} 14 | 15 | activate(): void { 16 | this.memory.reservedByFunc.current = { 17 | name: 'line', 18 | type: 'draw', 19 | group: 'brush' 20 | }; 21 | } 22 | 23 | recordTrail($newOffsetX: number, $newOffsetY: number): void { 24 | const trailId: number = this.memory.trailList.length > 0 ? this.memory.trailList.length - 1 : 0; 25 | const trail: Trail = this.memory.trailList[trailId]; 26 | 27 | // Initialize points until mouseup event occured 28 | trail.min = { 29 | x: Infinity, 30 | y: Infinity 31 | }; 32 | trail.max = { 33 | x: -Infinity, 34 | y: -Infinity 35 | }; 36 | trail.points = []; 37 | 38 | // Add points along straight line 39 | this.addNewPoints(trail, $newOffsetX, $newOffsetY); 40 | } 41 | 42 | private addNewPoints($trail: Trail, $newOffsetX: number, $newOffsetY): void { 43 | const totalLengthX: number = Math.abs($newOffsetX) * this.cutoff; 44 | const totalLengthY: number = Math.abs($newOffsetY) * this.cutoff; 45 | const cutoffX: number = totalLengthX / this.cutoff; 46 | const cutoffY: number = totalLengthY / this.cutoff; 47 | 48 | if ($newOffsetX > 0) { 49 | for (let i = 0; i <= totalLengthX; i += cutoffX) { 50 | const fixedI: number = i / this.cutoff; 51 | const point: Point = this._creatPoint($trail, fixedI, this._getYfromX($newOffsetX, $newOffsetY, fixedI)); 52 | 53 | // Add bounding 54 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 55 | 56 | $trail.points.push(point); 57 | } 58 | } else if ($newOffsetX < 0) { 59 | for (let i = 0; i <= totalLengthX; i += cutoffX) { 60 | const fixedI: number = i / this.cutoff; 61 | const point: Point = this._creatPoint($trail, -fixedI, this._getYfromX($newOffsetX, $newOffsetY, -fixedI)); 62 | 63 | // Add bounding 64 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 65 | 66 | $trail.points.push(point); 67 | } 68 | } else if ($newOffsetX === 0) { 69 | if ($newOffsetY > 0) { 70 | for (let i = 0; i <= totalLengthY; i += cutoffY) { 71 | const fixedI: number = i / this.cutoff; 72 | const point: Point = this._creatPoint($trail, 0, fixedI); 73 | 74 | // Add bounding 75 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 76 | 77 | $trail.points.push(point); 78 | } 79 | } else if ($newOffsetY < 0) { 80 | for (let i = 0; i <= totalLengthY; i += cutoffY) { 81 | const fixedI: number = i / this.cutoff; 82 | const point: Point = this._creatPoint($trail, 0, -fixedI); 83 | 84 | // Add bounding 85 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 86 | 87 | $trail.points.push(point); 88 | } 89 | } 90 | } 91 | } 92 | 93 | private _creatPoint($trail: Trail, $x: number, $y: number): Point { 94 | const point: Point = { 95 | id: $trail.points.length, 96 | color: this.memory.brush.color, 97 | visibility: true, 98 | relativeOffset: { 99 | x: $x / this.memory.canvasOffset.zoomRatio, 100 | y: $y / this.memory.canvasOffset.zoomRatio 101 | }, 102 | pressure: 1, 103 | lineWidth: this.memory.brush.lineWidth.draw 104 | }; 105 | 106 | return point; 107 | } 108 | 109 | private _getYfromX($newOffsetX: number, $newOffsetY: number, $i): number { 110 | return ($newOffsetY / $newOffsetX) * $i; 111 | } 112 | 113 | private _validateMinMax($trail: Trail, $x: number, $y: number): void { 114 | $trail.min.x = Math.min($trail.min.x, $x); 115 | $trail.min.y = Math.min($trail.min.y, $y); 116 | 117 | $trail.max.x = Math.max($trail.max.x, $x); 118 | $trail.max.y = Math.max($trail.max.y, $y); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/shared/service/module/create-square.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { Trail } from '../../model/trail.model'; 4 | import { Point } from '../../model/point.model'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class CreateSquareService { 10 | private cutoff = 50; 11 | 12 | constructor(private memory: MemoryService) {} 13 | 14 | activate(): void { 15 | this.memory.reservedByFunc.current = { 16 | name: 'square', 17 | type: 'draw', 18 | group: 'brush' 19 | }; 20 | } 21 | 22 | recordTrail($newOffsetX: number, $newOffsetY: number): void { 23 | const trailId: number = this.memory.trailList.length > 0 ? this.memory.trailList.length - 1 : 0; 24 | const trail: Trail = this.memory.trailList[trailId]; 25 | 26 | // Initialize points until mouseup event occured 27 | trail.min = { 28 | x: Infinity, 29 | y: Infinity 30 | }; 31 | trail.max = { 32 | x: -Infinity, 33 | y: -Infinity 34 | }; 35 | trail.points = []; 36 | 37 | // Add points along square 38 | this.addNewPoints(trail, $newOffsetX, $newOffsetY); 39 | } 40 | 41 | private addNewPoints($trail: Trail, $newOffsetX: number, $newOffsetY): void { 42 | const totalLengthX: number = Math.abs($newOffsetX) * this.cutoff; 43 | const totalLengthY: number = Math.abs($newOffsetY) * this.cutoff; 44 | const cutoffX: number = totalLengthX / this.cutoff; 45 | const cutoffY: number = totalLengthY / this.cutoff; 46 | 47 | // → 48 | if ($newOffsetX > 0) { 49 | for (let i = 0; i <= totalLengthX; i += cutoffX) { 50 | const fixedI: number = i / this.cutoff; 51 | const point: Point = this._creatPoint($trail, fixedI, 0); 52 | 53 | // Add bounding 54 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 55 | 56 | $trail.points.push(point); 57 | } 58 | } else if ($newOffsetX < 0) { 59 | for (let i = 0; i <= totalLengthX; i += cutoffX) { 60 | const fixedI: number = i / this.cutoff; 61 | const point: Point = this._creatPoint($trail, -fixedI, 0); 62 | 63 | // Add bounding 64 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 65 | 66 | $trail.points.push(point); 67 | } 68 | } 69 | 70 | // ↓ 71 | if ($newOffsetY > 0) { 72 | for (let i = 0; i <= totalLengthY; i += cutoffY) { 73 | const fixedI: number = i / this.cutoff; 74 | const point: Point = this._creatPoint($trail, $newOffsetX, fixedI); 75 | 76 | // Add bounding 77 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 78 | 79 | $trail.points.push(point); 80 | } 81 | } else if ($newOffsetY < 0) { 82 | for (let i = 0; i <= totalLengthY; i += cutoffY) { 83 | const fixedI: number = i / this.cutoff; 84 | const point: Point = this._creatPoint($trail, $newOffsetX, -fixedI); 85 | 86 | // Add bounding 87 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 88 | 89 | $trail.points.push(point); 90 | } 91 | } 92 | 93 | // ← 94 | if ($newOffsetX > 0) { 95 | for (let i = totalLengthX; i >= 0; i -= cutoffX) { 96 | const fixedI: number = i / this.cutoff; 97 | const point: Point = this._creatPoint($trail, fixedI, $newOffsetY); 98 | 99 | // Add bounding 100 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 101 | 102 | $trail.points.push(point); 103 | } 104 | } else if ($newOffsetX < 0) { 105 | for (let i = totalLengthX; i >= 0; i -= cutoffX) { 106 | const fixedI: number = i / this.cutoff; 107 | const point: Point = this._creatPoint($trail, -fixedI, $newOffsetY); 108 | 109 | // Add bounding 110 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 111 | 112 | $trail.points.push(point); 113 | } 114 | } 115 | 116 | // ↑ 117 | if ($newOffsetY > 0) { 118 | for (let i = totalLengthY; i >= 0; i -= cutoffY) { 119 | const fixedI: number = i / this.cutoff; 120 | const point: Point = this._creatPoint($trail, 0, fixedI); 121 | 122 | // Add bounding 123 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 124 | 125 | $trail.points.push(point); 126 | } 127 | } else if ($newOffsetY < 0) { 128 | for (let i = totalLengthY; i >= 0; i -= cutoffY) { 129 | const fixedI: number = i / this.cutoff; 130 | const point: Point = this._creatPoint($trail, 0, -fixedI); 131 | 132 | // Add bounding 133 | this._validateMinMax($trail, point.relativeOffset.x, point.relativeOffset.y); 134 | 135 | $trail.points.push(point); 136 | } 137 | } 138 | } 139 | 140 | private _creatPoint($trail: Trail, $x: number, $y: number): Point { 141 | const point: Point = { 142 | id: $trail.points.length, 143 | color: this.memory.brush.color, 144 | visibility: true, 145 | relativeOffset: { 146 | x: $x / this.memory.canvasOffset.zoomRatio, 147 | y: $y / this.memory.canvasOffset.zoomRatio 148 | }, 149 | pressure: 1, 150 | lineWidth: this.memory.brush.lineWidth.draw 151 | }; 152 | 153 | return point; 154 | } 155 | 156 | private _validateMinMax($trail: Trail, $x: number, $y: number): void { 157 | $trail.min.x = Math.min($trail.min.x, $x); 158 | $trail.min.y = Math.min($trail.min.y, $y); 159 | 160 | $trail.max.x = Math.max($trail.max.x, $x); 161 | $trail.max.y = Math.max($trail.max.y, $y); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/app/shared/service/module/draw.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { Point } from '../../model/point.model'; 4 | import { Trail } from '../../model/trail.model'; 5 | import { CoordService } from '../util/coord.service'; 6 | import { Pointer } from '../../model/pointer.model'; 7 | 8 | // Draw modules 9 | import { PenService } from '../module/pen.service'; 10 | import { CreateSquareService } from '../module/create-square.service'; 11 | import { CreateLineService } from '../module/create-line.service'; 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class DrawService { 17 | constructor( 18 | private memory: MemoryService, 19 | private coord: CoordService, 20 | private pen: PenService, 21 | private square: CreateSquareService, 22 | private line: CreateLineService 23 | ) {} 24 | 25 | registerDrawFuncs($newOffsetX: number, $newOffsetY: number): void { 26 | const name: string = this.memory.reservedByFunc.current.name; 27 | 28 | if (this.memory.selectedList.length > 0) this.memory.selectedList = []; 29 | 30 | if (name === 'pen') { 31 | this.pen.recordTrail(); 32 | } else if (name === 'square') { 33 | this.square.recordTrail($newOffsetX, $newOffsetY); 34 | } else if (name === 'line') { 35 | this.line.recordTrail($newOffsetX, $newOffsetY); 36 | } 37 | } 38 | 39 | registerOnMouseDown(): void { 40 | const trailList: Trail[] = this.memory.trailList; 41 | 42 | for (let i = 0; i < trailList.length; i++) { 43 | const t: Trail = trailList[i]; 44 | t.origin.prevOffsetX = t.origin.newOffsetX; 45 | t.origin.prevOffsetY = t.origin.newOffsetY; 46 | } 47 | } 48 | 49 | registerOnNoMouseDown(): void { 50 | this.registerOnMouseDown(); 51 | } 52 | 53 | registerOnWheel($event: Pointer): void { 54 | this._updateOffsets(0, 0, $event); 55 | } 56 | 57 | registerOnMouseMiddleMove($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 58 | this._updateOffsets($newOffsetX, $newOffsetY, $event); 59 | } 60 | 61 | private _updateOffsets($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 62 | this.coord.updateOffset($newOffsetX, $newOffsetY, this.memory.canvasOffset, $event); 63 | } 64 | 65 | updateTargetTrailOffsets($trail: Trail, $newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 66 | $trail.origin = this.coord.updateOffset( 67 | $newOffsetX / this.memory.canvasOffset.zoomRatio, 68 | $newOffsetY / this.memory.canvasOffset.zoomRatio, 69 | $trail.origin, 70 | $event 71 | ); 72 | } 73 | 74 | updateOffsetsByZoom($x: number, $y: number, $deltaFlg: boolean): void { 75 | this.registerOnNoMouseDown(); 76 | } 77 | 78 | render(): void { 79 | const ctxOekakiBuffer: CanvasRenderingContext2D = this.memory.renderer.ctx.oekakiBuffer; 80 | const c: HTMLCanvasElement = ctxOekakiBuffer.canvas; 81 | c.width = this.memory.renderer.canvasWrapper.clientWidth; 82 | c.height = this.memory.renderer.canvasWrapper.clientHeight; 83 | 84 | const trailList: Trail[] = this.memory.trailList; 85 | 86 | ctxOekakiBuffer.translate(0.5, 0.5); 87 | 88 | for (let i = 0; i < trailList.length; i++) { 89 | const trail: Trail = trailList[i]; 90 | 91 | if (trail.visibility) { 92 | ctxOekakiBuffer.beginPath(); 93 | ctxOekakiBuffer.lineCap = 'round'; 94 | ctxOekakiBuffer.lineJoin = 'round'; 95 | 96 | this.renderLine(ctxOekakiBuffer, trail); 97 | 98 | ctxOekakiBuffer.stroke(); 99 | } 100 | } 101 | } 102 | 103 | private renderLine($ctxOekakiBuffer: CanvasRenderingContext2D, $trail: Trail): void { 104 | for (let i = 0; i < $trail.points.length; i++) { 105 | const prevP: Point = $trail.points[i - 1]; 106 | const currentP: Point = $trail.points[i]; 107 | const nextP: Point = $trail.points[i + 1]; 108 | 109 | if (!currentP.visibility) continue; 110 | 111 | const ctx: CanvasRenderingContext2D = $ctxOekakiBuffer; 112 | ctx.lineWidth = currentP.lineWidth * currentP.pressure * this.memory.canvasOffset.zoomRatio; 113 | ctx.strokeStyle = currentP.color; 114 | 115 | const currentPoffsetX: number = 116 | (currentP.relativeOffset.x + $trail.origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 117 | this.memory.canvasOffset.newOffsetX; 118 | const currentPoffsetY: number = 119 | (currentP.relativeOffset.y + $trail.origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 120 | this.memory.canvasOffset.newOffsetY; 121 | 122 | ctx.moveTo(currentPoffsetX, currentPoffsetY); 123 | 124 | if (nextP && nextP.visibility) { 125 | const nextPoffsetX: number = 126 | (nextP.relativeOffset.x + $trail.origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 127 | this.memory.canvasOffset.newOffsetX; 128 | const nextPoffsetY: number = 129 | (nextP.relativeOffset.y + $trail.origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 130 | this.memory.canvasOffset.newOffsetY; 131 | 132 | ctx.lineTo(nextPoffsetX, nextPoffsetY); 133 | } else if (prevP && prevP.visibility) { 134 | const prevPoffsetX: number = 135 | (prevP.relativeOffset.x + $trail.origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 136 | this.memory.canvasOffset.newOffsetX; 137 | const prevPoffsetY: number = 138 | (prevP.relativeOffset.y + $trail.origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 139 | this.memory.canvasOffset.newOffsetY; 140 | 141 | ctx.lineTo(prevPoffsetX, prevPoffsetY); 142 | } 143 | } 144 | } 145 | 146 | private _midPointBetween(p1: { x: number; y: number }, p2: { x: number; y: number }): { x: number; y: number } { 147 | return { 148 | x: p1.x + (p2.x - p1.x) / 2, 149 | y: p1.y + (p2.y - p1.y) / 2 150 | }; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/app/shared/service/module/erase.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { Trail } from '../../model/trail.model'; 4 | import { DebugService } from '../util/debug.service'; 5 | import { Offset } from '../../model/offset.model'; 6 | import { PointerOffset } from '../../model/pointer-offset.model'; 7 | import { Point } from '../../model/point.model'; 8 | import { Erase } from '../../model/erase.model'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class EraseService { 14 | constructor(private memory: MemoryService, private debug: DebugService) {} 15 | 16 | activate(): void { 17 | this.memory.reservedByFunc.current = { 18 | name: 'eraser', 19 | type: 'erase', 20 | group: 'brush' 21 | }; 22 | } 23 | 24 | setVisibility() { 25 | const trailIndexes: number[] = this._validateTrails(); 26 | 27 | if (this.memory.selectedList.length > 0) this.memory.selectedList = []; 28 | 29 | for (let i = 0; i < trailIndexes.length; i++) { 30 | const tId: number = trailIndexes[i]; 31 | const trail: Trail = this.memory.trailList[tId]; 32 | 33 | const pointIndexes: number[] = this._validatePoints(tId); 34 | 35 | for (let j = 0; j < pointIndexes.length; j++) { 36 | const pId: number = pointIndexes[j]; 37 | const p: Point = this.memory.trailList[tId].points[pId]; 38 | 39 | if (p.visibility) { 40 | const erase: Erase = this.memory.eraseList[this.memory.eraseList.length - 1]; 41 | if (!erase.trailList[tId]) erase.trailList[tId] = { trailId: -1, pointIdList: [] }; 42 | erase.trailList[tId].trailId = tId; 43 | erase.trailList[tId].pointIdList.push(pId); 44 | 45 | p.visibility = false; 46 | } 47 | } 48 | } 49 | } 50 | 51 | private _validateTrails(): number[] { 52 | const validList: number[] = []; 53 | 54 | const trailList: Trail[] = this.memory.trailList; 55 | for (let i = 0; i < trailList.length; i++) { 56 | const min: { x: number; y: number } = trailList[i].min; 57 | const max: { x: number; y: number } = trailList[i].max; 58 | const pointerOffset: PointerOffset = this.memory.pointerOffset; 59 | 60 | const x0: number = 61 | (min.x + trailList[i].origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 62 | this.memory.canvasOffset.newOffsetX; 63 | const y0: number = 64 | (min.y + trailList[i].origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 65 | this.memory.canvasOffset.newOffsetY; 66 | const x1: number = 67 | (max.x + trailList[i].origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 68 | this.memory.canvasOffset.newOffsetX; 69 | const y1: number = 70 | (max.y + trailList[i].origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 71 | this.memory.canvasOffset.newOffsetY; 72 | const r: number = this.memory.brush.lineWidth.erase / 2; 73 | 74 | // diff 75 | const diffX0: number = x0 - pointerOffset.current.x; 76 | const diffY0: number = y0 - pointerOffset.current.y; 77 | const diffX1: number = x1 - pointerOffset.current.x; 78 | const diffY1: number = y1 - pointerOffset.current.y; 79 | 80 | // Corner 81 | const corner0: boolean = diffX0 < r && diffY0 < r; 82 | const corner1: boolean = diffX0 < r && diffY1 < r; 83 | const corner2: boolean = diffX1 < r && diffY0 < r; 84 | const corner3: boolean = diffX1 < r && diffY1 < r; 85 | const corner: boolean = corner0 || corner1 || corner2 || corner3; 86 | 87 | // Middle 88 | const middle0: boolean = y0 < pointerOffset.current.y - r && pointerOffset.current.y + r < y1; 89 | const middle1: boolean = x0 < pointerOffset.current.x - r && pointerOffset.current.x + r < x1; 90 | const middle: boolean = middle0 && middle1; 91 | 92 | if (corner || middle) validList.push(i); 93 | } 94 | 95 | return validList; 96 | } 97 | 98 | private _validatePoints($trailId: number): number[] { 99 | const validList: number[] = []; 100 | 101 | const trail: Trail = this.memory.trailList[$trailId]; 102 | const points: Point[] = trail.points; 103 | 104 | for (let i = 0; i < points.length; i++) { 105 | const p: Point = points[i]; 106 | const pointX: number = 107 | (p.relativeOffset.x + trail.origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 108 | this.memory.canvasOffset.newOffsetX; 109 | const pointY: number = 110 | (p.relativeOffset.y + trail.origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 111 | this.memory.canvasOffset.newOffsetY; 112 | const pointerOffset: PointerOffset = this.memory.pointerOffset; 113 | const r: number = this.memory.brush.lineWidth.erase / 2; 114 | 115 | const diffX: number = pointX - pointerOffset.current.x; 116 | const diffY: number = pointY - pointerOffset.current.y; 117 | const distance: number = Math.sqrt(diffX * diffX + diffY * diffY); 118 | 119 | const isCollided: boolean = distance - (p.lineWidth * p.pressure * this.memory.canvasOffset.zoomRatio) / 2 < r; 120 | 121 | if (isCollided) validList.push(i); 122 | } 123 | 124 | return validList; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/app/shared/service/module/pen.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { Point } from '../../model/point.model'; 4 | import { Trail } from '../../model/trail.model'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class PenService { 10 | private cutoff = 10; 11 | 12 | constructor(private memory: MemoryService) {} 13 | 14 | activate(): void { 15 | this.memory.reservedByFunc.current = { 16 | name: 'pen', 17 | type: 'draw', 18 | group: 'brush' 19 | }; 20 | } 21 | 22 | recordTrail(): void { 23 | const trailId: number = this.memory.trailList.length > 0 ? this.memory.trailList.length - 1 : 0; 24 | const trail: Trail = this.memory.trailList[trailId]; 25 | const point: Point = this._creatPoint(trail); 26 | 27 | // Update bounding 28 | this._validateMinMax(trail, point.relativeOffset.x, point.relativeOffset.y); 29 | 30 | // Add a new point 31 | if (trail.points.length > 0) { 32 | this.addNewPoints(trail, point); 33 | } else { 34 | trail.points.push(point); 35 | } 36 | } 37 | 38 | private addNewPoints($trail: Trail, $currentP: Point): void { 39 | const prevP: Point = $trail.points[$trail.points.length - 1]; 40 | const dist: number = this._distanceBetween(prevP, $currentP); 41 | const angle: number = this._angleBetween(prevP, $currentP); 42 | 43 | for (let i = 0; i < dist; i += this.cutoff) { 44 | const x: number = prevP.relativeOffset.x + Math.sin(angle) * i; 45 | const y: number = prevP.relativeOffset.y + Math.cos(angle) * i; 46 | const point: Point = this._creatPoint($trail); 47 | point.relativeOffset = { x, y }; 48 | 49 | $trail.points.push(point); 50 | } 51 | } 52 | 53 | private _distanceBetween($prevP: Point, $currentP: Point): number { 54 | return Math.sqrt( 55 | Math.pow($currentP.relativeOffset.x - $prevP.relativeOffset.x, 2) + 56 | Math.pow($currentP.relativeOffset.y - $prevP.relativeOffset.y, 2) 57 | ); 58 | } 59 | 60 | private _angleBetween($prevP: Point, $currentP: Point): number { 61 | return Math.atan2( 62 | $currentP.relativeOffset.x - $prevP.relativeOffset.x, 63 | $currentP.relativeOffset.y - $prevP.relativeOffset.y 64 | ); 65 | } 66 | 67 | private _creatPoint($trail: Trail): Point { 68 | const point: Point = { 69 | id: $trail.points.length, 70 | color: this.memory.brush.color, 71 | visibility: true, 72 | relativeOffset: { 73 | x: 74 | (this.memory.pointerOffset.current.x - 75 | (this.memory.canvasOffset.newOffsetX + $trail.origin.newOffsetX * this.memory.canvasOffset.zoomRatio)) / 76 | this.memory.canvasOffset.zoomRatio, 77 | y: 78 | (this.memory.pointerOffset.current.y - 79 | (this.memory.canvasOffset.newOffsetY + $trail.origin.newOffsetY * this.memory.canvasOffset.zoomRatio)) / 80 | this.memory.canvasOffset.zoomRatio 81 | }, 82 | pressure: 1, 83 | lineWidth: this.memory.brush.lineWidth.draw 84 | }; 85 | 86 | return point; 87 | } 88 | 89 | private _validateMinMax($trail: Trail, $x: number, $y: number): void { 90 | $trail.min.x = Math.min($trail.min.x, $x); 91 | $trail.min.y = Math.min($trail.min.y, $y); 92 | 93 | $trail.max.x = Math.max($trail.max.x, $x); 94 | $trail.max.y = Math.max($trail.max.y, $y); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/shared/service/module/select.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { LibService } from '../util/lib.service'; 4 | import { DrawService } from '../module/draw.service'; 5 | import { Point } from '../../model/point.model'; 6 | import { Trail } from '../../model/trail.model'; 7 | import { Pointer } from '../../model/pointer.model'; 8 | import { PointerOffset } from '../../model/pointer-offset.model'; 9 | import { Offset } from '../../model/offset.model'; 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class SelectService { 15 | constructor(private memory: MemoryService, private lib: LibService, private draw: DrawService) {} 16 | 17 | activate(): void { 18 | this.memory.reservedByFunc.current = { 19 | name: 'select', 20 | type: '', 21 | group: '' 22 | }; 23 | } 24 | 25 | updateTargetTrailOffset($newOffsetX: number, $newOffsetY: number, $event: Pointer): void { 26 | for (let i = 0; i < this.memory.selectedList.length; i++) { 27 | const id: number = this.memory.selectedList[i]; 28 | const trail: Trail = this.memory.trailList[id]; 29 | 30 | // If none selected, return 31 | if (id === -1) continue; 32 | 33 | this.draw.updateTargetTrailOffsets(trail, $newOffsetX, $newOffsetY, $event); 34 | } 35 | } 36 | 37 | getTargetTrailId(): void { 38 | // Render color buffer 39 | this.preComputeColorBuffer(); 40 | 41 | const ctx: CanvasRenderingContext2D = this.memory.renderer.ctx.colorBuffer; 42 | const trailListId: number = this.lib.checkHitArea(this.memory.pointerOffset, ctx, this.memory.trailList); 43 | 44 | this.select(trailListId); 45 | } 46 | 47 | private select($trailListId: number): void { 48 | const selectedList: number[] = this.memory.selectedList; 49 | 50 | if ($trailListId === -1) { 51 | let min = { 52 | x: Infinity, 53 | y: Infinity 54 | }; 55 | let max = { 56 | x: -Infinity, 57 | y: -Infinity 58 | }; 59 | let lineWidth = -Infinity; 60 | 61 | for (let i = 0; i < selectedList.length; i++) { 62 | const id: number = selectedList[i]; 63 | // Return if none is selected 64 | if (id === -1) continue; 65 | 66 | const trail: Trail = this.memory.trailList[id]; 67 | 68 | if (!trail.visibility) continue; 69 | 70 | let count = 0; 71 | for (let j = 0; j < trail.points.length; j++) { 72 | if (trail.points[j].visibility) continue; 73 | 74 | count++; 75 | } 76 | 77 | if (count === trail.points.length) continue; 78 | 79 | const fixedMin = { 80 | x: trail.min.x + trail.origin.newOffsetX, 81 | y: trail.min.y + trail.origin.newOffsetY 82 | }; 83 | const fixedMax = { 84 | x: trail.max.x + trail.origin.newOffsetX, 85 | y: trail.max.y + trail.origin.newOffsetY 86 | }; 87 | 88 | // Reset selectedId if its already selected 89 | // For multi-select 90 | if (this.memory.keyMap.Shift && this._validateBounding(fixedMin, fixedMax, trail.points[0].lineWidth)) { 91 | this.memory.selectedList[i] = -1; 92 | } 93 | 94 | const tmp: { 95 | min: { x: number; y: number }; 96 | max: { x: number; y: number }; 97 | lineWidth: number; 98 | } = this._getNewMinMax(min, max, lineWidth, trail); 99 | min = tmp.min; 100 | max = tmp.max; 101 | lineWidth = tmp.lineWidth; 102 | } 103 | 104 | // Initialize if none is selected 105 | if (!this._validateBounding(min, max, lineWidth)) this.memory.selectedList = []; 106 | } else { 107 | const selectedId: number = this._checkSelected($trailListId); 108 | 109 | if (selectedId === -1) { 110 | if (!this.memory.keyMap.Shift) this.memory.selectedList = []; 111 | 112 | this.memory.selectedList.push($trailListId); 113 | } else { 114 | // For multi-select 115 | if (this.memory.keyMap.Shift) this.memory.selectedList[selectedId] = -1; 116 | } 117 | } 118 | } 119 | 120 | // Return -1 if selected target is not present in the selectedList 121 | private _checkSelected($trailListId: number): number { 122 | const selectedList: number[] = this.memory.selectedList; 123 | 124 | for (let i = 0; i < selectedList.length; i++) { 125 | if ($trailListId === selectedList[i]) { 126 | return i; 127 | } 128 | } 129 | 130 | return -1; 131 | } 132 | 133 | private _validateBounding( 134 | $min: { x: number; y: number }, 135 | $max: { x: number; y: number }, 136 | $lineWidth: number 137 | ): boolean { 138 | const fixedOffsetX = 139 | (this.memory.pointerOffset.current.x - this.memory.canvasOffset.newOffsetX) / this.memory.canvasOffset.zoomRatio; 140 | const fixedOffsetY = 141 | (this.memory.pointerOffset.current.y - this.memory.canvasOffset.newOffsetY) / this.memory.canvasOffset.zoomRatio; 142 | 143 | const minX: number = $min.x + $lineWidth; 144 | const minY: number = $min.y + $lineWidth; 145 | const maxX: number = $max.x + $lineWidth * 2; 146 | const maxY: number = $max.y + $lineWidth * 2; 147 | 148 | const isInBoundingX: boolean = minX <= fixedOffsetX && fixedOffsetX <= maxX; 149 | const isInBoundingY: boolean = minY <= fixedOffsetY && fixedOffsetY <= maxY; 150 | 151 | return isInBoundingX && isInBoundingY; 152 | } 153 | 154 | private _getNewMinMax( 155 | $min: { x: number; y: number }, 156 | $max: { x: number; y: number }, 157 | $lineWidth: number, 158 | $trail: Trail 159 | ): { min: { x: number; y: number }; max: { x: number; y: number }; lineWidth: number } { 160 | $min.x = Math.min($min.x, $trail.min.x + $trail.origin.newOffsetX); 161 | $min.y = Math.min($min.y, $trail.min.y + $trail.origin.newOffsetY); 162 | 163 | $max.x = Math.max($max.x, $trail.max.x + $trail.origin.newOffsetX); 164 | $max.y = Math.max($max.y, $trail.max.y + $trail.origin.newOffsetY); 165 | 166 | $lineWidth = Math.max($lineWidth, $trail.points[0].lineWidth); 167 | 168 | return { min: $min, max: $max, lineWidth: $lineWidth }; 169 | } 170 | 171 | private preComputeColorBuffer(): void { 172 | const ctx: CanvasRenderingContext2D = this.memory.renderer.ctx.colorBuffer; 173 | const c: HTMLCanvasElement = ctx.canvas; 174 | c.width = this.memory.renderer.canvasWrapper.clientWidth; 175 | c.height = this.memory.renderer.canvasWrapper.clientHeight; 176 | 177 | const trailList: Trail[] = this.memory.trailList; 178 | 179 | ctx.translate(0.5, 0.5); 180 | 181 | for (let i = 0; i < trailList.length; i++) { 182 | const trail: Trail = trailList[i]; 183 | 184 | if (trail.visibility) { 185 | ctx.beginPath(); 186 | ctx.lineCap = 'round'; 187 | ctx.lineJoin = 'round'; 188 | 189 | this._renderLine(ctx, trail); 190 | 191 | ctx.stroke(); 192 | } 193 | } 194 | } 195 | 196 | private _renderLine($ctx: CanvasRenderingContext2D, $trail: Trail): void { 197 | for (let i = 0; i < $trail.points.length; i++) { 198 | const prevP: Point = $trail.points[i - 1]; 199 | const currentP: Point = $trail.points[i]; 200 | const nextP: Point = $trail.points[i + 1]; 201 | 202 | if (!currentP.visibility) continue; 203 | 204 | const ctx: CanvasRenderingContext2D = $ctx; 205 | ctx.lineWidth = currentP.lineWidth * currentP.pressure * this.memory.canvasOffset.zoomRatio; 206 | ctx.strokeStyle = '#' + $trail.colorId; 207 | 208 | const currentPoffsetX: number = 209 | (currentP.relativeOffset.x + $trail.origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 210 | this.memory.canvasOffset.newOffsetX; 211 | const currentPoffsetY: number = 212 | (currentP.relativeOffset.y + $trail.origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 213 | this.memory.canvasOffset.newOffsetY; 214 | 215 | ctx.moveTo(currentPoffsetX, currentPoffsetY); 216 | 217 | if (nextP && nextP.visibility) { 218 | const nextPoffsetX: number = 219 | (nextP.relativeOffset.x + $trail.origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 220 | this.memory.canvasOffset.newOffsetX; 221 | const nextPoffsetY: number = 222 | (nextP.relativeOffset.y + $trail.origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 223 | this.memory.canvasOffset.newOffsetY; 224 | 225 | ctx.lineTo(nextPoffsetX, nextPoffsetY); 226 | //this._createBezierCurve(ctx, currentP, nextP); 227 | } else if (prevP && prevP.visibility) { 228 | const prevPoffsetX: number = 229 | (prevP.relativeOffset.x + $trail.origin.newOffsetX) * this.memory.canvasOffset.zoomRatio + 230 | this.memory.canvasOffset.newOffsetX; 231 | const prevPoffsetY: number = 232 | (prevP.relativeOffset.y + $trail.origin.newOffsetY) * this.memory.canvasOffset.zoomRatio + 233 | this.memory.canvasOffset.newOffsetY; 234 | 235 | ctx.lineTo(prevPoffsetX, prevPoffsetY); 236 | //this._createBezierCurve(ctx, currentP, prevP); 237 | } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/app/shared/service/module/select.ui.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { Trail } from '../../model/trail.model'; 4 | import { Offset } from '../../model/offset.model'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class SelectUiService { 10 | private style = '#44AAFF'; 11 | private canvasColor = '#32303f'; 12 | private lineWidth = 1; 13 | private r = 5; 14 | 15 | constructor(private memory: MemoryService) {} 16 | 17 | render($ctx: CanvasRenderingContext2D): void { 18 | let min = { 19 | x: Infinity, 20 | y: Infinity 21 | }; 22 | let max = { 23 | x: -Infinity, 24 | y: -Infinity 25 | }; 26 | let lineWidth = -Infinity; 27 | 28 | for (let i = 0; i < this.memory.selectedList.length; i++) { 29 | if (this.memory.selectedList[i] === -1) continue; 30 | 31 | const id: number = this.memory.selectedList[i]; 32 | const trail: Trail = this.memory.trailList[id]; 33 | 34 | if (!trail.visibility) continue; 35 | 36 | let count = 0; 37 | for (let j = 0; j < trail.points.length; j++) { 38 | if (trail.points[j].visibility) continue; 39 | 40 | count++; 41 | } 42 | 43 | if (count === trail.points.length) continue; 44 | 45 | const fixedMin = { 46 | x: trail.min.x + trail.origin.newOffsetX, 47 | y: trail.min.y + trail.origin.newOffsetY 48 | }; 49 | const fixedMax = { 50 | x: trail.max.x + trail.origin.newOffsetX, 51 | y: trail.max.y + trail.origin.newOffsetY 52 | }; 53 | 54 | // Create only frame 55 | this.createSelectFrame(fixedMin, fixedMax, trail.points[0].lineWidth, $ctx); 56 | 57 | const tmp: { 58 | min: { x: number; y: number }; 59 | max: { x: number; y: number }; 60 | lineWidth: number; 61 | } = this._getNewMinMax(min, max, lineWidth, trail); 62 | min = tmp.min; 63 | max = tmp.max; 64 | lineWidth = tmp.lineWidth; 65 | } 66 | 67 | // Create select box 68 | this.createSelectBox(min, max, lineWidth, $ctx); 69 | } 70 | 71 | private _getNewMinMax( 72 | $min: { x: number; y: number }, 73 | $max: { x: number; y: number }, 74 | $lineWidth: number, 75 | $trail: Trail 76 | ): { min: { x: number; y: number }; max: { x: number; y: number }; lineWidth: number } { 77 | $min.x = Math.min($min.x, $trail.min.x + $trail.origin.newOffsetX); 78 | $min.y = Math.min($min.y, $trail.min.y + $trail.origin.newOffsetY); 79 | 80 | $max.x = Math.max($max.x, $trail.max.x + $trail.origin.newOffsetX); 81 | $max.y = Math.max($max.y, $trail.max.y + $trail.origin.newOffsetY); 82 | 83 | $lineWidth = Math.max($lineWidth, $trail.points[0].lineWidth); 84 | 85 | return { min: $min, max: $max, lineWidth: $lineWidth }; 86 | } 87 | 88 | private createSelectBox( 89 | $min: { x: number; y: number }, 90 | $max: { x: number; y: number }, 91 | $lineWidth: number, 92 | $ctx: CanvasRenderingContext2D 93 | ): void { 94 | const x: number = 95 | $min.x * this.memory.canvasOffset.zoomRatio + 96 | this.memory.canvasOffset.newOffsetX - 97 | $lineWidth * this.memory.canvasOffset.zoomRatio; 98 | const y: number = 99 | $min.y * this.memory.canvasOffset.zoomRatio + 100 | this.memory.canvasOffset.newOffsetY - 101 | $lineWidth * this.memory.canvasOffset.zoomRatio; 102 | const w: number = 103 | ($max.x - $min.x) * this.memory.canvasOffset.zoomRatio + $lineWidth * this.memory.canvasOffset.zoomRatio * 2; 104 | const h: number = 105 | ($max.y - $min.y) * this.memory.canvasOffset.zoomRatio + $lineWidth * this.memory.canvasOffset.zoomRatio * 2; 106 | 107 | // Frame 108 | this.createSelectFrame($min, $max, $lineWidth, $ctx); 109 | 110 | // Corner points 111 | $ctx.beginPath(); 112 | $ctx.strokeStyle = this.canvasColor; 113 | $ctx.fillStyle = this.style; 114 | $ctx.lineWidth = this.lineWidth * 5; 115 | // Left top 116 | $ctx.moveTo(x, y); 117 | $ctx.arc(x, y, this.r, 0, Math.PI * 2); 118 | // Top middle 119 | $ctx.moveTo(x + w / 2, y); 120 | $ctx.arc(x + w / 2, y, this.r, 0, Math.PI * 2); 121 | // Right top 122 | $ctx.moveTo(x + w, y); 123 | $ctx.arc(x + w, y, this.r, 0, Math.PI * 2); 124 | // Right middle 125 | $ctx.moveTo(x + w, y + h / 2); 126 | $ctx.arc(x + w, y + h / 2, this.r, 0, Math.PI * 2); 127 | // Right bottom 128 | $ctx.moveTo(x + w, y + h); 129 | $ctx.arc(x + w, y + h, this.r, 0, Math.PI * 2); 130 | // Bottom middle 131 | $ctx.moveTo(x + w / 2, y + h); 132 | $ctx.arc(x + w / 2, y + h, this.r, 0, Math.PI * 2); 133 | // Left bottom 134 | $ctx.moveTo(x, y + h); 135 | $ctx.arc(x, y + h, this.r, 0, Math.PI * 2); 136 | // Left bottom 137 | $ctx.moveTo(x, y + h / 2); 138 | $ctx.arc(x, y + h / 2, this.r, 0, Math.PI * 2); 139 | $ctx.stroke(); 140 | $ctx.fill(); 141 | 142 | // Stick 143 | $ctx.beginPath(); 144 | $ctx.strokeStyle = this.style; 145 | $ctx.lineWidth = this.lineWidth; 146 | // Stick 147 | $ctx.moveTo(x + w / 2, y); 148 | $ctx.lineTo(x + w / 2, y - 35); 149 | $ctx.stroke(); 150 | 151 | // Rotate point 152 | $ctx.beginPath(); 153 | $ctx.strokeStyle = this.style; 154 | $ctx.fillStyle = '#ffffff'; 155 | $ctx.lineWidth = this.lineWidth * 2; 156 | // Point 157 | $ctx.arc(x + w / 2, y - 35, this.r, 0, Math.PI * 2); 158 | $ctx.fill(); 159 | $ctx.stroke(); 160 | } 161 | 162 | private createSelectFrame( 163 | $min: { x: number; y: number }, 164 | $max: { x: number; y: number }, 165 | $lineWidth: number, 166 | $ctx: CanvasRenderingContext2D 167 | ): void { 168 | const x: number = 169 | $min.x * this.memory.canvasOffset.zoomRatio + 170 | this.memory.canvasOffset.newOffsetX - 171 | $lineWidth * this.memory.canvasOffset.zoomRatio; 172 | const y: number = 173 | $min.y * this.memory.canvasOffset.zoomRatio + 174 | this.memory.canvasOffset.newOffsetY - 175 | $lineWidth * this.memory.canvasOffset.zoomRatio; 176 | const w: number = 177 | ($max.x - $min.x) * this.memory.canvasOffset.zoomRatio + $lineWidth * this.memory.canvasOffset.zoomRatio * 2; 178 | const h: number = 179 | ($max.y - $min.y) * this.memory.canvasOffset.zoomRatio + $lineWidth * this.memory.canvasOffset.zoomRatio * 2; 180 | 181 | // Frame 182 | $ctx.beginPath(); 183 | $ctx.strokeStyle = this.style; 184 | $ctx.lineWidth = this.lineWidth; 185 | $ctx.rect(x, y, w, h); 186 | $ctx.stroke(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/app/shared/service/module/slide-brush-size.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class SlideBrushSizeService { 8 | private enableSliderFlg = false; 9 | 10 | constructor(private memory: MemoryService) {} 11 | 12 | activate($clientX: number): void { 13 | this.enableSliderFlg = true; 14 | 15 | this.changeSlideAmount($clientX); 16 | } 17 | 18 | disableSlider(): void { 19 | this.enableSliderFlg = false; 20 | } 21 | 22 | changeSlideAmount($clientX: number): void { 23 | if (this.enableSliderFlg) { 24 | let w: number = 25 | (($clientX - this.memory.brushSizeSlider.wrapper.getBoundingClientRect().left) / 26 | this.memory.brushSizeSlider.wrapper.getBoundingClientRect().width) * 27 | 100; 28 | if (w <= 0) w = 0.5; 29 | if (w > 100) w = 100; 30 | 31 | // update memory 32 | if (this.memory.reservedByFunc.current.type === 'draw') { 33 | this.memory.brush.meterWidth.draw = w; 34 | this.memory.brush.lineWidth.draw = Math.floor((w / 100) * this.memory.constant.MAX_BRUSH_SIZE); 35 | } else if (this.memory.reservedByFunc.current.type === 'erase') { 36 | this.memory.brush.meterWidth.erase = w; 37 | this.memory.brush.lineWidth.erase = Math.floor((w / 100) * this.memory.constant.MAX_BRUSH_SIZE); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/shared/service/module/zoom.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | import { CoordService } from '../util/coord.service'; 4 | import { CanvasService } from '../core/canvas.service'; 5 | import { DrawService } from '../module/draw.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class ZoomService { 11 | private prevX = 0; 12 | private prevY = 0; 13 | 14 | constructor( 15 | private memory: MemoryService, 16 | private coord: CoordService, 17 | private canvas: CanvasService, 18 | private draw: DrawService 19 | ) {} 20 | 21 | activate($toggleFlg: boolean): void { 22 | if ($toggleFlg) { 23 | this.memory.reservedByFunc.current = { 24 | name: 'zoom', 25 | type: '', 26 | group: '' 27 | }; 28 | } else { 29 | this.memory.reservedByFunc.current = this.memory.reservedByFunc.prev; 30 | } 31 | } 32 | 33 | updateOffsets(): void { 34 | const x: number = this.memory.pointerOffset.prev.x; 35 | const y: number = this.memory.pointerOffset.prev.y; 36 | const diffX: number = this.memory.pointerOffset.current.x - this.memory.pointerOffset.tmp.x; 37 | const diffY: number = this.memory.pointerOffset.current.y - this.memory.pointerOffset.tmp.y; 38 | 39 | if (Math.abs(diffX) <= Math.abs(diffY)) return; 40 | 41 | if (diffX > 0) { 42 | // Set for cursor 43 | this.memory.states.isZoomCursorPositive = true; 44 | 45 | this.canvas.updateOffsetByZoom(x, y, false); 46 | this.draw.updateOffsetsByZoom(x, y, false); 47 | } else { 48 | // Set for cursor 49 | this.memory.states.isZoomCursorPositive = false; 50 | 51 | this.canvas.updateOffsetByZoom(x, y, true); 52 | this.draw.updateOffsetsByZoom(x, y, true); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/shared/service/util/coord.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Pointer } from '../../model/pointer.model'; 3 | import { MemoryService } from '../core/memory.service'; 4 | import { Offset } from '../../model/offset.model'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class CoordService { 10 | constructor(private memory: MemoryService) {} 11 | 12 | updateOffset($newOffsetX: number, $newOffsetY: number, $offset: Offset, $event: Pointer): Offset { 13 | let offsetX: number = $offset.prevOffsetX; 14 | let offsetY: number = $offset.prevOffsetY; 15 | 16 | if (!this.memory.flgs.wheelFlg) { 17 | if ( 18 | ($event.btn === 0 && !this.memory.states.isPreventSelect) || 19 | ($event.btn === 1 && !this.memory.states.isPreventTrans) 20 | ) { 21 | offsetX += $newOffsetX; 22 | offsetY += $newOffsetY; 23 | } 24 | } else { 25 | offsetX -= this.memory.pointerOffset.current.x; 26 | offsetY -= this.memory.pointerOffset.current.y; 27 | 28 | if ($event.delta > 0) { 29 | const ratio: number = 1 - this.memory.constant.WHEEL_ZOOM_SPEED; 30 | offsetX = offsetX * ratio + this.memory.pointerOffset.current.x; 31 | offsetY = offsetY * ratio + this.memory.pointerOffset.current.y; 32 | } else { 33 | const ratio: number = 1 + this.memory.constant.WHEEL_ZOOM_SPEED; 34 | offsetX = offsetX * ratio + this.memory.pointerOffset.current.x; 35 | offsetY = offsetY * ratio + this.memory.pointerOffset.current.y; 36 | } 37 | } 38 | 39 | $offset.newOffsetX = offsetX; 40 | $offset.newOffsetY = offsetY; 41 | 42 | return $offset; 43 | } 44 | 45 | updateOffsetWithGivenPoint($x: number, $y: number, $offset: Offset, $deltaFlg: boolean): Offset { 46 | let offsetX: number = $offset.prevOffsetX; 47 | let offsetY: number = $offset.prevOffsetY; 48 | 49 | offsetX -= $x; 50 | offsetY -= $y; 51 | 52 | if ($deltaFlg) { 53 | const ratio: number = 1 - this.memory.constant.POINTER_ZOOM_SPEED; 54 | offsetX = offsetX * ratio + $x; 55 | offsetY = offsetY * ratio + $y; 56 | } else { 57 | const ratio: number = 1 + this.memory.constant.POINTER_ZOOM_SPEED; 58 | offsetX = offsetX * ratio + $x; 59 | offsetY = offsetY * ratio + $y; 60 | } 61 | 62 | $offset.newOffsetX = offsetX; 63 | $offset.newOffsetY = offsetY; 64 | 65 | return $offset; 66 | } 67 | 68 | updateZoomRatioByWheel($zoomRatio: number, $event: Pointer): number { 69 | let zoomRatio: number = $zoomRatio; 70 | 71 | let ratio = 1; 72 | if ($event.delta > 0) { 73 | // Negative zoom 74 | ratio -= this.memory.constant.WHEEL_ZOOM_SPEED; 75 | } else { 76 | // Positive zoom 77 | ratio += this.memory.constant.WHEEL_ZOOM_SPEED; 78 | } 79 | 80 | zoomRatio *= ratio; 81 | 82 | return zoomRatio; 83 | } 84 | 85 | updateZoomRatioByPointer($zoomRatio: number, $deltaFlg: boolean): number { 86 | let zoomRatio: number = $zoomRatio; 87 | 88 | let ratio = 1; 89 | if ($deltaFlg) { 90 | // Negative zoom 91 | ratio -= this.memory.constant.POINTER_ZOOM_SPEED; 92 | } else { 93 | // Positive zoom 94 | ratio += this.memory.constant.POINTER_ZOOM_SPEED; 95 | } 96 | 97 | zoomRatio *= ratio; 98 | 99 | return zoomRatio; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/shared/service/util/debug.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MemoryService } from '../core/memory.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class DebugService { 8 | private queueList: Function[] = []; 9 | 10 | constructor(private memory: MemoryService) {} 11 | 12 | setToQueue($callback: Function): void { 13 | this.queueList.push($callback); 14 | } 15 | 16 | render(): void { 17 | const ctxDebugger: CanvasRenderingContext2D = this.memory.renderer.ctx.debugger; 18 | const c: HTMLCanvasElement = ctxDebugger.canvas; 19 | c.width = this.memory.renderer.canvasWrapper.clientWidth; 20 | c.height = this.memory.renderer.canvasWrapper.clientHeight; 21 | 22 | ctxDebugger.translate(0.5, 0.5); 23 | 24 | for (let i = 0; i < this.queueList.length; i++) { 25 | this.queueList[i](ctxDebugger, this.memory); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/service/util/lib.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Trail } from '../../model/trail.model'; 3 | import { PointerOffset } from '../../model/pointer-offset.model'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class LibService { 9 | constructor() {} 10 | 11 | f2i($num: number): number { 12 | let rounded: number = (0.5 + $num) | 0; 13 | rounded = ~~(0.5 + $num); 14 | rounded = (0.5 + $num) << 0; 15 | 16 | return rounded; 17 | } 18 | 19 | genUniqueColor($colorsHash: { id: number; colorId: string }[]): string { 20 | let colorId = ''; 21 | let isUnique = false; 22 | 23 | while (!isUnique) { 24 | colorId = this._getRandomColor(); 25 | isUnique = colorId.length === 6 && colorId !== '000000'; 26 | if (isUnique) { 27 | isUnique = 28 | $colorsHash.filter(($ids: { id: number; colorId: string }) => { 29 | return $ids.colorId === colorId; 30 | }).length === 0; 31 | } 32 | } 33 | 34 | return colorId; 35 | } 36 | 37 | // https://css-tricks.com/snippets/javascript/random-hex-color/ 38 | private _getRandomColor(): string { 39 | return Math.floor(Math.random() * 16777215).toString(16); 40 | } 41 | 42 | checkHitArea($pointerOffset: PointerOffset, $ctx: CanvasRenderingContext2D, $list: Trail[]): number { 43 | const pixel = $ctx.getImageData($pointerOffset.current.x, $pointerOffset.current.y, 1, 1).data; 44 | const hex = this.rgbToHex(pixel[0], pixel[1], pixel[2]); 45 | const n: number = $list.length; 46 | 47 | for (let i = n - 1; i > -1; i--) { 48 | if (hex === $list[i].colorId) return i; 49 | } 50 | 51 | return -1; 52 | } 53 | 54 | rgbToHex(r: number, g: number, b: number): string { 55 | if (r > 255 || g > 255 || b > 255) console.log('Failed to convert RGB into HEX : ', r, g, b); 56 | return ((r << 16) | (g << 8) | b).toString(16); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/tool-bar/tool-bar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
選択
5 |
6 |
7 | 8 |
手のひら
9 |
10 |
11 | 12 |
ペン
13 |
14 |
15 | 16 |
消しゴム
17 |
18 |
19 | 20 |
図形の作成:四角形
21 |
22 |
23 |
24 |
図形の作成:直線
25 |
26 |
27 | 28 |
拡大・縮小
29 |
30 |
31 | -------------------------------------------------------------------------------- /src/app/tool-bar/tool-bar.component.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | .tool-bar-wrapper { 4 | display: flex; 5 | flex-direction: column; 6 | border-top: solid 1px $mid-white; 7 | border-right: solid 1px $mid-white; 8 | padding: 4px; 9 | height: 100%; 10 | 11 | .icon-prefix { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | width: 30px; 17 | height: 30px; 18 | color: rgba($white, 0.6); 19 | font-size: 1rem; 20 | margin: 1px 0; 21 | 22 | &:hover, 23 | &.active { 24 | border-radius: 2px; 25 | background-color: rgba($white, 0.2); 26 | } 27 | } 28 | 29 | .invert-icon { 30 | transform: scale(-1, 1); 31 | } 32 | } 33 | 34 | .fa-line { 35 | width: 2px; 36 | height: 50%; 37 | background-color: rgba($white, 0.6); 38 | transform: rotate(45deg); 39 | } 40 | 41 | .name-info-wrapper { 42 | position: relative; 43 | 44 | &:hover { 45 | .name-info-column { 46 | transition-delay: 500ms; 47 | opacity: 1; 48 | } 49 | } 50 | 51 | .name-info-column { 52 | display: flex; 53 | align-content: center; 54 | justify-content: center; 55 | position: absolute; 56 | z-index: 100; 57 | top: 50%; 58 | left: 45px; 59 | transform: translateY(-50%); 60 | font-size: 0.7rem; 61 | font-weight: bold; 62 | padding: 5px 10px; 63 | border-radius: 3px; 64 | white-space: nowrap; 65 | opacity: 0; 66 | 67 | color: white; 68 | background-color: black; 69 | border: none; 70 | 71 | user-select: none; 72 | pointer-events: none; 73 | transition: opacity $transition; 74 | 75 | &:before { 76 | content: ''; 77 | position: absolute; 78 | top: 50%; 79 | left: -12px; 80 | transform: translateY(-50%); 81 | border: 6px solid transparent; 82 | border-right: 6px solid black; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/tool-bar/tool-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToolBarComponent } from './tool-bar.component'; 4 | 5 | describe('ToolBarComponent', () => { 6 | let component: ToolBarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ToolBarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ToolBarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/tool-bar/tool-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { MemoryService } from '../shared/service/core/memory.service'; 3 | import { FuncService } from '../shared/service/core/func.service'; 4 | 5 | // Fontawesome 6 | import { faHandPaper } from '@fortawesome/free-regular-svg-icons'; 7 | import { faMousePointer } from '@fortawesome/free-solid-svg-icons'; 8 | import { faPenNib } from '@fortawesome/free-solid-svg-icons'; 9 | import { faEraser } from '@fortawesome/free-solid-svg-icons'; 10 | import { faSquare } from '@fortawesome/free-regular-svg-icons'; 11 | import { faCircle } from '@fortawesome/free-regular-svg-icons'; 12 | import { faSearch } from '@fortawesome/free-solid-svg-icons'; 13 | 14 | @Component({ 15 | selector: 'app-tool-bar', 16 | templateUrl: './tool-bar.component.html', 17 | styleUrls: ['./tool-bar.component.scss'] 18 | }) 19 | export class ToolBarComponent implements OnInit { 20 | @ViewChild('select', { static: true }) select: ElementRef; 21 | @ViewChild('hand', { static: true }) hand: ElementRef; 22 | @ViewChild('pen', { static: true }) pen: ElementRef; 23 | @ViewChild('eraser', { static: true }) eraser: ElementRef; 24 | @ViewChild('createSquare', { static: true }) createSquare: ElementRef; 25 | @ViewChild('createLine', { static: true }) createLine: ElementRef; 26 | @ViewChild('zoom', { static: true }) zoom: ElementRef; 27 | 28 | faMousePointer = faMousePointer; 29 | faHandPaper = faHandPaper; 30 | faPenNib = faPenNib; 31 | faEraser = faEraser; 32 | faSquare = faSquare; 33 | faZoom = faSearch; 34 | 35 | constructor(private memory: MemoryService, private func: FuncService) {} 36 | 37 | ngOnInit(): void { 38 | this.render(); 39 | } 40 | 41 | execFunc($name: string): void { 42 | switch ($name) { 43 | case 'select': 44 | this.func.select(); 45 | break; 46 | 47 | case 'hand': 48 | this.func.hand(); 49 | break; 50 | 51 | case 'pen': 52 | this.func.pen(); 53 | break; 54 | 55 | case 'eraser': 56 | this.func.eraser(); 57 | break; 58 | 59 | case 'square': 60 | this.func.createSquare(); 61 | break; 62 | 63 | case 'line': 64 | this.func.createLine(); 65 | break; 66 | 67 | case 'zoom': 68 | this.func.zoom(true); 69 | break; 70 | 71 | default: 72 | break; 73 | } 74 | } 75 | 76 | render(): void { 77 | const r: FrameRequestCallback = () => { 78 | this._render(); 79 | 80 | requestAnimationFrame(r); 81 | }; 82 | requestAnimationFrame(r); 83 | } 84 | 85 | private _render(): void { 86 | const name: string = this.memory.reservedByFunc.current.name; 87 | let t: HTMLDivElement; 88 | 89 | switch (name) { 90 | case 'select': 91 | t = this.select.nativeElement; 92 | break; 93 | 94 | case 'hand': 95 | t = this.hand.nativeElement; 96 | break; 97 | 98 | case 'pen': 99 | t = this.pen.nativeElement; 100 | break; 101 | 102 | case 'eraser': 103 | t = this.eraser.nativeElement; 104 | break; 105 | 106 | case 'square': 107 | t = this.createSquare.nativeElement; 108 | break; 109 | 110 | case 'line': 111 | t = this.createLine.nativeElement; 112 | break; 113 | 114 | case 'zoom': 115 | t = this.zoom.nativeElement; 116 | break; 117 | 118 | default: 119 | this._resetToolBarClassAll(); 120 | return; 121 | break; 122 | } 123 | 124 | // Toggle active 125 | this._toggleActive(t); 126 | } 127 | 128 | private _toggleActive($targetElem: HTMLDivElement): void { 129 | if ($targetElem.classList.contains('active')) return; 130 | 131 | this._resetToolBarClassAll(); 132 | $targetElem.classList.add('active'); 133 | } 134 | 135 | private _resetToolBarClassAll(): void { 136 | this._resetToolBarClass(this.select.nativeElement); 137 | this._resetToolBarClass(this.hand.nativeElement); 138 | this._resetToolBarClass(this.pen.nativeElement); 139 | this._resetToolBarClass(this.eraser.nativeElement); 140 | this._resetToolBarClass(this.createSquare.nativeElement); 141 | this._resetToolBarClass(this.createLine.nativeElement); 142 | this._resetToolBarClass(this.zoom.nativeElement); 143 | } 144 | 145 | private _resetToolBarClass($targetElem: HTMLDivElement): void { 146 | $targetElem.classList.remove('active'); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/app/tool-menu/tool-menu.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
保存
5 |
6 |
7 | 8 |
元に戻す
9 |
10 |
11 | 12 |
やり直す
13 |
14 |
15 | 16 |
白紙に戻す
17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
カラーピッカー
26 |
27 |
28 |
29 |
30 |
{{ drawBrushSize }}px
31 |
{{ eraseBrushSize }}px
32 |
33 |
ブラシのサイズ
34 |
35 |
36 | -------------------------------------------------------------------------------- /src/app/tool-menu/tool-menu.component.scss: -------------------------------------------------------------------------------- 1 | @import "../variables"; 2 | 3 | .tool-menu-wrapper { 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | width: 100vw; 8 | padding: 5px; 9 | border-top: solid 1px $mid-white; 10 | border-bottom: solid 1px $mid-white; 11 | 12 | user-select: none; 13 | } 14 | 15 | .icon-prefix { 16 | position: relative; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: center; 20 | align-items: center; 21 | width: 30px; 22 | height: 100%; 23 | color: rgba($white, 0.6); 24 | font-size: 1rem; 25 | 26 | &:hover { 27 | border-radius: 2px; 28 | background-color: rgba($white, 0.2); 29 | } 30 | } 31 | 32 | .name-info-wrapper { 33 | position: relative; 34 | margin: 0 1px; 35 | 36 | &:hover { 37 | .name-info-row { 38 | transition-delay: 500ms; 39 | opacity: 1; 40 | } 41 | } 42 | 43 | .name-info-row { 44 | display: flex; 45 | align-content: center; 46 | justify-content: center; 47 | position: absolute; 48 | z-index: 100; 49 | top: 40px; 50 | left: 50%; 51 | transform: translateX(-50%); 52 | font-size: 0.7rem; 53 | font-weight: bold; 54 | padding: 5px 10px; 55 | border-radius: 3px; 56 | white-space: nowrap; 57 | opacity: 0; 58 | 59 | color: white; 60 | background-color: black; 61 | border: none; 62 | 63 | user-select: none; 64 | pointer-events: none; 65 | transition: opacity $transition; 66 | 67 | &:before { 68 | content: ""; 69 | position: absolute; 70 | top: -12px; 71 | left: 50%; 72 | transform: translateX(-50%); 73 | border: 6px solid transparent; 74 | border-bottom: 6px solid black; 75 | } 76 | 77 | &.in-active { 78 | display: none; 79 | } 80 | } 81 | } 82 | 83 | .separator-wrapper { 84 | display: flex; 85 | flex-direction: column; 86 | justify-content: center; 87 | align-items: center; 88 | width: 30px; 89 | height: 100%; 90 | 91 | .separator { 92 | width: 1px; 93 | height: 20px; 94 | background-color: rgba($white, 0.3); 95 | } 96 | } 97 | 98 | .tool-menu-prefix { 99 | margin: 0 6px; 100 | } 101 | 102 | .brush-size-wrapper { 103 | position: relative; 104 | width: 60px; 105 | height: 18px; 106 | border: solid 1px rgba($white, 0.6); 107 | border-radius: 2px; 108 | 109 | .brush-size-meter { 110 | width: 1%; 111 | height: 100%; 112 | background-color: rgba($white, 0.3); 113 | } 114 | 115 | .brush-size-info { 116 | position: absolute; 117 | top: 0; 118 | left: 50%; 119 | font-size: 0.7rem; 120 | font-weight: bold; 121 | transform: translateX(-50%); 122 | color: $white; 123 | pointer-events: none; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/app/tool-menu/tool-menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToolMenuComponent } from './tool-menu.component'; 4 | 5 | describe('ToolMenuComponent', () => { 6 | let component: ToolMenuComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ToolMenuComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ToolMenuComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/tool-menu/tool-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { MemoryService } from '../shared/service/core/memory.service'; 3 | import { FuncService } from '../shared/service/core/func.service'; 4 | 5 | import Pickr from '@simonwep/pickr/dist/pickr.es5.min'; 6 | 7 | // Fontawesome 8 | import { faSave } from '@fortawesome/free-regular-svg-icons'; 9 | import { faUndo } from '@fortawesome/free-solid-svg-icons'; 10 | import { faRedo } from '@fortawesome/free-solid-svg-icons'; 11 | import { faQuidditch } from '@fortawesome/free-solid-svg-icons'; 12 | 13 | @Component({ 14 | selector: 'app-tool-menu', 15 | templateUrl: './tool-menu.component.html', 16 | styleUrls: ['./tool-menu.component.scss'] 17 | }) 18 | export class ToolMenuComponent implements OnInit { 19 | @ViewChild('pickrInfo', { static: true }) pickrInfoRef: ElementRef; 20 | @ViewChild('brushSizeWrapper', { static: true }) brushSizeWrapperRef: ElementRef; 21 | @ViewChild('brushSizeMeter', { static: true }) brushSizeMeterRef: ElementRef; 22 | 23 | // Fontawesome 24 | faSave = faSave; 25 | faUndo = faUndo; 26 | faRedo = faRedo; 27 | faQuidditch = faQuidditch; 28 | 29 | // Brush size 30 | previousBrushState = 'draw'; 31 | drawBrushSize = this.memory.brush.lineWidth.draw; 32 | eraseBrushSize = this.memory.brush.lineWidth.erase; 33 | 34 | constructor(private memory: MemoryService, private func: FuncService) {} 35 | 36 | ngOnInit(): void { 37 | // Initialize brushSizeSlider 38 | this.memory.initBrushSizeSlider(this.brushSizeWrapperRef, this.brushSizeMeterRef); 39 | 40 | // Create pickr 41 | this._createPickr(); 42 | 43 | // Check current states for tool-menu 44 | this.render(); 45 | } 46 | 47 | private _createPickr(): void { 48 | const pickr: any = Pickr.create({ 49 | el: '#pickr', 50 | container: '#pickr-wrapper', 51 | theme: 'monolith', 52 | appClass: 'custom-pickr', 53 | padding: 20, 54 | default: this.memory.brush.color, 55 | 56 | autoReposition: true, 57 | 58 | swatches: [ 59 | 'rgba(244, 67, 54, 1)', 60 | 'rgba(233, 30, 99, 0.95)', 61 | 'rgba(156, 39, 176, 0.9)', 62 | 'rgba(103, 58, 183, 0.85)', 63 | 'rgba(63, 81, 181, 0.8)', 64 | 'rgba(33, 150, 243, 0.75)', 65 | 'rgba(3, 169, 244, 0.7)', 66 | 'rgba(0, 188, 212, 0.7)', 67 | 'rgba(0, 150, 136, 0.75)', 68 | 'rgba(76, 175, 80, 0.8)', 69 | 'rgba(139, 195, 74, 0.85)', 70 | 'rgba(205, 220, 57, 0.9)', 71 | 'rgba(255, 235, 59, 0.95)', 72 | 'rgba(255, 193, 7, 1)' 73 | ], 74 | 75 | components: { 76 | // Main components 77 | preview: true, 78 | opacity: true, 79 | hue: true, 80 | 81 | // Input / output Options 82 | interaction: { 83 | hex: false, 84 | rgba: false, 85 | hsla: false, 86 | hsva: false, 87 | cmyk: false, 88 | input: false, 89 | clear: false, 90 | save: true 91 | } 92 | }, 93 | 94 | i18n: { 95 | // Strings visible in the UI 96 | 'ui:dialog': 'color picker dialog', 97 | 'btn:toggle': 'toggle color picker dialog', 98 | 'btn:swatch': 'color swatch', 99 | 'btn:last-color': 'use previous color', 100 | 'btn:save': '適用', 101 | 'btn:cancel': 'Cancel', 102 | 'btn:clear': 'Clear', 103 | 104 | // Strings used for aria-labels 105 | 'aria:btn:save': 'save and close', 106 | 'aria:btn:cancel': 'cancel and close', 107 | 'aria:btn:clear': 'clear and close', 108 | 'aria:input': 'color input field', 109 | 'aria:palette': 'color selection area', 110 | 'aria:hue': 'hue selection slider', 111 | 'aria:opacity': 'selection slider' 112 | } 113 | }); 114 | 115 | this._pickrEvents(pickr); 116 | } 117 | 118 | private _pickrEvents($pickr: any): void { 119 | $pickr 120 | .on('init', (instance) => {}) 121 | .on('hide', (instance) => { 122 | // Remove in-active after after completelly hided pickr 123 | this.pickrInfoRef.nativeElement.classList.remove('in-active'); 124 | this.memory.states.isCanvasLocked = false; 125 | }) 126 | .on('show', (color, instance) => { 127 | this.pickrInfoRef.nativeElement.classList.add('in-active'); 128 | this.memory.states.isCanvasLocked = true; 129 | }) 130 | .on('save', (color, instance) => { 131 | // Set brush color 132 | const rgba: string = color.toRGBA().toString(); 133 | this.memory.brush.color = rgba; 134 | 135 | // Hide pickr 136 | $pickr.hide(); 137 | }) 138 | .on('clear', (instance) => {}) 139 | .on('change', (color, instance) => {}) 140 | .on('changestop', (instance) => {}) 141 | .on('cancel', (instance) => {}) 142 | .on('swatchselect', (color, instance) => {}); 143 | } 144 | 145 | private render(): void { 146 | const r: FrameRequestCallback = () => { 147 | this._render(); 148 | 149 | requestAnimationFrame(r); 150 | }; 151 | requestAnimationFrame(r); 152 | } 153 | 154 | private _render(): void { 155 | const isDrawBrush = this.memory.reservedByFunc.current.type === 'draw'; 156 | const isEraseBrush = this.memory.reservedByFunc.current.type === 'erase'; 157 | if (isDrawBrush) { 158 | this.previousBrushState = 'draw'; 159 | this.drawBrushSize = this.memory.brush.lineWidth.draw; 160 | this.memory.brushSizeSlider.meter.style.width = this.memory.brush.meterWidth.draw + '%'; 161 | } else if (isEraseBrush) { 162 | this.previousBrushState = 'erase'; 163 | this.eraseBrushSize = this.memory.brush.lineWidth.erase; 164 | this.memory.brushSizeSlider.meter.style.width = this.memory.brush.meterWidth.erase + '%'; 165 | } 166 | } 167 | 168 | save(): void { 169 | this.func.save(); 170 | } 171 | 172 | undo(): void { 173 | this.func.undo(); 174 | } 175 | 176 | redo(): void { 177 | this.func.redo(); 178 | } 179 | 180 | cleanUp(): void { 181 | this.func.cleanUp(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkihrk/infi-draw/a38c1df1a4a6950ab4b916a28fe96257df870e20/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkihrk/infi-draw/a38c1df1a4a6950ab4b916a28fe96257df870e20/src/assets/image.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkihrk/infi-draw/a38c1df1a4a6950ab4b916a28fe96257df870e20/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | InfiDraw 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import './reset'; 2 | @import './app/variables'; 3 | // For pickr 4 | @import '@simonwep/pickr/dist/themes/monolith.min.css'; 5 | 6 | html { 7 | overflow: hidden; 8 | background-color: #32303f; 9 | } 10 | 11 | .custom-pickr { 12 | background-color: #32303f; 13 | border: solid 1px $mid-white; 14 | } 15 | 16 | .not-implemented-yet { 17 | pointer-events: none; 18 | color: rgba($white, 0.3) !important; 19 | 20 | .fa-line { 21 | background-color: rgba($white, 0.3) !important; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.app.json" 12 | }, 13 | { 14 | "path": "./tsconfig.spec.json" 15 | }, 16 | { 17 | "path": "./e2e/tsconfig.json" 18 | } 19 | ], 20 | "compilerOptions": { 21 | "emitDecoratorMetadata": true, 22 | "experimentalDecorators": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------