paint: new paint program featuring endless canvas, zoom, palette and use of external commands

This commit is contained in:
cinap_lenrek 2012-07-19 01:00:23 +02:00
parent 55ddbff77d
commit 72d4a35942
2 changed files with 692 additions and 174 deletions

View file

@ -3,54 +3,78 @@
.SH NAME
paint \- create image files by drawing with a mouse or other pointing device
.SH SYNOPSIS
.B paint [file]
.B paint
[
.I file
]
.SH DESCRIPTION
.I Paint
provides a window upon which can be drawn lines by moving the cursor while
holding down mouse button 1 or its equivalent.
shows a canvas upon which can be drawn lines using the mouse holding
down buttons 1 or 2 for foreground or background color. The canvas
can be moved with button 3. Colors and brush sizes can be selected by
clicking on the palette at the bottom of the screen with buttons 1 or 2.
.PP
A number of keyboard commands are recognized:
If the optional
.I file
argument is specified, then it is read and used as the canvas.
.I Paint
only recognizes Plan 9 bitmap format (see
.IR image (6)).
.PP
A number of immidiate keyboard commands are recognized:
.TP
.B b
Set the brush size to an ellipse with a horizontal semiaxis of
.I n
and a vertical semiaxis of
.I n
(see
.IR graphics (2)).
Type a number,
.I n,
in the pop-up box and hit enter.
.TP
.B c
Change the drawing color to
.I n,
where
0 = black, 1 = white, 2 = red, 3 = green, 4 = blue and 5 = yellow.
.B u
Undos the previous action.
.TP
.B f
Fill the screen with a color,
.I n.
Any unsaved work will be lost.
Fills the canvas with the background color.
.TP
.B o
Open a bitmap image file for editing. Type a path and filename into the
pop-up box and hit enter. If the path is omitted, the filename will be opened
from the current directory.
.B 1-9
Select brush size.
.TP
.B s
Save the current screen as a bitmap image. If the path is omitted, the filename will be
saved in the current directory.
.B +
Doubles magnification.
.TP
.B -
Halves magnification.
.TP
.B esc
Centers the canvas and resets magnification.
.PP
Hitting any other key on the keyboard shows a command prompt
where the following commands can be entered:
.TP
.BI r file
Reads the canvas from
.I file.
.TP
.BI w file
Writes the canvas to
.I file.
.TP
.BI < command
Executes
.I command
and read the canvas from its standard output.
.TP
.BI > command
Executes
.I command
and write the canvas to its standard input.
.TP
.BI | command
Transforms the canvas by running it thru
.I command.
.TP
.B q
Quit.
Quits the program.
.SH SOURCE
.B /sys/src/cmd/paint.c
.SH "SEE ALSO"
.IR graphics (2),
.IR resize (1),
.IR resample (1),
.IR rotate (1),
.IR crop (1),
.IR jpg (1),
.IR page (1),
.IR image (6)
.SH BUGS
.I Paint
offers a bare minimum of drawing functionality. Popular features such as
.B undo
have not yet been implemented.

View file

@ -2,149 +2,643 @@
#include <libc.h>
#include <draw.h>
#include <event.h>
#include <keyboard.h>
#define NCOLORS 6
char *filename;
int zoom = 1;
int thick = 1;
Point spos; /* position on screen */
Point cpos; /* position on canvas */
Image *canvas;
Image *ink;
Image *back;
Image *pal[16]; /* palette */
Rectangle palr; /* palette rect on screen */
Rectangle penr; /* pen size rect on screen */
Image *colors[NCOLORS];
int nundo = 0;
Image *undo[1024];
int c64[] = { /* c64 color palette */
0x000000,
0xFFFFFF,
0x68372B,
0x70A4B2,
0x6F3D86,
0x588D43,
0x352879,
0xB8C76F,
0x6F4F25,
0x433900,
0x9A6759,
0x444444,
0x6C6C6C,
0x9AD284,
0x6C5EB5,
0x959595,
};
/*
* A draw operation that touches only the area contained in bot but not in top.
* mp and sp get aligned with bot.min.
*/
static void
gendrawdiff(Image *dst, Rectangle bot, Rectangle top,
Image *src, Point sp, Image *mask, Point mp, int op)
{
Rectangle r;
Point origin;
Point delta;
if(Dx(bot)*Dy(bot) == 0)
return;
/* no points in bot - top */
if(rectinrect(bot, top))
return;
/* bot - top ≡ bot */
if(Dx(top)*Dy(top)==0 || rectXrect(bot, top)==0){
gendrawop(dst, bot, src, sp, mask, mp, op);
return;
}
origin = bot.min;
/* split bot into rectangles that don't intersect top */
/* left side */
if(bot.min.x < top.min.x){
r = Rect(bot.min.x, bot.min.y, top.min.x, bot.max.y);
delta = subpt(r.min, origin);
gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
bot.min.x = top.min.x;
}
/* right side */
if(bot.max.x > top.max.x){
r = Rect(top.max.x, bot.min.y, bot.max.x, bot.max.y);
delta = subpt(r.min, origin);
gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
bot.max.x = top.max.x;
}
/* top */
if(bot.min.y < top.min.y){
r = Rect(bot.min.x, bot.min.y, bot.max.x, top.min.y);
delta = subpt(r.min, origin);
gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
bot.min.y = top.min.y;
}
/* bottom */
if(bot.max.y > top.max.y){
r = Rect(bot.min.x, top.max.y, bot.max.x, bot.max.y);
delta = subpt(r.min, origin);
gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
bot.max.y = top.max.y;
}
}
int
alphachan(ulong chan)
{
for(; chan; chan >>= 8)
if(TYPE(chan) == CAlpha)
return 1;
return 0;
}
void
zoomdraw(Image *d, Rectangle r, Rectangle top, Image *b, Image *s, Point sp, int f)
{
Rectangle dr;
Image *t;
Point a;
int w;
a = ZP;
if(r.min.x < d->r.min.x){
sp.x += (d->r.min.x - r.min.x)/f;
a.x = (d->r.min.x - r.min.x)%f;
r.min.x = d->r.min.x;
}
if(r.min.y < d->r.min.y){
sp.y += (d->r.min.y - r.min.y)/f;
a.y = (d->r.min.y - r.min.y)%f;
r.min.y = d->r.min.y;
}
rectclip(&r, d->r);
w = s->r.max.x - sp.x;
if(w > Dx(r))
w = Dx(r);
dr = r;
dr.max.x = dr.min.x+w;
if(!alphachan(s->chan))
b = nil;
if(f <= 1){
if(b) gendrawdiff(d, dr, top, b, sp, nil, ZP, SoverD);
gendrawdiff(d, dr, top, s, sp, nil, ZP, SoverD);
return;
}
if((t = allocimage(display, dr, s->chan, 0, 0)) == nil)
return;
for(; dr.min.y < r.max.y; dr.min.y++){
dr.max.y = dr.min.y+1;
draw(t, dr, s, nil, sp);
if(++a.y == f){
a.y = 0;
sp.y++;
}
}
dr = r;
for(sp=dr.min; dr.min.x < r.max.x; sp.x++){
dr.max.x = dr.min.x+1;
if(b) gendrawdiff(d, dr, top, b, sp, nil, ZP, SoverD);
gendrawdiff(d, dr, top, t, sp, nil, ZP, SoverD);
for(dr.min.x++; ++a.x < f && dr.min.x < r.max.x; dr.min.x++){
dr.max.x = dr.min.x+1;
gendrawdiff(d, dr, top, d, Pt(dr.min.x-1, dr.min.y), nil, ZP, SoverD);
}
a.x = 0;
}
freeimage(t);
}
Point
s2c(Point p){
p = subpt(p, spos);
if(p.x < 0) p.x -= zoom-1;
if(p.y < 0) p.y -= zoom-1;
return addpt(divpt(p, zoom), cpos);
}
Point
c2s(Point p){
return addpt(mulpt(subpt(p, cpos), zoom), spos);
}
Rectangle
c2sr(Rectangle r){
return Rpt(c2s(r.min), c2s(r.max));
}
void
update(Rectangle *rp){
if(canvas==nil)
draw(screen, screen->r, back, nil, ZP);
else {
if(rp == nil)
rp = &canvas->r;
gendrawdiff(screen, screen->r, c2sr(canvas->r), back, ZP, nil, ZP, SoverD);
zoomdraw(screen, c2sr(*rp), ZR, back, canvas, rp->min, zoom);
}
flushimage(display, 1);
}
void
expand(Rectangle r)
{
Rectangle nr;
Image *tmp;
if(canvas==nil){
if((canvas = allocimage(display, r, screen->chan, 0, DNofill)) == nil)
sysfatal("allocimage: %r");
draw(canvas, canvas->r, back, nil, ZP);
return;
}
nr = canvas->r;
combinerect(&nr, r);
if(eqrect(nr, canvas->r))
return;
if((tmp = allocimage(display, nr, canvas->chan, 0, DNofill)) == nil)
return;
draw(tmp, canvas->r, canvas, nil, canvas->r.min);
gendrawdiff(tmp, tmp->r, canvas->r, back, ZP, nil, ZP, SoverD);
freeimage(canvas);
canvas = tmp;
}
void
save(Rectangle r, int mark)
{
Image *tmp;
int x;
if(mark){
x = nundo++ % nelem(undo);
if(undo[x])
freeimage(undo[x]);
undo[x] = nil;
}
if(canvas==nil || nundo<0)
return;
if(!rectclip(&r, canvas->r))
return;
if((tmp = allocimage(display, r, canvas->chan, 0, DNofill)) == nil)
return;
draw(tmp, r, canvas, nil, r.min);
x = nundo++ % nelem(undo);
if(undo[x])
freeimage(undo[x]);
undo[x] = tmp;
}
void
restore(int n)
{
Image *tmp;
int x;
while(nundo > 0){
if(n-- == 0)
return;
x = --nundo % nelem(undo);
if((tmp = undo[x]) == nil)
return;
undo[x] = nil;
expand(tmp->r);
draw(canvas, tmp->r, tmp, nil, tmp->r.min);
update(&tmp->r);
freeimage(tmp);
}
}
void
translate(Point d)
{
Rectangle r, nr;
if(canvas==nil || d.x==0 && d.y==0)
return;
r = c2sr(canvas->r);
nr = rectaddpt(r, d);
rectclip(&r, screen->clipr);
draw(screen, rectaddpt(r, d), screen, nil, r.min);
zoomdraw(screen, nr, rectaddpt(r, d), back, canvas, canvas->r.min, zoom);
gendrawdiff(screen, screen->r, nr, back, ZP, nil, ZP, SoverD);
spos = addpt(spos, d);
flushimage(display, 1);
}
void
setzoom(Point o, int z)
{
if(z < 1)
return;
cpos = s2c(o);
spos = o;
zoom = z;
update(nil);
}
void
center(void)
{
cpos = ZP;
if(canvas)
cpos = addpt(canvas->r.min,
divpt(subpt(canvas->r.max, canvas->r.min), 2));
spos = addpt(screen->r.min,
divpt(subpt(screen->r.max, screen->r.min), 2));
update(nil);
}
void
drawpal(void)
{
Rectangle r, rr;
int i;
r = screen->r;
r.min.y = r.max.y - 20;
replclipr(screen, 0, r);
penr = r;
penr.min.x = r.max.x - 10*Dy(r);
palr = r;
palr.max.x = penr.min.x;
r = penr;
draw(screen, r, back, nil, ZP);
for(i=0; i<10; i++){
r.max.x = penr.min.x + (i+1)*Dx(penr) / 10;
rr = r;
if(i == thick)
rr.min.y += Dy(r)/3;
fillellipse(screen, addpt(rr.min, divpt(subpt(rr.max, rr.min), 2)), i, i, ink, ZP);
r.min.x = r.max.x;
}
r = palr;
for(i=1; i<=nelem(pal); i++){
r.max.x = palr.min.x + i*Dx(palr) / nelem(pal);
rr = r;
if(ink == pal[i-1])
rr.min.y += Dy(r)/3;
draw(screen, rr, pal[i-1], nil, ZP);
gendrawdiff(screen, r, rr, back, ZP, nil, ZP, SoverD);
r.min.x = r.max.x;
}
r = screen->r;
r.max.y -= Dy(palr);
replclipr(screen, 0, r);
}
int
hitpal(Mouse m)
{
if(ptinrect(m.xy, penr)){
if(m.buttons & 7){
thick = ((m.xy.x - penr.min.x) * 10) / Dx(penr);
drawpal();
}
return 1;
}
if(ptinrect(m.xy, palr)){
Image *col;
col = pal[(m.xy.x - palr.min.x) * nelem(pal) / Dx(palr)];
switch(m.buttons & 7){
case 1:
ink = col;
drawpal();
break;
case 2:
back = col;
drawpal();
update(nil);
break;
}
return 1;
}
return 0;
}
void
catch(void *, char *msg)
{
if(strstr(msg, "closed pipe"))
noted(NCONT);
noted(NDFLT);
}
int
pipeline(char *fmt, ...)
{
char buf[1024];
va_list a;
int p[2];
va_start(a, fmt);
vsnprint(buf, sizeof(buf), fmt, a);
va_end(a);
if(pipe(p) < 0)
return -1;
switch(rfork(RFPROC|RFMEM|RFFDG|RFNOTEG)){
case -1:
close(p[0]);
close(p[1]);
return -1;
case 0:
close(p[1]);
dup(p[0], 0);
dup(p[0], 1);
close(p[0]);
execl("/bin/rc", "rc", "-c", buf, nil);
exits("exec");
}
close(p[0]);
return p[1];
}
void
usage(void)
{
fprint(2, "usage: %s [ file ]\n", argv0);
exits("usage");
}
void
main(int argc, char *argv[])
{
char *s, buf[1024];
Rectangle r;
Image *img;
int i, fd;
Event e;
Mouse m;
Point p, d;
ARGBEGIN {
default:
usage();
} ARGEND;
if(argc == 1)
filename = strdup(argv[0]);
else if(argc != 0)
usage();
if(initdraw(0, 0, "paint") < 0)
sysfatal("initdraw: %r");
if(filename){
if((fd = open(filename, OREAD)) < 0)
sysfatal("open: %r");
if((canvas = readimage(display, fd, 0)) == nil)
sysfatal("readimage: %r");
close(fd);
}
/* palette initialization */
for(i=0; i<nelem(pal); i++){
pal[i] = allocimage(display, Rect(0, 0, 1, 1), RGB24, 1,
c64[i % nelem(c64)]<<8 | 0xFF);
if(pal[i] == nil)
sysfatal("allocimage: %r");
}
ink = pal[0];
back = pal[1];
drawpal();
center();
einit(Emouse | Ekeyboard);
notify(catch);
for(;;) {
switch(event(&e)){
case Emouse:
if(hitpal(e.mouse))
continue;
img = ink;
switch(e.mouse.buttons & 7){
case 2:
img = back;
/* no break */
case 1:
p = s2c(e.mouse.xy);
r = Rect(p.x-thick, p.y-thick, p.x+thick+1, p.y+thick+1);
expand(r);
save(r, 1);
fillellipse(canvas, p, thick, thick, img, ZP);
update(&r);
for(;;){
m = e.mouse;
if(event(&e) != Emouse)
break;
if((e.mouse.buttons ^ m.buttons) & 7)
break;
d = s2c(e.mouse.xy);
if(eqpt(d, p))
continue;
r = canonrect(Rpt(p, d));
r.min.x -= thick;
r.min.y -= thick;
r.max.x += thick+1;
r.max.y += thick+1;
expand(r);
save(r, 0);
line(canvas, p, d, Enddisc, Enddisc, thick, img, ZP);
update(&r);
p = d;
}
break;
case 4:
for(;;){
m = e.mouse;
if(event(&e) != Emouse)
break;
if((e.mouse.buttons & 7) != 4)
break;
translate(subpt(e.mouse.xy, m.xy));
}
break;
}
break;
case Ekeyboard:
switch(e.kbdc){
case Kesc:
zoom = 1;
center();
break;
case '+':
setzoom(e.mouse.xy, zoom*2);
break;
case '-':
setzoom(e.mouse.xy, zoom/2);
break;
case 'f':
if(canvas == nil)
break;
save(canvas->r, 1);
freeimage(canvas);
canvas = nil;
update(nil);
break;
case 'u':
restore(16);
break;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
thick = e.kbdc - '0';
drawpal();
break;
default:
if(e.kbdc == Kdel)
e.kbdc = 'q';
buf[0] = 0;
if(filename && (e.kbdc == 'r' || e.kbdc == 'w'))
snprint(buf, sizeof(buf), "%C %s", e.kbdc, filename);
else if(e.kbdc > 0x20 && e.kbdc < 0x7f)
snprint(buf, sizeof(buf), "%C", e.kbdc);
if(eenter("Cmd", buf, sizeof(buf), &e.mouse) <= 0)
break;
if(strcmp(buf, "q") == 0)
exits(nil);
s = buf+1;
while(*s == ' ' || *s == '\t')
s++;
if(*s == 0)
break;
switch(buf[0]){
case 'r':
if((fd = open(s, OREAD)) < 0){
Error:
snprint(buf, sizeof(buf), "%r");
eenter(buf, nil, 0, &e.mouse);
break;
}
free(filename);
filename = strdup(s);
Readimage:
unlockdisplay(display);
img = readimage(display, fd, 1);
close(fd);
lockdisplay(display);
if(img == nil){
werrstr("readimage: %r");
goto Error;
}
if(canvas){
save(canvas->r, 1);
freeimage(canvas);
}
canvas = img;
center();
break;
case 'w':
if((fd = create(s, OWRITE, 0660)) < 0)
goto Error;
free(filename);
filename = strdup(s);
Writeimage:
if(canvas)
if(writeimage(fd, canvas, 0) < 0){
close(fd);
werrstr("writeimage: %r");
goto Error;
}
close(fd);
break;
case '<':
if((fd = pipeline("%s", s)) < 0)
goto Error;
goto Readimage;
case '>':
if((fd = pipeline("%s", s)) < 0)
goto Error;
goto Writeimage;
case '|':
if(canvas == nil)
break;
if((fd = pipeline("%s", s)) < 0)
goto Error;
switch(rfork(RFMEM|RFPROC|RFFDG)){
case -1:
close(fd);
werrstr("rfork: %r");
goto Error;
case 0:
writeimage(fd, canvas, 1);
exits(nil);
}
goto Readimage;
}
break;
}
break;
}
}
}
void
eresized(int)
{
if(getwindow(display, Refnone) < 0)
sysfatal("resize failed");
}
int
loadimg(char *name)
{
Image *b;
int fd;
if((fd = open(name, OREAD)) < 0)
return -1;
if((b = readimage(display, fd, 0)) == nil){
close(fd);
return -1;
}
draw(screen, screen->r, b, 0, b->r.min);
flushimage(display, 1);
freeimage(b);
close(fd);
return 0;
}
int
saveimg(char *name)
{
int fd;
if((fd = create(name, OWRITE|OTRUNC, 0666)) < 0)
return -1;
writeimage(fd, screen, 0);
close(fd);
return 0;
}
void
main(int argc, char *argv[])
{
Event e;
Point last;
int b = 1;
int c = 0;
int cn, f;
int haslast = 0;
char brush[128];
char color[NCOLORS];
char file[128];
char fill[NCOLORS];
if(initdraw(0, 0, "paint") < 0){
fprint(2, "paint: initdraw failed: %r\n");
exits("initdraw");
}
einit(Emouse | Ekeyboard);
draw(screen, screen->r, display->white, 0, ZP);
flushimage(display, 1);
colors[0] = display->black;
colors[1] = display->white;
colors[2] = allocimage(display, Rect(0,0,1,1), CMAP8, 1, DRed);
colors[3] = allocimage(display, Rect(0,0,1,1), CMAP8, 1, DGreen);
colors[4] = allocimage(display, Rect(0,0,1,1), CMAP8, 1, DBlue);
colors[5] = allocimage(display, Rect(0,0,1,1), CMAP8, 1, DYellow);
ARGBEGIN{
default:
goto Usage;
}ARGEND
switch(argc){
default:
Usage:
fprint(2, "Usage: [file]\n");
exits("usage");
case 0:
break;
case 1:
snprint(file, sizeof(file), "%s", argv[0]);
if(loadimg(file) < 0)
sysfatal("%r");
break;
}
while(1){
switch(event(&e)){
case Emouse:
if(e.mouse.buttons & 1){
if(haslast)
line(screen, last, e.mouse.xy, Enddisc, Enddisc, b, colors[c], ZP);
else
fillellipse(screen, e.mouse.xy, b, b, colors[c], ZP);
last = e.mouse.xy;
haslast = 1;
flushimage(display, 1);
} else
haslast = 0;
break;
case Ekeyboard:
if(e.kbdc == 'b'){
if(eenter("Brush", brush, sizeof(brush), &e.mouse) <= 0)
break;
b = atoi(brush);
}
if(e.kbdc == 'c'){
if(eenter("Color", color, sizeof(color), &e.mouse) <= 0)
break;
cn = atoi(color);
if(cn >= 0 && cn < NCOLORS)
c = cn;
}
if(e.kbdc == 'f'){
if(eenter("Fill", fill, sizeof(fill), &e.mouse) <= 0)
break;
f = atoi(fill);
if(f >= 0 && f < NCOLORS)
draw(screen, screen->r, colors[f], 0, ZP);
}
if(e.kbdc == 'o'){
if(eenter("Open file", file, sizeof(file), &e.mouse) <= 0)
break;
if(loadimg(file) < 0){
rerrstr(file, sizeof(file));
eenter(file, 0, 0, &e.mouse);
}
}
if(e.kbdc == 'q')
exits(nil);
if(e.kbdc == 's'){
if(eenter("Save to", file, sizeof(file), &e.mouse) <= 0)
break;
if(saveimg(file) < 0){
rerrstr(file, sizeof(file));
eenter(file, 0, 0, &e.mouse);
}
}
break;
}
}
drawpal();
update(nil);
}