Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to draw to an off screen buffer #387

Merged
merged 4 commits into from
Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/reference/graphics.rst
Original file line number Diff line number Diff line change
@@ -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:

3 changes: 2 additions & 1 deletion docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Reference
lights
materials
io
data
data
graphics
1 change: 0 additions & 1 deletion docs/reference/input.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------
Expand Down
1 change: 1 addition & 0 deletions p5/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@
from .material import *
from .light import *
from .api import *
from .graphics import *
3 changes: 2 additions & 1 deletion p5/core/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions p5/core/graphics.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions p5/core/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

"""

Expand Down
169 changes: 169 additions & 0 deletions p5/sketch/Skia2DRenderer/graphics.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions p5/sketch/Skia2DRenderer/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import skia
import builtins


class SkiaPImage(PImage):
def __init__(self, width, height, pixels=None):
self._width = width
Expand Down
17 changes: 12 additions & 5 deletions p5/sketch/Skia2DRenderer/renderer2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from p5.pmath.utils import *

from .image import SkiaPImage
from .graphics import create_graphics_helper, SkiaGraphics


@dataclass
Expand Down Expand Up @@ -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()
Expand All @@ -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)
5 changes: 5 additions & 0 deletions p5/sketch/Vispy2DRenderer/renderer2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)