blog:2020:0713_nervdraw_nervtech_logo

Action disabled: register

Building the new NervTech logo

So here we go, now I should really be able to build a logo for NervTech that will look nice and be my property! Using my new nvdraw app of course ;-)

Let's get started: heading directly to https://draw.nervtech.org/

As inspiration source I'm going to use the content available on https://www.brandcrowd.com/ which is an amazing website for logo generation (but it's not free of course).

One thing I quickly noticed is that I would really need some support for boolean operations on raphael elements and it turns out there is a plugin available for that: https://github.com/poilu/raphael-boolean (installing that…)

To support using the new boolean functions from the editor I also prepared a minimal extension typescript declaration string as follow:

const raphael_plug_dcl = `
export interface RaphaelStatic<TTechnology extends RaphaelTechnology = "SVG" | "VML"> {
    
    // Convert a given Raphael element to a path
    toPath(a: any): string;

    // Generate the union of 2 raphael elements
    union(a: any, b: any): string;
    
    difference(a: any, b: any): string;
    exclusion(a: any, b: any): string;
    intersection(a: any, b: any): string;
}`;

export default raphael_plug_dcl;

But then I tried to load that additional declaration in a separated addExtraLib() call, but this doesn't work:

    monaco.languages.typescript.typescriptDefaults.addExtraLib(
      raphael_plug_dcl,
      'a://node_modules/@types/raphael_plugins/index.d.ts'
    );

What worked instead is to simply concatenate that extension to the base raphael declaration:

    monaco.languages.typescript.typescriptDefaults.addExtraLib(
      raphael_dcl+raphael_plug_dcl,
      'a://node_modules/@types/raphael/index.d.ts'
    );

I think this could be related to the fact that I wasn't importing the “raphael_plugins” file… let's try that… hmmm not quite working.

But then I found this explanation page :-) So I updated my plugin declaration file as follow:

const raphael_plug_dcl = `
import { RaphaelPaper, RaphaelTechnology } from 'raphael';

declare module "raphael" {

    interface RaphaelPaper<TTechnology extends RaphaelTechnology = "SVG" | "VML"> {
    
        // Convert a given Raphael element to a path
        toPath(a: any): string;

        /**
         * perform a union of the two given paths
         *
         * @param object el1 (RaphaelJS element)
         * @param object el2 (RaphaelJS element)
         *
         * @returns string (path string)
         */
        union(a: any, b: any): string;

        difference(a: any, b: any): string;
        exclusion(a: any, b: any): string;
        intersection(a: any, b: any): string;
    }
}`;

export default raphael_plug_dcl;

⇒ And now I can use a second addExtraLib() call as desired, and I get the support for the raphael plugin functions in the monaco editor, cool ;-)!

[unrelated note] Some interesting indications on how to setup an SVG arc: http://darchevillepatrick.info/svg/svg_arcs.htm [ Could be useful for me here]

Note: I tried to perform a boolean addition with a path of surface 0 (ie. a single line) with a large stroke-width: this doesn't seem to work ;-) But that is not really surprising… instead I should build my path with a “real width”.

The nice thing with this web app setup is that I can easily extend the Raphaeljs framework with custom functions before rendering my page, so I added a few custom functions already:

  private loadRaphaelExtensions()
  {
    Raphael.fn.asPath = function(obj: RaphaelElement): RaphaelElement
    {
      if(obj.type=="rect" || obj.type=="circle" || obj.type=="ellipse") {
        let p = (this as RaphaelPaper)

        let pstr = p.toPath(obj)
        obj.remove()
  
        return p.path(pstr)
      }
      
      // otherwise we just return the object itself:
      return obj
    }

    Raphael.fn.subParts = function(obj: RaphaelElement, parts: RaphaelElement[]): RaphaelElement
    {
      let p = (this as RaphaelPaper)
      let rpath = p.asPath(obj);
      for(let i=0;i<parts.length;++i) {
        let rstr = p.difference(rpath, parts[i])
        rpath.remove()
        parts[i].remove()
        rpath = p.path(rstr)
      }
    
      return rpath
    };

    Raphael.fn.addParts = function(obj: RaphaelElement, parts: RaphaelElement[]): RaphaelElement
    {
      let p = (this as RaphaelPaper)
      let rpath = p.asPath(obj);
      for(let i=0;i<parts.length;++i) {
        let rstr = p.union(rpath, parts[i])
        rpath.remove()
        parts[i].remove()
        rpath = p.path(rstr)
      }
    
      return rpath
    };

  }

Of course, I'm also adding the new functions into to the raphael plugins typescript declaration string that I'm injecting into the monaco editor, so that the editor is aware of them.

⇒ Now I think I should also provide a dedicated webpage documenting the available extensions in case someone else is trying to use this [or just in case you forget about these later Manu lol]

When building the modal to provide additional export parameters (ie. desired width/height) I noticed that I would need the bootstrap typescript declarations to avoid typescript syntax errors, so:

npm install @types/bootstrap --save-dev

Then we just use in the code:

import * as bootstrap from "bootstrap";
//import * as $ from "jquery";

… And the error I got on the call to “modal” in a line such as “$('#exportModal').modal('hide');” is now gone.

Another nice thing with this rendering mechanism is that if your logo parameters somehow do not dependent linearly on the provided canvas size, then you will get different rendering result depending on the current canvas size. For instance, here is what I can observe myself on my NervTech logo:

Pushing it even further, here are the results I get for instance when rendering the logo with a size of 512×512, 1024×1024 or 2048×2048:

Of course, in most cases, when building a logo you don't want it to change dynamically depending on the render size, so you really want to make all your logo parameters proportionnal to the provided render dimensions.

One nasty issue I faced during this logo development was due to the so called “fill-rule” attribute that can be specified for some SVG elements (see this page):

In fact, when using the Raphael boolean plugin to compute differences between shapes, you may sometimes generate a hole in a shape (for instance removing a small disc from the inside of a larger disc), but the paths generated to represent the resulting shape will not use the conventional orientation rules (I think it is [by convention] clock-wise for interior area, and counter-clockwise to cut “holes” in an interior area ?).

⇒ So I had to switch to the “evenodd” fill rule: this one would automagically figure out what is inside/outside given your paths [not quite sure how this works, but it seems to works for me at least].

Except that raphaeljs .attr() element function will not accept that attribute (snifff…), so the code below will not work for instance:

  let c = paper.circle(10, 10, 50)
  c.attr("fill-rule", "evenodd")

The “fill-rule” attribute will not appear on the generated SVG element when we use this :-(. It took me some good time to figure this out, but what I eventually found would work instead was to apply the attribute directly on the underlying node (I'm using jquery to do this, but I'm sure this would actually works with plain DOM API only). So I extended my Raphael element API with the following function:

    Raphael.el.setFillRule = function (rule: string) : RaphaelElement {
      $(this.node).attr("fill-rule", rule);
      return this;
    };

And this did the trick ;-)!

Now we could call this function manually on any element if needed, but I'm also just automatically calling this function with the value “evenodd” inside my extension function subParts() as this is where you can generate such holes in shapes. So I didn't need to update my logo code to fix this actually.

But, of course, we are still quite far from the end of the story: then I realized that code like below would not work either:

  sat = paper.subParts(sat, [c4, c5])
  sat.attr(attr2)

  // Place each satellite now:
  for(let i=0;i<nsats;++i) {
    sat.clone().rotate(-90+360/nsats*i,cx,cy).attr({fill: satCol[i%satCol.length], stroke: "none"})
  }

And it turned out that the problem here was in the clone() function: it will not copy the fill-rule attribute from the source “sat” element [which contains it because it is the result of a call to “subParts” as mentioned just above]. So again, I had to find a way to fix that “as properly as possible” [because I didn't want to have to call setFillRule() manually after each clone operation obviously]. So I added the following “extension” [or rather, replacement of the legacy “clone()” function in Raphaeljs]:

    // Also copy the fill rule when cloning an element:
    let cloneFn = Raphael.el.clone

    Raphael.el.clone = function() : RaphaelElement
    {
      let c = cloneFn.apply(this)
      if(this.node.hasAttribute("fill-rule")) {
        // console.log("Copying fill rule in clone.")
        $(c.node).attr("fill-rule", this.node.getAttribute("fill-rule"))
      }

      return c
    }

This change worked as expected and fixed the issue at hand, yeahh! But we are still not “all good” yet, sorry ;-)

Then, the display on the webpage was OK so I moved to the export process…

In fact, initially I quickly fixed all this fill rule mess by simply adding the css style “fill-rule”: evenodd on my SVG container, but when I then tried to export the result to PNG I noticed that this style was not propagated correctly and my final images were not correct even if the display was OK on screen.

For the export process I was using the Raphael export plugin function toSVG() as follow:

    let svg = (this.exportPaper as any).toSVG();

    // svg = (svg as string).replace("<path ", "<path style=\"fill-rule: evenodd;\" ")
    // svg = (svg as string).replace(/<path /g, "<path style=\"fill-rule: evenodd;\" ")

    console.log("SVG data is: "+JSON.stringify(svg))

    const canvas = new OffscreenCanvas(this.exportWidth, this.exportHeight);

    const ctx = canvas.getContext('2d');

    const v = await Canvg.from(ctx, svg, presets.offscreen());

And of course, once more, my fill-rule attribute was discarded during the conversion to an SVG string. As show in the code above, I did a few tests to try to manually fix the SVG content before rendering on the canvas with canvg [That one seemed to be “fill-rule-friendly” already according to what I read] and this worked to some extend, but I didn't want to always enforce that attribute on all paths: that didn't sound like the correct thing to do.

Yet this got be thinking: I have Raphael generating a complete SVG graph in the DOM of my page, and that Raphael Export plugin generating an SVG string by somehow parsing the raphael elements: but why on earth would I need that ?! I mean, I could simply just retrieve the inner html of my SVG container and that should work just fine already a an “SVG string”, no ?

⇒ And indeed, I just updated my code to use instead:

let svg = $("#exportSVG").html()

And with that change, I was not loosing my fill-rule attribute values anymore in the process, then canvg will correctly use that value, and the image generated on the canvas look the same as the source SVG images now, yeepee! :-)

The Raphael Export plugin is still useful if you want to have support for very [very very ?] old browsers that do not support SVG natively. But honestly, I just don't care about those browsers myself lol

Then I thought this was it and I could finally generate my logo with different sizes… But no. LOL. I quickly noticed there was another bug around: I could render the logo correctly with sizes of 512×512 or 1024×1024, but when trying with a size of 2048×2048, one of the boolean subtraction operation was apparently failing. To me it felt as if there was some kind of threshold at play somewhere in the code that would lead to this problem.

So I read the code of the raphael boolean plugin, and indeed I eventually found this:

	var getIntersections = function(path1, path2, strict) {
		var d = 0.1; //min. deviation to assume point as different from another
		var inters = Raphael.pathIntersection(path1, path2);
		var validInters = [];
		var valid = true;

        // more code here
    }

Notice the min deviation variable which is set to 0.1 above: I'm not quite sure what this is exactly, but that's in fact the only fixed variable I found in that code, so I decided to give it a try and change the value to d = 0.001.

⇒ And… surprise! This worked! LOL I could then render my logo correctly even with a large size of 2048×2048 and it doesn't seem to introduce any artifact elsewhere for now, so I'm keeping it that way.

Feewww, this was definitely a bit harder than expected, but my raphaeljs skills were a bit rusty anyway, so with this session I should be “sort of back on rails” at least lol. And on top of that, now I finally have my own logo, cool! [ I will update my webservers to use it in a moment]

Of course, in the process, I updated the public version of the web app (available at https://draw.nervtech.org) with the latest version of the code to make it more usable.

Now I still need to write some proper documentation on the extension functions I created, but that will be for another time éh éh éh.

Meanwhile, happy hacking everyone!

  • blog/2020/0713_nervdraw_nervtech_logo.txt
  • Last modified: 2020/07/13 11:36
  • by 127.0.0.1