AppleVLC/generate_assets_v3.py

565 lines
19 KiB
Python

#!/usr/bin/env python3
"""
AppleVLC Skin v3 - Diseño Apple con capacidades reales de VLC Skins2
- PNG con canal alpha REAL (no alphacolor)
- Iconos SF Symbols limpios
- Colores Apple HIG
- Touch targets 44px mínimo
"""
from PIL import Image, ImageDraw
import math
import os
# === PALETA APPLE HIG ===
APPLE = {
# Primarios
'blue': (0, 122, 255, 255),
'red': (255, 59, 48, 255),
'green': (52, 199, 89, 255),
'orange': (255, 149, 0, 255),
# Grises - Light Mode
'label': (0, 0, 0, 255),
'secondary': (60, 60, 67, 255),
'tertiary': (60, 60, 67, 153),
'quaternary': (60, 60, 67, 46),
# Iconos secundarios - gris más claro para jerarquía
'icon_secondary': (142, 142, 147, 255), # #8E8E93 - Apple systemGray
# Fondos
'bg_primary': (242, 242, 247, 255),
'bg_secondary': (255, 255, 255, 255),
'fill': (120, 120, 128, 51),
# Controles ventana macOS
'win_close': (255, 95, 87, 255),
'win_min': (255, 189, 46, 255),
'win_max': (39, 201, 63, 255),
# Separadores
'separator': (60, 60, 67, 73),
# Sombra
'shadow': (0, 0, 0, 40),
}
IMG_DIR = 'images'
def superellipse_mask(width, height, n=4.5, padding=0):
"""Crea máscara con superelipse (squircle Apple) con alpha real."""
img = Image.new('L', (width, height), 0) # Máscara en escala de grises
draw = ImageDraw.Draw(img)
cx, cy = width / 2, height / 2
rx, ry = width / 2 - padding, height / 2 - padding
points = []
for i in range(200):
t = 2 * math.pi * i / 200
cos_t, sin_t = math.cos(t), math.sin(t)
x = cx + abs(cos_t) ** (2/n) * rx * (1 if cos_t >= 0 else -1)
y = cy + abs(sin_t) ** (2/n) * ry * (1 if sin_t >= 0 else -1)
points.append((x, y))
draw.polygon(points, fill=255)
return img
def create_background():
"""Fondo limpio con esquinas superelipse y borde sutil."""
w, h = 500, 225 # +20px en cada borde
# Fondo transparente
img = Image.new('RGBA', (w, h), (0, 0, 0, 0))
# Fondo principal con color Apple
base = Image.new('RGBA', (w, h), APPLE['bg_primary'])
mask = superellipse_mask(w, h, n=5, padding=1)
# Aplicar máscara superelipse
img.paste(base, (0, 0), mask)
# Borde sutil 1px para definición
border_color = (200, 200, 200, 180)
border_mask = superellipse_mask(w, h, n=5, padding=0)
inner_mask = superellipse_mask(w, h, n=5, padding=2)
for y in range(h):
for x in range(w):
if border_mask.getpixel((x, y)) > 128 and inner_mask.getpixel((x, y)) < 128:
img.putpixel((x, y), border_color)
img.save(f'{IMG_DIR}/background.png')
print('✓ background.png (con borde sutil)')
def create_video_background():
"""Fondo para ventana con video - más grande."""
w, h = 640, 480 # Tamaño para video 16:9 + controles
img = Image.new('RGBA', (w, h), (0, 0, 0, 0))
base = Image.new('RGBA', (w, h), APPLE['bg_primary'])
mask = superellipse_mask(w, h, n=5, padding=1)
img.paste(base, (0, 0), mask)
# Borde sutil
border_color = (200, 200, 200, 180)
border_mask = superellipse_mask(w, h, n=5, padding=0)
inner_mask = superellipse_mask(w, h, n=5, padding=2)
for y in range(h):
for x in range(w):
if border_mask.getpixel((x, y)) > 128 and inner_mask.getpixel((x, y)) < 128:
img.putpixel((x, y), border_color)
img.save(f'{IMG_DIR}/background_video.png')
print('✓ background_video.png (640x480)')
def create_playlist_background():
"""Fondo para área de playlist."""
w, h = 300, 200
img = Image.new('RGBA', (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Fondo ligeramente más oscuro para contraste
draw.rounded_rectangle([0, 0, w-1, h-1], radius=8,
fill=(230, 230, 235, 255), outline=(200, 200, 200, 180))
img.save(f'{IMG_DIR}/playlist_bg.png')
print('✓ playlist_bg.png')
def create_playlist_items():
"""Items de playlist: seleccionado, hover, normal."""
w, h = 280, 24
# Item normal (transparente)
normal = Image.new('RGBA', (w, h), (0, 0, 0, 0))
normal.save(f'{IMG_DIR}/playlist_item.png')
# Item seleccionado (azul Apple)
selected = Image.new('RGBA', (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(selected)
draw.rounded_rectangle([2, 2, w-3, h-3], radius=6, fill=(0, 122, 255, 255))
selected.save(f'{IMG_DIR}/playlist_selected.png')
# Item hover
hover = Image.new('RGBA', (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(hover)
draw.rounded_rectangle([2, 2, w-3, h-3], radius=6, fill=(0, 122, 255, 40))
hover.save(f'{IMG_DIR}/playlist_hover.png')
print('✓ playlist_*.png (items)')
def draw_icon(draw, bounds, icon_type, color, stroke=2.5):
"""Dibuja iconos estilo SF Symbols."""
x1, y1, x2, y2 = bounds
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
w, h = x2 - x1, y2 - y1
m = w * 0.22 # margen
if icon_type == 'play':
pts = [(x1 + m + 3, y1 + m), (x2 - m, cy), (x1 + m + 3, y2 - m)]
draw.polygon(pts, fill=color)
elif icon_type == 'pause':
bw = w * 0.16
gap = w * 0.1
for dx in [-1, 1]:
bx = cx + dx * (gap + bw/2) - bw/2
draw.rounded_rectangle([bx, y1+m, bx+bw, y2-m], radius=2, fill=color)
elif icon_type == 'stop':
draw.rounded_rectangle([x1+m+2, y1+m+2, x2-m-2, y2-m-2], radius=3, fill=color)
elif icon_type == 'prev':
# Barra + triángulo
bw = 4
draw.rectangle([x1+m, y1+m+2, x1+m+bw, y2-m-2], fill=color)
pts = [(x2-m, y1+m+2), (x1+m+bw+3, cy), (x2-m, y2-m-2)]
draw.polygon(pts, fill=color)
elif icon_type == 'next':
bw = 4
draw.rectangle([x2-m-bw, y1+m+2, x2-m, y2-m-2], fill=color)
pts = [(x1+m, y1+m+2), (x2-m-bw-3, cy), (x1+m, y2-m-2)]
draw.polygon(pts, fill=color)
elif icon_type == 'volume':
# Altavoz
sw = w * 0.3
draw.polygon([
(x1+m, cy-4), (x1+m+sw*0.4, cy-4), (x1+m+sw, cy-10),
(x1+m+sw, cy+10), (x1+m+sw*0.4, cy+4), (x1+m, cy+4)
], fill=color)
# Ondas
for i, offset in enumerate([8, 14]):
draw.arc([x1+m+sw+2+i*5, cy-offset, x1+m+sw+10+i*6, cy+offset],
-50, 50, fill=color, width=int(stroke))
elif icon_type == 'mute':
sw = w * 0.3
draw.polygon([
(x1+m, cy-4), (x1+m+sw*0.4, cy-4), (x1+m+sw, cy-10),
(x1+m+sw, cy+10), (x1+m+sw*0.4, cy+4), (x1+m, cy+4)
], fill=color)
# X
xstart = x2 - m - 10
draw.line([xstart, cy-6, xstart+10, cy+6], fill=APPLE['red'], width=int(stroke))
draw.line([xstart, cy+6, xstart+10, cy-6], fill=APPLE['red'], width=int(stroke))
elif icon_type == 'shuffle':
# Flechas cruzadas
draw.line([x1+m, y2-m-3, x2-m-5, y1+m+3], fill=color, width=int(stroke))
draw.line([x1+m, y1+m+3, x2-m-5, y2-m-3], fill=color, width=int(stroke))
# Puntas
aw = 5
draw.polygon([(x2-m, y1+m), (x2-m-aw, y1+m), (x2-m, y1+m+aw)], fill=color)
draw.polygon([(x2-m, y2-m), (x2-m-aw, y2-m), (x2-m, y2-m-aw)], fill=color)
elif icon_type == 'repeat':
# Rectángulo con flechas
draw.rounded_rectangle([x1+m, y1+m+3, x2-m, y2-m-3], radius=5,
outline=color, width=int(stroke))
aw = 5
# Flecha derecha
ax = x2 - m - 8
draw.polygon([(ax, y1+m+3-aw), (ax+aw, y1+m+3), (ax, y1+m+3+aw)], fill=color)
# Flecha izquierda
ax = x1 + m + 8
draw.polygon([(ax, y2-m-3-aw), (ax-aw, y2-m-3), (ax, y2-m-3+aw)], fill=color)
elif icon_type == 'fullscreen':
aw = 8
sw = int(stroke)
# Esquina superior izquierda
draw.line([x1+m, y1+m, x1+m+aw, y1+m], fill=color, width=sw)
draw.line([x1+m, y1+m, x1+m, y1+m+aw], fill=color, width=sw)
# Esquina inferior derecha
draw.line([x2-m, y2-m, x2-m-aw, y2-m], fill=color, width=sw)
draw.line([x2-m, y2-m, x2-m, y2-m-aw], fill=color, width=sw)
elif icon_type == 'playlist':
# Tres líneas con bullets
for i in range(3):
y = y1 + m + i * 8 + 2
draw.ellipse([x1+m, y, x1+m+4, y+4], fill=color)
draw.rectangle([x1+m+8, y+1, x2-m, y+3], fill=color)
elif icon_type == 'equalizer':
# Barras verticales de ecualizador
bar_w = 4
heights = [0.5, 0.8, 0.6, 0.9, 0.4]
gap = (w - 2*m - len(heights)*bar_w) / (len(heights) - 1)
for i, h_ratio in enumerate(heights):
bx = x1 + m + i * (bar_w + gap)
bar_h = (h - 2*m) * h_ratio
by = y2 - m - bar_h
draw.rounded_rectangle([bx, by, bx + bar_w, y2 - m], radius=2, fill=color)
elif icon_type == 'ab_loop':
# Letras A-B con flecha circular
# A
draw.polygon([(x1+m+2, y2-m-2), (x1+m+8, y1+m+2), (x1+m+14, y2-m-2)], outline=color, width=int(stroke))
draw.line([x1+m+5, cy+2, x1+m+11, cy+2], fill=color, width=int(stroke))
# B
draw.line([x2-m-12, y1+m+2, x2-m-12, y2-m-2], fill=color, width=int(stroke))
draw.arc([x2-m-14, y1+m+2, x2-m-4, cy], -90, 90, fill=color, width=int(stroke))
draw.arc([x2-m-14, cy, x2-m-4, y2-m-2], -90, 90, fill=color, width=int(stroke))
elif icon_type == 'speed':
# Velocímetro / gauge
draw.arc([x1+m, y1+m+2, x2-m, y2-m+6], 200, 340, fill=color, width=int(stroke)+1)
# Aguja
draw.line([cx, cy+2, cx+6, y1+m+6], fill=color, width=int(stroke))
def create_button(name, icon_type, color, size=44, with_disabled=False):
"""Crea botón con 3 estados - hover azul Apple."""
states = ['up', 'over', 'down']
if with_disabled:
states.append('disabled')
for state in states:
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
if state == 'disabled':
# Estado disabled - muy tenue
icon_color = (180, 180, 180, 100)
else:
# Fondo circular con tinte azul Apple en hover
if state == 'over':
draw.ellipse([2, 2, size-3, size-3], fill=(0, 122, 255, 30))
elif state == 'down':
draw.ellipse([2, 2, size-3, size-3], fill=(0, 122, 255, 50))
alpha = 255 if state == 'up' else 230 if state == 'over' else 200
icon_color = (*color[:3], alpha)
draw_icon(draw, (0, 0, size, size), icon_type, icon_color)
img.save(f'{IMG_DIR}/{name}_{state}.png')
print(f'{name}_*.png' + (' (con disabled)' if with_disabled else ''))
def create_window_buttons():
"""Botones de ventana macOS."""
buttons = [
('win_close', APPLE['win_close'], 'x'),
('win_min', APPLE['win_min'], '-'),
('win_max', APPLE['win_max'], '+'),
]
for name, color, symbol in buttons:
for state in ['up', 'over', 'down']:
size = 14
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Color según estado
alpha = 255 if state != 'down' else 200
c = (*color[:3], alpha)
draw.ellipse([0, 0, size-1, size-1], fill=c)
# Símbolo en hover
if state == 'over':
sc = (60, 0, 0, 200) if symbol == 'x' else (60, 50, 0, 200) if symbol == '-' else (0, 60, 0, 200)
if symbol == 'x':
draw.line([4, 4, 9, 9], fill=sc, width=2)
draw.line([4, 9, 9, 4], fill=sc, width=2)
elif symbol == '-':
draw.line([4, 6, 9, 6], fill=sc, width=2)
else:
draw.line([4, 6, 9, 6], fill=sc, width=2)
draw.line([6, 4, 6, 9], fill=sc, width=2)
img.save(f'{IMG_DIR}/{name}_{state}.png')
print('✓ win_*.png')
def create_volume_slider():
"""Slider de volumen limpio sin sombras."""
vol_w, vol_h = 80, 4 # Track compacto
# Track volumen - gris visible
track = Image.new('RGBA', (vol_w, vol_h), (0, 0, 0, 0))
draw = ImageDraw.Draw(track)
draw.rounded_rectangle([0, 0, vol_w-1, vol_h-1], radius=2, fill=(0, 0, 0, 80))
track.save(f'{IMG_DIR}/vol_slider_track.png')
# Knob volumen - 18px limpio sin sombra
knob_size = 18
for state in ['up', 'over', 'down']:
knob = Image.new('RGBA', (knob_size, knob_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(knob)
# Círculo blanco limpio con borde definido
if state == 'up':
c = (255, 255, 255, 255)
border = (200, 200, 200, 255)
elif state == 'over':
c = (248, 248, 248, 255)
border = (180, 180, 180, 255)
else:
c = (235, 235, 235, 255)
border = (160, 160, 160, 255)
draw.ellipse([1, 1, knob_size-2, knob_size-2], fill=c, outline=border)
knob.save(f'{IMG_DIR}/vol_knob_{state}.png')
print('✓ vol_slider_*.png (limpio, 18px knob)')
def create_repeat_one_icons():
"""Icono Repeat One (repetir una canción) - hover visible."""
size = 44
for checked in [False, True]:
for state in ['up', 'over', 'down']:
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Color - azul activo, gris secundario inactivo
base = APPLE['blue'][:3] if checked else APPLE['icon_secondary'][:3]
alpha = 255 if state == 'up' else 230 if state == 'over' else 200
color = (*base, alpha)
# Fondo hover - MÁS VISIBLE
if state == 'over':
draw.ellipse([4, 4, size-5, size-5], fill=(0, 0, 0, 35))
elif state == 'down':
draw.ellipse([4, 4, size-5, size-5], fill=(0, 0, 0, 55))
# Dibujar icono repeat con "1"
m = 10
# Rectángulo con flechas
draw.rounded_rectangle([m, m+3, size-m, size-m-3], radius=5,
outline=color, width=2)
# Flecha derecha
aw = 4
ax = size - m - 6
draw.polygon([(ax, m+3-aw), (ax+aw, m+3), (ax, m+3+aw)], fill=color)
# Flecha izquierda
ax = m + 6
draw.polygon([(ax, size-m-3-aw), (ax-aw, size-m-3), (ax, size-m-3+aw)], fill=color)
# Número "1" en el centro
# Dibujar línea vertical para el "1"
cx, cy = size // 2, size // 2
draw.line([cx, cy-5, cx, cy+5], fill=color, width=2)
draw.line([cx-2, cy-3, cx, cy-5], fill=color, width=2)
suffix = 'on' if checked else 'off'
img.save(f'{IMG_DIR}/repeat1_{suffix}_{state}.png')
print('✓ repeat1_*.png (repeat one)')
def create_slider():
"""Slider estilo Apple Music con track grueso."""
track_w, track_h = 400, 8 # 8px para mejor visibilidad
num_frames = 400
# Track (fondo gris visible)
track = Image.new('RGBA', (track_w, track_h), (0, 0, 0, 0))
draw = ImageDraw.Draw(track)
draw.rounded_rectangle([0, 0, track_w-1, track_h-1], radius=4,
fill=(0, 0, 0, 70))
track.save(f'{IMG_DIR}/slider_track.png')
# SliderBackground: imagen con N frames horizontales para progreso
num_frames = 50
frame_w = track_w
total_w = frame_w * num_frames
fill = Image.new('RGBA', (total_w, track_h), (0, 0, 0, 0))
draw = ImageDraw.Draw(fill)
apple_blue = APPLE['blue']
for i in range(num_frames):
x_start = i * frame_w
progress_w = int((i / (num_frames - 1)) * (frame_w - 1)) if i > 0 else 0
if progress_w >= 4:
draw.rounded_rectangle(
[x_start, 0, x_start + progress_w, track_h - 1],
radius=4, fill=apple_blue
)
elif progress_w > 0:
draw.rectangle(
[x_start, 0, x_start + progress_w, track_h - 1],
fill=apple_blue
)
fill.save(f'{IMG_DIR}/slider_fill.png')
print(f' → slider_fill.png ({num_frames} frames, 8px track)')
# Knob - círculo blanco limpio sin sombra
knob_size = 20
for state in ['up', 'over', 'down']:
knob = Image.new('RGBA', (knob_size, knob_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(knob)
# Círculo blanco principal - sin sombra
if state == 'up':
c = (255, 255, 255, 255)
border = (210, 210, 210, 255)
elif state == 'over':
c = (248, 248, 248, 255)
border = (190, 190, 190, 255)
else:
c = (235, 235, 235, 255)
border = (170, 170, 170, 255)
# Círculo con borde definido
draw.ellipse([1, 1, knob_size-2, knob_size-2], fill=c, outline=border)
knob.save(f'{IMG_DIR}/slider_knob_{state}.png')
print('✓ slider_*.png (limpio, 20px knob)')
def create_checkboxes():
"""Checkboxes para shuffle/repeat - hover azul Apple."""
for name, icon in [('shuffle', 'shuffle'), ('repeat', 'repeat')]:
for checked in [False, True]:
for state in ['up', 'over', 'down']:
size = 44
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Color - azul activo, gris inactivo
base = APPLE['blue'][:3] if checked else APPLE['icon_secondary'][:3]
alpha = 255 if state == 'up' else 230 if state == 'over' else 200
color = (*base, alpha)
# Hover con tinte azul Apple
if state == 'over':
draw.ellipse([4, 4, size-5, size-5], fill=(0, 122, 255, 25))
elif state == 'down':
draw.ellipse([4, 4, size-5, size-5], fill=(0, 122, 255, 45))
draw_icon(draw, (6, 6, size-6, size-6), icon, color, stroke=2.5)
suffix = 'on' if checked else 'off'
img.save(f'{IMG_DIR}/{name}_{suffix}_{state}.png')
print('✓ shuffle/repeat_*.png (hover azul)')
def main():
os.makedirs(IMG_DIR, exist_ok=True)
print('\n🍎 AppleVLC v8 - Assets completos...\n')
# Fondos
create_background()
create_video_background()
create_playlist_background()
create_playlist_items()
# Reproducción - Play/Pause 48px azul (protagonista)
create_button('play', 'play', APPLE['blue'][:3], 48, with_disabled=True)
create_button('pause', 'pause', APPLE['blue'][:3], 48)
# Iconos SECUNDARIOS con estados disabled
create_button('stop', 'stop', APPLE['icon_secondary'][:3], 44, with_disabled=True)
create_button('prev', 'prev', APPLE['icon_secondary'][:3], 44, with_disabled=True)
create_button('next', 'next', APPLE['icon_secondary'][:3], 44, with_disabled=True)
create_button('playlist', 'playlist', APPLE['icon_secondary'][:3], 44)
# Audio
create_button('volume', 'volume', APPLE['icon_secondary'][:3], 44)
create_button('mute', 'mute', APPLE['icon_secondary'][:3], 44)
# Otros controles
create_button('fullscreen', 'fullscreen', APPLE['icon_secondary'][:3], 44)
create_button('equalizer', 'equalizer', APPLE['icon_secondary'][:3], 44)
create_button('ab_loop', 'ab_loop', APPLE['icon_secondary'][:3], 44)
create_button('speed', 'speed', APPLE['icon_secondary'][:3], 44)
# Ventana macOS
create_window_buttons()
# Sliders
create_slider()
create_volume_slider()
# Checkboxes
create_checkboxes()
create_repeat_one_icons()
print('\n✅ Assets v8 - Video, playlist, disabled states')
if __name__ == '__main__':
main()