diff --git a/fonts/LiberationSans-Bold.ttf b/fonts/LiberationSans-Bold.ttf new file mode 100644 index 0000000..ee102d8 Binary files /dev/null and b/fonts/LiberationSans-Bold.ttf differ diff --git a/fonts/LiberationSans-Regular.ttf b/fonts/LiberationSans-Regular.ttf new file mode 100644 index 0000000..7769c41 Binary files /dev/null and b/fonts/LiberationSans-Regular.ttf differ diff --git a/generate_assets_v3.py b/generate_assets_v3.py new file mode 100644 index 0000000..b195d61 --- /dev/null +++ b/generate_assets_v3.py @@ -0,0 +1,564 @@ +#!/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() diff --git a/theme.xml b/theme.xml new file mode 100644 index 0000000..c8bbd6c --- /dev/null +++ b/theme.xml @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +