Various changes around tweaks

This commit is contained in:
Lea Verou
2025-03-14 16:33:14 -04:00
parent bb24db30b5
commit 12c5747cd2
4 changed files with 244 additions and 112 deletions

View File

@@ -16,7 +16,7 @@
<div id="palette-app" data-slug="{{ page.fileSlug }}" data-palette-id="{{ page.fileSlug }}">
<div
:class="{
seeded: seedColorObjects.length,
seeded: isSeeded,
tweaking: tweaking.chromaScale,
'tweaking-chroma': tweaking.chromaScale,
'tweaking-hue': tweaking.chromaScale,
@@ -115,7 +115,7 @@
</thead>
{% raw %}
<tbody v-cloak>
<tr v-for="hue in [...hues, 'gray']" v-show="hue === 'gray' || seedHues[hue] || !isCustom" :data-hue="hue"
<tr v-for="hue in paletteScalesList" :data-hue="hue" :key="hue"
class="color-scale" :class="{
tweaking: hue === 'gray' ? tweaking.grayChroma : tweaking.hue?.[hue],
tweaked: hue === 'gray' ? tweaked.grayChroma || tweaked.grayColor : hueShifts[hue],
@@ -148,7 +148,7 @@
Gray undertone
</div>
</wa-radio-group>
<color-slider class="gray-chroma-slider" type="percentage" :default-value="originalGrayChroma"
<color-slider class="gray-chroma-slider" type="scale" :default-value="originalGrayChroma"
:model-value="computedGrayChroma"
@update:model-value="grayChroma = $event"
v-model:tweaking="tweaking.grayChroma"
@@ -157,16 +157,30 @@
label="Gray colorfulness" label-min="Neutral" :label-max="moreHue[computedGrayColor]"
></color-slider>
</template>
<color-slider v-else class="hue-shift-slider"
:model-value="tweak('hueShift', {hue, level: 'core'})"
@update:model-value="tweak('hueShift', {hue, level: 'core'}, $event)"
:default-value="tweakDefault('hueShift', {hue, level: 'core'})"
<template v-else>
<color-slider v-if="isCustom && seedColors[colorToIndex[hue].core]"
v-model:color="seedColors[colorToIndex[hue].core].color"
:default-value="seedColors[colorToIndex[hue].core].inputColor.oklch.h"
v-model:tweaking="tweaking.hue[hue]"
class="hue-shift-slider"
color-component="oklch.h"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
:get-color="h => baseCoreColors[hue].clone().set('oklch.h', h)"
:label="`Tweak ${ hue } hue`" :label-min="`More ${hueBefore[hue]}`" :label-max="`More ${hueAfter[hue]}`"
></color-slider>
<color-slider v-if="!isCustom && baseCoreColors[hue]"
type="shift"
v-model="hueShifts[hue]"
:default-color="baseCoreColors[hue]"
v-model:tweaking="tweaking.hue[hue]"
class="hue-shift-slider"
color-component="oklch.h"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
:label="`Tweak ${ hue } hue`" :label-min="`More ${hueBefore[hue]}`" :label-max="`More ${hueAfter[hue]}`"
></color-slider>
</template>
<div class="wa-gap-s">
<code>--wa-color-{{ hue }}</code>
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
@@ -190,7 +204,7 @@
<color-slider
class="chroma-scale-slider wa-palette-{{ paletteId }}" :class="{ tweaked: chromaScale !== 1 }"
type="percentage" :default-value="1"
type="scale" :default-value="1"
v-model="chromaScale"
v-model:tweaking="tweaking.chromaScale"
:min="chromaScaleBounds.min" :max="chromaScaleBounds.max" :step="0.01"

View File

@@ -1,6 +1,7 @@
// TODO move these to local imports
import Color from 'https://colorjs.io/dist/color.js';
import { createApp, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
// import { createApp, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { createApp, nextTick } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
import generatePalette from './color/generate-palette.js';
import getPaletteCode from './color/get-palette-code.js';
import allPalettes from './color/palettes.js';
@@ -124,7 +125,7 @@ let paletteAppSpec = {
*/
step() {
if (this.isCustom) {
if (this.seedColorObjects.length > 0) {
if (this.isSeeded) {
return 2;
} else if (this.seedColors.length > 0) {
return 1;
@@ -248,14 +249,15 @@ let paletteAppSpec = {
},
seedColorObjects() {
return this.seedColors.map(c => c.color).filter(Boolean);
return this.seedColors.map(c => c.color);
},
isSeeded() {
return this.seedColorObjectsRaw.filter(Boolean).length > 0;
},
seedColorInfo() {
return this.seedColors.map(({ hue, level }) => ({ hue, level }));
// return this.seedColorObjectsRaw.map(colorObject =>
// colorObject ? identifyColor(colorObject, this.seedColorObjects) : null,
// );
},
/**
@@ -268,7 +270,7 @@ let paletteAppSpec = {
ret[hue] = {};
}
if (this.seedColorObjects.length === 0) {
if (!this.isSeeded) {
return ret;
}
@@ -365,8 +367,12 @@ let paletteAppSpec = {
return ret;
},
paletteScalesList() {
return Object.keys(this.paletteScales);
},
paletteScalesSet() {
return new Set(Object.keys(this.paletteScales));
return new Set(this.paletteScalesList);
},
tweaks() {
@@ -400,7 +406,7 @@ let paletteAppSpec = {
},
baseColors() {
if (this.seedColorObjects.length === 0) {
if (!this.isSeeded) {
return this.originalColors;
}
@@ -866,60 +872,6 @@ let paletteAppSpec = {
return { color, index };
},
tweak(type, ref, value) {
let { color, index } = this.getColor(ref);
if (color === undefined) {
return;
}
if (type === 'hueShift') {
if (this.isCustom) {
if (index === undefined) {
return;
}
if (value === undefined) {
return color.get('oklch.h');
} else {
// console.log(`About to set h of ${color} to ${value}`);
color.set('oklch.h', value);
}
} else {
let { hue, level } = ref;
let color = this.baseColors[hue][level];
let h = color.get('oklch.h');
if (value === undefined) {
// Get
return h + this.hueShifts[hue];
} else {
// Set
let { hue } = ref;
this.hueShifts[hue] = value - h;
}
}
}
},
tweakDefault(type, ref) {
if (type === 'hueShift') {
if (this.isCustom) {
let { index } = this.getColor(ref);
if (index === undefined || !this.seedColors[index]) {
return;
}
let { inputColor } = this.seedColors[index];
return inputColor?.get('oklch.h');
} else {
return 0;
}
}
},
},
directives: {

View File

@@ -1,5 +1,6 @@
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://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 { identifyColor, stringifyColor } from '../color/util.js';
import { ROLES } from '/assets/scripts/tweak/data.js';
import { capitalize } from '/assets/scripts/tweak/util.js';
@@ -49,6 +50,7 @@ export default {
inputColor,
editing: 0,
inputFocused: false,
watching: {},
};
},
computed: {
@@ -129,10 +131,9 @@ export default {
this.editing++;
let value = event.target.value;
// Editing the input manually also incorporates tweaks as part of the color itself
// Editing the input manually also incorporates any tweaks as part of the color itself
// I.e. input color and color are now the same
this.valueRaw = this.modelValue.valueRaw = this.modelValue.inputValueRaw = this.inputValueRaw = value;
this.$emit('update:modelValue', this.modelValue);
this.valueRaw = this.inputValueRaw = value;
nextTick().then(() => {
if (this.colorRaw) {
@@ -147,12 +148,44 @@ export default {
});
},
mutateModelValue(mutator) {
if (this.watching.modelValue === null) {
// If we're not watching modelValue, it means we're reacting to a change to it
// so no point in updating it again
return;
}
if (this.watching.modelValue) {
this.watching.modelValue();
this.watching.modelValue = null;
}
mutator();
this.watching.modelValue = this.$watch('modelValue', {
deep: true,
handler() {
let computedValue = this.computedValue;
// What changed?
if (this.modelValue.value !== computedValue.value) {
this.valueRaw = this.modelValue.value;
}
if (this.modelValue.color + '' !== computedValue.color + '') {
this.color = this.modelValue.color;
}
},
});
},
revert() {
this.$emit('update:modelValue', this.inputValue);
this.$emit('update:color', this.inputColor);
},
},
watch: {
/** colorRaw -> color */
colorRaw: {
deep: true,
handler() {
@@ -161,6 +194,7 @@ export default {
}
},
},
/** inputColorRaw -> inputColor */
inputColorRaw: {
deep: true,
handler() {
@@ -169,22 +203,25 @@ export default {
}
},
},
/** color -> value, valueRaw, modelValue.value */
color: {
deep: true,
handler() {
if (this.tweaked && this.color) {
// If tweaked, color is the source of truth
this.value = this.valueRaw = this.modelValue.value = this.color + '';
this.$emit('update:modelValue', this.modelValue);
this.value = this.valueRaw = this.color + '';
}
},
},
/** computedValue -> modelValue */
computedValue: {
deep: true,
immediate: true,
handler() {
Object.assign(this.modelValue, this.computedValue);
this.$emit('update:modelValue', this.modelValue);
this.mutateModelValue(() => {
Object.assign(this.modelValue, this.computedValue);
this.$emit('update:modelValue', this.modelValue);
});
},
},
},

View File

@@ -1,26 +1,36 @@
import Color from 'https://colorjs.io/dist/color.js';
import { capitalize } from '/assets/scripts/tweak/util.js';
import { capitalize, clamp } from '/assets/scripts/tweak/util.js';
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
export default {
props: {
colorComponent: String,
defaultColor: {
type: Color,
},
defaultValue: {
type: Number,
},
modelValue: {
type: Number,
default(rawProps) {
return rawProps.defaultValue ?? 0;
return rawProps.defaultValue;
},
},
min: Number,
max: Number,
step: {
type: Number,
default: 1,
default(rawProps) {
return clamp(0, Math.abs((rawProps.max - rawProps.min) / 100), 1);
},
},
defaultValue: Number,
type: {
type: String,
default: 'number',
default: 'raw',
},
getColor: {
@@ -29,7 +39,11 @@ export default {
color: {
type: Color,
default(rawProps) {
return rawProps.getColor(rawProps.modelValue);
if (rawProps.defaultColor) {
return rawProps.defaultColor;
}
return rawProps.getColor(getValue(rawProps), rawProps.modelValue);
},
},
@@ -41,56 +55,171 @@ export default {
},
emits: ['update:modelValue', 'update:tweaking', 'update:color'],
data() {
return {};
let { type, modelValue, defaultValue } = this;
let value = getValue({ type, modelValue, defaultValue });
return {
initialColor: this.color,
value,
};
},
mounted() {
if (this.value === undefined) {
this.value = this.computedDefaultValue;
}
if (this.$refs.slider) {
if (this.type === 'scale') {
this.$refs.slider.tooltipFormatter = percentFormatter;
}
this.$refs.slider.colorSliderData = this; // for debugging
}
},
beforeUnmount() {
delete this.$refs.slider?.colorSliderData;
},
computed: {
colorCurrent() {
return this.getColor(this.modelValue);
return this.getColorAt(this.value, this.modelValue) ?? this.initialColor;
},
colorCurrentString() {
return this.colorCurrent + '';
},
h() {
return this.colorCurrent?.get('oklch.h') ?? this.initialColor?.get('oklch.h');
},
c() {
return this.colorCurrent?.get('oklch.c') ?? this.initialColor?.get('oklch.c');
},
l() {
return this.colorCurrent?.get('oklch.l') ?? this.initialColor?.get('oklch.l');
},
colorMin() {
return this.getColor(this.min);
return this.getColorAt(this.min);
},
colorMax() {
return this.getColor(this.max);
return this.getColorAt(this.max);
},
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);
},
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);
},
},
mounted() {
if (this.$refs.slider && this.type === 'percentage') {
this.$refs.slider.tooltipFormatter = percentFormatter;
}
},
methods: {
capitalize,
getColorAt(value) {
if (this.getColor) {
return this.getColor(value, this.modelValue);
}
if (this.computedDefaultColor && this.colorComponent) {
return this.computedDefaultColor.clone().set(this.colorComponent, value);
}
},
handleInput(event) {
let value = (this.value = event.target.value);
let modelValue = getModelValue({ type: this.type, value, defaultValue: this.computedDefaultValue });
this.$emit('update:tweaking', true);
this.$emit('update:modelValue', event.target.value);
this.$emit('update:modelValue', modelValue);
},
reset() {
let { value, type, defaultValue } = this;
this.$emit('update:modelValue', getModelValue({ value, type, defaultValue }));
},
},
watch: {
colorCurrent() {
colorCurrentString() {
this.$emit('update:color', this.colorCurrent);
},
},
template: `
<div class="decorated-slider" :style="{
'--color': colorCurrent,
'--color-1': colorMin,
'--color-2': colorMax,
}">
<wa-slider ref="slider" :min="min" :max="max" :step="step"
@change="$emit('update:tweaking', false)" :value="modelValue" @input="handleInput">
<div slot="label">
{{ label }}
<wa-icon-button @click="$emit('update:modelValue', defaultValue)" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">{{ labelMin }}</div>
<div class="label-max">{{ labelMax }}</div>
</div>
<div class="decorated-slider"
:style="{'--color': colorCurrent, '--color-1': colorMin, '--color-2': colorMax}">
<wa-slider ref="slider" :min="min" :max="max" :step="step" :value="value"
@change="$emit('update:tweaking', false)" @input="handleInput">
<div slot="label">
{{ label }}
<wa-icon-button @click="reset" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">{{ labelMin }}</div>
<div class="label-max">{{ labelMax }}</div>
</div>
`,
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};
function getValue({ type, modelValue, defaultValue }) {
if (defaultValue === undefined && (type === 'shift' || type === 'scale')) {
return undefined;
}
if (type === 'shift') {
return modelValue + defaultValue;
}
if (type === 'scale') {
return modelValue * defaultValue;
}
return modelValue;
}
function getModelValue({ value, type, defaultValue }) {
if (defaultValue === undefined && (type === 'shift' || type === 'scale')) {
return undefined;
}
if (type === 'shift') {
return value - defaultValue;
}
if (type === 'scale') {
return value / defaultValue;
}
return value;
}