p5py / p5

p5 is a Python package based on the core ideas of Processing.
https://p5.readthedocs.io
GNU General Public License v3.0
724 stars 120 forks source link

SVG output #159

Open marcrleonard opened 4 years ago

marcrleonard commented 4 years ago

Are there any plans for SVG output? This would be a fantastic feature, as I know it's used a lot in processing and p5? Would this be attached to the PShape class?

Upvote & Fund

Fund with Polar

jeremydouglass commented 4 years ago

Simple support would be great, but SVG reading writing can be extremely complex -- some python libraries that exist and might help with this undertaking are svgwrite, drawsvg, cairo svg.

In terms of what exists currently (read) -- I believe that it is here: https://github.com/p5py/p5/blob/master/p5/core/svg.py https://github.com/p5py/p5/blob/master/p5/core/tests/test_svg.py

PShape in Java mode is a bit of a crazy mess -- partly because its internal representation and how its methods work differs based on whether you create one from SVG, or from as a new primitive, or by drawing vertices. It would be nice to not repeat those mistakes while building out:

https://github.com/p5py/p5/blob/master/p5/core/shape.py

marcrleonard commented 4 years ago

Yes. Handling svgs is surprising complex. Personally, the ultimate reason I’d like this feature is so I can send the output file to my pen plotter.

... any idea if there is any current interchange method that could do this?

marcrleonard commented 4 years ago

I was just taking a peek at the svg parser, and had a question:

Once the parser determines that a path may be a particular shape (for instance, it parses an SVG and finds a 'rectangle'), and it converts that to a PShape, does the PShape know that it is a rectangle? Or no? Seems like that would determine where the SVG functions live. At the PShape level, or it's derived shape classes.

Like the reader does, the writer would need to know what the 'shape' each PShape is. If that logic could be bidirectional (SVG reader and writer), then it could be a step in the right direction.

arihantparsoya commented 4 years ago

does the PShape know that it is a rectangle? Or no?

@marcrleonard the SVG parser does not store the meta information of the shape (rect, circle, etc)

the ultimate reason I’d like this feature is so I can send the output file to my pen plotter.

I believe Processing has a good rendering engine for exporting in PDFs and SVGs: https://processing.org/reference/libraries/svg/index.html

marcrleonard commented 4 years ago

I believe Processing has a good rendering engine for exporting in PDFs and SVGs:

It does, but I don't want to use processing... I want to use python ;-)

I would like to start hacking on SVG output for p5py. To mirror p5.js, that would ultimately be part of the save() method, right? Any other notable things before I start digging?

marcrleonard commented 4 years ago

Sorry to spam this thread, but I've got some more questions: I've been poking around on the flow from a shape/primitives creation, and how it eventually ripples into the Render2D, and the objects are added to the self.draw_queue ... Is this the last place they 'live' until they are rasterized to the output? Does list list contain EVERYTHING that needs to be rendered in a particular frame? I assume that list is emptied once a frame is done being shown?

arihantparsoya commented 4 years ago

that would ultimately be part of the save() method, right?

The p5.js save method saves the image file of the sketch not the svg version: https://p5js.org/reference/#/p5/save

the objects are added to the self.draw_queue ... Is this the last place they 'live' until they are rasterized to the output?

Yes, the vertex data is converted into vertex_buffer and index_buffer which is then used by OpenGL shaders (https://github.com/p5py/p5/blob/master/p5/sketch/shaders2d.py#L26) for rendering.

Does list list contain EVERYTHING that needs to be rendered in a particular frame?

Yes, depending on the shape, its relevant shape attributes are added to the draw_queue (https://github.com/p5py/p5/blob/master/p5/sketch/renderer2d.py#L282). This includes: vertices, idx (the index triangles which needs to be rendered from the vertices), fill, etc.

I assume that list is emptied once a frame is done being shown?

Yes

marcrleonard commented 4 years ago

Here is how an svg is saved in P5.js:

https://editor.p5js.org/dannyrozin/sketches/r1djoVow7

... In classic P5 form, it's a very odd API. You have to first define the canvas to be an SVG canvas, then it is saved.

Is this to be mirrored here? Looking for any implementation details that my be helpful. Is there some sort of hook or plugin architecture I could utilize? That way, it doesn't have to get baked into the core architecture (if the contributors don't want it).

marcrleonard commented 4 years ago

If you look at the Svg output from the p5js sketch above, you’ll see that each transform is applied to each element recursively (to reflect the loop).

It appears that the p5.renderer does not track transforms in a similar way. It appears all transforms are flattened before going to the GPU for final render (which is perfectly logical). Is there a place where transforms are stored individually, so they can be applied to all the ‘underlying’ elements? Or does each transform get applied directly to the vertices, and left there?

marcrleonard commented 4 years ago

Here is a crazy simplistic version I've started with. It doesn't support all the fills/strokes/etc... But it seems to work properly for simple lines and circles (though, the circles use 'triangles' ... so that is probably not ideal).

import drawSvg

def save_svg(output_path):
    dq = p5.renderer.draw_queue

    # why is this different from the the specified canvas size? is there a transform/scaler soemwhere?
    print(p5.sketch.size)

    svg_canvas = drawSvg.Drawing(window_w, window_h, origin=(0,0), displayInline=False)

    # There may be a better way to do this through the init above, but I found it confusing.
    # it was much easier to just hardcode it.
    svg_canvas.viewBox = (0, 0, window_w, window_h)

    for geo, meta in dq:

        if geo == 'lines':

            vertices, edges, stroke, stroke_weight, stroke_cap, stroke_join = meta

            verts = vertices.tolist()

            start_l = verts.pop(0)
            start_x = start_l[0]

            # the lib wants to always make Y coods negative. This is likely because of the assumption
            # the moving physically down on the Y axis puts an object in the correct region.
            # Basically, negative Y is 'viewable' where as in P5, positive Y is viewable.
            start_y = -start_l[1]

            other_verts = []
            for o_v in verts:
                x=o_v[0]

                # See note above about negative Y values.
                y=-o_v[1]

                z=o_v[2]
                other_verts.append(x)
                other_verts.append(y)

            svg_obj = drawSvg.Lines(start_x, start_y, *other_verts, fill='black', stroke='black', stroke_width=2, close=False)

        elif geo == 'triangles':
            vertices, idx, fill = meta

            verts = vertices.tolist()

            start_l = verts.pop(0)
            start_x = start_l[0]

            # See note above about negative Y values.
            start_y = -start_l[1]

            other_verts = []
            for o_v in verts:
                x = o_v[0]

                # See note above about negative Y values.
                y = -o_v[1]

                z = o_v[2]
                other_verts.extend([x,y])

            svg_obj = drawSvg.Lines(start_x, start_y, *other_verts, fill='black', stroke='black', stroke_width=2,
                                     close=False)

        else:
            # A good example is 'Points"
            # elif geo == 'points':
            #   vertices, idx, stroke = meta
            #   raise NotImplemented()

            raise NotImplemented("This geometry is not implemented yet.")

        svg_canvas.append(svg_obj)

    svg_canvas.saveSvg(output_path)

(as per a comment in there) why is p5.sketch.size different from the the specified canvas size? is there a transform/scaler somewhere?

arihantparsoya commented 4 years ago

Is there a place where transforms are stored individually, so they can be applied to all the ‘underlying’ elements?

No, when a function for a shape is called, its vertices are transformed and stored in the queue for rendering: https://github.com/p5py/p5/blob/master/p5/sketch/renderer2d.py#L221

arihantparsoya commented 4 years ago

why is p5.sketch.size different from the the specified canvas size? is there a transform/scaler somewhere?

The p5.sketch.size is updated when size() function is called: https://github.com/p5py/p5/blob/master/p5/sketch/userspace.py#L186

marcrleonard commented 4 years ago

Is there a place where transforms are stored individually, so they can be applied to all the ‘underlying’ elements?

No, when a function for a shape is called, its vertices are transformed and stored in the queue for rendering: https://github.com/p5py/p5/blob/master/p5/sketch/renderer2d.py#L221

Ok thanks. This will be a fundamental difference between how the SVG is created (versus p5.js). But honestly, it's better this way. Way less recursive mess :-)

arihantparsoya commented 4 years ago

Here is a crazy simplistic version I've started with.

I think this is a really good implementation. I was able to use it with minor changes.

marcrleonard commented 4 years ago

Here is a crazy simplistic version I've started with.

I think this is a really good implementation. I was able to use it with minor changes.

Great. I've also made some tweaks since. If you all want to continue with this code, is there a place I can start contributing/committing?

arihantparsoya commented 4 years ago

This can be added in the saveFrame API: https://github.com/p5py/p5/blob/master/p5/core/image.py#L611

The drawSvg needs to be added as dependency for this to work inside p5py. I am not sure if adding new dependency will cause any unnecessary issues. @abhikpal , what do you think?

harrywang commented 3 years ago

Adding SVG output support would be great for people using pen-plotters! Thanks for working on this!!

earthbound19 commented 3 years ago

I second that! Thanks for working on this! And SVG output would also be great for generative art archiving, and infinitely scalable images!

villares commented 3 years ago

Maybe one could have a look at what Ricardo Lafonte and Stuart Axon are doing with pycairo & pango at http://github.com/shoebot/shoebot (I suppose Skia anf Flat could be ways to solve this too)