├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── __tests__ ├── point-finder.test.js ├── rotate.test.js ├── scale.test.js ├── styler.js └── translate.test.js ├── package-lock.json ├── package.json └── src ├── index.js ├── point-finder.js ├── rotate.js ├── scale.js ├── styler.js └── translate.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread" 7 | ] 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/recommended" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module", 10 | "ecmaFeatures": { 11 | "jsx": true, 12 | "experimentalObjectRestSpread": true 13 | } 14 | }, 15 | "env": { 16 | "browser": true, 17 | "mocha": true, 18 | "node": true 19 | }, 20 | "rules": { 21 | "valid-jsdoc": 2, 22 | "indent": ["error", 2] 23 | 24 | }, 25 | "plugins": [ 26 | "import" 27 | ] 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /lib 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | .idea 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | _book/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | coverage 3 | node_modules 4 | src 5 | .babelrc 6 | .eslintrc 7 | .gitignore 8 | .npmignore 9 | .travis.yml 10 | package.json 11 | package-lock.json 12 | yarn-error.log 13 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | 6 | before_install: 7 | - 'nvm install-latest-npm' 8 | 9 | sudo: false 10 | 11 | script: 12 | - npm run lint 13 | - npm run test:cov 14 | 15 | after_success: 16 | - ./node_modules/codecov/bin/codecov 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Solaiman Kmail (skmail) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Free Transform Tool Utility 2 | 3 | 4 | [![NPM Version](https://img.shields.io/npm/v/free-transform.svg?style=flat)](https://www.npmjs.com/package/free-transform) [![NPM Downloads](https://img.shields.io/npm/dm/free-transform.svg?style=flat)](https://www.npmjs.com/package/free-transform) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://img.shields.io/travis/skmail/free-transform/master.svg?style=flat)](https://travis-ci.org/skmail/free-transform) [![codecov.io](https://codecov.io/gh/skmail/free-transform/branch/master/graph/badge.svg)](https://codecov.io/gh/skmail/free-transform) 5 | 6 | 7 | A set of functions to calculate boundries element resizing, translating, rotating and styles object extraction 8 | 9 | 10 | ## Installation 11 | `npm install free-transform` 12 | 13 | 14 | ## Usage 15 | 16 | ### Scale 17 | 18 | #### scale types (Handles) 19 | `tl` Top Left Handle 20 | 21 | `ml` Middle Left Handle 22 | 23 | `tr` Top Right Handle 24 | 25 | `tm` Top Middle Handle 26 | 27 | `bl` Bottom Left Handle 28 | 29 | `bm` Bottom Middle Handle 30 | 31 | `br` Bottom Right Handle 32 | 33 | `mr` Middle Right Handle 34 | 35 | 36 | ```js 37 | import {scale} from 'free-transform' 38 | 39 | let element = { 40 | x:0, 41 | y:0, 42 | scaleX:1, 43 | scaleY:1, 44 | width:100, 45 | height:100, 46 | angle:0, 47 | scaleLimit:0.1, 48 | } 49 | 50 | const onScaleHandleMouseDown = (event) => { 51 | 52 | event.stopPropagation(); 53 | event.preventDefault(); 54 | const drag = scale('tl', { 55 | startX: event.pageX, 56 | startY: event.pageY, 57 | scaleFromCenter: event.altKey, 58 | aspectRatio: event.shiftKey, 59 | ...element, 60 | }, (payload) => { // {x, y, scaleX, scaleY} 61 | // dragging 62 | element = { ...element, ...payload } 63 | }); 64 | 65 | const up = () => { 66 | document.removeEventListener('mousemove', drag); 67 | document.removeEventListener('mouseup', up); 68 | }; 69 | 70 | document.addEventListener("mousemove", drag) 71 | document.addEventListener("mouseup", up) 72 | 73 | } 74 | 75 | ``` 76 | ### Rotation 77 | 78 | 79 | 80 | ```js 81 | 82 | import {rotate} from 'free-transform' 83 | 84 | let element = { 85 | x:0, 86 | y:0, 87 | scaleX:1, 88 | scaleY:1, 89 | width:100, 90 | height:100, 91 | angle:0, 92 | scaleLimit:0.1, 93 | } 94 | 95 | const onRotateHandleMouseDown = (event) => { 96 | 97 | event.stopPropagation(); 98 | event.preventDefault(); 99 | 100 | const drag = rotate({ 101 | startX: event.pageX, 102 | startY: event.pageY, 103 | offsetX: 0, // the offset x of parent (parent.offsetLeft) 104 | offsetY: 0, // the offset y of parent (parent.offsetTop) 105 | ...element, 106 | }, (payload) => { // {angle} 107 | // dragging 108 | element = { ...element, ...payload } 109 | }); 110 | 111 | const up = () => { 112 | document.removeEventListener('mousemove', drag); 113 | document.removeEventListener('mouseup', up); 114 | }; 115 | 116 | document.addEventListener("mousemove", drag) 117 | document.addEventListener("mouseup", up) 118 | } 119 | 120 | ``` 121 | 122 | 123 | ### Translation (Dragging) 124 | 125 | ```js 126 | let element = { 127 | x:0, 128 | y:0, 129 | scaleX:1, 130 | scaleY:1, 131 | width:100, 132 | height:100, 133 | angle:0, 134 | scaleLimit:0.1, 135 | } 136 | 137 | const onElementMouseDown = (event) => { 138 | event.stopPropagation(); 139 | 140 | const drag = translate({ 141 | x: element.x, 142 | y: element.y, 143 | startX: event.pageX, 144 | startY: event.pageY 145 | }, (payload) => { // {x,y} 146 | // dragging 147 | element = { ...element, ...payload } 148 | }); 149 | 150 | const up = () => { 151 | document.removeEventListener('mousemove', drag); 152 | document.removeEventListener('mouseup', up); 153 | }; 154 | 155 | document.addEventListener('mousemove', drag); 156 | document.addEventListener('mouseup', up); 157 | } 158 | 159 | ``` -------------------------------------------------------------------------------- /__tests__/point-finder.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | getTL, 3 | getTR, 4 | getBL, 5 | getBR, 6 | getMR, 7 | getBM, 8 | getTM, 9 | getML, 10 | getPoint, 11 | getCenter, 12 | getMovePoint, 13 | getSineCosine, 14 | getOppositePoint, minMax, 15 | } from '../src/point-finder' 16 | 17 | const roundPoint = ({x, y}) => { 18 | return { 19 | x: Math.round(x), 20 | y: Math.round(y), 21 | } 22 | } 23 | it('calculate the center of element', function () { 24 | expect(getCenter({ 25 | x: 0, 26 | y: 0, 27 | width: 100, 28 | height: 100, 29 | scaleX: 1, 30 | scaleY: 1 31 | })).toEqual({ 32 | x: 50, 33 | y: 50 34 | }) 35 | }); 36 | 37 | it('calculate the center of scaled element', function () { 38 | expect(getCenter({ 39 | x: 0, 40 | y: 0, 41 | width: 100, 42 | height: 100, 43 | scaleX: 1.1, 44 | scaleY: 1.1 45 | })).toEqual({ 46 | x: (100 + (100 - 100 * 1.1)) / 2, 47 | y: (100 + (100 - 100 * 1.1)) / 2, 48 | }) 49 | }); 50 | 51 | it('calculate the center of moved & scaled element', function () { 52 | expect(getCenter({ 53 | x: 15, 54 | y: 15, 55 | width: 100, 56 | height: 100, 57 | scaleX: 1.1, 58 | scaleY: 1.1 59 | })).toEqual({ 60 | x: 15 + (100 + (100 - 100 * 1.1)) / 2, 61 | y: 15 + (100 + (100 - 100 * 1.1)) / 2, 62 | }) 63 | }); 64 | 65 | it('calculate the position of top left point', function () { 66 | expect(getTL({ 67 | x: 0, 68 | y: 0, 69 | width: 100, 70 | height: 100, 71 | scaleX: 1, 72 | scaleY: 1, 73 | angle: 0 74 | })).toEqual({ 75 | x: 0, 76 | y: 0 77 | }) 78 | }); 79 | 80 | 81 | it('calculate the position of top left point on scale', function () { 82 | expect(getTL({ 83 | x: 0, 84 | y: 0, 85 | width: 100, 86 | height: 100, 87 | scaleX: 1.1, 88 | scaleY: 1.1, 89 | angle: 0 90 | })).toEqual({ 91 | x: 0, 92 | y: 0 93 | }) 94 | }); 95 | 96 | it('calculate the position of top left point on scale and rotation', function () { 97 | expect(roundPoint(getTL({ 98 | x: 20, 99 | y: 20, 100 | width: 100, 101 | height: 100, 102 | scaleX: 1.1, 103 | scaleY: 1.1, 104 | angle: 90 105 | }))).toEqual({ 106 | x: Math.round(20 + (100 + (100 - 100 * 1.1))), 107 | y: 20 108 | }) 109 | }); 110 | 111 | 112 | it('calculate the position of top right point', function () { 113 | expect(getTR({ 114 | x: 0, 115 | y: 0, 116 | width: 100, 117 | height: 100, 118 | scaleX: 1, 119 | scaleY: 1, 120 | angle: 0 121 | })).toEqual({ 122 | x: 100, 123 | y: 0 124 | }) 125 | }); 126 | 127 | 128 | it('calculate the position of top right point on scale', function () { 129 | expect(roundPoint(getTR({ 130 | x: 0, 131 | y: 0, 132 | width: 100, 133 | height: 100, 134 | scaleX: 1.1, 135 | scaleY: 1.1, 136 | angle: 0 137 | }))).toEqual({ 138 | x: Math.round((100 - (100 - 100 * 1.1))), 139 | y: 0 140 | }) 141 | }); 142 | 143 | 144 | it('calculate the position of top right on scale and rotation', function () { 145 | const x = 0; 146 | const y = 0; 147 | const scaleX = 1.1 148 | const scaleY = 1.1 149 | const width = 100 150 | const height = 100 151 | const angle = 90 152 | 153 | const center = getCenter({ 154 | x, 155 | y, 156 | scaleX, 157 | scaleY, 158 | width, 159 | height 160 | }) 161 | 162 | const rad = angle * (Math.PI / 180) 163 | 164 | const xx = x + (width * scaleX) 165 | const yy = y 166 | 167 | expect(roundPoint(getTR({ 168 | x, 169 | y, 170 | width, 171 | height, 172 | scaleX, 173 | scaleY, 174 | angle 175 | }))).toEqual({ 176 | x: Math.round( 177 | (xx - center.x) * Math.cos(rad) - (yy - center.y) * Math.sin(rad) + center.x, 178 | ), 179 | y: Math.round( 180 | (xx - center.x) * Math.sin(rad) + (yy - center.y) * Math.cos(rad) + center.y 181 | ), 182 | }) 183 | }); 184 | 185 | 186 | it('calculate the position of Left Bottom point', function () { 187 | expect(getBL({ 188 | x: 0, 189 | y: 0, 190 | width: 100, 191 | height: 100, 192 | scaleX: 1, 193 | scaleY: 1, 194 | angle: 0 195 | })).toEqual({ 196 | x: 0, 197 | y: 100 198 | }) 199 | }) 200 | 201 | it('calculate the position of Left Bottom point on scale', function () { 202 | expect(roundPoint(getBL({ 203 | x: 0, 204 | y: 0, 205 | width: 100, 206 | height: 100, 207 | scaleX: 1.1, 208 | scaleY: 1.1, 209 | angle: 0 210 | }))).toEqual({ 211 | x: 0, 212 | y: Math.round(20 + (100 + (100 - 100 * 1.1))), 213 | }) 214 | }) 215 | 216 | it('calculate the position of Left Bottom point on scale and angle', function () { 217 | 218 | const x = 0; 219 | const y = 0; 220 | const scaleX = 1.1 221 | const scaleY = 1.1 222 | const width = 100 223 | const height = 100 224 | const angle = 90 225 | 226 | const center = getCenter({ 227 | x, 228 | y, 229 | scaleX, 230 | scaleY, 231 | width, 232 | height 233 | }) 234 | 235 | const rad = angle * (Math.PI / 180) 236 | 237 | const yy = y + (height * scaleY); 238 | 239 | expect(roundPoint(getBL({ 240 | x, 241 | y, 242 | width, 243 | height, 244 | scaleX, 245 | scaleY, 246 | angle 247 | }))).toEqual({ 248 | x: Math.round( 249 | (x - center.x) * Math.cos(rad) - (yy - center.y) * Math.sin(rad) + center.x, 250 | ), 251 | y: Math.round( 252 | (x - center.x) * Math.sin(rad) + (yy - center.y) * Math.cos(rad) + center.y 253 | ), 254 | }) 255 | }) 256 | 257 | it('calculate the position of middle right point', function () { 258 | const x = 0; 259 | const y = 0; 260 | const scaleX = 1.1 261 | const scaleY = 1.1 262 | const width = 100 263 | const height = 100 264 | const angle = 90 265 | 266 | const center = getCenter({ 267 | x, 268 | y, 269 | scaleX, 270 | scaleY, 271 | width, 272 | height 273 | }) 274 | 275 | const rad = angle * (Math.PI / 180) 276 | 277 | const xx = x + (width * scaleX) 278 | const yy = y + (height * scaleY) / 2 279 | 280 | expect(roundPoint(getMR({ 281 | x, 282 | y, 283 | width, 284 | height, 285 | scaleX, 286 | scaleY, 287 | angle 288 | }))).toEqual({ 289 | x: Math.round( 290 | (xx - center.x) * Math.cos(rad) - (yy - center.y) * Math.sin(rad) + center.x, 291 | ), 292 | y: Math.round( 293 | (xx - center.x) * Math.sin(rad) + (yy - center.y) * Math.cos(rad) + center.y 294 | ), 295 | }) 296 | 297 | }) 298 | 299 | it('calculate the position of middle bottom point', function () { 300 | const x = 0; 301 | const y = 0; 302 | const scaleX = 1.1 303 | const scaleY = 1.1 304 | const width = 100 305 | const height = 100 306 | const angle = 90 307 | 308 | const center = getCenter({ 309 | x, 310 | y, 311 | scaleX, 312 | scaleY, 313 | width, 314 | height 315 | }) 316 | 317 | const rad = angle * (Math.PI / 180) 318 | 319 | const xx = x + (width * scaleX) / 2 320 | const yy = y + (height * scaleY) 321 | 322 | expect(roundPoint(getBM({ 323 | x, 324 | y, 325 | width, 326 | height, 327 | scaleX, 328 | scaleY, 329 | angle 330 | }))).toEqual({ 331 | x: Math.round( 332 | (xx - center.x) * Math.cos(rad) - (yy - center.y) * Math.sin(rad) + center.x, 333 | ), 334 | y: Math.round( 335 | (xx - center.x) * Math.sin(rad) + (yy - center.y) * Math.cos(rad) + center.y 336 | ), 337 | }) 338 | }) 339 | 340 | it('calculate the position of middle top point', function () { 341 | const x = 0; 342 | const y = 0; 343 | const scaleX = 1.1 344 | const scaleY = 1.1 345 | const width = 100 346 | const height = 100 347 | const angle = 90 348 | 349 | const center = getCenter({ 350 | x, 351 | y, 352 | scaleX, 353 | scaleY, 354 | width, 355 | height 356 | }) 357 | 358 | const rad = angle * (Math.PI / 180) 359 | 360 | const xx = x + (width * scaleX) / 2 361 | const yy = y 362 | 363 | expect(roundPoint(getTM({ 364 | x, 365 | y, 366 | width, 367 | height, 368 | scaleX, 369 | scaleY, 370 | angle 371 | }))).toEqual({ 372 | x: Math.round( 373 | (xx - center.x) * Math.cos(rad) - (yy - center.y) * Math.sin(rad) + center.x, 374 | ), 375 | y: Math.round( 376 | (xx - center.x) * Math.sin(rad) + (yy - center.y) * Math.cos(rad) + center.y 377 | ), 378 | }) 379 | }) 380 | it('calculate the position of middle left point', function () { 381 | const x = 0; 382 | const y = 0; 383 | const scaleX = 1.1 384 | const scaleY = 1.1 385 | const width = 100 386 | const height = 100 387 | const angle = 90 388 | 389 | const center = getCenter({ 390 | x, 391 | y, 392 | scaleX, 393 | scaleY, 394 | width, 395 | height 396 | }) 397 | 398 | const rad = angle * (Math.PI / 180) 399 | 400 | const xx = x 401 | const yy = y + (height * scaleY) / 2 402 | 403 | expect(roundPoint(getML({ 404 | x, 405 | y, 406 | width, 407 | height, 408 | scaleX, 409 | scaleY, 410 | angle 411 | }))).toEqual({ 412 | x: Math.round( 413 | (xx - center.x) * Math.cos(rad) - (yy - center.y) * Math.sin(rad) + center.x, 414 | ), 415 | y: Math.round( 416 | (xx - center.x) * Math.sin(rad) + (yy - center.y) * Math.cos(rad) + center.y 417 | ), 418 | }) 419 | }) 420 | 421 | it('calculate the position of bottom right point', function () { 422 | const x = 0; 423 | const y = 0; 424 | const scaleX = 1.1 425 | const scaleY = 1.1 426 | const width = 100 427 | const height = 100 428 | const angle = 90 429 | 430 | const center = getCenter({ 431 | x, 432 | y, 433 | scaleX, 434 | scaleY, 435 | width, 436 | height 437 | }) 438 | 439 | const rad = angle * (Math.PI / 180) 440 | 441 | const xx = x + width * scaleX 442 | const yy = y + height * scaleY 443 | 444 | expect(roundPoint(getBR({ 445 | x, 446 | y, 447 | width, 448 | height, 449 | scaleX, 450 | scaleY, 451 | angle 452 | }))).toEqual({ 453 | x: Math.round( 454 | (xx - center.x) * Math.cos(rad) - (yy - center.y) * Math.sin(rad) + center.x, 455 | ), 456 | y: Math.round( 457 | (xx - center.x) * Math.sin(rad) + (yy - center.y) * Math.cos(rad) + center.y 458 | ), 459 | }) 460 | }) 461 | 462 | 463 | it('the opposite of TL is BR', function () { 464 | const props = { 465 | x: 0, 466 | y: 0, 467 | width: 100, 468 | height: 100, 469 | scaleX: 1.1, 470 | scaleY: 1.1, 471 | angle: 90 472 | } 473 | expect(getOppositePoint('tl', props)).toEqual(getPoint('br', props)) 474 | }) 475 | 476 | it('the opposite of ML is MR', function () { 477 | const props = { 478 | x: 0, 479 | y: 0, 480 | width: 100, 481 | height: 100, 482 | scaleX: 1.1, 483 | scaleY: 1.1, 484 | angle: 90 485 | } 486 | expect(getOppositePoint('ml', props)).toEqual(getPoint('mr', props)) 487 | }) 488 | 489 | it('the opposite of TR is BL', function () { 490 | const props = { 491 | x: 0, 492 | y: 0, 493 | width: 100, 494 | height: 100, 495 | scaleX: 1.1, 496 | scaleY: 1.1, 497 | angle: 90 498 | } 499 | expect(getOppositePoint('tr', props)).toEqual(getPoint('bl', props)) 500 | }) 501 | 502 | it('the opposite of BM is TM', function () { 503 | const props = { 504 | x: 0, 505 | y: 0, 506 | width: 100, 507 | height: 100, 508 | scaleX: 1.1, 509 | scaleY: 1.1, 510 | angle: 90 511 | } 512 | expect(getOppositePoint('bm', props)).toEqual(getPoint('tm', props)) 513 | }) 514 | 515 | it('the opposite of BR is TL', function () { 516 | const props = { 517 | x: 0, 518 | y: 0, 519 | width: 100, 520 | height: 100, 521 | scaleX: 1.1, 522 | scaleY: 1.1, 523 | angle: 90 524 | } 525 | expect(getOppositePoint('br', props)).toEqual(getPoint('tl', props)) 526 | }) 527 | 528 | it('the opposite of MR is ML', function () { 529 | const props = { 530 | x: 0, 531 | y: 0, 532 | width: 100, 533 | height: 100, 534 | scaleX: 1.1, 535 | scaleY: 1.1, 536 | angle: 90 537 | } 538 | expect(getOppositePoint('mr', props)).toEqual(getPoint('ml', props)) 539 | }) 540 | 541 | it('[tl] has [getTL] caller] ', function () { 542 | const props = { 543 | x: 0, 544 | y: 0, 545 | width: 100, 546 | height: 100, 547 | scaleX: 1, 548 | scaleY: 1, 549 | angle: 0 550 | } 551 | expect(getPoint('tl', props)).toEqual(getTL(props)) 552 | }) 553 | 554 | it('[ml] has [getML] caller] ', function () { 555 | const props = { 556 | x: 0, 557 | y: 0, 558 | width: 100, 559 | height: 100, 560 | scaleX: 1, 561 | scaleY: 1, 562 | angle: 0 563 | } 564 | expect(getPoint('ml', props)).toEqual(getML(props)) 565 | }) 566 | 567 | it('[tr] has [getTR] caller] ', function () { 568 | const props = { 569 | x: 0, 570 | y: 0, 571 | width: 100, 572 | height: 100, 573 | scaleX: 1, 574 | scaleY: 1, 575 | angle: 0 576 | } 577 | expect(getPoint('tr', props)).toEqual(getTR(props)) 578 | }) 579 | 580 | it('[tm] has [getTM] caller] ', function () { 581 | const props = { 582 | x: 0, 583 | y: 0, 584 | width: 100, 585 | height: 100, 586 | scaleX: 1, 587 | scaleY: 1, 588 | angle: 0 589 | } 590 | expect(getPoint('tm', props)).toEqual(getTM(props)) 591 | }) 592 | 593 | it('[bl] has [getBL] caller] ', function () { 594 | const props = { 595 | x: 0, 596 | y: 0, 597 | width: 100, 598 | height: 100, 599 | scaleX: 1, 600 | scaleY: 1, 601 | angle: 0 602 | } 603 | expect(getPoint('bl', props)).toEqual(getBL(props)) 604 | }) 605 | 606 | it('[bm] has [getBM] caller] ', function () { 607 | const props = { 608 | x: 0, 609 | y: 0, 610 | width: 100, 611 | height: 100, 612 | scaleX: 1, 613 | scaleY: 1, 614 | angle: 0 615 | } 616 | expect(getPoint('bm', props)).toEqual(getBM(props)) 617 | }) 618 | 619 | it('[br] has [getBR] caller] ', function () { 620 | const props = { 621 | x: 0, 622 | y: 0, 623 | width: 100, 624 | height: 100, 625 | scaleX: 1, 626 | scaleY: 1, 627 | angle: 0 628 | } 629 | expect(getPoint('br', props)).toEqual(getBR(props)) 630 | }) 631 | 632 | it('[mr] has [getMR] caller] ', function () { 633 | const props = { 634 | x: 0, 635 | y: 0, 636 | width: 100, 637 | height: 100, 638 | scaleX: 1, 639 | scaleY: 1, 640 | angle: 0 641 | } 642 | expect(getPoint('mr', props)).toEqual(getMR(props)) 643 | }) 644 | 645 | it('sine an cosine for tl', function () { 646 | const angle = 90 647 | const {sin, cos} = getSineCosine('tl', angle) 648 | 649 | expect(sin).toBe(Math.sin(angle * (Math.PI / 180))) 650 | expect(cos).toBe(Math.cos(angle * (Math.PI / 180))) 651 | }) 652 | 653 | it('sine an cosine for ml', function () { 654 | const angle = 90 655 | const {sin, cos} = getSineCosine('ml', angle) 656 | 657 | expect(sin).toBe(Math.sin(angle * (Math.PI / 180))) 658 | expect(cos).toBe(Math.cos(angle * (Math.PI / 180))) 659 | }) 660 | 661 | it('sine an cosine for tr', function () { 662 | const angle = 90 663 | const {sin, cos} = getSineCosine('tr', angle) 664 | 665 | expect(sin).toBe(Math.sin(-angle * (Math.PI / 180))) 666 | expect(cos).toBe(Math.cos(-angle * (Math.PI / 180))) 667 | }) 668 | 669 | it('sine an cosine for tm', function () { 670 | const angle = 90 671 | const {sin, cos} = getSineCosine('tm', angle) 672 | 673 | expect(sin).toBe(Math.sin(-angle * (Math.PI / 180))) 674 | expect(cos).toBe(Math.cos(-angle * (Math.PI / 180))) 675 | }) 676 | 677 | it('sine an cosine for bl', function () { 678 | const angle = 90 679 | const {sin, cos} = getSineCosine('bl', angle) 680 | 681 | expect(sin).toBe(Math.sin(-angle * (Math.PI / 180))) 682 | expect(cos).toBe(Math.cos(-angle * (Math.PI / 180))) 683 | }) 684 | 685 | it('sine an cosine for bm', function () { 686 | const angle = 90 687 | const {sin, cos} = getSineCosine('bm', angle) 688 | 689 | expect(sin).toBe(Math.sin(-angle * (Math.PI / 180))) 690 | expect(cos).toBe(Math.cos(-angle * (Math.PI / 180))) 691 | }) 692 | 693 | it('sine an cosine for br', function () { 694 | const angle = 90 695 | const {sin, cos} = getSineCosine('br', angle) 696 | 697 | expect(sin).toBe(Math.sin(angle * (Math.PI / 180))) 698 | expect(cos).toBe(Math.cos(angle * (Math.PI / 180))) 699 | }) 700 | 701 | it('sine an cosine for mr', function () { 702 | const angle = 90 703 | const {sin, cos} = getSineCosine('mr', angle) 704 | 705 | expect(sin).toBe(Math.sin(angle * (Math.PI / 180))) 706 | expect(cos).toBe(Math.cos(angle * (Math.PI / 180))) 707 | }) 708 | 709 | 710 | it('move point for tl', function () { 711 | 712 | const props = { 713 | x: 0, 714 | y: 0, 715 | width: 100, 716 | height: 100, 717 | scaleX: 1, 718 | scaleY: 1, 719 | angle: 0 720 | } 721 | 722 | const point = getOppositePoint('tl', props); 723 | const oppositePoint = getOppositePoint('tl', props); 724 | const moveDiff = { 725 | x: 5, 726 | y: 5 727 | } 728 | 729 | expect(roundPoint( 730 | getMovePoint('tl', point, oppositePoint, moveDiff) 731 | )).toEqual({ 732 | x: -5, 733 | y: -5, 734 | }) 735 | }) 736 | 737 | it('move point for ml', function () { 738 | const props = { 739 | x: 0, 740 | y: 0, 741 | width: 100, 742 | height: 100, 743 | scaleX: 1, 744 | scaleY: 1, 745 | angle: 0 746 | } 747 | 748 | const point = getOppositePoint('ml', props); 749 | const oppositePoint = getOppositePoint('ml', props); 750 | const moveDiff = { 751 | x: 5, 752 | y: 5 753 | } 754 | 755 | expect(roundPoint( 756 | getMovePoint('ml', point, oppositePoint, moveDiff) 757 | )).toEqual({ 758 | x: -5, 759 | y: -5, 760 | }) 761 | }) 762 | 763 | it('move point for tr', function () { 764 | const props = { 765 | x: 0, 766 | y: 0, 767 | width: 100, 768 | height: 100, 769 | scaleX: 1, 770 | scaleY: 1, 771 | angle: 0 772 | } 773 | 774 | const point = getOppositePoint('tr', props); 775 | const oppositePoint = getOppositePoint('tr', props); 776 | const moveDiff = { 777 | x: 5, 778 | y: 5 779 | } 780 | 781 | expect(roundPoint( 782 | getMovePoint('tr', point, oppositePoint, moveDiff) 783 | )).toEqual({ 784 | x: 5, 785 | y: -5, 786 | }) 787 | }) 788 | 789 | 790 | it('move point for tm', function () { 791 | const props = { 792 | x: 0, 793 | y: 0, 794 | width: 100, 795 | height: 100, 796 | scaleX: 1, 797 | scaleY: 1, 798 | angle: 0 799 | } 800 | 801 | const point = getOppositePoint('tm', props); 802 | const oppositePoint = getOppositePoint('tm', props); 803 | const moveDiff = { 804 | x: 5, 805 | y: 5 806 | } 807 | 808 | expect(roundPoint( 809 | getMovePoint('tm', point, oppositePoint, moveDiff) 810 | )).toEqual({ 811 | x: 5, 812 | y: -5, 813 | }) 814 | }) 815 | 816 | it('move point for mr', function () { 817 | const props = { 818 | x: 0, 819 | y: 0, 820 | width: 100, 821 | height: 100, 822 | scaleX: 1, 823 | scaleY: 1, 824 | angle: 0 825 | } 826 | 827 | const point = getOppositePoint('mr', props); 828 | const oppositePoint = getOppositePoint('mr', props); 829 | const moveDiff = { 830 | x: 5, 831 | y: 5 832 | } 833 | 834 | expect(roundPoint( 835 | getMovePoint('mr', point, oppositePoint, moveDiff) 836 | )).toEqual({ 837 | x: 5, 838 | y: 5, 839 | }) 840 | }) 841 | 842 | 843 | it('move point for br', function () { 844 | const props = { 845 | x: 0, 846 | y: 0, 847 | width: 100, 848 | height: 100, 849 | scaleX: 1, 850 | scaleY: 1, 851 | angle: 0 852 | } 853 | 854 | const point = getOppositePoint('br', props); 855 | const oppositePoint = getOppositePoint('br', props); 856 | const moveDiff = { 857 | x: 5, 858 | y: 5 859 | } 860 | 861 | expect(roundPoint( 862 | getMovePoint('br', point, oppositePoint, moveDiff) 863 | )).toEqual({ 864 | x: 5, 865 | y: 5, 866 | }) 867 | }) 868 | 869 | 870 | it('move point for bl', function () { 871 | const props = { 872 | x: 0, 873 | y: 0, 874 | width: 100, 875 | height: 100, 876 | scaleX: 1, 877 | scaleY: 1, 878 | angle: 0 879 | } 880 | 881 | const point = getOppositePoint('bl', props); 882 | const oppositePoint = getOppositePoint('bl', props); 883 | const moveDiff = { 884 | x: 5, 885 | y: 5 886 | } 887 | 888 | expect(roundPoint( 889 | getMovePoint('bl', point, oppositePoint, moveDiff) 890 | )).toEqual({ 891 | x: -5, 892 | y: 5, 893 | }) 894 | }) 895 | 896 | it('it find the min and max position', function () { 897 | const props = { 898 | x: 0, 899 | y: 0, 900 | width: 100, 901 | height: 100, 902 | scaleX: 1, 903 | scaleY: 1, 904 | angle: 0 905 | } 906 | 907 | expect(minMax(props)).toEqual({ 908 | xmax: 100, 909 | ymax: 100, 910 | xmin: 0, 911 | ymin: 0, 912 | }) 913 | }) 914 | -------------------------------------------------------------------------------- /__tests__/rotate.test.js: -------------------------------------------------------------------------------- 1 | import {rotate} from '../src' 2 | 3 | const roundPayload = ({angle}) => { 4 | return { 5 | angle:Math.round(angle) 6 | } 7 | } 8 | 9 | 10 | it("rotate element", function () { 11 | const state = { 12 | startX: 0, 13 | startY: 0, 14 | offsetX: 0, 15 | offsetY: 0, 16 | x: 0, 17 | y: 0, 18 | scaleX: 1, 19 | scaleY: 1, 20 | width: 100, 21 | height: 100, 22 | angle: 0, 23 | scaleLimit: 0.1, 24 | }; 25 | rotate(state, (payload) => { 26 | expect(roundPayload(payload)).toEqual({ 27 | angle:84, 28 | }) 29 | })({ 30 | pageX: 90, 31 | pageY: 0 32 | }) 33 | }) 34 | 35 | 36 | it("rotate element with shift key", function () { 37 | const state = { 38 | startX: 0, 39 | startY: 0, 40 | offsetX: 0, 41 | offsetY: 0, 42 | x: 0, 43 | y: 0, 44 | scaleX: 1, 45 | scaleY: 1, 46 | width: 100, 47 | height: 100, 48 | angle: 0, 49 | scaleLimit: 0.1, 50 | }; 51 | rotate(state, (payload) => { 52 | expect(payload).toEqual({ 53 | angle: 75 54 | }) 55 | })({ 56 | pageX: 90, 57 | pageY: 0, 58 | shiftKey:true 59 | }) 60 | }) -------------------------------------------------------------------------------- /__tests__/scale.test.js: -------------------------------------------------------------------------------- 1 | import {scale} from '../src' 2 | 3 | const roundPayload = ({x, y, scaleX, scaleY}) => { 4 | const precision = 2 5 | return { 6 | x: x.toFixed(precision), 7 | y: y.toFixed(precision), 8 | scaleX: scaleX.toFixed(precision), 9 | scaleY: scaleY.toFixed(precision), 10 | } 11 | } 12 | 13 | it('scale tl', () => { 14 | const state = { 15 | startX: 0, 16 | startY: 0, 17 | x: 0, 18 | y: 0, 19 | scaleX: 1, 20 | scaleY: 1, 21 | width: 100, 22 | height: 100, 23 | angle: 45, 24 | scaleLimit: 0.1, 25 | scaleFromCenter: false 26 | }; 27 | 28 | scale('tl', state, (payload) => { 29 | expect(roundPayload(payload)).toEqual({ 30 | scaleX: "1.14", 31 | scaleY: "1.00", 32 | x: "2.07", 33 | y: "-5.00", 34 | }) 35 | })({ 36 | pageX: -10, 37 | pageY: -10 38 | }) 39 | 40 | }) 41 | 42 | 43 | it('scale tl from center', () => { 44 | const state = { 45 | startX: 0, 46 | startY: 0, 47 | x: 0, 48 | y: 0, 49 | scaleX: 1, 50 | scaleY: 1, 51 | width: 100, 52 | height: 100, 53 | angle: 45, 54 | scaleLimit: 0.1, 55 | scaleFromCenter: true 56 | }; 57 | 58 | scale('tl', state, (payload) => { 59 | expect(roundPayload(payload)).toEqual({ 60 | scaleX: "1.28", 61 | scaleY: "1.00", 62 | x: "14.14", 63 | y: "0.00", 64 | }) 65 | })({ 66 | pageX: -10, 67 | pageY: -10, 68 | altKey: true 69 | }) 70 | 71 | }) 72 | // same to release resizing from centers/ 73 | // point position will be reset 74 | it('scale tl, activate from center while resizing', () => { 75 | const state = { 76 | startX: 0, 77 | startY: 0, 78 | x: 0, 79 | y: 0, 80 | scaleX: 1, 81 | scaleY: 1, 82 | width: 100, 83 | height: 100, 84 | angle: 45, 85 | scaleLimit: 0.1, 86 | scaleFromCenter: false 87 | }; 88 | 89 | scale('tl', state, (payload) => { 90 | expect(roundPayload(payload)).toEqual({ 91 | scaleX: "1.00", 92 | scaleY: "1.00", 93 | x: "0.00", 94 | y: "0.00", 95 | }) 96 | })({ 97 | pageX: -10, 98 | pageY: -10, 99 | altKey: true 100 | }) 101 | 102 | }) 103 | 104 | // reset startX and startY since there are no multiple movements allowed in test 105 | // element will not resized, but resetting position proofed 106 | 107 | it('scale tl with release shift while resizing', () => { 108 | const state = { 109 | startX: 0, 110 | startY: 0, 111 | x: 0, 112 | y: 0, 113 | scaleX: 1, 114 | scaleY: 1, 115 | width: 100, 116 | height: 100, 117 | angle: 45, 118 | scaleLimit: 0.1, 119 | scaleFromCenter: true 120 | }; 121 | scale('tl', state, (payload) => { 122 | expect(roundPayload(payload)).toEqual({ 123 | scaleX: "1.00", 124 | scaleY: "1.00", 125 | x: "0.00", 126 | y: "0.00", 127 | }) 128 | })({ 129 | pageX: -10, 130 | pageY: -10, 131 | altKey:false 132 | }) 133 | }) 134 | 135 | it('scale tl from with aspect ratio', () => { 136 | const state = { 137 | startX: 0, 138 | startY: 0, 139 | x: 0, 140 | y: 0, 141 | scaleX: 1, 142 | scaleY: 1, 143 | width: 100, 144 | height: 100, 145 | angle: 45, 146 | scaleLimit: 0.1, 147 | scaleFromCenter: false, 148 | aspectRatio: true 149 | }; 150 | 151 | scale('tl', state, (payload) => { 152 | expect(roundPayload(payload)).toEqual({ 153 | scaleX: "1.14", 154 | scaleY: "1.14", 155 | x: "7.07", 156 | y: "-2.93", 157 | }) 158 | })({ 159 | pageX: -10, 160 | pageY: -10, 161 | shiftKey: true 162 | }) 163 | 164 | }) 165 | 166 | 167 | 168 | it('scale tl with disable aspect ratio while resizing', () => { 169 | const state = { 170 | startX: 0, 171 | startY: 0, 172 | x: 0, 173 | y: 0, 174 | scaleX: 1, 175 | scaleY: 1, 176 | width: 100, 177 | height: 100, 178 | angle: 45, 179 | scaleLimit: 0.1, 180 | scaleFromCenter: false, 181 | aspectRatio: true 182 | }; 183 | 184 | scale('tl', state, (payload) => { 185 | expect(roundPayload(payload)).toEqual({ 186 | scaleX: "1.14", 187 | scaleY: "1.00", 188 | x: "2.07", 189 | y: "-5.00", 190 | }) 191 | })({ 192 | pageX: -10, 193 | pageY: -10, 194 | shiftKey: false 195 | }) 196 | 197 | }) 198 | 199 | 200 | it('scale tl from with enable aspect ratio on resizing', () => { 201 | const state = { 202 | startX: 0, 203 | startY: 0, 204 | x: 0, 205 | y: 0, 206 | scaleX: 1, 207 | scaleY: 1, 208 | width: 100, 209 | height: 100, 210 | angle: 45, 211 | scaleLimit: 0.1, 212 | scaleFromCenter: false, 213 | aspectRatio: false 214 | }; 215 | 216 | scale('tl', state, (payload) => { 217 | expect(roundPayload(payload)).toEqual({ 218 | scaleX: "1.14", 219 | scaleY: "1.14", 220 | x: "7.07", 221 | y: "-2.93", 222 | }) 223 | })({ 224 | pageX: -10, 225 | pageY: -10, 226 | shiftKey: true 227 | }) 228 | 229 | }) 230 | 231 | 232 | it('scale tl from center with aspect ratio', () => { 233 | const state = { 234 | startX: 0, 235 | startY: 0, 236 | x: 0, 237 | y: 0, 238 | scaleX: 1, 239 | scaleY: 1, 240 | width: 100, 241 | height: 100, 242 | angle: 45, 243 | scaleLimit: 0.1, 244 | scaleFromCenter: true, 245 | aspectRatio: true 246 | }; 247 | 248 | scale('tl', state, (payload) => { 249 | expect(roundPayload(payload)).toEqual({ 250 | scaleX: "1.28", 251 | scaleY: "1.28", 252 | x: "14.14", 253 | y: "14.14", 254 | }) 255 | })({ 256 | pageX: -10, 257 | pageY: -10, 258 | altKey: true, 259 | shiftKey: true 260 | }) 261 | }) 262 | 263 | it('scale bl', () => { 264 | const state = { 265 | startX: 0, 266 | startY: 0, 267 | x: 0, 268 | y: 0, 269 | scaleX: 1, 270 | scaleY: 1, 271 | width: 100, 272 | height: 100, 273 | angle: 45, 274 | scaleLimit: 0.1, 275 | scaleFromCenter: false 276 | }; 277 | 278 | scale('bl', state, (payload) => { 279 | expect(roundPayload(payload)).toEqual({ 280 | scaleX: "1.14", 281 | scaleY: "1.00", 282 | x: "2.07", 283 | y: "-5.00", 284 | }) 285 | })({ 286 | pageX: -10, 287 | pageY: -10 288 | }) 289 | }) 290 | 291 | it('scale ml', () => { 292 | const state = { 293 | startX: 0, 294 | startY: 0, 295 | x: 0, 296 | y: 0, 297 | scaleX: 1, 298 | scaleY: 1, 299 | width: 100, 300 | height: 100, 301 | angle: 45, 302 | scaleLimit: 0.1, 303 | scaleFromCenter: false 304 | }; 305 | 306 | scale('ml', state, (payload) => { 307 | expect(roundPayload(payload)).toEqual({ 308 | scaleX: "1.21", 309 | scaleY: "1.00", 310 | x: "3.11", 311 | y: "-7.50", 312 | }) 313 | })({ 314 | pageX: -15, 315 | pageY: -15 316 | }) 317 | }) 318 | 319 | it('scale ml with aspect ratio', () => { 320 | const state = { 321 | startX: 0, 322 | startY: 0, 323 | x: 0, 324 | y: 0, 325 | scaleX: 1, 326 | scaleY: 1, 327 | width: 100, 328 | height: 100, 329 | angle: 45, 330 | scaleLimit: 0.1, 331 | aspectRatio: true 332 | }; 333 | 334 | scale('ml', state, (payload) => { 335 | expect(roundPayload(payload)).toEqual({ 336 | scaleX: "1.21", 337 | scaleY: "1.21", 338 | x: "3.11", 339 | y: "3.11", 340 | }) 341 | })({ 342 | pageX: -15, 343 | pageY: -15, 344 | shiftKey: true 345 | }) 346 | }) 347 | 348 | 349 | it('scale tr', () => { 350 | const state = { 351 | startX: 100, 352 | startY: 100, 353 | x: 0, 354 | y: 0, 355 | scaleX: 1, 356 | scaleY: 1, 357 | width: 100, 358 | height: 100, 359 | angle: 45, 360 | scaleLimit: 0.1, 361 | scaleFromCenter: false 362 | }; 363 | 364 | scale('tr', state, (payload) => { 365 | expect(roundPayload(payload)).toEqual({ 366 | scaleX: "1.21", 367 | scaleY: "1.00", 368 | x: "18.11", 369 | y: "7.50", 370 | }) 371 | })({ 372 | pageX: 100 + 15, 373 | pageY: 100 + 15 374 | }) 375 | }) 376 | 377 | 378 | it('scale tm', () => { 379 | const state = { 380 | startX: 0, 381 | startY: 0, 382 | x: 0, 383 | y: 0, 384 | scaleX: 1, 385 | scaleY: 1, 386 | width: 100, 387 | height: 100, 388 | angle: 45, 389 | scaleLimit: 0.1, 390 | scaleFromCenter: false 391 | }; 392 | 393 | scale('tm', state, (payload) => { 394 | expect(roundPayload(payload)).toEqual({ 395 | scaleX: "1.00", 396 | scaleY: "1.11", 397 | x: "3.75", 398 | y: "1.55", 399 | }) 400 | })({ 401 | pageX: 0, 402 | pageY: -15 403 | }) 404 | }) 405 | 406 | 407 | it('scale tm with aspect ratio', () => { 408 | const state = { 409 | startX: 0, 410 | startY: 0, 411 | x: 0, 412 | y: 0, 413 | scaleX: 1, 414 | scaleY: 1, 415 | width: 100, 416 | height: 100, 417 | angle: 45, 418 | scaleLimit: 0.1, 419 | aspectRatio: true 420 | }; 421 | 422 | scale('tm', state, (payload) => { 423 | expect(roundPayload(payload)).toEqual({ 424 | scaleX: "1.11", 425 | scaleY: "1.11", 426 | x: "9.05", 427 | y: "1.55", 428 | }) 429 | })({ 430 | pageX: 0, 431 | pageY: -15, 432 | shiftKey: true 433 | }) 434 | }) 435 | 436 | 437 | it('scale bm', () => { 438 | const state = { 439 | startX: 0, 440 | startY: 0, 441 | x: 0, 442 | y: 0, 443 | scaleX: 1, 444 | scaleY: 1, 445 | width: 100, 446 | height: 100, 447 | angle: 45, 448 | scaleLimit: 0.1, 449 | scaleFromCenter: false 450 | }; 451 | 452 | scale('bm', state, (payload) => { 453 | expect(roundPayload(payload)).toEqual({ 454 | scaleX: "1.00", 455 | scaleY: "1.11", 456 | x: "-3.75", 457 | y: "9.05", 458 | }) 459 | })({ 460 | pageX: 0, 461 | pageY: 15 462 | }) 463 | }) 464 | 465 | 466 | it('scale bm with aspect ratio', () => { 467 | const state = { 468 | startX: 0, 469 | startY: 0, 470 | x: 0, 471 | y: 0, 472 | scaleX: 1, 473 | scaleY: 1, 474 | width: 100, 475 | height: 100, 476 | angle: 45, 477 | scaleLimit: 0.1, 478 | aspectRatio: true 479 | }; 480 | 481 | scale('bm', state, (payload) => { 482 | expect(roundPayload(payload)).toEqual({ 483 | scaleX: "1.11", 484 | scaleY: "1.11", 485 | x: "1.55", 486 | y: "9.05", 487 | }) 488 | })({ 489 | pageX: 0, 490 | pageY: 15, 491 | shiftKey: true 492 | }) 493 | }) 494 | 495 | 496 | it('scale br', () => { 497 | const state = { 498 | startX: 0, 499 | startY: 0, 500 | x: 0, 501 | y: 0, 502 | scaleX: 1, 503 | scaleY: 1, 504 | width: 100, 505 | height: 100, 506 | angle: 45, 507 | scaleLimit: 0.1, 508 | scaleFromCenter: false 509 | }; 510 | 511 | scale('br', state, (payload) => { 512 | expect(roundPayload(payload)).toEqual({ 513 | scaleX: "1.21", 514 | scaleY: "1.00", 515 | x: "18.11", 516 | y: "7.50", 517 | }) 518 | })({ 519 | pageX: 15, 520 | pageY: 15 521 | }) 522 | }) 523 | 524 | it('scale mr', () => { 525 | const state = { 526 | startX: 0, 527 | startY: 0, 528 | x: 0, 529 | y: 0, 530 | scaleX: 1, 531 | scaleY: 1, 532 | width: 100, 533 | height: 100, 534 | angle: 45, 535 | scaleLimit: 0.1, 536 | scaleFromCenter: false 537 | }; 538 | 539 | scale('mr', state, (payload) => { 540 | expect(roundPayload(payload)).toEqual({ 541 | scaleX: "1.21", 542 | scaleY: "1.00", 543 | x: "18.11", 544 | y: "7.50", 545 | }) 546 | })({ 547 | pageX: 15, 548 | pageY: 15, 549 | 550 | }) 551 | }) 552 | 553 | 554 | it('scale mr with aspect ratio', () => { 555 | const state = { 556 | startX: 0, 557 | startY: 0, 558 | x: 0, 559 | y: 0, 560 | scaleX: 1, 561 | scaleY: 1, 562 | width: 100, 563 | height: 100, 564 | angle: 45, 565 | scaleLimit: 0.1, 566 | aspectRatio: true 567 | }; 568 | 569 | scale('mr', state, (payload) => { 570 | expect(roundPayload(payload)).toEqual({ 571 | scaleX: "1.21", 572 | scaleY: "1.21", 573 | x: "18.11", 574 | y: "18.11", 575 | }) 576 | })({ 577 | pageX: 15, 578 | pageY: 15, 579 | shiftKey: true 580 | }) 581 | }) 582 | 583 | -------------------------------------------------------------------------------- /__tests__/styler.js: -------------------------------------------------------------------------------- 1 | import {styler} from '../src' 2 | 3 | import { 4 | scale, 5 | rotate, 6 | translate, 7 | transform, 8 | toCSS 9 | } from 'transformation-matrix'; 10 | 11 | it('it return basic styles', () => { 12 | 13 | const results = styler({ 14 | x:0, 15 | y:0, 16 | angle:0, 17 | scaleX:1, 18 | scaleY:1, 19 | width:100, 20 | height:100 21 | }) 22 | 23 | expect(results.element).toEqual({ 24 | width:100, 25 | height:100, 26 | position:"absolute", 27 | transform:toCSS(transform( 28 | translate(0,0), 29 | scale(1,1) 30 | )) 31 | }) 32 | expect(results.controls).toEqual({ 33 | width:100, 34 | height:100, 35 | position:"absolute", 36 | transform:toCSS(transform( 37 | translate(0,0), 38 | scale(1,1) 39 | )) 40 | }) 41 | }) 42 | 43 | it('it return complex matrix styles', () => { 44 | 45 | const results = styler({ 46 | x:0, 47 | y:0, 48 | angle:90, 49 | scaleX:1, 50 | scaleY:1, 51 | width:100, 52 | height:100 53 | }) 54 | 55 | expect(results.element).toEqual({ 56 | width:100, 57 | height:100, 58 | position:"absolute", 59 | transform:toCSS( transform( 60 | translate(0,0), 61 | rotate(90 * (Math.PI / 180)), 62 | scale(1,1) 63 | )) 64 | }) 65 | 66 | expect(results.controls).toEqual({ 67 | width:100, 68 | height:100, 69 | position:"absolute",transform:toCSS( transform( 70 | translate(0,0), 71 | rotate(90 * (Math.PI / 180)), 72 | scale(1,1) 73 | )) 74 | }) 75 | }) 76 | 77 | it('it return styles when disableScale is true', () => { 78 | 79 | const results = styler({ 80 | x:0, 81 | y:0, 82 | angle:0, 83 | scaleX:2, 84 | scaleY:2, 85 | width:100, 86 | height:100, 87 | disableScale:true 88 | }) 89 | 90 | expect(results.element).toEqual({ 91 | width:200, 92 | height:200, 93 | position:"absolute", 94 | transform:toCSS( transform( 95 | translate(-100,-100), 96 | rotate(0), 97 | )) 98 | }) 99 | 100 | expect(results.controls).toEqual({ 101 | width:200, 102 | height:200, 103 | position:"absolute",transform:toCSS( transform( 104 | translate(-100,-100), 105 | rotate(0), 106 | scale(1,1) 107 | )) 108 | }) 109 | }) -------------------------------------------------------------------------------- /__tests__/translate.test.js: -------------------------------------------------------------------------------- 1 | import {translate} from '../src' 2 | 3 | it("translate element", function () { 4 | const state = { 5 | startX: 0, 6 | startY: 0, 7 | x: 0, 8 | y: 0, 9 | scaleX: 1, 10 | scaleY: 1, 11 | width: 100, 12 | height: 100, 13 | angle: 45, 14 | scaleLimit: 0.1, 15 | }; 16 | translate(state, (payload) => { 17 | expect(payload).toEqual({ 18 | x: 15, 19 | y: 15 20 | }) 21 | })({ 22 | pageX: 15, 23 | pageY: 15 24 | }) 25 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-transform", 3 | "version": "0.1.6", 4 | "description": "Free transform tool utlity", 5 | "main": "./lib/index.js", 6 | "author": "Solaiman Kmail ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build:lib": "babel src --out-dir lib --ignore __tests__ ", 10 | "lib:watch": "babel src --out-dir lib --watch --ignore __tests__ ", 11 | "test": "jest --verbose", 12 | "test:watch": "npm run test --runInBand --colors --env=jsdom", 13 | "test:cov": "npm run test -- --coverage ", 14 | "lint": "eslint src", 15 | "coverage": "codecov" 16 | }, 17 | "dependencies": { 18 | "transformation-matrix": "^1.14.0" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.26.0", 22 | "babel-core": "^6.26.3", 23 | "babel-eslint": "^8.2.6", 24 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 25 | "babel-preset-env": "^1.7.0", 26 | "codecov": "^3.0.4", 27 | "enzyme": "^3.5.0", 28 | "eslint": "^5.3.0", 29 | "eslint-plugin-import": "^2.13.0", 30 | "jest": "^23.4.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import scale from './scale' 2 | import rotate from './rotate' 3 | import translate from './translate' 4 | import styler from './styler' 5 | 6 | export { 7 | scale, 8 | rotate, 9 | translate, 10 | styler 11 | } 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/point-finder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Find the actual point position of a transformed point 3 | * 4 | * @param {Object} payload an object holding required information to find actual point 5 | * @param {number} payload.x position of x 6 | * @param {number} payload.y position of y 7 | * @param {number} payload.angle the rotation angle 8 | * @param {Object} payload.center {{x,y}} the center of element 9 | * @param {number} payload.rad the a computed radians of a provided angle 10 | * 11 | * @returns {{x: number, y: number}} an object holding the position 12 | */ 13 | const findPoint = ({x, y, angle, center, rad = angle * (Math.PI / 180)}) => ({ 14 | x: (x - center.x) * Math.cos(rad) - (y - center.y) * Math.sin(rad) + center.x, 15 | y: (x - center.x) * Math.sin(rad) + (y - center.y) * Math.cos(rad) + center.y 16 | }) 17 | 18 | 19 | /** 20 | * Get the Center point of a box 21 | * 22 | * @param {Object} payload element information 23 | * @param {number} payload.x the position of x 24 | * @param {number} payload.y the position of y 25 | * @param {number} payload.scaleX the scaleX of element 26 | * @param {number} payload.scaleY the scaleY of element 27 | * @param {number} payload.width the original width of element 28 | * @param {number} payload.height the original height of element 29 | * 30 | * @returns {{x: *, y: *}} the center of point of element 31 | */ 32 | export const getCenter = ({x, y, scaleX, scaleY, width, height}) => { 33 | const changedWidth = width * scaleX 34 | const changedHeight = height * scaleY 35 | 36 | const changedWidthDiff = changedWidth - width 37 | const changedHeightDiff = changedHeight - height 38 | 39 | return { 40 | x: x - changedWidthDiff + changedWidth / 2, 41 | y: y - changedHeightDiff + changedHeight / 2 42 | } 43 | } 44 | 45 | /** 46 | * get the TopLeft point position 47 | * 48 | * @param {Object} payload element information 49 | * @param {number} payload.x the position of x 50 | * @param {number} payload.y the position of y 51 | * @param {number} payload.scaleX the scaleX of element 52 | * @param {number} payload.scaleY the scaleY of element 53 | * @param {number} payload.width the original width of element 54 | * @param {number} payload.height the original height of element 55 | * @param {number} payload.angle the rotation angle 56 | * @param {Object} payload.center {{x:number, y:number}} 57 | * 58 | * @returns {{x: number, y: number}} the position 59 | */ 60 | export const getTL = ({ 61 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 62 | x, 63 | y, 64 | scaleX, 65 | scaleY, 66 | width, 67 | height 68 | }) 69 | }) => ( 70 | findPoint({ 71 | x, 72 | y, 73 | angle, 74 | center 75 | }) 76 | ) 77 | 78 | /** 79 | * get the LeftBottom point position 80 | * 81 | * @param {Object} payload element information 82 | * @param {number} payload.x the position of x 83 | * @param {number} payload.y the position of y 84 | * @param {number} payload.scaleX the scaleX of element 85 | * @param {number} payload.scaleY the scaleY of element 86 | * @param {number} payload.width the original width of element 87 | * @param {number} payload.height the original height of element 88 | * @param {number} payload.angle the rotation angle 89 | * @param {Object} payload.center {{x:number, y:number}} 90 | * 91 | * @returns {{x: number, y: number}} the position 92 | */ 93 | export const getBL = ({ 94 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 95 | x, 96 | y, 97 | scaleX, 98 | scaleY, 99 | width, 100 | height 101 | }) 102 | }) => { 103 | 104 | return findPoint({ 105 | angle, 106 | center, 107 | x, 108 | y: y + (height * scaleY), 109 | }) 110 | } 111 | 112 | 113 | /** 114 | * Get TopRight point position 115 | * 116 | * @param {Object} payload element information 117 | * @param {number} payload.x the position of x 118 | * @param {number} payload.y the position of y 119 | * @param {number} payload.scaleX the scaleX of element 120 | * @param {number} payload.scaleY the scaleY of element 121 | * @param {number} payload.width the original width of element 122 | * @param {number} payload.height the original height of element 123 | * @param {number} payload.angle the rotation angle 124 | * @param {Object} payload.center {{x:number, y:number}} 125 | * 126 | * @returns {{x: number, y: number}} the position 127 | */ 128 | export const getTR = ({ 129 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 130 | x, 131 | y, 132 | scaleX, 133 | scaleY, 134 | width, 135 | height 136 | }) 137 | }) => ( 138 | findPoint({ 139 | angle, 140 | center, 141 | x: x + (width * scaleX), 142 | y, 143 | }) 144 | ) 145 | 146 | 147 | /** 148 | * Get BottomRight point position 149 | * 150 | * @param {Object} payload element information 151 | * @param {number} payload.x the position of x 152 | * @param {number} payload.y the position of y 153 | * @param {number} payload.scaleX the scaleX of element 154 | * @param {number} payload.scaleY the scaleY of element 155 | * @param {number} payload.width the original width of element 156 | * @param {number} payload.height the original height of element 157 | * @param {number} payload.angle the rotation angle 158 | * @param {Object} payload.center {{x:number, y:number}} 159 | * 160 | * @returns {{x: number, y: number}} the position 161 | */ 162 | export const getBR = ({ 163 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 164 | x, 165 | y, 166 | scaleX, 167 | scaleY, 168 | width, 169 | height 170 | }) 171 | }) => { 172 | return findPoint({ 173 | angle, 174 | center, 175 | x: x + width * scaleX, 176 | y: y + height * scaleY, 177 | }) 178 | } 179 | 180 | /** 181 | * get MiddleRight point position 182 | * 183 | * @param {Object} payload element information 184 | * @param {number} payload.x the position of x 185 | * @param {number} payload.y the position of y 186 | * @param {number} payload.scaleX the scaleX of element 187 | * @param {number} payload.scaleY the scaleY of element 188 | * @param {number} payload.width the original width of element 189 | * @param {number} payload.height the original height of element 190 | * @param {number} payload.angle the rotation angle 191 | * @param {Object} payload.center {{x:number, y:number}} 192 | * 193 | * @returns {{x: number, y: number}} the position 194 | */ 195 | export const getMR = ({ 196 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 197 | x, 198 | y, 199 | scaleX, 200 | scaleY, 201 | width, 202 | height 203 | }) 204 | }) => ( 205 | findPoint({ 206 | x: x + (width * scaleX), 207 | y: y + (height * scaleY) / 2, 208 | center, 209 | angle 210 | }) 211 | ) 212 | 213 | /** 214 | * get MiddleBottom point position 215 | * 216 | * @param {Object} payload element information 217 | * @param {number} payload.x the position of x 218 | * @param {number} payload.y the position of y 219 | * @param {number} payload.scaleX the scaleX of element 220 | * @param {number} payload.scaleY the scaleY of element 221 | * @param {number} payload.width the original width of element 222 | * @param {number} payload.height the original height of element 223 | * @param {number} payload.angle the rotation angle 224 | * @param {Object} payload.center {{x:number, y:number}} 225 | * 226 | * @returns {{x: number, y: number}} the position 227 | */ 228 | export const getBM = ({ 229 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 230 | x, 231 | y, 232 | scaleX, 233 | scaleY, 234 | width, 235 | height 236 | }) 237 | }) => ( 238 | findPoint({ 239 | x: x + (width * scaleX) / 2, 240 | y: y + (height * scaleY), 241 | center, 242 | angle 243 | }) 244 | ) 245 | 246 | /** 247 | * get MiddleTop point position 248 | * 249 | * @param {Object} payload element information 250 | * @param {number} payload.x the position of x 251 | * @param {number} payload.y the position of y 252 | * @param {number} payload.scaleX the scaleX of element 253 | * @param {number} payload.scaleY the scaleY of element 254 | * @param {number} payload.width the original width of element 255 | * @param {number} payload.height the original height of element 256 | * @param {number} payload.angle the rotation angle 257 | * @param {Object} payload.center {{x:number, y:number}} 258 | * 259 | * @returns {{x: number, y: number}} the position 260 | */ 261 | export const getTM = ({ 262 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 263 | x, 264 | y, 265 | scaleX, 266 | scaleY, 267 | width, 268 | height 269 | }) 270 | }) => ( 271 | findPoint({ 272 | x: x + (width * scaleX) / 2, 273 | y: y, 274 | center, 275 | angle 276 | }) 277 | ) 278 | 279 | /** 280 | * get MiddleLeft point position 281 | * 282 | * @param {Object} payload element information 283 | * @param {number} payload.x the position of x 284 | * @param {number} payload.y the position of y 285 | * @param {number} payload.scaleX the scaleX of element 286 | * @param {number} payload.scaleY the scaleY of element 287 | * @param {number} payload.width the original width of element 288 | * @param {number} payload.height the original height of element 289 | * @param {number} payload.angle the rotation angle 290 | * @param {Object} payload.center {{x:number, y:number}} 291 | * 292 | * @returns {{x: number, y: number}} the position 293 | */ 294 | export const getML = ({ 295 | x, y, scaleX, scaleY, width, height, angle, center = getCenter({ 296 | x, 297 | y, 298 | scaleX, 299 | scaleY, 300 | width, 301 | height 302 | }) 303 | }) => ( 304 | findPoint({ 305 | x: x, 306 | y: y + (height * scaleY) / 2, 307 | center, 308 | angle 309 | }) 310 | ) 311 | 312 | /** 313 | * given a point, get it's opposite point 314 | * 315 | * @param {string} scaleType scale point position name 316 | * @param {Object} props element information 317 | * @param {number} props.x the position of x 318 | * @param {number} props.y the position of y 319 | * @param {number} props.scaleX the scaleX of element 320 | * @param {number} props.scaleY the scaleY of element 321 | * @param {number} props.width the original width of element 322 | * @param {number} props.height the original height of element 323 | * @param {number} props.angle the rotation angle 324 | * @param {Object} props.center {{x:number, y:number}} 325 | * 326 | * @returns {{x:number, y:number}} point position 327 | */ 328 | export const getOppositePoint = (scaleType, props) => { 329 | 330 | let caller 331 | 332 | const center = getCenter({ 333 | x: props.x, 334 | y: props.y, 335 | width: props.width, 336 | height: props.height, 337 | scaleX: props.scaleX, 338 | scaleY: props.scaleY, 339 | }) 340 | 341 | props = { 342 | center, 343 | ...props, 344 | x: getOriginalPositionFromScale(props.x, props.width, props.scaleX), 345 | y: getOriginalPositionFromScale(props.y, props.height, props.scaleY) 346 | } 347 | 348 | switch (scaleType) { 349 | case 'tl': 350 | caller = getBR 351 | break 352 | 353 | case 'ml': 354 | caller = getMR 355 | break 356 | 357 | case 'tr' : 358 | caller = getBL 359 | break 360 | 361 | case 'tm' : 362 | caller = getBM 363 | break 364 | 365 | case 'bl' : 366 | caller = getTR 367 | break 368 | 369 | case 'bm' : 370 | caller = getTM 371 | break 372 | 373 | case 'br' : 374 | caller = getTL 375 | break 376 | 377 | case 'mr' : 378 | caller = getML 379 | break 380 | } 381 | return caller(props) 382 | } 383 | 384 | /** 385 | * given a point position by it's string name 386 | * 387 | * @param {string} scaleType scale point position name 388 | * @param {Object} props element information 389 | * @param {number} props.x the position of x 390 | * @param {number} props.y the position of y 391 | * @param {number} props.scaleX the scaleX of element 392 | * @param {number} props.scaleY the scaleY of element 393 | * @param {number} props.width the original width of element 394 | * @param {number} props.height the original height of element 395 | * @param {number} props.angle the rotation angle 396 | * @param {boolean} props.scaleFromCenter scaling performed from center 397 | * @param {Object} props.center {{x:number, y:number}} 398 | * 399 | * @returns {{x:number, y:number}} point position 400 | */ 401 | export const getPoint = (scaleType, props) => { 402 | 403 | const center = getCenter({ 404 | x: props.x, 405 | y: props.y, 406 | width: props.width, 407 | height: props.height, 408 | scaleX: props.scaleX, 409 | scaleY: props.scaleY, 410 | }) 411 | 412 | if (props.scaleFromCenter) { 413 | return center; 414 | } 415 | 416 | props = { 417 | center, 418 | ...props, 419 | x: getOriginalPositionFromScale(props.x, props.width, props.scaleX), 420 | y: getOriginalPositionFromScale(props.y, props.height, props.scaleY) 421 | } 422 | 423 | let caller 424 | switch (scaleType) { 425 | 426 | case 'tl': 427 | caller = getTL 428 | break; 429 | 430 | case 'ml': 431 | caller = getML 432 | break; 433 | 434 | case 'tr': 435 | caller = getTR 436 | break; 437 | 438 | case 'tm': 439 | caller = getTM 440 | break; 441 | 442 | case 'bl' : 443 | caller = getBL 444 | break; 445 | 446 | case 'bm' : 447 | caller = getBM 448 | break; 449 | 450 | case 'br' : 451 | caller = getBR 452 | break; 453 | 454 | case 'mr' : 455 | caller = getMR 456 | break; 457 | } 458 | 459 | return caller(props) 460 | } 461 | 462 | /** 463 | * get sine and cosine for a point based on angle and point name 464 | * 465 | * @param {string} scaleType scale point position name 466 | * @param {number} angle the rotation angle 467 | * 468 | * @returns {{sin: number, cos: number}} the sine and cosine of scale type 469 | */ 470 | export const getSineCosine = (scaleType, angle) => { 471 | switch (scaleType) { 472 | case 'tr': 473 | case 'tm': 474 | case 'bl': 475 | case 'bm': 476 | return { 477 | cos: Math.cos(-angle * (Math.PI / 180)), 478 | sin: Math.sin(-angle * (Math.PI / 180)) 479 | } 480 | default: 481 | return { 482 | sin: Math.sin(angle * (Math.PI / 180)), 483 | cos: Math.cos(angle * (Math.PI / 180)) 484 | } 485 | } 486 | } 487 | 488 | /** 489 | * get the amount of movement for a point 490 | * 491 | * @param {string} scaleType scale point position name 492 | * @param {object} oppositePoint the opposite point position {x: number,y: number} 493 | * @param {object} point the point position {x: number,y: number} 494 | * @param {object} moveDiff the the amount of pixels that element moved {x: number,y: number} 495 | * 496 | * @returns {{x: number, y:number}} the new position of moved element 497 | */ 498 | export const getMovePoint = (scaleType, oppositePoint, point, moveDiff) => { 499 | switch (scaleType) { 500 | 501 | case 'tl': 502 | return { 503 | x: oppositePoint.x - (moveDiff.x + point.x), 504 | y: oppositePoint.y - (moveDiff.y + point.y), 505 | } 506 | case 'ml': 507 | return { 508 | x: oppositePoint.x - moveDiff.x - point.x, 509 | y: oppositePoint.y - moveDiff.y - point.y, 510 | } 511 | 512 | case 'tr' : 513 | case 'tm': 514 | return { 515 | x: point.x + (moveDiff.x - oppositePoint.x), 516 | y: oppositePoint.y - (moveDiff.y + point.y) 517 | } 518 | case 'mr': 519 | case 'br': 520 | return { 521 | x: point.x + (moveDiff.x - oppositePoint.x), 522 | y: point.y + (moveDiff.y - oppositePoint.y) 523 | } 524 | case 'bl': 525 | case 'bm': 526 | return { 527 | x: oppositePoint.x - (moveDiff.x + point.x), 528 | y: point.y + (moveDiff.y - oppositePoint.y) 529 | } 530 | } 531 | } 532 | 533 | /** 534 | * guess the original point position based on scale and the position after scaling 535 | * 536 | * @param {number} position the position of x or y 537 | * @param {number} size the size of element (width for x, height for y) 538 | * @param {number} scale the amount of scaled element (scaleX for x, scaleY for y) 539 | * 540 | * @returns {number} the original point position 541 | */ 542 | const getOriginalPositionFromScale = (position, size, scale) => { 543 | const changed = size * scale 544 | 545 | const diff = changed - size 546 | 547 | return position - diff 548 | } 549 | 550 | 551 | /** 552 | * Find the real position of lowest and highest handle 553 | * 554 | * @param {object} point the point 555 | * @returns {{x: number, y: number}} the max and min values of X & Y 556 | */ 557 | export const minMax = (point) => { 558 | 559 | const points = [ 560 | getTL(point), 561 | getTR(point), 562 | getBL(point), 563 | getBR(point) 564 | ]; 565 | 566 | const bounds = points.reduce((bounds, point, c) => { 567 | if (c === 0) { 568 | bounds.xmin = point.x; 569 | bounds.xmax = point.x; 570 | bounds.ymin = point.y; 571 | bounds.ymax = point.y; 572 | } else { 573 | bounds.xmin = Math.min(bounds.xmin, point.x); 574 | bounds.xmax = Math.max(bounds.xmax, point.x); 575 | bounds.ymin = Math.min(bounds.ymin, point.y); 576 | bounds.ymax = Math.max(bounds.ymax, point.y); 577 | } 578 | return bounds 579 | }, {}); 580 | 581 | return bounds; 582 | } -------------------------------------------------------------------------------- /src/rotate.js: -------------------------------------------------------------------------------- 1 | import {getCenter} from "./point-finder"; 2 | 3 | export default ({x, y, scaleX, scaleY, width, height, angle, startX, startY, offsetX, offsetY},onUpdate) => { 4 | 5 | const center = getCenter({x, y, scaleX, scaleY, width, height}) 6 | 7 | const pressAngle = Math.atan2((startY - offsetY) - center.y, (startX - offsetX) - center.x) * 180 / Math.PI 8 | 9 | return (event) => { 10 | 11 | const degree = Math.atan2((event.pageY - offsetY) - center.y, (event.pageX - offsetX) - center.x) * 180 / Math.PI; 12 | 13 | let ang = angle + degree - pressAngle 14 | 15 | if (event.shiftKey) { 16 | ang = (ang / 15 >> 0) * 15; 17 | } 18 | 19 | onUpdate({ 20 | angle: ang 21 | }) 22 | } 23 | } -------------------------------------------------------------------------------- /src/scale.js: -------------------------------------------------------------------------------- 1 | import { 2 | getPoint, 3 | getOppositePoint, 4 | getMovePoint, 5 | getSineCosine, 6 | getCenter, 7 | } from './point-finder' 8 | 9 | /** 10 | * Perform Scaling based on a positioned handle 11 | * 12 | * @param {string} scaleType scale point position name 13 | * @param {Object} payload an object holding element information 14 | * @param {number} payload.startX mouse down position on X axis 15 | * @param {number} payload.startY mouse down position on Y axis 16 | * @param {number} payload.x position of x 17 | * @param {number} payload.y position of y 18 | * @param {number} payload.scaleX amount of scale for x (width) 19 | * @param {number} payload.scaleY amount of scale for y (height) 20 | * @param {number} payload.width original width 21 | * @param {number} payload.height original height 22 | * @param {number} payload.angle the angle of rotation 23 | * @param {number} payload.scaleLimit minimum scale limit 24 | * @param {boolean} payload.scaleFromCenter is scale from center 25 | * @param {boolean} payload.aspectRatio is scale on aspect ration 26 | * @param {Function} onUpdate a callback on mouse up 27 | * 28 | * @returns {Function} a function for mouse move 29 | */ 30 | export default (scaleType, { 31 | startX, 32 | startY, 33 | x, 34 | y, 35 | scaleX, 36 | scaleY, 37 | width, 38 | height, 39 | angle, 40 | scaleLimit, 41 | scaleFromCenter = false, 42 | enableScaleFromCenter = true, 43 | aspectRatio = false, 44 | enableAspectRatio = true 45 | }, onUpdate) => { 46 | 47 | const ratio = (width * scaleX) / (height * scaleY) 48 | 49 | let point = getPoint(scaleType, {x, y, scaleX, scaleY, width, height, angle, scaleFromCenter}); 50 | 51 | let oppositePoint = getOppositePoint(scaleType, { 52 | x, 53 | y, 54 | scaleX, 55 | scaleY, 56 | width, 57 | height, 58 | angle 59 | }) 60 | 61 | const currentProps = { 62 | x, 63 | y, 64 | scaleX, 65 | scaleY, 66 | } 67 | 68 | return (event) => { 69 | 70 | if(enableScaleFromCenter && ((event.altKey && !scaleFromCenter) || (!event.altKey && scaleFromCenter))){ 71 | 72 | startX = event.pageX 73 | startY = event.pageY 74 | 75 | scaleFromCenter = event.altKey && !scaleFromCenter 76 | 77 | point = getPoint(scaleType, { 78 | ...currentProps, 79 | width, 80 | height, 81 | angle, 82 | scaleFromCenter 83 | }); 84 | 85 | oppositePoint = getOppositePoint(scaleType, { 86 | ...currentProps, 87 | width, 88 | height, 89 | angle 90 | }) 91 | } 92 | 93 | if(!event.shiftKey && aspectRatio ){ 94 | aspectRatio = false 95 | } else if(event.shiftKey && !aspectRatio ){ 96 | aspectRatio = true 97 | } 98 | 99 | if(!enableAspectRatio){ 100 | aspectRatio = false 101 | } 102 | const moveDiff = { 103 | x: event.pageX - startX, 104 | y: event.pageY - startY 105 | } 106 | 107 | const movePoint = getMovePoint(scaleType, oppositePoint, point, moveDiff) 108 | 109 | if (enableScaleFromCenter && scaleFromCenter) { 110 | movePoint.x *= 2 111 | movePoint.y *= 2 112 | } 113 | 114 | const {sin, cos} = getSineCosine(scaleType, angle); 115 | 116 | const rotationPoint = { 117 | x: movePoint.x * cos + movePoint.y * sin, 118 | y: movePoint.y * cos - movePoint.x * sin 119 | } 120 | 121 | currentProps.scaleX = (rotationPoint.x / width) > scaleLimit ? rotationPoint.x / width : scaleLimit 122 | currentProps.scaleY = (rotationPoint.y / height) > scaleLimit ? rotationPoint.y / height : scaleLimit 123 | 124 | 125 | switch (scaleType) { 126 | case 'ml': 127 | case 'mr': 128 | currentProps.scaleY = scaleY 129 | if (aspectRatio) { 130 | currentProps.scaleY = ((width * currentProps.scaleX) * (1 / ratio)) / height; 131 | } 132 | break; 133 | case 'tm': 134 | case 'bm': 135 | currentProps.scaleX = scaleX 136 | if (aspectRatio) { 137 | currentProps.scaleX = ((height * currentProps.scaleY) * ratio) / width; 138 | } 139 | break; 140 | default: 141 | if (aspectRatio) { 142 | currentProps.scaleY = ((width * currentProps.scaleX) * (1 / ratio)) / height; 143 | } 144 | } 145 | 146 | if (enableScaleFromCenter && scaleFromCenter) { 147 | const center = getCenter({ 148 | x, 149 | y, 150 | width, 151 | height, 152 | scaleX: currentProps.scaleX, 153 | scaleY: currentProps.scaleY, 154 | }) 155 | currentProps.x = x + (point.x - center.x) 156 | currentProps.y = y + (point.y - center.y) 157 | } else { 158 | const freshOppositePoint = getOppositePoint(scaleType, { 159 | width, 160 | height, 161 | angle, 162 | x, 163 | y, 164 | scaleX: currentProps.scaleX, 165 | scaleY: currentProps.scaleY, 166 | }); 167 | 168 | currentProps.x = x + (oppositePoint.x - freshOppositePoint.x) 169 | currentProps.y = y + (oppositePoint.y - freshOppositePoint.y) 170 | } 171 | 172 | onUpdate(currentProps) 173 | } 174 | } -------------------------------------------------------------------------------- /src/styler.js: -------------------------------------------------------------------------------- 1 | import { 2 | scale, 3 | rotate, 4 | translate, 5 | transform, 6 | toCSS 7 | } from 'transformation-matrix'; 8 | 9 | //https://stackoverflow.com/questions/15762768/javascript-math-round-to-two-decimal-places 10 | const roundTo = (n, digits = 2) => { 11 | const multiplicator = Math.pow(10, digits) 12 | n = parseFloat((n * multiplicator).toFixed(11)) 13 | const test = (Math.round(n) / multiplicator) 14 | return +(test.toFixed(2)); 15 | } 16 | 17 | export default ({x, y, angle, scaleX, scaleY, width, height, disableScale = false}) => { 18 | 19 | const changedWidth = width * (1 - scaleX); 20 | const newWidth = width - changedWidth; 21 | const changedHeight = height * (1 - scaleY); 22 | const newHeight = height - changedHeight; 23 | 24 | let transformMatrix; 25 | 26 | if(disableScale === false){ 27 | transformMatrix = transform( 28 | translate(roundTo(x + changedWidth / 2), roundTo(y + changedHeight / 2)), 29 | rotate(angle * (Math.PI / 180)), 30 | scale(scaleX, scaleY) 31 | ); 32 | }else{ 33 | transformMatrix = transform( 34 | translate(roundTo(x + changedWidth ), roundTo(y + changedHeight )), 35 | rotate(angle * (Math.PI / 180)), 36 | ); 37 | width = newWidth; 38 | height = newHeight; 39 | } 40 | 41 | return { 42 | element: { 43 | width, 44 | height, 45 | transform: toCSS(transformMatrix), 46 | position: "absolute", 47 | }, 48 | controls: { 49 | width: newWidth, 50 | height: newHeight, 51 | transform: toCSS( 52 | transform( 53 | translate(roundTo(x + changedWidth), roundTo(y + changedHeight)), 54 | rotate(angle * (Math.PI / 180)), 55 | ) 56 | ), 57 | position: "absolute", 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/translate.js: -------------------------------------------------------------------------------- 1 | export default ({x,y,startX,startY},onUpdate) => ( 2 | (dragEvent) => { 3 | 4 | x += dragEvent.pageX - startX 5 | y += dragEvent.pageY - startY 6 | 7 | onUpdate({x,y}) 8 | 9 | startX = dragEvent.pageX 10 | startY = dragEvent.pageY 11 | } 12 | ) --------------------------------------------------------------------------------