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>
This commit is contained in:
374
studio-v2/lib/spatial-ui/PerformanceContext.tsx
Normal file
374
studio-v2/lib/spatial-ui/PerformanceContext.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
'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)
|
||||
}
|
||||
Reference in New Issue
Block a user