更正可拖动元素的像素位置以从任意角开始

Correct draggable element's pixel position to start from a arbitrary corner

包含draggablemoveable句柄的元素只能从topleft位置开始,我不知道如何调整计算或更正使可拖动手柄元素从左下角开始的像素值,就像普通图形一样。

handle 的位置用于为 xy 轴生成范围限制值,例如 0-100 之间的百分比值,而不管元素的像素大小。

它是一种范围输入或位置选择器,旨在用于颜色选择器小部件。
颜色渐变根据小部件相对于某物的左侧、顶部或右侧的相对位置而变化,因此选择器或手柄应相应地调整其范围的起点。

我正在使用 onpointermove 获取 div.handlexy 的位置 调整父元素的相对 widthheightlefttop 偏移量。

我一辈子都想不通的是允许范围输入从任意角跟踪位置所需的数学和代码,最好是 bottom left

很抱歉使用自定义库,但这个例子主要是普通的,至少重要的计算是。

const {dom, component, each, on, once, isNum, $, run} = rilti

// keep a number between a minimum and maximum ammount
const clamp = (n, min, max) => Math.min(Math.max(n, min), max)

// define element behavior
component('range-input', {
  // set up everything before element touches DOM
  create (range /* Proxy<Function => Element> */) {
    // setup zero values in state (observer-like abstraction tracking changes)
    range.state({value: 0, valueX: 0, valueY: 0})

    // local vars for easier logic
    let Value, ValueY

    // create element <div class="handle"> and append to <range-input>
    // also add property to range and get it as a const
    const handle = range.handle = dom.div.handle({$: range})

    // set the range limits at 0-100% by default for X and Y axis
    if (range.limit == null) range.limitX = range.limit = 100
    if (range.limit !== range.limitX) range.limitX = range.limit
    if (range.limitY == null) range.limitY = range.limit

    // set the X position by percentage/range number,
    // move the handle accordingly and change state
    range.setX = (value = range.value || 0, skipChecks) => {
      if (!skipChecks && value === Value) return
      if (value > range.limitX || value < 0) throw new Error('value out of range')
      
      // if the element is not in the dom
      // then wait for it to mount first
      if (!range.mounted) {
        range.once.mount(e => range.setX(value))
        return
      }

      // allow float values or round it to ints by default
      if (!range.decimals) value = Math.round(value)

      const hWidth = handle.offsetWidth
      // get pixel range
      const Min = hWidth / 2
      const Max = range.offsetWidth - Min
      // calculate pixel postion from range value
      const hLeft = (value / range.limitX) * (Max - Min)
      handle.style.left = hLeft + 'px'
      // update all the states
      Value = range.state.value = range.state.valueX = value
    }
    
    // same as setX but for Y axis
    range.setY = (value = range.valueY || 0, skipChecks) => {
      if (!skipChecks && value === Value) return
      if (value > range.limitY || value < 0) throw new Error('value out of range')
      if (!range.mounted) {
        range.once.mount(e => range.setY(value))
        return
      }
      const hHeight = handle.offsetHeight
      const Min = hHeight / 2
      const Max = range.offsetHeight - Min
      const hTop = (value / range.limitY) * (Max - Min)
      handle.style.top = hTop + 'px'

      if (!range.decimals) value = Math.round(value)
      ValueY = range.state.valueY = value
    }

    // get the raw Element/Node and define (s/g)etters
    Object.defineProperties(range() /* -> <range-input> */, {
      value: {get: () => Value, set: range.setX},
      valueX: {get: () => Value, set: range.setX},
      valueY: {get: () => ValueY, set: range.setY}
    })

    let rWidth // range.offsetWidth
    let rHeight // range.offsetHeight
    let rRect // cache of range.getBoundingClientRect()
    // called when user moves the handle
    const move = (x = 0, y = 0) => {
      // check the the axis is not locked
      // for when you want to use range-input as a slider
      if (!range.lockX) {
        // adjust for relative position
        if (x < rRect.left) x = rRect.left
        else if (x > rRect.left + rWidth) x = rRect.left + rWidth
        x -= rRect.left

        const hWidth = handle.offsetWidth
        
        // get pixel range
        const min = hWidth / 2
        const max = rWidth - min

        // keep it inside the block
        const hLeft = clamp(x, min, max) - min
        handle.style.left = hLeft + 'px'

        // pixel position -> percentage/value
        let value = (hLeft * range.limitX) / (max - min)
        
        // round value to an int by default
        if (!range.decimals) value = Math.round(value)
        
        // set it if it's not the same as the old value
        if (value !== Value) {
          Value = range.state.value = range.state.valueX = value
        }
      }

      // now do below as above for Y axis
      if (!range.lockY) { // when it's not locked
        if (y < rRect.top) y = rRect.top
        else if (y > rRect.top + rWidth) y = rRect.top + rHeight
        y -= rRect.top

        const hHeight = handle.offsetHeight
        const min = hHeight / 2
        const max = range.offsetHeight - min

        const hTop = clamp(y, min, max) - min
        handle.style.top = hTop + 'px'
        let value = (hTop * range.limitY) / (max - min)
        if (!range.decimals) value = Math.round(value)
        if (value !== ValueY) {
          ValueY = range.state.valueY = value
        }
      }

      // .dispatchEvent(new CustomEvent('input'))
      range.emit('input')
      // call an update function if it's present as a prop
      if (range.update) range.update(range, handle)
    }

    // track and manage starting, stopping and moving events
    // for .pointer(up/down/move) event types respectively.
    const events = range.state.events = {
      move: on.pointermove(document, e => move(e.x, e.y)).off(),

      stop: on.pointerup(document, () => {
        events.move.off()
        events.start.on()
      }).off(),

      start: once.pointerdown([range, handle], () => {
        [rWidth, rHeight] = [range.offsetWidth, range.offsetHeight]
        rRect = range.getBoundingClientRect()
        events.move.on()
        events.stop.on()
      }).off()
    }
    //    ^-- all the events are off at the start
    //        they get turned on when the element mounts
  },
  
  // when Element enters DOM set the positions
  mount (range) {
    if (!range.lockY) range.handle.style.top = 0
    range.setX()
    range.setY()
    // start listening for user interactions
    range.state.events.start.on()
  },
  
  // start listening again on DOM re-entry
  remount (range) {
    range.state.events.start.on()
  },
  
  // stop listening when removed from DOM
  unmount ({state: {events}}) { each(events, e => e.off()) },
  
  // track custom attribute to set some props conveniently
  attr: {
    opts (range, val) {
      run(() => // wait for DOMContentLoaded first
        val.split(';')
          .filter(v => v != null && v.length)
          .map(pair => pair.trim().split(':').map(part => part.trim()))
          .forEach(([prop, value]) => {
            if (value.toLowerCase() === 'true') value = true
            else if (value.toLowerCase() === 'false') value = false
            else {
              const temp = Number(value)
              if (isNum(temp)) value = temp
            }
            if (prop === 'x' || prop === 'v') {
              range.setX(value, true)
            } else if (prop === 'y') {
              range.setY(value, true)
            } else {
              range[prop] = value
            }
          })
        )
    }
  }
})

// show the values of the range-input
$('span.stats').append($('range-input').state`
  X: ${'valueX'}%, Y: ${'valueY'}%
`)

// add a title
dom.h4('<range-input>: custom element').prependTo('body')
range-input {
  position: relative;
  display: block;
  margin: 1em auto;
  width: 250px;
  height: 250px;
  border: 1px solid #ccc;
}

range-input > div.handle {
  position: absolute;
  background: #ccc;
  width: 20px;
  height: 20px;
  cursor: grab;
  user-drag: none;
  user-select: none;
  touch-action: none;
}



.details {
  width: 225px;
  text-align: left;
  margin: 3em auto;
}

* {
  box-sizing: border-box;
}

body {
  text-align: center;
  color: hsl(0,0%,40%);
}

h4 {
 margin: 0 auto;
}
<range-input opts="x: 35; y: 80;"></range-input>

<span class="stats"></span>

<section class="details">
  <p>
    <b>Please Help:</b><br>
    I can't figure out how to code it so that
    the range-input could start at an arbitrary corner
    instead of just top left.
    I'd like it to start counting from bottom left instead.
  </p>

<pre style="text-align: left;"><code>
// the handle should be able to start at
left: 0;
bottom: 0;
// with X/Y being zero;
// not sure how to achieve this.
</code></pre>

</section>

<script src="https://rawgit.com/SaulDoesCode/rilti.js/experimental/dist/rilti.js"></script>

同样的例子 Codepen

描述您的问题的另一种方式是,您的 y 轴从 100(技术上,limitY)变为 0,而它应该从 0 变为 100。因此,我们可以稍微更改您的代码以通过以下方式反转该轴完全计算 y 百分比,然后从 100 中减去它。(即 100 - 80 = 20 或 100 - 35 - 65。)这会将高值更改为低值,反之亦然。然后,如果我们想从百分比转换为像素,我们只需再次从 100 中减去它即可得到我们原来的翻转百分比(您已经完成了所有的工作。)

改变的两行是:

const hTop = (value / range.limitY) * (Max - Min)

变成

const hTop = (1 - value / range.limitY) * (Max - Min)
// 1 - value / range.limitY is a shortening of (range.limitY - value) / range.limitY

let value = (hTop * range.limitY) / (max - min)

变成

let value = range.limitY * (1 - hTop / (max - min))
// this is also a shortening, you could have written it,
// value = range.limitY - (hTop * range.limitY) / (max - min)

这是 Codepen

同样,如果你想翻转x轴,你可以在这部分代码上使用类似的逻辑。您可以翻转两个轴的各种组合以从各个角开始。

同一问题的更难版本(一个很好的练习)是如何正确地从像素转换,不仅是百分比,而且是任何范围 ab , b 可能小于 a.