# aardio 范例: 鹈鹕骑自行车 2D 动画 - aardio GDI+ 自绘



```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="鹈鹕骑自行车 2D 动画 - aardio GDI+ 自绘";right=794;bottom=437;bgcolor=0xFFFFFF;border="dialog frame";max=false)
winform.add(
canvas={cls="canvas";left=0;top=0;right=795;bottom=438;anchors={left=1;top=1;right=1;bottom=1};z=1}
)
/*}}*/

// ---------------- 动画状态变量 ----------------
var angle = 0; // 脚踏板/车轮旋转角度（弧度）
var bgPos = 0; // 马路与视差背景平移位置（逻辑像素）
var pedalSpeed = 9; // 脚踏板旋转速度（弧度/秒）
var backgroundSpeed = 300; // 背景平移速度（逻辑像素/秒）

// ---------------- GDI+ 逐帧绘制回调 ----------------
winform.canvas.onDrawForeground = function(graphics, rc, foregroundRect, foregroundColor, color, font){
    // 获取当前窗口的 DPI 缩放倍数
    var dpiX, dpiY = winform.dpiScale(1, 1);

    // 启用 GDI+ 画板全局 DPI 自动缩放，所有线条、形状、画笔粗细均自动缩放
    graphics.scale(dpiX, dpiY);

    // 将实际像素尺寸换算为设计时的逻辑坐标尺寸
    var width = rc.width / dpiX;
    var height = rc.height / dpiY;

    // --- 1. 天空与地面背景 ---
    var skyBrush = gdip.solidBrush(0xFF87CEEB); // 天蓝色
    graphics.fillRect(skyBrush, 0, 0, width, 320);
    skyBrush.delete();

    var groundBrush = gdip.solidBrush(0xFF7CFC00); // 草绿色
    graphics.fillRect(groundBrush, 0, 320, width, height - 320);
    groundBrush.delete();

    // --- 2. 沥青马路 ---
    var roadBrush = gdip.solidBrush(0xFF505050); // 深灰色
    graphics.fillRect(roadBrush, 0, 350, width, 100);
    roadBrush.delete();

    // --- 3. 马路中心动态虚线 ---
    var lineBrush = gdip.solidBrush(0xFFFFFFFF);
    var dashWidth = 40;
    var dashGap = 30;
    var totalDash = dashWidth + dashGap;
    var offsetX = (bgPos % totalDash);
    for(x = -totalDash; width + totalDash; totalDash){
        graphics.fillRect(lineBrush, x - offsetX, 395, dashWidth, 10);
    }
    lineBrush.delete();

    // --- 4. 远景白云（慢速视差平移）---
    var cloudBrush = gdip.solidBrush(0xFFFFFFFF);
    var cloudOffset = (bgPos * 0.3) % (width + 200);
    for(i = 0; 2; 1){
        var cx = (i * 400 - cloudOffset) + 100;
        if(cx < -100) cx += width + 200;
        graphics.fillEllipse(cloudBrush, cx, 60, 80, 40);
        graphics.fillEllipse(cloudBrush, cx + 30, 45, 70, 50);
        graphics.fillEllipse(cloudBrush, cx + 60, 60, 80, 40);
    }
    cloudBrush.delete();

    // --- 5. 远景树木（中速视差平移）---
    var treeTrunkBrush = gdip.solidBrush(0xFF8B4513);
    var treeLeavesBrush = gdip.solidBrush(0xFF228B22);
    var treeOffset = (bgPos * 0.6) % 300;
    for(x = -100; width + 200; 300){
        var tx = x - treeOffset;
        graphics.fillRect(treeTrunkBrush, tx + 35, 270, 20, 50);
        graphics.fillEllipse(treeLeavesBrush, tx, 200, 90, 80);
    }
    treeTrunkBrush.delete();
    treeLeavesBrush.delete();

    // ---------------- 自行车与鹈鹕坐标参数 ----------------
    var bob = math.sin(angle * 2) * 4; // 骑行时的上下颠簸幅度
    var bikeX = 400; // 自行车中心 X
    var bikeY = 360; // 车轮接地点 Y

    var wheelR = 45; // 车轮半径
    var rearWheelX = bikeX - 110;
    var rearWheelY = bikeY - wheelR;
    var frontWheelX = bikeX + 110;
    var frontWheelY = bikeY - wheelR;

    var crankX = bikeX - 10;
    var crankY = bikeY - wheelR;
    var crankR = 22; // 脚踏板旋转半径

    var seatX = bikeX - 45;
    var seatY = bikeY - wheelR - 85;

    var handleX = bikeX + 60;
    var handleY = bikeY - wheelR - 95;

    var thickPen = gdip.pen(0xFF202020, 5);
    var framePen = gdip.pen(0xFFE63946, 5); // 经典红车架
    var chromePen = gdip.pen(0xFFCCCCCC, 3);

    // --- 绘制车轮闭包 ---
    var drawWheel = function(wx, wy){
        // 轮胎
        var tirePen = gdip.pen(0xFF1E1E1E, 8);
        graphics.drawCircle(tirePen, wx, wy, wheelR);
        tirePen.delete();

        // 金属轮辋
        graphics.drawCircle(chromePen, wx, wy, wheelR - 4);

        // 轮毂
        var hubBrush = gdip.solidBrush(0xFF505050);
        graphics.fillCircle(hubBrush, wx, wy, 6);
        hubBrush.delete();

        // 动态辐条（随 angle 角度旋转）
        var spokePen = gdip.pen(0xFFAAAAAA, 1.5);
        for(spokeDeg = 0; 360; 45){
            var rad = (spokeDeg * math.pi / 180) + angle;
            var sx = wx + math.cos(rad) * (wheelR - 4);
            var sy = wy + math.sin(rad) * (wheelR - 4);
            graphics.drawLine(spokePen, wx, wy, sx, sy);
        }
        spokePen.delete();
    };

    drawWheel(rearWheelX, rearWheelY);
    drawWheel(frontWheelX, frontWheelY);

    // --- 绘制自行车车架 ---
    // 后叉
    graphics.drawLine(framePen, rearWheelX, rearWheelY, crankX, crankY);
    graphics.drawLine(framePen, rearWheelX, rearWheelY, seatX, seatY);
    // 三角车架
    var headTubeX = bikeX + 45;
    var headTubeY = bikeY - wheelR - 75;
    graphics.drawLine(framePen, crankX, crankY, seatX, seatY);
    graphics.drawLine(framePen, seatX, seatY, headTubeX, headTubeY);
    graphics.drawLine(framePen, crankX, crankY, headTubeX, headTubeY);
    // 前叉与车把
    graphics.drawLine(framePen, frontWheelX, frontWheelY, handleX, handleY);
    graphics.drawLine(thickPen, headTubeX, headTubeY, handleX, handleY);
    graphics.drawLine(thickPen, handleX, handleY, handleX + 15, handleY - 15);
    // 车座鞍座
    var seatBrush = gdip.solidBrush(0xFF111111);
    graphics.fillEllipse(seatBrush, seatX - 18, seatY - 8, 36, 12);
    seatBrush.delete();

    // --- 左右脚踏板坐标计算 ---
    var p1X = crankX + math.cos(angle) * crankR; // 近侧脚踏
    var p1Y = crankY + math.sin(angle) * crankR;
    var p2X = crankX - math.cos(angle) * crankR; // 远侧脚踏
    var p2Y = crankY - math.sin(angle) * crankR;

    // --- 鹈鹕脚腿绘制闭包 ---
    var drawLeg = function(hipX, hipY, footX, footY, isFront){
        var legPen = isFront ? gdip.pen(0xFFFF8C00, 6) : gdip.pen(0xFFCC7000, 5);
        // 膝盖向前方折弯的运动骨骼
        var midX = (hipX + footX) / 2 + 18;
        var midY = (hipY + footY) / 2 - 5;
        graphics.drawLine(legPen, hipX, hipY, midX, midY);
        graphics.drawLine(legPen, midX, midY, footX, footY);
        legPen.delete();

        // 鸭掌/脚蹼
        var footBrush = isFront ? gdip.solidBrush(0xFFFF8C00) : gdip.solidBrush(0xFFCC7000);
        graphics.fillEllipse(footBrush, footX - 10, footY - 3, 20, 8);
        footBrush.delete();
    };

    var pelicanHipX = seatX + 5;
    var pelicanHipY = seatY - 20 + bob;

    // 1. 远侧腿与曲柄 (位于后方图层)
    drawLeg(pelicanHipX, pelicanHipY, p2X, p2Y, false);
    graphics.drawLine(chromePen, crankX, crankY, p2X, p2Y);
    var pedalBrush = gdip.solidBrush(0xFF333333);
    graphics.fillRect(pedalBrush, p2X - 8, p2Y - 3, 16, 6);

    // --- 2. 鹈鹕身体组件 (Pelican Body) ---
    var bodyX = seatX + 10;
    var bodyY = seatY - 45 + bob;

    // 尾巴羽毛
    var featherBrush = gdip.solidBrush(0xFFE0E6ED);
    graphics.fillEllipse(featherBrush, bodyX - 45, bodyY + 5, 30, 18);

    // 白色羽毛躯干
    var pelicanBodyBrush = gdip.solidBrush(0xFFF8F9FA);
    graphics.fillEllipse(pelicanBodyBrush, bodyX - 25, bodyY - 30, 55, 65);

    // 脖子与头部
    var headX = bodyX + 25;
    var headY = bodyY - 70;
    var neckPen = gdip.pen(0xFFF8F9FA, 22);
    graphics.drawCurve(neckPen, [
        [bodyX + 5, bodyY - 15],
        [bodyX + 15, bodyY - 45],
        [headX - 10, headY + 15],
        [headX, headY]
    ]);
    neckPen.delete();

    // 头部圆润结构
    graphics.fillEllipse(pelicanBodyBrush, headX - 18, headY - 20, 36, 36);

    // --- 3. 核心特征：长嘴喙与巨型喉囊 (Beak & Pouch) ---
    var beakX = headX + 10;
    var beakY = headY - 5;

    // 坚硬的黄色长上嘴喙
    var upperBeakBrush = gdip.solidBrush(0xFFFFB703);
    graphics.fillPolygon(upperBeakBrush, [
        [beakX, beakY - 8],
        [beakX + 75, beakY + 2],
        [beakX + 72, beakY + 8],
        [beakX, beakY + 5]
    ]);
    upperBeakBrush.delete();

    // 嘴尖钩 Hook
    var hookBrush = gdip.solidBrush(0xFFE07A5F);
    graphics.fillCircle(hookBrush, beakX + 74, beakY + 6, 4);
    hookBrush.delete();

    // 巨大膨胀喉囊（带有骑行呼吸晃动感）
    var pouchPuff = math.sin(angle * 3) * 3;
    var pouchBrush = gdip.solidBrush(0xFFF4A261);
    graphics.fillClosedCurve2(pouchBrush, 0.35, [
        [beakX + 5, beakY + 5],
        [beakX + 35, beakY + 45 + pouchPuff],
        [beakX + 68, beakY + 10]
    ]);
    pouchBrush.delete();

    // 嘴缝线
    var mouthPen = gdip.pen(0xFFD4A373, 2);
    graphics.drawLine(mouthPen, beakX, beakY + 2, beakX + 72, beakY + 5);
    mouthPen.delete();

    // 眼睛与高光
    var eyeX = headX + 2;
    var eyeY = headY - 8;
    var eyeOuterBrush = gdip.solidBrush(0xFFFFFFFF);
    var eyePupilBrush = gdip.solidBrush(0xFF111111);
    graphics.fillCircle(eyeOuterBrush, eyeX, eyeY, 6);
    graphics.fillCircle(eyePupilBrush, eyeX, eyeY - 1, 3);
    var eyeGlintBrush = gdip.solidBrush(0xFFFFFFFF);
    graphics.fillCircle(eyeGlintBrush, eyeX - 1, eyeY - 3, 1);
    eyeOuterBrush.delete();
    eyePupilBrush.delete();
    eyeGlintBrush.delete();

    // 翅膀（扶在车把上）
    var wingBrush = gdip.solidBrush(0xFFE9ECEF);
    var wingPen = gdip.pen(0xFFCED4DA, 2);
    var wingPoints = [
        [bodyX - 10, bodyY - 15],
        [bodyX + 20, bodyY - 5],
        [handleX, handleY - 5], // 延伸扶在车把位置
        [bodyX + 15, bodyY + 15]
    ];
    graphics.fillClosedCurve2(wingBrush, 0.35, wingPoints);
    graphics.drawClosedCurve2(wingPen, 0.35, wingPoints);
    wingBrush.delete();
    wingPen.delete();

    // 绅士小礼帽 (Hat)
    var hatBrush = gdip.solidBrush(0xFF1D3557);
    graphics.fillRect(hatBrush, headX - 15, headY - 22, 30, 4); // 帽檐
    graphics.fillRect(hatBrush, headX - 10, headY - 36, 20, 14); // 帽体
    var hatRibbonBrush = gdip.solidBrush(0xFFE63946); // 红色丝带
    graphics.fillRect(hatRibbonBrush, headX - 10, headY - 25, 20, 3);
    hatBrush.delete();
    hatRibbonBrush.delete();

    // --- 4. 近侧脚踏板与腿 (前方图层) ---
    var chainringBrush = gdip.solidBrush(0xFF888888); // 牙盘
    graphics.fillCircle(chainringBrush, crankX, crankY, 16);
    chainringBrush.delete();

    graphics.drawLine(chromePen, crankX, crankY, p1X, p1Y);
    graphics.fillRect(pedalBrush, p1X - 8, p1Y - 3, 16, 6);
    pedalBrush.delete();

    drawLeg(pelicanHipX, pelicanHipY, p1X, p1Y, true);

    // 释放图层常用笔刷
    pelicanBodyBrush.delete();
    featherBrush.delete();
    thickPen.delete();
    framePen.delete();
    chromePen.delete();

    // 还原画板变换矩阵，清除 DPI 缩放
    graphics.resetTransform();
};

// ---------------- 动画定时器更新逻辑 ----------------
winform.canvas.onAnimation = function(prevState, beginning, change, timestamp, duration, deltaMs){
    var deltaSeconds = deltaMs / 1000;
    angle = (angle + pedalSpeed * deltaSeconds) % (math.pi * 2);
    bgPos += backgroundSpeed * deltaSeconds;

    return true; // 保持动画持续循环
};

// 启动动画循环(约 16毫秒/帧)
winform.canvas.startAnimation(16);

winform.onEraseBkgnd  = lambda () 0;

winform.show();
win.loopMessage();

```

