avatares
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
206
docs/ux_avatares_bloqueados.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# UX — Selección de avatares bloqueados y desbloqueo por anuncios
|
||||
|
||||
## Objetivo
|
||||
|
||||
Definir cómo debe verse y comportarse la pantalla de selección de avatar cuando la mayoría de avatares estén bloqueados por defecto y puedan desbloquearse mediante logros o anuncios.
|
||||
|
||||
Este documento complementa `Monetización publicidad y gamificación.md` y sirve como referencia para implementar la UI cuando se acometa el cambio.
|
||||
|
||||
---
|
||||
|
||||
## Principios de diseño
|
||||
|
||||
- El usuario debe percibir colección y progreso, no castigo.
|
||||
- Los avatares bloqueados deben tentar, no frustrar.
|
||||
- Los desbloqueados deben destacar inmediatamente.
|
||||
- El desbloqueo debe sentirse como una recompensa.
|
||||
- La pantalla debe seguir siendo limpia y usable aunque existan muchos avatares bloqueados.
|
||||
|
||||
---
|
||||
|
||||
## Estados de avatar
|
||||
|
||||
Cada avatar debe tener uno de estos estados:
|
||||
|
||||
### 1. Desbloqueado
|
||||
|
||||
- Se muestra normalmente.
|
||||
- Se puede seleccionar.
|
||||
- Al tocarlo, se elige como avatar activo.
|
||||
- Puede mostrar una pequeña marca de “desbloqueado” si ayuda visualmente.
|
||||
|
||||
### 2. Bloqueado
|
||||
|
||||
- Se muestra con un filtro suave.
|
||||
- El dibujo se intuye ligeramente, sin revelar demasiado.
|
||||
- No se puede seleccionar.
|
||||
- Puede mostrar un pequeño indicador discreto de bloqueo o un botón contextual de desbloqueo.
|
||||
|
||||
### 3. Desbloqueable por logro
|
||||
|
||||
- Igual que bloqueado, pero al tocarlo se muestra una sugerencia del tipo:
|
||||
- “Desbloquea este avatar con un logro”
|
||||
- o “Ver anuncio corto para desbloquear”
|
||||
- Nunca debe obligar; siempre debe ser opcional.
|
||||
|
||||
### 4. Desbloqueable por anuncio
|
||||
|
||||
- Se muestra la opción de ver un video corto o largo, según la regla de monetización definida.
|
||||
- Si el usuario acepta y el anuncio termina correctamente, el avatar cambia a desbloqueado.
|
||||
|
||||
---
|
||||
|
||||
## Orden de listado
|
||||
|
||||
El orden visual de la lista debe ser:
|
||||
|
||||
1. **Avatares desbloqueados**
|
||||
2. **Avatares bloqueados** mezclados en orden aleatorio
|
||||
|
||||
Reglas adicionales:
|
||||
|
||||
- El orden aleatorio de bloqueados debe recalcularse al cargar la pantalla o al cambiar el conjunto de desbloqueados.
|
||||
- No debe ser el mismo orden rígido siempre, para que la pantalla se sienta viva.
|
||||
- Los desbloqueados no deben mezclarse con los bloqueados.
|
||||
|
||||
---
|
||||
|
||||
## Apariencia visual de los bloqueados
|
||||
|
||||
### Filtro recomendado
|
||||
|
||||
El filtro debe ser sutil. La idea es que el usuario “adivine” el avatar, pero no lo vea claramente.
|
||||
|
||||
Opciones visuales permitidas:
|
||||
|
||||
- blur suave,
|
||||
- desaturación parcial,
|
||||
- oscurecimiento leve,
|
||||
- reducción de contraste,
|
||||
- ligero velo translúcido.
|
||||
|
||||
### Evitar
|
||||
|
||||
- candados gigantes,
|
||||
- overlays agresivos,
|
||||
- textos largos encima del avatar,
|
||||
- ocultar por completo la imagen,
|
||||
- una estética demasiado fea o punitiva.
|
||||
|
||||
La intención es que el avatar siga siendo tentador.
|
||||
|
||||
---
|
||||
|
||||
## Interacción al tocar un avatar bloqueado
|
||||
|
||||
Al pulsarlo:
|
||||
|
||||
1. No se selecciona.
|
||||
2. Se abre un panel o modal pequeño con:
|
||||
- nombre del avatar,
|
||||
- motivo de bloqueo o condición de desbloqueo,
|
||||
- acción disponible:
|
||||
- `Ver anuncio corto para desbloquear`,
|
||||
- `Ver anuncio largo para liberar`,
|
||||
- o `Consigue este logro para desbloquearlo`.
|
||||
3. Si el avatar no tiene desbloqueo disponible todavía, se muestra solo la condición de logro.
|
||||
|
||||
---
|
||||
|
||||
## Interacción al tocar un avatar desbloqueado
|
||||
|
||||
Al pulsarlo:
|
||||
|
||||
- se selecciona como avatar activo,
|
||||
- se resalta con animación breve,
|
||||
- puede sonar un feedback corto si está activado el sonido,
|
||||
- opcionalmente puede mostrar una etiqueta tipo “Seleccionado”.
|
||||
|
||||
---
|
||||
|
||||
## Flujo de desbloqueo por anuncio
|
||||
|
||||
### Video corto
|
||||
|
||||
Uso típico:
|
||||
|
||||
- el usuario consigue un logro,
|
||||
- ve que un avatar especial está bloqueado,
|
||||
- la app ofrece un video corto para desbloquearlo.
|
||||
|
||||
Regla:
|
||||
|
||||
- el desbloqueo puede ser temporal o permanente según la monetización definida;
|
||||
- si luego se define permanente, documentarlo explícitamente.
|
||||
|
||||
### Video largo
|
||||
|
||||
Uso típico:
|
||||
|
||||
- el usuario quiere desbloquear un avatar concreto sin esperar al logro,
|
||||
- puede ver un anuncio más largo,
|
||||
- el avatar queda desbloqueado.
|
||||
|
||||
Regla UX:
|
||||
|
||||
- el video largo debe sentirse como una elección del usuario, no como una imposición.
|
||||
|
||||
---
|
||||
|
||||
## Animaciones recomendadas
|
||||
|
||||
Al desbloquear un avatar:
|
||||
|
||||
- zoom in suave,
|
||||
- brillo breve,
|
||||
- transición de filtro bloqueado a estado normal,
|
||||
- pequeño destello de recompensa,
|
||||
- confetti muy moderado si encaja con la pantalla.
|
||||
|
||||
No usar demasiada pirotecnia. La recompensa debe sentirse elegante, no infantil.
|
||||
|
||||
---
|
||||
|
||||
## Texto sugerido en UI
|
||||
|
||||
### Bloqueado por logro
|
||||
|
||||
- `Desbloquéalo consiguiendo este logro`
|
||||
- `Completa el requisito para usarlo`
|
||||
|
||||
### Desbloqueo por anuncio corto
|
||||
|
||||
- `Ver anuncio corto y desbloquear`
|
||||
- `Desbloqueo rápido`
|
||||
|
||||
### Desbloqueo por anuncio largo
|
||||
|
||||
- `Ver anuncio largo y liberar avatar`
|
||||
- `Desbloqueo especial`
|
||||
|
||||
### Estado desbloqueado
|
||||
|
||||
- `Disponible`
|
||||
- `Desbloqueado`
|
||||
|
||||
---
|
||||
|
||||
## Reglas de compatibilidad con la app
|
||||
|
||||
- No romper la selección actual de avatar del perfil.
|
||||
- No dejar que un avatar bloqueado se guarde como avatar principal.
|
||||
- Si el avatar activo pasa a bloquearse por cambios futuros, la app debe migrar al avatar por defecto.
|
||||
- El sistema debe funcionar sin backend al principio.
|
||||
- Si más adelante hay cuenta online, la regla de desbloqueo debe seguir siendo cosmética/local salvo que se decida sincronizarla.
|
||||
|
||||
---
|
||||
|
||||
## Criterio de aceptación
|
||||
|
||||
Se considerará bien implementado cuando:
|
||||
|
||||
- los desbloqueados se vean claramente primero,
|
||||
- los bloqueados se intuyan pero no se puedan seleccionar,
|
||||
- el desbloqueo por anuncio tenga sentido visual y narrativo,
|
||||
- la pantalla siga siendo atractiva aunque el 80% de los avatares estén bloqueados,
|
||||
- el usuario entienda en un segundo qué está desbloqueado y qué no.
|
||||
@@ -62,18 +62,12 @@ class ServicioPerfilUsuario extends ChangeNotifier {
|
||||
'assets/avatars/avatar_28.png',
|
||||
'assets/avatars/avatar_29.png',
|
||||
'assets/avatars/avatar_30.png',
|
||||
'assets/avatars/capybara_01.png',
|
||||
'assets/avatars/capybara_02.png',
|
||||
'assets/avatars/capybara_03.png',
|
||||
'assets/avatars/capybara_04.png',
|
||||
'assets/avatars/capybara_05.png',
|
||||
'assets/avatars/capybara_06.png',
|
||||
'assets/avatars/capybara_07.png',
|
||||
'assets/avatars/capybara_08.png',
|
||||
'assets/avatars/capybara_09.png',
|
||||
'assets/avatars/capybara_10.png',
|
||||
'assets/avatars/capybara_11.png',
|
||||
'assets/avatars/capybara_12.png',
|
||||
'assets/avatars/avatar_79.png',
|
||||
'assets/avatars/avatar_80.png',
|
||||
'assets/avatars/avatar_81.png',
|
||||
'assets/avatars/avatar_82.png',
|
||||
'assets/avatars/avatar_83.png',
|
||||
'assets/avatars/avatar_84.png',
|
||||
'assets/avatars/avatar_31.png',
|
||||
'assets/avatars/avatar_32.png',
|
||||
'assets/avatars/avatar_33.png',
|
||||
|
||||
@@ -69,11 +69,11 @@ void main() {
|
||||
test('los avatares de capibara están disponibles en el perfil', () {
|
||||
expect(
|
||||
ServicioPerfilUsuario.avatares,
|
||||
contains('assets/avatars/capybara_01.png'),
|
||||
contains('assets/avatars/avatar_79.png'),
|
||||
);
|
||||
expect(
|
||||
ServicioPerfilUsuario.avatares,
|
||||
contains('assets/avatars/capybara_12.png'),
|
||||
contains('assets/avatars/avatar_84.png'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
154
tmp_avatar_cropper.html
Normal file
@@ -0,0 +1,154 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Farolero Avatar Cropper</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; --bg:#0b121c; --panel:#111b28; --gold:#ffc247; --line:#00e5ff; }
|
||||
body { margin:0; font-family: system-ui, Segoe UI, sans-serif; background:var(--bg); color:#ffecbe; }
|
||||
header { padding:16px 20px; border-bottom:1px solid #263241; background:#08101a; }
|
||||
h1 { margin:0; font-size:18px; color:var(--gold); }
|
||||
main { display:grid; grid-template-columns:340px 1fr; min-height:calc(100vh - 58px); }
|
||||
aside { padding:16px; background:var(--panel); border-right:1px solid #263241; overflow:auto; }
|
||||
.stageWrap { padding:16px; overflow:auto; }
|
||||
label { display:block; font-size:12px; color:#cbd5e1; margin:12px 0 6px; }
|
||||
input, button { width:100%; box-sizing:border-box; border-radius:10px; border:1px solid #344256; background:#0b121c; color:#ffecbe; padding:10px; }
|
||||
button { cursor:pointer; background:linear-gradient(#ffb232,#e88400); color:#1b1200; font-weight:700; border:0; margin-top:8px; }
|
||||
button.secondary { background:#162130; color:#ffecbe; border:1px solid #344256; }
|
||||
button.danger { background:#4a1212; color:#ffd6d6; border:1px solid #923333; }
|
||||
.row { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
||||
.hint { font-size:12px; color:#93a4b8; line-height:1.35; margin-top:12px; }
|
||||
canvas { background:#111; box-shadow:0 0 0 1px #344256,0 12px 40px #0008; cursor:crosshair; }
|
||||
.coords { font-family:ui-monospace,Consolas,monospace; font-size:12px; white-space:pre-wrap; background:#08101a; border:1px solid #263241; border-radius:10px; padding:10px; margin-top:10px; color:#b8f7ff; max-height:220px; overflow:auto; }
|
||||
.ok { color:#8cff9b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><h1>Farolero Avatar Cropper — cortes manuales</h1></header>
|
||||
<main>
|
||||
<aside>
|
||||
<label>Imagen sprite sheet</label>
|
||||
<input id="file" type="file" accept="image/*" />
|
||||
<div class="row">
|
||||
<div><label>Prefijo</label><input id="prefix" value="avatar_" /></div>
|
||||
<div><label>Inicio</label><input id="start" type="number" value="31" /></div>
|
||||
</div>
|
||||
<label>Tamaño exportado</label>
|
||||
<input id="outSize" type="number" value="256" />
|
||||
<div class="row"><button id="grid4">Reset 4x4</button><button id="addV" class="secondary">+ vertical</button></div>
|
||||
<div class="row"><button id="addH" class="secondary">+ horizontal</button><button id="clear" class="danger">Limpiar líneas</button></div>
|
||||
<label><input id="removeChroma" type="checkbox" checked style="width:auto" /> Quitar chroma verde/magenta al exportar</label>
|
||||
<button id="export">Exportar ZIP con recortes PNG</button>
|
||||
<button id="copyCoords" class="secondary">Copiar coordenadas</button>
|
||||
<p class="hint">Arrastrá las líneas cyan. Doble click añade línea vertical y horizontal. Exporta un ZIP único con todos los PNGs para evitar el bloqueo de múltiples descargas.</p>
|
||||
<div id="status" class="hint ok"></div>
|
||||
<div id="coords" class="coords"></div>
|
||||
</aside>
|
||||
<section class="stageWrap"><canvas id="canvas"></canvas></section>
|
||||
</main>
|
||||
<script>
|
||||
const canvas=document.getElementById('canvas');
|
||||
const ctx=canvas.getContext('2d');
|
||||
const file=document.getElementById('file');
|
||||
const coordsEl=document.getElementById('coords');
|
||||
const statusEl=document.getElementById('status');
|
||||
let bitmap=null, vLines=[], hLines=[], dragging=null;
|
||||
const hit=8;
|
||||
function pad(n){ return String(n).padStart(2,'0'); }
|
||||
function sortLines(){ vLines=[...new Set(vLines.map(Math.round))].sort((a,b)=>a-b); hLines=[...new Set(hLines.map(Math.round))].sort((a,b)=>a-b); }
|
||||
function reset4x4(){ if(!bitmap) return; vLines=[0,bitmap.width/4,bitmap.width/2,bitmap.width*3/4,bitmap.width]; hLines=[0,bitmap.height/4,bitmap.height/2,bitmap.height*3/4,bitmap.height]; sortLines(); draw(); }
|
||||
function draw(){
|
||||
if(!bitmap) return;
|
||||
canvas.width=bitmap.width; canvas.height=bitmap.height;
|
||||
ctx.clearRect(0,0,canvas.width,canvas.height);
|
||||
ctx.drawImage(bitmap,0,0);
|
||||
ctx.save(); ctx.lineWidth=3; ctx.strokeStyle='rgba(0,229,255,.95)'; ctx.fillStyle='rgba(0,229,255,.95)'; ctx.font='14px ui-monospace, Consolas';
|
||||
for(const x of vLines){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,canvas.height); ctx.stroke(); ctx.fillText(Math.round(x),x+4,14); }
|
||||
for(const y of hLines){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(canvas.width,y); ctx.stroke(); ctx.fillText(Math.round(y),4,Math.max(14,y-4)); }
|
||||
ctx.restore(); updateCoords();
|
||||
}
|
||||
function updateCoords(){
|
||||
sortLines(); const rects=[];
|
||||
for(let r=0;r<hLines.length-1;r++) for(let c=0;c<vLines.length-1;c++) rects.push([vLines[c],hLines[r],vLines[c+1],hLines[r+1]]);
|
||||
coordsEl.textContent=JSON.stringify({vLines,hLines,rects},null,2);
|
||||
statusEl.textContent=bitmap?`${rects.length} recortes listos`:'';
|
||||
}
|
||||
file.onchange=e=>{
|
||||
const f=e.target.files[0]; if(!f) return;
|
||||
const url=URL.createObjectURL(f);
|
||||
const img=new Image();
|
||||
img.onload=()=>{ bitmap=img; reset4x4(); URL.revokeObjectURL(url); };
|
||||
img.onerror=()=>{ statusEl.textContent='No pude cargar la imagen.'; URL.revokeObjectURL(url); };
|
||||
img.src=url;
|
||||
};
|
||||
function mousePos(e){ const r=canvas.getBoundingClientRect(); return {x:(e.clientX-r.left)*(canvas.width/r.width), y:(e.clientY-r.top)*(canvas.height/r.height)}; }
|
||||
canvas.onmousedown=e=>{
|
||||
if(!bitmap) return; const p=mousePos(e); let best=null;
|
||||
for(let i=0;i<vLines.length;i++){ const d=Math.abs(vLines[i]-p.x); if(d<hit&&(!best||d<best.d)) best={type:'v',i,d}; }
|
||||
for(let i=0;i<hLines.length;i++){ const d=Math.abs(hLines[i]-p.y); if(d<hit&&(!best||d<best.d)) best={type:'h',i,d}; }
|
||||
dragging=best;
|
||||
};
|
||||
canvas.onmousemove=e=>{ if(!dragging||!bitmap) return; const p=mousePos(e); if(dragging.type==='v') vLines[dragging.i]=Math.max(0,Math.min(bitmap.width,p.x)); else hLines[dragging.i]=Math.max(0,Math.min(bitmap.height,p.y)); draw(); };
|
||||
window.onmouseup=()=>{ if(dragging){ sortLines(); dragging=null; draw(); } };
|
||||
canvas.ondblclick=e=>{ if(!bitmap) return; const p=mousePos(e); vLines.push(p.x); hLines.push(p.y); sortLines(); draw(); };
|
||||
document.getElementById('grid4').onclick=reset4x4;
|
||||
document.getElementById('addV').onclick=()=>{ if(bitmap){ vLines.push(bitmap.width/2); sortLines(); draw(); } };
|
||||
document.getElementById('addH').onclick=()=>{ if(bitmap){ hLines.push(bitmap.height/2); sortLines(); draw(); } };
|
||||
document.getElementById('clear').onclick=()=>{ if(bitmap){ vLines=[0,bitmap.width]; hLines=[0,bitmap.height]; draw(); } };
|
||||
document.getElementById('copyCoords').onclick=async()=>{ await navigator.clipboard.writeText(coordsEl.textContent); statusEl.textContent='Coordenadas copiadas'; };
|
||||
function chromaToAlpha(imageData){
|
||||
const d=imageData.data; let green=0,mag=0;
|
||||
for(let i=0;i<d.length;i+=4){ if(d[i+1]>180&&d[i]<140&&d[i+2]<140) green++; if(d[i]>180&&d[i+2]>180&&d[i+1]<160) mag++; }
|
||||
for(let i=0;i<d.length;i+=4){ const r=d[i],g=d[i+1],b=d[i+2]; if(green>=mag){ if(g>150&&r<150&&b<150) d[i+3]=0; } else { if(r>150&&b>150&&g<160) d[i+3]=0; } }
|
||||
return imageData;
|
||||
}
|
||||
function crc32(bytes){
|
||||
let table=crc32.table; if(!table){ table=crc32.table=[]; for(let n=0;n<256;n++){ let c=n; for(let k=0;k<8;k++) c=(c&1)?(0xedb88320^(c>>>1)):(c>>>1); table[n]=c>>>0; } }
|
||||
let c=0xffffffff; for(const b of bytes) c=table[(c^b)&0xff]^(c>>>8); return (c^0xffffffff)>>>0;
|
||||
}
|
||||
function u16(n){ return [n&255,(n>>>8)&255]; }
|
||||
function u32(n){ return [n&255,(n>>>8)&255,(n>>>16)&255,(n>>>24)&255]; }
|
||||
function bytes(s){ return new TextEncoder().encode(s); }
|
||||
async function canvasBlob(c){ return await new Promise(resolve=>c.toBlob(resolve,'image/png')); }
|
||||
async function makeZip(entries){
|
||||
const chunks=[], central=[]; let offset=0;
|
||||
for(const e of entries){
|
||||
const nameBytes=bytes(e.name); const data=new Uint8Array(await e.blob.arrayBuffer()); const crc=crc32(data);
|
||||
const local=new Uint8Array([...u32(0x04034b50),...u16(20),...u16(0),...u16(0),...u16(0),...u16(0),...u32(crc),...u32(data.length),...u32(data.length),...u16(nameBytes.length),...u16(0)]);
|
||||
chunks.push(local,nameBytes,data);
|
||||
central.push({fileName:e.name,dataLen:data.length,crc,offset});
|
||||
offset+=local.length+nameBytes.length+data.length;
|
||||
}
|
||||
const centralStart=offset;
|
||||
for(const e of central){
|
||||
const nameBytes=bytes(e.fileName);
|
||||
const cdir=new Uint8Array([...u32(0x02014b50),...u16(20),...u16(20),...u16(0),...u16(0),...u16(0),...u16(0),...u32(e.crc),...u32(e.dataLen),...u32(e.dataLen),...u16(nameBytes.length),...u16(0),...u16(0),...u16(0),...u16(0),...u32(0),...u32(e.offset)]);
|
||||
chunks.push(cdir,nameBytes); offset+=cdir.length+nameBytes.length;
|
||||
}
|
||||
const centralSize=offset-centralStart;
|
||||
chunks.push(new Uint8Array([...u32(0x06054b50),...u16(0),...u16(0),...u16(central.length),...u16(central.length),...u32(centralSize),...u32(centralStart),...u16(0)]));
|
||||
return new Blob(chunks,{type:'application/zip'});
|
||||
}
|
||||
document.getElementById('export').onclick=async()=>{
|
||||
if(!bitmap) return; sortLines();
|
||||
const outSize=Number(document.getElementById('outSize').value)||256;
|
||||
const prefix=document.getElementById('prefix').value||'avatar_';
|
||||
let n=Number(document.getElementById('start').value)||1;
|
||||
const remove=document.getElementById('removeChroma').checked;
|
||||
const entries=[];
|
||||
for(let r=0;r<hLines.length-1;r++) for(let c=0;c<vLines.length-1;c++){
|
||||
const sx=vLines[c],sy=hLines[r],sw=vLines[c+1]-sx,sh=hLines[r+1]-sy; if(sw<8||sh<8) continue;
|
||||
const off=document.createElement('canvas'); off.width=outSize; off.height=outSize;
|
||||
const ox=off.getContext('2d'); ox.clearRect(0,0,outSize,outSize); ox.drawImage(bitmap,sx,sy,sw,sh,0,0,outSize,outSize);
|
||||
if(remove){ const data=ox.getImageData(0,0,outSize,outSize); ox.putImageData(chromaToAlpha(data),0,0); }
|
||||
entries.push({name:`${prefix}${pad(n)}.png`, blob:await canvasBlob(off)}); n++;
|
||||
}
|
||||
const zip=await makeZip(entries);
|
||||
const a=document.createElement('a'); a.download=`${prefix}${pad(Number(document.getElementById('start').value)||1)}_recortes.zip`; a.href=URL.createObjectURL(zip); a.click();
|
||||
setTimeout(()=>URL.revokeObjectURL(a.href),30000);
|
||||
statusEl.textContent=`ZIP exportado con ${entries.length} PNGs.`;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
tmp_avatar_slices_smart/avatar_31.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
tmp_avatar_slices_smart/avatar_32.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
tmp_avatar_slices_smart/avatar_33.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
tmp_avatar_slices_smart/avatar_34.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
tmp_avatar_slices_smart/avatar_35.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
tmp_avatar_slices_smart/avatar_36.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
tmp_avatar_slices_smart/avatar_37.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
tmp_avatar_slices_smart/avatar_38.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
tmp_avatar_slices_smart/avatar_39.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
tmp_avatar_slices_smart/avatar_40.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
tmp_avatar_slices_smart/avatar_41.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
tmp_avatar_slices_smart/avatar_42.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
tmp_avatar_slices_smart/avatar_43.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
tmp_avatar_slices_smart/avatar_44.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
tmp_avatar_slices_smart/avatar_45.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
tmp_avatar_slices_smart/avatar_46.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
tmp_avatar_slices_smart/avatar_47.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
tmp_avatar_slices_smart/avatar_48.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
tmp_avatar_slices_smart/avatar_49.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
tmp_avatar_slices_smart/avatar_50.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
tmp_avatar_slices_smart/avatar_51.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
tmp_avatar_slices_smart/avatar_52.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
tmp_avatar_slices_smart/avatar_53.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
tmp_avatar_slices_smart/avatar_54.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
tmp_avatar_slices_smart/avatar_55.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
tmp_avatar_slices_smart/avatar_56.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
tmp_avatar_slices_smart/avatar_57.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
tmp_avatar_slices_smart/avatar_58.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
tmp_avatar_slices_smart/avatar_59.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
tmp_avatar_slices_smart/avatar_60.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
tmp_avatar_slices_smart/avatar_61.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
tmp_avatar_slices_smart/avatar_62.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
tmp_avatar_slices_smart/avatar_63.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
tmp_avatar_slices_smart/avatar_64.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
tmp_avatar_slices_smart/avatar_65.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
tmp_avatar_slices_smart/avatar_66.png
Normal file
|
After Width: | Height: | Size: 125 KiB |