פרויקט גולה: יישום ציור#

פרק זה מרכיב את המדריך לתוכנית שאתם מריצים: יישום ציור קטן. לחצו וגררו לצייר, בחרו צבע, נקו את הקנבס, ושמרו את התוצאה ל־PNG. הוא בנוי על Prima, ערכת הכלים העצמאית המתוחזקת מפרק ה־GUI, מפני שהיא נותנת לנו חלון, אירועי עכבר, ומשטח מחוץ־למסך השומר את עצמו, הכול בלי ערכת כלים חיצונית להתקנה.

העיצוב הוא זה שכל תוכנית ציור משתמשת בו. יש תמונה מחוץ־למסך המחזיקה את הציור בפועל, וחלון המציג אותה. העכבר מצייר אל התמונה שמחוץ־למסך; מטפל הצביעה של החלון פשוט מעתיק את התמונה הזו למסך. הפרדת השניים משמעה שהציור שורד ציורי־מחדש של החלון (שינוי גודל, הסתרה) בחינם, והשמירה טריוויאלית מפני שהציור האמיתי כבר חי באובייקט תמונה.

המודל: תמונה מחוץ־למסך#

הציור עצמו הוא Prima::Image, מצוירת עם אותם פרימיטיבים שהייתם משתמשים בהם על קנבס חלון. אנחנו מתחילים אותה לבנה:

use strict;
use warnings;
use Prima qw(Application Buttons);

my $W = 640;
my $H = 480;

# the real drawing lives here, offscreen
my $canvas = Prima::Image->new(width => $W, height => $H, type => im::RGB);
$canvas->begin_paint;
$canvas->color(cl::White);
$canvas->bar(0, 0, $W, $H);         # bar is Prima's filled rectangle
$canvas->end_paint;

my $brush_color = cl::Black;        # current paint color
my $last;                           # last mouse point while dragging

begin_paint ו־end_paint תוחמים כל ציור אל התמונה שמחוץ־למסך. bar ממלא מלבן (זכרו מפרק ה־GUI שהמלבן הממולא של Prima הוא bar, לא rectangle). אנחנו מחזיקים את צבע המכחול הנוכחי ואת נקודת הגרירה הקודמת כמשתני Perl רגילים.

החלון: תצוגה וקלט#

תפקיד החלון הוא להציג את $canvas ולתרגם תנועת עכבר למשיכות ציור. מטפל ה־onPaint שלו מעתיק את התמונה שמחוץ־למסך למסך עם put_image; מטפלי העכבר שלו מציירים אל התמונה שמחוץ־למסך ומבקשים מהחלון לצייר מחדש.

my $window = Prima::MainWindow->new(
    text => 'pperl paint',
    size => [$W, $H + 40],           # extra height for the buttons

    onPaint => sub {
        my ($self, $canvas_widget) = @_;
        $canvas_widget->put_image(0, 40, $canvas);   # blit the drawing
    },

    onMouseDown => sub {
        my ($self, $btn, $mod, $x, $y) = @_;
        $last = [$x, $y - 40];       # translate for the button strip
    },

    onMouseMove => sub {
        my ($self, $mod, $x, $y) = @_;
        return unless $last;         # only while a button is held
        my @pt = ($x, $y - 40);
        stroke($last, \@pt);
        $last = \@pt;
        $self->repaint;
    },

    onMouseUp => sub { $last = undef; },
);

ההיסט y - 40 שומר רצועה בתחתית החלון עבור הכפתורים; אזור הציור הוא כל מה שמעליה. onMouseMove נורה ברציפות, ולכן אנחנו מציירים מקטע קו קצר מהנקודה הקודמת לנוכחית וזוכרים את הנקודה הנוכחית. גרירה מהירה עדיין מפיקה משיכה מחוברת מפני שאנחנו תמיד מחברים נקודות עוקבות ולא מנקדים פיקסלים בודדים.

המכחול: ציור אל התמונה שמחוץ־למסך#

stroke מצייר מקטע אחד של המכחול אל ה־$canvas שמחוץ־למסך. מפני שהציור הוא Prima::Image רגילה, אנחנו משתמשים באותן קריאות ציור כמו בכל מקום אחר, תחומות ב־begin_paint ו־end_paint:

sub stroke {
    my ($from, $to) = @_;
    $canvas->begin_paint;
    $canvas->color($brush_color);
    $canvas->lineWidth(3);
    $canvas->line($from->[0], $from->[1], $to->[0], $to->[1]);
    # round the joints so fast strokes stay smooth
    $canvas->fill_ellipse($to->[0], $to->[1], 3, 3);
    $canvas->end_paint;
}

הגדרת lineWidth לפני ה־line נותנת משיכה עבה יותר; ה־fill_ellipse הקטן בנקודת הקצה מעגל את החיבורים כך שזיג־זג מהיר אינו מציג פערים בפינות. זהו הפרט הסמוך־להחלקת־קצוות מפרק האלגוריתמים המופיע בפועל: קווים עבים הנפגשים בזווית משאירים חריץ אלא אם מעגלים את המפרק.

פקדים: צבע, ניקוי ושמירה#

שורה של כפתורים לאורך התחתית נותנת לכלי את פעליו. כל אחד הוא Prima::Button עם onClick. כפתורי צבע קובעים את $brush_color; ניקוי צובע מחדש את התמונה שמחוץ־למסך בלבן; שמירה כותבת אותה לדיסק.

my @palette = (
    ['Black', cl::Black], ['Red', cl::Red],
    ['Green', cl::Green], ['Blue', cl::Blue],
);
my $x = 5;
for my $entry (@palette) {
    my ($label, $color) = @$entry;
    $window->insert(Button =>
        text    => $label,
        origin  => [$x, 5],
        size    => [60, 30],
        onClick => sub { $brush_color = $color },
    );
    $x += 65;
}

$window->insert(Button =>
    text => 'Clear', origin => [$x, 5], size => [60, 30],
    onClick => sub {
        $canvas->begin_paint;
        $canvas->color(cl::White);
        $canvas->bar(0, 0, $W, $H);
        $canvas->end_paint;
        $window->repaint;
    },
);
$x += 65;

$window->insert(Button =>
    text => 'Save', origin => [$x, 5], size => [60, 30],
    onClick => sub {
        $canvas->save('painting.png')
            and print "saved painting.png\n";
    },
);

Prima->run;

insert(Button => ...) מוסיף וידג’ט ילד לחלון. $canvas->save כותב את התמונה שמחוץ־למסך ל־PNG, פורמט המוסק מן הסיומת, ומחזיר אמת בהצלחה. מפני שהציור חי באובייקט תמונה כל הזמן, השמירה היא קריאה יחידה בלי דבר לשחזר.

הרצתו#

הרכיבו את החלקים לעיל לקובץ אחד והריצו אותו עם pperl. חלון נפתח עם קנבס לבן ורצועת כפתורים; גררו לצייר, לחצו על צבע כדי להחליף, Clear לאיפוס, Save לכתיבת painting.png. כל חלק מתחקה בחזרה דרך המדריך: התמונה שמחוץ־למסך והפרימיטיבים שלה מפרק ה־GUI, חיבור־המשיכות מפרק האלגוריתמים, מודל הצבע מיסודות, ונתיב השמירה שהוא מתודה אחת מפני שהציור תמיד היה תמונה אמיתית.

היכן חוט ה־FFI היה נכנס#

פרויקט גולה זה נשאר על מחסנית ה־CPAN המתוחזקת, ובכוונה: הוא נועד לרוץ היום, ללא שינוי. חוט ה־FFI נכנס כשאתם גדלים מעבר למה שהמחסנית מציעה, למשל כשרוצים ספריית מנוע־מכחול של C בלי כריכת Perl למשיכות רגישות־לחץ. זה מושג בדיוק כפי שתצוגת Cairo הראתה: כריכה אצורה המציגה את ספריית ה־C כאובייקטי Perl. עבור תוכנית ציור, הפרימיטיבים של Prima עצמה הם יותר מספיקים, והבחירה ההנדסית הכנה היא להשתמש בהם. פרויקט הגולה הבא הוא היכן שחזית ה־FFI באמת נכנסת לתמונה, מפני שחלונות ואירועים בזמן־אמת דוחפים מעבר למה שהכריכות הנוכחיות מגיעות אליו.