Search code examples
javascriptfabricjs

FabricJS - Change Webfonts of added text


I have created a simple UI where I would like to change the font of my added text.

I use fabricjs.

Find below my minimum viable example:

<!DOCTYPE html>
<html lang="en">

<head>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.0/font/bootstrap-icons.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css">

  <style>
    html,
    body {
      height: 100%;
      width: 100%;
      padding: 0;
      margin: 0;
    }
    
    body {
      touch-action: none;
      background-image: linear-gradient(to bottom left, rgb(214, 240, 201) 10%, rgba(255, 231, 191, 1) 80%);
      -webkit-user-select: none;
      -moz-user-select: -moz-none;
      -ms-user-select: none;
      user-select: none;
    }
    
    .input-group {
      padding: 4px;
    }
    
    .canvas-container {
      margin: 0 auto;
      width: 100%;
      overflow: hidden;
      background: url(./transparent.png);
      background-size: 15px 15px;
      box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
    }
    
    .actived {
      background: #fff9a8;
    }
    
    #sortable {
      max-height: 200px;
      overflow: scroll;
    }
    
    .list-group {
      line-height: 35px;
    }
    
    .svg-icon {
      width: 1em;
      height: 1em;
    }
    
    .svg-icon path,
    .svg-icon polygon,
    .svg-icon rect {
      fill: #4691f6;
    }
    
    .svg-icon circle {
      stroke: #4691f6;
      stroke-width: 1;
    }
    
    #bkboxXX {
      background: url(transparent.png);
      border: 1px solid rgba(0, 0, 0, 0.3);
      display: inline-block;
      height: 19px;
      width: 20px;
    }
    
    .bk-btn {
      background: url(/transparent.png);
    }
    
    .fl {
      float: left;
    }
    
    .fr {
      float: right;
    }
    
    .input-group-text {
      background-color: #f4f4f4;
    }
    
    .list-group-item {
      padding: 2px 10px;
      ;
      cursor: pointer;
    }
    
    .list-group {
      border-radius: 0;
      text-align: left;
    }
  </style>
</head>

<body>
  <div class="container-fluid h-100">
    <div class="row h-100">
      <div class="col-sm-3 border-end text-center h-100 overflow-scroll bg-light py-3">
        <div class="form-floating w-100 mb-3 tour5">
          <h6 class="mb-3">Add Element</h6>
          <button class="btn btn-outline-primary" onclick="addText();" data-toggle="tooltip" data-placement="top" title="" data-bs-original-title="Add Text" aria-label="Add Text"><i
                            class="fa fa-font"></i></button>
        </div>
        <hr>
        <div class="w-100  tour7 Xd-none Xd-sm-block">
          <h6 class="mb-3">Elements</h6>
          <ul class="list-group" id="sortable"></ul>
        </div>
      </div>
      <div class="col-sm-6 my-auto py-4 overflow-hidden">
        <div class="canvas-container" style="width: 329px; height: 329px; position: relative; user-select: none;"><canvas id="c" class="lower-canvas" width="329" height="329" style="position: absolute; width: 329px; height: 329px; left: 0px; top: 0px; touch-action: none; user-select: none;"></canvas>
        </div>
      </div>
      <div class="col-sm-3 border-start py-3 h-100 overflow-scroll bg-light text-center">
        <form class="form-inline pt-4" id="f" onsubmit="return false;" style="display: none;">
          <div class="input-group input-group-sm mb-2 w-100 tour9 dropdown">
            <span class="input-group-text" id="inputGroup-sizing-sm">Font Family</span>
            <input id="font-search" class="font-search form-control form-control-sm dropdown-toggle show" type="text" value="" data-provide="typeahead" title="Search for a Google Font" name="fontFamily" autocomplete="off" data-bs-toggle="dropdown" aria-expanded="true"
              hidden>
            <select id="allFonts" class="form-select" aria-label="Select Font" onchange="runFont(this.value)">
              <option selected>Open this select menu</option>
            </select>
          </div>
        </form>
      </div>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
  <script src="https://code.jquery.com/jquery-3.6.0.js"></script>
  <script src="https://code.jquery.com/ui/1.13.1/jquery-ui.js"></script>
  <script src="https://unpkg.com/fabric@5.2.1/dist/fabric.min.js"></script>
  <script src="https://rawcdn.githack.com/lyzerk/fabric-history/8c223cbdc8305307b4a8f8710f97da54d9146ffa/src/index.js"></script>
  <script src="https://rawgit.com/fabricjs/fabric.js/master/lib/centering_guidelines.js"></script>
  <script src="https://rawgit.com/fabricjs/fabric.js/master/lib/aligning_guidelines.js"></script>

  <script src="https://ajax.googleapis.com/ajax/libs/webfont/1.5.10/webfont.js"></script>

  <script>
    const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`

    function cn2h(cn) {
      if (!cn) {
        return 'transparent';
      }
      d = document.createElement("span");
      d.style.display = "none";
      d.style.color = cn
      document.body.appendChild(d)
      return rgba2hex(window.getComputedStyle(d).color);
    }
  </script>

  <script>
    var canvas;
    var apiUrl;
    var startedLoadingFamilies = false;
    var to;
    var scale;

    fabric.Text.prototype.set({
      _getNonTransformedDimensions() { // Object dimensions
        return new fabric.Point(this.width, this.height).scalarAdd(this.padding);
      },
      _calculateCurrentDimensions() { // Controls dimensions
        return fabric.util.transformPoint(this._getTransformedDimensions(), this.getViewportTransform(), true);
      }
    });

    function fitCanvas(w, h) {
      var scaleW = (document.querySelectorAll(".col-sm-6")[0].offsetWidth - 100) / w;
      var scaleH = (document.querySelectorAll("body")[0].offsetHeight - 100) / h;
      scale = Math.min(scaleH, scaleW);

      if (scale > 1) {
        scale = 1;
      }

      canvas.setZoom(scale);
      canvas.setWidth(w * scale);
      canvas.setHeight(h * scale);

      initAligningGuidelines(canvas);
      initCenteringGuidelines(canvas);

      canvas.renderAll();
      console.log("finished!!");
    }

    function runFont(value) {

      if (typeof canvas.getActiveObject() !== "undefined") {
        canvas.getActiveObject().fontFamily = value.replace(/\s/g, '+');
        fabric.util.clearFabricFontCache();
        canvas.renderAll();
      }

    }

    (function() {
      canvas = new fabric.Canvas('c', {
        allowTouchScrolling: true
      });
      WebFont.load({
        google: {
          families: ["Inter:200"]
        },
        active: function() {
          canvas.loadFromJSON({
            "height": "500",
            "width": "500",
            "edit": "yes",
            "objects": [],
            "backgroundImage": {
              "crossOrigin": "anonymous"
            }
          }, function() {
            // document.querySelectorAll('.bruzu-loader')[0].style.display = 'none';
            fitCanvas(500, 500);
          })
        }
      });

      canvas.on('before:render', function(opt) {
        canvas.getObjects().map(function(o, i) {
          if (o.type.toLowerCase() == "textbox") {
            var maxH = (o.maxHeight) ? (o.maxHeight) : (canvas.height);
            o.set({
              'maxHeight': maxH
            });
            if (o.fontSize > 0 && (maxH > o.fontSize)) {
              while (o.height > maxH) {
                o.set({
                  'fontSize': o.fontSize - 1
                });
              }
            }
          }

          if (o.circleFrame) {
            o.set({
              clipPath: new fabric.Circle({
                radius: o.width / 2,
                originX: 'center',
                originY: 'center'
              })
            });

          }

          if ((o.rx || o.ry) && o.type == "image") {

            let rx = (o.rx || 0);
            let ry = (o.ry || 0);

            o.set({
              clipPath: new fabric.Rect({
                rx: rx,
                ry: ry,
                height: o.height,
                width: o.width,
                originX: 'center',
                originY: 'center'
              })
            });
          }
        });
      });
    })();
  </script>

  <script>
    fabric.Object.prototype.objectCaching = false;

    function setColor(clr, what = 'fill') {
      //console.log("set color",what, clr);
      canvas.getActiveObject().set(what, clr);
      canvas.renderAll();
    }

    function loadFont() {

      $.getJSON("https://www.googleapis.com/webfonts/v1/webfonts?key=AIzaSyAu0Djqnn98eyXTWHI-9OaiESODz2tfKBI", function(fonts) {
        for (var i = 0; i < fonts.items.length; i++) {
          $('#allFonts')
            .append($("<option></option>")
              .attr("value", fonts.items[i].family)
              .text(fonts.items[i].family));
        }
      });

      fabric.util.clearFabricFontCache();
      canvas.renderAll();
    }

    (function() {

      loadFont()

      fabric.Canvas.prototype.getItemByAttr = function(attr, name) {
        var object = null,
          objects = this.getObjects();
        for (var i = 0, len = this.size(); i < len; i++) {
          if (objects[i][attr] && objects[i][attr] == name) {
            object = objects[i];
            break;
          }
        }
        return object;
      };

      $("form#f").hide();

      var activeObject;

      $("#f input, #f select").on("input", function() {
        if (["height", "width", "top", "left", "strokeWidth", "charSpacing"].includes(this.name)) {
          canvas.getActiveObject().set(this.name, parseFloat(this.value)).setCoords();

        } else {
          canvas.getActiveObject().set(this.name, this.value).setCoords();
        }
        canvas.renderAll();
      });

      canvas.preserveObjectStacking = true;

      fabric.Object.prototype.toObject = (function(toObject) {
        return function() {
          return fabric.util.object.extend(toObject.call(this), {
            name: this.name,
            text: this.text,
            textAlign: this.textAlign,
            fontSize: this.fontSize,
            charSpacing: this.charSpacing,
            lineHeight: this.lineHeight,
            fontWeight: this.fontWeight,
            fontFamily: this.fontFamily,
            fontStyle: this.fontStyle,
            textBackgroundColor: this.textBackgroundColor,
            originX: this.originX,
            originY: this.originY,
            maxHeight: this.maxHeight,
            height: this.height,
            width: this.width,
            radius: this.radius,
            rx: this.rx,
            ry: this.ry,
            stroke: this.stroke,
            padding: this.padding,
            circleFrame: this.circleFrame
          });
        };
      })(fabric.Object.prototype.toObject);

      canvas.on('object:scaling', function(e) {
        if (e.target.toObject().type != "image" && e.target.toObject().type != "circle") {
          //console.log(e.target.toObject().type);
          e.target.set({
            width: e.target.width * e.target.scaleX,
            height: e.target.height * e.target.scaleY,
            scaleX: 1,
            scaleY: 1
          })
        }
      });

      canvas.on('object:modified', function(opt) {
        document.body.style.cursor = 'progress';
      });

      canvas.on("after:render", function() {

        $("#sortable").empty();

        canvas.includeDefaultValues = false;
        canvas.toObject().objects.forEach(function(layer, id) {

          if (typeof layer.name !== 'undefined') {
            canvas.getItemByAttr(`name`, layer.name).set({
              "name": layer.name.replaceAll(' ', '_')
            })
            var actived = '';
            if (canvas.getActiveObject()) {
              actived = (canvas.getActiveObject().name == layer.name) ? " actived" : "";
            }
          }
        });
        document.body.style.cursor = 'default'
      });

      canvas.on("selection:created", function(obj) {
        if ("image" == obj.selected[0].type) {
          canvas.getActiveObject().setControlsVisibility({
            mb: false,
            ml: false,
            mt: false,
            mr: false
          });
        }
        $("form#f input[type!='hidden'], #f select").parent().hide();
        $("form#f").hide();
        canvas.renderAll();
        showForm();
      });

      canvas.on("selection:updated", function(obj) {
        if ("image" == obj.selected[0].type) {
          canvas.getActiveObject().setControlsVisibility({
            mb: false,
            ml: false,
            mt: false,
            mr: false
          });
        }
        canvas.renderAll();
        $("form#f").hide();
        $("form#f input[type!='hidden'] , #f select, #f textarea").parent().hide();
        showForm();
      });

      canvas.on("selection:cleared", function() {
        canvas.renderAll();
        $("form#f").hide();
        $("form#f input[type!='hidden'], #f select, #f textarea").parent().hide();
      });

      canvas.hoverCursor = 'default';
      canvas.on('mouse:over', function(e) {
        if (e.target) {
          e.target._renderControls(canvas.contextTop, {
            hasControls: false
          })
        }
      });

      canvas.on('mouse:out', function(e) {
        canvas.clearContext(canvas.contextTop);
      });
      canvas.on('mouse:down', function(e) {
        canvas.clearContext(canvas.contextTop);
      });

      function showForm() {

        $("form#f").show();
        activeObject = canvas.getActiveObject();
        var v;
        for (i in activeObject) {

          v = activeObject[i];

          if (typeof v != "undefined") {
            //$("textarea[name='" + i + "']").val(v).parent().show();
            $("input[name='" + i + "']").val(v).parent().show();
            $("select[name='" + i + "']").val(v).parent().show();
          }
        }
      }

      addText = function() {
        var text = new fabric.Textbox("Edit this Text", {
          name: genNextName(),
          left: canvas.getWidth() / canvas.getZoom() / 2,
          top: canvas.getHeight() / canvas.getZoom() / 2,
          width: (canvas.getWidth() / canvas.getZoom()) / 2,
          fill: "#000000",
          originX: "center",
          originY: "center",
          fontFamily: "Inter",
          fontWeight: 400,
          fontSize: 60,
          padding: 20
        });
        text.setControlsVisibility({
          mt: false, // middle top
          mb: false, // midle bottom
          ml: true, // middle left
          mr: true, // middle right
          tl: true, //top left
          tr: true, //top right
          bl: true, //bottom left
          br: true //bottom right
        });

        canvas.add(text);
        canvas.setActiveObject(text);
        canvas.renderAll();
      };
    })();

    function hide_mh_box() {
      canvas.remove(canvas.getItemByAttr("isBB", true));
    }

    function show_mh_box() {
      var h = parseInt($("[name=maxHeight]").val());
      var n = new fabric.Rect({
        top: canvas.getActiveObject().get('top'),
        left: canvas.getActiveObject().get('left'),
        width: canvas.getActiveObject().get('width'),
        height: h,
        originX: canvas.getActiveObject().get('originX'),
        originY: canvas.getActiveObject().get('originY'),
        angle: canvas.getActiveObject().get('angle'),
        opacity: 1,
        strokeWidth: 2,
        stroke: "#FF00FF",
        fill: "rgba(0,0,0,0)",
        evented: !1,
        isBB: true
      });
      canvas.add(n);
      canvas.renderAll();
    }

    var visited = [];

    $(document).ready(function() {

      $("input[type=color]").on("input", function() {
        $(this).parent().parent().find('input').first().val(this.value);
      });

      $("textarea, input").attr("autocomplete", "off");


      if (window !== window.parent) {
        const url = new URL(document.referrer);
        $(".isIframe").removeClass('d-none');
        $(".notIframe").hide();
      }

    });
  </script>

  <script>
    function genNextName() {
      canvas.renderAll();
      var total = canvas.getObjects().length;
      //console.log(total);
      return String.fromCharCode(65 + total).toLowerCase()
    }

    function updateImg() {

      const file = document.getElementById("imageUploader").files[0]
      let src = null

      // Get the real path of the picture file
      // Due to browser security policy , Now you need to do this
      if (window.createObjcectURL != undefined) {
        src = window.createOjcectURL(file);
      } else if (window.URL != undefined) {
        src = window.URL.createObjectURL(file);
      } else if (window.webkitURL != undefined) {
        src = window.webkitURL.createObjectURL(file);
      }

      let ao = canvas.getActiveObject();

      fabric.Image.fromURL(src, function(img) {
        //console.log(img._element);
        if (img.width) {
          var imgElm = img.set({
            name: ao.name,
            originX: ao.originX,
            originY: ao.originY,
            left: ao.left,
            top: ao.top,

            scaleX: ao.scaleX,
            scaleY: ao.scaleY,
            src: src,
            lockUniScaling: !0

          });

          ao.setElement(imgElm.getElement());
          canvas.renderAll();
        }
      });
    }

    // customized control
    fabric.Object.prototype.transparentCorners = false;
    fabric.Object.prototype.borderColor = '#5cdce4';
    fabric.Object.prototype.cornerColor = 'white';
    fabric.Object.prototype.cornerStrokeColor = 'blue';
    fabric.Object.prototype.cornerStyle = 'circle';
    fabric.Object.prototype.cornerSize = 10;
    fabric.Object.prototype.borderScaleFactor = 2;
  </script>
</body>

</html>

When trying to change the font I can change it once, but it won't change again.

enter image description here

Any suggestions what I am doing wrong?

I appreciate your replies!


Solution

  • i could suggest you using https://fontfaceobserver.com/ script

    you also need load fonts, for my own projects i use jquery.fontpicker.js who handle nicely the job

    so, if you mix this two piece of code, load fonts not loaded and wait the effective load using FontFaceObserver it will work :

    let fontsLoaded = {} ;
    
    function runFont(font) {
    
      if(!fontsLoaded[font]){
        fontsLoaded[font] = true ;
        const url = 'https://fonts.googleapis.com/css?family=' + font.replace(/ /g,'+') + ':' + '&display=swap';
        console.log('Loading Google font ' + font + ' from ' + url);
        $('head').append($('<link>', {href:url, rel:'stylesheet', type:'text/css'}));
    
        runFont( font );
        return ;
      }
    
      var f = new FontFaceObserver( font );
    
      f.load().then(function () {
        console.log( "apply", font )
        canvas.getActiveObject().set("fontFamily", font );
      }, function () {
        console.log('Font',font,'is not available');
      });
    }
    

    edit > minimal implementation of the jquery font picker with fabricjs :

      let $textStyleIcons = $();
    
      let $fontPicker = $("<input/>").addClass("fonts");
    
      $textStyleIcons.on("update", function(){
        let o = canvas.getActiveObject();
        if(!o) return;
    
        $fontPicker.val( o.fontFamily ).trigger("change");
      });
    
      $textStyleIcons = $textStyleIcons.add( $fontPicker );
    
      let $textStyleIconsUpdate = $textStyleIcons.triggerHandler.bind($textStyleIcons, "update") ;
    
      function styleBlocsUpdate(){
        let o = canvas.getActiveObject();
        const type = o ? o.get("type") : null ;
        switch( type ){
          case "i-text":
            $textStyleIconsUpdate();
            setFontPickerText( canvas.getActiveObject().get("text") );
          break;
        }
      }
    
      $body.append( $textStyleIcons );
    
      function setFont(font) {
        let f = font.split(':')[0];
        canvas.getActiveObject().set("fontFamily", f);
        styleBlocsUpdate();
      }
    
      const $fp = $fontPicker
      .fontpicker({
        lang:'fr',
        variants:false
      })
      .on('change', function( ) {
        let v = this.value ;
        let o = canvas.getActiveObject();
        if( !o || o.get("type") !== "i-text" ) return ;
        if( o.get("fontFamily") === v ) return ;
    
        if( typeof FontFaceObserver !== 'function' ){
          setFont(v);
          return;
        }
    
        var font = new FontFaceObserver( v );
    
        font.load().then(function () {
          setFont(v);
        }, function () {
          console.log('Font',v,'is not available');
        });
    
      });
    
      function setFontPickerText(t){
        if( !t || !t.length ) return ;
        $fp.$sample.text(t);
      }
    
      canvas.on("selection:created",styleBlocsUpdate);
      canvas.on("selection:updated",styleBlocsUpdate);
      canvas.on("selection:cleared",styleBlocsUpdate);
      canvas.on("text:selection:changed",$textStyleIconsUpdate);
      canvas.on("text:editing:entered",$textStyleIconsUpdate);
      canvas.on("text:editing:exited",$textStyleIconsUpdate);
    
      styleBlocsUpdate();