Line-by-line text background color padding in Fabr

2019-06-07 02:36发布

问题:

I am using the library FabricJS to overlay text on canvas. I need to add padding (ideally just left & right) to a text element that includes the property textBackgroundColor.

Here is what I've tried so far:

let textObject = new fabric.Text('Black & White',{
    fontFamily: this.theme.font,
    fontSize: this.theme.size,
    textBaseline: 'bottom',
    textBackgroundColor: '#000000',
    left: 0,
    top: 0,
    width: 100,
    height: 40,
    padding: 20,
    fill: 'white',
});

The padding doesn't work as I anticipated. I have attempted to use the backgroundColor property but that adds background to the whole group block and not just the text.

I could add a a non-breaking space to achieve the same effect, but this doesn't seem like a reliable solution and I was hoping Fabric JS allowed this out-of-the-box. Any ideas how to achieve this?

Required solution (version on the right, with additional padding is what I would like):

回答1:

I give you 2 answers for two subtly different cases.

Case 1 - padding around the bounding box of single of multi-line text. The code follows the CSS approach where margin is outside of the box, as depicted by the red line, and padding is inside, as shown by the gold background. On the left hand image, black text background is what you get from the built-in 'textBackgroundColor'. The yellow area shows the padding currently applied. The right hand image shows the additional benefit when you harmonise the padding colour, an also that you can reduce opacity on the background whilst keeping the text full-opaque.

BTW the built-in 'padding' attribute for text pads in relation to the controlling border, but the background color fill does not cover the white-space created. In other words, it operates like CSS margin rather than CSS padding.

Therefore it is necessary to ignore this padding attribute, and instead introduce a coloured rect to give the background color required, grouping this with the text element and positioning accordingly.

Example snippet below.

var canvas = window._canvas = new fabric.Canvas('c');

// function to do the drawing. Could easily be accomodated into a class (excluding the canvas reset!) 
function reset(pos)
{

canvas.clear();

// Create the text node - note the position is (0, 0)
var text = new fabric.Text(pos.text, {
    fontFamily: 'Arial',
    left: 0,
    top: 0,
    fill: "#ffffff",
    stroke: "",
    textBackgroundColor: '#000000'
});

// create the outer 'margin' rect, note the position is negatively offset for padding & margin 
// and the width is sized from the dimensions of the text node plus 2 x (padding + margin).
var rectMargin =   new fabric.Rect({
    left: -1 *  (pos.padding.left + pos.margin.left), 
    top: -1 * (pos.padding.top + pos.margin.top),
    width: text.width + ((pos.padding.left + pos.padding.right) + (pos.margin.left + pos.margin.right)), 
    height: text.height + ((pos.padding.top + pos.padding.bottom) + (pos.margin.top + pos.margin.bottom)),
    strokeWidth: pos.border,
    stroke: 'red',
    fill: 'transparent'
})

// create the inner 'padding' rect, note the position is offset for padding only
// and the width is sized from the dimensions of the text node plus 2 x padding.
var rectPadding =   new fabric.Rect({
    width: text.width + (pos.padding.left + pos.padding.right), 
    height: text.height + (pos.padding.top + pos.padding.bottom),
    left: -1 *  pos.padding.left, top: -1 * pos.padding.top,
    fill: 'gold'
})

// create group and add shapes to group, rect first so it is below text.
// note that as the group is oversized, we position it at pos - padding. 
var group = new fabric.Group([ rectMargin, rectPadding, text ], {
  left: pos.x - (pos.padding.left - pos.margin.left),
  top: pos.y - (pos.padding.top - pos.margin.top),
  angle: pos.angle,
});

canvas.add(group);
}

// function to grab values from user inputs
function go()
{
var m = $('#margin').val().split(',');
var p = $('#padding').val().split(',');
for (var i = 0 ; i < 4; i = i + 1)
{
  p[i] = parseInt(p[i], 10); // ensure we have numbers and not strings !
  m[i] = parseInt(m[i], 10);
}


// Object holding position and content info
var pos = {x: 50, y : 10, text: 'Text with padding\nand another line', 
  padding: {top:p[0], right:p[1], bottom: p[2], left: p[3]}, margin: {top:m[0], right:m[1], bottom: m[2], left: m[3]}, border: 1, angle: 10};

reset(pos);
}

// click handler for go button
$('#go').on('click', function(e){
  go();
  
})

// call go once to show on load
go();
div
{
  background-color: silver;
  width: 600px;
  height: 300px;
}
.ipt
{
margin-right: 20px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<p>
<span class='ipt'> Margin: <input id='margin' value = '12,10,12,10' /></span> 
<span class='ipt'> Padding: <input id='padding' value = '0,5,0,5' /></span> 
<span class='ipt'><button id='go' />Go</button></span> 
<div>
  <canvas id="c" width="600" height="300"></canvas>
</div>

Case 2: padding boxing the individual text lines and not the full bounding box.

In this case you can see the difference in how the padded background tracks each line of text instead of applying to the outer bounding box of the text. The solution is more complex, involving creating a dummy text node which then provides line splitting and sizing information. We then loop thru the line data, outputting individual text lines and padding rects into a group which means we can position and handle the text as a single object, as illustrated by the applied angle.

var textIn = 'Text goat\nMillenium jam\nplumb\nBlack & White'

var canvas = window._canvas = new fabric.Canvas('c');

// function to do the drawing. Could easily be accomodated into a class (excluding the canvas reset!) 
function reset(pos)
{

 canvas.clear();
  
// Create the text measuring node - not added to the canvas !
var textMeasure = new fabric.IText(pos.text, {
    fontFamily: 'Arial',
    left: 0,
    top: 0,
    fill: "#ffffff",
    stroke: "",
    textBackgroundColor: '#000000'
});

// loop round the lines in the text creating a margin/pad scenario for each line
var theText, text, textHeight, rectPadding, rectMargin, top = 0, shapes = [];
for (var i = 0; i < textMeasure._textLines.length; i = i + 1){
  theText = textMeasure._textLines[i].join('');
  textHeight = Math.floor(textMeasure.lineHeight * textMeasure.fontSize) //textMeasure.getHeightOfLine(i)

  // Make the text node for line i
  text = new fabric.IText(theText, {
      fontFamily: 'Arial',
      left: 0,
      top: top,
      fill: "#ffffff",
      stroke: ""
  });


  // create the outer 'margin' rect, note the position is negatively offset for padding & margin 
  // and the width is sized from the dimensions of the text node plus 2 x (padding + margin).
  rectMargin =   new fabric.Rect({
      left: -1 *  (pos.padding.left + pos.margin.left), 
      top: top - (pos.padding.top + pos.margin.top),
      width: text.width + ((pos.padding.left + pos.padding.right) + (pos.margin.left + pos.margin.right)), 
      height: textHeight + ((pos.padding.top + pos.padding.bottom) + (pos.margin.top + pos.margin.bottom)),
      fill: 'transparent'
  })
  shapes.push(rectMargin);

  // create the inner 'padding' rect, note the position is offset for padding only
  // and the width is sized from the dimensions of the text node plus 2 x padding.
  rectPadding =   new fabric.Rect({
      width: text.width + (pos.padding.left + pos.padding.right), 
      height: textHeight + (pos.padding.top + pos.padding.bottom),
      left: -1 *  pos.padding.left, 
      top: top - pos.padding.top,
      fill: '#000000ff'
  })
  shapes.push(rectPadding);
  shapes.push(text);

  // move the insert point down by the height of the line
  var gap = 0; // text.lineHeight - textHeight;
  top = top - 1 + textHeight + pos.padding.top + pos.margin.top + pos.padding.bottom + pos.margin.bottom;
}

// At this point we have a list of shapes to output in the shapes[] array.
// Create group and add the shapes to group.
// note that group is positioned so that the topleft of the first text line is where
// it would fall if it were a standard text node. 
var group = new fabric.Group(shapes, {
  left: pos.x - (pos.padding.left - pos.margin.left),
  top: pos.y - (pos.padding.top - pos.margin.top),
  angle: pos.angle,
});

canvas.add(group);

}

// function to grab values from user inputs
function go()
{
var m = $('#margin').val().split(',');
var p = $('#padding').val().split(',');
for (var i = 0 ; i < 4; i = i + 1)
{
  p[i] = parseInt(p[i], 10); // ensure we have numbers and not strings !
  m[i] = parseInt(m[i], 10);
}


// Object holding position and content info
var pos = {x: 70, y : 10, text: textIn, 
  padding: {top:p[0], right:p[1], bottom: p[2], left: p[3]}, margin: {top:m[0], right:m[1], bottom: m[2], left: m[3]}, border: 1, angle: 10};

reset(pos);
}

// click handler for go button
$('#go').on('click', function(e){
  go();
  
})

// call go once to show on load
go();
div
{
  background-color: silver;
  width: 600px;
  height: 100px;
}
.ipt
{
margin-right: 20px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<p>
<span class='ipt'> Margin: <input id='margin' value = '0,0,0,0' /></span> 
<span class='ipt'> Padding: <input id='padding' value = '5,15,5,15' /></span> 
<span class='ipt'><button id='go' />Go</button></span> 
<div>
  <canvas id="c" width="600" height="300"></canvas>
</div>