Open Siyfion opened 11 years ago
+1000 for text wrapping!
I would love to have support text clipping and/or automatically adding an ellipsis. Being able to have text limited in length by an svg rectangle for example.
I have implemented some basic text wrapping, alignment (horizontal & vertical) and auto font sizing in my current project. While it's certainly not ready for merging into Snap, it could be a good reference for someone doing something similar...
'use strict'
angular.module('ClientApp')
.factory 'TextObject', ($timeout, FreeTransform) ->
class TextObject
constructor: (@snapSVG, @objectData, @labelPart) ->
@config = @objectData.settings
# Setup the configuration object with some safe default if
# there is any data missing from the object.
@config.x = @config.x || 0
@config.y = @config.y || 0
@config.rt = @config.rt || 0
@config.width = @config.width || 100
@config.height = @config.height || 100
@config.hAlign = @config.hAlign || 'middle'
@config.vAlign = @config.vAlign || 'middle'
@config.fontSize = @config.fontSize || 16
@config.fontFamily = @config.fontFamily || 'Lato'
@config.fontStyle = @config.fontStyle || 'normal'
@config.fontWeight = @config.fontWeight || 'normal'
@config.textDecoration = @config.textDecoration || 'none'
# Set the object to be
# deselected by default
@selected = false
# Create the free transform wrapper ready to attach
@ft = new FreeTransform(@snapSVG, @)
# Load the font used by this element and
# then finally create the element to display to the user
WebFont.load
google:
families: [ @config.fontFamily ]
fontactive: (familyName, fvd) =>
# Create the element with the correct font
@_createElement()
# Attach svgObject value to the template object data.
@objectData._svgObject = @
onSelected: (callbackFn) ->
@onSelectFn = callbackFn
bindingChanged: ->
@_createElement()
_createElement: ->
@text = @objectData.binding
@svgElement.remove() if @svgElement?
fontSize = @config.fontSize
loop
break if @_calculateText(fontSize)
break if fontSize == 1
fontSize -= 1
svgText = @_attemptRender(fontSize)
rectBoundary = @_drawTextBox()
# Create a group holding both the text and the
# rectangle that represents the boundaries.
@svgElement = @snapSVG.group svgText, rectBoundary
# Attach the free transform wrapper
# to correctly position, rotate and scale the object.
@ft.initialize()
if @selected
@ft.showHandles()
@svgElement.click =>
if @onSelectFn?
@selected = true
@onSelectFn()
# Attach this value to the child SVG element group.
@svgElement.parentObject = @
# Attempt to split the text so that it will fit into
# the bounding box provided. Returns false if the font-size
# needs to be lowered.
_calculateText: (fontSize) ->
# Create the array to hold the lines
@lines = []
# Running counter of existing lines height
totalHeight = 0
# Separate the text into paragraphs
paragraphs = _.str.lines(@text || '')
# Parse paragraphs one at a time
i = paragraphs.length
while i--
# Prepare line
line = ''
words = _.str.words paragraphs.shift()
# Add words one at a time
w = words.length
while w--
word = words.shift()
# Try out this line with the word added
testText = @snapSVG.text 0, 0, line + word
testText.attr
'font-family': @config.fontFamily
'font-size': fontSize + 'px'
'font-style': @config.fontStyle
'font-weight': @config.fontWeight
'text-decoration': @config.textDecoration
# Measure for width and height overflow
box = testText.getBBox()
# Delete the SVG element as it's not longer needed
testText.remove()
if box.width <= @config.width
# Add a space before adding the word (if required)
line += (if line.length > 0 then ' ' else '') + word
else if line == ''
# We can't fit a single word into the width
# so we need to reduce the font-size and try again
return false
else
# Add the height of this line to the total
totalHeight += fontSize
if totalHeight > @config.height
# We can't fit the text vertically into the height
# anymore, so reduce the font-size and try again.
return false
else
# Couldn't fit the word on, so push the line (as it stands) on
@lines.push line
# If this is the last word
if w == 0
# We need to check it fits width-wise before just
# pushing it onto the end.
testText = @snapSVG.text 0, 0, word
testText.attr
'font-family': @config.fontFamily
'font-size': fontSize + 'px'
'font-style': @config.fontStyle
'font-weight': @config.fontWeight
'text-decoration': @config.textDecoration
# Measure for width and height overflow
box = testText.getBBox()
# Delete the SVG element as it's not longer needed
testText.remove()
# Check the word width doesn't go over the limit.
if box.width > @config.width
return false
# Add the word that didn't fit to the next line calculation
line = word
# Add the height of this line to the total
totalHeight += fontSize
if totalHeight > @config.height
# We can't fit the text vertically into the height
# anymore, so reduce the font-size and try again.
return false
# Add the last line
@lines.push line
# Return success!
return true
# Attempt to render the final text to the designer,
# taking into account the alignment and positioning settings.
_attemptRender: (fontSize) ->
# Set the text display coords to 0,0 to
# begin with; position will be via the group.
textX = textY = 0.0
# Change simple left, middle, right to the
# correlating font-anchor values.
hAlign = 'middle'
switch @config.hAlign
when 'left'
hAlign = 'start'
when 'middle'
textX = @config.width / 2.0
when 'right'
hAlign = 'end'
textX = @config.width
deltaY = 0
switch @config.vAlign
# Setting 1em for the first line brings it
# inside the box, rather than sitting ontop of it.
when 'top' then deltaY = '1em'
when 'middle' then deltaY = '1em'
when 'bottom'
textY = @config.height
deltaY = -(@lines.length - 1) + 'em'
finalText = @snapSVG.text textX, textY, @lines
finalText.attr
'font-family': @config.fontFamily
'font-size': fontSize + 'px'
'font-style': @config.fontStyle
'font-weight': @config.fontWeight
'text-decoration': @config.textDecoration
'text-anchor': hAlign
tspans = finalText.selectAll('tspan')
for tspan, index in tspans
tspan.attr
'xml:space': 'preserve'
x: textX
dy: if index == 0 then deltaY else '1em'
if @config.vAlign is 'middle'
box = finalText.getBBox()
textY = (@config.height - box.height) / 2.0
finalText.attr
y: textY
return finalText
_drawTextBox: ->
svgRect = @snapSVG.rect 0.0, 0.0, @config.width, @config.height
svgRect.attr
fill: 'white'
'fill-opacity': 0.0
stroke: 'black'
'stroke-width': 0.5
'stroke-dasharray': '5 2'
I think that one of the things that is most lacking from "native" SVG and thus, one of the most useful things that an SVG library could provide, is extended text support for use-cases like word-wrapping, shrink-to-fit, etc.
Essentially, very similar to what the the Prawn PDF library exposes (http://prawn.majesticseacreature.com/manual.pdf page 33-onwards).
This would truely be something worth a lot to developers.