Further cleanups to the colour picker code.

This commit is contained in:
flash 2024-01-21 05:11:00 +00:00
parent 0b816b2bd4
commit 51694538ff
7 changed files with 221 additions and 154 deletions

View file

@ -157,11 +157,13 @@
} }
.colpick-presets-option { .colpick-presets-option {
display: block; display: block;
border: 0;
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 5px; border-radius: 5px;
text-decoration: none; text-decoration: none;
color: #fff; color: #fff;
cursor: pointer;
} }
.colpick-presets-option:focus { .colpick-presets-option:focus {
box-shadow: 0 0 0 1px #fff, 0 0 0 2px #000, inset 0 0 0 1px #000; box-shadow: 0 0 0 1px #fff, 0 0 0 2px #000, inset 0 0 0 1px #000;
@ -175,6 +177,8 @@
grid-template-columns: repeat(12, minmax(0, 1fr)); grid-template-columns: repeat(12, minmax(0, 1fr));
} }
.colpick-grid-option { .colpick-grid-option {
display: block;
border: 0;
width: 23px; width: 23px;
height: 23px; height: 23px;
z-index: 1; z-index: 1;

69
src/mami.js/colour.js Normal file
View file

@ -0,0 +1,69 @@
const MamiColour = (() => {
const readThres = 168,
lumiRed = .299,
lumiGreen = .587,
lumiBlue = .114;
const pub = {};
const extractRGB = raw => [
(raw >> 16) & 0xFF,
(raw >> 8) & 0xFF,
raw & 0xFF,
];
const luminance = raw => {
const rgb = extractRGB(raw);
return rgb[0] * lumiRed
+ rgb[1] * lumiGreen
+ rgb[2] * lumiBlue;
};
const weightedNumber = (num1, num2, weight) => {
weight = Math.min(1, Math.max(0, weight));
return Math.round((num1 * weight) + (num2 * (1 - weight)));
};
const weighted = (raw1, raw2, weight) => {
const rgb1 = extractRGB(raw1),
rgb2 = extractRGB(raw2);
return (weightedNumber(rgb1[0], rgb2[0], weight) << 16)
| (weightedNumber(rgb1[1], rgb2[1], weight) << 8)
| weightedNumber(rgb1[2], rgb2[2], weight);
};
pub.extractRGB = extractRGB;
pub.weightedNumber = weightedNumber;
pub.luminance = luminance;
pub.weighted = weighted;
pub.text = raw => luminance(raw) > readThres ? 0 : 0xFFFFFF;
pub.shaded = (raw, offset) => {
if(offset == 0)
return raw;
let dir = 0xFFFFFF;
if(offset < 0) {
dir = 0;
offset *= -1;
}
return weighted(dir, raw, offset);
};
pub.hexByte = raw => {
let str = raw.toString(16).substring(0, 2);
return str.length < 2
? `0${str}` : str;
};
pub.hex = raw => {
let str = raw.toString(16).substring(0, 6);
if(str.length < 6)
str = '000000'.substring(str.length) + str;
return `#${str}`;
};
return pub;
})();

View file

@ -1,3 +1,8 @@
#include colour.js
#include colpick/tgrid.jsx
#include colpick/tpresets.jsx
#include colpick/tsliders.jsx
const MamiColourPicker = function(callback, options, colour, onClose) { const MamiColourPicker = function(callback, options, colour, onClose) {
if(typeof callback !== 'function') if(typeof callback !== 'function')
return; return;
@ -6,59 +11,6 @@ const MamiColourPicker = function(callback, options, colour, onClose) {
if(typeof colour !== 'number') if(typeof colour !== 'number')
colour = parseInt(colour || 0); colour = parseInt(colour || 0);
const readThres = 168,
lumiRed = .299,
lumiGreen = .587,
lumiBlue = .114;
const hexFormat = raw => {
let str = raw.toString(16).substring(0, 6);
if(str.length < 6)
str = '000000'.substring(str.length) + str;
return `#${str}`;
};
const extractRGB = raw => [
(raw >> 16) & 0xFF,
(raw >> 8) & 0xFF,
raw & 0xFF,
];
const calcLumi = raw => {
const rgb = extractRGB(raw);
return rgb[0] * lumiRed
+ rgb[1] * lumiGreen
+ rgb[2] * lumiBlue;
};
const textColour = raw => calcLumi(raw) > readThres ? 0 : 0xFFFFFF;
const weightNum = (n1, n2, w) => {
w = Math.min(1, Math.max(0, w));
return Math.round((n1 * w) + (n2 * (1 - w)));
};
const weightColour = (c1, c2, w) => {
c1 = extractRGB(c1);
c2 = extractRGB(c2);
return (weightNum(c1[0], c2[0], w) << 16)
| (weightNum(c1[1], c2[1], w) << 8)
| weightNum(c1[2], c2[2], w);
};
const shadeColour = (raw, offset) => {
if(offset == 0)
return raw;
let dir = 0xFFFFFF;
if(offset < 0) {
dir = 0;
offset *= -1;
}
return weightColour(dir, raw, offset);
};
const verifyOption = (name, type, def) => { const verifyOption = (name, type, def) => {
if(typeof options[name] !== type) if(typeof options[name] !== type)
options[name] = def; options[name] = def;
@ -76,34 +28,27 @@ const MamiColourPicker = function(callback, options, colour, onClose) {
const onColourChange = []; const onColourChange = [];
const runOnColourChange = () => { const runOnColourChange = () => {
const text = textColour(colour); const text = MamiColour.text(colour);
for(const handler of onColourChange) for(const handler of onColourChange)
handler(colour, text); handler(colour, text);
}; };
const setColour = raw => { const setColour = (raw, mask) => {
colour = parseInt(raw || 0) & 0xFFFFFF; raw = raw === undefined ? 0 : parseInt(raw);
mask = mask === undefined ? ~0 : parseInt(mask);
colour = (colour & ~mask) | (raw & mask);
runOnColourChange(); runOnColourChange();
}; };
const runCallback = apply => {
const result = Object.defineProperties({}, {
apply: { value: apply },
colour: { value: colour },
hex: { get() { return hexFormat(colour); } },
});
callback(result);
};
const apply = () => { const apply = () => {
runCallback(true); callback(colour);
if(options.autoClose) if(options.autoClose)
close(); close();
}; };
const cancel = () => { const cancel = () => {
runCallback(false); callback(null);
if(options.autoClose) if(options.autoClose)
close(); close();
}; };
@ -118,8 +63,8 @@ const MamiColourPicker = function(callback, options, colour, onClose) {
const container = <div class="colpick" style={{ zIndex: '9001' }}/>; const container = <div class="colpick" style={{ zIndex: '9001' }}/>;
onColourChange.push((colour, text) => { onColourChange.push((colour, text) => {
container.style.setProperty('--colpick-colour', hexFormat(colour)); container.style.setProperty('--colpick-colour', MamiColour.hex(colour));
container.style.setProperty('--colpick-text', hexFormat(text)); container.style.setProperty('--colpick-text', MamiColour.hex(text));
}); });
const form = <form class="colpick-form" onsubmit={ev => { const form = <form class="colpick-form" onsubmit={ev => {
@ -155,8 +100,11 @@ const MamiColourPicker = function(callback, options, colour, onClose) {
} }
}; };
const createTab = (id, name, construct) => { const createTab = (id, name, tabInfo) => {
const tabContainer = construct(); tabInfo.onUpdate(setColour);
onColourChange.push(value => tabInfo.updateColour(value));
const tabContainer = tabInfo.getElement();
tabContainer.className = `colpick-tab-container colpick-tab-${id}-container colpick-tab-container-inactive`; tabContainer.className = `colpick-tab-container colpick-tab-${id}-container colpick-tab-container-inactive`;
tabbedContainer.appendChild(tabContainer); tabbedContainer.appendChild(tabContainer);
@ -173,88 +121,13 @@ const MamiColourPicker = function(callback, options, colour, onClose) {
}; };
if(options.showPresetsTab) if(options.showPresetsTab)
createTab('presets', 'Presets', () => { createTab('presets', 'Presets', new MamiColourPickerPresetsTab(options.presets));
const presets = options.presets;
const cont = <div/>;
for(const preset of presets) {
const option = <a href="javascript:void(0);" class="colpick-presets-option" style={{ background: hexFormat(preset.c) }} title={preset.n} onclick={() => setColour(preset.c)}/>;
onColourChange.push(value => option.classList.toggle('colpick-presets-option-active', value === preset.c));
cont.appendChild(option);
}
return cont;
});
if(options.showGridTab) if(options.showGridTab)
createTab('grid', 'Grid', () => { createTab('grid', 'Grid', new MamiColourPickerGridTab());
const greys = [0xFFFFFF, 0xEBEBEB, 0xD6D6D6, 0xC2C2C2, 0xADADAD, 0x999999, 0x858585, 0x707070, 0x5C5C5C, 0x474747, 0x333333, 0];
const colours = [0x00A1D8, 0x0061FE, 0x4D22B2, 0x982ABC, 0xB92D5D, 0xFF4015, 0xFF6A00, 0xFFAB01, 0xFDC700, 0xFEFB41, 0xD9EC37, 0x76BB40];
const shades = [-.675, -.499, -.345, -.134, 0, .134, .345, .499, .675];
const cont = <div/>;
for(const grey of greys) {
const option = <a href="javascript:void(0);" class="colpick-grid-option" style={{ background: hexFormat(grey) }} onclick={() => setColour(grey)}/>;
onColourChange.push(value => option.classList.toggle('colpick-grid-option-active', value === grey));
cont.appendChild(option);
}
for(const shade of shades)
for(const colour of colours) {
const shaded = shadeColour(colour, shade);
const option = <a href="javascript:void(0);" class="colpick-grid-option" style={{ background: hexFormat(shaded) }} onclick={() => setColour(shaded)}/>;
onColourChange.push(value => option.classList.toggle('colpick-grid-option-active', value === shaded));
cont.appendChild(option);
}
return cont;
});
if(options.showSlidersTab) if(options.showSlidersTab)
createTab('sliders', 'Sliders', () => { createTab('sliders', 'Sliders', new MamiColourPickerSlidersTab());
const cont = <div/>;
const addSlider = (id, name, update, apply) => {
let sGradient, sValueSlider, sValueInput;
const sCont = <div class={['colpick-slider', `colpick-slider-${id}`]}>
<div class="colpick-slider-name">{name}</div>
<div class="colpick-slider-value">
<div class="colpick-slider-value-slider-container">
{sGradient = <div class="colpick-slider-gradient" onmousedown={ev => { if(ev.button === 0) setColour(apply(colour, Math.floor(255 * (Math.max(0, Math.min(200, (ev.layerX - 5))) / 200)))) }}/>}
{sValueSlider = <input class="colpick-slider-value-slider" type="range" min="0" max="255" oninput={() => setColour(apply(colour, sValueSlider.value))}/>}
</div>
<div class="colpick-slider-value-input-container">
{sValueInput = <input class="colpick-slider-value-input" type="number" min="0" max="255" oninput={() => setColour(apply(colour, sValueInput.value))}/>}
</div>
</div>
</div>;
cont.appendChild(sCont);
onColourChange.push(colour => {
sCont.style.setProperty('--colpick-slider-colour', hexFormat(apply(0, update(colour))));
sValueSlider.value = update(colour);
sValueInput.value = update(colour);
sGradient.style.background = `linear-gradient(to right, ${hexFormat(apply(colour, 0))}, ${hexFormat(apply(colour, 0xFF))})`;
});
};
addSlider('red', 'Red',
value => (value >> 16) & 0xFF,
(colour, value) => (colour & 0xFFFF) | (Math.min(255, Math.max(0, value)) << 16)
);
addSlider('green', 'Green',
value => (value >> 8) & 0xFF,
(colour, value) => (colour & 0xFF00FF) | (Math.min(255, Math.max(0, value)) << 8)
);
addSlider('blue', 'Blue',
value => value & 0xFF,
(colour, value) => (colour & 0xFFFF00) | Math.min(255, Math.max(0, value))
);
return cont;
});
if(activeTab) { if(activeTab) {
height += 261; height += 261;
@ -285,7 +158,7 @@ const MamiColourPicker = function(callback, options, colour, onClose) {
}; };
if(options.showHexValue) if(options.showHexValue)
addValue('hex', 'Hex', 'text', value => hexFormat(value), value => { addValue('hex', 'Hex', 'text', value => MamiColour.hex(value), value => {
while(value.substring(0, 1) === '#') while(value.substring(0, 1) === '#')
value = value.substring(1); value = value.substring(1);
value = value.substring(0, 6); value = value.substring(0, 6);

View file

@ -0,0 +1,38 @@
#include colour.js
const MamiColourPickerGridTab = function() {
let onUpdate = () => {};
const greys = [0xFFFFFF, 0xEBEBEB, 0xD6D6D6, 0xC2C2C2, 0xADADAD, 0x999999, 0x858585, 0x707070, 0x5C5C5C, 0x474747, 0x333333, 0];
const colours = [0x00A1D8, 0x0061FE, 0x4D22B2, 0x982ABC, 0xB92D5D, 0xFF4015, 0xFF6A00, 0xFFAB01, 0xFDC700, 0xFEFB41, 0xD9EC37, 0x76BB40];
const shades = [-.675, -.499, -.345, -.134, 0, .134, .345, .499, .675];
const html = <div/>;
for(const grey of greys)
html.appendChild(<button class={['colpick-grid-option', `colpick-grid-option-${grey}`]}
style={{ background: MamiColour.hex(grey) }}
type="button" onclick={() => onUpdate(grey)}/>);
for(const shade of shades)
for(const colour of colours) {
const shaded = MamiColour.shaded(colour, shade);
html.appendChild(<button class={['colpick-grid-option', `colpick-grid-option-${shaded}`]}
style={{ background: MamiColour.hex(shaded) }}
type="button" onclick={() => onUpdate(shaded)}/>);
}
return {
getElement: () => html,
onUpdate: handler => onUpdate = handler,
updateColour: colour => {
let elem = html.querySelector('.colpick-grid-option-active');
if(elem !== null)
elem.classList.remove('colpick-grid-option-active');
elem = html.querySelector(`.colpick-grid-option-${colour}`);
if(elem !== null)
elem.classList.add('colpick-grid-option-active');
},
};
};

View file

@ -0,0 +1,26 @@
#include colour.js
const MamiColourPickerPresetsTab = function(presets) {
let onUpdate = () => {};
const html = <div/>;
for(const preset of presets)
html.appendChild(<button class={['colpick-presets-option', `colpick-presets-option-${preset.c}`]}
style={{ background: MamiColour.hex(preset.c) }} title={preset.n}
type="button" onclick={() => onUpdate(preset.c)}/>);
return {
getElement: () => html,
onUpdate: handler => onUpdate = handler,
updateColour: colour => {
let elem = html.querySelector('.colpick-presets-option-active');
if(elem !== null)
elem.classList.remove('colpick-presets-option-active');
elem = html.querySelector(`.colpick-presets-option-${colour}`);
if(elem !== null)
elem.classList.add('colpick-presets-option-active');
},
};
};

View file

@ -0,0 +1,56 @@
#include colour.js
const MamiColourPickerSlider = function(name, title, mask, shift) {
let onUpdate = () => {};
let gradient, slider, input;
const html = <div class={['colpick-slider', `colpick-slider-${name}`]}>
<div class="colpick-slider-name">{title}</div>
<div class="colpick-slider-value">
<div class="colpick-slider-value-slider-container">
{gradient = <div class="colpick-slider-gradient" onmousedown={ev => { if(ev.button === 0) onUpdate(Math.floor(255 * (Math.max(0, Math.min(200, (ev.layerX - 5))) / 200)) << shift, mask); }}/>}
{slider = <input class="colpick-slider-value-slider" type="range" min="0" max="255" oninput={() => onUpdate(slider.value << shift, mask)}/>}
</div>
<div class="colpick-slider-value-input-container">
{input = <input class="colpick-slider-value-input" type="number" min="0" max="255" oninput={() => onUpdate(input.value << shift, mask)}/>}
</div>
</div>
</div>;
return {
getElement: () => html,
onUpdate: handler => onUpdate = handler,
updateColour: colour => {
const masked = colour & mask,
shifted = masked >> shift,
inverted = colour & ~mask;
html.style.setProperty('--colpick-slider-colour', MamiColour.hex(masked));
slider.value = shifted;
input.value = shifted;
gradient.style.background = `linear-gradient(90deg, ${MamiColour.hex(inverted)}, ${MamiColour.hex(inverted | (0xFF << shift))})`;
},
};
};
const MamiColourPickerSlidersTab = function() {
const sliders = [
new MamiColourPickerSlider('red', 'Red', 0xFF0000, 16),
new MamiColourPickerSlider('green', 'Green', 0xFF00, 8),
new MamiColourPickerSlider('blue', 'Blue', 0xFF, 0),
];
const html = <div>{...sliders}</div>;
return {
getElement: () => html,
onUpdate: handler => {
for(const slider of sliders)
slider.onUpdate(handler);
},
updateColour: colour => {
for(const slider of sliders)
slider.updateColour(colour);
},
};
};

View file

@ -1,3 +1,4 @@
#include colour.js
#include common.js #include common.js
#include utility.js #include utility.js
#include colpick/picker.jsx #include colpick/picker.jsx
@ -23,9 +24,9 @@ Umi.UI.Markup = (function() {
elem.dataset.umiPickerVisible = 'yes'; elem.dataset.umiPickerVisible = 'yes';
const picker = new MamiColourPicker( const picker = new MamiColourPicker(
function(result) { function(colour) {
if(result.apply) if(colour !== null)
insertRaw(`[color=${result.hex}]`, '[/color]'); insertRaw(`[color=${MamiColour.hex(colour)}]`, '[/color]');
}, { presets: futami.get('colours') }, null, function() { }, { presets: futami.get('colours') }, null, function() {
elem.dataset.umiPickerVisible = 'no'; elem.dataset.umiPickerVisible = 'no';
} }