Matrix.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. // nolint: cyclo_complexity,method_line
  6. import { describe, test, expect, it } from 'vitest';
  7. import { Transform } from './Transform';
  8. import { Matrix as M, Matrix } from './Matrix';
  9. import { PI } from './const';
  10. describe('Matrix', () => {
  11. test('Matrix', async () => {
  12. expect(new M()).toEqual(M.IDENTITY);
  13. expect(new M()).toEqual(M.TEMP_MATRIX);
  14. });
  15. test('fromArray', async () => {
  16. expect(new M().fromArray([])).toEqual(M.IDENTITY);
  17. expect(new M().fromArray([1, 2, 3])).toEqual(M.IDENTITY);
  18. expect(new M().fromArray([0, 1, 2, 3, 4, 5])).toEqual(new M(0, 1, 3, 4, 2, 5));
  19. expect(new M().fromArray([0, 1, 2, 3, 4, 5, 6])).toEqual(new M(0, 1, 3, 4, 2, 5));
  20. });
  21. test('set', async () => {
  22. expect(new M().set(0, 1, 2, 3, 4, 5)).toEqual(new M(0, 1, 2, 3, 4, 5));
  23. });
  24. test('toArray', async () => {
  25. expect(new M(0, 1, 2, 3, 4, 5).toArray(true)).toEqual(
  26. Float32Array.from([0, 1, 0, 2, 3, 0, 4, 5, 1]),
  27. );
  28. expect(new M(0, 1, 2, 3, 4, 5).toArray(false)).toEqual(
  29. Float32Array.from([0, 2, 4, 1, 3, 5, 0, 0, 1]),
  30. );
  31. const arr = new Float32Array(9);
  32. new M(0, 1, 2, 3, 4, 5).toArray(false, arr);
  33. expect(arr).toEqual(Float32Array.from([0, 2, 4, 1, 3, 5, 0, 0, 1]));
  34. });
  35. test('apply', async () => {
  36. expect(M.IDENTITY.apply({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 });
  37. // translate only
  38. expect(new M(1, 0, 0, 1, 1, 1).apply({ x: 1, y: 2 })).toEqual({
  39. x: 2,
  40. y: 3,
  41. });
  42. // scale only
  43. expect(new M(2, 0, 0, 2).apply({ x: 1, y: 2 })).toEqual({ x: 2, y: 4 });
  44. // skew only
  45. expect(new M(1, 1, 1, 1).apply({ x: 1, y: 2 })).toEqual({ x: 3, y: 3 });
  46. expect(new M(1, 1, -1, 1).apply({ x: 1, y: 2 })).toEqual({ x: -1, y: 3 });
  47. });
  48. test('applyInverse', async () => {
  49. expect(M.IDENTITY.applyInverse({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 });
  50. // translate only
  51. expect(new M(1, 0, 0, 1, 1, 1).applyInverse({ x: 1, y: 2 })).toEqual({
  52. x: 0,
  53. y: 1,
  54. });
  55. // scale only
  56. expect(new M(2, 0, 0, 2).applyInverse({ x: 1, y: 2 })).toEqual({
  57. x: 0.5,
  58. y: 1,
  59. });
  60. // skew only
  61. expect(new M(1, 1, -1, 1).applyInverse({ x: 1, y: 2 })).toEqual({
  62. x: 1.5,
  63. y: 0.5,
  64. });
  65. });
  66. test('translate', async () => {
  67. expect(M.IDENTITY.translate(1, -2).apply({ x: 0, y: 0 })).toEqual({
  68. x: 1,
  69. y: -2,
  70. });
  71. });
  72. test('scale', async () => {
  73. expect(M.IDENTITY.scale(1, -2).apply({ x: 1, y: 2 })).toEqual({
  74. x: 1,
  75. y: -4,
  76. });
  77. expect(M.IDENTITY.scale(0, 0).apply({ x: 1, y: 2 })).toEqual({
  78. x: 0,
  79. y: 0,
  80. });
  81. });
  82. test('rotate', async () => {
  83. const r1 = M.IDENTITY.rotate(PI / 2).apply({ x: 1, y: 2 });
  84. expect(r1.x).toBeCloseTo(-2);
  85. expect(r1.y).toBeCloseTo(1);
  86. expect(M.IDENTITY.rotate(PI / 2).apply({ x: 0, y: 0 })).toEqual({
  87. x: 0,
  88. y: 0,
  89. });
  90. });
  91. test('append', async () => {
  92. expect(M.IDENTITY.append(M.IDENTITY)).toEqual(M.IDENTITY);
  93. expect(M.IDENTITY.append(new M(0, 1, 2, 3, 4, 5))).toEqual(new M(0, 1, 2, 3, 4, 5));
  94. expect(new M(0, 1, 2, 3, 4, 5).append(M.IDENTITY)).toEqual(new M(0, 1, 2, 3, 4, 5));
  95. expect(new M(0, 1, 2, 3, 4, 5).append(new M(0, 1, 2, 3, 4, 5))).toEqual(
  96. new M(2, 3, 6, 11, 14, 24),
  97. );
  98. });
  99. test('prepend', async () => {
  100. expect(M.IDENTITY.prepend(M.IDENTITY)).toEqual(M.IDENTITY);
  101. expect(M.IDENTITY.prepend(new M(0, 1, 2, 3, 4, 5))).toEqual(new M(0, 1, 2, 3, 4, 5));
  102. expect(new M(0, 1, 2, 3, 4, 5).prepend(M.IDENTITY)).toEqual(new M(0, 1, 2, 3, 4, 5));
  103. expect(new M(0, 1, 2, 3, 4, 5).prepend(new M(0, 1, 2, 3, 1, 2))).toEqual(
  104. new M(2, 3, 6, 11, 11, 21),
  105. );
  106. });
  107. test('identity', async () => {
  108. expect(new M(0, 1, 2, 3, 4, 5).identity()).toEqual(M.IDENTITY);
  109. });
  110. test('invert', async () => {
  111. expect(new M(0, 1, 2, 3, 4, 5).invert()).toEqual(new M(-1.5, 0.5, 1, -0, 1, -2));
  112. expect(new M(-1.5, 0.5, 1, -0, 1, -2).invert()).toEqual(new M(0, 1, 2, 3, 4, 5));
  113. // expect(M.IDENTITY.invert()).toEqual(M.IDENTITY)
  114. });
  115. test('copyTo', async () => {
  116. expect(new M(0, 1, 2, 3, 4, 5).copyTo(M.TEMP_MATRIX)).toEqual(new M(0, 1, 2, 3, 4, 5));
  117. });
  118. test('copyFrom', async () => {
  119. expect(M.TEMP_MATRIX.copyFrom(new M(0, 1, 2, 3, 4, 5))).toEqual(new M(0, 1, 2, 3, 4, 5));
  120. });
  121. test('isSimple', async () => {
  122. expect(new M(1, 0, 0, 1, 0, 0).isSimple()).toBeTruthy();
  123. expect(new M(0, 1, 2, 3, 4, 5).isSimple()).toBeFalsy();
  124. });
  125. /**
  126. * @see https://github.com/pixijs/pixijs/blob/dev/packages/math/test/Matrix.tests.ts
  127. */
  128. it('should create a new matrix', () => {
  129. const matrix = new Matrix();
  130. expect(matrix.a).toEqual(1);
  131. expect(matrix.b).toEqual(0);
  132. expect(matrix.c).toEqual(0);
  133. expect(matrix.d).toEqual(1);
  134. expect(matrix.tx).toEqual(0);
  135. expect(matrix.ty).toEqual(0);
  136. const input = [0, 1, 2, 3, 4, 5];
  137. matrix.fromArray(input);
  138. expect(matrix.a).toEqual(0);
  139. expect(matrix.b).toEqual(1);
  140. expect(matrix.c).toEqual(3);
  141. expect(matrix.d).toEqual(4);
  142. expect(matrix.tx).toEqual(2);
  143. expect(matrix.ty).toEqual(5);
  144. let output = matrix.toArray(true);
  145. expect(output.length).toEqual(9);
  146. expect(output[0]).toEqual(0);
  147. expect(output[1]).toEqual(1);
  148. expect(output[3]).toEqual(3);
  149. expect(output[4]).toEqual(4);
  150. expect(output[6]).toEqual(2);
  151. expect(output[7]).toEqual(5);
  152. output = matrix.toArray(false);
  153. expect(output.length).toEqual(9);
  154. expect(output[0]).toEqual(0);
  155. expect(output[1]).toEqual(3);
  156. expect(output[2]).toEqual(2);
  157. expect(output[3]).toEqual(1);
  158. expect(output[4]).toEqual(4);
  159. expect(output[5]).toEqual(5);
  160. });
  161. it('should apply different transforms', () => {
  162. const matrix = new Matrix();
  163. matrix.translate(10, 20);
  164. matrix.translate(1, 2);
  165. expect(matrix.tx).toEqual(11);
  166. expect(matrix.ty).toEqual(22);
  167. matrix.scale(2, 4);
  168. expect(matrix.a).toEqual(2);
  169. expect(matrix.b).toEqual(0);
  170. expect(matrix.c).toEqual(0);
  171. expect(matrix.d).toEqual(4);
  172. expect(matrix.tx).toEqual(22);
  173. expect(matrix.ty).toEqual(88);
  174. const m2 = matrix.clone();
  175. expect(m2).not.toBe(matrix);
  176. expect(m2.a).toEqual(2);
  177. expect(m2.b).toEqual(0);
  178. expect(m2.c).toEqual(0);
  179. expect(m2.d).toEqual(4);
  180. expect(m2.tx).toEqual(22);
  181. expect(m2.ty).toEqual(88);
  182. matrix.setTransform(14, 15, 0, 0, 4, 2, 0, 0, 0);
  183. expect(matrix.a).toEqual(4);
  184. expect(matrix.b).toEqual(0);
  185. // Object.is cant distinguish between 0 and -0
  186. expect(Math.abs(matrix.c)).toEqual(0);
  187. expect(matrix.d).toEqual(2);
  188. expect(matrix.tx).toEqual(14);
  189. expect(matrix.ty).toEqual(15);
  190. });
  191. it('should allow rotatation', () => {
  192. const matrix = new Matrix();
  193. matrix.rotate(Math.PI);
  194. expect(matrix.a).toEqual(-1);
  195. expect(matrix.b).toEqual(Math.sin(Math.PI));
  196. expect(matrix.c).toEqual(-Math.sin(Math.PI));
  197. expect(matrix.d).toEqual(-1);
  198. });
  199. it('should append matrix', () => {
  200. const m1 = new Matrix();
  201. const m2 = new Matrix();
  202. m2.tx = 100;
  203. m2.ty = 200;
  204. m1.append(m2);
  205. expect(m1.tx).toEqual(m2.tx);
  206. expect(m1.ty).toEqual(m2.ty);
  207. });
  208. it('should prepend matrix', () => {
  209. const m1 = new Matrix();
  210. const m2 = new Matrix();
  211. m2.set(2, 3, 4, 5, 100, 200);
  212. m1.prepend(m2);
  213. expect(m1.a).toEqual(m2.a);
  214. expect(m1.b).toEqual(m2.b);
  215. expect(m1.c).toEqual(m2.c);
  216. expect(m1.d).toEqual(m2.d);
  217. expect(m1.tx).toEqual(m2.tx);
  218. expect(m1.ty).toEqual(m2.ty);
  219. const m3 = new Matrix();
  220. const m4 = new Matrix();
  221. m3.prepend(m4);
  222. expect(m3.a).toEqual(m4.a);
  223. expect(m3.b).toEqual(m4.b);
  224. expect(m3.c).toEqual(m4.c);
  225. expect(m3.d).toEqual(m4.d);
  226. expect(m3.tx).toEqual(m4.tx);
  227. expect(m3.ty).toEqual(m4.ty);
  228. });
  229. it('should get IDENTITY and TEMP_MATRIX', () => {
  230. expect(Matrix.IDENTITY instanceof Matrix).toBe(true);
  231. expect(Matrix.TEMP_MATRIX instanceof Matrix).toBe(true);
  232. });
  233. it('should reset matrix to default when identity() is called', () => {
  234. const matrix = new Matrix();
  235. matrix.set(2, 3, 4, 5, 100, 200);
  236. expect(matrix.a).toEqual(2);
  237. expect(matrix.b).toEqual(3);
  238. expect(matrix.c).toEqual(4);
  239. expect(matrix.d).toEqual(5);
  240. expect(matrix.tx).toEqual(100);
  241. expect(matrix.ty).toEqual(200);
  242. matrix.identity();
  243. expect(matrix.a).toEqual(1);
  244. expect(matrix.b).toEqual(0);
  245. expect(matrix.c).toEqual(0);
  246. expect(matrix.d).toEqual(1);
  247. expect(matrix.tx).toEqual(0);
  248. expect(matrix.ty).toEqual(0);
  249. });
  250. it('should have the same transform after decompose', () => {
  251. const matrix = new Matrix();
  252. const transformInitial = new Transform();
  253. const transformDecomposed = new Transform();
  254. for (let x = 0; x < 50; ++x) {
  255. transformInitial.position.x = Math.random() * 1000 - 2000;
  256. transformInitial.position.y = Math.random() * 1000 - 2000;
  257. transformInitial.scale.x = Math.random() * 5 - 10;
  258. transformInitial.scale.y = Math.random() * 5 - 10;
  259. transformInitial.rotation = (Math.random() - 2) * Math.PI;
  260. transformInitial.skew.x = (Math.random() - 2) * Math.PI;
  261. transformInitial.skew.y = (Math.random() - 2) * Math.PI;
  262. matrix.setTransform(
  263. transformInitial.position.x,
  264. transformInitial.position.y,
  265. 0,
  266. 0,
  267. transformInitial.scale.x,
  268. transformInitial.scale.y,
  269. transformInitial.rotation,
  270. transformInitial.skew.x,
  271. transformInitial.skew.y,
  272. );
  273. matrix.decompose(transformDecomposed);
  274. transformInitial.updateLocalTransform();
  275. transformDecomposed.updateLocalTransform();
  276. expect(transformInitial.localTransform.a).toBeCloseTo(
  277. transformDecomposed.localTransform.a,
  278. 0.0001,
  279. );
  280. expect(transformInitial.localTransform.b).toBeCloseTo(
  281. transformDecomposed.localTransform.b,
  282. 0.0001,
  283. );
  284. expect(transformInitial.localTransform.c).toBeCloseTo(
  285. transformDecomposed.localTransform.c,
  286. 0.0001,
  287. );
  288. expect(transformInitial.localTransform.d).toBeCloseTo(
  289. transformDecomposed.localTransform.d,
  290. 0.0001,
  291. );
  292. expect(transformInitial.localTransform.tx).toBeCloseTo(
  293. transformDecomposed.localTransform.tx,
  294. 0.0001,
  295. );
  296. expect(transformInitial.localTransform.ty).toBeCloseTo(
  297. transformDecomposed.localTransform.ty,
  298. 0.0001,
  299. );
  300. }
  301. });
  302. it('should decompose corner case', () => {
  303. const matrix = new Matrix();
  304. const transform = new Transform();
  305. const result = transform.localTransform;
  306. matrix.a = -0.00001;
  307. matrix.b = -1;
  308. matrix.c = 1;
  309. matrix.d = 0;
  310. matrix.decompose(transform);
  311. transform.updateLocalTransform();
  312. expect(result.a).toBeCloseTo(matrix.a, 0.001);
  313. expect(result.b).toBeCloseTo(matrix.b, 0.001);
  314. expect(result.c).toBeCloseTo(matrix.c, 0.001);
  315. expect(result.d).toBeCloseTo(matrix.d, 0.001);
  316. });
  317. describe('decompose', () => {
  318. it('should be the inverse of updateLocalTransform even when pivot is set', () => {
  319. const matrix = new Matrix(0.01, 0.04, 0.04, 0.1, 2, 2);
  320. const transform = new Transform();
  321. transform.pivot.set(40, 40);
  322. matrix.decompose(transform);
  323. transform.updateLocalTransform();
  324. const { localTransform } = transform;
  325. expect(localTransform.a).toBeCloseTo(matrix.a, 0.001);
  326. expect(localTransform.b).toBeCloseTo(matrix.b, 0.001);
  327. expect(localTransform.c).toBeCloseTo(matrix.c, 0.001);
  328. expect(localTransform.d).toBeCloseTo(matrix.d, 0.001);
  329. // FIXME expect(localTransform.tx).toBeCloseTo(matrix.tx, 0.001)
  330. // FIXME expect(localTransform.ty).toBeCloseTo(matrix.ty, 0.001)
  331. });
  332. });
  333. });