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:
parent
77aec74483
commit
213bb2f882
202
cinera/cinera.c
202
cinera/cinera.c
|
@ -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);
|
||||
|
@ -11991,13 +12037,21 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF
|
|||
|
||||
bool NullTopic = StringsMatch(This->Marker, Wrap0("nullTopic"));
|
||||
CopyStringToBuffer(&MenuBuffers.FilterTopics,
|
||||
" <div %sclass=\"filter_content %s\">\n"
|
||||
" <span class=\"icon category %s\"></span><span class=\"cineraText\">%.*s</span>\n"
|
||||
" </div>\n",
|
||||
" <div%s class=\"filter_content %s\">\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,
|
||||
NullTopic ? " title=\"Timestamps that don't fit into the above topic(s) may be filtered using this pseudo-topic\"" : "",
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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,28 +581,34 @@ function setTextLightness(textElement)
|
|||
{
|
||||
var textHue = textElement.getAttribute("data-hue");
|
||||
var textSaturation = textElement.getAttribute("data-saturation");
|
||||
if(getBackgroundBrightness(textElement.parentNode) < 127)
|
||||
if(textHue && textSaturation)
|
||||
{
|
||||
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 76%)");
|
||||
}
|
||||
else
|
||||
{
|
||||
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 24%)");
|
||||
if(getBackgroundBrightness(textElement.parentNode) < 127)
|
||||
{
|
||||
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 76%)");
|
||||
}
|
||||
else
|
||||
{
|
||||
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];
|
||||
if(getBackgroundBrightness(topicDot.parentNode) < 127)
|
||||
var dotHue = topicDot.getAttribute("data-hue");
|
||||
var dotSaturation = topicDot.getAttribute("data-saturation");
|
||||
if(dotHue && dotSaturation)
|
||||
{
|
||||
topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 76%)");
|
||||
topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 76%)");
|
||||
}
|
||||
else
|
||||
{
|
||||
topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)");
|
||||
topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)");
|
||||
if(getBackgroundBrightness(topicDot.parentNode) < 127)
|
||||
{
|
||||
topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
|
||||
topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
|
||||
}
|
||||
else
|
||||
{
|
||||
topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
|
||||
topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue