kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
530 lines (527 loc) • 20.1 kB
JavaScript
import { useRef, useCallback, useEffect } from 'react';
import 'pixi-filters';
import '../managers/ShaderResourceManager.js';
import '../filters/advancedBloomFilter.js';
import 'pixi.js';
import '../filters/blurFilter.js';
import '../filters/colorMatrixFilter.js';
import '../filters/dropShadowFilter.js';
import 'gsap';
import '../filters/tiltShiftFilter.js';
import { FilterFactory } from '../filters/FilterFactory.js';
import { RenderScheduler } from '../managers/RenderScheduler.js';
import { UpdateType } from '../managers/UpdateTypes.js';
const isDevelopment = typeof import.meta.env !== "undefined" && true ? false : false;
const FILTER_COORDINATION_EVENT = "kinetic-slider:filter-update";
const useFilters = ({ pixi, props, resourceManager }) => {
if (isDevelopment) {
console.log("[useFilters] Hook called with:", {
hasApp: !!pixi.app.current,
hasStage: !!pixi.app.current?.stage,
slidesCount: pixi.slides.current?.length,
textContainersCount: pixi.textContainers.current?.length,
hasResourceManager: !!resourceManager,
imageFilters: props.imageFilters,
textFilters: props.textFilters
});
}
const filterMapRef = useRef({});
const originalConfigsRef = useRef({ image: [], text: [] });
const filtersInitializedRef = useRef(false);
const filtersActiveRef = useRef(false);
useRef(null);
useRef(true);
const applyFiltersToTargetRef = useRef(null);
const applyFiltersToObjectsRef = useRef(null);
useRef({
pendingFilters: [],
pendingObjects: []
});
const batchQueueRef = useRef([]);
const batchConfigRef = useRef({
bufferMs: 16,
// One frame at 60fps - will be adjusted dynamically
maxBatchSize: 10
// Will be adjusted dynamically
});
const batchTimeoutRef = useRef(null);
const performanceMetricsRef = useRef({
lastProcessTime: 0,
averageProcessTime: 0,
updateCount: 0,
bufferAdjustmentCounter: 0,
totalProcessTime: 0
});
useRef(/* @__PURE__ */ new Map());
const processBatchQueueRef = useRef(() => {
const start = performance.now();
const queue = [...batchQueueRef.current];
batchQueueRef.current = [];
if (queue.length === 0) return;
if (isDevelopment) {
console.log(`[useFilters] Processing batch of ${queue.length} filter updates`);
}
const updates = {};
const priorityOrder = {
"low": 0,
"normal": 1,
"high": 2,
"critical": 3
};
queue.sort((a, b) => {
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
for (const update of queue) {
if (updates[update.filterId] && priorityOrder[updates[update.filterId].priority] >= priorityOrder[update.priority]) {
continue;
}
updates[update.filterId] = update;
}
Object.values(updates).forEach((update) => {
const { filterId, changes } = update;
const [targetId, filterIndex] = filterId.split("-");
if (!filterMapRef.current[targetId]) {
if (isDevelopment) {
console.warn(`[useFilters] Filter target not found: ${targetId}`);
}
return;
}
const filterData = filterMapRef.current[targetId].filters[Number(filterIndex)];
if (!filterData) {
if (isDevelopment) {
console.warn(`[useFilters] Filter not found: ${filterId}`);
}
return;
}
if (changes.intensity !== void 0) {
filterData.updateIntensity(changes.intensity);
if (isDevelopment) {
console.log(`[useFilters] Set filter ${filterId} to ${changes.enabled ? "active" : "inactive"} with intensity ${changes.intensity}`);
}
}
if (changes.enabled !== void 0) {
filterData.instance.enabled = changes.enabled;
}
});
const end = performance.now();
const processingTime = end - start;
const metrics = performanceMetricsRef.current;
metrics.lastProcessTime = processingTime;
metrics.totalProcessTime += processingTime;
metrics.updateCount += queue.length;
metrics.averageProcessTime = metrics.totalProcessTime / metrics.updateCount;
metrics.bufferAdjustmentCounter++;
if (metrics.bufferAdjustmentCounter >= 10) {
metrics.bufferAdjustmentCounter = 0;
if (metrics.averageProcessTime > 4) {
batchConfigRef.current.bufferMs = Math.min(50, batchConfigRef.current.bufferMs + 4);
if (isDevelopment) {
console.log(`[useFilters] Increased batch buffer to ${batchConfigRef.current.bufferMs}ms due to slow processing`);
}
} else if (metrics.averageProcessTime < 2 && batchConfigRef.current.bufferMs > 16) {
batchConfigRef.current.bufferMs = Math.max(16, batchConfigRef.current.bufferMs - 2);
if (isDevelopment) {
console.log(`[useFilters] Decreased batch buffer to ${batchConfigRef.current.bufferMs}ms due to fast processing`);
}
}
}
if (pixi.app.current) {
pixi.app.current.render();
}
});
const scheduleNextBatchRef = useRef(() => {
if (batchTimeoutRef.current !== null) {
window.clearTimeout(batchTimeoutRef.current);
batchTimeoutRef.current = null;
}
if (batchQueueRef.current.length > 0) {
if (batchQueueRef.current.some((update) => update.priority === "critical")) {
processBatchQueueRef.current();
return;
}
const highPriorityCount = batchQueueRef.current.filter((update) => update.priority === "high").length;
if (highPriorityCount > 0 && (highPriorityCount >= 3 || batchQueueRef.current.length >= batchConfigRef.current.maxBatchSize)) {
processBatchQueueRef.current();
return;
}
batchTimeoutRef.current = window.setTimeout(() => {
processBatchQueueRef.current();
batchTimeoutRef.current = null;
}, batchConfigRef.current.bufferMs);
}
});
const processFilterConfigs = (filterConfig) => {
if (!filterConfig) {
return [];
}
const configs = Array.isArray(filterConfig) ? filterConfig : [filterConfig];
return configs.filter((config) => {
if (!config || !config.type) {
if (isDevelopment) {
console.warn("Invalid filter config - missing type:", config);
}
return false;
}
return true;
}).map((config) => {
return {
type: config.type,
// Set default values only if not defined in config
enabled: config.enabled ?? true,
// Default to enabled if not explicitly disabled
intensity: config.intensity ?? 1,
// Default intensity 1 if not defined
...config
// Keep all other properties from config (might override defaults)
};
});
};
const adaptFilterConfig = (config) => {
const { type, enabled = true, intensity = 1, options = {}, ...rest } = config;
const filterProperties = { ...rest, ...options };
return {
type,
enabled: enabled !== false,
intensity: intensity || 1,
...filterProperties
};
};
const applyFiltersToObjects = useCallback(async (targets, configs, baseId) => {
if (!configs || configs.length === 0 || !targets || targets.length === 0) {
if (isDevelopment) {
console.log(`No filter configs or targets provided for ${baseId}`);
}
return;
}
if (!applyFiltersToTargetRef.current) {
console.error("[useFilters] applyFiltersToTargetRef.current is not defined");
return;
}
const processedConfigs = configs.map((c) => adaptFilterConfig(c));
const applyPromises = targets.map((target, index) => {
const targetId = `${baseId}-${index}`;
return applyFiltersToTargetRef.current(target, processedConfigs, targetId);
});
await Promise.all(applyPromises);
}, []);
useEffect(() => {
applyFiltersToObjectsRef.current = applyFiltersToObjects;
}, [applyFiltersToObjects]);
const updateFilterIntensities = useCallback((active, force = false) => {
if (!filtersInitializedRef.current) {
if (isDevelopment) {
console.log("[useFilters] Filters not initialized, skipping intensity update");
}
return;
}
if (isDevelopment) {
console.log(`[useFilters] Setting filter intensity to ${active ? "active" : "inactive"}`);
}
const batchUpdates = [];
Object.entries(filterMapRef.current).forEach(([targetId, filterData]) => {
try {
const typedFilterData = filterData;
if (!typedFilterData.target) return;
const { target, filters } = typedFilterData;
if (!target || !filters || filters.length === 0) return;
if (!force && filters.some((f) => f.instance.enabled === active)) return;
filters.forEach((f) => {
f.instance.enabled = active;
if (active) {
if (f.initialIntensity !== void 0) {
f.updateIntensity(f.initialIntensity);
}
} else {
f.updateIntensity(0);
}
});
if (isDevelopment) {
console.log(`[useFilters] Set filter ${targetId} to ${active ? "active" : "inactive"} with intensity ${active ? "initial" : 0}`);
}
batchUpdates.push({
filterId: targetId,
changes: {
enabled: active
},
timestamp: performance.now(),
priority: force ? "critical" : "normal"
});
} catch (error) {
if (isDevelopment) {
console.error(`[useFilters] Error updating filter ${targetId}:`, error);
}
}
});
if (batchUpdates.length > 0) {
if (isDevelopment) {
console.log(`[useFilters] Processing batch of ${batchUpdates.length} filter updates`);
}
batchQueueRef.current.push(...batchUpdates);
if (force) {
processBatchQueueRef.current();
} else {
scheduleNextBatchRef.current();
}
}
filtersActiveRef.current = active;
}, [filtersInitializedRef.current]);
const resetAllFilters = useCallback(() => {
if (!filtersInitializedRef.current) {
if (isDevelopment) {
console.log("[useFilters] Filters not initialized, skipping reset");
}
return;
}
if (isDevelopment) {
console.log("[useFilters] Resetting all filters to default state");
}
const updates = [];
const now = performance.now();
Object.entries(filterMapRef.current).forEach(([targetId, filterData]) => {
const typedFilterData = filterData;
typedFilterData.filters.forEach((filter, index) => {
const filterId = `${targetId}-${index}`;
updates.push({
filterId,
changes: {
intensity: filter.initialIntensity,
enabled: true
},
timestamp: now,
priority: "normal"
});
if (isDevelopment) {
console.log(`[useFilters] Reset filter ${filterId} to default state`);
}
});
});
batchQueueRef.current.push(...updates);
scheduleNextBatchRef.current();
Object.entries(filterMapRef.current).forEach(([_, filterData]) => {
const typedFilterData = filterData;
typedFilterData.filters.forEach((filter) => {
filter.reset();
});
});
}, []);
const activateFilterEffects = useCallback(() => {
try {
if (filtersInitializedRef.current) {
updateFilterIntensities(true, true);
return;
}
if (isDevelopment) {
console.log("[useFilters] Filters not initialized, initializing now before activation");
}
if (!pixi.app.current || !pixi.slides.current || !pixi.slides.current.length) {
console.log("[useFilters] Missing required objects for filter initialization");
return;
}
const imageFilterConfigs = processFilterConfigs(props.imageFilters || []);
const textFilterConfigs = processFilterConfigs(props.textFilters || []);
originalConfigsRef.current = {
image: [...imageFilterConfigs],
text: [...textFilterConfigs]
};
console.log(`[useFilters] Initializing filters: ${imageFilterConfigs.length} image filters, ${textFilterConfigs.length} text filters`);
if (pixi.slides.current && pixi.slides.current.length) {
pixi.slides.current.forEach((slide, index) => {
if (!slide) return;
const slideName = `slide-${index}`;
console.log(`[useFilters] Creating filters for ${slideName}`);
filterMapRef.current[slideName] = {
target: slide,
filters: []
};
imageFilterConfigs.forEach(async (config, filterIndex) => {
try {
const filterResult = await FilterFactory.createFilterAsync(config);
const filter = filterResult.filter;
filter.enabled = true;
if (!slide.filters) {
slide.filters = [filter];
} else if (Array.isArray(slide.filters)) {
slide.filters.push(filter);
}
filterMapRef.current[slideName].filters.push({
instance: filter,
updateIntensity: (intensity) => {
console.log(`[useFilters] Updated ${slideName} filter ${filterIndex} intensity to ${intensity}`);
filterResult.updateIntensity(intensity);
},
reset: () => {
console.log(`[useFilters] Reset ${slideName} filter ${filterIndex}`);
filterResult.reset();
},
initialIntensity: config.intensity || 1
});
console.log(`[useFilters] Created filter for ${slideName} with type ${config.type}`);
} catch (err) {
console.error(`[useFilters] Error creating filter for ${slideName}:`, err);
}
});
});
}
if (pixi.textContainers.current && pixi.textContainers.current.length) {
pixi.textContainers.current.forEach((container, index) => {
if (!container) return;
const containerName = `text-container-${index}`;
console.log(`[useFilters] Creating filters for ${containerName}`);
filterMapRef.current[containerName] = {
target: container,
filters: []
};
textFilterConfigs.forEach(async (config, filterIndex) => {
try {
const filterResult = await FilterFactory.createFilterAsync(config);
const filter = filterResult.filter;
filter.enabled = true;
if (!container.filters) {
container.filters = [filter];
} else if (Array.isArray(container.filters)) {
container.filters.push(filter);
}
filterMapRef.current[containerName].filters.push({
instance: filter,
updateIntensity: (intensity) => {
console.log(`[useFilters] Updated ${containerName} filter ${filterIndex} intensity to ${intensity}`);
filterResult.updateIntensity(intensity);
},
reset: () => {
console.log(`[useFilters] Reset ${containerName} filter ${filterIndex}`);
filterResult.reset();
},
initialIntensity: config.intensity || 1
});
console.log(`[useFilters] Created filter for ${containerName} with type ${config.type}`);
} catch (err) {
console.error(`[useFilters] Error creating filter for ${containerName}:`, err);
}
});
});
}
filtersInitializedRef.current = true;
filtersActiveRef.current = true;
console.log("[useFilters] Filters initialized and activated");
} catch (error) {
if (isDevelopment) {
console.error("Error activating filter effects:", error);
}
}
}, [pixi.app, pixi.slides, pixi.textContainers, props.imageFilters, props.textFilters, updateFilterIntensities]);
const handleFilterCoordinationEvent = useCallback((event) => {
console.log("[useFilters] Event handler implemented");
}, []);
useEffect(() => {
window.addEventListener(FILTER_COORDINATION_EVENT, handleFilterCoordinationEvent);
return () => {
window.removeEventListener(FILTER_COORDINATION_EVENT, handleFilterCoordinationEvent);
};
}, [handleFilterCoordinationEvent]);
applyFiltersToTargetRef.current = async (target, configs, id) => {
console.warn(`[useFilters] Dummy applyFiltersToTarget called for ${id} - real implementation not yet available`);
return Promise.resolve();
};
useEffect(() => {
const scheduler = RenderScheduler.getInstance();
const scheduledProcessBatch = () => {
processBatchQueueRef.current();
};
const scheduleBatchProcessing = () => {
scheduler.scheduleTypedUpdate(
"useFilters",
UpdateType.FILTER_UPDATE,
scheduledProcessBatch
);
};
scheduleNextBatchRef.current = () => {
if (batchQueueRef.current.some((update) => update.priority === "critical")) {
if (batchTimeoutRef.current !== null) {
window.clearTimeout(batchTimeoutRef.current);
batchTimeoutRef.current = null;
}
batchTimeoutRef.current = window.setTimeout(() => {
processBatchQueueRef.current();
batchTimeoutRef.current = null;
}, 0);
return;
}
if (batchQueueRef.current.some((update) => update.priority === "high")) {
scheduleBatchProcessing();
return;
}
if (batchTimeoutRef.current !== null) {
window.clearTimeout(batchTimeoutRef.current);
batchTimeoutRef.current = null;
}
batchTimeoutRef.current = window.setTimeout(() => {
scheduleBatchProcessing();
batchTimeoutRef.current = null;
}, batchConfigRef.current.bufferMs);
};
return () => {
if (batchTimeoutRef.current !== null) {
window.clearTimeout(batchTimeoutRef.current);
batchTimeoutRef.current = null;
}
scheduler.cancelTypedUpdate("useFilters", UpdateType.FILTER_UPDATE);
};
}, []);
useEffect(() => {
return () => {
batchQueueRef.current = [];
if (batchTimeoutRef.current !== null) {
window.clearTimeout(batchTimeoutRef.current);
batchTimeoutRef.current = null;
}
if (filtersInitializedRef.current) {
Object.entries(filterMapRef.current).forEach(([_, filterData]) => {
const typedFilterData = filterData;
typedFilterData.filters.forEach((filter) => {
try {
filter.instance.enabled = false;
filter.reset();
} catch (err) {
if (isDevelopment) {
console.error("[useFilters] Error cleaning up filter:", err);
}
}
});
});
}
};
}, []);
useEffect(() => {
if (typeof window === "undefined") return;
const handleFilterCoordinationEvent2 = (event) => {
const customEvent = event;
const { type, intensity, priority } = customEvent.detail;
if (isDevelopment) {
console.log(`[useFilters] Received filter coordination event: ${type} = ${intensity}`);
}
if (type === "background-displacement" || type === "cursor-displacement") {
if (intensity === 0) {
updateFilterIntensities(false, priority === "critical");
} else {
updateFilterIntensities(true, priority === "critical");
}
}
};
window.addEventListener(FILTER_COORDINATION_EVENT, handleFilterCoordinationEvent2);
return () => {
window.removeEventListener(FILTER_COORDINATION_EVENT, handleFilterCoordinationEvent2);
};
}, [updateFilterIntensities]);
return {
updateFilterIntensities,
resetAllFilters,
activateFilterEffects,
isInitialized: filtersInitializedRef.current,
isActive: filtersActiveRef.current,
setFiltersActive: (active) => {
filtersActiveRef.current = active;
}
};
};
export { useFilters };
//# sourceMappingURL=useFilters.js.map