From 75fd7ed6c7ca635d39b2c303045d69b422419db6 Mon Sep 17 00:00:00 2001 From: jrojas Date: Fri, 10 May 2019 15:18:54 -0700 Subject: [PATCH 1/6] Allow radius to be dynamically configured. --- src/components/CircleSlider.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/CircleSlider.vue b/src/components/CircleSlider.vue index 7119c9d..1ec2996 100644 --- a/src/components/CircleSlider.vue +++ b/src/components/CircleSlider.vue @@ -29,10 +29,9 @@ export default { this.angle = this.circleSliderState.angleValue this.currentStepValue = this.circleSliderState.currentStep - let maxCurveWidth = Math.max(this.cpMainCircleStrokeWidth, this.cpPathStrokeWidth) - this.radius = (this.side / 2) - Math.max(maxCurveWidth, this.cpKnobRadius * 2) / 2 this.updateFromPropValue(this.value) }, + mounted () { this.touchPosition = new TouchPosition(this.$refs._svg, this.radius, this.radius / 2) }, @@ -130,7 +129,6 @@ export default { return { steps: null, stepsCount: null, - radius: 0, angle: 0, currentStepValue: 0, mousePressed: false, @@ -181,6 +179,11 @@ export default { parts.push(this.cpPathX) parts.push(this.cpPathY) return parts.join(' ') + }, + + radius() { + let maxCurveWidth = Math.max(this.cpMainCircleStrokeWidth, this.cpPathStrokeWidth) + return (this.side / 2) - Math.max(maxCurveWidth, this.cpKnobRadius * 2) / 2 } }, methods: { From a4263d2844bee85e1e5c26bd6a9ca39f4d0801cc Mon Sep 17 00:00:00 2001 From: jrojas Date: Sat, 11 May 2019 07:27:25 -0700 Subject: [PATCH 2/6] Adds two props to the component API: * arcLengthDegrees - the maximum arcLength of the slider in degrees (default: 360) * arcOffsetDegrees - the starting offset from the slider in degress (default: 90) This allows a developer to build semi circular components and components with a different starting position. --- src/components/CircleSlider.vue | 85 ++++++++++++++++++++---------- src/modules/circle_slider_state.js | 7 +-- src/modules/touch_position.js | 2 +- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/components/CircleSlider.vue b/src/components/CircleSlider.vue index 1ec2996..8aefc01 100644 --- a/src/components/CircleSlider.vue +++ b/src/components/CircleSlider.vue @@ -7,8 +7,8 @@ @mouseup="handleMouseUp" > - - + + @@ -25,7 +25,7 @@ export default { length: this.stepsCount }, (_, i) => this.min + i * this.stepSize) - this.circleSliderState = new CircleSliderState(this.steps, this.startAngleOffset, this.value) + this.circleSliderState = new CircleSliderState(this.steps, 0, this.value, this.arcLengthRadians) this.angle = this.circleSliderState.angleValue this.currentStepValue = this.circleSliderState.currentStep @@ -36,14 +36,6 @@ export default { this.touchPosition = new TouchPosition(this.$refs._svg, this.radius, this.radius / 2) }, props: { - startAngleOffset: { - type: Number, - required: false, - default: function () { - // return Math.PI / 20 - return 0 - } - }, value: { type: Number, required: false, @@ -113,6 +105,16 @@ export default { type: Number, required: false, default: 10 + }, + arcLengthDegrees: { + type: Number, + required: false, + default: 360 + }, + arcOffsetDegrees: { + type: Number, + required: false, + default: 0 } // limitMin: { // type: Number, @@ -146,13 +148,25 @@ export default { return this.side / 2 }, cpAngle () { - return this.angle + Math.PI / 2 + return this.angle + this.arcOffsetRadians }, cpMainCircleStrokeWidth () { return this.circleWidth || (this.side / 2) / this.circleWidthRel }, cpPathDirection () { - return (this.cpAngle < 3 / 2 * Math.PI) ? 0 : 1 + return (this.cpAngle < Math.PI + this.arcOffsetRadians) ? 0 : 1 + }, + cpStartX() { + return this.pathX(this.arcOffsetRadians) + }, + cpStartY() { + return this.pathY(this.arcOffsetRadians) + }, + cpEndX() { + return this.pathX((this.arcLengthRadians+this.arcOffsetRadians)*.99999) + }, + cpEndY() { + return this.pathY((this.arcLengthRadians+this.arcOffsetRadians)*.99999) }, cpPathX () { return this.cpCenter + this.radius * Math.cos(this.cpAngle) @@ -166,27 +180,42 @@ export default { cpKnobRadius () { return this.knobRadius || (this.side / 2) / this.knobRadiusRel }, - cpPathD () { + radius() { + let maxCurveWidth = Math.max(this.cpMainCircleStrokeWidth, this.cpPathStrokeWidth) + return (this.side / 2) - Math.max(maxCurveWidth, this.cpKnobRadius * 2) / 2 + }, + arcLengthRadians() { + return this.arcLengthDegrees * Math.PI * 2 / 360 + }, + arcOffsetRadians() { + return this.arcOffsetDegrees * Math.PI * 2 / 360 + } + }, + methods: { + + cpPathD (endX, endY, direction) { let parts = [] - parts.push('M' + this.cpCenter) - parts.push(this.cpCenter + this.radius) + parts.push('M' + this.cpStartX) + parts.push(this.cpStartY) parts.push('A') parts.push(this.radius) parts.push(this.radius) parts.push(0) - parts.push(this.cpPathDirection) + parts.push(direction) parts.push(1) - parts.push(this.cpPathX) - parts.push(this.cpPathY) + parts.push(endX) + parts.push(endY) return parts.join(' ') }, - radius() { - let maxCurveWidth = Math.max(this.cpMainCircleStrokeWidth, this.cpPathStrokeWidth) - return (this.side / 2) - Math.max(maxCurveWidth, this.cpKnobRadius * 2) / 2 - } - }, - methods: { + pathX (angle) { + return this.cpCenter + this.radius * Math.cos(angle) + }, + + pathY (angle) { + return this.cpCenter + this.radius * Math.sin(angle) + }, + /* */ fitToStep (val) { @@ -277,9 +306,9 @@ export default { /* */ updateSlider () { - const angle = this.touchPosition.sliderAngle - if (Math.abs(angle - this.angle) < Math.PI) { - this.updateAngle(angle) + const angle = (this.touchPosition.sliderAngle - this.arcOffsetRadians + Math.PI * 2) % (Math.PI * 2) + if (Math.abs(this.angle - angle) < Math.PI) { + this.updateAngle(Math.max( 0, Math.min(angle, this.arcLengthRadians))) } }, diff --git a/src/modules/circle_slider_state.js b/src/modules/circle_slider_state.js index 055cff7..06dc479 100644 --- a/src/modules/circle_slider_state.js +++ b/src/modules/circle_slider_state.js @@ -1,7 +1,7 @@ export default class CircleSliderState { /* */ - constructor (steps, offset, initialValue) { + constructor (steps, offset, initialValue, maxArcLength) { this.steps = steps this.offset = offset this.currentStepIndex = 0 @@ -14,12 +14,13 @@ export default class CircleSliderState { this.firstStep = this.steps[0] this.length = this.steps.length - 1 this.lastStep = this.steps[this.length] + this.maxArcLength = maxArcLength } /* */ get angleUnit () { - return (Math.PI * 2 - this.offset) / this.length + return (this.maxArcLength - this.offset) / this.length } /* @@ -27,7 +28,7 @@ export default class CircleSliderState { get angleValue () { return (Math.min( this.offset + this.angleUnit * this.currentStepIndex, - Math.PI * 2 - Number.EPSILON + this.maxArcLength - Number.EPSILON )) - 0.00001 // correct for 100% value } diff --git a/src/modules/touch_position.js b/src/modules/touch_position.js index 2004361..1ad7795 100644 --- a/src/modules/touch_position.js +++ b/src/modules/touch_position.js @@ -21,7 +21,7 @@ export default class TouchPosition { /* */ get sliderAngle () { - return (Math.atan2(this.relativeY - this.center, this.relativeX - this.center) + Math.PI * 3 / 2) % (Math.PI * 2) + return (Math.atan2(this.relativeY - this.center, this.relativeX - this.center) + 2 * Math.PI) % (Math.PI * 2) } /* From e13e76e9af2cd4038509cc756bf5139d85b05389 Mon Sep 17 00:00:00 2001 From: jrojas Date: Sat, 11 May 2019 07:33:37 -0700 Subject: [PATCH 3/6] Update README.md with new props --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7512843..3354d07 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # vue-circle-slider -[![npm](https://img.shields.io/npm/v/vue-circle-slider.svg) ![npm](https://img.shields.io/npm/dm/vue-circle-slider.svg)](https://www.npmjs.com/package/vue-circle-slider) [![vue2](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://vuejs.org/) +[![build](https://img.shields.io/wercker/ci/wercker/docs.svg)](https://github.com/devstark-com/vue-circle-slider) +[![npm](https://img.shields.io/npm/v/vue-circle-slider.svg) ![npm](https://img.shields.io/npm/dm/vue-circle-slider.svg)](https://www.npmjs.com/package/vue-circle-slider) +[![build](https://img.shields.io/npm/l/express.svg)](https://github.com/devstark-com/vue-circle-slider) Circle slider component for Vue.js @@ -86,6 +88,9 @@ or customize some properties: | circleWidthRel | Number | 20 | relative circle width. width value in px will be calculated as `(side/2) / circleWidthRel` | | progressWidth | Number | null | exact progress curve width in px | | progressWidthRel | Number | 10 | relative progress curve width. width value in px will be calculated as `(side/2) / progressWidthRel` | +| arcOffsetDegrees | Number | 90 | starting position offset in degrees | +| arcLengthDegrees | Number | 360 | maximum slider circumference in degrees | + ### Events From c3dbd02d7695c1b2f7593e3e0cc4308fea1f7144 Mon Sep 17 00:00:00 2001 From: jrojas Date: Sat, 11 May 2019 07:52:42 -0700 Subject: [PATCH 4/6] Update build --- dist/vue-circle-slider.browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/vue-circle-slider.browser.js b/dist/vue-circle-slider.browser.js index 74e1395..599282e 100644 --- a/dist/vue-circle-slider.browser.js +++ b/dist/vue-circle-slider.browser.js @@ -1 +1 @@ -!function(root,factory){"object"==typeof exports&&"object"==typeof module?module.exports=factory():"function"==typeof define&&define.amd?define([],factory):"object"==typeof exports?exports.VueCircleSlider=factory():root.VueCircleSlider=factory()}(this,function(){return function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.i=function(value){return value},__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{configurable:!1,enumerable:!0,get:getter})},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function(){return module.default}:function(){return module};return __webpack_require__.d(getter,"a",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=2)}([function(module,exports){var g,_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(obj){return typeof obj}:function(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};g=function(){return this}();try{g=g||Function("return this")()||(0,eval)("this")}catch(e){"object"===("undefined"==typeof window?"undefined":_typeof(window))&&(g=window)}module.exports=g},function(module,exports,__webpack_require__){var Component=__webpack_require__(6)(__webpack_require__(5),__webpack_require__(7),null,null);module.exports=Component.exports},function(module,__webpack_exports__,__webpack_require__){"use strict";Object.defineProperty(__webpack_exports__,"__esModule",{value:!0}),function(global){function install(Vue){Vue.component("circle-slider",__WEBPACK_IMPORTED_MODULE_0__components_CircleSlider_vue___default.a)}__webpack_exports__.install=install;var __WEBPACK_IMPORTED_MODULE_0__components_CircleSlider_vue__=__webpack_require__(1),__WEBPACK_IMPORTED_MODULE_0__components_CircleSlider_vue___default=__webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__components_CircleSlider_vue__);__webpack_require__.d(__webpack_exports__,"CircleSlider",function(){return __WEBPACK_IMPORTED_MODULE_0__components_CircleSlider_vue___default.a});var plugin={version:"1.0.1",install:install};__webpack_exports__.default=plugin;var GlobalVue=null;"undefined"!=typeof window?GlobalVue=window.Vue:void 0!==global&&(GlobalVue=global.Vue),GlobalVue&&GlobalVue.use(plugin)}.call(__webpack_exports__,__webpack_require__(0))},function(module,__webpack_exports__,__webpack_require__){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function defineProperties(target,props){for(var i=0;i1||e.changedTouches.length>1||e.touches.length>1)return!0;var lastTouch=e.targetTouches.item(e.targetTouches.length-1);this.touchPosition.setNewPosition(lastTouch),this.touchPosition.isTouchWithinSliderRange&&(e.preventDefault(),this.updateSlider())},updateAngle:function(angle){this.circleSliderState.updateCurrentStepFromAngle(angle),this.angle=this.circleSliderState.angleValue,this.currentStepValue=this.circleSliderState.currentStep,this.$emit("input",this.currentStepValue)},updateFromPropValue:function(value){var stepValue=this.fitToStep(value);this.circleSliderState.updateCurrentStepFromValue(stepValue),this.angle=this.circleSliderState.angleValue,this.currentStepValue=stepValue,this.$emit("input",this.currentStepValue)},updateSlider:function(){var angle=this.touchPosition.sliderAngle;Math.abs(angle-this.angle)1||e.changedTouches.length>1||e.touches.length>1)return!0;var lastTouch=e.targetTouches.item(e.targetTouches.length-1);this.touchPosition.setNewPosition(lastTouch),this.touchPosition.isTouchWithinSliderRange&&(e.preventDefault(),this.updateSlider())},updateAngle:function(angle){this.circleSliderState.updateCurrentStepFromAngle(angle),this.angle=this.circleSliderState.angleValue,this.currentStepValue=this.circleSliderState.currentStep,this.$emit("input",this.currentStepValue)},updateFromPropValue:function(value){var stepValue=this.fitToStep(value);this.circleSliderState.updateCurrentStepFromValue(stepValue),this.angle=this.circleSliderState.angleValue,this.currentStepValue=stepValue,this.$emit("input",this.currentStepValue)},updateSlider:function(){var angle=(this.touchPosition.sliderAngle-this.arcOffsetRadians+2*Math.PI)%(2*Math.PI);Math.abs(this.angle-angle) Date: Sat, 11 May 2019 11:03:35 -0700 Subject: [PATCH 5/6] Add support for origin --- README.md | 1 + src/components/CircleSlider.vue | 35 ++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3354d07..c14ed53 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ or customize some properties: | progressWidthRel | Number | 10 | relative progress curve width. width value in px will be calculated as `(side/2) / progressWidthRel` | | arcOffsetDegrees | Number | 90 | starting position offset in degrees | | arcLengthDegrees | Number | 360 | maximum slider circumference in degrees | +| origin | Number | null | progress value that represents the starting point for the progress bar (will default to min if null) | ### Events diff --git a/src/components/CircleSlider.vue b/src/components/CircleSlider.vue index 8aefc01..603d221 100644 --- a/src/components/CircleSlider.vue +++ b/src/components/CircleSlider.vue @@ -7,8 +7,8 @@ @mouseup="handleMouseUp" > - - + + @@ -29,6 +29,9 @@ export default { this.angle = this.circleSliderState.angleValue this.currentStepValue = this.circleSliderState.currentStep + this.originValue = this.origin === null ? this.min : this.origin + this.originValue = Math.min(this.max, Math.max(this.min, this.originValue)) + this.updateFromPropValue(this.value) }, @@ -115,6 +118,11 @@ export default { type: Number, required: false, default: 0 + }, + origin: { + type: Number, + required: false, + default: null } // limitMin: { // type: Number, @@ -129,6 +137,7 @@ export default { }, data () { return { + originValue: null, steps: null, stepsCount: null, angle: 0, @@ -153,9 +162,12 @@ export default { cpMainCircleStrokeWidth () { return this.circleWidth || (this.side / 2) / this.circleWidthRel }, - cpPathDirection () { + cpPathLongArc() { return (this.cpAngle < Math.PI + this.arcOffsetRadians) ? 0 : 1 }, + cpPathDirection() { + return (this.cpAngle < this.cpOriginRadians + this.arcOffsetRadians) ? 0 : 1 + }, cpStartX() { return this.pathX(this.arcOffsetRadians) }, @@ -168,6 +180,15 @@ export default { cpEndY() { return this.pathY((this.arcLengthRadians+this.arcOffsetRadians)*.99999) }, + cpOriginRadians() { + return this.circleSliderState.angleUnit * (this.originValue - this.min) + }, + cpOriginX() { + return this.pathX(this.arcOffsetRadians + this.cpOriginRadians) + }, + cpOriginY() { + return this.pathY(this.arcOffsetRadians + this.cpOriginRadians) + }, cpPathX () { return this.cpCenter + this.radius * Math.cos(this.cpAngle) }, @@ -193,16 +214,16 @@ export default { }, methods: { - cpPathD (endX, endY, direction) { + cpPathD (startX, startY, endX, endY, longArc, direction) { let parts = [] - parts.push('M' + this.cpStartX) - parts.push(this.cpStartY) + parts.push('M' + startX) + parts.push(startY) parts.push('A') parts.push(this.radius) parts.push(this.radius) parts.push(0) + parts.push(longArc) parts.push(direction) - parts.push(1) parts.push(endX) parts.push(endY) return parts.join(' ') From da8a131fb63314fad1f1cf828ffa0b2650eb6b84 Mon Sep 17 00:00:00 2001 From: jrojas Date: Wed, 15 May 2019 01:49:22 -0700 Subject: [PATCH 6/6] Fix click/touch events --- src/components/CircleSlider.vue | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/CircleSlider.vue b/src/components/CircleSlider.vue index 603d221..d95580f 100644 --- a/src/components/CircleSlider.vue +++ b/src/components/CircleSlider.vue @@ -36,8 +36,13 @@ export default { }, mounted () { - this.touchPosition = new TouchPosition(this.$refs._svg, this.radius, this.radius / 2) + this.createTouchPosition() }, + + updated() { + this.createTouchPosition() + }, + props: { value: { type: Number, @@ -214,6 +219,10 @@ export default { }, methods: { + createTouchPosition() { + this.touchPosition = new TouchPosition(this.$refs._svg, this.radius, this.radius / 2) + }, + cpPathD (startX, startY, endX, endY, longArc, direction) { let parts = [] parts.push('M' + startX) @@ -248,7 +257,7 @@ export default { handleClick (e) { this.touchPosition.setNewPosition(e) if (this.touchPosition.isTouchWithinSliderRange) { - const newAngle = this.touchPosition.sliderAngle + const newAngle = this.calculateAngle() this.animateSlider(this.angle, newAngle) } }, @@ -303,6 +312,11 @@ export default { } }, + calculateAngle() { + const angle = (this.touchPosition.sliderAngle - this.arcOffsetRadians + Math.PI * 2) % (Math.PI * 2) + return angle + }, + /* */ updateAngle (angle) { @@ -327,7 +341,7 @@ export default { /* */ updateSlider () { - const angle = (this.touchPosition.sliderAngle - this.arcOffsetRadians + Math.PI * 2) % (Math.PI * 2) + const angle = this.calculateAngle() if (Math.abs(this.angle - angle) < Math.PI) { this.updateAngle(Math.max( 0, Math.min(angle, this.arcLengthRadians))) }