sketchingpy.sketch2dweb

HTML5 Canvas-based renderer for Sketchingpy.

License:

BSD

   1"""HTML5 Canvas-based renderer for Sketchingpy.
   2
   3License:
   4    BSD
   5"""
   6
   7import csv
   8import io
   9import json
  10import os.path
  11import time
  12import typing
  13import urllib.parse
  14
  15has_js = False
  16try:
  17    import js  # type: ignore
  18    import pyodide.ffi  # type: ignore
  19    import pyodide.http  # type: ignore
  20    import pyscript  # type: ignore
  21    has_js = True
  22except:
  23    pass
  24
  25import sketchingpy.abstracted
  26import sketchingpy.const
  27import sketchingpy.control_struct
  28import sketchingpy.data_struct
  29import sketchingpy.state_struct
  30
  31DEFAULT_FPS = 20
  32
  33KEY_MAP = {
  34    'arrowleft': sketchingpy.const.KEYBOARD_LEFT_BUTTON,
  35    'arrowup': sketchingpy.const.KEYBOARD_UP_BUTTON,
  36    'arrowright': sketchingpy.const.KEYBOARD_RIGHT_BUTTON,
  37    'arrowdown': sketchingpy.const.KEYBOARD_DOWN_BUTTON,
  38    ' ': sketchingpy.const.KEYBOARD_SPACE_BUTTON,
  39    'control': sketchingpy.const.KEYBOARD_CTRL_BUTTON,
  40    'alt': sketchingpy.const.KEYBOARD_ALT_BUTTON,
  41    'shift': sketchingpy.const.KEYBOARD_SHIFT_BUTTON,
  42    'tab': sketchingpy.const.KEYBOARD_TAB_BUTTON,
  43    'home': sketchingpy.const.KEYBOARD_HOME_BUTTON,
  44    'end': sketchingpy.const.KEYBOARD_END_BUTTON,
  45    'enter': sketchingpy.const.KEYBOARD_RETURN_BUTTON,
  46    'backspace': sketchingpy.const.KEYBOARD_BACKSPACE_BUTTON,
  47    'null': None
  48}
  49
  50OPTIONAL_SKETCH_CALLBACK = typing.Optional[typing.Callable[[sketchingpy.abstracted.Sketch], None]]
  51
  52
  53class CanvasRegionEllipse:
  54    """Description of a region of a canvas expressed as an ellipse."""
  55
  56    def __init__(self, x: float, y: float, radius_x: float, radius_y: float):
  57        """Create a new elliptical region record.
  58
  59        Args:
  60            x: The center x coordinate of the region.
  61            y: The center y coordinate of the region.
  62            radius_x: The horizontal radius of the region.
  63            radius_y: The vertical radius of the region.
  64        """
  65        self._x = x
  66        self._y = y
  67        self._radius_x = radius_x
  68        self._radius_y = radius_y
  69
  70    def get_x(self) -> float:
  71        """Get the center horizontal coordinate of this region.
  72
  73        Returns:
  74            The center x coordinate of the region.
  75        """
  76        return self._x
  77
  78    def get_y(self) -> float:
  79        """Get the center vertical coordinate of this region.
  80
  81        Returns:
  82            The center y coordinate of the region.
  83        """
  84        return self._y
  85
  86    def get_radius_x(self) -> float:
  87        """Get the horizontal size of this region.
  88
  89        Returns:
  90            The horizontal radius of the region.
  91        """
  92        return self._radius_x
  93
  94    def get_radius_y(self) -> float:
  95        """Get the vertical size of this region.
  96
  97        Returns:
  98            The vertical radius of the region.
  99        """
 100        return self._radius_y
 101
 102
 103class CanvasRegionRect:
 104    """Description of a region of a canvas expressed as a rectangle."""
 105
 106    def __init__(self, x: float, y: float, width: float, height: float):
 107        """Create a new rectangular region record.
 108
 109        Args:
 110            x: The center x coordinate of the region.
 111            y: The center y coordinate of the region.
 112            radius_x: The horizontal size of the region.
 113            radius_y: The vertical size of the region.
 114        """
 115        self._x = x
 116        self._y = y
 117        self._width = width
 118        self._height = height
 119
 120    def get_x(self) -> float:
 121        """Get the start horizontal coordinate of this region.
 122
 123        Returns:
 124            The left x coordinate of the region.
 125        """
 126        return self._x
 127
 128    def get_y(self) -> float:
 129        """Get the start vertical coordinate of this region.
 130
 131        Returns:
 132            The top y coordinate of the region.
 133        """
 134        return self._y
 135
 136    def get_width(self) -> float:
 137        """Get the horizontal size of this region.
 138
 139        Returns:
 140            The horizontal width of the region.
 141        """
 142        return self._width
 143
 144    def get_height(self) -> float:
 145        """Get the vertical size of this region.
 146
 147        Returns:
 148            The vertical height of the region.
 149        """
 150        return self._height
 151
 152
 153class WebBuffer:
 154    """Structure for an offscreen buffer."""
 155
 156    def __init__(self, canvas, context, width: int, height: int):
 157        """Create a new offscreen record.
 158
 159        Args:
 160            canvas: The offscreen canvas element.
 161            context: The 2D drawing context.
 162            width: The horizontal size of the buffer in pixels.
 163            height: The vertical size of the buffer in pixels.
 164        """
 165        self._canvas = canvas
 166        self._context = context
 167        self._width = width
 168        self._height = height
 169
 170    def get_element(self):
 171        """Get the offscreen canvas object.
 172
 173        Returns:
 174            The offscreen canvas element.
 175        """
 176        return self._canvas
 177
 178    def get_context(self):
 179        """Get the drawing context compatiable with the main canvas.
 180
 181        Returns:
 182            The 2D drawing context.
 183        """
 184        return self._context
 185
 186    def get_width(self) -> int:
 187        """Get the horizontal size of the buffer in pixels.
 188
 189        Returns:
 190            Width of the offscreen canvas.
 191        """
 192        return self._width
 193
 194    def get_height(self) -> int:
 195        """Get the vertical size of the buffer in pixels.
 196
 197        Returns:
 198            Height of the offscreen canvas.
 199        """
 200        return self._height
 201
 202
 203class Sketch2DWeb(sketchingpy.abstracted.Sketch):
 204    """Sketch renderer for web / HTML5."""
 205
 206    def __init__(self, width: float, height: float, element_id: str = 'sketch-canvas',
 207        loading_id: typing.Optional[str] = 'sketch-load-message'):
 208        """Create a new HTML5 Canvas-based sketch.
 209
 210        Args:
 211            width: The horizontal size of the sketch in pixels. Will update the HTML5 element.
 212            height: The vertical size of the sketch in pixels. Will update the HTML5 element.
 213            element_id: The ID (HTML) of the canvas into which this sketch should be drawn.
 214            loading_id: The ID (HTML) of the loading message to hide upon showing the sketch.
 215        """
 216        super().__init__()
 217
 218        if not has_js:
 219            raise RuntimeError('Cannot access JS / pyodide.')
 220
 221        # Save elements required for running the canvas
 222        self._element_id = element_id
 223        self._element = js.document.getElementById(element_id)
 224        self._element.width = width
 225        self._element.height = height
 226        self._element.style.display = 'none'
 227        self._context = self._element.getContext('2d')
 228        self._last_render = None
 229
 230        self._loading_id = loading_id
 231        self._loading_element = js.document.getElementById(loading_id)
 232
 233        # Internal only elements
 234        self._internal_loop_callback = None
 235        self._internal_mouse_x = 0
 236        self._internal_mouse_y = 0
 237        self._internal_pre_show_actions: typing.List[typing.Callable] = []
 238        self._added_fonts: typing.Set[str] = set()
 239
 240        # Buffers
 241        self._buffers: typing.Dict[str, WebBuffer] = {}
 242        self._base_context = self._context
 243        self._base_element = self._element
 244        self._width = self._element.width
 245        self._height = self._element.height
 246
 247        # User configurable state
 248        self._state_frame_rate = DEFAULT_FPS
 249        self._stopped = False
 250
 251        # Callback
 252        self._callback_step: OPTIONAL_SKETCH_CALLBACK = None
 253        self._callback_quit: OPTIONAL_SKETCH_CALLBACK = None
 254
 255        # Control
 256        self._keyboard = PyscriptKeyboard(self._element)
 257        self._mouse = PyscriptMouse(self._element)
 258
 259    ##########
 260    # Buffer #
 261    ##########
 262
 263    def create_buffer(self, name: str, width: int, height: int,
 264        background: typing.Optional[str] = None):
 265        canvas = js.window.OffscreenCanvas.new(width, height)
 266        context = canvas.getContext('2d')
 267        self._buffers[name] = WebBuffer(canvas, context, width, height)
 268        if background is not None:
 269            context.clearRect(0, 0, self._width, self._height)
 270            context.fillStyle = background
 271            context.fillRect(0, 0, self._width, self._height)
 272
 273    def enter_buffer(self, name: str):
 274        web_buffer = self._buffers[name]
 275        self._context = web_buffer.get_context()
 276        self._element = web_buffer.get_element()
 277        self._width = web_buffer.get_width()
 278        self._height = web_buffer.get_height()
 279
 280    def exit_buffer(self):
 281        self._context = self._base_context
 282        self._element = self._base_element
 283        self._width = self._base_element.width
 284        self._height = self._base_element.height
 285
 286    def draw_buffer(self, x: float, y: float, name: str):
 287        web_buffer = self._buffers[name]
 288        self._context.drawImage(web_buffer.get_element(), x, y)
 289
 290    ############
 291    # Controls #
 292    ############
 293
 294    def get_keyboard(self) -> typing.Optional[sketchingpy.control_struct.Keyboard]:
 295        return self._keyboard
 296
 297    def get_mouse(self) -> typing.Optional[sketchingpy.control_struct.Mouse]:
 298        return self._mouse
 299
 300    ########
 301    # Data #
 302    ########
 303
 304    def get_data_layer(self) -> typing.Optional[sketchingpy.data_struct.DataLayer]:
 305        return WebDataLayer()
 306
 307    ###########
 308    # Dialogs #
 309    ###########
 310
 311    def get_dialog_layer(self) -> typing.Optional[sketchingpy.dialog_struct.DialogLayer]:
 312        return WebDialogLayer(self)
 313
 314    ###########
 315    # Drawing #
 316    ###########
 317
 318    def clear(self, color: str):
 319        self._context.clearRect(0, 0, self._width, self._height)
 320        self._context.fillStyle = color
 321        self._context.fillRect(0, 0, self._width, self._height)
 322
 323    def draw_arc(self, x1: float, y1: float, x2: float, y2: float, a1: float, a2: float):
 324        self._load_draw_params()
 325
 326        a1_rad = self._convert_to_radians(a1) - js.Math.PI / 2
 327        a2_rad = self._convert_to_radians(a2) - js.Math.PI / 2
 328
 329        current_machine = self._get_current_state_machine()
 330        mode_native = current_machine.get_arc_mode_native()
 331        mode_str = current_machine.get_arc_mode()
 332
 333        self._draw_arc_rad(x1, y1, x2, y2, a1_rad, a2_rad, mode_native, mode_str)
 334
 335    def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
 336        current_machine = self._get_current_state_machine()
 337        mode_native = current_machine.get_ellipse_mode_native()
 338        mode_str = current_machine.get_ellipse_mode()
 339
 340        self._draw_arc_rad(x1, y1, x2, y2, 0, 2 * js.Math.PI, mode_native, mode_str)
 341
 342    def draw_line(self, x1: float, y1: float, x2: float, y2: float):
 343        current_machine = self._get_current_state_machine()
 344        if not current_machine.get_stroke_enabled():
 345            return
 346
 347        self._load_draw_params()
 348
 349        self._context.beginPath()
 350        self._context.moveTo(x1, y1)
 351        self._context.lineTo(x2, y2)
 352        self._context.stroke()
 353
 354    def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
 355        self._load_draw_params()
 356
 357        current_machine = self._get_current_state_machine()
 358        native_mode = current_machine.get_rect_mode_native()
 359        mode_str = current_machine.get_rect_mode_native()
 360
 361        region = self._get_canvas_region_rect_like(x1, y1, x2, y2, native_mode, mode_str)
 362
 363        self._context.beginPath()
 364        self._context.rect(region.get_x(), region.get_y(), region.get_width(), region.get_height())
 365
 366        if current_machine.get_fill_enabled():
 367            self._context.fill()
 368
 369        if current_machine.get_stroke_enabled():
 370            self._context.stroke()
 371
 372    def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
 373        current_machine = self._get_current_state_machine()
 374
 375        self._load_draw_params()
 376
 377        self._context.beginPath()
 378        self._context.moveTo(shape.get_start_x(), shape.get_start_y())
 379
 380        for segment in shape.get_segments():
 381            strategy = segment.get_strategy()
 382            if strategy == 'straight':
 383                self._context.lineTo(segment.get_destination_x(), segment.get_destination_y())
 384            elif strategy == 'bezier':
 385                self._context.bezierCurveTo(
 386                    segment.get_control_x1(),
 387                    segment.get_control_y1(),
 388                    segment.get_control_x2(),
 389                    segment.get_control_y2(),
 390                    segment.get_destination_x(),
 391                    segment.get_destination_y()
 392                )
 393            else:
 394                raise RuntimeError('Unsupported segment type: ' + strategy)
 395
 396        if shape.get_is_closed():
 397            self._context.closePath()
 398
 399        if current_machine.get_fill_enabled():
 400            self._context.fill()
 401
 402        if current_machine.get_stroke_enabled():
 403            self._context.stroke()
 404
 405    def draw_text(self, x: float, y: float, content: str):
 406        content = str(content)
 407        current_machine = self._get_current_state_machine()
 408
 409        self._load_draw_params()
 410        self._load_font_params()
 411
 412        if current_machine.get_fill_enabled():
 413            self._context.fillText(content, x, y)
 414
 415        if current_machine.get_stroke_enabled():
 416            self._context.strokeText(content, x, y)
 417
 418    ##########
 419    # Events #
 420    ##########
 421
 422    def on_step(self, callback: sketchingpy.abstracted.StepCallback):
 423        self._callback_step = callback
 424
 425    def on_quit(self, callback: sketchingpy.abstracted.QuitCallback):
 426        self._callback_quit = callback
 427
 428    #########
 429    # Image #
 430    #########
 431
 432    def get_image(self, src: str) -> sketchingpy.abstracted.Image:
 433        return WebImage(src)
 434
 435    def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
 436        if not image.get_is_loaded():
 437            return
 438
 439        self._load_draw_params()
 440
 441        current_machine = self._get_current_state_machine()
 442        native_mode = current_machine.get_image_mode_native()
 443        mode_str = current_machine.get_image_mode_native()
 444
 445        width = image.get_width()
 446        height = image.get_height()
 447
 448        region = self._get_canvas_region_rect_like(x, y, width, height, native_mode, mode_str)
 449
 450        self._context.drawImage(
 451            image.get_native(),
 452            region.get_x(),
 453            region.get_y(),
 454            region.get_width(),
 455            region.get_height()
 456        )
 457
 458    def save_image(self, path: str):
 459        if not path.endswith('.png'):
 460            raise RuntimeError('Web export only supported to PNG.')
 461
 462        link = js.document.createElement('a')
 463        link.download = path
 464        link.href = self._element.toDataURL('image/png')
 465        link.click()
 466
 467    #########
 468    # State #
 469    #########
 470
 471    def set_text_font(self, identifier: str, size: float):
 472        super().set_text_font(identifier, size)
 473
 474        is_otf = identifier.endswith('.otf')
 475        is_ttf = identifier.endswith('.ttf')
 476        is_file = is_otf or is_ttf
 477
 478        if not is_file:
 479            return
 480
 481        current_machine = self._get_current_state_machine()
 482        font = current_machine.get_text_font()
 483        font_name = sketchingpy.abstracted.get_font_name(font, '/')
 484
 485        if font_name in self._added_fonts:
 486            return
 487
 488        naked_font_name_components = font_name.split(' ')[1:]
 489        naked_font_name = ' '.join(naked_font_name_components)
 490
 491        new_font = pyscript.window.FontFace.new(naked_font_name, 'url(%s)' % identifier)
 492        new_font.load()
 493        pyscript.document.fonts.add(new_font)
 494        self._added_fonts.add(font_name)
 495
 496    def push_transform(self):
 497        self._context.save()
 498
 499    def pop_transform(self):
 500        self._context.restore()
 501
 502    ##########
 503    # System #
 504    ##########
 505
 506    def get_native(self):
 507        return self._element
 508
 509    def set_fps(self, rate: int):
 510        self._state_frame_rate = rate
 511
 512    def set_title(self, title: str):
 513        js.document.title = title
 514
 515    def quit(self):
 516        self._stopped = True
 517
 518    def show(self, ax=None):
 519        self._show_internal(ax=ax, quit_immediately=False)
 520
 521    def show_and_quit(self, ax=None):
 522        self._show_internal(ax=ax, quit_immediately=True)
 523
 524    def print(self, message: str):
 525        console_id = self._element_id + '-console'
 526        target_root = js.document.getElementById(console_id)
 527
 528        if target_root is None:
 529            print(message)
 530            return
 531
 532        new_li = pyscript.document.createElement('li')
 533        new_content = pyscript.document.createTextNode(message)
 534        new_li.appendChild(new_content)
 535
 536        target_root.appendChild(new_li)
 537
 538    #############
 539    # Transform #
 540    #############
 541
 542    def translate(self, x: float, y: float):
 543        self._context.translate(x, y)
 544
 545    def rotate(self, angle: float):
 546        angle_rad = self._convert_to_radians(angle)
 547        self._context.rotate(angle_rad)
 548
 549    def scale(self, scale: float):
 550        self._context.scale(scale, scale)
 551
 552    ###########
 553    # Support #
 554    ###########
 555
 556    def _show_internal(self, ax=None, quit_immediately=False):
 557        self._loading_element.style.display = 'none'
 558        self._element.style.display = 'inline-block'
 559        self._version = str(round(time.time()))
 560        self._element.setAttribute('version', self._version)
 561
 562        self._snapshot_time()
 563
 564        for action in self._internal_pre_show_actions:
 565            action()
 566
 567        if not quit_immediately:
 568            self._stopped = False
 569
 570            self._last_render = time.time()
 571            self._internal_loop_callback = pyodide.ffi.create_proxy(lambda: self._inner_loop())
 572
 573            self._inner_loop()
 574
 575    def _inner_loop(self):
 576        if self._element.getAttribute('version') != self._version:
 577            self._stopped = True
 578
 579        if self._stopped:
 580            if self._callback_quit is not None:
 581                self._callback_quit(self)
 582            return
 583
 584        if self._callback_step is not None:
 585            self._callback_step(self)
 586
 587        time_elapsed = (time.time() - self._last_render) * 1000
 588        time_delay = round(1000 / self._state_frame_rate - time_elapsed)
 589
 590        js.setTimeout(self._internal_loop_callback, time_delay)
 591
 592    def _create_state_machine(self):
 593        return PyscriptSketchStateMachine()
 594
 595    def _get_canvas_region_arc_ellipse(self, x1: float, y1: float, x2: float,
 596        y2: float, mode_native: int, mode_str: str) -> CanvasRegionEllipse:
 597        if mode_native == sketchingpy.const.CENTER:
 598            center_x = x1
 599            center_y = y1
 600            radius_x = x2 / 2
 601            radius_y = y2 / 2
 602        elif mode_native == sketchingpy.const.RADIUS:
 603            center_x = x1
 604            center_y = y1
 605            radius_x = x2
 606            radius_y = y2
 607        elif mode_native == sketchingpy.const.CORNER:
 608            center_x = x1 + x2 / 2
 609            center_y = y1 + y2 / 2
 610            radius_x = x2 / 2
 611            radius_y = y2 / 2
 612        elif mode_native == sketchingpy.const.CORNERS:
 613            (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
 614            width = x2 - x1
 615            height = y2 - y1
 616            center_x = x1 + width / 2
 617            center_y = y1 + height / 2
 618            radius_x = width / 2
 619            radius_y = height / 2
 620        else:
 621            raise RuntimeError('Unknown mode: ' + mode_str)
 622
 623        return CanvasRegionEllipse(center_x, center_y, radius_x, radius_y)
 624
 625    def _get_canvas_region_rect_like(self, x1: float, y1: float, x2: float,
 626        y2: float, native_mode: int, mode_str: str) -> CanvasRegionRect:
 627        if native_mode == sketchingpy.const.CENTER:
 628            start_x = x1 - x2 / 2
 629            start_y = y1 - y2 / 2
 630            width = x2
 631            height = y2
 632        elif native_mode == sketchingpy.const.RADIUS:
 633            start_x = x1 - x2
 634            start_y = y1 - y2
 635            width = x2 * 2
 636            height = y2 * 2
 637        elif native_mode == sketchingpy.const.CORNER:
 638            start_x = x1
 639            start_y = y1
 640            width = 1 if x2 == 0 else x2
 641            height = 1 if y2 == 0 else y2
 642        elif native_mode == sketchingpy.const.CORNERS:
 643            (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
 644            start_x = x1
 645            start_y = y1
 646            width = x2 - x1
 647            height = y2 - y1
 648        else:
 649            raise RuntimeError('Unknown mode: ' + mode_str)
 650
 651        return CanvasRegionRect(start_x, start_y, width, height)
 652
 653    def _load_draw_params(self):
 654        current_machine = self._get_current_state_machine()
 655        self._context.fillStyle = current_machine.get_fill_native()
 656        self._context.strokeStyle = current_machine.get_stroke_native()
 657        self._context.lineWidth = current_machine.get_stroke_weight_native()
 658
 659    def _load_font_params(self):
 660        current_machine = self._get_current_state_machine()
 661
 662        self._context.font = current_machine.get_text_font_native()
 663
 664        text_align = current_machine.get_text_align_native()
 665        self._context.textAlign = text_align.get_horizontal_align()
 666        self._context.textBaseline = text_align.get_vertical_align()
 667
 668    def _draw_arc_rad(self, x1: float, y1: float, x2: float, y2: float, a1: float, a2: float,
 669        mode_native: int, mode_str: str):
 670        self._load_draw_params()
 671
 672        current_machine = self._get_current_state_machine()
 673        region = self._get_canvas_region_arc_ellipse(x1, y1, x2, y2, mode_native, mode_str)
 674
 675        self._context.beginPath()
 676
 677        self._context.ellipse(
 678            region.get_x(),
 679            region.get_y(),
 680            region.get_radius_x(),
 681            region.get_radius_y(),
 682            0,
 683            a1,
 684            a2
 685        )
 686
 687        if current_machine.get_fill_enabled():
 688            self._context.fill()
 689
 690        if current_machine.get_stroke_enabled():
 691            self._context.stroke()
 692
 693
 694class PyscriptSketchStateMachine(sketchingpy.state_struct.SketchStateMachine):
 695    """Implementation of SketchStateMachine for Pyscript types."""
 696
 697    def __init__(self):
 698        """Create a new state machine for Pyscript-based sketches."""
 699        super().__init__()
 700        self._text_align_native = self._transform_text_align(super().get_text_align_native())
 701
 702    def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
 703        super().set_text_align(text_align)
 704        self._text_align_native = self._transform_text_align(super().get_text_align_native())
 705
 706    def get_text_align_native(self):
 707        return self._text_align_native
 708
 709    def get_text_font_native(self):
 710        return sketchingpy.abstracted.get_font_name(self.get_text_font(), os.path.sep)
 711
 712    def _transform_text_align(self,
 713        text_align: sketchingpy.state_struct.TextAlign) -> sketchingpy.state_struct.TextAlign:
 714
 715        HORIZONTAL_ALIGNS = {
 716            sketchingpy.const.LEFT: 'left',
 717            sketchingpy.const.CENTER: 'center',
 718            sketchingpy.const.RIGHT: 'right'
 719        }
 720
 721        VERTICAL_ALIGNS = {
 722            sketchingpy.const.TOP: 'top',
 723            sketchingpy.const.CENTER: 'middle',
 724            sketchingpy.const.BASELINE: 'alphabetic',
 725            sketchingpy.const.BOTTOM: 'bottom'
 726        }
 727
 728        return sketchingpy.state_struct.TextAlign(
 729            HORIZONTAL_ALIGNS[text_align.get_horizontal_align()],
 730            VERTICAL_ALIGNS[text_align.get_vertical_align()]
 731        )
 732
 733
 734class WebImage(sketchingpy.abstracted.Image):
 735    """Strategy implementation for HTML images."""
 736
 737    def __init__(self, src: str):
 738        """Create a new image.
 739
 740        Args:
 741            src: Path to the image.
 742        """
 743        super().__init__(src)
 744
 745        preload_suffix = src.replace("./", "").replace("/", "").replace(".", "-").replace(" ", "-")
 746        preload_name = "preload-img-" + preload_suffix
 747        preloaded_image = js.document.getElementById(preload_name)
 748
 749        if preloaded_image:
 750            image = preloaded_image
 751        else:
 752            image = js.document.createElement("img")
 753            image.src = src
 754
 755        self._native = image
 756        self._width: typing.Optional[float] = None
 757        self._height: typing.Optional[float] = None
 758
 759    def get_width(self) -> float:
 760        if self._width is None:
 761            return self._native.width
 762        else:
 763            return self._width
 764
 765    def get_height(self) -> float:
 766        if self._height is None:
 767            return self._native.height
 768        else:
 769            return self._height
 770
 771    def resize(self, width: float, height: float):
 772        self._width = width
 773        self._height = height
 774
 775    def get_native(self):
 776        return self._native
 777
 778    def get_is_loaded(self):
 779        return self._native.width > 0
 780
 781
 782class PyscriptMouse(sketchingpy.control_struct.Mouse):
 783    """Strategy implementation for Pyscript-based mouse access."""
 784
 785    def __init__(self, element):
 786        """Create a new mouse strategy using HTML5.
 787
 788        Args:
 789            element: The element to which mouse event listeners should be added.
 790        """
 791        self._element = element
 792
 793        self._x = 0
 794        self._y = 0
 795
 796        self._buttons_pressed = set()
 797
 798        mouse_move_callback = pyodide.ffi.create_proxy(
 799            lambda event: self._report_mouse_move(event)
 800        )
 801        self._element.addEventListener(
 802            'mousemove',
 803            mouse_move_callback
 804        )
 805
 806        mouse_down_callback = pyodide.ffi.create_proxy(
 807            lambda event: self._report_mouse_down(event)
 808        )
 809        self._element.addEventListener(
 810            'mousedown',
 811            mouse_down_callback
 812        )
 813
 814        click_callback = pyodide.ffi.create_proxy(
 815            lambda event: self._report_click(event)
 816        )
 817        self._element.addEventListener(
 818            'click',
 819            click_callback
 820        )
 821
 822        context_menu_callback = pyodide.ffi.create_proxy(
 823            lambda event: self._report_context_menu(event)
 824        )
 825        self._element.addEventListener(
 826            'contextmenu',
 827            context_menu_callback
 828        )
 829
 830        self._press_callback = None
 831        self._release_callback = None
 832
 833    def get_pointer_x(self):
 834        return self._x
 835
 836    def get_pointer_y(self):
 837        return self._y
 838
 839    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
 840        return map(lambda x: sketchingpy.control_struct.Button(x), self._buttons_pressed)
 841
 842    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
 843        self._press_callback = callback
 844
 845    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
 846        self._release_callback = callback
 847
 848    def _report_mouse_move(self, event):
 849        bounding_box = self._element.getBoundingClientRect()
 850        self._x = event.clientX - bounding_box.left
 851        self._y = event.clientY - bounding_box.top
 852
 853    def _report_mouse_down(self, event):
 854        if event.button == 0:
 855            self._buttons_pressed.add(sketchingpy.const.MOUSE_LEFT_BUTTON)
 856            if self._press_callback is not None:
 857                button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
 858                self._press_callback(button)
 859        elif event.button == 2:
 860            self._buttons_pressed.add(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 861            if self._press_callback is not None:
 862                button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 863                self._press_callback(button)
 864
 865    def _report_click(self, event):
 866        self._buttons_pressed.remove(sketchingpy.const.MOUSE_LEFT_BUTTON)
 867
 868        if self._release_callback is not None:
 869            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
 870            self._release_callback(button)
 871
 872        event.preventDefault()
 873
 874    def _report_context_menu(self, event):
 875        self._buttons_pressed.remove(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 876
 877        if self._release_callback is not None:
 878            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 879            self._release_callback(button)
 880
 881        event.preventDefault()
 882
 883
 884class PyscriptKeyboard(sketchingpy.control_struct.Keyboard):
 885    """Strategy implementation for Pyscript-based keyboard access."""
 886
 887    def __init__(self, element):
 888        """Create a new mouse strategy using HTML5 and Pyscript.
 889
 890        Args:
 891            element: The element to which keyboard event listeners should be added.
 892        """
 893        super().__init__()
 894        self._element = element
 895        self._pressed = set()
 896        self._press_callback = None
 897        self._release_callback = None
 898
 899        keydown_callback = pyodide.ffi.create_proxy(
 900            lambda event: self._report_key_down(event)
 901        )
 902        self._element.addEventListener(
 903            'keydown',
 904            keydown_callback
 905        )
 906
 907        keyup_callback = pyodide.ffi.create_proxy(
 908            lambda event: self._report_key_up(event)
 909        )
 910        self._element.addEventListener(
 911            'keyup',
 912            keyup_callback
 913        )
 914
 915    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
 916        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)
 917
 918    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
 919        self._press_callback = callback
 920
 921    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
 922        self._release_callback = callback
 923
 924    def _report_key_down(self, event):
 925        key = self._map_key(event.key)
 926
 927        if key is None:
 928            return
 929
 930        self._pressed.add(key)
 931
 932        if self._press_callback is not None:
 933            button = sketchingpy.control_struct.Button(key)
 934            self._press_callback(button)
 935
 936        event.preventDefault()
 937
 938    def _report_key_up(self, event):
 939        key = self._map_key(event.key)
 940
 941        if key is None:
 942            return
 943
 944        self._pressed.remove(key)
 945
 946        if self._release_callback is not None:
 947            button = sketchingpy.control_struct.Button(key)
 948            self._release_callback(button)
 949
 950        event.preventDefault()
 951
 952    def _map_key(self, target: str) -> typing.Optional[str]:
 953        if target in KEY_MAP:
 954            return KEY_MAP[target.lower()]  # Required for browser compatibility
 955        else:
 956            return target.lower()
 957
 958
 959class WebDataLayer(sketchingpy.data_struct.DataLayer):
 960    """Data layer which interfaces with network and browser."""
 961
 962    def get_csv(self, path: str) -> sketchingpy.data_struct.Records:
 963        if os.path.exists(path):
 964            with open(path) as f:
 965                reader = csv.DictReader(f)
 966                return list(reader)
 967        else:
 968            string_io = pyodide.http.open_url(path)
 969            reader = csv.DictReader(string_io)
 970            return list(reader)
 971
 972    def write_csv(self, records: sketchingpy.data_struct.Records,
 973        columns: sketchingpy.data_struct.Columns, path: str):
 974        def build_record(target: typing.Dict) -> typing.Dict:
 975            return dict(map(lambda key: (key, target[key]), columns))
 976
 977        records_serialized = map(build_record, records)
 978
 979        target = io.StringIO()
 980
 981        writer = csv.DictWriter(target, fieldnames=columns)  # type: ignore
 982        writer.writeheader()
 983        writer.writerows(records_serialized)
 984
 985        self._download_text(target.getvalue(), path, 'text/csv')
 986
 987    def get_json(self, path: str):
 988        if os.path.exists(path):
 989            with open(path) as f:
 990                return json.load(f)
 991        else:
 992            string_io = pyodide.http.open_url(path)
 993            return json.loads(string_io.read())
 994
 995    def write_json(self, target, path: str):
 996        self._download_text(json.dumps(target), path, 'application/json')
 997
 998    def get_text(self, path: str):
 999        string_io = pyodide.http.open_url(path)
1000        return string_io.read()
1001
1002    def write_text(self, target, path: str):
1003        self._download_text(target, path, 'text/plain')
1004
1005    def _download_text(self, text: str, filename: str, mime: str):
1006        text_encoded = urllib.parse.quote(text)
1007
1008        link = js.document.createElement('a')
1009        link.download = filename
1010        link.href = 'data:%s;charset=utf-8,%s' % (mime, text_encoded)
1011
1012        link.click()
1013
1014
1015class WebDialogLayer(sketchingpy.dialog_struct.DialogLayer):
1016    """Dialog / simple UI layer for web apps."""
1017
1018    def __init__(self, sketch: Sketch2DWeb):
1019        """"Initialize tkinter but hide the root window."""
1020        self._sketch = sketch
1021
1022    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1023        pyscript.window.alert(message)
1024        if callback is not None:
1025            callback()
1026
1027    def show_prompt(self, message: str,
1028        callback: typing.Optional[typing.Callable[[str], None]] = None):
1029        response = pyscript.window.prompt(message)
1030        if callback is not None and response is not None:
1031            callback(response)
1032
1033    def get_file_save_location(self,
1034        callback: typing.Optional[typing.Callable[[str], None]] = None):
1035        self.show_prompt('Filename to save:', callback)
1036
1037    def get_file_load_location(self,
1038        callback: typing.Optional[typing.Callable[[str], None]] = None):
1039        self.show_prompt('Filename to load:', callback)
has_js = False
DEFAULT_FPS = 20
KEY_MAP = {'arrowleft': 'left', 'arrowup': 'up', 'arrowright': 'right', 'arrowdown': 'down', ' ': 'space', 'control': 'ctrl', 'alt': 'alt', 'shift': 'shift', 'tab': 'tab', 'home': 'home', 'end': 'end', 'enter': 'return', 'backspace': 'backspace', 'null': None}
OPTIONAL_SKETCH_CALLBACK = typing.Optional[typing.Callable[[sketchingpy.abstracted.Sketch], NoneType]]
class CanvasRegionEllipse:
 54class CanvasRegionEllipse:
 55    """Description of a region of a canvas expressed as an ellipse."""
 56
 57    def __init__(self, x: float, y: float, radius_x: float, radius_y: float):
 58        """Create a new elliptical region record.
 59
 60        Args:
 61            x: The center x coordinate of the region.
 62            y: The center y coordinate of the region.
 63            radius_x: The horizontal radius of the region.
 64            radius_y: The vertical radius of the region.
 65        """
 66        self._x = x
 67        self._y = y
 68        self._radius_x = radius_x
 69        self._radius_y = radius_y
 70
 71    def get_x(self) -> float:
 72        """Get the center horizontal coordinate of this region.
 73
 74        Returns:
 75            The center x coordinate of the region.
 76        """
 77        return self._x
 78
 79    def get_y(self) -> float:
 80        """Get the center vertical coordinate of this region.
 81
 82        Returns:
 83            The center y coordinate of the region.
 84        """
 85        return self._y
 86
 87    def get_radius_x(self) -> float:
 88        """Get the horizontal size of this region.
 89
 90        Returns:
 91            The horizontal radius of the region.
 92        """
 93        return self._radius_x
 94
 95    def get_radius_y(self) -> float:
 96        """Get the vertical size of this region.
 97
 98        Returns:
 99            The vertical radius of the region.
100        """
101        return self._radius_y

Description of a region of a canvas expressed as an ellipse.

CanvasRegionEllipse(x: float, y: float, radius_x: float, radius_y: float)
57    def __init__(self, x: float, y: float, radius_x: float, radius_y: float):
58        """Create a new elliptical region record.
59
60        Args:
61            x: The center x coordinate of the region.
62            y: The center y coordinate of the region.
63            radius_x: The horizontal radius of the region.
64            radius_y: The vertical radius of the region.
65        """
66        self._x = x
67        self._y = y
68        self._radius_x = radius_x
69        self._radius_y = radius_y

Create a new elliptical region record.

Arguments:
  • x: The center x coordinate of the region.
  • y: The center y coordinate of the region.
  • radius_x: The horizontal radius of the region.
  • radius_y: The vertical radius of the region.
def get_x(self) -> float:
71    def get_x(self) -> float:
72        """Get the center horizontal coordinate of this region.
73
74        Returns:
75            The center x coordinate of the region.
76        """
77        return self._x

Get the center horizontal coordinate of this region.

Returns:

The center x coordinate of the region.

def get_y(self) -> float:
79    def get_y(self) -> float:
80        """Get the center vertical coordinate of this region.
81
82        Returns:
83            The center y coordinate of the region.
84        """
85        return self._y

Get the center vertical coordinate of this region.

Returns:

The center y coordinate of the region.

def get_radius_x(self) -> float:
87    def get_radius_x(self) -> float:
88        """Get the horizontal size of this region.
89
90        Returns:
91            The horizontal radius of the region.
92        """
93        return self._radius_x

Get the horizontal size of this region.

Returns:

The horizontal radius of the region.

def get_radius_y(self) -> float:
 95    def get_radius_y(self) -> float:
 96        """Get the vertical size of this region.
 97
 98        Returns:
 99            The vertical radius of the region.
100        """
101        return self._radius_y

Get the vertical size of this region.

Returns:

The vertical radius of the region.

class CanvasRegionRect:
104class CanvasRegionRect:
105    """Description of a region of a canvas expressed as a rectangle."""
106
107    def __init__(self, x: float, y: float, width: float, height: float):
108        """Create a new rectangular region record.
109
110        Args:
111            x: The center x coordinate of the region.
112            y: The center y coordinate of the region.
113            radius_x: The horizontal size of the region.
114            radius_y: The vertical size of the region.
115        """
116        self._x = x
117        self._y = y
118        self._width = width
119        self._height = height
120
121    def get_x(self) -> float:
122        """Get the start horizontal coordinate of this region.
123
124        Returns:
125            The left x coordinate of the region.
126        """
127        return self._x
128
129    def get_y(self) -> float:
130        """Get the start vertical coordinate of this region.
131
132        Returns:
133            The top y coordinate of the region.
134        """
135        return self._y
136
137    def get_width(self) -> float:
138        """Get the horizontal size of this region.
139
140        Returns:
141            The horizontal width of the region.
142        """
143        return self._width
144
145    def get_height(self) -> float:
146        """Get the vertical size of this region.
147
148        Returns:
149            The vertical height of the region.
150        """
151        return self._height

Description of a region of a canvas expressed as a rectangle.

CanvasRegionRect(x: float, y: float, width: float, height: float)
107    def __init__(self, x: float, y: float, width: float, height: float):
108        """Create a new rectangular region record.
109
110        Args:
111            x: The center x coordinate of the region.
112            y: The center y coordinate of the region.
113            radius_x: The horizontal size of the region.
114            radius_y: The vertical size of the region.
115        """
116        self._x = x
117        self._y = y
118        self._width = width
119        self._height = height

Create a new rectangular region record.

Arguments:
  • x: The center x coordinate of the region.
  • y: The center y coordinate of the region.
  • radius_x: The horizontal size of the region.
  • radius_y: The vertical size of the region.
def get_x(self) -> float:
121    def get_x(self) -> float:
122        """Get the start horizontal coordinate of this region.
123
124        Returns:
125            The left x coordinate of the region.
126        """
127        return self._x

Get the start horizontal coordinate of this region.

Returns:

The left x coordinate of the region.

def get_y(self) -> float:
129    def get_y(self) -> float:
130        """Get the start vertical coordinate of this region.
131
132        Returns:
133            The top y coordinate of the region.
134        """
135        return self._y

Get the start vertical coordinate of this region.

Returns:

The top y coordinate of the region.

def get_width(self) -> float:
137    def get_width(self) -> float:
138        """Get the horizontal size of this region.
139
140        Returns:
141            The horizontal width of the region.
142        """
143        return self._width

Get the horizontal size of this region.

Returns:

The horizontal width of the region.

def get_height(self) -> float:
145    def get_height(self) -> float:
146        """Get the vertical size of this region.
147
148        Returns:
149            The vertical height of the region.
150        """
151        return self._height

Get the vertical size of this region.

Returns:

The vertical height of the region.

class WebBuffer:
154class WebBuffer:
155    """Structure for an offscreen buffer."""
156
157    def __init__(self, canvas, context, width: int, height: int):
158        """Create a new offscreen record.
159
160        Args:
161            canvas: The offscreen canvas element.
162            context: The 2D drawing context.
163            width: The horizontal size of the buffer in pixels.
164            height: The vertical size of the buffer in pixels.
165        """
166        self._canvas = canvas
167        self._context = context
168        self._width = width
169        self._height = height
170
171    def get_element(self):
172        """Get the offscreen canvas object.
173
174        Returns:
175            The offscreen canvas element.
176        """
177        return self._canvas
178
179    def get_context(self):
180        """Get the drawing context compatiable with the main canvas.
181
182        Returns:
183            The 2D drawing context.
184        """
185        return self._context
186
187    def get_width(self) -> int:
188        """Get the horizontal size of the buffer in pixels.
189
190        Returns:
191            Width of the offscreen canvas.
192        """
193        return self._width
194
195    def get_height(self) -> int:
196        """Get the vertical size of the buffer in pixels.
197
198        Returns:
199            Height of the offscreen canvas.
200        """
201        return self._height

Structure for an offscreen buffer.

WebBuffer(canvas, context, width: int, height: int)
157    def __init__(self, canvas, context, width: int, height: int):
158        """Create a new offscreen record.
159
160        Args:
161            canvas: The offscreen canvas element.
162            context: The 2D drawing context.
163            width: The horizontal size of the buffer in pixels.
164            height: The vertical size of the buffer in pixels.
165        """
166        self._canvas = canvas
167        self._context = context
168        self._width = width
169        self._height = height

Create a new offscreen record.

Arguments:
  • canvas: The offscreen canvas element.
  • context: The 2D drawing context.
  • width: The horizontal size of the buffer in pixels.
  • height: The vertical size of the buffer in pixels.
def get_element(self):
171    def get_element(self):
172        """Get the offscreen canvas object.
173
174        Returns:
175            The offscreen canvas element.
176        """
177        return self._canvas

Get the offscreen canvas object.

Returns:

The offscreen canvas element.

def get_context(self):
179    def get_context(self):
180        """Get the drawing context compatiable with the main canvas.
181
182        Returns:
183            The 2D drawing context.
184        """
185        return self._context

Get the drawing context compatiable with the main canvas.

Returns:

The 2D drawing context.

def get_width(self) -> int:
187    def get_width(self) -> int:
188        """Get the horizontal size of the buffer in pixels.
189
190        Returns:
191            Width of the offscreen canvas.
192        """
193        return self._width

Get the horizontal size of the buffer in pixels.

Returns:

Width of the offscreen canvas.

def get_height(self) -> int:
195    def get_height(self) -> int:
196        """Get the vertical size of the buffer in pixels.
197
198        Returns:
199            Height of the offscreen canvas.
200        """
201        return self._height

Get the vertical size of the buffer in pixels.

Returns:

Height of the offscreen canvas.

class Sketch2DWeb(sketchingpy.abstracted.Sketch):
204class Sketch2DWeb(sketchingpy.abstracted.Sketch):
205    """Sketch renderer for web / HTML5."""
206
207    def __init__(self, width: float, height: float, element_id: str = 'sketch-canvas',
208        loading_id: typing.Optional[str] = 'sketch-load-message'):
209        """Create a new HTML5 Canvas-based sketch.
210
211        Args:
212            width: The horizontal size of the sketch in pixels. Will update the HTML5 element.
213            height: The vertical size of the sketch in pixels. Will update the HTML5 element.
214            element_id: The ID (HTML) of the canvas into which this sketch should be drawn.
215            loading_id: The ID (HTML) of the loading message to hide upon showing the sketch.
216        """
217        super().__init__()
218
219        if not has_js:
220            raise RuntimeError('Cannot access JS / pyodide.')
221
222        # Save elements required for running the canvas
223        self._element_id = element_id
224        self._element = js.document.getElementById(element_id)
225        self._element.width = width
226        self._element.height = height
227        self._element.style.display = 'none'
228        self._context = self._element.getContext('2d')
229        self._last_render = None
230
231        self._loading_id = loading_id
232        self._loading_element = js.document.getElementById(loading_id)
233
234        # Internal only elements
235        self._internal_loop_callback = None
236        self._internal_mouse_x = 0
237        self._internal_mouse_y = 0
238        self._internal_pre_show_actions: typing.List[typing.Callable] = []
239        self._added_fonts: typing.Set[str] = set()
240
241        # Buffers
242        self._buffers: typing.Dict[str, WebBuffer] = {}
243        self._base_context = self._context
244        self._base_element = self._element
245        self._width = self._element.width
246        self._height = self._element.height
247
248        # User configurable state
249        self._state_frame_rate = DEFAULT_FPS
250        self._stopped = False
251
252        # Callback
253        self._callback_step: OPTIONAL_SKETCH_CALLBACK = None
254        self._callback_quit: OPTIONAL_SKETCH_CALLBACK = None
255
256        # Control
257        self._keyboard = PyscriptKeyboard(self._element)
258        self._mouse = PyscriptMouse(self._element)
259
260    ##########
261    # Buffer #
262    ##########
263
264    def create_buffer(self, name: str, width: int, height: int,
265        background: typing.Optional[str] = None):
266        canvas = js.window.OffscreenCanvas.new(width, height)
267        context = canvas.getContext('2d')
268        self._buffers[name] = WebBuffer(canvas, context, width, height)
269        if background is not None:
270            context.clearRect(0, 0, self._width, self._height)
271            context.fillStyle = background
272            context.fillRect(0, 0, self._width, self._height)
273
274    def enter_buffer(self, name: str):
275        web_buffer = self._buffers[name]
276        self._context = web_buffer.get_context()
277        self._element = web_buffer.get_element()
278        self._width = web_buffer.get_width()
279        self._height = web_buffer.get_height()
280
281    def exit_buffer(self):
282        self._context = self._base_context
283        self._element = self._base_element
284        self._width = self._base_element.width
285        self._height = self._base_element.height
286
287    def draw_buffer(self, x: float, y: float, name: str):
288        web_buffer = self._buffers[name]
289        self._context.drawImage(web_buffer.get_element(), x, y)
290
291    ############
292    # Controls #
293    ############
294
295    def get_keyboard(self) -> typing.Optional[sketchingpy.control_struct.Keyboard]:
296        return self._keyboard
297
298    def get_mouse(self) -> typing.Optional[sketchingpy.control_struct.Mouse]:
299        return self._mouse
300
301    ########
302    # Data #
303    ########
304
305    def get_data_layer(self) -> typing.Optional[sketchingpy.data_struct.DataLayer]:
306        return WebDataLayer()
307
308    ###########
309    # Dialogs #
310    ###########
311
312    def get_dialog_layer(self) -> typing.Optional[sketchingpy.dialog_struct.DialogLayer]:
313        return WebDialogLayer(self)
314
315    ###########
316    # Drawing #
317    ###########
318
319    def clear(self, color: str):
320        self._context.clearRect(0, 0, self._width, self._height)
321        self._context.fillStyle = color
322        self._context.fillRect(0, 0, self._width, self._height)
323
324    def draw_arc(self, x1: float, y1: float, x2: float, y2: float, a1: float, a2: float):
325        self._load_draw_params()
326
327        a1_rad = self._convert_to_radians(a1) - js.Math.PI / 2
328        a2_rad = self._convert_to_radians(a2) - js.Math.PI / 2
329
330        current_machine = self._get_current_state_machine()
331        mode_native = current_machine.get_arc_mode_native()
332        mode_str = current_machine.get_arc_mode()
333
334        self._draw_arc_rad(x1, y1, x2, y2, a1_rad, a2_rad, mode_native, mode_str)
335
336    def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
337        current_machine = self._get_current_state_machine()
338        mode_native = current_machine.get_ellipse_mode_native()
339        mode_str = current_machine.get_ellipse_mode()
340
341        self._draw_arc_rad(x1, y1, x2, y2, 0, 2 * js.Math.PI, mode_native, mode_str)
342
343    def draw_line(self, x1: float, y1: float, x2: float, y2: float):
344        current_machine = self._get_current_state_machine()
345        if not current_machine.get_stroke_enabled():
346            return
347
348        self._load_draw_params()
349
350        self._context.beginPath()
351        self._context.moveTo(x1, y1)
352        self._context.lineTo(x2, y2)
353        self._context.stroke()
354
355    def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
356        self._load_draw_params()
357
358        current_machine = self._get_current_state_machine()
359        native_mode = current_machine.get_rect_mode_native()
360        mode_str = current_machine.get_rect_mode_native()
361
362        region = self._get_canvas_region_rect_like(x1, y1, x2, y2, native_mode, mode_str)
363
364        self._context.beginPath()
365        self._context.rect(region.get_x(), region.get_y(), region.get_width(), region.get_height())
366
367        if current_machine.get_fill_enabled():
368            self._context.fill()
369
370        if current_machine.get_stroke_enabled():
371            self._context.stroke()
372
373    def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
374        current_machine = self._get_current_state_machine()
375
376        self._load_draw_params()
377
378        self._context.beginPath()
379        self._context.moveTo(shape.get_start_x(), shape.get_start_y())
380
381        for segment in shape.get_segments():
382            strategy = segment.get_strategy()
383            if strategy == 'straight':
384                self._context.lineTo(segment.get_destination_x(), segment.get_destination_y())
385            elif strategy == 'bezier':
386                self._context.bezierCurveTo(
387                    segment.get_control_x1(),
388                    segment.get_control_y1(),
389                    segment.get_control_x2(),
390                    segment.get_control_y2(),
391                    segment.get_destination_x(),
392                    segment.get_destination_y()
393                )
394            else:
395                raise RuntimeError('Unsupported segment type: ' + strategy)
396
397        if shape.get_is_closed():
398            self._context.closePath()
399
400        if current_machine.get_fill_enabled():
401            self._context.fill()
402
403        if current_machine.get_stroke_enabled():
404            self._context.stroke()
405
406    def draw_text(self, x: float, y: float, content: str):
407        content = str(content)
408        current_machine = self._get_current_state_machine()
409
410        self._load_draw_params()
411        self._load_font_params()
412
413        if current_machine.get_fill_enabled():
414            self._context.fillText(content, x, y)
415
416        if current_machine.get_stroke_enabled():
417            self._context.strokeText(content, x, y)
418
419    ##########
420    # Events #
421    ##########
422
423    def on_step(self, callback: sketchingpy.abstracted.StepCallback):
424        self._callback_step = callback
425
426    def on_quit(self, callback: sketchingpy.abstracted.QuitCallback):
427        self._callback_quit = callback
428
429    #########
430    # Image #
431    #########
432
433    def get_image(self, src: str) -> sketchingpy.abstracted.Image:
434        return WebImage(src)
435
436    def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
437        if not image.get_is_loaded():
438            return
439
440        self._load_draw_params()
441
442        current_machine = self._get_current_state_machine()
443        native_mode = current_machine.get_image_mode_native()
444        mode_str = current_machine.get_image_mode_native()
445
446        width = image.get_width()
447        height = image.get_height()
448
449        region = self._get_canvas_region_rect_like(x, y, width, height, native_mode, mode_str)
450
451        self._context.drawImage(
452            image.get_native(),
453            region.get_x(),
454            region.get_y(),
455            region.get_width(),
456            region.get_height()
457        )
458
459    def save_image(self, path: str):
460        if not path.endswith('.png'):
461            raise RuntimeError('Web export only supported to PNG.')
462
463        link = js.document.createElement('a')
464        link.download = path
465        link.href = self._element.toDataURL('image/png')
466        link.click()
467
468    #########
469    # State #
470    #########
471
472    def set_text_font(self, identifier: str, size: float):
473        super().set_text_font(identifier, size)
474
475        is_otf = identifier.endswith('.otf')
476        is_ttf = identifier.endswith('.ttf')
477        is_file = is_otf or is_ttf
478
479        if not is_file:
480            return
481
482        current_machine = self._get_current_state_machine()
483        font = current_machine.get_text_font()
484        font_name = sketchingpy.abstracted.get_font_name(font, '/')
485
486        if font_name in self._added_fonts:
487            return
488
489        naked_font_name_components = font_name.split(' ')[1:]
490        naked_font_name = ' '.join(naked_font_name_components)
491
492        new_font = pyscript.window.FontFace.new(naked_font_name, 'url(%s)' % identifier)
493        new_font.load()
494        pyscript.document.fonts.add(new_font)
495        self._added_fonts.add(font_name)
496
497    def push_transform(self):
498        self._context.save()
499
500    def pop_transform(self):
501        self._context.restore()
502
503    ##########
504    # System #
505    ##########
506
507    def get_native(self):
508        return self._element
509
510    def set_fps(self, rate: int):
511        self._state_frame_rate = rate
512
513    def set_title(self, title: str):
514        js.document.title = title
515
516    def quit(self):
517        self._stopped = True
518
519    def show(self, ax=None):
520        self._show_internal(ax=ax, quit_immediately=False)
521
522    def show_and_quit(self, ax=None):
523        self._show_internal(ax=ax, quit_immediately=True)
524
525    def print(self, message: str):
526        console_id = self._element_id + '-console'
527        target_root = js.document.getElementById(console_id)
528
529        if target_root is None:
530            print(message)
531            return
532
533        new_li = pyscript.document.createElement('li')
534        new_content = pyscript.document.createTextNode(message)
535        new_li.appendChild(new_content)
536
537        target_root.appendChild(new_li)
538
539    #############
540    # Transform #
541    #############
542
543    def translate(self, x: float, y: float):
544        self._context.translate(x, y)
545
546    def rotate(self, angle: float):
547        angle_rad = self._convert_to_radians(angle)
548        self._context.rotate(angle_rad)
549
550    def scale(self, scale: float):
551        self._context.scale(scale, scale)
552
553    ###########
554    # Support #
555    ###########
556
557    def _show_internal(self, ax=None, quit_immediately=False):
558        self._loading_element.style.display = 'none'
559        self._element.style.display = 'inline-block'
560        self._version = str(round(time.time()))
561        self._element.setAttribute('version', self._version)
562
563        self._snapshot_time()
564
565        for action in self._internal_pre_show_actions:
566            action()
567
568        if not quit_immediately:
569            self._stopped = False
570
571            self._last_render = time.time()
572            self._internal_loop_callback = pyodide.ffi.create_proxy(lambda: self._inner_loop())
573
574            self._inner_loop()
575
576    def _inner_loop(self):
577        if self._element.getAttribute('version') != self._version:
578            self._stopped = True
579
580        if self._stopped:
581            if self._callback_quit is not None:
582                self._callback_quit(self)
583            return
584
585        if self._callback_step is not None:
586            self._callback_step(self)
587
588        time_elapsed = (time.time() - self._last_render) * 1000
589        time_delay = round(1000 / self._state_frame_rate - time_elapsed)
590
591        js.setTimeout(self._internal_loop_callback, time_delay)
592
593    def _create_state_machine(self):
594        return PyscriptSketchStateMachine()
595
596    def _get_canvas_region_arc_ellipse(self, x1: float, y1: float, x2: float,
597        y2: float, mode_native: int, mode_str: str) -> CanvasRegionEllipse:
598        if mode_native == sketchingpy.const.CENTER:
599            center_x = x1
600            center_y = y1
601            radius_x = x2 / 2
602            radius_y = y2 / 2
603        elif mode_native == sketchingpy.const.RADIUS:
604            center_x = x1
605            center_y = y1
606            radius_x = x2
607            radius_y = y2
608        elif mode_native == sketchingpy.const.CORNER:
609            center_x = x1 + x2 / 2
610            center_y = y1 + y2 / 2
611            radius_x = x2 / 2
612            radius_y = y2 / 2
613        elif mode_native == sketchingpy.const.CORNERS:
614            (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
615            width = x2 - x1
616            height = y2 - y1
617            center_x = x1 + width / 2
618            center_y = y1 + height / 2
619            radius_x = width / 2
620            radius_y = height / 2
621        else:
622            raise RuntimeError('Unknown mode: ' + mode_str)
623
624        return CanvasRegionEllipse(center_x, center_y, radius_x, radius_y)
625
626    def _get_canvas_region_rect_like(self, x1: float, y1: float, x2: float,
627        y2: float, native_mode: int, mode_str: str) -> CanvasRegionRect:
628        if native_mode == sketchingpy.const.CENTER:
629            start_x = x1 - x2 / 2
630            start_y = y1 - y2 / 2
631            width = x2
632            height = y2
633        elif native_mode == sketchingpy.const.RADIUS:
634            start_x = x1 - x2
635            start_y = y1 - y2
636            width = x2 * 2
637            height = y2 * 2
638        elif native_mode == sketchingpy.const.CORNER:
639            start_x = x1
640            start_y = y1
641            width = 1 if x2 == 0 else x2
642            height = 1 if y2 == 0 else y2
643        elif native_mode == sketchingpy.const.CORNERS:
644            (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
645            start_x = x1
646            start_y = y1
647            width = x2 - x1
648            height = y2 - y1
649        else:
650            raise RuntimeError('Unknown mode: ' + mode_str)
651
652        return CanvasRegionRect(start_x, start_y, width, height)
653
654    def _load_draw_params(self):
655        current_machine = self._get_current_state_machine()
656        self._context.fillStyle = current_machine.get_fill_native()
657        self._context.strokeStyle = current_machine.get_stroke_native()
658        self._context.lineWidth = current_machine.get_stroke_weight_native()
659
660    def _load_font_params(self):
661        current_machine = self._get_current_state_machine()
662
663        self._context.font = current_machine.get_text_font_native()
664
665        text_align = current_machine.get_text_align_native()
666        self._context.textAlign = text_align.get_horizontal_align()
667        self._context.textBaseline = text_align.get_vertical_align()
668
669    def _draw_arc_rad(self, x1: float, y1: float, x2: float, y2: float, a1: float, a2: float,
670        mode_native: int, mode_str: str):
671        self._load_draw_params()
672
673        current_machine = self._get_current_state_machine()
674        region = self._get_canvas_region_arc_ellipse(x1, y1, x2, y2, mode_native, mode_str)
675
676        self._context.beginPath()
677
678        self._context.ellipse(
679            region.get_x(),
680            region.get_y(),
681            region.get_radius_x(),
682            region.get_radius_y(),
683            0,
684            a1,
685            a2
686        )
687
688        if current_machine.get_fill_enabled():
689            self._context.fill()
690
691        if current_machine.get_stroke_enabled():
692            self._context.stroke()

Sketch renderer for web / HTML5.

Sketch2DWeb( width: float, height: float, element_id: str = 'sketch-canvas', loading_id: Optional[str] = 'sketch-load-message')
207    def __init__(self, width: float, height: float, element_id: str = 'sketch-canvas',
208        loading_id: typing.Optional[str] = 'sketch-load-message'):
209        """Create a new HTML5 Canvas-based sketch.
210
211        Args:
212            width: The horizontal size of the sketch in pixels. Will update the HTML5 element.
213            height: The vertical size of the sketch in pixels. Will update the HTML5 element.
214            element_id: The ID (HTML) of the canvas into which this sketch should be drawn.
215            loading_id: The ID (HTML) of the loading message to hide upon showing the sketch.
216        """
217        super().__init__()
218
219        if not has_js:
220            raise RuntimeError('Cannot access JS / pyodide.')
221
222        # Save elements required for running the canvas
223        self._element_id = element_id
224        self._element = js.document.getElementById(element_id)
225        self._element.width = width
226        self._element.height = height
227        self._element.style.display = 'none'
228        self._context = self._element.getContext('2d')
229        self._last_render = None
230
231        self._loading_id = loading_id
232        self._loading_element = js.document.getElementById(loading_id)
233
234        # Internal only elements
235        self._internal_loop_callback = None
236        self._internal_mouse_x = 0
237        self._internal_mouse_y = 0
238        self._internal_pre_show_actions: typing.List[typing.Callable] = []
239        self._added_fonts: typing.Set[str] = set()
240
241        # Buffers
242        self._buffers: typing.Dict[str, WebBuffer] = {}
243        self._base_context = self._context
244        self._base_element = self._element
245        self._width = self._element.width
246        self._height = self._element.height
247
248        # User configurable state
249        self._state_frame_rate = DEFAULT_FPS
250        self._stopped = False
251
252        # Callback
253        self._callback_step: OPTIONAL_SKETCH_CALLBACK = None
254        self._callback_quit: OPTIONAL_SKETCH_CALLBACK = None
255
256        # Control
257        self._keyboard = PyscriptKeyboard(self._element)
258        self._mouse = PyscriptMouse(self._element)

Create a new HTML5 Canvas-based sketch.

Arguments:
  • width: The horizontal size of the sketch in pixels. Will update the HTML5 element.
  • height: The vertical size of the sketch in pixels. Will update the HTML5 element.
  • element_id: The ID (HTML) of the canvas into which this sketch should be drawn.
  • loading_id: The ID (HTML) of the loading message to hide upon showing the sketch.
def create_buffer( self, name: str, width: int, height: int, background: Optional[str] = None):
264    def create_buffer(self, name: str, width: int, height: int,
265        background: typing.Optional[str] = None):
266        canvas = js.window.OffscreenCanvas.new(width, height)
267        context = canvas.getContext('2d')
268        self._buffers[name] = WebBuffer(canvas, context, width, height)
269        if background is not None:
270            context.clearRect(0, 0, self._width, self._height)
271            context.fillStyle = background
272            context.fillRect(0, 0, self._width, self._height)

Create a new named in-memory (or equivalent) buffer.

Arguments:
  • name: The name of the buffer. If a prior buffer of this name exists, it will be replaced.
  • width: The width of the buffer in pixels. In some renderers, the buffer will clip. In others, out of buffer values may be drawn.
  • height: The height of the buffer in pixels. In some renderers, the buffer will clip. In others, out of buffer values may be drawn.
  • background: The background to use for this buffer or None if transparent. Defaults to None.
def enter_buffer(self, name: str):
274    def enter_buffer(self, name: str):
275        web_buffer = self._buffers[name]
276        self._context = web_buffer.get_context()
277        self._element = web_buffer.get_element()
278        self._width = web_buffer.get_width()
279        self._height = web_buffer.get_height()

Switch rendering context to a buffer, exiting current buffer if active.

Arguments:
  • name: The name of the buffer to which context should switch.
def exit_buffer(self):
281    def exit_buffer(self):
282        self._context = self._base_context
283        self._element = self._base_element
284        self._width = self._base_element.width
285        self._height = self._base_element.height

Exit the current offscreen buffer.

Exit the current offscreen buffer, returning to the actual sketch. This will act as a noop if not currently in a buffer.

def draw_buffer(self, x: float, y: float, name: str):
287    def draw_buffer(self, x: float, y: float, name: str):
288        web_buffer = self._buffers[name]
289        self._context.drawImage(web_buffer.get_element(), x, y)

Draw an offscreen buffer to the current buffer or sketch.

Arguments:
  • x: The horizontal position in pixels at which the left should be drawn.
  • y: The vertical position in pixels at which the top should be drawn.
  • name: The name of the buffer to draw.
def get_keyboard(self) -> Optional[sketchingpy.control_struct.Keyboard]:
295    def get_keyboard(self) -> typing.Optional[sketchingpy.control_struct.Keyboard]:
296        return self._keyboard

Get access to the keyboard.

Get access to the keyboard currently registered with the operating system for the sketch. Different sketches running at the same time may have different keyboards depending on focus or OS configuration.

Returns:

Current keyboard or None if not found / supported.

def get_mouse(self) -> Optional[sketchingpy.control_struct.Mouse]:
298    def get_mouse(self) -> typing.Optional[sketchingpy.control_struct.Mouse]:
299        return self._mouse

Get access to the mouse.

Get access to the mouse currently registered with the operating system for the sketch. Different sketches running at the same time may have different mouse objects depending on focus or OS configuration. Note that the mouse may also be emulated if the device uses a touch screen.

Returns:

Current mouse or None if not found / supported.

def get_data_layer(self) -> Optional[sketchingpy.data_struct.DataLayer]:
305    def get_data_layer(self) -> typing.Optional[sketchingpy.data_struct.DataLayer]:
306        return WebDataLayer()

Get access to reading and writing data.

Open access to the file system, network, or browser to read or write data.

Returns:

Facade for data access or None if not supported or insufficient permissions.

def get_dialog_layer(self) -> Optional[sketchingpy.dialog_struct.DialogLayer]:
312    def get_dialog_layer(self) -> typing.Optional[sketchingpy.dialog_struct.DialogLayer]:
313        return WebDialogLayer(self)

Get access to rendering and using simple dialogs.

Open access to a simple dialog prefabricated UI system to show alerts, prompts, and other dialog boxes.

Returns:

Facade for rendering dialogs or None if not supported or insufficient permissions.

def clear(self, color: str):
319    def clear(self, color: str):
320        self._context.clearRect(0, 0, self._width, self._height)
321        self._context.fillStyle = color
322        self._context.fillRect(0, 0, self._width, self._height)

Clear the sketch to a color.

Peform the equivalent of drawing a rectangle the size of the sketch without stroke and with the given fill color.

Arguments:
  • color: The color to use in clearing.
def draw_arc( self, x1: float, y1: float, x2: float, y2: float, a1: float, a2: float):
324    def draw_arc(self, x1: float, y1: float, x2: float, y2: float, a1: float, a2: float):
325        self._load_draw_params()
326
327        a1_rad = self._convert_to_radians(a1) - js.Math.PI / 2
328        a2_rad = self._convert_to_radians(a2) - js.Math.PI / 2
329
330        current_machine = self._get_current_state_machine()
331        mode_native = current_machine.get_arc_mode_native()
332        mode_str = current_machine.get_arc_mode()
333
334        self._draw_arc_rad(x1, y1, x2, y2, a1_rad, a2_rad, mode_native, mode_str)

Draw a partial ellipse using starting and ending angles.

Using starting and ending angles, draw a partial ellipse which is either drawn outside line only (stroke) and / or filled from the center of that ellipse.

Arguments:
  • x1: The x location at which to draw the arc.
  • y1: The y location at which to draw the arc.
  • x2: Horizontal size.
  • y2: Vertical size.
  • a1: Starting angle.
  • a2: Ending angle.
def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
336    def draw_ellipse(self, x1: float, y1: float, x2: float, y2: float):
337        current_machine = self._get_current_state_machine()
338        mode_native = current_machine.get_ellipse_mode_native()
339        mode_str = current_machine.get_ellipse_mode()
340
341        self._draw_arc_rad(x1, y1, x2, y2, 0, 2 * js.Math.PI, mode_native, mode_str)

Draw a circle or ellipse.

Draw an ellipse or, in the case of equal width and height, a circle.

Arguments:
  • x1: The x location at which to draw the ellipse.
  • y1: The y location at which to draw the ellipse.
  • x2: Horizontal size.
  • y2: Vertical size.
def draw_line(self, x1: float, y1: float, x2: float, y2: float):
343    def draw_line(self, x1: float, y1: float, x2: float, y2: float):
344        current_machine = self._get_current_state_machine()
345        if not current_machine.get_stroke_enabled():
346            return
347
348        self._load_draw_params()
349
350        self._context.beginPath()
351        self._context.moveTo(x1, y1)
352        self._context.lineTo(x2, y2)
353        self._context.stroke()

Draw a simple line.

Draw a line between two points.

Arguments:
  • x1: The x coordinate from which the line should be drawn.
  • y1: The y coordinate from which the line should be drawn.
  • x2: The x coordinate to which the line should be drawn.
  • y2: The y coordinate to which the line should be drawn.
def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
355    def draw_rect(self, x1: float, y1: float, x2: float, y2: float):
356        self._load_draw_params()
357
358        current_machine = self._get_current_state_machine()
359        native_mode = current_machine.get_rect_mode_native()
360        mode_str = current_machine.get_rect_mode_native()
361
362        region = self._get_canvas_region_rect_like(x1, y1, x2, y2, native_mode, mode_str)
363
364        self._context.beginPath()
365        self._context.rect(region.get_x(), region.get_y(), region.get_width(), region.get_height())
366
367        if current_machine.get_fill_enabled():
368            self._context.fill()
369
370        if current_machine.get_stroke_enabled():
371            self._context.stroke()

Draw a rectangle.

Draw a rectangle or, if width and height are the same, a square.

Arguments:
  • x1: The x location at which to draw the rectangle.
  • y1: The y location at which to draw the rectangle.
  • x2: Horizontal size.
  • y2: Vertical size.
def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
373    def draw_shape(self, shape: sketchingpy.shape_struct.Shape):
374        current_machine = self._get_current_state_machine()
375
376        self._load_draw_params()
377
378        self._context.beginPath()
379        self._context.moveTo(shape.get_start_x(), shape.get_start_y())
380
381        for segment in shape.get_segments():
382            strategy = segment.get_strategy()
383            if strategy == 'straight':
384                self._context.lineTo(segment.get_destination_x(), segment.get_destination_y())
385            elif strategy == 'bezier':
386                self._context.bezierCurveTo(
387                    segment.get_control_x1(),
388                    segment.get_control_y1(),
389                    segment.get_control_x2(),
390                    segment.get_control_y2(),
391                    segment.get_destination_x(),
392                    segment.get_destination_y()
393                )
394            else:
395                raise RuntimeError('Unsupported segment type: ' + strategy)
396
397        if shape.get_is_closed():
398            self._context.closePath()
399
400        if current_machine.get_fill_enabled():
401            self._context.fill()
402
403        if current_machine.get_stroke_enabled():
404            self._context.stroke()

Draw a shape.

Draw a shape which consists of multiple line or curve segments and which can be either open (stroke only) or closed (can be filled).

Arguments:
  • shape: The shape to draw.
def draw_text(self, x: float, y: float, content: str):
406    def draw_text(self, x: float, y: float, content: str):
407        content = str(content)
408        current_machine = self._get_current_state_machine()
409
410        self._load_draw_params()
411        self._load_font_params()
412
413        if current_machine.get_fill_enabled():
414            self._context.fillText(content, x, y)
415
416        if current_machine.get_stroke_enabled():
417            self._context.strokeText(content, x, y)

Draw text using the current font.

Draw text using the current font and alignment.

Arguments:
  • x: The x coordinate at which to draw the text.
  • y: The y coordinate at which to draw the text.
  • text: The string to draw.
def on_step(self, callback: Callable[[ForwardRef('Sketch')], NoneType]):
423    def on_step(self, callback: sketchingpy.abstracted.StepCallback):
424        self._callback_step = callback

Callback for when the sketch ends execution.

Register a callback for when the sketch redraws. This function should expect a single parameter which is the sketch redrawing.

Arguments:
  • callback: The function to invoke when the sketch stops execution.
def on_quit(self, callback: Callable[[ForwardRef('Sketch')], NoneType]):
426    def on_quit(self, callback: sketchingpy.abstracted.QuitCallback):
427        self._callback_quit = callback

Callback for when the sketch ends execution.

Register a callback for when the sketch terminates.

Arguments:
  • callback: The function to invoke when the sketch stops execution.
def get_image(self, src: str) -> sketchingpy.abstracted.Image:
433    def get_image(self, src: str) -> sketchingpy.abstracted.Image:
434        return WebImage(src)

Load an image file.

Load an image from the local file system or URL.

Arguments:
  • src: The location from which the file should be read.
def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
436    def draw_image(self, x: float, y: float, image: sketchingpy.abstracted.Image):
437        if not image.get_is_loaded():
438            return
439
440        self._load_draw_params()
441
442        current_machine = self._get_current_state_machine()
443        native_mode = current_machine.get_image_mode_native()
444        mode_str = current_machine.get_image_mode_native()
445
446        width = image.get_width()
447        height = image.get_height()
448
449        region = self._get_canvas_region_rect_like(x, y, width, height, native_mode, mode_str)
450
451        self._context.drawImage(
452            image.get_native(),
453            region.get_x(),
454            region.get_y(),
455            region.get_width(),
456            region.get_height()
457        )

Draw an image at a location.

Draw a previously loaded image at a specific coordinate using its current size.

Arguments:
  • x: Horizontal coordinate at which to draw the image.
  • y: Vertical coordinate at which to draw the image.
  • image: The image to draw.
def save_image(self, path: str):
459    def save_image(self, path: str):
460        if not path.endswith('.png'):
461            raise RuntimeError('Web export only supported to PNG.')
462
463        link = js.document.createElement('a')
464        link.download = path
465        link.href = self._element.toDataURL('image/png')
466        link.click()

Save an image file.

Save the sketch as an image file, either directly to the file system or as a download.

Arguments:
  • path: The location at which the file should be written.
def set_text_font(self, identifier: str, size: float):
472    def set_text_font(self, identifier: str, size: float):
473        super().set_text_font(identifier, size)
474
475        is_otf = identifier.endswith('.otf')
476        is_ttf = identifier.endswith('.ttf')
477        is_file = is_otf or is_ttf
478
479        if not is_file:
480            return
481
482        current_machine = self._get_current_state_machine()
483        font = current_machine.get_text_font()
484        font_name = sketchingpy.abstracted.get_font_name(font, '/')
485
486        if font_name in self._added_fonts:
487            return
488
489        naked_font_name_components = font_name.split(' ')[1:]
490        naked_font_name = ' '.join(naked_font_name_components)
491
492        new_font = pyscript.window.FontFace.new(naked_font_name, 'url(%s)' % identifier)
493        new_font.load()
494        pyscript.document.fonts.add(new_font)
495        self._added_fonts.add(font_name)

Set the type and size of text to draw.

Set the size and font to use for drawing text.

Arguments:
  • font: Path to the TTF font file.
  • size: Size of the font (px).
def push_transform(self):
497    def push_transform(self):
498        self._context.save()

Save current transformation state.

Save current sketch transformation state to the matrix history. This works as a stack (like a stack of plates) where this puts a new plate on the top of the pile. This will leave the current transformation matrix in the sketch unchanged.

def pop_transform(self):
500    def pop_transform(self):
501        self._context.restore()

Restore a previously saved transformation state.

Restore the most recently transformation configuration saved in matrix history, removing that "transform matrix" from the history. This works as a stack (like a stack of plates) where the top of the pile is taken off and restored, removing it from that stack. This will overwrite the current transformation configuration in the sketch.

def get_native(self):
507    def get_native(self):
508        return self._element

Get a reference to the underlying native renderer object.

Returns:

Native render object.

def set_fps(self, rate: int):
510    def set_fps(self, rate: int):
511        self._state_frame_rate = rate

Indicate how fast the sketch should redraw.

Indicate a target frames per second that the sketch will take a "step" or redraw. Note that this is a goal and, if the system fall behind, it will drop frames and cause the on_step callback to be executed fewer times than the target.

Arguments:
  • rate: The number of frames to try to draw per second.
def set_title(self, title: str):
513    def set_title(self, title: str):
514        js.document.title = title

Indicate the title to assign the window in the operating system.

Indicate the human-readable string title to assign to the sketch window.

Arguments:
  • title: The text of the title.
def quit(self):
516    def quit(self):
517        self._stopped = True

Finish execution of the sketch.

Cause the sketch to stop execution.

def show(self, ax=None):
519    def show(self, ax=None):
520        self._show_internal(ax=ax, quit_immediately=False)

Show the sketch.

Show the sketch to the user and, if applicable, start the draw loop specified by set_fps. For Sketch2DApp, will execute any waiting drawing instructions provided to the sketch prior to showing. This is conceptually the same as "starting" the sketch.

Arguments:
  • ax: The container into which the sketch should be shown. Currently only supported for Sketch2DStatic. Optional and ignored on most renderers.
def show_and_quit(self, ax=None):
522    def show_and_quit(self, ax=None):
523        self._show_internal(ax=ax, quit_immediately=True)

Show the sketch and quit immediatley afterwards.

Show the sketch to the user and quit immediately afterwards, a routine potentially useful for testing.

def print(self, message: str):
525    def print(self, message: str):
526        console_id = self._element_id + '-console'
527        target_root = js.document.getElementById(console_id)
528
529        if target_root is None:
530            print(message)
531            return
532
533        new_li = pyscript.document.createElement('li')
534        new_content = pyscript.document.createTextNode(message)
535        new_li.appendChild(new_content)
536
537        target_root.appendChild(new_li)

Print a message to terminal or equivalent.

Arguments:
  • message: The string message to be printed.
def translate(self, x: float, y: float):
543    def translate(self, x: float, y: float):
544        self._context.translate(x, y)

Change the location of the origin.

Change the transform matrix such that any drawing afterwards is moved by a set amount.

Arguments:
  • x: The number of pixels to offset horizontally.
  • y: The number of pixels to offset vertically.
def rotate(self, angle: float):
546    def rotate(self, angle: float):
547        angle_rad = self._convert_to_radians(angle)
548        self._context.rotate(angle_rad)

Rotate around the current origin.

Change the transform matrix such that any drawing afterwards is rotated around the current origin clock-wise.

Arguments:
  • angle: The angle by which to rotate.
def scale(self, scale: float):
550    def scale(self, scale: float):
551        self._context.scale(scale, scale)

Scale outwards from the current origin.

Change the transform matrix such that any drawing afterwards is scaled from the current origin.

Arguments:
  • scale: The factor by which to scale where values over 1 scale up and less than 1 scale down. A value of 1 will have no effect.
class PyscriptSketchStateMachine(sketchingpy.state_struct.SketchStateMachine):
695class PyscriptSketchStateMachine(sketchingpy.state_struct.SketchStateMachine):
696    """Implementation of SketchStateMachine for Pyscript types."""
697
698    def __init__(self):
699        """Create a new state machine for Pyscript-based sketches."""
700        super().__init__()
701        self._text_align_native = self._transform_text_align(super().get_text_align_native())
702
703    def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
704        super().set_text_align(text_align)
705        self._text_align_native = self._transform_text_align(super().get_text_align_native())
706
707    def get_text_align_native(self):
708        return self._text_align_native
709
710    def get_text_font_native(self):
711        return sketchingpy.abstracted.get_font_name(self.get_text_font(), os.path.sep)
712
713    def _transform_text_align(self,
714        text_align: sketchingpy.state_struct.TextAlign) -> sketchingpy.state_struct.TextAlign:
715
716        HORIZONTAL_ALIGNS = {
717            sketchingpy.const.LEFT: 'left',
718            sketchingpy.const.CENTER: 'center',
719            sketchingpy.const.RIGHT: 'right'
720        }
721
722        VERTICAL_ALIGNS = {
723            sketchingpy.const.TOP: 'top',
724            sketchingpy.const.CENTER: 'middle',
725            sketchingpy.const.BASELINE: 'alphabetic',
726            sketchingpy.const.BOTTOM: 'bottom'
727        }
728
729        return sketchingpy.state_struct.TextAlign(
730            HORIZONTAL_ALIGNS[text_align.get_horizontal_align()],
731            VERTICAL_ALIGNS[text_align.get_vertical_align()]
732        )

Implementation of SketchStateMachine for Pyscript types.

PyscriptSketchStateMachine()
698    def __init__(self):
699        """Create a new state machine for Pyscript-based sketches."""
700        super().__init__()
701        self._text_align_native = self._transform_text_align(super().get_text_align_native())

Create a new state machine for Pyscript-based sketches.

def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
703    def set_text_align(self, text_align: sketchingpy.state_struct.TextAlign):
704        super().set_text_align(text_align)
705        self._text_align_native = self._transform_text_align(super().get_text_align_native())

Indicate the alignment to use when drawing text.

Arguments:
  • text_align: Structure describing horizontal and vertical text alignment.
def get_text_align_native(self):
707    def get_text_align_native(self):
708        return self._text_align_native

Get the alignment to use when drawing text.

Returns:

Renderer-specific value.

def get_text_font_native(self):
710    def get_text_font_native(self):
711        return sketchingpy.abstracted.get_font_name(self.get_text_font(), os.path.sep)

Get the type and size for text drawing.

Returns:

Renderer-specific value.

class WebImage(sketchingpy.abstracted.Image):
735class WebImage(sketchingpy.abstracted.Image):
736    """Strategy implementation for HTML images."""
737
738    def __init__(self, src: str):
739        """Create a new image.
740
741        Args:
742            src: Path to the image.
743        """
744        super().__init__(src)
745
746        preload_suffix = src.replace("./", "").replace("/", "").replace(".", "-").replace(" ", "-")
747        preload_name = "preload-img-" + preload_suffix
748        preloaded_image = js.document.getElementById(preload_name)
749
750        if preloaded_image:
751            image = preloaded_image
752        else:
753            image = js.document.createElement("img")
754            image.src = src
755
756        self._native = image
757        self._width: typing.Optional[float] = None
758        self._height: typing.Optional[float] = None
759
760    def get_width(self) -> float:
761        if self._width is None:
762            return self._native.width
763        else:
764            return self._width
765
766    def get_height(self) -> float:
767        if self._height is None:
768            return self._native.height
769        else:
770            return self._height
771
772    def resize(self, width: float, height: float):
773        self._width = width
774        self._height = height
775
776    def get_native(self):
777        return self._native
778
779    def get_is_loaded(self):
780        return self._native.width > 0

Strategy implementation for HTML images.

WebImage(src: str)
738    def __init__(self, src: str):
739        """Create a new image.
740
741        Args:
742            src: Path to the image.
743        """
744        super().__init__(src)
745
746        preload_suffix = src.replace("./", "").replace("/", "").replace(".", "-").replace(" ", "-")
747        preload_name = "preload-img-" + preload_suffix
748        preloaded_image = js.document.getElementById(preload_name)
749
750        if preloaded_image:
751            image = preloaded_image
752        else:
753            image = js.document.createElement("img")
754            image.src = src
755
756        self._native = image
757        self._width: typing.Optional[float] = None
758        self._height: typing.Optional[float] = None

Create a new image.

Arguments:
  • src: Path to the image.
def get_width(self) -> float:
760    def get_width(self) -> float:
761        if self._width is None:
762            return self._native.width
763        else:
764            return self._width

Get the width of this image in pixels.

Returns:

Horizontal width of this image.

def get_height(self) -> float:
766    def get_height(self) -> float:
767        if self._height is None:
768            return self._native.height
769        else:
770            return self._height

Get the height of this image in pixels.

Returns:

Vertical height of this image.

def resize(self, width: float, height: float):
772    def resize(self, width: float, height: float):
773        self._width = width
774        self._height = height

Resize this image by scaling.

Arguments:
  • width: The new desired width of this image in pixels.
  • height: The new desired height of this image in pixels.
def get_native(self):
776    def get_native(self):
777        return self._native

Access the underlying native version of this image.

Returns:

Renderer specific native version.

def get_is_loaded(self):
779    def get_is_loaded(self):
780        return self._native.width > 0

Determine if this image has finished loading.

Returns:

True if loaded and ready to draw. False otherwise.

class PyscriptMouse(sketchingpy.control_struct.Mouse):
783class PyscriptMouse(sketchingpy.control_struct.Mouse):
784    """Strategy implementation for Pyscript-based mouse access."""
785
786    def __init__(self, element):
787        """Create a new mouse strategy using HTML5.
788
789        Args:
790            element: The element to which mouse event listeners should be added.
791        """
792        self._element = element
793
794        self._x = 0
795        self._y = 0
796
797        self._buttons_pressed = set()
798
799        mouse_move_callback = pyodide.ffi.create_proxy(
800            lambda event: self._report_mouse_move(event)
801        )
802        self._element.addEventListener(
803            'mousemove',
804            mouse_move_callback
805        )
806
807        mouse_down_callback = pyodide.ffi.create_proxy(
808            lambda event: self._report_mouse_down(event)
809        )
810        self._element.addEventListener(
811            'mousedown',
812            mouse_down_callback
813        )
814
815        click_callback = pyodide.ffi.create_proxy(
816            lambda event: self._report_click(event)
817        )
818        self._element.addEventListener(
819            'click',
820            click_callback
821        )
822
823        context_menu_callback = pyodide.ffi.create_proxy(
824            lambda event: self._report_context_menu(event)
825        )
826        self._element.addEventListener(
827            'contextmenu',
828            context_menu_callback
829        )
830
831        self._press_callback = None
832        self._release_callback = None
833
834    def get_pointer_x(self):
835        return self._x
836
837    def get_pointer_y(self):
838        return self._y
839
840    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
841        return map(lambda x: sketchingpy.control_struct.Button(x), self._buttons_pressed)
842
843    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
844        self._press_callback = callback
845
846    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
847        self._release_callback = callback
848
849    def _report_mouse_move(self, event):
850        bounding_box = self._element.getBoundingClientRect()
851        self._x = event.clientX - bounding_box.left
852        self._y = event.clientY - bounding_box.top
853
854    def _report_mouse_down(self, event):
855        if event.button == 0:
856            self._buttons_pressed.add(sketchingpy.const.MOUSE_LEFT_BUTTON)
857            if self._press_callback is not None:
858                button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
859                self._press_callback(button)
860        elif event.button == 2:
861            self._buttons_pressed.add(sketchingpy.const.MOUSE_RIGHT_BUTTON)
862            if self._press_callback is not None:
863                button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
864                self._press_callback(button)
865
866    def _report_click(self, event):
867        self._buttons_pressed.remove(sketchingpy.const.MOUSE_LEFT_BUTTON)
868
869        if self._release_callback is not None:
870            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
871            self._release_callback(button)
872
873        event.preventDefault()
874
875    def _report_context_menu(self, event):
876        self._buttons_pressed.remove(sketchingpy.const.MOUSE_RIGHT_BUTTON)
877
878        if self._release_callback is not None:
879            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
880            self._release_callback(button)
881
882        event.preventDefault()

Strategy implementation for Pyscript-based mouse access.

PyscriptMouse(element)
786    def __init__(self, element):
787        """Create a new mouse strategy using HTML5.
788
789        Args:
790            element: The element to which mouse event listeners should be added.
791        """
792        self._element = element
793
794        self._x = 0
795        self._y = 0
796
797        self._buttons_pressed = set()
798
799        mouse_move_callback = pyodide.ffi.create_proxy(
800            lambda event: self._report_mouse_move(event)
801        )
802        self._element.addEventListener(
803            'mousemove',
804            mouse_move_callback
805        )
806
807        mouse_down_callback = pyodide.ffi.create_proxy(
808            lambda event: self._report_mouse_down(event)
809        )
810        self._element.addEventListener(
811            'mousedown',
812            mouse_down_callback
813        )
814
815        click_callback = pyodide.ffi.create_proxy(
816            lambda event: self._report_click(event)
817        )
818        self._element.addEventListener(
819            'click',
820            click_callback
821        )
822
823        context_menu_callback = pyodide.ffi.create_proxy(
824            lambda event: self._report_context_menu(event)
825        )
826        self._element.addEventListener(
827            'contextmenu',
828            context_menu_callback
829        )
830
831        self._press_callback = None
832        self._release_callback = None

Create a new mouse strategy using HTML5.

Arguments:
  • element: The element to which mouse event listeners should be added.
def get_pointer_x(self):
834    def get_pointer_x(self):
835        return self._x

Get the x coordinate of the mouse pointer.

Get the current horizontal coordinate of the mouse pointer or, in the case of a touchscreen, the point of last touch input if available. Defaults to 0 if no mouse events have been seen.

Returns:

The horizontal coordinate of the mouse pointer.

def get_pointer_y(self):
837    def get_pointer_y(self):
838        return self._y

Get the y coordinate of the mouse pointer.

Get the current vertical coordinate of the mouse pointer or, in the case of a touchscreen, the point of last touch input if available. Defaults to 0 if no mouse events have been seen.

Returns:

The vertical coordinate of the mouse pointer.

def get_buttons_pressed(self) -> Iterable[sketchingpy.control_struct.Button]:
840    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
841        return map(lambda x: sketchingpy.control_struct.Button(x), self._buttons_pressed)

Information about the mouse buttons currently pressed.

Get information about mouse buttons currently pressed.

Returns:

Collection of buttons currently pressed.

def on_button_press( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Mouse')], NoneType]):
843    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
844        self._press_callback = callback

Callback for when a mouse button is pressed.

Register a callback for when a button is pressed, calling a function with the button and mouse. Will pass two arguments to that callback function: first a Button followed by a Mouse. Will unregister prior callbacks for on_button_press.

Arguments:
  • callback: The function to invoke when a mouse button or equivalent is pressed.
def on_button_release( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Mouse')], NoneType]):
846    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
847        self._release_callback = callback

Callback for when a mouse button is released.

Register a callback for when a button is unpressed, calling a function with the button and mouse. Will pass two arguments to that callback function: first a Button followed by a Mouse. Will unregister prior callbacks for on_button_press.

Arguments:
  • callback: The function to invoke when a mouse button or equivalent is unpressed.
class PyscriptKeyboard(sketchingpy.control_struct.Keyboard):
885class PyscriptKeyboard(sketchingpy.control_struct.Keyboard):
886    """Strategy implementation for Pyscript-based keyboard access."""
887
888    def __init__(self, element):
889        """Create a new mouse strategy using HTML5 and Pyscript.
890
891        Args:
892            element: The element to which keyboard event listeners should be added.
893        """
894        super().__init__()
895        self._element = element
896        self._pressed = set()
897        self._press_callback = None
898        self._release_callback = None
899
900        keydown_callback = pyodide.ffi.create_proxy(
901            lambda event: self._report_key_down(event)
902        )
903        self._element.addEventListener(
904            'keydown',
905            keydown_callback
906        )
907
908        keyup_callback = pyodide.ffi.create_proxy(
909            lambda event: self._report_key_up(event)
910        )
911        self._element.addEventListener(
912            'keyup',
913            keyup_callback
914        )
915
916    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
917        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)
918
919    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
920        self._press_callback = callback
921
922    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
923        self._release_callback = callback
924
925    def _report_key_down(self, event):
926        key = self._map_key(event.key)
927
928        if key is None:
929            return
930
931        self._pressed.add(key)
932
933        if self._press_callback is not None:
934            button = sketchingpy.control_struct.Button(key)
935            self._press_callback(button)
936
937        event.preventDefault()
938
939    def _report_key_up(self, event):
940        key = self._map_key(event.key)
941
942        if key is None:
943            return
944
945        self._pressed.remove(key)
946
947        if self._release_callback is not None:
948            button = sketchingpy.control_struct.Button(key)
949            self._release_callback(button)
950
951        event.preventDefault()
952
953    def _map_key(self, target: str) -> typing.Optional[str]:
954        if target in KEY_MAP:
955            return KEY_MAP[target.lower()]  # Required for browser compatibility
956        else:
957            return target.lower()

Strategy implementation for Pyscript-based keyboard access.

PyscriptKeyboard(element)
888    def __init__(self, element):
889        """Create a new mouse strategy using HTML5 and Pyscript.
890
891        Args:
892            element: The element to which keyboard event listeners should be added.
893        """
894        super().__init__()
895        self._element = element
896        self._pressed = set()
897        self._press_callback = None
898        self._release_callback = None
899
900        keydown_callback = pyodide.ffi.create_proxy(
901            lambda event: self._report_key_down(event)
902        )
903        self._element.addEventListener(
904            'keydown',
905            keydown_callback
906        )
907
908        keyup_callback = pyodide.ffi.create_proxy(
909            lambda event: self._report_key_up(event)
910        )
911        self._element.addEventListener(
912            'keyup',
913            keyup_callback
914        )

Create a new mouse strategy using HTML5 and Pyscript.

Arguments:
  • element: The element to which keyboard event listeners should be added.
def get_keys_pressed(self) -> Iterable[sketchingpy.control_struct.Button]:
916    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
917        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)

Get a list of keys currently pressed.

Get a list of keys as Buttons.

Returns:

Get list of buttons pressed.

def on_key_press( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Keyboard')], NoneType]):
919    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
920        self._press_callback = callback

Callback for when a key is pressed.

Register a callback for when a key is pressed, calling a function with the key and keyboard. Will pass two arguments to that callback function: first a Button followed by a Keyboard. Will unregister prior callbacks for on_key_press.

Arguments:
  • callback: The function to invoke when a key is pressed.
def on_key_release( self, callback: Callable[[sketchingpy.control_struct.Button, ForwardRef('Keyboard')], NoneType]):
922    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
923        self._release_callback = callback

Callback for when a key is released.

Register a callback for when a key is unpressed, calling a function with the key and keyboard. Will pass two arguments to that callback function: first a Button followed by a Keyboard. Will unregister prior callbacks for on_key_release.

Arguments:
  • callback: The function to invoke when a key is released.
class WebDataLayer(sketchingpy.data_struct.DataLayer):
 960class WebDataLayer(sketchingpy.data_struct.DataLayer):
 961    """Data layer which interfaces with network and browser."""
 962
 963    def get_csv(self, path: str) -> sketchingpy.data_struct.Records:
 964        if os.path.exists(path):
 965            with open(path) as f:
 966                reader = csv.DictReader(f)
 967                return list(reader)
 968        else:
 969            string_io = pyodide.http.open_url(path)
 970            reader = csv.DictReader(string_io)
 971            return list(reader)
 972
 973    def write_csv(self, records: sketchingpy.data_struct.Records,
 974        columns: sketchingpy.data_struct.Columns, path: str):
 975        def build_record(target: typing.Dict) -> typing.Dict:
 976            return dict(map(lambda key: (key, target[key]), columns))
 977
 978        records_serialized = map(build_record, records)
 979
 980        target = io.StringIO()
 981
 982        writer = csv.DictWriter(target, fieldnames=columns)  # type: ignore
 983        writer.writeheader()
 984        writer.writerows(records_serialized)
 985
 986        self._download_text(target.getvalue(), path, 'text/csv')
 987
 988    def get_json(self, path: str):
 989        if os.path.exists(path):
 990            with open(path) as f:
 991                return json.load(f)
 992        else:
 993            string_io = pyodide.http.open_url(path)
 994            return json.loads(string_io.read())
 995
 996    def write_json(self, target, path: str):
 997        self._download_text(json.dumps(target), path, 'application/json')
 998
 999    def get_text(self, path: str):
1000        string_io = pyodide.http.open_url(path)
1001        return string_io.read()
1002
1003    def write_text(self, target, path: str):
1004        self._download_text(target, path, 'text/plain')
1005
1006    def _download_text(self, text: str, filename: str, mime: str):
1007        text_encoded = urllib.parse.quote(text)
1008
1009        link = js.document.createElement('a')
1010        link.download = filename
1011        link.href = 'data:%s;charset=utf-8,%s' % (mime, text_encoded)
1012
1013        link.click()

Data layer which interfaces with network and browser.

def get_csv(self, path: str) -> Iterable[Dict]:
963    def get_csv(self, path: str) -> sketchingpy.data_struct.Records:
964        if os.path.exists(path):
965            with open(path) as f:
966                reader = csv.DictReader(f)
967                return list(reader)
968        else:
969            string_io = pyodide.http.open_url(path)
970            reader = csv.DictReader(string_io)
971            return list(reader)

Load a CSV file as a list of dictionaries.

Load a CSV file and parse it as a list where each row after the "header row" becomes a dictionary.

Arguments:
  • path: The location at which the CSV file can be found.
Returns:

List of dictionary.

def write_csv(self, records: Iterable[Dict], columns: Iterable[str], path: str):
973    def write_csv(self, records: sketchingpy.data_struct.Records,
974        columns: sketchingpy.data_struct.Columns, path: str):
975        def build_record(target: typing.Dict) -> typing.Dict:
976            return dict(map(lambda key: (key, target[key]), columns))
977
978        records_serialized = map(build_record, records)
979
980        target = io.StringIO()
981
982        writer = csv.DictWriter(target, fieldnames=columns)  # type: ignore
983        writer.writeheader()
984        writer.writerows(records_serialized)
985
986        self._download_text(target.getvalue(), path, 'text/csv')

Write a list of dictionaries as a CSV file.

Write a CSV file with header row, saving it to local file system or offering it as a download in the browser.

Arguments:
  • records: List of dictionaries to be written.
  • columns: Ordered list of columns to include in the CSV file.
  • path: The location at which the file should be written.
def get_json(self, path: str):
988    def get_json(self, path: str):
989        if os.path.exists(path):
990            with open(path) as f:
991                return json.load(f)
992        else:
993            string_io = pyodide.http.open_url(path)
994            return json.loads(string_io.read())

Read a JSON file.

Read a JSON file either from local file system or the network.

Arguments:
  • path: The location at which the JSON file can be found.
Returns:

Loaded JSON content.

def write_json(self, target, path: str):
996    def write_json(self, target, path: str):
997        self._download_text(json.dumps(target), path, 'application/json')

Write a JSON file.

Write a JSON file, saving it to local file system or offering it as a download in the browser.

Arguments:
  • target: The value to write as JSON.
  • path: The location at which the file should be written.
def get_text(self, path: str):
 999    def get_text(self, path: str):
1000        string_io = pyodide.http.open_url(path)
1001        return string_io.read()

Read a text file.

Read a text file either from local file system or the network.

Arguments:
  • path: The location where the file can be found.
Returns:

Loaded content as a string.

def write_text(self, target, path: str):
1003    def write_text(self, target, path: str):
1004        self._download_text(target, path, 'text/plain')

Write a text file.

Write a text file, saving it to local file system or offering it as a download in the browser.

Arguments:
  • target: The contents to write.
  • path: The location at which the file should be written.
class WebDialogLayer(sketchingpy.dialog_struct.DialogLayer):
1016class WebDialogLayer(sketchingpy.dialog_struct.DialogLayer):
1017    """Dialog / simple UI layer for web apps."""
1018
1019    def __init__(self, sketch: Sketch2DWeb):
1020        """"Initialize tkinter but hide the root window."""
1021        self._sketch = sketch
1022
1023    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1024        pyscript.window.alert(message)
1025        if callback is not None:
1026            callback()
1027
1028    def show_prompt(self, message: str,
1029        callback: typing.Optional[typing.Callable[[str], None]] = None):
1030        response = pyscript.window.prompt(message)
1031        if callback is not None and response is not None:
1032            callback(response)
1033
1034    def get_file_save_location(self,
1035        callback: typing.Optional[typing.Callable[[str], None]] = None):
1036        self.show_prompt('Filename to save:', callback)
1037
1038    def get_file_load_location(self,
1039        callback: typing.Optional[typing.Callable[[str], None]] = None):
1040        self.show_prompt('Filename to load:', callback)

Dialog / simple UI layer for web apps.

WebDialogLayer(sketch: Sketch2DWeb)
1019    def __init__(self, sketch: Sketch2DWeb):
1020        """"Initialize tkinter but hide the root window."""
1021        self._sketch = sketch

"Initialize tkinter but hide the root window.

def show_alert( self, message: str, callback: Optional[Callable[[], NoneType]] = None):
1023    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1024        pyscript.window.alert(message)
1025        if callback is not None:
1026            callback()

Show an alert dialog box.

Arguments:
  • callback: Method to invoke when the box closes.
  • message: The string to show the user.
def show_prompt( self, message: str, callback: Optional[Callable[[str], NoneType]] = None):
1028    def show_prompt(self, message: str,
1029        callback: typing.Optional[typing.Callable[[str], None]] = None):
1030        response = pyscript.window.prompt(message)
1031        if callback is not None and response is not None:
1032            callback(response)

Get a string input from the user.

Arguments:
  • message: The message to display to the user within the dialog.
  • callback: Method to invoke when the box closes with a single string parameter provided by the user. Not invoked if cancelled.
def get_file_save_location(self, callback: Optional[Callable[[str], NoneType]] = None):
1034    def get_file_save_location(self,
1035        callback: typing.Optional[typing.Callable[[str], None]] = None):
1036        self.show_prompt('Filename to save:', callback)

Get either the filename or full location for saving a file.

Arguments:
  • callback: Method to invoke when the box closes with single string parameter which is the filename or the path selected by the user. Not invoked if cancelled.
def get_file_load_location(self, callback: Optional[Callable[[str], NoneType]] = None):
1038    def get_file_load_location(self,
1039        callback: typing.Optional[typing.Callable[[str], None]] = None):
1040        self.show_prompt('Filename to load:', callback)

Get either the filename or full location for opening a file.

Arguments:
  • callback: Method to invoke when the box closes with single string parameter which is the filename or the path selected by the user. Not invoked if cancelled.