MinsoftK

add test repo

Showing 354 changed files with 29301 additions and 0 deletions
......@@ -4,6 +4,8 @@
*/
import React from 'react';
import TuiImageEditor from 'tui-image-editor';
import 'tui-image-editor/dist/tui-image-editor.css'
import ImageEditor from '@toast-ui/react-image-editor'
export default class ImageEditor extends React.Component {
rootEl = React.createRef();
......
{
"presets": ["es2015"],
"plugins": [
["transform-es2015-destructuring", { "loose": true }],
["transform-es2015-for-of", { "loose": true }],
["transform-es2015-spread", { "loose": true }]
]
}
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
\ No newline at end of file
# Ignore polyfill
src/js/polyfill.js
module.exports = {
extends: ['tui/es6', 'plugin:prettier/recommended'],
plugins: ['prettier'],
env: {
browser: true,
amd: true,
node: true,
jasmine: true,
jquery: true,
es6: true,
},
globals: {
fabric: true,
tui: true,
loadFixtures: true,
},
parserOptions: {
sourceType: 'module',
},
rules: {
'prefer-destructuring': [
'error',
{
VariableDeclarator: { array: true, object: true },
AssignmentExpression: { array: false, object: false },
},
],
'prettier/prettier': 'error',
},
};
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# 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
# IDEA
.idea
*.iml
# Window
Thumbs.db
Desktop.ini
# MAC
.DS_Store
# SVN
.svn
# eclipse
.project
.metadata
# etc
temp
doc
demo
report
*.vim
test.html
# Compiled files
dist
{
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"quoteProps": "as-needed",
"trailingComma": "es5",
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true,
"proseWrap": "preserve"
}
# 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
# 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.
```
<Type>: Short Description (fix #111)
<Type>: Short Description (fix #123, #111, #122)
<Type>: 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)
<!--
Thank you for your contribution.
When it comes to write an issue, please, use the template below.
To use the template is mandatory for submit new issue and we won't reply the issue that without the template.
And you can write template's contents in Korean also.
-->
<!-- TEMPLATE -->
## Version
<!-- Write the version of the grid you are currently using. -->
## Development Environment
<!-- Write the browser type, OS and so on -->
## Current Behavior
<!-- Write a description of the current operation. You can add example code, 'CodePen' or 'jsfiddle' links. -->
```js
// Write example code
```
## Expected Behavior
<!-- Write a description of the future action. -->
The MIT License
Copyright (c) 2019 NHN 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.
# ![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) [![bower version](https://img.shields.io/bower/v/tui.image-editor.svg)](https://github.com/nhn/tui.image-editor/releases/latest) [![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](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN%20Entertainment-ff1414.svg)](https://github.com/nhn)
## Wrappers
- [toast-ui.vue-image-editor](https://github.com/nhn/toast-ui.vue-image-editor): Vue wrapper component is powered by [NHN](https://github.com/nhn).
- [toast-ui.react-image-editor](https://github.com/nhn/toast-ui.react-image-editor): React wrapper component is powered by [NHN](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
- [Collect statistics on the use of open source](#Collect-statistics-on-the-use-of-open-source)
- [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)
- [Select only the desired function](#select-only-the-desired-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)
- [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)
- [Development](#-development)
- [Setup](#setup)
- [Run webpack-dev-server](#run-webpack-dev-server)
- [Documents](#-documents)
- [Contributing](#-contributing)
- [Dependency](#-dependency)
- [TOAST UI Family](#-toast-ui-family)
- [Used By](#-used-by)
- [License](#-license)
## 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
var options = {
//...
usageStatistics: false,
};
var 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;
```
## 🌏 Browser Support
| <img src="https://user-images.githubusercontent.com/1215767/34348387-a2e64588-ea4d-11e7-8267-a43365103afe.png" alt="Chrome" width="16px" height="16px" /> Chrome | <img src="https://user-images.githubusercontent.com/1215767/34348590-250b3ca2-ea4f-11e7-9efb-da953359321f.png" alt="IE" width="16px" height="16px" /> Internet Explorer | <img src="https://user-images.githubusercontent.com/1215767/34348380-93e77ae8-ea4d-11e7-8696-9a989ddbbbf5.png" alt="Edge" width="16px" height="16px" /> Edge | <img src="https://user-images.githubusercontent.com/1215767/34348394-a981f892-ea4d-11e7-9156-d128d58386b9.png" alt="Safari" width="16px" height="16px" /> Safari | <img src="https://user-images.githubusercontent.com/1215767/34348383-9e7ed492-ea4d-11e7-910c-03b39d52f496.png" alt="Firefox" width="16px" height="16px" /> 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...)
<table>
<tbody>
<tr>
<th width="20%">Crop</th>
<th width="20%">Flip</th>
<th width="20%">Rotation</th>
<th width="20%">Drawing</th>
<th width="20%">Shape</th>
</tr>
<tr>
<td><img src="https://user-images.githubusercontent.com/35218826/40904241-0c28ec68-6815-11e8-8296-89a1716b22d8.png" alt="2018-06-04 4 33 16" style="max-width:100%;"></td>
<td><img src="https://user-images.githubusercontent.com/35218826/40904521-f7c6e184-6815-11e8-8ba3-c94664da69a2.png" alt="2018-06-04 4 40 06" style="max-width:100%;"></td>
<td><img src="https://user-images.githubusercontent.com/35218826/40904664-656aa748-6816-11e8-9943-6607c209deac.png" alt="2018-06-04 4 43 02" style="max-width:100%;"></td>
<td><img src="https://user-images.githubusercontent.com/35218826/40904850-0f26ebde-6817-11e8-97d0-d3a7e4bc02da.png" alt="2018-06-04 4 47 40" style="max-width:100%;"></td>
<td><img src="https://user-images.githubusercontent.com/35218826/40905037-a026296a-6817-11e8-9d28-9e1ca7bc58c4.png" alt="2018-06-04 4 51 45" style="max-width:100%;"></td>
</tr>
<tr>
<th>Icon</th>
<th>Text</th>
<th>Mask</th>
<th>Filter</th>
<th></th>
</tr>
<tr>
<td><img src="https://user-images.githubusercontent.com/35218826/40931205-2d255db6-6865-11e8-98af-ad34c5a01da1.png" alt="2018-06-05 2 06 29" style="max-width:100%;"></td>
<td><img src="https://user-images.githubusercontent.com/35218826/40931484-46253948-6866-11e8-8a04-fa042920e457.png" alt="2018-06-05 2 14 36" style="max-width:100%;"></td>
<td><img src="https://user-images.githubusercontent.com/35218826/40931743-21eeb346-6867-11e8-8e31-a59f7a43482b.png" alt="2018-06-05 2 20 46" style="max-width:100%;"></td>
<td><img src="https://user-images.githubusercontent.com/35218826/40932016-093ed1f4-6868-11e8-8224-a048c3ee8a09.png" alt="2018-06-05 2 27 10" style="max-width:100%;"></td>
<td></td>
</tr>
</tbody>
</table>
### 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) |
### Select only the desired function
```javascripot
var imageEditor = new tui.ImageEditor('#tui-image-editor-container', {
includeUI: {
menu: ['shape', 'crop']
...
},
...
```
## 🙆 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
- Free drawing
- Line drawing
- Shape
- Icon
- Text
- Mask Filter
- Image Filter
## 💾 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 producs 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@<version> # 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#<tag> # Specific version
```
### Via Contents Delivery Network (CDN)
TOAST UI products are available over the CDN powered by [TOAST Cloud](https://www.toast.com).
You can use the CDN as below.
```html
<link
rel="stylesheet"
href="https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.css"
/>
<script src="https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.js"></script>
```
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
<body>
...
<div id="tui-image-editor"></div>
...
</body>
```
### javascript
Add dependencies & initialize ImageEditor class with given element to make an image editor.
```javascript
var ImageEditor = require('tui-image-editor');
var FileSaver = require('file-saver'); //to download edited image to local. Use after npm install file-saver
var blackTheme = require('./js/theme/black-theme.js');
var locale_ru_RU = {
// override default English locale to your custom
Crop: 'Обзрезать',
'Delete-all': 'Удалить всё',
// etc...
};
var 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 ~ UI
```javascript
var ImageEditor = require('tui-image-editor');
var 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 using 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');
var 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 informations.
## 🔧 Development
The TOAST UI products are open-source.
After fixing issues, create a pull request(PR).
Run npm scripts and develop with the following process.
### Setup
Fork `master` branch into your personal repository.
Clone to local computer.
Install node modules.
Before starting development, check for any errors.
```sh
$ git clone https://github.com/{username}/tui.image-editor.git
$ cd tui.image-editor
$ npm install
$ npm run test
```
### Run webpack-dev-server
```sh
$ npm run serve
```
## 📙 Documents
- **Tutorial** : [https://github.com/nhn/tui.image-editor/tree/master/docs](https://github.com/nhn/tui.image-editor/tree/master/docs)
- **Example** : [http://nhn.github.io/tui.image-editor/latest/tutorial-example01-includeUi](http://nhn.github.io/tui.image-editor/latest/tutorial-example01-includeUi)
- **API** : [http://nhn.github.io/tui.image-editor/latest](http://nhn.github.io/tui.image-editor/latest/index)
## 💬 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
- [TOAST 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)
{
"name": "tui-image-editor",
"authors": ["NHN FE Dev Lab <dl_javascript@nhn.com>"],
"license": "MIT",
"main": ["dist/tui-image-editor.js"],
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"src",
"server",
"data.js",
"Gruntfile.js",
"gulpfile.js",
"karma.*.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"
}
}
const fs = require('fs');
const path = require('path');
const config = require(path.resolve(process.cwd(), '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();
.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;
}
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;
}
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;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>0. Design</title>
<link
type="text/css"
href="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css"
rel="stylesheet"
/>
<link type="text/css" href="../dist/tui-image-editor.css" rel="stylesheet" />
<style>
@import url(http://fonts.googleapis.com/css?family=Noto+Sans);
html,
body {
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="tui-image-editor-container"></div>
<script
type="text/javascript"
src="https://api-storage.cloud.toast.com/v1/AUTH_e18353c4ea5746c097143946d0644e61/toast-ui-cdn/tui-image-editor/v3.11.0/example/fabric-v4.2.0.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui.code-snippet/v1.5.0/tui-code-snippet.min.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.js"
></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"
></script>
<script type="text/javascript" src="../dist/tui-image-editor.js"></script>
<script type="text/javascript" src="./js/theme/white-theme.js"></script>
<script type="text/javascript" src="./js/theme/black-theme.js"></script>
<script>
// Image editor
var imageEditor = new tui.ImageEditor('#tui-image-editor-container', {
includeUI: {
loadImage: {
path: 'img/sampleImage2.png',
name: 'SampleImage',
},
theme: blackTheme, // or whiteTheme
initMenu: 'filter',
menuBarPosition: 'bottom',
},
cssMaxWidth: 700,
cssMaxHeight: 500,
usageStatistics: false,
});
window.onresize = function () {
imageEditor.ui.resizeEditor();
};
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>1. Basic</title>
<link
type="text/css"
href="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css"
rel="stylesheet"
/>
<link type="text/css" href="css/service-basic.css" rel="stylesheet" />
</head>
<body>
<div class="body-container">
<div class="tui-image-editor-controls">
<div class="header">
<img class="logo" src="img/TOAST UI Component.png" />
<span class="name"> Image Editor</span>
<ul class="menu">
<li class="menu-item border input-wrapper">
Load
<input type="file" accept="image/*" id="input-image-file" />
</li>
<li class="menu-item border" id="btn-download">Download</li>
</ul>
</div>
<ul class="menu">
<li class="menu-item disabled" id="btn-undo">Undo</li>
<li class="menu-item disabled" id="btn-redo">Redo</li>
<li class="menu-item" id="btn-clear-objects">ClearObjects</li>
<li class="menu-item" id="btn-remove-active-object">RemoveActiveObject</li>
<li class="menu-item" id="btn-crop">Crop</li>
<li class="menu-item" id="btn-flip">Flip</li>
<li class="menu-item" id="btn-rotation">Rotation</li>
<li class="menu-item" id="btn-draw-line">DrawLine</li>
<li class="menu-item" id="btn-draw-shape">Shape</li>
<li class="menu-item" id="btn-add-icon">Icon</li>
<li class="menu-item" id="btn-text">Text</li>
<li class="menu-item" id="btn-mask-filter">Mask</li>
<li class="menu-item" id="btn-image-filter">Filter</li>
</ul>
<div class="sub-menu-container" id="crop-sub-menu">
<ul class="menu">
<li class="menu-item" id="btn-apply-crop">Apply</li>
<li class="menu-item" id="btn-cancel-crop">Cancel</li>
</ul>
</div>
<div class="sub-menu-container" id="flip-sub-menu">
<ul class="menu">
<li class="menu-item" id="btn-flip-x">FlipX</li>
<li class="menu-item" id="btn-flip-y">FlipY</li>
<li class="menu-item" id="btn-reset-flip">Reset</li>
<li class="menu-item close">Close</li>
</ul>
</div>
<div class="sub-menu-container" id="rotation-sub-menu">
<ul class="menu">
<li class="menu-item" id="btn-rotate-clockwise">Clockwise(30)</li>
<li class="menu-item" id="btn-rotate-counter-clockwise">Counter-Clockwise(-30)</li>
<li class="menu-item no-pointer">
<label>
Range input
<input id="input-rotation-range" type="range" min="-360" value="0" max="360" />
</label>
</li>
<li class="menu-item close">Close</li>
</ul>
</div>
<div class="sub-menu-container menu" id="draw-line-sub-menu">
<ul class="menu">
<li class="menu-item">
<label>
<input type="radio" name="select-line-type" value="freeDrawing" checked="checked" />
Free drawing
</label>
<label>
<input type="radio" name="select-line-type" value="lineDrawing" />
Straight line
</label>
</li>
<li class="menu-item">
<div id="tui-brush-color-picker">Brush color</div>
</li>
<li class="menu-item">
<label class="menu-item no-pointer">
Brush width
<input id="input-brush-width-range" type="range" min="5" max="30" value="12" />
</label>
</li>
<li class="menu-item close">Close</li>
</ul>
</div>
<div class="sub-menu-container" id="draw-shape-sub-menu">
<ul class="menu">
<li class="menu-item">
<label>
<input type="radio" name="select-shape-type" value="rect" checked="checked" />
rect
</label>
<label>
<input type="radio" name="select-shape-type" value="circle" />
circle
</label>
<label>
<input type="radio" name="select-shape-type" value="triangle" />
triangle
</label>
</li>
<li class="menu-item">
<select name="select-color-type">
<option value="fill">Fill</option>
<option value="stroke">Stroke</option>
</select>
<label>
<input
type="radio"
name="input-check-fill"
id="input-check-transparent"
value="transparent"
/>
transparent
</label>
<label>
<input
type="radio"
name="input-check-fill"
id="input-check-filter"
value="filter"
/>
filter
</label>
<div id="tui-shape-color-picker"></div>
</li>
<li class="menu-item">
<label class="menu-item no-pointer">
Stroke width
<input id="input-stroke-width-range" type="range" min="0" max="300" value="12" />
</label>
</li>
<li class="menu-item close">Close</li>
</ul>
</div>
<div class="sub-menu-container" id="icon-sub-menu">
<ul class="menu">
<li class="menu-item">
<div id="tui-icon-color-picker">Icon color</div>
</li>
<li class="menu-item border" id="btn-register-icon">Register custom icon</li>
<li class="menu-item icon-text" data-icon-type="arrow"></li>
<li class="menu-item icon-text" data-icon-type="cancel"></li>
<li class="menu-item close">Close</li>
</ul>
</div>
<div class="sub-menu-container" id="text-sub-menu">
<ul class="menu">
<li class="menu-item">
<div>
<button class="btn-text-style" data-style-type="b">Bold</button>
<button class="btn-text-style" data-style-type="i">Italic</button>
<button class="btn-text-style" data-style-type="u">Underline</button>
</div>
<div>
<button class="btn-text-style" data-style-type="l">Left</button>
<button class="btn-text-style" data-style-type="c">Center</button>
<button class="btn-text-style" data-style-type="r">Right</button>
</div>
</li>
<li class="menu-item">
<label class="no-pointer">
<input id="input-font-size-range" type="range" min="10" max="100" value="10" />
</label>
</li>
<li class="menu-item">
<div id="tui-text-color-picker">Text color</div>
</li>
<li class="menu-item close">Close</li>
</ul>
</div>
<div class="sub-menu-container" id="filter-sub-menu">
<ul class="menu">
<li class="menu-item border input-wrapper">
Load Mask Image
<input type="file" accept="image/*" id="input-mask-image-file" />
</li>
<li class="menu-item" id="btn-apply-mask">Apply mask filter</li>
<li class="menu-item close">Close</li>
</ul>
</div>
<div class="sub-menu-container" id="image-filter-sub-menu">
<ul class="menu">
<li class="menu-item align-left-top">
<table>
<tbody>
<tr>
<td>
<label><input type="checkbox" id="input-check-grayscale" />Grayscale</label>
</td>
<td>
<label><input type="checkbox" id="input-check-invert" />Invert</label>
</td>
<td>
<label><input type="checkbox" id="input-check-sepia" />Sepia</label>
</td>
</tr>
<tr>
<td>
<label><input type="checkbox" id="input-check-sepia2" />Sepia2</label>
</td>
<td>
<label><input type="checkbox" id="input-check-blur" />Blur</label>
</td>
<td>
<label><input type="checkbox" id="input-check-sharpen" />Sharpen</label>
</td>
</tr>
<tr>
<td>
<label><input type="checkbox" id="input-check-emboss" />Emboss</label>
</td>
</tr>
</tbody>
</table>
</li>
<li class="menu-item align-left-top">
<p>
<label>
<input type="checkbox" id="input-check-remove-white" />
RemoveWhite
</label>
<br />
<label>
Threshold
<input
class="range-narrow"
id="input-range-remove-white-threshold"
type="range"
min="0"
value="60"
max="255"
/>
</label>
<br />
<label>
Distance
<input
class="range-narrow"
id="input-range-remove-white-distance"
type="range"
min="0"
value="10"
max="255"
/>
</label>
</p>
</li>
<li class="menu-item align-left-top">
<p>
<label><input type="checkbox" id="input-check-brightness" />Brightness</label><br />
<label>
Value
<input
class="range-narrow"
id="input-range-brightness-value"
type="range"
min="-255"
value="100"
max="255"
/>
</label>
</p>
</li>
<li class="menu-item align-left-top">
<p>
<label><input type="checkbox" id="input-check-noise" />Noise</label><br />
<label>
Value
<input
class="range-narrow"
id="input-range-noise-value"
type="range"
min="0"
value="100"
max="1000"
/>
</label>
</p>
</li>
<li class="menu-item align-left-top">
<p>
<label>
<input type="checkbox" id="input-check-color-filter" />
ColorFilter
</label>
<br />
<label>
Threshold
<input
class="range-narrow"
id="input-range-color-filter-value"
type="range"
min="0"
value="45"
max="255"
/>
</label>
</p>
</li>
<li class="menu-item align-left-top">
<p>
<label><input type="checkbox" id="input-check-pixelate" />Pixelate</label><br />
<label>
Value
<input
class="range-narrow"
id="input-range-pixelate-value"
type="range"
min="2"
value="4"
max="20"
/>
</label>
</p>
</li>
<li class="menu-item align-left-top">
<p>
<label><input type="checkbox" id="input-check-tint" />Tint</label><br />
</p>
<div id="tui-tint-color-picker"></div>
<label>
Opacity
<input
class="range-narrow"
id="input-range-tint-opacity-value"
type="range"
min="0"
value="1"
max="1"
step="0.1"
/>
</label>
</li>
<li class="menu-item align-left-top">
<p>
<label><input type="checkbox" id="input-check-multiply" />Multiply</label>
</p>
<div id="tui-multiply-color-picker"></div>
</li>
<li class="menu-item align-left-top">
<p>
<label><input type="checkbox" id="input-check-blend" />Blend</label>
</p>
<div id="tui-blend-color-picker"></div>
<select name="select-blend-type">
<option value="add" selected>Add</option>
<option value="diff">Diff</option>
<option value="diff">Subtract</option>
<option value="multiply">Multiply</option>
<option value="screen">Screen</option>
<option value="lighten">Lighten</option>
<option value="darken">Darken</option>
</select>
</li>
<li class="menu-item close">Close</li>
</ul>
</div>
</div>
<div class="tui-image-editor"></div>
</div>
<script
type="text/javascript"
src="https://api-storage.cloud.toast.com/v1/AUTH_e18353c4ea5746c097143946d0644e61/toast-ui-cdn/tui-image-editor/v3.11.0/example/fabric-v4.2.0.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui.code-snippet/v1.5.0/tui-code-snippet.min.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.min.js"
></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"
></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"
></script>
<script type="text/javascript" src="../dist/tui-image-editor.js"></script>
<script src="js/service-basic.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title>2. Mobile</title>
<link
type="text/css"
href="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.css"
rel="stylesheet"
/>
<link type="text/css" href="css/service-mobile.css" rel="stylesheet" />
</head>
<body>
<!-- Image editor controls - top area -->
<div class="header">
<div>
<img class="logo" src="img/TOAST UI Component.png" /> <span class="name">Image Editor</span>
</div>
<div class="menu">
<span class="button">
<img src="img/openImage.png" style="margin-top: 5px" />
<input type="file" accept="image/*" id="input-image-file" />
</span>
<button class="button disabled" id="btn-undo"><img src="img/undo.png" /></button>
<button class="button disabled" id="btn-redo"><img src="img/redo.png" /></button>
<button class="button" id="btn-remove-active-object"><img src="img/remove.png" /></button>
<button class="button" id="btn-download"><img src="img/download.png" /></button>
</div>
</div>
<!-- Image editor area -->
<div class="tui-image-editor"></div>
<!-- Image editor controls - bottom area -->
<div class="tui-image-editor-controls">
<ul class="scrollable">
<li class="menu-item">
<button class="menu-button" id="btn-crop">Crop</button>
<div class="submenu">
<button class="btn-prev">&lt;</button>
<ul class="scrollable">
<li class="menu-item">
<button class="submenu-button" id="btn-apply-crop">Apply</button>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<button class="menu-button">Orientation</button>
<div class="submenu">
<button class="btn-prev">&lt;</button>
<ul class="scrollable">
<li class="menu-item">
<button class="submenu-button" id="btn-rotate-clockwise">Rotate +90</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-rotate-counter-clockwise">Rotate -90</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-flip-x">FilpX</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-flip-y">FilpY</button>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<button class="menu-button" id="btn-draw-line">Drawing</button>
<div class="submenu">
<button class="btn-prev">&lt;</button>
<ul class="scrollable">
<li class="menu-item">
<button class="submenu-button" id="btn-free-drawing">Free<br />Drawing</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-line-drawing">Line<br />Drawing</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-change-size">Brush<br />Size</button>
<div class="hiddenmenu">
<input id="input-brush-range" type="range" min="10" max="100" value="50" />
</div>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-change-text-color">Brush<br />Color</button>
<div class="hiddenmenu">
<div id="tui-brush-color-picker"></div>
</div>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<button class="menu-button" id="btn-draw-shape">Shape</button>
<div class="submenu">
<button class="btn-prev">&lt;</button>
<ul class="scrollable">
<li class="menu-item">
<button class="submenu-button" id="btn-add-rect">Rectagle</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-add-square">Square</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-add-ellipse">Ellipse</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-add-circle">Circle</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-add-triangle">Triangle</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-stroke-size">Stroke<br />Size</button>
<div class="hiddenmenu">
<input id="input-stroke-range" type="range" min="1" max="100" value="10" />
</div>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-change-shape-color">Color</button>
<div class="hiddenmenu">
<div class="top">
<label for="fill-color"
><input
type="radio"
id="fill-color"
name="select-color-type"
value="fill"
checked="checked"
/>
Fill</label
>
<label for="stroke-color"
><input
type="radio"
id="stroke-color"
name="select-color-type"
value="stroke"
/>
Stroke</label
>
<label for="input-check-transparent"
><input type="checkbox" id="input-check-transparent" />Transparent</label
>
</div>
<div id="tui-shape-color-picker"></div>
</div>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<button class="menu-button">Icon</button>
<div class="submenu">
<button class="btn-prev">&lt;</button>
<ul class="scrollable">
<li class="menu-item">
<button class="submenu-button" id="btn-add-arrow-icon">Arrow<br />Icon</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-add-cancel-icon">Cancel<br />Icon</button>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-change-icon-color">Color</button>
<div class="hiddenmenu">
<div id="tui-icon-color-picker"></div>
</div>
</li>
</ul>
</div>
</li>
<li class="menu-item">
<button class="menu-button" id="btn-add-text">Text</button>
<div class="submenu">
<button class="btn-prev">&lt;</button>
<ul class="scrollable">
<li class="menu-item">
<button class="submenu-button" id="btn-change-size">Size</button>
<div class="hiddenmenu">
<input id="input-text-size-range" type="range" min="10" max="240" value="120" />
</div>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-change-style">Style</button>
<div class="hiddenmenu">
<button class="hiddenmenu-button btn-change-text-style" data-style-type="bold">
<b>Bold</b>
</button>
<button class="hiddenmenu-button btn-change-text-style" data-style-type="italic">
<i>Italic</i>
</button>
<button
class="hiddenmenu-button btn-change-text-style"
data-style-type="underline"
>
<u>Underline</u>
</button>
</div>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-change-align">Align</button>
<div class="hiddenmenu">
<button class="hiddenmenu-button btn-change-text-style" data-style-type="left">
Left
</button>
<button class="hiddenmenu-button btn-change-text-style" data-style-type="center">
Center
</button>
<button class="hiddenmenu-button btn-change-text-style" data-style-type="right">
Right
</button>
</div>
</li>
<li class="menu-item">
<button class="submenu-button" id="btn-change-text-color">Color</button>
<div class="hiddenmenu">
<div id="tui-text-color-picker"></div>
</div>
</li>
</ul>
</div>
</li>
</ul>
<p class="msg">Menu Scrolling <b>Left ⇔ Right</b></p>
</div>
<script
type="text/javascript"
src="https://api-storage.cloud.toast.com/v1/AUTH_e18353c4ea5746c097143946d0644e61/toast-ui-cdn/tui-image-editor/v3.11.0/example/fabric-v4.2.0.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui.code-snippet/v1.5.0/tui-code-snippet.min.js"
></script>
<script
type="text/javascript"
src="https://uicdn.toast.com/tui-color-picker/v2.2.6/tui-color-picker.min.js"
></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js"
></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"
></script>
<script type="text/javascript" src="../dist/tui-image-editor.js"></script>
<script src="js/service-mobile.js"></script>
</body>
</html>
{
"example01-includeUi": {
"title": "1. Include ui"
},
"example02-useApiDirect": {
"title": "2. Use api direct (basic)"
},
"example03-mobile": {
"title": "3. Mobile"
}
}
/**
* basic.js
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview
*/
/* 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.undo();
}
});
$btnRedo.on('click', function () {
$displayingSubMenu.hide();
if (!$(this).hasClass('disabled')) {
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 = '<img src="' + dataURL + '">';
}
});
// 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('<li id="customArrow" class="menu-item icon-text" data-icon-type="customArrow">↑</li>');
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, 'Sepia2', null);
});
$inputCheckBlur.on('change', function () {
applyOrRemoveFilter(this.checked, 'Blur', null);
});
$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, 'removeWhite', {
threshold: parseInt($inputRangeRemoveWhiteThreshold.val(), 10),
distance: parseInt($inputRangeRemoveWhiteDistance.val(), 10),
});
});
$inputRangeRemoveWhiteThreshold.on('change', function () {
applyOrRemoveFilter($inputCheckRemoveWhite.is(':checked'), 'removeWhite', {
threshold: parseInt(this.value, 10),
});
});
$inputRangeRemoveWhiteDistance.on('change', function () {
applyOrRemoveFilter($inputCheckRemoveWhite.is(':checked'), 'removeWhite', {
distance: parseInt(this.value, 10),
});
});
$inputCheckBrightness.on('change', function () {
applyOrRemoveFilter(this.checked, 'brightness', {
brightness: parseInt($inputRangeBrightnessValue.val(), 10),
});
});
$inputRangeBrightnessValue.on('change', function () {
applyOrRemoveFilter($inputCheckBrightness.is(':checked'), 'brightness', {
brightness: parseInt(this.value, 10),
});
});
$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, 'tint', {
color: tintColorpicker.getColor(),
opacity: parseFloat($inputRangeTintOpacityValue.val()),
});
});
tintColorpicker.on('selectColor', function (e) {
applyOrRemoveFilter($inputCheckTint.is(':checked'), 'tint', {
color: e.color,
});
});
$inputRangeTintOpacityValue.on('change', function () {
applyOrRemoveFilter($inputCheckTint.is(':checked'), 'tint', {
opacity: parseFloat($inputRangeTintOpacityValue.val()),
});
});
$inputCheckMultiply.on('change', function () {
applyOrRemoveFilter(this.checked, 'multiply', {
color: multiplyColorpicker.getColor(),
});
});
multiplyColorpicker.on('selectColor', function (e) {
applyOrRemoveFilter($inputCheckMultiply.is(':checked'), 'multiply', {
color: e.color,
});
});
$inputCheckBlend.on('change', function () {
applyOrRemoveFilter(this.checked, 'blend', {
color: blendColorpicker.getColor(),
mode: $selectBlendType.val(),
});
});
blendColorpicker.on('selectColor', function (e) {
applyOrRemoveFilter($inputCheckBlend.is(':checked'), 'blend', {
color: e.color,
});
});
$selectBlendType.on('change', function () {
applyOrRemoveFilter($inputCheckBlend.is(':checked'), 'blend', {
mode: this.value,
});
});
$inputCheckColorFilter.on('change', function () {
applyOrRemoveFilter(this.checked, 'colorFilter', {
color: '#FFFFFF',
threshold: $inputRangeColorFilterValue.val(),
});
});
$inputRangeColorFilterValue.on('change', function () {
applyOrRemoveFilter($inputCheckColorFilter.is(':checked'), 'colorFilter', {
threshold: this.value,
});
});
// 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;
});
/**
* mobile.js
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview
*/
/* 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 = '<img src=' + dataURL + '>';
}
});
// 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();
});
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',
};
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',
};
// Type definitions for TOAST UI Image Editor v3.9.0
// 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;
};
}
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<void>;
}
class ImageEditor {
constructor(wrapper: string | Element, options: IOptions);
public ui: UI;
public addIcon(type: string, options?: IIconOptions): Promise<IObjectProps>;
public addImageObject(imgUrl: string): Promise<void>;
public addShape(type: string, options?: IShapeOptions): Promise<IObjectProps>;
public addText(text: string, options?: IGenerateTextOptions): Promise<ITextObjectProps>;
public applyFilter(
type: string,
options?: {
maskObjId: number;
},
isSilent?: boolean
): Promise<IFilterResolveObject>;
public changeCursor(cursorType: string): void;
public changeIconColor(id: number, color: string): Promise<void>;
public changeSelectableAll(selectable: boolean): void;
public changeShape(id: number, options?: IShapeOptions, isSilent?: boolean): Promise<void>;
public changeText(id: number, text?: string): Promise<void>;
public changeTextStyle(
id: number,
styleObj: ITextStyleConfig,
isSilent?: boolean
): Promise<void>;
public clearObjects(): Promise<void>;
public clearRedoStack(): void;
public clearUndoStack(): void;
public crop(rect: IRectConfig): Promise<ICropResolveObject>;
public deactivateAll(): void;
public destroy(): void;
public discardSelection(): void;
public flipX(): Promise<IFlipXYResolveObject>;
public flipY(): Promise<IFlipXYResolveObject>;
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<ICropResolveObject>;
public loadImageFromURL(url: string, imageName?: string): Promise<ICropResolveObject>;
public redo(): Promise<any>;
public registerIcons(infos: IIconInfo): void;
public removeActiveObject(): void;
public removeFilter(type?: string): Promise<IFilterResolveObject>;
public removeObject(id: number): Promise<void>;
public resetFlip(): Promise<IFlipXYResolveObject>;
public resizeCanvasDimension(dimension: ICanvasSize): Promise<void>;
public rotate(angle: AngleType, isSilent?: boolean): Promise<AngleType>;
public setAngle(angle: AngleType, isSilent?: boolean): Promise<AngleType>;
public setBrush(option: IBrushOptions): void;
public setCropzoneRect(mode?: number): void;
public setDrawingShape(type: string, options?: IShapeOptions): void;
public setObjectPosition(id: number, posInfo?: IPositionConfig): Promise<void>;
public setObjectProperties(id: number, keyValue?: IGraphicObjectProps): Promise<void>;
public setObjectPropertiesQuietly(id: number, keyValue?: IGraphicObjectProps): Promise<void>;
public startDrawingMode(mode: string, option?: { width?: number; color?: string }): boolean;
public stopDrawingMode(): void;
public toDataURL(options?: IToDataURLOptions): string;
public undo(): Promise<any>;
public on(eventName: string, handler: (...args: any[]) => void): void;
}
}
declare module 'tui-image-editor' {
export = tuiImageEditor.ImageEditor;
}
{
"source": {
"include": ["src", "README.md"],
"exclude": [],
"includePattern": ".+\\.js(doc)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"plugins": ["plugins/markdown"],
"templates": {
"name": "ImageEditor",
"logo": {
"url": "https://user-images.githubusercontent.com/35218826/40895380-0b9f4cd6-67ea-11e8-982f-18121daa3a04.png",
"width": "150px",
"height": "13px",
"link": "https://github.com/nhn/tui.jsdoc-template"
}
},
"opts": {
"private": false,
"recurse": true,
"destination": "doc",
"tutorials": "examples",
"template": "./node_modules/tui-jsdoc-template",
"package": "package.json"
}
}
/* eslint-disable consts-on-top, no-process-env, require-jsdoc */
/* eslint-disable no-process-env, require-jsdoc */
const webdriverConfig = {
hostname: 'fe.nhnent.com',
port: 4444,
remoteHost: true,
};
function setConfig(defaultConfig, server) {
if (server === 'ne') {
defaultConfig.customLaunchers = {
IE9: {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'internet explorer',
version: '9',
},
IE10: {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'internet explorer',
version: '10',
},
IE11: {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'internet explorer',
version: '11',
},
Edge: {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'MicrosoftEdge',
},
'Chrome-WebDriver': {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'chrome',
},
'Firefox-WebDriver': {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'firefox',
},
'Safari-WebDriver': {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'safari',
},
};
defaultConfig.browsers = [
'IE9',
'IE10',
// 'IE11',
// 'Edge',
'Chrome-WebDriver',
'Firefox-WebDriver',
// 'Safari-WebDriver'
];
defaultConfig.reporters.push('coverage');
defaultConfig.reporters.push('junit');
defaultConfig.coverageReporter = {
dir: 'report/coverage/',
reporters: [
{
type: 'html',
subdir(browser) {
return `report-html/${browser}`;
},
},
{
type: 'cobertura',
subdir(browser) {
return `report-cobertura/${browser}`;
},
file: 'cobertura.txt',
},
],
};
defaultConfig.junitReporter = {
outputDir: 'report/junit',
suite: '',
};
} else {
defaultConfig.browsers = ['ChromeHeadless'];
}
}
module.exports = function (config) {
const defaultConfig = {
basePath: './',
frameworks: ['jasmine', 'jquery-3.2.1', 'es5-shim'],
files: [
// reason for not using karma-jasmine-jquery framework is that including older jasmine-karma file
// included jasmine-karma version is 2.0.5 and this version don't support ie8
'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
'node_modules/fabric/dist/fabric.js',
'test/index.js',
{
pattern: 'test/fixtures/*.jpg',
watched: false,
included: false,
served: true,
},
{
pattern: 'test/fixtures/*.png',
watched: false,
included: false,
served: true,
},
{
pattern: 'test/fixtures/*.svg',
watched: false,
included: false,
served: true,
},
],
preprocessors: {
'test/index.js': ['webpack', 'sourcemap'],
},
reporters: ['dots'],
webpack: {
mode: 'development',
devtool: 'inline-source-map',
externals: {
fabric: 'fabric',
},
module: {
rules: [
{
test: /\.js$/,
include: /src/,
exclude: /node_modules/,
loader: 'eslint-loader',
enforce: 'pre',
},
{
test: /\.js$/,
exclude: /(test|node_modules)/,
loader: 'istanbul-instrumenter-loader',
query: {
esModules: true,
},
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader?cacheDirectory',
options: {
babelrc: true,
},
},
{
test: /\.styl$/,
use: ['css-loader', 'stylus-loader'],
},
{
test: /\.svg$/,
loader: 'svg-inline-loader',
},
],
},
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
singleRun: true,
};
/* eslint-disable */
setConfig(defaultConfig, process.env.KARMA_SERVER);
config.set(defaultConfig);
};
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', (mkdirpErr) => {
if (mkdirpErr) {
console.error(mkdirpErr);
} else {
fs.readdir(svgDir, (err, dirs) => {
dirs.forEach((dir) => {
getFileList(dir);
});
});
}
});
This diff could not be displayed because it is too large.
{
"name": "tui-image-editor",
"author": "NHN FE Development Lab <dl_javascript@nhn.com>",
"version": "3.11.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nhn/tui.image-editor.git"
},
"main": "dist/tui-image-editor.js",
"files": [
"src",
"dist",
"index.d.ts"
],
"description": "TOAST UI Component: ImageEditor",
"keywords": [
"nhn",
"nhnent",
"tui",
"component",
"image",
"editor",
"canvas",
"fabric"
],
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.3",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^3.4.1",
"dtslint": "^0.4.2",
"es5-shim": "^4.5.9",
"eslint": "^4.5.0",
"eslint-config-prettier": "^6.15.0",
"eslint-config-tui": "^1.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-prettier": "^3.1.4",
"file-saver": "^1.3.3",
"istanbul-instrumenter-loader": "^1.0.0",
"jasmine-core": "^2.4.1",
"jasmine-jquery": "^2.1.1",
"jquery": "^3.4.0",
"jsdoc": "^3.5.4",
"karma": "^4.4.1",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^2.0.1",
"karma-edge-launcher": "^0.4.2",
"karma-es5-shim": "0.0.4",
"karma-firefox-launcher": "^1.1.0",
"karma-ie-launcher": "^1.0.0",
"karma-jasmine": "^1.1.1",
"karma-jasmine-jquery-2": "^0.1.1",
"karma-jquery": "^0.2.4",
"karma-junit-reporter": "^1.2.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-webdriver-launcher": "git+https://github.com/nhn/karma-webdriver-launcher.git#v1.2.0",
"karma-webpack": "^4.0.2",
"mini-css-extract-plugin": "^0.9.0",
"mkdirp": "^0.5.1",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"prettier": "^2.2.1",
"safe-umd-webpack-plugin": "^4.0.0",
"selenium-webdriver": "^4.0.0-alpha.7",
"simulant": "^0.2.2",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"svg-inline-loader": "^0.8.2",
"svgstore": "^2.0.3",
"tslint": "^5.12.0",
"tui-jsdoc-template": "^1.0.4",
"typescript": "^3.2.2",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.1"
},
"scripts": {
"test": "karma start --no-single-run",
"test:ne": "KARMA_SERVER=ne karma start",
"test:types": "tsc --project test/types",
"bundle": "webpack && webpack -p && npm run bundle:svg && node tsBannerGenerator.js",
"bundle:svg": "node makesvg.js",
"serve": "webpack-dev-server",
"doc:dev": "tuidoc --dev",
"doc": "tuidoc",
"tslint": "tslint index.d.ts"
},
"dependencies": {
"core-js-pure": "^3.6.4",
"fabric": "4.2.0",
"tui-code-snippet": "^1.5.0",
"tui-color-picker": "^2.2.6"
}
}
/* 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;
/* 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
&.left .{prefix}-button.left svg > use.active,
&.center .{prefix}-button.center svg > use.active,
&.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;
/* 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;
/* 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;
/* 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
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;
/* 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}-controls
svg > use
display: none;
.enabled svg:hover > use.hover
.normal svg:hover > use.hover
display: block;
.active svg:hover > use.hover
display: none;
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;
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;
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
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;
/* 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 RIGIHT */
.{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;
/* VIRTUAL RANGE */
.{prefix}-container
.{prefix}-virtual-range-bar
.{prefix}-virtual-range-subbar
.{prefix}-virtual-range-pointer
.{prefix}-disabled
backbround-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: 21px;
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: #282828;
width: 1px;
height: 24px;
\ No newline at end of file
/* 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-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
display: table-cell;
.{prefix}-main.{prefix}-menu-crop,
.{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}-submenu
display: table;
import './js/polyfill';
import ImageEditor from './js/imageEditor';
import './css/index.styl';
// commands
import './js/command/addIcon';
import './js/command/addImageObject';
import './js/command/addObject';
import './js/command/addShape';
import './js/command/addText';
import './js/command/applyFilter';
import './js/command/changeIconColor';
import './js/command/changeShape';
import './js/command/changeText';
import './js/command/changeTextStyle';
import './js/command/clearObjects';
import './js/command/flip';
import './js/command/loadImage';
import './js/command/removeFilter';
import './js/command/removeObject';
import './js/command/resizeCanvasDimension';
import './js/command/rotate';
import './js/command/setObjectProperties';
import './js/command/setObjectPosition';
import './js/command/changeSelection';
module.exports = ImageEditor;
import { extend } from 'tui-code-snippet';
import { isSupportFileApi, base64ToBlob, toInteger } from './util';
import Imagetracer from './helper/imagetracer';
export default {
/**
* Get ui actions
* @returns {Object} actions for ui
* @private
*/
getActions() {
return {
main: this._mainAction(),
shape: this._shapeAction(),
crop: this._cropAction(),
flip: this._flipAction(),
rotate: this._rotateAction(),
text: this._textAction(),
mask: this._maskAction(),
draw: this._drawAction(),
icon: this._iconAction(),
filter: this._filterAction(),
};
},
/**
* 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;
};
return extend(
{
initLoadImage: (imagePath, imageName) =>
this.loadImageFromURL(imagePath, imageName).then((sizeValue) => {
exitCropOnAction();
this.ui.initializeImgUrl = imagePath;
this.ui.resizeEditor({ imageSize: sizeValue });
this.clearUndoStack();
}),
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();
this.ui.resizeEditor({ imageSize: sizeValue });
this.clearUndoStack();
});
},
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();
this.clearUndoStack();
this.ui.activeMenuEvent();
this.ui.resizeEditor({ imageSize: sizeValue });
})
['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 = `<img src='${dataURL}'>`;
}
},
},
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();
},
registDefalutIcons: (type, path) => {
const iconObj = {};
iconObj[type] = path;
this.registerIcons(iconObj);
},
registCustomIcon: (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) =>
this.loadImageFromURL(this.toDataURL(), 'FilterImage').then(() => {
this.addImageObject(imgUrl).then(() => {
URL.revokeObjectURL(file);
});
}),
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) {
this.crop(cropRect)
.then(() => {
this.stopDrawingMode();
this.ui.resizeEditor();
this.ui.changeMenu('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()
);
},
/**
* 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 (this.ui.submenu !== 'draw' && this.ui.submenu !== 'crop') {
this.stopDrawingMode();
}
},
});
},
/**
* Common Action
* @returns {Object} common actions for ui
* @private
*/
_commonAction() {
return {
modeChange: (menu) => {
switch (menu) {
case 'text':
this._changeActivateMode('TEXT');
break;
case 'crop':
this.startDrawingMode('CROPPER');
break;
case 'shape':
this._changeActivateMode('SHAPE');
this.setDrawingShape(this.ui.shape.type, this.ui.shape.options);
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);
},
};
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Add an icon
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Add an image object
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Add an object
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Add a shape
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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) => {
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Add a text object
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
import { componentNames, commandNames, rejectMessages } from '../consts';
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;
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Apply a filter into an image
*/
import snippet from 'tui-code-snippet';
import commandFactory from '../factory/command';
import { componentNames, rejectMessages, commandNames } from '../consts';
const { FILTER } = componentNames;
/**
* Chched data for undo
* @type {Object}
*/
let chchedUndoDataForSilent = 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);
}
snippet.extend(options, { mask: maskObj });
graphics.remove(options.mask);
}
if (!this.isRedo) {
const prevfilterOption = filterComp.getOptions(type);
const undoData = makeUndoData(type, prevfilterOption, options);
chchedUndoDataForSilent = this.setUndoData(undoData, chchedUndoDataForSilent, 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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Change icon color
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN. FE Development Team <dl_javascript@nhn.com>
* @fileoverview change selection
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview change a shape
*/
import snippet from 'tui-code-snippet';
import { Promise } from '../util';
import commandFactory from '../factory/command';
import { componentNames, rejectMessages, commandNames } from '../consts';
const { SHAPE } = componentNames;
/**
* Chched data for undo
* @type {Object}
*/
let chchedUndoDataForSilent = 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: {},
};
snippet.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);
chchedUndoDataForSilent = this.setUndoData(undoData, chchedUndoDataForSilent, 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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Change a text
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Change text styles
*/
import snippet from 'tui-code-snippet';
import commandFactory from '../factory/command';
import { Promise } from '../util';
import { componentNames, rejectMessages, commandNames } from '../consts';
const { TEXT } = componentNames;
/**
* Chched data for undo
* @type {Object}
*/
let chchedUndoDataForSilent = 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: {},
};
snippet.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);
chchedUndoDataForSilent = this.setUndoData(undoData, chchedUndoDataForSilent, 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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Clear all objects
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Flip an image
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Load a background (main) image
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Remove a filter from an image
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Remove an object
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Resize a canvas
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Rotate an image
*/
import commandFactory from '../factory/command';
import { componentNames, commandNames } from '../consts';
const { ROTATION } = componentNames;
/**
* Chched data for undo
* @type {Object}
*/
let chchedUndoDataForSilent = 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);
chchedUndoDataForSilent = this.setUndoData(undoData, chchedUndoDataForSilent, 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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Set object properties
*/
import commandFactory from '../factory/command';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Set object properties
*/
import snippet from 'tui-code-snippet';
import commandFactory from '../factory/command';
import { Promise } from '../util';
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 = {};
snippet.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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Image crop module (start cropping, end cropping)
*/
import snippet from 'tui-code-snippet';
import fabric from 'fabric';
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.<string, function>}
* @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,
snippet.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));
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
* @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 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) {
// 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;
}
}
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] = snippet.map([width, height], (sizeValue) => sizeValue * scaleWidth);
const scaleHeight = getScale(height, originalHeight);
[width, height] = snippet.map([width, height], (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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Add filter module
*/
import { isUndefined, extend, forEach, filter } from 'tui-code-snippet';
import { Promise } from '../util';
import fabric from 'fabric';
import Component from '../interface/component';
import Mask from '../extension/mask';
import { rejectMessages, componentNames } from '../consts';
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 = filter(sourceImg.filters, (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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Image flip module
*/
import snippet from 'tui-code-snippet';
import { Promise } from '../util';
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);
}
snippet.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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Free drawing module, Set brush
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Add icon module
*/
import fabric from 'fabric';
import snippet from 'tui-code-snippet';
import { Promise } from '../util';
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(
snippet.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) {
snippet.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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Image loader
*/
import Component from '../interface/component';
import { componentNames, rejectMessages } from '../consts';
import { Promise } from '../util';
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;
/**
* @author NHN. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Free drawing module, Set brush
*/
import fabric from 'fabric';
import snippet from 'tui-code-snippet';
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.<string, function>}
* @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
* @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object
* @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 snippet.extend({}, params, {
startPosition: {
x: x1,
y: y1,
},
endPosition: {
x: x2,
y: y2,
},
});
}
}
export default Line;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Image rotation module
*/
import fabric from 'fabric';
import { Promise } from '../util';
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Shape component
*/
import fabric from 'fabric';
import Component from '../interface/component';
import {
rejectMessages,
eventNames,
keyCodes as KEY_CODES,
componentNames,
fObjectOptions,
SHAPE_DEFAULT_OPTIONS,
SHAPE_FILL_TYPE,
} from '../consts';
import resizeHelper from '../helper/shapeResizeHelper';
import {
getFillImageFromShape,
rePositionFilterTypeFillImage,
reMakePatternImageSource,
makeFillPatternForFilter,
makeFilterOptionFromFabricImage,
resetFillPatternCanvas,
} from '../helper/shapeFilterFillHelper';
import {
Promise,
changeOrigin,
getCustomProperty,
getFillTypeFromOption,
getFillTypeFromObject,
isShape,
} from '../util';
import { extend } from 'tui-code-snippet';
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;
activeSelection.realizeTransform(shapeObj);
this._resetPositionFillFilter(shapeObj);
shapeObj.set({
angle,
left,
top,
});
}
}
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Text module
*/
import fabric from 'fabric';
import snippet from 'tui-code-snippet';
import Component from '../interface/component';
import { eventNames as events, componentNames, fObjectOptions } from '../consts';
import { Promise } from '../util';
const defaultStyles = {
fill: '#000000',
left: 0,
top: 0,
};
const resetStyles = {
fill: '#000000',
fontStyle: 'normal',
fontWeight: 'normal',
textAlign: '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),
};
/**
* 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:editing': this._listeners.modify,
});
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,
'object:selected': this._listeners.select,
'before:selection:cleared': this._listeners.selectClear,
'object:scaling': this._listeners.scaling,
'text:editing': this._listeners.modify,
});
}
/**
* 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 = snippet.extend(styles, options.styles);
}
if (!snippet.isExisty(options.autofocus)) {
options.autofocus = true;
}
newText = new fabric.IText(text, styles);
selectionStyle = snippet.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) => {
snippet.forEach(
styleObj,
(val, key) => {
if (activeObj[key] === val && key !== 'fontSize') {
styleObj[key] = resetStyles[key] || '';
}
},
this
);
if ('textDecoration' in styleObj) {
snippet.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;
const ratio = originWidth / cssWidth;
this._ratio = ratio;
}
/**
* 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: snippet.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;
const scalingSize = obj.fontSize * obj.scaleY;
obj.fontSize = scalingSize;
obj.scaleX = 1;
obj.scaleY = 1;
}
/**
* 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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Constants
*/
import { keyMirror } from './util';
/**
* Editor help features
* @type {Array.<string>}
*/
export const HELP_MENUS = ['undo', 'redo', 'reset', 'delete', 'deleteAll'];
/**
* Filter name value map
* @type {Object.<string, string>}
*/
export const FILTER_NAME_VALUE_MAP = {
blur: 'blur',
blocksize: 'pixelate',
};
/**
* Fill type for shape
* @type {Object.<string, string>}
*/
export const SHAPE_FILL_TYPE = {
FILTER: 'filter',
COLOR: 'color',
};
/**
* Shape type list
* @type {Array.<string>}
*/
export const SHAPE_TYPE = ['rect', 'circle', 'triangle'];
/**
* Component names
* @type {Object.<string, string>}
*/
export const componentNames = keyMirror(
'IMAGE_LOADER',
'CROPPER',
'FLIP',
'ROTATION',
'FREE_DRAWING',
'LINE',
'TEXT',
'ICON',
'FILTER',
'SHAPE'
);
/**
* 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.<string, string>}
*/
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',
};
/**
* Event names
* @type {Object.<string, string>}
*/
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',
};
/**
* Editor states
* @type {Object.<string, string>}
*/
export const drawingModes = keyMirror(
'NORMAL',
'CROPPER',
'FREE_DRAWING',
'LINE_DRAWING',
'TEXT',
'SHAPE',
'ICON'
);
/**
* Shortcut key values
* @type {Object.<string, number>}
*/
export const keyCodes = {
Z: 90,
Y: 89,
C: 67,
V: 86,
SHIFT: 16,
BACKSPACE: 8,
DEL: 46,
ARROW_DOWN: 40,
ARROW_UP: 38,
};
/**
* Fabric object options
* @type {Object.<string, Object>}
*/
export const fObjectOptions = {
SELECTION_STYLE: {
borderColor: 'red',
cornerColor: 'green',
cornerSize: 10,
originX: 'center',
originY: 'center',
transparentCorners: false,
},
};
/**
* Promise reject messages
* @type {Object.<string, string>}
*/
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.<string, string>}
*/
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 defaultRotateRangeValus = {
realTimeEvent: true,
min: -360,
max: 360,
value: 0,
};
export const defaultDrawRangeValus = {
min: 5,
max: 30,
value: 12,
};
export const defaultShapeStrokeValus = {
realTimeEvent: true,
min: 2,
max: 300,
value: 3,
};
export const defaultTextRangeValus = {
realTimeEvent: true,
min: 10,
max: 100,
value: 50,
};
export const defaultFilterRangeValus = {
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,
},
colorfilterThresholeRange: {
realTimeEvent: true,
min: 0,
max: 1,
value: 0.2,
useDecimal: true,
},
blurFilterRange: {
value: 0.1,
},
};
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview CropperDrawingMode class
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview FreeDrawingMode class
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview IconDrawingMode class
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview LineDrawingMode class
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview ShapeDrawingMode class
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview TextDrawingMode class
*/
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;
/**
* @author NHN. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Blur extending fabric.Image.filters.Convolute
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Blur extending fabric.Image.filters.Convolute
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview ColorFilter extending fabric.Image.filters.BaseFilter
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Cropzone extending fabric.Rect
*/
import snippet from 'tui-code-snippet';
import fabric from 'fabric';
import { clamp } from '../util';
import { eventNames as events } 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 = snippet.extend(options, extendsOptions);
options.type = 'cropzone';
this.callSuper('initialize', options);
this._addEventHandler();
this.canvas = canvas;
this.options = options;
},
canvasEventDelegation(eventName) {
let delegationState = 'unregisted';
const isRegisted = this.canvasEventTrigger[eventName] !== NOOP_FUNCTION;
if (isRegisted) {
delegationState = 'registed';
} 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),
});
},
_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<number>, y: Array<number>}} 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: snippet.map(
[
-(halfWidth + left), // x0
-halfWidth, // x1
halfWidth, // x2
halfWidth + (canvasWidth - left - width), // x3
],
Math.ceil
),
y: snippet.map(
[
-(halfHeight + top), // y0
-halfHeight, // y1
halfHeight, // y2
halfHeight + (canvasHeight - top - height), // y3
],
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) {
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;
},
}
);
export default Cropzone;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Emboss extending fabric.Image.filters.Convolute
*/
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() {
const matrix = [1, 1, 1, 1, 0.7, -1, -1, -1, -1];
this.matrix = matrix;
},
}
);
export default Emboss;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Mask extending fabric.Image.filters.Mask
*/
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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Sharpen extending fabric.Image.filters.Convolute
*/
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() {
const matrix = [0, -1, 0, -1, 5, -1, 0, -1, 0];
this.matrix = matrix;
},
}
);
export default Sharpen;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Command factory
*/
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,
};
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Error-message factory
*/
import snippet from 'tui-code-snippet';
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: snippet.extend({}, types),
create(type, ...args) {
type = type.toLowerCase();
const func = map[type];
return func(...args);
},
};
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Graphics module
*/
import snippet from 'tui-code-snippet';
import fabric from 'fabric';
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 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 { getProperties, includes, isShape, Promise } from './util';
import {
componentNames as components,
eventNames as events,
drawingModes,
fObjectOptions,
} from './consts';
import {
makeSelectionUndoData,
makeSelectionUndoDatum,
setCachedUndoDataForDimension,
} from './helper/selectionModifyHelper';
const {
extend,
stamp,
isArray,
isString,
forEachArray,
forEachOwnProperties,
CustomEvents,
} = snippet;
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.<string, DrawingMode>}
* @private
*/
this._drawingModeMap = {};
/**
* Component map
* @type {Object.<string, Component>}
* @private
*/
this._componentMap = {};
/**
* fabric event handlers
* @type {Object.<string, function>}
* @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();
}
/**
* Destroy canvas element
*/
destroy() {
const { wrapperEl } = this._canvas;
this._canvas.clear();
wrapperEl.parentNode.removeChild(wrapperEl);
}
/**
* 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.<Object>} 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 <I>'CROPPER', 'FREE_DRAWING', 'LINE', 'TEXT', 'SHAPE'</I>
* @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;
}
/**
* 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() {
const canvasImage = this.canvasImage.scale(1);
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}
* @private
*/
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<string>|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());
}
/**
* 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));
}
/**
* 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 === 'unregisted') {
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 = snippet.map(targetObjects, (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(
snippet.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);
});
});
}
}
CustomEvents.mixin(Graphics);
export default Graphics;
/*
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 = `<path ${options.desc ? `desc="l ${lnum} p ${pathnum}" ` : ''}${this.tosvgcolorstr(
tracedata.palette[lnum],
options
)}d="`;
if (options.roundcoords === -1) {
str += `M ${smp.segments[0].x1 * options.scale} ${smp.segments[0].y1 * options.scale} `;
for (pcnt = 0; pcnt < smp.segments.length; pcnt++) {
str += `${smp.segments[pcnt].type} ${smp.segments[pcnt].x2 * options.scale} ${
smp.segments[pcnt].y2 * options.scale
} `;
if (smp.segments[pcnt].hasOwnProperty('x3')) {
str += `${smp.segments[pcnt].x3 * options.scale} ${
smp.segments[pcnt].y3 * options.scale
} `;
}
}
str += 'Z ';
} else {
str += `M ${this.roundtodec(
smp.segments[0].x1 * options.scale,
options.roundcoords
)} ${this.roundtodec(smp.segments[0].y1 * options.scale, options.roundcoords)} `;
for (pcnt = 0; pcnt < smp.segments.length; pcnt++) {
str += `${smp.segments[pcnt].type} ${this.roundtodec(
smp.segments[pcnt].x2 * options.scale,
options.roundcoords
)} ${this.roundtodec(smp.segments[pcnt].y2 * options.scale, options.roundcoords)} `;
if (smp.segments[pcnt].hasOwnProperty('x3')) {
str += `${this.roundtodec(
smp.segments[pcnt].x3 * options.scale,
options.roundcoords
)} ${this.roundtodec(smp.segments[pcnt].y3 * options.scale, options.roundcoords)} `;
}
}
str += 'Z ';
}
for (var hcnt = 0; hcnt < smp.holechildren.length; hcnt++) {
var hsmp = layer[smp.holechildren[hcnt]];
if (options.roundcoords === -1) {
if (hsmp.segments[hsmp.segments.length - 1].hasOwnProperty('x3')) {
str += `M ${hsmp.segments[hsmp.segments.length - 1].x3 * options.scale} ${
hsmp.segments[hsmp.segments.length - 1].y3 * options.scale
} `;
} else {
str += `M ${hsmp.segments[hsmp.segments.length - 1].x2 * options.scale} ${
hsmp.segments[hsmp.segments.length - 1].y2 * options.scale
} `;
}
for (pcnt = hsmp.segments.length - 1; pcnt >= 0; pcnt--) {
str += `${hsmp.segments[pcnt].type} `;
if (hsmp.segments[pcnt].hasOwnProperty('x3')) {
str += `${hsmp.segments[pcnt].x2 * options.scale} ${
hsmp.segments[pcnt].y2 * options.scale
} `;
}
str += `${hsmp.segments[pcnt].x1 * options.scale} ${
hsmp.segments[pcnt].y1 * options.scale
} `;
}
} else {
if (hsmp.segments[hsmp.segments.length - 1].hasOwnProperty('x3')) {
str += `M ${this.roundtodec(
hsmp.segments[hsmp.segments.length - 1].x3 * options.scale
)} ${this.roundtodec(hsmp.segments[hsmp.segments.length - 1].y3 * options.scale)} `;
} else {
str += `M ${this.roundtodec(
hsmp.segments[hsmp.segments.length - 1].x2 * options.scale
)} ${this.roundtodec(hsmp.segments[hsmp.segments.length - 1].y2 * options.scale)} `;
}
for (pcnt = hsmp.segments.length - 1; pcnt >= 0; pcnt--) {
str += `${hsmp.segments[pcnt].type} `;
if (hsmp.segments[pcnt].hasOwnProperty('x3')) {
str += `${this.roundtodec(hsmp.segments[pcnt].x2 * options.scale)} ${this.roundtodec(
hsmp.segments[pcnt].y2 * options.scale
)} `;
}
str += `${this.roundtodec(hsmp.segments[pcnt].x1 * options.scale)} ${this.roundtodec(
hsmp.segments[pcnt].y1 * options.scale
)} `;
}
}
str += 'Z ';
}
str += '" />';
if (options.lcpr || options.qcpr) {
for (pcnt = 0; pcnt < smp.segments.length; pcnt++) {
if (smp.segments[pcnt].hasOwnProperty('x3') && options.qcpr) {
str += `<circle cx="${smp.segments[pcnt].x2 * options.scale}" cy="${
smp.segments[pcnt].y2 * options.scale
}" r="${options.qcpr}" fill="cyan" stroke-width="${
options.qcpr * 0.2
}" stroke="black" />`;
str += `<circle cx="${smp.segments[pcnt].x3 * options.scale}" cy="${
smp.segments[pcnt].y3 * options.scale
}" r="${options.qcpr}" fill="white" stroke-width="${
options.qcpr * 0.2
}" stroke="black" />`;
str += `<line x1="${smp.segments[pcnt].x1 * options.scale}" y1="${
smp.segments[pcnt].y1 * options.scale
}" x2="${smp.segments[pcnt].x2 * options.scale}" y2="${
smp.segments[pcnt].y2 * options.scale
}" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`;
str += `<line x1="${smp.segments[pcnt].x2 * options.scale}" y1="${
smp.segments[pcnt].y2 * options.scale
}" x2="${smp.segments[pcnt].x3 * options.scale}" y2="${
smp.segments[pcnt].y3 * options.scale
}" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`;
}
if (!smp.segments[pcnt].hasOwnProperty('x3') && options.lcpr) {
str += `<circle cx="${smp.segments[pcnt].x2 * options.scale}" cy="${
smp.segments[pcnt].y2 * options.scale
}" r="${options.lcpr}" fill="white" stroke-width="${
options.lcpr * 0.2
}" stroke="black" />`;
}
}
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 += `<circle cx="${hsmp.segments[pcnt].x2 * options.scale}" cy="${
hsmp.segments[pcnt].y2 * options.scale
}" r="${options.qcpr}" fill="cyan" stroke-width="${
options.qcpr * 0.2
}" stroke="black" />`;
str += `<circle cx="${hsmp.segments[pcnt].x3 * options.scale}" cy="${
hsmp.segments[pcnt].y3 * options.scale
}" r="${options.qcpr}" fill="white" stroke-width="${
options.qcpr * 0.2
}" stroke="black" />`;
str += `<line x1="${hsmp.segments[pcnt].x1 * options.scale}" y1="${
hsmp.segments[pcnt].y1 * options.scale
}" x2="${hsmp.segments[pcnt].x2 * options.scale}" y2="${
hsmp.segments[pcnt].y2 * options.scale
}" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`;
str += `<line x1="${hsmp.segments[pcnt].x2 * options.scale}" y1="${
hsmp.segments[pcnt].y2 * options.scale
}" x2="${hsmp.segments[pcnt].x3 * options.scale}" y2="${
hsmp.segments[pcnt].y3 * options.scale
}" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`;
}
if (!hsmp.segments[pcnt].hasOwnProperty('x3') && options.lcpr) {
str += `<circle cx="${hsmp.segments[pcnt].x2 * options.scale}" cy="${
hsmp.segments[pcnt].y2 * options.scale
}" r="${options.lcpr}" fill="white" stroke-width="${
options.lcpr * 0.2
}" stroke="black" />`;
}
}
}
}
return str;
}
getsvgstring(tracedata, options) {
options = this.checkoptions(options);
const w = tracedata.width * options.scale;
const h = tracedata.height * options.scale;
let svgstr = `<svg ${
options.viewbox ? `viewBox="0 0 ${w} ${h}" ` : `width="${w}" height="${h}" `
}version="1.1" xmlns="http://www.w3.org/2000/svg" desc="Created with imagetracer.js version ${
this.versionnumber
}" >`;
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 += '</svg>';
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);
}
}
}
/**
* @author NHN. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Selection modification helper
*/
import { extend } from 'tui-code-snippet/src/js/object';
/**
* 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;
obj.realizeTransform(item);
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);
}
/**
* @author NHN. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Shape resize helper
*/
import { forEach, map, extend } from 'tui-code-snippet';
import { capitalizeString, flipObject, setCustomProperty, getCustomProperty } from '../util';
import resizeHelper from '../helper/shapeResizeHelper';
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 map(imageObject.filters, (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 map([endPointIndex1, endPointIndex2], (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 fabricFiterClassName = capitalizeString(key);
const filter = new fabric.Image.filters[fabricFiterClassName]({
[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;
}
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Shape resize helper
*/
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
},
};
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Image-editor application class
*/
import snippet from 'tui-code-snippet';
import Invoker from './invoker';
import UI from './ui';
import action from './action';
import commandFactory from './factory/command';
import Graphics from './graphics';
import { sendHostName, Promise } from './util';
import { eventNames as events, commandNames as commands, keyCodes, rejectMessages } from './consts';
import { makeSelectionUndoData, makeSelectionUndoDatum } from './helper/selectionModifyHelper';
const { isUndefined, forEach, CustomEvents } = snippet;
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.<string, number>} ShapeFilterOption
*/
/**
* Shape filter option
* @typedef {object} ShapeFillOption - fill option of shape
* @property {string} type - fill type ('color' or 'filter')
* @property {Array.<ShapeFillFilterOption>} [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 = snippet.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();
}
fabric.enableGLFiltering = false;
}
/**
* 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 } = 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));
}
/**
* 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 (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 - selctable status
* @example
* imageEditor.changeSelectableAll(false); // or true
*/
changeSelectableAll(selectable) {
this._graphics.changeSelectableAll(selectable);
}
/**
* 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
* @returns {Promise}
* @example
* imageEditor.undo();
*/
undo() {
return this._invoker.undo();
}
/**
* Redo
* @returns {Promise}
* @example
* imageEditor.redo();
*/
redo() {
return this._invoker.redo();
}
/**
* Load image from file
* @param {File} imgFile - Image file
* @param {string} [imageName] - imageName
* @returns {Promise<SizeChange, ErrorMsg>}
* @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<SizeChange, ErrorMsg>}
* @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<ObjectProps, ErrorMsg>}
* @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 <I>'CROPPER', 'FREE_DRAWING', 'LINE_DRAWING', 'TEXT', 'SHAPE'</I>
* @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<FlipStatus, ErrorMsg>}
* @private
*/
_flip(type) {
return this.execute(commands.FLIP_IMAGE, type);
}
/**
* Flip x
* @returns {Promise<FlipStatus, ErrorMsg>}
* @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<FlipStatus, ErrorMsg>}
* @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<FlipStatus, ErrorMsg>}
* @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<RotateStatus, ErrorMsg>}
* @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<RotateStatus, ErrorMsg>}
* @example
* imageEditor.rotate(10); // angle = 10
* imageEditor.rotate(10); // angle = 20
* imageEidtor.rotate(5); // angle = 5
* imageEidtor.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<RotateStatus, ErrorMsg>}
* @example
* imageEditor.setAngle(10); // angle = 10
* imageEditor.rotate(10); // angle = 20
* imageEidtor.setAngle(5); // angle = 5
* imageEidtor.rotate(50); // angle = 55
* imageEidtor.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<ObjectProps, ErrorMsg>}
* @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<ObjectProps, ErrorMsg>}
* @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} objectProps changed object properties
* @private
*/
_onTextChanged(objectProps) {
this.changeText(objectProps.id, objectProps.text);
}
/**
* '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._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) {
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<ObjectProps, ErrorMsg>}
* @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) {
return this.execute(commands.REMOVE_OBJECT, id);
}
/**
* 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<FilterResult, ErrorMsg>}
* @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 {number} options.maskObjId - masking image object id
* @param {boolean} isSilent - is silent execution or not
* @returns {Promise<FilterResult, ErrorMsg>}
* @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.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<string>|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);
}
}
action.mixin(ImageEditor);
CustomEvents.mixin(ImageEditor);
export default ImageEditor;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Command interface
*/
import snippet from 'tui-code-snippet';
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.<string, Component>} compMap - Components injection
* @abstract
*/
execute() {
throw new Error(createMessage(errorTypes.UN_IMPLEMENTATION, 'execute'));
}
/**
* Undo action
* @param {Object.<string, Component>} 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;
}
/**
* 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) {
snippet.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;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Component interface
*/
/**
* 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();
}
}
export default Component;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview DrawingMode interface
*/
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
*/
stop() {
throw new Error(createMessage(errorTypes.UN_IMPLEMENTATION, 'stop'));
}
}
export default DrawingMode;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Invoker - invoke commands
*/
import snippet from 'tui-code-snippet';
import { Promise } from './util';
import commandFactory from './factory/command';
import { eventNames, rejectMessages } from './consts';
const { isFunction, isString, CustomEvents } = snippet;
/**
* Invoker
* @class
* @ignore
*/
class Invoker {
constructor() {
/**
* Undo stack
* @type {Array.<Command>}
* @private
*/
this._undoStack = [];
/**
* Redo stack
* @type {Array.<Command>}
* @private
*/
this._redoStack = [];
/**
* Lock-flag for executing command
* @type {boolean}
* @private
*/
this._isLocked = false;
this._isSilent = false;
}
/**
* Invoke command execution
* @param {Command} command - Command
* @returns {Promise}
* @private
*/
_invokeExecution(command) {
this.lock();
let { args } = command;
if (!args) {
args = [];
}
return command
.execute(...args)
.then((value) => {
if (!this._isSilent) {
this.pushUndoStack(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.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);
} 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;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
// Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/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;
if (!document.documentElement.contains(el)) return null;
do {
if (el.matches(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.1.20170427
*
* 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 window.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', 'An invalid or illegal string was specified');
}
if (/\s/.test(token)) {
throw new DOMEx('INVALID_CHARACTER_ERR', 'String contains an invalid character');
}
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) {
token += '';
return checkTokenAndGetIndex(this, token) !== -1;
};
classListProto.add = function () {
var tokens = arguments,
i = 0,
l = tokens.length,
token,
updated = false;
do {
token = tokens[i] + '';
if (checkTokenAndGetIndex(this, token) === -1) {
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 !== -1) {
this.splice(index, 1);
updated = true;
index = checkTokenAndGetIndex(this, token);
}
} while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.toggle = function (token, force) {
token += '';
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.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);
}
})(window.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);
}
};
}
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();
}
}
})();
import snippet from 'tui-code-snippet';
import { HELP_MENUS } from './consts';
import { getSelector, assignmentForDestroy, cls } from './util';
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 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 Locale from './ui/locale/locale';
const SUB_UI_COMPONENT = {
Shape,
Crop,
Flip,
Rotate,
Text,
Mask,
Icon,
Draw,
Filter,
};
const BI_EXPRESSION_MINSIZE_WHEN_TOP_POSITION = '1300';
/**
* 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._menuElement = null;
this._subMenuElement = null;
this._makeUiElement(element);
this._setUiSize();
this._initMenuEvent = false;
this._makeSubMenu();
}
/**
* 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 snippet.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');
}
}
/**
* 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 snippet.extend(
{
loadImage: {
path: '',
name: '',
},
locale: {},
menuIconPath: '',
menu: ['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() {
snippet.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._menuElement.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,
});
});
}
/**
* Make primary ui dom element
* @param {string|HTMLElement} element - Wrapper's element or selector
* @private
*/
_makeUiElement(element) {
let selectedElement;
window.snippet = snippet;
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'),
}) +
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._menuElement = 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();
}
/**
* make array for help menu output, including partitions.
* @returns {Array}
* @private
*/
_makeHelpMenuWithPartition() {
const helpMenuWithPartition = [...HELP_MENUS, ''];
helpMenuWithPartition.splice(3, 0, '');
return helpMenuWithPartition;
}
/**
* Add help menu
* @private
*/
_addHelpMenus() {
const helpMenuWithPartition = this._makeHelpMenuWithPartition();
snippet.forEach(helpMenuWithPartition, (menuName) => {
if (!menuName) {
this._makeMenuPartitionElement();
} else {
this._makeMenuElement(menuName, ['normal', 'disabled', 'hover'], 'help');
if (menuName) {
this._buttonElements[menuName] = this._menuElement.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._menuElement.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;
this._menuElement.appendChild(btnElement);
}
/**
* Add help action event
* @private
*/
_addHelpActionEvent() {
snippet.forEach(HELP_MENUS, (helpName) => {
this.eventHandler[helpName] = () => this._actions.main[helpName]();
this._buttonElements[helpName].addEventListener('click', this.eventHandler[helpName]);
});
}
/**
* Remove help action event
* @private
*/
_removeHelpActionEvent() {
snippet.forEach(HELP_MENUS, (helpName) => {
this._buttonElements[helpName].removeEventListener('click', this.eventHandler[helpName]);
});
}
/**
* 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();
snippet.forEach(this._buttonElements.download, (element) => {
element.addEventListener('click', this.eventHandler.download);
});
}
_removeDownloadEvent() {
snippet.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]);
snippet.forEach(this._buttonElements.load, (element) => {
element.addEventListener('change', this.eventHandler.loadImage);
});
}
/**
* Remmove load event
* @private
*/
_removeLoadEvent() {
snippet.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]);
}
/**
* Add menu event
* @private
*/
_addMenuEvent() {
snippet.forEach(this.options.menu, (menuName) => {
this._addMainMenuEvent(menuName);
this._addSubMenuEvent(menuName);
});
}
/**
* Remove menu event
* @private
*/
_removeMainMenuEvent() {
snippet.forEach(this.options.menu, (menuName) => {
this._buttonElements[menuName].removeEventListener('click', this.eventHandler[menuName]);
});
}
/**
* 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._initMenuEvent = true;
}
/**
* Remove ui event
* @private
*/
_removeUiEvent() {
this._removeHelpActionEvent();
this._removeDownloadEvent();
this._removeLoadEvent();
this._removeMainMenuEvent();
}
/**
* Destroy all menu instance
* @private
*/
_destroyAllMenu() {
snippet.forEach(this.options.menu, (menuName) => {
this[menuName].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 = `<table>
<tr><td class="dot left-top"></td><td></td><td class="dot right-top"></td></tr>
<tr><td></td><td></td><td></td></tr>
<tr><td class="dot left-bottom"></td><td></td><td class="dot right-bottom"></td></tr>
</table>`;
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 toogle 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.registDefaultIcon();
}
}
/**
* 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`;
}
}
export default Ui;
import snippet from 'tui-code-snippet';
import Submenu from './submenuBase';
import { assignmentForDestroy } from '../util';
import templateHtml from './template/submenu/crop';
/**
* 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) {
snippet.forEach([].slice.call(this._els.preset.querySelectorAll('.preset')), (presetButton) => {
presetButton.classList.remove('active');
});
if (button) {
button.classList.add('active');
}
}
}
export default Crop;
import { assignmentForDestroy, getRgb } from '../util';
import Colorpicker from './tools/colorpicker';
import Range from './tools/range';
import Submenu from './submenuBase';
import templateHtml from './template/submenu/draw';
import { defaultDrawRangeValus } 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'),
'#00a9ff',
this.toggleDirection,
this.usageStatistics
),
drawRange: new Range(
{
slider: this.selector('.tie-draw-range'),
input: this.selector('.tie-draw-range-value'),
},
defaultDrawRangeValus
),
};
this.type = null;
this.color = this._els.drawColorPicker.color;
this.width = this._els.drawRange.value;
}
/**
* 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));
}
/**
* Remove event
* @private
*/
_removeEvent() {
this._els.lineSelectButton.removeEventListener('click', this.eventHandler.changeDrawType);
this._els.drawColorPicker.off();
this._els.drawRange.off();
}
/**
* 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;
import snippet from 'tui-code-snippet';
import Colorpicker from './tools/colorpicker';
import Range from './tools/range';
import Submenu from './submenuBase';
import templateHtml from './template/submenu/filter';
import { toInteger, toCamelCase, assignmentForDestroy } from '../util';
import { defaultFilterRangeValus as FILTER_RANGE } 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',
'colorfilterThresholeRange',
'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() {
snippet.forEach(FILTER_OPTIONS, (filter) => {
const filterCheckElement = this.selector(`.tie-${filter}`);
const filterNameCamelCase = toCamelCase(filter);
filterCheckElement.removeEventListener('change', this.eventHandler[filterNameCamelCase]);
});
snippet.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);
}
_destroyToolInstance() {
snippet.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(),
};
snippet.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.colorfilterThresholeRange.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);
}
/**
* Set filter for undo changed
* @param {Object} chagedFilterInfos - changed command infos
* @param {string} type - filter type
* @param {string} action - add or remove
* @param {Object} options - filter options
*/
setFilterState(chagedFilterInfos) {
const { type, options, action } = chagedFilterInfos;
const filterName = this._getFilterNameFromOptions(type, options);
const isRemove = action === 'remove';
if (!isRemove) {
this._setFilterState(filterName, options);
}
this.checkedMap[filterName].checked = !isRemove;
}
/**
* 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.colorfilterThresholeRange.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 = snippet.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.colorfilterThresholeRange.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
),
colorfilterThresholeRange: new Range(
{ slider: this.selector('.tie-colorfilter-threshole-range') },
FILTER_RANGE.colorfilterThresholeRange
),
filterTintColor: new Colorpicker(
this.selector('.tie-filter-tint-color'),
'#03bd9e',
this.toggleDirection,
this.usageStatistics
),
filterMultiplyColor: new Colorpicker(
this.selector('.tie-filter-multiply-color'),
'#515ce6',
this.toggleDirection,
this.usageStatistics
),
filterBlendColor: new Colorpicker(
this.selector('.tie-filter-blend-color'),
'#ffbb3b',
this.toggleDirection,
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);
}
/**
* 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');
snippet.forEach(options, (option) => {
const optionElement = document.createElement('li');
optionElement.innerHTML = option.innerHTML;
optionElement.setAttribute('data-item', option.value);
optionlist.appendChild(optionElement);
});
}
/**
* custome 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) {
snippet.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;
import snippet from 'tui-code-snippet';
import { assignmentForDestroy } from '../util';
import Submenu from './submenuBase';
import templateHtml from './template/submenu/flip';
/**
* 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');
snippet.forEach(['flipX', 'flipY'], (type) => {
flipClassList.remove(type);
if (flipStatus[type]) {
flipClassList.add(type);
flipClassList.add('resetFlip');
this.flipStatus = true;
}
});
});
}
}
}
export default Flip;
import snippet from 'tui-code-snippet';
import Colorpicker from './tools/colorpicker';
import Submenu from './submenuBase';
import templateHtml from './template/submenu/icon';
import { isSupportFileApi, assignmentForDestroy } from '../util';
import { defaultIconPath } 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 = {
registrIconButton: this.selector('.tie-icon-image-file'),
addIconButton: this.selector('.tie-icon-add-button'),
iconColorpicker: new Colorpicker(
this.selector('.tie-icon-color'),
'#ffbb3b',
this.toggleDirection,
this.usageStatistics
),
};
}
/**
* 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.registCustomIcon - 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.registrIconButton.addEventListener('change', registerIcon);
this._els.addIconButton.addEventListener('click', addIcon);
}
/**
* Remove event
* @private
*/
_removeEvent() {
this._els.iconColorpicker.off();
this._els.registrIconButton.removeEventListener('change', this.eventHandler.registerIcon);
this._els.addIconButton.removeEventListener('click', this.eventHandler.addIcon);
}
/**
* Clear icon type
*/
clearIconType() {
this._els.addIconButton.classList.remove(this.iconType);
this.iconType = null;
}
/**
* Register default icon
*/
registDefaultIcon() {
snippet.forEach(defaultIconPath, (path, type) => {
this.actions.registDefalutIcons(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.registCustomIcon(imgUrl, file);
}
}
}
export default Icon;
/**
* 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;
import Submenu from './submenuBase';
import { assignmentForDestroy, isSupportFileApi } from '../util';
import templateHtml from './template/submenu/mask';
/**
* 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;
import Range from './tools/range';
import Submenu from './submenuBase';
import templateHtml from './template/submenu/rotate';
import { toInteger, assignmentForDestroy } from '../util';
import { defaultRotateRangeValus } 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-retate-button'),
rotateRange: new Range(
{
slider: this.selector('.tie-rotate-range'),
input: this.selector('.tie-ratate-range-value'),
},
defaultRotateRangeValus
),
};
}
/**
* 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;
import Colorpicker from './tools/colorpicker';
import Range from './tools/range';
import Submenu from './submenuBase';
import templateHtml from './template/submenu/shape';
import { toInteger, assignmentForDestroy } from '../util';
import { defaultShapeStrokeValus } 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'),
},
defaultShapeStrokeValus
),
fillColorpicker: new Colorpicker(
this.selector('.tie-color-fill'),
'',
this.toggleDirection,
this.usageStatistics
),
strokeColorpicker: new Colorpicker(
this.selector('.tie-color-stroke'),
'#ffbb3b',
this.toggleDirection,
this.usageStatistics
),
};
this.colorPickerControls.push(this._els.fillColorpicker);
this.colorPickerControls.push(this._els.strokeColorpicker);
}
/**
* 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 dreawing 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));
}
/**
* Remove event
* @private
*/
_removeEvent() {
this._els.shapeSelectButton.removeEventListener('click', this.eventHandler.shapeTypeSelected);
this._els.strokeRange.off();
this._els.fillColorpicker.off();
this._els.strokeColorpicker.off();
}
/**
* 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 = defaultShapeStrokeValus.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;
/**
* 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 butten 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 butten 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);
}
}
export default Submenu;
export default ({ locale, biImage, loadButtonStyle, downloadButtonStyle }) => `
<div class="tui-image-editor-controls">
<div class="tui-image-editor-controls-logo">
<img src="${biImage}" />
</div>
<ul class="tui-image-editor-menu"></ul>
<div class="tui-image-editor-controls-buttons">
<div style="${loadButtonStyle}">
${locale.localize('Load')}
<input type="file" class="tui-image-editor-load-btn" />
</div>
<button class="tui-image-editor-download-btn" style="${downloadButtonStyle}">
${locale.localize('Download')}
</button>
</div>
</div>
`;
export default ({
locale,
biImage,
commonStyle,
headerStyle,
loadButtonStyle,
downloadButtonStyle,
submenuStyle,
}) => `
<div class="tui-image-editor-main-container" style="${commonStyle}">
<div class="tui-image-editor-header" style="${headerStyle}">
<div class="tui-image-editor-header-logo">
<img src="${biImage}" />
</div>
<div class="tui-image-editor-header-buttons">
<div style="${loadButtonStyle}">
${locale.localize('Load')}
<input type="file" class="tui-image-editor-load-btn" />
</div>
<button class="tui-image-editor-download-btn" style="${downloadButtonStyle}">
${locale.localize('Download')}
</button>
</div>
</div>
<div class="tui-image-editor-main">
<div class="tui-image-editor-submenu">
<div class="tui-image-editor-submenu-style" style="${submenuStyle}"></div>
</div>
<div class="tui-image-editor-wrap">
<div class="tui-image-editor-size-wrap">
<div class="tui-image-editor-align-wrap">
<div class="tui-image-editor"></div>
</div>
</div>
</div>
</div>
</div>
`;
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-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.left .tui-image-editor-button.left label,
.tie-text-align-button.center .tui-image-editor-button.center label,
.tie-text-align-button.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 {
fill-rule: evenodd;
fill: ${menuIconStyle.normal.color};
stroke: ${menuIconStyle.normal.color};
}
.tui-image-editor-menu use.active.use-default {
fill-rule: evenodd;
fill: ${menuIconStyle.active.color};
stroke: ${menuIconStyle.active.color};
}
.tui-image-editor-menu use.hover.use-default {
fill-rule: evenodd;
fill: ${menuIconStyle.hover.color};
stroke: ${menuIconStyle.hover.color};
}
.tui-image-editor-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};
}
`;
/**
* @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 }) => `
<ul class="tui-image-editor-submenu-item">
<li class="tie-crop-preset-button">
<div class="tui-image-editor-button preset preset-none active">
<div>
${makeSvgIcon(['normal', 'active'], 'shape-rectangle', true)}
</div>
<label> ${locale.localize('Custom')} </label>
</div>
<div class="tui-image-editor-button preset preset-square">
<div>
${makeSvgIcon(['normal', 'active'], 'crop', true)}
</div>
<label> ${locale.localize('Square')} </label>
</div>
<div class="tui-image-editor-button preset preset-3-2">
<div>
${makeSvgIcon(['normal', 'active'], 'crop', true)}
</div>
<label> ${locale.localize('3:2')} </label>
</div>
<div class="tui-image-editor-button preset preset-4-3">
<div>
${makeSvgIcon(['normal', 'active'], 'crop', true)}
</div>
<label> ${locale.localize('4:3')} </label>
</div>
<div class="tui-image-editor-button preset preset-5-4">
<div>
${makeSvgIcon(['normal', 'active'], 'crop', true)}
</div>
<label> ${locale.localize('5:4')} </label>
</div>
<div class="tui-image-editor-button preset preset-7-5">
<div>
${makeSvgIcon(['normal', 'active'], 'crop', true)}
</div>
<label> ${locale.localize('7:5')} </label>
</div>
<div class="tui-image-editor-button preset preset-16-9">
<div>
${makeSvgIcon(['normal', 'active'], 'crop', true)}
</div>
<label> ${locale.localize('16:9')} </label>
</div>
</li>
<li class="tui-image-editor-partition tui-image-editor-newline">
</li>
<li class="tui-image-editor-partition only-left-right">
<div></div>
</li>
<li class="tie-crop-button action">
<div class="tui-image-editor-button apply">
${makeSvgIcon(['normal', 'active'], 'apply')}
<label>
${locale.localize('Apply')}
</label>
</div>
<div class="tui-image-editor-button cancel">
${makeSvgIcon(['normal', 'active'], 'cancel')}
<label>
${locale.localize('Cancel')}
</label>
</div>
</li>
</ul>
`;
/**
* @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 }) => `
<ul class="tui-image-editor-submenu-item">
<li class="tie-draw-line-select-button">
<div class="tui-image-editor-button free">
<div>
${makeSvgIcon(['normal', 'active'], 'draw-free', true)}
</div>
<label>
${locale.localize('Free')}
</label>
</div>
<div class="tui-image-editor-button line">
<div>
${makeSvgIcon(['normal', 'active'], 'draw-line', true)}
</div>
<label>
${locale.localize('Straight')}
</label>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li>
<div class="tie-draw-color" title="${locale.localize('Color')}"></div>
</li>
<li class="tui-image-editor-partition only-left-right">
<div></div>
</li>
<li class="tui-image-editor-newline tui-image-editor-range-wrap">
<label class="range">${locale.localize('Range')}</label>
<div class="tie-draw-range"></div>
<input class="tie-draw-range-value tui-image-editor-range-value" value="0" />
</li>
</ul>
`;
/**
* @param {Locale} locale - Translate text
* @returns {string}
*/
export default ({ locale }) => `
<ul class="tui-image-editor-submenu-item">
<li class="tui-image-editor-submenu-align">
<div class="tui-image-editor-checkbox-wrap fixed-width">
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-grayscale">
<span>${locale.localize('Grayscale')}</span>
</label>
</div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-invert">
<span>${locale.localize('Invert')}</span>
</label>
</div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-sepia">
<span>${locale.localize('Sepia')}</span>
</label>
</div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-vintage">
<span>${locale.localize('Sepia2')}</span>
</label>
</div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-blur">
<span>${locale.localize('Blur')}</span>
</label>
</div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-sharpen">
<span>${locale.localize('Sharpen')}</span>
</label>
</div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-emboss">
<span>${locale.localize('Emboss')}</span>
</label>
</div>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li class="tui-image-editor-submenu-align">
<div class="tui-image-editor-checkbox-group tui-image-editor-disabled" style="margin-bottom: 7px;">
<div class="tui-image-editor-checkbox-wrap">
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-remove-white">
<span>${locale.localize('Remove White')}</span>
</label>
</div>
</div>
<div class="tui-image-editor-newline tui-image-editor-range-wrap short">
<label>${locale.localize('Distance')}</label>
<div class="tie-removewhite-distance-range"></div>
</div>
</div>
<div class="tui-image-editor-checkbox-group tui-image-editor-disabled">
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-brightness">
<span>${locale.localize('Brightness')}</span>
</label>
</div>
<div class="tui-image-editor-range-wrap short">
<div class="tie-brightness-range"></div>
</div>
</div>
<div class="tui-image-editor-checkbox-group tui-image-editor-disabled">
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-noise">
<span>${locale.localize('Noise')}</span>
</label>
</div>
<div class="tui-image-editor-range-wrap short">
<div class="tie-noise-range"></div>
</div>
</div>
</li>
<li class="tui-image-editor-partition only-left-right">
<div></div>
</li>
<li class="tui-image-editor-submenu-align">
<div class="tui-image-editor-checkbox-group tui-image-editor-disabled">
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-pixelate">
<span>${locale.localize('Pixelate')}</span>
</label>
</div>
<div class="tui-image-editor-range-wrap short">
<div class="tie-pixelate-range"></div>
</div>
</div>
<div class="tui-image-editor-checkbox-group tui-image-editor-disabled">
<div class="tui-image-editor-newline tui-image-editor-checkbox-wrap">
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-color-filter">
<span>${locale.localize('Color Filter')}</span>
</label>
</div>
</div>
<div class="tui-image-editor-newline tui-image-editor-range-wrap short">
<label>${locale.localize('Threshold')}</label>
<div class="tie-colorfilter-threshole-range"></div>
</div>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li>
<div class="filter-color-item">
<div class="tie-filter-tint-color" title="${locale.localize('Tint')}"></div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-tint">
<span></span>
</label>
</div>
</div>
<div class="filter-color-item">
<div class="tie-filter-multiply-color" title="${locale.localize('Multiply')}"></div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-multiply">
<span></span>
</label>
</div>
</div>
<div class="filter-color-item">
<div class="tie-filter-blend-color" title="${locale.localize('Blend')}"></div>
<div class="tui-image-editor-checkbox">
<label>
<input type="checkbox" class="tie-blend">
<span></span>
</label>
</div>
</div>
</li>
</ul>
`;
/**
* @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 }) => `
<ul class="tie-flip-button tui-image-editor-submenu-item">
<li>
<div class="tui-image-editor-button flipX">
<div>
${makeSvgIcon(['normal', 'active'], 'flip-x', true)}
</div>
<label>
${locale.localize('Flip X')}
</label>
</div>
<div class="tui-image-editor-button flipY">
<div>
${makeSvgIcon(['normal', 'active'], 'flip-y', true)}
</div>
<label>
${locale.localize('Flip Y')}
</label>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li>
<div class="tui-image-editor-button resetFlip">
<div>
${makeSvgIcon(['normal', 'active'], 'flip-reset', true)}
</div>
<label>
${locale.localize('Reset')}
</label>
</div>
</li>
</ul>
`;
/**
* @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 }) => `
<ul class="tui-image-editor-submenu-item">
<li class="tie-icon-add-button">
<div class="tui-image-editor-button" data-icontype="icon-arrow">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-arrow', true)}
</div>
<label>
${locale.localize('Arrow')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-arrow-2">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-arrow-2', true)}
</div>
<label>
${locale.localize('Arrow-2')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-arrow-3">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-arrow-3', true)}
</div>
<label>
${locale.localize('Arrow-3')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-star">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-star', true)}
</div>
<label>
${locale.localize('Star-1')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-star-2">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-star-2', true)}
</div>
<label>
${locale.localize('Star-2')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-polygon">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-polygon', true)}
</div>
<label>
${locale.localize('Polygon')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-location">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-location', true)}
</div>
<label>
${locale.localize('Location')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-heart">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-heart', true)}
</div>
<label>
${locale.localize('Heart')}
</label>
</div>
<div class="tui-image-editor-button" data-icontype="icon-bubble">
<div>
${makeSvgIcon(['normal', 'active'], 'icon-bubble', true)}
</div>
<label>
${locale.localize('Bubble')}
</label>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li class="tie-icon-add-button">
<div class="tui-image-editor-button" style="margin:0">
<div>
<input type="file" accept="image/*" class="tie-icon-image-file">
${makeSvgIcon(['normal', 'active'], 'icon-load', true)}
</div>
<label>
${locale.localize('Custom icon')}
</label>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li>
<div class="tie-icon-color" title="${locale.localize('Color')}"></div>
</li>
</ul>
`;
/**
* @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 }) => `
<ul class="tui-image-editor-submenu-item">
<li>
<div class="tui-image-editor-button">
<div>
<input type="file" accept="image/*" class="tie-mask-image-file">
${makeSvgIcon(['normal', 'active'], 'mask-load', true)}
</div>
<label> ${locale.localize('Load Mask Image')} </label>
</div>
</li>
<li class="tui-image-editor-partition only-left-right">
<div></div>
</li>
<li class="tie-mask-apply tui-image-editor-newline apply" style="margin-top: 22px;margin-bottom: 5px">
<div class="tui-image-editor-button apply">
${makeSvgIcon(['normal', 'active'], 'apply')}
<label>
${locale.localize('Apply')}
</label>
</div>
</li>
</ul>
`;
/**
* @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 }) => `
<ul class="tui-image-editor-submenu-item">
<li class="tie-retate-button">
<div class="tui-image-editor-button clockwise">
<div>
${makeSvgIcon(['normal', 'active'], 'rotate-clockwise', true)}
</div>
<label> 30 </label>
</div>
<div class="tui-image-editor-button counterclockwise">
<div>
${makeSvgIcon(['normal', 'active'], 'rotate-counterclockwise', true)}
</div>
<label> -30 </label>
</div>
</li>
<li class="tui-image-editor-partition only-left-right">
<div></div>
</li>
<li class="tui-image-editor-newline tui-image-editor-range-wrap">
<label class="range">${locale.localize('Range')}</label>
<div class="tie-rotate-range"></div>
<input class="tie-ratate-range-value tui-image-editor-range-value" value="0" />
</li>
</ul>
`;
/**
* @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 }) => `
<ul class="tui-image-editor-submenu-item">
<li class="tie-shape-button">
<div class="tui-image-editor-button rect">
<div>
${makeSvgIcon(['normal', 'active'], 'shape-rectangle', true)}
</div>
<label> ${locale.localize('Rectangle')} </label>
</div>
<div class="tui-image-editor-button circle">
<div>
${makeSvgIcon(['normal', 'active'], 'shape-circle', true)}
</div>
<label> ${locale.localize('Circle')} </label>
</div>
<div class="tui-image-editor-button triangle">
<div>
${makeSvgIcon(['normal', 'active'], 'shape-triangle', true)}
</div>
<label> ${locale.localize('Triangle')} </label>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li class="tie-shape-color-button">
<div class="tie-color-fill" title="${locale.localize('Fill')}"></div>
<div class="tie-color-stroke" title="${locale.localize('Stroke')}"></div>
</li>
<li class="tui-image-editor-partition only-left-right">
<div></div>
</li>
<li class="tui-image-editor-newline tui-image-editor-range-wrap">
<label class="range">${locale.localize('Stroke')}</label>
<div class="tie-stroke-range"></div>
<input class="tie-stroke-range-value tui-image-editor-range-value" value="0" />
</li>
</ul>
`;
/**
* @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 }) => `
<ul class="tui-image-editor-submenu-item">
<li class="tie-text-effect-button">
<div class="tui-image-editor-button bold">
<div>
${makeSvgIcon(['normal', 'active'], 'text-bold', true)}
</div>
<label> ${locale.localize('Bold')} </label>
</div>
<div class="tui-image-editor-button italic">
<div>
${makeSvgIcon(['normal', 'active'], 'text-italic', true)}
</div>
<label> ${locale.localize('Italic')} </label>
</div>
<div class="tui-image-editor-button underline">
<div>
${makeSvgIcon(['normal', 'active'], 'text-underline', true)}
</div>
<label> ${locale.localize('Underline')} </label>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li class="tie-text-align-button">
<div class="tui-image-editor-button left">
<div>
${makeSvgIcon(['normal', 'active'], 'text-align-left', true)}
</div>
<label> ${locale.localize('Left')} </label>
</div>
<div class="tui-image-editor-button center">
<div>
${makeSvgIcon(['normal', 'active'], 'text-align-center', true)}
</div>
<label> ${locale.localize('Center')} </label>
</div>
<div class="tui-image-editor-button right">
<div>
${makeSvgIcon(['normal', 'active'], 'text-align-right', true)}
</div>
<label> ${locale.localize('Right')} </label>
</div>
</li>
<li class="tui-image-editor-partition">
<div></div>
</li>
<li>
<div class="tie-text-color" title="${locale.localize('Color')}"></div>
</li>
<li class="tui-image-editor-partition only-left-right">
<div></div>
</li>
<li class="tui-image-editor-newline tui-image-editor-range-wrap">
<label class="range">${locale.localize('Text size')}</label>
<div class="tie-text-range"></div>
<input class="tie-text-range-value tui-image-editor-range-value" value="0" />
</li>
</ul>
`;
import { assignmentForDestroy } from '../util';
import Range from './tools/range';
import Colorpicker from './tools/colorpicker';
import Submenu from './submenuBase';
import templateHtml from './template/submenu/text';
import { defaultTextRangeValus } from '../consts';
/**
* Crop ui class
* @class
* @ignore
*/
export default 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 = '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'),
'#ffbb3b',
this.toggleDirection,
this.usageStatistics
),
textRange: new Range(
{
slider: this.selector('.tie-text-range'),
input: this.selector('.tie-text-range-value'),
},
defaultTextRangeValus
),
};
}
/**
* 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));
}
/**
* 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();
}
/**
* Returns the menu to its default state.
*/
changeStandbyMode() {
this.actions.stopDrawingMode();
}
/**
* Executed when the menu starts.
*/
changeStartMode() {
this.actions.modeChange('text');
}
set textColor(color) {
this._els.textColorpicker.color = color;
}
/**
* Get text color
* @returns {string} - text color
*/
get textColor() {
return this._els.textColorpicker.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.setEffactState('italic', fontStyle);
this.setEffactState('bold', fontWeight);
this.setEffactState('underline', textDecoration);
this.setAlignState(textAlign);
}
setEffactState(effactName, value) {
const effactValue = value === 'italic' || value === 'bold' || value === 'underline';
const button = this._els.textEffectButton.querySelector(
`.tui-image-editor-button.${effactName}`
);
this.effect[effactName] = effactValue;
button.classList[effactValue ? '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');
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']);
event.currentTarget.classList.remove(this.align);
if (this.align !== styleType) {
event.currentTarget.classList.add(styleType);
}
this.actions.changeTextStyle({ textAlign: styleType });
this.align = styleType;
}
}
/**
* 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,
});
}
}
/**
* @fileoverview The standard theme
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
*/
/**
* Full configuration for theme.<br>
* @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',
};
import { extend, forEach, map } from 'tui-code-snippet';
import { styleLoad } from '../../util';
import style from '../template/style';
import standardTheme from './standard';
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 defulat svg icon
* @private
*/
_loadDefaultSvgIcon() {
if (!document.getElementById('tui-image-editor-svg-default-icons')) {
const parser = new DOMParser();
const dom = parser.parseFromString(icon, '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.<string>} 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 map(useIconTypes, (iconType) => {
const svgIconPrefix = this._makeSvgIconPrefix(iconType, isSubmenu);
const iconName = this._toUnderScore(menuName);
const svgIconClassName = this._makeIconClassName(iconType, isSubmenu);
return `<use xlink:href="${svgIconPrefix}ic-${iconName}" class="${svgIconClassName}"/>`;
}).join('');
}
/**
* Make svg icon set
* @param {Array.<string>} 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 `<svg class="svg_ic-${isSubmenu ? 'submenu' : 'menu'}">${this._makeSvgItem(
useIconTypes,
menuName,
isSubmenu
)}</svg>`;
}
}
export default Theme;
import snippet from 'tui-code-snippet';
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 = '';
snippet.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`;
}
}
snippet.CustomEvents.mixin(Colorpicker);
export default Colorpicker;
import snippet from 'tui-code-snippet';
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 {number} [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.eventHandler = {
startChangingSlide: this._startChangingSlide.bind(this),
stopChangingSlide: this._stopChangingSlide.bind(this),
changeSlide: this._changeSlide.bind(this),
changeSlideFinally: this._changeSlideFinally.bind(this),
changeInput: this._changeValueWithInput.bind(this, false),
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 = '';
snippet.forEach(this, (value, key) => {
this[key] = null;
});
}
/**
* 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 max() {
return this._max;
}
/**
* Get range value
* @returns {Number} range value
*/
get value() {
return this._value;
}
/**
* Set range value
* @param {Number} value range value
* @param {Boolean} fire whether fire custom event or not
*/
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 tirigger
* @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('keyup', 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('keyup', 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;
}
/**
* 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) {
let value = this._useDecimal ? Number(stringValue) : toInteger(stringValue);
value = clamp(value, this._min, this.max);
this.value = value;
this.fire('change', value, isLast);
}
}
/**
* 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');
}
}
snippet.CustomEvents.mixin(Range);
export default Range;
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Util
*/
import { forEach, sendHostname, extend, isString, pick, inArray } from 'tui-code-snippet';
import Promise from 'core-js-pure/features/promise';
import { SHAPE_FILL_TYPE, SHAPE_TYPE } from './consts';
const FLOATING_POINT_DIGIT = 2;
const CSS_PREFIX = 'tui-image-editor-';
const { min, max } = Math;
let hostnameSent = false;
/**
* Export Promise Class (for simplified module path)
* @returns {Promise} promise class
*/
export { Promise };
/**
* 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) {
let temp;
if (minValue > maxValue) {
temp = minValue;
minValue = maxValue;
maxValue = temp;
}
return max(minValue, min(value, maxValue));
}
/**
* Make key-value object from arguments
* @returns {object.<string, string>}
*/
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;
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg display="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs id="tui-image-editor-svg-default-icons">
<symbol id="ic-apply" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" stroke="none" fill="none"/>
<path fill="none" stroke="inherit" d="M4 12.011l5 5L20.011 6"/>
</symbol>
<symbol id="ic-cancel" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" stroke="none"/>
<path fill="none" stroke="inherit" d="M6 6l12 12M18 6L6 18"/>
</symbol>
<symbol id="ic-crop" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" stroke="none" fill="none" />
<path stroke="none" fill="inherit" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path stroke="none" fill="inherit" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</symbol>
<symbol id="ic-delete-all" viewBox="0 0 24 24">
<path stroke="none" fill="inherit" d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path stroke="none" fill="inherit" d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</symbol>
<symbol id="ic-delete" viewBox="0 0 24 24">
<path stroke="none" fill="inherit" d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path stroke="none" fill="inherit" d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</symbol>
<symbol id="ic-draw-free" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</symbol>
<symbol id="ic-draw-line" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" d="M2 15.5h28"/>
</symbol>
<symbol id="ic-draw" viewBox="0 0 24 24">
<path fill="none" stroke="inherit" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path stroke="none" fill="inherit" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</symbol>
<symbol id="ic-filter" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" stroke="none" />
<path stroke="none" fill="inherit" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path stroke="none" fill="inherit" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</symbol>
<symbol id="ic-flip-reset" viewBox="0 0 31 32">
<path fill="none" stroke="none" d="M31 0H0v32h31z"/>
<path stroke="none" fill="inherit" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path fill="none" stroke="inherit" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</symbol>
<symbol id="ic-flip-x" viewBox="0 0 32 32">
<path fill="none" stroke="none" d="M32 32H0V0h32z"/>
<path stroke="none" fill="inherit" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</symbol>
<symbol id="ic-flip-y" viewBox="0 0 32 32">
<path fill="none" stroke="none" d="M0 0v32h32V0z"/>
<path stroke="none" fill="inherit" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</symbol>
<symbol id="ic-flip" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" stroke="none" />
<path fill="inherit" stroke="none" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</symbol>
<symbol id="ic-icon-arrow-2" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</symbol>
<symbol id="ic-icon-arrow-3" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</symbol>
<symbol id="ic-icon-arrow" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</symbol>
<symbol id="ic-icon-bubble" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</symbol>
<symbol id="ic-icon-heart" viewBox="0 0 32 32">
<path fill-rule="nonzero" fill="none" stroke="inherit" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</symbol>
<symbol id="ic-icon-load" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path stroke="none" fill="inherit" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path stroke="none" fill="inherit" d="M25 3h1v9h-1z"/>
<path fill="none" stroke="inherit" d="M22 6l3.5-3.5L29 6"/>
</symbol>
<symbol id="ic-icon-location" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle fill="none" stroke="inherit" cx="16" cy="13" r="4.5"/>
</symbol>
<symbol id="ic-icon-polygon" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</symbol>
<symbol id="ic-icon-star-2" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</symbol>
<symbol id="ic-icon-star" viewBox="0 0 32 32">
<path fill="none" stroke="inherit" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</symbol>
<symbol id="ic-icon" viewBox="0 0 24 24">
<path fill="none" stroke="inherit" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</symbol>
<symbol id="ic-mask-load" viewBox="0 0 32 32">
<path stroke="none" fill="none" d="M0 0h32v32H0z"/>
<path stroke="none" fill="inherit" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path stroke="none" fill="inherit" d="M25 3h1v9h-1z"/>
<path fill="none" stroke="inherit" d="M22 6l3.5-3.5L29 6"/>
</symbol>
<symbol id="ic-mask" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="4.5" stroke="inherit" fill="none"/>
<path stroke="none" fill="inherit" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</symbol>
<symbol id="ic-redo" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" opacity=".5" fill="none" stroke="none" />
<path stroke="none" fill="inherit" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path fill="none" stroke="inherit" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</symbol>
<symbol id="ic-reset" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" opacity=".5" stroke="none" fill="none"/>
<path stroke="none" fill="inherit" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path fill="none" stroke="inherit" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</symbol>
<symbol id="ic-rotate-clockwise" viewBox="0 0 32 32">
<path stroke="none" fill="inherit" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path fill="none" stroke="inherit" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path stroke="none" fill="inherit" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</symbol>
<symbol id="ic-rotate-counterclockwise" viewBox="0 0 32 32">
<path stroke="none" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path stroke="none" fill="inherit" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path fill="none" stroke="inherit" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</symbol>
<symbol id="ic-rotate" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" stroke="none" />
<path fill="inherit" stroke="none" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="inherit" fill="none" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</symbol>
<symbol id="ic-shape-circle" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14.5" fill="none" stroke="inherit"/>
</symbol>
<symbol id="ic-shape-rectangle" viewBox="0 0 32 32">
<rect width="27" height="27" x="2.5" y="2.5" fill="none" stroke="inherit" rx="1"/>
</symbol>
<symbol id="ic-shape-triangle" viewBox="0 0 32 32">
<path fill="none" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</symbol>
<symbol id="ic-shape" viewBox="0 0 24 24">
<path stroke="none" fill="inherit" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path fill="none" stroke="inherit" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</symbol>
<symbol id="ic-text-align-center" viewBox="0 0 32 32">
<path stroke="none" fill="none" d="M0 0h32v32H0z"/>
<path stroke="none" fill="inherit" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</symbol>
<symbol id="ic-text-align-left" viewBox="0 0 32 32">
<path stroke="none" fill="none" d="M0 0h32v32H0z"/>
<path stroke="none" fill="inherit" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</symbol>
<symbol id="ic-text-align-right" viewBox="0 0 32 32">
<path stroke="none" fill="none" d="M0 0h32v32H0z"/>
<path stroke="none" fill="inherit" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</symbol>
<symbol id="ic-text-bold" viewBox="0 0 32 32">
<path fill="none" stroke="none" d="M0 0h32v32H0z"/>
<path stroke="none" fill="inherit" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path fill="none" stroke="inherit" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</symbol>
<symbol id="ic-text-italic" viewBox="0 0 32 32">
<path fill="none" stroke="none" d="M0 0h32v32H0z"/>
<path stroke="none" fill="inherit" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</symbol>
<symbol id="ic-text-underline" viewBox="0 0 32 32">
<path stroke="none" fill="none" d="M0 0h32v32H0z"/>
<path stroke="none" fill="inherit" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path stroke="none" fill="inherit" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</symbol>
<symbol id="ic-text" viewBox="0 0 24 24">
<path stroke="none" fill="inherit" d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path stroke="none" fill="inherit" d="M11 3h1v18h-1z"/>
<path stroke="none" fill="inherit" d="M10 20h3v1h-3z"/>
</symbol>
<symbol id="ic-undo" viewBox="0 0 24 24">
<path d="M24 0H0v24h24z" opacity=".5" fill="none" stroke="none" />
<path stroke="none" fill="inherit" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path fill="none" stroke="inherit" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</symbol>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#434343" d="M4 12.011l5 5L20.011 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#434343" d="M6 6l12 12M18 6L6 18"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<circle id="a" cx="16" cy="16" r="16"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<use fill="#FFF" xlink:href="#a"/>
<circle cx="16" cy="16" r="15.5" stroke="#D5D5D5"/>
</g>
<path stroke="#FF4040" stroke-width="1.5" d="M27 5L5 27"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#434343" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#434343" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#434343" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M2 15.5h28"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#434343" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#434343" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#434343" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="31" height="32" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#434343" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#434343" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#434343" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#434343" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#434343" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#434343" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#434343" d="M25 3h1v9h-1z"/>
<path stroke="#434343" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#434343">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#434343" d="M25 3h1v9h-1z"/>
<path stroke="#434343" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#434343"/>
<path fill="#434343" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#434343" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#434343" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#434343" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#434343" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#434343" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#434343" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#434343" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#434343" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#434343" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#434343" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#434343" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#434343"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#434343" rx="1"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#434343" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#434343" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#434343" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#434343" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#434343" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#434343" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="257" height="26" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#555555" d="M4 12.011l5 5L20.011 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#555555" d="M6 6l12 12M18 6L6 18"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#555555" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#555555" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#555555" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M2 15.5h28"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#555555" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#555555" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#555555" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="31" height="32" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#555555" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#555555" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#555555" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#555555" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#555555" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#555555" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#555555" d="M25 3h1v9h-1z"/>
<path stroke="#555555" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#555555">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#555555" d="M25 3h1v9h-1z"/>
<path stroke="#555555" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#555555"/>
<path fill="#555555" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#555555" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#555555" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#555555" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#555555" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#555555" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#555555" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#555555" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#555555" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#555555" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#555555" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#555555" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#555555"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#555555" rx="1"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#555555" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#555555" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#555555" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#555555" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#555555" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#555555" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="257" height="26" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#e9e9e9" d="M4 12.011l5 5L20.011 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#e9e9e9" d="M6 6l12 12M18 6L6 18"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#e9e9e9" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#e9e9e9" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#e9e9e9" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M2 15.5h28"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#e9e9e9" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#e9e9e9" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#e9e9e9" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="31" height="32" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#e9e9e9" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#e9e9e9" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#e9e9e9" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#e9e9e9" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#e9e9e9" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#e9e9e9" d="M25 3h1v9h-1z"/>
<path stroke="#e9e9e9" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#e9e9e9">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#e9e9e9" d="M25 3h1v9h-1z"/>
<path stroke="#e9e9e9" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#e9e9e9"/>
<path fill="#e9e9e9" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#e9e9e9" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#e9e9e9" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#e9e9e9" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#e9e9e9" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#e9e9e9" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#e9e9e9" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#e9e9e9"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#e9e9e9" rx="1"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#e9e9e9" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#e9e9e9" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#e9e9e9" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#e9e9e9" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#e9e9e9" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="257" height="26" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#8a8a8a" d="M4 12.011l5 5L20.011 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#8a8a8a" d="M6 6l12 12M18 6L6 18"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#8a8a8a" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#8a8a8a" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#8a8a8a" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M2 15.5h28"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#8a8a8a" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#8a8a8a" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#8a8a8a" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="31" height="32" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#8a8a8a" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#8a8a8a" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#8a8a8a" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#8a8a8a" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#8a8a8a" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#8a8a8a" d="M25 3h1v9h-1z"/>
<path stroke="#8a8a8a" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#8a8a8a">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#8a8a8a" d="M25 3h1v9h-1z"/>
<path stroke="#8a8a8a" d="M22 6l3.5-3.5L29 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#8a8a8a"/>
<path fill="#8a8a8a" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#8a8a8a" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#8a8a8a" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#8a8a8a" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#8a8a8a" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#8a8a8a" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#8a8a8a" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#8a8a8a"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#8a8a8a" rx="1"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#8a8a8a" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#8a8a8a" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#8a8a8a" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="#8a8a8a" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#8a8a8a" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="257" height="26" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</svg>
module.exports = {
rules: {
'max-nested-callbacks': 0,
},
};
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/action.js"
*/
import snippet from 'tui-code-snippet';
import { Promise } from '../src/js/util';
import ImageEditor from '../src/js/imageEditor';
import action from '../src/js/action';
import { eventNames } from '../src/js/consts';
describe('Ui', () => {
let actions;
let imageEditorMock;
beforeEach(() => {
action.mixin(ImageEditor);
imageEditorMock = new ImageEditor(document.createElement('div'), {
includeUI: {
loadImage: false,
initMenu: 'flip',
menuBarPosition: 'bottom',
applyCropSelectionStyle: true,
},
});
actions = imageEditorMock.getActions();
spyOn(snippet, 'imagePing');
});
afterEach(() => {
imageEditorMock.destroy();
});
describe('mainAction', () => {
let mainAction;
beforeEach(() => {
mainAction = actions.main;
});
it('LoadImageFromURL() API should be executed When the initLoadImage action occurs', (done) => {
const promise = new Promise((resolve) => {
resolve(300);
});
spyOn(imageEditorMock, 'loadImageFromURL').and.returnValue(promise);
spyOn(imageEditorMock, 'clearUndoStack');
spyOn(imageEditorMock.ui, 'resizeEditor');
mainAction.initLoadImage('path', 'imageName').then(() => {
expect(imageEditorMock.clearUndoStack).toHaveBeenCalled();
expect(imageEditorMock.ui.resizeEditor).toHaveBeenCalled();
expect(imageEditorMock.loadImageFromURL).toHaveBeenCalled();
done();
});
});
it('Undo() API should be executed When the undo action occurs', () => {
spyOn(imageEditorMock, 'isEmptyUndoStack').and.returnValue(false);
spyOn(imageEditorMock, 'undo').and.returnValue({ then: () => {} });
mainAction.undo();
expect(imageEditorMock.undo).toHaveBeenCalled();
});
it('Redo() API should be executed When the redo action occurs', () => {
spyOn(imageEditorMock, 'isEmptyRedoStack').and.returnValue(false);
spyOn(imageEditorMock, 'redo').and.returnValue({ then: () => {} });
mainAction.redo();
expect(imageEditorMock.redo).toHaveBeenCalled();
});
it('removeObject() API should be executed When the delete action occurs', () => {
imageEditorMock.activeObjectId = 10;
spyOn(imageEditorMock, 'removeActiveObject');
mainAction['delete']();
expect(imageEditorMock.removeActiveObject).toHaveBeenCalled();
expect(imageEditorMock.activeObjectId).toBe(null);
});
it('clearObjects() API should be run and the enabled state should be changed When the deleteAll action occurs', () => {
spyOn(imageEditorMock, 'clearObjects');
spyOn(imageEditorMock.ui, 'changeHelpButtonEnabled');
mainAction.deleteAll();
const changeHelpButtonCalls = imageEditorMock.ui.changeHelpButtonEnabled.calls;
expect(imageEditorMock.clearObjects).toHaveBeenCalled();
expect(changeHelpButtonCalls.argsFor(0)[0]).toBe('delete');
expect(changeHelpButtonCalls.argsFor(1)[0]).toBe('deleteAll');
});
it('loadImageFromFile() API should be executed When the load action occurs', (done) => {
const promise = new Promise((resolve) => {
resolve();
});
spyOn(imageEditorMock, 'loadImageFromFile').and.returnValue(promise);
spyOn(imageEditorMock, 'clearUndoStack');
spyOn(imageEditorMock.ui, 'resizeEditor');
window.URL = {
createObjectURL: jasmine.createSpy('URL'),
};
mainAction.load();
promise.then(() => {
expect(imageEditorMock.loadImageFromFile).toHaveBeenCalled();
expect(imageEditorMock.clearUndoStack).toHaveBeenCalled();
expect(imageEditorMock.ui.resizeEditor).toHaveBeenCalled();
done();
});
});
});
describe('shapeAction', () => {
let shapeAction;
beforeEach(() => {
shapeAction = actions.shape;
});
it('changeShape() API should be executed When the changeShape action occurs', () => {
imageEditorMock.activeObjectId = 10;
spyOn(imageEditorMock, 'changeShape');
shapeAction.changeShape({
strokeWidth: '#000000',
});
expect(imageEditorMock.changeShape).toHaveBeenCalled();
});
it('setDrawingShape() API should be executed When the setDrawingShape action occurs', () => {
spyOn(imageEditorMock, 'setDrawingShape');
shapeAction.setDrawingShape();
expect(imageEditorMock.setDrawingShape).toHaveBeenCalled();
});
});
describe('cropAction', () => {
let cropAction;
beforeEach(() => {
cropAction = actions.crop;
});
it('getCropzoneRect(), stopDrawingMode(), ui.resizeEditor(), ui.changeMenu() API should be executed When the crop action occurs', (done) => {
const promise = new Promise((resolve) => {
resolve();
});
spyOn(imageEditorMock, 'crop').and.returnValue(promise);
spyOn(imageEditorMock, 'getCropzoneRect').and.returnValue(true);
spyOn(imageEditorMock, 'stopDrawingMode');
spyOn(imageEditorMock.ui, 'resizeEditor');
spyOn(imageEditorMock.ui, 'changeMenu');
cropAction.crop();
expect(imageEditorMock.getCropzoneRect).toHaveBeenCalled();
expect(imageEditorMock.crop).toHaveBeenCalled();
promise.then(() => {
expect(imageEditorMock.stopDrawingMode).toHaveBeenCalled();
expect(imageEditorMock.ui.resizeEditor).toHaveBeenCalled();
expect(imageEditorMock.ui.changeMenu).toHaveBeenCalled();
done();
});
});
it('stopDrawingMode() API should be executed When the cancel action occurs', () => {
spyOn(imageEditorMock, 'stopDrawingMode');
spyOn(imageEditorMock.ui, 'changeMenu');
cropAction.cancel();
expect(imageEditorMock.stopDrawingMode).toHaveBeenCalled();
expect(imageEditorMock.ui.changeMenu).toHaveBeenCalled();
});
});
describe('flipAction', () => {
let flipAction;
beforeEach(() => {
flipAction = actions.flip;
});
it('{flipType}() API should be executed When the flip(fliptype) action occurs', () => {
spyOn(imageEditorMock, 'flipX');
spyOn(imageEditorMock, 'flipY');
flipAction.flip('flipX');
expect(imageEditorMock.flipX).toHaveBeenCalled();
flipAction.flip('flipY');
expect(imageEditorMock.flipY).toHaveBeenCalled();
});
});
describe('rotateAction', () => {
let rotateAction;
beforeEach(() => {
rotateAction = actions.rotate;
});
it('rotate() API should be executed When the rotate action occurs', () => {
spyOn(imageEditorMock, 'rotate');
spyOn(imageEditorMock.ui, 'resizeEditor');
rotateAction.rotate(30);
expect(imageEditorMock.rotate).toHaveBeenCalled();
expect(imageEditorMock.ui.resizeEditor).toHaveBeenCalled();
});
it('setAngle() API should be executed When the setAngle action occurs', () => {
spyOn(imageEditorMock, 'setAngle');
spyOn(imageEditorMock.ui, 'resizeEditor');
rotateAction.setAngle(30);
expect(imageEditorMock.setAngle).toHaveBeenCalled();
expect(imageEditorMock.ui.resizeEditor).toHaveBeenCalled();
});
});
describe('textAction', () => {
let textAction;
beforeEach(() => {
textAction = actions.text;
});
it('changeTextStyle() API should be executed When the changeTextStyle action occurs', () => {
imageEditorMock.activeObjectId = 10;
spyOn(imageEditorMock, 'changeTextStyle');
textAction.changeTextStyle({ fontSize: 10 });
expect(imageEditorMock.changeTextStyle.calls.mostRecent().args[0]).toBe(10);
expect(imageEditorMock.changeTextStyle.calls.mostRecent().args[1]).toEqual({ fontSize: 10 });
});
});
describe('maskAction', () => {
let maskAction;
beforeEach(() => {
maskAction = actions.mask;
});
it('applyFilter() API should be executed When the applyFilter action occurs', () => {
imageEditorMock.activeObjectId = 10;
spyOn(imageEditorMock, 'applyFilter');
maskAction.applyFilter();
expect(imageEditorMock.applyFilter.calls.mostRecent().args[1]).toEqual({ maskObjId: 10 });
});
});
describe('drawAction', () => {
let drawAction, expected;
beforeEach(() => {
drawAction = actions.draw;
});
it('startDrawingMode("FREE_DRAWING") API should be executed When the setDrawMode("free") action occurs', () => {
spyOn(imageEditorMock, 'startDrawingMode');
drawAction.setDrawMode('free');
expected = imageEditorMock.startDrawingMode.calls.mostRecent().args[0];
expect(expected).toBe('FREE_DRAWING');
});
it('setBrush() API should be executed When the setColor() action occurs', () => {
spyOn(imageEditorMock, 'setBrush');
drawAction.setColor('#000000');
expected = imageEditorMock.setBrush.calls.mostRecent().args[0].color;
expect(expected).toBe('#000000');
});
});
describe('iconAction', () => {
let iconAction;
beforeEach(() => {
iconAction = actions.icon;
});
it('when the add icon occurs, the drawing mode should be run.', () => {
spyOn(imageEditorMock, 'startDrawingMode');
spyOn(imageEditorMock, 'setDrawingIcon');
iconAction.addIcon('iconTypeA', '#fff');
expect(imageEditorMock.startDrawingMode).toHaveBeenCalled();
expect(imageEditorMock.setDrawingIcon).toHaveBeenCalled();
});
});
describe('filterAction', () => {
let filterAction;
beforeEach(() => {
filterAction = actions.filter;
});
it('removeFilter() API should be executed When the type of applyFilter is false', () => {
spyOn(imageEditorMock, 'removeFilter');
spyOn(imageEditorMock, 'hasFilter').and.returnValue(true);
filterAction.applyFilter(false, {});
expect(imageEditorMock.removeFilter).toHaveBeenCalled();
});
it('applyFilter() API should be executed When the type of applyFilter is true', () => {
spyOn(imageEditorMock, 'applyFilter');
filterAction.applyFilter(true, {});
expect(imageEditorMock.applyFilter).toHaveBeenCalled();
});
});
describe('commonAction', () => {
it('Each action returned to the getActions method must contain commonAction.', () => {
const submenus = [
'shape',
'crop',
'flip',
'rotate',
'text',
'mask',
'draw',
'icon',
'filter',
];
snippet.forEach(submenus, (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('_changeActivateMode("TEXT") API should be executed When the modeChange("text") action occurs', () => {
spyOn(imageEditorMock, '_changeActivateMode');
commonAction.modeChange('text');
expect(imageEditorMock._changeActivateMode).toHaveBeenCalled();
});
it('startDrawingMode() API should be executed When the modeChange("crop") action occurs', () => {
spyOn(imageEditorMock, 'startDrawingMode');
commonAction.modeChange('crop');
expect(imageEditorMock.startDrawingMode).toHaveBeenCalled();
});
it('stopDrawingMode(), setDrawingShape(), _changeActivateMode() API should be executed When the modeChange("shape") action occurs', () => {
spyOn(imageEditorMock, 'setDrawingShape');
spyOn(imageEditorMock, '_changeActivateMode');
commonAction.modeChange('shape');
expect(imageEditorMock.setDrawingShape).toHaveBeenCalled();
expect(imageEditorMock._changeActivateMode).toHaveBeenCalled();
});
});
});
describe('reAction', () => {
beforeEach(() => {
imageEditorMock.setReAction();
spyOn(imageEditorMock.ui, 'changeHelpButtonEnabled');
});
describe('undoStackChanged', () => {
it('If the undo stack has a length greater than zero, the state of changeUndoButtonStatus, changeResetButtonStatus should be true.', () => {
imageEditorMock.fire('undoStackChanged', 1);
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(0)).toEqual(['undo', true]);
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(1)).toEqual([
'reset',
true,
]);
});
it('If the undo stack has a length of 0, the state of changeUndoButtonStatus, changeResetButtonStatus should be false.', () => {
imageEditorMock.fire('undoStackChanged', 0);
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(0)).toEqual([
'undo',
false,
]);
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(1)).toEqual([
'reset',
false,
]);
});
});
describe('redoStackChanged', () => {
it('If the redo stack is greater than zero length, the state of changeRedoButtonStatus should be true.', () => {
imageEditorMock.fire('redoStackChanged', 1);
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(0)).toEqual(['redo', true]);
});
it('If the redo stack has a length of zero, the state of changeRedoButtonStatus should be false.', () => {
imageEditorMock.fire('redoStackChanged', 0);
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(0)).toEqual([
'redo',
false,
]);
});
});
describe('objectActivated', () => {
it('When objectActivated occurs, the state of the delete button should be enabled.', () => {
imageEditorMock.fire('objectActivated', { id: 1 });
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(0)).toEqual([
'delete',
true,
]);
expect(imageEditorMock.ui.changeHelpButtonEnabled.calls.argsFor(1)).toEqual([
'deleteAll',
true,
]);
});
it("When objectActivated's target is cropzone, changeApplyButtonStatus should be enabled.", () => {
spyOn(imageEditorMock.ui.crop, 'changeApplyButtonStatus');
imageEditorMock.fire('objectActivated', {
id: 1,
type: 'cropzone',
});
expect(imageEditorMock.ui.crop.changeApplyButtonStatus.calls.mostRecent().args[0]).toBe(
true
);
});
it('If the target of objectActivated is shape and the existing menu is not shpe, the menu should be changed to shape.', () => {
imageEditorMock.ui.submenu = 'crop';
spyOn(imageEditorMock.ui, 'changeMenu');
spyOn(imageEditorMock.ui.shape, 'setShapeStatus');
spyOn(imageEditorMock.ui.shape, 'setMaxStrokeValue');
imageEditorMock.fire('objectActivated', {
id: 1,
type: 'circle',
});
expect(imageEditorMock.ui.changeMenu.calls.mostRecent().args[0]).toBe('shape');
expect(imageEditorMock.ui.shape.setMaxStrokeValue).toHaveBeenCalled();
});
it('If the target of objectActivated is text and the existing menu is not text, the menu should be changed to text.', () => {
imageEditorMock.ui.submenu = 'crop';
spyOn(imageEditorMock.ui, 'changeMenu');
imageEditorMock.fire('objectActivated', {
id: 1,
type: 'i-text',
});
expect(imageEditorMock.ui.changeMenu.calls.mostRecent().args[0]).toBe('text');
});
it('If the target of objectActivated is icon and the existing menu is not icon, the menu should be changed to icon.', () => {
imageEditorMock.ui.submenu = 'crop';
spyOn(imageEditorMock.ui, 'changeMenu');
spyOn(imageEditorMock.ui.icon, 'setIconPickerColor');
imageEditorMock.fire('objectActivated', {
id: 1,
type: 'icon',
});
expect(imageEditorMock.ui.changeMenu.calls.mostRecent().args[0]).toBe('icon');
expect(imageEditorMock.ui.icon.setIconPickerColor).toHaveBeenCalled();
});
});
describe('addObjectAfter', () => {
it("When addObjectAfter occurs, the shape's maxStrokeValue should be changed to match the size of the added object.", () => {
spyOn(imageEditorMock.ui.shape, 'setMaxStrokeValue');
spyOn(imageEditorMock.ui.shape, 'changeStandbyMode');
imageEditorMock.fire('addObjectAfter', {
type: 'circle',
width: 100,
height: 200,
});
expect(imageEditorMock.ui.shape.setMaxStrokeValue.calls.mostRecent().args[0]).toBe(100);
expect(imageEditorMock.ui.shape.changeStandbyMode).toHaveBeenCalled();
});
});
describe('objectScaled', () => {
it('If objectScaled occurs on an object of type text, fontSize must be changed.', () => {
imageEditorMock.ui.text.fontSize = 0;
imageEditorMock.fire('objectScaled', {
type: 'i-text',
fontSize: 20,
});
expect(imageEditorMock.ui.text.fontSize).toBe(20);
});
it('If objectScaled is for a shape type object and strokeValue is greater than the size of the object, the value should change.', () => {
spyOn(imageEditorMock.ui.shape, 'getStrokeValue').and.returnValue(20);
spyOn(imageEditorMock.ui.shape, 'setStrokeValue');
imageEditorMock.fire('objectScaled', {
type: 'rect',
width: 10,
height: 10,
});
expect(imageEditorMock.ui.shape.setStrokeValue.calls.mostRecent().args[0]).toBe(10);
});
});
describe('selectionCleared', () => {
it('If selectionCleared occurs in the text menu state, the menu should be closed.', () => {
imageEditorMock.ui.submenu = 'text';
spyOn(imageEditorMock, 'changeCursor');
imageEditorMock.fire('selectionCleared');
expect(imageEditorMock.changeCursor.calls.mostRecent().args[0]).toBe('text');
});
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/extension/allowLine.js"
*/
import ArrowLine from '../src/js/extension/arrowLine';
describe('AllowLine', () => {
let ctx, arrowLine, linePath;
beforeEach(() => {
ctx = {
lineWidth: 1,
beginPath: jasmine.createSpy('beginPath'),
moveTo: jasmine.createSpy('moveTo'),
lineTo: jasmine.createSpy('lineTo'),
closePath: jasmine.createSpy('closePath'),
};
arrowLine = new ArrowLine();
arrowLine.ctx = ctx;
linePath = {
fromX: 1,
fromY: 1,
toX: 10,
toY: 10,
};
});
it('When attaching the "chevron" type to the endpoint, you need to draw the "v" calculated according to the angle around the "tail" of the line.', () => {
arrowLine.arrowType = {
tail: 'chevron',
};
arrowLine._drawDecoratorPath(linePath);
const firstPoint = ctx.moveTo.calls.argsFor(0).map((value) => Math.round(value));
const secondPoint = ctx.lineTo.calls.argsFor(0).map((value) => Math.round(value));
const lastPoint = ctx.lineTo.calls.argsFor(1).map((value) => Math.round(value));
expect(firstPoint).toEqual([9, 7]);
expect(secondPoint).toEqual([10, 10]);
expect(lastPoint).toEqual([7, 9]);
});
it('When attaching the "chevron" type to the startpoint, you need to draw the "v" calculated according to the angle around the "head" of the line.', () => {
arrowLine.arrowType = {
head: 'chevron',
};
arrowLine._drawDecoratorPath(linePath);
const firstPoint = ctx.moveTo.calls.argsFor(0).map((value) => Math.round(value));
const secondPoint = ctx.lineTo.calls.argsFor(0).map((value) => Math.round(value));
const lastPoint = ctx.lineTo.calls.argsFor(1).map((value) => Math.round(value));
expect(firstPoint).toEqual([2, 4]);
expect(secondPoint).toEqual([1, 1]);
expect(lastPoint).toEqual([4, 2]);
});
it('"triangle" should be a triangular shape that closes the path with closePath after drawing.', () => {
arrowLine.arrowType = {
head: 'triangle',
};
arrowLine._drawDecoratorPath(linePath);
const firstPoint = ctx.moveTo.calls.argsFor(0).map((value) => Math.round(value));
const secondPoint = ctx.lineTo.calls.argsFor(0).map((value) => Math.round(value));
const thirdPoint = ctx.lineTo.calls.argsFor(1).map((value) => Math.round(value));
expect(firstPoint).toEqual([1, 3]);
expect(secondPoint).toEqual([1, 1]);
expect(thirdPoint).toEqual([3, 1]);
expect(ctx.closePath.calls.count()).toBe(1);
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Tests command with command-factory
*/
import snippet from 'tui-code-snippet';
import { Promise } from '../src/js/util';
import fabric from 'fabric';
import Invoker from '../src/js/invoker';
import commandFactory from '../src/js/factory/command';
import Graphics from '../src/js/graphics';
import { commandNames as commands } from '../src/js/consts';
import { getCachedUndoDataForDimension } from '../src/js/helper/selectionModifyHelper';
describe('commandFactory', () => {
let invoker, mockImage, canvas, graphics;
beforeEach(() => {
graphics = new Graphics(document.createElement('canvas'));
invoker = new Invoker();
mockImage = new fabric.Image();
graphics.setCanvasImage('', mockImage);
canvas = graphics.getCanvas();
});
describe('functions', () => {
it('can register custom command', (done) => {
const testCommand = {
name: 'testCommand',
execute() {},
undo() {},
};
spyOn(testCommand, 'execute').and.returnValue(Promise.resolve('testCommand'));
spyOn(testCommand, 'undo').and.returnValue(Promise.resolve());
commandFactory.register(testCommand);
const command = commandFactory.create('testCommand');
expect(command).not.toBe(null);
invoker
.execute('testCommand', graphics)
.then((commandName) => {
expect(commandName).toBe('testCommand');
expect(testCommand.execute).toHaveBeenCalledWith(graphics);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('can pass parameters on execute', (done) => {
commandFactory.register({
name: 'testCommand',
execute(compMap, obj1, obj2, obj3) {
expect(obj1).toBe(1);
expect(obj2).toBe(2);
expect(obj3).toBe(3);
return Promise.resolve();
},
});
invoker
.execute('testCommand', graphics, 1, 2, 3)
.then(() => {
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('can pass parameters on undo', (done) => {
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();
},
});
invoker
.execute('testCommand', graphics, 1, 2, 3)
.then(() => invoker.undo())
.then(() => done())
['catch']((message) => {
fail(message);
done();
});
});
});
describe('addObjectCommand', () => {
let obj;
it('should stamp object', (done) => {
obj = new fabric.Rect();
invoker.execute(commands.ADD_OBJECT, graphics, obj).then(() => {
expect(snippet.hasStamp(obj)).toBe(true);
done();
});
});
it('should add object to canvas', (done) => {
obj = new fabric.Rect();
invoker.execute(commands.ADD_OBJECT, graphics, obj).then(() => {
expect(canvas.contains(obj)).toBe(true);
done();
});
});
it('"undo()" should remove object from canvas', (done) => {
obj = new fabric.Rect();
invoker
.execute(commands.ADD_OBJECT, graphics, obj)
.then(() => invoker.undo())
.then(() => {
expect(canvas.contains(obj)).toBe(false);
done();
});
});
});
describe('changeSelectionCommand', () => {
let obj;
beforeEach(() => {
spyOn(canvas, 'getPointer');
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', (done) => {
invoker.undo().then(() => {
expect(obj.width).toBe(10);
expect(obj.height).toBe(10);
expect(obj.left).toBe(10);
expect(obj.top).toBe(10);
expect(obj.scaleX).toBe(1);
expect(obj.scaleY).toBe(1);
expect(obj.angle).toBe(0);
done();
});
});
it('should work redo command correctly', (done) => {
invoker.undo().then(() => {
invoker.redo().then(() => {
expect(obj.width).toBe(30);
expect(obj.height).toBe(30);
expect(obj.left).toBe(30);
expect(obj.top).toBe(30);
expect(obj.scaleX).toBe(0.5);
expect(obj.scaleY).toBe(0.5);
expect(obj.angle).toBe(10);
done();
});
});
});
});
describe('loadImageCommand', () => {
const imageURL = 'base/test/fixtures/sampleImage.jpg';
beforeEach(() => {
graphics.setCanvasImage('', null);
});
it('should clear canvas', () => {
spyOn(canvas, 'clear');
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL);
expect(canvas.clear).toHaveBeenCalled();
});
it('should load new image', (done) => {
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then((sizeChange) => {
expect(graphics.getImageName()).toEqual('image');
expect(graphics.getCanvasImage().getSrc()).toContain(imageURL);
expect(sizeChange.oldWidth).toEqual(jasmine.any(Number));
expect(sizeChange.oldHeight).toEqual(jasmine.any(Number));
expect(sizeChange.newWidth).toEqual(jasmine.any(Number));
expect(sizeChange.newHeight).toEqual(jasmine.any(Number));
done();
});
});
it('After running the LOAD_IMAGE command, existing objects should not include cropzone.', (done) => {
const objCropzone = new fabric.Object({ type: 'cropzone' });
invoker.execute(commands.ADD_OBJECT, graphics, objCropzone).then(() => {
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => {
const lastUndoIndex = invoker._undoStack.length - 1;
const savedObjects = invoker._undoStack[lastUndoIndex].undoData.objects;
expect(savedObjects.length).toBe(0);
done();
});
});
});
it('`evented` attribute of the saved object must be true after LOAD_IMAGE.', (done) => {
const objCircle = new fabric.Object({
type: 'circle',
evented: false,
});
invoker.execute(commands.ADD_OBJECT, graphics, objCircle).then(() => {
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => {
const lastUndoIndex = invoker._undoStack.length - 1;
const [savedObject] = invoker._undoStack[lastUndoIndex].undoData.objects;
expect(savedObject.evented).toBe(true);
done();
});
});
});
it('"undo()" should clear image if not exists prev image', (done) => {
invoker
.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL)
.then(() => invoker.undo())
.then(() => {
expect(graphics.getCanvasImage()).toBe(null);
expect(graphics.getImageName()).toBe('');
done();
});
});
it('"undo()" should restore to prev image', (done) => {
const newImageURL = 'base/test/fixtures/TOAST%20UI%20Component.png';
invoker
.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL)
.then(() => invoker.execute(commands.LOAD_IMAGE, graphics, 'newImage', newImageURL))
.then(() => {
expect(graphics.getImageName()).toBe('newImage');
expect(graphics.getCanvasImage().getSrc()).toContain(newImageURL);
return invoker.undo();
})
.then(() => {
expect(graphics.getImageName()).toEqual('image');
expect(graphics.getCanvasImage().getSrc()).toContain(imageURL);
done();
});
});
});
describe('flipImageCommand', () => {
it('flipX', () => {
const originFlipX = mockImage.flipX;
invoker.execute(commands.FLIP_IMAGE, graphics, 'flipX');
expect(mockImage.flipX).toBe(!originFlipX);
});
it('flipY', () => {
const originFlipY = mockImage.flipY;
invoker.execute(commands.FLIP_IMAGE, graphics, 'flipY');
expect(mockImage.flipY).toBe(!originFlipY);
});
it('resetFlip', () => {
mockImage.flipX = true;
mockImage.flipY = true;
invoker.execute(commands.FLIP_IMAGE, graphics, 'reset');
expect(mockImage.flipX).toBe(false);
expect(mockImage.flipY).toBe(false);
});
it('"undo()" should restore flipX', (done) => {
const originFlipX = mockImage.flipX;
invoker
.execute(commands.FLIP_IMAGE, graphics, 'flipX')
.then(() => invoker.undo())
.then(() => {
expect(mockImage.flipX).toBe(originFlipX);
done();
});
});
it('"undo()" should restore filpY', (done) => {
const originFlipY = mockImage.flipY;
invoker
.execute(commands.FLIP_IMAGE, graphics, 'flipY')
.then(() => invoker.undo())
.then(() => {
expect(mockImage.flipY).toBe(originFlipY);
done();
});
});
});
describe('textCommand', () => {
let textObjectId;
const defaultFontSize = 50;
const defaultUnderline = false;
beforeEach((done) => {
invoker
.execute(commands.ADD_TEXT, graphics, 'text', {
styles: {
fontSize: defaultFontSize,
underline: false,
},
})
.then((textObject) => {
textObjectId = textObject.id;
done();
});
});
it('"changeTextStyle" should set text style', (done) => {
invoker
.execute(commands.CHANGE_TEXT_STYLE, graphics, textObjectId, {
fontSize: 30,
underline: true,
})
.then(() => {
const textObject = graphics.getObject(textObjectId);
expect(textObject.fontSize).toBe(30);
expect(textObject.underline).toBe(true);
done();
});
});
it('"undo()" should restore fontSize', (done) => {
invoker
.execute(commands.CHANGE_TEXT_STYLE, graphics, textObjectId, {
fontSize: 30,
underline: true,
})
.then(() => invoker.undo())
.then(() => {
const textObject = graphics.getObject(textObjectId);
expect(textObject.fontSize).toBe(defaultFontSize);
expect(textObject.underline).toBe(defaultUnderline);
done();
});
});
});
describe('rotationImageCommand', () => {
it('"rotate()" should add angle', () => {
const originAngle = mockImage.angle;
invoker.execute(commands.ROTATE_IMAGE, graphics, 'rotate', 10);
expect(mockImage.angle).toBe(originAngle + 10);
});
it('"setAngle()" should set angle', () => {
mockImage.angle = 100;
invoker.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 30);
expect(mockImage.angle).toBe(30);
});
it('"undo()" should restore angle', (done) => {
const originalAngle = mockImage.angle;
invoker
.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 100)
.then(() => invoker.undo())
.then(() => {
expect(mockImage.angle).toBe(originalAngle);
done();
});
});
});
describe('shapeCommand', () => {
let shapeObjectId;
const defaultStrokeWidth = 12;
beforeEach((done) => {
invoker
.execute(commands.ADD_SHAPE, graphics, 'rect', {
strokeWidth: defaultStrokeWidth,
})
.then((shapeObject) => {
shapeObjectId = shapeObject.id;
done();
});
});
it('"changeShape" should set strokeWidth', (done) => {
invoker
.execute(commands.CHANGE_SHAPE, graphics, shapeObjectId, {
strokeWidth: 50,
})
.then(() => {
const shapeObject = graphics.getObject(shapeObjectId);
expect(shapeObject.strokeWidth).toBe(50);
done();
});
});
it('"redo()" should restore strokeWidth', (done) => {
invoker
.execute(commands.CHANGE_SHAPE, graphics, shapeObjectId, {
strokeWidth: 50,
})
.then(() => invoker.undo())
.then(() => {
const shapeObject = graphics.getObject(shapeObjectId);
expect(shapeObject.strokeWidth).toBe(defaultStrokeWidth);
done();
});
});
});
describe('clearCommand', () => {
let canvasContext;
beforeEach(() => {
canvasContext = canvas;
});
it('should clear all objects', () => {
const objects = [new fabric.Rect(), new fabric.Rect(), new fabric.Rect()];
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);
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('"undo()" restore all objects', (done) => {
const objects = [new fabric.Rect(), new fabric.Rect(), new fabric.Rect()];
canvas.add.apply(canvasContext, objects);
invoker
.execute(commands.CLEAR_OBJECTS, graphics)
.then(() => invoker.undo())
.then(() => {
expect(canvas.contains(objects[0])).toBe(true);
expect(canvas.contains(objects[1])).toBe(true);
expect(canvas.contains(objects[2])).toBe(true);
done();
});
});
});
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', () => {
graphics.setActiveObject(object);
invoker.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(object));
expect(canvas.contains(object)).toBe(false);
});
it('should remove objects group', () => {
canvas.setActiveObject(group);
invoker.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(group));
expect(canvas.contains(object)).toBe(false);
expect(canvas.contains(object2)).toBe(false);
});
it('"undo()" should restore the removed object', (done) => {
canvas.setActiveObject(object);
invoker
.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(object))
.then(() => invoker.undo())
.then(() => {
expect(canvas.contains(object)).toBe(true);
done();
});
});
it('"undo()" should restore the removed objects (group)', (done) => {
canvas.setActiveObject(group);
invoker
.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(group))
.then(() => invoker.undo())
.then(() => {
expect(canvas.contains(object)).toBe(true);
expect(canvas.contains(object2)).toBe(true);
done();
});
});
it('"undo ()" should restore the position of the removed object (group). ', (done) => {
const activeSelection = graphics.getActiveSelectionFromObjects(canvas.getObjects());
graphics.setActiveObject(activeSelection);
invoker
.execute(commands.REMOVE_OBJECT, graphics, graphics.getActiveObjectIdForRemove())
.then(() => invoker.undo())
.then(() => {
expect(object.left).toBe(10);
expect(object.top).toBe(10);
expect(object2.left).toBe(5);
expect(object2.top).toBe(20);
done();
});
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/cropper.js"
*/
import snippet from 'tui-code-snippet';
import fabric from 'fabric';
import $ from 'jquery';
import Cropper from '../src/js/component/cropper';
import Graphics from '../src/js/graphics';
import { eventNames, CROPZONE_DEFAULT_OPTIONS } from '../src/js/consts';
describe('Cropper', () => {
let cropper, graphics, canvas;
beforeEach(() => {
graphics = new Graphics($('<canvas>')[0]);
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;
snippet.forEach(CROPZONE_DEFAULT_OPTIONS, (optionValue, optionName) => {
expect(cropzone[optionName]).toBe(optionValue);
});
});
it('should add a cropzone to canvas', () => {
spyOn(canvas, 'add');
cropper.start();
expect(canvas.add).toHaveBeenCalledWith(cropper._cropzone);
});
it('should no action if a croppzone has been defined', () => {
cropper._cropzone = {};
spyOn(canvas, 'add');
cropper.start();
expect(canvas.add).not.toHaveBeenCalled();
});
it('should set "evented" of all objects to false', () => {
const objects = [
new fabric.Rect({ evented: true }),
new fabric.Rect({ evented: true }),
new fabric.Rect({ evented: true }),
];
canvas.add(objects[0], objects[1], objects[2]);
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: {},
};
spyOn(canvas, 'getPointer').and.returnValue({
x: 10,
y: 20,
});
});
it('should set "selection" to false', () => {
cropper._onFabricMouseDown(fEvent);
expect(canvas.selection).toBe(false);
});
it('should set "startX, startY"', () => {
// canvas.getPointer will return object{x: 10, y: 20}
cropper._onFabricMouseDown(fEvent);
expect(cropper._startX).toEqual(10);
expect(cropper._startY).toEqual(20);
});
});
describe('"onFabricMouseMove()', () => {
beforeEach(() => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 10,
y: 20,
});
spyOn(canvas, 'getWidth').and.returnValue(100);
spyOn(canvas, 'getHeight').and.returnValue(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();
spyOn(canvas, 'remove');
spyOn(cropper._cropzone, 'set');
spyOn(canvas, 'add');
cropper._onFabricMouseMove({ e: {} });
expect(canvas.remove).toHaveBeenCalled();
expect(cropper._cropzone.set).toHaveBeenCalled();
expect(canvas.add).toHaveBeenCalled();
}
);
it('should not re-render cropzone ' + 'if the mouse moving is under the threshold', () => {
cropper._startX = 14;
cropper._startY = 18;
cropper.start();
spyOn(canvas, 'remove');
spyOn(cropper._cropzone, 'set');
spyOn(canvas, 'add');
cropper._onFabricMouseMove({ e: {} });
expect(canvas.remove).not.toHaveBeenCalled();
expect(cropper._cropzone.set).not.toHaveBeenCalled();
expect(canvas.add).not.toHaveBeenCalled();
});
});
describe('_calcRectDimensionFromPoint()', () => {
beforeEach(() => {
cropper._startX = 10;
cropper._startY = 20;
snippet.extend(canvas, {
getWidth() {
return 100;
},
getHeight() {
return 200;
},
});
});
it('should return cropzone-left&top (min: 0, max: startX,Y)', () => {
const x = 20,
y = -1,
expected = {
left: 10,
top: 0,
width: jasmine.any(Number),
height: jasmine.any(Number),
},
actual = cropper._calcRectDimensionFromPoint(x, y);
expect(actual).toEqual(expected);
});
it('should calculate and return cropzone-width&height', () => {
let x, y, expected, actual;
x = 30;
y = 40;
expected = {
left: 10,
top: 20,
width: 20,
height: 20,
};
actual = cropper._calcRectDimensionFromPoint(x, y);
expect(actual).toEqual(expected);
x = 300;
y = 400;
expected = {
left: 10,
top: 20,
width: 90,
height: 180,
};
actual = cropper._calcRectDimensionFromPoint(x, y);
expect(actual).toEqual(expected);
});
it('should create cropzone that has fixed ratio during shift key is pressed.', () => {
const x = 100;
const y = 200;
const expected = {
left: 10,
top: 20,
width: 180,
height: 180,
};
cropper._withShiftKey = true;
const actual = cropper._calcRectDimensionFromPoint(x, y);
expect(actual).toEqual(expected);
});
it('should create cropzone that inverted current mouse position during shift key is pressed.', () => {
const x = -10;
const y = -20;
const expected = {
left: -10,
top: 0,
width: 20,
height: 20,
};
cropper._withShiftKey = true;
const actual = cropper._calcRectDimensionFromPoint(x, y);
expect(actual).toEqual(expected);
});
});
it('"onFabricMouseUp()" should activate cropzone', () => {
canvas.setActiveObject = jasmine.createSpy();
cropper.start();
cropper._onFabricMouseUp();
expect(canvas.setActiveObject).toHaveBeenCalledWith(cropper._cropzone);
});
describe('"crop()"', () => {
it('should return cropzone rect', () => {
cropper.start();
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
expect(cropper.getCropzoneRect()).toBeTruthy();
cropper.end();
});
it('should return cropzone data if the cropzone is valid', () => {
cropper.start();
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
expect(cropper.getCroppedImageData(cropper.getCropzoneRect())).toEqual({
imageName: jasmine.any(String),
url: jasmine.any(String),
});
cropper.end();
});
});
describe('"presets - setCropzoneRect()"', () => {
beforeEach(() => {
cropper.start();
});
afterEach(() => {
cropper.end();
});
it('should return cropzone rect as a square', () => {
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
cropper.setCropzoneRect(1 / 1);
expect(cropper.getCropzoneRect().width).toBe(cropper.getCropzoneRect().height);
});
it('should return cropzone rect as a 3:2 aspect box', () => {
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
cropper.setCropzoneRect(3 / 2);
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)).toBe(
(3 / 2).toFixed(1)
);
});
it('should return cropzone rect as a 4:3 aspect box', () => {
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
cropper.setCropzoneRect(4 / 3);
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)).toBe(
(4 / 3).toFixed(1)
);
});
it('should return cropzone rect as a 5:4 aspect box', () => {
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
cropper.setCropzoneRect(5 / 4);
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)).toBe(
(5 / 4).toFixed(1)
);
});
it('should return cropzone rect as a 7:5 aspect box', () => {
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
cropper.setCropzoneRect(7 / 5);
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)).toBe(
(7 / 5).toFixed(1)
);
});
it('should return cropzone rect as a 16:9 aspect box', () => {
spyOn(cropper._cropzone, 'isValid').and.returnValue(true);
cropper.setCropzoneRect(16 / 9);
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)).toBe(
(16 / 9).toFixed(1)
);
});
it('Even in situations with floating point problems, should calculate the exact width you expect.', () => {
spyOn(canvas, 'getWidth').and.returnValue(408);
spyOn(canvas, 'getHeight').and.returnValue(312);
spyOn(cropper._cropzone, 'set').and.callThrough();
cropper.setCropzoneRect(16 / 9);
expect(cropper._cropzone.set.calls.first().args[0].width).toBe(408);
});
it('should remove cropzone of cropper when falsy is passed', () => {
cropper.setCropzoneRect();
expect(cropper.getCropzoneRect()).toBeFalsy();
cropper.setCropzoneRect(0);
expect(cropper.getCropzoneRect()).toBeFalsy();
cropper.setCropzoneRect(null);
expect(cropper.getCropzoneRect()).toBeFalsy();
});
});
describe('"end()"', () => {
it('should set cropzone of cropper to null', () => {
cropper.start();
cropper.end();
expect(cropper._cropzone).toBe(null);
});
it('should set "evented" of all obejcts to true', () => {
const objects = [
new fabric.Rect({ evented: false }),
new fabric.Rect({ evented: false }),
new fabric.Rect({ evented: false }),
];
canvas.add(objects[0], objects[1], objects[2]);
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();
spyOn(graphics, 'fire');
const events = eventNames;
const fEvent = {
target: cropper._cropzone,
};
const cropzone = cropper._cropzone;
canvas.fire('object:scaling', fEvent);
expect(graphics.fire.calls.count()).toBe(0);
cropzone.canvasEventTrigger[events.OBJECT_SCALED](cropzone);
expect(graphics.fire.calls.count()).toBe(1);
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/extension/cropzone.js"
*/
import snippet from 'tui-code-snippet';
import fabric from 'fabric';
import Cropzone from '../src/js/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('"_getCoordinates()" 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('"_onMoving()" should set left and top between 0 and canvas size', () => {
const cropzone = new Cropzone(canvas, options, {});
const mockFabricCanvas = {
getWidth() {
return 300;
},
getHeight() {
return 400;
},
};
cropzone.canvas = mockFabricCanvas;
cropzone.left = -1;
cropzone.top = -1;
cropzone._onMoving();
expect(cropzone.top).toEqual(0);
expect(cropzone.left).toEqual(0);
cropzone.left = 1000;
cropzone.top = 1000;
cropzone._onMoving();
expect(cropzone.left).toEqual(200);
expect(cropzone.top).toEqual(300);
});
it('"isValid()" 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('"_resizeTL" should give the expected value at run', () => {
const cropzone = new Cropzone(canvas, options, {});
expect(
cropzone._resizeCropZone(
{
x: 30,
y: 40,
},
'tl'
)
).toEqual({
left: 30,
top: 40,
width: 80,
height: 70,
});
});
it('"_resizeTR" should give the expected value at run', () => {
const cropzone = new Cropzone(canvas, options, {});
expect(
cropzone._resizeCropZone(
{
x: 80,
y: 50,
},
'tr'
)
).toEqual({
left: 10,
top: 50,
width: 70,
height: 60,
});
});
it('"_resizeBL" should give the expected value at run', () => {
const cropzone = new Cropzone(canvas, options, {});
expect(
cropzone._resizeCropZone(
{
x: 30,
y: 40,
},
'bl'
)
).toEqual({
left: 30,
top: 10,
width: 80,
height: 30,
});
});
it('"_resizeBR" should give the expected value at run', () => {
const cropzone = new Cropzone(canvas, options, {});
expect(
cropzone._resizeCropZone(
{
x: 30,
y: 40,
},
'br'
)
).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,
snippet.extend({}, options, {
width: 50,
height: 40,
presetRatio,
}),
{}
);
snippet.forEach(['tl', 'tr', 'mt', 'ml', 'mr', 'mb', 'bl', 'br'], (cornerType) => {
const { width, height } = cropzone._resizeCropZone(
{
x: 20,
y: 20,
},
cornerType
);
expect(width / height).toEqual(presetRatio);
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/imageEditor.js"
*/
import ImageEditor from '../src/js/imageEditor';
describe('DrawingMode', () => {
let imageEditor;
const imageURL = 'base/test/fixtures/sampleImage.jpg';
beforeEach((done) => {
imageEditor = new ImageEditor(document.createElement('div'), {
cssMaxWidth: 700,
cssMaxHeight: 500,
});
imageEditor.loadImageFromURL(imageURL, 'sampleImage').then(() => {
done();
});
});
afterEach(() => {
imageEditor.destroy();
});
it('enter a drawing mode with startDrawingMode, CROPPER', () => {
imageEditor.startDrawingMode('CROPPER');
expect(imageEditor.getDrawingMode()).toBe('CROPPER');
});
it('stop a drawing mode with stopDrawingMode, ie, to normal', () => {
imageEditor.stopDrawingMode();
expect(imageEditor.getDrawingMode()).toBe('NORMAL');
});
it('enter all drawing mode with startDrawingMode in consecutive order', () => {
const drawingModes = ['CROPPER', 'FREE_DRAWING', 'LINE_DRAWING', 'TEXT', 'SHAPE'];
const { length } = drawingModes;
let i;
for (i = 0; i < length; i += 1) {
imageEditor.startDrawingMode(drawingModes[i]);
expect(imageEditor.getDrawingMode()).toBe(drawingModes[i]);
}
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);
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/filter.js"
*/
import ImageEditor from '../src/js/imageEditor';
describe('Filter', () => {
let imageEditor;
const imageURL = 'base/test/fixtures/sampleImage.jpg';
beforeEach((done) => {
imageEditor = new ImageEditor(document.createElement('div'), {
cssMaxWidth: 700,
cssMaxHeight: 500,
});
imageEditor.loadImageFromURL(imageURL, 'sampleImage').then(() => {
imageEditor.clearUndoStack();
done();
});
});
afterEach(() => {
imageEditor.destroy();
});
it('applyFilter() can add undo stack', (done) => {
imageEditor
.applyFilter('colorFilter')
.then(() => {
expect(imageEditor.isEmptyUndoStack()).toBe(false);
done();
})
['catch'](() => {
fail();
done();
});
});
it('applyFilter() can not add undo stack at isSilent', (done) => {
const isSilent = true;
imageEditor
.applyFilter('colorFilter', {}, isSilent)
.then(() => {
expect(imageEditor.isEmptyUndoStack()).toBe(true);
done();
})
['catch'](() => {
fail();
done();
});
});
it('hasFilter', () => {
imageEditor.applyFilter('colorFilter');
expect(imageEditor.hasFilter('invert')).toBe(false);
expect(imageEditor.hasFilter('colorFilter')).toBe(true);
});
it('removeFilter() can remove added filter', (done) => {
imageEditor
.applyFilter('colorFilter')
.then(() => imageEditor.removeFilter('colorFilter'))
.then(() => {
expect(imageEditor.hasFilter('colorFilter')).toBe(false);
expect(imageEditor.isEmptyUndoStack()).toBe(false);
done();
})
['catch'](() => {
fail();
done();
});
});
});
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs>
<circle id="a" cx="16" cy="16" r="16"/>
</defs><symbol id="icon-a-ic-apply" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#434343" d="M4 12.011l5 5L20.011 6"/>
</g>
</symbol><symbol id="icon-a-ic-cancel" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#434343" d="M6 6l12 12M18 6L6 18"/>
</g>
</symbol><symbol id="icon-a-ic-color-transparent-w" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g>
<use fill="#FFF" xlink:href="#a"/>
<circle cx="16" cy="16" r="15.5" stroke="#D5D5D5"/>
</g>
<path stroke="#FF4040" stroke-width="1.5" d="M27 5L5 27"/>
</g>
</symbol><symbol id="icon-a-ic-crop" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#434343" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</symbol><symbol id="icon-a-ic-delete-all" viewBox="0 0 24 24">
<g fill="#434343" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</symbol><symbol id="icon-a-ic-delete" viewBox="0 0 24 24">
<g fill="#434343" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</symbol><symbol id="icon-a-ic-draw-free" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</symbol><symbol id="icon-a-ic-draw-line" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M2 15.5h28"/>
</g>
</symbol><symbol id="icon-a-ic-draw" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#434343" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#434343" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</symbol><symbol id="icon-a-ic-filter" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#434343" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</symbol><symbol id="icon-a-ic-flip-reset" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#434343" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#434343" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</symbol><symbol id="icon-a-ic-flip-x" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#434343" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</symbol><symbol id="icon-a-ic-flip-y" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#434343" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</symbol><symbol id="icon-a-ic-flip" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-arrow-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-arrow-3" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-arrow" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-bubble" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-heart" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#434343" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#434343" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#434343" d="M25 3h1v9h-1z"/>
<path stroke="#434343" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-a-ic-icon-location" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#434343">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</symbol><symbol id="icon-a-ic-icon-polygon" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-star-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</symbol><symbol id="icon-a-ic-icon-star" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</symbol><symbol id="icon-a-ic-icon" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</symbol><symbol id="icon-a-ic-mask-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#434343" d="M25 3h1v9h-1z"/>
<path stroke="#434343" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-a-ic-mask" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#434343"/>
<path fill="#434343" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</symbol><symbol id="icon-a-ic-redo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#434343" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#434343" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</symbol><symbol id="icon-a-ic-reset" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#434343" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#434343" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</symbol><symbol id="icon-a-ic-rotate-clockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#434343" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#434343" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#434343" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</symbol><symbol id="icon-a-ic-rotate-counterclockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#434343" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#434343" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#434343" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</symbol><symbol id="icon-a-ic-rotate" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#434343" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#434343" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</symbol><symbol id="icon-a-ic-shape-circle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#434343"/>
</g>
</symbol><symbol id="icon-a-ic-shape-rectangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#434343" rx="1"/>
</g>
</symbol><symbol id="icon-a-ic-shape-triangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</symbol><symbol id="icon-a-ic-shape" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#434343" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#434343" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</symbol><symbol id="icon-a-ic-text-align-center" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</symbol><symbol id="icon-a-ic-text-align-left" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</symbol><symbol id="icon-a-ic-text-align-right" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</symbol><symbol id="icon-a-ic-text-bold" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#434343" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</symbol><symbol id="icon-a-ic-text-italic" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</symbol><symbol id="icon-a-ic-text-underline" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#434343" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#434343" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</symbol><symbol id="icon-a-ic-text" viewBox="0 0 24 24">
<g fill="#434343" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</symbol><symbol id="icon-a-ic-undo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#434343" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#434343" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</symbol><symbol id="icon-a-img-bi" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</symbol></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs/><symbol id="icon-b-ic-apply" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#555555" d="M4 12.011l5 5L20.011 6"/>
</g>
</symbol><symbol id="icon-b-ic-cancel" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#555555" d="M6 6l12 12M18 6L6 18"/>
</g>
</symbol><symbol id="icon-b-ic-crop" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#555555" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</symbol><symbol id="icon-b-ic-delete-all" viewBox="0 0 24 24">
<g fill="#555555" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</symbol><symbol id="icon-b-ic-delete" viewBox="0 0 24 24">
<g fill="#555555" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</symbol><symbol id="icon-b-ic-draw-free" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</symbol><symbol id="icon-b-ic-draw-line" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M2 15.5h28"/>
</g>
</symbol><symbol id="icon-b-ic-draw" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#555555" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#555555" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</symbol><symbol id="icon-b-ic-filter" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#555555" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</symbol><symbol id="icon-b-ic-flip-reset" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#555555" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#555555" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</symbol><symbol id="icon-b-ic-flip-x" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#555555" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</symbol><symbol id="icon-b-ic-flip-y" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#555555" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</symbol><symbol id="icon-b-ic-flip" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-arrow-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-arrow-3" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-arrow" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-bubble" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-heart" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#555555" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#555555" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#555555" d="M25 3h1v9h-1z"/>
<path stroke="#555555" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-b-ic-icon-location" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#555555">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</symbol><symbol id="icon-b-ic-icon-polygon" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-star-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</symbol><symbol id="icon-b-ic-icon-star" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</symbol><symbol id="icon-b-ic-icon" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</symbol><symbol id="icon-b-ic-mask-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#555555" d="M25 3h1v9h-1z"/>
<path stroke="#555555" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-b-ic-mask" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#555555"/>
<path fill="#555555" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</symbol><symbol id="icon-b-ic-redo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#555555" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#555555" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</symbol><symbol id="icon-b-ic-reset" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#555555" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#555555" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</symbol><symbol id="icon-b-ic-rotate-clockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#555555" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#555555" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#555555" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</symbol><symbol id="icon-b-ic-rotate-counterclockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#555555" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#555555" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#555555" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</symbol><symbol id="icon-b-ic-rotate" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#555555" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#555555" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</symbol><symbol id="icon-b-ic-shape-circle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#555555"/>
</g>
</symbol><symbol id="icon-b-ic-shape-rectangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#555555" rx="1"/>
</g>
</symbol><symbol id="icon-b-ic-shape-triangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</symbol><symbol id="icon-b-ic-shape" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#555555" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#555555" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</symbol><symbol id="icon-b-ic-text-align-center" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</symbol><symbol id="icon-b-ic-text-align-left" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</symbol><symbol id="icon-b-ic-text-align-right" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</symbol><symbol id="icon-b-ic-text-bold" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#555555" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</symbol><symbol id="icon-b-ic-text-italic" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</symbol><symbol id="icon-b-ic-text-underline" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#555555" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#555555" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</symbol><symbol id="icon-b-ic-text" viewBox="0 0 24 24">
<g fill="#555555" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</symbol><symbol id="icon-b-ic-undo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#555555" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#555555" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</symbol><symbol id="icon-b-img-bi" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</symbol></svg>
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs/><symbol id="icon-c-ic-apply" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#e9e9e9" d="M4 12.011l5 5L20.011 6"/>
</g>
</symbol><symbol id="icon-c-ic-cancel" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#e9e9e9" d="M6 6l12 12M18 6L6 18"/>
</g>
</symbol><symbol id="icon-c-ic-crop" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#e9e9e9" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</symbol><symbol id="icon-c-ic-delete-all" viewBox="0 0 24 24">
<g fill="#e9e9e9" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</symbol><symbol id="icon-c-ic-delete" viewBox="0 0 24 24">
<g fill="#e9e9e9" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</symbol><symbol id="icon-c-ic-draw-free" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</symbol><symbol id="icon-c-ic-draw-line" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M2 15.5h28"/>
</g>
</symbol><symbol id="icon-c-ic-draw" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#e9e9e9" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#e9e9e9" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</symbol><symbol id="icon-c-ic-filter" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#e9e9e9" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</symbol><symbol id="icon-c-ic-flip-reset" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#e9e9e9" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</symbol><symbol id="icon-c-ic-flip-x" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#e9e9e9" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</symbol><symbol id="icon-c-ic-flip-y" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#e9e9e9" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</symbol><symbol id="icon-c-ic-flip" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-arrow-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-arrow-3" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-arrow" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-bubble" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-heart" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#e9e9e9" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#e9e9e9" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#e9e9e9" d="M25 3h1v9h-1z"/>
<path stroke="#e9e9e9" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-c-ic-icon-location" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#e9e9e9">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</symbol><symbol id="icon-c-ic-icon-polygon" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-star-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</symbol><symbol id="icon-c-ic-icon-star" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</symbol><symbol id="icon-c-ic-icon" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</symbol><symbol id="icon-c-ic-mask-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#e9e9e9" d="M25 3h1v9h-1z"/>
<path stroke="#e9e9e9" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-c-ic-mask" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#e9e9e9"/>
<path fill="#e9e9e9" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</symbol><symbol id="icon-c-ic-redo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#e9e9e9" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</symbol><symbol id="icon-c-ic-reset" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#e9e9e9" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</symbol><symbol id="icon-c-ic-rotate-clockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#e9e9e9" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#e9e9e9" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</symbol><symbol id="icon-c-ic-rotate-counterclockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#e9e9e9" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#e9e9e9" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</symbol><symbol id="icon-c-ic-rotate" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#e9e9e9" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</symbol><symbol id="icon-c-ic-shape-circle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#e9e9e9"/>
</g>
</symbol><symbol id="icon-c-ic-shape-rectangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#e9e9e9" rx="1"/>
</g>
</symbol><symbol id="icon-c-ic-shape-triangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</symbol><symbol id="icon-c-ic-shape" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#e9e9e9" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#e9e9e9" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</symbol><symbol id="icon-c-ic-text-align-center" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</symbol><symbol id="icon-c-ic-text-align-left" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</symbol><symbol id="icon-c-ic-text-align-right" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</symbol><symbol id="icon-c-ic-text-bold" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#e9e9e9" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</symbol><symbol id="icon-c-ic-text-italic" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</symbol><symbol id="icon-c-ic-text-underline" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#e9e9e9" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#e9e9e9" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</symbol><symbol id="icon-c-ic-text" viewBox="0 0 24 24">
<g fill="#e9e9e9" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</symbol><symbol id="icon-c-ic-undo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#e9e9e9" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#e9e9e9" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</symbol><symbol id="icon-c-img-bi" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</symbol></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs/><symbol id="icon-d-ic-apply" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#8a8a8a" d="M4 12.011l5 5L20.011 6"/>
</g>
</symbol><symbol id="icon-d-ic-cancel" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path stroke="#8a8a8a" d="M6 6l12 12M18 6L6 18"/>
</g>
</symbol><symbol id="icon-d-ic-crop" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M4 0h1v20a1 1 0 0 1-1-1V0zM20 17h-1V5h1v12zm0 2v5h-1v-5h1z"/>
<path fill="#8a8a8a" d="M5 19h19v1H5zM4.762 4v1H0V4h4.762zM7 4h12a1 1 0 0 1 1 1H7V4z"/>
</g>
</symbol><symbol id="icon-d-ic-delete-all" viewBox="0 0 24 24">
<g fill="#8a8a8a" fill-rule="evenodd">
<path d="M5 23H3a1 1 0 0 1-1-1V6h1v16h2v1zm16-10h-1V6h1v7zM9 13H8v-3h1v3zm3 0h-1v-3h1v3zm3 0h-1v-3h1v3zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM11.286 21H8.714L8 23H7l1-2.8V20h.071L9.5 16h1l1.429 4H12v.2l1 2.8h-1l-.714-2zm-.357-1L10 17.4 9.071 20h1.858zM20 22h3v1h-4v-7h1v6zm-5 0h3v1h-4v-7h1v6z"/>
</g>
</symbol><symbol id="icon-d-ic-delete" viewBox="0 0 24 24">
<g fill="#8a8a8a" fill-rule="evenodd">
<path d="M3 6v16h17V6h1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h1zM14.794 3.794L13 2h-3L8.206 3.794A.963.963 0 0 1 8 2.5l.703-1.055A1 1 0 0 1 9.535 1h3.93a1 1 0 0 1 .832.445L15 2.5a.965.965 0 0 1-.206 1.294zM14.197 4H8.803h5.394z"/>
<path d="M0 3h23v1H0zM8 10h1v6H8v-6zm3 0h1v6h-1v-6zm3 0h1v6h-1v-6z"/>
</g>
</symbol><symbol id="icon-d-ic-draw-free" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M2.5 20.929C2.594 10.976 4.323 6 7.686 6c5.872 0 2.524 19 7.697 19s1.89-14.929 6.414-14.929 1.357 10.858 5.13 10.858c1.802 0 2.657-2.262 2.566-6.786"/>
</g>
</symbol><symbol id="icon-d-ic-draw-line" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M2 15.5h28"/>
</g>
</symbol><symbol id="icon-d-ic-draw" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#8a8a8a" d="M2.5 21.5H5c.245 0 .48-.058.691-.168l.124-.065.14.01c.429.028.85-.127 1.16-.437L22.55 5.405a.5.5 0 0 0 0-.707l-3.246-3.245a.5.5 0 0 0-.707 0L3.162 16.888a1.495 1.495 0 0 0-.437 1.155l.01.14-.065.123c-.111.212-.17.448-.17.694v2.5z"/>
<path fill="#8a8a8a" d="M16.414 3.707l3.89 3.89-.708.706-3.889-3.889z"/>
</g>
</symbol><symbol id="icon-d-ic-filter" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M12 7v1H2V7h10zm6 0h4v1h-4V7zM12 16v1h10v-1H12zm-6 0H2v1h4v-1z"/>
<path fill="#8a8a8a" d="M8.5 20a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM15.5 11a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</symbol><symbol id="icon-d-ic-flip-reset" viewBox="0 0 31 32">
<g fill="none" fill-rule="evenodd">
<path d="M31 0H0v32h31z"/>
<path fill="#8a8a8a" d="M28 16a8 8 0 0 1-8 8H3v-1h1v-7H3a8 8 0 0 1 8-8h17v1h-1v7h1zM11 9a7 7 0 0 0-7 7v7h16a7 7 0 0 0 7-7V9H11z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M24 5l3.5 3.5L24 12M7 20l-3.5 3.5L7 27"/>
</g>
</symbol><symbol id="icon-d-ic-flip-x" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M32 32H0V0h32z"/>
<path fill="#8a8a8a" d="M17 32h-1V0h1zM27.167 11l.5 3h-1.03l-.546-3h1.076zm-.5-3h-1.122L25 5h-5V4h5.153a1 1 0 0 1 .986.836L26.667 8zm1.5 9l.5 3h-.94l-.545-3h.985zm1 6l.639 3.836A1 1 0 0 1 28.819 28H26v-1h3l-.726-4h.894zM23 28h-3v-1h3v1zM13 4v1H7L3 27h10v1H3.18a1 1 0 0 1-.986-1.164l3.666-22A1 1 0 0 1 6.847 4H13z"/>
</g>
</symbol><symbol id="icon-d-ic-flip-y" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0v32h32V0z"/>
<path fill="#8a8a8a" d="M0 16v1h32v-1zM11 27.167l3 .5v-1.03l-3-.546v1.076zm-3-.5v-1.122L5 25v-5H4v5.153a1 1 0 0 0 .836.986L8 26.667zm9 1.5l3 .5v-.94l-3-.545v.985zm6 1l3.836.639A1 1 0 0 0 28 28.82V26h-1v3l-4-.727v.894zM28 23v-3h-1v3h1zM4 13h1V7l22-4v10h1V3.18a1 1 0 0 0-1.164-.986l-22 3.667A1 1 0 0 0 4 6.847V13z"/>
</g>
</symbol><symbol id="icon-d-ic-flip" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M11 0h1v24h-1zM19 21v-1h2v-2h1v2a1 1 0 0 1-1 1h-2zm-2 0h-3v-1h3v1zm5-5h-1v-3h1v3zm0-5h-1V8h1v3zm0-5h-1V4h-2V3h2a1 1 0 0 1 1 1v2zm-5-3v1h-3V3h3zM9 3v1H2v16h7v1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-arrow-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M21.793 18.5H2.5v-5h18.935l-7.6-8h5.872l10.5 10.5-10.5 10.5h-5.914l8-8z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-arrow-3" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M25.288 16.42L14.208 27.5H6.792l11.291-11.291L6.826 4.5h7.381l11.661 11.661-.58.258z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-arrow" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M2.5 11.5v9h18v5.293L30.293 16 20.5 6.207V11.5h-18z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-bubble" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M22.207 24.5L16.5 30.207V24.5H8A6.5 6.5 0 0 1 1.5 18V9A6.5 6.5 0 0 1 8 2.5h16A6.5 6.5 0 0 1 30.5 9v9a6.5 6.5 0 0 1-6.5 6.5h-1.793z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-heart" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill-rule="nonzero" stroke="#8a8a8a" d="M15.996 30.675l1.981-1.79c7.898-7.177 10.365-9.718 12.135-13.012.922-1.716 1.377-3.37 1.377-5.076 0-4.65-3.647-8.297-8.297-8.297-2.33 0-4.86 1.527-6.817 3.824l-.38.447-.381-.447C13.658 4.027 11.126 2.5 8.797 2.5 4.147 2.5.5 6.147.5 10.797c0 1.714.46 3.375 1.389 5.098 1.775 3.288 4.26 5.843 12.123 12.974l1.984 1.806z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M17.314 18.867l1.951-2.53 4 5.184h-17l6.5-8.84 4.549 6.186z"/>
<path fill="#8a8a8a" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01z"/>
<path fill="#8a8a8a" d="M25 3h1v9h-1z"/>
<path stroke="#8a8a8a" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-d-ic-icon-location" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<g stroke="#8a8a8a">
<path d="M16 31.28C23.675 23.302 27.5 17.181 27.5 13c0-6.351-5.149-11.5-11.5-11.5S4.5 6.649 4.5 13c0 4.181 3.825 10.302 11.5 18.28z"/>
<circle cx="16" cy="13" r="4.5"/>
</g>
</g>
</symbol><symbol id="icon-d-ic-icon-polygon" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M.576 16L8.29 29.5h15.42L31.424 16 23.71 2.5H8.29L.576 16z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-star-2" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M19.446 31.592l2.265-3.272 3.946.25.636-3.94 3.665-1.505-1.12-3.832 2.655-2.962-2.656-2.962 1.12-3.832-3.664-1.505-.636-3.941-3.946.25-2.265-3.271L16 3.024 12.554 1.07 10.289 4.34l-3.946-.25-.636 3.941-3.665 1.505 1.12 3.832L.508 16.33l2.656 2.962-1.12 3.832 3.664 1.504.636 3.942 3.946-.25 2.265 3.27L16 29.638l3.446 1.955z"/>
</g>
</symbol><symbol id="icon-d-ic-icon-star" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" d="M25.292 29.878l-1.775-10.346 7.517-7.327-10.388-1.51L16 1.282l-4.646 9.413-10.388 1.51 7.517 7.327-1.775 10.346L16 24.993l9.292 4.885z"/>
</g>
</symbol><symbol id="icon-d-ic-icon" viewBox="0 0 24 24">
<g fill="none">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M11.923 19.136L5.424 22l.715-7.065-4.731-5.296 6.94-1.503L11.923 2l3.574 6.136 6.94 1.503-4.731 5.296L18.42 22z"/>
</g>
</symbol><symbol id="icon-d-ic-mask-load" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M18.01 4a11.798 11.798 0 0 0 0 1H3v24h24V14.986a8.738 8.738 0 0 0 1 0V29a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h15.01zM15 23a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1a5 5 0 1 0 0-10 5 5 0 0 0 0 10z"/>
<path fill="#8a8a8a" d="M25 3h1v9h-1z"/>
<path stroke="#8a8a8a" d="M22 6l3.5-3.5L29 6"/>
</g>
</symbol><symbol id="icon-d-ic-mask" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="4.5" stroke="#8a8a8a"/>
<path fill="#8a8a8a" d="M2 1h20a1 1 0 0 1 1 1v20a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm0 1v20h20V2H2z"/>
</g>
</symbol><symbol id="icon-d-ic-redo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#8a8a8a" d="M21 6H9a6 6 0 1 0 0 12h12v1H9A7 7 0 0 1 9 5h12v1z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M19 3l2.5 2.5L19 8"/>
</g>
</symbol><symbol id="icon-d-ic-reset" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z" opacity=".5"/>
<path fill="#8a8a8a" d="M2 13v-1a7 7 0 0 1 7-7h13v1h-1v5h1v1a7 7 0 0 1-7 7H2v-1h1v-5H2zm7-7a6 6 0 0 0-6 6v6h12a6 6 0 0 0 6-6V6H9z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M19 3l2.5 2.5L19 8M5 16l-2.5 2.5L5 21"/>
</g>
</symbol><symbol id="icon-d-ic-rotate-clockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#8a8a8a" d="M29 17h-.924c0 6.627-5.373 12-12 12-6.628 0-12-5.373-12-12C4.076 10.398 9.407 5.041 16 5V4C8.82 4 3 9.82 3 17s5.82 13 13 13 13-5.82 13-13z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M16 1.5l4 3-4 3"/>
<path fill="#8a8a8a" fill-rule="nonzero" d="M16 4h4v1h-4z"/>
</g>
</symbol><symbol id="icon-d-ic-rotate-counterclockwise" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path fill="#8a8a8a" d="M3 17h.924c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.602-5.331-11.96-11.924-12V4c7.18 0 13 5.82 13 13s-5.82 13-13 13S3 24.18 3 17z"/>
<path fill="#8a8a8a" fill-rule="nonzero" d="M12 4h4v1h-4z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M16 1.5l-4 3 4 3"/>
</g>
</symbol><symbol id="icon-d-ic-rotate" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h24v24H0z"/>
<path fill="#8a8a8a" d="M8.349 22.254a10.002 10.002 0 0 1-2.778-1.719l.65-.76a9.002 9.002 0 0 0 2.495 1.548l-.367.931zm2.873.704l.078-.997a9 9 0 1 0-.557-17.852l-.14-.99A10.076 10.076 0 0 1 12.145 3c5.523 0 10 4.477 10 10s-4.477 10-10 10c-.312 0-.62-.014-.924-.042zm-7.556-4.655a9.942 9.942 0 0 1-1.253-2.996l.973-.234a8.948 8.948 0 0 0 1.124 2.693l-.844.537zm-1.502-5.91A9.949 9.949 0 0 1 2.88 9.23l.925.382a8.954 8.954 0 0 0-.644 2.844l-.998-.062zm2.21-5.686c.687-.848 1.51-1.58 2.436-2.166l.523.852a9.048 9.048 0 0 0-2.188 1.95l-.771-.636z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M13 1l-2.5 2.5L13 6"/>
</g>
</symbol><symbol id="icon-d-ic-shape-circle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<circle cx="16" cy="16" r="14.5" stroke="#8a8a8a"/>
</g>
</symbol><symbol id="icon-d-ic-shape-rectangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<rect width="27" height="27" x="2.5" y="2.5" stroke="#8a8a8a" rx="1"/>
</g>
</symbol><symbol id="icon-d-ic-shape-triangle" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M16 2.5l15.5 27H.5z"/>
</g>
</symbol><symbol id="icon-d-ic-shape" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#8a8a8a" d="M14.706 8H21a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-4h1v4h12V9h-5.706l-.588-1z"/>
<path stroke="#8a8a8a" stroke-linecap="round" stroke-linejoin="round" d="M8.5 1.5l7.5 13H1z"/>
</g>
</symbol><symbol id="icon-d-ic-text-align-center" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M2 5h28v1H2zM8 12h16v1H8zM2 19h28v1H2zM8 26h16v1H8z"/>
</g>
</symbol><symbol id="icon-d-ic-text-align-left" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M2 5h28v1H2zM2 12h16v1H2zM2 19h28v1H2zM2 26h16v1H2z"/>
</g>
</symbol><symbol id="icon-d-ic-text-align-right" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M2 5h28v1H2zM14 12h16v1H14zM2 19h28v1H2zM14 26h16v1H14z"/>
</g>
</symbol><symbol id="icon-d-ic-text-bold" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M7 2h2v2H7zM7 28h2v2H7z"/>
<path stroke="#8a8a8a" stroke-width="2" d="M9 3v12h9a6 6 0 1 0 0-12H9zM9 15v14h10a7 7 0 0 0 0-14H9z"/>
</g>
</symbol><symbol id="icon-d-ic-text-italic" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M15 2h5v1h-5zM11 29h5v1h-5zM17 3h1l-4 26h-1z"/>
</g>
</symbol><symbol id="icon-d-ic-text-underline" viewBox="0 0 32 32">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h32v32H0z"/>
<path fill="#8a8a8a" d="M8 2v14a8 8 0 1 0 16 0V2h1v14a9 9 0 0 1-18 0V2h1zM3 29h26v1H3z"/>
<path fill="#8a8a8a" d="M5 2h5v1H5zM22 2h5v1h-5z"/>
</g>
</symbol><symbol id="icon-d-ic-text" viewBox="0 0 24 24">
<g fill="#8a8a8a" fill-rule="evenodd">
<path d="M4 3h15a1 1 0 0 1 1 1H3a1 1 0 0 1 1-1zM3 4h1v1H3zM19 4h1v1h-1z"/>
<path d="M11 3h1v18h-1z"/>
<path d="M10 20h3v1h-3z"/>
</g>
</symbol><symbol id="icon-d-ic-undo" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="M24 0H0v24h24z" opacity=".5"/>
<path fill="#8a8a8a" d="M3 6h12a6 6 0 1 1 0 12H3v1h12a7 7 0 0 0 0-14H3v1z"/>
<path stroke="#8a8a8a" stroke-linecap="square" d="M5 3L2.5 5.5 5 8"/>
</g>
</symbol><symbol id="icon-d-img-bi" viewBox="0 0 257 26">
<g fill="#FDBA3B">
<path d="M26 5a8.001 8.001 0 0 0 0 16 8.001 8.001 0 0 0 0-16M51.893 19.812L43.676 5.396A.78.78 0 0 0 43 5a.78.78 0 0 0-.677.396l-8.218 14.418a.787.787 0 0 0 0 .792c.14.244.396.394.676.394h16.436c.28 0 .539-.15.678-.396a.796.796 0 0 0-.002-.792M15.767 5.231A.79.79 0 0 0 15.21 5H.791A.791.791 0 0 0 0 5.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M85.767 5.231A.79.79 0 0 0 85.21 5H70.791a.791.791 0 0 0-.791.79v6.42a.793.793 0 0 0 .791.79h3.21v7.21c.001.21.082.408.234.56.147.148.347.23.558.23h6.416a.788.788 0 0 0 .792-.79V13h3.006c.413 0 .611-.082.762-.232.15-.149.23-.35.231-.559V5.791a.787.787 0 0 0-.233-.56M65.942 9.948l2.17-3.76a.78.78 0 0 0 0-.792.791.791 0 0 0-.684-.396h-8.54A5.889 5.889 0 0 0 53 10.86a5.887 5.887 0 0 0 3.07 5.17l-2.184 3.782A.792.792 0 0 0 54.571 21h8.54a5.89 5.89 0 0 0 2.831-11.052M105.7 21h2.3V5h-2.3zM91 5h2.4v10.286c0 1.893 1.612 3.429 3.6 3.429s3.6-1.536 3.6-3.429V5h2.4v10.286c0 3.156-2.686 5.714-6 5.714-3.313 0-6-2.558-6-5.714V5zM252.148 21.128h-2.377V9.659h2.27v1.64c.69-1.299 1.792-1.938 3.304-1.938.497 0 .95.065 1.382.192l-.215 2.277a3.734 3.734 0 0 0-1.275-.213c-1.814 0-3.089 1.234-3.089 3.638v5.873zm-7.095-5.744a3.734 3.734 0 0 0-1.101-2.703c-.714-.766-1.6-1.149-2.658-1.149-1.058 0-1.944.383-2.679 1.149a3.803 3.803 0 0 0-1.08 2.703c0 1.063.368 1.978 1.08 2.722.735.746 1.62 1.128 2.68 1.128 1.058 0 1.943-.382 2.657-1.128.734-.744 1.101-1.659 1.101-2.722zm-9.916 0c0-1.682.583-3.086 1.729-4.256 1.166-1.17 2.635-1.767 4.428-1.767 1.793 0 3.262.597 4.407 1.767 1.167 1.17 1.75 2.574 1.75 4.256 0 1.7-.583 3.127-1.75 4.297-1.145 1.17-2.614 1.745-4.407 1.745-1.793 0-3.262-.575-4.428-1.745-1.146-1.17-1.729-2.596-1.729-4.297zm-1.5 3.233l.821 1.83c-.864.638-1.944.958-3.22.958-2.526 0-3.822-1.554-3.822-4.383V11.66h-2.01v-2h2.031V5.595h2.355v4.063h4.018v2h-4.018v5.405c0 1.469.605 2.191 1.793 2.191.626 0 1.318-.212 2.052-.638zm-12.43 2.51h2.375V9.66h-2.376v11.469zm1.23-12.977c-.929 0-1.642-.682-1.642-1.596 0-.873.713-1.554 1.643-1.554.885 0 1.576.681 1.576 1.554 0 .914-.69 1.596-1.576 1.596zm-6.49 7.234c0-1.086-.346-1.98-1.037-2.724-.692-.745-1.599-1.128-2.7-1.128-1.102 0-2.01.383-2.7 1.128-.692.744-1.037 1.638-1.037 2.724 0 1.084.345 2.02 1.036 2.766.691.744 1.6 1.105 2.7 1.105 1.102 0 2.01-.361 2.7-1.105.692-.746 1.038-1.682 1.038-2.766zm-.173-4.129V5h2.397v16.128h-2.354v-1.596c-1.015 1.255-2.333 1.873-3.91 1.873-1.663 0-3.068-.575-4.169-1.724-1.102-1.17-1.663-2.596-1.663-4.297 0-1.682.561-3.107 1.663-4.256 1.101-1.17 2.485-1.745 4.148-1.745 1.534 0 2.83.617 3.888 1.872zm-11.48 9.873h-10.218V5.405h10.195v2.318h-7.711V12h7.15v2.32h-7.15v4.489h7.733v2.319zm-23.891-9.724c-1.793 0-3.132 1.192-3.478 2.979h6.783c-.194-1.808-1.555-2.979-3.305-2.979zm5.703 3.766c0 .32-.021.703-.086 1.128h-9.095c.346 1.787 1.62 3 3.867 3 1.318 0 2.916-.49 3.953-1.234l.994 1.724c-1.189.872-3.067 1.595-5.033 1.595-4.364 0-6.243-3-6.243-6.021 0-1.724.54-3.15 1.642-4.277 1.101-1.127 2.548-1.702 4.298-1.702 1.664 0 3.046.511 4.105 1.553 1.058 1.043 1.598 2.447 1.598 4.234zm-19.949 3.894c1.08 0 1.966-.362 2.68-1.085.712-.724 1.058-1.617 1.058-2.703 0-1.084-.346-2-1.059-2.701-.713-.702-1.599-1.064-2.679-1.064-1.058 0-1.944.362-2.656 1.085-.714.702-1.059 1.596-1.059 2.68 0 1.086.345 2 1.059 2.724.712.702 1.598 1.064 2.656 1.064zm3.673-7.936V9.66h2.29v10.299c0 1.85-.584 3.32-1.728 4.404-1.146 1.085-2.68 1.638-4.58 1.638-1.945 0-3.672-.553-5.206-1.638l1.037-1.808c1.296.915 2.679 1.36 4.126 1.36 2.484 0 3.996-1.51 3.996-3.637v-.83c-1.015 1.127-2.311 1.702-3.91 1.702-1.684 0-3.089-.554-4.19-1.68-1.102-1.128-1.642-2.532-1.642-4.214 0-1.68.561-3.085 1.706-4.191 1.145-1.128 2.571-1.681 4.234-1.681 1.534 0 2.83.575 3.867 1.745zm-18.07 8.127c1.102 0 1.988-.382 2.7-1.128.714-.744 1.06-1.659 1.06-2.743 0-1.065-.346-1.98-1.06-2.724-.712-.745-1.598-1.128-2.7-1.128-1.101 0-2.008.383-2.7 1.128-.691.744-1.036 1.66-1.036 2.745 0 1.084.345 2 1.037 2.745.691.744 1.598 1.105 2.7 1.105zm3.652-8V9.66h2.29v11.469h-2.29v-1.575c-1.059 1.234-2.399 1.852-3.976 1.852-1.663 0-3.067-.575-4.168-1.745-1.102-1.17-1.642-2.617-1.642-4.34 0-1.724.54-3.128 1.642-4.256 1.1-1.128 2.505-1.681 4.168-1.681 1.577 0 2.917.617 3.976 1.872zM138.79 9.34c1.404 0 2.527.448 3.37 1.34.863.873 1.295 2.086 1.295 3.596v6.852h-2.376V14.66c0-2.021-1.036-3.128-2.657-3.128-1.727 0-2.915 1.255-2.915 3.192v6.404h-2.377v-6.426c0-1.978-1.037-3.17-2.679-3.17-1.728 0-2.937 1.277-2.937 3.234v6.362h-2.377V9.659h2.333v1.66c.692-1.212 1.988-1.979 3.522-1.979 1.533.021 2.958.767 3.586 2.107.798-1.277 2.419-2.107 4.212-2.107zm-19.517 11.788h2.484V5.405h-2.484v15.723z"/>
</g>
</symbol></svg>
\ No newline at end of file
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/flip.js"
*/
import fabric from 'fabric';
import $ from 'jquery';
import Graphics from '../src/js/graphics';
import Flip from '../src/js/component/flip';
describe('Flip', () => {
let graphics, flipModule, mockImage;
beforeAll(() => {
graphics = new Graphics($('<canvas>')[0]);
flipModule = new Flip(graphics);
});
beforeEach(() => {
mockImage = new fabric.Image();
graphics.setCanvasImage('mockImage', mockImage);
});
it('"getCurrentSetting()" should return current flip-setting', () => {
let setting = flipModule.getCurrentSetting();
expect(setting).toEqual({
flipX: false,
flipY: false,
});
mockImage.set({ flipX: true });
setting = flipModule.getCurrentSetting();
expect(setting).toEqual({
flipX: true,
flipY: false,
});
});
it('"set()" should set flip-setting', () => {
flipModule.set({
flipX: false,
flipY: true,
});
expect(flipModule.getCurrentSetting()).toEqual({
flipX: false,
flipY: true,
});
});
it('"reset()" should reset flip-setting to false', () => {
mockImage.set({
flipX: true,
flipY: true,
});
flipModule.reset();
expect(flipModule.getCurrentSetting()).toEqual({
flipX: false,
flipY: false,
});
});
it('"flipX()" should toggle flipX', () => {
flipModule.flipX();
expect(flipModule.getCurrentSetting()).toEqual({
flipX: true,
flipY: false,
});
flipModule.flipX();
expect(flipModule.getCurrentSetting()).toEqual({
flipX: false,
flipY: false,
});
});
it('"flipY()" should toggle flipY', () => {
flipModule.flipY();
expect(flipModule.getCurrentSetting()).toEqual({
flipX: false,
flipY: true,
});
flipModule.flipY();
expect(flipModule.getCurrentSetting()).toEqual({
flipX: false,
flipY: false,
});
});
describe('Promise is returned with settings and angle,', () => {
beforeEach(() => {
mockImage.angle = 10;
});
it('flipX() is called.', (done) => {
flipModule.flipX().then((obj) => {
expect(obj).toEqual({
flipX: true,
flipY: false,
angle: -10,
});
done();
});
});
it('flipY() is called.', (done) => {
flipModule.flipY().then((obj) => {
expect(obj).toEqual({
flipX: false,
flipY: true,
angle: -10,
});
done();
});
});
it('flipY() is called.', (done) => {
flipModule.flipY().then((obj) => {
expect(obj).toEqual({
flipX: false,
flipY: true,
angle: -10,
});
done();
});
});
it('set() is called.', (done) => {
flipModule
.set({
flipX: true,
flipY: false,
})
.then((obj) => {
expect(obj).toEqual({
flipX: true,
flipY: false,
angle: -10,
});
done();
});
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Tests command with command-factory
*/
import snippet from 'tui-code-snippet';
import fabric from 'fabric';
import Graphics from '../src/js/graphics';
import { drawingModes, componentNames as components } from '../src/js/consts';
describe('Graphics', () => {
const cssMaxWidth = 900;
const cssMaxHeight = 700;
let graphics, canvas;
beforeEach(() => {
graphics = new Graphics(document.createElement('canvas'), {
cssMaxWidth,
cssMaxHeight,
});
canvas = graphics.getCanvas();
});
it('has several properties', () => {
expect(canvas).not.toBe(null);
expect(canvas).toEqual(jasmine.any(fabric.Canvas));
expect(graphics.cssMaxWidth).toBe(900);
expect(graphics.cssMaxHeight).toBe(700);
expect(graphics.canvasImage).toBe(null);
expect(graphics.imageName).toBe('');
expect(graphics._drawingMode).toBe(drawingModes.NORMAL);
expect(graphics._componentMap).not.toBe(null);
});
it('After the path has been drawn, "origin" should change to "left top-> center center" and "position" should change to the center coordinates of path.', () => {
const pathObj = new fabric.Path('M 0 0 L 100 0 L 100 100 L 0 100 z');
const expectPosition = pathObj.getCenterPoint();
const expectX = expectPosition.x;
const expectY = expectPosition.y;
graphics._onPathCreated({ path: pathObj });
expect(pathObj.originX).toBe('center');
expect(pathObj.originY).toBe('center');
expect(pathObj.left).toBe(expectX);
expect(pathObj.top).toBe(expectY);
});
it('can attach canvas events', () => {
const onMousedown = jasmine.createSpy('onMousedown');
const onObjectAdded = jasmine.createSpy('onObjectAdded');
const onObjectSelected = jasmine.createSpy('onObjectSelected');
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.calls.count()).toBe(2);
expect(onObjectAdded.calls.count()).toBe(2);
expect(onObjectSelected.calls.count()).toBe(1);
});
it('deactivates all objects', () => {
const triangle = new fabric.Triangle({
width: 20,
height: 30,
});
canvas.add(triangle).setActiveObject(triangle);
expect(canvas.getActiveObject()).not.toBe(null);
graphics.deactivateAll();
expect(canvas.getActiveObject()).toBe(null);
});
it('renders objects', (done) => {
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);
done();
});
graphics.renderAll();
});
it('removes a object or group by id', () => {
const triangle = new fabric.Triangle({
width: 20,
height: 30,
});
graphics.add(triangle);
const objectId = snippet.stamp(triangle);
graphics.removeObjectById(objectId);
expect(graphics.getObjects().length).toBe(0);
});
it('switches drawing modes', () => {
let modeName;
for (modeName in drawingModes) {
if (drawingModes.hasOwnProperty(modeName)) {
graphics.startDrawingMode(modeName);
expect(graphics.getDrawingMode()).toBe(modeName);
graphics.stopDrawingMode();
expect(graphics.getDrawingMode()).toBe(drawingModes.NORMAL);
}
}
});
it('can get the cropped image data', () => {
graphics.startDrawingMode(drawingModes.CROPPER);
spyOn(graphics.getComponent(components.CROPPER)._cropzone, 'isValid').and.returnValue(true);
expect(graphics.getCropzoneRect()).toBeTruthy();
expect(graphics.getCroppedImageData(graphics.getCropzoneRect())).toEqual({
imageName: jasmine.any(String),
url: jasmine.any(String),
});
graphics.stopDrawingMode();
});
it('Cropzone must be hidden initially and then redisplayed after completion at toDataURL is executed with a cropzone present', () => {
const cropper = graphics.getComponent(components.CROPPER);
spyOn(cropper, 'changeVisibility');
graphics.startDrawingMode(drawingModes.CROPPER);
graphics.toDataURL();
expect(cropper.changeVisibility.calls.allArgs()).toEqual([[false], [true]]);
});
it('can 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.width).toBe(12);
expect(brush.color).toBe('rgba(255,255,0,1)');
graphics.stopDrawingMode();
});
it('can 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('can register custom icon', () => {
const iconComp = graphics.getComponent(components.ICON);
graphics.registerPaths({
customIcon: 'M 0 0 L 20 20 L 10 10 Z',
});
expect(iconComp._pathMap).toEqual(
jasmine.objectContaining({
customIcon: 'M 0 0 L 20 20 L 10 10 Z',
})
);
});
it('has 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('Group objects must be duplicated as many as the number of objects in the group.', (done) => {
const groupObject = graphics.getActiveSelectionFromObjects(canvas.getObjects());
graphics.setActiveObject(groupObject);
graphics.resetTargetObjectForCopyPaste();
graphics.pasteObject().then(() => {
expect(canvas.getObjects().length).toBe(4);
done();
});
});
it('Only one object should be duplicated.', (done) => {
graphics.setActiveObject(targetObject1);
graphics.resetTargetObjectForCopyPaste();
graphics.pasteObject().then(() => {
expect(canvas.getObjects().length).toBe(3);
done();
});
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/icon.js"
*/
import fabric from 'fabric';
import $ from 'jquery';
import Graphics from '../src/js/graphics';
import Icon from '../src/js/component/icon';
describe('Icon', () => {
let canvas, graphics, mockImage, icon;
beforeAll(() => {
graphics = new Graphics($('<canvas>')[0]);
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((done) => {
fEvent = { e: {} };
icon._startPoint = {
x: 300,
y: 300,
};
icon
.add('arrow', {
left: icon._startPoint.x,
top: icon._startPoint.y,
color: '#000',
})
.then(() => {
[iconObj] = canvas.getObjects();
iconObj.set({
width: 10,
height: 10,
});
done();
});
});
it('When dragging to the right-down from the starting point, the icon scale value should increase.', () => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 500,
y: 500,
});
icon._onFabricMouseMove(fEvent);
expect(iconObj.scaleX).toBe(40);
expect(iconObj.scaleY).toBe(40);
});
it('When dragging to the left-up from the starting point, the icon scale value should increase.', () => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 100,
y: 100,
});
icon._onFabricMouseMove(fEvent);
expect(iconObj.scaleX).toBe(40);
expect(iconObj.scaleY).toBe(40);
});
});
it('add() should insert the activated icon object on canvas.', () => {
icon.add('arrow');
const activeObj = canvas.getActiveObject();
expect(activeObj).not.toEqual(null);
});
it('add() should insert the icon object on center of canvas image.', () => {
const centerPos = icon.getCanvasImage().getCenterPoint();
icon.add('arrow');
const activeObj = canvas.getActiveObject();
const halfStrokeWidth = activeObj.strokeWidth / 2;
expect(activeObj.left + halfStrokeWidth).toEqual(centerPos.x);
expect(activeObj.top + halfStrokeWidth).toEqual(centerPos.y);
});
it('add() should create the arrow icon when parameter value is "arrow".', () => {
const path = icon._pathMap.arrow;
spyOn(icon, '_createIcon').and.returnValue(new fabric.Object({}));
icon.add('arrow');
expect(icon._createIcon).toHaveBeenCalledWith(path);
});
it('add() should create the cancel icon when parameter value is "cancel".', () => {
const path = icon._pathMap.cancel;
spyOn(icon, '_createIcon').and.returnValue(new fabric.Object({}));
icon.add('cancel');
expect(icon._createIcon).toHaveBeenCalledWith(path);
});
it('setColor() should change color of next inserted icon.', () => {
let activeObj;
const color = '#ffffff';
icon.add('arrow');
activeObj = canvas.getActiveObject();
expect(activeObj.fill).not.toEqual(color);
icon.setColor(color);
icon.add('cancel');
activeObj = canvas.getActiveObject();
expect(activeObj.fill).toEqual(color);
});
});
/**
* @fileoverview Test env
* @author NHN Ent. FE Development Lab <dl_javascript@nhn.com>
*/
import snippet from 'tui-code-snippet';
import ImageEditor from '../src/js/imageEditor';
import fabric from 'fabric';
import { eventNames, keyCodes } from '../src/js/consts';
const { OBJECT_ROTATED } = eventNames;
describe('ImageEditor', () => {
// hostnameSent module scope variable can not be reset.
// maintain cases with xit as it always fail, if you want to test these cases, change xit to fit one by one
describe('constructor', () => {
let imageEditor, el;
beforeEach(() => {
el = document.createElement('div');
spyOn(snippet, 'sendHostname');
imageEditor = new ImageEditor(el, {
usageStatistics: false,
});
});
afterEach(() => {
imageEditor.destroy();
});
xit('should send hostname by default', () => {
imageEditor = new ImageEditor(el);
expect(snippet.sendHostname).toHaveBeenCalled();
});
xit('should not send hostname on usageStatistics option false', () => {
imageEditor = new ImageEditor(el, {
usageStatistics: false,
});
expect(snippet.sendHostname).not.toHaveBeenCalled();
});
it('`preventDefault` of BACKSPACE key events should not be executed when object is selected state.', () => {
const spyCallback = jasmine.createSpy();
spyOn(imageEditor._graphics, 'getActiveObject').and.returnValue(null);
imageEditor._onKeyDown({
keyCode: keyCodes.BACKSPACE,
preventDefault: spyCallback,
});
expect(spyCallback).not.toHaveBeenCalled();
});
it('"objectRotated" event should be fire at object is rotate.', () => {
const canvas = imageEditor._graphics.getCanvas();
const obj = new fabric.Object({});
const mock = { target: obj };
canvas.add(obj);
spyOn(imageEditor, 'fire').and.callThrough();
canvas.fire('object:rotating', mock);
expect(imageEditor.fire.calls.mostRecent().args[0]).toBe(OBJECT_ROTATED);
});
});
});
/**
* @fileoverview Test env
* @author NHN Ent. FE Development Lab <dl_javascript@nhn.com>
*/
import '../src';
import fabric from 'fabric';
fabric.Object.prototype.objectCaching = false;
const testsContext = require.context('.', true, /spec\.js$/);
testsContext.keys().forEach(testsContext);
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/invoker.js"
*/
import { Promise } from '../src/js/util';
import Invoker from '../src/js/invoker';
import Command from '../src/js/interface/command';
describe('Invoker', () => {
let invoker, cmd;
beforeEach(() => {
invoker = new Invoker();
cmd = new Command({
execute: jasmine.createSpy().and.returnValue(Promise.resolve()),
undo: jasmine.createSpy().and.returnValue(Promise.resolve()),
});
});
it('"redo()" should call "command.execute" again', (done) => {
invoker
.execute(cmd)
.then(() => invoker.undo())
.then(() => {
cmd.execute.calls.reset();
return invoker.redo();
})
.then(() => {
expect(cmd.execute).toHaveBeenCalled();
done();
});
});
it('should call the "command.executeCallback" after invoke', (done) => {
const spyCallback = jasmine.createSpy();
cmd.setExecuteCallback(spyCallback);
invoker.execute(cmd).then(() => {
expect(spyCallback).toHaveBeenCalled();
done();
});
});
it('should call the "command.undoCallback" after undo', (done) => {
const spyCallback = jasmine.createSpy();
cmd.setUndoCallback(spyCallback);
invoker
.execute(cmd)
.then(() => invoker.undo())
.then(() => {
expect(spyCallback).toHaveBeenCalled();
done();
});
});
describe('invoker.customEvents', () => {
let spyEvents;
beforeEach(() => {
spyEvents = {
undoStackChanged: jasmine.createSpy(),
redoStackChanged: jasmine.createSpy(),
};
});
it(
'"invoke()" should fire a event - ' + ' "pushUndoStack" (when redoStack is empty before)"',
(done) => {
invoker.on(spyEvents);
invoker.execute(cmd).then(() => {
expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1);
expect(spyEvents.redoStackChanged).not.toHaveBeenCalled();
done();
});
}
);
it(
'"invoke()" should fire events - ' +
' "pushUndoStack", "clearRedoStack" (when redoStack is not empty before)',
(done) => {
invoker.pushRedoStack({});
invoker.on(spyEvents);
invoker.execute(cmd).then(() => {
expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1);
expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(0);
done();
});
}
);
it(
'"undo()" should fire a event - ' + ' "pushRedoStack" (when undoStack is not empty after)',
(done) => {
invoker
.execute(cmd)
.then(() => invoker.execute(cmd))
.then(() => {
invoker.on(spyEvents);
return invoker.undo();
})
.then(() => {
expect(spyEvents.undoStackChanged).not.toHaveBeenCalled();
expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(1);
done();
});
}
);
it(
'"undo()" should fire events - ' +
' "pushRedoStack", "emptyUndoStack" (when undoStack is empty after)',
(done) => {
invoker
.execute(cmd)
.then(() => {
invoker.on(spyEvents);
return invoker.undo();
})
.then(() => {
expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(1);
expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(0);
done();
});
}
);
it(
'"redo()" should fire a event - ' + ' "pushUndoStack" (when redoStack is not empty after)',
(done) => {
invoker
.execute(cmd)
.then(() => invoker.execute(cmd))
.then(() => invoker.undo())
.then(() => invoker.undo())
.then(() => {
invoker.on(spyEvents);
return invoker.redo();
})
.then(() => {
expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1);
expect(spyEvents.redoStackChanged).not.toHaveBeenCalled();
done();
});
}
);
it(
'"redo()" should fire events - ' +
' "pushUndoStack", "emptyRedoStack" (when undoStack is empty after)',
(done) => {
invoker
.execute(cmd)
.then(() => invoker.undo())
.then(() => {
invoker.on(spyEvents);
return invoker.redo(cmd);
})
.then(() => {
expect(spyEvents.undoStackChanged).toHaveBeenCalledWith(1);
expect(spyEvents.redoStackChanged).toHaveBeenCalledWith(0);
done();
});
}
);
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/line.js"
*/
import fabric from 'fabric';
import $ from 'jquery';
import Graphics from '../src/js/graphics';
import Line from '../src/js/component/line';
import { eventNames } from '../src/js/consts';
describe('Line', () => {
let canvas, graphics, mockImage, line, fEvent;
beforeAll(() => {
graphics = new Graphics($('<canvas>')[0]);
canvas = graphics.getCanvas();
line = new Line(graphics);
});
beforeEach(() => {
mockImage = new fabric.Image();
graphics.setCanvasImage('mockImage', mockImage);
fEvent = {
e: {},
};
});
afterEach(() => {
canvas.forEachObject((obj) => {
canvas.remove(obj);
});
});
it('_onFabricMouseDown() should insert the line.', () => {
line._onFabricMouseDown(fEvent);
expect(canvas.getObjects().length).toEqual(1);
});
it('_onFabricMouseMove() should draw line located by mouse pointer.', () => {
line._line = new fabric.Line([10, 20, 10, 20]);
canvas.add(line._line);
spyOn(canvas, 'getPointer').and.returnValue({
x: 30,
y: 60,
});
expect(canvas.getObjects()[0].get('x2')).toEqual(10);
expect(canvas.getObjects()[0].get('y2')).toEqual(20);
line._onFabricMouseMove(fEvent);
expect(canvas.getObjects()[0].get('x2')).toEqual(30);
expect(canvas.getObjects()[0].get('y2')).toEqual(60);
});
it('end() should restore all drawing objects activated.', () => {
const path = new fabric.Path();
canvas.add(path);
line.start();
expect(canvas.getObjects()[0].get('evented')).toEqual(false);
line.end();
expect(canvas.getObjects()[0].get('evented')).toEqual(true);
});
it('"objectAdded" event should fire after the line is drawn.', () => {
spyOn(line, 'fire').and.callThrough();
line._onFabricMouseUp(fEvent);
expect(line.fire.calls.mostRecent().args[0]).toBe(eventNames.OBJECT_ADDED);
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/filter.js"
*/
import ImageEditor from '../src/js/imageEditor';
describe('Promise API', () => {
let imageEditor, canvas, activeObjectId;
const imageURL = 'base/test/fixtures/sampleImage.jpg';
beforeAll(() => {
imageEditor = new ImageEditor(document.createElement('div'), {
cssMaxWidth: 700,
cssMaxHeight: 500,
});
canvas = imageEditor._graphics.getCanvas();
imageEditor.on('objectActivated', (objectProps) => {
activeObjectId = objectProps.id;
});
});
afterAll(() => {
imageEditor.destroy();
});
beforeEach((done) => {
imageEditor
.loadImageFromURL(imageURL, 'sampleImage')
.then(() => done())
['catch'](() => done());
});
it('addIcon() supports Promise', (done) => {
imageEditor
.addIcon('arrow', {
left: 10,
top: 10,
})
.then(() => {
expect(canvas.getObjects().length).toBe(1);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('clearObjects() supports Promise', (done) => {
imageEditor
.addIcon('arrow', {
left: 10,
top: 10,
})
.then(() => imageEditor.clearObjects())
.then(() => {
expect(canvas.getObjects().length).toBe(0);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('changeIconColor() supports Promise', (done) => {
imageEditor
.addIcon('arrow', {
left: 10,
top: 10,
})
.then(() => imageEditor.changeIconColor(activeObjectId, '#FFFF00'))
.then(() => {
expect(canvas.getObjects()[0].fill).toBe('#FFFF00');
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('addShape() supports Promise', (done) => {
imageEditor
.addShape('rect', {
width: 100,
height: 100,
fill: '#FFFF00',
})
.then(() => {
const [shape] = canvas.getObjects();
expect(shape.type).toBe('rect');
expect(shape.width).toBe(100);
expect(shape.height).toBe(100);
expect(shape.fill).toBe('#FFFF00');
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('changeShape() supports Promise', (done) => {
imageEditor
.addShape('rect', {
width: 100,
height: 100,
fill: '#FFFF00',
})
.then(() =>
imageEditor.changeShape(activeObjectId, {
type: 'triangle',
width: 200,
fill: '#FF0000',
})
)
.then(() => {
const [shape] = canvas.getObjects();
expect(shape.type).toBe('triangle');
expect(shape.width).toBe(200);
expect(shape.fill).toBe('#FF0000');
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('can catch on failure', (done) => {
imageEditor
.addShape('rect', {
width: 100,
height: 100,
fill: '#FFFF00',
})
.then(() => {
imageEditor.deactivateAll();
return imageEditor.changeShape(null, {
type: 'triangle',
widht: 200,
fill: '#FF0000',
});
})
.then(() => {
fail();
done();
})
['catch']((message) => {
expect(message).toBe('The object is not in canvas.');
done();
});
});
it('addImageObject() supports Promise', (done) => {
const maskImageURL = 'base/test/fixtures/mask.png';
imageEditor
.addImageObject(maskImageURL)
.then((objectProps) => {
expect(canvas.getObjects().length).toBe(1);
expect(objectProps.id).toBe(activeObjectId);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('resizeCanvasDimension() supports Promise', (done) => {
imageEditor
.resizeCanvasDimension({
width: 900,
height: 700,
})
.then(() =>
// There is no way to get canvas dimension
done()
)
['catch']((message) => {
fail(message);
done();
});
});
it('undo() supports Promise', (done) => {
imageEditor
.addShape('rect', {
width: 100,
height: 100,
fill: '#FFFF00',
})
.then(() => imageEditor.undo())
.then(() => {
expect(canvas.getObjects().length).toBe(0);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('flipX() supports Promise', (done) => {
imageEditor
.flipX()
.then((obj) => {
expect(obj).toEqual({
flipX: true,
flipY: false,
angle: 0,
});
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('flipY() supports Promise', (done) => {
imageEditor
.flipY()
.then((obj) => {
expect(obj).toEqual({
flipX: false,
flipY: true,
angle: 0,
});
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('resetFlip() supports Promise', (done) => {
imageEditor
.resetFlip()
.then((obj) => {
expect(obj).toEqual({
flipX: false,
flipY: false,
angle: 0,
});
fail();
done();
})
['catch']((message) => {
expect(message).toBe('The flipX and flipY setting values are not changed.');
done();
});
});
it('rotate() supports Promise', (done) => {
imageEditor
.rotate(10)
.then((angle) => {
expect(angle).toBe(10);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('setAngle() supports Promise', (done) => {
imageEditor
.setAngle(10)
.then((angle) => {
expect(angle).toBe(10);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('removeObject() supports Promise', (done) => {
imageEditor
.addShape('rect', {
width: 100,
height: 100,
})
.then((objectProps) => imageEditor.removeObject(objectProps.id))
.then(() => {
expect(canvas.getObjects().length).toBe(0);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
describe('Watermark', () => {
const maskImageURL = 'base/test/fixtures/mask.png';
const properties = {
fill: 'rgba(255, 255, 0, 0.5)',
left: 150,
top: 30,
};
beforeEach((done) => {
imageEditor.addImageObject(maskImageURL).then(() => {
done();
});
});
it("setObjectProperties() should change object's properties", (done) => {
imageEditor
.setObjectProperties(activeObjectId, properties)
.then(() => {
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it("getObjectProperties() should return object's properties", (done) => {
imageEditor
.setObjectProperties(activeObjectId, properties)
.then(() => {
const propKeys = {
fill: null,
left: null,
top: null,
};
const result = imageEditor.getObjectProperties(activeObjectId, propKeys);
expect(result).not.toBe(null);
expect(result).toEqual(
jasmine.objectContaining({
fill: 'rgba(255, 255, 0, 0.5)',
left: 150,
top: 30,
})
);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it('getObjectProperties(objectKeys) should return false if there is no object', (done) => {
imageEditor
.setObjectProperties(activeObjectId, properties)
.then(() => {
const propKeys = {
fill: null,
width: null,
left: null,
top: null,
height: null,
};
imageEditor.deactivateAll();
const result = imageEditor.getObjectProperties(null, propKeys);
expect(result).toBe(null);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it("getObjectProperties(arrayKeys) should return object's properties", (done) => {
imageEditor
.setObjectProperties(activeObjectId, properties)
.then(() => {
const arrayKeys = ['fill', 'width', 'left', 'top', 'height'];
const result = imageEditor.getObjectProperties(activeObjectId, arrayKeys);
expect(result).not.toBe(null);
expect(result).toEqual(jasmine.objectContaining(properties));
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it("getObjectProperties(stringKey) should return object's property", (done) => {
imageEditor
.setObjectProperties(activeObjectId, properties)
.then(() => {
const result = imageEditor.getObjectProperties(activeObjectId, 'fill');
expect(result).not.toBe(null);
expect(result).toEqual(
jasmine.objectContaining({
fill: 'rgba(255, 255, 0, 0.5)',
})
);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
it("getCanvasSize() should return canvas's width, height.", () => {
expect(imageEditor.getCanvasSize()).toEqual(
jasmine.objectContaining({
width: 1600,
height: 1066,
})
);
});
it('getObjectPosition() should return global point by origin.', () => {
// ImageEditor's object has origin('center', 'center').
const { left, top, width, height } = imageEditor.getObjectProperties(activeObjectId, [
'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');
expect(ltPoint.x).toBe(left - width / 2);
expect(ltPoint.y).toBe(top - height / 2);
expect(ccPoint.x).toBe(left);
expect(ccPoint.y).toBe(top);
expect(rbPoint.x).toBe(left + width / 2);
expect(rbPoint.y).toBe(top + height / 2);
});
it('setObjectPosition() can set object position by origin', (done) => {
imageEditor
.setObjectProperties(activeObjectId, {
width: 200,
height: 100,
})
.then(() =>
imageEditor.setObjectPosition(activeObjectId, {
x: 0,
y: 0,
originX: 'left',
originY: 'top',
})
)
.then(() => {
const result = imageEditor.getObjectProperties(activeObjectId, ['left', 'top']);
expect(result.left).toBe(100);
expect(result.top).toBe(50);
done();
})
['catch']((message) => {
fail(message);
done();
});
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/rotation.js"
*/
import fabric from 'fabric';
import $ from 'jquery';
import Graphics from '../src/js/graphics';
import Rotation from '../src/js/component/rotation';
describe('Rotation', () => {
let graphics, rotationModule, mockImage, canvas;
beforeAll(() => {
graphics = new Graphics($('<canvas>')[0]);
canvas = graphics.getCanvas();
rotationModule = new Rotation(graphics);
});
beforeEach(() => {
mockImage = new fabric.Image();
graphics.setCanvasImage('mockImage', mockImage);
});
it('"getCurrentAngle()" should return current angle value', () => {
mockImage.angle = 30;
expect(rotationModule.getCurrentAngle()).toEqual(30);
});
it('"setAngle()" should set angle value', () => {
rotationModule.setAngle(40);
expect(rotationModule.getCurrentAngle()).toEqual(40);
});
it('"rotate()" should add angle value', () => {
let current = rotationModule.getCurrentAngle();
rotationModule.rotate(10);
expect(rotationModule.getCurrentAngle()).toBe(current + 10);
current = rotationModule.getCurrentAngle();
rotationModule.rotate(20);
expect(rotationModule.getCurrentAngle()).toBe(current + 20);
});
it('"rotate()" should add angle value modular 360(===2*PI)', (done) => {
rotationModule
.setAngle(10)
.then(() => rotationModule.rotate(380))
.then(() => {
expect(rotationModule.getCurrentAngle()).toBe(30);
done();
});
});
// @todo Move this tc to main.spec.js
it('"adjustCanvasDimension()" should set canvas dimension from image-rect', () => {
spyOn(mockImage, 'getBoundingRect').and.returnValue({
width: 100,
height: 110,
});
rotationModule.adjustCanvasDimension();
expect(canvas.getWidth()).toEqual(100);
expect(canvas.getHeight()).toEqual(110);
});
});
import {
setCachedUndoDataForDimension,
getCachedUndoDataForDimension,
makeSelectionUndoData,
makeSelectionUndoDatum,
} from '../src/js/helper/selectionModifyHelper';
import Graphics from '../src/js/graphics';
import fabric from 'fabric';
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,
});
});
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/line.js"
*/
import fabric from 'fabric';
import $ from 'jquery';
import Graphics from '../src/js/graphics';
import Shape from '../src/js/component/shape';
import { resize } from '../src/js/helper/shapeResizeHelper';
import {
getFillImageFromShape,
getCachedCanvasImageElement,
} from '../src/js/helper/shapeFilterFillHelper';
describe('Shape', () => {
let canvas, graphics, mockImage, fEvent, shape, shapeObj;
beforeAll(() => {
graphics = new Graphics($('<canvas>')[0]);
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('The origin direction and position value initially adjusted at resize must 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();
spyOn(shapeObj, 'set').and.callThrough();
resize(shapeObj, pointer);
const [{ left: resultLeft, top: resultTop }] = shapeObj.set.calls.first().args;
expect(resultLeft).toBe(settings.left - settings.width / 2);
expect(resultTop).toBe(settings.top - settings.height / 2);
});
it('The rectagle object is created on canvas.', () => {
shape.add('rect');
[shapeObj] = canvas.getObjects();
expect(shapeObj.type).toBe('rect');
});
it('The circle object(ellipse) is created on canvas.', () => {
shape.add('circle');
[shapeObj] = canvas.getObjects();
expect(shapeObj.type).toBe('circle');
});
it('The triangle object is created on canvas.', () => {
shape.add('triangle');
[shapeObj] = canvas.getObjects();
expect(shapeObj.type).toBe('triangle');
});
it('When add() is called with no options, the default options set the rectangle object.', () => {
shape.add('rect');
[shapeObj] = canvas.getObjects();
expect(shapeObj.width).toBe(1); // strokeWidth: 1, width: 1
expect(shapeObj.height).toBe(1); // strokeWidth: 1, height: 1
});
it('When add() is called with no options, the default options set the circle object.', () => {
shape.add('circle');
[shapeObj] = canvas.getObjects();
expect(shapeObj.width).toBe(0);
expect(shapeObj.height).toBe(0);
});
it('When add() is called with no options, the default options set the triangle object.', () => {
shape.add('triangle');
[shapeObj] = canvas.getObjects();
expect(shapeObj.width).toBe(1); // strokeWidth: 1, width: 1
expect(shapeObj.height).toBe(1); // strokeWidth: 1, height: 1
});
it('When add() is called with the options, this options set the rectagle object.', () => {
const settings = {
fill: 'blue',
stroke: 'red',
strokeWidth: 10,
type: 'rect',
width: 100,
height: 100,
};
shape.add('rect', settings);
[shapeObj] = canvas.getObjects();
expect(shapeObj.fill).toBe('blue');
expect(shapeObj.stroke).toBe('red');
expect(shapeObj.strokeWidth).toBe(10);
expect(shapeObj.width).toBe(100); // width + storkeWidth
expect(shapeObj.height).toBe(100); // height + storkeWidth
});
it('When add() is called with the options, this options set the circle object.', () => {
const settings = {
fill: 'blue',
stroke: 'red',
strokeWidth: 3,
type: 'circle',
rx: 100,
ry: 50,
};
shape.add('circle', settings);
[shapeObj] = canvas.getObjects();
expect(shapeObj.fill).toBe('blue');
expect(shapeObj.stroke).toBe('red');
expect(shapeObj.strokeWidth).toBe(3);
expect(shapeObj.width).toBe(200); // rx * 2 + stokeWidth
expect(shapeObj.height).toBe(100); // ry * 2 + stokeWidth
});
it('When add() is called with the options, this options set the triangle object.', () => {
const settings = {
fill: 'blue',
stroke: 'red',
strokeWidth: 0,
type: 'triangle',
width: 100,
height: 100,
};
shape.add('triangle', settings);
[shapeObj] = canvas.getObjects();
expect(shapeObj.fill).toBe('blue');
expect(shapeObj.stroke).toBe('red');
expect(shapeObj.strokeWidth).toBe(0);
expect(shapeObj.width).toBe(100);
expect(shapeObj.height).toBe(100);
});
it('When change() is called, the style of the rectagle object is changed.', () => {
shape.add('rect');
[shapeObj] = canvas.getObjects();
shape.change(shapeObj, {
fill: 'blue',
stroke: 'red',
width: 10,
height: 20,
});
expect(shapeObj.fill).toBe('blue');
expect(shapeObj.stroke).toBe('red');
expect(shapeObj.width).toBe(10);
expect(shapeObj.height).toBe(20);
});
it('When change() is called, the style of the circle object is changed.', () => {
shape.add('circle');
[shapeObj] = canvas.getObjects();
shape.change(shapeObj, {
fill: 'blue',
stroke: 'red',
rx: 10,
ry: 20,
});
expect(shapeObj.fill).toBe('blue');
expect(shapeObj.stroke).toBe('red');
expect(shapeObj.width).toBe(20);
expect(shapeObj.height).toBe(40);
});
it('When change() is called, the style of the triangle object is changed.', () => {
shape.add('triangle');
[shapeObj] = canvas.getObjects();
shape.change(shapeObj, {
width: 10,
height: 20,
});
expect(shapeObj.fill).toBe('#ffffff');
expect(shapeObj.stroke).toBe('#000000');
expect(shapeObj.width).toBe(10);
expect(shapeObj.height).toBe(20);
});
describe('Fill - filter type', () => {
beforeEach((done) => {
const imageURL = 'base/test/fixtures/sampleImage.jpg';
getCachedCanvasImageElement(canvas, true);
fabric.Image.fromURL(imageURL, (sampleImage) => {
graphics.setCanvasImage('', sampleImage);
shape.add('rect', {
strokeWidth: 0,
left: 20,
top: 30,
width: 100,
height: 80,
fill: {
type: 'filter',
filter: [{ pixelate: 20 }],
},
});
[shapeObj] = canvas.getObjects();
done();
});
});
it('"_resetPositionFillFilter" should be executed when a movement, rotation, and scaling event of a filter type fill is applied.', () => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 10,
y: 10,
});
spyOn(shape, '_resetPositionFillFilter');
shapeObj.fire('moving');
shapeObj.fire('rotating');
shapeObj.fire('scaling');
expect(shape._resetPositionFillFilter.calls.count()).toBe(3);
});
it('cropX and cropY values of the image filled with the shape background must be changed to match the canvas background exactly.', () => {
shape._resetPositionFillFilter(shapeObj);
const { cropX, cropY } = getFillImageFromShape(shapeObj);
expect(cropX).toBe(-30);
expect(cropY).toBe(-10);
});
it('The fill image should be the same size as the shape.', () => {
shape._resetPositionFillFilter(shapeObj);
const { width, height } = getFillImageFromShape(shapeObj);
expect(width).toBe(100);
expect(height).toBe(80);
});
it('The rotated object fill image must be the same size as the rectangle that draws the rotated object border.', () => {
shapeObj.set({
angle: 40,
});
shape._resetPositionFillFilter(shapeObj);
const { width, height } = getFillImageFromShape(shapeObj);
expect(Math.round(width)).toBe(128);
expect(Math.round(height)).toBe(126);
});
it('If repositioning is performed while the angle is changed, the angle value of the fill image must have the shape reverse rotation value.', () => {
shapeObj.set({
angle: 40,
});
shape._resetPositionFillFilter(shapeObj);
const { angle } = getFillImageFromShape(shapeObj);
expect(angle).toBe(-40);
});
it('For shapes that go outside the bottom right area of the canvas, the size and position of the image position should give the expected result.', (done) => {
shape
.add('rect', {
strokeWidth: 0,
left: 250,
top: 100,
width: 200,
height: 200,
fill: {
type: 'filter',
filter: [{ pixelate: 20 }],
},
})
.then((props) => {
shapeObj = graphics.getObject(props.id);
const fillImage = getFillImageFromShape(shapeObj);
const { top, height, left, width } = fillImage;
expect(top).toBe(75);
expect(left).toBe(75);
expect(height).toBe(150);
expect(width).toBe(150);
done();
});
});
it('For shapes that go outside the top left area of the canvas, the size and position of the image position should give the expected result.', (done) => {
shape
.add('rect', {
strokeWidth: 0,
left: 50,
top: 30,
width: 200,
height: 70,
fill: {
type: 'filter',
filter: [{ pixelate: 20 }],
},
})
.then((props) => {
shapeObj = graphics.getObject(props.id);
const fillImage = getFillImageFromShape(shapeObj);
const { top, height, left, width } = fillImage;
expect(Math.round(top)).toBe(40);
expect(left).toBe(150);
expect(height).toBe(70);
expect(width).toBe(200);
done();
});
});
it('Background image of the shape to which the filter fill is applied must have the filter applied.', () => {
const fillImage = getFillImageFromShape(shapeObj);
expect(fillImage.filters.length).toBeGreaterThan(0);
});
});
describe('_onFabricMouseMove()', () => {
beforeEach(() => {
shape.add('rect', {
left: 100,
top: 100,
});
[shapeObj] = canvas.getObjects();
shape._shapeObj = shapeObj;
});
it(
'When the mouse direction is in 1th quadrant,' +
'the origin values of shape set to "left" and "top".',
() => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 200,
y: 120,
});
shape._onFabricMouseMove(fEvent);
expect(shapeObj.originX).toBe('left');
expect(shapeObj.originY).toBe('top');
}
);
it(
'When the mouse direction is in 2th quadrant,' +
'the origin values of shape set to "right" and "top".',
() => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 80,
y: 100,
});
shape._onFabricMouseMove(fEvent);
expect(shapeObj.originX).toBe('right');
expect(shapeObj.originY).toBe('top');
}
);
it(
'When the mouse direction is in 3th quadrant,' +
'the origin values of shape set to "right" and "bottom".',
() => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 80,
y: 80,
});
shape._onFabricMouseMove(fEvent);
expect(shapeObj.originX).toBe('right');
expect(shapeObj.originY).toBe('bottom');
}
);
it(
'When the mouse direction is in 4th quadrant,' +
'the origin values of shape set to "left" and "bottom".',
() => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 200,
y: 80,
});
shape._onFabricMouseMove(fEvent);
expect(shapeObj.originX).toBe('left');
expect(shapeObj.originY).toBe('bottom');
}
);
});
describe('_onFabricMouseUp()', () => {
let startPoint, expectedPoint;
beforeEach(() => {
shape.add('circle', {
left: 100,
top: 100,
});
[shapeObj] = canvas.getObjects();
shape._shapeObj = shapeObj;
});
it('When the drawing shape is in 1th quadrant, "left" and "top" are the same as start point.', () => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 200,
y: 120,
});
startPoint = shapeObj.getPointByOrigin('left', 'top');
shape._onFabricMouseMove(fEvent);
shape._onFabricMouseUp();
expectedPoint = shapeObj.getPointByOrigin('left', 'top');
expect(expectedPoint.x).toBe(startPoint.x);
expect(expectedPoint.y).toBe(startPoint.y);
});
it('When the drawing shape is in 2th quadrant, "right" and "top" are the same as start point.', () => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 80,
y: 120,
});
startPoint = shapeObj.getPointByOrigin('right', 'top');
shape._onFabricMouseMove(fEvent);
shape._onFabricMouseUp();
expectedPoint = shapeObj.getPointByOrigin('right', 'top');
expect(expectedPoint.x).toBe(startPoint.x);
expect(expectedPoint.y).toBe(startPoint.y);
});
it('When the drawing shape is in 3th quadrant, "right" and "bottom" are the same as start point.', () => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 80,
y: 80,
});
startPoint = shapeObj.getPointByOrigin('right', 'bottom');
shape._onFabricMouseMove(fEvent);
shape._onFabricMouseUp();
expectedPoint = shapeObj.getPointByOrigin('right', 'bottom');
expect(expectedPoint.x).toBe(startPoint.x);
expect(expectedPoint.y).toBe(startPoint.y);
});
it('When the drawing shape is in 4th quadrant, "left" and "bottom" are the same as start point.', () => {
spyOn(canvas, 'getPointer').and.returnValue({
x: 120,
y: 80,
});
startPoint = shapeObj.getPointByOrigin('left', 'bottom');
shape._onFabricMouseMove(fEvent);
shape._onFabricMouseUp();
expectedPoint = shapeObj.getPointByOrigin('left', 'bottom');
expect(expectedPoint.x).toBe(startPoint.x);
expect(expectedPoint.y).toBe(startPoint.y);
});
});
it(
'When drawing the shape with mouse and the "isRegular" option set to true, ' +
'the created rectangle shape has the same "width" and "height" values.',
() => {
shape.add('rect', {
left: 0,
top: 0,
});
shape._withShiftKey = true;
[shapeObj] = canvas.getObjects();
shape._shapeObj = shapeObj;
spyOn(canvas, 'getPointer').and.returnValue({
x: 200,
y: 100,
});
shape._onFabricMouseMove(fEvent);
shape._onFabricMouseUp();
expect(shapeObj.width).toBe(200); // has 1 storkeWidth
expect(shapeObj.height).toBe(200); // has 1 storkeWidth
}
);
it(
'When drawing the shape with mouse and the "isRegular" option set to true, ' +
'the created rectangle shape has the same "width" and "height" values.',
() => {
shape.add('rect', {
left: 0,
top: 0,
});
shape._withShiftKey = true;
[shapeObj] = canvas.getObjects();
shape._shapeObj = shapeObj;
spyOn(canvas, 'getPointer').and.returnValue({
x: 100,
y: 200,
});
shape._onFabricMouseMove(fEvent);
shape._onFabricMouseUp();
expect(shapeObj.width).toBe(200); // has 1 storkeWidth
expect(shapeObj.height).toBe(200); // has 1 storkeWidth
}
);
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/text.js"
*/
import fabric from 'fabric';
import $ from 'jquery';
import Graphics from '../src/js/graphics';
import Text from '../src/js/component/text';
describe('Text', () => {
let canvas, graphics, mockImage, text;
beforeAll(() => {
graphics = new Graphics($('<canvas>')[0]);
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).toEqual('');
});
it('should make the text object set default option when parameter has not "styles" property.', () => {
const newTextStyle = activeObj.fontWeight;
expect(newTextStyle).toEqual('normal');
});
it('should create the text object on center of canvas when parameter has not "position" property.', () => {
const mockImagePos = mockImage.getCenterPoint();
expect(activeObj.left).toEqual(mockImagePos.x);
expect(activeObj.top).toEqual(mockImagePos.y);
});
it('Default option for autofocus should be true when adding text.', (done) => {
text.add('default', {}).then((info) => {
const newText = graphics.getObject(info.id);
expect(newText.selectionStart).toBe(0);
expect(newText.selectionEnd).toBe(7);
expect(newText.isEditing).toBe(true);
done();
});
});
});
it('Rotated text elements must also 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.left).toEqual(left);
expect(newText.top).toEqual(top);
});
it('change() should change contents in the text object as input.', () => {
text.add('text123', {});
const activeObj = canvas.getActiveObject();
text.change(activeObj, 'abc');
expect(activeObj.text).toEqual('abc');
text.change(activeObj, 'def');
expect(activeObj.text).toEqual('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.toEqual('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).toEqual('italic');
});
});
it('_onFabricScaling() should change size of selected text object.', () => {
const obj = new fabric.Text('test');
const mock = {
target: obj,
};
const scale = 10;
const originSize = obj.fontSize;
text.start({});
canvas.add(obj);
obj.scaleY = scale;
canvas.fire('object:scaling', mock);
expect(obj.fontSize).toEqual(originSize * scale);
});
});
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/component/cropper.js"
*/
import snippet from 'tui-code-snippet';
import Theme from '../src/js/ui/theme/theme';
import defaultTheme from '../src/js/ui/theme/standard';
describe('Theme', () => {
let theme;
beforeEach(() => {
theme = new Theme(defaultTheme);
});
describe('getStyle()', () => {
it('When the user sets the icon file location, the path and name information must be included.', () => {
const addUserIconPath = 'base/test/fixtures/icon-d.svg';
const addUserIconName = 'icon-d';
const themeForIconPathSet = new Theme(
snippet.extend({}, defaultTheme, {
'menu.normalIcon.path': addUserIconPath,
'menu.normalIcon.name': addUserIconName,
})
);
const {
normal: { path, name },
} = themeForIconPathSet.getStyle('menu.icon');
expect(path).toEqual('base/test/fixtures/icon-d.svg');
expect(name).toEqual('icon-d');
});
it('should return default icon color information.', () => {
const { normal, active, disabled, hover } = theme.getStyle('menu.icon');
expect(normal.color).toEqual('#8a8a8a');
expect(active.color).toEqual('#555555');
expect(disabled.color).toEqual('#434343');
expect(hover.color).toEqual('#e9e9e9');
});
it('In normal types, cssText should be returned.', () => {
theme.styles.normal = {
backgroundColor: '#fdba3b',
border: '1px solid #fdba3b',
color: '#fff',
fontFamily: 'NotoSans, sans-serif',
fontSize: '12px',
};
const expected =
'background-color: #fdba3b;border: 1px solid #fdba3b;color: #fff;font-family: NotoSans, sans-serif;font-size: 12px';
expect(theme.getStyle('normal')).toBe(expected);
});
it('If all members are objects, you must leave the structure intact and return cssText.', () => {
theme.styles['submenu.normalLabel'] = {
color: '#858585',
fontWeight: 'normal',
};
theme.styles['submenu.activeLabel'] = {
color: '#000',
fontWeight: 'normal',
};
const expected = {
normal: 'color: #858585;font-weight: normal',
active: 'color: #000;font-weight: normal',
};
expect(theme.getStyle('submenu.label')).toEqual(expected);
});
});
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',
};
const expected =
'background-color: #fff;background-image: url(./img/bg.png);border: 1px solid #ddd;color: #222;font-family: NotoSans, sans-serif;font-size: 12px';
expect(theme._makeCssText(styleObject)).toBe(expected);
});
});
describe('_makeSvgItem()', () => {
it('When using the default icon, a svg set with the path prefix and no use-default class should be created.', () => {
const useTagString = theme._makeSvgItem(['normal'], 'crop');
expect(useTagString).toBe('<use xlink:href="#ic-crop" class="normal use-default"/>');
});
it('Setting the icon file should create a svg path with the prefix.', () => {
const themeForIconPathSet = new Theme(
snippet.extend({}, defaultTheme, {
'menu.normalIcon.path': 'base/test/fixtures/icon-d.svg',
'menu.normalIcon.name': 'icon-d',
})
);
const useTagString = themeForIconPathSet._makeSvgItem(['normal'], 'crop');
expect(useTagString).toBe(
'<use xlink:href="base/test/fixtures/icon-d.svg#icon-d-ic-crop" class="normal"/>'
);
});
});
});
{
"compilerOptions": {
"noEmit": true,
"noImplicitAny": false
},
"include": ["../../index.d.ts", "./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 } });
/**
* @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Test cases of "src/js/ui.js"
*/
import snippet from 'tui-code-snippet';
import { Promise } from '../src/js/util';
import UI from '../src/js/ui';
import { HELP_MENUS } from '../src/js/consts';
describe('UI', () => {
let ui;
let uiOptions;
beforeEach(() => {
uiOptions = {
loadImage: {
path: 'mockImagePath',
name: '',
},
menu: ['crop', 'flip', 'rotate', 'draw', 'shape', 'icon', 'text', 'mask', 'filter'],
initMenu: 'shape',
menuBarPosition: 'bottom',
};
ui = new UI(document.createElement('div'), uiOptions, {});
});
describe('Destroy()', () => {
it('"_destroyAllMenu()" The "destroy" function of all menu instances must be executed.', () => {
snippet.forEach(uiOptions.menu, (menuName) => {
spyOn(ui[menuName], 'destroy');
});
ui._destroyAllMenu();
snippet.forEach(uiOptions.menu, (menuName) => {
expect(ui[menuName].destroy).toHaveBeenCalled();
});
});
it('"_removeUiEvent()" must execute "removeEventListener" of all menus.', () => {
const allUiButtonElementName = [...uiOptions.menu, ...HELP_MENUS];
snippet.forEach(allUiButtonElementName, (elementName) => {
spyOn(ui._buttonElements[elementName], 'removeEventListener');
});
ui._removeUiEvent();
snippet.forEach(allUiButtonElementName, (elementName) => {
expect(ui._buttonElements[elementName].removeEventListener).toHaveBeenCalled();
});
});
});
describe('_changeMenu()', () => {
beforeEach(() => {
ui.submenu = 'shape';
spyOn(ui, 'resizeEditor');
spyOn(ui.shape, 'changeStandbyMode');
spyOn(ui.filter, 'changeStartMode');
ui._actions.main = {
changeSelectableAll: jasmine.createSpy('changeSelectableAll'),
};
ui._changeMenu('filter', false, false);
});
it('When the menu changes, the changeStartMode () of the menu instance to be changed must be executed.', () => {
expect(ui.shape.changeStandbyMode).toHaveBeenCalled();
});
it('When the menu changes, the changeStandbyMode () of the existing menu instance must be executed.', () => {
expect(ui.filter.changeStartMode).toHaveBeenCalled();
});
});
describe('_makeSubMenu()', () => {
it('MakeMenuElement should be executed for the number of menus specified in the option.', () => {
spyOn(ui, '_makeMenuElement');
ui._makeSubMenu();
expect(ui._makeMenuElement.calls.count()).toBe(uiOptions.menu.length);
});
it('Instance of the menu specified in the option must be created.', () => {
spyOn(ui, '_makeMenuElement');
const getConstructorName = (constructor) =>
constructor.toString().match(/^function\s(.+?)\(/)[1];
ui._makeSubMenu();
snippet.forEach(uiOptions.menu, (menuName) => {
const constructorNameOfInstance = getConstructorName(ui[menuName].constructor);
const expected = menuName.replace(/^[a-z]/, ($0) => $0.toUpperCase());
expect(constructorNameOfInstance).toBe(expected);
});
});
});
describe('initCanvas()', () => {
let promise;
beforeEach(() => {
promise = new Promise((resolve) => {
resolve();
});
ui._editorElement = {
querySelector: jasmine
.createSpy('querySelector')
.and.returnValue(document.createElement('div')),
};
ui._actions.main = {
initLoadImage: jasmine.createSpy('initLoadImage').and.returnValue(promise),
};
});
it('When initCanvas is executed, some internal methods must be run as required.', (done) => {
spyOn(ui, 'activeMenuEvent');
spyOn(ui, '_addLoadEvent');
ui.initCanvas();
promise.then(() => {
expect(ui.activeMenuEvent).toHaveBeenCalled();
expect(ui._addLoadEvent).toHaveBeenCalled();
done();
});
});
it('`initLoadImage()` should not be run when has not image path.', () => {
spyOn(ui, '_getLoadImage').and.returnValue({ path: '' });
ui.initCanvas();
expect(ui._actions.main.initLoadImage).not.toHaveBeenCalled();
});
it('`_AddLoadEvent()` should be executed even if there is no image path.', () => {
spyOn(ui, '_getLoadImage').and.returnValue({ path: '' });
spyOn(ui, '_addLoadEvent');
ui.initCanvas();
expect(ui._addLoadEvent).toHaveBeenCalled();
});
});
describe('_setEditorPosition()', () => {
beforeEach(() => {
ui._editorElement = document.createElement('div');
spyOn(ui, '_getCanvasMaxDimension').and.returnValue({
width: 300,
height: 300,
});
});
it('Position is bottom, it should be reflected in the bottom of the editor position.', () => {
ui.submenu = true;
ui._setEditorPosition('bottom');
expect(ui._editorElement.style.top).toBe('150px');
expect(ui._editorElement.style.left).toBe('0px');
});
it('Position is top, it should be reflected in the top of the editor position.', () => {
ui.submenu = true;
ui._setEditorPosition('top');
expect(ui._editorElement.style.top).toBe('-150px');
expect(ui._editorElement.style.left).toBe('0px');
});
it('Position is left, it should be reflected in the left, right of the editor position.', () => {
ui.submenu = true;
ui._setEditorPosition('left');
expect(ui._editorElement.style.top).toBe('0px');
expect(ui._editorElement.style.left).toBe('-150px');
});
it('Position is right, it should be reflected in the right of the editor position.', () => {
ui.submenu = true;
ui._setEditorPosition('right');
expect(ui._editorElement.style.top).toBe('0px');
expect(ui._editorElement.style.left).toBe('150px');
});
});
});
import Range from '../src/js/ui/tools/range';
import { defaultRotateRangeValus } from '../src/js/consts';
describe('Range', () => {
let range, input, slider;
beforeEach(() => {
input = document.createElement('input');
slider = document.createElement('div');
range = new Range(
{
slider,
input,
},
defaultRotateRangeValus
);
});
it('The value must be incremented by 1, 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('The value must be decremented by 1, 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('The `changeInput` event handler should filter out any invalid input values.', () => {
const ev = {
target: input,
keyCode: 83,
};
input.value = '-3!!6s0s';
range.eventHandler.changeInput(ev);
expect(range.value).toBe(-360);
});
});
/*eslint-disable*/
var fs = require('fs');
var path = require('path');
var pkg = require('./package.json');
var tsVersion = /[0-9.]+/.exec(pkg.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, data) {
if (error) {
throw error;
}
console.log('Completed Write Banner for Typescript!');
});
});
{
"extends": "tslint:recommended",
"rules": {
"quotemark": [true, "single"],
"trailing-comma": false,
"max-classes-per-file": false,
"no-namespace": false
}
}
{
"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",
"linkUrl": "https://github.com/nhn"
},
{
"title": "FE Development Lab",
"linkUrl": "https://github.com/nhn/fe.javascript"
}
],
"main": {
"filePath": "README.md"
},
"api": {
"filePath": "src/js/**",
"fileLink": true
},
"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"
}
/**
* webpack.config.js created on 2016. 12. 01.
* @author NHN Ent. FE Development Lab <dl_javascript@nhn.com>
*/
const pkg = require('./package.json');
const path = require('path');
const webpack = require('webpack');
const SafeUmdPlugin = require('safe-umd-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizaeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const isProduction = process.argv.indexOf('-p') > -1;
const FILENAME = pkg.name + (isProduction ? '.min' : '');
const BANNER = [
`${FILENAME}.js`,
`@version ${pkg.version}`,
`@author ${pkg.author}`,
`@license ${pkg.license}`,
].join('\n');
module.exports = {
mode: isProduction ? 'production' : 'development',
entry: './src/index.js',
output: {
library: ['tui', 'ImageEditor'],
libraryTarget: 'umd',
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist',
filename: `${FILENAME}.js`,
},
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'],
},
fabric: {
commonjs: ['fabric', 'fabric'],
commonjs2: ['fabric', 'fabric'],
amd: 'fabric',
root: 'fabric',
},
},
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
enforce: 'pre',
options: {
failOnWarning: false,
failOnError: false,
},
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader?cacheDirectory',
options: {
babelrc: true,
},
},
{
test: /\.styl$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'stylus-loader',
options: {
sourceMap: true,
},
},
],
},
{
test: /\.svg$/,
loader: 'svg-inline-loader',
},
],
},
plugins: [
new webpack.BannerPlugin(BANNER),
new MiniCssExtractPlugin({
filename: `${FILENAME}.css`,
}),
new SafeUmdPlugin(),
],
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizaeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
},
},
}),
],
},
devServer: {
historyApiFallback: false,
progress: true,
inline: true,
host: '0.0.0.0',
disableHostCheck: true,
},
};