feat: IdleCompanion widget (v0.25.0)
Mascota animada reutilizable que aparece tras inactividad:
- Se asoma por bordes de paneles aleatorios
- Clipping correcto (respeta límites del panel)
- Ojos que miran izq/der, salto de pánico
- Estados: hidden → peeking → watching → hiding
- Diseño gato: orejas puntiagudas, pupilas verticales, mejillas
Uso:
const IdleCompanion = zcatgui.widgets.idle_companion;
var state: IdleCompanion.State = .{};
IdleCompanion.draw(ctx, &panels, &state, last_activity, color);
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d8f04f85bc
commit
3af97f6174
4 changed files with 528 additions and 3 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -44,6 +44,7 @@
|
||||||
| 2025-12-19 | v0.22.2 | Cursor blink rate: 500ms→300ms (más responsive durante edición) |
|
| 2025-12-19 | v0.22.2 | Cursor blink rate: 500ms→300ms (más responsive durante edición) |
|
||||||
| 2025-12-30 | v0.23.0 | ⭐ FilledTriangle primitive: scanline rasterizer for 3D graphics |
|
| 2025-12-30 | v0.23.0 | ⭐ FilledTriangle primitive: scanline rasterizer for 3D graphics |
|
||||||
| 2025-12-30 | v0.24.0 | ⭐ FilledCircle primitive: Midpoint Circle Algorithm (Bresenham) |
|
| 2025-12-30 | v0.24.0 | ⭐ FilledCircle primitive: Midpoint Circle Algorithm (Bresenham) |
|
||||||
|
| 2025-12-30 | v0.25.0 | ⭐ IdleCompanion widget: mascota animada que aparece tras inactividad |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -86,3 +87,30 @@ Nuevas primitivas para gráficos 2D y mascotas animadas:
|
||||||
|
|
||||||
→ Archivos: `core/command.zig`, `render/software.zig`
|
→ Archivos: `core/command.zig`, `render/software.zig`
|
||||||
→ Doc: `zsimifactu/docs/PLAN_CIRCULOS_Y_ZCAT_2025-12-30.md`
|
→ Doc: `zsimifactu/docs/PLAN_CIRCULOS_Y_ZCAT_2025-12-30.md`
|
||||||
|
|
||||||
|
### v0.25.0 - IdleCompanion Widget (2025-12-30)
|
||||||
|
Sistema de mascota animada reutilizable para cualquier aplicación:
|
||||||
|
|
||||||
|
- **Características:**
|
||||||
|
- Aparece tras periodo configurable de inactividad (default: 15s)
|
||||||
|
- Se asoma por bordes de paneles/rectángulos aleatorios
|
||||||
|
- Animación suave con clipping correcto (respeta bordes)
|
||||||
|
- Ojos que miran a los lados (izq/centro/der)
|
||||||
|
- "Salto de pánico" al detectar actividad del usuario
|
||||||
|
- Diseño de gato con orejas puntiagudas, pupilas verticales, mejillas y bigotes
|
||||||
|
|
||||||
|
- **Estados de animación:**
|
||||||
|
- `hidden`: Esperando idle
|
||||||
|
- `peeking`: Asomándose lentamente (4s)
|
||||||
|
- `watching`: Completamente visible, mirando alrededor (3s)
|
||||||
|
- `hiding`: Bajando normal (1s) o salto de pánico (0.2s)
|
||||||
|
|
||||||
|
- **Uso:**
|
||||||
|
```zig
|
||||||
|
const IdleCompanion = zcatgui.widgets.idle_companion;
|
||||||
|
var state: IdleCompanion.State = .{};
|
||||||
|
const panels = [_]IdleCompanion.Rect{ ... };
|
||||||
|
IdleCompanion.draw(ctx, &panels, &state, last_activity, color);
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Archivo: `widgets/idle_companion.zig`
|
||||||
|
|
|
||||||
20
CLAUDE.md
20
CLAUDE.md
|
|
@ -247,9 +247,9 @@ zcatgui/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ESTADO ACTUAL (v0.21.2)
|
## ESTADO ACTUAL (v0.25.0)
|
||||||
|
|
||||||
### Widgets (37 total)
|
### Widgets (38 total)
|
||||||
|
|
||||||
| Categoría | Widgets |
|
| Categoría | Widgets |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
|
|
@ -258,7 +258,7 @@ zcatgui/
|
||||||
| **Datos** | List, Table, Tree, ReorderableList, VirtualScroll |
|
| **Datos** | List, Table, Tree, ReorderableList, VirtualScroll |
|
||||||
| **Feedback** | Progress, Tooltip, Toast, Spinner |
|
| **Feedback** | Progress, Tooltip, Toast, Spinner |
|
||||||
| **Input avanzado** | AutoComplete, Select, TextArea, ColorPicker, DatePicker |
|
| **Input avanzado** | AutoComplete, Select, TextArea, ColorPicker, DatePicker |
|
||||||
| **Especial** | Image, Icon, Canvas, Chart, RichText |
|
| **Especial** | Image, Icon, Canvas, Chart, RichText, **IdleCompanion** |
|
||||||
| **Navegación** | Breadcrumb, Focus, Badge/TagGroup |
|
| **Navegación** | Breadcrumb, Focus, Badge/TagGroup |
|
||||||
|
|
||||||
### Backends (5 plataformas)
|
### Backends (5 plataformas)
|
||||||
|
|
@ -285,6 +285,20 @@ zcatgui/
|
||||||
|
|
||||||
## HITOS RECIENTES
|
## HITOS RECIENTES
|
||||||
|
|
||||||
|
### IdleCompanion Widget ✅ (2025-12-30)
|
||||||
|
Mascota animada que aparece tras inactividad del usuario:
|
||||||
|
- Se asoma por bordes de paneles aleatorios
|
||||||
|
- Ojos que miran a los lados, orejas con movimiento
|
||||||
|
- Salto de pánico al detectar actividad
|
||||||
|
- Clipping correcto (respeta límites del panel)
|
||||||
|
→ Archivo: `widgets/idle_companion.zig`
|
||||||
|
|
||||||
|
### Primitivas Gráficas 2D ✅ (2025-12-30)
|
||||||
|
Nuevas primitivas para formas orgánicas:
|
||||||
|
- **FilledTriangle**: Rasterización por scanlines (v0.23.0)
|
||||||
|
- **FilledCircle**: Algoritmo Midpoint/Bresenham (v0.24.0)
|
||||||
|
→ Archivos: `core/command.zig`, `render/software.zig`
|
||||||
|
|
||||||
### Refactorización Modular ✅ (2025-12-29)
|
### Refactorización Modular ✅ (2025-12-29)
|
||||||
Archivos grandes modularizados en carpetas:
|
Archivos grandes modularizados en carpetas:
|
||||||
- `autocomplete/` (910→571 LOC hub, -37%): state, types, filtering
|
- `autocomplete/` (910→571 LOC hub, -37%): state, types, filtering
|
||||||
|
|
|
||||||
474
src/widgets/idle_companion.zig
Normal file
474
src/widgets/idle_companion.zig
Normal file
|
|
@ -0,0 +1,474 @@
|
||||||
|
//! Idle Companion System - Zcat "The Drifter"
|
||||||
|
//!
|
||||||
|
//! Un sistema de mascota animada que aparece cuando el usuario está inactivo.
|
||||||
|
//! El gatito se asoma por los bordes de rectángulos definidos, mira alrededor,
|
||||||
|
//! y desaparece con un "salto de pánico" si detecta actividad.
|
||||||
|
//!
|
||||||
|
//! ## Características
|
||||||
|
//! - Aparece tras periodo configurable de inactividad (default: 15s)
|
||||||
|
//! - Se asoma por bordes de paneles/rectángulos aleatorios
|
||||||
|
//! - Animación suave con clipping correcto
|
||||||
|
//! - Ojos que miran a los lados
|
||||||
|
//! - "Salto de pánico" al detectar actividad del usuario
|
||||||
|
//! - Diseño de gato con orejas puntiagudas, pupilas verticales, mejillas y bigotes
|
||||||
|
//!
|
||||||
|
//! ## Uso básico
|
||||||
|
//! ```zig
|
||||||
|
//! const zcatgui = @import("zcatgui");
|
||||||
|
//! const IdleCompanion = zcatgui.widgets.IdleCompanion;
|
||||||
|
//!
|
||||||
|
//! // Estado persistente
|
||||||
|
//! var companion_state: IdleCompanion.State = .{};
|
||||||
|
//!
|
||||||
|
//! // Lista de rectángulos donde puede aparecer
|
||||||
|
//! const panels = [_]zcatgui.Rect{
|
||||||
|
//! .{ .x = 0, .y = 50, .w = 300, .h = 400 },
|
||||||
|
//! .{ .x = 304, .y = 50, .w = 300, .h = 200 },
|
||||||
|
//! };
|
||||||
|
//!
|
||||||
|
//! // En el loop de renderizado:
|
||||||
|
//! IdleCompanion.draw(ctx, &panels, &companion_state, last_activity_timestamp, theme.primary);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Personalización
|
||||||
|
//! Los tiempos son configurables mediante constantes en State:
|
||||||
|
//! - IDLE_THRESHOLD_MS: Tiempo de inactividad para aparecer (15s)
|
||||||
|
//! - PEEK_DURATION_MS: Duración del asomarse (4s)
|
||||||
|
//! - WATCH_DURATION_MS: Duración mirando alrededor (3s)
|
||||||
|
//! - HIDE_DURATION_MS: Duración al esconderse (1s)
|
||||||
|
//! - PANIC_HIDE_DURATION_MS: Duración del salto de pánico (0.2s)
|
||||||
|
//! - RELOCATE_DELAY_MS: Pausa antes de reaparecer (5s)
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Context = @import("../core/context.zig").Context;
|
||||||
|
const Style = @import("../core/style.zig");
|
||||||
|
const Color = Style.Color;
|
||||||
|
|
||||||
|
/// Rectángulo (compatible con zcatgui.Rect)
|
||||||
|
pub const Rect = struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Estado de animación del compañero
|
||||||
|
pub const AnimState = enum {
|
||||||
|
/// No visible, esperando idle
|
||||||
|
hidden,
|
||||||
|
/// Asomándose lentamente, mirando izq/der
|
||||||
|
peeking,
|
||||||
|
/// Completamente visible, mirando alrededor
|
||||||
|
watching,
|
||||||
|
/// Bajando (normal o salto de pánico)
|
||||||
|
hiding,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Estado completo del sistema Idle Companion
|
||||||
|
pub const State = struct {
|
||||||
|
/// Estado actual de la animación
|
||||||
|
anim_state: AnimState = .hidden,
|
||||||
|
|
||||||
|
/// Índice del panel/rectángulo donde está el compañero
|
||||||
|
panel_index: usize = 0,
|
||||||
|
|
||||||
|
/// Posición X relativa al panel (0.0 - 1.0)
|
||||||
|
x_ratio: f32 = 0.5,
|
||||||
|
|
||||||
|
/// Progreso de aparición (0 = oculto, 1 = completamente visible)
|
||||||
|
appear_progress: f32 = 0.0,
|
||||||
|
|
||||||
|
/// Dirección de mirada (-1 = izq, 0 = centro, 1 = der)
|
||||||
|
look_dir: i8 = 0,
|
||||||
|
|
||||||
|
/// Tiempo en que entró al estado actual
|
||||||
|
state_start_time: i64 = 0,
|
||||||
|
|
||||||
|
/// Fase de animación general (para orejas)
|
||||||
|
anim_phase: f32 = 0,
|
||||||
|
|
||||||
|
/// Si el usuario causó actividad durante la animación (salto de pánico)
|
||||||
|
panic_jump: bool = false,
|
||||||
|
|
||||||
|
/// Último timestamp de actividad registrado
|
||||||
|
last_known_activity: i64 = 0,
|
||||||
|
|
||||||
|
/// Semilla para números pseudo-aleatorios (LCG simple)
|
||||||
|
random_seed: u32 = 12345,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Constantes de tiempo (personalizables)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Tiempo de inactividad para aparecer (ms)
|
||||||
|
pub const IDLE_THRESHOLD_MS: i64 = 15_000;
|
||||||
|
/// Duración del estado peeking (ms)
|
||||||
|
pub const PEEK_DURATION_MS: i64 = 4_000;
|
||||||
|
/// Duración del estado watching (ms)
|
||||||
|
pub const WATCH_DURATION_MS: i64 = 3_000;
|
||||||
|
/// Duración al esconderse normal (ms)
|
||||||
|
pub const HIDE_DURATION_MS: i64 = 1_000;
|
||||||
|
/// Duración del salto de pánico (ms)
|
||||||
|
pub const PANIC_HIDE_DURATION_MS: i64 = 200;
|
||||||
|
/// Pausa antes de reaparecer en otro lugar (ms)
|
||||||
|
pub const RELOCATE_DELAY_MS: i64 = 5_000;
|
||||||
|
|
||||||
|
/// Generador de números pseudo-aleatorios simple (LCG)
|
||||||
|
fn nextRandom(self: *State) u32 {
|
||||||
|
self.random_seed = self.random_seed *% 1103515245 +% 12345;
|
||||||
|
return self.random_seed >> 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Random float 0.0 - 1.0
|
||||||
|
fn randomFloat(self: *State) f32 {
|
||||||
|
return @as(f32, @floatFromInt(self.nextRandom() % 10000)) / 10000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elige un panel y posición aleatorios
|
||||||
|
fn pickRandomLocation(self: *State, num_panels: usize) void {
|
||||||
|
if (num_panels == 0) return;
|
||||||
|
|
||||||
|
// Panel aleatorio
|
||||||
|
self.panel_index = self.nextRandom() % @as(u32, @intCast(num_panels));
|
||||||
|
|
||||||
|
// Posición X aleatoria (25% - 75% del ancho)
|
||||||
|
self.x_ratio = 0.25 + self.randomFloat() * 0.5;
|
||||||
|
|
||||||
|
// Resetear
|
||||||
|
self.appear_progress = 0.0;
|
||||||
|
self.look_dir = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actualiza el estado según el tiempo de inactividad
|
||||||
|
pub fn update(self: *State, last_activity: i64, num_panels: usize) void {
|
||||||
|
const now = std.time.milliTimestamp();
|
||||||
|
const idle_time = now - last_activity;
|
||||||
|
const state_elapsed = now - self.state_start_time;
|
||||||
|
|
||||||
|
// Detectar si hubo nueva actividad (para salto de pánico)
|
||||||
|
if (last_activity > self.last_known_activity and self.anim_state != .hidden) {
|
||||||
|
self.panic_jump = true;
|
||||||
|
self.last_known_activity = last_activity;
|
||||||
|
if (self.anim_state != .hiding) {
|
||||||
|
self.anim_state = .hiding;
|
||||||
|
self.state_start_time = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.last_known_activity = last_activity;
|
||||||
|
|
||||||
|
// Actualizar fase de animación general
|
||||||
|
if (self.anim_state != .hidden) {
|
||||||
|
self.anim_phase = @as(f32, @floatFromInt(@mod(now, 2000))) / 2000.0 * std.math.pi * 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Máquina de estados
|
||||||
|
switch (self.anim_state) {
|
||||||
|
.hidden => {
|
||||||
|
if (idle_time >= IDLE_THRESHOLD_MS and num_panels > 0) {
|
||||||
|
self.pickRandomLocation(num_panels);
|
||||||
|
self.anim_state = .peeking;
|
||||||
|
self.state_start_time = now;
|
||||||
|
self.panic_jump = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.peeking => {
|
||||||
|
// Subir lentamente (0 -> 1) con ease-out
|
||||||
|
const progress = @as(f32, @floatFromInt(@min(state_elapsed, PEEK_DURATION_MS))) /
|
||||||
|
@as(f32, @floatFromInt(PEEK_DURATION_MS));
|
||||||
|
self.appear_progress = 1.0 - (1.0 - progress) * (1.0 - progress);
|
||||||
|
|
||||||
|
// Mirar a los lados
|
||||||
|
const look_cycle = @mod(state_elapsed, 2000);
|
||||||
|
if (look_cycle < 600) {
|
||||||
|
self.look_dir = -1;
|
||||||
|
} else if (look_cycle < 1200) {
|
||||||
|
self.look_dir = 1;
|
||||||
|
} else {
|
||||||
|
self.look_dir = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state_elapsed >= PEEK_DURATION_MS) {
|
||||||
|
self.anim_state = .watching;
|
||||||
|
self.state_start_time = now;
|
||||||
|
self.appear_progress = 1.0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.watching => {
|
||||||
|
// Mirar alrededor más rápido
|
||||||
|
const look_cycle = @mod(state_elapsed, 1200);
|
||||||
|
if (look_cycle < 400) {
|
||||||
|
self.look_dir = -1;
|
||||||
|
} else if (look_cycle < 800) {
|
||||||
|
self.look_dir = 1;
|
||||||
|
} else {
|
||||||
|
self.look_dir = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state_elapsed >= WATCH_DURATION_MS) {
|
||||||
|
self.anim_state = .hiding;
|
||||||
|
self.state_start_time = now;
|
||||||
|
self.panic_jump = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.hiding => {
|
||||||
|
const hide_duration = if (self.panic_jump) PANIC_HIDE_DURATION_MS else HIDE_DURATION_MS;
|
||||||
|
const progress = @as(f32, @floatFromInt(@min(state_elapsed, hide_duration))) /
|
||||||
|
@as(f32, @floatFromInt(hide_duration));
|
||||||
|
|
||||||
|
if (self.panic_jump) {
|
||||||
|
// Salto de pánico: bajada muy rápida (ease-in)
|
||||||
|
self.appear_progress = 1.0 - progress * progress;
|
||||||
|
} else {
|
||||||
|
// Bajada suave
|
||||||
|
self.appear_progress = 1.0 - progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state_elapsed >= hide_duration) {
|
||||||
|
self.anim_state = .hidden;
|
||||||
|
self.state_start_time = now + RELOCATE_DELAY_MS;
|
||||||
|
self.appear_progress = 0;
|
||||||
|
self.panic_jump = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retorna true si el compañero es visible
|
||||||
|
pub fn isVisible(self: *const State) bool {
|
||||||
|
return self.anim_state != .hidden;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Dimensiones del gato
|
||||||
|
const CAT_WIDTH: i32 = 40;
|
||||||
|
const CAT_HEIGHT: i32 = 35;
|
||||||
|
|
||||||
|
/// Dibuja el compañero idle en los paneles especificados
|
||||||
|
///
|
||||||
|
/// Parámetros:
|
||||||
|
/// - ctx: Contexto de renderizado
|
||||||
|
/// - panels: Slice de rectángulos donde puede aparecer el compañero
|
||||||
|
/// - state: Estado persistente del compañero
|
||||||
|
/// - last_activity: Timestamp de la última actividad del usuario (ms)
|
||||||
|
/// - color: Color principal del pelaje
|
||||||
|
pub fn draw(
|
||||||
|
ctx: *Context,
|
||||||
|
panels: []const Rect,
|
||||||
|
state: *State,
|
||||||
|
last_activity: i64,
|
||||||
|
color: Color,
|
||||||
|
) void {
|
||||||
|
state.update(last_activity, panels.len);
|
||||||
|
|
||||||
|
if (!state.isVisible()) return;
|
||||||
|
if (state.panel_index >= panels.len) return;
|
||||||
|
|
||||||
|
const panel_rect = panels[state.panel_index];
|
||||||
|
drawCat(ctx, panel_rect, state, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dibuja el gato asomándose por el borde inferior del panel
|
||||||
|
fn drawCat(ctx: *Context, panel_rect: Rect, state: *const State, color: Color) void {
|
||||||
|
const panel_w = @as(i32, @intCast(panel_rect.w));
|
||||||
|
const panel_h = @as(i32, @intCast(panel_rect.h));
|
||||||
|
|
||||||
|
// Posición X centrada en el ratio del panel
|
||||||
|
const cat_x = panel_rect.x + @as(i32, @intFromFloat(state.x_ratio * @as(f32, @floatFromInt(panel_w)))) - CAT_WIDTH / 2;
|
||||||
|
|
||||||
|
// Cuánto del gato es visible
|
||||||
|
const visible_amount = @as(i32, @intFromFloat(state.appear_progress * @as(f32, CAT_HEIGHT)));
|
||||||
|
|
||||||
|
// Posición Y: sube desde el borde inferior
|
||||||
|
const cat_y = panel_rect.y + panel_h - visible_amount;
|
||||||
|
|
||||||
|
// === ESTABLECER CLIP al interior del panel ===
|
||||||
|
ctx.pushCommand(.{ .clip = .{
|
||||||
|
.x = panel_rect.x,
|
||||||
|
.y = panel_rect.y,
|
||||||
|
.w = panel_rect.w,
|
||||||
|
.h = panel_rect.h,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// Colores del gato
|
||||||
|
const fur_color = color;
|
||||||
|
const dark_fur = Color{
|
||||||
|
.r = color.r -| 40,
|
||||||
|
.g = color.g -| 40,
|
||||||
|
.b = color.b -| 40,
|
||||||
|
.a = 255,
|
||||||
|
};
|
||||||
|
const light_fur = Color{
|
||||||
|
.r = color.r +| 30,
|
||||||
|
.g = color.g +| 20,
|
||||||
|
.b = color.b,
|
||||||
|
.a = 255,
|
||||||
|
};
|
||||||
|
const pink = Color{ .r = 255, .g = 170, .b = 170, .a = 255 };
|
||||||
|
const eye_white = Color{ .r = 255, .g = 255, .b = 255, .a = 255 };
|
||||||
|
const eye_pupil = Color{ .r = 30, .g = 30, .b = 30, .a = 255 };
|
||||||
|
|
||||||
|
// Offset de mirada
|
||||||
|
const look_offset: i32 = state.look_dir * 2;
|
||||||
|
|
||||||
|
// Animación de orejas
|
||||||
|
const ear_wiggle = @as(i32, @intFromFloat(std.math.sin(state.anim_phase * 3.0) * 1.5));
|
||||||
|
|
||||||
|
// === OREJAS ===
|
||||||
|
// Oreja izquierda
|
||||||
|
ctx.pushCommand(.{ .filled_triangle = .{
|
||||||
|
.x1 = cat_x + 5,
|
||||||
|
.y1 = cat_y + 18,
|
||||||
|
.x2 = cat_x + 15,
|
||||||
|
.y2 = cat_y + 16,
|
||||||
|
.x3 = cat_x + 4 + ear_wiggle,
|
||||||
|
.y3 = cat_y - 2,
|
||||||
|
.color = fur_color,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .filled_triangle = .{
|
||||||
|
.x1 = cat_x + 7,
|
||||||
|
.y1 = cat_y + 16,
|
||||||
|
.x2 = cat_x + 13,
|
||||||
|
.y2 = cat_y + 15,
|
||||||
|
.x3 = cat_x + 6 + ear_wiggle,
|
||||||
|
.y3 = cat_y + 2,
|
||||||
|
.color = pink,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// Oreja derecha
|
||||||
|
ctx.pushCommand(.{ .filled_triangle = .{
|
||||||
|
.x1 = cat_x + 25,
|
||||||
|
.y1 = cat_y + 16,
|
||||||
|
.x2 = cat_x + 35,
|
||||||
|
.y2 = cat_y + 18,
|
||||||
|
.x3 = cat_x + 36 - ear_wiggle,
|
||||||
|
.y3 = cat_y - 2,
|
||||||
|
.color = fur_color,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .filled_triangle = .{
|
||||||
|
.x1 = cat_x + 27,
|
||||||
|
.y1 = cat_y + 15,
|
||||||
|
.x2 = cat_x + 33,
|
||||||
|
.y2 = cat_y + 16,
|
||||||
|
.x3 = cat_x + 34 - ear_wiggle,
|
||||||
|
.y3 = cat_y + 2,
|
||||||
|
.color = pink,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// === CABEZA ===
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{
|
||||||
|
.cx = cat_x + 20,
|
||||||
|
.cy = cat_y + 20,
|
||||||
|
.radius = 14,
|
||||||
|
.color = fur_color,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// Mejillas
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{
|
||||||
|
.cx = cat_x + 8,
|
||||||
|
.cy = cat_y + 24,
|
||||||
|
.radius = 6,
|
||||||
|
.color = light_fur,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{
|
||||||
|
.cx = cat_x + 32,
|
||||||
|
.cy = cat_y + 24,
|
||||||
|
.radius = 6,
|
||||||
|
.color = light_fur,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// === OJOS ===
|
||||||
|
// Ojo izquierdo
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{
|
||||||
|
.cx = cat_x + 13 + look_offset,
|
||||||
|
.cy = cat_y + 17,
|
||||||
|
.radius = 5,
|
||||||
|
.color = eye_white,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .rect = .{
|
||||||
|
.x = cat_x + 12 + look_offset,
|
||||||
|
.y = cat_y + 14,
|
||||||
|
.w = 3,
|
||||||
|
.h = 7,
|
||||||
|
.color = eye_pupil,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .rect = .{
|
||||||
|
.x = cat_x + 11 + look_offset,
|
||||||
|
.y = cat_y + 14,
|
||||||
|
.w = 2,
|
||||||
|
.h = 2,
|
||||||
|
.color = eye_white,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// Ojo derecho
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{
|
||||||
|
.cx = cat_x + 27 + look_offset,
|
||||||
|
.cy = cat_y + 17,
|
||||||
|
.radius = 5,
|
||||||
|
.color = eye_white,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .rect = .{
|
||||||
|
.x = cat_x + 26 + look_offset,
|
||||||
|
.y = cat_y + 14,
|
||||||
|
.w = 3,
|
||||||
|
.h = 7,
|
||||||
|
.color = eye_pupil,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .rect = .{
|
||||||
|
.x = cat_x + 25 + look_offset,
|
||||||
|
.y = cat_y + 14,
|
||||||
|
.w = 2,
|
||||||
|
.h = 2,
|
||||||
|
.color = eye_white,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// === NARIZ ===
|
||||||
|
ctx.pushCommand(.{ .filled_triangle = .{
|
||||||
|
.x1 = cat_x + 17,
|
||||||
|
.y1 = cat_y + 22,
|
||||||
|
.x2 = cat_x + 23,
|
||||||
|
.y2 = cat_y + 22,
|
||||||
|
.x3 = cat_x + 20,
|
||||||
|
.y3 = cat_y + 26,
|
||||||
|
.color = pink,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// === BOCA ===
|
||||||
|
ctx.pushCommand(.{ .line = .{
|
||||||
|
.x1 = cat_x + 20,
|
||||||
|
.y1 = cat_y + 26,
|
||||||
|
.x2 = cat_x + 15,
|
||||||
|
.y2 = cat_y + 29,
|
||||||
|
.color = dark_fur,
|
||||||
|
} });
|
||||||
|
ctx.pushCommand(.{ .line = .{
|
||||||
|
.x1 = cat_x + 20,
|
||||||
|
.y1 = cat_y + 26,
|
||||||
|
.x2 = cat_x + 25,
|
||||||
|
.y2 = cat_y + 29,
|
||||||
|
.color = dark_fur,
|
||||||
|
} });
|
||||||
|
|
||||||
|
// === BIGOTES ===
|
||||||
|
// Izquierdos
|
||||||
|
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 10, .y1 = cat_y + 23, .x2 = cat_x - 8, .y2 = cat_y + 20, .color = dark_fur } });
|
||||||
|
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 10, .y1 = cat_y + 25, .x2 = cat_x - 8, .y2 = cat_y + 25, .color = dark_fur } });
|
||||||
|
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 10, .y1 = cat_y + 27, .x2 = cat_x - 8, .y2 = cat_y + 30, .color = dark_fur } });
|
||||||
|
// Derechos
|
||||||
|
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 30, .y1 = cat_y + 23, .x2 = cat_x + 48, .y2 = cat_y + 20, .color = dark_fur } });
|
||||||
|
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 30, .y1 = cat_y + 25, .x2 = cat_x + 48, .y2 = cat_y + 25, .color = dark_fur } });
|
||||||
|
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 30, .y1 = cat_y + 27, .x2 = cat_x + 48, .y2 = cat_y + 30, .color = dark_fur } });
|
||||||
|
|
||||||
|
// === PATITAS ===
|
||||||
|
if (state.appear_progress > 0.6) {
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 8, .cy = cat_y + 32, .radius = 4, .color = fur_color } });
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 8, .cy = cat_y + 34, .radius = 2, .color = pink } });
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 32, .cy = cat_y + 32, .radius = 4, .color = fur_color } });
|
||||||
|
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 32, .cy = cat_y + 34, .radius = 2, .color = pink } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RESTAURAR CLIP ===
|
||||||
|
ctx.pushCommand(.clip_end);
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,9 @@ pub const table_core = @import("table_core/table_core.zig");
|
||||||
// Advanced widgets
|
// Advanced widgets
|
||||||
pub const advanced_table = @import("advanced_table/advanced_table.zig");
|
pub const advanced_table = @import("advanced_table/advanced_table.zig");
|
||||||
|
|
||||||
|
// Idle companion (mascot that appears when user is idle)
|
||||||
|
pub const idle_companion = @import("idle_companion.zig");
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Re-exports for convenience
|
// Re-exports for convenience
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -436,6 +439,12 @@ pub const SelectableColors = selectable.Colors;
|
||||||
pub const SelectableResult = selectable.Result;
|
pub const SelectableResult = selectable.Result;
|
||||||
pub const SelectionGroup = selectable.SelectionGroup;
|
pub const SelectionGroup = selectable.SelectionGroup;
|
||||||
|
|
||||||
|
// IdleCompanion (Zcat mascot)
|
||||||
|
pub const IdleCompanion = idle_companion;
|
||||||
|
pub const IdleCompanionState = idle_companion.State;
|
||||||
|
pub const IdleCompanionAnimState = idle_companion.AnimState;
|
||||||
|
pub const IdleCompanionRect = idle_companion.Rect;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tests
|
// Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue