aardio 文档

aardio 范例: canvas - 桌面圆形时钟

//canvas - 桌面圆形时钟
//canvas 转为透明分层窗口(orphanWindow(true))后可以画任意形状的窗口
import win.ui;
import time.lunar;
/*DSG{{*/
var winform = win.form(text="桌面圆形时钟";left=-20;top=-40;right=276;bottom=239;border="none";topmost=1)
winform.add(
clock={cls="canvas";left=0;top=0;right=277;bottom=240;anchors={left=1;top=1;right=1;bottom=1};z=1}
)
/*}}*/

/*
动静分层:
- 表盘是静态层,按尺寸与 DPI 缓存成位图,每帧整块复制。
- 指针与日期是动态层,每帧重画。

读时依赖的是表盘三分区而不是简单线段:
- 时针止于小时数字内沿,尺寸最短、面积最大。
- 分针穿过数字环,止于数字与分钟刻度之间,不接触刻度。
- 秒针最细最长,扫过刻度并保留配重尾。
*/

// 按暖白表盘半径统一计算边框、刻度、数字环与三针长度,避免表盘层和指针层各自约定。
var clockLayout = function(width,height,dpi){
    var centerX = width/2;
    var centerY = height/2;
    var outerRadius = math.min(centerX,centerY)-2*dpi;
    var rimRadius = outerRadius-1.4*dpi;
    var faceRadius = outerRadius-4.4*dpi;
    var tickOuter = faceRadius-6.2*dpi;
    var tickMinuteLen = 4.4*dpi;
    var numberRadius = faceRadius*0.72;

    // 分针穿过数字环,但停在数字与分钟刻度之间,不接触刻度。
    var tickInner = tickOuter-tickMinuteLen;
    var minuteLen = numberRadius+(tickInner-numberRadius)*0.55;

    return {
        centerX = centerX;
        centerY = centerY;
        outerRadius = outerRadius;
        rimRadius = rimRadius;
        faceRadius = faceRadius;
        tickOuter = tickOuter;
        tickMinuteLen = tickMinuteLen;
        tickHourLen = 8.6*dpi;
        tickQuarterLen = 11.2*dpi;
        numberRadius = numberRadius;
        hourLen = faceRadius*0.56;
        minuteLen = minuteLen;
        secondLen = tickOuter+0.6*dpi;
        secondTail = faceRadius*0.17;
        hourHalfW = 3.85*dpi;
        minuteHalfW = 2.45*dpi;
        secondWidth = 1.05*dpi;
        handTail = 3.6*dpi;
        hubOuter = 5.8*dpi;
        hubAccent = 3.5*dpi;
        hubPin = 1.55*dpi;
        weightR = 3.3*dpi;
    }
}

// 将 12 小时周期内的秒数转为画板角度。时针会把分、秒投影进小时区间。
var clockAngles = function(clockSeconds){
    return {
        hour = clockSeconds/3600*math.pi/6-math.pi/2;
        minute = (clockSeconds%3600)/60*math.pi/30-math.pi/2;
        second = (clockSeconds%60)*math.pi/30-math.pi/2;
    }
}

// 剜削式指针:腰部最宽,针尖收窄为尖点,便于对准刻度。
var fillTaperedHand = function(graphics,brush,cx,cy,angle,length,halfWidth,tail){
    var ca,sa = math.cos(angle),math.sin(angle);
    var nx,ny = -sa,ca;
    // 先保持稳定宽度,只在最后一段收成尖点,避免分针大半段细成像秒针。
    var waist = length*0.12;
    var neck = length*0.82;
    var tailW = halfWidth*0.78;
    var waistW = halfWidth;
    var neckW = halfWidth*0.55;

    graphics.fillPolygon(brush,0/*_FillModeAlternate*/,[ 
        [cx-tail*ca+nx*tailW,cy-tail*sa+ny*tailW],
        [cx+waist*ca+nx*waistW,cy+waist*sa+ny*waistW],
        [cx+neck*ca+nx*neckW,cy+neck*sa+ny*neckW],
        [cx+length*ca,cy+length*sa],
        [cx+neck*ca-nx*neckW,cy+neck*sa-ny*neckW],
        [cx+waist*ca-nx*waistW,cy+waist*sa-ny*waistW],
        [cx-tail*ca-nx*tailW,cy-tail*sa-ny*tailW]
    ]);
}

// -------------------- 静态表盘层 --------------------
var drawFace = function(graphics,layout,dpi,numberFont){
    /*
    边框使用三层实心圆构成,避免粗描边在圆周两侧裁切:
    深青灰外框负责收口,暖金内圈增加层次,最内层是暖白表盘。
    */
    var bezelBrush = gdip.solidBrush(0xFF26363D);
    var rimBrush = gdip.solidBrush(0xFFC7A45B);
    var faceBrush = gdip.solidBrush(0xFFF7F4EB);

    graphics.fillCircle(bezelBrush,layout.centerX,layout.centerY,layout.outerRadius);
    graphics.fillCircle(rimBrush,layout.centerX,layout.centerY,layout.rimRadius);
    graphics.fillCircle(faceBrush,layout.centerX,layout.centerY,layout.faceRadius);

    bezelBrush.delete();
    rimBrush.delete();
    faceBrush.delete();

    var minuteMarkPen = gdip.pen(0xFF9AA1A3,0.75*dpi);
    var hourMarkPen = gdip.pen(0xFF34444B,1.85*dpi);
    var quarterMarkPen = gdip.pen(0xFFAD7B2F,2.25*dpi);
    for(i,pen in [minuteMarkPen,hourMarkPen,quarterMarkPen]){
        pen.startCap = 2/*_LineCapRound*/;
        pen.endCap = 2/*_LineCapRound*/;
    }

    for(i=0;59;1){
        var angle = i*math.pi/30-math.pi/2;
        var tickLength = layout.tickMinuteLen;
        var markPen = minuteMarkPen;

        if(i%15==0){
            tickLength = layout.tickQuarterLen;
            markPen = quarterMarkPen;
        }
        elseif(i%5==0){
            tickLength = layout.tickHourLen;
            markPen = hourMarkPen;
        }

        graphics.drawLine(markPen,
            layout.centerX+(layout.tickOuter-tickLength)*math.cos(angle),
            layout.centerY+(layout.tickOuter-tickLength)*math.sin(angle),
            layout.centerX+layout.tickOuter*math.cos(angle),
            layout.centerY+layout.tickOuter*math.sin(angle)
        );
    }

    minuteMarkPen.delete();
    hourMarkPen.delete();
    quarterMarkPen.delete();

    var numberBrush = gdip.solidBrush(0xFF34444B);
    var strFormat = gdip.stringFormat();
    strFormat.align = 1/*_StringAlignmentCenter*/;
    strFormat.lineAlign = 1/*_StringAlignmentCenter*/;
    var boxW = layout.faceRadius*0.32;
    var boxH = layout.faceRadius*0.20;

    for(h=1;12;1){
        var hourAngle = (h%12)*math.pi/6-math.pi/2;
        var textX = layout.centerX+layout.numberRadius*math.cos(hourAngle);
        var textY = layout.centerY+layout.numberRadius*math.sin(hourAngle);
        graphics.drawString(tostring(h),numberFont,
            ::RECTF(textX-boxW/2,textY-boxH/2,boxW,boxH),
            strFormat,numberBrush
        );
    }

    numberBrush.delete();
    strFormat.delete();
}

// 表盘按像素尺寸与 DPI 缓存为 PARGB 位图(canvas 画布的格式,复制最快),尺寸或 DPI 改变时重建。
var faceLayer,faceKey;
var ensureFaceLayer = function(width,height,dpi){
    var key = width ++ "x" ++ height ++ "@" ++ dpi;
    if(faceLayer && faceKey === key) return faceLayer;
    if(faceLayer) faceLayer.delete();

    // 字体由调用方自己准备:数字用半粗体,字号随 DPI 缩放。
    var numberFont = gdip.font("Segoe UI Semibold",16*dpi,0/*_FontStyleRegular*/,2/*_UnitPixel*/)
        : gdip.font("Segoe UI",16*dpi,1/*_FontStyleBold*/,2/*_UnitPixel*/);

    faceLayer = gdip.bitmap(width,height,0xE200B/*_GdipPixelFormat32bppPARGB*/);
    var graphics = faceLayer.getGraphics();
    graphics.smoothingMode = 4/*_SmoothingModeAntiAlias*/;
    graphics.textRenderingHint = 4/*_TextRenderingHintAntiAlias*/;
    drawFace(graphics,clockLayout(width,height,dpi),dpi,numberFont);
    graphics.delete();
    numberFont.delete();

    faceKey = key;
    return faceLayer;
}

// 分层窗口模式下画布底色是全透明的,表盘圆形之外保持透明。
winform.clock.onDrawBackground = function(graphics,rc){
    var dpiX,dpiY = owner.dpiScale(1,1);
    var layer = ensureFaceLayer(rc.width,rc.height,math.min(dpiX,dpiY));

    // 表盘缓存与画布一样大,用 SourceCopy 整块复制(连同透明区域),比默认的混合模式快得多。
    graphics.compositingMode = 1/*_CompositingModeSourceCopy*/;
    graphics.drawImageRect(layer,0,0,rc.width,rc.height);
    graphics.compositingMode = 0/*_CompositingModeSourceOver*/;
}

// -------------------- 动态指针层 --------------------
var hourHandBrush,minuteHandBrush,secondHandPen;
var centerOuterBrush,centerAccentBrush,centerPinBrush;
var calendarFont,calendarDateBrush,calendarLunarBrush,calendarFormat;
var handDpiX,handDpiY = 0,0;

var releaseHandResources = function(){
    for(i,obj in [hourHandBrush,minuteHandBrush,secondHandPen,centerOuterBrush,centerAccentBrush,
        centerPinBrush,calendarFont,calendarDateBrush,calendarLunarBrush,calendarFormat]){
        obj.delete();
    }

    hourHandBrush = null;
    minuteHandBrush = null;
    secondHandPen = null;
    centerOuterBrush = null;
    centerAccentBrush = null;
    centerPinBrush = null;
    calendarFont = null;
    calendarDateBrush = null;
    calendarLunarBrush = null;
    calendarFormat = null;
}

var ensureHandResources = function(dpiX,dpiY){
    if(hourHandBrush
        && math.abs(handDpiX-dpiX)<0.001
        && math.abs(handDpiY-dpiY)<0.001) return;

    releaseHandResources();

    hourHandBrush = gdip.solidBrush(0xFF24343B);
    minuteHandBrush = gdip.solidBrush(0xFF3A4A51);
    secondHandPen = gdip.pen(0xFFDF5145,1.02*math.min(dpiX,dpiY));
    secondHandPen.startCap = 2/*_LineCapRound*/;
    secondHandPen.endCap = 2/*_LineCapRound*/;

    centerOuterBrush = gdip.solidBrush(0xFF26363D);
    centerAccentBrush = gdip.solidBrush(0xFFC7A45B);
    centerPinBrush = gdip.solidBrush(0xFFDF5145);

    // 日期文字比表盘数字小一档,并使用半透明深青灰色,清楚但不抢主视觉。
    calendarFont = gdip.font("Microsoft YaHei UI",10*math.min(dpiX,dpiY),0/*_FontStyleRegular*/,2/*_UnitPixel*/);
    calendarDateBrush = gdip.solidBrush(0xE0445258);
    calendarLunarBrush = gdip.solidBrush(0xE0445258);
    calendarFormat = gdip.stringFormat();
    calendarFormat.align = 1/*_StringAlignmentCenter*/;
    calendarFormat.lineAlign = 1/*_StringAlignmentCenter*/;

    handDpiX,handDpiY = dpiX,dpiY;
}

// 将系统时间转换为 12 小时周期内的秒数。
var readSystemClock = function(){
    var now = time();
    return (now.hour%12)*3600+now.minute*60+now.second+now.milliseconds/1000;
}

var clockSeconds = readSystemClock();
var clockSyncElapsed = 0;

// 公历、星期和农历只每 5 秒读取一次,绘制时直接使用缓存文本。
var calendarDateText,calendarLunarText;
var updateCalendarText = function(){
    var lunar = time.lunar();
    calendarDateText = tostring(lunar,"%Y/%m/%d %a");
    calendarLunarText = tostring(lunar,"%Lm %Ld");
}
updateCalendarText();

winform.clock.onDrawForeground = function(graphics,rc){
    var dpiX,dpiY = owner.dpiScale(1,1);
    var dpi = math.min(dpiX,dpiY);
    ensureHandResources(dpiX,dpiY);

    var layout = clockLayout(rc.width,rc.height,dpi);
    var angles = clockAngles(clockSeconds);

    // 日期放在表盘中下部,先画日期,再由指针自然覆盖。
    graphics.textRenderingHint = 4/*_TextRenderingHintAntiAlias*/;
    var calendarBoxW = layout.faceRadius*1.25;
    var calendarBoxH = 13*dpi;
    var calendarDateY = layout.centerY+layout.faceRadius*0.33;
    var calendarLunarY = calendarDateY+15*dpi;
    graphics.drawString(calendarDateText,calendarFont,
        ::RECTF(layout.centerX-calendarBoxW/2,calendarDateY-calendarBoxH/2,calendarBoxW,calendarBoxH),
        calendarFormat,calendarDateBrush
    );
    graphics.drawString(calendarLunarText,calendarFont,
        ::RECTF(layout.centerX-calendarBoxW/2,calendarLunarY-calendarBoxH/2,calendarBoxW,calendarBoxH),
        calendarFormat,calendarLunarBrush
    );

    fillTaperedHand(graphics,hourHandBrush,layout.centerX,layout.centerY,
        angles.hour,layout.hourLen,layout.hourHalfW,layout.handTail);
    fillTaperedHand(graphics,minuteHandBrush,layout.centerX,layout.centerY,
        angles.minute,layout.minuteLen,layout.minuteHalfW,layout.handTail);

    var secondTipX = layout.centerX+layout.secondLen*math.cos(angles.second);
    var secondTipY = layout.centerY+layout.secondLen*math.sin(angles.second);
    var secondTailX = layout.centerX-layout.secondTail*math.cos(angles.second);
    var secondTailY = layout.centerY-layout.secondTail*math.sin(angles.second);
    graphics.drawLine(secondHandPen,secondTailX,secondTailY,secondTipX,secondTipY);
    graphics.fillCircle(centerPinBrush,secondTailX,secondTailY,layout.weightR);

    graphics.fillCircle(centerOuterBrush,layout.centerX,layout.centerY,layout.hubOuter);
    graphics.fillCircle(centerAccentBrush,layout.centerX,layout.centerY,layout.hubAccent);
    graphics.fillCircle(centerPinBrush,layout.centerX,layout.centerY,layout.hubPin);
}

/*
onAnimation 返回后画布自动重绘。clockSeconds 按实际 deltaMs 连续推进,
每 5 秒重新读取系统时间,避免长期累计误差并响应系统校时。
*/
winform.clock.onAnimation = function(prevState,beginning,change,timestamp,duration,deltaMs){
    if(deltaMs>0){
        clockSeconds = (clockSeconds+deltaMs/1000)%43200;
        clockSyncElapsed += deltaMs;

        if(clockSyncElapsed>=5000){
            clockSeconds = readSystemClock();
            clockSyncElapsed = 0;
            updateCalendarText();
        }
    }

    // 仅全等于 null(或无返回值)才停止动画。
    return true;
}

// 画布转换为透明分层窗口,悬浮在原位置并跟随父窗体移动。
winform.clock.orphanWindow(true);

// 父窗体转为完全透明的分层窗口,只负责定位与拖动。
import win.ui.layered;
win.ui.layered(winform);

// 允许从表盘上拖动整个时钟。
winform.clock.onMouseDown = function(wParam,lParam){
    winform.hitCaption();
}

// 右键菜单。
import win.ui.menu;
winform.clock.onContextMenu = function(x,y){
    var menu = win.ui.popmenu(winform);
    menu.add("退出",function(){
        winform.close();
    });
    menu.popup();
}

winform.onDestroy = function(){
    releaseHandResources();
    if(faceLayer) faceLayer.delete();
}

winform.show();

// 约 30 FPS(canvas 按显示器刷新周期的整数倍降频)。刷新频率只影响平滑度,走时由 deltaMs 决定。
winform.clock.startAnimation(33);

win.loopMessage();

Markdown 格式