Commit c209d809 authored by Benjamin Schudel's avatar Benjamin Schudel
Browse files

add 3d sphere view

parent 6808778c
Pipeline #23091 passed with stage
in 2 minutes and 40 seconds
......@@ -13,7 +13,7 @@
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
},
"dependencies": {
"chroma-js": "^1.2.1",
"chroma-js": "^1.3.7",
"color-blind": "^0.1.1",
"color2": "^1.0.8",
"d3": "^4.3.0",
......@@ -22,6 +22,8 @@
"hyperform": "^0.8.9",
"lodash": "^4.16.4",
"node-sass": "^4.9.0",
"orbit-controls-es6": "^1.0.12",
"three": "^0.94.0",
"tinycolor2": "^1.4.1",
"vue": "^2.5.16",
"vue-clipboards": "^0.2.5",
......
......@@ -12,7 +12,9 @@
@change="onInputChange"
@focus="onInputFocus"
/>
<div class="color-input__sample"
<div
v-if="!noSample"
class="color-input__sample"
:style="sampleStyle"
@mouseenter="colorFocus = true"
@mouseleave="colorFocus = false"
......@@ -36,6 +38,9 @@
};
},
},
noSample: {
type: Boolean,
},
},
data() {
return {
......@@ -81,7 +86,6 @@
.color-input {
position: relative;
padding: 16px;
margin-bottom: 8px;
background-color: #f0f0f0;
overflow: hidden;
}
......
<template>
<div
:class="classes"
@click="onClick"
>
<color-swatch
:color="color"
></color-swatch>
<color-input
:value="color.toRgb()"
no-sample
@color-change="onChange"
></color-input>
</div>
</template>
<script>
import Color from '../scripts/Color';
import ColorInput from './ColorInput';
import ColorSwatch from './ColorSwatch';
export default {
name: 'color-selector',
props: {
color: {
type: Color,
},
selected: {
type: Boolean,
},
compact: {
type: Boolean,
},
},
computed: {
classes() {
const { name } = this.$options;
return {
[name]: true,
[`${name}--selected`]: this.selected,
[`${name}--compact`]: this.compact,
};
},
},
methods: {
onClick() {
this.$emit('select', this.color);
},
onChange(d) {
this.$emit('change', new Color(d));
},
},
components: {
ColorInput,
ColorSwatch,
},
};
</script>
<style lang="scss">
@import "../styles/core";
.color-selector {
min-width: 120px;
display: flex;
flex-direction: column;
&.color-selector--selected {
.color-swatch {
box-shadow: inset 0 0 0 8px rgba(0, 0, 0, 0.4);
}
}
&.color-selector--compact {
flex-direction: row;
.color-swatch {
height: auto;
width: 4rem;
}
}
}
</style>
......@@ -6,21 +6,21 @@
</template>
<script>
import Color from '../scripts/Color';
import Color from '../scripts/Color';
export default {
name: 'color-swatch',
props: {
color: Color,
},
computed: {
style() {
return {
backgroundColor: this.color.toRgbString(),
};
export default {
name: 'color-swatch',
props: {
color: Color,
},
},
};
computed: {
style() {
return {
backgroundColor: this.color.toRgbString(),
};
},
},
};
</script>
<style lang="scss">
......
......@@ -28,6 +28,11 @@ export default [
path: '/accessibility',
component: views.AccessibilityView,
},
{
name: 'three',
path: '/three',
component: views.ThreeView,
},
{
name: 'gradients',
path: '/gradients',
......
import Tinycolor from 'tinycolor2';
import tinycolor from 'tinycolor2';
import blinder from 'color-blind';
class Color {
constructor(value) {
this._color = new Tinycolor(value);
constructor(value, isRatio = false) {
this._color = (isRatio)
? tinycolor.fromRatio(value)
: tinycolor(value);
}
toRgbString() {
......@@ -14,6 +16,14 @@ class Color {
return this._color.toRgb();
}
toHexString() {
return this._color.toHexString();
}
toHex() {
return this._color.toHex();
}
deuteranomaly() {
return new Color(
blinder.deuteranomaly(this.toRgbString())
......
/* globals window, document */
import * as THREE from 'three';
import OrbitControls from 'orbit-controls-es6';
import chroma from 'chroma-js';
const times = (count) => [...Array(count).keys()];
class ThreePicker {
static RADIUS = 5;
constructor(element, {
columns = 24,
rows = 32,
useBezier = false,
colors = ['white', 'green', 'black'],
} = {}) {
this.element = element;
this.scene = new THREE.Scene();
this._columns = columns;
this._rows = rows;
this._useBezier = useBezier;
this._colors = colors;
this.setup();
this.draw();
this.render();
}
get radius() {
return this._radius;
}
set radius(v) {
this._radius = v;
this.draw();
}
get columns() {
return this._columns;
}
set columns(v) {
this._columns = v;
this.draw();
}
get rows() {
return this._rows;
}
set rows(v) {
this._rows = v;
this.draw();
}
get useBezier() {
return this._useBezier;
}
set useBezier(v) {
this._useBezier = v;
}
get colors() {
return this._colors;
}
set colors(v) {
this._colors = v;
this.tint();
}
setup() {
const width = 800;
const camera = this.camera = new THREE.PerspectiveCamera(75, 1, 0.1, 50);
camera.position.z = 10;
const renderer = this.renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, width);
renderer.setClearColor(0xffffff, 1);
this.element.appendChild(renderer.domElement);
const light = this.light = new THREE.AmbientLight(0xffffff);
this.scene.add(light);
const orbit = this.orbit = new OrbitControls(camera, renderer.domElement);
orbit.enableZoom = false;
document.addEventListener('pointerdown', this._onPointerDown.bind(this), false);
}
draw() {
this.scene.remove(this.sphere);
const geometry = new THREE.SphereGeometry(
ThreePicker.RADIUS,
this.columns,
this.rows,
);
const material = new THREE.MeshBasicMaterial(
{ color: 0xffffff, vertexColors: THREE.FaceColors }
);
const sphere = this.sphere = new THREE.Mesh(geometry, material);
this.scene.add(sphere);
this.tint();
}
tint() {
const { columns, rows, colors } = this;
const hueStep = 360 / columns;
const { geometry } = this.sphere;
let i = 0;
let j = 0;
let color;
function toRgb(chromaColor) {
return chromaColor.rgb(false).map(v => v / 255);
}
const scales = times(columns).map(i => {
const newColors = [...colors]
.map((c, j) => {
if (j === 0 || j > (colors.length - 2)) {
return c;
}
const hueAngle = i * hueStep;
return chroma(c)
.set('hsl.h', `+${hueAngle}`)
.hex();
});
return chroma
.scale(newColors)
.mode('rgb')
// .correctLightness()
.domain([0, rows]);
});
while (i < columns) {
color = toRgb(scales[i](0));
geometry.faces[i].color.setRGB(...color);
i++;
}
while (i < (geometry.faces.length - columns)) {
const row = Math.floor((i + columns) / (2 * columns));
const column = Math.floor((i + columns) / 2) % columns;
color = toRgb(scales[column](row));
geometry.faces[i].color.setRGB(...color);
geometry.faces[i + 1].color.setRGB(...color);
i += 2;
}
j = 0;
while (i < geometry.faces.length) {
color = toRgb(scales[j++](rows));
geometry.faces[i].color.setRGB(...color);
i++;
}
geometry.colorsNeedUpdate = true;
}
_onPointerDown(event) {
const { camera } = this;
const { devicePixelRatio: ratio } = window;
const rect = this.renderer.domElement.getBoundingClientRect();
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
mouse.x = (((event.clientX - rect.left) / (rect.right - rect.left)) * ratio) - 1;
mouse.y = -(((event.clientY - rect.top) / (rect.bottom - rect.top)) * ratio) + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([this.sphere]);
if (intersects.length > 0) {
const color = intersects[0].face.color;
this.onColorSelect(color);
}
}
render() {
this.renderer.render(this.scene, this.camera);
window.requestAnimationFrame(this.render.bind(this));
}
/* eslint-disable class-methods-use-this */
onColorSelect(value) {
// callback
}
}
export default ThreePicker;
export default {};
import Color from '../scripts/Color';
export default {
currentColor: (state) => new Color(state.currentColor),
};
import gradients from './gradients';
import palettes from './palettes';
import accessibility from './accessibility';
import three from './three';
export default {
gradients,
palettes,
accessibility,
three,
};
export default {
currentColor(state, getters, rootState) {
return { ...rootState.currentColor };
},
};
export default {};
import Color from '../../../scripts/Color';
export default {
colors: (state) => state.colors.map(d => new Color(d)),
};
import state from './state';
import mutations from './mutations';
import actions from './actions';
import getters from './getters';
export default {
state,
mutations,
actions,
getters,
};
import Color from '../../../scripts/Color';
import types from './types';
export default {
[types.UPDATE_COLORS](state, colors) {
const _colors = colors.map(d => ((d instanceof Color) ? d.toRgb() : d));
state.colors = _colors;
},
};
export default {
colors: [
{ r: 255, g: 255, b: 255 },
{ r: 255, g: 0, b: 0 },
{ r: 0, g: 0, b: 0 },
],
};
export default {
UPDATE_COLORS: 'three/updateColors',
};
/* eslint-disable no-multi-spaces */
import _ from '../utils';
import Color from '../scripts/Color';
import types from './types';
const toValidColor = _.utils.toValidColor;
......@@ -23,7 +23,8 @@ export default {
state.notifications.splice(index, 1);
},
[types.UPDATE_CURRENT_COLOR](state, data) {
state.currentColor = toValidColor(data);
[types.UPDATE_CURRENT_COLOR](state, color) {
// TODO: always accept only color instances
state.currentColor = (color instanceof Color) ? color.toRgb() : color;
},
};
......@@ -10,5 +10,8 @@ export default {
}, {
name: 'Accessibility',
route: 'accessibility',
}, {
name: '3D Space',
route: 'three',
}],
};
import gradients from './modules/gradients/types';
import palettes from './modules/palettes/types';
import accessibility from './modules/accessibility/types';
import three from './modules/three/types';
// TODO: move all mutations to namespaced and add ...
export default {
gradients,
palettes,
...accessibility,
accessibility,
three,
ADD_NOTIFICATION: 'core/addNotification',
DELETE_NOTIFICATION: 'core/deleteNotification',
UPDATE_CURRENT_COLOR: 'core/udpateCurrentColor',
......
import GradientView from './views/GradientView';
import MixerView from './views/MixerView';
import AccessibilityView from './views/AccessibilityView';
import ThreeView from './views/ThreeView';
export default {
GradientView,
MixerView,
AccessibilityView,
ThreeView,
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment