/* Copyright 2008 by Jens Andersson and Wade Brainerd. This file is part of Colors! XO. Colors is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Colors is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Colors. If not, see . */ #ifndef _CANVAS_H_ #define _CANVAS_H_ #include #include #include "colorsc.h" #include "drwfile.h" using namespace std; // Uncomment this to print all executed drawing commands to stdout. //#define CANVAS_DEBUG_COMMANDS // Structure for passing pixel data to and from Python. struct SurfaceA8R8G8B8 { int width, height; int stride; unsigned int* pixels; }; // Structure for passing buffers of draw commands to and from Python. struct DrawCommandBuffer { DrawCommandBuffer() { cmds = NULL; ncommands = 0; } DrawCommandBuffer(const char* _cmds, int _ncommands) { cmds = (char*)malloc(_ncommands*sizeof(unsigned int)); memcpy(cmds, _cmds, _ncommands*sizeof(unsigned int)); ncommands = _ncommands; } DrawCommandBuffer(const DrawCommandBuffer& b) { cmds = (char*)malloc(b.ncommands*sizeof(unsigned int)); memcpy(cmds, b.cmds, b.ncommands*sizeof(unsigned int)); ncommands = b.ncommands; } ~DrawCommandBuffer() { if (cmds) { free((void*)cmds); cmds = NULL; } } const DrawCommandBuffer& operator=(const DrawCommandBuffer& b) { if (cmds) free((void*)cmds); cmds = (char*)malloc(b.ncommands*sizeof(unsigned int)); memcpy(cmds, b.cmds, b.ncommands*sizeof(unsigned int)); ncommands = b.ncommands; return *this; } void append(const DrawCommandBuffer& b) { char* newcmds = (char*)malloc((ncommands+b.ncommands)*sizeof(unsigned int)); if (cmds) { memcpy(newcmds, cmds, ncommands*sizeof(unsigned int)); free(cmds); } memcpy(newcmds + ncommands*sizeof(unsigned int), b.cmds, b.ncommands*sizeof(unsigned int)); cmds = newcmds; ncommands = ncommands + b.ncommands; } void clear() { if (cmds) { free(cmds); cmds = NULL; } ncommands = 0; } ByteBuffer get_bytes() { ByteBuffer buf; buf.size = ncommands*sizeof(unsigned int); buf.data = cmds; return buf; } char* cmds; int ncommands; static DrawCommandBuffer create_from_string(const char* cmds, int ncommands) { return DrawCommandBuffer(cmds, ncommands); } }; struct DrawCommand { enum { TYPE_DRAW = 0, TYPE_DRAWEND = 1, TYPE_COLORCHANGE = 2, TYPE_SIZECHANGE = 3, }; int type; Pos pos; Color color; int pressure; bool flipx; bool flipy; bool is_text; uint32_t text; int brush_control; int brush_type; float size; float opacity; DrawCommand() { type = TYPE_DRAW; pressure = 0; flipx = false; flipy = false; is_text = false; text = 0; brush_control = 0; brush_type = 0; size = 0; opacity = 0; } static DrawCommand create_color_change(const Color& c) { DrawCommand cmd; cmd.type = TYPE_COLORCHANGE; cmd.color = c; return cmd; } static DrawCommand create_draw(const Pos& pos, int pressure) { DrawCommand cmd; cmd.type = TYPE_DRAW; cmd.pos = pos; cmd.pressure = pressure; return cmd; } static DrawCommand create_end_draw(int pressure) { DrawCommand cmd; cmd.type = TYPE_DRAWEND; cmd.pressure = pressure; return cmd; } static DrawCommand create_size_change(int brush_control, int brush_type, float size, float opacity) { DrawCommand cmd; cmd.type = TYPE_SIZECHANGE; cmd.brush_control = brush_control; cmd.brush_type = brush_type; cmd.size = size; cmd.opacity = opacity; return cmd; } static DrawCommand create_flip(bool flipx) { DrawCommand cmd; cmd.type = TYPE_COLORCHANGE; cmd.flipx = flipx; cmd.flipy = !flipx; return cmd; } }; struct BrushType { enum { BRUSHTYPE_HARD = 0, BRUSHTYPE_SOFT = 1, BRUSHTYPE_CURSOR = 2, NUM_BRUSHES = 3, }; static const int DIST_TABLE_WIDTH = 256; // Width of distance lookup-table static const int DIST_TABLE_CENTER = DIST_TABLE_WIDTH / 2; // Center of distance lookup-table static const int BRUSH_TABLE_WIDTH = 256; // Width of brush lookup-table static const int BRUSH_TABLE_HEIGHT = 65; // Height of 65 allows a brushsize down to 1.0f static const float EXTRA_BRUSH_SCALE = 1.023f; // Scales down the brush-size so we don't index-out-of-range. static unsigned char distance_tbl[DIST_TABLE_WIDTH][DIST_TABLE_WIDTH]; unsigned char intensity_tbl[BRUSH_TABLE_WIDTH][BRUSH_TABLE_HEIGHT]; // Creates a simple sqrt-lookup table. static void create_distance_table() { for (int x = 0; x < DIST_TABLE_WIDTH; x++) for (int y = 0; y < DIST_TABLE_WIDTH; y++) { int dx = x - DIST_TABLE_CENTER; int dy = y - DIST_TABLE_CENTER; float dist = sqrtf(float(dx * dx + dy * dy)); distance_tbl[x][y] = (unsigned char)min(255.0f, dist*255/DIST_TABLE_CENTER); } } // Calculates a gradient between 0 and 1 where the derivate of both 0 and 1 is 0. // This function is used to calculate the falloff of the brushes. float smooth_step(float a) { return sinf((a*a - 0.5f) * 3.14159f) * 0.5f + 0.5f; } void create_brush(float brush_border, float amp) { // Find at what range from brush-center the brush intensity goes below 2 float max_r = 0; for (int i = BRUSH_TABLE_WIDTH-1; i >= 0; i--) { float f = float(i) / BRUSH_TABLE_WIDTH; float f2 = 1.0f - (f - brush_border) / (1.0f - brush_border); if (round(smooth_step(f2) * amp) >= 2) { max_r = i; break; } } // Calculate a scale-factor so the brush optimally uses the area float r = float(max_r + 2) / BRUSH_TABLE_WIDTH / BRUSH_TABLE_WIDTH; for (int y = 0; y < BRUSH_TABLE_HEIGHT; y++) { // Each line in the brush-table is calculated for a specific brush-size // This has two functions: // 1. Be able to simulate the effect of resampling of the "perfect" big brush to a smaller one to improve precision // 2. Compensate for the need to scale small brushes to avoid index out of range during rastering // Calculate scale for this width float brushscale = EXTRA_BRUSH_SCALE + y * 2.0f / 64.0f; // Calculate brush unsigned int intensity_row[BRUSH_TABLE_WIDTH]; for (int i = 0; i < BRUSH_TABLE_WIDTH; i++) { float f = min(i * r * brushscale, 1.0f); // Apply the two different scales if (f < brush_border) intensity_row[i] = int(amp); else { float f2 = 1.0f - (f - brush_border) / (1.0f - brush_border); f2 = smooth_step(f2) * amp; // Make sure the border-falloff is smooth intensity_row[i] = int(round(f2)); } } // Simulate the effect of resampling int blurradius = int(round(y * BRUSH_TABLE_WIDTH / (brushscale * 64.0f))); float maxintensity = 0; for (int x = 0; x < BRUSH_TABLE_WIDTH; x++) { float l = 0; for (int x2 = x - blurradius; x2 < x + blurradius + 1; x2++) { int i = min(max(x2, 0), BRUSH_TABLE_WIDTH-1); if (i < BRUSH_TABLE_WIDTH) l += intensity_row[i]; } float intensity = l / (blurradius * 2 + 1); if (intensity > maxintensity) maxintensity = intensity; intensity_tbl[x][y] = int(intensity); } } } void create_hard_brush() { create_brush(0.8f, 255); } void create_soft_brush() { create_brush(0.0f, 128); } void create_cursor() { } }; class Brush { public: enum { BRUSHCONTROL_VARIABLEOPACITY = 1, BRUSHCONTROL_VARIABLESIZE = 2, }; static BrushType brush_type[BrushType::NUM_BRUSHES]; Color color; int type; int size; int control; float opacity; Brush() { type = BrushType::BRUSHTYPE_HARD; color = Color(255, 255, 255, 255); size = 32; control = 0; opacity = 1.0f; } Brush(const Brush& a) { type = a.type; color = a.color; size = a.size; control = a.control; opacity = a.opacity; } }; // The canvas represents the current state of the user's painting. It maintains both the pixels representing the // image, and also the complete list of drawing commands that contributed to the image. class Canvas { public: // Dimensions of the reference picture (webcam snapshot). static const int REFERENCE_WIDTH = 640; static const int REFERENCE_HEIGHT = 480; // Dimensions of the videopaint buffer. static const int VIDEO_WIDTH = 80; static const int VIDEO_HEIGHT = 60; enum { DRAWBRUSH_TYPE_NORMAL = 0, DRAWBRUSH_TYPE_OLDCURSOR = 1, DRAWBRUSH_TYPE_DIRECT = 2, DRAWBRUSH_TYPE_GETCOLOR = 3, DRAWBRUSH_TYPE_CURSOR = 4, }; // List of drawing commands that make up the painting. vector commands; // Canvas dimensions. int width; int height; // Current state of the canvas pixels. unsigned int* image; // Backup and alpha channel are used for multiple purposes. See description in Drawing section. unsigned int* image_backup; unsigned char* alpha; // Shared (master) picture for collaborative painting. unsigned int* image_shared; // Reference (webcam snapshot) picture. unsigned short* image_reference; // Videopaint variables. unsigned int* image_video[2]; int video_idx; Pos videopaint_pos; float videopaint_pressure; // Current brush state. Brush brush; // Variables for interpolating the brush across strokes. Pos lastpos; Pos lastorgpos; float lastpressure; // Dimensions of the canvas that have been modified since the last call to reset_dirty_rect. Pos dirtymin; Pos dirtymax; // Dimensions and state of the current stroke. Pos strokemin; Pos strokemax; bool stroke; int idle_while_drawing; int drawtype; // VCR playback variables. bool playing; int playback; int playback_speed; // True if the canvas has been modified since the last save. bool modified; Canvas(int width, int height) : width(width), height(height) { image = new unsigned int[width*height]; image_backup = new unsigned int[width*height]; alpha = new unsigned char[width*height]; image_shared = new unsigned int[width*height]; image_reference = new unsigned short[REFERENCE_WIDTH*REFERENCE_HEIGHT]; memset(image_reference, 0, REFERENCE_WIDTH*REFERENCE_HEIGHT*sizeof(unsigned short)); image_video[0] = new unsigned int[VIDEO_WIDTH*VIDEO_HEIGHT]; image_video[1] = new unsigned int[VIDEO_WIDTH*VIDEO_HEIGHT]; memset(image_video[0], 0, VIDEO_WIDTH*VIDEO_HEIGHT*sizeof(unsigned int)); memset(image_video[1], 0, VIDEO_WIDTH*VIDEO_HEIGHT*sizeof(unsigned int)); video_idx = 0; clear(); // Initialize lookup table. BrushType::create_distance_table(); // Initialize brushes. Brush::brush_type[BrushType::BRUSHTYPE_HARD].create_hard_brush(); Brush::brush_type[BrushType::BRUSHTYPE_SOFT].create_soft_brush(); Brush::brush_type[BrushType::BRUSHTYPE_CURSOR].create_cursor(); reset_brush(); lastpos = Pos(0,0); lastorgpos = Pos(0,0); lastpressure = 0; dirtymin = Pos(FLT_MAX,FLT_MAX); dirtymax = Pos(-FLT_MAX,-FLT_MAX); strokemin = Pos(0,0); strokemax = Pos(0,0); stroke = false; playing = false; playback = 0; playback_speed = 1; modified = false; idle_while_drawing = 0; drawtype = DRAWBRUSH_TYPE_NORMAL; } ~Canvas() { delete[] image; delete[] image_backup; delete[] image_shared; delete[] alpha; } // Clears the entire canvas (command history and image). void clear() { commands.clear(); clear_image(); } // Changes the size of the canvas. // Rather than trying to repaint everything from scratch, we simply quickly rescale it. void resize(int new_width, int new_height) { unsigned int* new_image = new unsigned int[new_width*new_height]; unsigned int* new_image_backup = new unsigned int[new_width*new_height]; unsigned char* new_alpha = new unsigned char[new_width*new_height]; unsigned int* new_image_shared = new unsigned int[new_width*new_height]; int dx = (1<<16) * width / new_width; int dy = (1<<16) * height / new_height; int ry = 0; for (int y = 0; y < new_height; y++) { int rx = 0; for (int x = 0; x < new_width; x++) { int sofs = (ry>>16)*width + (rx>>16); int dofs = y*new_width+x; new_image[dofs] = image[sofs]; new_image_backup[dofs] = image_backup[sofs]; new_alpha[dofs] = alpha[sofs]; new_image_shared[dofs] = image_shared[sofs]; rx += dx; } ry += dy; } delete[] image; delete[] image_backup; delete[] alpha; delete[] image_shared; width = new_width; height = new_height; image = new_image; image_backup = new_image_backup; alpha = new_alpha; image_shared = new_image_shared; } // Resets the brush to a random color and a default size and type. void reset_brush() { // Choose a suitable random brush color. srand(clock()); int c0 = rand()%3; int c1 = c0 + 1 + rand()%2; if (c1 > 2) c1 -= 3; brush.type = BrushType::BRUSHTYPE_HARD; brush.color = Color::create_from_a8r8g8b8(0xff000000 | (255 << (c0 * 8) | ((rand()%255) << (c1 * 8)))); brush.size = width/16; brush.control = Brush::BRUSHCONTROL_VARIABLESIZE; brush.opacity = 1.0f; } //--------------------------------------------------------------------------------------------- // Shared image. // // The shared image is used in collaborative painting, and contains the current 'master' state of the canvas // // The user can paint ahead of the master state, but when a new master state is received the canvas // state will be reset to the shared image. Before this happens though, the user commands are transmitted // to the activity host, so they will be received again as a new master image later and not be lost. void save_shared_image() { memcpy(image_shared, image, width*height*sizeof(unsigned int)); } void restore_shared_image() { memcpy(image, image_shared, width*height*sizeof(unsigned int)); memcpy(image_backup, image_shared, width*height*sizeof(unsigned int)); } //--------------------------------------------------------------------------------------------- // Drawing // // These methods are for drawing to and clearing the canvas. // // Drawing is not done directly to the canvas. The way the brush system works, the brush shapes are // actually drawn into the alpha channel of the canvas, and the alpha channel is used to blend the // brush color over the backup image to create the real image. // // The net effect this is that during a stroke, which may overlap itself many times over, the brush color // will only be applied to any particular canvas pixel up to the defined brush transparency level. // This is the core of our "natural media" engine. void clear_image() { memset(image, 0xff, width*height*sizeof(unsigned int)); memset(image_backup, 0xff, width*height*sizeof(unsigned int)); memset(alpha, 0, width*height*sizeof(unsigned char)); memset(image_shared, 0xff, width*height*sizeof(unsigned int)); dirtymin = Pos(0, 0); dirtymax = Pos(width, height); } // Called from command_draw and calculates a temporary brush size depending on pressure/alpha (0-255). float get_variable_brush_size(float pressure) { if (brush.control & Brush::BRUSHCONTROL_VARIABLESIZE) { float size = pressure * brush.size / 255.0f; if (size < 2.0f) size = 2.0f; return size; } else return brush.size; } // Called each tick while stylus is touching the screen and draws the selected brush into the Alpha of the Canvas. void command_draw(const Pos& pos, int pressure, bool forced) { lastorgpos = pos; if (brush.control == 0) pressure = 255; int size, opacity; if (!stroke) { // This is a new stroke. Just draw the brush on incoming position, store the information needed for interpolation and wait for next call if (brush.control & Brush::BRUSHCONTROL_VARIABLESIZE) size = int(get_variable_brush_size(pressure)); else size = brush.size; if (brush.control & Brush::BRUSHCONTROL_VARIABLEOPACITY) opacity = int(round(pressure * brush.opacity)); else opacity = int(round(255.0f * brush.opacity)); draw_brush(pos, size, opacity); // Reset stroke dirty regions strokemin = pos; strokemax = pos; lastpos = pos; lastpressure = pressure; idle_while_drawing = 0; stroke = true; } else { // This is continous stroke. Interpolate from last postion/pressure // Calculate the stroke-distance float distx = pos.x - lastpos.x; float disty = pos.y - lastpos.y; float dista = pressure - lastpressure; float distance = sqrtf(distx * distx + disty * disty); if (distance == 0.0f) // To avoid division by zero. None or very small movements are handled later distance = 0.0001f; // Calculate interpolation constants float dx = distx / distance; float dy = disty / distance; float da = dista / distance; // Calculate the spacing between two brush-stamp. Normal spacing is 25% of brushsize. float spacing = 0.225f; // Do this special spacing only for just VariableOpacity with Hard brush to avoid banding // Decrease spacing if pressure is changing rapidly if (da != 0 && brush.control == Brush::BRUSHCONTROL_VARIABLEOPACITY && brush.type == BrushType::BRUSHTYPE_HARD) spacing = min(0.225f, max(fabsf(15.0f / brush.size / (da * brush.opacity)), 0.05f)); // Calculate the distance between two brushes from spacing float spacingdistance = get_variable_brush_size(lastpressure) * spacing; if (distance < spacingdistance) { // Too small movement to interpolate idle_while_drawing++; if (idle_while_drawing > 15 || forced) { // We've been idling too long. Draw the brush and reduce idle-counter // Idle-counter is invalid during playback, since playback only records input that actually drew something idle_while_drawing = 10; lastpos = pos; lastpressure = pressure; if (brush.control & Brush::BRUSHCONTROL_VARIABLEOPACITY) draw_brush(pos, int(get_variable_brush_size(lastpressure)), int(round(pressure * brush.opacity))); else draw_brush(pos, int(get_variable_brush_size(lastpressure)), int(round(255.0f * brush.opacity))); } return; } if (brush.control & Brush::BRUSHCONTROL_VARIABLESIZE) { // Brush size is controlled by pressure while (distance >= spacingdistance) { // Interpolate stroke lastpressure += da * spacingdistance; lastpos.x += dx * spacingdistance; lastpos.y += dy * spacingdistance; distance -= spacingdistance; float brushsize = get_variable_brush_size(int(lastpressure)); if (brush.control & Brush::BRUSHCONTROL_VARIABLEOPACITY) draw_brush(lastpos, int(get_variable_brush_size(int(lastpressure))), int(round(pressure * brush.opacity))); else draw_brush(lastpos, int(get_variable_brush_size(int(lastpressure))), int(round(255.0f * brush.opacity))); // Since brush-size may have changed, we need to calculate new spacing spacingdistance = brushsize * spacing; } } else { // Brush size is static, so we can pre-multiply the interpolation constants dx *= spacingdistance; dy *= spacingdistance; da *= spacingdistance; while (distance >= spacingdistance) { lastpressure += da; lastpos.x += dx; lastpos.y += dy; distance -= spacingdistance; draw_brush(lastpos, brush.size, int(round(lastpressure * brush.opacity))); } } } } // Called when stylus stops touching the screen. void command_enddraw() { if (!stroke) return; // Copy current image to backup image and clear alpha in the region of the stroke. int x0 = max(min(int(strokemin.x), width), 0); int x1 = max(min(int(strokemax.x), width), 0); int y0 = max(min(int(strokemin.y), height), 0); int y1 = max(min(int(strokemax.y), height), 0); for (int y = y0; y < y1; y++) { memcpy(&image_backup[y*width+x0], &image[y*width+x0], (x1-x0)*sizeof(unsigned int)); memset(&alpha[y*width+x0], 0, (x1-x0)*sizeof(unsigned char)); } stroke = false; } // The canvas keeps track of the area that has been modified by draw commands, this is called the 'dirty' rectangle. // The dirty rectangle keeps accumulating until reset_dirty_rect is called, at which point it is cleared to empty. void reset_dirty_rect() { dirtymin = Pos(FLT_MAX,FLT_MAX); dirtymax = Pos(-FLT_MAX,-FLT_MAX); } // Rasters a brush with specified width and opacity into alpha at a specified position using lookup-tables. void draw_brush(const Pos& pos, int brushwidth, int opacity) { //printf("draw_brush %f,%f width=%d opacity=%d\n", pos.x, pos.y, brushwidth, opacity); // Enforce minimum brush size. if (brushwidth<2) brushwidth = 2; // Calculate drawing rectangle. float halfwidth = brushwidth/2; float p0x = pos.x - halfwidth; float p0y = pos.y - halfwidth; float p1x = pos.x + halfwidth + 1; float p1y = pos.y + halfwidth + 1; int x0 = int(max(min(p0x, p1x), 0.0f)); int x1 = int(min(max(p0x, p1x), float(width))); int y0 = int(max(min(p0y, p1y), 0.0f)); int y1 = int(min(max(p0y, p1y), float(height))); // Accumulate dirty regions. strokemin = Pos::create_from_min(strokemin, Pos(x0, y0)); strokemax = Pos::create_from_max(strokemax, Pos(x1, y1)); dirtymin = Pos::create_from_min(dirtymin, Pos(x0, y0)); dirtymax = Pos::create_from_max(dirtymax, Pos(x1, y1)); // Calculate interpolation constants float db = (BrushType::DIST_TABLE_WIDTH-1) / float(brushwidth); float xb = max(0.0f, BrushType::DIST_TABLE_CENTER - (pos.x - x0) * db); float yb = max(0.0f, BrushType::DIST_TABLE_CENTER - (pos.y - y0) * db); // Select which line of the brush-lookup-table to use that most closely matches the current brush width int brushidx = int(float(BrushType::BRUSH_TABLE_HEIGHT) / brushwidth); // Interpolate the distance table over the area. For each pixel find the distance, and look the // brush-intensity up in the brush-table if (drawtype == DRAWBRUSH_TYPE_NORMAL) { for (int y = y0; y < y1; y++) { float x2b = xb; for (int x = x0; x < x1; x++) { // Find brush-intensity and mulitply that with incoming opacity int lookup = BrushType::distance_tbl[int(x2b)][int(yb)]; int intensity = fixed_scale(Brush::brush_type[brush.type].intensity_tbl[lookup][brushidx], opacity); // New Alpha = Brush Intensity + Old Alpha - (Brush Intensity * Old Alpha) // Also make sure the result is clamped to the incoming opacity and isn't lower than the alpha // already stored int base = alpha[y*width+x]; int a = max(min(intensity + base - ((intensity * base) >> 8), opacity), base); alpha[y*width+x] = a; Color i = Color::create_from_a8r8g8b8(image_backup[y*width+x]); i = Color::create_from_lerp(brush.color, i, a); image[y*width+x] = i.get_a8r8g8b8(); x2b += db; } yb += db; } } else if (drawtype == DRAWBRUSH_TYPE_GETCOLOR) { // Calculate color from a weighted average of the area that the brush touches. Color c(0, 0, 0, 0); for (int y = y0; y < y1; y++) { float x2b = xb; for (int x = x0; x < x1; x++) { int lookup = BrushType::distance_tbl[int(x2b)][int(yb)]; int intensity = fixed_scale(Brush::brush_type[brush.type].intensity_tbl[lookup][brushidx], opacity); Color i = Color::create_from_a8r8g8b8(image[y*width+x]); c.r += i.r * intensity; c.g += i.g * intensity; c.b += i.b * intensity; c.a += intensity; x2b += db; } yb += db; } brush.color.r = c.r / c.a; brush.color.g = c.g / c.a; brush.color.b = c.b / c.a; } } // Return the color underneath the pos. Color pickup_color(const Pos& pos) { int x = int(max(min(pos.x, float(width)), 0.0f)); int y = int(max(min(pos.y, float(height)), 0.0f)); return Color::create_from_a8r8g8b8(image[y*width+x]); } //--------------------------------------------------------------------------------------------- // Playback // // These allow the canvas to be treated like a VCR, playing back and rewinding drawing commands // to recreate the state of the canvas at different times. void add_command(const DrawCommand& cmd) { commands.push_back(cmd); modified = true; } void play_command(const DrawCommand& cmd, bool add) { if (cmd.type == DrawCommand::TYPE_DRAW) { #ifdef CANVAS_DEBUG_COMMANDS printf("TYPE_DRAW x=%f y=%f pressure=%d\n", cmd.pos.x, cmd.pos.y, cmd.pressure); #endif Pos relpos = cmd.pos * Pos(width, height); bool forcedraw = !add; // If we are not adding, we are playing back command_draw(relpos, cmd.pressure, forcedraw); } else if (cmd.type == DrawCommand::TYPE_DRAWEND) { #ifdef CANVAS_DEBUG_COMMANDS printf("TYPE_DRAWEND pressure=%d\n", cmd.pressure); #endif //if self.stroke and (self.lastpos.x != self.lastorgpos.x or self.lastpos.y != self.lastorgpos.y): // self.command_draw(self.lastorgpos, cmd.pressure, add) command_enddraw(); } else if (cmd.type == DrawCommand::TYPE_COLORCHANGE) { #ifdef CANVAS_DEBUG_COMMANDS printf("TYPE_COLORCHANGE flipx=%d flipy=%d r=%d g=%d b=%d a=%d\n", cmd.flipx, cmd.flipy, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a); #endif if (cmd.flipx || cmd.flipy) { // fixme //pygame.transform.flip(self.image, cmd.flipx, cmd.flipy) } else brush.color = cmd.color; } else if (cmd.type == DrawCommand::TYPE_SIZECHANGE) { #ifdef CANVAS_DEBUG_COMMANDS printf("TYPE_SIZECHANGE size=%f control=%d type=%d opacity=%f\n", cmd.size, cmd.brush_control, cmd.brush_type, cmd.opacity); #endif brush.size = int(cmd.size * width); if (brush.size < 2) brush.size = 2; brush.control = cmd.brush_control; brush.type = cmd.brush_type; if (cmd.opacity > 0) brush.opacity = cmd.opacity; } #ifdef CANVAS_DEBUG_COMMANDS fflush(stdout); #endif if (add) add_command(cmd); } bool playback_done() { return playback < 0 || playback >= (int)commands.size(); } int playback_length() { return (int)commands.size(); } int playback_pos() { return playback; } void start_playback() { command_enddraw(); clear_image(); playback = 0; playing = true; } void pause_playback() { playing = false; } void resume_playback() { playing = true; } void stop_playback() { command_enddraw(); playback = -1; playing = false; } void finish_playback() { while (!playback_done()) play_command(commands[playback++], false); } // This is used to avoid leaving the playback state in the middle of a stroke. void playback_finish_stroke() { while (stroke && !playback_done()) play_command(commands[playback++], false); } void playback_to(int pos) { while (playback < pos && !playback_done()) play_command(commands[playback++], false); } void playback_step_to(int pos) { if (playback < pos && !playback_done()) play_command(commands[playback++], false); } // Same as playback_to, except it breaks if more than timeout seconds are taken. void playback_to_timed(int pos, float timeout) { clock_t start = clock(); clock_t end = (clock_t)(start + timeout * CLOCKS_PER_SEC); printf("start: %d end: %d CLOCKS_PER_SEC: %d\n", (int)start, (int)end, (int)CLOCKS_PER_SEC); while (playback < pos && !playback_done() && clock() < end) play_command(commands[playback++], false); //if (clock() > end) // printf("killed by timeout.\n"); } void set_playback_speed(int speed) { playback_speed = speed; } void truncate_at_playback() { commands.resize(playback+1); } void update_playback() { if (playing) { for (int i = 0; i < playback_speed; i++) { if (!playback_done()) play_command(commands[playback++], false); } } } int get_num_commands() { return commands.size(); } void play_range(int from, int to) { for (int i = from; i < to; i++) play_command(commands[i], false); } //--------------------------------------------------------------------------------------------- // Blit // // Draws a region of the canvas into a GdkImage for display on the screen, with optional scaling // and darkening. inline void to_pixel(guint src, depth16_t *dst) { guint r = (((src>>16)&0xff)>>3)<<11; guint g = (((src>> 8)&0xff)>>2)<<5; guint b = (((src>> 0)&0xff)>>3); *dst = r|g|b; } inline void to_pixel(guint src, depth24_t *dst) { *dst = src & 0xffffff; } struct scale1_t { template inline static void fill_pixel(pixel_t **rows, pixel_t value) { *rows[0]++ = value; } }; struct scale2_t { template inline static void fill_pixel(pixel_t **rows, pixel_t value) { *rows[0]++ = value; *rows[0]++ = value; *rows[1]++ = value; *rows[1]++ = value; } }; struct scale4_t { template inline static void fill_pixel(pixel_t **rows, pixel_t value) { *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; } }; struct scale8_t { template inline static void fill_pixel(pixel_t **rows, pixel_t value) { *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[0]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[1]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[2]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[3]++ = value; *rows[4]++ = value; *rows[4]++ = value; *rows[4]++ = value; *rows[4]++ = value; *rows[4]++ = value; *rows[4]++ = value; *rows[4]++ = value; *rows[4]++ = value; *rows[5]++ = value; *rows[5]++ = value; *rows[5]++ = value; *rows[5]++ = value; *rows[5]++ = value; *rows[5]++ = value; *rows[5]++ = value; *rows[5]++ = value; *rows[6]++ = value; *rows[6]++ = value; *rows[6]++ = value; *rows[6]++ = value; *rows[6]++ = value; *rows[6]++ = value; *rows[6]++ = value; *rows[6]++ = value; *rows[7]++ = value; *rows[7]++ = value; *rows[7]++ = value; *rows[7]++ = value; *rows[7]++ = value; *rows[7]++ = value; *rows[7]++ = value; *rows[7]++ = value; } }; template inline void blit(GdkImage *img, int src_x, int src_y, int dest_x, int dest_y, int dest_w, int dest_h, bool overlay) { pixel_t *pixels = (pixel_t*)img->mem; int pitch = img->bpl/sizeof(pixel_t); // Clip rectangle. // Clip destination rectangle. Source clipping is handled per pixel. if (dest_x < 0) { dest_w += dest_x; dest_x = 0; } if (dest_y < 0) { dest_h += dest_y; dest_y = 0; } if (dest_x + dest_w > img->width-scale) dest_w = (img->width-scale) - dest_x; if (dest_y + dest_h > img->height-scale) dest_h = (img->height-scale) - dest_y; int csy = src_y; for (int cdy = dest_y; cdy < dest_y+dest_h; cdy += scale) { pixel_t* __restrict rows[scale]; rows[0] = &pixels[cdy*pitch+dest_x]; for (int i = 1; i < scale; ++i) rows[i] = rows[i-1] + pitch; if (csy < 0 || csy >= height) { for (int cdx = 0; cdx < dest_w; cdx += scale) { pixel_t p = 0; scale_t::fill_pixel((pixel_t**)rows, p); } } else { unsigned int* __restrict src = &image[csy*width+src_x]; int cdx = 0; int csx = src_x; while (csx < 0 && cdx < dest_w) { pixel_t p = 0; scale_t::fill_pixel((pixel_t**)rows, p); src++; csx++; cdx += scale; } if (overlay) { while (csx < width && cdx < dest_w) { unsigned int p = *src; p &= ~0x03030303; p >>= 2; pixel_t rgb; to_pixel(p, &rgb); scale_t::fill_pixel((pixel_t**)rows, rgb); src++; csx++; cdx += scale; } } else { while (csx < width && cdx < dest_w) { pixel_t rgb; to_pixel(*src, &rgb); scale_t::fill_pixel((pixel_t**)rows, rgb); src++; csx++; cdx += scale; } } while (cdx < dest_w) { pixel_t p = 0; scale_t::fill_pixel((pixel_t**)rows, p); src++; csx++; cdx += scale; } } csy++; } } template inline void blit_x(GdkImage *img, int src_x, int src_y, int dest_x, int dest_y, int dest_w, int dest_h, bool overlay) { if (img->depth == 16) blit(img, src_x, src_y, dest_x, dest_y, dest_w, dest_h, overlay); else blit(img, src_x, src_y, dest_x, dest_y, dest_w, dest_h, overlay); } void blit_1x(GdkImage* img, int src_x, int src_y, int dest_x, int dest_y, int dest_w, int dest_h, bool overlay) { blit_x (img, src_x, src_y, dest_x, dest_y, dest_w, dest_h, overlay); } void blit_2x(GdkImage* img, int src_x, int src_y, int dest_x, int dest_y, int dest_w, int dest_h, bool overlay) { blit_x (img, src_x, src_y, dest_x, dest_y, dest_w, dest_h, overlay); } void blit_4x(GdkImage* img, int src_x, int src_y, int dest_x, int dest_y, int dest_w, int dest_h, bool overlay) { blit_x (img, src_x, src_y, dest_x, dest_y, dest_w, dest_h, overlay); } void blit_8x(GdkImage* img, int src_x, int src_y, int dest_x, int dest_y, int dest_w, int dest_h, bool overlay) { blit_x (img, src_x, src_y, dest_x, dest_y, dest_w, dest_h, overlay); } //--------------------------------------------------------------------------------------------- // Videopaint // // This code currently attempts to track a green object in the webcam and treat it as a // mouse cursor, using the detected size to control the brush size. void downsize_video(unsigned int* dest_pixels, GstBuffer* buf, int vwidth, int vheight) { if (vwidth != VIDEO_WIDTH*8 || vheight != VIDEO_HEIGHT*8 || buf->size != vwidth*vheight*sizeof(unsigned short)) { printf("Invalid Gst video buffer size %d (%dx%d)\n", buf->size, vwidth, vheight); return; } unsigned int* source_pixels = (unsigned int*)buf->data; for (int y = 0; y < VIDEO_HEIGHT; y++) { unsigned int* __restrict src = &source_pixels[(y*4)*vwidth]; unsigned int* __restrict dest = &dest_pixels[y*VIDEO_WIDTH]; for (int x = 0; x < VIDEO_WIDTH; x++) { *dest = *src; src += 4; dest++; } } } void videopaint_motion(GstBuffer* buf, int vwidth, int vheight) { downsize_video(image_video[1], buf, vwidth, vheight); unsigned int* pixels0 = image_video[0]; unsigned int* pixels1 = image_video[1]; double cx = 0, cy = 0, cnt = 0; for (int y = 0; y < VIDEO_HEIGHT; y++) { unsigned int* __restrict row = &pixels1[y*VIDEO_WIDTH]; unsigned int* destrow = &pixels0[y*VIDEO_WIDTH]; for (int x = 0; x < VIDEO_WIDTH; x++) { // Convert YUYV to HSV Color c = Color::yuv_to_hsv(*row); // Threshold green if ((c.r > 80) && (c.r < 150) && (c.g > 100)) { destrow[0] = 0xffff; cnt++; cx += (80-x); cy += y; } else { destrow[0] = 0; } row++; destrow++; } } if (cnt > 0) { cx /= cnt; cy /= cnt; // The mouse coordinates are scaled somewhat, as the nature of the video processing causes the blob to leave // the screen partially and become smaller (and less significant) towards the edges. videopaint_pos = Pos( map_range(cx, 0, VIDEO_WIDTH, -0.2f, 1.2f), map_range(cy, 0, VIDEO_HEIGHT, -0.2f, 1.2f)); // todo- estimate size videopaint_pressure = map_range(cnt, 0, (VIDEO_WIDTH*VIDEO_HEIGHT)/8, 0, 255); // Mark mouse pixel in red. int rx = int(cx); int ry = int(cy); unsigned short red = Color(255,0,0,0).get_r5g6b5(); for (int x = -3; x <= 3; x++) for (int y = -3; y <= 3; y++) image_video[0][ry+y*VIDEO_WIDTH+rx+x] = red; } } void blit_videopaint(GdkImage* img) { unsigned short* pixels = (unsigned short*)img->mem; int pitch = img->bpl/sizeof(unsigned short); for (int y = 0; y < VIDEO_HEIGHT; y++) { unsigned int* __restrict src = &image_video[0][y*VIDEO_WIDTH]; unsigned short* __restrict dest = &pixels[y*pitch]; for (int x = 0; x < VIDEO_WIDTH; x++) { unsigned int p = *src++; unsigned int r = (((p>>16)&0xff)>>3)<<11; unsigned int g = (((p>> 8)&0xff)>>2)<<5; unsigned int b = (((p>> 0)&0xff)>>3); unsigned int rgb = r|g|b; *dest++ = rgb; } } } //--------------------------------------------------------------------------------------------- // Reference image // // The reference image is a snapshot taken by the webcam that can transparently displayed over the canvas. // Note that painting currently cannot take place while the reference image is shown. void set_reference_buffer(GstBuffer* buf, int vwidth, int vheight) { if (vwidth != REFERENCE_WIDTH || vheight != REFERENCE_HEIGHT || buf->size != vwidth*vheight*sizeof(unsigned short)) { printf("Invalid Gst reference buffer size %d\n", buf->size); return; } memcpy(image_reference, buf->data, min(size_t(buf->size), size_t(REFERENCE_WIDTH*REFERENCE_HEIGHT*sizeof(unsigned short)))); } void render_reference_overlay() { // Uses image_backup to blend over the canvas without affecting the contents of the canvas. // Scales up from whatever REFERENCE_WIDTH/REFERENCE_HEIGHT are to the canvas size. int dx = (1<<16) * REFERENCE_WIDTH / width; int dy = (1<<16) * REFERENCE_HEIGHT / height; int ry = 0; for (int y = 0; y < height; y++, ry += dy) { int rx = 0; for (int x = 0; x < width; x++, rx += dx) { Color r = Color::create_from_r5g6b5(image_reference[(ry>>16)*REFERENCE_WIDTH+(rx>>16)]); r = Color::create_from_yuv(r.r, r.g, r.b); Color b = Color::create_from_a8r8g8b8(image_backup[y*width+x]); image[y*width+x] = Color::create_from_lerp(r, b, 192).get_a8r8g8b8(); } } } //--------------------------------------------------------------------------------------------- // Overlay // // These functions basically just temporarily darken the entire canvas so that a PyGTK overlay // like the brush controls can be drawn on top, and look like it has a translucent black background. void render_overlay() { // Since the image is backed up in image_backup, it's ok to destroy the contents of image since // clear_overlay will just restore it from image_backup. for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { unsigned int i = image_backup[y*width+x]; i &= ~0x03030303; i >>= 2; image[y*width+x] = i; } } void clear_overlay() { memcpy(image, image_backup, width*height*sizeof(unsigned int)); } //--------------------------------------------------------------------------------------------- // Load & Save void upgrade_drw_header(DRW_Header* hdr, DRW_Command* cmds) { if (hdr->version == DRW_Header::ID) // Paying for old bug hdr->version = 1002; if (hdr->version < DRW_VERSION) { for(int i = 0; i < hdr->ncommands; i++) { DRW_Command* cmd = &cmds[i]; if (hdr->version < 1001) { if (cmd->type == DrawCommand::TYPE_DRAW) { cmd->x = int(round(cmd->x * 1024.0f / 2047.0f + 512.0f)); cmd->x = int(round(cmd->y * 1024.0f / 2047.0f + 512.0f)); } } if (hdr->version < 1002) { if (cmd->type == DrawCommand::TYPE_SIZECHANGE) { int type = (cmd->brushtype << 2) | cmd->brushcontrol; switch(type) { case 0: cmd->brushtype = BrushType::BRUSHTYPE_HARD; cmd->brushcontrol = Brush::BRUSHCONTROL_VARIABLEOPACITY; break; case 2: cmd->brushtype = BrushType::BRUSHTYPE_SOFT; cmd->brushcontrol = Brush::BRUSHCONTROL_VARIABLEOPACITY; break; case 4: cmd->brushtype = BrushType::BRUSHTYPE_HARD; cmd->brushcontrol = 0; break; case 6: cmd->brushtype = BrushType::BRUSHTYPE_SOFT; cmd->brushcontrol = 0; break; } cmd->size -= (1 << 6); } } } hdr->version = DRW_VERSION; } } bool load(const char* filename) { FILE* drwfile = fopen(filename, "rb"); if (!drwfile) return false; int r; DRW_Header header; r = fread(&header, 1, sizeof(DRW_Header), drwfile); // Backward compatible code for early versions when there was no header and the filesize is used // to determine the number of commands. if (header.id != DRW_Header::ID) { header.colorsversion_initial = 0; fseek(drwfile, 0, SEEK_END); header.ncommands = ftell(drwfile) / 4; fseek(drwfile, 0, SEEK_SET); } DRW_Command* cmds = (DRW_Command*)malloc(header.ncommands*sizeof(DRW_Command)); r = fread(cmds, 1, header.ncommands*sizeof(DRW_Command), drwfile); fclose(drwfile); upgrade_drw_header(&header, cmds); clear(); convert_from_drw(cmds, 0, header.ncommands); free(cmds); return true; } bool save(const char* filename) { FILE* drwfile = fopen(filename, "wb"); if (!drwfile) return false; int r; DRW_Header header; header.id = DRW_Header::ID; header.version = DRW_VERSION; header.colorsversion_initial = DRW_VERSION; header.colorsversion_saved = DRW_VERSION; header.strokes = 0; header.time = 0; header.timessaved = 0; header.ncommands = commands.size(); r = fwrite(&header, 1, sizeof(DRW_Header), drwfile); DRW_Command* cmds; convert_to_drw(&cmds, 0, commands.size()); r = fwrite(cmds, sizeof(DRW_Command), commands.size(), drwfile); free(cmds); fclose(drwfile); return true; } void convert_from_drw(DRW_Command* cmds, int start, int ncommands) { commands.resize(start+ncommands); for (int i = 0; i < ncommands; i++) { DRW_Command* drw = &cmds[i]; DrawCommand* cmd = &commands[start+i]; cmd->type = drw->type; cmd->pos.x = (drw->x-512.0f) / 1024.0f; cmd->pos.y = (drw->y-512.0f) / 1024.0f; cmd->pressure = drw->alpha; cmd->color = Color::create_from_a8r8g8b8(drw->col); cmd->flipx = drw->flipx; cmd->flipy = drw->flipy; cmd->brush_control = drw->brushcontrol; cmd->brush_type = drw->brushtype; cmd->size = drw->size / float(1 << 15); cmd->opacity = drw->opacity / 255.0f; } } void convert_to_drw(DRW_Command** cmds, int start, int ncommands) { *cmds = (DRW_Command*)malloc(sizeof(DRW_Command) * ncommands); for (int i = 0; i < ncommands; i++) { DRW_Command* drw = &(*cmds)[i]; DrawCommand* cmd = &commands[start+i]; drw->type = cmd->type; if (cmd->type == DrawCommand::TYPE_DRAW) { drw->x = int((cmd->pos.x*1024)+512); drw->y = int((cmd->pos.y*1024)+512); drw->alpha = cmd->pressure; } else if (cmd->type == DrawCommand::TYPE_DRAWEND) { drw->alpha = cmd->pressure; } else if (cmd->type == DrawCommand::TYPE_COLORCHANGE) { drw->flipx = cmd->flipx; drw->flipy = cmd->flipy; drw->col = cmd->color.get_a8r8g8b8(); } else if (cmd->type == DrawCommand::TYPE_SIZECHANGE) { drw->brushcontrol = cmd->brush_control; drw->brushtype = cmd->brush_type; drw->size = int(cmd->size * float(1<<15)); drw->opacity = int(cmd->opacity * 255.0f); } } } DrawCommandBuffer send_drw_commands(int start, int ncommands) { DrawCommandBuffer buf; buf.ncommands = ncommands; convert_to_drw((DRW_Command**)&buf.cmds, start, ncommands); return buf; } void receive_drw_commands(const DrawCommandBuffer& buf, int start) { convert_from_drw((DRW_Command*)buf.cmds, start, buf.ncommands); } }; #endif