I've been beating around the bush a lot, so I'll explain my problem here and hope with the whole picture, somebody has some ideas. With the following image:
I need to detect a mouseover on the blobs over her eyes and mouth, and solve this problem in a general form. The model and blobs are on two different layers, so I can produce one image with only the blobs, and one with only the model, and somehow synchronise a virtual cursor over the blobs while it actually hovers over the model.
I can also make the blobs polygons, for hit testing, but I think a colour hit test would be much easier. If I hit blue, I am on her mouth and I show lipstick images; if I hit pink, I'm over her eyes, and display eye makeup images.
What are the suggestions and conversation of the learned ones here?
The simpler way to do it would be to load the layer image in a canvas, then get all its pixel data. When the mouse is hovering the model image, find out what color is currently selected and if it is different from a previous one, trigger an event to indicate that the selection has changed.
Here is an example, feel free to toy with it; but be aware that it doesn't handle all cases:
what if the layer and the model image are not the same size
what if the layer and the model image are not the same width/height ratio
what if you want to use some alpha channel (the example doesn't take it into account)
$(function() {
/* we load all the image data first */
var imageData = null;
var layerImage = new Image();
layerImage.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
context = canvas.getContext('2d');
context.drawImage(this, 0, 0, canvas.width, canvas.height);
imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
};
/* it's easier to set the image data for example as base64 data */
layerImage.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABp0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuMTFH80I3AAAA5klEQVR4Xu3WMQ7CMAwF0IxcgCtw/xsGdWApItWHKorLQ8qESe2HbWjNiwABAgQIECBAgAABAgQIECBAgMAUgd4e/ehMSeTMh4wK2j/nqPjt/TNzm3IXgEFb64CdgBG44hKcsmg8pKDAvbf+7SlY7nvK3xb/+lx5BAA/jMCGpwOqC/z9CFT/ApfP/9Z6T87yBaUJJsVvsen9y8cDMAJ2gCWY7IHll1qaYFL84a9AelnF+CFwxYLSnAGMBFLNivE6QAcMBCq2dJqzETACRuCzQDpPFePTv9riCRAgQIAAAQIECBC4nsATagY67TVyuhAAAAAASUVORK5CYII=";
var pColor = null;
/* on mouse over the model image */
$("#model").mousemove(function(event) {
/* we correct the offset */
var offset = $(this).offset();
var relX = event.pageX - offset.left;
var relY = event.pageY - Math.round(offset.top);
/* and get the pixel values at this place (note we are not keeping the alpha channel; it's your decision whether or not it is valuable */
var pixelIndex = relY * layerImage.width + relX;
var dataIndex = pixelIndex * 4;
var color = [imageData[dataIndex], imageData[dataIndex + 1], imageData[dataIndex + 2]];
if (pColor == null) {
/* we trigger when first entering the image */
$(this).trigger("newColor", {
message: "Initial layer color",
data: color
});
} else if (pColor[0] != color[0] || pColor[1] != color[1] || pColor[2] != color[2]) {
/* we trigger if the new position is a new color in the layer image */
$(this).trigger("newColor", {
message: "Changed layer color",
data: color
});
}
pColor = color;
});
/* some small help to convert rgb to css colors */
function rgb2hex(red, green, blue) {
var rgb = blue | (green << 8) | (red << 16);
return '#' + (0x1000000 + rgb).toString(16).slice(1)
}
/* there you have the new layer color event management; for the example sake we change the color of some text */
$("#model").on("newColor", function(event, eventData) {
$("#selector").css("color", rgb2hex(eventData.data[0], eventData.data[1], eventData.data[2]));
});
});
img {
border: 1px solid silver
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<body>
<h4>Model image</h4>
<img id="model" src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjExR/NCNwAAAtdJREFUaEPtmS2PwkAQhhGQEASEBIECFAJBgoCAxpDgsWBxSP4lPwGJRCLv5m4uk6Xt7s5He9dLFst2+z7vfG3bxsc//zX+uf6PBPDXEUwRSBEwOpBSyGig+fIUAbOFxg3KjMD1em3EfqvV6vl8GkW7l5cJsF6vY/p//i8RQwbwer22221U5W63K/T4fr93u1263LdMFB8ZAEc96BsOhwER5WIIAMB+NO92u4lMKlw8m80oFJaMEgCg/ZADdvW0A2GMRiPdtgKAEu13tUJG4c667sQFoBap8yl8FdiPDIqyZgFQ9oerU83mlrV0ExaAJfshdKfTiSMLg8BZKR5k4ewPS4TpNplMOLKqAqD88YngSwxjVAVgyR+O8bSmKoCKumeerVoAkZe6xQnA45vCGH7rfGuIFbVRBUDgvO2bCefzWXEj4I8PDtyXOYzQ0cVi4WMonAmWSR8HIDWbzUZ33grXNKifTqcA3Ov1FNUfB3AdhdOv4h6+S0D6fr/HWLVaLd1jBgsAFNChV5RLAVpKelDfbrd16lk1QCLczLZjoPEW6SiMG4FAdUphXO/tCSkD8GEwi9uVDvZLny6gZg6HQ8YvDYBr23g8DnR9319S6XjH5XIJGw4Gg7fxZwwiFDfsyGfQSQeRvnci1ghE+XXz1d3W7bb5WVF3ABpzvpZVdwDM+8CYqyOAmzPRx6l6ARyPx3w/CNd9jQDgNEHq+RO6LgCgvt/vA8Dlcol2tjLnQPRmhW00n+W4rNPpPB6P6J5cAHofqnhlSffIAIDT8/k8n+jNZhO8l6qPHOYKPxlJ3+Wj1swpqJRzKPc06n6JIOf4GBmz1U778kpWxJmvQ9EjEOQergEX1KegcEnIAHAvPgaIRgDpMwO/jjUA/N1hJT3Hia7iL64c4KtRfP/4mkQrq9r3rVUngEBMUgQYCZtqIGRSSqGUQgwHUgoZTSrrQ3KhjN8oYiN/+PJPqpb83Htu7qcAAAAASUVORK5CYII="
/>
<p>You are pointing at some <strong><span id="selector">color</span></strong>
</p>
<hr/>
<h4>Layer image (reference only, not displayed in page)</h4>
<img id="layer" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABp0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuMTFH80I3AAAA5klEQVR4Xu3WMQ7CMAwF0IxcgCtw/xsGdWApItWHKorLQ8qESe2HbWjNiwABAgQIECBAgAABAgQIECBAgMAUgd4e/ehMSeTMh4wK2j/nqPjt/TNzm3IXgEFb64CdgBG44hKcsmg8pKDAvbf+7SlY7nvK3xb/+lx5BAA/jMCGpwOqC/z9CFT/ApfP/9Z6T87yBaUJJsVvsen9y8cDMAJ2gCWY7IHll1qaYFL84a9AelnF+CFwxYLSnAGMBFLNivE6QAcMBCq2dJqzETACRuCzQDpPFePTv9riCRAgQIAAAQIECBC4nsATagY67TVyuhAAAAAASUVORK5CYII="
/>
</body>
If you can have both images (the one with the blob and the one without), I think you can do this using HTML5 canvas.
draw the image normally
draw the blob image beneath the master image so it is invisible
copy the blob to a Canvas
onMouseOver, retrieve pixel data (R,G,B and alpha) for the Canvas at the appropriate coordinates
profit
Twist: you might be able to do this with only one image and its alpha channel, if you don't need it for anything else - give the pixels a full opacity (A=255) everywhere except in blobs 1, 2 and 3, which will have opacity equal to 255-(1,2,3...). You can't have too many different blobs or the transparency will become noticeable. Haven't tried, but it should work. Given the likely compressibility of a "blob-only" image, a pair of images (one without transparency, one also without transparency and with only N+1 colours, PNG compressed) should yield better results.
More-or-less-pseudo code with two images, using jQuery (can be done without):
var image = document.getElementById('mainImage')
var blobs = document.getElementById('blobImage');
// Create a canvas
canvas = $('<canvas/>')[0];
canvas.width = image.width;
canvas.height = image.height;
// IMPORTANT: for this to work, this script and blobImage.src must be both
// in the same security domain, or you'll get "this operation is insecure"
canvas.getContext('2d').drawImage(blobs, 0, 0, image.width, image.height);
// Now wait for it.
$('#mainImage').mouseover(function(event) {
// TO DO: offset clientX, clientY by margin on mainImage
var ctx = canvas.getContext('2d');
// Get one pixel
var pix = ctx.getImageData(event.clientX, event.clientY, 1, 1);
// Retrieve the red component
var red = pix.data[0];
if (red > 128) {
// ... do something for red
}
});
You could use SVG graphics to layer over the image.
My example uses an ellipse but you could use a polygons just as easily.
You could use the colour like you stated in your question or add an extra property to the svg element. The example uses onclick but mouseover works as well.
example js:
function svg_clicked(objSVG)
{
alert(objSVG.style.fill);
alert(objSVG.getAttribute('data-category'));
}
example svg:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<ellipse cx="110" cy="80" rx="100" ry="50" style="fill:red;" onclick="svg_clicked(this);" data-category="lipstick" />
</svg>
Here's a fiddle (move the mouse over the O's in the picture)
It still works if you make the svg element transparent (using fill:transparent).
You can change the overlay to a colour or outline quickly for testing.
I highly recommend the time tested method.
The easiest way to both create blobs and to detect if the mouse is over them is to use svg graphics on top of the other image. SVG supports mouseover events and allows vector shapes which are going to give you far greater precision than using <map> or <area>.
I found this question that might also shed some light on where I am coming from: Hover only on non-transparent part of image. Read the second answer down because it will likely be prefered in your situation.
The svg elements on your image would be transparent (or whatever you would want), and you could easily detect the mouse over events.
The library from that question is called raphael. Hopefully this proves to be useful.
Related
I am using OpenSeadragon to display a large image so that it scrolls infinitely as a mosaic. This is working fine, with the code listed below. However, the initial zoom level varies when opened in Chrome, Firefox or Opera, and the image is displayed from a random position within the image, instead of having the desired coordinates in the upper left corner.
Two related questions:
Are there properties to be set to specify the coordinates of the image that should be displayed in the upper left corner when the image is first loaded?
Is there a property to specify the zoom level when the image is first loaded? I set defaultZoomLevel to 0.6 but each browser seem to react differently to it, with Chrome being the only one to get it about right and the other two showing a much more zoomed out image.
Thanks for any help!
<body>
<div id="openseadragon1" style="width: 6560px; height: 3801px;"></div>
<script src="/openseadragon/openseadragon.min.js"></script>
<script type="text/javascript">
var viewer = OpenSeadragon({
id: "openseadragon1",
showNavigationControl: false,
wrapHorizontal: true,
wrapVertical: true,
visibilityRatio: 0,
zoomPerScroll: 1.2,
defaultZoomLevel: 0.6,
minZoomImageRatio: 0.6,
maxZoomPixelRatio: 2.5,
prefixUrl: "/openseadragon/images/",
tileSources: {
type: 'image',
url: '/myBigImage.png'
}
});
// -------------------------------------
// Edit based on iangilman's reply
// -------------------------------------
viewer.addHandler('open', function() {
var tiledImage = viewer.world.getItemAt(0);
var imageRect = new OpenSeadragon.Rect(10, 50, 1000, 1000);
var viewportRect = tiledImage.imageToViewportRectangle(imageRect);
viewer.viewport.fitBounds(viewportRect, true);
});
</script>
</body>
Yes, the zoom levels in OpenSeadragon are relative to the width of the viewport (a zoom of 1 means the image is exactly filling the viewport width-wise), which is probably why you're getting different results on different devices. If you want to choose a specific portion of the image, you'll have to convert from image coordinates to viewport coordinates. Either way, your best bet for reliable results is to explicitly choose the location after the image loads. For example:
viewer.addHandler('open', function() {
// Assuming you are interested in the first image in the viewer (or you only have one image)
var tiledImage = viewer.world.getItemAt(0);
var imageRect = new OpenSeadragon.Rect(0, 0, 1000, 1000); // Or whatever area you want to focus on
var viewportRect = tiledImage.imageToViewportRectangle(imageRect);
viewer.viewport.fitBounds(viewportRect, true);
});
I have the .obj of a T-Shirt, it contains a few meshes and materials and I'm coloring it using a CanvasTexture fed by an inline svg.
Now I should add a logo at a specific location (more or less above the heart), but I'm struggling to understand which is the best/proper way of doing it (I'm quite new to 3D graphics and Three.js). This is what I tried so far:
since I'm coloring the T-Shirt through a CanvasTexture fed by an inline svg, I thought it would have been easy to just draw the logo into the svg at specific coordinates. And it was easy indeed, but the logo gets not rendered (or is not visible in some way) on the texture/mesh, although it is visible in the inline svg. So CanvasTexture probably doesn't work with embedded images (I tried both base64 and URL)
so, I started looking into more 3d "native" ways of doing it, but I haven't found one that really makes sense to me. I know there's ShaderMaterial in threejs, which I could use to selectively render pixels of the logo or pixels of the cloth, but that means making a lot of complex computation to figure out where the logo should be and I can't believe drawing a simple JPEG or PNG with specific coordinates and size can be so complex... I must have missed an obvious solution.
EDIT
Here is how I'm adding the image to the inline svg (option 1 above).
Add the image to the inline svg
const groups = Array.from(svg.querySelectorAll('g'));
// this is the "g" tag where I want to add the logo into
const targetGroup = groups.find((group: SVGGElement) => group.getAttribute('id') === "logo_placeholder");
const image = document.createElement('image');
image.setAttribute('width', '64');
image.setAttribute('height', '64');
image.setAttribute('x', '240');
image.setAttribute('y', '512');
image.setAttribute('xlink:href', `data:image/png;base64,${base64}`);
targetGroup.appendChild(image);
Draw inline svg to 2d canvas
static drawSvgToCanvas = async (canvas: HTMLCanvasElement, canvasSize: TSize, svgString: string) => {
return new Promise((resolve, reject) =>
canvas.width = canvasSize.width;
canvas.height = canvasSize.height;
const ctx = canvas.getContext('2d');
const image = new Image(); // eslint-disable-line no-undef
image.src = `data:image/svg+xml;base64,${btoa(svgString)}`;
image.onload = () => {
if (ctx) {
ctx.drawImage(image, 0, 0);
resolve();
} else {
reject(new Error('2D context is not set on canvas'));
}
};
image.onerror = () => {
reject(new Error('Could not load svg image'));
}
});
};
Draw 2d canvas to threejs Texture
const texture = new Three.CanvasTexture(canvas);
texture.mapping = Three.UVMapping; // it's the default
texture.wrapS = Three.RepeatWrapping;
texture.wrapT = Three.RepeatWrapping; // it's the default
texture.magFilter = Three.LinearFilter; // it's the default
texture.minFilter = Three.LinearFilter;
texture.needsUpdate = true;
[...add texture to material...]
For some reason, canvases don't like SVGs with embedded images, so for a similar project I had to do this in two steps, rendering the SVG and the image separately:
First, render the SVG on the canvas, and then render the image on top of that (on the same canvas).
const ctx = canvas.getContext('2d');
ctx.drawImage(imgSVG, 0, 0);
ctx.drawImage(img2, 130, 10, 65, 90);
const texture = new THREE.CanvasTexture(canvas);
Example: https://jsfiddle.net/0f9hm7gx/
I want to use image-maps inside fancybox 3. Goal is to display mountain panoramas, where the user could point on a summit and get name and data. The usual recommendation is to use a SVG based image map for this like in this pen. Due to the size of the images the fancybox zoom functionality is important.
While fancybox will display SVGs as an image like in this pen it is not possible to use the <image> tag with an external source inside the SVG file. Even worse: SVG files used as source of an <img> tag would not show the map functionality (see this question).
I tried to replace the <img> tag in fancybox with an <object> tag using the SVG file as data attribute. This shows the image with the map functionality correctly but fancybox won't zoom it any more.
So eventually the question boils down to how I can make an <object> (or an inline SVG or an iframe) zoomable just like an image in fancybox 3.
I'm open to other solutions as well. I only want to use fancybox to keep the appearance and usage the same as other image galleries on the same page. I'd even use an old style <map>, where I would change the coords using jquery to have it responsive. I tried that, attaching the map manually in developer tools as well as programmatically in afterLoad event handler, but apparently this doesn't work in fancybox 3 either.
The areas are polygons, so using positioned div's as overlays is no solution.
Edit: I just discovered that I can replace <img> with a <canvas> in fancybox without loosing the zoom and drag functionality. So in theory it would be possible to use canvas paths and isPointInPath() methode. Unfortunately I need more than one path, which requires the Path2D object, which is not available in IE...
Since all options discussed in the question turned out to be not feasible and I found the pnpoly point in polygon algorithm, I did the whole thing on my own. I put the coordinates as percentages (in order to be size-independent) in an array of javascript objects like so:
var maps = {
alpen : [
{type:'poly',name:'Finsteraarhorn (4274m)',vertx:[56.48,56.08,56.06,56.46], verty:[28.5,28.75,40.25,40.25]},
{type:'rect',name:'Fiescherhörner (4049m)',coords:[58.08,29.5,59.26,43.5]},
{type:'poly',name:'Eiger (3970m)',vertx:[61.95,61.31,61.31,60.5,60.5], verty:[43,35.25,30.25,30.25,45.5]}
]
}; // maps
Since the pnpoly function requires the vertices for x and y separately I provide the coordinates this way already.
The Id of the map is stored in a data attribute in the source link:
<a href="/img/bilder/Alpen.jpg" data-type='image' data-Id='alpen' data-fancybox="img" data-caption="<h5>panorama of the alps from the black forest Belchen at sunset</h5>">
<img src="/_pano/bilder/Alpen.jpg">
</a>
CSS for the tooltip:
.my-tooltip {
color: #ccc;
background: rgba(30,30,30,.6);
position: absolute;
padding: 5px;
text-align: left;
border-radius: 5px;
font-size: 12px;
}
pnpoly and pnrect are provided as simple functions, the handling of that all is done in the afterShow event handler:
// PNPoly algorithm checkes whether point in polygon
function pnpoly(vertx, verty, testx, testy) {
var i, j, c = false;
var nvert = vertx.length;
for(i=0, j=nvert-1; i<nvert; j=i++) {
if (((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
c = !c;
}
}
return c;
}
// checks whether point in rectangle
function pnrect(coords,testx,testy) {
return ((testx >= coords[0]) && (testx <= coords[2]) && (testy >= coords[1]) && (testy <= coords[3]));
}
$("[data-fancybox]").fancybox({
afterShow: function( instance, slide ) {
var map = maps[$(slide.opts.\$orig).data('id')]; // Get map name from source link data-ID
if (map && map.length) { // if map present
$(".fancybox-image")
.after("<span class='my-tooltip' style='display: none'></span>") // append tooltip after image
.mousemove(function(event) { // create mousemove event handler
var offset = $(this).offset(); // get image offset, since mouse coords are global
var perX = ((event.pageX - offset.left)*100)/$(this).width(); // calculate mouse coords in image as percentages
var perY = ((event.pageY - offset.top)*100)/$(this).height();
var found = false;
var i;
for (i = 0; i < map.length; i++) { // loop over map entries
if (found = (map[i].type == 'poly') // depending on area type
?pnpoly(map[i].vertx, map[i].verty, perX, perY) // look whether coords are in polygon
:pnrect(map[i].coords, perX, perY)) // or coords are in rectangle
break; // if found stop looping
} // for (i = 0; i < map.length; i++)
if (found) {
$(".my-tooltip")
.css({bottom: 'calc(15px + '+ (100 - perY) + '%'}) // tooltip 15px above mouse coursor
.css((perX < 50) // depending on which side we are
?{right:'', left: perX + '%'} // tooltip left of mouse cursor
:{right: (100 - perX) + '%', left:''}) // or tooltip right of mouse cursor
.text(map[i].name) // set tooltip text
.show(); // show tooltip
} else {
$(".my-tooltip").hide(); // if nothing found: hide.
}
});
} else { // if (map && map.length) // if no map present
$(".fancybox-image").off('mousemove'); // remove event mousemove handler
$(".my-tooltip").remove(); // remove tooltip
} // else if (map && map.length)
} // function( instance, slide )
});
Things left to do: Find a solution for touch devices, f.e. provide a button to show all tooltips (probably rotated 90°).
As soon as the page is online I'll provide a link here to see it working...
I have just upgraded from fabric 1.7 to 2 and now the image object is behaving differently.See the screenshot, the image where the arrow is is completely ignoring the fact that i set a width on it, it looks like it's actually scaling it based on the given height to keep the image ratio. I don't want this to happen, the image needs to stretch to the size i tell it to.
Anyone have any idea to stop it doing this? I mean if i set a width in the options for the image object i expect it to respect those dimensions. It should be stretching to fill where the red box is.
This is happening when loading the image initially as a square and setting {width:1000,height:400} for example, but instead it looks like it's taking the height and scaling the width down to keep it square.
You need to set scaleX for width and scaleY for height. It's a breaking change for v2.
DEMO
var canvas = new fabric.Canvas('c');
var index = 0,
json;
var url = '//fabricjs.com/assets/pug.jpg';
fabric.Image.fromURL(url, function(img) {
var elWidth = img.naturalWidth || img.width;
var elHeight = img.naturalHeight || img.height;
img.set({
scaleX:200/elWidth,
scaleY:200/elHeight
})
canvas.add(img);
})
canvas{
border:2px solid #000;
}
<script type="text/javascript" src="
https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" width="400" height="400"></canvas>
I wish to apply an image filter on a fabric.Image object but I am unable to do so.
Here is my code:
(Approach: 1) Create an image and add to canvas
var canvas = new fabric.Canvas('canvas');
var img = document.querySelector('.images img.img_dragging');
var newImg = new fabric.Image(img, {
scaleX: .50,
scaleY:.50,
});
newImg.filters.push(new fabric.Image.filters.RemoveWhite({ threshold:10 ,distance: 100 }));
newImg.applyFilters(canvas.renderAll.bind(canvas));
canvas.add(newImg);
Html (Image src is just a random image from net, but our project images are somewhat similar)
<img draggable="true" src="http://cdn.homedit.com/wp-content/uploads/2011/01/blue-small-tufted-sofa.jpg" width="250" height="250"></img>
With above I am able to add the image to the canvas and maneuver the image on the canvas.
My requirement is to remove the extra white spaces from the background of the above image.
As per my reading over net we can implement the above like:
(Approach: 2):
fabric.Image.fromURL('http://cdn.homedit.com/wp-content/uploads/2011/01/blue-small-tufted-sofa.jpg', function(img){
img.filters.push(new fabric.Image.filters.RemoveWhite({ threshold:10 ,distance: 100 }));
img.applyFilters(canvas.renderAll.bind(canvas));
img.scale(0.5);
canvas.add(img);
});
So I wish to seek whether I can use filters with approach 1 and if there is a snippet on how to implement it.
Or filters can only we applied to fromURL approach only.
Any pointers would be a great help.
Cheers!
AJ
P.S : Apologies for any typos.