第55节:无障碍设计 - 3D内容的可访问

概述

在3D应用开发中,无障碍设计(Accessibility)常常被忽视,但对于确保所有用户(包括残障人士)都能访问和使用3D内容至关重要。本节将深入探讨如何为Three.js应用实现全面的无障碍支持,让3D体验对每个人都更加友好。

3D无障碍设计架构:
3D无障碍设计 视觉无障碍 听觉无障碍 运动无障碍 认知无障碍 屏幕阅读器支持 高对比度模式 颜色盲友好 音频描述 声音反馈 字幕支持 键盘导航 语音控制 简化交互 清晰导航 一致布局 渐进披露 平等访问 多感官体验 灵活操作 易于理解

核心无障碍原则

WCAG 2.1标准在3D环境中的应用

原则 3D应用实现 技术要点
可感知性 多感官反馈、文本替代 屏幕阅读器、音频提示、高对比度
可操作性 多种交互方式 键盘导航、语音控制、简化操作
可理解性 直观的3D界面 清晰标识、一致交互、错误预防
健壮性 跨设备兼容 标准API、渐进增强、兼容性

3D特定无障碍挑战

  1. 空间导航

    • 3D空间中的方向感知
    • 对象关系和层级表达
    • 视点切换和场景理解
  2. 交互复杂性

    • 多维度的操作控制
    • 精确的目标选择
    • 复杂的操作序列
  3. 信息密度

    • 大量视觉信息同时呈现
    • 深度和遮挡关系
    • 动态变化的场景内容

完整无障碍实现

1. 屏幕阅读器支持系统

javascript 复制代码
// ScreenReaderSupport.js
class ScreenReaderSupport {
    constructor() {
        this.ariaLiveRegion = null;
        this.currentFocus = null;
        this.objectDescriptions = new Map();
        this.init();
    }

    init() {
        this.createAriaLiveRegion();
        this.setupFocusManagement();
        this.setupKeyboardNavigation();
    }

    // 创建ARIA实时区域
    createAriaLiveRegion() {
        this.ariaLiveRegion = document.createElement('div');
        this.ariaLiveRegion.setAttribute('aria-live', 'polite');
        this.ariaLiveRegion.setAttribute('aria-atomic', 'true');
        this.ariaLiveRegion.setAttribute('class', 'sr-only');
        this.ariaLiveRegion.style.cssText = `
            position: absolute;
            left: -10000px;
            width: 1px;
            height: 1px;
            overflow: hidden;
        `;
        document.body.appendChild(this.ariaLiveRegion);
    }

    // 设置焦点管理
    setupFocusManagement() {
        // 跟踪焦点变化
        document.addEventListener('focusin', (event) => {
            this.handleFocusChange(event.target);
        });

        // 为3D对象创建虚拟焦点
        this.setupVirtualFocus();
    }

    // 设置虚拟焦点系统
    setupVirtualFocus() {
        // 创建不可见的焦点陷阱元素
        this.focusTraps = new Map();
        
        // 监听键盘事件进行3D导航
        document.addEventListener('keydown', (event) => {
            this.handle3DNavigation(event);
        });
    }

    // 为3D对象注册描述
    registerObject(object, description, options = {}) {
        const objectInfo = {
            description: description,
            role: options.role || 'group',
            label: options.label || '',
            actions: options.actions || [],
            position: object.position.clone(),
            priority: options.priority || 0
        };

        this.objectDescriptions.set(object.uuid, objectInfo);

        // 为对象添加ARIA属性
        this.enhanceObjectWithARIA(object, objectInfo);
    }

    // 为对象增强ARIA属性
    enhanceObjectWithARIA(object, objectInfo) {
        object.userData.ariaLabel = objectInfo.label || objectInfo.description;
        object.userData.ariaRole = objectInfo.role;
        object.userData.accessibleActions = objectInfo.actions;
        
        // 创建虚拟焦点元素
        this.createVirtualFocusElement(object, objectInfo);
    }

    // 创建虚拟焦点元素
    createVirtualFocusElement(object, objectInfo) {
        const focusElement = document.createElement('div');
        focusElement.setAttribute('role', objectInfo.role);
        focusElement.setAttribute('aria-label', objectInfo.description);
        focusElement.setAttribute('tabindex', '0');
        focusElement.setAttribute('data-object-id', object.uuid);
        focusElement.className = 'virtual-focus-element sr-only';

        focusElement.style.cssText = `
            position: absolute;
            left: -10000px;
            width: 1px;
            height: 1px;
            overflow: hidden;
        `;

        // 添加事件监听
        focusElement.addEventListener('focus', () => {
            this.onObjectFocus(object);
        });

        focusElement.addEventListener('keydown', (event) => {
            this.onVirtualFocusKeydown(event, object);
        });

        document.body.appendChild(focusElement);
        this.focusTraps.set(object.uuid, focusElement);
    }

    // 对象获得焦点
    onObjectFocus(object) {
        this.currentFocus = object;
        const objectInfo = this.objectDescriptions.get(object.uuid);
        
        if (objectInfo) {
            this.announce(`${objectInfo.description} 已聚焦。按空格键选择,方向键导航。`);
            
            // 视觉反馈:高亮显示
            this.highlightObject(object);
        }
    }

    // 虚拟焦点键盘事件
    onVirtualFocusKeydown(event, object) {
        const objectInfo = this.objectDescriptions.get(object.uuid);
        
        switch (event.key) {
            case ' ':
            case 'Enter':
                event.preventDefault();
                this.activateObject(object);
                break;
                
            case 'ArrowUp':
            case 'ArrowDown':
            case 'ArrowLeft':
            case 'ArrowRight':
                event.preventDefault();
                this.navigateToAdjacentObject(object, event.key);
                break;
                
            case 'Escape':
                this.exitObjectFocus();
                break;
        }
    }

    // 激活对象
    activateObject(object) {
        const objectInfo = this.objectDescriptions.get(object.uuid);
        if (objectInfo && objectInfo.actions.length > 0) {
            this.announce(`已选择 ${objectInfo.description}。可用操作:${objectInfo.actions.join(',')}`);
            
            // 执行主要操作或显示操作菜单
            this.showObjectActions(object);
        } else {
            this.announce(`${objectInfo.description} 已激活`);
        }
    }

    // 导航到相邻对象
    navigateToAdjacentObject(currentObject, direction) {
        const adjacentObject = this.findAdjacentObject(currentObject, direction);
        if (adjacentObject) {
            const focusElement = this.focusTraps.get(adjacentObject.uuid);
            if (focusElement) {
                focusElement.focus();
            }
        } else {
            this.announce('没有可导航的对象');
        }
    }

    // 查找相邻对象
    findAdjacentObject(currentObject, direction) {
        const currentPos = currentObject.position;
        let candidates = [];
        
        // 收集所有可聚焦对象
        for (const [uuid, objectInfo] of this.objectDescriptions) {
            const object = this.findObjectByUUID(uuid);
            if (object && object !== currentObject) {
                const distance = object.position.distanceTo(currentPos);
                const directionVector = new THREE.Vector3()
                    .subVectors(object.position, currentPos)
                    .normalize();
                
                candidates.push({
                    object: object,
                    distance: distance,
                    direction: directionVector
                });
            }
        }
        
        // 根据方向筛选
        candidates = candidates.filter(candidate => {
            switch (direction) {
                case 'ArrowUp':
                    return candidate.direction.y > 0.5;
                case 'ArrowDown':
                    return candidate.direction.y < -0.5;
                case 'ArrowLeft':
                    return candidate.direction.x < -0.5;
                case 'ArrowRight':
                    return candidate.direction.x > 0.5;
                default:
                    return true;
            }
        });
        
        // 返回最近的对象
        if (candidates.length > 0) {
            candidates.sort((a, b) => a.distance - b.distance);
            return candidates[0].object;
        }
        
        return null;
    }

    // 通过UUID查找对象
    findObjectByUUID(uuid) {
        // 在实际应用中,需要维护对象引用
        for (const [storedUUID, objectInfo] of this.objectDescriptions) {
            if (storedUUID === uuid) {
                // 这里需要根据实际场景查找对象
                return null; // 实际实现中返回真实对象
            }
        }
        return null;
    }

    // 高亮对象
    highlightObject(object) {
        // 移除之前的高亮
        this.clearHighlights();
        
        // 添加高亮效果
        object.userData.originalMaterial = object.material;
        const highlightMaterial = new THREE.MeshBasicMaterial({
            color: 0xffff00,
            transparent: true,
            opacity: 0.3
        });
        object.material = highlightMaterial;
        
        // 添加轮廓效果
        this.addOutlineEffect(object);
    }

    // 添加轮廓效果
    addOutlineEffect(object) {
        // 使用后期处理或复制网格添加轮廓
        // 简化实现:改变材质颜色
        if (object.material && object.material.emissive) {
            object.material.emissive.set(0xffff00);
        }
    }

    // 清除高亮
    clearHighlights() {
        for (const [uuid, objectInfo] of this.objectDescriptions) {
            const object = this.findObjectByUUID(uuid);
            if (object && object.userData.originalMaterial) {
                object.material = object.userData.originalMaterial;
                delete object.userData.originalMaterial;
            }
        }
    }

    // 宣布消息
    announce(message, priority = 'polite') {
        if (this.ariaLiveRegion) {
            this.ariaLiveRegion.setAttribute('aria-live', priority);
            this.ariaLiveRegion.textContent = message;
            
            // 清除内容以便重复播报相同消息
            setTimeout(() => {
                this.ariaLiveRegion.textContent = '';
            }, 1000);
        }
        
        // 同时输出到控制台用于调试
        console.log(`[Screen Reader] ${message}`);
    }

    // 处理3D导航
    handle3DNavigation(event) {
        // 全局键盘导航处理
        if (event.altKey && event.key === 'Tab') {
            event.preventDefault();
            this.cycleFocusThroughObjects(event.shiftKey);
        }
    }

    // 循环遍历对象焦点
    cycleFocusThroughObjects(reverse = false) {
        const focusableElements = Array.from(this.focusTraps.values());
        if (focusableElements.length === 0) return;
        
        const currentIndex = focusableElements.findIndex(el => 
            el === document.activeElement
        );
        
        let nextIndex;
        if (reverse) {
            nextIndex = currentIndex <= 0 ? focusableElements.length - 1 : currentIndex - 1;
        } else {
            nextIndex = currentIndex >= focusableElements.length - 1 ? 0 : currentIndex + 1;
        }
        
        focusableElements[nextIndex].focus();
    }

    // 场景变化通知
    onSceneChange(description) {
        this.announce(`场景已更新:${description}`, 'assertive');
    }

    // 对象状态变化通知
    onObjectStateChange(object, newState) {
        const objectInfo = this.objectDescriptions.get(object.uuid);
        if (objectInfo) {
            this.announce(`${objectInfo.description} 状态已变为:${newState}`);
        }
    }

    // 清理资源
    dispose() {
        if (this.ariaLiveRegion) {
            this.ariaLiveRegion.remove();
        }
        
        for (const focusElement of this.focusTraps.values()) {
            focusElement.remove();
        }
        
        this.focusTraps.clear();
        this.objectDescriptions.clear();
    }
}

2. 高对比度和色盲友好模式

javascript 复制代码
// ColorAccessibility.js
class ColorAccessibility {
    constructor(renderer, scene) {
        this.renderer = renderer;
        this.scene = scene;
        this.originalMaterials = new Map();
        this.currentMode = 'default';
        
        this.modes = {
            default: { contrast: 1, adjustments: {} },
            highContrast: { contrast: 1.5, adjustments: { saturation: 1.2 } },
            protanopia: { 
                contrast: 1.3, 
                adjustments: { 
                    colorMatrix: this.getProtanopiaMatrix() 
                } 
            },
            deuteranopia: { 
                contrast: 1.3, 
                adjustments: { 
                    colorMatrix: this.getDeuteranopiaMatrix() 
                } 
            },
            tritanopia: { 
                contrast: 1.3, 
                adjustments: { 
                    colorMatrix: this.getTritanopiaMatrix() 
                } 
            },
            grayscale: { 
                contrast: 1.5, 
                adjustments: { 
                    colorMatrix: this.getGrayscaleMatrix() 
                } 
            }
        };
        
        this.init();
    }

    init() {
        this.setupColorModeSwitcher();
        this.storeOriginalMaterials();
    }

    // 存储原始材质
    storeOriginalMaterials() {
        this.scene.traverse((object) => {
            if (object.isMesh && object.material) {
                this.originalMaterials.set(object.uuid, {
                    material: object.material,
                    originalColor: object.material.color?.clone(),
                    originalEmissive: object.material.emissive?.clone()
                });
            }
        });
    }

    // 设置颜色模式切换器
    setupColorModeSwitcher() {
        // 创建模式切换UI
        this.createModeSelector();
        
        // 监听系统偏好
        this.detectSystemPreferences();
    }

    // 创建模式选择器
    createModeSelector() {
        const container = document.createElement('div');
        container.className = 'color-accessibility-panel';
        container.innerHTML = `
            <fieldset>
                <legend>颜色辅助模式</legend>
                <label>
                    <input type="radio" name="colorMode" value="default" checked>
                    默认模式
                </label>
                <label>
                    <input type="radio" name="colorMode" value="highContrast">
                    高对比度
                </label>
                <label>
                    <input type="radio" name="colorMode" value="protanopia">
                    红色盲模拟
                </label>
                <label>
                    <input type="radio" name="colorMode" value="deuteranopia">
                    绿色盲模拟
                </label>
                <label>
                    <input type="radio" name="colorMode" value="tritanopia">
                    蓝色盲模拟
                </label>
                <label>
                    <input type="radio" name="colorMode" value="grayscale">
                    灰度模式
                </label>
            </fieldset>
        `;
        
        container.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 1000;
            font-family: Arial, sans-serif;
        `;
        
        document.body.appendChild(container);
        
        // 添加事件监听
        const radios = container.querySelectorAll('input[type="radio"]');
        radios.forEach(radio => {
            radio.addEventListener('change', (event) => {
                this.setMode(event.target.value);
            });
        });
    }

    // 检测系统偏好
    detectSystemPreferences() {
        // 检测 prefers-contrast
        if (window.matchMedia('(prefers-contrast: high)').matches) {
            this.setMode('highContrast');
        }
        
        // 检测 prefers-color-scheme
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
            this.adjustForDarkMode();
        }
        
        // 持续监听系统偏好变化
        window.matchMedia('(prefers-contrast: high)').addEventListener('change', (event) => {
            if (event.matches) {
                this.setMode('highContrast');
            }
        });
    }

    // 设置颜色模式
    setMode(modeName) {
        if (!this.modes[modeName]) {
            console.warn(`未知的颜色模式: ${modeName}`);
            return;
        }
        
        this.currentMode = modeName;
        const modeConfig = this.modes[modeName];
        
        // 应用颜色调整
        this.applyColorAdjustments(modeConfig);
        
        // 更新UI反馈
        this.updateModeIndicator(modeName);
        
        // 保存用户偏好
        this.saveUserPreference(modeName);
    }

    // 应用颜色调整
    applyColorAdjustments(config) {
        this.scene.traverse((object) => {
            if (object.isMesh && object.material) {
                const original = this.originalMaterials.get(object.uuid);
                if (!original) return;
                
                // 根据模式调整材质
                this.adjustMaterialForMode(object.material, original, config);
            }
        });
        
        // 应用后期处理效果
        this.applyPostProcessingEffects(config);
    }

    // 为模式调整材质
    adjustMaterialForMode(material, original, config) {
        if (config.adjustments.colorMatrix) {
            // 应用颜色矩阵
            this.applyColorMatrix(material, config.adjustments.colorMatrix);
        } else {
            // 恢复原始颜色
            if (material.color && original.originalColor) {
                material.color.copy(original.originalColor);
            }
        }
        
        // 调整对比度
        if (config.contrast !== 1) {
            this.adjustContrast(material, config.contrast);
        }
        
        // 调整饱和度
        if (config.adjustments.saturation) {
            this.adjustSaturation(material, config.adjustments.saturation);
        }
        
        material.needsUpdate = true;
    }

    // 应用颜色矩阵
    applyColorMatrix(material, colorMatrix) {
        // 创建着色器材质或使用后期处理
        // 这里简化实现,实际应用中可能需要更复杂的处理
        if (material.isShaderMaterial) {
            material.uniforms.colorMatrix.value = colorMatrix;
        }
    }

    // 调整对比度
    adjustContrast(material, contrast) {
        if (material.color) {
            const original = this.originalMaterials.get(material.uuid);
            if (original && original.originalColor) {
                // 简化对比度调整
                material.color.r = this.applyContrast(original.originalColor.r, contrast);
                material.color.g = this.applyContrast(original.originalColor.g, contrast);
                material.color.b = this.applyContrast(original.originalColor.b, contrast);
            }
        }
    }

    // 应用对比度计算
    applyContrast(value, contrast) {
        return ((value - 0.5) * contrast) + 0.5;
    }

    // 调整饱和度
    adjustSaturation(material, saturation) {
        if (material.color) {
            const gray = material.color.r * 0.299 + material.color.g * 0.587 + material.color.b * 0.114;
            material.color.r = gray + (material.color.r - gray) * saturation;
            material.color.g = gray + (material.color.g - gray) * saturation;
            material.color.b = gray + (material.color.b - gray) * saturation;
        }
    }

    // 获取色盲模拟矩阵
    getProtanopiaMatrix() {
        // 红色盲模拟矩阵
        return [
            0.567, 0.433, 0, 0,
            0.558, 0.442, 0, 0,
            0, 0.242, 0.758, 0,
            0, 0, 0, 1
        ];
    }

    getDeuteranopiaMatrix() {
        // 绿色盲模拟矩阵
        return [
            0.625, 0.375, 0, 0,
            0.7, 0.3, 0, 0,
            0, 0.3, 0.7, 0,
            0, 0, 0, 1
        ];
    }

    getTritanopiaMatrix() {
        // 蓝色盲模拟矩阵
        return [
            0.95, 0.05, 0, 0,
            0, 0.433, 0.567, 0,
            0, 0.475, 0.525, 0,
            0, 0, 0, 1
        ];
    }

    getGrayscaleMatrix() {
        // 灰度矩阵
        return [
            0.299, 0.587, 0.114, 0,
            0.299, 0.587, 0.114, 0,
            0.299, 0.587, 0.114, 0,
            0, 0, 0, 1
        ];
    }

    // 应用后期处理效果
    applyPostProcessingEffects(config) {
        // 在实际应用中,这里会设置后期处理着色器
        // 使用颜色矩阵或自定义着色器应用全局颜色调整
        console.log('应用后期处理效果:', config);
    }

    // 更新模式指示器
    updateModeIndicator(modeName) {
        const modeNames = {
            default: '默认模式',
            highContrast: '高对比度模式',
            protanopia: '红色盲友好模式',
            deuteranopia: '绿色盲友好模式',
            tritanopia: '蓝色盲友好模式',
            grayscale: '灰度模式'
        };
        
        // 显示模式切换反馈
        if (window.screenReader) {
            window.screenReader.announce(`已切换到${modeNames[modeName]}`);
        }
    }

    // 保存用户偏好
    saveUserPreference(modeName) {
        localStorage.setItem('threejs-color-accessibility', modeName);
    }

    // 加载用户偏好
    loadUserPreference() {
        const savedMode = localStorage.getItem('threejs-color-accessibility');
        if (savedMode && this.modes[savedMode]) {
            this.setMode(savedMode);
        }
    }

    // 为暗色模式调整
    adjustForDarkMode() {
        // 自动调整材质以适应暗色模式
        this.scene.traverse((object) => {
            if (object.isMesh && object.material) {
                // 降低亮度,增加对比度
                if (object.material.color) {
                    object.material.color.multiplyScalar(0.8);
                }
            }
        });
        
        // 调整场景背景
        if (this.scene.background && this.scene.background.isColor) {
            this.scene.background = new THREE.Color(0x1a1a1a);
        }
    }
}

3. 键盘导航和语音控制

javascript 复制代码
// KeyboardNavigation.js
class KeyboardNavigation {
    constructor(camera, scene, renderer) {
        this.camera = camera;
        this.scene = scene;
        this.renderer = renderer;
        this.navigationMode = 'object'; // 'object', 'camera', 'ui'
        this.focusStack = [];
        this.keyboardState = new Set();
        
        this.init();
    }

    init() {
        this.setupEventListeners();
        this.createNavigationHelp();
        this.setupVoiceControl();
    }

    // 设置事件监听器
    setupEventListeners() {
        document.addEventListener('keydown', (event) => {
            this.onKeyDown(event);
        });
        
        document.addEventListener('keyup', (event) => {
            this.onKeyUp(event);
        });
        
        // 防止Tab键失去焦点
        this.renderer.domElement.setAttribute('tabindex', '0');
        this.renderer.domElement.addEventListener('keydown', (event) => {
            if (event.key === 'Tab') {
                event.preventDefault();
            }
        });
    }

    // 键盘按下处理
    onKeyDown(event) {
        this.keyboardState.add(event.key);
        
        // 防止默认行为
        if (this.isNavigationKey(event.key)) {
            event.preventDefault();
        }
        
        this.handleNavigation(event);
    }

    // 键盘释放处理
    onKeyUp(event) {
        this.keyboardState.delete(event.key);
    }

    // 检查是否是导航键
    isNavigationKey(key) {
        const navigationKeys = [
            'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
            'Tab', 'Enter', ' ', 'Escape', 'Home', 'End'
        ];
        return navigationKeys.includes(key);
    }

    // 处理导航
    handleNavigation(event) {
        switch (this.navigationMode) {
            case 'object':
                this.handleObjectNavigation(event);
                break;
            case 'camera':
                this.handleCameraNavigation(event);
                break;
            case 'ui':
                this.handleUINavigation(event);
                break;
        }
    }

    // 对象导航处理
    handleObjectNavigation(event) {
        switch (event.key) {
            case 'Tab':
                if (event.shiftKey) {
                    this.previousObject();
                } else {
                    this.nextObject();
                }
                break;
                
            case 'ArrowUp':
                this.navigateDirection('up');
                break;
                
            case 'ArrowDown':
                this.navigateDirection('down');
                break;
                
            case 'ArrowLeft':
                this.navigateDirection('left');
                break;
                
            case 'ArrowRight':
                this.navigateDirection('right');
                break;
                
            case ' ':
            case 'Enter':
                this.activateCurrentObject();
                break;
                
            case 'Escape':
                this.exitObjectMode();
                break;
                
            case 'c':
            case 'C':
                this.switchToCameraMode();
                break;
        }
    }

    // 相机导航处理
    handleCameraNavigation(event) {
        const moveSpeed = 0.5;
        const rotateSpeed = 0.02;
        
        switch (event.key) {
            case 'ArrowUp':
                this.camera.translateY(moveSpeed);
                break;
                
            case 'ArrowDown':
                this.camera.translateY(-moveSpeed);
                break;
                
            case 'ArrowLeft':
                this.camera.translateX(-moveSpeed);
                break;
                
            case 'ArrowRight':
                this.camera.translateX(moveSpeed);
                break;
                
            case 'w':
            case 'W':
                this.camera.translateZ(-moveSpeed);
                break;
                
            case 's':
            case 'S':
                this.camera.translateZ(moveSpeed);
                break;
                
            case 'a':
            case 'A':
                this.camera.rotation.y += rotateSpeed;
                break;
                
            case 'd':
            case 'D':
                this.camera.rotation.y -= rotateSpeed;
                break;
                
            case 'Escape':
            case 'o':
            case 'O':
                this.switchToObjectMode();
                break;
        }
        
        this.announceCameraPosition();
    }

    // 导航到下一个对象
    nextObject() {
        const focusableObjects = this.getFocusableObjects();
        if (focusableObjects.length === 0) return;
        
        const currentIndex = this.getCurrentObjectIndex();
        const nextIndex = (currentIndex + 1) % focusableObjects.length;
        this.focusObject(focusableObjects[nextIndex]);
    }

    // 导航到上一个对象
    previousObject() {
        const focusableObjects = this.getFocusableObjects();
        if (focusableObjects.length === 0) return;
        
        const currentIndex = this.getCurrentObjectIndex();
        const prevIndex = currentIndex <= 0 ? focusableObjects.length - 1 : currentIndex - 1;
        this.focusObject(focusableObjects[prevIndex]);
    }

    // 方向导航
    navigateDirection(direction) {
        const currentObject = this.getCurrentFocusedObject();
        if (!currentObject) return;
        
        const adjacentObject = this.findObjectInDirection(currentObject, direction);
        if (adjacentObject) {
            this.focusObject(adjacentObject);
        } else {
            this.announce(`${direction}方向没有可导航的对象`);
        }
    }

    // 获取可聚焦对象
    getFocusableObjects() {
        const objects = [];
        this.scene.traverse((object) => {
            if (object.userData && object.userData.focusable) {
                objects.push(object);
            }
        });
        
        // 按位置排序(从左到右,从上到下)
        objects.sort((a, b) => {
            if (a.position.y !== b.position.y) {
                return b.position.y - a.position.y; // 从上到下
            }
            return a.position.x - b.position.x; // 从左到右
        });
        
        return objects;
    }

    // 获取当前对象索引
    getCurrentObjectIndex() {
        const currentObject = this.getCurrentFocusedObject();
        const focusableObjects = this.getFocusableObjects();
        return focusableObjects.findIndex(obj => obj === currentObject);
    }

    // 获取当前聚焦对象
    getCurrentFocusedObject() {
        return this.focusStack.length > 0 ? 
            this.focusStack[this.focusStack.length - 1] : null;
    }

    // 聚焦对象
    focusObject(object) {
        // 移除之前的高亮
        this.clearFocus();
        
        // 添加新焦点
        this.focusStack.push(object);
        
        // 视觉反馈
        this.highlightObject(object);
        
        // 屏幕阅读器通知
        this.announceObjectFocus(object);
        
        // 相机看向对象
        this.lookAtObject(object);
    }

    // 宣布对象焦点
    announceObjectFocus(object) {
        const description = object.userData.ariaLabel || 
                           object.userData.description || 
                           '未命名对象';
        const actions = object.userData.accessibleActions || [];
        const actionText = actions.length > 0 ? 
            `可用操作:${actions.join(',')}` : '按空格键激活';
        
        this.announce(`${description}。${actionText}`);
    }

    // 宣布相机位置
    announceCameraPosition() {
        const position = this.camera.position;
        const roundedX = Math.round(position.x * 10) / 10;
        const roundedY = Math.round(position.y * 10) / 10;
        const roundedZ = Math.round(position.z * 10) / 10;
        
        this.announce(`相机位置:X ${roundedX}, Y ${roundedY}, Z ${roundedZ}`, 'off');
    }

    // 看向对象
    lookAtObject(object) {
        // 创建看向目标的动画
        const targetPosition = object.position.clone();
        const cameraPosition = this.camera.position.clone();
        
        // 计算看向方向
        const direction = new THREE.Vector3()
            .subVectors(targetPosition, cameraPosition)
            .normalize();
        
        // 平滑过渡
        this.animateCameraLook(direction);
    }

    // 动画相机看向
    animateCameraLook(targetDirection, duration = 500) {
        const startRotation = this.camera.rotation.clone();
        const targetRotation = new THREE.Euler().setFromRotationMatrix(
            new THREE.Matrix4().lookAt(
                this.camera.position,
                new THREE.Vector3().addVectors(
                    this.camera.position,
                    targetDirection
                ),
                new THREE.Vector3(0, 1, 0)
            )
        );
        
        const startTime = performance.now();
        
        const animate = (currentTime) => {
            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / duration, 1);
            
            // 使用缓动函数
            const easedProgress = this.easeInOutCubic(progress);
            
            this.camera.rotation.x = startRotation.x + 
                (targetRotation.x - startRotation.x) * easedProgress;
            this.camera.rotation.y = startRotation.y + 
                (targetRotation.y - startRotation.y) * easedProgress;
            
            if (progress < 1) {
                requestAnimationFrame(animate);
            }
        };
        
        requestAnimationFrame(animate);
    }

    // 缓动函数
    easeInOutCubic(t) {
        return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
    }

    // 设置语音控制
    setupVoiceControl() {
        if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
            this.setupSpeechRecognition();
        } else {
            console.log('浏览器不支持语音识别');
        }
    }

    // 设置语音识别
    setupSpeechRecognition() {
        const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
        this.recognition = new SpeechRecognition();
        
        this.recognition.continuous = true;
        this.recognition.interimResults = true;
        this.recognition.lang = 'zh-CN';
        
        this.recognition.onresult = (event) => {
            const transcript = Array.from(event.results)
                .map(result => result[0].transcript)
                .join('');
            
            this.processVoiceCommand(transcript);
        };
        
        this.recognition.onerror = (event) => {
            console.log('语音识别错误:', event.error);
        };
        
        // 添加语音控制开关
        this.createVoiceControlToggle();
    }

    // 处理语音命令
    processVoiceCommand(transcript) {
        const commands = {
            '下一个': () => this.nextObject(),
            '上一个': () => this.previousObject(),
            '选择': () => this.activateCurrentObject(),
            '退出': () => this.exitObjectMode(),
            '相机模式': () => this.switchToCameraMode(),
            '对象模式': () => this.switchToObjectMode(),
            '向左': () => this.navigateDirection('left'),
            '向右': () => this.navigateDirection('right'),
            '向上': () => this.navigateDirection('up'),
            '向下': () => this.navigateDirection('down')
        };
        
        for (const [command, action] of Object.entries(commands)) {
            if (transcript.includes(command)) {
                action();
                this.announce(`执行命令:${command}`);
                break;
            }
        }
    }

    // 创建语音控制开关
    createVoiceControlToggle() {
        const button = document.createElement('button');
        button.textContent = '🎤 语音控制';
        button.className = 'voice-control-toggle';
        button.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 10px 15px;
            background: #007acc;
            color: white;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            z-index: 1000;
        `;
        
        let isListening = false;
        
        button.addEventListener('click', () => {
            if (isListening) {
                this.recognition.stop();
                button.textContent = '🎤 语音控制';
                button.style.background = '#007acc';
            } else {
                this.recognition.start();
                button.textContent = '🔴 监听中...';
                button.style.background = '#cc0000';
            }
            isListening = !isListening;
        });
        
        document.body.appendChild(button);
    }

    // 创建导航帮助
    createNavigationHelp() {
        const helpButton = document.createElement('button');
        helpButton.textContent = '? 导航帮助';
        helpButton.className = 'navigation-help-toggle';
        helpButton.style.cssText = `
            position: fixed;
            bottom: 20px;
            left: 20px;
            padding: 10px 15px;
            background: #28a745;
            color: white;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            z-index: 1000;
        `;
        
        helpButton.addEventListener('click', () => {
            this.showNavigationHelp();
        });
        
        document.body.appendChild(helpButton);
    }

    // 显示导航帮助
    showNavigationHelp() {
        const helpText = `
            <h3>3D场景导航帮助</h3>
            <h4>对象导航模式:</h4>
            <ul>
                <li><kbd>Tab</kbd> - 下一个对象</li>
                <li><kbd>Shift + Tab</kbd> - 上一个对象</li>
                <li><kbd>方向键</kbd> - 按方向导航</li>
                <li><kbd>空格</kbd> 或 <kbd>Enter</kbd> - 激活对象</li>
                <li><kbd>Esc</kbd> - 退出对象模式</li>
                <li><kbd>C</kbd> - 切换到相机模式</li>
            </ul>
            <h4>相机导航模式:</h4>
            <ul>
                <li><kbd>方向键</kbd> - 移动相机</li>
                <li><kbd>WASD</kbd> - 移动和旋转相机</li>
                <li><kbd>Esc</kbd> 或 <kbd>O</kbd> - 返回对象模式</li>
            </ul>
            <h4>语音命令:</h4>
            <ul>
                <li>"下一个" - 下一个对象</li>
                <li>"上一个" - 上一个对象</li>
                <li>"选择" - 激活对象</li>
                <li>"向左/右/上/下" - 方向导航</li>
                <li>"相机模式" - 切换模式</li>
            </ul>
        `;
        
        const helpDialog = document.createElement('div');
        helpDialog.className = 'navigation-help-dialog';
        helpDialog.innerHTML = helpText;
        helpDialog.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            z-index: 1001;
            max-width: 500px;
            max-height: 80vh;
            overflow-y: auto;
        `;
        
        const overlay = document.createElement('div');
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 1000;
        `;
        
        overlay.addEventListener('click', () => {
            document.body.removeChild(overlay);
            document.body.removeChild(helpDialog);
        });
        
        document.body.appendChild(overlay);
        document.body.appendChild(helpDialog);
        
        // 屏幕阅读器通知
        this.announce('已打开导航帮助对话框');
    }

    // 通用消息宣布
    announce(message, priority = 'polite') {
        if (window.screenReader) {
            window.screenReader.announce(message, priority);
        } else {
            console.log(`[Navigation] ${message}`);
        }
    }
}

4. 无障碍CSS样式

css 复制代码
/* Accessibility.css */
.sr-only {
    position: absolute;
    left: -10000px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.virtual-focus-element:focus {
    outline: 2px solid #005fcc;
    outline-offset: 2px;
}

/* 高对比度模式样式 */
.high-contrast {
    --text-color: #ffffff;
    --background-color: #000000;
    --focus-color: #ffff00;
    --border-color: #ffffff;
}

.high-contrast .virtual-focus-element:focus {
    outline: 3px solid var(--focus-color);
}

/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* 增大光标偏好 */
@media (pointer: coarse) {
    .virtual-focus-element {
        min-width: 44px;
        min-height: 44px;
    }
}

/* 暗色模式支持 */
@media (prefers-color-scheme: dark) {
    .color-accessibility-panel {
        background: #2d2d2d;
        color: #ffffff;
    }
}

/* 焦点可见性 */
.focus-visible {
    outline: 2px solid #005fcc;
    outline-offset: 2px;
}

/* 无障碍工具提示 */
.accessibility-tooltip {
    position: absolute;
    background: #000000;
    color: #ffffff;
    padding: 8px 12px;
    border-radius: 4px;
    font-size: 14px;
    z-index: 10000;
    max-width: 300px;
    pointer-events: none;
}

/* 大字模式支持 */
.large-text-mode {
    font-size: 1.5em;
}

.large-text-mode .virtual-focus-element {
    min-height: 60px;
    min-width: 60px;
}

无障碍测试与验证

自动化无障碍测试

javascript 复制代码
// AccessibilityValidator.js
class AccessibilityValidator {
    constructor(scene, renderer) {
        this.scene = scene;
        this.renderer = renderer;
        this.issues = [];
    }

    // 运行完整验证
    async runFullValidation() {
        this.issues = [];
        
        await this.validateScreenReaderSupport();
        await this.validateKeyboardNavigation();
        await this.validateColorAccessibility();
        await this.validateTextAlternatives();
        await this.validateFocusManagement();
        
        return this.generateReport();
    }

    // 验证屏幕阅读器支持
    async validateScreenReaderSupport() {
        const objects = this.collectSceneObjects();
        
        objects.forEach(object => {
            // 检查是否有ARIA标签
            if (!object.userData.ariaLabel && !object.userData.description) {
                this.issues.push({
                    type: 'screen_reader',
                    severity: 'medium',
                    message: `对象 ${object.uuid} 缺少屏幕阅读器描述`,
                    element: object
                });
            }
            
            // 检查角色定义
            if (!object.userData.ariaRole) {
                this.issues.push({
                    type: 'screen_reader',
                    severity: 'low',
                    message: `对象 ${object.uuid} 缺少ARIA角色`,
                    element: object
                });
            }
        });
    }

    // 验证键盘导航
    async validateKeyboardNavigation() {
        const focusableObjects = this.collectFocusableObjects();
        
        if (focusableObjects.length === 0) {
            this.issues.push({
                type: 'keyboard_navigation',
                severity: 'high',
                message: '场景中没有可键盘导航的对象',
                element: null
            });
        }
        
        // 检查Tab顺序
        this.validateTabOrder(focusableObjects);
        
        // 检查键盘陷阱
        this.validateKeyboardTraps();
    }

    // 验证颜色可访问性
    async validateColorAccessibility() {
        const materials = this.collectMaterials();
        
        materials.forEach(material => {
            // 检查颜色对比度
            if (material.color) {
                const contrast = this.calculateColorContrast(material.color);
                if (contrast < 4.5) {
                    this.issues.push({
                        type: 'color_contrast',
                        severity: 'medium',
                        message: `材质颜色对比度不足: ${contrast.toFixed(2)}`,
                        element: material
                    });
                }
            }
            
            // 检查颜色单独使用
            if (material.userData && material.userData.usesColorAlone) {
                this.issues.push({
                    type: 'color_usage',
                    severity: 'medium',
                    message: '颜色被单独用于传达信息',
                    element: material
                });
            }
        });
    }

    // 收集场景对象
    collectSceneObjects() {
        const objects = [];
        this.scene.traverse(object => {
            if (object.isMesh) {
                objects.push(object);
            }
        });
        return objects;
    }

    // 收集可聚焦对象
    collectFocusableObjects() {
        return this.collectSceneObjects().filter(obj => 
            obj.userData && obj.userData.focusable
        );
    }

    // 收集材质
    collectMaterials() {
        const materials = new Set();
        this.scene.traverse(object => {
            if (object.material) {
                materials.add(object.material);
            }
        });
        return Array.from(materials);
    }

    // 计算颜色对比度
    calculateColorContrast(color) {
        // 简化实现 - 实际需要计算与背景的对比度
        const luminance = this.calculateLuminance(color);
        const backgroundLuminance = 0.05; // 假设暗色背景
        const lighter = Math.max(luminance, backgroundLuminance);
        const darker = Math.min(luminance, backgroundLuminance);
        return (lighter + 0.05) / (darker + 0.05);
    }

    // 计算亮度
    calculateLuminance(color) {
        const r = color.r <= 0.03928 ? color.r / 12.92 : Math.pow((color.r + 0.055) / 1.055, 2.4);
        const g = color.g <= 0.03928 ? color.g / 12.92 : Math.pow((color.g + 0.055) / 1.055, 2.4);
        const b = color.b <= 0.03928 ? color.b / 12.92 : Math.pow((color.b + 0.055) / 1.055, 2.4);
        return 0.2126 * r + 0.7152 * g + 0.0722 * b;
    }

    // 生成报告
    generateReport() {
        const summary = {
            totalIssues: this.issues.length,
            bySeverity: {
                high: this.issues.filter(issue => issue.severity === 'high').length,
                medium: this.issues.filter(issue => issue.severity === 'medium').length,
                low: this.issues.filter(issue => issue.severity === 'low').length
            },
            byType: {}
        };
        
        this.issues.forEach(issue => {
            summary.byType[issue.type] = (summary.byType[issue.type] || 0) + 1;
        });
        
        return {
            summary: summary,
            issues: this.issues,
            timestamp: new Date().toISOString()
        };
    }
}

通过实施这些无障碍设计策略,可以确保Three.js应用对所有用户都更加友好和可访问。无障碍设计不仅是一项法律要求,更是创造更好用户体验的重要实践。

相关推荐
reddingtons9 小时前
Firefly Text-to-Texture:一键生成PBR武器材质的游戏美术效率革命
人工智能·3d·prompt·材质·技术美术·游戏策划·游戏美术
GISer_Jing14 小时前
Three.js核心技术解析:3D开发指南
javascript·3d·webgl
geng_zhaoying1 天前
在VPython中使用向量计算3D物体移动
python·3d·vpython
中科米堆1 天前
机械行业案例 | 大型钢部件三维扫描3D尺寸检测解决方案-CASAIM
3d·3d全尺寸检测
top_designer2 天前
Substance 3D Stager:电商“虚拟摄影”工作流
人工智能·3d·设计模式·prompt·技术美术·教育电商·游戏美术
tealcwu2 天前
【Unity小技巧】如何将3D场景转换成2D场景
3d·unity·游戏引擎
3DVisionary2 天前
基于XTOM蓝光扫描的复杂中小尺寸零件3D形貌重建与全尺寸误差分析
数码相机·3d·质量控制·3d尺寸检测·xtom蓝光扫描·复杂结构零件·中小尺寸测量
2401_863801462 天前
最常见的 3D 文件类型
3d
3DVisionary2 天前
小尺寸手机零部件3D检测:高精度3D扫描如何助力高效质量控制
3d·智能手机·质量控制·精密测量·3d扫描检测·手机零部件·小幅面扫描