Skip to content

Instantly share code, notes, and snippets.

@zaaack
Created August 16, 2018 10:29
Show Gist options
  • Save zaaack/7d5beeadc3a06c31f52c569f6ca4e1af to your computer and use it in GitHub Desktop.
Save zaaack/7d5beeadc3a06c31f52c569f6ca4e1af to your computer and use it in GitHub Desktop.
SVG auto-wrapped text component for React
import 'core-js/fn/set'
import 'core-js/fn/map'
import React from 'react'
import svgTextSize from 'svg-text-size';
const rightSymbols = new Set(
'“‘[「【﹝〔<‹«{『((<《'.split('')
)
const leftSymbols = new Set(
',,,!!??”\]」】》>’»﹞〕〗〉}))』'.split('')
)
const toggleSymbols = new Map([
['\'', 'right'],
['"', 'right'],
])
// Takes a string, and a width (and svg attrs, if they apply), and returns
// an array of lines, representing the break points in the string.
const textWrap = (text, width, attrs, doc = document, {
_leftSymbols = leftSymbols,
_rightSymbols = rightSymbols,
_toggleSymbols = toggleSymbols,
}) => {
let words = []
let toggleMap = new Map()
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (_rightSymbols.has(char)) {
words.push(char + text[i + 1])
i += 1
} else if (_leftSymbols.has(char)) {
words[words.length - 1] += char
} else if (_toggleSymbols.has(char)) {
let isRightStart = _toggleSymbols.get(char) === 'right'
if (!toggleMap.has(char)) {
toggleMap.set(char, isRightStart ? 'right' : 'left')
}
let stickyDirection = toggleMap.get(char)
if (stickyDirection === 'right') {
words.push(char + text[i + 1])
i += 1
} else {
words[words.length - 1] += char
}
toggleMap.set(char, stickyDirection === 'left' ? 'right' : 'left')
} else if (/^\w+$/.test(char)) {
let last = words[words.length - 1]
if (/^\w+$/.test(last)) {
words[words.length - 1] += char
} else {
words.push(char)
}
} else {
words.push(char)
}
}
let lines = [];
let currentLine = [];
words.forEach(word => {
const newLine = [...currentLine, word];
const size = svgTextSize(newLine.join(''), attrs, doc);
if (size.width > width) {
lines.push(currentLine.join(''));
currentLine = [word];
} else {
currentLine.push(word);
}
});
lines.push(currentLine.join(''));
if (lines[0] === '') { lines.shift(); }
return lines;
};
function getReactAttr(attrs) {
let newAttrs = {}
for (let key in attrs) {
let val = attrs[key]
key = key.replace(/([A-Z])/g, s => '-' + s.toLowerCase())
switch (key) {
case 'font-size':
case 'line-spacing':
case 'letter-spacing':
if (typeof val === 'number') {
val += 'px'
}
break;
default:
break;
}
newAttrs[key] = val
}
return newAttrs
}
/**
```
props: {
spans: [
"some text",
{ tag: 'tspan', text: 'some text', props: { } }
],
width: 100,
x: 0,
y: 0,
}
```
*/
export default class SVGAutoText extends React.PureComponent {
getAttrProps() {
const {
spans, width, x, y,
leftSymbols, rightSymbols, toggleSymbols,
...props } = this.props
return props
}
getWrappedSpans() {
let spanSlices = []
let startIdx = 0
let fullText = ''
let spans = this.props.spans.map(
span => {
if (!span.tag) {
return {
tag: 'tspan',
text: span ? span + '' : '',
props: null,
}
}
span.text += ''
return span
}
)
for (const span of spans) {
let end = startIdx + span.text.length
spanSlices.push({
...span,
start: startIdx,
end,
})
startIdx = end
fullText += span.text
}
let globalProps = this.getAttrProps()
let lines = textWrap(fullText, this.props.width, getReactAttr(globalProps), this.props)
let tspans = []
let lineIdx = 0
let lineLen = 0
let sliceIdx = 0
let sliceLen = 0
let lastLine = -1
let { x: offsetX = 0, y: offsetY = 0 } = this.props
let lineHeight = parseInt(globalProps['lineSpacing'] || globalProps['lineHeight'], 10)
let fontSize = parseInt(globalProps['fontSize'], 10) || 16
function makeTspan(text, props, lineIdx) {
let x = lastLine === lineIdx ? void 0 : offsetX
lastLine = lineIdx
return (
<tspan
key={tspans.length + text}
{...props}
x={x}
y={lineIdx * (lineHeight ? lineHeight : fontSize * 1.4) + offsetY}
>
{text}
</tspan>
)
}
for (const line of lines) {
lineLen += line.length
while (sliceLen < lineLen && sliceIdx < spanSlices.length) {
let slice = spanSlices[sliceIdx]
tspans.push(makeTspan(slice.text, slice.props, lineIdx))
sliceIdx++
sliceLen += slice.text.length
}
if (sliceLen > lineLen) { // break slice
let lastSlice = spanSlices[sliceIdx - 1]
let text1 = lastSlice.text.slice(0, lastSlice.text.length - (sliceLen - lineLen))
let text2 = lastSlice.text.slice(text1.length)
tspans.pop()
tspans.push(makeTspan(text1, lastSlice.props, lineIdx))
tspans.push(makeTspan(text2, lastSlice.props, lineIdx + 1))
}
lineIdx++
}
return tspans
}
render() {
return (
<text {...this.getAttrProps()}>
{this.getWrappedSpans()}
</text>
)
}
}
@zaaack
Copy link
Author

zaaack commented Aug 16, 2018

          <AutoText
            width={50 / 375 * window.innerWidth} // width should be 'px'
            x={47} // x, y can be svg coordinate 
            y={337}
            fill="#494949"
            fontFamily="SourceHanSerif-Regular, Source Han Serif"
            fontSize={13}
            fontWeight="normal"
            lineSpacing={30} // or line-height
            spans={[
              'Helllo,',
              { tag: 'tspan', text: 'Amy', props: { fill: '#B09148' } },
              ', ',
              'what can I do for you?',
            ]}
          />

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment