From b999a05ad78264d6b7975bf39c8297f36a514ed1 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Wed, 18 Feb 2009 03:58:55 +0000 Subject: Add Makefile to build binary package in convenient way --- (limited to 'colorsc') diff --git a/colorsc/Makefile b/colorsc/Makefile new file mode 100644 index 0000000..3b0055b --- /dev/null +++ b/colorsc/Makefile @@ -0,0 +1,11 @@ +all: _colorsc.so + +clean: + rm -rf _colorsc.so *_wrap.cxx colorsc.py colorsc.pyc build + +%_wrap.cxx: %.i + swig -c++ -python $< + +_colorsc.so: colorsc_wrap.cxx colorsc.h canvas.h canvas.cpp + python setup.py build_ext --inplace + diff --git a/colorsc/__init__.py b/colorsc/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/colorsc/__init__.py diff --git a/colorsc/canvas.cpp b/colorsc/canvas.cpp new file mode 100644 index 0000000..ecdc11e --- /dev/null +++ b/colorsc/canvas.cpp @@ -0,0 +1,28 @@ +/* + 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 . +*/ +#include "canvas.h" + +unsigned char BrushType::distance_tbl[DIST_TABLE_WIDTH][DIST_TABLE_WIDTH]; + +BrushType Brush::brush_type[BrushType::NUM_BRUSHES]; + +void test_method(void* data) +{ +} + + diff --git a/colorsc/canvas.h b/colorsc/canvas.h new file mode 100644 index 0000000..6706c36 --- /dev/null +++ b/colorsc/canvas.h @@ -0,0 +1,1848 @@ +/* + 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 "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. + + 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) + { + unsigned short* pixels = (unsigned short*)img->mem; + int pitch = img->bpl/sizeof(unsigned short); + + // 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) + dest_w = img->width - dest_x; + if (dest_y+dest_h > img->height) + dest_h = img->height - dest_y; + + int csy = src_y; + for (int cdy = dest_y; cdy < dest_y+dest_h; cdy++) + { + unsigned short* __restrict row = &pixels[cdy*pitch+dest_x]; + + // If out of bounds vertically, fill row with the background color. + if (csy < 0 || csy >= height) + { + for (int cdx = 0; cdx < dest_w; cdx++) + { + unsigned int rgb = 0; + *row++ = rgb; + } + } + else + { + unsigned int* __restrict src = &image[csy*width+src_x]; + + int cdx = 0; + int csx = src_x; + + // Fill any portion that is to the left of the src image with + // background color. + while (csx < 0 && cdx < dest_w) + { + unsigned int rgb = 0; + *row++ = rgb; + src++; + csx++; + cdx++; + } + + // Copy the pixels. + if (overlay) + { + while (csx < width && cdx < dest_w) + { + unsigned int p = *src; + p &= ~0x03030303; + p >>= 2; + 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; + *row++ = rgb; + src++; + csx++; + cdx++; + } + } + else + { + while (csx < width && cdx < dest_w) + { + 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; + *row++ = rgb; + src++; + csx++; + cdx++; + } + } + + // Fill any portion to the right of src with background pixels. + while (cdx < dest_w) + { + unsigned int rgb = 0; + *row++ = rgb; + src++; + csx++; + cdx++; + } + } + + csy++; + } + } + + 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) + { + unsigned short* pixels = (unsigned short*)img->mem; + int pitch = img->bpl/sizeof(unsigned short); + + // 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-2) + dest_w = (img->width-2) - dest_x; + if (dest_y+dest_h > img->height-2) + dest_h = (img->height-2) - dest_y; + + int csy = src_y; + for (int cdy = dest_y; cdy < dest_y+dest_h; cdy += 2) + { + unsigned short* __restrict row0 = &pixels[cdy*pitch+dest_x]; + unsigned short* __restrict row1 = row0 + pitch; + + // If out of bounds vertically, fill row with the background color. + if (csy < 0 || csy >= height) + { + for (int cdx = 0; cdx < dest_w; cdx += 2) + { + unsigned int rgb = 0; + row0[0] = rgb; + row0[1] = rgb; + row1[0] = rgb; + row1[1] = rgb; + row0 += 2; + row1 += 2; + } + } + else + { + unsigned int* __restrict src = &image[csy*width+src_x]; + + int cdx = 0; + int csx = src_x; + + // Fill any portion that is to the left of the src image with + // background color. + while (csx < 0 && cdx < dest_w) + { + unsigned int rgb = 0; + row0[0] = rgb; + row0[1] = rgb; + row1[0] = rgb; + row1[1] = rgb; + row0 += 2; + row1 += 2; + src++; + csx++; + cdx += 2; + } + + // Copy the pixels. + if (overlay) + { + while (csx < width && cdx < dest_w) + { + unsigned int p = *src; + p &= ~0x03030303; + p >>= 2; + 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; + row0[0] = rgb; + row0[1] = rgb; + row1[0] = rgb; + row1[1] = rgb; + row0 += 2; + row1 += 2; + src++; + csx++; + cdx += 2; + } + } + else + { + while (csx < width && cdx < dest_w) + { + 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; + row0[0] = rgb; + row0[1] = rgb; + row1[0] = rgb; + row1[1] = rgb; + row0 += 2; + row1 += 2; + src++; + csx++; + cdx += 2; + } + } + + // Fill any portion to the right of src with background pixels. + while (cdx < dest_w) + { + unsigned int rgb = 0; + row0[0] = rgb; + row0[1] = rgb; + row1[0] = rgb; + row1[1] = rgb; + row0 += 2; + row1 += 2; + src++; + csx++; + cdx += 2; + } + } + + csy++; + } + } + + 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) + { + unsigned short* pixels = (unsigned short*)img->mem; + int pitch = img->bpl/sizeof(unsigned short); + + // 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-4) + dest_w = (img->width-4) - dest_x; + if (dest_y + dest_h > img->height-4) + dest_h = (img->height-4) - dest_y; + + int csy = src_y; + for (int cdy = dest_y; cdy < dest_y+dest_h; cdy += 4) + { + unsigned short* __restrict row0 = &pixels[cdy*pitch+dest_x]; + unsigned short* __restrict row1 = row0 + pitch; + unsigned short* __restrict row2 = row1 + pitch; + unsigned short* __restrict row3 = row2 + pitch; + +#define FILL_PIXEL(rgb) \ + row0[0] = rgb; row0[1] = rgb; row0[2] = rgb; row0[3] = rgb; \ + row1[0] = rgb; row1[1] = rgb; row1[2] = rgb; row1[3] = rgb; \ + row2[0] = rgb; row2[1] = rgb; row2[2] = rgb; row2[3] = rgb; \ + row3[0] = rgb; row3[1] = rgb; row3[2] = rgb; row3[3] = rgb; \ + row0 += 4; row1 += 4; row2 += 4; row3 += 4; + + if (csy < 0 || csy >= height) + { + for (int cdx = 0; cdx < dest_w; cdx += 4) + { + FILL_PIXEL(0) + } + } + else + { + unsigned int* __restrict src = &image[csy*width+src_x]; + + int cdx = 0; + int csx = src_x; + + while (csx < 0 && cdx < dest_w) + { + FILL_PIXEL(0) + + src++; + csx++; + cdx += 4; + } + + if (overlay) + { + while (csx < width && cdx < dest_w) + { + unsigned int p = *src; + p &= ~0x03030303; + p >>= 2; + 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; + FILL_PIXEL(rgb) + + src++; + csx++; + cdx += 4; + } + } + else + { + while (csx < width && cdx < dest_w) + { + 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; + FILL_PIXEL(rgb) + + src++; + csx++; + cdx += 4; + } + } + + while (cdx < dest_w) + { + FILL_PIXEL(0) + + src++; + csx++; + cdx += 4; + } + } + +#undef FILL_PIXEL + + csy++; + } + } + + 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) + { + unsigned short* pixels = (unsigned short*)img->mem; + int pitch = img->bpl/sizeof(unsigned short); + + // 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-8) + dest_w = (img->width-8) - dest_x; + if (dest_y + dest_h > img->height-8) + dest_h = (img->height-8) - dest_y; + + int csy = src_y; + for (int cdy = dest_y; cdy < dest_y+dest_h; cdy += 8) + { + unsigned short* __restrict row0 = &pixels[cdy*pitch+dest_x]; + unsigned short* __restrict row1 = row0 + pitch; + unsigned short* __restrict row2 = row1 + pitch; + unsigned short* __restrict row3 = row2 + pitch; + unsigned short* __restrict row4 = row3 + pitch; + unsigned short* __restrict row5 = row4 + pitch; + unsigned short* __restrict row6 = row5 + pitch; + unsigned short* __restrict row7 = row6 + pitch; + +#define FILL_PIXEL(rgb) \ + row0[0] = rgb; row0[1] = rgb; row0[2] = rgb; row0[3] = rgb; row0[4] = rgb; row0[5] = rgb; row0[6] = rgb; row0[7] = rgb; \ + row1[0] = rgb; row1[1] = rgb; row1[2] = rgb; row1[3] = rgb; row1[4] = rgb; row1[5] = rgb; row1[6] = rgb; row1[7] = rgb; \ + row2[0] = rgb; row2[1] = rgb; row2[2] = rgb; row2[3] = rgb; row2[4] = rgb; row2[5] = rgb; row2[6] = rgb; row2[7] = rgb; \ + row3[0] = rgb; row3[1] = rgb; row3[2] = rgb; row3[3] = rgb; row3[4] = rgb; row3[5] = rgb; row3[6] = rgb; row3[7] = rgb; \ + row4[0] = rgb; row4[1] = rgb; row4[2] = rgb; row4[3] = rgb; row4[4] = rgb; row4[5] = rgb; row4[6] = rgb; row4[7] = rgb; \ + row5[0] = rgb; row5[1] = rgb; row5[2] = rgb; row5[3] = rgb; row5[4] = rgb; row5[5] = rgb; row5[6] = rgb; row5[7] = rgb; \ + row6[0] = rgb; row6[1] = rgb; row6[2] = rgb; row6[3] = rgb; row6[4] = rgb; row6[5] = rgb; row6[6] = rgb; row6[7] = rgb; \ + row7[0] = rgb; row7[1] = rgb; row7[2] = rgb; row7[3] = rgb; row7[4] = rgb; row7[5] = rgb; row7[6] = rgb; row7[7] = rgb; \ + row0 += 8; row1 += 8; row2 += 8; row3 += 8; row4 += 8; row5 += 8; row6 += 8; row7 += 8; + + if (csy < 0 || csy >= height) + { + for (int cdx = 0; cdx < dest_w; cdx += 8) + { + FILL_PIXEL(0) + } + } + else + { + unsigned int* __restrict src = &image[csy*width+src_x]; + + int cdx = 0; + int csx = src_x; + + while (csx < 0 && cdx < dest_w) + { + FILL_PIXEL(0) + + src++; + csx++; + cdx += 8; + } + + if (overlay) + { + while (csx < width && cdx < dest_w) + { + unsigned int p = *src; + p &= ~0x03030303; + p >>= 2; + 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; + FILL_PIXEL(rgb) + + src++; + csx++; + cdx += 8; + } + } + else + { + while (csx < width && cdx < dest_w) + { + 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; + FILL_PIXEL(rgb) + + src++; + csx++; + cdx += 8; + } + } + + while (cdx < dest_w) + { + unsigned int rgb = 0; + FILL_PIXEL(rgb) + + src++; + csx++; + cdx += 8; + } + } + +#undef FILL_PIXEL + + csy++; + } + } + + //--------------------------------------------------------------------------------------------- + // 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 + diff --git a/colorsc/colorsc.h b/colorsc/colorsc.h new file mode 100644 index 0000000..543a2ce --- /dev/null +++ b/colorsc/colorsc.h @@ -0,0 +1,205 @@ +/* + 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 _COLORSC_H_ +#define _COLORSC_H_ + +#include + +#include +#include +using namespace std; + +#include +#include + +// todo- Include the real GTK headers when installed. +#include "gtk_types.h" + +static const float PI = 3.14159f; + +inline float sgn(float a) { if (a>0) return 1; if (a<0) return -1; return 0; } + +inline float sqr(float a) { return a*a; } + +inline float clamp(float a, float mn, float mx) { return max(mn, min(mx, a)); } + +inline float to_rad(float degrees) { return degrees * PI/180.0f; } +inline float to_deg(float rads) { return rads * 180.0f/PI; } + +// Maps an incoming value in a given f0-t0 range to a new value in the f1-t1 range, optionally clamping to f1-t1. +inline float map_range(float a, float f0, float t0, float f1, float t1, bool clmp=true) +{ + float r = (a-f0)/(t0-f0); + if (clmp) r = clamp(r, 0.0f, 1.0f); + return f1+r*(t1-f1); +} + +// Scales a value by an 8bit scale factor from 0-255. The MSB of the scale factor is added to it, which means that +// scaling by 255 is an identity. +inline int fixed_scale(int value, int scale) +{ + scale += scale >> 7; + return (value * scale) >> 8; +} + +inline unsigned short endian_swap(unsigned short v) +{ + return (v>>8) | (v<<8); +} + +struct Pos +{ + float x, y; + Pos() : x(0), y(0) {} + Pos(float x, float y) : x(x), y(y) {} + Pos operator+(const Pos& b) const { return Pos(x+b.x, y+b.y); } + Pos operator-(const Pos& b) const { return Pos(x-b.x, y-b.y); } + Pos operator*(const Pos& b) const { return Pos(x*b.x, y*b.y); } + Pos operator/(const Pos& b) const { return Pos(x/b.x, y/b.y); } + Pos operator*(float b) const { return Pos(x*b, y*b); } + Pos operator/(float b) const { return Pos(x/b, y/b); } + static Pos create_from_min(const Pos& a, const Pos& b) { return Pos(min(a.x,b.x), min(a.y,b.y)); } + static Pos create_from_max(const Pos& a, const Pos& b) { return Pos(max(a.x,b.x), max(a.y,b.y)); } + static Pos create_from_angle(float a, float r) { return Pos(cosf(a*PI/180.0f)*r, sinf(a*PI/180.0f)*r); } + static Pos create_from_rotation(const Pos& a, const Pos& center, float t) + { + return Pos( + (a.x-center.x)*cosf(t) - (a.y-center.y)*sinf(t) + center.x, + (a.y-center.y)*cosf(t) + (a.x-center.x)*sinf(t) + center.y); + } +}; + +struct Color +{ + unsigned char r, g, b, a; + Color() : r(0), g(0), b(0), a(0) {} + Color(int r, int g, int b, int a) : r(r), g(g), b(b), a(a) {} + unsigned int get_a8r8g8b8() { return (a<<24) | (r<<16) | (g<<8) | (b<<0); } + static Color create_from_a8r8g8b8(unsigned int v) + { + Color c; + c.a = (v>>24) & 0xff; + c.r = (v>>16) & 0xff; + c.g = (v>> 8) & 0xff; + c.b = (v>> 0) & 0xff; + return c; + } + unsigned int get_a8b8g8r8() { return (a<<24) | (b<<16) | (g<<8) | (r<<0); } + static Color create_from_a8b8g8r8(unsigned int v) + { + Color c; + c.a = (v>>24) & 0xff; + c.b = (v>>16) & 0xff; + c.g = (v>> 8) & 0xff; + c.r = (v>> 0) & 0xff; + return c; + } + unsigned int get_r5g6b5() { return ((r>>3)<<11) | ((g>>2)<<5) | (b>>3); } + static Color create_from_r5g6b5(unsigned short v) + { + Color c; + c.r = ((v>>11)&0x1f)<<3; + c.g = ((v>> 5)&0x1f)<<3; + c.b = ((v>> 0)&0x1f)<<3; + c.a = 255; + return c; + } + unsigned int get_b5g6r5() { return ((b>>3)<<11) | ((g>>2)<<5) | (r>>3); } + static Color create_from_float(float r, float g, float b, float a) + { + Color c; + c.r = int(r*255.0f); + c.g = int(g*255.0f); + c.b = int(b*255.0f); + c.a = int(a*255.0f); + return c; + } + static Color create_from_blend(const Color& a, const Color& b) + { + return create_from_lerp(a, b, a.a); + } + static Color create_from_lerp(const Color& a, const Color& b, unsigned int l) + { + unsigned int il = 255-l; + l += l >> 7; + il += il >> 7; + Color c; + c.r = ((a.r * l) + (b.r * il)) >> 8; + c.g = ((a.g * l) + (b.g * il)) >> 8; + c.b = ((a.b * l) + (b.b * il)) >> 8; + c.a = ((a.a * l) + (b.a * il)) >> 8; + return c; + } + static Color create_from_yuv(unsigned char y, unsigned char u, unsigned char v) + { + Color c; + c.r = (unsigned char)max(0.0f, min(255.0f, 1.164f*(y-16) + 1.596f*(v-128))); + c.g = (unsigned char)max(0.0f, min(255.0f, 1.164f*(y-16) - 0.813f*(v-128) - 0.391f*(u-128))); + c.b = (unsigned char)max(0.0f, min(255.0f, 1.164f*(y-16) + 2.018f*(u-128))); + return c; + } + + static Color yuv_to_hsv(unsigned int yuv) + { + Color c, d; + unsigned char y,u,v,val,delta,vmin; + float hue, sat; + // v4l2src is sending YUYV, meaning, 4 bytes per 2 pixels. + // pixel 1 gets the first 8 bit y and both 8 bit U and V. + // pixel 2 gets the other y and u and v. we are only using pixel 1. + y = ((yuv>>24)&0xFF); + u = ((yuv>>16)&0xFF); + v = (yuv&0xFF); + // for now, yuv->first. TODO: go straight from yuv to hsv + d = create_from_yuv(y, u, v); + + val = max(max(d.r, d.g), d.b); + vmin = min(min(d.r, d.g), d.b); + delta = val-vmin; + if (delta == 0) { + hue = 0.0; + sat = 0.0; + } else if (val == d.r) { + hue = 42.5*(d.g-d.b)/delta; + sat = 255.0*delta/val; + } else if (val == d.g) { + hue = 42.5*(d.b-d.r)/delta + 85.0; + sat = 255.0*delta/val; + } else { + hue = 42.5*(d.r-d.g)/delta + 170.0; + sat = 255.0*delta/val; + } + if (hue < 0.0) hue += 255.0; + if (hue > 255.0) hue -= 255.0; + c.r = (unsigned char)max(0.0f, min(255.0f, hue)); + c.g = (unsigned char)max(0.0f, min(255.0f, sat)); + c.b = (unsigned char)max((unsigned char)0, min((unsigned char)255, val)); + return c; + } + +}; + +// Structure for passing byte data to and from Python. +struct ByteBuffer +{ + int size; + void* data; +}; + +#endif + diff --git a/colorsc/colorsc.i b/colorsc/colorsc.i new file mode 100644 index 0000000..a1e3471 --- /dev/null +++ b/colorsc/colorsc.i @@ -0,0 +1,59 @@ +/* + 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 . +*/ +%module colorsc + +%{ +#include "canvas.h" +#include "palette.h" +%} + +// Testing facility. +%typemap(in) void* { + $1 = $input; +} + +// Pass a gst.Buffer as a GstBuffer*. +%typemap(in) GstBuffer* { + // todo- Error checking would be nice. + PyGObject* pygo = (PyGObject*)$input; + GstBuffer* buf = (GstBuffer*)pygo->obj; + $1 = buf; +} + +// Pass a gtk.gdk.Image as a GdkImage*. +%typemap(in) GdkImage* { + // todo- Error checking would be nice. + PyGObject* pygo = (PyGObject*)$input; + GdkImage* img = (GdkImage*)pygo->obj; + $1 = img; +} + +// Return SurfaceA8R8G8B8 as Python string. +%typemap(out) SurfaceA8R8G8B8 { + $result = PyString_FromStringAndSize((const char*)$1.pixels, $1.stride*$1.height); +} + +// Return ByteBuffer as Python string. +%typemap(out) ByteBuffer { + $result = PyString_FromStringAndSize((const char*)$1.data, $1.size); +} + +%include "colorsc.h" +%include "canvas.h" +%include "palette.h" + diff --git a/colorsc/deploy.sh b/colorsc/deploy.sh new file mode 100755 index 0000000..0d548de --- /dev/null +++ b/colorsc/deploy.sh @@ -0,0 +1,2 @@ +scp ~/Colors.activity/colors.py ~/Colors.activity/_colorsc.so ~/Colors.activity/colorsc.py olpc@$XOIP:~/Colors.activity/ + diff --git a/colorsc/drwfile.h b/colorsc/drwfile.h new file mode 100644 index 0000000..f581f9b --- /dev/null +++ b/colorsc/drwfile.h @@ -0,0 +1,77 @@ +/* + 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 _DRWFILE_H_ +#define _DRWFILE_H_ + +#define DRW_VERSION 1070 + +struct DRW_Command +{ + union + { + struct + { + unsigned int type0 : 2; + unsigned int alpha : 8; + unsigned int x : 11; + unsigned int y : 11; + }; + + struct + { + unsigned int type1 : 2; + unsigned int col : 24; + unsigned int flipx : 1; + unsigned int flipy : 1; + }; + + struct + { + unsigned int type2 : 2; + unsigned int size : 16; + unsigned int brushcontrol : 2; + unsigned int brushtype : 2; + unsigned int opacity : 8; + }; + + struct + { + unsigned int type : 2; + unsigned int undef : 30; + }; + + int raw; + }; +}; + +struct DRW_Header +{ + static const unsigned int ID = 0x436f6c21; // 'Col!' + + unsigned int id; + unsigned int version; + int colorsversion_initial; + int colorsversion_saved; + int strokes; + int time; + int timessaved; + int dummy[8]; + int ncommands; +}; + +#endif diff --git a/colorsc/gtk_types.h b/colorsc/gtk_types.h new file mode 100644 index 0000000..f3e26bd --- /dev/null +++ b/colorsc/gtk_types.h @@ -0,0 +1,130 @@ +// This file serves to allow access to a limited subset of GTK and GStreamer objects passed from PyGTK, without the full +// GLib + GTK + GST development environment. +// +// Obviously, it is limited to working with a specific version of GTK, PyGTK and GStreamer, but these structures appear +// to be fairly stable. +#ifndef GTK_TYPES_H +#define GTK_TYPES_H + +#include + +typedef uint8_t guint8; +typedef int32_t gint; +typedef uint32_t guint; +typedef uint16_t guint16; +typedef uint64_t guint64; +typedef void* gpointer; + +struct GTypeClass; +struct GData; + +struct _GTypeInstance +{ + /*< private >*/ + GTypeClass *g_class; +}; + +typedef struct _GTypeInstance GTypeInstance; + +struct _GObject +{ + GTypeInstance g_type_instance; + + /*< private >*/ + guint ref_count; + GData *qdata; +}; + +typedef struct _GObject GObject; + +typedef enum +{ + GDK_IMAGE_NORMAL, + GDK_IMAGE_SHARED, + GDK_IMAGE_FASTEST +} GdkImageType; + +typedef enum +{ + GDK_LSB_FIRST, + GDK_MSB_FIRST +} GdkByteOrder; + +struct GdkVisual; +struct GdkColormap; + +typedef struct { + GObject parent_instance; + + + GdkImageType type; /* read only. */ + GdkVisual *visual; /* read only. visual used to create the image */ + GdkByteOrder byte_order; /* read only. */ + gint width; /* read only. */ + gint height; /* read only. */ + guint16 depth; /* read only. */ + guint16 bpp; /* read only. bytes per pixel */ + guint16 bpl; /* read only. bytes per line */ + guint16 bits_per_pixel; /* read only. bits per pixel */ + gpointer mem; + + GdkColormap *colormap; /* read only. */ +} GdkImage; + +struct GSList; + +typedef struct { + PyObject_HEAD + GObject *obj; + PyObject *inst_dict; /* the instance dictionary -- must be last */ + PyObject *weakreflist; /* list of weak references */ + GSList *closures; +} PyGObject; + +struct _GstMiniObject { + GTypeInstance instance; + /*< public >*/ /* with COW */ + gint refcount; + guint flags; + + /*< private >*/ + gpointer _gst_reserved; +}; + +typedef struct _GstMiniObject GstMiniObject; + +struct GstCaps; + +typedef guint64 GstClockTime; + +#define GST_PADDING 4 + +struct _GstBuffer { + GstMiniObject mini_object; + + /*< public >*/ /* with COW */ + /* pointer to data and its size */ + guint8 *data; + guint size; + + /* timestamp */ + GstClockTime timestamp; + GstClockTime duration; + + /* the media type of this buffer */ + GstCaps *caps; + + /* media specific offset */ + guint64 offset; + guint64 offset_end; + + guint8 *malloc_data; + + /*< private >*/ + gpointer _gst_reserved[GST_PADDING]; +}; + +typedef struct _GstBuffer GstBuffer; + +#endif + diff --git a/colorsc/palette.cpp b/colorsc/palette.cpp new file mode 100644 index 0000000..0751983 --- /dev/null +++ b/colorsc/palette.cpp @@ -0,0 +1,19 @@ +/* + 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 . +*/ +#include "palette.h" + diff --git a/colorsc/palette.h b/colorsc/palette.h new file mode 100644 index 0000000..03d51eb --- /dev/null +++ b/colorsc/palette.h @@ -0,0 +1,458 @@ +/* + 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 _PALETTE_H_ +#define _PALETTE_H_ + +#include "colorsc.h" +#include "canvas.h" + +// The Palette is the color wheel and triangle that are part of the brush controls dialog. +// This class manages rendering the palette as efficiently as possible into GdkImage objects which are +// then displayed on the screen. +// It also manages responding to mouse inputs that are passed from the Python code. +class Palette +{ +public: + static const int WHEEL_WIDTH = 75; + + int size; + + float palette_h, palette_s, palette_v; + + Pos triangle_cursor; + bool triangle_capture; + bool wheel_capture; + + Palette(int size) : size(size) + { + palette_h = palette_s = palette_v = 0; + triangle_capture = false; + } + + float get_wheel_radius() + { + return size - WHEEL_WIDTH/2; + } + + void rgb_to_hsv(float r, float g, float b, float* h, float* s, float* v) + { + float mn = min(min(r, g), b); + float mx = max(max(r, g), b); + *v = mx; // v + float delta = mx - mn; + if (mx != 0) + *s = delta / mx; // s + else + { + // r = g = b = 0 // s = 0, v is undefined + *s = 0; + *h = -1; + return; + } + if (delta == 0) + *h = 0; + else if (r == mx) + *h = ( g - b ) / delta; // between yellow & magenta + else if (g == mx) + *h = 2 + ( b - r ) / delta; // between cyan & yellow + else + *h = 4 + ( r - g ) / delta; // between magenta & cyan + *h *= 60; // degrees + if (*h < 0) + *h += 360; + } + + void hsv_to_rgb(float* r, float* g, float* b, float h, float s, float v) + { + if (s == 0) + { + // achromatic (grey) + *r = *g = *b = v; + return; + } + h /= 60; // sector 0 to 5 + int i = int(floorf(h)); + float f = h - i; // factorial part of h + float p = v * (1-s); + float q = v * (1-s * f); + float t = v * (1-s * (1 - f)); + switch (i) + { + case 0: *r = v; *g = t; *b = p; break; + case 1: *r = q; *g = v; *b = p; break; + case 2: *r = p; *g = v; *b = t; break; + case 3: *r = p; *g = q; *b = v; break; + case 4: *r = t; *g = p; *b = v; break; + default: + case 5: *r = v; *g = p; *b = q; break; + } + } + + void set_color(const Color& c) + { + rgb_to_hsv(c.r/255.0f, c.g/255.0f, c.b/255.0f, &palette_h, &palette_s, &palette_v); + + // I couldn't work out an exact solution to the blobby triangle position, so I'm using a relaxation algorithm. + // It usually takes less than 10 iterations to converge and only runs when you first open the palette. + Pos p0, p1, p2; + get_triangle_points(&p0, &p1, &p2); + float side = distance(p0, p1); + + Pos p = (p0+p1+p2)*(1.0f/3.0f); + float epsilon = 0.001f; + int max_iterations = 50; + for (int i = 0; i < max_iterations; i++) + { + float d0 = distance(p, p0); + float d0ok = fabsf((side-d0)-palette_s*side); + p = p + ((p0-p)/length(p0-p)) * (d0-(1.0f-palette_s)*side); + + float d2 = distance(p, p2); + float d2ok = fabsf(d2-palette_v*side); + p = p + ((p2-p)/length(p2-p)) * (d2-palette_v*side); + + if (d0ok <= epsilon && d2ok <= epsilon) break; + } + triangle_cursor = p; + } + + Color get_color() + { + float r, g, b; + hsv_to_rgb(&r, &g, &b, palette_h, palette_s, palette_v); + return Color::create_from_float(r, g, b, 1.0f); + } + + float sqr(float a) + { + return a*a; + } + + float dot(const Pos& a, const Pos& b) + { + return a.x*b.x + a.y*b.y; + } + + float length(const Pos& a) + { + return sqrtf(dot(a,a)); + } + + float length_sqr(const Pos& a) + { + return dot(a,a); + } + + float distance(const Pos& a, const Pos& b) + { + return length(a-b); + } + + float distance_sqr(const Pos& a, const Pos& b) + { + return length_sqr(a-b); + } + + Pos normalize(const Pos& a) + { + return a/length(a); + } + + void get_triangle_points(Pos* p0, Pos* p1, Pos* p2) + { + Pos center(size/2, size/2); + *p0 = center + Pos::create_from_angle(palette_h+0.0f, size/2-WHEEL_WIDTH); + *p1 = center + Pos::create_from_angle(palette_h+120.0f, size/2-WHEEL_WIDTH); + *p2 = center + Pos::create_from_angle(palette_h+240.0f, size/2-WHEEL_WIDTH); + } + + // The wheel never changes, so it can be rendered once here. This also clears the image background. + void render_wheel(GdkImage* image) + { + if (image->width != size || image->height != size || image->bits_per_pixel != 16) + { + fprintf(stderr, "Error: Invalid Palette GdkImage.\n"); + return; + } + + Color bkg(64, 64, 64, 0); + + float wheel_radius = size/2; + float ring_min_sqr = sqr(wheel_radius-WHEEL_WIDTH); + float ring_max_sqr = sqr(wheel_radius); + + unsigned short* pixels = (unsigned short*)image->mem; + int stride = image->bpl/sizeof(unsigned short); + + for (int y = 0; y < size; y++) + { + unsigned short* row = &pixels[y*stride]; + for (int x = 0; x < size; x++) + { + // Get radius from center of palette. + float dx = x-wheel_radius; + float dy = y-wheel_radius; + float dist_sqr = dx*dx + dy*dy; + + // Inside color wheel ring? Draw the wheel. + if (dist_sqr >= ring_min_sqr && dist_sqr < ring_max_sqr) + { + // Calculate HSV from wheel angle. + float h = atan2f(dy, dx)*180.0f/PI; + while (h<0) h += 360.0f; + while (h>360.0f) h -= 360.0f; + float r, g, b; + hsv_to_rgb(&r, &g, &b, h, 1.0f, 1.0f); + *row++ = Color::create_from_float(r, g, b, 1).get_r5g6b5(); + } + else + *row++ = bkg.get_r5g6b5(); + } + } + } + + // Clears out the inner circle and redraws the color triangle, as fast as possible. + // Scales up implicitly by 2x to improve performance. + // The appearance was an accident which creates a kind of blobby triangle, like the intersection of three circles. + // But I like the look so I'm keeping it! + void render_triangle(GdkImage* image) + { + if (image->width != size || image->height != size || image->bits_per_pixel != 16 || (size&1)) + { + fprintf(stderr, "Error: Invalid Palette GdkImage.\n"); + return; + } + + Color bkg(64, 64, 64, 0); + unsigned short bkgc = bkg.get_r5g6b5(); + + Pos p0, p1, p2; + get_triangle_points(&p0, &p1, &p2); + float triangle_side = distance(p0, p1); + float triangle_side_sqr = triangle_side*triangle_side; + float inv_triangle_side = 1.0f/triangle_side; + + float wheel_radius = size/2; + float ring_min_sqr = sqr(wheel_radius-WHEEL_WIDTH); + + int x0 = WHEEL_WIDTH; + int x1 = size-WHEEL_WIDTH; + + unsigned short* pixels = (unsigned short*)image->mem; + int stride = image->bpl/sizeof(unsigned short); + + Pos p(x0,x0); + for (int y = x0; y < x1; y+=2, p.y += 2.0f) + { + p.x = x0; + unsigned short* __restrict row0 = &pixels[(y+0)*stride+x0]; + unsigned short* __restrict row1 = &pixels[(y+1)*stride+x0]; + for (int x = x0; x < x1; x+=2, p.x += 2.0f) + { + // Calculate position inside triangle. If inside, then use as HSV. + float d0_sqr = distance_sqr(p, p0); + float d1_sqr = distance_sqr(p, p1); + float d2_sqr = distance_sqr(p, p2); + if (d0_sqr <= triangle_side_sqr && d1_sqr <= triangle_side_sqr && d2_sqr <= triangle_side_sqr) + { + float r, g, b; + hsv_to_rgb(&r, &g, &b, palette_h, 1.0f-sqrtf(d0_sqr)*inv_triangle_side, sqrtf(d2_sqr)*inv_triangle_side); + unsigned short c = Color::create_from_float(r, g, b, 1).get_r5g6b5(); + row0[0] = c; + row0[1] = c; + row1[0] = c; + row1[1] = c; + } + else + { + // Inside ring? Clear to background. + if (distance_sqr(p, Pos(wheel_radius, wheel_radius)) < ring_min_sqr) + { + row0[0] = bkgc; + row0[1] = bkgc; + row1[0] = bkgc; + row1[1] = bkgc; + } + } + row0 += 2; + row1 += 2; + } + } + } + + Pos get_wheel_pos() + { + float a = palette_h*PI/180.0f; + float r = size/2 - WHEEL_WIDTH/2; + return Pos(size/2+r*cosf(a), size/2+r*sinf(a)); + } + + Pos get_triangle_pos() + { + return triangle_cursor; + } + + void process_mouse(int mx, int my) + { + Pos p0, p1, p2; + get_triangle_points(&p0, &p1, &p2); + + // If the triangle has the mouse capture, clip the mouse position to within the blobby triangle. + if (!wheel_capture) + { + if (triangle_capture) + { + Pos p(mx, my); + float d0 = distance(p, p0); + float d1 = distance(p, p1); + float d2 = distance(p, p2); + float side = distance(p0, p1)-1; + if (d0 > side || d1 > side || d2 > side) + { + Pos far_point = p0; + float far_dist = d0; + if (d1 > far_dist) { far_point = p1; far_dist = d1; } + if (d2 > far_dist) { far_point = p2; far_dist = d2; } + p = far_point + normalize(p-far_point) * side; + mx = int(p.x); + my = int(p.y); + } + } + + // Now check to see if it's on the triangle. + float side = 1.0f/distance(p0, p1); + Pos p(mx, my); + float d0 = distance(p, p0)*side; + float d1 = distance(p, p1)*side; + float d2 = distance(p, p2)*side; + if (d0 <= 1.0f && d1 <= 1.0f && d2 <= 1.0f) + { + triangle_capture = true; + triangle_cursor = p; + palette_s = 1.0f-d0; + palette_v = d2; + } + } + + if (!triangle_capture) + { + // Check to see if the mouse is in the wheel ring. + int dx = mx-size/2; + int dy = my-size/2; + float dist = sqrtf(dx*dx + dy*dy); + if (dist >= size/2-WHEEL_WIDTH || wheel_capture) + { + wheel_capture = true; + float h = atan2f(float(dy), float(dx))*180.0f/PI; + while (h<0) h += 360.0f; + while (h>360.0f) h -= 360.0f; + // Rotate the triangle cursor with the wheel. + triangle_cursor = Pos::create_from_rotation(triangle_cursor, (p0+p1+p2)/3.0f, (h-palette_h)*PI/180.0f); + palette_h = h; + } + } + } + + void process_mouse_release() + { + triangle_capture = false; + wheel_capture = false; + } +}; + +// The BrushPreview is a simple preview of the current brush as displayed in the Brush Controls panel. +// This class handles rendering the brush preview to a GdkImage to be displayed on the screen. +class BrushPreview +{ +public: + int size; + + Brush brush; + + BrushPreview(int size) : size(size) + { + } + + void render(GdkImage* image) + { + if (image->width != size || image->height != size || image->bits_per_pixel != 16 || (size&1)) + { + fprintf(stderr, "Error: Invalid BrushPreview GdkImage.\n"); + return; + } + + // Minimum brush size. + if (brush.size<2) brush.size = 2; + + // Calculate interpolation constants + float db = (BrushType::DIST_TABLE_WIDTH-1) / float(brush.size); + + float xb = BrushType::DIST_TABLE_CENTER - (size/2)*db; + float yb = BrushType::DIST_TABLE_CENTER - (size/2)*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) / brush.size); + + int opacity = int(round(255.0f * brush.opacity)); + + unsigned short* pixels = (unsigned short*)image->mem; + int stride = image->bpl/sizeof(unsigned short); + + // Interpolate the distance table over the area. For each pixel find the distance, and look the + // brush-intensity up in the brush-table + for (int y = 0; y < size; y+=2) + { + float x2b = xb; + unsigned short* __restrict row0 = &pixels[(y+0)*stride]; + unsigned short* __restrict row1 = &pixels[(y+1)*stride]; + for (int x = 0; x < size; x+=2) + { + // Find brush-intensity and mulitply that with incoming opacity + if (x2b >= 0 && x2b < BrushType::DIST_TABLE_WIDTH && yb >= 0 && yb < BrushType::DIST_TABLE_WIDTH) + { + 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(0xffffffff); + //Color i = Color::create_from_a8r8g8b8(image[y*size+x]); + i = Color::create_from_lerp(brush.color, i, intensity); + unsigned short c = i.get_r5g6b5(); + row0[0] = c; + row0[1] = c; + row1[0] = c; + row1[1] = c; + } + else + { + row0[0] = 0xffff; + row0[1] = 0xffff; + row1[0] = 0xffff; + row1[1] = 0xffff; + } + + row0 += 2; + row1 += 2; + + x2b += db*2; + } + yb += db*2; + } + } +}; + +#endif + diff --git a/colorsc/setup.py b/colorsc/setup.py new file mode 100644 index 0000000..1295652 --- /dev/null +++ b/colorsc/setup.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +""" +setup.py file for colorsc +""" + +from distutils.core import setup, Extension + +colorsc_module = Extension('_colorsc', sources=['canvas.cpp', 'palette.cpp', 'colorsc_wrap.cxx'] ) + +setup (name = 'colorsc', version = '0.1', + author = "Jens Andersson", + description = """Colors C extension library.""", + ext_modules = [colorsc_module], py_modules = ["colorsc"]) + -- cgit v0.9.1