Giter Club home page Giter Club logo

colorizer's Introduction

colorizer

A simple web-based colorscheme builder which focuses on contrast values.

The application is deployed using GitHub Pages and accessible under Colorizer.

colorizer's People

Contributors

mischback avatar

Stargazers

Return_of_the_Mack avatar

Watchers

 avatar  avatar

colorizer's Issues

Switch to ``TypeScript``

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.

User-specific Settings / Configuration

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.

  • the inputDebounceDelay of the ColorFormInputMethod class
  • which method is used to display the colors
    • This 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?
    • all rounding operations should be performed as late as possible (when the value is actually displayed!)
    • see roundToPrecision() in src/script/utility/index.ts
  • Light mode / dark mode (??)

Broken in Chromium/Chrome

Problem

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).

Research

  • This SO thread suggests that the error is raised because of a missing catch() while dealing with promises.
  • this may be solved during #42 , as failures during database access should end up in a notification, so there will be catch() blocks anyway.

Add input methods ``hsl`` and ``hwb``

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.

Re-implement ``<form>`` to add colors

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).

Features

  • ColorizerForm
    • setup the input methods; controlled by an argument during instance creation
    • submit callback; controlled by an argument during instance creation
    • keep track of the current color and expose it (getColor())
  • input method RGB
  • input method OkLCH

Why is the <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

Fix color inputs

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.

Possible Fixes

  • Enhance the underlying RegEx to include NaN
  • modify NaN to 0 in the engine

Improve Input of Colors

As 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.

Requirements

Resources

``CIE XYZ`` to ``OkLCH`` conversion

While 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).

Potential Problem

Our implementation assumes that the value range of C is [0..1]. The value of the conversion function

export function convertOklabToOklch(oklab: TOklab): TOklch {

is then converted to a percent-based value

tmp = roundToPrecision(colorOklch.c * 100, 2).toString();
which is plainly WRONG, as W3C CSS Color Module 4 explicitly states, that the range for the C 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!

Possible Fix

Change input-methods.ts#L564 to either

  • tmp = roundToPrecision((colorOklch.c / 0.4) * 100, 2).toString(); to keep the percent-based visualization
    • requires change in #L544: (Number(this.cBText.value) / 100) * 0.4,
  • tmp = roundToPrecision(colorOklch.c, 5).toString(); to display a raw value instead

This should be documented in all OkLCH-related functions!

Ref

<!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">&gt;</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;
}

Testing of TS/JS

This is a browser-centric application, so big parts of the codebase are actually DOM manipulations or are tight to DOM event processing/handling.

  • Are Unit Tests useful in that scenario?
  • Can End to End Tests be automated, at least during CI runs?
    • e.g. puppeteer?

Replace ``SortableJS``

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.

Fix calculation of relative luminance

The engine uses a function to determine the relative luminance of a given color. There is a bug!

  • File colorizer.js
  • Function 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.

Fix

Just change 0.03928 to 0.04045.

Use an existing library for handling colors

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.

Reasoning

  • It could possible completely replace the current ColorizerUtility function related to converting from / to RGB.
  • Probably higher code quality than my own implementations.

Candidates

  • color.js
    • supports tree shaking
    • seems well maintained
  • culori
    • supports tree shaking
    • seems well maintained

Evaluation

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.

Evaluation - Part II

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.

Sorting of Palette

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.

Functional Requirements

  • Colors should be re-arrangeable via drag'n'drop
  • The order should be applied to the contrast grid

Semantic names for palette items

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:

  • each item in the palette have a dedicated form for its label with an internal logic to update that label
  • for styling it would be great if there was a button to activate the form / input and keep it as plain text until then
    • clicking the activate button would make the form input writable, hide the button and replace it with a save button
    • clicking that save button would update the palette item in ColorizerPalette (and consequently in the database) and deactivate the form again, showing the activate button again

``v2`` Feature List

This 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.

  • Color Input
  • Palette Management (genearal implementation: #37)
    • store palette items in IndexedDB of the browser (related: #17)
    • sort the palette by drag'n'drop (#40 / #43)
    • remove items from the palette (#37)
    • clear / reset palette
      • confirmation dialogue required/desired?
    • export to JSON
    • import from JSON
  • Contrast Grid
    • the main output!
    • contrast ratio
    • contrast classification (WCAG2.0)
    • is this working without converting to RGB? (see #34)
    • allow sorting of the palette (by drag'n'drop) from the grid visualization (related: #40 / #43)
      • sort rows
      • sort columns (fix this in #43)
  • Overall layout / interface
    • be as accessible as possible!
    • tabs?
      • palette
      • color add form
      • contrast grid
      • (settings)
      • (about)

Build process for ``development`` and ``production``

Production Requirements

  • Script
    • create optimized bundle colorizer.js using Rollup's terser plugin
    • no source maps
  • Style
    • compile using sass
    • run through postcss with purgecss, autoprefixer and cssnano
  • Cache Busting
    • create the assets and then hash the respective file's content and add hash to the filename
    • replace original filename with filename with hash in index.html

Development Requirements

  • Script
    • create bundle colorizer.js in human readable form
    • include TS sources / sourcemaps
  • Style
    • compile using sass
    • run through postcss with purgecss

[Experimental] Contrast Calculation

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.

Experiment

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.

Result

As of 28b7d7b

  • When RGB-based inputs are used to set the color (RGB, HSL, HWB), the
    values of both methods correspond within an acceptable error margin
    (talking 1/1000 here).
  • When the okLCH-based input is used, the methods vary significantly.
    Probably this is not an issue of the actual calculating, but the
    clipping when converting the XYZ color (as it is stored internally) to
    RGB (ColorizerColor.toRgb()). This is already tracked as an issue
    (#25), which might need higher prio.

So What?

  • Actually not yet sure. It feels better and more modern to use the
    XYZ Y component directly. This will work out ok, as soon as #25 is
    fixed, but will still not be perfect in line with WCAG2.0
  • Possibly this is another subject for #23, letting the app's user make
    the decision, depending on his use case (if he's using RGB notation in
    his use case, he might want the more correct RGB-based calculation,
    but when he is already using XYZ or another (bigger) colorspace, he
    might want the XYZ Y solution).

Force colors into gamut

Internally, 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!

Resources

Build an overall layout

Overall

Palette Management

  • should be switchable between a compact and an extended output
    • compact only shows a preview of the color, the label and the possible actions (provided as buttons)
    • extended output shows all possible output formats #53
  • the display of output formats in extended view should be configurable
    • this configuration can be persistent between reloads (tracked in #23)
  • pin or fixate the toggle buttons when scrolling
  • hide output toggle buttons when compact mode is activated

Color Add Form

Palette Management

Implementation Idea

  • split the engine from the visualization
    • ColorizerPalette is the engine
      • manages the palette internally
        • add ColorizerPaletteItem
        • remove ColorizerPaletteItem
        • update ColorizerPaletteItem
      • implements the Observable part of the Observer pattern
      • interacts with the database abstraction
    • ColorizerPaletteInterface is the visualization
      • [REFACTOR] rename to ColorizerPaletteIO
      • handles all interaction with the DOM
        • visualize palette items by creating the required DOM elements
        • button to remove a palette item
        • drag'n'drop sorting of palette items (#40)

Todo

  • create ColorizerPaletteItem class
    • instances need a unique identifier throughout their life span
      • I don't really want to mess with this low-level stuff, better to leave this to IndexedDB, which might actually be possible
      • Leave it to IndexedDB:
        • Structuring the Database@MDN suggests, that when a 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.
        • when creating an actually new instance, let the unique identifier attribute be undefined. Hopefully it is provided when the instance is stored.
        • BUT: What to do in the meantime?!
      • Self-managed identifier:
        • Idea: Use the IndexedDB's store with a keyPath paletteItemID
        • determine a PaletteItem's paletteItemID in the constructor(), e.g. as a hash of the X, Y and Z coordinates.
        • additional benefit: no duplicate colors!
  • create ColorizerPalette class
    • Observer pattern!
      • 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
    • add new ColorizerPaletteItem to ColorizerPalette instance
    • remove ColorizerPaletteItem from ColorizerPalette instance

Resources

Setup development environment

Several tools should be in place to support the development:

  • NodeJS
    • 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.
    • Linting
    • TypeScript (see #11)
      • typescript / tsc (#16)
      • Rollup (#16)
      • testing (jest)? Skipped for now! See #19
    • SASS (see #18)
  • GitHub Actions
    • Dependabot
      • gh-actions (monthly)
      • npm (monthly)
      • run against development branch
      • apply auto-approving
    • development workflow
      • running linters on sources (TS and SASS) (#15)
      • running tests on TS sources? coveralls? Skipped for now! See #19
      • running build with Rollup (just to verify that this is working!)
    • release workflow (#30)

Drag'n'drop sorting of palette items

Implementation Idea

  • ColorizerEngineInterface handles the actual dragging'n'dropping (with the help of an external library; see below)
    • the original / v1 implementation handles all drag'n'drop stuff natively
    • Idea: with a third-party Drag'n'Drop library it might be possible to delegate all the sorting to that library (just the frontend, without directly involving Colorizer) 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)
    • sorting is based on an attribute of ColorizerPaletteItem (sorting)
    • ColorizerDatabase establishes and maintains an index on that field
    • the original / v1 implementation has a sorting attribute as type number (integer) and always updates all PaletteItem instances on sorting, which is highly inefficient!
    • Idea: Instead of a number-based sorting / ordering, use lexicographical ordering. This could possibly reduce the required number of update operations to 1!
      • Requires all instances of 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
        • have a private nextSorting: LexoRank on ColorizerPalette
        • have this.nextSorting = new LexoRank("foobar"); in ColorizerPalette.constructor() Yeah, kind of...
        • have this.nextSorting = this.nextSorting.increment() in ColorizerPalette.add() Yeah, kind of...
        • have this.nextSorting = new LexoRank(this._palette[this._palette.length - 1]?.sorting).increment() in ColorizerPalette.synchronizePaletteFromDb()
      • get the new index of the item in the overall palette, use newIndex = LexoRank.between(this._palette[newIndex - 1], this._palette[newIndex + 1]);
      • requires sorting of this._palette, either with an overall sorting or a manual insertion at the desired position (the latter should be more efficient)

Resources

Possible Libraries

Release Workflow

This project is meant to be hosted using GitHub Pages.

Intended workflow

  1. Happy hacking in feature branch
  2. Create PR against development branch, running a default CI workflow including linting (#16 / #26) and testing (#19)
  3. merge into development, create a PR against main and merge it into main
  4. run a GitHub Action workflow to build the application, including
    • .scss to .css (#18)
    • .ts to .js (#11)
    • cache busting including modification of index.html
  5. Profit, new version is available at mischback.github.io/colorizer/

Resources

Notification system

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.

  • error grade messages must be closed by the user
  • warnings / informations disappear after a given period

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.