Repository: nhn/tui.image-editor Branch: master Commit: 9ee993e21135 Files: 223 Total size: 905.1 KB Directory structure: gitextract__edmw0b4/ ├── .editorconfig ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── feature_request.md │ │ └── question.md │ ├── auto-comment.yml │ ├── composite-actions/ │ │ ├── build-package/ │ │ │ └── action.yml │ │ ├── install-dependencies/ │ │ │ └── action.yml │ │ ├── publish-cdn/ │ │ │ └── action.yml │ │ ├── publish-docs/ │ │ │ └── action.yml │ │ └── publish-package/ │ │ └── action.yml │ ├── stale.yml │ └── workflows/ │ ├── detectRuntimeError.yml │ ├── publish-docs.yml │ ├── publish-npm.yml │ └── publish-wrapper.yml ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── apps/ │ ├── image-editor/ │ │ ├── README.md │ │ ├── __mocks__/ │ │ │ ├── fileMock.js │ │ │ └── svgMock.js │ │ ├── createConfigVariable.js │ │ ├── examples/ │ │ │ ├── css/ │ │ │ │ ├── service-basic.css │ │ │ │ ├── service-mobile.css │ │ │ │ └── tui-example-style.css │ │ │ ├── example01-includeUi.html │ │ │ ├── example02-useApiDirect.html │ │ │ ├── example03-mobile.html │ │ │ ├── examples.json │ │ │ └── js/ │ │ │ ├── service-basic.js │ │ │ ├── service-mobile.js │ │ │ └── theme/ │ │ │ ├── black-theme.js │ │ │ └── white-theme.js │ │ ├── index.d.ts │ │ ├── jest-setup.js │ │ ├── jest.config.js │ │ ├── makesvg.js │ │ ├── package.json │ │ ├── scripts/ │ │ │ ├── publishToCDN.js │ │ │ └── updateWrapper.js │ │ ├── src/ │ │ │ ├── css/ │ │ │ │ ├── buttons.styl │ │ │ │ ├── checkbox.styl │ │ │ │ ├── colorpicker.styl │ │ │ │ ├── gridtable.styl │ │ │ │ ├── icon.styl │ │ │ │ ├── index.styl │ │ │ │ ├── main.styl │ │ │ │ ├── position.styl │ │ │ │ ├── range.styl │ │ │ │ └── submenu.styl │ │ │ ├── index.js │ │ │ └── js/ │ │ │ ├── action.js │ │ │ ├── command/ │ │ │ │ ├── addIcon.js │ │ │ │ ├── addImageObject.js │ │ │ │ ├── addObject.js │ │ │ │ ├── addShape.js │ │ │ │ ├── addText.js │ │ │ │ ├── applyFilter.js │ │ │ │ ├── changeIconColor.js │ │ │ │ ├── changeSelection.js │ │ │ │ ├── changeShape.js │ │ │ │ ├── changeText.js │ │ │ │ ├── changeTextStyle.js │ │ │ │ ├── clearObjects.js │ │ │ │ ├── flip.js │ │ │ │ ├── loadImage.js │ │ │ │ ├── removeFilter.js │ │ │ │ ├── removeObject.js │ │ │ │ ├── resize.js │ │ │ │ ├── resizeCanvasDimension.js │ │ │ │ ├── rotate.js │ │ │ │ ├── setObjectPosition.js │ │ │ │ └── setObjectProperties.js │ │ │ ├── component/ │ │ │ │ ├── cropper.js │ │ │ │ ├── filter.js │ │ │ │ ├── flip.js │ │ │ │ ├── freeDrawing.js │ │ │ │ ├── icon.js │ │ │ │ ├── imageLoader.js │ │ │ │ ├── line.js │ │ │ │ ├── resize.js │ │ │ │ ├── rotation.js │ │ │ │ ├── shape.js │ │ │ │ ├── text.js │ │ │ │ └── zoom.js │ │ │ ├── consts.js │ │ │ ├── drawingMode/ │ │ │ │ ├── cropper.js │ │ │ │ ├── freeDrawing.js │ │ │ │ ├── icon.js │ │ │ │ ├── lineDrawing.js │ │ │ │ ├── resize.js │ │ │ │ ├── shape.js │ │ │ │ ├── text.js │ │ │ │ └── zoom.js │ │ │ ├── extension/ │ │ │ │ ├── arrowLine.js │ │ │ │ ├── blur.js │ │ │ │ ├── colorFilter.js │ │ │ │ ├── cropzone.js │ │ │ │ ├── emboss.js │ │ │ │ ├── mask.js │ │ │ │ └── sharpen.js │ │ │ ├── factory/ │ │ │ │ ├── command.js │ │ │ │ └── errorMessage.js │ │ │ ├── graphics.js │ │ │ ├── helper/ │ │ │ │ ├── imagetracer.js │ │ │ │ ├── selectionModifyHelper.js │ │ │ │ ├── shapeFilterFillHelper.js │ │ │ │ └── shapeResizeHelper.js │ │ │ ├── imageEditor.js │ │ │ ├── interface/ │ │ │ │ ├── command.js │ │ │ │ ├── component.js │ │ │ │ └── drawingMode.js │ │ │ ├── invoker.js │ │ │ ├── polyfill.js │ │ │ ├── ui/ │ │ │ │ ├── crop.js │ │ │ │ ├── draw.js │ │ │ │ ├── filter.js │ │ │ │ ├── flip.js │ │ │ │ ├── history.js │ │ │ │ ├── icon.js │ │ │ │ ├── locale/ │ │ │ │ │ └── locale.js │ │ │ │ ├── mask.js │ │ │ │ ├── panelMenu.js │ │ │ │ ├── resize.js │ │ │ │ ├── rotate.js │ │ │ │ ├── shape.js │ │ │ │ ├── submenuBase.js │ │ │ │ ├── template/ │ │ │ │ │ ├── controls.js │ │ │ │ │ ├── mainContainer.js │ │ │ │ │ ├── style.js │ │ │ │ │ └── submenu/ │ │ │ │ │ ├── crop.js │ │ │ │ │ ├── draw.js │ │ │ │ │ ├── filter.js │ │ │ │ │ ├── flip.js │ │ │ │ │ ├── history.js │ │ │ │ │ ├── icon.js │ │ │ │ │ ├── mask.js │ │ │ │ │ ├── resize.js │ │ │ │ │ ├── rotate.js │ │ │ │ │ ├── shape.js │ │ │ │ │ ├── text.js │ │ │ │ │ └── zoom.js │ │ │ │ ├── text.js │ │ │ │ ├── theme/ │ │ │ │ │ ├── standard.js │ │ │ │ │ └── theme.js │ │ │ │ └── tools/ │ │ │ │ ├── colorpicker.js │ │ │ │ └── range.js │ │ │ ├── ui.js │ │ │ └── util.js │ │ ├── tests/ │ │ │ ├── __snapshots__/ │ │ │ │ ├── arrowLine.spec.js.snap │ │ │ │ ├── shape.spec.js.snap │ │ │ │ ├── text.spec.js.snap │ │ │ │ └── theme.spec.js.snap │ │ │ ├── action.spec.js │ │ │ ├── arrowLine.spec.js │ │ │ ├── command.spec.js │ │ │ ├── cropper.spec.js │ │ │ ├── cropzone.spec.js │ │ │ ├── drawingMode.spec.js │ │ │ ├── filter.spec.js │ │ │ ├── flip.spec.js │ │ │ ├── graphics.spec.js │ │ │ ├── history.spec.js │ │ │ ├── icon.spec.js │ │ │ ├── imageEditor.spec.js │ │ │ ├── index.js │ │ │ ├── invoker.spec.js │ │ │ ├── line.spec.js │ │ │ ├── promiseApi.spec.js │ │ │ ├── resize.spec.js │ │ │ ├── rotation.spec.js │ │ │ ├── selectionModifyHelper.spec.js │ │ │ ├── shape.spec.js │ │ │ ├── text.spec.js │ │ │ ├── theme.spec.js │ │ │ ├── types/ │ │ │ │ ├── tsconfig.json │ │ │ │ └── type-tests.ts │ │ │ ├── ui.spec.js │ │ │ ├── uiRange.spec.js │ │ │ └── zoom.spec.js │ │ ├── tsBannerGenerator.js │ │ ├── tuidoc.config.json │ │ ├── webpack.common.config.js │ │ ├── webpack.config.js │ │ ├── webpack.dev.config.js │ │ └── webpack.prod.config.js │ ├── react-image-editor/ │ │ ├── .babelrc.json │ │ ├── .eslintrc.js │ │ ├── .storybook/ │ │ │ ├── main.js │ │ │ └── preview.js │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.js │ │ ├── stories/ │ │ │ └── index.stories.js │ │ └── webpack.config.js │ └── vue-image-editor/ │ ├── .eslintrc.js │ ├── .storybook/ │ │ ├── main.js │ │ └── preview.js │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── ImageEditor.vue │ │ └── index.js │ ├── stories/ │ │ └── index.stories.js │ ├── vue.config.js │ └── webpack.config.js ├── babel.config.json ├── bower.json ├── docs/ │ ├── Apply-Mobile-Version-Image.md │ ├── Apply-Mobile-Version.md │ ├── Basic-Tutorial.md │ ├── COMMIT_MESSAGE_CONVENTION.md │ ├── ISSUE_TEMPLATE.md │ ├── ImageEditor-2.0.0-Migration-guide.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── Reference.md │ └── Structure.md ├── lerna.json └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, extends: ['tui/es6', 'plugin:jest/recommended', 'plugin:prettier/recommended'], plugins: ['jest', 'prettier'], env: { browser: true, amd: true, node: true, es6: true, jest: true, 'jest/globals': true, }, parser: '@babel/eslint-parser', parserOptions: { sourceType: 'module', babelOptions: { rootMode: 'upward', }, }, ignorePatterns: ['node_modules/*', 'dist', 'examples'], rules: { 'prefer-destructuring': [ 'error', { VariableDeclarator: { array: true, object: true }, AssignmentExpression: { array: false, object: false }, }, ], }, overrides: [ { files: ['*.spec.js'], rules: { 'max-nested-callbacks': ['error', { max: 5 }], 'dot-notation': ['error', { allowKeywords: true }], 'no-undefined': 'off', 'jest/expect-expect': ['error', { assertFunctionNames: ['expect', 'assert*'] }], }, }, ], }; ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: Bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: Enhancement, Need Discussion assignees: '' --- ## Version ## Development Environment ## Current Behavior ```js // Write example code ``` ## Expected Behavior ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: Create a question about imageEditor title: '' labels: Question assignees: '' --- **Summary** A clear and concise description of what the question is. **Screenshots** If applicable, add screenshots to help explain your question. **Version** Write the version of the imageEditor you are currently using. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/auto-comment.yml ================================================ # Comment to a new issue. issuesOpened: > Thank you for raising an issue. We will try and get back to you as soon as possible. Please make sure you have filled out issue respecting our form **in English** and given us as much context as possible. **If not, the issue will be closed or not replied.** ================================================ FILE: .github/composite-actions/build-package/action.yml ================================================ name: 'Build' description: 'Build package' inputs: ROOT_CACHE_KEY: description: 'Key of root dependencies cache' required: true BUILD_CACHE_KEY: description: 'Key of build cache' required: true runs: using: 'composite' steps: - name: Use Node.js 15.x uses: actions/setup-node@v1 with: node-version: '15.x' registry-url: https://registry.npmjs.org/ - name: Check root dependencies cache uses: actions/cache@v2 with: path: ./node_modules key: ${{ inputs.ROOT_CACHE_KEY }} - name: Install package dependencies and remove package-lock file working-directory: ./apps/image-editor run: | npm install && rm -rf ./package-lock.json shell: bash - name: Check build cache uses: actions/cache@v2 id: cache_built_packages with: path: ./apps/image-editor/dist key: ${{ inputs.BUILD_CACHE_KEY }} - name: Build package working-directory: ./apps/image-editor run: | if echo ${{ steps.cache_built_packages.outputs.cache-hit }} | grep -c "true" then echo "Cache hit - skipping building" else npm run build fi shell: bash ================================================ FILE: .github/composite-actions/install-dependencies/action.yml ================================================ name: 'Install root dependencies using cache' description: 'Set Node.js version and install node_modules' outputs: root_cache_key: description: 'Key of root dependencies cache' value: ${{ steps.root_lockfile_hash.outputs.hash }} runs: using: 'composite' steps: - name: Use Node.js 15.x uses: actions/setup-node@v1 with: node-version: '15.x' registry-url: https://registry.npmjs.org/ - name: Compute root dependencies cache key id: root_lockfile_hash run: echo "::set-output name=hash::${{ hashFiles('package-lock.json') }}" shell: bash - name: Check root dependencies cache uses: actions/cache@v2 id: root_cache_dependencies with: path: ./node_modules key: ${{ steps.root_lockfile_hash.outputs.hash }} - name: Install root dependencies run: | if echo ${{ steps.root_cache_dependencies.outputs.cache-hit }} | grep -c "true" then echo "Cache hit - skipping root dependency installation" else npm install fi shell: bash ================================================ FILE: .github/composite-actions/publish-cdn/action.yml ================================================ name: 'Publish CDN' description: 'Publish CDN' inputs: ROOT_CACHE_KEY: description: 'Key of root dependencies cache' required: true BUILD_CACHE_KEY: description: 'Key of build cache' required: true TOAST_CLOUD_TENANTID: description: 'Tenant id for CDN' required: true TOAST_CLOUD_STORAGEID: description: 'Storage id for CDN' required: true TOAST_CLOUD_USERNAME: description: 'User name for CDN' required: true TOAST_CLOUD_PASSWORD: description: 'password for CDN' required: true runs: using: 'composite' steps: - name: Use Node.js 15.x uses: actions/setup-node@v1 with: node-version: '15.x' registry-url: https://registry.npmjs.org/ - name: Check root dependencies cache uses: actions/cache@v2 with: path: ./node_modules key: ${{ inputs.ROOT_CACHE_KEY }} - name: Install package dependencies and remove package-lock file working-directory: ./apps/image-editor run: | npm install && rm -rf ./package-lock.json shell: bash - name: Check build cache uses: actions/cache@v2 with: path: ./apps/image-editor/dist key: ${{ inputs.BUILD_CACHE_KEY }} - name: Upload files to CDN working-directory: ./apps/image-editor run: | npm run publish:cdn env: TOAST_CLOUD_TENANTID: ${{ inputs.TOAST_CLOUD_TENANTID }} TOAST_CLOUD_STORAGEID: ${{ inputs.TOAST_CLOUD_STORAGEID }} TOAST_CLOUD_USERNAME: ${{ inputs.TOAST_CLOUD_USERNAME }} TOAST_CLOUD_PASSWORD: ${{ inputs.TOAST_CLOUD_PASSWORD }} shell: bash ================================================ FILE: .github/composite-actions/publish-docs/action.yml ================================================ name: 'Publish docs' description: 'Publish docs' inputs: ROOT_CACHE_KEY: description: 'Key of root dependencies cache' required: true BUILD_CACHE_KEY: description: 'Key of build' required: true GITHUB_TOKEN: description: 'Github token' required: true runs: using: 'composite' steps: - name: Use Node.js 15.x uses: actions/setup-node@v1 with: node-version: '15.x' registry-url: https://registry.npmjs.org/ - name: Get package version id: version uses: PostHog/check-package-version@v2 with: path: ./apps/image-editor/ - name: Check root dependencies cache uses: actions/cache@v2 with: path: ./node_modules key: ${{ inputs.ROOT_CACHE_KEY }} - name: Install package dependencies and remove package-lock file working-directory: ./apps/image-editor run: | npm install && rm -rf ./package-lock.json shell: bash - name: Check build cache uses: actions/cache@v2 with: path: ./apps/image-editor/dist key: ${{ inputs.BUILD_CACHE_KEY }} - name: Install toast-ui doc run: npm i -g @toast-ui/doc shell: bash - name: Run doc working-directory: ./apps/image-editor run: | npm run doc mv _${{ steps.version.outputs.committed-version }} ../../_${{ steps.version.outputs.committed-version }} mv -i _latest ../../_latest rm -rf tmpdoc git stash --include-untracked shell: bash - name: Checkout gh-pages branch uses: actions/checkout@v2 with: ref: gh-pages - name: Commit files run: | git config --local user.name "lja1018" git config --local user.email "jaeeon.lim@nhn.com" rm -rf latest rm -rf ${{ steps.version.outputs.committed-version }} git stash pop mv _${{ steps.version.outputs.committed-version }} ${{ steps.version.outputs.committed-version }} mv -i _latest latest git add . git commit -m "v${{ steps.version.outputs.committed-version }}" shell: bash - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ inputs.GITHUB_TOKEN }} branch: gh-pages - name: Checkout branch uses: actions/checkout@v2 with: ref: master ================================================ FILE: .github/composite-actions/publish-package/action.yml ================================================ name: 'Publish package' description: 'Publish package' inputs: ROOT_CACHE_KEY: description: 'Key of root dependencies cache' required: true BUILD_CACHE_KEY: description: 'Key of build cache' required: true NODE_AUTH_TOKEN: description: 'NPM authorization token' required: true runs: using: 'composite' steps: - name: Use Node.js 15.x uses: actions/setup-node@v1 with: node-version: '15.x' registry-url: https://registry.npmjs.org/ - name: Checkout production branch uses: actions/checkout@v2 with: ref: production - name: Check root dependencies cache uses: actions/cache@v2 with: path: ./node_modules key: ${{ inputs.ROOT_CACHE_KEY }} - name: Install package dependencies and remove package-lock file working-directory: ./apps/image-editor run: | npm install && rm -rf ./package-lock.json shell: bash - name: Check build cache uses: actions/cache@v2 with: path: ./apps/image-editor/dist key: ${{ inputs.BUILD_CACHE_KEY }} - name: Publish package working-directory: ./apps/image-editor run: npm publish env: NODE_AUTH_TOKEN: ${{ inputs.NODE_AUTH_TOKEN }} shell: bash ================================================ FILE: .github/stale.yml ================================================ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 30 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 7 # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - Feature - Enhancement - Bug - NHN Cloud # Label to use when marking as stale staleLabel: inactive # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as inactive because there hasn’t been much going on it lately. It is going to be closed after 7 days. Thanks! # Comment to post when removing the stale label. # unmarkComment: > # Your comment here. # Comment to post when closing a stale Issue or Pull Request. closeComment: > This issue will be closed due to inactivity. Thanks for your contribution! ================================================ FILE: .github/workflows/detectRuntimeError.yml ================================================ name: detect runtime error on: schedule: - cron: '0 22 * * *' jobs: detectError: runs-on: ubuntu-latest env: WORKING_DIRECTORY: ./apps/image-editor steps: - name: checkout repository uses: actions/checkout@v2 - name: create config variable working-directory: ${{ env.WORKING_DIRECTORY }} run: | node createConfigVariable.js - name: set global error variable working-directory: ${{ env.WORKING_DIRECTORY }} run: | errorVariable=`cat ./errorVariable.txt` echo "ERROR_VARIABLE=$errorVariable" >> $GITHUB_ENV - name: set url working-directory: ${{ env.WORKING_DIRECTORY }} shell: bash run: | url=`cat ./url.txt` echo "URLS=$url" >> $GITHUB_ENV - name: detect runtime error uses: nhn/toast-ui.detect-runtime-error-actions@v1.0.1 with: global-error-log-variable: ${{ env.ERROR_VARIABLE }} urls: ${{ env.URLS }} env: BROWSERSTACK_USERNAME: ${{secrets.BROWSERSTACK_USERNAME}} BROWSERSTACK_ACCESS_KEY: ${{secrets.BROWSERSTACK_ACCESS_KEY}} ================================================ FILE: .github/workflows/publish-docs.yml ================================================ name: Publish docs on: [workflow_dispatch] env: BUILD_CACHE_KEY: ${{ github.sha }} jobs: install-dependencies: name: Install dependencies runs-on: ubuntu-latest steps: - name: Checkout branch uses: actions/checkout@v2 - name: Install dependencies id: cache-keys uses: ./.github/composite-actions/install-dependencies outputs: root_cache_key: ${{ steps.cache-keys.outputs.root_cache_key }} build: name: Build package using cache needs: [install-dependencies] runs-on: ubuntu-latest steps: - name: Checkout branch uses: actions/checkout@v2 with: ref: production - name: build uses: ./.github/composite-actions/build-package with: ROOT_CACHE_KEY: ${{ needs.install-dependencies.outputs.root_cache_key }} BUILD_CACHE_KEY: ${{ env.BUILD_CACHE_KEY }} outputs: root_cache_key: ${{ needs.install-dependencies.outputs.root_cache_key }} publish-docs: needs: [build] name: Publish docs runs-on: ubuntu-latest steps: - name: Checkout branch uses: actions/checkout@v2 with: ref: production - name: Publish docs uses: ./.github/composite-actions/publish-docs with: ROOT_CACHE_KEY: ${{ needs.build.outputs.root_cache_key }} BUILD_CACHE_KEY: ${{ env.BUILD_CACHE_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/publish-npm.yml ================================================ name: Publish NPM on: [workflow_dispatch] env: BUILD_CACHE_KEY: ${{ github.sha }} jobs: check-version: name: Check package version runs-on: ubuntu-latest steps: - name: Checkout production branch uses: actions/checkout@v2 - name: Check package version id: check uses: PostHog/check-package-version@v2 with: path: ./apps/image-editor/ - name: Cancel actions when version is unchanged uses: andymckay/cancel-action@0.2 if: steps.check.outputs.is-new-version == 'false' install-dependencies: name: Install dependencies needs: [check-version] runs-on: ubuntu-latest steps: - name: Checkout production branch uses: actions/checkout@v2 - name: Install dependencies id: cache-keys uses: ./.github/composite-actions/install-dependencies outputs: root_cache_key: ${{ steps.cache-keys.outputs.root_cache_key }} build: name: Build package using cache needs: [install-dependencies] runs-on: ubuntu-latest steps: - name: Checkout production branch uses: actions/checkout@v2 with: ref: production - name: build uses: ./.github/composite-actions/build-package with: ROOT_CACHE_KEY: ${{ needs.install-dependencies.outputs.root_cache_key }} BUILD_CACHE_KEY: ${{ env.BUILD_CACHE_KEY }} - name: Get package version id: version uses: PostHog/check-package-version@v2 with: path: ./apps/image-editor/ - name: Commit files run: | git config --local user.name "lja1018" git config --local user.email "jaeeon.lim@nhn.com" git add . git commit -m "chore: update version to v${{ steps.version.outputs.committed-version }}" shell: bash - name: Push built file uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: production outputs: root_cache_key: ${{ needs.install-dependencies.outputs.root_cache_key }} push-tag: needs: [build] name: Push tag runs-on: ubuntu-latest steps: - name: Checkout production branch uses: actions/checkout@v2 with: ref: production - name: Use Node.js 15.x uses: actions/setup-node@v1 with: node-version: '15.x' registry-url: https://registry.npmjs.org/ - name: Get package version id: version uses: PostHog/check-package-version@v2 with: path: ./apps/image-editor/ - name: Create Tag run: | git config --local user.name "lja1018" git config --local user.email "jaeeon.lim@nhn.com" git tag v${{ steps.version.outputs.committed-version }} - name: Push tag run: | git push origin v${{ steps.version.outputs.committed-version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-package: needs: [build] name: Publish package runs-on: ubuntu-latest steps: - name: Checkout production branch uses: actions/checkout@v2 with: ref: production - name: Publish package uses: ./.github/composite-actions/publish-package with: ROOT_CACHE_KEY: ${{ needs.build.outputs.root_cache_key }} BUILD_CACHE_KEY: ${{ env.BUILD_CACHE_KEY }} NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} publish-cdn: needs: [build] name: Publish CDN runs-on: ubuntu-latest steps: - name: Checkout production branch uses: actions/checkout@v2 with: ref: production - name: Publish CDN uses: ./.github/composite-actions/publish-cdn with: ROOT_CACHE_KEY: ${{ needs.build.outputs.root_cache_key }} BUILD_CACHE_KEY: ${{ env.BUILD_CACHE_KEY }} TOAST_CLOUD_TENANTID: ${{ secrets.TOAST_CLOUD_TENANTID }} TOAST_CLOUD_STORAGEID: ${{ secrets.TOAST_CLOUD_STORAGEID }} TOAST_CLOUD_USERNAME: ${{ secrets.TOAST_CLOUD_USERNAME }} TOAST_CLOUD_PASSWORD: ${{ secrets.TOAST_CLOUD_PASSWORD }} publish-docs: needs: [build] name: Publish docs runs-on: ubuntu-latest steps: - name: Checkout production branch uses: actions/checkout@v2 with: ref: production - name: Publish docs uses: ./.github/composite-actions/publish-docs with: ROOT_CACHE_KEY: ${{ needs.build.outputs.root_cache_key }} BUILD_CACHE_KEY: ${{ env.BUILD_CACHE_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/publish-wrapper.yml ================================================ name: Publish wrapper on: [workflow_dispatch] env: WORKING_DIRECTORY: ./apps/image-editor REACT_WRAPPER_DIRECTORY: ./apps/react-image-editor VUE_WRAPPER_DIRECTORY: ./apps/vue-image-editor jobs: publish-wrapper: runs-on: ubuntu-latest steps: - name: Checkout branch uses: actions/checkout@v2 - name: Install root dependencies uses: ./.github/composite-actions/install-dependencies - name: Use Node.js 15.x uses: actions/setup-node@v1 with: node-version: '15.x' registry-url: https://registry.npmjs.org/ - name: Get package version id: version uses: PostHog/check-package-version@v2 with: path: ${{ env.WORKING_DIRECTORY }}/ - name: Update version of wrappers in package.json working-directory: ${{ env.WORKING_DIRECTORY }} run: | npm run update:wrapper - name: Update lock file of react wrapper working-directory: ${{ env.REACT_WRAPPER_DIRECTORY }} run: | npm install - name: Build react wrapper working-directory: ${{ env.REACT_WRAPPER_DIRECTORY }} run: | npm run build - name: Update lock file of Vue wrapper working-directory: ${{ env.VUE_WRAPPER_DIRECTORY }} run: | npm install - name: Build vue wrapper working-directory: ${{ env.VUE_WRAPPER_DIRECTORY }} run: | npm run build - name: Commit files run: | rm -rf ./apps/react-image-editor/package-lock.json rm -rf ./apps/vue-image-editor/package-lock.json git config --local user.name "lja1018" git config --local user.email "jaeeon.lim@nhn.com" git add . git commit -m "chore: update version of wrappers to v${{ steps.version.outputs.committed-version }}" - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: master - name: Publish react wrapper working-directory: ${{ env.REACT_WRAPPER_DIRECTORY }} run: | npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - name: Publish vue wrapper working-directory: ${{ env.VUE_WRAPPER_DIRECTORY }} run: | npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Deployed apps should consider commenting this line out: # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules # Bower Components bower_components lib #JSDOC doc # IDEA .idea *.iml # Window Thumbs.db Desktop.ini # MAC .DS_Store # SVN .svn # eclipse .project .metadata # build build # Atom tags .ctags .tern-project # etc .agignore *.swp etc temp api .tern-port *.vim .\#* .vscode dist !_*/dist/ ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "printWidth": 100, "tabWidth": 2, "useTabs": false, "semi": true, "quoteProps": "as-needed", "jsxSingleQuote": false, "trailingComma": "es5", "arrowParens": "always", "endOfLine": "lf", "bracketSpacing": true, "jsxBracketSameLine": false, "requirePragma": false, "insertPragma": false, "proseWrap": "preserve", "vueIndentScriptAndStyle": false } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dl_javascript@nhn.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to TOAST UI First off, thanks for taking the time to contribute! 🎉 😘 ✨ The following is a set of guidelines for contributing to TOAST UI. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. ## Reporting Bugs Bugs are tracked as GitHub issues. Search the list and try reproduce on [demo][demo] before you create an issue. When you create an issue, please provide the following information by filling in the template. Explain the problem and include additional details to help maintainers reproduce the problem: - **Use a clear and descriptive title** for the issue to identify the problem. - **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. For example, if you moved the cursor to the end of a line, explain if you used a mouse or a keyboard. - **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets on the issue, use Markdown code blocks. - **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. - **Explain which behavior you expected to see instead and why.** - **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. ## Suggesting Enhancements In case you want to suggest for TOAST UI ImageEditor, please follow this guideline to help maintainers and the community understand your suggestion. Before creating suggestions, please check [issue list](../../labels/enhancement) if there's already a request. Create an issue and provide the following information: - **Use a clear and descriptive title** for the issue to identify the suggestion. - **Provide a step-by-step description of the suggested enhancement** in as many details as possible. - **Provide specific examples to demonstrate the steps.** Include copy/pasteable snippets which you use in those examples, as Markdown code blocks. - **Include screenshots and animated GIFs** which helps demonstrate the steps or point out the part of TOAST UI ImageEditor which the suggestion is related to. - **Explain why this enhancement would be useful** to most TOAST UI users. - **List some other image editors or applications where this enhancement exists.** ## First Code Contribution Unsure where to begin contributing to TOAST UI? You can start by looking through these `document`, `good first issue` and `help wanted` issues: - **document issues**: issues which should be reviewed or improved. - **good first issues**: issues which should only require a few lines of code, and a test or two. - **help wanted issues**: issues which should be a bit more involved than beginner issues. ## Pull Requests ### Development WorkFlow - Set up your development environment - Make change from a right branch - Be sure the code passes `npm run lint`, `npm run test` - Make a pull request ### Development environment - Prepare your machine node and it's packages installed. - Checkout our repository - Install dependencies by `npm install && bower install` - Start webpack-dev-server by `npm run serve` ### Make changes #### Checkout a branch - **develop**: PR base branch. merge features, updates for next minor or major release - **master**: bug fix or document update for next patch release. develop branch will rebase every time master branch update. so keep code change to a minimum. - **production**: lastest release branch with distribution files. never make a PR on this - **gh-pages**: API docs, examples and demo #### Check Code Style Run `npm run eslint` and make sure all the tests pass. #### Test Run `npm run test` and verify all the tests pass. If you are adding new commands or features, they must include tests. If you are changing functionality, update the tests if you need to. #### Commit Follow our [commit message conventions](./docs/COMMIT_MESSAGE_CONVENTION.md). ### Yes! Pull request Make your pull request, then describe your changes. #### Title Follow other PR title format on below. ``` : Short Description (fix #111) : Short Description (fix #123, #111, #122) : Short Description (ref #111) ``` - capitalize first letter of Type - use present tense: 'change' not 'changed' or 'changes' #### Description If it has related to issues, add links to the issues(like `#123`) in the description. Fill in the [Pull Request Template](./docs/PULL_REQUEST_TEMPLATE.md) by check your case. ## Code of Conduct This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to dl_javascript@nhn.com. > This Guide is base on [atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [CocoaPods](http://guides.cocoapods.org/contributing/contribute-to-cocoapods.html) and [ESLint](http://eslint.org/docs/developer-guide/contributing/pull-requests) ================================================ FILE: ISSUE_TEMPLATE.md ================================================ ## Version ## Development Environment ## Current Behavior ```js // Write example code ``` ## Expected Behavior ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2019 NHN Cloud Corp. 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: README.md ================================================ # ![Toast UI ImageEditor](https://user-images.githubusercontent.com/35218826/40895380-0b9f4cd6-67ea-11e8-982f-18121daa3a04.png) > Full featured image editor using HTML5 Canvas. It's easy to use and provides powerful filters. [![github version](https://img.shields.io/github/release/nhn/tui.image-editor.svg)](https://github.com/nhn/tui.image-editor/releases/latest) [![npm version](https://img.shields.io/npm/v/tui-image-editor.svg)](https://www.npmjs.com/package/tui-image-editor) [![license](https://img.shields.io/github/license/nhn/tui.image-editor.svg)](https://github.com/nhn/tui.image-editor/blob/master/LICENSE) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.image-editor/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) [![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_CLOUD-ff1414.svg)](https://github.com/nhn) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) ## 📦 Packages - [toast-ui.image-editor](https://github.com/nhn/tui.image-editor/tree/master/apps/image-editor ) - Plain JavaScript component implemented by [NHN Cloud](https://github.com/nhn). - [toast-ui.vue-image-editor](https://github.com/nhn/tui.image-editor/tree/master/apps/vue-image-editor ) - **Vue** wrapper component is powered by [NHN Cloud](https://github.com/nhn). - [toast-ui.react-image-editor](https://github.com/nhn/tui.image-editor/tree/master/apps/react-image-editor ) - **React** wrapper component is powered by [NHN Cloud](https://github.com/nhn). ![6 -20-2018 17-45-54](https://user-images.githubusercontent.com/35218826/41647896-7b218ae0-74b2-11e8-90db-d7805cc23e8c.gif) ## 🚩 Table of Contents - [!Toast UI ImageEditor](#) - [📦 Packages](#packages) - [🚩 Table of Contents](#-table-of-contents) - [🌏 Browser Support](#-browser-support) - [💪 Has full features that stick to the basic.](#-has-full-features-that-stick-to-the-basic) - [Photo manipulation](#photo-manipulation) - [Integration function](#integration-function) - [Powerful filter function](#powerful-filter-function) - [🙆 Easy to apply the size and design you want](#-easy-to-apply-the-size-and-design-you-want) - [Can be used everywhere.](#can-be-used-everywhere) - [Nice default & Fully customizable Themes](#nice-default--fully-customizable-themes) - [🎨 Features](#-features) - [🔧 Pull Request Steps](#-pull-request-steps) - [Setup](#setup) - [Pull Request](#pull-request) - [📙 Documents](#-documents) - [💬 Contributing](#-contributing) - [🔩 Dependency](#-dependency) - [🍞 TOAST UI Family](#-toast-ui-family) - [🚀 Used By](#-used-by) - [📜 License](#-license) ## 🌏 Browser Support | Chrome Chrome | IE Internet Explorer | Edge Edge | Safari Safari | Firefox Firefox | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: | | Yes | 10+ | Yes | Yes | Yes | ## 💪 Has full features that stick to the basic. ### Photo manipulation - Crop, Flip, Rotation, Drawing, Shape, Icon, Text, Mask Filter, Image Filter ### Integration function - Download, Image Load, Undo, Redo, Reset, Delete Object(Shape, Line, Mask Image...)
Crop Flip Rotation Drawing Shape
2018-06-04 4 33 16 2018-06-04 4 40 06 2018-06-04 4 43 02 2018-06-04 4 47 40 2018-06-04 4 51 45
Icon Text Mask Filter
2018-06-05 2 06 29 2018-06-05 2 14 36 2018-06-05 2 20 46 2018-06-05 2 27 10
### Powerful filter function - Grayscale, Invert, Sepia, Blur Sharpen, Emboss, RemoveWhite, Brightness, Noise, Pixelate, ColorFilter, Tint, Multiply, Blend | Grayscale | Noise | Emboss | Pixelate | | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | ![grayscale](https://user-images.githubusercontent.com/35218826/41753470-930fb7b0-7608-11e8-9966-1c890e73d131.png) | ![noise](https://user-images.githubusercontent.com/35218826/41753458-9013bc82-7608-11e8-91d9-74dcc3ffce31.png) | ![emboss](https://user-images.githubusercontent.com/35218826/41753460-906c018a-7608-11e8-8861-c135c0117cea.png) | ![pixelate](https://user-images.githubusercontent.com/35218826/41753461-90a614a6-7608-11e8-97a7-0d3b7bb4aec4.png) | | Sepia | Sepia2 | Blend-righten | Blend-diff | Invert | | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | ![sepia](https://user-images.githubusercontent.com/35218826/41753464-91acc41c-7608-11e8-8652-572f935ea704.png) | ![sepia2](https://user-images.githubusercontent.com/35218826/41753640-91e57248-7609-11e8-8960-145e0de57e39.png) | ![blend-righten](https://user-images.githubusercontent.com/35218826/41753462-9114bc3a-7608-11e8-9ab4-16ce20a34321.png) | ![blend-diff](https://user-images.githubusercontent.com/35218826/41753465-91e4baf2-7608-11e8-9b8f-79e1b956d387.png) | ![invert](https://user-images.githubusercontent.com/35218826/41753466-9260b224-7608-11e8-848a-73231a02ae3a.png) | | Multifly | Tint | Brightness | Remove-white | Sharpen | | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | ![multifly](https://user-images.githubusercontent.com/35218826/41753467-92baae28-7608-11e8-80d2-187a310213f5.png) | ![tint](https://user-images.githubusercontent.com/35218826/41753468-92e6391c-7608-11e8-8977-651366ebe693.png) | ![brightness](https://user-images.githubusercontent.com/35218826/41753457-8fb3d3c6-7608-11e8-9e1d-10c6e4aeba68.png) | ![remove-white](https://user-images.githubusercontent.com/35218826/41753463-917feeb0-7608-11e8-862d-eb3af84e120a.png) | ![sharpen](https://user-images.githubusercontent.com/35218826/41753639-91b8470a-7609-11e8-8d13-48ac3232365b.png) | ## 🙆 Easy to apply the size and design you want ### Can be used everywhere. - Widely supported in browsers including IE10. - Option to support various display sizes. (allows you to use the editor features on your web pages at least over **550 \* 450 sizes**) ![2018-06-04 5 35 25](https://user-images.githubusercontent.com/35218826/40907369-9221f482-681e-11e8-801c-78d6f2e246a8.png) ### Nice default & Fully customizable Themes - Has a white and black theme, and you can modify the theme file to customize it. - Has an API so that you can create your own instead of the built-in. | black - top | black - bottom | white - left | white - right | | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | ![2018-06-05 1 41 13](https://user-images.githubusercontent.com/35218826/40930753-8b72502e-6863-11e8-9cff-1719aee9aef0.png) | ![2018-06-05 1 40 24](https://user-images.githubusercontent.com/35218826/40930755-8bcee136-6863-11e8-8e28-0a6722d38c59.png) | ![2018-06-05 1 41 48](https://user-images.githubusercontent.com/35218826/40930756-8bfe0b50-6863-11e8-8682-bab11a0a2289.png) | ![2018-06-05 1 42 27](https://user-images.githubusercontent.com/35218826/40930754-8ba1dba0-6863-11e8-9439-cc059241b675.png) | ## 🎨 Features - Load image to canvas - Undo/Redo (With shortcut) - Crop - Flip - Rotation - Resize - Free drawing - Line drawing - Shape - Icon - Text - Mask Filter - Image Filter ## 🔧 Pull Request Steps TOAST UI products are open source, so you can create a pull request(PR) after you fix issues. Run npm scripts and develop yourself with the following process. ### Setup Fork `develop` branch into your personal repository. Clone it to local computer. Install node modules. Before starting development, you should check if there are any errors. ```sh $ git clone https://github.com/{your-personal-repo}/[[repo name]].git $ cd [[repo name]] $ npm install ``` ### Pull Request Before uploading your PR, run test one last time to check if there are any errors. If it has no errors, commit and then push it! For more information on PR's steps, please see links in the Contributing section. ## 📙 Documents - [Getting Started](https://github.com/nhn/tui.image-editor/tree/master/docs/Basic-Tutorial.md) - [Tutorial](https://github.com/nhn/tui.image-editor/tree/master/docs) - [Example](http://nhn.github.io/tui.image-editor/latest/tutorial-example01-includeUi) - [API](http://nhn.github.io/tui.image-editor/latest) ## 💬 Contributing - [Code of Conduct](https://github.com/nhn/tui.image-editor/blob/master/CODE_OF_CONDUCT.md) - [Contributing guideline](https://github.com/nhn/tui.image-editor/blob/master/CONTRIBUTING.md) - [Issue guideline](https://github.com/nhn/tui.image-editor/blob/master/ISSUE_TEMPLATE.md) - [Commit convention](https://github.com/nhn/tui.image-editor/blob/production/docs/COMMIT_MESSAGE_CONVENTION.md) ## 🔩 Dependency - [fabric.js](https://github.com/fabricjs/fabric.js/releases) = 4.2.0 - [tui.code-snippet](https://github.com/nhn/tui.code-snippet/releases/tag/v1.5.0) >= 1.5.0 - [tui.color-picker](https://github.com/nhn/tui.color-picker/releases/tag/v2.2.6) >= 2.2.6 ## 🍞 TOAST UI Family - [TOAST UI Editor](https://github.com/nhn/tui.editor) - [TOAST UI Grid](https://github.com/nhn/tui.grid) - [TOAST UI Chart](https://github.com/nhn/tui.chart) - [TOAST UI Calendar](https://github.com/nhn/tui.calendar) - [TOAST UI Components](https://github.com/nhn) ## 🚀 Used By - [NHN Dooray! - Collaboration Service (Project, Messenger, Mail, Calendar, Drive, Wiki, Contacts)](https://dooray.com/home/) - [Catalyst](https://catalystapp.co/) ## 📜 License [MIT LICENSE](https://github.com/nhn/tui.image-editor/blob/master/LICENSE) ================================================ FILE: apps/image-editor/README.md ================================================ # ![Toast UI ImageEditor](https://user-images.githubusercontent.com/35218826/40895380-0b9f4cd6-67ea-11e8-982f-18121daa3a04.png) > Full featured image editor using HTML5 Canvas. It's easy to use and provides powerful filters. [![npm version](https://img.shields.io/npm/v/tui-image-editor.svg)](https://www.npmjs.com/package/tui-image-editor) ## 🚩 Table of Contents - [Collect statistics on the use of open source](#Collect-statistics-on-the-use-of-open-source) - [Install](#-install) - [Via Package Manager](#via-package-manager) - [Via Contents Delivery Network (CDN)](#via-contents-delivery-network-cdn) - [Download Source Files](#download-source-files) - [Usage](#-usage) - [HTML](#html) - [JavaScript](#javascript) - [Menu svg icon setting](#menu-svg-icon-setting) - [TypeScript](#typescript) ## Collect statistics on the use of open source TOAST UI ImageEditor applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI ImageEditor is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. > “ui.toast.com") is to be collected and the sole purpose is nothing but to measure statistics on the usage. To disable GA, use the following `usageStatistics` option when creating the instance. ```js const options = { //... usageStatistics: false, }; const imageEditor = new tui.ImageEditor('#tui-image-editor-container', options); ``` Or, include [`tui-code-snippet`](https://github.com/nhn/tui.code-snippet)(**v1.4.0** or **later**) and then immediately write the options as follows: ```js tui.usageStatistics = false; ``` ## 💾 Install The TOAST UI products can be installed by using the package manager or downloading the source directly. However, we highly recommend using the package manager. ### Via Package Manager You can find TOAST UI products via [npm](https://www.npmjs.com/) and [bower](https://bower.io/) package managers. Install by using the commands provided by each package manager. When using npm, be sure [Node.js](https://nodejs.org) is installed in the environment. #### npm ##### 1. ImageEditor installation ```sh $ npm install --save tui-image-editor # Latest version $ npm install --save tui-image-editor@ # Specific version ``` ##### 2. If the installation of the `fabric.js` dependency module does not go smoothly To solve the problem, you need to refer to [Some Steps](https://github.com/fabricjs/fabric.js#install-with-npm) to solve the problem. #### bower ```sh $ bower install tui-image-editor # Latest version $ bower install tui-image-editor# # Specific version ``` ### Via Contents Delivery Network (CDN) TOAST UI products are available over the CDN powered by [NHN Cloud](https://www.toast.com). You can use the CDN as below. ```html ``` If you want to use a specific version, use the tag name instead of `latest` in the URL. The CDN directory has the following structure. ``` tui-image-editor/ ├─ latest/ │ ├─ tui-image-editor.js │ ├─ tui-image-editor.min.js │ └─ tui-image-editor.css ├─ v3.1.0/ │ ├─ ... ``` ### Download Source Files - [Download bundle files from `dist` folder](https://github.com/nhn/tui.image-editor/tree/production/dist) - [Download all sources for each version](https://github.com/nhn/tui.image-editor/releases) ## 🔨 Usage ### HTML Add the container element where TOAST UI ImageEditor will be created. ```html ...
... ``` ### javascript Add dependencies & initialize ImageEditor class with given element to make an image editor. ```javascript const ImageEditor = require('tui-image-editor'); const FileSaver = require('file-saver'); //to download edited image to local. Use after npm install file-saver const blackTheme = require('./js/theme/black-theme.js'); const locale_ru_RU = { // override default English locale to your custom Crop: 'Обзрезать', 'Delete-all': 'Удалить всё', // etc... }; const instance = new ImageEditor(document.querySelector('#tui-image-editor'), { includeUI: { loadImage: { path: 'img/sampleImage.jpg', name: 'SampleImage', }, locale: locale_ru_RU, theme: blackTheme, // or whiteTheme initMenu: 'filter', menuBarPosition: 'bottom', }, cssMaxWidth: 700, cssMaxHeight: 500, selectionStyle: { cornerSize: 20, rotatingPointOffset: 70, }, }); ``` Or ```javascript const ImageEditor = require('tui-image-editor'); const instance = new ImageEditor(document.querySelector('#tui-image-editor'), { cssMaxWidth: 700, cssMaxHeight: 500, selectionStyle: { cornerSize: 20, rotatingPointOffset: 70, }, }); ``` ### Menu svg icon setting #### There are two ways to set icons. 1. **Use default svg built** into imageEditor without setting svg file path (Features added since version v3.9.0). 2. There is a way to use the **actual physical svg file** and **set the file location manually**. Can find more details in [this document](https://github.com/nhn/tui.image-editor/blob/master/docs/Basic-Tutorial.md#4-menu-submenu-svg-icon-setting). ### TypeScript If you use TypeScript, You must `import module = require('module')` on importing. [`export =` and `import = require()`](https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require) ```typescript import ImageEditor = require('tui-image-editor'); const FileSaver = require('file-saver'); //to download edited image to local. Use after npm install file-saver const instance = new ImageEditor(document.querySelector('#tui-image-editor'), { cssMaxWidth: 700, cssMaxHeight: 500, selectionStyle: { cornerSize: 20, rotatingPointOffset: 70, }, }); ``` See [details](https://nhn.github.io/tui.image-editor/latest) for additional information. ================================================ FILE: apps/image-editor/__mocks__/fileMock.js ================================================ const path = require('path'); module.exports = { process(src, filename) { return `module.exports = ${JSON.stringify(path.basename(filename))};`; }, }; ================================================ FILE: apps/image-editor/__mocks__/svgMock.js ================================================ module.exports = { process() { return `module.exports = 'PGRlZnMgaWQ9InR1aS1pbWFnZS1lZGl0b3Itc3ZnLWRlZmF1bHQtaWNvbnMiPg=='`; }, }; ================================================ FILE: apps/image-editor/createConfigVariable.js ================================================ const fs = require('fs'); const path = require('path'); const config = require(path.resolve(__dirname, 'tuidoc.config.json')); const examples = config.examples || {}; const { filePath, globalErrorLogVariable } = examples; /** * Get Examples Url */ function getTestUrls() { if (!filePath) { throw Error('not exist examples path at tuidoc.config.json'); } const urlPrefix = 'http://nhn.github.io/tui.image-editor/latest'; const testUrls = fs.readdirSync(filePath).reduce((urls, fileName) => { if (/html$/.test(fileName)) { urls.push(`${urlPrefix}/${filePath}/${fileName}`); } return urls; }, []); fs.writeFileSync('url.txt', testUrls.join(', ')); } function getGlobalVariable() { if (!globalErrorLogVariable) { throw Error('not exist examples path at tuidoc.config.json'); } fs.writeFileSync('errorVariable.txt', globalErrorLogVariable); } getTestUrls(); getGlobalVariable(); ================================================ FILE: apps/image-editor/examples/css/service-basic.css ================================================ .border { border: 1px solid black; } .body-container { width: 1000px; } .tui-image-editor-controls { min-height: 250px; } .menu { padding: 0; margin-bottom: 5px; text-align: center; color: #544b61; font-weight: 400; list-style-type: none; user-select: none; -moz-user-select: none; -ms-user-select: none; -webkit-user-select: none; } .logo { margin: 0 auto; width: 300px; vertical-align: middle; } .header .name { padding: 10px; line-height: 50px; font-size: 30px; font-weight: 100; vertical-align: middle; } .header .menu { display: inline-block; } .menu-item { padding: 10px; display: inline-block; cursor: pointer; vertical-align: middle; } .menu-item a { text-decoration: none; } .menu-item.no-pointer { cursor: default; } .menu-item.active, .menu-item:hover { background-color: #f3f3f3; } .menu-item.disabled { cursor: default; color: #bfbebe; } .align-left-top { text-align: left; vertical-align: top; } .range-narrow { width: 80px; } .sub-menu-container { font-size: 14px; margin-bottom: 1em; display: none; } .tui-image-editor { height: 500px; } .tui-image-editor-canvas-container { margin: 0 auto; top: 50%; transform: translateY(-50%); -ms-transform: translateY(-50%); -moz-transform: translateY(-50%); -webkit-transform: translateY(-50%); border: 1px dashed black; overflow: hidden; } .tui-colorpicker-container { margin: 5px auto 0; } .tui-colorpicker-palette-toggle-slider { display: none; } .input-wrapper { position: relative; } .input-wrapper input { cursor: pointer; position: absolute; font-size: 999px; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; overflow: hidden; } .btn-text-style { padding: 5px; margin: 3px 1px; border: 1px dashed #bfbebe; outline: 0; background-color: #eee; cursor: pointer; } .icon-text { font-size: 20px; } .select-line-type { outline: 0; vertical-align: middle; } #tui-color-picker { display: inline-block; vertical-align: middle; } #tui-text-palette { display: none; position: absolute; padding: 10px; border: 1px solid #bfbebe; background-color: #fff; z-index: 9999; } ================================================ FILE: apps/image-editor/examples/css/service-mobile.css ================================================ html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: #383838; font-family: Sans-Serif; } ul, li { list-style: none; margin: 0; padding: 0; } input[type='button'], button { -webkit-appearance: none; -moz-appearance: none; background-color: #fff; } input[type='file'] { position: absolute; margin: 0; padding: 0; top: 0; left: 0; width: 100%; height: 100%; cursor: pointer; opacity: 0; filter: alpha(opacity=0); } .header { position: fixed; left: 0; top: 0; width: 100%; background-color: #fff; text-align: center; z-index: 9999; } .header .logo { margin: 10px 5px; width: 180px; vertical-align: middle; } .header .name { font-size: 16px; font-weight: bold; } .header .menu { padding: 10px; background-color: #000; } .header .menu input { opacity: 0; } .header .menu img { width: 20px; height: 20px; vertical-align: middle; } .header .button { position: relative; display: inline-block; margin: 0 5px; padding: 0; border-radius: 5px 5px; width: 30px; height: 30px; border: 0; background-color: #fff; vertical-align: middle; } .header .button.disabled img { opacity: 0.5; } .tui-image-editor { height: 100%; } .tui-image-editor-canvas-container { margin: 0 auto; top: 50%; transform: translateY(-50%); -ms-transform: translateY(-50%); -moz-transform: translateY(-50%); -webkit-transform: translateY(-50%); overflow: hidden; } .tui-image-editor-controls { position: fixed; width: 100%; left: 0; bottom: 0; background-color: #fff; } .tui-image-editor-controls .scrollable { display: inline-block; overflow-x: auto; width: 100%; height: 100%; white-space: nowrap; font-size: 0; background-color: #000; vertical-align: middle; } .tui-image-editor-controls .no-scrollable { overflow-x: hidden; } .tui-image-editor-controls .menu-item { display: inline-block; height: 80px; border-right: 1px solid #383838; background-color: #ddd; vertical-align: middle; } .tui-image-editor-controls .menu-button { width: 80px; height: 80px; border: none; vertical-align: middle; background-color: #000; color: #fff; font-size: 12px; font-weight: bold; outline: 0; } .tui-image-editor-controls .submenu-button { width: 80px; height: 80px; border: none; background-color: #ddd; vertical-align: middle; } .tui-image-editor-controls .hiddenmenu-button { margin: 0 10px; padding: 5px; border: none; color: #fff; background-color: rgba(255, 255, 255, 0); } .tui-image-editor-controls .submenu { display: none; position: absolute; top: 0; left: 0; width: 100%; font-size: 0; } .tui-image-editor-controls .submenu.show { display: block; } .tui-image-editor-controls .submenu .menu-item:last-child { margin-right: 50px; } .tui-image-editor-controls .hiddenmenu { position: absolute; display: none; padding: 40px; width: 100%; left: 0; bottom: 80px; background-color: rgba(0, 0, 0, 0.7); text-align: center; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; z-index: 9999; } .tui-image-editor-controls .hiddenmenu.show { display: block; } .tui-image-editor-controls .hiddenmenu .top { font-size: 12px; color: #fff; margin-bottom: 20px; } .tui-image-editor-controls .btn-prev { display: inline-block; width: 30px; height: 80px; background-color: #000; color: #fff; border: none; vertical-align: middle; } .tui-image-editor-controls .tui-colorpicker-container { display: inline-block; } .tui-image-editor-controls .msg { position: absolute; margin-left: 50%; padding: 5px 10px; left: -86px; top: -50px; border-radius: 5px 5px; background-color: rgba(255, 255, 255, 0.5); font-size: 12px; } .tui-image-editor-controls .msg.hide { display: none; } ================================================ FILE: apps/image-editor/examples/css/tui-example-style.css ================================================ body { margin: 0; padding: 0; } .code-description { padding: 22px 52px; background-color: rgba(81, 92, 230, 0.1); line-height: 1.4em; } .code-description, .code-description a { font-family: Arial; font-size: 14px; color: #515ce6; } .code-html { padding: 20px 52px; } ================================================ FILE: apps/image-editor/examples/example01-includeUi.html ================================================ 0. Design
================================================ FILE: apps/image-editor/examples/example02-useApiDirect.html ================================================ 1. Basic
Image Editor
================================================ FILE: apps/image-editor/examples/example03-mobile.html ================================================ 2. Mobile
Image Editor

Menu Scrolling Left ⇔ Right

================================================ FILE: apps/image-editor/examples/examples.json ================================================ { "example01-includeUi": { "title": "1. Include ui" }, "example02-useApiDirect": { "title": "2. Use api direct (basic)" }, "example03-mobile": { "title": "3. Mobile" } } ================================================ FILE: apps/image-editor/examples/js/service-basic.js ================================================ /* eslint-disable vars-on-top,no-var,strict,prefer-template,prefer-arrow-callback,prefer-destructuring,object-shorthand,require-jsdoc,complexity,prefer-const,no-unused-vars */ var PIXELATE_FILTER_DEFAULT_VALUE = 20; var supportingFileAPI = !!(window.File && window.FileList && window.FileReader); var rImageType = /data:(image\/.+);base64,/; var shapeOptions = {}; var shapeType; var activeObjectId; // Buttons var $btns = $('.menu-item'); var $btnsActivatable = $btns.filter('.activatable'); var $inputImage = $('#input-image-file'); var $btnDownload = $('#btn-download'); var $btnUndo = $('#btn-undo'); var $btnRedo = $('#btn-redo'); var $btnClearObjects = $('#btn-clear-objects'); var $btnRemoveActiveObject = $('#btn-remove-active-object'); var $btnCrop = $('#btn-crop'); var $btnFlip = $('#btn-flip'); var $btnRotation = $('#btn-rotation'); var $btnDrawLine = $('#btn-draw-line'); var $btnDrawShape = $('#btn-draw-shape'); var $btnApplyCrop = $('#btn-apply-crop'); var $btnCancelCrop = $('#btn-cancel-crop'); var $btnFlipX = $('#btn-flip-x'); var $btnFlipY = $('#btn-flip-y'); var $btnResetFlip = $('#btn-reset-flip'); var $btnRotateClockwise = $('#btn-rotate-clockwise'); var $btnRotateCounterClockWise = $('#btn-rotate-counter-clockwise'); var $btnText = $('#btn-text'); var $btnTextStyle = $('.btn-text-style'); var $btnAddIcon = $('#btn-add-icon'); var $btnRegisterIcon = $('#btn-register-icon'); var $btnMaskFilter = $('#btn-mask-filter'); var $btnImageFilter = $('#btn-image-filter'); var $btnLoadMaskImage = $('#input-mask-image-file'); var $btnApplyMask = $('#btn-apply-mask'); var $btnClose = $('.close'); // Input etc. var $inputRotationRange = $('#input-rotation-range'); var $inputBrushWidthRange = $('#input-brush-width-range'); var $inputFontSizeRange = $('#input-font-size-range'); var $inputStrokeWidthRange = $('#input-stroke-width-range'); var $inputCheckTransparent = $('#input-check-transparent'); var $inputCheckFilter = $('#input-check-filter'); var $inputCheckGrayscale = $('#input-check-grayscale'); var $inputCheckInvert = $('#input-check-invert'); var $inputCheckSepia = $('#input-check-sepia'); var $inputCheckSepia2 = $('#input-check-sepia2'); var $inputCheckBlur = $('#input-check-blur'); var $inputCheckSharpen = $('#input-check-sharpen'); var $inputCheckEmboss = $('#input-check-emboss'); var $inputCheckRemoveWhite = $('#input-check-remove-white'); var $inputRangeRemoveWhiteThreshold = $('#input-range-remove-white-threshold'); var $inputRangeRemoveWhiteDistance = $('#input-range-remove-white-distance'); var $inputCheckBrightness = $('#input-check-brightness'); var $inputRangeBrightnessValue = $('#input-range-brightness-value'); var $inputCheckNoise = $('#input-check-noise'); var $inputRangeNoiseValue = $('#input-range-noise-value'); var $inputCheckPixelate = $('#input-check-pixelate'); var $inputRangePixelateValue = $('#input-range-pixelate-value'); var $inputCheckTint = $('#input-check-tint'); var $inputRangeTintOpacityValue = $('#input-range-tint-opacity-value'); var $inputCheckMultiply = $('#input-check-multiply'); var $inputCheckBlend = $('#input-check-blend'); var $inputCheckColorFilter = $('#input-check-color-filter'); var $inputRangeColorFilterValue = $('#input-range-color-filter-value'); // Sub menus var $displayingSubMenu = $(); var $cropSubMenu = $('#crop-sub-menu'); var $flipSubMenu = $('#flip-sub-menu'); var $rotationSubMenu = $('#rotation-sub-menu'); var $freeDrawingSubMenu = $('#free-drawing-sub-menu'); var $drawLineSubMenu = $('#draw-line-sub-menu'); var $drawShapeSubMenu = $('#draw-shape-sub-menu'); var $textSubMenu = $('#text-sub-menu'); var $iconSubMenu = $('#icon-sub-menu'); var $filterSubMenu = $('#filter-sub-menu'); var $imageFilterSubMenu = $('#image-filter-sub-menu'); // Select line type var $selectLine = $('[name="select-line-type"]'); // Select shape type var $selectShapeType = $('[name="select-shape-type"]'); // Select color of shape type var $selectColorType = $('[name="select-color-type"]'); // Select blend type var $selectBlendType = $('[name="select-blend-type"]'); // Image editor var imageEditor = new tui.ImageEditor('.tui-image-editor', { cssMaxWidth: 700, cssMaxHeight: 500, selectionStyle: { cornerSize: 20, rotatingPointOffset: 70, }, }); // Color picker for free drawing var brushColorpicker = tui.colorPicker.create({ container: $('#tui-brush-color-picker')[0], color: '#000000', }); // Color picker for text palette var textColorpicker = tui.colorPicker.create({ container: $('#tui-text-color-picker')[0], color: '#000000', }); // Color picker for shape var shapeColorpicker = tui.colorPicker.create({ container: $('#tui-shape-color-picker')[0], color: '#000000', }); // Color picker for icon var iconColorpicker = tui.colorPicker.create({ container: $('#tui-icon-color-picker')[0], color: '#000000', }); var tintColorpicker = tui.colorPicker.create({ container: $('#tui-tint-color-picker')[0], color: '#000000', }); var multiplyColorpicker = tui.colorPicker.create({ container: $('#tui-multiply-color-picker')[0], color: '#000000', }); var blendColorpicker = tui.colorPicker.create({ container: $('#tui-blend-color-picker')[0], color: '#00FF00', }); // Common global functions // HEX to RGBA function hexToRGBa(hex, alpha) { var r = parseInt(hex.slice(1, 3), 16); var g = parseInt(hex.slice(3, 5), 16); var b = parseInt(hex.slice(5, 7), 16); var a = alpha || 1; return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } function base64ToBlob(data) { var mimeString = ''; var raw, uInt8Array, i, rawLength; raw = data.replace(rImageType, function (header, imageType) { mimeString = imageType; return ''; }); raw = atob(raw); rawLength = raw.length; uInt8Array = new Uint8Array(rawLength); // eslint-disable-line for (i = 0; i < rawLength; i += 1) { uInt8Array[i] = raw.charCodeAt(i); } return new Blob([uInt8Array], { type: mimeString }); } function resizeEditor() { var $editor = $('.tui-image-editor'); var $container = $('.tui-image-editor-canvas-container'); var height = parseFloat($container.css('max-height')); $editor.height(height); } function getBrushSettings() { var brushWidth = parseInt($inputBrushWidthRange.val(), 10); var brushColor = brushColorpicker.getColor(); return { width: brushWidth, color: hexToRGBa(brushColor, 0.5), }; } function activateShapeMode() { if (imageEditor.getDrawingMode() !== 'SHAPE') { imageEditor.stopDrawingMode(); imageEditor.startDrawingMode('SHAPE'); } } function activateIconMode() { imageEditor.stopDrawingMode(); } function activateTextMode() { if (imageEditor.getDrawingMode() !== 'TEXT') { imageEditor.stopDrawingMode(); imageEditor.startDrawingMode('TEXT'); } } function setTextToolbar(obj) { var fontSize = obj.fontSize; var fontColor = obj.fill; $inputFontSizeRange.val(fontSize); textColorpicker.setColor(fontColor); } function setIconToolbar(obj) { var iconColor = obj.fill; iconColorpicker.setColor(iconColor); } function setShapeToolbar(obj) { var fillColor, isTransparent, isFilter; var colorType = $selectColorType.val(); var changeValue = colorType === 'stroke' ? obj.stroke : obj.fill.type; isTransparent = changeValue === 'transparent'; isFilter = changeValue === 'filter'; if (colorType === 'stroke') { if (!isTransparent && !isFilter) { shapeColorpicker.setColor(changeValue); } } else if (colorType === 'fill') { if (!isTransparent && !isFilter) { fillColor = obj.fill.color; shapeColorpicker.setColor(fillColor); } } $inputCheckTransparent.prop('checked', isTransparent); $inputCheckFilter.prop('checked', isFilter); $inputStrokeWidthRange.val(obj.strokeWidth); } function showSubMenu(type) { var $submenu; switch (type) { case 'shape': $submenu = $drawShapeSubMenu; break; case 'icon': $submenu = $iconSubMenu; break; case 'text': $submenu = $textSubMenu; break; default: $submenu = 0; } $displayingSubMenu.hide(); $displayingSubMenu = $submenu.show(); } function applyOrRemoveFilter(applying, type, options) { if (applying) { imageEditor.applyFilter(type, options).then(function (result) { console.log(result); }); } else { imageEditor.removeFilter(type); } } // Attach image editor custom events imageEditor.on({ objectAdded: function (objectProps) { console.info(objectProps); }, undoStackChanged: function (length) { if (length) { $btnUndo.removeClass('disabled'); } else { $btnUndo.addClass('disabled'); } resizeEditor(); }, redoStackChanged: function (length) { if (length) { $btnRedo.removeClass('disabled'); } else { $btnRedo.addClass('disabled'); } resizeEditor(); }, objectScaled: function (obj) { if (obj.type === 'text') { $inputFontSizeRange.val(obj.fontSize); } }, addText: function (pos) { imageEditor .addText('Double Click', { position: pos.originPosition, }) .then(function (objectProps) { console.log(objectProps); }); }, objectActivated: function (obj) { activeObjectId = obj.id; if (obj.type === 'rect' || obj.type === 'circle' || obj.type === 'triangle') { showSubMenu('shape'); setShapeToolbar(obj); activateShapeMode(); } else if (obj.type === 'icon') { showSubMenu('icon'); setIconToolbar(obj); activateIconMode(); } else if (obj.type === 'text') { showSubMenu('text'); setTextToolbar(obj); activateTextMode(); } }, mousedown: function (event, originPointer) { if ($imageFilterSubMenu.is(':visible') && imageEditor.hasFilter('colorFilter')) { imageEditor.applyFilter('colorFilter', { x: parseInt(originPointer.x, 10), y: parseInt(originPointer.y, 10), }); } }, }); // Attach button click event listeners $btns.on('click', function () { $btnsActivatable.removeClass('active'); }); $btnsActivatable.on('click', function () { $(this).addClass('active'); }); $btnUndo.on('click', function () { $displayingSubMenu.hide(); if (!$(this).hasClass('disabled')) { imageEditor.discardSelection(); imageEditor.undo(); } }); $btnRedo.on('click', function () { $displayingSubMenu.hide(); if (!$(this).hasClass('disabled')) { imageEditor.discardSelection(); imageEditor.redo(); } }); $btnClearObjects.on('click', function () { $displayingSubMenu.hide(); imageEditor.clearObjects(); }); $btnRemoveActiveObject.on('click', function () { $displayingSubMenu.hide(); imageEditor.removeObject(activeObjectId); }); $btnCrop.on('click', function () { imageEditor.startDrawingMode('CROPPER'); $displayingSubMenu.hide(); $displayingSubMenu = $cropSubMenu.show(); }); $btnFlip.on('click', function () { imageEditor.stopDrawingMode(); $displayingSubMenu.hide(); $displayingSubMenu = $flipSubMenu.show(); }); $btnRotation.on('click', function () { imageEditor.stopDrawingMode(); $displayingSubMenu.hide(); $displayingSubMenu = $rotationSubMenu.show(); }); $btnClose.on('click', function () { imageEditor.stopDrawingMode(); $displayingSubMenu.hide(); }); $btnApplyCrop.on('click', function () { imageEditor.crop(imageEditor.getCropzoneRect()).then(function () { imageEditor.stopDrawingMode(); resizeEditor(); }); }); $btnCancelCrop.on('click', function () { imageEditor.stopDrawingMode(); }); $btnFlipX.on('click', function () { imageEditor.flipX().then(function (status) { console.log('flipX: ', status.flipX); console.log('flipY: ', status.flipY); console.log('angle: ', status.angle); }); }); $btnFlipY.on('click', function () { imageEditor.flipY().then(function (status) { console.log('flipX: ', status.flipX); console.log('flipY: ', status.flipY); console.log('angle: ', status.angle); }); }); $btnResetFlip.on('click', function () { imageEditor.resetFlip().then(function (status) { console.log('flipX: ', status.flipX); console.log('flipY: ', status.flipY); console.log('angle: ', status.angle); }); }); $btnRotateClockwise.on('click', function () { imageEditor.rotate(30); }); $btnRotateCounterClockWise.on('click', function () { imageEditor.rotate(-30); }); $inputRotationRange.on('mousedown', function () { var changeAngle = function () { imageEditor.setAngle(parseInt($inputRotationRange.val(), 10))['catch'](function () {}); }; $(document).on('mousemove', changeAngle); $(document).on('mouseup', function stopChangingAngle() { $(document).off('mousemove', changeAngle); $(document).off('mouseup', stopChangingAngle); }); }); $inputRotationRange.on('change', function () { imageEditor.setAngle(parseInt($inputRotationRange.val(), 10))['catch'](function () {}); }); $inputBrushWidthRange.on('change', function () { imageEditor.setBrush({ width: parseInt(this.value, 10) }); }); $inputImage.on('change', function (event) { var file; if (!supportingFileAPI) { alert('This browser does not support file-api'); } file = event.target.files[0]; imageEditor.loadImageFromFile(file).then(function (result) { console.log(result); imageEditor.clearUndoStack(); }); }); $btnDownload.on('click', function () { var imageName = imageEditor.getImageName(); var dataURL = imageEditor.toDataURL(); var blob, type, w; if (supportingFileAPI) { blob = base64ToBlob(dataURL); type = blob.type.split('/')[1]; if (imageName.split('.').pop() !== type) { imageName += '.' + type; } // Library: FileSaver - saveAs saveAs(blob, imageName); // eslint-disable-line } else { alert('This browser needs a file-server'); w = window.open(); w.document.body.innerHTML = ''; } }); // control draw line mode $btnDrawLine.on('click', function () { imageEditor.stopDrawingMode(); $displayingSubMenu.hide(); $displayingSubMenu = $drawLineSubMenu.show(); $selectLine.eq(0).change(); }); $selectLine.on('change', function () { var mode = $(this).val(); var settings = getBrushSettings(); imageEditor.stopDrawingMode(); if (mode === 'freeDrawing') { imageEditor.startDrawingMode('FREE_DRAWING', settings); } else { imageEditor.startDrawingMode('LINE_DRAWING', settings); } }); brushColorpicker.on('selectColor', function (event) { imageEditor.setBrush({ color: hexToRGBa(event.color, 0.5), }); }); // control draw shape mode $btnDrawShape.on('click', function () { showSubMenu('shape'); // step 1. get options to draw shape from toolbar shapeType = $('[name="select-shape-type"]:checked').val(); shapeOptions.stroke = '#000000'; shapeOptions.fill = '#ffffff'; shapeOptions.strokeWidth = Number($inputStrokeWidthRange.val()); // step 2. set options to draw shape imageEditor.setDrawingShape(shapeType, shapeOptions); // step 3. start drawing shape mode activateShapeMode(); }); $selectShapeType.on('change', function () { shapeType = $(this).val(); imageEditor.setDrawingShape(shapeType); }); $selectColorType.on('change', function () { var colorType = $(this).val(); if (colorType === 'stroke') { $inputCheckFilter.prop('disabled', true); $inputCheckFilter.prop('checked', false); } else { $inputCheckTransparent.prop('disabled', false); $inputCheckFilter.prop('disabled', false); } }); $inputCheckTransparent.on('change', onChangeShapeFill); $inputCheckFilter.on('change', onChangeShapeFill); shapeColorpicker.on('selectColor', function (event) { $inputCheckTransparent.prop('checked', false); $inputCheckFilter.prop('checked', false); onChangeShapeFill(event); }); function onChangeShapeFill(event) { var colorType = $selectColorType.val(); var isTransparent = $inputCheckTransparent.prop('checked'); var isFilter = $inputCheckFilter.prop('checked'); var shapeOption; if (event.color) { shapeOption = event.color; } else if (isTransparent) { shapeOption = 'transparent'; } else if (isFilter) { shapeOption = { type: 'filter', filter: [{ pixelate: PIXELATE_FILTER_DEFAULT_VALUE }], }; } if (colorType === 'stroke') { imageEditor.changeShape(activeObjectId, { stroke: shapeOption, }); } else if (colorType === 'fill') { imageEditor.changeShape(activeObjectId, { fill: shapeOption, }); } imageEditor.setDrawingShape(shapeType, shapeOptions); } $inputStrokeWidthRange.on('change', function () { var strokeWidth = Number($(this).val()); imageEditor.changeShape(activeObjectId, { strokeWidth: strokeWidth, }); imageEditor.setDrawingShape(shapeType, shapeOptions); }); // control text mode $btnText.on('click', function () { showSubMenu('text'); activateTextMode(); }); $inputFontSizeRange.on('change', function () { imageEditor.changeTextStyle(activeObjectId, { fontSize: parseInt(this.value, 10), }); }); $btnTextStyle.on('click', function (e) { // eslint-disable-line var styleType = $(this).attr('data-style-type'); var styleObj; e.stopPropagation(); switch (styleType) { case 'b': styleObj = { fontWeight: 'bold' }; break; case 'i': styleObj = { fontStyle: 'italic' }; break; case 'u': styleObj = { underline: true }; break; case 'l': styleObj = { textAlign: 'left' }; break; case 'c': styleObj = { textAlign: 'center' }; break; case 'r': styleObj = { textAlign: 'right' }; break; default: styleObj = {}; } imageEditor.changeTextStyle(activeObjectId, styleObj); }); textColorpicker.on('selectColor', function (event) { imageEditor.changeTextStyle(activeObjectId, { fill: event.color, }); }); // control icon $btnAddIcon.on('click', function () { showSubMenu('icon'); activateIconMode(); }); function onClickIconSubMenu(event) { var element = event.target || event.srcElement; var iconType = $(element).attr('data-icon-type'); imageEditor.once('mousedown', function (e, originPointer) { imageEditor .addIcon(iconType, { left: originPointer.x, top: originPointer.y, }) .then(function (objectProps) { // console.log(objectProps); }); }); } $btnRegisterIcon.on('click', function () { $iconSubMenu .find('.menu-item') .eq(3) .after(''); imageEditor.registerIcons({ customArrow: 'M 60 0 L 120 60 H 90 L 75 45 V 180 H 45 V 45 L 30 60 H 0 Z', }); $btnRegisterIcon.off('click'); $iconSubMenu.on('click', '#customArrow', onClickIconSubMenu); }); $iconSubMenu.on('click', '.icon-text', onClickIconSubMenu); iconColorpicker.on('selectColor', function (event) { imageEditor.changeIconColor(activeObjectId, event.color); }); // control mask filter $btnMaskFilter.on('click', function () { imageEditor.stopDrawingMode(); $displayingSubMenu.hide(); $displayingSubMenu = $filterSubMenu.show(); }); $btnImageFilter.on('click', function () { var filters = { grayscale: $inputCheckGrayscale, invert: $inputCheckInvert, sepia: $inputCheckSepia, sepia2: $inputCheckSepia2, blur: $inputCheckBlur, shapren: $inputCheckSharpen, emboss: $inputCheckEmboss, removeWhite: $inputCheckRemoveWhite, brightness: $inputCheckBrightness, noise: $inputCheckNoise, pixelate: $inputCheckPixelate, tint: $inputCheckTint, multiply: $inputCheckMultiply, blend: $inputCheckBlend, colorFilter: $inputCheckColorFilter, }; tui.util.forEach(filters, function ($value, key) { $value.prop('checked', imageEditor.hasFilter(key)); }); $displayingSubMenu.hide(); $displayingSubMenu = $imageFilterSubMenu.show(); }); $btnLoadMaskImage.on('change', function () { var file; var imgUrl; if (!supportingFileAPI) { alert('This browser does not support file-api'); } file = event.target.files[0]; if (file) { imgUrl = URL.createObjectURL(file); imageEditor.loadImageFromURL(imageEditor.toDataURL(), 'FilterImage').then(function () { imageEditor.addImageObject(imgUrl).then(function (objectProps) { URL.revokeObjectURL(file); console.log(objectProps); }); }); } }); $btnApplyMask.on('click', function () { imageEditor .applyFilter('mask', { maskObjId: activeObjectId, }) .then(function (result) { console.log(result); }); }); $inputCheckGrayscale.on('change', function () { applyOrRemoveFilter(this.checked, 'Grayscale', null); }); $inputCheckInvert.on('change', function () { applyOrRemoveFilter(this.checked, 'Invert', null); }); $inputCheckSepia.on('change', function () { applyOrRemoveFilter(this.checked, 'Sepia', null); }); $inputCheckSepia2.on('change', function () { applyOrRemoveFilter(this.checked, 'vintage', null); }); $inputCheckBlur.on('change', function () { applyOrRemoveFilter(this.checked, 'Blur', { blur: 0.1 }); }); $inputCheckSharpen.on('change', function () { applyOrRemoveFilter(this.checked, 'Sharpen', null); }); $inputCheckEmboss.on('change', function () { applyOrRemoveFilter(this.checked, 'Emboss', null); }); $inputCheckRemoveWhite.on('change', function () { applyOrRemoveFilter(this.checked, 'removeColor', { color: '#FFFFFF', useAlpha: false, distance: parseInt($inputRangeRemoveWhiteDistance.val(), 10) / 255, }); }); $inputRangeRemoveWhiteDistance.on('change', function () { applyOrRemoveFilter($inputCheckRemoveWhite.is(':checked'), 'removeColor', { distance: parseInt(this.value, 10) / 255, }); }); $inputCheckBrightness.on('change', function () { applyOrRemoveFilter(this.checked, 'brightness', { brightness: parseInt($inputRangeBrightnessValue.val(), 10) / 255, }); }); $inputRangeBrightnessValue.on('change', function () { applyOrRemoveFilter($inputCheckBrightness.is(':checked'), 'brightness', { brightness: parseInt(this.value, 10) / 255, }); }); $inputCheckNoise.on('change', function () { applyOrRemoveFilter(this.checked, 'noise', { noise: parseInt($inputRangeNoiseValue.val(), 10), }); }); $inputRangeNoiseValue.on('change', function () { applyOrRemoveFilter($inputCheckNoise.is(':checked'), 'noise', { noise: parseInt(this.value, 10), }); }); $inputCheckPixelate.on('change', function () { applyOrRemoveFilter(this.checked, 'pixelate', { blocksize: parseInt($inputRangePixelateValue.val(), 10), }); }); $inputRangePixelateValue.on('change', function () { applyOrRemoveFilter($inputCheckPixelate.is(':checked'), 'pixelate', { blocksize: parseInt(this.value, 10), }); }); $inputCheckTint.on('change', function () { applyOrRemoveFilter(this.checked, 'blendColor', { mode: 'tint', color: tintColorpicker.getColor(), alpha: parseFloat($inputRangeTintOpacityValue.val()), }); }); tintColorpicker.on('selectColor', function (e) { applyOrRemoveFilter($inputCheckTint.is(':checked'), 'blendColor', { color: e.color, }); }); $inputRangeTintOpacityValue.on('change', function () { applyOrRemoveFilter($inputCheckTint.is(':checked'), 'blendColor', { alpha: parseFloat($inputRangeTintOpacityValue.val()), }); }); $inputCheckMultiply.on('change', function () { applyOrRemoveFilter(this.checked, 'blendColor', { color: multiplyColorpicker.getColor(), }); }); multiplyColorpicker.on('selectColor', function (e) { applyOrRemoveFilter($inputCheckMultiply.is(':checked'), 'blendColor', { color: e.color, }); }); $inputCheckBlend.on('change', function () { applyOrRemoveFilter(this.checked, 'blendColor', { mode: $selectBlendType.val(), color: blendColorpicker.getColor(), }); }); blendColorpicker.on('selectColor', function (e) { applyOrRemoveFilter($inputCheckBlend.is(':checked'), 'blendColor', { color: e.color, }); }); $selectBlendType.on('change', function () { applyOrRemoveFilter($inputCheckBlend.is(':checked'), 'blendColor', { mode: this.value, }); }); $inputCheckColorFilter.on('change', function () { applyOrRemoveFilter(this.checked, 'removeColor', { color: '#FFFFFF', distance: $inputRangeColorFilterValue.val() / 255, }); }); $inputRangeColorFilterValue.on('change', function () { applyOrRemoveFilter($inputCheckColorFilter.is(':checked'), 'removeColor', { distance: this.value / 255, }); }); // Etc.. // Load sample image imageEditor.loadImageFromURL('img/sampleImage.jpg', 'SampleImage').then(function (sizeValue) { console.log(sizeValue); imageEditor.clearUndoStack(); }); // IE9 Unselectable $('.menu').on('selectstart', function () { return false; }); ================================================ FILE: apps/image-editor/examples/js/service-mobile.js ================================================ /* eslint-disable vars-on-top,no-var,strict,prefer-template,prefer-arrow-callback,prefer-destructuring,object-shorthand,require-jsdoc,complexity */ 'use strict'; var MAX_RESOLUTION = 3264 * 2448; // 8MP (Mega Pixel) var supportingFileAPI = !!(window.File && window.FileList && window.FileReader); var rImageType = /data:(image\/.+);base64,/; var shapeOpt = { fill: '#fff', stroke: '#000', strokeWidth: 10, }; var activeObjectId; // Selector of image editor controls var submenuClass = '.submenu'; var hiddenmenuClass = '.hiddenmenu'; var $controls = $('.tui-image-editor-controls'); var $menuButtons = $controls.find('.menu-button'); var $submenuButtons = $controls.find('.submenu-button'); var $btnShowMenu = $controls.find('.btn-prev'); var $msg = $controls.find('.msg'); var $subMenus = $controls.find(submenuClass); var $hiddenMenus = $controls.find(hiddenmenuClass); // Image editor controls - top menu buttons var $inputImage = $('#input-image-file'); var $btnDownload = $('#btn-download'); var $btnUndo = $('#btn-undo'); var $btnRedo = $('#btn-redo'); var $btnRemoveActiveObject = $('#btn-remove-active-object'); // Image editor controls - bottom menu buttons var $btnCrop = $('#btn-crop'); var $btnAddText = $('#btn-add-text'); // Image editor controls - bottom submenu buttons var $btnApplyCrop = $('#btn-apply-crop'); var $btnFlipX = $('#btn-flip-x'); var $btnFlipY = $('#btn-flip-y'); var $btnRotateClockwise = $('#btn-rotate-clockwise'); var $btnRotateCounterClockWise = $('#btn-rotate-counter-clockwise'); var $btnAddArrowIcon = $('#btn-add-arrow-icon'); var $btnAddCancelIcon = $('#btn-add-cancel-icon'); var $btnAddCustomIcon = $('#btn-add-custom-icon'); var $btnFreeDrawing = $('#btn-free-drawing'); var $btnLineDrawing = $('#btn-line-drawing'); var $btnAddRect = $('#btn-add-rect'); var $btnAddSquare = $('#btn-add-square'); var $btnAddEllipse = $('#btn-add-ellipse'); var $btnAddCircle = $('#btn-add-circle'); var $btnAddTriangle = $('#btn-add-triangle'); var $btnChangeTextStyle = $('.btn-change-text-style'); // Image editor controls - etc. var $inputTextSizeRange = $('#input-text-size-range'); var $inputBrushWidthRange = $('#input-brush-range'); var $inputStrokeWidthRange = $('#input-stroke-range'); var $inputCheckTransparent = $('#input-check-transparent'); // Colorpicker var iconColorpicker = tui.colorPicker.create({ container: $('#tui-icon-color-picker')[0], color: '#000000', }); var textColorpicker = tui.colorPicker.create({ container: $('#tui-text-color-picker')[0], color: '#000000', }); var brushColorpicker = tui.colorPicker.create({ container: $('#tui-brush-color-picker')[0], color: '#000000', }); var shapeColorpicker = tui.colorPicker.create({ container: $('#tui-shape-color-picker')[0], color: '#000000', }); // Create image editor var imageEditor = new tui.ImageEditor('.tui-image-editor', { cssMaxWidth: document.documentElement.clientWidth, cssMaxHeight: document.documentElement.clientHeight, selectionStyle: { cornerSize: 50, rotatingPointOffset: 100, }, }); var $displayingSubMenu, $displayingHiddenMenu; function hexToRGBa(hex, alpha) { var r = parseInt(hex.slice(1, 3), 16); var g = parseInt(hex.slice(3, 5), 16); var b = parseInt(hex.slice(5, 7), 16); var a = alpha || 1; return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')'; } function base64ToBlob(data) { var mimeString = ''; var raw, uInt8Array, i, rawLength; raw = data.replace(rImageType, function (header, imageType) { mimeString = imageType; return ''; }); raw = atob(raw); rawLength = raw.length; uInt8Array = new Uint8Array(rawLength); // eslint-disable-line for (i = 0; i < rawLength; i += 1) { uInt8Array[i] = raw.charCodeAt(i); } return new Blob([uInt8Array], { type: mimeString }); } function getBrushSettings() { var brushWidth = $inputBrushWidthRange.val(); var brushColor = brushColorpicker.getColor(); return { width: brushWidth, color: hexToRGBa(brushColor, 0.5), }; } function activateShapeMode() { imageEditor.stopDrawingMode(); } function activateIconMode() { imageEditor.stopDrawingMode(); } function activateTextMode() { if (imageEditor.getDrawingMode() !== 'TEXT') { imageEditor.stopDrawingMode(); imageEditor.startDrawingMode('TEXT'); } } function setTextToolbar(obj) { var fontSize = obj.fontSize; var fontColor = obj.fill; $inputTextSizeRange.val(fontSize); textColorpicker.setColor(fontColor); } function setIconToolbar(obj) { var iconColor = obj.fill; iconColorpicker.setColor(iconColor); } function setShapeToolbar(obj) { var strokeColor, fillColor, isTransparent; var colorType = $('[name="select-color-type"]:checked').val(); if (colorType === 'stroke') { strokeColor = obj.stroke; isTransparent = strokeColor === 'transparent'; if (!isTransparent) { shapeColorpicker.setColor(strokeColor); } } else if (colorType === 'fill') { fillColor = obj.fill; isTransparent = fillColor === 'transparent'; if (!isTransparent) { shapeColorpicker.setColor(fillColor); } } $inputCheckTransparent.prop('checked', isTransparent); $inputStrokeWidthRange.val(obj.strokeWith); } function showSubMenu(type) { var index; switch (type) { case 'shape': index = 3; break; case 'icon': index = 4; break; case 'text': index = 5; break; default: index = 0; } $displayingSubMenu.hide(); $displayingHiddenMenu.hide(); $displayingSubMenu = $menuButtons.eq(index).parent().find(submenuClass).show(); } // Bind custom event of image editor imageEditor.on({ undoStackChanged: function (length) { if (length) { $btnUndo.removeClass('disabled'); } else { $btnUndo.addClass('disabled'); } }, redoStackChanged: function (length) { if (length) { $btnRedo.removeClass('disabled'); } else { $btnRedo.addClass('disabled'); } }, objectScaled: function (obj) { if (obj.type === 'text') { $inputTextSizeRange.val(obj.fontSize); } }, objectActivated: function (obj) { activeObjectId = obj.id; if (obj.type === 'rect' || obj.type === 'circle' || obj.type === 'triangle') { showSubMenu('shape'); setShapeToolbar(obj); activateShapeMode(); } else if (obj.type === 'icon') { showSubMenu('icon'); setIconToolbar(obj); activateIconMode(); } else if (obj.type === 'text') { showSubMenu('text'); setTextToolbar(obj); activateTextMode(); } }, }); // Image editor controls action $menuButtons.on('click', function () { $displayingSubMenu = $(this).parent().find(submenuClass).show(); $displayingHiddenMenu = $(this).parent().find(hiddenmenuClass); }); $submenuButtons.on('click', function () { $displayingHiddenMenu.hide(); $displayingHiddenMenu = $(this).parent().find(hiddenmenuClass).show(); }); $btnShowMenu.on('click', function () { $displayingSubMenu.hide(); $displayingHiddenMenu.hide(); $msg.show(); imageEditor.stopDrawingMode(); }); // Image load action $inputImage.on('change', function (event) { var file; var img; var resolution; if (!supportingFileAPI) { alert('This browser does not support file-api'); } file = event.target.files[0]; if (file) { img = new Image(); img.onload = function () { resolution = this.width * this.height; if (resolution <= MAX_RESOLUTION) { imageEditor.loadImageFromFile(file).then(function () { imageEditor.clearUndoStack(); }); } else { alert("Loaded image's resolution is too large!\nRecommended resolution is 3264 * 2448!"); } URL.revokeObjectURL(file); }; img.src = URL.createObjectURL(file); } }); // Undo action $btnUndo.on('click', function () { if (!$(this).hasClass('disabled')) { imageEditor.undo(); } }); // Redo action $btnRedo.on('click', function () { if (!$(this).hasClass('disabled')) { imageEditor.redo(); } }); // Remove active object action $btnRemoveActiveObject.on('click', function () { imageEditor.removeObject(activeObjectId); }); // Download action $btnDownload.on('click', function () { var imageName = imageEditor.getImageName(); var dataURL = imageEditor.toDataURL(); var blob, type, w; if (supportingFileAPI) { blob = base64ToBlob(dataURL); type = blob.type.split('/')[1]; if (imageName.split('.').pop() !== type) { imageName += '.' + type; } // Library: FileSaver - saveAs saveAs(blob, imageName); // eslint-disable-line } else { alert('This browser needs a file-server'); w = window.open(); w.document.body.innerHTML = ''; } }); // Crop menu action $btnCrop.on('click', function () { imageEditor.startDrawingMode('CROPPER'); }); $btnApplyCrop.on('click', function () { imageEditor.crop(imageEditor.getCropzoneRect()).then(function () { imageEditor.stopDrawingMode(); $subMenus.removeClass('show'); $hiddenMenus.removeClass('show'); }); }); // Orientation menu action $btnRotateClockwise.on('click', function () { imageEditor.rotate(90); }); $btnRotateCounterClockWise.on('click', function () { imageEditor.rotate(-90); }); $btnFlipX.on('click', function () { imageEditor.flipX(); }); $btnFlipY.on('click', function () { imageEditor.flipY(); }); // Icon menu action $btnAddArrowIcon.on('click', function () { imageEditor.addIcon('arrow'); }); $btnAddCancelIcon.on('click', function () { imageEditor.addIcon('cancel'); }); $btnAddCustomIcon.on('click', function () { imageEditor.addIcon('customArrow'); }); iconColorpicker.on('selectColor', function (event) { imageEditor.changeIconColor(activeObjectId, event.color); }); // Text menu action $btnAddText.on('click', function () { var initText = 'DoubleClick'; imageEditor.startDrawingMode('TEXT'); imageEditor.addText(initText, { styles: { fontSize: parseInt($inputTextSizeRange.val(), 10), }, }); }); $btnChangeTextStyle.on('click', function () { var styleType = $(this).attr('data-style-type'); var styleObj = {}; var styleObjKey; switch (styleType) { case 'bold': styleObjKey = 'fontWeight'; break; case 'italic': styleObjKey = 'fontStyle'; break; case 'underline': styleObjKey = 'underline'; break; case 'left': styleObjKey = 'textAlign'; break; case 'center': styleObjKey = 'textAlign'; break; case 'right': styleObjKey = 'textAlign'; break; default: styleObjKey = ''; } styleObj[styleObjKey] = styleType; imageEditor.changeTextStyle(activeObjectId, styleObj); }); $inputTextSizeRange.on('change', function () { imageEditor.changeTextStyle(activeObjectId, { fontSize: parseInt($(this).val(), 10), }); }); textColorpicker.on('selectColor', function (event) { imageEditor.changeTextStyle(activeObjectId, { fill: event.color, }); }); // Draw line menu action $btnFreeDrawing.on('click', function () { var settings = getBrushSettings(); imageEditor.stopDrawingMode(); imageEditor.startDrawingMode('FREE_DRAWING', settings); }); $btnLineDrawing.on('click', function () { var settings = getBrushSettings(); imageEditor.stopDrawingMode(); imageEditor.startDrawingMode('LINE_DRAWING', settings); }); $inputBrushWidthRange.on('change', function () { imageEditor.setBrush({ width: parseInt($(this).val(), 10), }); }); brushColorpicker.on('selectColor', function (event) { imageEditor.setBrush({ color: hexToRGBa(event.color, 0.5), }); }); // Add shape menu action $btnAddRect.on('click', function () { imageEditor.addShape( 'rect', tui.util.extend( { width: 500, height: 300, }, shapeOpt ) ); }); $btnAddSquare.on('click', function () { imageEditor.addShape( 'rect', tui.util.extend( { width: 400, height: 400, isRegular: true, }, shapeOpt ) ); }); $btnAddEllipse.on('click', function () { imageEditor.addShape( 'circle', tui.util.extend( { rx: 300, ry: 200, }, shapeOpt ) ); }); $btnAddCircle.on('click', function () { imageEditor.addShape( 'circle', tui.util.extend( { rx: 200, ry: 200, isRegular: true, }, shapeOpt ) ); }); $btnAddTriangle.on('click', function () { imageEditor.addShape( 'triangle', tui.util.extend( { width: 500, height: 400, isRegular: true, }, shapeOpt ) ); }); $inputStrokeWidthRange.on('change', function () { imageEditor.changeShape(activeObjectId, { strokeWidth: parseInt($(this).val(), 10), }); }); $inputCheckTransparent.on('change', function () { var colorType = $('[name="select-color-type"]:checked').val(); var isTransparent = $(this).prop('checked'); var color; if (!isTransparent) { color = shapeColorpicker.getColor(); } else { color = 'transparent'; } if (colorType === 'stroke') { imageEditor.changeShape(activeObjectId, { stroke: color, }); } else if (colorType === 'fill') { imageEditor.changeShape(activeObjectId, { fill: color, }); } }); shapeColorpicker.on('selectColor', function (event) { var colorType = $('[name="select-color-type"]:checked').val(); var isTransparent = $inputCheckTransparent.prop('checked'); var color = event.color; if (isTransparent) { return; } if (colorType === 'stroke') { imageEditor.changeShape(activeObjectId, { stroke: color, }); } else if (colorType === 'fill') { imageEditor.changeShape(activeObjectId, { fill: color, }); } }); // Load sample image imageEditor.loadImageFromURL('img/sampleImage.jpg', 'SampleImage').then(function () { imageEditor.clearUndoStack(); }); ================================================ FILE: apps/image-editor/examples/js/theme/black-theme.js ================================================ var blackTheme = { 'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png', 'common.bisize.width': '251px', 'common.bisize.height': '21px', 'common.backgroundImage': 'none', 'common.backgroundColor': '#1e1e1e', 'common.border': '0px', // header 'header.backgroundImage': 'none', 'header.backgroundColor': 'transparent', 'header.border': '0px', // load button 'loadButton.backgroundColor': '#fff', 'loadButton.border': '1px solid #ddd', 'loadButton.color': '#222', 'loadButton.fontFamily': "'Noto Sans', sans-serif", 'loadButton.fontSize': '12px', // download button 'downloadButton.backgroundColor': '#fdba3b', 'downloadButton.border': '1px solid #fdba3b', 'downloadButton.color': '#fff', 'downloadButton.fontFamily': "'Noto Sans', sans-serif", 'downloadButton.fontSize': '12px', // main icons 'menu.normalIcon.color': '#8a8a8a', 'menu.activeIcon.color': '#555555', 'menu.disabledIcon.color': '#434343', 'menu.hoverIcon.color': '#e9e9e9', 'menu.iconSize.width': '24px', 'menu.iconSize.height': '24px', // submenu icons 'submenu.normalIcon.color': '#8a8a8a', 'submenu.activeIcon.color': '#e9e9e9', 'submenu.iconSize.width': '32px', 'submenu.iconSize.height': '32px', // submenu primary color 'submenu.backgroundColor': '#1e1e1e', 'submenu.partition.color': '#3c3c3c', // submenu labels 'submenu.normalLabel.color': '#8a8a8a', 'submenu.normalLabel.fontWeight': 'lighter', 'submenu.activeLabel.color': '#fff', 'submenu.activeLabel.fontWeight': 'lighter', // checkbox style 'checkbox.border': '0px', 'checkbox.backgroundColor': '#fff', // range style 'range.pointer.color': '#fff', 'range.bar.color': '#666', 'range.subbar.color': '#d1d1d1', 'range.disabledPointer.color': '#414141', 'range.disabledBar.color': '#282828', 'range.disabledSubbar.color': '#414141', 'range.value.color': '#fff', 'range.value.fontWeight': 'lighter', 'range.value.fontSize': '11px', 'range.value.border': '1px solid #353535', 'range.value.backgroundColor': '#151515', 'range.title.color': '#fff', 'range.title.fontWeight': 'lighter', // colorpicker style 'colorpicker.button.border': '1px solid #1e1e1e', 'colorpicker.title.color': '#fff', }; ================================================ FILE: apps/image-editor/examples/js/theme/white-theme.js ================================================ var whiteTheme = { 'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png', 'common.bisize.width': '251px', 'common.bisize.height': '21px', 'common.backgroundImage': './img/bg.png', 'common.backgroundColor': '#fff', 'common.border': '1px solid #c1c1c1', // header 'header.backgroundImage': 'none', 'header.backgroundColor': 'transparent', 'header.border': '0px', // load button 'loadButton.backgroundColor': '#fff', 'loadButton.border': '1px solid #ddd', 'loadButton.color': '#222', 'loadButton.fontFamily': "'Noto Sans', sans-serif", 'loadButton.fontSize': '12px', // download button 'downloadButton.backgroundColor': '#fdba3b', 'downloadButton.border': '1px solid #fdba3b', 'downloadButton.color': '#fff', 'downloadButton.fontFamily': "'Noto Sans', sans-serif", 'downloadButton.fontSize': '12px', // main icons 'menu.normalIcon.color': '#8a8a8a', 'menu.activeIcon.color': '#555555', 'menu.disabledIcon.color': '#434343', 'menu.hoverIcon.color': '#e9e9e9', 'menu.iconSize.width': '24px', 'menu.iconSize.height': '24px', // submenu icons 'submenu.normalIcon.color': '#8a8a8a', 'submenu.activeIcon.color': '#555555', 'submenu.iconSize.width': '32px', 'submenu.iconSize.height': '32px', // submenu primary color 'submenu.backgroundColor': 'transparent', 'submenu.partition.color': '#e5e5e5', // submenu labels 'submenu.normalLabel.color': '#858585', 'submenu.normalLabel.fontWeight': 'normal', 'submenu.activeLabel.color': '#000', 'submenu.activeLabel.fontWeight': 'normal', // checkbox style 'checkbox.border': '1px solid #ccc', 'checkbox.backgroundColor': '#fff', // rango style 'range.pointer.color': '#333', 'range.bar.color': '#ccc', 'range.subbar.color': '#606060', 'range.disabledPointer.color': '#d3d3d3', 'range.disabledBar.color': 'rgba(85,85,85,0.06)', 'range.disabledSubbar.color': 'rgba(51,51,51,0.2)', 'range.value.color': '#000', 'range.value.fontWeight': 'normal', 'range.value.fontSize': '11px', 'range.value.border': '0', 'range.value.backgroundColor': '#f5f5f5', 'range.title.color': '#000', 'range.title.fontWeight': 'lighter', // colorpicker style 'colorpicker.button.border': '0px', 'colorpicker.title.color': '#000', }; ================================================ FILE: apps/image-editor/index.d.ts ================================================ // Type definitions for TOAST UI Image Editor v3.15.2 // TypeScript Version: 3.2.2 declare namespace tuiImageEditor { type AngleType = number; interface IThemeConfig { 'common.bi.image'?: string; 'common.bisize.width'?: string; 'common.bisize.height'?: string; 'common.backgroundImage'?: string; 'common.backgroundColor'?: string; 'common.border'?: string; 'header.backgroundImage'?: string; 'header.backgroundColor'?: string; 'header.border'?: string; 'loadButton.backgroundColor'?: string; 'loadButton.border'?: string; 'loadButton.color'?: string; 'loadButton.fontFamily'?: string; 'loadButton.fontSize'?: string; 'downloadButton.backgroundColor'?: string; 'downloadButton.border'?: string; 'downloadButton.color'?: string; 'downloadButton.fontFamily'?: string; 'downloadButton.fontSize'?: string; 'menu.normalIcon.path'?: string; 'menu.normalIcon.name'?: string; 'menu.activeIcon.path'?: string; 'menu.activeIcon.name'?: string; 'menu.iconSize.width'?: string; 'menu.iconSize.height'?: string; 'submenu.backgroundColor'?: string; 'submenu.partition.color'?: string; 'submenu.normalIcon.path'?: string; 'submenu.normalIcon.name'?: string; 'submenu.activeIcon.path'?: string; 'submenu.activeIcon.name'?: string; 'submenu.iconSize.width'?: string; 'submenu.iconSize.height'?: string; 'submenu.normalLabel.color'?: string; 'submenu.normalLabel.fontWeight'?: string; 'submenu.activeLabel.color'?: string; 'submenu.activeLabel.fontWeight'?: string; 'checkbox.border'?: string; 'checkbox.backgroundColor'?: string; 'range.pointer.color'?: string; 'range.bar.color'?: string; 'range.subbar.color'?: string; 'range.value.color'?: string; 'range.value.fontWeight'?: string; 'range.value.fontSize'?: string; 'range.value.border'?: string; 'range.value.backgroundColor'?: string; 'range.title.color'?: string; 'range.title.fontWeight'?: string; 'colorpicker.button.border'?: string; 'colorpicker.title.color'?: string; } interface IIconInfo { [propName: string]: string; } interface IIconOptions { fill?: string; left?: number; top?: number; } interface IShapeOptions { fill?: string; stroke?: string; strokeWidth?: number; width?: number; height?: number; rx?: number; ry?: number; left?: number; top?: number; isRegular?: boolean; } interface IGenerateTextOptions { styles?: ITextStyleConfig; position?: { x: number; y: number; }; } type IFilterOptions = | { blur: number } | { brightness: number } | { noise: number } | { blocksize: number } | { color: string; distance: number; useAlpha?: boolean } | { mode: string; color: string; alpha?: number } | { maskObjId: number }; interface ITextStyleConfig { fill?: string; fontFamily?: string; fontSize?: number; fontStyle?: string; fontWeight?: string; textAlign?: string; textDecoration?: string; } interface IRectConfig { left: number; top: number; width: number; height: number; } interface ICanvasSize { width: number; height: number; } interface IBrushOptions { width: number; color: string; } interface IPositionConfig { x: number; y: number; originX: string; originY: string; } interface IToDataURLOptions { format?: string; quality?: number; multiplier?: number; left?: number; top?: number; width?: number; height?: number; } interface IGraphicObjectProps { id?: number; type?: string; text?: string; left?: string | number; top?: string | number; width?: string | number; height?: string | number; fill?: string; stroke?: string; strokeWidth?: string | number; fontFamily?: string; fontSize?: number; fontStyle?: string; fontWeight?: string; textAlign?: string; textDecoration?: string; opacity?: number; [propName: string]: number | string | boolean | undefined; } interface IIncludeUIOptions { loadImage?: { path: string; name: string; }; theme?: IThemeConfig; menu?: string[]; initMenu?: string; uiSize?: { width: string; height: string; }; menuBarPosition?: string; usageStatistics?: boolean; } interface ISelectionStyleConfig { cornerStyle?: string; cornerSize?: number; cornerColor?: string; cornerStrokeColor?: string; transparentCorners?: boolean; lineWidth?: number; borderColor?: string; rotatingPointOffset?: number; } interface IObjectProps { // icon, shape fill: string; height: number; id: number; left: number; opacity: number; stroke: string | null; strokeWidth: number | null; top: number; type: string; width: number; } interface ITextObjectProps extends IObjectProps { fontFamily: string; fontSize: string; fontStyle: string; text: string; textAlign: string; textDecoration: string; } interface IFilterResolveObject { type: string; action: string; } interface ICropResolveObject { oldWidth: number; oldHeight: number; newWidth: number; newHeight: number; } interface IFlipXYResolveObject { flipX: boolean; flipY: boolean; angle: AngleType; } interface IOptions { includeUI?: IIncludeUIOptions; cssMaxWidth?: number; cssMaxHeight?: number; usageStatistics?: boolean; selectionStyle?: ISelectionStyleConfig; } interface IUIDimension { height?: string; width?: string; } interface IImageDimension { oldHeight?: number; oldWidth?: number; newHeight?: number; newWidth?: number; } interface IEditorSize { uiSize?: IUIDimension; imageSize?: IImageDimension; } interface UI { resizeEditor(dimension: IEditorSize): Promise; } class ImageEditor { constructor(wrapper: string | Element, options: IOptions); public ui: UI; public addIcon(type: string, options?: IIconOptions): Promise; public addImageObject(imgUrl: string): Promise; public addShape(type: string, options?: IShapeOptions): Promise; public addText(text: string, options?: IGenerateTextOptions): Promise; public applyFilter( type: string, options?: IFilterOptions, isSilent?: boolean ): Promise; public changeCursor(cursorType: string): void; public changeIconColor(id: number, color: string): Promise; public changeSelectableAll(selectable: boolean): void; public changeShape(id: number, options?: IShapeOptions, isSilent?: boolean): Promise; public changeText(id: number, text?: string): Promise; public changeTextStyle( id: number, styleObj: ITextStyleConfig, isSilent?: boolean ): Promise; public clearObjects(): Promise; public clearRedoStack(): void; public clearUndoStack(): void; public crop(rect: IRectConfig): Promise; public deactivateAll(): void; public destroy(): void; public discardSelection(): void; public flipX(): Promise; public flipY(): Promise; public getCanvasSize(): ICanvasSize; public getCropzoneRect(): IRectConfig; public getDrawingMode(): string; public getImageName(): string; public getObjectPosition(id: number, originX: string, originY: string): ICanvasSize; public getObjectProperties( id: number, keys: string | string[] | IGraphicObjectProps ): IGraphicObjectProps; public hasFilter(type: string): boolean; public isEmptyRedoStack(): boolean; public isEmptyUndoStack(): boolean; public loadImageFromFile(imgFile: File, imageName?: string): Promise; public loadImageFromURL(url: string, imageName?: string): Promise; public redo(iterationCount: number): Promise; public registerIcons(infos: IIconInfo): void; public removeActiveObject(): void; public removeFilter(type?: string): Promise; public removeObject(id: number): Promise; public resetFlip(): Promise; public resizeCanvasDimension(dimension: ICanvasSize): Promise; public rotate(angle: AngleType, isSilent?: boolean): Promise; public setAngle(angle: AngleType, isSilent?: boolean): Promise; public setBrush(option: IBrushOptions): void; public setCropzoneRect(mode?: number): void; public setDrawingShape(type: string, options?: IShapeOptions): void; public setObjectPosition(id: number, posInfo?: IPositionConfig): Promise; public setObjectProperties(id: number, keyValue?: IGraphicObjectProps): Promise; public setObjectPropertiesQuietly(id: number, keyValue?: IGraphicObjectProps): Promise; public startDrawingMode(mode: string, option?: { width?: number; color?: string }): boolean; public stopDrawingMode(): void; public toDataURL(options?: IToDataURLOptions): string; public undo(iterationCount: number): Promise; public on(eventName: string, handler: (...args: any[]) => void): void; } } declare module 'tui-image-editor' { export = tuiImageEditor.ImageEditor; } ================================================ FILE: apps/image-editor/jest-setup.js ================================================ import 'jest-canvas-mock'; ================================================ FILE: apps/image-editor/jest.config.js ================================================ const path = require('path'); const setupFile = path.resolve(__dirname, './jest-setup.js'); module.exports = { moduleFileExtensions: ['js'], testEnvironment: 'jsdom', transform: { '^.+\\.js$': 'jest-esm-transformer', '^.+\\.svg$': '/__mocks__/svgMock.js', }, transformIgnorePatterns: ['/node_modules/'], testMatch: ['/**/*.spec.js'], clearMocks: true, moduleNameMapper: { '^@/(.*)$': '/src/js/$1', '^@css/(.*)$': '/src/css/$1', '^@svg/(.*)$': '/src/svg/$1', '^fixtures/(.*)$': '/__mocks__/fileMock.js', }, setupFiles: [setupFile], }; ================================================ FILE: apps/image-editor/makesvg.js ================================================ /* eslint-disable */ const fs = require('fs'); const mkdirp = require('mkdirp'); const svgstore = require('svgstore'); const svgDir = './src/svg'; function getFileList(dir) { const targetDir = `${svgDir}/${dir}`; const sprites = svgstore(); fs.readdir(targetDir, (err, files) => { if (!files) return; files.forEach((file) => { if (file.match(/^\./)) return; const id = `${dir}-${file.replace(/\.svg$/, '')}`; const svg = fs.readFileSync(`${targetDir}/${file}`); sprites.add(id, svg); }); fs.writeFileSync(`./dist/svg/${dir}.svg`, sprites); }); } mkdirp('./dist/svg').then((path) => { if (path) { fs.readdir(svgDir, (err, dirs) => { dirs.forEach((dir) => { getFileList(dir); }); }); } }); ================================================ FILE: apps/image-editor/package.json ================================================ { "name": "tui-image-editor", "version": "3.15.3", "description": "TOAST UI ImageEditor", "keywords": [ "nhn", "nhn cloud", "tui", "component", "image", "editor", "canvas", "fabric" ], "main": "dist/tui-image-editor.js", "files": [ "src", "dist", "index.d.ts" ], "scripts": { "test": "jest --forceExit --detectOpenHandles", "test:types": "tsc --project tests/types", "build": "npm run build:clean && npm run build:svg && npm run build:prod && npm run build:minify && node tsBannerGenerator.js", "build:clean": "rm -rf ./dist", "build:prod": "webpack", "build:minify": "webpack --env minify", "build:svg": "node makesvg.js", "serve": "webpack serve", "doc:dev": "tuidoc --serv", "doc": "tuidoc", "update:wrapper": "node scripts/updateWrapper.js", "publish:cdn": "node scripts/publishToCDN.js" }, "homepage": "https://github.com/nhn/tui.image-editor", "bugs": "https://github.com/nhn/tui.image-editor/issues", "author": "NHN Cloud. FE Development Lab ", "repository": { "type": "git", "url": "https://github.com/nhn/tui.image-editor.git" }, "license": "MIT", "browserslist": [ "last 2 versions", "not ie <= 9" ], "dependencies": { "fabric": "^4.2.0", "tui-code-snippet": "^2.3.3", "tui-color-picker": "^2.2.6" } } ================================================ FILE: apps/image-editor/scripts/publishToCDN.js ================================================ /* eslint-disable */ const path = require('path'); const fs = require('fs'); const fetch = require('node-fetch'); const pkg = require('../package.json'); const LOCAL_DIST_PATH = path.join(__dirname, '../dist'); const STORAGE_API_URL = 'https://api-storage.cloud.toast.com/v1'; const IDENTITY_API_URL = 'https://api-identity.infrastructure.cloud.toast.com/v2.0'; const TOAST_CLOUD_TENANTID = process.env.TOAST_CLOUD_TENANTID; const TOAST_CLOUD_STORAGEID = process.env.TOAST_CLOUD_STORAGEID; const TOAST_CLOUD_USERNAME = process.env.TOAST_CLOUD_USERNAME; const TOAST_CLOUD_PASSWORD = process.env.TOAST_CLOUD_PASSWORD; async function getTOASTCloudContainer(token) { const response = await fetch(`${STORAGE_API_URL}/${TOAST_CLOUD_STORAGEID}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Auth-Token': token, }, }); const container = await response.text(); return `${container.trim()}/tui-image-editor`; } async function getTOASTCloudToken() { const data = { auth: { tenantId: TOAST_CLOUD_TENANTID, passwordCredentials: { username: TOAST_CLOUD_USERNAME, password: TOAST_CLOUD_PASSWORD, }, }, }; const response = await fetch(`${IDENTITY_API_URL}/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const result = await response.json(); return result.access.token.id; } function publishToCdn(token, localPath, cdnPath) { const files = fs.readdirSync(localPath); files.forEach((fileName) => { const objectPath = `${cdnPath}/${fileName}`; if (fileName.match(/.(js|css|svg)$/)) { const readStream = fs.createReadStream(`${localPath}/${fileName}`); const contentType = /css$/.test(fileName) ? 'text/css' : /js$/.test(fileName) ? 'text/javascript' : 'image/svg+xml'; fetch(`${STORAGE_API_URL}/${objectPath}`, { method: 'PUT', headers: { 'Content-Type': contentType, 'X-Auth-Token': token, }, body: readStream, }); } else { publishToCdn(token, `${localPath}/${fileName}`, objectPath); } }); } async function publish() { const token = await getTOASTCloudToken(); const container = await getTOASTCloudContainer(token); const cdnPath = `${TOAST_CLOUD_STORAGEID}/${container}`; [`v${pkg.version}`, 'latest'].forEach((dir) => { publishToCdn(token, LOCAL_DIST_PATH, `${cdnPath}/${dir}`); }); } publish(); ================================================ FILE: apps/image-editor/scripts/updateWrapper.js ================================================ /* eslint-disable */ const path = require('path'); const fs = require('fs'); const CORE_PACKAGE_JSON_PATH = path.join(__dirname, '../package.json'); const REACT_PACKAGE_JSON_PATH = path.join(__dirname, '../../react-image-editor/package.json'); const VUE_PACKAGE_JSON_PATH = path.join(__dirname, '../../vue-image-editor/package.json'); const corePackage = require(CORE_PACKAGE_JSON_PATH); const reactPackage = require(REACT_PACKAGE_JSON_PATH); const vuePackage = require(VUE_PACKAGE_JSON_PATH); const version = corePackage.version; reactPackage.version = version; reactPackage.dependencies['tui-image-editor'] = `^${version}`; fs.writeFileSync(REACT_PACKAGE_JSON_PATH, `${JSON.stringify(reactPackage, null, 2)}\n`); vuePackage.version = version; vuePackage.dependencies['tui-image-editor'] = `^${version}`; fs.writeFileSync(VUE_PACKAGE_JSON_PATH, `${JSON.stringify(vuePackage, null, 2)}\n`); ================================================ FILE: apps/image-editor/src/css/buttons.styl ================================================ /* ICON BUTTON */ .tie-icon-add-button &.icon-bubble .{prefix}-button[data-icontype="icon-bubble"] svg > use.active, &.icon-heart .{prefix}-button[data-icontype="icon-heart"] svg > use.active, &.icon-location .{prefix}-button[data-icontype="icon-location"] svg > use.active, &.icon-polygon .{prefix}-button[data-icontype="icon-polygon"] svg > use.active, &.icon-star .{prefix}-button[data-icontype="icon-star"] svg > use.active, &.icon-star-2 .{prefix}-button[data-icontype="icon-star-2"] svg > use.active, &.icon-arrow-3 .{prefix}-button[data-icontype="icon-arrow-3"] svg > use.active, &.icon-arrow-2 .{prefix}-button[data-icontype="icon-arrow-2"] svg > use.active, &.icon-arrow .{prefix}-button[data-icontype="icon-arrow"] svg > use.active, &.icon-bubble .{prefix}-button[data-icontype="icon-bubble"] svg > use.active display: block; /* DRAW BUTTON */ .tie-draw-line-select-button &.line .{prefix}-button.line svg > use.normal, &.free .{prefix}-button.free svg > use.normal display: none; &.line .{prefix}-button.line svg > use.active, &.free .{prefix}-button.free svg > use.active display: block; /* FLIP BUTTON */ .tie-flip-button &.resetFlip .{prefix}-button.resetFlip, &.flipX .{prefix}-button.flipX, &.flipY .{prefix}-button.flipY svg > use.normal display: none; svg > use.active display: block; /* MASK BUTTON */ .tie-mask-apply.apply.active .{prefix}-button.apply label color: #fff; svg > use.active display: block; /* CROP BUTTON */ .tie-crop-button, .tie-crop-preset-button .{prefix}-button.apply margin-right: 24px; .{prefix}-button.preset.active svg > use.active display: block; .{prefix}-button.apply.active svg > use.active display: block; /* RESIZE BUTTON */ .tie-resize-button, .tie-resize-preset-button .{prefix}-button.apply margin-right: 24px; .{prefix}-button.preset.active svg > use.active display: block; .{prefix}-button.apply.active svg > use.active display: block; /* SHAPE BUTTON */ .tie-shape-button &.rect .{prefix}-button.rect, &.circle .{prefix}-button.circle, &.triangle .{prefix}-button.triangle svg > use.normal display: none; svg > use.active display: block; /* TEXT BUTTON */ .tie-text-effect-button .{prefix}-button.active svg > use.active display: block; .tie-text-align-button &.tie-text-align-left .{prefix}-button.left svg > use.active, &.tie-text-align-center .{prefix}-button.center svg > use.active, &.tie-text-align-right .{prefix}-button.right svg > use.active display: block; .tie-mask-image-file, .tie-icon-image-file opacity: 0; position: absolute; width: 100%; height: 100%; border: 1px solid green; cursor: inherit; left: 0; top: 0; /* FLIP BUTTON */ .tie-zoom-button &.resetFlip .{prefix}-button.resetFlip, &.flipX .{prefix}-button.flipX, &.flipY .{prefix}-button.flipY svg > use.normal display: none; svg > use.active display: block; ================================================ FILE: apps/image-editor/src/css/checkbox.styl ================================================ /* VIRTUAL CHECKBOX */ .{prefix}-container .filter-color-item display: inline-block; .tui-image-editor-checkbox display: block; .{prefix}-checkbox-wrap display: inline-block !important; text-align: left; .{prefix}-checkbox-wrap.fixed-width width: 187px; white-space: normal; .{prefix}-checkbox display: inline-block; margin: 1px 0 1px 0; input width: 14px; height: 14px; opacity: 0; > label > span color: #fff; height: 14px; position: relative; input + label:before, > label > span:before content: ''; position: absolute; width: 14px; height: 14px; background-color: #fff; top: 6px; left: -19px; display: inline-block; margin: 0; text-align: center; font-size: 11px; border: 0; border-radius: 2px; padding-top: 1px; box-sizing: border-box; input[type='checkbox']:checked + span:before background-size: cover; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAMBJREFUKBWVkjEOwjAMRe2WgZW7IIHEDdhghhuwcQ42rlJugAQS54Cxa5cq1QM5TUpByZfS2j9+dlJVt/tX5ZxbS4ZU9VLkQvSHKTIGRaVJYFmKrBbTCJxE2UgCdDzMZDkHrOV6b95V0US6UmgKodujEZbJg0B0ZgEModO5lrY1TMQf1TpyJGBEjD+E2NPN7ukIUDiF/BfEXgRiGEw8NgkffYGYwCi808fpn/6OvfUfsDr/Vc1IfRf8sKnFVqeiVQfDu0tf/nWH9gAAAABJRU5ErkJggg=='); .{prefix}-selectlist-wrap position: relative; select width: 100%; height: 28px; margin-top: 4px; border: 0; outline: 0; border-radius: 0; border: 1px solid #cbdbdb; background-color: #fff; -webkit-appearance: none; -moz-appearance: none; appearance: none; padding: 0 7px 0 10px; .{prefix}-selectlist display: none; position: relative; top: -1px; border: 1px solid #ccc; background-color: #fff; border-top: 0px; padding: 4px 0; li display: block; text-align: left; padding: 7px 10px; font-family: 'Noto Sans', sans-serif; li:hover background-color: rgba(81, 92, 230, 0.05); .{prefix}-selectlist-wrap:before content: ''; position: absolute; display: inline-block; width: 14px; height: 14px; right: 5px; top: 10px; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAHlJREFUKBVjYBgFOEOAEVkmPDxc89+/f6eAYjzI4kD2FyYmJrOVK1deh4kzwRggGiQBVJCELAZig8SQNYHEmEEEMrh69eo1HR0dfqCYJUickZGxf9WqVf3IakBsFBthklpaWmVA9mEQhrJhUoTp0NBQCRAmrHL4qgAAuu4cWZOZIGsAAAAASUVORK5CYII='); background-size: cover; .{prefix}-selectlist-wrap select::-ms-expand display:none; ================================================ FILE: apps/image-editor/src/css/colorpicker.styl ================================================ /* COLOR PICKER */ .{prefix}-container div.tui-colorpicker-clearfix width: 159px; height: 28px; border: 1px solid #d5d5d5; border-radius: 2px; background-color: #f5f5f5; margin-top: 6px; padding: 4px 7px 4px 7px; .tui-colorpicker-palette-hex width: 114px; background-color: #f5f5f5; border: 0; font-size: 11px; margin-top: 2px; font-family: 'Noto Sans', sans-serif; .tui-colorpicker-palette-hex[value='#ffffff'] + .tui-colorpicker-palette-preview, .tui-colorpicker-palette-hex[value=''] + .tui-colorpicker-palette-preview border: 1px solid #ccc; .tui-colorpicker-palette-hex[value=''] + .tui-colorpicker-palette-preview background-size: cover; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAdBJREFUWAnFl0FuwjAQRZ0ukiugHqFSOQNdseuKW3ALzkA4BateICvUGyCxrtRFd4WuunH/TzykaYJrnLEYaTJJsP2+x8GZZCbQrLU5mj7Bn+EP8HvnCObd+R7xBV5lWfaNON4AnsA38E94qLEt+0yiFaBzAV/Bv+Cxxr4co7hKCDpw1q9wLeNYYdlAwyn8TYt8Hme3+8D5ozcTaMCZ68PXa2tnM2sbEcOZAJhrrpl2DAcTOGNjZPSfCdzkw6JrfbiMv+osBe4y9WOedhm4jZfhbENWuxS44H9Wz/xw4WzqLOAqh1+zycgAwzEMzr5k5gaHOa9ULBwuuDkFlHI1Kl4PJ66kgIpnoywOTmRFAYcbwYk9UMApWkD8zAV5ihcwHk4Rx7gl0IFTQL0EFc+CTQ9OZHWH3YhlVJiVpTHbrTGLhTHLZVgff6s9lyBsI9KduSS83oj+34rTwJutmBmCnMsvozRwZqB5GTkBw6/jdPDu69iJ6BYk6eCcfbcgcQIK/MByaaiMqm8rHcjol2TnpWDhyAKSGdA3FrxtJUToX0ODqatetfGE+8tyEUOV8GY5dGRwLP/MBS4RHQr4bT7NRAQjlcOTfZxmv2G+c4hI8nn+Ax5PG/zhI393AAAAAElFTkSuQmCC'); .tui-colorpicker-palette-preview border-radius: 100%; float: left; width: 17px; height: 17px; border: 0; .color-picker-control position: absolute; display: none; z-index: 99; width: 192px; background-color: #fff; box-shadow: 0 3px 22px 6px rgba(0, 0, 0, .15); padding: 16px; border-radius: 2px; .tui-colorpicker-palette-toggle-slider display: none; .tui-colorpicker-palette-button border: 0; border-radius: 100%; margin: 2px; background-size: cover; font-size: 1px; &[title='#ffffff'] border: 1px solid #ccc; &[title=''] border: 1px solid #ccc; .triangle width: 0; height: 0; border-right: 7px solid transparent; border-top: 8px solid #fff; border-left: 7px solid transparent; position: absolute; bottom: -8px; left: 84px; .tui-colorpicker-container, .tui-colorpicker-palette-container ul, .tui-colorpicker-palette-container width: 100%; height: auto; .filter-color-item .color-picker-control label font-color: #333; font-weight: normal; margin-right: 7pxleft .tui-image-editor-checkbox margin-top: 0; input + label:before, > label:before left: -16px; .color-picker width: 100%; height: auto; .color-picker-value width: 32px; height: 32px; border: 0px; border-radius: 100%; margin: auto; margin-bottom: 1px; &.transparent border: 1px solid #cbcbcb; background-size: cover; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAdBJREFUWAnFl0FuwjAQRZ0ukiugHqFSOQNdseuKW3ALzkA4BateICvUGyCxrtRFd4WuunH/TzykaYJrnLEYaTJJsP2+x8GZZCbQrLU5mj7Bn+EP8HvnCObd+R7xBV5lWfaNON4AnsA38E94qLEt+0yiFaBzAV/Bv+Cxxr4co7hKCDpw1q9wLeNYYdlAwyn8TYt8Hme3+8D5ozcTaMCZ68PXa2tnM2sbEcOZAJhrrpl2DAcTOGNjZPSfCdzkw6JrfbiMv+osBe4y9WOedhm4jZfhbENWuxS44H9Wz/xw4WzqLOAqh1+zycgAwzEMzr5k5gaHOa9ULBwuuDkFlHI1Kl4PJ66kgIpnoywOTmRFAYcbwYk9UMApWkD8zAV5ihcwHk4Rx7gl0IFTQL0EFc+CTQ9OZHWH3YhlVJiVpTHbrTGLhTHLZVgff6s9lyBsI9KduSS83oj+34rTwJutmBmCnMsvozRwZqB5GTkBw6/jdPDu69iJ6BYk6eCcfbcgcQIK/MByaaiMqm8rHcjol2TnpWDhyAKSGdA3FrxtJUToX0ODqatetfGE+8tyEUOV8GY5dGRwLP/MBS4RHQr4bT7NRAQjlcOTfZxmv2G+c4hI8nn+Ax5PG/zhI393AAAAAElFTkSuQmCC'); .color-picker-value + label color: #fff; .{prefix}-submenu svg > use display: none; .{prefix}-submenu svg > use.normal display: block; ================================================ FILE: apps/image-editor/src/css/gridtable.styl ================================================ /* GRID VISUAL OF FLIP AND ROTATE MENU */ .{prefix}-container .{prefix}-grid-visual display: none; position: absolute; width: 100%; height: 100%; border: 1px solid rgba(255,255,255,0.7); .{prefix}-main.{prefix}-menu-flip, .{prefix}-main.{prefix}-menu-rotate .tui-image-editor transition: none; .{prefix}-main.{prefix}-menu-flip .{prefix}-grid-visual, .{prefix}-main.{prefix}-menu-rotate .{prefix}-grid-visual, .{prefix}-main.{prefix}-menu-resize .{prefix}-grid-visual display: block; .{prefix}-grid-visual table width: 100%; height: 100%; border-collapse: collapse; td border: 1px solid rgba(255,255,255,0.3); td.dot:before content: ''; position: absolute; box-sizing: border-box; width: 10px; height: 10px; border: 0; box-shadow: 0 0 1px 0 rgba(0,0,0,0.3); border-radius: 100%; background-color: #fff; td.dot.left-top:before top: -5px; left: -5px; td.dot.right-top:before top: -5px; right: -5px; td.dot.left-bottom:before bottom: -5px; left: -5px; td.dot.right-bottom:before bottom: -5px; right: -5px; ================================================ FILE: apps/image-editor/src/css/icon.styl ================================================ /* ICON */ .{prefix}-container .tie-icon-add-button .{prefix}-button min-width: 42px; .svg_ic-menu .svg_ic-helpmenu width: 24px; height: 24px; .svg_ic-submenu width: 32px; height: 32px; .svg_img-bi width: 257px; height: 26px; .{prefix}-help-menu .{prefix}-controls svg > use display: none; .enabled svg:hover > use.hover .normal svg:hover > use.hover display: block; .active svg:hover > use.hover display: none; .on svg > use.hover, .opened svg > use.hover display: block; svg > use.normal display: block; .active svg > use.active display: block; .enabled svg > use.enabled display: block; .active svg > use.normal, .enabled svg > use.normal display: none; .help svg > use.disabled, .help.enabled svg > use.normal display: block; .help.enabled svg > use.disabled display: none; .{prefix}-controls:hover z-index: 3; ================================================ FILE: apps/image-editor/src/css/index.styl ================================================ prefix = 'tui-image-editor' @import 'main.styl' @import 'gridtable.styl' @import 'submenu.styl' @import 'checkbox.styl' @import 'range.styl' @import 'position.styl' @import 'icon.styl' @import 'colorpicker.styl' @import 'buttons.styl' .{prefix}-container.top &.{prefix}-top-optimization .{prefix}-controls ul text-align: right; .{prefix}-controls-logo display: none; ================================================ FILE: apps/image-editor/src/css/main.styl ================================================ body > textarea position: fixed !important; +prefix-classes(prefix) .-container margin: 0; padding: 0; box-sizing: border-box; min-height: 300px; height: 100%; position: relative; background-color: #282828; overflow: hidden; letter-spacing: 0.3px; div, ul, label, input, li box-sizing: border-box; margin: 0; padding: 0; -ms-user-select: none; -moz-user-select: -moz-none; -khtml-user-select: none; -webkit-user-select: none; user-select: none; .-header /* BUTTON AND LOGO */ min-width: 533px; position: absolute; background-color: #151515; top: 0; width: 100%; .-header-buttons, .-controls-buttons float: right; margin: 8px; .-header-logo, .-controls-logo float: left; width: 30%; padding: 17px; .-controls-logo, .-controls-buttons width: 270px; height: 100%; display: none; .-header-buttons button, .-header-buttons div, .-controls-buttons button, .-controls-buttons div display: inline-block; position: relative; width: 120px; height: 40px; padding: 0; line-height: 40px; outline: none; border-radius: 20px; border: 1px solid #ddd; font-family: 'Noto Sans', sans-serif; font-size: 12px; font-weight: bold; cursor: pointer; vertical-align: middle; letter-spacing: 0.3px; text-align: center; .-download-btn background-color: #fdba3b; border-color: #fdba3b; color: #fff; .-load-btn position: absolute; left: 0; right: 0; display: inline-block; top: 0; bottom: 0; width: 100%; cursor: pointer; opacity: 0; .-main-container position: absolute; width: 100%; top: 0; bottom: 64px; .-main position: absolute; text-align: center; top: 64px; bottom: 0; right: 0; left: 0; .-wrap position: absolute; bottom: 0; width: 100%; overflow: auto; .-size-wrap display: table; width: 100%; height: 100% .-align-wrap display: table-cell; vertical-align: middle; . position: relative; display: inline-block; /* BIG MENU */ .{prefix}-container .{prefix}-menu, .{prefix}-help-menu width: auto; list-style: none; padding: 0; margin: 0 auto; display: table-cell; text-align: center; vertical-align: middle; white-space: nowrap; > .{prefix}-item position: relative; display: inline-block; border-radius: 2px; padding: 7px 8px 3px 8px; cursor: pointer; margin: 0 4px; > .{prefix}-item[tooltip-content]:hover &:before content: ''; position: absolute; display: inline-block; margin: 0 auto 0; width: 0; height: 0; border-right: 7px solid transparent; border-top: 7px solid #2f2f2f; border-left: 7px solid transparent; left: 13px; top: -2px; &:after content: attr(tooltip-content); position: absolute; display: inline-block; background-color: #2f2f2f; color: #fff; padding: 5px 8px; font-size: 11px; font-weight: lighter; border-radius: 3px; max-height: 23px; top: -25px; left: 0; min-width: 24px; > .{prefix}-item.active background-color: #fff; transition: all .3s ease; .{prefix}-wrap position: absolute; ================================================ FILE: apps/image-editor/src/css/position.styl ================================================ /* POSITION LEFT */ .{prefix}-container &.left .{prefix}-menu > .{prefix}-item[tooltip-content] &:before left: 28px; top: 11px; border-right: 7px solid #2f2f2f; border-top: 7px solid transparent; border-bottom: 7px solid transparent; &:after top: 7px; left: 42px; white-space: nowrap; .{prefix}-submenu left: 0; height: 100%; width: 248px; .{prefix}-main-container left: 64px; width: calc(100% - 64px); height: 100%; .{prefix}-controls width: 64px; height: 100%; display: table; /* POSITION LEFT & RIGHT */ .{prefix}-container &.left, &.right .{prefix}-menu white-space: inherit; .{prefix}-submenu white-space: normal; > div vertical-align: middle; .{prefix}-controls li display: inline-block; margin: 4px auto; .{prefix}-icpartition position: relative; top: -7px; width: 24px; height: 1px; .{prefix}-submenu .{prefix}-partition display: block; width: 75%; margin: auto; > div border-left: 0; height:10px; border-bottom: 1px solid #3c3c3c; width: 100%; margin: 0; .{prefix}-submenu-align margin-right: 0; .{prefix}-submenu-item li margin-top: 15px; .tui-colorpicker-clearfix li margin-top: 0; .{prefix}-checkbox-wrap.fixed-width width: 182px; white-space: normal; .{prefix}-range-wrap.{prefix}-newline label.range display: block; text-align: left; width: 75%; margin: auto; .{prefix}-range width: 136px; /* POSITION RIGHT */ .{prefix}-container &.right .{prefix}-menu > .{prefix}-item[tooltip-content] &:before left: -3px; top: 11px; border-left: 7px solid #2f2f2f; border-top: 7px solid transparent; border-bottom: 7px solid transparent; &:after top: 7px; left: unset; right: 43px; white-space: nowrap; .{prefix}-submenu right: 0; height: 100%; width: 248px; .{prefix}-main-container right: 64px; width: calc(100% - 64px); height: 100%; .{prefix}-controls right: 0; width: 64px; height: 100%; display: table; /* POSITION TOP & BOTTOM */ .{prefix}-container &.top, &.bottom .{prefix}-submenu .{prefix}-partition.only-left-right display: none; /* POSITION BOTTOM */ .{prefix}-container &.bottom .tui-image-editor-submenu > div padding-bottom: 24px; /* POSITION TOP */ .{prefix}-container &.top .color-picker-control .triangle top: -8px; border-right: 7px solid transparent; border-top: 0px; border-left: 7px solid transparent; border-bottom: 8px solid #fff; .{prefix}-size-wrap height: 100%; .{prefix}-main-container bottom: 0; .{prefix}-menu > .{prefix}-item[tooltip-content] &:before left: 13px; border-top: 0; border-bottom: 7px solid #2f2f2f; top: 33px; &:after top: 38px; .{prefix}-submenu top: 0; bottom: auto; > div padding-top: 24px; vertical-align: top; .{prefix}-controls-logo display: table-cell; .{prefix}-controls-buttons display: table-cell; .{prefix}-main top: 64px; height: calc(100% - 64px); .{prefix}-controls top: 0; bottom: inherit; /* HELP MENUBAR POSITION TOP */ .{prefix}-container .{prefix}-help-menu &.top white-space: nowrap; width: 506px; height: 40px; top: 8px; left: 50%; transform: translateX(-50%); .tie-panel-history top: 45px; .opened .tie-panel-history:before border-right: 8px solid transparent; border-left: 8px solid transparent; border-bottom: 8px solid #fff; left: 90px; top: -8px; > .{prefix}-item[tooltip-content] &:before left: 13px; top: 35px; border: none; border-bottom: 7px solid #2f2f2f; border-left: 7px solid transparent; border-right: 7px solid transparent; &:after top: 41px; left: -4px; white-space: nowrap; > .{prefix}-item[tooltip-content].opened &:before, &:after content: none; /* HELP MENUBAR POSITION BOTTOM */ .{prefix}-container .{prefix}-help-menu &.bottom white-space: nowrap; width: 506px; height: 40px; bottom: 8px; left: 50%; transform: translateX(-50%); .tie-panel-history bottom: 45px; .opened .tie-panel-history:before border-right: 8px solid transparent; border-left: 8px solid transparent; border-top: 8px solid #fff; left: 90px; bottom: -8px; > .{prefix}-item[tooltip-content] &:before left: 13px; top: auto; bottom: 36px; border: none; border-top: 7px solid #2f2f2f; border-left: 7px solid transparent; border-right: 7px solid transparent; &:after top: auto; left: -4px; bottom: 41px; white-space: nowrap; > .{prefix}-item[tooltip-content].opened &:before, &:after content: none; /* HELP MENUBAR POSITION LEFT */ .{prefix}-container .{prefix}-help-menu &.left white-space: inherit; width: 40px; height: 506px; left: 8px; top: 50%; transform: translateY(-50%); .tie-panel-history left: 140px; top: -4px; .opened .tie-panel-history:before border-top: 8px solid transparent; border-bottom: 8px solid transparent; border-right: 8px solid #fff; left: -8px; top: 14px; .{prefix}-item margin: 4px auto; padding: 6px 8px; > .{prefix}-item[tooltip-content] &:before left: 27px; top: 11px; border: none; border-right: 7px solid #2f2f2f; border-top: 7px solid transparent; border-bottom: 7px solid transparent; &:after top: 7px; left: 40px; white-space: nowrap; > .{prefix}-item[tooltip-content].opened &:before, &:after content: none; /* HELP MENUBAR POSITION RIGHT */ .{prefix}-container .{prefix}-help-menu &.right white-space: inherit; width: 40px; height: 506px; right: 8px; top: 50%; transform: translateY(-50%); .tie-panel-history right: -30px; top: -4px; .opened .tie-panel-history:before border-top: 8px solid transparent; border-bottom: 8px solid transparent; border-left: 8px solid #fff; right: -8px; top: 14px; .{prefix}-item margin: 4px auto; padding: 6px 8px; > .{prefix}-item[tooltip-content] &:before left: -6px; top: 11px; border: none; border-left: 7px solid #2f2f2f; border-top: 7px solid transparent; border-bottom: 7px solid transparent; &:after top: 7px; left: auto; right: 39px; white-space: nowrap; > .{prefix}-item[tooltip-content].opened &:before, &:after content: none; ================================================ FILE: apps/image-editor/src/css/range.styl ================================================ /* VIRTUAL RANGE */ .{prefix}-container .{prefix}-virtual-range-bar .{prefix}-virtual-range-subbar .{prefix}-virtual-range-pointer .{prefix}-disabled background-color: red; .{prefix}-range position: relative; top: 5px; width: 166px; height: 17px; display: inline-block; .{prefix}-virtual-range-bar top: 7px; position: absolute; width: 100%; height: 2px; background-color: #666666; .{prefix}-virtual-range-subbar position: absolute; height: 100%; left: 0; right: 0; background-color: #d1d1d1; .{prefix}-virtual-range-pointer position: absolute; cursor: pointer; top: -5px; left: 0; width: 12px; height: 12px; background-color: #fff; border-radius: 100%; .{prefix}-range-wrap display: inline-block; margin-left: 4px; &.short .tui-image-editor-range width: 100px; .color-picker-control .{prefix}-range width: 108px; margin-left: 10px; .{prefix}-virtual-range-pointer background-color: #333; .{prefix}-virtual-range-bar background-color: #ccc; .{prefix}-virtual-range-subbar background-color: #606060; .{prefix}-range-wrap.{prefix}-newline.short margin-top: -2px; margin-left: 19px; label color: #8e8e8e; font-weight: normal; .{prefix}-range-wrap label vertical-align: baseline; font-size: 11px; margin-right: 7px; color: #fff; .{prefix}-range-value cursor: default; width: 40px; height: 24px; outline: none; border-radius: 2px; box-shadow: none; border: 1px solid #d5d5d5; text-align: center; background-color: #1c1c1c; color: #fff; font-weight: lighter; vertical-align: baseline; font-family: 'Noto Sans', sans-serif; margin-top: 15px; margin-left: 4px; .{prefix}-controls position: absolute; background-color: #151515; width: 100%; height: 64px; display: table; bottom: 0; z-index: 2; .{prefix}-icpartition display: inline-block; background-color: #444; width: 1px; height: 24px; ================================================ FILE: apps/image-editor/src/css/submenu.styl ================================================ /* SUBMENU */ .{prefix}-container .{prefix}-submenu display: none; position: absolute; bottom: 0; width:100%; height: 150px; white-space: nowrap; z-index: 2; .{prefix}-button:hover svg > use.active display: block; .{prefix}-submenu-item li display: inline-block; vertical-align: top; .{prefix}-newline display: block; margin-top: 0; .{prefix}-button position: relative; cursor: pointer; display: inline-block; font-weight: normal; font-size: 11px; margin: 0 9px 0 9px; .{prefix}-button.preset margin: 0 9px 20px 5px; label > span display: inline-block; cursor: pointer; padding-top: 5px; font-family: "Noto Sans", sans-serif; font-size: 11px; .{prefix}-button.apply label, .{prefix}-button.cancel label vertical-align: 7px; > div display: none; vertical-align: bottom; .{prefix}-submenu-style opacity: 0.95; z-index: -1; position: absolute; top: 0; bottom: 0; left: 0; right: 0; display: block; .{prefix}-partition > div width: 1px; height: 52px; border-left: 1px solid #3c3c3c; margin: 0 8px 0 8px; .{prefix}-main.{prefix}-menu-filter .{prefix}-partition > div height: 108px; margin: 0 29px 0 0px; .{prefix}-submenu-align text-align: left; margin-right: 30px; label > span width: 55px; white-space: nowrap; .{prefix}-submenu-align:first-child margin-right: 0; label > span width: 70px; .{prefix}-main.{prefix}-menu-crop .{prefix}-submenu > div.{prefix}-menu-crop, .{prefix}-main.{prefix}-menu-resize .{prefix}-submenu > div.{prefix}-menu-resize, .{prefix}-main.{prefix}-menu-flip .{prefix}-submenu > div.{prefix}-menu-flip, .{prefix}-main.{prefix}-menu-rotate .{prefix}-submenu > div.{prefix}-menu-rotate, .{prefix}-main.{prefix}-menu-shape .{prefix}-submenu > div.{prefix}-menu-shape, .{prefix}-main.{prefix}-menu-text .{prefix}-submenu > div.{prefix}-menu-text, .{prefix}-main.{prefix}-menu-mask .{prefix}-submenu > div.{prefix}-menu-mask, .{prefix}-main.{prefix}-menu-icon .{prefix}-submenu > div.{prefix}-menu-icon, .{prefix}-main.{prefix}-menu-draw .{prefix}-submenu > div.{prefix}-menu-draw, .{prefix}-main.{prefix}-menu-filter .{prefix}-submenu > div.{prefix}-menu-filter, .{prefix}-main.{prefix}-menu-zoom .{prefix}-submenu > div.{prefix}-menu-zoom display: table-cell; .{prefix}-main.{prefix}-menu-crop, .{prefix}-main.{prefix}-menu-resize, .{prefix}-main.{prefix}-menu-flip, .{prefix}-main.{prefix}-menu-rotate, .{prefix}-main.{prefix}-menu-shape, .{prefix}-main.{prefix}-menu-text, .{prefix}-main.{prefix}-menu-mask, .{prefix}-main.{prefix}-menu-icon, .{prefix}-main.{prefix}-menu-draw, .{prefix}-main.{prefix}-menu-filter, .{prefix}-main.{prefix}-menu-zoom .{prefix}-submenu display: table; /* Help menu bar */ .{prefix}-container .{prefix}-help-menu list-style: none; padding: 0; margin: 0 auto; text-align: center; vertical-align: middle; border-radius: 20px; background-color: rgba(255, 255, 255, 0.06); z-index: 2; position: absolute; .tie-panel-history display: none; background-color: #fff; color: #444; position: absolute; width: 196px; height: 276px; padding: 4px 2px; box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.15); cursor: auto; transform: translateX(calc(-50% + 12px)); .history-list height: 268px; padding: 0; overflow: hidden scroll; list-style: none; .history-item height: 24px; font-size: 11px; line-height: 24px; .{prefix}-history-item position: relative; height: 24px; cursor: pointer; svg width: 24px; height: 24px; span display: inline-block; width: 128px; height: 24px; text-align: left; .history-item-icon display: inline-block; width: 24px; height: 24px; position: absolute; top: 6px; left: 6px; .history-item-checkbox display: none; width: 24px; height: 24px; position: absolute; top: 5px; right: -6px; &.selected-item background-color: rgba(119, 119, 119, 0.12); .history-item-checkbox display: inline-block; &.disabled-item color: #333; opacity: 0.3; .opened .tie-panel-history display: block; &:before content: ''; position: absolute; display: inline-block; margin: 0 auto; width: 0; height: 0; ================================================ FILE: apps/image-editor/src/index.js ================================================ import '@/polyfill'; import ImageEditor from '@/imageEditor'; import '@css/index.styl'; // commands import '@/command/addIcon'; import '@/command/addImageObject'; import '@/command/addObject'; import '@/command/addShape'; import '@/command/addText'; import '@/command/applyFilter'; import '@/command/changeIconColor'; import '@/command/changeShape'; import '@/command/changeText'; import '@/command/changeTextStyle'; import '@/command/clearObjects'; import '@/command/flip'; import '@/command/loadImage'; import '@/command/removeFilter'; import '@/command/removeObject'; import '@/command/resizeCanvasDimension'; import '@/command/rotate'; import '@/command/setObjectProperties'; import '@/command/setObjectPosition'; import '@/command/changeSelection'; import '@/command/resize'; export default ImageEditor; export { ImageEditor }; ================================================ FILE: apps/image-editor/src/js/action.js ================================================ import extend from 'tui-code-snippet/object/extend'; import Imagetracer from '@/helper/imagetracer'; import { isSupportFileApi, base64ToBlob, toInteger, isEmptyCropzone, includes } from '@/util'; import { eventNames, historyNames, drawingModes, drawingMenuNames, zoomModes } from '@/consts'; export default { /** * Get ui actions * @returns {Object} actions for ui * @private */ getActions() { return { main: this._mainAction(), shape: this._shapeAction(), crop: this._cropAction(), resize: this._resizeAction(), flip: this._flipAction(), rotate: this._rotateAction(), text: this._textAction(), mask: this._maskAction(), draw: this._drawAction(), icon: this._iconAction(), filter: this._filterAction(), history: this._historyAction(), }; }, /** * Main Action * @returns {Object} actions for ui main * @private */ _mainAction() { const exitCropOnAction = () => { if (this.ui.submenu === 'crop') { this.stopDrawingMode(); this.ui.changeMenu('crop'); } }; const setAngleRangeBarOnAction = (angle) => { if (this.ui.submenu === 'rotate') { this.ui.rotate.setRangeBarAngle('setAngle', angle); } }; const setFilterStateRangeBarOnAction = (filterOptions) => { if (this.ui.submenu === 'filter') { this.ui.filter.setFilterState(filterOptions); } }; const onEndUndoRedo = (result) => { setAngleRangeBarOnAction(result); setFilterStateRangeBarOnAction(result); return result; }; const toggleZoomMode = () => { const zoomMode = this._graphics.getZoomMode(); this.stopDrawingMode(); if (zoomMode !== zoomModes.ZOOM) { this.startDrawingMode(drawingModes.ZOOM); this._graphics.startZoomInMode(); } else { this._graphics.endZoomInMode(); } }; const toggleHandMode = () => { const zoomMode = this._graphics.getZoomMode(); this.stopDrawingMode(); if (zoomMode !== zoomModes.HAND) { this.startDrawingMode(drawingModes.ZOOM); this._graphics.startHandMode(); } else { this._graphics.endHandMode(); } }; const initFilterState = () => { if (this.ui.filter) { this.ui.filter.initFilterCheckBoxState(); } }; return extend( { initLoadImage: (imagePath, imageName) => this.loadImageFromURL(imagePath, imageName).then((sizeValue) => { exitCropOnAction(); this.ui.initializeImgUrl = imagePath; this.ui.resizeEditor({ imageSize: sizeValue }); this.clearUndoStack(); this._invoker.fire(eventNames.EXECUTE_COMMAND, historyNames.LOAD_IMAGE); }), undo: () => { if (!this.isEmptyUndoStack()) { exitCropOnAction(); this.deactivateAll(); this.undo().then(onEndUndoRedo); } }, redo: () => { if (!this.isEmptyRedoStack()) { exitCropOnAction(); this.deactivateAll(); this.redo().then(onEndUndoRedo); } }, reset: () => { exitCropOnAction(); this.loadImageFromURL(this.ui.initializeImgUrl, 'resetImage').then((sizeValue) => { exitCropOnAction(); initFilterState(); this.ui.resizeEditor({ imageSize: sizeValue }); this.clearUndoStack(); this._initHistory(); }); }, delete: () => { this.ui.changeHelpButtonEnabled('delete', false); exitCropOnAction(); this.removeActiveObject(); this.activeObjectId = null; }, deleteAll: () => { exitCropOnAction(); this.clearObjects(); this.ui.changeHelpButtonEnabled('delete', false); this.ui.changeHelpButtonEnabled('deleteAll', false); }, load: (file) => { if (!isSupportFileApi()) { alert('This browser does not support file-api'); } this.ui.initializeImgUrl = URL.createObjectURL(file); this.loadImageFromFile(file) .then((sizeValue) => { exitCropOnAction(); initFilterState(); this.clearUndoStack(); this.ui.activeMenuEvent(); this.ui.resizeEditor({ imageSize: sizeValue }); this._clearHistory(); this._invoker.fire(eventNames.EXECUTE_COMMAND, historyNames.LOAD_IMAGE); }) ['catch']((message) => Promise.reject(message)); }, download: () => { const dataURL = this.toDataURL(); let imageName = this.getImageName(); let blob, type, w; if (isSupportFileApi() && window.saveAs) { blob = base64ToBlob(dataURL); type = blob.type.split('/')[1]; if (imageName.split('.').pop() !== type) { imageName += `.${type}`; } saveAs(blob, imageName); // eslint-disable-line } else { w = window.open(); w.document.body.innerHTML = ``; } }, history: (event) => { this.ui.toggleHistoryMenu(event); }, zoomIn: () => { this.ui.toggleZoomButtonStatus('zoomIn'); this.deactivateAll(); toggleZoomMode(); }, zoomOut: () => { this._graphics.zoomOut(); }, hand: () => { this.ui.offZoomInButtonStatus(); this.ui.toggleZoomButtonStatus('hand'); this.deactivateAll(); toggleHandMode(); }, }, this._commonAction() ); }, /** * Icon Action * @returns {Object} actions for ui icon * @private */ _iconAction() { return extend( { changeColor: (color) => { if (this.activeObjectId) { this.changeIconColor(this.activeObjectId, color); } }, addIcon: (iconType, iconColor) => { this.startDrawingMode('ICON'); this.setDrawingIcon(iconType, iconColor); }, cancelAddIcon: () => { this.ui.icon.clearIconType(); this.changeSelectableAll(true); this.changeCursor('default'); this.stopDrawingMode(); }, registerDefaultIcons: (type, path) => { const iconObj = {}; iconObj[type] = path; this.registerIcons(iconObj); }, registerCustomIcon: (imgUrl, file) => { const imagetracer = new Imagetracer(); imagetracer.imageToSVG( imgUrl, (svgstr) => { const [, svgPath] = svgstr.match(/path[^>]*d="([^"]*)"/); const iconObj = {}; iconObj[file.name] = svgPath; this.registerIcons(iconObj); this.addIcon(file.name, { left: 100, top: 100, }); }, Imagetracer.tracerDefaultOption() ); }, }, this._commonAction() ); }, /** * Draw Action * @returns {Object} actions for ui draw * @private */ _drawAction() { return extend( { setDrawMode: (type, settings) => { this.stopDrawingMode(); if (type === 'free') { this.startDrawingMode('FREE_DRAWING', settings); } else { this.startDrawingMode('LINE_DRAWING', settings); } }, setColor: (color) => { this.setBrush({ color, }); }, }, this._commonAction() ); }, /** * Mask Action * @returns {Object} actions for ui mask * @private */ _maskAction() { return extend( { loadImageFromURL: (imgUrl, file) => { return this.loadImageFromURL(this.toDataURL(), 'FilterImage').then(() => { this.addImageObject(imgUrl).then(() => { URL.revokeObjectURL(file); }); this._invoker.fire(eventNames.EXECUTE_COMMAND, historyNames.LOAD_MASK_IMAGE); }); }, applyFilter: () => { this.applyFilter('mask', { maskObjId: this.activeObjectId, }); }, }, this._commonAction() ); }, /** * Text Action * @returns {Object} actions for ui text * @private */ _textAction() { return extend( { changeTextStyle: (styleObj, isSilent) => { if (this.activeObjectId) { this.changeTextStyle(this.activeObjectId, styleObj, isSilent); } }, }, this._commonAction() ); }, /** * Rotate Action * @returns {Object} actions for ui rotate * @private */ _rotateAction() { return extend( { rotate: (angle, isSilent) => { this.rotate(angle, isSilent); this.ui.resizeEditor(); this.ui.rotate.setRangeBarAngle('rotate', angle); }, setAngle: (angle, isSilent) => { this.setAngle(angle, isSilent); this.ui.resizeEditor(); this.ui.rotate.setRangeBarAngle('setAngle', angle); }, }, this._commonAction() ); }, /** * Shape Action * @returns {Object} actions for ui shape * @private */ _shapeAction() { return extend( { changeShape: (changeShapeObject, isSilent) => { if (this.activeObjectId) { this.changeShape(this.activeObjectId, changeShapeObject, isSilent); } }, setDrawingShape: (shapeType) => { this.setDrawingShape(shapeType); }, }, this._commonAction() ); }, /** * Crop Action * @returns {Object} actions for ui crop * @private */ _cropAction() { return extend( { crop: () => { const cropRect = this.getCropzoneRect(); if (cropRect && !isEmptyCropzone(cropRect)) { this.crop(cropRect) .then(() => { this.stopDrawingMode(); this.ui.resizeEditor(); this.ui.changeMenu('crop'); this._invoker.fire(eventNames.EXECUTE_COMMAND, historyNames.CROP); }) ['catch']((message) => Promise.reject(message)); } }, cancel: () => { this.stopDrawingMode(); this.ui.changeMenu('crop'); }, /* eslint-disable */ preset: (presetType) => { switch (presetType) { case 'preset-square': this.setCropzoneRect(1 / 1); break; case 'preset-3-2': this.setCropzoneRect(3 / 2); break; case 'preset-4-3': this.setCropzoneRect(4 / 3); break; case 'preset-5-4': this.setCropzoneRect(5 / 4); break; case 'preset-7-5': this.setCropzoneRect(7 / 5); break; case 'preset-16-9': this.setCropzoneRect(16 / 9); break; default: this.setCropzoneRect(); this.ui.crop.changeApplyButtonStatus(false); break; } }, }, this._commonAction() ); }, /** * Resize Action * @returns {Object} actions for ui resize * @private */ _resizeAction() { return extend( { getCurrentDimensions: () => this._graphics.getCurrentDimensions(), preview: (actor, value, lockState) => { const currentDimensions = this._graphics.getCurrentDimensions(); const calcAspectRatio = () => currentDimensions.width / currentDimensions.height; let dimensions = {}; switch (actor) { case 'width': dimensions.width = value; if (lockState) { dimensions.height = value / calcAspectRatio(); } else { dimensions.height = currentDimensions.height; } break; case 'height': dimensions.height = value; if (lockState) { dimensions.width = value * calcAspectRatio(); } else { dimensions.width = currentDimensions.width; } break; default: dimensions = currentDimensions; } this._graphics.resize(dimensions).then(() => { this.ui.resizeEditor(); }); if (lockState) { this.ui.resize.setWidthValue(dimensions.width); this.ui.resize.setHeightValue(dimensions.height); } }, resize: (dimensions = null) => { if (!dimensions) { dimensions = this._graphics.getCurrentDimensions(); } this.resize(dimensions) .then(() => { this._graphics.setOriginalDimensions(dimensions); this.stopDrawingMode(); this.ui.resizeEditor(); this.ui.changeMenu('resize'); }) ['catch']((message) => Promise.reject(message)); }, reset: (standByMode = false) => { const dimensions = this._graphics.getOriginalDimensions(); this.ui.resize.setWidthValue(dimensions.width, true); this.ui.resize.setHeightValue(dimensions.height, true); this._graphics.resize(dimensions).then(() => { if (!standByMode) { this.stopDrawingMode(); this.ui.resizeEditor(); this.ui.changeMenu('resize'); } }); }, }, this._commonAction() ); }, /** * Flip Action * @returns {Object} actions for ui flip * @private */ _flipAction() { return extend( { flip: (flipType) => this[flipType](), }, this._commonAction() ); }, /** * Filter Action * @returns {Object} actions for ui filter * @private */ _filterAction() { return extend( { applyFilter: (applying, type, options, isSilent) => { if (applying) { this.applyFilter(type, options, isSilent); } else if (this.hasFilter(type)) { this.removeFilter(type); } }, }, this._commonAction() ); }, /** * Image Editor Event Observer */ setReAction() { this.on({ undoStackChanged: (length) => { if (length) { this.ui.changeHelpButtonEnabled('undo', true); this.ui.changeHelpButtonEnabled('reset', true); } else { this.ui.changeHelpButtonEnabled('undo', false); this.ui.changeHelpButtonEnabled('reset', false); } this.ui.resizeEditor(); }, redoStackChanged: (length) => { if (length) { this.ui.changeHelpButtonEnabled('redo', true); } else { this.ui.changeHelpButtonEnabled('redo', false); } this.ui.resizeEditor(); }, /* eslint-disable complexity */ objectActivated: (obj) => { this.activeObjectId = obj.id; this.ui.changeHelpButtonEnabled('delete', true); this.ui.changeHelpButtonEnabled('deleteAll', true); if (obj.type === 'cropzone') { this.ui.crop.changeApplyButtonStatus(true); } else if (['rect', 'circle', 'triangle'].indexOf(obj.type) > -1) { this.stopDrawingMode(); if (this.ui.submenu !== 'shape') { this.ui.changeMenu('shape', false, false); } this.ui.shape.setShapeStatus({ strokeColor: obj.stroke, strokeWidth: obj.strokeWidth, fillColor: obj.fill, }); this.ui.shape.setMaxStrokeValue(Math.min(obj.width, obj.height)); } else if (obj.type === 'path' || obj.type === 'line') { if (this.ui.submenu !== 'draw') { this.ui.changeMenu('draw', false, false); this.ui.draw.changeStandbyMode(); } } else if (['i-text', 'text'].indexOf(obj.type) > -1) { if (this.ui.submenu !== 'text') { this.ui.changeMenu('text', false, false); } this.ui.text.setTextStyleStateOnAction(obj); } else if (obj.type === 'icon') { this.stopDrawingMode(); if (this.ui.submenu !== 'icon') { this.ui.changeMenu('icon', false, false); } this.ui.icon.setIconPickerColor(obj.fill); } }, /* eslint-enable complexity */ addText: (pos) => { const { textColor: fill, fontSize, fontStyle, fontWeight, underline } = this.ui.text; const fontFamily = 'Noto Sans'; this.addText('Double Click', { position: pos.originPosition, styles: { fill, fontSize, fontFamily, fontStyle, fontWeight, underline }, }).then(() => { this.changeCursor('default'); }); }, addObjectAfter: (obj) => { if (obj.type === 'icon') { this.ui.icon.changeStandbyMode(); } else if (['rect', 'circle', 'triangle'].indexOf(obj.type) > -1) { this.ui.shape.setMaxStrokeValue(Math.min(obj.width, obj.height)); this.ui.shape.changeStandbyMode(); } }, objectScaled: (obj) => { if (['i-text', 'text'].indexOf(obj.type) > -1) { this.ui.text.fontSize = toInteger(obj.fontSize); } else if (['rect', 'circle', 'triangle'].indexOf(obj.type) >= 0) { const { width, height } = obj; const strokeValue = this.ui.shape.getStrokeValue(); if (width < strokeValue) { this.ui.shape.setStrokeValue(width); } if (height < strokeValue) { this.ui.shape.setStrokeValue(height); } } }, selectionCleared: () => { this.activeObjectId = null; if (this.ui.submenu === 'text') { this.changeCursor('text'); } else if (!includes(['draw', 'crop', 'resize'], this.ui.submenu)) { this.stopDrawingMode(); } }, }); }, /** * History Action * @returns {Object} history actions for ui * @private */ _historyAction() { return { undo: (count) => this.undo(count), redo: (count) => this.redo(count), }; }, /** * Common Action * @returns {Object} common actions for ui * @private */ _commonAction() { const { TEXT, CROPPER, SHAPE, ZOOM, RESIZE } = drawingModes; return { modeChange: (menu) => { switch (menu) { case drawingMenuNames.TEXT: this._changeActivateMode(TEXT); break; case drawingMenuNames.CROP: this.startDrawingMode(CROPPER); break; case drawingMenuNames.SHAPE: this._changeActivateMode(SHAPE); this.setDrawingShape(this.ui.shape.type, this.ui.shape.options); break; case drawingMenuNames.ZOOM: this.startDrawingMode(ZOOM); break; case drawingMenuNames.RESIZE: this.startDrawingMode(RESIZE); break; default: break; } }, deactivateAll: this.deactivateAll.bind(this), changeSelectableAll: this.changeSelectableAll.bind(this), discardSelection: this.discardSelection.bind(this), stopDrawingMode: this.stopDrawingMode.bind(this), }; }, /** * Mixin * @param {ImageEditor} ImageEditor instance */ mixin(ImageEditor) { extend(ImageEditor.prototype, this); }, }; ================================================ FILE: apps/image-editor/src/js/command/addIcon.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames } from '@/consts'; const { ICON } = componentNames; const command = { name: commandNames.ADD_ICON, /** * Add an icon * @param {Graphics} graphics - Graphics instance * @param {string} type - Icon type ('arrow', 'cancel', custom icon name) * @param {Object} options - Icon options * @param {string} [options.fill] - Icon foreground color * @param {string} [options.left] - Icon x position * @param {string} [options.top] - Icon y position * @returns {Promise} */ execute(graphics, type, options) { const iconComp = graphics.getComponent(ICON); return iconComp.add(type, options).then((objectProps) => { this.undoData.object = graphics.getObject(objectProps.id); return objectProps; }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { graphics.remove(this.undoData.object); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/addImageObject.js ================================================ import commandFactory from '@/factory/command'; import { commandNames } from '@/consts'; const command = { name: commandNames.ADD_IMAGE_OBJECT, /** * Add an image object * @param {Graphics} graphics - Graphics instance * @param {string} imgUrl - Image url to make object * @returns {Promise} */ execute(graphics, imgUrl) { return graphics.addImageObject(imgUrl).then((objectProps) => { this.undoData.object = graphics.getObject(objectProps.id); return objectProps; }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { graphics.remove(this.undoData.object); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/addObject.js ================================================ import commandFactory from '@/factory/command'; import { commandNames, rejectMessages } from '@/consts'; const command = { name: commandNames.ADD_OBJECT, /** * Add an object * @param {Graphics} graphics - Graphics instance * @param {Object} object - Fabric object * @returns {Promise} */ execute(graphics, object) { return new Promise((resolve, reject) => { if (!graphics.contains(object)) { graphics.add(object); resolve(object); } else { reject(rejectMessages.addedObject); } }); }, /** * @param {Graphics} graphics - Graphics instance * @param {Object} object - Fabric object * @returns {Promise} */ undo(graphics, object) { return new Promise((resolve, reject) => { if (graphics.contains(object)) { graphics.remove(object); resolve(object); } else { reject(rejectMessages.noObject); } }); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/addShape.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames } from '@/consts'; const { SHAPE } = componentNames; const command = { name: commandNames.ADD_SHAPE, /** * Add a shape * @param {Graphics} graphics - Graphics instance * @param {string} type - Shape type (ex: 'rect', 'circle', 'triangle') * @param {Object} options - Shape options * @param {string} [options.fill] - Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stroke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {number} [options.left] - Shape x position * @param {number} [options.top] - Shape y position * @param {number} [options.isRegular] - Whether resizing shape has 1:1 ratio or not * @returns {Promise} */ execute(graphics, type, options) { const shapeComp = graphics.getComponent(SHAPE); return shapeComp.add(type, options).then((objectProps) => { const { id } = objectProps; this.undoData.object = graphics.getObject(id); return objectProps; }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { graphics.remove(this.undoData.object); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/addText.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames, rejectMessages } from '@/consts'; import { setCachedUndoDataForDimension, makeSelectionUndoData, makeSelectionUndoDatum, } from '@/helper/selectionModifyHelper'; const { TEXT } = componentNames; const command = { name: commandNames.ADD_TEXT, /** * Add a text object * @param {Graphics} graphics - Graphics instance * @param {string} text - Initial input text * @param {Object} [options] Options for text styles * @param {Object} [options.styles] Initial styles * @param {string} [options.styles.fill] Color * @param {string} [options.styles.fontFamily] Font type for text * @param {number} [options.styles.fontSize] Size * @param {string} [options.styles.fontStyle] Type of inclination (normal / italic) * @param {string} [options.styles.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [options.styles.textAlign] Type of text align (left / center / right) * @param {string} [options.styles.textDecoration] Type of line (underline / line-through / overline) * @param {{x: number, y: number}} [options.position] - Initial position * @returns {Promise} */ execute(graphics, text, options) { const textComp = graphics.getComponent(TEXT); if (this.undoData.object) { const undoObject = this.undoData.object; return new Promise((resolve, reject) => { if (!graphics.contains(undoObject)) { graphics.add(undoObject); resolve(undoObject); } else { reject(rejectMessages.redo); } }); } return textComp.add(text, options).then((objectProps) => { const { id } = objectProps; const textObject = graphics.getObject(id); this.undoData.object = textObject; setCachedUndoDataForDimension( makeSelectionUndoData(textObject, () => makeSelectionUndoDatum(id, textObject, false)) ); return objectProps; }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { graphics.remove(this.undoData.object); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/applyFilter.js ================================================ import extend from 'tui-code-snippet/object/extend'; import commandFactory from '@/factory/command'; import { componentNames, rejectMessages, commandNames } from '@/consts'; const { FILTER } = componentNames; /** * Cached data for undo * @type {Object} */ let cachedUndoDataForSilent = null; /** * Make undoData * @param {string} type - Filter type * @param {Object} prevfilterOption - prev Filter options * @param {Object} options - Filter options * @returns {object} - undo data */ function makeUndoData(type, prevfilterOption, options) { const undoData = {}; if (type === 'mask') { undoData.object = options.mask; } undoData.options = prevfilterOption; return undoData; } const command = { name: commandNames.APPLY_FILTER, /** * Apply a filter into an image * @param {Graphics} graphics - Graphics instance * @param {string} type - Filter type * @param {Object} options - Filter options * @param {number} options.maskObjId - masking image object id * @param {boolean} isSilent - is silent execution or not * @returns {Promise} */ execute(graphics, type, options, isSilent) { const filterComp = graphics.getComponent(FILTER); if (type === 'mask') { const maskObj = graphics.getObject(options.maskObjId); if (!(maskObj && maskObj.isType('image'))) { return Promise.reject(rejectMessages.invalidParameters); } extend(options, { mask: maskObj }); graphics.remove(options.mask); } if (!this.isRedo) { const prevfilterOption = filterComp.getOptions(type); const undoData = makeUndoData(type, prevfilterOption, options); cachedUndoDataForSilent = this.setUndoData(undoData, cachedUndoDataForSilent, isSilent); } return filterComp.add(type, options); }, /** * @param {Graphics} graphics - Graphics instance * @param {string} type - Filter type * @returns {Promise} */ undo(graphics, type) { const filterComp = graphics.getComponent(FILTER); if (type === 'mask') { const mask = this.undoData.object; graphics.add(mask); graphics.setActiveObject(mask); return filterComp.remove(type); } // options changed case if (this.undoData.options) { return filterComp.add(type, this.undoData.options); } // filter added case return filterComp.remove(type); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/changeIconColor.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, rejectMessages, commandNames } from '@/consts'; const { ICON } = componentNames; const command = { name: commandNames.CHANGE_ICON_COLOR, /** * Change icon color * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @param {string} color - Color for icon * @returns {Promise} */ execute(graphics, id, color) { return new Promise((resolve, reject) => { const iconComp = graphics.getComponent(ICON); const targetObj = graphics.getObject(id); if (!targetObj) { reject(rejectMessages.noObject); } this.undoData.object = targetObj; this.undoData.color = iconComp.getColor(targetObj); iconComp.setColor(color, targetObj); resolve(); }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const iconComp = graphics.getComponent(ICON); const { object: icon, color } = this.undoData; iconComp.setColor(color, icon); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/changeSelection.js ================================================ import commandFactory from '@/factory/command'; import { commandNames } from '@/consts'; import { getCachedUndoDataForDimension } from '@/helper/selectionModifyHelper'; const command = { name: commandNames.CHANGE_SELECTION, execute(graphics, props) { if (this.isRedo) { props.forEach((prop) => { graphics.setObjectProperties(prop.id, prop); }); } else { this.undoData = getCachedUndoDataForDimension(); } return Promise.resolve(); }, undo(graphics) { this.undoData.forEach((datum) => { graphics.setObjectProperties(datum.id, datum); }); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/changeShape.js ================================================ import forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties'; import commandFactory from '@/factory/command'; import { componentNames, rejectMessages, commandNames } from '@/consts'; const { SHAPE } = componentNames; /** * Cached data for undo * @type {Object} */ let cachedUndoDataForSilent = null; /** * Make undoData * @param {object} options - shape options * @param {Component} targetObj - shape component * @returns {object} - undo data */ function makeUndoData(options, targetObj) { const undoData = { object: targetObj, options: {}, }; forEachOwnProperties(options, (value, key) => { undoData.options[key] = targetObj[key]; }); return undoData; } const command = { name: commandNames.CHANGE_SHAPE, /** * Change a shape * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @param {Object} options - Shape options * @param {string} [options.fill] - Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stroke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {number} [options.left] - Shape x position * @param {number} [options.top] - Shape y position * @param {number} [options.isRegular] - Whether resizing shape has 1:1 ratio or not * @param {boolean} isSilent - is silent execution or not * @returns {Promise} */ execute(graphics, id, options, isSilent) { const shapeComp = graphics.getComponent(SHAPE); const targetObj = graphics.getObject(id); if (!targetObj) { return Promise.reject(rejectMessages.noObject); } if (!this.isRedo) { const undoData = makeUndoData(options, targetObj); cachedUndoDataForSilent = this.setUndoData(undoData, cachedUndoDataForSilent, isSilent); } return shapeComp.change(targetObj, options); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const shapeComp = graphics.getComponent(SHAPE); const { object: shape, options } = this.undoData; return shapeComp.change(shape, options); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/changeText.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, rejectMessages, commandNames } from '@/consts'; const { TEXT } = componentNames; const command = { name: commandNames.CHANGE_TEXT, /** * Change a text * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @param {string} text - Changing text * @returns {Promise} */ execute(graphics, id, text) { const textComp = graphics.getComponent(TEXT); const targetObj = graphics.getObject(id); if (!targetObj) { return Promise.reject(rejectMessages.noObject); } this.undoData.object = targetObj; this.undoData.text = textComp.getText(targetObj); return textComp.change(targetObj, text); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const textComp = graphics.getComponent(TEXT); const { object: textObj, text } = this.undoData; return textComp.change(textObj, text); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/changeTextStyle.js ================================================ import forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties'; import commandFactory from '@/factory/command'; import { componentNames, rejectMessages, commandNames } from '@/consts'; const { TEXT } = componentNames; /** * Cached data for undo * @type {Object} */ let cachedUndoDataForSilent = null; /** * Make undoData * @param {object} styles - text styles * @param {Component} targetObj - text component * @returns {object} - undo data */ function makeUndoData(styles, targetObj) { const undoData = { object: targetObj, styles: {}, }; forEachOwnProperties(styles, (value, key) => { const undoValue = targetObj[key]; undoData.styles[key] = undoValue; }); return undoData; } const command = { name: commandNames.CHANGE_TEXT_STYLE, /** * Change text styles * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @param {Object} styles - text styles * @param {string} [styles.fill] Color * @param {string} [styles.fontFamily] Font type for text * @param {number} [styles.fontSize] Size * @param {string} [styles.fontStyle] Type of inclination (normal / italic) * @param {string} [styles.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [styles.textAlign] Type of text align (left / center / right) * @param {string} [styles.textDecoration] Type of line (underline / line-through / overline) * @param {boolean} isSilent - is silent execution or not * @returns {Promise} */ execute(graphics, id, styles, isSilent) { const textComp = graphics.getComponent(TEXT); const targetObj = graphics.getObject(id); if (!targetObj) { return Promise.reject(rejectMessages.noObject); } if (!this.isRedo) { const undoData = makeUndoData(styles, targetObj); cachedUndoDataForSilent = this.setUndoData(undoData, cachedUndoDataForSilent, isSilent); } return textComp.setStyle(targetObj, styles); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const textComp = graphics.getComponent(TEXT); const { object: textObj, styles } = this.undoData; return textComp.setStyle(textObj, styles); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/clearObjects.js ================================================ import commandFactory from '@/factory/command'; import { commandNames } from '@/consts'; const command = { name: commandNames.CLEAR_OBJECTS, /** * Clear all objects without background (main) image * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ execute(graphics) { return new Promise((resolve) => { this.undoData.objects = graphics.removeAll(); resolve(); }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} * @ignore */ undo(graphics) { graphics.add(this.undoData.objects); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/flip.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames } from '@/consts'; const { FLIP } = componentNames; const command = { name: commandNames.FLIP_IMAGE, /** * flip an image * @param {Graphics} graphics - Graphics instance * @param {string} type - 'flipX' or 'flipY' or 'reset' * @returns {Promise} */ execute(graphics, type) { const flipComp = graphics.getComponent(FLIP); this.undoData.setting = flipComp.getCurrentSetting(); return flipComp[type](); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const flipComp = graphics.getComponent(FLIP); return flipComp.set(this.undoData.setting); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/loadImage.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames } from '@/consts'; const { IMAGE_LOADER } = componentNames; const command = { name: commandNames.LOAD_IMAGE, /** * Load a background (main) image * @param {Graphics} graphics - Graphics instance * @param {string} imageName - Image name * @param {string} imgUrl - Image Url * @returns {Promise} */ execute(graphics, imageName, imgUrl) { const loader = graphics.getComponent(IMAGE_LOADER); const prevImage = loader.getCanvasImage(); const prevImageWidth = prevImage ? prevImage.width : 0; const prevImageHeight = prevImage ? prevImage.height : 0; const objects = graphics.removeAll(true).filter((objectItem) => objectItem.type !== 'cropzone'); objects.forEach((objectItem) => { objectItem.evented = true; }); this.undoData = { name: loader.getImageName(), image: prevImage, objects, }; return loader.load(imageName, imgUrl).then((newImage) => ({ oldWidth: prevImageWidth, oldHeight: prevImageHeight, newWidth: newImage.width, newHeight: newImage.height, })); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const loader = graphics.getComponent(IMAGE_LOADER); const { objects, name, image } = this.undoData; graphics.removeAll(true); graphics.add(objects); return loader.load(name, image); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/removeFilter.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames } from '@/consts'; const { FILTER } = componentNames; const command = { name: commandNames.REMOVE_FILTER, /** * Remove a filter from an image * @param {Graphics} graphics - Graphics instance * @param {string} type - Filter type * @returns {Promise} */ execute(graphics, type) { const filterComp = graphics.getComponent(FILTER); this.undoData.options = filterComp.getOptions(type); return filterComp.remove(type); }, /** * @param {Graphics} graphics - Graphics instance * @param {string} type - Filter type * @returns {Promise} */ undo(graphics, type) { const filterComp = graphics.getComponent(FILTER); const { options } = this.undoData; return filterComp.add(type, options); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/removeObject.js ================================================ import commandFactory from '@/factory/command'; import { commandNames, rejectMessages } from '@/consts'; const command = { name: commandNames.REMOVE_OBJECT, /** * Remove an object * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @returns {Promise} */ execute(graphics, id) { return new Promise((resolve, reject) => { this.undoData.objects = graphics.removeObjectById(id); if (this.undoData.objects.length) { resolve(); } else { reject(rejectMessages.noObject); } }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { graphics.add(this.undoData.objects); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/resize.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames } from '@/consts'; const { RESIZE } = componentNames; const command = { name: commandNames.RESIZE_IMAGE, /** * Resize an image * @param {Graphics} graphics - Graphics instance * @param {object} dimensions - Image Dimensions * @returns {Promise} */ execute(graphics, dimensions) { const resizeComp = graphics.getComponent(RESIZE); let originalDimensions = resizeComp.getOriginalDimensions(); if (!originalDimensions) { originalDimensions = resizeComp.getCurrentDimensions(); } this.undoData.dimensions = originalDimensions; return resizeComp.resize(dimensions); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const resizeComp = graphics.getComponent(RESIZE); return resizeComp.resize(this.undoData.dimensions); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/resizeCanvasDimension.js ================================================ import commandFactory from '@/factory/command'; import { commandNames } from '@/consts'; const command = { name: commandNames.RESIZE_CANVAS_DIMENSION, /** * resize the canvas with given dimension * @param {Graphics} graphics - Graphics instance * @param {{width: number, height: number}} dimension - Max width & height * @returns {Promise} */ execute(graphics, dimension) { return new Promise((resolve) => { this.undoData.size = { width: graphics.cssMaxWidth, height: graphics.cssMaxHeight, }; graphics.setCssMaxDimension(dimension); graphics.adjustCanvasDimension(); resolve(); }); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { graphics.setCssMaxDimension(this.undoData.size); graphics.adjustCanvasDimension(); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/rotate.js ================================================ import commandFactory from '@/factory/command'; import { componentNames, commandNames } from '@/consts'; const { ROTATION } = componentNames; /** * Cached data for undo * @type {Object} */ let cachedUndoDataForSilent = null; /** * Make undo data * @param {Component} rotationComp - rotation component * @returns {object} - undodata */ function makeUndoData(rotationComp) { return { angle: rotationComp.getCurrentAngle(), }; } const command = { name: commandNames.ROTATE_IMAGE, /** * Rotate an image * @param {Graphics} graphics - Graphics instance * @param {string} type - 'rotate' or 'setAngle' * @param {number} angle - angle value (degree) * @param {boolean} isSilent - is silent execution or not * @returns {Promise} */ execute(graphics, type, angle, isSilent) { const rotationComp = graphics.getComponent(ROTATION); if (!this.isRedo) { const undoData = makeUndoData(rotationComp); cachedUndoDataForSilent = this.setUndoData(undoData, cachedUndoDataForSilent, isSilent); } return rotationComp[type](angle); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const rotationComp = graphics.getComponent(ROTATION); const [, type, angle] = this.args; if (type === 'setAngle') { return rotationComp[type](this.undoData.angle); } return rotationComp.rotate(-angle); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/setObjectPosition.js ================================================ import commandFactory from '@/factory/command'; import { commandNames, rejectMessages } from '@/consts'; const command = { name: commandNames.SET_OBJECT_POSITION, /** * Set object properties * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @param {Object} posInfo - position object * @param {number} posInfo.x - x position * @param {number} posInfo.y - y position * @param {string} posInfo.originX - can be 'left', 'center', 'right' * @param {string} posInfo.originY - can be 'top', 'center', 'bottom' * @returns {Promise} */ execute(graphics, id, posInfo) { const targetObj = graphics.getObject(id); if (!targetObj) { return Promise.reject(rejectMessages.noObject); } this.undoData.objectId = id; this.undoData.props = graphics.getObjectProperties(id, ['left', 'top']); graphics.setObjectPosition(id, posInfo); graphics.renderAll(); return Promise.resolve(); }, /** * @param {Graphics} graphics - Graphics instance * @returns {Promise} */ undo(graphics) { const { objectId, props } = this.undoData; graphics.setObjectProperties(objectId, props); graphics.renderAll(); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/command/setObjectProperties.js ================================================ import forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties'; import commandFactory from '@/factory/command'; import { commandNames, rejectMessages } from '@/consts'; const command = { name: commandNames.SET_OBJECT_PROPERTIES, /** * Set object properties * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @param {Object} props - properties * @param {string} [props.fill] Color * @param {string} [props.fontFamily] Font type for text * @param {number} [props.fontSize] Size * @param {string} [props.fontStyle] Type of inclination (normal / italic) * @param {string} [props.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [props.textAlign] Type of text align (left / center / right) * @param {string} [props.textDecoration] Type of line (underline / line-through / overline) * @returns {Promise} */ execute(graphics, id, props) { const targetObj = graphics.getObject(id); if (!targetObj) { return Promise.reject(rejectMessages.noObject); } this.undoData.props = {}; forEachOwnProperties(props, (value, key) => { this.undoData.props[key] = targetObj[key]; }); graphics.setObjectProperties(id, props); return Promise.resolve(); }, /** * @param {Graphics} graphics - Graphics instance * @param {number} id - object id * @returns {Promise} */ undo(graphics, id) { const { props } = this.undoData; graphics.setObjectProperties(id, props); return Promise.resolve(); }, }; commandFactory.register(command); export default command; ================================================ FILE: apps/image-editor/src/js/component/cropper.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import Component from '@/interface/component'; import Cropzone from '@/extension/cropzone'; import { keyCodes, componentNames, CROPZONE_DEFAULT_OPTIONS } from '@/consts'; import { clamp, fixFloatingPoint } from '@/util'; const MOUSE_MOVE_THRESHOLD = 10; const DEFAULT_OPTION = { presetRatio: null, top: -10, left: -10, height: 1, width: 1, }; /** * Cropper components * @param {Graphics} graphics - Graphics instance * @extends {Component} * @class Cropper * @ignore */ class Cropper extends Component { constructor(graphics) { super(componentNames.CROPPER, graphics); /** * Cropzone * @type {Cropzone} * @private */ this._cropzone = null; /** * StartX of Cropzone * @type {number} * @private */ this._startX = null; /** * StartY of Cropzone * @type {number} * @private */ this._startY = null; /** * State whether shortcut key is pressed or not * @type {boolean} * @private */ this._withShiftKey = false; /** * Listeners * @type {object.} * @private */ this._listeners = { keydown: this._onKeyDown.bind(this), keyup: this._onKeyUp.bind(this), mousedown: this._onFabricMouseDown.bind(this), mousemove: this._onFabricMouseMove.bind(this), mouseup: this._onFabricMouseUp.bind(this), }; } /** * Start cropping */ start() { if (this._cropzone) { return; } const canvas = this.getCanvas(); canvas.forEachObject((obj) => { // {@link http://fabricjs.com/docs/fabric.Object.html#evented} obj.evented = false; }); this._cropzone = new Cropzone( canvas, extend( { left: 0, top: 0, width: 0.5, height: 0.5, strokeWidth: 0, // {@link https://github.com/kangax/fabric.js/issues/2860} cornerSize: 10, cornerColor: 'black', fill: 'transparent', }, CROPZONE_DEFAULT_OPTIONS, this.graphics.cropSelectionStyle ) ); canvas.discardActiveObject(); canvas.add(this._cropzone); canvas.on('mouse:down', this._listeners.mousedown); canvas.selection = false; canvas.defaultCursor = 'crosshair'; fabric.util.addListener(document, 'keydown', this._listeners.keydown); fabric.util.addListener(document, 'keyup', this._listeners.keyup); } /** * End cropping */ end() { const canvas = this.getCanvas(); const cropzone = this._cropzone; if (!cropzone) { return; } canvas.remove(cropzone); canvas.selection = true; canvas.defaultCursor = 'default'; canvas.off('mouse:down', this._listeners.mousedown); canvas.forEachObject((obj) => { obj.evented = true; }); this._cropzone = null; fabric.util.removeListener(document, 'keydown', this._listeners.keydown); fabric.util.removeListener(document, 'keyup', this._listeners.keyup); } /** * Change cropzone visible * @param {boolean} visible - cropzone visible state */ changeVisibility(visible) { if (this._cropzone) { this._cropzone.set({ visible }); } } /** * onMousedown handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onFabricMouseDown(fEvent) { const canvas = this.getCanvas(); if (fEvent.target) { return; } canvas.selection = false; const coord = canvas.getPointer(fEvent.e); this._startX = coord.x; this._startY = coord.y; canvas.on({ 'mouse:move': this._listeners.mousemove, 'mouse:up': this._listeners.mouseup, }); } /** * onMousemove handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onFabricMouseMove(fEvent) { const canvas = this.getCanvas(); const pointer = canvas.getPointer(fEvent.e); const { x, y } = pointer; const cropzone = this._cropzone; if (Math.abs(x - this._startX) + Math.abs(y - this._startY) > MOUSE_MOVE_THRESHOLD) { canvas.remove(cropzone); cropzone.set(this._calcRectDimensionFromPoint(x, y, cropzone.presetRatio)); canvas.add(cropzone); canvas.setActiveObject(cropzone); } } /** * Get rect dimension setting from Canvas-Mouse-Position(x, y) * @param {number} x - Canvas-Mouse-Position x * @param {number} y - Canvas-Mouse-Position Y * @param {number|null} presetRatio - fixed aspect ratio (width/height) of the cropzone (null if not set) * @returns {{left: number, top: number, width: number, height: number}} * @private */ _calcRectDimensionFromPoint(x, y, presetRatio = null) { const canvas = this.getCanvas(); const canvasWidth = canvas.getWidth(); const canvasHeight = canvas.getHeight(); const startX = this._startX; const startY = this._startY; let left = clamp(x, 0, startX); let top = clamp(y, 0, startY); let width = clamp(x, startX, canvasWidth) - left; // (startX <= x(mouse) <= canvasWidth) - left let height = clamp(y, startY, canvasHeight) - top; // (startY <= y(mouse) <= canvasHeight) - top if (this._withShiftKey && !presetRatio) { // make fixed ratio cropzone if (width > height) { height = width; } else if (height > width) { width = height; } if (startX >= x) { left = startX - width; } if (startY >= y) { top = startY - height; } } else if (presetRatio) { // Restrict cropzone to given presetRatio height = width / presetRatio; // If moving in a direction where the top left corner moves (ie. top-left, bottom-left, top-right) // the left and/or top values has to be changed based on the new height/width if (startX >= x) { left = clamp(startX - width, 0, canvasWidth); } if (startY >= y) { top = clamp(startY - height, 0, canvasHeight); } // Check if the new height is too large if (top + height > canvasHeight) { height = canvasHeight - top; // Set height to max available height width = height * presetRatio; // Restrict cropzone to given presetRatio based on the new height // If moving in a direction where the top left corner moves (ie. top-left, bottom-left, top-right) // the left and/or top values has to be changed based on the new height/width if (startX >= x) { left = clamp(startX - width, 0, canvasWidth); } if (startY >= y) { top = clamp(startY - height, 0, canvasHeight); } } } return { left, top, width, height, }; } /** * onMouseup handler in fabric canvas * @private */ _onFabricMouseUp() { const cropzone = this._cropzone; const listeners = this._listeners; const canvas = this.getCanvas(); canvas.setActiveObject(cropzone); canvas.off({ 'mouse:move': listeners.mousemove, 'mouse:up': listeners.mouseup, }); } /** * Get cropped image data * @param {Object} cropRect cropzone rect * @param {Number} cropRect.left left position * @param {Number} cropRect.top top position * @param {Number} cropRect.width width * @param {Number} cropRect.height height * @returns {?{imageName: string, url: string}} cropped Image data */ getCroppedImageData(cropRect) { const canvas = this.getCanvas(); const containsCropzone = canvas.contains(this._cropzone); if (!cropRect) { return null; } if (containsCropzone) { canvas.remove(this._cropzone); } const imageData = { imageName: this.getImageName(), url: canvas.toDataURL(cropRect), }; if (containsCropzone) { canvas.add(this._cropzone); } return imageData; } /** * Get cropped rect * @returns {Object} rect */ getCropzoneRect() { const cropzone = this._cropzone; if (!cropzone.isValid()) { return null; } return { left: cropzone.left, top: cropzone.top, width: cropzone.width, height: cropzone.height, }; } /** * Set a cropzone square * @param {number} [presetRatio] - preset ratio */ setCropzoneRect(presetRatio) { const canvas = this.getCanvas(); const cropzone = this._cropzone; canvas.discardActiveObject(); canvas.selection = false; canvas.remove(cropzone); cropzone.set(presetRatio ? this._getPresetPropertiesForCropSize(presetRatio) : DEFAULT_OPTION); canvas.add(cropzone); canvas.selection = true; if (presetRatio) { canvas.setActiveObject(cropzone); } } /** * get a cropzone square info * @param {number} presetRatio - preset ratio * @returns {{presetRatio: number, left: number, top: number, width: number, height: number}} * @private */ _getPresetPropertiesForCropSize(presetRatio) { const canvas = this.getCanvas(); const originalWidth = canvas.getWidth(); const originalHeight = canvas.getHeight(); const standardSize = originalWidth >= originalHeight ? originalWidth : originalHeight; const getScale = (value, orignalValue) => (value > orignalValue ? orignalValue / value : 1); let width = standardSize * presetRatio; let height = standardSize; const scaleWidth = getScale(width, originalWidth); [width, height] = [width, height].map((sizeValue) => sizeValue * scaleWidth); const scaleHeight = getScale(height, originalHeight); [width, height] = [width, height].map((sizeValue) => fixFloatingPoint(sizeValue * scaleHeight)); return { presetRatio, top: (originalHeight - height) / 2, left: (originalWidth - width) / 2, width, height, }; } /** * Keydown event handler * @param {KeyboardEvent} e - Event object * @private */ _onKeyDown(e) { if (e.keyCode === keyCodes.SHIFT) { this._withShiftKey = true; } } /** * Keyup event handler * @param {KeyboardEvent} e - Event object * @private */ _onKeyUp(e) { if (e.keyCode === keyCodes.SHIFT) { this._withShiftKey = false; } } } export default Cropper; ================================================ FILE: apps/image-editor/src/js/component/filter.js ================================================ import isUndefined from 'tui-code-snippet/type/isUndefined'; import extend from 'tui-code-snippet/object/extend'; import forEach from 'tui-code-snippet/collection/forEach'; import { fabric } from 'fabric'; import Component from '@/interface/component'; import { rejectMessages, componentNames } from '@/consts'; import Mask from '@/extension/mask'; import Sharpen from '@/extension/sharpen'; import Emboss from '@/extension/emboss'; import ColorFilter from '@/extension/colorFilter'; const { filters } = fabric.Image; filters.Mask = Mask; filters.Sharpen = Sharpen; filters.Emboss = Emboss; filters.ColorFilter = ColorFilter; /** * Filter * @class Filter * @param {Graphics} graphics - Graphics instance * @extends {Component} * @ignore */ class Filter extends Component { constructor(graphics) { super(componentNames.FILTER, graphics); } /** * Add filter to source image (a specific filter is added on fabric.js) * @param {string} type - Filter type * @param {Object} [options] - Options of filter * @returns {Promise} */ add(type, options) { return new Promise((resolve, reject) => { const sourceImg = this._getSourceImage(); const canvas = this.getCanvas(); let imgFilter = this._getFilter(sourceImg, type); if (!imgFilter) { imgFilter = this._createFilter(sourceImg, type, options); } if (!imgFilter) { reject(rejectMessages.invalidParameters); } this._changeFilterValues(imgFilter, options); this._apply(sourceImg, () => { canvas.renderAll(); resolve({ type, action: 'add', options, }); }); }); } /** * Remove filter to source image * @param {string} type - Filter type * @returns {Promise} */ remove(type) { return new Promise((resolve, reject) => { const sourceImg = this._getSourceImage(); const canvas = this.getCanvas(); const options = this.getOptions(type); if (!sourceImg.filters.length) { reject(rejectMessages.unsupportedOperation); } this._removeFilter(sourceImg, type); this._apply(sourceImg, () => { canvas.renderAll(); resolve({ type, action: 'remove', options, }); }); }); } /** * Whether this has the filter or not * @param {string} type - Filter type * @returns {boolean} true if it has the filter */ hasFilter(type) { return !!this._getFilter(this._getSourceImage(), type); } /** * Get a filter options * @param {string} type - Filter type * @returns {Object} filter options or null if there is no that filter */ getOptions(type) { const sourceImg = this._getSourceImage(); const imgFilter = this._getFilter(sourceImg, type); if (!imgFilter) { return null; } return extend({}, imgFilter.options); } /** * Change filter values * @param {Object} imgFilter object of filter * @param {Object} options object * @private */ _changeFilterValues(imgFilter, options) { forEach(options, (value, key) => { if (!isUndefined(imgFilter[key])) { imgFilter[key] = value; } }); forEach(imgFilter.options, (value, key) => { if (!isUndefined(options[key])) { imgFilter.options[key] = options[key]; } }); } /** * Apply filter * @param {fabric.Image} sourceImg - Source image to apply filter * @param {function} callback - Executed function after applying filter * @private */ _apply(sourceImg, callback) { sourceImg.filters.push(); const result = sourceImg.applyFilters(); if (result) { callback(); } } /** * Get source image on canvas * @returns {fabric.Image} Current source image on canvas * @private */ _getSourceImage() { return this.getCanvasImage(); } /** * Create filter instance * @param {fabric.Image} sourceImg - Source image to apply filter * @param {string} type - Filter type * @param {Object} [options] - Options of filter * @returns {Object} Fabric object of filter * @private */ _createFilter(sourceImg, type, options) { let filterObj; // capitalize first letter for matching with fabric image filter name const fabricType = this._getFabricFilterType(type); const ImageFilter = fabric.Image.filters[fabricType]; if (ImageFilter) { filterObj = new ImageFilter(options); filterObj.options = options; sourceImg.filters.push(filterObj); } return filterObj; } /** * Get applied filter instance * @param {fabric.Image} sourceImg - Source image to apply filter * @param {string} type - Filter type * @returns {Object} Fabric object of filter * @private */ _getFilter(sourceImg, type) { let imgFilter = null; if (sourceImg) { const fabricType = this._getFabricFilterType(type); const { length } = sourceImg.filters; let item, i; for (i = 0; i < length; i += 1) { item = sourceImg.filters[i]; if (item.type === fabricType) { imgFilter = item; break; } } } return imgFilter; } /** * Remove applied filter instance * @param {fabric.Image} sourceImg - Source image to apply filter * @param {string} type - Filter type * @private */ _removeFilter(sourceImg, type) { const fabricType = this._getFabricFilterType(type); sourceImg.filters = sourceImg.filters.filter((value) => value.type !== fabricType); } /** * Change filter class name to fabric's, especially capitalizing first letter * @param {string} type - Filter type * @example * 'grayscale' -> 'Grayscale' * @returns {string} Fabric filter class name */ _getFabricFilterType(type) { return type.charAt(0).toUpperCase() + type.slice(1); } } export default Filter; ================================================ FILE: apps/image-editor/src/js/component/flip.js ================================================ import extend from 'tui-code-snippet/object/extend'; import Component from '@/interface/component'; import { componentNames, rejectMessages } from '@/consts'; /** * Flip * @class Flip * @param {Graphics} graphics - Graphics instance * @extends {Component} * @ignore */ class Flip extends Component { constructor(graphics) { super(componentNames.FLIP, graphics); } /** * Get current flip settings * @returns {{flipX: Boolean, flipY: Boolean}} */ getCurrentSetting() { const canvasImage = this.getCanvasImage(); return { flipX: canvasImage.flipX, flipY: canvasImage.flipY, }; } /** * Set flipX, flipY * @param {{flipX: Boolean, flipY: Boolean}} newSetting - Flip setting * @returns {Promise} */ set(newSetting) { const setting = this.getCurrentSetting(); const isChangingFlipX = setting.flipX !== newSetting.flipX; const isChangingFlipY = setting.flipY !== newSetting.flipY; if (!isChangingFlipX && !isChangingFlipY) { return Promise.reject(rejectMessages.flip); } extend(setting, newSetting); this.setImageProperties(setting, true); this._invertAngle(isChangingFlipX, isChangingFlipY); this._flipObjects(isChangingFlipX, isChangingFlipY); return Promise.resolve({ flipX: setting.flipX, flipY: setting.flipY, angle: this.getCanvasImage().angle, }); } /** * Invert image angle for flip * @param {boolean} isChangingFlipX - Change flipX * @param {boolean} isChangingFlipY - Change flipY */ _invertAngle(isChangingFlipX, isChangingFlipY) { const canvasImage = this.getCanvasImage(); let { angle } = canvasImage; if (isChangingFlipX) { angle *= -1; } if (isChangingFlipY) { angle *= -1; } canvasImage.rotate(parseFloat(angle)).setCoords(); // parseFloat for -0 to 0 } /** * Flip objects * @param {boolean} isChangingFlipX - Change flipX * @param {boolean} isChangingFlipY - Change flipY * @private */ _flipObjects(isChangingFlipX, isChangingFlipY) { const canvas = this.getCanvas(); if (isChangingFlipX) { canvas.forEachObject((obj) => { obj .set({ angle: parseFloat(obj.angle * -1), // parseFloat for -0 to 0 flipX: !obj.flipX, left: canvas.width - obj.left, }) .setCoords(); }); } if (isChangingFlipY) { canvas.forEachObject((obj) => { obj .set({ angle: parseFloat(obj.angle * -1), // parseFloat for -0 to 0 flipY: !obj.flipY, top: canvas.height - obj.top, }) .setCoords(); }); } canvas.renderAll(); } /** * Reset flip settings * @returns {Promise} */ reset() { return this.set({ flipX: false, flipY: false, }); } /** * Flip x * @returns {Promise} */ flipX() { const current = this.getCurrentSetting(); return this.set({ flipX: !current.flipX, flipY: current.flipY, }); } /** * Flip y * @returns {Promise} */ flipY() { const current = this.getCurrentSetting(); return this.set({ flipX: current.flipX, flipY: !current.flipY, }); } } export default Flip; ================================================ FILE: apps/image-editor/src/js/component/freeDrawing.js ================================================ import { fabric } from 'fabric'; import Component from '@/interface/component'; import { componentNames } from '@/consts'; /** * FreeDrawing * @class FreeDrawing * @param {Graphics} graphics - Graphics instance * @extends {Component} * @ignore */ class FreeDrawing extends Component { constructor(graphics) { super(componentNames.FREE_DRAWING, graphics); /** * Brush width * @type {number} */ this.width = 12; /** * fabric.Color instance for brush color * @type {fabric.Color} */ this.oColor = new fabric.Color('rgba(0, 0, 0, 0.5)'); } /** * Start free drawing mode * @param {{width: ?number, color: ?string}} [setting] - Brush width & color */ start(setting) { const canvas = this.getCanvas(); canvas.isDrawingMode = true; this.setBrush(setting); } /** * Set brush * @param {{width: ?number, color: ?string}} [setting] - Brush width & color */ setBrush(setting) { const brush = this.getCanvas().freeDrawingBrush; setting = setting || {}; this.width = setting.width || this.width; if (setting.color) { this.oColor = new fabric.Color(setting.color); } brush.width = this.width; brush.color = this.oColor.toRgba(); } /** * End free drawing mode */ end() { const canvas = this.getCanvas(); canvas.isDrawingMode = false; } } export default FreeDrawing; ================================================ FILE: apps/image-editor/src/js/component/icon.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import forEach from 'tui-code-snippet/collection/forEach'; import Component from '@/interface/component'; import { eventNames as events, rejectMessages, componentNames, fObjectOptions } from '@/consts'; const pathMap = { arrow: 'M 0 90 H 105 V 120 L 160 60 L 105 0 V 30 H 0 Z', cancel: 'M 0 30 L 30 60 L 0 90 L 30 120 L 60 90 L 90 120 L 120 90 ' + 'L 90 60 L 120 30 L 90 0 L 60 30 L 30 0 Z', }; /** * Icon * @class Icon * @param {Graphics} graphics - Graphics instance * @extends {Component} * @ignore */ class Icon extends Component { constructor(graphics) { super(componentNames.ICON, graphics); /** * Default icon color * @type {string} */ this._oColor = '#000000'; /** * Path value of each icon type * @type {Object} */ this._pathMap = pathMap; /** * Type of the drawing icon * @type {string} * @private */ this._type = null; /** * Color of the drawing icon * @type {string} * @private */ this._iconColor = null; /** * Event handler list * @type {Object} * @private */ this._handlers = { mousedown: this._onFabricMouseDown.bind(this), mousemove: this._onFabricMouseMove.bind(this), mouseup: this._onFabricMouseUp.bind(this), }; } /** * Set states of the current drawing shape * @ignore * @param {string} type - Icon type ('arrow', 'cancel', custom icon name) * @param {string} iconColor - Icon foreground color */ setStates(type, iconColor) { this._type = type; this._iconColor = iconColor; } /** * Start to draw the icon on canvas * @ignore */ start() { const canvas = this.getCanvas(); canvas.selection = false; canvas.on('mouse:down', this._handlers.mousedown); } /** * End to draw the icon on canvas * @ignore */ end() { const canvas = this.getCanvas(); canvas.selection = true; canvas.off({ 'mouse:down': this._handlers.mousedown, }); } /** * Add icon * @param {string} type - Icon type * @param {Object} options - Icon options * @param {string} [options.fill] - Icon foreground color * @param {string} [options.left] - Icon x position * @param {string} [options.top] - Icon y position * @returns {Promise} */ add(type, options) { return new Promise((resolve, reject) => { const canvas = this.getCanvas(); const path = this._pathMap[type]; const selectionStyle = fObjectOptions.SELECTION_STYLE; const icon = path ? this._createIcon(path) : null; this._icon = icon; if (!icon) { reject(rejectMessages.invalidParameters); } icon.set( extend( { type: 'icon', fill: this._oColor, }, selectionStyle, options, this.graphics.controlStyle ) ); canvas.add(icon).setActiveObject(icon); resolve(this.graphics.createObjectProperties(icon)); }); } /** * Register icon paths * @param {{key: string, value: string}} pathInfos - Path infos */ registerPaths(pathInfos) { forEach( pathInfos, (path, type) => { this._pathMap[type] = path; }, this ); } /** * Set icon object color * @param {string} color - Color to set * @param {fabric.Path}[obj] - Current activated path object */ setColor(color, obj) { this._oColor = color; if (obj && obj.get('type') === 'icon') { obj.set({ fill: this._oColor }); this.getCanvas().renderAll(); } } /** * Get icon color * @param {fabric.Path}[obj] - Current activated path object * @returns {string} color */ getColor(obj) { return obj.fill; } /** * Create icon object * @param {string} path - Path value to create icon * @returns {fabric.Path} Path object */ _createIcon(path) { return new fabric.Path(path); } /** * MouseDown event handler on canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object * @private */ _onFabricMouseDown(fEvent) { const canvas = this.getCanvas(); this._startPoint = canvas.getPointer(fEvent.e); const { x: left, y: top } = this._startPoint; this.add(this._type, { left, top, fill: this._iconColor, }).then(() => { this.fire(events.ADD_OBJECT, this.graphics.createObjectProperties(this._icon)); canvas.on('mouse:move', this._handlers.mousemove); canvas.on('mouse:up', this._handlers.mouseup); }); } /** * MouseMove event handler on canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object * @private */ _onFabricMouseMove(fEvent) { const canvas = this.getCanvas(); if (!this._icon) { return; } const moveOriginPointer = canvas.getPointer(fEvent.e); const scaleX = (moveOriginPointer.x - this._startPoint.x) / this._icon.width; const scaleY = (moveOriginPointer.y - this._startPoint.y) / this._icon.height; this._icon.set({ scaleX: Math.abs(scaleX * 2), scaleY: Math.abs(scaleY * 2), }); this._icon.setCoords(); canvas.renderAll(); } /** * MouseUp event handler on canvas * @private */ _onFabricMouseUp() { const canvas = this.getCanvas(); this.fire(events.OBJECT_ADDED, this.graphics.createObjectProperties(this._icon)); this._icon = null; canvas.off('mouse:down', this._handlers.mousedown); canvas.off('mouse:move', this._handlers.mousemove); canvas.off('mouse:up', this._handlers.mouseup); } } export default Icon; ================================================ FILE: apps/image-editor/src/js/component/imageLoader.js ================================================ import Component from '@/interface/component'; import { componentNames, rejectMessages } from '@/consts'; const imageOption = { padding: 0, crossOrigin: 'Anonymous', }; /** * ImageLoader components * @extends {Component} * @class ImageLoader * @param {Graphics} graphics - Graphics instance * @ignore */ class ImageLoader extends Component { constructor(graphics) { super(componentNames.IMAGE_LOADER, graphics); } /** * Load image from url * @param {?string} imageName - File name * @param {?(fabric.Image|string)} img - fabric.Image instance or URL of an image * @returns {Promise} */ load(imageName, img) { let promise; if (!imageName && !img) { // Back to the initial state, not error. const canvas = this.getCanvas(); canvas.backgroundImage = null; canvas.renderAll(); promise = new Promise((resolve) => { this.setCanvasImage('', null); resolve(); }); } else { promise = this._setBackgroundImage(img).then((oImage) => { this.setCanvasImage(imageName, oImage); this.adjustCanvasDimension(); return oImage; }); } return promise; } /** * Set background image * @param {?(fabric.Image|String)} img fabric.Image instance or URL of an image to set background to * @returns {Promise} * @private */ _setBackgroundImage(img) { if (!img) { return Promise.reject(rejectMessages.loadImage); } return new Promise((resolve, reject) => { const canvas = this.getCanvas(); canvas.setBackgroundImage( img, () => { const oImage = canvas.backgroundImage; if (oImage && oImage.getElement()) { resolve(oImage); } else { reject(rejectMessages.loadingImageFailed); } }, imageOption ); }); } } export default ImageLoader; ================================================ FILE: apps/image-editor/src/js/component/line.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import Component from '@/interface/component'; import ArrowLine from '@/extension/arrowLine'; import { eventNames, componentNames, fObjectOptions } from '@/consts'; /** * Line * @class Line * @param {Graphics} graphics - Graphics instance * @extends {Component} * @ignore */ class Line extends Component { constructor(graphics) { super(componentNames.LINE, graphics); /** * Brush width * @type {number} * @private */ this._width = 12; /** * fabric.Color instance for brush color * @type {fabric.Color} * @private */ this._oColor = new fabric.Color('rgba(0, 0, 0, 0.5)'); /** * Listeners * @type {object.} * @private */ this._listeners = { mousedown: this._onFabricMouseDown.bind(this), mousemove: this._onFabricMouseMove.bind(this), mouseup: this._onFabricMouseUp.bind(this), }; } /** * Start drawing line mode * @param {{width: ?number, color: ?string}} [setting] - Brush width & color */ setHeadOption(setting) { const { arrowType = { head: null, tail: null, }, } = setting; this._arrowType = arrowType; } /** * Start drawing line mode * @param {{width: ?number, color: ?string}} [setting] - Brush width & color */ start(setting = {}) { const canvas = this.getCanvas(); canvas.defaultCursor = 'crosshair'; canvas.selection = false; this.setHeadOption(setting); this.setBrush(setting); canvas.forEachObject((obj) => { obj.set({ evented: false, }); }); canvas.on({ 'mouse:down': this._listeners.mousedown, }); } /** * Set brush * @param {{width: ?number, color: ?string}} [setting] - Brush width & color */ setBrush(setting) { const brush = this.getCanvas().freeDrawingBrush; setting = setting || {}; this._width = setting.width || this._width; if (setting.color) { this._oColor = new fabric.Color(setting.color); } brush.width = this._width; brush.color = this._oColor.toRgba(); } /** * End drawing line mode */ end() { const canvas = this.getCanvas(); canvas.defaultCursor = 'default'; canvas.selection = true; canvas.forEachObject((obj) => { obj.set({ evented: true, }); }); canvas.off('mouse:down', this._listeners.mousedown); } /** * Mousedown event handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object * @private */ _onFabricMouseDown(fEvent) { const canvas = this.getCanvas(); const { x, y } = canvas.getPointer(fEvent.e); const points = [x, y, x, y]; this._line = new ArrowLine(points, { stroke: this._oColor.toRgba(), strokeWidth: this._width, arrowType: this._arrowType, evented: false, }); this._line.set(fObjectOptions.SELECTION_STYLE); canvas.add(this._line); canvas.on({ 'mouse:move': this._listeners.mousemove, 'mouse:up': this._listeners.mouseup, }); this.fire(eventNames.ADD_OBJECT, this._createLineEventObjectProperties()); } /** * Mousemove event handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object * @private */ _onFabricMouseMove(fEvent) { const canvas = this.getCanvas(); const pointer = canvas.getPointer(fEvent.e); this._line.set({ x2: pointer.x, y2: pointer.y, }); this._line.setCoords(); canvas.renderAll(); } /** * Mouseup event handler in fabric canvas * @private */ _onFabricMouseUp() { const canvas = this.getCanvas(); this.fire(eventNames.OBJECT_ADDED, this._createLineEventObjectProperties()); this._line = null; canvas.off({ 'mouse:move': this._listeners.mousemove, 'mouse:up': this._listeners.mouseup, }); } /** * create line event object properties * @returns {Object} properties line object * @private */ _createLineEventObjectProperties() { const params = this.graphics.createObjectProperties(this._line); const { x1, x2, y1, y2 } = this._line; return extend({}, params, { startPosition: { x: x1, y: y1, }, endPosition: { x: x2, y: y2, }, }); } } export default Line; ================================================ FILE: apps/image-editor/src/js/component/resize.js ================================================ import Component from '@/interface/component'; import { componentNames } from '@/consts'; /** * Resize components * @param {Graphics} graphics - Graphics instance * @extends {Component} * @class Resize * @ignore */ class Resize extends Component { constructor(graphics) { super(componentNames.RESIZE, graphics); /** * Current dimensions * @type {Object} * @private */ this._dimensions = null; /** * Original dimensions * @type {Object} * @private */ this._originalDimensions = null; } /** * Get current dimensions * @returns {object} */ getCurrentDimensions() { const canvasImage = this.getCanvasImage(); if (!this._dimensions && canvasImage) { const { width, height } = canvasImage; this._dimensions = { width, height }; } return this._dimensions; } /** * Get original dimensions * @returns {object} */ getOriginalDimensions() { return this._originalDimensions; } /** * Set original dimensions * @param {object} dimensions - Dimensions */ setOriginalDimensions(dimensions) { this._originalDimensions = dimensions; } /** * Resize Image * @param {Object} dimensions - Resize dimensions * @returns {Promise} */ resize(dimensions) { const canvasImage = this.getCanvasImage(); const { width, height, scaleX, scaleY } = canvasImage; const { width: dimensionsWidth, height: dimensionsHeight } = dimensions; const scaleValues = { scaleX: dimensionsWidth ? dimensionsWidth / width : scaleX, scaleY: dimensionsHeight ? dimensionsHeight / height : scaleY, }; if (scaleX !== scaleValues.scaleX || scaleY !== scaleValues.scaleY) { canvasImage.set(scaleValues).setCoords(); this._dimensions = { width: canvasImage.width * canvasImage.scaleX, height: canvasImage.height * canvasImage.scaleY, }; } this.adjustCanvasDimensionBase(); return Promise.resolve(); } /** * Start resizing */ start() { const dimensions = this.getCurrentDimensions(); this.setOriginalDimensions(dimensions); } /** * End resizing */ end() {} } export default Resize; ================================================ FILE: apps/image-editor/src/js/component/rotation.js ================================================ import { fabric } from 'fabric'; import Component from '@/interface/component'; import { componentNames } from '@/consts'; /** * Image Rotation component * @class Rotation * @extends {Component} * @param {Graphics} graphics - Graphics instance * @ignore */ class Rotation extends Component { constructor(graphics) { super(componentNames.ROTATION, graphics); } /** * Get current angle * @returns {Number} */ getCurrentAngle() { return this.getCanvasImage().angle; } /** * Set angle of the image * * Do not call "this.setImageProperties" for setting angle directly. * Before setting angle, The originX,Y of image should be set to center. * See "http://fabricjs.com/docs/fabric.Object.html#setAngle" * * @param {number} angle - Angle value * @returns {Promise} */ setAngle(angle) { const oldAngle = this.getCurrentAngle() % 360; // The angle is lower than 2*PI(===360 degrees) angle %= 360; const canvasImage = this.getCanvasImage(); const oldImageCenter = canvasImage.getCenterPoint(); canvasImage.set({ angle }).setCoords(); this.adjustCanvasDimension(); const newImageCenter = canvasImage.getCenterPoint(); this._rotateForEachObject(oldImageCenter, newImageCenter, angle - oldAngle); return Promise.resolve(angle); } /** * Rotate for each object * @param {fabric.Point} oldImageCenter - Image center point before rotation * @param {fabric.Point} newImageCenter - Image center point after rotation * @param {number} angleDiff - Image angle difference after rotation * @private */ _rotateForEachObject(oldImageCenter, newImageCenter, angleDiff) { const canvas = this.getCanvas(); const centerDiff = { x: oldImageCenter.x - newImageCenter.x, y: oldImageCenter.y - newImageCenter.y, }; canvas.forEachObject((obj) => { const objCenter = obj.getCenterPoint(); const radian = fabric.util.degreesToRadians(angleDiff); const newObjCenter = fabric.util.rotatePoint(objCenter, oldImageCenter, radian); obj.set({ left: newObjCenter.x - centerDiff.x, top: newObjCenter.y - centerDiff.y, angle: (obj.angle + angleDiff) % 360, }); obj.setCoords(); }); canvas.renderAll(); } /** * Rotate the image * @param {number} additionalAngle - Additional angle * @returns {Promise} */ rotate(additionalAngle) { const current = this.getCurrentAngle(); return this.setAngle(current + additionalAngle); } } export default Rotation; ================================================ FILE: apps/image-editor/src/js/component/shape.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import Component from '@/interface/component'; import resizeHelper from '@/helper/shapeResizeHelper'; import { getFillImageFromShape, rePositionFilterTypeFillImage, reMakePatternImageSource, makeFillPatternForFilter, makeFilterOptionFromFabricImage, resetFillPatternCanvas, } from '@/helper/shapeFilterFillHelper'; import { changeOrigin, getCustomProperty, getFillTypeFromOption, getFillTypeFromObject, isShape, } from '@/util'; import { rejectMessages, eventNames, keyCodes as KEY_CODES, componentNames, fObjectOptions, SHAPE_DEFAULT_OPTIONS, SHAPE_FILL_TYPE, } from '@/consts'; const SHAPE_INIT_OPTIONS = extend( { strokeWidth: 1, stroke: '#000000', fill: '#ffffff', width: 1, height: 1, rx: 0, ry: 0, }, SHAPE_DEFAULT_OPTIONS ); const DEFAULT_TYPE = 'rect'; const DEFAULT_WIDTH = 20; const DEFAULT_HEIGHT = 20; /** * Make fill option * @param {Object} options - Options to create the shape * @param {Object.Image} canvasImage - canvas background image * @param {Function} createStaticCanvas - static canvas creater * @returns {Object} - shape option * @private */ function makeFabricFillOption(options, canvasImage, createStaticCanvas) { const fillOption = options.fill; const fillType = getFillTypeFromOption(options.fill); let fill = fillOption; if (fillOption.color) { fill = fillOption.color; } let extOption = null; if (fillType === 'filter') { const newStaticCanvas = createStaticCanvas(); extOption = makeFillPatternForFilter(canvasImage, fillOption.filter, newStaticCanvas); } else { extOption = { fill }; } return extend({}, options, extOption); } /** * Shape * @class Shape * @param {Graphics} graphics - Graphics instance * @extends {Component} * @ignore */ export default class Shape extends Component { constructor(graphics) { super(componentNames.SHAPE, graphics); /** * Object of The drawing shape * @type {fabric.Object} * @private */ this._shapeObj = null; /** * Type of the drawing shape * @type {string} * @private */ this._type = DEFAULT_TYPE; /** * Options to draw the shape * @type {Object} * @private */ this._options = extend({}, SHAPE_INIT_OPTIONS); /** * Whether the shape object is selected or not * @type {boolean} * @private */ this._isSelected = false; /** * Pointer for drawing shape (x, y) * @type {Object} * @private */ this._startPoint = {}; /** * Using shortcut on drawing shape * @type {boolean} * @private */ this._withShiftKey = false; /** * Event handler list * @type {Object} * @private */ this._handlers = { mousedown: this._onFabricMouseDown.bind(this), mousemove: this._onFabricMouseMove.bind(this), mouseup: this._onFabricMouseUp.bind(this), keydown: this._onKeyDown.bind(this), keyup: this._onKeyUp.bind(this), }; } /** * Start to draw the shape on canvas * @ignore */ start() { const canvas = this.getCanvas(); this._isSelected = false; canvas.defaultCursor = 'crosshair'; canvas.selection = false; canvas.uniformScaling = true; canvas.on({ 'mouse:down': this._handlers.mousedown, }); fabric.util.addListener(document, 'keydown', this._handlers.keydown); fabric.util.addListener(document, 'keyup', this._handlers.keyup); } /** * End to draw the shape on canvas * @ignore */ end() { const canvas = this.getCanvas(); this._isSelected = false; canvas.defaultCursor = 'default'; canvas.selection = true; canvas.uniformScaling = false; canvas.off({ 'mouse:down': this._handlers.mousedown, }); fabric.util.removeListener(document, 'keydown', this._handlers.keydown); fabric.util.removeListener(document, 'keyup', this._handlers.keyup); } /** * Set states of the current drawing shape * @ignore * @param {string} type - Shape type (ex: 'rect', 'circle') * @param {Object} [options] - Shape options * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or * Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stoke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) */ setStates(type, options) { this._type = type; if (options) { this._options = extend(this._options, options); } } /** * Add the shape * @ignore * @param {string} type - Shape type (ex: 'rect', 'circle') * @param {Object} options - Shape options * @param {(ShapeFillOption | string)} [options.fill] - ShapeFillOption or Shape foreground color (ex: '#fff', 'transparent') or ShapeFillOption object * @param {string} [options.stroke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {number} [options.isRegular] - Whether scaling shape has 1:1 ratio or not * @returns {Promise} */ add(type, options) { return new Promise((resolve) => { const canvas = this.getCanvas(); const extendOption = this._extendOptions(options); const shapeObj = this._createInstance(type, extendOption); const objectProperties = this.graphics.createObjectProperties(shapeObj); this._bindEventOnShape(shapeObj); canvas.add(shapeObj).setActiveObject(shapeObj); this._resetPositionFillFilter(shapeObj); resolve(objectProperties); }); } /** * Change the shape * @ignore * @param {fabric.Object} shapeObj - Selected shape object on canvas * @param {Object} options - Shape options * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or * Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stroke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {number} [options.isRegular] - Whether scaling shape has 1:1 ratio or not * @returns {Promise} */ change(shapeObj, options) { return new Promise((resolve, reject) => { if (!isShape(shapeObj)) { reject(rejectMessages.unsupportedType); } const hasFillOption = getFillTypeFromOption(options.fill) === 'filter'; const { canvasImage, createStaticCanvas } = this.graphics; shapeObj.set( hasFillOption ? makeFabricFillOption(options, canvasImage, createStaticCanvas) : options ); if (hasFillOption) { this._resetPositionFillFilter(shapeObj); } this.getCanvas().renderAll(); resolve(); }); } /** * make fill property for user event * @param {fabric.Object} shapeObj - fabric object * @returns {Object} */ makeFillPropertyForUserEvent(shapeObj) { const fillType = getFillTypeFromObject(shapeObj); const fillProp = {}; if (fillType === SHAPE_FILL_TYPE.FILTER) { const fillImage = getFillImageFromShape(shapeObj); const filterOption = makeFilterOptionFromFabricImage(fillImage); fillProp.type = fillType; fillProp.filter = filterOption; } else { fillProp.type = SHAPE_FILL_TYPE.COLOR; fillProp.color = shapeObj.fill || 'transparent'; } return fillProp; } /** * Copy object handling. * @param {fabric.Object} shapeObj - Shape object * @param {fabric.Object} originalShapeObj - Shape object */ processForCopiedObject(shapeObj, originalShapeObj) { this._bindEventOnShape(shapeObj); if (getFillTypeFromObject(shapeObj) === 'filter') { const fillImage = getFillImageFromShape(originalShapeObj); const filterOption = makeFilterOptionFromFabricImage(fillImage); const newStaticCanvas = this.graphics.createStaticCanvas(); shapeObj.set( makeFillPatternForFilter(this.graphics.canvasImage, filterOption, newStaticCanvas) ); this._resetPositionFillFilter(shapeObj); } } /** * Create the instance of shape * @param {string} type - Shape type * @param {Object} options - Options to creat the shape * @returns {fabric.Object} Shape instance * @private */ _createInstance(type, options) { let instance; switch (type) { case 'rect': instance = new fabric.Rect(options); break; case 'circle': instance = new fabric.Ellipse( extend( { type: 'circle', }, options ) ); break; case 'triangle': instance = new fabric.Triangle(options); break; default: instance = {}; } return instance; } /** * Get the options to create the shape * @param {Object} options - Options to creat the shape * @returns {Object} Shape options * @private */ _extendOptions(options) { const selectionStyles = fObjectOptions.SELECTION_STYLE; const { canvasImage, createStaticCanvas } = this.graphics; options = extend({}, SHAPE_INIT_OPTIONS, this._options, selectionStyles, options); return makeFabricFillOption(options, canvasImage, createStaticCanvas); } /** * Bind fabric events on the creating shape object * @param {fabric.Object} shapeObj - Shape object * @private */ _bindEventOnShape(shapeObj) { const self = this; const canvas = this.getCanvas(); shapeObj.on({ added() { self._shapeObj = this; resizeHelper.setOrigins(self._shapeObj); }, selected() { self._isSelected = true; self._shapeObj = this; canvas.uniformScaling = true; canvas.defaultCursor = 'default'; resizeHelper.setOrigins(self._shapeObj); }, deselected() { self._isSelected = false; self._shapeObj = null; canvas.defaultCursor = 'crosshair'; canvas.uniformScaling = false; }, modified() { const currentObj = self._shapeObj; resizeHelper.adjustOriginToCenter(currentObj); resizeHelper.setOrigins(currentObj); }, modifiedInGroup(activeSelection) { self._fillFilterRePositionInGroupSelection(shapeObj, activeSelection); }, moving() { self._resetPositionFillFilter(this); }, rotating() { self._resetPositionFillFilter(this); }, scaling(fEvent) { const pointer = canvas.getPointer(fEvent.e); const currentObj = self._shapeObj; canvas.setCursor('crosshair'); resizeHelper.resize(currentObj, pointer, true); self._resetPositionFillFilter(this); }, }); } /** * MouseDown event handler on canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object * @private */ _onFabricMouseDown(fEvent) { if (!fEvent.target) { this._isSelected = false; this._shapeObj = false; } if (!this._isSelected && !this._shapeObj) { const canvas = this.getCanvas(); this._startPoint = canvas.getPointer(fEvent.e); canvas.on({ 'mouse:move': this._handlers.mousemove, 'mouse:up': this._handlers.mouseup, }); } } /** * MouseDown event handler on canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object * @private */ _onFabricMouseMove(fEvent) { const canvas = this.getCanvas(); const pointer = canvas.getPointer(fEvent.e); const startPointX = this._startPoint.x; const startPointY = this._startPoint.y; const width = startPointX - pointer.x; const height = startPointY - pointer.y; const shape = this._shapeObj; if (!shape) { this.add(this._type, { left: startPointX, top: startPointY, width, height, }).then((objectProps) => { this.fire(eventNames.ADD_OBJECT, objectProps); }); } else { this._shapeObj.set({ isRegular: this._withShiftKey, }); resizeHelper.resize(shape, pointer); canvas.renderAll(); this._resetPositionFillFilter(shape); } } /** * MouseUp event handler on canvas * @private */ _onFabricMouseUp() { const canvas = this.getCanvas(); const startPointX = this._startPoint.x; const startPointY = this._startPoint.y; const shape = this._shapeObj; if (!shape) { this.add(this._type, { left: startPointX, top: startPointY, width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, }).then((objectProps) => { this.fire(eventNames.ADD_OBJECT, objectProps); }); } else if (shape) { resizeHelper.adjustOriginToCenter(shape); this.fire(eventNames.OBJECT_ADDED, this.graphics.createObjectProperties(shape)); } canvas.off({ 'mouse:move': this._handlers.mousemove, 'mouse:up': this._handlers.mouseup, }); } /** * Keydown event handler on document * @param {KeyboardEvent} e - Event object * @private */ _onKeyDown(e) { if (e.keyCode === KEY_CODES.SHIFT) { this._withShiftKey = true; if (this._shapeObj) { this._shapeObj.isRegular = true; } } } /** * Keyup event handler on document * @param {KeyboardEvent} e - Event object * @private */ _onKeyUp(e) { if (e.keyCode === KEY_CODES.SHIFT) { this._withShiftKey = false; if (this._shapeObj) { this._shapeObj.isRegular = false; } } } /** * Reset shape position and internal proportions in the filter type fill area. * @param {fabric.Object} shapeObj - Shape object * @private */ _resetPositionFillFilter(shapeObj) { if (getFillTypeFromObject(shapeObj) !== 'filter') { return; } const { patternSourceCanvas } = getCustomProperty(shapeObj, 'patternSourceCanvas'); const fillImage = getFillImageFromShape(shapeObj); const { originalAngle } = getCustomProperty(fillImage, 'originalAngle'); if (this.graphics.canvasImage.angle !== originalAngle) { reMakePatternImageSource(shapeObj, this.graphics.canvasImage); } const { originX, originY } = shapeObj; resizeHelper.adjustOriginToCenter(shapeObj); shapeObj.width *= shapeObj.scaleX; shapeObj.height *= shapeObj.scaleY; shapeObj.rx *= shapeObj.scaleX; shapeObj.ry *= shapeObj.scaleY; shapeObj.scaleX = 1; shapeObj.scaleY = 1; rePositionFilterTypeFillImage(shapeObj); changeOrigin(shapeObj, { originX, originY, }); resetFillPatternCanvas(patternSourceCanvas); } /** * Reset filter area position within group selection. * @param {fabric.Object} shapeObj - Shape object * @param {fabric.ActiveSelection} activeSelection - Shape object * @private */ _fillFilterRePositionInGroupSelection(shapeObj, activeSelection) { if (activeSelection.scaleX !== 1 || activeSelection.scaleY !== 1) { // This is necessary because the group's scale transition state affects the relative size of the fill area. // The only way to reset the object transformation scale state to neutral. // {@link https://github.com/fabricjs/fabric.js/issues/5372} activeSelection.addWithUpdate(); } const { angle, left, top } = shapeObj; fabric.util.addTransformToObject(shapeObj, activeSelection.calcTransformMatrix()); this._resetPositionFillFilter(shapeObj); shapeObj.set({ angle, left, top, }); } } ================================================ FILE: apps/image-editor/src/js/component/text.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import isExisty from 'tui-code-snippet/type/isExisty'; import forEach from 'tui-code-snippet/collection/forEach'; import Component from '@/interface/component'; import { stamp } from '@/util'; import { componentNames, eventNames as events, fObjectOptions } from '@/consts'; const defaultStyles = { fill: '#000000', left: 0, top: 0, }; const resetStyles = { fill: '#000000', fontStyle: 'normal', fontWeight: 'normal', textAlign: 'tie-text-align-left', underline: false, }; const DBCLICK_TIME = 500; /** * Text * @class Text * @param {Graphics} graphics - Graphics instance * @extends {Component} * @ignore */ class Text extends Component { constructor(graphics) { super(componentNames.TEXT, graphics); /** * Default text style * @type {Object} */ this._defaultStyles = defaultStyles; /** * Selected state * @type {boolean} */ this._isSelected = false; /** * Selected text object * @type {Object} */ this._selectedObj = {}; /** * Editing text object * @type {Object} */ this._editingObj = {}; /** * Listeners for fabric event * @type {Object} */ this._listeners = { mousedown: this._onFabricMouseDown.bind(this), select: this._onFabricSelect.bind(this), selectClear: this._onFabricSelectClear.bind(this), scaling: this._onFabricScaling.bind(this), textChanged: this._onFabricTextChanged.bind(this), }; /** * Textarea element for editing * @type {HTMLElement} */ this._textarea = null; /** * Ratio of current canvas * @type {number} */ this._ratio = 1; /** * Last click time * @type {Date} */ this._lastClickTime = new Date().getTime(); /** * Text object infos before editing * @type {Object} */ this._editingObjInfos = {}; /** * Previous state of editing * @type {boolean} */ this.isPrevEditing = false; } /** * Start input text mode */ start() { const canvas = this.getCanvas(); canvas.selection = false; canvas.defaultCursor = 'text'; canvas.on({ 'mouse:down': this._listeners.mousedown, 'selection:created': this._listeners.select, 'selection:updated': this._listeners.select, 'before:selection:cleared': this._listeners.selectClear, 'object:scaling': this._listeners.scaling, 'text:changed': this._listeners.textChanged, }); canvas.forEachObject((obj) => { if (obj.type === 'i-text') { this.adjustOriginPosition(obj, 'start'); } }); this.setCanvasRatio(); } /** * End input text mode */ end() { const canvas = this.getCanvas(); canvas.selection = true; canvas.defaultCursor = 'default'; canvas.forEachObject((obj) => { if (obj.type === 'i-text') { if (obj.text === '') { canvas.remove(obj); } else { this.adjustOriginPosition(obj, 'end'); } } }); canvas.off({ 'mouse:down': this._listeners.mousedown, 'selection:created': this._listeners.select, 'selection:updated': this._listeners.select, 'before:selection:cleared': this._listeners.selectClear, 'object:selected': this._listeners.select, 'object:scaling': this._listeners.scaling, 'text:changed': this._listeners.textChanged, }); } /** * Adjust the origin position * @param {fabric.Object} text - text object * @param {string} editStatus - 'start' or 'end' */ adjustOriginPosition(text, editStatus) { let [originX, originY] = ['center', 'center']; if (editStatus === 'start') { [originX, originY] = ['left', 'top']; } const { x: left, y: top } = text.getPointByOrigin(originX, originY); text.set({ left, top, originX, originY, }); text.setCoords(); } /** * Add new text on canvas image * @param {string} text - Initial input text * @param {Object} options - Options for generating text * @param {Object} [options.styles] Initial styles * @param {string} [options.styles.fill] Color * @param {string} [options.styles.fontFamily] Font type for text * @param {number} [options.styles.fontSize] Size * @param {string} [options.styles.fontStyle] Type of inclination (normal / italic) * @param {string} [options.styles.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [options.styles.textAlign] Type of text align (left / center / right) * @param {string} [options.styles.textDecoration] Type of line (underline / line-through / overline) * @param {{x: number, y: number}} [options.position] - Initial position * @returns {Promise} */ add(text, options) { return new Promise((resolve) => { const canvas = this.getCanvas(); let newText = null; let selectionStyle = fObjectOptions.SELECTION_STYLE; let styles = this._defaultStyles; this._setInitPos(options.position); if (options.styles) { styles = extend(styles, options.styles); } if (!isExisty(options.autofocus)) { options.autofocus = true; } newText = new fabric.IText(text, styles); selectionStyle = extend({}, selectionStyle, { originX: 'left', originY: 'top', }); newText.set(selectionStyle); newText.on({ mouseup: this._onFabricMouseUp.bind(this), }); canvas.add(newText); if (options.autofocus) { newText.enterEditing(); newText.selectAll(); } if (!canvas.getActiveObject()) { canvas.setActiveObject(newText); } this.isPrevEditing = true; resolve(this.graphics.createObjectProperties(newText)); }); } /** * Change text of activate object on canvas image * @param {Object} activeObj - Current selected text object * @param {string} text - Changed text * @returns {Promise} */ change(activeObj, text) { return new Promise((resolve) => { activeObj.set('text', text); this.getCanvas().renderAll(); resolve(); }); } /** * Set style * @param {Object} activeObj - Current selected text object * @param {Object} styleObj - Initial styles * @param {string} [styleObj.fill] Color * @param {string} [styleObj.fontFamily] Font type for text * @param {number} [styleObj.fontSize] Size * @param {string} [styleObj.fontStyle] Type of inclination (normal / italic) * @param {string} [styleObj.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [styleObj.textAlign] Type of text align (left / center / right) * @param {string} [styleObj.textDecoration] Type of line (underline / line-through / overline) * @returns {Promise} */ setStyle(activeObj, styleObj) { return new Promise((resolve) => { forEach( styleObj, (val, key) => { if (activeObj[key] === val && key !== 'fontSize') { styleObj[key] = resetStyles[key] || ''; } }, this ); if ('textDecoration' in styleObj) { extend(styleObj, this._getTextDecorationAdaptObject(styleObj.textDecoration)); } activeObj.set(styleObj); this.getCanvas().renderAll(); resolve(); }); } /** * Get the text * @param {Object} activeObj - Current selected text object * @returns {String} text */ getText(activeObj) { return activeObj.text; } /** * Set infos of the current selected object * @param {fabric.Text} obj - Current selected text object * @param {boolean} state - State of selecting */ setSelectedInfo(obj, state) { this._selectedObj = obj; this._isSelected = state; } /** * Whether object is selected or not * @returns {boolean} State of selecting */ isSelected() { return this._isSelected; } /** * Get current selected text object * @returns {fabric.Text} Current selected text object */ getSelectedObj() { return this._selectedObj; } /** * Set ratio value of canvas */ setCanvasRatio() { const canvasElement = this.getCanvasElement(); const cssWidth = parseInt(canvasElement.style.maxWidth, 10); const originWidth = canvasElement.width; this._ratio = originWidth / cssWidth; } /** * Get ratio value of canvas * @returns {number} Ratio value */ getCanvasRatio() { return this._ratio; } /** * Get text decoration adapt object * @param {string} textDecoration - text decoration option string * @returns {object} adapt object for override */ _getTextDecorationAdaptObject(textDecoration) { return { underline: textDecoration === 'underline', linethrough: textDecoration === 'line-through', overline: textDecoration === 'overline', }; } /** * Set initial position on canvas image * @param {{x: number, y: number}} [position] - Selected position * @private */ _setInitPos(position) { position = position || this.getCanvasImage().getCenterPoint(); this._defaultStyles.left = position.x; this._defaultStyles.top = position.y; } /** * Input event handler * @private */ _onInput() { const ratio = this.getCanvasRatio(); const obj = this._editingObj; const textareaStyle = this._textarea.style; textareaStyle.width = `${Math.ceil(obj.width / ratio)}px`; textareaStyle.height = `${Math.ceil(obj.height / ratio)}px`; } /** * Keydown event handler * @private */ _onKeyDown() { const ratio = this.getCanvasRatio(); const obj = this._editingObj; const textareaStyle = this._textarea.style; setTimeout(() => { obj.text(this._textarea.value); textareaStyle.width = `${Math.ceil(obj.width / ratio)}px`; textareaStyle.height = `${Math.ceil(obj.height / ratio)}px`; }, 0); } /** * Blur event handler * @private */ _onBlur() { const ratio = this.getCanvasRatio(); const editingObj = this._editingObj; const editingObjInfos = this._editingObjInfos; const textContent = this._textarea.value; let transWidth = editingObj.width / ratio - editingObjInfos.width / ratio; let transHeight = editingObj.height / ratio - editingObjInfos.height / ratio; if (ratio === 1) { transWidth /= 2; transHeight /= 2; } this._textarea.style.display = 'none'; editingObj.set({ left: editingObjInfos.left + transWidth, top: editingObjInfos.top + transHeight, }); if (textContent.length) { this.getCanvas().add(editingObj); const params = { id: stamp(editingObj), type: editingObj.type, text: textContent, }; this.fire(events.TEXT_CHANGED, params); } } /** * Scroll event handler * @private */ _onScroll() { this._textarea.scrollLeft = 0; this._textarea.scrollTop = 0; } /** * Fabric scaling event handler * @param {fabric.Event} fEvent - Current scaling event on selected object * @private */ _onFabricScaling(fEvent) { const obj = fEvent.target; obj.fontSize = obj.fontSize * obj.scaleY; obj.scaleX = 1; obj.scaleY = 1; } /** * textChanged event handler * @param {{target: fabric.Object}} props - changed text object * @private */ _onFabricTextChanged(props) { this.fire(events.TEXT_CHANGED, props.target); } /** * onSelectClear handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onFabricSelectClear(fEvent) { const obj = this.getSelectedObj(); this.isPrevEditing = true; this.setSelectedInfo(fEvent.target, false); if (obj) { // obj is empty object at initial time, will be set fabric object if (obj.text === '') { this.getCanvas().remove(obj); } } } /** * onSelect handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onFabricSelect(fEvent) { this.isPrevEditing = true; this.setSelectedInfo(fEvent.target, true); } /** * Fabric 'mousedown' event handler * @param {fabric.Event} fEvent - Current mousedown event on selected object * @private */ _onFabricMouseDown(fEvent) { const obj = fEvent.target; if (obj && !obj.isType('text')) { return; } if (this.isPrevEditing) { this.isPrevEditing = false; return; } this._fireAddText(fEvent); } /** * Fire 'addText' event if object is not selected. * @param {fabric.Event} fEvent - Current mousedown event on selected object * @private */ _fireAddText(fEvent) { const obj = fEvent.target; const e = fEvent.e || {}; const originPointer = this.getCanvas().getPointer(e); if (!obj) { this.fire(events.ADD_TEXT, { originPosition: { x: originPointer.x, y: originPointer.y, }, clientPosition: { x: e.clientX || 0, y: e.clientY || 0, }, }); } } /** * Fabric mouseup event handler * @param {fabric.Event} fEvent - Current mousedown event on selected object * @private */ _onFabricMouseUp(fEvent) { const { target } = fEvent; const newClickTime = new Date().getTime(); if (this._isDoubleClick(newClickTime) && !target.isEditing) { target.enterEditing(); } if (target.isEditing) { this.fire(events.TEXT_EDITING); // fire editing text event } this._lastClickTime = newClickTime; } /** * Get state of firing double click event * @param {Date} newClickTime - Current clicked time * @returns {boolean} Whether double clicked or not * @private */ _isDoubleClick(newClickTime) { return newClickTime - this._lastClickTime < DBCLICK_TIME; } } export default Text; ================================================ FILE: apps/image-editor/src/js/component/zoom.js ================================================ import { fabric } from 'fabric'; import Component from '@/interface/component'; import { clamp } from '@/util'; import { componentNames, eventNames, keyCodes, zoomModes } from '@/consts'; const MOUSE_MOVE_THRESHOLD = 10; const DEFAULT_SCROLL_OPTION = { left: 0, top: 0, width: 0, height: 0, stroke: '#000000', strokeWidth: 0, fill: '#000000', opacity: 0.4, evented: false, selectable: false, hoverCursor: 'auto', }; const DEFAULT_VERTICAL_SCROLL_RATIO = { SIZE: 0.0045, MARGIN: 0.003, BORDER_RADIUS: 0.003, }; const DEFAULT_HORIZONTAL_SCROLL_RATIO = { SIZE: 0.0066, MARGIN: 0.0044, BORDER_RADIUS: 0.003, }; const DEFAULT_ZOOM_LEVEL = 1.0; const { ZOOM_CHANGED, ADD_TEXT, TEXT_EDITING, OBJECT_MODIFIED, KEY_DOWN, KEY_UP, HAND_STARTED, HAND_STOPPED, } = eventNames; /** * Zoom components * @param {Graphics} graphics - Graphics instance * @extends {Component} * @class Zoom * @ignore */ class Zoom extends Component { constructor(graphics) { super(componentNames.ZOOM, graphics); /** * zoomArea * @type {?fabric.Rect} * @private */ this.zoomArea = null; /** * Start point of zoom area * @type {?{x: number, y: number}} */ this._startPoint = null; /** * Center point of every zoom * @type {Array.<{prevZoomLevel: number, zoomLevel: number, x: number, y: number}>} */ this._centerPoints = []; /** * Zoom level (default: 100%(1.0), max: 400%(4.0)) * @type {number} */ this.zoomLevel = DEFAULT_ZOOM_LEVEL; /** * Zoom mode ('normal', 'zoom', 'hand') * @type {string} */ this.zoomMode = zoomModes.DEFAULT; /** * Listeners * @type {Object.} * @private */ this._listeners = { startZoom: this._onMouseDownWithZoomMode.bind(this), moveZoom: this._onMouseMoveWithZoomMode.bind(this), stopZoom: this._onMouseUpWithZoomMode.bind(this), startHand: this._onMouseDownWithHandMode.bind(this), moveHand: this._onMouseMoveWithHandMode.bind(this), stopHand: this._onMouseUpWithHandMode.bind(this), zoomChanged: this._changeScrollState.bind(this), keydown: this._startHandModeWithSpaceBar.bind(this), keyup: this._endHandModeWithSpaceBar.bind(this), }; const canvas = this.getCanvas(); /** * Width:Height ratio (ex. width=1.5, height=1 -> aspectRatio=1.5) * @private */ this.aspectRatio = canvas.width / canvas.height; /** * vertical scroll bar * @type {fabric.Rect} * @private */ this._verticalScroll = new fabric.Rect(DEFAULT_SCROLL_OPTION); /** * horizontal scroll bar * @type {fabric.Rect} * @private */ this._horizontalScroll = new fabric.Rect(DEFAULT_SCROLL_OPTION); canvas.on(ZOOM_CHANGED, this._listeners.zoomChanged); this.graphics.on(ADD_TEXT, this._startTextEditingHandler.bind(this)); this.graphics.on(TEXT_EDITING, this._startTextEditingHandler.bind(this)); this.graphics.on(OBJECT_MODIFIED, this._stopTextEditingHandler.bind(this)); } /** * Attach zoom keyboard events */ attachKeyboardZoomEvents() { fabric.util.addListener(document, KEY_DOWN, this._listeners.keydown); fabric.util.addListener(document, KEY_UP, this._listeners.keyup); } /** * Detach zoom keyboard events */ detachKeyboardZoomEvents() { fabric.util.removeListener(document, KEY_DOWN, this._listeners.keydown); fabric.util.removeListener(document, KEY_UP, this._listeners.keyup); } /** * Handler when you started editing text * @private */ _startTextEditingHandler() { this.isTextEditing = true; } /** * Handler when you stopped editing text * @private */ _stopTextEditingHandler() { this.isTextEditing = false; } /** * Handler who turns on hand mode when the space bar is down * @param {KeyboardEvent} e - Event object * @private */ _startHandModeWithSpaceBar(e) { if (this.withSpace || this.isTextEditing) { return; } if (e.keyCode === keyCodes.SPACE) { this.withSpace = true; this.startHandMode(); } } /** * Handler who turns off hand mode when space bar is up * @param {KeyboardEvent} e - Event object * @private */ _endHandModeWithSpaceBar(e) { if (e.keyCode === keyCodes.SPACE) { this.withSpace = false; this.endHandMode(); } } /** * Start zoom-in mode */ startZoomInMode() { if (this.zoomArea) { return; } this.endHandMode(); this.zoomMode = zoomModes.ZOOM; const canvas = this.getCanvas(); this._changeObjectsEventedState(false); this.zoomArea = new fabric.Rect({ left: 0, top: 0, width: 0.5, height: 0.5, stroke: 'black', strokeWidth: 1, fill: 'transparent', hoverCursor: 'zoom-in', }); canvas.discardActiveObject(); canvas.add(this.zoomArea); canvas.on('mouse:down', this._listeners.startZoom); canvas.selection = false; canvas.defaultCursor = 'zoom-in'; } /** * End zoom-in mode */ endZoomInMode() { this.zoomMode = zoomModes.DEFAULT; const canvas = this.getCanvas(); const { startZoom, moveZoom, stopZoom } = this._listeners; canvas.selection = true; canvas.defaultCursor = 'auto'; canvas.off({ 'mouse:down': startZoom, 'mouse:move': moveZoom, 'mouse:up': stopZoom, }); this._changeObjectsEventedState(true); canvas.remove(this.zoomArea); this.zoomArea = null; } /** * Start zoom drawing mode */ start() { this.zoomArea = null; this._startPoint = null; this._startHandPoint = null; } /** * Stop zoom drawing mode */ end() { this.endZoomInMode(); this.endHandMode(); } /** * Start hand mode */ startHandMode() { this.endZoomInMode(); this.zoomMode = zoomModes.HAND; const canvas = this.getCanvas(); this._changeObjectsEventedState(false); canvas.discardActiveObject(); canvas.off('mouse:down', this._listeners.startHand); canvas.on('mouse:down', this._listeners.startHand); canvas.selection = false; canvas.defaultCursor = 'grab'; canvas.fire(HAND_STARTED); } /** * Stop hand mode */ endHandMode() { this.zoomMode = zoomModes.DEFAULT; const canvas = this.getCanvas(); this._changeObjectsEventedState(true); canvas.off('mouse:down', this._listeners.startHand); canvas.selection = true; canvas.defaultCursor = 'auto'; this._startHandPoint = null; canvas.fire(HAND_STOPPED); } /** * onMousedown handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onMouseDownWithZoomMode({ target, e }) { if (target) { return; } const canvas = this.getCanvas(); canvas.selection = false; this._startPoint = canvas.getPointer(e); this.zoomArea.set({ width: 0, height: 0 }); const { moveZoom, stopZoom } = this._listeners; canvas.on({ 'mouse:move': moveZoom, 'mouse:up': stopZoom, }); } /** * onMousemove handler in fabric canvas * @param {{e: MouseEvent}} fEvent - Fabric event * @private */ _onMouseMoveWithZoomMode({ e }) { const canvas = this.getCanvas(); const pointer = canvas.getPointer(e); const { x, y } = pointer; const { zoomArea, _startPoint } = this; const deltaX = Math.abs(x - _startPoint.x); const deltaY = Math.abs(y - _startPoint.y); if (deltaX + deltaY > MOUSE_MOVE_THRESHOLD) { canvas.remove(zoomArea); zoomArea.set(this._calcRectDimensionFromPoint(x, y)); canvas.add(zoomArea); } } /** * Get rect dimension setting from Canvas-Mouse-Position(x, y) * @param {number} x - Canvas-Mouse-Position x * @param {number} y - Canvas-Mouse-Position Y * @returns {{left: number, top: number, width: number, height: number}} * @private */ _calcRectDimensionFromPoint(x, y) { const canvas = this.getCanvas(); const canvasWidth = canvas.getWidth(); const canvasHeight = canvas.getHeight(); const { x: startX, y: startY } = this._startPoint; const { min } = Math; const left = min(startX, x); const top = min(startY, y); const width = clamp(x, startX, canvasWidth) - left; // (startX <= x(mouse) <= canvasWidth) - left const height = clamp(y, startY, canvasHeight) - top; // (startY <= y(mouse) <= canvasHeight) - top return { left, top, width, height }; } /** * onMouseup handler in fabric canvas * @private */ _onMouseUpWithZoomMode() { let { zoomLevel } = this; const { zoomArea } = this; const { moveZoom, stopZoom } = this._listeners; const canvas = this.getCanvas(); const center = this._getCenterPoint(); const { x, y } = center; if (!this._isMaxZoomLevel()) { this._centerPoints.push({ x, y, prevZoomLevel: zoomLevel, zoomLevel: zoomLevel + 1, }); zoomLevel += 1; canvas.zoomToPoint({ x, y }, zoomLevel); this._fireZoomChanged(canvas, zoomLevel); this.zoomLevel = zoomLevel; } canvas.off({ 'mouse:move': moveZoom, 'mouse:up': stopZoom, }); canvas.remove(zoomArea); this._startPoint = null; } /** * Get center point * @returns {{x: number, y: number}} * @private */ _getCenterPoint() { const { left, top, width, height } = this.zoomArea; const { x, y } = this._startPoint; const { aspectRatio } = this; if (width < MOUSE_MOVE_THRESHOLD && height < MOUSE_MOVE_THRESHOLD) { return { x, y }; } return width > height ? { x: left + (aspectRatio * height) / 2, y: top + height / 2 } : { x: left + width / 2, y: top + width / aspectRatio / 2 }; } /** * Zoom the canvas * @param {{x: number, y: number}} center - center of zoom * @param {?number} zoomLevel - zoom level */ zoom({ x, y }, zoomLevel = this.zoomLevel) { const canvas = this.getCanvas(); const centerPoints = this._centerPoints; for (let i = centerPoints.length - 1; i >= 0; i -= 1) { if (centerPoints[i].zoomLevel < zoomLevel) { break; } const { x: prevX, y: prevY, prevZoomLevel } = centerPoints.pop(); canvas.zoomToPoint({ x: prevX, y: prevY }, prevZoomLevel); this.zoomLevel = prevZoomLevel; } canvas.zoomToPoint({ x, y }, zoomLevel); if (!this._isDefaultZoomLevel(zoomLevel)) { this._centerPoints.push({ x, y, zoomLevel, prevZoomLevel: this.zoomLevel, }); } this.zoomLevel = zoomLevel; this._fireZoomChanged(canvas, zoomLevel); } /** * Zoom out one step */ zoomOut() { const centerPoints = this._centerPoints; if (!centerPoints.length) { return; } const canvas = this.getCanvas(); const point = centerPoints.pop(); const { x, y, prevZoomLevel } = point; if (this._isDefaultZoomLevel(prevZoomLevel)) { canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); } else { canvas.zoomToPoint({ x, y }, prevZoomLevel); } this.zoomLevel = prevZoomLevel; this._fireZoomChanged(canvas, this.zoomLevel); } /** * Zoom reset */ resetZoom() { const canvas = this.getCanvas(); canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); this.zoomLevel = DEFAULT_ZOOM_LEVEL; this._centerPoints = []; this._fireZoomChanged(canvas, this.zoomLevel); } /** * Whether zoom level is max (5.0) * @returns {boolean} * @private */ _isMaxZoomLevel() { return this.zoomLevel >= 5.0; } /** * Move point of zoom * @param {{x: number, y: number}} delta - move amount * @private */ _movePointOfZoom({ x: deltaX, y: deltaY }) { const centerPoints = this._centerPoints; if (!centerPoints.length) { return; } const canvas = this.getCanvas(); const { zoomLevel } = this; const point = centerPoints.pop(); const { x: originX, y: originY, prevZoomLevel } = point; const x = originX - deltaX; const y = originY - deltaY; canvas.zoomToPoint({ x: originX, y: originY }, prevZoomLevel); canvas.zoomToPoint({ x, y }, zoomLevel); centerPoints.push({ x, y, prevZoomLevel, zoomLevel }); this._fireZoomChanged(canvas, zoomLevel); } /** * onMouseDown handler in fabric canvas * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onMouseDownWithHandMode({ target, e }) { if (target) { return; } const canvas = this.getCanvas(); if (this.zoomLevel <= DEFAULT_ZOOM_LEVEL) { return; } canvas.selection = false; this._startHandPoint = canvas.getPointer(e); const { moveHand, stopHand } = this._listeners; canvas.on({ 'mouse:move': moveHand, 'mouse:up': stopHand, }); } /** * onMouseMove handler in fabric canvas * @param {{e: MouseEvent}} fEvent - Fabric event * @private */ _onMouseMoveWithHandMode({ e }) { const canvas = this.getCanvas(); const { x, y } = canvas.getPointer(e); const deltaX = x - this._startHandPoint.x; const deltaY = y - this._startHandPoint.y; this._movePointOfZoom({ x: deltaX, y: deltaY }); } /** * onMouseUp handler in fabric canvas * @private */ _onMouseUpWithHandMode() { const canvas = this.getCanvas(); const { moveHand, stopHand } = this._listeners; canvas.off({ 'mouse:move': moveHand, 'mouse:up': stopHand, }); this._startHandPoint = null; } /** * onChangeZoom handler in fabric canvas * @private */ _changeScrollState({ viewport, zoomLevel }) { const canvas = this.getCanvas(); canvas.remove(this._verticalScroll); canvas.remove(this._horizontalScroll); if (this._isDefaultZoomLevel(zoomLevel)) { return; } const canvasWidth = canvas.width; const canvasHeight = canvas.height; const { tl, tr, bl } = viewport; const viewportWidth = tr.x - tl.x; const viewportHeight = bl.y - tl.y; const horizontalScrollWidth = (viewportWidth * viewportWidth) / canvasWidth; const horizontalScrollHeight = viewportHeight * DEFAULT_HORIZONTAL_SCROLL_RATIO.SIZE; const horizontalScrollLeft = clamp( tl.x + (tl.x / canvasWidth) * viewportWidth, tl.x, tr.x - horizontalScrollWidth ); const horizontalScrollMargin = viewportHeight * DEFAULT_HORIZONTAL_SCROLL_RATIO.MARGIN; const horizontalScrollBorderRadius = viewportHeight * DEFAULT_HORIZONTAL_SCROLL_RATIO.BORDER_RADIUS; this._horizontalScroll.set({ left: horizontalScrollLeft, top: bl.y - horizontalScrollHeight - horizontalScrollMargin, width: horizontalScrollWidth, height: horizontalScrollHeight, rx: horizontalScrollBorderRadius, ry: horizontalScrollBorderRadius, }); const verticalScrollWidth = viewportWidth * DEFAULT_VERTICAL_SCROLL_RATIO.SIZE; const verticalScrollHeight = (viewportHeight * viewportHeight) / canvasHeight; const verticalScrollTop = clamp( tl.y + (tl.y / canvasHeight) * viewportHeight, tr.y, bl.y - verticalScrollHeight ); const verticalScrollMargin = viewportWidth * DEFAULT_VERTICAL_SCROLL_RATIO.MARGIN; const verticalScrollBorderRadius = viewportWidth * DEFAULT_VERTICAL_SCROLL_RATIO.BORDER_RADIUS; this._verticalScroll.set({ left: tr.x - verticalScrollWidth - verticalScrollMargin, top: verticalScrollTop, width: verticalScrollWidth, height: verticalScrollHeight, rx: verticalScrollBorderRadius, ry: verticalScrollBorderRadius, }); this._addScrollBar(); } /** * Change objects 'evented' state * @param {boolean} [evented=true] - objects 'evented' state */ _changeObjectsEventedState(evented = true) { const canvas = this.getCanvas(); canvas.forEachObject((obj) => { // {@link http://fabricjs.com/docs/fabric.Object.html#evented} obj.evented = evented; }); } /** * Add scroll bar and set remove timer */ _addScrollBar() { const canvas = this.getCanvas(); canvas.add(this._horizontalScroll); canvas.add(this._verticalScroll); if (this.scrollBarTid) { clearTimeout(this.scrollBarTid); } this.scrollBarTid = setTimeout(() => { canvas.remove(this._horizontalScroll); canvas.remove(this._verticalScroll); }, 3000); } /** * Check zoom level is default zoom level (1.0) * @param {number} zoomLevel - zoom level * @returns {boolean} - whether zoom level is 1.0 */ _isDefaultZoomLevel(zoomLevel) { return zoomLevel === DEFAULT_ZOOM_LEVEL; } /** * Fire 'zoomChanged' event * @param {fabric.Canvas} canvas - fabric canvas * @param {number} zoomLevel - 'zoomChanged' event params */ _fireZoomChanged(canvas, zoomLevel) { canvas.fire(ZOOM_CHANGED, { viewport: canvas.calcViewportBoundaries(), zoomLevel }); } /** * Get zoom mode */ get mode() { return this.zoomMode; } } export default Zoom; ================================================ FILE: apps/image-editor/src/js/consts.js ================================================ import { keyMirror } from '@/util'; /** * Help features for zoom * @type {Array.} */ export const ZOOM_HELP_MENUS = ['zoomIn', 'zoomOut', 'hand']; /** * Help features for command * @type {Array.} */ export const COMMAND_HELP_MENUS = ['history', 'undo', 'redo', 'reset']; /** * Help features for delete * @type {Array.} */ export const DELETE_HELP_MENUS = ['delete', 'deleteAll']; /** * Editor help features * @type {Array.} */ export const HELP_MENUS = [...ZOOM_HELP_MENUS, ...COMMAND_HELP_MENUS, ...DELETE_HELP_MENUS]; /** * Fill type for shape * @type {Object.} */ export const SHAPE_FILL_TYPE = { FILTER: 'filter', COLOR: 'color', }; /** * Shape type list * @type {Array.} */ export const SHAPE_TYPE = ['rect', 'circle', 'triangle']; /** * Object type * @type {Object.} */ export const OBJ_TYPE = { CROPZONE: 'cropzone', }; /** * Filter type map * @type {Object.} */ export const filterType = { VINTAGE: 'vintage', SEPIA2: 'sepia2', REMOVE_COLOR: 'removeColor', COLOR_FILTER: 'colorFilter', REMOVE_WHITE: 'removeWhite', BLEND_COLOR: 'blendColor', BLEND: 'blend', }; /** * Component names * @type {Object.} */ export const componentNames = keyMirror( 'IMAGE_LOADER', 'CROPPER', 'FLIP', 'ROTATION', 'FREE_DRAWING', 'LINE', 'TEXT', 'ICON', 'FILTER', 'SHAPE', 'ZOOM', 'RESIZE' ); /** * Shape default option * @type {Object} */ export const SHAPE_DEFAULT_OPTIONS = { lockSkewingX: true, lockSkewingY: true, bringForward: true, isRegular: false, }; /** * Cropzone default option * @type {Object} */ export const CROPZONE_DEFAULT_OPTIONS = { hasRotatingPoint: false, hasBorders: false, lockScalingFlip: true, lockRotation: true, lockSkewingX: true, lockSkewingY: true, }; /** * Command names * @type {Object.} */ export const commandNames = { CLEAR_OBJECTS: 'clearObjects', LOAD_IMAGE: 'loadImage', FLIP_IMAGE: 'flip', ROTATE_IMAGE: 'rotate', ADD_OBJECT: 'addObject', REMOVE_OBJECT: 'removeObject', APPLY_FILTER: 'applyFilter', REMOVE_FILTER: 'removeFilter', ADD_ICON: 'addIcon', CHANGE_ICON_COLOR: 'changeIconColor', ADD_SHAPE: 'addShape', CHANGE_SHAPE: 'changeShape', ADD_TEXT: 'addText', CHANGE_TEXT: 'changeText', CHANGE_TEXT_STYLE: 'changeTextStyle', ADD_IMAGE_OBJECT: 'addImageObject', RESIZE_CANVAS_DIMENSION: 'resizeCanvasDimension', SET_OBJECT_PROPERTIES: 'setObjectProperties', SET_OBJECT_POSITION: 'setObjectPosition', CHANGE_SELECTION: 'changeSelection', RESIZE_IMAGE: 'resize', }; /** * Event names * @type {Object.} */ export const eventNames = { OBJECT_ACTIVATED: 'objectActivated', OBJECT_MOVED: 'objectMoved', OBJECT_SCALED: 'objectScaled', OBJECT_CREATED: 'objectCreated', OBJECT_ROTATED: 'objectRotated', OBJECT_ADDED: 'objectAdded', OBJECT_MODIFIED: 'objectModified', TEXT_EDITING: 'textEditing', TEXT_CHANGED: 'textChanged', ICON_CREATE_RESIZE: 'iconCreateResize', ICON_CREATE_END: 'iconCreateEnd', ADD_TEXT: 'addText', ADD_OBJECT: 'addObject', ADD_OBJECT_AFTER: 'addObjectAfter', MOUSE_DOWN: 'mousedown', MOUSE_UP: 'mouseup', MOUSE_MOVE: 'mousemove', // UNDO/REDO Events REDO_STACK_CHANGED: 'redoStackChanged', UNDO_STACK_CHANGED: 'undoStackChanged', SELECTION_CLEARED: 'selectionCleared', SELECTION_CREATED: 'selectionCreated', EXECUTE_COMMAND: 'executeCommand', AFTER_UNDO: 'afterUndo', AFTER_REDO: 'afterRedo', ZOOM_CHANGED: 'zoomChanged', HAND_STARTED: 'handStarted', HAND_STOPPED: 'handStopped', KEY_DOWN: 'keydown', KEY_UP: 'keyup', INPUT_BOX_EDITING_STARTED: 'inputBoxEditingStarted', INPUT_BOX_EDITING_STOPPED: 'inputBoxEditingStopped', FOCUS: 'focus', BLUR: 'blur', IMAGE_RESIZED: 'imageResized', }; /** * Selector names * @type {Object.} */ export const selectorNames = { COLOR_PICKER_INPUT_BOX: '.tui-colorpicker-palette-hex', }; /** * History names * @type {Object.} */ export const historyNames = { LOAD_IMAGE: 'Load', LOAD_MASK_IMAGE: 'Mask', ADD_MASK_IMAGE: 'Mask', ADD_IMAGE_OBJECT: 'Mask', CROP: 'Crop', RESIZE: 'Resize', APPLY_FILTER: 'Filter', REMOVE_FILTER: 'Filter', CHANGE_SHAPE: 'Shape', CHANGE_ICON_COLOR: 'Icon', ADD_TEXT: 'Text', CHANGE_TEXT_STYLE: 'Text', REMOVE_OBJECT: 'Delete', CLEAR_OBJECTS: 'Delete', }; /** * Editor states * @type {Object.} */ export const drawingModes = keyMirror( 'NORMAL', 'CROPPER', 'FREE_DRAWING', 'LINE_DRAWING', 'TEXT', 'SHAPE', 'ICON', 'ZOOM', 'RESIZE' ); /** * Menu names with drawing mode * @type {Object.} */ export const drawingMenuNames = { TEXT: 'text', CROP: 'crop', RESIZE: 'resize', SHAPE: 'shape', ZOOM: 'zoom', }; /** * Zoom modes * @type {Object.} */ export const zoomModes = { DEFAULT: 'normal', ZOOM: 'zoom', HAND: 'hand', }; /** * Shortcut key values * @type {Object.} */ export const keyCodes = { Z: 90, Y: 89, C: 67, V: 86, SHIFT: 16, BACKSPACE: 8, DEL: 46, ARROW_DOWN: 40, ARROW_UP: 38, SPACE: 32, DIGIT_0: 48, DIGIT_9: 57, }; /** * Fabric object options * @type {Object.} */ export const fObjectOptions = { SELECTION_STYLE: { borderColor: 'red', cornerColor: 'green', cornerSize: 10, originX: 'center', originY: 'center', transparentCorners: false, }, }; /** * Promise reject messages * @type {Object.} */ export const rejectMessages = { addedObject: 'The object is already added.', flip: 'The flipX and flipY setting values are not changed.', invalidDrawingMode: 'This operation is not supported in the drawing mode.', invalidParameters: 'Invalid parameters.', isLock: 'The executing command state is locked.', loadImage: 'The background image is empty.', loadingImageFailed: 'Invalid image loaded.', noActiveObject: 'There is no active object.', noObject: 'The object is not in canvas.', redo: 'The promise of redo command is reject.', rotation: 'The current angle is same the old angle.', undo: 'The promise of undo command is reject.', unsupportedOperation: 'Unsupported operation.', unsupportedType: 'Unsupported object type.', }; /** * Default icon menu svg path * @type {Object.} */ export const defaultIconPath = { 'icon-arrow': 'M40 12V0l24 24-24 24V36H0V12h40z', 'icon-arrow-2': 'M49,32 H3 V22 h46 l-18,-18 h12 l23,23 L43,50 h-12 l18,-18 z ', 'icon-arrow-3': 'M43.349998,27 L17.354,53 H1.949999 l25.996,-26 L1.949999,1 h15.404 L43.349998,27 z ', 'icon-star': 'M35,54.557999 l-19.912001,10.468 l3.804,-22.172001 l-16.108,-15.7 l22.26,-3.236 L35,3.746 l9.956,20.172001 l22.26,3.236 l-16.108,15.7 l3.804,22.172001 z ', 'icon-star-2': 'M17,31.212 l-7.194,4.08 l-4.728,-6.83 l-8.234,0.524 l-1.328,-8.226 l-7.644,-3.14 l2.338,-7.992 l-5.54,-6.18 l5.54,-6.176 l-2.338,-7.994 l7.644,-3.138 l1.328,-8.226 l8.234,0.522 l4.728,-6.83 L17,-24.312 l7.194,-4.08 l4.728,6.83 l8.234,-0.522 l1.328,8.226 l7.644,3.14 l-2.338,7.992 l5.54,6.178 l-5.54,6.178 l2.338,7.992 l-7.644,3.14 l-1.328,8.226 l-8.234,-0.524 l-4.728,6.83 z ', 'icon-polygon': 'M3,31 L19,3 h32 l16,28 l-16,28 H19 z ', 'icon-location': 'M24 62C8 45.503 0 32.837 0 24 0 10.745 10.745 0 24 0s24 10.745 24 24c0 8.837-8 21.503-24 38zm0-28c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10z', 'icon-heart': 'M49.994999,91.349998 l-6.96,-6.333 C18.324001,62.606995 2.01,47.829002 2.01,29.690998 C2.01,14.912998 13.619999,3.299999 28.401001,3.299999 c8.349,0 16.362,5.859 21.594,12 c5.229,-6.141 13.242001,-12 21.591,-12 c14.778,0 26.390999,11.61 26.390999,26.390999 c0,18.138 -16.314001,32.916 -41.025002,55.374001 l-6.96,6.285 z ', 'icon-bubble': 'M44 48L34 58V48H12C5.373 48 0 42.627 0 36V12C0 5.373 5.373 0 12 0h40c6.627 0 12 5.373 12 12v24c0 6.627-5.373 12-12 12h-8z', }; export const defaultRotateRangeValues = { realTimeEvent: true, min: -360, max: 360, value: 0, }; export const defaultDrawRangeValues = { min: 5, max: 30, value: 12, }; export const defaultShapeStrokeValues = { realTimeEvent: true, min: 2, max: 300, value: 3, }; export const defaultTextRangeValues = { realTimeEvent: true, min: 10, max: 100, value: 50, }; export const defaultFilterRangeValues = { tintOpacityRange: { realTimeEvent: true, min: 0, max: 1, value: 0.7, useDecimal: true, }, removewhiteDistanceRange: { realTimeEvent: true, min: 0, max: 1, value: 0.2, useDecimal: true, }, brightnessRange: { realTimeEvent: true, min: -1, max: 1, value: 0, useDecimal: true, }, noiseRange: { realTimeEvent: true, min: 0, max: 1000, value: 100, }, pixelateRange: { realTimeEvent: true, min: 2, max: 20, value: 4, }, colorfilterThresholdRange: { realTimeEvent: true, min: 0, max: 1, value: 0.2, useDecimal: true, }, blurFilterRange: { value: 0.1, }, }; export const emptyCropRectValues = { LEFT: 0, TOP: 0, WIDTH: 0.5, HEIGHT: 0.5, }; export const defaultResizePixelValues = { realTimeEvent: true, min: 32, }; ================================================ FILE: apps/image-editor/src/js/drawingMode/cropper.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * CropperDrawingMode class * @class * @ignore */ class CropperDrawingMode extends DrawingMode { constructor() { super(drawingModes.CROPPER); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ start(graphics) { const cropper = graphics.getComponent(components.CROPPER); cropper.start(); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const cropper = graphics.getComponent(components.CROPPER); cropper.end(); } } export default CropperDrawingMode; ================================================ FILE: apps/image-editor/src/js/drawingMode/freeDrawing.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * FreeDrawingMode class * @class * @ignore */ class FreeDrawingMode extends DrawingMode { constructor() { super(drawingModes.FREE_DRAWING); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @param {{width: ?number, color: ?string}} [options] - Brush width & color * @override */ start(graphics, options) { const freeDrawing = graphics.getComponent(components.FREE_DRAWING); freeDrawing.start(options); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const freeDrawing = graphics.getComponent(components.FREE_DRAWING); freeDrawing.end(); } } export default FreeDrawingMode; ================================================ FILE: apps/image-editor/src/js/drawingMode/icon.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * IconDrawingMode class * @class * @ignore */ class IconDrawingMode extends DrawingMode { constructor() { super(drawingModes.ICON); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ start(graphics) { const icon = graphics.getComponent(components.ICON); icon.start(); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const icon = graphics.getComponent(components.ICON); icon.end(); } } export default IconDrawingMode; ================================================ FILE: apps/image-editor/src/js/drawingMode/lineDrawing.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * LineDrawingMode class * @class * @ignore */ class LineDrawingMode extends DrawingMode { constructor() { super(drawingModes.LINE_DRAWING); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @param {{width: ?number, color: ?string}} [options] - Brush width & color * @override */ start(graphics, options) { const lineDrawing = graphics.getComponent(components.LINE); lineDrawing.start(options); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const lineDrawing = graphics.getComponent(components.LINE); lineDrawing.end(); } } export default LineDrawingMode; ================================================ FILE: apps/image-editor/src/js/drawingMode/resize.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * ResizeDrawingMode class * @class * @ignore */ class ResizeDrawingMode extends DrawingMode { constructor() { super(drawingModes.RESIZE); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ start(graphics) { const resize = graphics.getComponent(components.RESIZE); resize.start(); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const resize = graphics.getComponent(components.RESIZE); resize.end(); } } export default ResizeDrawingMode; ================================================ FILE: apps/image-editor/src/js/drawingMode/shape.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * ShapeDrawingMode class * @class * @ignore */ class ShapeDrawingMode extends DrawingMode { constructor() { super(drawingModes.SHAPE); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ start(graphics) { const shape = graphics.getComponent(components.SHAPE); shape.start(); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const shape = graphics.getComponent(components.SHAPE); shape.end(); } } export default ShapeDrawingMode; ================================================ FILE: apps/image-editor/src/js/drawingMode/text.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * TextDrawingMode class * @class * @ignore */ class TextDrawingMode extends DrawingMode { constructor() { super(drawingModes.TEXT); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ start(graphics) { const text = graphics.getComponent(components.TEXT); text.start(); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const text = graphics.getComponent(components.TEXT); text.end(); } } export default TextDrawingMode; ================================================ FILE: apps/image-editor/src/js/drawingMode/zoom.js ================================================ import DrawingMode from '@/interface/drawingMode'; import { drawingModes, componentNames as components } from '@/consts'; /** * ZoomDrawingMode class * @class * @ignore */ class ZoomDrawingMode extends DrawingMode { constructor() { super(drawingModes.ZOOM); } /** * start this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ start(graphics) { const zoom = graphics.getComponent(components.ZOOM); zoom.start(); } /** * stop this drawing mode * @param {Graphics} graphics - Graphics instance * @override */ end(graphics) { const zoom = graphics.getComponent(components.ZOOM); zoom.end(); } } export default ZoomDrawingMode; ================================================ FILE: apps/image-editor/src/js/extension/arrowLine.js ================================================ import { fabric } from 'fabric'; const ARROW_ANGLE = 30; const CHEVRON_SIZE_RATIO = 2.7; const TRIANGLE_SIZE_RATIO = 1.7; const RADIAN_CONVERSION_VALUE = 180; const ArrowLine = fabric.util.createClass( fabric.Line, /** @lends Convolute.prototype */ { /** * Line type * @param {String} type * @default */ type: 'line', /** * Constructor * @param {Array} [points] Array of points * @param {Object} [options] Options object * @override */ initialize(points, options = {}) { this.callSuper('initialize', points, options); this.arrowType = options.arrowType; }, /** * Render ArrowLine * @private * @override */ _render(ctx) { const { x1: fromX, y1: fromY, x2: toX, y2: toY } = this.calcLinePoints(); const linePosition = { fromX, fromY, toX, toY, }; this.ctx = ctx; ctx.lineWidth = this.strokeWidth; this._renderBasicLinePath(linePosition); this._drawDecoratorPath(linePosition); this._renderStroke(ctx); }, /** * Render Basic line path * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _renderBasicLinePath({ fromX, fromY, toX, toY }) { this.ctx.beginPath(); this.ctx.moveTo(fromX, fromY); this.ctx.lineTo(toX, toY); }, /** * Render Arrow Head * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _drawDecoratorPath(linePosition) { this._drawDecoratorPathType('head', linePosition); this._drawDecoratorPathType('tail', linePosition); }, /** * Render Arrow Head * @param {string} type - 'head' or 'tail' * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _drawDecoratorPathType(type, linePosition) { switch (this.arrowType[type]) { case 'triangle': this._drawTrianglePath(type, linePosition); break; case 'chevron': this._drawChevronPath(type, linePosition); break; default: break; } }, /** * Render Triangle Head * @param {string} type - 'head' or 'tail' * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _drawTrianglePath(type, linePosition) { const decorateSize = this.ctx.lineWidth * TRIANGLE_SIZE_RATIO; this._drawChevronPath(type, linePosition, decorateSize); this.ctx.closePath(); }, /** * Render Chevron Head * @param {string} type - 'head' or 'tail' * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @param {number} decorateSize - decorate size * @private */ _drawChevronPath(type, { fromX, fromY, toX, toY }, decorateSize) { const { ctx } = this; if (!decorateSize) { decorateSize = this.ctx.lineWidth * CHEVRON_SIZE_RATIO; } const [standardX, standardY] = type === 'head' ? [fromX, fromY] : [toX, toY]; const [compareX, compareY] = type === 'head' ? [toX, toY] : [fromX, fromY]; const angle = (Math.atan2(compareY - standardY, compareX - standardX) * RADIAN_CONVERSION_VALUE) / Math.PI; const rotatedPosition = (changeAngle) => this.getRotatePosition(decorateSize, changeAngle, { x: standardX, y: standardY, }); ctx.moveTo(...rotatedPosition(angle + ARROW_ANGLE)); ctx.lineTo(standardX, standardY); ctx.lineTo(...rotatedPosition(angle - ARROW_ANGLE)); }, /** * return position from change angle. * @param {number} distance - change distance * @param {number} angle - change angle * @param {Object} referencePosition - reference position * @returns {Array} * @private */ getRotatePosition(distance, angle, referencePosition) { const radian = (angle * Math.PI) / RADIAN_CONVERSION_VALUE; const { x, y } = referencePosition; return [distance * Math.cos(radian) + x, distance * Math.sin(radian) + y]; }, } ); export default ArrowLine; ================================================ FILE: apps/image-editor/src/js/extension/blur.js ================================================ import { fabric } from 'fabric'; /** * Blur object * @class Blur * @extends {fabric.Image.filters.Convolute} * @ignore */ const Blur = fabric.util.createClass( fabric.Image.filters.Convolute, /** @lends Convolute.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Blur', /** * constructor * @override */ initialize() { this.matrix = [1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9]; }, } ); export default Blur; ================================================ FILE: apps/image-editor/src/js/extension/colorFilter.js ================================================ import { fabric } from 'fabric'; /** * ColorFilter object * @class ColorFilter * @extends {fabric.Image.filters.BaseFilter} * @ignore */ const ColorFilter = fabric.util.createClass( fabric.Image.filters.BaseFilter, /** @lends BaseFilter.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'ColorFilter', /** * Constructor * @member fabric.Image.filters.ColorFilter.prototype * @param {Object} [options] Options object * @param {Number} [options.color='#FFFFFF'] Value of color (0...255) * @param {Number} [options.threshold=45] Value of threshold (0...255) * @override */ initialize(options) { if (!options) { options = {}; } this.color = options.color || '#FFFFFF'; this.threshold = options.threshold || 45; this.x = options.x || null; this.y = options.y || null; }, /** * Applies filter to canvas element * @param {Object} canvas Canvas object passed by fabric */ // eslint-disable-next-line complexity applyTo(canvas) { const { canvasEl } = canvas; const context = canvasEl.getContext('2d'); const imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height); const { data } = imageData; const { threshold } = this; let filterColor = fabric.Color.sourceFromHex(this.color); let i, len; if (this.x && this.y) { filterColor = this._getColor(imageData, this.x, this.y); } for (i = 0, len = data.length; i < len; i += 4) { if ( this._isOutsideThreshold(data[i], filterColor[0], threshold) || this._isOutsideThreshold(data[i + 1], filterColor[1], threshold) || this._isOutsideThreshold(data[i + 2], filterColor[2], threshold) ) { continue; } data[i] = data[i + 1] = data[i + 2] = data[i + 3] = 0; } context.putImageData(imageData, 0, 0); }, /** * Check color if it is within threshold * @param {Number} color1 source color * @param {Number} color2 filtering color * @param {Number} threshold threshold * @returns {boolean} true if within threshold or false */ _isOutsideThreshold(color1, color2, threshold) { const diff = color1 - color2; return Math.abs(diff) > threshold; }, /** * Get color at (x, y) * @param {Object} imageData of canvas * @param {Number} x left position * @param {Number} y top position * @returns {Array} color array */ _getColor(imageData, x, y) { const color = [0, 0, 0, 0]; const { data, width } = imageData; const bytes = 4; const position = (width * y + x) * bytes; color[0] = data[position]; color[1] = data[position + 1]; color[2] = data[position + 2]; color[3] = data[position + 3]; return color; }, } ); export default ColorFilter; ================================================ FILE: apps/image-editor/src/js/extension/cropzone.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import { clamp } from '@/util'; import { eventNames as events, keyCodes } from '@/consts'; const CORNER_TYPE_TOP_LEFT = 'tl'; const CORNER_TYPE_TOP_RIGHT = 'tr'; const CORNER_TYPE_MIDDLE_TOP = 'mt'; const CORNER_TYPE_MIDDLE_LEFT = 'ml'; const CORNER_TYPE_MIDDLE_RIGHT = 'mr'; const CORNER_TYPE_MIDDLE_BOTTOM = 'mb'; const CORNER_TYPE_BOTTOM_LEFT = 'bl'; const CORNER_TYPE_BOTTOM_RIGHT = 'br'; const CORNER_TYPE_LIST = [ CORNER_TYPE_TOP_LEFT, CORNER_TYPE_TOP_RIGHT, CORNER_TYPE_MIDDLE_TOP, CORNER_TYPE_MIDDLE_LEFT, CORNER_TYPE_MIDDLE_RIGHT, CORNER_TYPE_MIDDLE_BOTTOM, CORNER_TYPE_BOTTOM_LEFT, CORNER_TYPE_BOTTOM_RIGHT, ]; const NOOP_FUNCTION = () => {}; /** * Align with cropzone ratio * @param {string} selectedCorner - selected corner type * @returns {{width: number, height: number}} * @private */ function cornerTypeValid(selectedCorner) { return CORNER_TYPE_LIST.indexOf(selectedCorner) >= 0; } /** * return scale basis type * @param {number} diffX - X distance of the cursor and corner. * @param {number} diffY - Y distance of the cursor and corner. * @returns {string} * @private */ function getScaleBasis(diffX, diffY) { return diffX > diffY ? 'width' : 'height'; } /** * Cropzone object * Issue: IE7, 8(with excanvas) * - Cropzone is a black zone without transparency. * @class Cropzone * @extends {fabric.Rect} * @ignore */ const Cropzone = fabric.util.createClass( fabric.Rect, /** @lends Cropzone.prototype */ { /** * Constructor * @param {Object} canvas canvas * @param {Object} options Options object * @param {Object} extendsOptions object for extends "options" * @override */ initialize(canvas, options, extendsOptions) { options = extend(options, extendsOptions); options.type = 'cropzone'; this.callSuper('initialize', options); this._addEventHandler(); this.canvas = canvas; this.options = options; }, canvasEventDelegation(eventName) { let delegationState = 'unregistered'; const isRegistered = this.canvasEventTrigger[eventName] !== NOOP_FUNCTION; if (isRegistered) { delegationState = 'registered'; } else if ([events.OBJECT_MOVED, events.OBJECT_SCALED].indexOf(eventName) < 0) { delegationState = 'none'; } return delegationState; }, canvasEventRegister(eventName, eventTrigger) { this.canvasEventTrigger[eventName] = eventTrigger; }, _addEventHandler() { this.canvasEventTrigger = { [events.OBJECT_MOVED]: NOOP_FUNCTION, [events.OBJECT_SCALED]: NOOP_FUNCTION, }; this.on({ moving: this._onMoving.bind(this), scaling: this._onScaling.bind(this), }); fabric.util.addListener(document, 'keydown', this._onKeyDown.bind(this)); fabric.util.addListener(document, 'keyup', this._onKeyUp.bind(this)); }, _renderCropzone(ctx) { const cropzoneDashLineWidth = 7; const cropzoneDashLineOffset = 7; // Calc original scale const originalFlipX = this.flipX ? -1 : 1; const originalFlipY = this.flipY ? -1 : 1; const originalScaleX = originalFlipX / this.scaleX; const originalScaleY = originalFlipY / this.scaleY; // Set original scale ctx.scale(originalScaleX, originalScaleY); // Render outer rect this._fillOuterRect(ctx, 'rgba(0, 0, 0, 0.5)'); if (this.options.lineWidth) { this._fillInnerRect(ctx); this._strokeBorder(ctx, 'rgb(255, 255, 255)', { lineWidth: this.options.lineWidth, }); } else { // Black dash line this._strokeBorder(ctx, 'rgb(0, 0, 0)', { lineDashWidth: cropzoneDashLineWidth, }); // White dash line this._strokeBorder(ctx, 'rgb(255, 255, 255)', { lineDashWidth: cropzoneDashLineWidth, lineDashOffset: cropzoneDashLineOffset, }); } // Reset scale ctx.scale(1 / originalScaleX, 1 / originalScaleY); }, /** * Render Crop-zone * @private * @override */ _render(ctx) { this.callSuper('_render', ctx); this._renderCropzone(ctx); }, /** * Cropzone-coordinates with outer rectangle * * x0 x1 x2 x3 * y0 +--------------------------+ * |///////|//////////|///////| // <--- "Outer-rectangle" * |///////|//////////|///////| * y1 +-------+----------+-------+ * |///////| Cropzone |///////| Cropzone is the "Inner-rectangle" * |///////| (0, 0) |///////| Center point (0, 0) * y2 +-------+----------+-------+ * |///////|//////////|///////| * |///////|//////////|///////| * y3 +--------------------------+ * * @typedef {{x: Array, y: Array}} cropzoneCoordinates * @ignore */ /** * Fill outer rectangle * @param {CanvasRenderingContext2D} ctx - Context * @param {string|CanvasGradient|CanvasPattern} fillStyle - Fill-style * @private */ _fillOuterRect(ctx, fillStyle) { const { x, y } = this._getCoordinates(); ctx.save(); ctx.fillStyle = fillStyle; ctx.beginPath(); // Outer rectangle // Numbers are +/-1 so that overlay edges don't get blurry. ctx.moveTo(x[0] - 1, y[0] - 1); ctx.lineTo(x[3] + 1, y[0] - 1); ctx.lineTo(x[3] + 1, y[3] + 1); ctx.lineTo(x[0] - 1, y[3] + 1); ctx.lineTo(x[0] - 1, y[0] - 1); ctx.closePath(); // Inner rectangle ctx.moveTo(x[1], y[1]); ctx.lineTo(x[1], y[2]); ctx.lineTo(x[2], y[2]); ctx.lineTo(x[2], y[1]); ctx.lineTo(x[1], y[1]); ctx.closePath(); ctx.fill(); ctx.restore(); }, /** * Draw Inner grid line * @param {CanvasRenderingContext2D} ctx - Context * @private */ _fillInnerRect(ctx) { const { x: outerX, y: outerY } = this._getCoordinates(); const x = this._caculateInnerPosition(outerX, (outerX[2] - outerX[1]) / 3); const y = this._caculateInnerPosition(outerY, (outerY[2] - outerY[1]) / 3); ctx.save(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = this.options.lineWidth; ctx.beginPath(); ctx.moveTo(x[0], y[1]); ctx.lineTo(x[3], y[1]); ctx.moveTo(x[0], y[2]); ctx.lineTo(x[3], y[2]); ctx.moveTo(x[1], y[0]); ctx.lineTo(x[1], y[3]); ctx.moveTo(x[2], y[0]); ctx.lineTo(x[2], y[3]); ctx.stroke(); ctx.closePath(); ctx.restore(); }, /** * Calculate Inner Position * @param {Array} outer - outer position * @param {number} size - interval for calculate * @returns {Array} - inner position * @private */ _caculateInnerPosition(outer, size) { const position = []; position[0] = outer[1]; position[1] = outer[1] + size; position[2] = outer[1] + size * 2; position[3] = outer[2]; return position; }, /** * Get coordinates * @returns {cropzoneCoordinates} - {@link cropzoneCoordinates} * @private */ _getCoordinates() { const { canvas, width, height, left, top } = this; const halfWidth = width / 2; const halfHeight = height / 2; const canvasHeight = canvas.getHeight(); // fabric object const canvasWidth = canvas.getWidth(); // fabric object return { x: [ -(halfWidth + left), // x0 -halfWidth, // x1 halfWidth, // x2 halfWidth + (canvasWidth - left - width), // x3 ].map(Math.ceil), y: [ -(halfHeight + top), // y0 -halfHeight, // y1 halfHeight, // y2 halfHeight + (canvasHeight - top - height), // y3 ].map(Math.ceil), }; }, /** * Stroke border * @param {CanvasRenderingContext2D} ctx - Context * @param {string|CanvasGradient|CanvasPattern} strokeStyle - Stroke-style * @param {number} lineDashWidth - Dash width * @param {number} [lineDashOffset] - Dash offset * @param {number} [lineWidth] - line width * @private */ _strokeBorder(ctx, strokeStyle, { lineDashWidth, lineDashOffset, lineWidth }) { const halfWidth = this.width / 2; const halfHeight = this.height / 2; ctx.save(); ctx.strokeStyle = strokeStyle; if (ctx.setLineDash) { ctx.setLineDash([lineDashWidth, lineDashWidth]); } if (lineDashOffset) { ctx.lineDashOffset = lineDashOffset; } if (lineWidth) { ctx.lineWidth = lineWidth; } ctx.beginPath(); ctx.moveTo(-halfWidth, -halfHeight); ctx.lineTo(halfWidth, -halfHeight); ctx.lineTo(halfWidth, halfHeight); ctx.lineTo(-halfWidth, halfHeight); ctx.lineTo(-halfWidth, -halfHeight); ctx.stroke(); ctx.restore(); }, /** * onMoving event listener * @private */ _onMoving() { const { height, width, left, top } = this; const maxLeft = this.canvas.getWidth() - width; const maxTop = this.canvas.getHeight() - height; this.left = clamp(left, 0, maxLeft); this.top = clamp(top, 0, maxTop); this.canvasEventTrigger[events.OBJECT_MOVED](this); }, /** * onScaling event listener * @param {{e: MouseEvent}} fEvent - Fabric event * @private */ _onScaling(fEvent) { const selectedCorner = fEvent.transform.corner; const pointer = this.canvas.getPointer(fEvent.e); const settings = this._calcScalingSizeFromPointer(pointer, selectedCorner); // On scaling cropzone, // change real width and height and fix scaleFactor to 1 this.scale(1).set(settings); this.canvasEventTrigger[events.OBJECT_SCALED](this); }, /** * Calc scaled size from mouse pointer with selected corner * @param {{x: number, y: number}} pointer - Mouse position * @param {string} selectedCorner - selected corner type * @returns {Object} Having left or(and) top or(and) width or(and) height. * @private */ _calcScalingSizeFromPointer(pointer, selectedCorner) { const isCornerTypeValid = cornerTypeValid(selectedCorner); return isCornerTypeValid && this._resizeCropZone(pointer, selectedCorner); }, /** * Align with cropzone ratio * @param {number} width - cropzone width * @param {number} height - cropzone height * @param {number} maxWidth - limit max width * @param {number} maxHeight - limit max height * @param {number} scaleTo - cropzone ratio * @returns {{width: number, height: number}} * @private */ adjustRatioCropzoneSize({ width, height, leftMaker, topMaker, maxWidth, maxHeight, scaleTo }) { width = maxWidth ? clamp(width, 1, maxWidth) : width; height = maxHeight ? clamp(height, 1, maxHeight) : height; if (!this.presetRatio) { if (this._withShiftKey) { // make fixed ratio cropzone if (width > height) { height = width; } else if (height > width) { width = height; } } return { width, height, left: leftMaker(width), top: topMaker(height), }; } if (scaleTo === 'width') { height = width / this.presetRatio; } else { width = height * this.presetRatio; } const maxScaleFactor = Math.min(maxWidth / width, maxHeight / height); if (maxScaleFactor <= 1) { [width, height] = [width, height].map((v) => v * maxScaleFactor); } return { width, height, left: leftMaker(width), top: topMaker(height), }; }, /** * Get dimension last state cropzone * @returns {{rectTop: number, rectLeft: number, rectWidth: number, rectHeight: number}} * @private */ _getCropzoneRectInfo() { const { width: canvasWidth, height: canvasHeight } = this.canvas; const { top: rectTop, left: rectLeft, width: rectWidth, height: rectHeight, } = this.getBoundingRect(false, true); return { rectTop, rectLeft, rectWidth, rectHeight, rectRight: rectLeft + rectWidth, rectBottom: rectTop + rectHeight, canvasWidth, canvasHeight, }; }, /** * Calc scaling dimension * @param {Object} position - Mouse position * @param {string} corner - corner type * @returns {{left: number, top: number, width: number, height: number}} * @private */ _resizeCropZone({ x, y }, corner) { const { rectWidth, rectHeight, rectTop, rectLeft, rectBottom, rectRight, canvasWidth, canvasHeight, } = this._getCropzoneRectInfo(); const resizeInfoMap = { tl: { width: rectRight - x, height: rectBottom - y, leftMaker: (newWidth) => rectRight - newWidth, topMaker: (newHeight) => rectBottom - newHeight, maxWidth: rectRight, maxHeight: rectBottom, scaleTo: getScaleBasis(rectLeft - x, rectTop - y), }, tr: { width: x - rectLeft, height: rectBottom - y, leftMaker: () => rectLeft, topMaker: (newHeight) => rectBottom - newHeight, maxWidth: canvasWidth - rectLeft, maxHeight: rectBottom, scaleTo: getScaleBasis(x - rectRight, rectTop - y), }, mt: { width: rectWidth, height: rectBottom - y, leftMaker: () => rectLeft, topMaker: (newHeight) => rectBottom - newHeight, maxWidth: canvasWidth - rectLeft, maxHeight: rectBottom, scaleTo: 'height', }, ml: { width: rectRight - x, height: rectHeight, leftMaker: (newWidth) => rectRight - newWidth, topMaker: () => rectTop, maxWidth: rectRight, maxHeight: canvasHeight - rectTop, scaleTo: 'width', }, mr: { width: x - rectLeft, height: rectHeight, leftMaker: () => rectLeft, topMaker: () => rectTop, maxWidth: canvasWidth - rectLeft, maxHeight: canvasHeight - rectTop, scaleTo: 'width', }, mb: { width: rectWidth, height: y - rectTop, leftMaker: () => rectLeft, topMaker: () => rectTop, maxWidth: canvasWidth - rectLeft, maxHeight: canvasHeight - rectTop, scaleTo: 'height', }, bl: { width: rectRight - x, height: y - rectTop, leftMaker: (newWidth) => rectRight - newWidth, topMaker: () => rectTop, maxWidth: rectRight, maxHeight: canvasHeight - rectTop, scaleTo: getScaleBasis(rectLeft - x, y - rectBottom), }, br: { width: x - rectLeft, height: y - rectTop, leftMaker: () => rectLeft, topMaker: () => rectTop, maxWidth: canvasWidth - rectLeft, maxHeight: canvasHeight - rectTop, scaleTo: getScaleBasis(x - rectRight, y - rectBottom), }, }; return this.adjustRatioCropzoneSize(resizeInfoMap[corner]); }, /** * Return the whether this cropzone is valid * @returns {boolean} */ isValid() { return this.left >= 0 && this.top >= 0 && this.width > 0 && this.height > 0; }, /** * Keydown event handler * @param {{number}} keyCode - Event keyCode * @private */ _onKeyDown({ keyCode }) { if (keyCode === keyCodes.SHIFT) { this._withShiftKey = true; } }, /** * Keyup event handler * @param {{number}} keyCode - Event keyCode * @private */ _onKeyUp({ keyCode }) { if (keyCode === keyCodes.SHIFT) { this._withShiftKey = false; } }, } ); export default Cropzone; ================================================ FILE: apps/image-editor/src/js/extension/emboss.js ================================================ import { fabric } from 'fabric'; /** * Emboss object * @class Emboss * @extends {fabric.Image.filters.Convolute} * @ignore */ const Emboss = fabric.util.createClass( fabric.Image.filters.Convolute, /** @lends Convolute.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Emboss', /** * constructor * @override */ initialize() { this.matrix = [1, 1, 1, 1, 0.7, -1, -1, -1, -1]; }, } ); export default Emboss; ================================================ FILE: apps/image-editor/src/js/extension/mask.js ================================================ import { fabric } from 'fabric'; /** * Mask object * @class Mask * @extends {fabric.Image.filters.BlendImage} * @ignore */ const Mask = fabric.util.createClass( fabric.Image.filters.BlendImage, /** @lends Mask.prototype */ { /** * Apply filter to canvas element * @param {Object} pipelineState - Canvas element to apply filter * @override */ applyTo(pipelineState) { if (!this.mask) { return; } const canvas = pipelineState.canvasEl; const { width, height } = canvas; const maskCanvasEl = this._createCanvasOfMask(width, height); const ctx = canvas.getContext('2d'); const maskCtx = maskCanvasEl.getContext('2d'); const imageData = ctx.getImageData(0, 0, width, height); this._drawMask(maskCtx, canvas, ctx); this._mapData(maskCtx, imageData, width, height); pipelineState.imageData = imageData; }, /** * Create canvas of mask image * @param {number} width - Width of main canvas * @param {number} height - Height of main canvas * @returns {HTMLElement} Canvas element * @private */ _createCanvasOfMask(width, height) { const maskCanvasEl = fabric.util.createCanvasElement(); maskCanvasEl.width = width; maskCanvasEl.height = height; return maskCanvasEl; }, /** * Draw mask image on canvas element * @param {Object} maskCtx - Context of mask canvas * @private */ _drawMask(maskCtx) { const { mask } = this; const maskImg = mask.getElement(); const { angle, left, scaleX, scaleY, top } = mask; maskCtx.save(); maskCtx.translate(left, top); maskCtx.rotate((angle * Math.PI) / 180); maskCtx.scale(scaleX, scaleY); maskCtx.drawImage(maskImg, -maskImg.width / 2, -maskImg.height / 2); maskCtx.restore(); }, /** * Map mask image data to source image data * @param {Object} maskCtx - Context of mask canvas * @param {Object} imageData - Data of source image * @param {number} width - Width of main canvas * @param {number} height - Height of main canvas * @private */ _mapData(maskCtx, imageData, width, height) { const { data, height: imgHeight, width: imgWidth } = imageData; const sourceData = data; const len = imgWidth * imgHeight * 4; const maskData = maskCtx.getImageData(0, 0, width, height).data; for (let i = 0; i < len; i += 4) { sourceData[i + 3] = maskData[i]; // adjust value of alpha data } }, } ); export default Mask; ================================================ FILE: apps/image-editor/src/js/extension/sharpen.js ================================================ import { fabric } from 'fabric'; /** * Sharpen object * @class Sharpen * @extends {fabric.Image.filters.Convolute} * @ignore */ const Sharpen = fabric.util.createClass( fabric.Image.filters.Convolute, /** @lends Convolute.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Sharpen', /** * constructor * @override */ initialize() { this.matrix = [0, -1, 0, -1, 5, -1, 0, -1, 0]; }, } ); export default Sharpen; ================================================ FILE: apps/image-editor/src/js/factory/command.js ================================================ import Command from '@/interface/command'; const commands = {}; /** * Create a command * @param {string} name - Command name * @param {...*} args - Arguments for creating command * @returns {Command} * @ignore */ function create(name, ...args) { const actions = commands[name]; if (actions) { return new Command(actions, args); } return null; } /** * Register a command with name as a key * @param {Object} command - {name:{string}, execute: {function}, undo: {function}} * @param {string} command.name - command name * @param {function} command.execute - executable function * @param {function} command.undo - undo function * @ignore */ function register(command) { commands[command.name] = command; } export default { create, register, }; ================================================ FILE: apps/image-editor/src/js/factory/errorMessage.js ================================================ import extend from 'tui-code-snippet/object/extend'; import { keyMirror } from '@/util'; const types = keyMirror('UN_IMPLEMENTATION', 'NO_COMPONENT_NAME'); const messages = { UN_IMPLEMENTATION: 'Should implement a method: ', NO_COMPONENT_NAME: 'Should set a component name', }; const map = { UN_IMPLEMENTATION(methodName) { return messages.UN_IMPLEMENTATION + methodName; }, NO_COMPONENT_NAME() { return messages.NO_COMPONENT_NAME; }, }; export default { types: extend({}, types), create(type, ...args) { type = type.toLowerCase(); const func = map[type]; return func(...args); }, }; ================================================ FILE: apps/image-editor/src/js/graphics.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import isArray from 'tui-code-snippet/type/isArray'; import isString from 'tui-code-snippet/type/isString'; import forEachArray from 'tui-code-snippet/collection/forEachArray'; import forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties'; import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; import ImageLoader from '@/component/imageLoader'; import Cropper from '@/component/cropper'; import Flip from '@/component/flip'; import Rotation from '@/component/rotation'; import FreeDrawing from '@/component/freeDrawing'; import Line from '@/component/line'; import Text from '@/component/text'; import Icon from '@/component/icon'; import Filter from '@/component/filter'; import Shape from '@/component/shape'; import Zoom from '@/component/zoom'; import CropperDrawingMode from '@/drawingMode/cropper'; import FreeDrawingMode from '@/drawingMode/freeDrawing'; import LineDrawingMode from '@/drawingMode/lineDrawing'; import ShapeDrawingMode from '@/drawingMode/shape'; import TextDrawingMode from '@/drawingMode/text'; import IconDrawingMode from '@/drawingMode/icon'; import ZoomDrawingMode from '@/drawingMode/zoom'; import { makeSelectionUndoData, makeSelectionUndoDatum, setCachedUndoDataForDimension, } from '@/helper/selectionModifyHelper'; import { getProperties, includes, isShape, stamp } from '@/util'; import { componentNames as components, eventNames as events, drawingModes, fObjectOptions, } from '@/consts'; import Resize from '@/component/resize'; import ResizeDrawingMode from '@/drawingMode/resize'; const DEFAULT_CSS_MAX_WIDTH = 1000; const DEFAULT_CSS_MAX_HEIGHT = 800; const EXTRA_PX_FOR_PASTE = 10; const cssOnly = { cssOnly: true, }; const backstoreOnly = { backstoreOnly: true, }; /** * Graphics class * @class * @param {string|HTMLElement} wrapper - Wrapper's element or selector * @param {Object} [option] - Canvas max width & height of css * @param {number} option.cssMaxWidth - Canvas css-max-width * @param {number} option.cssMaxHeight - Canvas css-max-height * @ignore */ class Graphics { constructor(element, { cssMaxWidth, cssMaxHeight } = {}) { /** * Fabric image instance * @type {fabric.Image} */ this.canvasImage = null; /** * Max width of canvas elements * @type {number} */ this.cssMaxWidth = cssMaxWidth || DEFAULT_CSS_MAX_WIDTH; /** * Max height of canvas elements * @type {number} */ this.cssMaxHeight = cssMaxHeight || DEFAULT_CSS_MAX_HEIGHT; /** * cropper Selection Style * @type {Object} */ this.cropSelectionStyle = {}; /** * target fabric object for copy paste feature * @type {fabric.Object} * @private */ this.targetObjectForCopyPaste = null; /** * Image name * @type {string} */ this.imageName = ''; /** * Object Map * @type {Object} * @private */ this._objects = {}; /** * Fabric-Canvas instance * @type {fabric.Canvas} * @private */ this._canvas = null; /** * Drawing mode * @type {string} * @private */ this._drawingMode = drawingModes.NORMAL; /** * DrawingMode map * @type {Object.} * @private */ this._drawingModeMap = {}; /** * Component map * @type {Object.} * @private */ this._componentMap = {}; /** * fabric event handlers * @type {Object.} * @private */ this._handler = { onMouseDown: this._onMouseDown.bind(this), onObjectAdded: this._onObjectAdded.bind(this), onObjectRemoved: this._onObjectRemoved.bind(this), onObjectMoved: this._onObjectMoved.bind(this), onObjectScaled: this._onObjectScaled.bind(this), onObjectModified: this._onObjectModified.bind(this), onObjectRotated: this._onObjectRotated.bind(this), onObjectSelected: this._onObjectSelected.bind(this), onPathCreated: this._onPathCreated.bind(this), onSelectionCleared: this._onSelectionCleared.bind(this), onSelectionCreated: this._onSelectionCreated.bind(this), }; this._setObjectCachingToFalse(); this._setCanvasElement(element); this._createDrawingModeInstances(); this._createComponents(); this._attachCanvasEvents(); this._attachZoomEvents(); } /** * Destroy canvas element */ destroy() { const { wrapperEl } = this._canvas; this._canvas.clear(); wrapperEl.parentNode.removeChild(wrapperEl); this._detachZoomEvents(); } /** * Attach zoom events */ _attachZoomEvents() { const zoom = this.getComponent(components.ZOOM); zoom.attachKeyboardZoomEvents(); } /** * Detach zoom events */ _detachZoomEvents() { const zoom = this.getComponent(components.ZOOM); zoom.detachKeyboardZoomEvents(); } /** * Deactivates all objects on canvas * @returns {Graphics} this */ deactivateAll() { this._canvas.discardActiveObject(); return this; } /** * Renders all objects on canvas * @returns {Graphics} this */ renderAll() { this._canvas.renderAll(); return this; } /** * Adds objects on canvas * @param {Object|Array} objects - objects */ add(objects) { let theArgs = []; if (isArray(objects)) { theArgs = objects; } else { theArgs.push(objects); } this._canvas.add(...theArgs); } /** * Removes the object or group * @param {Object} target - graphics object or group * @returns {boolean} true if contains or false */ contains(target) { return this._canvas.contains(target); } /** * Gets all objects or group * @returns {Array} all objects, shallow copy */ getObjects() { return this._canvas.getObjects().slice(); } /** * Get an object by id * @param {number} id - object id * @returns {fabric.Object} object corresponding id */ getObject(id) { return this._objects[id]; } /** * Removes the object or group * @param {Object} target - graphics object or group */ remove(target) { this._canvas.remove(target); } /** * Removes all object or group * @param {boolean} includesBackground - remove the background image or not * @returns {Array} all objects array which is removed */ removeAll(includesBackground) { const canvas = this._canvas; const objects = canvas.getObjects().slice(); canvas.remove(...this._canvas.getObjects()); if (includesBackground) { canvas.clear(); } return objects; } /** * Removes an object or group by id * @param {number} id - object id * @returns {Array} removed objects */ removeObjectById(id) { const objects = []; const canvas = this._canvas; const target = this.getObject(id); const isValidGroup = target && target.isType('group') && !target.isEmpty(); if (isValidGroup) { canvas.discardActiveObject(); // restore states for each objects target.forEachObject((obj) => { objects.push(obj); canvas.remove(obj); }); } else if (canvas.contains(target)) { objects.push(target); canvas.remove(target); } return objects; } /** * Get an id by object instance * @param {fabric.Object} object object * @returns {number} object id if it exists or null */ getObjectId(object) { let key = null; for (key in this._objects) { if (this._objects.hasOwnProperty(key)) { if (object === this._objects[key]) { return key; } } } return null; } /** * Gets an active object or group * @returns {Object} active object or group instance */ getActiveObject() { return this._canvas._activeObject; } /** * Returns the object ID to delete the object. * @returns {number} object id for remove */ getActiveObjectIdForRemove() { const activeObject = this.getActiveObject(); const { type, left, top } = activeObject; const isSelection = type === 'activeSelection'; if (isSelection) { const group = new fabric.Group([...activeObject.getObjects()], { left, top, }); return this._addFabricObject(group); } return this.getObjectId(activeObject); } /** * Verify that you are ready to erase the object. * @returns {boolean} ready for object remove */ isReadyRemoveObject() { const activeObject = this.getActiveObject(); return activeObject && !activeObject.isEditing; } /** * Gets an active group object * @returns {Object} active group object instance */ getActiveObjects() { const activeObject = this._canvas._activeObject; return activeObject && activeObject.type === 'activeSelection' ? activeObject : null; } /** * Get Active object Selection from object ids * @param {Array.} objects - fabric objects * @returns {Object} target - target object group */ getActiveSelectionFromObjects(objects) { const canvas = this.getCanvas(); return new fabric.ActiveSelection(objects, { canvas }); } /** * Activates an object or group * @param {Object} target - target object or group */ setActiveObject(target) { this._canvas.setActiveObject(target); } /** * Set Crop selection style * @param {Object} style - Selection styles */ setCropSelectionStyle(style) { this.cropSelectionStyle = style; } /** * Get component * @param {string} name - Component name * @returns {Component} */ getComponent(name) { return this._componentMap[name]; } /** * Get current drawing mode * @returns {string} */ getDrawingMode() { return this._drawingMode; } /** * Start a drawing mode. If the current mode is not 'NORMAL', 'stopDrawingMode()' will be called first. * @param {String} mode Can be one of 'CROPPER', 'FREE_DRAWING', 'LINE', 'TEXT', 'SHAPE' * @param {Object} [option] parameters of drawing mode, it's available with 'FREE_DRAWING', 'LINE_DRAWING' * @param {Number} [option.width] brush width * @param {String} [option.color] brush color * @returns {boolean} true if success or false */ startDrawingMode(mode, option) { if (this._isSameDrawingMode(mode)) { return true; } // If the current mode is not 'NORMAL', 'stopDrawingMode()' will be called first. this.stopDrawingMode(); const drawingModeInstance = this._getDrawingModeInstance(mode); if (drawingModeInstance && drawingModeInstance.start) { drawingModeInstance.start(this, option); this._drawingMode = mode; } return !!drawingModeInstance; } /** * Stop the current drawing mode and back to the 'NORMAL' mode */ stopDrawingMode() { if (this._isSameDrawingMode(drawingModes.NORMAL)) { return; } const drawingModeInstance = this._getDrawingModeInstance(this.getDrawingMode()); if (drawingModeInstance && drawingModeInstance.end) { drawingModeInstance.end(this); } this._drawingMode = drawingModes.NORMAL; } /** * Change zoom of canvas * @param {{x: number, y: number}} center - center of zoom * @param {number} zoomLevel - zoom level */ zoom({ x, y }, zoomLevel) { const zoom = this.getComponent(components.ZOOM); zoom.zoom({ x, y }, zoomLevel); } /** * Get zoom mode * @returns {string} */ getZoomMode() { const zoom = this.getComponent(components.ZOOM); return zoom.mode; } /** * Start zoom-in mode */ startZoomInMode() { const zoom = this.getComponent(components.ZOOM); zoom.startZoomInMode(); } /** * Stop zoom-in mode */ endZoomInMode() { const zoom = this.getComponent(components.ZOOM); zoom.endZoomInMode(); } /** * Zoom out one step */ zoomOut() { const zoom = this.getComponent(components.ZOOM); zoom.zoomOut(); } /** * Start hand mode */ startHandMode() { const zoom = this.getComponent(components.ZOOM); zoom.startHandMode(); } /** * Stop hand mode */ endHandMode() { const zoom = this.getComponent(components.ZOOM); zoom.endHandMode(); } /** * Zoom reset */ resetZoom() { const zoom = this.getComponent(components.ZOOM); zoom.resetZoom(); } /** * To data url from canvas * @param {Object} options - options for toDataURL * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in fabric v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in fabric v1.2.14 * @param {Number} [options.width] Cropping width. Introduced in fabric v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in fabric v1.2.14 * @returns {string} A DOMString containing the requested data URI. */ toDataURL(options) { const cropper = this.getComponent(components.CROPPER); cropper.changeVisibility(false); const dataUrl = this._canvas && this._canvas.toDataURL(options); cropper.changeVisibility(true); return dataUrl; } /** * Save image(background) of canvas * @param {string} name - Name of image * @param {?fabric.Image} canvasImage - Fabric image instance */ setCanvasImage(name, canvasImage) { if (canvasImage) { stamp(canvasImage); } this.imageName = name; this.canvasImage = canvasImage; } /** * Set css max dimension * @param {{width: number, height: number}} maxDimension - Max width & Max height */ setCssMaxDimension(maxDimension) { this.cssMaxWidth = maxDimension.width || this.cssMaxWidth; this.cssMaxHeight = maxDimension.height || this.cssMaxHeight; } /** * Adjust canvas dimension with scaling image */ adjustCanvasDimension() { this.adjustCanvasDimensionBase(this.canvasImage.scale(1)); } adjustCanvasDimensionBase(canvasImage = null) { if (!canvasImage) { canvasImage = this.canvasImage; } const { width, height } = canvasImage.getBoundingRect(); const maxDimension = this._calcMaxDimension(width, height); this.setCanvasCssDimension({ width: '100%', height: '100%', // Set height '' for IE9 'max-width': `${maxDimension.width}px`, 'max-height': `${maxDimension.height}px`, }); this.setCanvasBackstoreDimension({ width, height, }); this._canvas.centerObject(canvasImage); } /** * Set canvas dimension - css only * {@link http://fabricjs.com/docs/fabric.Canvas.html#setDimensions} * @param {Object} dimension - Canvas css dimension */ setCanvasCssDimension(dimension) { this._canvas.setDimensions(dimension, cssOnly); } /** * Set canvas dimension - backstore only * {@link http://fabricjs.com/docs/fabric.Canvas.html#setDimensions} * @param {Object} dimension - Canvas backstore dimension */ setCanvasBackstoreDimension(dimension) { this._canvas.setDimensions(dimension, backstoreOnly); } /** * Set image properties * {@link http://fabricjs.com/docs/fabric.Image.html#set} * @param {Object} setting - Image properties * @param {boolean} [withRendering] - If true, The changed image will be reflected in the canvas */ setImageProperties(setting, withRendering) { const { canvasImage } = this; if (!canvasImage) { return; } canvasImage.set(setting).setCoords(); if (withRendering) { this._canvas.renderAll(); } } /** * Returns canvas element of fabric.Canvas[[lower-canvas]] * @returns {HTMLCanvasElement} */ getCanvasElement() { return this._canvas.getElement(); } /** * Get fabric.Canvas instance * @returns {fabric.Canvas} */ getCanvas() { return this._canvas; } /** * Get canvasImage (fabric.Image instance) * @returns {fabric.Image} */ getCanvasImage() { return this.canvasImage; } /** * Get image name * @returns {string} */ getImageName() { return this.imageName; } /** * Add image object on canvas * @param {string} imgUrl - Image url to make object * @returns {Promise} */ addImageObject(imgUrl) { const callback = this._callbackAfterLoadingImageObject.bind(this); return new Promise((resolve) => { fabric.Image.fromURL( imgUrl, (image) => { callback(image); resolve(this.createObjectProperties(image)); }, { crossOrigin: 'Anonymous', } ); }); } /** * Get center position of canvas * @returns {Object} {left, top} */ getCenter() { return this._canvas.getCenter(); } /** * Get cropped rect * @returns {Object} rect */ getCropzoneRect() { return this.getComponent(components.CROPPER).getCropzoneRect(); } /** * Get cropped rect * @param {number} [mode] cropzone rect mode */ setCropzoneRect(mode) { this.getComponent(components.CROPPER).setCropzoneRect(mode); } /** * Get cropped image data * @param {Object} cropRect cropzone rect * @param {Number} cropRect.left left position * @param {Number} cropRect.top top position * @param {Number} cropRect.width width * @param {Number} cropRect.height height * @returns {?{imageName: string, url: string}} cropped Image data */ getCroppedImageData(cropRect) { return this.getComponent(components.CROPPER).getCroppedImageData(cropRect); } /** * Set brush option * @param {Object} option brush option * @param {Number} option.width width * @param {String} option.color color like 'FFFFFF', 'rgba(0, 0, 0, 0.5)' */ setBrush(option) { const drawingMode = this._drawingMode; let compName = components.FREE_DRAWING; if (drawingMode === drawingModes.LINE_DRAWING) { compName = components.LINE; } this.getComponent(compName).setBrush(option); } /** * Set states of current drawing shape * @param {string} type - Shape type (ex: 'rect', 'circle', 'triangle') * @param {Object} [options] - Shape options * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or * Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stoke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {number} [options.isRegular] - Whether resizing shape has 1:1 ratio or not */ setDrawingShape(type, options) { this.getComponent(components.SHAPE).setStates(type, options); } /** * Set style of current drawing icon * @param {string} type - icon type (ex: 'icon-arrow', 'icon-star') * @param {Object} [iconColor] - Icon color */ setIconStyle(type, iconColor) { this.getComponent(components.ICON).setStates(type, iconColor); } /** * Register icon paths * @param {Object} pathInfos - Path infos * @param {string} pathInfos.key - key * @param {string} pathInfos.value - value */ registerPaths(pathInfos) { this.getComponent(components.ICON).registerPaths(pathInfos); } /** * Change cursor style * @param {string} cursorType - cursor type */ changeCursor(cursorType) { const canvas = this.getCanvas(); canvas.defaultCursor = cursorType; canvas.renderAll(); } /** * Whether it has the filter or not * @param {string} type - Filter type * @returns {boolean} true if it has the filter */ hasFilter(type) { return this.getComponent(components.FILTER).hasFilter(type); } /** * Set selection style of fabric object by init option * @param {Object} styles - Selection styles */ setSelectionStyle(styles) { extend(fObjectOptions.SELECTION_STYLE, styles); } /** * Set object properties * @param {number} id - object id * @param {Object} props - props * @param {string} [props.fill] Color * @param {string} [props.fontFamily] Font type for text * @param {number} [props.fontSize] Size * @param {string} [props.fontStyle] Type of inclination (normal / italic) * @param {string} [props.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [props.textAlign] Type of text align (left / center / right) * @param {string} [props.textDecoration] Type of line (underline / line-through / overline) * @returns {Object} applied properties */ setObjectProperties(id, props) { const object = this.getObject(id); const clone = extend({}, props); object.set(clone); object.setCoords(); this.getCanvas().renderAll(); return clone; } /** * Get object properties corresponding key * @param {number} id - object id * @param {Array|ObjectProps|string} keys - property's key * @returns {Object} properties */ getObjectProperties(id, keys) { const object = this.getObject(id); const props = {}; if (isString(keys)) { props[keys] = object[keys]; } else if (isArray(keys)) { forEachArray(keys, (value) => { props[value] = object[value]; }); } else { forEachOwnProperties(keys, (value, key) => { props[key] = object[key]; }); } return props; } /** * Get object position by originX, originY * @param {number} id - object id * @param {string} originX - can be 'left', 'center', 'right' * @param {string} originY - can be 'top', 'center', 'bottom' * @returns {Object} {{x:number, y: number}} position by origin if id is valid, or null */ getObjectPosition(id, originX, originY) { const targetObj = this.getObject(id); if (!targetObj) { return null; } return targetObj.getPointByOrigin(originX, originY); } /** * Set object position by originX, originY * @param {number} id - object id * @param {Object} posInfo - position object * @param {number} posInfo.x - x position * @param {number} posInfo.y - y position * @param {string} posInfo.originX - can be 'left', 'center', 'right' * @param {string} posInfo.originY - can be 'top', 'center', 'bottom' * @returns {boolean} true if target id is valid or false */ setObjectPosition(id, posInfo) { const targetObj = this.getObject(id); const { x, y, originX, originY } = posInfo; if (!targetObj) { return false; } const targetOrigin = targetObj.getPointByOrigin(originX, originY); const centerOrigin = targetObj.getPointByOrigin('center', 'center'); const diffX = centerOrigin.x - targetOrigin.x; const diffY = centerOrigin.y - targetOrigin.y; targetObj.set({ left: x + diffX, top: y + diffY, }); targetObj.setCoords(); return true; } /** * Get the canvas size * @returns {Object} {{width: number, height: number}} image size */ getCanvasSize() { const image = this.getCanvasImage(); return { width: image ? image.width : 0, height: image ? image.height : 0, }; } /** * Create fabric static canvas * @returns {Object} {{width: number, height: number}} image size */ createStaticCanvas() { const staticCanvas = new fabric.StaticCanvas(); staticCanvas.set({ enableRetinaScaling: false, }); return staticCanvas; } /** * Get a DrawingMode instance * @param {string} modeName - DrawingMode Class Name * @returns {DrawingMode} DrawingMode instance * @private */ _getDrawingModeInstance(modeName) { return this._drawingModeMap[modeName]; } /** * Set object caching to false. This brought many bugs when draw Shape & cropzone * @see http://fabricjs.com/fabric-object-caching * @private */ _setObjectCachingToFalse() { fabric.Object.prototype.objectCaching = false; } /** * Set canvas element to fabric.Canvas * @param {Element|string} element - Wrapper or canvas element or selector * @private */ _setCanvasElement(element) { let selectedElement; let canvasElement; if (element.nodeType) { selectedElement = element; } else { selectedElement = document.querySelector(element); } if (selectedElement.nodeName.toUpperCase() !== 'CANVAS') { canvasElement = document.createElement('canvas'); selectedElement.appendChild(canvasElement); } this._canvas = new fabric.Canvas(canvasElement, { containerClass: 'tui-image-editor-canvas-container', enableRetinaScaling: false, }); } /** * Creates DrawingMode instances * @private */ _createDrawingModeInstances() { this._register(this._drawingModeMap, new CropperDrawingMode()); this._register(this._drawingModeMap, new FreeDrawingMode()); this._register(this._drawingModeMap, new LineDrawingMode()); this._register(this._drawingModeMap, new ShapeDrawingMode()); this._register(this._drawingModeMap, new TextDrawingMode()); this._register(this._drawingModeMap, new IconDrawingMode()); this._register(this._drawingModeMap, new ZoomDrawingMode()); this._register(this._drawingModeMap, new ResizeDrawingMode()); } /** * Create components * @private */ _createComponents() { this._register(this._componentMap, new ImageLoader(this)); this._register(this._componentMap, new Cropper(this)); this._register(this._componentMap, new Flip(this)); this._register(this._componentMap, new Rotation(this)); this._register(this._componentMap, new FreeDrawing(this)); this._register(this._componentMap, new Line(this)); this._register(this._componentMap, new Text(this)); this._register(this._componentMap, new Icon(this)); this._register(this._componentMap, new Filter(this)); this._register(this._componentMap, new Shape(this)); this._register(this._componentMap, new Zoom(this)); this._register(this._componentMap, new Resize(this)); } /** * Register component * @param {Object} map - map object * @param {Object} module - module which has getName method * @private */ _register(map, module) { map[module.getName()] = module; } /** * Get the current drawing mode is same with given mode * @param {string} mode drawing mode * @returns {boolean} true if same or false */ _isSameDrawingMode(mode) { return this.getDrawingMode() === mode; } /** * Calculate max dimension of canvas * The css-max dimension is dynamically decided with maintaining image ratio * The css-max dimension is lower than canvas dimension (attribute of canvas, not css) * @param {number} width - Canvas width * @param {number} height - Canvas height * @returns {{width: number, height: number}} - Max width & Max height * @private */ _calcMaxDimension(width, height) { const wScaleFactor = this.cssMaxWidth / width; const hScaleFactor = this.cssMaxHeight / height; let cssMaxWidth = Math.min(width, this.cssMaxWidth); let cssMaxHeight = Math.min(height, this.cssMaxHeight); if (wScaleFactor < 1 && wScaleFactor < hScaleFactor) { cssMaxWidth = width * wScaleFactor; cssMaxHeight = height * wScaleFactor; } else if (hScaleFactor < 1 && hScaleFactor < wScaleFactor) { cssMaxWidth = width * hScaleFactor; cssMaxHeight = height * hScaleFactor; } return { width: Math.floor(cssMaxWidth), height: Math.floor(cssMaxHeight), }; } /** * Callback function after loading image * @param {fabric.Image} obj - Fabric image object * @private */ _callbackAfterLoadingImageObject(obj) { const centerPos = this.getCanvasImage().getCenterPoint(); obj.set(fObjectOptions.SELECTION_STYLE); obj.set({ left: centerPos.x, top: centerPos.y, crossOrigin: 'Anonymous', }); this.getCanvas().add(obj).setActiveObject(obj); } /** * Attach canvas's events */ _attachCanvasEvents() { const canvas = this._canvas; const handler = this._handler; canvas.on({ 'mouse:down': handler.onMouseDown, 'object:added': handler.onObjectAdded, 'object:removed': handler.onObjectRemoved, 'object:moving': handler.onObjectMoved, 'object:scaling': handler.onObjectScaled, 'object:modified': handler.onObjectModified, 'object:rotating': handler.onObjectRotated, 'path:created': handler.onPathCreated, 'selection:cleared': handler.onSelectionCleared, 'selection:created': handler.onSelectionCreated, 'selection:updated': handler.onObjectSelected, }); } /** * "mouse:down" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onMouseDown(fEvent) { const { e: event, target } = fEvent; const originPointer = this._canvas.getPointer(event); if (target) { const { type } = target; const undoData = makeSelectionUndoData(target, (item) => makeSelectionUndoDatum(this.getObjectId(item), item, type === 'activeSelection') ); setCachedUndoDataForDimension(undoData); } this.fire(events.MOUSE_DOWN, event, originPointer); } /** * "object:added" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onObjectAdded(fEvent) { const obj = fEvent.target; if (obj.isType('cropzone')) { return; } this._addFabricObject(obj); } /** * "object:removed" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onObjectRemoved(fEvent) { const obj = fEvent.target; this._removeFabricObject(stamp(obj)); } /** * "object:moving" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onObjectMoved(fEvent) { this._lazyFire( events.OBJECT_MOVED, (object) => this.createObjectProperties(object), fEvent.target ); } /** * "object:scaling" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onObjectScaled(fEvent) { this._lazyFire( events.OBJECT_SCALED, (object) => this.createObjectProperties(object), fEvent.target ); } /** * "object:modified" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onObjectModified(fEvent) { const { target } = fEvent; if (target.type === 'activeSelection') { const items = target.getObjects(); items.forEach((item) => item.fire('modifiedInGroup', target)); } this.fire(events.OBJECT_MODIFIED, target, this.getObjectId(target)); } /** * "object:rotating" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onObjectRotated(fEvent) { this._lazyFire( events.OBJECT_ROTATED, (object) => this.createObjectProperties(object), fEvent.target ); } /** * Lazy event emitter * @param {string} eventName - event name * @param {Function} paramsMaker - make param function * @param {Object} [target] - Object of the event owner. * @private */ _lazyFire(eventName, paramsMaker, target) { const existEventDelegation = target && target.canvasEventDelegation; const delegationState = existEventDelegation ? target.canvasEventDelegation(eventName) : 'none'; if (delegationState === 'unregistered') { target.canvasEventRegister(eventName, (object) => { this.fire(eventName, paramsMaker(object)); }); } if (delegationState === 'none') { this.fire(eventName, paramsMaker(target)); } } /** * "object:selected" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onObjectSelected(fEvent) { const { target } = fEvent; const params = this.createObjectProperties(target); this.fire(events.OBJECT_ACTIVATED, params); } /** * "path:created" canvas event handler * @param {{path: fabric.Path}} obj - Path object * @private */ _onPathCreated(obj) { const { x: left, y: top } = obj.path.getCenterPoint(); obj.path.set( extend( { left, top, }, fObjectOptions.SELECTION_STYLE ) ); const params = this.createObjectProperties(obj.path); this.fire(events.ADD_OBJECT, params); } /** * "selction:cleared" canvas event handler * @private */ _onSelectionCleared() { this.fire(events.SELECTION_CLEARED); } /** * "selction:created" canvas event handler * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event * @private */ _onSelectionCreated(fEvent) { const { target } = fEvent; const params = this.createObjectProperties(target); this.fire(events.OBJECT_ACTIVATED, params); this.fire(events.SELECTION_CREATED, fEvent.target); } /** * Canvas discard selection all */ discardSelection() { this._canvas.discardActiveObject(); this._canvas.renderAll(); } /** * Canvas Selectable status change * @param {boolean} selectable - expect status */ changeSelectableAll(selectable) { this._canvas.forEachObject((obj) => { obj.selectable = selectable; obj.hoverCursor = selectable ? 'move' : 'crosshair'; }); } /** * Return object's properties * @param {fabric.Object} obj - fabric object * @returns {Object} properties object */ createObjectProperties(obj) { const predefinedKeys = [ 'left', 'top', 'width', 'height', 'fill', 'stroke', 'strokeWidth', 'opacity', 'angle', ]; const props = { id: stamp(obj), type: obj.type, }; extend(props, getProperties(obj, predefinedKeys)); if (includes(['i-text', 'text'], obj.type)) { extend(props, this._createTextProperties(obj, props)); } else if (includes(['rect', 'triangle', 'circle'], obj.type)) { const shapeComp = this.getComponent(components.SHAPE); extend(props, { fill: shapeComp.makeFillPropertyForUserEvent(obj), }); } return props; } /** * Get text object's properties * @param {fabric.Object} obj - fabric text object * @param {Object} props - properties * @returns {Object} properties object */ _createTextProperties(obj) { const predefinedKeys = [ 'text', 'fontFamily', 'fontSize', 'fontStyle', 'textAlign', 'textDecoration', 'fontWeight', ]; const props = {}; extend(props, getProperties(obj, predefinedKeys)); return props; } /** * Add object array by id * @param {fabric.Object} obj - fabric object * @returns {number} object id */ _addFabricObject(obj) { const id = stamp(obj); this._objects[id] = obj; return id; } /** * Remove an object in array yb id * @param {number} id - object id */ _removeFabricObject(id) { delete this._objects[id]; } /** * Reset targetObjectForCopyPaste value from activeObject */ resetTargetObjectForCopyPaste() { const activeObject = this.getActiveObject(); if (activeObject) { this.targetObjectForCopyPaste = activeObject; } } /** * Paste fabric object * @returns {Promise} */ pasteObject() { if (!this.targetObjectForCopyPaste) { return Promise.resolve([]); } const targetObject = this.targetObjectForCopyPaste; const isGroupSelect = targetObject.type === 'activeSelection'; const targetObjects = isGroupSelect ? targetObject.getObjects() : [targetObject]; let newTargetObject = null; this.discardSelection(); return this._cloneObject(targetObjects).then((addedObjects) => { if (addedObjects.length > 1) { newTargetObject = this.getActiveSelectionFromObjects(addedObjects); } else { [newTargetObject] = addedObjects; } this.targetObjectForCopyPaste = newTargetObject; this.setActiveObject(newTargetObject); }); } /** * Clone object * @param {fabric.Object} targetObjects - fabric object * @returns {Promise} * @private */ _cloneObject(targetObjects) { const addedObjects = targetObjects.map((targetObject) => this._cloneObjectItem(targetObject)); return Promise.all(addedObjects); } /** * Clone object one item * @param {fabric.Object} targetObject - fabric object * @returns {Promise} * @private */ _cloneObjectItem(targetObject) { return this._copyFabricObjectForPaste(targetObject).then((clonedObject) => { const objectProperties = this.createObjectProperties(clonedObject); this.add(clonedObject); this.fire(events.ADD_OBJECT, objectProperties); return clonedObject; }); } /** * Copy fabric object with Changed position for copy and paste * @param {fabric.Object} targetObject - fabric object * @returns {Promise} * @private */ _copyFabricObjectForPaste(targetObject) { const addExtraPx = (value, isReverse) => isReverse ? value - EXTRA_PX_FOR_PASTE : value + EXTRA_PX_FOR_PASTE; return this._copyFabricObject(targetObject).then((clonedObject) => { const { left, top, width, height } = clonedObject; const { width: canvasWidth, height: canvasHeight } = this.getCanvasSize(); const rightEdge = left + width / 2; const bottomEdge = top + height / 2; clonedObject.set( extend( { left: addExtraPx(left, rightEdge + EXTRA_PX_FOR_PASTE > canvasWidth), top: addExtraPx(top, bottomEdge + EXTRA_PX_FOR_PASTE > canvasHeight), }, fObjectOptions.SELECTION_STYLE ) ); return clonedObject; }); } /** * Copy fabric object * @param {fabric.Object} targetObject - fabric object * @returns {Promise} * @private */ _copyFabricObject(targetObject) { return new Promise((resolve) => { targetObject.clone((cloned) => { const shapeComp = this.getComponent(components.SHAPE); if (isShape(cloned)) { shapeComp.processForCopiedObject(cloned, targetObject); } resolve(cloned); }); }); } /** * Get current dimensions * @returns {object} */ getCurrentDimensions() { const resize = this.getComponent(components.RESIZE); return resize.getCurrentDimensions(); } /** * Get original dimensions * @returns {object} */ getOriginalDimensions() { const resize = this.getComponent(components.RESIZE); return resize.getOriginalDimensions(); } /** * Set original dimensions * @param {object} dimensions - Dimensions */ setOriginalDimensions(dimensions) { const resize = this.getComponent(components.RESIZE); resize.setOriginalDimensions(dimensions); } /** * Resize Image * @param {Object} dimensions - Resize dimensions * @returns {Promise} */ resize(dimensions) { const resize = this.getComponent(components.RESIZE); return resize.resize(dimensions); } } CustomEvents.mixin(Graphics); export default Graphics; ================================================ FILE: apps/image-editor/src/js/helper/imagetracer.js ================================================ /* imagetracer.js version 1.2.4 Simple raster image tracer and vectorizer written in JavaScript. andras@jankovics.net */ /* The Unlicense / PUBLIC DOMAIN This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 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 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. For more information, please refer to http://unlicense.org/ */ export default class ImageTracer { static tracerDefaultOption() { return { pathomit: 100, ltres: 0.1, qtres: 1, scale: 1, strokewidth: 5, viewbox: false, linefilter: true, desc: false, rightangleenhance: false, pal: [ { r: 0, g: 0, b: 0, a: 255, }, { r: 255, g: 255, b: 255, a: 255, }, ], }; } /* eslint-disable */ constructor() { this.versionnumber = '1.2.4'; this.optionpresets = { default: { corsenabled: false, ltres: 1, qtres: 1, pathomit: 8, rightangleenhance: true, colorsampling: 2, numberofcolors: 16, mincolorratio: 0, colorquantcycles: 3, layering: 0, strokewidth: 1, linefilter: false, scale: 1, roundcoords: 1, viewbox: false, desc: false, lcpr: 0, qcpr: 0, blurradius: 0, blurdelta: 20, }, posterized1: { colorsampling: 0, numberofcolors: 2, }, posterized2: { numberofcolors: 4, blurradius: 5, }, curvy: { ltres: 0.01, linefilter: true, rightangleenhance: false, }, sharp: { qtres: 0.01, linefilter: false }, detailed: { pathomit: 0, roundcoords: 2, ltres: 0.5, qtres: 0.5, numberofcolors: 64 }, smoothed: { blurradius: 5, blurdelta: 64 }, grayscale: { colorsampling: 0, colorquantcycles: 1, numberofcolors: 7 }, fixedpalette: { colorsampling: 0, colorquantcycles: 1, numberofcolors: 27 }, randomsampling1: { colorsampling: 1, numberofcolors: 8 }, randomsampling2: { colorsampling: 1, numberofcolors: 64 }, artistic1: { colorsampling: 0, colorquantcycles: 1, pathomit: 0, blurradius: 5, blurdelta: 64, ltres: 0.01, linefilter: true, numberofcolors: 16, strokewidth: 2, }, artistic2: { qtres: 0.01, colorsampling: 0, colorquantcycles: 1, numberofcolors: 4, strokewidth: 0, }, artistic3: { qtres: 10, ltres: 10, numberofcolors: 8 }, artistic4: { qtres: 10, ltres: 10, numberofcolors: 64, blurradius: 5, blurdelta: 256, strokewidth: 2, }, posterized3: { ltres: 1, qtres: 1, pathomit: 20, rightangleenhance: true, colorsampling: 0, numberofcolors: 3, mincolorratio: 0, colorquantcycles: 3, blurradius: 3, blurdelta: 20, strokewidth: 0, linefilter: false, roundcoords: 1, pal: [ { r: 0, g: 0, b: 100, a: 255 }, { r: 255, g: 255, b: 255, a: 255 }, ], }, }; this.pathscan_combined_lookup = [ [ [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], ], [ [0, 1, 0, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 2, -1, 0], ], [ [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 1, 0, -1], [0, 0, 1, 0], ], [ [0, 0, 1, 0], [-1, -1, -1, -1], [0, 2, -1, 0], [-1, -1, -1, -1], ], [ [-1, -1, -1, -1], [0, 0, 1, 0], [0, 3, 0, 1], [-1, -1, -1, -1], ], [ [13, 3, 0, 1], [13, 2, -1, 0], [7, 1, 0, -1], [7, 0, 1, 0], ], [ [-1, -1, -1, -1], [0, 1, 0, -1], [-1, -1, -1, -1], [0, 3, 0, 1], ], [ [0, 3, 0, 1], [0, 2, -1, 0], [-1, -1, -1, -1], [-1, -1, -1, -1], ], [ [0, 3, 0, 1], [0, 2, -1, 0], [-1, -1, -1, -1], [-1, -1, -1, -1], ], [ [-1, -1, -1, -1], [0, 1, 0, -1], [-1, -1, -1, -1], [0, 3, 0, 1], ], [ [11, 1, 0, -1], [14, 0, 1, 0], [14, 3, 0, 1], [11, 2, -1, 0], ], [ [-1, -1, -1, -1], [0, 0, 1, 0], [0, 3, 0, 1], [-1, -1, -1, -1], ], [ [0, 0, 1, 0], [-1, -1, -1, -1], [0, 2, -1, 0], [-1, -1, -1, -1], ], [ [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 1, 0, -1], [0, 0, 1, 0], ], [ [0, 1, 0, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 2, -1, 0], ], [ [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], ], ]; this.gks = [ [0.27901, 0.44198, 0.27901], [0.135336, 0.228569, 0.272192, 0.228569, 0.135336], [0.086776, 0.136394, 0.178908, 0.195843, 0.178908, 0.136394, 0.086776], [0.063327, 0.093095, 0.122589, 0.144599, 0.152781, 0.144599, 0.122589, 0.093095, 0.063327], [ 0.049692, 0.069304, 0.089767, 0.107988, 0.120651, 0.125194, 0.120651, 0.107988, 0.089767, 0.069304, 0.049692, ], ]; this.specpalette = [ { r: 0, g: 0, b: 0, a: 255 }, { r: 128, g: 128, b: 128, a: 255 }, { r: 0, g: 0, b: 128, a: 255 }, { r: 64, g: 64, b: 128, a: 255 }, { r: 192, g: 192, b: 192, a: 255 }, { r: 255, g: 255, b: 255, a: 255 }, { r: 128, g: 128, b: 192, a: 255 }, { r: 0, g: 0, b: 192, a: 255 }, { r: 128, g: 0, b: 0, a: 255 }, { r: 128, g: 64, b: 64, a: 255 }, { r: 128, g: 0, b: 128, a: 255 }, { r: 168, g: 168, b: 168, a: 255 }, { r: 192, g: 128, b: 128, a: 255 }, { r: 192, g: 0, b: 0, a: 255 }, { r: 255, g: 255, b: 255, a: 255 }, { r: 0, g: 128, b: 0, a: 255 }, ]; } imageToSVG(url, callback, options) { options = this.checkoptions(options); this.loadImage( url, (canvas) => { callback(this.imagedataToSVG(this.getImgdata(canvas), options)); }, options ); } imagedataToSVG(imgd, options) { options = this.checkoptions(options); const td = this.imagedataToTracedata(imgd, options); return this.getsvgstring(td, options); } imageToTracedata(url, callback, options) { options = this.checkoptions(options); this.loadImage( url, (canvas) => { callback(this.imagedataToTracedata(this.getImgdata(canvas), options)); }, options ); } imagedataToTracedata(imgd, options) { options = this.checkoptions(options); const ii = this.colorquantization(imgd, options); let tracedata; if (options.layering === 0) { tracedata = { layers: [], palette: ii.palette, width: ii.array[0].length - 2, height: ii.array.length - 2, }; for (let colornum = 0; colornum < ii.palette.length; colornum += 1) { const tracedlayer = this.batchtracepaths( this.internodes( this.pathscan(this.layeringstep(ii, colornum), options.pathomit), options ), options.ltres, options.qtres ); tracedata.layers.push(tracedlayer); } } else { const ls = this.layering(ii); if (options.layercontainerid) { this.drawLayers(ls, this.specpalette, options.scale, options.layercontainerid); } const bps = this.batchpathscan(ls, options.pathomit); const bis = this.batchinternodes(bps, options); tracedata = { layers: this.batchtracelayers(bis, options.ltres, options.qtres), palette: ii.palette, width: imgd.width, height: imgd.height, }; } return tracedata; } checkoptions(options) { options = options || {}; if (typeof options === 'string') { options = options.toLowerCase(); if (this.optionpresets[options]) { options = this.optionpresets[options]; } else { options = {}; } } const ok = Object.keys(this.optionpresets['default']); for (let k = 0; k < ok.length; k += 1) { if (!options.hasOwnProperty(ok[k])) { options[ok[k]] = this.optionpresets['default'][ok[k]]; } } return options; } colorquantization(imgd, options) { const arr = []; let idx = 0; let cd; let cdl; let ci; const paletteacc = []; const pixelnum = imgd.width * imgd.height; let i; let j; let k; let cnt; let palette; for (j = 0; j < imgd.height + 2; j += 1) { arr[j] = []; for (i = 0; i < imgd.width + 2; i += 1) { arr[j][i] = -1; } } if (options.pal) { palette = options.pal; } else if (options.colorsampling === 0) { palette = this.generatepalette(options.numberofcolors); } else if (options.colorsampling === 1) { palette = this.samplepalette(options.numberofcolors, imgd); } else { palette = this.samplepalette2(options.numberofcolors, imgd); } if (options.blurradius > 0) { imgd = this.blur(imgd, options.blurradius, options.blurdelta); } for (cnt = 0; cnt < options.colorquantcycles; cnt += 1) { if (cnt > 0) { for (k = 0; k < palette.length; k += 1) { if (paletteacc[k].n > 0) { palette[k] = { r: Math.floor(paletteacc[k].r / paletteacc[k].n), g: Math.floor(paletteacc[k].g / paletteacc[k].n), b: Math.floor(paletteacc[k].b / paletteacc[k].n), a: Math.floor(paletteacc[k].a / paletteacc[k].n), }; } if ( paletteacc[k].n / pixelnum < options.mincolorratio && cnt < options.colorquantcycles - 1 ) { palette[k] = { r: Math.floor(Math.random() * 255), g: Math.floor(Math.random() * 255), b: Math.floor(Math.random() * 255), a: Math.floor(Math.random() * 255), }; } } } for (i = 0; i < palette.length; i += 1) { paletteacc[i] = { r: 0, g: 0, b: 0, a: 0, n: 0 }; } for (j = 0; j < imgd.height; j += 1) { for (i = 0; i < imgd.width; i += 1) { idx = (j * imgd.width + i) * 4; ci = 0; cdl = 1024; for (k = 0; k < palette.length; k += 1) { cd = Math.abs(palette[k].r - imgd.data[idx]) + Math.abs(palette[k].g - imgd.data[idx + 1]) + Math.abs(palette[k].b - imgd.data[idx + 2]) + Math.abs(palette[k].a - imgd.data[idx + 3]); if (cd < cdl) { cdl = cd; ci = k; } } paletteacc[ci].r += imgd.data[idx]; paletteacc[ci].g += imgd.data[idx + 1]; paletteacc[ci].b += imgd.data[idx + 2]; paletteacc[ci].a += imgd.data[idx + 3]; paletteacc[ci].n += 1; arr[j + 1][i + 1] = ci; } } } return { array: arr, palette }; } samplepalette(numberofcolors, imgd) { let idx; const palette = []; for (let i = 0; i < numberofcolors; i += 1) { idx = Math.floor((Math.random() * imgd.data.length) / 4) * 4; palette.push({ r: imgd.data[idx], g: imgd.data[idx + 1], b: imgd.data[idx + 2], a: imgd.data[idx + 3], }); } return palette; } samplepalette2(numberofcolors, imgd) { let idx; const palette = []; const ni = Math.ceil(Math.sqrt(numberofcolors)); const nj = Math.ceil(numberofcolors / ni); const vx = imgd.width / (ni + 1); const vy = imgd.height / (nj + 1); for (let j = 0; j < nj; j += 1) { for (let i = 0; i < ni; i += 1) { if (palette.length === numberofcolors) { break; } else { idx = Math.floor((j + 1) * vy * imgd.width + (i + 1) * vx) * 4; palette.push({ r: imgd.data[idx], g: imgd.data[idx + 1], b: imgd.data[idx + 2], a: imgd.data[idx + 3], }); } } } return palette; } generatepalette(numberofcolors) { const palette = []; let rcnt; let gcnt; let bcnt; if (numberofcolors < 8) { const graystep = Math.floor(255 / (numberofcolors - 1)); for (let i = 0; i < numberofcolors; i += 1) { palette.push({ r: i * graystep, g: i * graystep, b: i * graystep, a: 255 }); } } else { const colorqnum = Math.floor(Math.pow(numberofcolors, 1 / 3)); const colorstep = Math.floor(255 / (colorqnum - 1)); const rndnum = numberofcolors - colorqnum * colorqnum * colorqnum; for (rcnt = 0; rcnt < colorqnum; rcnt += 1) { for (gcnt = 0; gcnt < colorqnum; gcnt += 1) { for (bcnt = 0; bcnt < colorqnum; bcnt += 1) { palette.push({ r: rcnt * colorstep, g: gcnt * colorstep, b: bcnt * colorstep, a: 255 }); } } } for (rcnt = 0; rcnt < rndnum; rcnt += 1) { palette.push({ r: Math.floor(Math.random() * 255), g: Math.floor(Math.random() * 255), b: Math.floor(Math.random() * 255), a: Math.floor(Math.random() * 255), }); } } return palette; } layering(ii) { const layers = []; let val = 0; const ah = ii.array.length; const aw = ii.array[0].length; let n1; let n2; let n3; let n4; let n5; let n6; let n7; let n8; let i; let j; let k; for (k = 0; k < ii.palette.length; k += 1) { layers[k] = []; for (j = 0; j < ah; j += 1) { layers[k][j] = []; for (i = 0; i < aw; i += 1) { layers[k][j][i] = 0; } } } for (j = 1; j < ah - 1; j += 1) { for (i = 1; i < aw - 1; i += 1) { val = ii.array[j][i]; n1 = ii.array[j - 1][i - 1] === val ? 1 : 0; n2 = ii.array[j - 1][i] === val ? 1 : 0; n3 = ii.array[j - 1][i + 1] === val ? 1 : 0; n4 = ii.array[j][i - 1] === val ? 1 : 0; n5 = ii.array[j][i + 1] === val ? 1 : 0; n6 = ii.array[j + 1][i - 1] === val ? 1 : 0; n7 = ii.array[j + 1][i] === val ? 1 : 0; n8 = ii.array[j + 1][i + 1] === val ? 1 : 0; layers[val][j + 1][i + 1] = 1 + n5 * 2 + n8 * 4 + n7 * 8; if (!n4) { layers[val][j + 1][i] = 0 + 2 + n7 * 4 + n6 * 8; } if (!n2) { layers[val][j][i + 1] = 0 + n3 * 2 + n5 * 4 + 8; } if (!n1) { layers[val][j][i] = 0 + n2 * 2 + 4 + n4 * 8; } } } return layers; } layeringstep(ii, cnum) { const layer = []; const ah = ii.array.length; const aw = ii.array[0].length; let i; let j; for (j = 0; j < ah; j += 1) { layer[j] = []; for (i = 0; i < aw; i += 1) { layer[j][i] = 0; } } for (j = 1; j < ah; j += 1) { for (i = 1; i < aw; i += 1) { layer[j][i] = (ii.array[j - 1][i - 1] === cnum ? 1 : 0) + (ii.array[j - 1][i] === cnum ? 2 : 0) + (ii.array[j][i - 1] === cnum ? 8 : 0) + (ii.array[j][i] === cnum ? 4 : 0); } } return layer; } pathscan(arr, pathomit) { const paths = []; let pacnt = 0; let pcnt = 0; let px = 0; let py = 0; const w = arr[0].length; const h = arr.length; let dir = 0; let pathfinished = true; let holepath = false; let lookuprow; for (let j = 0; j < h; j += 1) { for (let i = 0; i < w; i += 1) { if (arr[j][i] === 4 || arr[j][i] === 11) { px = i; py = j; paths[pacnt] = {}; paths[pacnt].points = []; paths[pacnt].boundingbox = [px, py, px, py]; paths[pacnt].holechildren = []; pathfinished = false; pcnt = 0; holepath = arr[j][i] === 11; dir = 1; while (!pathfinished) { paths[pacnt].points[pcnt] = {}; paths[pacnt].points[pcnt].x = px - 1; paths[pacnt].points[pcnt].y = py - 1; paths[pacnt].points[pcnt].t = arr[py][px]; if (px - 1 < paths[pacnt].boundingbox[0]) { paths[pacnt].boundingbox[0] = px - 1; } if (px - 1 > paths[pacnt].boundingbox[2]) { paths[pacnt].boundingbox[2] = px - 1; } if (py - 1 < paths[pacnt].boundingbox[1]) { paths[pacnt].boundingbox[1] = py - 1; } if (py - 1 > paths[pacnt].boundingbox[3]) { paths[pacnt].boundingbox[3] = py - 1; } lookuprow = this.pathscan_combined_lookup[arr[py][px]][dir]; arr[py][px] = lookuprow[0]; dir = lookuprow[1]; px += lookuprow[2]; py += lookuprow[3]; if (px - 1 === paths[pacnt].points[0].x && py - 1 === paths[pacnt].points[0].y) { pathfinished = true; if (paths[pacnt].points.length < pathomit) { paths.pop(); } else { paths[pacnt].isholepath = !!holepath; if (holepath) { let parentidx = 0, parentbbox = [-1, -1, w + 1, h + 1]; for (let parentcnt = 0; parentcnt < pacnt; parentcnt++) { if ( !paths[parentcnt].isholepath && this.boundingboxincludes( paths[parentcnt].boundingbox, paths[pacnt].boundingbox ) && this.boundingboxincludes(parentbbox, paths[parentcnt].boundingbox) ) { parentidx = parentcnt; parentbbox = paths[parentcnt].boundingbox; } } paths[parentidx].holechildren.push(pacnt); } pacnt += 1; } } pcnt += 1; } } } } return paths; } boundingboxincludes(parentbbox, childbbox) { return ( parentbbox[0] < childbbox[0] && parentbbox[1] < childbbox[1] && parentbbox[2] > childbbox[2] && parentbbox[3] > childbbox[3] ); } batchpathscan(layers, pathomit) { const bpaths = []; for (const k in layers) { if (!layers.hasOwnProperty(k)) { continue; } bpaths[k] = this.pathscan(layers[k], pathomit); } return bpaths; } internodes(paths, options) { const ins = []; let palen = 0; let nextidx = 0; let nextidx2 = 0; let previdx = 0; let previdx2 = 0; let pacnt; let pcnt; for (pacnt = 0; pacnt < paths.length; pacnt += 1) { ins[pacnt] = {}; ins[pacnt].points = []; ins[pacnt].boundingbox = paths[pacnt].boundingbox; ins[pacnt].holechildren = paths[pacnt].holechildren; ins[pacnt].isholepath = paths[pacnt].isholepath; palen = paths[pacnt].points.length; for (pcnt = 0; pcnt < palen; pcnt += 1) { nextidx = (pcnt + 1) % palen; nextidx2 = (pcnt + 2) % palen; previdx = (pcnt - 1 + palen) % palen; previdx2 = (pcnt - 2 + palen) % palen; if ( options.rightangleenhance && this.testrightangle(paths[pacnt], previdx2, previdx, pcnt, nextidx, nextidx2) ) { if (ins[pacnt].points.length > 0) { ins[pacnt].points[ins[pacnt].points.length - 1].linesegment = this.getdirection( ins[pacnt].points[ins[pacnt].points.length - 1].x, ins[pacnt].points[ins[pacnt].points.length - 1].y, paths[pacnt].points[pcnt].x, paths[pacnt].points[pcnt].y ); } ins[pacnt].points.push({ x: paths[pacnt].points[pcnt].x, y: paths[pacnt].points[pcnt].y, linesegment: this.getdirection( paths[pacnt].points[pcnt].x, paths[pacnt].points[pcnt].y, (paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x) / 2, (paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y) / 2 ), }); } ins[pacnt].points.push({ x: (paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x) / 2, y: (paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y) / 2, linesegment: this.getdirection( (paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x) / 2, (paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y) / 2, (paths[pacnt].points[nextidx].x + paths[pacnt].points[nextidx2].x) / 2, (paths[pacnt].points[nextidx].y + paths[pacnt].points[nextidx2].y) / 2 ), }); } } return ins; } testrightangle(path, idx1, idx2, idx3, idx4, idx5) { return ( (path.points[idx3].x === path.points[idx1].x && path.points[idx3].x === path.points[idx2].x && path.points[idx3].y === path.points[idx4].y && path.points[idx3].y === path.points[idx5].y) || (path.points[idx3].y === path.points[idx1].y && path.points[idx3].y === path.points[idx2].y && path.points[idx3].x === path.points[idx4].x && path.points[idx3].x === path.points[idx5].x) ); } getdirection(x1, y1, x2, y2) { let val = 8; if (x1 < x2) { if (y1 < y2) { val = 1; } else if (y1 > y2) { val = 7; } else { val = 0; } } else if (x1 > x2) { if (y1 < y2) { val = 3; } else if (y1 > y2) { val = 5; } else { val = 4; } } else if (y1 < y2) { val = 2; } else if (y1 > y2) { val = 6; } else { val = 8; } return val; } batchinternodes(bpaths, options) { const binternodes = []; for (const k in bpaths) { if (!bpaths.hasOwnProperty(k)) { continue; } binternodes[k] = this.internodes(bpaths[k], options); } return binternodes; } tracepath(path, ltres, qtres) { let pcnt = 0; let segtype1; let segtype2; let seqend; const smp = {}; smp.segments = []; smp.boundingbox = path.boundingbox; smp.holechildren = path.holechildren; smp.isholepath = path.isholepath; while (pcnt < path.points.length) { segtype1 = path.points[pcnt].linesegment; segtype2 = -1; seqend = pcnt + 1; while ( (path.points[seqend].linesegment === segtype1 || path.points[seqend].linesegment === segtype2 || segtype2 === -1) && seqend < path.points.length - 1 ) { if (path.points[seqend].linesegment !== segtype1 && segtype2 === -1) { segtype2 = path.points[seqend].linesegment; } seqend += 1; } if (seqend === path.points.length - 1) { seqend = 0; } smp.segments = smp.segments.concat(this.fitseq(path, ltres, qtres, pcnt, seqend)); if (seqend > 0) { pcnt = seqend; } else { pcnt = path.points.length; } } return smp; } fitseq(path, ltres, qtres, seqstart, seqend) { if (seqend > path.points.length || seqend < 0) { return []; } let errorpoint = seqstart, errorval = 0, curvepass = true, px, py, dist2; let tl = seqend - seqstart; if (tl < 0) { tl += path.points.length; } let vx = (path.points[seqend].x - path.points[seqstart].x) / tl, vy = (path.points[seqend].y - path.points[seqstart].y) / tl; let pcnt = (seqstart + 1) % path.points.length, pl; while (pcnt != seqend) { pl = pcnt - seqstart; if (pl < 0) { pl += path.points.length; } px = path.points[seqstart].x + vx * pl; py = path.points[seqstart].y + vy * pl; dist2 = (path.points[pcnt].x - px) * (path.points[pcnt].x - px) + (path.points[pcnt].y - py) * (path.points[pcnt].y - py); if (dist2 > ltres) { curvepass = false; } if (dist2 > errorval) { errorpoint = pcnt; errorval = dist2; } pcnt = (pcnt + 1) % path.points.length; } if (curvepass) { return [ { type: 'L', x1: path.points[seqstart].x, y1: path.points[seqstart].y, x2: path.points[seqend].x, y2: path.points[seqend].y, }, ]; } const fitpoint = errorpoint; curvepass = true; errorval = 0; let t = (fitpoint - seqstart) / tl, t1 = (1 - t) * (1 - t), t2 = 2 * (1 - t) * t, t3 = t * t; let cpx = (t1 * path.points[seqstart].x + t3 * path.points[seqend].x - path.points[fitpoint].x) / -t2, cpy = (t1 * path.points[seqstart].y + t3 * path.points[seqend].y - path.points[fitpoint].y) / -t2; pcnt = seqstart + 1; while (pcnt != seqend) { t = (pcnt - seqstart) / tl; t1 = (1 - t) * (1 - t); t2 = 2 * (1 - t) * t; t3 = t * t; px = t1 * path.points[seqstart].x + t2 * cpx + t3 * path.points[seqend].x; py = t1 * path.points[seqstart].y + t2 * cpy + t3 * path.points[seqend].y; dist2 = (path.points[pcnt].x - px) * (path.points[pcnt].x - px) + (path.points[pcnt].y - py) * (path.points[pcnt].y - py); if (dist2 > qtres) { curvepass = false; } if (dist2 > errorval) { errorpoint = pcnt; errorval = dist2; } pcnt = (pcnt + 1) % path.points.length; } if (curvepass) { return [ { type: 'Q', x1: path.points[seqstart].x, y1: path.points[seqstart].y, x2: cpx, y2: cpy, x3: path.points[seqend].x, y3: path.points[seqend].y, }, ]; } const splitpoint = fitpoint; return this.fitseq(path, ltres, qtres, seqstart, splitpoint).concat( this.fitseq(path, ltres, qtres, splitpoint, seqend) ); } batchtracepaths(internodepaths, ltres, qtres) { const btracedpaths = []; for (const k in internodepaths) { if (!internodepaths.hasOwnProperty(k)) { continue; } btracedpaths.push(this.tracepath(internodepaths[k], ltres, qtres)); } return btracedpaths; } batchtracelayers(binternodes, ltres, qtres) { const btbis = []; for (const k in binternodes) { if (!binternodes.hasOwnProperty(k)) { continue; } btbis[k] = this.batchtracepaths(binternodes[k], ltres, qtres); } return btbis; } roundtodec(val, places) { return Number(val.toFixed(places)); } svgpathstring(tracedata, lnum, pathnum, options) { let layer = tracedata.layers[lnum], smp = layer[pathnum], str = '', pcnt; if (options.linefilter && smp.segments.length < 3) { return str; } str = `'; if (options.lcpr || options.qcpr) { for (pcnt = 0; pcnt < smp.segments.length; pcnt++) { if (smp.segments[pcnt].hasOwnProperty('x3') && options.qcpr) { str += ``; str += ``; str += ``; str += ``; } if (!smp.segments[pcnt].hasOwnProperty('x3') && options.lcpr) { str += ``; } } for (var hcnt = 0; hcnt < smp.holechildren.length; hcnt++) { var hsmp = layer[smp.holechildren[hcnt]]; for (pcnt = 0; pcnt < hsmp.segments.length; pcnt++) { if (hsmp.segments[pcnt].hasOwnProperty('x3') && options.qcpr) { str += ``; str += ``; str += ``; str += ``; } if (!hsmp.segments[pcnt].hasOwnProperty('x3') && options.lcpr) { str += ``; } } } } return str; } getsvgstring(tracedata, options) { options = this.checkoptions(options); const w = tracedata.width * options.scale; const h = tracedata.height * options.scale; let svgstr = ``; for (let lcnt = 0; lcnt < tracedata.layers.length; lcnt += 1) { for (let pcnt = 0; pcnt < tracedata.layers[lcnt].length; pcnt += 1) { if (!tracedata.layers[lcnt][pcnt].isholepath) { svgstr += this.svgpathstring(tracedata, lcnt, pcnt, options); } } } svgstr += ''; return svgstr; } compareNumbers(a, b) { return a - b; } torgbastr(c) { return `rgba(${c.r},${c.g},${c.b},${c.a})`; } tosvgcolorstr(c, options) { return `fill="rgb(${c.r},${c.g},${c.b})" stroke="rgb(${c.r},${c.g},${c.b})" stroke-width="${ options.strokewidth }" opacity="${c.a / 255.0}" `; } appendSVGString(svgstr, parentid) { let div; if (parentid) { div = document.getElementById(parentid); if (!div) { div = document.createElement('div'); div.id = parentid; document.body.appendChild(div); } } else { div = document.createElement('div'); document.body.appendChild(div); } div.innerHTML += svgstr; } blur(imgd, radius, delta) { let i, j, k, d, idx, racc, gacc, bacc, aacc, wacc; const imgd2 = { width: imgd.width, height: imgd.height, data: [] }; radius = Math.floor(radius); if (radius < 1) { return imgd; } if (radius > 5) { radius = 5; } delta = Math.abs(delta); if (delta > 1024) { delta = 1024; } const thisgk = this.gks[radius - 1]; for (j = 0; j < imgd.height; j++) { for (i = 0; i < imgd.width; i++) { racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; for (k = -radius; k < radius + 1; k++) { if (i + k > 0 && i + k < imgd.width) { idx = (j * imgd.width + i + k) * 4; racc += imgd.data[idx] * thisgk[k + radius]; gacc += imgd.data[idx + 1] * thisgk[k + radius]; bacc += imgd.data[idx + 2] * thisgk[k + radius]; aacc += imgd.data[idx + 3] * thisgk[k + radius]; wacc += thisgk[k + radius]; } } idx = (j * imgd.width + i) * 4; imgd2.data[idx] = Math.floor(racc / wacc); imgd2.data[idx + 1] = Math.floor(gacc / wacc); imgd2.data[idx + 2] = Math.floor(bacc / wacc); imgd2.data[idx + 3] = Math.floor(aacc / wacc); } } const himgd = new Uint8ClampedArray(imgd2.data); for (j = 0; j < imgd.height; j++) { for (i = 0; i < imgd.width; i++) { racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; for (k = -radius; k < radius + 1; k++) { if (j + k > 0 && j + k < imgd.height) { idx = ((j + k) * imgd.width + i) * 4; racc += himgd[idx] * thisgk[k + radius]; gacc += himgd[idx + 1] * thisgk[k + radius]; bacc += himgd[idx + 2] * thisgk[k + radius]; aacc += himgd[idx + 3] * thisgk[k + radius]; wacc += thisgk[k + radius]; } } idx = (j * imgd.width + i) * 4; imgd2.data[idx] = Math.floor(racc / wacc); imgd2.data[idx + 1] = Math.floor(gacc / wacc); imgd2.data[idx + 2] = Math.floor(bacc / wacc); imgd2.data[idx + 3] = Math.floor(aacc / wacc); } } for (j = 0; j < imgd.height; j++) { for (i = 0; i < imgd.width; i++) { idx = (j * imgd.width + i) * 4; d = Math.abs(imgd2.data[idx] - imgd.data[idx]) + Math.abs(imgd2.data[idx + 1] - imgd.data[idx + 1]) + Math.abs(imgd2.data[idx + 2] - imgd.data[idx + 2]) + Math.abs(imgd2.data[idx + 3] - imgd.data[idx + 3]); if (d > delta) { imgd2.data[idx] = imgd.data[idx]; imgd2.data[idx + 1] = imgd.data[idx + 1]; imgd2.data[idx + 2] = imgd.data[idx + 2]; imgd2.data[idx + 3] = imgd.data[idx + 3]; } } } return imgd2; } loadImage(url, callback, options) { const img = new Image(); if (options && options.corsenabled) { img.crossOrigin = 'Anonymous'; } img.src = url; img.onload = function () { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const context = canvas.getContext('2d'); context.drawImage(img, 0, 0); callback(canvas); }; } getImgdata(canvas) { const context = canvas.getContext('2d'); return context.getImageData(0, 0, canvas.width, canvas.height); } drawLayers(layers, palette, scale, parentid) { scale = scale || 1; let w, h, i, j, k; let div; if (parentid) { div = document.getElementById(parentid); if (!div) { div = document.createElement('div'); div.id = parentid; document.body.appendChild(div); } } else { div = document.createElement('div'); document.body.appendChild(div); } for (k in layers) { if (!layers.hasOwnProperty(k)) { continue; } w = layers[k][0].length; h = layers[k].length; const canvas = document.createElement('canvas'); canvas.width = w * scale; canvas.height = h * scale; const context = canvas.getContext('2d'); for (j = 0; j < h; j += 1) { for (i = 0; i < w; i += 1) { context.fillStyle = this.torgbastr(palette[layers[k][j][i] % palette.length]); context.fillRect(i * scale, j * scale, scale, scale); } } div.appendChild(canvas); } } } ================================================ FILE: apps/image-editor/src/js/helper/selectionModifyHelper.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; /** * Cached selection's info * @type {Array} * @private */ let cachedUndoDataForChangeDimension = null; /** * Set cached undo data * @param {Array} undoData - selection object * @private */ export function setCachedUndoDataForDimension(undoData) { cachedUndoDataForChangeDimension = undoData; } /** * Get cached undo data * @returns {Object} cached undo data * @private */ export function getCachedUndoDataForDimension() { return cachedUndoDataForChangeDimension; } /** * Make undo data * @param {fabric.Object} obj - selection object * @param {Function} undoDatumMaker - make undo datum * @returns {Array} undoData * @private */ export function makeSelectionUndoData(obj, undoDatumMaker) { let undoData; if (obj.type === 'activeSelection') { undoData = obj.getObjects().map((item) => { const { angle, left, top, scaleX, scaleY, width, height } = item; fabric.util.addTransformToObject(item, obj.calcTransformMatrix()); const result = undoDatumMaker(item); item.set({ angle, left, top, width, height, scaleX, scaleY, }); return result; }); } else { undoData = [undoDatumMaker(obj)]; } return undoData; } /** * Make undo datum * @param {number} id - object id * @param {fabric.Object} obj - selection object * @param {boolean} isSelection - whether or not object is selection * @returns {Object} undo datum * @private */ export function makeSelectionUndoDatum(id, obj, isSelection) { return isSelection ? { id, width: obj.width, height: obj.height, top: obj.top, left: obj.left, angle: obj.angle, scaleX: obj.scaleX, scaleY: obj.scaleY, } : extend({ id }, obj); } ================================================ FILE: apps/image-editor/src/js/helper/shapeFilterFillHelper.js ================================================ import { fabric } from 'fabric'; import forEach from 'tui-code-snippet/collection/forEach'; import extend from 'tui-code-snippet/object/extend'; import resizeHelper from '@/helper/shapeResizeHelper'; import { capitalizeString, flipObject, setCustomProperty, getCustomProperty } from '@/util'; const FILTER_OPTION_MAP = { pixelate: 'blocksize', blur: 'blur', }; const POSITION_DIMENSION_MAP = { x: 'width', y: 'height', }; const FILTER_NAME_VALUE_MAP = flipObject(FILTER_OPTION_MAP); /** * Cached canvas image element for fill image * @type {boolean} * @private */ let cachedCanvasImageElement = null; /** * Get background image of fill * @param {fabric.Object} shapeObj - Shape object * @returns {fabric.Image} * @private */ export function getFillImageFromShape(shapeObj) { const { patternSourceCanvas } = getCustomProperty(shapeObj, 'patternSourceCanvas'); const [fillImage] = patternSourceCanvas.getObjects(); return fillImage; } /** * Reset the image position in the filter type fill area. * @param {fabric.Object} shapeObj - Shape object * @private */ export function rePositionFilterTypeFillImage(shapeObj) { const { angle, flipX, flipY } = shapeObj; const fillImage = getFillImageFromShape(shapeObj); const rotatedShapeCornerDimension = getRotatedDimension(shapeObj); const { right, bottom } = rotatedShapeCornerDimension; let { width, height } = rotatedShapeCornerDimension; const diffLeft = (width - shapeObj.width) / 2; const diffTop = (height - shapeObj.height) / 2; const cropX = shapeObj.left - shapeObj.width / 2 - diffLeft; const cropY = shapeObj.top - shapeObj.height / 2 - diffTop; let left = width / 2 - diffLeft; let top = height / 2 - diffTop; const fillImageMaxSize = Math.max(width, height) + Math.max(diffLeft, diffTop); [left, top, width, height] = calculateFillImageDimensionOutsideCanvas({ shapeObj, left, top, width, height, cropX, cropY, flipX, flipY, right, bottom, }); fillImage.set({ angle: flipX === flipY ? -angle : angle, left, top, width, height, cropX, cropY, flipX, flipY, }); setCustomProperty(fillImage, { fillImageMaxSize }); } /** * Make filter option from fabric image * @param {fabric.Image} imageObject - fabric image object * @returns {object} */ export function makeFilterOptionFromFabricImage(imageObject) { return imageObject.filters.map((filter) => { const [key] = Object.keys(filter); return { [FILTER_NAME_VALUE_MAP[key]]: filter[key], }; }); } /** * Calculate fill image position and size for out of Canvas * @param {Object} options - options for position dimension calculate * @param {fabric.Object} shapeObj - shape object * @param {number} left - original left position * @param {number} top - original top position * @param {number} width - image width * @param {number} height - image height * @param {number} cropX - image cropX * @param {number} cropY - image cropY * @param {boolean} flipX - shape flipX * @param {boolean} flipY - shape flipY * @returns {Object} */ function calculateFillImageDimensionOutsideCanvas({ shapeObj, left, top, width, height, cropX, cropY, flipX, flipY, right, bottom, }) { const overflowAreaPositionFixer = (type, outDistance, imageLeft, imageTop) => calculateDistanceOverflowPart({ type, outDistance, shapeObj, flipX, flipY, left: imageLeft, top: imageTop, }); const [originalWidth, originalHeight] = [width, height]; [left, top, width, height] = calculateDimensionLeftTopEdge(overflowAreaPositionFixer, { left, top, width, height, cropX, cropY, }); [left, top, width, height] = calculateDimensionRightBottomEdge(overflowAreaPositionFixer, { left, top, insideCanvasRealImageWidth: width, insideCanvasRealImageHeight: height, right, bottom, cropX, cropY, originalWidth, originalHeight, }); return [left, top, width, height]; } /** * Calculate fill image position and size for for right bottom edge * @param {Function} overflowAreaPositionFixer - position fixer * @param {Object} options - options for position dimension calculate * @param {fabric.Object} shapeObj - shape object * @param {number} left - original left position * @param {number} top - original top position * @param {number} width - image width * @param {number} height - image height * @param {number} right - image right * @param {number} bottom - image bottom * @param {number} cropX - image cropX * @param {number} cropY - image cropY * @param {boolean} originalWidth - image original width * @param {boolean} originalHeight - image original height * @returns {Object} */ function calculateDimensionRightBottomEdge( overflowAreaPositionFixer, { left, top, insideCanvasRealImageWidth, insideCanvasRealImageHeight, right, bottom, cropX, cropY, originalWidth, originalHeight, } ) { let [width, height] = [insideCanvasRealImageWidth, insideCanvasRealImageHeight]; const { width: canvasWidth, height: canvasHeight } = cachedCanvasImageElement; if (right > canvasWidth && cropX > 0) { width = originalWidth - Math.abs(right - canvasWidth); } if (bottom > canvasHeight && cropY > 0) { height = originalHeight - Math.abs(bottom - canvasHeight); } const diff = { x: (insideCanvasRealImageWidth - width) / 2, y: (insideCanvasRealImageHeight - height) / 2, }; forEach(['x', 'y'], (type) => { const cropDistance2 = diff[type]; if (cropDistance2 > 0) { [left, top] = overflowAreaPositionFixer(type, cropDistance2, left, top); } }); return [left, top, width, height]; } /** * Calculate fill image position and size for for left top * @param {Function} overflowAreaPositionFixer - position fixer * @param {Object} options - options for position dimension calculate * @param {fabric.Object} shapeObj - shape object * @param {number} left - original left position * @param {number} top - original top position * @param {number} width - image width * @param {number} height - image height * @param {number} cropX - image cropX * @param {number} cropY - image cropY * @returns {Object} */ function calculateDimensionLeftTopEdge( overflowAreaPositionFixer, { left, top, width, height, cropX, cropY } ) { const dimension = { width, height, }; forEach(['x', 'y'], (type) => { const cropDistance = type === 'x' ? cropX : cropY; const compareSize = dimension[POSITION_DIMENSION_MAP[type]]; const standardSize = cachedCanvasImageElement[POSITION_DIMENSION_MAP[type]]; if (compareSize > standardSize) { const outDistance = (compareSize - standardSize) / 2; dimension[POSITION_DIMENSION_MAP[type]] = standardSize; [left, top] = overflowAreaPositionFixer(type, outDistance, left, top); } if (cropDistance < 0) { [left, top] = overflowAreaPositionFixer(type, cropDistance, left, top); } }); return [left, top, dimension.width, dimension.height]; } /** * Make fill property of dynamic pattern type * @param {fabric.Image} canvasImage - canvas background image * @param {Array} filterOption - filter option * @param {fabric.StaticCanvas} patternSourceCanvas - fabric static canvas * @returns {Object} */ export function makeFillPatternForFilter(canvasImage, filterOption, patternSourceCanvas) { const copiedCanvasElement = getCachedCanvasImageElement(canvasImage); const fillImage = makeFillImage(copiedCanvasElement, canvasImage.angle, filterOption); patternSourceCanvas.add(fillImage); const fabricProperty = { fill: new fabric.Pattern({ source: patternSourceCanvas.getElement(), repeat: 'no-repeat', }), }; setCustomProperty(fabricProperty, { patternSourceCanvas }); return fabricProperty; } /** * Reset fill pattern canvas * @param {fabric.StaticCanvas} patternSourceCanvas - fabric static canvas */ export function resetFillPatternCanvas(patternSourceCanvas) { const [innerImage] = patternSourceCanvas.getObjects(); let { fillImageMaxSize } = getCustomProperty(innerImage, 'fillImageMaxSize'); fillImageMaxSize = Math.max(1, fillImageMaxSize); patternSourceCanvas.setDimensions({ width: fillImageMaxSize, height: fillImageMaxSize, }); patternSourceCanvas.renderAll(); } /** * Remake filter pattern image source * @param {fabric.Object} shapeObj - Shape object * @param {fabric.Image} canvasImage - canvas background image */ export function reMakePatternImageSource(shapeObj, canvasImage) { const { patternSourceCanvas } = getCustomProperty(shapeObj, 'patternSourceCanvas'); const [fillImage] = patternSourceCanvas.getObjects(); const filterOption = makeFilterOptionFromFabricImage(fillImage); patternSourceCanvas.remove(fillImage); const copiedCanvasElement = getCachedCanvasImageElement(canvasImage, true); const newFillImage = makeFillImage(copiedCanvasElement, canvasImage.angle, filterOption); patternSourceCanvas.add(newFillImage); } /** * Calculate a point line outside the canvas. * @param {fabric.Image} canvasImage - canvas background image * @param {boolean} reset - default is false * @returns {HTMLImageElement} */ export function getCachedCanvasImageElement(canvasImage, reset = false) { if (!cachedCanvasImageElement || reset) { cachedCanvasImageElement = canvasImage.toCanvasElement(); } return cachedCanvasImageElement; } /** * Calculate fill image position for out of Canvas * @param {string} type - 'x' or 'y' * @param {fabric.Object} shapeObj - shape object * @param {number} outDistance - distance away * @param {number} left - original left position * @param {number} top - original top position * @returns {Array} */ function calculateDistanceOverflowPart({ type, shapeObj, outDistance, left, top, flipX, flipY }) { const shapePointNavigation = getShapeEdgePoint(shapeObj); const shapeNeighborPointNavigation = [ [1, 2], [0, 3], [0, 3], [1, 2], ]; const linePointsOutsideCanvas = calculateLinePointsOutsideCanvas( type, shapePointNavigation, shapeNeighborPointNavigation ); const reatAngles = calculateLineAngleOfOutsideCanvas( type, shapePointNavigation, linePointsOutsideCanvas ); const { startPointIndex } = linePointsOutsideCanvas; const diffPosition = getReversePositionForFlip({ outDistance, startPointIndex, flipX, flipY, reatAngles, }); return [left + diffPosition.left, top + diffPosition.top]; } /** * Calculate fill image position for out of Canvas * @param {number} outDistance - distance away * @param {boolean} flipX - flip x statux * @param {boolean} flipY - flip y statux * @param {Array} reatAngles - Line angle of the rectangle vertex. * @returns {Object} diffPosition */ function getReversePositionForFlip({ outDistance, startPointIndex, flipX, flipY, reatAngles }) { const rotationChangePoint1 = outDistance * Math.cos((reatAngles[0] * Math.PI) / 180); const rotationChangePoint2 = outDistance * Math.cos((reatAngles[1] * Math.PI) / 180); const isForward = startPointIndex === 2 || startPointIndex === 3; const diffPosition = { top: isForward ? rotationChangePoint1 : rotationChangePoint2, left: isForward ? rotationChangePoint2 : rotationChangePoint1, }; if (isReverseLeftPositionForFlip(startPointIndex, flipX, flipY)) { diffPosition.left = diffPosition.left * -1; } if (isReverseTopPositionForFlip(startPointIndex, flipX, flipY)) { diffPosition.top = diffPosition.top * -1; } return diffPosition; } /** * Calculate a point line outside the canvas. * @param {string} type - 'x' or 'y' * @param {Array} shapePointNavigation - shape edge positions * @param {Object} shapePointNavigation.lefttop - left top position * @param {Object} shapePointNavigation.righttop - right top position * @param {Object} shapePointNavigation.leftbottom - lefttop position * @param {Object} shapePointNavigation.rightbottom - rightbottom position * @param {Array} shapeNeighborPointNavigation - Array to find adjacent edges. * @returns {Object} */ function calculateLinePointsOutsideCanvas( type, shapePointNavigation, shapeNeighborPointNavigation ) { let minimumPoint = 0; let minimumPointIndex = 0; forEach(shapePointNavigation, (point, index) => { if (point[type] < minimumPoint) { minimumPoint = point[type]; minimumPointIndex = index; } }); const [endPointIndex1, endPointIndex2] = shapeNeighborPointNavigation[minimumPointIndex]; return { startPointIndex: minimumPointIndex, endPointIndex1, endPointIndex2, }; } /** * Calculate a point line outside the canvas. * @param {string} type - 'x' or 'y' * @param {Array} shapePointNavigation - shape edge positions * @param {object} shapePointNavigation.lefttop - left top position * @param {object} shapePointNavigation.righttop - right top position * @param {object} shapePointNavigation.leftbottom - lefttop position * @param {object} shapePointNavigation.rightbottom - rightbottom position * @param {Object} linePointsOfOneVertexIndex - Line point of one vertex * @param {Object} linePointsOfOneVertexIndex.startPoint - start point index * @param {Object} linePointsOfOneVertexIndex.endPointIndex1 - end point index * @param {Object} linePointsOfOneVertexIndex.endPointIndex2 - end point index * @returns {Object} */ function calculateLineAngleOfOutsideCanvas(type, shapePointNavigation, linePointsOfOneVertexIndex) { const { startPointIndex, endPointIndex1, endPointIndex2 } = linePointsOfOneVertexIndex; const horizontalVerticalAngle = type === 'x' ? 180 : 270; return [endPointIndex1, endPointIndex2].map((pointIndex) => { const startPoint = shapePointNavigation[startPointIndex]; const endPoint = shapePointNavigation[pointIndex]; const diffY = startPoint.y - endPoint.y; const diffX = startPoint.x - endPoint.x; return (Math.atan2(diffY, diffX) * 180) / Math.PI - horizontalVerticalAngle; }); } /* eslint-disable complexity */ /** * Calculate a point line outside the canvas for horizontal. * @param {number} startPointIndex - start point index * @param {boolean} flipX - flip x statux * @param {boolean} flipY - flip y statux * @returns {boolean} flipY - flip y statux */ function isReverseLeftPositionForFlip(startPointIndex, flipX, flipY) { return ( (((!flipX && flipY) || (!flipX && !flipY)) && startPointIndex === 0) || (((flipX && flipY) || (flipX && !flipY)) && startPointIndex === 1) || (((!flipX && !flipY) || (!flipX && flipY)) && startPointIndex === 2) || (((flipX && !flipY) || (flipX && flipY)) && startPointIndex === 3) ); } /* eslint-enable complexity */ /* eslint-disable complexity */ /** * Calculate a point line outside the canvas for vertical. * @param {number} startPointIndex - start point index * @param {boolean} flipX - flip x statux * @param {boolean} flipY - flip y statux * @returns {boolean} flipY - flip y statux */ function isReverseTopPositionForFlip(startPointIndex, flipX, flipY) { return ( (((flipX && !flipY) || (!flipX && !flipY)) && startPointIndex === 0) || (((!flipX && !flipY) || (flipX && !flipY)) && startPointIndex === 1) || (((flipX && flipY) || (!flipX && flipY)) && startPointIndex === 2) || (((!flipX && flipY) || (flipX && flipY)) && startPointIndex === 3) ); } /* eslint-enable complexity */ /** * Shape edge points * @param {fabric.Object} shapeObj - Selected shape object on canvas * @returns {Array} shapeEdgePoint - shape edge positions */ function getShapeEdgePoint(shapeObj) { return [ shapeObj.getPointByOrigin('left', 'top'), shapeObj.getPointByOrigin('right', 'top'), shapeObj.getPointByOrigin('left', 'bottom'), shapeObj.getPointByOrigin('right', 'bottom'), ]; } /** * Rotated shape dimension * @param {fabric.Object} shapeObj - Shape object * @returns {Object} Rotated shape dimension */ function getRotatedDimension(shapeObj) { const [{ x: ax, y: ay }, { x: bx, y: by }, { x: cx, y: cy }, { x: dx, y: dy }] = getShapeEdgePoint(shapeObj); const left = Math.min(ax, bx, cx, dx); const top = Math.min(ay, by, cy, dy); const right = Math.max(ax, bx, cx, dx); const bottom = Math.max(ay, by, cy, dy); return { left, top, right, bottom, width: right - left, height: bottom - top, }; } /** * Make fill image * @param {HTMLImageElement} copiedCanvasElement - html image element * @param {number} currentCanvasImageAngle - current canvas angle * @param {Array} filterOption - filter option * @returns {fabric.Image} * @private */ function makeFillImage(copiedCanvasElement, currentCanvasImageAngle, filterOption) { const fillImage = new fabric.Image(copiedCanvasElement); forEach(extend({}, ...filterOption), (value, key) => { const fabricFilterClassName = capitalizeString(key); const filter = new fabric.Image.filters[fabricFilterClassName]({ [FILTER_OPTION_MAP[key]]: value, }); fillImage.filters.push(filter); }); fillImage.applyFilters(); setCustomProperty(fillImage, { originalAngle: currentCanvasImageAngle, fillImageMaxSize: Math.max(fillImage.width, fillImage.height), }); resizeHelper.adjustOriginToCenter(fillImage); return fillImage; } ================================================ FILE: apps/image-editor/src/js/helper/shapeResizeHelper.js ================================================ const DIVISOR = { rect: 1, circle: 2, triangle: 1, }; const DIMENSION_KEYS = { rect: { w: 'width', h: 'height', }, circle: { w: 'rx', h: 'ry', }, triangle: { w: 'width', h: 'height', }, }; /** * Set the start point value to the shape object * @param {fabric.Object} shape - Shape object * @ignore */ function setStartPoint(shape) { const { originX, originY } = shape; const originKey = originX.substring(0, 1) + originY.substring(0, 1); shape.startPoint = shape.origins[originKey]; } /** * Get the positions of ratated origin by the pointer value * @param {{x: number, y: number}} origin - Origin value * @param {{x: number, y: number}} pointer - Pointer value * @param {number} angle - Rotating angle * @returns {Object} Postions of origin * @ignore */ function getPositionsOfRotatedOrigin(origin, pointer, angle) { const sx = origin.x; const sy = origin.y; const px = pointer.x; const py = pointer.y; const r = (angle * Math.PI) / 180; const rx = (px - sx) * Math.cos(r) - (py - sy) * Math.sin(r) + sx; const ry = (px - sx) * Math.sin(r) + (py - sy) * Math.cos(r) + sy; return { originX: sx > rx ? 'right' : 'left', originY: sy > ry ? 'bottom' : 'top', }; } /** * Whether the shape has the center origin or not * @param {fabric.Object} shape - Shape object * @returns {boolean} State * @ignore */ function hasCenterOrigin(shape) { return shape.originX === 'center' && shape.originY === 'center'; } /** * Adjust the origin of shape by the start point * @param {{x: number, y: number}} pointer - Pointer value * @param {fabric.Object} shape - Shape object * @ignore */ function adjustOriginByStartPoint(pointer, shape) { const centerPoint = shape.getPointByOrigin('center', 'center'); const angle = -shape.angle; const originPositions = getPositionsOfRotatedOrigin(centerPoint, pointer, angle); const { originX, originY } = originPositions; const origin = shape.getPointByOrigin(originX, originY); const left = shape.left - (centerPoint.x - origin.x); const top = shape.top - (centerPoint.y - origin.y); shape.set({ originX, originY, left, top, }); shape.setCoords(); } /** * Adjust the origin of shape by the moving pointer value * @param {{x: number, y: number}} pointer - Pointer value * @param {fabric.Object} shape - Shape object * @ignore */ function adjustOriginByMovingPointer(pointer, shape) { const origin = shape.startPoint; const angle = -shape.angle; const originPositions = getPositionsOfRotatedOrigin(origin, pointer, angle); const { originX, originY } = originPositions; shape.setPositionByOrigin(origin, originX, originY); shape.setCoords(); } /** * Adjust the dimension of shape on firing scaling event * @param {fabric.Object} shape - Shape object * @ignore */ function adjustDimensionOnScaling(shape) { const { type, scaleX, scaleY } = shape; const dimensionKeys = DIMENSION_KEYS[type]; let width = shape[dimensionKeys.w] * scaleX; let height = shape[dimensionKeys.h] * scaleY; if (shape.isRegular) { const maxScale = Math.max(scaleX, scaleY); width = shape[dimensionKeys.w] * maxScale; height = shape[dimensionKeys.h] * maxScale; } const options = { hasControls: false, hasBorders: false, scaleX: 1, scaleY: 1, }; options[dimensionKeys.w] = width; options[dimensionKeys.h] = height; shape.set(options); } /** * Adjust the dimension of shape on firing mouse move event * @param {{x: number, y: number}} pointer - Pointer value * @param {fabric.Object} shape - Shape object * @ignore */ function adjustDimensionOnMouseMove(pointer, shape) { const { type, strokeWidth, startPoint: origin } = shape; const divisor = DIVISOR[type]; const dimensionKeys = DIMENSION_KEYS[type]; const isTriangle = !!(shape.type === 'triangle'); const options = {}; let width = Math.abs(origin.x - pointer.x) / divisor; let height = Math.abs(origin.y - pointer.y) / divisor; if (width > strokeWidth) { width -= strokeWidth / divisor; } if (height > strokeWidth) { height -= strokeWidth / divisor; } if (shape.isRegular) { width = height = Math.max(width, height); if (isTriangle) { height = (Math.sqrt(3) / 2) * width; } } options[dimensionKeys.w] = width; options[dimensionKeys.h] = height; shape.set(options); } module.exports = { /** * Set each origin value to shape * @param {fabric.Object} shape - Shape object */ setOrigins(shape) { const leftTopPoint = shape.getPointByOrigin('left', 'top'); const rightTopPoint = shape.getPointByOrigin('right', 'top'); const rightBottomPoint = shape.getPointByOrigin('right', 'bottom'); const leftBottomPoint = shape.getPointByOrigin('left', 'bottom'); shape.origins = { lt: leftTopPoint, rt: rightTopPoint, rb: rightBottomPoint, lb: leftBottomPoint, }; }, /** * Resize the shape * @param {fabric.Object} shape - Shape object * @param {{x: number, y: number}} pointer - Mouse pointer values on canvas * @param {boolean} isScaling - Whether the resizing action is scaling or not */ resize(shape, pointer, isScaling) { if (hasCenterOrigin(shape)) { adjustOriginByStartPoint(pointer, shape); setStartPoint(shape); } if (isScaling) { adjustDimensionOnScaling(shape, pointer); } else { adjustDimensionOnMouseMove(pointer, shape); } adjustOriginByMovingPointer(pointer, shape); }, /** * Adjust the origin position of shape to center * @param {fabric.Object} shape - Shape object */ adjustOriginToCenter(shape) { const centerPoint = shape.getPointByOrigin('center', 'center'); const { originX, originY } = shape; const origin = shape.getPointByOrigin(originX, originY); const left = shape.left + (centerPoint.x - origin.x); const top = shape.top + (centerPoint.y - origin.y); shape.set({ hasControls: true, hasBorders: true, originX: 'center', originY: 'center', left, top, }); shape.setCoords(); // For left, top properties }, }; ================================================ FILE: apps/image-editor/src/js/imageEditor.js ================================================ import { fabric } from 'fabric'; import extend from 'tui-code-snippet/object/extend'; import isUndefined from 'tui-code-snippet/type/isUndefined'; import forEach from 'tui-code-snippet/collection/forEach'; import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; import Invoker from '@/invoker'; import UI from '@/ui'; import action from '@/action'; import commandFactory from '@/factory/command'; import Graphics from '@/graphics'; import { makeSelectionUndoData, makeSelectionUndoDatum } from '@/helper/selectionModifyHelper'; import { sendHostName, getObjectType } from '@/util'; import { eventNames as events, commandNames as commands, keyCodes, rejectMessages, OBJ_TYPE, } from '@/consts'; const { MOUSE_DOWN, OBJECT_MOVED, OBJECT_SCALED, OBJECT_ACTIVATED, OBJECT_ROTATED, OBJECT_ADDED, OBJECT_MODIFIED, ADD_TEXT, ADD_OBJECT, TEXT_EDITING, TEXT_CHANGED, ICON_CREATE_RESIZE, ICON_CREATE_END, SELECTION_CLEARED, SELECTION_CREATED, ADD_OBJECT_AFTER, } = events; /** * Image filter result * @typedef {object} FilterResult * @property {string} type - filter type like 'mask', 'Grayscale' and so on * @property {string} action - action type like 'add', 'remove' */ /** * Flip status * @typedef {object} FlipStatus * @property {boolean} flipX - x axis * @property {boolean} flipY - y axis * @property {Number} angle - angle */ /** * Rotation status * @typedef {Number} RotateStatus * @property {Number} angle - angle */ /** * Old and new Size * @typedef {object} SizeChange * @property {Number} oldWidth - old width * @property {Number} oldHeight - old height * @property {Number} newWidth - new width * @property {Number} newHeight - new height */ /** * @typedef {string} ErrorMsg - {string} error message */ /** * @typedef {object} ObjectProps - graphics object properties * @property {number} id - object id * @property {string} type - object type * @property {string} text - text content * @property {(string | number)} left - Left * @property {(string | number)} top - Top * @property {(string | number)} width - Width * @property {(string | number)} height - Height * @property {string} fill - Color * @property {string} stroke - Stroke * @property {(string | number)} strokeWidth - StrokeWidth * @property {string} fontFamily - Font type for text * @property {number} fontSize - Font Size * @property {string} fontStyle - Type of inclination (normal / italic) * @property {string} fontWeight - Type of thicker or thinner looking (normal / bold) * @property {string} textAlign - Type of text align (left / center / right) * @property {string} textDecoration - Type of line (underline / line-through / overline) */ /** * Shape filter option * @typedef {object.} ShapeFilterOption */ /** * Shape filter option * @typedef {object} ShapeFillOption - fill option of shape * @property {string} type - fill type ('color' or 'filter') * @property {Array.} [filter] - {@link ShapeFilterOption} List. * only applies to filter types * (ex: \[\{pixelate: 20\}, \{blur: 0.3\}\]) * @property {string} [color] - Shape foreground color (ex: '#fff', 'transparent') */ /** * Image editor * @class * @param {string|HTMLElement} wrapper - Wrapper's element or selector * @param {Object} [options] - Canvas max width & height of css * @param {number} [options.includeUI] - Use the provided UI * @param {Object} [options.includeUI.loadImage] - Basic editing image * @param {string} options.includeUI.loadImage.path - image path * @param {string} options.includeUI.loadImage.name - image name * @param {Object} [options.includeUI.theme] - Theme object * @param {Array} [options.includeUI.menu] - It can be selected when only specific menu is used, Default values are \['crop', 'flip', 'rotate', 'draw', 'shape', 'icon', 'text', 'mask', 'filter'\]. * @param {string} [options.includeUI.initMenu] - The first menu to be selected and started. * @param {Object} [options.includeUI.uiSize] - ui size of editor * @param {string} options.includeUI.uiSize.width - width of ui * @param {string} options.includeUI.uiSize.height - height of ui * @param {string} [options.includeUI.menuBarPosition=bottom] - Menu bar position('top', 'bottom', 'left', 'right') * @param {number} options.cssMaxWidth - Canvas css-max-width * @param {number} options.cssMaxHeight - Canvas css-max-height * @param {Object} [options.selectionStyle] - selection style * @param {string} [options.selectionStyle.cornerStyle] - selection corner style * @param {number} [options.selectionStyle.cornerSize] - selection corner size * @param {string} [options.selectionStyle.cornerColor] - selection corner color * @param {string} [options.selectionStyle.cornerStrokeColor] = selection corner stroke color * @param {boolean} [options.selectionStyle.transparentCorners] - selection corner transparent * @param {number} [options.selectionStyle.lineWidth] - selection line width * @param {string} [options.selectionStyle.borderColor] - selection border color * @param {number} [options.selectionStyle.rotatingPointOffset] - selection rotating point length * @param {Boolean} [options.usageStatistics=true] - Let us know the hostname. If you don't want to send the hostname, please set to false. * @example * var ImageEditor = require('tui-image-editor'); * var blackTheme = require('./js/theme/black-theme.js'); * var instance = new ImageEditor(document.querySelector('#tui-image-editor'), { * includeUI: { * loadImage: { * path: 'img/sampleImage.jpg', * name: 'SampleImage' * }, * theme: blackTheme, // or whiteTheme * menu: ['shape', 'filter'], * initMenu: 'filter', * uiSize: { * width: '1000px', * height: '700px' * }, * menuBarPosition: 'bottom' * }, * cssMaxWidth: 700, * cssMaxHeight: 500, * selectionStyle: { * cornerSize: 20, * rotatingPointOffset: 70 * } * }); */ class ImageEditor { constructor(wrapper, options) { options = extend( { includeUI: false, usageStatistics: true, }, options ); this.mode = null; this.activeObjectId = null; /** * UI instance * @type {Ui} */ if (options.includeUI) { const UIOption = options.includeUI; UIOption.usageStatistics = options.usageStatistics; this.ui = new UI(wrapper, UIOption, this.getActions()); options = this.ui.setUiDefaultSelectionStyle(options); } /** * Invoker * @type {Invoker} * @private */ this._invoker = new Invoker(); /** * Graphics instance * @type {Graphics} * @private */ this._graphics = new Graphics(this.ui ? this.ui.getEditorArea() : wrapper, { cssMaxWidth: options.cssMaxWidth, cssMaxHeight: options.cssMaxHeight, }); /** * Event handler list * @type {Object} * @private */ this._handlers = { keydown: this._onKeyDown.bind(this), mousedown: this._onMouseDown.bind(this), objectActivated: this._onObjectActivated.bind(this), objectMoved: this._onObjectMoved.bind(this), objectScaled: this._onObjectScaled.bind(this), objectRotated: this._onObjectRotated.bind(this), objectAdded: this._onObjectAdded.bind(this), objectModified: this._onObjectModified.bind(this), createdPath: this._onCreatedPath, addText: this._onAddText.bind(this), addObject: this._onAddObject.bind(this), textEditing: this._onTextEditing.bind(this), textChanged: this._onTextChanged.bind(this), iconCreateResize: this._onIconCreateResize.bind(this), iconCreateEnd: this._onIconCreateEnd.bind(this), selectionCleared: this._selectionCleared.bind(this), selectionCreated: this._selectionCreated.bind(this), }; this._attachInvokerEvents(); this._attachGraphicsEvents(); this._attachDomEvents(); this._setSelectionStyle(options.selectionStyle, { applyCropSelectionStyle: options.applyCropSelectionStyle, applyGroupSelectionStyle: options.applyGroupSelectionStyle, }); if (options.usageStatistics) { sendHostName(); } if (this.ui) { this.ui.initCanvas(); this.setReAction(); this._attachColorPickerInputBoxEvents(); } fabric.enableGLFiltering = false; } _attachColorPickerInputBoxEvents() { this.ui.on(events.INPUT_BOX_EDITING_STARTED, () => { this.isColorPickerInputBoxEditing = true; }); this.ui.on(events.INPUT_BOX_EDITING_STOPPED, () => { this.isColorPickerInputBoxEditing = false; }); } _detachColorPickerInputBoxEvents() { this.ui.off(events.INPUT_BOX_EDITING_STARTED); this.ui.off(events.INPUT_BOX_EDITING_STOPPED); } /** * Set selection style by init option * @param {Object} selectionStyle - Selection styles * @param {Object} applyTargets - Selection apply targets * @param {boolean} applyCropSelectionStyle - whether apply with crop selection style or not * @param {boolean} applyGroupSelectionStyle - whether apply with group selection style or not * @private */ _setSelectionStyle(selectionStyle, { applyCropSelectionStyle, applyGroupSelectionStyle }) { if (selectionStyle) { this._graphics.setSelectionStyle(selectionStyle); } if (applyCropSelectionStyle) { this._graphics.setCropSelectionStyle(selectionStyle); } if (applyGroupSelectionStyle) { this.on('selectionCreated', (eventTarget) => { if (eventTarget.type === 'activeSelection') { eventTarget.set(selectionStyle); } }); } } /** * Attach invoker events * @private */ _attachInvokerEvents() { const { UNDO_STACK_CHANGED, REDO_STACK_CHANGED, EXECUTE_COMMAND, AFTER_UNDO, AFTER_REDO, HAND_STARTED, HAND_STOPPED, } = events; /** * Undo stack changed event * @event ImageEditor#undoStackChanged * @param {Number} length - undo stack length * @example * imageEditor.on('undoStackChanged', function(length) { * console.log(length); * }); */ this._invoker.on(UNDO_STACK_CHANGED, this.fire.bind(this, UNDO_STACK_CHANGED)); /** * Redo stack changed event * @event ImageEditor#redoStackChanged * @param {Number} length - redo stack length * @example * imageEditor.on('redoStackChanged', function(length) { * console.log(length); * }); */ this._invoker.on(REDO_STACK_CHANGED, this.fire.bind(this, REDO_STACK_CHANGED)); if (this.ui) { const canvas = this._graphics.getCanvas(); this._invoker.on(EXECUTE_COMMAND, (command) => this.ui.fire(EXECUTE_COMMAND, command)); this._invoker.on(AFTER_UNDO, (command) => this.ui.fire(AFTER_UNDO, command)); this._invoker.on(AFTER_REDO, (command) => this.ui.fire(AFTER_REDO, command)); canvas.on(HAND_STARTED, () => this.ui.fire(HAND_STARTED)); canvas.on(HAND_STOPPED, () => this.ui.fire(HAND_STOPPED)); } } /** * Attach canvas events * @private */ _attachGraphicsEvents() { this._graphics.on({ [MOUSE_DOWN]: this._handlers.mousedown, [OBJECT_MOVED]: this._handlers.objectMoved, [OBJECT_SCALED]: this._handlers.objectScaled, [OBJECT_ROTATED]: this._handlers.objectRotated, [OBJECT_ACTIVATED]: this._handlers.objectActivated, [OBJECT_ADDED]: this._handlers.objectAdded, [OBJECT_MODIFIED]: this._handlers.objectModified, [ADD_TEXT]: this._handlers.addText, [ADD_OBJECT]: this._handlers.addObject, [TEXT_EDITING]: this._handlers.textEditing, [TEXT_CHANGED]: this._handlers.textChanged, [ICON_CREATE_RESIZE]: this._handlers.iconCreateResize, [ICON_CREATE_END]: this._handlers.iconCreateEnd, [SELECTION_CLEARED]: this._handlers.selectionCleared, [SELECTION_CREATED]: this._handlers.selectionCreated, }); } /** * Attach dom events * @private */ _attachDomEvents() { // ImageEditor supports IE 9 higher document.addEventListener('keydown', this._handlers.keydown); } /** * Detach dom events * @private */ _detachDomEvents() { // ImageEditor supports IE 9 higher document.removeEventListener('keydown', this._handlers.keydown); } /** * Keydown event handler * @param {KeyboardEvent} e - Event object * @private */ /* eslint-disable complexity */ _onKeyDown(e) { const { ctrlKey, keyCode, metaKey } = e; const isModifierKey = ctrlKey || metaKey; if (isModifierKey) { if (keyCode === keyCodes.C) { this._graphics.resetTargetObjectForCopyPaste(); } else if (keyCode === keyCodes.V) { this._graphics.pasteObject(); this.clearRedoStack(); } else if (keyCode === keyCodes.Z) { // There is no error message on shortcut when it's empty this.undo()['catch'](() => {}); } else if (keyCode === keyCodes.Y) { // There is no error message on shortcut when it's empty this.redo()['catch'](() => {}); } } const isDeleteKey = keyCode === keyCodes.BACKSPACE || keyCode === keyCodes.DEL; const isRemoveReady = this._graphics.isReadyRemoveObject(); if (!this.isColorPickerInputBoxEditing && isRemoveReady && isDeleteKey) { e.preventDefault(); this.removeActiveObject(); } } /** * Remove Active Object */ removeActiveObject() { const activeObjectId = this._graphics.getActiveObjectIdForRemove(); this.removeObject(activeObjectId); } /** * mouse down event handler * @param {Event} event - mouse down event * @param {Object} originPointer - origin pointer * @param {Number} originPointer.x x position * @param {Number} originPointer.y y position * @private */ _onMouseDown(event, originPointer) { /** * The mouse down event with position x, y on canvas * @event ImageEditor#mousedown * @param {Object} event - browser mouse event object * @param {Object} originPointer origin pointer * @param {Number} originPointer.x x position * @param {Number} originPointer.y y position * @example * imageEditor.on('mousedown', function(event, originPointer) { * console.log(event); * console.log(originPointer); * if (imageEditor.hasFilter('colorFilter')) { * imageEditor.applyFilter('colorFilter', { * x: parseInt(originPointer.x, 10), * y: parseInt(originPointer.y, 10) * }); * } * }); */ this.fire(events.MOUSE_DOWN, event, originPointer); } /** * Add a 'addObject' command * @param {Object} obj - Fabric object * @private */ _pushAddObjectCommand(obj) { const command = commandFactory.create(commands.ADD_OBJECT, this._graphics, obj); this._invoker.pushUndoStack(command); } /** * Add a 'changeSelection' command * @param {fabric.Object} obj - selection object * @private */ _pushModifyObjectCommand(obj) { const { type } = obj; const props = makeSelectionUndoData(obj, (item) => makeSelectionUndoDatum(this._graphics.getObjectId(item), item, type === 'activeSelection') ); const command = commandFactory.create(commands.CHANGE_SELECTION, this._graphics, props); command.execute(this._graphics, props); this._invoker.pushUndoStack(command); } /** * 'objectActivated' event handler * @param {ObjectProps} props - object properties * @private */ _onObjectActivated(props) { /** * The event when object is selected(aka activated). * @event ImageEditor#objectActivated * @param {ObjectProps} objectProps - object properties * @example * imageEditor.on('objectActivated', function(props) { * console.log(props); * console.log(props.type); * console.log(props.id); * }); */ this.fire(events.OBJECT_ACTIVATED, props); } /** * 'objectMoved' event handler * @param {ObjectProps} props - object properties * @private */ _onObjectMoved(props) { /** * The event when object is moved * @event ImageEditor#objectMoved * @param {ObjectProps} props - object properties * @example * imageEditor.on('objectMoved', function(props) { * console.log(props); * console.log(props.type); * }); */ this.fire(events.OBJECT_MOVED, props); } /** * 'objectScaled' event handler * @param {ObjectProps} props - object properties * @private */ _onObjectScaled(props) { /** * The event when scale factor is changed * @event ImageEditor#objectScaled * @param {ObjectProps} props - object properties * @example * imageEditor.on('objectScaled', function(props) { * console.log(props); * console.log(props.type); * }); */ this.fire(events.OBJECT_SCALED, props); } /** * 'objectRotated' event handler * @param {ObjectProps} props - object properties * @private */ _onObjectRotated(props) { /** * The event when object angle is changed * @event ImageEditor#objectRotated * @param {ObjectProps} props - object properties * @example * imageEditor.on('objectRotated', function(props) { * console.log(props); * console.log(props.type); * }); */ this.fire(events.OBJECT_ROTATED, props); } /** * Get current drawing mode * @returns {string} * @example * // Image editor drawing mode * // * // NORMAL: 'NORMAL' * // CROPPER: 'CROPPER' * // FREE_DRAWING: 'FREE_DRAWING' * // LINE_DRAWING: 'LINE_DRAWING' * // TEXT: 'TEXT' * // * if (imageEditor.getDrawingMode() === 'FREE_DRAWING') { * imageEditor.stopDrawingMode(); * } */ getDrawingMode() { return this._graphics.getDrawingMode(); } /** * Clear all objects * @returns {Promise} * @example * imageEditor.clearObjects(); */ clearObjects() { return this.execute(commands.CLEAR_OBJECTS); } /** * Deactivate all objects * @example * imageEditor.deactivateAll(); */ deactivateAll() { this._graphics.deactivateAll(); this._graphics.renderAll(); } /** * discard selction * @example * imageEditor.discardSelection(); */ discardSelection() { this._graphics.discardSelection(); } /** * selectable status change * @param {boolean} selectable - selectable status * @example * imageEditor.changeSelectableAll(false); // or true */ changeSelectableAll(selectable) { this._graphics.changeSelectableAll(selectable); } /** * Init history */ _initHistory() { if (this.ui) { this.ui.initHistory(); } } /** * Clear history */ _clearHistory() { if (this.ui) { this.ui.clearHistory(); } } /** * Invoke command * @param {String} commandName - Command name * @param {...*} args - Arguments for creating command * @returns {Promise} * @private */ execute(commandName, ...args) { // Inject an Graphics instance as first parameter const theArgs = [this._graphics].concat(args); return this._invoker.execute(commandName, ...theArgs); } /** * Invoke command * @param {String} commandName - Command name * @param {...*} args - Arguments for creating command * @returns {Promise} * @private */ executeSilent(commandName, ...args) { // Inject an Graphics instance as first parameter const theArgs = [this._graphics].concat(args); return this._invoker.executeSilent(commandName, ...theArgs); } /** * Undo * @param {number} [iterationCount=1] - Iteration count of undo * @returns {Promise} * @example * imageEditor.undo(); */ undo(iterationCount = 1) { let promise = Promise.resolve(); for (let i = 0; i < iterationCount; i += 1) { promise = promise.then(() => this._invoker.undo()); } return promise; } /** * Redo * @param {number} [iterationCount=1] - Iteration count of redo * @returns {Promise} * @example * imageEditor.redo(); */ redo(iterationCount = 1) { let promise = Promise.resolve(); for (let i = 0; i < iterationCount; i += 1) { promise = promise.then(() => this._invoker.redo()); } return promise; } /** * Zoom * @param {number} x - x axis of center point for zoom * @param {number} y - y axis of center point for zoom * @param {number} zoomLevel - level of zoom(1.0 ~ 5.0) */ zoom({ x, y, zoomLevel }) { this._graphics.zoom({ x, y }, zoomLevel); } /** * Reset zoom. Change zoom level to 1.0 */ resetZoom() { this._graphics.resetZoom(); } /** * Load image from file * @param {File} imgFile - Image file * @param {string} [imageName] - imageName * @returns {Promise} * @example * imageEditor.loadImageFromFile(file).then(result => { * console.log('old : ' + result.oldWidth + ', ' + result.oldHeight); * console.log('new : ' + result.newWidth + ', ' + result.newHeight); * }); */ loadImageFromFile(imgFile, imageName) { if (!imgFile) { return Promise.reject(rejectMessages.invalidParameters); } const imgUrl = URL.createObjectURL(imgFile); imageName = imageName || imgFile.name; return this.loadImageFromURL(imgUrl, imageName).then((value) => { URL.revokeObjectURL(imgFile); return value; }); } /** * Load image from url * @param {string} url - File url * @param {string} imageName - imageName * @returns {Promise} * @example * imageEditor.loadImageFromURL('http://url/testImage.png', 'lena').then(result => { * console.log('old : ' + result.oldWidth + ', ' + result.oldHeight); * console.log('new : ' + result.newWidth + ', ' + result.newHeight); * }); */ loadImageFromURL(url, imageName) { if (!imageName || !url) { return Promise.reject(rejectMessages.invalidParameters); } return this.execute(commands.LOAD_IMAGE, imageName, url); } /** * Add image object on canvas * @param {string} imgUrl - Image url to make object * @returns {Promise} * @example * imageEditor.addImageObject('path/fileName.jpg').then(objectProps => { * console.log(ojectProps.id); * }); */ addImageObject(imgUrl) { if (!imgUrl) { return Promise.reject(rejectMessages.invalidParameters); } return this.execute(commands.ADD_IMAGE_OBJECT, imgUrl); } /** * Start a drawing mode. If the current mode is not 'NORMAL', 'stopDrawingMode()' will be called first. * @param {String} mode Can be one of 'CROPPER', 'FREE_DRAWING', 'LINE_DRAWING', 'TEXT', 'SHAPE' * @param {Object} [option] parameters of drawing mode, it's available with 'FREE_DRAWING', 'LINE_DRAWING' * @param {Number} [option.width] brush width * @param {String} [option.color] brush color * @param {Object} [option.arrowType] arrow decorate * @param {string} [option.arrowType.tail] arrow decorate for tail. 'chevron' or 'triangle' * @param {string} [option.arrowType.head] arrow decorate for head. 'chevron' or 'triangle' * @returns {boolean} true if success or false * @example * imageEditor.startDrawingMode('FREE_DRAWING', { * width: 10, * color: 'rgba(255,0,0,0.5)' * }); * imageEditor.startDrawingMode('LINE_DRAWING', { * width: 10, * color: 'rgba(255,0,0,0.5)', * arrowType: { * tail: 'chevron' // triangle * } * }); * */ startDrawingMode(mode, option) { return this._graphics.startDrawingMode(mode, option); } /** * Stop the current drawing mode and back to the 'NORMAL' mode * @example * imageEditor.stopDrawingMode(); */ stopDrawingMode() { this._graphics.stopDrawingMode(); } /** * Crop this image with rect * @param {Object} rect crop rect * @param {Number} rect.left left position * @param {Number} rect.top top position * @param {Number} rect.width width * @param {Number} rect.height height * @returns {Promise} * @example * imageEditor.crop(imageEditor.getCropzoneRect()); */ crop(rect) { const data = this._graphics.getCroppedImageData(rect); if (!data) { return Promise.reject(rejectMessages.invalidParameters); } return this.loadImageFromURL(data.url, data.imageName); } /** * Get the cropping rect * @returns {Object} {{left: number, top: number, width: number, height: number}} rect */ getCropzoneRect() { return this._graphics.getCropzoneRect(); } /** * Set the cropping rect * @param {number} [mode] crop rect mode [1, 1.5, 1.3333333333333333, 1.25, 1.7777777777777777] */ setCropzoneRect(mode) { this._graphics.setCropzoneRect(mode); } /** * Flip * @returns {Promise} * @param {string} type - 'flipX' or 'flipY' or 'reset' * @returns {Promise} * @private */ _flip(type) { return this.execute(commands.FLIP_IMAGE, type); } /** * Flip x * @returns {Promise} * @example * imageEditor.flipX().then((status => { * console.log('flipX: ', status.flipX); * console.log('flipY: ', status.flipY); * console.log('angle: ', status.angle); * }).catch(message => { * console.log('error: ', message); * }); */ flipX() { return this._flip('flipX'); } /** * Flip y * @returns {Promise} * @example * imageEditor.flipY().then(status => { * console.log('flipX: ', status.flipX); * console.log('flipY: ', status.flipY); * console.log('angle: ', status.angle); * }).catch(message => { * console.log('error: ', message); * }); */ flipY() { return this._flip('flipY'); } /** * Reset flip * @returns {Promise} * @example * imageEditor.resetFlip().then(status => { * console.log('flipX: ', status.flipX); * console.log('flipY: ', status.flipY); * console.log('angle: ', status.angle); * }).catch(message => { * console.log('error: ', message); * });; */ resetFlip() { return this._flip('reset'); } /** * @param {string} type - 'rotate' or 'setAngle' * @param {number} angle - angle value (degree) * @param {boolean} isSilent - is silent execution or not * @returns {Promise} * @private */ _rotate(type, angle, isSilent) { let result = null; if (isSilent) { result = this.executeSilent(commands.ROTATE_IMAGE, type, angle); } else { result = this.execute(commands.ROTATE_IMAGE, type, angle); } return result; } /** * Rotate image * @returns {Promise} * @param {number} angle - Additional angle to rotate image * @param {boolean} isSilent - is silent execution or not * @returns {Promise} * @example * imageEditor.rotate(10); // angle = 10 * imageEditor.rotate(10); // angle = 20 * imageEditor.rotate(5); // angle = 5 * imageEditor.rotate(-95); // angle = -90 * imageEditor.rotate(10).then(status => { * console.log('angle: ', status.angle); * })).catch(message => { * console.log('error: ', message); * }); */ rotate(angle, isSilent) { return this._rotate('rotate', angle, isSilent); } /** * Set angle * @param {number} angle - Angle of image * @param {boolean} isSilent - is silent execution or not * @returns {Promise} * @example * imageEditor.setAngle(10); // angle = 10 * imageEditor.rotate(10); // angle = 20 * imageEditor.setAngle(5); // angle = 5 * imageEditor.rotate(50); // angle = 55 * imageEditor.setAngle(-40); // angle = -40 * imageEditor.setAngle(10).then(status => { * console.log('angle: ', status.angle); * })).catch(message => { * console.log('error: ', message); * }); */ setAngle(angle, isSilent) { return this._rotate('setAngle', angle, isSilent); } /** * Set drawing brush * @param {Object} option brush option * @param {Number} option.width width * @param {String} option.color color like 'FFFFFF', 'rgba(0, 0, 0, 0.5)' * @example * imageEditor.startDrawingMode('FREE_DRAWING'); * imageEditor.setBrush({ * width: 12, * color: 'rgba(0, 0, 0, 0.5)' * }); * imageEditor.setBrush({ * width: 8, * color: 'FFFFFF' * }); */ setBrush(option) { this._graphics.setBrush(option); } /** * Set states of current drawing shape * @param {string} type - Shape type (ex: 'rect', 'circle', 'triangle') * @param {Object} [options] - Shape options * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or * Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stoke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {number} [options.isRegular] - Whether resizing shape has 1:1 ratio or not * @example * imageEditor.setDrawingShape('rect', { * fill: 'red', * width: 100, * height: 200 * }); * @example * imageEditor.setDrawingShape('rect', { * fill: { * type: 'filter', * filter: [{blur: 0.3}, {pixelate: 20}] * }, * width: 100, * height: 200 * }); * @example * imageEditor.setDrawingShape('circle', { * fill: 'transparent', * stroke: 'blue', * strokeWidth: 3, * rx: 10, * ry: 100 * }); * @example * imageEditor.setDrawingShape('triangle', { // When resizing, the shape keep the 1:1 ratio * width: 1, * height: 1, * isRegular: true * }); * @example * imageEditor.setDrawingShape('circle', { // When resizing, the shape keep the 1:1 ratio * rx: 10, * ry: 10, * isRegular: true * }); */ setDrawingShape(type, options) { this._graphics.setDrawingShape(type, options); } setDrawingIcon(type, iconColor) { this._graphics.setIconStyle(type, iconColor); } /** * Add shape * @param {string} type - Shape type (ex: 'rect', 'circle', 'triangle') * @param {Object} options - Shape options * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or * Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stroke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {number} [options.left] - Shape x position * @param {number} [options.top] - Shape y position * @param {boolean} [options.isRegular] - Whether resizing shape has 1:1 ratio or not * @returns {Promise} * @example * imageEditor.addShape('rect', { * fill: 'red', * stroke: 'blue', * strokeWidth: 3, * width: 100, * height: 200, * left: 10, * top: 10, * isRegular: true * }); * @example * imageEditor.addShape('circle', { * fill: 'red', * stroke: 'blue', * strokeWidth: 3, * rx: 10, * ry: 100, * isRegular: false * }).then(objectProps => { * console.log(objectProps.id); * }); * @example * imageEditor.addShape('rect', { * fill: { * type: 'filter', * filter: [{blur: 0.3}, {pixelate: 20}] * }, * stroke: 'blue', * strokeWidth: 3, * rx: 10, * ry: 100, * isRegular: false * }).then(objectProps => { * console.log(objectProps.id); * }); */ addShape(type, options) { options = options || {}; this._setPositions(options); return this.execute(commands.ADD_SHAPE, type, options); } /** * Change shape * @param {number} id - object id * @param {Object} options - Shape options * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or * Shape foreground color (ex: '#fff', 'transparent') * @param {string} [options.stroke] - Shape outline color * @param {number} [options.strokeWidth] - Shape outline width * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) * @param {boolean} [options.isRegular] - Whether resizing shape has 1:1 ratio or not * @param {boolean} isSilent - is silent execution or not * @returns {Promise} * @example * // call after selecting shape object on canvas * imageEditor.changeShape(id, { // change rectagle or triangle * fill: 'red', * stroke: 'blue', * strokeWidth: 3, * width: 100, * height: 200 * }); * @example * // call after selecting shape object on canvas * imageEditor.changeShape(id, { // change circle * fill: 'red', * stroke: 'blue', * strokeWidth: 3, * rx: 10, * ry: 100 * }); */ changeShape(id, options, isSilent) { const executeMethodName = isSilent ? 'executeSilent' : 'execute'; return this[executeMethodName](commands.CHANGE_SHAPE, id, options); } /** * Add text on image * @param {string} text - Initial input text * @param {Object} [options] Options for generating text * @param {Object} [options.styles] Initial styles * @param {string} [options.styles.fill] Color * @param {string} [options.styles.fontFamily] Font type for text * @param {number} [options.styles.fontSize] Size * @param {string} [options.styles.fontStyle] Type of inclination (normal / italic) * @param {string} [options.styles.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [options.styles.textAlign] Type of text align (left / center / right) * @param {string} [options.styles.textDecoration] Type of line (underline / line-through / overline) * @param {{x: number, y: number}} [options.position] - Initial position * @param {boolean} [options.autofocus] - text autofocus, default is true * @returns {Promise} * @example * imageEditor.addText('init text'); * @example * imageEditor.addText('init text', { * styles: { * fill: '#000', * fontSize: 20, * fontWeight: 'bold' * }, * position: { * x: 10, * y: 10 * } * }).then(objectProps => { * console.log(objectProps.id); * }); */ addText(text, options) { text = text || ''; options = options || {}; return this.execute(commands.ADD_TEXT, text, options); } /** * Change contents of selected text object on image * @param {number} id - object id * @param {string} text - Changing text * @returns {Promise} * @example * imageEditor.changeText(id, 'change text'); */ changeText(id, text) { text = text || ''; return this.execute(commands.CHANGE_TEXT, id, text); } /** * Set style * @param {number} id - object id * @param {Object} styleObj - text styles * @param {string} [styleObj.fill] Color * @param {string} [styleObj.fontFamily] Font type for text * @param {number} [styleObj.fontSize] Size * @param {string} [styleObj.fontStyle] Type of inclination (normal / italic) * @param {string} [styleObj.fontWeight] Type of thicker or thinner looking (normal / bold) * @param {string} [styleObj.textAlign] Type of text align (left / center / right) * @param {string} [styleObj.textDecoration] Type of line (underline / line-through / overline) * @param {boolean} isSilent - is silent execution or not * @returns {Promise} * @example * imageEditor.changeTextStyle(id, { * fontStyle: 'italic' * }); */ changeTextStyle(id, styleObj, isSilent) { const executeMethodName = isSilent ? 'executeSilent' : 'execute'; return this[executeMethodName](commands.CHANGE_TEXT_STYLE, id, styleObj); } /** * change text mode * @param {string} type - change type * @private */ _changeActivateMode(type) { if (type !== 'ICON' && this.getDrawingMode() !== type) { this.startDrawingMode(type); } } /** * 'textChanged' event handler * @param {Object} target - changed text object * @private */ _onTextChanged(target) { this.fire(events.TEXT_CHANGED, target); } /** * 'iconCreateResize' event handler * @param {Object} originPointer origin pointer * @param {Number} originPointer.x x position * @param {Number} originPointer.y y position * @private */ _onIconCreateResize(originPointer) { this.fire(events.ICON_CREATE_RESIZE, originPointer); } /** * 'iconCreateEnd' event handler * @param {Object} originPointer origin pointer * @param {Number} originPointer.x x position * @param {Number} originPointer.y y position * @private */ _onIconCreateEnd(originPointer) { this.fire(events.ICON_CREATE_END, originPointer); } /** * 'textEditing' event handler * @private */ _onTextEditing() { /** * The event which starts to edit text object * @event ImageEditor#textEditing * @example * imageEditor.on('textEditing', function() { * console.log('text editing'); * }); */ this.fire(events.TEXT_EDITING); } /** * Mousedown event handler in case of 'TEXT' drawing mode * @param {fabric.Event} event - Current mousedown event object * @private */ _onAddText(event) { /** * The event when 'TEXT' drawing mode is enabled and click non-object area. * @event ImageEditor#addText * @param {Object} pos * @param {Object} pos.originPosition - Current position on origin canvas * @param {Number} pos.originPosition.x - x * @param {Number} pos.originPosition.y - y * @param {Object} pos.clientPosition - Current position on client area * @param {Number} pos.clientPosition.x - x * @param {Number} pos.clientPosition.y - y * @example * imageEditor.on('addText', function(pos) { * console.log('text position on canvas: ' + pos.originPosition); * console.log('text position on brwoser: ' + pos.clientPosition); * }); */ this.fire(events.ADD_TEXT, { originPosition: event.originPosition, clientPosition: event.clientPosition, }); } /** * 'addObject' event handler * @param {Object} objectProps added object properties * @private */ _onAddObject(objectProps) { const obj = this._graphics.getObject(objectProps.id); this._invoker.fire(events.EXECUTE_COMMAND, getObjectType(obj.type)); this._pushAddObjectCommand(obj); } /** * 'objectAdded' event handler * @param {Object} objectProps added object properties * @private */ _onObjectAdded(objectProps) { /** * The event when object added * @event ImageEditor#objectAdded * @param {ObjectProps} props - object properties * @example * imageEditor.on('objectAdded', function(props) { * console.log(props); * }); */ this.fire(OBJECT_ADDED, objectProps); /** * The event when object added (deprecated) * @event ImageEditor#addObjectAfter * @param {ObjectProps} props - object properties * @deprecated */ this.fire(ADD_OBJECT_AFTER, objectProps); } /** * 'objectModified' event handler * @param {fabric.Object} obj - selection object * @private */ _onObjectModified(obj) { if (obj.type !== OBJ_TYPE.CROPZONE) { this._invoker.fire(events.EXECUTE_COMMAND, getObjectType(obj.type)); this._pushModifyObjectCommand(obj); } } /** * 'selectionCleared' event handler * @private */ _selectionCleared() { this.fire(SELECTION_CLEARED); } /** * 'selectionCreated' event handler * @param {Object} eventTarget - Fabric object * @private */ _selectionCreated(eventTarget) { this.fire(SELECTION_CREATED, eventTarget); } /** * Register custom icons * @param {{iconType: string, pathValue: string}} infos - Infos to register icons * @example * imageEditor.registerIcons({ * customIcon: 'M 0 0 L 20 20 L 10 10 Z', * customArrow: 'M 60 0 L 120 60 H 90 L 75 45 V 180 H 45 V 45 L 30 60 H 0 Z' * }); */ registerIcons(infos) { this._graphics.registerPaths(infos); } /** * Change canvas cursor type * @param {string} cursorType - cursor type * @example * imageEditor.changeCursor('crosshair'); */ changeCursor(cursorType) { this._graphics.changeCursor(cursorType); } /** * Add icon on canvas * @param {string} type - Icon type ('arrow', 'cancel', custom icon name) * @param {Object} options - Icon options * @param {string} [options.fill] - Icon foreground color * @param {number} [options.left] - Icon x position * @param {number} [options.top] - Icon y position * @returns {Promise} * @example * imageEditor.addIcon('arrow'); // The position is center on canvas * @example * imageEditor.addIcon('arrow', { * left: 100, * top: 100 * }).then(objectProps => { * console.log(objectProps.id); * }); */ addIcon(type, options) { options = options || {}; this._setPositions(options); return this.execute(commands.ADD_ICON, type, options); } /** * Change icon color * @param {number} id - object id * @param {string} color - Color for icon * @returns {Promise} * @example * imageEditor.changeIconColor(id, '#000000'); */ changeIconColor(id, color) { return this.execute(commands.CHANGE_ICON_COLOR, id, color); } /** * Remove an object or group by id * @param {number} id - object id * @returns {Promise} * @example * imageEditor.removeObject(id); */ removeObject(id) { const { type } = this._graphics.getObject(id); return this.execute(commands.REMOVE_OBJECT, id, getObjectType(type)); } /** * Whether it has the filter or not * @param {string} type - Filter type * @returns {boolean} true if it has the filter */ hasFilter(type) { return this._graphics.hasFilter(type); } /** * Remove filter on canvas image * @param {string} type - Filter type * @returns {Promise} * @example * imageEditor.removeFilter('Grayscale').then(obj => { * console.log('filterType: ', obj.type); * console.log('actType: ', obj.action); * }).catch(message => { * console.log('error: ', message); * }); */ removeFilter(type) { return this.execute(commands.REMOVE_FILTER, type); } /** * Apply filter on canvas image * @param {string} type - Filter type * @param {object} options - Options to apply filter * @param {boolean} isSilent - is silent execution or not * @returns {Promise} * @example * imageEditor.applyFilter('Grayscale'); * @example * imageEditor.applyFilter('mask', {maskObjId: id}).then(obj => { * console.log('filterType: ', obj.type); * console.log('actType: ', obj.action); * }).catch(message => { * console.log('error: ', message); * });; */ applyFilter(type, options, isSilent) { const executeMethodName = isSilent ? 'executeSilent' : 'execute'; return this[executeMethodName](commands.APPLY_FILTER, type, options); } /** * Get data url * @param {Object} options - options for toDataURL * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in fabric v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in fabric v1.2.14 * @param {Number} [options.width] Cropping width. Introduced in fabric v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in fabric v1.2.14 * @returns {string} A DOMString containing the requested data URI * @example * imgEl.src = imageEditor.toDataURL(); * * imageEditor.loadImageFromURL(imageEditor.toDataURL(), 'FilterImage').then(() => { * imageEditor.addImageObject(imgUrl); * }); */ toDataURL(options) { return this._graphics.toDataURL(options); } /** * Get image name * @returns {string} image name * @example * console.log(imageEditor.getImageName()); */ getImageName() { return this._graphics.getImageName(); } /** * Clear undoStack * @example * imageEditor.clearUndoStack(); */ clearUndoStack() { this._invoker.clearUndoStack(); } /** * Clear redoStack * @example * imageEditor.clearRedoStack(); */ clearRedoStack() { this._invoker.clearRedoStack(); } /** * Whehter the undo stack is empty or not * @returns {boolean} * imageEditor.isEmptyUndoStack(); */ isEmptyUndoStack() { return this._invoker.isEmptyUndoStack(); } /** * Whehter the redo stack is empty or not * @returns {boolean} * imageEditor.isEmptyRedoStack(); */ isEmptyRedoStack() { return this._invoker.isEmptyRedoStack(); } /** * Resize canvas dimension * @param {{width: number, height: number}} dimension - Max width & height * @returns {Promise} */ resizeCanvasDimension(dimension) { if (!dimension) { return Promise.reject(rejectMessages.invalidParameters); } return this.execute(commands.RESIZE_CANVAS_DIMENSION, dimension); } /** * Destroy */ destroy() { this.stopDrawingMode(); this._detachDomEvents(); this._graphics.destroy(); this._graphics = null; if (this.ui) { this._detachColorPickerInputBoxEvents(); this.ui.destroy(); } forEach( this, (value, key) => { this[key] = null; }, this ); } /** * Set position * @param {Object} options - Position options (left or top) * @private */ _setPositions(options) { const centerPosition = this._graphics.getCenter(); if (isUndefined(options.left)) { options.left = centerPosition.left; } if (isUndefined(options.top)) { options.top = centerPosition.top; } } /** * Set properties of active object * @param {number} id - object id * @param {Object} keyValue - key & value * @returns {Promise} * @example * imageEditor.setObjectProperties(id, { * left:100, * top:100, * width: 200, * height: 200, * opacity: 0.5 * }); */ setObjectProperties(id, keyValue) { return this.execute(commands.SET_OBJECT_PROPERTIES, id, keyValue); } /** * Set properties of active object, Do not leave an invoke history. * @param {number} id - object id * @param {Object} keyValue - key & value * @example * imageEditor.setObjectPropertiesQuietly(id, { * left:100, * top:100, * width: 200, * height: 200, * opacity: 0.5 * }); */ setObjectPropertiesQuietly(id, keyValue) { this._graphics.setObjectProperties(id, keyValue); } /** * Get properties of active object corresponding key * @param {number} id - object id * @param {Array|ObjectProps|string} keys - property's key * @returns {ObjectProps} properties if id is valid or null * @example * var props = imageEditor.getObjectProperties(id, 'left'); * console.log(props); * @example * var props = imageEditor.getObjectProperties(id, ['left', 'top', 'width', 'height']); * console.log(props); * @example * var props = imageEditor.getObjectProperties(id, { * left: null, * top: null, * width: null, * height: null, * opacity: null * }); * console.log(props); */ getObjectProperties(id, keys) { const object = this._graphics.getObject(id); if (!object) { return null; } return this._graphics.getObjectProperties(id, keys); } /** * Get the canvas size * @returns {Object} {{width: number, height: number}} canvas size * @example * var canvasSize = imageEditor.getCanvasSize(); * console.log(canvasSize.width); * console.height(canvasSize.height); */ getCanvasSize() { return this._graphics.getCanvasSize(); } /** * Get object position by originX, originY * @param {number} id - object id * @param {string} originX - can be 'left', 'center', 'right' * @param {string} originY - can be 'top', 'center', 'bottom' * @returns {Object} {{x:number, y: number}} position by origin if id is valid, or null * @example * var position = imageEditor.getObjectPosition(id, 'left', 'top'); * console.log(position); */ getObjectPosition(id, originX, originY) { return this._graphics.getObjectPosition(id, originX, originY); } /** * Set object position by originX, originY * @param {number} id - object id * @param {Object} posInfo - position object * @param {number} posInfo.x - x position * @param {number} posInfo.y - y position * @param {string} posInfo.originX - can be 'left', 'center', 'right' * @param {string} posInfo.originY - can be 'top', 'center', 'bottom' * @returns {Promise} * @example * // align the object to 'left', 'top' * imageEditor.setObjectPosition(id, { * x: 0, * y: 0, * originX: 'left', * originY: 'top' * }); * @example * // align the object to 'right', 'top' * var canvasSize = imageEditor.getCanvasSize(); * imageEditor.setObjectPosition(id, { * x: canvasSize.width, * y: 0, * originX: 'right', * originY: 'top' * }); * @example * // align the object to 'left', 'bottom' * var canvasSize = imageEditor.getCanvasSize(); * imageEditor.setObjectPosition(id, { * x: 0, * y: canvasSize.height, * originX: 'left', * originY: 'bottom' * }); * @example * // align the object to 'right', 'bottom' * var canvasSize = imageEditor.getCanvasSize(); * imageEditor.setObjectPosition(id, { * x: canvasSize.width, * y: canvasSize.height, * originX: 'right', * originY: 'bottom' * }); */ setObjectPosition(id, posInfo) { return this.execute(commands.SET_OBJECT_POSITION, id, posInfo); } /** * @param {object} dimensions - Image Dimensions * @returns {Promise} */ resize(dimensions) { return this.execute(commands.RESIZE_IMAGE, dimensions); } } action.mixin(ImageEditor); CustomEvents.mixin(ImageEditor); export default ImageEditor; ================================================ FILE: apps/image-editor/src/js/interface/command.js ================================================ import extend from 'tui-code-snippet/object/extend'; import errorMessage from '@/factory/errorMessage'; const createMessage = errorMessage.create; const errorTypes = errorMessage.types; /** * Command class * @class * @param {{name:function, execute: function, undo: function, * executeCallback: function, undoCallback: function}} actions - Command actions * @param {Array} args - passing arguments on execute, undo * @ignore */ class Command { constructor(actions, args) { /** * command name * @type {string} */ this.name = actions.name; /** * arguments * @type {Array} */ this.args = args; /** * Execute function * @type {function} */ this.execute = actions.execute; /** * Undo function * @type {function} */ this.undo = actions.undo; /** * executeCallback * @type {function} */ this.executeCallback = actions.executeCallback || null; /** * undoCallback * @type {function} */ this.undoCallback = actions.undoCallback || null; /** * data for undo * @type {Object} */ this.undoData = {}; } /** * Execute action * @param {Object.} compMap - Components injection * @abstract */ execute() { throw new Error(createMessage(errorTypes.UN_IMPLEMENTATION, 'execute')); } /** * Undo action * @param {Object.} compMap - Components injection * @abstract */ undo() { throw new Error(createMessage(errorTypes.UN_IMPLEMENTATION, 'undo')); } /** * command for redo if undoData exists * @returns {boolean} isRedo */ get isRedo() { return Object.keys(this.undoData).length > 0; } /** * Set undoData action * @param {Object} undoData - maked undo data * @param {Object} cachedUndoDataForSilent - cached undo data * @param {boolean} isSilent - is silent execution or not * @returns {Object} cachedUndoDataForSilent */ setUndoData(undoData, cachedUndoDataForSilent, isSilent) { if (cachedUndoDataForSilent) { undoData = cachedUndoDataForSilent; } if (!isSilent) { extend(this.undoData, undoData); cachedUndoDataForSilent = null; } else if (!cachedUndoDataForSilent) { cachedUndoDataForSilent = undoData; } return cachedUndoDataForSilent; } /** * Attach execute callabck * @param {function} callback - Callback after execution * @returns {Command} this */ setExecuteCallback(callback) { this.executeCallback = callback; return this; } /** * Attach undo callback * @param {function} callback - Callback after undo * @returns {Command} this */ setUndoCallback(callback) { this.undoCallback = callback; return this; } } export default Command; ================================================ FILE: apps/image-editor/src/js/interface/component.js ================================================ /** * Component interface * @class * @param {string} name - component name * @param {Graphics} graphics - Graphics instance * @ignore */ class Component { constructor(name, graphics) { /** * Component name * @type {string} */ this.name = name; /** * Graphics instance * @type {Graphics} */ this.graphics = graphics; } /** * Fire Graphics event * @returns {Object} return value */ fire(...args) { const context = this.graphics; return this.graphics.fire.apply(context, args); } /** * Save image(background) of canvas * @param {string} name - Name of image * @param {fabric.Image} oImage - Fabric image instance */ setCanvasImage(name, oImage) { this.graphics.setCanvasImage(name, oImage); } /** * Returns canvas element of fabric.Canvas[[lower-canvas]] * @returns {HTMLCanvasElement} */ getCanvasElement() { return this.graphics.getCanvasElement(); } /** * Get fabric.Canvas instance * @returns {fabric.Canvas} */ getCanvas() { return this.graphics.getCanvas(); } /** * Get canvasImage (fabric.Image instance) * @returns {fabric.Image} */ getCanvasImage() { return this.graphics.getCanvasImage(); } /** * Get image name * @returns {string} */ getImageName() { return this.graphics.getImageName(); } /** * Get image editor * @returns {ImageEditor} */ getEditor() { return this.graphics.getEditor(); } /** * Return component name * @returns {string} */ getName() { return this.name; } /** * Set image properties * @param {Object} setting - Image properties * @param {boolean} [withRendering] - If true, The changed image will be reflected in the canvas */ setImageProperties(setting, withRendering) { this.graphics.setImageProperties(setting, withRendering); } /** * Set canvas dimension - css only * @param {Object} dimension - Canvas css dimension */ setCanvasCssDimension(dimension) { this.graphics.setCanvasCssDimension(dimension); } /** * Set canvas dimension - css only * @param {Object} dimension - Canvas backstore dimension */ setCanvasBackstoreDimension(dimension) { this.graphics.setCanvasBackstoreDimension(dimension); } /** * Adjust canvas dimension with scaling image */ adjustCanvasDimension() { this.graphics.adjustCanvasDimension(); } adjustCanvasDimensionBase() { this.graphics.adjustCanvasDimensionBase(); } } export default Component; ================================================ FILE: apps/image-editor/src/js/interface/drawingMode.js ================================================ import errorMessage from '@/factory/errorMessage'; const createMessage = errorMessage.create; const errorTypes = errorMessage.types; /** * DrawingMode interface * @class * @param {string} name - drawing mode name * @ignore */ class DrawingMode { constructor(name) { /** * the name of drawing mode * @type {string} */ this.name = name; } /** * Get this drawing mode name; * @returns {string} drawing mode name */ getName() { return this.name; } /** * start this drawing mode * @param {Object} options - drawing mode options * @abstract */ start() { throw new Error(createMessage(errorTypes.UN_IMPLEMENTATION, 'start')); } /** * stop this drawing mode * @abstract */ end() { throw new Error(createMessage(errorTypes.UN_IMPLEMENTATION, 'stop')); } } export default DrawingMode; ================================================ FILE: apps/image-editor/src/js/invoker.js ================================================ import isString from 'tui-code-snippet/type/isString'; import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; import commandFactory from '@/factory/command'; import { isFunction } from '@/util'; import { eventNames, rejectMessages } from '@/consts'; /** * Invoker * @class * @ignore */ class Invoker { constructor() { /** * Undo stack * @type {Array.} * @private */ this._undoStack = []; /** * Redo stack * @type {Array.} * @private */ this._redoStack = []; /** * Lock-flag for executing command * @type {boolean} * @private */ this._isLocked = false; this._isSilent = false; } /** * Invoke command execution * @param {Command} command - Command * @param {boolean} [isRedo=false] - check if command is redo * @returns {Promise} * @private */ _invokeExecution(command, isRedo = false) { this.lock(); let { args } = command; if (!args) { args = []; } return command .execute(...args) .then((value) => { if (!this._isSilent) { this.pushUndoStack(command); this.fire(isRedo ? eventNames.AFTER_REDO : eventNames.EXECUTE_COMMAND, command); } this.unlock(); if (isFunction(command.executeCallback)) { command.executeCallback(value); } return value; }) ['catch']((message) => { this.unlock(); return Promise.reject(message); }); } /** * Invoke command undo * @param {Command} command - Command * @returns {Promise} * @private */ _invokeUndo(command) { this.lock(); let { args } = command; if (!args) { args = []; } return command .undo(...args) .then((value) => { this.pushRedoStack(command); this.fire(eventNames.AFTER_UNDO, command); this.unlock(); if (isFunction(command.undoCallback)) { command.undoCallback(value); } return value; }) ['catch']((message) => { this.unlock(); return Promise.reject(message); }); } /** * fire REDO_STACK_CHANGED event * @private */ _fireRedoStackChanged() { this.fire(eventNames.REDO_STACK_CHANGED, this._redoStack.length); } /** * fire UNDO_STACK_CHANGED event * @private */ _fireUndoStackChanged() { this.fire(eventNames.UNDO_STACK_CHANGED, this._undoStack.length); } /** * Lock this invoker */ lock() { this._isLocked = true; } /** * Unlock this invoker */ unlock() { this._isLocked = false; } executeSilent(...args) { this._isSilent = true; return this.execute(...args, this._isSilent).then(() => { this._isSilent = false; }); } /** * Invoke command * Store the command to the undoStack * Clear the redoStack * @param {String} commandName - Command name * @param {...*} args - Arguments for creating command * @returns {Promise} */ execute(...args) { if (this._isLocked) { return Promise.reject(rejectMessages.isLock); } let [command] = args; if (isString(command)) { command = commandFactory.create(...args); } return this._invokeExecution(command).then((value) => { this.clearRedoStack(); return value; }); } /** * Undo command * @returns {Promise} */ undo() { let command = this._undoStack.pop(); let promise; let message = ''; if (command && this._isLocked) { this.pushUndoStack(command, true); command = null; } if (command) { if (this.isEmptyUndoStack()) { this._fireUndoStackChanged(); } promise = this._invokeUndo(command); } else { message = rejectMessages.undo; if (this._isLocked) { message = `${message} Because ${rejectMessages.isLock}`; } promise = Promise.reject(message); } return promise; } /** * Redo command * @returns {Promise} */ redo() { let command = this._redoStack.pop(); let promise; let message = ''; if (command && this._isLocked) { this.pushRedoStack(command, true); command = null; } if (command) { if (this.isEmptyRedoStack()) { this._fireRedoStackChanged(); } promise = this._invokeExecution(command, true); } else { message = rejectMessages.redo; if (this._isLocked) { message = `${message} Because ${rejectMessages.isLock}`; } promise = Promise.reject(message); } return promise; } /** * Push undo stack * @param {Command} command - command * @param {boolean} [isSilent] - Fire event or not */ pushUndoStack(command, isSilent) { this._undoStack.push(command); if (!isSilent) { this._fireUndoStackChanged(); } } /** * Push redo stack * @param {Command} command - command * @param {boolean} [isSilent] - Fire event or not */ pushRedoStack(command, isSilent) { this._redoStack.push(command); if (!isSilent) { this._fireRedoStackChanged(); } } /** * Return whether the redoStack is empty * @returns {boolean} */ isEmptyRedoStack() { return this._redoStack.length === 0; } /** * Return whether the undoStack is empty * @returns {boolean} */ isEmptyUndoStack() { return this._undoStack.length === 0; } /** * Clear undoStack */ clearUndoStack() { if (!this.isEmptyUndoStack()) { this._undoStack = []; this._fireUndoStackChanged(); } } /** * Clear redoStack */ clearRedoStack() { if (!this.isEmptyRedoStack()) { this._redoStack = []; this._fireRedoStackChanged(); } } } CustomEvents.mixin(Invoker); export default Invoker; ================================================ FILE: apps/image-editor/src/js/polyfill.js ================================================ /* eslint-disable */ // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } if (!Element.prototype.closest) { Element.prototype.closest = function (s) { var el = this; do { if (Element.prototype.matches.call(el, s)) return el; el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; }; } /* * classList.js: Cross-browser full element.classList implementation. * 1.2.20171210 * * By Eli Grey, http://eligrey.com * License: Dedicated to the public domain. * See https://github.com/eligrey/classList.js/blob/master/LICENSE.md */ /*global self, document, DOMException */ /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ if ('document' in self) { // Full polyfill for browsers with no classList support // Including IE < Edge missing SVGElement.classList if ( !('classList' in document.createElement('_')) || (document.createElementNS && !('classList' in document.createElementNS('http://www.w3.org/2000/svg', 'g'))) ) { (function (view) { 'use strict'; if (!('Element' in view)) return; var classListProp = 'classList', protoProp = 'prototype', elemCtrProto = view.Element[protoProp], objCtr = Object, strTrim = String[protoProp].trim || function () { return this.replace(/^\s+|\s+$/g, ''); }, arrIndexOf = Array[protoProp].indexOf || function (item) { var i = 0, len = this.length; for (; i < len; i++) { if (i in this && this[i] === item) { return i; } } return -1; }, // Vendors: please allow content code to instantiate DOMExceptions DOMEx = function (type, message) { this.name = type; this.code = DOMException[type]; this.message = message; }, checkTokenAndGetIndex = function (classList, token) { if (token === '') { throw new DOMEx('SYNTAX_ERR', 'The token must not be empty.'); } if (/\s/.test(token)) { throw new DOMEx( 'INVALID_CHARACTER_ERR', 'The token must not contain space characters.' ); } return arrIndexOf.call(classList, token); }, ClassList = function (elem) { var trimmedClasses = strTrim.call(elem.getAttribute('class') || ''), classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [], i = 0, len = classes.length; for (; i < len; i++) { this.push(classes[i]); } this._updateClassName = function () { elem.setAttribute('class', this.toString()); }; }, classListProto = (ClassList[protoProp] = []), classListGetter = function () { return new ClassList(this); }; // Most DOMException implementations don't allow calling DOMException's toString() // on non-DOMExceptions. Error's toString() is sufficient here. DOMEx[protoProp] = Error[protoProp]; classListProto.item = function (i) { return this[i] || null; }; classListProto.contains = function (token) { return ~checkTokenAndGetIndex(this, token + ''); }; classListProto.add = function () { var tokens = arguments, i = 0, l = tokens.length, token, updated = false; do { token = tokens[i] + ''; if (!~checkTokenAndGetIndex(this, token)) { this.push(token); updated = true; } } while (++i < l); if (updated) { this._updateClassName(); } }; classListProto.remove = function () { var tokens = arguments, i = 0, l = tokens.length, token, updated = false, index; do { token = tokens[i] + ''; index = checkTokenAndGetIndex(this, token); while (~index) { this.splice(index, 1); updated = true; index = checkTokenAndGetIndex(this, token); } } while (++i < l); if (updated) { this._updateClassName(); } }; classListProto.toggle = function (token, force) { var result = this.contains(token), method = result ? force !== true && 'remove' : force !== false && 'add'; if (method) { this[method](token); } if (force === true || force === false) { return force; } else { return !result; } }; classListProto.replace = function (token, replacement_token) { var index = checkTokenAndGetIndex(token + ''); if (~index) { this.splice(index, 1, replacement_token); this._updateClassName(); } }; classListProto.toString = function () { return this.join(' '); }; if (objCtr.defineProperty) { var classListPropDesc = { get: classListGetter, enumerable: true, configurable: true, }; try { objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } catch (ex) { // IE 8 doesn't support enumerable:true // adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36 // modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected if (ex.number === undefined || ex.number === -0x7ff5ec54) { classListPropDesc.enumerable = false; objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } } } else if (objCtr[protoProp].__defineGetter__) { elemCtrProto.__defineGetter__(classListProp, classListGetter); } })(self); } // There is full or partial native classList support, so just check if we need // to normalize the add/remove and toggle APIs. (function () { 'use strict'; var testElement = document.createElement('_'); testElement.classList.add('c1', 'c2'); // Polyfill for IE 10/11 and Firefox <26, where classList.add and // classList.remove exist but support only one argument at a time. if (!testElement.classList.contains('c2')) { var createMethod = function (method) { var original = DOMTokenList.prototype[method]; DOMTokenList.prototype[method] = function (token) { var i, len = arguments.length; for (i = 0; i < len; i++) { token = arguments[i]; original.call(this, token); } }; }; createMethod('add'); createMethod('remove'); } testElement.classList.toggle('c3', false); // Polyfill for IE 10 and Firefox <24, where classList.toggle does not // support the second argument. if (testElement.classList.contains('c3')) { var _toggle = DOMTokenList.prototype.toggle; DOMTokenList.prototype.toggle = function (token, force) { if (1 in arguments && !this.contains(token) === !force) { return force; } else { return _toggle.call(this, token); } }; } // replace() polyfill if (!('replace' in document.createElement('_').classList)) { DOMTokenList.prototype.replace = function (token, replacement_token) { var tokens = this.toString().split(' '), index = tokens.indexOf(token + ''); if (~index) { tokens = tokens.slice(index); this.remove.apply(this, tokens); this.add(replacement_token); this.add.apply(this, tokens.slice(1)); } }; } testElement = null; })(); } /*! * @copyright Copyright (c) 2017 IcoMoon.io * @license Licensed under MIT license * See https://github.com/Keyamoon/svgxuse * @version 1.2.6 */ /*jslint browser: true */ /*global XDomainRequest, MutationObserver, window */ (function () { 'use strict'; if (typeof window !== 'undefined' && window.addEventListener) { var cache = Object.create(null); // holds xhr objects to prevent multiple requests var checkUseElems; var tid; // timeout id var debouncedCheck = function () { clearTimeout(tid); tid = setTimeout(checkUseElems, 100); }; var unobserveChanges = function () { return; }; var observeChanges = function () { var observer; window.addEventListener('resize', debouncedCheck, false); window.addEventListener('orientationchange', debouncedCheck, false); if (window.MutationObserver) { observer = new MutationObserver(debouncedCheck); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, }); unobserveChanges = function () { try { observer.disconnect(); window.removeEventListener('resize', debouncedCheck, false); window.removeEventListener('orientationchange', debouncedCheck, false); } catch (ignore) {} }; } else { document.documentElement.addEventListener('DOMSubtreeModified', debouncedCheck, false); unobserveChanges = function () { document.documentElement.removeEventListener('DOMSubtreeModified', debouncedCheck, false); window.removeEventListener('resize', debouncedCheck, false); window.removeEventListener('orientationchange', debouncedCheck, false); }; } }; var createRequest = function (url) { // In IE 9, cross origin requests can only be sent using XDomainRequest. // XDomainRequest would fail if CORS headers are not set. // Therefore, XDomainRequest should only be used with cross origin requests. function getOrigin(loc) { var a; if (loc.protocol !== undefined) { a = loc; } else { a = document.createElement('a'); a.href = loc; } return a.protocol.replace(/:/g, '') + a.host; } var Request; var origin; var origin2; if (window.XMLHttpRequest) { Request = new XMLHttpRequest(); origin = getOrigin(location); origin2 = getOrigin(url); if (Request.withCredentials === undefined && origin2 !== '' && origin2 !== origin) { Request = XDomainRequest || undefined; } else { Request = XMLHttpRequest; } } return Request; }; var xlinkNS = 'http://www.w3.org/1999/xlink'; checkUseElems = function () { var base; var bcr; var fallback = ''; // optional fallback URL in case no base path to SVG file was given and no symbol definition was found. var hash; var href; var i; var inProgressCount = 0; var isHidden; var Request; var url; var uses; var xhr; function observeIfDone() { // If done with making changes, start watching for chagnes in DOM again inProgressCount -= 1; if (inProgressCount === 0) { // if all xhrs were resolved unobserveChanges(); // make sure to remove old handlers observeChanges(); // watch for changes to DOM } } function attrUpdateFunc(spec) { return function () { if (cache[spec.base] !== true) { spec.useEl.setAttributeNS(xlinkNS, 'xlink:href', '#' + spec.hash); if (spec.useEl.hasAttribute('href')) { spec.useEl.setAttribute('href', '#' + spec.hash); } } }; } function onloadFunc(xhr) { return function () { var body = document.body; var x = document.createElement('x'); var svg; xhr.onload = null; x.innerHTML = xhr.responseText; svg = x.getElementsByTagName('svg')[0]; if (svg) { svg.setAttribute('aria-hidden', 'true'); svg.style.position = 'absolute'; svg.style.width = 0; svg.style.height = 0; svg.style.overflow = 'hidden'; body.insertBefore(svg, body.firstChild); } observeIfDone(); }; } function onErrorTimeout(xhr) { return function () { xhr.onerror = null; xhr.ontimeout = null; observeIfDone(); }; } unobserveChanges(); // stop watching for changes to DOM // find all use elements uses = document.getElementsByTagName('use'); for (i = 0; i < uses.length; i += 1) { try { bcr = uses[i].getBoundingClientRect(); } catch (ignore) { // failed to get bounding rectangle of the use element bcr = false; } href = uses[i].getAttribute('href') || uses[i].getAttributeNS(xlinkNS, 'href') || uses[i].getAttribute('xlink:href'); if (href && href.split) { url = href.split('#'); } else { url = ['', '']; } base = url[0]; hash = url[1]; isHidden = bcr && bcr.left === 0 && bcr.right === 0 && bcr.top === 0 && bcr.bottom === 0; if (bcr && bcr.width === 0 && bcr.height === 0 && !isHidden) { // the use element is empty // if there is a reference to an external SVG, try to fetch it // use the optional fallback URL if there is no reference to an external SVG if (fallback && !base.length && hash && !document.getElementById(hash)) { base = fallback; } if (uses[i].hasAttribute('href')) { uses[i].setAttributeNS(xlinkNS, 'xlink:href', href); } if (base.length) { // schedule updating xlink:href xhr = cache[base]; if (xhr !== true) { // true signifies that prepending the SVG was not required setTimeout( attrUpdateFunc({ useEl: uses[i], base: base, hash: hash, }), 0 ); } if (xhr === undefined) { Request = createRequest(base); if (Request !== undefined) { xhr = new Request(); cache[base] = xhr; xhr.onload = onloadFunc(xhr); xhr.onerror = onErrorTimeout(xhr); xhr.ontimeout = onErrorTimeout(xhr); xhr.open('GET', base); xhr.send(); inProgressCount += 1; } } } } else { if (!isHidden) { if (cache[base] === undefined) { // remember this URL if the use element was not empty and no request was sent cache[base] = true; } else if (cache[base].onload) { // if it turns out that prepending the SVG is not necessary, // abort the in-progress xhr. cache[base].abort(); delete cache[base].onload; cache[base] = true; } } else if (base.length && cache[base]) { setTimeout( attrUpdateFunc({ useEl: uses[i], base: base, hash: hash, }), 0 ); } } } uses = ''; inProgressCount += 1; observeIfDone(); }; var winLoad; winLoad = function () { window.removeEventListener('load', winLoad, false); // to prevent memory leaks tid = setTimeout(checkUseElems, 0); }; if (document.readyState !== 'complete') { // The load event fires when all resources have finished loading, which allows detecting whether SVG use elements are empty. window.addEventListener('load', winLoad, false); } else { // No need to add a listener if the document is already loaded, initialize immediately. winLoad(); } } })(); ================================================ FILE: apps/image-editor/src/js/ui/crop.js ================================================ import forEach from 'tui-code-snippet/collection/forEach'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/crop'; import { assignmentForDestroy } from '@/util'; /** * Crop ui class * @class * @ignore */ class Crop extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'crop', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this.status = 'active'; this._els = { apply: this.selector('.tie-crop-button .apply'), cancel: this.selector('.tie-crop-button .cancel'), preset: this.selector('.tie-crop-preset-button'), }; this.defaultPresetButton = this._els.preset.querySelector('.preset-none'); } /** * Destroys the instance. */ destroy() { this._removeEvent(); assignmentForDestroy(this); } /** * Add event for crop * @param {Object} actions - actions for crop * @param {Function} actions.crop - crop action * @param {Function} actions.cancel - cancel action * @param {Function} actions.preset - draw rectzone at a predefined ratio */ addEvent(actions) { const apply = this._applyEventHandler.bind(this); const cancel = this._cancelEventHandler.bind(this); const cropzonePreset = this._cropzonePresetEventHandler.bind(this); this.eventHandler = { apply, cancel, cropzonePreset, }; this.actions = actions; this._els.apply.addEventListener('click', apply); this._els.cancel.addEventListener('click', cancel); this._els.preset.addEventListener('click', cropzonePreset); } /** * Remove event * @private */ _removeEvent() { this._els.apply.removeEventListener('click', this.eventHandler.apply); this._els.cancel.removeEventListener('click', this.eventHandler.cancel); this._els.preset.removeEventListener('click', this.eventHandler.cropzonePreset); } _applyEventHandler() { this.actions.crop(); this._els.apply.classList.remove('active'); } _cancelEventHandler() { this.actions.cancel(); this._els.apply.classList.remove('active'); } _cropzonePresetEventHandler(event) { const button = event.target.closest('.tui-image-editor-button.preset'); if (button) { const [presetType] = button.className.match(/preset-[^\s]+/); this._setPresetButtonActive(button); this.actions.preset(presetType); } } /** * Executed when the menu starts. */ changeStartMode() { this.actions.modeChange('crop'); } /** * Returns the menu to its default state. */ changeStandbyMode() { this.actions.stopDrawingMode(); this._setPresetButtonActive(); } /** * Change apply button status * @param {Boolean} enableStatus - apply button status */ changeApplyButtonStatus(enableStatus) { if (enableStatus) { this._els.apply.classList.add('active'); } else { this._els.apply.classList.remove('active'); } } /** * Set preset button to active status * @param {HTMLElement} button - event target element * @private */ _setPresetButtonActive(button = this.defaultPresetButton) { forEach(this._els.preset.querySelectorAll('.preset'), (presetButton) => { presetButton.classList.remove('active'); }); if (button) { button.classList.add('active'); } } } export default Crop; ================================================ FILE: apps/image-editor/src/js/ui/draw.js ================================================ import Colorpicker from '@/ui/tools/colorpicker'; import Range from '@/ui/tools/range'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/draw'; import { assignmentForDestroy, getRgb } from '@/util'; import { defaultDrawRangeValues, eventNames, selectorNames } from '@/consts'; const DRAW_OPACITY = 0.7; /** * Draw ui class * @class * @ignore */ class Draw extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'draw', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this._els = { lineSelectButton: this.selector('.tie-draw-line-select-button'), drawColorPicker: new Colorpicker(this.selector('.tie-draw-color'), { defaultColor: '#00a9ff', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), drawRange: new Range( { slider: this.selector('.tie-draw-range'), input: this.selector('.tie-draw-range-value'), }, defaultDrawRangeValues ), }; this.type = null; this.color = this._els.drawColorPicker.color; this.width = this._els.drawRange.value; this.colorPickerInputBox = this._els.drawColorPicker.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ); } /** * Destroys the instance. */ destroy() { this._removeEvent(); this._els.drawColorPicker.destroy(); this._els.drawRange.destroy(); assignmentForDestroy(this); } /** * Add event for draw * @param {Object} actions - actions for crop * @param {Function} actions.setDrawMode - set draw mode */ addEvent(actions) { this.eventHandler.changeDrawType = this._changeDrawType.bind(this); this.actions = actions; this._els.lineSelectButton.addEventListener('click', this.eventHandler.changeDrawType); this._els.drawColorPicker.on('change', this._changeDrawColor.bind(this)); this._els.drawRange.on('change', this._changeDrawRange.bind(this)); this.colorPickerInputBox.addEventListener( eventNames.FOCUS, this._onStartEditingInputBox.bind(this) ); this.colorPickerInputBox.addEventListener( eventNames.BLUR, this._onStopEditingInputBox.bind(this) ); } /** * Remove event * @private */ _removeEvent() { this._els.lineSelectButton.removeEventListener('click', this.eventHandler.changeDrawType); this._els.drawColorPicker.off(); this._els.drawRange.off(); this.colorPickerInputBox.removeEventListener( eventNames.FOCUS, this._onStartEditingInputBox.bind(this) ); this.colorPickerInputBox.removeEventListener( eventNames.BLUR, this._onStopEditingInputBox.bind(this) ); } /** * set draw mode - action runner */ setDrawMode() { this.actions.setDrawMode(this.type, { width: this.width, color: getRgb(this.color, DRAW_OPACITY), }); } /** * Returns the menu to its default state. */ changeStandbyMode() { this.type = null; this.actions.stopDrawingMode(); this.actions.changeSelectableAll(true); this._els.lineSelectButton.classList.remove('free'); this._els.lineSelectButton.classList.remove('line'); } /** * Executed when the menu starts. */ changeStartMode() { this.type = 'free'; this._els.lineSelectButton.classList.add('free'); this.setDrawMode(); } /** * Change draw type event * @param {object} event - line select event * @private */ _changeDrawType(event) { const button = event.target.closest('.tui-image-editor-button'); if (button) { const lineType = this.getButtonType(button, ['free', 'line']); this.actions.discardSelection(); if (this.type === lineType) { this.changeStandbyMode(); return; } this.changeStandbyMode(); this.type = lineType; this._els.lineSelectButton.classList.add(lineType); this.setDrawMode(); } } /** * Change drawing color * @param {string} color - select drawing color * @private */ _changeDrawColor(color) { this.color = color || 'transparent'; if (!this.type) { this.changeStartMode(); } else { this.setDrawMode(); } } /** * Change drawing Range * @param {number} value - select drawing range * @private */ _changeDrawRange(value) { this.width = value; if (!this.type) { this.changeStartMode(); } else { this.setDrawMode(); } } } export default Draw; ================================================ FILE: apps/image-editor/src/js/ui/filter.js ================================================ import forEach from 'tui-code-snippet/collection/forEach'; import forEachArray from 'tui-code-snippet/collection/forEachArray'; import isExisty from 'tui-code-snippet/type/isExisty'; import Colorpicker from '@/ui/tools/colorpicker'; import Range from '@/ui/tools/range'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/filter'; import { toInteger, toCamelCase, assignmentForDestroy } from '@/util'; import { defaultFilterRangeValues as FILTER_RANGE, eventNames, selectorNames } from '@/consts'; const PICKER_CONTROL_HEIGHT = '130px'; const BLEND_OPTIONS = ['add', 'diff', 'subtract', 'multiply', 'screen', 'lighten', 'darken']; const FILTER_OPTIONS = [ 'grayscale', 'invert', 'sepia', 'vintage', 'blur', 'sharpen', 'emboss', 'remove-white', 'brightness', 'noise', 'pixelate', 'color-filter', 'tint', 'multiply', 'blend', ]; const filterNameMap = { grayscale: 'grayscale', invert: 'invert', sepia: 'sepia', blur: 'blur', sharpen: 'sharpen', emboss: 'emboss', removeWhite: 'removeColor', brightness: 'brightness', contrast: 'contrast', saturation: 'saturation', vintage: 'vintage', polaroid: 'polaroid', noise: 'noise', pixelate: 'pixelate', colorFilter: 'removeColor', tint: 'blendColor', multiply: 'blendColor', blend: 'blendColor', hue: 'hue', gamma: 'gamma', }; const RANGE_INSTANCE_NAMES = [ 'removewhiteDistanceRange', 'colorfilterThresholdRange', 'pixelateRange', 'noiseRange', 'brightnessRange', 'tintOpacity', ]; const COLORPICKER_INSTANCE_NAMES = ['filterBlendColor', 'filterMultiplyColor', 'filterTintColor']; /** * Filter ui class * @class * @ignore */ class Filter extends Submenu { constructor(subMenuElement, { locale, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'filter', menuBarPosition, templateHtml, usageStatistics, }); this.selectBoxShow = false; this.checkedMap = {}; this._makeControlElement(); } /** * Destroys the instance. */ destroy() { this._removeEvent(); this._destroyToolInstance(); assignmentForDestroy(this); } /** * Remove event for filter */ _removeEvent() { forEach(FILTER_OPTIONS, (filter) => { const filterCheckElement = this.selector(`.tie-${filter}`); const filterNameCamelCase = toCamelCase(filter); filterCheckElement.removeEventListener('change', this.eventHandler[filterNameCamelCase]); }); forEach([...RANGE_INSTANCE_NAMES, ...COLORPICKER_INSTANCE_NAMES], (instanceName) => { this._els[instanceName].off(); }); this._els.blendType.removeEventListener('change', this.eventHandler.changeBlendFilter); this._els.blendType.removeEventListener('click', this.eventHandler.changeBlendFilter); forEachArray( this.colorPickerInputBoxes, (inputBox) => { inputBox.removeEventListener(eventNames.FOCUS, this._onStartEditingInputBox.bind(this)); inputBox.removeEventListener(eventNames.BLUR, this._onStopEditingInputBox.bind(this)); }, this ); } _destroyToolInstance() { forEach([...RANGE_INSTANCE_NAMES, ...COLORPICKER_INSTANCE_NAMES], (instanceName) => { this._els[instanceName].destroy(); }); } /** * Add event for filter * @param {Object} actions - actions for crop * @param {Function} actions.applyFilter - apply filter option */ addEvent({ applyFilter }) { const changeFilterState = (filterName) => this._changeFilterState.bind(this, applyFilter, filterName); const changeFilterStateForRange = (filterName) => (value, isLast) => this._changeFilterState(applyFilter, filterName, isLast); this.eventHandler = { changeBlendFilter: changeFilterState('blend'), blandTypeClick: (event) => event.stopPropagation(), }; forEach(FILTER_OPTIONS, (filter) => { const filterCheckElement = this.selector(`.tie-${filter}`); const filterNameCamelCase = toCamelCase(filter); this.checkedMap[filterNameCamelCase] = filterCheckElement; this.eventHandler[filterNameCamelCase] = changeFilterState(filterNameCamelCase); filterCheckElement.addEventListener('change', this.eventHandler[filterNameCamelCase]); }); this._els.removewhiteDistanceRange.on('change', changeFilterStateForRange('removeWhite')); this._els.colorfilterThresholdRange.on('change', changeFilterStateForRange('colorFilter')); this._els.pixelateRange.on('change', changeFilterStateForRange('pixelate')); this._els.noiseRange.on('change', changeFilterStateForRange('noise')); this._els.brightnessRange.on('change', changeFilterStateForRange('brightness')); this._els.filterBlendColor.on('change', this.eventHandler.changeBlendFilter); this._els.filterMultiplyColor.on('change', changeFilterState('multiply')); this._els.filterTintColor.on('change', changeFilterState('tint')); this._els.tintOpacity.on('change', changeFilterStateForRange('tint')); this._els.filterMultiplyColor.on('changeShow', this.colorPickerChangeShow.bind(this)); this._els.filterTintColor.on('changeShow', this.colorPickerChangeShow.bind(this)); this._els.filterBlendColor.on('changeShow', this.colorPickerChangeShow.bind(this)); this._els.blendType.addEventListener('change', this.eventHandler.changeBlendFilter); this._els.blendType.addEventListener('click', this.eventHandler.blandTypeClick); forEachArray( this.colorPickerInputBoxes, (inputBox) => { inputBox.addEventListener(eventNames.FOCUS, this._onStartEditingInputBox.bind(this)); inputBox.addEventListener(eventNames.BLUR, this._onStopEditingInputBox.bind(this)); }, this ); } /** * Set filter for undo changed * @param {Object} changedFilterInfos - changed command infos * @param {string} type - filter type * @param {string} action - add or remove * @param {Object} options - filter options */ setFilterState(changedFilterInfos) { const { type, options, action } = changedFilterInfos; const filterName = this._getFilterNameFromOptions(type, options); const isRemove = action === 'remove'; if (!isRemove) { this._setFilterState(filterName, options); } this.checkedMap[filterName].checked = !isRemove; } /** * Init all filter's checkbox to unchecked state */ initFilterCheckBoxState() { forEach( this.checkedMap, (filter) => { filter.checked = false; }, this ); } /** * Set filter for undo changed * @param {string} filterName - filter name * @param {Object} options - filter options * @private */ // eslint-disable-next-line complexity _setFilterState(filterName, options) { if (filterName === 'colorFilter') { this._els.colorfilterThresholdRange.value = options.distance; } else if (filterName === 'removeWhite') { this._els.removewhiteDistanceRange.value = options.distance; } else if (filterName === 'pixelate') { this._els.pixelateRange.value = options.blocksize; } else if (filterName === 'brightness') { this._els.brightnessRange.value = options.brightness; } else if (filterName === 'noise') { this._els.noiseRange.value = options.noise; } else if (filterName === 'tint') { this._els.tintOpacity.value = options.alpha; this._els.filterTintColor.color = options.color; } else if (filterName === 'blend') { this._els.filterBlendColor.color = options.color; } else if (filterName === 'multiply') { this._els.filterMultiplyColor.color = options.color; } } /** * Get filter name * @param {string} type - filter type * @param {Object} options - filter options * @returns {string} filter name * @private */ _getFilterNameFromOptions(type, options) { let filterName = type; if (type === 'removeColor') { filterName = isExisty(options.useAlpha) ? 'removeWhite' : 'colorFilter'; } else if (type === 'blendColor') { filterName = { add: 'blend', multiply: 'multiply', tint: 'tint', }[options.mode]; } return filterName; } /** * Add event for filter * @param {Function} applyFilter - actions for firter * @param {string} filterName - filter name * @param {boolean} [isLast] - Is last change */ _changeFilterState(applyFilter, filterName, isLast = true) { const apply = this.checkedMap[filterName].checked; const type = filterNameMap[filterName]; const checkboxGroup = this.checkedMap[filterName].closest('.tui-image-editor-checkbox-group'); if (checkboxGroup) { if (apply) { checkboxGroup.classList.remove('tui-image-editor-disabled'); } else { checkboxGroup.classList.add('tui-image-editor-disabled'); } } applyFilter(apply, type, this._getFilterOption(filterName), !isLast); } /** * Get filter option * @param {String} type - filter type * @returns {Object} filter option object * @private */ // eslint-disable-next-line complexity _getFilterOption(type) { const option = {}; switch (type) { case 'removeWhite': option.color = '#FFFFFF'; option.useAlpha = false; option.distance = parseFloat(this._els.removewhiteDistanceRange.value); break; case 'colorFilter': option.color = '#FFFFFF'; option.distance = parseFloat(this._els.colorfilterThresholdRange.value); break; case 'pixelate': option.blocksize = toInteger(this._els.pixelateRange.value); break; case 'noise': option.noise = toInteger(this._els.noiseRange.value); break; case 'brightness': option.brightness = parseFloat(this._els.brightnessRange.value); break; case 'blend': option.mode = 'add'; option.color = this._els.filterBlendColor.color; option.mode = this._els.blendType.value; break; case 'multiply': option.mode = 'multiply'; option.color = this._els.filterMultiplyColor.color; break; case 'tint': option.mode = 'tint'; option.color = this._els.filterTintColor.color; option.alpha = this._els.tintOpacity.value; break; case 'blur': option.blur = this._els.blurRange.value; break; default: break; } return option; } /** * Make submenu range and colorpicker control * @private */ _makeControlElement() { this._els = { removewhiteDistanceRange: new Range( { slider: this.selector('.tie-removewhite-distance-range') }, FILTER_RANGE.removewhiteDistanceRange ), brightnessRange: new Range( { slider: this.selector('.tie-brightness-range') }, FILTER_RANGE.brightnessRange ), noiseRange: new Range({ slider: this.selector('.tie-noise-range') }, FILTER_RANGE.noiseRange), pixelateRange: new Range( { slider: this.selector('.tie-pixelate-range') }, FILTER_RANGE.pixelateRange ), colorfilterThresholdRange: new Range( { slider: this.selector('.tie-colorfilter-threshold-range') }, FILTER_RANGE.colorfilterThresholdRange ), filterTintColor: new Colorpicker(this.selector('.tie-filter-tint-color'), { defaultColor: '#03bd9e', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), filterMultiplyColor: new Colorpicker(this.selector('.tie-filter-multiply-color'), { defaultColor: '#515ce6', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), filterBlendColor: new Colorpicker(this.selector('.tie-filter-blend-color'), { defaultColor: '#ffbb3b', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), blurRange: FILTER_RANGE.blurFilterRange, }; this._els.tintOpacity = this._pickerWithRange(this._els.filterTintColor.pickerControl); this._els.blendType = this._pickerWithSelectbox(this._els.filterBlendColor.pickerControl); this.colorPickerControls.push(this._els.filterTintColor); this.colorPickerControls.push(this._els.filterMultiplyColor); this.colorPickerControls.push(this._els.filterBlendColor); this.colorPickerInputBoxes = []; this.colorPickerInputBoxes.push( this._els.filterTintColor.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ) ); this.colorPickerInputBoxes.push( this._els.filterMultiplyColor.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ) ); this.colorPickerInputBoxes.push( this._els.filterBlendColor.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ) ); } /** * Make submenu control for picker & range mixin * @param {HTMLElement} pickerControl - pickerControl dom element * @returns {Range} * @private */ _pickerWithRange(pickerControl) { const rangeWrap = document.createElement('div'); const rangeLabel = document.createElement('label'); const slider = document.createElement('div'); slider.id = 'tie-filter-tint-opacity'; rangeLabel.innerHTML = 'Opacity'; rangeWrap.appendChild(rangeLabel); rangeWrap.appendChild(slider); pickerControl.appendChild(rangeWrap); pickerControl.style.height = PICKER_CONTROL_HEIGHT; return new Range({ slider }, FILTER_RANGE.tintOpacityRange); } /** * Make submenu control for picker & selectbox * @param {HTMLElement} pickerControl - pickerControl dom element * @returns {HTMLElement} * @private */ _pickerWithSelectbox(pickerControl) { const selectlistWrap = document.createElement('div'); const selectlist = document.createElement('select'); const optionlist = document.createElement('ul'); selectlistWrap.className = 'tui-image-editor-selectlist-wrap'; optionlist.className = 'tui-image-editor-selectlist'; selectlistWrap.appendChild(selectlist); selectlistWrap.appendChild(optionlist); this._makeSelectOptionList(selectlist); pickerControl.appendChild(selectlistWrap); pickerControl.style.height = PICKER_CONTROL_HEIGHT; this._drawSelectOptionList(selectlist, optionlist); this._pickerWithSelectboxForAddEvent(selectlist, optionlist); return selectlist; } /** * Make selectbox option list custom style * @param {HTMLElement} selectlist - selectbox element * @param {HTMLElement} optionlist - custom option list item element * @private */ _drawSelectOptionList(selectlist, optionlist) { const options = selectlist.querySelectorAll('option'); forEach(options, (option) => { const optionElement = document.createElement('li'); optionElement.innerHTML = option.innerHTML; optionElement.setAttribute('data-item', option.value); optionlist.appendChild(optionElement); }); } /** * custom selectbox custom event * @param {HTMLElement} selectlist - selectbox element * @param {HTMLElement} optionlist - custom option list item element * @private */ _pickerWithSelectboxForAddEvent(selectlist, optionlist) { optionlist.addEventListener('click', (event) => { const optionValue = event.target.getAttribute('data-item'); const fireEvent = document.createEvent('HTMLEvents'); selectlist.querySelector(`[value="${optionValue}"]`).selected = true; fireEvent.initEvent('change', true, true); selectlist.dispatchEvent(fireEvent); this.selectBoxShow = false; optionlist.style.display = 'none'; }); selectlist.addEventListener('mousedown', (event) => { event.preventDefault(); this.selectBoxShow = !this.selectBoxShow; optionlist.style.display = this.selectBoxShow ? 'block' : 'none'; optionlist.setAttribute('data-selectitem', selectlist.value); optionlist.querySelector(`[data-item='${selectlist.value}']`).classList.add('active'); }); } /** * Make option list for select control * @param {HTMLElement} selectlist - blend option select list element * @private */ _makeSelectOptionList(selectlist) { forEach(BLEND_OPTIONS, (option) => { const selectOption = document.createElement('option'); selectOption.setAttribute('value', option); selectOption.innerHTML = option.replace(/^[a-z]/, ($0) => $0.toUpperCase()); selectlist.appendChild(selectOption); }); } } export default Filter; ================================================ FILE: apps/image-editor/src/js/ui/flip.js ================================================ import forEach from 'tui-code-snippet/collection/forEach'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/flip'; import { assignmentForDestroy } from '@/util'; /** * Flip ui class * @class * @ignore */ class Flip extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'flip', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this.flipStatus = false; this._els = { flipButton: this.selector('.tie-flip-button'), }; } /** * Destroys the instance. */ destroy() { this._removeEvent(); assignmentForDestroy(this); } /** * Add event for flip * @param {Object} actions - actions for flip * @param {Function} actions.flip - flip action */ addEvent(actions) { this.eventHandler.changeFlip = this._changeFlip.bind(this); this._actions = actions; this._els.flipButton.addEventListener('click', this.eventHandler.changeFlip); } /** * Remove event * @private */ _removeEvent() { this._els.flipButton.removeEventListener('click', this.eventHandler.changeFlip); } /** * change Flip status * @param {object} event - change event * @private */ _changeFlip(event) { const button = event.target.closest('.tui-image-editor-button'); if (button) { const flipType = this.getButtonType(button, ['flipX', 'flipY', 'resetFlip']); if (!this.flipStatus && flipType === 'resetFlip') { return; } this._actions.flip(flipType).then((flipStatus) => { const flipClassList = this._els.flipButton.classList; this.flipStatus = false; flipClassList.remove('resetFlip'); forEach(['flipX', 'flipY'], (type) => { flipClassList.remove(type); if (flipStatus[type]) { flipClassList.add(type); flipClassList.add('resetFlip'); this.flipStatus = true; } }); }); } } } export default Flip; ================================================ FILE: apps/image-editor/src/js/ui/history.js ================================================ import Panel from '@/ui/panelMenu'; import templateHtml from '@/ui/template/submenu/history'; import { assignmentForDestroy } from '@/util'; const historyClassName = 'history-item'; const selectedClassName = 'selected-item'; const disabledClassName = 'disabled-item'; /** * History ui class * @class * @ignore */ class History extends Panel { constructor(menuElement, { locale, makeSvgIcon }) { super(menuElement, { name: 'history' }); menuElement.classList.add('enabled'); this.locale = locale; this.makeSvgIcon = makeSvgIcon; this._eventHandler = {}; this._historyIndex = this.getListLength(); } /** * Add history * @param {string} name - name of history * @param {?string} detail - detail information of history */ add({ name, detail }) { if (this._hasDisabledItem()) { this.deleteListItemElement(this._historyIndex + 1, this.getListLength()); } const html = templateHtml({ locale: this.locale, makeSvgIcon: this.makeSvgIcon, name, detail }); const item = this.makeListItemElement(html); this.pushListItemElement(item); this._historyIndex = this.getListLength() - 1; this._selectItem(this._historyIndex); } /** * Init history */ init() { this.deleteListItemElement(1, this.getListLength()); this._historyIndex = 0; this._selectItem(this._historyIndex); } /** * Clear history */ clear() { this.deleteListItemElement(0, this.getListLength()); this._historyIndex = -1; } /** * Select previous history of current selected history */ prev() { this._historyIndex -= 1; this._selectItem(this._historyIndex); } /** * Select next history of current selected history */ next() { this._historyIndex += 1; this._selectItem(this._historyIndex); } /** * Whether history menu has disabled item * @returns {boolean} */ _hasDisabledItem() { return this.getListLength() - 1 > this._historyIndex; } /** * Add history menu event * @private */ _addHistoryEventListener() { this._eventHandler.history = (event) => this._clickHistoryItem(event); this.listElement.addEventListener('click', this._eventHandler.history); } /** * Remove history menu event * @private */ _removeHistoryEventListener() { this.listElement.removeEventListener('click', this._eventHandler.history); } /** * onClick history menu event listener * @param {object} event - event object * @private */ _clickHistoryItem(event) { const { target } = event; const item = target.closest(`.${historyClassName}`); if (!item) { return; } const index = Number.parseInt(item.getAttribute('data-index'), 10); if (index !== this._historyIndex) { const count = Math.abs(index - this._historyIndex); if (index < this._historyIndex) { this._actions.undo(count); } else { this._actions.redo(count); } } } /** * Change item's state to selected state * @param {number} index - index of selected item */ _selectItem(index) { for (let i = 0; i < this.getListLength(); i += 1) { this.removeClass(i, selectedClassName); this.removeClass(i, disabledClassName); if (i > index) { this.addClass(i, disabledClassName); } } this.addClass(index, selectedClassName); } /** * Destroys the instance. */ destroy() { this.removeEvent(); assignmentForDestroy(this); } /** * Add event for history * @param {Object} actions - actions for crop * @param {Function} actions.undo - undo action * @param {Function} actions.redo - redo action */ addEvent(actions) { this._actions = actions; this._addHistoryEventListener(); } /** * Remove event * @private */ removeEvent() { this._removeHistoryEventListener(); } } export default History; ================================================ FILE: apps/image-editor/src/js/ui/icon.js ================================================ import forEach from 'tui-code-snippet/collection/forEach'; import Colorpicker from '@/ui/tools/colorpicker'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/icon'; import { isSupportFileApi, assignmentForDestroy } from '@/util'; import { defaultIconPath, eventNames, selectorNames } from '@/consts'; /** * Icon ui class * @class * @ignore */ class Icon extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'icon', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this.iconType = null; this._iconMap = {}; this._els = { registerIconButton: this.selector('.tie-icon-image-file'), addIconButton: this.selector('.tie-icon-add-button'), iconColorpicker: new Colorpicker(this.selector('.tie-icon-color'), { defaultColor: '#ffbb3b', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), }; this.colorPickerInputBox = this._els.iconColorpicker.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ); } /** * Destroys the instance. */ destroy() { this._removeEvent(); this._els.iconColorpicker.destroy(); assignmentForDestroy(this); } /** * Add event for icon * @param {Object} actions - actions for icon * @param {Function} actions.registerCustomIcon - register icon * @param {Function} actions.addIcon - add icon * @param {Function} actions.changeColor - change icon color */ addEvent(actions) { const registerIcon = this._registerIconHandler.bind(this); const addIcon = this._addIconHandler.bind(this); this.eventHandler = { registerIcon, addIcon, }; this.actions = actions; this._els.iconColorpicker.on('change', this._changeColorHandler.bind(this)); this._els.registerIconButton.addEventListener('change', registerIcon); this._els.addIconButton.addEventListener('click', addIcon); this.colorPickerInputBox.addEventListener( eventNames.FOCUS, this._onStartEditingInputBox.bind(this) ); this.colorPickerInputBox.addEventListener( eventNames.BLUR, this._onStopEditingInputBox.bind(this) ); } /** * Remove event * @private */ _removeEvent() { this._els.iconColorpicker.off(); this._els.registerIconButton.removeEventListener('change', this.eventHandler.registerIcon); this._els.addIconButton.removeEventListener('click', this.eventHandler.addIcon); this.colorPickerInputBox.removeEventListener( eventNames.FOCUS, this._onStartEditingInputBox.bind(this) ); this.colorPickerInputBox.removeEventListener( eventNames.BLUR, this._onStopEditingInputBox.bind(this) ); } /** * Clear icon type */ clearIconType() { this._els.addIconButton.classList.remove(this.iconType); this.iconType = null; } /** * Register default icon */ registerDefaultIcon() { forEach(defaultIconPath, (path, type) => { this.actions.registerDefaultIcons(type, path); }); } /** * Set icon picker color * @param {string} iconColor - rgb color string */ setIconPickerColor(iconColor) { this._els.iconColorpicker.color = iconColor; } /** * Returns the menu to its default state. */ changeStandbyMode() { this.clearIconType(); this.actions.cancelAddIcon(); } /** * Change icon color * @param {string} color - color for change * @private */ _changeColorHandler(color) { color = color || 'transparent'; this.actions.changeColor(color); } /** * Change icon color * @param {object} event - add button event object * @private */ _addIconHandler(event) { const button = event.target.closest('.tui-image-editor-button'); if (button) { const iconType = button.getAttribute('data-icontype'); const iconColor = this._els.iconColorpicker.color; this.actions.discardSelection(); this.actions.changeSelectableAll(false); this._els.addIconButton.classList.remove(this.iconType); this._els.addIconButton.classList.add(iconType); if (this.iconType === iconType) { this.changeStandbyMode(); } else { this.actions.addIcon(iconType, iconColor); this.iconType = iconType; } } } /** * register icon * @param {object} event - file change event object * @private */ _registerIconHandler(event) { let imgUrl; if (!isSupportFileApi) { alert('This browser does not support file-api'); } const [file] = event.target.files; if (file) { imgUrl = URL.createObjectURL(file); this.actions.registerCustomIcon(imgUrl, file); } } } export default Icon; ================================================ FILE: apps/image-editor/src/js/ui/locale/locale.js ================================================ /** * Translate messages */ class Locale { constructor(locale) { this._locale = locale; } /** * localize message * @param {string} message - message who will be localized * @returns {string} */ localize(message) { return this._locale[message] || message; } } export default Locale; ================================================ FILE: apps/image-editor/src/js/ui/mask.js ================================================ import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/mask'; import { assignmentForDestroy, isSupportFileApi } from '@/util'; /** * Mask ui class * @class * @ignore */ class Mask extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'mask', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this._els = { applyButton: this.selector('.tie-mask-apply'), maskImageButton: this.selector('.tie-mask-image-file'), }; } /** * Destroys the instance. */ destroy() { this._removeEvent(); assignmentForDestroy(this); } /** * Add event for mask * @param {Object} actions - actions for crop * @param {Function} actions.loadImageFromURL - load image action * @param {Function} actions.applyFilter - apply filter action */ addEvent(actions) { const loadMaskFile = this._loadMaskFile.bind(this); const applyMask = this._applyMask.bind(this); this.eventHandler = { loadMaskFile, applyMask, }; this.actions = actions; this._els.maskImageButton.addEventListener('change', loadMaskFile); this._els.applyButton.addEventListener('click', applyMask); } /** * Remove event * @private */ _removeEvent() { this._els.maskImageButton.removeEventListener('change', this.eventHandler.loadMaskFile); this._els.applyButton.removeEventListener('click', this.eventHandler.applyMask); } /** * Apply mask * @private */ _applyMask() { this.actions.applyFilter(); this._els.applyButton.classList.remove('active'); } /** * Load mask file * @param {object} event - File change event object * @private */ _loadMaskFile(event) { let imgUrl; if (!isSupportFileApi()) { alert('This browser does not support file-api'); } const [file] = event.target.files; if (file) { imgUrl = URL.createObjectURL(file); this.actions.loadImageFromURL(imgUrl, file); this._els.applyButton.classList.add('active'); } } } export default Mask; ================================================ FILE: apps/image-editor/src/js/ui/panelMenu.js ================================================ /** * Menu Panel Class * @class * @ignore */ class Panel { /** * @param {HTMLElement} menuElement - menu dom element * @param {Object} options - menu options * @param {string} options.name - name of panel menu */ constructor(menuElement, { name }) { this.name = name; this.items = []; this.panelElement = this._makePanelElement(); this.listElement = this._makeListElement(); this.panelElement.appendChild(this.listElement); menuElement.appendChild(this.panelElement); } /** * Make Panel element * @returns {HTMLElement} */ _makePanelElement() { const panel = document.createElement('div'); panel.className = `tie-panel-${this.name}`; return panel; } /** * Make list element * @returns {HTMLElement} list element * @private */ _makeListElement() { const list = document.createElement('ol'); list.className = `${this.name}-list`; return list; } /** * Make list item element * @param {string} html - history list item html * @returns {HTMLElement} list item element */ makeListItemElement(html) { const listItem = document.createElement('li'); listItem.innerHTML = html; listItem.className = `${this.name}-item`; listItem.setAttribute('data-index', this.items.length); return listItem; } /** * Push list item element * @param {HTMLElement} item - list item element to add to the list */ pushListItemElement(item) { this.listElement.appendChild(item); this.listElement.scrollTop += item.offsetHeight; this.items.push(item); } /** * Delete list item element * @param {number} start - start index to delete * @param {number} end - end index to delete */ deleteListItemElement(start, end) { const { items } = this; for (let i = start; i < end; i += 1) { this.listElement.removeChild(items[i]); } items.splice(start, end - start + 1); } /** * Get list's length * @returns {number} */ getListLength() { return this.items.length; } /** * Add class name of item * @param {number} index - index of item * @param {string} className - class name to add */ addClass(index, className) { if (this.items[index]) { this.items[index].classList.add(className); } } /** * Remove class name of item * @param {number} index - index of item * @param {string} className - class name to remove */ removeClass(index, className) { if (this.items[index]) { this.items[index].classList.remove(className); } } /** * Toggle class name of item * @param {number} index - index of item * @param {string} className - class name to remove */ toggleClass(index, className) { if (this.items[index]) { this.items[index].classList.toggle(className); } } } export default Panel; ================================================ FILE: apps/image-editor/src/js/ui/resize.js ================================================ import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/resize'; import { assignmentForDestroy, toInteger } from '@/util'; import Range from '@/ui/tools/range'; import { defaultResizePixelValues } from '@/consts'; /** * Resize ui class * @class * @ignore */ class Resize extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'resize', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this.status = 'active'; this._lockState = false; /** * Original dimensions * @type {Object} * @private */ this._originalDimensions = null; this._els = { widthRange: new Range( { slider: this.selector('.tie-width-range'), input: this.selector('.tie-width-range-value'), }, defaultResizePixelValues ), heightRange: new Range( { slider: this.selector('.tie-height-range'), input: this.selector('.tie-height-range-value'), }, defaultResizePixelValues ), lockAspectRatio: this.selector('.tie-lock-aspect-ratio'), apply: this.selector('.tie-resize-button .apply'), cancel: this.selector('.tie-resize-button .cancel'), }; } /** * Executed when the menu starts. */ changeStartMode() { this.actions.modeChange('resize'); const dimensions = this.actions.getCurrentDimensions(); this._originalDimensions = dimensions; this.setWidthValue(dimensions.width); this.setHeightValue(dimensions.height); this.setLimit({ minWidth: defaultResizePixelValues.min, minHeight: defaultResizePixelValues.min, maxWidth: dimensions.width, maxHeight: dimensions.height, }); } /** * Returns the menu to its default state. */ changeStandbyMode() { this.actions.stopDrawingMode(); this.actions.reset(true); } /** * Set dimension limits * @param {object} limits - expect dimension limits for change */ setLimit(limits) { this._els.widthRange.min = this.calcMinValue(limits.minWidth); this._els.heightRange.min = this.calcMinValue(limits.minHeight); this._els.widthRange.max = this.calcMaxValue(limits.maxWidth); this._els.heightRange.max = this.calcMaxValue(limits.maxHeight); } /** * Calculate max value * @param {number} maxValue - max value * @returns {number} */ calcMaxValue(maxValue) { if (maxValue <= 0) { maxValue = defaultResizePixelValues.max; } return maxValue; } /** * Calculate min value * @param {number} minValue - min value * @returns {number} */ calcMinValue(minValue) { if (minValue <= 0) { minValue = defaultResizePixelValues.min; } return minValue; } /** * Set width value * @param {number} value - expect value for widthRange change * @param {boolean} trigger - fire change event control */ setWidthValue(value, trigger = false) { this._els.widthRange.value = value; if (trigger) { this._els.widthRange.trigger('change'); } } /** * Set height value * @param {number} value - expect value for heightRange change * @param {boolean} trigger - fire change event control */ setHeightValue(value, trigger = false) { this._els.heightRange.value = value; if (trigger) { this._els.heightRange.trigger('change'); } } /** * Destroys the instance. */ destroy() { this._removeEvent(); assignmentForDestroy(this); } /** * Add event for resize * @param {Object} actions - actions for resize * @param {Function} actions.resize - resize action * @param {Function} actions.preview - preview action * @param {Function} actions.getCurrentDimensions - Get current dimensions action * @param {Function} actions.modeChange - change mode * @param {Function} actions.stopDrawingMode - stop drawing mode * @param {Function} actions.lockAspectRatio - lock aspect ratio * @param {Function} actions.reset - reset action */ addEvent(actions) { this._els.widthRange.on('change', this._changeWidthRangeHandler.bind(this)); this._els.heightRange.on('change', this._changeHeightRangeHandler.bind(this)); this._els.lockAspectRatio.addEventListener('change', this._changeLockAspectRatio.bind(this)); const apply = this._applyEventHandler.bind(this); const cancel = this._cancelEventHandler.bind(this); this.eventHandler = { apply, cancel, }; this.actions = actions; this._els.apply.addEventListener('click', apply); this._els.cancel.addEventListener('click', cancel); } /** * Change width * @param {number} value - width range value * @private */ _changeWidthRangeHandler(value) { this.actions.preview('width', toInteger(value), this._lockState); } /** * Change height * @param {number} value - height range value * @private */ _changeHeightRangeHandler(value) { this.actions.preview('height', toInteger(value), this._lockState); } /** * Change lock aspect ratio state * @param {Event} event - aspect ratio check event * @private */ _changeLockAspectRatio(event) { this._lockState = event.target.checked; this.actions.lockAspectRatio(this._lockState); } /** * Remove event * @private */ _removeEvent() { this._els.apply.removeEventListener('click', this.eventHandler.apply); this._els.cancel.removeEventListener('click', this.eventHandler.cancel); } _applyEventHandler() { this.actions.resize(); this._els.apply.classList.remove('active'); } _cancelEventHandler() { this.actions.reset(); this._els.cancel.classList.remove('active'); } /** * Change apply button status * @param {Boolean} enableStatus - apply button status */ changeApplyButtonStatus(enableStatus) { if (enableStatus) { this._els.apply.classList.add('active'); } else { this._els.apply.classList.remove('active'); } } } export default Resize; ================================================ FILE: apps/image-editor/src/js/ui/rotate.js ================================================ import Range from '@/ui/tools/range'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/rotate'; import { toInteger, assignmentForDestroy } from '@/util'; import { defaultRotateRangeValues } from '@/consts'; const CLOCKWISE = 30; const COUNTERCLOCKWISE = -30; /** * Rotate ui class * @class * @ignore */ class Rotate extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'rotate', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this._value = 0; this._els = { rotateButton: this.selector('.tie-rotate-button'), rotateRange: new Range( { slider: this.selector('.tie-rotate-range'), input: this.selector('.tie-rotate-range-value'), }, defaultRotateRangeValues ), }; } /** * Destroys the instance. */ destroy() { this._removeEvent(); this._els.rotateRange.destroy(); assignmentForDestroy(this); } setRangeBarAngle(type, angle) { let resultAngle = angle; if (type === 'rotate') { resultAngle = parseInt(this._els.rotateRange.value, 10) + angle; } this._setRangeBarRatio(resultAngle); } _setRangeBarRatio(angle) { this._els.rotateRange.value = angle; } /** * Add event for rotate * @param {Object} actions - actions for crop * @param {Function} actions.rotate - rotate action * @param {Function} actions.setAngle - set angle action */ addEvent(actions) { this.eventHandler.rotationAngleChanged = this._changeRotateForButton.bind(this); // {rotate, setAngle} this.actions = actions; this._els.rotateButton.addEventListener('click', this.eventHandler.rotationAngleChanged); this._els.rotateRange.on('change', this._changeRotateForRange.bind(this)); } /** * Remove event * @private */ _removeEvent() { this._els.rotateButton.removeEventListener('click', this.eventHandler.rotationAngleChanged); this._els.rotateRange.off(); } /** * Change rotate for range * @param {number} value - angle value * @param {boolean} isLast - Is last change * @private */ _changeRotateForRange(value, isLast) { const angle = toInteger(value); this.actions.setAngle(angle, !isLast); this._value = angle; } /** * Change rotate for button * @param {object} event - add button event object * @private */ _changeRotateForButton(event) { const button = event.target.closest('.tui-image-editor-button'); const angle = this._els.rotateRange.value; if (button) { const rotateType = this.getButtonType(button, ['counterclockwise', 'clockwise']); const rotateAngle = { clockwise: CLOCKWISE, counterclockwise: COUNTERCLOCKWISE, }[rotateType]; const newAngle = parseInt(angle, 10) + rotateAngle; const isRotatable = newAngle >= -360 && newAngle <= 360; if (isRotatable) { this.actions.rotate(rotateAngle); } } } } export default Rotate; ================================================ FILE: apps/image-editor/src/js/ui/shape.js ================================================ import forEachArray from 'tui-code-snippet/collection/forEachArray'; import Colorpicker from '@/ui/tools/colorpicker'; import Range from '@/ui/tools/range'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/shape'; import { toInteger, assignmentForDestroy } from '@/util'; import { defaultShapeStrokeValues, eventNames, selectorNames } from '@/consts'; const SHAPE_DEFAULT_OPTION = { stroke: '#ffbb3b', fill: '', strokeWidth: 3, }; /** * Shape ui class * @class * @ignore */ class Shape extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'shape', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this.type = null; this.options = SHAPE_DEFAULT_OPTION; this._els = { shapeSelectButton: this.selector('.tie-shape-button'), shapeColorButton: this.selector('.tie-shape-color-button'), strokeRange: new Range( { slider: this.selector('.tie-stroke-range'), input: this.selector('.tie-stroke-range-value'), }, defaultShapeStrokeValues ), fillColorpicker: new Colorpicker(this.selector('.tie-color-fill'), { defaultColor: '', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), strokeColorpicker: new Colorpicker(this.selector('.tie-color-stroke'), { defaultColor: '#ffbb3b', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), }; this.colorPickerControls.push(this._els.fillColorpicker); this.colorPickerControls.push(this._els.strokeColorpicker); this.colorPickerInputBoxes = []; this.colorPickerInputBoxes.push( this._els.fillColorpicker.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ) ); this.colorPickerInputBoxes.push( this._els.strokeColorpicker.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ) ); } /** * Destroys the instance. */ destroy() { this._removeEvent(); this._els.strokeRange.destroy(); this._els.fillColorpicker.destroy(); this._els.strokeColorpicker.destroy(); assignmentForDestroy(this); } /** * Add event for shape * @param {Object} actions - actions for shape * @param {Function} actions.changeShape - change shape mode * @param {Function} actions.setDrawingShape - set drawing shape */ addEvent(actions) { this.eventHandler.shapeTypeSelected = this._changeShapeHandler.bind(this); this.actions = actions; this._els.shapeSelectButton.addEventListener('click', this.eventHandler.shapeTypeSelected); this._els.strokeRange.on('change', this._changeStrokeRangeHandler.bind(this)); this._els.fillColorpicker.on('change', this._changeFillColorHandler.bind(this)); this._els.strokeColorpicker.on('change', this._changeStrokeColorHandler.bind(this)); this._els.fillColorpicker.on('changeShow', this.colorPickerChangeShow.bind(this)); this._els.strokeColorpicker.on('changeShow', this.colorPickerChangeShow.bind(this)); forEachArray( this.colorPickerInputBoxes, (inputBox) => { inputBox.addEventListener(eventNames.FOCUS, this._onStartEditingInputBox.bind(this)); inputBox.addEventListener(eventNames.BLUR, this._onStopEditingInputBox.bind(this)); }, this ); } /** * Remove event * @private */ _removeEvent() { this._els.shapeSelectButton.removeEventListener('click', this.eventHandler.shapeTypeSelected); this._els.strokeRange.off(); this._els.fillColorpicker.off(); this._els.strokeColorpicker.off(); forEachArray( this.colorPickerInputBoxes, (inputBox) => { inputBox.removeEventListener(eventNames.FOCUS, this._onStartEditingInputBox.bind(this)); inputBox.removeEventListener(eventNames.BLUR, this._onStopEditingInputBox.bind(this)); }, this ); } /** * Set Shape status * @param {Object} options - options of shape status * @param {string} strokeWidth - stroke width * @param {string} strokeColor - stroke color * @param {string} fillColor - fill color */ setShapeStatus({ strokeWidth, strokeColor, fillColor }) { this._els.strokeRange.value = strokeWidth; this._els.strokeColorpicker.color = strokeColor; this._els.fillColorpicker.color = fillColor; this.options.stroke = strokeColor; this.options.fill = fillColor; this.options.strokeWidth = strokeWidth; this.actions.setDrawingShape(this.type, { strokeWidth }); } /** * Executed when the menu starts. */ changeStartMode() { this.actions.stopDrawingMode(); } /** * Returns the menu to its default state. */ changeStandbyMode() { this.type = null; this.actions.changeSelectableAll(true); this._els.shapeSelectButton.classList.remove('circle'); this._els.shapeSelectButton.classList.remove('triangle'); this._els.shapeSelectButton.classList.remove('rect'); } /** * set range stroke max value * @param {number} maxValue - expect max value for change */ setMaxStrokeValue(maxValue) { let strokeMaxValue = maxValue; if (strokeMaxValue <= 0) { strokeMaxValue = defaultShapeStrokeValues.max; } this._els.strokeRange.max = strokeMaxValue; } /** * Set stroke value * @param {number} value - expect value for strokeRange change */ setStrokeValue(value) { this._els.strokeRange.value = value; this._els.strokeRange.trigger('change'); } /** * Get stroke value * @returns {number} - stroke range value */ getStrokeValue() { return this._els.strokeRange.value; } /** * Change icon color * @param {object} event - add button event object * @private */ _changeShapeHandler(event) { const button = event.target.closest('.tui-image-editor-button'); if (button) { this.actions.stopDrawingMode(); this.actions.discardSelection(); const shapeType = this.getButtonType(button, ['circle', 'triangle', 'rect']); if (this.type === shapeType) { this.changeStandbyMode(); return; } this.changeStandbyMode(); this.type = shapeType; event.currentTarget.classList.add(shapeType); this.actions.changeSelectableAll(false); this.actions.modeChange('shape'); } } /** * Change stroke range * @param {number} value - stroke range value * @param {boolean} isLast - Is last change * @private */ _changeStrokeRangeHandler(value, isLast) { this.options.strokeWidth = toInteger(value); this.actions.changeShape( { strokeWidth: value, }, !isLast ); this.actions.setDrawingShape(this.type, this.options); } /** * Change shape color * @param {string} color - fill color * @private */ _changeFillColorHandler(color) { color = color || 'transparent'; this.options.fill = color; this.actions.changeShape({ fill: color, }); } /** * Change shape stroke color * @param {string} color - fill color * @private */ _changeStrokeColorHandler(color) { color = color || 'transparent'; this.options.stroke = color; this.actions.changeShape({ stroke: color, }); } } export default Shape; ================================================ FILE: apps/image-editor/src/js/ui/submenuBase.js ================================================ import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; import { eventNames } from '@/consts'; /** * Submenu Base Class * @class * @ignore */ class Submenu { /** * @param {HTMLElement} subMenuElement - submenu dom element * @param {Locale} locale - translate text * @param {string} name - name of sub menu * @param {Object} iconStyle - style of icon * @param {string} menuBarPosition - position of menu * @param {*} templateHtml - template for SubMenuElement * @param {boolean} [usageStatistics=false] - template for SubMenuElement */ constructor( subMenuElement, { locale, name, makeSvgIcon, menuBarPosition, templateHtml, usageStatistics } ) { this.subMenuElement = subMenuElement; this.menuBarPosition = menuBarPosition; this.toggleDirection = menuBarPosition === 'top' ? 'down' : 'up'; this.colorPickerControls = []; this.usageStatistics = usageStatistics; this.eventHandler = {}; this._makeSubMenuElement({ locale, name, makeSvgIcon, templateHtml, }); } /** * editor dom ui query selector * @param {string} selectName - query selector string name * @returns {HTMLElement} */ selector(selectName) { return this.subMenuElement.querySelector(selectName); } /** * change show state change for colorpicker instance * @param {Colorpicker} occurredControl - target Colorpicker Instance */ colorPickerChangeShow(occurredControl) { this.colorPickerControls.forEach((pickerControl) => { if (occurredControl !== pickerControl) { pickerControl.hide(); } }); } /** * Get button type * @param {HTMLElement} button - event target element * @param {array} buttonNames - Array of button names * @returns {string} - button type */ getButtonType(button, buttonNames) { return button.className.match(RegExp(`(${buttonNames.join('|')})`))[0]; } /** * Get button type * @param {HTMLElement} target - event target element * @param {string} removeClass - remove class name * @param {string} addClass - add class name */ changeClass(target, removeClass, addClass) { target.classList.remove(removeClass); target.classList.add(addClass); } /** * Interface method whose implementation is optional. * Returns the menu to its default state. */ changeStandbyMode() {} /** * Interface method whose implementation is optional. * Executed when the menu starts. */ changeStartMode() {} /** * Make submenu dom element * @param {Locale} locale - translate text * @param {string} name - submenu name * @param {Object} iconStyle - icon style * @param {*} templateHtml - template for SubMenuElement * @private */ _makeSubMenuElement({ locale, name, iconStyle, makeSvgIcon, templateHtml }) { const iconSubMenu = document.createElement('div'); iconSubMenu.className = `tui-image-editor-menu-${name}`; iconSubMenu.innerHTML = templateHtml({ locale, iconStyle, makeSvgIcon, }); this.subMenuElement.appendChild(iconSubMenu); } _onStartEditingInputBox() { this.fire(eventNames.INPUT_BOX_EDITING_STARTED); } _onStopEditingInputBox() { this.fire(eventNames.INPUT_BOX_EDITING_STOPPED); } } CustomEvents.mixin(Submenu); export default Submenu; ================================================ FILE: apps/image-editor/src/js/ui/template/controls.js ================================================ import { getHelpMenuBarPosition } from '@/util'; export default ({ locale, biImage, loadButtonStyle, downloadButtonStyle, menuBarPosition }) => `
      ${locale.localize('Load')}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/mainContainer.js ================================================ export default ({ locale, biImage, commonStyle, headerStyle, loadButtonStyle, downloadButtonStyle, submenuStyle, }) => `
      ${locale.localize('Load')}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/style.js ================================================ export default ({ subMenuLabelActive, subMenuLabelNormal, subMenuRangeTitle, submenuPartitionVertical, submenuPartitionHorizontal, submenuCheckbox, submenuRangePointer, submenuRangeValue, submenuColorpickerTitle, submenuColorpickerButton, submenuRangeBar, submenuRangeSubbar, submenuDisabledRangePointer, submenuDisabledRangeBar, submenuDisabledRangeSubbar, submenuIconSize, menuIconSize, biSize, menuIconStyle, submenuIconStyle, }) => ` .tie-icon-add-button.icon-bubble .tui-image-editor-button[data-icontype="icon-bubble"] label, .tie-icon-add-button.icon-heart .tui-image-editor-button[data-icontype="icon-heart"] label, .tie-icon-add-button.icon-location .tui-image-editor-button[data-icontype="icon-location"] label, .tie-icon-add-button.icon-polygon .tui-image-editor-button[data-icontype="icon-polygon"] label, .tie-icon-add-button.icon-star .tui-image-editor-button[data-icontype="icon-star"] label, .tie-icon-add-button.icon-star-2 .tui-image-editor-button[data-icontype="icon-star-2"] label, .tie-icon-add-button.icon-arrow-3 .tui-image-editor-button[data-icontype="icon-arrow-3"] label, .tie-icon-add-button.icon-arrow-2 .tui-image-editor-button[data-icontype="icon-arrow-2"] label, .tie-icon-add-button.icon-arrow .tui-image-editor-button[data-icontype="icon-arrow"] label, .tie-icon-add-button.icon-bubble .tui-image-editor-button[data-icontype="icon-bubble"] label, .tie-draw-line-select-button.line .tui-image-editor-button.line label, .tie-draw-line-select-button.free .tui-image-editor-button.free label, .tie-flip-button.flipX .tui-image-editor-button.flipX label, .tie-flip-button.flipY .tui-image-editor-button.flipY label, .tie-flip-button.resetFlip .tui-image-editor-button.resetFlip label, .tie-crop-button .tui-image-editor-button.apply.active label, .tie-crop-preset-button .tui-image-editor-button.preset.active label, .tie-resize-button .tui-image-editor-button.apply.active label, .tie-resize-preset-button .tui-image-editor-button.preset.active label, .tie-shape-button.rect .tui-image-editor-button.rect label, .tie-shape-button.circle .tui-image-editor-button.circle label, .tie-shape-button.triangle .tui-image-editor-button.triangle label, .tie-text-effect-button .tui-image-editor-button.active label, .tie-text-align-button.tie-text-align-left .tui-image-editor-button.left label, .tie-text-align-button.tie-text-align-center .tui-image-editor-button.center label, .tie-text-align-button.tie-text-align-right .tui-image-editor-button.right label, .tie-mask-apply.apply.active .tui-image-editor-button.apply label, .tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-button:hover > label, .tui-image-editor-container .tui-image-editor-checkbox label > span { ${subMenuLabelActive} } .tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-button > label, .tui-image-editor-container .tui-image-editor-range-wrap.tui-image-editor-newline.short label, .tui-image-editor-container .tui-image-editor-range-wrap.tui-image-editor-newline.short label > span { ${subMenuLabelNormal} } .tui-image-editor-container .tui-image-editor-range-wrap label > span { ${subMenuRangeTitle} } .tui-image-editor-container .tui-image-editor-partition > div { ${submenuPartitionVertical} } .tui-image-editor-container.left .tui-image-editor-submenu .tui-image-editor-partition > div, .tui-image-editor-container.right .tui-image-editor-submenu .tui-image-editor-partition > div { ${submenuPartitionHorizontal} } .tui-image-editor-container .tui-image-editor-checkbox label > span:before { ${submenuCheckbox} } .tui-image-editor-container .tui-image-editor-checkbox label > input:checked + span:before { border: 0; } .tui-image-editor-container .tui-image-editor-virtual-range-pointer { ${submenuRangePointer} } .tui-image-editor-container .tui-image-editor-virtual-range-bar { ${submenuRangeBar} } .tui-image-editor-container .tui-image-editor-virtual-range-subbar { ${submenuRangeSubbar} } .tui-image-editor-container .tui-image-editor-disabled .tui-image-editor-virtual-range-pointer { ${submenuDisabledRangePointer} } .tui-image-editor-container .tui-image-editor-disabled .tui-image-editor-virtual-range-subbar { ${submenuDisabledRangeSubbar} } .tui-image-editor-container .tui-image-editor-disabled .tui-image-editor-virtual-range-bar { ${submenuDisabledRangeBar} } .tui-image-editor-container .tui-image-editor-range-value { ${submenuRangeValue} } .tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-button .color-picker-value + label { ${submenuColorpickerTitle} } .tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-button .color-picker-value { ${submenuColorpickerButton} } .tui-image-editor-container .svg_ic-menu { ${menuIconSize} } .tui-image-editor-container .svg_ic-submenu { ${submenuIconSize} } .tui-image-editor-container .tui-image-editor-controls-logo > img, .tui-image-editor-container .tui-image-editor-header-logo > img { ${biSize} } .tui-image-editor-menu use.normal.use-default, .tui-image-editor-help-menu use.normal.use-default { fill-rule: evenodd; fill: ${menuIconStyle.normal.color}; stroke: ${menuIconStyle.normal.color}; } .tui-image-editor-menu use.active.use-default, .tui-image-editor-help-menu use.active.use-default { fill-rule: evenodd; fill: ${menuIconStyle.active.color}; stroke: ${menuIconStyle.active.color}; } .tui-image-editor-menu use.hover.use-default, .tui-image-editor-help-menu use.hover.use-default { fill-rule: evenodd; fill: ${menuIconStyle.hover.color}; stroke: ${menuIconStyle.hover.color}; } .tui-image-editor-menu use.disabled.use-default, .tui-image-editor-help-menu use.disabled.use-default { fill-rule: evenodd; fill: ${menuIconStyle.disabled.color}; stroke: ${menuIconStyle.disabled.color}; } .tui-image-editor-submenu use.normal.use-default { fill-rule: evenodd; fill: ${submenuIconStyle.normal.color}; stroke: ${submenuIconStyle.normal.color}; } .tui-image-editor-submenu use.active.use-default { fill-rule: evenodd; fill: ${submenuIconStyle.active.color}; stroke: ${submenuIconStyle.active.color}; } `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/crop.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'shape-rectangle', true)}
        ${makeSvgIcon(['normal', 'active'], 'crop', true)}
        ${makeSvgIcon(['normal', 'active'], 'crop', true)}
        ${makeSvgIcon(['normal', 'active'], 'crop', true)}
        ${makeSvgIcon(['normal', 'active'], 'crop', true)}
        ${makeSvgIcon(['normal', 'active'], 'crop', true)}
        ${makeSvgIcon(['normal', 'active'], 'crop', true)}
      • ${makeSvgIcon(['normal', 'active'], 'apply')}
        ${makeSvgIcon(['normal', 'active'], 'cancel')}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/draw.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'draw-free', true)}
        ${makeSvgIcon(['normal', 'active'], 'draw-line', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/filter.js ================================================ /** * @param {Locale} locale - Translate text * @returns {string} */ export default ({ locale }) => `
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/flip.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'flip-x', true)}
        ${makeSvgIcon(['normal', 'active'], 'flip-y', true)}
      • ${makeSvgIcon(['normal', 'active'], 'flip-reset', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/history.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @param {string} name - history name * @param {string} detail - history detail information * @returns {string} */ export default ({ locale, makeSvgIcon, name, detail }) => `
      ${makeSvgIcon(['normal', 'active'], `history-${name.toLowerCase()}`, true)}
      ${locale.localize(name)} ${detail ? `(${locale.localize(detail)})` : ''}
      ${makeSvgIcon(['normal'], 'history-check', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/icon.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'icon-arrow', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-arrow-2', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-arrow-3', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-star', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-star-2', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-polygon', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-location', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-heart', true)}
        ${makeSvgIcon(['normal', 'active'], 'icon-bubble', true)}
      • ${makeSvgIcon(['normal', 'active'], 'icon-load', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/mask.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'mask-load', true)}
      • ${makeSvgIcon(['normal', 'active'], 'apply')}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/resize.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'apply')}
        ${makeSvgIcon(['normal', 'active'], 'cancel')}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/rotate.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'rotate-clockwise', true)}
        ${makeSvgIcon(['normal', 'active'], 'rotate-counterclockwise', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/shape.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'shape-rectangle', true)}
        ${makeSvgIcon(['normal', 'active'], 'shape-circle', true)}
        ${makeSvgIcon(['normal', 'active'], 'shape-triangle', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/text.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'text-bold', true)}
        ${makeSvgIcon(['normal', 'active'], 'text-italic', true)}
        ${makeSvgIcon(['normal', 'active'], 'text-underline', true)}
      • ${makeSvgIcon(['normal', 'active'], 'text-align-left', true)}
        ${makeSvgIcon(['normal', 'active'], 'text-align-center', true)}
        ${makeSvgIcon(['normal', 'active'], 'text-align-right', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/template/submenu/zoom.js ================================================ /** * @param {Object} submenuInfo - submenu info for make template * @param {Locale} locale - Translate text * @param {Function} makeSvgIcon - svg icon generator * @returns {string} */ export default ({ locale, makeSvgIcon }) => `
      • ${makeSvgIcon(['normal', 'active'], 'zoom-in', true)}
        ${makeSvgIcon(['normal', 'active'], 'zoom-out', true)}
      • ${makeSvgIcon(['normal', 'active'], 'zoom-hand', true)}
      `; ================================================ FILE: apps/image-editor/src/js/ui/text.js ================================================ import Range from '@/ui/tools/range'; import Colorpicker from '@/ui/tools/colorpicker'; import Submenu from '@/ui/submenuBase'; import templateHtml from '@/ui/template/submenu/text'; import { assignmentForDestroy } from '@/util'; import { defaultTextRangeValues, eventNames, selectorNames } from '@/consts'; /** * Crop ui class * @class * @ignore */ class Text extends Submenu { constructor(subMenuElement, { locale, makeSvgIcon, menuBarPosition, usageStatistics }) { super(subMenuElement, { locale, name: 'text', makeSvgIcon, menuBarPosition, templateHtml, usageStatistics, }); this.effect = { bold: false, italic: false, underline: false, }; this.align = 'tie-text-align-left'; this._els = { textEffectButton: this.selector('.tie-text-effect-button'), textAlignButton: this.selector('.tie-text-align-button'), textColorpicker: new Colorpicker(this.selector('.tie-text-color'), { defaultColor: '#ffbb3b', toggleDirection: this.toggleDirection, usageStatistics: this.usageStatistics, }), textRange: new Range( { slider: this.selector('.tie-text-range'), input: this.selector('.tie-text-range-value'), }, defaultTextRangeValues ), }; this.colorPickerInputBox = this._els.textColorpicker.colorpickerElement.querySelector( selectorNames.COLOR_PICKER_INPUT_BOX ); } /** * Destroys the instance. */ destroy() { this._removeEvent(); this._els.textColorpicker.destroy(); this._els.textRange.destroy(); assignmentForDestroy(this); } /** * Add event for text * @param {Object} actions - actions for text * @param {Function} actions.changeTextStyle - change text style */ addEvent(actions) { const setTextEffect = this._setTextEffectHandler.bind(this); const setTextAlign = this._setTextAlignHandler.bind(this); this.eventHandler = { setTextEffect, setTextAlign, }; this.actions = actions; this._els.textEffectButton.addEventListener('click', setTextEffect); this._els.textAlignButton.addEventListener('click', setTextAlign); this._els.textRange.on('change', this._changeTextRnageHandler.bind(this)); this._els.textColorpicker.on('change', this._changeColorHandler.bind(this)); this.colorPickerInputBox.addEventListener( eventNames.FOCUS, this._onStartEditingInputBox.bind(this) ); this.colorPickerInputBox.addEventListener( eventNames.BLUR, this._onStopEditingInputBox.bind(this) ); } /** * Remove event * @private */ _removeEvent() { const { setTextEffect, setTextAlign } = this.eventHandler; this._els.textEffectButton.removeEventListener('click', setTextEffect); this._els.textAlignButton.removeEventListener('click', setTextAlign); this._els.textRange.off(); this._els.textColorpicker.off(); this.colorPickerInputBox.removeEventListener( eventNames.FOCUS, this._onStartEditingInputBox.bind(this) ); this.colorPickerInputBox.removeEventListener( eventNames.BLUR, this._onStopEditingInputBox.bind(this) ); } /** * Returns the menu to its default state. */ changeStandbyMode() { this.actions.stopDrawingMode(); } /** * Executed when the menu starts. */ changeStartMode() { this.actions.modeChange('text'); } /** * Get text color * @returns {string} - text color */ get textColor() { return this._els.textColorpicker.color; } set textColor(color) { this._els.textColorpicker.color = color; } /** * Get text size * @returns {string} - text size */ get fontSize() { return this._els.textRange.value; } /** * Set text size * @param {Number} value - text size */ set fontSize(value) { this._els.textRange.value = value; } /** * get font style * @returns {string} - font style */ get fontStyle() { return this.effect.italic ? 'italic' : 'normal'; } /** * get font weight * @returns {string} - font weight */ get fontWeight() { return this.effect.bold ? 'bold' : 'normal'; } /** * get text underline text underline * @returns {boolean} - true or false */ get underline() { return this.effect.underline; } setTextStyleStateOnAction(textStyle = {}) { const { fill, fontSize, fontStyle, fontWeight, textDecoration, textAlign } = textStyle; this.textColor = fill; this.fontSize = fontSize; this.setEffectState('italic', fontStyle); this.setEffectState('bold', fontWeight); this.setEffectState('underline', textDecoration); this.setAlignState(`tie-text-align-${textAlign}`); } setEffectState(effectName, value) { const effectValue = value === 'italic' || value === 'bold' || value === 'underline'; const button = this._els.textEffectButton.querySelector( `.tui-image-editor-button.${effectName}` ); this.effect[effectName] = effectValue; button.classList[effectValue ? 'add' : 'remove']('active'); } setAlignState(value) { const button = this._els.textAlignButton; button.classList.remove(this.align); button.classList.add(value); this.align = value; } /** * text effect set handler * @param {object} event - add button event object * @private */ _setTextEffectHandler(event) { const button = event.target.closest('.tui-image-editor-button'); if (button) { const [styleType] = button.className.match(/(bold|italic|underline)/); const styleObj = { bold: { fontWeight: 'bold' }, italic: { fontStyle: 'italic' }, underline: { textDecoration: 'underline' }, }[styleType]; this.effect[styleType] = !this.effect[styleType]; button.classList.toggle('active'); this.actions.changeTextStyle(styleObj); } } /** * text effect set handler * @param {object} event - add button event object * @private */ _setTextAlignHandler(event) { const button = event.target.closest('.tui-image-editor-button'); if (button) { const styleType = this.getButtonType(button, ['left', 'center', 'right']); const styleTypeAlias = `tie-text-align-${styleType}`; event.currentTarget.classList.remove(this.align); if (this.align !== styleTypeAlias) { event.currentTarget.classList.add(styleTypeAlias); } this.actions.changeTextStyle({ textAlign: styleType }); this.align = styleTypeAlias; } } /** * text align set handler * @param {number} value - range value * @param {boolean} isLast - Is last change * @private */ _changeTextRnageHandler(value, isLast) { this.actions.changeTextStyle( { fontSize: value, }, !isLast ); } /** * change color handler * @param {string} color - change color string * @private */ _changeColorHandler(color) { color = color || 'transparent'; this.actions.changeTextStyle({ fill: color, }); } } export default Text; ================================================ FILE: apps/image-editor/src/js/ui/theme/standard.js ================================================ /** * Full configuration for theme.
      * @typedef {object} themeConfig * @property {string} common.bi.image - Brand icon image * @property {string} common.bisize.width - Icon image width * @property {string} common.bisize.height - Icon Image Height * @property {string} common.backgroundImage - Background image * @property {string} common.backgroundColor - Background color * @property {string} common.border - Full area border style * @property {string} header.backgroundImage - header area background * @property {string} header.backgroundColor - header area background color * @property {string} header.border - header area border style * @property {string} loadButton.backgroundColor - load button background color * @property {string} loadButton.border - load button border style * @property {string} loadButton.color - load button foreground color * @property {string} loadButton.fontFamily - load button font type * @property {string} loadButton.fontSize - load button font size * @property {string} downloadButton.backgroundColor - download button background color * @property {string} downloadButton.border - download button border style * @property {string} downloadButton.color - download button foreground color * @property {string} downloadButton.fontFamily - download button font type * @property {string} downloadButton.fontSize - download button font size * @property {string} menu.normalIcon.color - Menu normal color for default icon * @property {string} menu.normalIcon.path - Menu normal icon svg bundle file path * @property {string} menu.normalIcon.name - Menu normal icon svg bundle name * @property {string} menu.activeIcon.color - Menu active color for default icon * @property {string} menu.activeIcon.path - Menu active icon svg bundle file path * @property {string} menu.activeIcon.name - Menu active icon svg bundle name * @property {string} menu.disabled.color - Menu disabled color for default icon * @property {string} menu.disabled.path - Menu disabled icon svg bundle file path * @property {string} menu.disabled.name - Menu disabled icon svg bundle name * @property {string} menu.hover.color - Menu default icon hover color * @property {string} menu.hover.path - Menu hover icon svg bundle file path * @property {string} menu.hover.name - Menu hover icon svg bundle name * @property {string} menu.iconSize.width - Menu icon Size Width * @property {string} menu.iconSize.height - Menu Icon Size Height * @property {string} submenu.backgroundColor - Sub-menu area background color * @property {string} submenu.partition.color - Submenu partition line color * @property {string} submenu.normalIcon.color - Submenu normal color for default icon * @property {string} submenu.normalIcon.path - Submenu default icon svg bundle file path * @property {string} submenu.normalIcon.name - Submenu default icon svg bundle name * @property {string} submenu.activeIcon.color - Submenu active color for default icon * @property {string} submenu.activeIcon.path - Submenu active icon svg bundle file path * @property {string} submenu.activeIcon.name - Submenu active icon svg bundle name * @property {string} submenu.iconSize.width - Submenu icon Size Width * @property {string} submenu.iconSize.height - Submenu Icon Size Height * @property {string} submenu.normalLabel.color - Submenu default label color * @property {string} submenu.normalLabel.fontWeight - Sub Menu Default Label Font Thickness * @property {string} submenu.activeLabel.color - Submenu active label color * @property {string} submenu.activeLabel.fontWeight - Submenu active label Font thickness * @property {string} checkbox.border - Checkbox border style * @property {string} checkbox.backgroundColor - Checkbox background color * @property {string} range.pointer.color - range control pointer color * @property {string} range.bar.color - range control bar color * @property {string} range.subbar.color - range control subbar color * @property {string} range.value.color - range number box font color * @property {string} range.value.fontWeight - range number box font thickness * @property {string} range.value.fontSize - range number box font size * @property {string} range.value.border - range number box border style * @property {string} range.value.backgroundColor - range number box background color * @property {string} range.title.color - range title font color * @property {string} range.title.fontWeight - range title font weight * @property {string} colorpicker.button.border - colorpicker button border style * @property {string} colorpicker.title.color - colorpicker button title font color * @example // default keys and styles var customTheme = { 'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png', 'common.bisize.width': '251px', 'common.bisize.height': '21px', 'common.backgroundImage': 'none', 'common.backgroundColor': '#1e1e1e', 'common.border': '0px', // header 'header.backgroundImage': 'none', 'header.backgroundColor': 'transparent', 'header.border': '0px', // load button 'loadButton.backgroundColor': '#fff', 'loadButton.border': '1px solid #ddd', 'loadButton.color': '#222', 'loadButton.fontFamily': 'NotoSans, sans-serif', 'loadButton.fontSize': '12px', // download button 'downloadButton.backgroundColor': '#fdba3b', 'downloadButton.border': '1px solid #fdba3b', 'downloadButton.color': '#fff', 'downloadButton.fontFamily': 'NotoSans, sans-serif', 'downloadButton.fontSize': '12px', // icons default 'menu.normalIcon.color': '#8a8a8a', 'menu.activeIcon.color': '#555555', 'menu.disabledIcon.color': '#434343', 'menu.hoverIcon.color': '#e9e9e9', 'submenu.normalIcon.color': '#8a8a8a', 'submenu.activeIcon.color': '#e9e9e9', 'menu.iconSize.width': '24px', 'menu.iconSize.height': '24px', 'submenu.iconSize.width': '32px', 'submenu.iconSize.height': '32px', // submenu primary color 'submenu.backgroundColor': '#1e1e1e', 'submenu.partition.color': '#858585', // submenu labels 'submenu.normalLabel.color': '#858585', 'submenu.normalLabel.fontWeight': 'lighter', 'submenu.activeLabel.color': '#fff', 'submenu.activeLabel.fontWeight': 'lighter', // checkbox style 'checkbox.border': '1px solid #ccc', 'checkbox.backgroundColor': '#fff', // rango style 'range.pointer.color': '#fff', 'range.bar.color': '#666', 'range.subbar.color': '#d1d1d1', 'range.disabledPointer.color': '#414141', 'range.disabledBar.color': '#282828', 'range.disabledSubbar.color': '#414141', 'range.value.color': '#fff', 'range.value.fontWeight': 'lighter', 'range.value.fontSize': '11px', 'range.value.border': '1px solid #353535', 'range.value.backgroundColor': '#151515', 'range.title.color': '#fff', 'range.title.fontWeight': 'lighter', // colorpicker style 'colorpicker.button.border': '1px solid #1e1e1e', 'colorpicker.title.color': '#fff' }; */ export default { 'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png', 'common.bisize.width': '251px', 'common.bisize.height': '21px', 'common.backgroundImage': 'none', 'common.backgroundColor': '#1e1e1e', 'common.border': '0px', // header 'header.backgroundImage': 'none', 'header.backgroundColor': 'transparent', 'header.border': '0px', // load button 'loadButton.backgroundColor': '#fff', 'loadButton.border': '1px solid #ddd', 'loadButton.color': '#222', 'loadButton.fontFamily': "'Noto Sans', sans-serif", 'loadButton.fontSize': '12px', // download button 'downloadButton.backgroundColor': '#fdba3b', 'downloadButton.border': '1px solid #fdba3b', 'downloadButton.color': '#fff', 'downloadButton.fontFamily': "'Noto Sans', sans-serif", 'downloadButton.fontSize': '12px', // main icons 'menu.normalIcon.color': '#8a8a8a', 'menu.activeIcon.color': '#555555', 'menu.disabledIcon.color': '#434343', 'menu.hoverIcon.color': '#e9e9e9', // submenu icons 'submenu.normalIcon.color': '#8a8a8a', 'submenu.activeIcon.color': '#e9e9e9', 'menu.iconSize.width': '24px', 'menu.iconSize.height': '24px', 'submenu.iconSize.width': '32px', 'submenu.iconSize.height': '32px', // submenu primary color 'submenu.backgroundColor': '#1e1e1e', 'submenu.partition.color': '#3c3c3c', // submenu labels 'submenu.normalLabel.color': '#8a8a8a', 'submenu.normalLabel.fontWeight': 'lighter', 'submenu.activeLabel.color': '#fff', 'submenu.activeLabel.fontWeight': 'lighter', // checkbox style 'checkbox.border': '0px', 'checkbox.backgroundColor': '#fff', // range style 'range.pointer.color': '#fff', 'range.bar.color': '#666', 'range.subbar.color': '#d1d1d1', 'range.disabledPointer.color': '#414141', 'range.disabledBar.color': '#282828', 'range.disabledSubbar.color': '#414141', 'range.value.color': '#fff', 'range.value.fontWeight': 'lighter', 'range.value.fontSize': '11px', 'range.value.border': '1px solid #353535', 'range.value.backgroundColor': '#151515', 'range.title.color': '#fff', 'range.title.fontWeight': 'lighter', // colorpicker style 'colorpicker.button.border': '1px solid #1e1e1e', 'colorpicker.title.color': '#fff', }; ================================================ FILE: apps/image-editor/src/js/ui/theme/theme.js ================================================ import extend from 'tui-code-snippet/object/extend'; import forEach from 'tui-code-snippet/collection/forEach'; import style from '@/ui/template/style'; import standardTheme from '@/ui/theme/standard'; import { styleLoad } from '@/util'; import icon from '@svg/default.svg'; /** * Theme manager * @class * @param {Object} customTheme - custom theme * @ignore */ class Theme { constructor(customTheme) { this.styles = this._changeToObject(extend({}, standardTheme, customTheme)); styleLoad(this._styleMaker()); this._loadDefaultSvgIcon(); } /** * Get a Style cssText or StyleObject * @param {string} type - style type * @returns {string|object} - cssText or StyleObject */ // eslint-disable-next-line complexity getStyle(type) { let result = null; const firstProperty = type.replace(/\..+$/, ''); const option = this.styles[type]; switch (type) { case 'common.bi': result = this.styles[type].image; break; case 'menu.icon': result = { active: this.styles[`${firstProperty}.activeIcon`], normal: this.styles[`${firstProperty}.normalIcon`], hover: this.styles[`${firstProperty}.hoverIcon`], disabled: this.styles[`${firstProperty}.disabledIcon`], }; break; case 'submenu.icon': result = { active: this.styles[`${firstProperty}.activeIcon`], normal: this.styles[`${firstProperty}.normalIcon`], }; break; case 'submenu.label': result = { active: this._makeCssText(this.styles[`${firstProperty}.activeLabel`]), normal: this._makeCssText(this.styles[`${firstProperty}.normalLabel`]), }; break; case 'submenu.partition': result = { vertical: this._makeCssText( extend({}, option, { borderLeft: `1px solid ${option.color}` }) ), horizontal: this._makeCssText( extend({}, option, { borderBottom: `1px solid ${option.color}` }) ), }; break; case 'range.disabledPointer': case 'range.disabledBar': case 'range.disabledSubbar': case 'range.pointer': case 'range.bar': case 'range.subbar': option.backgroundColor = option.color; result = this._makeCssText(option); break; default: result = this._makeCssText(option); break; } return result; } /** * Make css resource * @returns {string} - serialized css text * @private */ _styleMaker() { const submenuLabelStyle = this.getStyle('submenu.label'); const submenuPartitionStyle = this.getStyle('submenu.partition'); return style({ subMenuLabelActive: submenuLabelStyle.active, subMenuLabelNormal: submenuLabelStyle.normal, submenuPartitionVertical: submenuPartitionStyle.vertical, submenuPartitionHorizontal: submenuPartitionStyle.horizontal, biSize: this.getStyle('common.bisize'), subMenuRangeTitle: this.getStyle('range.title'), submenuRangePointer: this.getStyle('range.pointer'), submenuRangeBar: this.getStyle('range.bar'), submenuRangeSubbar: this.getStyle('range.subbar'), submenuDisabledRangePointer: this.getStyle('range.disabledPointer'), submenuDisabledRangeBar: this.getStyle('range.disabledBar'), submenuDisabledRangeSubbar: this.getStyle('range.disabledSubbar'), submenuRangeValue: this.getStyle('range.value'), submenuColorpickerTitle: this.getStyle('colorpicker.title'), submenuColorpickerButton: this.getStyle('colorpicker.button'), submenuCheckbox: this.getStyle('checkbox'), menuIconSize: this.getStyle('menu.iconSize'), submenuIconSize: this.getStyle('submenu.iconSize'), menuIconStyle: this.getStyle('menu.icon'), submenuIconStyle: this.getStyle('submenu.icon'), }); } /** * Change to low dimensional object. * @param {object} styleOptions - style object of user interface * @returns {object} low level object for style apply * @private */ _changeToObject(styleOptions) { const styleObject = {}; forEach(styleOptions, (value, key) => { const keyExplode = key.match(/^(.+)\.([a-z]+)$/i); const [, property, subProperty] = keyExplode; if (!styleObject[property]) { styleObject[property] = {}; } styleObject[property][subProperty] = value; }); return styleObject; } /** * Style object to Csstext serialize * @param {object} styleObject - style object * @returns {string} - css text string * @private */ _makeCssText(styleObject) { const converterStack = []; forEach(styleObject, (value, key) => { if (['backgroundImage'].indexOf(key) > -1 && value !== 'none') { value = `url(${value})`; } converterStack.push(`${this._toUnderScore(key)}: ${value}`); }); return converterStack.join(';'); } /** * Camel key string to Underscore string * @param {string} targetString - change target * @returns {string} * @private */ _toUnderScore(targetString) { return targetString.replace(/([A-Z])/g, ($0, $1) => `-${$1.toLowerCase()}`); } /** * Load default svg icon * @private */ _loadDefaultSvgIcon() { if (!document.getElementById('tui-image-editor-svg-default-icons')) { const parser = new DOMParser(); const encodedURI = icon.replace(/data:image\/svg\+xml;base64,/, ''); const dom = parser.parseFromString(atob(encodedURI), 'text/xml'); document.body.appendChild(dom.documentElement); } } /** * Make className for svg icon * @param {string} iconType - normal' or 'active' or 'hover' or 'disabled * @param {boolean} isSubmenu - submenu icon or not. * @returns {string} * @private */ _makeIconClassName(iconType, isSubmenu) { const iconStyleInfo = isSubmenu ? this.getStyle('submenu.icon') : this.getStyle('menu.icon'); const { path, name } = iconStyleInfo[iconType]; return path && name ? iconType : `${iconType} use-default`; } /** * Make svg use link path name * @param {string} iconType - normal' or 'active' or 'hover' or 'disabled * @param {boolean} isSubmenu - submenu icon or not. * @returns {string} * @private */ _makeSvgIconPrefix(iconType, isSubmenu) { const iconStyleInfo = isSubmenu ? this.getStyle('submenu.icon') : this.getStyle('menu.icon'); const { path, name } = iconStyleInfo[iconType]; return path && name ? `${path}#${name}-` : '#'; } /** * Make svg use link path name * @param {Array.} useIconTypes - normal' or 'active' or 'hover' or 'disabled * @param {string} menuName - menu name * @param {boolean} isSubmenu - submenu icon or not. * @returns {string} * @private */ _makeSvgItem(useIconTypes, menuName, isSubmenu) { return useIconTypes .map((iconType) => { const svgIconPrefix = this._makeSvgIconPrefix(iconType, isSubmenu); const iconName = this._toUnderScore(menuName); const svgIconClassName = this._makeIconClassName(iconType, isSubmenu); return ``; }) .join(''); } /** * Make svg icon set * @param {Array.} useIconTypes - normal' or 'active' or 'hover' or 'disabled * @param {string} menuName - menu name * @param {boolean} isSubmenu - submenu icon or not. * @returns {string} */ makeMenSvgIconSet(useIconTypes, menuName, isSubmenu = false) { return `${this._makeSvgItem( useIconTypes, menuName, isSubmenu )}`; } } export default Theme; ================================================ FILE: apps/image-editor/src/js/ui/tools/colorpicker.js ================================================ import forEach from 'tui-code-snippet/collection/forEach'; import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; import tuiColorPicker from 'tui-color-picker'; const PICKER_COLOR = [ '#000000', '#2a2a2a', '#545454', '#7e7e7e', '#a8a8a8', '#d2d2d2', '#ffffff', '', '#ff4040', '#ff6518', '#ffbb3b', '#03bd9e', '#00a9ff', '#515ce6', '#9e5fff', '#ff5583', ]; /** * Colorpicker control class * @class * @ignore */ class Colorpicker { constructor( colorpickerElement, { defaultColor = '#7e7e7e', toggleDirection = 'up', usageStatistics } ) { this.colorpickerElement = colorpickerElement; this.usageStatistics = usageStatistics; this._show = false; this._colorpickerElement = colorpickerElement; this._toggleDirection = toggleDirection; this._makePickerButtonElement(defaultColor); this._makePickerLayerElement(colorpickerElement, colorpickerElement.getAttribute('title')); this._color = defaultColor; this.picker = tuiColorPicker.create({ container: this.pickerElement, preset: PICKER_COLOR, color: defaultColor, usageStatistics: this.usageStatistics, }); this._addEvent(); } /** * Destroys the instance. */ destroy() { this._removeEvent(); this.picker.destroy(); this.colorpickerElement.innerHTML = ''; forEach(this, (value, key) => { this[key] = null; }); } /** * Get color * @returns {Number} color value */ get color() { return this._color; } /** * Set color * @param {string} color color value */ set color(color) { this._color = color; this._changeColorElement(color); } /** * Change color element * @param {string} color color value * #private */ _changeColorElement(color) { if (color) { this.colorElement.classList.remove('transparent'); this.colorElement.style.backgroundColor = color; } else { this.colorElement.style.backgroundColor = '#fff'; this.colorElement.classList.add('transparent'); } } /** * Make picker button element * @param {string} defaultColor color value * @private */ _makePickerButtonElement(defaultColor) { this.colorpickerElement.classList.add('tui-image-editor-button'); this.colorElement = document.createElement('div'); this.colorElement.className = 'color-picker-value'; if (defaultColor) { this.colorElement.style.backgroundColor = defaultColor; } else { this.colorElement.classList.add('transparent'); } } /** * Make picker layer element * @param {HTMLElement} colorpickerElement color picker element * @param {string} title picker title * @private */ _makePickerLayerElement(colorpickerElement, title) { const label = document.createElement('label'); const triangle = document.createElement('div'); this.pickerControl = document.createElement('div'); this.pickerControl.className = 'color-picker-control'; this.pickerElement = document.createElement('div'); this.pickerElement.className = 'color-picker'; label.innerHTML = title; triangle.className = 'triangle'; this.pickerControl.appendChild(this.pickerElement); this.pickerControl.appendChild(triangle); colorpickerElement.appendChild(this.pickerControl); colorpickerElement.appendChild(this.colorElement); colorpickerElement.appendChild(label); } /** * Add event * @private */ _addEvent() { this.picker.on('selectColor', (value) => { this._changeColorElement(value.color); this._color = value.color; this.fire('change', value.color); }); this.eventHandler = { pickerToggle: this._pickerToggleEventHandler.bind(this), pickerHide: () => this.hide(), }; this.colorpickerElement.addEventListener('click', this.eventHandler.pickerToggle); document.body.addEventListener('click', this.eventHandler.pickerHide); } /** * Remove event * @private */ _removeEvent() { this.colorpickerElement.removeEventListener('click', this.eventHandler.pickerToggle); document.body.removeEventListener('click', this.eventHandler.pickerHide); this.picker.off(); } /** * Picker toggle event handler * @param {object} event - change event * @private */ _pickerToggleEventHandler(event) { const { target } = event; const isInPickerControl = target && this._isElementInColorPickerControl(target); if (!isInPickerControl || (isInPickerControl && this._isPaletteButton(target))) { this._show = !this._show; this.pickerControl.style.display = this._show ? 'block' : 'none'; this._setPickerControlPosition(); this.fire('changeShow', this); } event.stopPropagation(); } /** * Check hex input or not * @param {Element} target - Event target element * @returns {boolean} * @private */ _isPaletteButton(target) { return target.className === 'tui-colorpicker-palette-button'; } /** * Check given element is in pickerControl element * @param {Element} element - element to check * @returns {boolean} * @private */ _isElementInColorPickerControl(element) { let parentNode = element; while (parentNode !== document.body) { if (!parentNode) { break; } if (parentNode === this.pickerControl) { return true; } parentNode = parentNode.parentNode; } return false; } hide() { this._show = false; this.pickerControl.style.display = 'none'; } /** * Set picker control position * @private */ _setPickerControlPosition() { const controlStyle = this.pickerControl.style; const halfPickerWidth = this._colorpickerElement.clientWidth / 2 + 2; const left = this.pickerControl.offsetWidth / 2 - halfPickerWidth; let top = (this.pickerControl.offsetHeight + 10) * -1; if (this._toggleDirection === 'down') { top = 30; } controlStyle.top = `${top}px`; controlStyle.left = `-${left}px`; } } CustomEvents.mixin(Colorpicker); export default Colorpicker; ================================================ FILE: apps/image-editor/src/js/ui/tools/range.js ================================================ import forEach from 'tui-code-snippet/collection/forEach'; import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; import { toInteger, clamp } from '@/util'; import { keyCodes } from '@/consts'; const INPUT_FILTER_REGEXP = /(-?)([0-9]*)[^0-9]*([0-9]*)/g; /** * Range control class * @class * @ignore */ class Range { /** * @constructor * @extends {View} * @param {Object} rangeElements - Html resources for creating sliders * @param {HTMLElement} rangeElements.slider - b * @param {HTMLElement} [rangeElements.input] - c * @param {Object} options - Slider make options * @param {number} options.min - min value * @param {number} options.max - max value * @param {number} options.value - default value * @param {number} [options.useDecimal] - Decimal point processing. * @param {boolean} [options.realTimeEvent] - Reflect live events. */ constructor(rangeElements, options = {}) { this._value = options.value || 0; this.rangeElement = rangeElements.slider; this.rangeInputElement = rangeElements.input; this._drawRangeElement(); this.rangeWidth = this._getRangeWidth(); this._min = options.min || 0; this._max = options.max || 100; this._useDecimal = options.useDecimal; this._absMax = this._min * -1 + this._max; this.realTimeEvent = options.realTimeEvent || false; this._userInputTimer = null; this.eventHandler = { startChangingSlide: this._startChangingSlide.bind(this), stopChangingSlide: this._stopChangingSlide.bind(this), changeSlide: this._changeSlide.bind(this), changeSlideFinally: this._changeSlideFinally.bind(this), changeInput: this._changeInput.bind(this), changeInputFinally: this._changeValueWithInput.bind(this, true), changeInputWithArrow: this._changeValueWithInputKeyEvent.bind(this), }; this._addClickEvent(); this._addDragEvent(); this._addInputEvent(); this.value = options.value; this.trigger('change'); } /** * Destroys the instance. */ destroy() { this._removeClickEvent(); this._removeDragEvent(); this._removeInputEvent(); this.rangeElement.innerHTML = ''; forEach(this, (value, key) => { this[key] = null; }); } get max() { return this._max; } /** * Set range max value and re position cursor * @param {number} maxValue - max value */ set max(maxValue) { this._max = maxValue; this._absMax = this._min * -1 + this._max; this.value = this._value; } get min() { return this._min; } /** * Set range min value and re position cursor * @param {number} minValue - min value */ set min(minValue) { this._min = minValue; this.max = this._max; } /** * Get range value * @returns {Number} range value */ get value() { return this._value; } /** * Set range value * @param {Number} value range value */ set value(value) { value = this._useDecimal ? value : toInteger(value); const absValue = value - this._min; let leftPosition = (absValue * this.rangeWidth) / this._absMax; if (this.rangeWidth < leftPosition) { leftPosition = this.rangeWidth; } this.pointer.style.left = `${leftPosition}px`; this.subbar.style.right = `${this.rangeWidth - leftPosition}px`; this._value = value; if (this.rangeInputElement) { this.rangeInputElement.value = value; } } /** * event trigger * @param {string} type - type */ trigger(type) { this.fire(type, this._value); } /** * Calculate slider width * @returns {number} - slider width */ _getRangeWidth() { const getElementWidth = (element) => toInteger(window.getComputedStyle(element, null).width); return getElementWidth(this.rangeElement) - getElementWidth(this.pointer); } /** * Make range element * @private */ _drawRangeElement() { this.rangeElement.classList.add('tui-image-editor-range'); this.bar = document.createElement('div'); this.bar.className = 'tui-image-editor-virtual-range-bar'; this.subbar = document.createElement('div'); this.subbar.className = 'tui-image-editor-virtual-range-subbar'; this.pointer = document.createElement('div'); this.pointer.className = 'tui-image-editor-virtual-range-pointer'; this.bar.appendChild(this.subbar); this.bar.appendChild(this.pointer); this.rangeElement.appendChild(this.bar); } /** * Add range input editing event * @private */ _addInputEvent() { if (this.rangeInputElement) { this.rangeInputElement.addEventListener('keydown', this.eventHandler.changeInputWithArrow); this.rangeInputElement.addEventListener('keydown', this.eventHandler.changeInput); this.rangeInputElement.addEventListener('blur', this.eventHandler.changeInputFinally); } } /** * Remove range input editing event * @private */ _removeInputEvent() { if (this.rangeInputElement) { this.rangeInputElement.removeEventListener('keydown', this.eventHandler.changeInputWithArrow); this.rangeInputElement.removeEventListener('keydown', this.eventHandler.changeInput); this.rangeInputElement.removeEventListener('blur', this.eventHandler.changeInputFinally); } } /** * change angle event * @param {object} event - key event * @private */ _changeValueWithInputKeyEvent(event) { const { keyCode, target } = event; if ([keyCodes.ARROW_UP, keyCodes.ARROW_DOWN].indexOf(keyCode) < 0) { return; } let value = Number(target.value); value = this._valueUpDownForKeyEvent(value, keyCode); const unChanged = value < this._min || value > this._max; if (!unChanged) { const clampValue = clamp(value, this._min, this.max); this.value = clampValue; this.fire('change', clampValue, false); } } /** * value up down for input * @param {number} value - original value number * @param {number} keyCode - input event key code * @returns {number} value - changed value * @private */ _valueUpDownForKeyEvent(value, keyCode) { const step = this._useDecimal ? 0.1 : 1; if (keyCode === keyCodes.ARROW_UP) { value += step; } else if (keyCode === keyCodes.ARROW_DOWN) { value -= step; } return value; } _changeInput(event) { clearTimeout(this._userInputTimer); const { keyCode } = event; if (keyCode < keyCodes.DIGIT_0 || keyCode > keyCodes.DIGIT_9) { event.preventDefault(); return; } this._userInputTimer = setTimeout(() => { this._inputSetValue(event.target.value); }, 350); } _inputSetValue(stringValue) { let value = this._useDecimal ? Number(stringValue) : toInteger(stringValue); value = clamp(value, this._min, this.max); this.value = value; this.fire('change', value, true); } /** * change angle event * @param {boolean} isLast - Is last change * @param {object} event - key event * @private */ _changeValueWithInput(isLast, event) { const { keyCode, target } = event; if ([keyCodes.ARROW_UP, keyCodes.ARROW_DOWN].indexOf(keyCode) >= 0) { return; } const stringValue = this._filterForInputText(target.value); const waitForChange = !stringValue || isNaN(stringValue); target.value = stringValue; if (!waitForChange) { this._inputSetValue(stringValue); } } /** * Add Range click event * @private */ _addClickEvent() { this.rangeElement.addEventListener('click', this.eventHandler.changeSlideFinally); } /** * Remove Range click event * @private */ _removeClickEvent() { this.rangeElement.removeEventListener('click', this.eventHandler.changeSlideFinally); } /** * Add Range drag event * @private */ _addDragEvent() { this.pointer.addEventListener('mousedown', this.eventHandler.startChangingSlide); } /** * Remove Range drag event * @private */ _removeDragEvent() { this.pointer.removeEventListener('mousedown', this.eventHandler.startChangingSlide); } /** * change angle event * @param {object} event - change event * @private */ _changeSlide(event) { const changePosition = event.screenX; const diffPosition = changePosition - this.firstPosition; let touchPx = this.firstLeft + diffPosition; touchPx = touchPx > this.rangeWidth ? this.rangeWidth : touchPx; touchPx = touchPx < 0 ? 0 : touchPx; this.pointer.style.left = `${touchPx}px`; this.subbar.style.right = `${this.rangeWidth - touchPx}px`; const ratio = touchPx / this.rangeWidth; const resultValue = this._absMax * ratio + this._min; const value = this._useDecimal ? resultValue : toInteger(resultValue); const isValueChanged = this.value !== value; if (isValueChanged) { this.value = value; if (this.realTimeEvent) { this.fire('change', this._value, false); } } } _changeSlideFinally(event) { event.stopPropagation(); if (event.target.className !== 'tui-image-editor-range') { return; } const touchPx = event.offsetX; const ratio = touchPx / this.rangeWidth; const value = this._absMax * ratio + this._min; this.pointer.style.left = `${ratio * this.rangeWidth}px`; this.subbar.style.right = `${(1 - ratio) * this.rangeWidth}px`; this.value = value; this.fire('change', value, true); } _startChangingSlide(event) { this.firstPosition = event.screenX; this.firstLeft = toInteger(this.pointer.style.left) || 0; document.addEventListener('mousemove', this.eventHandler.changeSlide); document.addEventListener('mouseup', this.eventHandler.stopChangingSlide); } /** * stop change angle event * @private */ _stopChangingSlide() { this.fire('change', this._value, true); document.removeEventListener('mousemove', this.eventHandler.changeSlide); document.removeEventListener('mouseup', this.eventHandler.stopChangingSlide); } /** * Unnecessary string filtering. * @param {string} inputValue - origin string of input * @returns {string} filtered string * @private */ _filterForInputText(inputValue) { return inputValue.replace(INPUT_FILTER_REGEXP, '$1$2$3'); } } CustomEvents.mixin(Range); export default Range; ================================================ FILE: apps/image-editor/src/js/ui.js ================================================ import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; import extend from 'tui-code-snippet/object/extend'; import forEach from 'tui-code-snippet/collection/forEach'; import { getSelector, assignmentForDestroy, cls, getHistoryTitle, isSilentCommand } from '@/util'; import { ZOOM_HELP_MENUS, COMMAND_HELP_MENUS, DELETE_HELP_MENUS, eventNames, HELP_MENUS, } from '@/consts'; import mainContainer from '@/ui/template/mainContainer'; import controls from '@/ui/template/controls'; import Theme from '@/ui/theme/theme'; import Shape from '@/ui/shape'; import Crop from '@/ui/crop'; import Resize from '@/ui/resize'; import Flip from '@/ui/flip'; import Rotate from '@/ui/rotate'; import Text from '@/ui/text'; import Mask from '@/ui/mask'; import Icon from '@/ui/icon'; import Draw from '@/ui/draw'; import Filter from '@/ui/filter'; import History from '@/ui/history'; import Locale from '@/ui/locale/locale'; const SUB_UI_COMPONENT = { Shape, Crop, Resize, Flip, Rotate, Text, Mask, Icon, Draw, Filter, }; const BI_EXPRESSION_MINSIZE_WHEN_TOP_POSITION = '1300'; const HISTORY_MENU = 'history'; const HISTORY_PANEL_CLASS_NAME = 'tie-panel-history'; const CLASS_NAME_ON = 'on'; const ZOOM_BUTTON_TYPE = { ZOOM_IN: 'zoomIn', HAND: 'hand', }; /** * Ui class * @class * @param {string|HTMLElement} element - Wrapper's element or selector * @param {Object} [options] - Ui setting options * @param {number} options.loadImage - Init default load image * @param {number} options.initMenu - Init start menu * @param {Boolean} [options.menuBarPosition=bottom] - Let * @param {Boolean} [options.applyCropSelectionStyle=false] - Let * @param {Boolean} [options.usageStatistics=false] - Use statistics or not * @param {Object} [options.uiSize] - ui size of editor * @param {string} options.uiSize.width - width of ui * @param {string} options.uiSize.height - height of ui * @param {Object} actions - ui action instance */ class Ui { constructor(element, options, actions) { this.options = this._initializeOption(options); this._actions = actions; this.submenu = false; this.imageSize = {}; this.uiSize = {}; this._locale = new Locale(this.options.locale); this.theme = new Theme(this.options.theme); this.eventHandler = {}; this._submenuChangeTransection = false; this._selectedElement = null; this._mainElement = null; this._editorElementWrap = null; this._editorElement = null; this._menuBarElement = null; this._subMenuElement = null; this._makeUiElement(element); this._setUiSize(); this._initMenuEvent = false; this._makeSubMenu(); this._attachHistoryEvent(); this._attachZoomEvent(); } /** * Destroys the instance. */ destroy() { this._removeUiEvent(); this._destroyAllMenu(); this._selectedElement.innerHTML = ''; assignmentForDestroy(this); } /** * Set Default Selection for includeUI * @param {Object} option - imageEditor options * @returns {Object} - extends selectionStyle option * @ignore */ setUiDefaultSelectionStyle(option) { return extend( { applyCropSelectionStyle: true, applyGroupSelectionStyle: true, selectionStyle: { cornerStyle: 'circle', cornerSize: 16, cornerColor: '#fff', cornerStrokeColor: '#fff', transparentCorners: false, lineWidth: 2, borderColor: '#fff', }, }, option ); } /** * Change editor size * @param {Object} resizeInfo - ui & image size info * @param {Object} [resizeInfo.uiSize] - image size dimension * @param {string} resizeInfo.uiSize.width - ui width * @param {string} resizeInfo.uiSize.height - ui height * @param {Object} [resizeInfo.imageSize] - image size dimension * @param {Number} resizeInfo.imageSize.oldWidth - old width * @param {Number} resizeInfo.imageSize.oldHeight - old height * @param {Number} resizeInfo.imageSize.newWidth - new width * @param {Number} resizeInfo.imageSize.newHeight - new height * @example * // Change the image size and ui size, and change the affected ui state together. * imageEditor.ui.resizeEditor({ * imageSize: {oldWidth: 100, oldHeight: 100, newWidth: 700, newHeight: 700}, * uiSize: {width: 1000, height: 1000} * }); * @example * // Apply the ui state while preserving the previous attribute (for example, if responsive Ui) * imageEditor.ui.resizeEditor(); */ resizeEditor({ uiSize, imageSize = this.imageSize } = {}) { if (imageSize !== this.imageSize) { this.imageSize = imageSize; } if (uiSize) { this._setUiSize(uiSize); } const { width, height } = this._getCanvasMaxDimension(); const editorElementStyle = this._editorElement.style; const { menuBarPosition } = this.options; editorElementStyle.height = `${height}px`; editorElementStyle.width = `${width}px`; this._setEditorPosition(menuBarPosition); this._editorElementWrap.style.bottom = `0px`; this._editorElementWrap.style.top = `0px`; this._editorElementWrap.style.left = `0px`; this._editorElementWrap.style.width = `100%`; const selectElementClassList = this._selectedElement.classList; if ( menuBarPosition === 'top' && this._selectedElement.offsetWidth < BI_EXPRESSION_MINSIZE_WHEN_TOP_POSITION ) { selectElementClassList.add('tui-image-editor-top-optimization'); } else { selectElementClassList.remove('tui-image-editor-top-optimization'); } } /** * Toggle zoom button status * @param {string} type - type of zoom button */ toggleZoomButtonStatus(type) { const targetClassList = this._buttonElements[type].classList; targetClassList.toggle(CLASS_NAME_ON); if (type === ZOOM_BUTTON_TYPE.ZOOM_IN) { this._buttonElements[ZOOM_BUTTON_TYPE.HAND].classList.remove(CLASS_NAME_ON); } else { this._buttonElements[ZOOM_BUTTON_TYPE.ZOOM_IN].classList.remove(CLASS_NAME_ON); } } /** * Turn off zoom-in button status */ offZoomInButtonStatus() { const zoomInClassList = this._buttonElements[ZOOM_BUTTON_TYPE.ZOOM_IN].classList; zoomInClassList.remove(CLASS_NAME_ON); } /** * Change hand button status * @param {boolean} enabled - status to change */ changeHandButtonStatus(enabled) { const handClassList = this._buttonElements[ZOOM_BUTTON_TYPE.HAND].classList; handClassList[enabled ? 'add' : 'remove'](CLASS_NAME_ON); } /** * Change help button status * @param {string} buttonType - target button type * @param {Boolean} enableStatus - enabled status * @ignore */ changeHelpButtonEnabled(buttonType, enableStatus) { const buttonClassList = this._buttonElements[buttonType].classList; buttonClassList[enableStatus ? 'add' : 'remove']('enabled'); } /** * Change delete button status * @param {Object} [options] - Ui setting options * @param {object} [options.loadImage] - Init default load image * @param {string} [options.initMenu] - Init start menu * @param {string} [options.menuBarPosition=bottom] - Let * @param {boolean} [options.applyCropSelectionStyle=false] - Let * @param {boolean} [options.usageStatistics=false] - Send statistics ping or not * @returns {Object} initialize option * @private */ _initializeOption(options) { return extend( { loadImage: { path: '', name: '', }, locale: {}, menuIconPath: '', menu: [ 'resize', 'crop', 'flip', 'rotate', 'draw', 'shape', 'icon', 'text', 'mask', 'filter', ], initMenu: '', uiSize: { width: '100%', height: '100%', }, menuBarPosition: 'bottom', }, options ); } /** * Set ui container size * @param {Object} uiSize - ui dimension * @param {string} uiSize.width - css width property * @param {string} uiSize.height - css height property * @private */ _setUiSize(uiSize = this.options.uiSize) { const elementDimension = this._selectedElement.style; elementDimension.width = uiSize.width; elementDimension.height = uiSize.height; } /** * Make submenu dom element * @private */ _makeSubMenu() { forEach(this.options.menu, (menuName) => { const SubComponentClass = SUB_UI_COMPONENT[menuName.replace(/^[a-z]/, ($0) => $0.toUpperCase())]; // make menu element this._makeMenuElement(menuName); // menu btn element this._buttonElements[menuName] = this._menuBarElement.querySelector(`.tie-btn-${menuName}`); // submenu ui instance this[menuName] = new SubComponentClass(this._subMenuElement, { locale: this._locale, makeSvgIcon: this.theme.makeMenSvgIconSet.bind(this.theme), menuBarPosition: this.options.menuBarPosition, usageStatistics: this.options.usageStatistics, }); }); } /** * Attach history event * @private */ _attachHistoryEvent() { this.on(eventNames.EXECUTE_COMMAND, this._addHistory.bind(this)); this.on(eventNames.AFTER_UNDO, this._selectPrevHistory.bind(this)); this.on(eventNames.AFTER_REDO, this._selectNextHistory.bind(this)); } /** * Attach zoom event * @private */ _attachZoomEvent() { this.on(eventNames.HAND_STARTED, () => { this.offZoomInButtonStatus(); this.changeHandButtonStatus(true); }); this.on(eventNames.HAND_STOPPED, () => this.changeHandButtonStatus(false)); } /** * Make primary ui dom element * @param {string|HTMLElement} element - Wrapper's element or selector * @private */ _makeUiElement(element) { let selectedElement; if (element.nodeType) { selectedElement = element; } else { selectedElement = document.querySelector(element); } const selector = getSelector(selectedElement); selectedElement.classList.add('tui-image-editor-container'); selectedElement.innerHTML = controls({ locale: this._locale, biImage: this.theme.getStyle('common.bi'), loadButtonStyle: this.theme.getStyle('loadButton'), downloadButtonStyle: this.theme.getStyle('downloadButton'), menuBarPosition: this.options.menuBarPosition, }) + mainContainer({ locale: this._locale, biImage: this.theme.getStyle('common.bi'), commonStyle: this.theme.getStyle('common'), headerStyle: this.theme.getStyle('header'), loadButtonStyle: this.theme.getStyle('loadButton'), downloadButtonStyle: this.theme.getStyle('downloadButton'), submenuStyle: this.theme.getStyle('submenu'), }); this._selectedElement = selectedElement; this._selectedElement.classList.add(this.options.menuBarPosition); this._mainElement = selector('.tui-image-editor-main'); this._editorElementWrap = selector('.tui-image-editor-wrap'); this._editorElement = selector('.tui-image-editor'); this._helpMenuBarElement = selector('.tui-image-editor-help-menu'); this._menuBarElement = selector('.tui-image-editor-menu'); this._subMenuElement = selector('.tui-image-editor-submenu'); this._buttonElements = { download: this._selectedElement.querySelectorAll('.tui-image-editor-download-btn'), load: this._selectedElement.querySelectorAll('.tui-image-editor-load-btn'), }; this._addHelpMenus(); this._historyMenu = new History(this._buttonElements[HISTORY_MENU], { locale: this._locale, makeSvgIcon: this.theme.makeMenSvgIconSet.bind(this.theme), }); this._activateZoomMenus(); } /** * Activate help menus for zoom. * @private */ _activateZoomMenus() { forEach(ZOOM_HELP_MENUS, (menu) => { this.changeHelpButtonEnabled(menu, true); }); } /** * make array for help menu output, including partitions. * @returns {Array} * @private */ _makeHelpMenuWithPartition() { return [...ZOOM_HELP_MENUS, '', ...COMMAND_HELP_MENUS, '', ...DELETE_HELP_MENUS]; } /** * Add help menu * @private */ _addHelpMenus() { const helpMenuWithPartition = this._makeHelpMenuWithPartition(); forEach(helpMenuWithPartition, (menuName) => { if (!menuName) { this._makeMenuPartitionElement(); } else { this._makeMenuElement(menuName, ['normal', 'disabled', 'hover'], 'help'); this._buttonElements[menuName] = this._helpMenuBarElement.querySelector( `.tie-btn-${menuName}` ); } }); } /** * Make menu partition element * @private */ _makeMenuPartitionElement() { const partitionElement = document.createElement('li'); const partitionInnerElement = document.createElement('div'); partitionElement.className = cls('item'); partitionInnerElement.className = cls('icpartition'); partitionElement.appendChild(partitionInnerElement); this._helpMenuBarElement.appendChild(partitionElement); } /** * Make menu button element * @param {string} menuName - menu name * @param {Array} useIconTypes - Possible values are \['normal', 'active', 'hover', 'disabled'\] * @param {string} menuType - 'normal' or 'help' * @private */ _makeMenuElement(menuName, useIconTypes = ['normal', 'active', 'hover'], menuType = 'normal') { const btnElement = document.createElement('li'); const menuItemHtml = this.theme.makeMenSvgIconSet(useIconTypes, menuName); this._addTooltipAttribute(btnElement, menuName); btnElement.className = `tie-btn-${menuName} ${cls('item')} ${menuType}`; btnElement.innerHTML = menuItemHtml; if (menuType === 'normal') { this._menuBarElement.appendChild(btnElement); } else { this._helpMenuBarElement.appendChild(btnElement); } } /** * Add help action event * @private */ _addHelpActionEvent() { forEach(HELP_MENUS, (helpName) => { this.eventHandler[helpName] = (event) => this._actions.main[helpName](event); this._buttonElements[helpName].addEventListener('click', this.eventHandler[helpName]); }); } /** * Remove help action event * @private */ _removeHelpActionEvent() { forEach(HELP_MENUS, (helpName) => { this._buttonElements[helpName].removeEventListener('click', this.eventHandler[helpName]); }); } /** * Add history * @param {Command|string} command - command or command name */ _addHistory(command) { if (!isSilentCommand(command)) { const historyTitle = typeof command === 'string' ? { name: command } : getHistoryTitle(command); this._historyMenu.add(historyTitle); } } /** * Init history */ initHistory() { this._historyMenu.init(); } /** * Clear history */ clearHistory() { this._historyMenu.clear(); } /** * Select prev history */ _selectPrevHistory() { this._historyMenu.prev(); } /** * Select next history */ _selectNextHistory() { this._historyMenu.next(); } /** * Toggle history menu * @param {object} event - event object */ toggleHistoryMenu(event) { const { target } = event; const item = target.closest(`.${HISTORY_PANEL_CLASS_NAME}`); if (item) { return; } const historyButtonClassList = this._buttonElements[HISTORY_MENU].classList; historyButtonClassList.toggle('opened'); } /** * Add attribute for menu tooltip * @param {HTMLElement} element - menu element * @param {string} tooltipName - tooltipName * @private */ _addTooltipAttribute(element, tooltipName) { element.setAttribute( 'tooltip-content', this._locale.localize(tooltipName.replace(/^[a-z]/g, ($0) => $0.toUpperCase())) ); } /** * Add download event * @private */ _addDownloadEvent() { this.eventHandler.download = () => this._actions.main.download(); forEach(this._buttonElements.download, (element) => { element.addEventListener('click', this.eventHandler.download); }); } _removeDownloadEvent() { forEach(this._buttonElements.download, (element) => { element.removeEventListener('click', this.eventHandler.download); }); } /** * Add load event * @private */ _addLoadEvent() { this.eventHandler.loadImage = (event) => this._actions.main.load(event.target.files[0]); forEach(this._buttonElements.load, (element) => { element.addEventListener('change', this.eventHandler.loadImage); }); } /** * Remove load event * @private */ _removeLoadEvent() { forEach(this._buttonElements.load, (element) => { element.removeEventListener('change', this.eventHandler.loadImage); }); } /** * Add menu event * @param {string} menuName - menu name * @private */ _addMainMenuEvent(menuName) { this.eventHandler[menuName] = () => this.changeMenu(menuName); this._buttonElements[menuName].addEventListener('click', this.eventHandler[menuName]); } /** * Add menu event * @param {string} menuName - menu name * @private */ _addSubMenuEvent(menuName) { this[menuName].addEvent(this._actions[menuName]); this[menuName].on(eventNames.INPUT_BOX_EDITING_STARTED, () => this.fire(eventNames.INPUT_BOX_EDITING_STARTED) ); this[menuName].on(eventNames.INPUT_BOX_EDITING_STOPPED, () => this.fire(eventNames.INPUT_BOX_EDITING_STOPPED) ); } /** * Add menu event * @private */ _addMenuEvent() { forEach(this.options.menu, (menuName) => { this._addMainMenuEvent(menuName); this._addSubMenuEvent(menuName); }); } /** * Remove menu event * @private */ _removeMainMenuEvent() { forEach(this.options.menu, (menuName) => { this._buttonElements[menuName].removeEventListener('click', this.eventHandler[menuName]); this[menuName].off(eventNames.INPUT_BOX_EDITING_STARTED); this[menuName].off(eventNames.INPUT_BOX_EDITING_STOPPED); }); } /** * Get editor area element * @returns {HTMLElement} editor area html element * @ignore */ getEditorArea() { return this._editorElement; } /** * Add event for menu items * @ignore */ activeMenuEvent() { if (this._initMenuEvent) { return; } this._addHelpActionEvent(); this._addDownloadEvent(); this._addMenuEvent(); this._initMenu(); this._historyMenu.addEvent(this._actions.history); this._initMenuEvent = true; } /** * Remove ui event * @private */ _removeUiEvent() { this._removeHelpActionEvent(); this._removeDownloadEvent(); this._removeLoadEvent(); this._removeMainMenuEvent(); this._historyMenu.removeEvent(); } /** * Destroy all menu instance * @private */ _destroyAllMenu() { forEach(this.options.menu, (menuName) => { this[menuName].destroy(); }); this._historyMenu.destroy(); } /** * Init canvas * @ignore */ initCanvas() { const loadImageInfo = this._getLoadImage(); if (loadImageInfo.path) { this._actions.main.initLoadImage(loadImageInfo.path, loadImageInfo.name).then(() => { this.activeMenuEvent(); }); } this._addLoadEvent(); const gridVisual = document.createElement('div'); gridVisual.className = cls('grid-visual'); const grid = `
      `; gridVisual.innerHTML = grid; this._editorContainerElement = this._editorElement.querySelector( '.tui-image-editor-canvas-container' ); this._editorContainerElement.appendChild(gridVisual); } /** * get editor area element * @returns {Object} load image option * @private */ _getLoadImage() { return this.options.loadImage; } /** * change menu * @param {string} menuName - menu name * @param {boolean} toggle - whether toogle or not * @param {boolean} discardSelection - discard selection * @ignore */ changeMenu(menuName, toggle = true, discardSelection = true) { if (!this._submenuChangeTransection) { this._submenuChangeTransection = true; this._changeMenu(menuName, toggle, discardSelection); this._submenuChangeTransection = false; } } /** * change menu * @param {string} menuName - menu name * @param {boolean} toggle - whether toggle or not * @param {boolean} discardSelection - discard selection * @private */ _changeMenu(menuName, toggle, discardSelection) { if (this.submenu) { this._buttonElements[this.submenu].classList.remove('active'); this._mainElement.classList.remove(`tui-image-editor-menu-${this.submenu}`); if (discardSelection) { this._actions.main.discardSelection(); } this._actions.main.changeSelectableAll(true); this[this.submenu].changeStandbyMode(); } if (this.submenu === menuName && toggle) { this.submenu = null; } else { this._buttonElements[menuName].classList.add('active'); this._mainElement.classList.add(`tui-image-editor-menu-${menuName}`); this.submenu = menuName; this[this.submenu].changeStartMode(); } this.resizeEditor(); } /** * Init menu * @private */ _initMenu() { if (this.options.initMenu) { const evt = document.createEvent('MouseEvents'); evt.initEvent('click', true, false); this._buttonElements[this.options.initMenu].dispatchEvent(evt); } if (this.icon) { this.icon.registerDefaultIcon(); } } /** * Get canvas max Dimension * @returns {Object} - width & height of editor * @private */ _getCanvasMaxDimension() { const { maxWidth, maxHeight } = this._editorContainerElement.style; const width = parseFloat(maxWidth); const height = parseFloat(maxHeight); return { width, height, }; } /** * Set editor position * @param {string} menuBarPosition - top or right or bottom or left * @private */ // eslint-disable-next-line complexity _setEditorPosition(menuBarPosition) { const { width, height } = this._getCanvasMaxDimension(); const editorElementStyle = this._editorElement.style; let top = 0; let left = 0; if (this.submenu) { if (menuBarPosition === 'bottom') { if (height > this._editorElementWrap.scrollHeight - 150) { top = (height - this._editorElementWrap.scrollHeight) / 2; } else { top = (150 / 2) * -1; } } else if (menuBarPosition === 'top') { if (height > this._editorElementWrap.offsetHeight - 150) { top = 150 / 2 - (height - (this._editorElementWrap.offsetHeight - 150)) / 2; } else { top = 150 / 2; } } else if (menuBarPosition === 'left') { if (width > this._editorElementWrap.offsetWidth - 248) { left = 248 / 2 - (width - (this._editorElementWrap.offsetWidth - 248)) / 2; } else { left = 248 / 2; } } else if (menuBarPosition === 'right') { if (width > this._editorElementWrap.scrollWidth - 248) { left = (width - this._editorElementWrap.scrollWidth) / 2; } else { left = (248 / 2) * -1; } } } editorElementStyle.top = `${top}px`; editorElementStyle.left = `${left}px`; } } CustomEvents.mixin(Ui); export default Ui; ================================================ FILE: apps/image-editor/src/js/util.js ================================================ import isUndefined from 'tui-code-snippet/type/isUndefined'; import forEach from 'tui-code-snippet/collection/forEach'; import sendHostname from 'tui-code-snippet/request/sendHostname'; import extend from 'tui-code-snippet/object/extend'; import isString from 'tui-code-snippet/type/isString'; import pick from 'tui-code-snippet/object/pick'; import inArray from 'tui-code-snippet/array/inArray'; import { commandNames, filterType, historyNames, SHAPE_FILL_TYPE, SHAPE_TYPE, emptyCropRectValues, } from '@/consts'; const FLOATING_POINT_DIGIT = 2; const CSS_PREFIX = 'tui-image-editor-'; const { min, max } = Math; let hostnameSent = false; let lastId = 0; export function stamp(obj) { if (!obj.__fe_id) { lastId += 1; // eslint-disable-next-line camelcase obj.__fe_id = lastId; } return obj.__fe_id; } export function hasStamp(obj) { return !isNil(obj?.__fe_id); } export function isNil(value) { return isUndefined(value) || value === null; } export function isFunction(value) { return typeof value === 'function'; } /** * Clamp value * @param {number} value - Value * @param {number} minValue - Minimum value * @param {number} maxValue - Maximum value * @returns {number} clamped value */ export function clamp(value, minValue, maxValue) { if (minValue > maxValue) { [minValue, maxValue] = [maxValue, minValue]; } return max(minValue, min(value, maxValue)); } /** * Make key-value object from arguments * @returns {object.} */ export function keyMirror(...args) { const obj = {}; forEach(args, (key) => { obj[key] = key; }); return obj; } /** * Make CSSText * @param {Object} styleObj - Style info object * @returns {string} Connected string of style */ export function makeStyleText(styleObj) { let styleStr = ''; forEach(styleObj, (value, prop) => { styleStr += `${prop}: ${value};`; }); return styleStr; } /** * Get object's properties * @param {Object} obj - object * @param {Array} keys - keys * @returns {Object} properties object */ export function getProperties(obj, keys) { const props = {}; const { length } = keys; let i = 0; let key; for (i = 0; i < length; i += 1) { key = keys[i]; props[key] = obj[key]; } return props; } /** * ParseInt simpliment * @param {number} value - Value * @returns {number} */ export function toInteger(value) { return parseInt(value, 10); } /** * String to camelcase string * @param {string} targetString - change target * @returns {string} * @private */ export function toCamelCase(targetString) { return targetString.replace(/-([a-z])/g, ($0, $1) => $1.toUpperCase()); } /** * Check browser file api support * @returns {boolean} * @private */ export function isSupportFileApi() { return !!(window.File && window.FileList && window.FileReader); } /** * hex to rgb * @param {string} color - hex color * @param {string} alpha - color alpha value * @returns {string} rgb expression */ export function getRgb(color, alpha) { if (color.length === 4) { color = `${color}${color.slice(1, 4)}`; } const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); const a = alpha || 1; return `rgba(${r}, ${g}, ${b}, ${a})`; } /** * send hostname */ export function sendHostName() { if (hostnameSent) { return; } hostnameSent = true; sendHostname('image-editor', 'UA-129999381-1'); } /** * Apply css resource * @param {string} styleBuffer - serialized css text * @param {string} tagId - style tag id */ export function styleLoad(styleBuffer, tagId) { const [head] = document.getElementsByTagName('head'); const linkElement = document.createElement('link'); const styleData = encodeURIComponent(styleBuffer); if (tagId) { linkElement.id = tagId; // linkElement.id = 'tui-image-editor-theme-style'; } linkElement.setAttribute('rel', 'stylesheet'); linkElement.setAttribute('type', 'text/css'); linkElement.setAttribute('href', `data:text/css;charset=UTF-8,${styleData}`); head.appendChild(linkElement); } /** * Get selector * @param {HTMLElement} targetElement - target element * @returns {Function} selector */ export function getSelector(targetElement) { return (str) => targetElement.querySelector(str); } /** * Change base64 to blob * @param {String} data - base64 string data * @returns {Blob} Blob Data */ export function base64ToBlob(data) { const rImageType = /data:(image\/.+);base64,/; let mimeString = ''; let raw, uInt8Array, i; raw = data.replace(rImageType, (header, imageType) => { mimeString = imageType; return ''; }); raw = atob(raw); const rawLength = raw.length; uInt8Array = new Uint8Array(rawLength); // eslint-disable-line for (i = 0; i < rawLength; i += 1) { uInt8Array[i] = raw.charCodeAt(i); } return new Blob([uInt8Array], { type: mimeString }); } /** * Fix floating point diff. * @param {number} value - original value * @returns {number} fixed value */ export function fixFloatingPoint(value) { return Number(value.toFixed(FLOATING_POINT_DIGIT)); } /** * Assignment for destroying objects. * @param {Object} targetObject - object to be removed. */ export function assignmentForDestroy(targetObject) { forEach(targetObject, (value, key) => { targetObject[key] = null; }); } /** * Make class name for ui * @param {String} str - main string of className * @param {String} prefix - prefix string of className * @returns {String} class name */ export function cls(str = '', prefix = '') { if (str.charAt(0) === '.') { return `.${CSS_PREFIX}${prefix}${str.slice(1)}`; } return `${CSS_PREFIX}${prefix}${str}`; } /** * Change object origin * @param {fabric.Object} fObject - fabric object * @param {Object} origin - origin of fabric object * @param {string} originX - horizontal basis. * @param {string} originY - vertical basis. */ export function changeOrigin(fObject, origin) { const { originX, originY } = origin; const { x: left, y: top } = fObject.getPointByOrigin(originX, originY); fObject.set({ left, top, originX, originY, }); fObject.setCoords(); } /** * Object key value flip * @param {Object} targetObject - The data object of the key value. * @returns {Object} */ export function flipObject(targetObject) { const result = {}; Object.keys(targetObject).forEach((key) => { result[targetObject[key]] = key; }); return result; } /** * Set custom properties * @param {Object} targetObject - target object * @param {Object} props - custom props object */ export function setCustomProperty(targetObject, props) { targetObject.customProps = targetObject.customProps || {}; extend(targetObject.customProps, props); } /** * Get custom property * @param {fabric.Object} fObject - fabric object * @param {Array|string} propNames - prop name array * @returns {object | number | string} */ export function getCustomProperty(fObject, propNames) { const resultObject = {}; if (isString(propNames)) { propNames = [propNames]; } forEach(propNames, (propName) => { resultObject[propName] = fObject.customProps[propName]; }); return resultObject; } /** * Capitalize string * @param {string} targetString - target string * @returns {string} */ export function capitalizeString(targetString) { return targetString.charAt(0).toUpperCase() + targetString.slice(1); } /** * Array includes check * @param {Array} targetArray - target array * @param {string|number} compareValue - compare value * @returns {boolean} */ export function includes(targetArray, compareValue) { return targetArray.indexOf(compareValue) >= 0; } /** * Get fill type * @param {Object | string} fillOption - shape fill option * @returns {string} 'color' or 'filter' */ export function getFillTypeFromOption(fillOption = {}) { return pick(fillOption, 'type') || SHAPE_FILL_TYPE.COLOR; } /** * Get fill type of shape type object * @param {fabric.Object} shapeObj - fabric object * @returns {string} 'transparent' or 'color' or 'filter' */ export function getFillTypeFromObject(shapeObj) { const { fill = {} } = shapeObj; if (fill.source) { return SHAPE_FILL_TYPE.FILTER; } return SHAPE_FILL_TYPE.COLOR; } /** * Check if the object is a shape object. * @param {fabric.Object} obj - fabric object * @returns {boolean} */ export function isShape(obj) { return inArray(obj.get('type'), SHAPE_TYPE) >= 0; } /** * Get object type * @param {string} type - fabric object type * @returns {string} type of object (ex: shape, icon, ...) */ export function getObjectType(type) { if (includes(SHAPE_TYPE, type)) { return 'Shape'; } switch (type) { case 'i-text': return 'Text'; case 'path': case 'line': return 'Draw'; case 'activeSelection': return 'Group'; default: return toStartOfCapital(type); } } /** * Get filter type * @param {string} type - fabric filter type * @param {object} [options] - filter type options * @param {boolean} [options.useAlpha=true] - usage of alpha(true is 'color filter', false is 'remove white') * @param {string} [options.mode] - mode of blendColor * @returns {string} type of filter (ex: sepia, blur, ...) */ function getFilterType(type, { useAlpha = true, mode } = {}) { const { VINTAGE, REMOVE_COLOR, BLEND_COLOR, SEPIA2, COLOR_FILTER, REMOVE_WHITE, BLEND } = filterType; let filterName; switch (type) { case VINTAGE: filterName = SEPIA2; break; case REMOVE_COLOR: filterName = useAlpha ? COLOR_FILTER : REMOVE_WHITE; break; case BLEND_COLOR: filterName = mode === 'add' ? BLEND : mode; break; default: filterName = type; } return toStartOfCapital(filterName); } /** * Check if command is silent command * @param {Command|string} command - command or command name * @returns {boolean} */ export function isSilentCommand(command) { const { LOAD_IMAGE } = commandNames; return typeof command === 'string' ? LOAD_IMAGE === command : LOAD_IMAGE === command.name; } /** * Get command name * @param {Command|string} command - command or command name * @returns {{name: string, ?detail: string}} */ // eslint-disable-next-line complexity, require-jsdoc export function getHistoryTitle(command) { const { FLIP_IMAGE, ROTATE_IMAGE, ADD_TEXT, APPLY_FILTER, REMOVE_FILTER, CHANGE_SHAPE, CHANGE_ICON_COLOR, CHANGE_TEXT_STYLE, CLEAR_OBJECTS, ADD_IMAGE_OBJECT, REMOVE_OBJECT, RESIZE_IMAGE, } = commandNames; const { name, args } = command; let historyInfo; switch (name) { case FLIP_IMAGE: historyInfo = { name, detail: args[1] === 'reset' ? args[1] : args[1].slice(4) }; break; case ROTATE_IMAGE: historyInfo = { name, detail: args[2] }; break; case APPLY_FILTER: historyInfo = { name: historyNames.APPLY_FILTER, detail: getFilterType(args[1], args[2]) }; break; case REMOVE_FILTER: historyInfo = { name: historyNames.REMOVE_FILTER, detail: 'Remove' }; break; case CHANGE_SHAPE: historyInfo = { name: historyNames.CHANGE_SHAPE, detail: 'Change' }; break; case CHANGE_ICON_COLOR: historyInfo = { name: historyNames.CHANGE_ICON_COLOR, detail: 'Change' }; break; case CHANGE_TEXT_STYLE: historyInfo = { name: historyNames.CHANGE_TEXT_STYLE, detail: 'Change' }; break; case REMOVE_OBJECT: historyInfo = { name: historyNames.REMOVE_OBJECT, detail: args[2] }; break; case CLEAR_OBJECTS: historyInfo = { name: historyNames.CLEAR_OBJECTS, detail: 'All' }; break; case ADD_IMAGE_OBJECT: historyInfo = { name: historyNames.ADD_IMAGE_OBJECT, detail: 'Add' }; break; case ADD_TEXT: historyInfo = { name: historyNames.ADD_TEXT }; break; case RESIZE_IMAGE: historyInfo = { name: historyNames.RESIZE, detail: `${~~args[1].width}x${~~args[1].height}` }; break; default: historyInfo = { name }; break; } if (args[1] === 'mask') { historyInfo = { name: historyNames.LOAD_MASK_IMAGE, detail: 'Apply' }; } return historyInfo; } /** * Get help menubar position(opposite of menubar) * @param {string} position - position of menubar * @returns {string} position of help menubar */ export function getHelpMenuBarPosition(position) { if (position === 'top') { return 'bottom'; } if (position === 'left') { return 'right'; } if (position === 'right') { return 'left'; } return 'top'; } /** * Change to capital start letter * @param {string} str - string to change * @returns {string} */ function toStartOfCapital(str) { return str.replace(/[a-z]/, (first) => first.toUpperCase()); } /** * Check if cropRect is Empty. * @param {Object} cropRect - cropRect object * @param {Number} cropRect.left - cropRect left position value * @param {Number} cropRect.top - cropRect top position value * @param {Number} cropRect.width - cropRect width value * @param {Number} cropRect.height - cropRect height value * @returns {boolean} */ export function isEmptyCropzone(cropRect) { const { left, top, width, height } = cropRect; const { LEFT, TOP, WIDTH, HEIGHT } = emptyCropRectValues; return left === LEFT && top === TOP && width === WIDTH && height === HEIGHT; } ================================================ FILE: apps/image-editor/tests/__snapshots__/arrowLine.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ArrowLine should be a triangular shape that closes the path with closePath after drawing 1`] = ` Array [ 1.4399923766742853, 2.642073904691416, ] `; exports[`ArrowLine should be a triangular shape that closes the path with closePath after drawing 2`] = ` Array [ 1, 1, ] `; exports[`ArrowLine should be a triangular shape that closes the path with closePath after drawing 3`] = ` Array [ 2.642073904691416, 1.4399923766742853, ] `; exports[`ArrowLine should draw the "v" calculated according to the angle around the "head" of the line when attaching the "chevron" type to the start point 1`] = ` Array [ 1.698811421776806, 3.6079997309804845, ] `; exports[`ArrowLine should draw the "v" calculated according to the angle around the "head" of the line when attaching the "chevron" type to the start point 2`] = ` Array [ 1, 1, ] `; exports[`ArrowLine should draw the "v" calculated according to the angle around the "head" of the line when attaching the "chevron" type to the start point 3`] = ` Array [ 3.6079997309804845, 1.698811421776806, ] `; exports[`ArrowLine should draw the "v" calculated according to the angle around the "tail" of the line when attaching the "chevron" type to the end point 1`] = ` Array [ 9.301188578223194, 7.3920002690195155, ] `; exports[`ArrowLine should draw the "v" calculated according to the angle around the "tail" of the line when attaching the "chevron" type to the end point 2`] = ` Array [ 10, 10, ] `; exports[`ArrowLine should draw the "v" calculated according to the angle around the "tail" of the line when attaching the "chevron" type to the end point 3`] = ` Array [ 7.3920002690195155, 9.301188578223194, ] `; ================================================ FILE: apps/image-editor/tests/__snapshots__/shape.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Shape Fill - filter type should be the same size as the rectangle that draws the rotated object border 1`] = ` Object { "angle": -40, "backgroundColor": "", "cropX": -44.01372654341047, "cropY": -32.78115820908609, "crossOrigin": null, "fill": "rgb(0,0,0)", "fillRule": "nonzero", "filters": Array [ Object { "blocksize": 20, "type": "Pixelate", }, ], "flipX": false, "flipY": false, "globalCompositeOperation": "source-over", "height": 125.56, "left": 104.79, "opacity": 1, "originX": "center", "originY": "center", "paintFirst": "fill", "scaleX": 1, "scaleY": 1, "shadow": null, "skewX": 0, "skewY": 0, "src": "data:image/png;base64,00", "stroke": null, "strokeDashArray": null, "strokeDashOffset": 0, "strokeLineCap": "butt", "strokeLineJoin": "miter", "strokeMiterLimit": 4, "strokeUniform": false, "strokeWidth": 0, "top": 36.82, "type": "image", "version": "4.6.0", "visible": true, "width": 128.03, } `; exports[`Shape Fill - filter type should give the expected result for shapes that go outside the top left area of the canvas 1`] = ` Object { "angle": 0, "backgroundColor": "", "cropX": -50, "cropY": -5, "crossOrigin": null, "fill": "rgb(0,0,0)", "fillRule": "nonzero", "filters": Array [ Object { "blocksize": 20, "type": "Pixelate", }, ], "flipX": false, "flipY": false, "globalCompositeOperation": "source-over", "height": 70, "left": 150, "opacity": 1, "originX": "center", "originY": "center", "paintFirst": "fill", "scaleX": 1, "scaleY": 1, "shadow": null, "skewX": 0, "skewY": 0, "src": "data:image/png;base64,00", "stroke": null, "strokeDashArray": null, "strokeDashOffset": 0, "strokeLineCap": "butt", "strokeLineJoin": "miter", "strokeMiterLimit": 4, "strokeUniform": false, "strokeWidth": 0, "top": 40, "type": "image", "version": "4.6.0", "visible": true, "width": 200, } `; ================================================ FILE: apps/image-editor/tests/__snapshots__/text.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Text should maintain consistent left and top positions after entering and exiting drawing mode 1`] = ` Object { "angle": 40, "backgroundColor": "", "charSpacing": 0, "direction": "ltr", "fill": "rgb(0,0,0)", "fillRule": "nonzero", "flipX": false, "flipY": false, "fontFamily": "Times New Roman", "fontSize": 40, "fontStyle": "normal", "fontWeight": "normal", "globalCompositeOperation": "source-over", "height": 45.2, "left": 10, "lineHeight": 1.16, "linethrough": false, "opacity": 1, "originX": "center", "originY": "center", "overline": false, "paintFirst": "fill", "path": null, "pathSide": "left", "pathStartOffset": 0, "scaleX": 1, "scaleY": 1, "shadow": null, "skewX": 0, "skewY": 0, "stroke": null, "strokeDashArray": null, "strokeDashOffset": 0, "strokeLineCap": "butt", "strokeLineJoin": "miter", "strokeMiterLimit": 4, "strokeUniform": false, "strokeWidth": 1, "styles": Object {}, "text": "testString", "textAlign": "left", "textBackgroundColor": "", "top": 20, "type": "i-text", "underline": false, "version": "4.6.0", "visible": true, "width": 1, } `; ================================================ FILE: apps/image-editor/tests/__snapshots__/theme.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Theme _makeCssText() should return the cssText of the expected value for the object 1`] = `"background-color: #fff;background-image: url(./img/bg.png);border: 1px solid #ddd;color: #222;font-family: NotoSans, sans-serif;font-size: 12px"`; exports[`Theme _makeSvgItem() should create a svg path with the prefix when set the icon file 1`] = `""`; exports[`Theme _makeSvgItem() should create path prefix and use-default class when using the default icon 1`] = `""`; exports[`Theme getStyle() should return cssText if all members are objects 1`] = ` Object { "active": "color: #000;font-weight: normal", "normal": "color: #858585;font-weight: normal", } `; exports[`Theme getStyle() should return cssText in normal types 1`] = `"background-color: #fdba3b;border: 1px solid #fdba3b;color: #fff;font-family: NotoSans, sans-serif;font-size: 12px"`; ================================================ FILE: apps/image-editor/tests/action.spec.js ================================================ import { fabric } from 'fabric'; import ImageEditor from '@/imageEditor'; import '@/command/loadImage'; describe('UI', () => { let actions, imageEditorMock, mockImage; beforeEach(() => { imageEditorMock = new ImageEditor(document.createElement('div'), { includeUI: { loadImage: false, initMenu: 'flip', menuBarPosition: 'bottom', applyCropSelectionStyle: true, }, }); actions = imageEditorMock.getActions(); mockImage = new fabric.Image(); imageEditorMock._graphics.setCanvasImage('mockImage', mockImage); }); describe('mainAction', () => { let mainAction; beforeEach(() => { mainAction = actions.main; }); it('should be executed When the initLoadImage action occurs', async () => { const loadImageFromURLSpy = jest .spyOn(imageEditorMock, 'loadImageFromURL') .mockReturnValue(Promise.resolve(300)); const clearUndoStackSpy = jest.spyOn(imageEditorMock, 'clearUndoStack'); const resizeEditorSpy = jest.spyOn(imageEditorMock.ui, 'resizeEditor'); await mainAction.initLoadImage('path', 'imageName'); expect(loadImageFromURLSpy).toHaveBeenCalled(); expect(clearUndoStackSpy).toHaveBeenCalled(); expect(resizeEditorSpy).toHaveBeenCalled(); }); it('should be executed When the undo action occurs', () => { jest.spyOn(imageEditorMock, 'isEmptyUndoStack').mockReturnValue(false); const undoSpy = jest.spyOn(imageEditorMock, 'undo').mockReturnValue({ then: () => {} }); mainAction.undo(); expect(undoSpy).toHaveBeenCalled(); }); it('should be executed When the redo action occurs', () => { jest.spyOn(imageEditorMock, 'isEmptyRedoStack').mockReturnValue(false); const redoSpy = jest.spyOn(imageEditorMock, 'redo').mockReturnValue({ then: () => {} }); mainAction.redo(); expect(redoSpy).toHaveBeenCalled(); }); it('should be executed When the delete action occurs', () => { imageEditorMock.activeObjectId = 10; imageEditorMock.removeActiveObject = jest.fn(); mainAction.delete(); expect(imageEditorMock.removeActiveObject).toHaveBeenCalled(); expect(imageEditorMock.activeObjectId).toBeNull(); }); it('should be run and the enabled state should be changed When the deleteAll action occurs', () => { imageEditorMock.clearObjects = jest.fn(); const changeHelpButtonEnabledSpy = jest.spyOn(imageEditorMock.ui, 'changeHelpButtonEnabled'); mainAction.deleteAll(); expect(imageEditorMock.clearObjects).toHaveBeenCalled(); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(1, 'delete', false); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(2, 'deleteAll', false); }); it('should be executed When the load action occurs', async () => { const loadImageFromFileSpy = jest .spyOn(imageEditorMock, 'loadImageFromFile') .mockReturnValue(Promise.resolve()); const clearUndoStackSpy = jest.spyOn(imageEditorMock, 'clearUndoStack'); const resizeEditorSpy = jest.spyOn(imageEditorMock.ui, 'resizeEditor'); global.URL.createObjectURL = jest.fn(); await mainAction.load(); expect(loadImageFromFileSpy).toHaveBeenCalled(); expect(clearUndoStackSpy).toHaveBeenCalled(); expect(resizeEditorSpy).toHaveBeenCalled(); }); }); describe('shapeAction', () => { let shapeAction; beforeEach(() => { shapeAction = actions.shape; }); it('should be executed When the changeShape action occurs', () => { imageEditorMock.activeObjectId = 10; imageEditorMock.changeShape = jest.fn(); shapeAction.changeShape({ strokeWidth: '#000000' }); expect(imageEditorMock.changeShape).toHaveBeenCalledWith( 10, { strokeWidth: '#000000' }, undefined ); }); it('should be executed When the setDrawingShape action occurs', () => { imageEditorMock.setDrawingShape = jest.fn(); shapeAction.setDrawingShape(); expect(imageEditorMock.setDrawingShape).toHaveBeenCalled(); }); }); describe('cropAction', () => { let cropAction; beforeEach(() => { cropAction = actions.crop; }); it('should be executed when the crop action occurs', async () => { const getCropzoneRectSpy = jest .spyOn(imageEditorMock, 'getCropzoneRect') .mockReturnValue(true); const cropSpy = jest.spyOn(imageEditorMock, 'crop').mockReturnValue(Promise.resolve()); const stopDrawingModeSpy = jest.spyOn(imageEditorMock, 'stopDrawingMode'); imageEditorMock.ui.changeMenu = jest.fn(); await cropAction.crop(); expect(getCropzoneRectSpy).toHaveBeenCalled(); expect(cropSpy).toHaveBeenCalled(); expect(stopDrawingModeSpy).toHaveBeenCalled(); }); it('should be executed When the cancel action occurs', () => { const stopDrawingModeSpy = jest.spyOn(imageEditorMock, 'stopDrawingMode'); imageEditorMock.ui.changeMenu = jest.fn(); cropAction.cancel(); expect(stopDrawingModeSpy).toHaveBeenCalled(); expect(imageEditorMock.ui.changeMenu).toHaveBeenCalled(); }); }); describe('flipAction', () => { let flipAction; beforeEach(() => { flipAction = actions.flip; }); it('should be executed When the flip(flipType) action occurs', () => { imageEditorMock.flipX = jest.fn(); imageEditorMock.flipY = jest.fn(); flipAction.flip('flipX'); expect(imageEditorMock.flipX).toHaveBeenCalled(); flipAction.flip('flipY'); expect(imageEditorMock.flipY).toHaveBeenCalled(); }); }); describe('rotateAction', () => { let rotateAction; beforeEach(() => { rotateAction = actions.rotate; }); it('should be executed When the rotate action occurs', () => { const resizeEditorSpy = jest.spyOn(imageEditorMock.ui, 'resizeEditor'); imageEditorMock.rotate = jest.fn(); rotateAction.rotate(30); expect(imageEditorMock.rotate).toHaveBeenCalledWith(30, undefined); expect(resizeEditorSpy).toHaveBeenCalled(); }); it('should be executed When the setAngle action occurs', () => { const resizeEditorSpy = jest.spyOn(imageEditorMock.ui, 'resizeEditor'); imageEditorMock.setAngle = jest.fn(); rotateAction.setAngle(30); expect(imageEditorMock.setAngle).toHaveBeenCalledWith(30, undefined); expect(resizeEditorSpy).toHaveBeenCalled(); }); }); describe('textAction', () => { let textAction; beforeEach(() => { textAction = actions.text; }); it('should be executed When the changeTextStyle action occurs', () => { imageEditorMock.activeObjectId = 10; imageEditorMock.changeTextStyle = jest.fn(); textAction.changeTextStyle({ fontSize: 10 }); expect(imageEditorMock.changeTextStyle).toHaveBeenCalledWith(10, { fontSize: 10 }, undefined); }); }); describe('maskAction', () => { let maskAction; beforeEach(() => { maskAction = actions.mask; }); it('should be executed When the applyFilter action occurs', () => { imageEditorMock.activeObjectId = 10; imageEditorMock.applyFilter = jest.fn(); jest.spyOn(imageEditorMock, 'applyFilter'); maskAction.applyFilter(); expect(imageEditorMock.applyFilter).toHaveBeenCalledWith('mask', { maskObjId: 10 }); }); }); describe('drawAction', () => { let drawAction; beforeEach(() => { drawAction = actions.draw; }); it('should be executed When the setDrawMode("free") action occurs', () => { imageEditorMock.startDrawingMode = jest.fn(); drawAction.setDrawMode('free'); expect(imageEditorMock.startDrawingMode).toHaveBeenCalledWith('FREE_DRAWING', undefined); }); it('should be executed When the setColor() action occurs', () => { imageEditorMock.setBrush = jest.fn(); drawAction.setColor('#000000'); expect(imageEditorMock.setBrush).toBeCalledWith({ color: '#000000' }); }); }); describe('iconAction', () => { let iconAction; beforeEach(() => { iconAction = actions.icon; }); it('should run drawing mode when the add icon occurs', () => { const startDrawingModeSpy = jest.spyOn(imageEditorMock, 'startDrawingMode'); const setDrawingIconSpy = jest.spyOn(imageEditorMock, 'setDrawingIcon'); iconAction.addIcon('iconTypeA', '#fff'); expect(startDrawingModeSpy).toHaveBeenCalledWith('ICON'); expect(setDrawingIconSpy).toHaveBeenCalledWith('iconTypeA', '#fff'); }); }); describe('filterAction', () => { let filterAction; beforeEach(() => { filterAction = actions.filter; }); it('should be executed When the type of applyFilter is false', () => { imageEditorMock.removeFilter = jest.fn(); jest.spyOn(imageEditorMock, 'hasFilter').mockReturnValue(true); filterAction.applyFilter(false, {}); expect(imageEditorMock.removeFilter).toHaveBeenCalled(); }); it('should be executed When the type of applyFilter is true', () => { imageEditorMock.applyFilter = jest.fn(); filterAction.applyFilter(true, {}); expect(imageEditorMock.applyFilter).toHaveBeenCalled(); }); }); describe('commonAction', () => { it('should return to the getActions method must contain commonAction.', () => { ['shape', 'crop', 'flip', 'rotate', 'text', 'mask', 'draw', 'icon', 'filter'].forEach( (submenu) => { expect(actions[submenu].modeChange).toBeDefined(); expect(actions[submenu].deactivateAll).toBeDefined(); expect(actions[submenu].changeSelectableAll).toBeDefined(); expect(actions[submenu].discardSelection).toBeDefined(); expect(actions[submenu].stopDrawingMode).toBeDefined(); } ); }); describe('modeChange()', () => { let commonAction; beforeEach(() => { commonAction = actions.main; }); it('should be executed When the modeChange("text") action occurs', () => { const changeActivateModeSpy = jest.spyOn(imageEditorMock, '_changeActivateMode'); commonAction.modeChange('text'); expect(changeActivateModeSpy).toHaveBeenCalledWith('TEXT'); }); it('should be executed When the modeChange("crop") action occurs', () => { const startDrawingModeSpy = jest.spyOn(imageEditorMock, 'startDrawingMode'); commonAction.modeChange('crop'); expect(startDrawingModeSpy).toHaveBeenCalledWith('CROPPER'); }); it('should be executed When the modeChange("shape") action occurs', () => { const setDrawingShapeSpy = jest.spyOn(imageEditorMock, 'setDrawingShape'); const changeActivateModeSpy = jest.spyOn(imageEditorMock, '_changeActivateMode'); commonAction.modeChange('shape'); expect(setDrawingShapeSpy).toHaveBeenCalled(); expect(changeActivateModeSpy).toHaveBeenCalledWith('SHAPE'); }); }); }); describe('reAction', () => { let changeHelpButtonEnabledSpy; beforeEach(() => { imageEditorMock.setReAction(); changeHelpButtonEnabledSpy = jest.spyOn(imageEditorMock.ui, 'changeHelpButtonEnabled'); }); describe('undoStackChanged', () => { it('should be true if the undo stack has a length greater than zero', () => { imageEditorMock.fire('undoStackChanged', 1); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(1, 'undo', true); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(2, 'reset', true); }); it('should be false if the undo stack has a length of 0', () => { imageEditorMock.fire('undoStackChanged', 0); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(1, 'undo', false); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(2, 'reset', false); }); }); describe('redoStackChanged', () => { it('should be true if the redo stack is greater than 0 length', () => { imageEditorMock.fire('redoStackChanged', 1); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(1, 'redo', true); }); it('should be false if the redo stack has a length of 0', () => { imageEditorMock.fire('redoStackChanged', 0); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(1, 'redo', false); }); }); describe('objectActivated', () => { it('should be enabled when objectActivated occurs', () => { imageEditorMock.fire('objectActivated', { id: 1 }); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(1, 'delete', true); expect(changeHelpButtonEnabledSpy).toHaveBeenNthCalledWith(2, 'deleteAll', true); }); it('should be enabled when objectActivated target is cropzone', () => { const changeApplyButtonStatusSpy = jest.spyOn( imageEditorMock.ui.crop, 'changeApplyButtonStatus' ); imageEditorMock.fire('objectActivated', { id: 1, type: 'cropzone' }); expect(changeApplyButtonStatusSpy).toHaveBeenCalledWith(true); }); it('should be changed to shape if the target of objectActivated is shape and the existing menu is not shape', () => { imageEditorMock.ui.submenu = 'crop'; imageEditorMock.ui.changeMenu = jest.fn(); imageEditorMock.ui.shape.setShapeStatus = jest.fn(); const setMaxStrokeValueSpy = jest.spyOn(imageEditorMock.ui.shape, 'setMaxStrokeValue'); imageEditorMock.fire('objectActivated', { id: 1, type: 'circle' }); expect(imageEditorMock.ui.changeMenu).toHaveBeenCalledWith('shape', false, false); expect(setMaxStrokeValueSpy).toHaveBeenCalled(); }); it('should be changed to text if the target of objectActivated is text and the existing menu is not text', () => { imageEditorMock.ui.submenu = 'crop'; imageEditorMock.ui.changeMenu = jest.fn(); imageEditorMock.fire('objectActivated', { id: 1, type: 'i-text' }); expect(imageEditorMock.ui.changeMenu).toHaveBeenCalledWith('text', false, false); }); it('should be changed to icon if the target of objectActivated is icon and the existing menu is not icon', () => { imageEditorMock.ui.submenu = 'crop'; imageEditorMock.ui.changeMenu = jest.fn(); const setIconPickerColorSpy = jest.spyOn(imageEditorMock.ui.icon, 'setIconPickerColor'); imageEditorMock.fire('objectActivated', { id: 1, type: 'icon' }); expect(imageEditorMock.ui.changeMenu).toHaveBeenCalledWith('icon', false, false); expect(setIconPickerColorSpy).toHaveBeenCalled(); }); }); describe('addObjectAfter', () => { it('should be changed to match the size of the added object when addObjectAfter occurs', () => { const setMaxStrokeValueSpy = jest.spyOn(imageEditorMock.ui.shape, 'setMaxStrokeValue'); imageEditorMock.ui.shape.changeStandbyMode = jest.fn(); imageEditorMock.fire('addObjectAfter', { type: 'circle', width: 100, height: 200 }); expect(setMaxStrokeValueSpy).toHaveBeenCalledWith(100); expect(imageEditorMock.ui.shape.changeStandbyMode).toHaveBeenCalled(); }); }); describe('objectScaled', () => { it('should be changed if objectScaled occurs on an object of type text', () => { imageEditorMock.ui.text.fontSize = 0; imageEditorMock.fire('objectScaled', { type: 'i-text', fontSize: 20 }); expect(imageEditorMock.ui.text.fontSize).toBe(20); }); it('should be changed if objectScaled is for a shape type object and strokeValue is greater than the size of the object', () => { jest.spyOn(imageEditorMock.ui.shape, 'getStrokeValue').mockReturnValue(20); const setStrokeValueSpy = jest.spyOn(imageEditorMock.ui.shape, 'setStrokeValue'); imageEditorMock.fire('objectScaled', { type: 'rect', width: 10, height: 10 }); expect(setStrokeValueSpy).toHaveBeenCalledWith(10); }); }); describe('selectionCleared', () => { it('should be closed if selectionCleared occurs in the text menu state', () => { imageEditorMock.ui.submenu = 'text'; const changeCursorSpy = jest.spyOn(imageEditorMock, 'changeCursor'); imageEditorMock.fire('selectionCleared'); expect(changeCursorSpy).toHaveBeenCalledWith('text'); }); }); }); }); ================================================ FILE: apps/image-editor/tests/arrowLine.spec.js ================================================ import ArrowLine from '@/extension/arrowLine'; describe('ArrowLine', () => { let ctx, arrowLine, linePath; function assertPointsToMatchSnapshots() { const [firstPoint] = ctx.moveTo.mock.calls; const [secondPoint] = ctx.lineTo.mock.calls; const [, lastPoint] = ctx.lineTo.mock.calls; expect(firstPoint).toMatchSnapshot(); expect(secondPoint).toMatchSnapshot(); expect(lastPoint).toMatchSnapshot(); } beforeEach(() => { ctx = { lineWidth: 1, beginPath: jest.fn(), moveTo: jest.fn(), lineTo: jest.fn(), closePath: jest.fn(), }; arrowLine = new ArrowLine(); arrowLine.ctx = ctx; linePath = { fromX: 1, fromY: 1, toX: 10, toY: 10, }; }); it('should draw the "v" calculated according to the angle around the "tail" of the line when attaching the "chevron" type to the end point', () => { arrowLine.arrowType = { tail: 'chevron' }; arrowLine._drawDecoratorPath(linePath); assertPointsToMatchSnapshots(); }); it('should draw the "v" calculated according to the angle around the "head" of the line when attaching the "chevron" type to the start point', () => { arrowLine.arrowType = { head: 'chevron' }; arrowLine._drawDecoratorPath(linePath); assertPointsToMatchSnapshots(); }); it('should be a triangular shape that closes the path with closePath after drawing', () => { arrowLine.arrowType = { head: 'triangle' }; arrowLine._drawDecoratorPath(linePath); assertPointsToMatchSnapshots(); expect(ctx.closePath).toBeCalledTimes(1); }); }); ================================================ FILE: apps/image-editor/tests/command.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Invoker from '@/invoker'; import commandFactory from '@/factory/command'; import { stamp, hasStamp } from '@/util'; import { commandNames as commands } from '@/consts'; import addObjectCommand from '@/command/addObject'; import changeSelectionCommand from '@/command/changeSelection'; import loadImageCommand from '@/command/loadImage'; import flipCommand from '@/command/flip'; import addTextCommand from '@/command/addText'; import changeTextStyleCommand from '@/command/changeTextStyle'; import rotateCommand from '@/command/rotate'; import addShapeCommand from '@/command/addShape'; import changeShapeCommand from '@/command/changeShape'; import clearObjectsCommand from '@/command/clearObjects'; import removeObjectCommand from '@/command/removeObject'; import resizeCommand from '@/command/resize'; import img1 from 'fixtures/sampleImage.jpg'; import img2 from 'fixtures/TOAST UI Component.png'; describe('commandFactory', () => { let invoker, mockImage, canvas, graphics, dimensions; beforeAll(() => { commandFactory.register(addObjectCommand); commandFactory.register(changeSelectionCommand); commandFactory.register(loadImageCommand); commandFactory.register(flipCommand); commandFactory.register(addTextCommand); commandFactory.register(changeTextStyleCommand); commandFactory.register(rotateCommand); commandFactory.register(addShapeCommand); commandFactory.register(changeShapeCommand); commandFactory.register(clearObjectsCommand); commandFactory.register(removeObjectCommand); commandFactory.register(resizeCommand); }); beforeEach(() => { dimensions = { width: 100, height: 100 }; graphics = new Graphics(document.createElement('canvas')); invoker = new Invoker(); mockImage = new fabric.Image(null, dimensions); graphics.setCanvasImage('', mockImage); canvas = graphics.getCanvas(); }); describe('functions', () => { it('should register custom command', async () => { const testCommand = { name: 'testCommand', execute: jest.fn(() => Promise.resolve('testCommand')), undo: jest.fn(() => Promise.resolve()), }; commandFactory.register(testCommand); const command = commandFactory.create('testCommand'); expect(command).not.toBeNull(); const commandName = await invoker.execute('testCommand', graphics); expect(commandName).toBe('testCommand'); expect(testCommand.execute).toHaveBeenCalledWith(graphics); }); it('should pass parameters on execute', async () => { commandFactory.register({ name: 'testCommand', execute(compMap, obj1, obj2, obj3) { expect(obj1).toBe(1); expect(obj2).toBe(2); expect(obj3).toBe(3); return Promise.resolve(); }, }); await invoker.execute('testCommand', graphics, 1, 2, 3); }); it('should pass parameters on undo', async () => { commandFactory.register({ name: 'testCommand', execute() { return Promise.resolve(); }, undo(compMap, obj1, obj2, obj3) { expect(obj1).toBe(1); expect(obj2).toBe(2); expect(obj3).toBe(3); return Promise.resolve(); }, }); await invoker.execute('testCommand', graphics, 1, 2, 3); await invoker.undo(); }); }); describe('addObjectCommand', () => { let obj; beforeEach(() => { obj = new fabric.Rect(); }); it('should stamp object', async () => { await invoker.execute(commands.ADD_OBJECT, graphics, obj); expect(hasStamp(obj)).toBe(true); }); it('should add object to canvas', async () => { await invoker.execute(commands.ADD_OBJECT, graphics, obj); expect(canvas.contains(obj)).toBe(true); }); it('should remove object from canvas', async () => { await invoker.execute(commands.ADD_OBJECT, graphics, obj); await invoker.undo(); expect(canvas.contains(obj)).toBe(false); }); }); describe('changeSelectionCommand', () => { let obj; beforeEach(() => { canvas.getPointer = jest.fn(); obj = new fabric.Rect({ width: 10, height: 10, top: 10, left: 10, scaleX: 1, scaleY: 1, angle: 0, }); graphics._addFabricObject(obj); graphics._onMouseDown({ target: obj }); const props = [ { id: graphics.getObjectId(obj), width: 30, height: 30, top: 30, left: 30, scaleX: 0.5, scaleY: 0.5, angle: 10, }, ]; const makeCommand = commandFactory.create(commands.CHANGE_SELECTION, graphics, props); makeCommand.execute(graphics, props); invoker.pushUndoStack(makeCommand); }); it('should work undo command correctly', async () => { await invoker.undo(); expect(obj).toMatchObject({ width: 10, height: 10, left: 10, top: 10, scaleX: 1, scaleY: 1, angle: 0, }); }); it('should work redo command correctly', async () => { await invoker.undo(); await invoker.redo(); expect(obj).toMatchObject({ width: 30, height: 30, left: 30, top: 30, scaleX: 0.5, scaleY: 0.5, angle: 10, }); }); }); describe('loadImageCommand', () => { const img = new fabric.Image(img1); beforeEach(() => { graphics.setCanvasImage('', null); }); it('should clear canvas', async () => { jest.spyOn(canvas, 'clear'); await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img); expect(canvas.clear).toHaveBeenCalled(); }); it('should load new image', async () => { const changedSize = await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img); expect(graphics.getImageName()).toBe('image'); expect(changedSize).toMatchObject({ oldWidth: expect.any(Number), oldHeight: expect.any(Number), newWidth: expect.any(Number), newHeight: expect.any(Number), }); }); it('should not include cropzone after running the LOAD_IMAGE command', async () => { const objCropzone = new fabric.Object({ type: 'cropzone' }); await invoker.execute(commands.ADD_OBJECT, graphics, objCropzone); await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img); const lastUndoIndex = invoker._undoStack.length - 1; const savedObjects = invoker._undoStack[lastUndoIndex].undoData.objects; expect(savedObjects).toHaveLength(0); }); it('should be true after LOAD_IMAGE command.', async () => { const objCircle = new fabric.Object({ type: 'circle', evented: false }); await invoker.execute(commands.ADD_OBJECT, graphics, objCircle); await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img); const lastUndoIndex = invoker._undoStack.length - 1; const [savedObject] = invoker._undoStack[lastUndoIndex].undoData.objects; expect(savedObject.evented).toBe(true); }); it('should clear image if not exists prev image', async () => { await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img); await invoker.undo(); expect(graphics.getCanvasImage()).toBeNull(); expect(graphics.getImageName()).toBe(''); }); it('should restore to prev image', async () => { const newImg = new fabric.Image(img2); await invoker.execute(commands.LOAD_IMAGE, graphics, 'image', img); await invoker.execute(commands.LOAD_IMAGE, graphics, 'newImage', newImg); expect(graphics.getImageName()).toBe('newImage'); await invoker.undo(); expect(graphics.getImageName()).toBe('image'); }); }); describe('flipImageCommand', () => { it('should be flipped over to the x-axis.', async () => { const flipStatus = mockImage.flipX; await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipX'); expect(mockImage.flipX).toBe(!flipStatus); }); it('should be flipped over to the y-axis.', async () => { const flipStatus = mockImage.flipY; await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipY'); expect(mockImage.flipY).toBe(!flipStatus); }); it('should reset flip', async () => { mockImage.flipX = true; mockImage.flipY = true; await invoker.execute(commands.FLIP_IMAGE, graphics, 'reset'); expect(mockImage).toMatchObject({ flipX: false, flipY: false }); }); it('should restore flipX', async () => { const flipStatus = mockImage.flipX; await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipX'); await invoker.undo(); expect(mockImage.flipX).toBe(flipStatus); }); it('should restore flipY', async () => { const flipStatus = mockImage.flipY; await invoker.execute(commands.FLIP_IMAGE, graphics, 'flipY'); await invoker.undo(); expect(mockImage.flipY).toBe(flipStatus); }); }); describe('textCommand', () => { let textObjectId; const fontSize = 50; const underline = false; const newFontSize = 30; const newUnderline = true; beforeEach(async () => { const textObject = await invoker.execute(commands.ADD_TEXT, graphics, 'text', { styles: { fontSize, underline, }, }); textObjectId = textObject.id; }); it('should set text style', async () => { await invoker.execute(commands.CHANGE_TEXT_STYLE, graphics, textObjectId, { fontSize: newFontSize, underline: newUnderline, }); const textObject = graphics.getObject(textObjectId); expect(textObject).toMatchObject({ fontSize: 30, underline: true }); }); it('should restore fontSize', async () => { await invoker.execute(commands.CHANGE_TEXT_STYLE, graphics, textObjectId, { fontSize: newFontSize, underline: newUnderline, }); await invoker.undo(); const textObject = graphics.getObject(textObjectId); expect(textObject).toMatchObject({ fontSize, underline }); }); }); describe('rotateCommand', () => { it('should add angle', async () => { const originAngle = mockImage.angle; await invoker.execute(commands.ROTATE_IMAGE, graphics, 'rotate', 10); expect(mockImage.angle).toBe(originAngle + 10); }); it('should set angle', async () => { mockImage.angle = 100; await invoker.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 30); expect(mockImage.angle).toBe(30); }); it('should restore angle', async () => { const originalAngle = mockImage.angle; await invoker.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 100); await invoker.undo(); expect(mockImage.angle).toBe(originalAngle); }); }); describe('shapeCommand', () => { let shapeObjectId; const defaultStrokeWidth = 12; const strokeWidth = 50; beforeEach(async () => { const shapeObject = await invoker.execute(commands.ADD_SHAPE, graphics, 'rect', { strokeWidth: defaultStrokeWidth, }); shapeObjectId = shapeObject.id; }); it('should set strokeWidth', async () => { await invoker.execute(commands.CHANGE_SHAPE, graphics, shapeObjectId, { strokeWidth }); const shapeObject = graphics.getObject(shapeObjectId); expect(shapeObject.strokeWidth).toBe(strokeWidth); }); it('should restore strokeWidth', async () => { await invoker.execute(commands.CHANGE_SHAPE, graphics, shapeObjectId, { strokeWidth }); await invoker.undo(); const shapeObject = graphics.getObject(shapeObjectId); expect(shapeObject.strokeWidth).toBe(defaultStrokeWidth); }); }); describe('clearCommand', () => { let canvasContext, objects; beforeEach(() => { canvasContext = canvas; objects = [new fabric.Rect(), new fabric.Rect(), new fabric.Rect()]; }); it('should clear all objects', async () => { canvas.add.apply(canvasContext, objects); expect(canvas.contains(objects[0])).toBe(true); expect(canvas.contains(objects[1])).toBe(true); expect(canvas.contains(objects[2])).toBe(true); await invoker.execute(commands.CLEAR_OBJECTS, graphics); expect(canvas.contains(objects[0])).toBe(false); expect(canvas.contains(objects[1])).toBe(false); expect(canvas.contains(objects[2])).toBe(false); }); it('should restore all objects', async () => { canvas.add.apply(canvasContext, objects); await invoker.execute(commands.CLEAR_OBJECTS, graphics); await invoker.undo(); expect(canvas.contains(objects[0])).toBe(true); expect(canvas.contains(objects[1])).toBe(true); expect(canvas.contains(objects[2])).toBe(true); }); }); describe('removeCommand', () => { let object, object2, group; beforeEach(() => { object = new fabric.Rect({ left: 10, top: 10 }); object2 = new fabric.Rect({ left: 5, top: 20 }); group = new fabric.Group(); graphics.add(object); graphics.add(object2); graphics.add(group); group.add(object, object2); }); it('should remove an object', async () => { graphics.setActiveObject(object); await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(object)); expect(canvas.contains(object)).toBe(false); }); it('should remove objects in group', async () => { canvas.setActiveObject(group); await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(group)); expect(canvas.contains(object)).toBe(false); expect(canvas.contains(object2)).toBe(false); }); it('should restore the removed object', async () => { canvas.setActiveObject(object); await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(object)); await invoker.undo(); expect(canvas.contains(object)).toBe(true); }); it('should restore the removed objects in group', async () => { canvas.setActiveObject(group); await invoker.execute(commands.REMOVE_OBJECT, graphics, stamp(group)); await invoker.undo(); expect(canvas.contains(object)).toBe(true); expect(canvas.contains(object2)).toBe(true); }); it('should restore the position of the removed object in group', async () => { const activeSelection = graphics.getActiveSelectionFromObjects(canvas.getObjects()); graphics.setActiveObject(activeSelection); await invoker.execute( commands.REMOVE_OBJECT, graphics, graphics.getActiveObjectIdForRemove() ); await invoker.undo(); expect(object).toMatchObject({ left: 10, top: 10 }); expect(object2).toMatchObject({ left: 5, top: 20 }); }); }); describe('resizeCommand', () => { const newDimensions = { width: 20, height: 20 }; it('should resize image', async () => { await invoker.execute(commands.RESIZE_IMAGE, graphics, newDimensions); const { width, height, scaleX, scaleY } = mockImage; expect({ width: width * scaleX, height: height * scaleY }).toEqual(newDimensions); }); it('should restore dimensions of image', async () => { await invoker.execute(commands.RESIZE_IMAGE, graphics, newDimensions); await invoker.undo(); const { width, height, scaleX, scaleY } = mockImage; expect({ width: width * scaleX, height: height * scaleY }).toEqual(dimensions); }); }); }); ================================================ FILE: apps/image-editor/tests/cropper.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Cropper from '@/component/cropper'; import { eventNames, CROPZONE_DEFAULT_OPTIONS } from '@/consts'; describe('Cropper', () => { let cropper, graphics, canvas; beforeEach(() => { graphics = new Graphics(document.createElement('canvas')); canvas = graphics.getCanvas(); cropper = new Cropper(graphics); }); describe('start()', () => { it('should create a cropzone', () => { cropper.start(); expect(cropper._cropzone).toBeDefined(); }); it('should be applied predefined default options When creating a cropzone', () => { cropper.start(); const cropzone = cropper._cropzone; Object.entries(CROPZONE_DEFAULT_OPTIONS).forEach(([optionName, optionValue]) => { expect(cropzone[optionName]).toBe(optionValue); }); }); it('should add a cropzone to canvas', () => { const addSpy = jest.spyOn(canvas, 'add'); cropper.start(); expect(addSpy).toHaveBeenCalledWith(cropper._cropzone); }); it('should no action if a croppzone has been defined', () => { cropper._cropzone = {}; const addSpy = jest.spyOn(canvas, 'add'); cropper.start(); expect(addSpy).not.toHaveBeenCalled(); }); it('should set "evented" of all objects to false', () => { const eventedOptions = { evented: true }; const objects = [ new fabric.Rect(eventedOptions), new fabric.Rect(eventedOptions), new fabric.Rect(eventedOptions), ]; canvas.add(...objects); cropper.start(); expect(objects[0].evented).toBe(false); expect(objects[1].evented).toBe(false); expect(objects[2].evented).toBe(false); }); }); describe('onFabricMouseDown()', () => { let fEvent; beforeEach(() => { fEvent = { e: {} }; jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 10, y: 20 }); }); it('should set "selection" to false', () => { cropper._onFabricMouseDown(fEvent); expect(canvas.selection).toBe(false); }); it('should set "startX, startY"', () => { cropper._onFabricMouseDown(fEvent); expect(cropper._startX).toEqual(10); expect(cropper._startY).toEqual(20); }); }); describe('onFabricMouseMove()', () => { beforeEach(() => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 10, y: 20 }); jest.spyOn(canvas, 'getWidth').mockReturnValue(100); jest.spyOn(canvas, 'getHeight').mockReturnValue(200); }); it('should re-render(remove->set->add) cropzone if the mouse moving is over the threshold(=10)', () => { cropper._startX = 0; cropper._startY = 0; cropper.start(); const removeSpy = jest.spyOn(canvas, 'remove'); const setSpy = jest.spyOn(cropper._cropzone, 'set'); const addSpy = jest.spyOn(canvas, 'add'); cropper._onFabricMouseMove({ e: {} }); expect(removeSpy).toHaveBeenCalled(); expect(setSpy).toHaveBeenCalled(); expect(addSpy).toHaveBeenCalled(); }); it('should not re-render cropzone if the mouse moving is under the threshold', () => { cropper._startX = 14; cropper._startY = 18; cropper.start(); const removeSpy = jest.spyOn(canvas, 'remove'); const setSpy = jest.spyOn(cropper._cropzone, 'set'); const addSpy = jest.spyOn(canvas, 'add'); cropper._onFabricMouseMove({ e: {} }); expect(removeSpy).not.toHaveBeenCalled(); expect(setSpy).not.toHaveBeenCalled(); expect(addSpy).not.toHaveBeenCalled(); }); }); describe('_calcRectDimensionFromPoint()', () => { beforeEach(() => { cropper._startX = 10; cropper._startY = 20; jest.spyOn(canvas, 'getWidth').mockReturnValue(100); jest.spyOn(canvas, 'getHeight').mockReturnValue(200); }); it('should return cropzone-left&top (min: 0, max: startX,Y)', () => { const dimension = cropper._calcRectDimensionFromPoint(20, -1); expect(dimension).toEqual({ left: 10, top: 0, width: expect.any(Number), height: expect.any(Number), }); }); it('should calculate and return cropzone-width&height', () => { let dimension; dimension = cropper._calcRectDimensionFromPoint(30, 40); expect(dimension).toEqual({ left: 10, top: 20, width: 20, height: 20, }); dimension = cropper._calcRectDimensionFromPoint(300, 400); expect(dimension).toEqual({ left: 10, top: 20, width: 90, height: 180, }); }); it('should create cropzone that has fixed ratio during shift key is pressed.', () => { cropper._withShiftKey = true; const dimension = cropper._calcRectDimensionFromPoint(100, 200); expect(dimension).toEqual({ left: 10, top: 20, width: 180, height: 180, }); }); it('should create cropzone that inverted current mouse position during shift key is pressed.', () => { cropper._withShiftKey = true; const dimension = cropper._calcRectDimensionFromPoint(-10, -20); expect(dimension).toEqual({ left: -10, top: 0, width: 20, height: 20, }); }); it('should restrict cropzone dimensions to presetRatio', () => { const dimension = cropper._calcRectDimensionFromPoint(50, 100, 16 / 9); expect(dimension).toEqual({ left: 10, top: 20, width: 40, height: 22.5, // width / presetRatio -> 60 / 1,777777778 }); }); it('should restrict cropzone within canvas and keep presetRatio when width too large', () => { const dimension = cropper._calcRectDimensionFromPoint(110, 100, 16 / 9); expect(dimension).toEqual({ left: 10, top: 20, width: 90, // maxwidth (100) minus start (10) height: 50.625, // width / presetRatio -> 90 / (16/9) }); }); it('should restrict cropzone within canvas and keep presetRatio when height too large', () => { cropper._startY = 177.5; const dimension = cropper._calcRectDimensionFromPoint(100, 250, 16 / 9); expect(dimension).toEqual({ left: 10, top: 177.5, width: 40, // height * presetRatio -> 22.5 * (16/9) height: 22.5, // maxwidth (200) minus start (177.5) }); }); }); it('should activate cropzone', () => { canvas.setActiveObject = jest.fn(); cropper.start(); cropper._onFabricMouseUp(); expect(canvas.setActiveObject).toHaveBeenCalledWith(cropper._cropzone); }); describe('crop()', () => { beforeEach(() => { cropper.start(); }); afterEach(() => { cropper.end(); }); it('should return cropzone rect', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); const cropzoneRect = cropper.getCropzoneRect(); expect(cropzoneRect).not.toBeNull(); }); it('should return cropzone data if the cropzone is valid', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); const cropzoneRect = cropper.getCropzoneRect(); const croppedImageData = cropper.getCroppedImageData(cropzoneRect); expect(croppedImageData).toEqual({ imageName: expect.any(String), url: expect.any(String), }); }); }); describe('presets - setCropzoneRect()', () => { beforeEach(() => { cropper.start(); }); afterEach(() => { cropper.end(); }); it('should return cropzone rect as a square', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); cropper.setCropzoneRect(1); const { width, height } = cropper.getCropzoneRect(); expect(width).toBe(height); }); it('should return cropzone rect as a 3:2 aspect box', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); cropper.setCropzoneRect(3 / 2); const { width, height } = cropper.getCropzoneRect(); expect((width / height).toFixed(1)).toBe((3 / 2).toFixed(1)); }); it('should return cropzone rect as a 4:3 aspect box', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); cropper.setCropzoneRect(4 / 3); const { width, height } = cropper.getCropzoneRect(); expect((width / height).toFixed(1)).toBe((4 / 3).toFixed(1)); }); it('should return cropzone rect as a 5:4 aspect box', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); cropper.setCropzoneRect(5 / 4); const { width, height } = cropper.getCropzoneRect(); expect((width / height).toFixed(1)).toBe((5 / 4).toFixed(1)); }); it('should return cropzone rect as a 7:5 aspect box', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); cropper.setCropzoneRect(7 / 5); const { width, height } = cropper.getCropzoneRect(); expect((width / height).toFixed(1)).toBe((7 / 5).toFixed(1)); }); it('should return cropzone rect as a 16:9 aspect box', () => { jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); cropper.setCropzoneRect(16 / 9); const { width, height } = cropper.getCropzoneRect(); expect((width / height).toFixed(1)).toBe((16 / 9).toFixed(1)); }); it('Even in situations with floating point problems, should calculate the exact width you expect.', () => { jest.spyOn(canvas, 'getWidth').mockReturnValue(408); jest.spyOn(canvas, 'getHeight').mockReturnValue(312); const setSpy = jest.spyOn(cropper._cropzone, 'set'); cropper.setCropzoneRect(16 / 9); expect(setSpy).toHaveBeenCalledWith(expect.objectContaining({ width: 408 })); }); it('should remove cropzone of cropper when falsy is passed', () => { cropper.setCropzoneRect(); expect(cropper.getCropzoneRect()).toBeNull(); cropper.setCropzoneRect(0); expect(cropper.getCropzoneRect()).toBeNull(); cropper.setCropzoneRect(null); expect(cropper.getCropzoneRect()).toBeNull(); }); }); describe('end()', () => { it('should set cropzone of cropper to null', () => { cropper.start(); cropper.end(); expect(cropper._cropzone).toBeNull(); }); it('should set "evented" of all objects to true', () => { const eventedOptions = { evented: false }; const objects = [ new fabric.Rect(eventedOptions), new fabric.Rect(eventedOptions), new fabric.Rect(eventedOptions), ]; canvas.add(...objects); cropper.start(); cropper.end(); expect(objects[0].evented).toBe(true); expect(objects[1].evented).toBe(true); expect(objects[2].evented).toBe(true); }); }); describe('canvas event delegator', () => { it('The event of an object with an eventDelegator must fire the graphics.fire registered with the trigger.', () => { cropper.start(); const fireSpy = jest.spyOn(graphics, 'fire'); const cropzone = cropper._cropzone; canvas.fire('object:scaling', { target: cropper._cropzone }); expect(fireSpy).not.toHaveBeenCalled(); cropzone.canvasEventTrigger[eventNames.OBJECT_SCALED](cropzone); expect(fireSpy).toHaveBeenCalled(); }); }); }); ================================================ FILE: apps/image-editor/tests/cropzone.spec.js ================================================ import { fabric } from 'fabric'; import Cropzone from '@/extension/cropzone'; describe('Cropzone', () => { const options = { left: 10, top: 10, width: 100, height: 100, cornerSize: 10, strokeWidth: 0, cornerColor: 'black', fill: 'transparent', hasRotatingPoint: false, hasBorders: false, lockScalingFlip: true, lockRotation: true, }; const canvas = new fabric.Canvas(); canvas.height = 400; canvas.width = 300; it('should return outer&inner rect coordinates(array)', () => { const cropzone = new Cropzone(canvas, options, {}); const coords = cropzone._getCoordinates(); expect(coords).toEqual({ x: [-60, -50, 50, 240], y: [-60, -50, 50, 340], }); }); it('should set left and top between 0 and canvas size', () => { const cropzone = new Cropzone(canvas, options, {}); jest.spyOn(cropzone.canvas, 'getWidth').mockReturnValue(300); jest.spyOn(cropzone.canvas, 'getHeight').mockReturnValue(400); cropzone.left = -1; cropzone.top = -1; cropzone._onMoving(); expect(cropzone).toMatchObject({ top: 0, left: 0 }); cropzone.left = 1000; cropzone.top = 1000; cropzone._onMoving(); expect(cropzone).toMatchObject({ top: 300, left: 200 }); }); it('should return whether the cropzone has real area or not', () => { const cropzone = new Cropzone(canvas, options, {}); cropzone.left = -1; expect(cropzone.isValid()).toBe(false); cropzone.left = 1; expect(cropzone.isValid()).toBe(true); cropzone.height = -1; expect(cropzone.isValid()).toBe(false); cropzone.height = 1; expect(cropzone.isValid()).toBe(true); }); it('should give the expected value at run', () => { const cropzone = new Cropzone(canvas, options, {}); let resizedCropzone = cropzone._resizeCropZone({ x: 30, y: 40 }, 'tl'); expect(resizedCropzone).toEqual({ left: 30, top: 40, width: 80, height: 70, }); resizedCropzone = cropzone._resizeCropZone({ x: 80, y: 50 }, 'tr'); expect(resizedCropzone).toEqual({ left: 10, top: 50, width: 70, height: 60, }); resizedCropzone = cropzone._resizeCropZone({ x: 30, y: 40 }, 'bl'); expect(resizedCropzone).toEqual({ left: 30, top: 10, width: 80, height: 30, }); resizedCropzone = cropzone._resizeCropZone({ x: 30, y: 40 }, 'br'); expect(resizedCropzone).toEqual({ left: 10, top: 10, width: 20, height: 30, }); }); it('should yield the result of maintaining the ratio at running the resize function at a fixed rate', () => { const presetRatio = 5 / 4; const cropzone = new Cropzone(canvas, { ...options, width: 50, height: 40, presetRatio }, {}); ['tl', 'tr', 'mt', 'ml', 'mr', 'mb', 'bl', 'br'].forEach((cornerType) => { const { width, height } = cropzone._resizeCropZone({ x: 20, y: 20 }, cornerType); expect(width / height).toEqual(presetRatio); }); }); }); ================================================ FILE: apps/image-editor/tests/drawingMode.spec.js ================================================ import { fabric } from 'fabric'; import ImageEditor from '@/imageEditor'; import '@/command/loadImage'; import img from 'fixtures/sampleImage.jpg'; describe('DrawingMode', () => { let imageEditor; beforeEach(async () => { imageEditor = new ImageEditor(document.createElement('div'), { cssMaxWidth: 700, cssMaxHeight: 500, }); const image = new fabric.Image(img); await imageEditor.loadImageFromURL(image, 'sampleImage'); }); afterEach(() => { imageEditor.destroy(); }); it('should enter a drawing mode with startDrawingMode, CROPPER', () => { imageEditor.startDrawingMode('CROPPER'); expect(imageEditor.getDrawingMode()).toBe('CROPPER'); }); it('should stop a drawing mode with stopDrawingMode, ie, to normal', () => { imageEditor.stopDrawingMode(); expect(imageEditor.getDrawingMode()).toBe('NORMAL'); }); it('should enter all drawing mode with startDrawingMode in consecutive order', () => { ['CROPPER', 'FREE_DRAWING', 'LINE_DRAWING', 'TEXT', 'SHAPE', 'RESIZE'].forEach( (drawingMode) => { imageEditor.startDrawingMode(drawingMode); expect(imageEditor.getDrawingMode()).toBe(drawingMode); } ); expect(imageEditor.startDrawingMode('CROPPER')).toBe(true); expect(imageEditor.startDrawingMode('CROPPER')).toBe(true); // call again, should return true expect(imageEditor.startDrawingMode('NOT_A_DRAWING_MODE')).toBe(false); }); }); ================================================ FILE: apps/image-editor/tests/filter.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Filter from '@/component/filter'; import img from 'fixtures/sampleImage.jpg'; describe('Filter', () => { const graphics = new Graphics(document.createElement('canvas')); const filter = new Filter(graphics); beforeEach(async () => { const image = new fabric.Image(img); jest.spyOn(image, 'applyFilters').mockReturnValue({}); graphics.setCanvasImage('mockImage', image); await filter.add('colorFilter', {}); }); it('should add filter', () => { expect(filter.hasFilter('invert')).toBe(false); expect(filter.hasFilter('colorFilter')).toBe(true); }); it('should remove added filter', async () => { await filter.remove('colorFilter'); expect(filter.hasFilter('colorFilter')).toBe(false); }); }); ================================================ FILE: apps/image-editor/tests/flip.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Flip from '@/component/flip'; describe('Flip', () => { let graphics, flip, mockImage; beforeAll(() => { graphics = new Graphics(document.createElement('canvas')); flip = new Flip(graphics); }); beforeEach(() => { mockImage = new fabric.Image(); graphics.setCanvasImage('mockImage', mockImage); }); it('should return current flip-setting', () => { let setting = flip.getCurrentSetting(); expect(setting).toEqual({ flipX: false, flipY: false }); mockImage.set({ flipX: true }); setting = flip.getCurrentSetting(); expect(setting).toEqual({ flipX: true, flipY: false }); }); it('should set flip-setting', () => { flip.set({ flipX: false, flipY: true }); expect(flip.getCurrentSetting()).toEqual({ flipX: false, flipY: true }); }); it('should reset flip-setting to false', () => { mockImage.set({ flipX: true, flipY: true }); flip.reset(); expect(flip.getCurrentSetting()).toEqual({ flipX: false, flipY: false }); }); it('should be flipped over relative to the x-axis', () => { flip.flipX(); expect(flip.getCurrentSetting()).toEqual({ flipX: true, flipY: false }); flip.flipX(); expect(flip.getCurrentSetting()).toEqual({ flipX: false, flipY: false }); }); it('should be flipped over relative to the y-axis', () => { flip.flipY(); expect(flip.getCurrentSetting()).toEqual({ flipX: false, flipY: true }); flip.flipY(); expect(flip.getCurrentSetting()).toEqual({ flipX: false, flipY: false }); }); describe('Promise is returned with settings and angle,', () => { beforeEach(() => { mockImage.angle = 10; }); it('should be changed if it is flipped over relative to the x-axis', async () => { const obj = await flip.flipX(); expect(obj).toEqual({ flipX: true, flipY: false, angle: -10 }); }); it('should be changed if it is flipped over relative to the y-axis', async () => { const obj = await flip.flipY(); expect(obj).toEqual({ flipX: false, flipY: true, angle: -10 }); }); it('should be changed if it is flipped over relative to the x-axis and y-axis', async () => { const obj = await flip.set({ flipX: true, flipY: false }); expect(obj).toEqual({ flipX: true, flipY: false, angle: -10 }); }); }); }); ================================================ FILE: apps/image-editor/tests/graphics.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import { stamp } from '@/util'; import { drawingModes, componentNames as components } from '@/consts'; describe('Graphics', () => { const cssMaxWidth = 900; const cssMaxHeight = 700; let graphics, canvas; beforeEach(() => { graphics = new Graphics(document.createElement('canvas'), { cssMaxWidth, cssMaxHeight }); canvas = graphics.getCanvas(); }); afterEach(() => { graphics.stopDrawingMode(); }); it('should have several properties', () => { expect(canvas).not.toBeNull(); expect(canvas).toEqual(expect.any(fabric.Canvas)); expect(graphics.cssMaxWidth).toBe(900); expect(graphics.cssMaxHeight).toBe(700); expect(graphics.canvasImage).toBeNull(); expect(graphics.imageName).toBe(''); expect(graphics._drawingMode).toBe(drawingModes.NORMAL); expect(graphics._componentMap).not.toBeNull(); }); it('should be changed after the path has been drawn', () => { const pathObj = new fabric.Path('M 0 0 L 100 0 L 100 100 L 0 100 z'); const { x, y } = pathObj.getCenterPoint(); graphics._onPathCreated({ path: pathObj }); expect(pathObj.originX).toBe('center'); expect(pathObj.originY).toBe('center'); expect(pathObj.left).toBe(x); expect(pathObj.top).toBe(y); }); it('should attach canvas events', () => { const onMousedown = jest.fn(); const onObjectAdded = jest.fn(); const onObjectSelected = jest.fn(); graphics.on({ mousedown: onMousedown, 'object:added': onObjectAdded }); graphics.once('object:selected', onObjectSelected); graphics.fire('mousedown'); graphics.fire('mousedown'); graphics.fire('object:added'); graphics.fire('object:added'); graphics.fire('object:selected'); graphics.fire('object:selected'); expect(onMousedown).toHaveBeenCalledTimes(2); expect(onObjectAdded).toHaveBeenCalledTimes(2); expect(onObjectSelected).toHaveBeenCalledTimes(1); }); it('should deactivate all objects', () => { const triangle = new fabric.Triangle({ width: 20, height: 30 }); canvas.add(triangle).setActiveObject(triangle); expect(canvas.getActiveObject()).not.toBeNull(); graphics.deactivateAll(); expect(canvas.getActiveObject()).toBeNull(); }); it('should render objects', () => { let beforeRender = false; const triangle = new fabric.Triangle({ width: 20, height: 30 }); canvas.add(triangle); canvas.on('before:render', () => { beforeRender = true; }); canvas.on('after:render', () => expect(beforeRender).toBe(true)); graphics.renderAll(); }); it('should remove a object or group by id', () => { const triangle = new fabric.Triangle({ width: 20, height: 30 }); graphics.add(triangle); const objectId = stamp(triangle); graphics.removeObjectById(objectId); expect(graphics.getObjects()).toHaveLength(0); }); it('should switch drawing modes', () => { Object.keys(drawingModes).forEach((modeName) => { graphics.startDrawingMode(modeName); expect(graphics.getDrawingMode()).toBe(modeName); graphics.stopDrawingMode(); expect(graphics.getDrawingMode()).toBe(drawingModes.NORMAL); }); }); it('should get the cropped image data', () => { const cropper = graphics.getComponent(components.CROPPER); graphics.startDrawingMode(drawingModes.CROPPER); jest.spyOn(cropper._cropzone, 'isValid').mockReturnValue(true); const cropzoneRect = graphics.getCropzoneRect(); expect(cropzoneRect).not.toBeNull(); expect(graphics.getCroppedImageData(cropzoneRect)).toEqual({ imageName: expect.any(String), url: expect.any(String), }); }); it('should be hidden initially and then redisplayed after completion at toDataURL is executed with a cropzone present', () => { const cropper = graphics.getComponent(components.CROPPER); const changeVisibilitySpy = jest.spyOn(cropper, 'changeVisibility'); graphics.startDrawingMode(drawingModes.CROPPER); graphics.toDataURL(); expect(changeVisibilitySpy).toHaveBeenNthCalledWith(1, false); expect(changeVisibilitySpy).toHaveBeenNthCalledWith(2, true); }); it('should set brush setting into LINE_DRAWING, FREE_DRAWING', () => { graphics.startDrawingMode(drawingModes.LINE_DRAWING); graphics.setBrush({ width: 12, color: 'FFFF00' }); const brush = canvas.freeDrawingBrush; expect(brush).toMatchObject({ width: 12, color: 'rgba(255,255,0,1)' }); }); it('should change a drawing shape', () => { const shapeComp = graphics.getComponent(components.SHAPE); graphics.setDrawingShape('circle', { fill: 'transparent', stroke: 'blue', strokeWidth: 3, rx: 10, ry: 100, }); expect(shapeComp._type).toBe('circle'); expect(shapeComp._options).toEqual({ strokeWidth: 3, stroke: 'blue', fill: 'transparent', width: 1, height: 1, rx: 10, ry: 100, lockSkewingX: true, lockSkewingY: true, bringForward: true, isRegular: false, }); }); it('should register custom icon', () => { const pathMap = { customIcon: 'M 0 0 L 20 20 L 10 10 Z' }; const iconComp = graphics.getComponent(components.ICON); graphics.registerPaths(pathMap); expect(iconComp._pathMap).toMatchObject(pathMap); }); it('should not have the filter', () => { expect(graphics.hasFilter('Grayscale')).toBe(false); }); describe('pasteObject()', () => { let targetObject1, targetObject2; beforeEach(() => { targetObject1 = new fabric.Object({}); targetObject2 = new fabric.Object({}); canvas.add(targetObject1); canvas.add(targetObject2); }); it('should be duplicated as many as the number of objects in the group', async () => { const groupObject = graphics.getActiveSelectionFromObjects(canvas.getObjects()); graphics.setActiveObject(groupObject); graphics.resetTargetObjectForCopyPaste(); await graphics.pasteObject(); expect(canvas.getObjects()).toHaveLength(4); }); it('should be duplicated', async () => { graphics.setActiveObject(targetObject1); graphics.resetTargetObjectForCopyPaste(); await graphics.pasteObject(); expect(canvas.getObjects()).toHaveLength(3); }); }); }); ================================================ FILE: apps/image-editor/tests/history.spec.js ================================================ import History from '@/ui/history'; describe('history', () => { let history, options, name, detail; beforeEach(() => { options = {}; history = new History(document.createElement('div'), options); history._actions = { undo() {}, redo() {} }; history.makeSvgIcon = () => {}; history.locale = { localize: (historyName) => historyName }; name = 'history-name'; detail = 'history-detail'; }); it('should add a history item', () => { jest.spyOn(history, '_selectItem'); history.add({ name, detail }); expect(history.getListLength()).toBe(1); expect(history._selectItem).toHaveBeenCalled(); }); it('should add an event listener', () => { jest.spyOn(history.listElement, 'addEventListener'); history._addHistoryEventListener(); expect(history.listElement.addEventListener).toHaveBeenCalled(); }); it('should remove an event listener', () => { jest.spyOn(history.listElement, 'removeEventListener'); history._removeHistoryEventListener(); expect(history.listElement.removeEventListener).toHaveBeenCalled(); }); describe('_clickHistoryItem', () => { let target; beforeEach(() => { name = 'history-name'; detail = 'history-detail'; target = document.createElement('li'); target.className = 'history-item'; target.setAttribute('data-index', 1); history.add({ name, detail }); history.add({ name, detail }); }); it('should do nothing when index is the same as historyIndex', () => { jest.spyOn(history, '_selectItem'); history._clickHistoryItem({ target }); expect(history._selectItem).not.toHaveBeenCalled(); }); }); describe('_selectItem', () => { let index, listLength; beforeEach(() => { history.add({ name, detail }); history.add({ name, detail }); index = 1; listLength = history.getListLength(); }); it('should select item', () => { jest.spyOn(history, 'addClass'); jest.spyOn(history, 'removeClass'); history._selectItem(index); expect(history.addClass).toHaveBeenCalledTimes(1); expect(history.addClass).toHaveBeenCalledWith(index, 'selected-item'); expect(history.removeClass).toHaveBeenCalledTimes(listLength * 2); }); }); it('should destroy history instance', () => { history.destroy(); Object.values(history).forEach((propValue) => { expect(propValue).toBeNull(); }); }); it('should register an action and add event listener', () => { const actions = {}; jest.spyOn(history, '_addHistoryEventListener'); history.addEvent(actions); expect(history._addHistoryEventListener).toHaveBeenCalled(); expect(history._actions).toEqual(actions); }); it('should remove an action and event listener', () => { jest.spyOn(history, '_removeHistoryEventListener'); history.removeEvent(); expect(history._removeHistoryEventListener).toHaveBeenCalled(); }); }); ================================================ FILE: apps/image-editor/tests/icon.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Icon from '@/component/icon'; describe('Icon', () => { let canvas, graphics, mockImage, icon; beforeAll(() => { graphics = new Graphics(document.createElement('canvas')); canvas = graphics.getCanvas(); icon = new Icon(graphics); }); beforeEach(() => { mockImage = new fabric.Image(); graphics.setCanvasImage('mockImage', mockImage); }); afterEach(() => { canvas.forEachObject((obj) => { canvas.remove(obj); }); }); describe('_onFabricMouseMove()', () => { let iconObj, fEvent; beforeEach(async () => { fEvent = { e: {} }; icon._startPoint = { x: 300, y: 300 }; await icon.add('arrow', { left: icon._startPoint.x, top: icon._startPoint.y, color: '#000' }); [iconObj] = canvas.getObjects(); iconObj.set({ width: 10, height: 10 }); }); it('should increase when dragging to the right-down from the starting point', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 500, y: 500 }); icon._onFabricMouseMove(fEvent); expect(iconObj).toMatchObject({ scaleX: 40, scaleY: 40 }); }); it('should increase when dragging to the left-up from the starting point', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 100, y: 100 }); icon._onFabricMouseMove(fEvent); expect(iconObj).toMatchObject({ scaleX: 40, scaleY: 40 }); }); }); it('should insert the activated icon object on canvas', () => { icon.add('arrow'); const activeObj = canvas.getActiveObject(); expect(activeObj).not.toBeNull(); }); it('should insert the icon object on center of canvas image', () => { const centerPos = icon.getCanvasImage().getCenterPoint(); icon.add('arrow'); const { left, top, strokeWidth } = canvas.getActiveObject(); const halfStrokeWidth = strokeWidth / 2; expect({ x: left + halfStrokeWidth, y: top + halfStrokeWidth }).toEqual(centerPos); }); it('should create the arrow icon when parameter value is arrow', () => { const path = icon._pathMap.arrow; const createIconSpy = jest.spyOn(icon, '_createIcon').mockReturnValue(new fabric.Object({})); icon.add('arrow'); expect(createIconSpy).toHaveBeenCalledWith(path); }); it('should create the cancel icon when parameter value is cancel', () => { const path = icon._pathMap.cancel; const createIconSpy = jest.spyOn(icon, '_createIcon').mockReturnValue(new fabric.Object({})); icon.add('cancel'); expect(createIconSpy).toHaveBeenCalledWith(path); }); it('should change color of next inserted icon', () => { const color = '#ffffff'; icon.add('arrow'); expect(canvas.getActiveObject().fill).not.toBe(color); icon.setColor(color); icon.add('cancel'); expect(canvas.getActiveObject().fill).toBe(color); }); }); ================================================ FILE: apps/image-editor/tests/imageEditor.spec.js ================================================ import { fabric } from 'fabric'; import ImageEditor from '@/imageEditor'; import * as util from '@/util'; import { eventNames, keyCodes } from '@/consts'; const { OBJECT_ROTATED } = eventNames; describe('ImageEditor', () => { describe('constructor', () => { let imageEditor, el, sendHostNameSpy; beforeEach(() => { el = document.createElement('div'); imageEditor = new ImageEditor(el, { usageStatistics: false }); sendHostNameSpy = jest.spyOn(util, 'sendHostName'); }); afterEach(() => { imageEditor.destroy(); }); it('should send hostname by default', () => { imageEditor = new ImageEditor(el); expect(sendHostNameSpy).toHaveBeenCalled(); }); it('should not send hostname on usageStatistics option false', () => { imageEditor = new ImageEditor(el, { usageStatistics: false }); expect(sendHostNameSpy).not.toHaveBeenCalled(); }); it('should not be executed when object is selected state', () => { const preventDefaultSpy = jest.fn(); jest.spyOn(imageEditor._graphics, 'getActiveObject').mockReturnValue(null); imageEditor._onKeyDown({ keyCode: keyCodes.BACKSPACE, preventDefault: preventDefaultSpy }); expect(preventDefaultSpy).not.toHaveBeenCalled(); }); it('should be fire at object is rotated', () => { const canvas = imageEditor._graphics.getCanvas(); const obj = new fabric.Object({}); canvas.add(obj); imageEditor.fire = jest.fn(); canvas.fire('object:rotating', { target: obj }); expect(imageEditor.fire).toHaveBeenCalledWith(OBJECT_ROTATED, expect.any(Object)); }); }); }); ================================================ FILE: apps/image-editor/tests/index.js ================================================ import { fabric } from 'fabric'; fabric.Object.prototype.objectCaching = false; ================================================ FILE: apps/image-editor/tests/invoker.spec.js ================================================ import Invoker from '@/invoker'; import Command from '@/interface/command'; describe('Invoker', () => { let invoker, cmd; beforeEach(() => { invoker = new Invoker(); cmd = new Command({ execute: jest.fn(() => Promise.resolve()), undo: jest.fn(() => Promise.resolve()), }); }); it('should call "command.execute" again', async () => { await invoker.execute(cmd); await invoker.undo(); await invoker.redo(); expect(cmd.execute).toHaveBeenCalledTimes(2); }); it('should call the "command.executeCallback" after invoke', async () => { const callbackSpy = jest.fn(); cmd.setExecuteCallback(callbackSpy); await invoker.execute(cmd); expect(callbackSpy).toHaveBeenCalled(); }); it('should call the "command.undoCallback" after undo', async () => { const callbackSpy = jest.fn(); cmd.setUndoCallback(callbackSpy); await invoker.execute(cmd); await invoker.undo(); expect(callbackSpy).toHaveBeenCalled(); }); describe('invoker.customEvents', () => { let spyEvents; beforeEach(() => { spyEvents = { undoStackChanged: jest.fn(), redoStackChanged: jest.fn(), }; }); it('should fire a event when redoStack is empty before', async () => { invoker.on(spyEvents); await invoker.execute(cmd); expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1); expect(spyEvents.redoStackChanged).not.toHaveBeenCalled(); }); it('should fire events when redoStack is not empty before', async () => { invoker.pushRedoStack({}); invoker.on(spyEvents); await invoker.execute(cmd); expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1); expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(0); }); it('should fire redo event when undoStack is not empty after', async () => { await invoker.execute(cmd); await invoker.execute(cmd); invoker.on(spyEvents); await invoker.undo(); expect(spyEvents.undoStackChanged).not.toHaveBeenCalled(); expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(1); }); it('should fire undo event when undoStack is empty after', async () => { await invoker.execute(cmd); invoker.on(spyEvents); await invoker.undo(); expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(0); expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(1); }); it('should fire undo event when redoStack is not empty after', async () => { await invoker.execute(cmd); await invoker.execute(cmd); await invoker.undo(); await invoker.undo(); invoker.on(spyEvents); await invoker.redo(); expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1); expect(spyEvents.redoStackChanged).not.toHaveBeenCalled(); }); it('should fire redo event when undoStack is empty after', async () => { await invoker.execute(cmd); await invoker.undo(); invoker.on(spyEvents); await invoker.redo(); expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1); expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(0); }); }); }); ================================================ FILE: apps/image-editor/tests/line.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Line from '@/component/line'; import { eventNames } from '@/consts'; describe('Line', () => { let canvas, graphics, mockImage, line, fEvent; beforeEach(() => { graphics = new Graphics(document.createElement('canvas')); canvas = graphics.getCanvas(); jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 30, y: 60 }); line = new Line(graphics); line._line = new fabric.Line([10, 20, 10, 20]); mockImage = new fabric.Image(); graphics.setCanvasImage('mockImage', mockImage); fEvent = { e: {} }; }); afterEach(() => { canvas.forEachObject((obj) => { canvas.remove(obj); }); }); it('should insert the line', () => { line._onFabricMouseDown(fEvent); expect(canvas.getObjects()).toHaveLength(1); }); it('should draw line located by mouse pointer', () => { canvas.add(line._line); const [object] = canvas.getObjects(); expect(object).toMatchObject({ x2: 10, y2: 20 }); line._onFabricMouseMove(fEvent); expect(object).toMatchObject({ x2: 30, y2: 60 }); }); it('should restore all drawing objects activated', () => { const path = new fabric.Path(); canvas.add(path); const [object] = canvas.getObjects(); line.start(); expect(object.evented).toBe(false); line.end(); expect(object.evented).toBe(true); }); it('should fire after the line is drawn', () => { line.fire = jest.fn(); line._onFabricMouseUp(fEvent); expect(line.fire).toHaveBeenCalledWith(eventNames.OBJECT_ADDED, expect.any(Object)); }); }); ================================================ FILE: apps/image-editor/tests/promiseApi.spec.js ================================================ import { fabric } from 'fabric'; import ImageEditor from '@/imageEditor'; import { stamp } from '@/util'; import { rejectMessages } from '@/consts'; import '@/command/loadImage'; import '@/command/addIcon'; import '@/command/clearObjects'; import '@/command/changeIconColor'; import '@/command/addShape'; import '@/command/changeShape'; import '@/command/addImageObject'; import '@/command/flip'; import '@/command/rotate'; import '@/command/removeObject'; import '@/command/setObjectProperties'; import '@/command/setObjectPosition'; import img from 'fixtures/sampleImage.jpg'; describe('Promise API', () => { let imageEditor, canvas, activeObjectId; beforeAll(() => { imageEditor = new ImageEditor(document.createElement('div'), { cssMaxWidth: 700, cssMaxHeight: 500, }); canvas = imageEditor._graphics.getCanvas(); imageEditor.on('objectActivated', ({ id }) => { activeObjectId = id; }); imageEditor.on('objectAdded', ({ id }) => { activeObjectId = id; }); }); afterAll(() => { imageEditor.destroy(); }); beforeEach(async () => { const image = new fabric.Image(img); await imageEditor.loadImageFromURL(image, 'sampleImage'); }); it('should support Promise(addIcon)', async () => { await imageEditor.addIcon('arrow', { left: 10, top: 10 }); expect(canvas.getObjects()).toHaveLength(1); }); it('should support Promise(clearObjects)', async () => { await imageEditor.addIcon('arrow', { left: 10, top: 10 }); await imageEditor.clearObjects(); expect(canvas.getObjects()).toHaveLength(0); }); it('should support Promise(changeIconColor)', async () => { await imageEditor.addIcon('arrow', { left: 10, top: 10 }); await imageEditor.changeIconColor(activeObjectId, '#FFFF00'); const [object] = canvas.getObjects(); expect(object).toMatchObject({ fill: '#FFFF00' }); }); it('should support Promise(addShape)', async () => { await imageEditor.addShape('rect', { width: 100, height: 100, fill: '#FFFF00' }); const [object] = canvas.getObjects(); expect(object).toMatchObject({ type: 'rect', width: 100, height: 100, fill: '#FFFF00' }); }); it('should support Promise(changeShape)', async () => { await imageEditor.addShape('rect', { width: 100, height: 100, fill: '#FFFF00' }); await imageEditor.changeShape(activeObjectId, { type: 'triangle', width: 200, fill: '#FF0000', }); const [object] = canvas.getObjects(); expect(object).toMatchObject({ type: 'triangle', width: 200, fill: '#FF0000' }); }); it('should catch on failure when object is not in canvas', async () => { await imageEditor.addShape('rect', { width: 100, height: 100, fill: '#FFFF00' }); imageEditor.deactivateAll(); await expect( imageEditor.changeShape(null, { type: 'triangle', width: 200, fill: '#FF0000' }) ).rejects.toBe(rejectMessages.noObject); }); it('should support Promise(addImageObject)', async () => { imageEditor._graphics.addImageObject = jest.fn(() => { canvas.add(new fabric.Object()); return Promise.resolve({ id: activeObjectId }); }); const objectProps = await imageEditor.addImageObject('fixtures/mask.png'); expect(canvas.getObjects()).toHaveLength(1); expect(objectProps.id).toBe(activeObjectId); }); it('should support Promise(undo)', async () => { await imageEditor.addShape('rect', { width: 100, height: 100, fill: '#FFFF00' }); await imageEditor.undo(); expect(canvas.getObjects()).toHaveLength(0); }); it('should support Promise(flipX)', async () => { const obj = await imageEditor.flipX(); expect(obj).toEqual({ flipX: true, flipY: false, angle: 0 }); }); it('should support Promise(flipY)', async () => { const obj = await imageEditor.flipY(); expect(obj).toEqual({ flipX: false, flipY: true, angle: 0 }); }); it('should support Promise(resetFlip)', async () => { await expect(imageEditor.resetFlip()).rejects.toBe(rejectMessages.flip); }); it('should support Promise(rotate)', async () => { const angle = await imageEditor.rotate(10); expect(angle).toBe(10); }); it('should support Promise(setAngle)', async () => { const angle = await imageEditor.setAngle(10); expect(angle).toBe(10); }); it('should support Promise(removeObject)', async () => { const objectProps = await imageEditor.addShape('rect', { width: 100, height: 100 }); await imageEditor.removeObject(objectProps.id); expect(canvas.getObjects()).toHaveLength(0); }); describe('Watermark', () => { const properties = { fill: 'rgba(255, 255, 0, 0.5)', left: 150, top: 30 }; beforeEach(async () => { imageEditor._graphics.addImageObject = jest.fn(() => { const obj = new fabric.Object({ width: 1, height: 1, strokeWidth: 0 }); canvas.add(obj); activeObjectId = stamp(obj); return Promise.resolve({ id: activeObjectId }); }); await imageEditor.addImageObject('fixtures/mask.png'); }); it("should return object's properties", async () => { await imageEditor.setObjectProperties(activeObjectId, properties); const propKeys = { fill: null, left: null, top: null }; const result = imageEditor.getObjectProperties(activeObjectId, propKeys); expect(result).not.toBeNull(); expect(result).toEqual(properties); }); it('should return null if there is no object', async () => { await imageEditor.setObjectProperties(activeObjectId, properties); const propKeys = { fill: null, width: null, left: null, top: null, height: null }; imageEditor.deactivateAll(); const result = imageEditor.getObjectProperties(null, propKeys); expect(result).toBeNull(); }); it("should return object's properties with object's keys", async () => { await imageEditor.setObjectProperties(activeObjectId, properties); const keys = ['fill', 'width', 'left', 'top', 'height']; const result = imageEditor.getObjectProperties(activeObjectId, keys); expect(result).not.toBeNull(); keys.forEach((key) => expect(result).toHaveProperty(key)); }); it("should return object's property with string keys", async () => { await imageEditor.setObjectProperties(activeObjectId, properties); const result = imageEditor.getObjectProperties(activeObjectId, 'fill'); expect(result).not.toBeNull(); expect(result).toEqual({ fill: properties.fill }); }); it("should return canvas's width, height", () => { expect(imageEditor.getCanvasSize()).toEqual({ width: expect.any(Number), height: expect.any(Number), }); }); it('should return global point by origin', () => { const keys = ['left', 'top', 'width', 'height']; const ltPoint = imageEditor.getObjectPosition(activeObjectId, 'left', 'top'); const ccPoint = imageEditor.getObjectPosition(activeObjectId, 'center', 'center'); const rbPoint = imageEditor.getObjectPosition(activeObjectId, 'right', 'bottom'); const { left, top, width, height } = imageEditor.getObjectProperties(activeObjectId, keys); expect(ltPoint).toMatchObject({ x: left, y: top }); expect(ccPoint).toMatchObject({ x: width / 2, y: height / 2 }); expect(rbPoint).toMatchObject({ x: left + width, y: top + height }); }); it('should set object position by origin', async () => { await imageEditor.setObjectProperties(activeObjectId, { width: 200, height: 100 }); await imageEditor.setObjectPosition(activeObjectId, { x: 0, y: 0, originX: 'left', originY: 'top', }); const result = imageEditor.getObjectProperties(activeObjectId, ['left', 'top']); expect(result).toMatchObject({ left: 100, top: 50 }); }); }); }); ================================================ FILE: apps/image-editor/tests/resize.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Resize from '@/component/resize'; describe('Resize', () => { let graphics, resize, mockImage; beforeAll(() => { graphics = new Graphics(document.createElement('canvas')); resize = new Resize(graphics); }); beforeEach(() => { mockImage = new fabric.Image(null, { width: 100, height: 100 }); graphics.setCanvasImage('mockImage', mockImage); }); it('should return current image dimensions', () => { let currentDimensions = resize.getCurrentDimensions(); expect(currentDimensions).toEqual({ width: 100, height: 100 }); const newDimensions = { width: 20, height: 20 }; resize.resize(newDimensions); currentDimensions = resize.getCurrentDimensions(); expect(newDimensions).toEqual(currentDimensions); }); it('should return original image dimensions after resizing', () => { const originalDimensionsBeforeResizing = resize.getOriginalDimensions(); const newDimensions = { width: 20, height: 20 }; resize.resize(newDimensions); const originalDimensionsAfterResizing = resize.getOriginalDimensions(); expect(originalDimensionsBeforeResizing).toEqual(originalDimensionsAfterResizing); }); it('should set original dimensions', () => { const newDimensions = { width: 20, height: 20 }; resize.setOriginalDimensions(newDimensions); const originalDimensions = resize.getOriginalDimensions(); expect(newDimensions).toEqual(originalDimensions); }); it('should resize image', () => { const originalDimensions = resize.getOriginalDimensions(); const newDimensions = { width: 20, height: 20 }; resize.resize(newDimensions); let currentDimensions = resize.getCurrentDimensions(); expect(newDimensions).toEqual(currentDimensions); resize.resize(originalDimensions); currentDimensions = resize.getCurrentDimensions(); expect(originalDimensions).toEqual(currentDimensions); }); it('should set original dimensions when drawing mode is started', () => { resize.setOriginalDimensions(null); resize.start(); expect(resize.getOriginalDimensions()).not.toBeNull(); }); it('should have end method', () => { expect(typeof resize.end === 'function').toBe(true); }); it('should return promise', async () => { const newDimensions = { width: 20, height: 20 }; const obj = await resize.resize(newDimensions); expect(obj).toBeUndefined(); }); }); ================================================ FILE: apps/image-editor/tests/rotation.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Rotation from '@/component/rotation'; describe('Rotation', () => { let graphics, rotation, mockImage, canvas; beforeAll(() => { graphics = new Graphics(document.createElement('canvas')); canvas = graphics.getCanvas(); rotation = new Rotation(graphics); }); beforeEach(() => { mockImage = new fabric.Image(); graphics.setCanvasImage('mockImage', mockImage); }); it('should return current angle', () => { mockImage.angle = 30; expect(rotation.getCurrentAngle()).toEqual(30); }); it('should set angle', () => { rotation.setAngle(40); expect(rotation.getCurrentAngle()).toEqual(40); }); it('should add angle', () => { let angle = rotation.getCurrentAngle(); rotation.rotate(10); expect(rotation.getCurrentAngle()).toBe(angle + 10); angle = rotation.getCurrentAngle(); rotation.rotate(20); expect(rotation.getCurrentAngle()).toBe(angle + 20); }); it('should add angle modular 360(===2*PI)', async () => { await rotation.setAngle(10); await rotation.rotate(380); expect(rotation.getCurrentAngle()).toBe(30); }); it('should set canvas dimension from image-rect', () => { jest.spyOn(mockImage, 'getBoundingRect').mockReturnValue({ width: 100, height: 110 }); rotation.adjustCanvasDimension(); expect(canvas.getWidth()).toBe(100); expect(canvas.getHeight()).toBe(110); }); }); ================================================ FILE: apps/image-editor/tests/selectionModifyHelper.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import { setCachedUndoDataForDimension, getCachedUndoDataForDimension, makeSelectionUndoData, makeSelectionUndoDatum, } from '@/helper/selectionModifyHelper'; describe('selectionModifyHelper', () => { let graphics, obj1, obj2; const rectOption = { width: 10, height: 10, top: 10, left: 10, scaleX: 1, scaleY: 1, angle: 0, }; beforeEach(() => { graphics = new Graphics(document.createElement('canvas')); obj1 = new fabric.Rect(rectOption); obj2 = new fabric.Rect(rectOption); }); it('should set/get cached undo data', () => { const undoData = [{ id: 1 }]; setCachedUndoDataForDimension(undoData); expect(getCachedUndoDataForDimension()).toEqual(undoData); }); describe('makeSelectionUndoData', () => { it('should make object undo data', () => { const result = makeSelectionUndoData(obj1, (obj) => obj); expect(result).toEqual([obj1]); }); it('should make selection undo data', () => { const selection = graphics.getActiveSelectionFromObjects([obj1, obj2]); const result = makeSelectionUndoData(selection, (obj) => obj); expect(result).toEqual([obj1, obj2]); }); }); describe('makeSelectionUndoDatum', () => { it('should return undo datum', () => { const result = makeSelectionUndoDatum(1, obj1, true); expect(result).toEqual({ id: 1, width: obj1.width, height: obj1.height, top: obj1.top, left: obj1.left, angle: obj1.angle, scaleX: obj1.scaleX, scaleY: obj1.scaleY, }); }); }); }); ================================================ FILE: apps/image-editor/tests/shape.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Shape from '@/component/shape'; import { resize } from '@/helper/shapeResizeHelper'; import { getFillImageFromShape, getCachedCanvasImageElement } from '@/helper/shapeFilterFillHelper'; describe('Shape', () => { let canvas, graphics, mockImage, fEvent, shape, shapeObj; beforeAll(() => { graphics = new Graphics(document.createElement('canvas')); canvas = graphics.getCanvas(); shape = new Shape(graphics); }); beforeEach(() => { mockImage = new fabric.Image(); graphics.setCanvasImage('mockImage', mockImage); fEvent = { e: {} }; }); afterEach(() => { canvas.forEachObject((obj) => { canvas.remove(obj); }); }); it('should be calculated correctly.', () => { const pointer = canvas.getPointer(fEvent.e); const settings = { strokeWidth: 0, type: 'rect', left: 150, top: 200, width: 40, height: 40, originX: 'center', originY: 'center', }; shape.add('rect', settings); [shapeObj] = canvas.getObjects(); const setSpy = jest.spyOn(shapeObj, 'set'); resize(shapeObj, pointer); expect(setSpy).toHaveBeenCalledWith( expect.objectContaining({ left: settings.left - settings.width / 2, top: settings.top - settings.height / 2, }) ); }); it('should be created on canvas(rect)', () => { shape.add('rect'); [shapeObj] = canvas.getObjects(); expect(shapeObj.get('type')).toBe('rect'); }); it('should be created on canvas(circle)', () => { shape.add('circle'); [shapeObj] = canvas.getObjects(); expect(shapeObj.get('type')).toBe('circle'); }); it('should be created on canvas(triangle)', () => { shape.add('triangle'); [shapeObj] = canvas.getObjects(); expect(shapeObj.type).toBe('triangle'); }); it('should be set the rectangle object when add() is called with no options', () => { shape.add('rect'); [shapeObj] = canvas.getObjects(); expect(shapeObj).toMatchObject({ width: 1, height: 1 }); // strokeWidth: 1, width: 1, height: 1 }); it('should be set the circle object when add() is called with no options', () => { shape.add('circle'); [shapeObj] = canvas.getObjects(); expect(shapeObj).toMatchObject({ width: 0, height: 0 }); }); it('should be set the triangle object when add() is called with no options', () => { shape.add('triangle'); [shapeObj] = canvas.getObjects(); expect(shapeObj).toMatchObject({ width: 1, height: 1 }); // strokeWidth: 1, width: 1, height: 1 }); it('should be set the rectangle object when add() is called with the options', () => { const settings = { fill: 'blue', stroke: 'red', strokeWidth: 10, type: 'rect', width: 100, height: 100, }; shape.add('rect', settings); [shapeObj] = canvas.getObjects(); expect(shapeObj).toMatchObject(settings); }); it('should be set the circle object when add() is called with the options', () => { const settings = { fill: 'blue', stroke: 'red', strokeWidth: 3, type: 'circle', rx: 100, ry: 50, }; shape.add('circle', settings); [shapeObj] = canvas.getObjects(); expect(shapeObj).toMatchObject(settings); }); it('should be set the triangle object when add() is called with the options', () => { const settings = { fill: 'blue', stroke: 'red', strokeWidth: 0, type: 'triangle', width: 100, height: 100, }; shape.add('triangle', settings); [shapeObj] = canvas.getObjects(); expect(shapeObj).toMatchObject(settings); }); it('should be changed when change() is called(rect)', () => { const settings = { fill: 'blue', stroke: 'red', width: 10, height: 20 }; shape.add('rect'); [shapeObj] = canvas.getObjects(); shape.change(shapeObj, settings); expect(shapeObj).toMatchObject(settings); }); it('should be changed when change() is called(circle)', () => { const settings = { fill: 'blue', stroke: 'red', rx: 10, ry: 20 }; shape.add('circle'); [shapeObj] = canvas.getObjects(); shape.change(shapeObj, settings); expect(shapeObj).toMatchObject(settings); }); it('should be changed when change() is called(triangle)', () => { const settings = { fill: 'blue', stroke: 'red', width: 10, height: 20 }; shape.add('triangle'); [shapeObj] = canvas.getObjects(); shape.change(shapeObj, settings); expect(shapeObj).toMatchObject(settings); }); describe('Fill - filter type', () => { beforeEach(() => { getCachedCanvasImageElement(canvas, true); mockImage = new fabric.Image(); graphics.setCanvasImage('mockImage', mockImage); shape.add('rect', { strokeWidth: 0, left: 20, top: 30, width: 100, height: 80, fill: { type: 'filter', filter: [{ pixelate: 20 }], }, }); [shapeObj] = canvas.getObjects(); }); it('should be executed when a movement, rotation, and scaling event of a filter type fill is applied', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 10, y: 10 }); const _resetPositionFillFilterSpy = jest.spyOn(shape, '_resetPositionFillFilter'); shapeObj.fire('moving'); shapeObj.fire('rotating'); shapeObj.fire('scaling'); expect(_resetPositionFillFilterSpy).toHaveBeenCalledTimes(3); }); it('should be changed cropX and cropY values of the image filled with the shape background', () => { shape._resetPositionFillFilter(shapeObj); const fillImage = getFillImageFromShape(shapeObj); expect(fillImage).toMatchObject({ cropX: -30, cropY: -10 }); }); it('should be the same size as the shape', () => { shape._resetPositionFillFilter(shapeObj); const fillImage = getFillImageFromShape(shapeObj); expect(fillImage).toMatchObject({ width: 100, height: 80 }); }); it('should be the same size as the rectangle that draws the rotated object border', () => { shapeObj.set({ angle: 40 }); shape._resetPositionFillFilter(shapeObj); const fillImage = getFillImageFromShape(shapeObj); expect(fillImage).toMatchSnapshot(); }); it('should have the shape reverse rotation value if repositioning is performed while the angle is changed', () => { shapeObj.set({ angle: 40 }); shape._resetPositionFillFilter(shapeObj); const { angle } = getFillImageFromShape(shapeObj); expect(angle).toBe(-40); }); it('should give the expected result for shapes that go outside the bottom right area of the canvas', async () => { const obj = await shape.add('rect', { strokeWidth: 0, left: 250, top: 100, width: 200, height: 200, fill: { type: 'filter', filter: [{ pixelate: 20 }], }, }); shapeObj = graphics.getObject(obj.id); const fillImage = getFillImageFromShape(shapeObj); expect(fillImage).toMatchObject({ top: 75, left: 75, width: 150, height: 150 }); }); it('should give the expected result for shapes that go outside the top left area of the canvas', async () => { const obj = await shape.add('rect', { strokeWidth: 0, left: 50, top: 30, width: 200, height: 70, fill: { type: 'filter', filter: [{ pixelate: 20 }], }, }); shapeObj = graphics.getObject(obj.id); const fillImage = getFillImageFromShape(shapeObj); expect(fillImage).toMatchSnapshot(); }); it('should have the applied filter', () => { const fillImage = getFillImageFromShape(shapeObj); expect(fillImage.filters).not.toHaveLength(0); }); }); describe('_onFabricMouseMove()', () => { beforeEach(() => { shape.add('rect', { left: 100, top: 100 }); [shapeObj] = canvas.getObjects(); shape._shapeObj = shapeObj; }); it('should be set to "left" and "top" when the mouse direction is in 1th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 200, y: 120 }); shape._onFabricMouseMove(fEvent); expect(shapeObj).toMatchObject({ originX: 'left', originY: 'top' }); }); it('should be set to "right" and "top" when the mouse direction is in 2th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 80, y: 100 }); shape._onFabricMouseMove(fEvent); expect(shapeObj).toMatchObject({ originX: 'right', originY: 'top' }); }); it('should be set to "right" and "bottom" when the mouse direction is in 3th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 80, y: 80 }); shape._onFabricMouseMove(fEvent); expect(shapeObj).toMatchObject({ originX: 'right', originY: 'bottom' }); }); it('should be set to "left" and "bottom" when the mouse direction is in 4th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 200, y: 80 }); shape._onFabricMouseMove(fEvent); expect(shapeObj).toMatchObject({ originX: 'left', originY: 'bottom' }); }); }); describe('_onFabricMouseUp()', () => { let startPoint; beforeEach(() => { shape.add('circle', { left: 100, top: 100 }); [shapeObj] = canvas.getObjects(); shape._shapeObj = shapeObj; }); it('should be the same as start point when the drawing shape is in 1th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 200, y: 120 }); startPoint = shapeObj.getPointByOrigin('left', 'top'); shape._onFabricMouseMove(fEvent); shape._onFabricMouseUp(); expect(shapeObj.getPointByOrigin('left', 'top')).toEqual(startPoint); }); it('should be the same as start point when the drawing shape is in 2th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 80, y: 120 }); startPoint = shapeObj.getPointByOrigin('right', 'top'); shape._onFabricMouseMove(fEvent); shape._onFabricMouseUp(); expect(shapeObj.getPointByOrigin('right', 'top')).toEqual(startPoint); }); it('should be the same as start point when the drawing shape is in 3th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 80, y: 80 }); startPoint = shapeObj.getPointByOrigin('right', 'bottom'); shape._onFabricMouseMove(fEvent); shape._onFabricMouseUp(); expect(shapeObj.getPointByOrigin('right', 'bottom')).toEqual(startPoint); }); it('should be the same as start point when the drawing shape is in 4th quadrant', () => { jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 120, y: 80 }); startPoint = shapeObj.getPointByOrigin('left', 'bottom'); shape._onFabricMouseMove(fEvent); shape._onFabricMouseUp(); expect(shapeObj.getPointByOrigin('left', 'bottom')).toEqual(startPoint); }); }); it('should have the same "width" and "height" values when drawing the shape with mouse and the "isRegular" option set to true(x-axis)', () => { shape.add('rect', { left: 0, top: 0 }); shape._withShiftKey = true; [shapeObj] = canvas.getObjects(); shape._shapeObj = shapeObj; jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 200, y: 100 }); shape._onFabricMouseMove(fEvent); shape._onFabricMouseUp(); expect(shapeObj).toMatchObject({ width: 200, height: 200 }); }); it('should have the same "width" and "height" values when drawing the shape with mouse and the "isRegular" option set to true(y-axis)', () => { shape.add('rect', { left: 0, top: 0 }); shape._withShiftKey = true; [shapeObj] = canvas.getObjects(); shape._shapeObj = shapeObj; jest.spyOn(canvas, 'getPointer').mockReturnValue({ x: 100, y: 200 }); shape._onFabricMouseMove(fEvent); shape._onFabricMouseUp(); expect(shapeObj).toMatchObject({ width: 200, height: 200 }); }); }); ================================================ FILE: apps/image-editor/tests/text.spec.js ================================================ import { fabric } from 'fabric'; import Graphics from '@/graphics'; import Text from '@/component/text'; describe('Text', () => { let canvas, graphics, mockImage, text; beforeAll(() => { graphics = new Graphics(document.createElement('canvas')); canvas = graphics.getCanvas(); text = new Text(graphics); }); beforeEach(() => { mockImage = new fabric.Image(); graphics.setCanvasImage('mockImage', mockImage); }); afterEach(() => { canvas.forEachObject((obj) => { canvas.remove(obj); }); }); describe('add()', () => { let activeObj; beforeEach(() => { text.add('', {}); activeObj = canvas.getActiveObject(); }); it('should make the blank text object when text parameter is empty string', () => { const newText = activeObj.text; expect(newText).toBe(''); }); it('should make the text object set default option when parameter has not "styles" property', () => { const newTextStyle = activeObj.fontWeight; expect(newTextStyle).toBe('normal'); }); it('should create the text object on center of canvas when parameter has not "position" property', () => { const { x, y } = mockImage.getCenterPoint(); expect(activeObj).toMatchObject({ left: x, top: y }); }); it('should be true when adding text', async () => { const info = await text.add('default', {}); const newText = graphics.getObject(info.id); expect(newText.selectionStart).toBe(0); expect(newText.selectionEnd).toBe(7); expect(newText.isEditing).toBe(true); }); }); it('should maintain consistent left and top positions after entering and exiting drawing mode', () => { const left = 10; const top = 20; const newText = new fabric.IText('testString', { left, top, width: 30, height: 50, angle: 40, originX: 'center', originY: 'center', }); text.useItext = true; canvas.add(newText); text.start(); text.end(); expect(newText).toMatchSnapshot(); }); it('should change contents in the text object as input', () => { text.add('text123', {}); const activeObj = canvas.getActiveObject(); text.change(activeObj, 'abc'); expect(activeObj.text).toBe('abc'); text.change(activeObj, 'def'); expect(activeObj.text).toBe('def'); }); describe('setStyle()', () => { beforeEach(() => { text.add('new text', { styles: { fontWeight: 'bold' } }); }); it('should unlock style when a selected style already apply on the activated text object', () => { const activeObj = canvas.getActiveObject(); text.setStyle(activeObj, { fontWeight: 'bold' }); expect(activeObj.fontWeight).not.toBe('bold'); }); it('should apply style when the activated text object has not a selected style', () => { const activeObj = canvas.getActiveObject(); text.setStyle(activeObj, { fontStyle: 'italic' }); expect(activeObj.fontStyle).toBe('italic'); }); }); it('should change size of selected text object', () => { const obj = new fabric.Text('test'); const scale = 10; const { fontSize } = obj; text.start({}); canvas.add(obj); obj.scaleY = scale; canvas.fire('object:scaling', { target: obj }); expect(obj.fontSize).toBe(fontSize * scale); }); }); ================================================ FILE: apps/image-editor/tests/theme.spec.js ================================================ import Theme from '@/ui/theme/theme'; import defaultTheme from '@/ui/theme/standard'; describe('Theme', () => { let theme; beforeEach(() => { theme = new Theme(defaultTheme); }); describe('getStyle()', () => { it('should have "path" and "name" when the user sets the icon file location', () => { const userIconPath = 'fixtures/icon-d.svg'; const userIconName = 'icon-d'; const themeForIconPathSet = new Theme({ ...defaultTheme, 'menu.normalIcon.path': userIconPath, 'menu.normalIcon.name': userIconName, }); const { normal: { path, name }, } = themeForIconPathSet.getStyle('menu.icon'); expect(path).toBe(userIconPath); expect(name).toBe(userIconName); }); it('should return default icon color information', () => { const { normal, active, disabled, hover } = theme.getStyle('menu.icon'); expect(normal.color).toBe('#8a8a8a'); expect(active.color).toBe('#555555'); expect(disabled.color).toBe('#434343'); expect(hover.color).toBe('#e9e9e9'); }); it('should return cssText in normal types', () => { theme.styles.normal = { backgroundColor: '#fdba3b', border: '1px solid #fdba3b', color: '#fff', fontFamily: 'NotoSans, sans-serif', fontSize: '12px', }; expect(theme.getStyle('normal')).toMatchSnapshot(); }); it('should return cssText if all members are objects', () => { theme.styles['submenu.normalLabel'] = { color: '#858585', fontWeight: 'normal', }; theme.styles['submenu.activeLabel'] = { color: '#000', fontWeight: 'normal', }; expect(theme.getStyle('submenu.label')).toMatchSnapshot(); }); }); describe('_makeCssText()', () => { it('should return the cssText of the expected value for the object', () => { const styleObject = { backgroundColor: '#fff', backgroundImage: './img/bg.png', border: '1px solid #ddd', color: '#222', fontFamily: 'NotoSans, sans-serif', fontSize: '12px', }; expect(theme._makeCssText(styleObject)).toMatchSnapshot(); }); }); describe('_makeSvgItem()', () => { it('should create path prefix and use-default class when using the default icon', () => { const useTagString = theme._makeSvgItem(['normal'], 'crop'); expect(useTagString).toMatchSnapshot(); }); it('should create a svg path with the prefix when set the icon file', () => { const themeForIconPathSet = new Theme({ ...defaultTheme, 'menu.normalIcon.path': 'fixtures/icon-d.svg', 'menu.normalIcon.name': 'icon-d', }); const useTagString = themeForIconPathSet._makeSvgItem(['normal'], 'crop'); expect(useTagString).toMatchSnapshot(); }); }); }); ================================================ FILE: apps/image-editor/tests/types/tsconfig.json ================================================ { "compilerOptions": { "noEmit": true, "noImplicitAny": false }, "include": [ "../../index.d.ts", "./type-tests.ts"] } ================================================ FILE: apps/image-editor/tests/types/type-tests.ts ================================================ import ImageEditor = require('tui-image-editor'); const blackTheme = { 'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png', 'common.bisize.width': '251px', 'common.bisize.height': '21px', 'common.backgroundImage': 'none', 'common.backgroundColor': '#1e1e1e', 'common.border': '0px', // header 'header.backgroundImage': 'none', 'header.backgroundColor': 'transparent', 'header.border': '0px', // load button 'loadButton.backgroundColor': '#fff', 'loadButton.border': '1px solid #ddd', 'loadButton.color': '#222', 'loadButton.fontFamily': 'NotoSans, sans-serif', 'loadButton.fontSize': '12px', // download button 'downloadButton.backgroundColor': '#fdba3b', 'downloadButton.border': '1px solid #fdba3b', 'downloadButton.color': '#fff', 'downloadButton.fontFamily': 'NotoSans, sans-serif', 'downloadButton.fontSize': '12px', // main icons 'menu.normalIcon.path': '../dist/svg/icon-b.svg', 'menu.normalIcon.name': 'icon-b', 'menu.activeIcon.path': '../dist/svg/icon-a.svg', 'menu.activeIcon.name': 'icon-a', 'menu.iconSize.width': '24px', 'menu.iconSize.height': '24px', // submenu primary color 'submenu.backgroundColor': '#1e1e1e', 'submenu.partition.color': '#858585', // submenu icons 'submenu.normalIcon.path': '../dist/svg/icon-a.svg', 'submenu.normalIcon.name': 'icon-a', 'submenu.activeIcon.path': '../dist/svg/icon-c.svg', 'submenu.activeIcon.name': 'icon-c', 'submenu.iconSize.width': '32px', 'submenu.iconSize.height': '32px', // submenu labels 'submenu.normalLabel.color': '#858585', 'submenu.normalLabel.fontWeight': 'lighter', 'submenu.activeLabel.color': '#fff', 'submenu.activeLabel.fontWeight': 'lighter', // checkbox style 'checkbox.border': '1px solid #ccc', 'checkbox.backgroundColor': '#fff', // rango style 'range.pointer.color': '#fff', 'range.bar.color': '#666', 'range.subbar.color': '#d1d1d1', 'range.value.color': '#fff', 'range.value.fontWeight': 'lighter', 'range.value.fontSize': '11px', 'range.value.border': '1px solid #353535', 'range.value.backgroundColor': '#151515', 'range.title.color': '#fff', 'range.title.fontWeight': 'lighter', // colorpicker style 'colorpicker.button.border': '1px solid #1e1e1e', 'colorpicker.title.color': '#fff', }; const imageEditor = new ImageEditor('#container', { includeUI: { loadImage: { path: 'img/sampleImage.jpg', name: 'SampleImage', }, theme: blackTheme, menu: ['shape', 'filter'], initMenu: 'filter', uiSize: { width: '1000px', height: '700px', }, menuBarPosition: 'bottom', }, cssMaxWidth: 700, cssMaxHeight: 500, selectionStyle: { cornerSize: 20, rotatingPointOffset: 70, }, }); imageEditor.addIcon('arrow'); imageEditor .addIcon('cancel', { left: 100, top: 100, }) .then((objectProps) => { console.log(objectProps.id); }); imageEditor.addImageObject('path/fileName.jpg').then((objectProps) => { console.log(objectProps); }); imageEditor.addShape('rect', { fill: 'red', stroke: 'blue', strokeWidth: 3, width: 100, height: 200, left: 10, top: 10, isRegular: true, }); imageEditor .addShape('circle', { fill: 'red', stroke: 'blue', strokeWidth: 3, rx: 10, ry: 100, isRegular: false, }) .then((objectProps) => { console.log(objectProps.id); }); imageEditor .addText('initText', { styles: { fill: '#000', fontSize: 20, fontWeight: 'bold', }, position: { x: 10, y: 10, }, }) .then((objectProps) => { console.log(objectProps.id); }); imageEditor.applyFilter('Grayscale'); imageEditor .applyFilter('mask', { maskObjId: 0, }) .then((obj) => { console.log(`filterType: ${obj.type}`); console.log(`actType: ${obj.action}`); }); imageEditor.changeCursor('crosshair'); imageEditor.changeIconColor(0, '#000000'); imageEditor.changeSelectableAll(false); imageEditor.changeShape(0, { fill: 'red', stroke: 'blue', strokeWidth: 3, rx: 10, ry: 100, }); imageEditor.changeText(0, 'change text'); imageEditor.changeTextStyle(0, { fontStyle: 'italic', }); imageEditor.clearObjects(); imageEditor.clearRedoStack(); imageEditor.clearUndoStack(); imageEditor.crop(imageEditor.getCropzoneRect()); imageEditor.deactivateAll(); imageEditor.destroy(); imageEditor.discardSelection(); imageEditor .flipX() .then((status) => { console.log(`flipX: ${status.flipX}`); console.log(`flipY: ${status.flipY}`); console.log(`angle: ${status.angle}`); }) .catch((message) => { console.log(`error: ${message}`); }); imageEditor.flipY(); imageEditor.getCanvasSize(); imageEditor.getCropzoneRect(); imageEditor.getDrawingMode(); imageEditor.getImageName(); imageEditor.getObjectPosition(0, 'left', 'top'); imageEditor.getObjectProperties(0, 'left'); imageEditor.getObjectProperties(0, ['left', 'top', 'width', 'height']); imageEditor.getObjectProperties(0, { left: null, top: null, height: null, opacity: null, }); imageEditor.hasFilter('filterType'); imageEditor.isEmptyRedoStack(); imageEditor.isEmptyUndoStack(); let fileObj: any; imageEditor.loadImageFromFile(fileObj, 'SampleImage').then((result) => { console.log(`old: ${result.oldWidth}, ${result.oldHeight}`); console.log(`new: ${result.newWidth}, ${result.newHeight}`); }); imageEditor.loadImageFromURL('http://url/testImage.png', 'lena').then((result) => { console.log(`old: ${result.oldWidth}, ${result.oldHeight}`); console.log(`new: ${result.newWidth}, ${result.newHeight}`); }); imageEditor.redo(); imageEditor.registerIcons({ customIcon: 'M 0 0 L 20 20 L 10 10 Z', customArrow: 'M 60 0 L 120 60 H 90 L 75 45 V 180 H 45 V 45 L 30 60 H 0 Z', }); imageEditor.removeActiveObject(); imageEditor .removeFilter('Grayscale') .then((obj) => { console.log(`filterType: ${obj.type}`); console.log(`actType: ${obj.action}`); }) .catch((message) => { console.log(`error : ${message}`); }); imageEditor.removeObject(0); imageEditor.resetFlip().then((status) => { console.log(`filpX : ${status.flipX}`); console.log(`flipY : ${status.flipY}`); console.log(`angle : ${status.angle}`); }); imageEditor.resizeCanvasDimension({ width: 300, height: 300, }); imageEditor.rotate(10); imageEditor.setAngle(45); imageEditor.setBrush({ width: 12, color: 'rgba(0, 0, 0, 0.5)', }); imageEditor.setBrush({ width: 20, color: '#FFFFFF', }); imageEditor.setCropzoneRect(1 / 1); imageEditor.setDrawingShape('rect', { fill: 'red', width: 100, height: 200, }); imageEditor.setDrawingShape('circle', { rx: 10, ry: 10, isRegular: true, }); imageEditor.setObjectPosition(0, { x: 0, y: 0, originX: 'left', originY: 'top', }); imageEditor .setObjectProperties(0, { left: 100, top: 100, width: 200, height: 200, opacity: 0.5, }) .then((arg) => { console.log(arg); }); imageEditor .setObjectPropertiesQuietly(0, { left: 100, top: 100, width: 200, height: 200, opacity: 0.5, }) .then((arg) => { console.log(arg); }); imageEditor.startDrawingMode('FREE_DRWARING', { width: 10, color: 'rgba(255, 0, 0, 0.5)', }); imageEditor.stopDrawingMode(); imageEditor.toDataURL(); imageEditor.undo(); imageEditor.on('addText', (pos) => { imageEditor.addText('Double Click', { position: pos.originPosition, }); console.log(`text position on canvas : ${pos.originPosition}`); console.log(`text position on browser : ${pos.clientPosition}`); }); imageEditor.ui.resizeEditor({ uiSize: { width: '600px', height: '1200px' } }); imageEditor.ui.resizeEditor({ imageSize: { newWidth: 300, newHeight: 140 } }); ================================================ FILE: apps/image-editor/tests/ui.spec.js ================================================ import UI from '@/ui'; import { HELP_MENUS } from '@/consts'; describe('UI', () => { let ui, options; beforeEach(() => { options = { loadImage: { path: 'mockImagePath', name: '' }, menu: ['resize', 'crop', 'flip', 'rotate', 'draw', 'shape', 'icon', 'text', 'mask', 'filter'], initMenu: 'shape', menuBarPosition: 'bottom', }; ui = new UI(document.createElement('div'), options, {}); }); describe('Destroy()', () => { it('should be executed for all menu instances', () => { const spies = []; options.menu.forEach((menu) => { spies.push(jest.spyOn(ui[menu], 'destroy')); }); ui._destroyAllMenu(); spies.forEach((spy) => { expect(spy).toHaveBeenCalled(); }); }); it('should execute "removeEventListener" for all menus', () => { const allUiButtonElementName = [...options.menu, ...HELP_MENUS]; allUiButtonElementName.forEach((element) => { jest.spyOn(ui._buttonElements[element], 'removeEventListener'); }); ui._removeUiEvent(); allUiButtonElementName.forEach((element) => { expect(ui._buttonElements[element].removeEventListener).toHaveBeenCalled(); }); }); }); describe('_changeMenu()', () => { it('should execute when the menu changes', () => { ui.submenu = 'shape'; jest.spyOn(ui, 'resizeEditor'); ui.shape.changeStandbyMode = jest.fn(); jest.spyOn(ui.filter, 'changeStartMode'); ui._actions.main = { changeSelectableAll: jest.fn() }; ui.resizeEditor = jest.fn(); ui._changeMenu('filter', false, false); expect(ui.shape.changeStandbyMode).toHaveBeenCalled(); expect(ui.filter.changeStartMode).toHaveBeenCalled(); }); }); describe('_makeSubMenu()', () => { it('should execute for the number of menus specified in the option.', () => { const makeMenuElementSpy = jest.spyOn(ui, '_makeMenuElement'); ui._makeSubMenu(); expect(makeMenuElementSpy).toHaveBeenCalledTimes(options.menu.length); }); it('should create instance of the menu specified in the option', () => { jest.spyOn(ui, '_makeMenuElement'); const getConstructorName = (constructor) => constructor.toString().match(/^class (.+?) /)[1]; ui._makeSubMenu(); options.menu.forEach((menu) => { const constructorNameOfInstance = getConstructorName(ui[menu].constructor); const expected = menu.replace(/^[a-z]/, ($0) => $0.toUpperCase()); expect(constructorNameOfInstance).toBe(expected); }); }); }); describe('initCanvas()', () => { beforeEach(() => { ui._editorElement = { querySelector: jest.fn(() => document.createElement('div')), }; ui._actions.main = { initLoadImage: jest.fn(() => Promise.resolve()), }; }); it('should be run as required when initCanvas is executed', async () => { ui.activeMenuEvent = jest.fn(); const addLoadEventSpy = jest.spyOn(ui, '_addLoadEvent'); await ui.initCanvas(); expect(addLoadEventSpy).toHaveBeenCalled(); }); it('should not be run when has not image path', () => { jest.spyOn(ui, '_getLoadImage').mockReturnValue({ path: '' }); ui.initCanvas(); expect(ui._actions.main.initLoadImage).not.toHaveBeenCalled(); }); it('should be executed even if there is no image path', () => { jest.spyOn(ui, '_getLoadImage').mockReturnValue({ path: '' }); jest.spyOn(ui, '_addLoadEvent'); ui.initCanvas(); expect(ui._addLoadEvent).toHaveBeenCalled(); }); }); describe('_setEditorPosition()', () => { beforeEach(() => { ui._editorElement = document.createElement('div'); jest.spyOn(ui, '_getCanvasMaxDimension').mockReturnValue({ width: 300, height: 300 }); }); it('should be reflected in the bottom of the editor position', () => { ui.submenu = true; ui._setEditorPosition('bottom'); expect(ui._editorElement.style).toMatchObject({ top: '150px', left: '0px' }); }); it('should be reflected in the top of the editor position', () => { ui.submenu = true; ui._setEditorPosition('top'); expect(ui._editorElement.style).toMatchObject({ top: '-150px', left: '0px' }); }); it('should be reflected in the left, right of the editor position', () => { ui.submenu = true; ui._setEditorPosition('left'); expect(ui._editorElement.style).toMatchObject({ top: '0px', left: '-150px' }); }); it('should be reflected in the right of the editor position', () => { ui.submenu = true; ui._setEditorPosition('right'); expect(ui._editorElement.style).toMatchObject({ top: '0px', left: '150px' }); }); }); }); ================================================ FILE: apps/image-editor/tests/uiRange.spec.js ================================================ import Range from '@/ui/tools/range'; import { defaultRotateRangeValues } from '@/consts'; describe('Range', () => { let range, input, slider; beforeEach(() => { input = document.createElement('input'); slider = document.createElement('div'); range = new Range({ slider, input }, defaultRotateRangeValues); }); it('should be incremented by one when keyCode 38 is found in the event handler with changeInputWithArrow', () => { const ev = { target: input, keyCode: 38 }; input.value = '3'; range.eventHandler.changeInputWithArrow(ev); expect(range.value).toBe(4); }); it('should be decremented by one when keyCode 40 is found in the event handler with changeInputWithArrow', () => { const ev = { target: input, keyCode: 40 }; input.value = '3'; range.eventHandler.changeInputWithArrow(ev); expect(range.value).toBe(2); }); it('should filter out any invalid input values', () => { const ev = { target: input, keyCode: 83, preventDefault: jest.fn() }; input.value = '-3!!6s0s'; range.eventHandler.changeInput(ev); expect(range.value).toBe(0); }); }); ================================================ FILE: apps/image-editor/tests/zoom.spec.js ================================================ import { fabric } from 'fabric'; import ImageEditor from '@/imageEditor'; import '@/command/loadImage'; import img from 'fixtures/sampleImage.jpg'; describe('Zoom', () => { let imageEditor, x, y, zoomLevel; beforeEach(async () => { imageEditor = new ImageEditor(document.createElement('div'), { cssMaxWidth: 700, cssMaxHeight: 500, }); const image = new fabric.Image(img); await imageEditor.loadImageFromURL(image, 'sampleImage'); x = 0; y = 0; zoomLevel = 1.0; }); afterEach(() => { imageEditor.destroy(); }); it('should change zoom of image', () => { zoomLevel += 1; imageEditor.zoom({ x, y, zoomLevel }); const canvas = imageEditor._graphics.getCanvas(); expect(canvas.getZoom()).toBe(zoomLevel); }); it('should reset zoom of image', () => { zoomLevel += 1; imageEditor.zoom({ x, y, zoomLevel }); imageEditor.resetZoom(); const canvas = imageEditor._graphics.getCanvas(); expect(canvas.getZoom()).toBe(1.0); }); }); ================================================ FILE: apps/image-editor/tsBannerGenerator.js ================================================ /*eslint-disable*/ var fs = require('fs'); var path = require('path'); var rootPkg = require('../../package.json'); var pkg = require('./package.json'); var tsVersion = /[0-9.]+/.exec(rootPkg.devDependencies.typescript)[0]; var declareFilePath = path.join(__dirname, 'index.d.ts'); var declareRows = []; var TS_BANNER = [ '// Type definitions for TOAST UI Image Editor v' + pkg.version, '// TypeScript Version: ' + tsVersion, ].join('\n'); fs.readFile(declareFilePath, 'utf8', function (error, data) { if (error) { throw error; } declareRows = data.toString().split('\n'); declareRows.splice(0, 2, TS_BANNER); fs.writeFile(declareFilePath, declareRows.join('\n'), 'utf8', function (error) { if (error) { throw error; } console.log('Completed Write Banner for Typescript!'); }); }); ================================================ FILE: apps/image-editor/tuidoc.config.json ================================================ { "header": { "logo": { "src": "https://uicdn.toast.com/toastui/img/tui-image-editor-bi-white.png" }, "title": { "text": "repo", "linkUrl": "https://github.com/nhn/tui.image-editor" } }, "footer": [ { "title": "NHN Cloud", "linkUrl": "https://github.com/nhn" }, { "title": "FE Development Lab", "linkUrl": "https://ui.toast.com/" } ], "main": { "filePath": "README.md" }, "api": { "filePath": "src/js/*.js", "fileLink": true, "permalink": false }, "examples": { "filePath": "examples", "titles": { "example01-includeUi": "1. Include ui", "example02-useApiDirect": "2. Use api direct (basic)", "example03-mobile": "3. Mobile" }, "globalErrorLogVariable": "errorLogs" }, "pathPrefix": "tui.image-editor" } ================================================ FILE: apps/image-editor/webpack.common.config.js ================================================ /* eslint-disable */ const path = require('path'); const ESLintPlugin = require('eslint-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = ({ minify, WEBPACK_BUILD }) => ({ entry: './src/index.js', output: { library: { export: 'default', type: 'umd', name: ['tui', 'ImageEditor'], }, path: path.resolve('dist'), publicPath: '/dist', filename: `tui-image-editor${minify ? '.min' : ''}.js`, }, resolve: { alias: { '@': path.resolve('src/js'), '@css': path.resolve('src/css'), '@svg': path.resolve('src/svg'), }, }, externals: [ { 'tui-code-snippet': { commonjs: 'tui-code-snippet', commonjs2: 'tui-code-snippet', amd: 'tui-code-snippet', root: ['tui', 'util'], }, 'tui-color-picker': { commonjs: 'tui-color-picker', commonjs2: 'tui-color-picker', amd: 'tui-color-picker', root: ['tui', 'colorPicker'], }, }, ], module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', options: { rootMode: 'upward', }, }, { test: /\.styl$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader'], }, { test: /\.svg$/, type: 'asset/inline', }, ], }, plugins: [ new ESLintPlugin({ extensions: ['js'], failOnError: WEBPACK_BUILD, }), new MiniCssExtractPlugin({ filename: `tui-image-editor${minify ? '.min' : ''}.css`, }), ], }); ================================================ FILE: apps/image-editor/webpack.config.js ================================================ /* eslint-disable */ const { merge } = require('webpack-merge'); const commonWebpackConfig = require('./webpack.common.config'); const prodWebpackConfig = require('./webpack.prod.config'); const devWebpackConfig = require('./webpack.dev.config'); module.exports = (env) => { const isProduction = env.WEBPACK_BUILD; const commonConfig = commonWebpackConfig(env); return merge(commonConfig, isProduction ? prodWebpackConfig(env) : devWebpackConfig()); }; ================================================ FILE: apps/image-editor/webpack.dev.config.js ================================================ /* eslint-disable */ module.exports = () => ({ mode: 'development', devServer: { compress: true, open: true, hot: true, host: '0.0.0.0', static: './examples', allowedHosts: 'all', }, devtool: 'eval-source-map', }); ================================================ FILE: apps/image-editor/webpack.prod.config.js ================================================ /* eslint-disable */ const { version, license } = require('./package.json'); const webpack = require('webpack'); const TerserPlugin = require('terser-webpack-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); module.exports = ({ minify }) => { const productionConfig = { mode: 'production', plugins: [ new webpack.BannerPlugin({ banner: ['TOAST UI ImageEditor', `@version ${version}`, `@license ${license}`].join('\n'), }), ], optimization: { minimize: false, }, }; if (minify) { productionConfig.optimization = { minimize: true, minimizer: [new TerserPlugin({ extractComments: false }), new CssMinimizerPlugin()], }; } return productionConfig; }; ================================================ FILE: apps/react-image-editor/.babelrc.json ================================================ { "extends": "../../babel.config.json", "presets": ["@babel/preset-react"] } ================================================ FILE: apps/react-image-editor/.eslintrc.js ================================================ module.exports = { extends: ['tui/es6', 'plugin:react/recommended', 'plugin:prettier/recommended'], plugins: ['react', 'prettier'], parser: '@babel/eslint-parser', parserOptions: { babelOptions: { presets: ['@babel/preset-react'], }, ecmaVersion: 7, sourceType: 'module', ecmaFeatures: { jsx: true, }, }, ignorePatterns: ['node_modules/*', 'dist'], rules: { 'react/prop-types': 0, }, settings: { react: { version: 'detect', }, }, }; ================================================ FILE: apps/react-image-editor/.storybook/main.js ================================================ module.exports = { core: { builder: 'webpack5', }, stories: ['../stories/*.stories.js'], }; ================================================ FILE: apps/react-image-editor/.storybook/preview.js ================================================ import 'tui-image-editor/dist/tui-image-editor.css'; import 'tui-color-picker/dist/tui-color-picker.css'; ================================================ FILE: apps/react-image-editor/README.md ================================================ # TOAST UI Image Editor for React > This is a React component wrapping [TOAST UI Image Editor](https://github.com/nhn/tui.image-editor). [![npm version](https://img.shields.io/npm/v/@toast-ui/react-image-editor.svg)](https://www.npmjs.com/package/@toast-ui/react-image-editor) ## 🚩 Table of Contents - [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source) - [Install](#-install) - [Using npm](#using-npm) - [Usage](#-usage) - [Import](#Import) - [Props](#props) - [Instance Methods](#Instance-Methods) - [Getting the root element](#Getting-the-root-element) - [Events](#events) ## Collect statistics on the use of open source React Wrapper of TOAST UI Image Editor applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Image Editor is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. > “ui.toast.com") is to be collected and the sole purpose is nothing but to measure statistics on the usage. To disable GA, use the `usageStatistics` props like the example below. ```js ``` Or, include `tui-code-snippet.js` (**v1.4.0** or **later**) and then immediately write the options as follows: ```js tui.usageStatistics = false; ``` ## 💾 Install ### Using npm ```sh npm install --save @toast-ui/react-image-editor ``` ## 📊 Usage ### Import You can use Toast UI Image Editor for React as a ECMAScript module or a CommonJS module. As this module does not contain CSS files, you should import `tui-image-editor.css` from `tui-image-editor/dist` manually. - Using ECMAScript module ```js import 'tui-image-editor/dist/tui-image-editor.css'; import ImageEditor from '@toast-ui/react-image-editor'; ``` - Using CommonJS module ```js require('tui-image-editor/dist/tui-image-editor.css'); const ImageEditor = require('@toast-ui/react-image-editor'); ``` ### Props [All the options of the TOAST UI Image Editor](http://nhn.github.io/tui.image-editor/latest/ImageEditor) are supported in the form of props. ```js const myTheme = { // Theme object to extends default dark theme. }; const MyComponent = () => ( ); ``` #### Theme Importing `black/white-theme.js` file is not working directly import yet. You want to use a white theme, please write own theme object by copy and paste. ### Instance Methods For using [instance methods of TOAST UI Image Editor](https://nhn.github.io/tui.date-picker/latest/DatePicker#createCalendar), first thing to do is creating Refs of wrapper component using [`createRef()`](https://reactjs.org/docs/refs-and-the-dom#creating-refs). But the wrapper component does not provide a way to call instance methods of TOAST UI Image Editor directly. Instead, you can call `getInstance()` method of the wrapper component to get the instance, and call the methods on it. ```js const imageEditorOptions = { // sort of option properties. }; class MyComponent extends React.Component { editorRef = React.createRef(); handleClickButton = () => { const editorInstance = this.editorRef.current.getInstance(); editorInstance.flipX(); }; render() { return ( <> ); } } ``` ### Getting the root element An instance of the wrapper component also provides a handy method for getting the root element. If you want to manipulate the root element directly, you can call `getRootElement` to get the element. ```js class MyComponent extends React.Component { editorRef = React.createRef(); handleClickButton = () => { this.editorRef.current.getRootElement().classList.add('image-editor-root'); }; render() { return ( <> ); } } ``` ### Events [All the events of TOAST UI Image Editor](https://nhn.github.io/tui.image-editor/latest/ImageEditor#event:addText) are supported in the form of `on[EventName]` props. The first letter of each event name should be capitalized. For example, for using `mousedown` event you can use `onMousedown` prop like the example below. ```js class MyComponent extends React.Component { handleMousedown = () => { console.log('Mouse button is downed!'); }; render() { return ; } } ``` ================================================ FILE: apps/react-image-editor/package.json ================================================ { "name": "@toast-ui/react-image-editor", "version": "3.15.3", "description": "TOAST UI Image-Editor for React", "main": "dist/toastui-react-image-editor.js", "files": [ "dist", "src" ], "scripts": { "build": "webpack", "storybook": "start-storybook -s ./node_modules/tui-image-editor/dist/svg,.storybook -p 6006", "build-storybook": "build-storybook" }, "homepage": "https://github.com/nhn/tui.image-editor", "bugs": "https://github.com/nhn/tui.image-editor/issues", "author": "NHN Cloud. FE Development Lab ", "repository": "https://github.com/nhn/tui.image-editor.git", "license": "MIT", "browserslist": [ "last 2 versions", "not ie <= 9" ], "peerDependencies": { "react": "^17.0.2" }, "devDependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" }, "dependencies": { "fabric": "^4.2.0", "tui-image-editor": "^3.15.2" } } ================================================ FILE: apps/react-image-editor/src/index.js ================================================ import React from 'react'; import TuiImageEditor from 'tui-image-editor'; export default class ImageEditor extends React.Component { rootEl = React.createRef(); imageEditorInst = null; componentDidMount() { this.imageEditorInst = new TuiImageEditor(this.rootEl.current, { ...this.props, }); this.bindEventHandlers(this.props); } componentWillUnmount() { this.unbindEventHandlers(); this.imageEditorInst.destroy(); this.imageEditorInst = null; } shouldComponentUpdate(nextProps) { this.bindEventHandlers(this.props, nextProps); return false; } getInstance() { return this.imageEditorInst; } getRootElement() { return this.rootEl.current; } bindEventHandlers(props, prevProps) { Object.keys(props) .filter(this.isEventHandlerKeys) .forEach((key) => { const eventName = key[2].toLowerCase() + key.slice(3); // For if (prevProps && prevProps[key] !== props[key]) { this.imageEditorInst.off(eventName); } this.imageEditorInst.on(eventName, props[key]); }); } unbindEventHandlers() { Object.keys(this.props) .filter(this.isEventHandlerKeys) .forEach((key) => { const eventName = key[2].toLowerCase() + key.slice(3); this.imageEditorInst.off(eventName); }); } isEventHandlerKeys(key) { return /on[A-Z][a-zA-Z]+/.test(key); } render() { return
      ; } } ================================================ FILE: apps/react-image-editor/stories/index.stories.js ================================================ import React from 'react'; import { storiesOf } from '@storybook/react'; import ImageEditor from '../src/index'; const stories = storiesOf('Toast UI ImageEditor', module); const props = { includeUI: { loadImage: { path: 'img/sampleImage2.png', name: 'sampleImage2', }, initMenu: 'shape', uiSize: { height: '700px', width: '1000px', }, }, cssMaxWidth: 700, cssMaxHeight: 500, }; stories.add('Include default UI', () => ); stories.add('Using Method', () => { class Story extends React.Component { ref = React.createRef(); imageEditor = null; componentDidMount() { this.imageEditor = this.ref.current.getInstance(); } flipImageByAxis(isXAxis) { this.imageEditor[isXAxis ? 'flipX' : 'flipY']() .then((status) => { console.log('flipX: ', status.flipX); console.log('flipY: ', status.flipY); console.log('angle: ', status.angle); }) ['catch']((message) => { console.log('error: ', message); }); } render() { return ( <> ); } } return ; }); stories.add('Events', () => { class Story2 extends React.Component { ref = React.createRef(); imageEditor = null; componentDidMount() { this.imageEditor = this.ref.current.getInstance(); } render() { return ( { console.log(event); console.log(originPointer); }} onAddText={(pos) => { const { x: ox, y: oy } = pos.originPosition; const { x: cx, y: cy } = pos.clientPosition; console.log(`text position on canvas(x, y): ${ox}px, ${oy}px`); console.log(`text position on brwoser(x, y): ${cx}px, ${cy}px`); }} /> ); } } return ; }); ================================================ FILE: apps/react-image-editor/webpack.config.js ================================================ /* eslint-disable */ const path = require('path'); const { version, author, license } = require('./package.json'); const webpack = require('webpack'); module.exports = () => ({ mode: 'production', entry: './src/index.js', output: { filename: 'toastui-react-image-editor.js', path: path.resolve(__dirname, 'dist'), library: { type: 'commonjs2' }, }, externals: { 'tui-image-editor': { commonjs: 'tui-image-editor', commonjs2: 'tui-image-editor', }, react: { commonjs: 'react', commonjs2: 'react', }, }, module: { rules: [ { test: /\.js$/, include: path.resolve(__dirname, 'src'), use: { loader: 'babel-loader', }, }, ], }, plugins: [ new webpack.BannerPlugin({ banner: [ 'TOAST UI ImageEditor : React Wrapper', `@version ${version}`, `@author ${author}`, `@license ${license}`, ].join('\n'), }), ], }); ================================================ FILE: apps/vue-image-editor/.eslintrc.js ================================================ module.exports = { extends: ['tui/es6', 'plugin:vue/recommended', 'plugin:prettier/recommended'], plugins: ['vue', 'prettier'], parser: 'vue-eslint-parser', parserOptions: { parser: '@babel/eslint-parser', ecmaVersion: 7, sourceType: 'module', }, ignorePatterns: ['node_modules/*', 'dist'], }; ================================================ FILE: apps/vue-image-editor/.storybook/main.js ================================================ module.exports = { core: { builder: 'webpack5', }, stories: ['../stories/*.stories.js'], }; ================================================ FILE: apps/vue-image-editor/.storybook/preview.js ================================================ import 'tui-image-editor/dist/tui-image-editor.css'; import 'tui-color-picker/dist/tui-color-picker.css'; ================================================ FILE: apps/vue-image-editor/README.md ================================================ # Vue wrapper for TOAST UI Image Editor > This is a Vue component wrapping [TOAST UI Image Editor](https://github.com/nhn/tui.image-editor). [![npm version](https://img.shields.io/npm/v/@toast-ui/vue-image-editor.svg)](https://www.npmjs.com/package/@toast-ui/vue-image-editor) ## 🚩 Table of Contents - [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source) - [Install](#-install) - [Using npm](#using-npm) - [Usage](#-usage) - [Load](#load) - [Implement](#implement) - [Props](#props) - [Events](#events) - [Method](#method) ## Collect statistics on the use of open source TOAST UI ImageEditor applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI ImageEditor is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. > “ui.toast.com") is to be collected and the sole purpose is nothing but to measure statistics on the usage. To disable GA, use the following `usageStatistics` option when creating the instance. ```js const options = { ... usageStatistics: false } const imageEditor = new tui.ImageEditor('#tui-image-editor-container', options); ``` Or, include [`tui-code-snippet`](https://github.com/nhn/tui.code-snippet)(**v1.4.0** or **later**) and then immediately write the options as follows: ```js tui.usageStatistics = false; ``` ## 💾 Install ### Using npm ```sh npm install --save @toast-ui/vue-image-editor ``` > **If you install other packages**, you may lose dependency on fabric. You need to **reinstall the fabric**. ``` npm install --no-save --no-optional fabric@~4.2.0 ``` ## 🔡 Usage ### Load - Using namespace ```js const ImageEditor = toastui.ImageEditor; ``` - Using module ```js // es modules import { ImageEditor } from '@toast-ui/vue-image-editor'; // commonjs require const { ImageEditor } = require('@toast-ui/vue-image-editor'); ``` - Using ` ``` - Using only Vue wrapper component (Single File Component) `toastui-vue-image-editor.js` has all of the tui.ImageEditor. If you only need vue wrapper component, you can use `@toast-ui/vue-image-editor/src/ImageEditor.vue` like this: ```js import ImageEditor from '@toast-ui/vue-image-editor/src/ImageEditor.vue'; ``` ### Implement First insert `` in the template or html. `includeUi` and `options` props are required. ```html ``` Load ImageEditor component and then add it to the `components` in your component or Vue instance. ```js import 'tui-color-picker/dist/tui-color-picker.css'; import 'tui-image-editor/dist/tui-image-editor.css'; import { ImageEditor } from '@toast-ui/vue-image-editor'; export default { components: { 'tui-image-editor': ImageEditor, }, data() { return { useDefaultUI: true, options: { // for tui-image-editor component's "options" prop cssMaxWidth: 700, cssMaxHeight: 500, }, }; }, }; ``` ### Props You can use `includeUi` and `options` props. Example of each props is in the [Getting Started](../../docs/Basic-Tutorial.md). - `includeUi` | Type | Required | Default | | ------- | -------- | ------- | | Boolean | X | true | This prop can contained the `includeUI` prop in the option. You can see the default UI. - `options` | Type | Required | Default | | ------ | -------- | --------------------------------------- | | Object | X | { cssMaxWidth: 700, cssMaxHeight: 500 } | You can configure your image editor using `options` prop. For more information which properties can be set in `options`, see [options of tui.image-editor](https://nhn.github.io/tui.image-editor/latest/ImageEditor). ### Events - addText: The event when 'TEXT' drawing mode is enabled and click non-object area. - mousedown: The mouse down event with position x, y on canvas - objectActivated: The event when object is selected(aka activated). - objectMoved: The event when object is moved. - objectScaled: The event when scale factor is changed. - redoStackChanged: Redo stack changed event - textEditing: The event which starts to edit text object. - undoStackChanged: Undo stack changed event ```html ``` ```js ... methods: { onAddText(pos) { ... }, onObjectMoved(props) { ... } } ... ``` For more information such as the parameters of each event, see [event of tui.image-editor](https://nhn.github.io/tui.image-editor/latest/ImageEditor#event-addText). ### Method For use method, first you need to assign `ref` attribute of element like this: ```html ``` After then, you can use methods through `this.$refs`. We provide `getRootElement` and `invoke` methods. - `getRootElement` You can get root element of image editor using this method. ```js this.$refs.tuiImageEditor.getRootElement(); ``` - `invoke` If you want to more manipulate the ImageEditor, you can use `invoke` method to call the method of tui.ImageEditor. First argument of `invoke` is name of the method and second argument is parameters of the method. To find out what kind of methods are available, see [method of tui.ImageEditor](https://nhn.github.io/tui.image-editor/latest/ImageEditor). ```js const drawingMode = this.$refs.tuiImageEditor.invoke('getDrawingMode'); this.$refs.tuiImageEditor.invoke('loadImageFromURL', './sampleImage.png', 'My sample image'); this.$refs.tuiImageEditor.invoke('startDrawingMode', 'FREE_DRAWING', { width: 10, color: 'rgba(255, 0, 0, 0.5)', }); ``` ================================================ FILE: apps/vue-image-editor/package.json ================================================ { "name": "@toast-ui/vue-image-editor", "version": "3.15.3", "description": "TOAST UI Image-Editor for Vue", "main": "dist/toastui-vue-image-editor.js", "files": [ "dist", "src" ], "scripts": { "build": "webpack", "storybook": "start-storybook -s .storybook -p 6006" }, "homepage": "https://github.com/nhn/tui.image-editor", "bugs": "https://github.com/nhn/tui.image-editor/issues", "author": "NHN Cloud. FE Development Lab ", "repository": "https://github.com/nhn/tui.image-editor.git", "license": "MIT", "browserslist": [ "last 2 versions", "not ie <= 9" ], "peerDependencies": { "vue": "^2.6.14" }, "devDependencies": { "@storybook/vue": "^6.3.12", "vue": "^2.6.14" }, "dependencies": { "fabric": "^4.2.0", "tui-image-editor": "^3.15.2" } } ================================================ FILE: apps/vue-image-editor/src/ImageEditor.vue ================================================ ================================================ FILE: apps/vue-image-editor/src/index.js ================================================ import ImageEditor from './ImageEditor.vue'; export { ImageEditor }; ================================================ FILE: apps/vue-image-editor/stories/index.stories.js ================================================ import { ImageEditor } from '../src/index'; export default { title: 'ImageEditor', }; const options = { includeUI: { loadImage: { path: 'img/sampleImage2.png', name: 'sampleImage2', }, initMenu: 'filter', uiSize: { width: '1000px', height: '700px', }, }, cssMaxWidth: 700, cssMaxHeight: 500, }; export const IncludeUI = () => { return { components: { ImageEditor, }, template: 'test', created() { this.props = { ...options }; }, }; }; ================================================ FILE: apps/vue-image-editor/vue.config.js ================================================ module.exports = { filenameHashing: false, chainWebpack: (config) => { config.module.rule('svg').use('file-loader').options({ name: '[name].[ext]', outputPath: '', }); // https://cli.vuejs.org/guide/troubleshooting.html#symbolic-links-in-node-modules config.resolve.symlinks(false); }, }; ================================================ FILE: apps/vue-image-editor/webpack.config.js ================================================ /* eslint-disable */ const path = require('path'); const { version, author, license } = require('./package.json'); const webpack = require('webpack'); const { VueLoaderPlugin } = require('vue-loader'); module.exports = { mode: 'production', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'toastui-vue-image-editor.js', library: { type: 'commonjs2' }, }, resolve: { alias: { vue: 'vue/dist/vue.js', }, }, externals: { 'tui-image-editor': { commonjs: 'tui-image-editor', commonjs2: 'tui-image-editor', amd: 'tui-image-editor', root: ['tui', 'ImageEditor'], }, }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { rootMode: 'upward', }, }, }, { test: /\.vue$/, loader: 'vue-loader', }, ], }, plugins: [ new VueLoaderPlugin(), new webpack.BannerPlugin({ banner: [ 'TOAST UI ImageEditor : Vue Wrapper', `@version ${version}`, `@author ${author}`, `@license ${license}`, ].join('\n'), }), ], }; ================================================ FILE: babel.config.json ================================================ { "presets": [["@babel/preset-env", { "targets": { "ie": "10" } }]], "plugins": [["@babel/plugin-transform-runtime", { "corejs": 3 }]] } ================================================ FILE: bower.json ================================================ { "name": "tui-image-editor", "authors": ["NHN Cloud. FE Dev Lab "], "license": "MIT", "main": ["dist/tui-image-editor.js"], "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests", "src", "server", "data.js", "Gruntfile.js", "gulpfile.js", "conf.json", "package.json", ".gitignore", "samples", "index.js", "jsdoc.conf.json", "webpack.*.js" ], "dependencies": { "fabric": "4.2.0", "tui-code-snippet": "^1.5.0", "tui-color-picker": "^2.2.0" }, "devDependencies": { "tui-component-colorpicker": "~1.0.1", "filesaver": "*" }, "resolutions": { "tui-code-snippet": "^1.5.0", "tui-color-picker": "^2.2.0" } } ================================================ FILE: docs/Apply-Mobile-Version-Image.md ================================================ # Mobile version: image load & save issues ## Load Image #### Issue - You can load photos directly from your mobile device into the image editor, but images with too high a resolution are not suitable for use. - For an action that includes a mouse gesture, such as cropping and drawing in the image editor, the action is determined by the aspect ratio relative to the original image size, so the higher the resolution, the less usable. - Maximum resolution per device * iPhone : `3264 * 2448` * Galaxy4 : `4128 * 3096` (High resolution) / `3264 * 2448` (Normal) / `2048 * 1152` (Low resolution) - The appropriate image size for usability is `3264 * 2448`. If you receive a file upload event when loading an image taken at high resolution on your Android device, do the following. #### How to handle high-resolution image uploads ```html ``` ```js var MAX_RESOLUTION = 3264 * 2448; $('input-image-file').on('change', function (event) { var file; var img; var resolution; if (!supportingFileAPI) { alert('This browser does not support file-api'); } file = event.target.files[0]; if (file) { img = new Image(); img.onload = function () { resolution = this.width * this.height; if (resolution <= MAX_RESOLUTION) { imageEditor.loadImageFromFile(file); } else { alert("Loaded image's resolution is too large!\nRecommended resolution is 3264 * 2448!"); } URL.revokeObjectURL(file); }; img.src = URL.createObjectURL(file); } }); ``` ## Save Image #### Issue - Saving an edited image does not appear in the current sample page, but the actual service must send the file to the server to save the image. - Uses Ajax communication. #### How to Save a Server Image Step 1. Import image data to be saved in the image editor. ```js var dataURL = imageEditor.toDataURL(); ``` Step 2. `base64` encoded image data is Ajax communicated and sent to the server. ```js $.ajax({ type: 'POST', url: serverUrl, data: { imgBase64: dataURL, // Data from Step 1. }, }).done(function () { console.log('saved!'); }); ``` Step 3. The server processes the received data and stores it. - [Using Java](https://sangupta.com/tech/saving-html5-canvas-to-java-server.html) - [Using Php](http://permadi.com/2010/10/html5-saving-canvas-image-data-using-php-and-ajax/) ================================================ FILE: docs/Apply-Mobile-Version.md ================================================ # How to apply the mobile version. ## Image editor How to apply a mobile device - Some settings are required to use Image Editor components on mobile devices. - Please refer to the [sample page](http://nhn.github.io/tui.image-editor/latest/tutorial-example03-mobile.html) first to check the UI configuration and operation. #### Step 1. Include the dependency file on the page. (PC version same) ```html ``` #### Step 2. `body` Add markup to the tag to create an image editor. (PC version same) ```html
      ``` #### Step 3. `head` Add a meta tag for setting the viewport to the tag. ```html ``` #### Step 4. Create an image editor by setting option values for mobile device optimization. ```js // Create image editor var imageEditor = new tui.component.ImageEditor('.tui-image-editor canvas', { cssMaxWidth: document.documentElement.clientWidth, cssMaxHeight: document.documentElement.clientHeight, selectionStyle: { cornerSize: 50, rotatingPointOffset: 100, }, }); ``` - `cssMaxWidth`, `cssMaxHeight` : - Sets maximum `width` and` height` values in the canvas area. - Do not set it to a fixed value like the PC version because the value changes depending on the mobile device to be connected. - `selectionStyle` : - Selection style setting options that are displayed when an object such as an icon, text, etc. is selected. - If the corner size is small, it is difficult to resize and rotate, so set the selection style. - The selection style options are the same as those provided by `fabric.js` and can be set with the following option values: ([Reference](http://fabricjs.com/customization)) ```js var options = { //... selectionStyle: { borderColor: 'red', // Selection line color cornerColor: 'green', // Selection corner color cornerSize: 6, // Selection corner size rotatingPointOffset: 100, // Distance from selection area to rotation corner transparentCorners: false, // Selection corner Transparency }, }; ``` ![2016-08-18 4 52 29](https://cloud.githubusercontent.com/assets/18183560/17766120/86f7c3fc-6564-11e6-86d7-554e8e946843.png) #### Step 5. Add a CSS file and markup for UI configuration. (PC version same) ```html ``` > The CSS file is used on the sample page and should only refer to the UI configuration, > It is recommended to customize image, CSS, and markup files when applying the service. #### Step 6. Apply the image editor API to the UI - API : [http://nhn.github.io/tui.image-editor/latest/](http://nhn.github.io/tui.image-editor/latest/) - Sample Page : [http://nhn.github.io/tui.image-editor/latest/tutorial-example03-mobile.html](http://nhn.github.io/tui.image-editor/latest/tutorial-example01-includeUi.html) ![all_feature_small](https://cloud.githubusercontent.com/assets/18183560/17803706/034ea17c-6633-11e6-914d-6602d12888f9.gif) ![text_feature_small](https://cloud.githubusercontent.com/assets/18183560/17803707/03530636-6633-11e6-8c03-cd5523716b9b.gif) ================================================ FILE: docs/Basic-Tutorial.md ================================================ ## Basic Follow these 3steps to create image-editor. ### 1. Load required files Load first the dependencies, and then load `image-editor.js` or `image-editor.min.js`. ```html ``` ### 2. HTML Markup ImageEditor needs a division element having a canvas element.
      And **the division element must have own (css)height.** ```html
      ``` ### 3. Javascript ImageEditor constructor needs two parameters. - The canvas element selector - Css max width & Css max height - Set the max width according to the size of your page. - The max height should be same the height of the division element (in this example, `#my-image-editor`). ```js // Create image editor var imageEditor = new tui.component.ImageEditor('#my-image-editor canvas', { cssMaxWidth: 1000, // Component default value: 1000 cssMaxHeight: 800, // Component default value: 800 }); // Load image imageEditor.loadImageFromURL('img/sampleImage.jpg', 'My sample image'); ```
      ### 4. Menu, Submenu SVG icon setting In the image below, the red and blue areas are set using the SVG icon. ![svgIcon](https://user-images.githubusercontent.com/35218826/75416627-1ca5e780-5972-11ea-9a55-b179686536de.png) #### Two ways to set the icon 1. **Use default SVG built** into imageEditor without setting SVG file path (Features added since version v3.9.0). - This is the default setting for Image Editor. - It's easy to change the color to match the icon state as shown below, but it uses the built-in default shape so you can't change the icon's appearance. ```js const instance = new ImageEditor(document.querySelector('#tui-image-editor'), { includeUI: { ..., theme: { 'menu.normalIcon.color': '#8a8a8a', 'menu.activeIcon.color': '#555555', 'menu.disabledIcon.color': '#434343', 'menu.hoverIcon.color': '#e9e9e9', 'submenu.normalIcon.color': '#8a8a8a', 'submenu.activeIcon.color': '#e9e9e9', }, ... } }); ``` 2. There is a way to use the **your SVG file** and **set the file location manually**. - This is used when you want to completely reconfigure the SVG icon itself rather than the built-in icon. - The disadvantage is that the color must be set by modifying the SVG file directly. - Need to set the path and name for each icon state as shown below. ```js const instance = new ImageEditor(document.querySelector('#tui-image-editor'), { includeUI: { ..., theme: { 'menu.normalIcon.path': '../dist/svg/icon-d.svg', 'menu.normalIcon.name': 'icon-d', 'menu.activeIcon.path': '../dist/svg/icon-b.svg', 'menu.activeIcon.name': 'icon-b', 'menu.disabledIcon.path': '../dist/svg/icon-a.svg', 'menu.disabledIcon.name': 'icon-a', 'menu.hoverIcon.path': '../dist/svg/icon-c.svg', 'menu.hoverIcon.name': 'icon-c', 'submenu.normalIcon.path': '../dist/svg/icon-a.svg', 'submenu.normalIcon.name': 'icon-a', 'submenu.activeIcon.path': '../dist/svg/icon-c.svg', 'submenu.activeIcon.name': 'icon-c' }, ..., } }); ``` - How to get SVG file sample - In the project folder where `tui-image-editor` is installed, the file is in the path described below ```bash // or use cdn (https://uicdn.toast.com/tui-image-editor/latest/svg/icon-a.svg) $ cd node_modules/tui-image-editor/dist/svg ``` - Or just get the file via cdn. - https://uicdn.toast.com/tui-image-editor/latest/svg/icon-a.svg - https://uicdn.toast.com/tui-image-editor/latest/svg/icon-b.svg - https://uicdn.toast.com/tui-image-editor/latest/svg/icon-c.svg - https://uicdn.toast.com/tui-image-editor/latest/svg/icon-d.svg - Don't forget to use the icon name setting of the `includeUI.theme` option to match the $ {iconName} part of the file. ```svg icon-a.svg file submenu.activeIcon.name <-> iconName ... ... ``` ### 5. Localization ImageEditor provide feature to customize all of inscriptions. Look at example. ```js var locale_ru_RU = { // override default English locale to your custom Crop: 'Обзрезать', // as result default English inscription will be translated into Russian 'Delete-all': 'Удалить всё', // etc... }; // Image editor const instance = new ImageEditor(document.querySelector('#tui-image-editor'), { includeUI: { loadImage: { path: 'img/sampleImage.jpg', name: 'SampleImage', }, locale: locale_ru_RU, // key-value object with localization theme: blackTheme, // or whiteTheme initMenu: 'filter', menuBarPosition: 'bottom', }, cssMaxWidth: 700, cssMaxHeight: 500, selectionStyle: { cornerSize: 20, rotatingPointOffset: 70, }, }); ```
      Full inscriptions list who can be replaced to custom ones: - 3:2 - 4:3 - 5:4 - 7:5 - 16:9 - Apply - Arrow - Arrow-2 - Arrow-3 - Blend - Blur - Bold - Brightness - Bubble - Cancel - Center - Circle - Color - Color Filter - Crop - Custom - Custom icon - Delete - Delete-all - Distance - Download - Draw - Emboss - Fill - Filter - Flip - Flip X - Flip Y - Free - Grayscale - Heart - Icon - Invert - Italic - Left - Load - Load Mask Image - Location - Mask - Multiply - Noise - Pixelate - Polygon - Range - Rectangle - Redo - Remove White - Reset - Right - Rotate - Sepia - Sepia2 - Shape - Sharpen - Square - Star-1 - Star-2 - Straight - Stroke - Text - Text size - Threshold - Tint - Triangle - Underline - Undo - Value ## More.. See the API page and the sample page - API: http://nhn.github.io/tui.image-editor/latest/ - Sample: http://nhn.github.io/tui.image-editor/latest/tutorial-example01-includeUi.html ================================================ FILE: docs/COMMIT_MESSAGE_CONVENTION.md ================================================ # Commit Message Convention The commit messages of the main branch should follow the convention. ## Commit Message Format ``` : short description (fix #1234) Longer description here if necessary BREAKING CHANGE: only contain breaking change ``` - Any line of the commit message cannot be longer 100 characters! ## Revert ``` revert: commit This reverts commit More description if needed ``` ## Type The type is determined by the intention. Must be one of the following: - **feat**: A new feature - **fix**: A bug fix - **docs**: Documentation only changes - **refactor**: A code change that neither fixes a bug nor adds a feature - **perf**: A code change that improves performance - **test**: Adding missing or correcting existing tests - **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation - **env**: Update dependencies, Changes to environment configuration files(package.json, elintrc, babelrc, webpack-config. browserlist, etc) ## Subject - use the imperative, **present** tense: "change" not "changed" nor "changes" - don't capitalize the first letter - no dot (.) at the end - reference GitHub issues at the end. If the commit doesn’t completely fix the issue, then use `(refs #1234)` instead of `(fixes #1234)`. ## Body - use the imperative, **present** tense: "change" not "changed" nor "changes". - the motivation for the change and contrast this with previous behavior. ## BREAKING CHANGE - This commit contains breaking change(s). - start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this. This convention is based on [AngularJS](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) and [ESLint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step2) ================================================ FILE: docs/ISSUE_TEMPLATE.md ================================================ ## Version ## Development Environment ## Current Behavior ```js // Write example code ``` ## Expected Behavior ================================================ FILE: docs/ImageEditor-2.0.0-Migration-guide.md ================================================ There are a lot of changes for ImageEditor 2.0.0 including API changes and new features. This migration document will be nicely moving to v2.0.0. ## New drawing mode change APIs - New APIs - `startDrawingMode(modeName)` starts a drawing mode - `stopDrawingMode()` stops current drawing mode and back to 'NORMAL' mode - `getDrawingMode()` returns current drawing mode name. - `getCropzoneRect()` returns cropping rect in 'CROPPER' drawing mode. - `crop(rect)` crops image given area - Removed APIs - `startCropping`, `endCropping` - `startDrawingShapeMode`, `endDrawingShapeMode` - `startFreeDrawing`, `endFreeDrawing` - `startLineDrawing`, `endLineDrawing` - `startTextMode`, `endTextMode` - `endAll` - `endCropping` is divided into three APIs ```js var rect = imageEditor.getCropzoneRect(); imageEditor.crop(rect).then(() => { imageEditor.stopDrawingMode(); }); ``` ## Changed APIs - `removeActiveObject()` ==> `removeObject(id)` - `getCurrentState()` ==> `getDrawingMode()` ## Use object is with all drawing APIs - In versions prior to 1.4.1, the users should select an object and manipulate it which is called 'active object'. There was no way to manipulate non-selected object. After 2.0.0 version, you can manipulate not only selected object, but also non-selected objects by receiving the Object Id. - To get the Object Id, use the parameter.id in Promise.then() and the event callback. ```js /* { id: number type: type left: number, top: number, width: number, fill: string stroke: string strokeWidth: number opacity: number, // text object text: string, fontFamily: string, fontSize: number, fontStyle: string, fontWeight: string, textAlign: string, textDecoration: string } */ imageEditor.on('objectActivated', function (props) { console.log(props); console.log(props.type); console.log(props.id); }); ``` ```js imageEditor .addShape('circle', { fill: 'red', stroke: 'blue', strokeWidth: 3, rx: 10, ry: 100, isRegular: false, }) .then(function (props) { console.log(props.id); imageEditor.changeShape(props.id, { // change circle fill: '#FFFF00', strokeWidth: 10, }); }); ``` ## Support Promise API - All drawing APIs returns Promise and supports Undo/Redo. - List of related APIs - `addIcon`, `addImageObject`, `addShape`, `changeIconColor` - `changeShape`, `addText`, `changeText`, `changeTextStyle`, - `resizeCanvasDimension`, `applyFilter`, `removeFilter`, - `clearObjects`, `flipX`, `flipY`, `loadImageFromFile`, - `loadImageFromURL`, `redo`, `undo`, `removeObject`, - `resetFlip`, `rotate`, `setAngle`, `crop`, - `setObjectPosition`, `setObjectProperties` ## Changed event type | As-Is | To-Be | Change | Why & Purpose | | ------------------------ | -------------------- | ------------------ | --------------------------------------------------------------------- | | **~~_activateText_~~** | **addText** | renamed | when mousedown event occurs in 'TEXT' drawing mode | | **_~~addObject~~_** | - | removed | unnecessary | | **_~~adjustObject~~_** | **objectMoved** | renamed
      changed | when user drags an object | | **_~~adjustObject~~_** | **objectScaled** | renamed
      changed | when object is being scaled | | ~~applyFilter~~ | - | removed | Replace it to `applyFilter()` Promise API | | ~~clearImage~~ | - | removed | Replace it to `loadImageFromFile()`, `loadImageFromURL()` Promise API | | ~~clearObjects~~ | - | removed | Replace it to `clearObjects()` Promise API | | **_~~editText~~_** | **textEditing** | renamed | when textbox is being edited | | **~~_emptyRedoStack_~~** | **redoStackChanged** | renamed
      changed | Replace it to `redoStackChanged` event with length `0` | | **~~_emptyUndoStack_~~** | **undoStackChanged** | renamed
      changed | Replace it to `undoStackChanged` event with length `0` | | ~~endCropping~~ | - | removed | unnecessary | | ~~endFreeDrawing~~ | - | removed | unnecessary | | ~~endLineDrawing~~ | - | removed | unnecessary | | ~~flipImage~~ | - | removed | Replace it to `flipX()`, `flipY()` Promise API | | ~~loadImage~~ | - | removed | Replace it to `loadImageFromFile()`, `loadImageFromURL()` Promise API | | **mousedown** | **mousedown** | remained | just mousedown | | **_~~pushRedoStack~~_** | **redoStackChanged** | renamed
      changed | redo change event | | **_~~pushUndoStack~~_** | **undoStackChanged** | renamed
      changed | undo change event | | ~~removeObject~~ | - | removed | Replace it to `removeObject()` Promise API | | ~~rotateImage~~ | - | removed | Replace it to `rotate()`, `setAngle()` Promise API | | **_~~selectObject~~_** | **objectActivated** | renamed
      changed | when user selects an object | | ~~startCropping~~ | - | removed | unnecessary | | ~~startFreeDrawing~~ | - | removed | unnecessary | | ~~startLineDrawing~~ | - | removed | unnecessary | ================================================ FILE: docs/PULL_REQUEST_TEMPLATE.md ================================================ ### Please check if the PR fulfills these requirements - [ ] It's submitted to right branch according to our branching model - [ ] It's right issue type on title - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number) - [ ] The commit message follows our guidelines - [ ] Tests for the changes have been added (for bug fixes/features) - [ ] Docs have been added/updated (for bug fixes/features) - [ ] It does not introduce a breaking change or has description for the breaking change ### Description --- Thank you for your contribution to TOAST UI product. 🎉 😘 ✨ ================================================ FILE: docs/Reference.md ================================================ ### Text - Insert text on the canvas and modify text. - Change text color, weight, align so on. > **Implementing to insert text on the canvas using the text palette** - The user can make the specific text palette and edit some text using this palette. - Call custom event API, it can insert text object and control the text palette. * `ImageEditor#activateText` : It occurs when the canvas is clicked. * `ImageEditor#adjustObject` : It occurs when any inserted text object is moved or resized. ```js imageEditor.on('activateText', function (obj) { console.log(obj.type); // Whether the current text object is new or aleady created console.log(obj.text); // Contents of the current text object console.log(obj.styles); // Styles of the current text object console.log(obj.originPosition); // Mouse position on the canvas console.log(obj.clientPosition); // Mouse position on browser - set the text palette's position }); ``` ```js imageEditor.on('adjustObject', function (obj) { console.log(obj.type); // Whether the selected object's type is "text" or others - control the the text palette's view state }); ``` ![image](https://cloud.githubusercontent.com/assets/18183560/16838164/cd200920-4a02-11e6-9c5a-304d1a07d82a.png) ### Icon - Insert the basic icon on the canvas. (type: _arrow_, _cancel_ icon) - Register the custom icon. - Change color of the icon. > **How to draw SVG path** - [Link](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) > **How to get SVG path value when registering the custom icon** - [Link](https://css-tricks.com/using-svg/) ![image](https://cloud.githubusercontent.com/assets/18183560/16838300/726f8a68-4a03-11e6-8703-6d0e36a7f3e3.png) ### Mask Filter - Load the image for using mask filter. (This image is called the "mask image") - When applying mask filter on the canvas image, the canvas image's areas matching the mask image's black areas should be transparent. ![image](https://cloud.githubusercontent.com/assets/18183560/16837578/07444c46-49ff-11e6-99fc-2355a6777dc0.gif) ### Line Drawing - Draw the straight line on the canvas. - Change the color and width value of brush to draw line. ![image](https://cloud.githubusercontent.com/assets/18183560/16837621/4beed348-49ff-11e6-8276-8e0f7e9e85e6.gif) ### Shourtcut - On the canvas * `ctrl + z` : undo * `ctrl + y` : redo - Crop * `shift` : making the cropzone of 1:1 ratio ![image](https://cloud.githubusercontent.com/assets/18183560/16837645/73e7614e-49ff-11e6-9460-e596dd683724.gif) ## More - Get started (Tutorial) : [https://github.com/nhn/tui.component.image-editor/wiki/Tutorial](https://github.com/nhn/tui.component.image-editor/wiki/Tutorial) - API : [http://nhn.github.io/tui.image-editor/latest/](http://nhn.github.io/tui.image-editor/latest/) - Sample : [http://nhn.github.io/tui.image-editor/latest/tutorial-example01-basic.html](http://nhn.github.io/tui.image-editor/latest/tutorial-example01-basic.html) ================================================ FILE: docs/Structure.md ================================================ # Introducation - The image editor includes an implementation that includes the UI via the includeUI option. However, you can express the implementation more freely without using the basic UI. # Modules Internally, it is separated into two major layers. - `ImageEditor` - The object responsible for the API has the having two layers, and communicates directly with the UI. - `Middle Layer` - It is composed of `Invoker`, `Command`, `CommandFactory` which provides the function of the application independent of the drawing operation. - `Graphics Layer` - Generally speaking, a canvas is composed of `Canvas` which consists of functions provided by ImageEditor. Drawing operation is abstracted and the actual implementation uses _fabric.js_. - `Component` is a modularized drawing operation of a specific function and belongs to the Graphics Layer. - Drawing mode is a feature that is essential in the Graphics Layer. ## ImageEditor The object responsible for the API, which has the Invoker and Graphics properties. ## CommandFactory It is a class to register and create `Command`. Provide an interface to register a command and create an instance of the registered command where you need to draw with ImageEditor or Command. - register - Command registration - create - Create Instance of Registered Command ## Command Command is a unit of execution for performing specific functions and is independent of other modules. In the image editor, it is used as an execution unit for Undo / Redo, and the Command instance is managed as a stack in the Invoker.
      Command registration receives objects actions and args that define `name`,` execute`, and `undo`. ### Command Class ```js class Command { constructor(actions, args) { this.name = actions.name; this.args = args; this.execute = actions.execute; this.undo = actions.undo; this.executeCallback = actions.executeCallback || null; this.undoCallback = actions.undoCallback || null; this.undoData = {}; } } ``` #### Command registration process. CommandFactory.register is executed when import. The Command class gets the Component to use and passes the Graphics instance to perform the function. ```js // commandName.js import commandFactory from '../factory/command'; const command = { name: 'commandName', execute(graphics, ...args) {}, undo(graphics, ...args) {}, }; CommandFactory.register(command); module.export = command; ``` #### Command creation and execution. ```js const command = commandFactory.create('commandName', param1, param2); command .execute(...command.args) .then((value) => { // push undo stack return value; }) .catch((message) => { // do something return Promise.reject(message); }); ``` ## Invoker Execute `Command` and manage Undo / Redo. - `Component` list management is removed here. Escalate to the Canvas to be specified below. ## Canvas - As the drawing core module of ImageEditor, we have all of the drawing functions we provide. - The underlying graphics module uses fabric.js. - Below is a list of functions provided by the image editor. | **Icon** | **Image** | **Shape** | **Text** | **Flip** | **FreeDrawing** | **LineDrawing** | **Rotate** | Crop | **기타** | | --------------- | ----------------- | --------------- | --------------- | --------- | --------------- | --------------- | ---------- | --------------- | --------------------- | | addIcon | addImageObject | addShape | addText | flipX | setBrush | setBrush | rotate | crop | resizeCanvasDimension | | registerIcons | loadImageFromFile | setDrawingShape | changeText | flipY | | | setAngle | getCropzoneRect | toDataURL | | changeIconColor | loadImageFromURL | changeShape | changeTextStyle | resetFlip | | | | | getDrawingMode | | | ApplyFilter | | | | | | | | setDrawingMode | | | RemoveFilter | | | | | | | | getImageName | | | hasFilter | | | | | | | | clearObjects | | | | | | | | | | | removeActiveObject | | | | | | | | | | | destroy | | | | | | | | | | | setDefaultPathStyle | ## Component - `Component` is a module that implements a specific drawing operation. - `Component` is a subset of the drawing set, so you can use it through` Canvas`. - `Command` uses `Canvas` to manage various Components. - The event that should be externally transmitted from the Component is passed through the Canvas. The Canvas passes the event back if it is registered outside Canvas. - The component list is shown below, and the components that need to change modes such as start / end are displayed. | Name | Need mode | Usage | | ----------- | --------- | -------------------------------------- | | Cropper | O | Crop module, event handling for Crop. | | filter | X | Image filter module | | flip | X | Image flip Module. | | freeDrawing | O | free drawing module | | icon | X | Add Icon Module | | imageLoader | X | Main image loading module | | line | O | Straight line drawing module | | rotation | X | Main image and objects rotation module | | shape | O | Shape drawing module | | text | O | Text object input module. | # The drawing mode is mutually exclusive, and Command operation is the user's part. - Only one drawing mode should be activated at a time, because the events and UI used for each mode are different. Therefore, the drawing mode is mutually exclusive. - On the other hand, `Command` is a command that defines the drawing operation, so it can be considered to be able to operate regardless of the drawing mode. - Command` is is no dependency on the drawing mode, and the operation is delegated to the user. - The image editor expects you to make the following general API calls: ``` editor.setDrawingMode("cropper"); editor.crop(editor.getCropzoneRect()); editor.setDrawingMode("normal"); ``` ``` editor.setDrawingMode("cropper"); editor.rotate(90); editor.setDrawingMode("normal"); ``` # Event handling - Canvas Layer For modularity, all events that occur inside the canvas are passed through the Canvas. - Events that occur on a Component managed by a Canvas are passed to the Canvas, and Canvas to the outside. - All callbacks that need to be passed to the UI are passed through the ImageEditor. For example, events that need to be imported from Canvas and Component are registered with ImageEditor and registered with Canvas. - If an event from the Canvas needs to be managed by an undo stack, the ImageEditor receives an event from the Canvas and calls the Invoker function. # UI delivery events. - Most events that are passed to the UI are replaced by Promise, which conveys the completion of execution. The undo / redo related events pass events to the UI as they are for state value management. | Name | Purpose | | ---------------- | -------------------------------------------------- | | addText | when mousedown event occurs in 'TEXT' drawing mode | | objectActivated | when user selects an object | | objectMoved | when user drags an object | | objectScaled | when object is being scaled | | textEditing | when textbox is being edited | | mousedown | just mousedown | | undoStackChanged | undo change event | | redoStackChanged | redo change event | ### Example ```js /* { id: number type: type left: number, top: number, width: number, fill: string stroke: string strokeWidth: number opacity: number, // text object text: string, fontFamily: string, fontSize: number, fontStyle: string, fontWeight: string, textAlign: string, textDecoration: string } */ imageEditor.on('objectActivated', function (props) { console.log(props); console.log(props.type); console.log(props.id); }); ``` # Class Diagram ```uml class ServiceUI { -ImageEditor _imageEditor } class ImageEditor { -Invoker _invoker -Graphics _graphics   -void execute(commandName) } package "Middle Layer" #DDDDDD { class Invoker class Command class CommandFactory } together { class Invoker class Command class CommandFactory } package "Graphics Layer" #DDDDDD { class Graphics } package "Component" { class Component class Cropper class Filter class Flip class FreeDrawing class Icon class ImageLoader class Line class Rotation class Shape class Text } package "DrawingMode" { class DrawingMode class CropperDrawingMode class FreeDrawingMode class LineDrawingMode class ShapeDrawingMode class TextDrawingMode } class Invoker { -Array _undoStack -Array _redoStack +Promise execute("commandName") +Promise undo() +Promise redo() +void pushUndoStack(command, isSilent) +void pushRedoStack(command, isSilent) } class CommandFactory { -Map _commands +void register(commandObject) +Command create("commandName", ...args) } class Command { +string name   +Array args +Object _undoData +Promise execute() +Promise undo() +Command setExecuteCallback(callback) +Command setUndoCallback(callback) } class Graphics { -DrawingMode[] _drawingModeInstances -Component[] _components -Fabric.Canvas _canvas +setDrawingMode("modeName") +setDefaultPathStyle(style) +on("eventName", callback) } class Component { } class DrawingMode { } ServiceUI o-- ImageEditor ImageEditor o-- Graphics ImageEditor o-- Invoker ImageEditor <|-- tui.util.CustomEvents ImageEditor --> CommandFactory ImageEditor --> Command Invoker <|-- tui.util.CustomEvents Invoker --> CommandFactory Invoker o-- Command Command o-- Graphics Command o-- Component Graphics o-- DrawingMode Graphics o-- Component Graphics o-- Fabric.Canvas Graphics <|-- tui.util.CustomEvents Component <|-- Cropper Component <|-- Filter Component <|-- Flip Component <|-- FreeDrawing Component <|-- Icon Component <|-- ImageLoader Component <|-- Line Component <|-- Rotation Component <|-- Shape Component <|-- Text Cropper <-- Fabric.Canvas Filter <-- Fabric.Canvas Flip <-- Fabric.Canvas FreeDrawing <-- Fabric.Canvas Icon <-- Fabric.Canvas ImageLoader <-- Fabric.Canvas Line <-- Fabric.Canvas Rotation <-- Fabric.Canvas Shape <-- Fabric.Canvas Text <-- Fabric.Canvas DrawingMode --> Component DrawingMode <|-- CropperDrawingMode DrawingMode <|-- FreeDrawingMode DrawingMode <|-- LineDrawingMode DrawingMode <|-- ShapeDrawingMode DrawingMode <|-- TextDrawingMode CropperDrawingMode <-- Cropper FreeDrawingMode <-- FreeDrawing LineDrawingMode <-- Line ShapeDrawingMode <-- Shape TextDrawingMode <-- Text ``` ================================================ FILE: lerna.json ================================================ { "packages": [ "apps/*" ], "version": "3.15.0" } ================================================ FILE: package.json ================================================ { "name": "root", "private": true, "devDependencies": { "@babel/core": "^7.15.0", "@babel/eslint-parser": "^7.15.0", "@babel/plugin-transform-runtime": "^7.15.0", "@babel/preset-env": "^7.15.0", "@babel/preset-react": "^7.14.5", "@storybook/builder-webpack5": "^6.3.10", "@storybook/cli": "^5.1.11", "@storybook/manager-webpack5": "^6.3.10", "@storybook/react": "^6.3.12", "babel-loader": "^8.2.2", "css-loader": "^6.2.0", "css-minimizer-webpack-plugin": "^3.0.2", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", "eslint-config-tui": "^4.2.0", "eslint-plugin-jest": "^24.4.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.25.1", "eslint-plugin-vue": "^7.17.0", "eslint-webpack-plugin": "^3.0.1", "file-saver": "^2.0.5", "jest": "^27.0.6", "jest-canvas-mock": "^2.3.1", "jest-esm-transformer": "^1.0.0", "lerna": "^4.0.0", "mini-css-extract-plugin": "^2.2.2", "mkdirp": "^1.0.4", "node-fetch": "^3.0.0", "prettier": "^2.0.5", "stylus-loader": "^6.1.0", "svgstore": "*", "terser-webpack-plugin": "^5.2.3", "typescript": "^3.2.2", "vue-eslint-parser": "^7.10.0", "vue-loader": "^15.9.8", "vue-template-compiler": "^2.6.14", "webpack": "^5.52.0", "webpack-cli": "^4.8.0", "webpack-dev-server": "^4.2.0", "webpack-merge": "^5.8.0" }, "dependencies": { "@babel/runtime-corejs3": "^7.15.4" }, "scripts": { "build": "lerna run build", "build:image-editor": "lerna run --scope tui-image-editor build", "build:react": "lerna run --scope @toast-ui/react-image-editor build", "build:vue": "lerna run --scope @toast-ui/vue-image-editor build" }, "workspaces": [ "apps/*" ] }