Related
I'm trying to add a signature feature to a web app. Users should have the possibility to sign with a pencil on a touch screen device. I'm using HTML5 canvas, JavaScript and PHP to get things done. I found a script for both mouse and touch support.
The drawing on the canvas works fine. When I submit the form, the scribble will get cropped properly (so it seems). But the generated base64-png data (saved in a database) is an empty image.
Here's the JS code I'm using:
var canvas, ctx;
var mouseX, mouseY, mouseDown = 0;
var touchX, touchY;
var lastX, lastY = -1;
canvas = document.getElementById('scribble-canvas');
ctx = canvas.getContext('2d');
canvas.addEventListener('mousedown', scribbleMouseDown, false);
canvas.addEventListener('mousemove', scribbleMouseMove, false);
canvas.addEventListener('mouseup', scribbleMouseUp, false);
canvas.addEventListener('touchstart', scribbleTouchStart, false);
canvas.addEventListener('touchend', scribbleTouchEnd, false);
canvas.addEventListener('touchmove', scribbleTouchMove, false);
function drawLine(ctx, x, y, size) {
if (lastX == -1) {
lastX = x;
lastY = y;
}
ctx.strokeStyle = "#000000";
ctx.lineCap = "round";
ctx.lineWidth = size;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
lastX = x;
lastY = y;
}
function scribbleMouseDown() {
mouseDown = 1;
drawLine(ctx, mouseX, mouseY, 3);
}
function scribbleMouseUp() {
mouseDown = 0;
lastX = -1;
lastY = -1;
}
function scribbleMouseMove(e) {
getMousePos(e);
if (mouseDown == 1) {
drawLine(ctx, mouseX, mouseY, 3);
}
}
function getMousePos(e) {
if ( ! e) {
var e = event;
}
if (e.offsetX) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
else if (e.layerX) {
mouseX = e.layerX;
mouseY = e.layerY;
}
}
function scribbleTouchStart() {
getTouchPos();
drawLine(ctx, touchX, touchY, 3);
event.preventDefault();
}
function scribbleTouchEnd() {
lastX = -1;
lastY = -1;
}
function scribbleTouchMove(e) {
getTouchPos(e);
drawLine(ctx, touchX, touchY, 3);
event.preventDefault();
}
function getTouchPos(e) {
if ( ! e) {
var e = event;
}
if (e.touches) {
if (e.touches.length == 1) {
var touch = e.touches[0];
touchX = touch.pageX - touch.target.offsetLeft;
touchY = touch.pageY - touch.target.offsetTop;
}
}
}
function crop(canvas) {
var w = canvas.width, h = canvas.height;
var pix = {x:[], y:[]};
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index+3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a,b){return a-b});
pix.y.sort(function(a,b){return a-b});
var n = pix.x.length-1;
w = 1 + pix.x[n] - pix.x[0];
h = 1 + pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
document.body.style.opacity = 0;
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
return canvas.toDataURL();
}
On the PHP side I just pick the data returned by the crop() function and save it to the database.
What I tried so far is to strip off the touch support to see if it works just on mouse devices.
Additional code as suggested by user traktor:
The following jQuery code submits the form with the image data to the PHP script:
$(document).on('click', '#scribble-submit', function(e) {
e.preventDefault();
var btn = $(this);
var data = crop(document.querySelector('#scribble-canvas'));
$.post('/scribble/ax/ins_scribble', $('#form-scribble').serialize() + '&data=' + data, function(ans) {
window.location.href = '/' + btn.data('redirect');
});
});
The PHP part looks like this (using the CI3 framework):
$post = $this->input->post();
$this->db->where('id', $id);
$this->db->update('personen', ['signatur' => $post['data']]);
echo TRUE;
So far, jQuery and PHP works fine. The data is successfully inserted into the database. The LONGBLOB field contains this code:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAAC3CAYAAABNEPEEAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAxKADAAQAAAABAAAAtwAAAAB1wSLaAAAYVElEQVR4Ae2dCZQeVZXHmyUhIYQ1gBAgHSDsCAxbWNPsO4ZBBBkwERxQcEARUUdQdJyj6DjA0VE2YdhBhs2ASAhDwo4gy4GwBmggILIZgbAKM79/893O7er1W r7arn3nH /W reu/ef71Xb63qhdpCssbAMhg0CowBoyuh4iaDYeAeUI0szcnt4P9AJ/gbGEiGer6dN5/EZoG7wGvgzyC3slBuLe9tuArRSGAFSjdsCWA3bmv0RcFsMFih4JRuWQVNUIGaC14E1YpsWA8sAp4DSmtxsGQlXKoSynbFy 48yrsY/VfwPHgDqLKsAN4G/wVuAJmWVlUIFYC9we5AN78TJAvpROJUcPp7IlpBVwFSQV8ebABCssvAFzDtwuya19aWZoVYGceXBWPBumAXoEKsp8Z4sBoYDkL6Z AFfnoJPANWAu B/h4Q/NSn2IMjrS6TunXLgTXAYC2bWtf/BTeCaeBNkClpZIVYGM9EyilAXYyRoFVPbBWieeDlSvgB4afAh2A2SLZGRPUrjewyfUQufwQq6OpGqIvhQxV4HVsou/MievipQowB6goqVHnYAWwC1HXycg4Hvwb3 8hW6/VWiHE4sBFYC6wH9gbqugxVHuDE50AnSBbSicQN1mVS/ipUtwEVNPVbld7HICQ7DKgFOQkcmzDpKY6fANcCVZDcip4AVwENnlRo 8M7/Canp4PbwXXgSLAlEEkh5WFgPK7uB84CyfKibuAIkEuZjNV3AjX/Scfs Ap U8uhZjQkGPAMqFulnoTGE1ZeFP4eHAByI8OxdEdwA/COSL8UfA3sDDT9GRIMDMbAlzlBPYdXgZUnTR5MApmX1bDw6 A YMYrVK0OCQbqYeBfuNiXKc1CfgNktku9Lcb9DmiGxgx/EP1woBmYkGCgHgZW5eKDgR6uVr7sYauylzlJGipjJ2XOyjAo7wzshAO QkifAZbKkmMrYoymxczQu9HXz5KBYUuhGNgcbzQbaeVNocYai4GWywpYcDzwgx7NHIUEA2kysB2JPwx8pTgqzQyHmrb6b96oY4Z6YZwXDNTJwIZc/9/Ayp/WuzYGLZVvk7sZpMW1kGCgmQzsQGZW/hQ ALQG1hLR2EEbsMygE1piRWRadgb2hYCbgZXDa9C1s7mpYmOHl8nVDFm7qRZEZsHAAga iGrlUOFdQFP TZPPkpM3IMYOTaM MuqDAe2BOg68BqxcXt/HealFaQOeZawZppBgIAsM3IQRVi470XdvllHaeWgZq3KEBANZYOAIjHgMWNl8E33dtA0bSwbqo1mm/5R2hpF MFAFAxtwru86nVjFtTWd2sFVflv3hJpSiYuCgfQYUCWwB7YqhypJKjKCVH8ILDNt6AsJBrLGgLpJ6i5ZOX0cXd2phss4UtR dMtIc8AhwUAWGdCAuhNYWU3l4a1MLIMP0UOCgSwzoKlXK6 z0dsbYezCLpFxTo/ZJUdGqJlkQBXi7Ypl sDFXo228uckaDXuB41OPNILBlJg4GjStDL7CnpHvXn4FkI7DE003xsSDGSdAbUSeoFNsjzYvEtrwJ/9ScO/97BOA9KMJIKBZjDwTTKxVkIPcr22ULdcQAqW6Bz0JetOMRIIBprDQAfZ GlYleW65QZSsApxed2pRQLBQHMZuJDsrPyqcnTUmr2NIfQtUZN7TYkwGMgJA2dipxboJKNBzWMJqxCjupL65M9sp4caDOSBAX2Y4FxnaAd6uzsesqoKIWjPuUnyo8MWH2EwkGUG1LPRxJBkT7BXl1bDHw2g/aBkzRrSiEuCgSwwcDRG2FiipnUJtQ6LAfW7TGz1z44jDAbywsD1GFr3uoS6S1arFNq4Ii8khJ3BgGeg7nUJLcJZhXjDpxx6MJBDBjqw2Q8BLqjWh824wCrEC9VeHOcHAxlkoOZ1CXWPRjiHooVwZISaWwZqXpdQhRjl3J7v9FCDgbwyUPO6hCrESOf1PKeHGgzkmYF7Mf7VigNVrUvoyxo2hrgizwyE7cFAgoGjObayPaR1CbUQi7tE3nJ6qMFA3hmoel1CFWI95/USTg81GMg7A504MNM5cRj6gO9LqEKs5S5YxumhBgNFYEBjCev5aM1t6kBOqUIs4k7QR8pCgoEiMTATZ6Y5h7ZHX90d91BVIea6GK 76FCDgVwzcB7W/6Xigb5G2V7RewVRIXpREhEFZGAGPs1yfv2D03uoqhAhwUAZGNBYwmRzU5JhVIgkI3FcVAbud47pA8n6RmwviQrRi5KIKCgDnfh1R8U3LTX0Of0aFaLCUASFZ0Af8j7feTkFfXV33KVGhUgyEsdFZkCb/p6qOLgN4cSks1EhkozEcZEZ0Jf93nEOHuz0LjUqRJKROC46A36tbZWks1EhkozEcdEZuMQ5OAq9x2xTVAjHTqilYOBuvLTZpjXRe8w2RYUoRRkIJx0DA842RYVwTIVaGgY02/RoxVvNNrVX9PgGkxERYakY0GzTI87j7r1N0UI4VkItFQN97m1atFQUhLP9MTCMH8aD5cFOYFMwHGiK8kWwNLA3K19H14tk hcK2kF6MXgF5E383qZJGL8z0K7YtpOBvYgtPaQcDCyOm uDq8AD4AVg5aCaUF/JUxp5k9Ux Elgvp4nB9RCjJVSEa9bXITFYGAcbuj at59QgW7EPoPXXNYtej6/cCz4Aygp wTIOsfzdZs05 AuJB0lX1VCL9a5/Wus JP7hnYEw9UYDVw7B489uPVXOJfA/pgnd5D1vGLoK8u03LEq4UZCSTt4CdSkGuA8sy6aE3ioIqRqgtdok91WLMxvRIXQb4ZaMd83egfAz2t7f72FX7A778Fa4BhoBoZw8lXgmS6HxOnSpH1r7hs72zXrFPX9wVOc5GXoofklwEVaG1Y0/9ISBZSHWsgfCc4BajCbAG6CgFhraJCr0H4t8CzwOer CyLXhTy9nZV4CNdpD4SG5IvBlbFXBVutfR6ynUCf5NNP4P47UDa8iwZWJ6qJFkWzZaZrQq7hgxfcJEXZ9n6sK0HA q7bwZ CvxN9fq5/PZ5oL5 s0SVwGx4tlmZ1piPxg1/cfZ2faPssy7i6hoTjsuax8DWZHU4 BV4Gljh8 HpxGsdoRWibpK35QaONT7JqszBMLNXXci2vV3EDEWEZJKBXbHqIvAUsBvow07irwN7ALUerRL1w/Vg1cDa7JvSKmOGkK/GVGZnh87fwUXcoYiQTDGwJdZ8GWhl1W5cMtw9UxZ/Ysx9zl5V1G0yaKNM gMwPicrYisX8YgiQjLBwCismATUKtgNs3AucRo77AtGgCyKCtczwNush2/W5BIMMhsPkXEbuQj1SUNay4BmjfTE/wl4EtjNsvA84nTP8iC7YKTZrVAzYVkTde/Mxp/JuHVcxOuKCGkZA0uS8zRgN8jCF4g7FaiA5Um0m3o6MD86M2i8xs1mn7p2bXoiWcQ76K0ckMmesoq6GBrgfQTsfliobm1eZR8MNz8Ufh10LYBlxCHNgpl90ttWcBH6YVlFhjSNgeHktCPwN0b3Qa3CEWApkGdR4VclsEKnUJUkK3I2hpht0rtqq0UonKDIkKYwsBq5qLD4GRndA3UziiadOGTlrKtrkhEHT3Z2nayVuvfBa2AMkGh2IyR9BnYli68CrRvoPkgeAr8AN qgYDIbf8ZVfFKvJJOiQc H4BVn3XJODzUdBjpI9iSgroNVBtS2Y8FvgKZViyb JSK1FN7vzPiqCiF57pOg669mOkLSY0CV4HSwrcviHvQNwCwXVzT1fueQtki0u PMqFYh3nUWre30UBvHgHidWsGnXbKa9lO8uhRFlkdx7lnnoNZZMifWbPmp1sHeqsqcEzkxaE/s/M ErcdxrPWFMojGqk A8RVnMzlWtQrxV3dHbHDtokKtg4GjufYzQAugXjTDpKnVMom6hrtXHJaeObEu00xn2YpOD7U Bjbi8ilAK8xaAJVoilWLcGWrDPI982ItxO3O0rXQlwevurhQq2NgLKerVdgfbOwu1bTq5u64bOpE57DXXXRrVasQWof4G1gKKG40iAoBCTWI1he AtQ1GFG5/i3CO8FFleOyBppuNfG6xbUiXNpl6vUeW3W3dCeFOjQG1LIeCNQ31s322I/jkLa20yDBeJGeBfGbD6dbCyHDngc2A6CWIqQ6Bk7ldM0kedF44Shwr48ssd7ufPe6i266Osrl IENqhX3uvtBY4iQoTGgbQjXgj0Sp1/G8aEgKsMCYnw3yesLzmi Nt9lOc23EC 7H2LHqyNjEFUVYV93zlPo3wVXuLhQP2Gg0xHhdRfdVFU9IesVKeMHfYXQwNok1iKMiYFDDZy1J8lETxuNJUJ6M6CWdH0Xva7TW6VuRsafcpnP6a/LJONDBmZAlUErz2u402Y5PdSeDGzPoX/jz28X6nlm845UKZeoZKdurx82dH3QymYAfts8m3KZk/Yi3QKML4UvAb9hj8OQCgPthNrLZHy9U4lvZaClhQucTV9KGqO sBl8R/LHOO5mQC3DzcC4Uvg08N0BDkMcA1PRPV/a09RqaceAj4DZ1WsPn/pT9uNj6CG9GeirZRBXURl6c VjpnJgZUthFl6A6nA2aeG0S/yg k2LJFS/WH2rt11c2VVVBn0YusMRcSv63qCbUPdbqAsYmIn6S7AmeA9cD1otKzoD7nJ6t6pla1 LN n JRSNDW5J8BMtQ77LxQnufp5prvhZJrUGfuywkp1U8nAM/h8LOhwPP0ffAsx2caHmiwFtvzfR2lGfog1o1kpc3ecZ5YucgsvzHC Po2uGIiTfDFyD VbWu/ea RZC7vnV6lXy7W9DrF GVDR3rhVNyYNA8 kxZhAb RXdTz8RoinzPmU7Yq3WaGpMBaKsohXnE8EzwDjZtaxkFMzvg/FHg3u7r8v155/2dWi2yU70 zz6u6ao8Qc6HsTHd4C1FEX1uSx XYGjVsbvH8zpOe5kza6UUdQS3AOMtBllJKGgPm NXxpE2709ZzA/f 9OnjzYyQX8Xa3A5Y4DEXdAAf0sq0uHu3s7F30jT0RyUK3f/MB6ZX9ySXS1Djs4Xy9DVxMbkn8GRuLCps6NS9AfcsdtfVUI1RoTvzXW4ooeamJh YqTWmf4YdEdLpF/mlnazfl7u9O71L4qxEvupLK1EIfi 1bOf1UGrUiH5J BVXHhc2B158p0p/er7s8vNuAo02ByS/x 0vmu5tSvZnIYkmMGOrDdyrVC7VoekkzkLLvwYfSFhnRV/k/Sa5/m98f5dyc8SDDg9y7dmfit 7CvLtMb3b9 8kVq60 76MKpG PRPs4rVY6Q4jAwDFe2du5c5fRB1VGcMQ/Y0/KQQa/I9wmbYP6vwPvAfB6Xb5fC gQDB3Ps1x56TLUmzu3z8FZirXCoL11kOQznzNe30PcqsrMl9K0dn/3a2ukca/q1Kjmbs62QdKKvV9XV Tl5V0y9D5ivg65a5se1sLTCwEHu/uo D6 FGS1M6evUVlA U0siGb9mEey7yPkoX/fIuM1hXvUM/JhLrByfW/3lC644yyX07wuiC6N14MnTwMg6vzCehSPGgD4tql3bdo8/bz/UEv6zS6jPd05rSTRD13zV deJvmGGbAtT6mdAuyyuBFYZFGqlumZRAbHE5qNrwa4osjKOqEUw/75fFMfCj24GjkDTa9F2j8/s/qVGZTTXneISvBl9yRrTytplGhNpRsnI2ilrBoY9dTGg7Rm/A3Z//4S Tl0pVi5em/AjYAkf24hEW5yGFuEudz5d2mJ7IvvGMrAFyWlq9TVg5XbbRmah2mUJX93IhFuUlgZW5s/z6EWdUm4RvS3P9hh3fz9E/yIY8lTrwkMw/0V3zopOz6O6GEZrE5/JdJRH7SDC3DOgWaWdnRdaVzoPfODi6la/TQr2RM37VuhN8eVx58/n6mYnEsgKA7q3GudaWdWAelIaxq3rMnkHfZ80MmlCmuPJ4z AEaZwiSbkG1mkz4Ame74H/L3VTGIqsgypPgAss0tSySX9RHdzPsiXIe HT9 0yKFOBrT/zG/eU VIVfzepvfI6degPdUcG5/490nSKrWa1pBiMKAey03A7q1CLcqlKpq2ehn4TKemmmNjE1 J5PwT5KjGJh ptZCBU8nbyuW96NrO3xS5llwsY4VTm5JrYzLRCyF6C87sb29MspFKCxlYjby1/eZ YPdVq9N1yaJVXD2Nc1WoRgI9bWeCPMg4jNwMLFQx9jLCzooeQX4Z0H/8 YUz/1z0Ge441H4Y2Jd4e4IoDMk/A/ IC3cBu695Xw5o6h053hF3aVNzjszSYEAtwyxglUHhoWlkVMQ0tRKvptTI 1oRnSyRT1vj62nufuq XlEi/ t2dQIpPAmsQvhl/boTjwSaysD25HY9sHup8BbQ0I17pFdo0e7cvwMjcVShvS2uc7vg2lnuPup /gwMAyFDZGAc52le2irDkUO8Lk7LFgPasPcwsPuo8ERgs4aoIUNhIGaXhsJSts ZjHnJD0EclG2Ts2tdzC5l994MxbIDOEmbSX3LoC5wqlLNwlyqhjQ4cc0u Rd/7mlw pFcegy0k7QGylpr0CKwibZ3a1U6pAYGYnapBtIycslh2OFbBemKC6mDgZhdqoO8Fl2q926 Am4EvkKMaZE9hcl2HJ7E7FL bucxmOwrwlscxxuNDbiP30wQ24AkI4kUGdBWDN2z24BViD nmF/pkvbb1GMQlv3brzUFqwgKHwJae2iJFHGWabRjcq7TQ80WA9tgzm5gb2fWneiKD2kgA38gLXvi/FsD042kGsfAMiTlv5Ch 3UdmNS4LCIlY8BXCD2BQrLDgGaMpoD7wEfAHlyXo4ekxMCjpGtEb5FSHpFs9QxswCX/CjRgtvuj8C6gRbeQFBhYnTT9xxBWTSGPSLI6Btbi9APBOcBXBOnqJoWkyMBE0jbS9f73kinmFUkPzoDGCslZJN2fS8GuYGkQkiIDU0jbKsQdKeYTSQ/MgI0VLua0Z4DdE4XHAb9HicOQtBjQrJKR/6O0Mol0B2Sgv7HCDK7SDtaQJjGgt6f B1iF0G7JkOYxMNBY4TvNMyNyMgaWRXkdWIXQloCQ5jAQY4Xm8FxVLnr/wSqDQrUYIekyEGOFdPmtK/WtuNoqxJy6UoqLh8KAvp6uLuo8YLwrjLECJGRB9P6t3RjtiQlpPAPDSVLTpSeA5Iv/4j73Y4Uibe6bwA0xedWUCBvGwIaktCOYCjYGXh7k4Fvgjz4y9NYycD3ZWwtxa2tNKUzuvkW40vFrPCs8G6xTFI L1EL4QfR7RblBLfRjoBZhJnZp28Ud4G5QGClShdCmvl0qd0Z6SPUMqEXoAOoSbQn6WsuZSvxVQK94Fk6KVCHmubvjdRcd6gAMlLJFSPJRpAqR9C2OB2dALYK dLETGAc DZIylYjCtghJZ6NCJBkpz7G1CHrBf2zC7ZkcF3KMkPCz12FUiF6UFDqi9GOEwe5uVIjBGCrO79YiTMUlDZq9aGV/NpgGzvc/lE0vUoXwzb7Xy3ZPvb/RIng2hqAXqUKs4vz1uosulTpQizATJko5RhisBBSpQnzgnF3C6WVSo0Uo090exNeT N22FOjbrmWTvXD4YvAcMB4svIW4bwC9cx5SEgb0yRkrAE g64WhIov82wR8CfwSPA3Mfx9OIX40CCkZA PxV3uYrDAkZ1KKQoe6RRoffA94f81vhZo1ihYBEsouj0CAFYxb0IvSRdDndLSafBTQf N8CpifPpxP/BlgfRASDLRdDQf6HpMVklkF4GQPfDgdvOT8Mv8UaszwU6AxxBogJBjowYAqgRUYVY77wOQeZ2T3YASm6eMIGhfo/Y6HgMZD5k8yPIDfQoKBARlQN0mVwrcUz3CsreELgyyKFhIPAb8BL4Bkwbfjm/jte0CtRnzsCxIaLQs1OsEMpXcDtuhFeC8zONCClLYpqOC9AV4FzRR9b3YloLfM1MVZE2wOVgOLgb5EXcHvgsf6 jHiGsdAkSuECtu2QN0K9a/7kueJfB10gjOB unqovhFPg5rFj3FVQGWA5uCbcCEik7QrzzOL 8C2SO7bgPxjgckpC1FrhDGnQrh5WCsRQwSvs3vmtNXRVEhHAPeB3oLr69CqXRXAZoO1bWqTCuApcDKQDNE/T35 amH/Iij88GcHrFx0DQGylAhROYOQPPyS4M3gQrwSkCFvRXydzLVgPn2SqgK CCQbSEtZKAsFaIvilUp1J05HuhJLi70HnEaMp9E9dRXq6MB/oUgvgwCCVmTMleI5L1Qt2Y0sO7Oiujq73eAYWCwLpOe hqsa5pXT3oV/rcqoSrEeyAk4wz8P tCAtne45WMAAAAAElFTkSuQmCC
Using this data as an image source for the img tag results in the empty image.
The problem is this jQuery line:
$.post('/scribble/ax/ins_scribble', $('#form-scribble').serialize() + '&data=' + data, function(ans) {
Combining the serialize function and an addition of &data= didn't work. So I changed it to:
$.post('/scribble/ax/ins_scribble', { data: data }, function(ans) {
You code runs fine:
var canvas, ctx;
var mouseX, mouseY, mouseDown = 0;
var touchX, touchY;
var lastX, lastY = -1;
canvas = document.getElementById('scribble-canvas');
ctx = canvas.getContext('2d');
canvas.addEventListener('mousedown', scribbleMouseDown, false);
canvas.addEventListener('mousemove', scribbleMouseMove, false);
canvas.addEventListener('mouseup', scribbleMouseUp, false);
canvas.addEventListener('touchstart', scribbleTouchStart, false);
canvas.addEventListener('touchend', scribbleTouchEnd, false);
canvas.addEventListener('touchmove', scribbleTouchMove, false);
function drawLine(ctx, x, y, size) {
if (lastX == -1) {
lastX = x;
lastY = y;
}
ctx.strokeStyle = "#000000";
ctx.lineCap = "round";
ctx.lineWidth = size;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
lastX = x;
lastY = y;
}
function scribbleMouseDown() {
mouseDown = 1;
drawLine(ctx, mouseX, mouseY, 3);
}
function scribbleMouseUp() {
mouseDown = 0;
lastX = -1;
lastY = -1;
}
function scribbleMouseMove(e) {
getMousePos(e);
if (mouseDown == 1) {
drawLine(ctx, mouseX, mouseY, 3);
}
}
function getMousePos(e) {
if (!e) {
var e = event;
}
if (e.offsetX) {
mouseX = e.offsetX;
mouseY = e.offsetY;
} else if (e.layerX) {
mouseX = e.layerX;
mouseY = e.layerY;
}
}
function scribbleTouchStart() {
getTouchPos();
drawLine(ctx, touchX, touchY, 3);
event.preventDefault();
}
function scribbleTouchEnd() {
lastX = -1;
lastY = -1;
}
function scribbleTouchMove(e) {
getTouchPos(e);
drawLine(ctx, touchX, touchY, 3);
event.preventDefault();
}
function getTouchPos(e) {
if (!e) {
var e = event;
}
if (e.touches) {
if (e.touches.length == 1) {
var touch = e.touches[0];
touchX = touch.pageX - touch.target.offsetLeft;
touchY = touch.pageY - touch.target.offsetTop;
}
}
}
function crop(canvas) {
var w = canvas.width,
h = canvas.height;
var pix = {
x: [],
y: []
};
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index + 3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a, b) {
return a - b
});
pix.y.sort(function(a, b) {
return a - b
});
if (pix.x.length == 0) {
return '#';
}
var n = pix.x.length - 1;
w = 1 + pix.x[n] - pix.x[0];
h = 1 + pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
//document.body.style.opacity = 0;
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
return canvas.toDataURL();
}
document.getElementById('scribble-submit').addEventListener('click', function(e) {
e.preventDefault();
var data = crop(document.querySelector('#scribble-canvas'));
document.getElementById('scribble-result').setAttribute('src', data);
});
<html>
<body>
<canvas id="scribble-canvas" style="width:200; height:200; border: 1px solid blue;"></canvas>
<br>
<button id="scribble-submit">
Run
</button>
<br>
Output:
<br>
<img src="#" id="scribble-result" />
</body>
</html>
You either send the wrong data or use different data to render.
If you are saving into a .png file, you may want to strip out the initial data:image/png;base64, before turning that into bytes.
I am trying to use the HTML5 canvas features. I set up a rectangle on the canvas and it is rotated. It is not returning back a true when I click inside the rotated rectangle. Perhaps I am just tripping up on understanding the solution. What am I doing incorrectly? I have the code below.
I am using the solution referenced in:
Mouse position within rotated rectangle in HTML5 Canvas
<?php
$canvas_width=800;
$canvas_height=1336;
echo <<<_END
<canvas id="myCanvas" width=$canvas_width height=$canvas_height style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
//Get the canvas element by using the getElementById method.
canvas = document.getElementById('myCanvas');
HEIGHT = canvas.height;
WIDTH = canvas.width;
CANVAS_LEFT = 9;
CANVAS_RIGHT = canvas.width+CANVAS_LEFT;
CANVAS_TOP = 9;
CANVAS_BOTTOM = canvas.height+CANVAS_TOP;
lastMouseX = 0; // The last seen mouse X coordinate
lastMouseY = 0; // the last seen mouse Y coordinate
rectangleDrawAndRotated=false;
mouseX =0;
mouseY=0;
offsetX = 0;
offsetY = 0;
originX=0;
originY=0;
width=0;
height=0;
//Get a 2D drawing context for the canvas.
ctx = canvas.getContext('2d');
myimage=new Image()
myimage.src='/Farm_planner/images/The_Farm_at_Dover_Vineyards_Google_Maps.png'
myimage.onload=function()
{
ctx.drawImage(myimage,1,1,800,1336)
}
// When true, moving the mouse draws on the canvas
isDrawing = false;
x = 0;
y = 0;
// event.offsetX, event.offsetY gives the (x,y) offset from the edge of the canvas.
// Add the event listeners for mousedown, mousemove, and mouseup
canvas.addEventListener('mousedown', e => {
x = e.offsetX;
y = e.offsetY;
isDrawing = true;
console.log("mouse down",x,y,width,height);
if (rectangleDrawAndRotated ===true){
originX = x + width/2;
originY = y + height/2;
getMouse(e);
// get the locations of the mouse and assign to mouseX and mouseY
clickState=false;
console.log(clickState,mouseX, mouseY,x,y);
if (clickedOnRectangle(x,y)) clickState=true;
console.log(clickState,mouseX, mouseY,x,y);
}
});
canvas.addEventListener('mousemove', e => {
if (isDrawing === true) {
// drawLine(ctx, x, y, e.offsetX, e.offsetY);
// x = e.offsetX;
// y = e.offsetY;
}
});
window.addEventListener('mouseup', e => {
if (isDrawing === true) {
drawLine(ctx, x, y, e.offsetX, e.offsetY);
isDrawing = false;
mouseX =0;
mouseY=0;
rectangleDrawAndRotated=true;
console.log("mouse up",x,y,width,height);
}
});
function drawLine(ctx, x1, y1, x2, y2) {
// ctx.beginPath();
// ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
// ctx.moveTo(x1, y1);
// ctx.lineTo(x2, y2);
// ctx.stroke();
// ctx.closePath();
width=x2-x1;
height=y2-y1;
xCoord=x1;
yCoord=y1;
drawRectangle(x1,y1,width,height) ;
rotateDrawing(x1,y1,width,height);
lastMouseX = 0; // The last seen mouse X coordinate
lastMouseY = 0; // the last seen mouse Y coordinate
}
function drawRectangle(xCoord,yCoord,width,height) {
// ctx.strokeRect(xCoord,yCoord,width,height);
ctx.rect(xCoord,yCoord,width,height);
originX = xCoord + width/2;
originY = yCoord + height/2;
r=45*(Math.PI / 180);
}
function clickedOnRectangle(x,y) {
// dtermine if the user clicked on the area of the rectangle that has been rotated
// Our origin of rotation is the center of the rectangle
// Our rectangle has its upper-left corner defined by x,y, its width
// defined in w, height in h, and rotation(in radians) in r.
// translate mouse point values to origin
dx = mouseX - originX;
dy = mouseY - originY;
// distance between the point and the center of the rectangle
var h1 = Math.sqrt(dx*dx + dy*dy);
var currA = Math.atan2(dy,dx);
// Angle of point rotated around origin of rectangle in opposition
var newA = currA - r;
// New position of mouse point when rotated
var x2 = Math.cos(newA) * h1;
var y2 = Math.sin(newA) * h1;
console.log(x,y,width,height,mouseX,mouseY,originX,originY,dx,dy,h1,currA,r,newA,x2,y2)
// Check relative to center of rectangle
if (x2 > -0.5 * width && x2 < 0.5 * width && y2 > -0.5 * height && y2 < 0.5 * height){
console.log('true');
return true;
}
}
// Sets mouseX and mouseY variables taking into account padding and borders
function getMouse(e) {
var element = canvas;
var offsetX = 0;
var offsetY = 0;
// Calculate offsets
if (element.offsetParent) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
// Calculate the mouse location
mouseX = e.pageX - offsetX;
mouseY = e.pageY - offsetY;
// Calculate the change in mouse position for the last
// time getMouse was called
changeInX = mouseX - lastMouseX;
changeInY = mouseY - lastMouseY;
// Store the current mouseX and mouseY positions
lastMouseX = mouseX;
lastMouseY = mouseY;
}
function rotateDrawing(xCoord,yCoord,width,height)
{
ctx.setTransform(1,0,0,1,0,0);
ctx.translate(xCoord+.5*width,yCoord+.5*height );
r=45*(Math.PI / 180); // 45 degrees as radian
ctx.rotate(r); //
ctx.fillStyle = "red";
ctx.fillRect(-.5*width, -.5*height, width, height);
console.log(xCoord,yCoord,width,height)
}
</script>
_END;
Yes, you are tripping on the solution, treat your rectangle as a polygon and you don't have to worry about doing rotation, also you can have more complex shapes than rectangles.
I'm using the ray-casting algorithm:
https://github.com/substack/point-in-polygon/blob/master/index.js
With that, all we need to do is check if the mouse is inside the polygon, that is all.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext("2d");
const rect = canvas.getBoundingClientRect();
const poly = [[89, 9], [13, 19], [19, 56], [98, 36], [89, 9]]
function draw(p) {
p.map(x => ctx.lineTo(x[0], x[1]));
ctx.stroke();
}
function inside(p, vs) {
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0], yi = vs[i][1];
var xj = vs[j][0], yj = vs[j][1];
var intersect = ((yi > p[1]) != (yj > p[1])) && (p[0] < (xj - xi) * (p[1] - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
draw(poly)
canvas.addEventListener('mousemove', function(evt) {
ctx.clearRect(100, 0, canvas.width, canvas.height);
let x = inside([ evt.clientX - rect.left, evt.clientY - rect.top ], poly)
ctx.fillText(x, 110, 20);
}, false);
<canvas id="canvas"></canvas>
I did it on mousemove so you can see the change right away...
but same can be applied to any event you like
I want to get coordinates of the painted area of an image placed in an HTML canvas tag and send it to the database. And populate it on the same area of that image in another page. And how can I reset or clear the painted area by clicking the reset button?
JSfiddle example
var canvas = document.getElementById("canvas");
var img = document.getElementById("imagearea"),
ctx = canvas.getContext("2d"),
painting = false,
lastX = 0,
lastY = 0,
lineThickness = 1;
canvas.width = canvas.height = 600;
ctx.fillRect(0, 0, 600, 600);
ctx.drawImage(img, 10, 10);
canvas.onmousedown = function(e) {
painting = true;
ctx.fillStyle = "#ff0000";
lastX = e.pageX - this.offsetLeft;
lastY = e.pageY - this.offsetTop;
};
canvas.onmouseup = function(e){
painting = false;
}
canvas.onmousemove = function(e) {
if (painting) {
mouseX = e.pageX - this.offsetLeft;
mouseY = e.pageY - this.offsetTop;
// find all points between
var x1 = mouseX,
x2 = lastX,
y1 = mouseY,
y2 = lastY;
var steep = (Math.abs(y2 - y1) > Math.abs(x2 - x1));
if (steep){
var x = x1;
x1 = y1;
y1 = x;
var y = y2;
y2 = x2;
x2 = y;
}
if (x1 > x2) {
var x = x1;
x1 = x2;
x2 = x;
var y = y1;
y1 = y2;
y2 = y;
}
var dx = x2 - x1,
dy = Math.abs(y2 - y1),
error = 0,
de = dy / dx,
yStep = -1,
y = y1;
if (y1 < y2) {
yStep = 1;
}
lineThickness = 5 - Math.sqrt((x2 - x1) *(x2-x1) + (y2 - y1) * (y2-y1))/10;
if(lineThickness < 1){
lineThickness = 1;
}
for (var x = x1; x < x2; x++) {
if (steep) {
ctx.fillRect(y, x, lineThickness , lineThickness );
} else {
ctx.fillRect(x, y, lineThickness , lineThickness );
}
error += de;
if (error >= 0.5) {
y += yStep;
error -= 1.0;
}
}
lastX = mouseX;
lastY = mouseY;
}
}
<canvas id="canvas">
</canvas>
<img id="imagearea" src="https://media.istockphoto.com/photos/green-apple-with-leaf-and-cut-isolated-on-white-picture-id1141004606?k=6&m=1141004606&s=170667a&w=0&h=zwbN4lLc7MFb6f_aZ4npNL3i4Tgde-yINlYTztlI1QQ=" style="display: none;" />
<button> Reset </button>
To achieve what you're looking for, you need to store the min(x, y) coords for the (top, left) position and the max(x, y) coords for the (bottom, right) position, and you can't get the image on the same canvas if you are looking to remove the drawing on the area. use an HTML element with absolute position relative to the canvas for the area frame, attach an event to crop, and display the target area another one to remove it.
Here is a working example with a lot of comments, it should be clear. Click on the area to display the preview on the "x" to remove it with the drawing, it can handle multiple areas.
const source = "https://media.istockphoto.com/photos/green-apple-with-leaf-and-cut-isolated-on-white-picture-id1141004606?k=6&m=1141004606&s=170667a&w=0&h=zwbN4lLc7MFb6f_aZ4npNL3i4Tgde-yINlYTztlI1QQ=";
const container = document.querySelector("#container");
const canvas = container.querySelector("canvas");
const ctx = canvas.getContext("2d");
const resetButton = document.querySelector("button");
let lastDrawnArea = [[Infinity, Infinity], [0, 0]];
let image;
let painting = false;
let lastX = 0;
let lastY = 0;
let lineThickness = 1;
init();
async function init() {
await loadDrawImage();
// Start Event Listening
canvas.onmousedown = function(e) {
painting = true;
ctx.fillStyle = "#ff0000";
lastX = e.pageX - this.offsetLeft;
lastY = e.pageY - this.offsetTop;
};
canvas.onmouseup = function(e){
painting = false;
// Set the drawing area
setDrawingArea();
}
canvas.onmousemove = function(e) {
if (painting) {
mouseX = e.pageX - this.offsetLeft;
mouseY = e.pageY - this.offsetTop;
// find all points between
var x1 = mouseX,
x2 = lastX,
y1 = mouseY,
y2 = lastY;
var steep = (Math.abs(y2 - y1) > Math.abs(x2 - x1));
if (steep){
var x = x1;
x1 = y1;
y1 = x;
var y = y2;
y2 = x2;
x2 = y;
}
if (x1 > x2) {
var x = x1;
x1 = x2;
x2 = x;
var y = y1;
y1 = y2;
y2 = y;
}
var dx = x2 - x1,
dy = Math.abs(y2 - y1),
error = 0,
de = dy / dx,
yStep = -1,
y = y1;
if (y1 < y2) {
yStep = 1;
}
lineThickness = 5 - Math.sqrt((x2 - x1) *(x2-x1) + (y2 - y1) * (y2-y1))/10;
if(lineThickness < 1){
lineThickness = 1;
}
for (var x = x1; x < x2; x++) {
if (steep) {
ctx.fillRect(y, x, lineThickness , lineThickness );
} else {
ctx.fillRect(x, y, lineThickness , lineThickness );
}
error += de;
if (error >= 0.5) {
y += yStep;
error -= 1.0;
}
}
lastX = mouseX;
lastY = mouseY;
// Set The min, max coordinate of the current drawing
// to define the current drawing area
lastDrawnArea = [
[// Top left min([x, y]) coords
Math.min(lastDrawnArea[0][0], mouseX),
Math.min(lastDrawnArea[0][1], mouseY)
],
[// Bottom right max([x, y]) coords
Math.max(lastDrawnArea[1][0], mouseX),
Math.max(lastDrawnArea[1][1], mouseY)
]
]
}
}
}
async function loadDrawImage() {
image = new Image();
// Load the image
await new Promise(resolve => {
image.onload = resolve;
image.src = source;
});
const [width, height] = [image.naturalWidth, image.naturalHeight];
// Set the container and canvas size
container.style.width = `${width}px`;
container.style.height = `${height}px`;
canvas.width = width;
canvas.height = height;
// Set the container in the background
container.style.background = `url(${image.src})`;
}
function setDrawingArea(){
const [TOP_LEFT, BOTTOM_RIGHT, X, Y] = [0, 1, 0, 1];
const container = document.querySelector("#container");
const template = document.querySelector("#areaTemplate");
const area = template.content.firstElementChild.cloneNode(true);
// You should replace this with the lineThickness
const offset = 10;
// Get the area size
const width = lastDrawnArea[BOTTOM_RIGHT][X] - lastDrawnArea[TOP_LEFT][X];
const height = lastDrawnArea[BOTTOM_RIGHT][Y] - lastDrawnArea[TOP_LEFT][Y];
area.style.left = `${lastDrawnArea[TOP_LEFT][X] - offset}px`;
area.style.top = `${lastDrawnArea[TOP_LEFT][Y] - offset}px`;
area.style.width = `${width + (offset * 2)}px`;
area.style.height = `${height + (offset * 2)}px`;
// Draw the template
container.append(area);
// Add the events
area.onclick = previewArea; // Preveiw event
area.querySelector("b").onclick = removeArea; // Remove event
// Reset "lastDrawnArea" value
lastDrawnArea = [[Infinity, Infinity], [0, 0]];
}
function previewArea(e) {
const preview = document.querySelector("#preview");
const previewCanvas = preview.querySelector("canvas");
const previewCtx = previewCanvas.getContext("2d");
// Get the drawing area coords
const area = e.target;
const [x, y, width, height] = [
parseFloat(area.style.left),
parseFloat(area.style.top),
parseFloat(area.style.width),
parseFloat(area.style.height)
];
// Draw the preview area
previewCanvas.width = width;
previewCanvas.height = height;
previewCtx.drawImage(image,
x, y, width, height,
0, 0, width, height);
}
function removeArea(e) {
const area = e.target.parentElement;
const [x, y, width, height] = [
parseFloat(area.style.left),
parseFloat(area.style.top),
parseFloat(area.style.width),
parseFloat(area.style.height)
];
ctx.clearRect(x, y, width, height);
area.remove();
}
resetButton.onclick = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
document.querySelectorAll('.area').forEach(el => el.remove());
}
body {
display: flex;
}
#container {
position: relative;
}
.area {
position: absolute;
border: 2px solid #333;
color: #333;
cursor: pointer;
}
.area b {
position: absolute;
right: 0;
top: 0;
transform: translate(100%, -100%);
color: red;
cursor: pointer;
}
<div id="container">
<canvas></canvas>
</div>
<div id="preview">
<canvas></canvas>
</div>
<template id="areaTemplate">
<div class="area">
<b>x</b>
</div>
</template>
<button> Reset All </button>
There is one problem with this example if two areas overlap the drawing will be removed at the intersection of both areas, to overcome this issue you need to keep each drawing on its own canvas (... a lot of work).
Last but not least, you can achieve the same result with ease and have better control over the canvas and its elements if you use a library like fabric js, take a look at this example of freedrawing, you will get the drawing coordinates for free, and you can still have everything on the same canvas (the image or whatever you need to add to canvas and all the drawing with no overlap ...), the initial learning curve may take some time but by the end of the day, you'll get a better understanding of HTML canvas overall.
Side note: the image you're using has a cross-origin limitation, you should use images from the same domain or from domains that allow cross-origin.
EDIT : Update with a working demo based on your fiddle
You may have to adapt this function to also include the thikness of the drawed lines (they may appear outside of the registered area).
But like this, you have te position and the size of your drawed area.
You can now do a ROI on it if you want.
You can track the area drawed with a function like this one:
var drawedArea = [0,0,0,0];
function drawedAreaTrack(x, y) {
// top left x
if (drawedArea[0] === 0) {
drawedArea[0] = x;
} else {
drawedArea[0] = Math.min(drawedArea[0], x);
}
// top left y
if (drawedArea[1] === 0) {
drawedArea[1] = y;
} else {
drawedArea[1] = Math.min(drawedArea[1], y);
}
// bottom right x
drawedArea[2] = Math.max(drawedArea[2], x);
// bottom right y
drawedArea[3] = Math.max(drawedArea[3], y);
console.log(drawedArea);
}
You could use those two point to get the total area drawed.
Here is a working example-> Fiddle :
http://jsfiddle.net/b90h6gaq/8/
The 'onmousemove' is doing all the drawing, I would save all you need to an array then on some event send to the database or draw to another canvas...
Here is a very simple example. I'm keeping the code minimal to convey my point that you already have all you need, all that is needed is to store it in a variable, You can bring back the lineThickness, the rest of the calculations and logic later, that should not be a problem.
var button = document.getElementById("btn");
var canvas = document.getElementById("canvas1");
var canvas2 = document.getElementById("canvas2");
var ctx = canvas.getContext("2d");
var ctx2 = canvas2.getContext("2d");
var painting = false;
var coordinates = [];
canvas.onmousedown = function(e) {painting = true;}
canvas.onmouseup = function(e) {painting = false;}
canvas.onmousemove = function(e) {
if (painting) {
x = e.pageX - this.offsetLeft;
y = e.pageY - this.offsetTop;
coordinates.push({x, y})
ctx.fillRect(x, y, 5, 5);
}
}
button.onmousedown = function(e) {
ctx.clearRect(0, 0, 300, 150);
coordinates.forEach(coord => {
ctx2.fillRect(coord.x, coord.y, 5, 5);
});
};
<canvas id="canvas1" width=300 height=150></canvas>
<button id="btn"> Reset </button>
<canvas id="canvas2" width=300 height=150></canvas>
Here we have a new event on the button click, I'm clearing the initial canvas and drawing all the coordinates to a second canvas, we could also be sending that data to server to be stored that should not be a problem.
I don’t know if this is quite what you were thinking, but if you simply want the same image to appear on another webpage, you could use ctx.getImageData() to copy the drawing from the canvas as an object, convert it to json, then send it to the database. Then on the other end, turn it back into an object and use ctx.putImageData() to place it back on the new canvas.
I have this problem, and I don't know how to deal with it.
I struggled almost 2 hours to fix it. I tried various methods, but with no luck. If it's a nobbish question please forgive me and explain me in depth every step you do.
I have created a canvas, and I drew a circle in it. Now, I want to move the circle with the mouse, but while the mouse is hold. Is there any way to do it, or I'm just wasting time?
Here is the code:
<canvas draggable="true" id="canv" name="canv" width="600" height="500" style="border:1px solid #000000;" onclick="">Your browser does not support the HTML5 canvas tag.</canvas> <!-- < -- the canvas -->
<script>
var canv = document.getElementById("canv");
var context = canv.getContext("2d");
var width = canv.offsetWidth-1;
var height = canv.offsetHeight-1;
context.beginPath();
context.arc(width/2, height/2, 75, 0, 2*Math.PI);
context.stroke();
</script>
Some methods I have tried. These one are left in remarks /**/. Other ones I deleted them.
1.
<canvas draggable="true" id="canv" name="canv" width="600" height="500" style="border:1px solid #000000;" onclick="circle_move()"></canvas>
function circle_move(event){
var x = event.clientX;
var y = event.clientY;
document.getElementById("txt").setAttribute("value", "X: "+x+" and Y: "+y);
document.getElementById("canv").addEventListener("onmousemove", function(){
context.beginPath();
context.clearRect(0, 0, width, height);
context.arc(x, y, 75, 0, 2*Math.PI);
context.stroke();
});
}
2.
document.getElementById("canv").addEventListener("mousedown",
function(event){
var x = event.clientX;
var y = event.clientY;
document.getElementById("txt").setAttribute("value", "X: "+x+" and Y: "+y);
context.beginPath();
context.clearRect(0, 0, width, height);
context.arc(x, y, 75, 0, 2*Math.PI);
context.stroke();
}
);
3.
<canvas draggable="true" id="canv" name="canv" width="600" height="500" style="border:1px solid #000000;" onmousedown="launch"></canvas>
function loop(event){
var x = event.clientX;
var y = event.clientY;
document.getElementById("txt").setAttribute("value", "X: "+x+" and Y: "+y);
context.beginPath();
context.clearRect(0, 0, width, height);
context.arc(x, y, 75, 0, 2*Math.PI);
context.stroke();
}
function launch(event){
loop();
if(event.which !== 1 || event.which !== 2 || event.which !== 3){
launch(event);
}
}
Animation and UI
When you drag anything on the canvas you are effectively animating the canvas. To get the best results you need to stop thinking in terms of how you did stuff with the DOM and start to codelike you are writing a game.
One function to rule them all.
To coordinate all that is going on you need one function that handles all the animation. Sometimes called the mainLoop in gaming circles it makes working with animated content a lot easier.
The main loop
function mainLoop(time){ // this is called 60 times a second if there is no delay
// clear the canvas ready to be rendered on.
ctx.clearRect(0,0,canvas.width,canvas.height);
updateDisplay(); // call the function that is rendering the display
// get the next frame
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
From the main loop you can check the current app state and call the the appropriate functionality. In this example there is not much to its just calling updateDisplay()
KISS IO events.
The mouse, keyboard, touch, etc event handlers should not be doing any functional logic. Theses event can fire very rapidly, but the display will ony update once every 60th of a second. The is no point rendering from an event that can fire 500 times in a second when the user can only see just over 10% of the effort the CPU is doing.
The best way to write IO events is just as data loggers. Get what info you can as quick as possible and get out, don't write a different event listener for each type of event, keep the code simple, write just one event handler to handle as much as you can. If you need the keyboard add that to the same listener.
var mouse = (function(){
var bounds;
var m = {x:0,y:0,button:false};
function mouseEvent(event){
bounds = event.currentTarget.getBoundingClientRect();
m.x = event.pageX - bounds.left + scrollX;
m.y = event.pageY - bounds.top + scrollY;
if(event.type === "mousedown"){
m.button = true;
}else if(event.type === "mouseup"){
m.button = false;
}
}
m.start = function(element){
["mousemove","mousedown","mouseup"].forEach(eventType => element.addEventListener(eventType, mouseEvent));
}
return m;
}())
mouse.start(canvas);
Now you have access to the mouse whenever you need it via the simple mouse interface.
Circles
There is no point adding a nice interface if there is nothing to move about in it. The following is a object that helps manage circle. It creates, draws, and locates circles.
Included is the findClosest function it get the circle under the mouse. It will return the smallest circle under the mouse. If nothing under the mouse it will return undefined.
var circles = {
items [],
drawCircle(){ // function for the circle
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.stroke();
},
each(callback){ // iterator
var i;
for(i = 0; i < this.items.length; i++){
callBack(this.items[i],i);
}
},
drawCircles(){
this.each(c => {
c.draw();
})
},
addCircle(x,y,radius){
var circle = {x, y, radius, draw : this.drawCircle};
this.items.push(circle);
return circle;
},
getClosest(pos){
var minDist, i, dist, x, y, foundCircle;
minDist = Infinity;
this.each(c =>{
x = pos.x - c.x;
y = pos.y - c.y;
dist = Math.sqrt(x * x + y * y);
if(dist <= c.radius && dist < minDist){
minDist = dist;
foundCircle = c;
}
})
return foundCircle;
}
}
How to Drag.
There is a lot involved in dragging objects. You need a function to find the closest object to the mouse. You need to provide feed back so that the user can see what can be dragged.
You may end up with many different drag type events. Drag to create, drag to move, drag something onto the canvas, or manipulate a custom rendered UI object. If you manage the dragging state from one object you can ensure that you dont accidentally click a UI item when dragging a circle.
When dragging starts you check what the drag will be doing. Then you flag that you are dragging, and indicate what the drag is to do. (see updateDisplay function)
While the drag is active do that action. When the mouse is up then just deactivate the drag
var dragging = {
started : false, // true if dragging
type : null, // string with the type of drag event
currentObj : null, // what we are dragging
startX : 0, // info about where the drag started
startY : 0,
start(type, obj){ // called at the start of a drag.
this.startX = mouse.x;
this.startY = mouse.y;
this.started = true;
this.type = type;
this.currentObj = obj;
}
}
Then the render function. It is called once every frame. I checks if the mouse button is down, what to do if it is, sets the cursor so people know what to do, and draws the circles.
var cursor = "default"; // to hold the cursor
var overCircle = null; // this holds whatever circle happens to be under the mouse when not dragging
function updateDisplay(){
var x,y, c;
cursor = "default"; // set default cursor
// check that the mouse if over the canvas
if(mouse.x >= 0 && mouse.x < canvas.width && mouse.y >= 0 && mouse.y < canvas.height){
cursor = "crosshair";
}
// is the mouse button down
if(mouse.button){ // the button is down
if(!dragging.started){ // if not dragging start
if(overCircle){ // start a move drag if over a circle
dragging.start("move",overCircle)
overCircle = null;
}else{ // start a create drag
dragging.start("create",circles.addCircle(mouse.x, mouse.y, 1));
}
}
c = dragging.currentObj;
// Update the drag state o fthe object being draged and the type of drag
if(dragging.type === "create"){
x = c.x - mouse.x;
y = c.y - mouse.y;
c.radius = Math.sqrt(x * x + y * y);
}else if(dragging.type === "move"){
x = dragging.startX - mouse.x;
y = dragging.startY - mouse.y;
c.x -= x;
c.y -= y;
dragging.startX = mouse.x;
dragging.startY = mouse.y;
}
cursor = "none";
} else { // button must be up
if(dragging.started){ // have we been dragging something.
dragging.started = false; // drop it
}
}
// draw the circles
ctx.strokeStyle = "black";
circles.draw();
// if not dragging
if(!dragging.started){
// find circle under the mouse
c = circles.getClosest(mouse);
if(c !== undefined){ // if there is a circle under the mouse highlight it
cursor = "move";
ctx.strokeStyle = "red";
ctx.fillStyle = "rgba(0,255,255,0.1)";
c.draw();
ctx.fill();
overCircle = c;
}else{
overCircle = null;
}
}
// set the cursor.
canvas.style.cursor = cursor;
}
As a working example.
var canvas = document.createElement("canvas");
canvas.style.border = "1px black solid";
canvas.width = 512;
canvas.height = 200;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
function mainLoop(time){ // this is called 60 times a second if there is no delay
ctx.clearRect(0,0,canvas.width,canvas.height);
updateDisplay(); // call the function that is rendering the display
// get the next frame
requestAnimationFrame(mainLoop);
}
// request the first frame. It will not start untill all the code below has been run
requestAnimationFrame(mainLoop);
var mouse = (function(){
var bounds;
var m = {x:0,y:0,button:false};
function mouseEvent(event){
bounds = event.currentTarget.getBoundingClientRect();
m.x = event.pageX - bounds.left + scrollX;
m.y = event.pageY - bounds.top + scrollY;
if(event.type === "mousedown"){
m.button = true;
}else if(event.type === "mouseup"){
m.button = false;
}
}
m.start = function(element){
["mousemove","mousedown","mouseup"].forEach(eventType => element.addEventListener(eventType, mouseEvent));
}
return m;
}())
mouse.start(canvas);
var circles = {
items : [],
drawCircle(){ // function for the circle
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.stroke();
},
each(callback){ // iterator
var i;
for(i = 0; i < this.items.length; i++){
callback(this.items[i],i);
}
},
draw(){
this.each(c => {
c.draw();
})
},
addCircle(x,y,radius){
var circle = {x, y, radius, draw : this.drawCircle};
this.items.push(circle);
return circle;
},
getClosest(pos){
var minDist, i, dist, x, y, foundCircle;
minDist = Infinity;
this.each(c =>{
x = pos.x - c.x;
y = pos.y - c.y;
dist = Math.sqrt(x * x + y * y);
if(dist <= c.radius){
if(foundCircle === undefined || (foundCircle && c.radius < foundCircle.radius)){
minDist = dist;
foundCircle = c;
}
}
})
return foundCircle;
}
}
var dragging = {
started : false,
type : null,
currentObj : null, // what we are dragging
startX : 0,
startY : 0,
start(type, obj){
this.startX = mouse.x;
this.startY = mouse.y;
this.started = true;
this.type = type;
this.currentObj = obj;
}
}
var cursor = "default";
var overCircle = null;
function updateDisplay(){
var x,y, c;
cursor = "default"
if(mouse.x >= 0 && mouse.x < canvas.width && mouse.y >= 0 && mouse.y < canvas.height){
cursor = "crosshair";
}
if(mouse.button){ // the button is down
if(!dragging.started){
if(overCircle){
dragging.start("move",overCircle)
overCircle = null;
}else{
dragging.start("create",circles.addCircle(mouse.x, mouse.y, 1));
}
}
c = dragging.currentObj;
if(dragging.type === "create"){
x = c.x - mouse.x;
y = c.y - mouse.y;
c.radius = Math.sqrt(x * x + y * y);
}else if(dragging.type === "move"){
x = dragging.startX - mouse.x;
y = dragging.startY - mouse.y;
c.x -= x;
c.y -= y;
dragging.startX = mouse.x;
dragging.startY = mouse.y;
}
cursor = "none";
} else { // button must be up
if(dragging.started){ // have we been dragging something.
dragging.started = false; // drop it
}
}
ctx.strokeStyle = "black";
circles.draw();
if(!dragging.started){
c = circles.getClosest(mouse);
if(c !== undefined){
cursor = "move";
ctx.strokeStyle = "red";
ctx.fillStyle = "rgba(0,255,255,0.1)";
c.draw();
ctx.fill();
overCircle = c;
}else{
overCircle = null;
}
}
canvas.style.cursor = cursor;
}
<div style="font-family:12px arial;">Click drag to create circle</div>
Note this is written in ES6 and will not run on IE without some type of compiler.
You can see from the comments and code what is happening. Basically you need a global mouse key state variable that lets the function know if there mouse key is depressed. I used the mousemove event to actually kick off the drawing of the circle. Finally, you need the distance between the cursor and center of circle to be less than the radius, otherwise you can drag the circle from outside the circle (which I presume is not what you wanted).
//keep track of previous x and y coordinates
var x0 = 200;
var y0 = 300;
//keep track of mouse key state
var down = false;
var x;
var y;
//draw the initial circle someplace random
var context = document.getElementById("canv").getContext("2d");
context.clearRect(0, 0, 600, 500);
context.arc(200,300, 75, 0, 2*Math.PI);
context.stroke();
document.addEventListener("mousedown", function()
{
//if mousedown event is logged and we are within the area of the circle then state of down is true
if(getDistance(x, x0, y, y0) < 75) down = true;
});
document.addEventListener("mouseup", function()
{
//if mouseup event is logged then down is now false
down = false;
});
document.getElementById("canv").addEventListener("mousemove",
function(event){
x = event.clientX;
y = event.clientY;
//we need to be in "down" state in order for a redraw to be necessary
if(down)
{
//set the previous coordinates to the new coordinates we are drawing.
x0 = x;
y0 = y;
//draw the darn thing
context.beginPath();
context.clearRect(0, 0, 600, 500);
context.arc(x, y, 75, 0, 2*Math.PI);
context.stroke();
}
}
);
function getDistance(x0, x1, y0, y1)
{
return Math.sqrt(Math.pow(x1-x0,2) + Math.pow(y1-y0, 2));
}
<canvas id="canv" name="canv" width="600" height="500" style="border:1px solid #000000;" onclick="">Your browser does not support the HTML5 canvas tag.</canvas>
var offsetx,offsety,posx,posy,run;
window.onmousedown=function(e){
run=true;
offsetx=e.clientX;
offsety=e.clientY;
};
window.onmouseup=function(){
run=false;
};
window.onmousemove=function(e){
if(run){
movedx=e.clientX-offsetx;//mouse moved this value
movedy=e.clientY-offsety;
posx+=movedx;
posy+=movedyy;
offsetx=e.clientX;
offsety=e.clientY
//todo draw at posx,posy
}
};
//todo draw circle+set posx,posy to its position
This is a canvas JavaScript code.
<canvas id="canvas" width="500" height="500" style="border:1px solid"></canvas>
<script>
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var radius = 50;
var nStartX = 0;
var nStartY = 0;
var bIsDrawing = false;
var putPoint = function(e) {
nStartX = e.clientX;
nStartY = e.clientY;
bIsDrawing = true;
radius = 0;
}
var drawPoint = function(e) {
if (!bIsDrawing)
return;
var nDeltaX = nStartX - e.clientX;
var nDeltaY = nStartY - e.clientY;
radius = Math.sqrt(nDeltaX * nDeltaX + nDeltaY * nDeltaY);
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(nStartX, nStartY, radius, 0, Math.PI * 2);
context.fill();
context.fillStyle = 'rgba(0, 0, 0, 0)';
context.lineWidth = 1;
context.stroke();
}
var stopPoint = function(e) {
bIsDrawing = false;
}
canvas.addEventListener('mousedown', putPoint);
canvas.addEventListener('mousemove', drawPoint);
canvas.addEventListener('mouseup', stopPoint);
</script>
I want radius to be shown or filled with color.
This is requirement. Start with a single left mouse click (down action) which will be the center of the circle and move the mouse without lifting the left mouse button to set the radius of the circle. The circle should be drawn as you move the mouse with radius equal to distance between the first point and the mouse position. Release the left mouse button to fix the radius. A marker is added at the point where the mouse button is released to mark that point. (This will be the point on the circumference of that circle)
You need to get the offset to the top left of the canvas and remove that from the mouse coordinates to you can get the coordinates relative to the canvas top left.
How I would handle the mouse.
// get canvas offset
var canvasTopLeft = canvas.getBoundingClientRect();
// create the mouse object
var mouse = {};
mouse.x = 0;
mouse.y = 0;
mouse.down = false;
mouse.buttonChanged = false;
mouse.button = 0;
mouse.origin = {};
mouse.origin.x = canvasTopLeft.left;
mouse.origin.y = canvasTopLeft.top;
// the mouse event handler handles all events
var mouseEvent = function (e) {
mouse.x = e.clientX - mouse.origin.x;
mouse.y = e.clientY - mouse.origin.y;
mouse.buttonChanged = 0;
if (e.type === "mousedown") {
mouse.down = true;
mouse.buttonChanged = e.which;
} else if (e.type === "mouseup") {
mouse.down = false;
mouse.buttonChanged = e.which;
}
update(); // updates the display
}
// add events we want to listen to;
canvas.addEventListener('mousedown', mouseEvent);
canvas.addEventListener('mousemove', mouseEvent);
canvas.addEventListener('mouseup', mouseEvent);
The snippet show how I use it to draw circles with position and radius marked.
<canvas id="canvas" width="500" height="500" style="border:1px solid"></canvas>
<script>
// constants
const TICK_SIZE = 5;
const GIZOM_SIZE = 3;
const CIRCLE_STYLE = "BLACK";
const CIRCLE_LINE_WIDTH = 1;
const FONT = "18px Arial";
const FONT_STYLE = "RED";
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
// array of circles;
var circles = [];
// current circle being drawn
var currentCircle = null;
// true if mouse dragging
var dragging = false;
// draws a circle;
function drawCircle(circle) {
var nx,
ny;
ctx.strokeStyle = CIRCLE_STYLE;
ctx.lineWidth = CIRCLE_LINE_WIDTH;
// draw circle
if (!circle.showGizom) {
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2);
ctx.stroke();
} else {
// calculate the radius
var nx = circle.gx - circle.x;
var ny = circle.gy - circle.y;
circle.r = Math.sqrt(nx * nx + ny * ny);
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2);
// draw center tick
ctx.moveTo(circle.x - TICK_SIZE, circle.y - TICK_SIZE);
ctx.lineTo(circle.x + TICK_SIZE, circle.y + TICK_SIZE);
ctx.moveTo(circle.x + TICK_SIZE, circle.y - TICK_SIZE);
ctx.lineTo(circle.x - TICK_SIZE, circle.y + TICK_SIZE);
// draw radius line
ctx.moveTo(circle.x, circle.y);
ctx.lineTo(circle.gx, circle.gy);
// draw gizom
ctx.moveTo(circle.gx - GIZOM_SIZE, circle.gy - GIZOM_SIZE);
ctx.lineTo(circle.gx + GIZOM_SIZE, circle.gy + GIZOM_SIZE);
ctx.moveTo(circle.gx + GIZOM_SIZE, circle.gy - GIZOM_SIZE);
ctx.lineTo(circle.gx - GIZOM_SIZE, circle.gy + GIZOM_SIZE);
ctx.stroke();
// draw radius
ctx.font = FONT;
ctx.fillStyle = FONT_STYLE;
ctx.textAlign = "center";
// get the transform to draw the text
nx /= circle.r; // normalise the vector from center to gizom
ny /= circle.r;
if(nx < 0){ // is text upside down
// set the transform to be the revers and from the gizom
ctx.setTransform(-nx, -ny, ny, -nx, circle.gx, circle.gy);
} else {
ctx.setTransform(nx, ny, -ny, nx, circle.x, circle.y);
}
ctx.fillText(circle.r.toFixed(1), circle.r / 2, 0);
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore transform
}
}
// draw all circles;
function drawCircles() {
var i = 0,
len = circles.length;
for (; i < len; i++) {
drawCircle(circles[i]);
}
}
// render everything
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawCircles();
}
// create a new circle
function createCircle(x, y) {
return currentCircle = {
x : x,
y : y,
r : 0,
gx : x, // gizom location
gy : y,
showGizom : true,
};
}
// get canvas offset
var canvasTopLeft = canvas.getBoundingClientRect();
// create the mouse object
var mouse = {};
mouse.x = 0;
mouse.y = 0;
mouse.down = false;
mouse.buttonChanged = false;
mouse.button = 0;
mouse.origin = {};
mouse.origin.x = canvasTopLeft.left;
mouse.origin.y = canvasTopLeft.top;
// the mouse event handler
var mouseEvent = function (e) {
mouse.x = e.clientX - mouse.origin.x;
mouse.y = e.clientY - mouse.origin.y;
mouse.buttonChanged = 0;
if (e.type === "mousedown") {
mouse.down = true;
mouse.buttonChanged = e.which;
} else if (e.type === "mouseup") {
mouse.down = false;
mouse.buttonChanged = e.which;
}
update();
}
// add events we want to listen to;
canvas.addEventListener('mousedown', mouseEvent);
canvas.addEventListener('mousemove', mouseEvent);
canvas.addEventListener('mouseup', mouseEvent);
// function to update mouse interaction
function update() {
//if (mouse.down || dragging) { // while the is a need to render
if (mouse.buttonChanged === 1) { // is the left button changed
if (mouse.down) { // is it down
if (currentCircle !== null) { // is there a circle active ??
currentCircle.showGizom = false; // turn off its gizom
}
circles.push(createCircle(mouse.x, mouse.y)); // create a new circle at mouse
dragging = true; // flag that we are dragging
} else {
dragging = false; // stop dragging
}
}
if (dragging) { // while dragging move the circle gizom
currentCircle.gx = mouse.x;
currentCircle.gy = mouse.y;
}
// draw any updates
render();
//}
}
</script>