diff --git a/demo/App.vue b/demo/App.vue index 68e4ffd..4c95741 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -1,18 +1,20 @@ @@ -20,16 +22,26 @@ export default { data () { return { - sliderValue: 0 + sliderMaxValue: 0, + sliderMinValue: 0, + // minKnobColor: '#EA1313' } }, computed: { - sliderValueOrZero: { + sliderMaxValueOrZero: { get () { - return this.sliderValue === '' ? 0 : this.sliderValue + return this.sliderMaxValue === '' ? 0 : this.sliderMaxValue }, set (value) { - this.sliderValue = value + this.sliderMaxValue = value + } + }, + sliderMinValueOrZero: { + get () { + return this.sliderMinValue === '' ? 0 : this.sliderMinValue + }, + set (value) { + this.sliderMinValue = value } } } @@ -41,6 +53,7 @@ export default { display: flex; flex-direction: column; align-items: center; + padding-top: 200px; } .heading { @@ -55,6 +68,11 @@ export default { margin-top: 20px; } + .min-value { + font-size: 20px; + margin-top: 20px; + } + input { height: 30px; font-size: 20px; diff --git a/src/components/CircleSlider.vue b/src/components/CircleSlider.vue index 55c2622..6ac21c4 100644 --- a/src/components/CircleSlider.vue +++ b/src/components/CircleSlider.vue @@ -9,7 +9,8 @@ - + + @@ -25,12 +26,17 @@ export default { this.defineInitialCurrentStepIndex() - this.angle = this.cpAngleValue - this.currentStepValue = this.cpCurrentStep + this.minAngle = this.cpMinAngleValue + this.maxAngle = this.cpMaxAngleValue + + this.currentMinStepValue = this.cpCurrentMinStep + this.currentMaxStepValue = this.cpCurrentMaxStep let maxCurveWidth = Math.max(this.cpMainCircleStrokeWidth, this.cpPathStrokeWidth) - this.radius = (this.side / 2) - Math.max(maxCurveWidth, this.cpKnobRadius * 2) / 2 - this.updateFromPropValue(this.value) + this.radius = (this.side / 2) - Math.max(maxCurveWidth, this.cpMinKnobRadius * 2, this.cpMaxKnobRadius * 2) / 2 + + this.updateFromPropMaxValue(this.value) + this.currentMinStepIndex > this.currentMaxStepIndex ? this.setDefaultMinValue() : this.updateFromPropMinValue(this.minValue) }, mounted () { this.containerElement = this.$refs._svg @@ -42,7 +48,6 @@ export default { type: Number, required: false, default: function () { - // return Math.PI / 20 return 0 } }, @@ -51,6 +56,11 @@ export default { required: false, default: 0 }, + minValue: { + type: Number, + required: false, + default: 0 + }, side: { type: Number, required: false, @@ -81,17 +91,32 @@ export default { required: false, default: '#00be7e' }, - knobColor: { + minKnobColor: { + type: String, + required: false, + default: '#00be7e' + }, + maxKnobColor: { type: String, required: false, default: '#00be7e' }, - knobRadius: { + minKnobRadius: { type: Number, required: false, default: null }, - knobRadiusRel: { + minKnobRadiusRel: { + type: Number, + required: false, + default: 7 + }, + maxKnobRadius: { + type: Number, + required: false, + default: null + }, + maxKnobRadiusRel: { type: Number, required: false, default: 7 @@ -115,6 +140,11 @@ export default { type: Number, required: false, default: 10 + }, + rangeSlider: { + type: Boolean, + required: false, + default: false } // limitMin: { // type: Number, @@ -132,15 +162,19 @@ export default { steps: null, stepsCount: null, radius: 0, - angle: 0, - currentStepValue: 0, + maxAngle: 0, + minAngle: 0, + currentMinStepValue: 0, + currentMaxStepValue: 0, mousePressed: false, mousemoveTicks: 0, - currentStepIndex: 0, + currentMinStepIndex: 0, + currentMaxStepIndex: 0, length: 0, sliderTolerance: 0, relativeX: 0, - relativeY: 0 + relativeY: 0, + currentKnob: '' } }, computed: { @@ -155,31 +189,37 @@ export default { cpCenter () { return this.side / 2 }, - cpAngle () { - return this.angle + Math.PI / 2 + cpMinAngle () { + return this.minAngle + Math.PI / 2 + }, + cpMaxAngle () { + return this.maxAngle + Math.PI / 2 }, cpMainCircleStrokeWidth () { return this.circleWidth || (this.side / 2) / this.circleWidthRel }, cpPathDirection () { - return (this.cpAngle < 3 / 2 * Math.PI) ? 0 : 1 + return (this.cpMaxAngle - (this.cpMinAngle - Math.PI / 2) < 3 / 2 * Math.PI) ? 0 : 1 }, cpPathX () { - return this.cpCenter + this.radius * Math.cos(this.cpAngle) + return this.cpCenter + this.radius * Math.cos(this.cpMaxAngle) }, cpPathY () { - return this.cpCenter + this.radius * Math.sin(this.cpAngle) + return this.cpCenter + this.radius * Math.sin(this.cpMaxAngle) }, cpPathStrokeWidth () { return this.progressWidth || (this.side / 2) / this.progressWidthRel }, - cpKnobRadius () { - return this.knobRadius || (this.side / 2) / this.knobRadiusRel + cpMinKnobRadius () { + return this.minKnobRadius || (this.side / 2) / this.minKnobRadiusRel + }, + cpMaxKnobRadius () { + return this.maxKnobRadius || (this.side / 2) / this.maxKnobRadiusRel }, cpPathD () { let parts = [] - parts.push('M' + this.cpCenter) - parts.push(this.cpCenter + this.radius) + parts.push('M' + this.cpMinKnobX) + parts.push(this.cpMinKnobY) parts.push('A') parts.push(this.radius) parts.push(this.radius) @@ -193,14 +233,23 @@ export default { cpAngleUnit () { return (Math.PI * 2 - this.startAngleOffset) / this.cpStepsLength }, - cpAngleValue () { + cpMinAngleValue () { + return (Math.min( + this.startAngleOffset + this.cpAngleUnit * this.currentMinStepIndex, + Math.PI * 2 - Number.EPSILON + )) // - 0.00001 // correct for 100% value + }, + cpMaxAngleValue () { return (Math.min( - this.startAngleOffset + this.cpAngleUnit * this.currentStepIndex, + this.startAngleOffset + this.cpAngleUnit * this.currentMaxStepIndex, Math.PI * 2 - Number.EPSILON )) - 0.00001 // correct for 100% value }, - cpCurrentStep () { - return this.steps[this.currentStepIndex] + cpCurrentMinStep () { + return this.steps[this.currentMinStepIndex] + }, + cpCurrentMaxStep () { + return this.steps[this.currentMaxStepIndex] }, cpSliderAngle () { return (Math.atan2(this.relativeY - this.cpCenter, this.relativeX - this.cpCenter) + Math.PI * 3 / 2) % (Math.PI * 2) @@ -208,6 +257,12 @@ export default { cpIsTouchWithinSliderRange () { const touchOffset = Math.sqrt(Math.pow(Math.abs(this.relativeX - this.cpCenter), 2) + Math.pow(Math.abs(this.relativeY - this.cpCenter), 2)) return Math.abs(touchOffset - this.radius) <= this.sliderTolerance + }, + cpMinKnobX () { + return this.cpCenter + this.radius * Math.cos(this.cpMinAngle) + }, + cpMinKnobY () { + return this.cpCenter + this.radius * Math.sin(this.cpMinAngle) } }, methods: { @@ -218,12 +273,22 @@ export default { this.setNewPosition(e) if (this.cpIsTouchWithinSliderRange) { const newAngle = this.cpSliderAngle - this.animateSlider(this.angle, newAngle) + this.defineCurrentKnob(newAngle) + + if (this.currentKnob === 'min') this.animateSlider(this.minAngle, newAngle) + else if (this.currentKnob === 'max') this.animateSlider(this.maxAngle, newAngle) } }, handleMouseDown (e) { e.preventDefault() this.mousePressed = true + + this.setNewPosition(e) + if (this.cpIsTouchWithinSliderRange) { + const newAngle = this.cpSliderAngle + this.defineCurrentKnob(newAngle) + } + window.addEventListener('mousemove', this.handleWindowMouseMove) window.addEventListener('mouseup', this.handleMouseUp) }, @@ -236,6 +301,8 @@ export default { }, handleWindowMouseMove (e) { e.preventDefault() + if (this.minAngle >= this.maxAngle) return + if (this.mousemoveTicks < 5) { this.mousemoveTicks++ return @@ -256,83 +323,146 @@ export default { if (this.cpIsTouchWithinSliderRange) { e.preventDefault() + const newAngle = this.cpSliderAngle + this.defineCurrentKnob(newAngle) this.updateSlider() } }, - updateAngle (angle, isAnimationFinished) { - this.updateCurrentStepFromAngle(angle) - this.angle = this.cpAngleValue - this.currentStepValue = this.cpCurrentStep + updateMinAngle (angle, isAnimationFinished) { + this.updateCurrentMinStepFromAngle(angle) + this.minAngle = this.cpMinAngleValue + this.currentMinStepValue = this.cpCurrentMinStep + if (isAnimationFinished) { - this.$emit('input', this.currentStepValue) + this.$emit('inputMin', this.currentMinStepValue) } }, - updateFromPropValue (value) { - let previousAngle = this.angle + updateMaxAngle (angle, isAnimationFinished) { + this.updateCurrentMaxStepFromAngle(angle) + this.maxAngle = this.cpMaxAngleValue + + this.currentMaxStepValue = this.cpCurrentMaxStep + + if (isAnimationFinished) { + this.$emit('input', this.currentMaxStepValue) + } + }, + updateFromPropMinValue (minValue) { + let previousAngle = this.minAngle + + let minStepValue = this.fitToStep(minValue) + this.updateCurrentMinStepFromValue(minStepValue) + + this.minAngle = this.cpMinAngleValue + this.currentMinStepValue = minStepValue + this.animateSlider(previousAngle, this.minAngle) + }, + updateFromPropMaxValue (maxValue) { + let previousAngle = this.maxAngle - let stepValue = this.fitToStep(value) - this.updateCurrentStepFromValue(stepValue) + let maxStepValue = this.fitToStep(maxValue) + this.updateCurrentMaxStepFromValue(maxStepValue) - this.angle = this.cpAngleValue - this.currentStepValue = stepValue - // this.$emit('input', this.currentStepValue) - this.animateSlider(previousAngle, this.angle) + this.maxAngle = this.cpMaxAngleValue + this.currentMaxStepValue = maxStepValue + this.animateSlider(previousAngle, this.maxAngle) }, updateSlider () { const angle = this.cpSliderAngle - if (Math.abs(angle - this.angle) < Math.PI) { - this.updateAngle(angle) - } + if ((this.currentKnob === 'max') && (Math.abs(angle - this.maxAngle) < Math.PI)) + this.updateMaxAngle(angle) + else if ((this.currentKnob === 'min') && (Math.abs(angle - this.minAngle) < Math.PI)) + this.updateMinAngle(angle) }, animateSlider (startAngle, endAngle) { const direction = startAngle < endAngle ? 1 : -1 const curveAngleMovementUnit = (direction * this.cpAngleUnit * 2) / this.stepSize - + const animate = () => { if (Math.abs(endAngle - startAngle) < Math.abs(2 * curveAngleMovementUnit)) { - this.updateAngle(endAngle, true) + if (this.currentKnob === 'max') this.updateMaxAngle(endAngle, true) + else if (this.currentKnob === 'min') this.updateMinAngle(endAngle, true) } else { const newAngle = startAngle + curveAngleMovementUnit - this.updateAngle(newAngle, false) + if (this.currentKnob === 'max') this.updateMaxAngle(newAngle, false) + else if (this.currentKnob === 'min') this.updateMinAngle(newAngle, false) this.animateSlider(newAngle, endAngle) } } - window.requestAnimationFrame(animate) }, defineInitialCurrentStepIndex () { for (let stepIndex in this.steps) { + if (this.steps[stepIndex] === this.minValue) { + this.currentMinStepIndex = stepIndex + } if (this.steps[stepIndex] === this.value) { - this.currentStepIndex = stepIndex - break + this.currentMaxStepIndex = stepIndex + // break } } }, - updateCurrentStepFromValue (value) { + updateCurrentMinStepFromValue (minValue) { for (let i = 0; i < this.cpStepsLength; i++) { - if (value <= this.steps[i]) { - this.currentStepIndex = i + if (minValue <= this.steps[i]) { + this.currentMinStepIndex = i return } } - - this.currentStepIndex = this.cpStepsLength }, - updateCurrentStepFromAngle (angle) { + updateCurrentMaxStepFromValue (maxValue) { + for (let i = 0; i < this.cpStepsLength; i++) { + if (maxValue <= this.steps[i]) { + this.currentMaxStepIndex = i + return + } + } + this.currentMaxStepIndex = this.cpStepsLength + }, + updateCurrentMinStepFromAngle (angle) { const stepIndex = Math.round((angle - this.startAngleOffset) / this.cpAngleUnit) - this.currentStepIndex = Math.min(Math.max(stepIndex, 0), this.cpStepsLength) + this.currentMinStepIndex = Math.min(Math.max(stepIndex, 0), this.cpStepsLength) + }, + updateCurrentMaxStepFromAngle (angle) { + const stepIndex = Math.round((angle - this.startAngleOffset) / this.cpAngleUnit) + this.currentMaxStepIndex = Math.min(Math.max(stepIndex, 0), this.cpStepsLength) }, setNewPosition (e) { const dimensions = this.containerElement.getBoundingClientRect() this.relativeX = e.clientX - dimensions.left this.relativeY = e.clientY - dimensions.top + }, + defineCurrentKnob (newAngle) { + if (!this.rangeSlider) { + this.currentKnob = 'max' + return + } + + if (newAngle > this.maxAngle) this.currentKnob = 'max' + else if (newAngle < this.minAngle) this.currentKnob = 'min' + else { + const offsetFromMax = Math.abs(this.maxAngle - newAngle) + const offsetFromMin = Math.abs(this.minAngle - newAngle) + this.currentKnob = offsetFromMax <= offsetFromMin ? 'max' : 'min' + } + }, + setDefaultMinValue () { + const defaultMinValue = this.currentMaxStepValue + this.updateFromPropMinValue(defaultMinValue) + this.$emit('inputMin', defaultMinValue) } }, watch: { value (val) { - if (val === this.currentStepValue) return - this.updateFromPropValue(val) + if (val === this.currentMaxStepValue) return + this.currentKnob = 'max' + this.updateFromPropMaxValue(val) + }, + minValue (val) { + if (!this.rangeSlider || val === this.currentMinStepValue) return + this.currentKnob = 'min' + this.currentMinStepIndex >= this.currentMaxStepIndex ? this.setDefaultMinValue() : this.updateFromPropMinValue(val) } } }