SOURCE

console 命令行工具 X clear

                    
>
console
const InputSpinner = {
  name: "InputSpinner",
  template: `
    <div 
      class="input-spinner"
      :class="{'is-dragging': dragging }"
    >
      <div 
        class="input-spinner__track u-flex-center"
        :class="{'is-min': isMin, 'is-max': isMax }"
      >
        <svg class="input-spinner__icon input-spinner__icon--minus" version="1.1" xmlns="http://www.w3.org/2000/svg" width="640" height="640" viewBox="0 0 640 640">
          <path d="M512 320c0 17.696-1.536 32-19.232 32h-345.536c-17.664 0-19.232-14.304-19.232-32s1.568-32 19.232-32h345.568c17.664 0 19.2 14.304 19.2 32z"></path>
        </svg>
        <div 
          class="input-spinner__knob u-flex-center"
          ref="knob"
        >
          <span>{{value}}</span>
        </div>
        <svg class="input-spinner__icon input-spinner__icon--plus" version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768">
          <path d="M607.5 415.5h-192v192h-63v-192h-192v-63h192v-192h63v192h192v63z"></path>
        </svg>
      </div>
      <div class="input-spinner__ball u-flex-center">
        <svg class="input-spinner__ball-bg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 695.5"><title>drop</title><path d="M587.75,523.75,384,727.5,180.25,523.75a281.43,281.43,0,0,1-62.87-94.5,289.88,289.88,0,0,1,0-218.5A289.43,289.43,0,0,1,274.75,53.5a288.32,288.32,0,0,1,218.5,0A288.37,288.37,0,0,1,650.5,210.88a288.43,288.43,0,0,1-62.75,312.88Z" transform="translate(-96 -32)"/></svg>
        <transition-group
          :name="transitionName"
          tag="div"
          class="input-spinner__values u-flex-center"
        >
          <span v-for="item in activeValue" :key="item">
            {{ item }}
          </span>
        </transition-group>
      </div>    
    </div>
  `,
  props: {
    defaultValue: {
      type: Number,
      default: 0
    },
    min: {
      type: Number,
      default: 0
    },
    max: {
      type: Number
    },
    timerStep: {
      type: Number,
      default: 1
    },
    interval: {
      type: Number,
      default: 1000
    },
    thresholds: {
      type: Array,
      default: [
        { threshold: 30, value: 1 },
        { threshold: 100, value: 10 },
      ]
    }
  },
  data() {
    return {
      value: this.defaultValue,
      dragging: false,
      position: 0,
      activeValue: [0],
      transitionName: 'list',
      initialInterval: 1500
    }
  },
  mounted () {
    this.width = this.$el.getBoundingClientRect().width;
    this.halfWidth = this.width / 2;
    this.knobWidth = this.$refs.knob.getBoundingClientRect().width;
    this.knobWidthHalf = this.knobWidth / 2;

    this.$refs.knob.addEventListener('mousedown', this.onStart);
    this.$refs.knob.addEventListener('touchstart', this.onStart);
    window.addEventListener('mousemove', this.onMove);
    window.addEventListener('touchmove', this.onMove);
    window.addEventListener('mouseup', this.onEnd);
    window.addEventListener('touchend', this.onEnd);
  },
  beforeDestroy () {
    this.$refs.knob.removeEventListener('mousedown', this.onStart);
    this.$refs.knob.addEventListener('touchstart', this.onStart);
    window.removeEventListener('mousemove', this.onMove);
    window.removeEventListener('touchmove', this.onMove);
    window.removeEventListener('mouseup', this.onEnd);
    window.removeEventListener('touchend', this.onEnd);
  },
  watch: {
    'position': function(val, oldVal) {
      this.transitionName = val > oldVal ? 'list' : 'list--reverse';
    }
  },
  methods: {
    onStart(e) {
      this.dragging = true;
      this.startX = this.getScreenX(e);
    },
    onEnd(e) {
      const deltaX = this.getScreenX(e) - this.startX;
      const position = this.positionPercent(deltaX);
      const newValue = this.value + this.activeValue[0];
      
      if(this.dragging) {
        this.value = newValue;
        
        if(newValue > this.max) {
          this.value = this.max;
        }
        if(newValue < this.min) {
          this.value = this.min;
        }
      }

      this.$emit('update', this.value);
      this.setPosition(0);
      this.dragging = false;
    },
    onMove(e) {
      if(this.dragging) {
        const deltaXFromCenter = this.getScreenX(e) - this.startX;
        this.setPosition(deltaXFromCenter);
      }
    },
    setPosition(deltaXFromCenter){
      this.position = this.positionPercent(deltaXFromCenter);
      
      if(Math.abs(this.position) >= 100 && !this.timerEnabled) {
          this.timerEnabled = true;
          this.setActiveValue(this.getThresholdValue(this.position));
          this.startTimer(this.position);
      }
      if(Math.abs(this.position) < 100) {
        this.timerEnabled = false;
      }

      if(deltaXFromCenter <= (this.halfWidth) && deltaXFromCenter >= -this.halfWidth) {
        this.setActiveValue(this.getThresholdValue(this.position));
        this.$el.style.setProperty(`--position`, Math.round(deltaXFromCenter) + 'px');
      }
    },
    setActiveValue(val) {
      this.activeValue = [val];
    },
    startTimer() {
      if(this.timer) {
        clearTimeout(this.timer);
      }
      this.timer = setTimeout(() => {
        if(this.timerEnabled) {
          const newValue = this.position >= 0
            ? this.activeValue[0] + this.timerStep
            : this.activeValue[0] - this.timerStep;
          
          this.setActiveValue(newValue);
          this.startTimer();
        }
      }, this.interval);
    },
    positionPercent(deltaXFromCenter) {
      return (deltaXFromCenter / this.halfWidth * 100);
    },
    getThresholdValue(val) {

      const availableValues = this.thresholds.filter(item => item.threshold <= Math.abs(Math.round(val)));
      let value = 0;

      if(availableValues.length > 0) {
        value = availableValues.pop().value;
      }

      return val >= 0 ? value : -1 * value;
    },
    getScreenX(e) {
      if(e.changedTouches) {
        return e.changedTouches[0].screenX;
      }
      return e.screenX;
    }
  },
  computed: {
    isMin() {
      return this.value <= this.min;
    },
    isMax() {
      return this.value >= this.max;
    }
  }
};

new Vue({
  el: '#app',
  components: {
    InputSpinner
  },
  data: {
    currentVal: 0,
    min: 0,
    max: 50,
    interval: 1000,
    timerStep: 5,
    thresholds: [
      { threshold: 30, value: 1 },
      { threshold: 100, value: 10 },
    ]
  },
  methods: {
    updateValue(val) {
      this.currentVal = val;
    },
    changeThreshold(thresholdIndex, evt) {
      this.thresholds = this.thresholds.map((item, index) => {
        if(thresholdIndex === index) {
          return {
            threshold: evt.target.value,
            value: item.value
          }
        }   
        return item;
      });
    },
    changeThresholdValue(thresholdIndex, evt) {
      this.thresholds = this.thresholds.map((item, index) => {
        if(thresholdIndex === index) {
          return {
            threshold: item.threshold,
            value: evt.target.value
          }
        }   
        return item;
      });
    }
  }
});
<a href="https://twitter.com/irkopal" class="twitter" target="_blank">
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="640" height="640" viewBox="0 0 640 640">
            <path d="M554.112 199.872c0.256 5.184 0.352 10.432 0.352 15.616 0 159.68-121.504 343.744-343.68 343.744-68.256 0-131.712-20-185.184-54.304 9.472 1.12 19.072 1.696 28.8 1.696 56.64 0 108.704-19.328 150.016-51.68-52.832-0.992-97.472-35.872-112.832-83.872 7.36 1.376 14.944 2.112 22.72 2.112 11.040 0 21.728-1.44 31.84-4.192-55.264-11.136-96.896-59.936-96.896-118.496 0-0.512 0-0.992 0-1.504 16.288 9.056 34.944 14.496 54.72 15.136-32.416-21.696-53.76-58.624-53.76-100.576 0-22.112 5.952-42.88 16.384-60.736 59.552 73.12 148.608 121.184 248.992 126.24-2.048-8.864-3.104-18.048-3.104-27.552 0-66.688 54.048-120.736 120.768-120.736 34.752 0 66.144 14.624 88.192 38.112 27.488-5.44 53.344-15.488 76.704-29.312-9.024 28.192-28.192 51.872-53.12 66.816 24.448-2.944 47.68-9.376 69.376-19.008-16.192 24.256-36.672 45.504-60.288 62.496z"></path>
        </svg> @irkopal
</a>
<div id="app" v-cloak>
  <div class="app">
    <div class="settings">
      <div class="settings__item">
        <label for="">Min value</label>
        <input type="number" v-model.number="min">
      </div>
      <div class="settings__item">
        <label for="">Max value</label>
        <input type="number" v-model.number="max">
      </div>
      <div class="settings__item">
        <label for="">Timer increase/decrease</label>
        <input type="number" v-model.number="timerStep">
      </div>
      <div class="settings__item">
        <label for="">Interval</label>
        <input type="number" v-model.number="interval" min="500" step="100">
      </div>
      <div class="settings__thresholds">
        <h3>Thresholds</h3>
        <table>
          <tr class="settings__threshold" v-for="(threshold, index) in thresholds" :key="index">
            <td>
              <label for="">Threshold</label>
              <input type="number" @change="changeThreshold(index, $event)" :value="threshold.threshold" max="100" min="0">
            </td>
            <td>
              <label for="">Value</label>
              <input type="number" @change="changeThresholdValue(index, $event)" :value="threshold.value"></td> 
          </tr>
        </table>
      </div>
    </div>
    <div class="app__content">
      <transition name="current-value" mode="out-in">
        <div class="current" :key="currentVal">{{ currentVal }}</div>
      </transition>
      <input-spinner :min="min" :max="max" @update="updateValue" :interval="interval" :thresholds="thresholds" :timer-step="timerStep"></input-spinner>
      <p style="margin-top: 3em;">
        Timer starts when the knob is dragged to the either end
      </p>
    </div>
  </div>
</div>
@import url('https://fonts.googleapis.com/css?family=Roboto:100,400');
*,
*::after,
*::before {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  min-height: 100%;
}

body {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Roboto', sans-serif;
  background: linear-gradient(to top right, #524ad0 10%, #D099FA);
  background-repeat: no-repeat;
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
}

[v-cloak] {
  display: none;
}

.twitter {
  position: absolute;
  top: 1em;
  left: 1em;
  text-decoration: none;
  color: rgba(#fff, .8);
  > * {
    vertical-align: middle;
  }
  svg {
    fill: currentColor;
    width: 1em;
    height: 1em;
  }
}

.app {
  display: flex;
  justify-content: center;
  padding: 2em;
  color: rgba(#fff, .8);
  &__content {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
}

.settings {
  display: none;
  justify-content: flex-start;
  flex-wrap: wrap;
  max-width: 30vw;
  margin-right: 10vw;
  @media (min-width: 780px) {
    display: flex;
  }
  &__item {
    padding: .2em;
  }
  &__thresholds {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    width: 100%;
  }
  label {
    display: block;
    color: rgba(#fff, .8);
    letter-spacing: 1px;
    font-size: .8em;
    margin-left: .5em;
    margin-bottom: .2em;
  }
  input {
    background: rgba(#fff, .3);
    border: none;
    border-radius: 2em;
    padding: .5em 1em;
    font-size: .8em;
    font-weight: 600;
    color: rgba(#fff, .8);
  }
}

.current {
  text-align: center;
  color: rgba(#fff, .3);
  font-size: calc(60px + 10vh);
  margin-bottom: 5vh;
}

.current-value-enter-active,
.current-value-leave-active {
  transition: all .2s ease;
}

.current-value-enter,
.current-value-leave-to {
  transform: scale(1.1);
  opacity: .5;
}

// Component spesific
.u-flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.input-spinner {
  --position: 0%;
  position: relative;
  font-size: 20px;
  &:after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border-radius: 2em;
    box-shadow: 0 .1em .6em rgba(#000, .4);
    opacity: 0;
    transition: opacity .3s;
  }
  &.is-dragging &__knob {
    transition: none;
  }
  &.is-dragging &__ball {
    opacity: 1;
    transform: translate3d(calc(-50% + var(--position)), 0, 0);
    transition: opacity .2s;
  }
  &.is-dragging:after {
    opacity: 1;
  }
}

.input-spinner__track {
  z-index: 2;
  position: relative;
  overflow: hidden;
  width: 12em;
  border-radius: 2em;
  background: rgba(#fff, 0.4);
  &.is-max .input-spinner__icon--plus {
    opacity: .3;
  }
  &.is-min .input-spinner__icon--minus {
    opacity: .3;
  }
}

.input-spinner__icon {
  position: absolute;
  top: 50%;
  width: 1.1em;
  height: 1.1em;
  fill: #fff;
  transform: translate(0, -50%);
  transition: opacity .3s;
  &--plus {
    right: .7em;
  }
  &--minus {
    left: .7em;
  }
}

.input-spinner__knob {
  z-index: 1;
  position: relative;
  height: 2.5em;
  width: 2.5em;
  font-size: 1.2em;
  color: rgba(#000, .8);
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 2px rgba(#000, .2);
  cursor: pointer;
  transform: translate3d(calc(var(--position)), 0, 0);
  transition: transform .5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  user-select: none;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  will-change: transform;
}

.input-spinner__ball {
  z-index: 3;
  position: absolute;
  overflow: hidden;
  left: 50%;
  bottom: 105%;
  opacity: 0;
  transform: translate3d(calc(-50% + var(--position)), 0, 0);
  transition: opacity .2s, transform .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  will-change: transform;
}

.input-spinner__values {
  position: absolute;
  top: -2px;
  left: 0;
  width: 100%;
  height: 100%;
  color: rgba(#000, .8);
  font-size: .6em;
}

.input-spinner__ball-bg {
  width: 1.4em;
  height: 1.4em;
  fill: rgba(#fff, .4);
}

.list {
  &-item {
    display: inline-block;
    user-select: none;
  }
  &-enter-active {
    opacity: 0;
    transform: translate3d(1em, 0, 0);
    transition: transform 0.2s .1s, opacity .2s .12s;
  }
  &-leave-active {
    position: absolute;
    top: 22%;
    transform: translate3d(0, 0, 0);
    transition: transform .3s, opacity .2s;
  }
  &-enter-to {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
  &-leave-to {
    position: absolute;
    top: 22%;
    opacity: 0;
    transform: translate3d(-1em, 0, 0);
  }
  &--reverse {
    &-enter-active {
      opacity: 0;
      transform: translate3d(-1em, 0, 0);
      transition: transform 0.2s .1s, opacity .2s .1s;
    }
    &-leave-active {
      position: absolute;
      top: 22%;
      transition: transform 0.3s, opacity .2s;
    }
    &-enter-to {
      opacity: 1;
      transform: translate3d(0, 0, 0);
    }
    &-leave-to {
      position: absolute;
      top: 22%;
      opacity: 0;
      transform: translate3d(1em, 0, 0);
    }
  }
}