Simulate kerning for a bitmapped font with Fabric JS

那年仲夏 提交于 2021-01-27 05:12:21

问题


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):

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


回答1:


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>



回答2:


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

FS Ariapenciroman license

Fabric license



来源:https://stackoverflow.com/questions/63663156/simulate-kerning-for-a-bitmapped-font-with-fabric-js

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!