Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save untergrundbiber/7c97f2474b85f3fe523db8e0382c079b to your computer and use it in GitHub Desktop.
Save untergrundbiber/7c97f2474b85f3fe523db8e0382c079b to your computer and use it in GitHub Desktop.
Der Bunt: Color Harmonies (Alpha 1.1)

Der Bunt: Color Harmonies (Alpha 1.1)

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 David A. on CodePen.

License.

.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="//cdn.rawgit.com/gka/chroma.js/master/chroma.min.js"></script>
<script src="//cdn.rawgit.com/dtao/nearest-color/master/nearestColor.js"></script>
<script src="//codepen.io/meodai/pen/VLVRYw"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/102565/hsluv.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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment