<commit_before>import {dia} from "jointjs"
import {debounce} from "lodash"
import svgPanZoom from "svg-pan-zoom"
import {CursorMask} from "./CursorMask"
import {Events} from "./joint-events"

type EventData = { panStart?: { x: number, y: number, touched?: boolean } }
type Event = JQuery.MouseEventBase<any, EventData>

export class PanZoomPlugin {
  private cursorMask: CursorMask
  private instance: SvgPanZoom.Instance

  constructor(paper: dia.Paper) {
    this.cursorMask = new CursorMask()
    this.instance = svgPanZoom(paper.svg, {
      viewportSelector: ".svg-pan-zoom_viewport",
      fit: false,
      contain: false,
      zoomScaleSensitivity: 0.4,
      controlIconsEnabled: false,
      panEnabled: false,
      dblClickZoomEnabled: false,
      minZoom: 0.05,
      maxZoom: 5,
    })

    paper.on(Events.BLANK_POINTERDOWN, (event: Event, x, y) => {
      const isModified = event.shiftKey || event.ctrlKey || event.altKey || event.metaKey
      if (!isModified) {
        this.cursorMask.enable("move")
        event.data = {...event.data, panStart: {x, y}}
      }
    })

    paper.on(Events.BLANK_POINTERMOVE, (event: Event, eventX, eventY) => {
      const isModified = event.shiftKey || event.ctrlKey || event.altKey || event.metaKey
      const panStart = event.data?.panStart
      if (!isModified && panStart) {
        const zoom = this.instance.getZoom()
        const dx = (eventX - panStart.x) * zoom
        const dy = (eventY - panStart.y) * zoom
        const {movementX: x = dx, movementY: y = dy} = event.originalEvent
        this.instance.panBy({x, y})
        panStart.touched = true
      } else {
        this.cleanup(event)
      }
    })

    paper.on(Events.BLANK_POINTERUP, (event: Event) => {
      if (event.data?.panStart?.touched) {
        event.stopImmediatePropagation()
      }
      this.cleanup(event)
    })
  }

  get zoom(): number {
    return this.instance.getZoom()
  }

  private cleanup(event: Event) {
    delete event.data?.panStart
    this.cursorMask.disable()
  }

  fitSmallAndLargeGraphs = debounce((): void => {
    this.instance.updateBBox()
    this.instance.resize()
    this.instance.fit()
    const {realZoom} = this.instance.getSizes()
    const toZoomBy = realZoom > 1.2 ? 1 / realZoom : 0.8 //the bigger zoom, the further we get
    this.instance.zoomBy(toZoomBy)
    this.instance.center()
  }, 200)

  zoomIn = () => {
    this.instance.zoomIn()
  }

  zoomOut = () => {
    this.instance.zoomOut()
  }
}
<commit_msg>feat: add zoom & pan animation (#2087)<commit_after>import {css} from "emotion"
import {dia} from "jointjs"
import {debounce} from "lodash"
import svgPanZoom from "svg-pan-zoom"
import {CursorMask} from "./CursorMask"
import {Events} from "./joint-events"

type EventData = {panStart?: {x: number, y: number, touched?: boolean}}
type Event = JQuery.MouseEventBase<any, EventData>

const getAnimationClass = (disabled?: boolean) => css({
  ".svg-pan-zoom_viewport": {
    transition: disabled ? "none" : `transform .5s cubic-bezier(0.86, 0, 0.07, 1)`,
  },
})

export class PanZoomPlugin {
  private cursorMask: CursorMask
  private instance: SvgPanZoom.Instance
  private animationClassHolder: HTMLElement

  constructor(paper: dia.Paper) {
    this.cursorMask = new CursorMask()
    this.instance = svgPanZoom(paper.svg, {
      fit: false,
      contain: false,
      zoomScaleSensitivity: 0.4,
      controlIconsEnabled: false,
      panEnabled: false,
      dblClickZoomEnabled: false,
      minZoom: 0.005,
      maxZoom: 500,
    })

    this.animationClassHolder = document.querySelector(".svg-pan-zoom_viewport").parentElement
    this.animationClassHolder.addEventListener("transitionend", () => {
      this.setAnimationClass({enabled: true})
    })

    paper.on(Events.BLANK_POINTERDOWN, (event: Event, x, y) => {
      const isModified = event.shiftKey || event.ctrlKey || event.altKey || event.metaKey
      if (!isModified) {
        this.cursorMask.enable("move")
        event.data = {...event.data, panStart: {x, y}}
      }
    })

    paper.on(Events.BLANK_POINTERMOVE, (event: Event, eventX, eventY) => {
      const isModified = event.shiftKey || event.ctrlKey || event.altKey || event.metaKey
      const panStart = event.data?.panStart
      if (!isModified && panStart) {
        const zoom = this.instance.getZoom()
        const dx = (eventX - panStart.x) * zoom
        const dy = (eventY - panStart.y) * zoom
        const {movementX: x = dx, movementY: y = dy} = event.originalEvent
        this.instance.panBy({x, y})
        panStart.touched = true
      } else {
        this.cleanup(event)
      }
    })

    paper.on(Events.BLANK_POINTERUP, (event: Event) => {
      if (event.data?.panStart?.touched) {
        event.stopImmediatePropagation()
      }
      this.cleanup(event)
    })
  }

  private setAnimationClass({enabled}: {enabled: boolean}) {
    this.animationClassHolder.classList.add(getAnimationClass(enabled))
    this.animationClassHolder.classList.remove(getAnimationClass(!enabled))
  }

  get zoom(): number {
    return this.instance.getZoom()
  }

  private cleanup(event: Event) {
    delete event.data?.panStart
    this.cursorMask.disable()
  }

  fitSmallAndLargeGraphs = debounce((): void => {
    this.setAnimationClass({enabled: false})
    this.instance.zoom(1)
    this.instance.resize()
    this.instance.updateBBox()
    this.instance.fit()
    const {realZoom} = this.instance.getSizes()
    const toZoomBy = realZoom > 1.2 ? 1 / realZoom : 0.8 //the bigger zoom, the further we get
    this.instance.zoomBy(toZoomBy)
    this.instance.center()
  }, 200)

  zoomIn = () => {
    this.instance.zoomIn()
  }

  zoomOut = () => {
    this.instance.zoomOut()
  }
}

{'fraction_non_alphanumeric': 0.1360223502706478, 'fraction_numerical': 0.00785751702462022, 'mean_word_length': 2.972260748959778, 'pattern_counts': {'":': 1, '<': 5, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3943971', 'n_tokens_mistral': 1892, 'n_tokens_neox': 1836, 'n_words': 395}