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        image = js.document.createElement("img")
 746        image.src = src
 747
 748        self._native = image
 749        self._width: typing.Optional[float] = None
 750        self._height: typing.Optional[float] = None
 751
 752    def get_width(self) -> float:
 753        if self._width is None:
 754            return self._native.width
 755        else:
 756            return self._width
 757
 758    def get_height(self) -> float:
 759        if self._height is None:
 760            return self._native.height
 761        else:
 762            return self._height
 763
 764    def resize(self, width: float, height: float):
 765        self._width = width
 766        self._height = height
 767
 768    def get_native(self):
 769        return self._native
 770
 771    def get_is_loaded(self):
 772        return self._native.width > 0
 773
 774
 775class PyscriptMouse(sketchingpy.control_struct.Mouse):
 776    """Strategy implementation for Pyscript-based mouse access."""
 777
 778    def __init__(self, element):
 779        """Create a new mouse strategy using HTML5.
 780
 781        Args:
 782            element: The element to which mouse event listeners should be added.
 783        """
 784        self._element = element
 785
 786        self._x = 0
 787        self._y = 0
 788
 789        self._buttons_pressed = set()
 790
 791        mouse_move_callback = pyodide.ffi.create_proxy(
 792            lambda event: self._report_mouse_move(event)
 793        )
 794        self._element.addEventListener(
 795            'mousemove',
 796            mouse_move_callback
 797        )
 798
 799        mouse_down_callback = pyodide.ffi.create_proxy(
 800            lambda event: self._report_mouse_down(event)
 801        )
 802        self._element.addEventListener(
 803            'mousedown',
 804            mouse_down_callback
 805        )
 806
 807        click_callback = pyodide.ffi.create_proxy(
 808            lambda event: self._report_click(event)
 809        )
 810        self._element.addEventListener(
 811            'click',
 812            click_callback
 813        )
 814
 815        context_menu_callback = pyodide.ffi.create_proxy(
 816            lambda event: self._report_context_menu(event)
 817        )
 818        self._element.addEventListener(
 819            'contextmenu',
 820            context_menu_callback
 821        )
 822
 823        self._press_callback = None
 824        self._release_callback = None
 825
 826    def get_pointer_x(self):
 827        return self._x
 828
 829    def get_pointer_y(self):
 830        return self._y
 831
 832    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
 833        return map(lambda x: sketchingpy.control_struct.Button(x), self._buttons_pressed)
 834
 835    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
 836        self._press_callback = callback
 837
 838    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
 839        self._release_callback = callback
 840
 841    def _report_mouse_move(self, event):
 842        bounding_box = self._element.getBoundingClientRect()
 843        self._x = event.clientX - bounding_box.left
 844        self._y = event.clientY - bounding_box.top
 845
 846    def _report_mouse_down(self, event):
 847        if event.button == 0:
 848            self._buttons_pressed.add(sketchingpy.const.MOUSE_LEFT_BUTTON)
 849            if self._press_callback is not None:
 850                button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
 851                self._press_callback(button)
 852        elif event.button == 2:
 853            self._buttons_pressed.add(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 854            if self._press_callback is not None:
 855                button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 856                self._press_callback(button)
 857
 858    def _report_click(self, event):
 859        self._buttons_pressed.remove(sketchingpy.const.MOUSE_LEFT_BUTTON)
 860
 861        if self._release_callback is not None:
 862            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_LEFT_BUTTON)
 863            self._release_callback(button)
 864
 865        event.preventDefault()
 866
 867    def _report_context_menu(self, event):
 868        self._buttons_pressed.remove(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 869
 870        if self._release_callback is not None:
 871            button = sketchingpy.control_struct.Button(sketchingpy.const.MOUSE_RIGHT_BUTTON)
 872            self._release_callback(button)
 873
 874        event.preventDefault()
 875
 876
 877class PyscriptKeyboard(sketchingpy.control_struct.Keyboard):
 878    """Strategy implementation for Pyscript-based keyboard access."""
 879
 880    def __init__(self, element):
 881        """Create a new mouse strategy using HTML5 and Pyscript.
 882
 883        Args:
 884            element: The element to which keyboard event listeners should be added.
 885        """
 886        super().__init__()
 887        self._element = element
 888        self._pressed = set()
 889        self._press_callback = None
 890        self._release_callback = None
 891
 892        keydown_callback = pyodide.ffi.create_proxy(
 893            lambda event: self._report_key_down(event)
 894        )
 895        self._element.addEventListener(
 896            'keydown',
 897            keydown_callback
 898        )
 899
 900        keyup_callback = pyodide.ffi.create_proxy(
 901            lambda event: self._report_key_up(event)
 902        )
 903        self._element.addEventListener(
 904            'keyup',
 905            keyup_callback
 906        )
 907
 908    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
 909        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)
 910
 911    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
 912        self._press_callback = callback
 913
 914    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
 915        self._release_callback = callback
 916
 917    def _report_key_down(self, event):
 918        key = self._map_key(event.key)
 919
 920        if key is None:
 921            return
 922
 923        self._pressed.add(key)
 924
 925        if self._press_callback is not None:
 926            button = sketchingpy.control_struct.Button(key)
 927            self._press_callback(button)
 928
 929        event.preventDefault()
 930
 931    def _report_key_up(self, event):
 932        key = self._map_key(event.key)
 933
 934        if key is None:
 935            return
 936
 937        self._pressed.remove(key)
 938
 939        if self._release_callback is not None:
 940            button = sketchingpy.control_struct.Button(key)
 941            self._release_callback(button)
 942
 943        event.preventDefault()
 944
 945    def _map_key(self, target: str) -> typing.Optional[str]:
 946        if target in KEY_MAP:
 947            return KEY_MAP[target.lower()]  # Required for browser compatibility
 948        else:
 949            return target.lower()
 950
 951
 952class WebDataLayer(sketchingpy.data_struct.DataLayer):
 953    """Data layer which interfaces with network and browser."""
 954
 955    def get_csv(self, path: str) -> sketchingpy.data_struct.Records:
 956        if os.path.exists(path):
 957            with open(path) as f:
 958                reader = csv.DictReader(f)
 959                return list(reader)
 960        else:
 961            string_io = pyodide.http.open_url(path)
 962            reader = csv.DictReader(string_io)
 963            return list(reader)
 964
 965    def write_csv(self, records: sketchingpy.data_struct.Records,
 966        columns: sketchingpy.data_struct.Columns, path: str):
 967        def build_record(target: typing.Dict) -> typing.Dict:
 968            return dict(map(lambda key: (key, target[key]), columns))
 969
 970        records_serialized = map(build_record, records)
 971
 972        target = io.StringIO()
 973
 974        writer = csv.DictWriter(target, fieldnames=columns)  # type: ignore
 975        writer.writeheader()
 976        writer.writerows(records_serialized)
 977
 978        self._download_text(target.getvalue(), path, 'text/csv')
 979
 980    def get_json(self, path: str):
 981        if os.path.exists(path):
 982            with open(path) as f:
 983                return json.load(f)
 984        else:
 985            string_io = pyodide.http.open_url(path)
 986            return json.loads(string_io.read())
 987
 988    def write_json(self, target, path: str):
 989        self._download_text(json.dumps(target), path, 'application/json')
 990
 991    def get_text(self, path: str):
 992        string_io = pyodide.http.open_url(path)
 993        return string_io.read()
 994
 995    def write_text(self, target, path: str):
 996        self._download_text(target, path, 'text/plain')
 997
 998    def _download_text(self, text: str, filename: str, mime: str):
 999        text_encoded = urllib.parse.quote(text)
1000
1001        link = js.document.createElement('a')
1002        link.download = filename
1003        link.href = 'data:%s;charset=utf-8,%s' % (mime, text_encoded)
1004
1005        link.click()
1006
1007
1008class WebDialogLayer(sketchingpy.dialog_struct.DialogLayer):
1009    """Dialog / simple UI layer for web apps."""
1010
1011    def __init__(self, sketch: Sketch2DWeb):
1012        """"Initialize tkinter but hide the root window."""
1013        self._sketch = sketch
1014
1015    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1016        pyscript.window.alert(message)
1017        if callback is not None:
1018            callback()
1019
1020    def show_prompt(self, message: str,
1021        callback: typing.Optional[typing.Callable[[str], None]] = None):
1022        response = pyscript.window.prompt(message)
1023        if callback is not None and response is not None:
1024            callback(response)
1025
1026    def get_file_save_location(self,
1027        callback: typing.Optional[typing.Callable[[str], None]] = None):
1028        self.show_prompt('Filename to save:', callback)
1029
1030    def get_file_load_location(self,
1031        callback: typing.Optional[typing.Callable[[str], None]] = None):
1032        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        image = js.document.createElement("img")
747        image.src = src
748
749        self._native = image
750        self._width: typing.Optional[float] = None
751        self._height: typing.Optional[float] = None
752
753    def get_width(self) -> float:
754        if self._width is None:
755            return self._native.width
756        else:
757            return self._width
758
759    def get_height(self) -> float:
760        if self._height is None:
761            return self._native.height
762        else:
763            return self._height
764
765    def resize(self, width: float, height: float):
766        self._width = width
767        self._height = height
768
769    def get_native(self):
770        return self._native
771
772    def get_is_loaded(self):
773        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        image = js.document.createElement("img")
747        image.src = src
748
749        self._native = image
750        self._width: typing.Optional[float] = None
751        self._height: typing.Optional[float] = None

Create a new image.

Arguments:
  • src: Path to the image.
def get_width(self) -> float:
753    def get_width(self) -> float:
754        if self._width is None:
755            return self._native.width
756        else:
757            return self._width

Get the width of this image in pixels.

Returns:

Horizontal width of this image.

def get_height(self) -> float:
759    def get_height(self) -> float:
760        if self._height is None:
761            return self._native.height
762        else:
763            return self._height

Get the height of this image in pixels.

Returns:

Vertical height of this image.

def resize(self, width: float, height: float):
765    def resize(self, width: float, height: float):
766        self._width = width
767        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):
769    def get_native(self):
770        return self._native

Access the underlying native version of this image.

Returns:

Renderer specific native version.

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

Strategy implementation for Pyscript-based mouse access.

PyscriptMouse(element)
779    def __init__(self, element):
780        """Create a new mouse strategy using HTML5.
781
782        Args:
783            element: The element to which mouse event listeners should be added.
784        """
785        self._element = element
786
787        self._x = 0
788        self._y = 0
789
790        self._buttons_pressed = set()
791
792        mouse_move_callback = pyodide.ffi.create_proxy(
793            lambda event: self._report_mouse_move(event)
794        )
795        self._element.addEventListener(
796            'mousemove',
797            mouse_move_callback
798        )
799
800        mouse_down_callback = pyodide.ffi.create_proxy(
801            lambda event: self._report_mouse_down(event)
802        )
803        self._element.addEventListener(
804            'mousedown',
805            mouse_down_callback
806        )
807
808        click_callback = pyodide.ffi.create_proxy(
809            lambda event: self._report_click(event)
810        )
811        self._element.addEventListener(
812            'click',
813            click_callback
814        )
815
816        context_menu_callback = pyodide.ffi.create_proxy(
817            lambda event: self._report_context_menu(event)
818        )
819        self._element.addEventListener(
820            'contextmenu',
821            context_menu_callback
822        )
823
824        self._press_callback = None
825        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):
827    def get_pointer_x(self):
828        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):
830    def get_pointer_y(self):
831        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]:
833    def get_buttons_pressed(self) -> sketchingpy.control_struct.Buttons:
834        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]):
836    def on_button_press(self, callback: sketchingpy.control_struct.MouseCallback):
837        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]):
839    def on_button_release(self, callback: sketchingpy.control_struct.MouseCallback):
840        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):
878class PyscriptKeyboard(sketchingpy.control_struct.Keyboard):
879    """Strategy implementation for Pyscript-based keyboard access."""
880
881    def __init__(self, element):
882        """Create a new mouse strategy using HTML5 and Pyscript.
883
884        Args:
885            element: The element to which keyboard event listeners should be added.
886        """
887        super().__init__()
888        self._element = element
889        self._pressed = set()
890        self._press_callback = None
891        self._release_callback = None
892
893        keydown_callback = pyodide.ffi.create_proxy(
894            lambda event: self._report_key_down(event)
895        )
896        self._element.addEventListener(
897            'keydown',
898            keydown_callback
899        )
900
901        keyup_callback = pyodide.ffi.create_proxy(
902            lambda event: self._report_key_up(event)
903        )
904        self._element.addEventListener(
905            'keyup',
906            keyup_callback
907        )
908
909    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
910        return map(lambda x: sketchingpy.control_struct.Button(x), self._pressed)
911
912    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
913        self._press_callback = callback
914
915    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
916        self._release_callback = callback
917
918    def _report_key_down(self, event):
919        key = self._map_key(event.key)
920
921        if key is None:
922            return
923
924        self._pressed.add(key)
925
926        if self._press_callback is not None:
927            button = sketchingpy.control_struct.Button(key)
928            self._press_callback(button)
929
930        event.preventDefault()
931
932    def _report_key_up(self, event):
933        key = self._map_key(event.key)
934
935        if key is None:
936            return
937
938        self._pressed.remove(key)
939
940        if self._release_callback is not None:
941            button = sketchingpy.control_struct.Button(key)
942            self._release_callback(button)
943
944        event.preventDefault()
945
946    def _map_key(self, target: str) -> typing.Optional[str]:
947        if target in KEY_MAP:
948            return KEY_MAP[target.lower()]  # Required for browser compatibility
949        else:
950            return target.lower()

Strategy implementation for Pyscript-based keyboard access.

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

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]:
909    def get_keys_pressed(self) -> sketchingpy.control_struct.Buttons:
910        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]):
912    def on_key_press(self, callback: sketchingpy.control_struct.KeyboardCallback):
913        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]):
915    def on_key_release(self, callback: sketchingpy.control_struct.KeyboardCallback):
916        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):
 953class WebDataLayer(sketchingpy.data_struct.DataLayer):
 954    """Data layer which interfaces with network and browser."""
 955
 956    def get_csv(self, path: str) -> sketchingpy.data_struct.Records:
 957        if os.path.exists(path):
 958            with open(path) as f:
 959                reader = csv.DictReader(f)
 960                return list(reader)
 961        else:
 962            string_io = pyodide.http.open_url(path)
 963            reader = csv.DictReader(string_io)
 964            return list(reader)
 965
 966    def write_csv(self, records: sketchingpy.data_struct.Records,
 967        columns: sketchingpy.data_struct.Columns, path: str):
 968        def build_record(target: typing.Dict) -> typing.Dict:
 969            return dict(map(lambda key: (key, target[key]), columns))
 970
 971        records_serialized = map(build_record, records)
 972
 973        target = io.StringIO()
 974
 975        writer = csv.DictWriter(target, fieldnames=columns)  # type: ignore
 976        writer.writeheader()
 977        writer.writerows(records_serialized)
 978
 979        self._download_text(target.getvalue(), path, 'text/csv')
 980
 981    def get_json(self, path: str):
 982        if os.path.exists(path):
 983            with open(path) as f:
 984                return json.load(f)
 985        else:
 986            string_io = pyodide.http.open_url(path)
 987            return json.loads(string_io.read())
 988
 989    def write_json(self, target, path: str):
 990        self._download_text(json.dumps(target), path, 'application/json')
 991
 992    def get_text(self, path: str):
 993        string_io = pyodide.http.open_url(path)
 994        return string_io.read()
 995
 996    def write_text(self, target, path: str):
 997        self._download_text(target, path, 'text/plain')
 998
 999    def _download_text(self, text: str, filename: str, mime: str):
1000        text_encoded = urllib.parse.quote(text)
1001
1002        link = js.document.createElement('a')
1003        link.download = filename
1004        link.href = 'data:%s;charset=utf-8,%s' % (mime, text_encoded)
1005
1006        link.click()

Data layer which interfaces with network and browser.

def get_csv(self, path: str) -> Iterable[Dict]:
956    def get_csv(self, path: str) -> sketchingpy.data_struct.Records:
957        if os.path.exists(path):
958            with open(path) as f:
959                reader = csv.DictReader(f)
960                return list(reader)
961        else:
962            string_io = pyodide.http.open_url(path)
963            reader = csv.DictReader(string_io)
964            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):
966    def write_csv(self, records: sketchingpy.data_struct.Records,
967        columns: sketchingpy.data_struct.Columns, path: str):
968        def build_record(target: typing.Dict) -> typing.Dict:
969            return dict(map(lambda key: (key, target[key]), columns))
970
971        records_serialized = map(build_record, records)
972
973        target = io.StringIO()
974
975        writer = csv.DictWriter(target, fieldnames=columns)  # type: ignore
976        writer.writeheader()
977        writer.writerows(records_serialized)
978
979        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):
981    def get_json(self, path: str):
982        if os.path.exists(path):
983            with open(path) as f:
984                return json.load(f)
985        else:
986            string_io = pyodide.http.open_url(path)
987            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):
989    def write_json(self, target, path: str):
990        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):
992    def get_text(self, path: str):
993        string_io = pyodide.http.open_url(path)
994        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):
996    def write_text(self, target, path: str):
997        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):
1009class WebDialogLayer(sketchingpy.dialog_struct.DialogLayer):
1010    """Dialog / simple UI layer for web apps."""
1011
1012    def __init__(self, sketch: Sketch2DWeb):
1013        """"Initialize tkinter but hide the root window."""
1014        self._sketch = sketch
1015
1016    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1017        pyscript.window.alert(message)
1018        if callback is not None:
1019            callback()
1020
1021    def show_prompt(self, message: str,
1022        callback: typing.Optional[typing.Callable[[str], None]] = None):
1023        response = pyscript.window.prompt(message)
1024        if callback is not None and response is not None:
1025            callback(response)
1026
1027    def get_file_save_location(self,
1028        callback: typing.Optional[typing.Callable[[str], None]] = None):
1029        self.show_prompt('Filename to save:', callback)
1030
1031    def get_file_load_location(self,
1032        callback: typing.Optional[typing.Callable[[str], None]] = None):
1033        self.show_prompt('Filename to load:', callback)

Dialog / simple UI layer for web apps.

WebDialogLayer(sketch: Sketch2DWeb)
1012    def __init__(self, sketch: Sketch2DWeb):
1013        """"Initialize tkinter but hide the root window."""
1014        self._sketch = sketch

"Initialize tkinter but hide the root window.

def show_alert( self, message: str, callback: Optional[Callable[[], NoneType]] = None):
1016    def show_alert(self, message: str, callback: typing.Optional[typing.Callable[[], None]] = None):
1017        pyscript.window.alert(message)
1018        if callback is not None:
1019            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):
1021    def show_prompt(self, message: str,
1022        callback: typing.Optional[typing.Callable[[str], None]] = None):
1023        response = pyscript.window.prompt(message)
1024        if callback is not None and response is not None:
1025            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):
1027    def get_file_save_location(self,
1028        callback: typing.Optional[typing.Callable[[str], None]] = None):
1029        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):
1031    def get_file_load_location(self,
1032        callback: typing.Optional[typing.Callable[[str], None]] = None):
1033        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.