cinera.c: Fix colouring of topic dots

The colour computed for topic dots and put into cinera_topics.css in HSL
format has its lightness value modified depending on whether it's on a
dark or light background. Web browsers do not tell us an element's
computed colour in HSL format, but in RGB, so we would need to convert
it from RGB to HSL when setting the lightness. Previously, this
conversion was busted, calculating too small a value for the saturation.

This commit obviates the need for any RGB→HSL conversion by writing the
hue and saturation values as attributes to the elements, which the
function responsible for setting the lightness may use directly.
This commit is contained in:
Matt Mascarenhas 2024-02-21 20:52:34 +00:00
parent 77aec74483
commit 213bb2f882
2 changed files with 150 additions and 119 deletions

View File

@ -23,7 +23,7 @@ typedef struct
version CINERA_APP_VERSION = {
.Major = 0,
.Minor = 10,
.Patch = 28
.Patch = 29
};
#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW
@ -4172,6 +4172,68 @@ LogEdit(edit_type_id EditType, string Lineage, string EntryID, string *EntryTitl
}
}
#define CINERA_HSL_TRANSPARENT_HUE 65535
typedef struct
{
unsigned int Hue:16;
unsigned int Saturation:8;
unsigned int Lightness:8;
} hsl_colour;
hsl_colour
CharToColour(char Char)
{
hsl_colour Colour;
if(Char >= 'a' && Char <= 'z')
{
Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360);
Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74);
}
else if(Char >= 'A' && Char <= 'Z')
{
Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360);
Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74);
}
else if(Char >= '0' && Char <= '9')
{
Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360);
Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74);
}
else
{
Colour.Hue = 180;
Colour.Saturation = 50;
}
return Colour;
}
void
StringToColourHash(hsl_colour *Colour, string String)
{
Colour->Hue = 0;
Colour->Saturation = 0;
Colour->Lightness = 74;
for(int i = 0; i < String.Length; ++i)
{
Colour->Hue += CharToColour(String.Base[i]).Hue;
Colour->Saturation += CharToColour(String.Base[i]).Saturation;
}
Colour->Hue = Colour->Hue % 360;
Colour->Saturation = Colour->Saturation % 26 + 74;
}
bool
IsValidHSLColour(hsl_colour Colour)
{
return (Colour.Hue >= 0 && Colour.Hue < 360)
&& (Colour.Saturation >= 0 && Colour.Saturation <= 100)
&& (Colour.Lightness >= 0 && Colour.Lightness <= 100);
}
#define SLASH 1
#define NULLTERM 1
typedef struct
@ -4326,6 +4388,7 @@ typedef struct
{
string Marker;
string WrittenText;
hsl_colour Colour;
} category_info;
#define CopyString(Dest, DestSize, Format, ...) CopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__)
@ -5481,58 +5544,6 @@ TimecodeToDottedSeconds(v4 Timecode)
return (float)Timecode.Hours * SECONDS_PER_HOUR + (float)Timecode.Minutes * SECONDS_PER_MINUTE + (float)Timecode.Seconds + (float)Timecode.Milliseconds / 1000;
}
typedef struct
{
unsigned int Hue:16;
unsigned int Saturation:8;
unsigned int Lightness:8;
} hsl_colour;
hsl_colour
CharToColour(char Char)
{
hsl_colour Colour;
if(Char >= 'a' && Char <= 'z')
{
Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360);
Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74);
}
else if(Char >= 'A' && Char <= 'Z')
{
Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360);
Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74);
}
else if(Char >= '0' && Char <= '9')
{
Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360);
Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74);
}
else
{
Colour.Hue = 180;
Colour.Saturation = 50;
}
return Colour;
}
void
StringToColourHash(hsl_colour *Colour, string String)
{
Colour->Hue = 0;
Colour->Saturation = 0;
Colour->Lightness = 74;
for(int i = 0; i < String.Length; ++i)
{
Colour->Hue += CharToColour(String.Base[i]).Hue;
Colour->Saturation += CharToColour(String.Base[i]).Saturation;
}
Colour->Hue = Colour->Hue % 360;
Colour->Saturation = Colour->Saturation % 26 + 74;
}
char *
SanitisePunctuation(char *String)
{
@ -8102,7 +8113,7 @@ BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metad
}
void
InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *GlobalMedia, _memory_book(category_info) *LocalMedia, string Marker)
InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *GlobalMedia, _memory_book(category_info) *LocalMedia, string Marker, hsl_colour *Colour)
{
medium *Medium = GetMediumFromProject(CurrentProject, Marker);
@ -8203,11 +8214,15 @@ InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_
{
category_info *Src = GetPlaceInBook(LocalTopics, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(LocalTopics, CategoryCount);
Dest->Marker = Src->Marker;
*Dest = *Src;
}
category_info *New = GetPlaceInBook(LocalTopics, CategoryCount);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
break;
}
}
@ -8218,6 +8233,10 @@ InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_
{
category_info *New = GetPlaceInBook(LocalTopics, TopicIndex);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
}
bool MadeGlobalSpace = FALSE;
@ -8241,11 +8260,15 @@ InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_
{
category_info *Src = GetPlaceInBook(GlobalTopics, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(GlobalTopics, CategoryCount);
Dest->Marker = Src->Marker;
*Dest = *Src;
}
category_info *New = GetPlaceInBook(GlobalTopics, CategoryCount);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
break;
}
}
@ -8257,6 +8280,10 @@ InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_
{
category_info *New = GetPlaceInBook(GlobalTopics, TopicIndex);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
}
}
}
@ -8348,9 +8375,18 @@ BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopi
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(CategoryIcons, "<div title=\"%.*s\" class=\"category %s\"></div>",
CopyStringToBuffer(CategoryIcons,
"<div title=\"%.*s\" class=\"category %s\"",
(int)This->Marker.Length, This->Marker.Base,
SanitisedMarker);
if(IsValidHSLColour(This->Colour))
{
CopyStringToBuffer(CategoryIcons,
" data-hue=\"%u\" data-saturation=\"%u%%\"",
This->Colour.Hue, This->Colour.Saturation);
}
CopyStringToBuffer(CategoryIcons,
"></div>");
}
}
@ -8630,7 +8666,7 @@ BuildQuote(memory_book *Strings, quote_info *Info, string Speaker, int ID, bool
}
rc
GenerateTopicColours(neighbourhood *N, string Topic)
GenerateTopicColours(neighbourhood *N, string Topic, hsl_colour *Dest)
{
rc Result = RC_SUCCESS;
// NOTE(matt): Stack-string
@ -8641,6 +8677,15 @@ GenerateTopicColours(neighbourhood *N, string Topic)
medium *Medium = GetMediumFromProject(CurrentProject, Topic);
if(!Medium)
{
if(StringsMatch(Topic, Wrap0("nullTopic")))
{
Dest->Hue = CINERA_HSL_TRANSPARENT_HUE;
}
else
{
StringToColourHash(Dest, Topic);
}
file Topics = {};
Topics.Path = 0;
Topics.Buffer.ID = BID_TOPICS;
@ -8706,10 +8751,8 @@ GenerateTopicColours(neighbourhood *N, string Topic)
}
else
{
hsl_colour Colour;
StringToColourHash(&Colour, Topic);
WriteToFile(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n",
SanitisedTopic, Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness);
SanitisedTopic, Dest->Hue, Dest->Saturation, Dest->Lightness, Dest->Hue, Dest->Saturation, Dest->Lightness);
}
#if DEBUG_MEM
@ -10899,7 +10942,7 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored"));
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored"), NULL);
hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, Author);
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
@ -10924,14 +10967,15 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
HMML_MarkerType Type = Timestamp->markers[MarkerIndex].type;
if(Type == HMML_CATEGORY)
{
Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker));
hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
if(Result == RC_SUCCESS)
{
if(!*HasFilterMenu)
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker));
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr);
}
else
@ -11162,14 +11206,15 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
while(MarkerIndex < Timestamp->marker_count)
{
Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker));
hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
if(Result == RC_SUCCESS)
{
if(!*HasFilterMenu)
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker));
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
++MarkerIndex;
}
else
@ -11182,10 +11227,11 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
{
if(LocalTopics.ItemCount == 0)
{
Result = GenerateTopicColours(N, Wrap0("nullTopic"));
hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour);
if(Result == RC_SUCCESS)
{
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("nullTopic"));
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("nullTopic"), &TopicColour);
}
}
@ -11193,7 +11239,7 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
{
if(LocalMedia.ItemCount == 0)
{
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, DefaultMedium->ID);
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, DefaultMedium->ID, NULL);
}
BuildTimestampClass(&IndexBuffers->Class, &LocalTopics, &LocalMedia, DefaultMedium->ID);
@ -11992,12 +12038,20 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF
bool NullTopic = StringsMatch(This->Marker, Wrap0("nullTopic"));
CopyStringToBuffer(&MenuBuffers.FilterTopics,
" <div%s class=\"filter_content %s\">\n"
" <span class=\"icon category %s\"></span><span class=\"cineraText\">%.*s</span>\n"
" </div>\n",
" <span class=\"icon category %s\"",
NullTopic ? " title=\"Timestamps that don't fit into the above topic(s) may be filtered using this pseudo-topic\"" : "",
SanitisedMarker,
SanitisedMarker,
SanitisedMarker);
if(IsValidHSLColour(This->Colour))
{
CopyStringToBuffer(&MenuBuffers.FilterTopics,
" data-hue=\"%u\" data-saturation=\"%u%%\"",
This->Colour.Hue, This->Colour.Saturation);
}
CopyStringToBuffer(&MenuBuffers.FilterTopics,
"></span><span class=\"cineraText\">%.*s</span>\n"
" </div>\n",
NullTopic ? (int)sizeof("(null topic)")-1 : (int)This->Marker.Length,
NullTopic ? "(null topic)" : This->Marker.Base);
}

View File

@ -551,35 +551,6 @@ BindHelp(Button, DocumentationContainer)
})
}
function RGBtoHSL(colour)
{
var rgb = colour.slice(4, -1).split(", ");
var red = rgb[0];
var green = rgb[1];
var blue = rgb[2];
var min = Math.min(red, green, blue);
var max = Math.max(red, green, blue);
var chroma = max - min;
var hue = 0;
if(max == red)
{
hue = ((green - blue) / chroma) % 6;
}
else if(max == green)
{
hue = ((blue - red) / chroma) + 2;
}
else if(max == blue)
{
hue = ((red - green) / chroma) + 4;
}
var saturation = chroma / 255 * 100;
hue = (hue * 60) < 0 ? 360 + (hue * 60) : (hue * 60);
return [hue, saturation]
}
function getBackgroundColourRGB(element) {
var Colour = getComputedStyle(element).getPropertyValue("background-color");
var depth = 0;
@ -610,6 +581,8 @@ function setTextLightness(textElement)
{
var textHue = textElement.getAttribute("data-hue");
var textSaturation = textElement.getAttribute("data-saturation");
if(textHue && textSaturation)
{
if(getBackgroundBrightness(textElement.parentNode) < 127)
{
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 76%)");
@ -619,19 +592,23 @@ function setTextLightness(textElement)
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 24%)");
}
}
}
function setDotLightness(topicDot)
{
var Hue = RGBtoHSL(getComputedStyle(topicDot).getPropertyValue("background-color"))[0];
var Saturation = RGBtoHSL(getComputedStyle(topicDot).getPropertyValue("background-color"))[1];
var dotHue = topicDot.getAttribute("data-hue");
var dotSaturation = topicDot.getAttribute("data-saturation");
if(dotHue && dotSaturation)
{
if(getBackgroundBrightness(topicDot.parentNode) < 127)
{
topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 76%)");
topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 76%)");
topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
}
else
{
topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)");
topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)");
topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
}
}
}