


Nailing the Perfect Contrast Between Light Text and a Background Image
Apr 03, 2025 am 09:44 AMHave you ever encountered light text on a website superimposed on a light background image? If you have encountered it, you will know how difficult it is to read. One common way to avoid this is to use a transparent overlay. But this brings up an important question: How high should the transparency of the overlay be? We don't always deal with the same font size, thickness, and color, and of course, different pictures will also produce different contrast.
Try to eliminate the problem of poor text contrast on the background image, just like playing the goblin game. Rather than guessing, use HTML<canvas></canvas>
And some mathematical methods to solve this problem.
Like this:
We can say "the problem is solved!" and then end this article. But what's the fun of this? What I want to show you is how this tool works so that you can master a new way to deal with this common problem.
plan
First, let’s clarify our goals. We said that we want to display readable text on the background image, but what exactly does "readable" mean? For our purposes, we will use WCAG's definition of AA-level readability, which states that there is a need for sufficient contrast between text and background colors so that one color is 4.5 times brighter than the other.
Let's choose a text color, a background image, and an overlay color as the starting point. Given these inputs, we want to find an overlay opacity level that makes the text readable without hiding the image so much that the image is difficult to see. To complicate things a little bit, we'll use a picture that has both dark and light colors, and make sure the overlay takes that into account.
Our end result will be a value that we can apply to the overlay's CSS opacity property, which makes the text 4.5 times brighter than the background.
To find the best overlay opacity, we will perform four steps:
- We put the image into HTML
<canvas></canvas>
This will allow us to read the color of each pixel in the picture. - We will find the pixels in the picture that have the least contrast to the text.
- Next, we will prepare a color mixing formula that we can use to test the effect of different opacity levels on the color of that pixel.
- Finally, we will adjust the opacity of the overlay until the text contrast reaches the readability goal. This won't be random guessing – we'll use binary search techniques to speed up this process.
Let's get started!
Step 1: Read the image color from the canvas
Canvas allows us to "read" the colors contained in the image. To do this, we need to "draw" the image to<canvas></canvas>
On the element, then use getImageData()
method of the canvas context (ctx) to generate a list of image colors.
function getImagePixelColorsUsingCanvas(image, canvas) { // The context of canvas (usually abbreviated as ctx) is an object containing many functions to control your canvas const ctx = canvas.getContext('2d'); // The width can be any value, so I chose 500 because it's big enough to capture details, but small enough to make the calculation faster. canvas.width = 500; // Make sure canvas match the scale of our image canvas.height = (image.height / image.width) * canvas.width; // Get the measurements of the image and canvas so that we can use them in the next step const sourceImageCoordinates = [0, 0, image.width, image.height]; const destinationCanvasCoordinates = [0, 0, canvas.width, canvas.height]; // Canvas' drawImage() works by mapping the measurements of our image to the canvas we want to draw it on ctx.drawImage( image, ...sourceImageCoordinates, ...destinationCanvasCoordinates ); // Remember that getImageData only works with images with the same source or cross-origin enabled. // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image const imagePixelColors = ctx.getImageData(...destinationCanvasCoordinates); return imagePixelColors; }
getImageData()
method provides us with a list of numbers representing the color of each pixel. Each pixel is represented by four numbers: red, green, blue, and opacity (also known as "alpha". Knowing this, we can iterate through the pixel list and find any information we need. This will be very useful in the next step.
Step 2: Find the pixel with the least contrast
Before that, we need to know how to calculate contrast. We will write a function called getContrast()
that takes two colors and outputs a number indicating the level of contrast between the two colors. The higher the numbers, the better the contrast and the better the readability.
When I started researching the colors of this project, I expected to find a simple formula. It turned out that there were multiple steps.
To calculate the contrast between two colors, we need to know their brightness level, which is essentially brightness (Stacie Arellano has an in-depth look at brightness, which is worth a look).
Thanks to W3C, we know the formula for calculating contrast using brightness:
const contrast = (lighterColorLuminance 0.05) / (darkerColorLuminance 0.05);
Getting the brightness of the color means we have to convert the color from the regular 8-bit RGB value used on the network (where each color is 0-255) to what is called linear RGB. We need to do this because the brightness does not increase evenly with the color change. We need to convert the color to a format where the brightness changes evenly with the color. This allows us to calculate the brightness correctly. Similarly, W3C provides help here:
const luminance = (0.2126 * getLinearRGB(r) 0.7152 * getLinearRGB(g) 0.0722 * getLinearRGB(b));
But wait, there are more! In order to convert 8-bit RGB (0 to 255) to linear RGB, we need to go through what is called standard RGB (also known as sRGB), which has a ratio of 0 to 1.
Therefore, the process is as follows:
<code>8位RGB → 標(biāo)準(zhǔn)RGB → 線性RGB → 亮度</code>
Once we have the brightness of the two colors we want to compare, we can substitute the brightness values ??into the formula to get the contrast between their respective colors.
// getContrast is the only function we need to interact directly. // The rest of the functions are intermediate auxiliary steps. function getContrast(color1, color2) { const color1_luminance = getLuminance(color1); const color2_luminance = getLuminance(color2); const lighterColorLuminance = Math.max(color1_luminance, color2_luminance); const darkerColorLuminance = Math.min(color1_luminance, color2_luminance); const contrast = (lighterColorLuminance 0.05) / (darkerColorLuminance 0.05); return contrast; } function getLuminance({r,g,b}) { return (0.2126 * getLinearRGB(r) 0.7152 * getLinearRGB(g) 0.0722 * getLinearRGB(b)); } function getLinearRGB(primaryColor_8bit) { // First convert from 8-bit rgb (0-255) to standard RGB (0-1) const primaryColor_sRGB = convert_8bit_RGB_to_standard_RGB(primaryColor_8bit); // Then convert from sRGB to linear RGB so that we can use it to calculate the brightness const primaryColor_RGB_linear = convert_standard_RGB_to_linear_RGB(primaryColor_sRGB); return primaryColor_RGB_linear; } function convert_8bit_RGB_to_standard_RGB(primaryColor_8bit) { return primaryColor_8bit / 255; } function convert_standard_RGB_to_linear_RGB(primaryColor_sRGB) { const primaryColor_linear = primaryColor_sRGB <p> Now that we can calculate the contrast, we need to look at the image in the previous step and iterate through each pixel to compare the contrast between the color of that pixel and the color of the foreground text. When we traverse the pixels of the image, we will track the worst (lowest) contrast so far, and when we reach the end of the loop, we will know the color with the worst contrast in the image.</p><pre class="brush:php;toolbar:false"> function getWorstContrastColorInImage(textColor, imagePixelColors) { let worstContrastColorInImage; let worstContrast = Infinity; // This ensures that we don't start with a value that is too low for (let i = 0; i <p></p><h3> Step 3: Prepare the color mixing formula to test the overlay opacity level</h3><p></p><p> Now that we know the color with the worst contrast in our image, the next step is to determine how high the transparency of the overlay should be and see how this will change the contrast with the text.</p><p></p><p> When I first implemented this I used a separate canvas to mix colors and read the results. However, thanks to Ana Tudor's article on transparency, I now know there is a convenient formula to calculate the resultant color after mixing the base color with the transparent overlay.</p><p></p><p> For each color channel (red, green, and blue), we will apply this formula to get the blended colors:</p><p> Mixed Color = Basic Color (overlapping Color - Basic Color) * Overlapping Opacity</p><p></p><p> So, in the code, this will look like this:</p><pre class="brush:php;toolbar:false"> function mixColors(baseColor, overlayColor, overlayOpacity) { const mixedColor = { r: baseColor.r (overlayColor.r - baseColor.r) * overlayOpacity, g: baseColor.g (overlayColor.g - baseColor.g) * overlayOpacity, b: baseColor.b (overlayColor.b - baseColor.b) * overlayOpacity, } return mixedColor; }
Now that we can mix colors, we can test the contrast when applying overlay opacity values.
function getTextContrastWithImagePlusOverlay({textColor, overlayColor, imagePixelColor, overlayOpacity}) { const colorOfImagePixelPlusOverlay = mixColors(imagePixelColor, overlayColor, overlayOpacity); const contrast = getContrast(textColor, colorOfImagePixelPlusOverlay); return contrast; }
With this we have all the tools we need to find the best overlay opacity!
Step 4: Find the overlay opacity that reaches the contrast target
We can test the opacity of the overlay and see how this will affect the contrast between the text and the image. We will try many different opacity levels until we find a value that reaches the target contrast, where the text is 4.5 times brighter than the background. This may sound crazy, but don't worry; we won't guess randomly. We will use binary search, a process that allows us to quickly narrow down possible sets of answers until we get accurate results.
Here is how binary search works:
<code>- 在中間猜測。 - 如果猜測過高,我們將消除答案的上半部分。太低了嗎?我們將改為消除下半部分。 - 在新的范圍中間猜測。 - 重復(fù)此過程,直到我們得到一個值。我碰巧有一個工具可以展示它是如何工作的:在這種情況下,我們試圖猜測一個介于0和1之間的不透明度值。因此,我們將從中間猜測,測試結(jié)果對比度是太高還是太低,消除一半的選項(xiàng),然后再次猜測。如果我們將二分查找限制為八次猜測,我們將立即得到一個精確的答案。在我們開始搜索之前,我們需要一種方法來檢查是否根本需要疊加層。我們根本不需要優(yōu)化我們不需要的疊加層! ```javascript function isOverlayNecessary(textColor, worstContrastColorInImage, desiredContrast) { const contrastWithoutOverlay = getContrast(textColor, worstContrastColorInImage); return contrastWithoutOverlay </code>
Now we can use binary search to find the best overlay opacity:
function findOptimalOverlayOpacity(textColor, overlayColor, worstContrastColorInImage, desiredContrast) { // If the contrast is good enough, we don't need to overlay, // So we can skip the rest. const isOverlayNecessary = isOverlayNecessary(textColor, worstContrastColorInImage, desiredContrast); if (!isOverlayNecessary) { return 0; } const opacityGuessRange = { lowerBound: 0, midpoint: 0.5, upperBound: 1, }; let numberOfGuesses = 0; const maxGuesses = 8; // If there is no solution, the opacity guess will be close to 1, // So we can use it as the upper limit to check for the situation without solutions. const opacityLimit = 0.99; // This loop repeatedly narrows down our guess until we get the result while (numberOfGuesses desiredContrast; if (isGuessTooLow) { opacityGuessRange.lowerBound = currentGuess; } else if (isGuessTooHigh) { opacityGuessRange.upperBound = currentGuess; } const newMidpoint = ((opacityGuessRange.upperBound - opacityGuessRange.lowerBound) / 2) opacityGuessRange.lowerBound; opacityGuessRange.midpoint = newMidpoint; } const optimalOpacity = opacityGuessRange.midpoint; const hasNoSolution = optimalOpacity > opacityLimit; if (hasNoSolution) { console.log('No solution'); // handle unsolvable situations as needed return opacityLimit; } return optimalOpacity; }
Once the experiment is done, we now know exactly how transparent the overlay needs to make the text readable without hiding too many background images.
We did it!
Improvements and limitations
The method we introduce is only effective if the text color and the overlay color themselves have sufficient contrast. For example, if you choose the same text color as the overlay, there will be no optimal solution unless the image does not require an overlay at all.
Also, even though contrast is mathematically acceptable, this doesn't always guarantee it looks great. This is especially true for dark text with light overlays and busy background images. Parts of the image may distract from the text, and may be difficult to read even if the contrast is numerically good. That's why the popular advice is to use light text on dark backgrounds.
We also did not consider the pixel position or the number of pixels per color. One disadvantage of this is that the pixels in the corners can have an excessive impact on the results. But the benefit is that we don't have to worry about how the colors of the image are distributed or where the text is, because as long as we deal with places with the least contrast, we can be safe anywhere else.
I learned something along the way
After this experiment, I have gained something and I want to share it with you:
<code>- **明確目標(biāo)非常有幫助!**我們從一個模糊的目標(biāo)開始,即想要在圖像上顯示可讀的文本,最終得到了一個我們可以努力達(dá)到的特定對比度級別。 - **明確術(shù)語非常重要。**例如,標(biāo)準(zhǔn)RGB并非我所期望的。我了解到,我認(rèn)為的“常規(guī)”RGB(0到255)正式稱為8位RGB。此外,我認(rèn)為我研究的方程式中的“L”表示“亮度”,但它實(shí)際上表示“亮度”,這不能與“光度”混淆。澄清術(shù)語有助于我們編寫代碼以及討論最終結(jié)果。 - **復(fù)雜并不意味著無法解決。**聽起來很困難的問題可以分解成更小、更容易管理的部分。 - **當(dāng)你走過這條路時,你會發(fā)現(xiàn)捷徑。**對于白色文本在黑色透明疊加層上的常見情況,您永遠(yuǎn)不需要超過0.54的不透明度即可達(dá)到WCAG AA級可讀性。 ### 總結(jié)…您現(xiàn)在有了一種方法可以在背景圖像上使文本可讀,而不會犧牲過多的圖像。如果您已經(jīng)讀到這里,我希望我已經(jīng)能夠讓您大致了解其工作原理。我最初開始這個項(xiàng)目是因?yàn)槲铱吹剑ú⒅谱髁耍┨嗑W(wǎng)站橫幅,其中文本在背景圖像上難以閱讀,或者背景圖像被疊加層過度遮擋。我想做些什么,我想給其他人提供一種同樣的方法。我寫這篇文章是為了希望你們能夠更好地理解網(wǎng)絡(luò)上的可讀性。我希望你們也學(xué)習(xí)了一些很酷的canvas技巧。如果您在可讀性或canvas方面做了一些有趣的事情,我很樂意在評論中聽到您的想法!</code>
The above is the detailed content of Nailing the Perfect Contrast Between Light Text and a Background Image. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

There are three ways to selectively include CSS on a specific page: 1. Inline CSS, suitable for pages that are not frequently accessed or require unique styles; 2. Load external CSS files using JavaScript conditions, suitable for situations where flexibility is required; 3. Containment on the server side, suitable for scenarios using server-side languages. This approach can optimize website performance and maintainability, but requires balance of modularity and performance.

Flexboxisidealforone-dimensionallayouts,whileGridsuitstwo-dimensional,complexlayouts.UseFlexboxforaligningitemsinasingleaxisandGridforprecisecontroloverrowsandcolumnsinintricatedesigns.

The HTML popover attribute transforms elements into top-layer elements that can be opened and closed with a button or JavaScript. Popovers can be dismissed a number of ways, but there is no option to auto-close them. Preethi has a technique you can u

CSS blocks page rendering because browsers view inline and external CSS as key resources by default, especially with imported stylesheets, header large amounts of inline CSS, and unoptimized media query styles. 1. Extract critical CSS and embed it into HTML; 2. Delay loading non-critical CSS through JavaScript; 3. Use media attributes to optimize loading such as print styles; 4. Compress and merge CSS to reduce requests. It is recommended to use tools to extract key CSS, combine rel="preload" asynchronous loading, and use media delayed loading reasonably to avoid excessive splitting and complex script control.

In the following tutorial, I will show you how to create Lottie animations in Figma. We'll use two colorful designs to exmplify how you can animate in Figma, and then I'll show you how to go from Figma to Lottie animations. All you need is a free Fig

We put it to the test and it turns out Sass can replace JavaScript, at least when it comes to low-level logic and puzzle behavior. With nothing but maps, mixins, functions, and a whole lot of math, we managed to bring our Tangram puzzle to life, no J

ThebestapproachforCSSdependsontheproject'sspecificneeds.Forlargerprojects,externalCSSisbetterduetomaintainabilityandreusability;forsmallerprojectsorsingle-pageapplications,internalCSSmightbemoresuitable.It'scrucialtobalanceprojectsize,performanceneed

No,CSSdoesnothavetobeinlowercase.However,usinglowercaseisrecommendedfor:1)Consistencyandreadability,2)Avoidingerrorsinrelatedtechnologies,3)Potentialperformancebenefits,and4)Improvedcollaborationwithinteams.
