#!/usr/bin/env python3 """Portada y esquemas del README, con la paleta del tema Acampada. Las capturas de movil (01..08) se hacen aparte, con la app arrancada en un nodo de pruebas a 412x915 y escala 2; este script compone 00-portada.png a partir de ellas y dibuja esquema-interfaz.png, esquema-rama.png y esquema-llamadas.png. El texto vive aqui: se cambia y se relanza, nunca se retocan los PNG a mano. python3 docs/capturas/generar_portada.py """ import math from pathlib import Path from PIL import Image, ImageDraw, ImageFont HERE = Path(__file__).resolve().parent ANTON = HERE.parents[1] / "src/client/assets/fonts/Anton-Regular.ttf" SANS = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" BG, FG, NEON, MUTED = "#ffffff", "#000000", "#00e5ff", "#555555" def font(path, size): return ImageFont.truetype(str(path), size) def phone(shot, width): """Una captura dentro de un marco negro grueso con la sombra cian de los botones.""" im = Image.open(HERE / shot).convert("RGB") h = round(im.height * width / im.width) im = im.resize((width, h), Image.LANCZOS) pad, radius, shadow = 10, 34, 10 frame = Image.new("RGBA", (width + 2 * pad, h + 2 * pad + shadow), (0, 0, 0, 0)) d = ImageDraw.Draw(frame) d.rounded_rectangle((0, shadow, width + 2 * pad - 1, h + 2 * pad + shadow - 1), radius, fill=NEON) d.rounded_rectangle((0, 0, width + 2 * pad - 1, h + 2 * pad - 1), radius, fill=FG) mask = Image.new("L", im.size, 0) ImageDraw.Draw(mask).rounded_rectangle((0, 0, width - 1, h - 1), radius - pad, fill=255) frame.paste(im, (pad, pad), mask) return frame def portada(): W, H = 1600, 1100 out = Image.new("RGB", (W, H), BG) d = ImageDraw.Draw(out) d.text((80, 40), "OASIS MOBILE", font=font(ANTON, 92), fill=FG) bottom = d.textbbox((80, 40), "OASIS MOBILE", font=font(ANTON, 92))[3] d.rectangle((80, bottom + 12, 420, bottom + 20), fill=FG) d.text((80, 196), "Rama PRUEBAS del hacklab · base Oasis 1.3.3 · tema Acampada, pensado para el pulgar", font=font(SANS, 27), fill=MUTED) shots = [("01-portada.png", "Lanzador de módulos"), ("02-tribus.png", "Tribus en filas"), ("03-votacion.png", "Votar en semáforo"), ("07-oasismobile.png", "O el tema del dev")] pw, gap, top = 320, 50, 270 x = (W - (4 * (pw + 20) + 3 * gap)) // 2 cap = font(ANTON, 34) for shot, label in shots: fr = phone(shot, pw) out.paste(fr, (x, top), fr) tw = d.textlength(label.upper(), font=cap) d.text((x + (fr.width - tw) / 2, top + fr.height + 18), label.upper(), font=cap, fill=FG) x += fr.width + gap out.save(HERE / "00-portada.png", optimize=True) def caja(d, box, titulo, lineas, relleno=BG): x0, y0, x1, y1 = box d.rounded_rectangle((x0, y0 + 6, x1, y1 + 6), 18, fill=FG) d.rounded_rectangle(box, 18, fill=relleno, outline=FG, width=4) t = font(ANTON, 30) d.text(((x0 + x1 - d.textlength(titulo, font=t)) / 2, y0 + 18), titulo, font=t, fill=FG) s = font(SANS, 18) for i, l in enumerate(lineas): d.text(((x0 + x1 - d.textlength(l, font=s)) / 2, y0 + 70 + i * 26), l, font=s, fill=FG) def esquema(): W, H = 1400, 840 out = Image.new("RGB", (W, H), BG) d = ImageDraw.Draw(out) d.text((46, 26), "LA INTERFAZ DE MÓVIL, POR DENTRO", font=font(ANTON, 46), fill=FG) bottom = d.textbbox((46, 26), "LA INTERFAZ DE MÓVIL, POR DENTRO", font=font(ANTON, 46))[3] d.rectangle((46, bottom + 10, 260, bottom + 16), fill=FG) d.text((46, bottom + 30), "Casi sesenta módulos no caben en una pantalla de teléfono. Se reparten en tres piezas.", font=font(SANS, 18), fill=MUTED) # el movil esquematico px0, py0, px1, py1 = 110, 170, 470, 790 d.rounded_rectangle((px0, py0, px1, py1), 30, outline=FG, width=5) d.rounded_rectangle((px0 + 16, py0 + 20, px1 - 16, py0 + 84), 14, fill=BG, outline=FG, width=3) s = font(SANS, 15) d.text((px0 + 46, py0 + 42), "logo · no molestar · avatar", font=s, fill=FG) secciones = [("COMUNIDAD", 8), ("BIBLIOTECA", 2), ("GOBERNANZA", 2)] y = py0 + 110 for nombre, n in secciones: d.text((px0 + 40, y), nombre, font=font(ANTON, 17), fill=FG) d.line((px0 + 40, y + 26, px1 - 40, y + 26), fill=FG, width=3) y += 38 for i in range(n): cx = px0 + 48 + (i % 4) * 76 if i and i % 4 == 0: y += 62 d.rounded_rectangle((cx, y + 4, cx + 48, y + 52), 12, fill=FG) d.rounded_rectangle((cx, y, cx + 48, y + 48), 12, fill=BG, outline=FG, width=3) y += 72 d.rounded_rectangle((px0 + 16, py1 - 84, px1 - 16, py1 - 20), 14, fill=NEON, outline=FG, width=3) d.text((px0 + 92, py1 - 62), "barra: 4 atajos que eliges tú", font=s, fill=FG) bx0, bx1 = 560, 1320 cajas = [ ((bx0, 190, bx1, 340), "ARRIBA — LA BARRA SUPERIOR", ["El logo vuelve al inicio y el avatar abre tu perfil.", "En medio, el botón de No molestar del Teléfono.", "Con muchos módulos activos salen también los filtros Personal y Social."], BG), ((bx0, 375, bx1, 565), "EN MEDIO — EL LANZADOR", ["Los módulos activos en una rejilla de iconos, por secciones,", "como el cajón de apps del móvil. Los nombres son los del Oasis", "oficial: Comunidad, Biblioteca, Gobernanza…", "Solo en la pantalla de inicio; dentro de cada módulo, filtros en fichas."], BG), ((bx0, 600, bx1, 750), "ABAJO — LOS ATAJOS", ["Cuatro módulos a tu elección: ni uno más, que es para el pulgar.", "Se cambian en Herramientas → Tu barra. La elección se guarda", "en tu nodo, así que sigue ahí después de cerrar la aplicación."], NEON), ] flechas = [((px1 + 10, py0 + 52), (bx0 - 12, 262)), ((px1 + 10, 440), (bx0 - 12, 470)), ((px1 + 10, py1 - 52), (bx0 - 12, 675))] for (a, b) in flechas: ang = math.atan2(b[1] - a[1], b[0] - a[0]) tip = lambda da, r: (b[0] - r * math.cos(ang + da), b[1] - r * math.sin(ang + da)) d.line((a, tip(0, 12)), fill=FG, width=4) d.polygon([b, tip(0.45, 20), tip(-0.45, 20)], fill=FG) for box, t, l, r in cajas: caja(d, box, t, l, r) out.save(HERE / "esquema-interfaz.png", optimize=True) def flecha(d, a, b, ancho=4): ang = math.atan2(b[1] - a[1], b[0] - a[0]) tip = lambda da, r: (b[0] - r * math.cos(ang + da), b[1] - r * math.sin(ang + da)) d.line((a, tip(0, 12)), fill=FG, width=ancho) d.polygon([b, tip(0.45, 20), tip(-0.45, 20)], fill=FG) def titulo(d, texto, sub): d.text((46, 26), texto, font=font(ANTON, 46), fill=FG) bottom = d.textbbox((46, 26), texto, font=font(ANTON, 46))[3] d.rectangle((46, bottom + 10, 260, bottom + 16), fill=FG) d.text((46, bottom + 30), sub, font=font(SANS, 18), fill=MUTED) def esquema_rama(): """Como va la rama encima de Oasis: lo del dev abajo, lo nuestro encima.""" W, H = 1400, 760 out = Image.new("RGB", (W, H), BG) d = ImageDraw.Draw(out) titulo(d, "CÓMO VA LA RAMA ENCIMA DE OASIS", "Oasis lo cambia el dev, casi una versión al día. La rama solo cambia cómo se ve y se usa en el móvil.") capa = [ ((60, 210, 380, 400), "ASPECTO", ["mobile_base.css: la maqueta", "mobile_ux.css: márgenes, letra,", "botones y piezas nuevas", "Acampada.css: el skin"], NEON), ((400, 210, 720, 400), "PANTALLAS", ["mobile_ui.js y las vistas", "mobile_*.js. En una vista", "del dev, como mucho una", "línea que las llama"], NEON), ((740, 210, 1060, 400), "RUTAS", ["fork_routes.js y", "mobile_layer.js, delante", "del router del dev: lo que", "no atienden pasa al suyo"], NEON), ((1080, 210, 1340, 400), "ANDROID", ["envoltorio propio:", "permisos de micro y", "cámara, y el puente", "de audio del Teléfono"], NEON), ] for box, t, l, r in capa: caja(d, box, t, l, r) d.text((60, 166), "LO DE LA RAMA", font=font(ANTON, 26), fill=FG) for x in (220, 560, 900, 1210): flecha(d, (x, 418), (x, 478)) caja(d, (60, 500, 1340, 690), "OASIS DEL DEV: NO SE TOCA", ["backend.js y su middleware · los modelos (lo que se publica y cómo se cuenta) · SSB y su arranque", "las traducciones · style.css y mobile.css · los temas, también OasisMobile.css", "De 6.400 líneas cambiadas en 43 ficheros suyos (octubre) a 1.045 en 40. Ver GUIA_DE_DESARROLLO.md"], BG) out.save(HERE / "esquema-rama.png", optimize=True) def esquema_llamadas(): """Las llamadas son las del dev; la rama solo enchufa el movil.""" W, H = 1400, 700 out = Image.new("RGB", (W, H), BG) d = ImageDraw.Draw(out) titulo(d, "LLAMADAS: LAS DEL DEV, CON EL MÓVIL ENCHUFADO", "Teléfono y Salas son de Oasis. La rama no tiene llamadas propias: solo hace que suenen en Android.") cajas = [ ((50, 200, 360, 430), "TELÉFONO Y SALAS", ["De Oasis, tal cual.", "Llamadas de uno a uno", "con buzón de voz; salas", "de hasta 50, con vídeo,", "grabación y emisión."], BG), ((400, 200, 700, 430), "LOS ENCHUFES", ["setAudioFactory", "setVideoFactory", "setStreamFactory", "", "Los deja el propio dev."], BG), ((740, 200, 1040, 430), "EL PUENTE", ["main.js enchufa ahí", "phone_*_android.js:", "el audio y la cámara", "pasan por la app de", "Android."], NEON), ((1080, 200, 1350, 430), "EL MÓVIL", ["Micrófono, cámara", "y altavoz. Los permisos", "se piden la primera vez", "que llamas o entras", "en una sala."], NEON), ] for box, t, l, r in cajas: caja(d, box, t, l, r) for a, b in (((362, 315), (398, 315)), ((702, 315), (738, 315)), ((1042, 315), (1078, 315))): flecha(d, a, b) caja(d, (50, 480, 1350, 630), "¿Y CON LA APK OFICIAL?", ["No declara micrófono ni cámara y su navegador interno deniega esas peticiones:", "en Android las llamadas no pueden sonar. Por eso esta rama trae su propio envoltorio."], BG) out.save(HERE / "esquema-llamadas.png", optimize=True) if __name__ == "__main__": portada() esquema() esquema_rama() esquema_llamadas()