XXIIVV
Improved the look and feel picture
18J04 — Improved the look and feel

Dotgrid is a vector graphics tool.

Dotgrid is a grid-based vector drawing software designed to create logos, icons and type. The present version uses a simple scripting language inspired from Postscript, and is designed to export .chr and .bmp files. The syntax supports setcolor, point, line, arc, bezier, rect and ellipse.

dotgrid.c

The following code is a single-file implementation written in a few lines of ANSI C, the only dependecy is SDL2. It uses a syntax for its source files similar to that of Postscript. It is meant to be used along-side nasu and moogle. Read the manual here.

cc dotgrid.c -std=c89 -Os -DNDEBUG -g0 -s -Wall -L/usr/local/lib -lSDL2 -lm -o dotgrid
#include <SDL2/SDL.h>
#include <stdio.h>
#include <math.h>

#define HOR 32
#define VER 16
#define PAD 8
#define ZOOM 2
#define color1 0x000000
#define color2 0x72DEC2
#define color3 0x888888
#define color4 0xFFFFFF
#define color0 0x222222

#define PLIMIT 256
#define SZ (HOR * VER * 16)
#define PI 3.14159265358979323846

typedef enum {
	POINT,
	LINE,
	ARC,
	BEZIER,
	RECTANGLE,
	ELLIPSE
} LineType;

typedef struct {
	int x, y;
} Point2d;

typedef struct {
	Point2d points[PLIMIT];
	LineType type;
	int color, len;
} Path2d;

typedef struct {
	Path2d paths[PLIMIT];
	int len;
} Shape2d;

Path2d stack;
Shape2d shape;
Point2d* selection;

unsigned char chrbuf[SZ];
int colors[] = {color1, color2, color3, color4, color0};
int WIDTH = 8 * HOR + PAD * 2;
int HEIGHT = 8 * VER + PAD * 2;
int FPS = 30;
int GUIDES = 1;
int COLOR = 3;
SDL_Window* gWindow = NULL;
SDL_Renderer* gRenderer = NULL;
SDL_Texture* gTexture = NULL;
uint32_t* pixels;

/* helpers */

Point2d
Pt2d(int x, int y)
{
	Point2d p;
	p.x = x;
	p.y = y;
	return p;
}

Point2d
clampt(Point2d p, int step)
{
	p.x = abs((p.x + step / 2) / step) * step;
	p.y = abs((p.y + step / 2) / step) * step;
	return p;
}

int
equpt(Point2d* a, Point2d* b)
{
	return a->x == b->x && a->y == b->y;
}

int
slen(char* s)
{
	int n = 0;
	while(s[n] != '\0' && s[++n])
		;
	return n;
}

int
scmp(char* a, char* b)
{
	int i, l = slen(a);
	if(l != slen(b))
		return 0;
	for(i = 0; i < l; ++i)
		if(a[i] != b[i])
			return 0;
	return 1;
}

int
cpos(char* s, char c)
{
	int i;
	for(i = 0; i < slen(s); i++)
		if(s[i] == c)
			return i;
	return -1;
}

int
sint(char* s, int len)
{
	int num = 0, i = 0;
	while(s[i] && i < len && (s[i] >= '0' && s[i] <= '9')) {
		num = num * 10 + (s[i] - '0');
		i++;
	}
	return num;
}

/* chr */

int
rowchr(int x, int y)
{
	return (y % 8) + ((x / 8 + y / 8 * HOR) * 16);
}

int
getchr(int x, int y)
{
	int r = rowchr(x, y), px = 7 - (x % 8), ch1, ch2;
	if(r < 0 || r > SZ - 8)
		return 0;
	ch1 = (chrbuf[r] >> px) & 1;
	ch2 = (chrbuf[r + 8] >> px) & 1;
	return ch1 && !ch2 ? 1 : !ch1 && ch2 ? 2 : ch1 && ch2 ? 3 : 0;
}

void
putchr(int x, int y, int color)
{
	int r = rowchr(x, y), px = 7 - (x % 8);
	if(x < 0 || y < 0 || x > 8 * HOR || y > 8 * VER || r > SZ - 8)
		return;
	if(color == 0) {
		chrbuf[r] &= ~(1UL << px);
		chrbuf[r + 8] &= ~(1UL << px);
	} else if(color == 2) {
		chrbuf[r] |= 1UL << px;
		chrbuf[r + 8] &= ~(1UL << px);
	} else if(color == 1) {
		chrbuf[r] &= ~(1UL << px);
		chrbuf[r + 8] |= 1UL << px;
	} else if(color == 3) {
		chrbuf[r] |= 1UL << px;
		chrbuf[r + 8] |= 1UL << px;
	}
}

void
newchr(void)
{
	int i;
	for(i = 0; i < SZ; ++i)
		chrbuf[i] = 0x00;
}

void
line(int x0, int y0, int x1, int y1, int color)
{
	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
	int err = dx + dy, e2;
	for(;;) {
		putchr(x0, y0, color);
		if(x0 == x1 && y0 == y1)
			break;
		e2 = 2 * err;
		if(e2 >= dy) {
			err += dy;
			x0 += sx;
		}
		if(e2 <= dx) {
			err += dx;
			y0 += sy;
		}
	}
}

void
ellipse(int x0, int y0, int x1, int y1, int cadran, int color)
{
	int a = abs(x1 - x0), b = abs(y1 - y0), b1 = b & 1;
	double dx = 4 * (1 - a) * b * b, dy = 4 * (b1 + 1) * a * a;
	double err = dx + dy + b1 * a * a, e2;
	if(x0 == x1 || y0 == y1)
		return;
	if(x0 > x1) {
		x0 = x1;
		x1 += a;
	}
	if(y0 > y1)
		y0 = y1;
	y0 += (b + 1) / 2;
	y1 = y0 - b1;
	a *= 8 * a;
	b1 = 8 * b * b;
	do {
		if(cadran == -1 || cadran == 1)
			putchr(x1, y0, color);
		if(cadran == -1 || cadran == 2)
			putchr(x0, y0, color);
		if(cadran == -1 || cadran == 3)
			putchr(x0, y1, color);
		if(cadran == -1 || cadran == 0)
			putchr(x1, y1, color);
		e2 = 2 * err;
		if(e2 <= dy) {
			y0++;
			y1--;
			err += dy += a;
		}
		if(e2 >= dx || 2 * err > dy) {
			x0++;
			x1--;
			err += dx += b1;
		}
	} while(x0 <= x1);
}

void
arc(int x0, int y0, int x1, int y1, int x2, int y2, int color)
{
	int cadran, col = (y1 - y0) * (x2 - x1) - (y2 - y1) * (x1 - x0);
	x1 = x2;
	y1 = y2;
	if(x2 > x0 && y2 > y0) {
		if(col < 0) {
			x0 = x0 - (x2 - x0);
			y1 = y2 + (y2 - y0);
			cadran = 0;
		} else {
			y0 = y0 - (y2 - y0);
			x1 = x2 + (x2 - x0);
			cadran = 2;
		}
	} else if(x2 < x0 && y2 > y0) {
		if(col < 0) {
			y0 = y0 - (y2 - y0);
			x1 = x2 + (x2 - x0);
			cadran = 1;
		} else {
			x0 = x0 - (x2 - x0);
			y1 = y2 + (y2 - y0);
			cadran = 3;
		}
	} else if(x2 < x0 && y2 < y0) {
		if(col < 0) {
			x0 = x0 - (x2 - x0);
			y1 = y2 + (y2 - y0);
			cadran = 2;
		} else {
			y0 = y0 - (y2 - y0);
			x1 = x2 + (x2 - x0);
			cadran = 0;
		}
	} else {
		if(col < 0) {
			y0 = y0 - (y2 - y0);
			x1 = x2 + (x2 - x0);
			cadran = 3;
		} else {
			x0 = x0 - (x2 - x0);
			y1 = y2 + (y2 - y0);
			cadran = 1;
		}
	}
	ellipse(x0, y0, x1, y1, cadran, color);
}

void
bezier(int x0, int y0, int x1, int y1, int x2, int y2, int color)
{
	int sx = x2 - x1, sy = y2 - y1;
	long xx = x0 - x1, yy = y0 - y1, xy;
	double dx, dy, err, cur = xx * sy - yy * sx;
	if(xx * sx > 0 || yy * sy > 0)
		return;
	if(sx * (long)sx + sy * (long)sy > xx * xx + yy * yy) {
		x2 = x0;
		x0 = sx + x1;
		y2 = y0;
		y0 = sy + y1;
		cur = -cur;
	}
	if(cur != 0) {
		xx += sx;
		xx *= sx = x0 < x2 ? 1 : -1;
		yy += sy;
		yy *= sy = y0 < y2 ? 1 : -1;
		xy = 2 * xx * yy;
		xx *= xx;
		yy *= yy;
		if(cur * sx * sy < 0) {
			xx = -xx;
			yy = -yy;
			xy = -xy;
			cur = -cur;
		}
		dx = 4.0 * sy * cur * (x1 - x0) + xx - xy;
		dy = 4.0 * sx * cur * (y0 - y1) + yy - xy;
		xx += xx;
		yy += yy;
		err = dx + dy + xy;
		do {
			putchr(x0, y0, color);
			if(x0 == x2 && y0 == y2)
				return;
			y1 = 2 * err < dx;
			if(2 * err > dy) {
				x0 += sx;
				dx -= xy;
				err += dy += yy;
			}
			if(y1) {
				y0 += sy;
				dy -= xy;
				err += dx += xx;
			}
		} while(dy < dx);
	}
	line(x0, y0, x2, y2, color);
}

void
rectangle(int x0, int y0, int x1, int y1, int color)
{
	line(x0, y0, x0, y1, color);
	line(x0, y1, x1, y1, color);
	line(x1, y1, x1, y0, color);
	line(x1, y0, x0, y0, color);
}

void
handle(int ax, int ay, int r, int color)
{
	line(ax, ay - r, ax + r, ay, color);
	line(ax + r, ay, ax, ay + r, color);
	line(ax, ay + r, ax - r, ay, color);
	line(ax - r, ay, ax, ay - r, color);
}

/* draw */

int
guide(int x, int y)
{
	if(!GUIDES)
		return 0;
	if(x % 32 == 0 && y % 32 == 0)
		return 3;
	else if(x % 8 == 0 && y % 8 == 0)
		return 4;
	return 0;
}

void
draw(uint32_t* dst)
{
	int b, i, j, id = 0;
	for(b = 0; b < SZ; b += 16)
		for(i = 0; i < 8; i++)
			for(j = 7; j >= 0; j--) {
				int ch1 = chrbuf[b + i];
				int ch2 = chrbuf[b + i + 8];
				int color = ((ch1 >> j) & 0x1) + (((ch2 >> j) & 0x1) << 1);
				int ti = id / 64;
				int px = (ti / (HOR * VER)) * (8 * HOR) + (ti % HOR) * 8 + (id % 8);
				int py = ((ti / HOR) * 8) + ((id % 64) / 8);
				dst[(py + PAD) * WIDTH + (px + PAD)] = colors[color == 0 ? guide(px, py) : color];
				id++;
			}
	SDL_UpdateTexture(gTexture, NULL, dst, WIDTH * sizeof(uint32_t));
	SDL_RenderClear(gRenderer);
	SDL_RenderCopy(gRenderer, gTexture, NULL, NULL);
	SDL_RenderPresent(gRenderer);
}

void
update(void)
{
	printf("[%d:%dx%d]\n",
	       HOR,
	       VER,
	       ZOOM);
}

void
renderpath(Path2d* path, int guides)
{
	int j;
	/* Draw control points */
	for(j = 0; j < path->len; ++j)
		if(guides)
			handle(path->points[j].x, path->points[j].y, 2, 1);
	/* Draw paths */
	for(j = 0; j < path->len; ++j) {
		if(j < path->len - 1) {
			if(path->type == LINE)
				line(path->points[j].x, path->points[j].y, path->points[j + 1].x, path->points[j + 1].y, path->color);
			else if(path->type == RECTANGLE)
				rectangle(path->points[j].x, path->points[j].y, path->points[j + 1].x, path->points[j + 1].y, path->color);
			else if(path->type == ELLIPSE)
				ellipse(path->points[j].x, path->points[j].y, path->points[j + 1].x, path->points[j + 1].y, -1, path->color);
		}
		if(j < path->len - 2) {
			if(path->type == ARC)
				arc(path->points[j].x, path->points[j].y, path->points[j + 1].x, path->points[j + 1].y, path->points[j + 2].x, path->points[j + 2].y, path->color);
			else if(path->type == BEZIER)
				bezier(path->points[j].x, path->points[j].y, path->points[j + 1].x, path->points[j + 1].y, path->points[j + 2].x, path->points[j + 2].y, path->color);
			if(path->type == ARC || path->type == BEZIER)
				j++;
		}
		if(path->type == POINT)
			putchr(path->points[j].x, path->points[j].y, path->color);
	}
}

void
render(void)
{
	int i, j;
	newchr();
	/* draw shape */
	for(i = 0; i < shape.len; i++) {
		renderpath(&shape.paths[i], GUIDES && i == shape.len - 1);
	}
	/* draw stack */
	for(j = 0; j < stack.len; ++j) {
		handle(stack.points[j].x,
		       stack.points[j].y,
		       2, 1);
		if(j < stack.len - 1)
			line(stack.points[j].x, stack.points[j].y, stack.points[j + 1].x, stack.points[j + 1].y, 1);
	}
	draw(pixels);
}

/* options */

void
addpoint(Point2d touch)
{
	if(stack.len > 0 && equpt(&touch, &stack.points[stack.len - 1]))
		return;
	stack.points[stack.len] = touch;
	stack.len++;
	render();
}

void
selectpoint(Point2d* touch)
{
	int i, j;
	selection = NULL;
	for(i = 0; i < shape.len; ++i) {
		Path2d* p = &shape.paths[i];
		for(j = 0; j < p->len; ++j)
			if(equpt(touch, &p->points[j]))
				selection = &p->points[j];
	}
}

void
dragpoint(Point2d* touch)
{
	if(equpt(touch, selection))
		return;
	selection->x = touch->x;
	selection->y = touch->y;
	stack.len = 0;
	render();
}

void
copypath(Path2d* src, Path2d* dst)
{
	int i;
	dst->len = src->len;
	for(i = 0; i < dst->len; ++i) {
		dst->points[i].x = src->points[i].x;
		dst->points[i].y = src->points[i].y;
	}
}

int
cancast(LineType type)
{
	if(type == POINT)
		return 1;
	else if(type == LINE || type == RECTANGLE || type == ELLIPSE) {
		if(stack.len > 1)
			return 1;
	} else if(type == ARC || type == BEZIER)
		if(stack.len > 2)
			return 1;
	return 0;
}

void
cast(LineType type)
{
	if(!cancast(type))
		return;
	copypath(&stack, &shape.paths[shape.len]);
	shape.paths[shape.len].color = COLOR;
	shape.paths[shape.len].type = type;
	shape.len++;
	stack.len = 0;
	render();
}

int
error(char* msg, const char* err)
{
	printf("Error %s: %s\n", msg, err);
	return 0;
}

void
totxt(void)
{
	int i, j, c = -1;
	FILE* f = fopen("dotgrid-shape.txt", "w");
	for(i = 0; i < shape.len; ++i) {
		Path2d* p = &shape.paths[i];
		if(c != p->color) {
			fprintf(f, "%d setcolor\n", p->color);
			c = p->color;
		}
		for(j = 0; j < p->len; ++j)
			fprintf(f, "%d,%d ", p->points[j].x, p->points[j].y);
		fprintf(f, "%s\n", p->type == POINT ? "point" : p->type == LINE ? "line" : p->type == ARC ? "arc" : p->type == RECTANGLE ? "rectangle" : p->type == ELLIPSE ? "ellipse" : "bezier");
	}
	fclose(f);
}

void
tochr(void)
{
	FILE* f = fopen("dotgrid-export.chr", "wb");
	if(!fwrite(chrbuf, sizeof(chrbuf), 1, f))
		error("Save", "Invalid output file");
	fclose(f);
}

void
tobmp(void)
{
	SDL_Surface* surface = SDL_GetWindowSurface(gWindow);
	GUIDES = 0;
	draw(pixels);
	SDL_RenderReadPixels(gRenderer,
	                     NULL,
	                     SDL_PIXELFORMAT_ARGB8888,
	                     surface->pixels,
	                     surface->pitch);
	SDL_SaveBMP(surface, "dotgrid-render.bmp");
	SDL_FreeSurface(surface);
}

void
loadtxt(FILE* f)
{
	char line[256], query[256];
	int i = 0, querylen = 0, setting = 0;
	if(!f)
		return;
	while(fgets(line, 256, f)) {
		if(line[0] == ';')
			continue;
		i = 0;
		while(line[i]) {
			if(line[i] == ' ' || line[i] == '\n' || line[i] == '\0') {
				int c = cpos(query, ',');
				if(c >= 0)
					addpoint(Pt2d(
					    sint(query, c),
					    sint(query + c + 1, slen(query) - c - 1)));
				else if(i > 0 && scmp(query, "setcolor"))
					COLOR = setting < 0 ? 0 : setting > 3 ? 3 : setting;
				else if(scmp(query, "line"))
					cast(LINE);
				else if(scmp(query, "arc"))
					cast(ARC);
				else if(scmp(query, "bezier"))
					cast(BEZIER);
				else if(scmp(query, "point"))
					cast(POINT);
				else if(scmp(query, "rectangle"))
					cast(RECTANGLE);
				else if(scmp(query, "ellipse"))
					cast(ELLIPSE);
				else if(slen(query) == 1)
					setting = query[0] - '0';
				querylen = 0;
				query[0] = '\0';
			} else {
				query[querylen++] = line[i];
				query[querylen] = '\0';
			}
			i++;
		}
	}
	fclose(f);
}

void
quit(void)
{
	free(pixels);
	SDL_DestroyTexture(gTexture);
	gTexture = NULL;
	SDL_DestroyRenderer(gRenderer);
	gRenderer = NULL;
	SDL_DestroyWindow(gWindow);
	gWindow = NULL;
	SDL_Quit();
	exit(0);
}

void
domouse(SDL_Event* event)
{
	Point2d touch = clampt(
	    Pt2d(
	        (event->motion.x - (PAD * ZOOM)) / ZOOM,
	        (event->motion.y - (PAD * ZOOM)) / ZOOM),
	    8);
	switch(event->type) {
	case SDL_MOUSEBUTTONUP:
		if(selection && !equpt(&touch, selection))
			dragpoint(&touch);
		else
			addpoint(touch);
		selection = NULL;
		break;
	case SDL_MOUSEBUTTONDOWN:
		selectpoint(&touch);
		break;
	case SDL_MOUSEMOTION:
		break;
	}
}

void
dokey(SDL_Event* event)
{
	switch(event->key.keysym.sym) {
	case SDLK_ESCAPE:
		stack.len = 0;
		render();
		break;
	case SDLK_BACKSPACE:
		stack.len = 0;
		if(shape.len > 0)
			shape.len--;
		render();
		break;
	case SDLK_e:
		tochr();
		break;
	case SDLK_r:
		tobmp();
		break;
	case SDLK_t:
		totxt();
		break;
	case SDLK_a:
		cast(LINE);
		break;
	case SDLK_s:
		cast(ARC);
		break;
	case SDLK_d:
		cast(BEZIER);
		break;
	case SDLK_z:
		cast(POINT);
		break;
	case SDLK_x:
		cast(RECTANGLE);
		break;
	case SDLK_c:
		cast(ELLIPSE);
		break;
	case SDLK_1:
		COLOR = 1;
		break;
	case SDLK_2:
		COLOR = 2;
		break;
	case SDLK_3:
		COLOR = 3;
		break;
	case SDLK_TAB:
		break;
	case SDLK_h:
		GUIDES = !GUIDES;
		render();
		break;
	case SDLK_n:
		newchr();
		shape.len = 0;
		stack.len = 0;
		draw(pixels);
		break;
	}
	/* update(); */
}

int
init(void)
{
	int i, j;
	if(SDL_Init(SDL_INIT_VIDEO) < 0)
		return error("Init", SDL_GetError());
	gWindow = SDL_CreateWindow("Dotgrid",
	                           SDL_WINDOWPOS_UNDEFINED,
	                           SDL_WINDOWPOS_UNDEFINED,
	                           WIDTH * ZOOM,
	                           HEIGHT * ZOOM,
	                           SDL_WINDOW_SHOWN);
	if(gWindow == NULL)
		return error("Window", SDL_GetError());
	gRenderer = SDL_CreateRenderer(gWindow, -1, 0);
	if(gRenderer == NULL)
		return error("Renderer", SDL_GetError());
	gTexture = SDL_CreateTexture(gRenderer,
	                             SDL_PIXELFORMAT_ARGB8888,
	                             SDL_TEXTUREACCESS_STATIC,
	                             WIDTH,
	                             HEIGHT);
	if(gTexture == NULL)
		return error("Texture", SDL_GetError());
	pixels = (uint32_t*)malloc(WIDTH * HEIGHT * sizeof(uint32_t));
	if(pixels == NULL)
		return error("Pixels", "Failed to allocate memory");
	for(i = 0; i < HEIGHT; i++)
		for(j = 0; j < WIDTH; j++)
			pixels[i * WIDTH + j] = color1;
	return 1;
}

int
main(int argc, char* argv[])
{
	int ticknext = 0;

	if(!init())
		return error("Init", "Failure");

	newchr();
	if(argc > 1)
		loadtxt(fopen(argv[1], "r"));
	draw(pixels);
	update();

	while(1) {
		int tick = SDL_GetTicks();
		SDL_Event event;
		if(tick < ticknext)
			SDL_Delay(ticknext - tick);
		ticknext = tick + (1000 / FPS);
		while(SDL_PollEvent(&event) != 0) {
			if(event.type == SDL_QUIT)
				quit();
			else if(event.type == SDL_MOUSEBUTTONUP ||
			        event.type == SDL_MOUSEBUTTONDOWN ||
			        event.type == SDL_MOUSEMOTION) {
				domouse(&event);
			} else if(event.type == SDL_KEYDOWN)
				dokey(&event);
			else if(event.type == SDL_WINDOWEVENT)
				if(event.window.event == SDL_WINDOWEVENT_EXPOSED)
					draw(pixels);
		}
	}
	quit();
	return 0;
}
— Found a mistake? Submit an edit to dotgrid.c.txt(797 lines)
Working away from snow picture
17Z11 — Working away from snow
General improvements picture
17W08 — General improvements
Dotless Dotgrid Interface picture
17W01 — Dotless Dotgrid Interface

incoming(6): themes ronin rafinograde computer postscript identity

Last update on 20U08, edited 45 times. +197/277fh----+-