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