sidebar: color picker: move color funcs to separate file (kind of)

This commit is contained in:
end-4 2024-02-16 16:46:25 +07:00
parent dc06bed4f8
commit 6f884ae92f
2 changed files with 309 additions and 128 deletions

View file

@ -0,0 +1,198 @@
const { Gio, GLib } = imports.gi;
import Service from 'resource:///com/github/Aylur/ags/service.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { exec, execAsync } = Utils;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
export class ColorPickerSelection extends Service {
static {
Service.register(this, {
'picked': [],
'assigned': ['int'],
'hue': [],
'sl': [],
});
}
_hue = 198;
_xAxis = 94;
_yAxis = 80;
get hue() { return this._hue; }
set hue(value) {
this._hue = clamp(value, 0, 360);
this.emit('hue');
this.emit('picked');
this.emit('changed');
}
get xAxis() { return this._xAxis; }
set xAxis(value) {
this._xAxis = clamp(value, 0, 100);
this.emit('sl');
this.emit('picked');
this.emit('changed');
}
get yAxis() { return this._yAxis; }
set yAxis(value) {
this._yAxis = clamp(value, 0, 100);
this.emit('sl');
this.emit('picked');
this.emit('changed');
}
setColorFromHex(hexString, id) {
const hsl = hexToHSL(hexString);
this._hue = hsl.hue;
this._xAxis = hsl.saturation;
// this._yAxis = hsl.lightness;
this._yAxis = (100 - hsl.saturation / 2) / 100 * hsl.lightness;
// console.log(this._hue, this._xAxis, this._yAxis)
this.emit('assigned', id);
this.emit('changed');
}
constructor() {
super();
this.emit('changed');
}
}
export function hslToRgbValues(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const to255 = x => Math.round(x * 255);
r = to255(r);
g = to255(g);
b = to255(b);
return `${Math.round(r)},${Math.round(g)},${Math.round(b)}`;
// return `rgb(${r},${g},${b})`;
}
export function hslToHex(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = x => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// export function hexToHSL(hex) {
// // Remove the '#' if present
// hex = hex.replace(/^#/, '');
// // Parse the hex value into RGB components
// const bigint = parseInt(hex, 16);
// const r = (bigint >> 16) & 255;
// const g = (bigint >> 8) & 255;
// const b = bigint & 255;
// // Normalize RGB values to range [0, 1]
// const normalizedR = r / 255;
// const normalizedG = g / 255;
// const normalizedB = b / 255;
// // Find the maximum and minimum values
// const max = Math.max(normalizedR, normalizedG, normalizedB);
// const min = Math.min(normalizedR, normalizedG, normalizedB);
// // Calculate the lightness
// const lightness = (max + min) / 2;
// // If the color is grayscale, set saturation to 0
// if (max === min) {
// return {
// hue: 0,
// saturation: 0,
// lightness: lightness * 100 // Convert to percentage
// };
// }
// // Calculate the saturation
// const d = max - min;
// const saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min);
// // Calculate the hue
// let hue;
// if (max === normalizedR) {
// hue = ((normalizedG - normalizedB) / d + (normalizedG < normalizedB ? 6 : 0)) * 60;
// } else if (max === normalizedG) {
// hue = ((normalizedB - normalizedR) / d + 2) * 60;
// } else {
// hue = ((normalizedR - normalizedG) / d + 4) * 60;
// }
// return {
// hue: Math.round(hue),
// saturation: Math.round(saturation * 100), // Convert to percentage
// lightness: Math.round(lightness * 100) // Convert to percentage
// };
// }
export function hexToHSL(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
var r = parseInt(result[1], 16);
var g = parseInt(result[2], 16);
var b = parseInt(result[3], 16);
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
s = s * 100;
s = Math.round(s);
l = l * 100;
l = Math.round(l);
h = Math.round(360 * h);
return {
hue: h,
saturation: s,
lightness: l
};
}

View file

@ -1,3 +1,4 @@
// TODO: Make selection update when entry changes
const { Gtk } = imports.gi;
import App from 'resource:///com/github/Aylur/ags/app.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
@ -9,76 +10,21 @@ import SidebarModule from './module.js';
import { MaterialIcon } from '../../../lib/materialicon.js';
import { setupCursorHover } from '../../../lib/cursorhover.js';
import { ColorPickerSelection, hslToHex, hslToRgbValues, hexToHSL } from './color.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
function hslToRgbValues(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const to255 = x => Math.round(x * 255);
r = to255(r);
g = to255(g);
b = to255(b);
return `${Math.round(r)},${Math.round(g)},${Math.round(b)}`;
// return `rgb(${r},${g},${b})`;
}
function hslToHex(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = x => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
export default () => {
const hue = Variable(198);
const xAxis = Variable(94);
const yAxis = Variable(80);
const alpha = Variable(1);
const selectedColor = new ColorPickerSelection();
function shouldUseBlackColor() {
return ((xAxis.value < 40 || (45 <= hue.value && hue.value <= 195)) &&
yAxis.value > 60);
return ((selectedColor.xAxis < 40 || (45 <= selectedColor.hue && selectedColor.hue <= 195)) &&
selectedColor.yAxis > 60);
}
const colorBlack = 'rgba(0,0,0,0.9)';
const colorWhite = 'rgba(255,255,255,0.9)';
const colorRed = '#be2222';
const colorGreen = '#51b932';
const colorBlue = '#2f87c2';
const hueRange = Box({
homogeneous: true,
className: 'sidebar-module-colorpicker-wrapper',
@ -90,13 +36,14 @@ export default () => {
const hueSlider = Box({
vpack: 'start',
className: 'sidebar-module-colorpicker-cursorwrapper',
css: `margin-top: ${13.636 * selectedColor.hue / 360}rem;`,
homogeneous: true,
children: [Box({
className: 'sidebar-module-colorpicker-hue-cursor',
})],
setup: (self) => self.hook(hue, () => {
setup: (self) => self.hook(selectedColor, () => {
const widgetHeight = hueRange.children[0].get_allocated_height();
self.setCss(`margin-top: ${widgetHeight * hue.value / 360}px;`)
self.setCss(`margin-top: ${13.636 * selectedColor.hue / 360}rem;`)
}),
});
const hueSelector = Box({
@ -111,7 +58,7 @@ export default () => {
const widgetHeight = hueRange.children[0].get_allocated_height();
const [_, cursorX, cursorY] = event.get_coords();
const cursorYPercent = clamp(cursorY / widgetHeight, 0, 1);
hue.value = Math.round(cursorYPercent * 360);
selectedColor.hue = Math.round(cursorYPercent * 360);
}
},
setup: (self) => self
@ -132,13 +79,19 @@ export default () => {
homogeneous: true,
children: [Box({
className: 'sidebar-module-colorpicker-saturationandlightness',
setup: (self) => self.hook(hue, () => {
// css: `background: linear-gradient(to right, #ffffff, color);`,
self.setCss(`background:
attribute: {
update: (self) => {
// css: `background: linear-gradient(to right, #ffffff, color);`,
self.setCss(`background:
linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1)),
linear-gradient(to right, #ffffff, ${hslToHex(hue.value, 100, 50)});
`);
}),
linear-gradient(to right, #ffffff, ${hslToHex(selectedColor.hue, 100, 50)});
`);
},
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
})],
});
const saturationAndLightnessCursor = Box({
@ -148,40 +101,42 @@ export default () => {
hpack: 'start',
homogeneous: true,
css: `
margin-left: ${13.636 * xAxis.value / 100}rem;
margin-top: ${13.636 * (100 - yAxis.value) / 100}rem;
margin-left: ${13.636 * selectedColor.xAxis / 100}rem;
margin-top: ${13.636 * (100 - selectedColor.yAxis) / 100}rem;
`, // Why 13.636rem? see class name in stylesheet
attribute: {
update: (self) => {
const allocation = saturationAndLightnessRange.children[0].get_allocation();
self.setCss(`
margin-left: ${13.636 * selectedColor.xAxis / 100}rem;
margin-top: ${13.636 * (100 - selectedColor.yAxis) / 100}rem;
`); // Why 13.636rem? see class name in stylesheet
}
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'assigned')
,
children: [Box({
className: 'sidebar-module-colorpicker-saturationandlightness-cursor',
css: `
background-color: ${hslToHex(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100))};
background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};
border-color: ${shouldUseBlackColor() ? colorBlack : colorWhite};
`,
attribute: {
updateCursorColor: (self) => {
update: (self) => {
self.setCss(`
background-color: ${hslToHex(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100))};
background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};
border-color: ${shouldUseBlackColor() ? colorBlack : colorWhite};
`);
}
},
setup: (self) => self
.hook(yAxis, (self) => self.attribute.updateCursorColor(self))
.hook(hue, (self) => self.attribute.updateCursorColor(self))
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
})],
attribute: {
update: (self) => {
const allocation = saturationAndLightnessRange.children[0].get_allocation();
self.setCss(`
margin-left: ${13.636 * xAxis.value / 100}rem;
margin-top: ${13.636 * (100 - yAxis.value) / 100}rem;
`); // Why 13.636rem? see class name in stylesheet
}
},
setup: (self) => self.hook(yAxis, (self) => { // And saturation, but both are updated at once so we only need to connect to one
self.attribute.update(self);
}),
})]
});
const saturationAndLightnessSelector = Box({
@ -199,8 +154,8 @@ export default () => {
const [_, cursorX, cursorY] = event.get_coords();
const cursorXPercent = clamp(cursorX / allocation.width, 0, 1);
const cursorYPercent = clamp(cursorY / allocation.height, 0, 1);
xAxis.value = Math.round(cursorXPercent * 100);
yAxis.value = Math.round(100 - cursorYPercent * 100);
selectedColor.xAxis = Math.round(cursorXPercent * 100);
selectedColor.yAxis = Math.round(100 - cursorYPercent * 100);
}
},
setup: (self) => self
@ -220,20 +175,21 @@ export default () => {
const resultColorBox = Box({
className: 'sidebar-module-colorpicker-result-box',
homogeneous: true,
css: `background-color: ${hslToHex(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100))};`,
css: `background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};`,
children: [Label({
className: 'txt txt-small',
label: 'Result',
}),],
attribute: {
updateColor: (self) => {
self.setCss(`background-color: ${hslToHex(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100))};`);
update: (self) => {
self.setCss(`background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};`);
self.children[0].setCss(`color: ${shouldUseBlackColor() ? colorBlack : colorWhite};`)
}
},
setup: (self) => self
.hook(yAxis, (self) => self.attribute.updateColor(self))
.hook(hue, (self) => self.attribute.updateColor(self))
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
});
const resultHex = Box({
@ -247,26 +203,40 @@ export default () => {
className: 'txt-tiny',
label: 'Hex',
}),
Entry({
widthChars: 10,
className: 'txt-small techfont',
css: 'min-width: 0rem;',
attribute: {
updateColor: (self) => {
self.text = hslToHex(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100));
}
},
setup: (self) => self
.hook(yAxis, (self) => self.attribute.updateColor(self))
.hook(hue, (self) => self.attribute.updateColor(self))
,
Overlay({
child: Entry({
widthChars: 10,
className: 'txt-small techfont',
// css: 'color: transparent;',
attribute: {
id: 0,
update: (self, id) => {
if (id && self.attribute.id === id) return;
self.text = hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100));
}
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
// .on('activate', (self) => {
// const newColor = self.text;
// if (newColor.length != 7) return;
// selectedColor.setColorFromHex(self.text, self.attribute.id);
// })
,
}),
})
]
}),
Button({
child: MaterialIcon('content_copy', 'norm'),
onClicked: () => Utils
.execAsync(['wl-copy', `${hslToHex(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100))}`])
onClicked: (self) => {
Utils.execAsync(['wl-copy', `${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}`])
self.child.label = 'done';
Utils.timeout(1000, () => self.child.label = 'content_copy');
},
setup: setupCursorHover,
})
]
});
@ -284,23 +254,29 @@ export default () => {
Entry({
widthChars: 10,
className: 'txt-small techfont',
css: 'min-width: 0rem;',
attribute: {
updateColor: (self) => {
self.text = hslToRgbValues(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100));
id: 1,
update: (self, id) => {
if (id && self.attribute.id === id) return;
self.text = hslToRgbValues(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100));
}
},
setup: (self) => self
.hook(yAxis, (self) => self.attribute.updateColor(self))
.hook(hue, (self) => self.attribute.updateColor(self))
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
})
]
}),
Button({
child: MaterialIcon('content_copy', 'norm'),
onClicked: () => Utils
.execAsync(['wl-copy', `rgb(${hslToRgbValues(hue.value, xAxis.value, yAxis.value / (1 + xAxis.value / 100))})`])
onClicked: (self) => {
Utils.execAsync(['wl-copy', `rgb(${hslToRgbValues(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))})`])
self.child.label = 'done';
Utils.timeout(1000, () => self.child.label = 'content_copy');
},
setup: setupCursorHover,
})
]
});
@ -318,23 +294,30 @@ export default () => {
Entry({
widthChars: 10,
className: 'txt-small techfont',
css: 'min-width: 0rem;',
attribute: {
updateColor: (self) => {
self.text = `${hue.value},${xAxis.value}%,${Math.round(yAxis.value / (1 + xAxis.value / 100))}%`;
id: 2,
update: (self, id) => {
if (id && self.attribute.id === id) return;
self.text = `${selectedColor.hue},${selectedColor.xAxis}%,${Math.round(selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}%`;
}
},
setup: (self) => self
.hook(yAxis, (self) => self.attribute.updateColor(self))
.hook(hue, (self) => self.attribute.updateColor(self))
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
})
]
}),
Button({
child: MaterialIcon('content_copy', 'norm'),
onClicked: () => Utils
.execAsync(['wl-copy', `hsl(${hue.value},${xAxis.value}%,${Math.round(yAxis.value / (1 + xAxis.value / 100))}%)`])
onClicked: (self) => {
Utils.execAsync(['wl-copy', `hsl(${selectedColor.hue},${selectedColor.xAxis}%,${Math.round(selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}%)`])
self.child.label = 'done';
Utils.timeout(1000, () => self.child.label = 'content_copy');
},
setup: setupCursorHover,
})
]
});
@ -352,7 +335,7 @@ export default () => {
return SidebarModule({
icon: MaterialIcon('colorize', 'norm'),
name: 'Color picker',
// revealChild: false,
revealChild: false,
child: Box({
className: 'spacing-h-5',
children: [