Θεμέλια: pixel, διαδρομές και χρώμα#
Πριν από οποιαδήποτε βιβλιοθήκη, τέσσερις ιδέες στηρίζουν όλο το θέμα: η διαφορά ανάμεσα σε ένα raster και ένα vector, το σύστημα συντεταγμένων μέσα στο οποίο ζει ένα σχέδιο, τι είναι στην πραγματικότητα ένα χρώμα από τη στιγμή που πρέπει να το αποθηκεύσετε, και πώς δύο χρώματα συνδυάζονται όταν το ένα είναι εν μέρει διαφανές. Κάθε κεφάλαιο μετά από αυτό στηρίζεται σε αυτά χωρίς να τα ξαναεξηγεί. Τίποτα από αυτά δεν είναι ειδικό για την Perl, αλλά όλα τα παραδείγματα είναι σε Perl, γιατί το ζητούμενο είναι να γίνουν οι ιδέες απτές σε κώδικα που μπορείτε να εκτελέσετε.
Raster έναντι vector#
Μια εικόνα raster είναι ένα πλέγμα από χρωματιστά κελιά. Έχει σταθερό πλάτος και ύψος σε pixel, και κάθε pixel κρατά ένα χρώμα. Η μεγέθυνσή της πέρα από την ανάλυσή της δεν επινοεί τίποτα: τα κελιά απλώς μεγαλώνουν. Οι φωτογραφίες, τα στιγμιότυπα οθόνης και οτιδήποτε έχει συνεχή τόνο είναι raster. Τα Imager, GD και ImageMagick είναι εργαλεία raster.
Μια εικόνα vector είναι ένα σύνολο σχημάτων που περιγράφονται μαθηματικά: «μια γραμμή από εδώ ως εκεί», «ένας κύκλος αυτής της ακτίνας», «γέμισε αυτή τη διαδρομή». Δεν υπάρχει ανάλυση μέχρι να τη σχεδιάσετε, οπότε κλιμακώνεται καθαρά σε οποιοδήποτε μέγεθος. Οι γραμματοσειρές, τα λογότυπα και τα διαγράμματα είναι vector. Τα Cairo και SVG είναι εργαλεία vector.
Τα δύο συναντιούνται στη rasterization (μετατροπή σε ψηφίδες): τη μετατροπή μιας περιγραφής vector σε pixel. Μια βιβλιοθήκη vector πρέπει και πάλι να αποφασίσει ποια pixel αγγίζει μια διαγώνια γραμμή, και αυτή η απόφαση είναι ο πρώτος αλγόριθμος στο επόμενο κεφάλαιο. Οτιδήποτε σχεδιάζετε καταλήγει ως pixel σε μια οθόνη· το μόνο ερώτημα είναι αν δεσμεύεστε σε μια ανάλυση νωρίς (raster) ή αργά (vector).
Ένα raster στην Perl είναι, στην απλούστερη μορφή του, ένας πίνακας από γραμμές:
my $w = 4;
my $h = 3;
# each pixel is [r, g, b], each channel 0..255
my @img = map { [ map { [0, 0, 0] } 1 .. $w ] } 1 .. $h;
$img[1][2] = [255, 0, 0]; # row 1, column 2, red
Αυτή η εικόνα του ένθετου πίνακα είναι όλο κι όλο ένα raster. Οι πραγματικές βιβλιοθήκες αποθηκεύουν το ίδιο πλέγμα σε έναν πακεταρισμένο ενταμιευτή C για ταχύτητα, αλλά το νοητικό μοντέλο δεν αλλάζει: ένα pixel διευθυνσιοδοτείται από μια ακέραια γραμμή και στήλη, και κρατά ένα χρώμα.
Το σύστημα συντεταγμένων#
Οι συντεταγμένες των γραφικών δεν είναι οι συντεταγμένες του μαθήματος των μαθηματικών. Η αρχή (0, 0) είναι η επάνω αριστερή γωνία, το x αυξάνεται προς τα δεξιά, και το y αυξάνεται προς τα κάτω. Αυτό παγιδεύει τον καθένα μία φορά: ένα μεγαλύτερο y είναι πιο χαμηλά στην οθόνη, όχι πιο ψηλά.
# (0,0) top-left x ->
# +-----------------
# | (0,0) (w-1, 0)
# y |
# | |
# v | (0, h-1) (w-1, h-1)
Ένα pixel είναι ένα μικρό τετράγωνο, και υπάρχει μια διαρκής αμφισημία για το αν η συντεταγμένη (3, 5) ονομάζει τη γωνία του τετραγώνου ή το κέντρο του. Οι βιβλιοθήκες raster αντιμετωπίζουν συνήθως τις ακέραιες συντεταγμένες ως κέντρα pixel· οι βιβλιοθήκες vector τις αντιμετωπίζουν ως τις γραμμές του πλέγματος ανάμεσα στα pixel, γι” αυτό και μια κατακόρυφη γραμμή πλάτους μίας μονάδας στο x = 3 μπορεί να βγει να πατάει σε δύο στήλες pixel με τη μισή ένταση. Όταν μια λεπτή γραμμή ή η ακμή ενός ορθογωνίου φαίνεται θολή ή μετατοπισμένη κατά ένα pixel, αυτός είναι σχεδόν πάντα ο λόγος, και η διόρθωση είναι μια μετατόπιση μισού pixel. Τα κεφάλαια που το συναντούν το αναφέρουν.
Τα μεγέθη και οι θέσεις είναι ακέραιοι στον χώρο raster και πραγματικοί αριθμοί στον χώρο vector. Αυτή είναι η πρακτική διαφορά που νιώθετε: στο Imager ορίζετε το pixel (120, 47)· στο Cairo μπορείτε να μετακινηθείτε στο (120.5, 47.25) και ο rasterizer υπολογίζει την κάλυψη.
Χρώμα: τι κρατά ένα pixel#
Ένα χρώμα σε μια οθόνη είναι τρεις αριθμοί: πόσο κόκκινο, πράσινο και μπλε φως να εκπεμφθεί. Προσθέστε και τα τρία με πλήρη ένταση και παίρνετε λευκό· αφήστε τα όλα σβηστά και παίρνετε μαύρο. Αυτό είναι το μοντέλο RGB, και είναι προσθετικό γιατί προσθέτετε φως, δεν αναμειγνύετε μπογιά.
Κάθε κανάλι αποθηκεύεται σχεδόν πάντα σε 8 bit, οπότε 0 .. 255 ανά κανάλι, και ένα χρώμα είναι τρία bytes. Αυτή είναι η δεκαεξαδική σημειογραφία #RRGGBB από τον ιστό: το #ff0000 είναι (255, 0, 0), καθαρό κόκκινο.
sub rgb_to_hex {
my ($r, $g, $b) = @_;
return sprintf "#%02x%02x%02x", $r, $g, $b;
}
print rgb_to_hex(255, 128, 0), "\n"; # #ff8000, an orange
Το RGB είναι ο τρόπος με τον οποίο αποθηκεύονται τα pixel, αλλά είναι ένα απαίσιο μοντέλο για έναν άνθρωπο που επιλέγει ένα χρώμα. Το «κάνε το λίγο πιο πορτοκαλί αλλά κράτησέ το στην ίδια φωτεινότητα» δεν είναι μια προφανής κίνηση στο RGB. Το HSV (hue, saturation, value) είναι το μοντέλο γι” αυτό: το hue (απόχρωση) είναι η θέση στον χρωματικό τροχό σε μοίρες 0 .. 360 (0 κόκκινο, 120 πράσινο, 240 μπλε), το saturation (κορεσμός) είναι πόσο ζωηρό έναντι γκρι 0 .. 1, και το value (τιμή) είναι πόσο φωτεινό 0 .. 1. Η περιστροφή της απόχρωσης κρατώντας σταθερά τον κορεσμό και την τιμή διατρέχει το ουράνιο τόξο με σταθερή ζωηρότητα και φωτεινότητα.
Κάθε επιλογέας χρώματος και εργαλείο διαβάθμισης δουλεύει σε HSV ή σε έναν στενό συγγενή του, και έπειτα μετατρέπει σε RGB για αποθήκευση. Αξίζει να έχετε τη μετατροπή σε κοινή θέα, γιατί θα την ξαναϋλοποιήσετε περισσότερες από μία φορές:
# h in 0..360, s and v in 0..1 -> (r, g, b) in 0..255
sub hsv_to_rgb {
my ($h, $s, $v) = @_;
my $c = $v * $s; # chroma
my $hp = $h / 60;
my $x = $c * (1 - abs($hp % 2 - 1 + ($hp - int $hp)));
my ($r, $g, $b) =
$hp < 1 ? ($c, $x, 0) :
$hp < 2 ? ($x, $c, 0) :
$hp < 3 ? (0, $c, $x) :
$hp < 4 ? (0, $x, $c) :
$hp < 5 ? ($x, 0, $c) :
($c, 0, $x);
my $m = $v - $c;
return map { int(($_ + $m) * 255 + 0.5) } ($r, $g, $b);
}
my @rainbow = map { [ hsv_to_rgb($_, 1, 1) ] } 0, 60, 120, 180, 240, 300;
# red, yellow, green, cyan, blue, magenta
Υπάρχουν και άλλοι χώροι που θα συναντήσετε: η κλίμακα του γκρι (ένα κανάλι, μια φωτεινότητα), το CMYK (αφαιρετικό, για εκτύπωση) και αντιληπτικά ομοιόμορφοι χώροι όπως το CIELAB όπου ίσα αριθμητικά βήματα φαίνονται ως ίσα αντιληπτικά βήματα. Για σχεδίαση στην οθόνη σε δύο διαστάσεις, το RGB για αποθήκευση και το HSV για επιλογή καλύπτουν σχεδόν τα πάντα.
Άλφα και σύνθεση#
Ένα τέταρτο κανάλι, το alpha (άλφα), καταγράφει την αδιαφάνεια: 0 πλήρως διαφανές, 255 (ή 1.0) πλήρως αδιαφανές. Ένα pixel με άλφα είναι RGBA. Το άλφα είναι αυτό που σας επιτρέπει να σχεδιάσετε μια εικόνα πάνω σε μια άλλη και να αφήσετε τα διαφανή μέρη της επάνω να αφήνουν να φανεί ό,τι είναι από κάτω.
Ο συνδυασμός ενός pixel προσκηνίου πάνω σε ένα pixel φόντου είναι η σύνθεση (compositing), και ο τυπικός κανόνας είναι το source-over. Με το άλφα ως κλάσμα 0 .. 1:
# composite one RGBA pixel over another (all channels 0..255,
# alpha treated as 0..1 for the blend)
sub over {
my ($fg, $bg) = @_;
my $a = $fg->[3] / 255; # source alpha
my @out = map {
int($fg->[$_] * $a + $bg->[$_] * (1 - $a) + 0.5)
} 0 .. 2;
return \@out;
}
my $red_half = [255, 0, 0, 128]; # red at 50% opacity
my $white = [255, 255, 255, 255];
my $result = over($red_half, $white);
print "@$result\n"; # 255 128 128, a pink
Το κόκκινο με μισή αδιαφάνεια πάνω στο λευκό δίνει ροζ, ακριβώς όπως θα περιμένατε να φαίνεται μια μπογιά αραιωμένη με νερό. Αυτός ο ένας τύπος, εφαρμοσμένος ανά pixel, είναι ο τρόπος με τον οποίο κάθε στρώμα, πινελιά και sprite με απαλές ακμές προσγειώνεται στον καμβά από κάτω του. Όταν στοιβάζετε πολλά ημιδιαφανή στρώματα, τον εφαρμόζετε επανειλημμένα, από κάτω προς τα πάνω.
Η λεπτομέρεια που δαγκώνει αργότερα είναι το premultiplied alpha (προπολλαπλασιασμένο άλφα): η αποθήκευση κάθε καναλιού χρώματος ήδη πολλαπλασιασμένου με το άλφα του. Κάνει την επαναλαμβανόμενη σύνθεση φθηνότερη και το φιλτράρισμα σωστό στις ακμές, και είναι ο λόγος για τον οποίο τα ακατέργαστα bytes των pixel μιας βιβλιοθήκης ορισμένες φορές δεν είναι το RGB που περιμένετε. Το Cairo χρησιμοποιεί εσωτερικά προπολλαπλασιασμένο άλφα· το κεφάλαιο για το Cairo σημειώνει πού φαίνεται.
Η μία ιδέα του 3D, ονομασμένη και αναβεβλημένη#
Θα ακούσετε για τον Z-buffer τη στιγμή που προκύπτει το 3D, και αξίζει μία παράγραφος εδώ ώστε ο όρος να μην είναι μυστήριο. Στο 2D, το «τι είναι από πάνω» αποφασίζεται από τη σειρά σχεδίασης: το τελευταίο πράγμα που σχεδιάστηκε κερδίζει, που είναι ακριβώς η σύνθεση source-over παραπάνω. Στο 3D, τα αντικείμενα έχουν βάθος (μια συντεταγμένη z), και το πράγμα που είναι πλησιέστερα στον θεατή θα έπρεπε να κερδίζει ανεξάρτητα από τη σειρά σχεδίασης. Ένας Z-buffer είναι ένα δεύτερο πλέγμα, ίδιου μεγέθους με την εικόνα, που κρατά το βάθος της πλησιέστερης επιφάνειας που έχει φανεί ως τώρα σε κάθε pixel· πριν σχεδιάσετε ένα pixel ελέγχετε αν η νέα επιφάνεια είναι πλησιέστερα από ό,τι έχει καταγραφεί, και το παραλείπετε αν όχι.
Αυτή είναι όλη η έννοια, και ως εκεί φτάνει αυτός ο οδηγός 2D με αυτήν. Η αποθήκευση βάθους, η προβολή και η υπόλοιπη τρίτη διάσταση ανήκουν στην προγραμματισμένη συνέχεια. Σε δύο διαστάσεις, η σειρά σχεδίασης είναι ο δικός σας ενταμιευτής βάθους.
Πού πηγαίνει αυτό στη συνέχεια#
Τώρα έχετε το λεξιλόγιο που μιλά ο υπόλοιπος οδηγός: raster και vector, συντεταγμένες με αρχή επάνω αριστερά, RGB και HSV, άλφα και σύνθεση source-over. Το επόμενο κεφάλαιο ξοδεύει αυτό το λεξιλόγιο στους κλασικούς αλγόριθμους σχεδίασης, υλοποιημένους από το μηδέν, ώστε όταν μια βιβλιοθήκη σχεδιάζει μια γραμμή ή γεμίζει ένα πολύγωνο να ξέρετε ακριβώς τι κάνει για λογαριασμό σας.