在React Native鸿蒙跨平台开发中实现一个计数排序算法,如何使用一个额外的数组来统计每个值的出现次数,然后根据这个统计结果来重构原数组的顺序
本文介绍了在React Native项目中实现计数排序算法的方法。计数排序是一种非比较型排序算法,适用于整数排序,通过统计元素出现次数来重构数组顺序。文章详细说明了计数排序的实现步骤:确定数值范围、创建计数数组、统计元素频率并重构排序数组。同时提供了React Native中的具体实现代码示例,包括排序函数封装和在组件中的应用。此外还指出了该算法的适用场景和性能注意事项,如大数据集处理时的空间复杂
在React Native项目中实现鸿蒙OS(HarmonyOS)的计数排序算法,首先需要理解计数排序的基本原理和步骤。计数排序是一种非比较型排序算法,适用于一定范围内的整数排序。其核心思想是使用一个额外的数组来统计每个值的出现次数,然后根据这个统计结果来重构原数组的顺序。
计数排序的基本步骤
- 找出待排序数组中的最大值和最小值,以确定计数数组的范围。
- 创建计数数组,并初始化所有元素为0。
- 遍历待排序数组,统计每个元素的出现次数,存储在计数数组中。
- 根据计数数组重构原数组,即根据计数数组的值将元素放回原数组的正确位置。
在React Native项目中实现计数排序
- 创建React Native项目
如果你还没有创建React Native项目,可以使用以下命令:
npx react-native init MyHarmonyApp
cd MyHarmonyApp
- 实现计数排序函数
在React Native项目中,你可以在任意组件中实现计数排序。例如,在App.js文件中添加一个排序函数:
function countSort(arr) {
if (arr.length === 0) return [];
// 找到数组中的最大值和最小值
let min = Math.min(...arr);
let max = Math.max(...arr);
let range = max - min + 1;
// 创建计数数组并初始化
let countArray = new Array(range).fill(0);
// 统计每个值的出现次数
for (let i = 0; i < arr.length; i++) {
countArray[arr[i] - min]++;
}
// 重构原数组
let sortedIndex = 0;
for (let i = 0; i < countArray.length; i++) {
while (countArray[i] > 0) {
arr[sortedIndex++] = i + min;
countArray[i]--;
}
}
return arr;
}
- 在组件中使用计数排序函数
在App.js中,你可以使用这个countSort函数来排序一个数组,并在界面上显示结果:
import React from 'react';
import { View, Text, Button } from 'react-native';
export default function App() {
const [array, setArray] = React.useState([5, 3, 2, 8, 5, 3, 6]);
const [sortedArray, setSortedArray] = React.useState([]);
const handleSort = () => {
const sorted = countSort([...array]); // 使用展开运算符复制数组以避免直接修改状态中的数组引用问题(可选)
setSortedArray(sorted);
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Original Array: {array.join(', ')}</Text>
<Text>Sorted Array: {sortedArray.join(', ')}</Text>
<Button title="Sort Array" onPress={handleSort} />
</View>
);
}
注意事项:
- 性能问题:对于大数据集或非常大的值范围,计数排序的性能可能会下降。在这种情况下,可以考虑使用更高效的排序算法如快速排序或归并排序。
- 空间复杂度:计数排序的空间复杂度为O(n+k),其中n是数组的长度,k是数据的范围。如果k非常大,这可能会成为一个问题。在这种情况下,可以考虑使用桶排序或其他优化策略。
- 适用场景:计数排序最适合于范围不太大的整数排序。对于浮点数或大范围数据,其他算法可能更合适。
通过上述步骤,你可以在React Native项目中实现和使用计数排序算法。
真实项目案例演示:
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Image } from 'react-native';
// Simple Icon Component using Unicode symbols
interface IconProps {
name: string;
size?: number;
color?: string;
style?: object;
}
const Icon: React.FC<IconProps> = ({
name,
size = 24,
color = '#333333',
style
}) => {
const getIconSymbol = () => {
switch (name) {
case 'prev': return '‹';
case 'next': return '›';
case 'today': return '◎';
case 'event': return '•';
case 'date': return '📅';
case 'month': return '📅';
case 'year': return '📅';
default: return '●';
}
};
return (
<View style={[{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }, style]}>
<Text style={{ fontSize: size * 0.8, color, includeFontPadding: false, textAlign: 'center' }}>
{getIconSymbol()}
</Text>
</View>
);
};
// Calendar Component
interface CalendarProps {
events?: { date: string; title: string }[];
onDateSelect?: (date: string) => void;
selectedDate?: string;
}
const Calendar: React.FC<CalendarProps> = ({
events = [],
onDateSelect,
selectedDate
}) => {
const [currentDate, setCurrentDate] = useState(new Date());
// Get days in month
const getDaysInMonth = (year: number, month: number) => {
return new Date(year, month + 1, 0).getDate();
};
// Get first day of month (0 = Sunday, 1 = Monday, etc)
const getFirstDayOfMonth = (year: number, month: number) => {
return new Date(year, month, 1).getDay();
};
// Format date as YYYY-MM-DD
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// Check if date has events
const hasEvents = (dateStr: string) => {
return events.some(event => event.date === dateStr);
};
// Navigate to previous month
const prevMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
};
// Navigate to next month
const nextMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
};
// Go to today
const goToToday = () => {
const today = new Date();
setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
if (onDateSelect) {
onDateSelect(formatDate(today));
}
};
// Render calendar header
const renderHeader = () => {
const year = currentDate.getFullYear();
const month = currentDate.toLocaleString('zh-CN', { month: 'long' });
return (
<View style={styles.calendarHeader}>
<TouchableOpacity onPress={prevMonth} style={styles.navButton}>
<Icon name="prev" size={24} color="#4a5568" />
</TouchableOpacity>
<View style={styles.monthYearContainer}>
<Text style={styles.monthText}>{month}</Text>
<Text style={styles.yearText}>{year}年</Text>
</View>
<TouchableOpacity onPress={nextMonth} style={styles.navButton}>
<Icon name="next" size={24} color="#4a5568" />
</TouchableOpacity>
</View>
);
};
// Render weekdays
const renderWeekdays = () => {
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
return (
<View style={styles.weekdaysContainer}>
{weekdays.map((day, index) => (
<View key={index} style={styles.weekdayCell}>
<Text style={[styles.weekdayText, index === 0 && styles.sundayText]}>{day}</Text>
</View>
))}
</View>
);
};
// Render calendar days
const renderDays = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const daysInMonth = getDaysInMonth(year, month);
const firstDayOfMonth = getFirstDayOfMonth(year, month);
const today = new Date();
const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
const todayDate = today.getDate();
const days = [];
// Previous month's days
const prevMonthDays = getDaysInMonth(year, month - 1);
for (let i = firstDayOfMonth - 1; i >= 0; i--) {
const day = prevMonthDays - i;
days.push(
<View key={`prev-${day}`} style={styles.dayCell}>
<Text style={styles.otherMonthDay}>{day}</Text>
</View>
);
}
// Current month's days
for (let day = 1; day <= daysInMonth; day++) {
const dateObj = new Date(year, month, day);
const dateStr = formatDate(dateObj);
const isToday = isCurrentMonth && day === todayDate;
const isSelected = selectedDate === dateStr;
const hasEvent = hasEvents(dateStr);
days.push(
<TouchableOpacity
key={`curr-${day}`}
style={[
styles.dayCell,
isToday && styles.todayCell,
isSelected && styles.selectedCell
]}
onPress={() => onDateSelect && onDateSelect(dateStr)}
>
<View style={styles.dayNumberContainer}>
<Text style={[
styles.dayNumber,
isToday && styles.todayText,
isSelected && styles.selectedText
]}>
{day}
</Text>
{hasEvent && (
<View style={styles.eventIndicator}>
<Icon name="event" size={8} color="#e53e3e" />
</View>
)}
</View>
</TouchableOpacity>
);
}
// Next month's days
const totalCells = 42; // 6 rows * 7 days
const remainingCells = totalCells - days.length;
for (let day = 1; day <= remainingCells; day++) {
days.push(
<View key={`next-${day}`} style={styles.dayCell}>
<Text style={styles.otherMonthDay}>{day}</Text>
</View>
);
}
return (
<View style={styles.daysContainer}>
{days}
</View>
);
};
// Render today button
const renderTodayButton = () => {
return (
<TouchableOpacity style={styles.todayButton} onPress={goToToday}>
<Icon name="today" size={16} color="#4a5568" style={styles.todayIcon} />
<Text style={styles.todayButtonText}>今天</Text>
</TouchableOpacity>
);
};
return (
<View style={styles.calendarContainer}>
{renderHeader()}
{renderWeekdays()}
{renderDays()}
{renderTodayButton()}
</View>
);
};
// Main App Component
const CalendarComponentApp = () => {
const [selectedDate, setSelectedDate] = useState<string>('');
const [events] = useState([
{ date: new Date().toISOString().split('T')[0], title: '会议' },
{ date: new Date(Date.now() + 86400000).toISOString().split('T')[0], title: '生日' },
{ date: new Date(Date.now() + 172800000).toISOString().split('T')[0], title: '假期' },
]);
const handleDateSelect = (date: string) => {
setSelectedDate(date);
};
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>计数排序组件</Text>
<Text style={styles.headerSubtitle}>逐步演示计数排序</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>计数排序(可视化 · 薄荷风格)</Text>
<View style={styles.ctContainer}>
<CountingSortVisualizer />
</View>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>© 2023 排序演示 | 现代化UI组件库</Text>
</View>
</ScrollView>
);
};
const { width } = Dimensions.get('window');
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f7fafc',
},
header: {
backgroundColor: '#ffffff',
paddingVertical: 30,
paddingHorizontal: 20,
marginBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
headerTitle: {
fontSize: 28,
fontWeight: '700',
color: '#2d3748',
textAlign: 'center',
marginBottom: 5,
},
headerSubtitle: {
fontSize: 16,
color: '#718096',
textAlign: 'center',
},
section: {
marginBottom: 25,
},
sectionTitle: {
fontSize: 20,
fontWeight: '700',
color: '#2d3748',
paddingHorizontal: 20,
paddingBottom: 15,
},
calendarWrapper: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 15,
padding: 15,
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
selectedDateCard: {
backgroundColor: '#ebf8ff',
marginHorizontal: 15,
borderRadius: 12,
padding: 20,
borderWidth: 1,
borderColor: '#bee3f8',
},
selectedDateText: {
fontSize: 18,
fontWeight: '700',
color: '#2b6cb0',
textAlign: 'center',
marginBottom: 5,
},
selectedDateDesc: {
fontSize: 14,
color: '#4a5568',
textAlign: 'center',
},
demosContainer: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 15,
padding: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
demoItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
demoItemLast: {
marginBottom: 0,
},
demoIcon: {
marginRight: 15,
},
demoTitle: {
fontSize: 16,
fontWeight: '600',
color: '#2d3748',
marginBottom: 3,
},
demoDesc: {
fontSize: 14,
color: '#718096',
},
usageSection: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 15,
padding: 20,
marginBottom: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
codeBlock: {
backgroundColor: '#2d3748',
borderRadius: 8,
padding: 15,
marginBottom: 15,
},
codeText: {
fontFamily: 'monospace',
color: '#e2e8f0',
fontSize: 14,
lineHeight: 22,
},
description: {
fontSize: 15,
color: '#4a5568',
lineHeight: 22,
},
featuresSection: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 15,
padding: 20,
marginBottom: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
featuresList: {
paddingLeft: 10,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 15,
},
featureIcon: {
marginRight: 15,
},
featureText: {
fontSize: 16,
color: '#2d3748',
},
footer: {
paddingVertical: 20,
alignItems: 'center',
},
footerText: {
color: '#a0aec0',
fontSize: 14,
},
// Calendar Styles
calendarContainer: {
backgroundColor: '#ffffff',
},
calendarHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 10,
paddingVertical: 15,
},
navButton: {
padding: 10,
},
monthYearContainer: {
alignItems: 'center',
},
monthText: {
fontSize: 18,
fontWeight: '700',
color: '#2d3748',
},
yearText: {
fontSize: 14,
color: '#718096',
},
weekdaysContainer: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#edf2f7',
paddingBottom: 10,
marginBottom: 5,
},
weekdayCell: {
flex: 1,
alignItems: 'center',
},
weekdayText: {
fontSize: 14,
fontWeight: '600',
color: '#4a5568',
},
sundayText: {
color: '#e53e3e',
},
daysContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
dayCell: {
width: '14.28%',
aspectRatio: 1,
justifyContent: 'center',
alignItems: 'center',
},
dayNumberContainer: {
position: 'relative',
},
dayNumber: {
fontSize: 16,
color: '#4a5568',
fontWeight: '500',
},
otherMonthDay: {
fontSize: 16,
color: '#cbd5e0',
},
todayCell: {
backgroundColor: '#ebf8ff',
borderRadius: 30,
},
todayText: {
color: '#3182ce',
fontWeight: '700',
},
selectedCell: {
backgroundColor: '#3182ce',
borderRadius: 30,
},
selectedText: {
color: '#ffffff',
fontWeight: '700',
},
eventIndicator: {
position: 'absolute',
top: -3,
right: -3,
},
todayButton: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 15,
borderTopWidth: 1,
borderTopColor: '#edf2f7',
marginTop: 5,
},
todayIcon: {
marginRight: 8,
},
todayButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#4a5568',
},
msContainer: {
marginHorizontal: 15,
},
msCard: {
backgroundColor: '#0f141c',
borderRadius: 16,
borderWidth: 1,
borderColor: '#1f2a36',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.18,
shadowRadius: 14,
paddingVertical: 14,
paddingHorizontal: 12,
},
msHeader: {
marginBottom: 8,
},
msTitle: {
fontSize: 18,
fontWeight: '700',
color: '#e6f1ff',
},
msSubtitle: {
fontSize: 13,
color: '#8aa0b8',
marginTop: 4,
},
msControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
msBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#1a2433',
borderWidth: 1,
borderColor: '#28374d',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
msBtnActive: {
backgroundColor: '#1f2d44',
borderColor: '#34507a',
},
msIcon: {
width: 20,
height: 20,
marginRight: 6,
},
msBtnText: {
color: '#e6f1ff',
fontSize: 13,
fontWeight: '600',
},
msChips: {
flexDirection: 'row',
alignSelf: 'flex-end',
marginLeft: 8,
},
msChip: {
backgroundColor: '#182030',
borderWidth: 1,
borderColor: '#26364d',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
msChipActive: {
backgroundColor: '#213049',
borderColor: '#385782',
},
msChipText: {
color: '#cfe1ff',
fontSize: 12,
fontWeight: '600',
},
msBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
msBar: {
width: 16,
backgroundColor: '#2f7de1',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
msBarHL: {
backgroundColor: '#49a2ff',
},
msBarText: {
fontSize: 10,
color: '#e6f1ff',
marginBottom: 4,
fontWeight: '600',
},
msFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
msFootText: {
color: '#8aa0b8',
fontSize: 12,
},
msDone: {
color: '#43d998',
fontWeight: '700',
},
bsContainer: {
marginHorizontal: 15,
},
bsCard: {
backgroundColor: '#ffffff',
borderRadius: 16,
borderWidth: 1,
borderColor: '#e7eef7',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.08,
shadowRadius: 12,
paddingVertical: 14,
paddingHorizontal: 12,
},
bsHeader: {
marginBottom: 8,
},
bsTitle: {
fontSize: 18,
fontWeight: '700',
color: '#2d3748',
},
bsSubtitle: {
fontSize: 13,
color: '#718096',
marginTop: 4,
},
bsControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
bsBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f1f5fb',
borderWidth: 1,
borderColor: '#e1eaf6',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
bsBtnActive: {
backgroundColor: '#e7eef7',
borderColor: '#d9e6f5',
},
bsIcon: {
width: 20,
height: 20,
marginRight: 6,
},
bsBtnText: {
color: '#2d3748',
fontSize: 13,
fontWeight: '600',
},
bsTargetRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 6,
},
bsChip: {
backgroundColor: '#edf2f7',
borderWidth: 1,
borderColor: '#e2e8f0',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
bsChipText: {
color: '#2d3748',
fontSize: 12,
fontWeight: '600',
},
bsBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
bsBar: {
width: 16,
backgroundColor: '#90cdf4',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
bsBarDim: {
backgroundColor: '#c5d8ee',
},
bsBarMid: {
backgroundColor: '#63b3ed',
},
bsBarTarget: {
backgroundColor: '#68d391',
},
bsBarText: {
fontSize: 10,
color: '#2d3748',
marginBottom: 4,
fontWeight: '600',
},
bsFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
bsFootText: {
color: '#718096',
fontSize: 12,
},
bsDone: {
color: '#2e7d32',
fontWeight: '700',
},
lsContainer: {
marginHorizontal: 15,
},
lsCard: {
backgroundColor: '#fff7ed',
borderRadius: 16,
borderWidth: 1,
borderColor: '#fde4d6',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.08,
shadowRadius: 12,
paddingVertical: 14,
paddingHorizontal: 12,
},
lsHeader: {
marginBottom: 8,
},
lsTitle: {
fontSize: 18,
fontWeight: '700',
color: '#7c2d12',
},
lsSubtitle: {
fontSize: 13,
color: '#9a3412',
marginTop: 4,
},
lsControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
lsBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff1e6',
borderWidth: 1,
borderColor: '#ffe1cc',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
lsBtnActive: {
backgroundColor: '#ffe8d6',
borderColor: '#ffd9bf',
},
lsIcon: {
width: 20,
height: 20,
marginRight: 6,
},
lsBtnText: {
color: '#7c2d12',
fontSize: 13,
fontWeight: '600',
},
lsTargetRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 6,
},
lsChip: {
backgroundColor: '#ffedd5',
borderWidth: 1,
borderColor: '#fed7aa',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
lsChipText: {
color: '#7c2d12',
fontSize: 12,
fontWeight: '600',
},
lsBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
lsBar: {
width: 16,
backgroundColor: '#fdba74',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
lsBarCur: {
backgroundColor: '#fb923c',
},
lsBarTarget: {
backgroundColor: '#22c55e',
},
lsBarText: {
fontSize: 10,
color: '#7c2d12',
marginBottom: 4,
fontWeight: '600',
},
lsFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
lsFootText: {
color: '#9a3412',
fontSize: 12,
},
lsDone: {
color: '#16a34a',
fontWeight: '700',
},
bbContainer: {
marginHorizontal: 15,
},
bbCard: {
backgroundColor: '#1B1633',
borderRadius: 16,
borderWidth: 1,
borderColor: '#2A2350',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.2,
shadowRadius: 14,
paddingVertical: 14,
paddingHorizontal: 12,
},
bbHeader: {
marginBottom: 8,
},
bbTitle: {
fontSize: 18,
fontWeight: '700',
color: '#EAE7FF',
},
bbSubtitle: {
fontSize: 13,
color: '#B9B4E6',
marginTop: 4,
},
bbControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
bbBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#231C44',
borderWidth: 1,
borderColor: '#3A2F6D',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
bbBtnActive: {
backgroundColor: '#2B2356',
borderColor: '#4C3F8F',
},
bbIcon: {
width: 20,
height: 20,
marginRight: 6,
},
bbBtnText: {
color: '#EAE7FF',
fontSize: 13,
fontWeight: '600',
},
bbChips: {
flexDirection: 'row',
marginLeft: 8,
},
bbChip: {
backgroundColor: '#211B3F',
borderWidth: 1,
borderColor: '#372F6B',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
bbChipActive: {
backgroundColor: '#2A2350',
borderColor: '#4D3F91',
},
bbChipText: {
color: '#DCD7FF',
fontSize: 12,
fontWeight: '600',
},
bbBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
bbBar: {
width: 16,
backgroundColor: '#7C3AED',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
bbBarHL: {
backgroundColor: '#A78BFA',
},
bbBarSwap: {
backgroundColor: '#F87171',
},
bbBarSorted: {
backgroundColor: '#6B7280',
},
bbBarText: {
fontSize: 10,
color: '#EAE7FF',
marginBottom: 4,
fontWeight: '600',
},
bbFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
bbFootText: {
color: '#B9B4E6',
fontSize: 12,
},
bbDone: {
color: '#34D399',
fontWeight: '700',
},
ssContainer: {
marginHorizontal: 15,
},
ssCard: {
backgroundColor: '#07242D',
borderRadius: 16,
borderWidth: 1,
borderColor: '#0D3A46',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.18,
shadowRadius: 14,
paddingVertical: 14,
paddingHorizontal: 12,
},
ssHeader: {
marginBottom: 8,
},
ssTitle: {
fontSize: 18,
fontWeight: '700',
color: '#DFF6FF',
},
ssSubtitle: {
fontSize: 13,
color: '#96C9D6',
marginTop: 4,
},
ssControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
ssBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#0B2E37',
borderWidth: 1,
borderColor: '#134753',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
ssBtnActive: {
backgroundColor: '#0E3B46',
borderColor: '#1B5C6A',
},
ssIcon: {
width: 20,
height: 20,
marginRight: 6,
},
ssBtnText: {
color: '#DFF6FF',
fontSize: 13,
fontWeight: '600',
},
ssChips: {
flexDirection: 'row',
marginLeft: 8,
},
ssChip: {
backgroundColor: '#0A2B35',
borderWidth: 1,
borderColor: '#134753',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
ssChipActive: {
backgroundColor: '#0E3B46',
borderColor: '#1B5C6A',
},
ssChipText: {
color: '#CDEBF4',
fontSize: 12,
fontWeight: '600',
},
ssBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
ssBar: {
width: 16,
backgroundColor: '#22D3EE',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
ssBarHL: {
backgroundColor: '#38BDF8',
},
ssBarMin: {
backgroundColor: '#2DD4BF',
},
ssBarSwap: {
backgroundColor: '#F59E0B',
},
ssBarSorted: {
backgroundColor: '#6B7280',
},
ssBarText: {
fontSize: 10,
color: '#DFF6FF',
marginBottom: 4,
fontWeight: '600',
},
ssFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
ssFootText: {
color: '#96C9D6',
fontSize: 12,
},
ssDone: {
color: '#34D399',
fontWeight: '700',
},
bstContainer: {
marginHorizontal: 15,
},
bstCard: {
backgroundColor: '#0b1f16',
borderRadius: 16,
borderWidth: 1,
borderColor: '#163529',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.18,
shadowRadius: 14,
paddingVertical: 14,
paddingHorizontal: 12,
},
bstHeader: {
marginBottom: 8,
},
bstTitle: {
fontSize: 18,
fontWeight: '700',
color: '#e7f6ef',
},
bstSubtitle: {
fontSize: 13,
color: '#a6c9bb',
marginTop: 4,
},
bstControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
bstBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#123426',
borderWidth: 1,
borderColor: '#1a4a37',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
bstBtnActive: {
backgroundColor: '#154234',
borderColor: '#22624a',
},
bstIcon: {
width: 20,
height: 20,
marginRight: 6,
},
bstBtnText: {
color: '#e7f6ef',
fontSize: 13,
fontWeight: '600',
},
bstChip: {
backgroundColor: '#0f2c20',
borderWidth: 1,
borderColor: '#1a4a37',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
bstChipText: {
color: '#cde7dc',
fontSize: 12,
fontWeight: '600',
},
bstTree: {
position: 'relative',
minHeight: 420,
backgroundColor: '#0e251c',
borderRadius: 12,
borderWidth: 1,
borderColor: '#153a2c',
padding: 8,
},
bstNode: {
position: 'absolute',
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#1f6f4d',
borderWidth: 2,
borderColor: '#2e8a64',
justifyContent: 'center',
alignItems: 'center',
},
bstNodeHL: {
backgroundColor: '#239f6b',
borderColor: '#3ac18b',
},
bstNodeFound: {
backgroundColor: '#1fbf72',
borderColor: '#57d69b',
},
bstNodeText: {
color: '#e7f6ef',
fontSize: 12,
fontWeight: '700',
},
bstLine: {
position: 'absolute',
backgroundColor: '#1a4a37',
},
bstFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
bstFootText: {
color: '#a6c9bb',
fontSize: 12,
},
bstDone: {
color: '#43d998',
fontWeight: '700',
},
rsContainer: {
marginHorizontal: 15,
},
rsCard: {
backgroundColor: '#FFF1F5',
borderRadius: 16,
borderWidth: 1,
borderColor: '#FFD6E2',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.12,
shadowRadius: 12,
paddingVertical: 14,
paddingHorizontal: 12,
},
rsHeader: {
marginBottom: 8,
},
rsTitle: {
fontSize: 18,
fontWeight: '700',
color: '#7A163B',
},
rsSubtitle: {
fontSize: 13,
color: '#B0435B',
marginTop: 4,
},
rsControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
rsBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFE4EA',
borderWidth: 1,
borderColor: '#FFC9D6',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
rsBtnActive: {
backgroundColor: '#FFDCE5',
borderColor: '#FFB3C6',
},
rsIcon: {
width: 20,
height: 20,
marginRight: 6,
},
rsBtnText: {
color: '#7A163B',
fontSize: 13,
fontWeight: '600',
},
rsChips: {
flexDirection: 'row',
marginLeft: 8,
},
rsChip: {
backgroundColor: '#FFE8EE',
borderWidth: 1,
borderColor: '#FFD1DE',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
rsChipActive: {
backgroundColor: '#FFDCE5',
borderColor: '#FFB3C6',
},
rsChipText: {
color: '#7A163B',
fontSize: 12,
fontWeight: '600',
},
rsBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
rsBar: {
width: 16,
backgroundColor: '#FB7185',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
rsBarText: {
fontSize: 10,
color: '#7A163B',
marginBottom: 4,
fontWeight: '600',
},
rsBuckets: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: 6,
paddingVertical: 8,
},
rsBucket: {
width: (width - 30) / 5 - 6,
backgroundColor: '#FFEFF3',
borderWidth: 1,
borderColor: '#FFD6E2',
borderRadius: 10,
paddingVertical: 8,
paddingHorizontal: 8,
margin: 3,
alignItems: 'center',
},
rsBucketLabel: {
color: '#7A163B',
fontSize: 12,
fontWeight: '700',
marginBottom: 4,
},
rsBucketCount: {
color: '#B0435B',
fontSize: 12,
fontWeight: '600',
},
rsFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
rsFootText: {
color: '#B0435B',
fontSize: 12,
},
rsDone: {
color: '#22C55E',
fontWeight: '700',
},
bkContainer: {
marginHorizontal: 15,
},
bkCard: {
backgroundColor: '#FFF7E6',
borderRadius: 16,
borderWidth: 1,
borderColor: '#FDEBC5',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 12,
paddingVertical: 14,
paddingHorizontal: 12,
},
bkHeader: {
marginBottom: 8,
},
bkTitle: {
fontSize: 18,
fontWeight: '700',
color: '#7A4B16',
},
bkSubtitle: {
fontSize: 13,
color: '#B06C2E',
marginTop: 4,
},
bkControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
bkBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFF0D6',
borderWidth: 1,
borderColor: '#F7E2B8',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
bkBtnActive: {
backgroundColor: '#FFE8C7',
borderColor: '#F5D79F',
},
bkIcon: {
width: 20,
height: 20,
marginRight: 6,
},
bkBtnText: {
color: '#7A4B16',
fontSize: 13,
fontWeight: '600',
},
bkChips: {
flexDirection: 'row',
marginLeft: 8,
},
bkChip: {
backgroundColor: '#FFF3DD',
borderWidth: 1,
borderColor: '#F7E2B8',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
bkChipActive: {
backgroundColor: '#FFE8C7',
borderColor: '#F5D79F',
},
bkChipText: {
color: '#7A4B16',
fontSize: 12,
fontWeight: '600',
},
bkBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
bkBar: {
width: 16,
backgroundColor: '#F59E0B',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
bkBarHL: {
backgroundColor: '#FBBF24',
},
bkBarCollected: {
backgroundColor: '#6B7280',
},
bkBarText: {
fontSize: 10,
color: '#7A4B16',
marginBottom: 4,
fontWeight: '600',
},
bkBuckets: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: 6,
paddingVertical: 8,
},
bkBucket: {
width: (width - 30) / 4 - 6,
backgroundColor: '#FEF7E6',
borderWidth: 1,
borderColor: '#FDEBC5',
borderRadius: 10,
paddingVertical: 8,
paddingHorizontal: 8,
margin: 3,
alignItems: 'center',
},
bkBucketActive: {
borderColor: '#2BBBAD',
},
bkBucketLabel: {
color: '#7A4B16',
fontSize: 12,
fontWeight: '700',
marginBottom: 4,
},
bkBucketCount: {
color: '#B06C2E',
fontSize: 12,
fontWeight: '600',
},
bkFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
bkFootText: {
color: '#B06C2E',
fontSize: 12,
},
bkDone: {
color: '#22C55E',
fontWeight: '700',
},
shContainer: {
marginHorizontal: 15,
},
shCard: {
backgroundColor: '#0F172A',
borderRadius: 16,
borderWidth: 1,
borderColor: '#1F2937',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.2,
shadowRadius: 14,
paddingVertical: 14,
paddingHorizontal: 12,
},
shHeader: {
marginBottom: 8,
},
shTitle: {
fontSize: 18,
fontWeight: '700',
color: '#E2E8F0',
},
shSubtitle: {
fontSize: 13,
color: '#94A3B8',
marginTop: 4,
},
shControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
shBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#111827',
borderWidth: 1,
borderColor: '#1F2937',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
shBtnActive: {
backgroundColor: '#1F2937',
borderColor: '#374151',
},
shIcon: {
width: 20,
height: 20,
marginRight: 6,
},
shBtnText: {
color: '#E2E8F0',
fontSize: 13,
fontWeight: '600',
},
shChips: {
flexDirection: 'row',
marginLeft: 8,
},
shChip: {
backgroundColor: '#0B1220',
borderWidth: 1,
borderColor: '#1F2937',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
shChipActive: {
backgroundColor: '#172133',
borderColor: '#334155',
},
shChipText: {
color: '#C7D2FE',
fontSize: 12,
fontWeight: '600',
},
shBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
shBar: {
width: 16,
backgroundColor: '#22D3EE',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
shBarHL: {
backgroundColor: '#A78BFA',
},
shBarSwap: {
backgroundColor: '#F87171',
},
shBarGroup: {
backgroundColor: '#0EA5E9',
},
shBarText: {
fontSize: 10,
color: '#E2E8F0',
marginBottom: 4,
fontWeight: '600',
},
shFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
shFootText: {
color: '#94A3B8',
fontSize: 12,
},
shDone: {
color: '#34D399',
fontWeight: '700',
},
ctContainer: {
marginHorizontal: 15,
},
ctCard: {
backgroundColor: '#ECFDF5',
borderRadius: 16,
borderWidth: 1,
borderColor: '#A7F3D0',
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 12,
paddingVertical: 14,
paddingHorizontal: 12,
},
ctHeader: {
marginBottom: 8,
},
ctTitle: {
fontSize: 18,
fontWeight: '700',
color: '#065F46',
},
ctSubtitle: {
fontSize: 13,
color: '#10B981',
marginTop: 4,
},
ctControls: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
marginTop: 8,
marginBottom: 12,
},
ctBtn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#D1FAE5',
borderWidth: 1,
borderColor: '#A7F3D0',
borderRadius: 12,
paddingVertical: 8,
paddingHorizontal: 10,
marginRight: 8,
},
ctBtnActive: {
backgroundColor: '#C7F9D7',
borderColor: '#6EE7B7',
},
ctIcon: {
width: 20,
height: 20,
marginRight: 6,
},
ctBtnText: {
color: '#065F46',
fontSize: 13,
fontWeight: '600',
},
ctChips: {
flexDirection: 'row',
marginLeft: 8,
},
ctChip: {
backgroundColor: '#E7FBEF',
borderWidth: 1,
borderColor: '#A7F3D0',
borderRadius: 10,
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 6,
},
ctChipActive: {
backgroundColor: '#C7F9D7',
borderColor: '#6EE7B7',
},
ctChipText: {
color: '#0F766E',
fontSize: 12,
fontWeight: '600',
},
ctBars: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 6,
minHeight: 220,
},
ctBar: {
width: 16,
backgroundColor: '#34D399',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
justifyContent: 'flex-end',
alignItems: 'center',
marginHorizontal: 2,
},
ctBarHL: {
backgroundColor: '#10B981',
},
ctBarPlaced: {
backgroundColor: '#6B7280',
},
ctBarText: {
fontSize: 10,
color: '#065F46',
marginBottom: 4,
fontWeight: '600',
},
ctBuckets: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: 6,
paddingVertical: 8,
},
ctBucket: {
width: (width - 30) / 4 - 6,
backgroundColor: '#F0FFF4',
borderWidth: 1,
borderColor: '#A7F3D0',
borderRadius: 10,
paddingVertical: 8,
paddingHorizontal: 8,
margin: 3,
alignItems: 'center',
},
ctBucketActive: {
borderColor: '#0EA5E9',
},
ctBucketLabel: {
color: '#0F766E',
fontSize: 12,
fontWeight: '700',
marginBottom: 4,
},
ctBucketCount: {
color: '#10B981',
fontSize: 12,
fontWeight: '600',
},
ctFooterRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 6,
},
ctFootText: {
color: '#10B981',
fontSize: 12,
},
ctDone: {
color: '#22C55E',
fontWeight: '700',
},
});
const ICON_BASE64_MS = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_BST = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
insert: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
remove: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
find: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_BUBBLE = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_SELECT = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_SHELL = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_COUNT = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_RADIX = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_BUCKET = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_BS = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const ICON_BASE64_LS = {
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};
const MergeSortVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [steps, setSteps] = useState<{ arr: number[]; highlights: number[] }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(600);
const timerRef = React.useRef<any>(null);
const maxBars = 18;
const genArray = () => {
const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
setArr(a);
const s = buildSteps(a);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (a: number[]) => {
const n = a.length;
let src = a.slice();
const out: { arr: number[]; highlights: number[] }[] = [{ arr: src.slice(), highlights: [] }];
for (let size = 1; size < n; size *= 2) {
for (let left = 0; left < n; left += 2 * size) {
const mid = Math.min(left + size, n);
const right = Math.min(left + 2 * size, n);
let i = left, j = mid;
const merged: number[] = [];
while (i < mid && j < right) {
if (src[i] <= src[j]) { merged.push(src[i]); i++; } else { merged.push(src[j]); j++; }
}
while (i < mid) { merged.push(src[i]); i++; }
while (j < right) { merged.push(src[j]); j++; }
for (let k = 0; k < merged.length; k++) src[left + k] = merged[k];
out.push({ arr: src.slice(), highlights: Array.from({ length: merged.length }, (_, k) => left + k) });
}
}
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
setArr(steps[next].arr);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const isDone = idx === steps.length - 1 && steps.length > 1;
return (
<View style={styles.msCard}>
<View style={styles.msHeader}>
<Text style={styles.msTitle}>归并排序 · 霓虹风格</Text>
<Text style={styles.msSubtitle}>播放、步进、重置、随机</Text>
</View>
<View style={styles.msControls}>
<TouchableOpacity style={[styles.msBtn, playing ? styles.msBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_MS.pause : ICON_BASE64_MS.play }} style={styles.msIcon} />
<Text style={styles.msBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.msBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_MS.step }} style={styles.msIcon} />
<Text style={styles.msBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.msBtn} onPress={resetSort} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_MS.reset }} style={styles.msIcon} />
<Text style={styles.msBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.msBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_MS.shuffle }} style={styles.msIcon} />
<Text style={styles.msBtnText}>随机</Text>
</TouchableOpacity>
<View style={styles.msChips}>
<TouchableOpacity style={styles.msChip} onPress={() => setSpeed(900)}><Text style={styles.msChipText}>0.5×</Text></TouchableOpacity>
<TouchableOpacity style={[styles.msChip, styles.msChipActive]} onPress={() => setSpeed(600)}><Text style={styles.msChipText}>1×</Text></TouchableOpacity>
<TouchableOpacity style={styles.msChip} onPress={() => setSpeed(300)}><Text style={styles.msChipText}>2×</Text></TouchableOpacity>
</View>
</View>
<View style={styles.msBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const hl = steps[idx]?.highlights.includes(i);
return (
<View key={i} style={[styles.msBar, { height: h }, hl ? styles.msBarHL : null]}>
<Text style={styles.msBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.msFooterRow}>
<Text style={styles.msFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
<Text style={[styles.msFootText, isDone ? styles.msDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
</View>
</View>
);
};
const BinarySearchVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [target, setTarget] = useState<number>(0);
const [steps, setSteps] = useState<{ low: number; high: number; mid: number; found: boolean }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(700);
const timerRef = React.useRef<any>(null);
const maxBars = 20;
const genArray = () => {
const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
const sorted = base.sort((a, b) => a - b);
setArr(sorted);
const t = sorted[Math.floor(Math.random() * sorted.length)];
setTarget(t);
const s = buildSteps(sorted, t);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (a: number[], t: number) => {
let low = 0, high = a.length - 1;
const out: { low: number; high: number; mid: number; found: boolean }[] = [];
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const found = a[mid] === t;
out.push({ low, high, mid, found });
if (found) break;
if (a[mid] < t) low = mid + 1; else high = mid - 1;
}
if (out.length === 0) out.push({ low: 0, high: a.length - 1, mid: -1, found: false });
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const isDone = idx === steps.length - 1 && steps.length > 0;
const cur = steps[idx] || { low: 0, high: arr.length - 1, mid: -1, found: false };
return (
<View style={styles.bsCard}>
<View style={styles.bsHeader}>
<Text style={styles.bsTitle}>二分查找 · 柔和风格</Text>
<Text style={styles.bsSubtitle}>播放、步进、重置、随机、目标调整</Text>
</View>
<View style={styles.bsControls}>
<TouchableOpacity style={[styles.bsBtn, playing ? styles.bsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_BS.pause : ICON_BASE64_BS.play }} style={styles.bsIcon} />
<Text style={styles.bsBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bsBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BS.step }} style={styles.bsIcon} />
<Text style={styles.bsBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bsBtn} onPress={resetSearch} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BS.reset }} style={styles.bsIcon} />
<Text style={styles.bsBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bsBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BS.shuffle }} style={styles.bsIcon} />
<Text style={styles.bsBtnText}>随机</Text>
</TouchableOpacity>
</View>
<View style={styles.bsTargetRow}>
<TouchableOpacity style={styles.bsChip} onPress={decTarget}><Text style={styles.bsChipText}>目标 -</Text></TouchableOpacity>
<View style={styles.bsChip}><Text style={styles.bsChipText}>当前目标:{target}</Text></View>
<TouchableOpacity style={styles.bsChip} onPress={incTarget}><Text style={styles.bsChipText}>目标 +</Text></TouchableOpacity>
<View style={styles.bsChip}><Text style={styles.bsChipText}>范围:{cur.low} - {cur.high}</Text></View>
</View>
<View style={styles.bsBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const inRange = i >= cur.low && i <= cur.high;
const isMid = i === cur.mid;
const isTarget = v === target && inRange;
return (
<View key={i} style={[styles.bsBar, { height: h }, !inRange ? styles.bsBarDim : null, isMid ? styles.bsBarMid : null, isTarget ? styles.bsBarTarget : null]}>
<Text style={styles.bsBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.bsFooterRow}>
<Text style={styles.bsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
<Text style={[styles.bsFootText, isDone && cur.found ? styles.bsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
</View>
</View>
);
};
const LinearSearchVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [target, setTarget] = useState<number>(0);
const [steps, setSteps] = useState<{ idx: number; found: boolean }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(700);
const timerRef = React.useRef<any>(null);
const maxBars = 22;
const genArray = () => {
const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
setArr(base);
const t = base[Math.floor(Math.random() * base.length)];
setTarget(t);
const s = buildSteps(base, t);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (a: number[], t: number) => {
const out: { idx: number; found: boolean }[] = [];
for (let i = 0; i < a.length; i++) {
const found = a[i] === t;
out.push({ idx: i, found });
if (found) break;
}
if (out.length === 0) out.push({ idx: -1, found: false });
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const isDone = idx === steps.length - 1 && steps.length > 0;
const cur = steps[idx] || { idx: -1, found: false };
return (
<View style={styles.lsCard}>
<View style={styles.lsHeader}>
<Text style={styles.lsTitle}>线性查找 · 暖色风格</Text>
<Text style={styles.lsSubtitle}>播放、步进、重置、随机、目标调整</Text>
</View>
<View style={styles.lsControls}>
<TouchableOpacity style={[styles.lsBtn, playing ? styles.lsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_LS.pause : ICON_BASE64_LS.play }} style={styles.lsIcon} />
<Text style={styles.lsBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.lsBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_LS.step }} style={styles.lsIcon} />
<Text style={styles.lsBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.lsBtn} onPress={resetSearch} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_LS.reset }} style={styles.lsIcon} />
<Text style={styles.lsBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.lsBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_LS.shuffle }} style={styles.lsIcon} />
<Text style={styles.lsBtnText}>随机</Text>
</TouchableOpacity>
</View>
<View style={styles.lsTargetRow}>
<TouchableOpacity style={styles.lsChip} onPress={decTarget}><Text style={styles.lsChipText}>目标 -</Text></TouchableOpacity>
<View style={styles.lsChip}><Text style={styles.lsChipText}>当前目标:{target}</Text></View>
<TouchableOpacity style={styles.lsChip} onPress={incTarget}><Text style={styles.lsChipText}>目标 +</Text></TouchableOpacity>
<View style={styles.lsChip}><Text style={styles.lsChipText}>索引:{cur.idx >= 0 ? cur.idx : '-'}</Text></View>
</View>
<View style={styles.lsBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const isCur = i === cur.idx;
const isTarget = v === target;
return (
<View key={i} style={[styles.lsBar, { height: h }, isCur ? styles.lsBarCur : null, isTarget ? styles.lsBarTarget : null]}>
<Text style={styles.lsBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.lsFooterRow}>
<Text style={styles.lsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
<Text style={[styles.lsFootText, isDone && cur.found ? styles.lsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
</View>
</View>
);
};
const BSTVisualizer: React.FC = () => {
const [root, setRoot] = useState<any>(null);
const [target, setTarget] = useState<number>(0);
const [steps, setSteps] = useState<number[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(700);
const timerRef = React.useRef<any>(null);
const levelGap = 90;
const padding = 24;
const insert = (k: number) => {
const node = { key: k, left: null, right: null };
if (!root) { setRoot(node); return; }
const r = JSON.parse(JSON.stringify(root));
let cur: any = r;
while (true) {
if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
else break;
}
setRoot(r);
};
const findPath = (k: number) => {
const path: number[] = [];
let cur: any = root;
while (cur) {
path.push(cur.key);
if (k === cur.key) break;
cur = k < cur.key ? cur.left : cur.right;
}
return path;
};
const minNode = (n: any) => { let c = n; while (c && c.left) c = c.left; return c; };
const removeKey = (k: number) => {
const del = (n: any, v: number): any => {
if (!n) return null;
if (v < n.key) { n.left = del(n.left, v); return n; }
if (v > n.key) { n.right = del(n.right, v); return n; }
if (!n.left) return n.right;
if (!n.right) return n.left;
const succ = minNode(n.right);
n.key = succ.key;
n.right = del(n.right, succ.key);
return n;
};
const r = JSON.parse(JSON.stringify(root));
setRoot(del(r, k));
};
const buildRandom = () => {
let r: any = null;
const values = Array.from({ length: 10 }, () => Math.floor(10 + Math.random() * 90));
values.forEach(v => { r = insertInto(r, v); });
setRoot(r);
const t = values[Math.floor(Math.random() * values.length)];
setTarget(t);
const p = findPathOn(r, t);
setSteps(p);
setIdx(0);
setPlaying(false);
};
const insertInto = (r: any, k: number) => {
const node = { key: k, left: null, right: null };
if (!r) return node;
let cur = r;
while (true) {
if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
else break;
}
return r;
};
const findPathOn = (r: any, k: number) => {
const path: number[] = [];
let cur = r;
while (cur) {
path.push(cur.key);
if (k === cur.key) break;
cur = k < cur.key ? cur.left : cur.right;
}
return path;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSearch = () => { const p = findPath(target); setSteps(p); setIdx(0); setPlaying(false); };
const shuffle = () => buildRandom();
const incTarget = () => setTarget(t => { const nt = t + 1; const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
const doInsert = () => { const v = Math.floor(10 + Math.random() * 90); insert(v); const p = findPath(target); setSteps(p); setIdx(0); };
const doRemove = () => { removeKey(target); const p = findPath(target); setSteps(p); setIdx(0); };
const clearTree = () => { setRoot(null); setSteps([]); setIdx(0); setPlaying(false); };
React.useEffect(() => { buildRandom(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const layout = buildLayout(root);
const curKey = steps[idx];
const isDone = steps.length > 0 && idx === steps.length - 1 && curKey === target;
return (
<View style={styles.bstCard}>
<View style={styles.bstHeader}>
<Text style={styles.bstTitle}>二叉搜索树 · 森林风格</Text>
<Text style={styles.bstSubtitle}>播放、步进、插入、删除、目标调整</Text>
</View>
<View style={styles.bstControls}>
<TouchableOpacity style={[styles.bstBtn, playing ? styles.bstBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_BST.pause : ICON_BASE64_BST.play }} style={styles.bstIcon} />
<Text style={styles.bstBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bstBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BST.step }} style={styles.bstIcon} />
<Text style={styles.bstBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bstBtn} onPress={resetSearch} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BST.reset }} style={styles.bstIcon} />
<Text style={styles.bstBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bstBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BST.shuffle }} style={styles.bstIcon} />
<Text style={styles.bstBtnText}>随机</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bstBtn} onPress={doInsert} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BST.insert }} style={styles.bstIcon} />
<Text style={styles.bstBtnText}>插入</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bstBtn} onPress={doRemove} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BST.remove }} style={styles.bstIcon} />
<Text style={styles.bstBtnText}>删除</Text>
</TouchableOpacity>
</View>
<View style={styles.bstControls}>
<TouchableOpacity style={styles.bstChip} onPress={decTarget}><Text style={styles.bstChipText}>目标 -</Text></TouchableOpacity>
<View style={styles.bstChip}><Text style={styles.bstChipText}>当前目标:{target}</Text></View>
<TouchableOpacity style={styles.bstChip} onPress={incTarget}><Text style={styles.bstChipText}>目标 +</Text></TouchableOpacity>
</View>
<View style={styles.bstTree}>
{layout.lines.map((l: any, i: number) => (
<View key={`line-${i}`} style={[styles.bstLine, { left: l.vx, top: l.vy, width: 2, height: l.vh }]} />
))}
{layout.lines.map((l: any, i: number) => (
<View key={`hline-${i}`} style={[styles.bstLine, { left: l.hx, top: l.hy, width: l.hw, height: 2 }]} />
))}
{layout.nodes.map((n: any) => (
<View key={`node-${n.key}-${n.x}-${n.y}`} style={[styles.bstNode, { left: n.x - 20, top: n.y - 20 }, n.key === curKey ? styles.bstNodeHL : null, n.key === target && curKey === target ? styles.bstNodeFound : null]}>
<Text style={styles.bstNodeText}>{n.key}</Text>
</View>
))}
</View>
<View style={styles.bstFooterRow}>
<Text style={styles.bstFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
<Text style={[styles.bstFootText, isDone ? styles.bstDone : null]}>{isDone ? '已找到目标' : '进行中'}</Text>
</View>
</View>
);
function buildLayout(r: any) {
const nodes: any[] = [];
const lines: any[] = [];
if (!r) return { nodes, lines };
const queue: any[] = [{ n: r, level: 0, idx: 0 }];
const levels: any = {};
while (queue.length) {
const item = queue.shift();
const lvl = item.level;
levels[lvl] = levels[lvl] || [];
levels[lvl].push(item.n);
if (item.n.left) queue.push({ n: item.n.left, level: lvl + 1, idx: 0 });
if (item.n.right) queue.push({ n: item.n.right, level: lvl + 1, idx: 0 });
}
const contentWidth = width - padding * 2;
const posMap: any = new Map();
const maxLevel = Math.max(0, ...Object.keys(levels).map(v => parseInt(v, 10)));
for (let l = 0; l <= maxLevel; l++) {
const row = levels[l] || [];
const count = row.length || 1;
for (let i = 0; i < row.length; i++) {
const x = padding + Math.round(((i + 1) * contentWidth) / (count + 1));
const y = padding + l * levelGap + 20;
posMap.set(row[i], { x, y });
nodes.push({ key: row[i].key, x, y });
}
}
const addLines = (n: any) => {
const p = posMap.get(n);
if (n.left) {
const c = posMap.get(n.left);
const vy = p.y + 20;
const vh = c.y - p.y - 20;
const vx = p.x;
const hx = Math.min(p.x, c.x);
const hy = c.y;
const hw = Math.abs(c.x - p.x);
lines.push({ vx, vy, vh, hx, hy, hw });
addLines(n.left);
}
if (n.right) {
const c = posMap.get(n.right);
const vy = p.y + 20;
const vh = c.y - p.y - 20;
const vx = p.x;
const hx = Math.min(p.x, c.x);
const hy = c.y;
const hw = Math.abs(c.x - p.x);
lines.push({ vx, vy, vh, hx, hy, hw });
addLines(n.right);
}
};
addLines(r);
return { nodes, lines };
}
};
const BubbleSortVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [steps, setSteps] = useState<{ arr: number[]; a: number; b: number; swapped: boolean; sorted: number }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(600);
const timerRef = React.useRef<any>(null);
const maxBars = 18;
const genArray = () => {
const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
setArr(a);
const s = buildSteps(a);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (base: number[]) => {
const a = base.slice();
const n = a.length;
const out: { arr: number[]; a: number; b: number; swapped: boolean; sorted: number }[] = [{ arr: a.slice(), a: -1, b: -1, swapped: false, sorted: n }];
for (let i = n - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
const swapped = a[j] > a[j + 1];
if (swapped) {
const t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
}
out.push({ arr: a.slice(), a: j, b: j + 1, swapped, sorted: i });
}
}
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
setArr(steps[next].arr);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const cur = steps[idx] || { a: -1, b: -1, swapped: false, sorted: arr.length };
const isDone = idx === steps.length - 1 && steps.length > 1;
return (
<View style={styles.bbCard}>
<View style={styles.bbHeader}>
<Text style={styles.bbTitle}>冒泡排序 · 玻璃风格</Text>
<Text style={styles.bbSubtitle}>播放、步进、重置、随机与速度</Text>
</View>
<View style={styles.bbControls}>
<TouchableOpacity style={[styles.bbBtn, playing ? styles.bbBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_BUBBLE.pause : ICON_BASE64_BUBBLE.play }} style={styles.bbIcon} />
<Text style={styles.bbBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bbBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BUBBLE.step }} style={styles.bbIcon} />
<Text style={styles.bbBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bbBtn} onPress={resetSort} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BUBBLE.reset }} style={styles.bbIcon} />
<Text style={styles.bbBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bbBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BUBBLE.shuffle }} style={styles.bbIcon} />
<Text style={styles.bbBtnText}>随机</Text>
</TouchableOpacity>
<View style={styles.bbChips}>
<TouchableOpacity style={styles.bbChip} onPress={() => setSpeed(900)}><Text style={styles.bbChipText}>0.5×</Text></TouchableOpacity>
<TouchableOpacity style={[styles.bbChip, styles.bbChipActive]} onPress={() => setSpeed(600)}><Text style={styles.bbChipText}>1×</Text></TouchableOpacity>
<TouchableOpacity style={styles.bbChip} onPress={() => setSpeed(300)}><Text style={styles.bbChipText}>2×</Text></TouchableOpacity>
</View>
</View>
<View style={styles.bbBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const inCompare = i === cur.a || i === cur.b;
const swapped = inCompare && cur.swapped;
const sorted = i >= cur.sorted;
return (
<View key={i} style={[styles.bbBar, { height: h }, inCompare ? styles.bbBarHL : null, swapped ? styles.bbBarSwap : null, sorted ? styles.bbBarSorted : null]}>
<Text style={styles.bbBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.bbFooterRow}>
<Text style={styles.bbFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
<Text style={[styles.bbFootText, isDone ? styles.bbDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
</View>
</View>
);
};
const SelectionSortVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [steps, setSteps] = useState<{ arr: number[]; i: number; j: number; minIdx: number; swapped: boolean; sortedStart: number }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(600);
const timerRef = React.useRef<any>(null);
const maxBars = 18;
const genArray = () => {
const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
setArr(a);
const s = buildSteps(a);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (base: number[]) => {
const a = base.slice();
const n = a.length;
const out: { arr: number[]; i: number; j: number; minIdx: number; swapped: boolean; sortedStart: number }[] = [{ arr: a.slice(), i: -1, j: -1, minIdx: -1, swapped: false, sortedStart: 0 }];
for (let i = 0; i < n - 1; i++) {
let minIdx = i;
for (let j = i + 1; j < n; j++) {
if (a[j] < a[minIdx]) minIdx = j;
out.push({ arr: a.slice(), i, j, minIdx, swapped: false, sortedStart: i });
}
if (minIdx !== i) {
const t = a[i]; a[i] = a[minIdx]; a[minIdx] = t;
out.push({ arr: a.slice(), i, j: minIdx, minIdx, swapped: true, sortedStart: i });
}
out.push({ arr: a.slice(), i, j: -1, minIdx, swapped: false, sortedStart: i + 1 });
}
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
setArr(steps[next].arr);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const cur = steps[idx] || { i: -1, j: -1, minIdx: -1, swapped: false, sortedStart: 0 };
const isDone = idx === steps.length - 1 && steps.length > 1;
return (
<View style={styles.ssCard}>
<View style={styles.ssHeader}>
<Text style={styles.ssTitle}>选择排序 · 海洋风格</Text>
<Text style={styles.ssSubtitle}>播放、步进、重置、随机与速度</Text>
</View>
<View style={styles.ssControls}>
<TouchableOpacity style={[styles.ssBtn, playing ? styles.ssBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_SELECT.pause : ICON_BASE64_SELECT.play }} style={styles.ssIcon} />
<Text style={styles.ssBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ssBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_SELECT.step }} style={styles.ssIcon} />
<Text style={styles.ssBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ssBtn} onPress={resetSort} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_SELECT.reset }} style={styles.ssIcon} />
<Text style={styles.ssBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ssBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_SELECT.shuffle }} style={styles.ssIcon} />
<Text style={styles.ssBtnText}>随机</Text>
</TouchableOpacity>
<View style={styles.ssChips}>
<TouchableOpacity style={styles.ssChip} onPress={() => setSpeed(900)}><Text style={styles.ssChipText}>0.5×</Text></TouchableOpacity>
<TouchableOpacity style={[styles.ssChip, styles.ssChipActive]} onPress={() => setSpeed(600)}><Text style={styles.ssChipText}>1×</Text></TouchableOpacity>
<TouchableOpacity style={styles.ssChip} onPress={() => setSpeed(300)}><Text style={styles.ssChipText}>2×</Text></TouchableOpacity>
</View>
</View>
<View style={styles.ssBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const inCompare = i === cur.i || i === cur.j;
const isMin = i === cur.minIdx;
const swapped = inCompare && cur.swapped;
const sorted = i < cur.sortedStart;
return (
<View key={i} style={[styles.ssBar, { height: h }, inCompare ? styles.ssBarHL : null, isMin ? styles.ssBarMin : null, swapped ? styles.ssBarSwap : null, sorted ? styles.ssBarSorted : null]}>
<Text style={styles.ssBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.ssFooterRow}>
<Text style={styles.ssFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
<Text style={[styles.ssFootText, isDone ? styles.ssDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
</View>
</View>
);
};
const RadixSortVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [steps, setSteps] = useState<{ arr: number[]; buckets: number[][]; digit: number; phase: 'dist' | 'collect'; base: number }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(600);
const [radix, setRadix] = useState(10);
const timerRef = React.useRef<any>(null);
const maxBars = 18;
const genArray = () => {
const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 980));
setArr(a);
const s = buildSteps(a, radix);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const maxDigits = (a: number[], base: number) => {
const m = Math.max(...a, 0);
let d = 0; let v = m;
while (v > 0) { d++; v = Math.floor(v / base); }
return Math.max(1, d);
};
const buildSteps = (baseArr: number[], base: number) => {
const a = baseArr.slice();
const md = maxDigits(a, base);
const out: { arr: number[]; buckets: number[][]; digit: number; phase: 'dist' | 'collect'; base: number }[] = [{ arr: a.slice(), buckets: Array.from({ length: base }, () => []), digit: 0, phase: 'dist', base }];
for (let d = 0; d < md; d++) {
const buckets: number[][] = Array.from({ length: base }, () => []);
for (let i = 0; i < a.length; i++) {
const digit = Math.floor(a[i] / Math.pow(base, d)) % base;
buckets[digit].push(a[i]);
}
out.push({ arr: a.slice(), buckets: buckets.map(b => b.slice()), digit: d, phase: 'dist', base });
const flattened: number[] = [];
for (let b = 0; b < base; b++) {
for (let k = 0; k < buckets[b].length; k++) flattened.push(buckets[b][k]);
}
for (let i = 0; i < a.length; i++) a[i] = flattened[i];
out.push({ arr: a.slice(), buckets: buckets.map(b => b.slice()), digit: d, phase: 'collect', base });
}
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
setArr(steps[next].arr);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSort = () => { const s = buildSteps(arr.slice(), radix); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
const setBase = (b: number) => { setRadix(b); const s = buildSteps(arr.slice(), b); setSteps(s); setIdx(0); setPlaying(false); };
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const cur = steps[idx] || { digit: 0, buckets: Array.from({ length: radix }, () => []), phase: 'dist', base: radix };
const isDone = idx === steps.length - 1 && steps.length > 1;
return (
<View style={styles.rsCard}>
<View style={styles.rsHeader}>
<Text style={styles.rsTitle}>基数排序 · 糖果风格</Text>
<Text style={styles.rsSubtitle}>播放、步进、重置、随机、进制切换</Text>
</View>
<View style={styles.rsControls}>
<TouchableOpacity style={[styles.rsBtn, playing ? styles.rsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_RADIX.pause : ICON_BASE64_RADIX.play }} style={styles.rsIcon} />
<Text style={styles.rsBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.rsBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_RADIX.step }} style={styles.rsIcon} />
<Text style={styles.rsBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.rsBtn} onPress={resetSort} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_RADIX.reset }} style={styles.rsIcon} />
<Text style={styles.rsBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.rsBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_RADIX.shuffle }} style={styles.rsIcon} />
<Text style={styles.rsBtnText}>随机</Text>
</TouchableOpacity>
<View style={styles.rsChips}>
<TouchableOpacity style={[styles.rsChip, radix === 10 ? styles.rsChipActive : null]} onPress={() => setBase(10)}><Text style={styles.rsChipText}>10进制</Text></TouchableOpacity>
<TouchableOpacity style={[styles.rsChip, radix === 16 ? styles.rsChipActive : null]} onPress={() => setBase(16)}><Text style={styles.rsChipText}>16进制</Text></TouchableOpacity>
<TouchableOpacity style={styles.rsChip} onPress={() => setSpeed(900)}><Text style={styles.rsChipText}>0.5×</Text></TouchableOpacity>
<TouchableOpacity style={[styles.rsChip, styles.rsChipActive]} onPress={() => setSpeed(600)}><Text style={styles.rsChipText}>1×</Text></TouchableOpacity>
<TouchableOpacity style={styles.rsChip} onPress={() => setSpeed(300)}><Text style={styles.rsChipText}>2×</Text></TouchableOpacity>
</View>
</View>
<View style={styles.rsBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
return (
<View key={i} style={[styles.rsBar, { height: h }]}>
<Text style={styles.rsBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.rsBuckets}>
{Array.from({ length: cur.base }, (_, b) => (
<View key={`b-${b}`} style={styles.rsBucket}>
<Text style={styles.rsBucketLabel}>桶 {b}</Text>
<Text style={styles.rsBucketCount}>{cur.buckets[b]?.length || 0} 项</Text>
</View>
))}
</View>
<View style={styles.rsFooterRow}>
<Text style={styles.rsFootText}>位数 {cur.digit + 1} / {Math.max(maxDigits(arr, radix), 1)} · 阶段 {cur.phase === 'dist' ? '分配' : '收集'}</Text>
<Text style={[styles.rsFootText, isDone ? styles.rsDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
</View>
</View>
);
};
const BucketSortVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [steps, setSteps] = useState<{ arr: number[]; buckets: number[][]; phase: 'dist' | 'sort' | 'collect'; curIndex?: number; curBucket?: number; collected?: number; bucketCount: number; min: number; max: number }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(600);
const [bucketCount, setBucketCount] = useState(8);
const timerRef = React.useRef<any>(null);
const maxBars = 18;
const genArray = () => {
const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
setArr(a);
const s = buildSteps(a, bucketCount);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (baseArr: number[], bucketsN: number) => {
const a = baseArr.slice();
const min = Math.min(...a);
const max = Math.max(...a);
const range = Math.max(1, max - min + 1);
const buckets: number[][] = Array.from({ length: bucketsN }, () => []);
const out: { arr: number[]; buckets: number[][]; phase: 'dist' | 'sort' | 'collect'; curIndex?: number; curBucket?: number; collected?: number; bucketCount: number; min: number; max: number }[] = [{ arr: a.slice(), buckets: buckets.map(b => b.slice()), phase: 'dist', bucketCount: bucketsN, min, max }];
for (let i = 0; i < a.length; i++) {
const bi = Math.min(bucketsN - 1, Math.floor(((a[i] - min) / range) * bucketsN));
buckets[bi].push(a[i]);
out.push({ arr: a.slice(), buckets: buckets.map(b => b.slice()), phase: 'dist', curIndex: i, bucketCount: bucketsN, min, max });
}
for (let b = 0; b < bucketsN; b++) {
buckets[b].sort((x, y) => x - y);
out.push({ arr: a.slice(), buckets: buckets.map(bb => bb.slice()), phase: 'sort', curBucket: b, bucketCount: bucketsN, min, max });
}
const flat: number[] = [];
for (let b = 0; b < bucketsN; b++) for (let k = 0; k < buckets[b].length; k++) flat.push(buckets[b][k]);
for (let i = 0; i < a.length; i++) {
a[i] = flat[i];
out.push({ arr: a.slice(), buckets: buckets.map(bb => bb.slice()), phase: 'collect', collected: i + 1, bucketCount: bucketsN, min, max });
}
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
setArr(steps[next].arr);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSort = () => { const s = buildSteps(arr.slice(), bucketCount); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
const setBuckets = (n: number) => { setBucketCount(n); const s = buildSteps(arr.slice(), n); setSteps(s); setIdx(0); setPlaying(false); };
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const cur = steps[idx] || { phase: 'dist', buckets: Array.from({ length: bucketCount }, () => []), bucketCount };
const isDone = idx === steps.length - 1 && steps.length > 1;
return (
<View style={styles.bkCard}>
<View style={styles.bkHeader}>
<Text style={styles.bkTitle}>桶排序 · 沙滩风格</Text>
<Text style={styles.bkSubtitle}>播放、步进、重置、随机、桶数切换</Text>
</View>
<View style={styles.bkControls}>
<TouchableOpacity style={[styles.bkBtn, playing ? styles.bkBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_BUCKET.pause : ICON_BASE64_BUCKET.play }} style={styles.bkIcon} />
<Text style={styles.bkBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bkBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BUCKET.step }} style={styles.bkIcon} />
<Text style={styles.bkBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bkBtn} onPress={resetSort} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BUCKET.reset }} style={styles.bkIcon} />
<Text style={styles.bkBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bkBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_BUCKET.shuffle }} style={styles.bkIcon} />
<Text style={styles.bkBtnText}>随机</Text>
</TouchableOpacity>
<View style={styles.bkChips}>
<TouchableOpacity style={[styles.bkChip, bucketCount === 5 ? styles.bkChipActive : null]} onPress={() => setBuckets(5)}><Text style={styles.bkChipText}>5桶</Text></TouchableOpacity>
<TouchableOpacity style={[styles.bkChip, bucketCount === 8 ? styles.bkChipActive : null]} onPress={() => setBuckets(8)}><Text style={styles.bkChipText}>8桶</Text></TouchableOpacity>
<TouchableOpacity style={[styles.bkChip, bucketCount === 12 ? styles.bkChipActive : null]} onPress={() => setBuckets(12)}><Text style={styles.bkChipText}>12桶</Text></TouchableOpacity>
<TouchableOpacity style={styles.bkChip} onPress={() => setSpeed(900)}><Text style={styles.bkChipText}>0.5×</Text></TouchableOpacity>
<TouchableOpacity style={[styles.bkChip, styles.bkChipActive]} onPress={() => setSpeed(600)}><Text style={styles.bkChipText}>1×</Text></TouchableOpacity>
<TouchableOpacity style={styles.bkChip} onPress={() => setSpeed(300)}><Text style={styles.bkChipText}>2×</Text></TouchableOpacity>
</View>
</View>
<View style={styles.bkBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const distHL = cur.phase === 'dist' && cur.curIndex === i;
const collectedHL = cur.phase === 'collect' && typeof cur.collected === 'number' && i < (cur.collected || 0);
return (
<View key={i} style={[styles.bkBar, { height: h }, distHL ? styles.bkBarHL : null, collectedHL ? styles.bkBarCollected : null]}>
<Text style={styles.bkBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.bkBuckets}>
{Array.from({ length: cur.bucketCount }, (_, b) => (
<View key={`bk-${b}`} style={[styles.bkBucket, cur.phase === 'sort' && cur.curBucket === b ? styles.bkBucketActive : null]}>
<Text style={styles.bkBucketLabel}>桶 {b}</Text>
<Text style={styles.bkBucketCount}>{cur.buckets[b]?.length || 0} 项</Text>
</View>
))}
</View>
<View style={styles.bkFooterRow}>
<Text style={styles.bkFootText}>阶段 {cur.phase === 'dist' ? '分配' : cur.phase === 'sort' ? '桶内排序' : '收集'} · 桶数 {cur.bucketCount}</Text>
<Text style={[styles.bkFootText, isDone ? styles.bkDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
</View>
</View>
);
};
const ShellSortVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [steps, setSteps] = useState<{ arr: number[]; i: number; j: number; gap: number; swapped: boolean; group: number }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(600);
const timerRef = React.useRef<any>(null);
const maxBars = 18;
const genArray = () => {
const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
setArr(a);
const s = buildSteps(a);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (base: number[]) => {
const a = base.slice();
const n = a.length;
const out: { arr: number[]; i: number; j: number; gap: number; swapped: boolean; group: number }[] = [{ arr: a.slice(), i: -1, j: -1, gap: Math.floor(n / 2), swapped: false, group: 0 }];
for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let j = gap; j < n; j++) {
let temp = a[j];
let i = j;
out.push({ arr: a.slice(), i, j: i - gap, gap, swapped: false, group: j % gap });
while (i >= gap && a[i - gap] > temp) {
a[i] = a[i - gap];
i -= gap;
out.push({ arr: a.slice(), i, j: i - gap, gap, swapped: true, group: j % gap });
}
a[i] = temp;
out.push({ arr: a.slice(), i, j: i - gap, gap, swapped: true, group: j % gap });
}
}
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
setArr(steps[next].arr);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const cur = steps[idx] || { i: -1, j: -1, gap: Math.floor(arr.length / 2), swapped: false, group: 0 };
const isDone = idx === steps.length - 1 && steps.length > 1;
return (
<View style={styles.shCard}>
<View style={styles.shHeader}>
<Text style={styles.shTitle}>希尔排序 · 暮色金属风格</Text>
<Text style={styles.shSubtitle}>播放、步进、重置、随机与速度</Text>
</View>
<View style={styles.shControls}>
<TouchableOpacity style={[styles.shBtn, playing ? styles.shBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_SHELL.pause : ICON_BASE64_SHELL.play }} style={styles.shIcon} />
<Text style={styles.shBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.shBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_SHELL.step }} style={styles.shIcon} />
<Text style={styles.shBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.shBtn} onPress={resetSort} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_SHELL.reset }} style={styles.shIcon} />
<Text style={styles.shBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.shBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_SHELL.shuffle }} style={styles.shIcon} />
<Text style={styles.shBtnText}>随机</Text>
</TouchableOpacity>
<View style={styles.shChips}>
<View style={styles.shChip}><Text style={styles.shChipText}>当前间隔:{cur.gap}</Text></View>
<TouchableOpacity style={[styles.shChip, styles.shChipActive]} onPress={() => setSpeed(600)}><Text style={styles.shChipText}>1×</Text></TouchableOpacity>
<TouchableOpacity style={styles.shChip} onPress={() => setSpeed(300)}><Text style={styles.shChipText}>2×</Text></TouchableOpacity>
<TouchableOpacity style={styles.shChip} onPress={() => setSpeed(900)}><Text style={styles.shChipText}>0.5×</Text></TouchableOpacity>
</View>
</View>
<View style={styles.shBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const inCompare = i === cur.i || i === cur.j;
const swapped = inCompare && cur.swapped;
const inGroup = cur.gap > 0 && i % cur.gap === cur.group;
return (
<View key={i} style={[styles.shBar, { height: h }, inGroup ? styles.shBarGroup : null, inCompare ? styles.shBarHL : null, swapped ? styles.shBarSwap : null]}>
<Text style={styles.shBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.shFooterRow}>
<Text style={styles.shFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
<Text style={[styles.shFootText, isDone ? styles.shDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
</View>
</View>
);
};
const CountingSortVisualizer: React.FC = () => {
const [arr, setArr] = useState<number[]>([]);
const [steps, setSteps] = useState<{ arr: number[]; counts: number[]; phase: 'count' | 'prefix' | 'place'; curIndex?: number; curBucket?: number; placed?: number; min: number; range: number }[]>([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(600);
const [rangeN, setRangeN] = useState(12);
const timerRef = React.useRef<any>(null);
const maxBars = 18;
const genArray = () => {
const min = 10;
const a = Array.from({ length: maxBars }, () => min + Math.floor(Math.random() * rangeN));
setArr(a);
const s = buildSteps(a, min, rangeN);
setSteps(s);
setIdx(0);
setPlaying(false);
};
const buildSteps = (baseArr: number[], min: number, range: number) => {
const a = baseArr.slice();
const counts = Array.from({ length: range }, () => 0);
const out: { arr: number[]; counts: number[]; phase: 'count' | 'prefix' | 'place'; curIndex?: number; curBucket?: number; placed?: number; min: number; range: number }[] = [{ arr: a.slice(), counts: counts.slice(), phase: 'count', min, range }];
for (let i = 0; i < a.length; i++) {
const b = a[i] - min;
counts[b] += 1;
out.push({ arr: a.slice(), counts: counts.slice(), phase: 'count', curIndex: i, curBucket: b, min, range });
}
for (let i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
out.push({ arr: a.slice(), counts: counts.slice(), phase: 'prefix', curBucket: i, min, range });
}
const outArr = Array.from({ length: a.length }, () => 0);
for (let i = a.length - 1; i >= 0; i--) {
const b = a[i] - min;
const pos = counts[b] - 1;
outArr[pos] = a[i];
counts[b] -= 1;
out.push({ arr: outArr.slice(), counts: counts.slice(), phase: 'place', curIndex: i, curBucket: b, placed: pos + 1, min, range });
}
out.push({ arr: outArr.slice(), counts: counts.slice(), phase: 'place', min, range });
return out;
};
const stepOnce = () => {
setIdx(prev => {
const next = Math.min(prev + 1, steps.length - 1);
setArr(steps[next].arr);
if (next === steps.length - 1) setPlaying(false);
return next;
});
};
const togglePlay = () => setPlaying(p => !p);
const resetSort = () => { const s = buildSteps(arr.slice(), 10, rangeN); setSteps(s); setIdx(0); setPlaying(false); };
const shuffle = () => genArray();
const setRange = (n: number) => { setRangeN(n); const min = 10; const a = Array.from({ length: maxBars }, () => min + Math.floor(Math.random() * n)); setArr(a); const s = buildSteps(a, min, n); setSteps(s); setIdx(0); setPlaying(false); };
React.useEffect(() => { genArray(); }, []);
React.useEffect(() => {
if (playing) {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(stepOnce, speed);
} else {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
}, [playing, speed, steps]);
const maxVal = Math.max(...arr, 1);
const cur = steps[idx] || { phase: 'count', counts: Array.from({ length: rangeN }, () => 0), min: 10, range: rangeN };
const isDone = idx === steps.length - 1 && steps.length > 1;
return (
<View style={styles.ctCard}>
<View style={styles.ctHeader}>
<Text style={styles.ctTitle}>计数排序 · 薄荷风格</Text>
<Text style={styles.ctSubtitle}>播放、步进、重置、随机、范围切换</Text>
</View>
<View style={styles.ctControls}>
<TouchableOpacity style={[styles.ctBtn, playing ? styles.ctBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
<Image source={{ uri: playing ? ICON_BASE64_COUNT.pause : ICON_BASE64_COUNT.play }} style={styles.ctIcon} />
<Text style={styles.ctBtnText}>{playing ? '暂停' : '播放'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ctBtn} onPress={stepOnce} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_COUNT.step }} style={styles.ctIcon} />
<Text style={styles.ctBtnText}>步进</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ctBtn} onPress={resetSort} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_COUNT.reset }} style={styles.ctIcon} />
<Text style={styles.ctBtnText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ctBtn} onPress={shuffle} activeOpacity={0.85}>
<Image source={{ uri: ICON_BASE64_COUNT.shuffle }} style={styles.ctIcon} />
<Text style={styles.ctBtnText}>随机</Text>
</TouchableOpacity>
<View style={styles.ctChips}>
<TouchableOpacity style={[styles.ctChip, rangeN === 8 ? styles.ctChipActive : null]} onPress={() => setRange(8)}><Text style={styles.ctChipText}>8范围</Text></TouchableOpacity>
<TouchableOpacity style={[styles.ctChip, rangeN === 12 ? styles.ctChipActive : null]} onPress={() => setRange(12)}><Text style={styles.ctChipText}>12范围</Text></TouchableOpacity>
<TouchableOpacity style={[styles.ctChip, rangeN === 16 ? styles.ctChipActive : null]} onPress={() => setRange(16)}><Text style={styles.ctChipText}>16范围</Text></TouchableOpacity>
<TouchableOpacity style={styles.ctChip} onPress={() => setSpeed(900)}><Text style={styles.ctChipText}>0.5×</Text></TouchableOpacity>
<TouchableOpacity style={[styles.ctChip, styles.ctChipActive]} onPress={() => setSpeed(600)}><Text style={styles.ctChipText}>1×</Text></TouchableOpacity>
<TouchableOpacity style={styles.ctChip} onPress={() => setSpeed(300)}><Text style={styles.ctChipText}>2×</Text></TouchableOpacity>
</View>
</View>
<View style={styles.ctBars}>
{arr.map((v, i) => {
const h = Math.round((v / maxVal) * 160) + 20;
const inCount = cur.phase === 'count' && cur.curIndex === i;
const placed = cur.phase === 'place' && typeof cur.placed === 'number' && i < (cur.placed || 0);
return (
<View key={i} style={[styles.ctBar, { height: h }, inCount ? styles.ctBarHL : null, placed ? styles.ctBarPlaced : null]}>
<Text style={styles.ctBarText}>{v}</Text>
</View>
);
})}
</View>
<View style={styles.ctBuckets}>
{Array.from({ length: cur.range }, (_, b) => (
<View key={`ct-${b}`} style={[styles.ctBucket, cur.curBucket === b ? styles.ctBucketActive : null]}>
<Text style={styles.ctBucketLabel}>值 {b + cur.min}</Text>
<Text style={styles.ctBucketCount}>{cur.counts[b] || 0} 项</Text>
</View>
))}
</View>
<View style={styles.ctFooterRow}>
<Text style={styles.ctFootText}>阶段 {cur.phase === 'count' ? '计数' : cur.phase === 'prefix' ? '前缀和' : '放置'}</Text>
<Text style={[styles.ctFootText, isDone ? styles.ctDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
</View>
</View>
);
};
export default CalendarComponentApp;
这段React Native日历组件代码在架构设计原理上与鸿蒙系统的分布式能力有着深刻的共鸣。从组件抽象化角度来看,Icon组件通过符号映射机制实现了轻量级的图标系统,这种将视觉元素与逻辑标识分离的设计模式正是鸿蒙原子化服务思想的微观体现。每个图标都被封装成独立的功能单元,通过统一的接口规范对外提供服务,这种设计哲学与鸿蒙Ability组件的能力抽象化高度一致。
代码采用状态驱动的声明式编程范式,通过currentDate这一核心状态来管理整个界面的渲染逻辑。这种单向数据流的设计确保了界面与数据的一致性,所有日期计算、事件匹配和视觉呈现都基于这一状态派生而来。这种响应式架构与鸿蒙ArkUI的更新机制在原理层面完全相通,都是通过状态变化自动触发界面的重新组合。组件内部的状态变化会自动触发界面的重新渲染,实现了数据与UI的自动同步,这种机制与鸿蒙的状态管理在本质上相通。
在日期计算模块中,代码通过JavaScript Date对象封装了复杂的日历算法,包括月份天数计算、首日星期定位等核心功能。这种将复杂逻辑隐藏在简洁接口之后的设计模式,与鸿蒙系统服务的抽象化封装有着相同的设计理念。getDaysInMonth函数通过设置下个月的第0天来获取当月最后一天的巧妙算法,展现了与鸿蒙系统级API相似的设计智慧。

事件管理机制采用数据过滤模式,通过hasEvents函数实现日期与业务数据的动态关联。这种松耦合的数据绑定方式与鸿蒙的分布式数据管理能力在架构思想上高度契合,都是通过统一的标识符来实现不同数据源之间的关联映射。通过events数组与日期字符串的匹配,实现了业务数据与界面显示的松耦合关联,这种设计理念正是鸿蒙分布式数据管理能力的微观体现。
界面渲染采用分层架构设计,将日历拆分为头部导航、星期栏和日期网格三个独立的渲染模块。这种关注点分离的设计原则使得每个组件都可以独立开发和测试,与鸿蒙的弹性部署能力形成了完美的呼应。每个渲染函数都专注于特定的界面区域,这种模块化的思维方式正是构建大型复杂应用系统的关键所在。
交互设计方面,TouchableOpacity组件提供了原生的触控反馈体验,这种对用户体验细节的关注与鸿蒙的人机交互设计理念完全吻合。导航按钮的点击事件处理展现了完整的用户操作响应链路,从触控输入到状态变更再到界面更新,这一完整的交互闭环与鸿蒙的分布式交互机制在原理层面高度一致。整个组件的国际化处理也展现了与鸿蒙全球化支持相一致的设计思路。月份名称通过toLocaleString方法根据系统语言自动适配,这种动态本地化机制正是鸿蒙多语言架构的核心特征。
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

更多推荐

所有评论(0)