Todo:
- Optimise for touch
- 3d view of color distribution in different color spaces
- contrast ratio from selected color to all other colors
- save & export palette
- alternate palette views
A Pen by mike-at-redspace on CodePen.
Todo:
A Pen by mike-at-redspace on CodePen.
| .js-colors | |
| .js-palette | |
| app-wrap('title'='Der Bunt', 'value'='#ffffff') | |
| .settings-background | |
| fan(':colors'='colors') | |
| .settings | |
| label.settings__entry | |
| h3.settings__label Method | |
| select('v-model'='currentSpace', 'v-on:change'='updatePalette') | |
| option('v-for'='space in spacesList', value='{{space}}') {{space}} | |
| div | |
| label.settings__entry | |
| h3.settings__label colors | |
| span.settings__value {{maxColors}} | |
| input.settings__input(type='range', min=2, 'v-bind:max'='colorsLimit', step=1, 'v-bind:value'='maxColors', 'v-model'='maxColors', 'v-on:input'='updatePalette') | |
| label.settings__entry('v-for'='attr in currentSettings.attr') | |
| h3.settings__label {{attr.name}} | |
| span.settings__value {{attr.value}} | |
| template(v-if='attr.type == "color"') | |
| input.settings__input(type='color', 'v-bind:value'='attr.value', 'v-on:input'='updatePalette', 'v-model'='attr.value') | |
| template(v-if='attr.type == "select"') | |
| select(type='{{attr.type}}', 'v-bind:value'='attr.value', 'v-on:change'='updatePalette', 'v-model'='attr.value') | |
| option(v-for='val in attr.values', value='{{val}}') {{val}} | |
| template(v-if='!attr.type') | |
| input.settings__input(type='range', 'v-bind:min'='attr.min', 'v-bind:max'='attr.max', 'v-bind:step'='attr.step', 'v-bind:value'='attr.value', 'v-model'='attr.value', 'v-on:input'='updatePalette') | |
| label.settings__entry('v-if'='space.hasStart') | |
| h3.settings__label start color | |
| input.settings__input(type='text', placeholder='HEX, RGB, HSL, Name, HSV etc..', 'v-on:change'='updatePalette') | |
| fan(':colors'='colors') |
| let currentColor = { | |
| title: 'Der Bunt', | |
| value: '#ffffff', | |
| index: 15, | |
| total: 16, | |
| }; | |
| let appWrap = Vue.extend({ | |
| template: '<div class="app-wrap background" v-bind:style="{background: value}">' | |
| + '<header class="app-wrap__header">' | |
| + '<h1 class="app-wrap__title js-title">{{title}}</h1>' | |
| + '<h2 class="app-wrap__sub js-value">{{value}}</h2>' | |
| + '<header>' | |
| + '<slot />' | |
| + '</div>', | |
| data: () => { | |
| return currentColor; | |
| }, | |
| }); | |
| Vue.component('app-wrap', appWrap); | |
| var blades = { | |
| hovered: null | |
| }; | |
| let blade = Vue.extend({ | |
| template: '<article class="blade" v-on:click="setActive"' | |
| + 'v-on:mouseover="hover(index)" v-bind:style="style()">' | |
| + '<h2 class="blade__value"><strong>{{color}}</strong></h2>' | |
| + '<h3 class="blade__label"><span class="blade__label--inner">{{label}}</span></h3>' | |
| + '</article>', | |
| props: { | |
| color: String, | |
| label: String, | |
| index: Number, | |
| total: Number, | |
| hoverindex: Number | |
| }, | |
| data: function(){ | |
| return { | |
| shared: blades, | |
| isHovered: false | |
| } | |
| }, | |
| methods: { | |
| style: function(){ | |
| let rotation = (this.index + 1) * (360 / this.total); | |
| const scale = this.index * 2; | |
| const X = this.hoverindex == this.index ? '-10%' : 0; | |
| /*if (this.hoverindex - 1 === this.index || (this.hoverindex === 0 && this.index == this.total - 1)) { | |
| rotation -= this.total * .3; | |
| } else if (this.hoverindex + 1 === this.index || (this.hoverindex === this.total - 1 && this.index === 0)){ | |
| rotation += this.total * .3; | |
| }*/ | |
| return { | |
| 'transform': `rotate(${rotation}deg) translate3d(0,${X},${scale + 20}px)`, | |
| 'background-color': this.color, | |
| 'color': this.color, | |
| //'height': 42 - ((this.total / 33) * 13) + 'vh' | |
| } | |
| }, | |
| setActive: function(event){ | |
| currentColor.title = this.label; | |
| currentColor.value = this.color; | |
| currentColor.index = this.index; | |
| //this.$dispatch('colorChange', this.index, this.label, this.color); | |
| }, | |
| hover: function(index){ | |
| //this.shared.hovered = index; | |
| //this.style(); | |
| } | |
| } | |
| }); | |
| Vue.component('blade', blade); | |
| let fan = Vue.extend({ | |
| template: '<section class="fan" v-bind:style="setRotation()">' | |
| + '<blade v-for="color in colors" track-by="$index" v-bind:color="color.hex" v-bind:label="color.name" v-bind:index="$index" v-bind:total="colors.length" />' | |
| + '</section>', | |
| props: { | |
| colors: Array, | |
| label: String, | |
| active: Number, | |
| }, | |
| data: () => { | |
| return currentColor | |
| }, | |
| watch: { | |
| 'index': function (val, oldVal) { | |
| this.setRotation(val); | |
| } | |
| }, | |
| created: function () { | |
| /*this.$on('colorChange', (index, label, color) => { | |
| //this.index = index; | |
| if( this.total != this.colors.length ){ | |
| this.hoverindex.blades.hovered = null; | |
| } | |
| this.total = this.colors.length; | |
| this.setRotation(); | |
| return true; | |
| });*/ | |
| }, | |
| methods: { | |
| setRotation: function () { | |
| const rotation = (this.index + 1) * (360 / this.total); | |
| return { | |
| transform: `translate3d(0,0,0) rotate(${-rotation || 0}deg)` | |
| } | |
| } | |
| } | |
| }); | |
| Vue.component('fan', fan); | |
| function colorConv(space, ...color) { | |
| let husl, c; | |
| switch (space) { | |
| case 'HSLuv': | |
| var hsl = chroma(color, 'hsl').hsl(); | |
| husl = hsluv.hsluvToHex([hsl[0], hsl[1] * 100, hsl[2] * 100]); | |
| case 'HSLuvP': | |
| var hsl = chroma(color, 'hsl').hsl(); | |
| husl = husl || hsluv.hpluvToHex([hsl[0], hsl[1] * 100, hsl[2] * 100]); | |
| c = chroma(husl, 'hex'); | |
| break; | |
| case 'lch': | |
| c = chroma(color[1], color[2], color[0], 'lch'); | |
| break; | |
| case 'cubehelix': | |
| c = chroma.cubehelix() | |
| .start(color[0]) | |
| .rotations(color[1]) | |
| .hue(color[2]) | |
| .gamma(color[3]) | |
| .lightness([color[4], color[5]]); | |
| c = c(color[0]/360); | |
| break; | |
| case 'scale': | |
| let mode = color[3]; | |
| if (color[3] === 'edg') { | |
| mode = 'hsv'; | |
| } | |
| let carr = chroma.scale([color[1], color[2]]).mode(mode).colors(33); | |
| if (color[3] === 'edg') { | |
| let carrRGB = chroma.scale([color[1], color[2]]).mode('rgb').colors(33); | |
| let colrRGB = carr.map((col, i) => { | |
| return chroma.average([col, carrRGB[i]]); | |
| }); | |
| carr = chroma.scale(colrRGB).colors(33); | |
| } | |
| c = chroma(carr[Math.ceil(32 * (color[0] / 360))]); | |
| break; | |
| default: | |
| c = chroma(color, space); | |
| } | |
| const hex = c.hex(); | |
| return { | |
| color: c, | |
| hex: hex, | |
| css: c.css('hsl'), | |
| name: getClosestNamedColor( hex ).name | |
| } | |
| }; | |
| let colorSpaces = [ | |
| { | |
| name: ['hsl', 'HSLuv', 'HSLuvP'], | |
| hasStart: true, | |
| attr: [ | |
| { | |
| name: 'hue', | |
| min: 0, | |
| max: 360, | |
| step: 1, | |
| value: 0, | |
| }, | |
| { | |
| name: 'saturation', | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| value: 1, | |
| }, | |
| { | |
| name: 'light', | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| value: .8, | |
| } | |
| ] | |
| }, | |
| { | |
| name: 'cubehelix', | |
| hasStart: true, | |
| attr: [ | |
| { | |
| name: 'start', | |
| min: 0, | |
| max: 360, | |
| step: 1, | |
| value: 0, | |
| }, | |
| { | |
| name: 'rotations', | |
| min: -2, | |
| max: 2, | |
| step: 0.01, | |
| value: -1.5, | |
| }, | |
| { | |
| name: 'hue', | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| value: 1, | |
| }, | |
| { | |
| name: 'gamma', | |
| min: 0, | |
| max: 1, | |
| step: 0.01, | |
| value: 1, | |
| }, | |
| { | |
| name: 'lightness min', | |
| min: 0, | |
| max: .9, | |
| step: 0.01, | |
| value: .2, | |
| }, | |
| { | |
| name: 'lightness max', | |
| min: .1, | |
| max: 1, | |
| step: 0.01, | |
| value: .8, | |
| } | |
| ] | |
| }, | |
| { | |
| name: 'lch', | |
| hasStart: false, | |
| attr: [ | |
| { | |
| name: 'h', | |
| min: 0, | |
| max: 360, | |
| step: 1, | |
| value: 20, | |
| }, | |
| { | |
| name: 'l', | |
| min: 0, | |
| max: 100, | |
| step: 1, | |
| value: 75, | |
| }, | |
| { | |
| name: 'c', | |
| min: 0, | |
| max: 100, | |
| step: 1, | |
| value: 100, | |
| } | |
| ] | |
| }, | |
| { | |
| name: 'scale', | |
| hasStart: false, | |
| attr: [ | |
| { | |
| name: 'shift', | |
| min: 0, | |
| max: 360, | |
| step: 1, | |
| value: 0, | |
| }, | |
| { | |
| name: 'start', | |
| value: '#72ffd7', | |
| type: 'color', | |
| }, | |
| { | |
| name: 'stop', | |
| value: '#f03b50', | |
| type: 'color', | |
| }, | |
| { | |
| name: 'space', | |
| value: 'lab', | |
| values: ['lab', 'hsl', 'hsv', 'hsi', 'lch', 'rgb', 'lrgb', 'edg', 'num'], | |
| type: 'select', | |
| } | |
| ] | |
| } | |
| ]; | |
| let palette = new Vue({ | |
| el: '.js-palette', | |
| data: { | |
| activeColor: 0, | |
| rawcolors: [], | |
| startColor: null, | |
| maxColors: 16, | |
| colorsLimit: 33, | |
| currentSpace: 'HSLuvP', | |
| spaces: colorSpaces, | |
| }, | |
| computed: { | |
| colors: { | |
| get: function(){ | |
| return this.rawcolors; | |
| }, | |
| set: function(colors){ | |
| const currentSpace = this.currentSpace; | |
| this.rawcolors = colors.map(function(color){ | |
| var colorConvArgs = color; | |
| colorConvArgs.unshift(currentSpace); | |
| return colorConv.apply(null, colorConvArgs); | |
| }); | |
| } | |
| }, | |
| currentSettings: function() { | |
| return this.spaces.find((space) => { | |
| return (this.currentSpace == space.name || space.name.indexOf(this.currentSpace) !== -1); | |
| }); | |
| }, | |
| spacesList: function(){ | |
| let list = []; | |
| this.spaces.forEach((space) => { | |
| list = list.concat(typeof space.name === 'string' ? [space.name] : space.name); | |
| }); | |
| return list; | |
| } | |
| }, | |
| methods: { | |
| updatePalette: function() { | |
| let colors = []; | |
| const currentSpace = this.currentSpace; | |
| let systemData = this.currentSettings; | |
| for(let i = 0; i < this.maxColors; i++){ | |
| let color = [(((i/this.maxColors) * 360) + systemData.attr[0].value) % 360]; | |
| systemData.attr.forEach((attr, i) => { | |
| if (i) | |
| color.push(attr.value); | |
| }); | |
| colors.push(color); | |
| } | |
| this.colors = colors; | |
| currentColor.total = colors.length; | |
| currentColor.index = colors.length - 1; | |
| } | |
| } | |
| }); | |
| palette.updatePalette(); |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script> | |
| <script src="//cdn.rawgit.com/dtao/nearest-color/master/nearestColor.js"></script> | |
| <script src="//codepen.io/meodai/pen/VLVRYw.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/102565/hsluv.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.4.2/chroma.min.js"></script> |
| $c-black: #212121; | |
| $c-white: #fff; | |
| $bg: $c-white; | |
| $golden: 1.61803398875; | |
| // <link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet"> | |
| @import 'https://fonts.googleapis.com/css?family=Inconsolata'; | |
| $t-code: 'Inconsolata', ipm, Menlo, 'Courier New', monospace; | |
| //@import 'https://fonts.googleapis.com/css?family=Space+Mono'; | |
| //$t-code: 'Space Mono', ipm, Menlo, 'Courier New', monospace; | |
| body, html { | |
| font-family: $t-code; | |
| height: 100%; | |
| font-size: calc(0.5rem + 1.4vh); | |
| } | |
| .background { | |
| position: absolute; | |
| top: 0; right: 0; bottom: 0; left: 0; | |
| overflow: hidden; | |
| transition: 200ms background-color linear 500ms; | |
| will-change: background-color; | |
| } | |
| .app-wrap { | |
| &__header { | |
| padding: 1rem; | |
| } | |
| &__title { | |
| //font-family: $t-copy; | |
| font-size: 2rem; | |
| margin-bottom: 0.15em; | |
| } | |
| &__sub { | |
| font-family: $t-code; | |
| } | |
| } | |
| .fan { | |
| position: absolute; | |
| top: 50vh; right: 50vw; | |
| perspective: 600; | |
| transition: 450ms transform cubic-bezier(0.370, 0.000, 0.250, 0.980); | |
| } | |
| .blade { | |
| position: absolute; | |
| cursor: pointer; | |
| display: flex; | |
| flex-direction: column; | |
| height: 40vh; width: 10vh; | |
| top: -40vh; left: 0; | |
| box-shadow: 0 0 0 1px rgba($c-white,.15), | |
| 0 0 15px rgba($c-black,.1); | |
| transform: translate3d(0,0,0) rotate(0deg); | |
| transform-origin: 1vh 39vh; | |
| border-radius: .5vh; | |
| overflow: hidden; | |
| transition: 200ms 200ms transform ease-in-out; | |
| transition: 200ms 200ms transform cubic-bezier(0.250, 0.250, 0.275, 1.265); | |
| &__label, &__value { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| padding: 1vh; | |
| line-height: 1.2; | |
| } | |
| &__label { | |
| color: $c-white; | |
| font-size: 1.6vh; | |
| padding-top: .75vh; | |
| &--inner { | |
| display: block; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| } | |
| &__value { | |
| font-size: 1.8vh; | |
| font-weight: 500; | |
| line-height: .75; | |
| text-transform: uppercase; | |
| background: $c-white; | |
| color: currentColor; | |
| } | |
| } | |
| .settings, | |
| .settings-background { | |
| position: fixed; | |
| top: 0; right: 0; bottom: 0; | |
| z-index: 10; | |
| width: 250px; | |
| transform: translateZ(1000px); | |
| } | |
| .settings { | |
| box-sizing: border-box; | |
| padding: 1rem; | |
| backdrop-filter: blur(5px); | |
| background-color: rgba($c-white,.2); | |
| box-shadow: -1px 0 0 rgba($c-black,.1); | |
| overflow: auto; | |
| &__entry { | |
| display: flex; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| margin-bottom: 1.5rem; | |
| } | |
| &__label { | |
| flex-grow: 1; | |
| width: 80%; | |
| flex-basis: 80%; | |
| font-size: 1rem; | |
| margin-bottom: .5rem; | |
| } | |
| &__input { | |
| width: 70%; | |
| } | |
| &__value { | |
| font-family: $t-code; | |
| font-size: 0.8rem; | |
| text-align: right; | |
| width: 20%; | |
| } | |
| } | |
| .settings-background { | |
| pointer-events: none; | |
| transform: translateZ(999px); | |
| filter: blur(4px); | |
| overflow: hidden; | |
| .blade { | |
| box-shadow: 0 0 0 1px rgba($c-white,.15); | |
| } | |
| } | |
| input { | |
| background-color: transparent; | |
| } | |
| input[type=range], | |
| input[type=color] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| } | |
| // range sliders | |
| input[type=range] { | |
| margin: 0 0 0.5rem 0; | |
| } | |
| input[type=range]:focus { | |
| outline: none; | |
| &::-webkit-slider-thumb { | |
| //height: .65rem; | |
| //background-color: $c-white; | |
| clip-path: polygon(100% 0%, 0% 0%, 50% 100%, 50% 100%); | |
| //clip-path: polygon(50% 0%, 50% 0%, 0% 100%, 100% 100%); | |
| } | |
| } | |
| @mixin slider-track { | |
| width: 100%; | |
| height: 1rem; | |
| cursor: pointer; | |
| animate: 0.2s; | |
| background: transparent; | |
| color: transparent; | |
| border-radius: 0; | |
| border: solid $c-black; | |
| border-width: 0 0 1px; | |
| } | |
| @mixin slider-thumb { | |
| border: 2px solid transparent; | |
| height: .75rem; width: .5rem; | |
| border-radius: 0; | |
| background: $c-black; | |
| cursor: pointer; | |
| -webkit-appearance: none; | |
| margin-top: 0.25rem; | |
| transition: 150ms background-color, 200ms clip-path, 200ms -webkit-clip-path; | |
| clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| @include slider-track; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| @include slider-thumb; | |
| } | |
| input[type=range]:focus::-webkit-slider-runnable-track { | |
| //background: $c-black; | |
| } | |
| input[type=range]::-moz-range-track { | |
| @include slider-track; | |
| } | |
| input[type=range]::-moz-range-thumb { | |
| @include slider-thumb; | |
| } | |
| input[type=range]::-ms-track { | |
| @include slider-track; | |
| } | |
| input[type=range]::-ms-fill-lower { | |
| background: $c-black; | |
| border: none; | |
| border-radius: 100%; | |
| } | |
| input[type=range]::-ms-fill-upper { | |
| background: $c-black; | |
| border-radius: 100%; | |
| box-shadow: none; | |
| } | |
| input[type=range]::-ms-thumb { | |
| @include slider-thumb; | |
| } | |
| input[type=range]:focus::-ms-fill-lower { | |
| //background: $c-black; | |
| } | |
| input[type=range]:focus::-ms-fill-upper { | |
| //background: $c-black; | |
| } | |
| select { | |
| font-family: $t-code; | |
| width: 100%; | |
| box-sizing: border-box; | |
| font-size: 0.8rem; | |
| -webkit-appearance: none; | |
| border: 0; | |
| box-shadow: 0 1px 0 0 $c-black; | |
| border-radius: 0; | |
| padding: 0.25rem 1rem 0.25rem 0.25rem; | |
| background-color: transparent; | |
| background-size: auto 40%; | |
| background-repeat: no-repeat; | |
| background-position: 98% 50%; | |
| background-image: url('data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%20%3C%21--%20Generator%3A%20IcoMoon.io%20--%3E%20%3C%21DOCTYPE%20svg%20PUBLIC%20%22-//W3C//DTD%20SVG%201.1//EN%22%20%22http%3A//www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd%22%3E%20%3Csvg%20width%3D%22512%22%20height%3D%22512%22%20viewBox%3D%220%200%20512%20512%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M%2096.00%2C96.00l-96.00%2C96.00l%20256.00%2C256.00l%20256.00-256.00l-96.00-96.00L%20256.00%2C256.00L%2096.00%2C96.00z%22%20%3E%3C/path%3E%3C/svg%3E'); | |
| transition: 150ms background-color; | |
| &:focus { | |
| outline: none; | |
| background-color: rgba($c-white,1); | |
| } | |
| } |