Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simulate kerning for a bitmapped font with Fabric JS

I am trying to create an effect with Fabric JS where letters appear to be "embroidered" on a sweater like this:

I can achieve this effect in Photoshop by using this action.

My idea for getting it into a <canvas> is to render out a png from Photoshop of every embroidered letter. Then, I will take each letter and place it on the canvas based on what the user types.

However this approach will not have correct kerning.

To fix this, I was trying to write out text in Fabric using the same font and then overlay each embroidered png on top of the letter it is replacing (and then hide the text itself).

Here's how I render the text:

window.chest_text = new fabric.IText("NYC", {
      fill: '#000',
      fontSize: 12,
      left: 210,
      top: 100,
      fontFamily: 'Graphik',
      fontWeight: 500,
      lineHeight: 1,
      originX: 'center',
    });

And then here's how I render the embroidered letters:

  var n_url = 'https://res.cloudinary.com/tricot/image/upload/v1598820746/tmp/n-embroidery-test.png'
  var y_url = 'https://res.cloudinary.com/tricot/image/upload/v1598820745/tmp/y-embroidery-test.png'
  var c_url = 'https://res.cloudinary.com/tricot/image/upload/v1598820745/tmp/c-embroidery-test.png'
  
  fabric.Image.fromURL(n_url, function(img) {
    img.set({
      left: Math.round(window.chest_text.aCoords.bl.x),
      top: window.chest_text.top
    })
    
    img.scaleToHeight(Math.floor(window.chest_text.__charBounds[0][0].height / 1.13), true)
    
    canvas.add(img);
  })
  
  fabric.Image.fromURL(y_url, function(img) {
    img.set({
      left: Math.round(window.chest_text.aCoords.bl.x + window.chest_text.__charBounds[0][1].left),
      top: window.chest_text.top
    })
    
    img.scaleToHeight(Math.floor(window.chest_text.__charBounds[0][1].height / 1.13), true)
    
    canvas.add(img);
  })
  
  fabric.Image.fromURL(c_url, function(img) {
    img.set({
      left: Math.round(window.chest_text.aCoords.bl.x + window.chest_text.__charBounds[0][2].left),
      top: window.chest_text.top
    })
    
    img.scaleToHeight(Math.floor(window.chest_text.__charBounds[0][2].height / 1.13), true)
    
    canvas.add(img);
  })
  
  window.chest_text.opacity = 0.5
  
  window.canvas.renderAll()

However I can't get the embroidered letters to EXACTLY overlay the regular text (even though it's the same font):

enter image description here

How can I achieve this? Is there a better way of getting kerning to work correctly?

like image 682
Tom Lehman Avatar asked Aug 30 '20 23:08

Tom Lehman


2 Answers

Using line dash as the stitch and ctx.globalCompositeOperation = "source-atop" to draw only inside the text. Vary the stroke width to build stitches from inside of font out.

Unfortunately the line dash spacing is only true for the stroke center so the approach works for some characters but not all.

Can be improved upon as there was no effort to round stitchs (highlight and shadow color per stitch) but as there is no control over positioning of stitches at line joins i could not see the point of refining it further.

Will work with any font.

See snippet for demo and code.

function stitchIt(text, stitchLen, stitchOffset, threadThickness, size, font, col1, col2 , shadowColor, offset, blur) {
    const can = document.createElement("canvas");
    const ctx = can.getContext("2d");
    ctx.font = size + "px "+font;
    const width = ctx.measureText(text).width;
    can.width = width;
    can.height = size;
    ctx.font = size + "px "+font;
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.globalCompositeOperation = "source-over";
    ctx.lineCap = "butt";
    ctx.lineJoin = "bevel";
    ctx.fillStyle = col2;
    ctx.setTransform(1,0,0,1,width / 2, size / 2);
    ctx.fillText(text, 0, 0);
    ctx.setLineDash([stitchLen, stitchLen]);
    var w = size, off = 0;
    ctx.globalCompositeOperation = "source-atop"
    while (w > 0) {
        ctx.lineWidth = w;
        ctx.strokeStyle = col1;
        ctx.lineDashOffset = off; 
        ctx.strokeText(text, 0, 0);
        if (w > threadThickness) {
            w -= threadThickness / 2;
            ctx.lineWidth = w;
            ctx.lineDashOffset = off + stitchLen; 
            ctx.strokeStyle = col2;
            ctx.strokeText(text, 0, 0);
            off += stitchLen * stitchOffset;
            w -= threadThickness / 2;
        } else {
            break;
        }
    }
    ctx.globalCompositeOperation = "destination-out";
    ctx.globalAlpha = 0.5;
    ctx.strokeStyle = col2;
    ctx.lineWidth = threadThickness / 2;
    ctx.lineDashOffset = off + stitchLen; 
    ctx.strokeText(text, 0, 0);
    ctx.globalCompositeOperation = "destination-over";
    ctx.save();
    ctx.shadowColor = "#000";
    ctx.shadowOffsetX = offset;
    ctx.shadowOffsetY = offset;
    ctx.shadowBlur = blur;
    ctx.fillText(text, 0, 0);
    ctx.restore();    
    ctx.globalCompositeOperation = "source-over";
    return can;
}
ctx = canvas.getContext("2d");
textEl.addEventListener("input", update);
sLenEl.addEventListener("change", update);
sOffEl.addEventListener("change", update);
sThreadEl.addEventListener("change", update);
sFontEl.addEventListener("change", update);
colEl.addEventListener("click", () => {
    color = colors[colIdx++ % colors.length];
    update();
});

// update debounces render
var tHdl;
function update() {
    clearTimeout(tHdl);
    tHdl = setTimeout(draw, 200);
}
const colors=[["#DDD","#888"],["#FFF","#666"],["#F88","#338"],["#8D8","#333"]];
var colIdx = 0;
var color = colors[colIdx++];
stitchIt("STITCH",5, 1.4, 4, 160,"Arial Black","#DDD","#888","#0004" , 4, 5);
function draw() {
    ctx.clearRect(0,0,1500,180);
    if (textEl.value) {
        const image = stitchIt(
            textEl.value,
            Number(sLenEl.value), 
            sOffEl.value / 100 + 0.8, 
            Number(sThreadEl.value), 
            Number(sFontEl.value),
            "Arial Black",
            color[0],
            color[1],
            "#0004" , 
            4, 
            5
        );
        ctx.drawImage(image,0,90-image.height / 2); 
   }
}
draw();
canvas {
    background: #49b;
    border: 2px solid #258;
    position: absolute;
    top: 0px;
    left: 230px;
}
<div>
    <input id="textEl" type="text" value="STITCH"></input><br>
    <button id="colEl">change Color</button><br>
    
    <input id="sLenEl" type="range" min="2" max="16" value="5"><label for="sLenEl">Stitch length</label><br>
    <input id="sOffEl" type="range" min="0" max="100" value="50"><label for="sOffEl">Stitch offset</label><br>
    <input id="sThreadEl" type="range" min="2" max="10" value="4"><label for="sThreadEl">Thread size</label><br>
    <input id="sFontEl" type="range" min="20" max="180" value="140"><label for="sFontEl">Font size</label><br>
</div>
<canvas id="canvas" width="1500" height="180"></canvas>
like image 105
Blindman67 Avatar answered Oct 21 '22 03:10

Blindman67


If you manage to find the font you could use a font.

There is a font that might be closer to what you need, NCD Embroidery but you have to contact the designer to get a license.

You might even find the font in one of the Photoshop libraries (I'm not that familiar with Photoshop, so this is just a guess)

Registered the font in .css file.

@font-face {
    font-family:'stitchfont';
    src:url('./fonts/fs-mom.ttf') format('truetype');
}
@font-face {
    font-family:'pencilfont';
    src:url('./fonts/fs-ariapenciroman.ttf') format('truetype');
}
@font-face {
    font-family:'fabricon';
    src:url('./fonts/stf-fabricon-cross-section.ttf') format('truetype');
}

.stitch{
    font-family: 'stitchfont';
}
.pencil {
    font-family: 'pencilfont';
}

.fabri {
    font-family: 'fabricon';
}

and the code:

<body>    
    <canvas id="stitchText" width="400" height="200"></canvas>
    <canvas id="pencilText" width="400" height="200"></canvas>
    <canvas id="fabricText" width="400" height="200"></canvas>

    <div class="stitch" style="visibility: hidden;">if the font is not used it doesn't seem to load consistently</div>
    <div class="pencil" style="visibility: hidden;">so assign the font to any element</div>
    <div class="fabri" style="visibility: hidden;">set the style visibility to hidden , dont use the html hidden tag</div>

    <script src="./lib/fabric.js/fabric.js"></script>
    <script>
        let stitchCanvas = new fabric.Canvas("stitchText")
        let stitchText = new fabric.Text("NYC", {
            fill: '#EEE',
            fontFamily: 'stitchfont',
            fontSize: 60,
            left: 190,
            top: 90,
            originX: 'center',
            originY: 'center'
        });
        stitchCanvas.add(stitchText);
        stitchCanvas.setBackgroundImage("images/embroid.png", stitchCanvas.renderAll.bind(stitchCanvas), { opacity: 0.8, scaleX: 0.28, scaleY: 0.28 });

        let pencilCanvas = new fabric.Canvas("pencilText")
        let pencilText = new fabric.Text("NYC", {
            fill: '#EEE',
            fontFamily: 'pencilfont',
            fontSize: 80,
            left: 190,
            top: 85,
            originX: 'center',
            originY: 'center'
        });
        pencilCanvas.add(pencilText);
        pencilCanvas.setBackgroundImage("images/embroid.png", pencilCanvas.renderAll.bind(pencilCanvas), { opacity: 0.8, scaleX: 0.28, scaleY: 0.28 });

        let fabricCanvas = new fabric.Canvas("fabricText")
        let fabricText = new fabric.Text("NYC", {
            fill: '#EEE',
            fontFamily: 'fabricon',
            fontSize: 40,
            left: 195,
            top: 85,
            originX: 'center',
            originY: 'center'
        });
        fabricCanvas.add(fabricText);
        fabricCanvas.setBackgroundImage("images/embroid.png", fabricCanvas.renderAll.bind(fabricCanvas), { opacity: 0.8, scaleX: 0.28, scaleY: 0.28 });

    </script>
</body>

The results from the fonts:

fs Mom license

fmMomFont

FS Ariapenciroman license

pencilFont

Fabric license

fabricFont

like image 32
CobyC Avatar answered Oct 21 '22 03:10

CobyC