Skip to content

Commit

Permalink
complete background image size support, basic repeat support
Browse files Browse the repository at this point in the history
  • Loading branch information
drinking-code committed Jun 27, 2021
1 parent 4c8c074 commit 5a63e4e
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 73 deletions.
3 changes: 1 addition & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"presets": [
"@babel/react",
"@babel/env",
"minify"
"@babel/env"
],
"plugins": [
"@babel/plugin-proposal-class-properties"
Expand Down
80 changes: 57 additions & 23 deletions src/css-utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import CSS_COLOR_NAMES from "./html-colors";
import toPx from "./css-length-converter";
import {element} from 'prop-types'

function _getAttributeFromString(string, method, ...data) {
if (!string) return false
Expand All @@ -12,16 +11,16 @@ function _getAttributeFromString(string, method, ...data) {
}
}

function _getColor(border, i) {
const val = border[i]
function _getColor(b, i) {
const val = b[i]
// color is a hex code
if (val.toLowerCase().match(/#([0-9a-f]{3}){1,2}/)) return val
// color is a function (rgb, rgba, hsl, hsla)
if (val.startsWith('rgb') || val.startsWith('hsl')) {
let color = val;
if (!val.endsWith(')'))
for (let j = 1; !border[i + j - 1].endsWith(')'); j++) {
color += border[i + j]
for (let j = 1; !b[i + j - 1].endsWith(')'); j++) {
color += b[i + j]
}
if (color[3] === 'a')
color = color.replace('a', '').replace(/,[^),]+\)/, ')')
Expand All @@ -37,13 +36,13 @@ function _getColor(border, i) {
return false
}

function _getImage(border, i) {
const val = border[i]
function _getImage(b, i) {
const val = b[i]

if (val.startsWith('url')) {
let url = val;
for (let j = 1; border[i + j]; j++) {
url += border[i + j]
for (let j = 1; b[i + j]; j++) {
url += b[i + j]
}
url = /url\(("[^"]+"|'[^']+'|[^)]+)\)/g.exec(url)
url = url ? url[1] : false
Expand All @@ -54,38 +53,66 @@ function _getImage(border, i) {
}
}

function _getImageSize(border, i, element) {
const val = border[i]
function _getImageSize(b, i, element) {
// "val" is always what is defined in backgrund-size ([i]th argument)
const val = b[i]

if (['cover', 'contain'].includes(val) || val.endsWith('%')) {
return val
if (['cover', 'contain'].includes(val)) {
return [val, null]
}
unitCheck(val, htmlBackgroundSizeNotSvgError)
if (val.match(/(\d+(\.\d+)?(ch|cm|em|ex|in|mm|pc|pt|px|rem|vh|vmax|vmin|vw)|0)/))
return toPx(element, val)

const returnIfCSSNumeric = (val, throwErr) => {
if (val?.endsWith('%'))
return val
else if (val?.match(/(\d+(\.\d+)?(ch|cm|em|ex|in|mm|pc|pt|px|rem|vh|vmax|vmin|vw)|0)/)) {
unitCheck(val, throwErr ? htmlBackgroundSizeNotSvgError : undefined)
return toPx(element, val)
} else
return null
}

const convertedVal = returnIfCSSNumeric(val, true) // has null as fallback already
// "background-size: 50% 50%" is different to "background-size: 50%"
return [convertedVal, returnIfCSSNumeric(b[i + 1])]
}

function _getOpacity(border, i) {
let val = border[i]
function _getOpacity(b, i) {
let val = b[i]
if (val.startsWith('rgba') || val.startsWith('hsla')) {
if (!val.endsWith(')'))
for (let j = 1; !border[i + j - 1].endsWith(')'); j++) {
val += border[i + j]
for (let j = 1; !b[i + j - 1].endsWith(')'); j++) {
val += b[i + j]
}
return val.replace(/(rgb|hsl)a?\(([^,)]+,){3}/, '').replace(/\)$/, '')
}
if (border.length - 1 === i)
if (b.length - 1 === i)
return 1
}

function _getRepeat(b, i) {
let val = b[i]
if (val === 'repeat-x')
return ['repeat', 'no-repeat']
else if (val === 'repeat-y')
return ['no-repeat', 'repeat']
else if (['repeat', 'space', 'round', 'no-repeat'].includes(val)) {
if (['repeat', 'space', 'round', 'no-repeat'].includes(b[i + 1] || ''))
return [val, b[i + 1]]
else
return [val, val]
}
}

const htmlLengthNotSvgErrorTemplate = (a, b) => `<RoundDiv> ${a} must be ${b ? `either ${b}, or` : ''} in one of the following units: ch, cm, em, ex, in, mm, pc, pt, px, rem, vh, vmax, vmin, vw.`
const htmlBorderLengthNotSvgError =
new Error(htmlLengthNotSvgErrorTemplate('border lengths', '"thin", "medium", "thick"'))
const htmlBackgroundSizeNotSvgError =
new Error(htmlLengthNotSvgErrorTemplate('background size', '"cover", "contain"'))

function unitCheck(length, err) {
if (length?.match(/(cap|ic|lh|rlh|vi|vm|vb|Q|mozmm)/g)) throw err
if (length?.match(/(cap|ic|lh|rlh|vi|vm|vb|Q|mozmm)/g))
if (err) throw err
else return false
return length
}

Expand All @@ -104,10 +131,17 @@ function _getWidth(border, i, element) {
return false
}

/** @returns {number} */
const getWidth = (s, el) => _getAttributeFromString(s, _getWidth, el),
/** @returns {string} */
getImage = s => _getAttributeFromString(s, _getImage),
/** @returns {Array<string|number>} */
getImageSize = (s, el) => _getAttributeFromString(s, _getImageSize, el),
/** @returns {string} */
getColor = s => _getAttributeFromString(s, _getColor),
/** @returns {Array<string>} */
getRepeat = s => _getAttributeFromString(s, _getRepeat),
/** @returns {number} */
getOpacity = s => _getAttributeFromString(s, _getOpacity)

export {getWidth, getImage, getImageSize, getColor, getOpacity}
export {getWidth, getImage, getImageSize, getColor, getRepeat, getOpacity}
76 changes: 52 additions & 24 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import ShadowRoot from "./react-shadow-dom"
import attachCSSWatcher from './style-sheet-watcher'

export default function RoundDiv({clip, style, children, ...props}) {
// welcome to react states hell
const [height, setHeight] = useState(0)
const [width, setWidth] = useState(0)
const [radius, setRadius] = useState(0)

const [background, setBackground] = useState('transparent')
const [backgroundImage, setBackgroundImage] = useState('none')
const [backgroundImageSize, setBackgroundImageSize] = useState(null)
// todo: background size two values (from css)
const [backgroundImageSize, setBackgroundImageSize] = useState([null, null])
const [backgroundOpacity, setBackgroundOpacity] = useState(0)
const [backgroundRepeat, setBackgroundRepeat] = useState(['repeat', 'repeat'])

const [borderColor, setBorderColor] = useState('transparent')
const [borderWidth, setBorderWidth] = useState(0)
const [borderOpacity, setBorderOpacity] = useState(1)
Expand All @@ -35,6 +40,7 @@ export default function RoundDiv({clip, style, children, ...props}) {
setBackgroundImage,
setBackgroundImageSize,
setBackgroundOpacity,
setBackgroundRepeat,
setBorderColor,
setBorderWidth,
setBorderOpacity
Expand All @@ -55,40 +61,56 @@ export default function RoundDiv({clip, style, children, ...props}) {
divStyle.borderColor = 'transparent'

const [backgroundImageAspectRatio, setBackgroundImageAspectRatio] = useState(1)
const [backgroundImageHeight, setBackgroundImageHeight] = useState(0)
const [backgroundImageWidth, setBackgroundImageWidth] = useState(0)
// const [backgroundImageHeight, setBackgroundImageHeight] = useState(0)
// const [backgroundImageWidth, setBackgroundImageWidth] = useState(0)
useEffect(() => {
const img = new Image()
img.onload = () => {
setBackgroundImageAspectRatio(img.naturalWidth / img.naturalHeight)
setBackgroundImageHeight(img.naturalHeight)
setBackgroundImageWidth(img.naturalWidth)
// setBackgroundImageHeight(img.naturalHeight)
// setBackgroundImageWidth(img.naturalWidth)
}
img.src = backgroundImage
}, [backgroundImage, setBackgroundImageAspectRatio])

const fullHeight = height + borderWidth * 2,
fullWidth = width + borderWidth * 2

console.log(backgroundImageSize)

const lengthCalculator = (isWidth) => {
if ((!isWidth && backgroundImageAspectRatio > 1)
|| (isWidth && backgroundImageAspectRatio < 1)
|| !backgroundImageSize
) return undefined
let n = isWidth ? 0 : 1

if (backgroundImageSize[0] === 'contain')
if (backgroundImageAspectRatio > 1)
return isWidth ? width : (height / backgroundImageAspectRatio)
else
return isWidth ? (width * backgroundImageAspectRatio) : height

if (typeof backgroundImageSize === 'number')
return backgroundImageSize
if (['cover', 'contain'].includes(backgroundImageSize[0]))
return isWidth ? width : height

if (['cover', 'contain'].includes(backgroundImageSize))
return fullHeight
if (backgroundImageSize[n] === null && !!backgroundImageSize[0])
return lengthCalculator(true) *
(backgroundImageAspectRatio < 1
? 1 / backgroundImageAspectRatio
: backgroundImageAspectRatio
)

if (backgroundImageSize.endsWith('%'))
return (isWidth ? fullWidth : fullHeight)
* (Number(backgroundImageSize.replace('%', '')) / 100)
if (!backgroundImageSize[n])
return undefined

if (backgroundImageSize[n]?.endsWith('%'))
return width * (Number(backgroundImageSize[n].replace('%', '')) / 100)

if (typeof backgroundImageSize[n] === 'number')
return backgroundImageSize[n]
}

const imageHeight = lengthCalculator(false),
imageWidth = lengthCalculator(true),
preserveImageAspectRatio = (
['cover', 'contain'].includes(backgroundImageSize[0])
)

return <div {...props} style={divStyle} ref={div}>
<ShadowRoot>
<svg viewBox={`0 0 ${width} ${height}`} style={{
Expand All @@ -102,13 +124,19 @@ export default function RoundDiv({clip, style, children, ...props}) {
<path d={
generateSvgSquircle(fullHeight, fullWidth, radius, clip)
} id="shape"/>

{/* todo: support for "repeat: space" and "repeat: round" */}
<pattern id="bg" patternUnits="userSpaceOnUse"
width={fullWidth} height={fullHeight}>
<image href={backgroundImage} x={borderWidth} y={borderWidth}
preserveAspectRatio={'xMinYMin ' + (backgroundImageSize === 'contain' ? 'meet' : 'slice')}
height={lengthCalculator(false)}
width={lengthCalculator(true)}/>
width={backgroundRepeat[0] === 'no-repeat' ? fullWidth : imageWidth}
height={backgroundRepeat[1] === 'no-repeat' ? fullHeight : imageHeight}
x={borderWidth} y={borderWidth}>
<image href={backgroundImage}
preserveAspectRatio={preserveImageAspectRatio ? 'xMinYMin ' + (
backgroundImageSize[0] === 'contain' || backgroundImageSize[0]?.endsWith('%')
? 'meet'
: 'slice'
) : 'none'}
height={imageHeight}
width={imageWidth}/>
</pattern>

<clipPath id="insideOnly">
Expand Down
39 changes: 15 additions & 24 deletions src/updateStates.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import {getColor, getImage, getImageSize, getOpacity, getWidth} from "./css-utils";
import {getColor, getImage, getImageSize, getOpacity, getRepeat, getWidth} from "./css-utils";
import getStyle from "./styles-extractor";

export default function updateStates(args) {
const {
div,
style,
setHeight,
setWidth,
setRadius,
setBackground,
setBackgroundImage,
setBackgroundImageSize,
setBackgroundOpacity,
setBorderColor,
setBorderWidth,
setBorderOpacity
} = args
const {div, style, setHeight, setWidth} = args
const boundingClientRect = div.current?.getBoundingClientRect()
if (boundingClientRect) {
setHeight(boundingClientRect.height)
Expand Down Expand Up @@ -54,31 +41,35 @@ export default function updateStates(args) {

const divStyle = div.current ? window?.getComputedStyle(div.current) : null
if (divStyle) {
setRadius(Number(
let states = args
states.setRadius(Number(
(divStyle.borderRadius || divStyle.borderTopLeftRadius)
.replace('px', ''))
)
setBackground(getColor(
states.setBackground(getColor(
getNthStyleAttrOrAlt('background', 'background-color', 1)
) || 'transparent')
setBackgroundImage(getImage(
states.setBackgroundImage(getImage(
getNthStyleAttrOrAlt('background', 'background-image', 1)
) || 'none')
setBackgroundImageSize(getImageSize(
states.setBackgroundImageSize(getImageSize(
getNthStyleAttr('background-size', 1)
) || null)
setBackgroundOpacity(getOpacity(
) || [null, null])
states.setBackgroundOpacity(getOpacity(
getNthStyleAttrOrAlt('background', 'background-color', 1)
) || 1)
states.setBackgroundRepeat(getRepeat(
getNthStyleAttrOrAlt('background', 'background-repeat', 1)
) || ['repeat', 'repeat'])

setBorderColor(getColor(
states.setBorderColor(getColor(
getNthStyleAttrOrAlt('border', 'border-color', 'border-top-color', 1)
) || 'transparent')
setBorderOpacity(getOpacity(
states.setBorderOpacity(getOpacity(
getNthStyleAttrOrAlt('border', 'border-color', 'border-top-color', 1)
) || 1)

setBorderWidth(getWidth(
states.setBorderWidth(getWidth(
getNthStyleAttrOrAlt('border', 'border-width', 'border-top-width', 0),
div.current
) || 0)
Expand Down

0 comments on commit 5a63e4e

Please sign in to comment.