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 格式