Skip to content
Snippets Groups Projects
Select Git revision
  • 447fb7a6bef8dd3125eb34b158fe3a2341070812
  • master default protected
  • updates
  • hierarchical-keywords
  • map
  • topoi
  • layer
  • tsne-layout
8 results

canvas.js

Blame
  • user avatar
    Christopher Pietsch authored
    447fb7a6
    History
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    canvas.js 25.28 KiB
    // christopher pietsch
    // cpietsch@gmail.com
    // 2015-2018
    
    function Canvas() {
      var margin = {
        top: 20,
        right: 50,
        bottom: 30,
        left: 50,
      }
    
      var minHeight = 400
      var width = window.innerWidth - margin.left - margin.right
      var widthOuter = window.innerWidth
      var height = window.innerHeight < minHeight ? minHeight : window.innerHeight
    
      var scale
      var scale1 = 1
      var scale2 = 1
      var scale3 = 1
      var allData = []
    
      var translate = [0, 0]
      var scale = 1
      var timeDomain = []
      var loadImagesCue = []
    
      var x = d3.scale.ordinal().rangeBands([margin.left, width + margin.left], 0.2)
    
      var Quadtree = d3.geom
        .quadtree()
        .x(function (d) {
          return d.x
        })
        .y(function (d) {
          return d.y
        })
    
      var quadtree
    
      var maxZoomLevel = utils.isMobile() ? 5000 : 2500
    
      var zoom = d3.behavior
        .zoom()
        .scaleExtent([1, maxZoomLevel])
        .size([width, height])
        .on('zoom', zoomed)
        .on('zoomend', zoomend)
        .on('zoomstart', zoomstart)
    
      var canvas
      var config
      var container
      var entries
      var years
      var data
      var rangeBand = 0
      var rangeBandImage = 0
      var imageSize = 256
      var imageSize2 = 1024
      var imageSize3 = 4000
      var collumns = 4
      var renderer, stage
    
      var svgscale, voronoi
    
      var selectedImageDistance = 0
      var selectedImage = null
    
      var drag = false
      var sleep = false
    
      var stagePadding = 40
      var imgPadding
    
      var bottomPadding = 70
      var extent = [0, 0]
      var bottomZooming = true
    
      var touchstart = 0
      var vizContainer
      var spriteClick = false
    
      var state = {
        lastZoomed: 0,
        zoomingToImage: false,
        mode: 'time',
        init: false,
      }
    
      var zoomedToImage = false
      var zoomedToImageScale = 117
      var zoomBarrier = 2
    
      var startTranslate = [0, 0]
      var startScale = 0
      var cursorCutoff = 1
      var zooming = false
      var detailContainer = d3.select('.sidebar')
      var timelineData
      var stage, stage1, stage2, stage3, stage4, stage5
      var timelineHover = false
      var tsne = []
      var tsneIndex = {}
      var mapIndex = {}
    
      function canvas() {}
    
      canvas.rangeBand = function () {
        return rangeBand
      }
      canvas.width = function () {
        return width
      }
      canvas.height = function () {
        return height
      }
      canvas.imgPadding = function () {
        return imgPadding
      }
      canvas.rangeBandImage = function () {
        return rangeBandImage
      }
      canvas.zoom = zoom
      canvas.selectedImage = function () {
        return selectedImage
      }
      canvas.x = x
    
      canvas.resize = function () {
        if (!state.init) return
        width = window.innerWidth - margin.left - margin.right
        height = window.innerHeight < minHeight ? minHeight : window.innerHeight
        widthOuter = window.innerWidth
        renderer.resize(width + margin.left + margin.right, height)
        canvas.makeScales()
        if (state.mode === 'map') {
          map.project()
          canvas.project()
          canvas.resetZoom()
        } else {
          canvas.project()
        }
      }
    
      canvas.makeScales = function () {
        x.rangeBands([margin.left, width + margin.left], 0.2)
    
        rangeBand = x.rangeBand()
        rangeBandImage = x.rangeBand() / collumns
    
        imgPadding = rangeBand / collumns / 2
    
        scale1 = imageSize / (x.rangeBand() / collumns)
        scale2 = imageSize2 / (x.rangeBand() / collumns)
        scale3 = imageSize3 / (x.rangeBand() / collumns)
    
        stage3.scale.x = 1 / scale1
        stage3.scale.y = 1 / scale1
        stage3.y = height
    
        stage4.scale.x = 1 / scale2
        stage4.scale.y = 1 / scale2
        stage4.y = height
    
        stage5.scale.x = 1 / scale3
        stage5.scale.y = 1 / scale3
        stage5.y = height
    
        timeline.rescale(scale1)
    
        cursorCutoff = (1 / scale1) * imageSize * 0.48
        zoomedToImageScale = 0.8 / (x.rangeBand() / collumns / width)
        // console.log("zoomedToImageScale", zoomedToImageScale)
      }
    
      canvas.init = function (_data, _timeline, _config) {
        data = _data
        config = _config
    
        container = d3.select('.page').append('div').classed('viz', true)
        detailVue._data.structure = config.detail.structure
    
        collumns = config.projection.columns
        imageSize = config.loader.textures.medium.size
        imageSize2 = config.loader.textures.detail.size
    
        if (config.loader.textures.big) {
          imageSize3 = config.loader.textures.big.size
        }
    
        PIXI.settings.SCALE_MODE = 1
        PIXI.settings.SPRITE_MAX_TEXTURES = Math.min(
          PIXI.settings.SPRITE_MAX_TEXTURES,
          16,
        )
    
        var renderOptions = {
          resolution: 1,
          antialiasing: false,
          width: width + margin.left + margin.right,
          height: height,
          transparent: true,
        }
        renderer = new PIXI.Renderer(renderOptions)
        // renderer.backgroundColor = parseInt(
        //   config.style.canvasBackground.substring(1),
        //   16
        // );
        window.renderer = renderer
    
        var renderElem = d3.select(container.node().appendChild(renderer.view))
    
        stage = new PIXI.Container()
        stage2 = new PIXI.Container()
        stage3 = new PIXI.Container()
        stage4 = new PIXI.Container()
        stage5 = new PIXI.Container()
    
        stage.addChild(stage2)
        stage2.addChild(stage3)
        stage2.addChild(stage4)
        stage2.addChild(stage5)
    
        _timeline.forEach(function (d) {
          d.type = 'timeline'
        })
    
        var canvasDomain = d3
          .nest()
          .key(function (d) {
            return d.year
          })
          .entries(_data.concat(_timeline))
          .sort(function (a, b) {
            return a.key - b.key
          })
          .map(function (d) {
            return d.key
          })
    
        timeDomain = canvasDomain.map(function (d) {
          return {
            key: d,
            values: _timeline.filter(function (e) {
              return d == e.year
            }),
          }
        })
    
        timeline.init(timeDomain)
        x.domain(canvasDomain)
        canvas.makeScales()
    
        // add preview pics
        data.forEach(function (d, i) {
          var sprite = new PIXI.Sprite(PIXI.Texture.WHITE)
    
          sprite.anchor.x = 0.5
          sprite.anchor.y = 0.5
    
          sprite.scale.x = d.scaleFactor
          sprite.scale.y = d.scaleFactor
    
          sprite._data = d
          d.sprite = sprite
    
          stage3.addChild(sprite)
        })
    
        vizContainer = d3
          .select('.viz')
          .call(zoom)
          .on('mousemove', mousemove)
          .on('dblclick.zoom', null)
          .on('touchstart', function (d) {
            mousemove(d)
            touchstart = new Date() * 1
          })
          .on('touchend', function (d) {
            var touchtime = new Date() * 1 - touchstart
            if (touchtime > 250) return
            if (selectedImageDistance > 15) return
            if (selectedImage && !selectedImage.id) return
            if (selectedImage && !selectedImage.active) return
            if (drag) return
    
            zoomToImage(selectedImage, 1400 / Math.sqrt(Math.sqrt(scale)))
          })
          .on('click', function () {
            console.log('click')
            if (spriteClick) {
              spriteClick = false
              return
            }
    
            if (selectedImage && !selectedImage.id) return
            if (drag) return
            if (selectedImageDistance > cursorCutoff) return
            if (selectedImage && !selectedImage.active) return
            if (timelineHover) return
            // console.log(selectedImage)
    
            if (Math.abs(zoomedToImageScale - scale) < 0.1) {
              canvas.resetZoom()
            } else {
              zoomToImage(selectedImage, 1400 / Math.sqrt(Math.sqrt(scale)))
            }
          })
    
        canvas.project()
        animate()
    
        // selectedImage = data.find(d => d.id == 88413)
        // showDetail(selectedImage)
        state.init = true
      }
    
      canvas.addTsneData = function (d) {
        console.time('tsne')
        var clean = d.map(function (d) {
          return {
            id: d.id,
            x: parseFloat(d.x),
            y: parseFloat(d.y),
          }
        })
        var xExtent = d3.extent(clean, function (d) {
          return d.x
        })
        var yExtent = d3.extent(clean, function (d) {
          return d.y
        })
    
        var x = d3.scale.linear().range([0, 1]).domain(xExtent)
        var y = d3.scale.linear().range([0, 1]).domain(yExtent)
    
        d.forEach(function (d) {
          tsneIndex[d.id] = [x(d.x), y(d.y)]
        })
    
        console.timeEnd('tsne')
      }
    
      canvas.setMapData = function (d) {
        console.time('map')
    
        d.forEach(function (d) {
          mapIndex[d.id] = [d.x, d.y]
        })
    
        console.timeEnd('map')
      }
    
      var mousePos
      function mousemove(d) {
        if (timelineHover) return
    
        var mouse = d3.mouse(vizContainer.node())
        var p = toScreenPoint(mouse)
    
        mousePos = p
    
        var distance = 200
    
        var best = nearest(
          p[0] - imgPadding,
          p[1] - imgPadding,
          {
            d: distance,
            p: null,
          },
          quadtree,
        )
    
        selectedImageDistance = best.d
        // console.log(cursorCutoff,scale, scale1,  selectedImageDistance)
    
        if (bottomZooming && best.p && best.p.ii < 3 && selectedImageDistance > 7) {
          selectedImage = null
          zoom.center(null)
          container.style('cursor', 'default')
        } else {
          if (best.p && !zoomedToImage) {
            var d = best.p
            var center = [
              (d.x + imgPadding) * scale + translate[0],
              (height + d.y + imgPadding) * scale + translate[1],
            ]
            zoom.center(center)
            selectedImage = d
          }
    
          container.style('cursor', function () {
            return selectedImageDistance < cursorCutoff && selectedImage.active
              ? 'pointer'
              : 'default'
          })
        }
      }
    
      function stackLayout(data, invert) {
        var years = d3
          .nest()
          .key(function (d) {
            return d.year
          })
          .entries(data)
    
        years.forEach(function (year) {
          var startX = x(year.key)
          var total = year.values.length
          year.values.sort(function (a, b) {
            return b.keywords.length - a.keywords.length
          })
    
          year.values.forEach(function (d, i) {
            var row = Math.floor(i / collumns) + 2
            d.ii = i
    
            d.x = startX + (i % collumns) * (rangeBand / collumns)
            d.y = (invert ? 1 : -1) * (row * (rangeBand / collumns))
    
            d.x1 = d.x * scale1 + imageSize / 2
            d.y1 = d.y * scale1 + imageSize / 2
    
            if (d.sprite.position.x == 0) {
              d.sprite.position.x = d.x1
              d.sprite.position.y = d.y1
            }
    
            if (d.sprite2) {
              d.sprite2.position.x = d.x * scale2 + imageSize2 / 2
              d.sprite2.position.y = d.y * scale2 + imageSize2 / 2
            }
    
            d.order = (invert ? 1 : 1) * (total - i)
          })
        })
      }
    
      canvas.distance = function (a, b) {
        return Math.sqrt(
          (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]),
        )
      }
    
      function toScreenPoint(p) {
        var p2 = [0, 0]
    
        p2[0] = p[0] / scale - translate[0] / scale
        p2[1] = p[1] / scale - height - translate[1] / scale
    
        return p2
      }
    
      function imageAnimation() {
        var sleep = true
    
        data.forEach(function (d, i) {
          var diff
          diff = d.x1 - d.sprite.position.x
          if (Math.abs(diff) > 0.1) {
            d.sprite.position.x += diff * 0.1
            sleep = false
          }
    
          diff = d.y1 - d.sprite.position.y
          if (Math.abs(diff) > 0.1) {
            d.sprite.position.y += diff * 0.1
            sleep = false
          }
    
          diff = d.alpha - d.sprite.alpha
          if (Math.abs(diff) > 0.01) {
            d.sprite.alpha += diff * 0.2
            sleep = false
          }
    
          d.sprite.visible = d.sprite.alpha > 0.1
    
          if (state.mode === 'map') {
            d.sprite.visible = d.active
          }
    
          if (d.sprite2) {
            diff = d.alpha2 - d.sprite2.alpha
            if (Math.abs(diff) > 0.01) {
              d.sprite2.alpha += diff * 0.2
              sleep = false
            }
    
            d.sprite2.visible = d.sprite2.alpha > 0.1
            //else d.sprite2.visible = d.visible;
          }
        })
        return sleep
      }
    
      canvas.wakeup = function () {
        sleep = false
      }
    
      canvas.setMode = function (mode) {
        state.mode = mode
        canvas.project()
      }
    
      function animate(time) {
        requestAnimationFrame(animate)
        loadImages()
        if (sleep) return
        sleep = imageAnimation()
        renderer.render(stage)
      }
    
      function zoomToYear(d) {
        var xYear = x(d.year)
        var scale = 1 / ((rangeBand * 4) / width)
        var padding = rangeBand * 1.5
        var translateNow = [-scale * (xYear - padding), -scale * (height + d.y)]
    
        vizContainer
          .call(zoom.translate(translate).event)
          .transition()
          .duration(2000)
          .call(zoom.scale(scale).translate(translateNow).event)
      }
    
      function zoomToImage(d, duration) {
        state.zoomingToImage = true
        zoom.center(null)
        loadMiddleImage(d)
        d3.select('.tagcloud').classed('hide', true)
        var padding = x.rangeBand() / collumns / 2
        var sidbar = width / 8
        var scale = 0.8 / (x.rangeBand() / collumns / width)
        var translateNow = [
          -scale * (d.x - padding / 2) - sidbar,
          -scale * (height + d.y),
        ]
    
        zoomedToImageScale = scale
    
        setTimeout(function () {
          hideTheRest(d)
        }, duration / 2)
    
        vizContainer
          .call(zoom.translate(translate).event)
          .transition()
          .duration(duration)
          .call(zoom.scale(scale).translate(translateNow).event)
          .each('end', function () {
            zoomedToImage = true
            selectedImage = d
            hideTheRest(d)
            showDetail(d)
            loadBigImage(d, 'click')
            state.zoomingToImage = false
          })
      }
    
      function showDetail(d) {
        // console.log("show detail", d)
    
        detailContainer.select('.outer').node().scrollTop = 0
    
        detailContainer.classed('hide', false).classed('sneak', utils.isMobile())
    
        // needs to be done better
        var detailData = {}
        for (field in selectedImage) {
          if (field[0] === '_') detailData[field] = selectedImage[field]
        }
        detailData['_id'] = selectedImage.id
        detailData['_keywords'] = selectedImage.keywords
        detailData['_year'] = selectedImage.year
        detailData['_imagenum'] = selectedImage.imagenum || 1
        detailVue._data.item = detailData
        detailVue._data.id = d.id
        detailVue._data.page = d.page
      }
    
      canvas.changePage = function (id, page) {
        console.log('changePage', id, page, selectedImage)
        selectedImage.page = page
        detailVue._data.page = page
        clearBigImages()
        loadBigImage(selectedImage)
      }
    
      function hideTheRest(d) {
        data.forEach(function (d2) {
          if (d2.id !== d.id) {
            d2.alpha = 0
            d2.alpha2 = 0
          }
        })
      }
    
      function showAllImages() {
        data.forEach(function (d) {
          d.alpha = d.active ? 1 : 0.2
          d.alpha2 = d.visible ? 1 : 0
        })
      }
    
      function zoomed() {
        translate = d3.event.translate
        scale = d3.event.scale
        if (!startTranslate) startTranslate = translate
        drag = startTranslate && translate !== startTranslate
        // check borders
        var x1 = (-1 * translate[0]) / scale
        var x2 = x1 + widthOuter / scale
    
        if (d3.event.sourceEvent != null && state.mode !== 'map') {
          if (x1 < 0) {
            translate[0] = 0
          } else if (x2 > widthOuter) {
            translate[0] = (widthOuter * scale - widthOuter) * -1
          }
    
          zoom.translate([translate[0], translate[1]])
    
          x1 = (-1 * translate[0]) / scale
          x2 = x1 + width / scale
        }
    
        if (
          zoomedToImageScale != 0 &&
          scale > zoomedToImageScale * 0.9 &&
          !zoomedToImage &&
          selectedImage &&
          selectedImage.type == 'image'
        ) {
          zoomedToImage = true
          zoom.center(null)
          zoomedToImageScale = scale
          hideTheRest(selectedImage)
          showDetail(selectedImage)
        }
    
        if (zoomedToImage && zoomedToImageScale * 0.8 > scale) {
          // console.log("clear")
          zoomedToImage = false
          state.lastZoomed = 0
          showAllImages()
          clearBigImages()
          detailContainer.classed('hide', true)
        }
    
        timeline.update(x1, x2, scale, translate, scale1)
        if (state.mode === 'map')
          map.zoom(selectedImage, mousePos, scale, translate, imageSize)
    
        // toggle zoom overlays
        if (scale > zoomBarrier) {
          d3.select('.tagcloud').classed('hide', true)
          d3.select('.searchbar').classed('hide', true)
          d3.select('.infobar').classed('sneak', true)
        } else {
          d3.select('.tagcloud').classed('hide', false)
          d3.select('.searchbar').classed('hide', false)
        }
    
        stage2.scale.x = d3.event.scale
        stage2.scale.y = d3.event.scale
        stage2.x = d3.event.translate[0]
        stage2.y = d3.event.translate[1]
    
        sleep = false
      }
    
      function zoomstart(d) {
        zooming = true
        startTranslate = false
        drag = false
        startScale = scale
      }
    
      function zoomend(d) {
        drag = startTranslate && translate !== startTranslate
        zooming = false
        filterVisible()
    
        if (
          zoomedToImage &&
          !selectedImage.big &&
          state.lastZoomed != selectedImage.id &&
          !state.zoomingToImage
        ) {
          loadBigImage(selectedImage, 'zoom')
        }
      }
    
      canvas.highlight = function () {
        data.forEach(function (d, i) {
          d.alpha = d.highlight ? 1 : 0.2
        })
        canvas.wakeup()
      }
    
      // canvas.project = function () {
      //     sleep = false
      //     canvas.split();
      //     canvas.resetZoom();
      // }
    
      canvas.project = function () {
        sleep = false
        data.forEach(function (d) {
          if (state.mode === 'time') {
            d.scaleFactor = 0.9
          }
          if (state.mode === 'map') {
            d.scaleFactor = scale1 / 40
          }
          d.sprite.scale.x = d.sprite.scale.y = d.scaleFactor
        })
    
        if (state.mode == 'tsne') {
          canvas.projectTSNE()
        } else if (state.mode == 'map') {
          canvas.projectMap()
        } else {
          canvas.split()
        }
        canvas.resetZoom()
      }
    
      canvas.projectMap = function () {
        // console.log(mapIndex)
    
        // var inactive = data.filter(function (d) {
        //   return !d.active
        // })
    
        // var active = data.filter(function (d) {
        //   return d.active
        // })
    
        data.forEach(function (d) {
          var mapEntry = mapIndex[d.id]
          if (mapEntry) {
            d.x = mapEntry[0] - margin.left - imgPadding
            d.y = mapEntry[1] - imgPadding - height
            // console.log(mapEntry)
          } else {
            // if there is no geo for entry
            d.x = 100
            d.y = -100
          }
    
          d.x1 = d.x * scale1 + imageSize / 2
          d.y1 = d.y * scale1 + imageSize / 2
    
          if (d.sprite.position.x == 0) {
            d.sprite.position.x = d.x1
            d.sprite.position.y = d.y1
          }
    
          if (d.sprite2) {
            d.sprite2.position.x = d.x * scale2 + imageSize2 / 2
            d.sprite2.position.y = d.y * scale2 + imageSize2 / 2
          }
        })
    
        quadtree = Quadtree(data)
        //chart.resetZoom();
      }
    
      canvas.projectTSNE = function () {
        var marginBottom = -height / 2.5
    
        var inactive = data.filter(function (d) {
          return !d.active
        })
        var inactiveSize = inactive.length
    
        var active = data.filter(function (d) {
          return d.active
        })
    
        // inactive.sort(function (a, b) {
        //     return a.rTSNE - b.rTSNE
        // });
    
        var dimension = Math.min(width, height) * 0.8
    
        inactive.forEach(function (d, i) {
          var r = dimension / 1.9 + Math.random() * 40
          var a = -Math.PI / 2 + (i / inactiveSize) * 2 * Math.PI
    
          d.x = r * Math.cos(a) + width / 2 + margin.left
          d.y = r * Math.sin(a) + marginBottom
        })
    
        active.forEach(function (d) {
          var factor = height / 2
          var tsneEntry = tsneIndex[d.id]
          if (tsneEntry) {
            d.x = tsneEntry[0] * dimension + width / 2 - dimension / 2 + margin.left
            d.y = tsneEntry[1] * dimension - dimension / 2 + marginBottom
          }
          // var tsneEntry = tsne.find(function (t) {
          //     return t.id == d.id
          // })
        })
    
        data.forEach(function (d) {
          d.x1 = d.x * scale1 + imageSize / 2
          d.y1 = d.y * scale1 + imageSize / 2
    
          if (d.sprite.position.x == 0) {
            d.sprite.position.x = d.x1
            d.sprite.position.y = d.y1
          }
    
          if (d.sprite2) {
            d.sprite2.position.x = d.x * scale2 + imageSize2 / 2
            d.sprite2.position.y = d.y * scale2 + imageSize2 / 2
          }
        })
    
        quadtree = Quadtree(data)
        //chart.resetZoom();
      }
    
      canvas.resetZoom = function () {
        var duration = 1400
    
        extent = d3.extent(data, function (d) {
          return d.y
        })
    
        var y = -extent[1] - bottomPadding
        y = extent[1] / -3 - bottomPadding
    
        // if (state.mode === 'map') {
        //   y = translate[1]
        // }
        console.log(translate, y)
    
        vizContainer
          .call(zoom.translate(translate).event)
          .transition()
          .duration(duration)
          .call(zoom.translate([0, y]).scale(1).event)
      }
    
      canvas.split = function () {
        var active = data.filter(function (d) {
          return d.active
        })
        stackLayout(active, false)
        var inactive = data.filter(function (d) {
          return !d.active
        })
        stackLayout(inactive, true)
        quadtree = Quadtree(data)
      }
    
      function filterVisible() {
        var zoomScale = scale
        if (zoomedToImage) return
    
        data.forEach(function (d, i) {
          var p = d.sprite.position
          var x = p.x / scale1 + translate[0] / zoomScale
          var y = p.y / scale1 + translate[1] / zoomScale
          var padding = width / 3 / scale
    
          if (
            x > -padding &&
            x < width / zoomScale + padding &&
            y + height < height / zoomScale + padding &&
            y > height * -1 - padding
          ) {
            d.visible = true
          } else {
            d.visible = false
          }
        })
    
        var visible = data.filter(function (d) {
          return d.visible
        })
    
        if (visible.length < 40) {
          data.forEach(function (d) {
            if (d.visible && d.loaded && d.active) d.alpha2 = 1
            else if (d.visible && !d.loaded && d.active) loadImagesCue.push(d)
            else d.alpha2 = 0
          })
        } else {
          data.forEach(function (d) {
            d.alpha2 = 0
          })
        }
      }
    
      function loadMiddleImage(d) {
        if (d.loaded) {
          d.alpha2 = 1
          return
        }
    
        // console.log("load", d)
        var url = config.loader.textures.detail.url + d.id + '.jpg'
        var texture = new PIXI.Texture.fromImage(url)
        var sprite = new PIXI.Sprite(texture)
    
        var update = function () {
          sleep = false
        }
    
        sprite.on('added', update)
        texture.once('update', update)
    
        sprite.scale.x = d.scaleFactor
        sprite.scale.y = d.scaleFactor
    
        sprite.anchor.x = 0.5
        sprite.anchor.y = 0.5
        sprite.position.x = d.x * scale2 + imageSize2 / 2
        sprite.position.y = d.y * scale2 + imageSize2 / 2
        sprite._data = d
        stage4.addChild(sprite)
        d.sprite2 = sprite
        d.alpha2 = d.highlight
        d.loaded = true
        sleep = false
      }
    
      function loadBigImage(d) {
        if (!config.loader.textures.big) {
          loadMiddleImage(d)
          return
        }
    
        state.lastZoomed = d.id
        var page = d.page ? '_' + d.page : ''
        var url = config.loader.textures.big.url + d.id + page + '.jpg'
    
        var texture = new PIXI.Texture.from(url)
        var sprite = new PIXI.Sprite(texture)
        var res = config.loader.textures.big.size
    
        var updateSize = function (t) {
          var size = Math.max(texture.width, texture.height)
          sprite.scale.x = sprite.scale.y = (imageSize3 / size) * d.scaleFactor
          sleep = false
          if (t.valid) {
            d.alpha = 0
            d.alpha2 = 0
          }
        }
    
        sprite.on('added', updateSize)
        texture.once('update', updateSize)
    
        if (d.imagenum > 1) {
          sprite.on('mousemove', function (s) {
            var pos = s.data.getLocalPosition(s.currentTarget)
            s.currentTarget.cursor = pos.x > 0 ? 'e-resize' : 'w-resize'
          })
          sprite.on('click', function (s) {
            if (drag) return
    
            s.stopPropagation()
            spriteClick = true
            var pos = s.data.getLocalPosition(s.currentTarget)
            var dir = pos.x > 0 ? 1 : -1
            var page = d.page + dir
            var nextPage = page
            if (page > d.imagenum - 1) nextPage = 0
            if (page < 0) nextPage = d.imagenum - 1
    
            canvas.changePage(d.id, nextPage)
          })
          sprite.interactive = true
        }
    
        sprite.anchor.x = 0.5
        sprite.anchor.y = 0.5
        sprite.position.x = d.x * scale3 + imageSize3 / 2
        sprite.position.y = d.y * scale3 + imageSize3 / 2
        sprite._data = d
        d.big = true
        stage5.addChild(sprite)
        sleep = false
      }
    
      function clearBigImages() {
        while (stage5.children[0]) {
          stage5.children[0]._data.big = false
          stage5.removeChild(stage5.children[0])
          sleep = false
        }
      }
    
      function loadImages() {
        if (zooming) return
        if (zoomedToImage) return
    
        if (loadImagesCue.length) {
          var d = loadImagesCue.pop()
          if (!d.loaded) {
            loadMiddleImage(d)
          }
        }
      }
    
      function nearest(x, y, best, node) {
        // mike bostock https://bl.ocks.org/mbostock/4343214
        var x1 = node.x1,
          y1 = node.y1,
          x2 = node.x2,
          y2 = node.y2
        node.visited = true
        //console.log(node, x , x1 , best.d);
        //return;
        // exclude node if point is farther away than best distance in either axis
        if (
          x < x1 - best.d ||
          x > x2 + best.d ||
          y < y1 - best.d ||
          y > y2 + best.d
        ) {
          return best
        }
        // test point if there is one, potentially updating best
        var p = node.point
        if (p) {
          p.scanned = true
          var dx = p.x - x,
            dy = p.y - y,
            d = Math.sqrt(dx * dx + dy * dy)
          if (d < best.d) {
            best.d = d
            best.p = p
          }
        }
        // check if kid is on the right or left, and top or bottom
        // and then recurse on most likely kids first, so we quickly find a
        // nearby point and then exclude many larger rectangles later
        var kids = node.nodes
        var rl = 2 * x > x1 + x2,
          bt = 2 * y > y1 + y2
        if (kids[bt * 2 + rl]) best = nearest(x, y, best, kids[bt * 2 + rl])
        if (kids[bt * 2 + (1 - rl)])
          best = nearest(x, y, best, kids[bt * 2 + (1 - rl)])
        if (kids[(1 - bt) * 2 + rl])
          best = nearest(x, y, best, kids[(1 - bt) * 2 + rl])
        if (kids[(1 - bt) * 2 + (1 - rl)])
          best = nearest(x, y, best, kids[(1 - bt) * 2 + (1 - rl)])
    
        return best
      }
    
      return canvas
    }