I try to find out the width of a text in a html canvas.
I'm using the measureText method but it gives me a value the is more than double as expected. I generate the Text by using the toSting method. If I just hard code the return from toSting into measureText it works fine...
console.log("--->width "+this.ctx.measureText("-100").width);
returns 22
console.log(labels[i]);
returns "-100"
console.log("->width "+this.ctx.measureText(labels[i]).width);
returns 48
Any ideas why?
thanks for help!
Here is the whole code: https://jsfiddle.net/falkinator/Lze1rnm5/4/
function createGraph(){
this.canvas = document.getElementById("graph");
this.ctx = this.canvas.getContext("2d");
this.options={};
this.datasets=[];
this.options.fontSize=11;
this.ctx.font=this.options.fontSize+"px Arial";
this.ctx.textAlign="left";
this.ctx.textBaseline="middle";
this.datasetLength=500;
this.dataset=function(options){
/*this.strokeColor=options.strokeColor;
this.signalName=options.signalName;
this.signalMin=options.signalMin;
this.signalMax=options.signalMax;
this.signalUnit=options.signalUnit;*/
this.data=[];
this.min;
this.max;
};
this.buildYLabels = function(scaleMin, scaleMax){
var labels = [];
var maxLabelWidth=0;
console.log("--->width "+this.ctx.measureText("-100").width);
for(i=10;i>=0;i--){
labels.push((scaleMin+((scaleMax-scaleMin)/10)*i).toString());
console.log((scaleMin+((scaleMax-scaleMin)/10)*i).toString());
console.log("->width "+this.ctx.measureText(labels[i]).width);
if(maxLabelWidth<this.ctx.measureText(labels[i]).width){
maxLabelWidth=this.ctx.measureText(labels[i]).width;
}
}
return {labels: labels,
maxLabelWidth: maxLabelWidth};
};
this.buildXLabels = function(x){
};
this.draw = function (){
var _this=this;
each(this.datasets,function(dataset, index){
//plot data
if(index>0)return;
//draw scale
var canvasHeight = _this.canvas.height;
console.log("canvas height="+canvasHeight);
var yLabels = _this.buildYLabels(-100, 500);
var currX = _this.options.fontSize/2;
var scaleHeight = canvasHeight-_this.options.fontSize*1.5;
var maxLabelWidth=yLabels.maxLabelWidth;
console.log(yLabels.maxLabelWidth);
each(yLabels.labels,function(label, index){
_this.ctx.fillText(label,0,currX);
console.log(label);
currX+=(scaleHeight/10);
});
_this.ctx.beginPath();
_this.ctx.moveTo(maxLabelWidth,0);
_this.ctx.lineTo(maxLabelWidth,canvasHeight);
_this.ctx.stroke();
});
};
this.addSignal = function(){
var dataset = new this.dataset({});
this.datasets.push(dataset);
};
this.pushData = function(data){
var _this=this;
if(data.length!=this.datasets.length){
console.error("the number of pushed data is diffrent to the number of datasets!");
return;
}
each(data,function(data, index){
_this.datasets[index].data.push(data);
if(_this.datasets[index].data.length>_this.datasetLength){
_this.datasets[index].data.shift();
}
});
};
this.calculateScaling = function(dataset){
var range = dataset.max - dataset.min;
var decStep = Math.pow(10,Math.floor(Math.log10(range)));
var scaleMin = roundTo(dataset.min/*+(decStep*10)/2*/, decStep);
var scaleMax = roundTo(dataset.max/*+(decStep*10)/2*/, decStep);
var scaleStep = (scaleMax - scaleMin)/10;
};
var minx=-34, maxx=424;
var range = maxx - minx;
var decStep = Math.pow(10, Math.floor(Math.log10(range)));
var scaleMin = roundTo(minx-(decStep/2), decStep);
var scaleMax = roundTo(maxx+(decStep/2), decStep);
var scaleStep = (scaleMax - scaleMin)/10;
console.log(this.buildYLabels(scaleMin,scaleMax));
console.log("range="+range);
console.log("log="+Math.floor(Math.log10(range)));
console.log("scaleStep="+scaleStep);
console.log("decStep="+decStep);
console.log("scaleMin="+scaleMin);
console.log("scaleMax="+scaleMax);
}
graph = new createGraph();
graph.addSignal();
graph.addSignal();
graph.addSignal();
graph.pushData([1,2,3]);
graph.pushData([1,2,3]);
graph.draw();
function each(array, callback){
console.log(callback);
for(i in array){
callback(array[i], i);
}
}
function roundTo(num, to){
return Math.round(num/to)*to;
}
<canvas id="graph"></canvas>
In your code that generates the labels, the loop index i is going from 10 to 0. Each time though the loop you are pushing a new label to the array (i.e. labels[0], labels[1], ...) but you are attempting to measure the labels using the loop index i (i.e. labels[10], labels[9], ...). Thus, the first few measurments are of the text "undefined".
Change...
console.log("->width "+this.ctx.measureText(labels[i]).width);
if(maxLabelWidth<this.ctx.measureText(labels[i]).width){
maxLabelWidth=this.ctx.measureText(labels[i]).width;
to...
console.log("->width "+this.ctx.measureText(labels[labels.length-1]).width);
if(maxLabelWidth<this.ctx.measureText(labels[labels.length-1]).width){
maxLabelWidth=this.ctx.measureText(labels[labels.length-1]).width;
Related
currelty working on a small matching puzzle game. Currently it displays text for the time remaining and the number of tiles that have been macthes. I'm trying to create on for best completion time as well (How fast the person finishes the game) however have been running into a lot of problems. for two variables that I created "txt" and "matchesFoundText" when I type them the autocomplete box will popup and ".text" is displayed as one of the options so i would get something like "txt.text. however I'm not getting that option at all for "bestTimeTxt" and I have no idea as to why this is happening. I did all three variables the same way so I'm at a loss as to why Text is not available to select from for "bestTimeTxt". Here is my entire script.
<!DOCTYPE html>
<html>
<head>
<title>Recipe: Drawing a square</title>
<script src="easel.js"></script>
<script type="text/javascript">
var canvas;
var stage;
var squareSide = 70;
var squareOutline = 5;
var max_rgb_color_value = 255;
var gray = Graphics.getRGB(20, 20, 20);
var placementArray = [];
var tileClicked;
var timeAllowable;
var totalMatchesPossible;
var matchesFound;
var txt;
var bestTime;
var bestTimeTxt;
var matchesFoundText;
var squares;
function init() {
var rows = 5;
var columns = 6;
var squarePadding = 10;
canvas = document.getElementById('myCanvas');
stage = new Stage(canvas);
var numberOfTiles = rows*columns;
matchesFound = 0;
timeAllowable = 5;
bestTime = 0;
txt = new Text(timeAllowable, "30px Monospace", "#000");
txt.textBaseline = "top"; // draw text relative to the top of the em box.
txt.x = 500;
txt.y = 0;
bestTimeTxt = new Text(bestTime, "30px Monospace", "#000");
bestTimeTxt.textBaseLine = "top";
bestTimeTxt.x = 500;
bestTimeTxt.y = 80;
stage.addChild(txt);
stage.addChild(bestTimeTxt);
squares = [];
totalMatchesPossible = numberOfTiles/2;
Ticker.init();
Ticker.addListener(window);
Ticker.setPaused(false);
matchesFoundText = new Text("Pairs Found: "+matchesFound+"/"+totalMatchesPossible, "30px Monospace", "#000");
matchesFoundText.textBaseline = "top"; // draw text relative to the top of the em box.
matchesFoundText.x = 500;
matchesFoundText.y = 40;
stage.addChild(matchesFoundText);
setPlacementArray(numberOfTiles);
for(var i=0;i<numberOfTiles;i++){
var placement = getRandomPlacement(placementArray);
if (i % 2 === 0){
var color = randomColor();
}
var square = drawSquare(gray);
square.color = color;
square.x = (squareSide+squarePadding) * (placement % columns);
square.y = (squareSide+squarePadding) * Math.floor(placement / columns);
squares.push(square);
stage.addChild(square);
square.cache(0, 0, squareSide + squarePadding, squareSide + squarePadding);
square.onPress = handleOnPress;
stage.update();
};
}
function drawSquare(color) {
var shape = new Shape();
var graphics = shape.graphics;
graphics.setStrokeStyle(squareOutline);
graphics.beginStroke(gray);
graphics.beginFill(color);
graphics.rect(squareOutline, squareOutline, squareSide, squareSide);
return shape;
}
function randomColor(){
var color = Math.floor(Math.random()*255);
var color2 = Math.floor(Math.random()*255);
var color3 = Math.floor(Math.random()*255);
return Graphics.getRGB(color, color2, color3)
}
function setPlacementArray(numberOfTiles){
for(var i = 0;i< numberOfTiles;i++){
placementArray.push(i);
}
}
function getRandomPlacement(placementArray){
randomNumber = Math.floor(Math.random()*placementArray.length);
return placementArray.splice(randomNumber, 1)[0];
}
function handleOnPress(event){
var tile = event.target;
tile.graphics.beginFill(tile.color).rect(squareOutline, squareOutline, squareSide, squareSide);
if(!!tileClicked === false || tileClicked === tile){
tileClicked = tile;
tileClicked.updateCache("source-overlay");
}else{
if(tileClicked.color === tile.color && tileClicked !== tile){
tileClicked.visible = false;
tile.visible = false;
matchesFound++;
matchesFoundText.text = "Pairs Found: "+matchesFound+"/"+totalMatchesPossible;
if (matchesFound===totalMatchesPossible){
gameOver(true);
}
}else{
tileClicked.graphics.beginFill(gray).rect(squareOutline, squareOutline, squareSide, squareSide);
}
tileClicked.updateCache("source-overlay");
tile.updateCache("source-overlay");
tileClicked = tile;
}
stage.update();
}
function tick() {
secondsLeft = Math.floor((timeAllowable-Ticker.getTime()/1000));
txt.text = secondsLeft;
;
if (secondsLeft <= 0){
gameOver(false);
}
stage.update();
}
function gameOver(win){
Ticker.setPaused(true);
for(var i=0;i<squares.length;i++){
squares[i].graphics.beginFill(squares[i].color).rect(5, 5, 70, 70);
squares[i].onPress = null;
if (win === false){
squares[i].uncache();
}
}
var replayParagraph = document.getElementById("replay");
replayParagraph.innerHTML = "<a href='#' onClick='history.go(0);'>Play Again?</a>";
if (win === true){
matchesFoundText.text = "You win!"
}else{
txt.text = secondsLeft + "... Game Over";
}
}
function replay(){
init();
}
</script>
</head>
<body onload="init()">
<header id="header">
<p id="replay"></p>
</header>
<canvas id="myCanvas" width="960" height="400"></canvas>
</body>
</html>
Okay so apparently the reason why I was having issues is because the x and y positions of the bestTimeTxt variable was causing it to be obscured by the position of everything else.
My code looks like this
var FamousEngine = require('famous/core/FamousEngine');
var DOMElement = require('famous/dom-renderables/DOMElement');
var Position = require('famous/components/Position');
var Transitionable = require('famous/transitions/Transitionable');
var Size = require('famous/components/Size')
var Tile = require('./Tile.js');
var Card = require('./Card.js');
var Selector = require('./Selector.js');
var ChannelCard = require('./ChannelCard.js');
var PanelCard = require('./PanelCard.js');
var KeyCodes = require('../utils/KeyCodes.js');
function App(selector, data) {
this.context = FamousEngine.createScene(selector);
this.rootNode = this.context.addChild();
this.tilesData = data.tilesData;
this.selectedTileIndex = 0;
this.tiles = [];
var firstNode = this.rootNode.addChild(new Card('url(images/tile-01-vidaa.png)','#9a105f', FIRST_X_POSITION, Y_POSITION));
}.bind(this));
module.exports = App;
where Card.js looks like this :
var DOMElement = require('famous/dom-renderables/DOMElement');
var FamousEngine = require('famous/core/FamousEngine');
var Transitionable = require('famous/transitions/Transitionable');
var Node = require('famous/core/Node');
var FIRST_X_POSITION = 100;
var Y_POSITION = 200;
function Card(bgUrl, bgColor, xpos, ypos) {
Node.call(this);
console.log("xpos + " + xpos);
console.log("ypos + " + ypos);
this.setSizeMode('absolute', 'absolute');
this.setAbsoluteSize(250, 250);
this.setPosition(xpos, ypos);
this.nodeDomElement = new DOMElement(this);
this.nodeDomElement.setProperty('backgroundImage', bgUrl);
this.nodeDomElement.setProperty('background-color', bgColor);
}
Card.prototype = Object.create(Node.prototype);
Card.prototype.constructor = Card;
module.exports = Card;
However, when I run in using famous dev, I get the following error
Uncaught Error: This node does not have access to a size component
setSizeMode # Node.js:1061Card # Card.js:11App # App.js:43
Please help me figure out what is causing the error and how to fix.
Your Node needs a context. If you have not yet added the node to a scene (or there is not yet a scene), add it before running any methods on the components. In the example of my 3D Asteroids game, I had to do the following to avoid the error:
function Game(scene, world) {
Node.call(this);
scene.addChild(this);
this._domElement = new DOMElement(this);
this.world = world;
this.asteroids = [];
this.bullets = [];
this.setProportionalSize(1, 1, 1);
this.addUIEvent('click');
this.addUIEvent('keydown');
}
If you have not yet created a scene, you can do so by running:
function Game() {
// ...
var context = FamousEngine.getContext('body');
context.addChild(this);
}
I believe the issue was a bug in Famous Engine and there were 2 manifestations of it.
The first manifestation was fixed by the following PR :
https://github.com/Famous/engine/pull/349
The second manifestation was fixed by the following PR :
https://github.com/Famous/engine/pull/350
However, at the time of writing this, these have not been merged to master so one needs to work off develop branch to have these fixes.
I have a line chart and every time the page refresh it changes the data, which is great but I need to to refresh by a user click. This is because there will eventually be other input fields on the page and refreshing the page would destroy their current session.
jsfiddle - http://jsfiddle.net/darcyvoutt/dXtv2/
Here is the code setup to create the line:
function economyData() {
// Rounds
var numRounds = 10;
// Stability of economy
var stable = 0.2;
var unstable = 0.6;
var stability = unstable;
// Type of economy
var boom = 0.02;
var flat = 0;
var poor = -0.02;
var economyTrend = boom;
// Range
var start = 1;
var max = start + stability;
var min = start - stability;
// Arrays
var baseLine = [];
var economy = [];
// Loop
for (var i = 0; i < numRounds + 1; i++) {
baseLine.push({x: i, y: 1});
if (i == 0) {
economyValue = 1;
} else {
var curve = Math.min(Math.max( start + ((Math.random() - 0.5) * stability), min), max);
economyValue = Math.round( ((1 + (economyTrend * i)) * curve) * 100) / 100;
}
economy.push({x: i, y: economyValue});
}
return [
{
key: 'Base Line',
values: baseLine
},
{
key: 'Economy',
values: economy
}
];
}
Here is what I tried to write but failed for updating:
function update() {
sel = svg.selectAll(".nv-line")
.datum(data);
sel
.exit()
.remove();
sel
.enter()
.append('path')
.attr('class','.nv-line');
sel
.transition().duration(1000);
};
d3.select("#update").on("click", data);
Here is what I did differently with your code.
// Maintian an instance of the chart
var chart;
// Maintain an Instance of the SVG selection with its data
var chartData;
nv.addGraph(function() {
chart = nv.models.lineChart().margin({
top : 5,
right : 10,
bottom : 38,
left : 10
}).color(["lightgrey", "rgba(242,94,34,0.58)"])
.useInteractiveGuideline(false)
.transitionDuration(350)
.showLegend(true).showYAxis(false)
.showXAxis(true).forceY([0.4, 1.6]);
chart.xAxis.tickFormat(d3.format('d')).axisLabel("Rounds");
chart.yAxis.tickFormat(d3.format('0.1f'));
var data = economyData();
// Assign the SVG selction
chartData = d3.select('#economyChart svg').datum(data);
chartData.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
Here's how the update() function looks like:
function update() {
var data = economyData();
// Update the SVG with the new data and call chart
chartData.datum(data).transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
};
// Update the CHART
d3.select("#update").on("click", update);
Here is a link to a working version of your code.
Hope it helps.
I wrote a little test program, exploring my problem, and it works the way I expect it, printing "four, five, six".
var foo = function() {
console.log("just one more test")
var destination = new Array;
data = [ "four", "five" , "six" ]
var parent = [ 0, 1, 2 ];
var something;
parent.forEach(function(element) {
something = data[element];
destination.push(something);
})
destination.forEach(function (thing) {
console.log(thing);
})
}
foo();
But in my real code, when I push things on to my 'clickable' array, all of the entries mutate into the current value of the 'clicker' variable. There are 3 entries in my 'scheme_list', and they draw correctly, but as I try to build areas to click on, at each iteration of the loop "in the loop" prints the value just pushed on to the array for every instance. That is, the instances of 'clicker' already in the loop change to the current value of clicker. I'm scratching my head trying to understand the difference between my real code and my test code. Or, more to the point, how to fix my real code.
var layout_color_schemes = function(scheme_list) {
var canvas = document.getElementById('color_picker_canvas');
var ctx = canvas.getContext('2d');
var x_upper_left = 5;
var y_upper_left = 5;
var width = 200;
var height = 30;
var clickables = new Array;
var clicker = {};
clicker.rect = {};
clicker.rect.width = width;
clicker.rect.height = height;
scheme_list.forEach(function(row) {
var grad = ctx.createLinearGradient(0, 0, 100, 0);
var cscheme = jQuery.parseJSON(row.json);
cscheme.forEach(function(color_point) {
grad.addColorStop(color_point[0], 'rgb(' + color_point[1] + ','
+ color_point[2] + ',' + color_point[3] + ')');
})
ctx.fillStyle = grad;
ctx.fillRect(x_upper_left, y_upper_left, width, height);
clicker.rect.x = x_upper_left;
clicker.rect.y = y_upper_left;
clicker.scheme = cscheme;
clicker.name = row.name;
clickables.push(clicker);
printf("clickables size = %d", clickables.length)
for (index = 0; index < clickables.length; ++index) {
printf("Index is %d", index)
printf("in the loop %j", clickables[index])
}
ctx.fillStyle = 'black'
ctx.fillText(row.name, x_upper_left + width + 10, 5 + y_upper_left
+ (height / 2));
y_upper_left = y_upper_left + height + 10;
});
clickables.forEach(function(area) {
printf("before call clickable area = %j", area)
});
wire_clickables(clickables);
};
function wire_clickables(clickables) {
clickables.forEach(function(area) {
if (area.hasOwnProperty('rect')) {
printf("wiring a clickable %j", area);
$("#color_picker_canvas").mousemove(function(e) {
var inside = false;
var offsetX = e.pageX - $(this).position().left;
var offsetY = e.pageY - $(this).position().top;
if (area && contains(area.rect, offsetX, offsetY)) {
document.body.style.cursor = 'pointer'
}
else {
document.body.style.cursor = 'default'
}
})
}
})
}
function contains(rect, x, y) {
return (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y
+ rect.height)
}
The clicker object exists outside of the loop, so all you are doing is pushing the same reference onto clickables. Instead, you want to push a copy of it. There are many ways to do this, but in your case this may be one of the simplest ways:
Replace
clickables.push(clicker);
with
clickables.push(JSON.parse(JSON.stringify(clicker));
Alternatively, move the clicker declaration and initialization inside the loop.
I am trying to add something to my code so as to select the image after drawing ( the selection handler should appear in the corners and in the middle of edge) and then drag or increase/decrease the height and width?
My sample code is in the fiddle , in this I am drawing a rectangle using the mouse event handlers. I want to select the rectangle and modify/alter it using the selection handlers instead of drawing it again.
Click the button ROI, metrics and then you can draw it using the mouse events.
http://jsfiddle.net/AhdJr/53/
var oImageBuffer = document.createElement('img');
var oCanvas=document.getElementById("SetupImageCanvas");
var o2DContext=oCanvas.getContext("2d");
var oRect = {};
var oROI = {};
var oMetrics ={};
var oLayers = new Array();
var bDragging = false;
var bSetROI = false;
var bSetLayers = false;
InitMouseEvents();
var oSelect = document.getElementById("ImageList");
oSelect.onchange=function() {
changeCanvasImage(oSelect[oSelect.selectedIndex].value);
}
// Canvas event handlers (listeners).
function InitMouseEvents() {
oCanvas.addEventListener('mousedown', MouseDownEvent, false);
oCanvas.addEventListener('mouseup', MouseUpEvent, false);
oCanvas.addEventListener('mousemove', MouseMoveEvent, false);
oCanvas.addEventListener('mouseout', MouseOutEvent, false);
}
function MouseDownEvent(e) {
oRect.startX = e.pageX - this.offsetLeft;
oRect.startY = e.pageY - this.offsetTop;
bDragging = true;
}
function MouseUpEvent() {
bDragging = false;
}
function MouseOutEvent() {
document.getElementById("MouseCoords").innerHTML="";
}
function MouseMoveEvent(e) {
if (bDragging) {
oRect.w = (e.pageX - this.offsetLeft) - oRect.startX;
oRect.h = (e.pageY - this.offsetTop) - oRect.startY;
oCanvas.getContext('2d').clearRect(0,0,oCanvas.width, oCanvas.height);
var oROI = document.getElementById("btnROI");
if (oROI.checked) {
SetROI();
}
var oLayer = document.getElementById("btnLAYER");
if (oLayer.checked) {
SetLayer();
}
var oMetrics = document.getElementById("btnMetrics");
if (oMetrics.checked) {
SetMetrics();
}
}
if (bSetROI) {
DrawROI();
}
if (bSetLayers) {
DrawLayers();
}
if(bSetMetrics){
DrawMetrics();
}
// Display the current mouse coordinates.
ShowCoordinates(e);
}
function ShowCoordinates(e) {
x=e.clientX;
y=e.clientY;
document.getElementById("MouseCoords").innerHTML="(" + x + "," + y + ") " + document.getElementById('txtPatchCount').value;
}
// Interactively draw ROI rectangle(s) on the canvas.
function SetROI() {
bSetROI = true;
oROI.startX = oRect.startX;
oROI.startY = oRect.startY;
oROI.w = oRect.w;
oROI.h = oRect.h;
}
function DrawROI() {
o2DContext.lineWidth=1.5;
o2DContext.strokeStyle = '#0F0';
o2DContext.strokeRect(oROI.startX, oROI.startY, oROI.w, oROI.h);
var iPatches = document.getElementById('txtPatchCount').value;
o2DContext.beginPath();
var iTop = oROI.startY;
var iBottom = oROI.startY + oROI.h;
var iLeft = oROI.startX;
var iX = iLeft;
for (var iPatch=1; iPatch<iPatches; ++iPatch) {
iX = iLeft + iPatch*oROI.w/iPatches;
o2DContext.moveTo(iX, iTop);
o2DContext.lineTo(iX, iBottom);
}
o2DContext.lineWidth=0.25;
o2DContext.stroke();
}
function SetMetrics() {
bSetMetrics = true;
oMetrics.startX = oRect.startX;
oMetrics.startY = oRect.startY;
oMetrics.w = oRect.w;
oMetrics.h = oRect.h;
}
function DrawMetrics(){
o2DContext.strokeStyle = 'black';
o2DContext.strokeRect(oMetrics.startX, oMetrics.startY, oMetrics.w, oMetrics.h);
o2DContext.beginPath();
var iTop = oMetrics.startY;
var iBottom = oMetrics.startY + oMetrics.h;
var iLeft = oMetrics.startX;
var iX = iLeft;
o2DContext.moveTo(iX, iTop);
o2DContext.lineTo(iX, iBottom);
o2DContext.stroke();
}
// Interactively draw layer boundaries on the canvas.
function SetLayer() {
bSetLayers = true;
oLayers.length = 0;
oLayers.push(oRect.startY);
oLayers.push(oRect.startY + oRect.h);
}
function DrawLayers() {
o2DContext.lineWidth=0.25;
o2DContext.strokeStyle = '#F00';
o2DContext.beginPath();
var iY = oLayers[0];
var iLeft = 0;
var iRight = oCanvas.width;
for (var iLayer=0; iLayer<oLayers.length; ++iLayer) {
iY = oLayers[iLayer];
o2DContext.moveTo(iLeft, iY);
o2DContext.lineTo(iRight, iY);
o2DContext.stroke();
}
}
The below blog is doing the same thing but I am not sure how to add this functionality in my code.
http://simonsarris.com/blog/225-canvas-selecting-resizing-shape
Please guide me how to add the same in mine.
Really appreciate the help.
try the following link.
It is doing somewhat you want to achieve.
http://simonsarris.com/blog/225-canvas-selecting-resizing-shape