Simplify color-slider and fix a bunch of bugs around it

This commit is contained in:
Lea Verou
2025-03-16 20:20:01 -04:00
parent 5d97db178a
commit e9ea0b7f1c
5 changed files with 128 additions and 112 deletions

View File

@@ -146,30 +146,27 @@
Gray undertone
</div>
</wa-radio-group>
<color-slider class="gray-chroma-slider" type="scale" :default-value="originalGrayChroma"
<color-slider coord="c" type="scale" :default-value="originalGrayChroma"
:model-value="computedGrayChroma"
@update:model-value="grayChroma = $event"
:default-color="baseCoreColors[computedGrayColor]"
:min="0" :max="maxGrayChroma" :step="0.01"
:get-color="scale => baseCoreColors[computedGrayColor].clone().set('oklch.c', c => c * scale)"
label="Gray colorfulness" label-min="Neutral" :label-max="moreHue[computedGrayColor]"
></color-slider>
</template>
<template v-else>
<color-slider v-if="isCustom && seedColors[colorToIndex[hue].core]"
<color-slider v-if="isCustom && seedColors[colorToIndex[hue].core]"
coord="h"
v-model:color="seedColors[colorToIndex[hue].core].color"
:default-value="seedColors[colorToIndex[hue].core].inputColor.oklch.h"
color-component="oklch.h"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
label="Tweak hue" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
></color-slider>
<color-slider v-if="!isCustom && baseCoreColors[hue]"
type="shift"
<color-slider v-if="!isCustom && baseCoreColors[hue]"
coord="h" type="shift"
v-model="hueShifts[hue]"
:default-color="baseCoreColors[hue]"
color-component="oklch.h"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
label="Tweak hue" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
></color-slider>
@@ -192,7 +189,7 @@
@input="!seedColors[colorToIndex[hue][tint]] ? addColor({hue, level: tint}) : null"
:default-value="colors[hue][tweakBase[hue][tint]].oklch.h"
color-component="oklch.h"
coord="h"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max" :step="1"
label="Hue shift" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
:label-default="`${capitalize(hue)} ${tweakBase[hue][tint]}`"
@@ -205,12 +202,13 @@
</tbody>
</table>
<color-slider v-if="!isCustom"
class="chroma-scale-slider wa-palette-{{ paletteId }}" :class="{ tweaked: chromaScale !== 1 }"
type="scale" :default-value="1"
<color-slider v-if="!isCustom" :class="{ tweaked: chromaScale !== 1 }"
type="scale"
v-model="chromaScale"
:min="chromaScaleBounds.min" :max="chromaScaleBounds.max" :step="0.01"
:get-color="scale => baseCoreColors.blue.clone().set('oklch.c', c => c * scale)"
coord="c"
:default-color="baseMaxChromaColor"
:default-value="baseMaxChroma"
:min="MAX_CHROMA_BOUNDS.min" :max="MAX_CHROMA_BOUNDS.max" :step="0.01"
label="Overall colorfulness" label-min="More muted" label-max="More vibrant"
></color-slider>

View File

@@ -84,7 +84,7 @@ wa-dropdown > .color.swatch {
}
}
[data-component='oklch.h'] {
[data-component='h'] {
--color-interpolation-space: oklch increasing hue;
}

View File

@@ -494,12 +494,27 @@ let paletteAppSpec = {
return ret;
},
baseMaxChromaHue() {
let maxChroma = -1;
let maxChromaHue = null;
for (let hue in this.baseCoreColors) {
let color = this.baseCoreColors[hue];
let chroma = color.get('oklch.c');
if (chroma > maxChroma || !maxChromaHue) {
maxChroma = chroma;
maxChromaHue = hue;
}
}
return maxChromaHue;
},
baseMaxChromaColor() {
return this.baseCoreColors[this.baseMaxChromaHue];
},
baseMaxChroma() {
return Math.max(
...Object.values(this.baseCoreColors)
.map(color => color.get('oklch.c'))
.filter(c => c >= 0),
);
return this.baseMaxChromaColor.get('oklch.c');
},
coreColors() {

View File

@@ -7,13 +7,13 @@ const template = `
<wa-icon-button name="sliders-simple" class="tweak-icon"></wa-icon-button>
<template #content>
<color-slider label="Hue" label-default="Entered color"
color-component="oklch.h" :min="0" :max="359" :step="1"
coord="h" :min="0" :max="359" :step="1"
v-model:color="color" :default-value="inputLCH[2]" ></color-slider>
<color-slider label="Colorfulness" label-default="Entered color"
color-component="oklch.c" :min="0" :max="maxChroma" :step="0.001"
coord="c" :min="0" :max="maxChroma" :step="0.001"
v-model:color="color" :default-value="inputLCH[1]" format-type="scale" :base-value="maxChroma" ></color-slider>
<color-slider label="Lightness" label-default="Entered color"
color-component="oklch.l" :min="0" :max="1" :step="0.01"
coord="l" :min="0" :max="1" :step="0.01"
v-model:color="color" :default-value="inputLCH[0]" format-type="scale" :base-value="1" ></color-slider>
</template>
</color-popup>
@@ -35,7 +35,7 @@ import Color from 'https://colorjs.io/dist/color.js';
// import { nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { nextTick } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
import getMaxChroma from '../color/get-max-chroma.js';
import { identifyColor, stringifyColor } from '../color/util.js';
import { identifyColor } from '../color/util.js';
import ColorPopup from './color-popup.js';
import ColorSlider from './color-slider.js';
import InfoTip from './info-tip.js';

View File

@@ -1,10 +1,10 @@
const template = `
<div class="color-slider" :style="{
'--color': colorCurrent, '--color-1': colorMin, '--color-2': colorMax,
'--color': computedColor, '--color-1': colorMin, '--color-2': colorMax,
'--default-value-progress': defaultProgress,
}" :data-component="colorComponent || null">
}" :data-component="coord || null">
<wa-slider ref="slider" :min="min" :max="max" :step="step" :value="value"
@input="handleInput($event.target.value); $emit('update:tweaking', true); $emit('input', $event.target.value);" @change="$emit('update:tweaking', false)">
@input="handleInput($event.target.value);" @change="inputEnd($event.target.value)">
<div slot="label">
{{ label }}
<wa-icon-button v-if="value !== computedDefaultValue" @click="reset" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
@@ -25,15 +25,22 @@ import { capitalize, clamp, promise, roundTo } from '/assets/scripts/tweak/util.
export default {
props: {
colorComponent: String,
coord: {
type: String,
required: true,
validator(value) {
return ['l', 'c', 'h'].includes(value);
},
},
color: {
type: Color,
},
defaultColor: {
type: Color,
},
defaultValue: {
type: Number,
default(rawProps) {
return rawProps.defaultColor?.get(rawProps.colorComponent);
},
},
/** Used for formatting only. Only specify if different from default value. */
baseValue: {
@@ -60,33 +67,30 @@ export default {
},
formatType: {
type: String,
default(rawProps) {
return rawProps.type ?? 'raw';
},
},
getColor: {
type: Function,
},
color: {
type: Color,
},
label: String,
labelMin: String,
labelMax: String,
labelDefault: String,
tweaking: Boolean,
},
emits: ['update:modelValue', 'update:tweaking', 'update:color', 'input'],
emits: ['update:modelValue', 'update:color', 'input'],
data() {
return {
mounted: promise(),
initialColor: this.color,
value: undefined,
tweaking: false,
};
},
created() {
if (!this.color && !this.defaultColor) {
console.warn(
`[${this.label}]`,
'<color-slider> requires at least one of the following props: color, defaultColor',
);
}
},
mounted() {
if (this.$refs.slider) {
this.$refs.slider.tooltipFormatter = value => this.formatValue(value);
@@ -96,7 +100,7 @@ export default {
this.value = getAbsoluteValue({
type: this.type,
relativeValue: this.modelValue,
baseValue: this.defaultValue,
baseValue: this.computedDefaultValue,
});
this.mounted.resolve();
@@ -105,30 +109,43 @@ export default {
delete this.$refs.slider?.colorSliderData;
},
computed: {
colorCurrent() {
return this.getColorAt(this.value, this.modelValue) ?? this.initialColor;
computedColor() {
return this.getColorAt(this.value);
},
colorCurrentString() {
return this.colorCurrent + '';
computedColorCoords() {
return this.computedColor.oklch.slice();
},
h() {
return this.colorCurrent?.get('oklch.h') ?? this.initialColor?.get('oklch.h');
colorCoords() {
let color = this.color ?? this.computedColor;
return color?.oklch.slice();
},
c() {
return this.colorCurrent?.get('oklch.c') ?? this.initialColor?.get('oklch.c');
computedColorString() {
return `oklch(${this.computedColorCoords.join(' ')})`;
},
l() {
return this.colorCurrent?.get('oklch.l') ?? this.initialColor?.get('oklch.l');
colorString() {
return `oklch(${this.colorCoords.join(' ')})`;
},
colorComponentValue() {
if (this.colorComponent) {
return this.color?.get(this.colorComponent);
defaultCoords() {
if (this.defaultColor) {
return this.defaultColor.oklch.slice();
}
let ret = this.color.oklch.slice();
if (this.defaultValue !== undefined) {
ret[this.coordIndex] = this.defaultValue;
}
return ret;
},
coordIndex() {
return ['l', 'c', 'h'].indexOf(this.coord);
},
colorMin() {
@@ -140,38 +157,11 @@ export default {
},
computedDefaultValue() {
let { defaultValue, colorComponent, defaultColor, type, min, max } = this;
if (defaultValue !== undefined) {
return defaultValue;
}
if (colorComponent && defaultColor) {
return this.computedDefaultColor.get(colorComponent);
}
return clamp(min, type === 'scale' ? 1 : 0, max);
return this.defaultValue ?? this.defaultCoords[this.coordIndex];
},
computedDefaultColor() {
if (this.defaultColor) {
return this.defaultColor;
}
let defaultValue = this.computedDefaultValue;
if (this.colorComponent && this.defaultValue !== undefined) {
switch (this.colorComponent) {
case 'oklch.l':
return new Color('oklch', [defaultValue, this.c, this.h]);
case 'oklch.c':
return new Color('oklch', [this.l, defaultValue, this.h]);
case 'oklch.h':
return new Color('oklch', [this.l, this.c, defaultValue]);
}
}
return this.getColor?.(defaultValue);
return this.defaultColor ?? this.getColorAt(this.computedDefaultValue);
},
computedLabelDefault() {
@@ -188,11 +178,12 @@ export default {
capitalize,
formatValue(value = this.value) {
let style = this.formatType === 'scale' ? 'percent' : undefined;
let formatType = this.formatType ?? this.type;
let style = formatType === 'scale' ? 'percent' : undefined;
if (this.formatType !== 'raw') {
if (formatType && formatType !== 'raw') {
let baseValue = this.baseValue ?? this.computedDefaultValue;
value = getRelativeValue({ type: this.formatType, absoluteValue: value, baseValue });
value = getRelativeValue({ type: formatType, absoluteValue: value, baseValue });
}
value = roundTo(value, this.step);
@@ -200,17 +191,15 @@ export default {
},
getColorAt(value) {
if (this.getColor) {
return this.getColor(value, this.modelValue);
}
if (this.computedDefaultColor && this.colorComponent) {
return this.computedDefaultColor.clone().set(this.colorComponent, value);
}
let coords = this.defaultCoords.slice();
coords[this.coordIndex] = value;
return new Color('oklch', coords);
},
/** Called when value changes due to user interaction */
handleInput(value) {
this.value = value;
this.tweaking = true;
let modelValue = getRelativeValue({
type: this.type,
@@ -219,31 +208,45 @@ export default {
});
this.$emit('update:modelValue', modelValue);
this.$emit('input', modelValue);
},
inputEnd() {
this.tweaking = false;
},
reset() {
this.handleInput(this.computedDefaultValue);
this.$emit('input', this.computedDefaultValue);
this.inputEnd();
},
},
watch: {
async colorCurrentString() {
if (!this.colorComponent) {
// If we're monitoring a specific color component, we can key off changes to that value
await this.$nextTick();
if (this.color + '' !== this.colorCurrentString) {
// Still different
this.$emit('update:color', this.colorCurrent);
}
computedColorString() {
if (this.color && this.colorString !== this.computedColorString) {
// Color changed, communicate to the outside world
this.$emit('update:color', this.computedColor);
}
},
async colorComponentValue() {
async colorString() {
await this.$nextTick();
if (this.value !== this.colorComponentValue) {
// Color changed externally
this.value = this.colorComponentValue;
this.$emit('update:color', this.colorCurrent);
await this.mounted;
if (this.color && this.colorString !== this.computedColorString) {
// Color changed in the outside world, update our internals
if (this.colorCoords[this.coordIndex] !== this.value) {
this.value = this.colorCoords[this.coordIndex];
await this.$nextTick();
let modelValue = getRelativeValue({
type: this.type,
absoluteValue: this.value,
baseValue: this.computedDefaultValue,
});
this.$emit('update:modelValue', modelValue);
}
}
},
},