测试Qwen3.5-27B-Claude-4.6-Opus-Distilled-MLX-4bit 效果 M5 PRO 64G

设备: M5 PRO 64G内存

模型 Qwen3.5-27B-Claude-4.6-Opus-Distilled-MLX-4bit

基准测试 速度有点慢

先测试下天气卡片效果

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>iOS 18 Weather Cards</title>
    <style>
        :root {
    --ios-font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
    --card-width: 280px;
    --card-height: 400px;
}

body {
    margin: 0;
    padding: 0;
    background-color: #000; /* 深色模式背景 */
    font-family: var(--ios-font);
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    overflow-x: auto; /* 允许水平滚动 */
}

/* 容器:横版排列 */
.container {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 24px;
    padding: 40px;
    width: 100%;
    max-width: 1400px;
}

/* 卡片基础样式 */
.card {
    width: var(--card-width);
    height: var(--card-height);
    border-radius: 40px;
    position: relative;
    perspective: 1000px; /* 为 3D 翻转做准备 */
    cursor: pointer;
    transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
    user-select: none;
    box-shadow: 0 10px 30px rgba(0,0,0,0.3);
    overflow: hidden;
}

/* 悬停交互 */
.card:hover {
    transform: scale(1.05) translateY(-10px);
    box-shadow: 0 20px 40px rgba(0,0,0,0.5);
    z-index: 10;
}

/* 翻转逻辑 */
.card.flipped .card-inner {
    transform: rotateY(180deg);
}

.card-inner {
    width: 100%;
    height: 100%;
    position: relative;
    transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1);
    transform-style: preserve-3d;
    border-radius: 40px;
}

/* 卡片正面和背面通用 */
.card-content, .card-back {
    position: absolute;
    width: 100%;
    height: 100%;
    border-radius: 40px;
    backface-visibility: hidden; /* 隐藏背面 */
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 30px;
    box-sizing: border-box;
}

/* 卡片正面背景图 */
.card-content {
    background-size: cover;
    background-position: center;
    color: white;
    text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}

/* 卡片背面样式 */
.card-back {
    background: rgba(255, 255, 255, 0.95);
    color: #333;
    transform: rotateY(180deg);
    align-items: center;
    justify-content: center;
    text-align: center;
}

.card-back h3 {
    font-size: 24px;
    margin-bottom: 20px;
    color: #007aff;
}

.card-back p {
    font-size: 16px;
    line-height: 1.6;
    margin: 5px 0;
}

.card-back .desc {
    font-size: 14px;
    color: #888;
    margin-top: 20px;
    font-style: italic;
}

/* =========================================
   具体天气卡片样式
   ========================================= */

/* 1. 晴天 (Sunny) */
.sunny-card .card-content {
    background: linear-gradient(160deg, #4facfe 0%, #00f2fe 100%);
}

.sun {
    width: 100px;
    height: 100px;
    margin: 0 auto 20px;
    position: relative;
}
.sun-core {
    width: 60px;
    height: 60px;
    background: #ffeb3b;
    border-radius: 50%;
    position: absolute;
    top: 20px; left: 20px;
    box-shadow: 0 0 40px #ffeb3b;
}
.sun-ray {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    border-radius: 50%;
    border: 2px dashed rgba(255, 235, 59, 0.4);
    animation: spin 10s linear infinite;
}

/* 2. 大风 (Windy) */
.windy-card .card-content {
    background: linear-gradient(160deg, #4b6cb7 0%, #182848 100%);
}

.cloud-wind {
    width: 140px;
    height: 80px;
    margin: 0 auto 20px;
    position: relative;
}
.cloud {
    width: 100px;
    height: 40px;
    background: #ddd;
    border-radius: 50px;
    position: absolute;
    top: 20px;
    animation: float 3s ease-in-out infinite;
}
.cloud::after {
    content: '';
    position: absolute;
    width: 50px; height: 50px;
    background: #ddd;
    border-radius: 50%;
    top: -25px; left: 20px;
}
.wind-line {
    position: absolute;
    height: 4px;
    background: rgba(255,255,255,0.6);
    border-radius: 2px;
    top: 50px;
    animation: wind 2s ease-in-out infinite;
}
.w1 { width: 40px; left: 80px; animation-delay: 0s; }
.w2 { width: 60px; left: 90px; animation-delay: 0.5s; top: 60px;}
.w3 { width: 30px; left: 70px; animation-delay: 1s; top: 40px;}

/* 3. 暴雨 (Heavy Rain) */
.rainy-card .card-content {
    background: linear-gradient(160deg, #203a43 0%, #2c5364 100%);
}

.heavy-rain {
    width: 140px;
    height: 100px;
    margin: 0 auto 20px;
    position: relative;
}
.rain-cloud {
    width: 100px; height: 40px;
    background: #556;
    border-radius: 50px;
    position: absolute; top: 10px;
}
.rain-cloud::after { content: ''; position: absolute; width: 50px; height: 50px; background: #556; border-radius: 50%; top: -25px; left: 20px; }
.rain-drop {
    width: 2px; height: 10px;
    background: #89f;
    position: absolute;
    opacity: 0.7;
    animation: rain 1s linear infinite;
}
.r1 { top: 50px; left: 20px; animation-delay: 0s; }
.r2 { top: 50px; left: 40px; animation-delay: 0.2s; height: 15px;}
.r3 { top: 60px; left: 60px; animation-delay: 0.5s; }
.r4 { top: 50px; left: 80px; animation-delay: 0.8s; height: 12px;}

/* 4. 暴雪 (Blizzard) */
.snowy-card .card-content {
    background: linear-gradient(160deg, #83a4d4 0%, #b6fbff 100%);
}

.blizzard {
    width: 140px;
    height: 100px;
    margin: 0 auto 20px;
    position: relative;
}
.snow-cloud {
    width: 100px; height: 40px;
    background: #fff;
    border-radius: 50px;
    position: absolute; top: 10px;
}
.snow-cloud::after { content: ''; position: absolute; width: 50px; height: 50px; background: #fff; border-radius: 50%; top: -25px; left: 20px; }
.snow-flake {
    width: 6px; height: 6px;
    background: #333;
    border-radius: 50%;
    position: absolute;
    opacity: 0.6;
    animation: snow 2s linear infinite;
}
.s1 { top: 50px; left: 20px; animation-delay: 0s; }
.s2 { top: 60px; left: 50px; animation-delay: 0.5s; width: 8px; height: 8px;}
.s3 { top: 40px; left: 80px; animation-delay: 1s; }

/* =========================================
   通用动画 Keyframes
   ========================================= */
@keyframes spin { 100% { transform: rotate(360deg); } }

@keyframes float {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-5px); }
}

@keyframes wind {
    0% { transform: translateX(0); opacity: 0; }
    50% { opacity: 1; }
    100% { transform: translateX(-40px); opacity: 0; }
}

@keyframes rain {
    0% { transform: translateY(0); opacity: 1; }
    100% { transform: translateY(60px); opacity: 0; }
}

@keyframes snow {
    0% { transform: translateY(0) rotate(0deg); opacity: 1; }
    100% { transform: translateY(60px) rotate(180deg); opacity: 0; }
}

/* 文字排版 */
h2 { font-size: 80px; margin: 0; font-weight: 200; line-height: 1; }
p { font-size: 20px; margin: 5px 0 0; font-weight: 500; }
.location { font-size: 16px; opacity: 0.9; margin-top: auto; padding-top: 20px; display: flex; align-items: center; gap: 5px;}

/* 响应式调整 */
@media (max-width: 768px) {
    .card { width: 80vw; height: 300px; }
    h2 { font-size: 60px; }
}

    </style>
</head>
<body>

<div class="container">
    <!-- 1. 晴天 (Sunny) -->
    <div class="card sunny-card" onclick="flipCard(this)">
        <div class="card-content">
            <div class="weather-icon sun">
                <div class="sun-core"></div>
                <div class="sun-ray"></div>
            </div>
            <h2>26°</h2>
            <p>晴朗</p>
            <div class="location">北京</div>
        </div>
        <div class="card-back">
            <h3>详细预报</h3>
            <p>紫外线指数:强</p>
            <p>空气质量:优</p>
            <p class="desc">适合户外活动和晾晒衣物。</p>
        </div>
    </div>

    <!-- 2. 大风 (Windy) -->
    <div class="card windy-card" onclick="flipCard(this)">
        <div class="card-content">
            <div class="weather-icon cloud-wind">
                <div class="cloud"></div>
                <div class="wind-line w1"></div>
                <div class="wind-line w2"></div>
                <div class="wind-line w3"></div>
            </div>
            <h2>18°</h2>
            <p>大风</p>
            <div class="location">呼和浩特</div>
        </div>
        <div class="card-back">
            <h3>详细预报</h3>
            <p>风力:6-7 级</p>
            <p>风向:西北风</p>
            <p class="desc">请注意防风,远离临时搭建物。</p>
        </div>
    </div>

    <!-- 3. 暴雨 (Heavy Rain) -->
    <div class="card rainy-card" onclick="flipCard(this)">
        <div class="card-content">
            <div class="weather-icon heavy-rain">
                <div class="cloud rain-cloud"></div>
                <div class="rain-drop r1"></div>
                <div class="rain-drop r2"></div>
                <div class="rain-drop r3"></div>
                <div class="rain-drop r4"></div>
            </div>
            <h2>15°</h2>
            <p>暴雨</p>
            <div class="location">广州</div>
        </div>
        <div class="card-back">
            <h3>详细预报</h3>
            <p>降雨量:大</p>
            <p>湿度:95%</p>
            <p class="desc">建议减少外出,注意交通安全。</p>
        </div>
    </div>

    <!-- 4. 暴雪 (Blizzard) -->
    <div class="card snowy-card" onclick="flipCard(this)">
        <div class="card-content">
            <div class="weather-icon blizzard">
                <div class="cloud snow-cloud"></div>
                <div class="snow-flake s1"></div>
                <div class="snow-flake s2"></div>
                <div class="snow-flake s3"></div>
                <div class="snow-flake s4"></div>
                <div class="wind-line snow-wind"></div>
            </div>
            <h2>-5°</h2>
            <p>暴雪</p>
            <div class="location">哈尔滨</div>
        </div>
        <div class="card-back">
            <h3>详细预报</h3>
            <p>能见度:低</p>
            <p>路面:结冰</p>
            <p class="desc">极端天气,请尽量留在室内。</p>
        </div>
    </div>
</div>

<script>
    // 交互逻辑
    function flipCard(card) {
        // 只有当卡片没有被锁定(正在翻转中)或者我们可以简单地切换 class
        card.classList.toggle('flipped');
    }
</script>

</style>
</body>
</html>

CASE2 莫比乌斯环

一次生成的效果好像不太好

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D 莫比乌斯环</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            overflow: hidden;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            font-family: 'Segoe UI', sans-serif;
        }
        canvas { display: block; }
        .controls {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(255,255,255,0.1);
            padding: 15px 25px;
            border-radius: 25px;
            backdrop-filter: blur(10px);
            display: flex;
            gap: 20px;
            align-items: center;
        }
        .controls label { color: #fff; font-size: 14px; }
        input[type="range"] {
            width: 120px;
            accent-color: #00d9ff;
        }
        .info {
            position: absolute;
            top: 20px;
            left: 20px;
            color: #fff;
            background: rgba(0,0,0,0.3);
            padding: 15px;
            border-radius: 8px;
        }
        .info h1 { font-size: 18px; margin-bottom: 5px; color: #00d9ff; }
        .info p { font-size: 12px; opacity: 0.8; }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    
    <div class="info">
        <h1>莫比乌斯环 Möbius Strip</h1>
        <p>🖱️ 拖动旋转 | 💫 单侧曲面</p>
    </div>
    
    <div class="controls">
        <label>环半径:<span id="radiusVal">3</span></label>
        <input type="range" id="radius" min="1.5" max="5" step="0.1" value="3">
        <label>环宽度:<span id="widthVal">1</span></label>
        <input type="range" id="width" min="0.3" max="2" step="0.1" value="1">
    </div>

    <script>
        const canvas = document.getElementById('canvas');
        const gl = canvas.getContext('webgl');

        if (!gl) {
            alert('您的浏览器不支持 WebGL');
            throw new Error('WebGL not supported');
        }

        // 调整画布大小
        function resize() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            gl.viewport(0, 0, canvas.width, canvas.height);
        }
        window.addEventListener('resize', resize);
        resize();

        // 顶点着色器
        const vsSource = `
            attribute vec3 position;
            attribute vec3 normal;
            attribute vec2 uv;
            
            uniform mat4 uProjection;
            uniform mat4 uView;
            uniform mat4 uModel;
            
            varying vec3 vNormal;
            varying vec2 vUV;
            varying vec3 vWorldPos;
            
            void main() {
                vec4 worldPos = uModel * vec4(position, 1.0);
                vWorldPos = worldPos.xyz;
                vNormal = mat3(uModel) * normal;
                vUV = uv;
                gl_Position = uProjection * uView * worldPos;
            }
        `;

        // 片元着色器
        const fsSource = `
            precision mediump float;
            
            varying vec3 vNormal;
            varying vec2 vUV;
            varying vec3 vWorldPos;
            
            uniform vec3 uColor1;
            uniform vec3 uColor2;
            uniform vec3 uColor3;
            uniform vec3 uColor4;
            
            void main() {
                vec3 normal = normalize(vNormal);
                
                // 基于 UV 的渐变着色
                float v = vUV.y;
                vec3 color;
                
                if (v < 0.25) {
                    color = mix(uColor1, uColor2, v * 4.0);
                } else if (v < 0.5) {
                    color = mix(uColor2, uColor3, (v - 0.25) * 4.0);
                } else if (v < 0.75) {
                    color = mix(uColor3, uColor4, (v - 0.5) * 4.0);
                } else {
                    color = mix(uColor4, uColor1, (v - 0.75) * 4.0);
                }
                
                // 简单光照
                vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
                float diff = max(dot(normal, lightDir), 0.3);
                
                gl_FragColor = vec4(color * diff, 1.0);
            }
        `;

        // 编译着色器
        function compileShader(source, type) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error(gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }
            return shader;
        }

        const vs = compileShader(vsSource, gl.VERTEX_SHADER);
        const fs = compileShader(fsSource, gl.FRAGMENT_SHADER);
        const program = gl.createProgram();
        gl.attachShader(program, vs);
        gl.attachShader(program, fs);
        gl.linkProgram(program);
        gl.useProgram(program);

        // 获取位置
        const positionsLoc = gl.getAttribLocation(program, 'position');
        const normalsLoc = gl.getAttribLocation(program, 'normal');
        const uvLoc = gl.getAttribLocation(program, 'uv');
        const projectionLoc = gl.getUniformLocation(program, 'uProjection');
        const viewLoc = gl.getUniformLocation(program, 'uView');
        const modelLoc = gl.getUniformLocation(program, 'uModel');
        const color1Loc = gl.getUniformLocation(program, 'uColor1');
        const color2Loc = gl.getUniformLocation(program, 'uColor2');
        const color3Loc = gl.getUniformLocation(program, 'uColor3');
        const color4Loc = gl.getUniformLocation(program, 'uColor4');

        // 矩阵工具函数
        function perspective(fov, aspect, near, far) {
            const f = 1.0 / Math.tan(fov / 2);
            const nf = 1 / (near - far);
            return new Float32Array([
                f / aspect, 0, 0, 0,
                0, f, 0, 0,
                0, 0, (far + near) * nf, -1,
                0, 0, 2 * far * near * nf, 0
            ]);
        }

        function lookAt(eye, center, up) {
            const z = normalize(sub(eye, center));
            const x = normalize(cross(up, z));
            const y = cross(z, x);
            return new Float32Array([
                x[0], y[0], z[0], 0,
                x[1], y[1], z[1], 0,
                x[2], y[2], z[2], 0,
                -dot(x, eye), -dot(y, eye), -dot(z, eye), 1
            ]);
        }

        function multiply(a, b) {
            const result = new Float32Array(16);
            for (let i = 0; i < 4; i++) {
                for (let j = 0; j < 4; j++) {
                    result[i * 4 + j] = 
                        a[i * 4 + 0] * b[0 * 4 + j] +
                        a[i * 4 + 1] * b[1 * 4 + j] +
                        a[i * 4 + 2] * b[2 * 4 + j] +
                        a[i * 4 + 3] * b[3 * 4 + j];
                }
            }
            return result;
        }

        function rotateX(angle) {
            const c = Math.cos(angle), s = Math.sin(angle);
            return new Float32Array([1,0,0,0, 0,c,-s,0, 0,s,c,0, 0,0,0,1]);
        }

        function rotateY(angle) {
            const c = Math.cos(angle), s = Math.sin(angle);
            return new Float32Array([c,0,s,0, 0,1,0,0, -s,0,c,0, 0,0,0,1]);
        }

        function normalize(v) {
            const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
            return [v[0]/len, v[1]/len, v[2]/len];
        }

        function sub(a, b) { return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]; }
        function cross(a, b) {
            return [
                a[1]*b[2] - a[2]*b[1],
                a[2]*b[0] - a[0]*b[2],
                a[0]*b[1] - a[1]*b[0]
            ];
        }
        function dot(a, b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }

        // 生成莫比乌斯环几何体
        function generateMobius(radius, width, segments, res) {
            const positions = [];
            const normals = [];
            const uvs = [];
            const indices = [];

            for (let i = 0; i <= segments; i++) {
                const u = (i / segments) * Math.PI * 2;
                const cosU2 = Math.cos(u / 2);
                const sinU2 = Math.sin(u / 2);
                
                for (let j = 0; j <= res; j++) {
                    const v = (j / res) * 2 - 1;
                    
                    // 莫比乌斯环参数方程
                    const x = (radius + v * width * cosU2) * Math.cos(u);
                    const y = (radius + v * width * cosU2) * Math.sin(u);
                    const z = v * width * sinU2;
                    
                    positions.push(x, y, z);
                    uvs.push(u / Math.PI / 2, j / res);
                }
            }

            // 计算法线
            const pos = new Float32Array(positions);
            for (let i = 0; i <= segments; i++) {
                for (let j = 0; j <= res; j++) {
                    const idx = (i * (res + 1) + j) * 3;
                    
                    // 计算切线
                    let tx = 0, ty = 0, tz = 0, bx = 0, by = 0, bz = 0;
                    
                    if (i > 0 && i < segments) {
                        const prev = ((i-1) * (res + 1) + j) * 3;
                        const next = ((i+1) * (res + 1) + j) * 3;
                        tx = pos[next] - pos[prev];
                        ty = pos[next+1] - pos[prev+1];
                        tz = pos[next+2] - pos[prev+2];
                    } else {
                        const prev = ((i - 1 + segments) % segments * (res + 1) + j) * 3;
                        const next = ((i + 1) % segments * (res + 1) + j) * 3;
                        tx = pos[next] - pos[prev];
                        ty = pos[next+1] - pos[prev+1];
                        tz = pos[next+2] - pos[prev+2];
                    }
                    
                    if (j > 0 && j < res) {
                        const prev = (i * (res + 1) + j - 1) * 3;
                        const next = (i * (res + 1) + j + 1) * 3;
                        bx = pos[next] - pos[prev];
                        by = pos[next+1] - pos[prev+1];
                        bz = pos[next+2] - pos[prev+2];
                    }
                    
                    // 叉积得到法线
                    const nx = ty * bz - tz * by;
                    const ny = tz * bx - tx * bz;
                    const nz = tx * by - ty * bx;
                    const len = Math.sqrt(nx*nx + ny*ny + nz*nz);
                    
                    normals.push(nx/len, ny/len, nz/len);
                }
            }

            // 生成索引
            for (let i = 0; i < segments; i++) {
                for (let j = 0; j < res; j++) {
                    const a = i * (res + 1) + j;
                    const b = (i + 1) * (res + 1) + j;
                    const c = (i + 1) * (res + 1) + j + 1;
                    const d = i * (res + 1) + j + 1;
                    
                    indices.push(a, b, d);
                    indices.push(b, c, d);
                }
            }

            return { positions: new Float32Array(positions), normals: new Float32Array(normals), 
                    uvs: new Float32Array(uvs), indices: new Uint16Array(indices) };
        }

        // 创建缓冲区
        let geometry = null;
        let positionBuffer, normalBuffer, uvBuffer, indexBuffer;
        let numIndices = 0;

        function updateGeometry(radius, width) {
            const segs = 80, res = 20;
            geometry = generateMobius(radius, width, segs, res);
            numIndices = geometry.indices.length;

            // 位置缓冲
            positionBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, geometry.positions, gl.STATIC_DRAW);

            // 法线缓冲
            normalBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, geometry.normals, gl.STATIC_DRAW);

            // UV 缓冲
            uvBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, geometry.uvs, gl.STATIC_DRAW);

            // 索引缓冲
            indexBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
            gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW);
        }

        let radius = 3, width = 1;
        updateGeometry(radius, width);

        // 设置颜色
        gl.uniform3f(color1Loc, 1, 0, 0.2);    // 红
        gl.uniform3f(color2Loc, 1, 0.5, 0);    // 橙
        gl.uniform3f(color3Loc, 0, 1, 0.8);    // 青
        gl.uniform3f(color4Loc, 0, 0.5, 1);    // 蓝

        // 动画参数
        let rotX = 0.3, rotY = 0;
        let autoRotate = true;
        let targetRotX = rotX, targetRotY = rotY;

        // 鼠标交互
        let isDragging = false, lastX = 0, lastY = 0;

        canvas.addEventListener('mousedown', e => {
            isDragging = true;
            autoRotate = false;
            lastX = e.clientX;
            lastY = e.clientY;
        });

        canvas.addEventListener('mousemove', e => {
            if (isDragging) {
                const dx = e.clientX - lastX;
                const dy = e.clientY - lastY;
                targetRotY += dx * 0.01;
                targetRotX += dy * 0.01;
                lastX = e.clientX;
                lastY = e.clientY;
            }
        });

        canvas.addEventListener('mouseup', () => isDragging = false);
        canvas.addEventListener('mouseleave', () => isDragging = false);

        // 触摸支持
        canvas.addEventListener('touchstart', e => {
            isDragging = true;
            autoRotate = false;
            lastX = e.touches[0].clientX;
            lastY = e.touches[0].clientY;
        });

        canvas.addEventListener('touchmove', e => {
            if (isDragging) {
                const dx = e.touches[0].clientX - lastX;
                const dy = e.touches[0].clientY - lastY;
                targetRotY += dx * 0.01;
                targetRotX += dy * 0.01;
                lastX = e.touches[0].clientX;
                lastY = e.touches[0].clientY;
            }
        });

        // 控件
        document.getElementById('radius').addEventListener('input', e => {
            radius = parseFloat(e.target.value);
            document.getElementById('radiusVal').textContent = radius.toFixed(1);
            updateGeometry(radius, width);
        });

        document.getElementById('width').addEventListener('input', e => {
            width = parseFloat(e.target.value);
            document.getElementById('widthVal').textContent = width.toFixed(1);
            updateGeometry(radius, width);
        });

        // 渲染循环
        function render() {
            // 自动旋转
            if (autoRotate) {
                targetRotY += 0.005;
            }

            // 平滑插值
            rotX += (targetRotX - rotX) * 0.1;
            rotY += (targetRotY - rotY) * 0.1;

            // 清空画布
            gl.clearColor(0.1, 0.1, 0.15, 1);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
            gl.enable(gl.DEPTH_TEST);
            gl.enable(gl.CULL_FACE);

            // 投影矩阵
            const aspect = canvas.width / canvas.height;
            const projection = perspective(Math.PI / 4, aspect, 0.1, 100);

            // 视图矩阵
            const view = lookAt([0, 0, 8], [0, 0, 0], [0, 1, 0]);

            // 模型矩阵
            const model = multiply(rotateX(rotX), rotateY(rotY));

            gl.uniformMatrix4fv(projectionLoc, false, projection);
            gl.uniformMatrix4fv(viewLoc, false, view);
            gl.uniformMatrix4fv(modelLoc, false, model);

            // 绑定属性
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            gl.vertexAttribPointer(positionsLoc, 3, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(positionsLoc);

            gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
            gl.vertexAttribPointer(normalsLoc, 3, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(normalsLoc);

            gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
            gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(uvLoc);

            // 绘制
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
            gl.drawElements(gl.TRIANGLES, numIndices, gl.UNSIGNED_SHORT, 0);

            requestAnimationFrame(render);
        }

        render();
    </script>
</body>
</html>

后面再慢慢补充测试内容

1 个赞

期待佬友测试qwen 3.6哈

不知道跑不跑得起来,到时候有测试效果补充这里看看

1 个赞

我记住你了 :grinning_face: