How to retrieve the visible texture from a png image in cocos2d-x - cocos2d-x

I am making a question answer game in which the answer images are made as sprites from png images.
The answer image is like this:
I am making rect on the image like this:
Rect rect = Rect(answerSprites.at(i)->getBoundingBox().origin.x,
answerSprites.at(i)->getBoundingBox().origin.y,
answerSprites.at(i)->getBoundingBox().size.width,
answerSprites.at(i)->getBoundingBox().size.height);
Then i am detecting touch on the rect as :
void HelloWorld::onTouchesBegan(const std::vector<Touch*>& touches,
Event *unused_event) {
auto target = static_cast<Sprite*>(unused_event->getCurrentTarget());
auto touchPointBegan = (Touch*) touches.front();
Vec2 locationBegan = touchPointEnded->getLocation();
Point locationInNode = target->convertToNodeSpace(locationEnded);
Size s = target->getContentSize();
if (rect.containsPoint(locationInNode)) {
log(“Correct Touch”);
}
}
The code is working fine but the problem is that it is detecting the touch on the full png, but i want to detect the touch on the flower only.
The flower can be at any position on the png.
How can i make the rect only on the flower?

Check the transparency of the touch location with this code:
// Answer sprite
m_sprite = Sprite::create("answer-1.png");
m_sprite->setPosition( Vec2(winSize.width*.5, winSize.height*.5) );
addChild(m_sprite);
bool HelloWorld::onTouchBegan(const cocos2d::Touch *touch, cocos2d::Event *event)
{
_originPoint = touch->getLocation();
_destinationPoint = _originPoint;
Vec2 locationInNode = m_sprite->convertToNodeSpace(touch->getLocation());
Rect rect = Rect(m_sprite->getBoundingBox().origin.x,
m_sprite->getBoundingBox().origin.y,
m_sprite->getContentSize().width,
m_sprite->getContentSize().height);
if (rect.containsPoint(touch->getLocation() )) {
if (tapsOnNonTransparent(locationInNode, "answer-1.png" )) {
log("Correct Touch");
}
}
return true;
}
const bool HelloWorld::tapsOnNonTransparent( const cocos2d::Point& tap, const std::string &spritePath )
{
auto imgPtr = std::make_unique<cocos2d::Image>();
imgPtr->initWithImageFile( spritePath );
const int width = imgPtr ->getWidth();
const int height = imgPtr ->getHeight();
unsigned x = unsigned( tap.x ) % width;
/// Don't forget to invert y coordinate.
unsigned y = unsigned( height - tap.y ) % height;
unsigned index = x + y * width;
unsigned dataLen = imgPtr ->getDataLen();
CCAssert( index < dataLen, "index is bigger than image size." );
unsigned char* pixel = imgPtr->getData() + (4 * index);
return !isZeroPixel( pixel );
}
const bool HelloWorld::isZeroPixel( const unsigned char* pixel )
{
return 0 == pixel[0] && 0 == pixel[1] && 0 == pixel[2] && 0 == pixel[3];
}

The rectangle you create covers the entire image. It would be a better approach to separate the flower and the frame.
Create a Sprite for the frame image and create a Button for the flower image. Then add the button as a child to frame.
auto spriteFrame = Sprite::create("frame.png");
spriteFrame->setPosition(Vec2(300,300));
addChild(spriteFrame);
auto btn = ui::Button::create("flower.png");
btn->setZoomScale(0);
// Place in the middle of the frame sprite
btn->setPosition(Vec2(spriteFrame->getContentSize().width*.5, spriteFrame->getContentSize().height*.5));
btn->addClickEventListener([=](Ref* sender){
log("Correct Touch");
});
spriteFrame->addChild(btn);

Related

How to get the position of overlay geometry on hover?

In my forge viewer project I am trying to add point-cloud markup to mark selected items in some saved viewpoint. I can create the markers using below code-
createPointCloud(points, overlayName) {
if (points.length > 0) {
try {
const vertexShader = `
attribute vec4 color;
varying vec4 vColor;
void main() {
vec4 vPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * vPosition;
gl_PointSize = 20.0;
vColor = color;
}
`;
// Fragment Shader code
const fragmentShader = `
#ifdef GL_ES
precision highp float;
#endif
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`
// Shader material parameters
this.shader = {
side: THREE.DoubleSide,
depthWrite: this.occlusion,
depthTest: this.occlusion,
fragmentShader,
vertexShader,
attributes: {
color: {
type: 'v4',
value: []
}
}
}
// Initialize geometry vertices
// and shader attribute colors
this.geometry = new THREE.Geometry()
for (const point of points) {
this.geometry.vertices.push(
new THREE.Vector3(point.position.x, point.position.y, point.position.z))
this.shader.attributes.color.value.push(
new THREE.Vector4(
point.color.r/255.00,
point.color.g / 255.00,
point.color.b / 255.00,
1.0)
)
}
// creates shader material
let shaderMaterial = new THREE.ShaderMaterial(this.shader);
// creates THREE.PointCloud
let pointCloud = new THREE.PointCloud(
this.geometry, shaderMaterial)
this.overlayPointCloudMap[overlayName] = pointCloud;
this.viewer.impl.createOverlayScene(overlayName);
this.viewer.impl.addOverlay(overlayName, pointCloud);
} catch (ex) {
alert('Can\'t show points, please try again!');
}
}
}
Also want to show some modal on hover over the markup. To do that I need to know the position of the markup that is currently hovered. I've tried a solution based on this. But not able to get the correct item. Sometimes it returns empty list. The hit-test code is like below-
updateHitTest(event) {
const pointer = event.pointers ? event.pointers[0] : event;
const pointerVector = new THREE.Vector3();
const pointerDir = new THREE.Vector3();
const ray = new THREE.Raycaster();
ray.params.PointCloud.threshold = 20; // hit-test markup size = 20
const camera = this.viewer.impl.camera;
const rect = this.viewer.impl.canvas.getBoundingClientRect();
const x = ((pointer.clientX - rect.left) / rect.width) * 2 - 1;
const y = - ((pointer.clientY - rect.top) / rect.height) * 2 + 1;
if (camera.isPerspective) {
pointerVector.set(x, y, 0.5);
pointerVector.unproject(camera);
ray.set(camera.position, pointerVector.sub(camera.position).normalize());
} else {
pointerVector.set(x, y, -1);
pointerVector.unproject(camera);
pointerDir.set(0, 0, -1);
ray.set(pointerVector, pointerDir.transformDirection(camera.matrixWorld));
}
let nodes = [];
// loop through all the overlays and intersect with the respected point-cloud
for (const overlay of this.overlayNames) {
nodes = nodes.concat( ray.intersectObject(this.overlayPointCloudMap[overlay]));
}
if (nodes.length > 0) {
if (this.lastClickedIndex != nodes[0].index) {
this.lastClickedIndex = nodes[0].index;
console.log(this.lastClickedIndex);
}
}
}
Could you please help me to figure out where to change or how can I get the job done? TIA
The code seems alright. But you may need to adjust the ray.params.PointCloud.threshold value. Also, try debugging the three.js code by adding a single point to the point cloud, and stepping into the ray.intersectObject method to see it can't find it.
Alternatively, if the number of points/markers you expect to have on screen is "reasonable", consider the approach of adding them as HTML elements overlaying the 3D view es explained in https://github.com/autodesk-forge/forge-extensions/tree/master/public/extensions/IconMarkupExtension.

How do I use mouselistener and press event to change a filled square to another color?

This is for homework, I have most of this done but I am stuck on the final step (implementing a mouse event that will change one of my randomized colored squares to become red instead of it's assigned colors) and am concerned that using the methods provided by my prof are not suitable to this, as he has us repaint after mouse event (which I feel my code will just override with more random colors). Any assistance or nudge in the right direction would be helpful, and I am sure this is a mess.
Update as per Camickr's assistance, my code has changed to the following:
import java.awt.GridLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.util.Random;
import javax.swing.*;
// MouseListener Imports
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
public class Test extends JPanel implements MouseListener
{
static Color[][] framework = new Color[8][8];
static int redCounter = 0;
// main Creates a JFrame and instantiates the 2d array of color objects
public static void main(String[] args)
{
// The frame handles all the outside window work
JFrame frame = new JFrame("MouseListener demo");
// Allows the 'X' in the upper right to cause the program to exit
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Create and set up the content pane.
// contentPane holds a panel
JComponent newContentPane = new Test();
// MouseListenerDemo isa JPanel isa JComponent
newContentPane.setOpaque(true); //content panes must be opaque
frame.setContentPane(newContentPane);
//Display the window.
frame.pack();
/* Causes this Window to be sized to fit the preferred size and layouts of its subcomponents.
The resulting width and height of the window are automatically enlarged if either of dimensions
is less than the minimum size as specified by the previous call to the setMinimumSize method.
If the window and/or its owner are not displayable yet, both of them are made displayable before
calculating the preferred size. The Window is validated after its size is being calculated.
*/
frame.setVisible(true);
for (int x = 0; x < framework.length; x++) {
for (int y = 0; y < framework.length; y++) {
Color rc = randomColor();
framework[x][y] = rc;
}
}
}
// Constructor
public Test()
{
// layout the panel with an area in which to draw
super(new GridLayout(0,1));
// get Graphics object, which allows you to draw on the panel
// set initial size of the panel
setPreferredSize(new Dimension(400, 500));
// ADD this JPanel to the list of components notified when a mouse event happens
this.addMouseListener(this);
// calls paint()
repaint();
}
// draws the screen
public void paint(Graphics g)
{
// Get the size
Dimension d = this.getSize();
int size = 50; // The edge length of the squares
int xSt = 50; // Starting x-coordinate
int ySt = 50; // Starting y-coordinate
int n = 8; // n X n board
int xPos, yPos;
for(int row = 0; row < framework.length; row ++){
for (int col = 0; col < framework.length; col ++){
xPos = xSt*col;
yPos = xSt*row;
// set color
g.setColor (framework[row][col]);
// Draw square
g.fillRect(xPos, yPos, size, size);
}
}
g.setColor(Color.black);
g.drawString("There are " + redCounter + " reds." , d.width/3, d.height);
}
public static Color randomColor(){
Random rg = new Random();
int result = rg.nextInt(12);
Color color;
switch(result){
case 0: color = Color.black;
break;
case 1: color = Color.blue;
break;
case 2: color = Color.cyan;
break;
case 3: color = Color.darkGray;
break;
case 4: color = Color.yellow;
break;
case 5: color = Color.green;
break;
case 6: color = Color.lightGray;
break;
case 7: color = Color.magenta;
break;
case 8: color = Color.orange;
break;
case 9: color = Color.pink;
break;
case 10: color = Color.red; redCounter = redCounter + 1;
break;
case 11: color = Color.white;
break;
default: color = Color.black;
break;
}
return color;
}
// MouseListener methods
public void mousePressed(MouseEvent evt)
{
// demonstrates how to use the parameter
// to get the position of the mouse press
Dimension d = this.getSize();
int x = evt.getX();
int y = evt.getY();
System.out.println(x+","+y);//these co-ords are relative to the component
System.out.println(evt.getSource());
for (int i = 0; i < framework.length; i++) {
for (int j = 0; j < framework.length; j++) {
System.out.println(framework[i][j]);
if (evt.getSource().equals(framework[i][j])) {
framework[i][j] = Color.red;
redCounter = redCounter + 1;
}
}
}
repaint(); // redisplay the frame by eventually calling the paint() method
}
// do nothing for the other mouse actions
public void mouseReleased(MouseEvent evt) {}
public void mouseClicked(MouseEvent evt) {}
public void mouseEntered(MouseEvent evt) {}
public void mouseExited(MouseEvent evt) {}
}
am concerned that using the methods provided by my prof are not suitable to this, as he has us repaint after mouse event (which I feel my code will just override with more random colors).
The repaint() in the MouseEvent is correct. However more random colors will be generated because your painting code is incorrect.
A painting method should only paint the state of your component, not change the state. Therefore:
You need to keep a data structure (lets say a 2D array) to hold the Color of each cell. In the constructor of your class you would then iterate through this array and assign the random colors to each entry in the array.
Then in the painting method you just iterate through the array and paint each cell using the color from the array.
Note you should be overriding paintComponent(), not paint() for the custom painting.
Then in your MouseListener code you just reset the Color in the Array for the cell that was clicked and invoke repaint().
I fixed it by getting the position of x/y and dividing them by 50 (my square height/width) and putting the int into the array index.
public void mousePressed(MouseEvent evt)
{
// demonstrates how to use the parameter
// to get the position of the mouse press
Dimension d = this.getSize();
int x = evt.getX()/50;
int y = evt.getY()/50;
framework[y][x] = Color.red;
redCounter = redCounter+1;
repaint(); // redisplay the frame by eventually calling the paint() method
}

How to make a sliding menu in cocos2dx in c++ for IOS game

I want to make a sliding menu just like the level menu in which on one screen there will be 40 sprites labelled as level 1 level 2 respectively up to 40.
At the bottom right there will be another sprite with a arrow to which when I click it should slide to other screen and show the levels 41 to 80.Please provide me with a basic concept how to use it.I will be thankful to you.
Note: I am using Xcode and ony want solution in cocos2d-x using c++
This is the way I have done this in the past...I had a game with the option for the player to select multiple space ships, 4 per page, with back/forward arrows on each page as well.
Create a CCScene derived class.
Place all your menu items, including the control arrows for ALL pages on it. You will have to space all the items so the items for the first page are on the visible part of the screen and the next group is 100% off to the right, the third group is 200% off the right, etc.
The control buttons on the scene start an action to move the layer 100% to the left (if they move right) or 100% to the right (if they move left).
All of these are attached to a single "Menu", which is what the actions are applied against. If you want, you can put the menu into a layer (so it has background that moves). This is up to you.
In the example Scene below, I just used a simple menu.
MainScene.h
#ifndef __MainScene__
#define __MainScene__
#include "cocos2d.h"
using namespace cocos2d;
class MainScene : public CCScene
{
private:
// This class follows the "create"/"autorelease" pattern.
// Private constructor.
MainScene();
CCMenu* _menu;
bool _sliding;
void MenuCallback(CCObject* sender);
void PageLeft();
void PageRight();
void SlidingDone();
protected:
// This is protected so that derived classes can call it
// in their create methods.
bool init();
private:
void CreateMenu();
public:
static MainScene* create();
~MainScene();
virtual void onEnter();
virtual void onExit();
virtual void onEnterTransitionDidFinish();
virtual void onExitTransitionDidStart();
};
#endif /* defined(__MainScene__) */
MainScene.cpp
#include "MainScene.h"
#define ARROW_LEFT (-1)
#define ARROW_RIGHT (-2)
#define MENU_ITEMS_ACROSS 4
#define MENU_ITEMS_DOWN 5
#define MENU_ITEMS_PAGE (MENU_ITEMS_ACROSS*MENU_ITEMS_DOWN)
#define MENU_ITEMS_TOTAL 50
#define MENU_PAGES ((MENU_ITEMS_TOTAL/MENU_ITEMS_PAGE)+1)
#define MENU_FRACTION (ccp(0.8,0.8))
#define MENU_ANCHOR (ccp(0.5,0.5))
#define SLIDE_DURATION 1.0
MainScene::MainScene() :
_menu(NULL)
_sliding(false)
{
}
MainScene::~MainScene()
{
}
static CCPoint CalculatePosition(int itemNum)
{
CCSize scrSize = CCDirector::sharedDirector()->getWinSize();
float Xs = scrSize.width;
float Ys = scrSize.height;
int gRows = MENU_ITEMS_DOWN;
int gCols = MENU_ITEMS_ACROSS;
int gBins = gRows*gCols;
float Xb = MENU_FRACTION.x*Xs/gCols;
float Yb = MENU_FRACTION.y*Ys/gRows;
float Xa = MENU_ANCHOR.x * Xs;
float Ya = MENU_ANCHOR.y * Ys;
int page = itemNum / gBins;
int binCol = itemNum % gCols;
int binRow = (itemNum-page*gBins) / gCols;
float xPos = binCol * Xb + Xb/2 + Xa - MENU_FRACTION.x*Xs/2 + page * Xs;
float yPos = Ya - binRow*Yb - Yb/2 + MENU_FRACTION.y * Ys/2;
CCPoint pos = ccp(xPos,yPos);
return pos;
}
void MainScene::CreateMenu()
{
if(_menu == NULL)
{
CCSize scrSize = CCDirector::sharedDirector()->getWinSize();
_menu = CCMenu::create();
_menu->setPosition(ccp(0,0));
addChild(_menu);
CCMenuItemFont* pItem;
CCPoint position;
// Create the next/back menu items.
for(int page = 0; page < MENU_PAGES; page++)
{
// Create the Back/Forward buttons for the page.
// Back arrow if there is a previous page.
if(page > 0)
{
pItem = CCMenuItemFont::create("Back", this, menu_selector(MainScene::MenuCallback));
pItem->setTag(ARROW_LEFT);
position = ccp(page*scrSize.width + scrSize.width*0.1,scrSize.height*0.1);
pItem->setPosition(position);
pItem->setFontSize(35);
pItem->setFontName("Arial");
_menu->addChild(pItem);
}
if(page < (MENU_PAGES-1))
{
pItem = CCMenuItemFont::create("Next", this, menu_selector(MainScene::MenuCallback));
pItem->setTag(ARROW_RIGHT);
position = ccp(page*scrSize.width + scrSize.width*0.9,scrSize.height*0.1);
pItem->setPosition(position);
pItem->setFontSize(35);
pItem->setFontName("Arial");
_menu->addChild(pItem);
}
}
// Create the actual items
for(int idx = 0; idx < MENU_ITEMS_TOTAL; idx++)
{
char buffer[256];
sprintf(buffer,"Item #%d",idx);
pItem = CCMenuItemFont::create(buffer, this, menu_selector(MainScene::MenuCallback));
pItem->setFontSize(35);
pItem->setFontName("Arial");
pItem->setTag(idx);
position = CalculatePosition(idx);
pItem->setPosition(position);
_menu->addChild(pItem);
}
}
}
bool MainScene::init()
{
return true;
}
MainScene* MainScene::create()
{
MainScene *pRet = new MainScene();
if (pRet && pRet->init())
{
pRet->autorelease();
return pRet;
}
else
{
CC_SAFE_DELETE(pRet);
return NULL;
}
}
void MainScene::onEnter()
{
CCScene::onEnter();
CreateMenu();
}
void MainScene::onExit()
{
CCScene::onExit();
}
void MainScene::onEnterTransitionDidFinish()
{
CCScene::onEnterTransitionDidFinish();
}
void MainScene::onExitTransitionDidStart()
{
CCScene::onExitTransitionDidStart();
}
void MainScene::SlidingDone()
{
_sliding = false;
}
void MainScene::PageLeft()
{
if(_sliding)
return;
_sliding = true;
CCSize scrSize = CCDirector::sharedDirector()->getWinSize();
CCFiniteTimeAction* act1 = CCMoveBy::create(SLIDE_DURATION, ccp(scrSize.width,0));
CCFiniteTimeAction* act2 = CCCallFunc::create(this, callfunc_selector(MainScene::SlidingDone));
_menu->runAction(CCSequence::create(act1,act2,NULL));
}
void MainScene::PageRight()
{
if(_sliding)
return;
_sliding = true;
CCSize scrSize = CCDirector::sharedDirector()->getWinSize();
CCFiniteTimeAction* act1 = CCMoveBy::create(SLIDE_DURATION, ccp(-scrSize.width,0));
CCFiniteTimeAction* act2 = CCCallFunc::create(this, callfunc_selector(MainScene::SlidingDone));
_menu->runAction(CCSequence::create(act1,act2,NULL));
}
void MainScene::MenuCallback(CCObject* sender)
{
// This is a very contrived example
// for handling the menu items.
// -1 ==> Left Arrow
// -2 ==> Right Arrow
// Anything else is a selection
CCMenuItem* pMenuItem = (CCMenuItem*)sender;
switch(pMenuItem->getTag())
{
case ARROW_LEFT:
PageLeft();
break;
case ARROW_RIGHT:
PageRight();
break;
default:
CCLOG("Got Item %d Pressed",pMenuItem->getTag());
break;
}
}
Note The formulas for getting the items spread across several pages can be a little tricky. There is a notion of "Screen fraction", which is how much the grid of items takes up on the page. There is also the notion of "menu anchor", which where on the page you want the grid to be.
Some screen shots
or you can do it the modern way with less code!!
// you have to include this header to use the ui classes
#include "ui/CocosGUI.h"
using namespace ui;
#define COLS 4
#define ROWS 4
#define ITEMS_PER_PAGE (ROWS * COLS)
#define TOTAL_PAGES_NUM 10
#define MENU_PADDING (Vec2(0.8,0.8))
#define MENU_ANCHOR (Vec2(0.5,0.5))
static Vec2 calcPosition(int itemNum)
{
Size scrSize = Director::getInstance()->getWinSize();
float Xs = scrSize.width;
float Ys = scrSize.height;
float Xb = MENU_PADDING.x*Xs / COLS;
float Yb = MENU_PADDING.y*Ys / ROWS;
float Xa = MENU_ANCHOR.x * Xs;
float Ya = MENU_ANCHOR.y * Ys;
int page = itemNum / ITEMS_PER_PAGE;
int binCol = itemNum % COLS;
int binRow = (itemNum - page * ITEMS_PER_PAGE) / COLS;
float xPos = binCol * Xb + Xb / 2 + Xa - MENU_PADDING.x*Xs / 2 + page * Xs;
float yPos = Ya - binRow*Yb - Yb / 2 + MENU_PADDING.y * Ys / 2;
return Vec2(xPos, yPos);
}
//init method
// pageView is the container that will contain all pages
auto pageView = PageView::create();
pageView->setContentSize(winSize);
//if you want pages indicator just uncomment this
//pageView->setIndicatorEnabled(true);
//pageView->setIndicatorPosition(some position);
//pageView->setIndicatorSelectedIndexColor(some Color3B);
for (int i = 0; i < TOTAL_PAGES_NUM; i++) {
auto layout = Layout::create();
layout->setContentSize(winSize);
// give each page a different random color
int r = rand() % 200;
int g = rand() % 200;
int b = rand() % 200;
auto bg = LayerColor::create(Color4B(Color3B(r, g, b)), winSize.width, winSize.height);
layout->addChild(bg, 0);
// populate each single page with items (which are in this case labels)
for (int i = 0; i < ITEMS_PER_PAGE; i++) {
auto label = LabelTTF::create(StringUtils::format("item %i", (i + 1)), "Comic Sans MS", 15);
Vec2 pos = calcPosition(i);
label->setPosition(pos);
layout->addChild(label, 1);
}
pageView->addPage(layout);
}
this->addChild(pageView);
I’ve modified existing one and uploaded in github. Here is the link:
GitHub Link to SlidingMenu
You may find it helpful. You can directly add it into your game.

HTML5 Canvas - Anti-aliasing and Paint Bucket / Flood Fill

Having trawled Stack Overflow and Google it seems to me that there is no way to disable antialiasing when drawing lines on an HTML5 canvas.
This makes for nice looking lines, but causes me a problem when it comes time to applying a paint bucket / flood fill algorithm.
Part of my application requires that users draw on a canvas, freestyle drawing with basic tools like line size, color... and a paint bucket.
Because lines are rendered with antialiasing they are not a consistent color... with that in mind consider the following:
Draw a thick line in black
Decide at some point later that the line should be red
Apply flood fill to black line
My flood fill algorithm fills the bulk of the line with red, but the edges that were antialiased are detected as being outside the area that should be filled... hence remain (greys / blues(?) left over from the black line).
The flood fill algorithm does not incorporate something akin to 'tolerance' like Photoshop does... I have considered something like that but am unsure it would help as I don't think the anti-aliasing does something simple like render grey next to a black line, I think it's more advanced than that and the anti-aliasing takes into consideration the surrounding colors and blends.
Does anyone have any suggestions as to how I can end up with a better paint bucket / flood fill that COMPLETELY flood fills / replaces an existing line or section of a drawing?
If you simply want to change a color of a line: don't use bucket paint fill at all.
Store all your lines and shapes as objects/arrays and redraw them when needed.
This not only allow you to change canvas size without losing everything on it, but to change a color is simply a matter of changing a color property on your object/array and redraw, as well as scaling everything based on vectors instead of raster.
This will be faster than a bucket fill as redrawing is handled in most part internally and not by pixel-by-pixel in JavaScript as is needed with a bucket fill.
That being said: you cannot, unfortunately, disable anti-alias for shapes and lines, only for images (using the imageSmoothingEnabled property).
An object could look like this:
function myLine(x1, y1, x2, y2, color) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.color = color;
return this;
}
And then allocate it by:
var newLine = new myLine(x1, y1, x2, y2, color);
Then store this to an array:
/// globally:
var myLineStack = [];
/// after x1/x2/y1/y2 and color is achieved in the draw function:
myLineStack.push(new myLine(x1, y1, x2, y2, color));
Then it is just a matter of iterating through the objects when an update is needed:
/// some index to a line you want to change color for:
myLineStack[index].color = newColor;
/// Redraw all (room for optimizations here...)
context.clearRect( ... );
for(var i = 0, currentLine; currentLine = myLineStack[i]; i++) {
/// new path
context.beginPath();
/// set the color for this line
context.strokeStyle = currentLine.color;
/// draw the actual line
context.moveTo(currentLine.x1, currentLine.y1);
context.lineTo(currentLine.x2, currentLine.y2);
context.stroke();
}
(For optimizations you can for example clear only the area that needs redraw and draw a single index. You can also group lines/shapes with the same colors and draw then with a single setting of strokeStyle etc.)
You can not always redraw the canvas, you may have used filters that can not be reversed, or just use so many fill and stroke calls it would be impractical to redraw.
I have my own flood fill based on a simple fill stack that paints to a tolerances and does its best to lessen anti-aliasing artifacts. Unfortunately if you have anti-aliasing on repeated fills will grow the filled region.
Below is the function, adapt it as suited, it is a direct lift from my code with comments added.
// posX,posY are the fill start position. The pixel at the location is used to test tolerance.
// RGBA is the fill colour as an array of 4 bytes all ranged 0-255 for R,G,B,A
// diagonal if true the also fill into pixels that touch at the corners.
// imgData canvas pixel data from ctx.getImageData method
// tolerance Fill tolerance range 0 only allow exact same colour to fill to 255
// fill all but the extreme opposite.
// antiAlias if true fill edges to reduce anti-Aliasing artifacts.
Bitmaps.prototype.floodFill = function (posX, posY, RGBA, diagonal,imgData,tolerance,antiAlias) {
var data = imgData.data; // image data to fill;
antiAlias = true;
var stack = []; // paint stack to find new pixels to paint
var lookLeft = false; // test directions
var lookRight = false;
var w = imgData.width; // width and height
var h = imgData.height;
var painted = new Uint8ClampedArray(w*h); // byte array to mark painted area;
var dw = w*4; // data width.
var x = posX; // just short version of pos because I am lazy
var y = posY;
var ind = y * dw + x * 4; // get the starting pixel index
var sr = data[ind]; // get the start colour tha we will use tollerance against.
var sg = data[ind+1];
var sb = data[ind+2];
var sa = data[ind+3];
var sp = 0;
var dontPaint = false; // flag to indicate if checkColour can paint
// function checks a pixel colour passes tollerance, is painted, or out of bounds.
// if the pixel is over tollerance and not painted set it do reduce anti alising artifacts
var checkColour = function(x,y){
if( x<0 || y < 0 || y >=h || x >= w){ // test bounds
return false;
}
var ind = y * dw + x * 4; // get index of pixel
var dif = Math.max( // get the max channel differance;
Math.abs(sr-data[ind]),
Math.abs(sg-data[ind+1]),
Math.abs(sb-data[ind+2]),
Math.abs(sa-data[ind+3])
);
if(dif < tolerance){ // if under tollerance pass it
dif = 0;
}
var paint = Math.abs(sp-painted[y * w + x]); // is it already painted
if(antiAlias && !dontPaint){ // mitigate anti aliasing effect
// if failed tollerance and has not been painted set the pixel to
// reduce anti alising artifact
if(dif !== 0 && paint !== 255){
data[ind] = RGBA[0];
data[ind+1] = RGBA[1];
data[ind+2] = RGBA[2];
data[ind+3] = (RGBA[3]+data[ind+3])/2; // blend the alpha channel
painted[y * w + x] = 255; // flag pixel as painted
}
}
return (dif+paint)===0?true:false; // return tollerance status;
}
// set a pixel and flag it as painted;
var setPixel = function(x,y){
var ind = y * dw + x * 4; // get index;
data[ind] = RGBA[0]; // set RGBA
data[ind+1] = RGBA[1];
data[ind+2] = RGBA[2];
data[ind+3] = RGBA[3];
painted[y * w + x] = 255; // 255 or any number >0 will do;
}
stack.push([x,y]); // push the first pixel to paint onto the paint stack
while (stack.length) { // do while pixels on the stack
var pos = stack.pop(); // get the pixel
x = pos[0];
y = pos[1];
dontPaint = true; // turn off anti alising
while (checkColour(x,y-1)) { // find the bottom most pixel within tolerance;
y -= 1;
}
dontPaint = false; // turn on anti alising if being used
//checkTop left and right if alowing diagonal painting
if(diagonal){
if(!checkColour(x-1,y) && checkColour(x-1,y-1)){
stack.push([x-1,y-1]);
}
if(!checkColour(x+1,y) && checkColour(x+1,y-1)){
stack.push([x+1,y-1]);
}
}
lookLeft = false; // set look directions
lookRight = false; // only look is a pixel left or right was blocked
while (checkColour(x,y)) { // move up till no more room
setPixel(x,y); // set the pixel
if (checkColour(x - 1,y)) { // check left is blocked
if (!lookLeft) {
stack.push([x - 1, y]); // push a new area to fill if found
lookLeft = true;
}
} else
if (lookLeft) {
lookLeft = false;
}
if (checkColour(x+1,y)) { // check right is blocked
if (!lookRight) {
stack.push([x + 1, y]); // push a new area to fill if found
lookRight = true;
}
} else
if (lookRight) {
lookRight = false;
}
y += 1; // move up one pixel
}
// check down left
if(diagonal){ // check for diagnal areas and push them to be painted
if(checkColour(x-1,y) && !lookLeft){
stack.push([x-1,y]);
}
if(checkColour(x+1,y) && !lookRight){
stack.push([x+1,y]);
}
}
}
// all done
}
There is a better way that gives high quality results, the above code can be adapted to do this by using the painted array to mark the paint edges and then after the fill has completed scan the painted array and apply a convolution filter to each edge pixel you have marked. The filter is directional (depending on which sides are painted) and the code too long for this answer. I have pointed you in the right direction and the infrastructure is above.
Another way to improve the image quality is to super sample the image you are drawing to. Hold a second canvas that is double the size of the image being painted. Do all you drawing to that image and display it to the user on another canvas with CTX.imageSmoothingEnabled and ctx.setTransform(0.5,0,0,0.5,0,0) half size, when done and the image is ready half its size manually with the following code (don't rely on canvas imageSmoothingEnabled as it gets it wrong.)
Doing this will greatly improve the quality of your final image and with the above fill almost completely eliminate anti-aliasing artifacts from flood fills.
// ctxS is the source canvas context
var w = ctxS.canvas.width;
var h = ctxS.canvas.height;
var data = ctxS.getImageData(0,0,w,h);
var d = data.data;
var x,y;
var ww = w*4;
var ww4 = ww+4;
for(y = 0; y < h; y+=2){
for(x = 0; x < w; x+=2){
var id = y*ww+x*4;
var id1 = Math.floor(y/2)*ww+Math.floor(x/2)*4;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
id += 1;
id1 += 1;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
id += 1;
id1 += 1;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
id += 1;
id1 += 1;
d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
}
}
ctxS.putImageData(data,0,0); // save imgData
// grab it again for new image we don't want to add artifacts from the GPU
var data = ctxS.getImageData(0,0,Math.floor(w/2),Math.floor(h/2));
var canvas = document.createElement("canvas");
canvas.width = Math.floor(w/2);
canvas.height =Math.floor(h/2);
var ctxS = canvas.getContext("2d",{ alpha: true });
ctxS.putImageData(data,0,0);
// result canvas with downsampled high quality image.

Working with canvas to display only the drawn image and not whole canvas

With the plugin i found earlier on stackoverflow. Drawing has become smooth and nice. What i want is to only get the image part which i draw cropped from the canvas as an output and not the complete canvas. Can somebody help.
This is the code i am using for my canvas now: http://jsfiddle.net/sVsZL/1/
function canvasDisplay() {
var c=document.getElementById("canvas");
canvasImage=c.toDataURL("image/png");
document.getElementById("SSMySelectedImage").src=canvasImage;
}
Adding another answer because the other one was completely off.
Live Demo
What you need essentially is to keep track of a bounding box. What I do is create an object that holds the min values and max values of where you've drawn. This enables you to keep track of how big the image is and where it begins/ends.
this.dim = {minX : 9999, minY : 9999, maxX : 0, maxY : 0};
Then I created a function that checks the bounds.
this.setDimensions = function(x,y){
if(x < this.dim.minX){
this.dim.minX = x;
}
if(y < this.dim.minY){
this.dim.minY = y;
}
if(x > this.dim.maxX){
this.dim.maxX= x;
}
if(y > this.dim.maxY){
this.dim.maxY = y;
}
}
Make sure to check during clicking or moving.
this.mousedown = function(ev) {
tool.setDimensions(ev._x,ev._y);
};
this.mousemove = function(ev) {
tool.setDimensions(ev._x,ev._y);
};
And this is just a sample function that draws the portion to a new canvas that you could then save with toDataUrl
var button = document.getElementsByTagName("input")[0];
button.addEventListener("click", function(){
var savedCanvas = document.createElement("canvas"),
savedCtx = savedCanvas.getContext("2d"),
minX = PEN.dim.minX,
minY = PEN.dim.minY,
maxX = PEN.dim.maxX,
maxY = PEN.dim.maxY,
width = maxX - minX,
height = maxY - minY;
savedCanvas.width = width;
savedCanvas.height = height;
document.body.appendChild(savedCanvas);
savedCtx.drawImage(canvas,minX,minY,width,height,0,0,width,height);
});