aardio 文档

aardio 范例: plus 控件 - GDI+ 绘图基础,按住鼠标左键可旋转方块

/*
此示例演示在 plus 控件内使用 GDI+ 实现简单 3D 绘图或小游戏绘图的方法。
GDI+ 适合静态场景配合少量动态对象,不适合复杂三维、大量单位或大面积高帧率特效。

示例采用小游戏常用的绘图约定:
1. 游戏状态、投影与命中统一使用 96-DPI 逻辑坐标。
2. 绘制时使用 save → scale(dpiX,dpiY) → restore 切换到逻辑坐标。
3. 动画速度使用“逻辑量/秒”,按 deltaMs 更新,不依赖实际帧率。
4. 不变的 GDI+ 对象复用;静态背景按当前像素尺寸与 DPI 缓存。
*/
import win.ui;
/*DSG{{*/
var winform = win.form(text="plus 控件 - GDI+ 绘图基础,按住鼠标左键可旋转方块";right=759;bottom=469;border="dialog frame";max=false)
winform.add(
plus={cls="plus";left=-1;top=0;right=760;bottom=470;anchors={left=1;top=1;right=1;bottom=1};notify=1;z=1}
)
/*}}*/

//以下几何数据全部使用 96-DPI 逻辑坐标。
var vertices = [
    [-50,-50,-50],[50,-50,-50],[50,50,-50],[-50,50,-50],
    [-50,-50,50],[50,-50,50],[50,50,50],[-50,50,50]
];

//立方体的面(顶点索引)。
var faces = [
    [1,2,3,4],[5,6,7,8],[1,2,6,5],
    [4,3,7,8],[1,4,8,5],[2,3,7,6]
];

var faceColors = [
    0xAA00D4FF,0xAA7209B7,0xAAF72585,
    0xAA4CC9F0,0xAA4361EE,0xAA3A0CA3
];

var angleX,angleY = 0.5,0.5;
var dragging = false;
var lastX,lastY = 0,0;
var smoothedDeltaMs = 16.67;

//自动旋转速度,单位为弧度/秒。
var autoSpeedX,autoSpeedY = 0.33,0.67;

//同一帧的三角函数值只计算一次,再用于全部顶点。
var rotate3D = function(point,cosX,sinX,cosY,sinY){
    var x,y,z = table.unpack(point);
    var y1 = y*cosX-z*sinX;
    var z1 = y*sinX+z*cosX;
    var x2 = x*cosY+z1*sinY;
    var z2 = -x*sinY+z1*cosY;
    return [x2,y1,z2];
}

//3D 投影到 2D,返回值仍是逻辑坐标。
var project = function(point,centerX,centerY,distance){
    var x,y,z = table.unpack(point);
    var projectionScale = distance/(distance+z);
    return [centerX+x*projectionScale,centerY+y*projectionScale,z];
}

//不变的画笔、画刷、字体与排版对象应缓存复用,避免逐帧创建。
var edgePen = gdip.pen(0xFFFFFFFF,1);
var curvePen = gdip.pen(0x40FFFFFF,2);
var pieBrush = gdip.solidBrush(0x30FFFF00);
var hudFont = gdip.font("Tahoma",14/*,0*/); // 字体样式(参数 @3)默认为 0/*_FontStyleRegular*/, 参数 @4 默认为 2/*_UnitPixel*/
var hudFormat = gdip.stringFormat();
var hudBrush = gdip.solidBrush(0xFFFFFFFF);
var faceBrushes = [];
for(i=1;#faceColors){
    faceBrushes[i] = gdip.solidBrush(faceColors[i]);
}

/*
静态背景使用当前 DPI 下的原尺寸位图缓存。
仅当控件像素尺寸或 DPI 改变时重建,动画帧只复制位图并绘制动态立方体。
*/
var staticLayer;
var staticWidth,staticHeight,staticDpiX,staticDpiY;

var ensureStaticLayer = function(pixelWidth,pixelHeight,dpiX,dpiY){
    if(pixelWidth<=0 || pixelHeight<=0) return;
    if(staticLayer
        && staticWidth==pixelWidth && staticHeight==pixelHeight
        && staticDpiX==dpiX && staticDpiY==dpiY){
        return;
    }

    if(staticLayer) staticLayer.delete();
    staticLayer = gdip.bitmap(pixelWidth,pixelHeight);

    var layerGraphics = gdip.graphics(staticLayer);
    var saved = layerGraphics.save();
    layerGraphics.smoothingMode = 4/*_SmoothingModeAntiAlias*/;
    layerGraphics.scale(dpiX,dpiY);

    var width = pixelWidth/dpiX;
    var height = pixelHeight/dpiY;
    var backgroundBrush = gdip.lineBrush(
        ::POINTF(0,0),::POINTF(width,height),
        0xFF1A1A2E,0xFF0F0F1A,0
    );
    layerGraphics.fillRect(backgroundBrush,0,0,width,height);
    backgroundBrush.delete();

    //静态装饰也进入缓存,不在每个动画帧重复构造与绘制。
    layerGraphics.drawCurve(curvePen,[
        0,height-50,
        width*0.26,height-100,
        width*0.66,height-20,
        width,height-60
    ]);
    layerGraphics.fillPie(pieBrush,width-100,20,80,80,0,270);

    layerGraphics.restore(saved);
    layerGraphics.delete();

    staticWidth,staticHeight = pixelWidth,pixelHeight;
    staticDpiX,staticDpiY = dpiX,dpiY;
}

//鼠标消息坐标是设备像素,进入游戏状态前先转换为逻辑坐标。
var getLogicalMousePos = function(lParam){
    var x,y = win.getMessagePos(lParam);
    var dpiX,dpiY = winform.dpiScale(1,1);
    return x/dpiX,y/dpiY;
}

winform.plus.onMouseDown = function(wParam,lParam){
    lastX,lastY = getLogicalMousePos(lParam);
    dragging = true;
}

winform.plus.onMouseDrag = function(wParam,lParam){
    if(!dragging) return;

    var x,y = getLogicalMousePos(lParam);
    angleY += (x-lastX)*0.01;
    angleX += (y-lastY)*0.01;
    lastX,lastY = x,y;

    //startAnimation 会统一刷新;不要在高频鼠标事件中额外密集 redraw。
}

winform.plus.onMouseUp = function(wParam,lParam){
    dragging = false;
}

//可选的自绘步骤 1:绘制背景。
winform.plus.onDrawBackground = function(graphics,rc,backgroundColor,color){
    var dpiX,dpiY = winform.dpiScale(1,1);
    ensureStaticLayer(rc.width,rc.height,dpiX,dpiY);
    if(staticLayer) graphics.drawImage(staticLayer,rc.left,rc.top);

    return true;//阻止绘制默认背景色或背景图像。
}

//可选的自绘步骤 2:绘制动态前景。
winform.plus.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){
    /*
    plus 自绘事件的 graphics 是双缓冲内存画布。
    rc、foregroundRect 与鼠标消息坐标默认都是 DPI 缩放后的设备像素。
    */
    var dpiX,dpiY = winform.dpiScale(1,1);
    rc.scale(1/dpiX,1/dpiY); // rc 为解耦副本(defensive copy),放心 In-place 修改,不影响上游

    //局部变换必须严格成对保存与恢复,不要 resetTransform 破坏调用方状态。
    var saved = graphics.save();
    graphics.smoothingMode = 4/*_SmoothingModeAntiAlias*/;
    graphics.scale(dpiX,dpiY);

    var centerX,centerY = rc.width/2,rc.height/2;
    var distance = 300;
    var cosX,sinX = math.cos(angleX),math.sin(angleX);
    var cosY,sinY = math.cos(angleY),math.sin(angleY);

    //3D 顶点计算。
    var projected = [];
    for(i=1;#vertices){
        var rotated = rotate3D(vertices[i],cosX,sinX,cosY,sinY);
        projected[i] = project(rotated,centerX,centerY,distance);
    }

    //按平均 Z 深度排序,远处的面先画。
    var faceDepths = [];
    for(i=1;#faces){
        var face = faces[i];
        var averageZ = 0;
        for(j=1;#face){
            averageZ += projected[face[j]][3];
        }
        faceDepths[i] = {index=i;depth=averageZ/#face};
    }
    table.sort(faceDepths,lambda(next) owner.depth>next.depth);

    for(i=1;#faceDepths){
        var faceIndex = faceDepths[i].index;
        var face = faces[faceIndex];
        var points = [];
        for(j=1;#face){
            var p = projected[face[j]];
            points[j] = ::POINTF(p[1],p[2]);
        }

        //第一个面演示路径渐变;路径随投影变化,因此只保留为当帧临时对象。
        if(faceIndex==1){
            var path = gdip.path();
            path.addPolygon(points);
            var pathBrush = gdip.pathGradientBrush(path);
            pathBrush.centerColor = 0xFFFFFFFF;
            pathBrush.surroundColors = [0xFF00D4FF];
            graphics.fillPath(pathBrush,path);
            pathBrush.delete();
            path.delete();
        }
        else {
            graphics.fillPolygon(faceBrushes[faceIndex],points);
        }

        graphics.drawPolygon(edgePen,points);
    }

    graphics.restore(saved);
    return true;//阻止绘制默认前景色或前景图像。
}

//可选的自绘步骤 3:在前景之后绘制边框。
winform.plus.onDrawBorder = function(graphics,rc,foregroundRect,border){

}

//可选的自绘步骤 4:在边框之后绘制文本。
winform.plus.onDrawString = function(graphics,text,font,rectf,strFormat,brush){
    var dpiX,dpiY = winform.dpiScale(1,1);
    var rc = winform.plus.getClientRect();
    var width = rc.width/dpiX;
    var fps = 1000/math.max(smoothedDeltaMs,1);
    var info = string.format("旋转角度: X=%.2f, Y=%.2f  |  %.0f FPS",angleX,angleY,fps);

    var saved = graphics.save();
    graphics.textRenderingHint = 4/*_TextRenderingHintAntiAlias*/;
    graphics.scale(dpiX,dpiY);

    //画板已经缩放,_UnitPixel 字体与此处矩形都直接使用逻辑像素。
    graphics.drawString(info,hudFont,::RECTF(20,20,math.max(120,width-40),40),hudFormat,hudBrush);

    graphics.restore(saved);
    return true;//阻止绘制默认文本与图标字体。
}

//onAnimation 基于窗口定时器并自动重绘;动画速度按 deltaMs 计算;限制异常大帧间隔,避免窗口阻塞后角度突跳。
winform.plus.onAnimation = function(prevState,beginning,change,timestamp,duration,deltaMs){
    var clampedDeltaMs = math.min(deltaMs,50);
    var dt = clampedDeltaMs/1000;

    if(deltaMs>0){
        smoothedDeltaMs = smoothedDeltaMs*0.9+deltaMs*0.1;
    }

    if(!dragging){
        angleX += autoSpeedX*dt;
        angleY += autoSpeedY*dt;
    }

    //长期运行时将角度限制在一圈内,避免浮点数无限增大。
    var fullTurn = math.pi*2;
    angleX = angleX%fullTurn;
    angleY = angleY%fullTurn;

    return true; //返回值全等于 null(或无返回值)才停止动画。下次回调时 prevState 接收此返回值
}

/*
约 60 FPS 的目标间隔。实际帧率可能因绘图耗时和系统调度而变化,
但 deltaMs 保证旋转速度仍按真实经过时间计算。
*/
winform.plus.startAnimation(16);
winform.plus.setFocus();

/**
    //其他常用事件模板。
    winform.plus.onMouseMove = function(wParam,lParam){
        //高频事件只更新状态,由 onAnimation 统一刷新。
    }
    winform.plus.onMouseDoubleClick = function(wParam,lParam){}
    winform.plus.onMouseWheel = function(flags,delta,lParam){
        //可在这里修改逻辑状态中的投影距离或模型大小。
    }
    winform.plus.onMouseHWheel = function(flags,delta,lParam){}
    winform.plus.onMouseEnter = function(wParam,lParam){}
    winform.plus.onMouseLeave = function(wParam,lParam){}
    winform.plus.onMouseHover = function(wParam,lParam){}
    winform.plus.onRightMouseDown = function(wParam,lParam){}
    winform.plus.onRightMouseUp = function(wParam,lParam){}

    winform.plus.dlgCode = 4/*_DLGC_WANTALLKEYS*/
    winform.plus.onKeyDown = function(keyCode,lParam,repeat){
        //设置 dlgCode 后可接收按键。
    }
    winform.plus.onKeyUp = function(keyCode,lParam){}
    winform.plus.onContextMenu = function(x,y){}
**/

//缓存的原生 GDI+ 对象在窗口销毁时统一释放。
winform.onDestroy = function(){
    if(staticLayer) staticLayer.delete();
    edgePen.delete();
    curvePen.delete();
    pieBrush.delete();
    hudFont.delete();
    hudFormat.delete();
    hudBrush.delete();

    for(i,faceBrush in faceBrushes){
        faceBrush.delete();
    }
}

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