MinsoftK

redownload

Showing 276 changed files with 0 additions and 19259 deletions
/* 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>