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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
Get a reference to the underlying native renderer object.
Returns:
Native render object.
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.
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.
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.
Show the sketch and quit immediatley afterwards.
Show the sketch to the user and quit immediately afterwards, a routine potentially useful for testing.
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.
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.
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.
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.
Inherited Members
- sketchingpy.abstracted.Sketch
- set_fill
- clear_fill
- set_stroke
- clear_stroke
- set_arc_mode
- set_ellipse_mode
- set_rect_mode
- draw_pixel
- start_shape
- set_stroke_weight
- set_text_align
- set_map_pan
- set_map_zoom
- set_map_placement
- convert_geo_to_pixel
- start_geo_polygon
- push_map
- pop_map
- parse_geojson
- set_image_mode
- set_angle_mode
- push_style
- pop_style
- get_millis_shown
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.
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.
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.
Get the alignment to use when drawing text.
Returns:
Renderer-specific value.
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.
Inherited Members
- sketchingpy.state_struct.SketchStateMachine
- set_fill
- get_fill
- get_fill_native
- get_fill_enabled
- clear_fill
- set_stroke
- get_stroke
- get_stroke_native
- get_stroke_enabled
- clear_stroke
- set_arc_mode
- get_arc_mode
- get_arc_mode_native
- set_ellipse_mode
- get_ellipse_mode
- get_ellipse_mode_native
- set_rect_mode
- get_rect_mode
- get_rect_mode_native
- set_stroke_weight
- get_stroke_weight
- get_stroke_weight_native
- set_text_font
- get_text_font
- get_text_align
- set_image_mode
- get_image_mode
- get_image_mode_native
- set_angle_mode
- get_angle_mode
- get_angle_mode_native
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.
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.
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.
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.
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.
Access the underlying native version of this image.
Returns:
Renderer specific native version.
Determine if this image has finished loading.
Returns:
True if loaded and ready to draw. False otherwise.
Inherited Members
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.