385 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
			
		
		
	
	
			385 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
| <template>
 | |
|   <div
 | |
|     class="md-ruler"
 | |
|     @touchstart="$_startDrag"
 | |
|     @touchend="$_stopDrag"
 | |
|   >
 | |
|     <canvas
 | |
|       class="md-ruler-canvas"
 | |
|       ref="canvas"
 | |
|     ></canvas>
 | |
|     <div
 | |
|       class="md-ruler-cursor"
 | |
|       :class="[isStepTextBottom && 'md-ruler-cursor-bottom']"
 | |
|     ></div>
 | |
|     <div class="md-ruler-arrow"></div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script>
 | |
| import Scroller from '../_util/scroller'
 | |
| import {throttle, noop} from '../_util'
 | |
| 
 | |
| export default {
 | |
|   name: 'md-ruler',
 | |
| 
 | |
|   components: {},
 | |
| 
 | |
|   props: {
 | |
|     value: {
 | |
|       type: Number,
 | |
|       default: 0,
 | |
|     },
 | |
|     scope: {
 | |
|       type: Array,
 | |
|       default: () => [0, 100],
 | |
|     },
 | |
|     step: {
 | |
|       type: Number,
 | |
|       default: 10,
 | |
|     },
 | |
|     unit: {
 | |
|       type: Number,
 | |
|       default: 1,
 | |
|     },
 | |
|     min: {
 | |
|       type: Number,
 | |
|       default: 0,
 | |
|     },
 | |
|     max: {
 | |
|       type: Number,
 | |
|       default: 100,
 | |
|     },
 | |
|     stepTextPosition: {
 | |
|       type: String,
 | |
|       default: 'top',
 | |
|       validator: val => !!~['top', 'bottom'].indexOf(val),
 | |
|     },
 | |
|     stepTextRender: {
 | |
|       type: Function,
 | |
|       default: noop,
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   data() {
 | |
|     return {
 | |
|       clientHeight: 60,
 | |
|       scroller: null,
 | |
|       ratio: 2,
 | |
| 
 | |
|       isInitialed: false,
 | |
|       isDragging: false,
 | |
|       isScrolling: false,
 | |
| 
 | |
|       x: 0,
 | |
|       scrollingX: 0,
 | |
|       blank: 30, // unit blank
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   computed: {
 | |
|     unitCount() {
 | |
|       const {scope: [min, max], unit} = this
 | |
|       return Math.ceil((max - min) / unit)
 | |
|     },
 | |
| 
 | |
|     canvasWidth() {
 | |
|       return this.$refs.canvas.clientWidth * this.ratio
 | |
|     },
 | |
| 
 | |
|     realMin() {
 | |
|       const {scope, min} = this
 | |
|       const [left, right] = scope
 | |
|       if (min > right) {
 | |
|         return left
 | |
|       }
 | |
|       return min > left ? min : left
 | |
|     },
 | |
| 
 | |
|     realMax() {
 | |
|       let {scope, max} = this
 | |
|       const [left, right] = scope
 | |
|       if (left > max) {
 | |
|         return right
 | |
|       }
 | |
|       return max > right ? right : max
 | |
|     },
 | |
| 
 | |
|     blankLeft() {
 | |
|       const {scope, realMin, unit, blank} = this
 | |
|       const [min] = scope
 | |
|       return Math.ceil((realMin - min) / unit) * blank
 | |
|     },
 | |
| 
 | |
|     blankRight() {
 | |
|       const {scope, realMax, unit, blank} = this
 | |
|       const [, max] = scope
 | |
|       return Math.ceil((max - realMax) / unit) * blank
 | |
|     },
 | |
|     isStepTextBottom() {
 | |
|       return this.stepTextPosition === 'bottom'
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   watch: {
 | |
|     value() {
 | |
|       if (this.isScrolling) {
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       this.scrollingX = 0
 | |
|       this.isScrolling = true
 | |
|       const x = this.$_initX()
 | |
|       this.$_draw(x)
 | |
|       this.scroller.scrollTo(x, 0, true)
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   mounted() {
 | |
|     const {$refs} = this
 | |
| 
 | |
|     // without watch ctx
 | |
|     this.ctx = $refs.canvas.getContext('2d')
 | |
| 
 | |
|     this.$_initCanvas()
 | |
|     this.x = this.canvasWidth
 | |
|     this.$_initScroller()
 | |
|   },
 | |
| 
 | |
|   methods: {
 | |
|     // MARK: private methods
 | |
|     $_initCanvas() {
 | |
|       const {ratio, ctx, canvasWidth, clientHeight, $refs} = this
 | |
|       const {canvas} = $refs
 | |
| 
 | |
|       canvas.width = canvasWidth
 | |
|       canvas.height = clientHeight * ratio
 | |
| 
 | |
|       const scale = 1 / ratio
 | |
|       ctx.scale(scale, 1)
 | |
|     },
 | |
| 
 | |
|     $_initScroller() {
 | |
|       const {blankLeft, blankRight, blank, unitCount, canvasWidth, clientHeight} = this
 | |
| 
 | |
|       const drawFn = throttle(this.$_draw, 10)
 | |
|       const scroller = new Scroller(
 | |
|         left => {
 | |
|           if (this.isInitialed) {
 | |
|             drawFn(left)
 | |
|           } else {
 | |
|             this.$_draw(left)
 | |
|           }
 | |
|         },
 | |
|         {
 | |
|           scrollingX: true,
 | |
|           scrollingY: false,
 | |
|           snapping: true,
 | |
|           snappingVelocity: 1,
 | |
|           animationDuration: 200,
 | |
|           inRequestAnimationFrame: true,
 | |
|           scrollingComplete: () => {
 | |
|             this.isScrolling = false
 | |
|           },
 | |
|         },
 | |
|       )
 | |
| 
 | |
|       // set real scroll width
 | |
|       const innerWidth = unitCount * blank + canvasWidth - blankLeft - blankRight
 | |
|       const x = this.$_initX()
 | |
|       this.$_draw(x)
 | |
|       scroller.setDimensions(canvasWidth, clientHeight, innerWidth, clientHeight)
 | |
|       scroller.setSnapSize(blank, 0)
 | |
|       scroller.scrollTo(x, 0, false)
 | |
| 
 | |
|       this.scroller = scroller
 | |
|       this.isInitialed = true
 | |
|     },
 | |
| 
 | |
|     $_initX() {
 | |
|       const {value, scope, realMin, realMax, unit, blank, unitCount, canvasWidth} = this
 | |
|       const [min] = scope
 | |
| 
 | |
|       this.x = canvasWidth - Math.ceil((realMin - min) / unit) * blank
 | |
| 
 | |
|       if (value <= realMin) {
 | |
|         return 0
 | |
|       } else if (value >= realMax) {
 | |
|         return unitCount * blank
 | |
|       } else {
 | |
|         return Math.ceil((value - realMin) / unit) * blank
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     $_draw(left) {
 | |
|       left = +left.toFixed(2)
 | |
|       const {ctx, ratio, scrollingX, canvasWidth, clientHeight} = this
 | |
| 
 | |
|       this.scrollingX = left
 | |
|       this.x += scrollingX - left
 | |
| 
 | |
|       // clear canvas
 | |
|       const scale = ratio * ratio
 | |
|       ctx.clearRect(0, 0, canvasWidth * scale, clientHeight * scale)
 | |
| 
 | |
|       this.$_drawLine()
 | |
|     },
 | |
| 
 | |
|     $_drawLine() {
 | |
|       const {ctx, x, scope, step, unit, ratio, blank, unitCount, isStepTextBottom} = this
 | |
|       const {blankLeft, blankRight, canvasWidth} = this
 | |
|       const [scopeLeft] = scope
 | |
| 
 | |
|       const _fontSize = 22
 | |
|       const _y = 120 - (isStepTextBottom ? _fontSize + 40 : 0)
 | |
|       const _stepUnit = Math.round(step / unit)
 | |
| 
 | |
|       ctx.lineWidth = 2
 | |
|       ctx.font = `${_fontSize *
 | |
|         ratio}px DIN Alternate, "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif`
 | |
| 
 | |
|       for (let i = 0; i <= unitCount; i++) {
 | |
|         const _x = x + i * blank
 | |
| 
 | |
|         if (_x < 0 || _x > canvasWidth * 2) {
 | |
|           continue
 | |
|         }
 | |
| 
 | |
|         // over range use another color
 | |
|         const outRange = _x < x + blankLeft || _x > x + 1 + unitCount * blank - blankRight
 | |
|         if (outRange) {
 | |
|           ctx.fillStyle = '#E2E4EA'
 | |
|           ctx.strokeStyle = '#E2E4EA'
 | |
|         } else {
 | |
|           ctx.fillStyle = '#C5CAD5'
 | |
|           ctx.strokeStyle = '#858B9C'
 | |
|         }
 | |
| 
 | |
|         ctx.beginPath()
 | |
|         ctx.moveTo(_x, _y)
 | |
| 
 | |
|         if (i % _stepUnit === 0) {
 | |
|           // draw text
 | |
|           const text = this.$_matchStepText(scopeLeft + unit * i)
 | |
|           const textOffset = String(text).length * _fontSize / 2
 | |
|           ctx.fillText(text, _x - textOffset, _fontSize * ratio + (isStepTextBottom ? 70 : 0))
 | |
| 
 | |
|           // draw line
 | |
|           ctx.lineTo(_x, _y - 40)
 | |
|         } else {
 | |
|           ctx.lineTo(_x, _y - 20)
 | |
|         }
 | |
|         ctx.stroke()
 | |
|       }
 | |
| 
 | |
|       // draw base line
 | |
|       ctx.strokeStyle = '#E2E4EA'
 | |
|       ctx.beginPath()
 | |
|       ctx.moveTo(x, _y)
 | |
|       ctx.lineTo(x + unitCount * blank, _y)
 | |
|       ctx.stroke()
 | |
| 
 | |
|       this.$_updateValue()
 | |
|     },
 | |
| 
 | |
|     $_matchStepText(step) {
 | |
|       const match = this.stepTextRender(step)
 | |
|       return match !== undefined && match !== null ? match : step
 | |
|     },
 | |
| 
 | |
|     $_startDrag(event) {
 | |
|       if (this.isDragging) {
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       event.preventDefault()
 | |
|       event.stopPropagation()
 | |
|       this.scroller.doTouchStart(event.touches, event.timeStamp)
 | |
| 
 | |
|       this.isDragging = true
 | |
|       this.isScrolling = true
 | |
| 
 | |
|       window.addEventListener('touchmove', this.$_onDrag)
 | |
|     },
 | |
| 
 | |
|     $_onDrag(event) {
 | |
|       event.preventDefault()
 | |
|       event.stopPropagation()
 | |
|       if (!this.isDragging) {
 | |
|         return
 | |
|       }
 | |
|       this.scroller.doTouchMove(event.touches, event.timeStamp, event.scale)
 | |
|     },
 | |
| 
 | |
|     $_stopDrag(event) {
 | |
|       event.preventDefault()
 | |
|       event.stopPropagation()
 | |
|       this.isDragging = false
 | |
| 
 | |
|       this.scroller.doTouchEnd(event.timeStamp)
 | |
| 
 | |
|       window.removeEventListener('touchmove', this.$_onDrag)
 | |
|     },
 | |
| 
 | |
|     $_updateValue() {
 | |
|       if (!this.isInitialed) {
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       const {x, scope: [min], realMin, realMax, unit, blank, canvasWidth} = this
 | |
| 
 | |
|       if (x > canvasWidth) {
 | |
|         this.$_onInput(realMin)
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       const _x = x >= 0 ? Math.abs(x - canvasWidth) : Math.abs(x) + canvasWidth
 | |
|       let value = min + Math.round(_x / blank) * unit
 | |
| 
 | |
|       value > realMax && (value = realMax)
 | |
|       value < realMin && (value = realMin)
 | |
|       this.$_onInput(value)
 | |
|     },
 | |
| 
 | |
|     // MARK: events handler, 如 $_onButtonClick
 | |
|     $_onInput(value) {
 | |
|       this.$emit('input', value)
 | |
|       this.$emit('change', value)
 | |
|     },
 | |
|   },
 | |
| }
 | |
| 
 | |
| </script>
 | |
| 
 | |
| <style lang="stylus">
 | |
| .md-ruler
 | |
|   position relative
 | |
|   padding 36px 0 20px
 | |
|   width 100%
 | |
|   height 142px
 | |
|   box-sizing border-box
 | |
|   font-family font-family-number
 | |
|   .md-ruler-canvas
 | |
|     width 100%
 | |
|     height 60px
 | |
|   .md-ruler-cursor
 | |
|     z-index 10
 | |
|     position absolute
 | |
|     top 26px
 | |
|     left 50%
 | |
|     width 2px
 | |
|     height 70px
 | |
|     transform translate(-50%)
 | |
|     background-color #2F86F6
 | |
|     box-shadow 0 2px 4px #2F86F6
 | |
|     &-bottom
 | |
|       height 40px
 | |
|   .md-ruler-arrow
 | |
|     z-index 10
 | |
|     position absolute
 | |
|     bottom 25px
 | |
|     left 50%
 | |
|     border-bottom 10px solid #2F86F6
 | |
|     border-left 10px solid transparent
 | |
|     border-right 10px solid transparent
 | |
|     transform translate(-50%)
 | |
| </style>
 |