Why I love coding out my ideas

Many of the pens I construct here on CodePen comes out of an idea where I want to see the effect of playing around with known methods. I kind of like science, and love to watch Discovery alike tv shows about clever guys doing science to discover new effects or rules. I see the internet as a great place to play around with browser and language features.

One my prefered playgrounds is SVG. It is Scalable Vector Graphics, which in practice means that you can draw something in the browser using some tags, some stying and some code. How cool is that? Compared with a piece of paper and a pen you can go crazy and the result is generated instantly as you change the elements of your playground in your pen on CodePen.

I have an idea

Every time I see someone sharing a new "discovery" in the area of web technology my mind goes crazy, asking lots of questions related to the discovery. I guess that it is kind of the same feeling science guys get: whenever a new path shows they want to jump into the way of the path curious about what is in that path.

I want to follow an idea I got watching someone else sharing something related to the idea I have:

I want to create a way to auto generate corners on a SVG object. A method which can add custom corners to (any) SVG objects.

I have made a drawing which should give you some idea of what is inside my head: The idea of constructing custom borders on a SVG element

The algorytm could be broken down into these parts:

  1. Add an object (A) to the SVG
  2. Add a new SVG path (B)
  3. Find the points (here 4 corner points) of the elements A
  4. Generate two points between each pair of two point in the element
  5. Draw lines (and bezier curves) between each point
  6. Add the path to the SVG
  7. Convert a SVG mask element from the generated path
  8. Apply the mask to original element

That is roughly the tasks of constructing a custom corner which I now needs to do...

The pen boilerplate and task 1-2

I have made many pens on CodePen and I sometimes use the same boilerplate. I could create a (CodePen template)[https://blog.codepen.io/documentation/api/templates/] so that I do not have to type in the same bits of code every time. My boilerplate template has this description:

This boilerplate contains a SVG which fills the whole view of the page. It also has javascript code which automatically discovers elements with "_" prefixed classnames and creates global references to them which will be the same as the class name but prefixed with yet another "underscore". For instance the SVG element have a class "_svg" and can be referred to from javascript through "__svg".

You should be able to fork my template "Boilerplate SVG with auto creation of global vars".

This is an easy way to get autogenerated global references to some elements. You simply add a class name starting with underscore and the element can be accessed in javascript with the classname prefixed with yet another underscore.

Now task 1 and 2. The template contains a SVG element which contains a path drawing a stroked rectangle on the the screen. I have also added a path stroked in orange.

Finding the points of a path

I have searched many times for a simple API which gives me the points of a SVG element. I have not found it yet, so I have to find them by deconstructing the D attribute of the path. Its a pity, but for now I have to rely on that method, knowing that it is not at all perfect.

A SVG path has some drawing primitives. It has its roots in PostScript (which is a scripting language created by Adobe for prepress industry. It is also used today in the PDF). The drawing primitives lets you controle a "pen" which moves from coordinate to coordinate leaving a trace which is the path - the stroke you see on the screen.

The simple path in this pen consists of only two drawing primitives: M - move and L - line. It is important that the primitives are written in CAPITAL LETTERS, as this will result in the coordinates being specified in absolute coordinate values. The drawing primitives can also be written in lower case in which case the coordinates will be relative coordinates.

Looking at the SVG element I can easily see the drawing primitives in the d attribute of the path:

M 200 100 L 400 100 400 300 200 300 200 100

Which means:

  • Move to (200,100)
  • Line to (400,100) and then to (400,300), (200,300) and finally (200,100)

Now we need to convert this into an array of coordinates using javascript. Here is what I want to do:

  13 function getPointsFromString(dp) {
14  const _dp = dp.replace(/[MLZCQ]/ig,'').trim();
15  const points = _dp.split(' ')
16                 .filter(coor => coor.trim()!==''); // Removing empty coordinates
17  return points
18         .map(coor => parseFloat(coor)); // Converting to float
19 }
21 const d = __rect.getAttribute('d');
22 let drawPri = getPointsFromString(d);

I have a function which will get the points from path D string. I does that by first removing draw primitives and unwanted spaces. Splitting the sting using a space gives me an array of coordinates.

In line 21 I find the value of the D attribute, which I in line 22 pass to my function getPointsFromString and get an array of x,y values.

Lets add a group of blue circles on each coordinate, using the coordinates in drawPri.

SVG has its own namespace

Before we start creating the circles, we need to find the namespace of the SVG element. SVG is not a native part of HTML, and it lives inside its own domain or namespace. It is easy to find the namespace of any DOM element:

  24 const svgNS = __svg.namespaceURI; // http://www.w3.org/2000/svg

Now we can start dynamically adding circles. First I add a g (group) to the SVG, as it is easier to add circles to the SVG in this way. You can see the group element as a DIV in HTML: it can be used as a container for related elements. My group I have given a class _circles and my boilerplate/template will therefore already have created a global javascript reference to the group (__circles):

  26 for(var i=0; i<drawPri.length-1; i+=2) {
27  const circle = document.createElementNS(svgNS, 'circle');
28  circle.setAttribute('cx', drawPri[i]);
29  circle.setAttribute('cy', drawPri[i+1]);
30  circle.setAttribute('fill', 'blue');
31  circle.setAttribute('r', '10');
32  circle.setAttribute('opacity', '.5');
33  __circles.appendChild(circle);
34 };

I loop through the drawPri which contains the coordinates. Each point requires two points, so I step with 2 and get cx at offset i and cy at offset i+1. I create a namespaced element which I use appendChild method to append to the group elemenent __circles. We now have:

4. Generate two points between each pair of two point in the element

We now need to add two points between each of the found points. This is where it starts to get fun. I have an idea to do something like this:

  200 100 
xP0 yP0
xP1 yP1
400 100 

I want to do this by calculating the distance between both X and Y of the two points. Then I want to place the first X and Y of each point at 35% and at 65% of the length. This is not the final solution I need, but it will get me started and will also let me view the effect on the page, hopefully giving me a new clue as how I need to calculate the final placement.

I intend to loop through the known points and then create an new Array, which I set on the D attribute of the orange path. Lets try it!

  36 let path = [];
37 for(var i=0; i<drawPri.length-1; i+=2) {
38   // 
39   const nextI = (i+2>drawPri.length-1) ? 0 : i+2;
40   const lenX = drawPri[nextI]-drawPri[i],
41                lenY = drawPri[nextI+1]-drawPri[i+1]
42 path.push({x:drawPri[i]+lenX*.35, y:drawPri[i+1]+lenY*.35});
43 path.push({x:drawPri[i]+lenX*.65, y:drawPri[i+1]+lenY*.65});
44 }
46 var D = '';
47 for(var i=0; i<path.length; i++) {
48   const {x,y} = path[i];
49   const pre = (i===0) ? 'M ' : '',
50               post = (i===1) ? ' L' : '';
51   D += `${pre}${x} ${y}${post} `;
52 }
53 D+='Z';
54 __path.setAttribute('d', D);

In the loop from line 37 to 44 I iterate through the known coordinates. For each one I calculate the length between current coordinate X an Y. If it is the last coordinate, I use first point as next coordinate. I then add two new points (one at 25% and one at 75%) and push the points to the new path array.

In lines 47 to 54 I generate the D attribute for the path. It contains three draw primitives: M for the first point, L for the rest of the points and finally I close the path using the Z primitive. You may have asked, why do I only add L one time? Well, you instruct the path to use a given primitive, and it will keep using that primitive until you give it instructions to use a new draw primitive. It saves some bytes :-).

This results in this result:

Can you see what has changed? The orange path now lays on top of the rectangle. Not what I have as my goal, but we are getting there!

Next step

We need to do some math now. First let me put words on what we need to do:

Each point in the path (b) should be "pushed" out from the center of the points. We need to calculate the current radius for each point and then move it away, say, 35% from the center of the points.

Not so hard to describe, but how do we do that? First lets calculate the center of the rectangle:

  56 let minX = Infinity, maxX = -Infinity,
57      minY = Infinity, maxY = -Infinity,
58      count = 0;
59 for(var i=0; i<drawPri.length-1; i+=2) {
60  const x = drawPri[i],
61              y = drawPri[i+1];
62  minX = (x<minX) ? x : minX;
63  maxX = (x>maxX) ? x : maxX;
64  minY = (y<minY) ? y : minY;
65  maxY = (y>maxY) ? y : maxY;
66 }
67 const cx = (maxX-minX)/2+minX,
68          cy = (maxY-minY)/2+minY;
69 var center = document.createElementNS(svgNS, 'path');
70 center.setAttribute('stroke', 'black');
71 center.setAttribute('d', `M ${cx-5} ${cy-5} L ${cx+5} ${cy+5} M ${cx-5} ${cy+5} ${cx+5} ${cy-5}`);
72 __svg.appendChild(center);

I simply find the minimum and maximum of points and calculate the center. I then added a cross at that point using a path.

Actually I changed my mind - I wanted to use a simpler method for calculating the placement of each point. I simple extend the distance of each point in x and y direction, based on current distance to the center. Here you go:

  path = path.map(point => {
    var {x,y} = point;
    x = cx - (cx-x)*1.2;
    y = cy - (cy-y)*1.2;
    return {x,y}

After that is done the pen now looks like this:

Now to convert it to a mask I will change the SVG a bit. I will add a defs area with a mask element. I will add the path inside that mask, and use it on my main object (B). Also I need to draw a big rectangle in the mask to mask out the inner part of the figure. I needed to add a fill-rule="evenodd" and some other small brushing up in the code (now that is common as I do a pen!).

The final result looks like this:

Going further in the playground mode

Ofcause many part of the "final" pen could be improved, modified and optimized. Just to show you that I have made some extra things and also added some sliders (range sliders) for dynam ic changing the mask. See here and have fun constructing your ideas on CodePen! :-)

6,647 2 46