Explorar el Código

Merge pull request #2218 from zauberzeug/blank-interactive-image

Allow using `ui.interactive_image` without image
Rodja Trappe hace 1 año
padre
commit
e926909f59

+ 18 - 10
nicegui/elements/interactive_image.js

@@ -1,17 +1,17 @@
 export default {
   template: `
-    <div style="position:relative">
+    <div :style="{ position: 'relative', aspectRatio: size ? size[0] / size[1] : undefined }">
       <img
         ref="img"
         :src="computed_src"
-        style="width:100%; height:100%;"
+        :style="{ width: '100%', height: '100%', opacity: src ? 1 : 0 }"
         @load="onImageLoaded"
         v-on="onCrossEvents"
         v-on="onUserEvents"
         draggable="false"
       />
       <svg style="position:absolute;top:0;left:0;pointer-events:none" :viewBox="viewBox">
-        <g v-if="cross" :style="{ display: cssDisplay }">
+        <g v-if="cross" :style="{ display: showCross ? 'block' : 'none' }">
           <line :x1="x" y1="0" :x2="x" y2="100%" stroke="black" />
           <line x1="0" :y1="y" x2="100%" :y2="y" stroke="black" />
         </g>
@@ -25,7 +25,7 @@ export default {
       viewBox: "0 0 0 0",
       x: 100,
       y: 100,
-      cssDisplay: "none",
+      showCross: false,
       computed_src: undefined,
       waiting_source: undefined,
       loading: false,
@@ -60,19 +60,26 @@ export default {
         this.computed_src = new_src;
         this.loading = true;
       }
+      if (!this.src && this.size) {
+        this.viewBox = `0 0 ${this.size[0]} ${this.size[1]}`;
+      }
     },
     updateCrossHair(e) {
-      this.x = (e.offsetX * e.target.naturalWidth) / e.target.clientWidth;
-      this.y = (e.offsetY * e.target.naturalHeight) / e.target.clientHeight;
+      const width = this.src ? e.target.naturalWidth : this.size[0];
+      const height = this.src ? e.target.naturalHeight : this.size[1];
+      this.x = (e.offsetX * width) / e.target.clientWidth;
+      this.y = (e.offsetY * height) / e.target.clientHeight;
     },
     onImageLoaded(e) {
       this.viewBox = `0 0 ${e.target.naturalWidth} ${e.target.naturalHeight}`;
     },
     onMouseEvent(type, e) {
+      const width = this.src ? e.target.naturalWidth : this.size[0];
+      const height = this.src ? e.target.naturalHeight : this.size[1];
       this.$emit("mouse", {
         mouse_event_type: type,
-        image_x: (e.offsetX * e.target.naturalWidth) / e.target.clientWidth,
-        image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
+        image_x: (e.offsetX * width) / e.target.clientWidth,
+        image_y: (e.offsetY * height) / e.target.clientHeight,
         button: e.button,
         buttons: e.buttons,
         altKey: e.altKey,
@@ -86,8 +93,8 @@ export default {
     onCrossEvents() {
       if (!this.cross) return {};
       return {
-        mouseenter: () => (this.cssDisplay = "block"),
-        mouseleave: () => (this.cssDisplay = "none"),
+        mouseenter: () => (this.showCross = true),
+        mouseleave: () => (this.showCross = false),
         mousemove: (event) => this.updateCrossHair(event),
       };
     },
@@ -102,6 +109,7 @@ export default {
   props: {
     src: String,
     content: String,
+    size: Object,
     events: Array,
     cross: Boolean,
     t: String,

+ 8 - 2
nicegui/elements/interactive_image.py

@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import time
 from pathlib import Path
-from typing import Any, Callable, List, Optional, Union, cast
+from typing import Any, Callable, List, Optional, Tuple, Union, cast
 
 from .. import optional_features
 from ..events import GenericEventArguments, MouseEventArguments, handle_event
@@ -24,6 +24,7 @@ class InteractiveImage(SourceElement, ContentElement, component='interactive_ima
     def __init__(self,
                  source: Union[str, Path, 'PIL_Image'] = '', *,
                  content: str = '',
+                 size: Optional[Tuple[int, int]] = None,
                  on_mouse: Optional[Callable[..., Any]] = None,
                  events: List[str] = ['click'],
                  cross: bool = False,
@@ -36,8 +37,12 @@ class InteractiveImage(SourceElement, ContentElement, component='interactive_ima
         Thereby repeatedly updating the image source will automatically adapt to the available bandwidth.
         See `OpenCV Webcam <https://github.com/zauberzeug/nicegui/tree/main/examples/opencv_webcam/main.py>`_ for an example.
 
-        :param source: the source of the image; can be an URL, local file path or a base64 string
+        You can also pass a tuple of width and height instead of an image source.
+        This will create an empty image with the given size.
+
+        :param source: the source of the image; can be an URL, local file path, a base64 string or just an image size
         :param content: SVG content which should be overlaid; viewport has the same dimensions as the image
+        :param size: size of the image (width, height) in pixels; only used if `source` is not set
         :param on_mouse: callback for mouse events (yields `type`, `image_x` and `image_y`)
         :param events: list of JavaScript events to subscribe to (default: `['click']`)
         :param cross: whether to show crosshairs (default: `False`)
@@ -45,6 +50,7 @@ class InteractiveImage(SourceElement, ContentElement, component='interactive_ima
         super().__init__(source=source, content=content)
         self._props['events'] = events
         self._props['cross'] = cross
+        self._props['size'] = size
 
         def handle_mouse(e: GenericEventArguments) -> None:
             if on_mouse is None:

+ 15 - 2
website/documentation/content/interactive_image_documentation.py

@@ -5,9 +5,9 @@ from . import doc
 
 @doc.demo(ui.interactive_image)
 def main_demo() -> None:
-    from nicegui.events import MouseEventArguments
+    from nicegui import events
 
-    def mouse_handler(e: MouseEventArguments):
+    def mouse_handler(e: events.MouseEventArguments):
         color = 'SkyBlue' if e.type == 'mousedown' else 'SteelBlue'
         ii.content += f'<circle cx="{e.image_x}" cy="{e.image_y}" r="15" fill="none" stroke="{color}" stroke-width="4" />'
         ui.notify(f'{e.type} at ({e.image_x:.1f}, {e.image_y:.1f})')
@@ -38,4 +38,17 @@ def force_reload():
     ui.button('Force reload', on_click=img.force_reload)
 
 
+@doc.demo('Blank canvas', '''
+    You can also create a blank canvas with a given size.
+    This is useful if you want to draw something without loading a background image.
+''')
+def blank_canvas():
+    ui.interactive_image(
+        size=(800, 600), cross=True,
+        on_mouse=lambda e: e.sender.set_content(f'''
+            <circle cx="{e.image_x}" cy="{e.image_y}" r="50" fill="orange" />
+        '''),
+    ).classes('w-64 bg-blue-50')
+
+
 doc.reference(ui.interactive_image)