I have a Pie chart with lots of elements.
I've left the color picking to google charts, but after the chart is drawn, I need to retrieve the color of each row.
I know that I could generate the colors array, and set it in the charts options, and that would solve my problem, I would be able to retrieve the colors from there. But I really don't want to create that big array by hand.
So is there a way to get the color for a given row?
Thanks!
I'm not familiar with Google pie charts, but I have a basic JavaScript suggestion for you. Maybe this helps. If you have a canvas image you could use a JavaScript pixel picking method something like the following I use for mine:
//get the image
var sampleImage = document.getElementById("checkimg");
//convert the image to canvas
function convertImageToCanvas(image) {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
}
var canvas = convertImageToCanvas(sampleImage);
sampleImage.style.display = 'none';
//real position functions
function absleft(el) {
return (el.offsetParent) ? el.offsetLeft+absleft(el.offsetParent) : el.offsetLeft;
}
function abstop(el) {
return (el.offsetParent) ? el.offsetTop+abstop(el.offsetParent) : el.offsetTop;
}
canvas.onclick = function(event) {
//get real position
if (event.hasOwnProperty('offsetX')) {
var x = event.offsetX;
var y = event.offsetY;
}
else {
var x = event.layerX - absleft(canvas);
var y = event.layerY - abstop(canvas);
}
//get the pixel data
var pixelData = this.getContext('2d').getImageData(x, y, 1, 1).data;
}
document.getElementById("temp").appendChild(canvas);
Related
I am working on ionic based application.
I want to functionality like user has Filled the red color on image(canvas) with finger. So I have done the filled functionality but I want to crop the Filled portion from canvas. I have attached one image for reference.
I want to crop red portion from above image. I has googling but not found any solution.
Creating a image mask.
If you are rendering the selection area (red) then the solution is simple.
Create a second canvas the same size as the drawing, and don't add it to the DOM. Draw the red marking content onto that canvas
The on the display canvas render the image first and then render that marking
canvas over the image with composite mode like "overlay" so that the original image can be seen and the marked areas are red.
Now you have two layers, one is the image and the other the mask you can use to get a copy of the marked content.
To do that create a 3rd canvas, draw the original image onto it, then set the composite mode to "destination-in". Then draw the mask over it. Only the marked pixels will remain.
See the example for more details
setTimeout(example,0); // ensures that the run us after parsing
function example(){
const ctx = canvas.getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var selectLayer = CImageCtx(w,h); // creates a canvas
var selectedContent = CImageCtx(w,h); // the selected content
document.body.appendChild(selectedContent);
var image = new Image; // the image
image.src = " https://i.stack.imgur.com/QhFct.png";
// updates the masked result
function updateSelected(){
var ctx = selectedContent.ctx;
ctx.drawImage(image,0,0);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(selectLayer,0,0);
ctx.globalCompositeOperation = "source-over";
}
function update(){
// if mouse down then
if(mouse.but){
// clear the mask if on the right image
if(mouse.oldBut === false && mouse.x > 256){
selectLayer.ctx.clearRect(0,0,w,h);
mouse.but = false;
}else{
// draw the red
selectLayer.ctx.fillStyle = "red";
fillCircle(mouse.x, mouse.y, 20, selectLayer.ctx);
}
// update the masked result
updateSelected();
}
// clear the canvas
ctx.clearRect(0,0,w,h);
// draw the image
ctx.drawImage(image,0,0);
// then draw the marking layer over it with comp overlay
ctx.globalCompositeOperation = "overlay";
ctx.drawImage(selectLayer,0,0);
ctx.globalCompositeOperation = "source-over";
mouse.oldBut = mouse.but;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
//#############################################################################
// helper functions not part of the answer
//#############################################################################
const mouse = {
x : 0, y : 0, but : false,
events(e){
const m = mouse;
const bounds = canvas.getBoundingClientRect();
m.x = e.pageX - bounds.left - scrollX;
m.y = e.pageY - bounds.top - scrollY;
m.but = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.but;
}
};
(["down","up","move"]).forEach(name => document.addEventListener("mouse" + name,mouse.events));
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
const fillCircle = (l,y=ctx,r=ctx,c=ctx) =>{if(l.p1){c=y; r=leng(l);y=l.p1.y;l=l.p1.x }else if(l.x){c=r;r=y;y=l.y;l=l.x}c.beginPath(); c.arc(l,y,r,0,Math.PI*2); c.fill()}
body { font-family : arial; }
canvas { border : 2px solid black; }
Draw on image and the selected parts are shown on the right<br>
Click right image to reset selection<br>
<canvas id="canvas" width=256 height=256></canvas>
Already masked.
If the red mask is already applied to the image then there is not much you can do apart from do a threshold filter depending on how red the image is. But even then you are going to have problems with darker areas, and areas that already contain red.
Unless you have the original image you will have poor results.
If you have the original image then you will have to access the image data and create a new image as a mask by comparing each pixel and selecting only pixels that are different. That will force you to same domain images only (or with CORS cross origin headers)
Website: http://minimedit.com/
Currently implementing an eye dropper. It works fine in my normal resolution of 1080p, but when testing on a higher or lower resolution it doesn't work.
This is the basics of the code:
var ctx = canvas.getContext("2d");
canvas.on('mouse:down', function(e) {
var newColor = dropColor(e, ctx);
}
function dropColor(e, ctx) {
var mouse = canvas.getPointer(e.e),
x = parseInt(mouse.x),
y = parseInt(mouse.y),
px = ctx.getImageData(x, y, 1, 1).data;
return rgb2hex('rgba('+px+')');
}
When I first initiate the canvas I have it resize to fit resolution:
setResolution(16/9);
function setResolution(ratio) {
var conWidth = ($(".c-container").css('width')).replace(/\D/g,'');
var conHeight = ($(".c-container").css('height')).replace(/\D/g,'');
var tempWidth = 0;
var tempHeight = 0;
tempHeight = conWidth / ratio;
tempWidth = conHeight * ratio;
if (tempHeight > conHeight) {
canvas.setWidth(tempWidth);
canvas.setHeight(conHeight);
} else {
canvas.setWidth(conWidth);
canvas.setHeight(tempHeight);
}
}
The x and y mouse coordinates work fine when zoomed in, but they don't line up with the returned image data. It seems as though the ctx isn't changing it's width and height and scaling along with the actual canvas size.
The canvas element is showing the correct width and height before using getContext as well.
Any ideas on a solution?
Feel free to check out the full scripts on the live website at: http://minimedit.com/
Try "fabric.devicePixelRatio" for calculating actual position, for example:
x = parseInt(mouse.x) * fabric.devicePixelRatio
I have multiple canvas elements layered on top of each other using absolute positioning. Each canvas has some transparent space within it. When I click on this stack of canvases, I get the pixel value where the click event happened and determine if that pixel is transparent like so:
var context = this.element.getContext('2d');
var rect = this.element.getBoundingClientRect();
var x = Math.round(event.clientX - rect.left);
var y = Math.round(event.clientY - rect.top);
var pixelData = context.getImageData(x, y, 1, 1).data;
if (pixelData[3] === 0) {
console.log('Transparent!')
}
When the pixel is transparent, I'd like to treat this layer as though it had the css property pointer-event: none;, which would cause the click event to pass through to the next layer.
Is there a way to accomplish this?
If you have an array of the canvas elements, you can just click the canvas below the current canvas. Like so:
var canvasElements = new Array();
canvasElements[0] = canvas.element.getContext('2d');
canvasElements[1] = canvas2.element.getContext('2d');
canvasElements[2] = canvas3.element.getContext('2d');
Then when you run your test:
var context = this.element.getContext('2d');
var rect = this.element.getBoundingClientRect();
var x = Math.round(event.clientX - rect.left);
var y = Math.round(event.clientY - rect.top);
for(var i = 0; i < canvasElements.length; i++){
var pixelData = canvasElements[1].getImageData(x, y, 1, 1).data;
if (pixelData[3] === 0) {
console.log('Transparent!');
} else {
break;
//clicked canvas i
}
}
I need to have a couple of images on one canvas.
I have a trouble with a function clear(); which is used when I need to drag images on canvas.
The problem is that we are Canvas appears only on the last image.
I try to use context.save() and context.restore() but it was not usefull in my case.
switch(i)
{
case 0:
challengerImg = new Image();
challengerImg.onload = function(){
drawImage(this,x,y,i);
};
challengerImg.src = "<?php echo $base_url; ?>/themes/bartik/images/sheep.png";
break;
case 1:
tshirt = new Image();
tshirt.onload = function(){
drawImage(this,x,y,i);
};
tshirt.src = "<?php echo $base_url; ?>/themes/bartik/images/tshirt.png";
break;
}
And function which draw on canvas:
function drawImage(challengerImg,x,y,i){
console.log("Function drawImage start");
var events = new Events("layer0");
var canvas = events.getCanvas();
var context = events.getContext();
var rectX = x;
var rectY = y;
var draggingRect = false;
var draggingRectOffsetX = 0;
var draggingRectOffsetY = 0;
events.setStage(function(){
var mousePos = this.getMousePos();
if (draggingRect) {
rectX = mousePos.x - draggingRectOffsetX;
rectY = mousePos.y - draggingRectOffsetY;
}
this.clear(); //Here is trouble
this.beginRegion();
context.drawImage(challengerImg, rectX, rectY, challengerImg.width, challengerImg.height);
// draw rectangular region for image
context.beginPath();
context.rect(rectX, rectY, challengerImg.width, challengerImg.height);
context.closePath();
this.addRegionEventListener("mousedown", function(){
draggingRect = true;
var mousePos = events.getMousePos();
draggingRectOffsetX = mousePos.x - rectX;
draggingRectOffsetY = mousePos.y - rectY;
});
this.addRegionEventListener("mouseup", function(){
draggingRect = false;
});
this.addRegionEventListener("mouseover", function(){
document.body.style.cursor = "pointer";
});
this.addRegionEventListener("mouseout", function(){
document.body.style.cursor = "default";
});
this.closeRegion();
});
}
context.save and context.restore only works for state of the context (transformation, globalAlpha,...), but not for what is rendered inside.
When you clear your context, it makes it empty.
What you have to do is to :
catch mouse events and change position variables
clear the canvas
redraw all images at their new position
save(), restore() do not handle bitmap data at all :).
To drag stuff around, basically you need to have an offscreen canvas which doesn't contain the element you are drawing, and after each update draw the offscreen canvas plus the element being dragged.
It's probably simpler to use a library that already does it, most do, like http://www.html5canvastutorials.com/labs/html5-canvas-interactive-scatter-plot-with-20000-nodes-using-kineticjs/
The canvas is a flat object - once you draw an image onto it, the image has no more context as it renders as part of the canvas. You can either create an array of rectangle objects with their coordinates to keep track of what's already on the canvas or test the mouse position for the background color to determine whether there is an image or not. Then you can remove the item where the event happened by clearing the canvas and redrawing all the other items again.
Hope this helps!
I want to draw a grid of 10 x 10 squares on a HTML5 canvas with number 1-100 displayed on the squares. Clicking a square should call a JavaScript function with the square's number passed as a variable to the function.
First, I encourage you to read this answer to another question involving the HTML5 Canvas. You need to understand that there are no squares. In order to detect a click on a 'square', you would have to keep track of a mapping from each canvas coordinate to the square(s) that it logically contains, handle a single click event on the entire canvas, work out which square(s) you want to change, and then redraw the canvas with the changes you want.
Then—since you seem to have no objection to using a more appropriate technology—I encourage you to do this in either HTML (where each 'square' is something like a <div> that is absolutely-positioned and sized and colored using CSS), or SVG (using <rect> if you need the squares to be able to be rotated, or want to introduce other shapes).
HTML and SVG are both 'retained-mode' graphics mode systems, where drawing a shape 'retains' the concept of that shape. You can move the shape, change its colors, size, etc. and the computer will automatically redraw it for you. Moreover, and more importantly for your use case, you can (with both HTML and SVG):
function changeColor(evt){
var clickedOn = evt.target;
// for HTML
clickedOn.style.backgroundColor = '#f00';
// for SVG
clickedOn.setAttribute('fill','red');
}
mySquare.addEventListener('click',changeColor,false);
Edit: I've created a simple implementation in JavaScript and HTML: http://jsfiddle.net/6qkdP/2/
Here's the core code, in case JSFiddle is down:
function clickableGrid( rows, cols, callback ){
var i=0;
var grid = document.createElement('table');
grid.className = 'grid';
for (var r=0;r<rows;++r){
var tr = grid.appendChild(document.createElement('tr'));
for (var c=0;c<cols;++c){
var cell = tr.appendChild(document.createElement('td'));
cell.innerHTML = ++i;
cell.addEventListener('click',(function(el,r,c,i){
return function(){ callback(el,r,c,i); }
})(cell,r,c,i),false);
}
}
return grid;
}
EDIT: Using HTML elements rather than drawing these things on a canvas or using SVG is another option and quite possibly preferable.
Following up on Phrogz's suggestions, see here for an SVG implementation:
jsfiddle example
document.createSvg = function(tagName) {
var svgNS = "http://www.w3.org/2000/svg";
return this.createElementNS(svgNS, tagName);
};
var numberPerSide = 20;
var size = 10;
var pixelsPerSide = 400;
var grid = function(numberPerSide, size, pixelsPerSide, colors) {
var svg = document.createSvg("svg");
svg.setAttribute("width", pixelsPerSide);
svg.setAttribute("height", pixelsPerSide);
svg.setAttribute("viewBox", [0, 0, numberPerSide * size, numberPerSide * size].join(" "));
for(var i = 0; i < numberPerSide; i++) {
for(var j = 0; j < numberPerSide; j++) {
var color1 = colors[(i+j) % colors.length];
var color2 = colors[(i+j+1) % colors.length];
var g = document.createSvg("g");
g.setAttribute("transform", ["translate(", i*size, ",", j*size, ")"].join(""));
var number = numberPerSide * i + j;
var box = document.createSvg("rect");
box.setAttribute("width", size);
box.setAttribute("height", size);
box.setAttribute("fill", color1);
box.setAttribute("id", "b" + number);
g.appendChild(box);
var text = document.createSvg("text");
text.appendChild(document.createTextNode(i * numberPerSide + j));
text.setAttribute("fill", color2);
text.setAttribute("font-size", 6);
text.setAttribute("x", 0);
text.setAttribute("y", size/2);
text.setAttribute("id", "t" + number);
g.appendChild(text);
svg.appendChild(g);
}
}
svg.addEventListener(
"click",
function(e){
var id = e.target.id;
if(id)
alert(id.substring(1));
},
false);
return svg;
};
var container = document.getElementById("container");
container.appendChild(grid(5, 10, 200, ["red", "white"]));
container.appendChild(grid(3, 10, 200, ["white", "black", "yellow"]));
container.appendChild(grid(7, 10, 200, ["blue", "magenta", "cyan", "cornflowerblue"]));
container.appendChild(grid(2, 8, 200, ["turquoise", "gold"]));
As the accepted answer shows, doing this in HTML/CSS is easiest if this is all your design amounts to, but here's an example using canvas as an alternative for folks whose use case might make more sense in canvas (and to juxtapose against HTML/CSS).
The first step of the problem boils down to figuring out where in the canvas the user's mouse is, and that requires knowing the offset of the canvas element. This is the same as finding the mouse position in an element, so there's really nothing unique to canvas here in this respect. I'm using event.offsetX/Y to do this.
Drawing a grid on canvas amounts to a nested loop for rows and columns. Use a tileSize variable to control the step amount. Basic math lets you figure out which tile (coordinates and/or cell number) your mouse is in based on the width and height and row and column values. Use context.fill... methods to write text and draw squares. I've kept everything 0-indexed for sanity, but you can normalize this as a final step before display (don't mix 1-indexing in your logic, though).
Finally, add event listeners to the canvas element to detect mouse actions which will trigger re-computations of the mouse position and selected tile and re-renders of the canvas. I attached most of the logic to mousemove because it's easier to visualize, but the same code applies to click events if you choose.
Keep in mind that the below approach is not particularly performance-conscious; I only re-render when the cursor moves between cells, but partial re-drawing or moving an overlay to indicate the highlighted element would be faster (if available). There are a lot of micro-optimizations I've ignored. Consider this a proof-of-concept.
const drawGrid = (canvas, ctx, tileSize, highlightNum) => {
for (let y = 0; y < canvas.width / tileSize; y++) {
for (let x = 0; x < canvas.height / tileSize; x++) {
const parity = (x + y) % 2;
const tileNum = x + canvas.width / tileSize * y;
const xx = x * tileSize;
const yy = y * tileSize;
if (tileNum === highlightNum) {
ctx.fillStyle = "#f0f";
}
else {
ctx.fillStyle = parity ? "#555" : "#ddd";
}
ctx.fillRect(xx, yy, tileSize, tileSize);
ctx.fillStyle = parity ? "#fff" : "#000";
ctx.fillText(tileNum, xx, yy);
}
}
};
const size = 10;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 200;
const ctx = canvas.getContext("2d");
ctx.font = "11px courier";
ctx.textBaseline = "top";
const tileSize = canvas.width / size;
const status = document.createElement("pre");
let lastTile = -1;
drawGrid(canvas, ctx, tileSize);
document.body.style.display = "flex";
document.body.style.alignItems = "flex-start";
document.body.appendChild(canvas);
document.body.appendChild(status);
canvas.addEventListener("mousemove", evt => {
event.target.style.cursor = "pointer";
const tileX = ~~(evt.offsetX / tileSize);
const tileY = ~~(evt.offsetY / tileSize);
const tileNum = tileX + canvas.width / tileSize * tileY;
if (tileNum !== lastTile) {
lastTile = tileNum;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid(canvas, ctx, tileSize, tileNum);
}
status.innerText = ` mouse coords: {${evt.offsetX}, ${evt.offsetX}}
tile coords : {${tileX}, ${tileY}}
tile number : ${tileNum}`;
});
canvas.addEventListener("click", event => {
status.innerText += "\n [clicked]";
});
canvas.addEventListener("mouseout", event => {
drawGrid(canvas, ctx, tileSize);
status.innerText = "";
lastTile = -1;
});