Related
I'm currently working on web app for photo editing using FabricJS and one of features I need to implement is something like Clipping masks from Photoshop.
For example I have this assets: frame, mask and image. I need to insert image inside frame and clip it with mask. Most tricky part is in requirements:
User should be able to modify image inside frame, e.g. move, rotate, skew... Frame itself also can be moved inside canvas.
Number of layers is not limited so user can add objects under or above masked image.
Masks, frames and images is not predefined, user should be able to upload and use new assets.
My current solution is this:
Load assets
Set globalCompositeOperation of image to source-out
Set clipTo function for image.
Add assets on canvas as a group
In this solution clipTo function preserve image inside rectangular area of frame and with help of globalCompositeOperation I'm clipping image to actual mask. At first sight it works fine but if I add new layer above this newly added group it will be cutted off because of globalCompositeOperation="source-out" rule. I've created JSFiddle to show this.
So, that else could I try? I've seen some posts on StackOverflow with advices to use SVGs for clipping mask, but if I understand it correctly SVG must contain only one path. This could be a problem because of third requirement of my app.
Any advice in right direction will help, because right now I'm totally stuck with this problem.
You can do this by using ClipPath property of Img Object which you want to mask. With this, you can Mask Any Type of Object. and also you need to add some Ctx Configuration in ClipTo function of Img Object.
check this link https://jsfiddle.net/naimsajjad/8w7hye2v/8/
(function() {
var img01URL = 'http://fabricjs.com/assets/printio.png';
var img02URL = 'http://fabricjs.com/lib/pug.jpg';
var img03URL = 'http://fabricjs.com/assets/ladybug.png';
var img03URL = 'http://fabricjs.com/assets/ladybug.png';
var canvas = new fabric.Canvas('c');
canvas.backgroundColor = "red";
canvas.setHeight(500);
canvas.setWidth(500);
canvas.setZoom(1)
var circle = new fabric.Circle({radius: 40, top: 50, left: 50, fixed: true, fill: '', stroke: '1' });
canvas.add(circle);
canvas.renderAll();
fabric.Image.fromURL(img01URL, function(oImg) {
oImg.scale(.25);
oImg.left = 10;
oImg.top = 10;
oImg.clipPath = circle;
oImg.clipTo = function(ctx) {
clipObject(this,ctx)
}
canvas.add(oImg);
canvas.renderAll();
});
var bili = new fabric.Path('M85.6,606.2c-13.2,54.5-3.9,95.7,23.3,130.7c27.2,35-3.1,55.2-25.7,66.1C60.7,814,52.2,821,50.6,836.5c-1.6,15.6,19.5,76.3,29.6,86.4c10.1,10.1,32.7,31.9,47.5,54.5c14.8,22.6,34.2,7.8,34.2,7.8c14,10.9,28,0,28,0c24.9,11.7,39.7-4.7,39.7-4.7c12.4-14.8-14-30.3-14-30.3c-16.3-28.8-28.8-5.4-33.5-11.7s-8.6-7-33.5-35.8c-24.9-28.8,39.7-19.5,62.2-24.9c22.6-5.4,65.4-34.2,65.4-34.2c0,34.2,11.7,28.8,28.8,46.7c17.1,17.9,24.9,29.6,47.5,38.9c22.6,9.3,33.5,7.8,53.7,21c20.2,13.2,62.2,10.9,62.2,10.9c18.7,6.2,36.6,0,36.6,0c45.1,0,26.5-15.6,10.1-36.6c-16.3-21-49-3.1-63.8-13.2c-14.8-10.1-51.4-25.7-70-36.6c-18.7-10.9,0-30.3,0-48.2c0-17.9,14-31.9,14-31.9h72.4c0,0,56-3.9,70.8,26.5c14.8,30.3,37.3,36.6,38.1,52.9c0.8,16.3-13.2,17.9-13.2,17.9c-31.1-8.6-31.9,41.2-31.9,41.2c38.1,50.6,112-21,112-21c85.6-7.8,79.4-133.8,79.4-133.8c17.1-12.4,44.4-45.1,62.2-74.7c17.9-29.6,68.5-52.1,113.6-30.3c45.1,21.8,52.9-14.8,52.9-14.8c15.6,2.3,20.2-17.9,20.2-17.9c20.2-22.6-15.6-28-16.3-84c-0.8-56-47.5-66.1-45.1-82.5c2.3-16.3,49.8-68.5,38.1-63.8c-10.2,4.1-53,25.3-63.7,30.7c-0.4-1.4-1.1-3.4-2.5-6.6c-6.2-14-74.7,30.3-74.7,30.3s-108.5,64.2-129.6,68.9c-21,4.7-18.7-9.3-44.3-7c-25.7,2.3-38.5,4.7-154.1-44.4c-115.6-49-326,29.8-326,29.8s-168.1-267.9-28-383.4C265.8,13,78.4-83.3,32.9,168.8C-12.6,420.9,98.9,551.7,85.6,606.2z',{top: 0, left: 180, fixed: true, fill: 'white', stroke: '', scaleX: 0.2, scaleY: 0.2 });
canvas.add(bili);
canvas.renderAll();
fabric.Image.fromURL(img02URL, function(oImg) {
oImg.scale(0.5);
oImg.left = 180;
oImg.top = 0;
oImg.clipPath = bili;
oImg.clipTo = function(ctx) {
clipObject(this,ctx)
}
canvas.add(oImg);
canvas.renderAll();
});
function clipObject(thisObj,ctx)
{
if (thisObj.clipPath) {
ctx.save();
if (thisObj.clipPath.fixed) {
var retina = thisObj.canvas.getRetinaScaling();
ctx.setTransform(retina, 0, 0, retina, 0, 0);
// to handle zoom
ctx.transform.apply(ctx, thisObj.canvas.viewportTransform);
thisObj.clipPath.transform(ctx);
}
thisObj.clipPath._render(ctx);
ctx.restore();
ctx.clip();
var x = -thisObj.width / 2, y = -thisObj.height / 2, elementToDraw;
if (thisObj.isMoving === false && thisObj.resizeFilter && thisObj._needsResize()) {
thisObj._lastScaleX = thisObj.scaleX;
thisObj._lastScaleY = thisObj.scaleY;
thisObj.applyResizeFilters();
}
elementToDraw = thisObj._element;
elementToDraw && ctx.drawImage(elementToDraw,
0, 0, thisObj.width, thisObj.height,
x, y, thisObj.width, thisObj.height);
thisObj._stroke(ctx);
thisObj._renderStroke(ctx);
}
}
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
<canvas id="c" width="400" height="400"></canvas>
Not sure what you want.
If you want the last image loaded (named img2), the one you send to the back to not effect the layers above do the following.
You have mask,frame,img, and img2;
Put them in the following order and with the following comp settings.
img2, source-over
img, source-over
mask, destination-out
frame, source-over
If you want something else you will have to explain it in more detail.
Personally when I provide masking to the client I give them full access to all the composite methods and allow them to work out what they need to do to achieve a desired effect. Providing a UI that allows you to change the comp setting, and layer order makes it a lot easier to sort out the sometimes confusing canvas composite rules.
I'd suggest looking at this solution.
Multiple clipping areas on Fabric.js canvas
You end up with a shape layer that is used to define the mask shape. That shape then gets applied as a clipTo to your image.
The one limitation I can think off though that you might run into is when you start to rotate various shapes. I know I have it working great with a rectangle and a circle, however ran into some issues with polygons from what I recall... This was all setup under and older version of FabricJS however, so there may have been some improvements there that I'm not experienced with.
The other issue I ran into was drop shadows didn't render correctly when passed to a NodeJS server running FabricJS.
I want to create a simple jsp / servlet code which should have the following:
1) Display an image having different sections. For example: a country map.
2) Allow the user to mark sections on this image using mouse drag. As the user keeps dragging the mouse, the area gets overlay-ed with some different color.
3) As the user moves the mouse, the x and y coordinates on the image should also get calculated. [This feature is however optional.]
The purpose of this application is to mark different 'zones' in an image, which will get saved in the database along with their x-y coordinates.
Can someone please suggest how to achieve this ? Is there any library / API available which could be helpful ?
Regards,
Here is a snippet to get you going. Replace the text setting of the div#server with code to send coordinates to your server. I'll leave the background image for canvas and other important stuff up to you.
var c = $("#map");
var ctx = c[0].getContext("2d");
var down = [0, 0];
var bound = c[0].getBoundingClientRect();
c.mousedown(function(e) {
down = [e.clientX - bound.left, e.clientY - bound.top];
});
c.mouseup(function(e) {
var rect = [
down[0],
down[1], ((e.clientX - bound.left) - down[0]), ((e.clientY - bound.top) - down[1])
];
ctx.fillStyle = "#bbbbbb";
ctx.fillRect(rect[0], rect[1], rect[2], rect[3]);
$("#server").text("Send: " + rect);
});
body {
background-color: lightblue;
}
#map {
background-color: white;
cursor: crosshair;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="map" width="256" height="256"></canvas>
<div id="server"></div>
For a certain image I have a list containing the pixel coordinates of all the points in a polygon segmenting all the objects it contains (look at the image below).
For instance, for the person I have a list l1 = [x0,y0,x1,y1,...,xn,yn], for the cat a list l2 = [x0',y0',x1',y1',...,xk',yk'], and similarly for all the objects.
I have 2 questions:
What is the best javascript library to use to draw on top of an image? Given the raw image I would like to obtain the result seen below.
I would like each segmentation to be visualized only when the mouse hovers on top of it. For this I believe I should bind this drawing function to the mouse position.
I'm thinking at something with the structure below but don't know how to fill the gaps, could you please give me some indication?
$(.container).hover( function(e) {
//get coordinates of mouse
//if mouse is over one object
//draw on top of image the segmentation for that object
});
container is the class of the div containing the image so I should be able to get the coordinates of the mouse since the image starts at the top left corner of the container div.
Simply rebuild the polygons from each array and do a hit test using the mouse position.
First: If you have many arrays defining the shapes it could be smarter to approach it in a more general way instead of using variables for each array as this can soon be hard to maintain. Better yet, an object holding the array and for example id could be better.
Using an object you could do - example:
function Shape(id, points, color) {
this.id = id;
this.points = points;
this.color = color;
}
// this will build the path for this shape and do hit-testing:
Shape.prototype.hitTest = function(ctx, x, y) {
ctx.beginPath();
// start point
ctx.moveTo(this.points[0], this.points[1]);
// build path
for(var i = 2, l = this.points.length; i < l; i += 2) {
ctx.lineTo(this.points[i], this.points[i+1]);
}
ctx.closePath();
return ctx.isPointInPath(x, y);
};
Now you can create new instances with the various point arrays like this:
var shapes = [];
shapes.push(new Shape("Cat", [x0,y0,x1,y1, ...], "rgba(255,0,0,0.5)");
shapes.push(new Shape("Woman", [x0,y0,x1,y1, ...], "rgba(0,255,0,0.5)"));
...
When you get a mouse position simply hit-test each shape:
$(".container").hover( function(e) {
//get corrected coordinates of mouse to x/y
// redraw canvas without shapes highlighted
for(var i = 0, shape; shape = shapes[i]; i++) { // get a shape from array
if (shape.hitTest(ctx, x, y)) { // is x/y inside shape?
ctx.fillStyle = shape.color; // we already have a path
ctx.fill(); // when testing so just fill
// other tasks here...
break;
}
}
});
check this Link it might be slow your problem.
Include necessary javascript library files
jquery.min.js,raphael.min.js,json2.min.js,raphael.sketchpad.js
To create an editor
<div id="editor"></div>
<form action="save.php" method="post">
<input type="hidden" name="data" />
<input type="submit" value="Save" />
</form>
<script type="text/javascript">
var sketchpad = Raphael.sketchpad("editor", {
width: 400,
height: 400,
editing: true
});
// When the sketchpad changes, update the input field.
sketchpad.change(function() {
$("#data").val(sketchpad.json());
});
</script>
I created a jsfiddle for my current code. http://jsfiddle.net/gL5sB/38/
I am trying to change the body background css on scroll event. When the background changes it appears to flicker when the css is updated and new image is loaded. At times it seems smooth and then it seems to get worse. Very strange. Curious if anyone knows how to optimize?
I am preloading the images. Not sure why the flicker. Any ideas?
$(document).ready(function () {
switchImage();
});
$(window).scroll(function () {
switchImage();
});
var pics = []; // CREATE PICS ARRAY
//PRELOAD FUNCTION TO SET UP PICS ARRAY IN MEMORY USING IMAGE OBJECT
function preload() {
for (i = 0; i < arguments.length; i++) {
pics[i] = new Image();
pics[i].src = arguments[i];
//alert("preload " + arguments[i]);
}
}
preload(
'bgImage/100.jpg',
'bgImage/101.jpg',
'bgImage/102.jpg',
'bgImage/103.jpg',
'bgImage/104.jpg',
'bgImage/105.jpg',
'bgImage/106.jpg',
'bgImage/107.jpg',
'bgImage/108.jpg',
'bgImage/109.jpg',
'bgImage/110.jpg',
'bgImage/111.jpg',
'bgImage/112.jpg',
'bgImage/113.jpg'
);
function switchImage() {
var s = $(window).scrollTop()/10;
var index = Math.floor(s / 5);
$('body').css('background-image', 'url(' + pics[index].src + ')');
}
Why don't you use one image (sprite image) and just move it with background-position instead of replacing the image?
(about the size - you can set percentage based background-size in your case - the height will be 1400%
Because your'e preloading all the images anyway - it won't cost you in page loading time - and it might also save some time because with the right compression 1 image of 14 will weight less then 14 images of 1
Chrome is OK. I can see the flicker in IE. A Solution for that is at the bottom of this post.
I suspect A video version would compress and load faster than all the images, but as suggested by #Allendar drawing it would be the most transmission efficient. I would suggest canvas or SVG.
Another way using images along would be to have individual components as either images or icon fonts placed on the display with absolute positioning and then just turning them on or off in script. But that's a very complex solution.
I think the simplest and fastest method for you to solve the issue today though would be to just tweak the solution you have. As other people have suggested, the multiple images approach won't be super efficient and if you're going to take that route at least make sure you set the caching headers on your web server to cache indefinitely;
Cache-Control: public;
Expires: Mon, 31 Dec 2035 12:00:00 GMT
OK, so the flicker problem is just a bug/inefficiency in the rendering engine in IE, so here's a workaround that uses a different approach.
Basically stretch an absolutely positioned DIV over the body, instead of using the body itself. In fact, in this case we use multiple DIVs, one for each image and overlay them on top of one another. You could create these nodes in script as well.
Secondly, add another DIV for your content, and overlay that over the body as well;
<body>
<div id="b100" class="background" style="background-image:url('http://ingodwetrustthemovie.com/bgImage/100.jpg')"></div>
<!-- rest omitted -->
<div id="b113" class="background" style="background-image:url('http://ingodwetrustthemovie.com/bgImage/113.jpg')"></div>
<div id="content">
<div id="test">test div</div>
here is some text
</div>
</body>
The simply show one at a time and hide the rest;
function switchImage() {
var s = $(window).scrollTop()/10;
var index = Math.floor(s / 5);
$('.background').hide();
$('.background').eq(index).show();
}
I had a suspicion that twiddling the css display option would be less flickery than changing the src attribute and it seems to do the trick.
Here's the Fiddle
Of course you may still need to optimise the code to allow for the first loaded image to be shown first instead of the plain background, but I think this demonstrates the principle of a fix.
You could also consider making a very large CSS Sprite, by bundling the images into one huge strip and then using background-position. That would probably work on the body tag itself then. Of course this would mean downloading a huge file before you can display any images at all, but there are two advantages;
One image (especially with such similarity) will compress way better than each individual one in isolation.
Using the same caching directives, that's only one HTTP/GET/302 cycle instead of 13 once you've fetched the image the first time, so your page may load faster still.
SVG
SVG elements work much like the DOM. If you can get your content delivered as an SVG you can drill into the graphic, locate the elements, give them IDs etc, and manipulate them much like you would any other DOM element;
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<ellipse id="e1" cy="420" cx="200" rx="420" ry="30" style="fill:3f5566" />
<ellipse id="e2" cy="420" cx="170" rx="390" ry="20" style="fill:4f5566" />
<ellipse id="e3" cy="420" cx="145" rx="370" ry="15" style="fill:5f5566" />
<ellipse id="e4" cy="420" cx="100" rx="370" ry="20" style="fill:6f5566" />
<ellipse id="e5" cy="420" cx="45" rx="300" ry="15" style="fill:8f5566" />
</svg>
Here's another fiddle that hides/unhides SVG elements based on a scroll.
Ideally, assuming they've generated that graphic in 'layers', try and have your designers deliver you an SVG where the layers are converted into groups. Adobe Illustrator can do that for instance.
Then you can easily turn off the layers/groups as necessary to create the animated effect.
If the images aren't really needed, you can create some system for yourself where you draw, and keep updating, the background using canvas. This would be start;
JSfiddle
HTML
<img id="template" src="http://ingodwetrustthemovie.com/bgImage/100.jpg" />
<canvas id="absBg" width="1000px" height="560px"></canvas>
CSS
body {
margin: 0px;
}
#template {
position: absolute;
width: 1000px;
height: 563px;
}
#absBg {
position: absolute;
/*background: rgba(0, 0, 0, 0.50);*/
/*height: 100%;*/
}
JavaScript/jQuery
'use strict';
function drawBlock(ctx, color, line_width, start, lines) {
ctx.lineWidth = line_width;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
for (var i = 0; i < lines.length; i++) {
ctx.lineTo(lines[i][0], lines[i][1]);
}
ctx.closePath();
ctx.stroke();
}
function drawBg() {
var absBg = document.getElementById('absBg');
var ctx = absBg.getContext('2d');
var demo_red = 'red';
var grey = '#28282';
var color = demo_red;
drawBlock(ctx, color, 1, [185, 87], [[205, 75], [226, 98], [207, 110]]);
drawBlock(ctx, color, 1, [235, 60], [[253, 50], [272, 71], [254, 81]]);
}
$(document).ready(function () {
drawBg();
// Scroll trigger
$(window).scroll(function () {
});
});
The demo uses the actual background image you have as a background-model, where you can draw over with the canvas. This way you can mimick the look of the original image you have.
You can try to manage some kind of "difference-array" where you store which blocks are different on which locations. This way you can trigger the function on scroll with certain parameters to let it change the drawing based on that.
I hope for you that you don't "need" the images per se. Drawing with canvas is so much faster than loading tons of images :)
Here is a solution that works (2014.7.11) at firefox 30.0, chrome 35.0, opera 22.0, ie 11.0:
STEP 1: add these lines at .htaccess:
# cache for images
<FilesMatch "\.(png)$">
Header set Cache-Control "max-age=10000, public"
</FilesMatch>
detailed description of this problem and how to fix it:
https://code.google.com/p/chromium/issues/detail?id=102706
STEP 2: add images preloading, for example:
var pics = []; // CREATE PICS ARRAY
$(document).ready(function(){
...
preload(
'/public/images/stars.red.1.star.png',
'/public/images/stars.red.2.star.png',
'/public/images/stars.red.3.star.png',
'/public/images/stars.red.4.star.png',
'/public/images/stars.red.5.star.png',
'/public/images/stars.empty.png'
);
...
$('.rating').on('mousemove', function(event){
var x = event.pageX - this.offsetLeft;
var id = getIdByCoord(x); //
if ($(this).data('current-image') != id) {
$(this).css('background-image', 'url(' + pics[id].src + ')');
$(this).data('current-image', id);
}
})
...
})
...
// PRELOAD FUNCTION TO SET UP PICS ARRAY IN MEMORY USING IMAGE OBJECT
function preload() {
for (i = 0; i < arguments.length; i++) {
pics[i] = new Image();
pics[i].src = arguments[i];
// alert("preload " + arguments[i]);
}
}
P.S. thanks Shawn Altman
I've been working on adapting arbor.js to use images.
However, being a relative JS noob what I have is totally un-optimised.
As far as I can tell, the way I've set it up is recreating the image object for every image and every frame, resulting in tons of flicker.
Can anyone suggest a way to move the new Image() stuff out of the redraw function into the initiation? As far as I know this is a basic OOP issue, but totally stuck.
Thanks!
Pastebin of where I'm up to on the output script
Current status.
Apologies all! There's a few steps. I'll highlight the key stages, the rest is from the tutorial.
First, add the relevant information to your JSON, for example:
nodes:{
innovation:{ 'color':colour.darkblue,
'shape':'dot',
'radius':30,
'image': 'innovation.png',
'image_w':130,
'image_h':24,
'alpha':1 },
participation:{ 'color':colour.purple,
'shape':'dot',
'radius':40,
'image':'participation.png',
'image_w':130,
'image_h':24,
'alpha':1 },
...
Cache all your images when the thing loads.
init:function(system){
// Normal initialisation
particleSystem = system
particleSystem.screenSize(canvas.width, canvas.height)
particleSystem.screenPadding(25, 50)
that.initMouseHandling()
// Preload all images into the node object
particleSystem.eachNode(function(node, pt) {
if(node.data.image) {
node.data.imageob = new Image()
node.data.imageob.src = imagepath + node.data.image
}
})
...
Then, for moving the images themselves...
particleSystem.eachNode(function(node, pt){
...
// Image info from JSON
var imageob = node.data.imageob
var imageH = node.data.image_h
var imageW = node.data.image_w
...
// Draw the object
if (node.data.shape=='dot'){
// Check if it's a dot
gfx.oval(pt.x-w/2, pt.y-w/2, w,w, {fill:ctx.fillStyle, alpha:node.data.alpha})
nodeBoxes[node.name] = [pt.x-w/2, pt.y-w/2, w,w]
// Does it have an image?
if (imageob){
// Images are drawn from cache
ctx.drawImage(imageob, pt.x-(imageW/2), pt.y+radius/2, imageW, imageH)
}
}else {
// If none of the above, draw a rectangle
gfx.rect(pt.x-w/2, pt.y-10, w,20, 4, {fill:ctx.fillStyle, alpha:node.data.alpha})
nodeBoxes[node.name] = [pt.x-w/2, pt.y-11, w, 22]
}
...