Jump to content

What is the fastest way to paint?


clark
 Share

Recommended Posts

There is actually a couple of questions here. 

 

Imagine a simple paint tool,  what is the best (in theory) way to deal with the segments of drawing?

 

There is 2 approaches:

 

1) You can see that a user has stroked.  For example, between any 2 given frames, you can interpolate a circlular brush texture to a render texture.  For example overlapping ooooooooooooooo between the brush strokes.  This is good on desktop, but slowwwwwww on mobile.

 

2) You can do the same, but instead of interpolating a circular brush, you could create your 2 cicular points, and then place a rectangle between these points.  I guess this is faster?

 

But are there any other ways that I should consider before re-writing number one above?

 

3) What is the best way to deal with Dynamic colours? Is it better to have a brush texture (for example 12 colors, 12 brush textures?).  I ask because in Starling we used GrayScale with Tint but the colours were washed out. 

 

Thanks!

Link to comment
Share on other sites

Hey Rich that sounds cool :D Is this Aardman?

 

Anyway, I will try to make it more digestible.  

 

This is what I am after, this uses an "Interpolation" method.  It will look good when the textures are repaired which is out of my hands ATM.  

 

Q2kt0I1.jpg
 

What is the best way to have one Brush Texture which can be dynamically coloured?

As stated, in Starling this was awkward. We used a GrayScale image which was Tinted but this lost vibrancy. Colours tended to look a bit flat. Something to do with "Multiplying" colour tints on Gray Scale textures.  There was also something preventing me from doing this easily.  I think that the textures had to be pre-generated, a new SpriteSheet created, and then uploaded to the GPU otherwise ContextLoss would wipe out the brushes.  I do not know how much of this stuff applies to Canvas/WebGL. 

What is the best way to achieve Interpolation?

 

In my naive process,  I find point A and point B which occurred on an update cycle, and I interpolate all of the one pixel points between these 2 points and then draw a circle on each of these pixel points.  Here is a fast swipe which looks fine.

 

YqvkiJI.jpg

 

I thought that this process is the performance killer on Mobile.  It is painfully easy to swipe 300 pixels on an IPad within a single frame, that is 300 draw calls in the space of one update to the BitmapData. So it is this iteration count..... or how I am doing this operation which I feel is killing performance.  


However it is good to know that I am on the right track.

 

Link to comment
Share on other sites

I think a brush which has 2/3 colours at once (Not Blending). I think the result will kind of suck but my hand is forced.

 

There is a few ways we were thinking

 

1) A circle split into 3 equal vertical parts

2) A pie chart circle split into 3 parts (wedges)

3) 3 small brushes grouped together perhaps even with a rotation but that probably looks pretty bad. 
 

I guess depending on dynamic colours, some of these options might be better suited than others. 

 

Link to comment
Share on other sites

Ok I'm back so can write a bit more now :)

 

The way I handle the painting is to hook the tool to a mousemove callback (never an update loop, assuming Phaser of course) and then I do as you've suggested, so I iterate the distance from A to B over a pre-defined number of steps, every movement. Sometimes you only need one update, and sometimes if they're moving real fast you need a few more (mine is capped at 20). In either case I draw directly to a BitmapData as they move. If I wanted to limit this I could instead record the points and then only draw at a set interval - it won't be as smooth though.

 

I handle dynamic coloured brushes two ways. If I can do it via Canvas paths then it's easy, you just define a shape and fill it (easy for circles or whatever). For textured brushes we carefully created the paint texture so it worked well with the way Pixi tints graphics. Each tool has its own "texture" BitmapData, which I clear off, draw the brush texture to, tint it and then when painting I just paint directly from that BMDs canvas. So it only tints when the colour changes. Some brushes have 2 layers.. a tinted mask layer and an overlay layer. Note that Pixi has a bug in it re: tinting on Android, but this is fixed in Phaser.

 

The end result can often look quite cool :) Here's an example running on a black paper type:

 

post-1-0-39900400-1418746223.jpg

Link to comment
Share on other sites

Thanks for taking the time to share this Rich, know you are a busy guy right now.

 

These are things I need to look at:

 

mousemove callback (never an update loop)

 

I may use an update step (it is phaser). Ill check it

 

Note: Yes, this has helped to increase the responsive nature of the input cheers for the tip!

 

Sometimes you only need one update, and sometimes if they're moving real fast you need a few more (mine is capped at 20). 

 

With you until you said capped at 20.  Does this mean that the maximum distance you interpolate between 2 updates is capped to 20 pixels per update? Unlike my 300 pixel example which is possibly the biggest problem for me. For example if the user moves 100 pixels in a swipe, do you fill that with spots in 20 iterations, or do you set the distance to 20?

 

If I can do it via Canvas paths then it's easy, you just define a shape and fill it (easy for circles or whatever)

 

Checking this out this evening thanks!

 

For textured brushes we carefully created the paint texture so it worked well with the way Pixi tints graphics.

 

Ah yes, this may be our old Starling problem with washed out colours.  We used GrayScale but not "in a way that works well with tints" perhaps.  This could explain dull colours.  


This gives me plenty to look into!  The end result is awesome by the way!  

Link to comment
Share on other sites

The "20" cap means the maximum number of steps I make during the interpolation, per frame. So I don't step every single pixel difference, but divide the distance by (up to) 20 and then move based on that.

 

Our paint textures are virtually all white. I've attached an example (although you may not see it, but it's below!)

 

 

post-1-0-07526000-1418748874.png

Link to comment
Share on other sites

For the tinting you should look into how the blend mode 'overlay' works. Draw the resulted tint to a buffer canvas and you should be good to go no matter what lib you're using.

 

I'm going assume you are using the 2d context so my RGB values will be based on the range of 0-255. 

 

You'll probably have to write the code yourself but it gives much better results. Most systems just use multiply which is probably the easiest to implement:

(r1*r2) / 255, (g1*g2) / 255, (b1*b2) / 255 

 

but you loose some of the detail for lighter colors and dark colors are twice as dark. The opposite of that would be Screen which actually takes the negative value and blends it:

 

(r1* -r2) / 255, (g1* -g2) / 255, (b1* -b2 ) / 255

 

which will always be lighter. But you loose the darks completely.

 

So what Overlay does is it has a threshold, usually around 128 (or .5) and varies how it treats the combined colors based on that. It will either Multiply or Screen the color. Allowing you to have both shadows and highlights.

 

Check out this link for the math of all the Photoshop blend modes. http://www.venture-ware.com/kevin/coding/lets-learn-math-photoshop-blend-modes/

 

Overlay is the way to go if you ask me.

Link to comment
Share on other sites

  • 3 weeks later...

Thanks for all your help guys! 

I am pretty much on the road to success.  There is one final thing stumping me though, and that is patterns. 
 

jfgdRGg.png

 

The actual article for this image is here: 

http://perfectionkills.com/exploring-canvas-drawing-techniques/

I kind of understand it maybe. 

The problem is that I have a sub texture in a Texture Atlas which is 128x128.  I am trying to figure out how I can start off with this Phaser.Image and end up somehow being able to follow that link.

So for example, it seems like I need a Phaser.Image, and then I will draw that to a BitmapData, and then create a HTMLImageElement set the src of that to toDataURL() of my BitmapData.  And then I have an image which I can use with the context pattern method? 

Is this the right track do you think or am I missing something easier?
 

Link to comment
Share on other sites

So you guys are all pretty awesome :)!

 

I was very limited before by process.  I was unsure as to the features of the canvas because I had never directly used it, and also, I was unsure about how Phaser and the drawing methods gelled together. 

 

But I finally figured it out!  Just the simple fact that Phaser.BitmapData IS a Canvas basically and knowing that is my friend. 

 

So there are 2 methods which are needed to cover almost all possible drawing tools.

 

1) An iterative process (I was trying to stuff everything into this technique). Where you build a texture from colours/overlays/whatever, and then you paint that final result iteratively between 2 points.  I still am unsure if I will like performance, but for now its the best bet.  

2) A stroke based process using native canvas.  Pens, Pencils, Crayons, Spray Tools, Patterns, Erasing....  This is much faster than draw() and covers the majority of otherwise awkward functionality. 

 

You can also use method 2 to construct a brush for method 1.  For example, imagine a Pie chart multi colour brush. Draw the colours with drawing API, overlay it onto a Shape, and then use that outcome as the brush. 

 

Luckily, for the most part, these 2 features work with similar API.  Although one uses textures and one uses geometry. None the less, you can cover a vast amount of stuff. 

Also "Canvas for Dummies" never hurts anyone!

Thanks for your help with this guys, appreciated.

 

Omni Vibe:

 

For Erasing, I used this simple code:
 

Given that you know what the currentX/Y and previousX/Y (all tools need this but this is real demo code) and have access to a BitmapData (or other 2d context), it is as simple as this.  And this is really smooth and looks better than the iterative method on devices. 

 

Although I am troubled reading that browsers may be a problem. In that case, I will need to use a clearRect or translated Line. 

            if (previousX === 0 && previousY === 0) {                previousX = currentX;                previousY = currentY;            }                        //dst is a reference to a bitmap data which is the main canvas we draw on            var ctx: CanvasRenderingContext2D = dst.context;            ctx.globalCompositeOperation = "destination-out";            ctx.beginPath();            ctx.moveTo(previousX, previousY);            ctx.lineTo(currentX, currentY);            ctx.lineWidth = 30;            ctx.lineCap = "round";            //set the stroke style to 100% transparency, may not work on all browsers?            ctx.strokeStyle = "rbg(0, 0, 0, 1)";            ctx.stroke();                        //remember to dirty it because otherwise you see jack on webGL            dst.dirty = true;

You may want to store the original globalCompositionOperation in a variable and return it after dst.dirty, but I just set the correct operation on a per tool basis.

Link to comment
Share on other sites

We are actually doing something similar to you this past year in terms of configuration.  The engines we make can be customisable via forms. 

 

For example Hidden Object can be mapped out by an operator and X levels and themes and so on.

 

I am finding however, that making games, and making customisable games are worlds apart in complexity. 

Link to comment
Share on other sites

  • 2 weeks later...

 

 

For Erasing, I used this simple code:

 

Given that you know what the currentX/Y and previousX/Y (all tools need this but this is real demo code) and have access to a BitmapData (or other 2d context), it is as simple as this.  And this is really smooth and looks better than the iterative method on devices. 

 

Although I am troubled reading that browsers may be a problem. In that case, I will need to use a clearRect or translated Line. 

            if (previousX === 0 && previousY === 0) {                previousX = currentX;                previousY = currentY;            }                        //dst is a reference to a bitmap data which is the main canvas we draw on            var ctx: CanvasRenderingContext2D = dst.context;            ctx.globalCompositeOperation = "destination-out";            ctx.beginPath();            ctx.moveTo(previousX, previousY);            ctx.lineTo(currentX, currentY);            ctx.lineWidth = 30;            ctx.lineCap = "round";            //set the stroke style to 100% transparency, may not work on all browsers?            ctx.strokeStyle = "rbg(0, 0, 0, 1)";            ctx.stroke();                        //remember to dirty it because otherwise you see jack on webGL            dst.dirty = true;

You may want to store the original globalCompositionOperation in a variable and return it after dst.dirty, but I just set the correct operation on a per tool basis.

 

This seems very specific to Phaser... any idea how to translate that into PIXI (esp for WebGL)?

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...