aardio 文档

aardio 范例: WebView2 - Three.js Robot 演示

import win.ui;
var winform = win.form(text="WebView2 - Three.js Robot 演示")

import web.view;
var wb = web.view(winform);
wb.transparent = true;

wb.external = {
    closeWindow = function(){
        winform.close();
    };
}

wb.html = /**********
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Three.js Robot</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      min-height: 100vh;
      background: transparent;
      overflow: hidden;
      font-family: 'Segoe UI', Arial, sans-serif;
    }

    #canvas-container {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }

    .controls {
      position: fixed;
      bottom: 30px;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 12px 20px;
      border-radius: 30px;
      background: rgba(0, 0, 0, 0.65);
      backdrop-filter: blur(15px);
      border: 1px solid rgba(255, 255, 255, 0.15);
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
      z-index: 100;
    }

    .controls label {
      color: white;
      font-size: 14px;
      font-weight: 500;
    }

    .controls select {
      padding: 8px 16px;
      border-radius: 15px;
      border: 1px solid rgba(255, 255, 255, 0.2);
      background: rgba(255, 255, 255, 0.1);
      color: white;
      font-size: 13px;
      cursor: pointer;
      outline: none;
      transition: all 0.3s;
    }

    .controls select:hover {
      background: rgba(255, 255, 255, 0.2);
    }

    .controls select option {
      background: #2a2a2a;
      color: white;
    }

    .hint {
      color: rgba(255, 255, 255, 0.5);
      font-size: 12px;
      padding-left: 10px;
      border-left: 1px solid rgba(255, 255, 255, 0.2);
    }

    .close-btn {
      width: 30px;
      height: 30px;
      border-radius: 50%;
      border: none;
      background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
      color: white;
      font-size: 18px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.3s ease;
      margin-left: 5px;
    }

    .close-btn:hover {
      background: linear-gradient(135deg, #ff5252, #e53935);
      transform: scale(1.1) rotate(90deg);
      box-shadow: 0 0 20px rgba(255, 100, 100, 0.6);
    }

    .loading {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      text-align: center;
    }

    .loading-text {
      color: white;
      font-size: 20px;
      margin-top: 20px;
      text-shadow: 0 0 20px rgba(0, 200, 255, 0.8);
    }

    .spinner {
      width: 60px;
      height: 60px;
      border: 4px solid rgba(255, 255, 255, 0.1);
      border-top-color: #00d4ff;
      border-radius: 50%;
      animation: spin 1s linear infinite;
      margin: 0 auto;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    .emote-btn {
      padding: 6px 12px;
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.3);
      background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(100, 0, 255, 0.3));
      color: white;
      font-size: 12px;
      cursor: pointer;
      transition: all 0.3s;
    }

    .emote-btn:hover {
      background: linear-gradient(135deg, rgba(0, 200, 255, 0.5), rgba(100, 0, 255, 0.5));
      transform: scale(1.05);
    }

    .auto-indicator {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: #00ff88;
      animation: pulse 1.5s ease-in-out infinite;
      margin-left: 5px;
    }

    .auto-indicator.paused {
      background: #ffaa00;
      animation: none;
    }

    @keyframes pulse {
      0%, 100% { opacity: 1; transform: scale(1); }
      50% { opacity: 0.5; transform: scale(0.8); }
    }
  </style>
</head>
<body>
  <div id="canvas-container"></div>

  <div class="loading" id="loading">
    <div class="spinner"></div>
    <div class="loading-text">加载机器人模型中...</div>
  </div>

  <div class="controls" id="controls" style="display: none;">
    <label>🤖 动作:</label>
    <select id="animationSelect"></select>
    <button class="emote-btn" id="randomBtn">🎲 随机</button>
    <span class="hint">🖱️ 拖拽旋转 | 滚轮缩放</span>
    <div class="auto-indicator" id="autoIndicator" title="自动模式"></div>
    <button class="close-btn" onclick="aardio.closeWindow()" title="关闭">×</button>
  </div>

  <script type="importmap">
  {
    "imports": {
      "three": "https://registry.npmmirror.com/three/0.170.0/files/build/three.module.js",
      "three/addons/": "https://registry.npmmirror.com/three/0.170.0/files/examples/jsm/"
    }
  }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

    let mixer, actions = {}, activeAction, previousAction;
    let model;
    let lastInteractionTime = Date.now();
    let autoActionTimer = null;
    let isSitting = false;
    const AUTO_ACTION_DELAY = 5000;
    const AUTO_ACTION_INTERVAL = 4000;

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      0.1,
      100
    );
    camera.position.set(0, 1.2, 17);

    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true
    });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setClearColor(0x000000, 0);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1;
    document.getElementById('canvas-container').appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.target.set(-3, 4.0, 0);
    controls.minDistance = 3;
    controls.maxDistance = 15;
    controls.update();

    function onUserInteraction() {
      lastInteractionTime = Date.now();
      document.getElementById('autoIndicator')?.classList.add('paused');
    }

    renderer.domElement.addEventListener('mousedown', onUserInteraction);
    renderer.domElement.addEventListener('wheel', onUserInteraction);
    renderer.domElement.addEventListener('touchstart', onUserInteraction);
    document.getElementById('controls')?.addEventListener('click', onUserInteraction);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);

    const mainLight = new THREE.DirectionalLight(0xffffff, 2);
    mainLight.position.set(5, 10, 7);
    mainLight.castShadow = true;
    mainLight.shadow.mapSize.width = 2048;
    mainLight.shadow.mapSize.height = 2048;
    mainLight.shadow.camera.near = 0.5;
    mainLight.shadow.camera.far = 50;
    mainLight.shadow.camera.left = -10;
    mainLight.shadow.camera.right = 10;
    mainLight.shadow.camera.top = 10;
    mainLight.shadow.camera.bottom = -10;
    scene.add(mainLight);

    const fillLight = new THREE.DirectionalLight(0x4488ff, 0.8);
    fillLight.position.set(-5, 5, -5);
    scene.add(fillLight);

    const rimLight = new THREE.DirectionalLight(0xff8844, 0.6);
    rimLight.position.set(0, 2, -6);
    scene.add(rimLight);

    // 地面
    const groundGeometry = new THREE.CircleGeometry(4, 64);
    const groundMaterial = new THREE.MeshStandardMaterial({
      color: 0x111111,
      transparent: true,
      opacity: 0.4,
      roughness: 0.9
    });
    const ground = new THREE.Mesh(groundGeometry, groundMaterial);
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    scene.add(ground);

    const ringGeometry = new THREE.RingGeometry(3.8, 4, 64);
    const ringMaterial = new THREE.MeshBasicMaterial({
      color: 0x00aaff,
      transparent: true,
      opacity: 0.3,
      side: THREE.DoubleSide
    });
    const ring = new THREE.Mesh(ringGeometry, ringMaterial);
    ring.rotation.x = -Math.PI / 2;
    ring.position.y = 0.01;
    scene.add(ring);

    function playRandomAction() {
      const actionNames = Object.keys(actions);
      if (actionNames.length === 0) return;

      const availableActions = actionNames.filter(name => {
        if (actions[name] === activeAction) return false;
        if (name === 'Standing' && !isSitting) return false;
        if (name === 'Sitting' && isSitting) return false;
        return true;
      });

      if (availableActions.length === 0) return;

      const randomName = availableActions[Math.floor(Math.random() * availableActions.length)];
      const select = document.getElementById('animationSelect');
      if (select) select.value = randomName;
      fadeToAction(randomName, 0.5);
    }

    function checkAutoAction() {
      const now = Date.now();
      const indicator = document.getElementById('autoIndicator');

      if (now - lastInteractionTime > AUTO_ACTION_DELAY) {
        if (indicator) indicator.classList.remove('paused');
        playRandomAction();
      }
    }

    // 加载模型
    const loader = new GLTFLoader();

    loader.load(
      'https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb',
      (gltf) => {
        model = gltf.scene;

        model.traverse((child) => {
          if (child.isMesh) {
            child.castShadow = true;
            child.receiveShadow = true;
          }
        });

        scene.add(model);

        mixer = new THREE.AnimationMixer(model);

        const animations = gltf.animations;
        const select = document.getElementById('animationSelect');

        const animNames = {
          'Dance': '💃 跳舞',
          'Death': '💀 倒地',
          'Idle': '🧍 待机',
          'Jump': '🦘 跳跃',
          'No': '🙅 摇头',
          'Punch': '👊 出拳',
          'Running': '🏃 奔跑',
          'Sitting': '🪑 坐下',
          'Standing': '🧍 站起来',
          'ThumbsUp': '👍 点赞',
          'Walking': '🚶 行走',
          'WalkJump': '🏃 跳步',
          'Wave': '👋 挥手',
          'Yes': '🙆 点头'
        };

        animations.forEach((clip) => {
          const action = mixer.clipAction(clip);
          actions[clip.name] = action;

          // 一次性动作:播放完保持最后一帧
          if (['Jump', 'Death', 'Punch', 'ThumbsUp', 'Wave', 'Yes', 'No', 'Sitting', 'Standing'].includes(clip.name)) {
            action.clampWhenFinished = true;
            action.loop = THREE.LoopOnce;
          }

          const option = document.createElement('option');
          option.value = clip.name;
          option.textContent = animNames[clip.name] || clip.name;
          select.appendChild(option);
        });

        const defaultAnim = actions['Wave'] || actions['Idle'] || actions[animations[0].name];
        if (defaultAnim) {
          activeAction = defaultAnim;
          activeAction.play();
          select.value = activeAction.getClip().name;
        }

        mixer.addEventListener('finished', (e) => {
          const finishedClipName = e.action.getClip().name;

          // 挥手结束后切换到待机
          if (finishedClipName === 'Wave') {
            fadeToAction('Idle', 0.5);
            document.getElementById('animationSelect').value = 'Idle';
          }
          // 其他一次性动作(除了 Sitting/Standing/Death)结束后也切换到待机或当前姿态
          else if (['Jump', 'Punch', 'ThumbsUp', 'Yes', 'No'].includes(finishedClipName)) {
            if (isSitting) {
              // 如果是坐姿状态,保持坐姿(重新播放Sitting的最后一帧)
              // 这里直接不做切换,保持当前状态
            } else {
              fadeToAction('Idle', 0.5);
              document.getElementById('animationSelect').value = 'Idle';
            }
          }
        });

        select.addEventListener('change', (e) => {
          onUserInteraction();
          fadeToAction(e.target.value, 0.4);
        });

        document.getElementById('randomBtn').addEventListener('click', () => {
          onUserInteraction();
          playRandomAction();
        });

        document.getElementById('loading').style.display = 'none';
        document.getElementById('controls').style.display = 'flex';

        // 启动自动动作定时器
        autoActionTimer = setInterval(checkAutoAction, AUTO_ACTION_INTERVAL);
      },
      (progress) => {
        if (progress.total > 0) {
          const percent = Math.round((progress.loaded / progress.total) * 100);
          document.querySelector('.loading-text').textContent = `加载中 ${percent}%`;
        }
      },
      (error) => {
        document.querySelector('.loading-text').textContent = '❌ 加载失败';
      }
    );

    function fadeToAction(name, duration) {
      if (name === 'Standing' && !isSitting) {
        return;
      }
      if (name === 'Sitting' && isSitting) {
        return;
      }

      if (name === 'Sitting') isSitting = true;
      if (name === 'Standing') isSitting = false;

      previousAction = activeAction;
      activeAction = actions[name];

      if (previousAction !== activeAction) {
        previousAction?.fadeOut(duration);
      }

      activeAction?.reset().setEffectiveTimeScale(1).setEffectiveWeight(1).fadeIn(duration).play();
    }

    const clock = new THREE.Clock();

    function animate() {
      requestAnimationFrame(animate);

      const delta = clock.getDelta();
      const time = clock.getElapsedTime();

      if (mixer) mixer.update(delta);

      ring.material.opacity = 0.2 + Math.sin(time * 2) * 0.1;

      controls.update();
      renderer.render(scene, camera);
    }

    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });

    animate();
  </script>
</body>
</html>
**********/

winform.fullscreen(true);
winform.show();
win.loopMessage();
Markdown 格式