2

The reason I'm interested in canvases having any shape is that it would then be possible to cut out images with Bezier curves and have the text of a web page flow around the canvas shape, i.e. cut-out image.

What is needed is the possibility to have a free-form shaped div, SVG and HTML5 canvas. (Applied to SVG, I understand this would be equivalent to Flash symbols.) You could then imagine applying a box model (padding, border and margin) for shapes, but it wouldn't be a box (it would be parallel to the shape)!

I suppose it would also then be possible to have text that wraps inside a shape as much as text that flows around a shape.

I read an interesting blog post about "Creating Non-Rectangular Layouts with CSS Shapes" here: http://sarasoueidan.com/blog/css-shapes/

but it doesn't include text wrapping inside a shape.

Then, there's also a CSS Shapes editor for Brackets (a code editor): http://blogs.adobe.com/webplatform/2014/04/17/css-shapes-editor-in-brackets/

Olivier
  • 607
  • 1
  • 6
  • 21
  • Canvas is a cartesian coordinate system, which is rectangular by definition. – Robert Harvey Apr 15 '14 at 17:19
  • 2
    The canvas area is rectangular, but what you draw is not limited by that constraint. By design, you could make it appear in any shape that you desire. In regards to a webpage, you could design a layout that gives your desired effect. Think of images with transparent backgrounds. They are still rectangular in the end, but don't appear that way to the viewer. – CM Kanode Apr 15 '14 at 17:20
  • 1
    Actually, *you can apply css styling to canvas*, but you do so at the risk of distorting the pixels drawn into the canvas. As an example of non-distorting CSS, setting a border-radius on the canvas will alter the canvas element by creating a pseudo-clip, but it won't distort the pixel content. Here is a link showing inventive use of border radius to clip a rect (like canvas) into shapes: http://www.css3shapes.com/ – markE Apr 15 '14 at 17:25

1 Answers1

5

As simple as it may sound it actually involves quite a few steps to achieve.

An outline would look something like this:

  • Define the shape as a polygon, ie. point array
  • Find bounds of polygon (the region the polygon fits inside)
  • Contract polygon with padding using either a cetronid algorithm or simply a brute-force approach using center of bounds
  • Define line height of text and use that as a basis for number of scan-lines
  • Basically use a polygon-fill algorithm to find segment within the shape which can fill in text. The steps for this is:
    • Use an odd/even scanner by getting an intersection point (using line intersection math) with text scan line and each of the lines between the points in the polygon
    • Sort the points by x
    • use odd and even point to create a segment. This segment will always be inside the polygon
  • Add clipping using original polygon
  • Draw in image
  • Use the segments to get a width. Start parsing the text to fill and measure the width.
  • When text width fits within the segment width then print the chars that fits
  • Repeat for next text/words/chars until end of text or segments

In other words: you would need to implement a polygon fill algorithm but instead of filling in lines (per pixel line) you use the line as basis for the text.

This is fully doable; actually, I went ahead to create a challenge for myself on this problem, for the fun of it, so I created a generic solution that I put on GitHub released under MIT license.

The principle described above are implemented, and to visualize the steps:

Define the polygon and padding - here I chose to just use a simple brute-force and calculate a smaller polygon based on center and a padding value - the light grey is the original polygon and the black obviously the contracted version:

Polygon

The points are defined as an array [x1, y1, x2, y2, ... xn, yn] and the code to contract it (see link to project for full source on all these parts):

var pPoints = [],
    i = 0, x, y, a, d, dx, dy;

for(; i < points.length; i += 2) {
    x = points[i];
    y = points[i+1];
    dx = x - bounds.px;
    dy = y - bounds.py;
    a = Math.atan2(dy, dx);
    d = Math.sqrt(dx*dx + dy*dy) - padding;

    pPoints.push(bounds.px + d * Math.cos(a),
                 bounds.py + d * Math.sin(a));
}

Next step is to define the lines we want to scan. The lines are based on line height for font:

Scanlines

That is simple enough - just make sure the start and end points are outside the polygon.

We use an odd/even scan approach and check intersection of the scanline versus all lines in the polygon. If we get a intersect point we store that in a list for that line.

The code to detect intersecting lines is:

function getIntersection(line1, line2) {

    // "unroll" the objects
    var p0x = line1.x1,
        p0y = line1.y1,
        p1x = line1.x2,
        p1y = line1.y2,
        p2x = line2.x1,
        p2y = line2.y1,
        p3x = line2.x2,
        p3y = line2.y2,

    // calc difference between the coords
        d1x = p1x - p0x,
        d1y = p1y - p0y,
        d2x = p3x - p2x,
        d2y = p3y - p2y,

    // determinator
        d = d1x * d2y - d2x * d1y,

        px, py,
        s, t;

    // if is not intersecting/is parallel then return immediately
    if (Math.abs(d) < 1e-14)
        return null;

    // solve x and y for intersecting point
    px = p0x - p2x;
    py = p0y - p2y;

    s = (d1x * py - d1y * px) / d;
    if (s >= 0 && s <= 1) {

        // if s was in range, calc t
        t = (d2x * py - d2y * px) / d;
        if (t >= 0 && t <= 1) {

            return {x: p0x + (t * d1x),
                    y: p0y + (t * d1y)}
        }
    }
    return null;
}

Intersections

Then we sort the point for each line and use pairs of points to create segments - this is actually a polygon-fill algorithm. The result will be:

Segments

The code to build segments is a bit extensive for this post so check out the project linked above.

And finally we use those segments to replace with actual text. We need to scan a text from current text pointer and see how much will fit inside the segment width. The current code is somewhat basic and skips a lot of considerations such as word breaks, text base-line position and so forth, but for initial use it will do.

The result when put together will be:

Result

Hope this gives an idea about the steps involved.

  • 1
    I had the same idea you had with the horizontal lines corresponding to the font height intersecting the shape. Your example also applies to a polygon shape. We now have to do the same for shapes delimited by Bezier curves. To find the intersection of the horizontal lines with a shape, we could possibly use the KineticJS framework to make things easier. It includes a Kinetic.Path.getPointOnCubicBezier function. I've requested more info from Eric Rowell, but he hasn't responded yet. The algorithm could also be further improved with justified/right/left text alignment options. Nice work, Crypto! – Olivier Apr 16 '14 at 20:46
  • http://stackoverflow.com/questions/12420092/html5-canvas-get-curves-coordinates?rq=1 – Olivier Apr 16 '14 at 20:47