├── .eslintignore ├── .eslintrc.js ├── .fatherrc.ts ├── .gitignore ├── .prettierrc ├── LICENSE ├── README _CN.md ├── README.md ├── index.ts ├── package.json ├── publish.sh ├── src ├── KonvaGroup.tsx ├── KonvaImg.tsx ├── KonvaShape.tsx ├── KonvaText.tsx ├── hoc │ └── withTransform.tsx ├── hooks │ └── usePrevious.ts ├── index.tsx ├── keyboardListener.ts ├── readme.md ├── type.ts ├── utils │ ├── circularQueue.ts │ ├── debounce.ts │ ├── handleKonvaItem.ts │ ├── handleSize.ts │ ├── imageAdapt.ts │ ├── index.ts │ ├── textHandler.ts │ └── utils.ts └── 字段说明.md ├── tsconfig.json └── unpublish.sh /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | scripts/* 3 | dist/* 4 | config/* 5 | src/**/*.less 6 | src/**/*.css -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['react-app'], 3 | plugins: ['react', 'prettier'], 4 | rules: { 5 | 'prettier/prettier': 1, 6 | 'max-len': [1, 160], 7 | 'no-fallthrough': [0], 8 | }, 9 | env: { 10 | es6: true, 11 | browser: true, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "father"; 2 | 3 | export default defineConfig({ 4 | cjs: { 5 | output: "lib", 6 | }, 7 | esm: { 8 | output: "esm", 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #project 2 | node_modules 3 | dist 4 | .npmrc 5 | storage 6 | types 7 | .npm_token 8 | lib 9 | esm 10 | package-lock.json 11 | .idea -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "endOfLine": "lf" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | Copy license text to clipboard 676 | Suggest this license 677 | Make a pull request to suggest this license for a project that is not licensed. Please be polite: see if a license has already been suggested, try to suggest a license fitting for the project’s community, and keep your communication with project maintainers friendly. 678 | 679 | Enter GitHub repository URL 680 | How to apply this license 681 | Create a text file (typically named COPYING, as per GNU conventions) in the root of your source code and copy the text of the license into the file. 682 | 683 | Optional steps 684 | The Free Software Foundation recommends taking the additional step of adding a boilerplate notice to the top of each file. The boilerplate can be found at the end of the license. 685 | 686 | Add GPL-3.0-or-later (or GPL-3.0-only to disallow future versions) to your project’s package description, if applicable (e.g., Node.js, Ruby, and Rust). This will ensure the license is displayed in package directories. 687 | 688 | Source 689 | Who’s using this license? 690 | Ansible 691 | Bash 692 | GIMP 693 | -------------------------------------------------------------------------------- /README _CN.md: -------------------------------------------------------------------------------- 1 | # react-konva-editor 2 | 3 | 使用`react`和`konva`开发的图像编辑器底层组件,纯数据驱动,无副作用! 4 | 可以与[react-konva-editor-kit](https://github.com/mytac/react-konva-editor-kit)(包含一系列图像、文字的编辑控件)共同使用。 5 | 6 | ## 安装方式 7 | 8 | ``` 9 | $ npm install react-konva-editor 10 | ``` 11 | 12 | 或 13 | 14 | ``` 15 | $ yarn add react-konva-editor 16 | ``` 17 | 18 | ## 使用方法 19 | 20 | ```tsx 21 | 41 | ``` 42 | 43 | ## Props 44 | 45 | | propName | type | required | description | 46 | | ------------------ | ------------------------- | -------- | ---------------------------------------------------------- | 47 | | backgroundStyle | `Object` | √ | 画布背景样式 | 48 | | width | `number` | √ | 画布宽 | 49 | | height | `number` | √ | 画布高 | 50 | | backgroundColor | `string` | - | 画布颜色(默认为`#fff`) | 51 | | addItem | `ItemProp` | - | 需在画布上新增元素时,需要改变`addItem` | 52 | | onChangeSelected | `(ItemProp)=>void` | √ | 在画布上选中元素时触发的回调函数,会接收当前选中的元素信息 | 53 | | selectedItemChange | `Object` | - | 改变选中图层的属性 | 54 | | maxStep | `number` | - | 存储操作队列最大缓存步数 | 55 | | stepInfo | `Array\` | √ | 画布上所有元素的信息,是一个数组 | 56 | | bindRef | (ReactRef)=>void | √ | 绑定外层 ref 的函数 | 57 | | setWithdraw | ()=>void | - | 撤销操作的回调函数 | 58 | | setRedo | ()=>void | - | 重做操作的回调函数 | 59 | | onChangeStep | (Array\ )=>void | √ | 画布上任意改变,会触发该函数,返回当前画布信息 | 60 | 61 | ### StepInfo 62 | 63 | #### example 64 | 65 | ```json 66 | [ 67 | { 68 | "type": "shape", 69 | "value": "star", 70 | "fill": "red", 71 | "id": 1001, 72 | "scaleX": 1, 73 | "scaleY": 1, 74 | "rotation": 0, 75 | "skewX": 0, 76 | "skewY": 0, 77 | "x": 195, 78 | "y": 345 79 | }, 80 | { 81 | "type": "text", 82 | "value": "可定义内外径、角数量", 83 | "fontFamily": "默认", 84 | "id": 1002, 85 | "scaleX": 1.1925926495341033, 86 | "scaleY": 1.192592649534104, 87 | "rotation": 0, 88 | "skewX": 0, 89 | "skewY": 0, 90 | "x": 63, 91 | "y": 168, 92 | "fontSize": 25 93 | }, 94 | { 95 | "type": "image", 96 | "value": "https://image.yonghuivip.com/jimu/1/1638943345035942610", 97 | "elementName": "上传人", 98 | "id": 1003, 99 | "x": 19, 100 | "y": 436, 101 | "width": 400, 102 | "height": 400, 103 | "scaleX": 1, 104 | "scaleY": 1, 105 | "rotation": 0, 106 | "skewX": 0, 107 | "skewY": 0 108 | } 109 | ] 110 | ``` 111 | 112 | ![demo](https://s1.ax1x.com/2023/07/25/pCXJyW9.png) 113 | 114 | ### ItemProp 115 | 116 | TODO... 117 | 118 | ## API 119 | 120 | | API name | type | description | 121 | | ------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------- | 122 | | exportToImage | (fileName:string,options?:[ImageExportProps](#ImageExportProps))=>void | 根据当前画布信息输出并下载图片,可定制名称和参数 | 123 | | exportToBASE64 | ()=>void | 根据当前画布信息输出 BASE64 | 124 | | withdraw | ()=>void | 撤销操作,画布信息回退上一步操作 | 125 | | redo | ()=>void | 重做操作,画布信息前移一步操作 | 126 | | canvasScale | (ratio:number)=>void | 缩放当前画布大小 倍数范围:(0,2.75]) | 127 | | deleteItem | ()=>void | 删除当前画布上选中的元素 | 128 | | moveLayerLevel | `(i: number)=>void` | 当 i 小于 0 时,当前选中元素下移图层;当 i 大于 0 时,当前选中元素上移图层 | 129 | | moveLayer | `(direction: 'right' \| 'left' \| 'top'\| 'bottom', delta: number)=>void` | 根据方向参数移动当前选中的元素 | 130 | | setSelectedIndex | `(id:number)=>void` | 根据层级关系选中某个元素 | 131 | | clearSelected | `()=>void` | 清空选中状态 | 132 | | toggleMultiSelected | `(state:boolean)=>void` | 切换多选模式(长按空格打开多选模式,松开关闭) | 133 | | toogleLock | `(index:number)=>void` | 切换图层锁定状态 | 134 | | madeGroup | `(layers:Array\)=>void` | 对选中的图层进行成组(多选模式下) | 135 | | divideGroup | `(groupId:string)=>void` | 根据 groupId 拆开图层组 | 136 | 137 |

ImageExportProps

138 | 139 | | propName | type | required | description | 140 | | -------- | ----------------------------- | -------- | ---------------------------------- | 141 | | scale | `number[0.1-3]` | - | 图像尺寸缩放倍数(默认为 1) | 142 | | quality | `number [0.1-1]` | - | 输出图像的质量(默认为 11) | 143 | | fileType | `'image/png' \| 'image/jpeg'` | - | 输出图像格式(默认为`'image/png'` ) | 144 | 145 | ## Timeline 146 | 147 | 23-2-20 支持多选图层,拆组和解组-可配合 toolkit 使用 148 | 149 | ## Tips 150 | 151 | 1. 如何开启/关闭多选图层模式? 152 | 153 | ```tsx 154 | const handler = useCallback((e: KeyboardEvent) => { 155 | if (e.keyCode === 16) { 156 | // 开启多选模式 157 | KonvaCanvas.toggleMultiSelected(true); 158 | store.setMultiSelected(true); 159 | } 160 | }, []); 161 | 162 | useEffect(() => { 163 | window.addEventListener('keydown', handler); 164 | return () => { 165 | window.removeEventListener('keydown', handler); 166 | }; 167 | }, []); 168 | ``` 169 | 170 | ## reference 171 | 172 | 1. [build reference](https://itnext.io/step-by-step-building-and-publishing-an-npm-typescript-package-44fe7164964c) 173 | 2. [react-konva](https://github.com/konvajs/react-konva) 174 | 175 | ## Todos 176 | 177 | | 序号 | 内容 | 完成情况 | 178 | | ---- | -------------- | -------- | 179 | | 1 | js 环境调通 | √ | 180 | | 2 | ts type 规范化 | delay | 181 | | 3 | ts 环境调通 | √ | 182 | | 4 | 依赖库配置 | delay | 183 | | 5 | 使用文档 | ing | 184 | | 6 | 线上示例 | - | 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-konva-editor 2 | 3 | A fundamental image editor component based on react and konva.You can use it with [react-konva-editor-kit](https://github.com/mytac/react-konva-editor-kit) which supplies some tools about transforming and styling of text and image on canvas. 4 | 5 | [中文](./README%20_CN.md) 6 | 7 | ## Installation 8 | 9 | ``` 10 | $ npm install react-konva-editor 11 | ``` 12 | 13 | or 14 | 15 | ``` 16 | $ yarn add react-konva-editor 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```tsx 22 | 42 | ``` 43 | 44 | ## Props 45 | 46 | | propName | type | required | description | 47 | | ------------------ | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------- | 48 | | backgroundStyle | Object | √ | canvas component background style | 49 | | width | number | √ | canvas width | 50 | | height | number | √ | canvas height | 51 | | backgroundColor | string | - | canvas background color | 52 | | addItem | ItemProp | - | when you need to add something on canvas,you should change `addItem` | 53 | | onChangeSelected | (ItemProp)=>void | √ | return selected item data when you click a layer on canvas | 54 | | selectedItemChange | Object | - | when you need to change the selected item,you should put a object with properties in `selectedItemChange` | 55 | | maxStep | number | - | max length of withdraw/redo queue (default is 10) | 56 | | stepInfo | Array\ | √ | A series of `ItemProp` which had been shown on your canvas | 57 | | bindRef | (ReactRef)=>void | √ | A function bind React Ref | 58 | | setWithdraw | ()=>void | - | A callback when you withdraw operation | 59 | | setRedo | ()=>void | - | A callback when you redo operation | 60 | | onChangeStep | (Array\ )=>void | √ | A callback when you change anything on canvas,it will return ItemProps which means infomations on canvas | 61 | 62 | ### StepInfo 63 | 64 | #### example 65 | 66 | ```json 67 | [ 68 | { 69 | "type": "shape", 70 | "value": "star", 71 | "fill": "red", 72 | "id": 1001, 73 | "scaleX": 1, 74 | "scaleY": 1, 75 | "rotation": 0, 76 | "skewX": 0, 77 | "skewY": 0, 78 | "x": 195, 79 | "y": 345 80 | }, 81 | { 82 | "type": "text", 83 | "value": "可定义内外径、角数量", 84 | "fontFamily": "默认", 85 | "id": 1002, 86 | "scaleX": 1.1925926495341033, 87 | "scaleY": 1.192592649534104, 88 | "rotation": 0, 89 | "skewX": 0, 90 | "skewY": 0, 91 | "x": 63, 92 | "y": 168, 93 | "fontSize": 25 94 | }, 95 | { 96 | "type": "image", 97 | "value": "https://image.yonghuivip.com/jimu/1/1638943345035942610", 98 | "elementName": "上传人", 99 | "id": 1003, 100 | "x": 19, 101 | "y": 436, 102 | "width": 400, 103 | "height": 400, 104 | "scaleX": 1, 105 | "scaleY": 1, 106 | "rotation": 0, 107 | "skewX": 0, 108 | "skewY": 0 109 | } 110 | ] 111 | ``` 112 | 113 | ![demo](https://s1.ax1x.com/2023/07/25/pCXJyW9.png) 114 | 115 | ### ItemProp 116 | 117 | ## API 118 | 119 | | API name | type | description | 120 | | ------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------- | 121 | | exportToImage | (fileName:string,options?:[ImageExportProps](#ImageExportProps))=>void | export image can be customized | 122 | | exportToBASE64 | ()=>void | export BASE64 of the canvas | 123 | | withdraw | ()=>void | withdraw action | 124 | | redo | ()=>void | redo action | 125 | | canvasScale | (ratio:number)=>void | zoom ratio of the canvas (ratio is (0,2.75]) | 126 | | deleteItem | ()=>void | delete selected item on canvas | 127 | | moveLayerLevel | `(i: number)=>void` | When i is less than 0, the selected layer will move to the next layer | 128 | | moveLayer | `(direction: 'right' \| 'left' \| 'top'\| 'bottom', delta: number)=>void` | move `delta` unit on canvas on specific `direction` | 129 | | setSelectedIndex | `(id:number)=>void` | Select the selected layers in order | 130 | | clearSelected | `()=>void` | Unselected layer | 131 | | toggleMultiSelected | `(state:boolean)=>void` | switch multi-selected mode | 132 | | toogleLock | `(index:number)=>void` | toggle the lock state of specific layer | 133 | | madeGroup | `(layers:Array\)=>void` | Group the selected layers | 134 | | divideGroup | `(groupId:string)=>void` | Ungroup by a specific groupId | 135 | 136 |

ImageExportProps

137 | 138 | | propName | type | required | description | 139 | | -------- | --------------------------- | -------- | ------------------------------------------------- | 140 | | scale | number[0.1-3] | - | Scale ratio of output image (default is 1) | 141 | | quality | number [0.1-1] | - | quality of export image(default is 1) | 142 | | fileType | 'image/png' \| 'image/jpeg' | - | filetype of export image(default is 'image/png' ) | 143 | 144 | ## Tips 145 | 146 | 1. How to switch on multi-select Mode? 147 | 148 | ```tsx 149 | const handler = useCallback((e: KeyboardEvent) => { 150 | if (e.keyCode === 16) { 151 | // 开启多选模式 152 | KonvaCanvas.toggleMultiSelected(true); 153 | store.setMultiSelected(true); 154 | } 155 | }, []); 156 | 157 | useEffect(() => { 158 | window.addEventListener('keydown', handler); 159 | return () => { 160 | window.removeEventListener('keydown', handler); 161 | }; 162 | }, []); 163 | ``` 164 | 165 | ## Timeline 166 | 167 | 23-2-20 支持多选图层,拆组和解组-可配合 toolkit 使用 168 | 169 | ## reference 170 | 171 | 1. [build reference](https://itnext.io/step-by-step-building-and-publishing-an-npm-typescript-package-44fe7164964c) 172 | 2. [react-konva](https://github.com/konvajs/react-konva) 173 | 174 | ## Todos 175 | 176 | | 序号 | 内容 | 完成情况 | 177 | | ---- | -------------- | -------- | 178 | | 1 | js 环境调通 | √ | 179 | | 2 | ts type 规范化 | delay | 180 | | 3 | ts 环境调通 | √ | 181 | | 4 | 依赖库配置 | delay | 182 | | 5 | 使用文档 | ing | 183 | | 6 | 线上示例 | - | 184 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import Editor from './src/index'; 2 | export type { 3 | itemType, IaddItem, IProps, 4 | IcommonInfo, IimageInfo, IShapeInfo, IgroupInfo, ItextInfo, 5 | IFunc, 6 | LayerIdType, 7 | Iinfo 8 | } from './src/type' 9 | 10 | 11 | export default Editor; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-konva-editor", 3 | "version": "0.2.0-r17", 4 | "author": "mytac", 5 | "description": "An image editor which is built by konva and react.", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "files": [ 10 | "dist/**/*" 11 | ], 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "cli": "node ./scripts/index.mjs", 15 | "build": "tsc -outDir ./dist && tsc -module commonjs -outDir ./lib", 16 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 17 | "lint": "eslint --fix --ext .js,.ts,jsx,tsx src", 18 | "dev:release": "./publish.sh -dev", 19 | "prod:release": "./publish.sh -prod", 20 | "npm-server": "verdaccio", 21 | "unrelease": "./unpublish.sh -dev", 22 | "prod:unrelease": "./unpublish.sh -prod", 23 | "type": " tsc --emitDeclarationOnly -outDir ./dist" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/mytac/react-konva-editor.git" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "keywords": [ 35 | "editor", 36 | "react", 37 | "konva", 38 | "react-component" 39 | ], 40 | "license": "ISC", 41 | "bugs": { 42 | "url": "https://github.com/mytac/react-konva-editor/issues" 43 | }, 44 | "homepage": "https://github.com/mytac/react-konva-editor#readme", 45 | "dependencies": { 46 | "lodash": "^4.17.21", 47 | "tinykeys": "^1.4.0", 48 | "use-image": "1.0.8" 49 | }, 50 | "devDependencies": { 51 | "eslint-config-react-app": "^6.0.0", 52 | "eslint-plugin-prettier": "^3.3.1", 53 | "@types/lodash": "^4.14.177", 54 | "@types/node": "^12.0.0", 55 | "@types/react": "^17.0.1", 56 | "eslint": "^7.11.0", 57 | "father": "4.1.8", 58 | "prettier": "^2.2.1", 59 | "react": "17.0.1", 60 | "react-konva": "17.0.2-0", 61 | "konva": "^8.2.3", 62 | "typescript": "^4.9.5" 63 | }, 64 | "eslintConfig": { 65 | "extends": [ 66 | "react-app", 67 | "react-app/jest" 68 | ] 69 | }, 70 | "peerDependencies": { 71 | "react": ">=17.0.0", 72 | "react-dom": ">=17.0.0", 73 | "react-konva": ">=17.0.2-0", 74 | "konva":">=8.2.3" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "执行的文件名:$0"; 3 | echo "第一个参数为:$1"; 4 | echo "第二个参数为:$2"; 5 | echo "第三个参数为:$3"; 6 | 7 | DEV_ENV="-dev" 8 | PROD_ENV="-prod" 9 | 10 | 11 | if test $1 = $DEV_ENV 12 | then 13 | REPO="http://localhost:4873/" 14 | elif test $1 = $PROD_ENV 15 | then 16 | token=$(cat ./.npm_token) 17 | echo "token=$token" 18 | REPO="https://registry.npmjs.org/" 19 | else 20 | echo "enviroment invalid" 21 | exit 8 22 | fi 23 | 24 | echo "REPO=$REPO" 25 | 26 | rm -rf ./dist && yarn build && cp {package.json,README.md} ./dist/ -r && yarn type 27 | echo "【copied!】" 28 | # npm unpublish react-konva-editor@0.0.2 --force --registry $REPO|| echo "【no need to unpublish】" 29 | # echo "【unpublished successfully!】" 30 | cd ./dist && npm publish --registry $REPO 31 | echo "【published!!】" 32 | 33 | echo $n press any key to exit: $c 34 | read name 35 | echo "$name" 36 | 37 | # 删除老版本 38 | # 发布新版本 -------------------------------------------------------------------------------- /src/KonvaGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from 'react'; 2 | import { noop } from 'lodash'; 3 | import { Group } from 'react-konva'; 4 | import { IShapeInfo } from './type'; 5 | 6 | const KonvaGroup: FC = ({ 7 | stageRef, 8 | myRef, 9 | setShowTransformer, 10 | handleSelected, 11 | handleInfo, 12 | onRef, 13 | banDrag, 14 | trRef, 15 | isSelected, 16 | fill, 17 | value, 18 | id, 19 | width = 100, 20 | height = 30, 21 | isNew = false, 22 | ...props 23 | }) => { 24 | // const ref=useRef 25 | // 选中态,显示transformer 26 | useEffect(() => { 27 | if (isSelected && trRef?.current) { 28 | trRef.current.nodes([myRef.current]); 29 | } 30 | }, [isSelected, trRef, myRef]); 31 | 32 | useEffect(() => { 33 | // 新增的组 34 | if (isNew) { 35 | const width = myRef.current.width; 36 | handleInfo({ width, isNew: false }); 37 | } 38 | // eslint-disable-next-line react-hooks/exhaustive-deps 39 | }, [myRef, isNew]); 40 | 41 | const commonProps = { 42 | id: String(id), 43 | ref: myRef, 44 | onClick: banDrag ? noop : handleSelected, 45 | ...props, 46 | }; 47 | 48 | const childrenWithProps = React.Children.map(props.children, (child) => { 49 | // Checking isValidElement is the safe way and avoids a 50 | // typescript error too. 51 | if (React.isValidElement(child)) { 52 | // @ts-ignore 53 | return React.cloneElement(child, { banDrag: true }); 54 | } 55 | return child; 56 | }); 57 | 58 | return {childrenWithProps}; 59 | }; 60 | 61 | export default KonvaGroup; 62 | -------------------------------------------------------------------------------- /src/KonvaImg.tsx: -------------------------------------------------------------------------------- 1 | import React,{ FC, useEffect } from 'react'; 2 | import { noop } from 'lodash'; 3 | import { Image } from 'react-konva'; 4 | import useImage from 'use-image'; 5 | import { AdaptStrategy } from './utils'; 6 | import { IimageInfo } from './type'; 7 | 8 | const KonvaImage: FC = ({ 9 | stageRef, 10 | myRef, 11 | setShowTransformer, 12 | handleSelected, 13 | handleInfo, 14 | onRef, 15 | value, 16 | banDrag, 17 | trRef, 18 | crop, 19 | isNew, 20 | isSelected, 21 | _isAdaptStage, 22 | _isProportionalScaling, 23 | _isChangedCrop, 24 | ...props 25 | }) => { 26 | const [image /* status */] = useImage(value, 'anonymous'); 27 | 28 | useEffect(() => { 29 | if (image && (isNew || _isChangedCrop) && _isAdaptStage && stageRef) { 30 | // 先适配舞台 31 | // 如果有crop x、y保持原图比例,crop的width和height为适配后的 32 | const item = AdaptStrategy.adaptNewImage( 33 | _isChangedCrop ? crop : image, 34 | stageRef.current, 35 | ); 36 | if (item) { 37 | const { width, height } = item; 38 | if (_isChangedCrop && width && height) { 39 | // @ts-ignore 40 | // item.crop = { ...cropXY, width, height }; 41 | item.crop = { ...crop }; 42 | } 43 | handleInfo({ 44 | ...item, 45 | width: Number(width), 46 | height: Number(height), 47 | scaleX: 1, 48 | scaleY: 1, 49 | _isChangedCrop: false, 50 | _isAdaptStage: false, 51 | _isProportionalScaling: false, 52 | }); 53 | trRef.current.nodes([myRef.current]); 54 | } 55 | } 56 | // eslint-disable-next-line react-hooks/exhaustive-deps 57 | }, [isNew, _isAdaptStage, image, stageRef, trRef, myRef]); 58 | 59 | useEffect(() => { 60 | if (onRef) { 61 | onRef(myRef); 62 | } 63 | // eslint-disable-next-line react-hooks/exhaustive-deps 64 | }, [myRef]); 65 | 66 | useEffect(() => { 67 | const prevLayer = trRef?.current; 68 | const { height, width, scaleX = 1, scaleY = 1 } = props; 69 | const prevW = prevLayer.getWidth(); 70 | const prevH = prevLayer.getHeight(); 71 | 72 | let oldSize; 73 | if (width && height) { 74 | // console.log('use 2', width, height); 75 | oldSize = { 76 | width: width * scaleX, 77 | height: height * scaleY, 78 | }; 79 | } else if (!isNaN(prevW)) { 80 | // console.log('use 1', prevW, prevH); 81 | 82 | oldSize = { 83 | width: prevW, 84 | height: prevH, 85 | }; 86 | } else return; 87 | // console.log('oldSize', oldSize); 88 | 89 | // 直接替换,等比缩放 90 | if (isSelected && _isProportionalScaling && oldSize) { 91 | /// adapt image-replacement 92 | const _isP = // 是否等比缩放 93 | myRef?.current?.attrs?._isProportionalScaling || _isProportionalScaling; 94 | 95 | if (_isP && image && oldSize) { 96 | // 替换模式 97 | const item = AdaptStrategy.adaptReplaceImage(image, oldSize); 98 | if (item) { 99 | handleInfo({ 100 | ...item, 101 | scaleX: 1, 102 | scaleY: 1, 103 | _isProportionalScaling: 0, 104 | }); 105 | } 106 | } 107 | trRef.current.nodes([myRef.current]); 108 | } 109 | 110 | // eslint-disable-next-line react-hooks/exhaustive-deps 111 | }, [ 112 | image, 113 | myRef, 114 | isNew, 115 | trRef.current, 116 | isSelected, 117 | _isProportionalScaling, 118 | props.width, 119 | props.height, 120 | ]); 121 | 122 | useEffect(() => { 123 | if (trRef && trRef.current) { 124 | trRef.current.nodes([myRef.current]); 125 | } 126 | }, [image, trRef, myRef]); 127 | 128 | const commonParams: any = {}; 129 | if (crop) { 130 | commonParams.crop = crop; 131 | } 132 | 133 | return ( 134 | 143 | ); 144 | }; 145 | 146 | export default KonvaImage; 147 | -------------------------------------------------------------------------------- /src/KonvaShape.tsx: -------------------------------------------------------------------------------- 1 | import React,{ FC, useEffect } from 'react'; 2 | import { noop } from 'lodash'; 3 | import { Rect, Circle, Arc, Star, Arrow, Ellipse } from 'react-konva'; 4 | import { IShapeInfo } from './type'; 5 | 6 | const KonvaShape: FC = ({ 7 | stageRef, 8 | myRef, 9 | setShowTransformer, 10 | handleSelected, 11 | handleInfo, 12 | onRef, 13 | banDrag, 14 | trRef, 15 | isSelected, 16 | fill, 17 | value, 18 | id, 19 | width = 100, 20 | height = 30, 21 | ...props 22 | }) => { 23 | // 选中态,显示transformer 24 | useEffect(() => { 25 | if (isSelected && trRef?.current) { 26 | trRef.current.nodes([myRef.current]); 27 | } 28 | }, [isSelected, trRef, myRef]); 29 | 30 | const commonProps = { 31 | key: String(id), 32 | id: String(id), 33 | ref: myRef, 34 | onClick: banDrag ? noop : handleSelected, 35 | ...props, 36 | }; 37 | 38 | if (value === 'rect') { 39 | return ( 40 | //@ts-ignore 41 | 42 | ); 43 | } 44 | 45 | if (value === 'circle') { 46 | const { radius } = props; 47 | return ; 48 | } 49 | 50 | if (value === 'arc') { 51 | const { innerRadius = 100, outerRadius = 60, angle = 180 } = props; 52 | 53 | return ( 54 | 61 | ); 62 | } 63 | 64 | if (value === 'star') { 65 | const { innerRadius = 50, outerRadius = 100, numPoints = 5 } = props; 66 | return ( 67 | 74 | ); 75 | } 76 | 77 | if (value === 'arrow') { 78 | const { points = [0, 0, 50, 50] } = props; 79 | return ( 80 | 88 | ); 89 | } 90 | 91 | if (value === 'ellipse') { 92 | const { ellipseRadius = { radiusX: 40, radiusY: 20 } } = props; 93 | return ; 94 | } 95 | 96 | return null; 97 | }; 98 | 99 | export default KonvaShape; 100 | -------------------------------------------------------------------------------- /src/KonvaText.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useEffect } from 'react'; 2 | import { Text } from 'react-konva'; 3 | import { SelectChangeListener } from './utils/textHandler'; 4 | import { ItextInfo } from './type'; 5 | 6 | const KonvaText: FC = ({ 7 | stageRef, 8 | myRef, 9 | setShowTransformer, 10 | handleSelected, 11 | handleInfo, 12 | onRef, 13 | isNew, 14 | stageScale, 15 | // @ts-ignore 16 | trRef, 17 | ...props 18 | }) => { 19 | const [showText, setShowText] = useState(true); 20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 21 | const [gradient, setGradient] = useState({}); 22 | 23 | const changeGardient = ( 24 | allWidth: number, 25 | a: number, 26 | b: number, 27 | total: number, 28 | ) => { 29 | console.log('allWidth', allWidth); 30 | // const EveryWidth = allWidth / total; 31 | const targetColor = 'yellow'; 32 | const originColor = props.color || '#000'; 33 | const start = a >= b ? b : a; 34 | const end = a > b ? a : b; 35 | const split = [start / total, end / total]; 36 | const obj = { 37 | fill: 'red', 38 | // fillLinearGradientStartPoint: { x: 0, y: 0 }, 39 | fillLinearGradientStartPoint: { 40 | x: 0, 41 | // x: start * EveryWidth * stageScale, 42 | y: 0, 43 | }, 44 | // fillLinearGradientEndPoint: { x: end * EveryWidth * stageScale, y: 0 }, 45 | fillLinearGradientEndPoint: { x: allWidth, y: 0 }, 46 | fillPatternRepeat: 'repeat-x', 47 | // fillLinearGradientEndPoint: { x: 100, y: 0 }, 48 | fillLinearGradientColorStops: [ 49 | 0, 50 | originColor, 51 | split[0], 52 | originColor, 53 | split[0], 54 | targetColor, 55 | split[1], 56 | targetColor, 57 | split[1], 58 | originColor, 59 | 1, 60 | originColor, 61 | ], 62 | }; 63 | console.log('obj', obj); 64 | setGradient(obj); 65 | }; 66 | 67 | const onDblClick = (e: any) => { 68 | const textNode = myRef.current; 69 | const originalText = textNode.text(); 70 | if (!textNode) return; 71 | 72 | const transformerBoxAttr = trRef.current.children?.[0].attrs; 73 | 74 | const stageBox = stageRef.current.container().getBoundingClientRect(); 75 | const areaPosition = { 76 | x: stageBox.x + (props.x || 0) * stageScale, 77 | y: stageBox.y + (props.y || 0) * stageScale, 78 | }; 79 | 80 | setShowText(false); 81 | const textarea = document.createElement('textarea'); 82 | setShowTransformer(false); 83 | document.body.appendChild(textarea); 84 | 85 | textarea.value = originalText; 86 | const scaleX = textNode.attrs.scaleX || 1; 87 | const originFontSize = props.fontSize || 40; 88 | const transformedFontSize = originFontSize * scaleX * stageScale; 89 | 90 | textarea.style.position = 'fixed'; 91 | textarea.style.top = areaPosition.y + 'px'; 92 | textarea.style.left = areaPosition.x + 'px'; 93 | const realWidth = 94 | transformerBoxAttr.width * stageScale - textNode.padding() * 2; 95 | const realHeight = 96 | transformerBoxAttr.height * stageScale - textNode.padding() * 2 + 'px'; 97 | textarea.style.width = realWidth + 'px'; 98 | textarea.style.height = realHeight; 99 | 100 | const selectChanger = new SelectChangeListener( 101 | textarea, 102 | originalText, 103 | changeGardient, 104 | realWidth, 105 | ); 106 | //@ts-ignore 107 | textarea.style.fontSize = transformedFontSize + 'px'; 108 | textarea.style.border = 'none'; 109 | textarea.style.padding = '0px'; 110 | textarea.style.margin = '0px'; 111 | textarea.style.overflow = 'hidden'; 112 | textarea.style.background = 'none'; 113 | // textarea.style.whiteSpace = 'nowrap'; 114 | 115 | textarea.style.outline = 'none'; 116 | textarea.style.zIndex = '100'; 117 | textarea.style.resize = 'none'; 118 | textarea.style.lineHeight = textNode.lineHeight(); 119 | textarea.style.fontFamily = textNode.fontFamily(); 120 | const textAreaFontStyle = textNode.fontStyle(); 121 | const textDecoration = textNode.textDecoration(); 122 | if (textAreaFontStyle.includes('italic')) { 123 | textarea.style.fontStyle = 'italic'; 124 | } 125 | if (textAreaFontStyle.includes('bold')) { 126 | textarea.style.fontWeight = 'bold'; 127 | } 128 | 129 | if (textDecoration.includes('underline')) { 130 | textarea.style.textDecoration = 'underline'; 131 | } 132 | textarea.style.transformOrigin = 'left top'; 133 | textarea.style.textAlign = textNode.align(); 134 | textarea.style.color = textNode.fill(); 135 | selectChanger.listen(); 136 | let rotation = textNode.rotation(); 137 | let transform = ''; 138 | if (rotation) { 139 | transform += 'rotateZ(' + rotation + 'deg) '; 140 | } 141 | 142 | let px = 0; 143 | // also we need to slightly move textarea on firefox 144 | // because it jumps a bit 145 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; 146 | if (isFirefox) { 147 | px += 2 + Math.round(textNode.fontSize() / 20); 148 | } 149 | transform += 'translateY(-' + px + 'px) '; 150 | textarea.style.transform = transform; 151 | // reset height 152 | // textarea.style.height = 'auto'; 153 | // after browsers resized it we can set actual value 154 | // textarea.style.height = textarea.scrollHeight + 3 + 'px'; 155 | 156 | textarea.focus(); 157 | 158 | function removeTextarea() { 159 | try { 160 | if (textarea) { 161 | document.body.removeChild(textarea); 162 | setShowText(true); 163 | selectChanger.destory(); 164 | } 165 | } catch (err) { 166 | console.log(err); 167 | } 168 | } 169 | 170 | function setTextareaWidth(newWidth: number) { 171 | try { 172 | if (!newWidth) { 173 | // set width for placeholder 174 | newWidth = textNode.placeholder.length * transformedFontSize; 175 | } 176 | // some extra fixes on different browsers 177 | const isSafari = /^((?!chrome|android).)*safari/i.test( 178 | navigator.userAgent, 179 | ); 180 | const isFirefox = 181 | navigator.userAgent.toLowerCase().indexOf('firefox') > -1; 182 | if (isSafari || isFirefox) { 183 | newWidth = Math.ceil(newWidth); 184 | } 185 | // @ts-ignore 186 | const isEdge = 187 | // @ts-ignore 188 | document.documentMode || /Edge/.test(navigator.userAgent); 189 | if (isEdge) { 190 | newWidth += 1; 191 | } 192 | textarea.style.width = newWidth + 'px'; 193 | } catch (err) { 194 | console.log('err in konvatext', err); 195 | } 196 | } 197 | 198 | textarea.addEventListener('keydown', function (e: any) { 199 | if (setShowTransformer) { 200 | setShowTransformer(false); 201 | } 202 | if (e.keyCode === 27) { 203 | removeTextarea(); 204 | } 205 | 206 | const scale = textNode.getAbsoluteScale().x; 207 | textNode.text(textarea.value); 208 | handleInfo({ value: textarea.value }); 209 | setTextareaWidth(textNode.width() * scale); 210 | textarea.style.height = 'auto'; 211 | textarea.style.height = 212 | textarea.scrollHeight + transformedFontSize + 'px'; 213 | e.stopPropagation(); 214 | }); 215 | 216 | textarea.addEventListener('blur', function () { 217 | setShowTransformer(true); 218 | handleInfo({ value: textarea.value }); 219 | 220 | removeTextarea(); 221 | }); 222 | }; 223 | 224 | useEffect(() => { 225 | if (myRef && myRef.current) { 226 | const el = myRef.current; 227 | if (showText) { 228 | el.show(); 229 | } else { 230 | el.hide(); 231 | } 232 | } 233 | }, [myRef, showText]); 234 | 235 | // 不知道产品要不要,先保留吧,产品与设计有argue,这段代码会造成文字闪现,从x=0,y=0,跳到当前位置,体验不好 236 | // useEffect(() => { 237 | // if (isNew && stageRef && myRef) { 238 | // // 如果是新增元素,需要将元素置于画布中央 239 | // const stage = stageRef.current; 240 | // const textNode = myRef.current; 241 | 242 | // const stageW = stage.getWidth(); 243 | // const stageH = stage.getHeight(); 244 | 245 | // const textW = textNode.getWidth(); 246 | // const textH = textNode.getHeight(); 247 | 248 | // const areaPosition = { 249 | // x: (stageW - textW) / 2, 250 | // y: (stageH - textH) / 2, 251 | // }; 252 | 253 | // handleInfo(areaPosition); 254 | // } 255 | // }, [isNew, stageRef, myRef, handleInfo]); 256 | 257 | useEffect(() => { 258 | if (onRef) { 259 | onRef(myRef); 260 | } 261 | // eslint-disable-next-line react-hooks/exhaustive-deps 262 | }, []); 263 | 264 | // useEffect(() => { 265 | // handleInfo({ gradient }); 266 | // // eslint-dissable-next-line react-hooks/exhaustive-deps 267 | // }, [gradient]); 268 | 269 | return ( 270 | <> 271 | 293 | 294 | ); 295 | }; 296 | 297 | export default KonvaText; 298 | -------------------------------------------------------------------------------- /src/hoc/withTransform.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState, useRef, useCallback } from 'react'; 2 | import Konva from 'konva'; 3 | import { Transformer } from 'react-konva'; 4 | import handleKonvaItem from '../utils/handleKonvaItem'; 5 | 6 | const withTransform = (Component: FC) => { 7 | const Inner = (props: { 8 | isSelected: boolean; 9 | handleInfo: (a: any) => void; 10 | handleSelected: (ref: any) => void; 11 | rotation?: number; 12 | opacity?: number; 13 | banDrag?: boolean; 14 | resizeEnabled?: boolean; 15 | type?: 'text' | 'image' | 'group'; 16 | }) => { 17 | const { 18 | isSelected = false, 19 | handleInfo = () => {}, 20 | opacity = 1, 21 | handleSelected, 22 | resizeEnabled = true, 23 | banDrag, 24 | } = props; 25 | 26 | const [showTransformer, setShowTransformer] = useState(true); 27 | 28 | const eleRef = useRef(null); 29 | const trRef = useRef(null); 30 | 31 | useEffect(() => { 32 | const transformer = trRef.current; 33 | if (isSelected && eleRef?.current && transformer) { 34 | if (props?.type === 'text') { 35 | // @ts-ignore 36 | transformer.nodes([eleRef.current]); 37 | transformer.getLayer()?.batchDraw(); 38 | } 39 | } 40 | // eslint-disable-next-line react-hooks/exhaustive-deps 41 | }, [isSelected]); 42 | 43 | const handleDragStart = useCallback(() => { 44 | handleSelected(eleRef); 45 | }, [handleSelected]); 46 | 47 | const handleDragEnd = (e: any) => { 48 | const info = handleKonvaItem(e.target); 49 | handleInfo(info); 50 | }; 51 | 52 | useEffect(() => { 53 | if (trRef) { 54 | const tr = trRef.current; 55 | if (isSelected && showTransformer) { 56 | // @ts-ignore 57 | tr.show(); 58 | // @ts-ignore 59 | tr.forceUpdate(); 60 | } else { 61 | // @ts-ignore 62 | tr.hide(); 63 | } 64 | } 65 | }, [isSelected, showTransformer, trRef]); 66 | 67 | const boundBoxFunc = (oldBox: any, newBox: any) => { 68 | if (newBox.width < 5 || newBox.height < 5) { 69 | return oldBox; 70 | } 71 | return newBox; 72 | }; 73 | 74 | return ( 75 | <> 76 | 94 | 95 | { 102 | handleInfo(handleKonvaItem(a.target)); 103 | }} 104 | /> 105 | 106 | ); 107 | }; 108 | return Inner; 109 | }; 110 | 111 | export default withTransform; 112 | -------------------------------------------------------------------------------- /src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | export default function usePrevious(value: any) { 4 | const ref = useRef(); 5 | useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | } 10 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useRef, useEffect, useCallback } from 'react'; 2 | import { Stage, Layer, Rect } from 'react-konva'; 3 | import Konva from 'konva'; 4 | import { 5 | IProps, 6 | Iinfo, 7 | IaddItem, 8 | IFunc, 9 | IcommonInfo, 10 | IgroupInfo, 11 | LayerIdType, 12 | } from './type'; 13 | import withTransform from './hoc/withTransform'; 14 | import MyImage from './KonvaImg'; 15 | import MyText from './KonvaText'; 16 | import MyShape from './KonvaShape'; 17 | import MyGroup from './KonvaGroup'; 18 | import { handleDuplicateId, downloadURI, isSelectedId } from './utils/utils'; 19 | import circularQueue from './utils/circularQueue'; 20 | import KeyboardListener from './keyboardListener'; 21 | import { isArray, isNumber } from 'lodash'; 22 | 23 | // @ts-ignore 24 | const KonvaGroup = withTransform(MyGroup); 25 | // @ts-ignore 26 | const KonvaImage = withTransform(MyImage); 27 | // @ts-ignore 28 | const KonvaText = withTransform(MyText); 29 | // @ts-ignore 30 | const KonvaShape = withTransform(MyShape); 31 | const hotkeyListener = new KeyboardListener(); 32 | let stepCached: circularQueue | undefined; 33 | 34 | interface ShapePropsNApi extends FC, IFunc {} 35 | 36 | const outerInstance: any = { 37 | value: {}, 38 | attach(k: string, v: any) { 39 | this.value[k] = v; 40 | }, 41 | clean() { 42 | this.value = {}; 43 | }, 44 | }; 45 | 46 | const Core: ShapePropsNApi = ({ 47 | width, 48 | height, 49 | backgroundColor = '#fff', 50 | backgroundStyle = {}, 51 | addItem, 52 | selectedItemChange, 53 | maxStep = 10, // 撤销重做保存的最大步数 54 | setRedo = () => {}, 55 | setWithdraw = () => {}, 56 | onChangeSelected = () => {}, 57 | bindRef = () => {}, 58 | stepInfo = [], 59 | onChangeStep = () => {}, 60 | }) => { 61 | const stageRef = useRef(null); 62 | const outRef = useRef(null); 63 | const [newId, setNewId] = useState(-2); 64 | const [selectedId, setSelected] = useState>(0); 65 | const [steps, setSteps] = useState([]); 66 | const [stageScale, setStageScale] = useState(0.7); 67 | const [multiSelected, setMultiSelected] = useState(false); 68 | 69 | // 添加新元素时 70 | const onAdd = (item: IaddItem) => { 71 | if (stepCached) { 72 | const currentItem = stepCached.getCurrent(); 73 | if (currentItem) { 74 | const infos: Iinfo[] = currentItem; 75 | const newItem = { ...item, id: 0 }; 76 | const maxId = infos.reduce( 77 | (prev, info) => 78 | isNumber(info.id) ? Math.max(Number(info.id), prev) : prev + 100, 79 | 0 80 | ); 81 | const newId = isNaN(maxId) || !maxId ? new Date().getTime() : maxId + 1; 82 | setNewId(newId); 83 | setSelected(newId); 84 | newItem.id = newId; 85 | const list = [...infos, newItem]; 86 | stepCached.enqueue(list); 87 | const currentInfo = stepCached.getCurrent(); 88 | 89 | setSteps(currentInfo); 90 | } 91 | } 92 | }; 93 | 94 | // 当元素进行改变时 95 | const handleInfo = useCallback((index: number, item: object) => { 96 | if (stepCached) { 97 | const infos: Iinfo[] = stepCached.getCurrent(); 98 | const current = stepCached.getCurrent()[index]; 99 | const newInfo = { ...current, ...item }; 100 | const ins = [...infos]; 101 | ins[index] = newInfo; 102 | stepCached.enqueue(ins); 103 | setSteps(stepCached.getCurrent()); 104 | } 105 | }, []); 106 | 107 | // 通过id改变图层属性 108 | const changeLayerInfoById = useCallback( 109 | (id: LayerIdType, item: object) => { 110 | const index = steps.findIndex((layer: Iinfo) => layer.id === id); 111 | if (~index) { 112 | handleInfo(index, item); 113 | } 114 | }, 115 | // eslint-disable-next-line react-hooks/exhaustive-deps 116 | [selectedId] 117 | ); 118 | 119 | useEffect(() => { 120 | if (addItem) { 121 | onAdd(addItem); 122 | } 123 | // eslint-disable-next-line react-hooks/exhaustive-deps 124 | }, [addItem]); 125 | 126 | useEffect(() => { 127 | if ( 128 | // 当改变选择元素的参数 129 | selectedItemChange && 130 | Object.keys(selectedItemChange).length && 131 | selectedId 132 | ) { 133 | handleSelectItem((item: IcommonInfo | undefined, index = -1) => { 134 | if (item && item.type === 'stage') { 135 | return; 136 | } 137 | // 当改变的为item时,更新图层信息 138 | 139 | if (stepCached) { 140 | const infos = stepCached.getCurrent(); 141 | if (Array.isArray(selectedItemChange)) { 142 | // 多选图层批量更新属性 143 | // 多选图层 144 | const needUpdateLayers = [...selectedItemChange]; 145 | const infos = [...stepCached.getCurrent()]; 146 | for (let i = 0; i < needUpdateLayers.length; i++) { 147 | const index = infos.findIndex( 148 | (layer: Iinfo) => layer.id === needUpdateLayers[i]?.id 149 | ); 150 | infos[index] = needUpdateLayers[i]; 151 | } 152 | stepCached.enqueue(infos); 153 | } else { 154 | // 单选图层,改变属性 155 | const properties = { 156 | ...item, 157 | ...selectedItemChange, 158 | }; 159 | 160 | if (properties._ignore === true) { 161 | // 不入队,替换当前指针所指元素,并清空current之后的队内元素 162 | delete properties._ignore; 163 | const ins = [...infos]; 164 | ins[index] = properties; 165 | //@ts-ignore 166 | stepCached.list[stepCached.current] = ins; 167 | stepCached.clearAfterCurrent(); 168 | stepCached.enqueue(ins); 169 | } else { 170 | const newInfos = [...infos]; 171 | newInfos[index] = properties; 172 | stepCached.enqueue(newInfos); 173 | } 174 | } 175 | 176 | const newSteps = stepCached.getCurrent(); 177 | console.log('newSteps', newSteps); 178 | setSteps(newSteps); 179 | // bindRef(ref) 180 | } 181 | }); 182 | } 183 | // eslint-disable-next-line react-hooks/exhaustive-deps 184 | }, [selectedItemChange]); // 更改选中元素的属性 185 | 186 | const handleSelectItem = useCallback( 187 | (cb = (a: Iinfo | undefined) => {}) => { 188 | if (selectedId === -1) { 189 | // stage 190 | const item = { type: 'stage' }; 191 | cb(item); 192 | } else if (Array.isArray(selectedId)) { 193 | // 多选形态 194 | const indexes = []; 195 | const items = []; 196 | for (let i = 0; i < steps.length; i++) { 197 | const current: Iinfo = steps[i] 198 | // @ts-ignore 199 | if (selectedId.includes(current.id)) { 200 | // @ts-ignore 201 | indexes.push(current.id); 202 | // @ts-ignore 203 | items.push(current); 204 | } 205 | } 206 | cb(items); 207 | } else { 208 | if (Array.isArray(steps)) { 209 | const idx = steps.findIndex((i: any) => i?.id === selectedId); 210 | if (idx > -1) { 211 | const item = steps[idx]; 212 | cb(item, idx); 213 | } 214 | } else { 215 | cb(undefined); 216 | } 217 | } 218 | }, 219 | [selectedId, steps] 220 | ); 221 | 222 | const onRef = useCallback( 223 | (ref: any) => { 224 | if (ref) { 225 | handleSelectItem((item: IcommonInfo | undefined, index = 0) => { 226 | if (item && item.type === 'stage') { 227 | return; 228 | } 229 | const ele = ref.current; 230 | // const initInfo = handleKonvaItem(ele); 231 | // const newInfo = { ...item, ...initInfo }; 232 | // const newStep = [...steps]; 233 | // newStep[newStep.length - 1] = newInfo; 234 | // setSteps(newStep); 235 | // @ts-ignore 236 | // stepCached.list[stepCached.current] = newStep; 237 | bindRef(ele); // 把当前选定的元素的ref传给上层 238 | }); 239 | } 240 | }, 241 | // eslint-disable-next-line react-hooks/exhaustive-deps 242 | [bindRef, handleSelectItem, steps] 243 | ); 244 | 245 | useEffect(() => { 246 | if (!(stepCached instanceof circularQueue)) { 247 | // 初始化 248 | stepCached = new circularQueue(maxStep); 249 | if (stepInfo?.length) { 250 | //@ts-ignore 251 | stepCached.list[0] = handleDuplicateId(stepInfo); 252 | } 253 | setSteps(stepCached.getCurrent()); 254 | setWithdraw(false); 255 | setRedo(false); 256 | } else { 257 | if (stepInfo && stepInfo.length) { 258 | //@ts-ignore 259 | stepCached.list[stepCached.current] = handleDuplicateId(stepInfo); 260 | const res = stepCached.getCurrent(); 261 | setSteps(res); 262 | setWithdraw(false); 263 | setRedo(false); 264 | } 265 | } 266 | // window.addEventListener('keypress', dragListener); 267 | return () => { 268 | // window.removeEventListener('keypress', dragListener); 269 | }; 270 | // eslint-disable-next-line react-hooks/exhaustive-deps 271 | }, [stepInfo]); 272 | 273 | useEffect(() => { 274 | const { current } = outRef; 275 | hotkeyListener.init(Core); 276 | hotkeyListener.listening(current); 277 | return () => { 278 | stepCached = undefined; 279 | hotkeyListener.destory(current); 280 | }; 281 | }, []); 282 | 283 | useEffect(() => { 284 | if (stepCached) { 285 | setRedo(stepCached.canMoveForward); 286 | setWithdraw(stepCached.canMoveBack); 287 | handleSelectItem((item: IcommonInfo | Array | undefined) => { 288 | if (Array.isArray(item) || (item && item.type !== 'stage')) { 289 | onChangeSelected(item); 290 | } 291 | }); 292 | } 293 | onChangeStep(steps); 294 | 295 | // eslint-disable-next-line react-hooks/exhaustive-deps 296 | }, [steps]); 297 | 298 | useEffect(() => { 299 | handleSelectItem((item: Iinfo | Array | undefined) => { 300 | if (item) { 301 | onChangeSelected(item); 302 | } 303 | }); 304 | 305 | // eslint-disable-next-line react-hooks/exhaustive-deps 306 | }, [selectedId]); 307 | 308 | const onClickStage = (e: any) => { 309 | setSelected(-1); 310 | }; 311 | 312 | const handleSelected = (id: LayerIdType, ref: any) => { 313 | if (multiSelected) { 314 | // 多选 315 | if (isArray(selectedId)) { 316 | // 已开启多选模式下的 selectedId 317 | setSelected([...selectedId, id]); 318 | } else { 319 | setSelected([selectedId, id]); 320 | } 321 | } else { 322 | setSelected(id); 323 | } 324 | if (ref && ref.current) { 325 | bindRef(ref.current); 326 | } 327 | }; 328 | 329 | const copyItem = useCallback(() => { 330 | handleSelectItem((item: any, index = 0) => { 331 | if (Array.isArray(item)) { 332 | console.warn('暂不支持多选复制'); 333 | return; 334 | } 335 | if (item && item.type === 'stage') { 336 | return; 337 | } 338 | let coordinate: any = { x: 0, y: 0 }; 339 | const { x = 0, y = 0 } = item; 340 | coordinate = { 341 | x: x + 10, 342 | y: y + 10, 343 | }; 344 | const copyItem = { ...item, ...coordinate, banDrag: 0, _isAdaptStage: 0 }; 345 | onAdd(copyItem); 346 | }); 347 | // eslint-disable-next-line react-hooks/exhaustive-deps 348 | }, [selectedId, steps]); 349 | 350 | const deleteItem = useCallback(() => { 351 | handleSelectItem((item: any, index = -1) => { 352 | if (item) { 353 | if (Array.isArray(item)) { 354 | console.warn('暂不支持多选删除'); 355 | return; 356 | } 357 | const info = [...steps]; 358 | if (index > -1 && stepCached && !item.banDrag) { 359 | info.splice(index, 1); 360 | stepCached.enqueue(info); 361 | setSteps(stepCached.getCurrent()); 362 | setSelected(-1); // 删除一个就指向舞台 363 | onChangeSelected({}); 364 | } 365 | } 366 | }); 367 | // eslint-disable-next-line react-hooks/exhaustive-deps 368 | }, [selectedId, steps]); 369 | 370 | // 上下左右微调图层 371 | const moveLayer = useCallback( 372 | (direction: string, delta: number) => { 373 | if (!direction || !delta) { 374 | return; 375 | } 376 | 377 | const current = [...steps]; 378 | let isChanged = false; 379 | 380 | handleSelectItem((item: any, currentLayerIndex = -1) => { 381 | if (item && item.type === 'stage') { 382 | return; 383 | } 384 | if (Array.isArray(item)) { 385 | console.warn('暂不支持多选移动'); 386 | return; 387 | } 388 | if (currentLayerIndex >= 0) { 389 | const info = current[currentLayerIndex]; 390 | const oldValue = info[direction] || 0; 391 | const patch = { 392 | [direction]: oldValue + delta, 393 | }; 394 | current[currentLayerIndex] = { 395 | ...info, 396 | ...patch, 397 | }; 398 | isChanged = true; 399 | } 400 | if (isChanged && stepCached) { 401 | stepCached.enqueue(current); 402 | setSteps(stepCached.getCurrent()); 403 | } 404 | }); 405 | }, 406 | // eslint-disable-next-line react-hooks/exhaustive-deps 407 | [selectedId, steps] 408 | ); 409 | 410 | // 全量更新 411 | // const updateAllLayers = (newLayers:Array) => { 412 | // setSteps(newLayers) 413 | // } 414 | 415 | // 移动图层层级 416 | const moveLayerLevel = useCallback( 417 | (i: number) => { 418 | const current = [...steps]; 419 | let isChanged = false; 420 | handleSelectItem((item: any, currentLayerIndex = -1) => { 421 | if (item && item.type === 'stage') { 422 | return; 423 | } 424 | if (Array.isArray(item)) { 425 | console.warn('暂不支持多选移动'); 426 | return; 427 | } 428 | if (currentLayerIndex >= 0) { 429 | const tmp = item; 430 | if (i > 0) { 431 | if (currentLayerIndex < current.length - 1) { 432 | current[currentLayerIndex] = current[currentLayerIndex + 1]; 433 | current[currentLayerIndex + 1] = tmp; 434 | isChanged = true; 435 | } 436 | } else if (i < 0) { 437 | if (currentLayerIndex > 0) { 438 | current[currentLayerIndex] = current[currentLayerIndex - 1]; 439 | current[currentLayerIndex - 1] = tmp; 440 | isChanged = true; 441 | } 442 | } 443 | } 444 | if (isChanged && stepCached) { 445 | stepCached.enqueue(current); 446 | setSteps(stepCached.getCurrent()); 447 | } 448 | }); 449 | }, 450 | [handleSelectItem, steps] 451 | ); 452 | 453 | // 成组 454 | const madeGroup = useCallback( 455 | (layers: any) => { 456 | const infos:any = [...steps]; 457 | if (Array.isArray(layers) && stepCached) { 458 | // 拿到最大索引,最终group所属层级为最高层 459 | const maxIndex = layers.reduce( 460 | (cur, _, index) => Math.max(cur, index), 461 | 0 462 | ); 463 | const newId = new Date().getTime(); 464 | // 删除索引 465 | const group = { 466 | type: 'group', 467 | elements: [...layers], 468 | id: newId, 469 | isNew: true, 470 | }; 471 | infos.splice(maxIndex + 1, 0, group); 472 | // 删除原图层 473 | layers.forEach((layer) => { 474 | const { id } = layer; 475 | const index = infos.findIndex((oldLayer) => oldLayer.id === id); 476 | if (~index) { 477 | infos.splice(index, 1); 478 | } 479 | }); 480 | 481 | stepCached.enqueue(infos); 482 | setSteps(stepCached.getCurrent()); 483 | setSelected(newId); 484 | console.log('stepCached.getCurrent()', stepCached.getCurrent()); 485 | } 486 | }, 487 | [steps] 488 | ); 489 | 490 | // 拆组 491 | const divideGroup = useCallback( 492 | (groupId: string) => { 493 | if (stepCached) { 494 | const infos = [...steps]; 495 | const index = infos.findIndex((layer) => layer.id === groupId); 496 | if (~index) { 497 | const group = infos[index]; 498 | if ((group as IgroupInfo)?.elements?.length) { 499 | const { elements } = group as IgroupInfo; 500 | infos.splice(index, 1, ...elements); 501 | stepCached.enqueue(infos); 502 | setSteps(stepCached.getCurrent()); 503 | // @ts-ignore 504 | setSelected(elements[0].id); 505 | console.log('stepCached.getCurrent()', stepCached.getCurrent()); 506 | } 507 | } 508 | } 509 | }, 510 | [steps] 511 | ); 512 | 513 | useEffect(() => { 514 | outerInstance.attach('stageRef', stageRef); 515 | outerInstance.attach('setSelected', setSelected); 516 | outerInstance.attach('setStageScale', setStageScale); 517 | outerInstance.attach('setSteps', setSteps); 518 | outerInstance.attach('toggleMultiSelected', setMultiSelected); 519 | }, []); 520 | 521 | useEffect(() => { 522 | outerInstance.attach('deleteItem', deleteItem); 523 | }, [deleteItem]); 524 | 525 | useEffect(() => { 526 | outerInstance.attach('copyItem', copyItem); 527 | }, [copyItem]); 528 | 529 | useEffect(() => { 530 | outerInstance.attach('moveLayerLevel', moveLayerLevel); 531 | }, [moveLayerLevel]); 532 | 533 | useEffect(() => { 534 | outerInstance.attach('madeGroup', madeGroup); 535 | }, [madeGroup]); 536 | 537 | useEffect(() => { 538 | outerInstance.attach('divideGroup', divideGroup); 539 | }, [divideGroup]); 540 | 541 | useEffect(() => { 542 | outerInstance.attach('moveLayer', moveLayer); 543 | }, [moveLayer]); 544 | 545 | useEffect(() => { 546 | outerInstance.attach('changeLayerInfoById', changeLayerInfoById); 547 | }, [changeLayerInfoById]); 548 | 549 | const renderGroup = (info: Iinfo, idx: number, inGroup: boolean = false) => { 550 | const { type } = info; 551 | if (type === 'group' && (info as IgroupInfo).elements) { 552 | return ( 553 | // @ts-ignore 554 | 567 | {(info as IgroupInfo)?.elements.map((i: Iinfo, iidx: number) => 568 | renderGroup(i, idx, true) 569 | )} 570 | 571 | ); 572 | } 573 | if (type === 'image') { 574 | return ( 575 | 590 | ); 591 | } 592 | 593 | if (type === 'text') { 594 | return ( 595 | 611 | ); 612 | } 613 | 614 | if (type === 'shape') { 615 | return ( 616 | 632 | ); 633 | } 634 | return null; 635 | }; 636 | 637 | return ( 638 |
647 | 653 | 654 | 662 | 663 | 664 | {steps && 665 | steps.map((info: Iinfo, idx: number) => 666 | info ? ( 667 | info.type === 'group' ? ( 668 | // @ts-ignore 669 | 684 | {/* @ts-ignore */} 685 | {info?.elements?.map((i: Iinfo, iidx: number) => 686 | renderGroup(i, idx, true) 687 | )} 688 | 689 | ) : ( 690 | renderGroup(info, idx) 691 | ) 692 | ) : null 693 | )} 694 | 695 | 696 |
697 | ); 698 | }; 699 | 700 | // 输出并下载图片 701 | Core.exportToImage = ( 702 | filename = 'stage.jpg', 703 | options: { scale?: number; quality?: number; fileType?: string } = { 704 | scale: 1, 705 | quality: 1, 706 | } 707 | ) => { 708 | const { scale = 1, quality = 1, fileType = 'image/png' } = options; 709 | // 先把Transformer去掉 710 | const { stageRef, setSelected } = outerInstance.value; 711 | setSelected(0); 712 | setTimeout(() => { 713 | try { 714 | if (stageRef && stageRef.current) { 715 | const [, ext] = fileType.split('/'); 716 | const FileName = filename + '.' + ext; 717 | const uri = stageRef.current.toDataURL({ 718 | pixelRatio: scale, 719 | quality, 720 | mimeType: fileType, 721 | }); 722 | downloadURI(uri, FileName); 723 | } 724 | } catch (err) { 725 | console.log('err in exportToImage', err); 726 | } 727 | }, 100); 728 | }; 729 | 730 | // 输出base64 731 | Core.exportToBASE64 = () => { 732 | const { stageRef, setSelected } = outerInstance.value; 733 | // 先把Transformer去掉 734 | setSelected(0); 735 | return new Promise((resolve, reject) => { 736 | setTimeout(() => { 737 | if (stageRef && stageRef.current) { 738 | const b64 = stageRef.current.toDataURL(); 739 | resolve(b64); 740 | } else { 741 | reject(); 742 | } 743 | }, 1000); 744 | }); 745 | }; 746 | 747 | // 输出文件类型 748 | Core.exportToFile = (format = 'png', fileName) => { 749 | const { stageRef, setSelected } = outerInstance.value; 750 | function dataURLtoFile(dataurl: string, filename: string) { 751 | const arr = dataurl.split(','); 752 | if (arr[0]) { 753 | const reg = /:(.*?);/; 754 | const regString = arr[0].match(reg); 755 | if (regString) { 756 | const mime = regString[1]; 757 | const bstr = atob(arr[1]); 758 | let n = bstr.length; 759 | const u8arr = new Uint8Array(n); 760 | while (n--) { 761 | u8arr[n] = bstr.charCodeAt(n); 762 | } 763 | return new File([u8arr], filename, { type: mime }); 764 | } 765 | } 766 | } 767 | if (stageRef && stageRef.current) { 768 | // 先把Transformer去掉 769 | setSelected(-1); 770 | const b64 = stageRef.current.toDataURL(); 771 | return dataURLtoFile(b64, fileName + '.' + format); 772 | } 773 | }; 774 | 775 | // 撤销 776 | Core.withdraw = () => { 777 | const { setSteps } = outerInstance.value; 778 | if (stepCached && stepCached.canMoveBack) { 779 | stepCached.moveBack(); 780 | const curr = stepCached.getCurrent(); 781 | setSteps(curr); 782 | } 783 | }; 784 | 785 | // 重做 786 | Core.redo = () => { 787 | const { setSteps } = outerInstance.value; 788 | if (stepCached && stepCached.canMoveForward) { 789 | stepCached.moveForward(); 790 | setSteps(stepCached.getCurrent()); 791 | } 792 | }; 793 | 794 | // 画布缩放 795 | Core.canvasScale = (ratio: number) => { 796 | const { setStageScale } = outerInstance.value; 797 | // ratio属于[0.25,2] 798 | if (ratio <= 2.75 && ratio > 0) { 799 | setStageScale(ratio); 800 | } 801 | }; 802 | 803 | // 删除选中元素 804 | Core.deleteItem = () => { 805 | const { deleteItem } = outerInstance.value; 806 | deleteItem(); 807 | }; 808 | 809 | // 复制图层 810 | Core.copyItem = () => { 811 | const { copyItem } = outerInstance.value; 812 | copyItem(); 813 | }; 814 | 815 | // 获取当前画布信息 816 | Core.getInfo = () => { 817 | if (stepCached) { 818 | /* Removing some private properties of the step information 819 | (especially _ignore,_isProportionalScaling etc.) 820 | to reduce redundant data. 821 | */ 822 | const unHandledInfos = stepCached.getCurrent(); 823 | const result = unHandledInfos.map((info: any) => { 824 | const res = { ...info }; 825 | // 删除私有字段 826 | delete res._isProportionalScaling; 827 | delete res._ignore; 828 | delete res._isAdaptStage; 829 | delete res._isChangedCrop; 830 | return res; 831 | }); 832 | return result; 833 | } 834 | }; 835 | 836 | // i正数往上移动,负数往下移动 837 | Core.moveLayerLevel = (i: number) => { 838 | const { moveLayerLevel } = outerInstance.value; 839 | moveLayerLevel(i); 840 | }; 841 | 842 | // 将图层向四个方向移动像素 843 | Core.moveLayer = (direction: string, delta: number) => { 844 | const { moveLayer } = outerInstance.value; 845 | moveLayer(direction, delta); 846 | }; 847 | 848 | // 清空选项 849 | Core.clearSelected = () => { 850 | const { setSelected } = outerInstance.value; 851 | setSelected(-1); 852 | }; 853 | 854 | // 设置选中图层 855 | Core.setSelectedIndex = (id: LayerIdType) => { 856 | const { setSelected } = outerInstance.value; 857 | setSelected(id); 858 | }; 859 | 860 | // 多选图层开关 861 | Core.toggleMultiSelected = (state: boolean) => { 862 | const { toggleMultiSelected } = outerInstance.value; 863 | toggleMultiSelected(state); 864 | }; 865 | 866 | // 锁定/解锁某个图层 867 | Core.toogleLock = (id: LayerIdType) => { 868 | const { setSteps } = outerInstance.value; 869 | if (stepCached) { 870 | const currentLayer = [...stepCached.getCurrent()]; 871 | const index = currentLayer.findIndex((layer) => layer.id === id); 872 | const isBanDrag = currentLayer[index].banDrag; 873 | currentLayer[index].banDrag = !isBanDrag; 874 | setSteps(currentLayer); 875 | } 876 | }; 877 | // // 成组 878 | Core.madeGroup = (layers: any) => { 879 | const { madeGroup } = outerInstance.value; 880 | madeGroup(layers); 881 | }; 882 | 883 | // 拆组 884 | Core.divideGroup = (groupId: string) => { 885 | const { divideGroup } = outerInstance.value; 886 | divideGroup(groupId); 887 | }; 888 | // 改变某个图层的某个属性 889 | Core.changeLayerInfoById = (id: LayerIdType, item: object) => { 890 | const { changeLayerInfoById } = outerInstance.value; 891 | changeLayerInfoById(id, item); 892 | }; 893 | 894 | export default Core; 895 | -------------------------------------------------------------------------------- /src/keyboardListener.ts: -------------------------------------------------------------------------------- 1 | import { createKeybindingsHandler } from 'tinykeys'; 2 | 3 | /* 4 | 快捷键需适配win系统和mac系统 5 | 删除:delete 6 | 复制:Ctrl+C;苹果系统Cmd+c 7 | 粘贴:Ctrl+V;苹果系统Cmd+v 8 | 撤销:Ctrl+Z;苹果系统Cmd+z 9 | 恢复:shift+ctrl+z 10 | 置顶:shift+ctrl+向上键 11 | 置底:shift+ctrl+向下键 12 | 多选移动:按住shift,可以加选文本/图片/商品,一起移动 13 | */ 14 | 15 | class KeyboardListener { 16 | handler: any; 17 | canvasInstance: any; 18 | multiHandlerOn: any; 19 | multiHandlerOff: any; 20 | constructor() { 21 | this.handler = undefined; 22 | this.canvasInstance = undefined; 23 | this.multiHandlerOn = undefined; 24 | this.multiHandlerOff = undefined; 25 | } 26 | 27 | init = (konvaCanvasPoint: any) => { 28 | console.log('init'); 29 | this.canvasInstance = konvaCanvasPoint; 30 | if (!this.handler) { 31 | const handler = createKeybindingsHandler({ 32 | Delete: () => { 33 | this.canvasInstance.deleteItem(); 34 | }, 35 | BackSpace: () => { 36 | this.canvasInstance.deleteItem(); 37 | }, 38 | '$mod+KeyV': (event) => { 39 | event.preventDefault(); 40 | this.canvasInstance.copyItem(); 41 | }, 42 | '$mod+KeyZ': (event) => { 43 | event.preventDefault(); 44 | this.canvasInstance.withdraw(); 45 | }, 46 | '$mod+Shift+KeyZ': (event) => { 47 | event.preventDefault(); 48 | this.canvasInstance.redo(); 49 | }, 50 | ArrowUp: (event) => { 51 | event.preventDefault(); 52 | this.canvasInstance.moveLayer('y', -1); 53 | }, 54 | ArrowDown: (event) => { 55 | event.preventDefault(); 56 | this.canvasInstance.moveLayer('y', 1); 57 | }, 58 | ArrowLeft: (event) => { 59 | event.preventDefault(); 60 | this.canvasInstance.moveLayer('x', -1); 61 | }, 62 | ArrowRight: (event) => { 63 | event.preventDefault(); 64 | this.canvasInstance.moveLayer('x', 1); 65 | }, 66 | 'Shift+ArrowUp': (event) => { 67 | event.preventDefault(); 68 | this.canvasInstance.moveLayer('y', -10); 69 | }, 70 | 71 | 'Shift+ArrowDown': (event) => { 72 | event.preventDefault(); 73 | this.canvasInstance.moveLayer('y', 10); 74 | }, 75 | 'Shift+ArrowLeft': (event) => { 76 | event.preventDefault(); 77 | this.canvasInstance.moveLayer('x', -10); 78 | }, 79 | 'Shift+ArrowRight': (event) => { 80 | event.preventDefault(); 81 | this.canvasInstance.moveLayer('x', 10); 82 | }, 83 | '$mod+Shift+ArrowUp': (event) => { 84 | event.preventDefault(); 85 | this.canvasInstance.moveLayerLevel(1); 86 | }, 87 | '$mod+Shift+ArrowDown': (event) => { 88 | event.preventDefault(); 89 | this.canvasInstance.moveLayerLevel(-1); 90 | }, 91 | }); 92 | this.handler = handler; 93 | } 94 | if (!this.multiHandlerOn) { 95 | this.multiHandlerOn = (e: KeyboardEvent) => { 96 | if (e.keyCode === 16) { 97 | console.log('shift on'); 98 | // 打开multi 99 | // this.canvasInstance.toggleMultiSelected(true); 100 | } 101 | }; 102 | } 103 | 104 | if (!this.multiHandlerOff) { 105 | this.multiHandlerOff = (e: KeyboardEvent) => { 106 | if (e.keyCode === 16) { 107 | console.log('shift off'); 108 | // 打开multi 109 | // this.canvasInstance.toggleMultiSelected(false); 110 | } 111 | }; 112 | } 113 | }; 114 | 115 | listening = (target: any) => { 116 | target.addEventListener('keydown', this.handler); 117 | window.addEventListener('keydown', this.multiHandlerOn); 118 | window.addEventListener('keyup', this.multiHandlerOff); 119 | }; 120 | 121 | destory = (target: any) => { 122 | target.removeEventListener('keydown', this.handler); 123 | window.removeEventListener('keydown', this.multiHandlerOn); 124 | window.removeEventListener('keyup', this.multiHandlerOff); 125 | this.handler = undefined; 126 | this.multiHandlerOn = undefined; 127 | this.multiHandlerOff = undefined; 128 | }; 129 | } 130 | 131 | export default KeyboardListener; 132 | -------------------------------------------------------------------------------- /src/readme.md: -------------------------------------------------------------------------------- 1 | ```tsx 2 | {}} // 撤销 6 | onStepForward={() => {}} // 重做 7 | onScale={(a: number) => {}} // 缩放倍率 a[10,200] 8 | addItem={KonvaItem} // 见下 9 | onDel={(id: number) => {}} 10 | saveImg={} //生成图片 11 | saveData={} // 存储信息 12 | /> 13 | ``` 14 | 15 | ## KonvaItem 16 | 17 | ``` 18 | { 19 | type:'img'|'text', 20 | value:'' 是图片就传地址,文本类型就写个默认值 21 | } 22 | ``` 23 | 24 | ## props 25 | 26 | ## 更新 27 | 28 | 增加文字特效组 29 | 30 | ## 字段说明 31 | 32 | ### 通用字段 33 | 34 | | 字段 | 类型 | 必填 | 含义 | 35 | | ------------- | ------------- | ------- | ---------------------------------------------------- | -------- | 36 | | id | String | √ | 唯一标识符 | 37 | | type | `"image" | "text"` | √ | 图层类型 | 38 | | elementName | string | √ | 图层名称 | | 39 | | x | number | | 水平位置定位(以画布左上角为原点)默认为 0 | 40 | | y | number | | 垂直位置定位(以画布左上角为原点)默认为 0 | 41 | | opacity | number [0,1] | | 图层透明度,默认为 1 | 42 | | scaleX | number | | 水平方向缩放倍率,默认:1;负数时向 x 轴的负方向缩放 | 43 | | scaleY | number | | 垂直方向缩放倍率,默认:1;负数时向 y 轴的负方向缩放 | 44 | | rotation | number | | 顺时针旋转角度 | 45 | | shadowOffsetX | number | | 阴影水平偏移 | 46 | | shadowOffsetY | number | | 阴影垂直偏移 | 47 | | shadowColor | string | | 阴影颜色 | 48 | | shadowBlur | number [0,40] | | 投影模糊扩散 | 49 | | shadowOpacity | number [0,1] | | 投影透明度度 | 50 | | stroke | string | | 描边颜色 | 51 | | strokeWidth | number | | 描边宽度 | 52 | | shadowOpacity | number | | 阴影透明度 | 53 | 54 | ### 图像类型字段 55 | 56 | type 为 image 时 57 | 58 | | 字段 | 类型 | 必填 | 含义 | 59 | | ------ | ----------- | ---- | -------- | 60 | | width | number | √ | 图像宽度 | 61 | | height | number | √ | 图像高度 | 62 | | value | string | √ | 图像链接 | 63 | | crop | `CropProps` | | 剪裁参数 | 64 | | skewX | number | | 65 | | skewY | number | | 66 | 67 | #### CropProps 68 | 69 | | 字段 | 类型 | 必填 | 含义 | 70 | | ------------ | ------ | ---- | ---------------- | 71 | | originWidth | number | √ | 原始图片宽度 | 72 | | originHeight | number | √ | 原始图片高度 | 73 | | width | number | √ | 剪裁宽度 | 74 | | height | number | √ | 剪裁高度 | 75 | | unit | `px` | √ | 单位,必须写"px" | 76 | | x | number | √ | 剪裁框水平定位 | 77 | | y | number | √ | 剪裁框水平定位 | 78 | 79 | ### 文字类型参数 80 | 81 | type 为 text 时 82 | | 字段 | 类型 | 必填 | 含义 | 83 | | ----------- | -------- | ------- | -------------------------------------------------------- | -------- | 84 | value|string|√ |文本 85 | color|string ||文本颜色,默认为`#000` 86 | fontSize|number||字体大小 87 | fill|string||字体颜色,这里 hexcode 必须为 8 位,如:"#f800004d",最后两位为透明度,具体转换规则见下 88 | | fontStyle | `'bold' | 'italic' |'bold italic' ` | | 加粗 "bold" 斜体"italic" 二者的任意排列组合 | 89 | | textDecoration |`'underline' | 'line-through' |'underline line-through' ` | | 下划线 "underline" 贯穿线"line-through" 二者的任意排列组合 | 90 | align|`'left' | 'right' | 'center'`||对齐方式,默认左对齐 91 | 92 | #### hexcode 转换规则 93 | 94 | alpha 为透明度,当 alpha 为 0 时彻底透明; 95 | 96 | ``` 97 | hexcode最后两位 = alpha < 0.01 ? '00' : Math.round(255 * alpha).toString(16) 98 | ``` 99 | 100 | ### reference 101 | 102 | 1. [text-shadow](https://www.w3.org/Style/Examples/007/text-shadow.zh_CN.html) 103 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import Konva from 'konva'; 3 | export type itemType = 'image' | 'text' | 'shape' | 'stage' | 'group'; 4 | 5 | // props 6 | export interface IaddItem { 7 | type: itemType; 8 | value: string; 9 | } 10 | 11 | export type LayerIdType = string | number; 12 | 13 | export interface IProps { 14 | width: number; 15 | height: number; 16 | backgroundColor?: string; 17 | addItem?: IaddItem; 18 | backgroundStyle?: CSSProperties; 19 | selectedItemChange?: any; 20 | maxStep?: number; 21 | setRedo?: (a: boolean) => void; 22 | setWithdraw?: (a: boolean) => void; 23 | onChangeSelected?: (a?: any) => void; // 监听当前元素改变 24 | bindRef?: (a: any) => void; 25 | stepInfo?: Iinfo[]; 26 | onChangeStep?: (steps: any) => void; 27 | } 28 | 29 | // 内部的 30 | export interface IcommonInfo { 31 | id: LayerIdType; 32 | type: itemType; 33 | isSelected?: boolean; 34 | handleInfo: (a: any) => void; 35 | handleSelected?: (id: number) => void; 36 | setShowTransformer: (s: boolean) => void; 37 | stageRef?: any; 38 | myRef?: any; 39 | trRef?: any; 40 | onRef?: (a: any) => void; 41 | banDrag?: boolean; 42 | isNew?: boolean; 43 | x: number; 44 | y: number; 45 | // w: number; 46 | // h: number; 47 | scaleX?: number; 48 | scaleY?: number; 49 | stageScale: number; 50 | fontSize?: number; 51 | mType?: number; 52 | elementName?: string; 53 | label?: string; // 元素层名称 54 | name?: string; //psd解析出来的图层名称 55 | } 56 | 57 | export interface IimageInfo extends IcommonInfo { 58 | type: 'image'; 59 | value: string; 60 | trRef: any; 61 | crop: any; 62 | width?: number; 63 | height?: number; 64 | _isAdaptStage?: number; 65 | _isProportionalScaling?: number; 66 | _isChangedCrop?: boolean; 67 | } 68 | 69 | interface IshapeCommon { 70 | id: LayerIdType; 71 | } 72 | 73 | interface RectProps extends Konva.RectConfig {} 74 | interface CircleProps extends Konva.CircleConfig {} 75 | interface ArcProps extends Konva.ArcConfig {} 76 | interface StarProps extends Konva.StarConfig {} 77 | interface ArrowProps extends Konva.ArrowConfig {} 78 | interface EllipseProps extends Konva.EllipseConfig {} 79 | 80 | export type ShapeType = 81 | | 'rect' 82 | | 'circle' 83 | | 'arc' 84 | | 'star' 85 | | 'arrow' 86 | | 'ellipse'; 87 | 88 | type ShapePropsMap = { 89 | rect: RectProps; 90 | circle: CircleProps; 91 | arc: ArcProps; 92 | star: StarProps; 93 | arrow: ArrowProps; 94 | ellipse: EllipseProps; 95 | }; 96 | export type IShapeInfo = ShapePropsMap[ShapeType]; 97 | 98 | // interface Shape extends ShapeProps, Konva.ShapeConfig { 99 | // type: ShapeType; 100 | // } 101 | 102 | type Shape = ShapePropsMap[T] & 103 | Konva.ShapeConfig & { type: T }; 104 | 105 | // export interface IShapeInfo extends IcommonInfo { 106 | // type: 'shape'; 107 | // value: ShapeType; 108 | // fill?: string; 109 | // width?: number; 110 | // height?: number; 111 | // stroke?: string; // 描边颜色 112 | // strokeWidth?: number; // 描边宽度 113 | 114 | // // 以下为Rect专属 115 | // cornerRadius?: number | Array; 116 | 117 | // // 以下为Circle专属props 118 | // radius?: number; 119 | 120 | // // 以下为arc专属字段 121 | // innerRadius?: number; // 内径 122 | // outerRadius?: number; // 外径 123 | // angle?: number; // 弧形圆角 124 | 125 | // // 以下为star专属 126 | // numPoints?: number; 127 | 128 | // // 以下为arrow专属 129 | // points?: Array; 130 | 131 | // // 以下为ellipse专属 132 | // ellipseRadius?: { radiusX: number; radiusY: number }; 133 | // // pointerLength?: number; 134 | // // pointerWidth?: number; 135 | // } 136 | 137 | export interface IgroupInfo extends IcommonInfo { 138 | type: 'group'; 139 | elements: Array; 140 | } 141 | 142 | export interface ItextInfo extends IcommonInfo { 143 | type: 'text'; 144 | value: string; 145 | color?: 'string'; 146 | width?: number; 147 | height?: number; 148 | } 149 | 150 | export interface IFunc { 151 | exportToImage: ( 152 | a: string, 153 | opt?: { scale?: number; quality?: number; fileType?: string } 154 | ) => void; 155 | exportToBASE64: () => Promise; 156 | exportToFile: (format: string, filename: string) => File | undefined; 157 | withdraw: () => void; 158 | redo: () => void; 159 | canvasScale: (a: number) => void; 160 | deleteItem: () => void; 161 | copyItem: () => void; 162 | getInfo: () => any; 163 | moveLayerLevel: (i: number) => void; 164 | moveLayer: (direction: string, delta: number) => void; 165 | clearSelected: () => void; 166 | setSelectedIndex: (id: LayerIdType) => void; 167 | toogleLock: (id: LayerIdType) => void; 168 | toggleMultiSelected: (state: boolean) => void; 169 | madeGroup: (layers: any) => void; 170 | divideGroup: (groupId: string) => void; 171 | changeLayerInfoById: (id: LayerIdType, item: object) => void; 172 | // getSelectedInfo: () => Iinfo | Array; 173 | } 174 | 175 | export type Iinfo = IimageInfo | ItextInfo | IShapeInfo | IgroupInfo; 176 | 177 | -------------------------------------------------------------------------------- /src/utils/circularQueue.ts: -------------------------------------------------------------------------------- 1 | class circularQueue { 2 | private list: any[] = []; 3 | private front: number = 0; 4 | private tail: number = 0; 5 | public length: number = 0; 6 | public current: number = 0; 7 | 8 | constructor(size: number, defaultElement = []) { 9 | this.length = size; 10 | this.front = 0; 11 | this.current = 0; 12 | this.list = new Array(size); 13 | this.list[0] = defaultElement; 14 | this.tail = 1; 15 | } 16 | 17 | get canMoveForward() { 18 | return !this.isEmpty() && (this.current + 1) % this.length !== this.tail; 19 | } 20 | get canMoveBack() { 21 | return this.current !== this.front; 22 | } 23 | 24 | // 清空current之后的元素 25 | clearAfterCurrent = () => { 26 | let i = this.current; 27 | const length = this.length; 28 | 29 | while ((i + 1) % length !== this.tail) { 30 | const clearIndex = (i + 1) % length; 31 | this.list[clearIndex] = undefined; 32 | i = clearIndex; 33 | } 34 | this.tail = (this.current + 1) % this.length; 35 | }; 36 | 37 | // 入队 38 | enqueue = (item: any) => { 39 | // 当入队时current不是处于队尾指针的前驱时,需要清空current到队尾之间的所有元素,并重置尾指针 40 | if (this.isFull() && (this.current + 1) % this.length !== this.tail) { 41 | this.clearAfterCurrent(); 42 | } 43 | 44 | if (this.isFull()) { 45 | this.tail = (this.current + 1) % this.length; 46 | // 满了移动头指针 47 | this.front = (this.front + 1) % this.length; 48 | } 49 | // const index = this.tail % this.length; 50 | this.list[this.tail] = item; 51 | this.current = this.tail; 52 | this.tail = (this.tail + 1) % this.length; 53 | }; 54 | 55 | // 不涉及 56 | dequeue() {} 57 | 58 | isEmpty = () => { 59 | return typeof this.list[this.front] === 'undefined'; 60 | }; 61 | 62 | isFull = () => { 63 | return ( 64 | this.front === this.tail && typeof this.list[this.front] !== 'undefined' 65 | ); 66 | }; 67 | 68 | getCurrent = () => { 69 | return this.list[this.current]; 70 | }; 71 | 72 | // 改变当前current指向的 73 | // changeCurrent() { 74 | 75 | // } 76 | 77 | // 往右移一步 (尾指针方向) 78 | moveForward = () => { 79 | if (this.canMoveForward) { 80 | this.current = this.isFull() 81 | ? (this.current + 1 + this.length) % this.length 82 | : this.current + 1; 83 | } 84 | }; 85 | // 往左移一步 (头指针方向) 86 | moveBack = () => { 87 | if (this.canMoveBack) { 88 | this.current = this.isFull() 89 | ? (this.current - 1 + this.length) % this.length 90 | : this.current - 1; 91 | } 92 | }; 93 | 94 | print = () => { 95 | let i = 0; 96 | let p = this.front; 97 | while (i < this.length) { 98 | p = (p + 1) % this.length; 99 | i++; 100 | } 101 | }; 102 | 103 | // 清空当前队列中所有内容 104 | clear = () => { 105 | this.length = 0; 106 | this.front = 0; 107 | this.current = 0; 108 | this.list = []; 109 | this.tail = 0; 110 | }; 111 | } 112 | 113 | export default circularQueue; 114 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | const debounce = (callback = (a?: any, b?: any) => {}, time = 100) => { 2 | let timer: any = null; 3 | // @ts-ignore 4 | return (...params) => { 5 | clearTimeout(timer); 6 | timer = setTimeout(() => { 7 | callback(...params); 8 | }, time); 9 | }; 10 | }; 11 | 12 | export default debounce; 13 | -------------------------------------------------------------------------------- /src/utils/handleKonvaItem.ts: -------------------------------------------------------------------------------- 1 | const handleKonvaItem = (konvaNode: any) => { 2 | // const { attrs, textWidth, textHeight } = konvaNode; 3 | const { attrs } = konvaNode; 4 | const { 5 | scaleX = 1, 6 | scaleY = 1, 7 | rotation, 8 | skewX, 9 | skewY, 10 | x = 0, 11 | y = 0, 12 | type, 13 | } = attrs; 14 | const otherProperty: any = {}; 15 | if (type === 'text') { 16 | otherProperty.x = Math.round(x); 17 | otherProperty.y = Math.round(y); 18 | // otherProperty.w = Math.round(textWidth * scaleX); 19 | // otherProperty.h = Math.round(textHeight * scaleY); 20 | } 21 | 22 | return { 23 | scaleX, 24 | scaleY, 25 | rotation, 26 | skewX, 27 | skewY, 28 | x, 29 | y, 30 | ...otherProperty, 31 | }; 32 | }; 33 | 34 | export default handleKonvaItem; 35 | -------------------------------------------------------------------------------- /src/utils/handleSize.ts: -------------------------------------------------------------------------------- 1 | export const getRealSize = (ref: any) => { 2 | if (ref?.current) { 3 | console.log('ref', ref); 4 | 5 | const textNode = ref.current; 6 | const width = textNode.getWidth(); 7 | console.log('width', width); 8 | 9 | const height = textNode.getHeight(); 10 | console.log('height', height); 11 | 12 | return { width, height }; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/imageAdapt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | type resType = { width?: number; height?: number; x?: number; y?: number }; 3 | 4 | const _coreAdaption = ( 5 | substract: number, 6 | ratio: number, 7 | containerW: number, 8 | containerH: number 9 | ) => { 10 | const res: resType = {}; 11 | 12 | switch (true) { 13 | case substract > 0: { 14 | res.width = Math.round(containerH * ratio); 15 | res.height = containerH; 16 | break; 17 | } 18 | case substract < 0: { 19 | res.width = containerW; 20 | res.height = Math.round(containerW / ratio); 21 | break; 22 | } 23 | default: { 24 | res.height = containerH; 25 | res.width = containerW; 26 | } 27 | } 28 | 29 | return res; 30 | }; 31 | 32 | /** generate the coordinate of the image placed in the center of the stage. 33 | */ 34 | const _computeCenterInStage = ( 35 | imgW: number, 36 | imgH: number, 37 | stageW: number, 38 | stageH: number 39 | ) => { 40 | const x = Math.round((stageW - imgW) / 2); 41 | const y = Math.round((stageH - imgH) / 2); 42 | return { x, y, width: imgW, height: imgH }; 43 | }; 44 | 45 | /** generate properties of konvaImage for image-replacement. 46 | * The rules of image-replacement: 47 | 1. Compute the original ratio:original_ratio=oldImage.width/oldImage.height 48 | 2. Compute the current ratio:current_ratio=cur.width/cur.height 49 | 3. if the original_ratio is greater than current_ratio, the current image is visually vertical, 50 | change the height of current into the old one. 51 | 4. if the original_ratio is less than current_ratio, the current image is visually horizontal, 52 | change the width of current into the old one. 53 | 5. if both of the ratio are equal,just scaling the current one to the same ratio as the old one. 54 | */ 55 | const adaptReplaceImage = ( 56 | image: any, 57 | oldSize: { width: number; height: number } 58 | ) => { 59 | if (image && oldSize) { 60 | const imgW = image.width; 61 | const imgH = image.height; 62 | const curRatio = imgW / imgH; 63 | 64 | const oldW = oldSize.width; 65 | const oldH = oldSize.height; 66 | const oldRatio = oldW / oldH; 67 | const substract = oldRatio - curRatio; 68 | const res: resType = _coreAdaption(substract, curRatio, oldW, oldH); 69 | return res; 70 | } 71 | }; 72 | 73 | /** generate properties of konvaImage for coming-image in stage. 74 | * The rules of coming-image rendering: 75 | * 1. compute the size of stage and image. 76 | * if both of the width and height of the image is less than those of the stage, just return. 77 | * 2. compute the ratio of stage: ratio=stage.width/stage.height.if the ratio is much than 1,the stage is visually horizontal. 78 | * The height of the coming-image must be changed into the stage's height. 79 | * 3. if the ratio is less than 1,the stage is visually vertical. 80 | * The width of the coming-image must be changed into the stage's width. 81 | */ 82 | const adaptNewImage = (image: any, stage: any) => { 83 | if (!stage || !image) return; 84 | 85 | const imgW = image.width; 86 | const imgH = image.height; 87 | 88 | const stageW = stage.width(); 89 | const stageH = stage.height(); 90 | 91 | if (imgW < stageW && imgH < stageH) { 92 | return _computeCenterInStage(imgW, imgH, stageW, stageH); 93 | } 94 | 95 | const stageRatio = stageW / stageH; 96 | const curRatio = imgW / imgH; 97 | const substract = stageRatio - curRatio; 98 | 99 | const size: resType = _coreAdaption(substract, curRatio, stageW, stageH); 100 | if (size.width && size.height) { 101 | const coordinate = _computeCenterInStage( 102 | size.width, 103 | size.height, 104 | stageW, 105 | stageH 106 | ); 107 | return { ...coordinate, ...size }; 108 | } else { 109 | return size; 110 | } 111 | }; 112 | 113 | /** 114 | * 根据画布尺寸,适配相应的缩放比例 115 | */ 116 | const stageScaleAdapt = (width: number, height: number) => { 117 | const max = Math.max(width, height); 118 | switch (true) { 119 | case max <= 960: 120 | return 0.7; 121 | case max <= 1200: 122 | return 0.6; 123 | case max <= 1400: 124 | return 0.5; 125 | case max <= 1700: 126 | return 0.4; 127 | default: 128 | return 0.3; 129 | } 130 | }; 131 | 132 | /** 133 | * 根据舞台宽高和画布宽高,自动计算缩放比例 134 | */ 135 | const stageScaleAutoAdapt = ( 136 | stageW: number, 137 | stageH: number, 138 | imgW: number, 139 | imgH: number 140 | ) => { 141 | if (imgW < stageW && imgH < stageH) { 142 | return 1; // 画布比舞台小的不缩放 143 | } else { 144 | const xScale = imgW / stageW; 145 | const yScale = imgH / stageH; 146 | return Math.floor((1 / Math.max(xScale, yScale)) * 100) / 100; 147 | } 148 | }; 149 | 150 | const cropImageAdaptStage = ( 151 | crop: any, 152 | changedWidth: number, 153 | changedHeight: number 154 | ) => { 155 | const { x, y, width, height } = crop; 156 | const kx = width / changedWidth; 157 | const ky = height / changedHeight; 158 | return { x: x / kx, y: y / ky }; 159 | // return { x: x, y: y }; 160 | }; 161 | 162 | export { 163 | adaptReplaceImage, 164 | adaptNewImage, 165 | cropImageAdaptStage, 166 | stageScaleAdapt, 167 | stageScaleAutoAdapt, 168 | }; 169 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as AdaptStrategy from './imageAdapt'; 2 | 3 | export { AdaptStrategy }; 4 | -------------------------------------------------------------------------------- /src/utils/textHandler.ts: -------------------------------------------------------------------------------- 1 | class SelectChangeListener { 2 | target: HTMLElement; 3 | originalText: string; 4 | handleAnchor: ( 5 | allWidth: number, 6 | start: number, 7 | end: number, 8 | total: number 9 | ) => void; 10 | allWidth: number; 11 | constructor( 12 | target: HTMLElement, 13 | originalText: string, 14 | handleAnchor: ( 15 | allWidth: number, 16 | start: number, 17 | end: number, 18 | total: number 19 | ) => void, 20 | allWidth: number 21 | ) { 22 | this.target = target; 23 | this.originalText = originalText; 24 | this.handleAnchor = handleAnchor; 25 | this.allWidth = allWidth; 26 | } 27 | 28 | public handler = (e: any) => { 29 | const content = e.target.value; 30 | console.log('content', content); 31 | const selection = document.all 32 | ? // @ts-ignore 33 | document.selection.createRange().text 34 | : document.getSelection(); 35 | 36 | const text = selection.toString(); 37 | const startIndex = this.originalText.indexOf(text); 38 | console.log('text', text); 39 | if (startIndex > -1 && content) { 40 | this.handleAnchor( 41 | this.allWidth, 42 | startIndex, 43 | startIndex + text.length, 44 | content.length 45 | ); 46 | } 47 | }; 48 | 49 | listen = () => { 50 | document.addEventListener('mouseup', this.handler); 51 | }; 52 | 53 | destory = () => { 54 | document.removeEventListener('mouseup', this.handler); 55 | }; 56 | } 57 | 58 | const getRealBoxSize = (trRef: any, stageScale: number, textNode: any) => { 59 | const transformerBoxAttr = trRef.current.children?.[0].attrs; 60 | 61 | const size: any = {}; 62 | 63 | size.width = 64 | transformerBoxAttr.width * stageScale - textNode.padding() * 2 + 'px'; 65 | size.height = 66 | transformerBoxAttr.height * stageScale - textNode.padding() * 2 + 'px'; 67 | 68 | return size; 69 | }; 70 | 71 | export { SelectChangeListener, getRealBoxSize }; 72 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import _, { isObject } from 'lodash'; 2 | import { Iinfo, IcommonInfo, LayerIdType } from '../type'; 3 | 4 | const randomId = () => { 5 | const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; 6 | const length = 10; // 生成10位的随机字符串 7 | const randomChars = _.sampleSize(chars, length); 8 | return randomChars.join(''); 9 | }; 10 | 11 | /** 12 | * canvasInfo中有重复的id,对其进行替换并输出 13 | * TODO: 未进行嵌套组结构适配 14 | */ 15 | const handleDuplicateId = (canvasInfo: IcommonInfo[] = []) => { 16 | const idMap: any = {}; 17 | 18 | /** 19 | * 产生独一的key 20 | * @param key label或原本的id 21 | * @returns keystring 22 | */ 23 | const handleKey = (key: string | number, label?: string) => { 24 | const isExisit = !!idMap[key]; 25 | if (!isExisit) { 26 | idMap[key] = 1; 27 | return key; 28 | } else { 29 | if (label) { 30 | return idMap[label] ? randomId() : label; 31 | } 32 | return randomId(); 33 | } 34 | }; 35 | 36 | return canvasInfo.map((info: IcommonInfo) => { 37 | if (info) { 38 | // @ts-ignore 39 | const { id, label, value, elementName, name } = info; 40 | return { 41 | ...info, 42 | label: label || name || value || elementName, 43 | id: handleKey(id, label), 44 | }; 45 | } 46 | return info; 47 | }); 48 | }; 49 | 50 | const downloadURI = (uri: string, name: string) => { 51 | const link = document.createElement('a'); 52 | link.download = name; 53 | link.href = uri; 54 | document.body.appendChild(link); 55 | link.click(); 56 | document.body.removeChild(link); 57 | }; 58 | 59 | const isSelectedId = (id: LayerIdType, layerId: number) => { 60 | if (Array.isArray(id)) { 61 | return id.includes(layerId); 62 | } else { 63 | return id === layerId; 64 | } 65 | }; 66 | 67 | // 多选元素更新patch 68 | const updateMultiPatch = (patch: any, layers: Array) => { 69 | const newLayers = [...layers]; 70 | if (isObject(patch)) { 71 | const ids = Object.keys(patch); 72 | ids.forEach((id: string) => { 73 | const index = newLayers.findIndex((layer: any) => layer?.id === id); 74 | // @ts-ignore 75 | if (index > -1 && isObject(patch[id])) { 76 | //@ts-ignore 77 | newLayers[index] = { ...newLayers[index], ...patch[id] }; 78 | } 79 | }); 80 | } 81 | console.error('多选patch格式错误'); 82 | }; 83 | 84 | export { handleDuplicateId, downloadURI, isSelectedId, updateMultiPatch }; 85 | -------------------------------------------------------------------------------- /src/字段说明.md: -------------------------------------------------------------------------------- 1 | ### 通用字段 2 | 3 | | 字段 | 类型 | 必填 | 含义 | 4 | | ------------- | ------------- | ------- | ---------------------------------------------------- | -------- | 5 | | id | String | √ | 唯一标识符 | 6 | | type | `"image" | "text"` | √ | 图层类型 | 7 | | elementName | string | √ | 图层名称 | | 8 | | x | number | | 水平位置定位(以画布左上角为原点)默认为 0 | 9 | | y | number | | 垂直位置定位(以画布左上角为原点)默认为 0 | 10 | | opacity | number [0,1] | | 图层透明度,默认为 1 | 11 | | scaleX | number | | 水平方向缩放倍率,默认:1;负数时向 x 轴的负方向缩放 | 12 | | scaleY | number | | 垂直方向缩放倍率,默认:1;负数时向 y 轴的负方向缩放 | 13 | | rotation | number | | 顺时针旋转角度 | 14 | | shadowOffsetX | number | | 阴影水平偏移 | 15 | | shadowOffsetY | number | | 阴影垂直偏移 | 16 | | shadowColor | string | | 阴影颜色 | 17 | | shadowBlur | number [0,40] | | 投影模糊扩散 | 18 | | shadowOpacity | number [0,1] | | 投影透明度度 | 19 | | stroke | string | | 描边颜色 | 20 | | strokeWidth | number | | 描边宽度 | 21 | | shadowOpacity | number | | 阴影透明度 | 22 | 23 | ### 图像类型字段 24 | 25 | type 为 image 时 26 | 27 | | 字段 | 类型 | 必填 | 含义 | 28 | | ------ | ----------- | ---- | -------- | 29 | | width | number | √ | 图像宽度 | 30 | | height | number | √ | 图像高度 | 31 | | value | string | √ | 图像链接 | 32 | | crop | `CropProps` | | 剪裁参数 | 33 | | skewX | number | | 34 | | skewY | number | | 35 | 36 | #### CropProps 37 | 38 | | 字段 | 类型 | 必填 | 含义 | 39 | | ------------ | ------ | ---- | ---------------- | 40 | | originWidth | number | √ | 原始图片宽度 | 41 | | originHeight | number | √ | 原始图片高度 | 42 | | width | number | √ | 剪裁宽度 | 43 | | height | number | √ | 剪裁高度 | 44 | | unit | `px` | √ | 单位,必须写"px" | 45 | | x | number | √ | 剪裁框水平定位 | 46 | | y | number | √ | 剪裁框水平定位 | 47 | 48 | ### 文字类型参数 49 | 50 | type 为 text 时 51 | | 字段 | 类型 | 必填 | 含义 | 52 | | ----------- | -------- | ------- | -------------------------------------------------------- | -------- | 53 | value|string|√ |文本 54 | color|string ||文本颜色,默认为`#000` 55 | fontSize|number||字体大小 56 | fill|string||字体颜色,这里 hexcode 必须为 8 位,如:"#f800004d",最后两位为透明度,具体转换规则见下 57 | | fontStyle | `'bold' | 'italic' |'bold italic' ` | | 加粗 "bold" 斜体"italic" 二者的任意排列组合 | 58 | | textDecoration |`'underline' | 'line-through' |'underline line-through' ` | | 下划线 "underline" 贯穿线"line-through" 二者的任意排列组合 | 59 | align|`'left' | 'right' | 'center'`||对齐方式,默认左对齐 60 | 61 | #### hexcode 转换规则 62 | 63 | alpha 为透明度,当 alpha 为 0 时彻底透明; 64 | 65 | ``` 66 | hexcode最后两位 = alpha < 0.01 ? '00' : Math.round(255 * alpha).toString(16) 67 | ``` 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es2015", 5 | "allowJs": true, 6 | "lib": ["DOM"], 7 | "declaration": true, 8 | "emitDeclarationOnly": false, 9 | "resolveJsonModule":true, 10 | "esModuleInterop": true, 11 | "baseUrl": "./", 12 | "paths": { 13 | "src/*": ["src/*.js"] 14 | }, 15 | "moduleResolution": "node", 16 | "isolatedModules": true, 17 | "jsx": "react-jsx", 18 | "noImplicitAny": false, 19 | "skipLibCheck": true, 20 | "allowSyntheticDefaultImports": true, 21 | "removeComments": true 22 | }, 23 | "include": ["./index.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /unpublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "执行的文件名:$0"; 3 | echo "第一个参数为:$1"; 4 | echo "第二个参数为:$2"; 5 | echo "第三个参数为:$3"; 6 | 7 | DEV_ENV="-dev" 8 | PROD_ENV="-prod" 9 | 10 | 11 | if test $1 = $DEV_ENV 12 | then 13 | REPO="http://localhost:4873/" 14 | elif test $1 = $PROD_ENV 15 | then 16 | token=$(cat ./.npm_token) 17 | echo "token=$token" 18 | REPO="https://registry.npmjs.org/" 19 | else 20 | echo "enviroment invalid" 21 | exit 8 22 | fi 23 | 24 | echo "REPO=$REPO" 25 | version=$(jq -r '.version' package.json) 26 | 27 | npm unpublish react-konva-editor@$version --force --registry $REPO|| echo "【no need to unpublish】" 28 | echo "【unpublish!!】" 29 | 30 | echo $n press any key to exit: $c 31 | read name 32 | echo "$name" 33 | 34 | # 删除老版本 35 | # 发布新版本 --------------------------------------------------------------------------------