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>
375 lines
11 KiB
TypeScript
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)
|
|
}
|