Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import { delimiter } from '../modules/core/regex.js'
  2. import { radians } from '../utils/utils.js'
  3. import { register } from '../utils/adopter.js'
  4. import Element from '../elements/Element.js'
  5. import Point from './Point.js'
  6. function closeEnough (a, b, threshold) {
  7. return Math.abs(b - a) < (threshold || 1e-6)
  8. }
  9. export default class Matrix {
  10. constructor (...args) {
  11. this.init(...args)
  12. }
  13. static formatTransforms (o) {
  14. // Get all of the parameters required to form the matrix
  15. const flipBoth = o.flip === 'both' || o.flip === true
  16. const flipX = o.flip && (flipBoth || o.flip === 'x') ? -1 : 1
  17. const flipY = o.flip && (flipBoth || o.flip === 'y') ? -1 : 1
  18. const skewX = o.skew && o.skew.length
  19. ? o.skew[0]
  20. : isFinite(o.skew)
  21. ? o.skew
  22. : isFinite(o.skewX)
  23. ? o.skewX
  24. : 0
  25. const skewY = o.skew && o.skew.length
  26. ? o.skew[1]
  27. : isFinite(o.skew)
  28. ? o.skew
  29. : isFinite(o.skewY)
  30. ? o.skewY
  31. : 0
  32. const scaleX = o.scale && o.scale.length
  33. ? o.scale[0] * flipX
  34. : isFinite(o.scale)
  35. ? o.scale * flipX
  36. : isFinite(o.scaleX)
  37. ? o.scaleX * flipX
  38. : flipX
  39. const scaleY = o.scale && o.scale.length
  40. ? o.scale[1] * flipY
  41. : isFinite(o.scale)
  42. ? o.scale * flipY
  43. : isFinite(o.scaleY)
  44. ? o.scaleY * flipY
  45. : flipY
  46. const shear = o.shear || 0
  47. const theta = o.rotate || o.theta || 0
  48. const origin = new Point(o.origin || o.around || o.ox || o.originX, o.oy || o.originY)
  49. const ox = origin.x
  50. const oy = origin.y
  51. // We need Point to be invalid if nothing was passed because we cannot default to 0 here. That is why NaN
  52. const position = new Point(o.position || o.px || o.positionX || NaN, o.py || o.positionY || NaN)
  53. const px = position.x
  54. const py = position.y
  55. const translate = new Point(o.translate || o.tx || o.translateX, o.ty || o.translateY)
  56. const tx = translate.x
  57. const ty = translate.y
  58. const relative = new Point(o.relative || o.rx || o.relativeX, o.ry || o.relativeY)
  59. const rx = relative.x
  60. const ry = relative.y
  61. // Populate all of the values
  62. return {
  63. scaleX, scaleY, skewX, skewY, shear, theta, rx, ry, tx, ty, ox, oy, px, py
  64. }
  65. }
  66. static fromArray (a) {
  67. return { a: a[0], b: a[1], c: a[2], d: a[3], e: a[4], f: a[5] }
  68. }
  69. static isMatrixLike (o) {
  70. return (
  71. o.a != null
  72. || o.b != null
  73. || o.c != null
  74. || o.d != null
  75. || o.e != null
  76. || o.f != null
  77. )
  78. }
  79. // left matrix, right matrix, target matrix which is overwritten
  80. static matrixMultiply (l, r, o) {
  81. // Work out the product directly
  82. const a = l.a * r.a + l.c * r.b
  83. const b = l.b * r.a + l.d * r.b
  84. const c = l.a * r.c + l.c * r.d
  85. const d = l.b * r.c + l.d * r.d
  86. const e = l.e + l.a * r.e + l.c * r.f
  87. const f = l.f + l.b * r.e + l.d * r.f
  88. // make sure to use local variables because l/r and o could be the same
  89. o.a = a
  90. o.b = b
  91. o.c = c
  92. o.d = d
  93. o.e = e
  94. o.f = f
  95. return o
  96. }
  97. around (cx, cy, matrix) {
  98. return this.clone().aroundO(cx, cy, matrix)
  99. }
  100. // Transform around a center point
  101. aroundO (cx, cy, matrix) {
  102. const dx = cx || 0
  103. const dy = cy || 0
  104. return this.translateO(-dx, -dy).lmultiplyO(matrix).translateO(dx, dy)
  105. }
  106. // Clones this matrix
  107. clone () {
  108. return new Matrix(this)
  109. }
  110. // Decomposes this matrix into its affine parameters
  111. decompose (cx = 0, cy = 0) {
  112. // Get the parameters from the matrix
  113. const a = this.a
  114. const b = this.b
  115. const c = this.c
  116. const d = this.d
  117. const e = this.e
  118. const f = this.f
  119. // Figure out if the winding direction is clockwise or counterclockwise
  120. const determinant = a * d - b * c
  121. const ccw = determinant > 0 ? 1 : -1
  122. // Since we only shear in x, we can use the x basis to get the x scale
  123. // and the rotation of the resulting matrix
  124. const sx = ccw * Math.sqrt(a * a + b * b)
  125. const thetaRad = Math.atan2(ccw * b, ccw * a)
  126. const theta = 180 / Math.PI * thetaRad
  127. const ct = Math.cos(thetaRad)
  128. const st = Math.sin(thetaRad)
  129. // We can then solve the y basis vector simultaneously to get the other
  130. // two affine parameters directly from these parameters
  131. const lam = (a * c + b * d) / determinant
  132. const sy = ((c * sx) / (lam * a - b)) || ((d * sx) / (lam * b + a))
  133. // Use the translations
  134. const tx = e - cx + cx * ct * sx + cy * (lam * ct * sx - st * sy)
  135. const ty = f - cy + cx * st * sx + cy * (lam * st * sx + ct * sy)
  136. // Construct the decomposition and return it
  137. return {
  138. // Return the affine parameters
  139. scaleX: sx,
  140. scaleY: sy,
  141. shear: lam,
  142. rotate: theta,
  143. translateX: tx,
  144. translateY: ty,
  145. originX: cx,
  146. originY: cy,
  147. // Return the matrix parameters
  148. a: this.a,
  149. b: this.b,
  150. c: this.c,
  151. d: this.d,
  152. e: this.e,
  153. f: this.f
  154. }
  155. }
  156. // Check if two matrices are equal
  157. equals (other) {
  158. if (other === this) return true
  159. const comp = new Matrix(other)
  160. return closeEnough(this.a, comp.a) && closeEnough(this.b, comp.b)
  161. && closeEnough(this.c, comp.c) && closeEnough(this.d, comp.d)
  162. && closeEnough(this.e, comp.e) && closeEnough(this.f, comp.f)
  163. }
  164. // Flip matrix on x or y, at a given offset
  165. flip (axis, around) {
  166. return this.clone().flipO(axis, around)
  167. }
  168. flipO (axis, around) {
  169. return axis === 'x'
  170. ? this.scaleO(-1, 1, around, 0)
  171. : axis === 'y'
  172. ? this.scaleO(1, -1, 0, around)
  173. : this.scaleO(-1, -1, axis, around || axis) // Define an x, y flip point
  174. }
  175. // Initialize
  176. init (source) {
  177. const base = Matrix.fromArray([ 1, 0, 0, 1, 0, 0 ])
  178. // ensure source as object
  179. source = source instanceof Element
  180. ? source.matrixify()
  181. : typeof source === 'string'
  182. ? Matrix.fromArray(source.split(delimiter).map(parseFloat))
  183. : Array.isArray(source)
  184. ? Matrix.fromArray(source)
  185. : (typeof source === 'object' && Matrix.isMatrixLike(source))
  186. ? source
  187. : (typeof source === 'object')
  188. ? new Matrix().transform(source)
  189. : arguments.length === 6
  190. ? Matrix.fromArray([].slice.call(arguments))
  191. : base
  192. // Merge the source matrix with the base matrix
  193. this.a = source.a != null ? source.a : base.a
  194. this.b = source.b != null ? source.b : base.b
  195. this.c = source.c != null ? source.c : base.c
  196. this.d = source.d != null ? source.d : base.d
  197. this.e = source.e != null ? source.e : base.e
  198. this.f = source.f != null ? source.f : base.f
  199. return this
  200. }
  201. inverse () {
  202. return this.clone().inverseO()
  203. }
  204. // Inverses matrix
  205. inverseO () {
  206. // Get the current parameters out of the matrix
  207. const a = this.a
  208. const b = this.b
  209. const c = this.c
  210. const d = this.d
  211. const e = this.e
  212. const f = this.f
  213. // Invert the 2x2 matrix in the top left
  214. const det = a * d - b * c
  215. if (!det) throw new Error('Cannot invert ' + this)
  216. // Calculate the top 2x2 matrix
  217. const na = d / det
  218. const nb = -b / det
  219. const nc = -c / det
  220. const nd = a / det
  221. // Apply the inverted matrix to the top right
  222. const ne = -(na * e + nc * f)
  223. const nf = -(nb * e + nd * f)
  224. // Construct the inverted matrix
  225. this.a = na
  226. this.b = nb
  227. this.c = nc
  228. this.d = nd
  229. this.e = ne
  230. this.f = nf
  231. return this
  232. }
  233. lmultiply (matrix) {
  234. return this.clone().lmultiplyO(matrix)
  235. }
  236. lmultiplyO (matrix) {
  237. const r = this
  238. const l = matrix instanceof Matrix
  239. ? matrix
  240. : new Matrix(matrix)
  241. return Matrix.matrixMultiply(l, r, this)
  242. }
  243. // Left multiplies by the given matrix
  244. multiply (matrix) {
  245. return this.clone().multiplyO(matrix)
  246. }
  247. multiplyO (matrix) {
  248. // Get the matrices
  249. const l = this
  250. const r = matrix instanceof Matrix
  251. ? matrix
  252. : new Matrix(matrix)
  253. return Matrix.matrixMultiply(l, r, this)
  254. }
  255. // Rotate matrix
  256. rotate (r, cx, cy) {
  257. return this.clone().rotateO(r, cx, cy)
  258. }
  259. rotateO (r, cx = 0, cy = 0) {
  260. // Convert degrees to radians
  261. r = radians(r)
  262. const cos = Math.cos(r)
  263. const sin = Math.sin(r)
  264. const { a, b, c, d, e, f } = this
  265. this.a = a * cos - b * sin
  266. this.b = b * cos + a * sin
  267. this.c = c * cos - d * sin
  268. this.d = d * cos + c * sin
  269. this.e = e * cos - f * sin + cy * sin - cx * cos + cx
  270. this.f = f * cos + e * sin - cx * sin - cy * cos + cy
  271. return this
  272. }
  273. // Scale matrix
  274. scale (x, y, cx, cy) {
  275. return this.clone().scaleO(...arguments)
  276. }
  277. scaleO (x, y = x, cx = 0, cy = 0) {
  278. // Support uniform scaling
  279. if (arguments.length === 3) {
  280. cy = cx
  281. cx = y
  282. y = x
  283. }
  284. const { a, b, c, d, e, f } = this
  285. this.a = a * x
  286. this.b = b * y
  287. this.c = c * x
  288. this.d = d * y
  289. this.e = e * x - cx * x + cx
  290. this.f = f * y - cy * y + cy
  291. return this
  292. }
  293. // Shear matrix
  294. shear (a, cx, cy) {
  295. return this.clone().shearO(a, cx, cy)
  296. }
  297. shearO (lx, cx = 0, cy = 0) {
  298. const { a, b, c, d, e, f } = this
  299. this.a = a + b * lx
  300. this.c = c + d * lx
  301. this.e = e + f * lx - cy * lx
  302. return this
  303. }
  304. // Skew Matrix
  305. skew (x, y, cx, cy) {
  306. return this.clone().skewO(...arguments)
  307. }
  308. skewO (x, y = x, cx = 0, cy = 0) {
  309. // support uniformal skew
  310. if (arguments.length === 3) {
  311. cy = cx
  312. cx = y
  313. y = x
  314. }
  315. // Convert degrees to radians
  316. x = radians(x)
  317. y = radians(y)
  318. const lx = Math.tan(x)
  319. const ly = Math.tan(y)
  320. const { a, b, c, d, e, f } = this
  321. this.a = a + b * lx
  322. this.b = b + a * ly
  323. this.c = c + d * lx
  324. this.d = d + c * ly
  325. this.e = e + f * lx - cy * lx
  326. this.f = f + e * ly - cx * ly
  327. return this
  328. }
  329. // SkewX
  330. skewX (x, cx, cy) {
  331. return this.skew(x, 0, cx, cy)
  332. }
  333. // SkewY
  334. skewY (y, cx, cy) {
  335. return this.skew(0, y, cx, cy)
  336. }
  337. toArray () {
  338. return [ this.a, this.b, this.c, this.d, this.e, this.f ]
  339. }
  340. // Convert matrix to string
  341. toString () {
  342. return 'matrix(' + this.a + ',' + this.b + ',' + this.c + ',' + this.d + ',' + this.e + ',' + this.f + ')'
  343. }
  344. // Transform a matrix into another matrix by manipulating the space
  345. transform (o) {
  346. // Check if o is a matrix and then left multiply it directly
  347. if (Matrix.isMatrixLike(o)) {
  348. const matrix = new Matrix(o)
  349. return matrix.multiplyO(this)
  350. }
  351. // Get the proposed transformations and the current transformations
  352. const t = Matrix.formatTransforms(o)
  353. const current = this
  354. const { x: ox, y: oy } = new Point(t.ox, t.oy).transform(current)
  355. // Construct the resulting matrix
  356. const transformer = new Matrix()
  357. .translateO(t.rx, t.ry)
  358. .lmultiplyO(current)
  359. .translateO(-ox, -oy)
  360. .scaleO(t.scaleX, t.scaleY)
  361. .skewO(t.skewX, t.skewY)
  362. .shearO(t.shear)
  363. .rotateO(t.theta)
  364. .translateO(ox, oy)
  365. // If we want the origin at a particular place, we force it there
  366. if (isFinite(t.px) || isFinite(t.py)) {
  367. const origin = new Point(ox, oy).transform(transformer)
  368. // TODO: Replace t.px with isFinite(t.px)
  369. // Doesn't work because t.px is also 0 if it wasn't passed
  370. const dx = isFinite(t.px) ? t.px - origin.x : 0
  371. const dy = isFinite(t.py) ? t.py - origin.y : 0
  372. transformer.translateO(dx, dy)
  373. }
  374. // Translate now after positioning
  375. transformer.translateO(t.tx, t.ty)
  376. return transformer
  377. }
  378. // Translate matrix
  379. translate (x, y) {
  380. return this.clone().translateO(x, y)
  381. }
  382. translateO (x, y) {
  383. this.e += x || 0
  384. this.f += y || 0
  385. return this
  386. }
  387. valueOf () {
  388. return {
  389. a: this.a,
  390. b: this.b,
  391. c: this.c,
  392. d: this.d,
  393. e: this.e,
  394. f: this.f
  395. }
  396. }
  397. }
  398. export function ctm () {
  399. return new Matrix(this.node.getCTM())
  400. }
  401. export function screenCTM () {
  402. /* https://bugzilla.mozilla.org/show_bug.cgi?id=1344537
  403. This is needed because FF does not return the transformation matrix
  404. for the inner coordinate system when getScreenCTM() is called on nested svgs.
  405. However all other Browsers do that */
  406. if (typeof this.isRoot === 'function' && !this.isRoot()) {
  407. const rect = this.rect(1, 1)
  408. const m = rect.node.getScreenCTM()
  409. rect.remove()
  410. return new Matrix(m)
  411. }
  412. return new Matrix(this.node.getScreenCTM())
  413. }
  414. register(Matrix, 'Matrix')