You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Box.js 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import { delimiter } from '../modules/core/regex.js'
  2. import { globals } from '../utils/window.js'
  3. import { register } from '../utils/adopter.js'
  4. import { registerMethods } from '../utils/methods.js'
  5. import Matrix from './Matrix.js'
  6. import Point from './Point.js'
  7. import parser from '../modules/core/parser.js'
  8. export function isNulledBox (box) {
  9. return !box.width && !box.height && !box.x && !box.y
  10. }
  11. export function domContains (node) {
  12. return node === globals.document
  13. || (globals.document.documentElement.contains || function (node) {
  14. // This is IE - it does not support contains() for top-level SVGs
  15. while (node.parentNode) {
  16. node = node.parentNode
  17. }
  18. return node === globals.document
  19. }).call(globals.document.documentElement, node)
  20. }
  21. export default class Box {
  22. constructor (...args) {
  23. this.init(...args)
  24. }
  25. addOffset () {
  26. // offset by window scroll position, because getBoundingClientRect changes when window is scrolled
  27. this.x += globals.window.pageXOffset
  28. this.y += globals.window.pageYOffset
  29. return new Box(this)
  30. }
  31. init (source) {
  32. const base = [ 0, 0, 0, 0 ]
  33. source = typeof source === 'string'
  34. ? source.split(delimiter).map(parseFloat)
  35. : Array.isArray(source)
  36. ? source
  37. : typeof source === 'object'
  38. ? [ source.left != null
  39. ? source.left
  40. : source.x, source.top != null ? source.top : source.y, source.width, source.height ]
  41. : arguments.length === 4
  42. ? [].slice.call(arguments)
  43. : base
  44. this.x = source[0] || 0
  45. this.y = source[1] || 0
  46. this.width = this.w = source[2] || 0
  47. this.height = this.h = source[3] || 0
  48. // Add more bounding box properties
  49. this.x2 = this.x + this.w
  50. this.y2 = this.y + this.h
  51. this.cx = this.x + this.w / 2
  52. this.cy = this.y + this.h / 2
  53. return this
  54. }
  55. isNulled () {
  56. return isNulledBox(this)
  57. }
  58. // Merge rect box with another, return a new instance
  59. merge (box) {
  60. const x = Math.min(this.x, box.x)
  61. const y = Math.min(this.y, box.y)
  62. const width = Math.max(this.x + this.width, box.x + box.width) - x
  63. const height = Math.max(this.y + this.height, box.y + box.height) - y
  64. return new Box(x, y, width, height)
  65. }
  66. toArray () {
  67. return [ this.x, this.y, this.width, this.height ]
  68. }
  69. toString () {
  70. return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height
  71. }
  72. transform (m) {
  73. if (!(m instanceof Matrix)) {
  74. m = new Matrix(m)
  75. }
  76. let xMin = Infinity
  77. let xMax = -Infinity
  78. let yMin = Infinity
  79. let yMax = -Infinity
  80. const pts = [
  81. new Point(this.x, this.y),
  82. new Point(this.x2, this.y),
  83. new Point(this.x, this.y2),
  84. new Point(this.x2, this.y2)
  85. ]
  86. pts.forEach(function (p) {
  87. p = p.transform(m)
  88. xMin = Math.min(xMin, p.x)
  89. xMax = Math.max(xMax, p.x)
  90. yMin = Math.min(yMin, p.y)
  91. yMax = Math.max(yMax, p.y)
  92. })
  93. return new Box(
  94. xMin, yMin,
  95. xMax - xMin,
  96. yMax - yMin
  97. )
  98. }
  99. }
  100. function getBox (el, getBBoxFn, retry) {
  101. let box
  102. try {
  103. // Try to get the box with the provided function
  104. box = getBBoxFn(el.node)
  105. // If the box is worthless and not even in the dom, retry
  106. // by throwing an error here...
  107. if (isNulledBox(box) && !domContains(el.node)) {
  108. throw new Error('Element not in the dom')
  109. }
  110. } catch (e) {
  111. // ... and calling the retry handler here
  112. box = retry(el)
  113. }
  114. return box
  115. }
  116. export function bbox () {
  117. // Function to get bbox is getBBox()
  118. const getBBox = (node) => node.getBBox()
  119. // Take all measures so that a stupid browser renders the element
  120. // so we can get the bbox from it when we try again
  121. const retry = (el) => {
  122. try {
  123. const clone = el.clone().addTo(parser().svg).show()
  124. const box = clone.node.getBBox()
  125. clone.remove()
  126. return box
  127. } catch (e) {
  128. // We give up...
  129. throw new Error(`Getting bbox of element "${el.node.nodeName}" is not possible: ${e.toString()}`)
  130. }
  131. }
  132. const box = getBox(this, getBBox, retry)
  133. const bbox = new Box(box)
  134. return bbox
  135. }
  136. export function rbox (el) {
  137. const getRBox = (node) => node.getBoundingClientRect()
  138. const retry = (el) => {
  139. // There is no point in trying tricks here because if we insert the element into the dom ourselves
  140. // it obviously will be at the wrong position
  141. throw new Error(`Getting rbox of element "${el.node.nodeName}" is not possible`)
  142. }
  143. const box = getBox(this, getRBox, retry)
  144. const rbox = new Box(box)
  145. // If an element was passed, we want the bbox in the coordinate system of that element
  146. if (el) {
  147. return rbox.transform(el.screenCTM().inverseO())
  148. }
  149. // Else we want it in absolute screen coordinates
  150. // Therefore we need to add the scrollOffset
  151. return rbox.addOffset()
  152. }
  153. // Checks whether the given point is inside the bounding box
  154. export function inside (x, y) {
  155. const box = this.bbox()
  156. return x > box.x
  157. && y > box.y
  158. && x < box.x + box.width
  159. && y < box.y + box.height
  160. }
  161. registerMethods({
  162. viewbox: {
  163. viewbox (x, y, width, height) {
  164. // act as getter
  165. if (x == null) return new Box(this.attr('viewBox'))
  166. // act as setter
  167. return this.attr('viewBox', new Box(x, y, width, height))
  168. },
  169. zoom (level, point) {
  170. // Its best to rely on the attributes here and here is why:
  171. // clientXYZ: Doesn't work on non-root svgs because they dont have a CSSBox (silly!)
  172. // getBoundingClientRect: Doesn't work because Chrome just ignores width and height of nested svgs completely
  173. // that means, their clientRect is always as big as the content.
  174. // Furthermore this size is incorrect if the element is further transformed by its parents
  175. // computedStyle: Only returns meaningful values if css was used with px. We dont go this route here!
  176. // getBBox: returns the bounding box of its content - that doesn't help!
  177. let { width, height } = this.attr([ 'width', 'height' ])
  178. // Width and height is a string when a number with a unit is present which we can't use
  179. // So we try clientXYZ
  180. if ((!width && !height) || (typeof width === 'string' || typeof height === 'string')) {
  181. width = this.node.clientWidth
  182. height = this.node.clientHeight
  183. }
  184. // Giving up...
  185. if (!width || !height) {
  186. throw new Error('Impossible to get absolute width and height. Please provide an absolute width and height attribute on the zooming element')
  187. }
  188. const v = this.viewbox()
  189. const zoomX = width / v.width
  190. const zoomY = height / v.height
  191. const zoom = Math.min(zoomX, zoomY)
  192. if (level == null) {
  193. return zoom
  194. }
  195. let zoomAmount = zoom / level
  196. // Set the zoomAmount to the highest value which is safe to process and recover from
  197. // The * 100 is a bit of wiggle room for the matrix transformation
  198. if (zoomAmount === Infinity) zoomAmount = Number.MAX_SAFE_INTEGER / 100
  199. point = point || new Point(width / 2 / zoomX + v.x, height / 2 / zoomY + v.y)
  200. const box = new Box(v).transform(
  201. new Matrix({ scale: zoomAmount, origin: point })
  202. )
  203. return this.viewbox(box)
  204. }
  205. }
  206. })
  207. register(Box, 'Box')