diff --git a/apps/perspectivator/README.md b/apps/perspectivator/README.md new file mode 100644 index 00000000..0838b1cf --- /dev/null +++ b/apps/perspectivator/README.md @@ -0,0 +1,37 @@ +``` + ____ __ __ +/\ _`\ /\ \__ __ /\ \__ +\ \ \L\ \ __ _ __ ____ _____ __ ___\ \ ,_\/\_\ __ __ __ \ \ ,_\ ___ _ __ + \ \ ,__/'__`\/\`'__\/',__\/\ '__`\ /'__`\ /'___\ \ \/\/\ \/\ \/\ \ /'__`\ \ \ \/ / __`\/\`'__\ + \ \ \/\ __/\ \ \//\__, `\ \ \L\ \/\ __//\ \__/\ \ \_\ \ \ \ \_/ |/\ \L\.\_\ \ \_/\ \L\ \ \ \/ + \ \_\ \____\\ \_\\/\____/\ \ ,__/\ \____\ \____\\ \__\\ \_\ \___/ \ \__/.\_\\ \__\ \____/\ \_\ + \/_/\/____/ \/_/ \/___/ \ \ \/ \/____/\/____/ \/__/ \/_/\/__/ \/__/\/_/ \/__/\/___/ \/_/ + \ \_\ + \/_/ +``` + + +![outImage perspectivator](https://github.com/user-attachments/assets/2718001c-96ef-4308-96f2-e9006f26c1d8) + +# Perspectivator + +**Perspectivator** is a tool designed to help you create stunning hero images for your software projects. With Perspectivator, you can easily compose multiple screenshots (or any image) onto a stylish, reflective surface, producing eye-catching visuals perfect for landing pages, presentations, or promotional materials. + +The app allows you to: +- Use as many images as you want. +- Arrange and transform them in perspective to simulate depth and realism. +- Automatically generate realistic reflections and subtle effects. +- Export high-quality hero images ready for use in your projects. + +Whether you're a developer, designer, or marketer, Perspectivator streamlines the process of producing professional hero images that showcase your software at its best. + +# Installation/Run +```bash +# Clone the git repo +git clone https://github.com/ceccopierangiolieugenio/pyTermTk.git +# Install the required packages +pip install numpy pillow +# Run Perspectivator +python apps/perspectivator/perspectivator.py +# Enjoy!!! +``` \ No newline at end of file diff --git a/apps/perspectivator/perspectivator.pil.py b/apps/perspectivator/perspectivator.pil.py new file mode 100644 index 00000000..e7b4a430 --- /dev/null +++ b/apps/perspectivator/perspectivator.pil.py @@ -0,0 +1,992 @@ +#!/usr/bin/env python3 +# MIT License +# +# Copyright (c) 2025 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys,os +import math +import argparse +import json + +from dataclasses import dataclass +from typing import Optional,Tuple,List,Dict,Any + +import numpy as np,array + +from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageOps + +sys.path.append(os.path.join(sys.path[0],'../..')) + +import TermTk as ttk + +@dataclass +class RenderData: + # fogNear: float + # fogFar: float + # bgColor: Tuple[int,int,int,int] + resolution: Tuple[int,int] + # outFile: str + # offY: int + # show: bool = False + # mirror: Tuple[int,int] + +class _ThreadingData: + __slots__ = ('timer') + timer: ttk.TTkTimer + def __init__(self): + self.timer = ttk.TTkTimer() + +def find_coeffs(pa, pb): + matrix = [] + for p1, p2 in zip(pa, pb): + matrix.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0]*p1[0], -p2[0]*p1[1]]) + matrix.append([0, 0, 0, p1[0], p1[1], 1, -p2[1]*p1[0], -p2[1]*p1[1]]) + + A = np.matrix(matrix, dtype=float) + B = np.array(pb).reshape(8) + + res = np.dot(np.linalg.inv(A.T * A) * A.T, B) + return np.array(res).reshape(8) + +def project_point(camera_position, look_at, point_3d): + """ + Project a 3D point into a normalized projection matrix. + + Args: + camera_position: The 3D position of the camera (x, y, z). + look_at: The 3D position the camera is looking at (x, y, z). + point_3d: The 3D point to project (x, y, z). + + Returns: + The 2D coordinates of the projected point in normalized space. + """ + # Step 1: Calculate the forward, right, and up vectors + def normalize(v): + return v / np.linalg.norm(v) + + forward = normalize(np.array(look_at) - np.array(camera_position)) + right = normalize(np.cross(forward, [0, 1, 0])) + if np.all(np.isnan(right)): + right = [0,1,0] + up = np.cross(right, forward) + + # Step 2: Create the view matrix + view_matrix = np.array([ + [ right[0] , right[1] , right[2] , -np.dot( right , camera_position)], + [ up[0] , up[1] , up[2] , -np.dot( up , camera_position)], + [-forward[0] , -forward[1] , -forward[2] , np.dot(forward , camera_position)], + [ 0 , 0 , 0 , 1] + ]) + + # Step 3: Create the projection matrix + near = 1.0 # Near plane normalized to 1 + width = 1.0 # Width of the near plane + height = 1.0 # Height of the near plane + aspect_ratio = width / height + + projection_matrix = np.array([ + [1 / aspect_ratio, 0, 0, 0], + [ 0, 1, 0, 0], + [ 0, 0, -1, -2 * near], + [ 0, 0, -1, 0] + ]) + + # Step 4: Transform the 3D point into clip space + point_3d_homogeneous = np.array([point_3d[0], point_3d[1], point_3d[2], 1]) + view_space_point = view_matrix @ point_3d_homogeneous + clip_space_point = projection_matrix @ view_space_point + + # Step 5: Perform perspective divide to get normalized device coordinates (NDC) + if clip_space_point[3] == 0: + raise ValueError("Invalid projection: w component is zero.") + ndc_x = clip_space_point[0] / clip_space_point[3] + ndc_y = clip_space_point[1] / clip_space_point[3] + + return ndc_x, ndc_y + +class _Toolbox(): + __slots__ = ('widget', 'updated') + updated:ttk.pyTTkSignal + + def __init__(self): + self.updated = ttk.pyTTkSignal() + self.widget = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"t.toolbox.tui.json")) + + @ttk.pyTTkSlot(str) + def _presetChanged(preset): + w,h = preset.split('x') + self.widget.getWidgetByName("SB_HRes").setValue(int(w)) + self.widget.getWidgetByName("SB_VRes").setValue(int(h)) + + pres = self.widget.getWidgetByName("CB_ResPresets") + pres.addItems([ + '320x200', '320x240', '400x300', '512x384', + '640x400', '640x480', '800x600', '1024x768', + '1152x864', '1280x720', '1280x800', '1280x960', + '1280x1024', '1360x768', '1366x768', '1400x1050', + '1440x900', '1600x900', '1600x1200', '1680x1050', + '1920x1080', '1920x1200', '2048x1152', '2048x1536', + '2560x1080', '2560x1440', '2560x1600', '2880x1800', + '3200x1800', '3440x1440', '3840x2160', '4096x2160', + '5120x2880', '7680x4320']) + pres.setCurrentIndex(6) + pres.currentTextChanged.connect(_presetChanged) + + aa = self.widget.getWidgetByName("CB_AA") + aa.addItems(['0X','2X','3X','4X']) + aa.setCurrentIndex(0) + + self.widget.getWidgetByName("SB_HRes").valueChanged.connect(self._triggerUpdated) + self.widget.getWidgetByName("SB_VRes").valueChanged.connect(self._triggerUpdated) + self.widget.getWidgetByName("CB_Bg").stateChanged.connect(self._triggerUpdated) + self.widget.getWidgetByName("BG_Color").colorSelected.connect(self._triggerUpdated) + self.widget.getWidgetByName("SB_Fog_Near").valueChanged.connect(self._triggerUpdated) + self.widget.getWidgetByName("SB_Fog_Far").valueChanged.connect(self._triggerUpdated) + self.widget.getWidgetByName("SB_OffY").valueChanged.connect(self._triggerUpdated) + self.widget.getWidgetByName("SB_FadingFrom").valueChanged.connect(self._triggerUpdated) + self.widget.getWidgetByName("SB_FadingTo").valueChanged.connect(self._triggerUpdated) + + @ttk.pyTTkSlot() + def _triggerUpdated(self): + self.updated.emit() + + def mirror(self) -> Tuple[float,float]: + return ( + self.widget.getWidgetByName("SB_FadingFrom").value() , + self.widget.getWidgetByName("SB_FadingTo").value() ) + + def bg(self) -> Tuple[int,int,int,int]: + if self.widget.getWidgetByName("CB_Bg").isChecked(): + btnColor = self.widget.getWidgetByName("BG_Color").color() + return (*btnColor.fgToRGB(),255) + else: + return (0,0,0,0) + + def fog(self) -> tuple[float,float]: + return ( + self.widget.getWidgetByName("SB_Fog_Near").value() , + self.widget.getWidgetByName("SB_Fog_Far").value() ) + + def resolution(self) -> Tuple[int,int]: + return( + self.widget.getWidgetByName("SB_HRes").value(), + self.widget.getWidgetByName("SB_VRes").value()) + + def offset_y(self) -> float: + return self.widget.getWidgetByName("SB_OffY").value() + + def serialize(self) -> Dict: + return { + 'bg':self.bg(), + 'fog':self.fog(), + 'resolution':self.resolution(), + 'offset_y': self.offset_y(), + 'mirror': self.mirror(), + } + + @staticmethod + def deserialize(data:Dict) -> '_Toolbox': + ret = _Toolbox() + ret.widget.getWidgetByName("SB_HRes").setValue(data['resolution'][0]) + ret.widget.getWidgetByName("SB_VRes").setValue(data['resolution'][1]) + if data['bg']==[0,0,0,0]: + ret.widget.getWidgetByName("CB_Bg").setChecked(False) + else: + ret.widget.getWidgetByName("CB_Bg").setChecked(True) + r = data['bg'][0] + g = data['bg'][1] + b = data['bg'][2] + color = ttk.TTkColor.fg(f"#{r<<16|g<<8|b:06x}") + ret.widget.getWidgetByName("BG_Color").setColor(color) + ret.widget.getWidgetByName("SB_Fog_Near").setValue(data['fog'][0]) + ret.widget.getWidgetByName("SB_Fog_Far").setValue(data['fog'][1]) + ret.widget.getWidgetByName("SB_OffY").setValue(data['offset_y']) + ret.widget.getWidgetByName("SB_OffY").setValue(data['offset_y']) + ret.widget.getWidgetByName("SB_FadingFrom").setValue(data['mirror'][0]) + ret.widget.getWidgetByName("SB_FadingTo").setValue(data['mirror'][1]) + return ret + +class _Movable(): + __slots__ = ('_x','_y','_z','data', 'selected', 'name','widget', 'updated') + _x:int + _y:int + _z:int + data:Dict[str,Any] + selected:ttk.pyTTkSignal + updated:ttk.pyTTkSignal + name:ttk.TTkLabel + widget:ttk.TTkWidget + + def __init__(self, x:int=0,y:int=0,z:int=0, name:str=""): + self.selected = ttk.pyTTkSignal(_Movable) + self.updated = ttk.pyTTkSignal() + self.data = {} + self._x = x + self._y = y + self._z = z + self.name = ttk.TTkLabel(text=ttk.TTkString(name),maxHeight=1) + self.widget = ttk.TTkWidget() + + def serialize(self) -> Dict: + return { + 'x':self._x, + 'y':self._y, + 'z':self._z, + 'name': self.name.text().toAscii() + } + + @staticmethod + def deserialize(data:Dict) -> '_Movable': + return _Movable(**data) + + def x(self) -> int: + return self._x + def setX(self, value:int): + self._x = value + self._updateBox() + self.updated.emit() + + def y(self) -> int: + return self._y + def setY(self, value:int): + self._y = value + self._updateBox() + self.updated.emit() + + def z(self) -> int: + return self._z + def setZ(self, value:int): + self._z = value + self._updateBox() + self.updated.emit() + + def getBox(self) -> Tuple[int,int,int,int]: + return (self.x-1,self._y,4,1) + + def _updateBox(self): + pass + +class _Image(_Movable): + __slots__ = ('_size','_tilt','fileName', 'box', 'image') + _size:int + _tilt:float + fileName:str + image:Image + box:Tuple[int,int,int,int] + def __init__(self,size:int,tilt:float,fileName:str, **kwargs): + self._size = size + self._tilt = tilt + self.fileName = fileName + super().__init__(**kwargs) + self._loadStuff() + + def _loadStuff(self): + self.widget = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"t.image.tui.json")) + if not os.path.isfile(self.fileName): + raise ValueError(f"{self.fileName} is not a file") + try: + self.image = Image.open(self.fileName).convert("RGBA") + except FileNotFoundError: + raise ValueError(f"Failed to open {self.fileName}") + self._updateBox() + self.widget.getWidgetByName("SB_X").valueChanged.connect(self.setX) + self.widget.getWidgetByName("SB_Y").valueChanged.connect(self.setY) + self.widget.getWidgetByName("SB_Z").valueChanged.connect(self.setZ) + self.widget.getWidgetByName("SB_Tilt").valueChanged.connect(self.setTilt) + self.widget.getWidgetByName("SB_Size").valueChanged.connect(self.setSize) + self.updated.connect(self._updated) + self._updated() + + def serialize(self) -> Dict: + ret = super().serialize() + return ret | { + 'size': self._size, + 'tilt': self._tilt, + 'fileName': self.fileName + } + + @staticmethod + def deserialize(data:Dict) -> '_Image': + return _Image(**data) + + @ttk.pyTTkSlot() + def _updated(self): + self.widget.getWidgetByName("SB_X").setValue(self._x) + self.widget.getWidgetByName("SB_Y").setValue(self._y) + self.widget.getWidgetByName("SB_Z").setValue(self._z) + self.widget.getWidgetByName("SB_Tilt").setValue(self._tilt) + self.widget.getWidgetByName("SB_Size").setValue(self._size) + + def size(self) -> int: + return self._size + def setSize(self, value:int): + self._size = value + self._updateBox() + self.updated.emit() + + def tilt(self) -> float: + return self._tilt + def setTilt(self, value:float): + self._tilt = value + self._updateBox() + self.updated.emit() + + def _updateBox(self): + size = float(self._size) + w = 1 + 2*size*abs(math.cos(self._tilt)) + h = 1 + size*abs(math.sin(self._tilt)) + self.box = ( + int(self._x-w/2), + int(self._y-h/2), + int(w), int(h), + ) + + def getBox(self) -> Tuple[int,int,int,int]: + return self.box + +class _Camera(_Movable): + __slots__= ('_tilt') + _tilt:float + def __init__(self, tilt:float=0, **kwargs): + self._tilt = tilt + super().__init__(**kwargs) + self._loadStuff() + + def _loadStuff(self): + self.widget = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"t.camera.tui.json")) + self.widget.getWidgetByName("SB_X").valueChanged.connect(self.setX) + self.widget.getWidgetByName("SB_Y").valueChanged.connect(self.setY) + self.widget.getWidgetByName("SB_Z").valueChanged.connect(self.setZ) + self.widget.getWidgetByName("SB_Tilt").valueChanged.connect(self.setTilt) + self.updated.connect(self._updated) + self._updated() + + def serialize(self) -> Dict: + ret = super().serialize() + return ret | { + 'tilt': self._tilt, + } + + @staticmethod + def deserialize(data:Dict) -> '_Camera': + return _Camera(**data) + + @ttk.pyTTkSlot() + def _updated(self): + self.widget.getWidgetByName("SB_X").setValue(self._x) + self.widget.getWidgetByName("SB_Y").setValue(self._y) + self.widget.getWidgetByName("SB_Z").setValue(self._z) + self.widget.getWidgetByName("SB_Tilt").setValue(self._tilt) + + def tilt(self) -> float: + return self._tilt + def setTilt(self, value:float): + self._tilt = value + self.updated.emit() + +class _State(): + __slots__ = ( + 'camera','images', 'toolbox', + '_currentMovable','highlightedMovable', + 'currentMovableUpdated','updated') + camera: _Camera + images: List[_Image] + _currentMovable: Optional[_Movable] + highlightedMovable: Optional[_Movable] + currentMovableUpdated: ttk.pyTTkSignal + updated: ttk.pyTTkSignal + def __init__(self, camera: _Camera, images: List[_Image], toolbox: Optional[_Toolbox]=None): + if not toolbox: + self.toolbox = _Toolbox() + else: + self.toolbox = toolbox + self.currentMovableUpdated = ttk.pyTTkSignal(_Movable) + self.updated = ttk.pyTTkSignal() + self.camera = camera + self.images = images + self._currentMovable = None + self.highlightedMovable = None + self.camera.updated.connect(self.updated.emit) + self.toolbox.updated.connect(self.updated.emit) + for img in images: + img.updated.connect(self.updated.emit) + + @property + def currentMovable(self) -> Optional[_Movable]: + return self._currentMovable + @currentMovable.setter + def currentMovable(self, value: Optional[_Movable]): + if self._currentMovable != value: + self._currentMovable = value + if value: + self.currentMovableUpdated.emit(value) + + @ttk.pyTTkSlot(str) + def save(self, fileName): + data = { + 'camera': self.camera.serialize(), + 'images': [img.serialize() for img in self.images], + 'toolbox': self.toolbox.serialize() + } + with open(fileName, "w") as f: + json.dump(data, f, indent=4) + + @staticmethod + def load(fileName) -> '_State': + with open(fileName, "r") as f: + data = json.load(f) + toolbox = _Toolbox.deserialize(data['toolbox']) + images = [] + for imgData in data['images']: + images.append(_Image.deserialize(imgData)) + state = _State( + toolbox=toolbox, + camera=_Camera.deserialize(data['camera']), + images=images) + + return state + +class Perspectivator(ttk.TTkWidget): + __slots__ = ('_state') + _state:_State + def __init__(self, state:_State, **kwargs): + self._state = state + super().__init__(**kwargs) + self.setFocusPolicy(ttk.TTkK.ClickFocus) + state.updated.connect(self.update) + + def mousePressEvent(self, evt): + self._state.highlightedMovable = None + self._state.currentMovable = None + cx,cy = self._state.camera.x(),self._state.camera.y() + if cx-1 <= evt.x <= cx+2 and cy-1 <= evt.y <= cy+1: + self._state.currentMovable = self._state.camera + self._state.camera.data |= { + 'mx':self._state.camera.x()-evt.x, + 'my':self._state.camera.y()-evt.y} + else: + for image in self._state.images: + ix,iy,iw,ih = image.getBox() + if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: + image.data |= { + 'mx':image.x()-evt.x, + 'my':image.y()-evt.y} + self._state.currentMovable = image + index = self._state.images.index(image) + self._state.images.pop(index) + self._state.images.append(image) + break + if self._state.currentMovable: + self._state.currentMovable.selected.emit(self._state.currentMovable) + self.update() + return True + + def mouseMoveEvent(self, evt:ttk.TTkMouseEvent): + self._state.highlightedMovable = None + cx,cy = self._state.camera.x(),self._state.camera.y() + if cx-1 <= evt.x <= cx+2 and cy-1 <= evt.y <= cy+1: + self._state.highlightedMovable = self._state.camera + else: + for image in self._state.images: + ix,iy,iw,ih = image.getBox() + if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: + self._state.highlightedMovable = image + break + self.update() + return True + + def mouseReleaseEvent(self, evt:ttk.TTkMouseEvent): + self._state.highlightedMovable = None + self._state.currentMovable = None + self.update() + return True + + def mouseDragEvent(self, evt:ttk.TTkMouseEvent): + if not (movable:=self._state.currentMovable): + pass + elif evt.key == ttk.TTkK.RightButton and isinstance(movable,_Image): + x = evt.x-movable.x() + y = evt.y-movable.y() + movable.setTilt(math.atan2(x,y*2)) + self.update() + elif evt.key == ttk.TTkK.LeftButton: + mx,my = movable.data['mx'],movable.data['my'] + movable.setX(evt.x+mx) + movable.setY(evt.y+my) + self.update() + return True + + def wheelEvent(self, evt:ttk.TTkMouseEvent) -> bool: + if not ((image:=self._state.highlightedMovable) and isinstance(image,_Image)): + pass + elif evt.evt == ttk.TTkK.WHEEL_Up: + image.setSize(min(image.size()+1,50)) + self.update() + elif evt.evt == ttk.TTkK.WHEEL_Down: + image.setSize(max(image.size()-1,5)) + self.update() + return True + + def getImage(self, data:RenderData) -> Image: + return self.getImagePil(data) + + def getImagePil(self, data:RenderData) -> Image: + screen_width, screen_height = data.resolution + w,h = screen_width,screen_height + + fog = self._state.toolbox.fog() + fogNear=fog[0] + fogFar=fog[1] + bgColor=self._state.toolbox.bg() + offY=self._state.toolbox.offset_y()*h/600 + mirror = self._state.toolbox.mirror() + + ww,wh = self.size() + cam_x = self._state.camera.x() + cam_y = self._state.camera.y() + cam_z = self._state.camera.z() + cam_tilt = self._state.camera.tilt() + observer = (cam_x , cam_z , cam_y ) # Observer's position + # observer = (ww/2 , -data.cameraY , wh-3 ) # Observer's position + dz = 10*math.cos(math.pi * cam_tilt/360) + dy = 10*math.sin(math.pi * cam_tilt/360) + look_at = (cam_x , cam_z-dy, cam_y+dz) # Observer is looking along the positive Z-axis + + prw,prh = screen_width/2, screen_height/2 + + # step1, sort Images based on the distance + images = sorted(self._state.images,key=lambda img:img.getBox()[1]) + znear,zfar = 0xFFFFFFFF,-0xFFFFFFFF + for img in images: + ix,iy,iw,ih = img.getBox() + iz = img.z() + ih-=1 + znear=min(znear,iy,iy+ih) + zfar=max(zfar,iy,iy+ih) + isz = img.size()*2 + if math.pi/2 <= img.tilt() < math.pi or -math.pi <= img.tilt() < 0: + zleft = iy + zright = iy+ih + ip1x,ip1y = project_point(observer,look_at,(ix+iw , iz+isz , iy+ih )) + ip2x,ip2y = project_point(observer,look_at,(ix , iz+isz , iy )) + ip3x,ip3y = project_point(observer,look_at,(ix+iw , iz , iy+ih )) + ip4x,ip4y = project_point(observer,look_at,(ix , iz , iy )) + ip5x,ip5y = project_point(observer,look_at,(ix+iw , -iz-isz , iy+ih )) + ip6x,ip6y = project_point(observer,look_at,(ix , -iz-isz , iy )) + ip7x,ip7y = project_point(observer,look_at,(ix+iw , -iz , iy+ih )) + ip8x,ip8y = project_point(observer,look_at,(ix , -iz , iy )) + else: + zleft = iy+ih + zright = iy + ip1x,ip1y = project_point(observer,look_at,(ix+iw , iz+isz , iy )) + ip2x,ip2y = project_point(observer,look_at,(ix , iz+isz , iy+ih )) + ip3x,ip3y = project_point(observer,look_at,(ix+iw , iz , iy )) + ip4x,ip4y = project_point(observer,look_at,(ix , iz , iy+ih )) + ip5x,ip5y = project_point(observer,look_at,(ix+iw , -iz-isz , iy )) + ip6x,ip6y = project_point(observer,look_at,(ix , -iz-isz , iy+ih )) + ip7x,ip7y = project_point(observer,look_at,(ix+iw , -iz , iy )) + ip8x,ip8y = project_point(observer,look_at,(ix , -iz , iy+ih )) + img.data |= { + 'zleft':zleft, + 'zright':zright, + 'top' : { + 'p1':(int((ip1x+1)*prw) , int(offY+(ip1y+1)*prh)), + 'p2':(int((ip2x+1)*prw) , int(offY+(ip2y+1)*prh)), + 'p3':(int((ip3x+1)*prw) , int(offY+(ip3y+1)*prh)), + 'p4':(int((ip4x+1)*prw) , int(offY+(ip4y+1)*prh)), + }, + 'bottom' : { + 'p1':(int((ip5x+1)*prw) , int(offY+(ip5y+1)*prh)), + 'p2':(int((ip6x+1)*prw) , int(offY+(ip6y+1)*prh)), + 'p3':(int((ip7x+1)*prw) , int(offY+(ip7y+1)*prh)), + 'p4':(int((ip8x+1)*prw) , int(offY+(ip8y+1)*prh)), + } + + } + + # step2, get all the layers and masks for alla the images + for img in images: + image = img.image + + imageTop = image.copy() + imageBottom = image.copy() + imageTopAlpha = imageTop.split()[-1] + imageBottomAlpha = imageBottom.split()[-1] + + imw, imh = image.size + + # Create a gradient mask for the mirrored image + gradient = Image.new("L", (imw, imh), 0) + draw = ImageDraw.Draw(gradient) + # Fading Math: + # ----|--f--y___h-----t--- + # av = 1-(y-f)/(t-f) + # bv = 1-(y+h-f)/(t-f) + _f,_t = mirror + if _f == _t: + _f-=0.01 + _y = img.z() + _h = img.size()*imh/imw + _av = 1-(-_y-_f)/(_t-_f) + _bv = 1-(-_y-_h-_f)/(_t-_f) + for i in range(imh): + _p = (i/imh) + alpha = int(min(1,max(0,((_p)*_av+(1-_p)*_bv)))*255) # alpha goes from 0 to 204 + draw.rectangle((0, i, imw, i), fill=alpha) + # Apply the mirror mask to the image + imageBottomAlphaGradient = ImageChops.multiply(imageBottomAlpha, gradient) + + # Create a gradient mask for the fog + gradient = Image.new("L", (imw, imh), 0) + draw = ImageDraw.Draw(gradient) + for i in range(imw): + an = 255-fogNear + af = 255-fogFar + zl = img.data['zleft'] + zr = img.data['zright'] + zi = (i/imw)*(zr-zl)+zl + znorm = (zi-znear)/(zfar-znear) + alpha = znorm*(an-af)+af + draw.rectangle((i, 0, i, imh), fill=int(alpha)) + # resultAlpha.show() + imageTop.putalpha(ImageChops.multiply(imageTopAlpha, gradient)) + imageBottom.putalpha(ImageChops.multiply(imageBottomAlphaGradient, gradient)) + + # Define the source and destination points + src_points = [(imw, 0), (0, 0), (imw, imh), (0, imh)] + dst_top = [ + img.data['top']['p1'], + img.data['top']['p2'], + img.data['top']['p3'], + img.data['top']['p4'], + ] + dst_bottom = [ + img.data['bottom']['p1'], + img.data['bottom']['p2'], + img.data['bottom']['p3'], + img.data['bottom']['p4'], + ] + def _transform(_img:Image, _dst:List) -> Image: + return _img.transform( + (w, h), Image.Transform.PERSPECTIVE, + find_coeffs(_dst, src_points), + Image.Resampling.BICUBIC) + blurRadius = (math.sqrt(w*h)*4/math.sqrt(800*600)) + img.data['imageTop'] = _transform(imageTop, dst_top) + img.data['imageBottom'] = _transform(imageBottom, dst_bottom).filter(ImageFilter.BoxBlur(blurRadius)) + img.data['imageTopAlpha'] = _transform(imageTopAlpha, dst_top) + img.data['imageBottomAlpha'] = _transform(imageBottomAlpha, dst_bottom).filter(ImageFilter.BoxBlur(blurRadius)) + + # def _customBlur(_img:Image, _alpha:Image) -> Image: + # thresholds = [(0,70), (70,150), (150,255)] + # blur_radius = [7, 4, 0] + # _out = Image.new("RGBA", _img.size, (0, 0, 0, 0)) + # # Create a new image to store the blurred result + # for (_f,_t),_r in zip(thresholds,blur_radius): + # # Create a mask for the current threshold + # _mask = _alpha.point(lambda p: p if _f < p <= _t else 0) + # # Apply Gaussian blur to the image using the mask + # _blurred = _img.filter(ImageFilter.BoxBlur(radius=_r)) + # _blurred.putalpha(_mask) + # # Composite the blurred image with the original image using the mask + # _out = Image.alpha_composite(_out,_blurred) + # #_alpha.show() + # #_mask.show() + # #_blurred.show() + # #_out.show() + # pass + # return _out + + # img.data['imageBottom'] = _customBlur( + # img.data['imageBottom'], img.data['imageBottom'].split()[-1]) + + # return image + # Apply blur to the alpha channel + # alpha = image.split()[-1] + # alpha = alpha.filter(ImageFilter.GaussianBlur(radius=5)) + # image.putalpha(alpha) + # image = image.filter(ImageFilter.GaussianBlur(radius=3)) + + # Paste the processed image onto the output image + + # Create a new image with a transparent background + outImage = Image.new("RGBA", (w, h), bgColor) + + # Apply the masks and Draw all the images + for img in images: + imageTop = img.data['imageTop'] + imageBottom = img.data['imageBottom'] + imageTopAlpha = imageTop.split()[-1] + imageBottomAlpha = imageBottom.split()[-1] + for maskImg in reversed(images): + if img==maskImg: + break + maskTop = ImageOps.invert(maskImg.data['imageTopAlpha']) + maskBottom = ImageOps.invert(maskImg.data['imageBottomAlpha']) + imageTopAlpha = ImageChops.multiply(imageTopAlpha, maskTop) + imageBottomAlpha = ImageChops.multiply(imageBottomAlpha, maskTop) + imageBottomAlpha = ImageChops.multiply(imageBottomAlpha, maskBottom) + + + imageTop.putalpha(imageTopAlpha) + imageBottom.putalpha(imageBottomAlpha) + + # imageBottom.show() + + # outImage.paste(imageBottom,box=None,mask=imageBottom) + # outImage.paste(imageTop,box=None,mask=imageTop) + + outImage = Image.alpha_composite(outImage,imageBottom) + outImage = Image.alpha_composite(outImage,imageTop) + # imageBottom.show() + + return outImage + + def paintEvent(self, canvas): + w,h = self.size() + cx,cy=self._state.camera.x(),self._state.camera.y() + if self._state.highlightedMovable == self._state.camera: + canvas.drawTTkString(pos=(cx-1,cy),text=ttk.TTkString("<😘>")) + elif self._state.currentMovable == self._state.camera: + canvas.drawTTkString(pos=(cx,cy),text=ttk.TTkString("😍")) + else: + canvas.drawTTkString(pos=(cx,cy),text=ttk.TTkString("😎")) + + # Draw Fov + for y in range(cy): + canvas.drawChar(char='/', pos=(cx+cy-y+1,y)) + canvas.drawChar(char='\\',pos=(cx-cy+y,y)) + + # Draw Images + for image in self._state.images: + ix,iy,iw,ih = image.getBox() + canvas.drawText(pos=(ix,iy-1),text=f"{image.tilt():.2f}", color=ttk.TTkColor.YELLOW) + canvas.drawText(pos=(ix+5,iy-1),text=f"{image.fileName}", color=ttk.TTkColor.CYAN) + if image == self._state.highlightedMovable: + canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.GREEN) + elif image == self._state.currentMovable: + canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.YELLOW) + else: + canvas.fill(pos=(ix,iy),size=(iw,ih),char='+') + if ih > iw > 1: + for dy in range(ih): + dx = iw*dy//ih + if ( + math.pi/2 < image.tilt() < math.pi or + -math.pi/2 < image.tilt() < 0 ): + canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) + else: + canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) + elif iw >= ih > 1: + for dx in range(iw): + dy = ih*dx//iw + if ( + math.pi/2 < image.tilt() < math.pi or + -math.pi/2 < image.tilt() < 0 ): + canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) + else: + canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) + +class _Preview(ttk.TTkWidget): + __slots__ = ('_canvasImage') + def __init__(self, **kwargs): + self._canvasImage = ttk.TTkCanvas(width=20,height=3) + self._canvasImage.drawText(pos=(0,0),text="Preview...") + super().__init__(**kwargs) + + def updateCanvas(self, img:Image): + w,h = img.size + pixels = img.load() + self._canvasImage.resize(w,h//2) + self._canvasImage.updateSize() + for x in range(w): + for y in range(h//2): + bg = 100 if (x//4+y//2)%2 else 150 + p1 = pixels[x,y*2] + p2 = pixels[x,y*2+1] + def _c2hex(p) -> str: + a = p[3]/255 + r = int(bg*(1-a)+a*p[0]) + g = int(bg*(1-a)+a*p[1]) + b = int(bg*(1-a)+a*p[2]) + return f"#{r<<16|g<<8|b:06x}" + c = ttk.TTkColor.fgbg(_c2hex(p2),_c2hex(p1)) + self._canvasImage.drawChar(pos=(x,y), char='▄', color=c) + self.update() + + def paintEvent(self, canvas): + w,h = self.size() + canvas.paintCanvas(self._canvasImage, (0,0,w,h), (0,0,w,h), (0,0,w,h)) + +class ControlPanel(ttk.TTkSplitter): + __slots__ = ( + 'previewPressed','renderPressed','_toolbox', '_previewImage', '_state', '_movableLayout','_threadData') + def __init__(self, state:_State, **kwargs): + self._threadData:_ThreadingData = _ThreadingData() + self.previewPressed = ttk.pyTTkSignal(RenderData) + self.renderPressed = ttk.pyTTkSignal(RenderData) + self._movableLayout = ttk.TTkGridLayout() + self._movableLayout.addItem(ttk.TTkLayout(),2,0) + self._state = state + super().__init__(**kwargs|{"orientation":ttk.TTkK.VERTICAL}) + self._previewImage = _Preview() + + self._state.toolbox.widget.getWidgetByName("Btn_Render").clicked.connect(self._renderClicked) + self._state.toolbox.widget.getWidgetByName("Btn_Preview").clicked.connect(self._previewChanged) + self._state.toolbox.widget.getWidgetByName("Btn_SaveCfg").clicked.connect(self._saveCfg) + self._state.updated.connect(self._previewChanged) + self.addWidget(self._previewImage,size=5) + self.addWidget(self._state.toolbox.widget) + self.addItem(self._movableLayout) + state.currentMovableUpdated.connect(self._movableChanged) + self._threadData.timer.timeout.connect(self._previewThread) + + @ttk.pyTTkSlot(_Movable) + def _movableChanged(self, movable:_Movable): + if isinstance(movable,_Movable): + self._movableLayout.addWidget(movable.name,0,0) + self._movableLayout.addWidget(movable.widget,1,0) + else: + raise ValueError(f"Unknown movable {movable}") + + def drawPreview(self, img:Image): + self._previewImage.updateCanvas(img) + + @ttk.pyTTkSlot() + def _saveCfg(self): + filePath = os.path.join(os.path.abspath('.'),'perspectivator.cfg.json') + filePicker = ttk.TTkFileDialogPicker( + pos = (3,3), size=(80,30), + acceptMode=ttk.TTkK.AcceptMode.AcceptSave, + caption="Save As...", + fileMode=ttk.TTkK.FileMode.AnyFile, + path=filePath, + filter="TTk Tui Files (*.cfg.json);;Json Files (*.json);;All Files (*)") + filePicker.pathPicked.connect(self._state.save) + ttk.TTkHelper.overlay(None, filePicker, 5, 5, True) + + @ttk.pyTTkSlot() + def _renderClicked(self): + w,h = self._state.toolbox.resolution() + # fog = self._state.toolbox.fog() + mult = { + '0X':1,'2X':2,'3X':3,'4X':4}.get( + self._state.toolbox.widget.getWidgetByName("CB_AA").currentText(),1) + waa = w*mult + haa = h*mult + data = RenderData( + # outFile=self._state.toolbox.widget.getWidgetByName("LE_OutFile").text().toAscii(), + # fogNear=fog[0], + # fogFar=fog[1], + # bgColor=self._state.toolbox.bg(), + resolution=(waa,haa), + # offY=self._state.toolbox.offset_y(), + # show = self._state.toolbox.widget.getWidgetByName("CB_Show").isChecked(), + # mirror = self._state.toolbox.mirror() + ) + self.renderPressed.emit(data) + + @ttk.pyTTkSlot() + def _previewChanged(self): + if not self._state.toolbox.widget.getWidgetByName("Btn_Preview").isChecked(): + return + self._threadData.timer.start() + + def _previewThread(self): + w,h = self._previewImage.size() + # fog = self._state.toolbox.fog() + data = RenderData( + # outFile=self._state.toolbox.widget.getWidgetByName("LE_OutFile").text().toAscii(), + # fogNear=fog[0], + # fogFar=fog[1], + # bgColor=self._state.toolbox.bg(), + resolution=(w,h*2), + # offY=self._state.toolbox.offset_y(), + # show = self._state.toolbox.widget.getWidgetByName("CB_Show").isChecked(), + # mirror = self._state.toolbox.mirror() + ) + self.previewPressed.emit(data) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('filename', type=str, nargs='+', + help='the images to compose or the json config file') + args = parser.parse_args() + + ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) + + root = ttk.TTk( + layout=ttk.TTkGridLayout(), + title="TTkode", + mouseTrack=True) + + + if len(args.filename) == 1 and args.filename[0].endswith('.json'): + _state = _State.load(args.filename[0]) + else: + _images = [] + _camera = _Camera(x=25,y=25,z=5, name="Camera") + for fileName in args.filename: + d=len(_images)*2 + image = _Image(x=25+d,y=15+d, z=0, size=5, tilt=0,fileName=fileName, name=fileName) + _camera.setX(_camera.x()+1) + _camera.setY(_camera.y()+1) + _images.append(image) + _state = _State( + camera=_camera, + images=_images) + + perspectivator = Perspectivator(state=_state) + controlPanel = ControlPanel(state=_state) + + at = ttk.TTkAppTemplate() + at.setWidget(widget=perspectivator,position=at.MAIN) + at.setWidget(widget=controlPanel,position=at.RIGHT, size=30) + + root.layout().addWidget(at) + + def _render(data:RenderData): + outImage = perspectivator.getImage(data) + # outImage.save(filename='outImage.png') + + bbox = outImage.getbbox() + if bbox: + outImage = outImage.crop(bbox) + + outw,outh = _state.toolbox.resolution() + cropw,croph = outImage.size + + outImage = outImage.resize((outw,outh*croph//cropw), Image.LANCZOS) + outImage.save(_state.toolbox.widget.getWidgetByName("LE_OutFile").text().toAscii()) + if _state.toolbox.widget.getWidgetByName("CB_Show").isChecked(): + outImage.show() + + def _preview(data): + img = perspectivator.getImage(data) + controlPanel.drawPreview(img) + + controlPanel.renderPressed.connect(_render) + controlPanel.previewPressed.connect(_preview) + + root.mainloop() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/apps/perspectivator/perspectivator.py b/apps/perspectivator/perspectivator.py new file mode 120000 index 00000000..552b5e7f --- /dev/null +++ b/apps/perspectivator/perspectivator.py @@ -0,0 +1 @@ +perspectivator.pil.py \ No newline at end of file diff --git a/apps/perspectivator/perspectivator.wand.py b/apps/perspectivator/perspectivator.wand.py new file mode 100644 index 00000000..7205fd82 --- /dev/null +++ b/apps/perspectivator/perspectivator.wand.py @@ -0,0 +1,798 @@ +# MIT License +# +# Copyright (c) 2025 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys,os +import math +import argparse +from dataclasses import dataclass +from typing import Optional,Tuple,List,Dict + +import numpy as np + +from wand.image import Image +from wand.drawing import Drawing +from wand.color import Color + +# from PIL import Image, ImageDraw, ImageFilter + + + + +sys.path.append(os.path.join(sys.path[0],'../..')) + +import TermTk as ttk + +@dataclass +class RenderData: + cameraY: int + cameraAngle: float + bgColor: Tuple[int,int,int,int] + resolution: Tuple[int,int] + +def project_point(camera_position, look_at, point_3d): + """ + Project a 3D point into a normalized projection matrix. + + Args: + camera_position: The 3D position of the camera (x, y, z). + look_at: The 3D position the camera is looking at (x, y, z). + point_3d: The 3D point to project (x, y, z). + + Returns: + The 2D coordinates of the projected point in normalized space. + """ + # Step 1: Calculate the forward, right, and up vectors + def normalize(v): + return v / np.linalg.norm(v) + + forward = normalize(np.array(look_at) - np.array(camera_position)) + right = normalize(np.cross(forward, [0, 1, 0])) + up = np.cross(right, forward) + + # Step 2: Create the view matrix + view_matrix = np.array([ + [ right[0] , right[1] , right[2] , -np.dot( right , camera_position)], + [ up[0] , up[1] , up[2] , -np.dot( up , camera_position)], + [-forward[0] , -forward[1] , -forward[2] , np.dot(forward , camera_position)], + [ 0 , 0 , 0 , 1] + ]) + + # Step 3: Create the projection matrix + near = 1.0 # Near plane normalized to 1 + width = 1.0 # Width of the near plane + height = 1.0 # Height of the near plane + aspect_ratio = width / height + + projection_matrix = np.array([ + [1 / aspect_ratio, 0, 0, 0], + [ 0, 1, 0, 0], + [ 0, 0, -1, -2 * near], + [ 0, 0, -1, 0] + ]) + + # Step 4: Transform the 3D point into clip space + point_3d_homogeneous = np.array([point_3d[0], point_3d[1], point_3d[2], 1]) + view_space_point = view_matrix @ point_3d_homogeneous + clip_space_point = projection_matrix @ view_space_point + + # Step 5: Perform perspective divide to get normalized device coordinates (NDC) + if clip_space_point[3] == 0: + raise ValueError("Invalid projection: w component is zero.") + ndc_x = clip_space_point[0] / clip_space_point[3] + ndc_y = clip_space_point[1] / clip_space_point[3] + + return ndc_x, ndc_y + +def project_3d_to_2d(observer, look_at, fov_h, fov_v, screen_width, screen_height, point_3d): + """ + Project a 3D point onto a 2D screen. + + Args: + observer: The observer's position in 3D space (x, y, z). + look_at: The point the observer is looking at in 3D space (x, y, z). + fov_h: Horizontal field of view in radians. + fov_v: Vertical field of view in radians. + screen_width: Width of the 2D screen. + screen_height: Height of the 2D screen. + point_3d: The 3D point to project (x, y, z). + + Returns: + The 2D coordinates of the projected point (x, y) on the screen. + """ + # Step 1: Calculate the forward, right, and up vectors + def normalize(v): + length = math.sqrt(sum(coord ** 2 for coord in v)) + return tuple(coord / length for coord in v) + + forward = normalize((look_at[0] - observer[0], look_at[1] - observer[1], look_at[2] - observer[2])) + right = normalize(( + forward[1] * 0 - forward[2] * 1, + forward[2] * 0 - forward[0] * 0, + forward[0] * 1 - forward[1] * 0 + )) + up = ( + right[1] * forward[2] - right[2] * forward[1], + right[2] * forward[0] - right[0] * forward[2], + right[0] * forward[1] - right[1] * forward[0] + ) + + # Step 2: Transform the 3D point into the observer's coordinate system + relative_point = ( + point_3d[0] - observer[0], + point_3d[1] - observer[1], + point_3d[2] - observer[2] + ) + x_in_view = sum(relative_point[i] * right[i] for i in range(3)) + y_in_view = sum(relative_point[i] * up[i] for i in range(3)) + z_in_view = sum(relative_point[i] * forward[i] for i in range(3)) + + # Step 3: Perform perspective projection + if z_in_view <= 0: + raise ValueError("The point is behind the observer and cannot be projected.") + + aspect_ratio = screen_width / screen_height + tan_fov_h = math.tan(fov_h / 2) + tan_fov_v = math.tan(fov_v / 2) + + ndc_x = x_in_view / (z_in_view * tan_fov_h * aspect_ratio) + ndc_y = y_in_view / (z_in_view * tan_fov_v) + + # Step 4: Map normalized device coordinates (NDC) to screen coordinates + screen_x = (ndc_x + 1) / 2 * screen_width + screen_y = (1 - ndc_y) / 2 * screen_height + + return int(screen_x), int(screen_y) + +class _Image(): + __slots__ = ('x','y','size','tilt','fileName', 'box', 'data') + x:int + y:int + size:int + tilt:float + fileName:str + data:Dict + box:Tuple[int,int,int,int] + def __init__(self,x:int,y:int,size:int,tilt:float,fileName:str): + if not os.path.isfile(fileName): + raise ValueError(f"{fileName} is not a file") + self.x = x + self.y = y + self.size = size + self.tilt = tilt + self.fileName = fileName + self.data={} + self._updateBox() + + def _updateBox(self): + size:float = float(self.size) + w:float = 1 + 2*size*abs(math.cos(self.tilt)) + h:float = 1 + size*abs(math.sin(self.tilt)) + self.box = ( + int(self.x-w/2), + int(self.y-h/2), + int(w), int(h), + ) + + def getBox(self) -> Tuple[int,int,int,int]: + return self.box + +class Perspectivator(ttk.TTkWidget): + __slots__ = ('_images','_currentImage','_highlightedImage') + _highlightedImage:Optional[_Image] + _currentImage:Optional[_Image] + _images:List[_Image] + def __init__(self, **kwargs): + self._highlightedImage = None + self._currentImage = None + self._images = [] + super().__init__(**kwargs) + self.setFocusPolicy(ttk.TTkK.ClickFocus) + + def addImage(self, fileName:str): + d=len(self._images)*2 + image = _Image(x=25+d,y=5+d, size=5, tilt=0,fileName=fileName) + self._images.append(image) + self.update() + + def mousePressEvent(self, evt): + self._highlightedImage = None + self._currentImage = None + for image in self._images: + ix,iy,iw,ih = image.getBox() + if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: + image.data = { + 'mx':image.x-evt.x, + 'my':image.y-evt.y} + self._currentImage = image + index = self._images.index(image) + self._images.pop(index) + self._images.append(image) + break + self.update() + return True + + def mouseMoveEvent(self, evt:ttk.TTkMouseEvent): + self._highlightedImage = None + for image in self._images: + ix,iy,iw,ih = image.getBox() + if ix <= evt.x < ix+iw and iy <= evt.y < iy+ih: + self._highlightedImage = image + break + self.update() + return True + + def mouseReleaseEvent(self, evt:ttk.TTkMouseEvent): + self._highlightedImage = None + self._currentImage = None + self.update() + return True + + def mouseDragEvent(self, evt:ttk.TTkMouseEvent): + if not (image:=self._currentImage): + pass + elif evt.key == ttk.TTkK.RightButton: + x = evt.x-image.x + y = evt.y-image.y + image.tilt = math.atan2(x,y*2) + image._updateBox() + self.update() + elif evt.key == ttk.TTkK.LeftButton: + mx,my = image.data['mx'],image.data['my'] + image.x = evt.x+mx + image.y = evt.y+my + image._updateBox() + self.update() + return True + + def wheelEvent(self, evt:ttk.TTkMouseEvent) -> bool: + if not (image:=self._highlightedImage): + pass + elif evt.evt == ttk.TTkK.WHEEL_Up: + image.size = min(image.size+1,50) + image._updateBox() + self.update() + elif evt.evt == ttk.TTkK.WHEEL_Down: + image.size = max(image.size-1,5) + image._updateBox() + self.update() + return True + + @ttk.pyTTkSlot(RenderData) + def render(self, data:RenderData): + outImage = self.getImage(data) + outImage.save(filename='outImage.png') + # outImage.save('outImage.png') + + def getImage(self, data:RenderData) -> Image: + return self.getImageWand(data) + + def getImageWand(self, data:RenderData) -> Image: + ww,wh = self.size() + observer = (ww/2 , -data.cameraY , wh-3 ) # Observer's position + dz = 10*math.cos(math.pi + math.pi * data.cameraAngle/360) + dy = 10*math.sin(math.pi + math.pi * data.cameraAngle/360) + look_at = (ww/2 , dy-data.cameraY , wh-3+dz) # Observer is looking along the positive Z-axis + screen_width, screen_height = data.resolution + w,h = screen_width,screen_height + prw,prh = screen_width/2, screen_height/2 + + # step1, sort Images based on the distance + images = sorted(self._images,key=lambda img:img.getBox()[1]) + znear,zfar = 0xFFFFFFFF,-0xFFFFFFFF + for img in images: + ix,iy,iw,ih = img.getBox() + ih-=1 + znear=min(znear,iy,iy+ih) + zfar=max(zfar,iy,iy+ih) + isz = img.size*2 + if math.pi/2 <= img.tilt < math.pi or -math.pi <= img.tilt < 0: + zleft = iy + zright = iy+ih + ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy+ih )) + ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy )) + ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) + ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy )) + ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy+ih )) + ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy )) + ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) + ip8x,ip8y = project_point(observer,look_at,(ix , 0 , iy )) + else: + zleft = iy+ih + zright = iy + ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy )) + ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy+ih )) + ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy )) + ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy+ih )) + ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy )) + ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy+ih )) + ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0.1 , iy )) + ip8x,ip8y = project_point(observer,look_at,(ix , 0.1 , iy+ih )) + img.data = { + 'zleft':zleft, + 'zright':zright, + 'top' : { + 'p1':(int((ip1x+1)*prw) , int((ip1y+1)*prh)), + 'p2':(int((ip2x+1)*prw) , int((ip2y+1)*prh)), + 'p3':(int((ip3x+1)*prw) , int((ip3y+1)*prh)), + 'p4':(int((ip4x+1)*prw) , int((ip4y+1)*prh)), + }, + 'bottom' : { + 'p1':(int((ip5x+1)*prw) , int((ip5y+1)*prh)), + 'p2':(int((ip6x+1)*prw) , int((ip6y+1)*prh)), + 'p3':(int((ip7x+1)*prw) , int((ip7y+1)*prh)), + 'p4':(int((ip8x+1)*prw) , int((ip8y+1)*prh)), + } + + } + + # with Image(width=w, height=h, background=Color('yellow')) as outImage: + # with Image(width=w, height=h, background=Color(f'rgba{data.bgColor}')) as outImage: + # with Image(width=w, height=h) as outImage: + outImage = Image(width=w, height=h, background=Color(f'rgba{data.bgColor}')) + + # step2, render the mirrored images + for img in images: + with Image(filename=img.fileName) as image: + image.background_color = Color('transparent') + image.virtual_pixel = 'background' + imw,imh = image.width, image.height + # with Image(width=imw, height=imh) as gradient: + with Image(width=imw, height=imh, pseudo='gradient:rgba(0,0,0,0)-rgba(1,1,1,0.8)') as gradient: + gradient.alpha_channel = 'activate' + # outImage.composite(gradient,0,0) + gradient.transparent_color(Color('white'), alpha=0.0, fuzz=0) + # Apply the mask to the image + image.composite_channel('alpha', gradient, 'copy_alpha', 0, 0) + prw,prh = screen_width/2, screen_height/2 + image.distort('perspective', [ + # From: to: + imw , 0 , *img.data['bottom']['p1'] , + 0 , 0 , *img.data['bottom']['p2'] , + imw , imh , *img.data['bottom']['p3'] , + 0 , imh , *img.data['bottom']['p4'] + ]) + # # Mask the image + # image.alpha_channel = 'activate' + # for maskImg in reversed(images): + # if img==maskImg: + # break + # with Drawing() as draw: + # # draw.fill_color = Color('rgba(0, 0, 1, 0.5)') + # draw.fill_color = Color('transparent') + # draw.fill_opacity = 0.1 # Fully opaque + # points = [ + # maskImg.data['bottom']['p1'], + # maskImg.data['bottom']['p2'], + # maskImg.data['bottom']['p4'], + # maskImg.data['bottom']['p3'], + # ] + # draw.polygon(points) + # draw(image) + # Mask the image + image.alpha_channel = 'activate' + for maskImg in reversed(images): + if img==maskImg: + break + with Image(width=imw, height=imh, background=Color('transparent')) as transparent_img: + with Drawing() as draw: + draw.fill_color = Color('black') # Opaque + draw.fill_opacity = 1.0 # Fully opaque + draw.stroke_color = Color('black') + draw.stroke_width = 0 + points = [ + maskImg.data['top']['p1'], + maskImg.data['top']['p2'], + maskImg.data['top']['p4'], + maskImg.data['top']['p3'], + ] + draw.polygon(points) + points = [ + maskImg.data['bottom']['p1'], + maskImg.data['bottom']['p2'], + maskImg.data['bottom']['p4'], + maskImg.data['bottom']['p3'], + ] + draw.polygon(points) + draw(transparent_img) + image.composite(transparent_img, left=0, top=0, operator='dst_out') + + + # Blur the mirrored image based on the alpha + alpha_channel = image.clone() + alpha_channel.alpha_channel = 'extract' + alpha_channel.blur(radius=5, sigma=3) + image.composite_channel(channel='alpha', image=alpha_channel, operator='copy_alpha') + image.blur(radius=5, sigma=3) + + outImage.composite(image,0,0) + + # step3, render the top images + for img in images: + with Image(filename=img.fileName) as image: + image.background_color = Color('transparent') + image.virtual_pixel = 'background' + imw,imh = image.width, image.height + + if zfar-znear != 0: + zl = 180 + 75*(img.data['zleft']-znear )/(zfar-znear) + zr = 180 + 75*(img.data['zright']-znear)/(zfar-znear) + else: + zl=zr=255 + + # apply gradient transparency based on the distance + with Image(width=imh, height=imw, pseudo=f'gradient:rgba({zl},{zl},{zl},1)-rgba({zr},{zr},{zr},1)') as gradient: + gradient.rotate(-90) + # image.composite(gradient) + alphaImage = image.clone() + alphaImage.alpha_channel = 'extract' + alphaImage.composite(gradient,left=0,top=0,operator='multiply') + alphaImage.alpha_channel = 'copy' + image.composite_channel(channel='alpha', image=alphaImage, operator='copy_alpha') + + prw,prh = screen_width/2, screen_height/2 + image.distort('perspective', [ + # From: to: + imw , 0 , *img.data['top']['p1'] , + 0 , 0 , *img.data['top']['p2'] , + imw , imh , *img.data['top']['p3'] , + 0 , imh , *img.data['top']['p4'] + ]) + + image.alpha_channel = 'activate' + for maskImg in reversed(images): + if img==maskImg: + break + with Image(width=imw, height=imh, background=Color('transparent')) as transparent_img: + with Drawing() as draw: + draw.fill_color = Color('black') # Opaque + draw.fill_opacity = 1.0 # Fully opaque + draw.stroke_color = Color('black') + draw.stroke_width = 0 + points = [ + maskImg.data['top']['p1'], + maskImg.data['top']['p2'], + maskImg.data['top']['p4'], + maskImg.data['top']['p3'], + ] + draw.polygon(points) + draw(transparent_img) + image.composite(transparent_img, left=0, top=0, operator='dst_out') + + outImage.composite(image,0,0) + return outImage + + + def getImagePil(self, data:RenderData) -> Image: + ww,wh = self.size() + observer = (ww/2 , -data.cameraY , wh-3 ) # Observer's position + dz = 10*math.cos(math.pi + math.pi * data.cameraAngle/360) + dy = 10*math.sin(math.pi + math.pi * data.cameraAngle/360) + look_at = (ww/2 , dy-data.cameraY , wh-3+dz) # Observer is looking along the positive Z-axis + screen_width, screen_height = data.resolution + w,h = screen_width,screen_height + prw,prh = screen_width/2, screen_height/2 + + # step1, sort Images based on the distance + images = sorted(self._images,key=lambda img:img.getBox()[1]) + znear,zfar = 0xFFFFFFFF,-0xFFFFFFFF + for img in images: + ix,iy,iw,ih = img.getBox() + ih-=1 + znear=min(znear,iy,iy+ih) + zfar=max(zfar,iy,iy+ih) + isz = img.size*2 + if math.pi/2 <= img.tilt < math.pi or -math.pi <= img.tilt < 0: + zleft = iy + zright = iy+ih + ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy+ih )) + ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy )) + ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) + ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy )) + ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy+ih )) + ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy )) + ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0 , iy+ih )) + ip8x,ip8y = project_point(observer,look_at,(ix , 0 , iy )) + else: + zleft = iy+ih + zright = iy + ip1x,ip1y = project_point(observer,look_at,(ix+iw , -isz , iy )) + ip2x,ip2y = project_point(observer,look_at,(ix , -isz , iy+ih )) + ip3x,ip3y = project_point(observer,look_at,(ix+iw , 0 , iy )) + ip4x,ip4y = project_point(observer,look_at,(ix , 0 , iy+ih )) + ip5x,ip5y = project_point(observer,look_at,(ix+iw , isz , iy )) + ip6x,ip6y = project_point(observer,look_at,(ix , isz , iy+ih )) + ip7x,ip7y = project_point(observer,look_at,(ix+iw , 0.1 , iy )) + ip8x,ip8y = project_point(observer,look_at,(ix , 0.1 , iy+ih )) + img.data = { + 'zleft':zleft, + 'zright':zright, + 'top' : { + 'p1':(int((ip1x+1)*prw) , int((ip1y+1)*prh)), + 'p2':(int((ip2x+1)*prw) , int((ip2y+1)*prh)), + 'p3':(int((ip3x+1)*prw) , int((ip3y+1)*prh)), + 'p4':(int((ip4x+1)*prw) , int((ip4y+1)*prh)), + }, + 'bottom' : { + 'p1':(int((ip5x+1)*prw) , int((ip5y+1)*prh)), + 'p2':(int((ip6x+1)*prw) , int((ip6y+1)*prh)), + 'p3':(int((ip7x+1)*prw) , int((ip7y+1)*prh)), + 'p4':(int((ip8x+1)*prw) , int((ip8y+1)*prh)), + } + + } + + # Create a new image with a transparent background + outImage = Image.new("RGBA", (w, h), data.bgColor) + + # step2, render the mirrored images + for img in images: + try: + image = Image.open(img.fileName).convert("RGBA") + except FileNotFoundError: + print(f"Error: File not found: {img.fileName}") + continue + + imw, imh = image.size + + # Create a gradient mask + gradient = Image.new("L", (imw, imh), 0) + draw = ImageDraw.Draw(gradient) + for i in range(imh): + alpha = int((i / imh) * 204) # alpha goes from 0 to 204 + draw.rectangle((0, i, imw, i), fill=alpha) + + # Apply the gradient mask to the image + image.putalpha(gradient) + + # Perspective distortion + points_bottom = [ + img.data['bottom']['p1'][0], img.data['bottom']['p1'][1], + img.data['bottom']['p2'][0], img.data['bottom']['p2'][1], + img.data['bottom']['p3'][0], img.data['bottom']['p3'][1], + img.data['bottom']['p4'][0], img.data['bottom']['p4'][1] + ] + image = image.transform((w, h), Image.PERSPECTIVE, points_bottom, Image.BILINEAR) + + # Apply blur to the alpha channel + alpha = image.split()[-1] + alpha = alpha.filter(ImageFilter.GaussianBlur(radius=5)) + image.putalpha(alpha) + image = image.filter(ImageFilter.GaussianBlur(radius=3)) + + # Paste the processed image onto the output image + outImage.paste(image, (0, 0), image) + return outImage + # step3, render the top images + for img in images: + try: + image = Image.open(img.fileName).convert("RGBA") + except FileNotFoundError: + print(f"Error: File not found: {img.fileName}") + continue + + imw, imh = image.size + + if zfar-znear != 0: + zl = 180 + 75*(img.data['zleft']-znear )/(zfar-znear) + zr = 180 + 75*(img.data['zright']-znear)/(zfar-znear) + else: + zl=zr=255 + + # apply gradient transparency based on the distance + gradient = Image.new("L", (imw, imh), 0) + draw = ImageDraw.Draw(gradient) + for i in range(imh): + alpha = int(zl + (zr - zl) * (i / imh)) + draw.rectangle((0, i, imw, i), fill=alpha) + image.putalpha(gradient) + + # Perspective distortion + points_top = [ + img.data['top']['p1'][0], img.data['top']['p1'][1], + img.data['top']['p2'][0], img.data['top']['p2'][1], + img.data['top']['p3'][0], img.data['top']['p3'][1], + img.data['top']['p4'][0], img.data['top']['p4'][1] + ] + image = image.transform((w, h), Image.PERSPECTIVE, points_top, Image.BILINEAR) + + # Mask the image + for maskImg in reversed(images): + if img==maskImg: + break + mask = Image.new("L", (imw, imh), 0) + draw = ImageDraw.Draw(mask) + points = [ + maskImg.data['top']['p1'], + maskImg.data['top']['p2'], + maskImg.data['top']['p4'], + maskImg.data['top']['p3'], + maskImg.data['bottom']['p1'], + maskImg.data['bottom']['p2'], + maskImg.data['bottom']['p4'], + maskImg.data['bottom']['p3'], + ] + draw.polygon(points, fill=255) + image.putalpha(mask) + + # Paste the processed image onto the output image + outImage.paste(image, (0, 0), image) + + return outImage + + def paintEvent(self, canvas): + w,h = self.size() + canvas.drawTTkString(pos=(w//2,h-3),text=ttk.TTkString("😎")) + # Draw Fov + for y in range(h-3): + canvas.drawChar(char='/', pos=(w//2+(h-3)-y,y)) + canvas.drawChar(char='\\',pos=(w//2-(h-3)+y,y)) + for image in self._images: + ix,iy,iw,ih = image.getBox() + canvas.drawText(pos=(ix,iy-1),text=f"{image.tilt:.2f}", color=ttk.TTkColor.YELLOW) + canvas.drawText(pos=(ix+5,iy-1),text=f"{image.fileName}", color=ttk.TTkColor.BLUE) + if image == self._highlightedImage: + canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.GREEN) + elif image == self._currentImage: + canvas.fill(pos=(ix,iy),size=(iw,ih),char='+',color=ttk.TTkColor.YELLOW) + else: + canvas.fill(pos=(ix,iy),size=(iw,ih),char='+') + if ih > iw > 1: + for dy in range(ih): + dx = iw*dy//ih + if ( + math.pi/2 < image.tilt < math.pi or + -math.pi/2 < image.tilt < 0 ): + canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) + else: + canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) + elif iw >= ih > 1: + for dx in range(iw): + dy = ih*dx//iw + if ( + math.pi/2 < image.tilt < math.pi or + -math.pi/2 < image.tilt < 0 ): + canvas.drawChar(char='X',pos=(ix+dx,iy+dy)) + else: + canvas.drawChar(char='X',pos=(ix+iw-dx,iy+dy)) + +class _Preview(ttk.TTkWidget): + __slots__ = ('_canvasImage') + def __init__(self, **kwargs): + self._canvasImage = ttk.TTkCanvas() + super().__init__(**kwargs) + + def updateCanvas(self, img:Image): + w,h = img.width, img.height + self._canvasImage.resize(w,h//2) + self._canvasImage.updateSize() + for x in range(img.width): + for y in range(img.height//2): + bg = 100 if (x//4+y//2)%2 else 150 + p1 = img[x,y*2] + p2 = img[x,y*2+1] + def _c2hex(p) -> str: + a = p.alpha + r = int(bg*(1-a)+255*a*p.red) + g = int(bg*(1-a)+255*a*p.green) + b = int(bg*(1-a)+255*a*p.blue) + return f"#{r<<16|g<<8|b:06x}" + c = ttk.TTkColor.fgbg(_c2hex(p2),_c2hex(p1)) + self._canvasImage.drawChar(pos=(x,y), char='▄', color=c) + self._canvasImage.drawText(pos=(0,1), text="Eugenio") + + self.update() + + def paintEvent(self, canvas): + w,h = self.size() + canvas.paintCanvas(self._canvasImage, (0,0,w,h), (0,0,w,h), (0,0,w,h)) + canvas.drawText(pos=(0,0), text="Eugenio") + +class ControlPanel(ttk.TTkContainer): + __slots__ = ( + 'previewPressed','renderPressed','_toolbox', '_previewImage') + def __init__(self, **kwargs): + self.previewPressed = ttk.pyTTkSignal(RenderData) + self.renderPressed = ttk.pyTTkSignal(RenderData) + super().__init__(**kwargs|{'layout':ttk.TTkGridLayout()}) + self._previewImage = _Preview(minHeight=20,maxHeight=20) + + self._toolbox = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"toolbox.tui.json")) + self._toolbox.getWidgetByName("Btn_Render").clicked.connect(self._renderClicked) + self._toolbox.getWidgetByName("Btn_Preview").clicked.connect(self._previewClicked) + # self._toolbox.getWidgetByName("SB_CamY") + # self._toolbox.getWidgetByName("SB_CamA") + # self._toolbox.getWidgetByName("CB_Bg") + # self._toolbox.getWidgetByName("BG_Color") + self.layout().addWidget(self._previewImage,0,0) + self.layout().addWidget(self._toolbox,1,0) + self.layout().addItem(ttk.TTkLayout(),2,0) + + def drawPreview(self, img:Image): + self._previewImage.updateCanvas(img) + + @ttk.pyTTkSlot() + def _renderClicked(self): + if self._toolbox.getWidgetByName("CB_Bg").isChecked(): + btnColor = self._toolbox.getWidgetByName("BG_Color").color() + color = (*btnColor.fgToRGB(),1) + else: + color = (0,0,0,0) + data = RenderData( + cameraAngle=self._toolbox.getWidgetByName("SB_CamA").value(), + cameraY=self._toolbox.getWidgetByName("SB_CamY").value(), + bgColor=color, + resolution=(800,600) + ) + self.renderPressed.emit(data) + + @ttk.pyTTkSlot() + def _previewClicked(self): + w,h = self.size() + if self._toolbox.getWidgetByName("CB_Bg").isChecked(): + btnColor = self._toolbox.getWidgetByName("BG_Color").color() + color = (*btnColor.fgToRGB(),1) + else: + color = (0,0,0,0) + data = RenderData( + cameraAngle=self._toolbox.getWidgetByName("SB_CamA").value(), + cameraY=self._toolbox.getWidgetByName("SB_CamY").value(), + bgColor=color, + resolution=(w,40) + ) + self.previewPressed.emit(data) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('filename', type=str, nargs='*', + help='the filename/s') + args = parser.parse_args() + + ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) + + root = ttk.TTk( + layout=ttk.TTkGridLayout(), + title="TTkode", + mouseTrack=True) + + perspectivator = Perspectivator() + controlPanel = ControlPanel() + + at = ttk.TTkAppTemplate() + at.setWidget(widget=perspectivator,position=at.MAIN) + at.setWidget(widget=controlPanel,position=at.RIGHT, size=30) + + for file in args.filename: + perspectivator.addImage(file) + + root.layout().addWidget(at) + + def _preview(data): + img = perspectivator.getImage(data) + controlPanel.drawPreview(img) + + controlPanel.renderPressed.connect(perspectivator.render) + controlPanel.previewPressed.connect(_preview) + + root.mainloop() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/apps/perspectivator/t.camera.tui.json b/apps/perspectivator/t.camera.tui.json new file mode 100644 index 00000000..3c467b7d --- /dev/null +++ b/apps/perspectivator/t.camera.tui.json @@ -0,0 +1,346 @@ +{ + "type": "TTkUi/Document", + "version": "2.1.0", + "tui": { + "class": "TTkContainer", + "params": { + "Name": "MainWindow", + "Position": [ + 4, + 2 + ], + "Size": [ + 80, + 4 + ], + "Min Width": 0, + "Min Height": 0, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 0 + ], + "Layout": "TTkGridLayout" + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 80, + 4 + ] + }, + "children": [ + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel", + "Position": [ + 0, + 2 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 2, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "z:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 2, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-1", + "Position": [ + 0, + 0 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 2, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "x:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-2", + "Position": [ + 0, + 1 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 2, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "y:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 1, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-3", + "Position": [ + 0, + 3 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 5, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "tilt:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 3, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_X", + "Position": [ + 40, + 0 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 1, + "Minimum": -1000, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Y", + "Position": [ + 40, + 1 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 1, + "Minimum": -1000, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 1, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Z", + "Position": [ + 40, + 2 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 1, + "Minimum": 0, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 2, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Tilt", + "Position": [ + 40, + 3 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 0, + "Minimum": -179, + "Maximum": 180 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 3, + "col": 1, + "rowspan": 1, + "colspan": 1 + } + ] + } + }, + "connections": [] +} \ No newline at end of file diff --git a/apps/perspectivator/t.image.tui.json b/apps/perspectivator/t.image.tui.json new file mode 100644 index 00000000..8a4a32cf --- /dev/null +++ b/apps/perspectivator/t.image.tui.json @@ -0,0 +1,421 @@ +{ + "type": "TTkUi/Document", + "version": "2.1.0", + "tui": { + "class": "TTkContainer", + "params": { + "Name": "MainWindow", + "Position": [ + 4, + 2 + ], + "Size": [ + 80, + 5 + ], + "Min Width": 0, + "Min Height": 0, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 0 + ], + "Layout": "TTkGridLayout" + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 80, + 5 + ] + }, + "children": [ + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel", + "Position": [ + 0, + 2 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 2, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "z:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 2, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-1", + "Position": [ + 0, + 0 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 2, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "x:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-2", + "Position": [ + 0, + 1 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 2, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "y:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 1, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-3", + "Position": [ + 0, + 3 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 5, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "tilt:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 3, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_X", + "Position": [ + 40, + 0 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 1, + "Minimum": -1000, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Y", + "Position": [ + 40, + 1 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 1, + "Minimum": -1000, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 1, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Z", + "Position": [ + 40, + 2 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 1000, + "Minimum": -1000, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 2, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Tilt", + "Position": [ + 40, + 3 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 10, + "Minimum": -10, + "Maximum": 10 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 3, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Size", + "Position": [ + 40, + 4 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 10, + "Minimum": -10, + "Maximum": 10 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 38, + 1 + ] + }, + "children": [] + }, + "row": 4, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-4", + "Position": [ + 0, + 4 + ], + "Size": [ + 40, + 1 + ], + "Min Width": 5, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Size:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 4, + "col": 0, + "rowspan": 1, + "colspan": 1 + } + ] + } + }, + "connections": [] +} \ No newline at end of file diff --git a/apps/perspectivator/t.toolbox.tui.json b/apps/perspectivator/t.toolbox.tui.json new file mode 100644 index 00000000..a060318e --- /dev/null +++ b/apps/perspectivator/t.toolbox.tui.json @@ -0,0 +1,921 @@ +{ + "type": "TTkUi/Document", + "version": "2.1.0", + "tui": { + "class": "TTkContainer", + "params": { + "Name": "MainWindow", + "Position": [ + 5, + 4 + ], + "Size": [ + 70, + 16 + ], + "Min Width": 0, + "Min Height": 16, + "Max Width": 65536, + "Max Height": 16, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 0 + ], + "Layout": "TTkGridLayout" + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 70, + 16 + ] + }, + "children": [ + { + "class": "TTkButton", + "params": { + "Name": "Btn_Preview", + "Position": [ + 0, + 0 + ], + "Size": [ + 35, + 3 + ], + "Min Width": 9, + "Min Height": 3, + "Max Width": 65536, + "Max Height": 3, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Preview", + "Border": true, + "Checkable": true, + "Checked": false + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkButton", + "params": { + "Name": "Btn_Render", + "Position": [ + 0, + 3 + ], + "Size": [ + 70, + 3 + ], + "Min Width": 8, + "Min Height": 3, + "Max Width": 65536, + "Max Height": 3, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Render", + "Border": true, + "Checkable": false, + "Checked": false + }, + "row": 1, + "col": 0, + "rowspan": 1, + "colspan": 2 + }, + { + "class": "TTkButton", + "params": { + "Name": "Btn_SaveCfg", + "Position": [ + 35, + 0 + ], + "Size": [ + 35, + 3 + ], + "Min Width": 12, + "Min Height": 3, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Save (Cfg)", + "Border": true, + "Checkable": false, + "Checked": false + }, + "row": 0, + "col": 1, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLayout", + "params": { + "Geometry": [ + 0, + 6, + 70, + 10 + ] + }, + "children": [ + { + "class": "TTkCheckbox", + "params": { + "Name": "CB_Show", + "Position": [ + 0, + 0 + ], + "Size": [ + 15, + 1 + ], + "Min Width": 7, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 1, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Show", + "Tristate": false, + "Checked": false, + "Check State": 0 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel", + "Position": [ + 9, + 0 + ], + "Size": [ + 9, + 1 + ], + "Min Width": 5, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "File:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLineEdit", + "params": { + "Name": "LE_OutFile", + "Position": [ + 16, + 0 + ], + "Size": [ + 22, + 1 + ], + "Min Width": 1, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 1, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Input Type": 1, + "Echo Mode": 0, + "Text": "outImage.png" + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkCheckbox", + "params": { + "Name": "CB_Bg", + "Position": [ + 0, + 1 + ], + "Size": [ + 15, + 1 + ], + "Min Width": 13, + "Min Height": 1, + "Max Width": 15, + "Max Height": 1, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Background", + "Tristate": false, + "Checked": false, + "Check State": 0 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkColorButtonPicker", + "params": { + "Name": "BG_Color", + "Position": [ + 14, + 1 + ], + "Size": [ + 24, + 1 + ], + "Min Width": 2, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 1, + "Visible": true, + "Enabled": false, + "ToolTip": "", + "Text": "", + "Border": false, + "Checkable": false, + "Checked": false, + "Color": "\u001b[38;2;0;0;0m", + "Return Type": 0 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-2", + "Position": [ + 0, + 2 + ], + "Size": [ + 11, + 1 + ], + "Min Width": 10, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Fog (Near)", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-3", + "Position": [ + 0, + 3 + ], + "Size": [ + 11, + 1 + ], + "Min Width": 9, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Fog (Far)", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkComboBox", + "params": { + "Name": "CB_ResPresets", + "Position": [ + 0, + 5 + ], + "Size": [ + 20, + 1 + ], + "Min Width": 5, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 1, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 0 + ], + "Layout": "TTkLayout", + "Editable": false, + "Text Align.": 4, + "Insert Policy": 3 + }, + "layout": { + "class": "TTkLayout", + "params": { + "Geometry": [ + 0, + 0, + 20, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Fog_Far", + "Position": [ + 11, + 3 + ], + "Size": [ + 14, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 128, + "Minimum": 0, + "Maximum": 255 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 12, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_Fog_Near", + "Position": [ + 11, + 2 + ], + "Size": [ + 14, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 0, + "Minimum": 0, + "Maximum": 255 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 12, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_HRes", + "Position": [ + 1, + 6 + ], + "Size": [ + 8, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 800, + "Minimum": 0, + "Maximum": 5000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 6, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_VRes", + "Position": [ + 10, + 6 + ], + "Size": [ + 8, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 600, + "Minimum": 0, + "Maximum": 5000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 6, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-1", + "Position": [ + 0, + 4 + ], + "Size": [ + 11, + 1 + ], + "Min Width": 8, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Offset-Y", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_OffY", + "Position": [ + 11, + 4 + ], + "Size": [ + 14, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 0, + "Minimum": -1000, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 12, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-4", + "Position": [ + 19, + 6 + ], + "Size": [ + 3, + 1 + ], + "Min Width": 3, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "AA:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkComboBox", + "params": { + "Name": "CB_AA", + "Position": [ + 23, + 6 + ], + "Size": [ + 6, + 1 + ], + "Min Width": 5, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 1, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 0 + ], + "Layout": "TTkLayout", + "Editable": false, + "Text Align.": 4, + "Insert Policy": 3 + }, + "layout": { + "class": "TTkLayout", + "params": { + "Geometry": [ + 0, + 0, + 6, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-5", + "Position": [ + 2, + 7 + ], + "Size": [ + 14, + 1 + ], + "Min Width": 14, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "Mirror Fading:", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-6", + "Position": [ + 0, + 8 + ], + "Size": [ + 6, + 1 + ], + "Min Width": 6, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "From-Y", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkLabel", + "params": { + "Name": "TTkLabel-7", + "Position": [ + 0, + 9 + ], + "Size": [ + 4, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Text": "To-Y", + "Color": "\u001b[0m", + "Alignment": 1 + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_FadingTo", + "Position": [ + 9, + 9 + ], + "Size": [ + 10, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": 10, + "Minimum": -1000, + "Maximum": 1000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 8, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + }, + { + "class": "TTkSpinBox", + "params": { + "Name": "SB_FadingFrom", + "Position": [ + 9, + 8 + ], + "Size": [ + 10, + 1 + ], + "Min Width": 4, + "Min Height": 1, + "Max Width": 65536, + "Max Height": 65536, + "Visible": true, + "Enabled": true, + "ToolTip": "", + "Padding": [ + 0, + 0, + 0, + 2 + ], + "Layout": "TTkGridLayout", + "Value": -3, + "Minimum": -1000, + "Maximum": 10000 + }, + "layout": { + "class": "TTkGridLayout", + "params": { + "Geometry": [ + 0, + 0, + 8, + 1 + ] + }, + "children": [] + }, + "row": 0, + "col": 0, + "rowspan": 1, + "colspan": 1 + } + ], + "row": 2, + "col": 0, + "rowspan": 3, + "colspan": 2 + } + ] + } + }, + "connections": [ + { + "sender": "CB_Bg", + "receiver": "BG_Color", + "signal": "toggled(bool)", + "slot": "setEnabled(bool)" + } + ] +} \ No newline at end of file