-
Notifications
You must be signed in to change notification settings - Fork 788
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(color-contrast): get text stoke from offset shadows (#4079)
* fix(color-contrast): get text stoke from offset shadows * Apply suggestions from code review Co-authored-by: Dan Bjorge <[email protected]> * Refactor & cleanup * Have partial text-shadows report as incomplete * Ignore thin shadows on one side of text * Resolve feedback --------- Co-authored-by: Dan Bjorge <[email protected]>
- Loading branch information
1 parent
9d5f496
commit 13acffe
Showing
15 changed files
with
737 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import Color from './color'; | ||
|
||
/** Magic numbers **/ | ||
// Alpha value to use when text shadows are offset between .5px and 1.5px | ||
const SHADOW_STROKE_ALPHA = 0.54; | ||
// Shadows offset by less than this are not visible enough no matter how much you stack them | ||
const VISIBLE_SHADOW_MIN_PX = 0.5; | ||
// Shadows offset by more than this have full opacity | ||
const OPAQUE_STROKE_OFFSET_MIN_PX = 1.5; | ||
|
||
const edges = ['top', 'right', 'bottom', 'left']; | ||
|
||
/** | ||
* Work out which color(s) of an array of text shadows form a stroke around the text. | ||
* @param {Array[]} testShadows Parsed test shadows (see color.parseTestShadow()) | ||
* @param {Object} options (optional) | ||
* @property {Bool} ignoreEdgeCount Do not return null when if shadows cover 2 or 3 edges, ignore those instead | ||
* @returns {Array|null} Array of colors or null if text-shadow was too complex to measure | ||
*/ | ||
export default function getStrokeColorsFromShadows( | ||
parsedShadows, | ||
{ ignoreEdgeCount = false } = {} | ||
) { | ||
const shadowMap = getShadowColorsMap(parsedShadows); | ||
const shadowsByColor = Object.entries(shadowMap).map(([colorStr, sides]) => { | ||
const edgeCount = edges.filter(side => sides[side].length !== 0).length; | ||
return { colorStr, sides, edgeCount }; | ||
}); | ||
|
||
// Bail immediately if any shadow group covers too much of the text to be ignored, but not enough to be tested | ||
if ( | ||
!ignoreEdgeCount && | ||
shadowsByColor.some(({ edgeCount }) => edgeCount > 1 && edgeCount < 4) | ||
) { | ||
return null; | ||
} | ||
|
||
return shadowsByColor | ||
.map(shadowGroupToColor) | ||
.filter(shadow => shadow !== null); | ||
} | ||
|
||
/** | ||
* Create a map of colors to the sides they are on | ||
*/ | ||
function getShadowColorsMap(parsedShadows) { | ||
const colorMap = {}; | ||
for (const { colorStr, pixels } of parsedShadows) { | ||
colorMap[colorStr] ??= { top: [], right: [], bottom: [], left: [] }; | ||
const borders = colorMap[colorStr]; | ||
const [offsetX, offsetY] = pixels; | ||
|
||
if (offsetX > VISIBLE_SHADOW_MIN_PX) { | ||
borders.right.push(offsetX); | ||
} else if (-offsetX > VISIBLE_SHADOW_MIN_PX) { | ||
borders.left.push(-offsetX); | ||
} | ||
if (offsetY > VISIBLE_SHADOW_MIN_PX) { | ||
borders.bottom.push(offsetY); | ||
} else if (-offsetY > VISIBLE_SHADOW_MIN_PX) { | ||
borders.top.push(-offsetY); | ||
} | ||
} | ||
return colorMap; | ||
} | ||
|
||
/** | ||
* Using colorStr and thickness of sides, create a color object | ||
*/ | ||
function shadowGroupToColor({ colorStr, sides, edgeCount }) { | ||
if (edgeCount !== 4) { | ||
return null; // ignore thin shadows and shadows on one side of the text | ||
} | ||
const strokeColor = new Color(); | ||
strokeColor.parseString(colorStr); | ||
|
||
// Detect whether any sides' shadows are thin enough to be considered | ||
// translucent, and if so, calculate an alpha value to apply on top of | ||
// the parsed color. | ||
let density = 0; | ||
let isSolid = true; | ||
edges.forEach(edge => { | ||
// Decimal values are ignored. a .6px shadow is treated as 1px | ||
// because it is not rendered evenly around the text. | ||
// I.e. .6 ends up as 70% alpha on one side and 16% on the other. | ||
density += sides[edge].length / 4; | ||
isSolid &&= sides[edge].every( | ||
offset => offset > OPAQUE_STROKE_OFFSET_MIN_PX | ||
); | ||
}); | ||
|
||
if (!isSolid) { | ||
// As more shadows surround the text, the opacity increases | ||
strokeColor.alpha = 1 - Math.pow(SHADOW_STROKE_ALPHA, density); | ||
} | ||
return strokeColor; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import assert from '../../core/utils/assert'; | ||
|
||
/** | ||
* Parse text-shadow property value. Required for IE, which can return the color | ||
* either at the start or the end, and either in rgb(a) or as a named color | ||
* @param {String} textShadow | ||
* @returns {Array} Array of objects with `pixels` and `colorStr` properties | ||
*/ | ||
export default function parseTextShadows(textShadow) { | ||
let current = { pixels: [] }; | ||
let str = textShadow.trim(); | ||
const shadows = [current]; | ||
if (!str) { | ||
return []; | ||
} | ||
|
||
while (str) { | ||
const colorMatch = | ||
str.match(/^rgba?\([0-9,.\s]+\)/i) || | ||
str.match(/^[a-z]+/i) || | ||
str.match(/^#[0-9a-f]+/i); | ||
const pixelMatch = str.match(/^([0-9.-]+)px/i) || str.match(/^(0)/); | ||
|
||
if (colorMatch) { | ||
assert( | ||
!current.colorStr, | ||
`Multiple colors identified in text-shadow: ${textShadow}` | ||
); | ||
str = str.replace(colorMatch[0], '').trim(); | ||
current.colorStr = colorMatch[0]; | ||
} else if (pixelMatch) { | ||
assert( | ||
current.pixels.length < 3, | ||
`Too many pixel units in text-shadow: ${textShadow}` | ||
); | ||
str = str.replace(pixelMatch[0], '').trim(); | ||
const pixelUnit = parseFloat( | ||
(pixelMatch[1][0] === '.' ? '0' : '') + pixelMatch[1] | ||
); | ||
current.pixels.push(pixelUnit); | ||
} else if (str[0] === ',') { | ||
// multiple text-shadows in a single string (e.g. `text-shadow: 1px 1px 1px #000, 3px 3px 5px blue;` | ||
assert( | ||
current.pixels.length >= 2, | ||
`Missing pixel value in text-shadow: ${textShadow}` | ||
); | ||
current = { pixels: [] }; | ||
shadows.push(current); | ||
str = str.substr(1).trim(); | ||
} else { | ||
throw new Error(`Unable to process text-shadows: ${textShadow}`); | ||
} | ||
} | ||
|
||
shadows.forEach(({ pixels }) => { | ||
if (pixels.length === 2) { | ||
pixels.push(0); // Append default blur | ||
} | ||
}); | ||
|
||
return shadows; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.