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.
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()1. Ouvrez Pydroid3
2. Installez les dépendances:
3. Collez le code et lancez
• Interface responsive automatique
• Boutons adaptés aux textes
• Popup Bot éclairci et moderne
• Barre de statut avec indicateurs
• Messages d'action améliorés