├── .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 | 
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------