From c39d3c5ea118b5aaa3b821ce211f8ad04fea626e Mon Sep 17 00:00:00 2001 From: Ross Moody Date: Sun, 8 Aug 2021 21:14:23 -0700 Subject: [PATCH] Version 3.8 (#108) * white fill * fix * refaactor filename form * image filename * data uri fetching --- .../components/card/card-action-footer.tsx | 11 +- src/build/components/card/card-actions.tsx | 3 +- src/build/components/card/filename-modal.tsx | 62 +++--- src/build/components/card/image-modal.tsx | 75 ++++--- src/build/components/card/index.tsx | 22 ++- .../components/drawer/drawer-content.tsx | 17 +- src/find/scripts/fetch-svg.ts | 8 +- src/find/scripts/find-svgs.ts | 11 +- src/find/scripts/helpers.ts | 19 +- src/find/scripts/process-svgs.ts | 24 +-- src/find/scripts/svg-class.ts | 112 +++++++---- testing/index.html | 186 ++++++++++++++++++ testing/style.css | 69 +++++++ 13 files changed, 484 insertions(+), 135 deletions(-) create mode 100644 testing/index.html create mode 100644 testing/style.css diff --git a/src/build/components/card/card-action-footer.tsx b/src/build/components/card/card-action-footer.tsx index 6fdc7b13..0206b9f5 100644 --- a/src/build/components/card/card-action-footer.tsx +++ b/src/build/components/card/card-action-footer.tsx @@ -29,9 +29,15 @@ interface CardActionFooter { svgString: string height: number width: number + whiteFill: boolean } -const CardActionFooter = ({ svgString, height, width }: CardActionFooter) => { +const CardActionFooter = ({ + svgString, + height, + width, + whiteFill, +}: CardActionFooter) => { const [showModal, setShowModal] = useState(false) const [showDrawer, setShowDrawer] = useState(false) const [showOgModal, setShowOgModal] = useState(false) @@ -84,7 +90,7 @@ const CardActionFooter = ({ svgString, height, width }: CardActionFooter) => { aria-label="Options" borderRadius="md" /> - + } onClick={() => setShowOptimizedModal(true)} @@ -126,6 +132,7 @@ const CardActionFooter = ({ svgString, height, width }: CardActionFooter) => { svgString={svgString} height={height} width={width} + whiteFill={whiteFill} /> )} diff --git a/src/build/components/card/card-actions.tsx b/src/build/components/card/card-actions.tsx index d952bb9e..321db124 100644 --- a/src/build/components/card/card-actions.tsx +++ b/src/build/components/card/card-actions.tsx @@ -10,7 +10,7 @@ interface CardActions { } const CardActions = ({ data }: CardActions) => { - const { cors, imgSrcHref, svgString, height, width } = data + const { cors, imgSrcHref, svgString, height, width, whiteFill } = data if (cors && imgSrcHref) { return @@ -20,6 +20,7 @@ const CardActions = ({ data }: CardActions) => { svgString={svgString!} height={height || 24} width={width || 24} + whiteFill={whiteFill} /> ) } diff --git a/src/build/components/card/filename-modal.tsx b/src/build/components/card/filename-modal.tsx index f9b3bf8c..ab10e6bf 100644 --- a/src/build/components/card/filename-modal.tsx +++ b/src/build/components/card/filename-modal.tsx @@ -29,7 +29,6 @@ const FilenameModal = ({ download, }: PopoverFormTypes) => { const [filename, setFilename] = useState('svg-gobbler') - const firstFieldRef = useRef(null) return ( @@ -41,38 +40,37 @@ const FilenameModal = ({ isCentered > - - {title} - - - - Filename - - setFilename(event.target.value)} - /> - .svg - - - +
{ + download(svgString, filename) + callback(false) + }} + > + + {title} + + + + Filename + + setFilename(event.target.value)} + /> + .svg + + + - - - - + + + + +
) } diff --git a/src/build/components/card/image-modal.tsx b/src/build/components/card/image-modal.tsx index 65bab937..5e2aadd8 100644 --- a/src/build/components/card/image-modal.tsx +++ b/src/build/components/card/image-modal.tsx @@ -9,7 +9,6 @@ import { ModalFooter, ModalBody, ModalCloseButton, - FormControl, FormLabel, Box, Center, @@ -19,6 +18,7 @@ import { InputGroup, Text, useColorModeValue, + FormControl, } from '@chakra-ui/react' import handle from '../utils/actions' @@ -29,6 +29,7 @@ interface ImageModalProps { svgString: string height: number width: number + whiteFill: boolean } const ImageModal = ({ @@ -36,10 +37,13 @@ const ImageModal = ({ svgString, height, width, + whiteFill, }: ImageModalProps) => { const [size, setSize] = React.useState({ height, width }) + const [filename, setFilename] = React.useState('svg-image') const state = new SVGImage(svgString, height, width) + const whiteFillBg = useColorModeValue('gray.100', 'null') return ( callback(false)} size="lg"> @@ -48,7 +52,7 @@ const ImageModal = ({ Export image -
+
+ {whiteFill && ( + + )}
- Resize + Configure - Specify height or width to resize the SVG proportionally before - export. + Specify height, width, or filename before export. The SVG will + proportionally resize based on height or width. + @@ -139,26 +154,38 @@ const ImageModal = ({ - - + + Filename + + setFilename(event.target.value)} + /> + .png + + + + diff --git a/src/build/components/card/index.tsx b/src/build/components/card/index.tsx index 46010ec2..a4cf58ee 100644 --- a/src/build/components/card/index.tsx +++ b/src/build/components/card/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { Center, Box, @@ -18,14 +18,16 @@ interface CardData { } const Card = ({ data }: CardData) => { - const [showActions, setShowActions] = React.useState(false) + const [showActions, setShowActions] = useState(false) + const { presentationSvg, size, type, whiteFill } = data - const { presentationSvg, size, type } = data + const cardFillBg = useColorModeValue('white', 'gray.700') + const whiteFillBg = useColorModeValue('gray.200', 'null') return ( setShowActions(true)} @@ -33,7 +35,7 @@ const Card = ({ data }: CardData) => { onMouseLeave={() => setShowActions(false)} maxWidth="280px" > -
+
{ padding="0 0 100%" dangerouslySetInnerHTML={{ __html: presentationSvg! }} overflow="hidden" + zIndex={1} sx={{ '& > svg': { position: 'absolute', @@ -55,6 +58,15 @@ const Card = ({ data }: CardData) => { }, }} /> + {whiteFill && ( + + )}
diff --git a/src/build/components/drawer/drawer-content.tsx b/src/build/components/drawer/drawer-content.tsx index d7c50b8e..31fec667 100644 --- a/src/build/components/drawer/drawer-content.tsx +++ b/src/build/components/drawer/drawer-content.tsx @@ -1,5 +1,12 @@ import React from 'react' -import { Box, Center, Flex, Stack, Divider } from '@chakra-ui/react' +import { + Box, + Center, + Flex, + Stack, + Divider, + useColorModeValue, +} from '@chakra-ui/react' import { SVGOConfig } from '../../types' @@ -27,6 +34,8 @@ function DrawerContent({ svgString }: DrawerContent) { const [radioGroup, setRadioGroup] = React.useState('default') const [isReact, setIsReact] = React.useState(false) + const whiteFillBg = useColorModeValue('gray.100', 'null') + React.useEffect(() => { const newString = runSvgo(svgString, config) if (isReact) { @@ -59,14 +68,16 @@ function DrawerContent({ svgString }: DrawerContent) { p={4} dangerouslySetInnerHTML={{ __html: svgString }} overflow="hidden" + bg={whiteFillBg} sx={{ '& > svg': { - height: '90%', - width: '90%', + height: '80%', + width: '80%', overflow: 'visible', }, }} /> + { const imgSrcResponse = await fetchFromUrl(imgSrcHref) if (imgSrcResponse) { - this.originalElementRef = imgSrcResponse + this.elementClone = imgSrcResponse } else { this.cors = true } @@ -44,7 +44,7 @@ async function fetchSVGContent(this: SVG): Promise { if (hasSymbolElements) { this.spriteSymbolArray = symbolElements } else { - this.originalElementRef = spriteResponse + this.elementClone = spriteResponse } } @@ -52,7 +52,7 @@ async function fetchSVGContent(this: SVG): Promise { const dataResponse = await fetchFromUrl(dataSrcHref) if (dataResponse) { - this.originalElementRef = dataResponse + this.elementClone = dataResponse } else { this.type = 'invalid' } @@ -61,4 +61,4 @@ async function fetchSVGContent(this: SVG): Promise { return this } -export { fetchSVGContent } +export { fetchSVGContent, fetchFromUrl } diff --git a/src/find/scripts/find-svgs.ts b/src/find/scripts/find-svgs.ts index 7122dd29..3b9a9319 100644 --- a/src/find/scripts/find-svgs.ts +++ b/src/find/scripts/find-svgs.ts @@ -10,21 +10,16 @@ function findSVGs(): PageElement[] { const svgTags = Array.from(document.querySelectorAll('svg')) const objDatas = Array.from(document.querySelectorAll('object[data*=".svg"]')) const symbolElements = Array.from(document.getElementsByTagName('symbol')) - const imgSrcs = Array.from(document.querySelectorAll('img')).filter( - (element) => element.src - ) - const pageDivs = Array.from(document.querySelectorAll('div')).filter( - (element) => element.style.backgroundImage - ) + const imgSrcs = Array.from(document.querySelectorAll('img')) + const divs = Array.from(document.querySelectorAll('div')) const gElements = Array.from(document.getElementsByTagName('g')).filter( (element) => element.id ) - const pageSVGs = [ ...svgTags, ...imgSrcs, ...objDatas, - ...pageDivs, + ...divs, ...symbolElements, ...gElements, ] diff --git a/src/find/scripts/helpers.ts b/src/find/scripts/helpers.ts index 9c33d7f5..6740d3f8 100644 --- a/src/find/scripts/helpers.ts +++ b/src/find/scripts/helpers.ts @@ -12,11 +12,11 @@ function dedupSVGs(svg: SVG, index: number, originalArray: SVG[]) { function convertElementRefToSVGString(this: SVG) { const serializer = new XMLSerializer() - this.svgString = serializer.serializeToString(this.originalElementRef) + this.svgString = serializer.serializeToString(this.elementClone) } function removeFillNone(this: SVG) { - const svgElement = this.originalElementRef + const svgElement = this.elementClone const fill = svgElement.getAttribute('fill') const stroke = svgElement.getAttribute('stroke') @@ -28,19 +28,19 @@ function removeFillNone(this: SVG) { } function removeClass(this: SVG) { - const svgElement = this.originalElementRef + const svgElement = this.elementClone svgElement.removeAttribute('class') } function setViewBox(this: SVG) { - const svgElement = this.originalElementRef + const svgElement = this.elementClone const hasViewbox = svgElement.hasAttribute('viewBox') if (hasViewbox) this.viewBox = svgElement.getAttribute('viewBox')! } function setWidthHeight(this: SVG) { - const svgElement = this.originalElementRef + const svgElement = this.elementClone let width: string | undefined let height: string | undefined @@ -87,7 +87,7 @@ function setSize(this: SVG) { } function createPresentationSvg(this: SVG) { - const htmlElement = this.originalElementRef.cloneNode(true) as HTMLElement + const htmlElement = this.elementClone.cloneNode(true) as HTMLElement const isCorsRestricted = this.cors @@ -99,6 +99,12 @@ function createPresentationSvg(this: SVG) { this.presentationSvg = new XMLSerializer().serializeToString(htmlElement) } +function hasWhiteFill(this: SVG) { + const whiteFills = ['#FFF', '#fff', '#FFFFFF', '#ffffff', 'white'] + const svgOuterHtml = this.elementClone.outerHTML + this.whiteFill = whiteFills.some((fill) => svgOuterHtml.includes(fill)) +} + export { dedupSVGs, removeFillNone, @@ -108,4 +114,5 @@ export { setSize, removeClass, createPresentationSvg, + hasWhiteFill, } diff --git a/src/find/scripts/process-svgs.ts b/src/find/scripts/process-svgs.ts index 1f410613..6295baec 100644 --- a/src/find/scripts/process-svgs.ts +++ b/src/find/scripts/process-svgs.ts @@ -10,6 +10,7 @@ import { removeFillNone, removeClass, createPresentationSvg, + hasWhiteFill, } from './helpers' const filterInvalid = (svg: SVG) => svg.type !== 'invalid' @@ -24,19 +25,16 @@ async function processSVGs() { .map((ele) => fetchSVGContent.call(ele)) ) - // ! this needs improved - // The symbols can't be built until the result of the spriteHref - // fetch call is made. This also results in exponential duplicate symbol builds - // that need immediately deduped + // Process the result of finding symbol sheets in + // remotely located SVG fetch calls const validSVGs = preliminarySVGs.flatMap((svg) => { const hasSpriteSymbolArray = Boolean(svg.spriteSymbolArray) - if (!hasSpriteSymbolArray) return svg - const symbolSvgs = svg.spriteSymbolArray!.map((symbol) => { - return new SVG(symbol) - }) - - return symbolSvgs + if (hasSpriteSymbolArray) { + return svg.spriteSymbolArray!.map((symbol) => new SVG(symbol)) + } else { + return svg + } }) const processedSVGs = validSVGs @@ -54,10 +52,14 @@ async function processSVGs() { setSize.call(svg) convertElementRefToSVGString.call(svg) createPresentationSvg.call(svg) + hasWhiteFill.call(svg) // Must delete reference to DOM Node for sending messages // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - delete svg.originalElementRef + delete svg.elementClone + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete svg.originalElementReference return svg }) diff --git a/src/find/scripts/svg-class.ts b/src/find/scripts/svg-class.ts index 949cae2f..4aafb6c9 100644 --- a/src/find/scripts/svg-class.ts +++ b/src/find/scripts/svg-class.ts @@ -12,9 +12,11 @@ type SVGType = | 'g' export class SVGClass { - originalElementRef: PageElement + originalElementReference: PageElement + elementClone: PageElement type: SVGType = 'invalid' cors = false + whiteFill = false spriteHref?: string imgSrcHref?: string @@ -31,16 +33,29 @@ export class SVGClass { readonly id = Math.random() constructor(element: PageElement) { - this.originalElementRef = element.cloneNode(true) as HTMLElement + this.originalElementReference = element + this.elementClone = element.cloneNode(true) as HTMLElement this.determineType() this.setSpriteHref() this.buildSymbolElement() this.buildGElement() } + private hasBase64BgImg(string: string) { + return string.includes('data:image/svg+xml;base64') + } + + private hasDataUriBgImg(string: string) { + return string.includes('data:image/svg+xml;utf8') + } + + private hasSvgFilename(string: string) { + return string.includes('.svg') + } + private determineType() { - const tagName = this.originalElementRef.tagName - const svgElement = this.originalElementRef + const tagName = this.elementClone.tagName + const svgElement = this.elementClone const querySvgTag = (tag: keyof SVGElementTagNameMap) => Array.from(svgElement.querySelectorAll(tag)) @@ -75,46 +90,67 @@ export class SVGClass { break } - case 'IMG': { - const imageSourceHref = (this.originalElementRef as HTMLImageElement) - .src + case 'OBJECT': { + this.type = 'object' + this.dataSrcHref = (this.elementClone as HTMLObjectElement).data - if (!imageSourceHref) return + break + } - const imgSrcFileType = imageSourceHref.split('.').pop() - const hasSVGFileType = imgSrcFileType === 'svg' - const hasBase64 = imageSourceHref.includes('data:image/svg+xml;base64') + case 'IMG': { + const imgSrc = (this.elementClone as HTMLImageElement).src - if (hasSVGFileType || hasBase64) { + if (this.hasBase64BgImg(imgSrc) || this.hasSvgFilename(imgSrc)) { this.type = 'img src' - this.imgSrcHref = imageSourceHref + this.imgSrcHref = imgSrc } - break - } + if (this.hasDataUriBgImg(imgSrc)) { + const regex = /(?=)/ + const svgString = regex.exec(imgSrc) - case 'OBJECT': { - this.type = 'object' - this.dataSrcHref = (this.originalElementRef as HTMLObjectElement).data + if (svgString) { + const { documentElement } = new DOMParser().parseFromString( + svgString[0], + 'image/svg+xml' + ) + + this.elementClone = documentElement + this.type = 'img src' + } + } break } case 'DIV': { - const imgElement = this.originalElementRef as HTMLDivElement - const computedStyle = window.getComputedStyle(imgElement, null) - const bgImgUrl = computedStyle.backgroundImage - .slice(4, -1) - .replace(/"/g, '') - - if (!bgImgUrl) return - - const fileType = bgImgUrl.substr(bgImgUrl.lastIndexOf('.') + 1) - const isSVGFileType = /(svg)$/gi.test(fileType) - - if (isSVGFileType) { + const divElement = this.originalElementReference as HTMLDivElement + const backgroundImageUrl = + window.getComputedStyle(divElement).backgroundImage + + if ( + this.hasBase64BgImg(backgroundImageUrl) || + this.hasSvgFilename(backgroundImageUrl) + ) { this.type = 'bg img' - this.imgSrcHref = bgImgUrl + this.imgSrcHref = backgroundImageUrl + } + + if (this.hasDataUriBgImg(backgroundImageUrl)) { + const regex = /(?=)/ + const regexResult = regex.exec(backgroundImageUrl) + const validRegex = Boolean(regexResult && regexResult[0]) + + if (validRegex) { + const string = regexResult![0].replace(/\\/g, '') + const { documentElement } = new DOMParser().parseFromString( + string, + 'image/svg+xml' + ) + + this.type = 'bg img' + this.elementClone = documentElement + } } break @@ -128,9 +164,7 @@ export class SVGClass { private buildSymbolElement() { if (this.type !== 'symbol') return - const symbolElement = this.originalElementRef.cloneNode( - true - ) as SVGSymbolElement + const symbolElement = this.elementClone.cloneNode(true) as SVGSymbolElement symbolElement.removeAttribute('fill') @@ -154,20 +188,20 @@ export class SVGClass { useElement.setAttributeNS( 'http://www.w3.org/1999/xlink', 'xlink:href', - `#${this.originalElementRef.id}` + `#${this.elementClone.id}` ) svgElement.appendChild(symbolElement) svgElement.appendChild(useElement) this.type = 'sprite' - this.originalElementRef = svgElement + this.elementClone = svgElement } private buildGElement() { if (this.type !== 'g') return - const gElement = this.originalElementRef.cloneNode(true) as SVGGElement + const gElement = this.elementClone.cloneNode(true) as SVGGElement const gId = gElement.id const viewBox = gElement.getAttribute('viewBox') @@ -197,14 +231,14 @@ export class SVGClass { svgElement.appendChild(useElement) this.type = 'sprite' - this.originalElementRef = svgElement + this.elementClone = svgElement } private setSpriteHref() { const isSpriteInstance = this.type === 'sprite' if (!isSpriteInstance) return - const useElement = this.originalElementRef.querySelector('use')! + const useElement = this.elementClone.querySelector('use')! const ownerDocument = document.URL const xlinkHref = useElement.getAttribute('xlink:href') diff --git a/testing/index.html b/testing/index.html new file mode 100644 index 00000000..de97e3a2 --- /dev/null +++ b/testing/index.html @@ -0,0 +1,186 @@ + + + + + CodePen - SVG using Data URI (Multiple Ways) + + + + + +
+

Data URI in SVG-as-img

+ +
+ +
+

Data URI in background-image

+
+
+ +
+

Base64 Data URI in SVG-as-img

+ +
+ +
+

Base64 Data URI in background-image

+
+
+ +
+

Img src

+ +
+ +
+

Background image on article tag

+
+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada + fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, + ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam + egestas semper. Aenean ultricies mi vitae est. Mauris placerat + eleifend leo. +

+
+
+ +
+

Symbol different colors

+ + + + + + + +
+ +
+

Inline

+ + + +
+ +
+

Symbol

+ + + + Basketball + + + + + + + + + + + + + + + +
+ + + diff --git a/testing/style.css b/testing/style.css new file mode 100644 index 00000000..b07a0d3a --- /dev/null +++ b/testing/style.css @@ -0,0 +1,69 @@ +.data-uri { + /* NO WHITESPACE in */ + background: url('data:image/svg+xml;utf8,'); + width: 100px; + height: 100px; +} + +.base64 { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiIGhlaWdodD0iMTAwcHgiIHdpZHRoPSIxMDBweCI+CjxnPgoJPHBhdGggZD0iTTI4LjEsMzYuNmM0LjYsMS45LDEyLjIsMS42LDIwLjksMS4xYzguOS0wLjQsMTktMC45LDI4LjksMC45YzYuMywxLjIsMTEuOSwzLjEsMTYuOCw2Yy0xLjUtMTIuMi03LjktMjMuNy0xOC42LTMxLjMgICBjLTQuOS0wLjItOS45LDAuMy0xNC44LDEuNEM0Ny44LDE3LjksMzYuMiwyNS42LDI4LjEsMzYuNnoiLz4KCTxwYXRoIGQ9Ik03MC4zLDkuOEM1Ny41LDMuNCw0Mi44LDMuNiwzMC41LDkuNWMtMyw2LTguNCwxOS42LTUuMywyNC45YzguNi0xMS43LDIwLjktMTkuOCwzNS4yLTIzLjFDNjMuNywxMC41LDY3LDEwLDcwLjMsOS44eiIvPgoJPHBhdGggZD0iTTE2LjUsNTEuM2MwLjYtMS43LDEuMi0zLjQsMi01LjFjLTMuOC0zLjQtNy41LTctMTEtMTAuOGMtMi4xLDYuMS0yLjgsMTIuNS0yLjMsMTguN0M5LjYsNTEuMSwxMy40LDUwLjIsMTYuNSw1MS4zeiIvPgoJPHBhdGggZD0iTTksMzEuNmMzLjUsMy45LDcuMiw3LjYsMTEuMSwxMS4xYzAuOC0xLjYsMS43LTMuMSwyLjYtNC42YzAuMS0wLjIsMC4zLTAuNCwwLjQtMC42Yy0yLjktMy4zLTMuMS05LjItMC42LTE3LjYgICBjMC44LTIuNywxLjgtNS4zLDIuNy03LjRjLTUuMiwzLjQtOS44LDgtMTMuMywxMy43QzEwLjgsMjcuOSw5LjgsMjkuNyw5LDMxLjZ6Ii8+Cgk8cGF0aCBkPSJNMTUuNCw1NC43Yy0yLjYtMS02LjEsMC43LTkuNywzLjRjMS4yLDYuNiwzLjksMTMsOCwxOC41QzEzLDY5LjMsMTMuNSw2MS44LDE1LjQsNTQuN3oiLz4KCTxwYXRoIGQ9Ik0zOS44LDU3LjZDNTQuMyw2Ni43LDcwLDczLDg2LjUsNzYuNGMwLjYtMC44LDEuMS0xLjYsMS43LTIuNWM0LjgtNy43LDctMTYuMyw2LjgtMjQuOGMtMTMuOC05LjMtMzEuMy04LjQtNDUuOC03LjcgICBjLTkuNSwwLjUtMTcuOCwwLjktMjMuMi0xLjdjLTAuMSwwLjEtMC4yLDAuMy0wLjMsMC40Yy0xLDEuNy0yLDMuNC0yLjksNS4xQzI4LjIsNDkuNywzMy44LDUzLjksMzkuOCw1Ny42eiIvPgoJPHBhdGggZD0iTTI2LjIsODguMmMzLjMsMiw2LjcsMy42LDEwLjIsNC43Yy0zLjUtNi4yLTYuMy0xMi42LTguOC0xOC41Yy0zLjEtNy4yLTUuOC0xMy41LTktMTcuMmMtMS45LDgtMiwxNi40LTAuMywyNC43ICAgQzIwLjYsODQuMiwyMy4yLDg2LjMsMjYuMiw4OC4yeiIvPgoJPHBhdGggZD0iTTMwLjksNzNjMi45LDYuOCw2LjEsMTQuNCwxMC41LDIxLjJjMTUuNiwzLDMyLTIuMyw0Mi42LTE0LjZDNjcuNyw3Niw1Mi4yLDY5LjYsMzcuOSw2MC43QzMyLDU3LDI2LjUsNTMsMjEuMyw0OC42ICAgYy0wLjYsMS41LTEuMiwzLTEuNyw0LjZDMjQuMSw1Ny4xLDI3LjMsNjQuNSwzMC45LDczeiIvPgo8L2c+Cjwvc3ZnPg==); + width: 100px; + height: 100px; +} + +.example { + padding: 10px; + background: #eee; + border: 1px solid #ccc; + margin: 20px; +} + +article { + background: white; + width: 40%; + padding: 20px; + position: relative; +} + +article::after { + content: ''; + position: absolute; + top: 100%; + left: 0; + height: 20px; + width: 100%; + background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/rip.svg) bottom + center; + background-size: 150%; +} + +.icon-basketball { + width: 100px; + height: 100px; + fill: red; +} + +h2 { + margin: 0; +} + +.logo { + width: 150px; + height: 150px; +} + +.logo { + fill: black; +} +.logo-2 { + fill: red; + color: orange; +} +.logo-3 { + fill: lightblue; + color: green; +} + +.inner-box { + fill: currentColor; +}