├── .gitignore ├── .babelrc ├── postcss.config.js ├── src ├── util │ ├── dom-stylize.js │ ├── angle.js │ └── geometry.js ├── lib │ ├── config.js │ ├── canvas.js │ └── points.js ├── index.js └── scss │ └── styles.scss ├── webpack.config.js ├── package.json ├── README.md ├── index.html └── dist ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | .vscode -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread" 7 | ] 8 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | require('cssnano'), 5 | ] 6 | }; -------------------------------------------------------------------------------- /src/util/dom-stylize.js: -------------------------------------------------------------------------------- 1 | // add styles to a dom element 2 | function stylize(domElem, styles) { 3 | for(let prop in styles) { 4 | domElem.style[prop] = styles[prop]; 5 | } 6 | 7 | return domElem; 8 | } 9 | 10 | export { stylize }; -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | radius: 200, 3 | angle: 120, 4 | pointSize: 24, 5 | points: [ 6 | {id: 'POINT_ID_1', label: 'Point label 1'}, 7 | {id: 'POINT_ID_2', label: 'Point label 2'}, 8 | {id: 'POINT_ID_3', label: 'Point label 3'}, 9 | {id: 'POINT_ID_4', label: 'Point label 4'}, 10 | {id: 'POINT_ID_5', label: 'Point label 5'}, 11 | ], 12 | } -------------------------------------------------------------------------------- /src/util/angle.js: -------------------------------------------------------------------------------- 1 | // get sin of an angle 2 | function getSin( angle ) { 3 | if (angle < 90) { 4 | return Math.sin(angle * (Math.PI / 180)); 5 | } else { 6 | return 1; 7 | } 8 | } 9 | 10 | // get cos of an angle 11 | function getCos( angle ) { 12 | if (angle < 90) { 13 | return Math.cos(angle * (Math.PI / 180)); 14 | } else { 15 | return 0; 16 | } 17 | } 18 | 19 | export { getSin, getCos }; -------------------------------------------------------------------------------- /src/util/geometry.js: -------------------------------------------------------------------------------- 1 | import { getSin, getCos } from './angle'; 2 | 3 | // get minimum width of div to fit points 4 | // r - (cos(angle/2) * r) 5 | function getWidth(radius, angle) { 6 | var cos = getCos(angle / 2); 7 | return Math.round(radius - (cos * radius)); 8 | } 9 | 10 | // get minimum height of div to fit points 11 | // r * sin(angle/2) * 2 12 | function getHeight(radius, angle) { 13 | var sin = getSin(angle / 2); 14 | return Math.round(radius * sin * 2); 15 | } 16 | 17 | // get canvas width and height 18 | // canvas: area in which navigation menu will be rendered 19 | function getCanvasSize(radius, angle) { 20 | var width = getWidth(radius, angle); 21 | var height = getHeight(radius, angle); 22 | 23 | return { 24 | width, 25 | height 26 | }; 27 | } 28 | 29 | export { getWidth, getCos, getCanvasSize }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 4 | const uglifyJsPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: './src/index.js', 8 | output: { 9 | library: 'CurvedMenu', 10 | libraryTarget: 'umd', 11 | libraryExport: 'default', 12 | path: path.resolve(__dirname, 'dist'), 13 | filename: 'index.js' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | exclude: /node_modules/, 20 | use: ['babel-loader'] 21 | }, 22 | { 23 | test: /\.scss$/, 24 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new uglifyJsPlugin(), 30 | new HTMLWebpackPlugin({ 31 | template: path.resolve(__dirname, 'index.html') 32 | }), 33 | new webpack.HotModuleReplacementPlugin(), 34 | ] 35 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "curved-menu", 3 | "version": "1.0.9", 4 | "description": "VanillaJS curved navigation menu (circular navigation)", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "start": "webpack-dev-server" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/thatisuday/curved-menu.git" 14 | }, 15 | "keywords": [ 16 | "menu", 17 | "html", 18 | "js", 19 | "vanillajs", 20 | "navigation", 21 | "menubar" 22 | ], 23 | "author": "Uday Hiwarale ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/thatisuday/curved-menu/issues" 27 | }, 28 | "homepage": "https://github.com/thatisuday/curved-menu#readme", 29 | "devDependencies": { 30 | "autoprefixer": "^8.5.0", 31 | "babel-core": "^6.26.3", 32 | "babel-loader": "^7.1.4", 33 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 34 | "babel-preset-env": "^1.7.0", 35 | "css-loader": "^0.28.11", 36 | "cssnano": "^3.10.0", 37 | "html-webpack-plugin": "^3.2.0", 38 | "node-sass": "^4.9.0", 39 | "postcss-loader": "^2.1.5", 40 | "sass-loader": "^7.0.1", 41 | "style-loader": "^0.21.0", 42 | "uglifyjs-webpack-plugin": "^1.2.5", 43 | "webpack": "^3.12.0", 44 | "webpack-dev-server": "^2.11.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { getCanvas } from './lib/canvas'; 2 | import { getPointElements } from './lib/points'; 3 | import defaultConfig from './lib/config'; 4 | import './scss/styles.scss'; 5 | 6 | // CurvedMenu plugin constructor class 7 | export default class CurvedMenu { 8 | constructor(rootElem, config) { 9 | this.initialized = false; 10 | this.rootElem = rootElem; 11 | this.config = Object.assign(defaultConfig, config); 12 | } 13 | 14 | // initialize curved menu 15 | init() { 16 | // destroy previous instance, if exists 17 | if( this.initialized ) { 18 | this.destroy(); 19 | } 20 | 21 | // create canvas element from root element 22 | this.canvasElement = getCanvas({rootElem: this.rootElem, ...this.config}); 23 | 24 | // create DOM point elements 25 | this.pointElements = getPointElements(this.config, { 26 | click: ( pointElem ) => { 27 | this.setActivePoint( pointElem.getAttribute('point-id') ); 28 | } 29 | }); 30 | 31 | // append ppints to canvas element 32 | this.canvasElement = this.canvasElement.append(...this.pointElements); 33 | 34 | // notify init event 35 | if( this.config.onInit && ( typeof this.config.onInit == 'function' ) ) { 36 | this.config.onInit(); 37 | } 38 | 39 | // set initialized value to true 40 | this.initialized = true; 41 | } 42 | 43 | // set active point ( class ) 44 | setActivePoint( selectedPointId ) { 45 | // toggle active class 46 | for( let pointElem of this.pointElements ) { 47 | let pointId = pointElem.getAttribute('point-id'); 48 | 49 | if( selectedPointId == pointId ) { 50 | this.activePoint = selectedPointId; 51 | pointElem.classList.add('curved-menu__point--active'); 52 | } 53 | else { 54 | pointElem.classList.remove('curved-menu__point--active'); 55 | } 56 | } 57 | 58 | // dispatch on click callback 59 | if( this.config.onClick && ( typeof this.config.onClick == 'function' ) ) { 60 | this.config.onClick( selectedPointId ); 61 | } 62 | } 63 | 64 | // destroy component 65 | destroy() { 66 | this.canvasElement = null; 67 | this.pointElements = null; 68 | this.activePoint = null; 69 | 70 | // reset root element container 71 | this.rootElem.style = ''; 72 | this.rootElem.innerHTML = ''; 73 | 74 | // unset initialized value to true 75 | this.initialized = false; 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/lib/canvas.js: -------------------------------------------------------------------------------- 1 | // API related to canvas 2 | // Canvas is area in which curved menu points (navigation elements) will be rendered 3 | 4 | import { getCanvasSize } from '../util/geometry'; 5 | import { stylize } from '../util/dom-stylize'; 6 | 7 | // Stylize (css) canvas area 8 | function stylizeCanvas({canvasElem, radius, angle, pointSize}) { 9 | // get recommended size of canvas element 10 | let { width, height } = getCanvasSize(radius, angle); 11 | 12 | // add reference class 13 | canvasElem.classList.add('curved-menu'); 14 | 15 | // return canvas element 16 | return stylize(canvasElem, { 17 | width: width + 'px', 18 | height: height + 'px', 19 | marginTop: pointSize + 'px', 20 | marginLeft: pointSize + 'px', 21 | }); 22 | } 23 | 24 | // Get curve path DOM element 25 | // Curve path element is dashed (or solid) path on which menu points will be placed 26 | function getPathElement({radius, angle}) { 27 | let pathContainerElem = document.createElement('div'); 28 | let pathElem = document.createElement('div'); 29 | 30 | // add reference classes 31 | pathContainerElem.classList.add('curved-menu__curve-container'); 32 | pathElem.classList.add('curved-menu__curve-container__curve'); 33 | 34 | // stylize path element 35 | stylize(pathElem, { 36 | width: (radius * 2) + 'px', 37 | height: (radius * 2) + 'px', 38 | }); 39 | 40 | // append path DOM element to path container 41 | pathContainerElem.appendChild(pathElem); 42 | 43 | return pathContainerElem; 44 | } 45 | 46 | // get svg node 47 | function _getNode(tag, config) { 48 | tag = document.createElementNS('http://www.w3.org/2000/svg', tag); 49 | 50 | for (let prop in config){ 51 | tag.setAttributeNS(null, prop, config[prop]); 52 | } 53 | 54 | return tag; 55 | } 56 | 57 | // get svg filter element 58 | function getSVGElement() { 59 | let svgElement = document.createElement('svg'); 60 | svgElement.setAttribute('version', '1.1'); 61 | 62 | let defs = svgElement.appendChild( _getNode('defs') ); 63 | let filter = defs.appendChild( _getNode('filter', { id: 'curved-menu-goo-effect' }) ); 64 | filter.appendChild( _getNode('feGaussianBlur', {in: 'SourceGraphic', stdDeviation: '3', result: 'blurred'}) ); 65 | filter.appendChild( _getNode('feColorMatrix', {in: 'blurred', mode: 'matrix', values:'1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -5', result: 'goo'}) ); 66 | filter.appendChild( _getNode('feBlend', {in: 'SourceGraphic', in2: 'goo'}) ); 67 | 68 | return svgElement; 69 | } 70 | 71 | // get canvas element 72 | // stylize canavs and add path element 73 | function getCanvas({rootElem, radius, angle, pointSize}) { 74 | // empty root element 75 | rootElem.innerHTML = ''; 76 | 77 | // stylize canvas 78 | stylizeCanvas({canvasElem: rootElem, radius, angle, pointSize}); 79 | 80 | // get path element 81 | let pathElement = getPathElement({radius, angle}); 82 | 83 | // get SVG element 84 | let svgElement = getSVGElement(); 85 | 86 | // append path and svg element to root element 87 | rootElem.appendChild(pathElement); 88 | rootElem.appendChild(svgElement); 89 | 90 | return rootElem; 91 | } 92 | 93 | export { getCanvas, stylizeCanvas, getPathElement }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Curved Menu 2 | VanillaJS Curved Menu (circular navigation) with radius and angle control. 3 | 4 | [![npm](https://img.shields.io/npm/dt/curved-menu.svg?style=flat-square)](https://www.npmjs.com/package/curved-menu) 5 | [![npm](https://img.shields.io/npm/v/curved-menu.svg?style=flat-square)](https://www.npmjs.com/package/curved-menu) 6 | [![David](https://img.shields.io/david/thatisuday/curved-menu.svg?style=flat-square)](https://www.npmjs.com/package/curved-menu) 7 | 8 | [![](https://i.imgur.com/QbA2Xvn.png)](https://rawgit.com/thatisuday/curved-menu/master/dist/index.html) 9 | 10 | # Preview 11 | [click here](https://rawgit.com/thatisuday/curved-menu/master/dist/index.html) 12 | 13 | # Install 14 | ```js 15 | npm install --save curved-menu 16 | 17 | import CurveMenu from 'curved-menu'; 18 | ``` 19 | 20 | > or use `index.js` file from `dist` folder. `CurveMenu` will be available on `window`. 21 | 22 | # Use 23 | ```html 24 | 30 | ``` 31 | 32 | ```js 33 | var radius = 300; // radius of circle in px 34 | var angle = 90; // span angle of points on circle (angle between first and last point) 35 | var pointSize = 25; // size of points in px 36 | 37 | // point elements (bullet/buttons) 38 | var points = [ 39 | { id: 'POINT_ID_1', label: 'Point label 1' }, 40 | { id: 'POINT_ID_2', label: 'Point label 2' }, 41 | { id: 'POINT_ID_3', label: 'Point label 3' }, 42 | { id: 'POINT_ID_4', label: 'Point label 4' }, 43 | { id: 'POINT_ID_5', label: 'Point label 5' }, 44 | ]; 45 | 46 | // log notification (demo purpose only) 47 | function insertNotification(message) { 48 | var notifier = document.getElementById('notifier'); 49 | 50 | var notif = document.createElement('p'); 51 | notif.textContent = message; 52 | 53 | notifier.appendChild(notif); 54 | } 55 | 56 | // once DOM is ready 57 | window.addEventListener('DOMContentLoaded', function() { 58 | // DOM element for curve menu 59 | var navElem = document.getElementById('nav'); 60 | 61 | // create curve menu instance 62 | var instance = new CurvedMenu(navElem, { 63 | radius: radius, 64 | angle: angle, 65 | pointSize: pointSize, 66 | points: points, 67 | onInit: function( ) { 68 | insertNotification( 'Curved menu initialized!' ); 69 | console.log( 'Curved menu initialized!' ); 70 | }, 71 | onClick: function( id ) { 72 | insertNotification( 'Clicked point id: ' + id ); 73 | console.log( 'Selected point id: ', id ); 74 | } 75 | }); 76 | 77 | // initialize curve menu instance at your will 78 | document.getElementById('button').addEventListener('click', function(){ 79 | if(instance) { 80 | // initialize 81 | instance.init(); 82 | 83 | // set active point using `id` 84 | setTimeout(function() { 85 | instance.setActivePoint('POINT_ID_2'); 86 | }, 2000); 87 | 88 | // destroy instance 89 | setTimeout(function() { 90 | // destory 91 | instance.destroy(); 92 | 93 | setTimeout(function(){ 94 | // re-initialize 95 | instance.init(); 96 | }, 3000); 97 | }, 6000); 98 | } 99 | }); 100 | }); 101 | ``` 102 | 103 | # CSS classes 104 | you can override style using these classes 105 | 106 | - .curved-menu 107 | - .curved-menu__curve-container 108 | - .curved-menu__curve-container__curve 109 | - .curved-menu__point 110 | - .curved-menu__point__bullet 111 | - .curved-menu__point__label 112 | 113 | # Need more development 114 | This plugin was developed just to test circular geometry and SVG filters. It needs more functionality to dynamically add/remove points and add extra configurations. So, any help is appreciated. 115 | -------------------------------------------------------------------------------- /src/lib/points.js: -------------------------------------------------------------------------------- 1 | // API related to menu points (navigation elements) 2 | 3 | import { getSin, getCos } from '../util/angle'; 4 | import { getCanvasSize } from '../util/geometry'; 5 | import { stylize } from '../util/dom-stylize'; 6 | 7 | // set point element position relative to canvas 8 | function _setPointElemPosition({ radius, angle, elements, totalElements, atBottom }) { 9 | let { width, height } = getCanvasSize(radius, angle); 10 | 11 | // current angle of first point element to render at from x-axis 12 | let currentAngle = (angle / 2); 13 | 14 | // angle between two adjacent point 15 | let separationAngle = (angle / 2) / elements.length; 16 | 17 | for (let index in elements) { 18 | let top = (height / 2) - (getSin(currentAngle) * radius); 19 | let left = (getCos(currentAngle) * radius) - (radius - width); 20 | 21 | // if bottom points, position vertically relative to bottom of the canvas 22 | if (atBottom) { 23 | top = height - top; 24 | } 25 | 26 | // stylize pointElement 27 | stylize(elements[index], { 28 | top : Math.round(top) + 'px', 29 | left : Math.round(left) + 'px', 30 | }); 31 | 32 | // if even total point elements, add extra angle of separation 33 | // to compensate lack of middle point 34 | if ((totalElements % 2 == 0)) { 35 | currentAngle -= (separationAngle + (separationAngle / (totalElements - 1))); 36 | } 37 | else { 38 | currentAngle -= separationAngle; 39 | } 40 | } 41 | 42 | return elements; 43 | } 44 | 45 | // Get list of DOM point elements 46 | function getPointElements({ radius, angle, points = [{id: 'POINT_ID_1', label: 'provide some points'}], pointSize }, actionHandlers) { 47 | let { width, height } = getCanvasSize(radius, angle); 48 | 49 | let pointsCount = points.length; 50 | 51 | // make list of DOM point elements 52 | let pointElements = points.map(({ id, label }) => { 53 | let pointElem = document.createElement('div'); 54 | let pointElemBullet = document.createElement('div'); 55 | let pointElemLabel = document.createElement('div'); 56 | 57 | // add point attribute to point label 58 | pointElem.setAttribute('point-id', id); 59 | 60 | // add text inside point element label 61 | pointElemLabel.innerText = label; 62 | 63 | // insert point bullet and label inside point element 64 | pointElem.appendChild(pointElemBullet); 65 | pointElem.appendChild(pointElemLabel); 66 | 67 | // style point element 68 | stylize(pointElem, { 69 | top: (height / 2) + 'px', 70 | left: width + 'px', 71 | marginLeft: ( -pointSize / 2 ) - 5 + 'px', // 5px extra because of css padding 72 | marginTop: ( -pointSize / 2 ) - 5 + 'px', // 5px extra because of css padding 73 | }); 74 | 75 | // style point bullet element 76 | stylize(pointElemBullet, { 77 | width: pointSize + 'px', 78 | height: pointSize + 'px' 79 | }); 80 | 81 | // add reference classes 82 | pointElem.classList.add('curved-menu__point'); 83 | pointElemBullet.classList.add('curved-menu__point__bullet'); 84 | pointElemLabel.classList.add('curved-menu__point__label'); 85 | 86 | // attach click event handler 87 | pointElem.addEventListener('click', function( event ) { 88 | actionHandlers.click( event.currentTarget ); 89 | }); 90 | 91 | return pointElem; 92 | }); 93 | 94 | // split point elements into three vertical zones 95 | let topElements = []; 96 | let middleElements = []; 97 | let bottomElements = []; 98 | 99 | if (pointsCount > 1 && (pointsCount % 2) == 0) { 100 | topElements = pointElements.slice(0, pointsCount / 2); 101 | bottomElements = pointElements.slice(pointsCount / 2); 102 | } else if (pointsCount > 1 && pointsCount % 2 != 0) { 103 | topElements = pointElements.slice(0, pointsCount / 2); 104 | middleElements = pointElements.slice((pointsCount / 2), ((pointsCount / 2) + 1)); 105 | bottomElements = pointElements.slice(((pointsCount / 2) + 1)); 106 | } else { 107 | topElements = pointElements; 108 | } 109 | 110 | // set margin to point elements to render on circumference 111 | if (pointsCount > 1) { 112 | //radius, angle, elements, totalElements, atBottom 113 | _setPointElemPosition({ 114 | radius, 115 | angle, 116 | elements: topElements, 117 | totalElements: pointsCount, 118 | atBottom: false 119 | }); 120 | 121 | _setPointElemPosition({ 122 | radius, 123 | angle, 124 | elements: bottomElements.reverse(), 125 | totalElements: pointsCount, 126 | atBottom: true 127 | }); 128 | } 129 | 130 | // return point DOM elements 131 | return [...topElements, ...middleElements, ...bottomElements.reverse()]; 132 | } 133 | 134 | export { getPointElements }; -------------------------------------------------------------------------------- /src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | $inactive-gradient: linear-gradient(to right, #e95b75 0%, #fd868c 100%); 2 | $inactive-color: #fd868c; 3 | 4 | $active-gradient: linear-gradient(to right, #00BCD4 0%, #00d499 100%); 5 | $active-color: #00d499; 6 | 7 | .curved-menu{ 8 | position: relative; 9 | z-index: 100; 10 | display: inline-block; 11 | 12 | &__curve-container{ 13 | position : absolute; 14 | z-index: 1; 15 | width : 100%; 16 | height : 100%; 17 | overflow : hidden; 18 | box-sizing : border-box; 19 | left : 0; 20 | top : 0; 21 | 22 | &__curve{ 23 | border-radius: 50%; 24 | box-sizing: border-box; 25 | position: absolute; 26 | z-index: 2; 27 | right: 0; 28 | top: 50%; 29 | transform: translateY(-50%); 30 | transition: all 0.3s ease; 31 | overflow: hidden; 32 | background-image: linear-gradient(to top, #dad4ec 0%, #dad4ec 1%, #f3e7e9 100%); 33 | 34 | &:after{ 35 | content: ""; 36 | display: block; 37 | position: absolute; 38 | z-index: 1; 39 | background-color: #fff; 40 | left: 2px; 41 | top: 2px; 42 | right: 2px; 43 | bottom: 2px; 44 | border-radius: 50%; 45 | } 46 | } 47 | } 48 | 49 | &__point{ 50 | position: absolute; 51 | z-index: 2; 52 | cursor: pointer; 53 | display: flex; 54 | align-items: center; 55 | padding: 5px; 56 | filter: url('#curved-menu-goo-effect'); 57 | 58 | &__bullet{ 59 | position: relative; 60 | flex: 0 0 auto; 61 | box-sizing: border-box; 62 | z-index: 2; 63 | transform: scale(1); 64 | transition: all 300ms ease; 65 | 66 | &:before{ 67 | content: ""; 68 | display: block; 69 | position: absolute; 70 | z-index: 0; 71 | left: 0; 72 | top: 0; 73 | right: 0; 74 | bottom: 0; 75 | background-image: $inactive-gradient; 76 | border-radius: 50%; 77 | } 78 | 79 | &:after{ 80 | content: ""; 81 | display: block; 82 | position: absolute; 83 | z-index: 0; 84 | left: 5px; 85 | top: 5px; 86 | right: 5px; 87 | bottom: 5px; 88 | border-radius: 50% !important; 89 | background-color: #fff; 90 | transition: all 300ms ease; 91 | } 92 | } 93 | 94 | &__label{ 95 | position: relative; 96 | flex: 0 0 auto; 97 | padding: 2px 15px 3px; 98 | color: #fff; 99 | background-color: $inactive-color; 100 | //box-shadow: 1px 1px 0 2px #fff, 2px 2px 5px $inactive-color; 101 | box-sizing: border-box; 102 | border-radius: 30px; 103 | font-size: 12px; 104 | text-transform: uppercase; 105 | font-family: sans-serif; 106 | white-space: nowrap; 107 | z-index: 1; 108 | opacity: 0; 109 | transform: translateX(-10px); 110 | transition: opacity 300ms ease, transform 300ms ease; 111 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 112 | font-weight: 100; 113 | 114 | &:before{ 115 | content: ""; 116 | display: block; 117 | position: absolute; 118 | width: 0px; 119 | height: 0px; 120 | left: -3px; 121 | top: 50%; 122 | transform: translateY(-50%); 123 | border-top: 5px solid transparent; 124 | border-bottom: 5px solid transparent; 125 | border-right: 5px solid $inactive-color; 126 | } 127 | } 128 | 129 | &:hover &{ 130 | &__bullet{ 131 | transform: scale(1.1); 132 | 133 | &:after{ 134 | transform: scale(0.6); 135 | background-color: #eee; 136 | } 137 | } 138 | 139 | &__label{ 140 | transform: translateX(5px); 141 | opacity: 1; 142 | } 143 | } 144 | 145 | &--active &{ 146 | &__bullet{ 147 | &:before{ 148 | background-image: $active-gradient; 149 | } 150 | } 151 | 152 | &__label{ 153 | background-color: $active-color; 154 | //box-shadow: 1px 1px 0 2px #fff, 2px 2px 5px $active-color; 155 | 156 | &:before{ 157 | border-right: 5px solid $active-color; 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Curved Menu 7 | 8 | 65 | 66 | 67 | 68 |

69 | Curved Menu JS Plugin 70 |

71 | 72 |
73 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |
90 | 91 | 178 | 179 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Curved Menu 7 | 8 | 65 | 66 | 67 | 68 |

69 | Curved Menu JS Plugin 70 |

71 | 72 |
73 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |
90 | 91 | 178 | 179 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CurvedMenu=t():e.CurvedMenu=t()}("undefined"!=typeof self?self:this,function(){return function(e){var t=window.webpackHotUpdateCurvedMenu;window.webpackHotUpdateCurvedMenu=function(e,n){!function(e,t){if(!_[e]||!y[e])return;for(var n in y[e]=!1,t)Object.prototype.hasOwnProperty.call(t,n)&&(h[n]=t[n]);0==--m&&0===b&&O()}(e,n),t&&t(e,n)};var n,r=!0,o="65d7011573cce59e927f",i=1e4,a={},s=[],c=[];function l(e){var t=k[e];if(!t)return C;var r=function(r){return t.hot.active?(k[r]?k[r].parents.indexOf(e)<0&&k[r].parents.push(e):(s=[e],n=r),t.children.indexOf(r)<0&&t.children.push(r)):(console.warn("[HMR] unexpected require("+r+") from disposed module "+e),s=[]),C(r)},o=function(e){return{configurable:!0,enumerable:!0,get:function(){return C[e]},set:function(t){C[e]=t}}};for(var i in C)Object.prototype.hasOwnProperty.call(C,i)&&"e"!==i&&Object.defineProperty(r,i,o(i));return r.e=function(e){return"ready"===d&&f("prepare"),b++,C.e(e).then(t,function(e){throw t(),e});function t(){b--,"prepare"===d&&(g[e]||E(e),0===b&&0===m&&O())}},r}var u=[],d="idle";function f(e){d=e;for(var t=0;t0;){var o=r.pop(),i=o.id,a=o.chain;if((c=k[i])&&!c.hot._selfAccepted){if(c.hot._selfDeclined)return{type:"self-declined",chain:a,moduleId:i};if(c.hot._main)return{type:"unaccepted",chain:a,moduleId:i};for(var s=0;s=0||(u.hot._acceptedDependencies[i]?(n[l]||(n[l]=[]),p(n[l],[i])):(delete n[l],t.push(l),r.push({chain:a.concat([l]),id:l})))}}}}return{type:"accepted",moduleId:e,outdatedModules:t,outdatedDependencies:n}}function p(e,t){for(var n=0;n ")),E.type){case"self-declined":t.onDeclined&&t.onDeclined(E),t.ignoreDeclined||(O=new Error("Aborted because of self decline: "+E.moduleId+I));break;case"declined":t.onDeclined&&t.onDeclined(E),t.ignoreDeclined||(O=new Error("Aborted because of declined dependency: "+E.moduleId+" in "+E.parentId+I));break;case"unaccepted":t.onUnaccepted&&t.onUnaccepted(E),t.ignoreUnaccepted||(O=new Error("Aborted because "+l+" is not accepted"+I));break;case"accepted":t.onAccepted&&t.onAccepted(E),j=!0;break;case"disposed":t.onDisposed&&t.onDisposed(E),P=!0;break;default:throw new Error("Unexception type "+E.type)}if(O)return f("abort"),Promise.reject(O);if(j)for(l in g[l]=h[l],p(b,E.outdatedModules),E.outdatedDependencies)Object.prototype.hasOwnProperty.call(E.outdatedDependencies,l)&&(m[l]||(m[l]=[]),p(m[l],E.outdatedDependencies[l]));P&&(p(b,[E.moduleId]),g[l]=y)}var M,A=[];for(r=0;r0;)if(l=D.pop(),c=k[l]){var L={},U=c.hot._disposeHandlers;for(i=0;i=0&&T.parents.splice(M,1))}}for(l in m)if(Object.prototype.hasOwnProperty.call(m,l)&&(c=k[l]))for(z=m[l],i=0;i=0&&c.children.splice(M,1);for(l in f("apply"),o=v,g)Object.prototype.hasOwnProperty.call(g,l)&&(e[l]=g[l]);var H=null;for(l in m)if(Object.prototype.hasOwnProperty.call(m,l)&&(c=k[l])){z=m[l];var R=[];for(r=0;r=0)continue;R.push(n)}for(r=0;r=0&&t._disposeHandlers.splice(n,1)},check:w,apply:j,status:function(e){if(!e)return d;u.push(e)},addStatusHandler:function(e){u.push(e)},removeStatusHandler:function(e){var t=u.indexOf(e);t>=0&&u.splice(t,1)},data:a[e]};return n=void 0,t}(t),parents:(c=s,s=[],c),children:[]};return e[t].call(r.exports,r,r.exports,l(t)),r.l=!0,r.exports}return C.m=e,C.c=k,C.d=function(e,t,n){C.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},C.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return C.d(t,"a",t),t},C.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},C.p="",C.h=function(){return o},l(4)(C.s=4)}([function(e,t,n){(e.exports=n(9)(!1)).push([e.i,'.curved-menu{position:relative;z-index:3;display:inline-block}.curved-menu__curve-container{position:absolute;z-index:1;width:100%;height:100%;overflow:hidden;box-sizing:border-box;left:0;top:0}.curved-menu__curve-container__curve{border-radius:50%;box-sizing:border-box;position:absolute;z-index:2;right:0;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);transition:all .3s ease;overflow:hidden;background-image:linear-gradient(0deg,#dad4ec 0,#dad4ec 1%,#f3e7e9)}.curved-menu__curve-container__curve:after{content:"";display:block;position:absolute;z-index:1;background-color:#fff;left:2px;top:2px;right:2px;bottom:2px;border-radius:50%}.curved-menu__point{position:absolute;z-index:2;cursor:pointer;display:flex;align-items:center;padding:5px;-webkit-filter:url(#curved-menu-goo-effect);filter:url(#curved-menu-goo-effect)}.curved-menu__point__bullet{position:relative;flex:0 0 auto;box-sizing:border-box;z-index:2;-webkit-transform:scale(1);transform:scale(1);transition:all .3s ease}.curved-menu__point__bullet:before{content:"";display:block;position:absolute;z-index:0;left:0;top:0;right:0;bottom:0;background-image:linear-gradient(90deg,#e95b75 0,#fd868c);border-radius:50%}.curved-menu__point__bullet:after{content:"";display:block;position:absolute;z-index:0;left:5px;top:5px;right:5px;bottom:5px;border-radius:50%!important;background-color:#fff;transition:all .3s ease}.curved-menu__point__label{position:relative;flex:0 0 auto;padding:2px 15px 3px;color:#fff;background-color:#fd868c;box-sizing:border-box;border-radius:30px;font-size:12px;text-transform:uppercase;font-family:sans-serif;white-space:nowrap;z-index:1;opacity:0;-webkit-transform:translateX(-10px);transform:translateX(-10px);transition:opacity .3s ease,-webkit-transform .3s ease;transition:opacity .3s ease,transform .3s ease;transition:opacity .3s ease,transform .3s ease,-webkit-transform .3s ease;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-weight:100}.curved-menu__point__label:before{content:"";display:block;position:absolute;width:0;height:0;left:-3px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #fd868c}.curved-menu__point:hover .curved-menu__point__bullet{-webkit-transform:scale(1.1);transform:scale(1.1)}.curved-menu__point:hover .curved-menu__point__bullet:after{-webkit-transform:scale(.6);transform:scale(.6);background-color:#eee}.curved-menu__point:hover .curved-menu__point__label{-webkit-transform:translateX(5px);transform:translateX(5px);opacity:1}.curved-menu__point--active .curved-menu__point__bullet:before{background-image:linear-gradient(90deg,#00bcd4 0,#00d499)}.curved-menu__point--active .curved-menu__point__label{background-color:#00d499}.curved-menu__point--active .curved-menu__point__label:before{border-right:5px solid #00d499}',""])},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getCanvasSize=t.getCos=t.getWidth=void 0;var r=n(2);function o(e,t){var n=(0,r.getCos)(t/2);return Math.round(e-n*e)}t.getWidth=o,t.getCos=r.getCos,t.getCanvasSize=function(e,t){return{width:o(e,t),height:function(e,t){var n=(0,r.getSin)(t/2);return Math.round(e*n*2)}(e,t)}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getSin=function(e){return e<90?Math.sin(e*(Math.PI/180)):1},t.getCos=function(e){return e<90?Math.cos(e*(Math.PI/180)):0}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.stylize=function(e,t){for(var n in t)e.style[n]=t[n];return e}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r,o=Object.assign||function(e){for(var t=1;t1&&h%2==0?(m=v.slice(0,h/2),g=v.slice(h/2)):h>1&&h%2!=0?(m=v.slice(0,h/2),b=v.slice(h/2,h/2+1),g=v.slice(h/2+1)):m=v,h>1&&(s({radius:n,angle:r,elements:m,totalElements:h,atBottom:!1}),s({radius:n,angle:r,elements:g.reverse(),totalElements:h,atBottom:!0})),[].concat(a(m),a(b),a(g.reverse()))}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={radius:200,angle:120,pointSize:24,points:[{id:"POINT_ID_1",label:"Point label 1"},{id:"POINT_ID_2",label:"Point label 2"},{id:"POINT_ID_3",label:"Point label 3"},{id:"POINT_ID_4",label:"Point label 4"},{id:"POINT_ID_5",label:"Point label 5"}]}},function(e,t,n){var r=n(0);"string"==typeof r&&(r=[[e.i,r,""]]);var o={hmr:!0,transform:void 0,insertInto:void 0},i=n(10)(r,o);r.locals&&(e.exports=r.locals),e.hot.accept(0,function(){var t=n(0);if("string"==typeof t&&(t=[[e.i,t,""]]),!function(e,t){var n,r=0;for(n in e){if(!t||e[n]!==t[n])return!1;r++}for(n in t)r--;return 0===r}(r.locals,t.locals))throw new Error("Aborting CSS HMR due to changed css-modules locals.");i(t)}),e.hot.dispose(function(){i()})},function(e,t){e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var n=function(e,t){var n=e[1]||"",r=e[3];if(!r)return n;if(t&&"function"==typeof btoa){var o=(a=r,"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(a))))+" */"),i=r.sources.map(function(e){return"/*# sourceURL="+r.sourceRoot+e+" */"});return[n].concat(i).concat([o]).join("\n")}var a;return[n].join("\n")}(t,e);return t[2]?"@media "+t[2]+"{"+n+"}":n}).join("")},t.i=function(e,n){"string"==typeof e&&(e=[[null,e,""]]);for(var r={},o=0;o=0&&u.splice(t,1)}function m(e){var t=document.createElement("style");return void 0===e.attrs.type&&(e.attrs.type="text/css"),b(t,e.attrs),h(e,t),t}function b(e,t){Object.keys(t).forEach(function(n){e.setAttribute(n,t[n])})}function g(e,t){var n,r,o,i;if(t.transform&&e.css){if(!(i=t.transform(e.css)))return function(){};e.css=i}if(t.singleton){var a=l++;n=c||(c=m(t)),r=x.bind(null,n,a,!1),o=x.bind(null,n,a,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(e){var t=document.createElement("link");return void 0===e.attrs.type&&(e.attrs.type="text/css"),e.attrs.rel="stylesheet",b(t,e.attrs),h(e,t),t}(t),r=function(e,t,n){var r=n.css,o=n.sourceMap,i=void 0===t.convertToAbsoluteUrls&&o;(t.convertToAbsoluteUrls||i)&&(r=d(r));o&&(r+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */");var a=new Blob([r],{type:"text/css"}),s=e.href;e.href=URL.createObjectURL(a),s&&URL.revokeObjectURL(s)}.bind(null,n,t),o=function(){v(n),n.href&&URL.revokeObjectURL(n.href)}):(n=m(t),r=function(e,t){var n=t.css,r=t.media;r&&e.setAttribute("media",r);if(e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}.bind(null,n),o=function(){v(n)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else o()}}e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(t=t||{}).attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||"boolean"==typeof t.singleton||(t.singleton=a()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var n=p(e,t);return f(n,t),function(e){for(var r=[],o=0;o