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 = { version CINERA_APP_VERSION = {
.Major = 0, .Major = 0,
.Minor = 10, .Minor = 10,
.Patch = 28 .Patch = 29
}; };
#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW #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 SLASH 1
#define NULLTERM 1 #define NULLTERM 1
typedef struct typedef struct
@ -4326,6 +4388,7 @@ typedef struct
{ {
string Marker; string Marker;
string WrittenText; string WrittenText;
hsl_colour Colour;
} category_info; } category_info;
#define CopyString(Dest, DestSize, Format, ...) CopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__) #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; 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 * char *
SanitisePunctuation(char *String) SanitisePunctuation(char *String)
{ {
@ -8102,7 +8113,7 @@ BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metad
} }
void 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); 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 *Src = GetPlaceInBook(LocalTopics, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(LocalTopics, CategoryCount); category_info *Dest = GetPlaceInBook(LocalTopics, CategoryCount);
Dest->Marker = Src->Marker; *Dest = *Src;
} }
category_info *New = GetPlaceInBook(LocalTopics, CategoryCount); category_info *New = GetPlaceInBook(LocalTopics, CategoryCount);
New->Marker = Marker; New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
break; break;
} }
} }
@ -8218,6 +8233,10 @@ InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_
{ {
category_info *New = GetPlaceInBook(LocalTopics, TopicIndex); category_info *New = GetPlaceInBook(LocalTopics, TopicIndex);
New->Marker = Marker; New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
} }
bool MadeGlobalSpace = FALSE; 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 *Src = GetPlaceInBook(GlobalTopics, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(GlobalTopics, CategoryCount); category_info *Dest = GetPlaceInBook(GlobalTopics, CategoryCount);
Dest->Marker = Src->Marker; *Dest = *Src;
} }
category_info *New = GetPlaceInBook(GlobalTopics, CategoryCount); category_info *New = GetPlaceInBook(GlobalTopics, CategoryCount);
New->Marker = Marker; New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
break; break;
} }
} }
@ -8257,6 +8280,10 @@ InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_
{ {
category_info *New = GetPlaceInBook(GlobalTopics, TopicIndex); category_info *New = GetPlaceInBook(GlobalTopics, TopicIndex);
New->Marker = Marker; 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); CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker); 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, (int)This->Marker.Length, This->Marker.Base,
SanitisedMarker); 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 rc
GenerateTopicColours(neighbourhood *N, string Topic) GenerateTopicColours(neighbourhood *N, string Topic, hsl_colour *Dest)
{ {
rc Result = RC_SUCCESS; rc Result = RC_SUCCESS;
// NOTE(matt): Stack-string // NOTE(matt): Stack-string
@ -8641,6 +8677,15 @@ GenerateTopicColours(neighbourhood *N, string Topic)
medium *Medium = GetMediumFromProject(CurrentProject, Topic); medium *Medium = GetMediumFromProject(CurrentProject, Topic);
if(!Medium) if(!Medium)
{ {
if(StringsMatch(Topic, Wrap0("nullTopic")))
{
Dest->Hue = CINERA_HSL_TRANSPARENT_HUE;
}
else
{
StringToColourHash(Dest, Topic);
}
file Topics = {}; file Topics = {};
Topics.Path = 0; Topics.Path = 0;
Topics.Buffer.ID = BID_TOPICS; Topics.Buffer.ID = BID_TOPICS;
@ -8706,10 +8751,8 @@ GenerateTopicColours(neighbourhood *N, string Topic)
} }
else 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", 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 #if DEBUG_MEM
@ -10899,7 +10942,7 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
{ {
*HasFilterMenu = TRUE; *HasFilterMenu = TRUE;
} }
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored")); InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored"), NULL);
hsl_colour AuthorColour; hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, Author); StringToColourHash(&AuthorColour, Author);
// TODO(matt): That EDITION_NETWORK site database API-polling stuff // 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; HMML_MarkerType Type = Timestamp->markers[MarkerIndex].type;
if(Type == HMML_CATEGORY) 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(Result == RC_SUCCESS)
{ {
if(!*HasFilterMenu) if(!*HasFilterMenu)
{ {
*HasFilterMenu = TRUE; *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); CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr);
} }
else else
@ -11162,14 +11206,15 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
while(MarkerIndex < Timestamp->marker_count) 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(Result == RC_SUCCESS)
{ {
if(!*HasFilterMenu) if(!*HasFilterMenu)
{ {
*HasFilterMenu = TRUE; *HasFilterMenu = TRUE;
} }
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker)); InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
++MarkerIndex; ++MarkerIndex;
} }
else else
@ -11182,10 +11227,11 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
{ {
if(LocalTopics.ItemCount == 0) if(LocalTopics.ItemCount == 0)
{ {
Result = GenerateTopicColours(N, Wrap0("nullTopic")); hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour);
if(Result == RC_SUCCESS) 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) 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); BuildTimestampClass(&IndexBuffers->Class, &LocalTopics, &LocalMedia, DefaultMedium->ID);
@ -11991,13 +12037,21 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF
bool NullTopic = StringsMatch(This->Marker, Wrap0("nullTopic")); bool NullTopic = StringsMatch(This->Marker, Wrap0("nullTopic"));
CopyStringToBuffer(&MenuBuffers.FilterTopics, CopyStringToBuffer(&MenuBuffers.FilterTopics,
" <div %sclass=\"filter_content %s\">\n" " <div%s class=\"filter_content %s\">\n"
" <span class=\"icon category %s\"></span><span class=\"cineraText\">%.*s</span>\n" " <span class=\"icon category %s\"",
" </div>\n",
NullTopic ? "title=\"Timestamps that don't fit into the above topic(s) may be filtered using this pseudo-topic\" " : "", NullTopic ? " title=\"Timestamps that don't fit into the above topic(s) may be filtered using this pseudo-topic\"" : "",
SanitisedMarker,
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 ? (int)sizeof("(null topic)")-1 : (int)This->Marker.Length,
NullTopic ? "(null topic)" : This->Marker.Base); 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) { function getBackgroundColourRGB(element) {
var Colour = getComputedStyle(element).getPropertyValue("background-color"); var Colour = getComputedStyle(element).getPropertyValue("background-color");
var depth = 0; var depth = 0;
@ -610,6 +581,8 @@ function setTextLightness(textElement)
{ {
var textHue = textElement.getAttribute("data-hue"); var textHue = textElement.getAttribute("data-hue");
var textSaturation = textElement.getAttribute("data-saturation"); var textSaturation = textElement.getAttribute("data-saturation");
if(textHue && textSaturation)
{
if(getBackgroundBrightness(textElement.parentNode) < 127) if(getBackgroundBrightness(textElement.parentNode) < 127)
{ {
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 76%)"); textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 76%)");
@ -618,20 +591,24 @@ function setTextLightness(textElement)
{ {
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 24%)"); textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 24%)");
} }
}
} }
function setDotLightness(topicDot) function setDotLightness(topicDot)
{ {
var Hue = RGBtoHSL(getComputedStyle(topicDot).getPropertyValue("background-color"))[0]; var dotHue = topicDot.getAttribute("data-hue");
var Saturation = RGBtoHSL(getComputedStyle(topicDot).getPropertyValue("background-color"))[1]; var dotSaturation = topicDot.getAttribute("data-saturation");
if(dotHue && dotSaturation)
{
if(getBackgroundBrightness(topicDot.parentNode) < 127) if(getBackgroundBrightness(topicDot.parentNode) < 127)
{ {
topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 76%)"); topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 76%)"); topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
} }
else else
{ {
topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)"); topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)"); topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
}
} }
} }