This issue had been rolling around in my mind in the past but I never had to implement something like it before. I had a text box where I set the background colour based on a colour picker but that meant that the text value would remain black - which wouldn’t be visible if the selected colour was also black, or even dark.
My solution was to pick a colour that differed as much as possible from black - white - and apply this methodology with all colours. If our background was white, then I would use black as the foreground. For colours in between I used a threshold - as soon as the background colour passed it (as it became darker), I would change the foreground from black to white (making it lighter). I believe the best foreground should be either white or black, since it should differ from the background as much as possible - so linear interpolation between black and white wouldn’t help here. My method was to calculate a distance from white, which is the origin at (0,0). The x-axis is saturation and the y-axis is (1 - value) in the HSV model. Using a threshold amount of 0.5, I created a radius around white where my foreground is black. Any position outside this radius means my foreground is white. By using multipliers I was able to create an ellipse shape to my liking. I ran into another issue - for colours that appear light - yellow, green, light blue - I needed to allow more black. So I stretched the radius in the x-axis for these colours. This methodology is generic enough to be applied anywhere this problem comes up, but for my implementation I used jQuery and TinyColor. See an online demo here: Grab the source: