-
Notifications
You must be signed in to change notification settings - Fork 793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(color-contrast): get text stoke from offset shadows #4079
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
07e7f01
fix(color-contrast): get text stoke from offset shadows
WilcoFiers 43f9389
Apply suggestions from code review
WilcoFiers 1032d42
Refactor & cleanup
WilcoFiers 0e2947d
Have partial text-shadows report as incomplete
WilcoFiers 80d86e5
Ignore thin shadows on one side of text
WilcoFiers 019f8cf
Resolve feedback
WilcoFiers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels off that this check is still here when the caller is now also doing an overlapping edgeCount check. Maybe more clear to move this check out of
shadowGroupToColor
and update the.filter
on L32 to filter out based on edgeCount before mapping this function, instead of mapping this function and then filtering based on its return value.