Add padding to live engraved text on canvas

Viewed 640

I'm working on a solution for live engraving text on a bracelet, using a canvas overlay.

My fiddle is here:

var first = true;
startIt();


function startIt()
{
    canvasDiv = document.getElementById('canvasDiv');
    canvasDiv.innerHTML = '<canvas id="layer0" width="400" height="400"></canvas>'; //for IE
    canvas = document.getElementById('layer0');
    ctx = canvas.getContext('2d');
    ctx.fillStyle = "green";
    ctx.font = "20px Courier New";
    curve = document.getElementById('curve');
    curveText = document.getElementById('text');
    $(curve).keyup(function(e) {changeCurve();});
    $(curveText).keyup(function(e) {changeCurve();});
    
    
    
       
    if (first)
    {
        changeCurve();
        first = false;
    }
    
}

function changeCurve()
{
points = curve.value.split(',');  
    if (points.length == 8)
        drawStack();
  
}

function drawStack()
{
    Ribbon = {maxChar: 50, startX: points[0], startY: points[1], 
              control1X: points[2], control1Y: points[3], 
              control2X: points[4], control2Y: points[5], 
              endX: points[6], endY: points[7]};
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save();
    ctx.beginPath();
    
    ctx.moveTo(Ribbon.startX,Ribbon.startY);
    ctx.bezierCurveTo(Ribbon.control1X,Ribbon.control1Y,
                   Ribbon.control2X,Ribbon.control2Y,
                   Ribbon.endX,Ribbon.endY);
    
    ctx.stroke();
    ctx.restore();
    
   FillRibbon(curveText.value,Ribbon); 
}

function FillRibbon(text,Ribbon)
{

var textCurve = [];
var ribbon = text.substring(0,Ribbon.maxChar);
var curveSample = 1000;


xDist = 0;
var i = 0;
for (i = 0; i < curveSample; i++)
    {
        a = new bezier2(i/curveSample,Ribbon.startX,Ribbon.startY,Ribbon.control1X,Ribbon.control1Y,Ribbon.control2X,Ribbon.control2Y,Ribbon.endX,Ribbon.endY);
        b = new bezier2((i+1)/curveSample,Ribbon.startX,Ribbon.startY,Ribbon.control1X,Ribbon.control1Y,Ribbon.control2X,Ribbon.control2Y,Ribbon.endX,Ribbon.endY);
        c = new bezier(a,b);
        textCurve.push({bezier: a, curve: c.curve});
    }

letterPadding = ctx.measureText("").width / 4; 
w = ribbon.length;
ww = Math.round(ctx.measureText(ribbon).width);


totalPadding = (w-1) * letterPadding;
totalLength = ww + totalPadding;
p = 0;

cDist = textCurve[curveSample-1].curve.cDist;

z = (cDist / 2) - (totalLength / 2);

for (i=0;i<curveSample;i++)
    {
        if (textCurve[i].curve.cDist >= z)
        {
            p = i;
            break;
        }
    }

for (i = 0; i < w ; i++)
    {
        ctx.save();
        ctx.translate(textCurve[p].bezier.point.x,textCurve[p].bezier.point.y);
        ctx.rotate(textCurve[p].curve.rad);
        ctx.fillText(ribbon[i],0,0);
        ctx.restore();
        
        x1 = ctx.measureText(ribbon[i]).width + letterPadding;
        x2 = 0; 
        for (j=p;j<curveSample;j++)
        {
        x2 = x2 + textCurve[j].curve.dist;
        if (x2 >= x1)
            {
                p = j;
                break;
            }
        }
        



    }
} //end FillRibon

function bezier(b1, b2)
{
//Final stage which takes p, p+1 and calculates the rotation, distance on the path and accumulates the total distance
this.rad = Math.atan(b1.point.mY/b1.point.mX);
this.b2 = b2;
this.b1 = b1;
dx = (b2.x - b1.x);
dx2 = (b2.x - b1.x) * (b2.x - b1.x);
this.dist = Math.sqrt( ((b2.x - b1.x) * (b2.x - b1.x)) + ((b2.y - b1.y) * (b2.y - b1.y)) );
xDist = xDist + this.dist;
this.curve = {rad: this.rad, dist: this.dist, cDist: xDist};
}

function bezierT(t,startX, startY,control1X,control1Y,control2X,control2Y,endX,endY)
{
//calculates the tangent line to a point in the curve; later used to calculate the degrees of rotation at this point.
this.mx = (3*(1-t)*(1-t) * (control1X - startX)) + ((6 * (1-t) * t) * (control2X - control1X)) + (3 * t * t * (endX - control2X));
this.my = (3*(1-t)*(1-t) * (control1Y - startY)) + ((6 * (1-t) * t) * (control2Y - control1Y)) + (3 * t * t * (endY - control2Y));
}

function bezier2(t,startX, startY,control1X,control1Y,control2X,control2Y,endX,endY)
{
//Quadratic bezier curve plotter
this.Bezier1 = new bezier1(t,startX,startY,control1X,control1Y,control2X,control2Y);
this.Bezier2 = new bezier1(t,control1X,control1Y,control2X,control2Y,endX,endY);
this.x = ((1 - t) * this.Bezier1.x) + (t * this.Bezier2.x);
this.y = ((1 - t) * this.Bezier1.y) + (t * this.Bezier2.y);
this.slope = new bezierT(t,startX, startY,control1X,control1Y,control2X,control2Y,endX,endY);

this.point = {t: t, x: this.x, y: this.y, mX: this.slope.mx, mY: this.slope.my};
}
function bezier1(t,startX, startY,control1X,control1Y,control2X,control2Y)
{
//linear bezier curve plotter; used recursivly in the quadratic bezier curve calculation
this.x = (( 1 - t) * (1 - t) * startX) + (2 * (1 - t) * t * control1X) + (t * t * control2X);
this.y = (( 1 - t) * (1 - t) * startY) + (2 * (1 - t) * t * control1Y) + (t * t * control2Y);

}
.product-images-container {
  
  width: 400px;
  height: 400px; 
  position:relative;
}
canvas {
  position: absolute;
  top: 0;
}
.productImage {
  
  width: 100%
  
}
.engravingArea {
    position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="product-images-container">
    <input size="80" type="text" id="curve" name="curve" value="40,228,100,260,270,260,330,240">
    <input size="80" type="text" id="text" name="text" value="testing 1234567890">
<div class="engravingArea">
<div id="canvasDiv"></div>
</div>
    
<img class="productImage" src="https://i.imgur.com/BaoChJT.jpeg"> 
</div>

  1. What i wish to achieve, is to centralize the text on the cuff (adding some letter padding to the bottom i suppose?)

  2. The text is a little blurry? Is there anyway to optimize it to be better quality?

  3. I wish to scale the canvas (and text inside) dynamically when its container resizes as well. The container uses percentage width, and we'd like the canvas to follow the exact measures of either the container or the image itself. I tried setting canvas.width = $(".product-images-container").width(), without luck in this case.

Looking forward to hearing your feedback!

1 Answers

Try like this: https://jsfiddle.net/uajfh78p/

var first = true;

var productImage = document.getElementsByClassName('productImage')[0];
var curveOffset = document.getElementById('offset');
var fontSize = document.getElementById('fontsize');

$(productImage).on('load', function() {
    startIt();
});


function startIt()
{
    canvasDiv = document.getElementById('canvasDiv');
    canvasDiv.innerHTML = '<canvas id="layer0" width="' + productImage.naturalWidth + '" height="' + productImage.naturalHeight + '"></canvas>'; //for IE
    canvas = document.getElementById('layer0');
    ctx = canvas.getContext('2d');
    ctx.fillStyle = "green";
    curve = document.getElementById('curve');
    curveText = document.getElementById('text');
    scale = productImage.naturalWidth/400;
    $(curve).keyup(function(e) {changeCurve();});
    $(curveText).keyup(function(e) {changeCurve();});
    
    
    $(curveOffset).on('change', changeCurve);
    $(fontSize).on('change', changeCurve);
    
       
    if (first)
    {
        changeCurve();
        first = false;
    }
    
}

function changeCurve()
{
points = curve.value.split(',');  
offset = (parseInt(curveOffset.value) || 0);
ctx.font = (parseInt(fontSize.value) || 20)*scale + "px Courier New";

    if (points.length == 8)
        drawStack();
        
    
  
}

function drawStack()
{
    Ribbon = {maxChar: 50, startX: points[0]*scale, startY: (points[1] - offset)*scale, 
              control1X: points[2]*scale, control1Y: (points[3] - offset)*scale, 
              control2X: points[4]*scale, control2Y: (points[5] - offset)*scale, 
              endX: points[6]*scale, endY: (points[7] - offset)*scale};
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save();
    ctx.beginPath();
    
    ctx.moveTo(Ribbon.startX,Ribbon.startY);
    ctx.bezierCurveTo(Ribbon.control1X,Ribbon.control1Y,
                   Ribbon.control2X,Ribbon.control2Y,
                   Ribbon.endX,Ribbon.endY);
    
    ctx.stroke();
    ctx.restore();
    
   FillRibbon(curveText.value,Ribbon); 
}

function FillRibbon(text,Ribbon)
{

var textCurve = [];
var ribbon = text.substring(0,Ribbon.maxChar);
var curveSample = 1000;


xDist = 0;
var i = 0;
for (i = 0; i < curveSample; i++)
    {
        a = new bezier2(i/curveSample,Ribbon.startX,Ribbon.startY,Ribbon.control1X,Ribbon.control1Y,Ribbon.control2X,Ribbon.control2Y,Ribbon.endX,Ribbon.endY);
        b = new bezier2((i+1)/curveSample,Ribbon.startX,Ribbon.startY,Ribbon.control1X,Ribbon.control1Y,Ribbon.control2X,Ribbon.control2Y,Ribbon.endX,Ribbon.endY);
        c = new bezier(a,b);
        textCurve.push({bezier: a, curve: c.curve});
    }

letterPadding = ctx.measureText("").width / 4; 
w = ribbon.length;
ww = Math.round(ctx.measureText(ribbon).width);


totalPadding = (w-1) * letterPadding;
totalLength = ww + totalPadding;
p = 0;

cDist = textCurve[curveSample-1].curve.cDist;

z = (cDist / 2) - (totalLength / 2);

for (i=0;i<curveSample;i++)
    {
        if (textCurve[i].curve.cDist >= z)
        {
            p = i;
            break;
        }
    }

for (i = 0; i < w ; i++)
    {
        ctx.save();
        ctx.translate(textCurve[p].bezier.point.x,textCurve[p].bezier.point.y);
        ctx.rotate(textCurve[p].curve.rad);
        ctx.fillText(ribbon[i],0,0);
        ctx.restore();
        
        x1 = ctx.measureText(ribbon[i]).width + letterPadding;
        x2 = 0; 
        for (j=p;j<curveSample;j++)
        {
        x2 = x2 + textCurve[j].curve.dist;
        if (x2 >= x1)
            {
                p = j;
                break;
            }
        }
        



    }
} //end FillRibon

function bezier(b1, b2)
{
//Final stage which takes p, p+1 and calculates the rotation, distance on the path and accumulates the total distance
this.rad = Math.atan(b1.point.mY/b1.point.mX);
this.b2 = b2;
this.b1 = b1;
dx = (b2.x - b1.x);
dx2 = (b2.x - b1.x) * (b2.x - b1.x);
this.dist = Math.sqrt( ((b2.x - b1.x) * (b2.x - b1.x)) + ((b2.y - b1.y) * (b2.y - b1.y)) );
xDist = xDist + this.dist;
this.curve = {rad: this.rad, dist: this.dist, cDist: xDist};
}

function bezierT(t,startX, startY,control1X,control1Y,control2X,control2Y,endX,endY)
{
//calculates the tangent line to a point in the curve; later used to calculate the degrees of rotation at this point.
this.mx = (3*(1-t)*(1-t) * (control1X - startX)) + ((6 * (1-t) * t) * (control2X - control1X)) + (3 * t * t * (endX - control2X));
this.my = (3*(1-t)*(1-t) * (control1Y - startY)) + ((6 * (1-t) * t) * (control2Y - control1Y)) + (3 * t * t * (endY - control2Y));
}

function bezier2(t,startX, startY,control1X,control1Y,control2X,control2Y,endX,endY)
{
//Quadratic bezier curve plotter
this.Bezier1 = new bezier1(t,startX,startY,control1X,control1Y,control2X,control2Y);
this.Bezier2 = new bezier1(t,control1X,control1Y,control2X,control2Y,endX,endY);
this.x = ((1 - t) * this.Bezier1.x) + (t * this.Bezier2.x);
this.y = ((1 - t) * this.Bezier1.y) + (t * this.Bezier2.y);
this.slope = new bezierT(t,startX, startY,control1X,control1Y,control2X,control2Y,endX,endY);

this.point = {t: t, x: this.x, y: this.y, mX: this.slope.mx, mY: this.slope.my};
}
function bezier1(t,startX, startY,control1X,control1Y,control2X,control2Y)
{
//linear bezier curve plotter; used recursivly in the quadratic bezier curve calculation
this.x = (( 1 - t) * (1 - t) * startX) + (2 * (1 - t) * t * control1X) + (t * t * control2X);
this.y = (( 1 - t) * (1 - t) * startY) + (2 * (1 - t) * t * control1Y) + (t * t * control2Y);

}
.product-images-container {
  
    width:100%;
  position:relative;
}
canvas {
  position: absolute;
  top: 0;
}
.productImage {
  
  width: 100%
  
}
.engravingArea {
    position: relative;
}

#canvasDiv > canvas {
    width:100%;
    height: auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="product-images-container">
    <label for="curve">Curve</label><input size="80" type="text" id="curve" name="curve" value="40,228,100,260,270,260,330,240">
    <label for="text">Text</label><input size="80" type="text" id="text" name="text" value="testing 1234567890">
    <label for="offset">Y offset</label><input size="80" type="number" id="offset" name="offset" value="5">
    <label for="fontsize">Font size</label><input size="80" type="number" id="fontsize" name="fontsize" value="20">
<div class="engravingArea">
<div id="canvasDiv"></div>
</div>
    
<img class="productImage" src="https://i.imgur.com/BaoChJT.jpeg"> 
</div>

  1. I've added a field called offset, which allows you to specify the y offset from the bottom. This value gets incorporated into the curve digits before rendering to the canvas. This, together with a similar field for font size, allows easy adjustment to match the image (changeCurve() is called when either is changed).

  2. I would recommend making the canvas the same pixel size as the image, so that it will be just as sharp. I've changed the code to read the image width, then create the canvas at that size, using a scaling factor to scale dimensions accordingly.

  3. Having resized the canvas in 2., scaling the whole thing simply becomes a case of setting the CSS so that both the canvas and image fill their container width, so that they scale together.

Related