- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
- 539
- 540
- 541
- 542
- 543
- 544
- 545
- 546
- 547
- 548
- 549
- 550
- 551
- 552
- 553
- 554
- 555
- 556
- 557
- 558
- 559
- 560
- 561
- 562
- 563
- 564
- 565
- 566
- 567
- 568
- 569
- 570
- 571
- 572
- 573
- 574
- 575
- 576
- 577
- 578
- 579
- 580
- 581
- 582
- 583
- 584
- 585
- 586
- 587
- 588
- 589
- 590
- 591
- 592
- 593
- 594
- 595
- 596
- 597
- 598
- 599
- 600
- 601
- 602
- 603
- 604
- 605
- 606
- 607
- 608
- 609
- 610
- 611
- 612
- 613
- 614
- 615
- 616
- 617
- 618
- 619
- 620
- 621
- 622
- 623
- 624
- 625
- 626
- 627
- 628
- 629
- 630
- 631
- 632
- 633
- 634
- 635
- 636
- 637
- 638
- 639
- 640
- 641
- 642
- 643
- 644
- 645
- 646
- 647
- 648
- 649
- 650
- 651
- 652
- 653
- 654
- 655
- 656
- 657
- 658
- 659
- 660
- 661
- 662
- 663
- 664
- 665
- 666
- 667
- 668
- 669
- 670
- 671
- 672
- 673
- 674
- 675
- 676
- 677
- 678
- 679
- 680
- 681
- 682
- 683
- 684
- 685
- 686
- 687
- 688
- 689
- 690
- 691
- 692
- 693
- 694
- 695
- 696
- 697
- 698
- 699
- 700
- 701
- 702
- 703
- 704
- 705
- 706
- 707
- 708
- 709
- 710
- 711
- 712
- 713
- 714
- 715
- 716
- 717
- 718
- 719
- 720
- 721
- 722
- 723
- 724
- 725
- 726
- 727
- 728
- 729
- 730
- 731
- 732
- 733
- 734
- 735
- 736
- 737
- 738
- 739
- 740
- 741
- 742
- 743
- 744
- 745
- 746
- 747
- 748
- 749
- 750
- 751
- 752
- 753
- 754
- 755
- 756
- 757
- 758
- 759
- 760
- 761
- 762
- 763
- 764
- 765
- 766
- 767
- 768
- 769
- 770
- 771
- 772
- 773
- 774
- 775
- 776
- 777
- 778
- 779
- 780
- 781
- 782
- 783
- 784
- 785
- 786
- 787
- 788
- 789
- 790
- 791
- 792
- 793
- 794
- 795
- 796
- 797
- 798
- 799
- 800
- 801
- 802
- 803
- 804
- 805
- 806
- 807
- 808
- 809
- 810
- 811
- 812
- 813
- 814
- 815
- 816
- 817
- 818
- 819
- 820
- 821
- 822
- 823
- 824
- 825
- 826
- 827
- 828
- 829
- 830
- 831
- 832
- 833
- 834
- 835
- 836
- 837
- 838
- 839
- 840
- 841
- 842
- 843
- 844
- 845
- 846
- 847
- 848
- 849
- 850
- 851
- 852
- 853
- 854
- 855
- 856
- 857
- 858
- 859
- 860
- 861
- 862
- 863
- 864
- 865
- 866
- 867
- 868
- 869
- 870
- 871
- 872
- 873
- 874
- 875
- 876
- 877
- 878
- 879
- 880
- 881
- 882
- 883
- 884
- 885
- 886
- 887
- 888
- 889
- 890
- 891
- 892
- 893
- 894
- 895
- 896
- 897
- 898
- 899
- 900
- 901
- 902
- 903
- 904
- 905
- 906
- 907
- 908
- 909
- 910
- 911
- 912
- 913
- 914
- 915
- 916
- 917
- 918
- 919
- 920
- 921
- 922
- 923
- 924
- 925
- 926
- 927
- 928
- 929
- 930
- 931
- 932
- 933
- 934
- 935
- 936
- 937
- 938
- 939
- 940
- 941
- 942
- 943
- 944
- 945
- 946
- 947
- 948
- 949
- 950
- 951
- 952
- 953
- 954
- 955
- 956
- 957
- 958
- 959
- 960
- 961
- 962
- 963
- 964
- 965
- 966
- 967
- 968
- 969
- 970
- 971
- 972
- 973
- 974
- 975
- 976
- 977
- 978
- 979
- 980
- 981
- 982
- 983
- 984
- 985
- 986
- 987
- 988
- 989
- 990
- 991
- 992
- 993
- 994
- 995
- 996
- 997
- 998
- 999
- 1000
- 1001
- 1002
- 1003
- 1004
- 1005
- 1006
- 1007
- 1008
- 1009
- 1010
- 1011
- 1012
- 1013
- 1014
- 1015
- 1016
- 1017
- 1018
- 1019
- 1020
- 1021
- 1022
- 1023
- 1024
- 1025
- 1026
- 1027
- 1028
- 1029
- 1030
- 1031
- 1032
- 1033
- 1034
- 1035
- 1036
- 1037
- 1038
- 1039
- 1040
- 1041
- 1042
- 1043
- 1044
- 1045
- 1046
- 1047
- 1048
- 1049
- 1050
- 1051
- 1052
- 1053
- 1054
- 1055
- 1056
- 1057
- 1058
- 1059
- 1060
- 1061
- 1062
- 1063
- 1064
- 1065
- 1066
- 1067
- 1068
- 1069
- 1070
- 1071
- 1072
- 1073
- 1074
- 1075
- 1076
- 1077
- 1078
- 1079
- 1080
- 1081
- 1082
- 1083
- 1084
- 1085
- 1086
- 1087
- 1088
- 1089
- 1090
- 1091
- 1092
- 1093
- 1094
- 1095
- 1096
- 1097
- 1098
- 1099
- 1100
- 1101
- 1102
- 1103
- 1104
- 1105
- 1106
- 1107
- 1108
- 1109
- 1110
- 1111
- 1112
- 1113
- 1114
- 1115
- 1116
- 1117
- 1118
- 1119
- 1120
- 1121
- 1122
- 1123
- 1124
- 1125
- 1126
- 1127
- 1128
- 1129
- 1130
- 1131
- 1132
- 1133
- 1134
- 1135
- 1136
- 1137
- 1138
- 1139
- 1140
- 1141
- 1142
- 1143
- 1144
- 1145
- 1146
- 1147
- 1148
- 1149
- 1150
- 1151
- 1152
- 1153
- 1154
- 1155
- 1156
- 1157
- 1158
- 1159
- 1160
- 1161
- 1162
- 1163
- 1164
- 1165
- 1166
- 1167
- 1168
- 1169
- 1170
- 1171
- 1172
- 1173
- 1174
- 1175
- 1176
- 1177
- 1178
- 1179
- 1180
- 1181
- 1182
- 1183
- 1184
- 1185
- 1186
- 1187
- 1188
- 1189
- 1190
- 1191
- 1192
- 1193
- 1194
- 1195
- 1196
- 1197
- 1198
- 1199
- 1200
- 1201
- 1202
- 1203
- 1204
- 1205
- 1206
- 1207
- 1208
- 1209
- 1210
- 1211
- 1212
- 1213
- 1214
- 1215
- 1216
- 1217
- 1218
- 1219
- 1220
- 1221
- 1222
- 1223
- 1224
- 1225
- 1226
- 1227
- 1228
- 1229
- 1230
- 1231
- 1232
- 1233
- 1234
- 1235
- 1236
- 1237
- 1238
- 1239
- 1240
- 1241
- 1242
- 1243
- 1244
- 1245
- 1246
- 1247
- 1248
- 1249
- 1250
- 1251
- 1252
- 1253
- 1254
- 1255
- 1256
- 1257
- 1258
- 1259
- 1260
- 1261
- 1262
- 1263
- 1264
- 1265
- 1266
- 1267
- 1268
- 1269
- 1270
- 1271
- 1272
- 1273
- 1274
- 1275
- 1276
- 1277
- 1278
- 1279
- 1280
- 1281
- 1282
- 1283
- 1284
- 1285
- 1286
- 1287
- 1288
- 1289
- 1290
- 1291
- 1292
- 1293
- 1294
- 1295
- 1296
- 1297
- 1298
- 1299
- 1300
- 1301
- 1302
- 1303
- 1304
- 1305
- 1306
- 1307
- 1308
- 1309
- 1310
- 1311
- 1312
- 1313
- 1314
- 1315
- 1316
- 1317
- 1318
- 1319
- 1320
- 1321
- 1322
- 1323
- 1324
- 1325
- 1326
- 1327
- 1328
- 1329
- 1330
- 1331
- 1332
- 1333
- 1334
- 1335
- 1336
- 1337
- 1338
- 1339
- 1340
- 1341
- 1342
- 1343
- 1344
- 1345
- 1346
- 1347
- 1348
- 1349
- 1350
- 1351
- 1352
- 1353
- 1354
- 1355
- 1356
- 1357
- 1358
- 1359
- 1360
- 1361
- 1362
- 1363
- 1364
- 1365
- 1366
- 1367
- 1368
- 1369
- 1370
- 1371
- 1372
- 1373
- 1374
- 1375
- 1376
- 1377
- 1378
- 1379
- 1380
- 1381
- 1382
- 1383
- 1384
- 1385
- 1386
- 1387
- 1388
- 1389
- 1390
- 1391
- 1392
- 1393
- 1394
- 1395
- 1396
- 1397
- 1398
- 1399
- 1400
- 1401
- 1402
- 1403
- 1404
- 1405
- 1406
- 1407
- 1408
- 1409
- 1410
- 1411
- 1412
- 1413
- 1414
- 1415
- 1416
- 1417
- 1418
- 1419
- 1420
- 1421
- 1422
- 1423
- 1424
- 1425
- 1426
- 1427
- 1428
- 1429
- 1430
- 1431
- 1432
- 1433
- 1434
- 1435
- 1436
- 1437
- 1438
- 1439
- 1440
- 1441
- 1442
- 1443
- 1444
- 1445
- 1446
- 1447
- 1448
- 1449
- 1450
- 1451
- 1452
- 1453
- 1454
- 1455
- 1456
- 1457
- 1458
- 1459
- 1460
- 1461
- 1462
- 1463
- 1464
- 1465
- 1466
- 1467
- 1468
- 1469
- 1470
- 1471
- 1472
- 1473
- 1474
- 1475
- 1476
- 1477
- 1478
- 1479
- 1480
- 1481
- 1482
- 1483
- 1484
- 1485
- 1486
- 1487
- 1488
- 1489
- 1490
- 1491
- 1492
- 1493
- 1494
- 1495
- 1496
- 1497
- 1498
- 1499
- 1500
- 1501
- 1502
- 1503
- 1504
- 1505
- 1506
- 1507
- 1508
- 1509
- 1510
- 1511
- 1512
- 1513
- 1514
- 1515
- 1516
- 1517
- 1518
- 1519
- 1520
- 1521
- 1522
- 1523
- 1524
- 1525
- 1526
- 1527
- 1528
- 1529
- 1530
- 1531
- 1532
- 1533
- 1534
- 1535
- 1536
- 1537
- 1538
- 1539
- 1540
- 1541
- 1542
- 1543
- 1544
- 1545
- 1546
- 1547
- 1548
- 1549
- 1550
- 1551
- 1552
- 1553
- 1554
- 1555
- 1556
- 1557
- 1558
- 1559
- 1560
- 1561
- 1562
- 1563
- 1564
- 1565
- 1566
- 1567
- 1568
- 1569
- 1570
- 1571
- 1572
- 1573
- 1574
- 1575
- 1576
- 1577
- 1578
- 1579
- 1580
- 1581
- 1582
- 1583
- 1584
- 1585
- 1586
- 1587
- 1588
- 1589
- 1590
- 1591
- 1592
- 1593
- 1594
- 1595
- 1596
- 1597
- 1598
- 1599
- 1600
- 1601
- 1602
- 1603
- 1604
- 1605
- 1606
- 1607
- 1608
- 1609
- 1610
- 1611
- 1612
- 1613
- 1614
- 1615
- 1616
- 1617
- 1618
- 1619
- 1620
- 1621
- 1622
- 1623
- 1624
- 1625
- 1626
- 1627
- 1628
- 1629
- 1630
- 1631
- 1632
- 1633
- 1634
- 1635
- 1636
- 1637
- 1638
- 1639
- 1640
- 1641
- 1642
- 1643
- 1644
- 1645
- 1646
- 1647
- 1648
- 1649
- 1650
- 1651
- 1652
- 1653
- 1654
- 1655
- 1656
- 1657
- 1658
- 1659
- 1660
- 1661
- 1662
- 1663
- 1664
- 1665
- 1666
- 1667
- 1668
- 1669
- 1670
- 1671
- 1672
- 1673
- 1674
- 1675
- 1676
- 1677
- 1678
- 1679
- 1680
- 1681
- 1682
- 1683
- 1684
- 1685
- 1686
- 1687
- 1688
- 1689
- 1690
- 1691
- 1692
- 1693
- 1694
- 1695
- 1696
- 1697
- 1698
- 1699
- 1700
- 1701
- 1702
- 1703
- 1704
- 1705
- 1706
- 1707
- 1708
- 1709
- 1710
- 1711
- 1712
- 1713
- 1714
- 1715
- 1716
- 1717
- 1718
- 1719
- 1720
- 1721
- 1722
- 1723
- 1724
- 1725
- 1726
- 1727
- 1728
- 1729
import { Renderer } from '@pixi/core';
import { DisplayObject, Container } from '@pixi/display';
import { Point, Matrix, Transform, Rectangle } from '@pixi/math';
import { OrientedBounds } from '@pixi-essentials/bounds';
import { ObjectPoolFactory } from '@pixi-essentials/object-pool';
import { TransformerHandle } from './TransformerHandle';
import { TransformerWireframe } from './TransformerWireframe';
import { createHorizontalSkew, createVerticalSkew } from './utils/skewTransform';
import { decomposeTransform } from './utils/decomposeTransform';
import { multiplyTransform } from './utils/multiplyTransform';
import type { InteractionEvent } from '@pixi/interaction';
import type { ITransformerHandleStyle } from './TransformerHandle';
// Preallocated objects
const tempTransform = new Transform();
const tempCorners: [Point, Point, Point, Point] = [new Point(), new Point(), new Point(), new Point()];
const tempMatrix = new Matrix();
const tempPoint = new Point();
const tempBounds = new OrientedBounds();
const tempRect = new Rectangle();
const tempHull = [new Point(), new Point(), new Point(), new Point()];
const tempPointer = new Point();
const emitMatrix = new Matrix();// Used to pass to event handlers
// Pool for allocating an arbitrary number of points
const pointPool = ObjectPoolFactory.build<Point>(Point as any);
/**
* The handles used for rotation.
*
* @public
* @ignore
*/
export type RotateHandle = 'rotator'
| 'boxRotateTopLeft'
| 'boxRotateTopRight'
| 'boxRotateBottomLeft'
| 'boxRotateBottomRight';
/**
* The handles used for scaling.
*
* @public
* @ignore
*/
export type ScaleHandle = 'topLeft' |
'topCenter' |
'topRight' |
'middleLeft' |
'middleCenter' |
'middleRight' |
'bottomLeft' |
'bottomCenter' |
'bottomRight';
/**
* The handles used for skewing
*
* @public
* @ignore
*/
export type SkewHandle = 'skewHorizontal' | 'skewVertical';
/**
* All the handles provided by {@link Transformer}.
*
* @public
* @ignore
*/
export type Handle = RotateHandle | ScaleHandle | SkewHandle;
/**
* Specific cursors for each handle
*
* @ignore
*/
export const HANDLE_TO_CURSOR: { [H in Handle]?: string } = {
topLeft: 'nw-resize',
topCenter: 'n-resize',
topRight: 'ne-resize',
middleLeft: 'w-resize',
middleRight: 'e-resize',
bottomLeft: 'sw-resize',
bottomCenter: 's-resize',
bottomRight: 'se-resize',
};
/**
* An array of all {@link ScaleHandle} values.
*
* @internal
* @ignore
*/
const SCALE_HANDLES: ScaleHandle[] = [
'topLeft',
'topCenter',
'topRight',
'middleLeft',
'middleCenter',
'middleRight',
'bottomLeft',
'bottomCenter',
'bottomRight',
];
/**
* This maps each scaling handle to the directions in which the x, y components are outward. A value of
* zero means that no scaling occurs along that component's axis.
*
* @internal
* @ignore
*/
const SCALE_COMPONENTS: {
[H in ScaleHandle]: { x: (-1 | 0 | 1); y: (-1 | 0 | 1) };
} = {
topLeft: { x: -1, y: -1 },
topCenter: { x: 0, y: -1 },
topRight: { x: 1, y: -1 },
middleLeft: { x: -1, y: 0 },
middleCenter: { x: 0, y: 0 },
middleRight: { x: 1, y: 0 },
bottomLeft: { x: -1, y: 1 },
bottomCenter: { x: 0, y: 1 },
bottomRight: { x: 1, y: 1 },
};
/**
* All possible values of {@link Handle}.
*
* @ignore
*/
const HANDLES = [
...SCALE_HANDLES,
'rotator',
'skewHorizontal',
'skewVertical',
];
/**
* The default tolerance for scaling by dragging the bounding-box edges.
*
* @ignore
*/
const DEFAULT_BOX_SCALING_TOLERANCE = 4;
/**
* The default tolerance for box-rotation handles.
*
* @ignore
*/
const DEFUALT_BOX_ROTATION_TOLERANCE = 16;
/**
* The default snap angles for rotation, in radians.
*
* @ignore
*/
const DEFAULT_ROTATION_SNAPS = [
Math.PI / 4,
Math.PI / 2,
Math.PI * 3 / 4,
Math.PI,
0,
-Math.PI / 4,
-Math.PI / 2,
-Math.PI * 3 / 4,
-Math.PI,
];
/**
* The default snap tolerance, i.e. the maximum angle b/w the pointer & nearest snap ray for snapping.
*
* @ignore
*/
const DEFAULT_ROTATION_SNAP_TOLERANCE = Math.PI / 90;
/**
* The default snap angles for skewing, in radians.
*
* @ignore
*/
const DEFAULT_SKEW_SNAPS = [
Math.PI / 4,
-Math.PI / 4,
];
/**
* The default snap tolerance for skewing.
*
* @ignore
*/
const DEFAULT_SKEW_SNAP_TOLERANCE = Math.PI / 90;
/**
* @ignore
*/
export interface ITransformerStyle
{
color: number;
thickness: number;
}
/**
* The default wireframe style for {@link Transformer}.
*
* @ignore
*/
const DEFAULT_WIREFRAME_STYLE: ITransformerStyle = {
color: 0x000000,
thickness: 2,
};
/**
* @public
*/
export interface ITransformerCursors {
default: string;
boxRotate?: string;
boxScaleLeft?: string;
boxScaleTop?: string;
boxScaleRight?: string;
boxScaleBottom?: string;
translate?: string;
}
/**
* @public
*/
export interface ITransformerOptions
{
/** "all" or "groupOnly". "groupOnly" won't show individual bounding boxes. */
boundingBoxes?: 'all' | 'groupOnly' | 'none';
/** Enable box rotation by dragging outside of corners */
boxRotationEnabled?: boolean;
/** The radial tolerance for box rotation */
boxRotationTolerance?: number;
/** Enable box scaling by dragging at the edges */
boxScalingEnabled?: boolean;
/** The tolerance for scaling at the edges */
boxScalingTolerance: number;
/** Whether scaling should always be centered */
centeredScaling: boolean;
/** */
cursors?: ITransformerCursors;
/** Specifically define which handles are to be enabled */
enabledHandles?: Array<Handle>;
/** The group of display-objects being transformed */
group: DisplayObject[];
/** A custom transformer-handle class */
handleConstructor: typeof TransformerHandle;
/** Styling options for the handle. These cannot be modified afterwards! */
handleStyle: Partial<ITransformerHandleStyle>;
/** Lock aspect ratio when scaling using one of the corner handles. */
lockAspectRatio?: boolean;
/** Whether rotate handles are enabled */
rotateEnabled?: boolean;
/**
* The rotation snap angles, in radians. By default, transformer will
* snap for each 1/8th of a revolution.
*/
rotationSnaps?: number[];
/** The snap tolerance for rotation in radians */
rotationSnapTolerance?: number;
/** Whether scale handles are enabled */
scaleEnabled?: boolean;
/** Whether skew handles are enabled */
skewEnabled?: boolean;
/**
* Distance of skew handles from center of transformer box
* (`skewTransform` should be enabled)
*/
skewRadius?: number;
/** The skew snap angles, in radians. */
skewSnaps?: number[];
/** The skew snap tolerance angle. */
skewSnapTolerance?: number;
/** The root object in your scene in which the transformed objects exist. */
stage: Container;
/** Whether dragging the transformer should move the group */
translateEnabled?: boolean;
/**
* Whether the transformer should reset the wireframe's rotation
* after a rotator handle is "defocused".
*/
transientGroupTilt?: boolean;
/** Styling options for the wireframe. */
wireframeStyle: Partial<ITransformerStyle>;
}
/**
* {@code Transformer} provides an interactive interface for editing the transforms in a group. It supports translating,
* scaling, rotating, and skewing display-objects both through interaction and code.
*
* A transformer operates in world-space, and it is best to not position, scale, rotate, or skew one. If you do so, the
* wireframe itself will not distort (i.e. will adapt _against_ your transforms). However, the wireframe may become
* thinner/thicker and the handles will scale & rotate. For example, setting `transformer.scale.set(2)` will make the handles
* twice as big, but will not scale the wireframe (assuming the display-object group itself has not been
* scaled up).
*
* To enable scaling via dragging the edges of the wireframe, set `boxScalingEnabled` to `true`.
*
* NOTE: The transformer needs to capture all interaction events that would otherwise go to the display-objects in the
* group. Hence, it must be placed after them in the scene graph.
*/
export class Transformer extends Container
{
/** The group of display-objects under transformation. */
public group: DisplayObject[];
/**
* Specify which bounding boxes should be drawn in the wireframe.
*
* "groupOnly" won't show individual bounding boxes. "none" will not render anything visible.
*
* @default "all"
*/
public boundingBoxes: 'all' | 'groupOnly' | 'none';
/** Set this to enable rotation at the four corners */
public boxRotationEnabled: boolean;
/** The thickness of the box rotation area */
public boxRotationTolerance: number;
/** Set this to enable scaling by dragging at the edges of the bounding box */
public boxScalingEnabled: boolean;
/** The padding around the bounding-box to capture dragging on the edges. */
public boxScalingTolerance: number;
/** This will prevent the wireframe's center from shifting on scaling. */
public centeredScaling: boolean;
/** Cursors to use in the transformer */
public cursors: ITransformerCursors;
/**
* Flags whether the transformer should **not** redraw each frame (good for performance)
*
* @default false
*/
public lazyMode: boolean;
/** Set this when you want the transformer to redraw when using {@link Transformer#lazyMode lazyMode}. */
public lazyDirty: boolean;
/** Lock aspect ratio when using one of the corner handles. */
public lockAspectRatio: boolean;
/**
* This is used when the display-object group are rendered through a projection transformation (i.e. are disconnected
* from the transformer in the scene graph). The transformer project itself into their frame-of-reference using this
* transform.
*
* Specifically, the projection-transform converts points from the group's world space to the transformer's world
* space. If you are not applying a projection on the transformer itself, this means it is the group's
* world-to-screen transformation.
*/
public projectionTransform: Matrix;
/** The angles at which rotation should snap. */
public rotationSnaps: number[];
/** The maximum angular difference for snapping rotation. */
public rotationSnapTolerance: number;
/** The distance of skewing handles from the group's center. */
public skewRadius: number;
/** The angles at which both the horizontal & vertical skew handles should snap. */
public skewSnaps: number[];
/**
* The maximum angular difference for snapping skew handles.
*/
public skewSnapTolerance: number;
/**
* The root object in your scene in which objects can move.
*
* {@code Transformer} will subscribe to this object for `pointermove` events, if provided. This
* should be used when:
*
* * {@link InteractionManager.moveWhenInside moveWhenInside} is enabled on the interaction plugin.
* * {@link EventBoundary.moveOnAll moveOnAll} is not turned off when using the new {@link EventSystem}.
*
* Otherwise, the transformer will receive **not** `pointermove` events when the user drags fast enough that
* the cursor leaves the transformer's bounds.
*
* The stage must be fully interactive in the area you want objects to move. Generally, this is the
* whole canvas:
*
* ```ts
* stage.interactive = true;
* stage.hitArea = renderer.screen;// or pass custom rect for the canvas dimensions
* ```
*/
public stage: Container | null;
/**
* This will enable translation on dragging the transformer. By default, it is turned on.
*
* @default true
*/
public translateEnabled: boolean;
/**
* This will reset the rotation angle after the user finishes rotating a group with more than one display-object.
*
* @default true
*/
public transientGroupTilt: boolean;
/** The last calculated bounds of the whole group being transformed */
protected groupBounds: OrientedBounds;
/** Object mapping handle-names to the handle display-objects. */
protected handles: { [H in Handle]?: TransformerHandle };
/**
* Positions of the various handles
*
* @internal
* @ignore
*/
public handleAnchors: { [H in Handle]: Point };
/** Draws the bounding boxes */
protected wireframe: TransformerWireframe;
/** @see Transformer#enabledHandles */
protected _enabledHandles: Handle[];
/** @see Transformer#rotateEnabled */
protected _rotateEnabled: boolean;
/** @see Transformer#scaleEnabled */
protected _scaleEnabled: boolean;
/** @see Transformer#skewEnabled */
protected _skewEnabled: boolean;
/** The horizontal skew value. Rotating the group by 𝜽 will also change this value by 𝜽. */
protected _skewX: number;
/** The vertical skew value. Rotating the group by 𝜽 will also change this value by 𝜽. */
protected _skewY: number;
/** The currently grabbed handle. This can be used to get the type of transformation. */
protected _transformHandle: Handle;
/** The current type of transform being applied by the user. */
protected _transformType: 'translate' | 'scale' | 'rotate' | 'skew' | 'none';
/** The style applied on transformer handles */
protected _handleStyle: Partial<ITransformerHandleStyle>;
/** The wireframe style applied on the transformer */
protected _wireframeStyle: Partial<ITransformerStyle>;
private _pointerDown: boolean;
private _pointerDragging: boolean;
private _pointerPosition: Point;
private _pointerMoveTarget: DisplayObject;
/* eslint-disable max-len */
/**
* | Handle | Type | Notes |
* | --------------------- | ------------------------ | ----- |
* | rotator | Rotate | |
* | boxRotateTopLeft | Rotate | Invisible |
* | boxRotateTopRight | Rotate | Invisible |
* | boxRotateBottomLeft | Rotate | Invisible |
* | boxRotateBottomRight | Rotate | Invisible |
* | topLeft | Scale | |
* | topCenter | Scale | |
* | topRight | Scale | |
* | middleLeft | Scale | |
* | middleCenter | Scale | This cannot be enabled! |
* | middleRight | Scale | |
* | bottomLeft | Scale | |
* | bottomCenter | Scale | |
* | bottomRight | Scale | |
* | skewHorizontal | Skew | Applies vertical shear. Handle segment is horizontal at skew.y = 0! |
* | skewVertical | Skew | Applied horizontal shear. Handle segment is vertical at skew.x = 0! |
*/
constructor(options: Partial<ITransformerOptions> = {})
{
/* eslint-enable max-len */
super();
this.interactive = true;
this.cursors = Object.assign({ default: 'move' }, options.cursors);
this.cursor = this.cursors.default;
this.boundingBoxes = options.boundingBoxes || 'all';
this.group = options.group || [];
this.boxRotationTolerance = options.boxRotationTolerance || DEFUALT_BOX_ROTATION_TOLERANCE;
this.boxScalingTolerance = options.boxScalingTolerance || DEFAULT_BOX_SCALING_TOLERANCE;
this.centeredScaling = !!options.centeredScaling;
this.projectionTransform = new Matrix();
this.lockAspectRatio = options.lockAspectRatio === true;
this.rotationSnaps = options.rotationSnaps || DEFAULT_ROTATION_SNAPS;
this.rotationSnapTolerance = options.rotationSnapTolerance !== undefined
? options.rotationSnapTolerance
: DEFAULT_ROTATION_SNAP_TOLERANCE;
this.skewRadius = options.skewRadius || 64;
this.skewSnaps = options.skewSnaps || DEFAULT_SKEW_SNAPS;
this.skewSnapTolerance = options.skewSnapTolerance !== undefined
? options.skewSnapTolerance
: DEFAULT_SKEW_SNAP_TOLERANCE;
this.boxRotationEnabled = options.boxRotationEnabled === true;
this.boxScalingEnabled = options.boxScalingEnabled === true;
this._rotateEnabled = options.rotateEnabled !== false;
this._scaleEnabled = options.scaleEnabled !== false;
this._skewEnabled = options.skewEnabled === true;
this.translateEnabled = options.translateEnabled !== false;
this.transientGroupTilt = options.transientGroupTilt !== undefined ? options.transientGroupTilt : true;
this.wireframe = this.addChild(new TransformerWireframe(this));
this.wireframe.cursor = 'none';
this.stage = options.stage || null;
this._skewX = 0;
this._skewY = 0;
this._transformType = 'none';
this._wireframeStyle = Object.assign({}, DEFAULT_WIREFRAME_STYLE, options.wireframeStyle || {});
const HandleConstructor = options.handleConstructor || TransformerHandle;
const handleStyle = options.handleStyle || {};
this._handleStyle = handleStyle;
// Initialize transformer handles
const rotatorHandles = {
rotator: this.addChild(
new HandleConstructor(
this,
'rotator',
handleStyle,
(pointerPosition) =>
{
// The origin is the rotator handle's position, yes.
this.rotateGroup('rotator', pointerPosition);
},
this.commitGroup,
),
),
};
const scaleHandles = SCALE_HANDLES.reduce((scaleHandles, handleKey: ScaleHandle) =>
{
const handle = new HandleConstructor(
this,
handleKey,
handleStyle,
null,
this.commitGroup,
HANDLE_TO_CURSOR[handleKey]);
handle.onHandleDelta = (pointerPosition: Point): void =>
{
// Scale handles can be swapped with each other, i.e. handle.handle can change!
this.scaleGroup(handle.handle as ScaleHandle, pointerPosition);
};
handle.visible = this._scaleEnabled;
scaleHandles[handleKey] = handle;
this.addChild(scaleHandles[handleKey]);
return scaleHandles;
}, {});
const skewHandles = {
skewHorizontal: this.addChild(
new HandleConstructor(
this,
'skewHorizontal',
handleStyle,
(pointerPosition: Point) => { this.skewGroup('skewHorizontal', pointerPosition); },
this.commitGroup,
'pointer',
)),
skewVertical: this.addChild(
new HandleConstructor(
this,
'skewVertical',
handleStyle,
(pointerPosition: Point) => { this.skewGroup('skewVertical', pointerPosition); },
this.commitGroup,
'pointer',
)),
};
// Scale handles have higher priority
this.handles = Object.assign({}, scaleHandles, rotatorHandles, skewHandles) as { [H in Handle]?: TransformerHandle };
this.handles.middleCenter.visible = false;
this.handles.skewHorizontal.visible = this._skewEnabled;
this.handles.skewVertical.visible = this._skewEnabled;
this.handleAnchors = {
rotator: new Point(),
boxRotateTopLeft: new Point(),
boxRotateTopRight: new Point(),
boxRotateBottomLeft: new Point(),
boxRotateBottomRight: new Point(),
topLeft: new Point(),
topCenter: new Point(),
topRight: new Point(),
middleLeft: new Point(),
middleCenter: new Point(),
middleRight: new Point(),
bottomLeft: new Point(),
bottomCenter: new Point(),
bottomRight: new Point(),
skewHorizontal: new Point(),
skewVertical: new Point(),
};
// Update groupBounds immediately. This is because mouse events can propagate before the next animation frame.
this.groupBounds = new OrientedBounds();
this.updateGroupBounds();
// Pointer events
this._pointerDown = false;
this._pointerDragging = false;
this._pointerPosition = new Point();
this._pointerMoveTarget = null;
this.on('pointerdown', this.onPointerDown, this);
this.on('pointerup', this.onPointerUp, this);
this.on('pointerupoutside', this.onPointerUp, this);
}
/** The list of enabled handles, if applied manually. */
get enabledHandles(): Array<Handle>
{
return this._enabledHandles;
}
set enabledHandles(value: Array<Handle>)
{
if (!this._enabledHandles && !value)
{
return;
}
this._enabledHandles = value;
HANDLES.forEach((handleKey) => { this.handles[handleKey].visible = false; });
if (value)
{
value.forEach((handleKey) => { this.handles[handleKey].visible = true; });
}
else
{
this.handles.rotator.visible = this._rotateEnabled;
this.handles.skewHorizontal.visible = this._skewEnabled;
this.handles.skewVertical.visible = this._skewEnabled;
SCALE_HANDLES.forEach((handleKey) =>
{
if (handleKey === 'middleCenter') return;
this.handles[handleKey].visible = this._scaleEnabled;
});
}
}
/** The currently applied handle style. If you have edited the transformer handles directly, this may be inaccurate. */
get handleStyle(): Partial<ITransformerHandleStyle>
{
return this._handleStyle;
}
set handleStyle(value: Partial<ITransformerHandleStyle>)
{
const handles = this.handles;
for (const handleKey in handles)
{
(handles[handleKey] as TransformerHandle).style = value;
}
this._handleStyle = value;
}
/** This will enable the rotate handles. */
get rotateEnabled(): boolean
{
return this._rotateEnabled;
}
set rotateEnabled(value: boolean)
{
if (this._rotateEnabled !== value)
{
this._rotateEnabled = value;
if (this._enabledHandles)
{
return;
}
this.handles.rotator.visible = value;
}
}
/** This will enable the scale handles. */
get scaleEnabled(): boolean
{
return this._scaleEnabled;
}
set scaleEnabled(value: boolean)
{
if (this._scaleEnabled !== value)
{
this._scaleEnabled = value;
if (this._enabledHandles)
{
return;
}
SCALE_HANDLES.forEach((handleKey) =>
{
if (handleKey === 'middleCenter')
{
return;
}
this.handles[handleKey].visible = value;
});
}
}
/** This will enable the skew handles. */
get skewEnabled(): boolean
{
return this._skewEnabled;
}
set skewEnabled(value: boolean)
{
if (this._skewEnabled !== value)
{
this._skewEnabled = value;
if (this._enabledHandles)
{
return;
}
this.handles.skewHorizontal.visible = value;
this.handles.skewVertical.visible = value;
}
}
/**
* This is the type of transformation being applied by the user on the group. It can be inaccurate if you call one of
* `translateGroup`, `scaleGroup`, `rotateGroup`, `skewGroup` without calling `commitGroup` afterwards.
*
* @readonly
*/
get transformType(): 'translate' | 'scale' | 'rotate' | 'skew' | 'none'
{
return this._transformType;
}
/** The currently applied wireframe style. */
get wireframeStyle(): Partial<ITransformerStyle>
{
return this._wireframeStyle;
}
set wireframeStyle(value: Partial<ITransformerStyle>)
{
this._wireframeStyle = Object.assign({}, DEFAULT_WIREFRAME_STYLE, value);
}
/**
* @param forceUpdate - forces a recalculation of the group bounds
* @returns the oriented bounding box of the wireframe
*/
getGroupBounds(forceUpdate = false): OrientedBounds
{
if (forceUpdate)
{
this.updateGroupBounds();
}
return this.groupBounds;
}
/**
* This will translate the group by {@code delta} in their world-space.
*
* NOTE: There is no handle that provides translation. The user drags the transformer directly.
*
* @param delta
*/
translateGroup = (delta: Point): void =>
{
this._transformHandle = null;
this._transformType = 'translate';
// Translation matrix
const matrix = tempMatrix
.identity()
.translate(delta.x, delta.y);
this.prependTransform(matrix);
};
/**
* This will rotate the group such that the handle will come to {@code pointerPosition}.
*
* @param handle - the rotator handle was dragged
* @param pointerPosition - the new pointer position, in screen space
*/
rotateGroup = (handle: RotateHandle, pointerPosition: Point): void =>
{
this._transformHandle = handle;
this._transformType = 'rotate';
const bounds = this.groupBounds;
const handlePosition = this.worldTransform.apply(this.handleAnchors[handle], tempPoint);
this.projectionTransform.applyInverse(handlePosition, handlePosition);
pointerPosition = this.projectionTransform.applyInverse(pointerPosition, tempPointer);
// Center of rotation - does not change in transformation
const rOrigin = bounds.center;
// Original tilt
const orgAngle = Math.atan2(handlePosition.y - rOrigin.y, handlePosition.x - rOrigin.x);
// Final tilt
const dstAngle = Math.atan2(pointerPosition.y - rOrigin.y, pointerPosition.x - rOrigin.x);
// The angle by which bounds should be rotated
let deltaAngle = dstAngle - orgAngle;
// Snap
let newRotation = this.groupBounds.rotation + deltaAngle;
newRotation = this.snapAngle(newRotation, this.rotationSnapTolerance, this.rotationSnaps);
deltaAngle = newRotation - this.groupBounds.rotation;
// Rotation matrix
const matrix = tempMatrix
.identity()
.translate(-rOrigin.x, -rOrigin.y)
.rotate(deltaAngle)
.translate(rOrigin.x, rOrigin.y);
this.prependTransform(matrix, true);
this.updateGroupBounds(newRotation);
// Rotation moves both skew.x & skew.y
this._skewX += deltaAngle;
this._skewY += deltaAngle;
};
/**
* This will scale the group such that the scale handle will come under {@code pointerPosition}.
*
* @param handle - the scaling handle that was dragged
* @param pointerPosition - the new pointer position, in screen space
*/
scaleGroup = (handle: ScaleHandle, pointerPosition: Point): void =>
{
this._transformHandle = handle;
this._transformType = 'scale';
// Directions along x,y axes that will produce positive scaling
const xDir = SCALE_COMPONENTS[handle].x;
const yDir = SCALE_COMPONENTS[handle].y;
const handles = this.handles;
const bounds = this.groupBounds;
const angle = bounds.rotation;
const innerBounds = bounds.innerBounds;
// Position of handle in the group's world-space
const handlePosition = this.worldTransform.apply(this.handleAnchors[handle], tempPoint);
this.projectionTransform.applyInverse(handlePosition, handlePosition);
pointerPosition = this.projectionTransform.applyInverse(pointerPosition, tempPointer);
// Delta vector in world frame
const dx = pointerPosition.x - handlePosition.x;
const dy = pointerPosition.y - handlePosition.y;
// Unit vector along u-axis (horizontal axis after rotation) of bounds
const uxvec = (bounds.topRight.x - bounds.topLeft.x) / innerBounds.width;
const uyvec = (bounds.topRight.y - bounds.topLeft.y) / innerBounds.width;
// Unit vector along v-axis (vertical axis after rotation) of bounds
const vxvec = (bounds.bottomLeft.x - bounds.topLeft.x) / innerBounds.height;
const vyvec = (bounds.bottomLeft.y - bounds.topLeft.y) / innerBounds.height;
// Delta vector in rotated frame of bounds
const du = (dx * uxvec) + (dy * uyvec);
const dv = (dx * vxvec) + (dy * vyvec);
// Scaling factors along x,y axes
let sx = 1 + (du * xDir / innerBounds.width);
let sy = 1 + (dv * yDir / innerBounds.height);
// Only lock aspect ratio if using a handle that scales along both axes.
const lockAspectRatio = this.lockAspectRatio && (
handle === 'topLeft' ||
handle === 'topRight' ||
handle === 'bottomLeft' ||
handle === 'bottomRight'
);
if (lockAspectRatio)
{
if (sx > sy)
{
sy = sx;
}
else
{
sx = sy;
}
}
const matrix = tempMatrix.identity();
// NOTE: Do not apply scaling when sx,sy = 0 to prevent matrices from being degenerate.
if (xDir !== 0 && sx !== 0)
{
// Origin of horizontal scaling - a point which does not move after applying the transform
// eslint-disable-next-line no-nested-ternary
const hsOrigin = !this.centeredScaling ? (xDir === 1 ? bounds.topLeft : bounds.topRight) : bounds.center;
matrix.translate(-hsOrigin.x, -hsOrigin.y)
.rotate(-angle)
.scale(sx, 1)
.rotate(angle)
.translate(hsOrigin.x, hsOrigin.y);
}
if (yDir !== 0 && sy !== 0)
{
// Origin of vertical scaling - a point which does not move after applying the transform
// eslint-disable-next-line no-nested-ternary
const vsOrigin = !this.centeredScaling ? (yDir === 1 ? bounds.topLeft : bounds.bottomLeft) : bounds.center;
matrix.translate(-vsOrigin.x, -vsOrigin.y)
.rotate(-angle)
.scale(1, sy)
.rotate(angle)
.translate(vsOrigin.x, vsOrigin.y);
}
// Handles flips along x & y axis. Handles are always flipped along the y-axis, however. This is
// because a negative x-scale adds 180° to the rotation - as a result, the handles are automatically
// flipped along the x-axis but also the y-axis - and this needs to be reversed (by flipping again).
//
// NOTE: When both x & y axes are flipped, then there is no need for swapping b/c they cancel out.
if ((sy < 0 || sx < 0) && !(sy < 0 && sx < 0))
{
switch (handle)
{
case 'topLeft':
case 'bottomLeft':
this.swapHandles(handles.topLeft, handles.bottomLeft);
break;
case 'topCenter':
case 'bottomCenter':
this.swapHandles(handles.topCenter, handles.bottomCenter);
break;
case 'topRight':
case 'bottomRight':
this.swapHandles(handles.topRight, handles.bottomRight);
break;
}
}
this.prependTransform(matrix);
};
/**
* This will skew the group such that the skew handle would move to the {@code pointerPosition}.
*
* @param handle
* @param pointerPosition - pointer position, in screen space
*/
skewGroup = (handle: SkewHandle, pointerPosition: Point): void =>
{
this._transformHandle = handle;
this._transformType = 'skew';
const bounds = this.groupBounds;
// Destination point
const dst = tempPoint.copyFrom(pointerPosition);
this.projectionTransform.applyInverse(dst, dst);
// Center of skew (same as center of rotation!)
const sOrigin = bounds.center;
// Skew matrix
const matrix = tempMatrix.identity()
.translate(-sOrigin.x, -sOrigin.y);
let rotation = this.groupBounds.rotation;
if (handle === 'skewHorizontal')
{
const oldSkew = this._skewX;
// Calculate new skew
this._skewX = Math.atan2(dst.y - sOrigin.y, dst.x - sOrigin.x);
this._skewX = this.snapAngle(this._skewX, this.skewSnapTolerance, this.skewSnaps);
// Skew by new skew.x
matrix.prepend(createVerticalSkew(-oldSkew));
matrix.prepend(createVerticalSkew(this._skewX));
}
else // skewVertical
{
const oldSkew = this._skewY;
// Calculate new skew
const newSkew = Math.atan2(dst.y - sOrigin.y, dst.x - sOrigin.x) - (Math.PI / 2);
this._skewY = newSkew;
this._skewY = this.snapAngle(this._skewY, this.skewSnapTolerance, this.skewSnaps);
// HINT: skewY is applied negatively b/c y-axis is flipped
matrix.prepend(createHorizontalSkew(oldSkew));
matrix.prepend(createHorizontalSkew(-this._skewY));
rotation -= this._skewY - oldSkew;
}
matrix.translate(sOrigin.x, sOrigin.y);
this.prependTransform(matrix, true);
this.updateGroupBounds(rotation);
};
/**
* This is called after the user finishes dragging a handle. If {@link this.transientGroupTilt} is enabled, it will
* reset the rotation of this group (if more than one display-object is grouped).
*/
commitGroup = (): void =>
{
this._transformHandle = null;
this._transformType = 'none';
if (this.transientGroupTilt !== false && this.group.length > 1)
{
this.updateGroupBounds(0);
}
this.emit('transformcommit');
};
/**
* This will update the transformer's geometry and render it to the canvas.
*
* @override
* @param renderer
*/
render(renderer: Renderer): void
{
if (this.renderable && this.visible && (!this.lazyMode || this.lazyDirty))
{
this.draw();
}
super.render(renderer);
}
/** Recalculates the transformer's geometry. This is called on each render. */
protected draw(): void
{
const targets = this.group;
const { color, thickness } = this._wireframeStyle;
// Updates occur right here!
this.wireframe.clear();
if (this.boundingBoxes !== 'none')
{
this.wireframe.lineStyle(thickness, color);
}
if (this.translateEnabled)
{
this.wireframe.beginFill(0xffffff, 1e-4);
}
for (let i = 0, j = targets.length; i < j && this.boundingBoxes === 'all'; i++)
{
this.wireframe.drawBounds(Transformer.calculateOrientedBounds(targets[i], tempBounds));
}
// groupBounds may change on each render-loop b/c of any ongoing animation
const groupBounds = targets.length !== 1
? Transformer.calculateGroupOrientedBounds(targets, this.groupBounds.rotation, tempBounds, true)
: Transformer.calculateOrientedBounds(targets[0], tempBounds);// Auto-detect rotation
// Redraw skeleton and position handles
this.wireframe.drawBounds(groupBounds);
this.drawHandles(groupBounds);
// Update cached groupBounds
this.groupBounds.copyFrom(groupBounds);
if (this.boxRotationEnabled)
{
this.wireframe.closePath()
.beginFill(0xffffff, 1e-4)
.lineStyle();
this.wireframe.drawBoxRotationTolerance();
}
if (this.boxScalingEnabled)
{
this.wireframe
.closePath()
.beginFill(0xfff0ff, 1e-4)
.lineStyle();
this.wireframe.drawBoxScalingTolerance(groupBounds);
}
this.lazyDirty = false;
}
/**
* Draw the handles and any remaining parts of the wireframe.
*
* @param groupBounds
*/
protected drawHandles(groupBounds: OrientedBounds): void
{
const handles = this.handles;
const handleAnchors = this.handleAnchors;
const {
topLeft: worldTopLeft,
topRight: worldTopRight,
bottomLeft: worldBottomLeft,
bottomRight: worldBottomRight,
center: worldCenter,
} = groupBounds;
const [topLeft, topRight, bottomLeft, bottomRight] = tempHull;
const center = tempPoint;
this.projectToLocal(worldTopLeft, topLeft);
this.projectToLocal(worldTopRight, topRight);
this.projectToLocal(worldBottomLeft, bottomLeft);
this.projectToLocal(worldBottomRight, bottomRight);
this.projectToLocal(worldCenter, center);
handleAnchors.topLeft.copyFrom(topLeft);
handleAnchors.topCenter.set((topLeft.x + topRight.x) / 2, (topLeft.y + topRight.y) / 2);
handleAnchors.topRight.copyFrom(topRight);
handleAnchors.middleLeft.set((topLeft.x + bottomLeft.x) / 2, (topLeft.y + bottomLeft.y) / 2);
handleAnchors.middleCenter.set((topLeft.x + bottomRight.x) / 2, (topLeft.y + bottomRight.y) / 2);
handleAnchors.middleRight.set((topRight.x + bottomRight.x) / 2, (topRight.y + bottomRight.y) / 2);
handleAnchors.bottomLeft.copyFrom(bottomLeft);
handleAnchors.bottomCenter.set((bottomLeft.x + bottomRight.x) / 2, (bottomLeft.y + bottomRight.y) / 2);
handleAnchors.bottomRight.copyFrom(bottomRight);
if (this.boxRotationEnabled)
{
handleAnchors.boxRotateTopLeft.copyFrom(handleAnchors.topLeft);
handleAnchors.boxRotateTopRight.copyFrom(handleAnchors.topRight);
handleAnchors.boxRotateBottomLeft.copyFrom(handleAnchors.bottomLeft);
handleAnchors.boxRotateBottomRight.copyFrom(handleAnchors.bottomRight);
}
if (this._rotateEnabled)
{
// Midpoint from topLeft to topRight
const bx = (topLeft.x + topRight.x) / 2;
const by = (topLeft.y + topRight.y) / 2;
// Vector perpendicular to <bx,by>.
let px = -(topLeft.y - topRight.y);
let py = (topLeft.x - topRight.x);
// Normalize <px,py> to 32 units.
const pl = Math.sqrt((px * px) + (py * py));
px *= 32 / pl;
py *= 32 / pl;
handles.rotator.position.x = bx + px;
handles.rotator.position.y = by + py;
this.wireframe.moveTo(bx, by)
.lineTo(handles.rotator.position.x, handles.rotator.position.y);
this.handleAnchors.rotator.copyFrom(handles.rotator.position);
}
if (this._skewEnabled)
{
const cx = center.x;
const cy = center.y;
// Transform center into screen space
this.worldTransform.apply(center, center);
// Calculate skew handle positions in screen space, and then transform back into local-space. This ensures that
// the handles appear at skewRadius distance, regardless of the projection.
handleAnchors.skewHorizontal.set(
center.x + (Math.cos(this._skewX) * this.skewRadius),
center.y + (Math.sin(this._skewX) * this.skewRadius));
handleAnchors.skewVertical.set( // HINT: Slope = skew.y + Math.PI / 2
center.x + (-Math.sin(this._skewY) * this.skewRadius),
center.y + (Math.cos(this._skewY) * this.skewRadius));
this.worldTransform.applyInverse(handleAnchors.skewHorizontal, handleAnchors.skewHorizontal);
this.worldTransform.applyInverse(handleAnchors.skewVertical, handleAnchors.skewVertical);
// Restore center to local-space
center.set(cx, cy);
this.wireframe
.beginFill(this.wireframeStyle.color)
.drawCircle(center.x, center.y, this.wireframeStyle.thickness * 2)
.endFill();
this.wireframe
.moveTo(center.x, center.y)
.lineTo(handles.skewHorizontal.x, handles.skewHorizontal.y)
.moveTo(center.x, center.y)
.lineTo(handles.skewVertical.x, handles.skewVertical.y);
}
// Update transforms
for (const handleName in handles)
{
let rotation = this.groupBounds.rotation;
if (handleName === 'skewHorizontal')
{
rotation = this._skewX;
}
else if (handleName === 'skewVertical')
{
rotation = this._skewY;
}
const handle: TransformerHandle = handles[handleName];
handle.rotation = rotation;
handle.position.copyFrom(handleAnchors[handleName]);
handle.getBounds(false, tempRect);
}
}
/** Called on the `pointerdown` event. You must call the super implementation. */
protected onPointerDown(e: InteractionEvent): void
{
this._pointerDown = true;
this._pointerDragging = false;
e.stopPropagation();
if (this._pointerMoveTarget)
{
this._pointerMoveTarget.off('pointermove', this.onPointerMove, this);
this._pointerMoveTarget = null;
}
this._pointerMoveTarget = this.stage || this;
this._pointerMoveTarget.on('pointermove', this.onPointerMove, this);
}
/** Called on the `pointermove` event. You must call the super implementation. */
protected onPointerMove(e: InteractionEvent): void
{
const lastPointerPosition = this._pointerPosition;
const currentPointerPosition = pointPool.allocate().copyFrom(e.data.global);
const hoveredHandle = this.wireframe.hitHandleType(
this.groupBounds,
this.projectionTransform,
currentPointerPosition);
if (!this._pointerDown)
{
this.setCursorFromHoveredHandle(hoveredHandle);
}
else
{
const cx = currentPointerPosition.x;
const cy = currentPointerPosition.y;
// Translate group by difference
if (this._pointerDragging)
{
switch (this._transformHandle)
{
case 'boxRotateTopLeft':
case 'boxRotateTopRight':
case 'boxRotateBottomLeft':
case 'boxRotateBottomRight':
this.rotateGroup(this._transformHandle, currentPointerPosition);
break;
case 'topCenter':
case 'middleLeft':
case 'middleRight':
case 'bottomCenter':
this.scaleGroup(this._transformHandle, currentPointerPosition);
break;
default:
{
if (this.translateEnabled)
{
const [worldOrigin, worldDestination, worldDelta] = tempHull;
// HINT: The pointer has moved from lastPointerPosition to currentPointerPosition in the
// transformer's world space. However, we want to translate the display-object's in their
// world space; to do this, we project (0,0) and the delta into their world-space, and take
// the difference.
worldOrigin.set(0, 0);
worldDestination.set(
currentPointerPosition.x - lastPointerPosition.x,
currentPointerPosition.y - lastPointerPosition.y);
this.projectionTransform.applyInverse(worldOrigin, worldOrigin);
this.projectionTransform.applyInverse(worldDestination, worldDestination);
worldDelta.set(worldDestination.x - worldOrigin.x, worldDestination.y - worldOrigin.y);
this.translateGroup(worldDelta);
}
}
}
}
else
{
this._transformHandle = this.wireframe.hitHandleType(
this.groupBounds,
this.projectionTransform,
currentPointerPosition);
this.setCursorFromHoveredHandle(hoveredHandle);
}
this._pointerPosition.x = cx;
this._pointerPosition.y = cy;
this._pointerDragging = true;
e.stopPropagation();
}
pointPool.release(currentPointerPosition);
}
/** Called on the `pointerup` and `pointerupoutside` events. You must call the super implementation. */
protected onPointerUp(e: InteractionEvent): void
{
this._pointerDragging = false;
this._pointerDown = false;
this.commitGroup();
e.stopPropagation();
if (this._pointerMoveTarget)
{
this._pointerMoveTarget.off('pointermove', this.onPointerMove, this);
this._pointerMoveTarget = null;
}
}
/**
* Applies the given transformation matrix {@code delta} to all the display-objects in the group.
*
* @param delta - transformation matrix
* @param skipUpdate - whether to skip updating the group-bounds after applying the transform
*/
private prependTransform(delta: Matrix, skipUpdate = false): void
{
const group = this.group;
for (let i = 0, j = group.length; i < j; i++)
{
multiplyTransform(group[i], delta, false);
}
emitMatrix.copyFrom(delta);
if (!skipUpdate)
{
this.updateGroupBounds();
}
this.emit('transformchange', emitMatrix);
}
/**
* Recalculates {@code this.groupBounds} at the same angle.
*
* @param rotation - override the group's rotation
*/
private updateGroupBounds(rotation: number = this.groupBounds.rotation): void
{
Transformer.calculateGroupOrientedBounds(this.group, rotation, this.groupBounds);
this.drawHandles(this.groupBounds);
this.lazyDirty = true;
}
/**
* Snaps the given {@code angle} to one of the snapping angles, if possible.
*
* @param angle - the input angle
* @param snapTolerance - the maximum difference b/w the given angle & a snapping angle
* @param snaps - the snapping angles
* @returns the snapped angle
*/
private snapAngle(angle: number, snapTolerance: number, snaps?: number[]): number
{
angle = angle % (Math.PI * 2);
if (!snaps || snaps.length === 1 || !snapTolerance)
{
return angle;
}
for (let i = 0, j = snaps.length; i < j; i++)
{
if (Math.abs(angle - snaps[i]) <= snapTolerance)
{
return snaps[i];
}
}
return angle;
}
/**
* Swap the handles represented by the two {@code TransformerHandle} instances.
*
* @param handle0
* @param handle1
*/
private swapHandles(handle0: TransformerHandle, handle1: TransformerHandle): void
{
const key0 = handle0.handle;
const key1 = handle1.handle;
const cursor0 = handle0.cursor;
const cursor1 = handle1.cursor;
const x0 = handle0.x;
const x1 = handle1.x;
const y0 = handle0.y;
const y1 = handle1.y;
handle0.handle = key1;
handle1.handle = key0;
handle0.position.set(x1, y1);
handle1.position.set(x0, y0);
handle0.cursor = cursor1;
handle1.cursor = cursor0;
this.handles[key0] = handle1;
this.handles[key1] = handle0;
if (this._transformHandle === key0)
{
this._transformHandle = key1;
}
else if (this._transformHandle === key1)
{
this._transformHandle = key0;
}
}
/** Set cursor from {@code this.cursors} based on the handle hovered currently. */
private setCursorFromHoveredHandle(hoveredHandle: string): void
{
if (hoveredHandle && hoveredHandle.startsWith('boxRotate'))
{
this.cursor = this.cursors.boxRotate || this.cursors.default;
}
else if (hoveredHandle)
{
switch (hoveredHandle)
{
case 'topCenter':
this.cursor = this.cursors.boxScaleTop || this.cursors.default;
break;
case 'middleLeft':
this.cursor = this.cursors.boxScaleLeft || this.cursors.default;
break;
case 'middleRight':
this.cursor = this.cursors.boxScaleRight || this.cursors.default;
break;
case 'bottomCenter':
this.cursor = this.cursors.boxScaleBottom || this.cursors.default;
break;
}
}
else
{
this.cursor = this.cursors.translate || this.cursors.default;
}
}
/**
* Projects {@code input} from the group's world space into the transformer's local space, and puts the result
* into {@code output}.
*
* @param input
* @param output
* @returns the output
*/
projectToLocal(input: Point, output?: Point): Point
{
if (!output)
{
output = new Point();
}
this.projectionTransform.apply(input, output);
this.worldTransform.applyInverse(output, output);
return output;
}
/**
* Calculates the positions of the four corners of the display-object. The quadrilateral formed by
* these points will be the tightest fit around it.
*
* @param displayObject - The display object whose corners are to be calculated
* @param transform - The transform applied on the display-object. By default, this is its world-transform
* @param corners - Optional array of four points to put the result into
* @param index - Optional index into "corners"
* @returns an array of four points holding the positions of the corners
*/
static calculateTransformedCorners(
displayObject: DisplayObject,
transform: Matrix = displayObject.worldTransform,
corners?: Point[],
index = 0,
): Point[]
{
const localBounds = displayObject.getLocalBounds();
// Don't modify transforms
displayObject.getBounds();
corners = corners || [new Point(), new Point(), new Point(), new Point()];
corners[index].set(localBounds.x, localBounds.y);
corners[index + 1].set(localBounds.x + localBounds.width, localBounds.y);
corners[index + 2].set(localBounds.x + localBounds.width, localBounds.y + localBounds.height);
corners[index + 3].set(localBounds.x, localBounds.y + localBounds.height);
transform.apply(corners[index], corners[index]);
transform.apply(corners[index + 1], corners[index + 1]);
transform.apply(corners[index + 2], corners[index + 2]);
transform.apply(corners[index + 3], corners[index + 3]);
return corners;
}
/**
* Calculates the oriented bounding box of the display-object. This would not bending with any skew
* applied on the display-object, i.e. it is guaranteed to be rectangular.
*
* @param displayObject
* @param bounds - the bounds instance to set
*/
static calculateOrientedBounds(displayObject: DisplayObject, bounds?: OrientedBounds): OrientedBounds
{
const parent = !displayObject.parent ? displayObject.enableTempParent() : displayObject.parent;
displayObject.updateTransform();
displayObject.disableTempParent(parent);
// Decompose displayObject.worldTransform to get its (world) rotation
decomposeTransform(tempTransform, displayObject.worldTransform);
tempTransform.updateLocalTransform();
const angle = tempTransform.rotation;
const corners = Transformer.calculateTransformedCorners(displayObject, displayObject.worldTransform, tempCorners);
// Calculate centroid, which is our center of rotation
const cx = (corners[0].x + corners[1].x + corners[2].x + corners[3].x) / 4;
const cy = (corners[0].y + corners[1].y + corners[2].y + corners[3].y) / 4;
// Unrotation matrix
const matrix = tempMatrix
.identity()
.translate(-cx, -cy)
.rotate(-tempTransform.rotation)
.translate(cx, cy);
// Calculate unrotated corners
matrix.apply(corners[0], corners[0]);
matrix.apply(corners[1], corners[1]);
matrix.apply(corners[2], corners[2]);
matrix.apply(corners[3], corners[3]);
bounds = bounds || new OrientedBounds();
bounds.rotation = angle;
bounds.innerBounds.x = Math.min(corners[0].x, corners[1].x, corners[2].x, corners[3].x);
bounds.innerBounds.y = Math.min(corners[0].y, corners[1].y, corners[2].y, corners[3].y);
bounds.innerBounds.width = Math.max(corners[0].x, corners[1].x, corners[2].x, corners[3].x) - bounds.innerBounds.x;
bounds.innerBounds.height = Math.max(corners[0].y, corners[1].y, corners[2].y, corners[3].y) - bounds.innerBounds.y;
return bounds;
}
/**
* Calculates the oriented bounding box of a group of display-objects at a specific angle.
*
* @param group
* @param rotation
* @param bounds
* @param skipUpdate
*/
static calculateGroupOrientedBounds(
group: DisplayObject[],
rotation: number,
bounds?: OrientedBounds,
skipUpdate = false,
): OrientedBounds
{
const groupLength = group.length;
const frames = pointPool.allocateArray(groupLength * 4);// Zero allocations!
// Calculate display-object frame vertices
for (let i = 0; i < groupLength; i++)
{
const displayObject = group[i];
// Update worldTransform
if (!skipUpdate)
{
const parent = !displayObject.parent ? displayObject.enableTempParent() : displayObject.parent;
displayObject.updateTransform();
displayObject.disableTempParent(parent);
}
Transformer.calculateTransformedCorners(displayObject, displayObject.worldTransform, frames, i * 4);
}
// Unrotation matrix
const matrix = tempMatrix
.identity()
.rotate(-rotation);
let minX = Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
let maxX = -Number.MAX_VALUE;
let maxY = -Number.MAX_VALUE;
// Unrotate all frame vertices, calculate minX, minY, maxX, maxY for innerBounds
for (let i = 0, j = frames.length; i < j; i++)
{
const point = frames[i];
matrix.apply(point, point);
const x = point.x;
const y = point.y;
minX = x < minX ? x : minX;
minY = y < minY ? y : minY;
maxX = x > maxX ? x : maxX;
maxY = y > maxY ? y : maxY;
}
pointPool.releaseArray(frames);
bounds = bounds || new OrientedBounds();
bounds.innerBounds.x = minX;
bounds.innerBounds.y = minY;
bounds.innerBounds.width = maxX - minX;
bounds.innerBounds.height = maxY - minY;
bounds.rotation = rotation;
matrix.applyInverse(bounds.center, tempPoint);
bounds.center.copyFrom(tempPoint);
return bounds;
}
}
/**
* This is fired when the transformer modifies the transforms of display-objects.
*
* @event Transformer#transformchange
* @type {Matrix}
*/
/**
* This is fired when the user lifts the mouse button after dragging a transformer handle. It can be used
*
* @event Transformer#transformcommit
*/