Files
breakpilot-lehrer/studio-v2/lib/spatial-ui/PerformanceContext.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

375 lines
11 KiB
TypeScript

'use client'
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'
/**
* Performance Context - Adaptive Quality System
*
* Monitors device capabilities and runtime performance to automatically
* adjust visual quality. This ensures smooth 60fps (or 120fps on capable devices)
* by degrading effects when needed.
*/
// =============================================================================
// TYPES
// =============================================================================
export type QualityLevel = 'high' | 'medium' | 'low' | 'minimal'
export interface PerformanceMetrics {
/** Frames per second (rolling average) */
fps: number
/** Dropped frames in last second */
droppedFrames: number
/** Device memory in GB (if available) */
deviceMemory: number | null
/** Number of logical CPU cores */
hardwareConcurrency: number
/** Whether device prefers reduced motion */
prefersReducedMotion: boolean
/** Whether device is on battery/power-save */
isLowPowerMode: boolean
/** Current quality level */
qualityLevel: QualityLevel
}
export interface QualitySettings {
/** Enable backdrop blur effects */
enableBlur: boolean
/** Blur intensity multiplier (0-1) */
blurIntensity: number
/** Enable shadow effects */
enableShadows: boolean
/** Shadow complexity (0-1) */
shadowComplexity: number
/** Enable parallax effects */
enableParallax: boolean
/** Parallax intensity multiplier (0-1) */
parallaxIntensity: number
/** Enable spring animations */
enableSpringAnimations: boolean
/** Animation duration multiplier (0.5-1.5) */
animationSpeed: number
/** Enable typewriter effects */
enableTypewriter: boolean
/** Max concurrent animations */
maxConcurrentAnimations: number
}
interface PerformanceContextType {
metrics: PerformanceMetrics
settings: QualitySettings
/** Force a specific quality level (null = auto) */
forceQuality: (level: QualityLevel | null) => void
/** Report that an animation started */
reportAnimationStart: () => void
/** Report that an animation ended */
reportAnimationEnd: () => void
/** Check if we can start another animation */
canStartAnimation: () => boolean
}
// =============================================================================
// DEFAULT VALUES
// =============================================================================
const DEFAULT_METRICS: PerformanceMetrics = {
fps: 60,
droppedFrames: 0,
deviceMemory: null,
hardwareConcurrency: 4,
prefersReducedMotion: false,
isLowPowerMode: false,
qualityLevel: 'high',
}
const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {
high: {
enableBlur: true,
blurIntensity: 1,
enableShadows: true,
shadowComplexity: 1,
enableParallax: true,
parallaxIntensity: 1,
enableSpringAnimations: true,
animationSpeed: 1,
enableTypewriter: true,
maxConcurrentAnimations: 10,
},
medium: {
enableBlur: true,
blurIntensity: 0.7,
enableShadows: true,
shadowComplexity: 0.7,
enableParallax: true,
parallaxIntensity: 0.5,
enableSpringAnimations: true,
animationSpeed: 0.9,
enableTypewriter: true,
maxConcurrentAnimations: 6,
},
low: {
enableBlur: false,
blurIntensity: 0,
enableShadows: true,
shadowComplexity: 0.4,
enableParallax: false,
parallaxIntensity: 0,
enableSpringAnimations: false,
animationSpeed: 0.7,
enableTypewriter: true,
maxConcurrentAnimations: 3,
},
minimal: {
enableBlur: false,
blurIntensity: 0,
enableShadows: false,
shadowComplexity: 0,
enableParallax: false,
parallaxIntensity: 0,
enableSpringAnimations: false,
animationSpeed: 0.5,
enableTypewriter: false,
maxConcurrentAnimations: 1,
},
}
// =============================================================================
// CONTEXT
// =============================================================================
const PerformanceContext = createContext<PerformanceContextType | null>(null)
// =============================================================================
// PROVIDER
// =============================================================================
export function PerformanceProvider({ children }: { children: React.ReactNode }) {
const [metrics, setMetrics] = useState<PerformanceMetrics>(DEFAULT_METRICS)
const [forcedQuality, setForcedQuality] = useState<QualityLevel | null>(null)
const [activeAnimations, setActiveAnimations] = useState(0)
const frameTimesRef = useRef<number[]>([])
const lastFrameTimeRef = useRef<number>(performance.now())
const rafIdRef = useRef<number | null>(null)
// Detect device capabilities on mount
useEffect(() => {
if (typeof window === 'undefined') return
const detectCapabilities = () => {
// Hardware concurrency
const cores = navigator.hardwareConcurrency || 4
// Device memory (Chrome only)
const memory = (navigator as any).deviceMemory || null
// Reduced motion preference
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
// Battery/power save mode (approximation)
let lowPower = false
if ('getBattery' in navigator) {
(navigator as any).getBattery?.().then((battery: any) => {
lowPower = battery.level < 0.2 && !battery.charging
setMetrics(prev => ({ ...prev, isLowPowerMode: lowPower }))
})
}
// Determine initial quality level
let initialQuality: QualityLevel = 'high'
if (reducedMotion) {
initialQuality = 'minimal'
} else if (cores <= 2 || (memory && memory <= 2)) {
initialQuality = 'low'
} else if (cores <= 4 || (memory && memory <= 4)) {
initialQuality = 'medium'
}
setMetrics(prev => ({
...prev,
hardwareConcurrency: cores,
deviceMemory: memory,
prefersReducedMotion: reducedMotion,
qualityLevel: initialQuality,
}))
}
detectCapabilities()
// Listen for reduced motion changes
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
const handleChange = (e: MediaQueryListEvent) => {
setMetrics(prev => ({
...prev,
prefersReducedMotion: e.matches,
qualityLevel: e.matches ? 'minimal' : prev.qualityLevel,
}))
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
// FPS monitoring loop
useEffect(() => {
if (typeof window === 'undefined') return
let frameCount = 0
let lastFpsUpdate = performance.now()
const measureFrame = (timestamp: number) => {
const delta = timestamp - lastFrameTimeRef.current
lastFrameTimeRef.current = timestamp
// Track frame times (keep last 60)
frameTimesRef.current.push(delta)
if (frameTimesRef.current.length > 60) {
frameTimesRef.current.shift()
}
frameCount++
// Update FPS every second
if (timestamp - lastFpsUpdate >= 1000) {
const avgFrameTime =
frameTimesRef.current.reduce((a, b) => a + b, 0) / frameTimesRef.current.length
const fps = Math.round(1000 / avgFrameTime)
// Count dropped frames (frames > 20ms = dropped at 60fps)
const dropped = frameTimesRef.current.filter(t => t > 20).length
setMetrics(prev => {
// Auto-adjust quality based on performance
let newQuality = prev.qualityLevel
if (!forcedQuality) {
if (dropped > 10 && prev.qualityLevel !== 'minimal') {
// Downgrade
const levels: QualityLevel[] = ['high', 'medium', 'low', 'minimal']
const currentIndex = levels.indexOf(prev.qualityLevel)
newQuality = levels[Math.min(currentIndex + 1, levels.length - 1)]
} else if (dropped === 0 && fps >= 58 && prev.qualityLevel !== 'high') {
// Consider upgrade (only if stable for a while)
// This is conservative - we don't want to oscillate
}
}
return {
...prev,
fps,
droppedFrames: dropped,
qualityLevel: forcedQuality || newQuality,
}
})
frameCount = 0
lastFpsUpdate = timestamp
}
rafIdRef.current = requestAnimationFrame(measureFrame)
}
rafIdRef.current = requestAnimationFrame(measureFrame)
return () => {
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current)
}
}
}, [forcedQuality])
// Get current settings based on quality level
const settings = QUALITY_PRESETS[forcedQuality || metrics.qualityLevel]
// Force quality level
const forceQuality = useCallback((level: QualityLevel | null) => {
setForcedQuality(level)
if (level) {
setMetrics(prev => ({ ...prev, qualityLevel: level }))
}
}, [])
// Animation tracking
const reportAnimationStart = useCallback(() => {
setActiveAnimations(prev => prev + 1)
}, [])
const reportAnimationEnd = useCallback(() => {
setActiveAnimations(prev => Math.max(0, prev - 1))
}, [])
const canStartAnimation = useCallback(() => {
return activeAnimations < settings.maxConcurrentAnimations
}, [activeAnimations, settings.maxConcurrentAnimations])
return (
<PerformanceContext.Provider
value={{
metrics,
settings,
forceQuality,
reportAnimationStart,
reportAnimationEnd,
canStartAnimation,
}}
>
{children}
</PerformanceContext.Provider>
)
}
// =============================================================================
// HOOKS
// =============================================================================
export function usePerformance() {
const context = useContext(PerformanceContext)
if (!context) {
// Return defaults if used outside provider
return {
metrics: DEFAULT_METRICS,
settings: QUALITY_PRESETS.high,
forceQuality: () => {},
reportAnimationStart: () => {},
reportAnimationEnd: () => {},
canStartAnimation: () => true,
}
}
return context
}
/**
* Hook to get adaptive blur value
*/
export function useAdaptiveBlur(baseBlur: number): number {
const { settings } = usePerformance()
if (!settings.enableBlur) return 0
return Math.round(baseBlur * settings.blurIntensity)
}
/**
* Hook to get adaptive shadow
*/
export function useAdaptiveShadow(baseShadow: string, fallbackShadow: string = 'none'): string {
const { settings } = usePerformance()
if (!settings.enableShadows) return fallbackShadow
// Could also reduce shadow complexity here
return baseShadow
}
/**
* Hook for reduced motion check
*/
export function usePrefersReducedMotion(): boolean {
const { metrics } = usePerformance()
return metrics.prefersReducedMotion
}
/**
* Hook to get animation duration with performance adjustment
*/
export function useAdaptiveAnimationDuration(baseDuration: number): number {
const { settings } = usePerformance()
return Math.round(baseDuration * settings.animationSpeed)
}