packages/@interactjs/modifiers/aspectRatio.ts

  1. /**
  2. * @module modifiers/aspectRatio
  3. *
  4. * @description
  5. * This module forces elements to be resized with a specified dx/dy ratio.
  6. *
  7. * ```js
  8. * interact(target).resizable({
  9. * modifiers: [
  10. * interact.modifiers.snapSize({
  11. * targets: [ interact.snappers.grid({ x: 20, y: 20 }) ],
  12. * }),
  13. * interact.aspectRatio({ ratio: 'preserve' }),
  14. * ],
  15. * });
  16. * ```
  17. */
  18. import type { Point, Rect, EdgeOptions } from '@interactjs/types/index'
  19. import extend from '@interactjs/utils/extend'
  20. import { addEdges } from '@interactjs/utils/rect'
  21. import Modification from './Modification'
  22. import type { Modifier, ModifierModule, ModifierState } from './base'
  23. import { makeModifier } from './base'
  24. export interface AspectRatioOptions {
  25. ratio?: number | 'preserve'
  26. equalDelta?: boolean
  27. modifiers?: Modifier[]
  28. enabled?: boolean
  29. }
  30. export type AspectRatioState = ModifierState<
  31. AspectRatioOptions,
  32. {
  33. startCoords: Point
  34. startRect: Rect
  35. linkedEdges: EdgeOptions
  36. ratio: number
  37. equalDelta: boolean
  38. xIsPrimaryAxis: boolean
  39. edgeSign: 1 | -1
  40. subModification: Modification
  41. }
  42. >
  43. const aspectRatio: ModifierModule<AspectRatioOptions, AspectRatioState> = {
  44. start (arg) {
  45. const { state, rect, edges: originalEdges, pageCoords: coords } = arg
  46. let { ratio } = state.options
  47. const { equalDelta, modifiers } = state.options
  48. if (ratio === 'preserve') {
  49. ratio = rect.width / rect.height
  50. }
  51. state.startCoords = extend({}, coords)
  52. state.startRect = extend({}, rect)
  53. state.ratio = ratio
  54. state.equalDelta = equalDelta
  55. const linkedEdges = (state.linkedEdges = {
  56. top: originalEdges.top || (originalEdges.left && !originalEdges.bottom),
  57. left: originalEdges.left || (originalEdges.top && !originalEdges.right),
  58. bottom: originalEdges.bottom || (originalEdges.right && !originalEdges.top),
  59. right: originalEdges.right || (originalEdges.bottom && !originalEdges.left),
  60. })
  61. state.xIsPrimaryAxis = !!(originalEdges.left || originalEdges.right)
  62. if (state.equalDelta) {
  63. state.edgeSign = ((linkedEdges.left ? 1 : -1) * (linkedEdges.top ? 1 : -1)) as 1 | -1
  64. } else {
  65. const negativeSecondaryEdge = state.xIsPrimaryAxis ? linkedEdges.top : linkedEdges.left
  66. state.edgeSign = negativeSecondaryEdge ? -1 : 1
  67. }
  68. extend(arg.edges, linkedEdges)
  69. if (!modifiers || !modifiers.length) return
  70. const subModification = new Modification(arg.interaction)
  71. subModification.copyFrom(arg.interaction.modification)
  72. subModification.prepareStates(modifiers)
  73. state.subModification = subModification
  74. subModification.startAll({ ...arg })
  75. },
  76. set (arg) {
  77. const { state, rect, coords } = arg
  78. const initialCoords = extend({}, coords)
  79. const aspectMethod = state.equalDelta ? setEqualDelta : setRatio
  80. aspectMethod(state, state.xIsPrimaryAxis, coords, rect)
  81. if (!state.subModification) {
  82. return null
  83. }
  84. const correctedRect = extend({}, rect)
  85. addEdges(state.linkedEdges, correctedRect, {
  86. x: coords.x - initialCoords.x,
  87. y: coords.y - initialCoords.y,
  88. })
  89. const result = state.subModification.setAll({
  90. ...arg,
  91. rect: correctedRect,
  92. edges: state.linkedEdges,
  93. pageCoords: coords,
  94. prevCoords: coords,
  95. prevRect: correctedRect,
  96. })
  97. const { delta } = result
  98. if (result.changed) {
  99. const xIsCriticalAxis = Math.abs(delta.x) > Math.abs(delta.y)
  100. // do aspect modification again with critical edge axis as primary
  101. aspectMethod(state, xIsCriticalAxis, result.coords, result.rect)
  102. extend(coords, result.coords)
  103. }
  104. return result.eventProps
  105. },
  106. defaults: {
  107. ratio: 'preserve',
  108. equalDelta: false,
  109. modifiers: [],
  110. enabled: false,
  111. },
  112. }
  113. function setEqualDelta ({ startCoords, edgeSign }: AspectRatioState, xIsPrimaryAxis: boolean, coords: Point) {
  114. if (xIsPrimaryAxis) {
  115. coords.y = startCoords.y + (coords.x - startCoords.x) * edgeSign
  116. } else {
  117. coords.x = startCoords.x + (coords.y - startCoords.y) * edgeSign
  118. }
  119. }
  120. function setRatio (
  121. { startRect, startCoords, ratio, edgeSign }: AspectRatioState,
  122. xIsPrimaryAxis: boolean,
  123. coords: Point,
  124. rect: Rect,
  125. ) {
  126. if (xIsPrimaryAxis) {
  127. const newHeight = rect.width / ratio
  128. coords.y = startCoords.y + (newHeight - startRect.height) * edgeSign
  129. } else {
  130. const newWidth = rect.height * ratio
  131. coords.x = startCoords.x + (newWidth - startRect.width) * edgeSign
  132. }
  133. }
  134. export default makeModifier(aspectRatio, 'aspectRatio')
  135. export { aspectRatio }