diff --git a/docs/reference/graphics.rst b/docs/reference/graphics.rst new file mode 100644 index 00000000..1dd73978 --- /dev/null +++ b/docs/reference/graphics.rst @@ -0,0 +1,20 @@ +Graphics +======== + +.. module:: p5 + :noindex: + +create_graphics() +----------------- + +.. autofunction:: p5.core.graphics.create_graphics + + + +Graphics +-------- + +.. autoclass:: p5.core.graphics.Graphics + :members: + :special-members: + diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 4c1843d9..b58d9c21 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -19,4 +19,5 @@ Reference lights materials io - data \ No newline at end of file + data + graphics \ No newline at end of file diff --git a/docs/reference/input.rst b/docs/reference/input.rst index 9ce9b4d1..f6d6dc90 100644 --- a/docs/reference/input.rst +++ b/docs/reference/input.rst @@ -145,7 +145,6 @@ is `False` otherwise. if mouse_is_pressed: # code to run when the mouse button is held down. -.. code:: python mouse_button ------------ diff --git a/p5/core/__init__.py b/p5/core/__init__.py index 95e85dcd..0f05fbd2 100644 --- a/p5/core/__init__.py +++ b/p5/core/__init__.py @@ -29,3 +29,4 @@ from .material import * from .light import * from .api import * +from .graphics import * diff --git a/p5/core/font.py b/p5/core/font.py index c18dcb0c..2f4889ec 100644 --- a/p5/core/font.py +++ b/p5/core/font.py @@ -97,7 +97,8 @@ def text_font(font, size=None): :param font: PIL.ImageFont.ImageFont for Vispy, Object|String: a font loaded via loadFont(), or a String representing a web safe font (a font that is generally available across all systems) - :type font: PIL.ImageFont.ImageFont + + :type font: PIL.ImageFont.ImageFont or Font Object """ p5.renderer.text_font(font, size) diff --git a/p5/core/graphics.py b/p5/core/graphics.py new file mode 100644 index 00000000..ac66e941 --- /dev/null +++ b/p5/core/graphics.py @@ -0,0 +1,39 @@ +from abc import ABC +from . import constants +from . import p5 + +__all__ = ["create_graphics"] + + +class Graphics(ABC): + """ + Thin wrapper around a renderer, to be used for creating a graphics buffer object. + Use this class if you need to draw into an off-screen graphics buffer. + The two parameters define the width and height in pixels. + The fields and methods for this class are extensive, but mirror the normal drawing API for p5. + + Graphics object are not meant to be used directly in sketches. + User should always use create_graphics to make an offscreen buffer + """ + + pass + + +def create_graphics(width, height, renderer=constants.P2D): + """ + Creates and returns a new off-screen graphics buffer that you can draw on + + :param width: width of the offscreen graphics buffer in pixels + :type width: int + + :param height: height of the offscreen graphics buffer in pixels + :type height: int + + :param renderer: Default P2D, and only available in skia renderer + :type renderer: constant + + :returns: Off screen graphics buffer + :rtype: Graphics + + """ + return p5.renderer.create_graphics(width, height, renderer) diff --git a/p5/core/image.py b/p5/core/image.py index 07d62fff..26d1d56f 100644 --- a/p5/core/image.py +++ b/p5/core/image.py @@ -184,8 +184,8 @@ def image(img, x, y, w=None, h=None): should be explicitly mentioned). The color of an image may be modified with the :meth:`p5.tint` function. - :param img: the image to be displayed. - :type img: PImage + :param img: PImage | Graphics object to be displayed. + :type img: PImage or Graphics :param x: x-coordinate of the image by default :type x: float @@ -227,7 +227,7 @@ def image_mode(mode): :type mode: str :raises ValueError: When the given image mode is not understood. - Check for typoes. + Check for types. """ diff --git a/p5/sketch/Skia2DRenderer/graphics.py b/p5/sketch/Skia2DRenderer/graphics.py new file mode 100644 index 00000000..82c29e6f --- /dev/null +++ b/p5/sketch/Skia2DRenderer/graphics.py @@ -0,0 +1,169 @@ +import skia + +from p5.core.graphics import Graphics +from . import renderer2d +from p5.core import p5 + +import p5 as p5_lib + + +def setup_default_renderer_dec(func): + def helper(*args, **kwargs): + current_renderer = p5.renderer + p5.renderer = args[0].renderer + return_value = func(*args, **kwargs) + p5.renderer = current_renderer + return return_value + + return helper + + +def wrap_instance_helper(func): + def helper(*args, **kwargs): + """ + Ignore the first argument passed and call the function. First argument would be reference to graphics object + of the method + """ + return func(*args[1:], **kwargs) + + return helper + + +class SkiaGraphics(Graphics): + def __init__(self, width, height): + """ + Creates a Skia based Graphics object + + :param width: width in pixels + :type width: int + + :param height: height in pixels + :type height: int + """ + self.width = width + self.height = height + # TODO: Try creating a GPU backed surface for better results + self.surface = skia.Surface(width, height) + self.canvas = self.surface.getCanvas() + self.path = skia.Path() + self.paint = skia.Paint() + self.renderer = renderer2d.SkiaRenderer() + + self.renderer.initialize_renderer(self.canvas, self.paint, self.path) + + +def bind(instance, func, as_name=None): + """ + Bind the function *func* to *instance*, with either provided name *as_name* + or the existing name of *func*. The provided *func* should accept the + instance as the first argument, i.e. "self". + """ + if as_name is None: + as_name = func.__name__ + bound_method = func.__get__(instance, instance.__class__) + setattr(instance, as_name, bound_method) + return bound_method + + +methods = [ + # Setting + p5_lib.background, + p5_lib.clear, + p5_lib.color_mode, + p5_lib.fill, + p5_lib.no_fill, + p5_lib.no_stroke, + p5_lib.stroke, + # Shape + p5_lib.arc, + p5_lib.ellipse, + p5_lib.circle, + p5_lib.point, + p5_lib.quad, + p5_lib.rect, + p5_lib.line, + p5_lib.square, + p5_lib.rect, + # Attributes + p5_lib.ellipse_mode, + p5_lib.rect_mode, + p5_lib.stroke_cap, + p5_lib.stroke_join, + p5_lib.stroke_weight, + # Curves + p5_lib.bezier, + p5_lib.bezier_detail, + p5_lib.bezier_point, + p5_lib.curve, + p5_lib.curve_detail, + p5_lib.curve_tightness, + p5_lib.curve_point, + p5_lib.curve_tangent, + # Vertex + p5_lib.begin_contour, + p5_lib.begin_shape, + p5_lib.bezier_vertex, + p5_lib.curve_vertex, + p5_lib.end_contour, + p5_lib.end_shape, + p5_lib.quadratic_vertex, + p5_lib.vertex, + # Structure + p5_lib.push, + p5_lib.pop, + p5_lib.push_matrix, + p5_lib.pop_matrix, + p5_lib.push_style, + p5_lib.pop_style, + # Transform + p5_lib.apply_matrix, + p5_lib.reset_matrix, + p5_lib.rotate, + p5_lib.scale, + p5_lib.shear_x, + p5_lib.shear_y, + p5_lib.translate, + # Local Storage + p5_lib.get_item, + p5_lib.clear_storage, + p5_lib.remove_item, + p5_lib.set_item, + # Image + p5_lib.save_canvas, + p5_lib.image, + p5_lib.tint, + p5_lib.no_tint, + p5_lib.image_mode, + p5_lib.load_pixels, + p5_lib.update_pixels, + p5_lib.noise, + p5_lib.noise_detail, + p5_lib.noise_seed, + # Random + p5_lib.random_seed, + p5_lib.random_uniform, + p5_lib.random_gaussian, + # Typography + p5_lib.text_align, + p5_lib.text_leading, + p5_lib.text_size, + p5_lib.text_style, + p5_lib.text_width, + p5_lib.text_ascent, + p5_lib.text_descent, + p5_lib.text_wrap, + p5_lib.text, + p5_lib.text_font, +] + + +def create_graphics_helper(width, height): + graphics = SkiaGraphics(width, height) + for method in methods: + bind( + graphics, + setup_default_renderer_dec(wrap_instance_helper(method)), + method.__name__, + ) + + return graphics diff --git a/p5/sketch/Skia2DRenderer/image.py b/p5/sketch/Skia2DRenderer/image.py index 6920156d..bf50fae7 100644 --- a/p5/sketch/Skia2DRenderer/image.py +++ b/p5/sketch/Skia2DRenderer/image.py @@ -4,6 +4,7 @@ import skia import builtins + class SkiaPImage(PImage): def __init__(self, width, height, pixels=None): self._width = width diff --git a/p5/sketch/Skia2DRenderer/renderer2d.py b/p5/sketch/Skia2DRenderer/renderer2d.py index 08a78eba..4e05878a 100644 --- a/p5/sketch/Skia2DRenderer/renderer2d.py +++ b/p5/sketch/Skia2DRenderer/renderer2d.py @@ -10,6 +10,7 @@ from p5.pmath.utils import * from .image import SkiaPImage +from .graphics import create_graphics_helper, SkiaGraphics @dataclass @@ -723,7 +724,11 @@ def image(self, pimage, x, y, w=None, h=None): size = pimage.size x += size[0] // 2 y += size[1] // 2 - self.canvas.drawImage(pimage.get_skia_image(), x, y) + + if isinstance(pimage, SkiaGraphics): + self.canvas.drawImage(pimage.surface.makeImageSnapshot(), x, y) + else: + self.canvas.drawImage(pimage.get_skia_image(), x, y) def load_pixels(self): c_array = self.canvas.toarray() @@ -740,9 +745,11 @@ def load_image(self, filename): def save_canvas(self, filename, canvas): if canvas: - # TODO: Get the surface of the PGraphics object yet to be implemented - pass - else: - canvas = self.canvas + canvas = canvas.canvas image = canvas.getSurface().makeImageSnapshot() image.save(filename) + + def create_graphics(self, width, height, renderer): + if renderer != constants.P2D: + raise NotImplementedError("Skia is only available for 2D sketches") + return create_graphics_helper(width, height) diff --git a/p5/sketch/Vispy2DRenderer/renderer2d.py b/p5/sketch/Vispy2DRenderer/renderer2d.py index 25a4ada9..96193d78 100644 --- a/p5/sketch/Vispy2DRenderer/renderer2d.py +++ b/p5/sketch/Vispy2DRenderer/renderer2d.py @@ -600,3 +600,8 @@ def save_canvas(self, filename, canvas): p5.sketch.screenshot(filename) else: p5.sketch.screenshot("Screen.png") + + def create_graphics(self, width, height, renderer): + raise NotImplementedError( + "Vispy Renderer does not support offscreen buffers yet, use 'skia' as your backend renderer" + )