Code Python - Interface Responsive

🎯 Studio d'Artiste Optimisé

Interface parfaitement responsive avec boutons adaptés, popup Dessinateur Bot éclairci, barre de statut avec indicateurs visuels, et textes qui s'ajustent à la taille des éléments.

🚀Partager le Code

studio_responsive.py
import os
import io
import random
import requests
import urllib.parse
from datetime import datetime
from threading import Thread

# --- Kivy Imports ---
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.slider import Slider
from kivy.uix.label import Label
from kivy.uix.popup import Popup
from kivy.uix.textinput import TextInput
from kivy.graphics import Color, Line, Rectangle, Ellipse
from kivy.clock import Clock
from kivy.core.window import Window
from PIL import Image

# Theme visuel moderne et responsive - Compatible Pydroid
Window.clearcolor = (0.96, 0.97, 0.98, 1)
CANVAS_COLOR = (1, 1, 1, 1)
TOOLBAR_COLOR = (0.25, 0.27, 0.32, 1)
BUTTON_COLOR = (0.35, 0.65, 0.95, 1)
BUTTON_ACTIVE_COLOR = (0.25, 0.55, 0.85, 1)
TEXT_COLOR = (0.15, 0.15, 0.15, 1)
ACCENT_COLOR = (0.92, 0.35, 0.65, 1)
SUCCESS_COLOR = (0.25, 0.82, 0.35, 1)
ERROR_COLOR = (0.92, 0.25, 0.25, 1)
POPUP_BG_COLOR = (0.98, 0.99, 1, 1)

class DrawingCanvas(Widget):
    def on_touch_up(self, touch):
        if self.collide_point(*touch.pos):
            App.get_running_app().root.save_state_for_undo()
        return super().on_touch_up(touch)
    
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            parent = App.get_running_app().root
            with self.canvas:
                Color(*parent.couleur_actuelle_rgba)
                touch.ud['line'] = Line(
                    points=(touch.x, touch.y), 
                    width=parent.taille_pinceau, 
                    cap='round', 
                    joint='round'
                )
            return True
    
    def on_touch_move(self, touch):
        if self.collide_point(*touch.pos) and 'line' in touch.ud:
            touch.ud['line'].points += [touch.x, touch.y]
        return True

class DessinLayout(BoxLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.orientation = 'vertical'
        self.spacing = 8
        self.padding = [12, 12, 12, 12]
        
        # Variables d'etat
        self.undo_stack = []
        self.redo_stack = []
        self.couleur_actuelle_rgba = (0, 0, 0, 1)
        self.taille_pinceau = 3.0
        self.drawing_in_progress = False
        self.historique_dir = "historique_mobile"
        self.is_coloring_request = False
        self.current_tool = "pinceau"
        
        os.makedirs(self.historique_dir, exist_ok=True)
        
        # Interface utilisateur responsive
        self.create_responsive_toolbar()
        self.create_canvas_area()
        self.create_modern_status_bar()
        
        Clock.schedule_once(self.post_build_init)

    def create_responsive_toolbar(self):
        # Conteneur principal avec hauteur adaptative
        toolbar_container = BoxLayout(
            size_hint_y=None, 
            height='110dp',
            padding=[6, 6, 6, 6]
        )
        
        # Fond avec degradé
        with toolbar_container.canvas.before:
            Color(*TOOLBAR_COLOR)
            self.toolbar_bg = Rectangle()
        
        toolbar_container.bind(
            pos=self.update_toolbar_bg,
            size=self.update_toolbar_bg
        )
        
        # Barre d'outils principale responsive
        main_toolbar = BoxLayout(spacing=8, padding=[8, 8, 8, 8])
        
        # Section 1: Outils (30%)
        tools_section = self.create_compact_tools_section()
        
        # Section 2: Taille (40%)
        size_section = self.create_compact_size_section()
        
        # Section 3: Actions (30%)
        actions_section = self.create_compact_actions_section()
        
        # Assemblage avec proportions fixes
        main_toolbar.add_widget(tools_section)
        main_toolbar.add_widget(size_section)
        main_toolbar.add_widget(actions_section)
        
        toolbar_container.add_widget(main_toolbar)
        self.add_widget(toolbar_container)

    def create_compact_tools_section(self):
        tools_section = BoxLayout(
            orientation='vertical',
            spacing=4,
            size_hint_x=0.3
        )
        
        # Titre compact
        tools_title = Label(
            text="OUTILS",
            size_hint_y=None,
            height='16dp',
            color=(1, 1, 1, 1),
            font_size='10sp',
            bold=True
        )
        tools_section.add_widget(tools_title)
        
        # Boutons compacts et responsives
        tools_grid = BoxLayout(orientation='vertical', spacing=3, size_hint_y=None, height='75dp')
        
        self.color_btn = Button(
            text="Couleur",
            font_size='9sp',
            background_color=BUTTON_COLOR,
            size_hint_y=None,
            height='22dp'
        )
        self.color_btn.bind(on_release=self.choisir_couleur_moderne)
        
        self.brush_btn = Button(
            text="Pinceau",
            font_size='9sp',
            background_color=ACCENT_COLOR,
            size_hint_y=None,
            height='22dp'
        )
        self.brush_btn.bind(on_release=self.activer_pinceau)
        
        self.eraser_btn = Button(
            text="Gomme",
            font_size='9sp',
            background_color=BUTTON_COLOR,
            size_hint_y=None,
            height='22dp'
        )
        self.eraser_btn.bind(on_release=self.activer_gomme)
        
        tools_grid.add_widget(self.color_btn)
        tools_grid.add_widget(self.brush_btn)
        tools_grid.add_widget(self.eraser_btn)
        
        tools_section.add_widget(tools_grid)
        return tools_section

    def create_compact_size_section(self):
        size_section = BoxLayout(
            orientation='vertical',
            spacing=4,
            size_hint_x=0.4
        )
        
        # Titre
        size_title = Label(
            text="TAILLE",
            size_hint_y=None,
            height='16dp',
            color=(1, 1, 1, 1),
            font_size='10sp',
            bold=True
        )
        size_section.add_widget(size_title)
        
        # Contenu compact
        size_content = BoxLayout(
            orientation='vertical',
            spacing=3,
            size_hint_y=None,
            height='75dp'
        )
        
        # Affichage compact
        self.size_display = Label(
            text="3px",
            size_hint_y=None,
            height='14dp',
            color=ACCENT_COLOR,
            font_size='9sp',
            bold=True
        )
        size_content.add_widget(self.size_display)
        
        # Slider responsive
        self.taille_slider = Slider(
            min=1,
            max=25,
            value=3,
            size_hint_y=None,
            height='20dp'
        )
        self.taille_slider.bind(value=self.changer_taille_pinceau)
        size_content.add_widget(self.taille_slider)
        
        # Boutons de taille compacts
        quick_sizes = GridLayout(cols=4, spacing=2, size_hint_y=None, height='18dp')
        for size in [2, 5, 10, 20]:
            btn = Button(
                text=str(size),
                font_size='8sp',
                background_color=(0.55, 0.55, 0.55, 1),
                on_release=lambda x, s=size: self.set_quick_size(s)
            )
            quick_sizes.add_widget(btn)
        size_content.add_widget(quick_sizes)
        
        # Indicateur visuel compact
        self.size_indicator = Label(
            text="o",
            size_hint_y=None,
            height='10dp',
            color=(1, 1, 1, 1),
            font_size='7sp'
        )
        size_content.add_widget(self.size_indicator)
        
        size_section.add_widget(size_content)
        return size_section

    def create_compact_actions_section(self):
        actions_section = BoxLayout(
            orientation='vertical',
            spacing=4,
            size_hint_x=0.3
        )
        
        # Titre
        actions_title = Label(
            text="ACTIONS",
            size_hint_y=None,
            height='16dp',
            color=(1, 1, 1, 1),
            font_size='10sp',
            bold=True
        )
        actions_section.add_widget(actions_title)
        
        # Grille d'actions compacte
        actions_grid = GridLayout(cols=2, spacing=2, size_hint_y=None, height='75dp')
        
        self.undo_button = Button(
            text="Annul",
            font_size='8sp',
            disabled=True,
            background_color=BUTTON_COLOR,
            size_hint_y=None,
            height='18dp'
        )
        self.undo_button.bind(on_release=self.undo)
        
        self.redo_button = Button(
            text="Retab",
            font_size='8sp',
            disabled=True,
            background_color=BUTTON_COLOR,
            size_hint_y=None,
            height='18dp'
        )
        self.redo_button.bind(on_release=self.redo)
        
        bot_button = Button(
            text="Bot",
            font_size='8sp',
            background_color=ACCENT_COLOR,
            size_hint_y=None,
            height='18dp'
        )
        bot_button.bind(on_release=self.generer_image_bot_moderne)
        
        save_button = Button(
            text="Save",
            font_size='8sp',
            background_color=SUCCESS_COLOR,
            size_hint_y=None,
            height='18dp'
        )
        save_button.bind(on_release=self.sauvegarder)
        
        clear_button = Button(
            text="Clear",
            font_size='8sp',
            background_color=ERROR_COLOR,
            size_hint_y=None,
            height='18dp'
        )
        clear_button.bind(on_release=self.confirmer_effacer_moderne)
        
        stop_button = Button(
            text="Stop",
            font_size='8sp',
            background_color=(0.5, 0.5, 0.5, 1),
            size_hint_y=None,
            height='18dp'
        )
        stop_button.bind(on_release=self.arreter_dessin_bot)
        
        actions_grid.add_widget(self.undo_button)
        actions_grid.add_widget(self.redo_button)
        actions_grid.add_widget(bot_button)
        actions_grid.add_widget(save_button)
        actions_grid.add_widget(clear_button)
        actions_grid.add_widget(stop_button)
        
        actions_section.add_widget(actions_grid)
        return actions_section

    def create_canvas_area(self):
        # Zone de dessin optimisée
        canvas_container = BoxLayout(
            padding=[8, 8, 8, 8],
            size_hint_y=1
        )
        
        # Canvas avec bordure subtile
        self.canvas_widget = DrawingCanvas()
        
        with self.canvas_widget.canvas.before:
            Color(0.8, 0.8, 0.8, 1)
            self.canvas_border = Rectangle()
            Color(*CANVAS_COLOR)
            self.canvas_bg = Rectangle()
        
        canvas_container.add_widget(self.canvas_widget)
        self.add_widget(canvas_container)

    def create_modern_status_bar(self):
        # Barre de statut moderne et claire
        status_container = BoxLayout(
            size_hint_y=None,
            height='35dp',
            padding=[8, 5, 8, 5]
        )
        
        # Fond clair et moderne
        with status_container.canvas.before:
            Color(0.95, 0.96, 0.98, 1)
            self.status_bg = Rectangle()
        
        # Contenu organisé
        status_content = BoxLayout(spacing=10)
        
        # Message principal
        self.status_bar = Label(
            text="Pret a creer !",
            color=(0.2, 0.2, 0.2, 1),
            font_size='11sp',
            bold=True
        )
        
        # Indicateurs d'état
        indicators_layout = BoxLayout(
            size_hint_x=None,
            width='180dp',
            spacing=8
        )
        
        # Outil actuel
        self.tool_indicator = Label(
            text="Pinceau",
            color=ACCENT_COLOR,
            font_size='10sp',
            bold=True,
            size_hint_x=None,
            width='50dp'
        )
        
        # Taille actuelle
        self.size_status = Label(
            text="3px",
            color=BUTTON_COLOR,
            font_size='10sp',
            bold=True,
            size_hint_x=None,
            width='30dp'
        )
        
        # Couleur actuelle
        self.color_indicator = Label(
            text="■",
            color=(0, 0, 0, 1),
            font_size='12sp',
            size_hint_x=None,
            width='20dp'
        )
        
        indicators_layout.add_widget(self.tool_indicator)
        indicators_layout.add_widget(self.size_status)
        indicators_layout.add_widget(self.color_indicator)
        
        status_content.add_widget(self.status_bar)
        status_content.add_widget(indicators_layout)
        
        status_container.add_widget(status_content)
        status_container.bind(
            pos=self.update_status_bg,
            size=self.update_status_bg
        )
        
        self.add_widget(status_container)

    def update_toolbar_bg(self, instance, value):
        if hasattr(self, 'toolbar_bg'):
            self.toolbar_bg.pos = instance.pos
            self.toolbar_bg.size = instance.size

    def update_status_bg(self, instance, value):
        if hasattr(self, 'status_bg'):
            self.status_bg.pos = instance.pos
            self.status_bg.size = instance.size

    def post_build_init(self, dt):
        self.canvas_widget.bind(pos=self.update_canvas_bg, size=self.update_canvas_bg)
        self.update_canvas_bg(self.canvas_widget, None)
        self.save_state_for_undo()

    def update_canvas_bg(self, instance, value):
        if hasattr(self, 'canvas_bg') and hasattr(self, 'canvas_border'):
            self.canvas_border.pos = (instance.x - 2, instance.y - 2)
            self.canvas_border.size = (instance.width + 4, instance.height + 4)
            self.canvas_bg.pos = instance.pos
            self.canvas_bg.size = instance.size

    def set_quick_size(self, size):
        self.taille_slider.value = size
        self.taille_pinceau = float(size)
        self.update_size_display()

    def update_size_display(self):
        size_int = int(self.taille_pinceau)
        self.size_display.text = f"{size_int}px"
        self.size_status.text = f"{size_int}px"
        
        if size_int <= 3:
            indicator = "."
        elif size_int <= 8:
            indicator = "o"
        elif size_int <= 15:
            indicator = "O"
        else:
            indicator = "@"
        
        self.size_indicator.text = indicator

    def update_status_indicators(self):
        self.update_size_display()
        r, g, b, a = self.couleur_actuelle_rgba
        self.color_indicator.color = (r, g, b, a)
        self.tool_indicator.text = self.current_tool.capitalize()

    def activer_pinceau(self, instance):
        self.current_tool = "pinceau"
        if self.couleur_actuelle_rgba == CANVAS_COLOR:
            self.couleur_actuelle_rgba = (0, 0, 0, 1)
        
        self.brush_btn.background_color = ACCENT_COLOR
        self.eraser_btn.background_color = BUTTON_COLOR
        self.status_bar.text = "Mode Pinceau active"
        self.update_status_indicators()

    def activer_gomme(self, instance):
        self.current_tool = "gomme"
        self.couleur_actuelle_rgba = CANVAS_COLOR
        self.eraser_btn.background_color = ACCENT_COLOR
        self.brush_btn.background_color = BUTTON_COLOR
        self.status_bar.text = "Mode Gomme active"
        self.update_status_indicators()

    def changer_taille_pinceau(self, instance, value):
        self.taille_pinceau = value
        self.update_size_display()
        self.status_bar.text = f"Taille changee: {int(value)}px"

    def choisir_couleur_moderne(self, instance):
        content = BoxLayout(orientation='vertical', spacing=15, padding=20)
        
        title = Label(
            text="CHOISIR UNE COULEUR",
            size_hint_y=None,
            height='30dp',
            font_size='16sp',
            bold=True,
            color=(0.2, 0.2, 0.2, 1)
        )
        content.add_widget(title)
        
        colors_container = BoxLayout(orientation='vertical', spacing=8)
        
        main_colors = GridLayout(cols=6, spacing=6, size_hint_y=None, height='50dp')
        main_color_list = [
            (0, 0, 0, 1), (0.9, 0.1, 0.1, 1), (0.1, 0.7, 0.1, 1),
            (0.1, 0.1, 0.9, 1), (0.9, 0.9, 0.1, 1), (0.9, 0.1, 0.9, 1)
        ]
        
        for color in main_color_list:
            btn = self.create_color_button(color, '40dp')
            main_colors.add_widget(btn)
        
        secondary_colors = GridLayout(cols=8, spacing=4, size_hint_y=None, height='35dp')
        secondary_color_list = [
            (0.1, 0.9, 0.9, 1), (0.9, 0.5, 0.1, 1), (0.5, 0.1, 0.5, 1), (0.9, 0.6, 0.8, 1),
            (0.6, 0.3, 0.1, 1), (0.5, 0.5, 0.5, 1), (0.8, 0.8, 0.8, 1), (1, 1, 1, 1)
        ]
        
        for color in secondary_color_list:
            btn = self.create_color_button(color, '30dp')
            secondary_colors.add_widget(btn)
        
        colors_container.add_widget(main_colors)
        colors_container.add_widget(secondary_colors)
        content.add_widget(colors_container)
        
        close_btn = Button(
            text="FERMER",
            size_hint_y=None,
            height='40dp',
            background_color=ACCENT_COLOR,
            font_size='12sp',
            bold=True
        )
        
        self.popup = Popup(
            title='',
            content=content,
            size_hint=(0.85, 0.6),
            background_color=POPUP_BG_COLOR,
            separator_color=(0, 0, 0, 0)
        )
        
        close_btn.bind(on_release=lambda x: self.popup.dismiss())
        content.add_widget(close_btn)
        self.popup.open()

    def create_color_button(self, color, size):
        btn = Button(
            background_color=color,
            size_hint=(None, None),
            size=(size, size)
        )
        btn.bind(on_release=lambda btn_instance, c=color: self.set_couleur_moderne(c))
        return btn

    def set_couleur_moderne(self, color_rgba):
        self.couleur_actuelle_rgba = color_rgba
        self.current_tool = "pinceau"
        self.brush_btn.background_color = ACCENT_COLOR
        self.eraser_btn.background_color = BUTTON_COLOR
        self.color_btn.background_color = color_rgba
        self.status_bar.text = "Nouvelle couleur selectionnee"
        self.update_status_indicators()
        self.popup.dismiss()

    def generer_image_bot_moderne(self, instance):
        content = BoxLayout(orientation='vertical', spacing=18, padding=25)
        
        header = BoxLayout(orientation='vertical', spacing=6, size_hint_y=None, height='60dp')
        
        title = Label(
            text="DESSINATEUR BOT",
            size_hint_y=None,
            height='28dp',
            font_size='18sp',
            bold=True,
            color=(0.2, 0.2, 0.2, 1)
        )
        
        subtitle = Label(
            text="Assistant Artistique Intelligent",
            size_hint_y=None,
            height='20dp',
            font_size='12sp',
            color=(0.4, 0.4, 0.4, 1),
            italic=True
        )
        
        header.add_widget(title)
        header.add_widget(subtitle)
        content.add_widget(header)
        
        description = Label(
            text="Decrivez votre vision et le Bot la transformera !",
            size_hint_y=None,
            height='40dp',
            font_size='12sp',
            color=(0.3, 0.3, 0.3, 1),
            halign='center'
        )
        content.add_widget(description)
        
        input_container = BoxLayout(orientation='vertical', spacing=6, size_hint_y=None, height='80dp')
        
        input_label = Label(
            text="VOTRE IDEE:",
            size_hint_y=None,
            height='20dp',
            font_size='12sp',
            bold=True,
            color=(0.2, 0.2, 0.2, 1)
        )
        
        text_input = TextInput(
            hint_text='dragon colore, paysage montagne...',
            multiline=True,
            font_size='12sp',
            size_hint_y=None,
            height='50dp',
            background_color=(1, 1, 1, 1),
            foreground_color=(0.2, 0.2, 0.2, 1),
            cursor_color=ACCENT_COLOR
        )
        
        input_container.add_widget(input_label)
        input_container.add_widget(text_input)
        content.add_widget(input_container)
        
        style_container = BoxLayout(orientation='vertical', spacing=6, size_hint_y=None, height='60dp')
        
        style_label = Label(
            text="STYLE:",
            size_hint_y=None,
            height='20dp',
            font_size='12sp',
            bold=True,
            color=(0.2, 0.2, 0.2, 1)
        )
        
        style_buttons = GridLayout(cols=2, spacing=8, size_hint_y=None, height='32dp')
        
        sketch_btn = Button(
            text="Esquisse Crayon",
            font_size='10sp',
            background_color=BUTTON_COLOR
        )
        
        color_btn = Button(
            text="Dessin Colorie",
            font_size='10sp',
            background_color=BUTTON_COLOR
        )
        
        style_buttons.add_widget(sketch_btn)
        style_buttons.add_widget(color_btn)
        
        style_container.add_widget(style_label)
        style_container.add_widget(style_buttons)
        content.add_widget(style_container)
        
        buttons_layout = BoxLayout(spacing=12, size_hint_y=None, height='45dp')
        
        btn_create = Button(
            text="CREER",
            background_color=ACCENT_COLOR,
            font_size='12sp',
            bold=True
        )
        
        btn_cancel = Button(
            text="ANNULER",
            background_color=(0.6, 0.6, 0.6, 1),
            font_size='12sp'
        )
        
        buttons_layout.add_widget(btn_create)
        buttons_layout.add_widget(btn_cancel)
        content.add_widget(buttons_layout)
        
        popup = Popup(
            title='',
            content=content,
            size_hint=(0.9, 0.75),
            background_color=(0.99, 0.99, 1, 1),
            separator_color=(0, 0, 0, 0)
        )
        
        selected_style = ["esquisse"]
        
        def select_sketch(instance):
            selected_style[0] = "esquisse"
            sketch_btn.background_color = ACCENT_COLOR
            color_btn.background_color = BUTTON_COLOR
        
        def select_color(instance):
            selected_style[0] = "colorie"
            color_btn.background_color = ACCENT_COLOR
            sketch_btn.background_color = BUTTON_COLOR
        
        sketch_btn.bind(on_release=select_sketch)
        color_btn.bind(on_release=select_color)
        
        def on_generate(instance):
            prompt_fr = text_input.text.strip()
            popup.dismiss()
            if prompt_fr:
                self.is_coloring_request = selected_style[0] == "colorie"
                self.status_bar.text = "Bot: Preparation..."
                Thread(target=self._traduire_et_generer, args=(prompt_fr,)).start()
            else:
                self.show_popup_moderne("ATTENTION", "Veuillez saisir une description.")
        
        btn_create.bind(on_release=on_generate)
        btn_cancel.bind(on_release=lambda x: popup.dismiss())
        
        popup.open()

    def _traduire_et_generer(self, prompt_fr):
        try:
            self.status_bar.text = "Bot: Traduction..."
            url = f"https://api.mymemory.translated.net/get?q={urllib.parse.quote(prompt_fr)}&langpair=fr|en"
            response = requests.get(url, timeout=15)
            response.raise_for_status()
            data = response.json()
            prompt_en = data['responseData']['translatedText']
            Clock.schedule_once(lambda dt: setattr(self.status_bar, 'text', "Bot: Creation..."))
        except Exception:
            prompt_en = prompt_fr
            Clock.schedule_once(lambda dt: setattr(self.status_bar, 'text', "Bot: Creation directe..."))
        
        if self.is_coloring_request:
            prompt_stylise = f"vibrant colored illustration of {prompt_en}, artistic masterpiece"
        else:
            prompt_stylise = f"pencil sketch of {prompt_en}, artistic drawing, white background"
        
        self._call_api_and_animate(prompt_stylise)

    def _call_api_and_animate(self, prompt):
        try:
            encoded_prompt = urllib.parse.quote(prompt)
            url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?model=sdxl&nologo=true&width=800&height=600"
            self.status_bar.text = "Bot: Telechargement..."
            response = requests.get(url, timeout=120)
            response.raise_for_status()
            pil_img = Image.open(io.BytesIO(response.content))
            Clock.schedule_once(lambda dt: self.animer_dessin_bot(pil_img))
        except Exception as e:
            Clock.schedule_once(lambda dt: (
                self.show_popup_moderne("ERREUR BOT", f"Probleme: {str(e)}"),
                setattr(self.status_bar, 'text', 'Bot: Erreur')
            ))

    def animer_dessin_bot(self, pil_img):
        self.arreter_dessin_bot(None)
        self.effacer_canvas()
        self.drawing_in_progress = True
        self.status_bar.text = "Bot: Analyse image..."
        
        source_img = pil_img.convert("RGBA") if self.is_coloring_request else pil_img.convert("L")
        canvas_w, canvas_h = self.canvas_widget.size
        
        img_ratio = source_img.width / source_img.height
        canvas_ratio = canvas_w / canvas_h
        
        if img_ratio > canvas_ratio:
            new_w = int(canvas_w * 0.9)
            new_h = int(new_w / img_ratio)
        else:
            new_h = int(canvas_h * 0.9)
            new_w = int(new_h * img_ratio)
        
        source_img = source_img.resize((new_w, new_h), Image.Resampling.LANCZOS)
        largeur, hauteur = source_img.size
        offset_x = (canvas_w - largeur) / 2
        offset_y = (canvas_h - hauteur) / 2
        pixels = source_img.load()
        
        couches_de_points = []
        
        if self.is_coloring_request:
            points_couleur = []
            for y in range(0, hauteur, 2):
                for x in range(0, largeur, 2):
                    px = pixels[x, y]
                    if px[3] > 30:
                        points_couleur.append({
                            'pos': (x + offset_x, hauteur - y + offset_y),
                            'val': (px[0]/255., px[1]/255., px[2]/255., min(px[3]/255., 0.9))
                        })
            random.shuffle(points_couleur)
            couches_de_points.append({'points': points_couleur, 'message': "Bot: Coloriage..."})
        else:
            points_contours = []
            points_ombres = []
            
            for y in range(0, hauteur, 1):
                for x in range(0, largeur, 1):
                    px = pixels[x, y]
                    intensity = px / 255.
                    pos = (x + offset_x, hauteur - y + offset_y)
                    
                    if px < 80:
                        points_contours.append({'pos': pos, 'val': intensity})
                    elif px < 160:
                        points_ombres.append({'pos': pos, 'val': intensity})
            
            for points_list in [points_contours, points_ombres]:
                random.shuffle(points_list)
            
            if points_contours:
                couches_de_points.append({'points': points_contours, 'message': "Bot: Contours..."})
            if points_ombres:
                couches_de_points.append({'points': points_ombres, 'message': "Bot: Ombres..."})
        
        self.couches_a_dessiner = couches_de_points
        
        if not self.couches_a_dessiner:
            self.status_bar.text = "Bot: Image trop claire"
            self.drawing_in_progress = False
            return
        
        with self.canvas_widget.canvas:
            Color(1, 0.3, 0.6, 0.8)
            self.pen_cursor = Ellipse(size=(15, 15), pos=(-25, -25))
        
        self.dessiner_prochaine_couche_bot()

    def dessiner_prochaine_couche_bot(self, index_couche=0):
        if not self.drawing_in_progress or index_couche >= len(self.couches_a_dessiner):
            self.finir_animation_bot()
            return
        
        couche = self.couches_a_dessiner[index_couche]
        self.status_bar.text = couche['message']
        points_iter = iter(couche['points'])
        on_complete = lambda: self.dessiner_prochaine_couche_bot(index_couche + 1)
        self.dessiner_rafale_bot(points_iter, on_complete)

    def dessiner_rafale_bot(self, points_iter, on_complete_callback):
        if not self.drawing_in_progress:
            on_complete_callback()
            return
        
        try:
            with self.canvas_widget.canvas:
                for i in range(300):
                    point_data = next(points_iter)
                    pos = point_data['pos']
                    valeur = point_data['val']
                    
                    if isinstance(valeur, tuple):
                        Color(*valeur)
                    else:
                        alpha = 0.7 if valeur < 0.3 else 0.5
                        Color(valeur, valeur, valeur, alpha)
                    
                    taille = random.uniform(1.2, 2.5)
                    Rectangle(pos=pos, size=(taille, taille))
                    
                    if i % 50 == 0:
                        self.pen_cursor.pos = (pos[0] - 7.5, pos[1] - 7.5)
            
            Clock.schedule_once(lambda dt: self.dessiner_rafale_bot(points_iter, on_complete_callback), 0.01)
        except StopIteration:
            on_complete_callback()

    def finir_animation_bot(self):
        if hasattr(self, 'pen_cursor') and self.pen_cursor in self.canvas_widget.canvas.children:
            self.canvas_widget.canvas.remove(self.pen_cursor)
        
        if self.drawing_in_progress:
            self.status_bar.text = "Bot: Oeuvre terminee !"
            Clock.schedule_once(lambda dt: self.auto_save_bot_art(), 1.0)
        
        self.drawing_in_progress = False
        self.save_state_for_undo()

    def auto_save_bot_art(self):
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(self.historique_dir, f"bot_{timestamp}.png")
        self.canvas_widget.export_to_png(filename)
        self.status_bar.text = f"Sauve: bot_{timestamp}.png"

    def save_state_for_undo(self):
        canvas_state = list(self.canvas_widget.canvas.children)
        self.undo_stack.append(canvas_state)
        if len(self.undo_stack) > 15:
            self.undo_stack.pop(0)
        self.redo_stack.clear()
        self.update_undo_redo_buttons()

    def _restore_state(self, state):
        self.canvas_widget.canvas.clear()
        with self.canvas_widget.canvas.before:
            Color(*CANVAS_COLOR)
            self.canvas_bg = Rectangle(pos=self.canvas_widget.pos, size=self.canvas_widget.size)
        for instruction in state:
            self.canvas_widget.canvas.add(instruction)

    def undo(self, instance):
        if len(self.undo_stack) > 1:
            self.redo_stack.append(self.undo_stack.pop())
            self._restore_state(self.undo_stack[-1])
            self.update_undo_redo_buttons()
            self.status_bar.text = "Action annulee"

    def redo(self, instance):
        if self.redo_stack:
            state = self.redo_stack.pop()
            self.undo_stack.append(state)
            self._restore_state(state)
            self.update_undo_redo_buttons()
            self.status_bar.text = "Action retablie"

    def update_undo_redo_buttons(self):
        self.undo_button.disabled = len(self.undo_stack) <= 1
        self.redo_button.disabled = not self.redo_stack
        self.undo_button.background_color = BUTTON_COLOR if not self.undo_button.disabled else (0.4, 0.4, 0.4, 1)
        self.redo_button.background_color = BUTTON_COLOR if not self.redo_button.disabled else (0.4, 0.4, 0.4, 1)

    def confirmer_effacer_moderne(self, instance):
        content = BoxLayout(orientation='vertical', spacing=15, padding=20)
        
        warning_title = Label(
            text="ATTENTION",
            size_hint_y=None,
            height='30dp',
            font_size='16sp',
            bold=True,
            color=(0.8, 0.2, 0.2, 1)
        )
        content.add_widget(warning_title)
        
        warning_text = Label(
            text="Effacer tout le travail ?\nAction irreversible !",
            size_hint_y=None,
            height='50dp',
            font_size='12sp',
            color=(0.3, 0.3, 0.3, 1),
            halign='center'
        )
        content.add_widget(warning_text)
        
        buttons_layout = BoxLayout(spacing=10, size_hint_y=None, height='40dp')
        
        btn_confirm = Button(
            text="OUI",
            background_color=ERROR_COLOR,
            font_size='12sp',
            bold=True
        )
        
        btn_cancel = Button(
            text="NON",
            background_color=BUTTON_COLOR,
            font_size='12sp'
        )
        
        buttons_layout.add_widget(btn_confirm)
        buttons_layout.add_widget(btn_cancel)
        content.add_widget(buttons_layout)
        
        popup = Popup(
            title='',
            content=content,
            size_hint=(0.7, 0.4),
            background_color=(0.99, 0.99, 1, 1)
        )
        
        btn_confirm.bind(on_release=lambda x: (self.effacer_canvas(), popup.dismiss()))
        btn_cancel.bind(on_release=lambda x: popup.dismiss())
        popup.open()

    def effacer_canvas(self):
        self.canvas_widget.canvas.clear()
        with self.canvas_widget.canvas.before:
            Color(*CANVAS_COLOR)
            self.canvas_bg = Rectangle(pos=self.canvas_widget.pos, size=self.canvas_widget.size)
        self.status_bar.text = "Canvas efface"
        self.save_state_for_undo()

    def sauvegarder(self, instance):
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(self.historique_dir, f"dessin_{timestamp}.png")
        try:
            self.canvas_widget.export_to_png(filename)
            self.show_popup_moderne("SUCCES", f"Sauvegarde reussie:\n{filename}")
            self.status_bar.text = f"Sauve: dessin_{timestamp}.png"
        except Exception as e:
            self.show_popup_moderne("ERREUR", f"Echec sauvegarde: {str(e)}")

    def show_popup_moderne(self, title, message):
        content = BoxLayout(orientation='vertical', spacing=15, padding=20)
        
        title_label = Label(
            text=title,
            size_hint_y=None,
            height='30dp',
            font_size='14sp',
            bold=True,
            color=(0.2, 0.2, 0.2, 1)
        )
        content.add_widget(title_label)
        
        message_label = Label(
            text=message,
            color=(0.3, 0.3, 0.3, 1),
            font_size='11sp',
            halign='center'
        )
        content.add_widget(message_label)
        
        ok_button = Button(
            text="OK",
            size_hint_y=None,
            height='35dp',
            background_color=ACCENT_COLOR,
            font_size='12sp',
            bold=True
        )
        content.add_widget(ok_button)
        
        popup = Popup(
            title='',
            content=content,
            size_hint=(0.7, 0.4),
            background_color=(0.99, 0.99, 1, 1)
        )
        
        ok_button.bind(on_release=lambda x: popup.dismiss())
        popup.open()

    def arreter_dessin_bot(self, instance):
        self.drawing_in_progress = False
        self.status_bar.text = "Bot arrete"

class DessinApp(App):
    def build(self):
        self.title = "Studio Artiste - Dessinateur Bot Responsive"
        return DessinLayout()

if __name__ == '__main__':
    DessinApp().run()

📱Responsive

  • • Boutons adaptés à leur contenu
  • • Textes qui ne débordent plus
  • • Interface mobile optimisée
  • • Proportions fixes et équilibrées

🤖Bot Amélioré

  • • Popup avec fond très clair
  • • Interface épurée et moderne
  • • Textes bien lisibles
  • • Navigation intuitive

📊Statut Moderne

  • • Barre claire avec indicateurs
  • • Messages d'action précis
  • • Informations visuelles utiles
  • • Design cohérent et professionnel

📚Installation et Utilisation

⚙️ Installation:

1. Ouvrez Pydroid3

2. Installez les dépendances:

pip install kivy pillow requests

3. Collez le code et lancez

▶️ Utilisation:

• Interface responsive automatique

• Boutons adaptés aux textes

• Popup Bot éclairci et moderne

• Barre de statut avec indicateurs

• Messages d'action améliorés