A simple web-based colorscheme builder which focuses on contrast values.
The application is deployed using GitHub Pages
and accessible under
Colorizer.
A simple web-based colorscheme builder which focuses on contrast values.
Home Page: https://mischback.github.io/colorizer/
License: MIT License
A simple web-based colorscheme builder which focuses on contrast values.
The application is deployed using GitHub Pages
and accessible under
Colorizer.
As of now, this is a typical JS-related project: "Oh, let's just kluge this together quickly..." - "Hey, let's put at least some comments into it..." - "Oh fuck, the JS code is about 10.000 LoC and no longer maintainable!!!"
This issue is not about switching to TypeScript because TypeScript is nicer than VanillaJS, it's about structuring the codebase better, having clearly defined interfaces and at least some type safety.
There are some things, which should be exposed through user-specific settings. This is an additional feature beyond the core functions of the application, thus, it is not part of the v2 milestone.
This issue tracks the places, where a user-specific setting is beneficial / desired.
inputDebounceDelay
of the ColorFormInputMethod
classThis will be a major effort, possibly worth a dedicated issue!
Idea: All elements that are created to display a color have a dedicated class. Somewhere in the beginning of the DOM hierarchy, a class is set, controlling the method and setting the color is then handled in CSS by custom properties.
Dummy code:
<html>
<body class="rgb" style="--colorized-a=255; --colorized-b=128; --colorized-c=64">
...
<SOME GENERIC ELEMENT class="colorized">
.rgb .colorized {
background-color: rgb(var(--colorized-a) var(--colorized-b) var(--colorized-c));
}
.oklch .colorized {
background-color: oklch(var(--colorized-a) var(--colorized-b) var(--colorized-c));
}
precision
: Several input methods can operate on decimal numbers / floats. Should this be exposed? Should this be a dynamic thing, meaning that the the precision may be adjusted while using the application?
roundToPrecision()
in src/script/utility/index.ts
Chromium (Ubuntu 22.04) is not working anymore!
The palette is not synchronized from the IndexedDB
, console logs:
tslib.es6.js:118 Uncaught (in promise) Invalid lex string
fulfilled @ tslib.es6.js:118
Promise.then (async)
step @ tslib.es6.js:120
fulfilled @ tslib.es6.js:118
Promise.then (async)
step @ tslib.es6.js:120
(anonymous) @ tslib.es6.js:121
__awaiter @ tslib.es6.js:117
synchronizePaletteFromDb @ palette.ts:298
ColorizerPalette @ palette.ts:122
ColorizerController @ controller.ts:30
(anonymous) @ colorizer.ts:10
Show 6 more frames
While testing in the local network with a (mobile) Chrome, adding colors is broken aswell (as that test was executed from my phone, no detailled error messages, might be the same issue).
catch()
while dealing with promises.catch()
blocks anyway.These are more like different notations of sRGB, but still useful and common.
The original implementation (v1) does include them, so they should be part of v2 aswell.
Milestone v2 includes the restructuring of the codebase. This might mean making some code obsolete by introducing another library as dependency or re-implementing existing code in TypeScript (to leverage the benefits of type safety and syntactic sugar).
ColorizerForm
submit
callback; controlled by an argument during instance creationgetColor()
)<form>
logic the first refactoring subject?This comment includes an experimental, VanillaJS re-implementation of the <form>
logic. It is sufficiently complex to verify that the TS setup (#16) is working and the implementation will be one place, where the (external) color libraries (see #10) can be evaluated.
[repo_root]
├── src/
│ ├── script/
│ | ├── colorizer/
| │ | ├── interface/
| | │ | ├── color_form/
| | | │ | ├── input_methods.ts
| | | │ | ├── form.ts
| | | | │ └── index.ts
| | │ | ├── grid_view.ts
| | │ | ├── palette.ts
| | | │ └── index.ts
| │ | ├── engine.ts
| | │ └── index.ts
│ | ├── utility/
| | │ └── index.ts
| │ └── index.ts
│ └── style/ <-- details in #18
└── tsconfig.json
There is a issue with adding colors, if one of the form fields (<input type="text" ...>
, usually this is for a Hue component) has the value NaN
.
While this is semantically correct (the Hue component is powerless under certain conditions), the form
does not validate and is not submittable.
NaN
NaN
to 0
in the engineAs of now, colors have to be added using hex notation in a text-based input
field. This works well enough but is not really convenient.
type="color"
ColorizerInterface
and/or in ColorizerEngine
, because that input field includes the leading #
character.ColorizerEngine
remains the hex notation (see above, especially the this resource)0..255
internally!Oklch
seems more intuitive!sRGB
(which is predominant for usual computer screens) is described here, including sample implementation in JSWhile playing around with the color-form
, it appears that a RGB
input of 255, 0, 0
is not a pure red
in OkLCH
(visually).
evilmartians' converter returns oklch(62.8% 0.25768330773615683 29.2338851923426)
.
Our implementation assumes that the value range of C
is [0..1]
. The value of the conversion function
is then converted to a percent-based value
which is plainly WRONG, as W3C CSS Color Module 4 explicitly states, that the range for theC
component is 0% = 0; 100% = 0.4
.
Additionally, in the conversion function, hue
is calculated like this: const hue = (Math.atan2(oklab.b, oklab.a) * 180) / Math.PI;
, while the implementation source has const hue = Math.atan2(oklab.b, oklab.a) * 180 / Math.PI;
. I'm humble enough to state that I'm not sure about the brackets here! Needs verification!
Change input-methods.ts#L564
to either
tmp = roundToPrecision((colorOklch.c / 0.4) * 100, 2).toString();
to keep the percent-based visualization
(Number(this.cBText.value) / 100) * 0.4,
tmp = roundToPrecision(colorOklch.c, 5).toString();
to display a raw value insteadThis should be documented in all OkLCH-related functions!
Follow up to #43
The custom implementation of drag'n'drop is based on HTML5's API. This seems to not work properly with touch inputs.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Colorizer</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="assets/style.css">
<script src="assets/colorizer.js" defer></script>
</head>
<body>
<header>
<h1>Colorizer</h1>
<p>A tool to build color palettes while considering contrast values.</p>
</header>
<div id="ctrl">
<button id="ctrl-toggle">></button>
<section id="color-add">
<form>
<label for="new-color">New Color:</label>
<input id="new-color" type="text" required>
<button>Add</button>
</form>
<div id="color-palette">
<ul></ul>
</div>
</section>
</div>
<div id="contrast-grid"></div>
</body>
</html>
/* This is the actual engine that powers the web-based colorscheme builder.
*
* It stores colors of a palette in the browser, using IndexedDB.
*
* The colors of the palette are displayed in a matrix/grid and evaluated
* considering their contrast values.
*
* Resources:
* ----------
* - https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage#storing_complex_data_%E2%80%94_indexeddb
*/
// The database object
let db;
var color_palette_list = [];
// Get DOM elements
const DOM_color_palette_list = document.querySelector("#color-palette ul");
const DOM_color_add_form = document.querySelector("#color-add form");
const DOM_color_add_input = document.querySelector("#new-color");
const DOM_contrast_grid = document.querySelector("#contrast-grid");
// open an existing database or create a new one
//
// FIXME: Probably this should be wrapped in a function to make the site more
// responsive, applying progressive enhancement methods
const openRequest = window.indexedDB.open("colorizer_palette", 1);
/** Error handler */
openRequest.addEventListener("error", () => {
console.error("Could not open database");
});
/** The database was successfully opened, now process the content. */
openRequest.addEventListener("success", () => {
console.log("Database opened successfully");
// store the handle to the database
db = openRequest.result;
// TODO: Trigger function to display the palette!
updatePalette();
});
/** Initialize the desired database structure. */
openRequest.addEventListener("upgradeneeded", (e) => {
db = e.target.result;
// setup the objectStore
const objectStore = db.createObjectStore("colors", {
keyPath: "id",
autoIncrement: true,
});
objectStore.createIndex("color_hex", "color_hex", { unique: true });
objectStore.createIndex("color_r", "color_r", { unique: false });
objectStore.createIndex("color_g", "color_g", { unique: false });
objectStore.createIndex("color_b", "color_b", { unique: false });
console.log("Database setup completed");
});
function hexToRGB(hex) {
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16)
] : null;
}
function w3Category(contrastValue) {
switch(true) {
case contrastValue >= 7:
return "AAA";
break;
case contrastValue >= 4.5:
return "AA";
break;
case contrastValue >= 3:
return "A";
break;
default:
return "FAIL";
break;
}
}
function luminance(r, g, b) {
var a = [r, g, b].map(function(v) {
v /= 255;
return v <= 0.03928
? v / 12.92
: Math.pow((v+0.055) / 1.055, 2,4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
function contrast(rgb1, rgb2) {
var lum1 = luminance(rgb1[0], rgb1[1], rgb1[2]);
var lum2 = luminance(rgb2[0], rgb2[1], rgb2[2]);
var brighter = Math.max(lum1, lum2);
var darker = Math.min(lum1, lum2);
return (brighter + 0.05) / (darker + 0.05);
}
function buildContrastGrid(colorList, container) {
/* Takes a list of hex colors (provided as strings) and builds a grid of
* boxes with their respective contrast values.
*/
let grid_row;
let this_container;
let this_container_content;
let container_text;
// empty the existing palette to prevent duplicates
while (container.firstChild) {
container.removeChild(container.firstChild);
}
for (let i=0; i<colorList.length; i++) {
console.log("Color: " + color_palette_list[i]);
grid_row = document.createElement("div");
grid_row.classList.add("grid-row");
for (var j=0; j<colorList.length; j++) {
console.log("Color: " + colorList[i]);
this_container = document.createElement("div");
this_container.classList.add("grid-element");
this_container.style.cssText = "background-color: #" + colorList[i] + "; color: #" + colorList[j] + ";";
this_contrast = contrast(hexToRGB(colorList[i]), hexToRGB(colorList[j]))
this_container_content = document.createElement("div");
this_container_content.classList.add("w3cat");
container_text = document.createTextNode(w3Category(this_contrast));
this_container_content.appendChild(container_text);
this_container.appendChild(this_container_content);
this_container_content = document.createElement("div");
this_container_content.classList.add("contrast-value");
container_text = document.createTextNode(this_contrast.toFixed(2));
this_container_content.appendChild(container_text);
this_container.appendChild(this_container_content);
this_container_content = document.createElement("div");
this_container_content.classList.add("color-value");
container_text = document.createTextNode(colorList[j]);
this_container_content.appendChild(container_text);
this_container.appendChild(this_container_content);
grid_row.appendChild(this_container);
}
container.appendChild(grid_row);
}
}
function generatePaletteItem(color_id, color_hex) {
// create the general list element
const listItem = document.createElement("li");
listItem.setAttribute("palette-color-id", color_id);
// create a <span> to hold the color code as text
const itemColorCode = document.createElement("span");
itemColorCode.textContent = "#" + color_hex;
// create an empty <div> to visualize the color
const itemColorPreview = document.createElement("div");
itemColorPreview.style.cssText = "background-color: #" + color_hex + ";";
// create a <button> to delete the color from the palette
const itemColorDelete = document.createElement("button");
itemColorDelete.textContent = "remove";
itemColorDelete.addEventListener("click", deleteColorFromPalette);
listItem.appendChild(itemColorCode);
listItem.appendChild(itemColorPreview);
listItem.appendChild(itemColorDelete);
return listItem;
}
function deleteColorFromPalette(e) {
const color_id = Number(e.target.parentNode.getAttribute("palette-color-id"));
const transaction = db.transaction(["colors"], "readwrite");
const objectStore = transaction.objectStore("colors");
const deleteRequest = objectStore.delete(color_id);
transaction.addEventListener("complete", () => {
e.target.parentNode.parentNode.removeChild(e.target.parentNode);
console.log("Color deleted");
// TODO: Trigger function to display the palette!
updatePalette();
});
}
function updatePalette() {
// empty the existing palette to prevent duplicates
while (DOM_color_palette_list.firstChild) {
DOM_color_palette_list.removeChild(DOM_color_palette_list.firstChild);
}
color_palette_list = [];
const transaction = db.transaction("colors");
transaction.addEventListener("complete", () => {
buildContrastGrid(color_palette_list, DOM_contrast_grid);
});
const objectStore = transaction.objectStore("colors");
objectStore.openCursor().addEventListener("success", (e) => {
const cursor = e.target.result;
if (cursor) {
DOM_color_palette_list.appendChild(generatePaletteItem(cursor.value.id, cursor.value.color_hex));
color_palette_list.push(cursor.value.color_hex);
// iterate to the next item
cursor.continue();
}
});
}
// Hook into the DOM
DOM_color_add_form.addEventListener("submit", (e) => {
// don't actually submit the form, intercept with this code
e.preventDefault();
let color = hexToRGB(DOM_color_add_input.value);
if (color === null)
// leave the function if the input can not be parsed as hex color code
return;
const new_item = { color_hex: DOM_color_add_input.value, color_r: color[0], color_g: color[1], color_b: color[2] };
const transaction = db.transaction(["colors"], "readwrite");
const objectStore = transaction.objectStore("colors");
const addRequest = objectStore.add(new_item);
addRequest.addEventListener("success", () => {
// clear the form's field
DOM_color_add_input.value = "";
});
transaction.addEventListener("complete", () => {
console.log("Transaction completed, database modification finished");
// TODO: Trigger function to display the palette!
updatePalette();
});
transaction.addEventListener("error", () => {
console.error("Transaction not opened due to error");
});
});
/**** **** **** **** **** **** **** **** **** **** **** **** **** **** **** ***/
// NEW SHIT!
const DOM_ctrl_container = document.querySelector("#ctrl");
const DOM_ctrl_toggle = document.querySelector("#ctrl-toggle");
DOM_ctrl_toggle.addEventListener("click", (e) => {
e.preventDefault();
if (DOM_ctrl_toggle.textContent === "<") {
DOM_ctrl_toggle.textContent = ">";
DOM_ctrl_container.style.cssText = "";
} else {
DOM_ctrl_toggle.textContent = "<";
DOM_ctrl_container.style.cssText = "left: 0;";
}
});
* {
margin: 0;
padding: 0;
}
body {
font-family: Verdana, sans-serif;
background-color: #868686;
color: #000;
box-sizing: border-box;
}
#contrast-grid {
display: inline-block;
margin: 1em 1em 1em 3rem;
padding: 1em;
}
.grid-element {
padding: 0.5em;
font-size: 0.7em;
display: inline-block;
width: 6em;
height: 6em;
text-align: right;
border: 1px solid #000;
}
.grid-element .w3cat {
font-weight: bold;
font-size: 2em;
}
.grid-element .color-value {
font-weight: bold;
}
/* THIS IS NEW SHIT */
header {
margin: 1px 0 1rem 0;
padding: 0.5rem 1rem 1rem 1rem;
position: relative;
text-align: right;
background: linear-gradient(to top, #868686, rgba(0, 0, 0, 60%) 5px, #999 7px);
}
#ctrl {
position: absolute;
top: 1rem;
left: -50vw;
width: 50vw;
height: calc(100vh - 5rem);
background-color: #444;
color: #fefefe;
padding: 1em;
box-shadow: inset 0 0 3px 2px rgba(0, 0, 0, 60%);
border-top-right-radius: 1.5rem;
border-bottom-right-radius: 0.5rem;
}
#color-add {
margin: 0.5em 0 1em 0;
}
#color-add form label {
display: block;
font-size: 0.7em;
}
#color-add form input {
border: none;
border-bottom: 1px solid #fefefe;
background-color: #868686;
padding: 0.5em 1em;
font-size: 1.2em;
border-radius: 5px;
}
#color-add form button {
font-size: 1.2em;
padding: 0.5em 1em;
}
#ctrl-toggle {
position: absolute;
top: 1.75rem;
right: -1rem;
font-size: 1.5rem;
}
button {
color: #333;
border: none;
box-shadow: 0 0 3px 2px rgba(0, 0, 0, 60%);
background: linear-gradient(to bottom, #f9f9f9 5%, #e9e9e9 15% 40%, #d0d0d0 50% 90%, #666 95%);
border-radius: 5px;
font-weight: bold;
padding: 0.25em 0.5em;
}
button:hover {
background: linear-gradient(to bottom, #eedd99 5%, #ffcc00 15% 40%, #eeaa00 50% 90%, #666 95%);
}
#color-palette ul {
margin: 3em 0;
padding: 0;
list-style: none;
}
#color-palette ul li {
display: inline-block;
background-color: #f00;
color: #000;
padding: 0.25em 0.5em;
margin: 0.25em 0.5em;
border-radius: 5px;
background-color: #888;
}
#color-palette ul li span {
display: inline-block;
vertical-align: middle;
font-family: monospace;
font-size: 1.1em;
}
#color-palette ul li button {
vertical-align: middle;
}
#color-palette ul li div {
display: inline-block;
vertical-align: middle;
width: 1.5em;
height: 1.5em;
border: 1px solid rgba(0, 0, 0, 60%);
border-radius: 5px;
margin: 0 0.5em;
}
This is a browser-centric application, so big parts of the codebase are actually DOM manipulations or are tight to DOM event processing/handling.
puppeteer
?During implementation of #40, the actual drag'n'drop operation is handled with SortableJS. However, this adds around 40 - 50kB to the bundle size, which is kind of not acceptable.
After finishing the initial implementation of the contrast grid, which should also allow drag'n'drop operations, SortableJS
should be replaced, because I feel like the app is using just a minimal subset of its functions.
v1
/ the original implementation had its own drag'n'drop logic already implemented. Should be easily adaptable.
The engine uses a function to determine the relative luminance of a given color. There is a bug!
colorizer.js
ColorizerUtility.luminance()
The implementation is most-likely based on a SO answer, but there's a wrong threshold in the function. See https://www.w3.org/WAI/WCAG20/errata/ for more details.
Just change 0.03928
to 0.04045
.
As of now, color handling is based on (s)RGB internally. All conversion functions are implemented (more or less) following the reference implementations in W3C's Color Module Level 4.
ColorizerUtility
function related to converting from / to RGB
.color.js
culori
color.js
aswell as culori
add lots of overhead while using the tree-shakable APIs. Both of them are based around Color Spaces / Color Modes, which are - as of now - way out of scope of this application, which is intended to provide different input methods to define colors and then work with them.
Just for now, we're implementing our own interface for colors, which is tightly geared towards the specific needs of this application.
Ok, I gave culori
another try.
culori
exposes its conversion functions directly, making it suitable for tree-shaking (color.js
has the conversion functions tied to classes/objects, so the resulting bundle would contain more overhead). BUT, culori
does not provide a full set of conversion functions!
Everything went smooth to convert from sRGB
to CIE XYZ
and back, but there are no conversion functions from CIE XYZ
to (i.e.) OkLCH
. You would have to convert to sRGB
and then to OkLCH
which is a) overhead and b) might lead to rounding/calculation/precision issues.
The palette should be "sortable", meaning the existing colors should be re-arrangeable by the user. This should then be applied to the contrast grid output aswell.
As of now, the ColorizerPaletteIO
visualization of the palette items used the item's paletteItemId
in the interface, which is not really nice nor does it provide relevant information for the user.
Having a semantic name for the color would be the desired option, but this does require quite a lot of internal logic:
form
/ input
and keep it as plain text until then
input
writable, hide the button and replace it with a save buttonColorizerPalette
(and consequently in the database) and deactivate the form again, showing the activate button againThis is a meta issue for the features that should be provided with v2
. It is not meant to track everything related to the v2 milestone, but instead be a backlog of required features / functions.
RGB
with R
, G
and B
in range [0..255]
(#20)RGB
with hexadecimal input (output already implemented in #53)RGB
with HSL
notation (#24)RGB
with HWB
notation (#24)CIELab
with L
in range [0..100]
and a
/b
in range [-125..125]
CIELab
with CIELCH
notation with L
in range [0..100]
, C
in range [0..150]
and H
specified in deg
([0..360[
).OkLab
with L
in range [0..1]
and a
/b
in range [-0.4..0.4]
OkLab
with OkLCH
notation with L
in range [0..1]
, C
in range [0..0.4]
and H
specified in deg
([0..360[
) (#20, #36)OkHSL
OkHSV
The JS code is meant to be run after the DOM is ready. As of now, this is not enforced/implemented.
colorizer.js
using Rollup's terser
pluginsass
postcss
with purgecss
, autoprefixer
and cssnano
filename with hash
in index.html
colorizer.js
in human readable formsass
postcss
with purgecss
WCAG2.0
calculates the contrast ratio between two colors depending on their relative luminance.
The relative luminance is calculated like this:
luminance: function(red, green, blue) {
var a = [red, green, blue].map(function(v) {
v /= 255;
return v <= 0.04045
? v / 12.92
: Math.pow((v+0.055) / 1.055, 2,4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
},
However, this article states, that the Y
of XYZ
corresponds to the relative luminance already.
Define two colors (a, b)
in sRGB colorspace. Calculate their contrast ratio in sRGB using the functions of WCAG2.0
and then use their corresponding XYZ representations to calculate the contrast ratio depending on Y
alone.
As of 28b7d7b
ColorizerColor.toRgb()
). This is already tracked as an issueInternally, colors are stored in CIE XYZ D65
colorspace, which is really big (meant to provide the means to describe all visible colors). The different input methods are working on their respective color spaces (e.g. sRGB
and Oklch
), which are not congruent.
Colors must be forced into the color space's gamut. The easiest way is to just clip values, that are outside of the expected range, e.g. for sRGB
values < 0
and > 1
.
Currently this is the most basic implementation, just discarding these values. Might need more attention!
colorjs
/ coluri
and apply to our codebasesafelisting
in postcss.config.js
and use the inline comment /* purgecss ignore current */
instead (included in #54)settings.scss
) for notifications (#54)focus
might be an issue!settings.scss
git grep -e "transition:"
)src/index.html
, explicitly defining the whole form; must use templates instead, which are initialized in src/script/colorizer/interface/color-input-method.ts
(#55)xyz(0 0 0)
(#55)<form>
<form>
<fieldset>
is visible or hidden;sRGB
, OkLab
, ...) and provide a tab-based interfaceColorizerPalette
is the engine
ColorizerPaletteItem
ColorizerPaletteItem
ColorizerPaletteItem
ColorizerPaletteInterface
is the visualization
ColorizerPaletteIO
DOM
DOM
elementsColorizerPaletteItem
class
IndexedDB
, which might actually be possibleIndexedDB
:
keyPath
and a keyGenerator
are used on the store, keys are actually managed/provided/assigned by the IndexedDB
. The W3C spec provides some more technical details. As far as I understand it, the auto-generated keys are automatically deconflicted.undefined
. Hopefully it is provided when the instance is stored.IndexedDB
's store
with a keyPath
paletteItemID
PaletteItem
's paletteItemID
in the constructor()
, e.g. as a hash of the X
, Y
and Z
coordinates.ColorizerPalette
class
src/script/colorizer/lib/types.ts
: rename IColorizerObserver
to IColorizerColorObserver
src/script/colorizer/lib/types.ts
: rename IColorizerSubject
to IColorizerColorObservable
src/script/colorizer/lib/types.ts
: create IColorizerPaletteObserver
src/script/colorizer/lib/types.ts
: create IColorizerPaletteObservable
ColorizerPaletteItem
to ColorizerPalette
instanceColorizerPaletteItem
from ColorizerPalette
instanceAs of now, the use of IndexedDB
is implemented in the sources, with several problems, e.g. rather tight coupling.
Several tools should be in place to support the development:
package.json
This is not a NodeJS module, so no complete package.json
is required. Provide some minimal meta information, but the focus is tracking dependencies here.lint-staged
and simple-git-hooks
(#15)prettier
(#15)eslint
, setup for TypeScript (#16)tsc
(#16)stylelint
(see .stylelintrc
@ mischback.de)
Dependabot
gh-actions
(monthly)npm
(monthly)development
branchColorizerEngineInterface
handles the actual dragging'n'dropping (with the help of an external library; see below)
v1
implementation handles all drag'n'drop stuff nativelyColorizer
) and then attach our palette management by one of the provided callback hooks.ColorizerEngine
makes the sorting persistent in the database and publishes updates (Observer pattern)
ColorizerPaletteItem
(sorting
)ColorizerDatabase
establishes and maintains an index
on that fieldv1
implementation has a sorting
attribute as type number
(integer) and always updates all PaletteItem
instances on sorting, which is highly inefficient!ColorizerPaletteItem
to be instantiated with a valid sorting
value! Idea: Manage this in ColorizerPalette
, as the only way to add new palette items should be through the ColorizerPalette
instance
private nextSorting: LexoRank
on ColorizerPalette
this.nextSorting = new LexoRank("foobar");
in ColorizerPalette.constructor()
Yeah, kind of...this.nextSorting = this.nextSorting.increment()
in ColorizerPalette.add()
Yeah, kind of...this.nextSorting = new LexoRank(this._palette[this._palette.length - 1]?.sorting).increment()
in ColorizerPalette.synchronizePaletteFromDb()
newIndex = LexoRank.between(this._palette[newIndex - 1], this._palette[newIndex + 1]);
this._palette
, either with an overall sorting or a manual insertion at the desired position (the latter should be more efficient)LexoRank
, which is part of JIRALexoRank
and might be way over the top for the estimated number of palette itemslexorank-ts
(see above)Follow up to #20, dependent on #18.
There is (PoC-grade) styling in this comment.
This project is meant to be hosted using GitHub Pages
.
development
branch, running a default CI workflow including linting (#16 / #26) and testing (#19)development
, create a PR against main
and merge it into main
mischback.github.io/colorizer/
As of now, there is no dedicated feedback about the internal operations of the application (beside the logging to the browser's console
, which is not meant to be open by normal users).
A message system, where messages of different severities are visualized in the actual frontend would be great.
sass
to generate the stylesheetpostcss
into the build process
autoprefixer
purgecss
or uncss
purgecss is is...cssnano
browserslist
requiredA declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.