Color picker progress

This commit is contained in:
Cory LaViska
2020-05-07 08:14:21 -04:00
parent f5a287e1b2
commit 56784c94f9
8 changed files with 272 additions and 73 deletions

33
package-lock.json generated
View File

@@ -355,6 +355,23 @@
"dev": true,
"requires": {
"color-convert": "^1.9.0"
},
"dependencies": {
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
}
}
},
"ansi-wrap": {
@@ -1017,19 +1034,17 @@
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "1.1.3"
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-support": {
"version": "1.1.3",

View File

@@ -43,6 +43,7 @@
"@popperjs/core": "^2.1.1",
"@stencil/core": "^1.12.6",
"@stencil/sass": "^1.1.1",
"color-convert": "^2.0.1",
"feather-icons": "^4.28.0",
"normalize.css": "^8.0.1",
"resize-observer-polyfill": "^1.5.1",

View File

@@ -11,19 +11,17 @@
.sl-color-picker__menu {
// position: absolute;
max-height: 50vh;
font-family: var(--sl-font-sans);
font-size: var(--sl-font-size-medium);
font-weight: var(--sl-font-weight-normal);
color: var(--color);
background-color: var(--sl-color-white);
border: solid 1px var(--sl-color-gray-90);
border-radius: 4px;
border-radius: var(--sl-border-radius-medium);
box-shadow: var(--sl-shadow-large);
padding-top: var(--sl-spacing-x-small);
padding-bottom: var(--sl-spacing-x-small);
// opacity: 0;
transition: var(--sl-transition-fast) opacity;
user-select: none;
// &[hidden] {
// display: none;
@@ -38,7 +36,8 @@
position: relative;
width: var(--grid-width);
height: var(--grid-height);
margin-bottom: 8px;
border-top-left-radius: var(--sl-border-radius-medium);
border-top-right-radius: var(--sl-border-radius-medium);
cursor: crosshair;
}
@@ -49,6 +48,8 @@
width: 100%;
height: 100%;
background-image: linear-gradient(to right, rgb(255, 255, 255), rgba(255, 255, 255, 0));
border-top-left-radius: calc(var(--sl-border-radius-medium) - 1px);
border-top-right-radius: calc(var(--sl-border-radius-medium) - 1px);
&::after {
content: '';
@@ -58,6 +59,7 @@
width: 100%;
height: 100%;
background-image: linear-gradient(to top, rgb(0, 0, 0), rgba(0, 0, 0, 0));
border-radius: inherit;
}
}
@@ -66,17 +68,47 @@
width: var(--sight-size);
height: var(--sight-size);
border-radius: 50%;
border: solid 1px rgba(0, 0, 0, 0.125);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.125), inset 0 0 0 1px rgba(0, 0, 0, 0.125);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(0, 0, 0, 0.25);
border: solid 2px white;
margin-top: calc(var(--sight-size) / -2);
margin-left: calc(var(--sight-size) / -2);
cursor: pointer;
}
.sl-color-picker__hue {
.sl-color-picker__controls {
padding: var(--sl-spacing-small);
display: flex;
align-items: center;
}
.sl-color-picker__sliders {
flex: 1 1 auto;
}
.sl-color-picker__slider {
position: relative;
height: 12px;
height: 10px;
border-radius: var(--sl-border-radius-pill);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
&:not(:last-of-type) {
margin-bottom: var(--sl-spacing-small);
}
}
.sl-color-picker__slider-handle {
position: absolute;
top: calc(50% - var(--slider-size) / 2);
width: var(--slider-size);
height: var(--slider-size);
background-color: white;
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
margin-left: calc(var(--slider-size) / -2);
cursor: pointer;
}
.sl-color-picker__hue {
background-image: linear-gradient(
to right,
rgb(255, 0, 0) 0%,
@@ -87,18 +119,13 @@
rgb(255, 0, 255) 83%,
rgb(255, 0, 0) 100%
);
margin-bottom: 8px;
border-radius: 2px;
}
.sl-color-picker__alpha {
position: relative;
height: 12px;
border-radius: 2px;
background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%),
linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(45deg, #eee 25%, transparent 25%);
background-size: 12px 12px;
background-position: 0 0, 0 0, -6px -6px, 6px 6px;
background-size: 10px 10px;
background-position: 0 0, 0 0, -5px -5px, 5px 5px;
.sl-color-picker__alpha-gradient {
position: absolute;
@@ -106,32 +133,63 @@
left: 0;
width: 100%;
height: 100%;
border-radius: 2px;
border-radius: inherit;
}
}
.sl-color-picker__slider {
position: absolute;
top: calc(50% - var(--slider-size) / 2);
width: var(--slider-size);
height: var(--slider-size);
background-color: white;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
.sl-color-picker__preview {
flex: 0 0 auto;
position: relative;
width: 2rem;
height: 2rem;
background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%),
linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(45deg, #eee 25%, transparent 25%);
background-size: 10px 10px;
background-position: 0 0, 0 0, -5px -5px, 5px 5px;
border-radius: 50%;
margin-left: calc(var(--slider-size) / -2);
cursor: pointer;
margin-left: var(--sl-spacing-medium);
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
background-color: currentColor;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
}
}
.sl-color-picker__preview-color {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
border: solid 1px rgba(0, 0, 0, 0.125);
}
.sl-color-picker__input {
padding: 0 var(--sl-spacing-small) var(--sl-spacing-small) var(--sl-spacing-small);
}
.sl-color-picker__swatches {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-gap: 6px;
justify-items: center;
border-top: solid 1px var(--sl-color-gray-90);
padding: var(--sl-spacing-small);
}
.sl-color-picker__swatch {
flex: 0 0 auto;
width: 16px;
height: 16px;
width: 20px;
height: 20px;
border-radius: 2px;
border: solid 1px rgba(0, 0, 0, 0.125);
cursor: pointer;
}

View File

@@ -1,4 +1,6 @@
import { Component, h } from '@stencil/core';
import { Component, State, h } from '@stencil/core';
import convert from 'color-convert';
import { clamp } from '../../utilities/math';
@Component({
tag: 'sl-color-picker',
@@ -9,87 +11,183 @@ export class ColorPicker {
menu: HTMLElement;
trigger: HTMLElement;
constructor() {
this.handleHueInput = this.handleHueInput.bind(this);
this.handleSaturationInput = this.handleSaturationInput.bind(this);
this.handleLightnessInput = this.handleLightnessInput.bind(this);
this.handleOpacityInput = this.handleOpacityInput.bind(this);
}
@State() hue = 0;
@State() saturation = 100;
@State() lightness = 50;
@State() opacity = 100;
handleHueInput(event: Event) {
const target = event.target as HTMLInputElement;
this.hue = clamp(Number(target.value), 0, 360);
}
handleSaturationInput(event: Event) {
const target = event.target as HTMLInputElement;
this.saturation = clamp(Number(target.value), 0, 100);
}
handleLightnessInput(event: Event) {
const target = event.target as HTMLInputElement;
this.lightness = clamp(Number(target.value), 0, 100);
}
handleOpacityInput(event: Event) {
const target = event.target as HTMLInputElement;
this.opacity = clamp(Number(target.value), 0, 100);
}
render() {
const hsl = [this.hue, this.saturation, this.lightness];
const rgb = convert.hsl.rgb(hsl);
const hex = convert.hsl.hex(hsl);
// const x = clamp(this.saturation, 0, 100);
// const y = 100 - this.lightness * 100;
const x = Math.abs((this.saturation * 260) / 100);
const y = Math.abs((220 - this.lightness * 220) / 100);
// console.log(x / 100, y / 100);
return (
<div ref={el => (this.trigger = el)} class="sl-color-picker">
<div class="sl-color-picker__trigger">Trigger</div>
<div ref={el => (this.menu = el)} class="sl-color-picker__menu">
<div class="sl-color-picker__grid" style={{ backgroundColor: 'red' }}>
<div
class="sl-color-picker__grid"
style={{
backgroundColor: `hsl(${this.hue}deg, 100%, 50%)`
}}
>
<div class="sl-color-picker__grid-gradient" />
<span
class="sl-color-picker__sight"
style={{
transform: `translate(100px, 100px)`
top: `${y}px`, // TODO: %
left: `${x}px` // TODO: %
}}
/>
</div>
<div class="sl-color-picker__hue">
<span
class="sl-color-picker__slider"
style={{
transform: 'translateX(20px)'
}}
/>
</div>
<div class="sl-color-picker__controls">
<div class="sl-color-picker__sliders">
<div class="sl-color-picker__hue sl-color-picker__slider">
<span
class="sl-color-picker__slider-handle"
style={{
left: `${this.hue === 0 ? 0 : 100 / (360 / this.hue)}%`
}}
/>
</div>
<div class="sl-color-picker__alpha sl-color-picker__slider">
<div
class="sl-color-picker__alpha-gradient"
style={{
backgroundImage: `linear-gradient(
to right,
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, 0%) 0%,
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%) 100%
)`
}}
/>
<span
class="sl-color-picker__slider-handle"
style={{
left: `${this.opacity}%`
}}
/>
</div>
</div>
<div class="sl-color-picker__alpha">
<div
class="sl-color-picker__alpha-gradient"
class="sl-color-picker__preview"
style={{
backgroundImage: `linear-gradient(to right, rgba(255, 0, 0, 0) 0%, rgb(255, 0, 0) 100%)`
}}
/>
<span
class="sl-color-picker__slider"
style={{
transform: 'translateX(60px)'
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.opacity}%)`
}}
/>
</div>
<div class="sl-color-picker__preview"></div>
<div class="sl-color-picker__inputs">
<div class="sl-color-picker__input sl-color-picker__input--hex">
<label>Hex</label>
<input type="text" pattern="[a-fA-F\d]+" />
<sl-input size="small" type="text" pattern="[a-fA-F\d]+" value={hex}>
<span slot="prefix">#</span>
</sl-input>
</div>
<div class="sl-color-picker__input sl-color-picker__input--rgba">
<div class="sl-color-picker__input sl-color-picker__input--rgb">
<label>R</label>
<input type="number" min="0" max="255" />
<sl-input size="small" type="number" min={0} max={255} inputmode="numeric" value={rgb[0]} />
</div>
<div class="sl-color-picker__input sl-color-picker__input--rgba">
<div class="sl-color-picker__input sl-color-picker__input--rgb">
<label>G</label>
<input type="number" min="0" max="255" />
<sl-input size="small" type="number" min={0} max={255} inputmode="numeric" value={rgb[1]} />
</div>
<div class="sl-color-picker__input sl-color-picker__input--rgba">
<div class="sl-color-picker__input sl-color-picker__input--rgb">
<label>B</label>
<input type="number" min="0" max="255" />
<sl-input size="small" type="number" min={0} max={255} inputmode="numeric" value={rgb[2]} />
</div>
<div class="sl-color-picker__input sl-color-picker__input--hsl">
<label>H</label>
<input type="number" min="0" max="255" />
<sl-input
size="small"
type="number"
min={0}
max={360}
inputmode="numeric"
value={this.hue.toString()}
onInput={this.handleHueInput}
/>
</div>
<div class="sl-color-picker__input sl-color-picker__input--hsl">
<label>S</label>
<input type="number" min="0" max="255" />
<sl-input
size="small"
type="number"
min={0}
max={100}
inputmode="numeric"
value={this.saturation.toString()}
onInput={this.handleSaturationInput}
/>
</div>
<div class="sl-color-picker__input sl-color-picker__input--hsl">
<label>L</label>
<input type="number" min="0" max="255" />
<sl-input
size="small"
type="number"
min={0}
max={100}
inputmode="numeric"
value={this.lightness.toString()}
onInput={this.handleLightnessInput}
/>
</div>
<div class="sl-color-picker__input sl-color-picker__input--rgba">
<div class="sl-color-picker__input sl-color-picker__input--alpha">
<label>A</label>
<input type="number" min="0" max="100" />
<sl-input
size="small"
type="number"
min={0}
max={100}
inputmode="numeric"
value={this.opacity.toString()}
onInput={this.handleOpacityInput}
/>
</div>
</div>

View File

@@ -8,6 +8,20 @@
<!-- Auto Generated Below -->
## Dependencies
### Depends on
- [sl-input](../input)
### Graph
```mermaid
graph TD;
sl-color-picker --> sl-input
sl-input --> sl-icon
style sl-color-picker fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------

View File

@@ -43,7 +43,7 @@ export class Input {
@Prop() name = '';
/** The input's value attribute. */
@Prop({ mutable: true }) value = '';
@Prop({ mutable: true }) value: string = '';
/** The input's placeholder text. */
@Prop() placeholder: string;

View File

@@ -200,6 +200,10 @@ Type: `Promise<void>`
## Dependencies
### Used by
- [sl-color-picker](../color-picker)
### Depends on
- [sl-icon](../icon)
@@ -208,6 +212,7 @@ Type: `Promise<void>`
```mermaid
graph TD;
sl-input --> sl-icon
sl-color-picker --> sl-input
style sl-input fill:#f9f,stroke:#333,stroke-width:4px
```

8
src/utilities/math.ts Normal file
View File

@@ -0,0 +1,8 @@
//
// Ensures a number stays within a minimum and maximum value
//
export function clamp(value: number, min: number, max: number) {
if (value < min) return min;
if (value > max) return max;
return value;
}