# Pixcen Shader Effects - Complete Guide

This directory contains JavaScript shader scripts for Pixcen's Shader Fill tool. Shaders are per-pixel effects that transform colors on the canvas, creating procedural patterns, gradients, and visual effects.

## Table of Contents

1. [Quick Start](#quick-start)
2. [Shader Structure](#shader-structure)
3. [Metadata Fields](#metadata-fields)
4. [Parameters](#parameters)
5. [The Process Function](#the-process-function)
6. [Return Value](#return-value)
7. [Context Information](#context-information)
8. [Advanced Techniques](#advanced-techniques)
9. [Examples](#examples)
10. [Best Practices](#best-practices)
11. [Troubleshooting](#troubleshooting)

---

## Quick Start

Create a new `.js` file in this directory with the following minimal structure:

```javascript
const shader = {
    name: "My Shader",
    description: "A simple shader example",
    category: "effects",
    version: "1.0.0",
    author: "Your Name",
    
    process: function(x, y, color) {
        // Return a color value (0-15)
        return (x + y) % 16;
    }
};

pixcen.registerShader(shader);
```

Save the file, restart Pixcen, and select **Tools → Shader Fill** to use your new shader!

---

## Shader Structure

Every shader script must define a shader object and register it with `pixcen.registerShader()`. The shader object contains:

- **Metadata** (required): `name`, `description`, `category`, `version`, `author`
- **Parameters** (optional): `params` object with user-configurable settings
- **Process Function** (required): `process()` function that transforms each pixel

```javascript
const shader = {
    // === METADATA (Required) ===
    name: "Shader Name",
    description: "What this shader does",
    category: "procedural",
    version: "1.0.0",
    author: "Your Name",
    
    // === PARAMETERS (Optional) ===
    params: {
        // Parameter definitions here
    },
    
    // === PROCESS FUNCTION (Required) ===
    process: function(x, y, color) {
        // Pixel transformation code here
        return newColor;  // 0-15
    }
};

pixcen.registerShader(shader);
```

---

## Metadata Fields

### Required Fields

- **`name`** (string): Display name shown in the shader selection dialog
- **`description`** (string): Tooltip/help text describing the shader
- **`category`** (string): Organization category (see [Categories](#categories) below)
- **`version`** (string): Version number (e.g., "1.0.0")
- **`author`** (string): Your name or handle

### Categories

Use these standard categories for consistent organization:

- **`"procedural"`**: Mathematically generated patterns (noise, fractals, waves)
- **`"effects"`**: Visual effects (fire, rain, glitch, animations)
- **`"gradients"`**: Color gradients and transitions
- **`"textured"`**: Texture patterns (wood, stone, fabric)
- **`"organic"`**: Natural/organic patterns (cells, clouds, terrain)

---

## Parameters

Parameters allow users to customize shader behavior. They appear in the shader parameters dialog when the shader is selected.

### Parameter Types

#### Number Parameters

For numeric values with optional min/max constraints:

```javascript
params: {
    scale: {
        type: "number",
        default: 16,
        label: "Wave Scale",
        min: 4,      // Optional
        max: 32      // Optional
    },
    intensity: {
        type: "number",
        default: 2,
        label: "Color Intensity",
        min: 1,
        max: 4
    }
}
```

**Access in `process()`:**
```javascript
const scale = shader.params.scale.default;
const intensity = shader.params.intensity.default;
```

#### Color Parameters

For C64 color selection (0-15):

```javascript
params: {
    baseColor: {
        type: "color",
        default: 1,  // White (C64 color index)
        label: "Base Color"
    }
}
```

**Access in `process()`:**
```javascript
const baseColor = shader.params.baseColor.default;  // Returns 0-15
```

#### ColorRange Parameters

For selecting from user-defined color ranges (palettes with up to 16 colors):

```javascript
params: {
    colorRange: {
        type: "colorRange",
        default: 0,  // Color range index (0-31)
        label: "Color Range"
    }
}
```

**Access in `process()`:**
```javascript
// Get the selected color range index
const rangeIndex = shader.params.colorRange.default;  // Returns 0-31

// Get the actual colors from the range
const rangeColors = pixcen.colors.getColorRangeColors(rangeIndex);
const colorCount = rangeColors.length;

// Handle empty ranges gracefully
if (!rangeColors || colorCount === 0) {
    return color; // Return original color if range is empty
}

// Use colors from the range
const colorIndex = Math.floor(someValue * colorCount);
return rangeColors[colorIndex];
```

**Color Range System:**
- Pixcen supports 32 color ranges (indices 0-31)
- Each range can contain up to 16 colors
- Ranges are managed in the Palette window
- Only non-empty ranges appear in the shader parameter dialog
- `pixcen.colors.getColorRangeColors()` returns an array of color indices (0-15)

**Example Shaders:**
- `gradient_colorrange.js` - Linear gradients using color ranges
- `noise_colorrange.js` - Dithered noise with color ranges
- `wave_colorrange.js` - Sine wave patterns using color ranges

### Parameter Access Pattern

All parameters are accessed via `shader.params.paramName.default`. The `default` property contains the current user-selected value (which may differ from the initial default).

**Important:** Always read parameter values inside the `process()` function, as they may change between shader runs when the user adjusts settings.

---

## The Process Function

The `process()` function is called **once for each pixel** on the canvas (or selected area). It must be fast and deterministic.

### Function Signature

```javascript
process: function(x, y, color) {
    // x: Canvas X coordinate (0 to canvasWidth-1)
    // y: Canvas Y coordinate (0 to canvasHeight-1)
    // color: Current pixel color (0-15, C64 palette)
    
    // Return new color value (0-15)
    return newColor;
}
```

### Implementation Pattern

```javascript
process: function(x, y, color) {
    // 1. Read parameters
    const scale = shader.params.scale.default;
    
    // 2. Get canvas dimensions (if needed)
    const canvasWidth = pixcen.canvas.getWidth();
    const canvasHeight = pixcen.canvas.getHeight();
    
    // 3. Calculate new color based on position and/or original color
    const newColor = /* your algorithm */;
    
    // 4. Return color value (0-15)
    return Math.max(0, Math.min(15, newColor));
}
```

### Performance Considerations

- **Called per pixel**: For a 320×200 canvas, `process()` is called 64,000 times
- **Must be fast**: Aim for < 0.1ms per pixel for smooth operation
- **Avoid heavy calculations**: Cache values, minimize function calls
- **Use integer math**: Prefer integer operations over floating-point when possible

---

## Return Value

The `process()` function **must** return a single integer value:

- **Range**: 0-15 (C64 color palette index)
- **Type**: Integer (will be clamped if out of range)
- **Meaning**: The new color for this pixel

**Always clamp your return value:**
```javascript
return Math.max(0, Math.min(15, calculatedColor));
```

---

## Context Information

The `process()` function receives context about the pixel being processed:

### `x` / `y`

- Canvas coordinates of the current pixel
- `x` ranges from 0 to `canvasWidth - 1`
- `y` ranges from 0 to `canvasHeight - 1`
- Use for position-aware effects (gradients, patterns, distance calculations)

**Example - Distance from Center:**
```javascript
const canvasWidth = pixcen.canvas.getWidth();
const canvasHeight = pixcen.canvas.getHeight();
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;

const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
```

### `color`

- Current pixel color (0-15, C64 palette index)
- Use this to:
  - **Transform existing colors**: Modify, blend, or enhance
  - **Ignore for new patterns**: Generate completely new patterns
  - **Use as input**: Base effects on existing image content

**Example - Color Transformation:**
```javascript
// Brighten existing color
return Math.min(15, color + 2);

// Invert color
return 15 - color;

// Use original color in pattern
return (color + (x + y) % 8) % 16;
```

**Example - Ignore Original Color:**
```javascript
// Generate new pattern regardless of original
const pattern = (x + y) % 16;
return pattern;
```

### Canvas Dimensions

Access canvas size using `pixcen.canvas`:

```javascript
const canvasWidth = pixcen.canvas.getWidth();   // Typically 320
const canvasHeight = pixcen.canvas.getHeight(); // Typically 200
```

**Use Cases:**
- Normalize coordinates (0.0 to 1.0)
- Calculate center points
- Create full-canvas effects
- Handle different canvas sizes

**Example - Normalized Coordinates:**
```javascript
const canvasWidth = pixcen.canvas.getWidth();
const canvasHeight = pixcen.canvas.getHeight();

const nx = x / canvasWidth;   // 0.0 to 1.0
const ny = y / canvasHeight;  // 0.0 to 1.0
```

---

## Advanced Techniques

### Deterministic Randomness

For consistent, reproducible patterns, use seeded hash functions:

```javascript
// Hash function for deterministic randomness
function hash2D(x, y) {
    let h = (x * 73856093) ^ (y * 19349663);
    h = (h ^ (h >> 13)) * 1274126177;
    return (h ^ (h >> 16)) & 0x7fffffff;
}

process: function(x, y, color) {
    const hash = hash2D(x, y);
    const randomValue = (hash % 100) / 100.0;  // 0.0 to 1.0
    
    if (randomValue < density) {
        return starColor;
    }
    return 0;
}
```

### Noise Functions

For smooth, organic patterns:

```javascript
// Simple noise function
function noise(x, y) {
    let n = x * 57 + y * 131;
    n = (n << 13) ^ n;
    return (1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0);
}

process: function(x, y, color) {
    const n = noise(x / 50.0, y / 50.0);
    const colorValue = Math.floor((n + 1) / 2 * 15);
    return Math.max(0, Math.min(15, colorValue));
}
```

### Multi-Octave Noise (Perlin-style)

For detailed, natural-looking patterns:

```javascript
function gradient(x, y) {
    const hash = ((x * 73856093) ^ (y * 19349663)) & 0xFFFF;
    const angle = (hash / 0xFFFF) * Math.PI * 2;
    return { x: Math.cos(angle), y: Math.sin(angle) };
}

function smoothstep(t) {
    return t * t * (3 - 2 * t);
}

function dot(a, b) {
    return a.x * b.x + a.y * b.y;
}

process: function(x, y, color) {
    const scale = shader.params.scale.default;
    const octaves = shader.params.octaves.default;
    
    let value = 0;
    let amplitude = 1;
    let frequency = 1;
    let maxValue = 0;
    
    // Multi-octave noise
    for (let i = 0; i < octaves; i++) {
        const sx = x * frequency / scale;
        const sy = y * frequency / scale;
        
        // Grid cell coordinates
        const x0 = Math.floor(sx);
        const y0 = Math.floor(sy);
        const x1 = x0 + 1;
        const y1 = y0 + 1;
        
        // Local coordinates within cell
        const lx = sx - x0;
        const ly = sy - y0;
        
        // Gradients at corners
        const g00 = gradient(x0, y0);
        const g10 = gradient(x1, y0);
        const g01 = gradient(x0, y1);
        const g11 = gradient(x1, y1);
        
        // Distance vectors and dot products
        const d00 = dot(g00, { x: lx, y: ly });
        const d10 = dot(g10, { x: lx - 1, y: ly });
        const d01 = dot(g01, { x: lx, y: ly - 1 });
        const d11 = dot(g11, { x: lx - 1, y: ly - 1 });
        
        // Smooth interpolation
        const tx = smoothstep(lx);
        const ty = smoothstep(ly);
        
        const i0 = d00 * (1 - tx) + d10 * tx;
        const i1 = d01 * (1 - tx) + d11 * tx;
        const noise = i0 * (1 - ty) + i1 * ty;
        
        value += noise * amplitude;
        maxValue += amplitude;
        
        amplitude *= 0.5;
        frequency *= 2;
    }
    
    // Normalize and map to color range
    value = (value / maxValue + 1) / 2;
    return Math.floor(value * 15);
}
```

### Polar Coordinates

For radial patterns (spirals, circles, vortices):

```javascript
process: function(x, y, color) {
    const canvasWidth = pixcen.canvas.getWidth();
    const canvasHeight = pixcen.canvas.getHeight();
    const centerX = shader.params.centerX.default;
    const centerY = shader.params.centerY.default;
    
    // Convert to polar coordinates
    const dx = x - centerX;
    const dy = y - centerY;
    const angle = Math.atan2(dy, dx);
    const distance = Math.sqrt(dx * dx + dy * dy);
    
    // Create spiral pattern
    const tightness = shader.params.tightness.default;
    const spiral = (angle + distance / tightness) / (Math.PI * 2);
    const ring = Math.floor(spiral * 16) % 16;
    
    return ring;
}
```

### Animation with Time Parameter

For animated effects:

```javascript
params: {
    time: {
        type: "number",
        default: 0,
        label: "Animation Phase",
        min: 0,
        max: 100
    }
},

process: function(x, y, color) {
    const time = shader.params.time.default;
    
    // Animate based on time
    const animX = x + Math.sin(time / 10) * 10;
    const animY = y + Math.cos(time / 10) * 10;
    
    // Use animated coordinates
    const pattern = (Math.floor(animX / 8) + Math.floor(animY / 8)) % 16;
    return pattern;
}
```

### Color Palettes

For custom color ramps:

```javascript
process: function(x, y, color) {
    // Define custom palette
    const firePalette = [0, 1, 9, 8, 7, 15];  // Black -> Red -> Orange -> Yellow -> White
    
    // Calculate intensity (0.0 to 1.0)
    const intensity = /* your calculation */;
    
    // Map to palette
    const paletteIndex = Math.floor(intensity * (firePalette.length - 1));
    return firePalette[paletteIndex];
}
```

### Performance Optimization

- **Pre-calculate constants**: Move calculations outside `process()` when possible
- **Cache canvas dimensions**: Store `getWidth()`/`getHeight()` results
- **Use integer math**: Avoid floating-point in tight loops
- **Minimize function calls**: Inline small functions, cache repeated calculations
- **Avoid expensive operations**: Minimize `Math.sqrt()`, `Math.sin()`, etc.

```javascript
// Good: Cache canvas dimensions
const canvasWidth = pixcen.canvas.getWidth();
const canvasHeight = pixcen.canvas.getHeight();
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;

// Good: Use squared distance to avoid sqrt
const dx = x - centerX;
const dy = y - centerY;
const distSq = dx * dx + dy * dy;  // Compare squared distance

// Good: Pre-calculate parameter values
const scale = shader.params.scale.default;
const intensity = shader.params.intensity.default;
```

---

## Examples

### Example 1: Simple Checkerboard

```javascript
const shader = {
    name: "Checkerboard",
    description: "Classic checkerboard pattern",
    category: "procedural",
    version: "1.0.0",
    author: "Your Name",
    
    params: {
        tileSize: {
            type: "number",
            default: 8,
            label: "Tile Size",
            min: 1,
            max: 64
        },
        color1: {
            type: "color",
            default: 0,
            label: "Color 1"
        },
        color2: {
            type: "color",
            default: 15,
            label: "Color 2"
        }
    },
    
    process: function(x, y, color) {
        const tileSize = shader.params.tileSize.default;
        const color1 = shader.params.color1.default;
        const color2 = shader.params.color2.default;
        
        const tileX = Math.floor(x / tileSize);
        const tileY = Math.floor(y / tileSize);
        const isEven = (tileX + tileY) % 2 === 0;
        
        return isEven ? color1 : color2;
    }
};

pixcen.registerShader(shader);
```

### Example 2: Plasma Effect

```javascript
const shader = {
    name: "Plasma Waves",
    description: "Psychedelic plasma effect using sine wave interference",
    category: "procedural",
    version: "1.0.0",
    author: "Your Name",
    
    params: {
        scale: {
            type: "number",
            default: 16,
            label: "Wave Scale",
            min: 4,
            max: 32
        },
        time: {
            type: "number",
            default: 0,
            label: "Animation Phase",
            min: 0,
            max: 100
        },
        intensity: {
            type: "number",
            default: 2,
            label: "Color Intensity",
            min: 1,
            max: 4
        }
    },
    
    process: function(x, y, color) {
        const scale = shader.params.scale.default;
        const time = shader.params.time.default;
        const intensity = shader.params.intensity.default;
        
        // Apply time-based animation offset
        const cx = x + 0.5 * Math.sin(time / 5);
        const cy = y + 0.5 * Math.cos(time / 3);
        
        // Four interfering sine waves
        const value1 = Math.sin(cx / scale);
        const value2 = Math.sin(cy / (scale / 2));
        const value3 = Math.sin((cx + cy) / scale);
        const value4 = Math.sin(Math.sqrt(cx * cx + cy * cy) / (scale / 2));
        
        // Combine waves
        const plasma = value1 + value2 + value3 + value4;
        
        // Normalize to 0-15 color range
        const normalized = Math.floor((plasma + 4) * intensity) % 16;
        
        return Math.max(0, Math.min(15, normalized));
    }
};

pixcen.registerShader(shader);
```

### Example 3: Concentric Rings

```javascript
const shader = {
    name: "Concentric Rings",
    description: "Circular rings radiating from center",
    category: "procedural",
    version: "1.0.0",
    author: "Your Name",
    
    params: {
        ringWidth: {
            type: "number",
            default: 8,
            label: "Ring Width",
            min: 2,
            max: 32
        },
        centerX: {
            type: "number",
            default: 160,
            label: "Center X",
            min: 0,
            max: 320
        },
        centerY: {
            type: "number",
            default: 100,
            label: "Center Y",
            min: 0,
            max: 200
        },
        colorCount: {
            type: "number",
            default: 16,
            label: "Color Cycle",
            min: 2,
            max: 16
        }
    },
    
    process: function(x, y, color) {
        const ringWidth = shader.params.ringWidth.default;
        const centerX = shader.params.centerX.default;
        const centerY = shader.params.centerY.default;
        const colorCount = shader.params.colorCount.default;
        
        // Calculate distance from center
        const dx = x - centerX;
        const dy = y - centerY;
        const distance = Math.sqrt(dx * dx + dy * dy);
        
        // Determine which ring
        const ring = Math.floor(distance / ringWidth) % colorCount;
        
        return Math.max(0, Math.min(15, ring));
    }
};

pixcen.registerShader(shader);
```

### Example 4: Color Transformation (Brighten)

```javascript
const shader = {
    name: "Brighten",
    description: "Brightens existing image colors",
    category: "effects",
    version: "1.0.0",
    author: "Your Name",
    
    params: {
        amount: {
            type: "number",
            default: 2,
            label: "Brightness Amount",
            min: 1,
            max: 5
        }
    },
    
    process: function(x, y, color) {
        const amount = shader.params.amount.default;
        
        // Brighten by adding to color value
        const brightened = color + amount;
        
        return Math.max(0, Math.min(15, brightened));
    }
};

pixcen.registerShader(shader);
```

### Example 5: Gradient Based on Y Position

```javascript
const shader = {
    name: "Vertical Gradient",
    description: "Creates a vertical gradient from top to bottom",
    category: "gradients",
    version: "1.0.0",
    author: "Your Name",

    params: {
        topColor: {
            type: "color",
            default: 0,
            label: "Top Color"
        },
        bottomColor: {
            type: "color",
            default: 15,
            label: "Bottom Color"
        }
    },

    process: function(x, y, color) {
        const canvasHeight = pixcen.canvas.getHeight();
        const topColor = shader.params.topColor.default;
        const bottomColor = shader.params.bottomColor.default;
        
        // Normalize Y position (0.0 to 1.0)
        const t = y / canvasHeight;
        
        // Interpolate between colors
        const gradientColor = Math.floor(topColor + (bottomColor - topColor) * t);
        
        return Math.max(0, Math.min(15, gradientColor));
    }
};

pixcen.registerShader(shader);
```

---

## Best Practices

### Performance

1. **Keep `process()` fast** - Aim for < 0.1ms per pixel
2. **Cache canvas dimensions** - Store `getWidth()`/`getHeight()` results
3. **Pre-calculate constants** - Move calculations outside `process()` when possible
4. **Use integer math** - Avoid floating-point operations in tight loops
5. **Minimize function calls** - Inline small functions, cache repeated calculations

### Code Quality

1. **Validate inputs** - Clamp return values to 0-15 range
2. **Use meaningful names** - Clear variable names improve readability
3. **Add comments** - Document complex algorithms and non-obvious logic
4. **Handle edge cases** - Test with different canvas sizes

### User Experience

1. **Provide good defaults** - Parameters should have sensible default values
2. **Use clear labels** - Parameter labels should be self-explanatory
3. **Set appropriate ranges** - Min/max values should prevent invalid inputs
4. **Write helpful descriptions** - Explain what the shader does and when to use it

### Determinism

1. **Use seeded randomness** - For reproducible results, seed RNG with position
2. **Avoid global state** - Don't rely on variables outside the `process()` function
3. **Test consistency** - Same inputs should produce same outputs

---

## Troubleshooting

### Shader Not Appearing

- **Check file location**: Must be in `scripts/shaders/` directory
- **Check syntax**: JavaScript syntax errors prevent loading
- **Check registration**: Must call `pixcen.registerShader(shader)` at the end
- **Restart Pixcen**: Shaders are loaded at startup

### Shader Not Working Correctly

- **Check return value**: Must return integer 0-15
- **Check parameter access**: Use `shader.params.paramName.default`
- **Check canvas access**: Use `pixcen.canvas.getWidth()`/`getHeight()` inside `process()`
- **Validate calculations**: Ensure color values stay in 0-15 range

### Performance Issues

- **Profile your code**: Identify slow operations
- **Optimize loops**: Reduce calculations, cache values
- **Simplify algorithms**: Consider simpler approaches
- **Avoid expensive functions**: Minimize `Math.sqrt()`, `Math.sin()`, etc.

### Parameter Issues

- **Check parameter access**: Use `shader.params.paramName.default`
- **Check parameter types**: Number vs Color must match definition
- **Check default values**: Ensure defaults are within min/max range

### Canvas Access Issues

- **Check availability**: `pixcen.canvas` may not be available in all contexts
- **Use fallbacks**: Provide default values if canvas is unavailable
- **Check bounds**: Ensure coordinates are within canvas bounds

---

## Included Shaders

Pixcen comes with several example shaders:

### Procedural Patterns
- **plasma_waves.js**: Psychedelic plasma effect
- **concentric_rings.js**: Circular rings pattern
- **wave_interference.js**: Wave interference patterns
- **procedural_checkerboard.js**: Checkerboard pattern
- **voronoi_cells.js**: Voronoi diagram cells
- **perlin_noise.js**: Perlin noise texture
- **fractal_mandelbrot.js**: Mandelbrot set fractal

### Effects
- **fire_effect.js**: Procedural fire animation
- **rain_effect.js**: Falling rain effect
- **glitch_art.js**: Digital glitch effects
- **metaballs.js**: Metaball blob effect

### Textures
- **wood_grain.js**: Wood grain texture
- **colorful_bricks.js**: Brick pattern
- **woven_texture.js**: Woven fabric pattern
- **pcb_traces.js**: Circuit board traces

### Other
- **starfield.js**: Starfield pattern
- **3d_tunnel.js**: 3D tunnel effect
- **diamond_pattern.js**: Diamond pattern
- **diagonal_stripes.js**: Diagonal stripe pattern

Study these examples to learn advanced techniques!

---

## Differences from Brushes

Shaders differ from brushes in several important ways:

| Feature | Shaders | Brushes |
|---------|---------|---------|
| **Processing** | Per-pixel (called once per pixel) | Per-stamp (called once per brush imprint) |
| **Input** | `x, y, color` (single pixel) | `width, height, frame, x, y, color` (brush area) |
| **Output** | Single color value (0-15) | Brush data object (`{width, height, pixels, mask}`) |
| **Canvas Access** | Can read canvas dimensions | Cannot access canvas directly |
| **Use Case** | Transform existing pixels | Generate new brush patterns |
| **Performance** | Must be extremely fast (< 0.1ms/pixel) | Can be slower (< 16ms/stamp) |

---

## Creating Your Own Shader

1. **Create a new `.js` file** in `scripts/shaders/` directory
2. **Follow the structure** outlined in this guide
3. **Test your shader** with various parameter values
4. **Restart Pixcen** to load your shader
5. **Select "Shader Fill"** from the Tools menu
6. **Your shader will appear** in the shader selection dialog

Happy shading! 🎨
