ADR-004: Deterministic Gradient Generation
Status: Accepted
Date: 2024-01-15
Deciders: Core Development Team
Context
The Retro Floppy component generates colorful gradients for disk labels. We needed to decide how gradients are generated:
- Consistency: Should the same label always have the same gradient?
- Uniqueness: Should different labels have different gradients?
- Randomness: Should gradients be random or deterministic?
- Performance: How to generate gradients efficiently?
- User Control: Should users be able to customize gradients?
Use Cases
- Gallery View: Multiple disks displayed together
- Persistence: Disk appearance should be consistent across sessions
- Sharing: Shared disks should look the same for all users
- Customization: Users may want specific colors
Decision
We chose Deterministic Gradient Generation using seeded pseudo-random number generation:
- Seed from Label Name: Hash the label name to create a numeric seed
- Seeded PRNG: Use Mulberry32 algorithm for deterministic randomness
- Consistent Output: Same label name always generates same gradient
- Override Option: Allow users to provide custom seed or colors
Implementation
// 1. Convert label name to numeric seed
const seed = stringToSeed(labelName); // "My Disk" → 1234567
// 2. Create seeded random number generator
const random = createSeededRandom(seed);
// 3. Generate deterministic colors
const baseHue = random() * 360; // Always same for "My Disk"
const colors = generateColorPalette(seed);
// 4. Allow custom override
const finalColors = options?.colors || colors;
Rationale
Why Deterministic?
-
Consistency: Same label always looks the same
- Across page reloads
- Across different users
- Across different devices
-
Predictability: Users can rely on visual appearance
- Easier to find specific disks in a collection
- Visual identity remains stable
-
Shareability: Shared disks look identical for everyone
- Screenshots match live view
- Documentation stays accurate
-
Performance: No need to store gradient data
- Regenerate on demand
- No database/localStorage needed
Why Seeded PRNG?
- Deterministic: Same seed always produces same sequence
- Fast: Mulberry32 is extremely fast (< 1μs per call)
- Good Distribution: Produces well-distributed random numbers
- Small Code: Only ~10 lines of code
- No Dependencies: Pure JavaScript implementation
Why Hash Label Name?
- Natural Seed: Label name is always available
- Unique: Different labels produce different seeds
- Intuitive: "My Disk" always gets same gradient
- No Storage: Don't need to store seed separately
Why Allow Override?
- Flexibility: Users can customize if needed
- Branding: Companies can use brand colors
- Accessibility: Users can choose high-contrast colors
- Testing: Easier to test with fixed colors
Consequences
Positive Consequences
- Consistency: Same label always has same appearance
- Performance: No storage or network requests needed
- Simplicity: No state management for gradients
- Shareability: Screenshots and demos stay accurate
- Predictability: Users can rely on visual identity
- Testing: Deterministic output is easier to test
- Flexibility: Can override with custom colors when needed
Negative Consequences
- Limited Variety: Can't get different gradient for same label
- Mitigated by: Custom seed option
- Mitigated by: Custom colors option
- Hash Collisions: Different labels might produce same seed (rare)
- Impact: Minimal, hash space is large (32-bit)
- Probability: ~0.0001% for 1000 labels
- No True Randomness: Can't get "surprise me" random gradients
- Mitigated by: Users can change label name to get new gradient
- Mitigated by: Custom seed option for randomness
Neutral Consequences
- Algorithm Dependency: Changing PRNG algorithm changes all gradients
- Addressed by: Version gradients if algorithm changes
- Seed Stability: Hash algorithm must remain stable
- Addressed by: Document hash algorithm in code
Implementation Notes
String to Seed Algorithm
function stringToSeed(str: string): number {
if (!str) return 12345; // Default seed for empty strings
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
This is a simple, fast hash function that:
- Produces consistent output for same input
- Distributes well across the 32-bit integer space
- Handles Unicode characters correctly
Mulberry32 PRNG
function createSeededRandom(seed: number): () => number {
let state = seed;
return () => {
state = (state + 0x6d2b79f5) | 0;
let t = Math.imul(state ^ (state >>> 15), 1 | state);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
Mulberry32 is chosen because:
- Fast: ~10x faster than Math.random()
- Deterministic: Same seed always produces same sequence
- Good Quality: Passes statistical randomness tests
- Small: Only 6 lines of code
Custom Override Options
interface GradientGenerationOptions {
seed?: number; // Custom seed for different gradient
colors?: string[]; // Custom colors override generation
angle?: number; // Custom gradient angle
}
// Usage
generateLabelGradient('My Disk', 'linear', {
seed: 42, // Different gradient
colors: ['#ff0000', '#00ff00'], // Custom colors
angle: 45, // Custom angle
});
Testing Strategy
Deterministic generation makes testing straightforward:
test('same label produces same gradient', () => {
const gradient1 = generateLabelGradient('Test', 'linear');
const gradient2 = generateLabelGradient('Test', 'linear');
expect(gradient1).toEqual(gradient2);
});
test('different labels produce different gradients', () => {
const gradient1 = generateLabelGradient('Test1', 'linear');
const gradient2 = generateLabelGradient('Test2', 'linear');
expect(gradient1).not.toEqual(gradient2);
});
Future Enhancements
-
Gradient Versioning: Version gradients to allow algorithm updates
generateLabelGradient('My Disk', 'linear', { version: 2 }); -
Gradient Presets: Named gradient presets
generateLabelGradient('My Disk', 'linear', { preset: 'sunset' }); -
Gradient Gallery: Show all possible gradients for a label
getAllGradientVariations('My Disk'); // Returns 10 variations
References
- Mulberry32 PRNG
- Hash Functions
- Deterministic Random
- Implementation:
src/gradientUtils.ts-stringToSeed(),createSeededRandom() - Tests:
src/__tests__/gradientUtils.test.ts